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