Files
zyt/admin/src/views/finance/mubiao-dept-node.vue
T
2026-04-30 14:04:12 +08:00

390 lines
9.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- 根节点section 分组展示 -->
<template v-if="depth === 0">
<div class="group">
<div class="group__header">
<div class="group__accent" />
<div class="group__name">
<el-icon class="group__name-icon"><FolderOpened /></el-icon>
{{ node.dept_name }}
</div>
<div class="group__amount-wrap">
<el-input-number
v-model="node.target_amount"
:min="0"
:precision="2"
:step="1000"
controls-position="right"
placeholder="0.00"
class="group__amount-input"
/>
</div>
<div class="group__remark-wrap">
<el-input
v-model="node.remark"
maxlength="255"
placeholder="备注(选填)"
clearable
/>
</div>
<div class="group__badge-wrap">
<el-tag v-if="node.target_id" size="small" type="success" effect="plain" round>已保存</el-tag>
<span v-else class="group__unsaved-dot" />
</div>
</div>
<div v-if="node.children?.length" class="group__body">
<MubiaoDeptNode
v-for="c in node.children"
:key="c.dept_id"
:node="c"
:depth="1"
/>
</div>
</div>
</template>
<!-- 子节点表格行 -->
<template v-else>
<div class="row" :data-depth="String(Math.min(depth, 4))">
<div class="row__indent" :style="{ width: `${(depth - 1) * 20 + 16}px` }">
<div class="row__connector" />
</div>
<div class="row__dept">
<div class="row__dot" />
<div class="row__dept-text">
<span class="row__name">{{ node.dept_name }}</span>
<span v-if="parentPath" class="row__path">{{ parentPath }}</span>
</div>
<el-tag v-if="node.target_id" size="small" type="success" effect="plain" round class="row__saved">已保存</el-tag>
</div>
<div class="row__amount">
<el-input-number
v-model="node.target_amount"
:min="0"
:precision="2"
:step="1000"
controls-position="right"
placeholder="0.00"
class="row__amount-input"
/>
</div>
<div class="row__remark">
<el-input
v-model="node.remark"
maxlength="255"
placeholder="备注(选填)"
clearable
/>
</div>
</div>
<MubiaoDeptNode
v-for="c in node.children"
:key="c.dept_id"
:node="c"
:depth="depth + 1"
/>
</template>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { FolderOpened } from '@element-plus/icons-vue'
defineOptions({ name: 'MubiaoDeptNode' })
export interface DeptTargetRow {
dept_id: number
dept_name: string
dept_path: string
target_id: number
target_amount: number
remark: string
children?: DeptTargetRow[]
}
const props = defineProps<{
node: DeptTargetRow
depth: number
}>()
const segments = computed(() =>
props.node.dept_path.split(/\s*\/\s*/).filter(Boolean)
)
const parentPath = computed(() =>
segments.value.slice(0, -1).join(' ')
)
</script>
<style lang="scss" scoped>
/* ── 根节点:分组区块 ── */
.group {
border-bottom: 1px solid var(--el-border-color-lighter);
&:last-child {
border-bottom: none;
}
}
.group__header {
display: flex;
align-items: center;
gap: 0;
min-height: 54px;
background: linear-gradient(90deg,
color-mix(in srgb, var(--el-color-primary) 6%, var(--el-bg-color)) 0%,
color-mix(in srgb, var(--el-color-primary) 2%, var(--el-bg-color)) 50%,
var(--el-bg-color) 100%
);
border-bottom: 1px solid color-mix(in srgb, var(--el-color-primary) 12%, var(--el-border-color-lighter));
padding-right: 16px;
cursor: default;
transition: background 0.18s ease;
&:hover {
background: linear-gradient(90deg,
color-mix(in srgb, var(--el-color-primary) 10%, var(--el-bg-color)) 0%,
color-mix(in srgb, var(--el-color-primary) 4%, var(--el-bg-color)) 50%,
var(--el-fill-color-extra-light) 100%
);
}
}
.group__accent {
flex-shrink: 0;
width: 4px;
align-self: stretch;
background: linear-gradient(180deg, var(--el-color-primary), var(--el-color-primary-light-3));
border-radius: 0 2px 2px 0;
margin-right: 12px;
}
.group__name {
flex: 0 0 auto;
width: 220px;
font-size: 13.5px;
font-weight: 700;
color: var(--el-color-primary-dark-2);
padding-right: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 6px;
}
.group__name-icon {
font-size: 15px;
opacity: 0.7;
flex-shrink: 0;
}
.group__amount-wrap {
flex: 0 0 194px;
border-left: 1px solid color-mix(in srgb, var(--el-color-primary) 15%, var(--el-border-color-lighter));
padding: 9px 12px;
}
.group__amount-input {
width: 170px;
:deep(.el-input__wrapper) {
border-radius: 7px;
background: var(--el-bg-color);
box-shadow: 0 0 0 1.5px var(--el-color-primary-light-5) inset,
0 1px 3px rgba(0, 0, 0, 0.06);
transition: box-shadow 0.15s;
&:hover {
box-shadow: 0 0 0 1.5px var(--el-color-primary-light-3) inset,
0 2px 6px rgba(0, 0, 0, 0.08);
}
&.is-focus {
box-shadow: 0 0 0 2px var(--el-color-primary-light-3) inset !important;
}
}
}
.group__remark-wrap {
flex: 1;
border-left: 1px solid color-mix(in srgb, var(--el-color-primary) 15%, var(--el-border-color-lighter));
padding: 9px 12px;
}
.group__badge-wrap {
flex: 0 0 60px;
display: flex;
justify-content: center;
align-items: center;
}
.group__unsaved-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--el-border-color);
display: block;
}
.group__body {
background: var(--el-bg-color);
}
/* ── 子节点:表格行 ── */
.row {
display: flex;
align-items: center;
min-height: 46px;
border-bottom: 1px solid var(--el-border-color-lighter);
transition: background 0.12s ease;
padding-right: 16px;
cursor: default;
&:last-child {
border-bottom: none;
}
&:hover {
background: var(--el-fill-color-extra-light);
.row__dot {
transform: scale(1.3);
}
}
/* 深度色阶 */
&[data-depth='1'] {
--dot: #67c23a;
--connector: color-mix(in srgb, #67c23a 30%, var(--el-border-color));
}
&[data-depth='2'] {
--dot: #e6a23c;
--connector: color-mix(in srgb, #e6a23c 30%, var(--el-border-color));
}
&[data-depth='3'] {
--dot: #909399;
--connector: var(--el-border-color);
}
&[data-depth='4'] {
--dot: var(--el-border-color-darker);
--connector: var(--el-border-color-light);
}
}
.row__indent {
flex-shrink: 0;
position: relative;
align-self: stretch;
min-width: 16px;
}
.row__connector {
position: absolute;
left: calc(100% - 2px);
top: 0;
bottom: 50%;
width: 10px;
border-left: 1.5px dashed var(--connector, var(--el-border-color));
border-bottom: 1.5px dashed var(--connector, var(--el-border-color));
border-radius: 0 0 0 4px;
}
.row__dept {
flex: 0 0 auto;
width: 220px;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px 8px 8px;
}
.row__dot {
flex-shrink: 0;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--dot, #909399);
transition: transform 0.15s ease;
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dot, #909399) 20%, transparent);
}
.row__dept-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 1px;
}
.row__name {
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row__path {
font-size: 11px;
color: var(--el-text-color-placeholder);
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row__saved {
flex-shrink: 0;
}
.row__amount {
flex: 0 0 194px;
border-left: 1px solid var(--el-border-color-lighter);
border-right: 1px solid var(--el-border-color-lighter);
padding: 8px 12px;
}
.row__amount-input {
width: 170px;
:deep(.el-input__wrapper) {
border-radius: 7px;
transition: box-shadow 0.15s;
&:hover {
box-shadow: 0 0 0 1px var(--el-color-primary-light-5) inset;
}
}
}
.row__remark {
flex: 1;
padding: 8px 0 8px 12px;
}
@media (max-width: 900px) {
.group__header,
.row {
flex-wrap: wrap;
padding-bottom: 10px;
}
.group__name,
.row__dept { width: 100%; }
.group__amount-wrap,
.row__amount { border: none; flex: 0 0 auto; }
.group__remark-wrap,
.row__remark { flex: 1 1 180px; border: none; }
.group__amount-input,
.row__amount-input { width: 160px; }
}
</style>