From 38e2ea1c6cb9d69f1670a0bd576c2d6da8da6ea2 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Thu, 25 Jun 2026 15:42:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E5=95=86=E5=93=81=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E6=95=B0=E6=8D=AE=E5=8A=A0=E9=A6=99=E5=9E=8B=E5=AD=97?= =?UTF-8?q?=E5=85=B8=20tab=EF=BC=88=E6=8E=A5=E5=90=8E=E7=AB=AF=20categorie?= =?UTF-8?q?s=20CRUD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 ProductCategoryOption 模型 + repo(list/create/update/deleteCategory)+ provider(productCategoryListProvider),基础数据页加「香型」tab(增删改查、 搜索、导出,仅名称字段)。接通后端 /product-options/categories。 (原型 5-tab 整合是更大 IA 任务,本次先把香型管理补齐可用。) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX --- client/lib/models/product_option.dart | 14 +++ .../providers/product_option_provider.dart | 39 ++++++ .../product_option_repository.dart | 42 +++++++ .../lib/screens/products/products_screen.dart | 115 ++++++++++++++++++ 4 files changed, 210 insertions(+) diff --git a/client/lib/models/product_option.dart b/client/lib/models/product_option.dart index 8c18159..16c190c 100644 --- a/client/lib/models/product_option.dart +++ b/client/lib/models/product_option.dart @@ -42,6 +42,20 @@ class ProductSeriesOption { ); } +/// 香型 / 分类字典项(对应后端 ProductCategory)。 +class ProductCategoryOption { + final int id; + final String name; + + const ProductCategoryOption({required this.id, required this.name}); + + factory ProductCategoryOption.fromJson(Map json) => + ProductCategoryOption( + id: (json['id'] as num).toInt(), + name: json['name'] as String, + ); +} + class ProductSpecOption { final int id; final String? code; diff --git a/client/lib/providers/product_option_provider.dart b/client/lib/providers/product_option_provider.dart index 9f2d531..63caa44 100644 --- a/client/lib/providers/product_option_provider.dart +++ b/client/lib/providers/product_option_provider.dart @@ -281,3 +281,42 @@ class ProductDescriptionDocListNotifier extends AsyncNotifier>( + ProductCategoryListNotifier.new, +); + +class ProductCategoryListNotifier + extends AsyncNotifier> { + @override + Future> build() async { + ref.watch(authStateProvider.select((s) => s.user?.shopId)); + ref.watch(networkRecoveryCountProvider); + return ref.read(productOptionRepositoryProvider).listCategories(); + } + + void reload() { + state = const AsyncValue.loading(); + ref.read(productOptionRepositoryProvider).listCategories().then( + (data) => state = AsyncValue.data(data), + onError: (e, st) => state = AsyncValue.error(e, st), + ); + } + + Future create(Map data) async { + await ref.read(productOptionRepositoryProvider).createCategory(data); + reload(); + } + + Future updateItem(int id, Map data) async { + await ref.read(productOptionRepositoryProvider).updateCategory(id, data); + reload(); + } + + Future delete(int id) async { + await ref.read(productOptionRepositoryProvider).deleteCategory(id); + reload(); + } +} diff --git a/client/lib/repositories/product_option_repository.dart b/client/lib/repositories/product_option_repository.dart index 3349931..3de9de6 100644 --- a/client/lib/repositories/product_option_repository.dart +++ b/client/lib/repositories/product_option_repository.dart @@ -88,6 +88,48 @@ class ProductOptionRepository { } } + // ── 香型 / 分类 ────────────────────────────────────────────── + + Future> listCategories() async { + try { + final resp = await _client.get('/product-options/categories'); + final data = (resp.data as Map)['data'] as List? ?? []; + return data + .map((e) => ProductCategoryOption.fromJson(e as Map)) + .toList(); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '获取香型列表失败', + statusCode: e.response?.statusCode); + } + } + + Future createCategory(Map data) async { + try { + await _client.post('/product-options/categories', data: data); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '创建失败', + statusCode: e.response?.statusCode); + } + } + + Future updateCategory(int id, Map data) async { + try { + await _client.put('/product-options/categories/$id', data: data); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '更新失败', + statusCode: e.response?.statusCode); + } + } + + Future deleteCategory(int id) async { + try { + await _client.delete('/product-options/categories/$id'); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '删除失败', + statusCode: e.response?.statusCode); + } + } + // ── 规格 ──────────────────────────────────────────────────── Future> listSpecs() async { diff --git a/client/lib/screens/products/products_screen.dart b/client/lib/screens/products/products_screen.dart index 3d6d655..f19280e 100644 --- a/client/lib/screens/products/products_screen.dart +++ b/client/lib/screens/products/products_screen.dart @@ -5,6 +5,7 @@ import '../../core/responsive/responsive.dart'; import '../../core/theme/context_tokens.dart'; import '../../models/warehouse.dart'; import '../../providers/product_option_provider.dart'; +import '../../models/product_option.dart' show ProductCategoryOption; import '../../providers/warehouse_provider.dart'; import '../../providers/shop_provider.dart'; import '../../widgets/data_table_card.dart'; @@ -31,6 +32,7 @@ class _ProductsScreenState extends ConsumerState { final _shelfLifeSearchCtrl = TextEditingController(); final _storageSearchCtrl = TextEditingController(); final _descDocSearchCtrl = TextEditingController(); + final _categorySearchCtrl = TextEditingController(); int _namePage = 1; int _namePageSize = 20; @@ -46,6 +48,8 @@ class _ProductsScreenState extends ConsumerState { int _storagePageSize = 20; int _descDocPage = 1; int _descDocPageSize = 20; + int _categoryPage = 1; + int _categoryPageSize = 20; @override void dispose() { @@ -56,6 +60,7 @@ class _ProductsScreenState extends ConsumerState { _shelfLifeSearchCtrl.dispose(); _storageSearchCtrl.dispose(); _descDocSearchCtrl.dispose(); + _categorySearchCtrl.dispose(); super.dispose(); } @@ -67,6 +72,7 @@ class _ProductsScreenState extends ConsumerState { Tab(text: '商品名称'), Tab(text: '系列'), Tab(text: '规格'), + Tab(text: '香型'), Tab(text: '产地'), Tab(text: '保质期'), Tab(text: '储存方式'), @@ -77,6 +83,7 @@ class _ProductsScreenState extends ConsumerState { _buildNameTab(), _buildSeriesTab(), _buildSpecTab(), + _buildCategoryTab(), _buildOriginTab(), _buildShelfLifeTab(), _buildStorageTab(), @@ -356,6 +363,114 @@ class _ProductsScreenState extends ConsumerState { ); } + // ── 香型 tab(ProductCategory,仅名称) ────────────────────── + Widget _buildCategoryTab() { + final async = ref.watch(productCategoryListProvider); + return async.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => _buildError( + () => ref.read(productCategoryListProvider.notifier).reload()), + data: (items) { + final keyword = _categorySearchCtrl.text.toLowerCase(); + final filtered = keyword.isEmpty + ? items + : items + .where((it) => it.name.toLowerCase().contains(keyword)) + .toList(); + final paged = filtered + .skip((_categoryPage - 1) * _categoryPageSize) + .take(_categoryPageSize) + .toList(); + final notifier = ref.read(productCategoryListProvider.notifier); + + void edit(ProductCategoryOption it) => _showOptionDialog( + title: '编辑香型', + hasQuantity: false, + initial: {'name': it.name}, + onSave: (data) => + notifier.updateItem(it.id, {'name': data['name']}), + ); + List rowActions(ProductCategoryOption it) => + WriteGuard.isReadonly(ref) + ? const [] + : [ + WriteGuard( + child: TextButton( + onPressed: () => edit(it), + child: const Text('编辑', + style: TextStyle(fontSize: 12)), + ), + ), + WriteGuard( + child: TextButton( + onPressed: () => _confirmDelete( + '删除香型「${it.name}」?', + () => notifier.delete(it.id), + ), + child: Text('删除', + style: TextStyle( + fontSize: 12, color: context.tokens.danger)), + ), + ), + ]; + + return DataTableCard( + totalCount: filtered.length, + page: _categoryPage, + pageSize: _categoryPageSize, + onPageChanged: (p) => setState(() => _categoryPage = p), + onPageSizeChanged: (s) => setState(() { + _categoryPageSize = s; + _categoryPage = 1; + }), + toolbar: _buildToolbar( + searchCtrl: _categorySearchCtrl, + hint: '搜索香型', + onSearchChanged: () => setState(() => _categoryPage = 1), + onAdd: () => _showOptionDialog( + title: '新建香型', + hasQuantity: false, + onSave: (data) => notifier.create({'name': data['name']}), + ), + onExport: () => exportExcel( + filename: '商品香型', + headers: ['香型名称'], + rows: items.map((it) => [it.name]).toList(), + ), + ), + mobileCards: paged + .map((it) => MobileListCard( + title: Text(it.name), + actions: rowActions(it).isEmpty ? null : rowActions(it), + )) + .toList(), + columns: const [ + DataColumn(label: Text('香型名称')), + DataColumn(label: Text('操作')), + ], + rows: paged.isEmpty + ? [ + DataRow(cells: [ + DataCell(Text('暂无数据', + style: TextStyle(color: context.tokens.muted))), + const DataCell(SizedBox()), + ]) + ] + : paged + .map((it) => DataRow(cells: [ + DataCell(Text(it.name, + style: + const TextStyle(fontWeight: FontWeight.w500))), + DataCell(Row( + mainAxisSize: MainAxisSize.min, + children: rowActions(it))), + ])) + .toList(), + ); + }, + ); + } + // ── 规格 tab ────────────────────────────────────────────── Widget _buildSpecTab() {