初始化
This commit is contained in:
45
saiadmin-artd/src/hooks/core/useAppMode.ts
Normal file
45
saiadmin-artd/src/hooks/core/useAppMode.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* useAppMode - 应用模式管理
|
||||
*
|
||||
* 提供应用访问模式的判断和管理功能,支持前端和后端两种权限控制模式。
|
||||
* 根据环境变量 VITE_ACCESS_MODE 自动识别当前运行模式。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 模式识别 - 自动识别前端模式或后端模式
|
||||
* 2. 前端模式 - 权限由前端路由配置控制,适合小型项目或演示环境
|
||||
* 3. 后端模式 - 权限由后端接口返回的菜单数据控制,适合企业级应用
|
||||
* 4. 响应式状态 - 提供响应式的模式判断,方便在组件中使用
|
||||
*
|
||||
* @module useAppMode
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
export function useAppMode() {
|
||||
// 获取访问模式配置
|
||||
const accessMode = import.meta.env.VITE_ACCESS_MODE
|
||||
|
||||
/**
|
||||
* 是否为前端控制模式
|
||||
* 前端模式:权限由前端路由配置控制
|
||||
*/
|
||||
const isFrontendMode = computed(() => accessMode === 'frontend')
|
||||
/**
|
||||
* 是否为后端控制模式
|
||||
* 后端模式:权限由后端接口返回的菜单数据控制
|
||||
*/
|
||||
const isBackendMode = computed(() => accessMode === 'backend')
|
||||
|
||||
/**
|
||||
* 当前应用模式
|
||||
*/
|
||||
const currentMode = computed(() => accessMode)
|
||||
|
||||
return {
|
||||
isFrontendMode,
|
||||
isBackendMode,
|
||||
currentMode
|
||||
}
|
||||
}
|
||||
74
saiadmin-artd/src/hooks/core/useAuth.ts
Normal file
74
saiadmin-artd/src/hooks/core/useAuth.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* useAuth - 权限验证管理
|
||||
*
|
||||
* 提供统一的权限验证功能,支持前端和后端两种权限模式。
|
||||
* 用于控制页面按钮、操作等功能的显示和访问权限。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 权限检查 - 检查用户是否拥有指定的权限标识
|
||||
* 2. 双模式支持 - 自动适配前端模式和后端模式的权限验证
|
||||
* 3. 前端模式 - 从用户信息中获取按钮权限列表(如 ['add', 'edit', 'delete'])
|
||||
* 4. 后端模式 - 从路由 meta 配置中获取权限列表(如 [{ authMark: 'add' }])
|
||||
*
|
||||
* ## 使用示例
|
||||
*
|
||||
* ```typescript
|
||||
* const { hasAuth } = useAuth()
|
||||
*
|
||||
* // 检查是否有新增权限
|
||||
* if (hasAuth('add')) {
|
||||
* // 显示新增按钮
|
||||
* }
|
||||
*
|
||||
* // 在模板中使用
|
||||
* <el-button v-if="hasAuth('edit')">编辑</el-button>
|
||||
* <el-button v-if="hasAuth('delete')">删除</el-button>
|
||||
* ```
|
||||
*
|
||||
* @module useAuth
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useAppMode } from '@/hooks/core/useAppMode'
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
|
||||
type AuthItem = NonNullable<AppRouteRecord['meta']['authList']>[number]
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
export const useAuth = () => {
|
||||
const route = useRoute()
|
||||
const { isFrontendMode } = useAppMode()
|
||||
const { info } = storeToRefs(userStore)
|
||||
|
||||
// 前端按钮权限(例如:['add', 'edit'])
|
||||
const frontendAuthList = info.value?.buttons ?? []
|
||||
|
||||
// 后端路由 meta 配置的权限列表(例如:[{ authMark: 'add' }])
|
||||
const backendAuthList: AuthItem[] = Array.isArray(route.meta.authList)
|
||||
? (route.meta.authList as AuthItem[])
|
||||
: []
|
||||
|
||||
/**
|
||||
* 检查是否拥有某权限标识(前后端模式通用)
|
||||
* @param auth 权限标识
|
||||
* @returns 是否有权限
|
||||
*/
|
||||
const hasAuth = (auth: string): boolean => {
|
||||
// 前端模式
|
||||
if (isFrontendMode.value) {
|
||||
return frontendAuthList.includes(auth)
|
||||
}
|
||||
|
||||
// 后端模式
|
||||
return backendAuthList.some((item) => item?.authMark === auth)
|
||||
}
|
||||
|
||||
return {
|
||||
hasAuth
|
||||
}
|
||||
}
|
||||
184
saiadmin-artd/src/hooks/core/useCeremony.ts
Normal file
184
saiadmin-artd/src/hooks/core/useCeremony.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* useCeremony - 节日庆祝管理
|
||||
*
|
||||
* 提供节日烟花效果和祝福文本展示功能,为系统增添节日氛围。
|
||||
* 自动检测当前日期是否为节日,并在首次进入时播放烟花动画和显示祝福语。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 节日检测 - 自动匹配当前日期与节日配置列表,支持单日和跨日期节日
|
||||
* 2. 烟花动画 - 播放节日烟花特效,支持自定义图片和触发次数
|
||||
* 3. 祝福文本 - 烟花结束后显示节日祝福文本
|
||||
* 4. 状态管理 - 记录烟花播放状态,避免重复播放
|
||||
* 5. 清理机制 - 提供清理方法,支持手动停止和重置
|
||||
*
|
||||
* ## 使用示例
|
||||
*
|
||||
* ```typescript
|
||||
* // 在配置文件中定义节日
|
||||
* // 单日节日
|
||||
* {
|
||||
* date: '2024-12-25',
|
||||
* name: '圣诞节',
|
||||
* image: christmasImage,
|
||||
* count: 3 // 可选,不设置则使用默认值 3 次
|
||||
* scrollText: 'Merry Christmas!',
|
||||
* }
|
||||
*
|
||||
* // 跨日期节日
|
||||
* {
|
||||
* date: '2025-11-07',
|
||||
* endDate: '2025-11-10',
|
||||
* name: 'v3.0 测试阶段',
|
||||
* image: '',
|
||||
* count: 5 // 自定义烟花播放次数
|
||||
* scrollText: '系统 v3.0 测试阶段正式开启!',
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @module useCeremony
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { useTimeoutFn, useIntervalFn, useDateFormat } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { mittBus } from '@/utils/sys'
|
||||
import { festivalConfigList } from '@/config/modules/festival'
|
||||
|
||||
/**
|
||||
* 节日庆祝配置常量
|
||||
*/
|
||||
const FESTIVAL_CONFIG = {
|
||||
/** 初始延迟(毫秒) */
|
||||
INITIAL_DELAY: 300,
|
||||
/** 烟花播放间隔(毫秒) */
|
||||
FIREWORK_INTERVAL: 1000,
|
||||
/** 文本显示延迟(毫秒) */
|
||||
TEXT_DELAY: 2000,
|
||||
/** 默认烟花播放次数 */
|
||||
DEFAULT_FIREWORKS_COUNT: 3
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 节日庆祝功能
|
||||
* 提供节日烟花效果和祝福文本展示
|
||||
*/
|
||||
export function useCeremony() {
|
||||
const settingStore = useSettingStore()
|
||||
const { holidayFireworksLoaded, isShowFireworks } = storeToRefs(settingStore)
|
||||
|
||||
let fireworksInterval: { pause: () => void } | null = null
|
||||
|
||||
/**
|
||||
* 检查日期是否在节日范围内
|
||||
* @param currentDate 当前日期
|
||||
* @param festivalDate 节日开始日期
|
||||
* @param festivalEndDate 节日结束日期(可选)
|
||||
*/
|
||||
const isDateInRange = (
|
||||
currentDate: string,
|
||||
festivalDate: string,
|
||||
festivalEndDate?: string
|
||||
): boolean => {
|
||||
if (!festivalEndDate) {
|
||||
// 单日节日
|
||||
return currentDate === festivalDate
|
||||
}
|
||||
|
||||
// 跨日期节日
|
||||
const current = new Date(currentDate)
|
||||
const start = new Date(festivalDate)
|
||||
const end = new Date(festivalEndDate)
|
||||
|
||||
return current >= start && current <= end
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日期对应的节日数据
|
||||
*/
|
||||
const currentFestivalData = computed(() => {
|
||||
const currentDate = useDateFormat(new Date(), 'YYYY-MM-DD').value
|
||||
return festivalConfigList.find((item) => isDateInRange(currentDate, item.date, item.endDate))
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新节日日期到 store
|
||||
*/
|
||||
const updateFestivalDate = () => {
|
||||
settingStore.setFestivalDate(currentFestivalData.value?.date || '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发烟花效果
|
||||
*/
|
||||
const triggerFirework = () => {
|
||||
mittBus.emit('triggerFireworks', currentFestivalData.value?.image)
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成烟花效果后显示文本
|
||||
*/
|
||||
const showFestivalText = () => {
|
||||
settingStore.setholidayFireworksLoaded(true)
|
||||
|
||||
useTimeoutFn(() => {
|
||||
settingStore.setShowFestivalText(true)
|
||||
updateFestivalDate()
|
||||
}, FESTIVAL_CONFIG.TEXT_DELAY)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动烟花循环
|
||||
*/
|
||||
const startFireworksLoop = () => {
|
||||
let playedCount = 0
|
||||
// 使用节日配置的播放次数,如果没有则使用默认值
|
||||
const count = currentFestivalData.value?.count ?? FESTIVAL_CONFIG.DEFAULT_FIREWORKS_COUNT
|
||||
|
||||
const { pause } = useIntervalFn(() => {
|
||||
triggerFirework()
|
||||
playedCount++
|
||||
|
||||
if (playedCount >= count) {
|
||||
pause()
|
||||
showFestivalText()
|
||||
}
|
||||
}, FESTIVAL_CONFIG.FIREWORK_INTERVAL)
|
||||
|
||||
fireworksInterval = { pause }
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启节日庆祝
|
||||
*/
|
||||
const openFestival = () => {
|
||||
if (!currentFestivalData.value || !isShowFireworks.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const { start } = useTimeoutFn(startFireworksLoop, FESTIVAL_CONFIG.INITIAL_DELAY)
|
||||
start()
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理烟花效果
|
||||
*/
|
||||
const cleanup = () => {
|
||||
if (fireworksInterval) {
|
||||
fireworksInterval.pause()
|
||||
fireworksInterval = null
|
||||
}
|
||||
settingStore.setShowFestivalText(false)
|
||||
updateFestivalDate()
|
||||
}
|
||||
|
||||
return {
|
||||
openFestival,
|
||||
cleanup,
|
||||
holidayFireworksLoaded,
|
||||
currentFestivalData,
|
||||
isShowFireworks
|
||||
}
|
||||
}
|
||||
745
saiadmin-artd/src/hooks/core/useChart.ts
Normal file
745
saiadmin-artd/src/hooks/core/useChart.ts
Normal file
@@ -0,0 +1,745 @@
|
||||
/**
|
||||
* useChart - ECharts 图表管理
|
||||
*
|
||||
* 提供完整的 ECharts 图表生命周期管理和配置能力,简化图表开发流程。
|
||||
* 自动处理图表初始化、更新、销毁、主题切换、响应式调整等复杂逻辑。
|
||||
*
|
||||
* ## 核心功能
|
||||
*
|
||||
* 1. 图表生命周期管理 - 自动处理初始化、更新、销毁,支持延迟加载和可见性检测
|
||||
* 2. 主题自动适配 - 响应系统主题变化,自动更新图表样式和配色
|
||||
* 3. 响应式调整 - 监听窗口大小、菜单展开等变化,自动调整图表尺寸
|
||||
* 4. 空状态处理 - 优雅的空数据展示,自动显示"暂无数据"提示
|
||||
* 5. 样式配置统一 - 提供坐标轴、图例、提示框等统一的样式配置方法
|
||||
* 6. 性能优化 - 防抖处理、样式缓存、requestAnimationFrame 优化
|
||||
* 7. 高级组件抽象 - useChartComponent 提供更高层次的图表组件封装
|
||||
*
|
||||
* ## 使用示例
|
||||
*
|
||||
* ```typescript
|
||||
* // 基础用法
|
||||
* const {
|
||||
* chartRef,
|
||||
* initChart,
|
||||
* updateChart,
|
||||
* getAxisLineStyle,
|
||||
* getTooltipStyle
|
||||
* } = useChart()
|
||||
*
|
||||
* onMounted(() => {
|
||||
* initChart({
|
||||
* xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
|
||||
* yAxis: { type: 'value' },
|
||||
* series: [{ data: [120, 200, 150], type: 'bar' }]
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* // 高级用法 - 组件抽象
|
||||
* const chart = useChartComponent({
|
||||
* props,
|
||||
* generateOptions: () => ({
|
||||
* // ECharts 配置
|
||||
* }),
|
||||
* checkEmpty: () => data.value.length === 0,
|
||||
* watchSources: [() => props.data]
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @module useChart
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { echarts, type EChartsOption } from '@/plugins/echarts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { getCssVar } from '@/utils/ui'
|
||||
import type { BaseChartProps, ChartThemeConfig, UseChartOptions } from '@/types/component/chart'
|
||||
|
||||
// 图表主题配置
|
||||
export const useChartOps = (): ChartThemeConfig => ({
|
||||
/** */
|
||||
chartHeight: '16rem',
|
||||
/** 字体大小 */
|
||||
fontSize: 13,
|
||||
/** 字体颜色 */
|
||||
fontColor: '#999',
|
||||
/** 主题颜色 */
|
||||
themeColor: getCssVar('--el-color-primary-light-1'),
|
||||
/** 颜色组 */
|
||||
colors: [
|
||||
getCssVar('--el-color-primary-light-1'),
|
||||
'#4ABEFF',
|
||||
'#EDF2FF',
|
||||
'#14DEBA',
|
||||
'#FFAF20',
|
||||
'#FA8A6C',
|
||||
'#FFAF20'
|
||||
]
|
||||
})
|
||||
|
||||
// 常量定义
|
||||
const RESIZE_DELAYS = [50, 100, 200, 350] as const
|
||||
const MENU_RESIZE_DELAYS = [50, 100, 200] as const
|
||||
const RESIZE_DEBOUNCE_DELAY = 100
|
||||
|
||||
export function useChart(options: UseChartOptions = {}) {
|
||||
const { initOptions, initDelay = 0, threshold = 0.1, autoTheme = true } = options
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const { isDark, menuOpen, menuType } = storeToRefs(settingStore)
|
||||
|
||||
const chartRef = ref<HTMLElement>()
|
||||
let chart: echarts.ECharts | null = null
|
||||
let intersectionObserver: IntersectionObserver | null = null
|
||||
let pendingOptions: EChartsOption | null = null
|
||||
let resizeTimeoutId: number | null = null
|
||||
let resizeFrameId: number | null = null
|
||||
let isDestroyed = false
|
||||
let emptyStateDiv: HTMLElement | null = null
|
||||
|
||||
// 清理定时器的统一方法
|
||||
const clearTimers = () => {
|
||||
if (resizeTimeoutId) {
|
||||
clearTimeout(resizeTimeoutId)
|
||||
resizeTimeoutId = null
|
||||
}
|
||||
if (resizeFrameId) {
|
||||
cancelAnimationFrame(resizeFrameId)
|
||||
resizeFrameId = null
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 requestAnimationFrame 优化 resize 处理
|
||||
const requestAnimationResize = () => {
|
||||
if (resizeFrameId) {
|
||||
cancelAnimationFrame(resizeFrameId)
|
||||
}
|
||||
resizeFrameId = requestAnimationFrame(() => {
|
||||
handleResize()
|
||||
resizeFrameId = null
|
||||
})
|
||||
}
|
||||
|
||||
// 防抖的resize处理(用于窗口resize事件)
|
||||
const debouncedResize = () => {
|
||||
if (resizeTimeoutId) {
|
||||
clearTimeout(resizeTimeoutId)
|
||||
}
|
||||
resizeTimeoutId = window.setTimeout(() => {
|
||||
requestAnimationResize()
|
||||
resizeTimeoutId = null
|
||||
}, RESIZE_DEBOUNCE_DELAY)
|
||||
}
|
||||
|
||||
// 多延迟resize处理 - 统一方法
|
||||
const multiDelayResize = (delays: readonly number[]) => {
|
||||
// 立即调用一次,快速响应
|
||||
nextTick(requestAnimationResize)
|
||||
|
||||
// 使用延迟时间,确保图表正确适应变化
|
||||
delays.forEach((delay) => {
|
||||
setTimeout(requestAnimationResize, delay)
|
||||
})
|
||||
}
|
||||
|
||||
// 收缩菜单时,重新计算图表大小(仅在图表存在时监听)
|
||||
let menuOpenStopHandle: (() => void) | null = null
|
||||
let menuTypeStopHandle: (() => void) | null = null
|
||||
|
||||
const setupMenuWatchers = () => {
|
||||
menuOpenStopHandle = watch(menuOpen, () => multiDelayResize(RESIZE_DELAYS))
|
||||
menuTypeStopHandle = watch(menuType, () => {
|
||||
nextTick(requestAnimationResize)
|
||||
setTimeout(() => multiDelayResize(MENU_RESIZE_DELAYS), 0)
|
||||
})
|
||||
}
|
||||
|
||||
const cleanupMenuWatchers = () => {
|
||||
menuOpenStopHandle?.()
|
||||
menuTypeStopHandle?.()
|
||||
menuOpenStopHandle = null
|
||||
menuTypeStopHandle = null
|
||||
}
|
||||
|
||||
// 主题变化时重新设置图表选项
|
||||
let themeStopHandle: (() => void) | null = null
|
||||
|
||||
const setupThemeWatcher = () => {
|
||||
if (autoTheme) {
|
||||
themeStopHandle = watch(isDark, () => {
|
||||
// 更新空状态样式
|
||||
emptyStateManager.updateStyle()
|
||||
|
||||
if (chart && !isDestroyed) {
|
||||
// 使用 requestAnimationFrame 优化主题更新
|
||||
requestAnimationFrame(() => {
|
||||
if (chart && !isDestroyed) {
|
||||
const currentOptions = chart.getOption()
|
||||
if (currentOptions) {
|
||||
updateChart(currentOptions as EChartsOption)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupThemeWatcher = () => {
|
||||
themeStopHandle?.()
|
||||
themeStopHandle = null
|
||||
}
|
||||
|
||||
// 样式生成器 - 统一的样式配置
|
||||
const createLineStyle = (color: string, width = 1, type?: 'solid' | 'dashed') => ({
|
||||
color,
|
||||
width,
|
||||
...(type && { type })
|
||||
})
|
||||
|
||||
// 缓存样式配置以减少重复计算
|
||||
const styleCache = {
|
||||
axisLine: null as any,
|
||||
splitLine: null as any,
|
||||
axisLabel: null as any,
|
||||
lastDarkValue: isDark.value
|
||||
}
|
||||
|
||||
const clearStyleCache = () => {
|
||||
styleCache.axisLine = null
|
||||
styleCache.splitLine = null
|
||||
styleCache.axisLabel = null
|
||||
styleCache.lastDarkValue = isDark.value
|
||||
}
|
||||
|
||||
// 坐标轴线样式
|
||||
const getAxisLineStyle = (show: boolean = true) => {
|
||||
if (styleCache.lastDarkValue !== isDark.value) {
|
||||
clearStyleCache()
|
||||
}
|
||||
if (!styleCache.axisLine) {
|
||||
styleCache.axisLine = {
|
||||
show,
|
||||
lineStyle: createLineStyle(isDark.value ? '#444' : '#EDEDED')
|
||||
}
|
||||
}
|
||||
return styleCache.axisLine
|
||||
}
|
||||
|
||||
// 分割线样式
|
||||
const getSplitLineStyle = (show: boolean = true) => {
|
||||
if (styleCache.lastDarkValue !== isDark.value) {
|
||||
clearStyleCache()
|
||||
}
|
||||
if (!styleCache.splitLine) {
|
||||
styleCache.splitLine = {
|
||||
show,
|
||||
lineStyle: createLineStyle(isDark.value ? '#444' : '#EDEDED', 1, 'dashed')
|
||||
}
|
||||
}
|
||||
return styleCache.splitLine
|
||||
}
|
||||
|
||||
// 坐标轴标签样式
|
||||
const getAxisLabelStyle = (show: boolean = true) => {
|
||||
if (styleCache.lastDarkValue !== isDark.value) {
|
||||
clearStyleCache()
|
||||
}
|
||||
if (!styleCache.axisLabel) {
|
||||
const { fontColor, fontSize } = useChartOps()
|
||||
styleCache.axisLabel = {
|
||||
show,
|
||||
color: fontColor,
|
||||
fontSize
|
||||
}
|
||||
}
|
||||
return styleCache.axisLabel
|
||||
}
|
||||
|
||||
// 坐标轴刻度样式(静态配置,无需缓存)
|
||||
const getAxisTickStyle = () => ({
|
||||
show: false
|
||||
})
|
||||
|
||||
// 获取动画配置
|
||||
const getAnimationConfig = (animationDelay: number = 50, animationDuration: number = 1500) => ({
|
||||
animationDelay: (idx: number) => idx * animationDelay + 200,
|
||||
animationDuration: (idx: number) => animationDuration - idx * 50,
|
||||
animationEasing: 'quarticOut' as const
|
||||
})
|
||||
|
||||
// 获取统一的 tooltip 配置
|
||||
const getTooltipStyle = (trigger: 'item' | 'axis' = 'axis', customOptions: any = {}) => ({
|
||||
trigger,
|
||||
backgroundColor: isDark.value ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.9)',
|
||||
borderColor: isDark.value ? '#333' : '#ddd',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: isDark.value ? '#fff' : '#333'
|
||||
},
|
||||
...customOptions
|
||||
})
|
||||
|
||||
// 获取统一的图例配置
|
||||
const getLegendStyle = (
|
||||
position: 'bottom' | 'top' | 'left' | 'right' = 'bottom',
|
||||
customOptions: any = {}
|
||||
) => {
|
||||
const baseConfig = {
|
||||
textStyle: {
|
||||
color: isDark.value ? '#fff' : '#333'
|
||||
},
|
||||
itemWidth: 12,
|
||||
itemHeight: 12,
|
||||
itemGap: 20,
|
||||
...customOptions
|
||||
}
|
||||
|
||||
// 根据位置设置不同的配置
|
||||
switch (position) {
|
||||
case 'bottom':
|
||||
return {
|
||||
...baseConfig,
|
||||
bottom: 0,
|
||||
left: 'center',
|
||||
orient: 'horizontal',
|
||||
icon: 'roundRect'
|
||||
}
|
||||
case 'top':
|
||||
return {
|
||||
...baseConfig,
|
||||
top: 0,
|
||||
left: 'center',
|
||||
orient: 'horizontal',
|
||||
icon: 'roundRect'
|
||||
}
|
||||
case 'left':
|
||||
return {
|
||||
...baseConfig,
|
||||
left: 0,
|
||||
top: 'center',
|
||||
orient: 'vertical',
|
||||
icon: 'roundRect'
|
||||
}
|
||||
case 'right':
|
||||
return {
|
||||
...baseConfig,
|
||||
right: 0,
|
||||
top: 'center',
|
||||
orient: 'vertical',
|
||||
icon: 'roundRect'
|
||||
}
|
||||
default:
|
||||
return baseConfig
|
||||
}
|
||||
}
|
||||
|
||||
// 根据图例位置计算 grid 配置
|
||||
const getGridWithLegend = (
|
||||
showLegend: boolean,
|
||||
legendPosition: 'bottom' | 'top' | 'left' | 'right' = 'bottom',
|
||||
baseGrid: any = {}
|
||||
) => {
|
||||
const defaultGrid = {
|
||||
top: 15,
|
||||
right: 15,
|
||||
bottom: 8,
|
||||
left: 0,
|
||||
containLabel: true,
|
||||
...baseGrid
|
||||
}
|
||||
|
||||
if (!showLegend) {
|
||||
return defaultGrid
|
||||
}
|
||||
|
||||
// 根据图例位置调整 grid
|
||||
switch (legendPosition) {
|
||||
case 'bottom':
|
||||
return {
|
||||
...defaultGrid,
|
||||
bottom: 40
|
||||
}
|
||||
case 'top':
|
||||
return {
|
||||
...defaultGrid,
|
||||
top: 40
|
||||
}
|
||||
case 'left':
|
||||
return {
|
||||
...defaultGrid,
|
||||
left: 120
|
||||
}
|
||||
case 'right':
|
||||
return {
|
||||
...defaultGrid,
|
||||
right: 120
|
||||
}
|
||||
default:
|
||||
return defaultGrid
|
||||
}
|
||||
}
|
||||
|
||||
// 创建IntersectionObserver
|
||||
const createIntersectionObserver = () => {
|
||||
if (intersectionObserver || !chartRef.value) return
|
||||
|
||||
intersectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && pendingOptions && !isDestroyed) {
|
||||
// 使用 requestAnimationFrame 确保在下一帧初始化图表
|
||||
requestAnimationFrame(() => {
|
||||
if (!isDestroyed && pendingOptions) {
|
||||
try {
|
||||
// 元素变为可见,初始化图表
|
||||
if (!chart) {
|
||||
chart = echarts.init(entry.target as HTMLElement)
|
||||
}
|
||||
|
||||
// 触发自定义事件,让组件处理动画逻辑
|
||||
const event = new CustomEvent('chartVisible', {
|
||||
detail: { options: pendingOptions }
|
||||
})
|
||||
entry.target.dispatchEvent(event)
|
||||
|
||||
pendingOptions = null
|
||||
cleanupIntersectionObserver()
|
||||
} catch (error) {
|
||||
console.error('图表初始化失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold }
|
||||
)
|
||||
|
||||
intersectionObserver.observe(chartRef.value)
|
||||
}
|
||||
|
||||
// 清理IntersectionObserver
|
||||
const cleanupIntersectionObserver = () => {
|
||||
if (intersectionObserver) {
|
||||
intersectionObserver.disconnect()
|
||||
intersectionObserver = null
|
||||
}
|
||||
}
|
||||
|
||||
// 检查容器是否可见
|
||||
const isContainerVisible = (element: HTMLElement): boolean => {
|
||||
const rect = element.getBoundingClientRect()
|
||||
return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight && rect.bottom > 0
|
||||
}
|
||||
|
||||
// 图表初始化核心逻辑
|
||||
const performChartInit = (options: EChartsOption) => {
|
||||
if (!chart && chartRef.value && !isDestroyed) {
|
||||
chart = echarts.init(chartRef.value)
|
||||
// 图表创建后立即设置监听器
|
||||
setupMenuWatchers()
|
||||
setupThemeWatcher()
|
||||
}
|
||||
if (chart && !isDestroyed) {
|
||||
chart.setOption(options)
|
||||
pendingOptions = null
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态管理器
|
||||
const emptyStateManager = {
|
||||
create: () => {
|
||||
if (!chartRef.value || emptyStateDiv) return
|
||||
|
||||
emptyStateDiv = document.createElement('div')
|
||||
emptyStateDiv.style.cssText = `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: ${isDark.value ? '#555555' : '#B3B2B2'};
|
||||
background: transparent;
|
||||
z-index: 10;
|
||||
`
|
||||
emptyStateDiv.innerHTML = `<span>暂无数据</span>`
|
||||
|
||||
// 确保父容器有相对定位
|
||||
if (
|
||||
chartRef.value.style.position !== 'relative' &&
|
||||
chartRef.value.style.position !== 'absolute'
|
||||
) {
|
||||
chartRef.value.style.position = 'relative'
|
||||
}
|
||||
|
||||
chartRef.value.appendChild(emptyStateDiv)
|
||||
},
|
||||
|
||||
remove: () => {
|
||||
if (emptyStateDiv && chartRef.value) {
|
||||
chartRef.value.removeChild(emptyStateDiv)
|
||||
emptyStateDiv = null
|
||||
}
|
||||
},
|
||||
|
||||
updateStyle: () => {
|
||||
if (emptyStateDiv) {
|
||||
emptyStateDiv.style.color = isDark.value ? '#666' : '#999'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initChart = (options: EChartsOption = {}, isEmpty: boolean = false) => {
|
||||
if (!chartRef.value || isDestroyed) return
|
||||
|
||||
const mergedOptions = { ...initOptions, ...options }
|
||||
|
||||
try {
|
||||
if (isEmpty) {
|
||||
// 处理空数据情况 - 显示自定义空状态div
|
||||
if (chart) {
|
||||
chart.clear()
|
||||
}
|
||||
emptyStateManager.create()
|
||||
return
|
||||
} else {
|
||||
// 有数据时移除空状态div
|
||||
emptyStateManager.remove()
|
||||
}
|
||||
|
||||
if (isContainerVisible(chartRef.value)) {
|
||||
// 容器可见,正常初始化
|
||||
if (initDelay > 0) {
|
||||
setTimeout(() => performChartInit(mergedOptions), initDelay)
|
||||
} else {
|
||||
performChartInit(mergedOptions)
|
||||
}
|
||||
} else {
|
||||
// 容器不可见,保存选项并设置监听器
|
||||
pendingOptions = mergedOptions
|
||||
createIntersectionObserver()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('图表初始化失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
const updateChart = (options: EChartsOption) => {
|
||||
if (isDestroyed) return
|
||||
|
||||
try {
|
||||
if (!chart) {
|
||||
// 如果图表不存在,先初始化
|
||||
initChart(options)
|
||||
return
|
||||
}
|
||||
chart.setOption(options)
|
||||
} catch (error) {
|
||||
console.error('图表更新失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (chart && !isDestroyed) {
|
||||
try {
|
||||
chart.resize()
|
||||
} catch (error) {
|
||||
console.error('图表resize失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁图表
|
||||
const destroyChart = () => {
|
||||
isDestroyed = true
|
||||
|
||||
if (chart) {
|
||||
try {
|
||||
chart.dispose()
|
||||
} catch (error) {
|
||||
console.error('图表销毁失败:', error)
|
||||
} finally {
|
||||
chart = null
|
||||
}
|
||||
}
|
||||
|
||||
// 清理所有监听器和资源
|
||||
cleanupMenuWatchers()
|
||||
cleanupThemeWatcher()
|
||||
emptyStateManager.remove()
|
||||
cleanupIntersectionObserver()
|
||||
clearTimers()
|
||||
clearStyleCache()
|
||||
pendingOptions = null
|
||||
}
|
||||
|
||||
// 获取图表实例
|
||||
const getChartInstance = () => chart
|
||||
|
||||
// 获取图表是否已初始化
|
||||
const isChartInitialized = () => chart !== null
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', debouncedResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', debouncedResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
destroyChart()
|
||||
})
|
||||
|
||||
return {
|
||||
isDark,
|
||||
chartRef,
|
||||
initChart,
|
||||
updateChart,
|
||||
handleResize,
|
||||
destroyChart,
|
||||
getChartInstance,
|
||||
isChartInitialized,
|
||||
emptyStateManager,
|
||||
getAxisLineStyle,
|
||||
getSplitLineStyle,
|
||||
getAxisLabelStyle,
|
||||
getAxisTickStyle,
|
||||
getAnimationConfig,
|
||||
getTooltipStyle,
|
||||
getLegendStyle,
|
||||
useChartOps,
|
||||
getGridWithLegend
|
||||
}
|
||||
}
|
||||
|
||||
// 高级图表组件抽象
|
||||
interface UseChartComponentOptions<T extends BaseChartProps> {
|
||||
/** Props响应式对象 */
|
||||
props: T
|
||||
/** 图表配置生成函数 */
|
||||
generateOptions: () => EChartsOption
|
||||
/** 空数据检查函数 */
|
||||
checkEmpty?: () => boolean
|
||||
/** 自定义监听的响应式数据 */
|
||||
watchSources?: (() => any)[]
|
||||
/** 自定义可视事件处理 */
|
||||
onVisible?: () => void
|
||||
/** useChart选项 */
|
||||
chartOptions?: UseChartOptions
|
||||
}
|
||||
|
||||
export function useChartComponent<T extends BaseChartProps>(options: UseChartComponentOptions<T>) {
|
||||
const {
|
||||
props,
|
||||
generateOptions,
|
||||
checkEmpty,
|
||||
watchSources = [],
|
||||
onVisible,
|
||||
chartOptions = {}
|
||||
} = options
|
||||
|
||||
const chart = useChart(chartOptions)
|
||||
const { chartRef, initChart, isDark, emptyStateManager } = chart
|
||||
|
||||
// 检查是否为空数据
|
||||
const isEmpty = computed(() => {
|
||||
if (props.isEmpty) return true
|
||||
if (checkEmpty) return checkEmpty()
|
||||
return false
|
||||
})
|
||||
|
||||
// 更新图表
|
||||
const updateChart = () => {
|
||||
nextTick(() => {
|
||||
if (isEmpty.value) {
|
||||
// 处理空数据情况 - 显示自定义空状态div
|
||||
if (chart.getChartInstance()) {
|
||||
chart.getChartInstance()?.clear()
|
||||
}
|
||||
emptyStateManager.create()
|
||||
} else {
|
||||
// 有数据时移除空状态div并初始化图表
|
||||
emptyStateManager.remove()
|
||||
initChart(generateOptions())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理图表进入可视区域时的逻辑
|
||||
const handleChartVisible = () => {
|
||||
if (onVisible) {
|
||||
onVisible()
|
||||
} else {
|
||||
updateChart()
|
||||
}
|
||||
}
|
||||
|
||||
// 存储监听器停止函数
|
||||
const stopHandles: (() => void)[] = []
|
||||
|
||||
// 设置数据监听
|
||||
const setupWatchers = () => {
|
||||
// 监听自定义数据源
|
||||
if (watchSources.length > 0) {
|
||||
const stopHandle = watch(watchSources, updateChart, { deep: true })
|
||||
stopHandles.push(stopHandle)
|
||||
}
|
||||
|
||||
// 监听主题变化
|
||||
const themeStopHandle = watch(isDark, () => {
|
||||
emptyStateManager.updateStyle()
|
||||
updateChart()
|
||||
})
|
||||
stopHandles.push(themeStopHandle)
|
||||
}
|
||||
|
||||
// 清理所有监听器
|
||||
const cleanupWatchers = () => {
|
||||
stopHandles.forEach((stop) => stop())
|
||||
stopHandles.length = 0
|
||||
}
|
||||
|
||||
// 设置生命周期
|
||||
const setupLifecycle = () => {
|
||||
onMounted(() => {
|
||||
updateChart()
|
||||
|
||||
// 监听图表可见事件
|
||||
if (chartRef.value) {
|
||||
chartRef.value.addEventListener('chartVisible', handleChartVisible)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 清理事件监听器
|
||||
if (chartRef.value) {
|
||||
chartRef.value.removeEventListener('chartVisible', handleChartVisible)
|
||||
}
|
||||
// 清理所有监听器
|
||||
cleanupWatchers()
|
||||
// 清理空状态div
|
||||
emptyStateManager.remove()
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
setupWatchers()
|
||||
setupLifecycle()
|
||||
|
||||
return {
|
||||
...chart,
|
||||
isEmpty,
|
||||
updateChart,
|
||||
handleChartVisible
|
||||
}
|
||||
}
|
||||
87
saiadmin-artd/src/hooks/core/useCommon.ts
Normal file
87
saiadmin-artd/src/hooks/core/useCommon.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* useCommon - 通用功能集合
|
||||
*
|
||||
* 提供常用的页面操作功能,包括页面刷新、滚动控制、路径获取等。
|
||||
* 这些功能在多个页面和组件中都会用到,统一封装便于复用。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 首页路径 - 获取系统配置的首页路径
|
||||
* 2. 页面刷新 - 刷新当前页面内容
|
||||
* 3. 滚动控制 - 提供多种滚动到顶部和指定位置的方法
|
||||
* 4. 平滑滚动 - 支持平滑滚动动画效果
|
||||
*
|
||||
* @module useCommon
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
|
||||
export function useCommon() {
|
||||
const menuStore = useMenuStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
/**
|
||||
* 首页路径
|
||||
* 从菜单 store 中获取配置的首页路径
|
||||
*/
|
||||
const homePath = computed(() => menuStore.getHomePath())
|
||||
|
||||
/**
|
||||
* 刷新当前页面
|
||||
* 通过切换 setting store 中的 refresh 状态触发页面重新渲染
|
||||
*/
|
||||
const refresh = () => {
|
||||
settingStore.reload()
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到页面顶部
|
||||
* 查找主内容区域并将其滚动位置重置为顶部
|
||||
*/
|
||||
const scrollToTop = () => {
|
||||
const scrollContainer = document.getElementById('app-main')
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTop = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑滚动到页面顶部
|
||||
* 使用 smooth 行为实现平滑滚动效果
|
||||
*/
|
||||
const smoothScrollToTop = () => {
|
||||
const scrollContainer = document.getElementById('app-main')
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到指定位置
|
||||
* @param top 目标滚动位置(像素)
|
||||
* @param smooth 是否使用平滑滚动
|
||||
*/
|
||||
const scrollTo = (top: number, smooth: boolean = false) => {
|
||||
const scrollContainer = document.getElementById('app-main')
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
top,
|
||||
behavior: smooth ? 'smooth' : 'auto'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
homePath,
|
||||
refresh,
|
||||
scrollTo,
|
||||
scrollToTop,
|
||||
smoothScrollToTop
|
||||
}
|
||||
}
|
||||
55
saiadmin-artd/src/hooks/core/useFastEnter.ts
Normal file
55
saiadmin-artd/src/hooks/core/useFastEnter.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* useFastEnter - 快速入口管理
|
||||
*
|
||||
* 管理顶部栏的快速入口功能,提供应用列表和快速链接的配置和过滤。
|
||||
* 支持动态启用/禁用、自定义排序、响应式宽度控制等功能。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 应用列表管理 - 获取启用的应用列表,自动按排序权重排序
|
||||
* 2. 快速链接管理 - 获取启用的快速链接,支持自定义排序
|
||||
* 3. 响应式配置 - 所有配置自动响应变化,无需手动更新
|
||||
* 4. 宽度控制 - 提供最小显示宽度配置,支持响应式布局
|
||||
*
|
||||
* @module useFastEnter
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { computed } from 'vue'
|
||||
import appConfig from '@/config'
|
||||
import type { FastEnterApplication, FastEnterQuickLink } from '@/types/config'
|
||||
|
||||
export function useFastEnter() {
|
||||
// 获取快速入口配置
|
||||
const fastEnterConfig = computed(() => appConfig.fastEnter)
|
||||
|
||||
// 获取启用的应用列表(按排序权重排序)
|
||||
const enabledApplications = computed<FastEnterApplication[]>(() => {
|
||||
if (!fastEnterConfig.value?.applications) return []
|
||||
|
||||
return fastEnterConfig.value.applications
|
||||
.filter((app) => app.enabled !== false)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
})
|
||||
|
||||
// 获取启用的快速链接(按排序权重排序)
|
||||
const enabledQuickLinks = computed<FastEnterQuickLink[]>(() => {
|
||||
if (!fastEnterConfig.value?.quickLinks) return []
|
||||
|
||||
return fastEnterConfig.value.quickLinks
|
||||
.filter((link) => link.enabled !== false)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
})
|
||||
|
||||
// 获取最小显示宽度
|
||||
const minWidth = computed(() => {
|
||||
return fastEnterConfig.value?.minWidth || 1200
|
||||
})
|
||||
|
||||
return {
|
||||
fastEnterConfig,
|
||||
enabledApplications,
|
||||
enabledQuickLinks,
|
||||
minWidth
|
||||
}
|
||||
}
|
||||
201
saiadmin-artd/src/hooks/core/useHeaderBar.ts
Normal file
201
saiadmin-artd/src/hooks/core/useHeaderBar.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* useHeaderBar - 顶部栏功能管理
|
||||
*
|
||||
* 统一管理顶部栏各个功能模块的显示状态和配置信息。
|
||||
* 提供灵活的功能开关控制,支持动态显示/隐藏顶部栏的各个功能按钮。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 功能开关控制 - 统一管理菜单按钮、刷新按钮、快速入口等功能的显示状态
|
||||
* 2. 配置信息获取 - 获取各个功能模块的详细配置信息
|
||||
* 3. 功能列表查询 - 快速获取所有启用或禁用的功能列表
|
||||
* 4. 响应式状态 - 所有状态自动响应配置和 store 变化
|
||||
*
|
||||
* @module useHeaderBar
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { headerBarConfig } from '@/config/modules/headerBar'
|
||||
import { HeaderBarFeatureConfig } from '@/types'
|
||||
|
||||
/**
|
||||
* 顶部栏功能管理
|
||||
* @returns 顶部栏功能相关的状态和方法
|
||||
*/
|
||||
export function useHeaderBar() {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
// 获取顶部栏配置
|
||||
const headerBarConfigRef = computed<HeaderBarFeatureConfig>(() => headerBarConfig)
|
||||
|
||||
// 从store中获取相关状态
|
||||
const { showMenuButton, showFastEnter, showRefreshButton, showCrumbs, showLanguage } =
|
||||
storeToRefs(settingStore)
|
||||
|
||||
/**
|
||||
* 检查特定功能是否启用
|
||||
* @param feature 功能名称
|
||||
* @returns 是否启用
|
||||
*/
|
||||
const isFeatureEnabled = (feature: keyof HeaderBarFeatureConfig): boolean => {
|
||||
return headerBarConfigRef.value[feature]?.enabled ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取功能配置信息
|
||||
* @param feature 功能名称
|
||||
* @returns 功能配置信息
|
||||
*/
|
||||
const getFeatureConfig = (feature: keyof HeaderBarFeatureConfig) => {
|
||||
return headerBarConfigRef.value[feature]
|
||||
}
|
||||
|
||||
// 检查菜单按钮是否显示
|
||||
const shouldShowMenuButton = computed(() => {
|
||||
return isFeatureEnabled('menuButton') && showMenuButton.value
|
||||
})
|
||||
|
||||
// 检查刷新按钮是否显示
|
||||
const shouldShowRefreshButton = computed(() => {
|
||||
return isFeatureEnabled('refreshButton') && showRefreshButton.value
|
||||
})
|
||||
|
||||
// 检查快速入口是否显示
|
||||
const shouldShowFastEnter = computed(() => {
|
||||
return isFeatureEnabled('fastEnter') && showFastEnter.value
|
||||
})
|
||||
|
||||
// 检查面包屑是否显示
|
||||
const shouldShowBreadcrumb = computed(() => {
|
||||
return isFeatureEnabled('breadcrumb') && showCrumbs.value
|
||||
})
|
||||
|
||||
// 检查全局搜索是否显示
|
||||
const shouldShowGlobalSearch = computed(() => {
|
||||
return isFeatureEnabled('globalSearch')
|
||||
})
|
||||
|
||||
// 检查全屏按钮是否显示
|
||||
const shouldShowFullscreen = computed(() => {
|
||||
return isFeatureEnabled('fullscreen')
|
||||
})
|
||||
|
||||
// 检查通知中心是否显示
|
||||
const shouldShowNotification = computed(() => {
|
||||
return isFeatureEnabled('notification')
|
||||
})
|
||||
|
||||
// 检查聊天功能是否显示
|
||||
const shouldShowChat = computed(() => {
|
||||
return isFeatureEnabled('chat')
|
||||
})
|
||||
|
||||
// 检查语言切换是否显示
|
||||
const shouldShowLanguage = computed(() => {
|
||||
return isFeatureEnabled('language') && showLanguage.value
|
||||
})
|
||||
|
||||
// 检查设置面板是否显示
|
||||
const shouldShowSettings = computed(() => {
|
||||
return isFeatureEnabled('settings')
|
||||
})
|
||||
|
||||
// 检查主题切换是否显示
|
||||
const shouldShowThemeToggle = computed(() => {
|
||||
return isFeatureEnabled('themeToggle')
|
||||
})
|
||||
|
||||
// 获取快速入口的最小宽度
|
||||
const fastEnterMinWidth = computed(() => {
|
||||
const config = getFeatureConfig('fastEnter')
|
||||
return (config as any)?.minWidth || 1200
|
||||
})
|
||||
|
||||
/**
|
||||
* 检查功能是否启用(别名)
|
||||
* @param feature 功能名称
|
||||
* @returns 是否启用
|
||||
*/
|
||||
const isFeatureActive = (feature: keyof HeaderBarFeatureConfig): boolean => {
|
||||
return isFeatureEnabled(feature)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取功能配置(别名)
|
||||
* @param feature 功能名称
|
||||
* @returns 功能配置
|
||||
*/
|
||||
const getFeatureInfo = (feature: keyof HeaderBarFeatureConfig) => {
|
||||
return getFeatureConfig(feature)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有启用的功能列表
|
||||
* @returns 启用的功能名称数组
|
||||
*/
|
||||
const getEnabledFeatures = (): (keyof HeaderBarFeatureConfig)[] => {
|
||||
return Object.keys(headerBarConfigRef.value).filter(
|
||||
(key) => headerBarConfigRef.value[key as keyof HeaderBarFeatureConfig]?.enabled
|
||||
) as (keyof HeaderBarFeatureConfig)[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有禁用的功能列表
|
||||
* @returns 禁用的功能名称数组
|
||||
*/
|
||||
const getDisabledFeatures = (): (keyof HeaderBarFeatureConfig)[] => {
|
||||
return Object.keys(headerBarConfigRef.value).filter(
|
||||
(key) => !headerBarConfigRef.value[key as keyof HeaderBarFeatureConfig]?.enabled
|
||||
) as (keyof HeaderBarFeatureConfig)[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有启用的功能(别名)
|
||||
* @returns 启用的功能列表
|
||||
*/
|
||||
const getActiveFeatures = () => {
|
||||
return getEnabledFeatures()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有禁用的功能(别名)
|
||||
* @returns 禁用的功能列表
|
||||
*/
|
||||
const getInactiveFeatures = () => {
|
||||
return getDisabledFeatures()
|
||||
}
|
||||
|
||||
return {
|
||||
// 配置
|
||||
headerBarConfig: headerBarConfigRef,
|
||||
|
||||
// 显示状态计算属性
|
||||
shouldShowMenuButton, // 是否显示菜单按钮
|
||||
shouldShowRefreshButton, // 是否显示刷新按钮
|
||||
shouldShowFastEnter, // 是否显示快速入口
|
||||
shouldShowBreadcrumb, // 是否显示面包屑
|
||||
shouldShowGlobalSearch, // 是否显示全局搜索
|
||||
shouldShowFullscreen, // 是否显示全屏按钮
|
||||
shouldShowNotification, // 是否显示通知中心
|
||||
shouldShowChat, // 是否显示聊天功能
|
||||
shouldShowLanguage, // 是否显示语言切换
|
||||
shouldShowSettings, // 是否显示设置面板
|
||||
shouldShowThemeToggle, // 是否显示主题切换
|
||||
|
||||
// 配置相关
|
||||
fastEnterMinWidth, // 快速入口最小宽度
|
||||
|
||||
// 方法
|
||||
isFeatureEnabled, // 检查功能是否启用
|
||||
isFeatureActive, // 检查功能是否启用(别名)
|
||||
getFeatureConfig, // 获取功能配置
|
||||
getFeatureInfo, // 获取功能配置(别名)
|
||||
getEnabledFeatures, // 获取所有启用的功能
|
||||
getDisabledFeatures, // 获取所有禁用的功能
|
||||
getActiveFeatures, // 获取所有启用的功能(别名)
|
||||
getInactiveFeatures // 获取所有禁用的功能(别名)
|
||||
}
|
||||
}
|
||||
148
saiadmin-artd/src/hooks/core/useLayoutHeight.ts
Normal file
148
saiadmin-artd/src/hooks/core/useLayoutHeight.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* useLayoutHeight - 页面布局高度管理
|
||||
*
|
||||
* 自动计算和管理页面内容区域的高度,确保内容区域能够正确填充剩余空间。
|
||||
* 监听头部元素高度变化,动态调整内容区域高度,避免出现滚动条或布局错乱。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 动态高度计算 - 根据头部元素高度自动计算内容区域高度
|
||||
* 2. 响应式监听 - 自动监听元素尺寸变化并更新高度
|
||||
* 3. CSS 变量同步 - 自动更新 CSS 变量,方便全局使用
|
||||
* 4. 灵活配置 - 支持自定义间距、CSS 变量名等
|
||||
* 5. 自动查找模式 - 提供通过 ID 自动查找元素的便捷方式
|
||||
*
|
||||
* @module useLayoutHeight
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
|
||||
/**
|
||||
* 页面容器高度配置
|
||||
*/
|
||||
interface LayoutHeightOptions {
|
||||
/** 额外的间距(默认 15px) */
|
||||
extraSpacing?: number
|
||||
/** 是否自动更新 CSS 变量(默认 true) */
|
||||
updateCssVar?: boolean
|
||||
/** CSS 变量名称(默认 '--art-full-height') */
|
||||
cssVarName?: string
|
||||
}
|
||||
|
||||
export function useLayoutHeight(options: LayoutHeightOptions = {}) {
|
||||
const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options
|
||||
|
||||
// 元素引用
|
||||
const headerRef = ref<HTMLElement>()
|
||||
const contentHeaderRef = ref<HTMLElement>()
|
||||
|
||||
// 使用 VueUse 自动监听元素尺寸变化
|
||||
const { height: headerHeight } = useElementSize(headerRef)
|
||||
const { height: contentHeaderHeight } = useElementSize(contentHeaderRef)
|
||||
|
||||
// 计算容器最小高度(响应式)
|
||||
const containerMinHeight = computed(() => {
|
||||
const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing
|
||||
return `calc(100vh - ${totalHeight}px)`
|
||||
})
|
||||
|
||||
if (updateCssVar) {
|
||||
watch(
|
||||
containerMinHeight,
|
||||
(newHeight) => {
|
||||
requestAnimationFrame(() => {
|
||||
document.documentElement.style.setProperty(cssVarName, newHeight)
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
/** 容器最小高度(响应式) */
|
||||
containerMinHeight,
|
||||
/** 头部元素引用 */
|
||||
headerRef,
|
||||
/** 内容头部元素引用 */
|
||||
contentHeaderRef,
|
||||
/** 头部高度(响应式) */
|
||||
headerHeight,
|
||||
/** 内容头部高度(响应式) */
|
||||
contentHeaderHeight
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 ID 自动查找元素的布局高度管理
|
||||
* 适用于无法直接获取元素引用的场景
|
||||
*
|
||||
* @param headerIds 头部元素的 ID 数组
|
||||
* @param options 配置选项
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
export function useAutoLayoutHeight(
|
||||
headerIds: string[] = ['app-header', 'app-content-header'],
|
||||
options: LayoutHeightOptions = {}
|
||||
) {
|
||||
const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options
|
||||
|
||||
// 创建元素引用
|
||||
const headerRef = ref<HTMLElement>()
|
||||
const contentHeaderRef = ref<HTMLElement>()
|
||||
|
||||
// 使用 VueUse 自动监听元素尺寸变化
|
||||
const { height: headerHeight } = useElementSize(headerRef)
|
||||
const { height: contentHeaderHeight } = useElementSize(contentHeaderRef)
|
||||
|
||||
// 计算容器最小高度(响应式)
|
||||
const containerMinHeight = computed(() => {
|
||||
const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing
|
||||
return `calc(100vh - ${totalHeight}px)`
|
||||
})
|
||||
|
||||
if (updateCssVar) {
|
||||
watch(
|
||||
containerMinHeight,
|
||||
(newHeight) => {
|
||||
requestAnimationFrame(() => {
|
||||
document.documentElement.style.setProperty(cssVarName, newHeight)
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
// 在 DOM 挂载后查找元素
|
||||
onMounted(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
// 使用 nextTick 确保 DOM 完全渲染
|
||||
requestAnimationFrame(() => {
|
||||
const header = document.getElementById(headerIds[0])
|
||||
const contentHeader = document.getElementById(headerIds[1])
|
||||
|
||||
if (header) {
|
||||
headerRef.value = header
|
||||
}
|
||||
if (contentHeader) {
|
||||
contentHeaderRef.value = contentHeader
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
/** 容器最小高度(响应式) */
|
||||
containerMinHeight,
|
||||
/** 头部元素引用 */
|
||||
headerRef,
|
||||
/** 内容头部元素引用 */
|
||||
contentHeaderRef,
|
||||
/** 头部高度(响应式) */
|
||||
headerHeight,
|
||||
/** 内容头部高度(响应式) */
|
||||
contentHeaderHeight
|
||||
}
|
||||
}
|
||||
767
saiadmin-artd/src/hooks/core/useTable.ts
Normal file
767
saiadmin-artd/src/hooks/core/useTable.ts
Normal file
@@ -0,0 +1,767 @@
|
||||
/**
|
||||
* useTable - 企业级表格数据管理方案
|
||||
*
|
||||
* 功能完整的表格数据管理解决方案,专为后台管理系统设计。
|
||||
* 封装了表格开发中的所有常见需求,让你专注于业务逻辑。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 数据管理 - 自动处理 API 请求、响应转换、加载状态和错误处理
|
||||
* 2. 分页控制 - 自动同步分页状态、移动端适配、智能页码边界处理
|
||||
* 3. 搜索功能 - 防抖搜索优化、参数管理、一键重置、参数过滤
|
||||
* 4. 缓存系统 - 智能请求缓存、多种清理策略、自动过期管理、统计信息
|
||||
* 5. 刷新策略 - 提供 5 种刷新方法适配不同业务场景(新增/更新/删除/手动/定时)
|
||||
* 6. 列配置管理 - 动态显示/隐藏列、列排序、配置持久化、批量操作(可选)
|
||||
*
|
||||
* @module useTable
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, readonly } from 'vue'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { useTableColumns } from './useTableColumns'
|
||||
import type { ColumnOption } from '@/types/component'
|
||||
import {
|
||||
TableCache,
|
||||
CacheInvalidationStrategy,
|
||||
type ApiResponse
|
||||
} from '../../utils/table/tableCache'
|
||||
import {
|
||||
type TableError,
|
||||
defaultResponseAdapter,
|
||||
extractTableData,
|
||||
updatePaginationFromResponse,
|
||||
createSmartDebounce,
|
||||
createErrorHandler
|
||||
} from '../../utils/table/tableUtils'
|
||||
import { tableConfig } from '../../utils/table/tableConfig'
|
||||
|
||||
// 类型推导工具类型
|
||||
type InferApiParams<T> = T extends (params: infer P) => any ? P : never
|
||||
type InferApiResponse<T> = T extends (params: any) => Promise<infer R> ? R : never
|
||||
type InferRecordType<T> = T extends Api.Common.PaginatedResponse<infer U> ? U : never
|
||||
|
||||
// 优化的配置接口 - 支持自动类型推导
|
||||
export interface UseTableConfig<
|
||||
TApiFn extends (params: any) => Promise<any> = (params: any) => Promise<any>,
|
||||
TRecord = InferRecordType<InferApiResponse<TApiFn>>,
|
||||
TParams = InferApiParams<TApiFn>,
|
||||
TResponse = InferApiResponse<TApiFn>
|
||||
> {
|
||||
// 核心配置
|
||||
core: {
|
||||
/** API 请求函数 */
|
||||
apiFn: TApiFn
|
||||
/** 默认请求参数 */
|
||||
apiParams?: Partial<TParams>
|
||||
/** 排除 apiParams 中的属性 */
|
||||
excludeParams?: string[]
|
||||
/** 是否立即加载数据 */
|
||||
immediate?: boolean
|
||||
/** 列配置工厂函数 */
|
||||
columnsFactory?: () => ColumnOption<TRecord>[]
|
||||
/** 自定义分页字段映射 */
|
||||
paginationKey?: {
|
||||
/** 当前页码字段名,默认为 'current' */
|
||||
current?: string
|
||||
/** 每页条数字段名,默认为 'size' */
|
||||
size?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 数据处理
|
||||
transform?: {
|
||||
/** 数据转换函数 */
|
||||
dataTransformer?: (data: TRecord[]) => TRecord[]
|
||||
/** 响应数据适配器 */
|
||||
responseAdapter?: (response: TResponse) => ApiResponse<TRecord>
|
||||
}
|
||||
|
||||
// 性能优化
|
||||
performance?: {
|
||||
/** 是否启用缓存 */
|
||||
enableCache?: boolean
|
||||
/** 缓存时间(毫秒) */
|
||||
cacheTime?: number
|
||||
/** 防抖延迟时间(毫秒) */
|
||||
debounceTime?: number
|
||||
/** 最大缓存条数限制 */
|
||||
maxCacheSize?: number
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
hooks?: {
|
||||
/** 数据加载成功回调(仅网络请求成功时触发) */
|
||||
onSuccess?: (data: TRecord[], response: ApiResponse<TRecord>) => void
|
||||
/** 错误处理回调 */
|
||||
onError?: (error: TableError) => void
|
||||
/** 缓存命中回调(从缓存获取数据时触发) */
|
||||
onCacheHit?: (data: TRecord[], response: ApiResponse<TRecord>) => void
|
||||
/** 加载状态变化回调 */
|
||||
onLoading?: (loading: boolean) => void
|
||||
/** 重置表单回调函数 */
|
||||
resetFormCallback?: () => void
|
||||
}
|
||||
|
||||
// 调试配置
|
||||
debug?: {
|
||||
/** 是否启用日志输出 */
|
||||
enableLog?: boolean
|
||||
/** 日志级别 */
|
||||
logLevel?: 'info' | 'warn' | 'error'
|
||||
}
|
||||
}
|
||||
|
||||
export function useTable<TApiFn extends (params: any) => Promise<any>>(
|
||||
config: UseTableConfig<TApiFn>
|
||||
) {
|
||||
return useTableImpl(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* useTable 的核心实现 - 强大的表格数据管理 Hook
|
||||
*
|
||||
* 提供完整的表格解决方案,包括:
|
||||
* - 数据获取与缓存
|
||||
* - 分页控制
|
||||
* - 搜索功能
|
||||
* - 智能刷新策略
|
||||
* - 错误处理
|
||||
* - 列配置管理
|
||||
*/
|
||||
function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
||||
config: UseTableConfig<TApiFn>
|
||||
) {
|
||||
type TRecord = InferRecordType<InferApiResponse<TApiFn>>
|
||||
type TParams = InferApiParams<TApiFn>
|
||||
const {
|
||||
core: {
|
||||
apiFn,
|
||||
apiParams = {} as Partial<TParams>,
|
||||
excludeParams = [],
|
||||
immediate = true,
|
||||
columnsFactory,
|
||||
paginationKey
|
||||
},
|
||||
transform: { dataTransformer, responseAdapter = defaultResponseAdapter } = {},
|
||||
performance: {
|
||||
enableCache = false,
|
||||
cacheTime = 5 * 60 * 1000,
|
||||
debounceTime = 300,
|
||||
maxCacheSize = 50
|
||||
} = {},
|
||||
hooks: { onSuccess, onError, onCacheHit, resetFormCallback } = {},
|
||||
debug: { enableLog = false } = {}
|
||||
} = config
|
||||
|
||||
// 分页字段名配置:优先使用传入的配置,否则使用全局配置
|
||||
const pageKey = paginationKey?.current || tableConfig.paginationKey.current
|
||||
const sizeKey = paginationKey?.size || tableConfig.paginationKey.size
|
||||
const orderFieldKey = tableConfig.paginationKey.orderField
|
||||
const orderTypeKey = tableConfig.paginationKey.orderType
|
||||
|
||||
// 响应式触发器,用于手动更新缓存统计信息
|
||||
const cacheUpdateTrigger = ref(0)
|
||||
|
||||
// 日志工具函数
|
||||
const logger = {
|
||||
log: (message: string, ...args: unknown[]) => {
|
||||
if (enableLog) {
|
||||
console.log(`[useTable] ${message}`, ...args)
|
||||
}
|
||||
},
|
||||
warn: (message: string, ...args: unknown[]) => {
|
||||
if (enableLog) {
|
||||
console.warn(`[useTable] ${message}`, ...args)
|
||||
}
|
||||
},
|
||||
error: (message: string, ...args: unknown[]) => {
|
||||
if (enableLog) {
|
||||
console.error(`[useTable] ${message}`, ...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存实例
|
||||
const cache = enableCache ? new TableCache<TRecord>(cacheTime, maxCacheSize, enableLog) : null
|
||||
|
||||
// 加载状态机
|
||||
type LoadingState = 'idle' | 'loading' | 'success' | 'error'
|
||||
const loadingState = ref<LoadingState>('idle')
|
||||
const loading = computed(() => loadingState.value === 'loading')
|
||||
|
||||
// 错误状态
|
||||
const error = ref<TableError | null>(null)
|
||||
|
||||
// 表格数据
|
||||
const data = ref<TRecord[]>([])
|
||||
|
||||
// 请求取消控制器
|
||||
let abortController: AbortController | null = null
|
||||
|
||||
// 缓存清理定时器
|
||||
let cacheCleanupTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive(
|
||||
Object.assign(
|
||||
{
|
||||
[pageKey]: 1,
|
||||
[sizeKey]: 10
|
||||
},
|
||||
apiParams || {}
|
||||
) as TParams
|
||||
)
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive<Api.Common.PaginationParams>({
|
||||
current: ((searchParams as Record<string, unknown>)[pageKey] as number) || 1,
|
||||
size: ((searchParams as Record<string, unknown>)[sizeKey] as number) || 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 移动端分页 (响应式)
|
||||
const { width } = useWindowSize()
|
||||
const mobilePagination = computed(() => ({
|
||||
...pagination,
|
||||
small: width.value < 768
|
||||
}))
|
||||
|
||||
// 列配置
|
||||
const columnConfig = columnsFactory ? useTableColumns<TRecord>(columnsFactory) : null
|
||||
const columns = columnConfig?.columns
|
||||
const columnChecks = columnConfig?.columnChecks
|
||||
|
||||
// 是否有数据
|
||||
const hasData = computed(() => data.value.length > 0)
|
||||
|
||||
// 缓存统计信息
|
||||
const cacheInfo = computed(() => {
|
||||
// 依赖触发器,确保缓存变化时重新计算
|
||||
void cacheUpdateTrigger.value
|
||||
if (!cache) return { total: 0, size: '0KB', hitRate: '0 avg hits' }
|
||||
return cache.getStats()
|
||||
})
|
||||
|
||||
// 错误处理函数
|
||||
const handleError = createErrorHandler(onError, enableLog)
|
||||
|
||||
// 清理缓存,根据不同的业务场景选择性地清理缓存
|
||||
const clearCache = (strategy: CacheInvalidationStrategy, context?: string): void => {
|
||||
if (!cache) return
|
||||
|
||||
let clearedCount = 0
|
||||
|
||||
switch (strategy) {
|
||||
case CacheInvalidationStrategy.CLEAR_ALL:
|
||||
cache.clear()
|
||||
logger.log(`清空所有缓存 - ${context || ''}`)
|
||||
break
|
||||
|
||||
case CacheInvalidationStrategy.CLEAR_CURRENT:
|
||||
clearedCount = cache.clearCurrentSearch(searchParams)
|
||||
logger.log(`清空当前搜索缓存 ${clearedCount} 条 - ${context || ''}`)
|
||||
break
|
||||
|
||||
case CacheInvalidationStrategy.CLEAR_PAGINATION:
|
||||
clearedCount = cache.clearPagination()
|
||||
logger.log(`清空分页缓存 ${clearedCount} 条 - ${context || ''}`)
|
||||
break
|
||||
|
||||
case CacheInvalidationStrategy.KEEP_ALL:
|
||||
default:
|
||||
logger.log(`保持缓存不变 - ${context || ''}`)
|
||||
break
|
||||
}
|
||||
// 手动触发缓存状态更新
|
||||
cacheUpdateTrigger.value++
|
||||
}
|
||||
|
||||
// 获取数据的核心方法
|
||||
const fetchData = async (
|
||||
params?: Partial<TParams>,
|
||||
useCache = enableCache
|
||||
): Promise<ApiResponse<TRecord>> => {
|
||||
// 取消上一个请求
|
||||
if (abortController) {
|
||||
abortController.abort()
|
||||
}
|
||||
|
||||
// 创建新的取消控制器
|
||||
const currentController = new AbortController()
|
||||
abortController = currentController
|
||||
|
||||
// 状态机:进入 loading 状态
|
||||
loadingState.value = 'loading'
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
let requestParams = Object.assign(
|
||||
{},
|
||||
searchParams,
|
||||
{
|
||||
[pageKey]: pagination.current,
|
||||
[sizeKey]: pagination.size
|
||||
},
|
||||
params || {}
|
||||
) as TParams
|
||||
|
||||
// 剔除不需要的参数
|
||||
if (excludeParams.length > 0) {
|
||||
const filteredParams = { ...requestParams }
|
||||
excludeParams.forEach((key) => {
|
||||
delete (filteredParams as Record<string, unknown>)[key]
|
||||
})
|
||||
requestParams = filteredParams as TParams
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
if (useCache && cache) {
|
||||
const cachedItem = cache.get(requestParams)
|
||||
if (cachedItem) {
|
||||
data.value = cachedItem.data
|
||||
updatePaginationFromResponse(pagination, cachedItem.response)
|
||||
|
||||
// 修复:避免重复设置相同的值,防止响应式循环更新
|
||||
const paramsRecord = searchParams as Record<string, unknown>
|
||||
if (paramsRecord[pageKey] !== pagination.current) {
|
||||
paramsRecord[pageKey] = pagination.current
|
||||
}
|
||||
if (paramsRecord[sizeKey] !== pagination.size) {
|
||||
paramsRecord[sizeKey] = pagination.size
|
||||
}
|
||||
|
||||
// 状态机:缓存命中,进入 success 状态
|
||||
loadingState.value = 'success'
|
||||
|
||||
// 缓存命中时触发专门的回调,而不是 onSuccess
|
||||
if (onCacheHit) {
|
||||
onCacheHit(cachedItem.data, cachedItem.response)
|
||||
}
|
||||
|
||||
logger.log(`缓存命中`)
|
||||
return cachedItem.response
|
||||
}
|
||||
}
|
||||
|
||||
const response = await apiFn(requestParams)
|
||||
|
||||
// 检查请求是否被取消
|
||||
if (currentController.signal.aborted) {
|
||||
throw new Error('请求已取消')
|
||||
}
|
||||
|
||||
// 使用响应适配器转换为标准格式
|
||||
const standardResponse = responseAdapter(response)
|
||||
|
||||
// 处理响应数据
|
||||
let tableData = extractTableData(standardResponse)
|
||||
|
||||
// 应用数据转换函数
|
||||
if (dataTransformer) {
|
||||
tableData = dataTransformer(tableData)
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
data.value = tableData
|
||||
updatePaginationFromResponse(pagination, standardResponse)
|
||||
|
||||
// 修复:避免重复设置相同的值,防止响应式循环更新
|
||||
const paramsRecord = searchParams as Record<string, unknown>
|
||||
if (paramsRecord[pageKey] !== pagination.current) {
|
||||
paramsRecord[pageKey] = pagination.current
|
||||
}
|
||||
if (paramsRecord[sizeKey] !== pagination.size) {
|
||||
paramsRecord[sizeKey] = pagination.size
|
||||
}
|
||||
|
||||
// 缓存数据
|
||||
if (useCache && cache) {
|
||||
cache.set(requestParams, tableData, standardResponse)
|
||||
// 手动触发缓存状态更新
|
||||
cacheUpdateTrigger.value++
|
||||
logger.log(`数据已缓存`)
|
||||
}
|
||||
|
||||
// 状态机:请求成功,进入 success 状态
|
||||
loadingState.value = 'success'
|
||||
|
||||
// 成功回调
|
||||
if (onSuccess) {
|
||||
onSuccess(tableData, standardResponse)
|
||||
}
|
||||
|
||||
return standardResponse
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message === '请求已取消') {
|
||||
// 请求被取消,回到 idle 状态
|
||||
loadingState.value = 'idle'
|
||||
return { records: [], total: 0, current: 1, size: 10 }
|
||||
}
|
||||
|
||||
// 状态机:请求失败,进入 error 状态
|
||||
loadingState.value = 'error'
|
||||
data.value = []
|
||||
const tableError = handleError(err, '获取表格数据失败')
|
||||
throw tableError
|
||||
} finally {
|
||||
// 只有当前控制器是活跃的才清空
|
||||
if (abortController === currentController) {
|
||||
abortController = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取数据 (保持当前页)
|
||||
const getData = async (params?: Partial<TParams>): Promise<ApiResponse<TRecord> | void> => {
|
||||
try {
|
||||
return await fetchData(params)
|
||||
} catch {
|
||||
// 错误已在 fetchData 中处理
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
// 分页获取数据 (重置到第一页) - 专门用于搜索场景
|
||||
const getDataByPage = async (params?: Partial<TParams>): Promise<ApiResponse<TRecord> | void> => {
|
||||
pagination.current = 1
|
||||
;(searchParams as Record<string, unknown>)[pageKey] = 1
|
||||
|
||||
// 搜索时清空当前搜索条件的缓存,确保获取最新数据
|
||||
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '搜索数据')
|
||||
|
||||
try {
|
||||
return await fetchData(params, false) // 搜索时不使用缓存
|
||||
} catch {
|
||||
// 错误已在 fetchData 中处理
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
// 智能防抖搜索函数
|
||||
const debouncedGetDataByPage = createSmartDebounce(getDataByPage, debounceTime)
|
||||
|
||||
// 重置搜索参数
|
||||
const resetSearchParams = async (): Promise<void> => {
|
||||
// 取消防抖的搜索
|
||||
debouncedGetDataByPage.cancel()
|
||||
|
||||
// 保存分页相关的默认值
|
||||
const paramsRecord = searchParams as Record<string, unknown>
|
||||
const defaultPagination = {
|
||||
[pageKey]: 1,
|
||||
[sizeKey]: (paramsRecord[sizeKey] as number) || 10
|
||||
}
|
||||
|
||||
// 清空所有搜索参数
|
||||
Object.keys(searchParams).forEach((key) => {
|
||||
delete paramsRecord[key]
|
||||
})
|
||||
|
||||
// 重新设置默认参数
|
||||
Object.assign(searchParams, apiParams || {}, defaultPagination)
|
||||
|
||||
// 重置分页
|
||||
pagination.current = 1
|
||||
pagination.size = defaultPagination[sizeKey] as number
|
||||
|
||||
// 清空错误状态
|
||||
error.value = null
|
||||
|
||||
// 清空缓存
|
||||
clearCache(CacheInvalidationStrategy.CLEAR_ALL, '重置搜索')
|
||||
|
||||
// 重新获取数据
|
||||
await getData()
|
||||
|
||||
// 执行重置回调
|
||||
if (resetFormCallback) {
|
||||
await nextTick()
|
||||
resetFormCallback()
|
||||
}
|
||||
}
|
||||
|
||||
// 防重复调用的标志
|
||||
let isCurrentChanging = false
|
||||
|
||||
// 处理分页大小变化
|
||||
const handleSizeChange = async (newSize: number): Promise<void> => {
|
||||
if (newSize <= 0) return
|
||||
|
||||
debouncedGetDataByPage.cancel()
|
||||
|
||||
const paramsRecord = searchParams as Record<string, unknown>
|
||||
pagination.size = newSize
|
||||
pagination.current = 1
|
||||
paramsRecord[sizeKey] = newSize
|
||||
paramsRecord[pageKey] = 1
|
||||
|
||||
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '分页大小变化')
|
||||
|
||||
await getData()
|
||||
}
|
||||
|
||||
// 处理当前页变化
|
||||
const handleCurrentChange = async (newCurrent: number): Promise<void> => {
|
||||
if (newCurrent <= 0) return
|
||||
|
||||
// 修复:防止重复调用
|
||||
if (isCurrentChanging) {
|
||||
return
|
||||
}
|
||||
|
||||
// 修复:如果当前页没有变化,不需要重新请求
|
||||
if (pagination.current === newCurrent) {
|
||||
logger.log('分页页码未变化,跳过请求')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isCurrentChanging = true
|
||||
|
||||
// 修复:只更新必要的状态
|
||||
const paramsRecord = searchParams as Record<string, unknown>
|
||||
pagination.current = newCurrent
|
||||
// 只有当 searchParams 的分页字段与新值不同时才更新
|
||||
if (paramsRecord[pageKey] !== newCurrent) {
|
||||
paramsRecord[pageKey] = newCurrent
|
||||
}
|
||||
|
||||
await getData()
|
||||
} finally {
|
||||
isCurrentChanging = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理表格排序变化:更新查询参数中的排序字段与排序类型,并请求后端数据
|
||||
const handleSortChange = async (payload: {
|
||||
column?: unknown
|
||||
prop?: string
|
||||
order?: 'ascending' | 'descending' | null
|
||||
}): Promise<void> => {
|
||||
const paramsRecord = searchParams as Record<string, unknown>
|
||||
|
||||
// 如果清除排序,则移除相关查询参数
|
||||
if (!payload.order || !payload.prop) {
|
||||
delete paramsRecord[orderFieldKey]
|
||||
delete paramsRecord[orderTypeKey]
|
||||
} else {
|
||||
paramsRecord[orderFieldKey] = payload.prop
|
||||
paramsRecord[orderTypeKey] = payload.order === 'ascending' ? 'asc' : 'desc'
|
||||
}
|
||||
|
||||
// 排序变化通常回到第一页以保持数据一致性
|
||||
pagination.current = 1
|
||||
paramsRecord[pageKey] = 1
|
||||
|
||||
// 清除当前搜索缓存,确保拿到最新排序数据
|
||||
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '排序变化')
|
||||
|
||||
await getData()
|
||||
}
|
||||
|
||||
// 针对不同业务场景的刷新方法
|
||||
|
||||
// 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后)
|
||||
const refreshCreate = async (): Promise<void> => {
|
||||
debouncedGetDataByPage.cancel()
|
||||
pagination.current = 1
|
||||
;(searchParams as Record<string, unknown>)[pageKey] = 1
|
||||
clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '新增数据')
|
||||
await getData()
|
||||
}
|
||||
|
||||
// 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后)
|
||||
const refreshUpdate = async (): Promise<void> => {
|
||||
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '编辑数据')
|
||||
await getData()
|
||||
}
|
||||
|
||||
// 删除后刷新:智能处理页码,避免空页面(适用于删除数据后)
|
||||
const refreshRemove = async (): Promise<void> => {
|
||||
const { current } = pagination
|
||||
|
||||
// 清除缓存并获取最新数据
|
||||
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '删除数据')
|
||||
await getData()
|
||||
|
||||
// 如果当前页为空且不是第一页,回到上一页
|
||||
if (data.value.length === 0 && current > 1) {
|
||||
pagination.current = current - 1
|
||||
;(searchParams as Record<string, unknown>)[pageKey] = current - 1
|
||||
await getData()
|
||||
}
|
||||
}
|
||||
|
||||
// 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮)
|
||||
const refreshData = async (): Promise<void> => {
|
||||
debouncedGetDataByPage.cancel()
|
||||
clearCache(CacheInvalidationStrategy.CLEAR_ALL, '手动刷新')
|
||||
await getData()
|
||||
}
|
||||
|
||||
// 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新)
|
||||
const refreshSoft = async (): Promise<void> => {
|
||||
clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '软刷新')
|
||||
await getData()
|
||||
}
|
||||
|
||||
// 取消当前请求
|
||||
const cancelRequest = (): void => {
|
||||
if (abortController) {
|
||||
abortController.abort()
|
||||
}
|
||||
debouncedGetDataByPage.cancel()
|
||||
}
|
||||
|
||||
// 清空数据
|
||||
const clearData = (): void => {
|
||||
data.value = []
|
||||
error.value = null
|
||||
clearCache(CacheInvalidationStrategy.CLEAR_ALL, '清空数据')
|
||||
}
|
||||
|
||||
// 清理已过期的缓存条目,释放内存空间
|
||||
const clearExpiredCache = (): number => {
|
||||
if (!cache) return 0
|
||||
const cleanedCount = cache.cleanupExpired()
|
||||
if (cleanedCount > 0) {
|
||||
// 手动触发缓存状态更新
|
||||
cacheUpdateTrigger.value++
|
||||
}
|
||||
return cleanedCount
|
||||
}
|
||||
|
||||
// 设置定期清理过期缓存
|
||||
if (enableCache && cache) {
|
||||
cacheCleanupTimer = setInterval(() => {
|
||||
const cleanedCount = cache.cleanupExpired()
|
||||
if (cleanedCount > 0) {
|
||||
logger.log(`自动清理 ${cleanedCount} 条过期缓存`)
|
||||
// 手动触发缓存状态更新
|
||||
cacheUpdateTrigger.value++
|
||||
}
|
||||
}, cacheTime / 2) // 每半个缓存周期清理一次
|
||||
}
|
||||
|
||||
// 挂载时自动加载数据
|
||||
if (immediate) {
|
||||
onMounted(async () => {
|
||||
await getData()
|
||||
})
|
||||
}
|
||||
|
||||
// 组件卸载时彻底清理
|
||||
onUnmounted(() => {
|
||||
cancelRequest()
|
||||
if (cache) {
|
||||
cache.clear()
|
||||
}
|
||||
if (cacheCleanupTimer) {
|
||||
clearInterval(cacheCleanupTimer)
|
||||
}
|
||||
})
|
||||
|
||||
// 优化的返回值结构
|
||||
return {
|
||||
// 数据相关
|
||||
/** 表格数据 */
|
||||
data,
|
||||
/** 数据加载状态 */
|
||||
loading: readonly(loading),
|
||||
/** 错误状态 */
|
||||
error: readonly(error),
|
||||
/** 数据是否为空 */
|
||||
isEmpty: computed(() => data.value.length === 0),
|
||||
/** 是否有数据 */
|
||||
hasData,
|
||||
|
||||
// 分页相关
|
||||
/** 分页状态信息 */
|
||||
pagination: readonly(pagination),
|
||||
/** 移动端分页配置 */
|
||||
paginationMobile: mobilePagination,
|
||||
/** 页面大小变化处理 */
|
||||
handleSizeChange,
|
||||
/** 当前页变化处理 */
|
||||
handleCurrentChange,
|
||||
/** 排序变化处理 */
|
||||
handleSortChange,
|
||||
|
||||
// 搜索相关 - 统一前缀
|
||||
/** 搜索参数 */
|
||||
searchParams,
|
||||
/** 重置搜索参数 */
|
||||
resetSearchParams,
|
||||
|
||||
// 数据操作 - 更明确的操作意图
|
||||
/** 加载数据 */
|
||||
fetchData: getData,
|
||||
/** 获取数据 */
|
||||
getData: getDataByPage,
|
||||
/** 获取数据(防抖) */
|
||||
getDataDebounced: debouncedGetDataByPage,
|
||||
/** 清空数据 */
|
||||
clearData,
|
||||
|
||||
// 刷新策略
|
||||
/** 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮) */
|
||||
refreshData,
|
||||
/** 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新) */
|
||||
refreshSoft,
|
||||
/** 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后) */
|
||||
refreshCreate,
|
||||
/** 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后) */
|
||||
refreshUpdate,
|
||||
/** 删除后刷新:智能处理页码,避免空页面(适用于删除数据后) */
|
||||
refreshRemove,
|
||||
|
||||
// 缓存控制
|
||||
/** 缓存统计信息 */
|
||||
cacheInfo,
|
||||
/** 清除缓存,根据不同的业务场景选择性地清理缓存: */
|
||||
clearCache,
|
||||
// 支持4种清理策略
|
||||
// clearCache(CacheInvalidationStrategy.CLEAR_ALL, '手动刷新') // 清空所有缓存
|
||||
// clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '搜索数据') // 只清空当前搜索条件的缓存
|
||||
// clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '新增数据') // 清空分页相关缓存
|
||||
// clearCache(CacheInvalidationStrategy.KEEP_ALL, '保持缓存') // 不清理任何缓存
|
||||
/** 清理已过期的缓存条目,释放内存空间 */
|
||||
clearExpiredCache,
|
||||
|
||||
// 请求控制
|
||||
/** 取消当前请求 */
|
||||
cancelRequest,
|
||||
|
||||
// 列配置 (如果提供了 columnsFactory)
|
||||
...(columnConfig && {
|
||||
/** 表格列配置 */
|
||||
columns,
|
||||
/** 列显示控制 */
|
||||
columnChecks,
|
||||
/** 新增列 */
|
||||
addColumn: columnConfig.addColumn,
|
||||
/** 删除列 */
|
||||
removeColumn: columnConfig.removeColumn,
|
||||
/** 切换列显示状态 */
|
||||
toggleColumn: columnConfig.toggleColumn,
|
||||
/** 更新列配置 */
|
||||
updateColumn: columnConfig.updateColumn,
|
||||
/** 批量更新列配置 */
|
||||
batchUpdateColumns: columnConfig.batchUpdateColumns,
|
||||
/** 重新排序列 */
|
||||
reorderColumns: columnConfig.reorderColumns,
|
||||
/** 获取指定列配置 */
|
||||
getColumnConfig: columnConfig.getColumnConfig,
|
||||
/** 获取所有列配置 */
|
||||
getAllColumns: columnConfig.getAllColumns,
|
||||
/** 重置所有列配置到默认状态 */
|
||||
resetColumns: columnConfig.resetColumns
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 重新导出类型和枚举,方便使用
|
||||
export { CacheInvalidationStrategy } from '../../utils/table/tableCache'
|
||||
export type { ApiResponse, CacheItem } from '../../utils/table/tableCache'
|
||||
export type { BaseRequestParams, TableError } from '../../utils/table/tableUtils'
|
||||
312
saiadmin-artd/src/hooks/core/useTableColumns.ts
Normal file
312
saiadmin-artd/src/hooks/core/useTableColumns.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
/**
|
||||
* useTableColumns - 表格列配置管理
|
||||
*
|
||||
* 提供动态的表格列配置管理能力,支持运行时灵活控制列的显示、隐藏、排序等操作。
|
||||
* 通常与 useTable 配合使用,为表格提供完整的列管理功能。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 列显示控制 - 动态显示/隐藏列,支持批量操作
|
||||
* 2. 列排序 - 拖拽或编程方式重新排列列顺序
|
||||
* 3. 列配置管理 - 新增、删除、更新列配置
|
||||
* 4. 特殊列支持 - 自动处理 selection、expand、index 等特殊列
|
||||
* 5. 状态持久化 - 保持列的显示状态,支持重置到初始状态
|
||||
*
|
||||
* ## 使用示例
|
||||
*
|
||||
* ```typescript
|
||||
* const { columns, columnChecks, toggleColumn, reorderColumns } = useTableColumns(() => [
|
||||
* { prop: 'name', label: '姓名', visible: true },
|
||||
* { prop: 'email', label: '邮箱', visible: true },
|
||||
* { prop: 'status', label: '状态', visible: false }
|
||||
* ])
|
||||
*
|
||||
* // 切换列显示
|
||||
* toggleColumn('email', false)
|
||||
*
|
||||
* // 重新排序
|
||||
* reorderColumns(0, 2)
|
||||
* ```
|
||||
*
|
||||
* @module useTableColumns
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { $t } from '@/locales'
|
||||
import type { ColumnOption } from '@/types/component'
|
||||
|
||||
/**
|
||||
* 特殊列类型
|
||||
*/
|
||||
const SPECIAL_COLUMNS: Record<string, { prop: string; label: string }> = {
|
||||
selection: { prop: '__selection__', label: $t('table.column.selection') },
|
||||
expand: { prop: '__expand__', label: $t('table.column.expand') },
|
||||
index: { prop: '__index__', label: $t('table.column.index') }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列的唯一标识
|
||||
*/
|
||||
export const getColumnKey = <T>(col: ColumnOption<T>) =>
|
||||
SPECIAL_COLUMNS[col.type as keyof typeof SPECIAL_COLUMNS]?.prop ?? (col.prop as string)
|
||||
|
||||
/**
|
||||
* 获取列的显示状态
|
||||
* 优先使用 visible 字段,如果不存在则使用 checked 字段
|
||||
*/
|
||||
export const getColumnVisibility = <T>(col: ColumnOption<T>): boolean => {
|
||||
// visible 优先级高于 checked
|
||||
if (col.visible !== undefined) {
|
||||
return col.visible
|
||||
}
|
||||
// 如果 visible 未定义,使用 checked,默认为 true
|
||||
return col.checked ?? true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列的检查状态
|
||||
*/
|
||||
export const getColumnChecks = <T>(columns: ColumnOption<T>[]) =>
|
||||
columns.map((col) => {
|
||||
const special = col.type && SPECIAL_COLUMNS[col.type]
|
||||
const visibility = getColumnVisibility(col)
|
||||
|
||||
if (special) {
|
||||
return { ...col, prop: special.prop, label: special.label, checked: true, visible: true }
|
||||
}
|
||||
return { ...col, checked: visibility, visible: visibility }
|
||||
})
|
||||
|
||||
/**
|
||||
* 动态列配置接口
|
||||
*/
|
||||
export interface DynamicColumnConfig<T = any> {
|
||||
/**
|
||||
* 新增列(支持单个或批量)
|
||||
* @param column 列配置或列配置数组
|
||||
* @param index 可选的插入位置,默认末尾(批量时为第一个列的位置)
|
||||
*/
|
||||
addColumn: (column: ColumnOption<T> | ColumnOption<T>[], index?: number) => void
|
||||
/**
|
||||
* 删除列(支持单个或批量)
|
||||
* @param prop 列的唯一标识或标识数组
|
||||
*/
|
||||
removeColumn: (prop: string | string[]) => void
|
||||
/**
|
||||
* 切换列显示状态(支持单个或批量)
|
||||
* @param prop 列的唯一标识或标识数组
|
||||
* @param visible 可选的显示状态,默认取反
|
||||
*/
|
||||
toggleColumn: (prop: string | string[], visible?: boolean) => void
|
||||
|
||||
/**
|
||||
* 更新列(支持单个或批量)
|
||||
* @param prop 列的唯一标识或更新配置数组
|
||||
* @param updates 列配置更新(当 prop 为字符串时使用)
|
||||
*/
|
||||
updateColumn: (
|
||||
prop: string | Array<{ prop: string; updates: Partial<ColumnOption<T>> }>,
|
||||
updates?: Partial<ColumnOption<T>>
|
||||
) => void
|
||||
/**
|
||||
* 批量更新列(兼容旧版本,推荐使用 updateColumn 的数组模式)
|
||||
* @param updates 列更新配置
|
||||
* @deprecated 推荐使用 updateColumn 的数组模式
|
||||
*/
|
||||
batchUpdateColumns: (updates: Array<{ prop: string; updates: Partial<ColumnOption<T>> }>) => void
|
||||
/**
|
||||
* 重新排序列
|
||||
* @param fromIndex 源索引
|
||||
* @param toIndex 目标索引
|
||||
*/
|
||||
reorderColumns: (fromIndex: number, toIndex: number) => void
|
||||
/**
|
||||
* 获取列配置
|
||||
* @param prop 列的唯一标识
|
||||
* @returns 列配置
|
||||
*/
|
||||
getColumnConfig: (prop: string) => ColumnOption<T> | undefined
|
||||
/**
|
||||
* 获取所有列配置
|
||||
* @returns 所有列配置
|
||||
*/
|
||||
getAllColumns: () => ColumnOption<T>[]
|
||||
/**
|
||||
* 重置所有列
|
||||
*/
|
||||
resetColumns: () => void
|
||||
}
|
||||
|
||||
export function useTableColumns<T = any>(
|
||||
columnsFactory: () => ColumnOption<T>[]
|
||||
): {
|
||||
columns: any
|
||||
columnChecks: any
|
||||
} & DynamicColumnConfig<T> {
|
||||
const dynamicColumns = ref<ColumnOption<T>[]>(columnsFactory())
|
||||
const columnChecks = ref<ColumnOption<T>[]>(getColumnChecks(dynamicColumns.value))
|
||||
|
||||
// 当 dynamicColumns 变动时,重新生成 columnChecks 且保留已存在的显示状态
|
||||
watch(
|
||||
dynamicColumns,
|
||||
(newCols) => {
|
||||
const visibilityMap = new Map(
|
||||
columnChecks.value.map((c) => [getColumnKey(c), getColumnVisibility(c)])
|
||||
)
|
||||
const newChecks = getColumnChecks(newCols).map((c) => {
|
||||
const key = getColumnKey(c)
|
||||
const visibility = visibilityMap.has(key) ? visibilityMap.get(key) : getColumnVisibility(c)
|
||||
return {
|
||||
...c,
|
||||
checked: visibility,
|
||||
visible: visibility
|
||||
}
|
||||
})
|
||||
columnChecks.value = newChecks
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 当前显示列(基于 columnChecks 的 checked 或 visible)
|
||||
const columns = computed(() => {
|
||||
const colMap = new Map(dynamicColumns.value.map((c) => [getColumnKey(c), c]))
|
||||
return columnChecks.value
|
||||
.filter((c) => getColumnVisibility(c))
|
||||
.map((c) => colMap.get(getColumnKey(c)))
|
||||
.filter(Boolean) as ColumnOption<T>[]
|
||||
})
|
||||
|
||||
// 支持 updater 返回新数组或直接在传入数组上 mutate
|
||||
const setDynamicColumns = (updater: (cols: ColumnOption<T>[]) => void | ColumnOption<T>[]) => {
|
||||
const copy = [...dynamicColumns.value]
|
||||
const result = updater(copy)
|
||||
dynamicColumns.value = Array.isArray(result) ? result : copy
|
||||
}
|
||||
|
||||
return {
|
||||
columns,
|
||||
columnChecks,
|
||||
|
||||
/**
|
||||
* 新增列(支持单个或批量)
|
||||
*/
|
||||
addColumn: (column: ColumnOption<T> | ColumnOption<T>[], index?: number) =>
|
||||
setDynamicColumns((cols) => {
|
||||
const next = [...cols]
|
||||
const columnsToAdd = Array.isArray(column) ? column : [column]
|
||||
const insertIndex =
|
||||
typeof index === 'number' && index >= 0 && index <= next.length ? index : next.length
|
||||
|
||||
// 批量插入
|
||||
next.splice(insertIndex, 0, ...columnsToAdd)
|
||||
return next
|
||||
}),
|
||||
|
||||
/**
|
||||
* 删除列(支持单个或批量)
|
||||
*/
|
||||
removeColumn: (prop: string | string[]) =>
|
||||
setDynamicColumns((cols) => {
|
||||
const propsToRemove = Array.isArray(prop) ? prop : [prop]
|
||||
return cols.filter((c) => !propsToRemove.includes(getColumnKey(c)))
|
||||
}),
|
||||
|
||||
/**
|
||||
* 更新列(支持单个或批量)
|
||||
*/
|
||||
updateColumn: (
|
||||
prop: string | Array<{ prop: string; updates: Partial<ColumnOption<T>> }>,
|
||||
updates?: Partial<ColumnOption<T>>
|
||||
) => {
|
||||
// 批量模式:prop 是数组
|
||||
if (Array.isArray(prop)) {
|
||||
setDynamicColumns((cols) => {
|
||||
const map = new Map(prop.map((u) => [u.prop, u.updates]))
|
||||
return cols.map((c) => {
|
||||
const key = getColumnKey(c)
|
||||
const upd = map.get(key)
|
||||
return upd ? { ...c, ...upd } : c
|
||||
})
|
||||
})
|
||||
}
|
||||
// 单个模式:prop 是字符串
|
||||
else if (updates) {
|
||||
setDynamicColumns((cols) =>
|
||||
cols.map((c) => (getColumnKey(c) === prop ? { ...c, ...updates } : c))
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换列显示状态(支持单个或批量)
|
||||
*/
|
||||
toggleColumn: (prop: string | string[], visible?: boolean) => {
|
||||
const propsToToggle = Array.isArray(prop) ? prop : [prop]
|
||||
const next = [...columnChecks.value]
|
||||
|
||||
propsToToggle.forEach((p) => {
|
||||
const i = next.findIndex((c) => getColumnKey(c) === p)
|
||||
if (i > -1) {
|
||||
const currentVisibility = getColumnVisibility(next[i])
|
||||
const newVisibility = visible ?? !currentVisibility
|
||||
// 同时更新 checked 和 visible 以保持兼容性
|
||||
next[i] = { ...next[i], checked: newVisibility, visible: newVisibility }
|
||||
}
|
||||
})
|
||||
|
||||
columnChecks.value = next
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置所有列
|
||||
*/
|
||||
resetColumns: () => {
|
||||
dynamicColumns.value = columnsFactory()
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量更新列(兼容旧版本)
|
||||
* @deprecated 推荐使用 updateColumn 的数组模式
|
||||
*/
|
||||
batchUpdateColumns: (updates) =>
|
||||
setDynamicColumns((cols) => {
|
||||
const map = new Map(updates.map((u) => [u.prop, u.updates]))
|
||||
return cols.map((c) => {
|
||||
const key = getColumnKey(c)
|
||||
const upd = map.get(key)
|
||||
return upd ? { ...c, ...upd } : c
|
||||
})
|
||||
}),
|
||||
|
||||
/**
|
||||
* 重新排序列
|
||||
*/
|
||||
reorderColumns: (fromIndex: number, toIndex: number) =>
|
||||
setDynamicColumns((cols) => {
|
||||
if (
|
||||
fromIndex < 0 ||
|
||||
fromIndex >= cols.length ||
|
||||
toIndex < 0 ||
|
||||
toIndex >= cols.length ||
|
||||
fromIndex === toIndex
|
||||
) {
|
||||
return cols
|
||||
}
|
||||
const next = [...cols]
|
||||
const [moved] = next.splice(fromIndex, 1)
|
||||
next.splice(toIndex, 0, moved)
|
||||
return next
|
||||
}),
|
||||
|
||||
/**
|
||||
* 获取列配置
|
||||
*/
|
||||
getColumnConfig: (prop: string) => dynamicColumns.value.find((c) => getColumnKey(c) === prop),
|
||||
|
||||
/**
|
||||
* 获取所有列配置
|
||||
*/
|
||||
getAllColumns: () => [...dynamicColumns.value]
|
||||
}
|
||||
}
|
||||
105
saiadmin-artd/src/hooks/core/useTableHeight.ts
Normal file
105
saiadmin-artd/src/hooks/core/useTableHeight.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* useTableHeight - 表格高度自动计算
|
||||
*
|
||||
* 自动计算表格容器的最佳高度,确保表格在不同布局场景下都能正确显示。
|
||||
* 根据表格头部、分页器等元素的高度动态调整容器高度,避免出现滚动条或布局错乱。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 动态高度计算 - 根据表格头部、分页器高度自动计算容器高度
|
||||
* 2. 响应式更新 - 配置变化时自动重新计算高度
|
||||
* 3. 灵活配置 - 支持自定义各部分高度和间距
|
||||
* 4. 智能适配 - 无额外元素时自动使用 100% 高度
|
||||
*
|
||||
* @module useTableHeight
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { computed, type Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* 表格高度计算器配置接口
|
||||
*/
|
||||
interface TableHeightOptions {
|
||||
/** 是否显示表格头部 */
|
||||
showTableHeader: Ref<boolean>
|
||||
/** 分页器高度 */
|
||||
paginationHeight: Ref<number>
|
||||
/** 表格头部高度 */
|
||||
tableHeaderHeight: Ref<number>
|
||||
/** 分页器间距 */
|
||||
paginationSpacing: Ref<number>
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格高度计算器类
|
||||
*/
|
||||
class TableHeightCalculator {
|
||||
// 常量配置
|
||||
private static readonly DEFAULT_TABLE_HEADER_HEIGHT = 44
|
||||
private static readonly TABLE_HEADER_SPACING = 12
|
||||
|
||||
constructor(private options: TableHeightOptions) {}
|
||||
|
||||
/**
|
||||
* 计算容器高度
|
||||
*/
|
||||
calculate(): { height: string } {
|
||||
const offset = this.calculateOffset()
|
||||
return {
|
||||
height: offset === 0 ? '100%' : `calc(100% - ${offset}px)`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算偏移量
|
||||
*/
|
||||
private calculateOffset(): number {
|
||||
if (!this.options.showTableHeader.value) {
|
||||
return this.calculatePaginationOffset()
|
||||
}
|
||||
|
||||
const headerHeight = this.getHeaderHeight()
|
||||
const paginationOffset = this.calculatePaginationOffset()
|
||||
|
||||
return headerHeight + paginationOffset + TableHeightCalculator.TABLE_HEADER_SPACING
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表格头部高度
|
||||
*/
|
||||
private getHeaderHeight(): number {
|
||||
return this.options.tableHeaderHeight.value || TableHeightCalculator.DEFAULT_TABLE_HEADER_HEIGHT
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算分页器偏移量
|
||||
*/
|
||||
private calculatePaginationOffset(): number {
|
||||
const { paginationHeight, paginationSpacing } = this.options
|
||||
return paginationHeight.value === 0 ? 0 : paginationHeight.value + paginationSpacing.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格高度计算 Hook
|
||||
*
|
||||
* 提供表格容器高度的自动计算功能,支持:
|
||||
* - 表格头部高度
|
||||
* - 分页器高度
|
||||
* - 动态间距计算
|
||||
*
|
||||
* @param options 配置选项
|
||||
* @returns 容器高度计算结果
|
||||
*/
|
||||
export function useTableHeight(options: TableHeightOptions) {
|
||||
const containerHeight = computed(() => {
|
||||
const calculator = new TableHeightCalculator(options)
|
||||
return calculator.calculate()
|
||||
})
|
||||
|
||||
return {
|
||||
/** 容器高度样式对象 */
|
||||
containerHeight
|
||||
}
|
||||
}
|
||||
174
saiadmin-artd/src/hooks/core/useTheme.ts
Normal file
174
saiadmin-artd/src/hooks/core/useTheme.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* useTheme - 系统主题管理
|
||||
*
|
||||
* 提供完整的主题切换和管理功能,支持亮色、暗色和自动模式。
|
||||
* 自动处理主题切换时的过渡效果,确保切换流畅无闪烁。
|
||||
*
|
||||
* ## 主要功能
|
||||
*
|
||||
* 1. 主题切换 - 支持亮色、暗色、自动三种主题模式
|
||||
* 2. 自动模式 - 根据系统偏好自动切换主题
|
||||
* 3. 颜色适配 - 自动调整主题色的明暗变体(9 个层级)
|
||||
* 4. 过渡优化 - 切换时临时禁用过渡效果,避免闪烁
|
||||
* 5. 状态持久化 - 主题设置自动保存到 store
|
||||
*
|
||||
* ## 使用示例
|
||||
*
|
||||
* ```typescript
|
||||
* const { switchThemeStyles } = useTheme()
|
||||
*
|
||||
* // 切换到暗色主题
|
||||
* switchThemeStyles(SystemThemeEnum.DARK)
|
||||
*
|
||||
* // 切换到亮色主题
|
||||
* switchThemeStyles(SystemThemeEnum.LIGHT)
|
||||
*
|
||||
* // 切换到自动模式(跟随系统)
|
||||
* switchThemeStyles(SystemThemeEnum.AUTO)
|
||||
* ```
|
||||
*
|
||||
* @module useTheme
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { SystemThemeEnum } from '@/enums/appEnum'
|
||||
import AppConfig from '@/config'
|
||||
import { SystemThemeTypes } from '@/types/store'
|
||||
import { getDarkColor, getLightColor, setElementThemeColor } from '@/utils/ui'
|
||||
import { usePreferredDark } from '@vueuse/core'
|
||||
import { watch } from 'vue'
|
||||
|
||||
export function useTheme() {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
// 禁用过渡效果
|
||||
const disableTransitions = () => {
|
||||
const style = document.createElement('style')
|
||||
style.setAttribute('id', 'disable-transitions')
|
||||
style.textContent = '* { transition: none !important; }'
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
|
||||
// 启用过渡效果
|
||||
const enableTransitions = () => {
|
||||
const style = document.getElementById('disable-transitions')
|
||||
if (style) {
|
||||
style.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// 设置系统主题
|
||||
const setSystemTheme = (theme: SystemThemeEnum, themeMode?: SystemThemeEnum) => {
|
||||
// 临时禁用过渡效果
|
||||
disableTransitions()
|
||||
|
||||
const el = document.getElementsByTagName('html')[0]
|
||||
const isDark = theme === SystemThemeEnum.DARK
|
||||
|
||||
if (!themeMode) {
|
||||
themeMode = theme
|
||||
}
|
||||
|
||||
const currentTheme = AppConfig.systemThemeStyles[theme as keyof SystemThemeTypes]
|
||||
|
||||
if (currentTheme) {
|
||||
el.setAttribute('class', currentTheme.className)
|
||||
}
|
||||
|
||||
// 设置按钮颜色加深或变浅
|
||||
const primary = settingStore.systemThemeColor
|
||||
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
document.documentElement.style.setProperty(
|
||||
`--el-color-primary-light-${i}`,
|
||||
isDark ? `${getDarkColor(primary, i / 10)}` : `${getLightColor(primary, i / 10)}`
|
||||
)
|
||||
}
|
||||
|
||||
// 更新store中的主题设置
|
||||
settingStore.setGlopTheme(theme, themeMode)
|
||||
|
||||
// 使用 requestAnimationFrame 确保在下一帧恢复过渡效果
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
enableTransitions()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 使用 VueUse 的 usePreferredDark 检测系统主题偏好
|
||||
const prefersDark = usePreferredDark()
|
||||
|
||||
// 自动设置系统主题
|
||||
const setSystemAutoTheme = () => {
|
||||
const theme = prefersDark.value ? SystemThemeEnum.DARK : SystemThemeEnum.LIGHT
|
||||
setSystemTheme(theme, SystemThemeEnum.AUTO)
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
const switchThemeStyles = (theme: SystemThemeEnum) => {
|
||||
if (theme === SystemThemeEnum.AUTO) {
|
||||
setSystemAutoTheme()
|
||||
} else {
|
||||
setSystemTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
setSystemTheme,
|
||||
setSystemAutoTheme,
|
||||
switchThemeStyles,
|
||||
prefersDark
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主题系统
|
||||
*/
|
||||
export function initializeTheme() {
|
||||
const settingStore = useSettingStore()
|
||||
const prefersDark = usePreferredDark()
|
||||
|
||||
// 根据系统偏好应用主题
|
||||
const applyThemeByMode = () => {
|
||||
const el = document.getElementsByTagName('html')[0]
|
||||
let actualTheme = settingStore.systemThemeType
|
||||
|
||||
// 如果是 AUTO 模式,检测系统偏好
|
||||
if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) {
|
||||
actualTheme = prefersDark.value ? SystemThemeEnum.DARK : SystemThemeEnum.LIGHT
|
||||
// 更新实际应用的主题类型
|
||||
settingStore.systemThemeType = actualTheme
|
||||
}
|
||||
|
||||
// 设置主题 class
|
||||
const currentTheme = AppConfig.systemThemeStyles[actualTheme as keyof SystemThemeTypes]
|
||||
if (currentTheme) {
|
||||
el.setAttribute('class', currentTheme.className)
|
||||
}
|
||||
|
||||
// 设置主题颜色
|
||||
setElementThemeColor(settingStore.systemThemeColor)
|
||||
|
||||
// 设置圆角
|
||||
document.documentElement.style.setProperty('--custom-radius', `${settingStore.customRadius}rem`)
|
||||
}
|
||||
|
||||
// 应用主题
|
||||
applyThemeByMode()
|
||||
|
||||
// 如果是 AUTO 模式,监听系统主题变化(使用 VueUse 的响应式特性)
|
||||
if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) {
|
||||
watch(
|
||||
prefersDark,
|
||||
() => {
|
||||
// 只有在 AUTO 模式下才响应系统主题变化
|
||||
if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) {
|
||||
applyThemeByMode()
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
)
|
||||
}
|
||||
}
|
||||
32
saiadmin-artd/src/hooks/index.ts
Normal file
32
saiadmin-artd/src/hooks/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// 通用功能集合
|
||||
export { useCommon } from './core/useCommon'
|
||||
|
||||
// 应用模式
|
||||
export { useAppMode } from './core/useAppMode'
|
||||
|
||||
// 权限控制
|
||||
export { useAuth } from './core/useAuth'
|
||||
|
||||
// 表格数据管理方案
|
||||
export { useTable } from './core/useTable'
|
||||
|
||||
// 表格列配置管理
|
||||
export { useTableColumns } from './core/useTableColumns'
|
||||
|
||||
// 主题相关
|
||||
export { useTheme } from './core/useTheme'
|
||||
|
||||
// 礼花+文字滚动
|
||||
export { useCeremony } from './core/useCeremony'
|
||||
|
||||
// 顶栏快速入口
|
||||
export { useFastEnter } from './core/useFastEnter'
|
||||
|
||||
// 顶栏功能管理
|
||||
export { useHeaderBar } from './core/useHeaderBar'
|
||||
|
||||
// 图表相关
|
||||
export { useChart, useChartComponent, useChartOps } from './core/useChart'
|
||||
|
||||
// 布局高度
|
||||
export { useLayoutHeight, useAutoLayoutHeight } from './core/useLayoutHeight'
|
||||
Reference in New Issue
Block a user