|
|
<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为categoryId,value为分类对象
|
|
|
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>
|