diff --git a/docs/choice类型字段说明与数据供给.md b/docs/choice类型字段说明与数据供给.md new file mode 100644 index 0000000..86f8dc9 --- /dev/null +++ b/docs/choice类型字段说明与数据供给.md @@ -0,0 +1,84 @@ +# OA「choice」类型字段说明与数据供给 + +本文先说明 OA 模块里 **choice** 相关字段类型的含义(用普通人能懂的话),再说明在「选择事前流程」弹窗里,这类字段的数据从哪里来、如何展示。 + +--- + +## 一、OA 里「choice」到底是什么意思? + +在 OA 的**流程表单设计**里,配置字段时有两种和「选择」有关的类型,最终在库里都存成 `type = choice`,靠另一个字段 `multiple` 区分: + +| 配置时选的类型 | 界面上叫法 | 实际含义(人话) | 存库后 | +|----------------|------------|------------------|--------| +| **choice** | **人员选择** | 从系统用户名单里**选人**,可多选。例如「经办人」「参与人」。选项固定为「用户」,不能改成别的。 | `type=choice`,`multiple=10`,`selection_model=App\Models\User` | +| **choices** | **多选** | 从某一类**数据**里勾选多项。例如部门、项目、预算来源等。具体选项来自「下拉数据」配置的模型,或「数据选项」里手写的列表。 | 保存时被改成 `type=choice`,`multiple=1`,`selection_model` 按配置来 | + +所以: + +- **「人员选择」**:专门用来选人的,只能选用户,可多选。 +- **「多选」**:通用多选,选什么是配置决定的(用户、部门、项目等),可多选。 + +两种在系统里都叫 **choice 类型**,只是「人员选择」和「多选」的配置不同。 + +--- + +## 二、选项从哪里来?(数据供给,人话版) + +**无论是「人员选择」还是「多选」,选项列表都是同一条链路:** + +1. 配置里有一个 **「下拉数据」**(即 `selection_model`),表示:从哪个**数据表/模型**拉选项。 + - 人员选择:固定为用户表(`App\Models\User`)。 + - 多选:在表单设计里选,比如用户、部门、项目等。 +2. 前端调用接口:**根据「下拉数据」去要选项列表**。 + 接口:`GET /oa/flow/selection-options?selection_model=xxx`, + 后端按这个模型查数据,返回 `[{id, value, label, name}, ...]`。 +3. 用户填表时**选中的是 id**;存库时,多选一般存成 **一串 id**: + - 有的地方用 `|` 分隔,如 `123|456|789`; + - 有的地方用 `,` 分隔,如 `123,456,789`; + - 也可能是数组 `[123, 456, 789]`。 + +**总结**: +选项 = 按「下拉数据」从系统里查出来的列表; +用户选的是 id;存的是多个 id 拼成的一串或数组。展示时,再用 id 去选项列表里找对应的「名称」来显示。 + +--- + +## 三、在「选择事前流程」弹窗里,choice 列怎么回事? + +弹窗里流程列表的**列**,有一部分来自 OA 流程模型的「列表展示字段」。如果某列是 **choice 类型**(即「人员选择」或「多选」),逻辑应该是: + +- **选项**:和上面一样,按 `selection_model` 调 `getSelectionOptions` 拿选项列表。 +- **展示**:每个格子里存的是 id(或 `id1|id2`、`id1,id2`),应当查选项列表转成「张三、李四」这类名称再显示。 + +但当前 **PreApprovalFlowPicker** 组件里: + +- 加载选项时,只处理了 `select`、`radio`,**没有单独处理 `choice`**。 + 所以,如果某一列**仅是 choice**(没有同列的 select/radio 共用同一个 selection_model),这根列**不会去拉选项**。 +- 格式化展示时,也只处理了 `select`、`radio`,**没有 `choice` 分支**。 + 结果就是:**直接显示原始存值**(例如 `123|456` 或 `123,456`),不会变成「张三、李四」。 + +因此: +**从设计上**,choice 的选项和数据供给与 select/radio 一样,都是 `selection_model` → 选项接口; +**从实现上**,弹窗里的 choice 列目前没有按「多选 + 选项映射」完整做,所以会看到一串 id 而不是人名或名称。 + +--- + +## 四、若要让 choice 列正常显示「名称」 + +需要至少做两件事: + +1. **拉选项**:在加载列表列配置时,把 `choice` 也当作「有 `selection_model` 就要拉选项」的类型,和 select/radio 一起调 `getSelectionOptions`。 +2. **展示时按多选解析**: + - 把单元格里的值解析成多个 id(支持 `|`、`,`、数组等); + - 用选项列表把每个 id 转成对应的 label/name; + - 用顿号或逗号拼接后显示,例如:「张三、李四、王五」。 + +这样,**choice 类型在「选择事前流程」弹窗里的数据供给和展示**就与 OA 的设计一致了,普通用户看到的是可读的「名称」而不是 id 串。 + +--- + +## 五、相关代码与接口(给开发看) + +- OA 字段配置与保存:`backend/Modules/Oa/resources/views/admin/custom-model/set-fields.blade.php`(`choice` / `choices` 的隐藏、保存逻辑)。 +- 选项接口:`GET /oa/flow/selection-options`,`FlowController::getSelectionOptions`;前端 `oaFlowAPI.getSelectionOptions(selection_model)`。 +- 弹窗列表与格式化:`PreApprovalFlowPicker.vue` 的 `loadSubFormFieldsAndOptions`、`formatFieldValue`;可选参考 `ProcessQuery.vue` 里对 choice/choices 的 `parseChoiceValueToIds` 及多选展示逻辑。 diff --git a/docs/付款详情打印页-相关流程详情渲染说明.md b/docs/付款详情打印页-相关流程详情渲染说明.md new file mode 100644 index 0000000..22f7ada --- /dev/null +++ b/docs/付款详情打印页-相关流程详情渲染说明.md @@ -0,0 +1,233 @@ +# 付款详情打印页「相关流程详情」渲染说明 + +**页面**: `http://localhost:3000/#/payment/payment-detail-print/:id`(如 `/payment-detail-print/167`) +**组件**: `czemc-budget-execution-frontend/src/views/payment/PaymentDetailPrint.vue` + +--- + +## 一、渲染位置与条件 + +「相关流程详情」是一个**独立区块**,位于付款基本信息、关联内容(合同/事前流程等)**之后**。 + +```248:262:czemc-budget-execution-frontend/src/views/payment/PaymentDetailPrint.vue + +
+

