장비관리 화면을 기준으로 전체 화면 UI 일관성 개선: - 모든 화면 검색바/버튼/드롭다운 높이 40px 통일 - 테이블 헤더 패딩 vertical 10px, 행 패딩 vertical 4px 통일 - 배지 스타일 통일 (border 제거, opacity 0.9 적용) - 페이지네이션 10개 이하에서도 항상 표시 - 테이블 헤더 폰트 스타일 통일 (fontSize: 13, fontWeight: w500) 각 화면별 수정사항: 1. 장비관리: 컬럼 너비 최적화, 검색 컴포넌트 높이 명시 2. 입고지 관리: 페이지네이션 조건 개선 3. 회사관리: UnifiedSearchBar 통합, 배지 스타일 개선 4. 유지보수: ListView.builder → map() 변경, 테이블 구조 재설계 키포인트 색상을 teal로 통일하여 브랜드 일관성 확보
315 lines
8.1 KiB
Dart
315 lines
8.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:superport/screens/common/theme_shadcn.dart';
|
|
|
|
/// 표준 데이터 테이블 컬럼 정의
|
|
class DataColumn {
|
|
final String label;
|
|
final double? width;
|
|
final int? flex;
|
|
final bool isNumeric;
|
|
final TextAlign textAlign;
|
|
|
|
DataColumn({
|
|
required this.label,
|
|
this.width,
|
|
this.flex,
|
|
this.isNumeric = false,
|
|
TextAlign? textAlign,
|
|
}) : textAlign = textAlign ?? (isNumeric ? TextAlign.right : TextAlign.left);
|
|
}
|
|
|
|
/// 표준 데이터 테이블 위젯
|
|
///
|
|
/// 모든 리스트 화면에서 일관된 테이블 스타일 제공
|
|
class StandardDataTable extends StatelessWidget {
|
|
final List<DataColumn> columns;
|
|
final List<Widget> rows;
|
|
final bool showCheckbox;
|
|
final bool? isAllSelected;
|
|
final ValueChanged<bool?>? onSelectAll;
|
|
final bool enableHorizontalScroll;
|
|
final ScrollController? horizontalScrollController;
|
|
final Widget? emptyWidget;
|
|
final bool applyZebraStripes; // 짝수 행 배경색 적용 여부
|
|
|
|
const StandardDataTable({
|
|
Key? key,
|
|
required this.columns,
|
|
required this.rows,
|
|
this.showCheckbox = false,
|
|
this.isAllSelected,
|
|
this.onSelectAll,
|
|
this.enableHorizontalScroll = false,
|
|
this.horizontalScrollController,
|
|
this.emptyWidget,
|
|
this.applyZebraStripes = true,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (rows.isEmpty) {
|
|
return _buildEmptyState();
|
|
}
|
|
|
|
final table = Container(
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.card,
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
|
border: Border.all(color: Colors.black),
|
|
boxShadow: ShadcnTheme.cardShadow,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 테이블 헤더
|
|
_buildHeader(),
|
|
// 테이블 데이터 행들
|
|
...rows,
|
|
],
|
|
),
|
|
);
|
|
|
|
if (enableHorizontalScroll) {
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
controller: horizontalScrollController,
|
|
child: table,
|
|
);
|
|
}
|
|
|
|
return table;
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: ShadcnTheme.spacing4,
|
|
vertical: 10,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
|
border: Border(
|
|
bottom: BorderSide(color: Colors.black),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// 체크박스 컬럼
|
|
if (showCheckbox)
|
|
SizedBox(
|
|
width: 40,
|
|
child: Checkbox(
|
|
value: isAllSelected,
|
|
onChanged: onSelectAll,
|
|
tristate: false,
|
|
),
|
|
),
|
|
|
|
// 데이터 컬럼들
|
|
...columns.map((column) {
|
|
Widget child = Text(
|
|
column.label,
|
|
style: ShadcnTheme.bodyMedium.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
textAlign: column.textAlign,
|
|
);
|
|
|
|
if (column.width != null) {
|
|
return SizedBox(
|
|
width: column.width,
|
|
child: child,
|
|
);
|
|
} else if (column.flex != null) {
|
|
return Expanded(
|
|
flex: column.flex!,
|
|
child: child,
|
|
);
|
|
} else {
|
|
return Expanded(child: child);
|
|
}
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(ShadcnTheme.spacing8),
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.card,
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
|
border: Border.all(color: Colors.black),
|
|
boxShadow: ShadcnTheme.cardShadow,
|
|
),
|
|
child: emptyWidget ??
|
|
Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.inbox_outlined,
|
|
size: 48,
|
|
color: ShadcnTheme.muted,
|
|
),
|
|
const SizedBox(height: ShadcnTheme.spacing4),
|
|
Text(
|
|
'데이터가 없습니다',
|
|
style: ShadcnTheme.bodyMuted,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 표준 데이터 행 위젯
|
|
class StandardDataRow extends StatelessWidget {
|
|
final int index;
|
|
final List<Widget> cells;
|
|
final bool showCheckbox;
|
|
final bool? isSelected;
|
|
final ValueChanged<bool?>? onSelect;
|
|
final bool applyZebraStripes;
|
|
final List<DataColumn> columns;
|
|
|
|
const StandardDataRow({
|
|
Key? key,
|
|
required this.index,
|
|
required this.cells,
|
|
this.showCheckbox = false,
|
|
this.isSelected,
|
|
this.onSelect,
|
|
this.applyZebraStripes = true,
|
|
required this.columns,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: ShadcnTheme.spacing4,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: applyZebraStripes && index % 2 == 1
|
|
? ShadcnTheme.muted.withValues(alpha: 0.1)
|
|
: null,
|
|
border: Border(
|
|
bottom: BorderSide(color: Colors.black),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// 체크박스
|
|
if (showCheckbox)
|
|
SizedBox(
|
|
width: 40,
|
|
child: Checkbox(
|
|
value: isSelected,
|
|
onChanged: onSelect,
|
|
tristate: false,
|
|
),
|
|
),
|
|
|
|
// 데이터 셀들
|
|
...cells.asMap().entries.map((entry) {
|
|
final cellIndex = entry.key;
|
|
final cell = entry.value;
|
|
|
|
if (cellIndex >= columns.length) return const SizedBox.shrink();
|
|
|
|
final column = columns[cellIndex];
|
|
|
|
if (column.width != null) {
|
|
return SizedBox(
|
|
width: column.width,
|
|
child: cell,
|
|
);
|
|
} else if (column.flex != null) {
|
|
return Expanded(
|
|
flex: column.flex!,
|
|
child: cell,
|
|
);
|
|
} else {
|
|
return Expanded(child: cell);
|
|
}
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 표준 관리 버튼 세트
|
|
class StandardActionButtons extends StatelessWidget {
|
|
final VoidCallback? onView;
|
|
final VoidCallback? onEdit;
|
|
final VoidCallback? onDelete;
|
|
final List<Widget>? customButtons;
|
|
final double buttonSize;
|
|
|
|
const StandardActionButtons({
|
|
Key? key,
|
|
this.onView,
|
|
this.onEdit,
|
|
this.onDelete,
|
|
this.customButtons,
|
|
this.buttonSize = 32,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (onView != null)
|
|
_buildIconButton(
|
|
Icons.visibility_outlined,
|
|
onView!,
|
|
'보기',
|
|
ShadcnTheme.primary,
|
|
),
|
|
if (onEdit != null)
|
|
_buildIconButton(
|
|
Icons.edit_outlined,
|
|
onEdit!,
|
|
'수정',
|
|
ShadcnTheme.primary,
|
|
),
|
|
if (onDelete != null)
|
|
_buildIconButton(
|
|
Icons.delete_outline,
|
|
onDelete!,
|
|
'삭제',
|
|
ShadcnTheme.destructive,
|
|
),
|
|
if (customButtons != null) ...customButtons!,
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildIconButton(
|
|
IconData icon,
|
|
VoidCallback onPressed,
|
|
String tooltip,
|
|
Color color,
|
|
) {
|
|
return IconButton(
|
|
constraints: BoxConstraints(
|
|
minWidth: buttonSize,
|
|
minHeight: buttonSize,
|
|
maxWidth: buttonSize,
|
|
maxHeight: buttonSize,
|
|
),
|
|
padding: const EdgeInsets.all(4),
|
|
icon: Icon(icon, size: 16, color: color),
|
|
onPressed: onPressed,
|
|
tooltip: tooltip,
|
|
);
|
|
}
|
|
} |