@mtn-ui-z/utils 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ /**
2
+ * 权限管理模块
3
+ * 提供统一的权限检查功能与 usePermission composable
4
+ */
5
+ import {
6
+ inject,
7
+ computed,
8
+ toValue,
9
+ unref,
10
+ type InjectionKey,
11
+ type Ref,
12
+ type MaybeRefOrGetter,
13
+ type ComputedRef
14
+ } from 'vue'
15
+
16
+ /**
17
+ * 权限检查函数类型
18
+ * @param permission 权限码(支持单个或多个,多个时满足任一即可)
19
+ * @returns 是否有权限
20
+ */
21
+ export type PermissionChecker = (permission: string | string[]) => boolean
22
+
23
+ /**
24
+ * 权限行为类型
25
+ * - hide: 无权限时隐藏元素
26
+ * - disable: 无权限时禁用元素
27
+ */
28
+ export type PermissionAction = 'hide' | 'disable'
29
+
30
+ /** 将权限码规范为数组,供内部与 hasPermission / createPermissionChecker 复用 */
31
+ function normalizePermissionList(permission: string | string[]): string[] {
32
+ return Array.isArray(permission) ? permission : [permission]
33
+ }
34
+
35
+ /**
36
+ * 默认权限检查函数
37
+ * 在未配置自定义检查函数时使用,默认返回 true(所有人都有权限)
38
+ */
39
+ export const defaultPermissionChecker: PermissionChecker = (_permission) => {
40
+ if (import.meta.env.DEV) {
41
+ console.warn(
42
+ '[mtn-ui] 未配置权限检查函数,默认返回 true。' +
43
+ '请在应用根组件中通过 provide 提供 PERMISSION_CHECKER_KEY。'
44
+ )
45
+ }
46
+ return true
47
+ }
48
+
49
+ /**
50
+ * 权限检查函数的 Provide/Inject Key
51
+ * 在应用根组件中通过 provide 提供自定义的权限检查函数
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * import { provide, ref, computed } from 'vue'
56
+ * import { PERMISSION_CHECKER_KEY } from '@mtn-ui-z/utils'
57
+ *
58
+ * const userPermissions = ref(['user:add', 'user:edit'])
59
+ * provide(PERMISSION_CHECKER_KEY, computed(() => {
60
+ * return (permission: string | string[]) => {
61
+ * const permArray = Array.isArray(permission) ? permission : [permission]
62
+ * return permArray.some((perm) => userPermissions.value.includes(perm))
63
+ * }
64
+ * }))
65
+ * ```
66
+ */
67
+ export const PERMISSION_CHECKER_KEY: InjectionKey<Ref<PermissionChecker>> = Symbol('permissionChecker')
68
+
69
+ /**
70
+ * 用户权限列表的 Provide/Inject Key(可选)
71
+ * 如果使用 Pinia/Vuex 管理权限,可以不使用此 Key
72
+ */
73
+ export const USER_PERMISSIONS_KEY: InjectionKey<Ref<string[]>> = Symbol('userPermissions')
74
+
75
+ /**
76
+ * 创建默认的权限检查函数
77
+ * 基于权限列表进行检查
78
+ *
79
+ * @param permissions 用户权限列表
80
+ * @returns 权限检查函数
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * const userPermissions = ref(['user:add', 'user:edit'])
85
+ * const checker = createPermissionChecker(userPermissions)
86
+ *
87
+ * provide(PERMISSION_CHECKER_KEY, computed(() => checker))
88
+ * ```
89
+ */
90
+ export function createPermissionChecker(permissions: Ref<string[]>): PermissionChecker {
91
+ return (permission) =>
92
+ normalizePermissionList(permission).some((perm) => permissions.value.includes(perm))
93
+ }
94
+
95
+ /**
96
+ * 工具函数:检查是否有指定权限
97
+ * 可用于非组件场景的权限判断
98
+ *
99
+ * @param userPermissions 用户权限列表
100
+ * @param requiredPermission 需要的权限
101
+ * @returns 是否有权限
102
+ */
103
+ export function hasPermission(
104
+ userPermissions: string[],
105
+ requiredPermission: string | string[]
106
+ ): boolean {
107
+ return normalizePermissionList(requiredPermission).some((perm) =>
108
+ userPermissions.includes(perm)
109
+ )
110
+ }
111
+
112
+ // ==================== usePermission Composable ====================
113
+
114
+ export interface UsePermissionOptions {
115
+ /** 权限码(支持响应式) */
116
+ permission?: MaybeRefOrGetter<string | string[] | undefined>
117
+ /** 无权限时的行为(支持响应式) */
118
+ permissionAction?: MaybeRefOrGetter<PermissionAction | undefined>
119
+ }
120
+
121
+ export interface UsePermissionReturn {
122
+ /** 是否有权限 */
123
+ hasPermission: ComputedRef<boolean>
124
+ /** 是否应该隐藏 */
125
+ shouldHide: ComputedRef<boolean>
126
+ /** 是否应该禁用 */
127
+ shouldDisable: ComputedRef<boolean>
128
+ }
129
+
130
+ /**
131
+ * 使用权限控制
132
+ * @param options 权限配置选项(支持响应式)
133
+ * @returns 权限状态
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const { hasPermission, shouldHide, shouldDisable } = usePermission({
138
+ * permission: props.permission,
139
+ * permissionAction: props.permissionAction,
140
+ * })
141
+ * ```
142
+ */
143
+ export function usePermission(options: UsePermissionOptions): UsePermissionReturn {
144
+ const permissionCheckerRef = inject<Ref<PermissionChecker> | null>(PERMISSION_CHECKER_KEY, null)
145
+
146
+ const hasPermission = computed(() => {
147
+ const p = toValue(options.permission)
148
+ if (!p) return true
149
+ const checker: PermissionChecker = permissionCheckerRef
150
+ ? unref(permissionCheckerRef)
151
+ : defaultPermissionChecker
152
+ return checker(p)
153
+ })
154
+
155
+ const action = computed(() => toValue(options.permissionAction) ?? 'hide')
156
+
157
+ const shouldHide = computed(() => !hasPermission.value && action.value === 'hide')
158
+ const shouldDisable = computed(() => !hasPermission.value && action.value === 'disable')
159
+
160
+ return {
161
+ hasPermission,
162
+ shouldHide,
163
+ shouldDisable
164
+ }
165
+ }
@@ -0,0 +1,57 @@
1
+ /*
2
+ * @Author: liaokt
3
+ * @Description: API 相关类型定义
4
+ * @Date: 2025-11-06 09:44:01
5
+ * @LastEditors: liaokt
6
+ * @LastEditTime: 2025-12-10 09:12:22
7
+ */
8
+
9
+ /** API 响应基础类型(Result 是 ApiResponse 的别名,保持向后兼容) */
10
+ export interface Result<T = unknown> {
11
+ code: number
12
+ message?: string
13
+ msg?: string
14
+ data: T
15
+ success?: boolean
16
+ }
17
+
18
+ /** ApiResponse 作为 Result 的别名,保持向后兼容 */
19
+ export type ApiResponse<T = unknown> = Result<T>
20
+
21
+ /** 分页请求参数 */
22
+ export interface PaginationParams {
23
+ page: number
24
+ page_size: number
25
+ [key: string]: unknown
26
+ }
27
+
28
+ /** 分页响应数据 */
29
+ export interface PaginationData<T> {
30
+ list: T[]
31
+ total: number
32
+ page: number
33
+ page_size: number
34
+ }
35
+
36
+ /** 分页响应类型 */
37
+ export interface PaginationResponse<T> extends Result<PaginationData<T>> {}
38
+
39
+ /** 列表响应类型 */
40
+ export interface ListResponse<T> extends Result<T[]> {}
41
+
42
+ /** 响应处理结果接口 */
43
+ export interface ResponseResult<T = unknown> {
44
+ success: boolean
45
+ data: T | null
46
+ message: string
47
+ code: number
48
+ }
49
+
50
+ /** 请求配置扩展 */
51
+ export interface RequestConfig {
52
+ hiddenError?: boolean
53
+ showLoading?: boolean
54
+ }
55
+
56
+ /** 默认成功码(与后端约定一致时可覆盖) */
57
+ export const DEFAULT_API_SUCCESS_CODE = 10200
package/src/request.ts ADDED
@@ -0,0 +1,178 @@
1
+ /*
2
+ * @Author: liaokt
3
+ * @Description: 基于 axios 的请求封装,支持拦截器与可配置错误/401 处理
4
+ * @Date: 2025-11-06 09:14:28
5
+ * @LastEditors: liaokt
6
+ * @LastEditTime: 2025-12-26 10:23:57
7
+ */
8
+ import axios from 'axios'
9
+ import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
10
+ import { getStorage, removeStorage } from './storage'
11
+ import type { Result } from './request-types'
12
+ import { DEFAULT_API_SUCCESS_CODE } from './request-types'
13
+
14
+ /**
15
+ * 扩展 AxiosRequestConfig
16
+ */
17
+ declare module 'axios' {
18
+ export interface AxiosRequestConfig {
19
+ hiddenError?: boolean
20
+ }
21
+ }
22
+
23
+ export interface CreateRequestOptions {
24
+ baseURL?: string
25
+ timeout?: number
26
+ /** 业务成功码,默认 10200 */
27
+ successCode?: number
28
+ /** 获取 token,用于请求头 Authorization */
29
+ getToken?: () => string | undefined
30
+ /** 显示错误提示,不传则仅 console */
31
+ showError?: (msg: string) => void
32
+ /** 401 未授权时的回调(如清 token、跳转登录) */
33
+ onUnauthorized?: (message?: string) => void
34
+ /** 存储 key:token、用户信息等,用于 401 时清除 */
35
+ storageKeysToClearOn401?: string[]
36
+ /** 响应头带 key 时保存(如验证码 key),不传则不处理 */
37
+ onSaveCaptchaKey?: (key: string) => void
38
+ }
39
+
40
+ let errorMessageTimer: ReturnType<typeof setTimeout> | null = null
41
+
42
+ function debouncedShowError(showErrorFn: (msg: string) => void, errorMsg: string): void {
43
+ if (errorMessageTimer) clearTimeout(errorMessageTimer)
44
+ errorMessageTimer = setTimeout(() => showErrorFn(errorMsg), 300)
45
+ }
46
+
47
+ /**
48
+ * 创建请求实例(可传入 showError、onUnauthorized 等与 UI/路由对接)
49
+ */
50
+ export function createRequest(options: CreateRequestOptions = {}) {
51
+ const {
52
+ baseURL = '',
53
+ timeout = 10000,
54
+ successCode = DEFAULT_API_SUCCESS_CODE,
55
+ getToken,
56
+ showError = (msg) => console.error(msg),
57
+ onUnauthorized,
58
+ storageKeysToClearOn401 = ['token', 'userInfo'],
59
+ onSaveCaptchaKey
60
+ } = options
61
+
62
+ const service: AxiosInstance = axios.create({
63
+ baseURL,
64
+ timeout,
65
+ headers: {
66
+ 'Content-Type': 'application/json;charset=UTF-8'
67
+ }
68
+ })
69
+
70
+ service.interceptors.request.use(
71
+ (config) => {
72
+ const token = getToken?.() ?? getStorage<string>('token')
73
+ if (token) {
74
+ const h = config.headers as Record<string, string>
75
+ if (!h.Authorization) h.Authorization = `JWT ${token}`
76
+ }
77
+ const headers = config.headers as Record<string, string>
78
+ headers['X-Timestamp'] = String(Math.floor(Date.now() / 1000))
79
+ const csrfToken = typeof document !== 'undefined' && document.cookie
80
+ ? document.cookie
81
+ .split('; ')
82
+ .find((row) => row.startsWith('csrftoken='))
83
+ ?.split('=')[1]
84
+ : undefined
85
+ if (csrfToken) headers['X-CSRFToken'] = csrfToken
86
+ return config
87
+ },
88
+ (error: AxiosError) => {
89
+ console.error('请求错误:', error.message)
90
+ return Promise.reject(error)
91
+ }
92
+ )
93
+
94
+ service.interceptors.response.use(
95
+ (response: AxiosResponse) => {
96
+ const headers = response.headers as Record<string, string>
97
+ if (headers && 'key' in headers && onSaveCaptchaKey) {
98
+ onSaveCaptchaKey(headers.key as string)
99
+ }
100
+ if (response.config.responseType === 'blob') {
101
+ return response
102
+ }
103
+
104
+ const body = response.data as Result
105
+ const code = body?.code
106
+ const msg = body?.message ?? body?.msg
107
+
108
+ if (code === successCode) {
109
+ return response.data
110
+ }
111
+
112
+ if (!response.config.hiddenError) {
113
+ debouncedShowError(showError, msg ?? '请求失败')
114
+ }
115
+ return Promise.reject(response.data)
116
+ },
117
+ (error: AxiosError) => {
118
+ const status = error.response?.status
119
+ const errorData = error.response?.data as Result | undefined
120
+ let errorMessage: string | undefined
121
+ if (errorData && typeof errorData === 'object') {
122
+ errorMessage = (errorData as Result).message ?? (errorData as Result).msg
123
+ }
124
+
125
+ switch (status) {
126
+ case 401: {
127
+ storageKeysToClearOn401.forEach((key) => removeStorage(key))
128
+ const msg = errorMessage ?? '登录已失效,请重新登录'
129
+ if (!error?.config?.hiddenError) debouncedShowError(showError, msg)
130
+ onUnauthorized?.(msg)
131
+ return Promise.reject(error)
132
+ }
133
+ case 403:
134
+ errorMessage = errorMessage ?? '无访问权限'
135
+ break
136
+ case 404:
137
+ errorMessage = errorMessage ?? '请求地址不存在'
138
+ break
139
+ case 500:
140
+ errorMessage = errorMessage ?? '服务器内部错误'
141
+ break
142
+ default:
143
+ errorMessage = errorMessage ?? '网络请求失败'
144
+ }
145
+
146
+ if (!error?.config?.hiddenError && status !== 401 && status !== 404) {
147
+ debouncedShowError(showError, errorMessage ?? '网络请求失败')
148
+ }
149
+ return Promise.reject(error)
150
+ }
151
+ )
152
+
153
+ const request = {
154
+ get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T> {
155
+ return service.get(url, config)
156
+ },
157
+ post<T = unknown>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> {
158
+ return service.post(url, data, config)
159
+ },
160
+ put<T = unknown>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> {
161
+ return service.put(url, data, config)
162
+ },
163
+ patch<T = unknown>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> {
164
+ return service.patch(url, data, config)
165
+ },
166
+ delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T> {
167
+ return service.delete(url, config)
168
+ }
169
+ }
170
+
171
+ return { request, axiosInstance: service }
172
+ }
173
+
174
+ /** 默认实例(无 UI/路由注入,仅控制台报错),应用内建议使用 createRequest 传入 showError/onUnauthorized */
175
+ const { request, axiosInstance } = createRequest()
176
+
177
+ export { request, axiosInstance }
178
+ export default axiosInstance
package/src/state.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export {
2
+ useToggle,
3
+ useCounter,
4
+ useLocalStorage,
5
+ useSessionStorage
6
+ } from '@vueuse/core'
package/src/state.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 状态管理工具
3
+ * 响应式状态管理相关的工具函数
4
+ */
5
+
6
+ export {
7
+ useToggle,
8
+ useCounter,
9
+ useLocalStorage,
10
+ useSessionStorage
11
+ } from '@vueuse/core'
package/src/storage.ts ADDED
@@ -0,0 +1,143 @@
1
+ /*
2
+ * @Author: liaokt
3
+ * @Description: 本地存储工具函数
4
+ * @Date: 2025-12-10
5
+ */
6
+
7
+ function getStorageImpl(): Storage | null {
8
+ return typeof localStorage !== 'undefined' ? localStorage : null
9
+ }
10
+
11
+ function getSessionImpl(): Storage | null {
12
+ return typeof sessionStorage !== 'undefined' ? sessionStorage : null
13
+ }
14
+
15
+ /**
16
+ * 获取本地存储数据
17
+ * @param key 键名
18
+ * @param defaultValue 默认值
19
+ */
20
+ export function getStorage<T = unknown>(key: string, defaultValue?: T): T | undefined {
21
+ try {
22
+ const storage = getStorageImpl()
23
+ if (!storage) return defaultValue
24
+ const item = storage.getItem(key)
25
+ if (item === null) return defaultValue
26
+ return JSON.parse(item) as T
27
+ } catch {
28
+ return defaultValue
29
+ }
30
+ }
31
+
32
+ /**
33
+ * 设置本地存储数据
34
+ * @param key 键名
35
+ * @param value 数据值
36
+ */
37
+ export function setStorage<T = unknown>(key: string, value: T): void {
38
+ try {
39
+ const storage = getStorageImpl()
40
+ if (storage) storage.setItem(key, JSON.stringify(value))
41
+ } catch (error) {
42
+ console.error('存储数据失败:', error)
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 移除本地存储数据
48
+ * @param key 键名
49
+ */
50
+ export function removeStorage(key: string): void {
51
+ try {
52
+ const storage = getStorageImpl()
53
+ if (storage) storage.removeItem(key)
54
+ } catch (error) {
55
+ console.error('删除数据失败:', error)
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 清空所有本地存储数据
61
+ */
62
+ export function clearStorage(): void {
63
+ try {
64
+ const storage = getStorageImpl()
65
+ if (storage) storage.clear()
66
+ } catch (error) {
67
+ console.error('清空存储失败:', error)
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 检查本地存储中是否存在某个键
73
+ * @param key 键名
74
+ */
75
+ export function hasStorage(key: string): boolean {
76
+ const storage = getStorageImpl()
77
+ return storage ? storage.getItem(key) !== null : false
78
+ }
79
+
80
+ /**
81
+ * 获取会话存储数据
82
+ * @param key 键名
83
+ * @param defaultValue 默认值
84
+ */
85
+ export function getSession<T = unknown>(key: string, defaultValue?: T): T | undefined {
86
+ try {
87
+ const storage = getSessionImpl()
88
+ if (!storage) return defaultValue
89
+ const item = storage.getItem(key)
90
+ if (item === null) return defaultValue
91
+ return JSON.parse(item) as T
92
+ } catch {
93
+ return defaultValue
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 设置会话存储数据
99
+ * @param key 键名
100
+ * @param value 数据值
101
+ */
102
+ export function setSession<T = unknown>(key: string, value: T): void {
103
+ try {
104
+ const storage = getSessionImpl()
105
+ if (storage) storage.setItem(key, JSON.stringify(value))
106
+ } catch (error) {
107
+ console.error('存储数据失败:', error)
108
+ }
109
+ }
110
+
111
+ /**
112
+ * 移除会话存储数据
113
+ * @param key 键名
114
+ */
115
+ export function removeSession(key: string): void {
116
+ try {
117
+ const storage = getSessionImpl()
118
+ if (storage) storage.removeItem(key)
119
+ } catch (error) {
120
+ console.error('删除数据失败:', error)
121
+ }
122
+ }
123
+
124
+ /**
125
+ * 清空所有会话存储数据
126
+ */
127
+ export function clearSession(): void {
128
+ try {
129
+ const storage = getSessionImpl()
130
+ if (storage) storage.clear()
131
+ } catch (error) {
132
+ console.error('清空存储失败:', error)
133
+ }
134
+ }
135
+
136
+ /**
137
+ * 检查会话存储中是否存在某个键
138
+ * @param key 键名
139
+ */
140
+ export function hasSession(key: string): boolean {
141
+ const storage = getSessionImpl()
142
+ return storage ? storage.getItem(key) !== null : false
143
+ }
package/src/theme.d.ts ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 运行时主题色类型声明
3
+ * 避免在部分环境下 TS 解析 node_modules 内 .ts 失败
4
+ */
5
+ import type { Ref } from 'vue'
6
+
7
+ export interface ThemeColorOverrides {
8
+ primary?: string
9
+ primaryHover?: string
10
+ primaryActive?: string
11
+ success?: string
12
+ successHover?: string
13
+ warning?: string
14
+ warningHover?: string
15
+ danger?: string
16
+ dangerHover?: string
17
+ dangerActive?: string
18
+ info?: string
19
+ infoHover?: string
20
+ }
21
+
22
+ export type ThemeMode = 'light' | 'dark'
23
+ export type DensityMode = 'default' | 'compact'
24
+
25
+ export declare const MTN_THEME_PRESETS: readonly ['default', 'ocean']
26
+
27
+ export type MtnThemePreset = (typeof MTN_THEME_PRESETS)[number]
28
+
29
+ export interface MtnAntThemeToken {
30
+ colorPrimary?: string
31
+ colorPrimaryHover?: string
32
+ colorPrimaryActive?: string
33
+ colorLink?: string
34
+ colorLinkHover?: string
35
+ colorLinkActive?: string
36
+ borderRadius?: number
37
+ colorBgBase?: string
38
+ colorText?: string
39
+ colorTextSecondary?: string
40
+ colorBorder?: string
41
+ colorSuccess?: string
42
+ colorWarning?: string
43
+ colorError?: string
44
+ colorInfo?: string
45
+ controlHeight?: number
46
+ fontSize?: number
47
+ padding?: number
48
+ paddingXS?: number
49
+ paddingSM?: number
50
+ paddingLG?: number
51
+ margin?: number
52
+ marginXS?: number
53
+ marginSM?: number
54
+ marginLG?: number
55
+ }
56
+
57
+ export const themeModeRef: Ref<ThemeMode>
58
+ export const densityModeRef: Ref<DensityMode>
59
+ export const antThemeConfigRef: Ref<{ token: MtnAntThemeToken }>
60
+
61
+ export function syncAntTokensFromCssVars(): void
62
+
63
+ export interface ApplyMtnThemeOptions {
64
+ mode?: ThemeMode
65
+ density?: DensityMode
66
+ preset?: MtnThemePreset | string | null
67
+ colors?: ThemeColorOverrides
68
+ antToken?: Partial<MtnAntThemeToken>
69
+ }
70
+
71
+ export function applyMtnTheme(opts: ApplyMtnThemeOptions): void
72
+
73
+ export function setThemeColors(colors: ThemeColorOverrides): void
74
+ export function setThemePrimary(
75
+ primary: string,
76
+ hoverLighten?: number,
77
+ activeDarken?: number
78
+ ): void
79
+ export function setThemeMode(
80
+ mode: ThemeMode,
81
+ options?: { persist?: boolean; storageKey?: string }
82
+ ): void
83
+ export function toggleThemeMode(options?: {
84
+ persist?: boolean
85
+ storageKey?: string
86
+ }): ThemeMode
87
+ export function getThemeMode(): ThemeMode
88
+ export function setDensityMode(
89
+ mode: DensityMode,
90
+ options?: { persist?: boolean; storageKey?: string }
91
+ ): void
92
+ export function getDensityMode(): DensityMode
93
+ export function toggleDensityMode(options?: {
94
+ persist?: boolean
95
+ storageKey?: string
96
+ }): DensityMode
97
+ export function initDensityFromStorage(options?: { storageKey?: string }): DensityMode
98
+ export function persistDensityMode(mode: DensityMode, options?: { storageKey?: string }): void
99
+ export function initThemeFromStorage(options?: { storageKey?: string }): ThemeMode
100
+ export function persistThemeMode(mode: ThemeMode, options?: { storageKey?: string }): void
101
+ export function initTheme(options?: {
102
+ storageKey?: string
103
+ densityStorageKey?: string
104
+ preferColorScheme?: boolean
105
+ }): ThemeMode
106
+ export function setThemePreset(preset: MtnThemePreset | string | null | undefined): void
107
+ export function getThemePreset(): string | null