init
This commit is contained in:
24
src/components/treeGrid/component/renderCol.vue
Normal file
24
src/components/treeGrid/component/renderCol.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'RenderCol',
|
||||
props: {
|
||||
col: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
param: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
render(h) {
|
||||
try {
|
||||
const content = this.col(h, this.param)
|
||||
return <div class="text-center">{content}</div>
|
||||
} catch (e) {
|
||||
// 渲染失败时提供一个容错显示
|
||||
return <div class="text-center">-</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
23
src/components/treeGrid/component/subColmns.vue
Normal file
23
src/components/treeGrid/component/subColmns.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<colgroup>
|
||||
<col v-for="(col, index) in columns" :key="col.key || index" :style="colStyle(col)" />
|
||||
</colgroup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SubColmns',
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
colStyle(col) {
|
||||
const width = col && col.width ? col.width : '150px'
|
||||
return { width }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
31
src/components/treeGrid/component/subThead.vue
Normal file
31
src/components/treeGrid/component/subThead.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<thead class="ivu-table-header">
|
||||
<tr>
|
||||
<th v-for="(col, index) in columns" :key="col.key || index" :style="headerStyle(col)">
|
||||
<div class="ivu-table-cell">
|
||||
<span>{{ col.title }}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SubThead',
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
headerStyle(col) {
|
||||
const align = col && col.align ? col.align : 'center'
|
||||
return { textAlign: align }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
341
src/components/treeGrid/component/subTreeGrid.vue
Normal file
341
src/components/treeGrid/component/subTreeGrid.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<table cellspacing="0" width="100%" cellpadding="0" border="0">
|
||||
<SubColmns :columns="columns"></SubColmns>
|
||||
<tbody class="endTbody" v-for="(row, index) in subData" :key="row.id">
|
||||
|
||||
<tr class="tr-row" v-if="row">
|
||||
<td v-for="(col, sonColIndex) in columns"
|
||||
:key="sonColIndex"
|
||||
:class="[
|
||||
{'td-expand': sonColIndex === 0},
|
||||
{'td-operations': col.type === 'operation'},
|
||||
col.className
|
||||
]"
|
||||
:style="{
|
||||
width: col.width || 'auto',
|
||||
minWidth: col.minWidth || (sonColIndex === 0 ? '200px' : '100px')
|
||||
}"
|
||||
>
|
||||
<RenderCol v-if="col.render" :col="col.render" :param="{ row, index, col }" />
|
||||
<template v-else>
|
||||
<div class="first-box" v-if="sonColIndex == 1">
|
||||
<div class="col-row expand-row" v-if="row.children && row.children.length > 0" @click="expander(row)">
|
||||
<span class="indent-span" :style="{width:`${row.indentWidth}px`}"></span>
|
||||
<i :class="['ivu-icon', 'link', 'ivu-icon-' + row.indexIcon]" aria-hidden="true"></i>
|
||||
<label>{{ row[col.key] }}</label>
|
||||
</div>
|
||||
<div class="col-row" v-else>
|
||||
<span class="indent-span" :style="{width:`${row.indentWidth}px`}"></span>
|
||||
<label>{{ row[col.key] }}</label>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<label v-else>{{ row[col.key] }}</label>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-show="row._expand" class="treegrid-tr-tree ivu-table-row">
|
||||
<th :colspan="columns.length" class="non-right ">
|
||||
<SubTreeGrid :columns="columns" :data="row['children']" :grade="grade + 1" />
|
||||
</th>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SubTreeGrid from './subTreeGrid.vue'
|
||||
import SubColmns from './subColmns.vue'
|
||||
import RenderCol from './renderCol.vue'
|
||||
export default {
|
||||
name: 'SubTreeGrid',
|
||||
props: ['columns', 'data', 'grade'],
|
||||
components: {
|
||||
RenderCol,
|
||||
SubTreeGrid,
|
||||
SubColmns
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
subData: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.setData()
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler() {
|
||||
this.setData()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 本地存储 key,用于持久化展开状态
|
||||
_expandStoreKey() {
|
||||
return 'treegrid_expand_map_v1'
|
||||
},
|
||||
|
||||
loadExpandMap() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this._expandStoreKey())
|
||||
return raw ? JSON.parse(raw) : {}
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
|
||||
saveExpandMap(map) {
|
||||
try {
|
||||
localStorage.setItem(this._expandStoreKey(), JSON.stringify(map))
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
// 递归收集当前 subData 的展开状态(用于在 data 更新时保留)
|
||||
collectExpandMapFromList(list, map) {
|
||||
if (!list || !list.length) return
|
||||
list.forEach((r) => {
|
||||
const id = r.id || r.timeKey
|
||||
if (id !== undefined) map[id] = !!r._expand
|
||||
if (r.children && r.children.length) this.collectExpandMapFromList(r.children, map)
|
||||
})
|
||||
},
|
||||
|
||||
setData() {
|
||||
if (this.data && this.data.length > 0) {
|
||||
// 优先从本地存储读取上次的展开状态
|
||||
const storedMap = this.loadExpandMap()
|
||||
|
||||
// 也从当前内存 subData 中读取(以便在快速编辑时保留)
|
||||
const currentMap = {}
|
||||
this.collectExpandMapFromList(this.subData, currentMap)
|
||||
|
||||
let { expand } = this.$attrs
|
||||
|
||||
const subData = this.data.map((p) => {
|
||||
// 不直接修改传入对象,复制一份以避免副作用
|
||||
const row = Object.assign({}, p)
|
||||
|
||||
const id = row.id || row.timeKey
|
||||
|
||||
// 优先使用 currentMap(内存),其次使用本地存储,最后使用传入的默认 expand
|
||||
if (id !== undefined && currentMap.hasOwnProperty(id)) {
|
||||
row._expand = !!currentMap[id]
|
||||
} else if (id !== undefined && storedMap.hasOwnProperty(id)) {
|
||||
row._expand = !!storedMap[id]
|
||||
} else {
|
||||
row._expand = expand
|
||||
}
|
||||
|
||||
// 保留已有 timeKey,若不存在则创建
|
||||
row.timeKey = row.timeKey || new Date().getTime()
|
||||
row.indexIcon = row._expand ? 'md-remove' : 'md-add'
|
||||
row.indentWidth = (this.grade - 1) * 30
|
||||
// 递归处理 children(保留结构,不展开子项的数据填充在子组件中)
|
||||
if (row.children && row.children.length) {
|
||||
// children 内容将在子组件中处理,不在此处深拷贝以节约性能
|
||||
}
|
||||
return row
|
||||
})
|
||||
|
||||
// 保存当前展开状态
|
||||
const newMap = {}
|
||||
this.collectExpandMapFromList(subData, newMap)
|
||||
// 合并之前的存储(优先保留 newMap 中的值)
|
||||
const merged = Object.assign({}, storedMap, newMap)
|
||||
this.saveExpandMap(merged)
|
||||
|
||||
this.subData = subData
|
||||
} else {
|
||||
this.subData = []
|
||||
}
|
||||
},
|
||||
|
||||
expander(row) {
|
||||
console.log('expander called:', {
|
||||
id: row.id,
|
||||
timeKey: row.timeKey,
|
||||
children: row.children,
|
||||
currentExpand: row._expand,
|
||||
icon: row.indexIcon
|
||||
});
|
||||
|
||||
// use Vue.set for reactivity
|
||||
const newExpand = !row._expand;
|
||||
this.$set(row, '_expand', newExpand);
|
||||
this.$set(row, 'indexIcon', newExpand ? 'md-remove' : 'md-add');
|
||||
|
||||
// 更新本地存储状态
|
||||
const map = this.loadExpandMap();
|
||||
const id = row.id || row.timeKey;
|
||||
if (id !== undefined) {
|
||||
map[id] = newExpand;
|
||||
this.saveExpandMap(map);
|
||||
}
|
||||
|
||||
// 打印展开后的状态
|
||||
console.log('expander updated:', {
|
||||
id: id,
|
||||
newExpand: newExpand,
|
||||
newIcon: row.indexIcon,
|
||||
storedState: map[id]
|
||||
});
|
||||
|
||||
// 确保视图更新
|
||||
this.$nextTick(() => {
|
||||
console.log('After nextTick:', {
|
||||
element: document.querySelector(`.tr-row[data-id="${id}"] .expand-row`),
|
||||
expandState: row._expand,
|
||||
icon: row.indexIcon
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/* 表格容器样式 */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* 确保表格可以在容器内滚动 */
|
||||
.endTbody {
|
||||
max-height: calc(100vh - 200px); /* 预留头部和其他元素的空间 */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #2d8cf0;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.expand-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(45,140,240,0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.fallback-icon {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
background: rgba(45,140,240,0.08);
|
||||
color: #2d8cf0;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tr-row {
|
||||
padding: 0px 5px;
|
||||
td {
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.first-box {
|
||||
padding: 0px 10px;
|
||||
|
||||
.col-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
.indent-span {
|
||||
display: inline-block;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.non-right {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* 表格基础样式 */
|
||||
.tr-row {
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
td {
|
||||
position: relative;
|
||||
padding: 12px 8px;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
|
||||
/* 基础文本溢出处理 */
|
||||
&:not(.td-operations) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
/* 展开列样式 */
|
||||
.td-expand {
|
||||
width: auto;
|
||||
min-width: 200px; /* 给展开图标和文本预留足够空间 */
|
||||
}
|
||||
|
||||
/* 操作列样式 */
|
||||
.td-operations {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
min-width: 180px; /* 增加按钮空间 */
|
||||
padding-right: 16px;
|
||||
overflow: visible !important; /* 强制确保内容可见 */
|
||||
position: relative;
|
||||
z-index: 1; /* 确保按钮在最上层 */
|
||||
}
|
||||
|
||||
/* 确保操作列中的按钮组样式 */
|
||||
.td-operations .ivu-btn-group {
|
||||
display: inline-flex;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 确保单个按钮样式 */
|
||||
.td-operations .ivu-btn {
|
||||
display: inline-block;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
54
src/components/treeGrid/index.vue
Normal file
54
src/components/treeGrid/index.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="ivu-table-wrapper">
|
||||
<div class="ivu-table ivu-table-border">
|
||||
<table cellspacing="0" width="100%" cellpadding="0" border="0">
|
||||
<SubColmns :columns="columns"></SubColmns>
|
||||
<subTheads :columns="columns"></subTheads>
|
||||
|
||||
<tr class="ivu-table-tbody">
|
||||
<td style="width: 100%;" :colspan="columns.length" class="non-right">
|
||||
<SubTreeGrid :columns="columns" :data="data" :grade="1" v-bind="$attrs" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SubTreeGrid from './component/subTreeGrid.vue'
|
||||
import SubColmns from './component/subColmns.vue'
|
||||
import subTheads from './component/subThead.vue'
|
||||
|
||||
export default {
|
||||
props: ['columns', 'data'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
components: {
|
||||
SubTreeGrid,
|
||||
SubColmns,
|
||||
subTheads
|
||||
},
|
||||
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.ivu-table {
|
||||
/* 限制最大宽度,超出显示横向滚动 */
|
||||
overflow: auto !important;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.ivu-table-tbody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.non-right {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user