13 KiB
付款详情打印页「相关流程详情」渲染说明
页面: http://localhost:3000/#/payment/payment-detail-print/:id(如 /payment-detail-print/167)
组件: czemc-budget-execution-frontend/src/views/payment/PaymentDetailPrint.vue
一、渲染位置与条件
「相关流程详情」是一个独立区块,位于付款基本信息、关联内容(合同/事前流程等)之后。
<!-- 相关流程打印模版 -->
<div v-if="renderedPrintTemplates.length > 0" class="flow-print-templates flow-print-templates--page-break">
<h2 class="section-title">相关流程详情</h2>
<div
v-for="(template, idx) in renderedPrintTemplates"
:key="`flow_template_${template.flow_id}_${idx}`"
class="flow-print-template-item"
:class="{ 'flow-print-template-item--page-break': idx > 0 }"
>
<h3 class="flow-template-title">
{{ idx + 1 }}、{{ template.flow_info?.no || `流程ID: ${template.flow_id}` }} - {{ template.flow_info?.title || template.flow_title || '-' }}({{ template.flow_info?.creator_name || '-' }} {{ formatFlowCreatedAt(template.flow_info?.created_at) }})
</h3>
<div class="flow-template-content" v-html="template.html"></div>
</div>
</div>
- 显示条件:
renderedPrintTemplates.length > 0 - 标题:
<h2 class="section-title">相关流程详情</h2> - 列表: 遍历
renderedPrintTemplates,每条流程一个.flow-print-template-item
二、单条流程的渲染内容
每条流程渲染两块:
| 部分 | 说明 |
|---|---|
标题行 <h3 class="flow-template-title"> |
序号、流程编号 - 流程标题(创建人 创建时间)。flow_info 缺省时用 flow_id / flow_title 兜底,创建时间由 formatFlowCreatedAt 格式化为 YYYY-MM-DD HH:mm。 |
正文 <div class="flow-template-content" v-html="template.html"> |
后端返回的 HTML 字符串,直接 v-html 插入。来自 OA 流程的打印模版(CustomModel.print_format)渲染结果。 |
每条模板项的数据结构(后端 renderPrintTemplates 返回)大致为:
{
flow_id: number,
flow_title?: string,
flow_info?: {
no: string,
title: string,
creator_name: string | null,
created_at: string | null
},
html: string, // 打印模版渲染出的 HTML
error?: string // 出错时才有
}
三、流程 ID 从哪里来(数据供给)
要渲染「相关流程详情」,必须先收集流程 ID,再调接口拿到 renderedPrintTemplates。
3.1 收集流程 ID
- 存储:
collectedFlowIds(ref<Set<number>>),在loadPaymentDetail时会clear()后重新收集。 - 收集函数:
collectFlowId(flowId):flowId为 number 时collectedFlowIds.value.add(flowId)。
收集来源:
-
付款主流程
- 在
loadPaymentDetail里:若payment?.flow_info?.id存在,则collectFlowId(payment.flow_info.id)。
- 在
-
关联内容里的流程
以下组件在渲染流程时,会emit('collect-flow-id', flowId),页面层用@collect-flow-id="collectFlowId"接收:ContractInfoCard(相关合同信息)- 当
embedPlannedExpenditures === true时,渲染「关联合同的事前流程」,用PlannedExpenditureTemplateReadonly展示各事前流程;该组件内部会为每个流程 emitcollect-flow-id。 - 出现场景:
relatedTypeCase === 'null'且payment?.contract_id;或relatedTypeCase === 'contract';或relatedTypeCase === 'planned_expenditure'时合同区块(embed-planned-expenditures="false"不嵌入事前流程,但合同区块仍挂载)。
- 当
PlannedExpenditureInfoCard(相关事前流程)- 当
relatedTypeCase === 'planned_expenditure'时渲染,内部同样是PlannedExpenditureTemplateReadonly,对每个事前流程 emitcollect-flow-id。
- 当
-
PlannedExpenditureTemplateReadonly如何产生 flowId- 根据模板配置中的
oa_custom_model字段,从flow_bindings和element_values里解析出流程 ID。 - 在
flowItems计算属性中,每得到一个id就会emit('collect-flow-id', id)。 - 见
PlannedExpenditureTemplateReadonly.vue约 1270–1296 行(遍历 config 收集)、605–626 行(构造 flowItems 时 emit)。
- 根据模板配置中的
因此,流程 ID = 付款主流程 + 合同相关事前流程(若嵌入)+ 当前付款关联的事前流程,经 collectFlowId 汇总到 collectedFlowIds。
3.2 调用接口拿到「相关流程详情」HTML
- 时机:
loadPaymentDetail在拉完付款、关联事前流程、合同等并nextTick后,setTimeout(..., 300)调用renderPrintTemplates()。- 另外有
watch监听collectedFlowIds.value.size,变化时也会触发renderPrintTemplates(防抖 500ms)。
- 请求:
oaFlowAPI.renderPrintTemplates(flowIds)flowIds = Array.from(collectedFlowIds.value),即当前收集到的全部流程 ID。
- 接口:
POST /oa/flow/render-print-templates,body:{ flow_ids: number[] }- 实现:
backend/Modules/Oa/app/Http/Controllers/FlowController.php的renderPrintTemplates。
3.3 后端 renderPrintTemplates 做了什么
对每个 flow_id:
- 用
Flow::with(['customModel', 'creator', 'creatorDepartment', 'logs'…])查流程,取打印用customModel、发起人、部门、审批日志等。 $flow->withData()加载流程业务数据。- 若流程不存在 / 无 data / 未配置打印模版,则 push 一条带
error的占位项(如「流程不存在」「未配置打印模版」),并有简单flow_info和html。 - 若正常:读取
customModel.print_format,用createFlowForPrint构建打印用 flow 对象,再renderPrintTemplate渲染出 HTML;对 HTML 做removeScriptAndStyleTags等清理。 - 返回项包含
flow_id、flow_title、flow_info(no、title、creator_name、created_at)、html。
前端把接口返回的数组赋给 renderedPrintTemplates,模板里据此渲染「相关流程详情」的标题列表 + 每条 v-html 的正文。
四、打印与分页样式
- 区块整体:
.flow-print-templates--page-break使用break-before: page/page-break-before: always,从「相关流程详情」整块起新的一页。
- 每条流程:
- 第一条(
idx === 0)无额外分页。 - 从第二条开始(
idx > 0)加上flow-print-template-item--page-break,同样是break-before: page,每条流程新起一页。
- 第一条(
详见 PaymentDetailPrint.vue 约 1579–1589 行。
五、流程简要串联
loadPaymentDetail
→ paymentAPI.getDetail(id)
→ collectFlowId(payment.flow_info.id) // 付款主流程
→ loadPaymentTemplateElements / loadApprovalFlowDetails
→ loadRelatedPlannedExpenditure
→ nextTick + setTimeout(300) → renderPrintTemplates()
子组件挂载并渲染:
ContractInfoCard / PlannedExpenditureInfoCard
→ PlannedExpenditureTemplateReadonly
→ 从 flow_bindings / element_values 解析流程 ID
→ emit('collect-flow-id', id) → collectFlowId(id)
renderPrintTemplates:
flowIds = Array.from(collectedFlowIds)
→ oaFlowAPI.renderPrintTemplates(flowIds)
→ POST /oa/flow/render-print-templates { flow_ids }
→ 后端 per flow_id 渲染 print_format → HTML
→ renderedPrintTemplates = res.data
模板:
v-if="renderedPrintTemplates.length > 0"
→ 展示「相关流程详情」标题
→ v-for template in renderedPrintTemplates
→ h3: 序号、编号、标题、创建人、时间
→ div v-html="template.html"
六、相关文件
| 用途 | 路径 |
|---|---|
| 页面与「相关流程详情」模板 | czemc-budget-execution-frontend/src/views/payment/PaymentDetailPrint.vue |
| 事前流程只读展示 + collect-flow-id | czemc-budget-execution-frontend/src/components/PlannedExpenditureTemplateReadonly.vue |
| 合同信息卡片 | czemc-budget-execution-frontend/src/components/payment-print/ContractInfoCard.vue |
| 事前流程信息卡片 | czemc-budget-execution-frontend/src/components/payment-print/PlannedExpenditureInfoCard.vue |
| 渲染打印模版 API | oaFlowAPI.renderPrintTemplates → POST /oa/flow/render-print-templates |
| 后端实现 | backend/Modules/Oa/app/Http/Controllers/FlowController.php :: renderPrintTemplates |
七、choice 类型字段在「相关流程详情」中的处理
结合 OA 自定义模型里的 choice 类型(含「人员选择」「多选」),说明每个流程的正文 HTML 里,choice 字段如何被渲染。
7.1 流程正文 HTML 的来源
- 每条流程的
template.html来自后端 打印模版(CustomModel.print_format)。 - 模版里使用
<field name="xxx">...</field>占位;后端在renderPrintTemplate里先跑 Blade,再调renderFieldTags,把每个<field name="...">替换成对应字段的渲染结果。 - 字段渲染统一走
renderFieldValueByOaRules(FlowController.php),根据oa_custom_model_fields的type、selection_model等规则输出 HTML 或纯文本。
因此,choice 类型在「相关流程详情」中的表现,完全由后端 renderFieldValueByOaRules 的 choice 分支决定;前端只做 v-html 展示,不再解析字段类型。
7.2 存值从哪里取
renderFieldValueByOaRules里:$rawValue = $flow->data->{$field->name}。$flow->data即该流程的业务数据(OA 的oa_cmt_{custom_model_id}等表的一条记录)。- 亦即:choice 的存值 = 流程业务表里该字段名对应的列。
- 与 OA 设计一致:人员选择 多为
id1|id2|...(竖线分隔),多选 也可能为|分隔或数组,依写入方而定。
7.3 choice 的渲染逻辑(后端)
当 $field->type === 'choice' 且 $field->selection_model 存在时(FlowController.php 约 5422–5445 行):
-
解析多选 ID
- 若
$rawValue为字符串:preg_split('/[|,]/', $rawValue),即按|或,拆成多个 id,再trim、array_filter去空。 - 若为数组:
$ids = $rawValue,直接当作 id 数组。
- 若
-
按 id 查展示名
$cls = $field->selection_model(如App\Models\User)。- 对每个
$id:$m = (new $cls())->withTrashed()->find($id);若有记录,取$m->name ?? $m->title ?? $id,否则用$id。 - 人员选择时通常为 User,展示的就是姓名(
name)或title。
-
拼成 HTML
- 将上述展示名
$escapeText(...)后放进$itemsHtml,最后return implode('<br/>', $itemsHtml)。 - 即:多个选项竖排,用
<br/>换行;若仅一个,就一行。
- 将上述展示名
-
异常与兜底
- 上述过程包在
try/catch里,出错则退回$nl2brSafe($rawValue),直接展示原始存值。
- 上述过程包在
与 select 的区分:select 单选,用 getSelections() 等做 id→label 映射;choice 按多选处理,支持 |、, 分隔字符串与数组,逐 id 查模型再拼接。
7.4 小结(choice 在「相关流程详情」里)
| 项目 | 说明 |
|---|---|
| 发生位置 | 打印模版 print_format 中的 <field name="...">,对应 OA 自定义字段 type = choice(人员选择 / 多选)。 |
| 数据来源 | $flow->data->{字段名},即流程业务表里该字段的存值。 |
| 存值格式 | 多为 `id1 |
| 展示逻辑 | 按 selection_model 实例化模型,对每个 id find 取 name ?? title ?? id,再 implode('<br/>', ...) 输出;多选用换行展示。字符串按 | 或 , 拆成 id 数组。 |
| 实现位置 | FlowController::renderFieldValueByOaRules,约 5422–5445 行。 |
因此,在「相关流程详情」的每个流程里,choice 类型字段 = 多选 id → 按 selection_model 解析成姓名/名称 → 用 <br/> 竖排展示;与《choice类型字段说明与数据供给》中 OA 的 choice/人员选择/多选语义一致,且后端已完整支持,无需前端再参与。
7.5 子表单中的 choice
- 打印模版若包含 relation 子表单,子表会渲染成
<table>;每个单元格同样经renderFieldValueByOaRules生成(传入子表CustomModel与子表字段$sf)。 - 若子表字段为 choice,走的仍是同一套 choice 分支:
$rawValue来源于当前上下文的$flow->data;子表场景下通常需从子表行读取对应列,若后端传入的$flow->data已包含子表行维度,则逻辑一致;否则可能需在实现上区分主表 / 子表取值来源。