Files
klp-oa/klp-ui/src/views/crm/orderItem/index.vue
砂糖 379aa9d44b feat(crm): 优化合同模板管理和订单明细页面功能
1. 合同模板管理:添加选中状态高亮样式,新增当前选中模板状态管理
2. 订单明细页面:
   - 合并搜索框为产品名称/材质统一搜索
   - 重构表格数据结构,拆分合同与产品明细展示
   - 添加批量/单行保存功能,自动计算产品金额字段
   - 优化筛选条件和默认日期范围,调整表格列样式与配置
2026-06-18 10:27:09 +08:00

745 lines
23 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="order-item-page">
<!-- 筛选栏 -->
<div class="filter-section">
<el-input
v-model="queryParams.contractCode"
placeholder="合同号"
size="small"
clearable
style="width: 140px"
@keyup.enter.native="handleQuery"
/>
<el-input
v-model="queryParams.customer"
placeholder="客户名称"
size="small"
clearable
style="width: 140px"
@keyup.enter.native="handleQuery"
/>
<el-input
v-model="queryParams.keyword"
placeholder="产品名称/材质"
size="small"
clearable
style="width: 160px"
@keyup.enter.native="handleQuery"
/>
<el-date-picker
v-model="signDateRange"
type="daterange"
range-separator=""
start-placeholder="签订开始"
end-placeholder="签订结束"
size="small"
style="width: 220px"
value-format="yyyy-MM-dd"
:unlink-panels="true"
@change="handleSignDateChange"
/>
<el-date-picker
v-model="deliveryDateRange"
type="daterange"
range-separator=""
start-placeholder="交货开始"
end-placeholder="交货结束"
size="small"
style="width: 220px"
value-format="yyyy-MM-dd"
:unlink-panels="true"
@change="handleDeliveryDateChange"
/>
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">筛选</el-button>
<el-button size="small" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
<span class="sort-hint">已按合同签订日期默认当月结果按交货日期倒序排列</span>
</div>
<!-- 合同产品表格 -->
<div class="table-section">
<el-table
v-loading="loading"
:data="tableList"
size="small"
border
style="width: 100%"
class="order-item-table"
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 600 }"
:row-class-name="groupRowClassName"
>
<!-- 操作列 -->
<el-table-column label="操作" width="70" fixed="left" align="center">
<template slot-scope="{ row }">
<el-button
type="text"
size="small"
icon="el-icon-check"
style="color: #67c23a"
@click="handleSaveRow(row)"
>保存</el-button>
</template>
</el-table-column>
<!-- 合同信息列只读 -->
<el-table-column label="合同信息" align="center">
<el-table-column label="合同号" prop="contractCode" min-width="160">
<template slot-scope="{ row }">
<span class="contract-info" :title="row.contractCode">{{ row.contractCode }}</span>
</template>
</el-table-column>
<el-table-column label="供方" prop="supplier" min-width="180">
<template slot-scope="{ row }">
<span class="contract-info" :title="row.supplier">{{ row.supplier }}</span>
</template>
</el-table-column>
<el-table-column label="需方" prop="customer" min-width="200">
<template slot-scope="{ row }">
<span class="contract-info" :title="row.customer">{{ row.customer }}</span>
</template>
</el-table-column>
<el-table-column label="签订日期" prop="signTime" width="100">
<template slot-scope="{ row }">
<span class="contract-info contract-date">{{ formatDate(row.signTime) }}</span>
</template>
</el-table-column>
<el-table-column label="交货日期" prop="deliveryDate" width="100">
<template slot-scope="{ row }">
<span class="contract-info contract-date">{{ formatDate(row.deliveryDate) }}</span>
</template>
</el-table-column>
</el-table-column>
<!-- 产品明细列可编辑 -->
<el-table-column label="产品明细" align="center">
<el-table-column label="产品名称" prop="productName" min-width="120">
<template slot-scope="{ row }">
<span class="contract-info contract-product-name" :title="row.productName">{{ row.productName }}</span>
</template>
</el-table-column>
<el-table-column label="规格(mm)" prop="spec" min-width="120">
<template slot-scope="scope">
<div class="editable-cell-wrapper">
<el-input
v-model="scope.row.spec"
size="small"
class="editable-cell"
:title="scope.row.spec"
@blur="handleProductChange(scope.row)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="材质" prop="material" width="110">
<template slot-scope="scope">
<div class="editable-cell-wrapper">
<el-select
v-model="scope.row.material"
size="small"
class="editable-cell-blue select-material"
clearable
filterable
allow-create
@change="handleProductChange(scope.row)"
>
<el-option
v-for="m in materialOptions"
:key="m.value"
:label="m.label"
:value="m.value"
/>
</el-select>
</div>
</template>
</el-table-column>
<el-table-column label="数量(吨)" prop="quantity" width="90">
<template slot-scope="scope">
<div class="editable-cell-wrapper">
<el-input
v-model="scope.row.quantity"
size="small"
class="editable-cell-green"
:title="String(scope.row.quantity)"
@blur="handleProductChange(scope.row)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="含税单价" prop="taxPrice" width="100">
<template slot-scope="scope">
<div class="editable-cell-wrapper">
<el-input
v-model="scope.row.taxPrice"
size="small"
class="editable-cell-green"
:title="String(scope.row.taxPrice)"
@blur="handleProductChange(scope.row)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="税率除数" prop="taxDivisor" width="85">
<template slot-scope="scope">
<div class="editable-cell-wrapper">
<el-input
v-model="scope.row.taxDivisor"
size="small"
class="editable-cell-blue"
:title="String(scope.row.taxDivisor)"
@blur="handleProductChange(scope.row)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="无税单价" prop="noTaxPrice" width="100">
<template slot-scope="scope">
<div class="editable-cell-wrapper">
<el-input
v-model="scope.row.noTaxPrice"
size="small"
class="editable-cell-green"
:title="String(scope.row.noTaxPrice)"
@blur="handleProductChange(scope.row)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="含税总额" prop="taxTotal" width="110">
<template slot-scope="scope">
<div class="editable-cell-wrapper">
<el-input
v-model="scope.row.taxTotal"
size="small"
class="editable-cell-green"
:title="String(scope.row.taxTotal)"
@blur="handleProductChange(scope.row)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="无税总额" prop="noTaxTotal" width="110">
<template slot-scope="scope">
<div class="editable-cell-wrapper">
<el-input
v-model="scope.row.noTaxTotal"
size="small"
class="editable-cell-green"
:title="String(scope.row.noTaxTotal)"
@blur="handleProductChange(scope.row)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="税额" prop="taxAmount" width="100">
<template slot-scope="scope">
<div class="editable-cell-wrapper">
<el-input
v-model="scope.row.taxAmount"
size="small"
class="editable-cell-green"
:title="String(scope.row.taxAmount)"
@blur="handleProductChange(scope.row)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="150">
<template slot-scope="scope">
<div class="editable-cell-wrapper">
<el-input
v-model="scope.row.remark"
size="small"
class="editable-cell-pink"
:title="scope.row.remark"
@blur="handleProductChange(scope.row)"
/>
</div>
</template>
</el-table-column>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</div>
</template>
<script>
import { listOrder, updateOrder } from '@/api/crm/order'
import { parseProductContent, stringifyProductContent, calculateProductFields, recalculateTotals } from '@/utils/productContent'
export default {
name: 'OrderItemList',
data() {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const firstDay = `${year}-${month}-01`
const lastDay = new Date(year, now.getMonth() + 1, 0)
const lastDayStr = `${year}-${month}-${String(lastDay.getDate()).padStart(2, '0')}`
return {
loading: false,
tableList: [],
total: 0,
signDateRange: [firstDay, lastDayStr],
deliveryDateRange: null,
queryParams: {
pageNum: 1,
pageSize: 50,
contractCode: undefined,
customer: undefined,
keyword: undefined,
signDateStart: firstDay,
signDateEnd: lastDayStr,
deliveryDateStart: undefined,
deliveryDateEnd: undefined
},
originalData: {},
materialOptions: [
{ label: 'SPCC', value: 'SPCC' },
{ label: 'DX51D+Z', value: 'DX51D+Z' },
{ label: 'DC01', value: 'DC01' },
{ label: 'DC01-H', value: 'DC01-H' }
]
}
},
created() {
this.getList()
},
methods: {
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
if (isNaN(date.getTime())) {
return String(dateStr).substring(0, 10)
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
getList() {
this.loading = true
listOrder(this.queryParams).then(res => {
const rows = []
const orders = res.rows || []
orders.forEach(order => {
const parsed = parseProductContent(order.productContent)
const products = parsed.products || []
const productName = parsed.productName || ''
products.forEach((product, index) => {
rows.push({
_order: { ...order },
orderId: order.orderId,
contractCode: order.contractCode,
supplier: order.supplier,
customer: order.customer,
signTime: order.signTime,
deliveryDate: order.deliveryDate,
productName: productName,
productIndex: index,
spec: product.spec || '',
material: product.material || '',
quantity: product.quantity || 0,
taxPrice: product.taxPrice || 0,
taxDivisor: product.taxDivisor || 1.13,
noTaxPrice: product.noTaxPrice || 0,
taxTotal: product.taxTotal || 0,
noTaxTotal: product.noTaxTotal || 0,
taxAmount: product.taxAmount || 0,
remark: product.remark || ''
})
})
if (products.length === 0) {
rows.push({
_order: { ...order },
orderId: order.orderId,
contractCode: order.contractCode,
supplier: order.supplier,
customer: order.customer,
signTime: order.signTime,
deliveryDate: order.deliveryDate,
productName: productName,
productIndex: -1,
spec: '',
material: '',
quantity: 0,
taxPrice: 0,
taxDivisor: 1.13,
noTaxPrice: 0,
taxTotal: 0,
noTaxTotal: 0,
taxAmount: 0,
remark: ''
})
}
})
rows.forEach(row => {
row.noTaxPrice = this.format3Decimal(row.noTaxPrice)
row.taxTotal = this.format3Decimal(row.taxTotal)
row.noTaxTotal = this.format3Decimal(row.noTaxTotal)
row.taxAmount = this.format3Decimal(row.taxAmount)
})
this.tableList = rows
this.total = res.total || 0
this.originalData = {}
rows.forEach((row, i) => {
this.originalData[i] = JSON.stringify({
spec: row.spec,
material: row.material,
quantity: row.quantity,
taxPrice: row.taxPrice,
taxDivisor: row.taxDivisor,
noTaxPrice: row.noTaxPrice,
taxTotal: row.taxTotal,
noTaxTotal: row.noTaxTotal,
taxAmount: row.taxAmount,
remark: row.remark
})
})
}).catch(() => {
this.tableList = []
this.total = 0
}).finally(() => {
this.loading = false
})
},
handleProductChange(row) {
const updated = calculateProductFields(row, 'quantity')
Object.assign(row, {
noTaxPrice: this.format3Decimal(updated.noTaxPrice),
taxTotal: this.format3Decimal(updated.taxTotal),
noTaxTotal: this.format3Decimal(updated.noTaxTotal),
taxAmount: this.format3Decimal(updated.taxAmount),
taxDivisor: updated.taxDivisor
})
},
format3Decimal(v) {
return Number(v).toFixed(3)
},
saveOrderProducts(row, silent = false) {
if (!row.orderId) {
return Promise.resolve()
}
return new Promise((resolve, reject) => {
const orderRows = this.tableList.filter(r => r.orderId === row.orderId)
const newProducts = orderRows
.filter(r => r.productIndex >= 0)
.sort((a, b) => a.productIndex - b.productIndex)
.map(r => ({
spec: r.spec,
material: r.material,
quantity: parseFloat(r.quantity) || 0,
taxPrice: parseFloat(r.taxPrice) || 0,
taxDivisor: parseFloat(r.taxDivisor) || 1.13,
noTaxPrice: parseFloat(r.noTaxPrice) || 0,
taxTotal: parseFloat(r.taxTotal) || 0,
noTaxTotal: parseFloat(r.noTaxTotal) || 0,
taxAmount: parseFloat(r.taxAmount) || 0,
remark: r.remark || ''
}))
const parsed = parseProductContent(row._order.productContent)
parsed.products = newProducts
const updatedContent = recalculateTotals(parsed)
const productContentJson = stringifyProductContent(updatedContent)
const orderData = { ...row._order }
orderData.productContent = productContentJson
updateOrder(orderData).then(() => {
row._order.productContent = productContentJson
orderRows.forEach(r => {
r._order.productContent = productContentJson
})
if (!silent) {
this.$message.success('保存成功')
}
resolve()
}).catch(() => {
if (!silent) {
this.$message.error('保存失败')
this.getList()
}
reject()
})
})
},
handleSaveAll() {
const orderIds = [...new Set(this.tableList.map(r => r.orderId).filter(Boolean))]
if (orderIds.length === 0) {
this.$message.warning('没有需要保存的数据')
return
}
this.loading = true
let savedCount = 0
let failedCount = 0
const promises = orderIds.map(orderId => {
const row = this.tableList.find(r => r.orderId === orderId)
return this.saveOrderProducts(row, true).then(() => {
savedCount++
}).catch(() => {
failedCount++
})
})
Promise.all(promises).then(() => {
this.loading = false
if (failedCount === 0) {
this.$message.success(`成功保存 ${savedCount} 个订单`)
} else {
this.$message.warning(`成功保存 ${savedCount} 个,${failedCount} 个失败`)
}
})
},
handleSaveRow(row) {
this.saveOrderProducts(row)
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
handleSignDateChange(val) {
if (val && val.length === 2) {
this.queryParams.signDateStart = val[0]
this.queryParams.signDateEnd = val[1]
} else {
this.queryParams.signDateStart = undefined
this.queryParams.signDateEnd = undefined
}
this.handleQuery()
},
handleDeliveryDateChange(val) {
if (val && val.length === 2) {
this.queryParams.deliveryDateStart = val[0]
this.queryParams.deliveryDateEnd = val[1]
} else {
this.queryParams.deliveryDateStart = undefined
this.queryParams.deliveryDateEnd = undefined
}
this.handleQuery()
},
resetQuery() {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const firstDay = `${year}-${month}-01`
const lastDay = new Date(year, now.getMonth() + 1, 0)
const lastDayStr = `${year}-${month}-${String(lastDay.getDate()).padStart(2, '0')}`
this.signDateRange = [firstDay, lastDayStr]
this.deliveryDateRange = null
this.queryParams.contractCode = undefined
this.queryParams.customer = undefined
this.queryParams.keyword = undefined
this.queryParams.signDateStart = firstDay
this.queryParams.signDateEnd = lastDayStr
this.queryParams.deliveryDateStart = undefined
this.queryParams.deliveryDateEnd = undefined
this.queryParams.pageNum = 1
this.getList()
},
groupRowClassName({ row, rowIndex }) {
if (row.productIndex === 0) {
return 'group-row-a'
}
if (row.productIndex > 0) {
return 'group-row-b'
}
return rowIndex % 2 === 0 ? 'group-row-a' : 'group-row-b'
}
}
}
</script>
<style scoped>
.order-item-page {
padding: 16px;
min-height: 100%;
}
.filter-section {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
padding: 12px 16px;
background: #fff;
border-radius: 6px;
border: 1px solid #ebeef5;
}
.table-section {
background: #fff;
border-radius: 6px;
border: 1px solid #ebeef5;
padding: 16px;
}
.contract-info {
color: #606266;
background: #f5f7fa;
padding: 4px 6px;
border-radius: 4px;
display: inline-block;
width: 100%;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.contract-product-name {
background: #e6f0fa;
font-weight: 600;
}
.contract-date {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 11px;
letter-spacing: -0.3px;
padding: 4px 4px;
text-align: center;
}
.editable-cell-wrapper {
width: 100%;
}
.editable-cell ::v-deep .el-input__inner {
background-color: #fff3e6;
border-color: transparent;
padding: 0 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.editable-cell ::v-deep .el-input__inner:focus {
border-color: #5F7BA0;
background-color: #fff;
overflow: visible;
white-space: normal;
}
.editable-cell-blue ::v-deep .el-input__inner {
background-color: #e6f7ff;
border-color: transparent;
padding: 0 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.editable-cell-blue ::v-deep .el-input__inner:focus {
border-color: #5F7BA0;
background-color: #fff;
overflow: visible;
white-space: normal;
}
.select-material ::v-deep .el-input__inner {
background-color: #e6f7ff;
border-color: transparent;
padding: 0 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.select-material ::v-deep .el-input__inner:focus {
border-color: #5F7BA0;
background-color: #fff;
overflow: visible;
white-space: normal;
}
.editable-cell-green ::v-deep .el-input__inner {
background-color: #f6ffed;
border-color: transparent;
padding: 0 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.editable-cell-green ::v-deep .el-input__inner:focus {
border-color: #5F7BA0;
background-color: #fff;
overflow: visible;
white-space: normal;
}
.editable-cell-pink ::v-deep .el-input__inner {
background-color: #fff0f6;
border-color: transparent;
padding: 0 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.editable-cell-pink ::v-deep .el-input__inner:focus {
border-color: #5F7BA0;
background-color: #fff;
overflow: visible;
white-space: normal;
}
.order-item-table ::v-deep .el-input__inner {
border-color: transparent;
}
.order-item-table ::v-deep .el-input__inner:focus {
border-color: #5F7BA0;
}
.order-item-table ::v-deep .el-table__body td {
padding: 4px 0;
white-space: normal !important;
word-break: break-all !important;
}
.order-item-table ::v-deep .el-table__body td .cell {
white-space: normal !important;
word-break: break-all !important;
line-height: 1.4;
}
::v-deep .el-button--primary {
color: #fff !important;
background: #5F7BA0 !important;
border-color: #5F7BA0 !important;
}
::v-deep .el-button--primary:hover,
::v-deep .el-button--primary:focus {
background: #4d6a8e !important;
border-color: #4d6a8e !important;
}
.sort-hint {
margin-left: auto;
font-size: 12px;
color: #909399;
white-space: nowrap;
}
::v-deep .el-table .group-row-a td {
background-color: #ffffff !important;
}
::v-deep .el-table .group-row-b td {
background-color: #fafcff !important;
}
</style>