初始化

This commit is contained in:
2026-03-03 09:53:54 +08:00
commit 3f349a35a4
437 changed files with 65639 additions and 0 deletions

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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 // 获取所有禁用的功能(别名)
}
}

View 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
}
}

View 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'

View 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]
}
}

View 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
}
}

View 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 }
)
}
}

View 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'