feat(contract): 添加合同模板管理功能并优化导出逻辑

- 新增合同模板管理组件,支持模板的增删改查
- 优化合同导出功能,从订单项获取产品数据并添加金额大写转换
- 在合同编辑页面添加模板选择功能
- 为字典键值字段添加溢出提示
- 将数据键值输入框改为多行文本框
This commit is contained in:
2026-04-21 09:38:42 +08:00
parent e22648bff0
commit 626aca5b85
4 changed files with 433 additions and 7 deletions

View File

@@ -121,6 +121,7 @@
<script>
import { listOrder, updateOrder } from "@/api/crm/order";
import { listOrderItem } from '@/api/crm/orderItem'
import * as ExcelJS from 'exceljs';
import { saveAs } from 'file-saver';
@@ -197,11 +198,73 @@ export default {
toggleMoreFilter() {
this.showMoreFilter = !this.showMoreFilter;
},
convertToChinese(amount) {
if (amount === 0) return '零元整'
const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
const units = ['', '拾', '佰', '仟']
const bigUnits = ['', '万', '亿']
let integerPart = Math.floor(amount)
let decimalPart = Math.round((amount - integerPart) * 100)
let result = ''
let unitIndex = 0
let bigUnitIndex = 0
if (integerPart === 0) {
result = '零'
} else {
while (integerPart > 0) {
let section = integerPart % 10000
if (section > 0) {
let sectionResult = ''
let sectionUnitIndex = 0
while (section > 0) {
let digit = section % 10
if (digit > 0) {
sectionResult = digits[digit] + units[sectionUnitIndex] + sectionResult
} else if (sectionResult && !sectionResult.startsWith('零')) {
sectionResult = '零' + sectionResult
}
section = Math.floor(section / 10)
sectionUnitIndex++
}
result = sectionResult + bigUnits[bigUnitIndex] + result
}
integerPart = Math.floor(integerPart / 10000)
bigUnitIndex++
}
}
result += '元'
if (decimalPart === 0) {
result += '整'
} else {
const jiao = Math.floor(decimalPart / 10)
const fen = decimalPart % 10
if (jiao > 0) {
result += digits[jiao] + '角'
}
if (fen > 0) {
result += digits[fen] + '分'
}
}
return result
},
/** 导出合同 */
async handleExport(row) {
// 1. 创建excel
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('产品销售合同');
let orderItems = [];
// 2. 查询合同详情
const res = await listOrderItem({ orderId: row.orderId, pageNum: 1, pageSize: 1000 });
orderItems = res.rows || [];
// 2. 设置列宽
worksheet.columns = [
@@ -345,9 +408,33 @@ export default {
totalAmountInWords: '零元整'
};
if (row.productContent) {
if (orderItems) {
try {
productData = JSON.parse(row.productContent);
// 改为从orderItems中获取产品内容
const productName = orderItems[0].productType || '冷硬钢卷';
const remark = row.remark || '';
const products = orderItems.map(item => ({
spec: item.finishedProductSpec || '',
material: item.material || '',
quantity: parseFloat(item.weight || 0),
taxPrice: parseFloat(item.contractPrice || 0),
noTaxPrice: parseFloat(item.itemAmount || 0),
taxTotal: parseFloat(item.contractPrice) * parseFloat(item.weight || 0),
remark: item.remark || ''
}));
const totalQuantity = products.reduce((acc, product) => acc + parseFloat(product.quantity || 0), 0);
const totalTaxTotal = products.reduce((acc, product) => acc + parseFloat(product.taxTotal || 0), 0);
const totalAmountInWords = this.convertToChinese(totalTaxTotal);
productData = {
products,
productName,
remark,
totalQuantity,
totalTaxTotal,
totalAmountInWords
};
} catch (error) {
console.error('解析产品内容失败:', error);
}

View File

@@ -0,0 +1,319 @@
<template>
<div>
<div class="template-buttons">
<el-button
v-for="template in contractTemplateList"
:key="template.dictValue"
@click="handleTemplateSelect(template)"
>
{{ template.dictLabel }}
</el-button>
<el-button type="primary" @click="handleContractManagement">管理合同</el-button>
</div>
<!-- 合同模板管理对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="1200px" append-to-body>
<div class="template-management-layout">
<!-- 左侧模板列表 -->
<div class="left-panel">
<div class="left-header">
<span>模板列表</span>
<el-button type="primary" size="small" @click="handleAdd">新增模板</el-button>
</div>
<div class="template-list">
<div
v-for="template in contractTemplateList"
:key="template.dictCode"
class="template-item"
:class="{ active: selectedTemplate && selectedTemplate.dictCode === template.dictCode }"
@click="selectTemplate(template)"
>
<span class="template-name">{{ template.dictLabel }}</span>
<el-button size="mini" @click.stop="handleDelete(template)">删除</el-button>
</div>
</div>
</div>
<!-- 右侧编辑区域 -->
<div class="right-panel">
<div class="right-header">
<el-input v-model="selectedTemplate.dictLabel"></el-input>
</div>
<div class="editor-container">
<editor v-if="selectedTemplate" v-model="selectedTemplate.dictValue" height="400"/>
<div v-else class="empty-template">
请选择一个模板进行编辑
</div>
</div>
<div class="right-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveTemplate" :disabled="!selectedTemplate" :loading="saveLoading">保存</el-button>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import { getDicts, addData, updateData, deleteData } from "@/api/system/dict/data";
export default {
name: "ContractTemplateManager",
props: {
// 可以根据需要添加props
},
data() {
return {
// 合同模板列表
contractTemplateList: [],
// 管理对话框
dialogVisible: false,
dialogTitle: "合同模板管理",
// 选中的模板
selectedTemplate: null,
// 保存按钮loading状态
saveLoading: false,
// 表单数据
form: {
dictLabel: '',
dictValue: '',
dictType: 'crm_contract_template',
status: '0',
sort: 0
},
// 表单校验规则
rules: {
dictLabel: [
{ required: true, message: "模板名称不能为空", trigger: "blur" }
],
dictValue: [
{ required: true, message: "模板内容不能为空", trigger: "blur" }
]
}
};
},
created() {
this.getDictList();
},
methods: {
getDictList() {
getDicts('crm_contract_template').then(res => {
this.contractTemplateList = res.data || [];
// 如果有模板,默认选择第一个
if (this.contractTemplateList.length > 0 && !this.selectedTemplate) {
this.selectedTemplate = this.contractTemplateList[0];
}
});
},
/** 处理合同模板选择 */
handleTemplateSelect(template) {
// 抛出选择事件,将选中的模板数据传出
this.$emit('select', template);
},
/** 处理合同管理 */
handleContractManagement() {
this.getDictList();
this.dialogVisible = true;
},
/** 选择模板 */
selectTemplate(template) {
this.selectedTemplate = template;
},
/** 新增模板 */
handleAdd() {
const newTemplate = {
dictLabel: '新模板',
dictValue: '',
dictType: 'crm_contract_template',
status: '0',
sort: 0
};
this.selectedTemplate = newTemplate;
},
/** 编辑模板 */
handleEdit(template) {
this.selectedTemplate = template;
},
/** 删除模板 */
handleDelete(template) {
this.$confirm('确定要删除这个模板吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteData(template.dictCode).then(res => {
if (res.code === 200) {
this.$message.success('删除成功');
this.getDictList();
}
});
});
},
/** 保存模板 */
saveTemplate() {
if (!this.selectedTemplate) return;
this.saveLoading = true;
if (this.selectedTemplate.dictCode) {
// 编辑
updateData(this.selectedTemplate).then(res => {
if (res.code === 200) {
this.$message.success('保存成功');
this.getDictList();
}
}).finally(() => {
this.saveLoading = false;
});
} else {
// 新增
addData(this.selectedTemplate).then(res => {
if (res.code === 200) {
this.$message.success('保存成功');
this.getDictList();
}
}).finally(() => {
this.saveLoading = false;
});
}
}
}
};
</script>
<style scoped>
.template-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.template-management-layout {
display: flex;
height: 70vh;
gap: 20px;
}
.left-panel {
width: 300px;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
flex-direction: column;
max-height: 100%;
}
.left-header {
padding: 12px 16px;
border-bottom: 1px solid #e4e7ed;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f5f7fa;
flex-shrink: 0;
}
.template-list {
flex: 1;
overflow-y: auto;
padding: 8px;
max-height: calc(100% - 52px); /* 减去头部高度 */
}
/* 自定义滚动条样式 */
.template-list::-webkit-scrollbar {
width: 6px;
}
.template-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.template-list::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.template-list::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.template-item {
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s;
display: flex;
justify-content: space-between;
align-items: center;
}
.template-item:hover {
border-color: #409eff;
}
.template-item.active {
border-color: #409eff;
background-color: #ecf5ff;
}
.template-name {
/* display: block; */
margin-bottom: 8px;
font-weight: 500;
}
.template-actions {
display: flex;
gap: 4px;
}
.right-panel {
flex: 1;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
flex-direction: column;
}
.right-header {
padding: 12px 16px;
border-bottom: 1px solid #e4e7ed;
background-color: #f5f7fa;
}
.editor-container {
flex: 1;
padding: 16px;
overflow: hidden;
}
.empty-template {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #909399;
}
.right-footer {
padding: 12px 16px;
border-top: 1px solid #e4e7ed;
display: flex;
justify-content: flex-end;
gap: 8px;
background-color: #f5f7fa;
}
</style>

View File

@@ -85,6 +85,7 @@
<ProductContent v-model="form.productContent" :readonly="false" />
</el-form-item> -->
<el-form-item label="合同内容">
<ContractTemplateManager @select="handleTemplateSelect" />
<editor v-model="form.contractContent" :min-height="192" />
</el-form-item>
@@ -161,17 +162,24 @@
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<el-dialog>
<div>
<!-- 左右布局左侧是合同标题右侧是合同模板内容可以新增删除修改合同模板 -->
</div>
</el-dialog>
</div>
</template>
<script>
import { delOrder, listOrderPackaging, updateOrder, getOrder, addOrder } from "@/api/crm/order";
import { getDicts, addData, updateData } from "@/api/system/dict/data";
import { listDeliveryWaybill } from "@/api/wms/deliveryWaybill";
import ContractList from "./components/ContractList.vue";
import ContractPreview from "./components/ContractPreview.vue";
import ContractTabs from "./components/ContractTabs.vue";
import ProductContent from "./components/ProductContent.vue";
import ContractTemplateManager from "./components/ContractTemplateManager.vue";
import CustomerSelect from "@/components/KLPService/CustomerSelect/index.vue";
export default {
@@ -181,8 +189,10 @@ export default {
ContractPreview,
ContractTabs,
ProductContent,
ContractTemplateManager,
CustomerSelect,
},
// 'crm_contract_template'
dicts: ['wip_pack_saleman'],
data() {
return {
@@ -284,10 +294,13 @@ export default {
status: [
{ required: true, message: "合同状态不能为空", trigger: "change" }
],
}
},
};
},
created() {
this.getDictList();
},
methods: {
/** 处理客户选择 */
handleCustomerChange(customer) {
@@ -313,6 +326,13 @@ export default {
console.log(customer);
},
/** 处理合同模板选择 */
handleTemplateSelect(template) {
this.form.contractContent = template.dictValue;
},
/** 处理合同状态更新 */
handleStatusChange(status) {
this.form.status = status;

View File

@@ -108,7 +108,7 @@
<el-tag v-else :type="scope.row.listClass == 'primary' ? '' : scope.row.listClass">{{scope.row.dictLabel}}</el-tag>
</template>
</el-table-column>
<el-table-column label="字典键值" align="center" prop="dictValue" />
<el-table-column label="字典键值" align="center" prop="dictValue" show-overflow-tooltip />
<el-table-column label="字典排序" align="center" prop="dictSort" />
<el-table-column label="状态" align="center" prop="status">
<template slot-scope="scope">
@@ -159,7 +159,7 @@
<el-input v-model="form.dictLabel" placeholder="请输入数据标签" />
</el-form-item>
<el-form-item label="数据键值" prop="dictValue">
<el-input v-model="form.dictValue" placeholder="请输入数据键值" />
<el-input type="textarea" v-model="form.dictValue" placeholder="请输入数据键值" />
</el-form-item>
<el-form-item label="样式属性" prop="cssClass">
<el-input v-model="form.cssClass" placeholder="请输入样式属性" />