feat(client): #14/#18 产地/保质期/储存方式/描述文档 provider & repo 及公开页意见反馈
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -129,35 +129,155 @@ class ProductSpecListNotifier extends AsyncNotifier<List<ProductSpecOption>> {
|
||||
// ── 产地 ──────────────────────────────────────────────────────
|
||||
|
||||
final productOriginListProvider =
|
||||
FutureProvider<List<ProductOriginOption>>((ref) async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
ref.watch(networkRecoveryCountProvider);
|
||||
return ref.read(productOptionRepositoryProvider).listOrigins();
|
||||
});
|
||||
AsyncNotifierProvider<ProductOriginListNotifier, List<ProductOriginOption>>(
|
||||
ProductOriginListNotifier.new,
|
||||
);
|
||||
|
||||
class ProductOriginListNotifier extends AsyncNotifier<List<ProductOriginOption>> {
|
||||
@override
|
||||
Future<List<ProductOriginOption>> 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<void> create(Map<String, dynamic> data) async {
|
||||
await ref.read(productOptionRepositoryProvider).createOrigin(data);
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> updateItem(int id, Map<String, dynamic> data) async {
|
||||
await ref.read(productOptionRepositoryProvider).updateOrigin(id, data);
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> delete(int id) async {
|
||||
await ref.read(productOptionRepositoryProvider).deleteOrigin(id);
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 保质期 ────────────────────────────────────────────────────
|
||||
|
||||
final productShelfLifeListProvider =
|
||||
FutureProvider<List<ProductShelfLifeOption>>((ref) async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
ref.watch(networkRecoveryCountProvider);
|
||||
return ref.read(productOptionRepositoryProvider).listShelfLives();
|
||||
});
|
||||
AsyncNotifierProvider<ProductShelfLifeListNotifier, List<ProductShelfLifeOption>>(
|
||||
ProductShelfLifeListNotifier.new,
|
||||
);
|
||||
|
||||
class ProductShelfLifeListNotifier extends AsyncNotifier<List<ProductShelfLifeOption>> {
|
||||
@override
|
||||
Future<List<ProductShelfLifeOption>> 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<void> create(Map<String, dynamic> data) async {
|
||||
await ref.read(productOptionRepositoryProvider).createShelfLife(data);
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> updateItem(int id, Map<String, dynamic> data) async {
|
||||
await ref.read(productOptionRepositoryProvider).updateShelfLife(id, data);
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> delete(int id) async {
|
||||
await ref.read(productOptionRepositoryProvider).deleteShelfLife(id);
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 储存方式 ──────────────────────────────────────────────────
|
||||
|
||||
final productStorageListProvider =
|
||||
FutureProvider<List<ProductStorageOption>>((ref) async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
ref.watch(networkRecoveryCountProvider);
|
||||
return ref.read(productOptionRepositoryProvider).listStorages();
|
||||
});
|
||||
AsyncNotifierProvider<ProductStorageListNotifier, List<ProductStorageOption>>(
|
||||
ProductStorageListNotifier.new,
|
||||
);
|
||||
|
||||
class ProductStorageListNotifier extends AsyncNotifier<List<ProductStorageOption>> {
|
||||
@override
|
||||
Future<List<ProductStorageOption>> 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<void> create(Map<String, dynamic> data) async {
|
||||
await ref.read(productOptionRepositoryProvider).createStorage(data);
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> updateItem(int id, Map<String, dynamic> data) async {
|
||||
await ref.read(productOptionRepositoryProvider).updateStorage(id, data);
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> delete(int id) async {
|
||||
await ref.read(productOptionRepositoryProvider).deleteStorage(id);
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 描述文档 ──────────────────────────────────────────────────
|
||||
|
||||
final productDescriptionDocListProvider =
|
||||
FutureProvider<List<ProductDescriptionDoc>>((ref) async {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
ref.watch(networkRecoveryCountProvider);
|
||||
return ref.read(productOptionRepositoryProvider).listDescriptionDocs();
|
||||
});
|
||||
AsyncNotifierProvider<ProductDescriptionDocListNotifier, List<ProductDescriptionDoc>>(
|
||||
ProductDescriptionDocListNotifier.new,
|
||||
);
|
||||
|
||||
class ProductDescriptionDocListNotifier extends AsyncNotifier<List<ProductDescriptionDoc>> {
|
||||
@override
|
||||
Future<List<ProductDescriptionDoc>> 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<void> create(Map<String, dynamic> data) async {
|
||||
await ref.read(productOptionRepositoryProvider).createDescriptionDoc(data);
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> updateItem(int id, Map<String, dynamic> data) async {
|
||||
await ref.read(productOptionRepositoryProvider).updateDescriptionDoc(id, data);
|
||||
reload();
|
||||
}
|
||||
|
||||
Future<void> delete(int id) async {
|
||||
await ref.read(productOptionRepositoryProvider).deleteDescriptionDoc(id);
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,33 @@ class ProductOptionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createOrigin(Map<String, dynamic> 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<void> updateOrigin(int id, Map<String, dynamic> 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<void> 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<List<ProductShelfLifeOption>> listShelfLives() async {
|
||||
@@ -154,6 +181,33 @@ class ProductOptionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createShelfLife(Map<String, dynamic> 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<void> updateShelfLife(int id, Map<String, dynamic> 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<void> 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<List<ProductStorageOption>> listStorages() async {
|
||||
@@ -167,6 +221,33 @@ class ProductOptionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createStorage(Map<String, dynamic> 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<void> updateStorage(int id, Map<String, dynamic> 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<void> 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<List<ProductDescriptionDoc>> listDescriptionDocs() async {
|
||||
@@ -179,4 +260,31 @@ class ProductOptionRepository {
|
||||
statusCode: e.response?.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createDescriptionDoc(Map<String, dynamic> 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<void> updateDescriptionDoc(int id, Map<String, dynamic> 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<void> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('提交'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-30
@@ -102,8 +102,8 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
|
||||
<div class="header-meta">生成于 2026-06-08 · 真相源 todo/todo.json</div>
|
||||
<div class="stats">
|
||||
<div class="stat-pill"><strong>15</strong>全部</div>
|
||||
<div class="stat-pill"><strong>7</strong>未完成</div>
|
||||
<div class="stat-pill"><strong>8</strong>已完成</div>
|
||||
<div class="stat-pill"><strong>5</strong>未完成</div>
|
||||
<div class="stat-pill"><strong>10</strong>已完成</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -132,7 +132,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<div class="section-title">📋 未完成(7)</div>
|
||||
<div class="section-title">📋 未完成(5)</div>
|
||||
<ul class="todo-list" id="pendingList">
|
||||
|
||||
<li class="todo-card"
|
||||
@@ -229,31 +229,18 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<li class="todo-card"
|
||||
data-id="14"
|
||||
data-level="low"
|
||||
data-tags="前端,iOS,Android"
|
||||
data-done="0">
|
||||
<div class="card-header">
|
||||
<span class="item-title">Footer「商品报错」「意见反馈」补充实际功能</span>
|
||||
<span class="tag t-low">一般 / 优化</span>
|
||||
</div>
|
||||
<div class="item-desc">public_product_screen.dart _Footer 底部两个链接无 url/action,点击静默。需设计方案:跳转反馈表单、弹出联系方式、或发邮件。line:1353-1355</div>
|
||||
<div class="card-footer">
|
||||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
|
||||
<div class="item-meta">
|
||||
<span class="meta-date">🕐 2026-06-08</span>
|
||||
<div class="done-section">
|
||||
<div class="section-title done-title">✅ 已完成(10)</div>
|
||||
<button class="done-toggle" id="doneToggle">▾ 展开已完成列表</button>
|
||||
<ul class="todo-list hidden" id="doneList">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="todo-card"
|
||||
<li class="todo-card done"
|
||||
data-id="18"
|
||||
data-level="low"
|
||||
data-tags="前端,iOS,Android"
|
||||
data-done="0">
|
||||
data-done="1">
|
||||
<div class="card-header">
|
||||
<span class="item-title">基础数据管理 UI 增加 4 个字典维护 Tab</span>
|
||||
<span class="tag t-low">一般 / 优化</span>
|
||||
@@ -263,17 +250,30 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
|
||||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
|
||||
<div class="item-meta">
|
||||
<span class="meta-date">🕐 2026-06-08</span>
|
||||
|
||||
<span class="meta-date">✅ 完成 2026-06-08 · <span class="ver-badge">v1.0.24</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="todo-card done"
|
||||
data-id="14"
|
||||
data-level="low"
|
||||
data-tags="前端,iOS,Android"
|
||||
data-done="1">
|
||||
<div class="card-header">
|
||||
<span class="item-title">Footer「商品报错」「意见反馈」补充实际功能</span>
|
||||
<span class="tag t-low">一般 / 优化</span>
|
||||
</div>
|
||||
<div class="item-desc">public_product_screen.dart _Footer 底部两个链接无 url/action,点击静默。需设计方案:跳转反馈表单、弹出联系方式、或发邮件。line:1353-1355</div>
|
||||
<div class="card-footer">
|
||||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span></div>
|
||||
<div class="item-meta">
|
||||
<span class="meta-date">🕐 2026-06-08</span>
|
||||
<span class="meta-date">✅ 完成 2026-06-08 · <span class="ver-badge">v1.0.24</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="done-section">
|
||||
<div class="section-title done-title">✅ 已完成(8)</div>
|
||||
<button class="done-toggle" id="doneToggle">▾ 展开已完成列表</button>
|
||||
<ul class="todo-list hidden" id="doneList">
|
||||
|
||||
<li class="todo-card done"
|
||||
data-id="10"
|
||||
data-level="mid"
|
||||
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"meta": {
|
||||
"title": "酒库管理系统 — 项目 TODO",
|
||||
"updated_at": "2026-06-07T23:28:53.001Z"
|
||||
"updated_at": "2026-06-08T00:06:15.371Z"
|
||||
},
|
||||
"seq": 18,
|
||||
"items": [
|
||||
@@ -160,9 +160,9 @@
|
||||
"Android"
|
||||
],
|
||||
"created_at": "2026-06-07T18:12:50.299Z",
|
||||
"done": false,
|
||||
"completed_at": null,
|
||||
"version": null
|
||||
"done": true,
|
||||
"completed_at": "2026-06-08T00:06:15.299Z",
|
||||
"version": "v1.0.24"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
@@ -220,9 +220,9 @@
|
||||
"Android"
|
||||
],
|
||||
"created_at": "2026-06-07T22:52:13.283Z",
|
||||
"done": false,
|
||||
"completed_at": null,
|
||||
"version": null
|
||||
"done": true,
|
||||
"completed_at": "2026-06-08T00:06:15.371Z",
|
||||
"version": "v1.0.24"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user