390 lines
9.8 KiB
Vue
390 lines
9.8 KiB
Vue
<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>
|