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-oa/src/components/BudgetSourcePickerField.vue

298 lines
8.1 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="budget-source-field">
<!-- 提交用隐藏字段 -->
<input :name="fieldName" type="hidden" :value="normalizedValue" />
<!-- 当前展示文本有值时展示 -->
<span v-if="hasValue && displayText" class="budget-source-label" :title="displayText">
{{ displayText }}
</span>
<span
v-else-if="hasValue"
class="budget-source-label budget-source-label--muted"
:title="normalizedValue"
>
ID: {{ normalizedValue }}
</span>
<el-button type="primary" size="small" @click="openDialog"></el-button>
<el-button v-if="hasValue" size="small" @click="clearValue">清除</el-button>
<el-dialog
title="选择预算来源"
:visible.sync="dialogVisible"
width="720px"
:close-on-click-modal="false"
append-to-body
@open="onDialogOpen"
@close="onDialogClose"
>
<div style="margin-bottom: 12px">
<el-select
v-model="yearId"
placeholder="请选择年份"
filterable
style="width: 100%"
:loading="yearLoading"
@change="onYearChange"
>
<el-option
v-for="y in yearOptions"
:key="String(y.value)"
:label="y.label"
:value="y.value"
/>
</el-select>
</div>
<div v-loading="treeLoading" style="max-height: 420px; overflow: auto">
<div v-if="departmentTree.length" class="tree-block">
<div class="tree-title">部门预算</div>
<el-tree
:data="departmentTree"
node-key="id"
:props="treeProps"
highlight-current
default-expand-all
:expand-on-click-node="false"
:current-node-key="pendingId"
@node-click="onNodeClick"
/>
</div>
<div v-if="projectTree.length" class="tree-block" style="margin-top: 16px">
<div class="tree-title">自有账户</div>
<el-tree
:data="projectTree"
node-key="id"
:props="treeProps"
highlight-current
default-expand-all
:expand-on-click-node="false"
:current-node-key="pendingId"
@node-click="onNodeClick"
/>
</div>
<div
v-if="!treeLoading && departmentTree.length === 0 && projectTree.length === 0"
style="text-align: center; color: #909399; padding: 32px 0"
>
暂无数据
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :disabled="!pendingId" @click="confirmPick">
</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import request from "@/utils/request";
import { getBudgetYearOptions, getBudgetDataTree } from "@/api/flow";
export default {
name: "BudgetSourcePickerField",
props: {
fieldName: { type: String, required: true },
value: { type: [String, Number], default: "" },
display: { type: String, default: "" },
},
data() {
return {
dialogVisible: false,
yearLoading: false,
treeLoading: false,
yearOptions: [],
yearId: null,
treeData: [],
pendingId: null,
};
},
computed: {
normalizedValue() {
return this.value === null || this.value === undefined ? "" : String(this.value);
},
hasValue() {
return this.normalizedValue !== "";
},
displayText() {
// 可写且有值时:只展示后端返回的 `{字段名}_display`
// 不再回退到原始 id避免出现 “1年” 这种展示
return this.hasValue ? (this.display || "") : "";
},
treeProps() {
return {
children: "children",
label: "name",
disabled: (data) => !data?.is_leaf,
};
},
departmentTree() {
return (this.treeData || []).filter((n) => n?.budget_type !== "project");
},
projectTree() {
return (this.treeData || []).filter((n) => n?.budget_type === "project");
},
},
methods: {
openDialog() {
this.dialogVisible = true;
},
onDialogOpen() {
this.pendingId = null;
this.ensureInit();
},
onDialogClose() {
this.pendingId = null;
},
async ensureInit() {
if (!this.yearOptions || this.yearOptions.length === 0) {
await this.loadYearOptions();
}
if (!this.yearId && this.yearOptions.length > 0) {
// 尝试根据当前 value 反查 year_id可选否则默认第一项
const fromValue = await this.tryResolveYearIdByValue();
this.yearId = fromValue || this.yearOptions[0].value;
}
if (this.yearId) {
await this.loadTree(this.yearId);
}
},
async loadYearOptions() {
this.yearLoading = true;
try {
const res = await getBudgetYearOptions(false);
const arr = Array.isArray(res) ? res : [];
this.yearOptions = arr.map((y) => ({
value: y.value,
label: y.label || (y.year ? `${y.year}` : `${y.value}`),
year: y.year,
}));
} finally {
this.yearLoading = false;
}
},
async tryResolveYearIdByValue() {
if (!this.hasValue) return null;
try {
const detail = await request({
method: "get",
url: `/api/budget/budget-data/${this.normalizedValue}`,
isLoading: false,
});
const yearId = detail?.year_id ?? null;
return yearId;
} catch (e) {
return null;
}
},
async loadTree(yearId) {
if (!yearId) return;
this.treeLoading = true;
try {
const res = await getBudgetDataTree({ year_id: yearId }, false);
this.treeData = Array.isArray(res) ? res : [];
} finally {
this.treeLoading = false;
}
},
async onYearChange(val) {
this.pendingId = null;
this.treeData = [];
await this.loadTree(val);
},
onNodeClick(node) {
if (!node || !node.is_leaf) return;
this.pendingId = node.id;
},
findPathNames(tree, id, path = []) {
if (!Array.isArray(tree)) return null;
for (const n of tree) {
const nextPath = [...path, n];
if (String(n.id) === String(id)) return nextPath;
if (n.children && n.children.length) {
const found = this.findPathNames(n.children, id, nextPath);
if (found) return found;
}
}
return null;
},
getYearLabel() {
const opt = (this.yearOptions || []).find(
(y) => String(y.value) === String(this.yearId)
);
return opt?.label || (this.yearId ? `${this.yearId}` : "");
},
confirmPick() {
if (!this.pendingId) return;
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 =
leaf?.budget_type === "project"
? "自有账户"
: leaf?.budget_type === "department"
? "部门预算"
: leaf?.budget_type
? String(leaf.budget_type)
: "";
const yearLabel = this.getYearLabel();
const display = [yearLabel, typeText, ...names].filter(Boolean).join(" / ");
// 更新主值与展示值(展示值用于只读/回显)
this.$emit("input", String(this.pendingId));
this.$emit("update:display", display);
this.dialogVisible = false;
this.pendingId = null;
},
clearValue() {
this.$emit("input", "");
this.$emit("update:display", "");
},
},
};
</script>
<style scoped>
.budget-source-field {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.budget-source-label {
color: #606266;
font-size: 14px;
flex: 1;
min-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.budget-source-label--muted {
color: #909399;
}
.tree-title {
font-size: 14px;
font-weight: 600;
color: #606266;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #e4e7ed;
}
</style>