|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
|