Private
Public Access
1
0

新增:连接管理、数据查询等功能

This commit is contained in:
2026-01-22 18:34:59 +08:00
parent 95d3a20292
commit 652f5e5d60
87 changed files with 15082 additions and 162 deletions

View File

@@ -0,0 +1,8 @@
/**
* 全局 Composables 导出
*/
export * from './useLocalStorage'
export * from './useDebounce'
export * from './useTablePage'
export * from './useApiError'

View File

@@ -0,0 +1,61 @@
/**
* API Error handling composable
* 统一的 API 错误处理
*/
import { ref } from 'vue'
import { Message } from '@arco-design/web-vue'
export interface ApiErrorState {
hasError: boolean
message: string
code?: string | number
}
export function useApiError() {
const error = ref<ApiErrorState>({
hasError: false,
message: ''
})
const setError = (err: Error | string | unknown, defaultMessage: string = '操作失败') => {
let message = defaultMessage
let code: string | number | undefined
if (err instanceof Error) {
message = err.message || defaultMessage
} else if (typeof err === 'string') {
message = err
} else if (err && typeof err === 'object' && 'message' in err) {
message = (err as any).message || defaultMessage
if ('code' in err) code = (err as any).code
}
error.value = {
hasError: true,
message,
code
}
return { message, code }
}
const showError = (err: Error | string | unknown, defaultMessage: string = '操作失败') => {
const { message } = setError(err, defaultMessage)
Message.error(message)
}
const clearError = () => {
error.value = {
hasError: false,
message: ''
}
}
return {
error,
setError,
showError,
clearError
}
}

View File

@@ -0,0 +1,34 @@
/**
* Debounce composable
* 防抖函数
*/
import { ref, watch, type Ref, type ComputedRef } from 'vue'
export function useDebounce<T>(value: Ref<T> | ComputedRef<T>, delay: number = 300): Ref<T> {
const debouncedValue = ref<T>(value.value) as Ref<T>
let timeout: ReturnType<typeof setTimeout> | null = null
watch(value, (newValue) => {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})
return debouncedValue
}
export function debounceFn<T extends (...args: any[]) => any>(
fn: T,
delay: number = 300
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | null = null
return (...args: Parameters<T>) => {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
fn(...args)
}, delay)
}
}

View File

@@ -0,0 +1,34 @@
/**
* LocalStorage composable
* 通用的 localStorage 操作
*/
import { watch, type Ref } from 'vue'
export function useLocalStorage<T>(
key: string,
defaultValue: T,
storage: Storage = localStorage
): [Ref<T>, (value: T) => void, () => void] {
const stored = storage.getItem(key)
const value = ref<T>(stored ? JSON.parse(stored) : defaultValue)
const setValue = (newValue: T) => {
value.value = newValue
}
const clearValue = () => {
value.value = defaultValue
storage.removeItem(key)
}
watch(value, (newValue) => {
try {
storage.setItem(key, JSON.stringify(newValue))
} catch (e) {
console.warn(`Failed to save ${key} to localStorage:`, e)
}
}, { deep: true })
return [value, setValue, clearValue]
}

View File

@@ -0,0 +1,60 @@
/**
* Table Pagination composable
* 表格分页逻辑
*/
import { ref, computed } from 'vue'
export interface PaginationOptions {
pageSize?: number
initialPage?: number
}
export function useTablePage(options: PaginationOptions = {}) {
const { pageSize = 10, initialPage = 1 } = options
const currentPage = ref(initialPage)
const currentPageSize = ref(pageSize)
const canGoPrev = computed(() => currentPage.value > 1)
const canGoNext = computed(() => currentPage.value * currentPageSize.value < totalItems.value)
const totalItems = ref(0)
const totalPages = computed(() => Math.ceil(totalItems.value / currentPageSize.value))
const nextPage = () => {
if (canGoNext.value) currentPage.value++
}
const prevPage = () => {
if (canGoPrev.value) currentPage.value--
}
const goToPage = (page: number) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
const reset = () => {
currentPage.value = initialPage
}
const setTotalItems = (total: number) => {
totalItems.value = total
}
return {
currentPage,
currentPageSize,
canGoPrev,
canGoNext,
totalItems,
totalPages,
nextPage,
prevPage,
goToPage,
reset,
setTotalItems
}
}

View File

@@ -0,0 +1,78 @@
import { ref, computed } from 'vue'
type Theme = 'light' | 'dark'
const THEME_STORAGE_KEY = 'app-theme'
// 单例模式:全局共享主题状态
const theme = ref<Theme>('light')
let systemThemeListener: (() => void) | null = null
// 应用主题到 DOM
const applyTheme = (newTheme: Theme) => {
theme.value = newTheme
if (newTheme === 'dark') {
document.body.setAttribute('arco-theme', 'dark')
} else {
document.body.removeAttribute('arco-theme')
}
localStorage.setItem(THEME_STORAGE_KEY, newTheme)
}
// 初始化主题(只调用一次)
const initTheme = () => {
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY) as Theme
if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark')) {
applyTheme(savedTheme)
} else {
// 检测系统偏好
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
applyTheme('dark')
} else {
applyTheme('light')
}
}
// 监听系统主题变化
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = (e: MediaQueryListEvent) => {
// 如果用户没有手动设置过主题,则跟随系统
if (!localStorage.getItem(THEME_STORAGE_KEY)) {
applyTheme(e.matches ? 'dark' : 'light')
}
}
mediaQuery.addEventListener('change', handleChange)
systemThemeListener = () => mediaQuery.removeEventListener('change', handleChange)
}
}
export function useTheme() {
// 切换主题
const toggleTheme = () => {
const newTheme: Theme = theme.value === 'light' ? 'dark' : 'light'
applyTheme(newTheme)
}
// 设置为亮色主题
const setLightTheme = () => {
applyTheme('light')
}
// 设置为暗色主题
const setDarkTheme = () => {
applyTheme('dark')
}
return {
theme: computed(() => theme.value),
isDark: computed(() => theme.value === 'dark'),
toggleTheme,
setLightTheme,
setDarkTheme,
initTheme
}
}
// 导出初始化函数(在 main.js 中使用)
export { initTheme }