From 22f07ed80534598654947b5908e3e7d74d77e80c Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 8 Jun 2026 07:03:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E5=85=A5=E5=BA=93=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E5=85=B3=E8=81=94=E5=95=86=E5=93=81=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E5=AD=97=E5=85=B8=EF=BC=88=E4=BA=A7=E5=9C=B0/=E4=BF=9D?= =?UTF-8?q?=E8=B4=A8=E6=9C=9F/=E5=82=A8=E5=AD=98=E6=96=B9=E5=BC=8F/?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E6=96=87=E6=A1=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - product_option.dart: 新增 4 个字典 model(Origin/ShelfLife/Storage/DescriptionDoc) - product_option_repository.dart: 新增 4 个 list 方法(listOrigins/ShelfLives/Storages/DescriptionDocs) - product_option_provider.dart: 新增 4 个 FutureProvider 供表单使用 - product_repository.dart: findOrCreate 加 originId/shelfLifeId/storageId/descriptionDocId 可选参数 - stock_in_form_screen.dart: _ItemRow 加 4 个可选 ID 字段;新增 4 个 SearchableOptionField 方法;移动端卡片视图增加 4 个可选字段;提交时传入属性 ID Co-Authored-By: Claude Sonnet 4.6 --- client/lib/models/product_option.dart | 92 +++++++++++++++++++ .../providers/product_option_provider.dart | 36 ++++++++ .../product_option_repository.dart | 52 +++++++++++ .../lib/repositories/product_repository.dart | 13 ++- .../stock_in/stock_in_form_screen.dart | 91 +++++++++++++++++- 5 files changed, 281 insertions(+), 3 deletions(-) diff --git a/client/lib/models/product_option.dart b/client/lib/models/product_option.dart index 43a88ca..8c18159 100644 --- a/client/lib/models/product_option.dart +++ b/client/lib/models/product_option.dart @@ -66,3 +66,95 @@ class ProductSpecOption { remark: json['remark'] as String?, ); } + +// ── 商品属性字典(产地/保质期/储存方式) ───────────────────────── + +class ProductOriginOption { + final int id; + final String? code; + final String name; + final String? remark; + + const ProductOriginOption({ + required this.id, + this.code, + required this.name, + this.remark, + }); + + factory ProductOriginOption.fromJson(Map json) => + ProductOriginOption( + id: (json['id'] as num).toInt(), + code: json['code'] as String?, + name: json['name'] as String, + remark: json['remark'] as String?, + ); +} + +class ProductShelfLifeOption { + final int id; + final String? code; + final String name; + final String? remark; + + const ProductShelfLifeOption({ + required this.id, + this.code, + required this.name, + this.remark, + }); + + factory ProductShelfLifeOption.fromJson(Map json) => + ProductShelfLifeOption( + id: (json['id'] as num).toInt(), + code: json['code'] as String?, + name: json['name'] as String, + remark: json['remark'] as String?, + ); +} + +class ProductStorageOption { + final int id; + final String? code; + final String name; + final String? remark; + + const ProductStorageOption({ + required this.id, + this.code, + required this.name, + this.remark, + }); + + factory ProductStorageOption.fromJson(Map json) => + ProductStorageOption( + id: (json['id'] as num).toInt(), + code: json['code'] as String?, + name: json['name'] as String, + remark: json['remark'] as String?, + ); +} + +// ── 描述文档 ───────────────────────────────────────────────────── + +class ProductDescriptionDoc { + final int id; + final String title; + final String? content; + final String? remark; + + const ProductDescriptionDoc({ + required this.id, + required this.title, + this.content, + this.remark, + }); + + factory ProductDescriptionDoc.fromJson(Map json) => + ProductDescriptionDoc( + id: (json['id'] as num).toInt(), + title: json['title'] as String, + content: json['content'] as String?, + remark: json['remark'] as String?, + ); +} diff --git a/client/lib/providers/product_option_provider.dart b/client/lib/providers/product_option_provider.dart index 323c14f..5e2af53 100644 --- a/client/lib/providers/product_option_provider.dart +++ b/client/lib/providers/product_option_provider.dart @@ -125,3 +125,39 @@ class ProductSpecListNotifier extends AsyncNotifier> { reload(); } } + +// ── 产地 ────────────────────────────────────────────────────── + +final productOriginListProvider = + FutureProvider>((ref) async { + ref.watch(authStateProvider.select((s) => s.user?.shopId)); + ref.watch(networkRecoveryCountProvider); + return ref.read(productOptionRepositoryProvider).listOrigins(); +}); + +// ── 保质期 ──────────────────────────────────────────────────── + +final productShelfLifeListProvider = + FutureProvider>((ref) async { + ref.watch(authStateProvider.select((s) => s.user?.shopId)); + ref.watch(networkRecoveryCountProvider); + return ref.read(productOptionRepositoryProvider).listShelfLives(); +}); + +// ── 储存方式 ────────────────────────────────────────────────── + +final productStorageListProvider = + FutureProvider>((ref) async { + ref.watch(authStateProvider.select((s) => s.user?.shopId)); + ref.watch(networkRecoveryCountProvider); + return ref.read(productOptionRepositoryProvider).listStorages(); +}); + +// ── 描述文档 ────────────────────────────────────────────────── + +final productDescriptionDocListProvider = + FutureProvider>((ref) async { + ref.watch(authStateProvider.select((s) => s.user?.shopId)); + ref.watch(networkRecoveryCountProvider); + return ref.read(productOptionRepositoryProvider).listDescriptionDocs(); +}); diff --git a/client/lib/repositories/product_option_repository.dart b/client/lib/repositories/product_option_repository.dart index 1c4f177..6a9134d 100644 --- a/client/lib/repositories/product_option_repository.dart +++ b/client/lib/repositories/product_option_repository.dart @@ -127,4 +127,56 @@ class ProductOptionRepository { statusCode: e.response?.statusCode); } } + + // ── 产地 ──────────────────────────────────────────────────── + + Future> listOrigins() async { + try { + final resp = await _client.get('/product-options/origins'); + final data = (resp.data as Map)['data'] as List? ?? []; + return data.map((e) => ProductOriginOption.fromJson(e as Map)).toList(); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '获取产地列表失败', + statusCode: e.response?.statusCode); + } + } + + // ── 保质期 ────────────────────────────────────────────────── + + Future> listShelfLives() async { + try { + final resp = await _client.get('/product-options/shelf-lives'); + final data = (resp.data as Map)['data'] as List? ?? []; + return data.map((e) => ProductShelfLifeOption.fromJson(e as Map)).toList(); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '获取保质期列表失败', + statusCode: e.response?.statusCode); + } + } + + // ── 储存方式 ───────────────────────────────────────────────── + + Future> listStorages() async { + try { + final resp = await _client.get('/product-options/storages'); + final data = (resp.data as Map)['data'] as List? ?? []; + return data.map((e) => ProductStorageOption.fromJson(e as Map)).toList(); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '获取储存方式列表失败', + statusCode: e.response?.statusCode); + } + } + + // ── 描述文档 ───────────────────────────────────────────────── + + Future> listDescriptionDocs() async { + try { + final resp = await _client.get('/product-options/description-docs'); + final data = (resp.data as Map)['data'] as List? ?? []; + return data.map((e) => ProductDescriptionDoc.fromJson(e as Map)).toList(); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '获取描述文档列表失败', + statusCode: e.response?.statusCode); + } + } } diff --git a/client/lib/repositories/product_repository.dart b/client/lib/repositories/product_repository.dart index 353223b..8cde304 100644 --- a/client/lib/repositories/product_repository.dart +++ b/client/lib/repositories/product_repository.dart @@ -135,13 +135,22 @@ class ProductRepository { required String name, String series = '', String spec = '', + int? originId, + int? shelfLifeId, + int? storageId, + int? descriptionDocId, }) async { try { - final resp = await _client.post('/products/find-or-create', data: { + final body = { 'name': name, 'series': series, 'spec': spec, - }); + }; + if (originId != null) body['origin_id'] = originId; + if (shelfLifeId != null) body['shelf_life_id'] = shelfLifeId; + if (storageId != null) body['storage_id'] = storageId; + if (descriptionDocId != null) body['description_doc_id'] = descriptionDocId; + final resp = await _client.post('/products/find-or-create', data: body); return Product.fromJson( (resp.data as Map)['data'] as Map); } on DioException catch (e) { diff --git a/client/lib/screens/stock_in/stock_in_form_screen.dart b/client/lib/screens/stock_in/stock_in_form_screen.dart index 6f3ce59..adcdd0d 100644 --- a/client/lib/screens/stock_in/stock_in_form_screen.dart +++ b/client/lib/screens/stock_in/stock_in_form_screen.dart @@ -226,7 +226,15 @@ class _StockInFormScreenState extends ConsumerState { try { final product = await ref .read(productRepositoryProvider) - .findOrCreate(name: name, series: series, spec: spec); + .findOrCreate( + name: name, + series: series, + spec: spec, + originId: item.selectedOriginId, + shelfLifeId: item.selectedShelfLifeId, + storageId: item.selectedStorageId, + descriptionDocId: item.selectedDescriptionDocId, + ); item.productId = product.id; } catch (e) { if (mounted) { @@ -761,6 +769,78 @@ class _StockInFormScreenState extends ConsumerState { ); } + Widget _originField(_ItemRow item) { + final asyncOrigins = ref.watch(productOriginListProvider); + return asyncOrigins.when( + loading: () => const LinearProgressIndicator(), + error: (_, __) => const Text('加载失败'), + data: (origins) => SearchableOptionField( + options: origins + .map((o) => OptionItem(id: o.id, name: o.name, code: o.code)) + .toList(), + selectedId: item.selectedOriginId, + hint: '选择产地(可选)', + dialogTitle: '选择产地', + isRequired: false, + onChanged: (v) => setState(() => item.selectedOriginId = v), + ), + ); + } + + Widget _shelfLifeField(_ItemRow item) { + final asyncShelfLives = ref.watch(productShelfLifeListProvider); + return asyncShelfLives.when( + loading: () => const LinearProgressIndicator(), + error: (_, __) => const Text('加载失败'), + data: (shelfLives) => SearchableOptionField( + options: shelfLives + .map((o) => OptionItem(id: o.id, name: o.name, code: o.code)) + .toList(), + selectedId: item.selectedShelfLifeId, + hint: '选择保质期(可选)', + dialogTitle: '选择保质期', + isRequired: false, + onChanged: (v) => setState(() => item.selectedShelfLifeId = v), + ), + ); + } + + Widget _storageField(_ItemRow item) { + final asyncStorages = ref.watch(productStorageListProvider); + return asyncStorages.when( + loading: () => const LinearProgressIndicator(), + error: (_, __) => const Text('加载失败'), + data: (storages) => SearchableOptionField( + options: storages + .map((o) => OptionItem(id: o.id, name: o.name, code: o.code)) + .toList(), + selectedId: item.selectedStorageId, + hint: '选择储存方式(可选)', + dialogTitle: '选择储存方式', + isRequired: false, + onChanged: (v) => setState(() => item.selectedStorageId = v), + ), + ); + } + + Widget _descriptionDocField(_ItemRow item) { + final asyncDocs = ref.watch(productDescriptionDocListProvider); + return asyncDocs.when( + loading: () => const LinearProgressIndicator(), + error: (_, __) => const Text('加载失败'), + data: (docs) => SearchableOptionField( + options: docs + .map((o) => OptionItem(id: o.id, name: o.title)) + .toList(), + selectedId: item.selectedDescriptionDocId, + hint: '选择介绍文档(可选)', + dialogTitle: '选择介绍文档', + isRequired: false, + onChanged: (v) => setState(() => item.selectedDescriptionDocId = v), + ), + ); + } + int _specQtyOf(_ItemRow item) => ref.read(productSpecListProvider).valueOrNull ?.where((o) => o.id == item.selectedSpecId) @@ -864,6 +944,10 @@ class _StockInFormScreenState extends ConsumerState { MobileCardField('金额', '¥${amount.toStringAsFixed(2)}'), MobileCardField('批次号', null, valueWidget: _batchField(item)), MobileCardField('生产日期', null, valueWidget: _dateField(item)), + MobileCardField('产地', null, valueWidget: _originField(item)), + MobileCardField('保质期', null, valueWidget: _shelfLifeField(item)), + MobileCardField('储存方式', null, valueWidget: _storageField(item)), + MobileCardField('介绍文档', null, valueWidget: _descriptionDocField(item)), ], ); } @@ -908,6 +992,11 @@ class _ItemRow { int? selectedNameId; int? selectedSeriesId; int? selectedSpecId; + // 商品属性字典(可选,入库时关联到商品) + int? selectedOriginId; + int? selectedShelfLifeId; + int? selectedStorageId; + int? selectedDescriptionDocId; final TextEditingController qtyCtrl = TextEditingController(text: '1'); final TextEditingController priceCtrl = TextEditingController(); final TextEditingController batchNoCtrl = TextEditingController();