|
|
# 计划支出模板设置页面 - 可视化预览区域默认配置数据来源
|
|
|
|
|
|
## 页面路径
|
|
|
`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行**,包含以下四个区域:
|
|
|
|
|
|
1. **区域a:基本信息** (`basic_info`)
|
|
|
2. **区域b:资金申请上会** (`meeting_info`)
|
|
|
3. **区域c:事前流程** (`pre_approval`)
|
|
|
4. **区域d:支付流程** (`payment`)
|
|
|
|
|
|
## 默认配置数据来源
|
|
|
|
|
|
### 1. 前端硬编码默认配置
|
|
|
|
|
|
**位置**: `czemc-budget-execution-frontend/src/views/settings/CanvasSettings.vue` 第 **286-328行**
|
|
|
|
|
|
```286:328:czemc-budget-execution-frontend/src/views/settings/CanvasSettings.vue
|
|
|
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`
|
|
|
|
|
|
```25:31:backend/Modules/Budget/app/Models/PlannedExpenditureTemplate.php
|
|
|
protected $casts = [
|
|
|
'category_id' => 'integer',
|
|
|
'config' => 'array', // JSON自动转换为数组
|
|
|
'sort_order' => 'integer',
|
|
|
'created_by' => 'integer',
|
|
|
'updated_by' => 'integer',
|
|
|
];
|
|
|
```
|
|
|
|
|
|
## 数据加载流程
|
|
|
|
|
|
### 初始化流程
|
|
|
|
|
|
1. **组件挂载** (`onMounted` 钩子,第 **730-741行**)
|
|
|
```javascript
|
|
|
onMounted(async () => {
|
|
|
// 先加载分类树以构建映射表
|
|
|
await loadCategoryTree()
|
|
|
|
|
|
// 从URL参数获取categoryId
|
|
|
const categoryIdFromQuery = route.query.categoryId
|
|
|
if (categoryIdFromQuery) {
|
|
|
await loadCategoryAndTemplate(categoryIdFromQuery)
|
|
|
}
|
|
|
})
|
|
|
```
|
|
|
|
|
|
2. **加载分类树** (`loadCategoryTree` 函数,第 **397-407行**)
|
|
|
- API调用: `plannedExpenditureCategoryAPI.getCategoryTree()`
|
|
|
- 后端路由: `GET /budget/planned-expenditure-categories-tree`
|
|
|
- 用途: 构建分类映射表 (`categoryMap`),用于快速查找分类名称
|
|
|
|
|
|
3. **加载分类和模板** (`loadCategoryAndTemplate` 函数,第 **409-472行**)
|
|
|
|
|
|
**关键逻辑**:
|
|
|
```javascript
|
|
|
// 加载该分类的模板(每个分类只有一个模板)
|
|
|
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
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### 数据优先级
|
|
|
|
|
|
1. **数据库模板配置** (优先级最高)
|
|
|
- 如果该分类在数据库中存在模板记录,使用 `template.config`
|
|
|
- 如果 `template.config` 为空,回退到默认配置
|
|
|
|
|
|
2. **前端默认配置** (兜底方案)
|
|
|
- 当数据库中没有模板记录时使用
|
|
|
- 当API调用失败时使用
|
|
|
- 当模板配置为空时使用
|
|
|
|
|
|
## API接口
|
|
|
|
|
|
### 获取分类模板
|
|
|
|
|
|
**前端API定义**: `czemc-budget-execution-frontend/src/utils/api.js` 第 **162-164行**
|
|
|
|
|
|
```javascript
|
|
|
getByCategory: (categoryId) => {
|
|
|
return request.get(`/budget/planned-expenditure-templates/by-category/${categoryId}`)
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**后端路由**: `backend/Modules/Budget/routes/api.php` 第 **98行**
|
|
|
|
|
|
```php
|
|
|
Route::get('planned-expenditure-templates/by-category/{categoryId}',
|
|
|
[PlannedExpenditureTemplateController::class, 'getByCategory']);
|
|
|
```
|
|
|
|
|
|
**后端控制器**: `backend/Modules/Budget/app/Http/Controllers/Api/PlannedExpenditureTemplateController.php` 第 **222-241行**
|
|
|
|
|
|
```222:241:backend/Modules/Budget/app/Http/Controllers/Api/PlannedExpenditureTemplateController.php
|
|
|
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行**):
|
|
|
|
|
|
```307:330:backend/Modules/Budget/app/Http/Controllers/Api/PlannedExpenditureTemplateController.php
|
|
|
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` 属性:
|
|
|
|
|
|
```344:377:czemc-budget-execution-frontend/src/views/settings/CanvasSettings.vue
|
|
|
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行**):
|
|
|
|
|
|
```javascript
|
|
|
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行**),将配置保存到数据库:
|
|
|
|
|
|
```javascript
|
|
|
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)
|
|
|
}
|
|
|
```
|
|
|
|
|
|
## 总结
|
|
|
|
|
|
可视化预览区域的默认配置数据来源优先级:
|
|
|
|
|
|
1. **数据库模板配置** (`budget_planned_expenditure_templates.config`)
|
|
|
- 通过API `GET /budget/planned-expenditure-templates/by-category/{categoryId}` 获取
|
|
|
- 每个分类只有一个模板
|
|
|
- 配置以JSON格式存储在数据库的 `config` 字段中
|
|
|
|
|
|
2. **前端硬编码默认配置** (`defaultSectionConfig`)
|
|
|
- 定义在 `CanvasSettings.vue` 组件中
|
|
|
- 当数据库中没有模板时使用
|
|
|
- 包含四个区域的完整字段定义
|
|
|
|
|
|
3. **配置规范化处理**
|
|
|
- 确保所有字段都有 `visible` 和 `required` 属性
|
|
|
- 处理普通区域和支付流程区域的不同数据结构
|
|
|
|
|
|
4. **实时更新**
|
|
|
- 配置修改后通过 `currentConfig` 响应式变量实时反映在预览区域
|
|
|
- 保存后更新数据库,下次加载时优先使用数据库配置
|
|
|
|