mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-17 15:05:40 +00:00
refactor: 项目整体优化与功能迭代
主要变更: 1. 移除冗余代码:删除重复的CommonUtil、旧主题管理、缓存请求等模块 2. 依赖升级:更新vite-plugin-uni-components到0.3.2版本 3. 样式优化:调整卡片高度、主题色配置、隐私弹窗样式 4. 代码重构:简化用户store、请求类型、列表分页逻辑 5. 新增功能:添加工单统计、tabbar高亮、通用工具类等新模块 6. 修复细节:调整排版顺序、修复水印主题store引用、优化表单样式
This commit is contained in:
@@ -52,14 +52,6 @@ export interface ThemeState {
|
||||
themeVars: ConfigProviderThemeVars
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统主题状态接口(简化版)
|
||||
*/
|
||||
export interface SystemThemeState {
|
||||
theme: ThemeMode
|
||||
themeVars: ConfigProviderThemeVars
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义的主题色选项
|
||||
*/
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import { TicketAPI } from '@/api/module_system/ticket'
|
||||
|
||||
/**
|
||||
* 缓存的请求结果
|
||||
*/
|
||||
interface CachedData<T> {
|
||||
data: T
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求中标记(防重复并发请求)
|
||||
*/
|
||||
const pendingRequests = new Map<string, Promise<any>>()
|
||||
|
||||
/**
|
||||
* 本地缓存存储
|
||||
*/
|
||||
const cacheStore = new Map<string, CachedData<any>>()
|
||||
|
||||
/** 默认缓存有效期(30秒) */
|
||||
const DEFAULT_TTL = 30_000
|
||||
|
||||
/**
|
||||
* 带缓存的请求 composable
|
||||
*
|
||||
* 解决多个页面重复请求同一接口的问题(如 work 和 mine 页面都调用工单统计)。
|
||||
* 提供本地内存缓存 + 并发去重 + TTL 过期机制。
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { getCached } = useCachedRequest()
|
||||
*
|
||||
* // 多页面共享缓存,30秒内不重复请求
|
||||
* const stats = await getCached('ticket-stats', () => fetchTicketStats(), 30_000)
|
||||
* ```
|
||||
*/
|
||||
export function useCachedRequest() {
|
||||
/**
|
||||
* 获取带缓存的数据
|
||||
*
|
||||
* @param key 缓存键
|
||||
* @param fetcher 数据获取函数
|
||||
* @param ttl 缓存有效期(毫秒),默认 30 秒
|
||||
* @returns 数据
|
||||
*/
|
||||
async function getCached<T>(key: string, fetcher: () => Promise<T>, ttl = DEFAULT_TTL): Promise<T> {
|
||||
// 检查缓存是否有效
|
||||
const cached = cacheStore.get(key)
|
||||
if (cached && Date.now() - cached.timestamp < ttl) {
|
||||
return cached.data as T
|
||||
}
|
||||
|
||||
// 检查是否有进行中的相同请求
|
||||
const pending = pendingRequests.get(key)
|
||||
if (pending) {
|
||||
return pending as Promise<T>
|
||||
}
|
||||
|
||||
// 发起新请求
|
||||
const promise = fetcher()
|
||||
.then((data) => {
|
||||
cacheStore.set(key, { data, timestamp: Date.now() })
|
||||
pendingRequests.delete(key)
|
||||
return data
|
||||
})
|
||||
.catch((error) => {
|
||||
pendingRequests.delete(key)
|
||||
throw error
|
||||
})
|
||||
|
||||
pendingRequests.set(key, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
/** 强制刷新缓存 */
|
||||
async function refresh<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
|
||||
cacheStore.delete(key)
|
||||
return getCached(key, fetcher, 0)
|
||||
}
|
||||
|
||||
/** 清除指定缓存 */
|
||||
function invalidate(key: string): void {
|
||||
cacheStore.delete(key)
|
||||
}
|
||||
|
||||
/** 清除所有缓存 */
|
||||
function clearAll(): void {
|
||||
cacheStore.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
getCached,
|
||||
refresh,
|
||||
invalidate,
|
||||
clearAll,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单统计缓存(work 和 mine 页面共享)
|
||||
*/
|
||||
export interface TicketStats {
|
||||
pending: number
|
||||
processing: number
|
||||
done: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工单统计(带缓存,多页面共享)
|
||||
*
|
||||
* 缓存键 'ticket-stats',TTL 30 秒。
|
||||
* work 页面和 mine 页面切换时不会重复请求。
|
||||
*/
|
||||
export async function getTicketStats(): Promise<TicketStats> {
|
||||
const { getCached } = useCachedRequest()
|
||||
return getCached('ticket-stats', async () => {
|
||||
const [pending, processing, done] = await Promise.allSettled([
|
||||
TicketAPI.getPage({ page_no: 1, page_size: 1, status: '0' }),
|
||||
TicketAPI.getPage({ page_no: 1, page_size: 1, status: '1' }),
|
||||
TicketAPI.getPage({ page_no: 1, page_size: 1, status: '2' }),
|
||||
])
|
||||
return {
|
||||
pending: pending.status === 'fulfilled' ? (pending.value.total || 0) : 0,
|
||||
processing: processing.status === 'fulfilled' ? (processing.value.total || 0) : 0,
|
||||
done: done.status === 'fulfilled' ? (done.value.total || 0) : 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,12 +1,3 @@
|
||||
/*
|
||||
* @Author: weisheng
|
||||
* @Date: 2025-09-25 20:32:22
|
||||
* @LastEditTime: 2026-04-07 14:06:20
|
||||
* @LastEditors: weisheng
|
||||
* @Description:
|
||||
* @FilePath: /wot-starter/src/composables/useGlobalLoading.ts
|
||||
* 记得注释
|
||||
*/
|
||||
import type { ToastOptions } from '@wot-ui/ui/components/wd-toast/types'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Method } from 'alova'
|
||||
import type { AlovaGenerics, Method } from 'alova'
|
||||
import { usePagination } from 'alova/client'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@@ -7,8 +7,8 @@ export interface ListPageParams {
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface ListPageOptions<T> {
|
||||
fetcher: (params: ListPageParams) => Promise<PageResult<T>>
|
||||
export interface ListPageOptions<AG extends AlovaGenerics = AlovaGenerics> {
|
||||
fetcher: (params: ListPageParams) => Method<AG>
|
||||
pageSize?: number
|
||||
onError?: (error: unknown) => void
|
||||
}
|
||||
@@ -16,10 +16,10 @@ export interface ListPageOptions<T> {
|
||||
/**
|
||||
* 通用列表分页逻辑(基于 alova usePagination 封装)
|
||||
* 底层使用 alova 状态管理,统一暴露 list/total/loading/error 三态与翻页动作
|
||||
* 说明:fetcher 运行时返回的是 alova Method(http 层 get/post 均为 Method 实例,Method extends Promise),
|
||||
* 这里仅做类型桥接,请求发送与错误处理完全交给 alova hook 管理
|
||||
* 说明:fetcher 需返回 alova Method(http 层 get/post 均为 Method 实例),
|
||||
* 请求发送与错误处理完全交给 alova hook 管理
|
||||
*/
|
||||
export function useListPage<T>(options: ListPageOptions<T>) {
|
||||
export function useListPage<T, AG extends AlovaGenerics = AlovaGenerics>(options: ListPageOptions<AG>) {
|
||||
const { fetcher, pageSize = 10, onError } = options
|
||||
|
||||
const pageParams = ref<ListPageParams>({ page_no: 1, page_size: pageSize })
|
||||
@@ -31,9 +31,8 @@ export function useListPage<T>(options: ListPageOptions<T>) {
|
||||
error,
|
||||
send,
|
||||
onError: onPageError,
|
||||
} = usePagination<any, T[], any>(
|
||||
(pageNo: number, pageSizeNo: number) =>
|
||||
fetcher({ page_no: pageNo, page_size: pageSizeNo }) as unknown as Method,
|
||||
} = usePagination<AG, T[], any[]>(
|
||||
(pageNo: number, pageSizeNo: number) => fetcher({ page_no: pageNo, page_size: pageSizeNo }),
|
||||
{
|
||||
initialPage: 1,
|
||||
initialPageSize: pageSize,
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import type { ThemeColorOption, ThemeMode } from '@/composables/types/theme'
|
||||
import { themeColorOptions } from '@/composables/types/theme'
|
||||
import { initializeThemeOnce, subscribeSystemThemeChange } from '@/utils/systemTheme'
|
||||
|
||||
/**
|
||||
* 完整版主题管理组合式API
|
||||
*
|
||||
* 功能特性:
|
||||
* - 支持手动切换暗黑模式
|
||||
* - 支持主题色选择
|
||||
* - 支持跟随系统主题
|
||||
* - 自动同步导航栏颜色
|
||||
* - 持久化用户设置
|
||||
*
|
||||
* 适用场景:
|
||||
* - 需要用户手动控制主题的应用
|
||||
* - 需要主题色自定义的应用
|
||||
* - 需要完整主题管理功能的复杂应用
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup>
|
||||
* import { useManualTheme } from '@/composables/useManualTheme'
|
||||
*
|
||||
* const {
|
||||
* theme,
|
||||
* isDark,
|
||||
* toggleTheme,
|
||||
* openThemeColorPicker,
|
||||
* currentThemeColor,
|
||||
* themeVars
|
||||
* } = useManualTheme()
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <wd-config-provider :theme="theme" :theme-vars="themeVars">
|
||||
* <view :class="{ 'dark-mode': isDark }">
|
||||
* <wd-button @click="toggleTheme">切换主题</wd-button>
|
||||
* <wd-button @click="openThemeColorPicker">选择主题色</wd-button>
|
||||
* </view>
|
||||
* </wd-config-provider>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useManualTheme() {
|
||||
const store = useManualThemeStore()
|
||||
const showThemeColorSheet = ref(false)
|
||||
let stopThemeChangeListener: (() => void) | undefined
|
||||
|
||||
/**
|
||||
* 切换暗黑模式
|
||||
* @param mode 指定主题模式,不传则自动切换
|
||||
* @param isFollowSystem 是否跟随系统
|
||||
*/
|
||||
function toggleTheme(mode?: ThemeMode, isFollowSystem: boolean = false) {
|
||||
store.toggleTheme(mode, isFollowSystem)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开主题色选择器
|
||||
*/
|
||||
function openThemeColorPicker() {
|
||||
showThemeColorSheet.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭主题色选择器
|
||||
*/
|
||||
function closeThemeColorPicker() {
|
||||
showThemeColorSheet.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择主题色
|
||||
* @param option 主题色选项
|
||||
*/
|
||||
function selectThemeColor(option: ThemeColorOption) {
|
||||
store.setCurrentThemeColor(option)
|
||||
closeThemeColorPicker()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主题
|
||||
*/
|
||||
function initTheme() {
|
||||
store.initTheme()
|
||||
}
|
||||
|
||||
// 组件挂载前初始化主题
|
||||
onBeforeMount(() => {
|
||||
initializeThemeOnce(store, initTheme)
|
||||
stopThemeChangeListener = subscribeSystemThemeChange(store, (res) => {
|
||||
if (store.followSystem) {
|
||||
store.toggleTheme(res.theme, true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 页面显示时更新导航栏颜色,确保每次切换页面时导航栏颜色都是正确的
|
||||
onShow(() => {
|
||||
store.setNavigationBarColor()
|
||||
})
|
||||
|
||||
// 组件卸载时清理监听
|
||||
onUnmounted(() => {
|
||||
stopThemeChangeListener?.()
|
||||
stopThemeChangeListener = undefined
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态
|
||||
theme: computed(() => store.theme),
|
||||
isDark: computed(() => store.isDark),
|
||||
followSystem: computed(() => store.followSystem),
|
||||
hasUserSet: computed(() => store.hasUserSet),
|
||||
currentThemeColor: computed(() => store.currentThemeColor),
|
||||
themeVars: computed(() => store.themeVars),
|
||||
showThemeColorSheet,
|
||||
|
||||
// 常量
|
||||
themeColorOptions,
|
||||
|
||||
// 方法
|
||||
initTheme,
|
||||
toggleTheme,
|
||||
setFollowSystem: store.setFollowSystem,
|
||||
openThemeColorPicker,
|
||||
closeThemeColorPicker,
|
||||
selectThemeColor,
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型和常量供外部使用
|
||||
export type { ThemeColorOption, ThemeMode }
|
||||
export { themeColorOptions }
|
||||
@@ -0,0 +1,16 @@
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
|
||||
/**
|
||||
* tab 页 onShow 时同步底部导航高亮。
|
||||
* 通过 getCurrentPages 校验当前路由,避免从子页面返回时误触发
|
||||
* (子页面返回的中间态路由是子页面自身,不会 emit 到错误 tab)。
|
||||
*/
|
||||
export function useTabbarActive(route: string, tabKey: string, onActive?: () => void) {
|
||||
onShow(() => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0 && pages[pages.length - 1].route === route) {
|
||||
uni.$emit('updateTabbar', tabKey)
|
||||
onActive?.()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,36 +1,35 @@
|
||||
import type { ThemeColorOption, ThemeMode } from '@/composables/types/theme'
|
||||
import { themeColorOptions } from '@/composables/types/theme'
|
||||
import { initializeThemeOnce, subscribeSystemThemeChange } from '@/utils/systemTheme'
|
||||
|
||||
/**
|
||||
* 简化版系统主题管理组合式API
|
||||
* 主题管理组合式API(统一入口)
|
||||
*
|
||||
* 功能特性:
|
||||
* - 仅跟随系统主题变化
|
||||
* - 自动响应系统主题切换
|
||||
* - 导航栏颜色通过 theme.json 自动处理
|
||||
* - 轻量级,无额外功能
|
||||
*
|
||||
* 适用场景:
|
||||
* - 只需要系统主题适应的简单应用
|
||||
* - 不需要用户手动控制主题的应用
|
||||
* - 追求轻量级主题管理的应用
|
||||
*
|
||||
* 注意事项:
|
||||
* - 不支持手动切换主题
|
||||
* - 不支持主题色自定义
|
||||
* - 导航栏颜色依赖 theme.json 配置
|
||||
* - 支持手动切换暗黑模式
|
||||
* - 支持主题色选择
|
||||
* - 支持跟随系统主题
|
||||
* - 自动同步导航栏颜色
|
||||
* - 持久化用户设置
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup>
|
||||
* import { useTheme } from '@/composables/useTheme'
|
||||
*
|
||||
* const { theme, isDark, themeVars } = useTheme()
|
||||
* const {
|
||||
* theme,
|
||||
* isDark,
|
||||
* toggleTheme,
|
||||
* openThemeColorPicker,
|
||||
* currentThemeColor,
|
||||
* themeVars
|
||||
* } = useTheme()
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <wd-config-provider :theme="theme" :theme-vars="themeVars">
|
||||
* <view :class="{ 'dark-mode': isDark }">
|
||||
* <text>当前主题: {{ theme }}</text>
|
||||
* <wd-button @click="toggleTheme">切换主题</wd-button>
|
||||
* <wd-button @click="openThemeColorPicker">选择主题色</wd-button>
|
||||
* </view>
|
||||
* </wd-config-provider>
|
||||
* </template>
|
||||
@@ -38,17 +37,63 @@ import { initializeThemeOnce, subscribeSystemThemeChange } from '@/utils/systemT
|
||||
*/
|
||||
export function useTheme() {
|
||||
const store = useThemeStore()
|
||||
const showThemeColorSheet = ref(false)
|
||||
let stopThemeChangeListener: (() => void) | undefined
|
||||
|
||||
// 组件挂载前初始化系统主题
|
||||
/**
|
||||
* 切换暗黑模式
|
||||
* @param mode 指定主题模式,不传则自动切换
|
||||
* @param isFollowSystem 是否跟随系统
|
||||
*/
|
||||
function toggleTheme(mode?: ThemeMode, isFollowSystem: boolean = false) {
|
||||
store.toggleTheme(mode, isFollowSystem)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开主题色选择器
|
||||
*/
|
||||
function openThemeColorPicker() {
|
||||
showThemeColorSheet.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭主题色选择器
|
||||
*/
|
||||
function closeThemeColorPicker() {
|
||||
showThemeColorSheet.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择主题色
|
||||
* @param option 主题色选项
|
||||
*/
|
||||
function selectThemeColor(option: ThemeColorOption) {
|
||||
store.setCurrentThemeColor(option)
|
||||
closeThemeColorPicker()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主题
|
||||
*/
|
||||
function initTheme() {
|
||||
store.initTheme()
|
||||
}
|
||||
|
||||
// 组件挂载前初始化主题
|
||||
onBeforeMount(() => {
|
||||
initializeThemeOnce(store, () => store.initSystemTheme())
|
||||
initializeThemeOnce(store, initTheme)
|
||||
stopThemeChangeListener = subscribeSystemThemeChange(store, (res) => {
|
||||
// 系统主题变化时自动更新,导航栏颜色由 theme.json 自动处理
|
||||
store.setTheme(res.theme)
|
||||
if (store.followSystem) {
|
||||
store.toggleTheme(res.theme, true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 页面显示时更新导航栏颜色,确保每次切换页面时导航栏颜色都是正确的
|
||||
onShow(() => {
|
||||
store.setNavigationBarColor()
|
||||
})
|
||||
|
||||
// 组件卸载时清理监听
|
||||
onUnmounted(() => {
|
||||
stopThemeChangeListener?.()
|
||||
@@ -56,9 +101,28 @@ export function useTheme() {
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态(只读)
|
||||
// 状态
|
||||
theme: computed(() => store.theme),
|
||||
isDark: computed(() => store.isDark),
|
||||
followSystem: computed(() => store.followSystem),
|
||||
hasUserSet: computed(() => store.hasUserSet),
|
||||
currentThemeColor: computed(() => store.currentThemeColor),
|
||||
themeVars: computed(() => store.themeVars),
|
||||
showThemeColorSheet,
|
||||
|
||||
// 常量
|
||||
themeColorOptions,
|
||||
|
||||
// 方法
|
||||
initTheme,
|
||||
toggleTheme,
|
||||
setFollowSystem: store.setFollowSystem,
|
||||
openThemeColorPicker,
|
||||
closeThemeColorPicker,
|
||||
selectThemeColor,
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型和常量供外部使用
|
||||
export type { ThemeColorOption, ThemeMode }
|
||||
export { themeColorOptions }
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ref } from 'vue'
|
||||
import { TicketAPI } from '@/api/module_system/ticket'
|
||||
|
||||
/**
|
||||
* 工单统计(后端聚合接口 + alova 30 秒缓存),work/mine 两页共用
|
||||
*/
|
||||
export function useTicketStats() {
|
||||
const pendingTickets = ref<number | null>(null)
|
||||
const processingTickets = ref<number | null>(null)
|
||||
const doneTickets = ref<number | null>(null)
|
||||
|
||||
async function loadTicketStats() {
|
||||
try {
|
||||
const stats = await TicketAPI.getStats()
|
||||
pendingTickets.value = stats.pending
|
||||
processingTickets.value = stats.processing
|
||||
doneTickets.value = stats.done
|
||||
}
|
||||
catch { /* silent */ }
|
||||
}
|
||||
|
||||
return { pendingTickets, processingTickets, doneTickets, loadTicketStats }
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { Storage } from '@/utils/storage'
|
||||
*/
|
||||
export function useWatermark() {
|
||||
const userStore = useUserStore()
|
||||
const themeStore = useManualThemeStore()
|
||||
const themeStore = useThemeStore()
|
||||
// 兼容历史脏对象 { value },统一兜底为布尔(wd-switch change 事件参数为对象)
|
||||
const stored = Storage.get<boolean | { value: boolean }>(WATERMARK_KEY)
|
||||
const localSwitch = ref(typeof stored === 'object' && stored ? Boolean(stored.value) : (stored ?? true))
|
||||
|
||||
Reference in New Issue
Block a user