master
weizong song 6 months ago
parent e6b86e7696
commit e5e9da422b

@ -24,3 +24,8 @@ VITE_ENABLE_DEBUG=true
# 预发布环境示例: dist-staging
# 生产环境示例: dist-prod
VITE_BUILD_OUT_DIR=dist
# Vite 开发代理配置(仅开发环境需要)
# 开发环境代理目标,用于解决 CORS 问题
# 示例: http://czemc.localhost 或 http://localhost:8000
VITE_API_PROXY_TARGET=http://czemc.localhost

@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5501
}

@ -0,0 +1,952 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>画布设置 - 事前流程模板</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/bootstrap/5.3.0/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.staticfile.org/bootstrap-icons/1.11.0/font/bootstrap-icons.min.css">
<style>
:root {
--panel-bg: #fff;
--border: #e5e7eb;
--muted: #6b7280;
--primary: #4f46e5;
--a4-width: 210mm;
--a4-height: 297mm;
}
* { box-sizing: border-box; }
body {
background: #f5f7fb;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
color: #111827;
height: 100vh;
display: flex;
flex-direction: column;
margin: 0;
overflow: hidden;
}
.layout {
display: grid;
grid-template-columns: 380px 1fr 400px;
grid-template-rows: 1fr;
gap: 12px;
padding: 12px;
height: calc(100vh - 60px);
overflow: hidden;
}
.bottom-toolbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--panel-bg);
border-top: 1px solid var(--border);
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
box-shadow: 0 -2px 8px rgba(0,0,0,0.04);
z-index: 100;
}
.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;
}
.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: auto; }
/* 分支配置区域 */
.branch-config-section {
margin-bottom: 20px;
}
.branch-list-table {
font-size: 12px;
}
.branch-list-table table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
}
.branch-list-table th {
background: #f9fafb;
padding: 8px 6px;
text-align: left;
font-weight: 600;
border: 1px solid var(--border);
}
.branch-list-table td {
padding: 6px;
border: 1px solid var(--border);
}
.branch-list-table tbody tr {
cursor: pointer;
transition: background 0.2s;
}
.branch-list-table tbody tr:hover {
background: #f3f4f6;
}
.branch-list-table tbody tr.active {
background: #eef2ff;
border-left: 3px solid var(--primary);
}
.branch-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
background: #eef2ff;
color: var(--primary);
margin: 1px;
}
.condition-editor {
background: #f9fafb;
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px;
margin-top: 10px;
}
.condition-row {
display: grid;
grid-template-columns: 100px 1fr 80px;
gap: 8px;
margin-bottom: 8px;
align-items: center;
}
/* A4画布区域 */
.canvas-wrapper {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
overflow: auto;
background: #e5e7eb;
}
.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;
}
.document-section:hover {
border-color: var(--primary);
background: #fafbff;
}
.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-header .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: 10px;
margin-bottom: 12px;
background: #fafbff;
}
.property-group-title {
font-weight: 600;
margin-bottom: 10px;
color: #374151;
font-size: 13px;
}
.json-editor {
font-family: 'Courier New', monospace;
font-size: 12px;
min-height: 300px;
}
</style>
</head>
<body>
<div class="layout">
<!-- 左侧:分支配置区域 -->
<div class="panel">
<div class="panel-header">
<span>分支配置管理</span>
</div>
<div class="panel-body">
<!-- 第一层:事前流程分类选择 -->
<div class="branch-config-section">
<div class="panel-header" style="padding: 8px 0; border-bottom: 1px solid var(--border); margin-bottom: 10px;">
<span style="font-size: 13px; font-weight: 600;">事前流程分类</span>
</div>
<select class="form-select form-select-sm mb-3" id="processCategorySelect" onchange="switchProcessCategory()">
<option value="">请选择分类</option>
<option value="采购管理">采购管理</option>
<option value="会议培训接待">会议、培训、接待</option>
<option value="车船管理及维修">车船管理及维修</option>
<option value="仪器管理">仪器管理</option>
<option value="出差管理">出差管理</option>
<option value="合同管理">合同管理</option>
<option value="安装维修">安装维修</option>
</select>
</div>
<!-- 第二层:该分类下的分支列表 -->
<div class="branch-config-section" id="branchListSection" style="display: none;">
<div class="d-flex justify-content-between align-items-center mb-2">
<span style="font-size: 13px; font-weight: 600;">分支条件组合</span>
<button class="btn btn-sm btn-outline-primary" id="btnAddBranch" style="padding: 2px 8px; font-size: 11px;">
<i class="bi bi-plus"></i> 添加分支
</button>
</div>
<div class="branch-list-table">
<table>
<thead>
<tr>
<th style="width: 30px;">#</th>
<th>条件组合</th>
<th style="width: 50px;">操作</th>
</tr>
</thead>
<tbody id="branchListTableBody">
<!-- 动态生成 -->
</tbody>
</table>
</div>
</div>
<!-- 第三层:当前分支条件设置 -->
<div class="branch-config-section" id="conditionSection" style="display: none;">
<div class="panel-header" style="padding: 8px 0; border-bottom: 1px solid var(--border); margin-bottom: 10px;">
<span style="font-size: 13px;">当前分支条件设置</span>
</div>
<div id="conditionEditor">
<div class="condition-editor">
<div class="condition-row">
<label class="form-label small">合同分类</label>
<select class="form-select form-select-sm" id="condContractClass">
<option value="">全部</option>
<option value="合同类">合同类</option>
<option value="其他支出类">其他支出类</option>
</select>
<button class="btn btn-sm btn-outline-secondary" onclick="clearCondition('contract_classification')">清除</button>
</div>
<div class="condition-row">
<label class="form-label small">事务类型</label>
<select class="form-select form-select-sm" id="condBusinessType">
<option value="">全部</option>
<option value="项目采购类">项目采购类</option>
<option value="信水借电类">信水借电类</option>
<option value="差旅报销">差旅报销</option>
</select>
<button class="btn btn-sm btn-outline-secondary" onclick="clearCondition('business_type')">清除</button>
</div>
<div class="condition-row">
<label class="form-label small">合同类型</label>
<select class="form-select form-select-sm" id="condContractType">
<option value="">全部</option>
<option value="支出类">支出类</option>
<option value="收入类">收入类</option>
<option value="无费用发生类">无费用发生类</option>
</select>
<button class="btn btn-sm btn-outline-secondary" onclick="clearCondition('contract_type')">清除</button>
</div>
<div class="condition-row">
<label class="form-label small">采购形式</label>
<select class="form-select form-select-sm" id="condProcurementMethod">
<option value="">全部</option>
<option value="政府采购">政府采购</option>
<option value="小型项目">小型项目</option>
<option value="直接发包">直接发包</option>
</select>
<button class="btn btn-sm btn-outline-secondary" onclick="clearCondition('procurement_method')">清除</button>
</div>
<button class="btn btn-primary btn-sm w-100 mt-2" onclick="applyConditions()">
<i class="bi bi-check-circle me-1"></i>应用条件
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 中间A4画布区域 -->
<div class="panel">
<div class="panel-header">
<span>A4文档预览</span>
<span class="badge bg-info" id="canvasBranchBadge">全部可见</span>
</div>
<div class="canvas-wrapper">
<div class="a4-canvas" id="a4Canvas">
<!-- 区域a基本信息 -->
<div class="document-section" data-section="basic_info">
<div class="section-header">
<span>a. 基本信息</span>
<span class="section-badge">基本信息</span>
</div>
<div id="sectionBasicInfo" class="section-content">
<!-- 动态生成字段 -->
</div>
</div>
<!-- 区域b资金申请上会 -->
<div class="document-section" data-section="meeting_info">
<div class="section-header">
<span>b. 资金申请上会</span>
<span class="section-badge">上会流程</span>
</div>
<div id="sectionMeetingInfo" class="section-content">
<!-- 动态生成字段 -->
</div>
</div>
<!-- 区域c事前流程 -->
<div class="document-section" data-section="pre_approval">
<div class="section-header">
<span>c. 事前流程</span>
<span class="section-badge">附件流程</span>
</div>
<div id="sectionPreApproval" class="section-content">
<!-- 动态生成字段 -->
</div>
</div>
<!-- 区域d支付流程 -->
<div class="document-section" data-section="payment">
<div class="section-header">
<span>d. 支付流程</span>
<span class="section-badge">多轮次支付</span>
</div>
<div id="sectionPayment" class="section-content">
<!-- 动态生成字段 -->
</div>
</div>
</div>
</div>
</div>
<!-- 右侧JSON配置和属性面板 -->
<div class="panel">
<div class="panel-header">
<ul class="nav nav-tabs nav-tabs-sm" style="border-bottom: none; margin: -12px -14px 0;">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-json">JSON配置</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-properties">字段属性</button>
</li>
</ul>
</div>
<div class="panel-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tab-json">
<div class="property-group">
<div class="property-group-title">区域配置JSON</div>
<textarea class="form-control json-editor" id="jsonConfig" rows="15"></textarea>
<button class="btn btn-primary btn-sm w-100 mt-2" onclick="applyJsonConfig()">
<i class="bi bi-check-circle me-1"></i>应用配置
</button>
</div>
</div>
<div class="tab-pane fade" id="tab-properties">
<div id="propertyPanel">
<div class="text-muted small">点击画布区域进行配置</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部工具栏 -->
<div class="bottom-toolbar">
<button class="btn btn-outline-secondary btn-sm" id="btnImport">
<i class="bi bi-upload me-1"></i>导入
</button>
<button class="btn btn-outline-secondary btn-sm" id="btnExport">
<i class="bi bi-download me-1"></i>导出
</button>
<button class="btn btn-primary btn-sm" id="btnSave">
<i class="bi bi-save me-1"></i>保存
</button>
<button class="btn btn-success btn-sm" id="btnPreview">
<i class="bi bi-printer me-1"></i>打印预览
</button>
</div>
<script src="https://cdn.staticfile.org/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script>
// 默认区域配置JSON
const defaultSectionConfig = {
basic_info: {
title: "基本信息",
fields: [
{ key: "title", label: "事项标题", type: "text", required: true },
{ key: "amount", label: "金额", type: "number", required: true },
{ key: "project_name", label: "项目名称", type: "text" },
{ key: "budget_source", label: "资金来源", type: "text" },
{ key: "apply_date", label: "申请日期", type: "date" }
]
},
meeting_info: {
title: "拟支上会",
fields: [
{ key: "meeting_date", label: "上会日期", type: "date" },
{ key: "meeting_result", label: "上会结果", type: "text" },
{ key: "meeting_amount", label: "上会金额", type: "number" },
{ key: "meeting_notes", label: "上会备注", type: "textarea" }
]
},
pre_approval: {
title: "事前流程",
fields: [
{ key: "procurement_approval", label: "采购审批", type: "checkbox" },
{ key: "contract_approval", label: "合同审批", type: "checkbox" },
{ key: "bidding_process", label: "招标流程", type: "checkbox" },
{ key: "attachments", label: "附件清单", type: "textarea" }
]
},
payment: {
title: "支付流程",
rounds: [
{
round: 1,
fields: [
{ key: "payment_amount_1", label: "支付金额", type: "number" },
{ key: "payment_date_1", label: "支付日期", type: "date" },
{ key: "payment_status_1", label: "支付状态", type: "text" }
]
}
]
}
};
let currentProcessCategory = ''; // 当前选中的事前流程分类
let currentBranch = ''; // 当前选中的分支ID
let processCategories = {}; // 数据结构:{ "采购管理": { branches: {...}, configs: {...} } }
// 初始化分类结构
function initProcessCategories() {
const categories = ['采购管理', '会议培训接待', '车船管理及维修', '仪器管理', '出差管理', '合同管理', '安装维修'];
categories.forEach(category => {
if (!processCategories[category]) {
processCategories[category] = {
branches: {},
configs: {}
};
}
});
}
// 初始化
function initialize() {
initProcessCategories();
// 绑定事件
document.getElementById('btnAddBranch').addEventListener('click', addNewBranch);
document.getElementById('btnSave').addEventListener('click', saveConfig);
document.getElementById('btnExport').addEventListener('click', exportConfig);
document.getElementById('btnImport').addEventListener('click', importConfig);
document.getElementById('btnPreview').addEventListener('click', previewPrint);
// 区域点击事件
document.querySelectorAll('.document-section').forEach(section => {
section.addEventListener('click', () => {
const sectionId = section.dataset.section;
showSectionProperties(sectionId);
});
});
}
// 切换事前流程分类
function switchProcessCategory() {
const category = document.getElementById('processCategorySelect').value;
currentProcessCategory = category;
currentBranch = '';
if (category) {
document.getElementById('branchListSection').style.display = 'block';
document.getElementById('conditionSection').style.display = 'none';
renderBranchListTable();
document.getElementById('canvasBranchBadge').textContent = category;
} else {
document.getElementById('branchListSection').style.display = 'none';
document.getElementById('conditionSection').style.display = 'none';
document.getElementById('canvasBranchBadge').textContent = '请选择分类';
}
renderDocument();
updateJsonConfig();
}
// 渲染分支列表(显示当前分类下的分支)
function renderBranchListTable() {
const tbody = document.getElementById('branchListTableBody');
tbody.innerHTML = '';
if (!currentProcessCategory || !processCategories[currentProcessCategory]) {
return;
}
const categoryData = processCategories[currentProcessCategory];
const branches = Object.values(categoryData.branches);
if (branches.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted" style="padding: 20px;">该分类下暂无分支,点击"添加分支"创建</td></tr>';
return;
}
branches.forEach((branch, index) => {
const tr = document.createElement('tr');
tr.className = branch.id === currentBranch ? 'active' : '';
tr.onclick = () => switchToBranch(branch.id);
const conditionBadges = branch.conditionLabels.map(label =>
`<span class="branch-badge">${label}</span>`
).join('');
tr.innerHTML = `
<td>${index + 1}</td>
<td>
<div style="font-weight: 500; margin-bottom: 2px; font-size: 12px;">${branch.name}</div>
<div style="font-size: 10px;">${conditionBadges || '<span class="text-muted">默认分支</span>'}</div>
</td>
<td>
<button class="btn btn-xs btn-outline-danger" onclick="event.stopPropagation(); deleteBranch('${branch.id}')" title="删除">
<i class="bi bi-trash"></i>
</button>
</td>
`;
tbody.appendChild(tr);
});
}
// 渲染文档
function renderDocument() {
let config = defaultSectionConfig;
if (currentProcessCategory && currentBranch) {
const categoryData = processCategories[currentProcessCategory];
if (categoryData && categoryData.configs[currentBranch]) {
config = categoryData.configs[currentBranch];
}
} else if (currentProcessCategory) {
// 如果只选择了分类,使用分类的默认配置
const categoryData = processCategories[currentProcessCategory];
if (categoryData && categoryData.defaultConfig) {
config = categoryData.defaultConfig;
}
}
// 渲染基本信息
renderSection('sectionBasicInfo', config.basic_info);
// 渲染拟支上会
renderSection('sectionMeetingInfo', config.meeting_info);
// 渲染事前流程
renderSection('sectionPreApproval', config.pre_approval);
// 渲染支付流程
renderPaymentSection('sectionPayment', config.payment);
}
function renderSection(containerId, sectionConfig) {
const container = document.getElementById(containerId);
if (!container || !sectionConfig) return;
container.innerHTML = '';
if (sectionConfig.fields) {
sectionConfig.fields.forEach(field => {
const fieldDiv = document.createElement('div');
fieldDiv.className = 'field-item';
fieldDiv.innerHTML = `
<div class="field-label">${field.label}${field.required ? '<span style="color: red;">*</span>' : ''}:</div>
<div class="field-value ${!field.value ? 'empty' : ''}">${field.value || `[${field.type}]`}</div>
`;
container.appendChild(fieldDiv);
});
}
}
function renderPaymentSection(containerId, paymentConfig) {
const container = document.getElementById(containerId);
if (!container || !paymentConfig) return;
container.innerHTML = '';
if (paymentConfig.rounds) {
paymentConfig.rounds.forEach((round, index) => {
const roundDiv = document.createElement('div');
roundDiv.className = 'payment-round';
roundDiv.innerHTML = `<div class="payment-round-header">第 ${round.round} 轮支付</div>`;
const fieldsDiv = document.createElement('div');
fieldsDiv.className = 'field-row';
round.fields.forEach(field => {
const fieldDiv = document.createElement('div');
fieldDiv.className = 'field-item';
fieldDiv.innerHTML = `
<div class="field-label">${field.label}:</div>
<div class="field-value ${!field.value ? 'empty' : ''}">${field.value || `[${field.type}]`}</div>
`;
fieldsDiv.appendChild(fieldDiv);
});
roundDiv.appendChild(fieldsDiv);
container.appendChild(roundDiv);
});
}
}
// 切换分支
function switchToBranch(branchId) {
if (!currentProcessCategory) return;
currentBranch = branchId;
const categoryData = processCategories[currentProcessCategory];
const branch = categoryData.branches[branchId];
if (branch) {
document.getElementById('canvasBranchBadge').textContent = `${currentProcessCategory} - ${branch.name}`;
renderBranchListTable();
renderDocument();
updateJsonConfig();
// 显示条件设置区域
document.getElementById('conditionSection').style.display = 'block';
// 回填条件设置到编辑器
if (branch.conditions) {
const conditions = branch.conditions;
document.getElementById('condContractClass').value = conditions.contract_classification || '';
document.getElementById('condBusinessType').value = conditions.business_type || '';
document.getElementById('condContractType').value = conditions.contract_type || '';
document.getElementById('condProcurementMethod').value = conditions.procurement_method || '';
}
} else {
// 清空所有条件
document.getElementById('condContractClass').value = '';
document.getElementById('condBusinessType').value = '';
document.getElementById('condContractType').value = '';
document.getElementById('condProcurementMethod').value = '';
document.getElementById('conditionSection').style.display = 'none';
}
}
// 应用条件
function applyConditions() {
if (!currentProcessCategory) {
alert('请先选择事前流程分类');
return;
}
const conditions = {
contract_classification: document.getElementById('condContractClass').value,
business_type: document.getElementById('condBusinessType').value,
contract_type: document.getElementById('condContractType').value,
procurement_method: document.getElementById('condProcurementMethod').value
};
const labels = Object.values(conditions).filter(v => v).join('-');
if (!labels) {
alert('请至少选择一个条件');
return;
}
const branchId = 'branch_' + Date.now();
const categoryData = processCategories[currentProcessCategory];
categoryData.branches[branchId] = {
id: branchId,
name: labels || '新分支',
conditions,
conditionLabels: Object.values(conditions).filter(v => v)
};
categoryData.configs[branchId] = JSON.parse(JSON.stringify(defaultSectionConfig));
switchToBranch(branchId);
}
function clearCondition(key) {
// 清除对应条件的下拉框
const map = {
'contract_classification': 'condContractClass',
'business_type': 'condBusinessType',
'contract_type': 'condContractType',
'procurement_method': 'condProcurementMethod'
};
const el = document.getElementById(map[key]);
if (el) el.value = '';
}
// 更新JSON配置
function updateJsonConfig() {
let config = defaultSectionConfig;
if (currentProcessCategory && currentBranch) {
const categoryData = processCategories[currentProcessCategory];
if (categoryData && categoryData.configs[currentBranch]) {
config = categoryData.configs[currentBranch];
}
}
document.getElementById('jsonConfig').value = JSON.stringify(config, null, 2);
}
// 应用JSON配置
function applyJsonConfig() {
if (!currentProcessCategory || !currentBranch) {
alert('请先选择分类和分支');
return;
}
try {
const config = JSON.parse(document.getElementById('jsonConfig').value);
const categoryData = processCategories[currentProcessCategory];
categoryData.configs[currentBranch] = config;
renderDocument();
alert('配置已应用');
} catch (e) {
alert('JSON格式错误' + e.message);
}
}
// 显示区域属性
function showSectionProperties(sectionId) {
let config = defaultSectionConfig;
if (currentProcessCategory && currentBranch) {
const categoryData = processCategories[currentProcessCategory];
if (categoryData && categoryData.configs[currentBranch]) {
config = categoryData.configs[currentBranch];
}
}
const sectionConfig = config[sectionId];
if (!sectionConfig) return;
const panel = document.getElementById('propertyPanel');
panel.innerHTML = `
<div class="property-group">
<div class="property-group-title">${sectionConfig.title || sectionId}</div>
<div class="mb-2">
<label class="form-label small">区域标题</label>
<input class="form-control form-control-sm" value="${sectionConfig.title || ''}"
onchange="updateSectionTitle('${sectionId}', this.value)">
</div>
<div class="mb-2">
<label class="form-label small">字段数量</label>
<input type="number" class="form-control form-control-sm"
value="${sectionConfig.fields ? sectionConfig.fields.length : (sectionConfig.rounds ? sectionConfig.rounds.length : 0)}" readonly>
</div>
<button class="btn btn-primary btn-sm w-100" onclick="editSectionFields('${sectionId}')">
<i class="bi bi-pencil me-1"></i>编辑字段
</button>
</div>
`;
// 切换到属性标签页
document.querySelector('[data-bs-target="#tab-properties"]').click();
}
function updateSectionTitle(sectionId, title) {
if (!currentProcessCategory || !currentBranch) return;
const categoryData = processCategories[currentProcessCategory];
if (!categoryData.configs[currentBranch]) {
categoryData.configs[currentBranch] = JSON.parse(JSON.stringify(defaultSectionConfig));
}
categoryData.configs[currentBranch][sectionId].title = title;
renderDocument();
updateJsonConfig();
}
function editSectionFields(sectionId) {
alert('字段编辑功能(可扩展为弹窗编辑)');
}
function addNewBranch() {
if (!currentProcessCategory) {
alert('请先选择事前流程分类');
return;
}
// 显示条件设置区域
document.getElementById('conditionSection').style.display = 'block';
// 清空条件
document.getElementById('condContractClass').value = '';
document.getElementById('condBusinessType').value = '';
document.getElementById('condContractType').value = '';
document.getElementById('condProcurementMethod').value = '';
currentBranch = '';
}
function deleteBranch(branchId) {
if (!currentProcessCategory) return;
if (confirm('确定要删除该分支吗?')) {
const categoryData = processCategories[currentProcessCategory];
delete categoryData.branches[branchId];
delete categoryData.configs[branchId];
if (currentBranch === branchId) {
currentBranch = '';
document.getElementById('conditionSection').style.display = 'none';
document.getElementById('canvasBranchBadge').textContent = currentProcessCategory;
}
renderBranchListTable();
renderDocument();
}
}
// 保存配置
function saveConfig() {
const data = {
processCategories: processCategories
};
console.log('保存配置:', data);
alert('配置已保存(模拟)');
}
// 导出配置
function exportConfig() {
const data = {
processCategories: processCategories
};
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.json';
a.click();
URL.revokeObjectURL(url);
}
// 导入配置
function 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 = (event) => {
try {
const data = JSON.parse(event.target.result);
if (data.processCategories) {
processCategories = data.processCategories;
// 重新初始化分类选择器
if (currentProcessCategory && processCategories[currentProcessCategory]) {
renderBranchListTable();
renderDocument();
updateJsonConfig();
}
alert('配置已导入');
} else {
alert('文件格式不正确');
}
} catch (e) {
alert('文件格式错误:' + e.message);
}
};
reader.readAsText(file);
};
input.click();
}
// 打印预览
function previewPrint() {
window.print();
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
</script>
</body>
</html>

@ -20,5 +20,27 @@ body {
width: 100%;
height: 100vh;
}
/* 修复Element Plus按钮hover状态 - 确保hover时背景色更深 */
.el-button--primary {
background-color: #409eff;
border-color: #409eff;
}
.el-button--primary:hover {
background-color: #337ecc !important;
border-color: #337ecc !important;
}
.el-button--primary:active,
.el-button--primary.is-active {
background-color: #2b6cb0 !important;
border-color: #2b6cb0 !important;
}
.el-button--primary:focus {
background-color: #337ecc !important;
border-color: #337ecc !important;
}
</style>

@ -17,7 +17,11 @@ const config = {
// API 配置
api: {
baseURL: env.VITE_API_BASE_URL || 'http://localhost:8080/api',
// 开发环境使用相对路径通过Vite代理转发避免CORS问题
// 生产环境使用完整URL
baseURL: env.MODE === 'development'
? '/api' // 开发环境使用相对路径由Vite代理处理
: (env.VITE_API_BASE_URL || 'http://localhost:8080/api'), // 生产环境使用完整URL
timeout: Number(env.VITE_API_TIMEOUT) || 30000
},

@ -89,6 +89,21 @@
<template #title>执行率管理</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="settings">
<template #title>
<el-icon><Setting /></el-icon>
<span>系统设置</span>
</template>
<el-menu-item index="/settings/canvas-settings">
<el-icon><DocumentChecked /></el-icon>
<template #title>事前流程设置</template>
</el-menu-item>
<el-menu-item index="/settings/branch-condition-settings">
<el-icon><List /></el-icon>
<template #title>分支条件设置</template>
</el-menu-item>
</el-sub-menu>
</el-menu>
<div class="sidebar-toggle" @click="toggleCollapse">
@ -117,7 +132,7 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
@ -134,7 +149,9 @@ import {
DataAnalysis,
TrendCharts,
Fold,
Expand
Expand,
Setting,
List
} from '@element-plus/icons-vue'
const router = useRouter()
@ -153,6 +170,23 @@ const toggleCollapse = () => {
isCollapse.value = !isCollapse.value
}
//
const collapseSidebar = () => {
isCollapse.value = true
}
//
const expandSidebar = () => {
isCollapse.value = false
}
// 访 canvas-settings
watch(() => route.path, (newPath) => {
if (newPath === '/settings/canvas-settings') {
collapseSidebar()
}
}, { immediate: true })
//
const showUserMenu = () => {
ElMessage.info('用户菜单功能待开发')
@ -178,6 +212,10 @@ const handleLogout = async () => {
onMounted(() => {
username.value = localStorage.getItem('username') || '管理员'
// canvas-settings
if (route.path === '/settings/canvas-settings') {
collapseSidebar()
}
})
</script>
@ -248,14 +286,30 @@ onMounted(() => {
font-size: 16px;
}
:deep(.el-button) {
/* Header区域的按钮样式 - 只影响header内的按钮 */
.main-header :deep(.el-button) {
border-color: rgba(255, 255, 255, 0.5);
color: white;
background-color: transparent;
}
:deep(.el-button:hover) {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.7);
.main-header :deep(.el-button:hover) {
background-color: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.8);
color: white !important;
}
.main-header :deep(.el-button:active),
.main-header :deep(.el-button.is-active) {
background-color: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.9);
color: white !important;
}
.main-header :deep(.el-button:focus) {
background-color: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.8);
color: white !important;
}
/* 主容器 */

@ -20,6 +20,10 @@ import DraftQuery from '@/views/payment/DraftQuery.vue'
import Budget from '@/views/funds/Budget.vue'
import ExecutionRate from '@/views/funds/ExecutionRate.vue'
// 系统设置
import CanvasSettings from '@/views/settings/CanvasSettings.vue'
import BranchConditionSettings from '@/views/settings/BranchConditionSettings.vue'
const routes = [
{
path: '/login',
@ -79,6 +83,17 @@ const routes = [
name: 'ExecutionRate',
component: ExecutionRate
},
// 系统设置
{
path: 'settings/canvas-settings',
name: 'CanvasSettings',
component: CanvasSettings
},
{
path: 'settings/branch-condition-settings',
name: 'BranchConditionSettings',
component: BranchConditionSettings
},
// 保留原有路由
{
path: 'budget-list',

@ -75,6 +75,56 @@ export const reportAPI = {
}
}
/**
* 分支条件相关 API
*/
export const branchConditionAPI = {
// 获取分支条件列表
getList: (params) => {
return request.get('/budget/branch-conditions', params)
},
// 获取分支条件详情
getDetail: (id) => {
return request.get(`/budget/branch-conditions/${id}`)
},
// 创建分支条件
create: (data) => {
return request.post('/budget/branch-conditions', data)
},
// 更新分支条件
update: (id, data) => {
return request.put(`/budget/branch-conditions/${id}`, data)
},
// 删除分支条件
delete: (id) => {
return request.delete(`/budget/branch-conditions/${id}`)
},
// 批量删除分支条件
batchDelete: (ids) => {
return request.post('/budget/branch-conditions/batch-destroy', { ids })
},
// 添加选项
addOption: (id, value) => {
return request.post(`/budget/branch-conditions/${id}/add-option`, { value })
},
// 移除选项
removeOption: (id, value) => {
return request.post(`/budget/branch-conditions/${id}/remove-option`, { value })
},
// 验证值
validateValue: (id, value) => {
return request.post(`/budget/branch-conditions/${id}/validate-value`, { value })
}
}
/**
* 使用示例
*

@ -197,7 +197,6 @@ import {
View,
ArrowDown,
ArrowRight,
Shield,
Key
} from '@element-plus/icons-vue'
import { budgetTreeData, budgetSummary } from '@/mock'
@ -224,7 +223,7 @@ const permissionType = computed(() => {
})
const permissionIcon = computed(() => {
return filterForm.value.permission === 'department' ? Shield : Key
return filterForm.value.permission === 'department' ? Key : Key
})
const permissionText = computed(() => {

@ -264,11 +264,8 @@ import {
CircleCheck,
ArrowLeft,
ArrowRight,
LightningCharged,
User,
Wallet,
Heart,
Receipt,
WalletFilled,
Document as DocIcon
} from '@element-plus/icons-vue'
@ -293,11 +290,11 @@ const uploadForm = ref({})
const uploadFields = ref([])
const paymentTypes = [
{ id: 'utilities', title: '水电费、邮电费、食堂经费、体检费', icon: 'LightningCharged' },
{ id: 'utilities', title: '水电费、邮电费、食堂经费、体检费', icon: 'Lightning' },
{ id: 'union', title: '工会经费', icon: 'User' },
{ id: 'salary', title: '人员工资、独生子女费', icon: 'Wallet' },
{ id: 'children', title: '儿童统筹、幼托费', icon: 'Heart' },
{ id: 'tax', title: '税费', icon: 'Receipt' },
{ id: 'salary', title: '人员工资、独生子女费', icon: 'WalletFilled' },
{ id: 'children', title: '儿童统筹、幼托费', icon: 'User' },
{ id: 'tax', title: '税费', icon: 'Document' },
{ id: 'contract', title: '历史合同款项', icon: 'Document' }
]
@ -362,11 +359,9 @@ const formRules = {
const getIcon = (iconName) => {
const iconMap = {
LightningCharged,
Lightning,
User,
Wallet,
Heart,
Receipt,
WalletFilled,
Document: DocIcon
}
return iconMap[iconName] || DocIcon

@ -34,7 +34,7 @@
class="subprocess-item"
@click.stop="handleSubprocessClick(subprocess)"
>
<el-icon :size="8"><Circle /></el-icon>
<el-icon :size="10"><CircleCheck /></el-icon>
{{ subprocess.name }}
</div>
</div>
@ -68,7 +68,7 @@ import {
Tools,
Monitor,
Van,
Circle,
CircleCheck,
ArrowDown,
ArrowUp
} from '@element-plus/icons-vue'

@ -0,0 +1,377 @@
<template>
<div class="branch-condition-settings">
<div class="page-header">
<h2>分支条件设置</h2>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增条件
</el-button>
</div>
<div class="page-content">
<el-table
:data="conditionList"
v-loading="loading"
stripe
border
style="width: 100%"
>
<el-table-column type="index" label="#" width="60" />
<el-table-column prop="name" label="条件名称" min-width="150" />
<el-table-column label="可选项" min-width="300">
<template #default="{ row }">
<el-tag
v-for="(option, index) in row.options_array"
:key="index"
style="margin-right: 8px; margin-bottom: 4px"
>
{{ option }}
</el-tag>
<span v-if="!row.options_array || row.options_array.length === 0" class="text-muted">
暂无选项
</span>
</template>
</el-table-column>
<el-table-column prop="sort_order" label="排序" width="80" align="center" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.is_active ? 'success' : 'info'">
{{ row.is_active ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)"></el-button>
<el-button size="small" type="danger" @click="handleDelete(row)"></el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="条件名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入条件名称" />
</el-form-item>
<el-form-item label="可选项" prop="options">
<div class="options-editor">
<div
v-for="(option, index) in formData.options"
:key="index"
class="option-item"
>
<el-input
v-model="formData.options[index]"
placeholder="请输入选项值"
style="flex: 1"
/>
<el-button
type="danger"
size="small"
:icon="Delete"
@click="removeOption(index)"
>
删除
</el-button>
</div>
<el-button
type="primary"
size="small"
:icon="Plus"
@click="addOption"
style="width: 100%; margin-top: 10px"
>
添加选项
</el-button>
</div>
</el-form-item>
<el-form-item label="排序顺序" prop="sort_order">
<el-input-number
v-model="formData.sort_order"
:min="0"
:max="9999"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="状态" prop="is_active">
<el-switch v-model="formData.is_active" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入描述"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete } from '@element-plus/icons-vue'
import { branchConditionAPI } from '@/utils/api'
const loading = ref(false)
const submitting = ref(false)
const dialogVisible = ref(false)
const dialogTitle = ref('新增条件')
const formRef = ref(null)
const conditionList = ref([])
const currentId = ref(null)
const formData = reactive({
name: '',
options: [''],
sort_order: 0,
is_active: true,
description: ''
})
const formRules = {
name: [
{ required: true, message: '请输入条件名称', trigger: 'blur' }
],
options: [
{ required: true, message: '请至少添加一个选项', trigger: 'change' },
{
validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请至少添加一个选项'))
} else if (value.some(opt => !opt || opt.trim() === '')) {
callback(new Error('选项值不能为空'))
} else {
callback()
}
},
trigger: 'change'
}
]
}
//
const fetchConditionList = async () => {
loading.value = true
try {
const response = await branchConditionAPI.getList({ active_only: false })
if (response.code === 0) {
conditionList.value = response.data
} else {
ElMessage.error(response.msg || '获取条件列表失败')
}
} catch (error) {
ElMessage.error('获取条件列表失败:' + error.message)
} finally {
loading.value = false
}
}
//
const handleAdd = () => {
currentId.value = null
dialogTitle.value = '新增条件'
resetForm()
dialogVisible.value = true
}
//
const handleEdit = (row) => {
currentId.value = row.id
dialogTitle.value = '编辑条件'
formData.name = row.name
formData.options = row.options_array && row.options_array.length > 0
? [...row.options_array]
: ['']
formData.sort_order = row.sort_order
formData.is_active = row.is_active
formData.description = row.description || ''
dialogVisible.value = true
}
//
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm(
`确定要删除条件"${row.name}"吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const response = await branchConditionAPI.delete(row.id)
if (response.code === 0) {
ElMessage.success('删除成功')
fetchConditionList()
} else {
ElMessage.error(response.msg || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败:' + error.message)
}
}
}
//
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
//
const options = formData.options.filter(opt => opt && opt.trim() !== '')
if (options.length === 0) {
ElMessage.warning('请至少添加一个选项')
return
}
submitting.value = true
try {
const data = {
name: formData.name,
options: options,
sort_order: formData.sort_order,
is_active: formData.is_active,
description: formData.description
}
let response
if (currentId.value) {
response = await branchConditionAPI.update(currentId.value, data)
} else {
response = await branchConditionAPI.create(data)
}
if (response.code === 0) {
ElMessage.success(currentId.value ? '更新成功' : '创建成功')
dialogVisible.value = false
fetchConditionList()
} else {
ElMessage.error(response.msg || '操作失败')
}
} catch (error) {
ElMessage.error('操作失败:' + error.message)
} finally {
submitting.value = false
}
})
}
//
const addOption = () => {
formData.options.push('')
}
//
const removeOption = (index) => {
if (formData.options.length > 1) {
formData.options.splice(index, 1)
} else {
ElMessage.warning('至少需要保留一个选项')
}
}
//
const resetForm = () => {
formData.name = ''
formData.options = ['']
formData.sort_order = 0
formData.is_active = true
formData.description = ''
formRef.value?.clearValidate()
}
//
const handleDialogClose = () => {
resetForm()
currentId.value = null
}
onMounted(() => {
fetchConditionList()
})
</script>
<style scoped>
.branch-condition-settings {
padding: 20px;
background: #f5f7fa;
min-height: calc(100vh - 120px);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.page-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #303133;
}
.page-content {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.options-editor {
width: 100%;
}
.option-item {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}
.text-muted {
color: #909399;
font-size: 12px;
}
:deep(.el-table) {
font-size: 14px;
}
:deep(.el-dialog__body) {
padding: 20px;
}
</style>

@ -0,0 +1,978 @@
<template>
<div class="canvas-settings">
<div class="layout">
<!-- 左侧分支配置区域 -->
<div class="panel">
<div class="panel-header">
<span>分支配置管理</span>
</div>
<div class="panel-body">
<!-- 第一层事前流程分类选择 -->
<div class="branch-config-section">
<div class="section-title">事前流程分类</div>
<el-select
v-model="currentProcessCategory"
placeholder="请选择分类"
size="small"
style="width: 100%; margin-bottom: 12px"
@change="switchProcessCategory"
>
<el-option label="请选择分类" value="" />
<el-option label="采购管理" value="采购管理" />
<el-option label="会议、培训、接待" value="会议培训接待" />
<el-option label="车船管理及维修" value="车船管理及维修" />
<el-option label="仪器管理" value="仪器管理" />
<el-option label="出差管理" value="出差管理" />
<el-option label="合同管理" value="合同管理" />
<el-option label="安装维修" value="安装维修" />
</el-select>
</div>
<!-- 第二层该分类下的分支列表 -->
<div v-if="currentProcessCategory" class="branch-config-section">
<div class="d-flex justify-between align-center" style="margin-bottom: 8px">
<span class="section-title">分支条件组合</span>
<el-button size="small" type="primary" @click="addNewBranch">
<el-icon><Plus /></el-icon>
添加分支
</el-button>
</div>
<div class="branch-list-table">
<el-table
:data="branchList"
size="small"
style="font-size: 12px"
:row-class-name="getRowClassName"
@row-click="switchToBranch"
>
<el-table-column type="index" label="#" width="50" />
<el-table-column label="条件组合">
<template #default="{ row }">
<div style="font-weight: 500; margin-bottom: 4px; font-size: 12px">
{{ row.name }}
</div>
<div style="font-size: 10px">
<el-tag
v-for="(label, idx) in row.conditionLabels"
:key="idx"
size="small"
type="primary"
style="margin-right: 4px; margin-bottom: 2px"
>
{{ label }}
</el-tag>
<span v-if="!row.conditionLabels || row.conditionLabels.length === 0" class="text-muted">
默认分支
</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ row }">
<el-button
size="small"
type="danger"
text
@click.stop="deleteBranch(row.id)"
>
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="branchList.length === 0" class="empty-branch">
该分类下暂无分支点击"添加分支"创建
</div>
</div>
</div>
<!-- 第三层当前分支条件设置 -->
<div v-if="showConditionSection" class="branch-config-section">
<div class="section-title">当前分支条件设置</div>
<div class="condition-editor">
<div class="condition-row">
<label class="form-label">合同分类</label>
<el-select v-model="conditions.contract_classification" size="small" placeholder="全部">
<el-option label="全部" value="" />
<el-option label="合同类" value="合同类" />
<el-option label="其他支出类" value="其他支出类" />
</el-select>
<el-button size="small" @click="clearCondition('contract_classification')"></el-button>
</div>
<div class="condition-row">
<label class="form-label">事务类型</label>
<el-select v-model="conditions.business_type" size="small" placeholder="全部">
<el-option label="全部" value="" />
<el-option label="项目采购类" value="项目采购类" />
<el-option label="信水借电类" value="信水借电类" />
<el-option label="差旅报销" value="差旅报销" />
</el-select>
<el-button size="small" @click="clearCondition('business_type')"></el-button>
</div>
<div class="condition-row">
<label class="form-label">合同类型</label>
<el-select v-model="conditions.contract_type" size="small" placeholder="全部">
<el-option label="全部" value="" />
<el-option label="支出类" value="支出类" />
<el-option label="收入类" value="收入类" />
<el-option label="无费用发生类" value="无费用发生类" />
</el-select>
<el-button size="small" @click="clearCondition('contract_type')"></el-button>
</div>
<div class="condition-row">
<label class="form-label">采购形式</label>
<el-select v-model="conditions.procurement_method" size="small" placeholder="全部">
<el-option label="全部" value="" />
<el-option label="政府采购" value="政府采购" />
<el-option label="小型项目" value="小型项目" />
<el-option label="直接发包" value="直接发包" />
</el-select>
<el-button size="small" @click="clearCondition('procurement_method')"></el-button>
</div>
<el-button type="primary" size="small" style="width: 100%; margin-top: 8px" @click="applyConditions">
<el-icon><Check /></el-icon>
应用条件
</el-button>
</div>
</div>
</div>
</div>
<!-- 中间A4画布区域 -->
<div class="panel">
<div class="panel-header">
<span>A4文档预览</span>
<el-tag type="info" size="small">{{ canvasBranchBadge }}</el-tag>
</div>
<div class="canvas-wrapper">
<div class="a4-canvas">
<!-- 区域a基本信息 -->
<div
class="document-section"
:class="{ active: activeSection === 'basic_info' }"
@click="selectSection('basic_info')"
>
<div class="section-header">
<span>a. 基本信息</span>
<span class="section-badge">基本信息</span>
</div>
<div class="section-content">
<div
v-for="field in currentConfig.basic_info?.fields || []"
:key="field.key"
class="field-item"
>
<div class="field-label">
{{ field.label }}{{ field.required ? '*' : '' }}:
</div>
<div class="field-value" :class="{ empty: !field.value }">
{{ field.value || `[${field.type}]` }}
</div>
</div>
</div>
</div>
<!-- 区域b资金申请上会 -->
<div
class="document-section"
:class="{ active: activeSection === 'meeting_info' }"
@click="selectSection('meeting_info')"
>
<div class="section-header">
<span>b. 资金申请上会</span>
<span class="section-badge">上会流程</span>
</div>
<div class="section-content">
<div
v-for="field in currentConfig.meeting_info?.fields || []"
:key="field.key"
class="field-item"
>
<div class="field-label">{{ field.label }}:</div>
<div class="field-value" :class="{ empty: !field.value }">
{{ field.value || `[${field.type}]` }}
</div>
</div>
</div>
</div>
<!-- 区域c事前流程 -->
<div
class="document-section"
:class="{ active: activeSection === 'pre_approval' }"
@click="selectSection('pre_approval')"
>
<div class="section-header">
<span>c. 事前流程</span>
<span class="section-badge">附件流程</span>
</div>
<div class="section-content">
<div
v-for="field in currentConfig.pre_approval?.fields || []"
:key="field.key"
class="field-item"
>
<div class="field-label">{{ field.label }}:</div>
<div class="field-value" :class="{ empty: !field.value }">
{{ field.value || `[${field.type}]` }}
</div>
</div>
</div>
</div>
<!-- 区域d支付流程 -->
<div
class="document-section"
:class="{ active: activeSection === 'payment' }"
@click="selectSection('payment')"
>
<div class="section-header">
<span>d. 支付流程</span>
<span class="section-badge">多轮次支付</span>
</div>
<div class="section-content">
<div
v-for="(round, idx) in currentConfig.payment?.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 || `[${field.type}]` }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧JSON配置和属性面板 -->
<div class="panel">
<div class="panel-header">
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane label="JSON配置" name="json">
<div class="property-group">
<div class="property-group-title">区域配置JSON</div>
<el-input
v-model="jsonConfig"
type="textarea"
:rows="15"
class="json-editor"
/>
<el-button type="primary" size="small" style="width: 100%; margin-top: 8px" @click="applyJsonConfig">
<el-icon><Check /></el-icon>
应用配置
</el-button>
</div>
</el-tab-pane>
<el-tab-pane label="字段属性" name="properties">
<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="mb-2">
<label class="form-label">区域标题</label>
<el-input
v-model="sectionTitle"
size="small"
@blur="updateSectionTitle"
/>
</div>
<div class="mb-2">
<label class="form-label">字段数量</label>
<el-input
:value="fieldCount"
size="small"
readonly
/>
</div>
<el-button type="primary" size="small" style="width: 100%" @click="editSectionFields">
<el-icon><Edit /></el-icon>
编辑字段
</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
<!-- 底部工具栏 -->
<div class="bottom-toolbar">
<el-button size="small" @click="importConfig">
<el-icon><Upload /></el-icon>
导入
</el-button>
<el-button size="small" @click="exportConfig">
<el-icon><Download /></el-icon>
导出
</el-button>
<el-button type="primary" size="small" @click="saveConfig">
<el-icon><Document /></el-icon>
保存
</el-button>
<el-button type="success" size="small" @click="previewPrint">
<el-icon><Printer /></el-icon>
打印预览
</el-button>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Plus,
Delete,
Check,
Edit,
Upload,
Download,
Document,
Printer
} from '@element-plus/icons-vue'
// JSON
const defaultSectionConfig = {
basic_info: {
title: '基本信息',
fields: [
{ key: 'title', label: '事项标题', type: 'text', required: true },
{ key: 'amount', label: '金额', type: 'number', required: true },
{ key: 'project_name', label: '项目名称', type: 'text' },
{ key: 'budget_source', label: '资金来源', type: 'text' },
{ key: 'apply_date', label: '申请日期', type: 'date' }
]
},
meeting_info: {
title: '拟支上会',
fields: [
{ key: 'meeting_date', label: '上会日期', type: 'date' },
{ key: 'meeting_result', label: '上会结果', type: 'text' },
{ key: 'meeting_amount', label: '上会金额', type: 'number' },
{ key: 'meeting_notes', label: '上会备注', type: 'textarea' }
]
},
pre_approval: {
title: '事前流程',
fields: [
{ key: 'procurement_approval', label: '采购审批', type: 'checkbox' },
{ key: 'contract_approval', label: '合同审批', type: 'checkbox' },
{ key: 'bidding_process', label: '招标流程', type: 'checkbox' },
{ key: 'attachments', label: '附件清单', type: 'textarea' }
]
},
payment: {
title: '支付流程',
rounds: [
{
round: 1,
fields: [
{ key: 'payment_amount_1', label: '支付金额', type: 'number' },
{ key: 'payment_date_1', label: '支付日期', type: 'date' },
{ key: 'payment_status_1', label: '支付状态', type: 'text' }
]
}
]
}
}
const currentProcessCategory = ref('')
const currentBranch = ref('')
const processCategories = ref({})
const conditions = ref({
contract_classification: '',
business_type: '',
contract_type: '',
procurement_method: ''
})
const activeSection = ref('')
const activeTab = ref('json')
const sectionTitle = ref('')
//
const initProcessCategories = () => {
const categories = ['采购管理', '会议培训接待', '车船管理及维修', '仪器管理', '出差管理', '合同管理', '安装维修']
categories.forEach(category => {
if (!processCategories.value[category]) {
processCategories.value[category] = {
branches: {},
configs: {}
}
}
})
}
//
const branchList = computed(() => {
if (!currentProcessCategory.value || !processCategories.value[currentProcessCategory.value]) {
return []
}
return Object.values(processCategories.value[currentProcessCategory.value].branches)
})
const showConditionSection = computed(() => {
return currentProcessCategory.value && (currentBranch.value || true)
})
const canvasBranchBadge = computed(() => {
if (currentProcessCategory.value && currentBranch.value) {
const branch = processCategories.value[currentProcessCategory.value]?.branches[currentBranch.value]
return branch ? `${currentProcessCategory.value} - ${branch.name}` : currentProcessCategory.value
}
return currentProcessCategory.value || '请选择分类'
})
const currentConfig = computed(() => {
if (currentProcessCategory.value && currentBranch.value) {
const categoryData = processCategories.value[currentProcessCategory.value]
if (categoryData && categoryData.configs[currentBranch.value]) {
return categoryData.configs[currentBranch.value]
}
}
return defaultSectionConfig
})
const jsonConfig = computed({
get() {
return JSON.stringify(currentConfig.value, null, 2)
},
set(val) {
// JSON
}
})
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 switchProcessCategory = () => {
currentBranch.value = ''
conditions.value = {
contract_classification: '',
business_type: '',
contract_type: '',
procurement_method: ''
}
updateJsonConfig()
}
const addNewBranch = () => {
if (!currentProcessCategory.value) {
ElMessage.warning('请先选择事前流程分类')
return
}
currentBranch.value = ''
conditions.value = {
contract_classification: '',
business_type: '',
contract_type: '',
procurement_method: ''
}
}
const applyConditions = () => {
if (!currentProcessCategory.value) {
ElMessage.warning('请先选择事前流程分类')
return
}
const conditionLabels = Object.values(conditions.value).filter(v => v)
if (conditionLabels.length === 0) {
ElMessage.warning('请至少选择一个条件')
return
}
const branchId = 'branch_' + Date.now()
const categoryData = processCategories.value[currentProcessCategory.value]
categoryData.branches[branchId] = {
id: branchId,
name: conditionLabels.join('-') || '新分支',
conditions: { ...conditions.value },
conditionLabels
}
categoryData.configs[branchId] = JSON.parse(JSON.stringify(defaultSectionConfig))
switchToBranch(categoryData.branches[branchId])
}
const switchToBranch = (row) => {
if (!currentProcessCategory.value || !row) return
const branchId = row.id
currentBranch.value = branchId
const categoryData = processCategories.value[currentProcessCategory.value]
const branch = categoryData.branches[branchId]
if (branch && branch.conditions) {
conditions.value = { ...branch.conditions }
}
updateJsonConfig()
}
const getRowClassName = ({ row }) => {
return row.id === currentBranch.value ? 'active-row' : ''
}
const deleteBranch = async (branchId) => {
try {
await ElMessageBox.confirm('确定要删除该分支吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
if (!currentProcessCategory.value) return
const categoryData = processCategories.value[currentProcessCategory.value]
delete categoryData.branches[branchId]
delete categoryData.configs[branchId]
if (currentBranch.value === branchId) {
currentBranch.value = ''
conditions.value = {
contract_classification: '',
business_type: '',
contract_type: '',
procurement_method: ''
}
updateJsonConfig()
}
} catch {
//
}
}
const clearCondition = (key) => {
conditions.value[key] = ''
}
const selectSection = (sectionId) => {
activeSection.value = sectionId
if (currentSectionConfig.value) {
sectionTitle.value = currentSectionConfig.value.title || ''
}
activeTab.value = 'properties'
}
const updateSectionTitle = () => {
if (!currentProcessCategory.value || !currentBranch.value || !activeSection.value) return
const categoryData = processCategories.value[currentProcessCategory.value]
if (!categoryData.configs[currentBranch.value]) {
categoryData.configs[currentBranch.value] = JSON.parse(JSON.stringify(defaultSectionConfig))
}
categoryData.configs[currentBranch.value][activeSection.value].title = sectionTitle.value
updateJsonConfig()
}
const editSectionFields = () => {
ElMessage.info('字段编辑功能(可扩展为弹窗编辑)')
}
const applyJsonConfig = () => {
if (!currentProcessCategory.value || !currentBranch.value) {
ElMessage.warning('请先选择分类和分支')
return
}
try {
const config = JSON.parse(jsonConfig.value)
const categoryData = processCategories.value[currentProcessCategory.value]
categoryData.configs[currentBranch.value] = config
ElMessage.success('配置已应用')
} catch (e) {
ElMessage.error('JSON格式错误' + e.message)
}
}
const updateJsonConfig = () => {
// JSON使computed
}
const saveConfig = () => {
const data = {
processCategories: processCategories.value
}
console.log('保存配置:', data)
ElMessage.success('配置已保存(模拟)')
}
const exportConfig = () => {
const data = {
processCategories: processCategories.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.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 = (event) => {
try {
const data = JSON.parse(event.target.result)
if (data.processCategories) {
processCategories.value = data.processCategories
if (currentProcessCategory.value && processCategories.value[currentProcessCategory.value]) {
updateJsonConfig()
}
ElMessage.success('配置已导入')
} else {
ElMessage.error('文件格式不正确')
}
} catch (e) {
ElMessage.error('文件格式错误:' + e.message)
}
}
reader.readAsText(file)
}
input.click()
}
const previewPrint = () => {
window.print()
}
//
watch(branchList, (newList) => {
if (newList.length > 0 && !currentBranch.value) {
//
}
})
//
initProcessCategories()
</script>
<style scoped>
.canvas-settings {
height: 100%;
display: flex;
flex-direction: column;
background: #f5f7fb;
overflow: hidden;
}
:root {
--panel-bg: #fff;
--border: #e5e7eb;
--muted: #6b7280;
--primary: #4f46e5;
--a4-width: 210mm;
--a4-height: 297mm;
}
.layout {
display: grid;
grid-template-columns: 380px 1fr 400px;
grid-template-rows: 1fr;
gap: 12px;
padding: 12px;
flex: 1;
overflow: hidden;
}
.bottom-toolbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--panel-bg);
border-top: 1px solid var(--border);
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
z-index: 100;
}
.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;
}
.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: auto;
flex: 1;
}
.branch-config-section {
margin-bottom: 20px;
}
.section-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.branch-list-table {
font-size: 12px;
}
.empty-branch {
text-align: center;
padding: 20px;
color: var(--muted);
font-size: 12px;
}
.condition-editor {
background: #f9fafb;
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px;
margin-top: 10px;
}
.condition-row {
display: grid;
grid-template-columns: 80px 1fr 60px;
gap: 8px;
margin-bottom: 8px;
align-items: center;
}
.form-label {
font-size: 12px;
color: #374151;
}
.canvas-wrapper {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
overflow: auto;
background: #e5e7eb;
flex: 1;
}
.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: 10px;
margin-bottom: 12px;
background: #fafbff;
}
.property-group-title {
font-weight: 600;
margin-bottom: 10px;
color: #374151;
font-size: 13px;
}
.json-editor {
font-family: 'Courier New', monospace;
font-size: 12px;
}
.property-panel {
padding: 10px 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: 8px;
}
:deep(.el-tabs__header) {
margin: -12px -14px 0;
}
:deep(.el-tabs__content) {
padding: 12px 14px;
}
:deep(.el-table .active-row) {
background: #eef2ff;
}
:deep(.el-table .active-row:hover) {
background: #e0e7ff;
}
</style>

@ -6,6 +6,22 @@ export default defineConfig(({ mode }) => {
// 加载环境变量
const env = loadEnv(mode, process.cwd(), '')
// 开发环境必须配置代理目标
if (mode === 'development') {
if (!env.VITE_API_PROXY_TARGET) {
throw new Error(
'❌ 错误: 开发环境必须配置 VITE_API_PROXY_TARGET 环境变量\n' +
'请在 .env.development 文件中添加:\n' +
'VITE_API_PROXY_TARGET=http://czemc.localhost\n' +
'\n' +
'示例:\n' +
'VITE_API_PROXY_TARGET=http://czemc.localhost\n' +
'或\n' +
'VITE_API_PROXY_TARGET=http://localhost:8000'
)
}
}
return {
plugins: [vue()],
resolve: {
@ -15,7 +31,17 @@ export default defineConfig(({ mode }) => {
},
server: {
port: 3000,
open: true
open: true,
proxy: mode === 'development' ? {
// 代理所有 /api 请求到后端服务器
'/api': {
target: env.VITE_API_PROXY_TARGET,
changeOrigin: true,
secure: false,
// 如果需要重写路径,可以取消下面的注释
// rewrite: (path) => path.replace(/^\/api/, '')
}
} : undefined
},
// 定义全局常量替换
define: {

Loading…
Cancel
Save