refactor: UI 일관성 개선 및 테이블 구조 통일
장비관리 화면을 기준으로 전체 화면 UI 일관성 개선: - 모든 화면 검색바/버튼/드롭다운 높이 40px 통일 - 테이블 헤더 패딩 vertical 10px, 행 패딩 vertical 4px 통일 - 배지 스타일 통일 (border 제거, opacity 0.9 적용) - 페이지네이션 10개 이하에서도 항상 표시 - 테이블 헤더 폰트 스타일 통일 (fontSize: 13, fontWeight: w500) 각 화면별 수정사항: 1. 장비관리: 컬럼 너비 최적화, 검색 컴포넌트 높이 명시 2. 입고지 관리: 페이지네이션 조건 개선 3. 회사관리: UnifiedSearchBar 통합, 배지 스타일 개선 4. 유지보수: ListView.builder → map() 변경, 테이블 구조 재설계 키포인트 색상을 teal로 통일하여 브랜드 일관성 확보
This commit is contained in:
315
lib/screens/common/widgets/standard_data_table.dart
Normal file
315
lib/screens/common/widgets/standard_data_table.dart
Normal file
@@ -0,0 +1,315 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user