장비관리 화면을 기준으로 전체 화면 UI 일관성 개선: - 모든 화면 검색바/버튼/드롭다운 높이 40px 통일 - 테이블 헤더 패딩 vertical 10px, 행 패딩 vertical 4px 통일 - 배지 스타일 통일 (border 제거, opacity 0.9 적용) - 페이지네이션 10개 이하에서도 항상 표시 - 테이블 헤더 폰트 스타일 통일 (fontSize: 13, fontWeight: w500) 각 화면별 수정사항: 1. 장비관리: 컬럼 너비 최적화, 검색 컴포넌트 높이 명시 2. 입고지 관리: 페이지네이션 조건 개선 3. 회사관리: UnifiedSearchBar 통합, 배지 스타일 개선 4. 유지보수: ListView.builder → map() 변경, 테이블 구조 재설계 키포인트 색상을 teal로 통일하여 브랜드 일관성 확보
510 lines
14 KiB
Dart
510 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:superport/screens/common/theme_shadcn.dart';
|
|
|
|
/// shadcn/ui 스타일 기본 컴포넌트들
|
|
|
|
// 카드 컴포넌트
|
|
class ShadcnCard extends StatelessWidget {
|
|
final Widget child;
|
|
final EdgeInsetsGeometry? padding;
|
|
final EdgeInsetsGeometry? margin;
|
|
final double? width;
|
|
final double? height;
|
|
final VoidCallback? onTap;
|
|
|
|
const ShadcnCard({
|
|
Key? key,
|
|
required this.child,
|
|
this.padding,
|
|
this.margin,
|
|
this.width,
|
|
this.height,
|
|
this.onTap,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final cardContent = Container(
|
|
width: width,
|
|
height: height,
|
|
padding: padding ?? const EdgeInsets.all(ShadcnTheme.spacing6),
|
|
margin: margin,
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.card,
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
|
border: Border.all(color: Colors.black),
|
|
boxShadow: ShadcnTheme.cardShadow,
|
|
),
|
|
child: child,
|
|
);
|
|
|
|
if (onTap != null) {
|
|
return GestureDetector(onTap: onTap, child: cardContent);
|
|
}
|
|
|
|
return cardContent;
|
|
}
|
|
}
|
|
|
|
// 버튼 컴포넌트
|
|
class ShadcnButton extends StatelessWidget {
|
|
final String text;
|
|
final VoidCallback? onPressed;
|
|
final ShadcnButtonVariant variant;
|
|
final ShadcnButtonSize size;
|
|
final Widget? icon;
|
|
final bool fullWidth;
|
|
final bool loading;
|
|
final Color? backgroundColor;
|
|
final Color? textColor;
|
|
|
|
const ShadcnButton({
|
|
Key? key,
|
|
required this.text,
|
|
this.onPressed,
|
|
this.variant = ShadcnButtonVariant.primary,
|
|
this.size = ShadcnButtonSize.medium,
|
|
this.icon,
|
|
this.fullWidth = false,
|
|
this.loading = false,
|
|
this.backgroundColor,
|
|
this.textColor,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final ButtonStyle style = _getButtonStyle();
|
|
final EdgeInsetsGeometry padding = _getPadding();
|
|
|
|
Widget buttonChild = Row(
|
|
mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (loading)
|
|
SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
textColor ?? _getDefaultTextColor(),
|
|
),
|
|
),
|
|
)
|
|
else if (icon != null)
|
|
icon!,
|
|
if ((loading || icon != null) && text.isNotEmpty)
|
|
const SizedBox(width: ShadcnTheme.spacing2),
|
|
if (text.isNotEmpty) Text(text, style: _getTextStyle()),
|
|
],
|
|
);
|
|
|
|
if (variant == ShadcnButtonVariant.primary) {
|
|
return SizedBox(
|
|
width: fullWidth ? double.infinity : null,
|
|
child: ElevatedButton(
|
|
onPressed: loading ? null : onPressed,
|
|
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
|
child: buttonChild,
|
|
),
|
|
);
|
|
} else if (variant == ShadcnButtonVariant.secondary) {
|
|
return SizedBox(
|
|
width: fullWidth ? double.infinity : null,
|
|
child: OutlinedButton(
|
|
onPressed: loading ? null : onPressed,
|
|
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
|
child: buttonChild,
|
|
),
|
|
);
|
|
} else {
|
|
return SizedBox(
|
|
width: fullWidth ? double.infinity : null,
|
|
child: TextButton(
|
|
onPressed: loading ? null : onPressed,
|
|
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
|
child: buttonChild,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
ButtonStyle _getButtonStyle() {
|
|
switch (variant) {
|
|
case ShadcnButtonVariant.primary:
|
|
return ElevatedButton.styleFrom(
|
|
backgroundColor: backgroundColor ?? ShadcnTheme.primary,
|
|
foregroundColor: textColor ?? ShadcnTheme.primaryForeground,
|
|
elevation: 0,
|
|
shadowColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
),
|
|
);
|
|
case ShadcnButtonVariant.secondary:
|
|
return OutlinedButton.styleFrom(
|
|
backgroundColor: backgroundColor ?? ShadcnTheme.secondary,
|
|
foregroundColor: textColor ?? ShadcnTheme.secondaryForeground,
|
|
side: const BorderSide(color: Colors.black),
|
|
elevation: 0,
|
|
shadowColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
),
|
|
);
|
|
case ShadcnButtonVariant.destructive:
|
|
return ElevatedButton.styleFrom(
|
|
backgroundColor: backgroundColor ?? ShadcnTheme.destructive,
|
|
foregroundColor: textColor ?? ShadcnTheme.destructiveForeground,
|
|
elevation: 0,
|
|
shadowColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
),
|
|
);
|
|
case ShadcnButtonVariant.ghost:
|
|
return TextButton.styleFrom(
|
|
backgroundColor: backgroundColor ?? Colors.transparent,
|
|
foregroundColor: textColor ?? ShadcnTheme.foreground,
|
|
elevation: 0,
|
|
shadowColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
EdgeInsetsGeometry _getPadding() {
|
|
switch (size) {
|
|
case ShadcnButtonSize.small:
|
|
return const EdgeInsets.symmetric(
|
|
horizontal: ShadcnTheme.spacing3,
|
|
vertical: 6,
|
|
);
|
|
case ShadcnButtonSize.medium:
|
|
return const EdgeInsets.symmetric(
|
|
horizontal: ShadcnTheme.spacing4,
|
|
vertical: 10,
|
|
);
|
|
case ShadcnButtonSize.large:
|
|
return const EdgeInsets.symmetric(
|
|
horizontal: ShadcnTheme.spacing8,
|
|
vertical: ShadcnTheme.spacing3,
|
|
);
|
|
}
|
|
}
|
|
|
|
TextStyle _getTextStyle() {
|
|
TextStyle baseStyle;
|
|
switch (size) {
|
|
case ShadcnButtonSize.small:
|
|
baseStyle = ShadcnTheme.labelSmall;
|
|
break;
|
|
case ShadcnButtonSize.medium:
|
|
baseStyle = ShadcnTheme.labelMedium;
|
|
break;
|
|
case ShadcnButtonSize.large:
|
|
baseStyle = ShadcnTheme.labelLarge;
|
|
break;
|
|
}
|
|
return textColor != null ? baseStyle.copyWith(color: textColor) : baseStyle;
|
|
}
|
|
|
|
Color _getDefaultTextColor() {
|
|
switch (variant) {
|
|
case ShadcnButtonVariant.primary:
|
|
return ShadcnTheme.primaryForeground;
|
|
case ShadcnButtonVariant.secondary:
|
|
return ShadcnTheme.secondaryForeground;
|
|
case ShadcnButtonVariant.destructive:
|
|
return ShadcnTheme.destructiveForeground;
|
|
case ShadcnButtonVariant.ghost:
|
|
return ShadcnTheme.foreground;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 버튼 variants
|
|
enum ShadcnButtonVariant { primary, secondary, destructive, ghost }
|
|
|
|
// 버튼 사이즈
|
|
enum ShadcnButtonSize { small, medium, large }
|
|
|
|
// 입력 필드 컴포넌트
|
|
class ShadcnInput extends StatelessWidget {
|
|
final String? label;
|
|
final String? placeholder;
|
|
final String? errorText;
|
|
final TextEditingController? controller;
|
|
final bool obscureText;
|
|
final TextInputType? keyboardType;
|
|
final ValueChanged<String>? onChanged;
|
|
final VoidCallback? onTap;
|
|
final Widget? prefixIcon;
|
|
final Widget? suffixIcon;
|
|
final bool readOnly;
|
|
final bool enabled;
|
|
final int? maxLines;
|
|
|
|
const ShadcnInput({
|
|
Key? key,
|
|
this.label,
|
|
this.placeholder,
|
|
this.errorText,
|
|
this.controller,
|
|
this.obscureText = false,
|
|
this.keyboardType,
|
|
this.onChanged,
|
|
this.onTap,
|
|
this.prefixIcon,
|
|
this.suffixIcon,
|
|
this.readOnly = false,
|
|
this.enabled = true,
|
|
this.maxLines = 1,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (label != null) ...[
|
|
Text(label!, style: ShadcnTheme.labelMedium),
|
|
const SizedBox(height: ShadcnTheme.spacing1),
|
|
],
|
|
TextFormField(
|
|
controller: controller,
|
|
obscureText: obscureText,
|
|
keyboardType: keyboardType,
|
|
onChanged: onChanged,
|
|
onTap: onTap,
|
|
readOnly: readOnly,
|
|
enabled: enabled,
|
|
maxLines: maxLines,
|
|
decoration: InputDecoration(
|
|
hintText: placeholder,
|
|
prefixIcon: prefixIcon,
|
|
suffixIcon: suffixIcon,
|
|
errorText: errorText,
|
|
filled: true,
|
|
fillColor: ShadcnTheme.background,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: ShadcnTheme.spacing3,
|
|
vertical: 10,
|
|
),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
borderSide: const BorderSide(color: ShadcnTheme.input),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
borderSide: const BorderSide(color: ShadcnTheme.input),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
borderSide: const BorderSide(color: ShadcnTheme.ring, width: 2),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
borderSide: const BorderSide(color: ShadcnTheme.destructive),
|
|
),
|
|
focusedErrorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
borderSide: const BorderSide(
|
|
color: ShadcnTheme.destructive,
|
|
width: 2,
|
|
),
|
|
),
|
|
hintStyle: ShadcnTheme.bodyMedium.copyWith(
|
|
color: ShadcnTheme.mutedForeground.withValues(alpha: 0.8),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// 배지 컴포넌트
|
|
class ShadcnBadge extends StatelessWidget {
|
|
final String text;
|
|
final ShadcnBadgeVariant variant;
|
|
final ShadcnBadgeSize size;
|
|
|
|
const ShadcnBadge({
|
|
Key? key,
|
|
required this.text,
|
|
this.variant = ShadcnBadgeVariant.primary,
|
|
this.size = ShadcnBadgeSize.medium,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: _getPadding(),
|
|
decoration: BoxDecoration(
|
|
color: _getBackgroundColor(),
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusXl),
|
|
border: Border.all(color: _getBorderColor()),
|
|
),
|
|
child: Text(text, style: _getTextStyle()),
|
|
);
|
|
}
|
|
|
|
EdgeInsetsGeometry _getPadding() {
|
|
switch (size) {
|
|
case ShadcnBadgeSize.small:
|
|
return const EdgeInsets.symmetric(
|
|
horizontal: ShadcnTheme.spacing1,
|
|
vertical: ShadcnTheme.spacing1 / 2,
|
|
);
|
|
case ShadcnBadgeSize.medium:
|
|
return const EdgeInsets.symmetric(
|
|
horizontal: ShadcnTheme.spacing2,
|
|
vertical: ShadcnTheme.spacing1,
|
|
);
|
|
case ShadcnBadgeSize.large:
|
|
return const EdgeInsets.symmetric(
|
|
horizontal: ShadcnTheme.spacing3,
|
|
vertical: ShadcnTheme.spacing1,
|
|
);
|
|
}
|
|
}
|
|
|
|
Color _getBackgroundColor() {
|
|
switch (variant) {
|
|
case ShadcnBadgeVariant.primary:
|
|
return ShadcnTheme.primary;
|
|
case ShadcnBadgeVariant.secondary:
|
|
return ShadcnTheme.secondary;
|
|
case ShadcnBadgeVariant.destructive:
|
|
return ShadcnTheme.destructive;
|
|
case ShadcnBadgeVariant.success:
|
|
return ShadcnTheme.success;
|
|
case ShadcnBadgeVariant.warning:
|
|
return ShadcnTheme.warning;
|
|
case ShadcnBadgeVariant.outline:
|
|
return Colors.transparent;
|
|
}
|
|
}
|
|
|
|
Color _getBorderColor() {
|
|
switch (variant) {
|
|
case ShadcnBadgeVariant.outline:
|
|
return Colors.black;
|
|
default:
|
|
return Colors.transparent;
|
|
}
|
|
}
|
|
|
|
TextStyle _getTextStyle() {
|
|
final Color textColor =
|
|
variant == ShadcnBadgeVariant.outline
|
|
? ShadcnTheme.foreground
|
|
: variant == ShadcnBadgeVariant.secondary
|
|
? ShadcnTheme.secondaryForeground
|
|
: ShadcnTheme.primaryForeground;
|
|
|
|
switch (size) {
|
|
case ShadcnBadgeSize.small:
|
|
return ShadcnTheme.labelSmall.copyWith(color: textColor);
|
|
case ShadcnBadgeSize.medium:
|
|
return ShadcnTheme.labelMedium.copyWith(color: textColor);
|
|
case ShadcnBadgeSize.large:
|
|
return ShadcnTheme.labelLarge.copyWith(color: textColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 배지 variants
|
|
enum ShadcnBadgeVariant {
|
|
primary,
|
|
secondary,
|
|
destructive,
|
|
success,
|
|
warning,
|
|
outline,
|
|
}
|
|
|
|
// 배지 사이즈
|
|
enum ShadcnBadgeSize { small, medium, large }
|
|
|
|
// 구분선 컴포넌트
|
|
class ShadcnSeparator extends StatelessWidget {
|
|
final Axis direction;
|
|
final double thickness;
|
|
final Color? color;
|
|
|
|
const ShadcnSeparator({
|
|
Key? key,
|
|
this.direction = Axis.horizontal,
|
|
this.thickness = 1.0,
|
|
this.color,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: direction == Axis.horizontal ? double.infinity : thickness,
|
|
height: direction == Axis.vertical ? double.infinity : thickness,
|
|
color: color ?? Colors.black,
|
|
);
|
|
}
|
|
}
|
|
|
|
// 아바타 컴포넌트
|
|
class ShadcnAvatar extends StatelessWidget {
|
|
final String? imageUrl;
|
|
final String? initials;
|
|
final double size;
|
|
final Color? backgroundColor;
|
|
|
|
const ShadcnAvatar({
|
|
Key? key,
|
|
this.imageUrl,
|
|
this.initials,
|
|
this.size = 40,
|
|
this.backgroundColor,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: size,
|
|
height: size,
|
|
decoration: BoxDecoration(
|
|
color: backgroundColor ?? ShadcnTheme.muted,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: Colors.black),
|
|
),
|
|
child: ClipOval(
|
|
child:
|
|
imageUrl != null
|
|
? Image.network(
|
|
imageUrl!,
|
|
fit: BoxFit.cover,
|
|
errorBuilder:
|
|
(context, error, stackTrace) => _buildFallback(),
|
|
)
|
|
: _buildFallback(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFallback() {
|
|
return Container(
|
|
color: backgroundColor ?? ShadcnTheme.muted,
|
|
child: Center(
|
|
child: Text(
|
|
initials ?? '?',
|
|
style: ShadcnTheme.labelMedium.copyWith(
|
|
color: ShadcnTheme.mutedForeground,
|
|
fontSize: size * 0.4,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|