From 8a87d4a82c7dfd9428872f625c2629dacaae9139 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 8 Jun 2026 08:20:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20#14/#18=20=E4=BA=A7=E5=9C=B0/?= =?UTF-8?q?=E4=BF=9D=E8=B4=A8=E6=9C=9F/=E5=82=A8=E5=AD=98=E6=96=B9?= =?UTF-8?q?=E5=BC=8F/=E6=8F=8F=E8=BF=B0=E6=96=87=E6=A1=A3=20provider=20&?= =?UTF-8?q?=20repo=20=E5=8F=8A=E5=85=AC=E5=BC=80=E9=A1=B5=E6=84=8F?= =?UTF-8?q?=E8=A7=81=E5=8F=8D=E9=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../providers/product_option_provider.dart | 160 +++++++++++++++--- .../product_option_repository.dart | 108 ++++++++++++ .../screens/public/public_product_screen.dart | 154 +++++++++++++++-- todo/todo.html | 60 +++---- todo/todo.json | 14 +- 5 files changed, 429 insertions(+), 67 deletions(-) diff --git a/client/lib/providers/product_option_provider.dart b/client/lib/providers/product_option_provider.dart index 5e2af53..9f2d531 100644 --- a/client/lib/providers/product_option_provider.dart +++ b/client/lib/providers/product_option_provider.dart @@ -129,35 +129,155 @@ class ProductSpecListNotifier extends AsyncNotifier> { // ── 产地 ────────────────────────────────────────────────────── final productOriginListProvider = - FutureProvider>((ref) async { - ref.watch(authStateProvider.select((s) => s.user?.shopId)); - ref.watch(networkRecoveryCountProvider); - return ref.read(productOptionRepositoryProvider).listOrigins(); -}); + AsyncNotifierProvider>( + ProductOriginListNotifier.new, +); + +class ProductOriginListNotifier extends AsyncNotifier> { + @override + Future> build() async { + ref.watch(authStateProvider.select((s) => s.user?.shopId)); + ref.watch(networkRecoveryCountProvider); + return ref.read(productOptionRepositoryProvider).listOrigins(); + } + + void reload() { + state = const AsyncValue.loading(); + ref.read(productOptionRepositoryProvider).listOrigins().then( + (data) => state = AsyncValue.data(data), + onError: (e, st) => state = AsyncValue.error(e, st), + ); + } + + Future create(Map data) async { + await ref.read(productOptionRepositoryProvider).createOrigin(data); + reload(); + } + + Future updateItem(int id, Map data) async { + await ref.read(productOptionRepositoryProvider).updateOrigin(id, data); + reload(); + } + + Future delete(int id) async { + await ref.read(productOptionRepositoryProvider).deleteOrigin(id); + reload(); + } +} // ── 保质期 ──────────────────────────────────────────────────── final productShelfLifeListProvider = - FutureProvider>((ref) async { - ref.watch(authStateProvider.select((s) => s.user?.shopId)); - ref.watch(networkRecoveryCountProvider); - return ref.read(productOptionRepositoryProvider).listShelfLives(); -}); + AsyncNotifierProvider>( + ProductShelfLifeListNotifier.new, +); + +class ProductShelfLifeListNotifier extends AsyncNotifier> { + @override + Future> build() async { + ref.watch(authStateProvider.select((s) => s.user?.shopId)); + ref.watch(networkRecoveryCountProvider); + return ref.read(productOptionRepositoryProvider).listShelfLives(); + } + + void reload() { + state = const AsyncValue.loading(); + ref.read(productOptionRepositoryProvider).listShelfLives().then( + (data) => state = AsyncValue.data(data), + onError: (e, st) => state = AsyncValue.error(e, st), + ); + } + + Future create(Map data) async { + await ref.read(productOptionRepositoryProvider).createShelfLife(data); + reload(); + } + + Future updateItem(int id, Map data) async { + await ref.read(productOptionRepositoryProvider).updateShelfLife(id, data); + reload(); + } + + Future delete(int id) async { + await ref.read(productOptionRepositoryProvider).deleteShelfLife(id); + reload(); + } +} // ── 储存方式 ────────────────────────────────────────────────── final productStorageListProvider = - FutureProvider>((ref) async { - ref.watch(authStateProvider.select((s) => s.user?.shopId)); - ref.watch(networkRecoveryCountProvider); - return ref.read(productOptionRepositoryProvider).listStorages(); -}); + AsyncNotifierProvider>( + ProductStorageListNotifier.new, +); + +class ProductStorageListNotifier extends AsyncNotifier> { + @override + Future> build() async { + ref.watch(authStateProvider.select((s) => s.user?.shopId)); + ref.watch(networkRecoveryCountProvider); + return ref.read(productOptionRepositoryProvider).listStorages(); + } + + void reload() { + state = const AsyncValue.loading(); + ref.read(productOptionRepositoryProvider).listStorages().then( + (data) => state = AsyncValue.data(data), + onError: (e, st) => state = AsyncValue.error(e, st), + ); + } + + Future create(Map data) async { + await ref.read(productOptionRepositoryProvider).createStorage(data); + reload(); + } + + Future updateItem(int id, Map data) async { + await ref.read(productOptionRepositoryProvider).updateStorage(id, data); + reload(); + } + + Future delete(int id) async { + await ref.read(productOptionRepositoryProvider).deleteStorage(id); + reload(); + } +} // ── 描述文档 ────────────────────────────────────────────────── final productDescriptionDocListProvider = - FutureProvider>((ref) async { - ref.watch(authStateProvider.select((s) => s.user?.shopId)); - ref.watch(networkRecoveryCountProvider); - return ref.read(productOptionRepositoryProvider).listDescriptionDocs(); -}); + AsyncNotifierProvider>( + ProductDescriptionDocListNotifier.new, +); + +class ProductDescriptionDocListNotifier extends AsyncNotifier> { + @override + Future> build() async { + ref.watch(authStateProvider.select((s) => s.user?.shopId)); + ref.watch(networkRecoveryCountProvider); + return ref.read(productOptionRepositoryProvider).listDescriptionDocs(); + } + + void reload() { + state = const AsyncValue.loading(); + ref.read(productOptionRepositoryProvider).listDescriptionDocs().then( + (data) => state = AsyncValue.data(data), + onError: (e, st) => state = AsyncValue.error(e, st), + ); + } + + Future create(Map data) async { + await ref.read(productOptionRepositoryProvider).createDescriptionDoc(data); + reload(); + } + + Future updateItem(int id, Map data) async { + await ref.read(productOptionRepositoryProvider).updateDescriptionDoc(id, data); + reload(); + } + + Future delete(int id) async { + await ref.read(productOptionRepositoryProvider).deleteDescriptionDoc(id); + reload(); + } +} diff --git a/client/lib/repositories/product_option_repository.dart b/client/lib/repositories/product_option_repository.dart index 6a9134d..3349931 100644 --- a/client/lib/repositories/product_option_repository.dart +++ b/client/lib/repositories/product_option_repository.dart @@ -141,6 +141,33 @@ class ProductOptionRepository { } } + Future createOrigin(Map data) async { + try { + await _client.post('/product-options/origins', data: data); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '创建失败', + statusCode: e.response?.statusCode); + } + } + + Future updateOrigin(int id, Map data) async { + try { + await _client.put('/product-options/origins/$id', data: data); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '更新失败', + statusCode: e.response?.statusCode); + } + } + + Future deleteOrigin(int id) async { + try { + await _client.delete('/product-options/origins/$id'); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '删除失败', + statusCode: e.response?.statusCode); + } + } + // ── 保质期 ────────────────────────────────────────────────── Future> listShelfLives() async { @@ -154,6 +181,33 @@ class ProductOptionRepository { } } + Future createShelfLife(Map data) async { + try { + await _client.post('/product-options/shelf-lives', data: data); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '创建失败', + statusCode: e.response?.statusCode); + } + } + + Future updateShelfLife(int id, Map data) async { + try { + await _client.put('/product-options/shelf-lives/$id', data: data); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '更新失败', + statusCode: e.response?.statusCode); + } + } + + Future deleteShelfLife(int id) async { + try { + await _client.delete('/product-options/shelf-lives/$id'); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '删除失败', + statusCode: e.response?.statusCode); + } + } + // ── 储存方式 ───────────────────────────────────────────────── Future> listStorages() async { @@ -167,6 +221,33 @@ class ProductOptionRepository { } } + Future createStorage(Map data) async { + try { + await _client.post('/product-options/storages', data: data); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '创建失败', + statusCode: e.response?.statusCode); + } + } + + Future updateStorage(int id, Map data) async { + try { + await _client.put('/product-options/storages/$id', data: data); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '更新失败', + statusCode: e.response?.statusCode); + } + } + + Future deleteStorage(int id) async { + try { + await _client.delete('/product-options/storages/$id'); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '删除失败', + statusCode: e.response?.statusCode); + } + } + // ── 描述文档 ───────────────────────────────────────────────── Future> listDescriptionDocs() async { @@ -179,4 +260,31 @@ class ProductOptionRepository { statusCode: e.response?.statusCode); } } + + Future createDescriptionDoc(Map data) async { + try { + await _client.post('/product-options/description-docs', data: data); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '创建失败', + statusCode: e.response?.statusCode); + } + } + + Future updateDescriptionDoc(int id, Map data) async { + try { + await _client.put('/product-options/description-docs/$id', data: data); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '更新失败', + statusCode: e.response?.statusCode); + } + } + + Future deleteDescriptionDoc(int id) async { + try { + await _client.delete('/product-options/description-docs/$id'); + } on DioException catch (e) { + throw AppException(e.response?.data?['error'] as String? ?? '删除失败', + statusCode: e.response?.statusCode); + } + } } diff --git a/client/lib/screens/public/public_product_screen.dart b/client/lib/screens/public/public_product_screen.dart index 57be073..19a290f 100644 --- a/client/lib/screens/public/public_product_screen.dart +++ b/client/lib/screens/public/public_product_screen.dart @@ -59,6 +59,32 @@ class _PublicProductScreenState extends State { } } + void _showFeedbackDialog({ + required String title, + required String hint, + required String errorType, + }) { + final ctrl = TextEditingController(); + showDialog( + context: context, + builder: (ctx) => _FeedbackDialog( + title: title, + hint: hint, + onSubmit: (msg) async { + await _dio.post( + '${AppConfig.apiBaseUrl}/public/errors', + data: { + 'error_type': errorType, + 'error_msg': msg, + 'platform': 'web-scan', + 'shop_no': (_data?['shop'] as Map?)?['code'] as String? ?? '', + }, + ); + }, + ), + ); + } + @override Widget build(BuildContext context) { if (_loading) { @@ -196,7 +222,20 @@ class _PublicProductScreenState extends State { if (shop != null) SliverToBoxAdapter(child: _ShopCard(shop: shop)), // 9. Footer - const SliverToBoxAdapter(child: _Footer()), + SliverToBoxAdapter( + child: _Footer( + onReport: () => _showFeedbackDialog( + title: '商品报错', + hint: '请描述商品信息有误的地方...', + errorType: 'product_error', + ), + onFeedback: () => _showFeedbackDialog( + title: '意见反馈', + hint: '请输入您的建议或意见...', + errorType: 'feedback', + ), + ), + ), const SliverToBoxAdapter(child: SizedBox(height: 24)), ], ), @@ -1387,7 +1426,9 @@ class _ShopInfoRow extends StatelessWidget { // ── Footer ──────────────────────────────────────────────────────────── class _Footer extends StatelessWidget { - const _Footer(); + final VoidCallback? onReport; + final VoidCallback? onFeedback; + const _Footer({this.onReport, this.onFeedback}); @override Widget build(BuildContext context) { @@ -1440,9 +1481,9 @@ class _Footer extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - _FooterTextLink(text: '商品报错'), + _FooterTextLink(text: '商品报错', onTap: onReport), const _FooterDivider(), - _FooterTextLink(text: '意见反馈'), + _FooterTextLink(text: '意见反馈', onTap: onFeedback), const _FooterDivider(), _FooterTextLink(text: '关于岩美', url: 'https://www.yanmei.com'), ], @@ -1456,21 +1497,23 @@ class _Footer extends StatelessWidget { class _FooterTextLink extends StatelessWidget { final String text; final String? url; - const _FooterTextLink({required this.text, this.url}); + final VoidCallback? onTap; + const _FooterTextLink({required this.text, this.url, this.onTap}); @override Widget build(BuildContext context) { + final active = url != null || onTap != null; return GestureDetector( - onTap: url != null + onTap: onTap ?? (url != null ? () => launchUrl(Uri.parse(url!), mode: LaunchMode.externalApplication) - : null, + : null), child: Text(text, style: TextStyle( fontSize: 11, - color: url != null ? const Color(0xFF2563AC) : _kTextMid, + color: active ? const Color(0xFF2563AC) : _kTextMid, letterSpacing: 0.02, - decoration: url != null ? TextDecoration.underline : null, - decorationColor: url != null ? const Color(0xFF2563AC) : null, + decoration: active ? TextDecoration.underline : null, + decorationColor: active ? const Color(0xFF2563AC) : null, )), ); } @@ -1487,3 +1530,94 @@ class _FooterDivider extends StatelessWidget { ); } } + +// ── Feedback / Report Dialog ────────────────────────────────────────── + +class _FeedbackDialog extends StatefulWidget { + final String title; + final String hint; + final Future Function(String) onSubmit; + const _FeedbackDialog({required this.title, required this.hint, required this.onSubmit}); + + @override + State<_FeedbackDialog> createState() => _FeedbackDialogState(); +} + +class _FeedbackDialogState extends State<_FeedbackDialog> { + final _ctrl = TextEditingController(); + bool _submitting = false; + bool _done = false; + + @override + void dispose() { + _ctrl.dispose(); + super.dispose(); + } + + Future _submit() async { + if (_ctrl.text.trim().isEmpty) return; + setState(() => _submitting = true); + try { + await widget.onSubmit(_ctrl.text.trim()); + if (mounted) setState(() { _done = true; _submitting = false; }); + } catch (_) { + if (mounted) setState(() => _submitting = false); + } + } + + @override + Widget build(BuildContext context) { + if (_done) { + return AlertDialog( + title: Text(widget.title), + content: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.check_circle_outline, size: 48, color: _kSuccess), + SizedBox(height: 12), + Text('提交成功,感谢您的反馈!', + style: TextStyle(fontSize: 14, color: _kInkDeep)), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('关闭'), + ), + ], + ); + } + return AlertDialog( + title: Text(widget.title), + content: SizedBox( + width: 320, + child: TextField( + controller: _ctrl, + maxLines: 4, + autofocus: true, + decoration: InputDecoration( + hintText: widget.hint, + hintStyle: const TextStyle(fontSize: 13, color: _kTextMid), + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.all(12), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: _submitting ? null : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: _kBurgundy, foregroundColor: Colors.white), + child: _submitting + ? const SizedBox(width: 16, height: 16, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) + : const Text('提交'), + ), + ], + ); + } +} diff --git a/todo/todo.html b/todo/todo.html index 25fda0f..8264c70 100644 --- a/todo/todo.html +++ b/todo/todo.html @@ -102,8 +102,8 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
生成于 2026-06-08 · 真相源 todo/todo.json
15全部
-
7未完成
-
8已完成
+
5未完成
+
10已完成
@@ -132,7 +132,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
-
📋 未完成(7)
+
📋 未完成(5)
+ -
  • -
    - Footer「商品报错」「意见反馈」补充实际功能 - 一般 / 优化 -
    -
    public_product_screen.dart _Footer 底部两个链接无 url/action,点击静默。需设计方案:跳转反馈表单、弹出联系方式、或发邮件。line:1353-1355
    -
  • - -
  • + data-done="1">
    基础数据管理 UI 增加 4 个字典维护 Tab 一般 / 优化 @@ -263,17 +250,30 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
    前端 iOS Android
    🕐 2026-06-08 - + ✅ 完成 2026-06-08 · v1.0.24 +
    +
    +
  • + +
  • +
    + Footer「商品报错」「意见反馈」补充实际功能 + 一般 / 优化 +
    +
    public_product_screen.dart _Footer 底部两个链接无 url/action,点击静默。需设计方案:跳转反馈表单、弹出联系方式、或发邮件。line:1353-1355
    +
  • - -
    -
    ✅ 已完成(8)
    - -