mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-27 22:55:59 +00:00
feat: 重构前端项目结构并优化代码
refactor: 迁移前端资源文件至web目录 feat: 新增多种图标资源 style: 统一代码风格和格式化配置 docs: 更新README和文档说明 chore: 更新依赖和配置文件 fix: 修复部分类型定义和枚举 perf: 优化路由和组件加载逻辑
This commit is contained in:
@@ -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);
|
||||
}
|
||||
};
|
||||
Executable
+273
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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 配置
|
||||
};
|
||||
Reference in New Issue
Block a user