项目创建

This commit is contained in:
浅念
2025-07-04 17:16:38 +08:00
parent ce47b39fa5
commit 719911918f
372 changed files with 105035 additions and 32 deletions
+46
View File
@@ -0,0 +1,46 @@
import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/modules/user'
import { useCommon } from '@/composables/useCommon'
import type { AppRouteRecord } from '@/types/router'
type AuthItem = NonNullable<AppRouteRecord['meta']['authList']>[number]
const userStore = useUserStore()
/**
* 按钮权限(前后端模式通用)
* 用法:
* const { hasAuth } = useAuth()
* hasAuth('add') // 检查是否拥有新增权限
*/
export const useAuth = () => {
const route = useRoute()
const { isFrontendMode } = useCommon()
const { info } = storeToRefs(userStore)
// 前端按钮权限(例如:['add', 'edit'])
const frontendAuthList = info.value?.buttons ?? []
// 后端路由 meta 配置的权限列表(例如:[{ auth_mark: '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?.auth_mark === auth)
}
return {
hasAuth
}
}
+85
View File
@@ -0,0 +1,85 @@
import { useTimeoutFn, useIntervalFn } from '@vueuse/core'
import { useDateFormat } from '@vueuse/core'
import { useSettingStore } from '@/store/modules/setting'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { mittBus } from '@/utils/sys'
import { festivalConfigList } from '@/config/festival'
// 节日庆祝相关配置
export function useCeremony() {
const settingStore = useSettingStore()
const { holidayFireworksLoaded, isShowFireworks } = storeToRefs(settingStore)
// 烟花间隔引用,用于清理
let fireworksInterval: { pause: () => void } | null = null
// 判断当前日期是否是节日
const currentFestivalData = computed(() => {
const currentDate = useDateFormat(new Date(), 'YYYY-MM-DD').value
return festivalConfigList.find((item) => item.date === currentDate)
})
// 节日庆祝相关配置
const FESTIVAL_CONFIG = {
INITIAL_DELAY: 300, // 初始延迟时间,单位毫秒
FIREWORK_INTERVAL: 1000, // 烟花效果触发间隔,单位毫秒
TEXT_DELAY: 2000, // 文本显示延迟时间,单位毫秒
MAX_TRIGGERS: 6 // 最大触发次数
} as const
// 根据节日列表显示节日祝福
const openFestival = () => {
// 没有节日数据,不显示
if (!currentFestivalData.value) return
// 礼花效果结束,不显示
if (!isShowFireworks.value) return
let triggers = 0
const { start: startFireworks } = useTimeoutFn(() => {
const { pause } = useIntervalFn(() => {
// console.log(currentFestivalData.value?.image)
mittBus.emit('triggerFireworks', currentFestivalData.value?.image)
triggers++
if (triggers >= FESTIVAL_CONFIG.MAX_TRIGGERS) {
pause()
settingStore.setholidayFireworksLoaded(true)
// 主页显示节日文本
useTimeoutFn(() => {
settingStore.setShowFestivalText(true)
setFestivalDate()
}, FESTIVAL_CONFIG.TEXT_DELAY)
}
}, FESTIVAL_CONFIG.FIREWORK_INTERVAL)
fireworksInterval = { pause }
}, FESTIVAL_CONFIG.INITIAL_DELAY)
startFireworks()
}
// 清理函数
const cleanup = () => {
if (fireworksInterval) {
fireworksInterval.pause()
settingStore.setShowFestivalText(false)
setFestivalDate()
}
}
// 设置节日日期
const setFestivalDate = () => {
settingStore.setFestivalDate(currentFestivalData.value?.date || '')
}
return {
openFestival,
cleanup,
holidayFireworksLoaded,
currentFestivalData,
isShowFireworks
}
}
+324
View File
@@ -0,0 +1,324 @@
import * as echarts from 'echarts'
import type { EChartsOption } from 'echarts'
import { ref, watch, nextTick, onMounted, onUnmounted, onBeforeUnmount } from 'vue'
import { storeToRefs } from 'pinia'
import { useSettingStore } from '@/store/modules/setting'
import { getCssVar } from '@/utils/ui'
interface ChartThemeConfig {
chartHeight: string
fontSize: number
fontColor: string
themeColor: string
}
// 图表主题配置
export const useChartOps = (): ChartThemeConfig => ({
chartHeight: '16rem',
fontSize: 13,
fontColor: '#999',
themeColor: getCssVar('--el-color-primary-light-1')
})
interface UseChartOptions {
/** 初始化选项 */
initOptions?: EChartsOption
/** 延迟初始化时间(ms) */
initDelay?: number
/** IntersectionObserver阈值 */
threshold?: number
/** 是否自动响应主题变化 */
autoTheme?: boolean
}
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
// 使用 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
}, 100)
}
// 收缩菜单时,重新计算图表大小
watch(menuOpen, () => {
// 立即调用一次,快速响应
nextTick(() => {
requestAnimationResize()
})
// 使用更短的延迟时间,确保图表正确适应宽度变化
const delays = [50, 100, 200, 350]
delays.forEach((delay) => {
setTimeout(() => {
requestAnimationResize()
}, delay)
})
})
// 菜单类型变化触发
watch(menuType, () => {
// 立即调用一次,快速响应
nextTick(() => {
requestAnimationResize()
})
// 菜单类型变化也使用多延迟处理
setTimeout(() => {
const delays = [50, 100, 200]
delays.forEach((delay) => {
setTimeout(() => {
requestAnimationResize()
}, delay)
})
}, 0)
})
// 主题变化时重新设置图表选项
if (autoTheme) {
watch(isDark, () => {
if (chart && !isDestroyed) {
// 使用 requestAnimationFrame 优化主题更新
requestAnimationFrame(() => {
if (chart && !isDestroyed) {
const currentOptions = chart.getOption()
if (currentOptions) {
updateChart(currentOptions as EChartsOption)
}
}
})
}
})
}
// 坐标轴线样式
const getAxisLineStyle = (show: boolean = true) => ({
show,
lineStyle: {
color: isDark.value ? '#444' : '#EDEDED',
width: 1
}
})
// 分割线样式
const getSplitLineStyle = (show: boolean = true) => ({
show,
lineStyle: {
color: isDark.value ? '#444' : '#EDEDED',
width: 1,
type: 'dashed' as const
}
})
// 坐标轴标签样式
const getAxisLabelStyle = (show: boolean = true) => ({
show,
color: useChartOps().fontColor,
fontSize: useChartOps().fontSize
})
// 坐标轴刻度样式
const getAxisTickStyle = () => ({
show: false
})
// 创建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)
}
chart.setOption(pendingOptions)
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 initChart = (options: EChartsOption = {}) => {
if (!chartRef.value || isDestroyed) return
const mergedOptions = { ...initOptions, ...options }
try {
if (isContainerVisible(chartRef.value)) {
// 容器可见,正常初始化
const initFn = () => {
if (!chart && chartRef.value && !isDestroyed) {
chart = echarts.init(chartRef.value)
}
if (chart && !isDestroyed) {
chart.setOption(mergedOptions)
pendingOptions = null
}
}
if (initDelay > 0) {
setTimeout(initFn, initDelay)
} else {
initFn()
}
} 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
}
}
cleanupIntersectionObserver()
if (resizeTimeoutId) {
clearTimeout(resizeTimeoutId)
resizeTimeoutId = null
}
if (resizeFrameId) {
cancelAnimationFrame(resizeFrameId)
resizeFrameId = null
}
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,
getAxisLineStyle,
getSplitLineStyle,
getAxisLabelStyle,
getAxisTickStyle,
useChartOps
}
}
+100
View File
@@ -0,0 +1,100 @@
// 动态列配置
import { ref, computed } from 'vue'
import { $t } from '@/locales'
// 定义列配置接口
export interface ColumnOption {
type?: 'selection' | 'expand' | 'index'
prop?: string
label?: string
width?: number | string
minWidth?: number | string
fixed?: boolean | 'left' | 'right'
sortable?: boolean
disabled?: boolean
formatter?: (row: any) => any
[key: string]: any
}
// 定义列选择状态接口
export interface ColumnCheck {
prop: string
label: string
checked: boolean
}
// 特殊列类型常量
const SELECTION_KEY = '__selection__'
const EXPAND_KEY = '__expand__'
const INDEX_KEY = '__index__'
// 工具函数:根据列配置生成列选择状态
const getColumnChecks = (columns: ColumnOption[]): ColumnCheck[] => {
const checks: ColumnCheck[] = []
columns.forEach((column) => {
if (column.type === 'selection') {
checks.push({
prop: SELECTION_KEY,
label: $t('table.column.selection'),
checked: true
})
} else if (column.type === 'expand') {
checks.push({
prop: EXPAND_KEY,
label: $t('table.column.expand'),
checked: true
})
} else if (column.type === 'index') {
checks.push({
prop: INDEX_KEY,
label: $t('table.column.index'),
checked: true
})
} else {
checks.push({
prop: column.prop as string,
label: column.label as string,
checked: true
})
}
})
return checks
}
export function useCheckedColumns(columnsFactory: () => ColumnOption[]) {
// 获取所有列定义
const allColumns = columnsFactory()
// 列选中状态,初始包含所有普通列和特殊类型列
const columnChecks = ref<ColumnCheck[]>(getColumnChecks(allColumns))
// 当前显示的列
const columns = computed(() => {
const cols = allColumns
const columnMap = new Map<string, ColumnOption>()
cols.forEach((column) => {
if (column.type === 'selection') {
columnMap.set(SELECTION_KEY, column)
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column)
} else if (column.type === 'index') {
columnMap.set(INDEX_KEY, column)
} else {
columnMap.set(column.prop as string, column)
}
})
return columnChecks.value
.filter((item) => item.checked)
.map((check) => columnMap.get(check.prop) as ColumnOption)
})
return {
columns,
columnChecks
}
}
+36
View File
@@ -0,0 +1,36 @@
import { getTabConfig } from '@/utils/ui'
import { useSettingStore } from '@/store/modules/setting'
// 通用函数
export function useCommon() {
const settingStore = useSettingStore()
const { showWorkTab, tabStyle } = storeToRefs(settingStore)
// 是否是前端控制模式
const isFrontendMode = computed(() => {
return import.meta.env.VITE_ACCESS_MODE === 'frontend'
})
// 刷新页面
const refresh = () => {
settingStore.reload()
}
// 回到顶部
const scrollToTop = () => {
window.scrollTo({ top: 0 })
}
// 页面最小高度
const containerMinHeight = computed(() => {
const { openHeight, closeHeight } = getTabConfig(tabStyle.value)
return `calc(100vh - ${showWorkTab.value ? openHeight : closeHeight}px)`
})
return {
isFrontendMode,
refresh,
scrollToTop,
containerMinHeight
}
}
+9
View File
@@ -0,0 +1,9 @@
import AppConfig from '@/config'
export function useSystemInfo() {
const getSystemName = () => AppConfig.systemInfo.name
return {
getSystemName
}
}
+88
View File
@@ -0,0 +1,88 @@
import { useSettingStore } from '@/store/modules/setting'
import { SystemThemeEnum } from '@/enums/appEnum'
import AppConfig from '@/config'
import { SystemThemeTypes } from '@/types/store'
import { getDarkColor, getLightColor } from '@/utils/ui'
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()
})
})
}
// 自动设置系统主题
const setSystemAutoTheme = () => {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
setSystemTheme(SystemThemeEnum.DARK, SystemThemeEnum.AUTO)
} else {
setSystemTheme(SystemThemeEnum.LIGHT, SystemThemeEnum.AUTO)
}
}
// 切换主题
const switchThemeStyles = (theme: SystemThemeEnum) => {
if (theme === SystemThemeEnum.AUTO) {
setSystemAutoTheme()
} else {
setSystemTheme(theme)
}
}
return {
setSystemTheme,
setSystemAutoTheme,
switchThemeStyles
}
}