refactor: 项目整体优化与功能迭代

主要变更:
1. 移除冗余代码:删除重复的CommonUtil、旧主题管理、缓存请求等模块
2. 依赖升级:更新vite-plugin-uni-components到0.3.2版本
3. 样式优化:调整卡片高度、主题色配置、隐私弹窗样式
4. 代码重构:简化用户store、请求类型、列表分页逻辑
5. 新增功能:添加工单统计、tabbar高亮、通用工具类等新模块
6. 修复细节:调整排版顺序、修复水印主题store引用、优化表单样式
This commit is contained in:
zhangtao
2026-08-16 02:30:56 +08:00
parent 85de5c350e
commit 61336936a0
65 changed files with 1455 additions and 1341 deletions
@@ -69,6 +69,9 @@
:is="() => renderColumnHeader(headerScope, col)"
/>
</template>
<template #filter-icon="{ filterOpened }">
<Filter class="fa-filter-icon" :class="{ 'is-opened': filterOpened }" />
</template>
<template #default="slotScope">
<component
v-if="col.useSlot && col.prop && shouldRenderSlotScope(slotScope)"
@@ -138,6 +141,7 @@ import { useCommon } from "@/hooks/core/useCommon";
import { useTableHeight } from "@/hooks/core/useTableHeight";
import { useWindowSize } from "@vueuse/core";
import { VueDraggable } from "vue-draggable-plus";
import { Filter } from "@element-plus/icons-vue";
import { MOBILE_BREAKPOINT } from "@utils/constants/definitions";
import type { ColumnOption } from "@/types/component";
@@ -542,6 +546,57 @@ const cleanColumnProps = (col: ColumnOption) => {
return columnProps;
};
/** 创建人 / 更新人列识别:通过 prop 或 label 匹配 */
const PERSON_FILTER_RE = /(?:create|update)(?:d)?_(?:by|user|name|id)|creator|updater/i;
const isPersonFilterColumn = (col: ColumnOption): boolean => {
// 已显式配置筛选则跳过自动逻辑
if (col.filters || col.filterMethod) return false;
const prop = col.prop || "";
const label = col.label || "";
return PERSON_FILTER_RE.test(prop) || /创建人|更新人|创建者|更新者/.test(label);
};
/** 将创建人/更新人列 prop 归一化为名称取值路径:
* created_id → created_by.name;created_by / created_by.name 等保持不变 */
const getPersonNamePath = (prop: string): string => prop.replace(/_id(?:\..*)?$/i, "_by.name");
/** 解析嵌套路径取值(如 created_by.name),与 ElTable 单元格取值规则一致 */
const getPathValue = (row: Record<string, any>, path: string): unknown => {
let value: unknown = row;
for (const key of path.split(".")) {
if (value == null) return undefined;
value = (value as Record<string, unknown>)[key];
}
return value;
};
/** 单元格值规范化为筛选文本:对象时优先取 name(created_by/updated_by 为对象) */
const toFilterText = (v: unknown): string => {
if (v && typeof v === "object" && "name" in v) {
return String((v as { name: unknown }).name ?? "");
}
return String(v);
};
/** 从当前表格数据中提取某列的去重值作为筛选选项 */
const getColumnFilterOptions = (col: ColumnOption): { text: string; value: string }[] => {
if (!col.prop) return [];
const seen = new Set<string>();
const options: { text: string; value: string }[] = [];
const path = getPersonNamePath(col.prop);
for (const row of props.data || []) {
const v = getPathValue(row as Record<string, any>, path);
if (v === null || v === undefined || v === "") continue;
const text = toFilterText(v);
if (!text) continue;
if (!seen.has(text)) {
seen.add(text);
options.push({ text, value: text });
}
}
return options;
};
/** 普通列:单元格已由插槽内 TableFormatterOutlet 渲染,勿再把 formatter 传给 ElTableColumn,避免与 EP 内置 renderCell 混用 */
const cleanBodyColumnProps = (col: ColumnOption) => {
const columnProps = cleanColumnProps(col);
@@ -551,6 +606,13 @@ const cleanBodyColumnProps = (col: ColumnOption) => {
if (isOpCol && isMobile.value) {
columnProps.width = 80;
}
// 创建人 / 更新人列:自动生成表头筛选选项,无需调用方手写 filters
if (isPersonFilterColumn(col)) {
columnProps.filters = getColumnFilterOptions(col);
columnProps.filterMethod = (value: any, row: any) =>
toFilterText(getPathValue(row as Record<string, any>, getPersonNamePath(col.prop as string))) ===
String(value);
}
return columnProps;
};
@@ -780,5 +842,17 @@ defineExpose({
justify-content: flex-end;
}
}
/* 表头筛选图标:替换 EP 默认箭头为漏斗,悬停/展开/已筛选时高亮为主题色 */
:deep(.el-table__column-filter-trigger .fa-filter-icon) {
color: var(--el-text-color-placeholder);
transition: color 0.2s ease;
}
:deep(.el-table__column-filter-trigger:hover .fa-filter-icon),
:deep(.el-table__column-filter-trigger .fa-filter-icon.is-opened),
:deep(.cell.highlight .fa-filter-icon) {
color: var(--theme-color);
}
}
</style>
@@ -96,7 +96,7 @@
<ElCol :xs="24" :sm="12" :md="12" class="mb-5">
<ElCard
shadow="hover"
class="overflow-hidden border border-(--el-border-color-lighter) rounded-xl flex flex-col h-full"
class="fa-card overflow-hidden border border-(--el-border-color-lighter) rounded-xl flex flex-col h-138"
>
<template #header>
<div class="flex flex-wrap gap-3 items-start justify-between w-full">
@@ -142,35 +142,13 @@
</ElRow>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="6" :md="5" class="mb-5">
<FaImageCard
:imageUrl="imageCards.imageUrl"
:title="imageCards.title"
:category="imageCards.category"
:readTime="imageCards.readTime"
:views="imageCards.views"
:comments="imageCards.comments"
:date="imageCards.date"
@click="handleImageCardClick"
/>
<ElCol :xs="24" :sm="6" :md="6" class="mb-5">
<ImageCards />
</ElCol>
<ElCol :xs="24" :sm="6" :md="5" class="mb-5">
<FaCardBanner
:image="bannerIcon4"
title="版本更新提醒"
description="FastapiAdmin v3.0.0 已发布,包含优化和新功能。"
:button="{
show: true,
text: '立即更新',
color: 'var(--theme-color)',
textColor: '#fff',
}"
:cancelButton="{ show: true, text: '稍后提醒', color: '#eee', textColor: '#333' }"
@click="handleBannerDemoConfirm"
@cancel="handleBannerDemoCancel"
/>
<ElCol :xs="24" :sm="6" :md="6" class="mb-5">
<ItBanners />
</ElCol>
<ElCol :xs="24" :sm="12" :md="14" class="mb-5">
<ElCol :xs="24" :sm="12" :md="12" class="mb-5">
<AboutProject />
</ElCol>
</ElRow>
@@ -184,9 +162,8 @@ defineOptions({ name: "Home", inheritAttrs: false });
import { ref, onMounted, defineAsyncComponent } from "vue";
import { ElMessage } from "element-plus";
import { getDashboardMock } from "@/mock/dashboard";
import bannerIcon4 from "@imgs/3d/icon4.webp";
import cover2 from "@imgs/cover/img2.webp";
import ImageCards from "./modules/image_cards.vue";
import ItBanners from "./modules/it_banners.vue";
import Banner from "./modules/banner.vue";
import NewUser from "./modules/new-user.vue";
import TodoList from "./modules/todo-list.vue";
@@ -216,38 +193,11 @@ const FaDonutChartCard = defineAsyncComponent(
() => import("@/components/cards/fa-donut-chart-card/index.vue")
);
// 非关键组件异步导入(延迟加载,提升首屏速度)
const FaCardBanner = defineAsyncComponent(
() => import("@/components/banners/fa-card-banner/index.vue")
);
const FaImageCard = defineAsyncComponent(
() => import("@/components/cards/fa-image-card/index.vue")
);
const FaTimelineListCard = defineAsyncComponent(
() => import("@/components/cards/fa-timeline-list-card/index.vue")
);
function handleBannerDemoConfirm() {
// TODO: 接入真实操作
}
function handleBannerDemoCancel() {
// TODO: 接入真实操作
}
// === 卡片演示数据 ← workplace ===
const imageCards = {
id: 1,
imageUrl: cover2,
title: "大数据分析助力企业决策的实践案例",
category: "技术",
readTime: "3分钟",
views: 7234,
comments: 5,
date: "12月20日 周二",
};
function handleMore() {
ElMessage.info("查看更多");
}
function handleImageCardClick() {
// TODO: 接入真实跳转
}
</script>
@@ -1,13 +1,13 @@
<template>
<div class="fa-card p-5 flex justify-between h-full">
<div class="fa-card p-5 flex justify-between h-100">
<div>
<h2 class="text-2xl font-medium">关于项目</h2>
<p class="text-g-700 mt-1">{{ systemName }} 是一款兼具设计美学与高效开发的后台系统</p>
<p class="text-g-700 mt-1">使用了 Vue3、TypeScript、Vite、Element Plus 等前沿技术</p>
<div class="flex flex-wrap gap-3.5 max-w-150 mt-9">
<div class="grid grid-cols-2 gap-3.5 max-w-150 mt-9">
<div
class="w-60 flex items-center justify-between h-12.5 px-3.5 border border-g-300 cursor-pointer rounded-lg text-sm bg-g-100 duration-300 hover:-translate-y-1 max-sm:w-full"
class="flex items-center justify-between h-12.5 px-3.5 border border-g-300 cursor-pointer rounded-lg text-sm bg-g-100 duration-300 hover:-translate-y-1"
v-for="link in linkList"
:key="link.label"
@click="goPage(link.url)"
@@ -1,46 +1,132 @@
<template>
<FaBasicBanner
class="justify-center h-54! max-sm:pt-5! max-sm:h-auto!"
:title="bannerTitle"
:subtitle="bannerSubtitle"
boxStyle="bg-theme/10!"
titleColor="var(--fa-gray-900)"
subtitleColor="var(--fa-gray-500)"
:decoration="false"
:meteorConfig="{
enabled: true,
count: 10,
}"
:buttonConfig="{
show: false,
text: '开始探索',
color: 'var(--fa-success)',
textColor: '#fff',
radius: '6px',
}"
:imageConfig="{
src: bannerCover,
width: '18rem',
bottom: '-7.5rem',
}"
@click="handleBannerClick"
>
<div class="flex items-center gap-4 mt-2">
<ElAvatar
v-if="currentUser.avatar"
:size="80"
:src="currentUser.avatar"
style="background-color: transparent"
/>
<ElIcon v-else :size="56" class="text-g-500"><UserFilled /></ElIcon>
<div>
<div class="text-lg font-semibold text-g-800">{{ currentUser.name }}</div>
<div class="text-sm text-g-600">
{{ currentUser.dept_name }} · {{ currentUser.description }} · {{ currentUser.last_login }}
<ElCarousel height="220px" :interval="6000">
<ElCarouselItem>
<FaBasicBanner
height="100%"
:title="bannerTitle"
:subtitle="bannerSubtitle"
boxStyle="bg-theme/10!"
titleColor="var(--fa-gray-900)"
subtitleColor="var(--fa-gray-500)"
:decoration="false"
:meteorConfig="{
enabled: true,
count: 10,
}"
:buttonConfig="{
show: false,
text: '开始探索',
color: 'var(--fa-success)',
textColor: '#fff',
radius: '6px',
}"
:imageConfig="{
src: bannerCover,
width: '18rem',
bottom: '-7.5rem',
}"
@click="handleBannerClick"
>
<div class="flex items-center gap-3 mt-2">
<ElAvatar
v-if="currentUser.avatar"
:size="44"
:src="currentUser.avatar"
style="background-color: transparent"
/>
<ElIcon v-else :size="40" class="text-g-500"><UserFilled /></ElIcon>
<div>
<div class="text-base font-semibold text-g-800">{{ currentUser.name }}</div>
<div class="text-xs text-g-600">
{{ currentUser.dept_name }} · {{ currentUser.description }} · {{ currentUser.last_login }}
</div>
</div>
</div>
</div>
</div>
</FaBasicBanner>
</FaBasicBanner>
</ElCarouselItem>
<ElCarouselItem>
<FaBasicBanner
height="100%"
title="数据中心运行状态"
subtitle="系统访问量同比增长 23%,所有服务运行稳定,数据监控正常。"
/>
</ElCarouselItem>
<ElCarouselItem>
<FaBasicBanner
height="100%"
title="欢迎使用 FastapiAdmin"
subtitle="基于 Vue 3 + TypeScript + Element Plus 构建的现代化管理系统。"
titleColor="#333"
subtitleColor="#666"
boxStyle="!bg-[#D4F1F7]"
:buttonConfig="{
show: true,
text: '开始探索',
color: 'var(--fa-success)',
textColor: '#fff',
radius: '6px'
}"
@buttonClick="handleBannerClick"
/>
</ElCarouselItem>
<ElCarouselItem>
<FaBasicBanner
height="100%"
title="探索星空计划"
subtitle="加入我们的天文观测活动,发现宇宙的奥秘"
boxStyle="!bg-[#FF8AAB]"
:buttonConfig="{
show: true,
text: '立即参与',
color: '#FF5A89',
textColor: '#fff'
}"
:imageConfig="{
src: icon3
}"
/>
</ElCarouselItem>
<ElCarouselItem>
<FaBasicBanner
height="100%"
boxStyle="!bg-[#70B1FF]"
:imageConfig="{
src: icon5
}"
>
<template #title>
<h2 style="margin: 0; font-size: 1.6rem; color: #fff !important">智能组件系统</h2>
</template>
<template #subtitle>
<div style="margin-top: 12px">
<p style="position: relative; z-index: 10; font-style: italic"
>灵活配置,强大扩展,支持自定义插槽内容</p
>
</div>
</template>
<template #button>
<div style="margin-top: 12px">
<ElButton type="primary" color="#04A1FF"> 查看文档 </ElButton>
</div>
</template>
</FaBasicBanner>
</ElCarouselItem>
<ElCarouselItem>
<FaBasicBanner v-bind="PresetBanners.marketing" height="100%" />
</ElCarouselItem>
<ElCarouselItem>
<FaBasicBanner v-bind="PresetBanners.info" height="100%" />
</ElCarouselItem>
</ElCarousel>
</template>
<script setup lang="ts">
@@ -49,6 +135,8 @@ import bannerCover from "@imgs/login/lf_icon2.webp";
import { useUserStore } from "@stores";
import { greetings } from "@utils";
import { UserFilled } from "@element-plus/icons-vue";
import icon3 from '@imgs/3d/icon3.webp'
import icon5 from '@imgs/3d/icon7.webp'
const userStore = useUserStore();
@@ -56,6 +144,7 @@ const userInfo = computed(() => userStore.basicInfo);
const handleBannerClick = (): void => {
// TODO: 接入真实跳转或路由
console.log('banner clicked')
};
const timefix = greetings();
@@ -72,4 +161,39 @@ const currentUser = {
const bannerTitle = `欢迎回来 ~ ${currentUser.name}(${currentUser.username}) ${timefix} ${welcome}`;
const bannerSubtitle = `基于 FastAPI + Vue3 + TypeScript 构建的企业级中后台解决方案,支持多端开发。`;
/**
* 预设横幅配置
* 提供常用的横幅样式配置,可直接通过 v-bind 使用
*/
const PresetBanners = {
marketing: {
title: '限时优惠活动',
subtitle: '精选商品 48 小时闪购,最高享受 7 折优惠,数量有限!',
titleColor: 'var(--fa-gray-900)',
subtitleColor: 'var(--fa-gray-900)',
boxStyle: '!bg-success/15',
meteorConfig: { enabled: true, count: 15 },
buttonConfig: {
show: true,
text: '立即抢购',
color: 'var(--fa-success)',
textColor: '#fff'
}
},
info: {
title: '服务到期提醒',
subtitle: '您的高级服务将在 7 天后到期,请及时续费以继续享受完整功能。',
titleColor: 'var(--fa-gray-900)',
subtitleColor: 'var(--fa-gray-900)',
boxStyle: '!bg-theme/15',
meteorConfig: { enabled: true, count: 15 },
buttonConfig: {
show: true,
text: '立即续费',
color: 'var(--fa-secondary)',
textColor: '#fff'
}
}
} as const
</script>
@@ -0,0 +1,94 @@
<template>
<ElCarousel height="400px">
<ElCarouselItem v-for="card in imageCards" :key="card.id" class="mb-5">
<FaImageCard
:imageUrl="card.imageUrl"
:title="card.title"
:category="card.category"
:readTime="card.readTime"
:views="card.views"
:comments="card.comments"
:date="card.date"
@click="handleImageCardClick(card)"
/>
</ElCarouselItem>
</ElCarousel>
</template>
<script setup lang="ts">
import FaImageCard from "@/components/cards/fa-image-card/index.vue"
import cover1 from '@imgs/cover/img1.webp'
import cover2 from '@imgs/cover/img2.webp'
import cover3 from '@imgs/cover/img3.webp'
import cover4 from '@imgs/cover/img4.webp'
defineOptions({ name: 'ImageCards' })
// === 卡片演示数据 ← workplace ===
const imageCards = [
{
id: 1,
imageUrl: cover1,
title: 'AI技术在医疗领域的创新应用与发展前景',
category: '社交',
readTime: '2分钟',
views: 9125,
comments: 3,
date: '12月19日 周一'
},
{
id: 2,
imageUrl: cover2,
title: '大数据分析助力企业决策的实践案例',
category: '技术',
readTime: '3分钟',
views: 7234,
comments: 5,
date: '12月20日 周二'
},
{
id: 3,
imageUrl: cover3,
title: '区块链技术在供应链管理中的应用',
category: '科技',
readTime: '4分钟',
views: 5678,
comments: 8,
date: '12月21日 周三'
},
{
id: 4,
imageUrl: cover4,
title: '云计算技术发展趋势与未来展望',
category: '云技术',
readTime: '5分钟',
views: 4321,
comments: 6,
date: '12月22日 周四'
}
]
/**
* 图片卡片类型定义
*/
interface ImageCard {
id: number
imageUrl: string
title: string
category: string
readTime: string
views: number
comments: number
date: string
}
/**
* 处理图片卡片点击事件
* @param card 卡片数据
*/
const handleImageCardClick = (card: ImageCard) => {
// TODO: 接入真实跳转
console.log('点击卡片:', card)
}
</script>
@@ -0,0 +1,87 @@
<template>
<ElCarousel height="400px">
<ElCarouselItem>
<FaCardBanner
title="系统运行正常"
description="所有核心服务运行稳定,响应时间在正常范围内。"
/>
</ElCarouselItem>
<ElCarouselItem>
<FaCardBanner
:image="icon2"
title="重要消息通知"
description="您有 3 条待处理的重要消息,请及时查看。"
:button="{
show: true,
text: '查看详情',
color: 'var(--fa-warning)',
textColor: '#fff'
}"
/>
</ElCarouselItem>
<ElCarouselItem>
<FaCardBanner
:image="icon3"
title="数据分析报告"
description="本周业务数据分析报告已完成,请查看关键指标。"
:button="{
show: true,
text: '下载报告',
color: 'var(--fa-error)',
textColor: '#fff'
}"
/>
</ElCarouselItem>
<ElCarouselItem>
<FaCardBanner
:image="icon4"
title="版本更新提醒"
description="FastapiAdmin v3.1.0 已发布,包含优化和新功能。"
:button="{
show: true,
text: '立即更新',
color: 'var(--theme-color)',
textColor: '#fff'
}"
:cancelButton="{
show: true,
text: '稍后提醒',
color: '#eee',
textColor: '#333'
}"
@click="handleConfirm"
@cancel="handleCancel"
/>
</ElCarouselItem>
</ElCarousel>
</template>
<script setup lang="ts">
// 非关键组件异步导入(延迟加载,提升首屏速度)
import FaCardBanner from "@/components/banners/fa-card-banner/index.vue";
import icon2 from '@imgs/3d/icon2.webp'
import icon3 from '@imgs/3d/icon3.webp'
import icon4 from '@imgs/3d/icon4.webp'
defineOptions({ name: 'ItBanners' })
/**
* 处理确认按钮点击事件
*/
const handleConfirm = () => {
// TODO: 接入真实操作
console.log('confirm clicked')
}
/**
* 处理取消按钮点击事件
*/
const handleCancel = () => {
// TODO: 接入真实操作
console.log('cancel clicked')
}
</script>
@@ -1,5 +1,5 @@
<template>
<div class="fa-card flex flex-col p-5 h-134 overflow-hidden">
<div class="fa-card flex flex-col p-5 h-138 overflow-hidden">
<div class="fa-card-header">
<div class="title">
<h4>新用户</h4>
@@ -1,5 +1,5 @@
<template>
<div class="fa-card p-5 pb-3 h-54 max-sm:h-54 flex flex-col relative overflow-hidden">
<div class="fa-card p-5 pb-3 h-55 max-sm:h-55 flex flex-col relative overflow-hidden">
<div class="fa-card-header">
<div class="title">
<h4>
@@ -1,5 +1,5 @@
<template>
<div class="fa-card p-5 h-134 overflow-hidden">
<div class="fa-card p-5 h-138 overflow-hidden">
<div class="fa-card-header">
<div class="title">
<h4>待办事项</h4>
@@ -190,7 +190,7 @@ const {
label: "IP地址",
minWidth: 150,
formatter: (row: OnlineUserTable) =>
h("span", { class: "inline-flex items-center flex-wrap gap-0.5" }, [
h("span", { class: "inline-flex items-center gap-0.5 whitespace-nowrap" }, [
row.ipaddr ?? "",
row.ipaddr
? h(FaCopyButton, {
@@ -19,7 +19,7 @@
@reset="onResetSearch"
/>
<ElSegmented v-model="menuClientTab" :options="menuSegmentedOptions" @change="handleMenuClientTabChange" />
<ElSegmented class="mt-3" v-model="menuClientTab" :options="menuSegmentedOptions" @change="handleMenuClientTabChange" />
<ElCard
class="fa-table-card"