相关流程详情

+
+

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

+
+
+
+``` + +- **显示条件**: `renderedPrintTemplates.length > 0` +- **标题**: `

相关流程详情

` +- **列表**: 遍历 `renderedPrintTemplates`,每条流程一个 `.flow-print-template-item` + +--- + +## 二、单条流程的渲染内容 + +每条流程渲染两块: + +| 部分 | 说明 | +|------|------| +| **标题行** `

` | `序号、流程编号 - 流程标题(创建人 创建时间)`。`flow_info` 缺省时用 `flow_id` / `flow_title` 兜底,创建时间由 `formatFlowCreatedAt` 格式化为 `YYYY-MM-DD HH:mm`。 | +| **正文** `
` | 后端返回的 **HTML 字符串**,直接 `v-html` 插入。来自 OA 流程的**打印模版**(`CustomModel.print_format`)渲染结果。 | + +每条模板项的数据结构(后端 `renderPrintTemplates` 返回)大致为: + +```ts +{ + 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>`),在 `loadPaymentDetail` 时会 `clear()` 后重新收集。 +- **收集函数**: `collectFlowId(flowId)`:`flowId` 为 number 时 `collectedFlowIds.value.add(flowId)`。 + +**收集来源**: + +1. **付款主流程** + - 在 `loadPaymentDetail` 里:若 `payment?.flow_info?.id` 存在,则 `collectFlowId(payment.flow_info.id)`。 + +2. **关联内容里的流程** + 以下组件在渲染流程时,会 `emit('collect-flow-id', flowId)`,页面层用 `@collect-flow-id="collectFlowId"` 接收: + - **`ContractInfoCard`**(相关合同信息) + - 当 `embedPlannedExpenditures === true` 时,渲染「关联合同的事前流程」,用 `PlannedExpenditureTemplateReadonly` 展示各事前流程;该组件内部会为每个流程 emit `collect-flow-id`。 + - 出现场景:`relatedTypeCase === 'null'` 且 `payment?.contract_id`;或 `relatedTypeCase === 'contract'`;或 `relatedTypeCase === 'planned_expenditure'` 时合同区块(`embed-planned-expenditures="false"` 不嵌入事前流程,但合同区块仍挂载)。 + - **`PlannedExpenditureInfoCard`**(相关事前流程) + - 当 `relatedTypeCase === 'planned_expenditure'` 时渲染,内部同样是 `PlannedExpenditureTemplateReadonly`,对每个事前流程 emit `collect-flow-id`。 + +3. **`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`: + +1. 用 `Flow::with(['customModel', 'creator', 'creatorDepartment', 'logs'…])` 查流程,取打印用 `customModel`、发起人、部门、审批日志等。 +2. `$flow->withData()` 加载流程业务数据。 +3. 若流程不存在 / 无 data / 未配置打印模版,则 push 一条带 `error` 的占位项(如「流程不存在」「未配置打印模版」),并有简单 `flow_info` 和 `html`。 +4. 若正常:读取 `customModel.print_format`,用 `createFlowForPrint` 构建打印用 flow 对象,再 `renderPrintTemplate` 渲染出 HTML;对 HTML 做 `removeScriptAndStyleTags` 等清理。 +5. 返回项包含 `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`)。 +- 模版里使用 **`...`** 占位;后端在 `renderPrintTemplate` 里先跑 Blade,再调 **`renderFieldTags`**,把每个 `` 替换成对应字段的渲染结果。 +- 字段渲染统一走 **`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 行): + +1. **解析多选 ID** + - 若 `$rawValue` 为**字符串**:`preg_split('/[|,]/', $rawValue)`,即按 **`|` 或 `,`** 拆成多个 id,再 `trim`、`array_filter` 去空。 + - 若为**数组**:`$ids = $rawValue`,直接当作 id 数组。 + +2. **按 id 查展示名** + - `$cls = $field->selection_model`(如 `App\Models\User`)。 + - 对每个 `$id`:`$m = (new $cls())->withTrashed()->find($id)`;若有记录,取 **`$m->name ?? $m->title ?? $id`**,否则用 `$id`。 + - 人员选择时通常为 User,展示的就是**姓名**(`name`)或 `title`。 + +3. **拼成 HTML** + - 将上述展示名 `$escapeText(...)` 后放进 `$itemsHtml`,最后 **`return implode('
', $itemsHtml)`**。 + - 即:**多个选项竖排,用 `
` 换行**;若仅一个,就一行。 + +4. **异常与兜底** + - 上述过程包在 `try/catch` 里,出错则退回 `$nl2brSafe($rawValue)`,直接展示原始存值。 + +**与 select 的区分**:`select` 单选,用 `getSelections()` 等做 id→label 映射;**choice 按多选处理**,支持 **`|`、`,` 分隔字符串**与数组,逐 id 查模型再拼接。 + +### 7.4 小结(choice 在「相关流程详情」里) + +| 项目 | 说明 | +|------|------| +| **发生位置** | 打印模版 `print_format` 中的 ``,对应 OA 自定义字段 type = `choice`(人员选择 / 多选)。 | +| **数据来源** | `$flow->data->{字段名}`,即流程业务表里该字段的存值。 | +| **存值格式** | 多为 `id1|id2|...` 或 `id1,id2,...` 或 id 数组;后端同时支持 **`|`、`,`** 分隔。 | +| **展示逻辑** | 按 `selection_model` 实例化模型,对每个 id `find` 取 `name ?? title ?? id`,再 `implode('
', ...)` 输出;多选用换行展示。字符串按 `\|` 或 `,` 拆成 id 数组。 | +| **实现位置** | `FlowController::renderFieldValueByOaRules`,约 5422–5445 行。 | + +因此,在「相关流程详情」的每个流程里,**choice 类型字段 = 多选 id → 按 selection_model 解析成姓名/名称 → 用 `
` 竖排展示**;与《choice类型字段说明与数据供给》中 OA 的 choice/人员选择/多选语义一致,且**后端已完整支持**,无需前端再参与。 + +### 7.5 子表单中的 choice + +- 打印模版若包含 **relation 子表单**,子表会渲染成 ``;每个单元格同样经 **`renderFieldValueByOaRules`** 生成(传入子表 `CustomModel` 与子表字段 `$sf`)。 +- 若子表字段为 **choice**,走的仍是同一套 choice 分支:`$rawValue` 来源于当前上下文的 `$flow->data`;子表场景下通常需从子表行读取对应列,若后端传入的 `$flow->data` 已包含子表行维度,则逻辑一致;否则可能需在实现上区分主表 / 子表取值来源。 diff --git a/src/components/PreApprovalFlowPicker.vue b/src/components/PreApprovalFlowPicker.vue index a769e0e..ad6f06d 100644 --- a/src/components/PreApprovalFlowPicker.vue +++ b/src/components/PreApprovalFlowPicker.vue @@ -460,6 +460,27 @@ const formatDate = (dateString) => { } } +// 将 choice/choices 的值(数组、| 或 , 分隔字符串、单值)解析成 ID 数组 +const parseChoiceValueToIds = (value) => { + if (value === null || value === undefined || value === '') return [] + if (Array.isArray(value)) { + return value + .map(v => (v === null || v === undefined ? '' : String(v).trim())) + .filter(v => v.length > 0) + } + if (typeof value === 'object') { + const id = value.id ?? value.value + if (id === null || id === undefined || id === '') return [] + return [String(id).trim()].filter(v => v.length > 0) + } + const s = String(value).trim() + if (!s) return [] + return s + .split(/[|,]/) + .map(v => v.trim()) + .filter(v => v.length > 0) +} + // 格式化字段值 const formatFieldValue = (value, col, selectionOptionsMap = {}) => { if (value === null || value === undefined || value === '') { @@ -481,36 +502,34 @@ const formatFieldValue = (value, col, selectionOptionsMap = {}) => { // 如果有 selection_model,从 selectionOptionsMap 中查找 if (selectionModel && selectionOptionsMap[selectionModel]) { const options = selectionOptionsMap[selectionModel] - - // 处理多值情况(逗号分隔) - const valueStr = String(value) - if (valueStr.includes(',')) { - // 分割多个值 - const values = valueStr.split(',').map(v => v.trim()).filter(v => v) - const labels = values.map(val => { - const option = options.find(opt => - opt.id == val || - opt.value == val || - String(opt.id) === String(val) || - String(opt.value) === String(val) - ) - return option ? (option.label || option.name || val) : val - }) - return labels.length > 0 ? labels.join(', ') : valueStr - } else { - // 单值情况 - const option = options.find(opt => - opt.id == value || - opt.value == value || - String(opt.id) === String(value) || - String(opt.value) === String(value) - ) - if (option) { - return option.label || option.name || value - } + const option = options.find(opt => + opt.id == value || + opt.value == value || + String(opt.id) === String(value) || + String(opt.value) === String(value) + ) + if (option) { + return option.label || option.name || value } } return String(value) + case 'choice': + case 'choices': { + const ids = parseChoiceValueToIds(value) + if (ids.length === 0) return '-' + if (selectionModel && selectionOptionsMap[selectionModel]) { + const options = selectionOptionsMap[selectionModel] + const names = ids.map(id => { + const opt = options.find(o => + o.id == id || o.value == id || + String(o.id) === String(id) || String(o.value) === String(id) + ) + return opt ? (opt.label || opt.name || id) : id + }) + return names.join('、') + } + return ids.join('、') + } default: return String(value) } @@ -633,10 +652,11 @@ const loadSubFormFieldsAndOptions = async () => { return } - // 收集需要加载的 relation 字段和 select 字段 + // 收集需要加载的 relation 字段和 select/choice 字段 const relationFields = listColumns.value.filter(col => col.type === 'relation' && col.sub_custom_model_id) - const selectFields = listColumns.value.filter(col => - (col.type === 'select' || col.type === 'radio') && col.selection_model + const selectFields = listColumns.value.filter(col => + (col.type === 'select' || col.type === 'radio' || col.type === 'choice' || col.type === 'choices') && + col.selection_model ) // 加载 relation 字段的子表单字段定义 @@ -652,9 +672,9 @@ const loadSubFormFieldsAndOptions = async () => { // 按 myindex 排序 subFormFieldsMap.value[fieldName] = subFields.sort((a, b) => (a.myindex || 0) - (b.myindex || 0)) - // 加载子表单中 select 类型字段的选项数据(如果有 selection_model) - const subSelectFields = subFields.filter(field => - (field.type === 'select' || field.type === 'radio') && + // 加载子表单中 select/choice 类型字段的选项数据(如果有 selection_model) + const subSelectFields = subFields.filter(field => + (field.type === 'select' || field.type === 'radio' || field.type === 'choice' || field.type === 'choices') && field.selection_model ) for (const subSelectField of subSelectFields) { diff --git a/src/components/RelationFieldDisplay.vue b/src/components/RelationFieldDisplay.vue index 2da67d8..018437c 100644 --- a/src/components/RelationFieldDisplay.vue +++ b/src/components/RelationFieldDisplay.vue @@ -83,6 +83,27 @@ const relationData = computed(() => { return [] }) +// 将 choice/choices 的值解析成 ID 数组(支持数组、| 或 , 分隔、单值) +const parseChoiceValueToIds = (value) => { + if (value === null || value === undefined || value === '') return [] + if (Array.isArray(value)) { + return value + .map(v => (v === null || v === undefined ? '' : String(v).trim())) + .filter(v => v.length > 0) + } + if (typeof value === 'object') { + const id = value.id ?? value.value + if (id === null || id === undefined || id === '') return [] + return [String(id).trim()].filter(v => v.length > 0) + } + const s = String(value).trim() + if (!s) return [] + return s + .split(/[|,]/) + .map(v => v.trim()) + .filter(v => v.length > 0) +} + // 格式化子表单字段值 const formatSubFieldValue = (value, field) => { if (value === null || value === undefined || value === '') { @@ -101,39 +122,34 @@ const formatSubFieldValue = (value, field) => { return value ? '是' : '否' case 'select': case 'radio': - // 如果有 selection_model,从 selectionOptionsMap 中查找 if (selectionModel && props.selectionOptionsMap[selectionModel]) { const options = props.selectionOptionsMap[selectionModel] - - // 处理多值情况(逗号分隔) - const valueStr = String(value) - if (valueStr.includes(',')) { - // 分割多个值 - const values = valueStr.split(',').map(v => v.trim()).filter(v => v) - const labels = values.map(val => { - const option = options.find(opt => - opt.id == val || - opt.value == val || - String(opt.id) === String(val) || - String(opt.value) === String(val) - ) - return option ? (option.label || option.name || val) : val - }) - return labels.length > 0 ? labels.join(', ') : valueStr - } else { - // 单值情况 - const option = options.find(opt => - opt.id == value || - opt.value == value || - String(opt.id) === String(value) || - String(opt.value) === String(value) - ) - if (option) { - return option.label || option.name || value - } + const option = options.find(opt => + opt.id == value || opt.value == value || + String(opt.id) === String(value) || String(opt.value) === String(value) + ) + if (option) { + return option.label || option.name || value } } return String(value) + case 'choice': + case 'choices': { + const ids = parseChoiceValueToIds(value) + if (ids.length === 0) return '-' + if (selectionModel && props.selectionOptionsMap[selectionModel]) { + const options = props.selectionOptionsMap[selectionModel] + const names = ids.map(id => { + const opt = options.find(o => + o.id == id || o.value == id || + String(o.id) === String(id) || String(o.value) === String(id) + ) + return opt ? (opt.label || opt.name || id) : id + }) + return names.join('、') + } + return ids.join('、') + } default: return String(value) } diff --git a/src/views/payment/IndirectPayment.vue b/src/views/payment/IndirectPayment.vue index 45a4bf7..b6d210e 100644 --- a/src/views/payment/IndirectPayment.vue +++ b/src/views/payment/IndirectPayment.vue @@ -2871,33 +2871,14 @@ const formatRelationSubFieldValue = (value, field) => { case 'radio': if (selectionModel && relationSelectionOptionsMap.value[selectionModel]) { const options = relationSelectionOptionsMap.value[selectionModel] - - // 处理多值情况(逗号分隔) - const valueStr = String(value) - if (valueStr.includes(',')) { - // 分割多个值 - const values = valueStr.split(',').map(v => v.trim()).filter(v => v) - const labels = values.map(val => { - const option = options.find(opt => - opt.id == val || - opt.value == val || - String(opt.id) === String(val) || - String(opt.value) === String(val) - ) - return option ? (option.label || option.name || val) : val - }) - return labels.length > 0 ? labels.join(', ') : valueStr - } else { - // 单值情况 - const option = options.find(opt => - opt.id == value || - opt.value == value || - String(opt.id) === String(value) || - String(opt.value) === String(value) - ) - if (option) { - return option.label || option.name || value - } + const option = options.find(opt => + opt.id == value || + opt.value == value || + String(opt.id) === String(value) || + String(opt.value) === String(value) + ) + if (option) { + return option.label || option.name || value } } return String(value) diff --git a/src/views/pre-approval/ProcessQuery.vue b/src/views/pre-approval/ProcessQuery.vue index a8821bc..d429d43 100644 --- a/src/views/pre-approval/ProcessQuery.vue +++ b/src/views/pre-approval/ProcessQuery.vue @@ -507,58 +507,26 @@ const formatSubFieldValue = (rowData, subField) => { return '-' case 'select': case 'radio': - // 处理多值情况(逗号分隔) - const valueStr = String(value) - if (valueStr.includes(',')) { - // 分割多个值 - const values = valueStr.split(',').map(v => v.trim()).filter(v => v) - const labels = values.map(val => { - // 优先从 selectionOptionsMap 中查找 - if (subField.selection_model && selectionOptionsMap.value[subField.selection_model]) { - const options = selectionOptionsMap.value[subField.selection_model] - const option = options.find(opt => - opt.id == val || - opt.value == val || - String(opt.id) === String(val) || - String(opt.value) === String(val) - ) - if (option) { - return option.label || option.name || val - } - } - // 如果没有 selection_model 或找不到,尝试使用 subField.options - if (subField.options && Array.isArray(subField.options)) { - const option = subField.options.find(opt => opt.value === val || opt.id === val) - if (option) { - return option.label || option.name || val - } - } - return val - }) - return labels.length > 0 ? labels.join(', ') : valueStr - } else { - // 单值情况 - // 如果有 selection_model,从 selectionOptionsMap 中查找 - if (subField.selection_model && selectionOptionsMap.value[subField.selection_model]) { - const options = selectionOptionsMap.value[subField.selection_model] - const option = options.find(opt => - opt.id == value || - opt.value == value || - String(opt.id) === String(value) || - String(opt.value) === String(value) - ) - if (option) { - return option.label || option.name || value - } - } - - // 如果没有 selection_model 或找不到,尝试使用 subField.options - if (subField.options && Array.isArray(subField.options)) { - const option = subField.options.find(opt => opt.value === value || opt.id === value) - return option ? (option.label || option.name || value) : value + // 如果有 selection_model,从 selectionOptionsMap 中查找 + if (subField.selection_model && selectionOptionsMap.value[subField.selection_model]) { + const options = selectionOptionsMap.value[subField.selection_model] + const option = options.find(opt => + opt.id == value || + opt.value == value || + String(opt.id) === String(value) || + String(opt.value) === String(value) + ) + if (option) { + return option.label || option.name || value } - return value } + + // 如果没有 selection_model 或找不到,尝试使用 subField.options + if (subField.options && Array.isArray(subField.options)) { + const option = subField.options.find(opt => opt.value === value || opt.id === value) + return option ? (option.label || option.name || value) : value + } + return value case 'choice': case 'choices': { const ids = parseChoiceValueToIds(value) @@ -778,58 +746,26 @@ const formatFieldValue = (data, field) => { return '-' case 'select': case 'radio': - // 处理多值情况(逗号分隔) - const valueStr2 = String(value) - if (valueStr2.includes(',')) { - // 分割多个值 - const values = valueStr2.split(',').map(v => v.trim()).filter(v => v) - const labels = values.map(val => { - // 优先从 selectionOptionsMap 中查找 - if (field.selection_model && selectionOptionsMap.value[field.selection_model]) { - const options = selectionOptionsMap.value[field.selection_model] - const option = options.find(opt => - opt.id == val || - opt.value == val || - String(opt.id) === String(val) || - String(opt.value) === String(val) - ) - if (option) { - return option.label || option.name || val - } - } - // 如果没有 selection_model 或找不到,尝试使用 field.options - if (field.options && Array.isArray(field.options)) { - const option = field.options.find(opt => opt.value === val || opt.id === val) - if (option) { - return option.label || option.name || val - } - } - return val - }) - return labels.length > 0 ? labels.join(', ') : valueStr2 - } else { - // 单值情况 - // 如果有 selection_model,从 selectionOptionsMap 中查找 - if (field.selection_model && selectionOptionsMap.value[field.selection_model]) { - const options = selectionOptionsMap.value[field.selection_model] - const option = options.find(opt => - opt.id == value || - opt.value == value || - String(opt.id) === String(value) || - String(opt.value) === String(value) - ) - if (option) { - return option.label || option.name || value - } - } - - // 如果没有 selection_model 或找不到,尝试使用 field.options - if (field.options && Array.isArray(field.options)) { - const option = field.options.find(opt => opt.value === value || opt.id === value) - return option ? (option.label || option.name || value) : value + // 如果有 selection_model,从 selectionOptionsMap 中查找 + if (field.selection_model && selectionOptionsMap.value[field.selection_model]) { + const options = selectionOptionsMap.value[field.selection_model] + const option = options.find(opt => + opt.id == value || + opt.value == value || + String(opt.id) === String(value) || + String(opt.value) === String(value) + ) + if (option) { + return option.label || option.name || value } - return value } + + // 如果没有 selection_model 或找不到,尝试使用 field.options + if (field.options && Array.isArray(field.options)) { + const option = field.options.find(opt => opt.value === value || opt.id === value) + return option ? (option.label || option.name || value) : value + } + return value case 'choice': case 'choices': { // 人员选择:后端通常会配置 selection_model=App\Models\User