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();