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

5 months ago
<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>