feat/合同模板管理

This commit is contained in:
季万俊 2026-01-13 17:59:30 +08:00
parent a19f48034d
commit 5fb095f392
4 changed files with 3517 additions and 272 deletions

File diff suppressed because it is too large Load Diff

100
src/api/contractTemplate.js Normal file
View File

@ -0,0 +1,100 @@
import request from '@/utils/request'
// 合同模板列表
export function listContractTemplates(query) {
return request({
url: '/api/v1/contract-template',
method: 'get',
params: {
contractType: query.contractType,
templateName: query.templateName,
pageSize: query.pageSize,
pageIndex: query.pageNum
}
})
}
// 获取单个合同模板详情
export function getContractTemplate(id) {
return request({
url: `/api/v1/contract-template/${id}`,
method: 'get'
})
}
// 新增合同模板(文件必填)
export function createContractTemplate(data) {
const formData = new FormData()
formData.append('contractType', data.contractType)
formData.append('templateName', data.templateName)
formData.append('publishDate', data.publishDate)
if (data.file) {
formData.append('file', data.file)
}
return request({
url: '/api/v1/contract-template',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
repeatSubmit: false
}
})
}
// 修改合同模板(文件可选)
export function updateContractTemplate(data) {
const formData = new FormData()
if (data.contractType) {
formData.append('contractType', data.contractType)
}
if (data.templateName) {
formData.append('templateName', data.templateName)
}
if (data.publishDate) {
formData.append('publishDate', data.publishDate)
}
if (data.file) {
formData.append('file', data.file)
}
return request({
url: `/api/v1/contract-template/${data.id}`,
method: 'put',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
repeatSubmit: false
}
})
}
// 仅更新模板文件
export function updateContractTemplateFile(id, file) {
const formData = new FormData()
formData.append('file', file)
return request({
url: `/api/v1/contract-template/${id}/file`,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
repeatSubmit: false
}
})
}
// 删除合同模板(支持批量)
export function deleteContractTemplates(ids) {
return request({
url: '/api/v1/contract-template',
method: 'delete',
data: {
ids
}
})
}

View File

