248 lines
10 KiB
Vue
248 lines
10 KiB
Vue
<template>
|
|
<div ref="root" class="table-column-settings">
|
|
<button
|
|
type="button"
|
|
class="table-column-settings__trigger"
|
|
:aria-expanded="open ? 'true' : 'false'"
|
|
aria-haspopup="dialog"
|
|
title="设置显示字段和字段顺序"
|
|
@click="open = !open"
|
|
>
|
|
<span class="material-icons-outlined" aria-hidden="true">view_column</span>
|
|
<span>列设置</span>
|
|
</button>
|
|
|
|
<div v-if="open" class="table-column-settings__menu" role="dialog" aria-label="列设置">
|
|
<div class="table-column-settings__head">
|
|
<strong>显示字段</strong>
|
|
<button type="button" class="table-column-settings__reset" @click="reset">
|
|
<span class="material-icons-outlined" aria-hidden="true">restart_alt</span>
|
|
恢复默认
|
|
</button>
|
|
</div>
|
|
<p class="table-column-settings__hint">拖动字段可调整表格和导出的字段顺序</p>
|
|
<div class="table-column-settings__list">
|
|
<div
|
|
v-for="(column, index) in workingColumns"
|
|
:key="column.key"
|
|
class="table-column-settings__item"
|
|
:class="{ 'is-dragging': dragIndex === index }"
|
|
draggable="true"
|
|
@dragstart="startDrag(index, $event)"
|
|
@dragover.prevent
|
|
@drop.prevent="dropAt(index)"
|
|
@dragend="dragIndex = null"
|
|
>
|
|
<span class="material-icons-outlined table-column-settings__drag" aria-hidden="true">drag_indicator</span>
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
:checked="column.visible"
|
|
:disabled="column.fixed || (visibleCount === 1 && column.visible)"
|
|
@change="toggle(column.key)"
|
|
/>
|
|
<span>{{ column.label }}</span>
|
|
</label>
|
|
<span v-if="column.fixed" class="table-column-settings__fixed">固定</span>
|
|
<button type="button" class="table-column-settings__move" :disabled="index === 0" :aria-label="`上移${column.label}`" @click="move(index, -1)"><span class="material-icons-outlined" aria-hidden="true">arrow_upward</span></button>
|
|
<button type="button" class="table-column-settings__move" :disabled="index === workingColumns.length - 1" :aria-label="`下移${column.label}`" @click="move(index, 1)"><span class="material-icons-outlined" aria-hidden="true">arrow_downward</span></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
name: 'TableColumnSettings',
|
|
|
|
props: {
|
|
columns: { type: Array, required: true },
|
|
userKey: { type: [String, Number], required: true },
|
|
pageKey: { type: String, required: true },
|
|
tabKey: { type: String, required: true },
|
|
tableKey: { type: String, required: true },
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
open: false,
|
|
workingColumns: [],
|
|
dragIndex: null,
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
storageKey() {
|
|
return ['caiwu', 'table-columns', 'v1', this.userKey, this.pageKey, this.tabKey, this.tableKey]
|
|
.map((part) => encodeURIComponent(String(part)))
|
|
.join(':');
|
|
},
|
|
visibleCount() {
|
|
return this.workingColumns.filter((column) => column.visible).length;
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
columns: {
|
|
immediate: true,
|
|
deep: true,
|
|
handler() {
|
|
this.restore();
|
|
},
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
document.addEventListener('click', this.onDocumentClick);
|
|
document.addEventListener('keydown', this.onKeydown);
|
|
},
|
|
|
|
beforeDestroy() {
|
|
document.removeEventListener('click', this.onDocumentClick);
|
|
document.removeEventListener('keydown', this.onKeydown);
|
|
},
|
|
|
|
methods: {
|
|
normalizedDefaults() {
|
|
return this.columns.map((column, index) => ({
|
|
...column,
|
|
key: column.key || column.field || `column-${index}`,
|
|
visible: column.visible !== false,
|
|
fixed: column.fixed === true,
|
|
}));
|
|
},
|
|
restore() {
|
|
const defaults = this.normalizedDefaults();
|
|
let saved = null;
|
|
|
|
try {
|
|
saved = JSON.parse(window.localStorage.getItem(this.storageKey));
|
|
} catch (error) {
|
|
saved = null;
|
|
}
|
|
|
|
const savedMap = new Map(Array.isArray(saved) ? saved.filter((item) => item && typeof item.key === 'string').map((item) => [item.key, item]) : []);
|
|
const ordered = defaults
|
|
.map((column, defaultIndex) => ({
|
|
...column,
|
|
visible: column.fixed ? true : (savedMap.has(column.key) ? savedMap.get(column.key).visible !== false : column.visible),
|
|
savedOrder: savedMap.has(column.key) ? Number(savedMap.get(column.key).order) : defaultIndex + defaults.length,
|
|
defaultIndex,
|
|
}))
|
|
.sort((left, right) => left.savedOrder - right.savedOrder || left.defaultIndex - right.defaultIndex)
|
|
.map(({ savedOrder, defaultIndex, ...column }) => column);
|
|
|
|
this.workingColumns = ordered;
|
|
this.emitChange(false);
|
|
},
|
|
toggle(key) {
|
|
const column = this.workingColumns.find((item) => item.key === key);
|
|
if (!column || column.fixed || (column.visible && this.visibleCount === 1)) return;
|
|
column.visible = !column.visible;
|
|
this.emitChange();
|
|
},
|
|
startDrag(index, event) {
|
|
this.dragIndex = index;
|
|
event.dataTransfer.effectAllowed = 'move';
|
|
event.dataTransfer.setData('text/plain', String(index));
|
|
},
|
|
dropAt(index) {
|
|
if (this.dragIndex === null || this.dragIndex === index) return;
|
|
const next = this.workingColumns.slice();
|
|
const [moved] = next.splice(this.dragIndex, 1);
|
|
next.splice(index, 0, moved);
|
|
this.workingColumns = next;
|
|
this.dragIndex = null;
|
|
this.emitChange();
|
|
},
|
|
reset() {
|
|
try { window.localStorage.removeItem(this.storageKey); } catch (_) {}
|
|
this.workingColumns = this.normalizedDefaults();
|
|
this.emitChange(false);
|
|
},
|
|
emitChange(persist = true) {
|
|
if (persist) {
|
|
const value = this.workingColumns.map((column, order) => ({
|
|
key: column.key,
|
|
visible: column.visible,
|
|
order,
|
|
}));
|
|
try { window.localStorage.setItem(this.storageKey, JSON.stringify(value)); } catch (_) {}
|
|
}
|
|
this.$emit('change', this.workingColumns.map((column) => ({ ...column })));
|
|
},
|
|
onDocumentClick(event) {
|
|
if (this.open && this.$refs.root && !this.$refs.root.contains(event.target)) {
|
|
this.open = false;
|
|
}
|
|
},
|
|
move(index, direction) {
|
|
const destination = index + direction;
|
|
if (destination < 0 || destination >= this.workingColumns.length) return;
|
|
const next = this.workingColumns.slice();
|
|
const [column] = next.splice(index, 1);
|
|
next.splice(destination, 0, column);
|
|
this.workingColumns = next;
|
|
this.emitChange();
|
|
},
|
|
onKeydown(event) {
|
|
if (event.key === 'Escape' && this.open) {
|
|
this.open = false;
|
|
this.$refs.root.querySelector('button').focus();
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.table-column-settings { position: relative; }
|
|
.table-column-settings__trigger,
|
|
.table-column-settings__reset {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.375rem;
|
|
height: 2.125rem;
|
|
padding: 0 0.875rem;
|
|
border: 1px solid #d8d2c7;
|
|
border-radius: 0.5rem;
|
|
background: #fff;
|
|
color: #171717;
|
|
font: 500 0.8125rem/1 "JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace;
|
|
cursor: pointer;
|
|
}
|
|
.table-column-settings__trigger:hover,
|
|
.table-column-settings__trigger:focus-visible { border-color: #856632; color: #856632; outline: none; }
|
|
.table-column-settings__trigger .material-icons-outlined { font-size: 1.125rem; }
|
|
.table-column-settings__menu {
|
|
position: absolute;
|
|
top: calc(100% + 0.5rem);
|
|
right: 0;
|
|
z-index: 30;
|
|
width: min(22rem, calc(100vw - 2rem));
|
|
padding: 0.75rem;
|
|
border: 1px solid #d8d2c7;
|
|
border-radius: 0.5rem;
|
|
background: #fff;
|
|
box-shadow: 0 4px 8px rgba(23, 23, 23, 0.1);
|
|
color: #171717;
|
|
}
|
|
.table-column-settings__head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; font-size: 0.8125rem; }
|
|
.table-column-settings__reset { height: 1.875rem; padding: 0 0.5rem; border-color: transparent; color: #856632; font-size: 0.75rem; }
|
|
.table-column-settings__reset .material-icons-outlined { font-size: 1rem; }
|
|
.table-column-settings__hint { margin: 0.375rem 0 0.625rem; color: #595959; font-size: 0.75rem; }
|
|
.table-column-settings__list { max-height: min(24rem, 60vh); overflow-y: auto; border-top: 1px solid #d8d2c7; }
|
|
.table-column-settings__item { display: flex; align-items: center; min-height: 2.25rem; border-bottom: 1px solid #eeeae2; font-size: 0.78125rem; }
|
|
.table-column-settings__item.is-dragging { background: #f7f0de; }
|
|
.table-column-settings__item label { display: flex; flex: 1; align-items: center; gap: 0.5rem; cursor: pointer; }
|
|
.table-column-settings__item input { accent-color: #856632; }
|
|
.table-column-settings__drag { color: #856632; cursor: grab; font-size: 1.125rem; }
|
|
.table-column-settings__fixed { color: #595959; font-size: 0.71875rem; }
|
|
.table-column-settings__move { border: 0; background: transparent; color: #856632; padding: .25rem; cursor: pointer; }
|
|
.table-column-settings__move .material-icons-outlined { font-size: 1rem; }
|
|
.table-column-settings__move:disabled { opacity: .35; cursor: default; }
|
|
.table-column-settings__move:focus-visible { outline: 2px solid #856632; outline-offset: 1px; }
|
|
</style>
|