feat: 重构前端项目结构并优化代码

refactor: 迁移前端资源文件至web目录
feat: 新增多种图标资源
style: 统一代码风格和格式化配置
docs: 更新README和文档说明
chore: 更新依赖和配置文件
fix: 修复部分类型定义和枚举
perf: 优化路由和组件加载逻辑
This commit is contained in:
zhangtao
2026-05-01 01:01:17 +08:00
parent 1355a09441
commit 80263e98e1
887 changed files with 116349 additions and 33825 deletions
+80
View File
@@ -0,0 +1,80 @@
/**
* 主题动画工具模块
*
* 提供主题切换的视觉动画效果
*
* ## 主要功能
*
* - 基于鼠标点击位置的圆形扩散动画
* - View Transition API 支持(现代浏览器)
* - 降级处理(不支持动画的浏览器)
* - 暗黑主题切换过渡效果
* - 页面刷新时的主题过渡优化
*
* ## 使用场景
*
* - 明暗主题切换
* - 提升用户体验的视觉反馈
* - 页面刷新时的平滑过渡
*
* ## 技术实现
*
* - 使用 CSS 变量存储点击位置和半径
* - 利用 View Transition API 实现流畅动画
* - 通过 CSS class 控制过渡效果
* - 自动计算最大扩散半径
*
* @module utils/theme/animation
* @author Fastapi Admin Team
*/
import { useCommon } from '@/hooks/core/useCommon';
import { useTheme } from '@/hooks/core/useTheme';
import { SystemThemeEnum } from '@/enums/appEnum';
import { useSettingStore } from '@/store/modules/setting';
const { LIGHT, DARK } = SystemThemeEnum;
/**
* 主题切换动画
* @param e 鼠标点击事件
*/
export const themeAnimation = (e: any) => {
const x = e.clientX;
const y = e.clientY;
// 计算鼠标点击位置距离视窗的最大圆半径
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
// 设置CSS变量
document.documentElement.style.setProperty('--x', x + 'px');
document.documentElement.style.setProperty('--y', y + 'px');
document.documentElement.style.setProperty('--r', endRadius + 'px');
if (document.startViewTransition) {
document.startViewTransition(() => toggleTheme());
} else {
toggleTheme();
}
};
/**
* 切换主题
*/
const toggleTheme = () => {
useTheme().switchThemeStyles(useSettingStore().systemThemeType === LIGHT ? DARK : LIGHT);
useCommon().refresh();
};
/**
* 切换主题过渡效果
* @param enable 是否启用过渡效果
*/
export const toggleTransition = (enable: boolean) => {
const body = document.body;
if (enable) {
body.classList.add('theme-change');
} else {
setTimeout(() => {
body.classList.remove('theme-change');
}, 300);
}
};
+273
View File
@@ -0,0 +1,273 @@
/**
* 颜色处理工具模块
*
* 提供完整的颜色格式转换和处理功能
*
* ## 主要功能
*
* - Hex 与 RGB/RGBA 格式互转
* - 颜色混合计算
* - 颜色变浅/变深处理
* - Element Plus 主题色自动生成
* - 颜色格式验证
* - CSS 变量读取
* - 暗黑模式颜色适配
*
* ## 使用场景
*
* - 主题色动态切换
* - Element Plus 组件主题定制
* - 颜色渐变生成
* - 明暗主题颜色计算
* - 颜色格式标准化
*
* ## 核心功能
*
* - hexToRgba: Hex 转 RGBA(支持透明度)
* - hexToRgb: Hex 转 RGB 数组
* - rgbToHex: RGB 转 Hex
* - colourBlend: 两种颜色混合
* - getLightColor: 生成变浅的颜色
* - getDarkColor: 生成变深的颜色
* - handleElementThemeColor: 处理 Element Plus 主题色
* - setElementThemeColor: 设置完整的主题色系统
*
* ## 支持格式
*
* - Hex: #FFF, #FFFFFF
* - RGB: rgb(255, 255, 255)
* - RGBA: rgba(255, 255, 255, 0.5)
*
* @module utils/ui/colors
* @author Fastapi Admin Team
*/
import { useSettingStore } from '@/store/modules/setting';
/**
* 颜色转换结果接口
*/
interface RgbaResult {
red: number;
green: number;
blue: number;
rgba: string;
}
/**
* 获取CSS变量值(别名函数)
* @param name CSS变量名
* @returns CSS变量值
*/
export function getCssVar(name: string): string {
return getComputedStyle(document.documentElement).getPropertyValue(name);
}
/**
* 验证hex颜色格式
* @param hex hex颜色值
* @returns 是否为有效的hex颜色
*/
function isValidHexColor(hex: string): boolean {
const cleanHex = hex.trim().replace(/^#/, '');
return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleanHex);
}
/**
* 验证RGB颜色值
* @param r 红色值
* @param g 绿色值
* @param b 蓝色值
* @returns 是否为有效的RGB值
*/
function isValidRgbValue(r: number, g: number, b: number): boolean {
const isValid = (value: number) => Number.isInteger(value) && value >= 0 && value <= 255;
return isValid(r) && isValid(g) && isValid(b);
}
/**
* 将hex颜色转换为RGBA
* @param hex hex颜色值 (支持 #FFF 或 #FFFFFF 格式)
* @param opacity 透明度 (0-1)
* @returns 包含RGB值和RGBA字符串的对象
*/
export function hexToRgba(hex: string, opacity: number): RgbaResult {
if (!isValidHexColor(hex)) {
throw new Error('Invalid hex color format');
}
// 移除可能存在的 # 前缀并转换为大写
let cleanHex = hex.trim().replace(/^#/, '').toUpperCase();
// 如果是缩写形式(如 FFF),转换为完整形式
if (cleanHex.length === 3) {
cleanHex = cleanHex
.split('')
.map((char) => char.repeat(2))
.join('');
}
// 解析 RGB 值
const [red, green, blue] = cleanHex.match(/\w\w/g)!.map((x) => parseInt(x, 16));
// 确保 opacity 在有效范围内
const validOpacity = Math.max(0, Math.min(1, opacity));
// 构建 RGBA 字符串
const rgba = `rgba(${red}, ${green}, ${blue}, ${validOpacity.toFixed(2)})`;
return { red, green, blue, rgba };
}
/**
* 将hex颜色转换为RGB数组
* @param hexColor hex颜色值
* @returns RGB数组 [r, g, b]
*/
export function hexToRgb(hexColor: string): number[] {
if (!isValidHexColor(hexColor)) {
ElMessage.warning('输入错误的hex颜色值');
throw new Error('Invalid hex color format');
}
const cleanHex = hexColor.replace(/^#/, '');
let hex = cleanHex;
// 处理缩写形式
if (hex.length === 3) {
hex = hex
.split('')
.map((char) => char.repeat(2))
.join('');
}
const hexPairs = hex.match(/../g);
if (!hexPairs) {
throw new Error('Invalid hex color format');
}
return hexPairs.map((hexPair) => parseInt(hexPair, 16));
}
/**
* 将RGB颜色转换为hex
* @param r 红色值 (0-255)
* @param g 绿色值 (0-255)
* @param b 蓝色值 (0-255)
* @returns hex颜色值
*/
export function rgbToHex(r: number, g: number, b: number): string {
if (!isValidRgbValue(r, g, b)) {
ElMessage.warning('输入错误的RGB颜色值');
throw new Error('Invalid RGB color values');
}
const toHex = (value: number) => {
const hex = value.toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
/**
* 颜色混合
* @param color1 第一个颜色
* @param color2 第二个颜色
* @param ratio 混合比例 (0-1)
* @returns 混合后的颜色
*/
export function colourBlend(color1: string, color2: string, ratio: number): string {
const validRatio = Math.max(0, Math.min(1, Number(ratio)));
const rgb1 = hexToRgb(color1);
const rgb2 = hexToRgb(color2);
const blendedRgb = rgb1.map((value1, index) => {
const value2 = rgb2[index];
return Math.round(value1 * (1 - validRatio) + value2 * validRatio);
});
return rgbToHex(blendedRgb[0], blendedRgb[1], blendedRgb[2]);
}
/**
* 获取变浅的颜色
* @param color 原始颜色
* @param level 变浅程度 (0-1)
* @param isDark 是否为暗色主题
* @returns 变浅后的颜色
*/
export function getLightColor(color: string, level: number, isDark: boolean = false): string {
if (!isValidHexColor(color)) {
ElMessage.warning('输入错误的hex颜色值');
throw new Error('Invalid hex color format');
}
if (isDark) {
return getDarkColor(color, level);
}
const rgb = hexToRgb(color);
const lightRgb = rgb.map((value) => Math.floor((255 - value) * level + value));
return rgbToHex(lightRgb[0], lightRgb[1], lightRgb[2]);
}
/**
* 获取变深的颜色
* @param color 原始颜色
* @param level 变深程度 (0-1)
* @returns 变深后的颜色
*/
export function getDarkColor(color: string, level: number): string {
if (!isValidHexColor(color)) {
ElMessage.warning('输入错误的hex颜色值');
throw new Error('Invalid hex color format');
}
const rgb = hexToRgb(color);
const darkRgb = rgb.map((value) => Math.floor(value * (1 - level)));
return rgbToHex(darkRgb[0], darkRgb[1], darkRgb[2]);
}
/**
* 处理 Element Plus 主题颜色
* @param theme 主题颜色
* @param isDark 是否为暗色主题
*/
export function handleElementThemeColor(theme: string, isDark: boolean = false): void {
document.documentElement.style.setProperty('--el-color-primary', theme);
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-light-${i}`,
getLightColor(theme, i / 10, isDark)
);
}
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-dark-${i}`,
getDarkColor(theme, i / 10)
);
}
}
/**
* 设置 Element Plus 主题颜色
* @param color 主题颜色
*/
export function setElementThemeColor(color: string): void {
const mixColor = '#ffffff';
const elStyle = document.documentElement.style;
elStyle.setProperty('--el-color-primary', color);
handleElementThemeColor(color, useSettingStore().isDark);
// 生成更淡一点的颜色
for (let i = 1; i < 16; i++) {
const itemColor = colourBlend(color, mixColor, i / 16);
elStyle.setProperty(`--el-color-primary-custom-${i}`, itemColor);
}
}
+24
View File
@@ -0,0 +1,24 @@
/**
* 表情
* 用于在消息提示的时候显示对应的表情
*
* 用法
* ElMessage.success(`${EmojiText[200]} 图片上传成功`)
* ElMessage.error(`${EmojiText[400]} 图片上传失败`)
* ElMessage.error(`${EmojiText[500]} 图片上传失败`)
*
* @module utils/ui/emojo
* @author Fastapi Admin Team
*/
// macos 用户 按 shift + 6 可以唤出更多表情……
const EmojiText: { [key: string]: string } = {
'0': 'O_O', // 空
'200': '^_^', // 成功
'400': 'T_T', // 错误请求
'500': 'X_X', // 服务器内部错误,无法完成请求
};
// const EmojiIcon = ['🟢', '🔴', '🟡 ', '🚀', '✨', '💡', '🛠️', '🔥', '🎉', '🌟', '🌈']
export default EmojiText;
@@ -0,0 +1,31 @@
/**
* 离线图标加载器
*
* 用于在内网环境下支持 Iconify 图标的离线加载。
* 通过预加载图标集数据,避免运行时从 CDN 获取图标。
*
* 使用方式:
* 1. 安装所需图标集:pnpm add -D @iconify-json/[icon-set-name]
* 2. 在此文件中导入并注册图标集
* 3. 在组件中使用:<ArtSvgIcon icon="ri:home-line" />
*
* @module utils/ui/iconify-loader
* @author Fastapi Admin Team
*/
// import { addCollection } from '@iconify/vue'
// // 导入离线图标数据
// // 系统必要图标库
// import riIcons from '@iconify-json/ri/icons.json'
// // 演示图标库(可选,生产环境可移除)
// import svgSpinners from '@iconify-json/svg-spinners/icons.json'
// import lineMd from '@iconify-json/line-md/icons.json'
// // 注册离线图标集
// addCollection(riIcons)
// addCollection(svgSpinners)
// addCollection(lineMd)
+11
View File
@@ -0,0 +1,11 @@
/**
* UI 相关工具函数统一导出
*
* @module utils/ui/index
* @author Fastapi Admin Team
*/
export * from './colors';
export * from './loading';
export * from './tabs';
export * from './emojo';
+85
View File
@@ -0,0 +1,85 @@
/**
* 全局 Loading 加载管理模块
*
* 提供统一的全屏加载动画管理
*
* ## 主要功能
*
* - 全屏 Loading 显示和隐藏
* - 自动适配明暗主题背景色
* - 自定义 SVG 加载动画
* - 单例模式防止重复创建
* - 锁定页面交互
*
* ## 使用场景
*
* - 页面初始化加载
* - 大量数据请求
* - 路由切换过渡
* - 异步操作等待
*
* ## 特性
*
* - 自动检测当前主题并应用对应背景色
* - 使用自定义 SVG 动画(四点旋转)
* - 单例模式确保同时只有一个 Loading
* - 提供便捷的显示/隐藏方法
*
* @module utils/ui/loading
* @author Fastapi Admin Team
*/
import { ElLoading } from 'element-plus';
import { fourDotsSpinnerSvg } from '@/assets/svg/loading';
/**
* 获取当前主题对应的loading背景色
* @returns 背景色字符串
*/
const getLoadingBackground = (): string => {
const isDark = document.documentElement.classList.contains('dark');
return isDark ? 'rgba(7, 7, 7, 0.85)' : '#fff';
};
const DEFAULT_LOADING_CONFIG = {
lock: true,
get background() {
return getLoadingBackground();
},
svg: fourDotsSpinnerSvg,
svgViewBox: '0 0 40 40',
customClass: 'art-loading-fix',
} as const;
interface LoadingInstance {
close: () => void;
}
let loadingInstance: LoadingInstance | null = null;
export const loadingService = {
/**
* 显示 loading
* @returns 关闭 loading 的函数
*/
showLoading(): () => void {
if (!loadingInstance) {
// 每次显示时获取最新的配置,确保背景色与当前主题同步
const config = {
...DEFAULT_LOADING_CONFIG,
background: getLoadingBackground(),
};
loadingInstance = ElLoading.service(config);
}
return () => this.hideLoading();
},
/**
* 隐藏 loading
*/
hideLoading(): void {
if (loadingInstance) {
loadingInstance.close();
loadingInstance = null;
}
},
};
+60
View File
@@ -0,0 +1,60 @@
/**
* 标签页布局配置模块
*
* 提供不同标签页样式的高度和间距配置
*
* ## 主要功能
*
* - 多种标签页样式配置(默认、卡片、谷歌风格)
* - 标签页打开/关闭状态的高度管理
* - 顶部间距自动计算
* - 配置获取和默认值处理
*
* ## 使用场景
*
* - 工作标签页(Worktab)布局计算
* - 页面内容区域高度调整
* - 标签页显示/隐藏时的动画
* - 响应式布局适配
*
* ## 配置项说明
*
* - openTop: 标签页显示时,内容区域距离顶部的距离
* - closeTop: 标签页隐藏时,内容区域距离顶部的距离
* - openHeight: 标签页显示时的总高度(包含标签栏)
* - closeHeight: 标签页隐藏时的总高度(仅头部)
*
* ## 支持的样式
*
* - tab-default: 默认标签页样式
* - tab-card: 卡片式标签页
* - tab-google: 谷歌浏览器风格标签页
*
* @module utils/ui/tabs
* @author Fastapi Admin Team
*/
export const TAB_CONFIG = {
'tab-default': {
openTop: 106,
closeTop: 60,
openHeight: 121,
closeHeight: 75,
},
'tab-card': {
openTop: 122,
closeTop: 78,
openHeight: 139,
closeHeight: 95,
},
'tab-google': {
openTop: 122,
closeTop: 78,
openHeight: 139,
closeHeight: 95,
},
};
// 获取当前 tab 样式配置,设置默认值
export const getTabConfig = (style: string) => {
return TAB_CONFIG[style as keyof typeof TAB_CONFIG] || TAB_CONFIG['tab-card']; // 默认使用 tab-card 配置
};