Files
FastapiAdmin/frontend/web/src/components/tables/fa-table-header/index.vue
T
zhangtao 73f2823692 refactor: 大规模代码整理与功能优化
1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码
2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑
3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件
4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
2026-06-17 01:56:31 +08:00

372 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 表格头部,包含表格大小、刷新、全屏、列设置、其他设置 -->
<template>
<div class="flex-cb max-md:block!" id="fa-table-header">
<div class="flex-wrap">
<slot name="left"></slot>
</div>
<div class="flex-c md:justify-end max-md:mt-3 max-sm:hidden!">
<!-- 搜索区域显示/隐藏:默认展示搜索(未高亮);点按收起后高亮表示当前为隐藏状态 -->
<ElTooltip
v-if="showSearchBar != null"
placement="bottom"
:content="showSearchBar ? t('table.toolbar.hideSearch') : t('table.toolbar.showSearch')"
>
<div
class="button"
@click="search"
:class="!showSearchBar ? 'active bg-theme! hover:bg-theme/80!' : ''"
>
<FaSvgIcon icon="ri:search-line" :class="!showSearchBar ? 'text-white' : 'text-g-700'" />
</div>
</ElTooltip>
<!-- 刷新 -->
<div
v-if="shouldShow('refresh')"
class="button"
@click="refresh"
:class="{ loading: loading && isManualRefresh }"
>
<FaSvgIcon
icon="ri:refresh-line"
:class="loading && isManualRefresh ? 'animate-spin text-g-600' : ''"
/>
</div>
<!-- 表格大小 -->
<ElDropdown v-if="shouldShow('size')" @command="handleTableSizeChange">
<div class="button">
<FaSvgIcon icon="ri:arrow-up-down-fill" />
</div>
<template #dropdown>
<ElDropdownMenu>
<div
v-for="item in tableSizeOptions"
:key="item.value"
class="table-size-btn-item [&_.el-dropdown-menu__item]:mb-[3px]! last:[&_.el-dropdown-menu__item]:mb-0!"
>
<ElDropdownItem
:key="item.value"
:command="item.value"
:class="tableSize === item.value ? 'bg-g-300/55!' : ''"
>
{{ item.label }}
</ElDropdownItem>
</div>
</ElDropdownMenu>
</template>
</ElDropdown>
<!-- 全屏 -->
<div v-if="shouldShow('fullscreen')" class="button" @click="toggleFullScreen">
<FaSvgIcon :icon="isFullScreen ? 'ri:fullscreen-exit-line' : 'ri:fullscreen-line'" />
</div>
<!-- 行拖拽排序 -->
<ElTooltip
v-if="shouldShow('rowDrag')"
placement="bottom"
:content="isRowDrag ? t('table.toolbar.disableRowDrag') : t('table.toolbar.enableRowDrag')"
>
<div
class="button"
@click="toggleRowDrag"
:class="isRowDrag ? 'active bg-theme! hover:bg-theme/80!' : ''"
>
<FaSvgIcon icon="ri:drag-move-line" :class="isRowDrag ? 'text-white' : 'text-g-700'" />
</div>
</ElTooltip>
<!-- 列设置 -->
<ElPopover v-if="shouldShow('columns')" placement="bottom" trigger="click">
<template #reference>
<div class="button">
<FaSvgIcon icon="ri:align-right" />
</div>
</template>
<div>
<ElScrollbar max-height="380px">
<VueDraggable
v-model="columns"
:disabled="false"
filter=".fixed-column"
:prevent-on-filter="false"
@move="checkColumnMove"
>
<div
v-for="item in columns"
:key="item.prop || item.type"
class="column-option flex-c"
:class="{ 'fixed-column': item.fixed }"
>
<div
class="drag-icon mr-2 h-4.5 flex-cc text-g-500"
:class="item.fixed ? 'cursor-default text-g-300' : 'cursor-move'"
>
<FaSvgIcon
:icon="item.fixed ? 'ri:unpin-line' : 'ri:drag-move-2-fill'"
class="text-base"
/>
</div>
<ElCheckbox
:model-value="getColumnVisibility(item)"
@update:model-value="(val) => updateColumnVisibility(item, val)"
:disabled="item.disabled"
class="flex-1 min-w-0 [&_.el-checkbox__label]:overflow-hidden [&_.el-checkbox__label]:text-ellipsis [&_.el-checkbox__label]:whitespace-nowrap"
>
{{ item.label || (item.type === "selection" ? t("table.selection") : "") }}
</ElCheckbox>
</div>
</VueDraggable>
</ElScrollbar>
</div>
</ElPopover>
<!-- 其他设置 -->
<ElPopover v-if="shouldShow('settings')" placement="bottom" trigger="click">
<template #reference>
<div class="button">
<FaSvgIcon icon="ri:settings-line" />
</div>
</template>
<div class="flex min-w-[200px] flex-col gap-2">
<ElCheckbox v-model="isZebra" :value="true">
{{ t("table.zebra") }}
</ElCheckbox>
<ElCheckbox v-model="isBorder" :value="true">
{{ t("table.border") }}
</ElCheckbox>
<ElCheckbox v-model="isHeaderBackground" :value="true">
{{ t("table.headerBackground") }}
</ElCheckbox>
<ElCheckbox v-model="highlightCurrentRow" :value="true">
{{ t("table.highlightCurrentRow") }}
</ElCheckbox>
</div>
</ElPopover>
<slot name="right"></slot>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, onMounted, onUnmounted } from "vue";
import { storeToRefs } from "pinia";
import { TableSizeEnum } from "@/enums/formEnum";
import { useTableStore } from "@stores";
import { VueDraggable } from "vue-draggable-plus";
import { useI18n } from "vue-i18n";
import type { ColumnOption } from "@/types/component";
import { ElScrollbar } from "element-plus";
defineOptions({ name: "FaTableHeader" });
// 显式声明插槽类型
defineSlots<{
default?: () => void;
left?: () => void;
right?: () => void;
}>();
const { t } = useI18n();
interface Props {
/** 全屏 class */
fullClass?: string;
/** 组件布局,子组件名用逗号分隔 */
layout?: string;
/** 加载中 */
loading?: boolean;
/** 搜索栏显示状态 */
showSearchBar?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
fullClass: "fa-full-height",
layout: "search,refresh,size,fullscreen,columns,rowDrag,settings",
showSearchBar: undefined,
});
const columns = defineModel<ColumnOption[]>("columns", {
required: false,
default: () => [],
});
interface Emits {
refresh: [];
search: [];
"update:showSearchBar": [value: boolean];
}
const emit = defineEmits<Emits>();
/**
* 获取列的显示状态
* 优先使用 visible 字段,如果不存在则使用 checked 字段
*/
const getColumnVisibility = (col: ColumnOption): boolean => {
if (col.visible !== undefined) {
return col.visible;
}
return col.checked ?? true;
};
/**
* 更新列的显示状态
* 同时更新 checked 和 visible 字段以保持兼容性
*/
const updateColumnVisibility = (col: ColumnOption, value: boolean | string | number): void => {
const boolValue = !!value;
col.checked = boolValue;
col.visible = boolValue;
};
/** 表格大小选项配置 */
const tableSizeOptions = [
{ value: TableSizeEnum.SMALL, label: t("table.sizeOptions.small") },
{ value: TableSizeEnum.DEFAULT, label: t("table.sizeOptions.default") },
{ value: TableSizeEnum.LARGE, label: t("table.sizeOptions.large") },
];
const tableStore = useTableStore();
const { tableSize, isZebra, isBorder, isHeaderBackground, isRowDrag, highlightCurrentRow } =
storeToRefs(tableStore);
const toggleRowDrag = () => {
tableStore.setIsRowDrag(!isRowDrag.value);
};
/** 解析 layout 属性,转换为数组 */
const layoutItems = computed(() => {
return props.layout.split(",").map((item) => item.trim());
});
/**
* 检查组件是否应该显示
* @param componentName 组件名称
* @returns 是否显示
*/
const shouldShow = (componentName: string) => {
return layoutItems.value.includes(componentName);
};
/**
* 拖拽移动事件处理 - 防止固定列位置改变
* @param evt move事件对象
* @returns 是否允许移动
*/
const checkColumnMove = (event: any) => {
// 拖拽进入的目标 DOM 元素
const toElement = event.related as HTMLElement;
// 如果目标位置是 fixed 列,则不允许移动
if (toElement && toElement.classList.contains("fixed-column")) {
return false;
}
return true;
};
/** 搜索事件处理 */
const search = () => {
// 切换搜索栏显示状态
emit("update:showSearchBar", !props.showSearchBar);
emit("search");
};
/** 刷新事件处理 */
const refresh = () => {
isManualRefresh.value = true;
emit("refresh");
};
/**
* 表格大小变化处理
* @param command 表格大小枚举值
*/
const handleTableSizeChange = (command: TableSizeEnum) => {
useTableStore().setTableSize(command);
};
/** 是否手动点击刷新 */
const isManualRefresh = ref(false);
/** 加载中 */
const isFullScreen = ref(false);
/** 保存原始的 overflow 样式,用于退出全屏时恢复 */
const originalOverflow = ref("");
/**
* 切换全屏状态
* 进入全屏时会隐藏页面滚动条,退出时恢复原状态
*/
const toggleFullScreen = () => {
const el = document.querySelector(`.${props.fullClass}`);
if (!el) return;
isFullScreen.value = !isFullScreen.value;
if (isFullScreen.value) {
// 进入全屏:保存原始样式并隐藏滚动条
originalOverflow.value = document.body.style.overflow;
document.body.style.overflow = "hidden";
el.classList.add("el-full-screen");
tableStore.setIsFullScreen(true);
} else {
// 退出全屏:恢复原始样式
document.body.style.overflow = originalOverflow.value;
el.classList.remove("el-full-screen");
tableStore.setIsFullScreen(false);
}
};
/**
* ESC键退出全屏的事件处理器
* 需要保存引用以便在组件卸载时正确移除监听器
*/
const handleEscapeKey = (e: KeyboardEvent) => {
if (e.key === "Escape" && isFullScreen.value) {
toggleFullScreen();
}
};
/** 组件挂载时注册全局事件监听器 */
onMounted(() => {
document.addEventListener("keydown", handleEscapeKey);
});
/** 组件卸载时清理资源 */
onUnmounted(() => {
// 移除事件监听器
document.removeEventListener("keydown", handleEscapeKey);
// 如果组件在全屏状态下被卸载,恢复页面滚动状态
if (isFullScreen.value) {
document.body.style.overflow = originalOverflow.value;
const el = document.querySelector(`.${props.fullClass}`);
if (el) {
el.classList.remove("el-full-screen");
}
}
});
</script>
<style scoped>
@reference '@styles/core/tailwind.css';
.button {
@apply ml-2
size-8
flex
items-center
justify-center
cursor-pointer
rounded-md
bg-g-300/55
dark:bg-g-300/40
text-g-700
hover:bg-g-300
md:ml-0
md:mr-2.5;
}
</style>