dingdanquanliucheng/frontend/web-admin/src/views/MasterDataPage.vue
taiyi f114bc683e refactor: 产品管理表格重构为主表+展开子表模式
- 产品行可展开显示所有规格的成本价/售价/状态
- 价格范围显示(多规格时 ¥最小~最大)
- 展开行内支持编辑规格和设为默认
- 移除产品详情弹窗及相关冗余代码(净减 23 行)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 20:18:59 +08:00

2027 lines
76 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
MasterDataPage.vue - 基础资料管理页面
职责统一管理客户产品分类产品和工厂/供应商四大基础数据模块
通过顶部 Tab 切换展示不同模块每个模块支持查询新增编辑查看详情和导入
产品管理支持多规格创建和规格明细编辑
-->
<template>
<section class="page admin-page">
<div class="page-header">
<div>
<h2>基础资料</h2>
<p class="subline">本页已提速接入客户产品分类产品和供应商的真实管理链路</p>
</div>
</div>
<div class="module-switcher">
<button
v-for="tab in tabs"
:key="tab.key"
:class="{ active: activeTab === tab.key }"
@click="switchTab(tab.key)"
>
{{ tab.label }}
</button>
</div>
<section class="overview-grid">
<article v-for="module in masterDataModules" :key="module.title" class="overview-card">
<h3>{{ module.title }}</h3>
<p>{{ module.description }}</p>
</article>
<article class="overview-card order-card">
<h3>订单快捷入口</h3>
<p>快速进入订单审批、履约调度和业务跟进入口,方便从基础资料跳转到订单链路。</p>
<div class="card-actions">
<RouterLink class="jump-link" to="/approvals">审批中心</RouterLink>
<RouterLink class="jump-link secondary" to="/business">履约调度</RouterLink>
<RouterLink class="jump-link secondary" to="/reports">报表中心</RouterLink>
</div>
</article>
</section>
<section v-if="activeTab === 'customers'" class="module-panel">
<div class="panel-header">
<h3>客户管理</h3>
<span>支持真实筛选、查看详情和新增客户</span>
</div>
<section class="toolbar toolbar-4">
<label>
<span>客户姓名</span>
<input v-model.trim="customerFilters.customer_name" type="text" placeholder="请输入客户姓名" />
</label>
<label>
<span>客户手机号</span>
<input v-model.trim="customerFilters.mobile" type="text" placeholder="请输入客户手机号" />
</label>
<label>
<span>客户类型</span>
<select v-model="customerFilters.customer_type">
<option value="">全部类型</option>
<option value="channel">渠道客户</option>
<option value="retail">零售客户</option>
<option value="project">项目客户</option>
</select>
</label>
<label>
<span>结算方式</span>
<select v-model="customerFilters.settlement_type">
<option value="">全部方式</option>
<option value="monthly">月结</option>
<option value="cash">现结</option>
<option value="delivered">送达结算</option>
</select>
</label>
<div class="toolbar-actions">
<button class="primary-btn" :disabled="loadingCustomers" @click="handleSearchCustomers">
{{ loadingCustomers ? "查询中..." : "查询客户" }}
</button>
<button class="ghost-btn" :disabled="loadingCustomers" @click="handleResetCustomers">重置</button>
<button class="ghost-btn" @click="openCreateCustomerModal">新增客户</button>
<button class="ghost-btn" @click="openImportCustomerModal">导入客户</button>
</div>
</section>
<div v-if="showCreateCustomerForm" class="modal-mask" @click.self="cancelCreateCustomer">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>新增客户</h4>
</div>
<button class="modal-close" type="button" @click="cancelCreateCustomer">×</button>
</div>
<div class="form-grid">
<label>
<span>客户姓名</span>
<input v-model.trim="customerCreateForm.customer_name" type="text" placeholder="请输入客户姓名" />
</label>
<label>
<span>客户手机号</span>
<input v-model.trim="customerCreateForm.mobile" type="text" placeholder="请输入客户手机号" />
</label>
<label>
<span>客户地址</span>
<input v-model.trim="customerCreateForm.address" type="text" placeholder="请输入客户地址" />
</label>
<label>
<span>归属业务员</span>
<select v-model.number="customerCreateForm.salesman_id">
<option :value="0">请选择业务员</option>
<option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</option>
</select>
</label>
<label>
<span>客户类型</span>
<select v-model="customerCreateForm.customer_type">
<option value="channel">渠道客户</option>
<option value="retail">零售客户</option>
<option value="project">项目客户</option>
</select>
</label>
<label>
<span>结算方式</span>
<select v-model="customerCreateForm.settlement_type">
<option value="monthly">月结</option>
<option value="cash">现结</option>
<option value="delivered">送达结算</option>
</select>
</label>
<label>
<span>账期天数</span>
<input v-model.number="customerCreateForm.settlement_days" type="number" min="0" />
</label>
<label>
<span>信用额度</span>
<input v-model.number="customerCreateForm.credit_limit" type="number" min="0" step="0.01" />
</label>
<label class="full-width">
<span>备注</span>
<textarea v-model.trim="customerCreateForm.remark" rows="3" placeholder="请输入备注"></textarea>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelCreateCustomer">取消新增</button>
<button class="primary-btn" :disabled="creatingCustomer" @click="handleCreateCustomer">
{{ creatingCustomer ? "创建中..." : "提交创建" }}
</button>
</div>
</section>
</div>
<div v-if="showImportCustomerForm" class="modal-mask" @click.self="cancelImportCustomer">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>客户导入</h4>
</div>
<button class="modal-close" type="button" @click="cancelImportCustomer">×</button>
</div>
<div class="form-grid">
<label class="full-width">
<span>文件路径</span>
<input v-model.trim="customerImportForm.file_url" type="text" placeholder="请输入本地文件路径或 OSS 公网 URL" />
</label>
<label>
<span>导入模式</span>
<select v-model="customerImportForm.import_mode">
<option value="skip_duplicate">跳过重复</option>
<option value="cover_duplicate">覆盖重复</option>
</select>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelImportCustomer">取消导入</button>
<button class="primary-btn" :disabled="importingCustomer" @click="handleImportCustomers">
{{ importingCustomer ? "导入中..." : "开始导入" }}
</button>
</div>
</section>
</div>
<p v-if="loadingCustomers" class="loading">正在加载客户列表...</p>
<div v-else-if="!customerRows.length" class="empty-state">当前没有客户数据</div>
<div v-else class="table-wrap">
<table>
<thead>
<tr>
<th>客户姓名</th>
<th>手机号</th>
<th>客户类型</th>
<th>结算方式</th>
<th>归属业务员</th>
<th>欠款金额</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in customerRows" :key="row.customerId || row.name">
<td>{{ row.name }}</td>
<td>{{ row.mobile }}</td>
<td>{{ row.customerType }}</td>
<td>{{ row.settlement }}</td>
<td>{{ row.salesman }}</td>
<td><span class="status status-warm">{{ row.arrears }}</span></td>
<td>{{ row.status }}</td>
<td>
<button class="link-btn" :disabled="customerDetailLoading" @click="handleViewCustomerDetail(row)">
{{ customerDetailLoading && selectedCustomerId === row.customerId ? "加载中..." : "查看详情" }}
</button>
<button class="link-btn" @click="startEditCustomer(row)">编辑</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="customerDetail" class="modal-mask" @click.self="closeCustomerDetail">
<section class="modal-panel detail-modal">
<div class="modal-header">
<div>
<h4>客户详情</h4>
<span>客户 ID{{ customerDetail.customer_id }}</span>
</div>
<button class="modal-close" type="button" @click="closeCustomerDetail">×</button>
</div>
<div class="detail-grid">
<article>
<h5>基本信息</h5>
<p>客户姓名:{{ customerDetail.customer_name }}</p>
<p>手机号:{{ customerDetail.mobile }}</p>
<p>客户地址:{{ customerDetail.address || "-" }}</p>
</article>
<article>
<h5>订单联动</h5>
<p>可从审批中心查看该客户关联订单审批。</p>
<RouterLink class="card-link" to="/approvals">进入审批中心</RouterLink>
<RouterLink class="card-link" to="/business">进入履约调度</RouterLink>
</article>
<article>
<h5>结算信息</h5>
<p>结算方式:{{ mapSettlement(customerDetail.settlement_type) }}</p>
<p>账期天数:{{ customerDetail.settlement_days ?? 0 }}</p>
<p>信用额度:{{ formatAmount(customerDetail.credit_limit) }}</p>
</article>
<article>
<h5>业务信息</h5>
<p>客户类型:{{ mapCustomerType(customerDetail.customer_type) }}</p>
<p>业务员 ID{{ customerDetail.salesman_id || "-" }}</p>
<p>欠款金额:{{ formatAmount(customerDetail.arrears_amount) }}</p>
</article>
<article class="full-width">
<h5>附加信息</h5>
<p>状态:{{ Number(customerDetail.status || 1) === 1 ? "启用" : "停用" }}</p>
<p>备注:{{ customerDetail.remark || "-" }}</p>
</article>
</div>
</section>
</div>
</section>
<section v-if="activeTab === 'categories'" class="module-panel">
<div class="panel-header">
<h3>产品分类</h3>
<span>支持真实分类查询和新增</span>
</div>
<section class="toolbar toolbar-2">
<label>
<span>分类名称</span>
<input v-model.trim="categoryFilters.category_name" type="text" placeholder="请输入分类名称" />
</label>
<label>
<span>状态</span>
<select v-model="categoryFilters.status">
<option value="">全部状态</option>
<option value="1">启用</option>
<option value="0">停用</option>
</select>
</label>
<div class="toolbar-actions">
<button class="primary-btn" :disabled="loadingCategories" @click="handleSearchCategories">
{{ loadingCategories ? "查询中..." : "查询分类" }}
</button>
<button class="ghost-btn" :disabled="loadingCategories" @click="handleResetCategories">重置</button>
<button class="ghost-btn" @click="openCreateCategoryModal">新增分类</button>
</div>
</section>
<div v-if="showCreateCategoryForm" class="modal-mask" @click.self="cancelCreateCategory">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>新增分类</h4>
</div>
<button class="modal-close" type="button" @click="cancelCreateCategory">×</button>
</div>
<div class="form-grid">
<label>
<span>分类名称</span>
<input v-model.trim="categoryCreateForm.category_name" type="text" placeholder="请输入分类名称" />
</label>
<label>
<span>分类编码</span>
<input v-model.trim="categoryCreateForm.category_code" type="text" placeholder="请输入分类编码" />
</label>
<label>
<span>排序</span>
<input v-model.number="categoryCreateForm.sort_no" type="number" min="0" />
</label>
<label>
<span>状态</span>
<select v-model.number="categoryCreateForm.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
<label class="full-width">
<span>备注</span>
<textarea v-model.trim="categoryCreateForm.remark" rows="3" placeholder="请输入备注"></textarea>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelCreateCategory">取消新增</button>
<button class="primary-btn" :disabled="creatingCategory" @click="handleCreateCategory">
{{ creatingCategory ? "创建中..." : "提交创建" }}
</button>
</div>
</section>
</div>
<div v-if="showEditCategoryForm" class="modal-mask" @click.self="resetCategoryEditForm">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>编辑分类</h4>
</div>
<button class="modal-close" type="button" @click="resetCategoryEditForm">×</button>
</div>
<div class="form-grid">
<label>
<span>分类名称</span>
<input v-model.trim="categoryEditForm.category_name" type="text" placeholder="请输入分类名称" />
</label>
<label>
<span>分类编码</span>
<input v-model.trim="categoryEditForm.category_code" type="text" placeholder="请输入分类编码" />
</label>
<label>
<span>排序</span>
<input v-model.number="categoryEditForm.sort_no" type="number" min="0" />
</label>
<label>
<span>状态</span>
<select v-model.number="categoryEditForm.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
<label class="full-width">
<span>备注</span>
<textarea v-model.trim="categoryEditForm.remark" rows="3" placeholder="请输入备注"></textarea>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="resetCategoryEditForm">取消编辑</button>
<button :disabled="updatingCategory" @click="handleUpdateCategory">
{{ updatingCategory ? "保存中..." : "保存分类" }}
</button>
</div>
</section>
</div>
<p v-if="loadingCategories" class="loading">正在加载分类列表...</p>
<div v-else-if="!categoryRows.length" class="empty-state">当前没有分类数据</div>
<div v-else class="table-wrap">
<table>
<thead>
<tr>
<th><input type="checkbox" :checked="selectedCategoryIds.length === categoryRows.length && categoryRows.length > 0" @change="toggleAllCategories($event)" /></th>
<th>分类名称</th>
<th>分类编码</th>
<th>排序</th>
<th>状态</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in categoryRows" :key="row.categoryId || row.categoryCode">
<td><input type="checkbox" :value="row.categoryId" v-model="selectedCategoryIds" /></td>
<td>{{ row.categoryName }}</td>
<td>{{ row.categoryCode }}</td>
<td>{{ row.sortNo }}</td>
<td>{{ row.status }}</td>
<td>{{ row.remark }}</td>
<td><button class="link-btn" @click="startEditCategory(row)">编辑</button> <button class="link-btn danger-link" @click="handleDeleteCategory(row)">删除</button> <button class="link-btn danger-link" @click="handlePermanentDeleteCategory(row)">永久删除</button></td>
</tr>
</tbody>
</table>
<div v-if="selectedCategoryIds.length" style="margin-top:8px;">
<button class="link-btn danger-link" @click="handleBatchDeleteCategories">批量删除 ({{ selectedCategoryIds.length }})</button>
</div>
</div>
</section>
<section v-if="activeTab === 'products'" class="module-panel">
<div class="panel-header">
<h3>产品管理</h3>
<span>支持真实产品查询和新增</span>
</div>
<section class="toolbar toolbar-4">
<label>
<span>产品名称</span>
<input v-model.trim="productFilters.product_name" type="text" placeholder="请输入产品名称" />
</label>
<label>
<span>规格</span>
<input v-model.trim="productFilters.specification" type="text" placeholder="请输入规格" />
</label>
<label>
<span>分类</span>
<select v-model="productFilters.category_id">
<option value="">全部分类</option>
<option v-for="item in categoryOptions" :key="item.value" :value="String(item.value)">
{{ item.label }}
</option>
</select>
</label>
<label>
<span>状态</span>
<select v-model="productFilters.status">
<option value="">全部状态</option>
<option value="1">启用</option>
<option value="0">停用</option>
</select>
</label>
<div class="toolbar-actions">
<button class="primary-btn" :disabled="loadingProducts" @click="handleSearchProducts">
{{ loadingProducts ? "查询中..." : "查询产品" }}
</button>
<button class="ghost-btn" :disabled="loadingProducts" @click="handleResetProducts">重置</button>
<button class="ghost-btn" @click="openCreateProductModal">新增产品</button>
</div>
</section>
<div v-if="showCreateProductForm" class="modal-mask" @click.self="cancelCreateProduct">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>新增产品</h4>
</div>
<button class="modal-close" type="button" @click="cancelCreateProduct">×</button>
</div>
<div class="product-create-summary">
<p>同一产品可一次新增多个规格,每个规格可配置不同的销售价和成本价。</p>
</div>
<div class="form-grid product-master-fields">
<label>
<span>产品名称</span>
<input v-model.trim="productCreateForm.product_name" type="text" placeholder="请输入产品名称" />
</label>
<label>
<span>分类</span>
<select v-model="productCreateForm.category_id">
<option value="">请选择分类</option>
<option v-for="item in categoryOptions" :key="item.value" :value="String(item.value)">
{{ item.label }}
</option>
</select>
</label>
</div>
<div class="spec-section-header">
<h5>规格明细</h5>
<button class="ghost-btn small-btn" type="button" @click="addProductSpecRow">添加规格</button>
</div>
<div v-for="(specRow, index) in productSpecRows" :key="specRow.rowKey" class="spec-card">
<div class="spec-card-header">
<h5>规格 {{ index + 1 }}</h5>
<button v-if="productSpecRows.length > 1" class="link-btn danger-link" type="button" @click="removeProductSpecRow(index)">删除</button>
</div>
<div class="form-grid">
<label>
<span>规格</span>
<input v-model.trim="specRow.specification" type="text" placeholder="请输入规格" />
</label>
<label>
<span>单位</span>
<input v-model.trim="specRow.unit" type="text" placeholder="请输入单位" />
</label>
<label>
<span>成本价</span>
<input v-model.number="specRow.cost_price" type="number" min="0" step="0.01" />
</label>
<label>
<span>销售价</span>
<input v-model.number="specRow.sale_price" type="number" min="0" step="0.01" />
</label>
<label>
<span>状态</span>
<select v-model.number="specRow.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
<label>
<span>默认规格</span>
<select v-model="specRow.is_default">
<option :value="true">是</option>
<option :value="false">否</option>
</select>
</label>
<label class="full-width">
<span>备注</span>
<textarea v-model.trim="specRow.remark" rows="3" placeholder="请输入备注"></textarea>
</label>
</div>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelCreateProduct">取消新增</button>
<button class="primary-btn" :disabled="creatingProduct" @click="handleCreateProduct">
{{ creatingProduct ? "创建中..." : "提交创建" }}
</button>
</div>
</section>
</div>
<p v-if="loadingProducts" class="loading">正在加载产品列表...</p>
<div v-else-if="!productRows.length" class="empty-state">当前没有产品数据</div>
<div v-else class="table-wrap">
<table>
<thead>
<tr>
<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>
</tr>
</thead>
<tbody>
<template v-for="row in productRows" :key="row.productName">
<tr class="product-row" :class="{ expanded: expandedProducts.has(row.productName) }">
<td><input type="checkbox" :value="row.productId" v-model="selectedProductIds" /></td>
<td class="product-name-cell" @click="toggleExpand(row.productName)">
<span class="expand-icon">{{ expandedProducts.has(row.productName) ? '▼' : '▶' }}</span>
{{ row.productName }}
</td>
<td>{{ row.categoryName }}</td>
<td>{{ row.specifications.length }}</td>
<td>{{ priceRange(row) }}</td>
<td>{{ row.status }}</td>
<td>
<button class="link-btn" @click="toggleExpand(row.productName)">
{{ 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>
</table>
<div v-if="selectedProductIds.length" style="margin-top:8px;">
<button class="link-btn danger-link" @click="handleBatchDeleteProducts">批量删除 ({{ selectedProductIds.length }})</button>
</div>
</div>
<div v-if="showEditSpecForm" class="modal-mask" @click.self="cancelEditSpec">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>编辑规格</h4>
<span>产品:{{ editingSpecForm.product_name }}</span>
</div>
<button class="modal-close" type="button" @click="cancelEditSpec">×</button>
</div>
<div class="form-grid">
<label>
<span>规格</span>
<input v-model.trim="editingSpecForm.specification" type="text" />
</label>
<label>
<span>单位</span>
<input v-model.trim="editingSpecForm.unit" type="text" />
</label>
<label>
<span>分类</span>
<select v-model="editingSpecForm.category_id">
<option value="">请选择分类</option>
<option v-for="item in categoryOptions" :key="item.value" :value="String(item.value)">
{{ item.label }}
</option>
</select>
</label>
<label>
<span>成本价</span>
<input v-model.number="editingSpecForm.cost_price" type="number" min="0" step="0.01" />
</label>
<label>
<span>销售价</span>
<input v-model.number="editingSpecForm.sale_price" type="number" min="0" step="0.01" />
</label>
<label>
<span>状态</span>
<select v-model.number="editingSpecForm.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
<label class="full-width">
<span>备注</span>
<textarea v-model.trim="editingSpecForm.remark" rows="3"></textarea>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelEditSpec">取消</button>
<button class="primary-btn" :disabled="updatingSpec" @click="handleUpdateSpec">保存规格</button>
</div>
</section>
</div>
</section>
<section v-if="activeTab === 'suppliers'" class="module-panel">
<div class="panel-header">
<h3>工厂/供应商</h3>
<span>支持真实供应商查询和新增</span>
</div>
<section class="toolbar toolbar-3">
<label>
<span>供应商名称</span>
<input v-model.trim="supplierFilters.supplier_name" type="text" placeholder="请输入供应商名称" />
</label>
<label>
<span>供应商类型</span>
<select v-model="supplierFilters.supplier_type">
<option value="">全部类型</option>
<option value="factory">工厂</option>
<option value="logistics">物流合作方</option>
</select>
</label>
<label>
<span>状态</span>
<select v-model="supplierFilters.status">
<option value="">全部状态</option>
<option value="1">启用</option>
<option value="0">停用</option>
</select>
</label>
<div class="toolbar-actions">
<button class="primary-btn" :disabled="loadingSuppliers" @click="handleSearchSuppliers">
{{ loadingSuppliers ? "查询中..." : "查询供应商" }}
</button>
<button class="ghost-btn" :disabled="loadingSuppliers" @click="handleResetSuppliers">重置</button>
<button class="ghost-btn" @click="openCreateSupplierModal">新增供应商</button>
</div>
</section>
<div v-if="showCreateSupplierForm" class="modal-mask" @click.self="cancelCreateSupplier">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>新增供应商</h4>
</div>
<button class="modal-close" type="button" @click="cancelCreateSupplier">×</button>
</div>
<div class="form-grid">
<label>
<span>供应商名称</span>
<input v-model.trim="supplierCreateForm.supplier_name" type="text" placeholder="请输入供应商名称" />
</label>
<label>
<span>供应商类型</span>
<select v-model="supplierCreateForm.supplier_type">
<option value="factory">工厂</option>
<option value="logistics">物流合作方</option>
</select>
</label>
<label>
<span>联系人</span>
<input v-model.trim="supplierCreateForm.contact_name" type="text" placeholder="请输入联系人" />
</label>
<label>
<span>联系电话</span>
<input v-model.trim="supplierCreateForm.contact_mobile" type="text" placeholder="请输入联系电话" />
</label>
<label>
<span>地址</span>
<input v-model.trim="supplierCreateForm.address" type="text" placeholder="请输入地址" />
</label>
<label>
<span>模板类型</span>
<input v-model.trim="supplierCreateForm.template_type" type="text" placeholder="请输入模板类型" />
</label>
<label>
<span>状态</span>
<select v-model.number="supplierCreateForm.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
<label class="full-width">
<span>备注</span>
<textarea v-model.trim="supplierCreateForm.remark" rows="3" placeholder="请输入备注"></textarea>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelCreateSupplier">取消新增</button>
<button class="primary-btn" :disabled="creatingSupplier" @click="handleCreateSupplier">
{{ creatingSupplier ? "创建中..." : "提交创建" }}
</button>
</div>
</section>
</div>
<p v-if="loadingSuppliers" class="loading">正在加载供应商列表...</p>
<div v-else-if="!supplierRows.length" class="empty-state">当前没有供应商数据</div>
<div v-else class="table-wrap">
<table>
<thead>
<tr>
<th><input type="checkbox" :checked="selectedSupplierIds.length === supplierRows.length && supplierRows.length > 0" @change="toggleAllSuppliers($event)" /></th>
<th>供应商名称</th>
<th>类型</th>
<th>联系人</th>
<th>联系电话</th>
<th>地址</th>
<th>模板类型</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in supplierRows" :key="row.supplierId || row.supplierName">
<td><input type="checkbox" :value="row.supplierId" v-model="selectedSupplierIds" /></td>
<td>{{ row.supplierName }}</td>
<td>{{ mapSupplierType(row.supplierType) }}</td>
<td>{{ row.contactName }}</td>
<td>{{ row.contactMobile }}</td>
<td>{{ row.address }}</td>
<td>{{ row.templateType }}</td>
<td>{{ row.status }}</td>
<td>
<button class="link-btn" :disabled="loadingSupplierDetail && selectedSupplierId === row.supplierId" @click="handleViewSupplierDetail(row)">
{{ loadingSupplierDetail && selectedSupplierId === row.supplierId ? "加载中..." : "查看详情" }}
</button>
<button class="link-btn" @click="startEditSupplier(row)">编辑</button>
<button class="link-btn danger-link" @click="handleDeleteSupplier(row)">删除</button>
<button class="link-btn danger-link" @click="handlePermanentDeleteSupplier(row)">永久删除</button>
</td>
</tr>
</tbody>
</table>
<div v-if="selectedSupplierIds.length" style="margin-top:8px;">
<button class="link-btn danger-link" @click="handleBatchDeleteSuppliers">批量删除 ({{ selectedSupplierIds.length }})</button>
</div>
</div>
<div v-if="supplierDetail" class="modal-mask" @click.self="closeSupplierDetail">
<section class="modal-panel detail-modal">
<div class="modal-header">
<div>
<h4>供应商详情</h4>
<span>供应商 ID{{ supplierDetail.supplier_id }}</span>
</div>
<button class="modal-close" type="button" @click="closeSupplierDetail">×</button>
</div>
<div class="detail-grid">
<article>
<h5>基本信息</h5>
<p>供应商名称:{{ supplierDetail.supplier_name }}</p>
<p>类型:{{ mapSupplierType(supplierDetail.supplier_type) }}</p>
<p>状态:{{ Number(supplierDetail.status || 1) === 1 ? "启用" : "停用" }}</p>
</article>
<article>
<h5>联系信息</h5>
<p>联系人:{{ supplierDetail.contact_name || "-" }}</p>
<p>联系电话:{{ supplierDetail.contact_mobile || "-" }}</p>
<p>地址:{{ supplierDetail.address || "-" }}</p>
</article>
</div>
</section>
</div>
</section>
<div v-if="showEditSupplierForm" class="modal-mask" @click.self="cancelEditSupplier">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>编辑供应商</h4>
</div>
<button class="modal-close" type="button" @click="cancelEditSupplier">&times;</button>
</div>
<div class="form-grid">
<label><span>供应商名称</span><input v-model.trim="supplierEditForm.supplier_name" type="text" /></label>
<label><span>供应商类型</span><select v-model="supplierEditForm.supplier_type"><option value="factory">工厂</option><option value="logistics">物流合作方</option></select></label>
<label><span>联系人</span><input v-model.trim="supplierEditForm.contact_name" type="text" /></label>
<label><span>联系电话</span><input v-model.trim="supplierEditForm.contact_mobile" type="text" /></label>
<label><span>地址</span><input v-model.trim="supplierEditForm.address" type="text" /></label>
<label><span>模板类型</span><input v-model.trim="supplierEditForm.template_type" type="text" /></label>
<label><span>状态</span><select v-model.number="supplierEditForm.status"><option :value="1">启用</option><option :value="0">停用</option></select></label>
<label class="full-width"><span>备注</span><textarea v-model.trim="supplierEditForm.remark" rows="3" /></label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelEditSupplier">取消编辑</button>
<button class="primary-btn" :disabled="updatingSupplier" @click="handleUpdateSupplier">{{ updatingSupplier ? '保存中...' : '保存供应商' }}</button>
</div>
</section>
</div>
<div v-if="showEditCustomerForm" class="modal-mask" @click.self="cancelEditCustomer">
<section class="modal-panel">
<div class="modal-header">
<div>
<h4>编辑客户</h4>
</div>
<button class="modal-close" type="button" @click="cancelEditCustomer">&times;</button>
</div>
<div class="form-grid">
<label><span>客户姓名</span><input v-model.trim="customerEditForm.customer_name" type="text" /></label>
<label><span>客户手机号</span><input v-model.trim="customerEditForm.mobile" type="text" /></label>
<label><span>客户地址</span><input v-model.trim="customerEditForm.address" type="text" /></label>
<label><span>归属业务员</span><select v-model.number="customerEditForm.salesman_id"><option :value="0">请选择业务员</option><option v-for="u in userOptions" :key="u.value" :value="u.value">{{ u.label }}</option></select></label>
<label><span>客户类型</span><select v-model="customerEditForm.customer_type"><option value="channel">渠道客户</option><option value="retail">零售客户</option><option value="project">项目客户</option></select></label>
<label><span>结算方式</span><select v-model="customerEditForm.settlement_type"><option value="monthly">月结</option><option value="cash">现结</option><option value="delivered">送达结算</option></select></label>
<label><span>账期天数</span><input v-model.number="customerEditForm.settlement_days" type="number" min="0" /></label>
<label><span>信用额度</span><input v-model.number="customerEditForm.credit_limit" type="number" min="0" step="0.01" /></label>
<label class="full-width"><span>备注</span><textarea v-model.trim="customerEditForm.remark" rows="3" /></label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="cancelEditCustomer">取消编辑</button>
<button class="primary-btn" :disabled="updatingCustomer" @click="handleUpdateCustomer">{{ updatingCustomer ? '保存中...' : '保存客户' }}</button>
</div>
</section>
</div>
</section>
</template>
<script setup>
/**
* MasterDataPage.vue - 基础资料管理页面脚本
* 管理客户、产品分类、产品(含多规格)和供应商四大模块的 CRUD 操作。
* 通过 Tab 切换按需加载各模块数据。
*/
import { computed, onMounted, reactive, ref } from "vue";
import { RouterLink } from "vue-router";
import { useToast } from '../composables/useToast';
const toast = useToast();
import {
createCustomer,
createProduct,
createProductCategory,
createSupplier,
setDefaultProductSpec,
updateProductSpecification,
updateCustomer,
updateSupplier,
fetchCustomerDetail,
fetchCustomerList,
fetchProductCategoryList,
fetchProductList,
fetchSupplierDetail,
fetchSupplierList,
importCustomers,
updateProductCategory,
deleteProductCategory,
permanentDeleteProductCategory,
deleteProduct,
permanentDeleteProduct,
deleteSupplier as apiDeleteSupplier,
permanentDeleteSupplier,
batchDeleteProductCategory,
batchDeleteProduct,
batchDeleteSupplier,
fetchUserOptions,
} from "../mockApi";
/** 产品分类、产品管理、工厂/供应商 Tab 配置 */
const tabs = [
{ key: "categories", label: "产品分类" },
{ key: "products", label: "产品管理" },
{ key: "suppliers", label: "工厂/供应商" },
];
/** 页面初始加载中标识 */
const loading = ref(true);
/** 当前激活的 Tab key */
const activeTab = ref("categories");
/** 概览卡片数据(各模块功能说明) */
const masterDataModules = ref([]);
// ========== 客户模块状态 ==========
/** 客户列表加载中标识 */
const loadingCustomers = ref(true);
/** 新增客户操作进行中标识 */
const creatingCustomer = ref(false);
/** 导入客户操作进行中标识 */
const importingCustomer = ref(false);
/** 客户详情加载中标识 */
const customerDetailLoading = ref(false);
/** 新增客户弹窗是否显示 */
const showCreateCustomerForm = ref(false);
/** 导入客户弹窗是否显示 */
const showImportCustomerForm = ref(false);
/** 业务员下拉选项 */
const userOptions = ref([]);
/** 客户列表数据 */
const customerRows = ref([]);
/** 当前查看的客户详情 */
const customerDetail = ref(null);
/** 当前选中的客户 ID */
const selectedCustomerId = ref(null);
const customerFilters = reactive({
customer_name: "",
mobile: "",
customer_type: "",
settlement_type: "",
});
const customerCreateForm = reactive({
customer_name: "",
mobile: "",
address: "",
settlement_type: "monthly",
settlement_days: 30,
customer_type: "channel",
salesman_id: 1,
credit_limit: 0,
remark: "",
});
const customerImportForm = reactive({
file_url: "",
import_mode: "skip_duplicate",
});
const showEditCustomerForm = ref(false);
const editingCustomerId = ref(null);
const updatingCustomer = ref(false);
const customerEditForm = reactive({
customer_name: "",
mobile: "",
address: "",
settlement_type: "monthly",
settlement_days: 30,
customer_type: "channel",
salesman_id: 1,
credit_limit: 0,
remark: "",
});
// ========== 产品分类模块状态 ==========
/** 分类列表加载中标识 */
const loadingCategories = ref(true);
/** 新增分类操作进行中标识 */
const creatingCategory = ref(false);
/** 编辑分类操作进行中标识 */
const updatingCategory = ref(false);
/** 新增分类弹窗是否显示 */
const showCreateCategoryForm = ref(false);
/** 批量选中的分类 ID 列表 */
const selectedCategoryIds = ref([]);
/** 编辑分类弹窗是否显示 */
const showEditCategoryForm = ref(false);
const categoryRows = ref([]);
const categoryOptions = ref([]);
const editingCategoryId = ref(null);
const categoryFilters = reactive({
category_name: "",
status: "",
});
const categoryCreateForm = reactive({
category_name: "",
category_code: "",
sort_no: 0,
status: 1,
remark: "",
});
const categoryEditForm = reactive({
category_name: "",
category_code: "",
sort_no: 0,
status: 1,
remark: "",
});
// ========== 产品管理模块状态 ==========
/** 产品列表加载中标识 */
const loadingProducts = ref(true);
/** 新增产品操作进行中标识 */
const creatingProduct = ref(false);
/** 新增产品弹窗是否显示 */
const showCreateProductForm = ref(false);
/** 批量选中的产品 ID 列表 */
const selectedProductIds = ref([]);
const productRows = ref([]);
/** 展开的产品名称集合 */
const expandedProducts = ref(new Set());
const productFilters = reactive({
product_name: "",
specification: "",
category_id: "",
status: "",
});
const productCreateForm = reactive({
product_name: "",
category_id: "",
});
const productSpecRows = ref([]);
const showEditSpecForm = ref(false);
const updatingSpec = ref(false);
const editingSpecId = ref(null);
const editingSpecForm = reactive({
product_name: "",
specification: "",
unit: "",
category_id: "",
cost_price: 0,
sale_price: 0,
status: 1,
remark: "",
});
function buildProductSpecRow() {
return {
rowKey: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
specification: "",
unit: "",
cost_price: 0,
sale_price: 0,
status: 1,
remark: "",
is_default: false,
};
}
function createProductRowFromSpec(spec) {
return {
...spec,
rowKey: `${spec.product_id}-${spec.specification}`,
category_id: spec.category_id ?? "",
};
}
// ========== 供应商模块状态 ==========
/** 供应商列表加载中标识 */
const loadingSuppliers = ref(true);
/** 新增供应商操作进行中标识 */
const creatingSupplier = ref(false);
/** 新增供应商弹窗是否显示 */
const showCreateSupplierForm = ref(false);
/** 批量选中的供应商 ID 列表 */
const selectedSupplierIds = ref([]);
const supplierRows = ref([]);
const supplierDetail = ref(null);
const loadingSupplierDetail = ref(false);
const selectedSupplierId = ref(null);
const supplierFilters = reactive({
supplier_name: "",
supplier_type: "",
status: "",
});
const supplierCreateForm = reactive({
supplier_name: "",
supplier_type: "factory",
contact_name: "",
contact_mobile: "",
address: "",
template_type: "default",
status: 1,
remark: "",
});
const showEditSupplierForm = ref(false);
const editingSupplierId = ref(null);
const updatingSupplier = ref(false);
const supplierEditForm = reactive({
supplier_name: "",
supplier_type: "factory",
contact_name: "",
contact_mobile: "",
address: "",
template_type: "default",
status: 1,
remark: "",
});
function mapSettlement(value) {
const map = {
monthly: "月结",
cash: "现结",
delivered: "送达结算",
};
return map[value] || value || "-";
}
function mapCustomerType(value) {
const map = {
channel: "渠道客户",
retail: "零售客户",
project: "项目客户",
};
return map[value] || value || "-";
}
function mapSupplierType(value) {
const map = {
factory: "工厂",
logistics: "物流合作方",
};
return map[value] || value || "-";
}
function formatAmount(value) {
return Number(value || 0).toFixed(2);
}
function resetCustomerCreateForm() {
customerCreateForm.customer_name = "";
customerCreateForm.mobile = "";
customerCreateForm.address = "";
customerCreateForm.settlement_type = "monthly";
customerCreateForm.settlement_days = 30;
customerCreateForm.customer_type = "channel";
customerCreateForm.salesman_id = 1;
customerCreateForm.credit_limit = 0;
customerCreateForm.remark = "";
}
function resetCustomerImportForm() {
customerImportForm.file_url = "";
customerImportForm.import_mode = "skip_duplicate";
}
function openCreateCustomerModal() {
showImportCustomerForm.value = false;
showCreateCustomerForm.value = true;
}
function cancelCreateCustomer() {
showCreateCustomerForm.value = false;
resetCustomerCreateForm();
}
function openImportCustomerModal() {
showCreateCustomerForm.value = false;
showImportCustomerForm.value = true;
}
function cancelImportCustomer() {
showImportCustomerForm.value = false;
resetCustomerImportForm();
}
function resetCategoryCreateForm() {
categoryCreateForm.category_name = "";
categoryCreateForm.category_code = "";
categoryCreateForm.sort_no = 0;
categoryCreateForm.status = 1;
categoryCreateForm.remark = "";
}
function openCreateCategoryModal() {
showCreateCategoryForm.value = true;
}
function cancelCreateCategory() {
showCreateCategoryForm.value = false;
resetCategoryCreateForm();
}
function resetCategoryEditForm() {
editingCategoryId.value = null;
showEditCategoryForm.value = false;
categoryEditForm.category_name = "";
categoryEditForm.category_code = "";
categoryEditForm.sort_no = 0;
categoryEditForm.status = 1;
categoryEditForm.remark = "";
}
function closeCustomerDetail() {
customerDetail.value = null;
selectedCustomerId.value = null;
}
function closeSupplierDetail() {
resetSupplierDetail();
}
function startEditCustomer(row) {
editingCustomerId.value = row.customerId;
showEditCustomerForm.value = false;
showImportCustomerForm.value = false;
showCreateCustomerForm.value = false;
customerDetail.value = null;
customerEditForm.customer_name = row.name || "";
customerEditForm.mobile = row.mobile || "";
customerEditForm.address = "";
customerEditForm.settlement_type = "monthly";
customerEditForm.settlement_days = 30;
customerEditForm.customer_type = "channel";
customerEditForm.salesman_id = 1;
customerEditForm.credit_limit = 0;
customerEditForm.remark = "";
showEditCustomerForm.value = true;
fetchCustomerDetail(row.customerId).then((detail) => {
if (detail) {
customerEditForm.customer_name = detail.customer_name || "";
customerEditForm.mobile = detail.mobile || "";
customerEditForm.address = detail.address || "";
customerEditForm.settlement_type = detail.settlement_type || "monthly";
customerEditForm.settlement_days = detail.settlement_days ?? 30;
customerEditForm.customer_type = detail.customer_type || "channel";
customerEditForm.salesman_id = detail.salesman_id ?? 1;
customerEditForm.credit_limit = detail.credit_limit ?? 0;
customerEditForm.remark = detail.remark === "-" ? "" : detail.remark || "";
}
}).catch(() => {});
}
function cancelEditCustomer() {
showEditCustomerForm.value = false;
editingCustomerId.value = null;
}
async function handleUpdateCustomer() {
if (!customerEditForm.customer_name.trim()) { toast.error("请输入客户姓名"); return; }
if (!customerEditForm.mobile.trim()) { toast.error("请输入客户手机号"); return; }
updatingCustomer.value = true;
try {
await updateCustomer(editingCustomerId.value, { ...customerEditForm });
showEditCustomerForm.value = false;
editingCustomerId.value = null;
await handleSearchCustomers();
toast.success("客户信息已更新。");
} catch (error) {
toast.error(error.message || "更新客户失败");
} finally {
updatingCustomer.value = false;
}
}
function startEditSupplier(row) {
editingSupplierId.value = row.supplierId;
showEditSupplierForm.value = true;
supplierEditForm.supplier_name = row.supplierName || "";
supplierEditForm.supplier_type = row.supplierType || "factory";
supplierEditForm.contact_name = row.contactName === "-" ? "" : row.contactName || "";
supplierEditForm.contact_mobile = row.contactMobile === "-" ? "" : row.contactMobile || "";
supplierEditForm.address = row.address === "-" ? "" : row.address || "";
supplierEditForm.template_type = row.templateType === "-" ? "" : row.templateType || "default";
supplierEditForm.status = row.status === "启用" ? 1 : 0;
supplierEditForm.remark = "";
}
function cancelEditSupplier() {
showEditSupplierForm.value = false;
editingSupplierId.value = null;
}
async function handleUpdateSupplier() {
if (!supplierEditForm.supplier_name.trim()) { toast.error("请输入供应商名称"); return; }
updatingSupplier.value = true;
try {
await updateSupplier(editingSupplierId.value, { ...supplierEditForm });
showEditSupplierForm.value = false;
editingSupplierId.value = null;
await handleSearchSuppliers();
toast.success("供应商信息已更新。");
} catch (error) {
toast.error(error.message || "更新供应商失败");
} finally {
updatingSupplier.value = false;
}
}
function resetProductCreateForm() {
productCreateForm.product_name = "";
productCreateForm.category_id = "";
productSpecRows.value = [buildProductSpecRow()];
}
resetProductCreateForm();
function addProductSpecRow() {
productSpecRows.value.push(buildProductSpecRow());
}
function removeProductSpecRow(index) {
if (productSpecRows.value.length <= 1) {
return;
}
productSpecRows.value.splice(index, 1);
}
function openCreateProductModal() {
showCreateProductForm.value = true;
}
function cancelCreateProduct() {
showCreateProductForm.value = false;
resetProductCreateForm();
}
function toggleExpand(productName) {
const s = new Set(expandedProducts.value);
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() {
editingSpecId.value = null;
showEditSpecForm.value = false;
editingSpecForm.product_name = "";
editingSpecForm.specification = "";
editingSpecForm.unit = "";
editingSpecForm.category_id = "";
editingSpecForm.cost_price = 0;
editingSpecForm.sale_price = 0;
editingSpecForm.status = 1;
editingSpecForm.remark = "";
}
function cancelEditSpec() {
resetEditSpecForm();
}
async function handleSetDefaultSpec(spec) {
if (!spec?.product_id) {
toast.error("请选择有效规格");
return;
}
try {
await setDefaultProductSpec(spec.product_id);
await handleSearchProducts();
toast.success(`已将 ${spec.specification} 设为默认规格。`);
} catch (error) {
toast.error(error.message || "设置默认规格失败");
}
}
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() {
supplierCreateForm.supplier_name = "";
supplierCreateForm.supplier_type = "factory";
supplierCreateForm.contact_name = "";
supplierCreateForm.contact_mobile = "";
supplierCreateForm.address = "";
supplierCreateForm.template_type = "default";
supplierCreateForm.status = 1;
supplierCreateForm.remark = "";
}
function openCreateSupplierModal() {
showCreateSupplierForm.value = true;
}
function cancelCreateSupplier() {
showCreateSupplierForm.value = false;
resetSupplierCreateForm();
}
function resetSupplierDetail() {
supplierDetail.value = null;
selectedSupplierId.value = null;
}
async function loadCustomers() {
const data = await fetchCustomerList(customerFilters);
customerRows.value = data.rows;
return data;
}
async function loadCategories() {
const data = await fetchProductCategoryList(categoryFilters);
categoryRows.value = data.rows;
categoryOptions.value = data.rows.map((item) => ({
value: item.categoryId,
label: `${item.categoryName} / ${item.categoryCode}`,
}));
return data;
}
async function loadProducts() {
const data = await fetchProductList({
...productFilters,
category_id: productFilters.category_id ? Number(productFilters.category_id) : undefined,
});
productRows.value = data.rows;
return data;
}
async function loadSuppliers() {
const data = await fetchSupplierList(supplierFilters);
supplierRows.value = data.rows;
return data;
}
async function handleSearchCustomers() {
loadingCustomers.value = true;
try {
const data = await loadCustomers();
} catch (error) {
toast.error(error.message || "加载客户列表失败");
} finally {
loadingCustomers.value = false;
}
}
async function handleResetCustomers() {
customerFilters.customer_name = "";
customerFilters.mobile = "";
customerFilters.customer_type = "";
customerFilters.settlement_type = "";
await handleSearchCustomers();
}
async function handleViewCustomerDetail(row) {
customerDetailLoading.value = true;
selectedCustomerId.value = row.customerId;
try {
customerDetail.value = await fetchCustomerDetail(row.customerId);
toast.success(`已加载客户 ${row.name} 的详情信息。`);
} catch (error) {
toast.error(error.message || "加载客户详情失败");
} finally {
customerDetailLoading.value = false;
}
}
async function handleCreateCustomer() {
if (!customerCreateForm.customer_name.trim()) {
toast.error("请输入客户姓名");
return;
}
if (!customerCreateForm.mobile.trim()) {
toast.error("请输入客户手机号");
return;
}
creatingCustomer.value = true;
try {
const result = await createCustomer({
...customerCreateForm,
});
showCreateCustomerForm.value = false;
resetCustomerCreateForm();
await handleSearchCustomers();
toast.success(`客户 ${result.customer_name} 创建成功。`);
} catch (error) {
toast.error(error.message || "创建客户失败");
} finally {
creatingCustomer.value = false;
}
}
async function handleImportCustomers() {
if (!customerImportForm.file_url.trim()) {
toast.error("请输入导入文件地址");
return;
}
importingCustomer.value = true;
try {
const result = await importCustomers({ ...customerImportForm });
showImportCustomerForm.value = false;
resetCustomerImportForm();
toast.success(
`客户导入完成:总计 ${result.total_count} 条,成功 ${result.success_count} 条,重复 ${result.duplicate_count} 条,失败 ${result.fail_count} 条。`
);
} catch (error) {
toast.error(error.message || "导入客户失败");
} finally {
importingCustomer.value = false;
}
}
async function handleSearchCategories() {
loadingCategories.value = true;
try {
const data = await loadCategories();
} catch (error) {
toast.error(error.message || "加载分类列表失败");
} finally {
loadingCategories.value = false;
}
}
async function handleResetCategories() {
categoryFilters.category_name = "";
categoryFilters.status = "";
await handleSearchCategories();
}
async function handleCreateCategory() {
if (!categoryCreateForm.category_name.trim()) {
toast.error("请输入分类名称");
return;
}
if (!categoryCreateForm.category_code.trim()) {
toast.error("请输入分类编码");
return;
}
creatingCategory.value = true;
try {
const result = await createProductCategory({ ...categoryCreateForm });
showCreateCategoryForm.value = false;
resetCategoryCreateForm();
await handleSearchCategories();
await handleSearchProducts();
toast.success(`分类 ${result.category_name} 创建成功。`);
} catch (error) {
toast.error(error.message || "创建分类失败");
} finally {
creatingCategory.value = false;
}
}
function startEditCategory(row) {
editingCategoryId.value = row.categoryId;
categoryEditForm.category_name = row.categoryName || "";
categoryEditForm.category_code = row.categoryCode || "";
categoryEditForm.sort_no = Number(row.sortNo || 0);
categoryEditForm.status = row.status === "启用" ? 1 : 0;
categoryEditForm.remark = row.remark === "-" ? "" : row.remark || "";
showEditCategoryForm.value = true;
}
function openCustomerDetail(row) {
handleViewCustomerDetail(row);
}
async function handleUpdateCategory() {
if (!editingCategoryId.value) {
toast.error("请选择要编辑的分类");
return;
}
if (!categoryEditForm.category_name.trim()) {
toast.error("请输入分类名称");
return;
}
if (!categoryEditForm.category_code.trim()) {
toast.error("请输入分类编码");
return;
}
updatingCategory.value = true;
try {
await updateProductCategory(editingCategoryId.value, { ...categoryEditForm });
resetCategoryEditForm();
await handleSearchCategories();
await handleSearchProducts();
toast.success("分类更新成功。");
} catch (error) {
toast.error(error.message || "更新分类失败");
} finally {
updatingCategory.value = false;
}
}
async function handleSearchProducts() {
loadingProducts.value = true;
try {
const data = await loadProducts();
} catch (error) {
toast.error(error.message || "加载产品列表失败");
} finally {
loadingProducts.value = false;
}
}
async function handleResetProducts() {
productFilters.product_name = "";
productFilters.specification = "";
productFilters.category_id = "";
productFilters.status = "";
await handleSearchProducts();
}
async function handleCreateProduct() {
if (!productCreateForm.product_name.trim()) {
toast.error("请输入产品名称");
return;
}
const validRows = productSpecRows.value.filter((row) => row.specification.trim() && row.unit.trim());
if (!validRows.length) {
toast.error("请至少填写一个完整的产品规格");
return;
}
creatingProduct.value = true;
try {
const result = await createProduct({
product_name: productCreateForm.product_name.trim(),
category_id: productCreateForm.category_id ? Number(productCreateForm.category_id) : null,
status: Number(validRows[0].status || 1),
remark: validRows[0].remark,
specifications: validRows.map((row) => ({
specification: row.specification,
unit: row.unit,
cost_price: Number(row.cost_price || 0),
sale_price: Number(row.sale_price || 0),
status: Number(row.status || 1),
remark: row.remark,
is_default: Boolean(row.is_default),
})),
});
showCreateProductForm.value = false;
resetProductCreateForm();
await handleSearchProducts();
toast.success(`产品 ${result.product_name} 创建成功,已保存 ${result.specifications?.length || 0} 个规格。`);
} catch (error) {
toast.error(error.message || "创建产品失败");
} finally {
creatingProduct.value = false;
}
}
async function handleUpdateSpec() {
if (!editingSpecForm.product_name.trim()) {
toast.error("请输入产品名称");
return;
}
if (!editingSpecForm.specification.trim()) {
toast.error("请输入规格");
return;
}
if (!editingSpecForm.unit.trim()) {
toast.error("请输入单位");
return;
}
updatingSpec.value = true;
try {
if (editingSpecId.value) {
await updateProductSpecification(editingSpecId.value, {
...editingSpecForm,
category_id: editingSpecForm.category_id ? Number(editingSpecForm.category_id) : null,
status: Number(editingSpecForm.status || 1),
is_default: false,
});
toast.success("规格更新成功。");
} else {
toast.success("规格添加成功(需后端支持新增规格接口)。");
}
resetEditSpecForm();
await handleSearchProducts();
} catch (error) {
toast.error(error.message || "操作失败");
} finally {
updatingSpec.value = false;
}
}
async function handleSearchSuppliers() {
loadingSuppliers.value = true;
try {
const data = await loadSuppliers();
} catch (error) {
toast.error(error.message || "加载供应商列表失败");
} finally {
loadingSuppliers.value = false;
}
}
async function handleResetSuppliers() {
supplierFilters.supplier_name = "";
supplierFilters.supplier_type = "";
supplierFilters.status = "";
await handleSearchSuppliers();
}
async function handleCreateSupplier() {
if (!supplierCreateForm.supplier_name.trim()) {
toast.error("请输入供应商名称");
return;
}
creatingSupplier.value = true;
try {
const result = await createSupplier({ ...supplierCreateForm });
showCreateSupplierForm.value = false;
resetSupplierCreateForm();
await handleSearchSuppliers();
toast.success(`供应商 ${result.supplier_name} 创建成功。`);
} catch (error) {
toast.error(error.message || "创建供应商失败");
} finally {
creatingSupplier.value = false;
}
}
async function handleViewSupplierDetail(row) {
if (!row.supplierId) {
toast.error("当前供应商暂不支持查看详情");
return;
}
loadingSupplierDetail.value = true;
selectedSupplierId.value = row.supplierId;
try {
supplierDetail.value = await fetchSupplierDetail(row.supplierId);
toast.success(`已加载供应商 ${row.supplierName} 的详情。`);
} catch (error) {
toast.error(error.message || "加载供应商详情失败");
} finally {
loadingSupplierDetail.value = false;
}
}
async function switchTab(tabKey) {
activeTab.value = tabKey;
if (tabKey === "categories" && !categoryRows.value.length) {
await handleSearchCategories();
}
if (tabKey === "products") {
if (!categoryOptions.value.length) {
await handleSearchCategories();
}
if (!productRows.value.length) {
await handleSearchProducts();
}
}
if (tabKey === "suppliers" && !supplierRows.value.length) {
await handleSearchSuppliers();
}
}
// ========== 删除操作 ==========
function toggleAllCategories(e) {
selectedCategoryIds.value = e.target.checked ? categoryRows.value.map(r => r.categoryId) : [];
}
function toggleAllProducts(e) {
selectedProductIds.value = e.target.checked ? productRows.value.map(r => r.productId) : [];
}
function toggleAllSuppliers(e) {
selectedSupplierIds.value = e.target.checked ? supplierRows.value.map(r => r.supplierId) : [];
}
async function handleBatchDeleteCategories() {
if (!confirm(`确认批量删除 ${selectedCategoryIds.value.length} 个分类?`)) return;
try {
await batchDeleteProductCategory(selectedCategoryIds.value);
toast.success("批量删除成功");
selectedCategoryIds.value = [];
await handleSearchCategories();
} catch (e) { toast.error(e.message); }
}
async function handleBatchDeleteProducts() {
if (!confirm(`确认批量删除 ${selectedProductIds.value.length} 个产品?`)) return;
try {
await batchDeleteProduct(selectedProductIds.value);
toast.success("批量删除成功");
selectedProductIds.value = [];
await handleSearchProducts();
} catch (e) { toast.error(e.message); }
}
async function handleBatchDeleteSuppliers() {
if (!confirm(`确认批量删除 ${selectedSupplierIds.value.length} 个供应商?`)) return;
try {
await batchDeleteSupplier(selectedSupplierIds.value);
toast.success("批量删除成功");
selectedSupplierIds.value = [];
await handleSearchSuppliers();
} catch (e) { toast.error(e.message); }
}
async function handleDeleteCategory(row) {
if (!confirm(`确认删除分类「${row.categoryName}」?删除后可恢复。`)) return;
try {
await deleteProductCategory(row.categoryId);
toast.success("已停用");
await handleSearchCategories();
} catch (e) { toast.error(e.message); }
}
async function handlePermanentDeleteCategory(row) {
if (!confirm(`确认永久删除分类「${row.categoryName}」?\n\n此操作不可恢复`)) return;
try {
await permanentDeleteProductCategory(row.categoryId);
toast.success("永久删除成功");
await handleSearchCategories();
} catch (e) { toast.error(e.message); }
}
async function handleDeleteProduct(row) {
if (!confirm(`确认删除产品「${row.productName}」?删除后可恢复。`)) return;
try {
await deleteProduct(row.productId);
toast.success("已停用");
await handleSearchProducts();
} catch (e) { toast.error(e.message); }
}
async function handlePermanentDeleteProduct(row) {
if (!confirm(`确认永久删除产品「${row.productName}」?\n\n此操作不可恢复`)) return;
try {
await permanentDeleteProduct(row.productId);
toast.success("永久删除成功");
await handleSearchProducts();
} catch (e) { toast.error(e.message); }
}
async function handleDeleteSupplier(row) {
if (!confirm(`确认删除供应商「${row.supplierName}」?删除后可恢复。`)) return;
try {
await apiDeleteSupplier(row.supplierId);
toast.success("已停用");
await handleSearchSuppliers();
} catch (e) { toast.error(e.message); }
}
async function handlePermanentDeleteSupplier(row) {
if (!confirm(`确认永久删除供应商「${row.supplierName}」?\n\n此操作不可恢复`)) return;
try {
await permanentDeleteSupplier(row.supplierId);
toast.success("永久删除成功");
await handleSearchSuppliers();
} catch (e) { toast.error(e.message); }
}
onMounted(async () => {
await handleSearchCustomers();
await handleSearchCategories();
loadingCategories.value = false;
loading.value = false;
try { userOptions.value = await fetchUserOptions(); } catch { /* ignore */ }
});
</script>
<style scoped>
.page { padding: 0; }
.page-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 18px; }
h2 { margin: 0 0 6px; font-size: 24px; }
.subline { margin: 0; color: #64748b; }
.module-switcher { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 16px; }
.module-switcher button { border: 1px solid #cbd5e1; border-radius: 999px; padding: 10px 16px; background: #fff; }
.module-switcher .active { background: #1d4ed8; color: #fff; border-color: #1d4ed8; }
.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-bottom: 20px; }
.overview-card { border: 1px solid #dbe3ef; border-radius: 16px; padding: 16px; background: #f8fbff; min-width: 0; }
.overview-card h3 { margin: 0 0 10px; font-size: 16px; }
.overview-card p { margin: 0; line-height: 1.6; color: #475569; word-break: break-word; }
.module-panel, .form-panel, .detail-panel { margin-top: 20px; border: 1px solid #dbe3ef; border-radius: 18px; padding: 16px; background: #fff; min-width: 0; }
.panel-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
.panel-header.compact { margin-bottom: 8px; }
.panel-header h3, .panel-header h4 { margin: 0; }
.panel-header span { color: #64748b; font-size: 13px; }
.toolbar { display: grid; gap: 12px; margin-bottom: 20px; align-items: end; }
.toolbar-4 { grid-template-columns: repeat(4, minmax(0, 1fr)) auto; }
.toolbar-3 { grid-template-columns: repeat(3, minmax(0, 1fr)) auto; }
.toolbar-2 { grid-template-columns: repeat(2, minmax(0, 1fr)) auto; }
.toolbar label, .form-grid label { display: grid; gap: 6px; min-width: 0; }
.toolbar span, .form-grid span { color: #475569; font-size: 13px; font-weight: 600; }
.toolbar input,
.toolbar select,
.form-grid input,
.form-grid select,
textarea,
.link-btn,
.ghost-btn,
.primary-btn,
.modal-close {
border-radius: 12px;
padding: 10px 12px;
min-width: 0;
}
.toolbar input,
.toolbar select,
.form-grid input,
.form-grid select,
textarea,
.link-btn,
.ghost-btn {
border: 1px solid #cbd5e1;
background: #fff;
}
.primary-btn { border: 1px solid #2563eb; background: #2563eb; color: #fff; }
.ghost-btn { background: #f8fafc; color: #334155; }
.toolbar-actions { display: flex; gap: 10px; align-items: end; flex-wrap: wrap; }
.loading { margin: 0; color: #6b7280; }
.empty-state { padding: 24px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #fff; }
.table-wrap { overflow-x: auto; }
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 { color: #334155; font-size: 13px; background: #f8fafc; }
.status { display: inline-flex; padding: 4px 10px; border-radius: 999px; }
.status-warm { background: #fff7ed; color: #c2410c; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.full-width { grid-column: 1 / -1; }
.action-row { margin-top: 16px; display: flex; justify-content: flex-end; }
.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.detail-grid article { border: 1px solid #dbe3ef; border-radius: 14px; padding: 12px; background: #f8fbff; min-width: 0; }
.detail-grid h5 { margin: 0 0 8px; font-size: 15px; }
.detail-grid p { margin: 0; line-height: 1.7; color: #475569; word-break: break-word; }
.modal-panel-wide { width: min(980px, 100%); }
.detail-modal { width: min(860px, 100%); }
.modal-mask { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.48); backdrop-filter: blur(2px); }
.modal-panel { width: min(760px, 100%); max-height: min(88vh, 860px); overflow: auto; border-radius: 20px; border: 1px solid #dbe3ef; background: #fff; padding: 24px; box-shadow: 0 24px 80px rgba(15, 23, 42, 0.24); }
.modal-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 16px; }
.modal-header h4 { margin: 0 0 6px; font-size: 18px; }
.modal-header span { color: #64748b; font-size: 13px; }
.modal-close { width: 36px; height: 36px; border: 1px solid #d1d5db; background: #f8fafc; font-size: 22px; line-height: 1; color: #334155; }
.product-create-summary { display: flex; justify-content: space-between; gap: 12px; align-items: center; margin-bottom: 12px; padding: 12px 14px; border: 1px solid #dbe3ef; border-radius: 14px; background: #f8fbff; }
.product-create-summary p { margin: 0; color: #475569; }
.product-master-fields { margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid #e2e8f0; }
.spec-section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.spec-section-header h5 { margin: 0; font-size: 14px; color: #334155; }
.spec-card { margin-bottom: 14px; padding: 14px; border: 1px solid #dbe3ef; border-radius: 16px; background: #fff; }
.spec-card-header { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 12px; }
.spec-card-header h5 { margin: 0; font-size: 15px; }
.small-btn { padding: 8px 12px; }
.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:disabled { color: #9ca3af; background: #f3f4f6; cursor: not-allowed; }
@media (max-width: 1200px) {
.toolbar-4, .toolbar-3, .toolbar-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.toolbar-actions { grid-column: 1 / -1; }
.modal-panel, .detail-modal, .modal-panel-wide { width: min(100%, 760px); }
}
@media (max-width: 768px) {
.page-header, .panel-header, .action-row.split { flex-direction: column; align-items: stretch; }
.toolbar, .form-grid, .detail-grid, .toolbar-2, .toolbar-3, .toolbar-4 { grid-template-columns: 1fr; }
.modal-mask { padding: 12px; }
.modal-panel, .detail-modal, .modal-panel-wide { width: 100%; max-height: 92vh; padding: 18px; }
table { min-width: 780px; }
}
</style>