Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f788a7ab74 | |||
| 8a87d4a82c | |||
| 0d17533120 | |||
| 4e64e5eb47 |
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.25] - 2026-06-08
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- 基础数据页新增「产地」「保质期」「储存方式」「描述文档」4 个字典维护 Tab,可直接在基础数据中增删改查
|
||||||
|
- 商品详情页展示产地、保质期、储存方式参数,并在介绍区上方显示建议零售价
|
||||||
|
- 入库单详情中商品列展示产地、保质期、储存方式信息
|
||||||
|
- 公开扫码页新增「商品报错」「意见反馈」功能,用户可提交反馈
|
||||||
|
|
||||||
## [1.0.24] - 2026-06-08
|
## [1.0.24] - 2026-06-08
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ func (h *ProductHandler) Detail(c *gin.Context) {
|
|||||||
var product model.Product
|
var product model.Product
|
||||||
if err := h.db.Where("id = ? AND shop_id = ? AND deleted_at IS NULL", id, shopID).
|
if err := h.db.Where("id = ? AND shop_id = ? AND deleted_at IS NULL", id, shopID).
|
||||||
Preload("Category").Preload("Images").
|
Preload("Category").Preload("Images").
|
||||||
|
Preload("Origin").Preload("ShelfLife").Preload("Storage").
|
||||||
First(&product).Error; err != nil {
|
First(&product).Error; err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ func (h *StockInHandler) List(c *gin.Context) {
|
|||||||
func (h *StockInHandler) Get(c *gin.Context) {
|
func (h *StockInHandler) Get(c *gin.Context) {
|
||||||
shopID := middleware.GetShopID(c)
|
shopID := middleware.GetShopID(c)
|
||||||
var order model.StockInOrder
|
var order model.StockInOrder
|
||||||
if err := h.db.Preload("Items.Product").Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer").
|
if err := h.db.Preload("Items.Product").Preload("Items.Product.Origin").
|
||||||
|
Preload("Items.Product.ShelfLife").Preload("Items.Product.Storage").
|
||||||
|
Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer").
|
||||||
Where("id = ? AND shop_id = ? AND deleted_at IS NULL", c.Param("id"), shopID).
|
Where("id = ? AND shop_id = ? AND deleted_at IS NULL", c.Param("id"), shopID).
|
||||||
First(&order).Error; err != nil {
|
First(&order).Error; err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ class Product {
|
|||||||
final String? remark;
|
final String? remark;
|
||||||
final Map<String, dynamic>? customFields;
|
final Map<String, dynamic>? customFields;
|
||||||
final List<ProductImage> images;
|
final List<ProductImage> images;
|
||||||
|
// 产地/保质期/储存方式名称(Detail 接口 Preload 后填充)
|
||||||
|
final String? originName;
|
||||||
|
final String? shelfLifeName;
|
||||||
|
final String? storageName;
|
||||||
|
|
||||||
const Product({
|
const Product({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -37,6 +41,9 @@ class Product {
|
|||||||
this.remark,
|
this.remark,
|
||||||
this.customFields,
|
this.customFields,
|
||||||
this.images = const [],
|
this.images = const [],
|
||||||
|
this.originName,
|
||||||
|
this.shelfLifeName,
|
||||||
|
this.storageName,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Product.fromJson(Map<String, dynamic> json) => Product(
|
factory Product.fromJson(Map<String, dynamic> json) => Product(
|
||||||
@@ -68,6 +75,9 @@ class Product {
|
|||||||
?.map((e) => ProductImage.fromJson(e as Map<String, dynamic>))
|
?.map((e) => ProductImage.fromJson(e as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[],
|
||||||
|
originName: (json['origin'] as Map<String, dynamic>?)?['name'] as String?,
|
||||||
|
shelfLifeName: (json['shelf_life'] as Map<String, dynamic>?)?['name'] as String?,
|
||||||
|
storageName: (json['storage'] as Map<String, dynamic>?)?['name'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ class StockInItem {
|
|||||||
final String? productSeries;
|
final String? productSeries;
|
||||||
final String? productSpec;
|
final String? productSpec;
|
||||||
final String? productUnit;
|
final String? productUnit;
|
||||||
|
final String? productOrigin;
|
||||||
|
final String? productShelfLife;
|
||||||
|
final String? productStorage;
|
||||||
|
|
||||||
const StockInItem({
|
const StockInItem({
|
||||||
this.orderId,
|
this.orderId,
|
||||||
@@ -26,6 +29,9 @@ class StockInItem {
|
|||||||
this.productSeries,
|
this.productSeries,
|
||||||
this.productSpec,
|
this.productSpec,
|
||||||
this.productUnit,
|
this.productUnit,
|
||||||
|
this.productOrigin,
|
||||||
|
this.productShelfLife,
|
||||||
|
this.productStorage,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory StockInItem.fromJson(Map<String, dynamic> json) => StockInItem(
|
factory StockInItem.fromJson(Map<String, dynamic> json) => StockInItem(
|
||||||
@@ -43,6 +49,12 @@ class StockInItem {
|
|||||||
productSeries: (json['product'] as Map<String, dynamic>?)?['series'] as String?,
|
productSeries: (json['product'] as Map<String, dynamic>?)?['series'] as String?,
|
||||||
productSpec: (json['product'] as Map<String, dynamic>?)?['spec'] as String?,
|
productSpec: (json['product'] as Map<String, dynamic>?)?['spec'] as String?,
|
||||||
productUnit: (json['product'] as Map<String, dynamic>?)?['unit'] as String?,
|
productUnit: (json['product'] as Map<String, dynamic>?)?['unit'] as String?,
|
||||||
|
productOrigin: ((json['product'] as Map<String, dynamic>?)?['origin']
|
||||||
|
as Map<String, dynamic>?)?['name'] as String?,
|
||||||
|
productShelfLife: ((json['product'] as Map<String, dynamic>?)?['shelf_life']
|
||||||
|
as Map<String, dynamic>?)?['name'] as String?,
|
||||||
|
productStorage: ((json['product'] as Map<String, dynamic>?)?['storage']
|
||||||
|
as Map<String, dynamic>?)?['name'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
|
|||||||
@@ -129,35 +129,155 @@ class ProductSpecListNotifier extends AsyncNotifier<List<ProductSpecOption>> {
|
|||||||
// ── 产地 ──────────────────────────────────────────────────────
|
// ── 产地 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
final productOriginListProvider =
|
final productOriginListProvider =
|
||||||
FutureProvider<List<ProductOriginOption>>((ref) async {
|
AsyncNotifierProvider<ProductOriginListNotifier, List<ProductOriginOption>>(
|
||||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
ProductOriginListNotifier.new,
|
||||||
ref.watch(networkRecoveryCountProvider);
|
);
|
||||||
return ref.read(productOptionRepositoryProvider).listOrigins();
|
|
||||||
});
|
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 =
|
final productShelfLifeListProvider =
|
||||||
FutureProvider<List<ProductShelfLifeOption>>((ref) async {
|
AsyncNotifierProvider<ProductShelfLifeListNotifier, List<ProductShelfLifeOption>>(
|
||||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
ProductShelfLifeListNotifier.new,
|
||||||
ref.watch(networkRecoveryCountProvider);
|
);
|
||||||
return ref.read(productOptionRepositoryProvider).listShelfLives();
|
|
||||||
});
|
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 =
|
final productStorageListProvider =
|
||||||
FutureProvider<List<ProductStorageOption>>((ref) async {
|
AsyncNotifierProvider<ProductStorageListNotifier, List<ProductStorageOption>>(
|
||||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
ProductStorageListNotifier.new,
|
||||||
ref.watch(networkRecoveryCountProvider);
|
);
|
||||||
return ref.read(productOptionRepositoryProvider).listStorages();
|
|
||||||
});
|
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 =
|
final productDescriptionDocListProvider =
|
||||||
FutureProvider<List<ProductDescriptionDoc>>((ref) async {
|
AsyncNotifierProvider<ProductDescriptionDocListNotifier, List<ProductDescriptionDoc>>(
|
||||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
ProductDescriptionDocListNotifier.new,
|
||||||
ref.watch(networkRecoveryCountProvider);
|
);
|
||||||
return ref.read(productOptionRepositoryProvider).listDescriptionDocs();
|
|
||||||
});
|
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 {
|
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 {
|
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 {
|
Future<List<ProductDescriptionDoc>> listDescriptionDocs() async {
|
||||||
@@ -179,4 +260,31 @@ class ProductOptionRepository {
|
|||||||
statusCode: e.response?.statusCode);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,6 +269,10 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
_buildImageSection(p),
|
_buildImageSection(p),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildInfoSection(p),
|
_buildInfoSection(p),
|
||||||
|
if (p.salePrice != null && p.salePrice! > 0) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildPriceSection(p),
|
||||||
|
],
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildDescSection(),
|
_buildDescSection(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
@@ -350,7 +354,12 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
SizedBox(width: 200, child: row('品牌', p.brand?.isEmpty ?? true ? '-' : p.brand!)),
|
SizedBox(width: 200, child: row('品牌', p.brand?.isEmpty ?? true ? '-' : p.brand!)),
|
||||||
SizedBox(width: 200, child: row('单位', p.unit.isEmpty ? '-' : p.unit)),
|
SizedBox(width: 200, child: row('单位', p.unit.isEmpty ? '-' : p.unit)),
|
||||||
SizedBox(width: 200, child: row('进价', p.purchasePrice != null ? '¥${p.purchasePrice!.toStringAsFixed(2)}' : '-')),
|
SizedBox(width: 200, child: row('进价', p.purchasePrice != null ? '¥${p.purchasePrice!.toStringAsFixed(2)}' : '-')),
|
||||||
SizedBox(width: 200, child: row('售价', p.salePrice != null ? '¥${p.salePrice!.toStringAsFixed(2)}' : '-')),
|
if (p.originName?.isNotEmpty ?? false)
|
||||||
|
SizedBox(width: 200, child: row('产地', p.originName!)),
|
||||||
|
if (p.shelfLifeName?.isNotEmpty ?? false)
|
||||||
|
SizedBox(width: 200, child: row('保质期', p.shelfLifeName!)),
|
||||||
|
if (p.storageName?.isNotEmpty ?? false)
|
||||||
|
SizedBox(width: 200, child: row('储存方式', p.storageName!)),
|
||||||
if (p.barcode?.isNotEmpty ?? false)
|
if (p.barcode?.isNotEmpty ?? false)
|
||||||
SizedBox(width: 200, child: row('条码', p.barcode!)),
|
SizedBox(width: 200, child: row('条码', p.barcode!)),
|
||||||
],
|
],
|
||||||
@@ -359,6 +368,35 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPriceSection(Product p) {
|
||||||
|
final price = p.salePrice!;
|
||||||
|
final priceStr = price % 1 == 0
|
||||||
|
? price.toInt().toString()
|
||||||
|
: price.toStringAsFixed(2);
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
side: BorderSide(color: AppTheme.border, width: 0.5),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Text('建议零售价',
|
||||||
|
style: TextStyle(fontSize: 13, color: AppTheme.textSecondary)),
|
||||||
|
const Spacer(),
|
||||||
|
Text('¥$priceStr',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppTheme.danger)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDescSection() {
|
Widget _buildDescSection() {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
|||||||
@@ -22,16 +22,28 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
final _nameSearchCtrl = TextEditingController();
|
final _nameSearchCtrl = TextEditingController();
|
||||||
final _seriesSearchCtrl = TextEditingController();
|
final _seriesSearchCtrl = TextEditingController();
|
||||||
final _specSearchCtrl = TextEditingController();
|
final _specSearchCtrl = TextEditingController();
|
||||||
|
final _originSearchCtrl = TextEditingController();
|
||||||
|
final _shelfLifeSearchCtrl = TextEditingController();
|
||||||
|
final _storageSearchCtrl = TextEditingController();
|
||||||
|
final _descDocSearchCtrl = TextEditingController();
|
||||||
|
|
||||||
int _namePage = 1; int _namePageSize = 20;
|
int _namePage = 1; int _namePageSize = 20;
|
||||||
int _seriesPage = 1; int _seriesPageSize = 20;
|
int _seriesPage = 1; int _seriesPageSize = 20;
|
||||||
int _specPage = 1; int _specPageSize = 20;
|
int _specPage = 1; int _specPageSize = 20;
|
||||||
|
int _originPage = 1; int _originPageSize = 20;
|
||||||
|
int _shelfLifePage = 1; int _shelfLifePageSize = 20;
|
||||||
|
int _storagePage = 1; int _storagePageSize = 20;
|
||||||
|
int _descDocPage = 1; int _descDocPageSize = 20;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_nameSearchCtrl.dispose();
|
_nameSearchCtrl.dispose();
|
||||||
_seriesSearchCtrl.dispose();
|
_seriesSearchCtrl.dispose();
|
||||||
_specSearchCtrl.dispose();
|
_specSearchCtrl.dispose();
|
||||||
|
_originSearchCtrl.dispose();
|
||||||
|
_shelfLifeSearchCtrl.dispose();
|
||||||
|
_storageSearchCtrl.dispose();
|
||||||
|
_descDocSearchCtrl.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,12 +55,20 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
Tab(text: '商品名称'),
|
Tab(text: '商品名称'),
|
||||||
Tab(text: '系列'),
|
Tab(text: '系列'),
|
||||||
Tab(text: '规格'),
|
Tab(text: '规格'),
|
||||||
|
Tab(text: '产地'),
|
||||||
|
Tab(text: '保质期'),
|
||||||
|
Tab(text: '储存方式'),
|
||||||
|
Tab(text: '描述文档'),
|
||||||
Tab(text: '仓库'),
|
Tab(text: '仓库'),
|
||||||
],
|
],
|
||||||
tabViews: [
|
tabViews: [
|
||||||
_buildNameTab(),
|
_buildNameTab(),
|
||||||
_buildSeriesTab(),
|
_buildSeriesTab(),
|
||||||
_buildSpecTab(),
|
_buildSpecTab(),
|
||||||
|
_buildOriginTab(),
|
||||||
|
_buildShelfLifeTab(),
|
||||||
|
_buildStorageTab(),
|
||||||
|
_buildDescDocTab(),
|
||||||
_buildWarehousesTab(),
|
_buildWarehousesTab(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -455,6 +475,362 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 产地 tab ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildOriginTab() {
|
||||||
|
final async = ref.watch(productOriginListProvider);
|
||||||
|
return async.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, _) => _buildError(() => ref.read(productOriginListProvider.notifier).reload()),
|
||||||
|
data: (items) {
|
||||||
|
final keyword = _originSearchCtrl.text.toLowerCase();
|
||||||
|
final filtered = keyword.isEmpty
|
||||||
|
? items
|
||||||
|
: items.where((it) => it.name.toLowerCase().contains(keyword) ||
|
||||||
|
(it.code?.toLowerCase().contains(keyword) ?? false)).toList();
|
||||||
|
final paged = filtered.skip((_originPage - 1) * _originPageSize).take(_originPageSize).toList();
|
||||||
|
return DataTableCard(
|
||||||
|
totalCount: filtered.length,
|
||||||
|
page: _originPage,
|
||||||
|
pageSize: _originPageSize,
|
||||||
|
onPageChanged: (p) => setState(() => _originPage = p),
|
||||||
|
onPageSizeChanged: (s) => setState(() { _originPageSize = s; _originPage = 1; }),
|
||||||
|
toolbar: _buildToolbar(
|
||||||
|
searchCtrl: _originSearchCtrl,
|
||||||
|
hint: '搜索产地/编号',
|
||||||
|
onSearchChanged: () => setState(() => _originPage = 1),
|
||||||
|
onAdd: () => _showOptionDialog(
|
||||||
|
title: '新建产地',
|
||||||
|
hasQuantity: false,
|
||||||
|
onSave: (data) => ref.read(productOriginListProvider.notifier).create(data),
|
||||||
|
),
|
||||||
|
onExport: () => exportExcel(
|
||||||
|
filename: '产地',
|
||||||
|
headers: ['编号', '名称', '备注'],
|
||||||
|
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
mobileCards: paged.map((it) => _optionCard(
|
||||||
|
code: it.code, name: it.name, remark: it.remark,
|
||||||
|
onEdit: () => _showOptionDialog(
|
||||||
|
title: '编辑产地', hasQuantity: false,
|
||||||
|
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
|
||||||
|
onSave: (data) => ref.read(productOriginListProvider.notifier).updateItem(it.id, data),
|
||||||
|
),
|
||||||
|
onDelete: () => _confirmDelete('删除产地「${it.name}」?',
|
||||||
|
() => ref.read(productOriginListProvider.notifier).delete(it.id)),
|
||||||
|
)).toList(),
|
||||||
|
columns: const [DataColumn(label: Text('编号')), DataColumn(label: Text('名称')), DataColumn(label: Text('备注')), DataColumn(label: Text('操作'))],
|
||||||
|
rows: paged.isEmpty
|
||||||
|
? [DataRow(cells: [const DataCell(SizedBox()), const DataCell(Text('暂无数据', style: TextStyle(color: AppTheme.textSecondary))), const DataCell(SizedBox()), const DataCell(SizedBox())])]
|
||||||
|
: paged.map((it) => DataRow(cells: [
|
||||||
|
DataCell(Text(it.code ?? '-', style: const TextStyle(fontFamily: 'monospace', fontSize: 12, color: AppTheme.textSecondary))),
|
||||||
|
DataCell(Text(it.name, style: const TextStyle(fontWeight: FontWeight.w500))),
|
||||||
|
DataCell(Text(it.remark ?? '-')),
|
||||||
|
DataCell(_actionButtons(
|
||||||
|
onEdit: () => _showOptionDialog(
|
||||||
|
title: '编辑产地', hasQuantity: false,
|
||||||
|
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
|
||||||
|
onSave: (data) => ref.read(productOriginListProvider.notifier).updateItem(it.id, data),
|
||||||
|
),
|
||||||
|
onDelete: () => _confirmDelete('删除产地「${it.name}」?',
|
||||||
|
() => ref.read(productOriginListProvider.notifier).delete(it.id)),
|
||||||
|
)),
|
||||||
|
])).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 保质期 tab ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildShelfLifeTab() {
|
||||||
|
final async = ref.watch(productShelfLifeListProvider);
|
||||||
|
return async.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, _) => _buildError(() => ref.read(productShelfLifeListProvider.notifier).reload()),
|
||||||
|
data: (items) {
|
||||||
|
final keyword = _shelfLifeSearchCtrl.text.toLowerCase();
|
||||||
|
final filtered = keyword.isEmpty
|
||||||
|
? items
|
||||||
|
: items.where((it) => it.name.toLowerCase().contains(keyword) ||
|
||||||
|
(it.code?.toLowerCase().contains(keyword) ?? false)).toList();
|
||||||
|
final paged = filtered.skip((_shelfLifePage - 1) * _shelfLifePageSize).take(_shelfLifePageSize).toList();
|
||||||
|
return DataTableCard(
|
||||||
|
totalCount: filtered.length,
|
||||||
|
page: _shelfLifePage,
|
||||||
|
pageSize: _shelfLifePageSize,
|
||||||
|
onPageChanged: (p) => setState(() => _shelfLifePage = p),
|
||||||
|
onPageSizeChanged: (s) => setState(() { _shelfLifePageSize = s; _shelfLifePage = 1; }),
|
||||||
|
toolbar: _buildToolbar(
|
||||||
|
searchCtrl: _shelfLifeSearchCtrl,
|
||||||
|
hint: '搜索保质期/编号',
|
||||||
|
onSearchChanged: () => setState(() => _shelfLifePage = 1),
|
||||||
|
onAdd: () => _showOptionDialog(
|
||||||
|
title: '新建保质期',
|
||||||
|
hasQuantity: false,
|
||||||
|
onSave: (data) => ref.read(productShelfLifeListProvider.notifier).create(data),
|
||||||
|
),
|
||||||
|
onExport: () => exportExcel(
|
||||||
|
filename: '保质期',
|
||||||
|
headers: ['编号', '名称', '备注'],
|
||||||
|
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
mobileCards: paged.map((it) => _optionCard(
|
||||||
|
code: it.code, name: it.name, remark: it.remark,
|
||||||
|
onEdit: () => _showOptionDialog(
|
||||||
|
title: '编辑保质期', hasQuantity: false,
|
||||||
|
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
|
||||||
|
onSave: (data) => ref.read(productShelfLifeListProvider.notifier).updateItem(it.id, data),
|
||||||
|
),
|
||||||
|
onDelete: () => _confirmDelete('删除保质期「${it.name}」?',
|
||||||
|
() => ref.read(productShelfLifeListProvider.notifier).delete(it.id)),
|
||||||
|
)).toList(),
|
||||||
|
columns: const [DataColumn(label: Text('编号')), DataColumn(label: Text('名称')), DataColumn(label: Text('备注')), DataColumn(label: Text('操作'))],
|
||||||
|
rows: paged.isEmpty
|
||||||
|
? [DataRow(cells: [const DataCell(SizedBox()), const DataCell(Text('暂无数据', style: TextStyle(color: AppTheme.textSecondary))), const DataCell(SizedBox()), const DataCell(SizedBox())])]
|
||||||
|
: paged.map((it) => DataRow(cells: [
|
||||||
|
DataCell(Text(it.code ?? '-', style: const TextStyle(fontFamily: 'monospace', fontSize: 12, color: AppTheme.textSecondary))),
|
||||||
|
DataCell(Text(it.name, style: const TextStyle(fontWeight: FontWeight.w500))),
|
||||||
|
DataCell(Text(it.remark ?? '-')),
|
||||||
|
DataCell(_actionButtons(
|
||||||
|
onEdit: () => _showOptionDialog(
|
||||||
|
title: '编辑保质期', hasQuantity: false,
|
||||||
|
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
|
||||||
|
onSave: (data) => ref.read(productShelfLifeListProvider.notifier).updateItem(it.id, data),
|
||||||
|
),
|
||||||
|
onDelete: () => _confirmDelete('删除保质期「${it.name}」?',
|
||||||
|
() => ref.read(productShelfLifeListProvider.notifier).delete(it.id)),
|
||||||
|
)),
|
||||||
|
])).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 储存方式 tab ───────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildStorageTab() {
|
||||||
|
final async = ref.watch(productStorageListProvider);
|
||||||
|
return async.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, _) => _buildError(() => ref.read(productStorageListProvider.notifier).reload()),
|
||||||
|
data: (items) {
|
||||||
|
final keyword = _storageSearchCtrl.text.toLowerCase();
|
||||||
|
final filtered = keyword.isEmpty
|
||||||
|
? items
|
||||||
|
: items.where((it) => it.name.toLowerCase().contains(keyword) ||
|
||||||
|
(it.code?.toLowerCase().contains(keyword) ?? false)).toList();
|
||||||
|
final paged = filtered.skip((_storagePage - 1) * _storagePageSize).take(_storagePageSize).toList();
|
||||||
|
return DataTableCard(
|
||||||
|
totalCount: filtered.length,
|
||||||
|
page: _storagePage,
|
||||||
|
pageSize: _storagePageSize,
|
||||||
|
onPageChanged: (p) => setState(() => _storagePage = p),
|
||||||
|
onPageSizeChanged: (s) => setState(() { _storagePageSize = s; _storagePage = 1; }),
|
||||||
|
toolbar: _buildToolbar(
|
||||||
|
searchCtrl: _storageSearchCtrl,
|
||||||
|
hint: '搜索储存方式/编号',
|
||||||
|
onSearchChanged: () => setState(() => _storagePage = 1),
|
||||||
|
onAdd: () => _showOptionDialog(
|
||||||
|
title: '新建储存方式',
|
||||||
|
hasQuantity: false,
|
||||||
|
onSave: (data) => ref.read(productStorageListProvider.notifier).create(data),
|
||||||
|
),
|
||||||
|
onExport: () => exportExcel(
|
||||||
|
filename: '储存方式',
|
||||||
|
headers: ['编号', '名称', '备注'],
|
||||||
|
rows: items.map((it) => [it.code ?? '', it.name, it.remark ?? '']).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
mobileCards: paged.map((it) => _optionCard(
|
||||||
|
code: it.code, name: it.name, remark: it.remark,
|
||||||
|
onEdit: () => _showOptionDialog(
|
||||||
|
title: '编辑储存方式', hasQuantity: false,
|
||||||
|
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
|
||||||
|
onSave: (data) => ref.read(productStorageListProvider.notifier).updateItem(it.id, data),
|
||||||
|
),
|
||||||
|
onDelete: () => _confirmDelete('删除储存方式「${it.name}」?',
|
||||||
|
() => ref.read(productStorageListProvider.notifier).delete(it.id)),
|
||||||
|
)).toList(),
|
||||||
|
columns: const [DataColumn(label: Text('编号')), DataColumn(label: Text('名称')), DataColumn(label: Text('备注')), DataColumn(label: Text('操作'))],
|
||||||
|
rows: paged.isEmpty
|
||||||
|
? [DataRow(cells: [const DataCell(SizedBox()), const DataCell(Text('暂无数据', style: TextStyle(color: AppTheme.textSecondary))), const DataCell(SizedBox()), const DataCell(SizedBox())])]
|
||||||
|
: paged.map((it) => DataRow(cells: [
|
||||||
|
DataCell(Text(it.code ?? '-', style: const TextStyle(fontFamily: 'monospace', fontSize: 12, color: AppTheme.textSecondary))),
|
||||||
|
DataCell(Text(it.name, style: const TextStyle(fontWeight: FontWeight.w500))),
|
||||||
|
DataCell(Text(it.remark ?? '-')),
|
||||||
|
DataCell(_actionButtons(
|
||||||
|
onEdit: () => _showOptionDialog(
|
||||||
|
title: '编辑储存方式', hasQuantity: false,
|
||||||
|
initial: {'code': it.code ?? '', 'name': it.name, 'remark': it.remark ?? ''},
|
||||||
|
onSave: (data) => ref.read(productStorageListProvider.notifier).updateItem(it.id, data),
|
||||||
|
),
|
||||||
|
onDelete: () => _confirmDelete('删除储存方式「${it.name}」?',
|
||||||
|
() => ref.read(productStorageListProvider.notifier).delete(it.id)),
|
||||||
|
)),
|
||||||
|
])).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 描述文档 tab ───────────────────────────────────────────
|
||||||
|
|
||||||
|
Widget _buildDescDocTab() {
|
||||||
|
final async = ref.watch(productDescriptionDocListProvider);
|
||||||
|
return async.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, _) => _buildError(() => ref.read(productDescriptionDocListProvider.notifier).reload()),
|
||||||
|
data: (items) {
|
||||||
|
final keyword = _descDocSearchCtrl.text.toLowerCase();
|
||||||
|
final filtered = keyword.isEmpty
|
||||||
|
? items
|
||||||
|
: items.where((it) => it.title.toLowerCase().contains(keyword)).toList();
|
||||||
|
final paged = filtered.skip((_descDocPage - 1) * _descDocPageSize).take(_descDocPageSize).toList();
|
||||||
|
return DataTableCard(
|
||||||
|
totalCount: filtered.length,
|
||||||
|
page: _descDocPage,
|
||||||
|
pageSize: _descDocPageSize,
|
||||||
|
onPageChanged: (p) => setState(() => _descDocPage = p),
|
||||||
|
onPageSizeChanged: (s) => setState(() { _descDocPageSize = s; _descDocPage = 1; }),
|
||||||
|
toolbar: _buildToolbar(
|
||||||
|
searchCtrl: _descDocSearchCtrl,
|
||||||
|
hint: '搜索标题',
|
||||||
|
onSearchChanged: () => setState(() => _descDocPage = 1),
|
||||||
|
onAdd: () => _showDescDocDialog(
|
||||||
|
title: '新建描述文档',
|
||||||
|
onSave: (data) => ref.read(productDescriptionDocListProvider.notifier).create(data),
|
||||||
|
),
|
||||||
|
onExport: () => exportExcel(
|
||||||
|
filename: '描述文档',
|
||||||
|
headers: ['标题', '内容', '备注'],
|
||||||
|
rows: items.map((it) => [it.title, it.content ?? '', it.remark ?? '']).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
mobileCards: paged.map((it) => MobileListCard(
|
||||||
|
title: Text(it.title),
|
||||||
|
fields: [
|
||||||
|
if (it.content?.isNotEmpty == true)
|
||||||
|
MobileCardField('内容', it.content),
|
||||||
|
if (it.remark?.isNotEmpty == true)
|
||||||
|
MobileCardField('备注', it.remark),
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _showDescDocDialog(
|
||||||
|
title: '编辑描述文档',
|
||||||
|
initial: {'title': it.title, 'content': it.content ?? '', 'remark': it.remark ?? ''},
|
||||||
|
onSave: (data) => ref.read(productDescriptionDocListProvider.notifier).updateItem(it.id, data),
|
||||||
|
),
|
||||||
|
child: const Text('编辑', style: TextStyle(fontSize: 13)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _confirmDelete('删除描述文档「${it.title}」?',
|
||||||
|
() => ref.read(productDescriptionDocListProvider.notifier).delete(it.id)),
|
||||||
|
child: const Text('删除', style: TextStyle(fontSize: 13, color: AppTheme.danger)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)).toList(),
|
||||||
|
columns: const [DataColumn(label: Text('标题')), DataColumn(label: Text('内容预览')), DataColumn(label: Text('备注')), DataColumn(label: Text('操作'))],
|
||||||
|
rows: paged.isEmpty
|
||||||
|
? [DataRow(cells: [const DataCell(SizedBox()), const DataCell(Text('暂无数据', style: TextStyle(color: AppTheme.textSecondary))), const DataCell(SizedBox()), const DataCell(SizedBox())])]
|
||||||
|
: paged.map((it) {
|
||||||
|
final preview = (it.content ?? '').length > 30
|
||||||
|
? '${it.content!.substring(0, 30)}…'
|
||||||
|
: (it.content ?? '-');
|
||||||
|
return DataRow(cells: [
|
||||||
|
DataCell(Text(it.title, style: const TextStyle(fontWeight: FontWeight.w500))),
|
||||||
|
DataCell(Text(preview, style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary))),
|
||||||
|
DataCell(Text(it.remark ?? '-')),
|
||||||
|
DataCell(_actionButtons(
|
||||||
|
onEdit: () => _showDescDocDialog(
|
||||||
|
title: '编辑描述文档',
|
||||||
|
initial: {'title': it.title, 'content': it.content ?? '', 'remark': it.remark ?? ''},
|
||||||
|
onSave: (data) => ref.read(productDescriptionDocListProvider.notifier).updateItem(it.id, data),
|
||||||
|
),
|
||||||
|
onDelete: () => _confirmDelete('删除描述文档「${it.title}」?',
|
||||||
|
() => ref.read(productDescriptionDocListProvider.notifier).delete(it.id)),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showDescDocDialog({
|
||||||
|
required String title,
|
||||||
|
required Future<void> Function(Map<String, dynamic>) onSave,
|
||||||
|
Map<String, dynamic>? initial,
|
||||||
|
}) async {
|
||||||
|
final titleCtrl = TextEditingController(text: initial?['title'] as String? ?? '');
|
||||||
|
final contentCtrl = TextEditingController(text: initial?['content'] as String? ?? '');
|
||||||
|
final remarkCtrl = TextEditingController(text: initial?['remark'] as String? ?? '');
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
await showAppDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: SizedBox(
|
||||||
|
width: context.dialogWidth(400),
|
||||||
|
child: Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: titleCtrl,
|
||||||
|
decoration: const InputDecoration(labelText: '标题 *(如:酱香型白酒)'),
|
||||||
|
validator: (v) => (v == null || v.trim().isEmpty) ? '请输入标题' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: contentCtrl,
|
||||||
|
decoration: const InputDecoration(labelText: '内容(商品介绍正文)'),
|
||||||
|
maxLines: 5,
|
||||||
|
minLines: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: remarkCtrl,
|
||||||
|
decoration: const InputDecoration(labelText: '备注'),
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('取消')),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (!formKey.currentState!.validate()) return;
|
||||||
|
final data = <String, dynamic>{
|
||||||
|
'title': titleCtrl.text.trim(),
|
||||||
|
if (contentCtrl.text.isNotEmpty) 'content': contentCtrl.text.trim(),
|
||||||
|
if (remarkCtrl.text.isNotEmpty) 'remark': remarkCtrl.text.trim(),
|
||||||
|
};
|
||||||
|
Navigator.of(ctx).pop();
|
||||||
|
try {
|
||||||
|
await onSave(data);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('保存失败:$e'), backgroundColor: AppTheme.danger),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('保存'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── 仓库 tab ──────────────────────────────────────────────
|
// ── 仓库 tab ──────────────────────────────────────────────
|
||||||
|
|
||||||
Widget _buildWarehousesTab() {
|
Widget _buildWarehousesTab() {
|
||||||
|
|||||||
@@ -59,6 +59,31 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showFeedbackDialog({
|
||||||
|
required String title,
|
||||||
|
required String hint,
|
||||||
|
required String errorType,
|
||||||
|
}) {
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_loading) {
|
if (_loading) {
|
||||||
@@ -196,7 +221,20 @@ class _PublicProductScreenState extends State<PublicProductScreen> {
|
|||||||
if (shop != null)
|
if (shop != null)
|
||||||
SliverToBoxAdapter(child: _ShopCard(shop: shop)),
|
SliverToBoxAdapter(child: _ShopCard(shop: shop)),
|
||||||
// 9. Footer
|
// 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)),
|
const SliverToBoxAdapter(child: SizedBox(height: 24)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1387,7 +1425,9 @@ class _ShopInfoRow extends StatelessWidget {
|
|||||||
// ── Footer ────────────────────────────────────────────────────────────
|
// ── Footer ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class _Footer extends StatelessWidget {
|
class _Footer extends StatelessWidget {
|
||||||
const _Footer();
|
final VoidCallback? onReport;
|
||||||
|
final VoidCallback? onFeedback;
|
||||||
|
const _Footer({this.onReport, this.onFeedback});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -1440,9 +1480,9 @@ class _Footer extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_FooterTextLink(text: '商品报错'),
|
_FooterTextLink(text: '商品报错', onTap: onReport),
|
||||||
const _FooterDivider(),
|
const _FooterDivider(),
|
||||||
_FooterTextLink(text: '意见反馈'),
|
_FooterTextLink(text: '意见反馈', onTap: onFeedback),
|
||||||
const _FooterDivider(),
|
const _FooterDivider(),
|
||||||
_FooterTextLink(text: '关于岩美', url: 'https://www.yanmei.com'),
|
_FooterTextLink(text: '关于岩美', url: 'https://www.yanmei.com'),
|
||||||
],
|
],
|
||||||
@@ -1456,21 +1496,23 @@ class _Footer extends StatelessWidget {
|
|||||||
class _FooterTextLink extends StatelessWidget {
|
class _FooterTextLink extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
final String? url;
|
final String? url;
|
||||||
const _FooterTextLink({required this.text, this.url});
|
final VoidCallback? onTap;
|
||||||
|
const _FooterTextLink({required this.text, this.url, this.onTap});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final active = url != null || onTap != null;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: url != null
|
onTap: onTap ?? (url != null
|
||||||
? () => launchUrl(Uri.parse(url!), mode: LaunchMode.externalApplication)
|
? () => launchUrl(Uri.parse(url!), mode: LaunchMode.externalApplication)
|
||||||
: null,
|
: null),
|
||||||
child: Text(text,
|
child: Text(text,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: url != null ? const Color(0xFF2563AC) : _kTextMid,
|
color: active ? const Color(0xFF2563AC) : _kTextMid,
|
||||||
letterSpacing: 0.02,
|
letterSpacing: 0.02,
|
||||||
decoration: url != null ? TextDecoration.underline : null,
|
decoration: active ? TextDecoration.underline : null,
|
||||||
decorationColor: url != null ? const Color(0xFF2563AC) : null,
|
decorationColor: active ? const Color(0xFF2563AC) : null,
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1487,3 +1529,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('提交'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -813,13 +813,34 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
|
|||||||
...o.items.asMap().entries.map((e) {
|
...o.items.asMap().entries.map((e) {
|
||||||
final i = e.key;
|
final i = e.key;
|
||||||
final item = e.value;
|
final item = e.value;
|
||||||
|
// 产地/保质期/储存拼成一行辅助文本
|
||||||
|
final attrs = [
|
||||||
|
if (item.productOrigin?.isNotEmpty ?? false) item.productOrigin!,
|
||||||
|
if (item.productShelfLife?.isNotEmpty ?? false) item.productShelfLife!,
|
||||||
|
if (item.productStorage?.isNotEmpty ?? false) item.productStorage!,
|
||||||
|
].join(' · ');
|
||||||
return TableRow(
|
return TableRow(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: i.isEven ? Colors.white : const Color(0xFFFAFAFA)),
|
color: i.isEven ? Colors.white : const Color(0xFFFAFAFA)),
|
||||||
children: [
|
children: [
|
||||||
_TableCell('${i + 1}'),
|
_TableCell('${i + 1}'),
|
||||||
_TableCell(item.productCode ?? '-'),
|
_TableCell(item.productCode ?? '-'),
|
||||||
_TableCell(item.productName ?? '-'),
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(item.productName ?? '-',
|
||||||
|
style: const TextStyle(fontSize: 13)),
|
||||||
|
if (attrs.isNotEmpty)
|
||||||
|
Text(attrs,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Color(0xFF888888))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
_TableCell(item.productSeries ?? '-'),
|
_TableCell(item.productSeries ?? '-'),
|
||||||
_TableCell(item.productSpec ?? '-'),
|
_TableCell(item.productSpec ?? '-'),
|
||||||
_TableCell(item.quantity.toStringAsFixed(3)),
|
_TableCell(item.quantity.toStringAsFixed(3)),
|
||||||
|
|||||||
+29
-29
@@ -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="header-meta">生成于 2026-06-08 · 真相源 todo/todo.json</div>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat-pill"><strong>15</strong>全部</div>
|
<div class="stat-pill"><strong>15</strong>全部</div>
|
||||||
<div class="stat-pill"><strong>7</strong>未完成</div>
|
<div class="stat-pill"><strong>5</strong>未完成</div>
|
||||||
<div class="stat-pill"><strong>8</strong>已完成</div>
|
<div class="stat-pill"><strong>10</strong>已完成</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -132,7 +132,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
|
|||||||
|
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
|
|
||||||
<div class="section-title">📋 未完成(7)</div>
|
<div class="section-title">📋 未完成(5)</div>
|
||||||
<ul class="todo-list" id="pendingList">
|
<ul class="todo-list" id="pendingList">
|
||||||
|
|
||||||
<li class="todo-card"
|
<li class="todo-card"
|
||||||
@@ -229,31 +229,18 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<li class="todo-card"
|
<div class="done-section">
|
||||||
data-id="14"
|
<div class="section-title done-title">✅ 已完成(10)</div>
|
||||||
data-level="low"
|
<button class="done-toggle" id="doneToggle">▾ 展开已完成列表</button>
|
||||||
data-tags="前端,iOS,Android"
|
<ul class="todo-list hidden" id="doneList">
|
||||||
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>
|
<li class="todo-card done"
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="todo-card"
|
|
||||||
data-id="18"
|
data-id="18"
|
||||||
data-level="low"
|
data-level="low"
|
||||||
data-tags="前端,iOS,Android"
|
data-tags="前端,iOS,Android"
|
||||||
data-done="0">
|
data-done="1">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="item-title">基础数据管理 UI 增加 4 个字典维护 Tab</span>
|
<span class="item-title">基础数据管理 UI 增加 4 个字典维护 Tab</span>
|
||||||
<span class="tag t-low">一般 / 优化</span>
|
<span class="tag t-low">一般 / 优化</span>
|
||||||
@@ -263,16 +250,29 @@ 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="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">
|
<div class="item-meta">
|
||||||
<span class="meta-date">🕐 2026-06-08</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="done-section">
|
<li class="todo-card done"
|
||||||
<div class="section-title done-title">✅ 已完成(8)</div>
|
data-id="14"
|
||||||
<button class="done-toggle" id="doneToggle">▾ 展开已完成列表</button>
|
data-level="low"
|
||||||
<ul class="todo-list hidden" id="doneList">
|
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>
|
||||||
|
|
||||||
<li class="todo-card done"
|
<li class="todo-card done"
|
||||||
data-id="10"
|
data-id="10"
|
||||||
|
|||||||
+7
-7
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"title": "酒库管理系统 — 项目 TODO",
|
"title": "酒库管理系统 — 项目 TODO",
|
||||||
"updated_at": "2026-06-07T23:28:53.001Z"
|
"updated_at": "2026-06-08T00:06:15.371Z"
|
||||||
},
|
},
|
||||||
"seq": 18,
|
"seq": 18,
|
||||||
"items": [
|
"items": [
|
||||||
@@ -160,9 +160,9 @@
|
|||||||
"Android"
|
"Android"
|
||||||
],
|
],
|
||||||
"created_at": "2026-06-07T18:12:50.299Z",
|
"created_at": "2026-06-07T18:12:50.299Z",
|
||||||
"done": false,
|
"done": true,
|
||||||
"completed_at": null,
|
"completed_at": "2026-06-08T00:06:15.299Z",
|
||||||
"version": null
|
"version": "v1.0.24"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 15,
|
"id": 15,
|
||||||
@@ -220,9 +220,9 @@
|
|||||||
"Android"
|
"Android"
|
||||||
],
|
],
|
||||||
"created_at": "2026-06-07T22:52:13.283Z",
|
"created_at": "2026-06-07T22:52:13.283Z",
|
||||||
"done": false,
|
"done": true,
|
||||||
"completed_at": null,
|
"completed_at": "2026-06-08T00:06:15.371Z",
|
||||||
"version": null
|
"version": "v1.0.24"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user