This commit is contained in:
张成
2025-10-08 15:10:33 +08:00
commit 2e1cd65b07
161 changed files with 19936 additions and 0 deletions

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

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

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

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

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