From 7f2e8e9c560f1872e93a40df297401315e636d5b Mon Sep 17 00:00:00 2001 From: weizong song Date: Tue, 6 Jan 2026 05:06:28 +0800 Subject: [PATCH] up --- rebuild.sh | 4 + src/api/me.js | 8 + src/api/payment.js | 40 + src/components/BudgetSourcePickerField.vue | 720 +++++++++++--- src/utils/formBuilder.js | 32 +- src/views/MeetingMinutes/index.vue | 30 +- src/views/flow/DesktopForm.vue | 127 ++- src/views/flow/MobileForm.vue | 119 +++ src/views/flow/create.vue | 1017 ++++++++++++++++++-- 错误诊断和解决方案.md | 4 + 10 files changed, 1839 insertions(+), 262 deletions(-) diff --git a/rebuild.sh b/rebuild.sh index c1f4537..957041b 100644 --- a/rebuild.sh +++ b/rebuild.sh @@ -113,3 +113,7 @@ echo "" + + + + diff --git a/src/api/me.js b/src/api/me.js index f654981..a9e8cf5 100644 --- a/src/api/me.js +++ b/src/api/me.js @@ -35,3 +35,11 @@ export function permissions () { } }) } + +export function getModuleRoles(module) { + return request({ + url: `/api/auth/module-roles/${module}`, + method: "get", + isLoading: false + }) +} diff --git a/src/api/payment.js b/src/api/payment.js index e360c5e..165a603 100644 --- a/src/api/payment.js +++ b/src/api/payment.js @@ -31,6 +31,46 @@ export function getPaymentCategoryTemplateElements(categoryId, isLoading = false }) } +/** + * 根据合同ID获取支付列表 + * 后端路由:GET /api/budget/payments?contract_id=xxx + */ +export function getPaymentsByContractId(contractId, params = {}, isLoading = false) { + return request({ + method: 'get', + url: '/api/budget/payments', + params: { + contract_id: contractId, + ...params + }, + isLoading + }) +} + +/** + * 获取 Budget 合同详情 + * 后端路由:GET /api/budget/contracts/{id} + */ +export function getBudgetContractDetail(id, isLoading = false) { + return request({ + method: 'get', + url: `/api/budget/contracts/${id}`, + isLoading + }) +} + +/** + * 获取模板元素定义(用于 checklist/附件等最原始 options) + * 后端路由:GET /api/budget/template-elements/{id} + */ +export function getTemplateElementDetail(id, isLoading = false) { + return request({ + method: 'get', + url: `/api/budget/template-elements/${id}`, + isLoading + }) +} + /** * 获取明细表格字段定义 * 后端路由:GET /api/budget/detail-table-fields/{elementId} diff --git a/src/components/BudgetSourcePickerField.vue b/src/components/BudgetSourcePickerField.vue index 0085a60..e53858a 100644 --- a/src/components/BudgetSourcePickerField.vue +++ b/src/components/BudgetSourcePickerField.vue @@ -28,7 +28,7 @@ -
+
-
+
部门预算
- {{ node.label }} + {{ node.label }} 预算: @@ -95,17 +99,21 @@
自有账户
- {{ node.label }} + {{ node.label }} 预算: @@ -132,17 +140,62 @@
专项资金
- {{ node.label }} + {{ node.label }} + + + 预算: + {{ formatAmount(data.budget_amount) }} + + + 已用: + {{ formatAmount(data.used_amount || 0) }} + + + 执行率: + + {{ formatExecutionRate(data.execution_rate || 0) }} + + + + + +
+ +
+
上一年结转资金
+ + + {{ node.label }} 预算: @@ -167,24 +220,25 @@
暂无数据
- -
-
+ +
+
部门金额分配
-
- 该预算来源授权给多个部门,请将支付总额分配到各个部门(分配金额可以为0): +
+ 请选择预算来源(可多选),并将本次付款金额分配到对应部门与预算(分配金额可以为0,但分配总额必须匹配本次付款金额)。
- + + - + -
-
- 已分配总额: - +
+
+ 已分配总额: + {{ formatAmount(allocationTotal) }}
-
- 支付总额: - {{ formatAmount(totalAmount) }} +
+ 本次付款金额: + {{ formatAmount(totalAmount) }}
- 剩余金额: - + 剩余金额: + {{ formatAmount(remainingAmount) }}
-
- 提示:当前支付总额为0,您可以在财务确认时填写实际金额 +
+ 提示:未识别到"本次付款金额",当前按 0 校验;若需要校验,请确保流程表单存在该字段。
@@ -226,7 +279,7 @@ 取消 确定 @@ -256,11 +309,18 @@ export default { yearOptions: [], yearId: null, treeData: [], - pendingId: null, - // 分配相关 - pendingAuthorizations: [], + // 选择相关(支持多选) + selectedBudgetIds: [], + lastValidBudgetIds: [], + selectionError: "", + // 授权缓存:budget_data_id -> authorizations[] + authCache: {}, + // 分配相关(右侧始终展示) pendingAllocations: [], + // “本次付款金额(元)”——从流程自定义模型字段中按 label 模糊匹配“本次付款金额”解析 totalAmount: 0, + // 生成的显示文本(用于展示区) + generatedDisplayText: "", }; }, computed: { @@ -268,7 +328,7 @@ export default { parsedValue() { const value = this.value; if (!value || value === null || value === undefined) { - return { budget_data_id: null, allocations: null }; + return { budget_data_id: null, allocations: null, budget_items: null }; } // 已经是对象(推荐的新格式:直接对象落库/回传) @@ -276,12 +336,13 @@ export default { return { budget_data_id: value?.budget_data_id ?? null, allocations: value?.allocations ?? null, + budget_items: value?.budget_items ?? null, }; } // 兼容:如果是数组(极端旧数据/异常数据),当作 allocations if (Array.isArray(value)) { - return { budget_data_id: null, allocations: value }; + return { budget_data_id: null, allocations: value, budget_items: null }; } // 字符串:可能是 @@ -290,7 +351,7 @@ export default { // 3) 双重编码: '"{\"budget_data_id\":\"2\",\"allocations\":[...]}"' if (typeof value === "string") { const raw = value.trim(); - if (!raw) return { budget_data_id: null, allocations: null }; + if (!raw) return { budget_data_id: null, allocations: null, budget_items: null }; const parseMaybeJson = (s) => { try { @@ -307,11 +368,12 @@ export default { return { budget_data_id: p1.data?.budget_data_id ?? null, allocations: p1.data?.allocations ?? null, + budget_items: p1.data?.budget_items ?? null, }; } // 解析结果是数组:当作 allocations(仍可能缺 budget_data_id) if (Array.isArray(p1.data)) { - return { budget_data_id: null, allocations: p1.data }; + return { budget_data_id: null, allocations: p1.data, budget_items: null }; } // 解析结果是字符串:双重编码常见形态 if (typeof p1.data === "string") { @@ -322,24 +384,26 @@ export default { return { budget_data_id: p2.data?.budget_data_id ?? null, allocations: p2.data?.allocations ?? null, + budget_items: p2.data?.budget_items ?? null, }; } if (p2.ok && Array.isArray(p2.data)) { - return { budget_data_id: null, allocations: p2.data }; + return { budget_data_id: null, allocations: p2.data, budget_items: null }; } // inner 可能是数字字符串 return { budget_data_id: isNaN(inner) ? null : String(inner), allocations: null, + budget_items: null, }; } } // 非JSON:当作旧格式 ID - return { budget_data_id: isNaN(raw) ? null : String(raw), allocations: null }; + return { budget_data_id: isNaN(raw) ? null : String(raw), allocations: null, budget_items: null }; } - return { budget_data_id: null, allocations: null }; + return { budget_data_id: null, allocations: null, budget_items: null }; }, budgetDataId() { return this.parsedValue.budget_data_id; @@ -347,8 +411,15 @@ export default { allocations() { return this.parsedValue.allocations; }, + budgetItems() { + const items = this.parsedValue.budget_items; + return Array.isArray(items) ? items : []; + }, normalizedValue() { // 返回JSON格式的字符串(用于表单提交/兜底) + if (this.budgetItems.length > 0) { + return JSON.stringify({ budget_items: this.budgetItems }); + } if (!this.budgetDataId) return ""; return JSON.stringify({ budget_data_id: this.budgetDataId, @@ -356,7 +427,7 @@ export default { }); }, hasValue() { - return this.budgetDataId !== null && this.budgetDataId !== ""; + return (this.budgetDataId !== null && this.budgetDataId !== "") || this.budgetItems.length > 0; }, displayText() { // 优先使用后端返回的 `{字段名}_display` @@ -364,15 +435,12 @@ export default { return this.display; } - // 如果display为空,但有值,尝试从JSON中解析并生成显示文本 - if (this.hasValue && this.budgetDataId) { - // 如果value是JSON格式但display为空,显示budget_data_id作为fallback - // 注意:这里不显示原始JSON,而是显示解析出的ID - return `预算ID: ${this.budgetDataId}`; + // 使用生成的显示文本 + if (this.generatedDisplayText && this.generatedDisplayText.trim()) { + return this.generatedDisplayText; } - // 如果value存在但不是JSON格式(旧格式),也不显示原始值 - // 避免显示原始JSON字符串 + // 如果都没有,返回空字符串(避免显示JSON或ID) return ""; }, treeProps() { @@ -391,11 +459,15 @@ export default { specialFundTree() { return (this.treeData || []).filter((n) => n?.budget_type === "special_fund"); }, + lastYearCarryoverTree() { + return (this.treeData || []).filter((n) => n?.budget_type === "last_year_carryover"); + }, budgetTypeMap() { return { department: "部门预算", project: "自有账户", special_fund: "专项资金", + last_year_carryover: "上一年结转资金", }; }, // 分配总额 @@ -408,17 +480,26 @@ export default { remainingAmount() { return this.totalAmount - this.allocationTotal; }, + canConfirm() { + // 必须已选择至少一个预算来源,且分配总额必须等于本次付款金额(未识别时 totalAmount=0,则要求分配总额也为0) + if (!this.selectedBudgetIds || this.selectedBudgetIds.length === 0) return false; + if (this.selectionError) return false; + return Number(this.allocationTotal) === Number(this.totalAmount); + }, }, methods: { openDialog() { this.dialogVisible = true; }, onDialogOpen() { - this.pendingId = null; + this.selectionError = ""; + this.selectedBudgetIds = []; + // 读取“本次付款金额” + this.totalAmount = this.extractPaymentAmountFromForm(); this.ensureInit(); }, onDialogClose() { - this.pendingId = null; + this.selectionError = ""; }, async ensureInit() { if (!this.yearOptions || this.yearOptions.length === 0) { @@ -450,11 +531,13 @@ export default { } }, async tryResolveYearIdByValue() { - if (!this.hasValue || !this.budgetDataId) return null; + if (!this.hasValue) return null; + const idFromValue = this.budgetDataId || (this.budgetItems[0] && this.budgetItems[0].budget_data_id); + if (!idFromValue) return null; try { const detail = await request({ method: "get", - url: `/api/budget/budget-data/${this.budgetDataId}`, + url: `/api/budget/budget-data/${idFromValue}`, isLoading: false, }); const yearId = detail?.year_id ?? null; @@ -464,14 +547,20 @@ export default { } }, async getDepartmentAuthorizations(budgetDataId) { + const key = String(budgetDataId || ""); + if (!key) return []; + if (this.authCache[key]) return this.authCache[key]; try { const res = await request({ method: "get", url: `/api/budget/budget-data/${budgetDataId}/department-authorizations`, isLoading: false, }); - return Array.isArray(res) ? res : []; + const list = Array.isArray(res) ? res : []; + this.$set(this.authCache, key, list); + return list; } catch (e) { + this.$set(this.authCache, key, []); return []; } }, @@ -481,51 +570,223 @@ export default { try { const res = await getBudgetDataTree({ year_id: yearId }, false); this.treeData = Array.isArray(res) ? res : []; + // 回显已有选择 + this.$nextTick(async () => { + await this.restoreSelectionFromValue(); + await this.rebuildAllocationsFromCurrentSelection(); + // 树数据加载完成后,重新生成显示文本 + if (this.hasValue && !this.display) { + await this.generateDisplayText(); + } + }); } finally { this.treeLoading = false; } }, async onYearChange(val) { - this.pendingId = null; + this.selectionError = ""; + this.selectedBudgetIds = []; + this.pendingAllocations = []; this.treeData = []; await this.loadTree(val); }, - async onNodeClick(node) { - if (!node || !node.is_leaf) return; - this.pendingId = node.id; - - // 选择叶子节点时,检查部门授权并显示分配界面 - const authorizations = await this.getDepartmentAuthorizations(node.id); - if (authorizations.length > 1) { - // 多部门:显示分配界面 - this.pendingAuthorizations = authorizations; - // 恢复之前的分配方案(如果有) - const existingAllocations = this.allocations || []; - if (existingAllocations.length > 0) { - // 合并:保留已有的分配,新增的部门设为0 - this.pendingAllocations = authorizations.map(auth => { - const existing = existingAllocations.find( - a => String(a.department_id) === String(auth.department_id) - ); + onNodeClickToggle(data, node, tree) { + if (!data || !data.is_leaf) return; + // 点击节点也切换勾选 + if (tree && typeof tree.setChecked === "function") { + tree.setChecked(data.id, !node.checked, true); + } + }, + onLabelClick(data, node, tree) { + // 点击节点名称文字时切换勾选(仅叶子节点) + if (!data || !data.is_leaf) return; + if (tree && typeof tree.setChecked === "function") { + const isChecked = tree.getCheckedKeys().includes(data.id); + tree.setChecked(data.id, !isChecked, true); + } + }, + onTreeCheckChange() { + // 任意树勾选变化后统一重算 + this.rebuildAllocationsFromCurrentSelection(); + }, + collectCheckedLeafNodes() { + const refs = [ + this.$refs.departmentTreeRef, + this.$refs.projectTreeRef, + this.$refs.specialFundTreeRef, + this.$refs.lastYearCarryoverTreeRef, + ].filter(Boolean); + const nodes = []; + refs.forEach((tree) => { + if (typeof tree.getCheckedNodes === "function") { + const checked = tree.getCheckedNodes(true, false) || []; + checked.forEach((n) => { + if (n && n.is_leaf) nodes.push(n); + }); + } + }); + // 去重 + const map = {}; + nodes.forEach((n) => (map[String(n.id)] = n)); + return Object.values(map); + }, + setCheckedIds(ids) { + const list = (ids || []).map((x) => String(x)); + const refs = [ + this.$refs.departmentTreeRef, + this.$refs.projectTreeRef, + this.$refs.specialFundTreeRef, + this.$refs.lastYearCarryoverTreeRef, + ].filter(Boolean); + refs.forEach((tree) => { + if (typeof tree.setCheckedKeys === "function") { + tree.setCheckedKeys(list); + } + }); + }, + async restoreSelectionFromValue() { + // 按 value 回显勾选 + let ids = []; + if (this.budgetItems.length > 0) { + ids = this.budgetItems.map((x) => String(x.budget_data_id)).filter(Boolean); + } else if (this.budgetDataId) { + ids = [String(this.budgetDataId)]; + } + this.lastValidBudgetIds = ids; + this.selectedBudgetIds = ids; + this.setCheckedIds(ids); + }, + extractPaymentAmountFromForm() { + // 从页面 DOM 中模糊匹配 label “本次付款金额”,解析金额(兼容单位后缀) + try { + const labels = Array.from(document.querySelectorAll(".el-form-item__label")); + const labelEl = labels.find((el) => (el.innerText || "").includes("本次付款金额")); + if (!labelEl) return 0; + const formItem = labelEl.closest(".el-form-item"); + const content = formItem ? formItem.querySelector(".el-form-item__content") : null; + const input = + (content && content.querySelector("input")) || + (content && content.querySelector("textarea")) || + null; + const raw = input ? String(input.value || "") : ""; + const num = parseFloat(raw.replace(/,/g, "").replace(/[^\d.-]/g, "")); + return isNaN(num) ? 0 : num; + } catch (e) { + return 0; + } + }, + getExistingAllocationMap() { + // 兼容旧/新结构:返回 { `${budgetId}:${deptId}`: amount } + const map = {}; + // 新:budget_items + if (this.budgetItems.length > 0) { + this.budgetItems.forEach((item) => { + const bid = String(item.budget_data_id || ""); + const allocs = Array.isArray(item.allocations) ? item.allocations : []; + allocs.forEach((a) => { + const did = String(a.department_id || ""); + if (!bid || !did) return; + map[`${bid}:${did}`] = parseFloat(a.allocated_amount) || 0; + }); + }); + } else if (this.budgetDataId && Array.isArray(this.allocations)) { + const bid = String(this.budgetDataId); + this.allocations.forEach((a) => { + const did = String(a.department_id || ""); + if (!did) return; + map[`${bid}:${did}`] = parseFloat(a.allocated_amount) || 0; + }); + } + return map; + }, + async rebuildAllocationsFromCurrentSelection() { + this.selectionError = ""; + const selectedNodes = this.collectCheckedLeafNodes(); + const ids = selectedNodes.map((n) => String(n.id)); + this.selectedBudgetIds = ids; + if (ids.length === 0) { + this.pendingAllocations = []; + return; + } + + // 重新读取“本次付款金额” + this.totalAmount = this.extractPaymentAmountFromForm(); + + const existingMap = this.getExistingAllocationMap(); + + // 单条预算来源 + if (selectedNodes.length === 1) { + const node = selectedNodes[0]; + const auths = await this.getDepartmentAuthorizations(node.id); + if (!auths || auths.length === 0) { + this.selectionError = "该预算来源未配置部门授权"; + this.$message.warning(this.selectionError); + this.setCheckedIds(this.lastValidBudgetIds); + return; + } + + if (auths.length === 1) { + // 情形(1):单预算 + 单部门 -> 自动全额填充 + const a = auths[0]; + this.pendingAllocations = [ + { + budget_data_id: node.id, + budget_name: node.name || "", + department_id: a.department_id, + department_name: a.department?.name || "", + allocated_amount: this.totalAmount, + }, + ]; + } else { + // 情形(2):单预算 + 多部门 -> 与原逻辑一致 + this.pendingAllocations = auths.map((a) => { + const key = `${String(node.id)}:${String(a.department_id)}`; return { - department_id: auth.department_id, - department_name: auth.department?.name || '', - allocated_amount: existing ? (parseFloat(existing.allocated_amount) || 0) : 0 + budget_data_id: node.id, + budget_name: node.name || "", + department_id: a.department_id, + department_name: a.department?.name || "", + allocated_amount: existingMap[key] ?? 0, }; }); - } else { - // 没有之前的分配方案,创建新的 - this.pendingAllocations = authorizations.map(auth => ({ - department_id: auth.department_id, - department_name: auth.department?.name || '', - allocated_amount: 0 - })); } - } else { - // 单部门:清空分配界面 - this.pendingAuthorizations = []; - this.pendingAllocations = []; + + this.lastValidBudgetIds = ids; + return; + } + + // 多条预算来源:同一直接父级 + 每条唯一授权部门 + const parentId = selectedNodes[0].parent_id ?? null; + const parentMismatch = selectedNodes.some((n) => (n.parent_id ?? null) !== parentId); + if (parentMismatch) { + this.selectionError = "多条预算来源必须同属一个直接父级"; + this.$message.warning(this.selectionError); + this.setCheckedIds(this.lastValidBudgetIds); + return; } + + const rows = []; + for (const node of selectedNodes) { + const auths = await this.getDepartmentAuthorizations(node.id); + if (!auths || auths.length !== 1) { + this.selectionError = "多条预算来源时,每条预算必须且仅能授权给一个部门"; + this.$message.warning(this.selectionError); + this.setCheckedIds(this.lastValidBudgetIds); + return; + } + const a = auths[0]; + const key = `${String(node.id)}:${String(a.department_id)}`; + rows.push({ + budget_data_id: node.id, + budget_name: node.name || "", + department_id: a.department_id, + department_name: a.department?.name || "", + // 情形(3) 默认全为 0(由用户手动填充) + allocated_amount: existingMap[key] ?? 0, + }); + } + this.pendingAllocations = rows; + this.lastValidBudgetIds = ids; }, findPathNames(tree, id, path = []) { if (!Array.isArray(tree)) return null; @@ -546,67 +807,57 @@ export default { return opt?.label || (this.yearId ? `${this.yearId}年` : ""); }, async confirmPick() { - if (!this.pendingId) return; - - // 验证多部门分配总额(如果totalAmount > 0) - if (this.pendingAuthorizations.length > 1 && this.totalAmount > 0) { - if (this.allocationTotal !== this.totalAmount) { - this.$message.warning(`分配总额必须等于支付总额 ${this.formatAmount(this.totalAmount)}`); - return; - } + if (!this.canConfirm) { + this.$message.warning("请先选择预算来源,并确保分配总额等于本次付款金额"); + return; } - - // 获取部门授权列表(用于确认) - const authorizations = await this.getDepartmentAuthorizations(this.pendingId); - - let allocations = []; - if (authorizations.length === 1) { - // 单部门:自动创建分配方案 - allocations = [{ - department_id: authorizations[0].department_id, - department_name: authorizations[0].department?.name || '', - allocated_amount: 0 // 占位,实际金额在financialConfirm时确定 - }]; - } else if (authorizations.length > 1) { - // 多部门:使用已填写的分配方案 - allocations = this.pendingAllocations || []; + + // 基于 pendingAllocations 构建最终 payload(必须包含预算ID与部门ID,支持多条) + const byBudget = {}; + (this.pendingAllocations || []).forEach((row) => { + const bid = String(row.budget_data_id || ""); + const did = String(row.department_id || ""); + if (!bid || !did) return; + if (!byBudget[bid]) byBudget[bid] = { budget_data_id: bid, allocations: [] }; + byBudget[bid].allocations.push({ + department_id: did, + department_name: row.department_name || "", + allocated_amount: parseFloat(row.allocated_amount) || 0, + }); + }); + + const budgetIds = Object.keys(byBudget); + let payload; + if (budgetIds.length === 1) { + const bid = budgetIds[0]; + payload = { + budget_data_id: bid, + allocations: byBudget[bid].allocations, + }; + } else { + payload = { + budget_items: budgetIds.map((bid) => byBudget[bid]), + }; } - - this.doConfirmPick(allocations); - }, - doConfirmPick(allocations) { - const nodes = this.findPathNames(this.treeData, this.pendingId) || []; - const names = nodes.map((n) => n?.name).filter(Boolean); - const leaf = nodes[nodes.length - 1]; - const typeText = this.budgetTypeMap[leaf?.budget_type] || leaf?.budget_type || ""; - const yearLabel = this.getYearLabel(); - const display = [yearLabel, typeText, ...names].filter(Boolean).join(" / "); - // 关键:不要再把JSON对象当“字符串”提交给后端 json 字段,否则后端会再次 json_encode 导致双重编码 - const payload = { - budget_data_id: String(this.pendingId), - allocations: allocations.length > 0 ? allocations : null, - }; + // 生成展示文本(多条用 “;” 分隔) + const yearLabel = this.getYearLabel(); + const displayParts = budgetIds.map((bid) => { + const nodes = this.findPathNames(this.treeData, bid) || []; + const names = nodes.map((n) => n?.name).filter(Boolean); + const leaf = nodes[nodes.length - 1]; + const typeText = this.budgetTypeMap[leaf?.budget_type] || leaf?.budget_type || ""; + return [yearLabel, typeText, ...names].filter(Boolean).join(" / "); + }); + const display = displayParts.filter(Boolean).join(";"); - // 更新主值与展示值(展示值用于只读/回显) this.$emit("input", payload); this.$emit("update:display", display); - this.dialogVisible = false; - this.pendingId = null; - // 清空分配相关数据 - this.pendingAuthorizations = []; - this.pendingAllocations = []; }, onAllocationChange() { // 分配金额变化时的处理(实时验证已在按钮disabled中处理) }, - onDialogClose() { - this.pendingId = null; - // 清空分配相关数据 - this.pendingAuthorizations = []; - this.pendingAllocations = []; - }, formatAmount(amount) { if (amount === null || amount === undefined) return "0.00"; const num = parseFloat(amount); @@ -630,7 +881,144 @@ export default { clearValue() { this.$emit("input", ""); this.$emit("update:display", ""); + this.generatedDisplayText = ""; }, + async generateDisplayText() { + // 如果没有值,清空显示文本 + if (!this.hasValue) { + this.generatedDisplayText = ""; + return; + } + + // 如果已经有后端返回的 display,不需要生成 + if (this.display && this.display.trim()) { + return; + } + + // 需要加载树数据来生成显示文本 + if (!this.yearOptions || this.yearOptions.length === 0) { + await this.loadYearOptions(); + } + + // 尝试获取年份ID + if (!this.yearId) { + const fromValue = await this.tryResolveYearIdByValue(); + this.yearId = fromValue || (this.yearOptions.length > 0 ? this.yearOptions[0].value : null); + } + + // 如果没有年份ID,无法生成显示文本 + if (!this.yearId) { + this.generatedDisplayText = ""; + return; + } + + // 加载树数据 + if (!this.treeData || this.treeData.length === 0) { + try { + const res = await getBudgetDataTree({ year_id: this.yearId }, false); + this.treeData = Array.isArray(res) ? res : []; + } catch (e) { + console.error("加载树数据失败:", e); + this.generatedDisplayText = ""; + return; + } + } + + // 生成显示文本 + const parts = []; + const yearLabel = this.getYearLabel(); + + // 处理多预算来源的情况 + if (this.budgetItems.length > 0) { + const displayParts = []; + for (const item of this.budgetItems) { + const budgetId = String(item.budget_data_id || ""); + if (!budgetId) continue; + + const nodes = this.findPathNames(this.treeData, budgetId) || []; + const names = nodes.map((n) => n?.name).filter(Boolean); + const leaf = nodes[nodes.length - 1]; + const typeText = this.budgetTypeMap[leaf?.budget_type] || leaf?.budget_type || ""; + + // 获取部门信息 + const allocations = Array.isArray(item.allocations) ? item.allocations : []; + const deptNames = allocations + .map((a) => a.department_name) + .filter(Boolean) + .slice(0, 3); // 最多显示3个部门 + + let partText = [yearLabel, typeText, ...names].filter(Boolean).join(" / "); + if (deptNames.length > 0) { + if (deptNames.length === 1) { + partText += `(${deptNames[0]})`; + } else if (deptNames.length <= 3) { + partText += `(${deptNames.join("、")})`; + } else { + partText += `(${deptNames.slice(0, 3).join("、")}等${allocations.length}个部门)`; + } + } + displayParts.push(partText); + } + this.generatedDisplayText = displayParts.filter(Boolean).join(";"); + return; + } + + // 处理单预算来源的情况 + if (this.budgetDataId) { + const nodes = this.findPathNames(this.treeData, this.budgetDataId) || []; + const names = nodes.map((n) => n?.name).filter(Boolean); + const leaf = nodes[nodes.length - 1]; + const typeText = this.budgetTypeMap[leaf?.budget_type] || leaf?.budget_type || ""; + + let displayText = [yearLabel, typeText, ...names].filter(Boolean).join(" / "); + + // 获取部门信息 + const allocations = Array.isArray(this.allocations) ? this.allocations : []; + if (allocations.length > 0) { + const deptNames = allocations + .map((a) => a.department_name) + .filter(Boolean) + .slice(0, 3); // 最多显示3个部门 + + if (deptNames.length === 1) { + displayText += `(${deptNames[0]})`; + } else if (deptNames.length <= 3) { + displayText += `(${deptNames.join("、")})`; + } else { + displayText += `(${deptNames.slice(0, 3).join("、")}等${allocations.length}个部门)`; + } + } + + this.generatedDisplayText = displayText; + return; + } + + // 如果都没有,清空显示文本 + this.generatedDisplayText = ""; + }, + }, + watch: { + value: { + immediate: true, + handler() { + // 当 value 变化时,重新生成显示文本 + this.$nextTick(() => { + this.generateDisplayText(); + }); + }, + }, + display(newVal) { + // 如果后端返回了 display,清空生成的显示文本 + if (newVal && newVal.trim()) { + this.generatedDisplayText = ""; + } + }, + }, + mounted() { + // 组件挂载时,如果有值,生成显示文本 + if (this.hasValue) { + this.generateDisplayText(); + } }, }; @@ -644,8 +1032,9 @@ export default { } .budget-source-label { - color: #606266; + color: #303133; font-size: 14px; + font-weight: 500; flex: 1; min-width: 160px; overflow: hidden; @@ -654,16 +1043,16 @@ export default { } .budget-source-label--muted { - color: #909399; + color: #606266; } .tree-title { - font-size: 14px; + font-size: 16px; font-weight: 600; - color: #606266; - margin-bottom: 8px; - padding-bottom: 8px; - border-bottom: 1px solid #e4e7ed; + color: #303133; + margin-bottom: 12px; + padding-bottom: 10px; + border-bottom: 2px solid #e4e7ed; } .custom-tree-node { @@ -672,7 +1061,7 @@ export default { align-items: center; justify-content: space-between; font-size: 14px; - padding-right: 8px; + padding-right: 12px; } .node-label { @@ -680,59 +1069,74 @@ export default { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + color: #303133; + font-weight: 500; +} + +.node-label--clickable { + cursor: pointer; + transition: color 0.2s; +} + +.node-label--clickable:hover { + color: #409eff; } .node-usage-info { display: flex; align-items: center; - gap: 12px; - margin-left: 12px; + gap: 16px; + margin-left: 16px; flex-shrink: 0; - font-size: 12px; + font-size: 14px; } .usage-item { display: flex; align-items: center; - gap: 4px; + gap: 6px; } .usage-label { - color: #909399; - font-size: 12px; + color: #606266; + font-size: 14px; + font-weight: 500; } .usage-value { - color: #606266; - font-weight: 500; - font-size: 12px; + color: #303133; + font-weight: 600; + font-size: 14px; } .usage-value--used { color: #409eff; + font-weight: 600; } .usage-value--rate { - min-width: 50px; + min-width: 60px; text-align: right; } .usage-value--rate.rate-high { color: #f56c6c; - font-weight: 600; + font-weight: 700; } .usage-value--rate.rate-medium { color: #e6a23c; - font-weight: 500; + font-weight: 600; } .usage-value--rate.rate-normal { color: #409eff; + font-weight: 600; } .usage-value--rate.rate-low { color: #909399; + font-weight: 500; } diff --git a/src/utils/formBuilder.js b/src/utils/formBuilder.js index a215c86..a3ed9e2 100644 --- a/src/utils/formBuilder.js +++ b/src/utils/formBuilder.js @@ -1386,7 +1386,21 @@ export default function formBuilder( case "budget-source": // 只读模式下显示后端返回的 _display 值 const displayFieldName = info.name + '_display'; - const displayValue = target[displayFieldName] || target[info.name] || ''; + let displayValue = target[displayFieldName] || ''; + + // 如果 _display 为空,尝试从原始值生成显示文本(避免显示JSON) + if (!displayValue && target[info.name]) { + const rawValue = target[info.name]; + // 如果是字符串且看起来像JSON,不直接显示 + if (typeof rawValue === 'string' && (rawValue.trim().startsWith('{') || rawValue.trim().startsWith('['))) { + displayValue = ''; // 不显示原始JSON + } else if (typeof rawValue === 'object' || Array.isArray(rawValue)) { + displayValue = ''; // 不显示对象或数组 + } else { + displayValue = String(rawValue); + } + } + console.log('[budget-source] 只读模式渲染', { fieldName: info.name, displayFieldName, @@ -2591,7 +2605,21 @@ export default function formBuilder( case "budget-source": // 只读模式下显示后端返回的 _display 值 const displayFieldNameMobile = info.name + '_display'; - const displayValueMobile = target[displayFieldNameMobile] || target[info.name] || ''; + let displayValueMobile = target[displayFieldNameMobile] || ''; + + // 如果 _display 为空,尝试从原始值生成显示文本(避免显示JSON) + if (!displayValueMobile && target[info.name]) { + const rawValue = target[info.name]; + // 如果是字符串且看起来像JSON,不直接显示 + if (typeof rawValue === 'string' && (rawValue.trim().startsWith('{') || rawValue.trim().startsWith('['))) { + displayValueMobile = ''; // 不显示原始JSON + } else if (typeof rawValue === 'object' || Array.isArray(rawValue)) { + displayValueMobile = ''; // 不显示对象或数组 + } else { + displayValueMobile = String(rawValue); + } + } + console.log('[budget-source] 只读模式渲染(移动端)', { fieldName: info.name, displayFieldName: displayFieldNameMobile, diff --git a/src/views/MeetingMinutes/index.vue b/src/views/MeetingMinutes/index.vue index 88d9781..0974b17 100644 --- a/src/views/MeetingMinutes/index.vue +++ b/src/views/MeetingMinutes/index.vue @@ -4,6 +4,7 @@