CaiWuXiTong/resources/assets/js/components/business/TableColumnSettings.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>