32 changed files with 1623 additions and 3039 deletions
@ -1,96 +0,0 @@ |
|||||
import request from '@/utils/request' |
|
||||
|
|
||||
/** |
|
||||
* 获取模板列表 |
|
||||
*/ |
|
||||
export function getDocumentTemplateList(params?: any) { |
|
||||
return request.get('/document_template/lists', { params }) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 获取模板详情 |
|
||||
*/ |
|
||||
export function getDocumentTemplateInfo(id: number) { |
|
||||
return request.get(`/document_template/info/${id}`) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 删除模板 |
|
||||
*/ |
|
||||
export function deleteDocumentTemplate(id: number) { |
|
||||
return request.delete(`/document_template/delete/${id}`) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 复制模板 |
|
||||
*/ |
|
||||
export function copyDocumentTemplate(id: number) { |
|
||||
return request.post(`/document_template/copy/${id}`) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 上传模板 |
|
||||
*/ |
|
||||
export function uploadDocumentTemplate(formData: FormData) { |
|
||||
return request.post('/document_template/upload', formData, { |
|
||||
headers: { |
|
||||
'Content-Type': 'multipart/form-data' |
|
||||
} |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 解析占位符 |
|
||||
*/ |
|
||||
export function parseDocumentPlaceholder(data: any) { |
|
||||
return request.post('/document_template/parse', data) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 预览模板 |
|
||||
*/ |
|
||||
export function previewDocumentTemplate(id: number) { |
|
||||
return request.get(`/document_template/preview/${id}`) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 保存占位符配置 |
|
||||
*/ |
|
||||
export function saveDocumentPlaceholderConfig(data: any) { |
|
||||
return request.post('/document_template/config/save', data) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 获取数据源列表 |
|
||||
*/ |
|
||||
export function getDocumentDataSources() { |
|
||||
return request.get('/document_template/datasources') |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 生成文档 |
|
||||
*/ |
|
||||
export function generateDocumentFile(data: any) { |
|
||||
return request.post('/document_template/generate', data) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 下载文档 |
|
||||
*/ |
|
||||
export function downloadDocumentFile(logId: number) { |
|
||||
return request.get(`/document_template/download/${logId}`, { responseType: 'blob' }) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 获取生成记录 |
|
||||
*/ |
|
||||
export function getDocumentGenerateLog(params?: any) { |
|
||||
return request.get('/document_template/log/lists', { params }) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 批量删除生成记录 |
|
||||
*/ |
|
||||
export function batchDeleteDocumentLog(ids: number[]) { |
|
||||
return request.post('/document_template/log/batch_delete', { ids }) |
|
||||
} |
|
||||
@ -1,12 +0,0 @@ |
|||||
<!-- |
|
||||
Word模板管理页面 - 路由入口文件 |
|
||||
此文件是为了匹配动态路由系统的组件加载规则而创建的 |
|
||||
实际功能组件位于: ./document-template/index.vue |
|
||||
--> |
|
||||
<template> |
|
||||
<DocumentTemplateIndex /> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
import DocumentTemplateIndex from './document-template/index.vue' |
|
||||
</script> |
|
||||
@ -1,300 +0,0 @@ |
|||||
<template> |
|
||||
<el-dialog v-model="visible" title="生成Word文档" width="800px" :before-close="handleClose"> |
|
||||
<div v-if="loading" class="text-center py-8"> |
|
||||
<el-icon class="is-loading"><Loading /></el-icon> |
|
||||
<span class="ml-2">加载中...</span> |
|
||||
</div> |
|
||||
|
|
||||
<div v-else> |
|
||||
<el-form :model="generateForm" label-width="120px" ref="generateFormRef"> |
|
||||
<!-- 模板选择 --> |
|
||||
<el-form-item label="选择模板" prop="template_id" :rules="[{required: true, message: '请选择模板'}]"> |
|
||||
<el-select v-model="generateForm.template_id" |
|
||||
@change="onTemplateChange" |
|
||||
placeholder="请选择要使用的模板" |
|
||||
style="width: 100%" |
|
||||
filterable> |
|
||||
<el-option v-for="template in activeTemplates" |
|
||||
:key="template.id" |
|
||||
:label="template.contract_name" |
|
||||
:value="template.id"> |
|
||||
<div class="flex justify-between items-center"> |
|
||||
<span>{{ template.contract_name }}</span> |
|
||||
<el-tag size="small" type="primary">{{ getTypeText(template.contract_type) }}</el-tag> |
|
||||
</div> |
|
||||
</el-option> |
|
||||
</el-select> |
|
||||
</el-form-item> |
|
||||
|
|
||||
<!-- 输出文件名 --> |
|
||||
<el-form-item label="输出文件名" prop="output_filename"> |
|
||||
<el-input v-model="generateForm.output_filename" |
|
||||
placeholder="不填写将自动生成文件名" |
|
||||
:suffix-icon="DocumentAdd"> |
|
||||
<template #append>.docx</template> |
|
||||
</el-input> |
|
||||
</el-form-item> |
|
||||
|
|
||||
<!-- 动态填充字段 --> |
|
||||
<div v-if="placeholderConfigs && Object.keys(placeholderConfigs).length > 0"> |
|
||||
<el-divider> |
|
||||
<span class="text-gray-600">数据填充</span> |
|
||||
</el-divider> |
|
||||
|
|
||||
<div class="space-y-4 max-h-96 overflow-y-auto p-4 bg-gray-50 rounded"> |
|
||||
<div v-for="(config, placeholder) in placeholderConfigs" |
|
||||
:key="placeholder" |
|
||||
class="bg-white p-4 rounded border"> |
|
||||
<div class="flex items-center justify-between mb-3"> |
|
||||
<label class="font-semibold text-gray-700"> |
|
||||
{{ config.name }} |
|
||||
<el-tag size="small" class="ml-2">{{ placeholder }}</el-tag> |
|
||||
</label> |
|
||||
<div class="text-sm text-gray-500"> |
|
||||
<span v-if="config.data_source === 'database'" class="text-blue-600"> |
|
||||
<el-icon><Database /></el-icon> |
|
||||
自动获取 |
|
||||
</span> |
|
||||
<span v-else class="text-orange-600"> |
|
||||
<el-icon><Edit /></el-icon> |
|
||||
手动填写 |
|
||||
<el-tag v-if="config.is_required" type="danger" size="small" class="ml-1">必填</el-tag> |
|
||||
</span> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- 手动填写的字段 --> |
|
||||
<div v-if="config.data_source === 'manual'"> |
|
||||
<el-form-item :prop="`fill_data.${placeholder}`" |
|
||||
:rules="config.is_required ? [{required: true, message: `${config.name}不能为空`}] : []"> |
|
||||
<el-input v-model="generateForm.fill_data[placeholder]" |
|
||||
:placeholder="`请输入${config.name}`" |
|
||||
:value="generateForm.fill_data[placeholder] || config.default_value"> |
|
||||
<template #prepend v-if="config.process_function"> |
|
||||
<el-tooltip :content="getProcessFunctionDesc(config.process_function)" placement="top"> |
|
||||
<el-icon><Magic /></el-icon> |
|
||||
</el-tooltip> |
|
||||
</template> |
|
||||
</el-input> |
|
||||
</el-form-item> |
|
||||
</div> |
|
||||
|
|
||||
<!-- 数据库字段显示 --> |
|
||||
<div v-else class="bg-blue-50 p-3 rounded text-sm"> |
|
||||
<div class="text-blue-700"> |
|
||||
<el-icon><Database /></el-icon> |
|
||||
数据来源:{{ getTableDisplayName(config.table_name) }} > {{ getFieldDisplayName(config.table_name, config.field_name) }} |
|
||||
</div> |
|
||||
<div v-if="config.process_function" class="text-blue-600 mt-1"> |
|
||||
<el-icon><Magic /></el-icon> |
|
||||
处理函数:{{ getProcessFunctionDesc(config.process_function) }} |
|
||||
</div> |
|
||||
<div v-if="config.default_value" class="text-blue-600 mt-1"> |
|
||||
<el-icon><Star /></el-icon> |
|
||||
默认值:{{ config.default_value }} |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- 无占位符提示 --> |
|
||||
<div v-else-if="generateForm.template_id" class="text-center py-8 text-gray-500"> |
|
||||
<el-icon size="48"><DocumentRemove /></el-icon> |
|
||||
<div class="mt-4">该模板暂无配置的占位符</div> |
|
||||
<div class="text-sm mt-2">请先配置模板的占位符</div> |
|
||||
</div> |
|
||||
</el-form> |
|
||||
</div> |
|
||||
|
|
||||
<template #footer> |
|
||||
<div class="dialog-footer"> |
|
||||
<el-button @click="handleClose">取消</el-button> |
|
||||
<el-button type="primary" |
|
||||
@click="generateDocument" |
|
||||
:loading="generateLoading" |
|
||||
:disabled="!generateForm.template_id"> |
|
||||
<el-icon v-if="!generateLoading"><Document /></el-icon> |
|
||||
{{ generateLoading ? '生成中...' : '生成文档' }} |
|
||||
</el-button> |
|
||||
</div> |
|
||||
</template> |
|
||||
</el-dialog> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
import { reactive, ref, onMounted } from 'vue' |
|
||||
import { ElMessage, type FormInstance } from 'element-plus' |
|
||||
import { getDocumentTemplateList, getDocumentTemplateInfo, generateDocumentFile, getDocumentDataSources } from '@/app/api/document' |
|
||||
|
|
||||
const emit = defineEmits(['complete']) |
|
||||
|
|
||||
// 组件状态 |
|
||||
const visible = ref(false) |
|
||||
const loading = ref(false) |
|
||||
const generateLoading = ref(false) |
|
||||
|
|
||||
// 数据 |
|
||||
const activeTemplates = ref([]) |
|
||||
const dataSources = ref({}) |
|
||||
const placeholderConfigs = ref({}) |
|
||||
|
|
||||
// 生成表单 |
|
||||
const generateForm = reactive({ |
|
||||
template_id: '', |
|
||||
output_filename: '', |
|
||||
fill_data: {} |
|
||||
}) |
|
||||
|
|
||||
// 组件引用 |
|
||||
const generateFormRef = ref<FormInstance>() |
|
||||
|
|
||||
// 打开生成对话框 |
|
||||
const open = async (preSelectedTemplate?: any) => { |
|
||||
visible.value = true |
|
||||
loading.value = true |
|
||||
|
|
||||
try { |
|
||||
// 获取活跃模板列表和数据源 |
|
||||
const [templatesResult, dataSourceResult] = await Promise.all([ |
|
||||
getDocumentTemplateList({ contract_status: 'active', limit: 100 }), |
|
||||
getDocumentDataSources() |
|
||||
]) |
|
||||
|
|
||||
activeTemplates.value = templatesResult.data.data |
|
||||
dataSources.value = dataSourceResult.data |
|
||||
|
|
||||
// 如果有预选模板,自动选择 |
|
||||
if (preSelectedTemplate) { |
|
||||
generateForm.template_id = preSelectedTemplate.id |
|
||||
await onTemplateChange() |
|
||||
} |
|
||||
} catch (error) { |
|
||||
ElMessage.error('加载数据失败') |
|
||||
console.error(error) |
|
||||
} finally { |
|
||||
loading.value = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 模板变化 |
|
||||
const onTemplateChange = async () => { |
|
||||
if (!generateForm.template_id) { |
|
||||
placeholderConfigs.value = {} |
|
||||
generateForm.fill_data = {} |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
const { data } = await getDocumentTemplateInfo(generateForm.template_id) |
|
||||
placeholderConfigs.value = data.placeholder_config || {} |
|
||||
|
|
||||
// 初始化填充数据 |
|
||||
generateForm.fill_data = {} |
|
||||
Object.keys(placeholderConfigs.value).forEach(placeholder => { |
|
||||
const config = placeholderConfigs.value[placeholder] |
|
||||
if (config.data_source === 'manual') { |
|
||||
generateForm.fill_data[placeholder] = config.default_value || '' |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
// 自动设置文件名 |
|
||||
const template = activeTemplates.value.find(t => t.id === generateForm.template_id) |
|
||||
if (template && !generateForm.output_filename) { |
|
||||
const timestamp = new Date().toISOString().slice(0, 16).replace(/[-:T]/g, '') |
|
||||
generateForm.output_filename = `${template.contract_name}_${timestamp}` |
|
||||
} |
|
||||
} catch (error) { |
|
||||
ElMessage.error('获取模板信息失败') |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 类型文本映射 |
|
||||
const getTypeText = (type: string) => { |
|
||||
const typeMap = { |
|
||||
'general': '通用', |
|
||||
'course': '课程', |
|
||||
'membership': '会员' |
|
||||
} |
|
||||
return typeMap[type] || '未知' |
|
||||
} |
|
||||
|
|
||||
// 获取表显示名称 |
|
||||
const getTableDisplayName = (tableName: string) => { |
|
||||
return dataSources.value[tableName]?.table_alias || tableName |
|
||||
} |
|
||||
|
|
||||
// 获取字段显示名称 |
|
||||
const getFieldDisplayName = (tableName: string, fieldName: string) => { |
|
||||
const fields = dataSources.value[tableName]?.fields || [] |
|
||||
const field = fields.find(f => f.field_name === fieldName) |
|
||||
return field?.field_alias || fieldName |
|
||||
} |
|
||||
|
|
||||
// 获取处理函数描述 |
|
||||
const getProcessFunctionDesc = (funcName: string) => { |
|
||||
const funcMap = { |
|
||||
'formatDate': '格式化为:2024年01月01日', |
|
||||
'formatDateTime': '格式化为:2024年01月01日 10:30', |
|
||||
'formatNumber': '格式化为数字:1,234.56', |
|
||||
'toUpper': '转换为大写', |
|
||||
'toLower': '转换为小写' |
|
||||
} |
|
||||
return funcMap[funcName] || funcName |
|
||||
} |
|
||||
|
|
||||
// 生成文档 |
|
||||
const generateDocument = async () => { |
|
||||
if (!generateFormRef.value) return |
|
||||
|
|
||||
const valid = await generateFormRef.value.validate() |
|
||||
if (!valid) return |
|
||||
|
|
||||
generateLoading.value = true |
|
||||
try { |
|
||||
const { data } = await generateDocumentFile({ |
|
||||
template_id: generateForm.template_id, |
|
||||
output_filename: generateForm.output_filename, |
|
||||
fill_data: generateForm.fill_data |
|
||||
}) |
|
||||
|
|
||||
ElMessage.success('文档生成成功!') |
|
||||
|
|
||||
// 下载文件 |
|
||||
if (data.download_url) { |
|
||||
window.open(data.download_url, '_blank') |
|
||||
} |
|
||||
|
|
||||
handleClose() |
|
||||
emit('complete') |
|
||||
} catch (error) { |
|
||||
ElMessage.error(error.message || '生成失败') |
|
||||
} finally { |
|
||||
generateLoading.value = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 关闭对话框 |
|
||||
const handleClose = () => { |
|
||||
visible.value = false |
|
||||
generateForm.template_id = '' |
|
||||
generateForm.output_filename = '' |
|
||||
generateForm.fill_data = {} |
|
||||
placeholderConfigs.value = {} |
|
||||
} |
|
||||
|
|
||||
// 暴露方法给父组件 |
|
||||
defineExpose({ |
|
||||
open |
|
||||
}) |
|
||||
</script> |
|
||||
|
|
||||
<style scoped> |
|
||||
.dialog-footer { |
|
||||
text-align: right; |
|
||||
} |
|
||||
|
|
||||
.space-y-4 > * + * { |
|
||||
margin-top: 1rem; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,280 +0,0 @@ |
|||||
<template> |
|
||||
<el-dialog v-model="visible" :title="`配置占位符 - ${templateInfo.contract_name}`" width="900px" :before-close="handleClose"> |
|
||||
<div v-if="loading" class="text-center py-8"> |
|
||||
<el-icon class="is-loading"><Loading /></el-icon> |
|
||||
<span class="ml-2">加载中...</span> |
|
||||
</div> |
|
||||
|
|
||||
<div v-else-if="placeholders.length === 0" class="text-center py-8 text-gray-500"> |
|
||||
<el-icon size="48"><DocumentRemove /></el-icon> |
|
||||
<div class="mt-4">该模板中未发现占位符</div> |
|
||||
<div class="text-sm mt-2">请确保模板中包含 {{变量名}} 格式的占位符</div> |
|
||||
</div> |
|
||||
|
|
||||
<div v-else> |
|
||||
<div class="mb-4 p-4 bg-blue-50 rounded"> |
|
||||
<h4 class="text-blue-800 mb-2"> |
|
||||
<el-icon><InfoFilled /></el-icon> |
|
||||
配置说明 |
|
||||
</h4> |
|
||||
<ul class="text-sm text-blue-700 space-y-1"> |
|
||||
<li>• 为每个占位符配置数据源和显示名称</li> |
|
||||
<li>• 数据库数据源:从系统数据表中自动获取数据</li> |
|
||||
<li>• 手动填写数据源:生成文档时需要手动输入</li> |
|
||||
<li>• 可设置默认值和处理函数</li> |
|
||||
</ul> |
|
||||
</div> |
|
||||
|
|
||||
<el-form :model="configForm" label-width="120px" ref="configFormRef"> |
|
||||
<div class="space-y-6"> |
|
||||
<div v-for="(placeholder, index) in placeholders" :key="placeholder" |
|
||||
class="border rounded-lg p-4 bg-gray-50"> |
|
||||
<div class="flex items-center justify-between mb-4"> |
|
||||
<h4 class="text-lg font-semibold text-gray-800"> |
|
||||
<el-tag type="primary" size="large">{{ placeholder }}</el-tag> |
|
||||
</h4> |
|
||||
<el-tag v-if="configForm.configs[placeholder]?.is_required" type="danger" size="small">必填</el-tag> |
|
||||
</div> |
|
||||
|
|
||||
<el-row :gutter="16"> |
|
||||
<el-col :span="12"> |
|
||||
<el-form-item label="显示名称" :prop="`configs.${placeholder}.name`" |
|
||||
:rules="[{required: true, message: '请输入显示名称'}]"> |
|
||||
<el-input v-model="configForm.configs[placeholder].name" |
|
||||
placeholder="请输入显示名称" /> |
|
||||
</el-form-item> |
|
||||
</el-col> |
|
||||
<el-col :span="12"> |
|
||||
<el-form-item label="数据源类型" :prop="`configs.${placeholder}.data_source`" |
|
||||
:rules="[{required: true, message: '请选择数据源类型'}]"> |
|
||||
<el-select v-model="configForm.configs[placeholder].data_source" |
|
||||
@change="onDataSourceChange(placeholder)" |
|
||||
style="width: 100%"> |
|
||||
<el-option label="数据库" value="database"></el-option> |
|
||||
<el-option label="手动填写" value="manual"></el-option> |
|
||||
</el-select> |
|
||||
</el-form-item> |
|
||||
</el-col> |
|
||||
</el-row> |
|
||||
|
|
||||
<!-- 数据库数据源配置 --> |
|
||||
<div v-if="configForm.configs[placeholder]?.data_source === 'database'"> |
|
||||
<el-row :gutter="16"> |
|
||||
<el-col :span="12"> |
|
||||
<el-form-item label="数据表" :prop="`configs.${placeholder}.table_name`" |
|
||||
:rules="[{required: true, message: '请选择数据表'}]"> |
|
||||
<el-select v-model="configForm.configs[placeholder].table_name" |
|
||||
@change="onTableChange(placeholder)" |
|
||||
placeholder="请选择数据表" style="width: 100%"> |
|
||||
<el-option v-for="(tableInfo, tableName) in dataSources" |
|
||||
:key="tableName" |
|
||||
:label="tableInfo.table_alias || tableName" |
|
||||
:value="tableName"> |
|
||||
</el-option> |
|
||||
</el-select> |
|
||||
</el-form-item> |
|
||||
</el-col> |
|
||||
<el-col :span="12"> |
|
||||
<el-form-item label="字段" :prop="`configs.${placeholder}.field_name`" |
|
||||
:rules="[{required: true, message: '请选择字段'}]"> |
|
||||
<el-select v-model="configForm.configs[placeholder].field_name" |
|
||||
placeholder="请选择字段" style="width: 100%" |
|
||||
:disabled="!configForm.configs[placeholder]?.table_name"> |
|
||||
<el-option v-for="field in getTableFields(configForm.configs[placeholder]?.table_name)" |
|
||||
:key="field.field_name" |
|
||||
:label="field.field_alias || field.field_name" |
|
||||
:value="field.field_name"> |
|
||||
<span>{{ field.field_alias || field.field_name }}</span> |
|
||||
<span class="text-gray-400 text-xs ml-2">({{ field.field_type }})</span> |
|
||||
</el-option> |
|
||||
</el-select> |
|
||||
</el-form-item> |
|
||||
</el-col> |
|
||||
</el-row> |
|
||||
</div> |
|
||||
|
|
||||
<el-row :gutter="16"> |
|
||||
<el-col :span="8"> |
|
||||
<el-form-item label="处理函数"> |
|
||||
<el-select v-model="configForm.configs[placeholder].process_function" |
|
||||
clearable placeholder="请选择处理函数" style="width: 100%"> |
|
||||
<el-option label="无" value=""></el-option> |
|
||||
<el-option label="格式化日期" value="formatDate"></el-option> |
|
||||
<el-option label="格式化日期时间" value="formatDateTime"></el-option> |
|
||||
<el-option label="格式化数字" value="formatNumber"></el-option> |
|
||||
<el-option label="转大写" value="toUpper"></el-option> |
|
||||
<el-option label="转小写" value="toLower"></el-option> |
|
||||
</el-select> |
|
||||
</el-form-item> |
|
||||
</el-col> |
|
||||
<el-col :span="8"> |
|
||||
<el-form-item label="默认值"> |
|
||||
<el-input v-model="configForm.configs[placeholder].default_value" |
|
||||
placeholder="请输入默认值" /> |
|
||||
</el-form-item> |
|
||||
</el-col> |
|
||||
<el-col :span="8"> |
|
||||
<el-form-item label="是否必填"> |
|
||||
<el-switch v-model="configForm.configs[placeholder].is_required" /> |
|
||||
</el-form-item> |
|
||||
</el-col> |
|
||||
</el-row> |
|
||||
</div> |
|
||||
</div> |
|
||||
</el-form> |
|
||||
</div> |
|
||||
|
|
||||
<template #footer v-if="placeholders.length > 0"> |
|
||||
<div class="dialog-footer"> |
|
||||
<el-button @click="handleClose">取消</el-button> |
|
||||
<el-button type="primary" @click="saveConfig" :loading="saveLoading"> |
|
||||
{{ saveLoading ? '保存中...' : '保存配置' }} |
|
||||
</el-button> |
|
||||
</div> |
|
||||
</template> |
|
||||
</el-dialog> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
import { reactive, ref, nextTick } from 'vue' |
|
||||
import { ElMessage, type FormInstance } from 'element-plus' |
|
||||
import { getDocumentTemplateInfo, saveDocumentPlaceholderConfig, getDocumentDataSources } from '@/app/api/document' |
|
||||
|
|
||||
const emit = defineEmits(['complete']) |
|
||||
|
|
||||
// 组件状态 |
|
||||
const visible = ref(false) |
|
||||
const loading = ref(false) |
|
||||
const saveLoading = ref(false) |
|
||||
|
|
||||
// 数据 |
|
||||
const templateInfo = ref({}) |
|
||||
const placeholders = ref([]) |
|
||||
const dataSources = ref({}) |
|
||||
|
|
||||
// 配置表单 |
|
||||
const configForm = reactive({ |
|
||||
template_id: 0, |
|
||||
configs: {} |
|
||||
}) |
|
||||
|
|
||||
// 组件引用 |
|
||||
const configFormRef = ref<FormInstance>() |
|
||||
|
|
||||
// 打开配置对话框 |
|
||||
const open = async (template: any) => { |
|
||||
templateInfo.value = template |
|
||||
configForm.template_id = template.id |
|
||||
visible.value = true |
|
||||
loading.value = true |
|
||||
|
|
||||
try { |
|
||||
// 同时获取模板详情和数据源 |
|
||||
const [templateResult, dataSourceResult] = await Promise.all([ |
|
||||
getDocumentTemplateInfo(template.id), |
|
||||
getDocumentDataSources() |
|
||||
]) |
|
||||
|
|
||||
placeholders.value = templateResult.data.placeholders || [] |
|
||||
dataSources.value = dataSourceResult.data |
|
||||
|
|
||||
// 初始化配置表单 |
|
||||
initConfigForm(templateResult.data.placeholder_config) |
|
||||
} catch (error) { |
|
||||
ElMessage.error('加载数据失败') |
|
||||
console.error(error) |
|
||||
} finally { |
|
||||
loading.value = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 初始化配置表单 |
|
||||
const initConfigForm = (existingConfig: any) => { |
|
||||
configForm.configs = {} |
|
||||
|
|
||||
placeholders.value.forEach(placeholder => { |
|
||||
// 如果有现有配置,使用现有配置,否则使用默认值 |
|
||||
const existing = existingConfig && existingConfig[placeholder] |
|
||||
|
|
||||
configForm.configs[placeholder] = { |
|
||||
name: existing?.name || placeholder, |
|
||||
data_source: existing?.data_source || 'manual', |
|
||||
table_name: existing?.table_name || '', |
|
||||
field_name: existing?.field_name || '', |
|
||||
process_function: existing?.process_function || '', |
|
||||
default_value: existing?.default_value || '', |
|
||||
is_required: existing?.is_required ?? true |
|
||||
} |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
// 数据源类型变化 |
|
||||
const onDataSourceChange = (placeholder: string) => { |
|
||||
if (configForm.configs[placeholder].data_source === 'manual') { |
|
||||
configForm.configs[placeholder].table_name = '' |
|
||||
configForm.configs[placeholder].field_name = '' |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 数据表变化 |
|
||||
const onTableChange = (placeholder: string) => { |
|
||||
configForm.configs[placeholder].field_name = '' |
|
||||
} |
|
||||
|
|
||||
// 获取表字段 |
|
||||
const getTableFields = (tableName: string) => { |
|
||||
return dataSources.value[tableName]?.fields || [] |
|
||||
} |
|
||||
|
|
||||
// 保存配置 |
|
||||
const saveConfig = async () => { |
|
||||
if (!configFormRef.value) return |
|
||||
|
|
||||
const valid = await configFormRef.value.validate() |
|
||||
if (!valid) return |
|
||||
|
|
||||
saveLoading.value = true |
|
||||
try { |
|
||||
await saveDocumentPlaceholderConfig({ |
|
||||
template_id: configForm.template_id, |
|
||||
placeholder_config: configForm.configs |
|
||||
}) |
|
||||
|
|
||||
ElMessage.success('配置保存成功') |
|
||||
handleClose() |
|
||||
emit('complete') |
|
||||
} catch (error) { |
|
||||
ElMessage.error(error.message || '保存失败') |
|
||||
} finally { |
|
||||
saveLoading.value = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 关闭对话框 |
|
||||
const handleClose = () => { |
|
||||
visible.value = false |
|
||||
templateInfo.value = {} |
|
||||
placeholders.value = [] |
|
||||
configForm.configs = {} |
|
||||
configForm.template_id = 0 |
|
||||
} |
|
||||
|
|
||||
// 暴露方法给父组件 |
|
||||
defineExpose({ |
|
||||
open |
|
||||
}) |
|
||||
</script> |
|
||||
|
|
||||
<style scoped> |
|
||||
.dialog-footer { |
|
||||
text-align: right; |
|
||||
} |
|
||||
|
|
||||
.space-y-6 > * + * { |
|
||||
margin-top: 1.5rem; |
|
||||
} |
|
||||
|
|
||||
.space-y-1 > * + * { |
|
||||
margin-top: 0.25rem; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,477 +0,0 @@ |
|||||
<template> |
|
||||
<div class="main-container"> |
|
||||
<el-card class="box-card !border-none" shadow="never"> |
|
||||
<div class="flex justify-between items-center"> |
|
||||
<span class="text-lg">Word模板管理</span> |
|
||||
<div class="space-x-2"> |
|
||||
<el-button type="primary" @click="uploadTemplate"> |
|
||||
<el-icon><Upload /></el-icon> |
|
||||
上传模板 |
|
||||
</el-button> |
|
||||
<el-button type="success" @click="generateDocument"> |
|
||||
<el-icon><Document /></el-icon> |
|
||||
生成文档 |
|
||||
</el-button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never"> |
|
||||
<el-form :inline="true" :model="templateTable.searchParam" ref="searchFormRef"> |
|
||||
<el-form-item label="模板名称" prop="contract_name"> |
|
||||
<el-input class="w-[280px]" v-model="templateTable.searchParam.contract_name" clearable placeholder="请输入模板名称" /> |
|
||||
</el-form-item> |
|
||||
|
|
||||
<el-form-item label="状态" prop="contract_status"> |
|
||||
<el-select class="w-[280px]" v-model="templateTable.searchParam.contract_status" clearable placeholder="请选择状态"> |
|
||||
<el-option label="全部" value=""></el-option> |
|
||||
<el-option label="草稿" value="draft"></el-option> |
|
||||
<el-option label="启用" value="active"></el-option> |
|
||||
<el-option label="禁用" value="disabled"></el-option> |
|
||||
</el-select> |
|
||||
</el-form-item> |
|
||||
|
|
||||
<el-form-item label="类型" prop="contract_type"> |
|
||||
<el-select class="w-[280px]" v-model="templateTable.searchParam.contract_type" clearable placeholder="请选择类型"> |
|
||||
<el-option label="全部" value=""></el-option> |
|
||||
<el-option label="通用" value="general"></el-option> |
|
||||
<el-option label="课程" value="course"></el-option> |
|
||||
<el-option label="会员" value="membership"></el-option> |
|
||||
</el-select> |
|
||||
</el-form-item> |
|
||||
|
|
||||
<el-form-item label="创建时间" prop="created_at"> |
|
||||
<el-date-picker v-model="templateTable.searchParam.created_at" type="datetimerange" |
|
||||
format="YYYY-MM-DD HH:mm:ss" start-placeholder="开始时间" end-placeholder="结束时间" /> |
|
||||
</el-form-item> |
|
||||
|
|
||||
<el-form-item> |
|
||||
<el-button type="primary" @click="loadTemplateList()">搜索</el-button> |
|
||||
<el-button @click="resetForm(searchFormRef)">重置</el-button> |
|
||||
</el-form-item> |
|
||||
</el-form> |
|
||||
</el-card> |
|
||||
|
|
||||
<div class="mt-[10px]"> |
|
||||
<el-table :data="templateTable.data" size="large" v-loading="templateTable.loading"> |
|
||||
<template #empty> |
|
||||
<span>{{ !templateTable.loading ? '暂无数据' : '' }}</span> |
|
||||
</template> |
|
||||
|
|
||||
<el-table-column prop="contract_name" label="模板名称" min-width="200" :show-overflow-tooltip="true"/> |
|
||||
|
|
||||
<el-table-column label="原始文件名" min-width="180" :show-overflow-tooltip="true"> |
|
||||
<template #default="{ row }"> |
|
||||
<span>{{ row.original_filename || '-' }}</span> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
|
|
||||
<el-table-column label="文件大小" min-width="100" align="center"> |
|
||||
<template #default="{ row }"> |
|
||||
<span>{{ row.file_size_formatted || '-' }}</span> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
|
|
||||
<el-table-column label="占位符数量" min-width="120" align="center"> |
|
||||
<template #default="{ row }"> |
|
||||
<el-tag type="info" size="small">{{ row.placeholders ? row.placeholders.length : 0 }}</el-tag> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
|
|
||||
<el-table-column label="状态" min-width="100" align="center"> |
|
||||
<template #default="{ row }"> |
|
||||
<el-tag :type="getStatusType(row.contract_status)" size="small"> |
|
||||
{{ getStatusText(row.contract_status) }} |
|
||||
</el-tag> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
|
|
||||
<el-table-column label="类型" min-width="100" align="center"> |
|
||||
<template #default="{ row }"> |
|
||||
<el-tag type="primary" size="small" plain> |
|
||||
{{ getTypeText(row.contract_type) }} |
|
||||
</el-tag> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
|
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="160" :show-overflow-tooltip="true"/> |
|
||||
|
|
||||
<el-table-column label="操作" fixed="right" min-width="280"> |
|
||||
<template #default="{ row }"> |
|
||||
<el-button type="primary" link size="small" @click="previewTemplate(row)"> |
|
||||
<el-icon><View /></el-icon> |
|
||||
预览 |
|
||||
</el-button> |
|
||||
<el-button type="warning" link size="small" @click="configPlaceholder(row)" v-if="row.contract_status === 'draft'"> |
|
||||
<el-icon><Setting /></el-icon> |
|
||||
配置 |
|
||||
</el-button> |
|
||||
<el-button type="success" link size="small" @click="generateFromTemplate(row)" v-if="row.contract_status === 'active'"> |
|
||||
<el-icon><Document /></el-icon> |
|
||||
生成 |
|
||||
</el-button> |
|
||||
<el-button type="info" link size="small" @click="copyTemplate(row)"> |
|
||||
<el-icon><CopyDocument /></el-icon> |
|
||||
复制 |
|
||||
</el-button> |
|
||||
<el-button type="danger" link size="small" @click="deleteTemplate(row)"> |
|
||||
<el-icon><Delete /></el-icon> |
|
||||
删除 |
|
||||
</el-button> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
</el-table> |
|
||||
|
|
||||
<div class="mt-[16px] flex justify-end"> |
|
||||
<el-pagination v-model:current-page="templateTable.page" v-model:page-size="templateTable.limit" |
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="templateTable.total" |
|
||||
@size-change="loadTemplateList()" @current-change="loadTemplateList" /> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- 上传模板对话框 --> |
|
||||
<el-dialog v-model="uploadDialog.visible" title="上传Word模板" width="500px" :before-close="closeUploadDialog"> |
|
||||
<el-form :model="uploadDialog.form" label-width="100px" ref="uploadFormRef"> |
|
||||
<el-form-item label="模板名称" prop="template_name" :rules="[{required: true, message: '请输入模板名称'}]"> |
|
||||
<el-input v-model="uploadDialog.form.template_name" placeholder="请输入模板名称" /> |
|
||||
</el-form-item> |
|
||||
<el-form-item label="模板类型" prop="template_type" :rules="[{required: true, message: '请选择模板类型'}]"> |
|
||||
<el-select v-model="uploadDialog.form.template_type" placeholder="请选择模板类型" style="width: 100%"> |
|
||||
<el-option label="通用" value="general"></el-option> |
|
||||
<el-option label="课程" value="course"></el-option> |
|
||||
<el-option label="会员" value="membership"></el-option> |
|
||||
</el-select> |
|
||||
</el-form-item> |
|
||||
<el-form-item label="选择文件" prop="file" :rules="[{required: true, message: '请选择Word文件'}]"> |
|
||||
<el-upload |
|
||||
class="upload-demo" |
|
||||
:before-upload="beforeUpload" |
|
||||
:on-change="handleFileChange" |
|
||||
:auto-upload="false" |
|
||||
accept=".doc,.docx" |
|
||||
:show-file-list="true" |
|
||||
:limit="1"> |
|
||||
<el-button type="primary"> |
|
||||
<el-icon><Upload /></el-icon> |
|
||||
选择文件 |
|
||||
</el-button> |
|
||||
<template #tip> |
|
||||
<div class="el-upload__tip"> |
|
||||
只能上传 .doc/.docx 文件,且不超过 10MB |
|
||||
</div> |
|
||||
</template> |
|
||||
</el-upload> |
|
||||
</el-form-item> |
|
||||
<el-form-item label="备注"> |
|
||||
<el-input v-model="uploadDialog.form.remarks" type="textarea" :rows="3" placeholder="请输入备注信息" /> |
|
||||
</el-form-item> |
|
||||
</el-form> |
|
||||
<template #footer> |
|
||||
<div class="dialog-footer"> |
|
||||
<el-button @click="closeUploadDialog">取消</el-button> |
|
||||
<el-button type="primary" @click="submitUpload" :loading="uploadDialog.loading"> |
|
||||
{{ uploadDialog.loading ? '上传中...' : '上传' }} |
|
||||
</el-button> |
|
||||
</div> |
|
||||
</template> |
|
||||
</el-dialog> |
|
||||
|
|
||||
<!-- 模板预览对话框 --> |
|
||||
<el-dialog v-model="previewDialog.visible" title="模板预览" width="800px"> |
|
||||
<div v-if="previewDialog.loading" class="text-center py-8"> |
|
||||
<el-icon class="is-loading"><Loading /></el-icon> |
|
||||
<span class="ml-2">加载中...</span> |
|
||||
</div> |
|
||||
<div v-else> |
|
||||
<div class="mb-4"> |
|
||||
<h4>占位符列表:</h4> |
|
||||
<div class="flex flex-wrap gap-2 mt-2"> |
|
||||
<el-tag v-for="placeholder in previewDialog.placeholders" :key="placeholder" size="small" type="info"> |
|
||||
{{ placeholder }} |
|
||||
</el-tag> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="border rounded p-4 max-h-96 overflow-y-auto"> |
|
||||
<pre class="whitespace-pre-wrap text-sm">{{ previewDialog.content }}</pre> |
|
||||
</div> |
|
||||
</div> |
|
||||
</el-dialog> |
|
||||
|
|
||||
<!-- 占位符配置组件 --> |
|
||||
<PlaceholderConfig ref="placeholderConfigRef" @complete="loadTemplateList" /> |
|
||||
|
|
||||
<!-- 文档生成组件 --> |
|
||||
<DocumentGenerate ref="documentGenerateRef" @complete="loadTemplateList" /> |
|
||||
</el-card> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
import { reactive, ref, onMounted } from 'vue' |
|
||||
import { ElMessage, ElMessageBox, type FormInstance } from 'element-plus' |
|
||||
import { getDocumentTemplateList, deleteDocumentTemplate, uploadDocumentTemplate, previewDocumentTemplate, copyDocumentTemplate } from '@/app/api/document' |
|
||||
import PlaceholderConfig from './components/PlaceholderConfig.vue' |
|
||||
import DocumentGenerate from './components/DocumentGenerate.vue' |
|
||||
|
|
||||
// 表格数据 |
|
||||
const templateTable = reactive({ |
|
||||
page: 1, |
|
||||
limit: 10, |
|
||||
total: 0, |
|
||||
loading: false, |
|
||||
data: [], |
|
||||
searchParam: { |
|
||||
contract_name: '', |
|
||||
contract_status: '', |
|
||||
contract_type: '', |
|
||||
created_at: [] |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
// 上传对话框 |
|
||||
const uploadDialog = reactive({ |
|
||||
visible: false, |
|
||||
loading: false, |
|
||||
form: { |
|
||||
template_name: '', |
|
||||
template_type: 'general', |
|
||||
remarks: '', |
|
||||
file: null |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
// 预览对话框 |
|
||||
const previewDialog = reactive({ |
|
||||
visible: false, |
|
||||
loading: false, |
|
||||
content: '', |
|
||||
placeholders: [] |
|
||||
}) |
|
||||
|
|
||||
// 组件引用 |
|
||||
const searchFormRef = ref<FormInstance>() |
|
||||
const uploadFormRef = ref<FormInstance>() |
|
||||
const placeholderConfigRef = ref() |
|
||||
const documentGenerateRef = ref() |
|
||||
|
|
||||
// 获取模板列表 |
|
||||
const loadTemplateList = async () => { |
|
||||
templateTable.loading = true |
|
||||
try { |
|
||||
const { data } = await getDocumentTemplateList({ |
|
||||
page: templateTable.page, |
|
||||
limit: templateTable.limit, |
|
||||
...templateTable.searchParam |
|
||||
}) |
|
||||
templateTable.data = data.data |
|
||||
templateTable.total = data.total |
|
||||
} catch (error) { |
|
||||
ElMessage.error('获取模板列表失败') |
|
||||
} finally { |
|
||||
templateTable.loading = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 重置搜索表单 |
|
||||
const resetForm = (formEl: FormInstance | undefined) => { |
|
||||
if (!formEl) return |
|
||||
formEl.resetFields() |
|
||||
loadTemplateList() |
|
||||
} |
|
||||
|
|
||||
// 状态类型映射 |
|
||||
const getStatusType = (status: string) => { |
|
||||
const statusMap = { |
|
||||
'draft': 'info', |
|
||||
'active': 'success', |
|
||||
'disabled': 'danger' |
|
||||
} |
|
||||
return statusMap[status] || 'info' |
|
||||
} |
|
||||
|
|
||||
// 状态文本映射 |
|
||||
const getStatusText = (status: string) => { |
|
||||
const statusMap = { |
|
||||
'draft': '草稿', |
|
||||
'active': '启用', |
|
||||
'disabled': '禁用' |
|
||||
} |
|
||||
return statusMap[status] || '未知' |
|
||||
} |
|
||||
|
|
||||
// 类型文本映射 |
|
||||
const getTypeText = (type: string) => { |
|
||||
const typeMap = { |
|
||||
'general': '通用', |
|
||||
'course': '课程', |
|
||||
'membership': '会员' |
|
||||
} |
|
||||
return typeMap[type] || '未知' |
|
||||
} |
|
||||
|
|
||||
// 上传模板 |
|
||||
const uploadTemplate = () => { |
|
||||
uploadDialog.visible = true |
|
||||
uploadDialog.form = { |
|
||||
template_name: '', |
|
||||
template_type: 'general', |
|
||||
remarks: '', |
|
||||
file: null |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 关闭上传对话框 |
|
||||
const closeUploadDialog = () => { |
|
||||
uploadDialog.visible = false |
|
||||
uploadDialog.form = { |
|
||||
template_name: '', |
|
||||
template_type: 'general', |
|
||||
remarks: '', |
|
||||
file: null |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 文件上传前验证 |
|
||||
const beforeUpload = (file: File) => { |
|
||||
const isValidType = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || |
|
||||
file.type === 'application/msword' |
|
||||
const isLt10M = file.size / 1024 / 1024 < 10 |
|
||||
|
|
||||
if (!isValidType) { |
|
||||
ElMessage.error('只能上传 Word 文档!') |
|
||||
return false |
|
||||
} |
|
||||
if (!isLt10M) { |
|
||||
ElMessage.error('文件大小不能超过 10MB!') |
|
||||
return false |
|
||||
} |
|
||||
return false // 阻止自动上传 |
|
||||
} |
|
||||
|
|
||||
// 文件选择变化 |
|
||||
const handleFileChange = (file: any) => { |
|
||||
uploadDialog.form.file = file.raw |
|
||||
// 自动设置模板名称 |
|
||||
if (!uploadDialog.form.template_name) { |
|
||||
const fileName = file.name.replace(/\.[^/.]+$/, '') |
|
||||
uploadDialog.form.template_name = fileName |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 提交上传 |
|
||||
const submitUpload = async () => { |
|
||||
if (!uploadFormRef.value) return |
|
||||
|
|
||||
const valid = await uploadFormRef.value.validate() |
|
||||
if (!valid) return |
|
||||
|
|
||||
if (!uploadDialog.form.file) { |
|
||||
ElMessage.error('请选择要上传的文件') |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
uploadDialog.loading = true |
|
||||
try { |
|
||||
const formData = new FormData() |
|
||||
formData.append('file', uploadDialog.form.file) |
|
||||
formData.append('template_name', uploadDialog.form.template_name) |
|
||||
formData.append('template_type', uploadDialog.form.template_type) |
|
||||
formData.append('remarks', uploadDialog.form.remarks) |
|
||||
|
|
||||
await uploadDocumentTemplate(formData) |
|
||||
ElMessage.success('模板上传成功') |
|
||||
closeUploadDialog() |
|
||||
loadTemplateList() |
|
||||
} catch (error) { |
|
||||
ElMessage.error(error.message || '上传失败') |
|
||||
} finally { |
|
||||
uploadDialog.loading = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 预览模板 |
|
||||
const previewTemplate = async (row: any) => { |
|
||||
previewDialog.visible = true |
|
||||
previewDialog.loading = true |
|
||||
try { |
|
||||
const { data } = await previewDocumentTemplate(row.id) |
|
||||
previewDialog.content = data.content |
|
||||
previewDialog.placeholders = data.placeholders |
|
||||
} catch (error) { |
|
||||
ElMessage.error('预览失败') |
|
||||
} finally { |
|
||||
previewDialog.loading = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 配置占位符 |
|
||||
const configPlaceholder = (row: any) => { |
|
||||
placeholderConfigRef.value?.open(row) |
|
||||
} |
|
||||
|
|
||||
// 生成文档 |
|
||||
const generateDocument = () => { |
|
||||
documentGenerateRef.value?.open() |
|
||||
} |
|
||||
|
|
||||
// 从模板生成文档 |
|
||||
const generateFromTemplate = (row: any) => { |
|
||||
documentGenerateRef.value?.open(row) |
|
||||
} |
|
||||
|
|
||||
// 复制模板 |
|
||||
const copyTemplate = async (row: any) => { |
|
||||
try { |
|
||||
await copyDocumentTemplate(row.id) |
|
||||
ElMessage.success('模板复制成功') |
|
||||
loadTemplateList() |
|
||||
} catch (error) { |
|
||||
ElMessage.error('复制失败') |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 删除模板 |
|
||||
const deleteTemplate = (row: any) => { |
|
||||
ElMessageBox.confirm( |
|
||||
`确定要删除模板 "${row.contract_name}" 吗?此操作不可恢复!`, |
|
||||
'删除确认', |
|
||||
{ |
|
||||
confirmButtonText: '确定', |
|
||||
cancelButtonText: '取消', |
|
||||
type: 'warning' |
|
||||
} |
|
||||
).then(async () => { |
|
||||
try { |
|
||||
await deleteDocumentTemplate(row.id) |
|
||||
ElMessage.success('删除成功') |
|
||||
loadTemplateList() |
|
||||
} catch (error) { |
|
||||
ElMessage.error('删除失败') |
|
||||
} |
|
||||
}).catch(() => { |
|
||||
// 用户取消 |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
// 页面加载时获取数据 |
|
||||
onMounted(() => { |
|
||||
loadTemplateList() |
|
||||
}) |
|
||||
</script> |
|
||||
|
|
||||
<style scoped> |
|
||||
.main-container { |
|
||||
padding: 20px; |
|
||||
} |
|
||||
|
|
||||
.table-search-wrap { |
|
||||
background-color: #f8f9fa; |
|
||||
} |
|
||||
|
|
||||
.upload-demo { |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
|
||||
.dialog-footer { |
|
||||
text-align: right; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,305 +0,0 @@ |
|||||
<template> |
|
||||
<el-dialog v-model="visible" title="占位符配置" width="1000px" @open="loadConfig"> |
|
||||
<div class="config-container"> |
|
||||
<!-- 说明文档 --> |
|
||||
<el-alert |
|
||||
title="配置说明" |
|
||||
type="info" |
|
||||
:closable="false" |
|
||||
show-icon |
|
||||
> |
|
||||
<template #default> |
|
||||
<p>1. 占位符格式:双大括号包围,例如:学员姓名</p> |
|
||||
<p>2. 请为每个占位符配置对应的数据源表和字段</p> |
|
||||
<p>3. 必填项在生成合同时必须有值,否则会报错</p> |
|
||||
</template> |
|
||||
</el-alert> |
|
||||
|
|
||||
<!-- 配置表格 --> |
|
||||
<el-table :data="configList" v-loading="loading" class="config-table"> |
|
||||
<el-table-column prop="placeholder" label="占位符" width="200"> |
|
||||
<template #default="{ row }"> |
|
||||
<code>{{ `${row.placeholder}` }}</code> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
|
|
||||
<el-table-column label="数据源表" width="150"> |
|
||||
<template #default="{ row, $index }"> |
|
||||
<el-select v-model="row.table_name" placeholder="选择表" @change="onTableChange(row, $index)"> |
|
||||
<el-option |
|
||||
v-for="table in tableOptions" |
|
||||
:key="table.value" |
|
||||
:label="table.label" |
|
||||
:value="table.value" |
|
||||
/> |
|
||||
</el-select> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
|
|
||||
<el-table-column label="字段名" width="150"> |
|
||||
<template #default="{ row, $index }"> |
|
||||
<el-select v-model="row.field_name" placeholder="选择字段"> |
|
||||
<el-option |
|
||||
v-for="field in getFieldOptions(row.table_name)" |
|
||||
:key="field.value" |
|
||||
:label="field.label" |
|
||||
:value="field.value" |
|
||||
/> |
|
||||
</el-select> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
|
|
||||
<el-table-column label="字段类型" width="120"> |
|
||||
<template #default="{ row }"> |
|
||||
<el-select v-model="row.field_type" placeholder="选择类型"> |
|
||||
<el-option label="文本" value="text" /> |
|
||||
<el-option label="数字" value="number" /> |
|
||||
<el-option label="日期" value="date" /> |
|
||||
<el-option label="金额" value="money" /> |
|
||||
</el-select> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
|
|
||||
<el-table-column label="是否必填" width="100"> |
|
||||
<template #default="{ row }"> |
|
||||
<el-switch v-model="row.is_required" :active-value="1" :inactive-value="0" /> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
|
|
||||
<el-table-column label="默认值" width="150"> |
|
||||
<template #default="{ row }"> |
|
||||
<el-input v-model="row.default_value" placeholder="默认值" /> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
|
|
||||
<el-table-column label="操作" width="80"> |
|
||||
<template #default="{ $index }"> |
|
||||
<el-button type="danger" size="small" @click="removeConfig($index)"> |
|
||||
删除 |
|
||||
</el-button> |
|
||||
</template> |
|
||||
</el-table-column> |
|
||||
</el-table> |
|
||||
|
|
||||
<!-- 添加配置 --> |
|
||||
<div class="add-config"> |
|
||||
<el-button type="primary" @click="addConfig"> |
|
||||
<el-icon><Plus /></el-icon> |
|
||||
添加占位符 |
|
||||
</el-button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<template #footer> |
|
||||
<el-button @click="visible = false">取消</el-button> |
|
||||
<el-button type="primary" :loading="saving" @click="saveConfig">保存配置</el-button> |
|
||||
</template> |
|
||||
</el-dialog> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue' |
|
||||
import { ElMessage } from 'element-plus' |
|
||||
import { Plus } from '@element-plus/icons-vue' |
|
||||
import { contractTemplateApi, type PlaceholderConfig } from '@/api/contract' |
|
||||
|
|
||||
interface Props { |
|
||||
modelValue: boolean |
|
||||
contractId: number |
|
||||
} |
|
||||
|
|
||||
interface Emits { |
|
||||
(e: 'update:modelValue', value: boolean): void |
|
||||
(e: 'success'): void |
|
||||
} |
|
||||
|
|
||||
const props = defineProps<Props>() |
|
||||
const emit = defineEmits<Emits>() |
|
||||
|
|
||||
const loading = ref(false) |
|
||||
const saving = ref(false) |
|
||||
const configList = ref<PlaceholderConfig[]>([]) |
|
||||
|
|
||||
const visible = computed({ |
|
||||
get: () => props.modelValue, |
|
||||
set: (value) => emit('update:modelValue', value) |
|
||||
}) |
|
||||
|
|
||||
// 监听弹窗状态变化 |
|
||||
watch(() => props.modelValue, (newVal) => { |
|
||||
if (newVal) { |
|
||||
// 弹窗打开时加载配置 |
|
||||
loadConfig() |
|
||||
} |
|
||||
}, { immediate: false }) |
|
||||
|
|
||||
// 重置表单状态 |
|
||||
const resetForm = () => { |
|
||||
configList.value = [] |
|
||||
loading.value = false |
|
||||
saving.value = false |
|
||||
} |
|
||||
|
|
||||
// 表选项 |
|
||||
const tableOptions = [ |
|
||||
{ label: '学员信息', value: 'school_student' }, |
|
||||
{ label: '人员信息', value: 'school_personnel' }, |
|
||||
{ label: '课程信息', value: 'school_course' }, |
|
||||
{ label: '班级信息', value: 'school_class' } |
|
||||
] |
|
||||
|
|
||||
// 字段选项映射 |
|
||||
const fieldOptionsMap: Record<string, Array<{label: string, value: string}>> = { |
|
||||
school_student: [ |
|
||||
{ label: '学员姓名', value: 'name' }, |
|
||||
{ label: '手机号', value: 'phone' }, |
|
||||
{ label: '身份证号', value: 'id_card' }, |
|
||||
{ label: '年龄', value: 'age' } |
|
||||
], |
|
||||
school_personnel: [ |
|
||||
{ label: '员工姓名', value: 'name' }, |
|
||||
{ label: '工号', value: 'employee_number' }, |
|
||||
{ label: '手机号', value: 'phone' }, |
|
||||
{ label: '邮箱', value: 'email' } |
|
||||
], |
|
||||
school_course: [ |
|
||||
{ label: '课程名称', value: 'course_name' }, |
|
||||
{ label: '课程价格', value: 'price' }, |
|
||||
{ label: '课时数', value: 'class_hours' } |
|
||||
], |
|
||||
school_class: [ |
|
||||
{ label: '班级名称', value: 'class_name' }, |
|
||||
{ label: '开课时间', value: 'start_time' }, |
|
||||
{ label: '结课时间', value: 'end_time' } |
|
||||
] |
|
||||
} |
|
||||
|
|
||||
// 获取字段选项 |
|
||||
const getFieldOptions = (tableName: string) => { |
|
||||
return fieldOptionsMap[tableName] || [] |
|
||||
} |
|
||||
|
|
||||
// 加载配置 |
|
||||
const loadConfig = async () => { |
|
||||
if (!props.contractId) return |
|
||||
|
|
||||
loading.value = true |
|
||||
try { |
|
||||
const { data } = await contractTemplateApi.getPlaceholderConfig(props.contractId) |
|
||||
console.log('API返回数据:', data) |
|
||||
|
|
||||
// 处理API返回的数据格式 |
|
||||
if (data && typeof data === 'object') { |
|
||||
// 如果返回的是合同对象,提取placeholder_config字段 |
|
||||
if (data.placeholder_config) { |
|
||||
configList.value = Array.isArray(data.placeholder_config) ? data.placeholder_config : [] |
|
||||
} |
|
||||
// 如果有placeholders字段,转换为配置格式 |
|
||||
else if (data.placeholders && Array.isArray(data.placeholders)) { |
|
||||
configList.value = data.placeholders.map(placeholder => ({ |
|
||||
placeholder: placeholder, |
|
||||
table_name: '', |
|
||||
field_name: '', |
|
||||
field_type: 'text', |
|
||||
is_required: 0, |
|
||||
default_value: '' |
|
||||
})) |
|
||||
} |
|
||||
// 如果直接是数组 |
|
||||
else if (Array.isArray(data)) { |
|
||||
configList.value = data |
|
||||
} |
|
||||
// 其他情况 |
|
||||
else { |
|
||||
configList.value = [] |
|
||||
} |
|
||||
} else { |
|
||||
configList.value = [] |
|
||||
} |
|
||||
|
|
||||
console.log('处理后的配置列表:', configList.value) |
|
||||
} catch (error) { |
|
||||
console.error('加载配置失败:', error) |
|
||||
ElMessage.error('加载配置失败') |
|
||||
configList.value = [] |
|
||||
} finally { |
|
||||
loading.value = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 表变化时清空字段 |
|
||||
const onTableChange = (row: PlaceholderConfig, index: number) => { |
|
||||
row.field_name = '' |
|
||||
} |
|
||||
|
|
||||
// 添加配置 |
|
||||
const addConfig = () => { |
|
||||
configList.value.push({ |
|
||||
id: 0, |
|
||||
contract_id: props.contractId, |
|
||||
placeholder: '', |
|
||||
table_name: '', |
|
||||
field_name: '', |
|
||||
field_type: 'text', |
|
||||
is_required: 0, |
|
||||
default_value: '' |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
// 删除配置 |
|
||||
const removeConfig = (index: number) => { |
|
||||
configList.value.splice(index, 1) |
|
||||
} |
|
||||
|
|
||||
// 保存配置 |
|
||||
const saveConfig = async () => { |
|
||||
// 验证配置 |
|
||||
for (const config of configList.value) { |
|
||||
if (!config.placeholder) { |
|
||||
ElMessage.error('请填写占位符名称') |
|
||||
return |
|
||||
} |
|
||||
if (!config.table_name) { |
|
||||
ElMessage.error('请选择数据源表') |
|
||||
return |
|
||||
} |
|
||||
if (!config.field_name) { |
|
||||
ElMessage.error('请选择字段名') |
|
||||
return |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
saving.value = true |
|
||||
try { |
|
||||
await contractTemplateApi.savePlaceholderConfig(props.contractId, configList.value) |
|
||||
emit('success') |
|
||||
} catch (error) { |
|
||||
ElMessage.error('保存失败') |
|
||||
} finally { |
|
||||
saving.value = false |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<style scoped> |
|
||||
.config-container { |
|
||||
max-height: 600px; |
|
||||
overflow-y: auto; |
|
||||
} |
|
||||
|
|
||||
.config-table { |
|
||||
margin: 20px 0; |
|
||||
} |
|
||||
|
|
||||
.add-config { |
|
||||
text-align: center; |
|
||||
margin-top: 20px; |
|
||||
} |
|
||||
|
|
||||
code { |
|
||||
background-color: #f5f7fa; |
|
||||
padding: 2px 4px; |
|
||||
border-radius: 3px; |
|
||||
font-family: 'Courier New', monospace; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,72 +0,0 @@ |
|||||
<template> |
|
||||
<el-dialog |
|
||||
v-model="visible" |
|
||||
title="占位符配置" |
|
||||
width="800px" |
|
||||
:close-on-click-modal="false" |
|
||||
> |
|
||||
<div> |
|
||||
<p>合同ID: {{ contractId }}</p> |
|
||||
<p>弹窗状态: {{ visible ? '打开' : '关闭' }}</p> |
|
||||
<div v-if="loading">加载中...</div> |
|
||||
<div v-else> |
|
||||
<p>配置数据: {{ JSON.stringify(configData) }}</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<template #footer> |
|
||||
<el-button @click="visible = false">取消</el-button> |
|
||||
<el-button type="primary" @click="save">保存</el-button> |
|
||||
</template> |
|
||||
</el-dialog> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
import { ref, computed, watch } from 'vue' |
|
||||
import { ElMessage } from 'element-plus' |
|
||||
|
|
||||
interface Props { |
|
||||
modelValue: boolean |
|
||||
contractId: number |
|
||||
} |
|
||||
|
|
||||
const props = defineProps<Props>() |
|
||||
const emit = defineEmits<{ |
|
||||
'update:modelValue': [value: boolean] |
|
||||
'success': [] |
|
||||
}>() |
|
||||
|
|
||||
const loading = ref(false) |
|
||||
const configData = ref<any>({}) |
|
||||
|
|
||||
const visible = computed({ |
|
||||
get: () => props.modelValue, |
|
||||
set: (value) => emit('update:modelValue', value) |
|
||||
}) |
|
||||
|
|
||||
// 监听弹窗打开 |
|
||||
watch(() => props.modelValue, (newVal) => { |
|
||||
if (newVal) { |
|
||||
loadData() |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
const loadData = async () => { |
|
||||
loading.value = true |
|
||||
try { |
|
||||
// 模拟API调用 |
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)) |
|
||||
configData.value = { test: '测试数据', contractId: props.contractId } |
|
||||
} catch (error) { |
|
||||
ElMessage.error('加载失败') |
|
||||
} finally { |
|
||||
loading.value = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const save = () => { |
|
||||
ElMessage.success('保存成功') |
|
||||
emit('success') |
|
||||
visible.value = false |
|
||||
} |
|
||||
</script> |
|
||||
@ -1,175 +0,0 @@ |
|||||
<template> |
|
||||
<el-dialog v-model="visible" title="上传合同模板" width="600px" @close="resetForm"> |
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> |
|
||||
<el-form-item label="模板名称" prop="contract_name"> |
|
||||
<el-input v-model="form.contract_name" placeholder="请输入模板名称" /> |
|
||||
</el-form-item> |
|
||||
|
|
||||
<el-form-item label="合同类型" prop="contract_type"> |
|
||||
<el-select v-model="form.contract_type" placeholder="请选择合同类型"> |
|
||||
<el-option label="课程合同" value="course" /> |
|
||||
<el-option label="服务合同" value="service" /> |
|
||||
</el-select> |
|
||||
</el-form-item> |
|
||||
|
|
||||
<el-form-item label="模板文件" prop="file"> |
|
||||
<FileUpload |
|
||||
:upload-url="uploadUrl" |
|
||||
@success="handleFileSuccess" |
|
||||
@error="handleFileError" |
|
||||
/> |
|
||||
<div v-if="form.file_path" class="file-info"> |
|
||||
<el-icon><Document /></el-icon> |
|
||||
<span>{{ form.file_name }}</span> |
|
||||
</div> |
|
||||
</el-form-item> |
|
||||
|
|
||||
<el-form-item label="备注"> |
|
||||
<el-input v-model="form.remarks" type="textarea" :rows="3" placeholder="请输入备注信息" /> |
|
||||
</el-form-item> |
|
||||
</el-form> |
|
||||
|
|
||||
<template #footer> |
|
||||
<el-button @click="visible = false">取消</el-button> |
|
||||
<el-button type="primary" :loading="loading" @click="submit">确定</el-button> |
|
||||
</template> |
|
||||
</el-dialog> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
import { ref, reactive, computed, watch } from 'vue' |
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus' |
|
||||
import { Document } from '@element-plus/icons-vue' |
|
||||
import { contractTemplateApi } from '@/api/contract' |
|
||||
import FileUpload from '@/components/FileUpload/index.vue' |
|
||||
|
|
||||
interface Props { |
|
||||
modelValue: boolean |
|
||||
} |
|
||||
|
|
||||
interface Emits { |
|
||||
(e: 'update:modelValue', value: boolean): void |
|
||||
(e: 'success'): void |
|
||||
} |
|
||||
|
|
||||
const props = defineProps<Props>() |
|
||||
const emit = defineEmits<Emits>() |
|
||||
|
|
||||
const formRef = ref<FormInstance>() |
|
||||
const loading = ref(false) |
|
||||
|
|
||||
const visible = computed({ |
|
||||
get: () => props.modelValue, |
|
||||
set: (value) => emit('update:modelValue', value) |
|
||||
}) |
|
||||
|
|
||||
const form = reactive({ |
|
||||
contract_name: '', |
|
||||
contract_type: '', |
|
||||
file_path: '', |
|
||||
file_name: '', |
|
||||
remarks: '' |
|
||||
}) |
|
||||
|
|
||||
const rules: FormRules = { |
|
||||
contract_name: [ |
|
||||
{ required: true, message: '请输入模板名称', trigger: 'blur' } |
|
||||
], |
|
||||
contract_type: [ |
|
||||
{ required: true, message: '请选择合同类型', trigger: 'change' } |
|
||||
], |
|
||||
file: [ |
|
||||
{ required: true, message: '请上传模板文件', trigger: 'change' } |
|
||||
] |
|
||||
} |
|
||||
|
|
||||
const uploadUrl = `${import.meta.env.VITE_APP_BASE_URL}document_template/upload` |
|
||||
|
|
||||
// 文件上传成功 |
|
||||
const handleFileSuccess = (data: any) => { |
|
||||
// 文件上传成功后,直接保存模板信息 |
|
||||
form.file_path = data.file_path || data.url |
|
||||
form.file_name = data.file_name || data.original_name |
|
||||
|
|
||||
// 如果上传接口已经返回了完整的模板信息,直接完成 |
|
||||
if (data.id) { |
|
||||
ElMessage.success('模板上传成功') |
|
||||
emit('success') |
|
||||
visible.value = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 文件上传失败 |
|
||||
const handleFileError = (error: any) => { |
|
||||
console.error('文件上传失败:', error) |
|
||||
ElMessage.error('文件上传失败') |
|
||||
} |
|
||||
|
|
||||
// 提交表单(如果需要额外信息) |
|
||||
const submit = async () => { |
|
||||
if (!formRef.value) return |
|
||||
|
|
||||
try { |
|
||||
await formRef.value.validate() |
|
||||
|
|
||||
if (!form.file_path) { |
|
||||
ElMessage.error('请先上传模板文件') |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
loading.value = true |
|
||||
|
|
||||
// 如果文件已经上传但需要更新模板信息 |
|
||||
const data = { |
|
||||
contract_name: form.contract_name, |
|
||||
contract_type: form.contract_type, |
|
||||
remarks: form.remarks |
|
||||
} |
|
||||
|
|
||||
// 这里可以调用更新模板信息的接口 |
|
||||
// await contractTemplateApi.updateTemplate(templateId, data) |
|
||||
|
|
||||
ElMessage.success('模板信息保存成功') |
|
||||
emit('success') |
|
||||
|
|
||||
} catch (error) { |
|
||||
console.error('提交失败:', error) |
|
||||
ElMessage.error('提交失败') |
|
||||
} finally { |
|
||||
loading.value = false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 重置表单 |
|
||||
const resetForm = () => { |
|
||||
Object.assign(form, { |
|
||||
contract_name: '', |
|
||||
contract_type: '', |
|
||||
file_path: '', |
|
||||
file_name: '', |
|
||||
remarks: '' |
|
||||
}) |
|
||||
formRef.value?.resetFields() |
|
||||
} |
|
||||
|
|
||||
// 监听对话框关闭 |
|
||||
watch(visible, (newVal) => { |
|
||||
if (!newVal) { |
|
||||
resetForm() |
|
||||
} |
|
||||
}) |
|
||||
</script> |
|
||||
|
|
||||
<style scoped> |
|
||||
.file-info { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
gap: 8px; |
|
||||
margin-top: 8px; |
|
||||
padding: 8px; |
|
||||
background-color: #f5f7fa; |
|
||||
border-radius: 4px; |
|
||||
font-size: 14px; |
|
||||
color: #606266; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,37 +0,0 @@ |
|||||
<template> |
|
||||
<el-dialog |
|
||||
v-model="dialogVisible" |
|
||||
title="测试弹窗" |
|
||||
width="500px" |
|
||||
> |
|
||||
<div> |
|
||||
<p>这是一个测试弹窗</p> |
|
||||
<p>合同ID: {{ contractId }}</p> |
|
||||
</div> |
|
||||
|
|
||||
<template #footer> |
|
||||
<el-button @click="dialogVisible = false">关闭</el-button> |
|
||||
</template> |
|
||||
</el-dialog> |
|
||||
</template> |
|
||||
|
|
||||
<script setup> |
|
||||
import { ref, watch } from 'vue' |
|
||||
|
|
||||
const props = defineProps({ |
|
||||
modelValue: Boolean, |
|
||||
contractId: Number |
|
||||
}) |
|
||||
|
|
||||
const emit = defineEmits(['update:modelValue']) |
|
||||
|
|
||||
const dialogVisible = ref(false) |
|
||||
|
|
||||
watch(() => props.modelValue, (val) => { |
|
||||
dialogVisible.value = val |
|
||||
}) |
|
||||
|
|
||||
watch(dialogVisible, (val) => { |
|
||||
emit('update:modelValue', val) |
|
||||
}) |
|
||||
</script> |
|
||||
File diff suppressed because it is too large
Loading…
Reference in new issue