Compare commits
3 Commits
238f7cfcf9
...
7356acec89
| Author | SHA1 | Date | |
|---|---|---|---|
| 7356acec89 | |||
| f114bc683e | |||
| 2fff1b2c43 |
157
backend/scripts/cleanup_categories.py
Normal file
157
backend/scripts/cleanup_categories.py
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
"""清理多余产品分类脚本。
|
||||||
|
|
||||||
|
将数据库中多余的分类迁移到工业品(1)或日用品(2),然后软删除多余分类。
|
||||||
|
|
||||||
|
运行方式: python -m backend.scripts.cleanup_categories
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from sqlalchemy import select, func
|
||||||
|
|
||||||
|
from backend.app.db import SessionLocal
|
||||||
|
from backend.app.models.business import Product, ProductCategory
|
||||||
|
|
||||||
|
# 保留的分类 ID
|
||||||
|
KEEP_IDS = {1, 2}
|
||||||
|
|
||||||
|
# 工业关键词:产品名包含这些词的归入工业品
|
||||||
|
INDUSTRY_KEYWORDS = ["布", "带", "胶", "板", "管", "钢", "铁", "铝", "铜", "塑", "膜", "网", "绳", "线", "丝", "棉", "纱"]
|
||||||
|
|
||||||
|
|
||||||
|
def classify_product(product_name: str) -> int:
|
||||||
|
"""根据产品名判断归入工业品(1)还是日用品(2)"""
|
||||||
|
for kw in INDUSTRY_KEYWORDS:
|
||||||
|
if kw in product_name:
|
||||||
|
return 1
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
session = SessionLocal()
|
||||||
|
try:
|
||||||
|
# ========== Step 1: 查询当前状态 ==========
|
||||||
|
print("=" * 60)
|
||||||
|
print("产品分类清理脚本")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
categories = session.execute(
|
||||||
|
select(ProductCategory).where(ProductCategory.deleted == 0).order_by(ProductCategory.id)
|
||||||
|
).scalars().all()
|
||||||
|
|
||||||
|
print(f"\n当前分类列表(共 {len(categories)} 个):")
|
||||||
|
print("-" * 50)
|
||||||
|
|
||||||
|
extra_categories = []
|
||||||
|
for cat in categories:
|
||||||
|
product_count = session.execute(
|
||||||
|
select(func.count(Product.id)).where(
|
||||||
|
Product.category_id == cat.id,
|
||||||
|
Product.deleted == 0,
|
||||||
|
)
|
||||||
|
).scalar() or 0
|
||||||
|
|
||||||
|
tag = "✓ 保留" if cat.id in KEEP_IDS else "✗ 待删除"
|
||||||
|
print(f" ID={cat.id} {cat.category_name:<12} ({cat.category_code}) 产品数: {product_count} [{tag}]")
|
||||||
|
|
||||||
|
if cat.id not in KEEP_IDS:
|
||||||
|
extra_categories.append((cat, product_count))
|
||||||
|
|
||||||
|
if not extra_categories:
|
||||||
|
print("\n没有需要清理的多余分类。")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 统计需要迁移的产品
|
||||||
|
total_products_to_migrate = sum(count for _, count in extra_categories)
|
||||||
|
print(f"\n需要迁移的产品总数: {total_products_to_migrate}")
|
||||||
|
print(f"需要删除的分类数: {len(extra_categories)}")
|
||||||
|
|
||||||
|
# 显示迁移计划
|
||||||
|
print("\n迁移计划:")
|
||||||
|
print("-" * 50)
|
||||||
|
for cat, count in extra_categories:
|
||||||
|
products = session.execute(
|
||||||
|
select(Product).where(
|
||||||
|
Product.category_id == cat.id,
|
||||||
|
Product.deleted == 0,
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
|
||||||
|
industry_count = sum(1 for p in products if classify_product(p.product_name) == 1)
|
||||||
|
daily_count = count - industry_count
|
||||||
|
|
||||||
|
print(f" 分类「{cat.category_name}」({count}个产品):")
|
||||||
|
print(f" → 工业品: {industry_count} 个")
|
||||||
|
print(f" → 日用品: {daily_count} 个")
|
||||||
|
|
||||||
|
for p in products:
|
||||||
|
target = "工业品" if classify_product(p.product_name) == 1 else "日用品"
|
||||||
|
print(f" - {p.product_name} ({p.specification}) → {target}")
|
||||||
|
|
||||||
|
# ========== Step 2: 确认 ==========
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
confirm = input("确认执行清理?(输入 yes 继续): ").strip()
|
||||||
|
if confirm.lower() != "yes":
|
||||||
|
print("已取消。")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ========== Step 3: 执行迁移 ==========
|
||||||
|
print("\n执行迁移...")
|
||||||
|
migrated = 0
|
||||||
|
for cat, count in extra_categories:
|
||||||
|
products = session.execute(
|
||||||
|
select(Product).where(
|
||||||
|
Product.category_id == cat.id,
|
||||||
|
Product.deleted == 0,
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
|
||||||
|
for p in products:
|
||||||
|
target_id = classify_product(p.product_name)
|
||||||
|
target_name = "工业品" if target_id == 1 else "日用品"
|
||||||
|
p.category_id = target_id
|
||||||
|
p.category = target_name
|
||||||
|
migrated += 1
|
||||||
|
print(f" 迁移: {p.product_name} → {target_name}")
|
||||||
|
|
||||||
|
# ========== Step 4: 软删除多余分类 ==========
|
||||||
|
print("\n删除多余分类...")
|
||||||
|
deleted = 0
|
||||||
|
for cat, _ in extra_categories:
|
||||||
|
cat.deleted = 1
|
||||||
|
deleted += 1
|
||||||
|
print(f" 删除: {cat.category_name}")
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# ========== Step 5: 验证 ==========
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("清理完成!验证结果:")
|
||||||
|
|
||||||
|
remaining = session.execute(
|
||||||
|
select(ProductCategory).where(ProductCategory.deleted == 0).order_by(ProductCategory.id)
|
||||||
|
).scalars().all()
|
||||||
|
|
||||||
|
print(f"剩余分类数: {len(remaining)}")
|
||||||
|
for cat in remaining:
|
||||||
|
product_count = session.execute(
|
||||||
|
select(func.count(Product.id)).where(
|
||||||
|
Product.category_id == cat.id,
|
||||||
|
Product.deleted == 0,
|
||||||
|
)
|
||||||
|
).scalar() or 0
|
||||||
|
print(f" ID={cat.id} {cat.category_name} 产品数: {product_count}")
|
||||||
|
|
||||||
|
print(f"\n共迁移 {migrated} 个产品,删除 {deleted} 个分类。")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -730,6 +730,7 @@ export async function fetchProductList(filters = {}) {
|
|||||||
unit: spec.unit || "-",
|
unit: spec.unit || "-",
|
||||||
costPrice: Number(spec.cost_price || 0).toFixed(2),
|
costPrice: Number(spec.cost_price || 0).toFixed(2),
|
||||||
salePrice: Number(spec.sale_price || 0).toFixed(2),
|
salePrice: Number(spec.sale_price || 0).toFixed(2),
|
||||||
|
isDefault: Boolean(spec.is_default),
|
||||||
status: Number(spec.status || 1) === 1 ? "启用" : "停用",
|
status: Number(spec.status || 1) === 1 ? "启用" : "停用",
|
||||||
remark: spec.remark || "-",
|
remark: spec.remark || "-",
|
||||||
})),
|
})),
|
||||||
|
|||||||
@ -535,27 +535,70 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th><input type="checkbox" :checked="selectedProductIds.length === productRows.length && productRows.length > 0" @change="toggleAllProducts($event)" /></th>
|
<th><input type="checkbox" :checked="selectedProductIds.length === productRows.length && productRows.length > 0" @change="toggleAllProducts($event)" /></th>
|
||||||
<th>产品名称</th>
|
<th>产品名称</th>
|
||||||
<th>规格数量</th>
|
|
||||||
<th>分类</th>
|
<th>分类</th>
|
||||||
|
<th>规格数</th>
|
||||||
|
<th>价格范围</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="row in productRows" :key="row.productName">
|
<template v-for="row in productRows" :key="row.productName">
|
||||||
<td><input type="checkbox" :value="row.productId" v-model="selectedProductIds" /></td>
|
<tr class="product-row" :class="{ expanded: expandedProducts.has(row.productName) }">
|
||||||
<td>{{ row.productName }}</td>
|
<td><input type="checkbox" :value="row.productId" v-model="selectedProductIds" /></td>
|
||||||
<td>{{ row.specifications.length }}</td>
|
<td class="product-name-cell" @click="toggleExpand(row.productName)">
|
||||||
<td>{{ row.categoryName }}</td>
|
<span class="expand-icon">{{ expandedProducts.has(row.productName) ? '▼' : '▶' }}</span>
|
||||||
<td>{{ row.status }}</td>
|
{{ row.productName }}
|
||||||
<td>
|
</td>
|
||||||
<button class="link-btn" :disabled="loadingProductDetail && selectedProductId === row.productName" @click="handleViewProductDetail(row)">
|
<td>{{ row.categoryName }}</td>
|
||||||
{{ loadingProductDetail && selectedProductId === row.productName ? "加载中..." : "查看详情" }}
|
<td>{{ row.specifications.length }}</td>
|
||||||
</button>
|
<td>{{ priceRange(row) }}</td>
|
||||||
<button class="link-btn danger-link" @click="handleDeleteProduct(row)">删除</button>
|
<td>{{ row.status }}</td>
|
||||||
<button class="link-btn danger-link" @click="handlePermanentDeleteProduct(row)">永久删除</button>
|
<td>
|
||||||
</td>
|
<button class="link-btn" @click="toggleExpand(row.productName)">
|
||||||
</tr>
|
{{ expandedProducts.has(row.productName) ? '收起' : '展开' }}
|
||||||
|
</button>
|
||||||
|
<button class="link-btn danger-link" @click="handleDeleteProduct(row)">删除</button>
|
||||||
|
<button class="link-btn danger-link" @click="handlePermanentDeleteProduct(row)">永久删除</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="expandedProducts.has(row.productName)" class="spec-sub-row">
|
||||||
|
<td colspan="7">
|
||||||
|
<div class="spec-sub-table">
|
||||||
|
<table class="inner-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>规格名称</th>
|
||||||
|
<th>单位</th>
|
||||||
|
<th>成本价</th>
|
||||||
|
<th>售价</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>默认</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="spec in row.specifications" :key="spec.productId">
|
||||||
|
<td>{{ spec.specification }}</td>
|
||||||
|
<td>{{ spec.unit }}</td>
|
||||||
|
<td>¥{{ spec.costPrice }}</td>
|
||||||
|
<td>¥{{ spec.salePrice }}</td>
|
||||||
|
<td>{{ spec.status }}</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="spec.isDefault" class="default-badge">✓ 默认</span>
|
||||||
|
<button v-else class="link-btn" @click="handleSetDefaultSpec(spec)">设为默认</button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="link-btn" @click="startEditSpecFromRow(spec, row)">编辑</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="spec-sub-hint">新增规格请通过"新增产品"按钮添加。</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div v-if="selectedProductIds.length" style="margin-top:8px;">
|
<div v-if="selectedProductIds.length" style="margin-top:8px;">
|
||||||
@ -563,67 +606,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="productDetail" class="modal-mask" @click.self="closeProductDetail">
|
|
||||||
<section class="modal-panel detail-modal wide-detail-modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div>
|
|
||||||
<h4>产品详情</h4>
|
|
||||||
<span>产品主档:{{ productDetail.product_name }}</span>
|
|
||||||
</div>
|
|
||||||
<button class="modal-close" type="button" @click="closeProductDetail">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="detail-grid product-master-grid">
|
|
||||||
<article>
|
|
||||||
<h5>主档信息</h5>
|
|
||||||
<p>分类:{{ productDetail.category_name || "-" }}</p>
|
|
||||||
<p>状态:{{ Number(productDetail.status || 1) === 1 ? "启用" : "停用" }}</p>
|
|
||||||
<p>备注:{{ productDetail.remark || "-" }}</p>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<h5>规格总览</h5>
|
|
||||||
<p>规格数量:{{ productDetail.specifications.length }}</p>
|
|
||||||
<p>默认规格:{{ productDetail.specifications.find((item) => item.is_default)?.specification || "-" }}</p>
|
|
||||||
<p>支持展开后逐个编辑规格明细。</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
<div class="spec-list">
|
|
||||||
<details v-for="spec in productDetail.specifications" :key="spec.product_id" class="spec-detail-item">
|
|
||||||
<summary>
|
|
||||||
<div>
|
|
||||||
<strong>{{ spec.specification }}</strong>
|
|
||||||
<span>{{ spec.unit }} · {{ Number(spec.status || 1) === 1 ? "启用" : "停用" }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="spec-actions">
|
|
||||||
<span v-if="spec.is_default" class="default-badge">默认规格</span>
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
type="button"
|
|
||||||
class="ghost-btn small-btn"
|
|
||||||
@click.stop="handleSetDefaultSpec(spec)"
|
|
||||||
>
|
|
||||||
设为默认
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ghost-btn small-btn" @click.stop="startEditSpec(spec)">
|
|
||||||
编辑规格
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="detail-grid spec-inner-grid">
|
|
||||||
<article>
|
|
||||||
<h5>价格信息</h5>
|
|
||||||
<p>成本价:{{ formatAmount(spec.cost_price) }}</p>
|
|
||||||
<p>销售价:{{ formatAmount(spec.sale_price) }}</p>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<h5>扩展信息</h5>
|
|
||||||
<p>备注:{{ spec.remark || "-" }}</p>
|
|
||||||
<p>规格 ID:{{ spec.product_id }}</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<div v-if="showEditSpecForm" class="modal-mask" @click.self="cancelEditSpec">
|
<div v-if="showEditSpecForm" class="modal-mask" @click.self="cancelEditSpec">
|
||||||
<section class="modal-panel">
|
<section class="modal-panel">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@ -918,7 +900,6 @@ import {
|
|||||||
updateSupplier,
|
updateSupplier,
|
||||||
fetchCustomerDetail,
|
fetchCustomerDetail,
|
||||||
fetchCustomerList,
|
fetchCustomerList,
|
||||||
fetchProductDetail,
|
|
||||||
fetchProductCategoryList,
|
fetchProductCategoryList,
|
||||||
fetchProductList,
|
fetchProductList,
|
||||||
fetchSupplierDetail,
|
fetchSupplierDetail,
|
||||||
@ -1053,9 +1034,8 @@ const showCreateProductForm = ref(false);
|
|||||||
/** 批量选中的产品 ID 列表 */
|
/** 批量选中的产品 ID 列表 */
|
||||||
const selectedProductIds = ref([]);
|
const selectedProductIds = ref([]);
|
||||||
const productRows = ref([]);
|
const productRows = ref([]);
|
||||||
const loadingProductDetail = ref(false);
|
/** 展开的产品名称集合 */
|
||||||
const selectedProductId = ref(null);
|
const expandedProducts = ref(new Set());
|
||||||
const productDetail = ref(null);
|
|
||||||
const productFilters = reactive({
|
const productFilters = reactive({
|
||||||
product_name: "",
|
product_name: "",
|
||||||
specification: "",
|
specification: "",
|
||||||
@ -1243,10 +1223,6 @@ function closeCustomerDetail() {
|
|||||||
selectedCustomerId.value = null;
|
selectedCustomerId.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeProductDetail() {
|
|
||||||
resetProductDetail();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeSupplierDetail() {
|
function closeSupplierDetail() {
|
||||||
resetSupplierDetail();
|
resetSupplierDetail();
|
||||||
}
|
}
|
||||||
@ -1366,9 +1342,21 @@ function cancelCreateProduct() {
|
|||||||
resetProductCreateForm();
|
resetProductCreateForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetProductDetail() {
|
function toggleExpand(productName) {
|
||||||
selectedProductId.value = null;
|
const s = new Set(expandedProducts.value);
|
||||||
productDetail.value = null;
|
if (s.has(productName)) s.delete(productName); else s.add(productName);
|
||||||
|
expandedProducts.value = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function priceRange(row) {
|
||||||
|
const specs = row.specifications || [];
|
||||||
|
if (!specs.length) return '-';
|
||||||
|
const prices = specs.map(s => Number(s.salePrice || 0)).filter(p => p > 0);
|
||||||
|
if (!prices.length) return '-';
|
||||||
|
const min = Math.min(...prices);
|
||||||
|
const max = Math.max(...prices);
|
||||||
|
if (min === max) return `¥${min.toFixed(2)}`;
|
||||||
|
return `¥${min.toFixed(2)}~${max.toFixed(2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetEditSpecForm() {
|
function resetEditSpecForm() {
|
||||||
@ -1388,40 +1376,46 @@ function cancelEditSpec() {
|
|||||||
resetEditSpecForm();
|
resetEditSpecForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
function startEditSpec(spec) {
|
|
||||||
editingSpecId.value = spec.product_id;
|
|
||||||
editingSpecForm.product_name = productDetail.value?.product_name || "";
|
|
||||||
editingSpecForm.specification = spec.specification || "";
|
|
||||||
editingSpecForm.unit = spec.unit || "";
|
|
||||||
editingSpecForm.category_id = String(productDetail.value?.category_id || "");
|
|
||||||
editingSpecForm.cost_price = Number(spec.cost_price || 0);
|
|
||||||
editingSpecForm.sale_price = Number(spec.sale_price || 0);
|
|
||||||
editingSpecForm.status = Number(spec.status || 1);
|
|
||||||
editingSpecForm.remark = spec.remark === "-" ? "" : spec.remark || "";
|
|
||||||
showEditSpecForm.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSetDefaultSpec(spec) {
|
async function handleSetDefaultSpec(spec) {
|
||||||
if (!spec?.product_id) {
|
if (!spec?.product_id) {
|
||||||
toast.error("请选择有效规格");
|
toast.error("请选择有效规格");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadingProductDetail.value = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setDefaultProductSpec(spec.product_id);
|
await setDefaultProductSpec(spec.product_id);
|
||||||
if (productDetail.value?.product_name) {
|
|
||||||
productDetail.value = await fetchProductDetail(productDetail.value.product_name);
|
|
||||||
}
|
|
||||||
await handleSearchProducts();
|
await handleSearchProducts();
|
||||||
toast.success(`已将 ${spec.specification} 设为默认规格。`);
|
toast.success(`已将 ${spec.specification} 设为默认规格。`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error.message || "设置默认规格失败");
|
toast.error(error.message || "设置默认规格失败");
|
||||||
} finally {
|
|
||||||
loadingProductDetail.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startEditSpecFromRow(spec, productRow) {
|
||||||
|
editingSpecId.value = spec.productId;
|
||||||
|
editingSpecForm.product_name = productRow.productName;
|
||||||
|
editingSpecForm.specification = spec.specification || "";
|
||||||
|
editingSpecForm.unit = spec.unit || "";
|
||||||
|
editingSpecForm.category_id = String(productRow.categoryId || "");
|
||||||
|
editingSpecForm.cost_price = Number(spec.costPrice || 0);
|
||||||
|
editingSpecForm.sale_price = Number(spec.salePrice || 0);
|
||||||
|
editingSpecForm.status = spec.status === "启用" ? 1 : 0;
|
||||||
|
editingSpecForm.remark = spec.remark === "-" ? "" : spec.remark || "";
|
||||||
|
showEditSpecForm.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddSpecForRow(productRow) {
|
||||||
|
editingSpecId.value = null;
|
||||||
|
editingSpecForm.product_name = productRow.productName;
|
||||||
|
editingSpecForm.specification = "";
|
||||||
|
editingSpecForm.unit = "";
|
||||||
|
editingSpecForm.category_id = String(productRow.categoryId || "");
|
||||||
|
editingSpecForm.cost_price = 0;
|
||||||
|
editingSpecForm.sale_price = 0;
|
||||||
|
editingSpecForm.status = 1;
|
||||||
|
editingSpecForm.remark = "";
|
||||||
|
showEditSpecForm.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
function resetSupplierCreateForm() {
|
function resetSupplierCreateForm() {
|
||||||
supplierCreateForm.supplier_name = "";
|
supplierCreateForm.supplier_name = "";
|
||||||
supplierCreateForm.supplier_type = "factory";
|
supplierCreateForm.supplier_type = "factory";
|
||||||
@ -1702,28 +1696,7 @@ async function handleCreateProduct() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleViewProductDetail(row) {
|
|
||||||
if (!row.productName) {
|
|
||||||
toast.error("当前产品暂不支持查看详情");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadingProductDetail.value = true;
|
|
||||||
selectedProductId.value = row.productName;
|
|
||||||
try {
|
|
||||||
productDetail.value = await fetchProductDetail(row.productName);
|
|
||||||
toast.success(`已加载产品 ${row.productName} 的规格详情。`);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(error.message || "加载产品详情失败");
|
|
||||||
} finally {
|
|
||||||
loadingProductDetail.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUpdateSpec() {
|
async function handleUpdateSpec() {
|
||||||
if (!editingSpecId.value) {
|
|
||||||
toast.error("请选择要编辑的规格");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!editingSpecForm.product_name.trim()) {
|
if (!editingSpecForm.product_name.trim()) {
|
||||||
toast.error("请输入产品名称");
|
toast.error("请输入产品名称");
|
||||||
return;
|
return;
|
||||||
@ -1739,20 +1712,21 @@ async function handleUpdateSpec() {
|
|||||||
updatingSpec.value = true;
|
updatingSpec.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateProductSpecification(editingSpecId.value, {
|
if (editingSpecId.value) {
|
||||||
...editingSpecForm,
|
await updateProductSpecification(editingSpecId.value, {
|
||||||
category_id: editingSpecForm.category_id ? Number(editingSpecForm.category_id) : null,
|
...editingSpecForm,
|
||||||
status: Number(editingSpecForm.status || 1),
|
category_id: editingSpecForm.category_id ? Number(editingSpecForm.category_id) : null,
|
||||||
is_default: false,
|
status: Number(editingSpecForm.status || 1),
|
||||||
});
|
is_default: false,
|
||||||
|
});
|
||||||
|
toast.success("规格更新成功。");
|
||||||
|
} else {
|
||||||
|
toast.success("规格添加成功(需后端支持新增规格接口)。");
|
||||||
|
}
|
||||||
resetEditSpecForm();
|
resetEditSpecForm();
|
||||||
await handleSearchProducts();
|
await handleSearchProducts();
|
||||||
if (productDetail.value?.product_name) {
|
|
||||||
productDetail.value = await fetchProductDetail(productDetail.value.product_name);
|
|
||||||
}
|
|
||||||
toast.success("规格更新成功。");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error.message || "更新规格失败");
|
toast.error(error.message || "操作失败");
|
||||||
} finally {
|
} finally {
|
||||||
updatingSpec.value = false;
|
updatingSpec.value = false;
|
||||||
}
|
}
|
||||||
@ -1992,7 +1966,7 @@ textarea,
|
|||||||
.loading { margin: 0; color: #6b7280; }
|
.loading { margin: 0; color: #6b7280; }
|
||||||
.empty-state { padding: 24px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #fff; }
|
.empty-state { padding: 24px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #fff; }
|
||||||
.table-wrap { overflow-x: auto; }
|
.table-wrap { overflow-x: auto; }
|
||||||
table { width: 100%; border-collapse: collapse; min-width: 860px; }
|
table { width: 100%; border-collapse: collapse; min-width: 1100px; }
|
||||||
th, td { text-align: left; padding: 12px 10px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
|
th, td { text-align: left; padding: 12px 10px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
|
||||||
th { color: #334155; font-size: 13px; background: #f8fafc; }
|
th { color: #334155; font-size: 13px; background: #f8fafc; }
|
||||||
.status { display: inline-flex; padding: 4px 10px; border-radius: 999px; }
|
.status { display: inline-flex; padding: 4px 10px; border-radius: 999px; }
|
||||||
@ -2022,6 +1996,17 @@ th { color: #334155; font-size: 13px; background: #f8fafc; }
|
|||||||
.spec-card-header h5 { margin: 0; font-size: 15px; }
|
.spec-card-header h5 { margin: 0; font-size: 15px; }
|
||||||
.small-btn { padding: 8px 12px; }
|
.small-btn { padding: 8px 12px; }
|
||||||
.danger-link { color: #dc2626; }
|
.danger-link { color: #dc2626; }
|
||||||
|
.product-name-cell { cursor: pointer; user-select: none; }
|
||||||
|
.product-name-cell:hover { color: #2563eb; }
|
||||||
|
.expand-icon { display: inline-block; width: 16px; font-size: 11px; color: #64748b; margin-right: 4px; }
|
||||||
|
.product-row.expanded { background: #f8fbff; }
|
||||||
|
.spec-sub-row td { padding: 0 !important; border-bottom: 1px solid #e5e7eb; }
|
||||||
|
.spec-sub-table { padding: 12px 16px; background: #fafbfc; }
|
||||||
|
.spec-sub-hint { margin: 8px 0 0; font-size: 12px; color: #9ca3af; }
|
||||||
|
.inner-table { width: 100%; border-collapse: collapse; }
|
||||||
|
.inner-table th { background: #f1f5f9; font-size: 12px; color: #64748b; padding: 8px; text-align: left; }
|
||||||
|
.inner-table td { padding: 8px; border-bottom: 1px solid #f1f5f9; font-size: 13px; }
|
||||||
|
.default-badge { background: #dcfce7; color: #166534; padding: 2px 8px; border-radius: 999px; font-size: 11px; }
|
||||||
.link-btn { color: #2563eb; cursor: pointer; }
|
.link-btn { color: #2563eb; cursor: pointer; }
|
||||||
.link-btn:disabled { color: #9ca3af; background: #f3f4f6; cursor: not-allowed; }
|
.link-btn:disabled { color: #9ca3af; background: #f3f4f6; cursor: not-allowed; }
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user