mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-30 16:03:50 +00:00
feat: 移除系统管理路由并优化路由结构
- 删除不再使用的系统管理路由模块 - 更新路由配置,确保仅保留必要的模块 - 添加Element Plus图标全局注册功能 - 优化分页组件,增加更多属性支持 - 重构表格操作列渲染逻辑,提升代码可读性和复用性 - 更新多个组件以支持新的操作列渲染方式
This commit is contained in:
@@ -6,8 +6,11 @@
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:background="background"
|
||||
:disabled="disabled"
|
||||
:layout="layout"
|
||||
:page-sizes="pageSizes"
|
||||
:pager-count="pagerCount"
|
||||
:size="size"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@@ -17,6 +20,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, type PropType } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
total: {
|
||||
type: Number as PropType<number>,
|
||||
@@ -25,7 +30,7 @@ const props = defineProps({
|
||||
pageSizes: {
|
||||
type: Array as PropType<number[]>,
|
||||
default() {
|
||||
return [10, 20, 30, 50];
|
||||
return [10, 20, 30, 50, 100];
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
@@ -36,6 +41,20 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 页码按钮数量(透传 ElPagination) */
|
||||
pagerCount: {
|
||||
type: Number as PropType<number>,
|
||||
default: undefined,
|
||||
},
|
||||
/** 分页器尺寸(透传 ElPagination) */
|
||||
size: {
|
||||
type: String as PropType<"" | "default" | "small" | "large">,
|
||||
default: undefined,
|
||||
},
|
||||
autoScroll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
|
||||
<!-- 渲染展开行 -->
|
||||
<ElTableColumn v-else-if="col.type === 'expand'" v-bind="cleanColumnProps(col)">
|
||||
<template #default="{ row }">
|
||||
<component :is="col.formatter ? col.formatter(row) : null" />
|
||||
<template #default="{ row: expandRow }">
|
||||
<component :is="col.formatter ? col.formatter(expandRow) : null" />
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<!-- 渲染普通列 -->
|
||||
<ElTableColumn v-else v-bind="cleanColumnProps(col)">
|
||||
<!-- 渲染普通列:default 插槽须紧凑书写,避免空白文本抢占 formatter(见 renderColumnFormatter 注释) -->
|
||||
<ElTableColumn v-else v-bind="cleanBodyColumnProps(col)">
|
||||
<template v-if="col.useHeaderSlot && col.prop" #header="headerScope">
|
||||
<slot
|
||||
:name="col.headerSlotName || `${col.prop}-header`"
|
||||
@@ -30,17 +30,9 @@
|
||||
{{ col.label }}
|
||||
</slot>
|
||||
</template>
|
||||
<template v-if="col.useSlot && col.prop" #default="slotScope">
|
||||
<slot
|
||||
v-if="shouldRenderSlotScope(slotScope)"
|
||||
:name="col.slotName || col.prop"
|
||||
v-bind="{
|
||||
...slotScope,
|
||||
prop: col.prop,
|
||||
value: col.prop ? slotScope.row[col.prop] : undefined,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<!-- 整段单行:插槽内兄弟节点之间的空白文本会让 EP 误判单元格内容 -->
|
||||
<!-- eslint-disable-next-line vue/max-attributes-per-line, prettier/prettier -->
|
||||
<template #default="slotScope"><slot v-if="col.useSlot && col.prop && shouldRenderSlotScope(slotScope)" :name="col.slotName || col.prop" v-bind="{ ...slotScope, prop: col.prop, value: col.prop ? slotScope.row[col.prop] : undefined }" /><TableFormatterOutlet v-else-if="col.formatter && !col.useSlot && shouldRenderSlotScope(slotScope)" :column="col" :record="slotScope.row" /></template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
|
||||
@@ -58,21 +50,37 @@
|
||||
:class="mergedPaginationOptions?.align"
|
||||
ref="paginationRef"
|
||||
>
|
||||
<ElPagination
|
||||
v-bind="mergedPaginationOptions"
|
||||
:total="pagination?.total"
|
||||
:disabled="loading"
|
||||
:page-size="pagination?.size"
|
||||
:current-page="pagination?.current"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
<Pagination
|
||||
v-if="pagination"
|
||||
:page="pagination.current"
|
||||
:limit="pagination.size"
|
||||
:total="pagination.total"
|
||||
:page-sizes="mergedPaginationOptions.pageSizes"
|
||||
:layout="mergedPaginationOptions.layout"
|
||||
:background="mergedPaginationOptions.background ?? true"
|
||||
:disabled="!!loading"
|
||||
:hidden="paginationHidden"
|
||||
:pager-count="mergedPaginationOptions.pagerCount"
|
||||
:size="mergedPaginationOptions.size"
|
||||
@pagination="handlePaginationEvent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, watchEffect, getCurrentInstance, useAttrs } from "vue";
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
nextTick,
|
||||
watchEffect,
|
||||
getCurrentInstance,
|
||||
useAttrs,
|
||||
isVNode,
|
||||
h,
|
||||
defineComponent,
|
||||
type PropType,
|
||||
} from "vue";
|
||||
import type { ElTable, TableProps } from "element-plus";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { ColumnOption } from "@/types";
|
||||
@@ -80,6 +88,7 @@ import { useTableStore } from "@/store/modules/table.store";
|
||||
import { useCommon } from "@/hooks/core/useCommon";
|
||||
import { useTableHeight } from "@/hooks/core/useTableHeight";
|
||||
import { useResizeObserver, useWindowSize } from "@vueuse/core";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
|
||||
defineOptions({ name: "ArtTable" });
|
||||
|
||||
@@ -183,6 +192,17 @@ const mergedPaginationOptions = computed(() => ({
|
||||
...props.paginationOptions,
|
||||
}));
|
||||
|
||||
/** 对齐 ElPagination hide-on-single-page,交给封装组件的 hidden */
|
||||
const paginationHidden = computed(() => {
|
||||
const p = props.pagination;
|
||||
const opts = mergedPaginationOptions.value;
|
||||
if (!p || !opts.hideOnSinglePage) return false;
|
||||
const size = p.size || 10;
|
||||
const total = p.total ?? 0;
|
||||
if (total <= 0) return false;
|
||||
return Math.ceil(total / size) <= 1;
|
||||
});
|
||||
|
||||
// 边框 (优先级:props > store)
|
||||
const border = computed(() => props.border ?? isBorder.value);
|
||||
// 斑马纹
|
||||
@@ -269,6 +289,11 @@ const mergedTableProps = computed(() => ({
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "pagination:size-change", val: number): void;
|
||||
(e: "pagination:current-change", val: number): void;
|
||||
}>();
|
||||
|
||||
// 是否显示分页器
|
||||
const showPagination = computed(() => props.pagination && !isEmpty.value);
|
||||
|
||||
@@ -278,6 +303,36 @@ const shouldRenderSlotScope = (slotScope: { $index?: number }) => {
|
||||
return slotScope.$index === undefined || slotScope.$index >= 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* ElTableColumn 若存在 default 插槽且插槽产物含任意非 Comment 的 vnode(含空白文本节点),
|
||||
* 将不会执行 formatter(见 element-plus render-helper setColumnRenders)。
|
||||
* ArtTable 中 ElTableColumn 与子节点之间的换行/缩进可能被编译进默认插槽,导致 formatter(如操作列里的按钮)永远不渲染。
|
||||
* 对声明了 formatter 且未使用 useSlot 的列,在此显式渲染 formatter 返回值。
|
||||
*/
|
||||
const renderColumnFormatter = (col: ColumnOption, row: Record<string, unknown>) => {
|
||||
if (!col.formatter) return null;
|
||||
const result = col.formatter(row as never);
|
||||
if (isVNode(result)) return result;
|
||||
if (result === null || result === undefined) return null;
|
||||
return h("span", String(result));
|
||||
};
|
||||
|
||||
/**
|
||||
* 在 render 里调用 formatter(row) 生成 VNode;勿把 VNode 当 props 传入(克隆后会失效)。
|
||||
* 与 renderColumnFormatter 同文件定义,保证闭包一致。
|
||||
*/
|
||||
const TableFormatterOutlet = defineComponent({
|
||||
name: "TableFormatterOutlet",
|
||||
props: {
|
||||
column: { type: Object as PropType<ColumnOption>, required: true },
|
||||
/** 避免 prop 名 row 与插槽解构冲突 */
|
||||
record: { type: Object as PropType<Record<string, unknown>>, required: true },
|
||||
},
|
||||
setup(props) {
|
||||
return () => renderColumnFormatter(props.column, props.record);
|
||||
},
|
||||
});
|
||||
|
||||
// 清理列属性,移除插槽相关的自定义属性,确保它们不会被 ElTableColumn 错误解释
|
||||
const cleanColumnProps = (col: ColumnOption) => {
|
||||
const columnProps = { ...col };
|
||||
@@ -289,15 +344,11 @@ const cleanColumnProps = (col: ColumnOption) => {
|
||||
return columnProps;
|
||||
};
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (val: number) => {
|
||||
emit("pagination:size-change", val);
|
||||
};
|
||||
|
||||
// 分页当前页变化
|
||||
const handleCurrentChange = (val: number) => {
|
||||
emit("pagination:current-change", val);
|
||||
scrollToTop(); // 页码改变后滚动到表格顶部
|
||||
/** 普通列:单元格已由插槽内 TableFormatterOutlet 渲染,勿再把 formatter 传给 ElTableColumn,避免与 EP 内置 renderCell 混用 */
|
||||
const cleanBodyColumnProps = (col: ColumnOption) => {
|
||||
const columnProps = cleanColumnProps(col);
|
||||
delete columnProps.formatter;
|
||||
return columnProps;
|
||||
};
|
||||
|
||||
const { scrollToTop: scrollPageToTop } = useCommon();
|
||||
@@ -310,6 +361,20 @@ const scrollToTop = () => {
|
||||
});
|
||||
};
|
||||
|
||||
/** 对接封装分页 @pagination,保持对外仍为 size-change / current-change 事件 */
|
||||
const handlePaginationEvent = (payload: { page: number; limit: number }) => {
|
||||
const p = props.pagination;
|
||||
if (!p) return;
|
||||
if (payload.limit !== p.size) {
|
||||
emit("pagination:size-change", payload.limit);
|
||||
return;
|
||||
}
|
||||
if (payload.page !== p.current) {
|
||||
emit("pagination:current-change", payload.page);
|
||||
scrollToTop();
|
||||
}
|
||||
};
|
||||
|
||||
// 全局序号
|
||||
const getGlobalIndex = (index: number) => {
|
||||
if (!props.pagination) return index + 1;
|
||||
@@ -317,11 +382,6 @@ const getGlobalIndex = (index: number) => {
|
||||
return (current - 1) * size + index + 1;
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "pagination:size-change", val: number): void;
|
||||
(e: "pagination:current-change", val: number): void;
|
||||
}>();
|
||||
|
||||
// 查找并绑定表格头部元素 - 使用 VueUse 优化
|
||||
const findTableHeader = () => {
|
||||
if (!props.showTableHeader) {
|
||||
|
||||
@@ -19,20 +19,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
import { useSettingsStore } from "@/store/modules/setting.store";
|
||||
import { useCeremony } from "@/hooks/core/useCeremony";
|
||||
import { WEB_LINKS } from "@/utils/constants";
|
||||
|
||||
defineOptions({ name: "ArtFestivalTextScroll" });
|
||||
|
||||
const route = useRoute();
|
||||
const settingStore = useSettingsStore();
|
||||
const { showFestivalText } = storeToRefs(settingStore);
|
||||
const { currentFestivalData, closeFestivalScroll } = useCeremony();
|
||||
|
||||
const isFullPageLayout = computed(() => route.matched.some((r) => r.meta?.isFullPage));
|
||||
|
||||
function versionLabel(): string {
|
||||
const v = String(import.meta.env.VITE_VERSION ?? "").trim();
|
||||
if (!v) return "";
|
||||
@@ -49,7 +45,6 @@ const showFestivalStrip = computed(
|
||||
() =>
|
||||
showFestivalText.value &&
|
||||
!!currentFestivalData.value?.scrollText &&
|
||||
currentFestivalData.value.scrollText !== "" &&
|
||||
!isFullPageLayout.value
|
||||
currentFestivalData.value.scrollText !== ""
|
||||
);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user