feat(client): #14/#18 产地/保质期/储存方式/描述文档 provider & repo 及公开页意见反馈

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-08 08:20:00 +08:00
parent 0d17533120
commit 8a87d4a82c
5 changed files with 429 additions and 67 deletions
@@ -59,6 +59,32 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
}
}
void _showFeedbackDialog({
required String title,
required String hint,
required String errorType,
}) {
final ctrl = TextEditingController();
showDialog<void>(
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<String, dynamic>?)?['code'] as String? ?? '',
},
);
},
),
);
}
@override
Widget build(BuildContext context) {
if (_loading) {
@@ -196,7 +222,20 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
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<void> 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<void> _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('提交'),
),
],
);
}
}