@ -86,25 +86,7 @@ export const constantRoutes = [
] ]
}, },
// ==================== 采购管理 ==================== // ==================== 采购管理 ====================
{
path: '/purchase',
component: Layout,
redirect: 'noRedirect',
alwaysShow: true,
name: 'Purchase',
meta: {
title: '采购管理',
icon: 'shopping'
},
children: [
{
path: 'task-management',
component: () => import('@/views/purchase/task-management'),
name: 'PurchaseTaskManagement',
meta: { title: '采购任务管理', icon: 'list' }
}
]
},
// { // {
// path: '/purchase', // path: '/purchase',
// component: Layout, // component: Layout,
@ -117,6 +99,12 @@ export const constantRoutes = [
// }, // },
// children: [ // children: [
// { // {
// path: 'task-management',
// component: () => import('@/views/purchase/task-management'),
// name: 'PurchaseTaskManagement',
// meta: { title: '采购任务管理', icon: 'list' }
// }
// {
// path: 'order-list', // path: 'order-list',
// component: () => import('@/views/purchase/order-list'), // component: () => import('@/views/purchase/order-list'),
// name: 'PurchaseOrderList', // name: 'PurchaseOrderList',

View File

@ -3,6 +3,14 @@
<el-card> <el-card>
<div class="filter-toolbar"> <div class="filter-toolbar">
<el-form :model="queryParams" ref="queryForm" :inline="true"> <el-form :model="queryParams" ref="queryForm" :inline="true">
<el-form-item label="模板名称" prop="templateName">
<el-input
v-model="queryParams.templateName"
placeholder="请输入模板名称"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="合同类型" prop="contractType"> <el-form-item label="合同类型" prop="contractType">
<el-select v-model="queryParams.contractType" placeholder="全部" clearable style="width: 150px"> <el-select v-model="queryParams.contractType" placeholder="全部" clearable style="width: 150px">
<el-option label="全部" value="" /> <el-option label="全部" value="" />
@ -19,13 +27,13 @@
</div> </div>
<el-table v-loading="loading" :data="templateList" border style="width: 100%; margin-top: 20px;"> <el-table v-loading="loading" :data="templateList" border style="width: 100%; margin-top: 20px;">
<el-table-column prop="contractCode" label="合同编号" /> <el-table-column prop="templateName" label="模板名称" />
<el-table-column prop="contractName" label="合同名称" />
<el-table-column prop="contractType" label="合同类型"> <el-table-column prop="contractType" label="合同类型">
<template #default="scope"> <template #default="scope">
{{ getContractTypeName(scope.row.contractType) }} {{ getContractTypeName(scope.row.contractType) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="publishDate" label="发布日期" />
<el-table-column label="操作" width="280" fixed="right" align="center"> <el-table-column label="操作" width="280" fixed="right" align="center">
<template #default="scope"> <template #default="scope">
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button> <el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
@ -58,18 +66,10 @@
:rules="templateFormRules" :rules="templateFormRules"
label-width="100px" label-width="100px"
> >
<el-form-item label="合同编号" prop="contractCode"> <el-form-item label="模板名称" prop="templateName">
<el-input <el-input
v-model="templateForm.contractCode" v-model="templateForm.templateName"
placeholder="请输入合同编号" placeholder="请输入模板名称"
:disabled="isEdit"
maxlength="50"
/>
</el-form-item>
<el-form-item label="合同名称" prop="contractName">
<el-input
v-model="templateForm.contractName"
placeholder="请输入合同名称"
maxlength="100" maxlength="100"
/> />
</el-form-item> </el-form-item>
@ -83,14 +83,22 @@
<el-option label="销售合同" value="sales" /> <el-option label="销售合同" value="sales" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="发布日期" prop="publishDate">
<el-date-picker
v-model="templateForm.publishDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择发布日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="合同模板" prop="templateFile"> <el-form-item label="合同模板" prop="templateFile">
<el-upload <el-upload
ref="formUploadRef" ref="formUploadRef"
:file-list="formFileList" :file-list="formFileList"
:auto-upload="false" :auto-upload="false"
:on-change="handleFormFileChange" @change="handleFormFileChange"
:on-remove="handleFormFileRemove" @remove="handleFormFileRemove"
:limit="1"
accept=".doc,.docx,.xls,.xlsx,.pdf" accept=".doc,.docx,.xls,.xlsx,.pdf"
> >
<el-button type="primary">选择文件</el-button> <el-button type="primary">选择文件</el-button>
@ -100,11 +108,15 @@
</div> </div>
</template> </template>
</el-upload> </el-upload>
<div v-if="templateForm.templateFile" style="margin-top: 10px;"> <div v-if="templateForm.templateFilePath && !templateForm.file" style="margin-top: 10px;">
<el-button link type="primary" size="small" @click="handlePreviewFile(templateForm.templateFile)"> <span style="margin-right: 10px;">当前文件</span>
{{ templateForm.templateFile.name }} <el-button link type="primary" size="small" @click="handlePreviewFile(templateForm.templateFilePath)">
{{ getFileName(templateForm.templateFilePath) }}
</el-button> </el-button>
</div> </div>
<div v-if="templateForm.file" style="margin-top: 10px; color: #409eff;">
<span>已选择新文件{{ templateForm.file.name }}</span>
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@ -123,6 +135,7 @@
<script setup name="ContractTemplate"> <script setup name="ContractTemplate">
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { listContractTemplates, createContractTemplate, updateContractTemplate, deleteContractTemplates } from '@/api/contractTemplate'
import Pagination from '@/components/Pagination' import Pagination from '@/components/Pagination'
const loading = ref(false) const loading = ref(false)
@ -138,28 +151,41 @@ const formFileList = ref([])
const queryParams = reactive({ const queryParams = reactive({
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
templateName: '',
contractType: '' contractType: ''
}) })
const templateForm = reactive({ const templateForm = reactive({
id: null, id: null,
contractCode: '', templateName: '',
contractName: '',
contractType: '', contractType: '',
templateFile: null publishDate: '',
templateFile: '',
file: null,
templateFilePath: ''
}) })
const validateTemplateFile = (rule, value, callback) => {
if (!templateForm.file && !templateForm.templateFilePath) {
callback(new Error('请上传合同模板文件'))
} else {
callback()
}
}
const templateFormRules = { const templateFormRules = {
contractCode: [ templateName: [
{ required: true, message: '请输入合同编号', trigger: 'blur' }, { required: true, message: '请输入模板名称', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
contractName: [
{ required: true, message: '请输入合同名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' } { min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
], ],
contractType: [ contractType: [
{ required: true, message: '请选择合同类型', trigger: 'change' } { required: true, message: '请选择合同类型', trigger: 'change' }
],
publishDate: [
{ required: true, message: '请选择发布日期', trigger: 'change' }
],
templateFile: [
{ required: true, validator: validateTemplateFile, trigger: 'change' }
] ]
} }
@ -176,60 +202,58 @@ const getContractTypeName = (type) => {
const getList = async () => { const getList = async () => {
loading.value = true loading.value = true
try { try {
const mockData = JSON.parse(localStorage.getItem('mock_contract_templates') || '[]') const res = await listContractTemplates(queryParams)
// const pageData = res.data || {}
let filtered = mockData.filter(item => { const list = pageData.list || []
// total.value = pageData.count || 0
if (item.contractType === 'trade') return false
//
if (queryParams.contractType && item.contractType !== queryParams.contractType) return false
return true
})
// localStorage templateList.value = list.map(item => ({
if (filtered.length !== mockData.length) { ...item,
localStorage.setItem('mock_contract_templates', JSON.stringify(filtered)) publishDate: formatPublishDate(item.publishDate)
} }))
const start = (queryParams.pageNum - 1) * queryParams.pageSize
const end = start + queryParams.pageSize
templateList.value = filtered.slice(start, end)
total.value = filtered.length
if (filtered.length === 0) {
generateMockData()
}
} catch (error) { } catch (error) {
ElMessage.error('获取合同模板列表失败:' + error.message) ElMessage.error('获取合同模板列表失败:' + (error.message || '未知错误'))
} finally { } finally {
loading.value = false loading.value = false
} }
} }
const generateMockData = () => { // 2026-01-13T00:00:00+08:00
setTimeout(() => { // YYYY-MM-DD moment(value).format('YYYY-MM-DD')
const contractTypes = ['purchase', 'sales'] const formatPublishDate = (value) => {
const contractTypeNames = { if (!value) return ''
'purchase': '采购合同', // YYYY-MM-DD
'sales': '销售合同' if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
return value
} }
const newTemplates = [] // 10 2026-01-13
const now = Date.now() if (value.length >= 10) {
for (let i = 1; i <= 10; i++) { return value.substring(0, 10)
const contractType = contractTypes[Math.floor(Math.random() * contractTypes.length)]
newTemplates.push({
id: i,
contractCode: 'HT' + String(i).padStart(4, '0'),
contractName: contractTypeNames[contractType] + '模板_' + i,
contractType: contractType,
templateFile: i <= 3 ? { name: `合同模板_${i}.docx`, url: '#', type: 'file' } : null,
createTime: new Date(now - i * 30 * 86400000).toISOString(),
updateTime: new Date(now - i * 30 * 86400000).toISOString()
})
} }
localStorage.setItem('mock_contract_templates', JSON.stringify(newTemplates)) // Date YYYY-MM-DD
getList() const d = new Date(value)
}, 10) if (!isNaN(d.getTime())) {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
return value
}
const getFileName = (path) => {
if (!path) return ''
const parts = path.split('/')
return parts[parts.length - 1]
}
const getFileUrl = (path) => {
if (!path) return ''
if (/^https?:\/\//.test(path)) {
return path
}
const base = import.meta.env.VITE_APP_BASE_API || ''
return `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`
} }
const handleQuery = () => { const handleQuery = () => {
@ -238,29 +262,14 @@ const handleQuery = () => {
} }
const resetQuery = () => { const resetQuery = () => {
queryParams.templateName = ''
queryParams.contractType = '' queryParams.contractType = ''
handleQuery() handleQuery()
} }
const generateContractCode = () => {
const mockData = JSON.parse(localStorage.getItem('mock_contract_templates') || '[]')
let maxCode = 0
mockData.forEach(item => {
const match = item.contractCode.match(/^HT(\d+)$/)
if (match) {
const num = parseInt(match[1])
if (num > maxCode) {
maxCode = num
}
}
})
return 'HT' + String(maxCode + 1).padStart(4, '0')
}
const handleAdd = () => { const handleAdd = () => {
isEdit.value = false isEdit.value = false
resetForm() resetForm()
templateForm.contractCode = generateContractCode()
dialogVisible.value = true dialogVisible.value = true
} }
@ -268,17 +277,19 @@ const handleEdit = (row) => {
isEdit.value = true isEdit.value = true
Object.assign(templateForm, { Object.assign(templateForm, {
id: row.id, id: row.id,
contractCode: row.contractCode, templateName: row.templateName,
contractName: row.contractName,
contractType: row.contractType, contractType: row.contractType,
templateFile: row.templateFile || null publishDate: formatPublishDate(row.publishDate),
templateFile: row.templateFile ? '1' : '',
file: null,
templateFilePath: row.templateFile || ''
}) })
// //
if (row.templateFile) { if (row.templateFile) {
formFileList.value = [{ formFileList.value = [{
name: row.templateFile.name, name: getFileName(row.templateFile),
url: row.templateFile.url url: getFileUrl(row.templateFile)
}] }]
} else { } else {
formFileList.value = [] formFileList.value = []
@ -288,10 +299,10 @@ const handleEdit = (row) => {
} }
const handleDownload = (row) => { const handleDownload = (row) => {
if (row.templateFile && row.templateFile.url && row.templateFile.url !== '#') { if (row.templateFile) {
const link = document.createElement('a') const link = document.createElement('a')
link.href = row.templateFile.url link.href = getFileUrl(row.templateFile)
link.download = row.templateFile.name link.download = getFileName(row.templateFile)
link.click() link.click()
ElMessage.success('下载成功') ElMessage.success('下载成功')
} else { } else {
@ -304,57 +315,54 @@ const handleDelete = (row) => {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(async () => {
const templates = JSON.parse(localStorage.getItem('mock_contract_templates') || '[]') try {
const filtered = templates.filter(t => t.id != row.id) await deleteContractTemplates([row.id])
localStorage.setItem('mock_contract_templates', JSON.stringify(filtered))
ElMessage.success('删除成功') ElMessage.success('删除成功')
getList() getList()
} catch (error) {
ElMessage.error('删除失败:' + (error.message || '未知错误'))
}
}).catch(() => {}) }).catch(() => {})
} }
const handleFormFileChange = (file) => { const handleFormFileChange = (uploadFile, uploadFiles) => {
const rawFile = uploadFile?.raw
// 10MB // 10MB
const maxSize = 10 * 1024 * 1024 const maxSize = 10 * 1024 * 1024
if (file.raw && file.raw.size > maxSize) { if (rawFile && rawFile.size > maxSize) {
ElMessage.warning('文件大小不能超过 10MB') ElMessage.warning('文件大小不能超过 10MB')
formUploadRef.value?.handleRemove(file) formUploadRef.value?.clearFiles()
return return
} }
// base64 //
if (file.raw) { formFileList.value = uploadFile ? [uploadFile] : []
const reader = new FileReader()
reader.onload = (e) => {
const fileType = file.raw.type
let type = 'file'
if (fileType.includes('pdf')) {
type = 'pdf'
} else if (fileType.includes('word') || fileType.includes('document')) {
type = 'doc'
} else if (fileType.includes('sheet') || fileType.includes('excel')) {
type = 'xls'
}
templateForm.templateFile = { //
name: file.name, if (rawFile) {
url: e.target.result, templateForm.file = rawFile
type: type, templateForm.templateFile = '1'
size: file.raw.size //
if (isEdit.value) {
templateForm.templateFilePath = ''
} }
} templateFormRef.value?.validateField('templateFile')
reader.readAsDataURL(file.raw)
} }
} }
const handleFormFileRemove = () => { const handleFormFileRemove = () => {
templateForm.templateFile = null templateForm.file = null
templateForm.templateFilePath = ''
templateForm.templateFile = ''
formFileList.value = [] formFileList.value = []
templateFormRef.value?.validateField('templateFile')
} }
const handlePreviewFile = (file) => { const handlePreviewFile = (file) => {
if (file && file.url && file.url !== '#') { if (file) {
window.open(file.url, '_blank') window.open(getFileUrl(file), '_blank')
} else { } else {
ElMessage.warning('文件不存在') ElMessage.warning('文件不存在')
} }
@ -363,10 +371,12 @@ const handlePreviewFile = (file) => {
const resetForm = () => { const resetForm = () => {
Object.assign(templateForm, { Object.assign(templateForm, {
id: null, id: null,
contractCode: '', templateName: '',
contractName: '',
contractType: '', contractType: '',
templateFile: null publishDate: '',
templateFile: '',
file: null,
templateFilePath: ''
}) })
formFileList.value = [] formFileList.value = []
templateFormRef.value?.clearValidate() templateFormRef.value?.clearValidate()
@ -380,65 +390,36 @@ const handleSubmit = async () => {
if (!valid) return if (!valid) return
submitLoading.value = true submitLoading.value = true
;(async () => {
try { try {
const mockData = JSON.parse(localStorage.getItem('mock_contract_templates') || '[]')
const now = new Date().toISOString()
if (isEdit.value) { if (isEdit.value) {
// //
const index = mockData.findIndex(item => item.id === templateForm.id) await updateContractTemplate({
if (index !== -1) { id: templateForm.id,
//
const codeExists = mockData.find(item =>
item.id !== templateForm.id &&
item.contractCode === templateForm.contractCode
)
if (codeExists) {
ElMessage.warning('合同编号已存在')
submitLoading.value = false
return
}
mockData[index] = {
...mockData[index],
contractCode: templateForm.contractCode,
contractName: templateForm.contractName,
contractType: templateForm.contractType, contractType: templateForm.contractType,
templateFile: templateForm.templateFile, templateName: templateForm.templateName,
updateTime: now publishDate: templateForm.publishDate,
} file: templateForm.file || undefined
} })
} else { } else {
// //
// await createContractTemplate({
const codeExists = mockData.find(item => item.contractCode === templateForm.contractCode)
if (codeExists) {
ElMessage.warning('合同编号已存在')
submitLoading.value = false
return
}
const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1
mockData.push({
id: newId,
contractCode: templateForm.contractCode,
contractName: templateForm.contractName,
contractType: templateForm.contractType, contractType: templateForm.contractType,
templateFile: templateForm.templateFile, templateName: templateForm.templateName,
createTime: now, publishDate: templateForm.publishDate,
updateTime: now file: templateForm.file
}) })
} }
localStorage.setItem('mock_contract_templates', JSON.stringify(mockData))
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功') ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
dialogVisible.value = false dialogVisible.value = false
getList() getList()
} catch (error) { } catch (error) {
ElMessage.error('操作失败:' + error.message) ElMessage.error('操作失败:' + (error.message || '未知错误'))
} finally { } finally {
submitLoading.value = false submitLoading.value = false
} }
})()
}) })
} }