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
@@ -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'
+8 -9
View File
@@ -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?.()
}
})
}
+88 -24
View File
@@ -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 }
}
+1 -1
View File
@@ -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))