feat(contract): 添加合同模板管理功能并优化导出逻辑
- 新增合同模板管理组件,支持模板的增删改查 - 优化合同导出功能,从订单项获取产品数据并添加金额大写转换 - 在合同编辑页面添加模板选择功能 - 为字典键值字段添加溢出提示 - 将数据键值输入框改为多行文本框
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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="请输入样式属性" />
|
||||
|
||||
Reference in New Issue
Block a user