Initial commit: One Pipe System
完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
181
src/composables/useAgentManagement.ts
Normal file
181
src/composables/useAgentManagement.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* 代理商管理 Composable
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { AccountService } from '@/api/modules'
|
||||
import type { Agent, AgentQueryParams, CreateAgentParams } from '@/types/api'
|
||||
import { usePagination } from './usePagination'
|
||||
|
||||
export function useAgentManagement() {
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<Agent[]>([])
|
||||
|
||||
// 代理商树数据
|
||||
const treeData = ref<Agent[]>([])
|
||||
|
||||
// 分页
|
||||
const { pagination, setTotal, handleCurrentChange, handleSizeChange } = usePagination()
|
||||
|
||||
/**
|
||||
* 获取代理商列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
const fetchAgentList = async (params?: AgentQueryParams) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await AccountService.getAgents({
|
||||
current: pagination.current,
|
||||
size: pagination.size,
|
||||
...params
|
||||
})
|
||||
|
||||
tableData.value = res.data.items
|
||||
setTotal(res.data.total)
|
||||
} catch (error) {
|
||||
console.error('获取代理商列表失败:', error)
|
||||
ElMessage.error('获取代理商列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理商树
|
||||
*/
|
||||
const fetchAgentTree = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await AccountService.getAgentTree()
|
||||
treeData.value = res.data
|
||||
} catch (error) {
|
||||
console.error('获取代理商树失败:', error)
|
||||
ElMessage.error('获取代理商树失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理商详情
|
||||
* @param id 代理商ID
|
||||
*/
|
||||
const fetchAgentDetail = async (id: string | number) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await AccountService.getAgent(id)
|
||||
return res.data
|
||||
} catch (error) {
|
||||
console.error('获取代理商详情失败:', error)
|
||||
ElMessage.error('获取代理商详情失败')
|
||||
return null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建代理商
|
||||
* @param data 代理商数据
|
||||
*/
|
||||
const createAgent = async (data: CreateAgentParams) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await AccountService.createAgent(data)
|
||||
ElMessage.success('创建成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('创建失败:', error)
|
||||
ElMessage.error('创建失败')
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新代理商
|
||||
* @param id 代理商ID
|
||||
* @param data 代理商数据
|
||||
*/
|
||||
const updateAgent = async (id: string | number, data: Partial<CreateAgentParams>) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await AccountService.updateAgent(id, data)
|
||||
ElMessage.success('更新成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('更新失败:', error)
|
||||
ElMessage.error('更新失败')
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换代理商状态
|
||||
* @param id 代理商ID
|
||||
* @param status 状态
|
||||
*/
|
||||
const toggleStatus = async (id: string | number, status: 0 | 1) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await AccountService.toggleAgentStatus(id, status)
|
||||
ElMessage.success('状态更新成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('状态更新失败:', error)
|
||||
ElMessage.error('状态更新失败')
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下级代理列表
|
||||
* @param id 代理商ID
|
||||
*/
|
||||
const fetchSubAgents = async (id: string | number) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await AccountService.getSubAgents(id)
|
||||
return res.data
|
||||
} catch (error) {
|
||||
console.error('获取下级代理失败:', error)
|
||||
ElMessage.error('获取下级代理失败')
|
||||
return []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新列表
|
||||
*/
|
||||
const refresh = () => {
|
||||
fetchAgentList()
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
tableData,
|
||||
treeData,
|
||||
pagination,
|
||||
handleCurrentChange,
|
||||
handleSizeChange,
|
||||
fetchAgentList,
|
||||
fetchAgentTree,
|
||||
fetchAgentDetail,
|
||||
createAgent,
|
||||
updateAgent,
|
||||
toggleStatus,
|
||||
fetchSubAgents,
|
||||
refresh
|
||||
}
|
||||
}
|
||||
46
src/composables/useAuth.ts
Normal file
46
src/composables/useAuth.ts
Normal 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
|
||||
}
|
||||
}
|
||||
172
src/composables/useCardManagement.ts
Normal file
172
src/composables/useCardManagement.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 网卡管理 Composable
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CardService } from '@/api/modules'
|
||||
import type { Card, CardQueryParams, CardOperationParams } from '@/types/api'
|
||||
import { usePagination } from './usePagination'
|
||||
import { useTableSelection } from './useTableSelection'
|
||||
|
||||
export function useCardManagement() {
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<Card[]>([])
|
||||
|
||||
// 分页
|
||||
const { pagination, setTotal, handleCurrentChange, handleSizeChange } = usePagination()
|
||||
|
||||
// 表格选择
|
||||
const { selectedRows, selectedIds, hasSelected, handleSelectionChange, clearSelection } =
|
||||
useTableSelection<Card>()
|
||||
|
||||
/**
|
||||
* 获取网卡列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
const fetchCardList = async (params?: CardQueryParams) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await CardService.getCards({
|
||||
current: pagination.current,
|
||||
size: pagination.size,
|
||||
...params
|
||||
})
|
||||
|
||||
tableData.value = res.data.items
|
||||
setTotal(res.data.total)
|
||||
} catch (error) {
|
||||
console.error('获取网卡列表失败:', error)
|
||||
ElMessage.error('获取网卡列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ICCID获取单卡信息
|
||||
* @param iccid ICCID
|
||||
*/
|
||||
const fetchCardByIccid = async (iccid: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await CardService.getCardByIccid(iccid)
|
||||
return res.data
|
||||
} catch (error) {
|
||||
console.error('获取单卡信息失败:', error)
|
||||
ElMessage.error('获取单卡信息失败')
|
||||
return null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网卡操作
|
||||
* @param params 操作参数
|
||||
*/
|
||||
const cardOperation = async (params: CardOperationParams) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await CardService.cardOperation(params)
|
||||
ElMessage.success('操作成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停机
|
||||
* @param iccid ICCID
|
||||
* @param remark 备注
|
||||
*/
|
||||
const suspendCard = async (iccid: string, remark?: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await CardService.suspend(iccid, remark)
|
||||
ElMessage.success('停机成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('停机失败:', error)
|
||||
ElMessage.error('停机失败')
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复机
|
||||
* @param iccid ICCID
|
||||
* @param remark 备注
|
||||
*/
|
||||
const resumeCard = async (iccid: string, remark?: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await CardService.resume(iccid, remark)
|
||||
ElMessage.success('复机成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('复机失败:', error)
|
||||
ElMessage.error('复机失败')
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入网卡
|
||||
* @param file Excel文件
|
||||
* @param params 额外参数
|
||||
*/
|
||||
const importCards = async (file: File, params?: Record<string, any>) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await CardService.importCards(file, params)
|
||||
ElMessage.success('导入成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error)
|
||||
ElMessage.error('导入失败')
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新列表
|
||||
*/
|
||||
const refresh = () => {
|
||||
fetchCardList()
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
tableData,
|
||||
pagination,
|
||||
selectedRows,
|
||||
selectedIds,
|
||||
hasSelected,
|
||||
handleSelectionChange,
|
||||
handleCurrentChange,
|
||||
handleSizeChange,
|
||||
clearSelection,
|
||||
fetchCardList,
|
||||
fetchCardByIccid,
|
||||
cardOperation,
|
||||
suspendCard,
|
||||
resumeCard,
|
||||
importCards,
|
||||
refresh
|
||||
}
|
||||
}
|
||||
85
src/composables/useCeremony.ts
Normal file
85
src/composables/useCeremony.ts
Normal 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
src/composables/useChart.ts
Normal file
324
src/composables/useChart.ts
Normal 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
|
||||
}
|
||||
}
|
||||
99
src/composables/useCheckedColumns.ts
Normal file
99
src/composables/useCheckedColumns.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
// 动态列配置
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 定义列配置接口
|
||||
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: '勾选',
|
||||
checked: true
|
||||
})
|
||||
} else if (column.type === 'expand') {
|
||||
checks.push({
|
||||
prop: EXPAND_KEY,
|
||||
label: '展开',
|
||||
checked: true
|
||||
})
|
||||
} else if (column.type === 'index') {
|
||||
checks.push({
|
||||
prop: INDEX_KEY,
|
||||
label: '序号',
|
||||
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
src/composables/useCommon.ts
Normal file
36
src/composables/useCommon.ts
Normal 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
|
||||
}
|
||||
}
|
||||
131
src/composables/useFormDialog.ts
Normal file
131
src/composables/useFormDialog.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 表单对话框管理 Composable
|
||||
*/
|
||||
|
||||
import { ref, reactive, nextTick } from 'vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
export type DialogMode = 'add' | 'edit' | 'view'
|
||||
|
||||
export interface FormDialogOptions<T> {
|
||||
initialData?: () => T
|
||||
}
|
||||
|
||||
export function useFormDialog<T extends Record<string, any>>(options: FormDialogOptions<T> = {}) {
|
||||
// 对话框可见性
|
||||
const visible = ref(false)
|
||||
|
||||
// 对话框模式
|
||||
const mode = ref<DialogMode>('add')
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<T>((options.initialData?.() || {}) as T)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 对话框标题
|
||||
const dialogTitle = ref('')
|
||||
|
||||
/**
|
||||
* 打开对话框
|
||||
* @param dialogMode 对话框模式
|
||||
* @param data 初始数据(编辑/查看模式)
|
||||
* @param title 自定义标题
|
||||
*/
|
||||
const open = (dialogMode: DialogMode = 'add', data?: Partial<T>, title?: string) => {
|
||||
mode.value = dialogMode
|
||||
visible.value = true
|
||||
|
||||
// 设置标题
|
||||
if (title) {
|
||||
dialogTitle.value = title
|
||||
} else {
|
||||
const titles: Record<DialogMode, string> = {
|
||||
add: '新增',
|
||||
edit: '编辑',
|
||||
view: '查看'
|
||||
}
|
||||
dialogTitle.value = titles[dialogMode]
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
nextTick(() => {
|
||||
formRef.value?.resetFields()
|
||||
|
||||
// 如果有初始数据,填充表单
|
||||
if (data) {
|
||||
Object.assign(formData, data)
|
||||
} else if (options.initialData) {
|
||||
Object.assign(formData, options.initialData())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭对话框
|
||||
*/
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
nextTick(() => {
|
||||
formRef.value?.resetFields()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证表单
|
||||
*/
|
||||
const validate = async (): Promise<boolean> => {
|
||||
if (!formRef.value) return false
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
*/
|
||||
const resetFields = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空验证
|
||||
*/
|
||||
const clearValidate = () => {
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 是否为新增模式
|
||||
const isAddMode = () => mode.value === 'add'
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEditMode = () => mode.value === 'edit'
|
||||
|
||||
// 是否为查看模式
|
||||
const isViewMode = () => mode.value === 'view'
|
||||
|
||||
return {
|
||||
visible,
|
||||
mode,
|
||||
formRef,
|
||||
formData,
|
||||
loading,
|
||||
dialogTitle,
|
||||
open,
|
||||
close,
|
||||
validate,
|
||||
resetFields,
|
||||
clearValidate,
|
||||
isAddMode,
|
||||
isEditMode,
|
||||
isViewMode
|
||||
}
|
||||
}
|
||||
273
src/composables/useLogin.ts
Normal file
273
src/composables/useLogin.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 登录逻辑 Composable
|
||||
*/
|
||||
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, ElNotification } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { HOME_PAGE } from '@/router/routesAlias'
|
||||
import AppConfig from '@/config'
|
||||
import { AuthService } from '@/api/authApi'
|
||||
import { ApiStatus } from '@/utils/http/status'
|
||||
import {
|
||||
MOCK_ACCOUNTS,
|
||||
mockLogin,
|
||||
mockGetUserInfo,
|
||||
type MockAccount
|
||||
} from '@/mock/auth'
|
||||
import {
|
||||
saveCredentials,
|
||||
getRememberedCredentials
|
||||
} from '@/utils/auth/rememberPassword'
|
||||
import { usernameRules, passwordRules } from '@/utils/auth/loginValidation'
|
||||
import { getRedirectPath } from '@/router/guards/permission'
|
||||
|
||||
// 开发模式:是否使用 Mock 数据
|
||||
const USE_MOCK = false
|
||||
|
||||
/**
|
||||
* 登录表单数据
|
||||
*/
|
||||
interface LoginForm {
|
||||
account: string
|
||||
username: string
|
||||
password: string
|
||||
rememberPassword: boolean
|
||||
}
|
||||
|
||||
export function useLogin() {
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 拖拽验证
|
||||
const isPassing = ref(false)
|
||||
const isClickPass = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<LoginForm>({
|
||||
account: '',
|
||||
username: '',
|
||||
password: '',
|
||||
rememberPassword: true
|
||||
})
|
||||
|
||||
// 验证规则
|
||||
const rules = computed<FormRules>(() => ({
|
||||
username: usernameRules(t),
|
||||
password: passwordRules(t)
|
||||
}))
|
||||
|
||||
// Mock 账号列表
|
||||
const mockAccounts = computed<MockAccount[]>(() => MOCK_ACCOUNTS)
|
||||
|
||||
/**
|
||||
* 初始化表单
|
||||
*/
|
||||
const initForm = () => {
|
||||
// 尝试加载记住的密码
|
||||
const remembered = getRememberedCredentials()
|
||||
if (remembered) {
|
||||
formData.username = remembered.username
|
||||
formData.password = remembered.password
|
||||
formData.rememberPassword = remembered.rememberPassword
|
||||
|
||||
// 如果使用 Mock,尝试匹配 Mock 账号
|
||||
if (USE_MOCK) {
|
||||
const matchedAccount = MOCK_ACCOUNTS.find(
|
||||
(acc) => acc.username === remembered.username
|
||||
)
|
||||
if (matchedAccount) {
|
||||
formData.account = matchedAccount.key
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 开发环境:使用真实 API 默认账号
|
||||
if (import.meta.env.MODE === 'development') {
|
||||
formData.username = 'admin'
|
||||
formData.password = 'Admin@123456'
|
||||
formData.rememberPassword = false
|
||||
}
|
||||
// 生产环境:不填充默认值
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Mock 账号
|
||||
*/
|
||||
const setupAccount = (key: string) => {
|
||||
const selectedAccount = MOCK_ACCOUNTS.find((account) => account.key === key)
|
||||
if (selectedAccount) {
|
||||
formData.account = key
|
||||
formData.username = selectedAccount.username
|
||||
formData.password = selectedAccount.password
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行登录
|
||||
*/
|
||||
const handleLogin = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
// 验证表单
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
// 检查拖拽验证
|
||||
if (!isPassing.value) {
|
||||
isClickPass.value = true
|
||||
ElMessage.warning(t('login.placeholder[2]'))
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
// 判断使用 Mock 还是真实 API
|
||||
if (USE_MOCK) {
|
||||
await handleMockLogin()
|
||||
} else {
|
||||
await handleRealLogin()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error)
|
||||
ElMessage.error(error.message || t('login.error.loginFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 登录(开发测试用)
|
||||
*/
|
||||
const handleMockLogin = async () => {
|
||||
const loginResult = mockLogin(formData.username, formData.password)
|
||||
|
||||
if (!loginResult) {
|
||||
ElMessage.error(t('login.error.invalidCredentials'))
|
||||
return
|
||||
}
|
||||
|
||||
// 保存 Token
|
||||
userStore.setToken(loginResult.token, loginResult.refreshToken)
|
||||
|
||||
// 获取用户信息
|
||||
const userInfo = mockGetUserInfo(loginResult.token)
|
||||
|
||||
if (!userInfo) {
|
||||
ElMessage.error(t('login.error.getUserInfoFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
userStore.setUserInfo(userInfo)
|
||||
userStore.setLoginStatus(true)
|
||||
|
||||
// 保存记住密码
|
||||
saveCredentials(formData.username, formData.password, formData.rememberPassword)
|
||||
|
||||
// 显示登录成功提示
|
||||
showLoginSuccessNotice()
|
||||
|
||||
// 跳转到重定向页面或首页
|
||||
const redirectPath = getRedirectPath(route)
|
||||
router.push(redirectPath || HOME_PAGE)
|
||||
}
|
||||
|
||||
/**
|
||||
* 真实 API 登录
|
||||
*/
|
||||
const handleRealLogin = async () => {
|
||||
// 调用登录接口
|
||||
const response = await AuthService.login({
|
||||
username: formData.username,
|
||||
password: formData.password
|
||||
})
|
||||
|
||||
// 检查响应状态
|
||||
if (response.code !== ApiStatus.success) {
|
||||
ElMessage.error(response.msg || t('login.error.invalidCredentials'))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查返回数据
|
||||
if (!response.data || !response.data.access_token) {
|
||||
ElMessage.error(t('login.error.loginFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
// 保存 Token
|
||||
userStore.setToken(response.data.access_token, response.data.refresh_token)
|
||||
|
||||
// 保存用户信息
|
||||
if (response.data.user) {
|
||||
userStore.setUserInfo(response.data.user)
|
||||
userStore.setLoginStatus(true)
|
||||
} else {
|
||||
ElMessage.error(t('login.error.getUserInfoFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
// 保存记住密码
|
||||
saveCredentials(formData.username, formData.password, formData.rememberPassword)
|
||||
|
||||
// 显示登录成功提示
|
||||
showLoginSuccessNotice()
|
||||
|
||||
// 跳转到重定向页面或首页
|
||||
const redirectPath = getRedirectPath(route)
|
||||
router.push(redirectPath || HOME_PAGE)
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示登录成功通知
|
||||
*/
|
||||
const showLoginSuccessNotice = () => {
|
||||
setTimeout(() => {
|
||||
ElNotification({
|
||||
title: t('login.success.title'),
|
||||
type: 'success',
|
||||
duration: 2500,
|
||||
zIndex: 10000,
|
||||
message: `${t('login.success.message')}, ${AppConfig.systemInfo.name}!`
|
||||
})
|
||||
}, 150)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置拖拽验证
|
||||
*/
|
||||
const resetDragVerify = () => {
|
||||
isPassing.value = false
|
||||
isClickPass.value = false
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
initForm()
|
||||
})
|
||||
|
||||
return {
|
||||
formRef,
|
||||
formData,
|
||||
rules,
|
||||
loading,
|
||||
isPassing,
|
||||
isClickPass,
|
||||
mockAccounts,
|
||||
setupAccount,
|
||||
handleLogin,
|
||||
resetDragVerify
|
||||
}
|
||||
}
|
||||
117
src/composables/usePagination.ts
Normal file
117
src/composables/usePagination.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 分页管理 Composable
|
||||
*/
|
||||
|
||||
import { reactive, computed } from 'vue'
|
||||
|
||||
export interface PaginationOptions {
|
||||
initialPage?: number
|
||||
initialPageSize?: number
|
||||
pageSizes?: number[]
|
||||
layout?: string
|
||||
}
|
||||
|
||||
export function usePagination(options: PaginationOptions = {}) {
|
||||
const {
|
||||
initialPage = 1,
|
||||
initialPageSize = 20,
|
||||
pageSizes = [10, 20, 50, 100],
|
||||
layout = 'total, sizes, prev, pager, next, jumper'
|
||||
} = options
|
||||
|
||||
// 分页状态
|
||||
const pagination = reactive({
|
||||
current: initialPage,
|
||||
size: initialPageSize,
|
||||
total: 0,
|
||||
pageSizes,
|
||||
layout
|
||||
})
|
||||
|
||||
// 当前页码(别名)
|
||||
const currentPage = computed({
|
||||
get: () => pagination.current,
|
||||
set: (val) => (pagination.current = val)
|
||||
})
|
||||
|
||||
// 每页数量(别名)
|
||||
const pageSize = computed({
|
||||
get: () => pagination.size,
|
||||
set: (val) => (pagination.size = val)
|
||||
})
|
||||
|
||||
// 总页数
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(pagination.total / pagination.size)
|
||||
})
|
||||
|
||||
// 是否有上一页
|
||||
const hasPrev = computed(() => pagination.current > 1)
|
||||
|
||||
// 是否有下一页
|
||||
const hasNext = computed(() => pagination.current < totalPages.value)
|
||||
|
||||
// 重置分页
|
||||
const reset = () => {
|
||||
pagination.current = initialPage
|
||||
pagination.size = initialPageSize
|
||||
pagination.total = 0
|
||||
}
|
||||
|
||||
// 设置总数
|
||||
const setTotal = (total: number) => {
|
||||
pagination.total = total
|
||||
}
|
||||
|
||||
// 处理页码变化
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.current = page
|
||||
}
|
||||
|
||||
// 处理每页数量变化
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.size = size
|
||||
pagination.current = 1 // 改变每页数量时重置到第一页
|
||||
}
|
||||
|
||||
// 跳转到第一页
|
||||
const toFirstPage = () => {
|
||||
pagination.current = 1
|
||||
}
|
||||
|
||||
// 跳转到最后一页
|
||||
const toLastPage = () => {
|
||||
pagination.current = totalPages.value
|
||||
}
|
||||
|
||||
// 上一页
|
||||
const prevPage = () => {
|
||||
if (hasPrev.value) {
|
||||
pagination.current--
|
||||
}
|
||||
}
|
||||
|
||||
// 下一页
|
||||
const nextPage = () => {
|
||||
if (hasNext.value) {
|
||||
pagination.current++
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pagination,
|
||||
currentPage,
|
||||
pageSize,
|
||||
totalPages,
|
||||
hasPrev,
|
||||
hasNext,
|
||||
reset,
|
||||
setTotal,
|
||||
handleCurrentChange,
|
||||
handleSizeChange,
|
||||
toFirstPage,
|
||||
toLastPage,
|
||||
prevPage,
|
||||
nextPage
|
||||
}
|
||||
}
|
||||
9
src/composables/useSystemInfo.ts
Normal file
9
src/composables/useSystemInfo.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import AppConfig from '@/config'
|
||||
|
||||
export function useSystemInfo() {
|
||||
const getSystemName = () => AppConfig.systemInfo.name
|
||||
|
||||
return {
|
||||
getSystemName
|
||||
}
|
||||
}
|
||||
57
src/composables/useTableSelection.ts
Normal file
57
src/composables/useTableSelection.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 表格选择管理 Composable
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export function useTableSelection<T = any>() {
|
||||
// 选中的行
|
||||
const selectedRows = ref<T[]>([])
|
||||
|
||||
// 选中的ID列表
|
||||
const selectedIds = computed(() => {
|
||||
return selectedRows.value.map((row: any) => row.id)
|
||||
})
|
||||
|
||||
// 选中的数量
|
||||
const selectedCount = computed(() => selectedRows.value.length)
|
||||
|
||||
// 是否有选中项
|
||||
const hasSelected = computed(() => selectedRows.value.length > 0)
|
||||
|
||||
// 处理选择变化
|
||||
const handleSelectionChange = (selection: T[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 清空选择
|
||||
const clearSelection = () => {
|
||||
selectedRows.value = []
|
||||
}
|
||||
|
||||
// 判断是否选中指定项
|
||||
const isSelected = (row: any) => {
|
||||
return selectedIds.value.includes(row.id)
|
||||
}
|
||||
|
||||
// 切换选中状态
|
||||
const toggleSelection = (row: T) => {
|
||||
const index = selectedRows.value.findIndex((item: any) => item.id === (row as any).id)
|
||||
if (index > -1) {
|
||||
selectedRows.value.splice(index, 1)
|
||||
} else {
|
||||
selectedRows.value.push(row)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectedRows,
|
||||
selectedIds,
|
||||
selectedCount,
|
||||
hasSelected,
|
||||
handleSelectionChange,
|
||||
clearSelection,
|
||||
isSelected,
|
||||
toggleSelection
|
||||
}
|
||||
}
|
||||
88
src/composables/useTheme.ts
Normal file
88
src/composables/useTheme.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user