You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1993 lines
59 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="canvas-settings">
<!-- 工具栏 - 横跨两个区域 -->
<div class="top-toolbar">
<div class="toolbar-left">
<el-button @click="handleGoBack">
<el-icon><ArrowLeft /></el-icon>
返回
</el-button>
<el-button @click="reloadGroupsOnly" type="warning">
<el-icon><Refresh /></el-icon>
重新加载分组
</el-button>
<el-button @click="importConfig">
<el-icon><Upload /></el-icon>
导入
</el-button>
<el-button @click="exportConfig">
<el-icon><Download /></el-icon>
导出
</el-button>
</div>
<div class="toolbar-right">
<el-button
type="primary"
@click="saveConfig"
:disabled="!categoryId"
>
<el-icon><Document /></el-icon>
保存{{ hasUnsavedChanges ? ' *' : '' }}
</el-button>
<el-button type="success" @click="previewPrint">
<el-icon><Printer /></el-icon>
打印预览
</el-button>
</div>
</div>
<div class="layout">
<!-- A4画布区域 -->
<div class="panel">
<div class="panel-header">
<span>可视化预览</span>
<el-tag v-if="currentCategoryName" type="info" size="small">{{ currentCategoryName }}</el-tag>
</div>
<div class="canvas-wrapper">
<div class="a4-canvas">
<!-- 动态渲染所有分组 -->
<div
v-for="(section, sectionKey, index) in currentConfig"
:key="sectionKey"
class="document-section"
:class="{ active: activeSection === sectionKey }"
@click="selectSection(sectionKey)"
>
<div class="section-header">
<span>{{ String.fromCharCode(97 + index) }}. {{ section.title || sectionKey }}</span>
<span class="section-badge">{{ section.title || sectionKey }}</span>
</div>
<div class="section-content">
<!-- 普通字段区域 -->
<template v-if="section.fields && Array.isArray(section.fields)">
<div
v-for="field in section.fields.filter(f => f.visible !== false)"
:key="field.key"
class="field-item"
>
<div class="field-label">
{{ field.label }}{{ field.required ? '*' : '' }}:
</div>
<div class="field-value" :class="{ empty: !field.value }">
<template v-if="field.element_type === 'oa_custom_model'">
<div class="element-oa-model">
<el-tag type="warning" size="small">事前流程实例</el-tag>
<span v-if="field.model_id" class="model-name">{{ getModelName(field.model_id) }}</span>
</div>
</template>
<template v-else-if="field.element_type === 'payment_model_list'">
<div class="element-legacy">
<el-tag type="info" size="small">历史类型</el-tag>
<span class="text-muted">payment_model_list将由迁移清理</span>
</div>
</template>
<template v-else>
{{ field.value || `[${getElementTypeLabel(field.element_type || field.type)}]` }}
</template>
</div>
</div>
</template>
<!-- 支付流程轮次区域 -->
<template v-else-if="section.rounds && Array.isArray(section.rounds)">
<div
v-for="(round, idx) in section.rounds"
:key="idx"
class="payment-round"
>
<div class="payment-round-header">第 {{ round.round }} 轮支付</div>
<div class="field-row">
<div
v-for="field in round.fields"
:key="field.key"
class="field-item"
>
<div class="field-label">{{ field.label }}:</div>
<div class="field-value" :class="{ empty: !field.value }">
{{ field.value || `[${getElementTypeLabel(field.element_type || field.type)}]` }}
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧:属性面板 -->
<div class="panel">
<div class="panel-header">
<span>事前流程设置</span>
</div>
<div class="panel-body">
<div class="property-panel">
<div v-if="!activeSection" class="text-muted">
点击画布区域进行配置
</div>
<div v-else class="property-group">
<div class="property-group-title">
{{ currentSectionConfig?.title || activeSection }}
</div>
<div class="form-item-wrapper">
<label class="form-label">区域标题</label>
<el-input
v-model="sectionTitle"
@blur="updateSectionTitle"
/>
</div>
<div class="form-item-wrapper">
<label class="form-label">字段数量</label>
<el-input
:value="fieldCount"
readonly
/>
</div>
<div class="form-item-wrapper">
<el-button type="primary" style="width: 100%" @click="editSectionFields">
<el-icon><Edit /></el-icon>
编辑元素
</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 元素编辑弹窗 -->
<el-dialog
v-model="fieldEditDialogVisible"
title="编辑元素配置"
width="1000px"
:close-on-click-modal="false"
>
<div style="margin-bottom: 16px">
<el-button type="primary" @click="showAddElementDialog">
<el-icon><Plus /></el-icon>
添加元素
</el-button>
</div>
<el-table
:data="currentSectionFields"
border
size="small"
style="margin-bottom: 20px"
>
<el-table-column type="index" label="#" width="60" align="center" />
<el-table-column prop="label" label="元素名称" width="150" />
<el-table-column prop="element_type" label="元素类型" width="120" align="center">
<template #default="{ row }">
<el-tag size="small" :type="getElementTypeTagType(row.element_type)">
{{ getElementTypeLabel(row.element_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="关联模型" width="150" align="center">
<template #default="{ row }">
<span v-if="row.model_id">{{ getModelName(row.model_id) }}</span>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="显示" width="80" align="center">
<template #default="{ row }">
<el-switch
v-model="row.visible"
:active-value="true"
:inactive-value="false"
/>
</template>
</el-table-column>
<el-table-column label="必填" width="80" align="center">
<template #default="{ row }">
<el-switch
v-model="row.required"
:active-value="true"
:inactive-value="false"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center">
<template #default="{ row, $index }">
<el-button
type="primary"
size="small"
circle
:icon="ArrowUp"
:disabled="$index === 0"
@click="moveFieldUp($index)"
title="上移"
/>
<el-button
type="primary"
size="small"
circle
:icon="ArrowDown"
:disabled="$index === currentSectionFields.length - 1"
@click="moveFieldDown($index)"
title="下移"
style="margin-left: 4px"
/>
<el-button
type="primary"
size="small"
:icon="Edit"
@click="editElement(row, $index)"
style="margin-left: 4px"
/>
<el-button
type="danger"
size="small"
:icon="Delete"
@click="deleteElement($index)"
style="margin-left: 4px"
/>
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="fieldEditDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveFieldConfig">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 添加/编辑元素对话框 -->
<el-dialog
v-model="elementDialogVisible"
:title="editingElementIndex !== null ? '编辑元素' : '添加元素'"
width="600px"
:close-on-click-modal="false"
>
<el-form :model="elementForm" label-width="100px">
<el-form-item label="元素名称" required>
<el-input v-model="elementForm.label" placeholder="请输入元素名称" />
</el-form-item>
<el-form-item label="选择元素" required>
<el-select
v-model="elementForm.element_id"
placeholder="请选择元素"
style="width: 100%"
filterable
:loading="loadingElements"
@change="onElementSelectChange"
>
<el-option-group label="常规表单字段">
<el-option
v-for="element in availableElements.filter(e => e.category === 'form_field')"
:key="element.id"
:label="element.name"
:value="element.id"
>
<span>{{ element.name }}</span>
<span style="color: #8492a6; font-size: 12px; margin-left: 8px">
({{ getElementTypeLabel(element.type) }})
</span>
</el-option>
</el-option-group>
<el-option-group label="事前流程">
<el-option
v-for="element in availableElements.filter(e => {
// 匹配 category === 'pre_approval_flow' 的元素
// 或者 category 为空但有 model_id 且 type === 'oa_custom_model' 的元素(兼容旧数据)
return e.category === 'pre_approval_flow' ||
(!e.category && e.model_id && (e.type === 'oa_custom_model' || e.type === 'pre_approval_flow'))
})"
:key="element.id"
:label="element.name"
:value="element.id"
>
<span>{{ element.name }}</span>
<span v-if="element.model_id" style="color: #8492a6; font-size: 12px; margin-left: 8px">
({{ getModelNameById(element.model_id) }})
</span>
</el-option>
</el-option-group>
<el-option-group label="勾选清单">
<el-option
v-for="element in availableElements.filter(e => e.category === 'checklist')"
:key="element.id"
:label="element.name"
:value="element.id"
>
<span>{{ element.name }}</span>
<span style="color: #8492a6; font-size: 12px; margin-left: 8px">
(勾选清单)
</span>
</el-option>
</el-option-group>
<el-option-group label="会议纪要">
<el-option
v-for="element in availableElements.filter(e => e.category === 'meeting_minutes' || e.type === 'meeting_minutes')"
:key="element.id"
:label="element.name"
:value="element.id"
>
<span>{{ element.name }}</span>
<span style="color: #8492a6; font-size: 12px; margin-left: 8px">
(会议纪要)
</span>
</el-option>
</el-option-group>
<el-option-group label="明细表格">
<el-option
v-for="element in availableElements.filter(e => e.category === 'detail_table')"
:key="element.id"
:label="element.name"
:value="element.id"
>
<span>{{ element.name }}</span>
<span style="color: #8492a6; font-size: 12px; margin-left: 8px">
(明细表格)
</span>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item label="字段标识">
<el-input v-model="elementForm.key" placeholder="自动生成,也可手动输入" />
</el-form-item>
<el-form-item label="是否显示">
<el-switch v-model="elementForm.visible" :active-value="true" :inactive-value="false" />
</el-form-item>
<el-form-item label="是否必填">
<el-switch v-model="elementForm.required" :active-value="true" :inactive-value="false" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="elementDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveElement"></el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Check,
Edit,
Upload,
Download,
Document,
Printer,
ArrowUp,
ArrowDown,
ArrowLeft,
Plus,
Delete,
Refresh
} from '@element-plus/icons-vue'
import { plannedExpenditureCategoryAPI, plannedExpenditureTemplateAPI, templateElementAPI, preApprovalTemplateGroupAPI } from '@/utils/api'
const route = useRoute()
const router = useRouter()
// 创建空的默认配置结构(动态结构,从接口获取分组)
const createEmptyConfig = () => ({})
// 从URL参数获取分类ID
const categoryId = ref(null)
const currentCategoryName = ref('')
const categoryMap = ref({}) // 分类映射表key为categoryIdvalue为分类对象
const currentConfig = ref(createEmptyConfig()) // 当前画布配置
const activeSection = ref('')
const sectionTitle = ref('')
const hasUnsavedChanges = ref(false) // 是否有未保存的更改
const lastSavedState = ref({}) // 上次保存的状态快照
const isInitializing = ref(false) // 是否正在初始化加载
const fieldEditDialogVisible = ref(false) // 元素编辑弹窗显示状态
const currentSectionFields = ref([]) // 当前编辑的元素列表
const currentTemplateId = ref(null) // 当前模板ID
const elementDialogVisible = ref(false) // 添加/编辑元素对话框显示状态
const editingElementIndex = ref(null) // 正在编辑的元素索引
const elementForm = ref({
key: '',
label: '',
element_type: 'text',
model_id: null,
visible: true,
required: false
}) // 元素表单数据
const oaCustomModels = ref([]) // 事前流程模型列表
const loadingModels = ref(false) // 加载模型列表状态
const availableElements = ref([]) // 可用的模版元素列表(从模版元素设置获取)
const loadingElements = ref(false) // 加载元素列表状态
// 元素类型定义(从模版元素设置动态获取)
const elementTypes = ref([])
// 规范化字段配置,确保所有字段都有 visible 和 required 属性并兼容旧的type字段
const normalizeFieldConfig = (config) => {
Object.keys(config).forEach(sectionKey => {
const section = config[sectionKey]
// 处理普通区域的 fields
if (section.fields && Array.isArray(section.fields)) {
section.fields.forEach(field => {
// 兼容旧的type字段转换为element_type
if (field.type && !field.element_type) {
field.element_type = field.type
}
if (field.visible === undefined) {
field.visible = true
}
if (field.required === undefined) {
field.required = false
}
// 确保有key字段
if (!field.key && field.label) {
field.key = field.label.toLowerCase().replace(/\s+/g, '_')
}
})
normalizeFieldKeysInList(section.fields)
}
// 处理 payment 区域的 rounds
if (section.rounds && Array.isArray(section.rounds)) {
section.rounds.forEach(round => {
if (round.fields && Array.isArray(round.fields)) {
round.fields.forEach(field => {
// 兼容旧的type字段转换为element_type
if (field.type && !field.element_type) {
field.element_type = field.type
}
if (field.visible === undefined) {
field.visible = true
}
if (field.required === undefined) {
field.required = false
}
// 确保有key字段
if (!field.key && field.label) {
field.key = field.label.toLowerCase().replace(/\s+/g, '_')
}
})
normalizeFieldKeysInList(round.fields)
}
})
}
})
}
// 按照分组的 sort_order 重新排序配置对象
const sortConfigByGroupOrder = async (config) => {
try {
// 获取所有启用的分组及其排序
const groupsResponse = await preApprovalTemplateGroupAPI.getList({ is_active: 1, all: true })
if (groupsResponse.code !== 0 || !groupsResponse.data) {
// 如果获取分组失败,返回原配置
return config
}
const groups = Array.isArray(groupsResponse.data) ? groupsResponse.data : (groupsResponse.data?.data || [])
// 创建分组ID到sort_order的映射
const groupSortMap = new Map()
groups.forEach(group => {
groupSortMap.set(group.id, group.sort_order || 0)
})
// 分离有group_id的分组和没有group_id的分组
const sectionsWithGroup = []
const sectionsWithoutGroup = []
Object.keys(config).forEach(sectionKey => {
const section = config[sectionKey]
if (section && section.group_id) {
sectionsWithGroup.push({
key: sectionKey,
section: section,
sort_order: groupSortMap.get(section.group_id) ?? 999999 // 如果找不到,放在最后
})
} else {
sectionsWithoutGroup.push({
key: sectionKey,
section: section
})
}
})
// 按照sort_order排序有group_id的分组
sectionsWithGroup.sort((a, b) => a.sort_order - b.sort_order)
// 重新构建配置对象先放有group_id的分组已排序再放没有group_id的分组
const sortedConfig = {}
sectionsWithGroup.forEach(item => {
sortedConfig[item.key] = item.section
})
sectionsWithoutGroup.forEach(item => {
sortedConfig[item.key] = item.section
})
return sortedConfig
} catch (error) {
console.error('按分组排序配置失败:', error)
// 如果出错,返回原配置
return config
}
}
// 获取元素类型标签
const getElementTypeLabel = (elementType) => {
if (!elementType) return ''
// 如果elementTypes还没有加载使用硬编码的映射
if (!elementTypes.value || elementTypes.value.length === 0) {
const typeLabels = {
text: '文本',
number: '数字',
date: '日期',
textarea: '文本域',
checkbox: '复选框',
radio: '单选框',
select: '下拉选择',
file: '文件上传',
oa_custom_model: '事前流程',
// 兼容历史遗留类型(将由迁移清理)
payment_model_list: '历史类型(待清理)',
checklist: '勾选清单',
meeting_minutes: '会议纪要',
detail_table: '明细表格'
}
return typeLabels[elementType] || elementType
}
const type = elementTypes.value.find(t => t.value === elementType)
return type ? type.label : elementType
}
// 获取元素类型标签颜色
const getElementTypeTagType = (elementType) => {
if (elementType === 'oa_custom_model' || elementType === 'payment_model_list') {
return 'warning'
}
if (elementType === 'checklist') {
return 'primary'
}
if (elementType === 'detail_table') {
return 'success'
}
return ''
}
// 获取模型名称
const getModelName = (modelId) => {
const model = oaCustomModels.value.find(m => m.id === modelId)
return model ? model.name : `模型ID: ${modelId}`
}
// 加载事前流程模型列表
const loadOaCustomModels = async () => {
try {
loadingModels.value = true
const response = await templateElementAPI.getPreApprovalFlowModels()
if (response.code === 0 && response.data) {
oaCustomModels.value = Array.isArray(response.data) ? response.data : []
} else {
ElMessage.warning(response.msg || '加载事前流程模型列表失败')
}
} catch (error) {
console.error('加载事前流程模型列表失败:', error)
ElMessage.warning('加载事前流程模型列表失败')
} finally {
loadingModels.value = false
}
}
// 补充配置中的 model_id用于显示不保存到配置中
const enrichConfigWithModelId = async (config) => {
if (!config) return config
// 收集所有需要补充 model_id 的字段
const preApprovalFlowFields = []
Object.keys(config).forEach(sectionKey => {
const section = config[sectionKey]
if (section.fields && Array.isArray(section.fields)) {
section.fields.forEach(field => {
if (field.element_type === 'oa_custom_model' && field.element_id && !field.model_id) {
preApprovalFlowFields.push(field)
}
})
}
})
// 如果有需要补充的字段,批量获取模板元素
if (preApprovalFlowFields.length > 0) {
try {
const response = await templateElementAPI.getList({ all: true })
if (response.code === 0) {
const elements = Array.isArray(response.data) ? response.data : (response.data?.data || [])
const elementMap = new Map(elements.map(e => [e.id, e]))
// 为事前流程字段补充 model_id仅用于显示
preApprovalFlowFields.forEach(field => {
const element = elementMap.get(field.element_id)
if (element && element.model_id) {
field.model_id = element.model_id
}
})
}
} catch (error) {
console.error('补充 model_id 失败:', error)
}
}
return config
}
// 加载可用的模版元素列表(从模版元素设置获取)
const loadAvailableElements = async () => {
try {
loadingElements.value = true
const response = await templateElementAPI.getList({ is_active: 1, all: true })
if (response.code === 0) {
const data = Array.isArray(response.data) ? response.data : (response.data?.data || [])
availableElements.value = data
// 构建元素类型列表
const typesMap = new Map()
// 常规表单字段类型
const formFieldTypes = ['text', 'number', 'date', 'textarea', 'checkbox', 'radio', 'select', 'file']
formFieldTypes.forEach(type => {
const typeLabels = {
text: '文本',
number: '数字',
date: '日期',
textarea: '文本域',
checkbox: '复选框',
radio: '单选框',
select: '下拉选择',
file: '文件上传'
}
typesMap.set(type, typeLabels[type] || type)
})
// 事前流程类型
typesMap.set('oa_custom_model', '事前流程')
// 不再提供“关联合同/关联支付流程”类元素
// 勾选清单类型
typesMap.set('checklist', '勾选清单')
// 明细表格类型
typesMap.set('detail_table', '明细表格')
// 转换为数组格式
elementTypes.value = Array.from(typesMap.entries()).map(([value, label]) => ({
value,
label
}))
} else {
ElMessage.warning(response.msg || '加载模版元素列表失败')
}
} catch (error) {
console.error('加载模版元素列表失败:', error)
ElMessage.warning('加载模版元素列表失败')
} finally {
loadingElements.value = false
}
}
// 显示添加元素对话框
const showAddElementDialog = () => {
editingElementIndex.value = null
elementForm.value = {
element_id: null,
key: '',
label: '',
element_type: '',
model_id: null,
visible: true,
required: false
}
elementDialogVisible.value = true
}
// 编辑元素
const editElement = (element, index) => {
editingElementIndex.value = index
// 优先通过 element_id 查找,如果没有 element_id 则通过其他条件查找(兼容旧数据)
let templateElement = null
if (element.element_id) {
templateElement = availableElements.value.find(e => e.id === element.element_id)
} else {
// 兼容旧数据:通过其他条件查找
templateElement = availableElements.value.find(e =>
e.name === element.label ||
(element.element_type === 'oa_custom_model' && e.category === 'pre_approval_flow' && e.model_id === element.model_id) ||
// payment_model_list 等历史类型已废弃(模板配置由迁移清理)
(element.element_type === 'checklist' && e.category === 'checklist') ||
(element.element_type === 'meeting_minutes' && (e.category === 'meeting_minutes' || e.type === 'meeting_minutes')) ||
(element.element_type === 'detail_table' && e.category === 'detail_table')
)
}
// 如果找到了模板元素,从模板元素中获取 model_id不再使用配置中的 model_id
const modelId = templateElement && templateElement.model_id ? templateElement.model_id : null
elementForm.value = {
element_id: templateElement ? templateElement.id : (element.element_id || null),
key: element.key || '',
label: element.label || '',
element_type: element.element_type || element.type || '',
model_id: modelId, // 从模板元素中获取,不再使用配置中的 model_id
visible: element.visible !== undefined ? element.visible : true,
required: element.required !== undefined ? element.required : false
}
elementDialogVisible.value = true
}
// 删除元素
const deleteElement = (index) => {
ElMessageBox.confirm('确定要删除这个元素吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
currentSectionFields.value.splice(index, 1)
ElMessage.success('删除成功')
}).catch(() => {})
}
const getUniqueFieldKey = (baseKey, editingIndex = null) => {
const normalizedBase = String(baseKey || '').trim()
if (!normalizedBase) return ''
const usedKeys = new Set()
currentSectionFields.value.forEach((field, idx) => {
if (editingIndex !== null && idx === editingIndex) return
const key = String(field?.key || '').trim()
if (key) {
usedKeys.add(key)
}
})
if (!usedKeys.has(normalizedBase)) {
return normalizedBase
}
let seq = 2
let candidate = `${normalizedBase}__${seq}`
while (usedKeys.has(candidate)) {
seq += 1
candidate = `${normalizedBase}__${seq}`
}
return candidate
}
const normalizeFieldKeysInList = (fields = []) => {
const usedKeys = new Set()
let autoKeySeed = 1
fields.forEach((field) => {
if (!field || typeof field !== 'object') return
let baseKey = String(field.key || '').trim()
if (!baseKey) {
baseKey = `field_${autoKeySeed++}`
}
let candidate = baseKey
let seq = 2
while (usedKeys.has(candidate)) {
candidate = `${baseKey}__${seq}`
seq += 1
}
field.key = candidate
usedKeys.add(candidate)
})
return fields
}
// 元素选择改变时的处理
const onElementSelectChange = () => {
const selectedElement = availableElements.value.find(e => e.id === elementForm.value.element_id)
if (selectedElement) {
elementForm.value.label = selectedElement.name
elementForm.value.key = getUniqueFieldKey(`element_${selectedElement.id}`, editingElementIndex.value)
// 根据元素的category和type设置element_type
if (selectedElement.category === 'form_field') {
elementForm.value.element_type = selectedElement.type || 'text'
elementForm.value.model_id = null
} else if (selectedElement.category === 'pre_approval_flow' ||
(!selectedElement.category && selectedElement.model_id && (selectedElement.type === 'oa_custom_model' || selectedElement.type === 'pre_approval_flow'))) {
// 事前流程category === 'pre_approval_flow' 或 category为空但有model_id
elementForm.value.element_type = 'oa_custom_model'
elementForm.value.model_id = selectedElement.model_id || null
} else if (selectedElement.category === 'checklist') {
// 勾选清单
elementForm.value.element_type = 'checklist'
elementForm.value.model_id = null
} else if (selectedElement.category === 'meeting_minutes' || selectedElement.type === 'meeting_minutes') {
// 会议纪要
elementForm.value.element_type = 'meeting_minutes'
elementForm.value.model_id = null
} else if (selectedElement.category === 'detail_table') {
// 明细表格
elementForm.value.element_type = 'detail_table'
elementForm.value.model_id = null
}
}
}
// 根据model_id获取模型名称
const getModelNameById = (modelId) => {
const model = oaCustomModels.value.find(m => m.id === modelId)
return model ? model.name : `模型ID: ${modelId}`
}
// 保存元素
const saveElement = async () => {
if (!elementForm.value.element_id) {
ElMessage.warning('请选择元素')
return
}
const selectedElement = availableElements.value.find(e => e.id === elementForm.value.element_id)
if (!selectedElement) {
ElMessage.warning('选择的元素不存在')
return
}
const baseKey = String(elementForm.value.key || '').trim() || `element_${selectedElement.id}`
const uniqueKey = getUniqueFieldKey(baseKey, editingElementIndex.value)
if (uniqueKey !== baseKey) {
ElMessage.warning(`字段键名重复,已自动调整为 ${uniqueKey}`)
}
elementForm.value.key = uniqueKey
const element = {
key: elementForm.value.key,
label: selectedElement.name,
element_type: elementForm.value.element_type,
visible: elementForm.value.visible,
required: elementForm.value.required,
element_id: selectedElement.id // 保存元素ID以便后续使用通过element_id查询model_id
}
// 不再在配置中存储model_id改为通过element_id动态查询
if (editingElementIndex.value !== null) {
// 编辑
currentSectionFields.value[editingElementIndex.value] = element
} else {
// 添加
currentSectionFields.value.push(element)
}
elementDialogVisible.value = false
ElMessage.success(editingElementIndex.value !== null ? '编辑成功' : '添加成功')
}
// 构建分类映射表(递归)
const buildCategoryMap = (categories) => {
categories.forEach(cat => {
categoryMap.value[cat.id] = {
id: cat.id,
name: cat.name,
parent_id: cat.parent_id,
level: cat.level,
is_leaf: cat.is_leaf
}
if (cat.children && cat.children.length > 0) {
buildCategoryMap(cat.children)
}
})
}
// 加载分类树(用于构建映射表)
const loadCategoryTree = async () => {
try {
const response = await plannedExpenditureCategoryAPI.getCategoryTree()
if (response.code === 0) {
const tree = response.data || []
buildCategoryMap(tree)
}
} catch (error) {
ElMessage.error('加载分类树失败:' + error.message)
}
}
// 根据分类ID加载分类信息和模板
const loadCategoryAndTemplate = async (catId) => {
if (!catId) {
ElMessage.warning('请提供分类ID')
return
}
try {
// 设置分类ID和名称
categoryId.value = parseInt(catId)
const category = categoryMap.value[categoryId.value]
if (category) {
currentCategoryName.value = category.name
} else {
// 如果映射表中没有尝试从API获取
const response = await plannedExpenditureCategoryAPI.getCategoryTree()
if (response.code === 0) {
buildCategoryMap(response.data || [])
const foundCategory = categoryMap.value[categoryId.value]
if (foundCategory) {
currentCategoryName.value = foundCategory.name
}
}
}
// 加载该分类的模板(每个分类只有一个模板)
const templateResponse = await plannedExpenditureTemplateAPI.getByCategory(categoryId.value)
if (templateResponse.code === 0) {
const templates = templateResponse.data || []
// 每个分类只有一个模板,直接取第一个
const template = templates.length > 0 ? templates[0] : null
if (template && template.config) {
// 使用模板的配置
currentTemplateId.value = template.id
normalizeFieldConfig(template.config)
// 按照分组的 sort_order 重新排序配置
const sortedConfig = await sortConfigByGroupOrder(template.config)
// 补充 model_id 用于显示(不保存到配置中)
await enrichConfigWithModelId(sortedConfig)
currentConfig.value = sortedConfig
} else {
// 如果没有模板,从"事前流程模版设置"获取默认配置
currentTemplateId.value = null
const newConfig = await loadDefaultTemplateFromGroups()
if (newConfig) {
normalizeFieldConfig(newConfig)
// 补充 model_id 用于显示(不保存到配置中)
await enrichConfigWithModelId(newConfig)
currentConfig.value = newConfig
} else {
// 如果获取默认配置失败,使用空配置
currentConfig.value = createEmptyConfig()
}
}
markAsSaved()
} else {
// 如果没有模板,从"事前流程模版设置"获取默认配置
currentTemplateId.value = null
const newConfig = await loadDefaultTemplateFromGroups()
if (newConfig) {
normalizeFieldConfig(newConfig)
// 补充 model_id 用于显示(不保存到配置中)
await enrichConfigWithModelId(newConfig)
currentConfig.value = newConfig
} else {
// 如果获取默认配置失败,使用空配置
currentConfig.value = createEmptyConfig()
}
markAsSaved()
}
} catch (error) {
ElMessage.error('加载分类和模板失败:' + error.message)
// 出错时从"事前流程模版设置"获取默认配置
try {
const newConfig = await loadDefaultTemplateFromGroups()
if (newConfig) {
normalizeFieldConfig(newConfig)
// 补充 model_id 用于显示(不保存到配置中)
await enrichConfigWithModelId(newConfig)
currentConfig.value = newConfig
} else {
// 如果获取默认配置失败,使用空配置
currentConfig.value = createEmptyConfig()
}
} catch (e) {
console.error('获取默认配置失败:', e)
// 如果获取默认配置也失败,使用空配置
currentConfig.value = createEmptyConfig()
ElMessage.warning('无法加载模板配置,请检查"事前流程模版设置"是否已配置')
}
}
}
// 从"事前流程模版设置"获取默认模板配置
const loadDefaultTemplateFromGroups = async () => {
try {
// 确保availableElements已加载
if (availableElements.value.length === 0) {
await loadAvailableElements()
}
// 获取所有启用的分组
const groupsResponse = await preApprovalTemplateGroupAPI.getList({ is_active: 1, all: true })
if (groupsResponse.code !== 0 || !groupsResponse.data) {
throw new Error('获取分组列表失败')
}
const groups = Array.isArray(groupsResponse.data) ? groupsResponse.data : (groupsResponse.data?.data || [])
// 创建空配置对象
const config = {}
// 按分组排序顺序处理
const sortedGroups = groups.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0))
for (const group of sortedGroups) {
// 获取分组元素
const elementsResponse = await preApprovalTemplateGroupAPI.getElements(group.id)
if (elementsResponse.code === 0 && elementsResponse.data) {
const elements = elementsResponse.data || []
// 将元素转换为字段配置
const fields = elements.map(element => {
// 从availableElements中查找对应的元素获取完整信息用于获取options等
const templateElement = availableElements.value.find(e => e.id === element.element_id)
const field = {
key: `element_${element.element_id}`,
label: element.element_name,
element_type: element.element_type,
required: false,
visible: true,
element_id: element.element_id // 保存元素ID通过它查询model_id
}
// 不再在配置中存储model_id改为通过element_id动态查询
// 如果是勾选清单类型需要获取options
if (element.element_type === 'checklist' && templateElement && templateElement.options) {
field.options = templateElement.options
}
return field
})
normalizeFieldKeysInList(fields)
// 使用分组编码或ID作为配置的key分组名称作为title
const sectionKey = group.code || `group_${group.id}`
config[sectionKey] = {
title: group.name,
fields: fields,
group_id: group.id, // 保存分组ID以便后续使用
group_code: group.code
}
}
}
return config
} catch (error) {
console.error('从分组加载默认配置失败:', error)
// 返回空配置
return null
}
}
const currentSectionConfig = computed(() => {
if (!activeSection.value) return null
return currentConfig.value[activeSection.value]
})
const fieldCount = computed(() => {
if (!currentSectionConfig.value) return 0
if (currentSectionConfig.value.fields) {
return currentSectionConfig.value.fields.length
}
if (currentSectionConfig.value.rounds) {
return currentSectionConfig.value.rounds.length
}
return 0
})
// 方法
const handleGoBack = () => {
router.push('/settings/planned-expenditure-category')
}
const selectSection = (sectionId) => {
activeSection.value = sectionId
if (currentSectionConfig.value) {
sectionTitle.value = currentSectionConfig.value.title || ''
}
}
const updateSectionTitle = () => {
if (!categoryId.value || !activeSection.value) return
if (!currentConfig.value[activeSection.value]) {
// 如果区域不存在创建空结构默认使用fields
currentConfig.value[activeSection.value] = { title: '', fields: [] }
}
currentConfig.value[activeSection.value].title = sectionTitle.value
markAsUnsaved()
}
const editSectionFields = async () => {
if (!currentSectionConfig.value) {
ElMessage.warning('请先选择区域')
return
}
// 如果区域有rounds结构支付流程取第一个round的fields
if (currentSectionConfig.value.rounds && Array.isArray(currentSectionConfig.value.rounds)) {
const rounds = currentSectionConfig.value.rounds || []
if (rounds.length === 0) {
ElMessage.warning('该区域没有配置轮次')
return
}
const fields = rounds[0].fields || []
currentSectionFields.value = JSON.parse(JSON.stringify(fields))
} else {
// 其他区域的字段直接在 fields 数组中
const fields = currentSectionConfig.value.fields || []
currentSectionFields.value = JSON.parse(JSON.stringify(fields))
}
// 确保所有字段都有 visible 和 required 属性并兼容旧的type字段
currentSectionFields.value.forEach(field => {
// 兼容旧的type字段转换为element_type
if (field.type && !field.element_type) {
field.element_type = field.type
}
if (field.visible === undefined) {
field.visible = true
}
if (field.required === undefined) {
field.required = false
}
})
fieldEditDialogVisible.value = true
}
// 字段上移
const moveFieldUp = (index) => {
if (index === 0) return
const fields = currentSectionFields.value
;[fields[index - 1], fields[index]] = [fields[index], fields[index - 1]]
}
// 字段下移
const moveFieldDown = (index) => {
if (index === currentSectionFields.value.length - 1) return
const fields = currentSectionFields.value
;[fields[index], fields[index + 1]] = [fields[index + 1], fields[index]]
}
// 保存字段配置
const saveFieldConfig = () => {
if (!categoryId.value || !activeSection.value) {
ElMessage.warning('请先选择区域')
return
}
const section = currentConfig.value[activeSection.value]
if (!section) {
ElMessage.warning('区域不存在')
return
}
normalizeFieldKeysInList(currentSectionFields.value)
// 如果区域有rounds结构支付流程更新rounds[0].fields
if (section.rounds && Array.isArray(section.rounds)) {
if (section.rounds.length === 0) {
section.rounds.push({ round: 1, fields: [] })
}
section.rounds[0].fields = JSON.parse(
JSON.stringify(currentSectionFields.value)
)
} else {
// 其他区域的字段直接在 fields 数组中
if (!section.fields) {
section.fields = []
}
section.fields = JSON.parse(
JSON.stringify(currentSectionFields.value)
)
}
markAsUnsaved()
fieldEditDialogVisible.value = false
ElMessage.success('字段配置已更新')
}
// 标记为未保存
const markAsUnsaved = () => {
hasUnsavedChanges.value = true
}
// 标记为已保存
const markAsSaved = () => {
hasUnsavedChanges.value = false
// 保存当前状态快照
lastSavedState.value = JSON.parse(JSON.stringify(currentConfig.value))
}
// 重新加载分组(只更新分组结构,保留已有数据)
const reloadGroupsOnly = async () => {
try {
// 获取所有启用的分组
const groupsResponse = await preApprovalTemplateGroupAPI.getList({ is_active: 1, all: true })
if (groupsResponse.code !== 0 || !groupsResponse.data) {
ElMessage.error('获取分组列表失败')
return
}
const groups = Array.isArray(groupsResponse.data) ? groupsResponse.data : (groupsResponse.data?.data || [])
// 按分组排序顺序处理
const sortedGroups = groups.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0))
// 创建新的配置对象,保留现有配置的所有数据
const newConfig = {}
const existingConfig = currentConfig.value || {}
// 遍历所有分组
for (const group of sortedGroups) {
// 使用分组编码或ID作为配置的key
const sectionKey = group.code || `group_${group.id}`
// 检查是否存在该分组的配置
const existingSection = existingConfig[sectionKey]
if (existingSection) {
// 如果已存在,只更新分组名称和分组信息,完全保留字段配置
newConfig[sectionKey] = {
...existingSection,
title: group.name, // 只更新分组名称
group_id: group.id,
group_code: group.code
}
} else {
// 如果不存在(新分组),从模版加载元素
const elementsResponse = await preApprovalTemplateGroupAPI.getElements(group.id)
if (elementsResponse.code === 0 && elementsResponse.data) {
const elements = elementsResponse.data || []
// 确保availableElements已加载
if (availableElements.value.length === 0) {
await loadAvailableElements()
}
// 将元素转换为字段配置
const fields = elements.map(element => {
// 从availableElements中查找对应的元素获取完整信息
const templateElement = availableElements.value.find(e => e.id === element.element_id)
const field = {
key: `element_${element.element_id}`,
label: element.element_name,
element_type: element.element_type,
required: false,
visible: true,
element_id: element.element_id // 保存element_id通过它查询model_id
}
// 不再在配置中存储model_id改为通过element_id动态查询
// 如果是勾选清单类型需要获取options
if (element.element_type === 'checklist' && templateElement && templateElement.options) {
field.options = templateElement.options
}
return field
})
normalizeFieldKeysInList(fields)
newConfig[sectionKey] = {
title: group.name,
fields: fields,
group_id: group.id,
group_code: group.code
}
} else {
// 如果获取元素失败,创建空的分组结构
newConfig[sectionKey] = {
title: group.name,
fields: [],
group_id: group.id,
group_code: group.code
}
}
}
}
// 更新配置
normalizeFieldConfig(newConfig)
currentConfig.value = newConfig
// 清除当前选中的区域
activeSection.value = ''
sectionTitle.value = ''
ElMessage.success('已重新加载分组结构,已保留所有现有字段配置,新分组已加载元素')
markAsUnsaved()
} catch (error) {
ElMessage.error('重新加载分组失败:' + (error.message || '未知错误'))
console.error('重新加载分组错误:', error)
}
}
// 保存配置
const saveConfig = async () => {
if (!categoryId.value) {
ElMessage.warning('请先提供分类ID')
return
}
try {
// 根据画布上的分组顺序更新分组的 sort_order
// JavaScript 对象保持插入顺序,所以 Object.keys() 的顺序就是画布上的显示顺序
const configKeys = Object.keys(currentConfig.value)
const groupUpdatePromises = []
for (let index = 0; index < configKeys.length; index++) {
const sectionKey = configKeys[index]
const section = currentConfig.value[sectionKey]
// 如果分组有 group_id说明是从"事前流程模版设置"来的分组
if (section && section.group_id) {
groupUpdatePromises.push(
preApprovalTemplateGroupAPI.update(section.group_id, {
sort_order: index
}).catch(error => {
console.warn(`更新分组 ${section.group_id} 排序失败:`, error)
// 不阻止保存流程,只记录警告
})
)
}
}
// 并行更新所有分组的排序(不等待结果,避免影响保存流程)
Promise.all(groupUpdatePromises).catch(error => {
console.warn('更新分组排序时出现错误:', error)
})
// 构建保存数据
const templateData = {
category_id: categoryId.value,
name: currentCategoryName.value || '默认模板',
config: currentConfig.value
}
let result
// 如果已有模板ID则更新否则创建
if (currentTemplateId.value) {
result = await plannedExpenditureTemplateAPI.update(currentTemplateId.value, templateData)
} else {
result = await plannedExpenditureTemplateAPI.findOrCreate(templateData)
}
if (result.code === 0 && result.data) {
// 更新模板ID
currentTemplateId.value = result.data.id
ElMessage.success('配置已保存')
markAsSaved()
} else {
ElMessage.error('保存失败:' + (result.msg || '未知错误'))
}
} catch (error) {
ElMessage.error('保存配置失败:' + (error.message || '未知错误'))
}
}
const exportConfig = () => {
const data = {
categoryId: categoryId.value,
categoryName: currentCategoryName.value,
config: currentConfig.value
}
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `canvas-config-${categoryId.value || 'default'}.json`
a.click()
URL.revokeObjectURL(url)
ElMessage.success('配置已导出')
}
const importConfig = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = (e) => {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = async (event) => {
try {
const data = JSON.parse(event.target.result)
if (data.config) {
normalizeFieldConfig(data.config)
// 按照分组的 sort_order 重新排序配置
const sortedConfig = await sortConfigByGroupOrder(data.config)
currentConfig.value = sortedConfig
ElMessage.success('配置已导入')
markAsUnsaved()
} else {
ElMessage.error('文件格式不正确')
}
} catch (e) {
ElMessage.error('文件格式错误:' + e.message)
}
}
reader.readAsText(file)
}
input.click()
}
const previewPrint = () => {
window.print()
}
// 监听配置变化,标记为未保存
watch(
() => currentConfig.value,
() => {
// 如果正在初始化,不触发未保存状态
if (isInitializing.value) {
return
}
// 延迟检查,避免初始化时误触发
setTimeout(() => {
if (isInitializing.value) {
return
}
// 检查是否有实际变化
const currentState = JSON.stringify(currentConfig.value)
const savedState = JSON.stringify(lastSavedState.value)
if (currentState !== savedState && Object.keys(lastSavedState.value).length > 0) {
hasUnsavedChanges.value = true
}
}, 200)
},
{ deep: true }
)
// 监听路由参数变化
watch(
() => route.query.categoryId,
(newCategoryId) => {
if (newCategoryId) {
loadCategoryAndTemplate(newCategoryId)
}
},
{ immediate: true }
)
// 初始化
onMounted(async () => {
// 先加载分类树以构建映射表
await loadCategoryTree()
// 加载事前流程模型列表
await loadOaCustomModels()
// 加载可用的模版元素列表
await loadAvailableElements()
// 从URL参数获取categoryId
const categoryIdFromQuery = route.query.categoryId
if (categoryIdFromQuery) {
await loadCategoryAndTemplate(categoryIdFromQuery)
} else {
ElMessage.warning('请通过URL参数提供分类ID例如?categoryId=123')
}
})
</script>
<style scoped>
.canvas-settings {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f7fb;
overflow: hidden;
}
.top-toolbar {
background: #fff;
border-bottom: 1px solid #e4e7ed;
padding: 12px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
z-index: 100;
flex-shrink: 0;
}
:root {
--panel-bg: #fff;
--border: #e5e7eb;
--muted: #6b7280;
--primary: #4f46e5;
--a4-width: 280mm;
--a4-height: 297mm;
}
.layout {
display: grid;
grid-template-columns: 1fr 320px;
grid-template-rows: 1fr;
gap: 12px;
padding: 12px;
flex: 1;
overflow: hidden;
min-height: 0;
}
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.panel {
background: var(--panel-bg);
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
}
.panel-header {
padding: 12px 14px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 600;
color: #374151;
font-size: 14px;
}
.panel-body {
padding: 12px;
overflow: hidden;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
height: 100%;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 8px;
}
.canvas-wrapper {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 12px;
overflow: auto;
background: #e5e7eb;
flex: 1;
min-height: 0;
}
.a4-canvas {
width: var(--a4-width);
min-height: var(--a4-height);
background: white;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
padding: 20mm;
position: relative;
}
.document-section {
margin-bottom: 20px;
border: 1px dashed #d1d5db;
border-radius: 6px;
padding: 12px;
position: relative;
min-height: 80px;
cursor: pointer;
transition: all 0.2s;
}
.document-section:hover {
border-color: var(--primary);
background: #fafbff;
}
.document-section.active {
border-color: var(--primary);
background: #fafbff;
border-width: 2px;
}
.section-header {
font-weight: 700;
font-size: 16px;
color: var(--primary);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--primary);
display: flex;
justify-content: space-between;
align-items: center;
}
.section-badge {
font-size: 12px;
padding: 2px 8px;
background: var(--primary);
color: white;
border-radius: 4px;
}
.field-item {
display: flex;
margin-bottom: 10px;
align-items: center;
}
.field-label {
min-width: 100px;
font-weight: 500;
color: #374151;
font-size: 13px;
}
.field-value {
flex: 1;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 4px;
background: #f9fafb;
font-size: 13px;
min-height: 32px;
}
.field-value.empty {
color: #9ca3af;
font-style: italic;
}
.field-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.payment-round {
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px;
margin-bottom: 10px;
background: #f9fafb;
}
.payment-round-header {
font-weight: 600;
color: var(--primary);
margin-bottom: 8px;
font-size: 13px;
}
.property-group {
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
background: #fafbff;
}
.property-group-title {
font-weight: 600;
margin-bottom: 16px;
color: #374151;
font-size: 14px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.form-item-wrapper {
margin-bottom: 20px;
}
.form-item-wrapper:last-child {
margin-bottom: 0;
}
.json-editor {
font-family: 'Courier New', monospace;
font-size: 12px;
}
.property-panel {
padding: 10px 0;
overflow-y: auto;
flex: 1;
min-height: 0;
}
.text-muted {
color: var(--muted);
font-size: 12px;
}
.d-flex {
display: flex;
}
.justify-between {
justify-content: space-between;
}
.align-center {
align-items: center;
}
.mb-2 {
margin-bottom: 16px;
}
:deep(.el-table .active-row) {
background: #eef2ff;
}
:deep(.el-table .active-row:hover) {
background: #e0e7ff;
}
.element-oa-model,
.element-payment-list {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #fef3c7;
border-radius: 4px;
}
.model-name {
font-size: 12px;
color: #92400e;
}
.payment-table-preview {
margin-top: 8px;
width: 100%;
}
.preview-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.preview-table thead {
background: #f3f4f6;
}
.preview-table th,
.preview-table td {
padding: 6px 8px;
border: 1px solid #e5e7eb;
text-align: left;
}
.preview-table th {
font-weight: 600;
color: #374151;
}
/* 打印样式 - 只打印可视化预览区域 */
@media print {
/* 隐藏不需要打印的元素 */
.top-toolbar {
display: none !important;
}
/* 隐藏右侧属性面板 */
.layout > .panel:last-child {
display: none !important;
}
/* 隐藏左侧面板的header */
.layout > .panel:first-child > .panel-header {
display: none !important;
}
/* 调整布局为单列 */
.layout {
display: block !important;
width: 100% !important;
}
/* 左侧面板全宽显示 */
.layout > .panel:first-child {
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
/* 画布包装器样式 */
.canvas-wrapper {
display: block !important;
padding: 0 !important;
background: white !important;
overflow: visible !important;
height: auto !important;
}
/* A4画布区域样式 */
.a4-canvas {
display: block !important;
width: 100% !important;
min-height: auto !important;
max-width: 100% !important;
box-shadow: none !important;
padding: 20mm !important;
margin: 0 auto !important;
background: white !important;
page-break-after: auto;
}
/* 文档区域打印样式 */
.document-section {
page-break-inside: avoid;
border: none !important;
padding: 10px 0 !important;
margin-bottom: 15px !important;
background: white !important;
}
.document-section:hover,
.document-section.active {
background: white !important;
border: none !important;
}
.section-header {
border-bottom: 1px solid #000 !important;
color: #000 !important;
padding-bottom: 8px !important;
margin-bottom: 12px !important;
}
.section-badge {
display: none !important;
}
/* 字段样式 */
.field-item {
page-break-inside: avoid;
margin-bottom: 8px !important;
}
.field-label {
color: #000 !important;
}
.field-value {
border: 1px solid #ccc !important;
background: white !important;
color: #000 !important;
}
.field-value.empty {
color: #666 !important;
}
/* 隐藏所有按钮和交互元素 */
button,
.el-button,
.el-icon {
display: none !important;
}
/* 隐藏Element Plus标签的样式但保留文本内容 */
.el-tag {
background: transparent !important;
border: none !important;
padding: 0 !important;
color: #000 !important;
font-size: inherit !important;
}
/* 隐藏面板header中的标签 */
.panel-header .el-tag {
display: none !important;
}
/* 元素标签中的文本内容 */
.element-oa-model .el-tag,
.element-payment-list .el-tag {
display: inline !important;
font-weight: normal !important;
}
/* 确保页面背景为白色 */
body {
background: white !important;
margin: 0 !important;
padding: 0 !important;
}
.canvas-settings {
background: white !important;
margin: 0 !important;
padding: 0 !important;
}
/* 支付轮次样式 */
.payment-round {
border: 1px solid #ccc !important;
background: white !important;
page-break-inside: avoid;
}
.payment-round-header {
color: #000 !important;
border-bottom: 1px solid #ccc !important;
padding-bottom: 5px !important;
margin-bottom: 8px !important;
}
/* 表格样式 */
.preview-table {
border: 1px solid #000 !important;
}
.preview-table th,
.preview-table td {
border: 1px solid #000 !important;
color: #000 !important;
}
.preview-table thead {
background: #f0f0f0 !important;
}
}
</style>