You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cz-hjjc-budget/docs/planned-expenditure-templat...

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行,包含以下四个区域:

  1. 区域a基本信息 (basic_info)
  2. 区域b资金申请上会 (meeting_info)
  3. 区域c事前流程 (pre_approval)
  4. 区域d支付流程 (payment)

默认配置数据来源

1. 前端硬编码默认配置

位置: czemc-budget-execution-frontend/src/views/settings/CanvasSettings.vue286-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',
    ];

数据加载流程

初始化流程

  1. 组件挂载 (onMounted 钩子,第 730-741行)

    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行)

    关键逻辑:

    // 加载该分类的模板(每个分类只有一个模板)
    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.js162-164行

getByCategory: (categoryId) => {
  return request.get(`/budget/planned-expenditure-templates/by-category/${categoryId}`)
}

后端路由: backend/Modules/Budget/routes/api.php98行

Route::get('planned-expenditure-templates/by-category/{categoryId}', 
    [PlannedExpenditureTemplateController::class, 'getByCategory']);

后端控制器: backend/Modules/Budget/app/Http/Controllers/Api/PlannedExpenditureTemplateController.php222-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行)确保所有字段都有 visiblerequired 属性:

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)
}

总结

可视化预览区域的默认配置数据来源优先级:

  1. 数据库模板配置 (budget_planned_expenditure_templates.config)

    • 通过API GET /budget/planned-expenditure-templates/by-category/{categoryId} 获取
    • 每个分类只有一个模板
    • 配置以JSON格式存储在数据库的 config 字段中
  2. 前端硬编码默认配置 (defaultSectionConfig)

    • 定义在 CanvasSettings.vue 组件中
    • 当数据库中没有模板时使用
    • 包含四个区域的完整字段定义
  3. 配置规范化处理

    • 确保所有字段都有 visiblerequired 属性
    • 处理普通区域和支付流程区域的不同数据结构
  4. 实时更新

    • 配置修改后通过 currentConfig 响应式变量实时反映在预览区域
    • 保存后更新数据库,下次加载时优先使用数据库配置