This commit is contained in:
lixiaobang 2026-01-13 18:02:36 +08:00
commit 3293e5a068
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',
// component: Layout,
@ -117,6 +99,12 @@ export const constantRoutes = [
// },
// children: [
// {
// path: 'task-management',
// component: () => import('@/views/purchase/task-management'),
// name: 'PurchaseTaskManagement',
// meta: { title: '采购任务管理', icon: 'list' }
// }
// {
// path: 'order-list',
// component: () => import('@/views/purchase/order-list'),
// name: 'PurchaseOrderList',

View File

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