Files
erp-next/ruoyi-ui/src/views/bid/client/index.vue
王文昊 93785be505 feat(bid): 完成招投标业务模块多需求迭代
本次提交包含多项功能改进与业务优化:
1.  全局主题色替换为#4A6FA5,统一前端UI风格
2.  新增客户报价单clientId字段,完善客户报价数据结构
3.  实现发货单状态流转功能,支持发货、完成、撤回、设置结单日期操作
4.  新增物料发货记录多表关联查询功能
5.  优化客户管理页面UI布局与交互体验
6.  修复客户报价表单自动补全逻辑,关联clientId与clientName
7.  补充租户ID自动填充逻辑,完善多租户数据隔离
2026-06-10 20:47:14 +08:00

379 lines
16 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.

<template>
<div class="client-manage">
<!-- 标签页 -->
<el-tabs v-model="activeTab" class="client-tabs">
<el-tab-pane label="客户列表" name="list">
<div class="toolbar">
<el-input
v-model="queryParams.clientName"
placeholder="搜索名称 / 编号 / 联系人"
size="small"
clearable
style="width:280px"
prefix-icon="el-icon-search"
@keyup.enter.native="handleSearch"
/>
<el-button type="primary" size="small" icon="el-icon-search" @click="handleSearch">搜索</el-button>
<div class="toolbar-right">
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">新增客户</el-button>
</div>
</div>
<el-table v-loading="loading" :data="clientList" border size="small" stripe style="width:100%">
<el-table-column label="编号" prop="clientNo" width="90" />
<el-table-column label="客户名称" prop="clientName" min-width="160" show-overflow-tooltip />
<el-table-column label="联系人" prop="contact" width="90" />
<el-table-column label="电话" prop="phone" width="120" />
<el-table-column label="城市" prop="city" width="100" />
<el-table-column label="订单数" prop="orderCount" width="70" align="center" />
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="130" align="center" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</el-tab-pane>
<el-tab-pane label="历史发货单" name="orders">
<div class="toolbar">
<el-select v-model="orderClientId" filterable placeholder="选择甲方客户" style="width:380px" @change="loadClientOrders" clearable>
<el-option v-for="c in clientOptions" :key="c.clientId" :label="c.clientNo + ' | ' + c.clientName" :value="c.clientId" />
</el-select>
<span v-if="orderClientName" class="order-hint"> {{ orderList.length }} 条记录</span>
</div>
<el-table v-loading="orderLoading" :data="orderList" border size="small" stripe style="width:100%">
<el-table-column label="发货单号" prop="doNo" width="160" />
<el-table-column label="供应商" prop="supplierName" min-width="130" show-overflow-tooltip />
<el-table-column label="金额" width="130" align="right">
<template slot-scope="scope"><span class="amount">¥{{ scope.row.totalAmount }}</span></template>
</el-table-column>
<el-table-column label="交货期" prop="deliveryDate" width="100" />
<el-table-column label="结单日期" prop="actualCloseDate" width="100" />
<el-table-column label="物料数" prop="itemCount" width="65" align="center" />
<el-table-column label="状态" width="95">
<template slot-scope="scope">
<el-tag :type="statusType(scope.row.deliveryStatus)" size="small" effect="dark">{{ statusLabel(scope.row.deliveryStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="65" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="showOrderDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="!orderClientId && !orderLoading" class="empty-box">
<i class="el-icon-document" />
<p>请先选择甲方客户</p>
</div>
<div v-if="orderClientId && !orderList.length && !orderLoading" class="empty-box">
<i class="el-icon-document" />
<p>该客户暂无发货记录</p>
</div>
</el-tab-pane>
</el-tabs>
<!-- 新增/编辑弹窗 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogOpen" width="620px" append-to-body class="client-dialog" @close="cancelDialog">
<el-form ref="form" :model="form" :rules="rules" label-width="85px" size="small">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户编号" prop="clientNo">
<el-input v-model="form.clientNo" placeholder="如 CU-001" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户名称" prop="clientName">
<el-input v-model="form.clientName" placeholder="客户企业名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="联系人" prop="contact">
<el-input v-model="form.contact" placeholder="联系人姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone" placeholder="手机号 / 固话" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="电子邮箱" prop="email">
<el-input v-model="form.email" placeholder="电子邮箱" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所在城市" prop="city">
<el-input v-model="form.city" placeholder="如:广东深圳" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="详细地址" prop="address">
<el-input v-model="form.address" placeholder="详细地址" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="等级" prop="grade">
<el-select v-model="form.grade" style="width:100%">
<el-option label="A级" value="A" />
<el-option label="B级" value="B" />
<el-option label="C级" value="C" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" style="width:100%">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注信息" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="cancelDialog">取消</el-button>
<el-button type="primary" @click="submitForm">保存</el-button>
</div>
</el-dialog>
<!-- 发货单详情弹窗 -->
<el-dialog title="发货单详情" :visible.sync="detailOpen" width="800px" append-to-body class="detail-dialog">
<div v-if="detailData">
<div class="detail-grid">
<div class="detail-item">
<span class="dl">发货单号</span>
<span class="dv"><b>{{ detailData.doNo }}</b></span>
</div>
<div class="detail-item">
<span class="dl">供应商</span>
<span class="dv">{{ detailData.supplierName || '-' }}</span>
</div>
<div class="detail-item">
<span class="dl">总金额</span>
<span class="dv" style="color:#409EFF;font-weight:700">¥{{ detailData.totalAmount }}</span>
</div>
<div class="detail-item">
<span class="dl">状态</span>
<span class="dv"><el-tag :type="statusType(detailData.deliveryStatus)" size="small" effect="dark">{{ statusLabel(detailData.deliveryStatus) }}</el-tag></span>
</div>
<div class="detail-item">
<span class="dl">交货期</span>
<span class="dv">{{ detailData.deliveryDate || '-' }}</span>
</div>
<div class="detail-item">
<span class="dl">结单日期</span>
<span class="dv">{{ detailData.actualCloseDate || '-' }}</span>
</div>
</div>
<div v-if="detailData.remark" class="detail-remark">备注{{ detailData.remark }}</div>
<div class="section-bar">物料明细</div>
<el-table :data="detailData.items || []" border size="small" style="width:100%">
<el-table-column label="物料名称" prop="materialName" min-width="140" />
<el-table-column label="规格" prop="spec" width="120" show-overflow-tooltip />
<el-table-column label="单位" prop="unit" width="60" />
<el-table-column label="数量" prop="quantity" width="80" align="right" />
<el-table-column label="单价" width="100" align="right">
<template slot-scope="s">¥{{ s.row.unitPrice }}</template>
</el-table-column>
<el-table-column label="小计" width="100" align="right">
<template slot-scope="s">¥{{ s.row.totalPrice }}</template>
</el-table-column>
</el-table>
</div>
<div slot="footer">
<el-button @click="detailOpen = false">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listClient, getClient, addClient, updateClient, delClient, getClientOrders } from "@/api/bid/client"
import { getDelivery } from "@/api/bid/delivery"
export default {
name: "Client",
data() {
return {
activeTab: "list",
loading: false,
clientList: [],
total: 0,
queryParams: { pageNum: 1, pageSize: 20, clientName: "" },
dialogOpen: false,
dialogTitle: "",
form: { grade: "B", status: "0" },
rules: {
clientNo: [{ required: true, message: "客户编号不能为空", trigger: "blur" }],
clientName: [{ required: true, message: "客户名称不能为空", trigger: "blur" }]
},
editId: null,
orderClientId: null,
orderClientName: "",
orderLoading: false,
orderList: [],
clientOptions: [],
detailOpen: false,
detailData: null
}
},
created() { this.getList(); this.loadClientOptions() },
methods: {
getList() {
this.loading = true
listClient(this.queryParams).then(r => { this.clientList = r.rows || []; this.total = r.total || 0; this.loading = false }).catch(() => { this.loading = false })
},
handleSearch() { this.queryParams.pageNum = 1; this.getList(); this.loadClientOptions() },
handleAdd() { this.editId = null; this.form = { grade: "B", status: "0", clientNo: "", clientName: "", contact: "", phone: "", email: "", city: "", address: "", remark: "" }; this.dialogTitle = "新增客户"; this.dialogOpen = true },
handleEdit(row) { this.editId = row.clientId; this.form = { ...row }; this.dialogTitle = "编辑客户"; this.dialogOpen = true },
cancelDialog() { this.dialogOpen = false; this.$refs.form && this.$refs.form.clearValidate() },
submitForm() {
this.$refs.form.validate(v => {
if (!v) return
const action = this.editId ? updateClient(this.form) : addClient(this.form)
action.then(() => { this.$modal.msgSuccess(this.editId ? "修改成功" : "新增成功"); this.dialogOpen = false; this.getList() }).catch(() => {})
})
},
handleDelete(row) {
this.$modal.confirm('确认删除客户 "' + row.clientName + '"').then(() => { delClient(row.clientId).then(() => { this.$modal.msgSuccess("删除成功"); this.getList() }) }).catch(() => {})
},
loadClientOptions() { listClient({ pageNum: 1, pageSize: 999 }).then(r => { this.clientOptions = r.rows || [] }).catch(() => {}) },
loadClientOrders(clientId) {
if (!clientId) { this.orderList = []; this.orderClientName = ""; return }
this.orderLoading = true; this.orderList = []
const c = this.clientOptions.find(o => o.clientId === clientId)
this.orderClientName = c ? c.clientName : ""
getClientOrders(clientId).then(r => {
this.orderList = (r.data || []).map(o => ({
...o, totalAmount: o.totalAmount || o.total_amount, deliveryDate: o.deliveryDate || o.delivery_date,
actualCloseDate: o.actualCloseDate || o.actual_close_date, deliveryStatus: o.deliveryStatus || o.delivery_status, itemCount: o.itemCount || o.item_count
}))
this.orderLoading = false
}).catch(() => { this.orderLoading = false })
},
showOrderDetail(row) {
getDelivery(row.doId || row.do_id).then(r => { this.detailData = r.data; this.detailOpen = true }).catch(() => {})
},
statusType(s) { return { pending: "warning", transit: "primary", history: "success" }[s] || "" },
statusLabel(s) { return { pending: "待发", transit: "在途", history: "已收货" }[s] || s || "-" }
}
}
</script>
<style scoped>
/* ═══════ 整体布局 ═══════ */
.client-manage {
padding: 12px;
background: #f5f7fa;
min-height: calc(100vh - 84px);
}
.client-manage ::v-deep .el-tabs__header {
background: #fff;
padding: 0 16px;
margin: 0;
border-radius: 4px 4px 0 0;
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}
.client-manage ::v-deep .el-tabs__content {
background: #fff;
padding: 16px;
border-radius: 0 0 4px 4px;
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}
/* ═══════ 工具栏 ═══════ */
.toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.toolbar-right { margin-left: auto; }
.order-hint {
margin-left: 12px;
font-size: 12px;
color: #909399;
}
/* ═══════ 金额 ═══════ */
.amount {
color: #409EFF;
font-weight: 700;
}
/* ═══════ 空状态 ═══════ */
.empty-box {
text-align: center;
padding: 60px 20px;
color: #c0c4cc;
}
.empty-box i { font-size: 48px; display: block; margin-bottom: 12px; }
.empty-box p { font-size: 14px; margin: 0; }
/* ═══════ 详情弹窗 ═══════ */
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border: 1px solid #ebeef5;
border-radius: 4px;
margin-bottom: 16px;
}
.detail-item {
display: flex;
border-bottom: 1px solid #ebeef5;
}
.detail-item:nth-last-child(-n+2) { border-bottom: none; }
.detail-item:nth-child(odd) { border-right: 1px solid #ebeef5; }
.dl {
width: 90px;
flex-shrink: 0;
background: #f5f7fa;
padding: 10px 12px;
font-size: 12px;
color: #606266;
font-weight: 600;
border-right: 1px solid #ebeef5;
}
.dv {
padding: 10px 12px;
font-size: 13px;
color: #303133;
flex: 1;
}
.detail-remark {
padding: 8px 12px;
background: #fdf6ec;
border: 1px solid #faecd8;
border-radius: 4px;
font-size: 12px;
color: #e6a23c;
margin-bottom: 16px;
}
.section-bar {
font-size: 13px;
font-weight: 700;
color: #1a2c4e;
padding: 8px 0;
margin-bottom: 10px;
border-bottom: 2px solid #1171c4;
padding-left: 8px;
}
/* ═══════ 弹窗统一样式 ═══════ */
.client-dialog ::v-deep .el-dialog__body { padding: 20px 30px; }
</style>