11 KiB
计划支出模板设置页面 - 可视化预览区域默认配置数据来源
页面路径
http://localhost:3000/#/settings/planned-expenditure-template-settings?categoryId=32
路由配置
- 路由路径:
/settings/planned-expenditure-template-settings - 路由名称:
CanvasSettings - 组件位置:
czemc-budget-execution-frontend/src/views/settings/CanvasSettings.vue
可视化预览区域
可视化预览区域位于组件模板的第 36-150行,包含以下四个区域:
- 区域a:基本信息 (
basic_info) - 区域b:资金申请上会 (
meeting_info) - 区域c:事前流程 (
pre_approval) - 区域d:支付流程 (
payment)
默认配置数据来源
1. 前端硬编码默认配置
位置: czemc-budget-execution-frontend/src/views/settings/CanvasSettings.vue 第 286-328行
const defaultSectionConfig = {
basic_info: {
title: '基本信息',
fields: [
{ key: 'title', label: '事项标题', type: 'text', required: true, visible: true },
{ key: 'amount', label: '金额', type: 'number', required: true, visible: true },
{ key: 'project_name', label: '项目名称', type: 'text', required: false, visible: true },
{ key: 'budget_source', label: '资金来源', type: 'text', required: false, visible: true },
{ key: 'apply_date', label: '申请日期', type: 'date', required: false, visible: true }
]
},
meeting_info: {
title: '拟支上会',
fields: [
{ key: 'meeting_date', label: '上会日期', type: 'date', required: false, visible: true },
{ key: 'meeting_result', label: '上会结果', type: 'text', required: false, visible: true },
{ key: 'meeting_amount', label: '上会金额', type: 'number', required: false, visible: true },
{ key: 'meeting_notes', label: '上会备注', type: 'textarea', required: false, visible: true }
]
},
pre_approval: {
title: '事前流程',
fields: [
{ key: 'procurement_approval', label: '采购审批', type: 'checkbox', required: false, visible: true },
{ key: 'contract_approval', label: '合同审批', type: 'checkbox', required: false, visible: true },
{ key: 'bidding_process', label: '招标流程', type: 'checkbox', required: false, visible: true },
{ key: 'attachments', label: '附件清单', type: 'textarea', required: false, visible: true }
]
},
payment: {
title: '支付流程',
rounds: [
{
round: 1,
fields: [
{ key: 'payment_amount_1', label: '支付金额', type: 'number', required: false, visible: true },
{ key: 'payment_date_1', label: '支付日期', type: 'date', required: false, visible: true },
{ key: 'payment_status_1', label: '支付状态', type: 'text', required: false, visible: true }
]
}
]
}
}
2. 数据库模板配置
数据表: budget_planned_expenditure_templates
- 字段:
config(JSON类型,自动转换为数组) - 关联: 通过
category_id关联到budget_planned_expenditure_categories表
模型位置: backend/Modules/Budget/app/Models/PlannedExpenditureTemplate.php
protected $casts = [
'category_id' => 'integer',
'config' => 'array', // JSON自动转换为数组
'sort_order' => 'integer',
'created_by' => 'integer',
'updated_by' => 'integer',
];
数据加载流程
初始化流程
-
组件挂载 (
onMounted钩子,第 730-741行)onMounted(async () => { // 先加载分类树以构建映射表 await loadCategoryTree() // 从URL参数获取categoryId const categoryIdFromQuery = route.query.categoryId if (categoryIdFromQuery) { await loadCategoryAndTemplate(categoryIdFromQuery) } }) -
加载分类树 (
loadCategoryTree函数,第 397-407行)- API调用:
plannedExpenditureCategoryAPI.getCategoryTree() - 后端路由:
GET /budget/planned-expenditure-categories-tree - 用途: 构建分类映射表 (
categoryMap),用于快速查找分类名称
- API调用:
-
加载分类和模板 (
loadCategoryAndTemplate函数,第 409-472行)关键逻辑:
// 加载该分类的模板(每个分类只有一个模板) 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) { // 使用模板的配置 currentTemplateId.value = template.id const config = template.config || JSON.parse(JSON.stringify(defaultSectionConfig)) normalizeFieldConfig(config) currentConfig.value = config } else { // 如果没有模板,使用默认配置 currentTemplateId.value = null const newConfig = JSON.parse(JSON.stringify(defaultSectionConfig)) normalizeFieldConfig(newConfig) currentConfig.value = newConfig } }
数据优先级
-
数据库模板配置 (优先级最高)
- 如果该分类在数据库中存在模板记录,使用
template.config - 如果
template.config为空,回退到默认配置
- 如果该分类在数据库中存在模板记录,使用
-
前端默认配置 (兜底方案)
- 当数据库中没有模板记录时使用
- 当API调用失败时使用
- 当模板配置为空时使用
API接口
获取分类模板
前端API定义: czemc-budget-execution-frontend/src/utils/api.js 第 162-164行
getByCategory: (categoryId) => {
return request.get(`/budget/planned-expenditure-templates/by-category/${categoryId}`)
}
后端路由: backend/Modules/Budget/routes/api.php 第 98行
Route::get('planned-expenditure-templates/by-category/{categoryId}',
[PlannedExpenditureTemplateController::class, 'getByCategory']);
后端控制器: backend/Modules/Budget/app/Http/Controllers/Api/PlannedExpenditureTemplateController.php 第 222-241行
public function getByCategory($categoryId)
{
try {
$templates = PlannedExpenditureTemplate::with('category')
->byCategory($categoryId)
->whereHas('category', function ($q) {
$q->where('is_active', true);
})
->orderBy('sort_order')
->orderBy('id')
->get()
->map(function ($template) {
return $this->formatTemplate($template);
});
return $this->success($templates);
} catch (\Exception $e) {
return $this->fail([500, '获取模板列表失败:' . $e->getMessage()]);
}
}
返回数据格式 (formatTemplate 方法,第 307-330行):
private function formatTemplate($template, $includeRelations = false)
{
$data = [
'id' => $template->id,
'category_id' => $template->category_id,
'name' => $template->name,
'config' => $template->config,
'sort_order' => $template->sort_order,
'description' => $template->description,
'created_at' => $template->created_at,
'updated_at' => $template->updated_at,
];
if ($includeRelations && $template->relationLoaded('category') && $template->category) {
$data['category'] = [
'id' => $template->category->id,
'name' => $template->category->name,
'is_leaf' => $template->category->is_leaf,
'is_active' => $template->category->is_active,
];
}
return $data;
}
配置规范化
在加载配置后,会调用 normalizeFieldConfig 函数(第 344-377行)确保所有字段都有 visible 和 required 属性:
const normalizeFieldConfig = (config) => {
Object.keys(config).forEach(sectionKey => {
const section = config[sectionKey]
// 处理普通区域的 fields
if (section.fields && Array.isArray(section.fields)) {
section.fields.forEach(field => {
if (field.visible === undefined) {
field.visible = true
}
if (field.required === undefined) {
field.required = false
}
})
}
// 处理 payment 区域的 rounds
if (section.rounds && Array.isArray(section.rounds)) {
section.rounds.forEach(round => {
if (round.fields && Array.isArray(round.fields)) {
round.fields.forEach(field => {
if (field.visible === undefined) {
field.visible = true
}
if (field.required === undefined) {
field.required = false
}
})
}
})
}
})
}
当前配置状态
配置数据存储在组件的响应式变量 currentConfig 中(第 334行):
const currentConfig = ref(JSON.parse(JSON.stringify(defaultSectionConfig)))
可视化预览区域通过 v-for 指令遍历 currentConfig 的各个区域来渲染字段:
- 基本信息区域:
currentConfig.basic_info?.fields - 上会信息区域:
currentConfig.meeting_info?.fields - 事前流程区域:
currentConfig.pre_approval?.fields - 支付流程区域:
currentConfig.payment?.rounds
数据保存
当用户修改配置并点击"保存"按钮时,会调用 saveConfig 函数(第 608-641行),将配置保存到数据库:
const templateData = {
category_id: categoryId.value,
name: currentCategoryName.value || '默认模板',
config: currentConfig.value
}
// 如果已有模板ID,则更新;否则创建
if (currentTemplateId.value) {
result = await plannedExpenditureTemplateAPI.update(currentTemplateId.value, templateData)
} else {
result = await plannedExpenditureTemplateAPI.findOrCreate(templateData)
}
总结
可视化预览区域的默认配置数据来源优先级:
-
数据库模板配置 (
budget_planned_expenditure_templates.config)- 通过API
GET /budget/planned-expenditure-templates/by-category/{categoryId}获取 - 每个分类只有一个模板
- 配置以JSON格式存储在数据库的
config字段中
- 通过API
-
前端硬编码默认配置 (
defaultSectionConfig)- 定义在
CanvasSettings.vue组件中 - 当数据库中没有模板时使用
- 包含四个区域的完整字段定义
- 定义在
-
配置规范化处理
- 确保所有字段都有
visible和required属性 - 处理普通区域和支付流程区域的不同数据结构
- 确保所有字段都有
-
实时更新
- 配置修改后通过
currentConfig响应式变量实时反映在预览区域 - 保存后更新数据库,下次加载时优先使用数据库配置
- 配置修改后通过