- 产品行可展开显示所有规格的成本价/售价/状态 - 价格范围显示(多规格时 ¥最小~最大) - 展开行内支持编辑规格和设为默认 - 移除产品详情弹窗及相关冗余代码(净减 23 行) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2027 lines
76 KiB
Vue
2027 lines
76 KiB
Vue
<!--
|
||
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">×</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">×</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>
|