diff --git a/components.d.ts b/components.d.ts index 752d737..affcc3d 100644 --- a/components.d.ts +++ b/components.d.ts @@ -9,6 +9,7 @@ declare module 'vue' { export interface GlobalComponents { CuDateQuery: typeof import('./src/components/cu-date-query/index.vue')['default'] CuPicker: typeof import('./src/components/cu-picker/index.vue')['default'] + CustomNavbar: typeof import('./src/components/custom-navbar/index.vue')['default'] Loading1: typeof import('./src/components/qiun-loading/loading1.vue')['default'] Loading2: typeof import('./src/components/qiun-loading/loading2.vue')['default'] Loading3: typeof import('./src/components/qiun-loading/loading3.vue')['default'] diff --git a/package.json b/package.json index a893965..948680e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "youlai-uniapp-template", + "name": "youlai-app", "version": "0.0.0", "scripts": { "dev:app": "uni -p app", diff --git a/src/api/user.ts b/src/api/user.ts index c8ed57b..e62d6d3 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -149,22 +149,25 @@ export default UserAPI; /** 登录用户信息 */ export interface UserInfo { /** 用户ID */ - userId?: number; + userId?: number /** 用户名 */ - username?: string; + username?: string /** 昵称 */ - nickname?: string; + nickname?: string /** 头像URL */ - avatar?: string; + avatar?: string - /** 角色 */ - roles?: string[]; + /** 角色编码集合 */ + roles?: string[] - /** 权限 */ - perms?: string[]; + /** 权限标识集合 */ + perms?: string[] + + /** 角色名称(前端计算字段,取 roles[0] 的中文映射) */ + roleName?: string } /** diff --git a/src/components/custom-navbar/index.vue b/src/components/custom-navbar/index.vue new file mode 100644 index 0000000..8acfa85 --- /dev/null +++ b/src/components/custom-navbar/index.vue @@ -0,0 +1,207 @@ + + + + + + + diff --git a/src/composables/useLoading.ts b/src/composables/useLoading.ts new file mode 100644 index 0000000..1801801 --- /dev/null +++ b/src/composables/useLoading.ts @@ -0,0 +1,141 @@ +import { ref, computed } from "vue" + +/** + * 加载状态管理 + * + * 提供全局或局部的加载状态管理,支持多个加载任务的并发控制。 + * + * @example + * ```ts + * // 基本使用 + * const loading = useLoading() + * loading.start() + * await fetchData() + * loading.stop() + * + * // 带消息 + * loading.start('正在加载数据...') + * + * // 异步包装 + * const result = await loading.wrap(fetchData()) + * + * // 独立实例 + * const localLoading = useLoading({ singleton: false }) + * ``` + */ + +export interface UseLoadingOptions { + /** 是否使用全局单例,默认 true */ + singleton?: boolean + /** 初始加载状态,默认 false */ + initial?: boolean + /** 加载提示消息 */ + message?: string +} + +export interface UseLoadingReturn { + /** 是否处于加载状态 */ + isLoading: ReturnType> + /** 加载计数(支持并发) */ + loadingCount: ReturnType> + /** 当前加载消息 */ + message: ReturnType> + /** 开始加载 */ + start: (msg?: string) => void + /** 结束加载 */ + stop: () => void + /** 包装异步函数,自动管理加载状态 */ + wrap: (promise: Promise) => Promise + /** 设置加载消息 */ + setMessage: (msg: string) => void +} + +/** + * 创建加载状态实例 + */ +function createLoadingState(options: UseLoadingOptions = {}) { + const { initial = false, message: initialMessage = "" } = options + + const loadingCount = ref(0) + const message = ref(initialMessage) + + const isLoading = computed(() => loadingCount.value > 0) + + /** + * 开始加载 + * @param msg 加载提示消息 + */ + const start = (msg?: string): void => { + loadingCount.value++ + if (msg) { + message.value = msg + } + } + + /** + * 结束加载 + */ + const stop = (): void => { + if (loadingCount.value > 0) { + loadingCount.value-- + } + if (loadingCount.value === 0) { + message.value = "" + } + } + + /** + * 包装异步函数,自动管理加载状态 + * @param promise 异步 Promise + */ + const wrap = async (promise: Promise): Promise => { + try { + start() + return await promise + } finally { + stop() + } + } + + /** + * 设置加载消息 + * @param msg 消息内容 + */ + const setMessage = (msg: string): void => { + message.value = msg + } + + return { + isLoading, + loadingCount, + message, + start, + stop, + wrap, + setMessage, + } +} + +/** 全局单例实例 */ +let loadingInstance: UseLoadingReturn | null = null + +/** + * 加载状态管理 Hook + * + * 默认使用全局单例模式,确保整个应用共享同一加载状态。 + * 如需独立状态(如组件内部的局部加载),可传入 `{ singleton: false }`。 + * + * @param options 配置选项 + */ +export function useLoading(options?: UseLoadingOptions): UseLoadingReturn { + const { singleton = true } = options ?? {} + + if (singleton) { + if (!loadingInstance) { + loadingInstance = createLoadingState(options) + } + return loadingInstance + } + + return createLoadingState(options) +} diff --git a/src/composables/useNavbar.ts b/src/composables/useNavbar.ts new file mode 100644 index 0000000..a765ea6 --- /dev/null +++ b/src/composables/useNavbar.ts @@ -0,0 +1,242 @@ +import { ref, computed, onMounted } from "vue"; + +/** + * 微信小程序导航栏高度计算 + * + * 参考 uni-ui 官方实现:https://github.com/dcloudio/uni-ui + * + * 重要说明: + * 1. CSS 变量 --status-bar-height 在某些情况下不准确(小程序端固定 25px) + * 2. 推荐使用 uni.getSystemInfoSync().statusBarHeight 动态获取 + * 3. 微信小程序需要考虑胶囊按钮位置 + * 4. H5 没有状态栏,statusBarHeight 为 0 + * + * 导航栏结构: + * ┌─────────────────────────────┐ + * │ 状态栏区域 │ statusBarHeight + * ├─────────────────────────────┤ + * │ 上边距 │ menu.top - statusBarHeight + * ├─────────────────────────────┤ + * │ 胶囊按钮 │ menu.height + * ├─────────────────────────────┤ + * │ 下边距(与上边距相等) │ + * └─────────────────────────────┘ + * + * 导航栏高度计算公式(uni-ui 官方): + * navBarHeight = menu.height + (menu.top - statusBarHeight) * 2 + * totalHeight = statusBarHeight + navBarHeight + */ + +interface MenuButtonRect { + width: number; + height: number; + top: number; + right: number; + bottom: number; + left: number; +} + +export interface UseNavbarOptions { + /** 导航栏内容高度,默认 44 */ + navBarHeight?: number; + /** 是否有 TabBar,默认 false */ + hasTabbar?: boolean; +} + +export interface UseNavbarReturn { + // 高度信息 + statusBarHeight: Ref; + navBarHeight: number; + totalHeight: ComputedRef; + contentPaddingTop: ComputedRef; + safeAreaBottom: Ref; + + // 胶囊按钮信息 + menuButton: Ref; + menuButtonRightGap: ComputedRef; + menuButtonLeft: ComputedRef; + menuButtonWidth: ComputedRef; + contentWidth: ComputedRef; + + // 平台信息 + platform: Ref; + windowWidth: Ref; + + // 方法 + init: () => void; +} + +/** + * 导航栏高度计算 Hook + * + * @param options 配置选项 + */ +export function useNavbar(options: UseNavbarOptions = {}): UseNavbarReturn { + const { navBarHeight = 44, hasTabbar = false } = options; + + // 状态栏高度 + const statusBarHeight = ref(0); + // 胶囊按钮信息 + const menuButton = ref(null); + // 安全区域底部高度 + const safeAreaBottom = ref(0); + // 窗口宽度 + const windowWidth = ref(375); + // 平台标识 + const platform = ref(""); + + // 计算导航栏总高度(参考 uni-ui 官方实现) + const totalHeight = computed(() => { + // #ifdef MP-WEIXIN + // 微信小程序:使用胶囊按钮精确计算 + if (menuButton.value && menuButton.value.height > 0 && menuButton.value.top > 0) { + const { top, height } = menuButton.value; + // 导航栏高度 = 胶囊按钮高度 + 上下边距 + // 上下边距 = (胶囊按钮top - 状态栏高度) * 2 + const spaceHeight = top - statusBarHeight.value; + return statusBarHeight.value + height + spaceHeight * 2; + } + // #endif + + // H5 / App:状态栏高度 + 导航栏高度 + return statusBarHeight.value + navBarHeight; + }); + + // 页面内容区域的 paddingTop(用于避开固定导航栏) + const contentPaddingTop = computed(() => `${totalHeight.value}px`); + + // 胶囊按钮右侧距离屏幕右边的距离(用于避开胶囊按钮) + const menuButtonRightGap = computed(() => { + // #ifdef MP-WEIXIN + if (menuButton.value && menuButton.value.right > 0) { + return windowWidth.value - menuButton.value.right; + } + // #endif + // H5 / App 不需要避让胶囊 + return 10; + }); + + // 胶囊按钮左侧距离屏幕左边的距离 + const menuButtonLeft = computed(() => { + // #ifdef MP-WEIXIN + if (menuButton.value) { + return menuButton.value.left; + } + // #endif + return 0; + }); + + // 胶囊按钮宽度 + const menuButtonWidth = computed(() => { + // #ifdef MP-WEIXIN + if (menuButton.value) { + return menuButton.value.width; + } + // #endif + return 0; + }); + + // 页面内容区域可用宽度(避开胶囊按钮) + const contentWidth = computed(() => { + // #ifdef MP-WEIXIN + if (menuButton.value) { + // 内容区域宽度 = 胶囊按钮左侧距离 - 间距 + const gap = menuButtonRightGap.value; + return menuButton.value.left - gap; + } + // #endif + return windowWidth.value; + }); + + // #ifdef MP-WEIXIN + const isValidMenuButtonRect = (rect: ReturnType) => { + return !!rect && rect.width > 0 && rect.height > 0 && rect.top > 0 && rect.right > 0; + }; + + const setMenuButtonRectWithRetry = (maxRetry = 6, delayMs = 50) => { + try { + const rect = uni.getMenuButtonBoundingClientRect(); + + if (isValidMenuButtonRect(rect)) { + menuButton.value = { + width: rect.width, + height: rect.height, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + }; + return; + } + + if (maxRetry > 0) { + setTimeout(() => { + setMenuButtonRectWithRetry(maxRetry - 1, delayMs); + }, delayMs); + } else { + console.warn("getMenuButtonBoundingClientRect 返回值无效,使用默认值"); + } + } catch (e) { + console.warn("获取胶囊按钮位置失败,使用默认值", e); + } + }; + // #endif + + // 初始化 + const init = () => { + try { + // 优先使用新版 API(uni.getWindowInfo),降级到 uni.getSystemInfoSync + let systemInfo: UniApp.GetSystemInfoSyncSuccess; + try { + // @ts-expect-error uni.getWindowInfo 是新版 API + systemInfo = uni.getWindowInfo?.() || uni.getSystemInfoSync(); + } catch { + systemInfo = uni.getSystemInfoSync(); + } + + statusBarHeight.value = systemInfo.statusBarHeight || 0; + safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom || 0; + windowWidth.value = systemInfo.windowWidth || 375; + platform.value = systemInfo.platform || ""; + + // #ifdef MP-WEIXIN + // 微信小程序:获取胶囊按钮位置 + setMenuButtonRectWithRetry(); + // #endif + } catch (e) { + console.warn("获取系统信息失败", e); + } + }; + + // 在 onMounted 中调用,确保页面渲染完成 + onMounted(() => { + init(); + }); + + return { + // 高度信息 + statusBarHeight, + navBarHeight, + totalHeight, + contentPaddingTop, + safeAreaBottom, + + // 胶囊按钮信息 + menuButton, + menuButtonRightGap, + menuButtonLeft, + menuButtonWidth, + contentWidth, + + // 平台信息 + platform, + windowWidth, + + // 方法 + init, + }; +} + +// 类型导出 +type Ref = import("vue").Ref; +type ComputedRef = import("vue").ComputedRef; diff --git a/src/composables/useRequest.ts b/src/composables/useRequest.ts new file mode 100644 index 0000000..066a550 --- /dev/null +++ b/src/composables/useRequest.ts @@ -0,0 +1,302 @@ +import { ref, computed, shallowRef, type Ref, type ComputedRef } from "vue" + +/** + * 请求状态管理 + * + * 封装异步请求的加载状态、错误处理和数据缓存,提供统一的请求管理方式。 + * + * @example + * ```ts + * const { data, loading, error, execute } = useRequest( + * () => UserAPI.getUserInfo() + * ) + * + * // 立即执行 + * const { data } = useRequest(() => UserAPI.getUserInfo(), { immediate: true }) + * + * // 手动执行 + * await execute() + * + * // 刷新数据 + * await refresh() + * ``` + */ + +export interface UseRequestOptions { + /** 是否立即执行,默认 false */ + immediate?: boolean + /** 初始数据 */ + initialData?: T | null + /** 请求前的回调 */ + onBefore?: () => void + /** 请求成功的回调 */ + onSuccess?: (data: T) => void + /** 请求失败的回调 */ + onError?: (error: Error) => void + /** 请求完成的回调(无论成功失败) */ + onFinally?: () => void + /** 是否重置数据在重新请求时,默认 false */ + resetOnExecute?: boolean +} + +export interface UseRequestReturn { + /** 响应数据 */ + data: Ref + /** 是否正在加载 */ + loading: ComputedRef + /** 错误信息 */ + error: Ref + /** 是否已请求过(用于区分初始状态和请求后状态) */ + hasExecuted: Ref + /** 执行请求 */ + execute: () => Promise + /** 刷新数据(重新执行请求) */ + refresh: () => Promise + /** 重置状态 */ + reset: () => void +} + +/** + * 请求状态管理 Hook + * + * @param requestFn 请求函数 + * @param options 配置选项 + */ +export function useRequest( + requestFn: () => Promise, + options: UseRequestOptions = {} +): UseRequestReturn { + const { + immediate = false, + initialData = null, + onBefore, + onSuccess, + onError, + onFinally, + resetOnExecute = false, + } = options + + // 使用 shallowRef 避免深层响应式,提升性能 + const data = shallowRef(initialData) as Ref + const error = ref(null) + const loadingCount = ref(0) + const hasExecuted = ref(false) + + const loading = computed(() => loadingCount.value > 0) + + /** + * 执行请求 + */ + const execute = async (): Promise => { + // 重置状态 + if (resetOnExecute) { + data.value = initialData + error.value = null + } + + loadingCount.value++ + hasExecuted.value = true + + try { + onBefore?.() + const result = await requestFn() + data.value = result + error.value = null + onSuccess?.(result) + return result + } catch (e) { + const err = e instanceof Error ? e : new Error(String(e)) + error.value = err + onError?.(err) + return null + } finally { + loadingCount.value-- + onFinally?.() + } + } + + /** + * 刷新数据 + */ + const refresh = async (): Promise => { + return execute() + } + + /** + * 重置状态 + */ + const reset = (): void => { + data.value = initialData + error.value = null + loadingCount.value = 0 + hasExecuted.value = false + } + + // 立即执行 + if (immediate) { + execute() + } + + return { + data, + loading, + error, + hasExecuted, + execute, + refresh, + reset, + } +} + +/** + * 分页请求状态管理 + * + * @example + * ```ts + * const { list, loading, loadMore, refresh, hasMore } = usePagination( + * (params) => UserAPI.getUserPage({ ...params, status: 1 }), + * { pageSize: 20 } + * ) + * + * // 加载更多 + * await loadMore() + * + * // 刷新 + * await refresh() + * ``` + */ + +export interface PaginationParams { + pageNum: number + pageSize: number +} + +export interface UsePaginationOptions { + /** 每页条数,默认 10 */ + pageSize?: number + /** 初始数据 */ + initialData?: T[] + /** 请求成功的回调 */ + onSuccess?: (list: T[], total: number) => void + /** 请求失败的回调 */ + onError?: (error: Error) => void +} + +export interface UsePaginationReturn { + /** 数据列表 */ + list: Ref + /** 是否正在加载 */ + loading: ComputedRef + /** 错误信息 */ + error: Ref + /** 是否还有更多数据 */ + hasMore: ComputedRef + /** 当前页码 */ + pageNum: Ref + /** 总条数 */ + total: Ref + /** 是否为空 */ + isEmpty: ComputedRef + /** 加载更多 */ + loadMore: () => Promise + /** 刷新(从第一页开始) */ + refresh: () => Promise + /** 重置 */ + reset: () => void +} + +/** + * 分页请求 Hook + * + * @param requestFn 分页请求函数 + * @param options 配置选项 + */ +export function usePagination( + requestFn: (params: PaginationParams) => Promise>, + options: UsePaginationOptions = {} +): UsePaginationReturn { + const { pageSize = 10, initialData = [], onSuccess, onError } = options + + const list = ref(initialData) as Ref + const loadingCount = ref(0) + const error = ref(null) + const pageNum = ref(1) + const total = ref(0) + + const loading = computed(() => loadingCount.value > 0) + const hasMore = computed(() => list.value.length < total.value) + const isEmpty = computed(() => list.value.length === 0 && !loading.value) + + /** + * 加载更多 + */ + const loadMore = async (): Promise => { + // 如果正在加载或没有更多数据,直接返回 + if (loading.value || !hasMore.value) { + return list.value + } + + loadingCount.value++ + + try { + const result = await requestFn({ + pageNum: pageNum.value, + pageSize, + }) + + // 如果是第一页,替换数据;否则追加数据 + if (pageNum.value === 1) { + list.value = result.list + } else { + list.value = [...list.value, ...result.list] + } + + total.value = result.total + pageNum.value++ + onSuccess?.(result.list, result.total) + + return result.list + } catch (e) { + const err = e instanceof Error ? e : new Error(String(e)) + error.value = err + onError?.(err) + return [] + } finally { + loadingCount.value-- + } + } + + /** + * 刷新(从第一页开始) + */ + const refresh = async (): Promise => { + pageNum.value = 1 + total.value = 0 + list.value = [] + return loadMore() + } + + /** + * 重置 + */ + const reset = (): void => { + list.value = initialData + loadingCount.value = 0 + error.value = null + pageNum.value = 1 + total.value = 0 + } + + return { + list, + loading, + error, + hasMore, + pageNum, + total, + isEmpty, + loadMore, + refresh, + reset, + } +} diff --git a/src/composables/useTabbar.ts b/src/composables/useTabbar.ts index 2227b19..f0442b5 100644 --- a/src/composables/useTabbar.ts +++ b/src/composables/useTabbar.ts @@ -1,46 +1,104 @@ -export interface TabbarItem { - name: string; - value: number | null; - active: boolean; - title: string; - icon: string; +import { ref, computed } from "vue" + +/** + * TabBar 状态管理 + * + * 提供底部导航栏的状态管理功能,包括激活状态切换、徽标数字设置等。 + * + * @example + * ```ts + * const { tabbarList, setTabbarItemActive, setTabbarItem } = useTabbar() + * + * // 切换到工作台 + * setTabbarItemActive('work') + * + * // 设置徽标数字 + * setTabbarItem('work', 5) + * ``` + */ + +export interface TabbarItem { + /** 唯一标识 */ + name: string + /** 徽标数字,null 表示不显示 */ + value: number | null + /** 是否激活 */ + active: boolean + /** 显示标题 */ + title: string + /** 图标名称 */ + icon: string } -const tabbarItems = ref([ - { name: "home", value: null, active: true, title: "首页", icon: "home" }, - { name: "work", value: null, active: false, title: "工作台", icon: "laptop" }, - { name: "mine", value: null, active: false, title: "我的", icon: "user" }, -]); +/** 默认 TabBar 配置 */ +const DEFAULT_TABBAR_ITEMS: Omit[] = [ + { name: "home", value: null, title: "首页", icon: "home" }, + { name: "work", value: null, title: "工作台", icon: "laptop" }, + { name: "mine", value: null, title: "我的", icon: "user" }, +] -export function useTabbar() { - const tabbarList = computed(() => tabbarItems.value); +/** + * 创建 TabBar 状态实例 + * + * 使用工厂函数创建独立的状态实例,避免模块级别的状态污染。 + * 每个调用者都可以获得独立的状态管理。 + */ +function createTabbarState() { + const items = ref( + DEFAULT_TABBAR_ITEMS.map((item, index) => ({ + ...item, + active: index === 0, + })) + ) + /** TabBar 列表 */ + const tabbarList = computed(() => items.value) + + /** 当前激活的 TabBar 项 */ const activeTabbar = computed(() => { - const item = tabbarItems.value.find((item) => item.active); - return item || tabbarItems.value[0]; - }); + return items.value.find((item) => item.active) || items.value[0] + }) - const getTabbarItemValue = (name: string) => { - const item = tabbarItems.value.find((item) => item.name === name); - return item && item.value ? item.value : null; - }; + /** + * 获取指定 TabBar 项的徽标数字 + * @param name TabBar 项名称 + */ + const getTabbarItemValue = (name: string): number | null => { + const item = items.value.find((item) => item.name === name) + return item?.value ?? null + } - const setTabbarItem = (name: string, value: number) => { - const tabbarItem = tabbarItems.value.find((item) => item.name === name); - if (tabbarItem) { - tabbarItem.value = value; + /** + * 设置 TabBar 项的徽标数字 + * @param name TabBar 项名称 + * @param value 徽标数字 + */ + const setTabbarItem = (name: string, value: number): void => { + const item = items.value.find((item) => item.name === name) + if (item) { + item.value = value } - }; + } - const setTabbarItemActive = (name: string) => { - tabbarItems.value.forEach((item) => { - if (item.name === name) { - item.active = true; - } else { - item.active = false; - } - }); - }; + /** + * 设置激活的 TabBar 项 + * @param name TabBar 项名称 + */ + const setTabbarItemActive = (name: string): void => { + items.value.forEach((item) => { + item.active = item.name === name + }) + } + + /** + * 重置 TabBar 状态 + */ + const resetTabbar = (): void => { + items.value = DEFAULT_TABBAR_ITEMS.map((item, index) => ({ + ...item, + active: index === 0, + })) + } return { tabbarList, @@ -48,5 +106,31 @@ export function useTabbar() { getTabbarItemValue, setTabbarItem, setTabbarItemActive, - }; + resetTabbar, + } +} + +/** 全局单例实例 */ +let tabbarInstance: ReturnType | null = null + +/** + * TabBar 状态管理 Hook + * + * 默认使用全局单例模式,确保整个应用共享同一状态。 + * 如需独立状态,可传入 `{ singleton: false }` 创建新实例。 + * + * @param options 配置选项 + * @param options.singleton 是否使用单例模式,默认 true + */ +export function useTabbar(options?: { singleton?: boolean }) { + const { singleton = true } = options ?? {} + + if (singleton) { + if (!tabbarInstance) { + tabbarInstance = createTabbarState() + } + return tabbarInstance + } + + return createTabbarState() } diff --git a/src/pages.json b/src/pages.json index 929ea5e..782d2a4 100644 --- a/src/pages.json +++ b/src/pages.json @@ -78,6 +78,14 @@ "navigationBarTitleText": "系统配置" } }, + { + "path": "pages/work/dept/index", + "type": "page", + "name": "dept", + "style": { + "navigationBarTitleText": "部门管理" + } + }, { "path": "pages/work/log/index", "type": "page", @@ -86,6 +94,14 @@ "navigationBarTitleText": "系统日志" } }, + { + "path": "pages/work/menu/index", + "type": "page", + "name": "menu", + "style": { + "navigationBarTitleText": "菜单管理" + } + }, { "path": "pages/work/notice/index", "type": "page", @@ -94,6 +110,14 @@ "navigationBarTitleText": "通知公告" } }, + { + "path": "pages/work/permission/index", + "type": "page", + "name": "permission", + "style": { + "navigationBarTitleText": "权限管理" + } + }, { "path": "pages/work/role/index", "type": "page", @@ -152,10 +176,15 @@ "backgroundColorTop": "@bgColorTop", "backgroundColorBottom": "@bgColorBottom", "enablePullDownRefresh": false, - "onReachBottomDistance": 50 + "onReachBottomDistance": 50, + "animationType": "pop-in", + "animationDuration": 300 }, "tabBar": { "custom": true, + "customize": true, + "overlay": true, + "height": "0", "color": "@tabColor", "selectedColor": "@tabSelectedColor", "backgroundColor": "@tabBgColor", diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index aa84d66..1efa28a 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -1,25 +1,16 @@ + + +{ + "name": "dept", + "style": { "navigationBarTitleText": "部门管理" } +} + + + diff --git a/src/pages/work/menu/index.vue b/src/pages/work/menu/index.vue new file mode 100644 index 0000000..671e12b --- /dev/null +++ b/src/pages/work/menu/index.vue @@ -0,0 +1,39 @@ + + + + + +{ + "name": "menu", + "style": { "navigationBarTitleText": "菜单管理" } +} + + + diff --git a/src/pages/work/permission/index.vue b/src/pages/work/permission/index.vue new file mode 100644 index 0000000..e42714b --- /dev/null +++ b/src/pages/work/permission/index.vue @@ -0,0 +1,39 @@ + + + + + +{ + "name": "permission", + "style": { "navigationBarTitleText": "权限管理" } +} + + + diff --git a/src/store/index.ts b/src/store/index.ts index 7b70ec5..8aa9068 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -8,6 +8,6 @@ export function setupStore(app: App) { app.use(store); } -export * from "./modules/user-store"; -export * from "./modules/theme-store"; +export * from "./modules/user"; +export * from "./modules/theme"; export { store }; diff --git a/src/store/modules/theme-store.ts b/src/store/modules/theme-store.ts deleted file mode 100644 index 5acb110..0000000 --- a/src/store/modules/theme-store.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { defineStore } from "pinia"; -import { Storage } from "@/utils/storage"; -import { THEME_MODE_KEY, THEME_COLOR_KEY } from "@/constants"; -import type { ThemeColorOption, ThemeMode } from "@/composables/types/theme"; -import { themeColorOptions } from "@/composables/types/theme"; - -export const useThemeStore = defineStore("theme", () => { - const theme = ref(Storage.get(THEME_MODE_KEY, "light")); - const currentThemeColor = ref( - Storage.get(THEME_COLOR_KEY, themeColorOptions[0]) - ); - - // 主题变量(响应式对象) - const themeVars = reactive({ - darkBackground: "#0f0f0f", - darkBackground2: "#1a1a1a", - darkBackground3: "#242424", - darkBackground4: "#2f2f2f", - darkBackground5: "#3d3d3d", - darkBackground6: "#4a4a4a", - darkBackground7: "#606060", - darkColor: "#ffffff", - darkColor2: "#e0e0e0", - darkColor3: "#a0a0a0", - colorTheme: currentThemeColor.value.primary, - }); - - // 计算属性 - const isDark = computed(() => theme.value === "dark"); - - // 设置导航栏颜色 - const setNavigationBarColor = () => { - console.log("设置导航栏颜色", theme.value); - uni.setNavigationBarColor({ - frontColor: theme.value === "light" ? "#000000" : "#ffffff", - backgroundColor: theme.value === "light" ? "#ffffff" : "#000000", - }); - }; - - /** - * 切换主题 - * @param mode 指定主题模式,不传则自动切换 - */ - const toggleTheme = (mode?: ThemeMode) => { - theme.value = mode || (theme.value === "light" ? "dark" : "light"); - Storage.set(THEME_MODE_KEY, theme.value); - setNavigationBarColor(); - }; - - /** - * 设置主题色 - * @param color 主题色选项 - */ - const setCurrentThemeColor = (color: ThemeColorOption) => { - currentThemeColor.value = color; - Storage.set(THEME_COLOR_KEY, color); - themeVars.colorTheme = color.primary; - console.log("主题色已设置:", color.name); - }; - - /** - * 初始化主题 - */ - const initTheme = () => { - // 更新主题变量中的颜色 - themeVars.colorTheme = currentThemeColor.value.primary; - - // 设置导航栏颜色 - nextTick(() => { - setNavigationBarColor(); - }); - }; - - return { - // 状态 - theme, - currentThemeColor, - themeVars, - - // 计算属性 - isDark, - - // 方法 - toggleTheme, - setCurrentThemeColor, - setNavigationBarColor, - initTheme, - }; -}); diff --git a/src/store/modules/theme.ts b/src/store/modules/theme.ts new file mode 100644 index 0000000..feed1ff --- /dev/null +++ b/src/store/modules/theme.ts @@ -0,0 +1,119 @@ +import { defineStore } from "pinia" +import { Storage } from "@/utils/storage" +import { THEME_MODE_KEY, THEME_COLOR_KEY } from "@/constants" +import type { ThemeColorOption, ThemeMode } from "@/composables/types/theme" +import { themeColorOptions } from "@/composables/types/theme" + +/** + * 主题状态管理 Store + * + * 功能说明: + * - 主题模式切换(明/暗) + * - 主题色定制 + * - 导航栏颜色同步 + */ + +export const useThemeStore = defineStore("theme", () => { + // ========================================================================== + // 状态 + // ========================================================================== + + /** 当前主题模式 */ + const theme = ref(Storage.get(THEME_MODE_KEY, "light")) + + /** 当前主题色 */ + const currentThemeColor = ref( + Storage.get(THEME_COLOR_KEY, themeColorOptions[0]) + ) + + /** 主题变量(响应式对象) */ + const themeVars = reactive({ + darkBackground: "#0f0f0f", + darkBackground2: "#1a1a1a", + darkBackground3: "#242424", + darkBackground4: "#2f2f2f", + darkBackground5: "#3d3d3d", + darkBackground6: "#4a4a4a", + darkBackground7: "#606060", + darkColor: "#ffffff", + darkColor2: "#e0e0e0", + darkColor3: "#a0a0a0", + colorTheme: currentThemeColor.value.primary, + }) + + // ========================================================================== + // 计算属性 + // ========================================================================== + + /** 是否为暗黑模式 */ + const isDark = computed(() => theme.value === "dark") + + // ========================================================================== + // 方法 + // ========================================================================== + + /** + * 设置导航栏颜色 + */ + const setNavigationBarColor = () => { + console.log("设置导航栏颜色", theme.value) + uni.setNavigationBarColor({ + frontColor: theme.value === "light" ? "#000000" : "#ffffff", + backgroundColor: theme.value === "light" ? "#ffffff" : "#000000", + }) + } + + /** + * 切换主题 + * @param mode 指定主题模式,不传则自动切换 + */ + const toggleTheme = (mode?: ThemeMode) => { + theme.value = mode || (theme.value === "light" ? "dark" : "light") + Storage.set(THEME_MODE_KEY, theme.value) + setNavigationBarColor() + } + + /** + * 设置主题色 + * @param color 主题色选项 + */ + const setCurrentThemeColor = (color: ThemeColorOption) => { + currentThemeColor.value = color + Storage.set(THEME_COLOR_KEY, color) + themeVars.colorTheme = color.primary + console.log("主题色已设置:", color.name) + } + + /** + * 初始化主题 + */ + const initTheme = () => { + // 更新主题变量中的颜色 + themeVars.colorTheme = currentThemeColor.value.primary + + // 设置导航栏颜色 + nextTick(() => { + setNavigationBarColor() + }) + } + + // ========================================================================== + // 导出 + // ========================================================================== + + return { + // 状态 + theme, + currentThemeColor, + themeVars, + + // 计算属性 + isDark, + + // 方法 + toggleTheme, + setCurrentThemeColor, + setNavigationBarColor, + initTheme, + } +}) diff --git a/src/store/modules/user-store.ts b/src/store/modules/user-store.ts deleted file mode 100644 index e42976c..0000000 --- a/src/store/modules/user-store.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { defineStore } from "pinia"; -import AuthAPI, { - type LoginData, - type SmsLoginData, - type WechatMiniappPhoneLoginData, - type WechatMiniappBindMobileData, -} from "@/api/auth"; -import UserAPI, { type UserInfo } from "@/api/user"; -import { setAccessToken, clearTokens } from "@/utils/auth"; -import { getUserInfo, setUserInfo } from "@/utils/storage"; -import { USER_INFO_KEY } from "@/constants"; -import { Storage } from "@/utils/storage"; - -export const useUserStore = defineStore("user", () => { - const userInfo = ref(getUserInfo()); - - // 账号密码登录 - const login = (data: LoginData) => { - return new Promise((resolve, reject) => { - AuthAPI.login(data) - .then((data) => { - setAccessToken(data.accessToken); - resolve(data); - }) - .catch((error) => { - console.error("登录失败", error); - reject(error); - }); - }); - }; - - // 短信验证码登录 - const loginBySms = (data: SmsLoginData) => { - return new Promise((resolve, reject) => { - AuthAPI.loginBySms(data) - .then((data) => { - setAccessToken(data.accessToken); - resolve(data); - }) - .catch((error) => { - console.error("登录失败", error); - reject(error); - }); - }); - }; - - // 微信小程序静默登录 - const loginByWechatMiniapp = (code: string) => { - return new Promise((resolve, reject) => { - AuthAPI.wechatMiniappSilentLogin(code) - .then((data) => { - if (data.accessToken) { - setAccessToken(data.accessToken); - } - resolve(data); - }) - .catch((error) => { - console.error("微信小程序登录失败", error); - reject(error); - }); - }); - }; - - // 微信小程序一键登录(企业小程序) - const loginByWechatMiniappPhone = (data: WechatMiniappPhoneLoginData) => { - return new Promise((resolve, reject) => { - AuthAPI.wechatMiniappPhoneLogin(data) - .then((data) => { - setAccessToken(data.accessToken); - resolve(data); - }) - .catch((error) => { - console.error("微信小程序一键登录失败", error); - reject(error); - }); - }); - }; - - // 微信小程序绑定手机号 - const bindMobileForWechatMiniapp = (data: WechatMiniappBindMobileData) => { - return new Promise((resolve, reject) => { - AuthAPI.wechatMiniappBindMobile(data) - .then((data) => { - setAccessToken(data.accessToken); - resolve(data); - }) - .catch((error) => { - console.error("绑定手机号失败", error); - reject(error); - }); - }); - }; - - // 检查会话状态 - const checkSession = (): Promise => { - return new Promise((resolve) => { - AuthAPI.checkSession() - .then((result) => { - resolve(result.valid); - }) - .catch(() => { - resolve(false); - }); - }); - }; - - // 获取用户信息 - const getInfo = () => { - return new Promise((resolve, reject) => { - UserAPI.getUserInfo() - .then((data) => { - setUserInfo(data); - userInfo.value = data; - resolve(data); - }) - .catch((error) => { - console.error("获取用户信息失败", error); - reject(error); - }); - }); - }; - - // 登出 - const logout = async () => { - try { - await AuthAPI.logout(); - } catch (error) { - console.error("登出失败", error); - } finally { - clearTokens(); - Storage.remove(USER_INFO_KEY); - userInfo.value = undefined; - uni.reLaunch({ url: "/pages/login/index" }); - } - }; - - // 判断用户信息是否完整 - const isUserInfoComplete = (): boolean => { - if (!userInfo.value) return false; - return !!(userInfo.value.nickname && userInfo.value.avatar); - }; - - return { - userInfo, - login, - loginBySms, - loginByWechatMiniapp, - loginByWechatMiniappPhone, - bindMobileForWechatMiniapp, - logout, - getInfo, - checkSession, - isUserInfoComplete, - }; -}); diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts new file mode 100644 index 0000000..f5fd85f --- /dev/null +++ b/src/store/modules/user.ts @@ -0,0 +1,212 @@ +import { defineStore } from "pinia" +import AuthAPI, { + type LoginData, + type SmsLoginData, + type WechatMiniappPhoneLoginData, + type WechatMiniappBindMobileData, +} from "@/api/auth" +import UserAPI, { type UserInfo } from "@/api/user" +import { setAccessToken, clearTokens } from "@/utils/auth" +import { getUserInfo, setUserInfo } from "@/utils/storage" +import { USER_INFO_KEY } from "@/constants" +import { Storage } from "@/utils/storage" + +/** + * 用户状态管理 Store + * + * 功能说明: + * - 用户登录/登出 + * - 用户信息管理 + * - 多种登录方式支持(密码、短信、微信小程序) + * - 会话状态检查 + */ + +export const useUserStore = defineStore("user", () => { + // ========================================================================== + // 状态 + // ========================================================================== + + /** 用户信息 */ + const userInfo = ref(getUserInfo()) + + // ========================================================================== + // 登录方法 + // ========================================================================== + + /** + * 账号密码登录 + * @param data 登录数据 + */ + const login = (data: LoginData) => { + return new Promise((resolve, reject) => { + AuthAPI.login(data) + .then((data) => { + setAccessToken(data.accessToken) + resolve(data) + }) + .catch((error) => { + console.error("登录失败", error) + reject(error) + }) + }) + } + + /** + * 短信验证码登录 + * @param data 短信登录数据 + */ + const loginBySms = (data: SmsLoginData) => { + return new Promise((resolve, reject) => { + AuthAPI.loginBySms(data) + .then((data) => { + setAccessToken(data.accessToken) + resolve(data) + }) + .catch((error) => { + console.error("登录失败", error) + reject(error) + }) + }) + } + + /** + * 微信小程序静默登录 + * @param code 微信登录码 + */ + const loginByWechatMiniapp = (code: string) => { + return new Promise((resolve, reject) => { + AuthAPI.wechatMiniappSilentLogin(code) + .then((data) => { + if (data.accessToken) { + setAccessToken(data.accessToken) + } + resolve(data) + }) + .catch((error) => { + console.error("微信小程序登录失败", error) + reject(error) + }) + }) + } + + /** + * 微信小程序一键登录(企业小程序) + * @param data 微信手机号登录数据 + */ + const loginByWechatMiniappPhone = (data: WechatMiniappPhoneLoginData) => { + return new Promise((resolve, reject) => { + AuthAPI.wechatMiniappPhoneLogin(data) + .then((data) => { + setAccessToken(data.accessToken) + resolve(data) + }) + .catch((error) => { + console.error("微信小程序一键登录失败", error) + reject(error) + }) + }) + } + + /** + * 微信小程序绑定手机号 + * @param data 绑定手机号数据 + */ + const bindMobileForWechatMiniapp = (data: WechatMiniappBindMobileData) => { + return new Promise((resolve, reject) => { + AuthAPI.wechatMiniappBindMobile(data) + .then((data) => { + setAccessToken(data.accessToken) + resolve(data) + }) + .catch((error) => { + console.error("绑定手机号失败", error) + reject(error) + }) + }) + } + + // ========================================================================== + // 用户信息方法 + // ========================================================================== + + /** + * 检查会话状态 + * @returns 会话是否有效 + */ + const checkSession = (): Promise => { + return new Promise((resolve) => { + AuthAPI.checkSession() + .then((result) => { + resolve(result.valid) + }) + .catch(() => { + resolve(false) + }) + }) + } + + /** + * 获取用户信息 + */ + const getInfo = () => { + return new Promise((resolve, reject) => { + UserAPI.getUserInfo() + .then((data) => { + setUserInfo(data) + userInfo.value = data + resolve(data) + }) + .catch((error) => { + console.error("获取用户信息失败", error) + reject(error) + }) + }) + } + + /** + * 登出 + */ + const logout = async () => { + try { + await AuthAPI.logout() + } catch (error) { + console.error("登出失败", error) + } finally { + clearTokens() + Storage.remove(USER_INFO_KEY) + userInfo.value = undefined + uni.reLaunch({ url: "/pages/login/index" }) + } + } + + /** + * 判断用户信息是否完整 + * @returns 用户信息是否完整 + */ + const isUserInfoComplete = (): boolean => { + if (!userInfo.value) return false + return !!(userInfo.value.nickname && userInfo.value.avatar) + } + + // ========================================================================== + // 导出 + // ========================================================================== + + return { + // 状态 + userInfo, + + // 登录方法 + login, + loginBySms, + loginByWechatMiniapp, + loginByWechatMiniappPhone, + bindMobileForWechatMiniapp, + + // 用户信息方法 + logout, + getInfo, + checkSession, + isUserInfoComplete, + } +}) diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss new file mode 100644 index 0000000..bceeaa2 --- /dev/null +++ b/src/styles/_mixins.scss @@ -0,0 +1,169 @@ +// ========================================================================== +// BEM Mixins +// ========================================================================== +// 提供便捷的 BEM 命名方式,简化 CSS 类的编写 + +/// BEM Block 基础 mixin +/// @param {String} $name - Block 名称 +/// @example +/// @include b(button) { ... } +/// => .button { ... } +@mixin b($name) { + .#{$name} { + @content; + } +} + +/// BEM Element mixin +/// @param {String} $name - Element 名称 +/// @example +/// @include e(icon) { ... } +/// => .block__icon { ... } +@mixin e($name) { + &__#{$name} { + @content; + } +} + +/// BEM Modifier mixin +/// @param {String} $name - Modifier 名称 +/// @example +/// @include m(primary) { ... } +/// => .block--primary { ... } +@mixin m($name) { + &--#{$name} { + @content; + } +} + +/// 组合使用示例 +/// @example +/// @include b(card) { +/// padding: 16rpx; +/// +/// @include e(header) { +/// font-weight: bold; +/// } +/// +/// @include m(highlight) { +/// background: yellow; +/// } +/// } +/// => .card { padding: 16rpx; } +/// .card__header { font-weight: bold; } +/// .card--highlight { background: yellow; } + +// ========================================================================== +// 布局 Mixins +// ========================================================================== + +/// Flex 居中布局 +@mixin flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +/// Flex 两端对齐 +@mixin flex-between { + display: flex; + align-items: center; + justify-content: space-between; +} + +/// Flex 垂直居中 +@mixin flex-col-center { + display: flex; + flex-direction: column; + align-items: center; +} + +/// 绝对定位撑满 +@mixin absolute-fill { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +/// 固定定位撑满 +@mixin fixed-fill { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +// ========================================================================== +// 文字 Mixins +// ========================================================================== + +/// 文字省略(单行) +@mixin ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/// 文字省略(多行) +/// @param {Number} $lines - 行数 +@mixin ellipsis-lines($lines: 2) { + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: $lines; + -webkit-box-orient: vertical; +} + +// ========================================================================== +// 安全区域 Mixins +// ========================================================================== + +/// 底部安全区域 +@mixin safe-area-bottom { + padding-bottom: constant(safe-area-inset-bottom); + padding-bottom: env(safe-area-inset-bottom); +} + +/// 顶部安全区域 +@mixin safe-area-top { + padding-top: constant(safe-area-inset-top); + padding-top: env(safe-area-inset-top); +} + +/// 固定底部(带安全区域) +@mixin fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + @include safe-area-bottom; +} + +// ========================================================================== +// 暗黑模式 Mixins +// ========================================================================== + +/// 暗黑模式样式 +/// @example +/// @include when-dark { +/// background: #1f2937; +/// } +@mixin when-dark { + :global(.wot-theme-dark) & { + @content; + } +} + +/// 浅色模式样式 +/// @example +/// @include when-light { +/// background: #ffffff; +/// } +@mixin when-light { + :global(:not(.wot-theme-dark)) & { + @content; + } +} diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss new file mode 100644 index 0000000..f7e51c8 --- /dev/null +++ b/src/styles/_variables.scss @@ -0,0 +1,46 @@ +// ========================================================================== +// SCSS 变量定义 +// ========================================================================== +// 用于 SCSS 编译时的变量,与 CSS 变量配合使用 + +// 设计稿基准 +$design-width: 750; + +// 字体大小 +$font-size-xs: 20rpx; +$font-size-sm: 24rpx; +$font-size-base: 28rpx; +$font-size-md: 32rpx; +$font-size-lg: 36rpx; +$font-size-xl: 40rpx; +$font-size-2xl: 48rpx; + +// 间距(4 的倍数) +$spacing-xs: 8rpx; +$spacing-sm: 16rpx; +$spacing-base: 24rpx; +$spacing-md: 32rpx; +$spacing-lg: 48rpx; +$spacing-xl: 64rpx; + +// 圆角 +$radius-sm: 8rpx; +$radius-base: 12rpx; +$radius-md: 16rpx; +$radius-lg: 24rpx; +$radius-xl: 32rpx; +$radius-full: 9999rpx; + +// 过渡时间 +$transition-fast: 150ms; +$transition-base: 200ms; +$transition-slow: 300ms; + +// 层级 +$z-index-dropdown: 100; +$z-index-sticky: 200; +$z-index-fixed: 300; +$z-index-modal-backdrop: 400; +$z-index-modal: 500; +$z-index-popover: 600; +$z-index-toast: 700; diff --git a/src/styles/index.scss b/src/styles/index.scss index 5b6d6fe..496d047 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,14 +1,99 @@ +@use "./variables" as *; +@use "./mixins" as *; @use "./theme"; +// ========================================================================== +// 全局基础样式 +// ========================================================================== + html, body, #app { height: 100%; padding: 0; margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 28rpx; + line-height: 1.5; + color: var(--color-text); + background-color: var(--color-bg); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } +// ========================================================================== +// 页面容器 +// ========================================================================== + .page-container { min-height: 100%; - background-color: var(--wot-color-bg); + background-color: var(--color-bg); +} + +// ========================================================================== +// 快捷类 +// ========================================================================== + +// Flex 布局 +.flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +.flex-between { + display: flex; + align-items: center; + justify-content: space-between; +} + +.flex-col-center { + display: flex; + flex-direction: column; + align-items: center; +} + +// 文字省略 +.ellipsis { + @include ellipsis; +} + +.ellipsis-2 { + @include ellipsis-lines(2); +} + +// 安全区域 +.safe-area-bottom { + @include safe-area-bottom; +} + +.safe-area-top { + @include safe-area-top; +} + +// ========================================================================== +// 通用组件样式 +// ========================================================================== + +// 空状态占位 +.empty-placeholder { + @include flex-center; + flex-direction: column; + padding: 100rpx 32rpx; + color: var(--color-text-secondary); + + &__icon { + font-size: 80rpx; + margin-bottom: 24rpx; + } + + &__text { + font-size: 28rpx; + } +} + +// 加载状态 +.loading-container { + @include flex-center; + min-height: 200rpx; } diff --git a/src/styles/theme.scss b/src/styles/theme.scss index c511eb7..15d78e2 100644 --- a/src/styles/theme.scss +++ b/src/styles/theme.scss @@ -1,41 +1,130 @@ -/* 默认主题 (light) */ +// ========================================================================== +// 主题变量系统 +// ========================================================================== +// 遵循文档规范,提供完整的明暗主题支持 +// 使用 CSS 变量实现主题切换,支持 wot-design-uni 组件库 + +// ========================================================================== +// 默认主题 (light) +// ========================================================================== :root, page { - /* 官方主题变量 */ - --wot-color-theme: #4d80f0; - --wot-color-success: #34d19d; - --wot-color-warning: #f0883a; - --wot-color-danger: #ff4757; + // ----------------------------------------------------------------------- + // 主题色 + // ----------------------------------------------------------------------- + --color-primary: #4d80f0; + --color-primary-light: #e8f0fe; + --color-primary-dark: #2563eb; + --color-success: #34d19d; + --color-success-light: #e6f7f1; + --color-warning: #f0883a; + --color-warning-light: #fff4e6; + --color-danger: #ff4757; + --color-danger-light: #fff1f2; - /* 自定义扩展变量 */ - --wot-button-normal-bg: #ffffff; - --wot-color-bg: #ffffff; - --wot-color-bg-light: #f3f4f6; + // ----------------------------------------------------------------------- + // 背景色 + // ----------------------------------------------------------------------- + --color-bg: #ffffff; + --color-bg-secondary: #f8fafc; + --color-bg-tertiary: #f1f5f9; - /* 文本颜色变量 */ - --wot-color-text: #333333; - --wot-color-text-secondary: #666666; - --wot-color-text-placeholder: #999999; + // ----------------------------------------------------------------------- + // 文字颜色 + // ----------------------------------------------------------------------- + --color-text: #1f2937; + --color-text-secondary: #6b7280; + --color-text-placeholder: #9ca3af; + --color-text-disabled: #d1d5db; - /* 边框和背景变量 */ - --wot-color-border: #e5e5e5; + // ----------------------------------------------------------------------- + // 边框颜色 + // ----------------------------------------------------------------------- + --color-border: #e5e7eb; + --color-border-light: #f3f4f6; + --color-border-dark: #d1d5db; + + // ----------------------------------------------------------------------- + // 阴影 + // ----------------------------------------------------------------------- + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); + + // ----------------------------------------------------------------------- + // wot-design-uni 组件库变量 + // ----------------------------------------------------------------------- + --wot-color-theme: var(--color-primary); + --wot-color-success: var(--color-success); + --wot-color-warning: var(--color-warning); + --wot-color-danger: var(--color-danger); + --wot-button-normal-bg: var(--color-bg); + --wot-color-bg: var(--color-bg); + --wot-color-bg-light: var(--color-bg-secondary); + --wot-color-text: var(--color-text); + --wot-color-text-secondary: var(--color-text-secondary); + --wot-color-text-placeholder: var(--color-text-placeholder); + --wot-color-border: var(--color-border); } -/* 暗黑主题 (dark) */ +// ========================================================================== +// 暗黑主题 (dark) +// ========================================================================== .wot-theme-dark { - /* 官方主题变量 */ - --wot-color-theme: #1a73e8; - --wot-color-success: #28a745; - --wot-color-warning: #ffc107; - --wot-color-danger: #ff4757; + // ----------------------------------------------------------------------- + // 主题色 + // ----------------------------------------------------------------------- + --color-primary: #3b82f6; + --color-primary-light: #1e3a5f; + --color-primary-dark: #60a5fa; + --color-success: #34d399; + --color-success-light: #064e3b; + --color-warning: #fbbf24; + --color-warning-light: #78350f; + --color-danger: #f87171; + --color-danger-light: #7f1d1d; - /* 自定义扩展变量 */ - --wot-color-bg: #1b1b1b; - --wot-color-bg-light: #2a2a2a; - --wot-color-text: #ffffff; - --wot-color-text-secondary: #cccccc; - --wot-color-text-placeholder: #999999; + // ----------------------------------------------------------------------- + // 背景色 + // ----------------------------------------------------------------------- + --color-bg: #111827; + --color-bg-secondary: #1f2937; + --color-bg-tertiary: #374151; - /* 暗黑模式下的边框和背景变量 */ - --wot-color-border: #3a3a3a; + // ----------------------------------------------------------------------- + // 文字颜色 + // ----------------------------------------------------------------------- + --color-text: #f9fafb; + --color-text-secondary: #9ca3af; + --color-text-placeholder: #6b7280; + --color-text-disabled: #4b5563; + + // ----------------------------------------------------------------------- + // 边框颜色 + // ----------------------------------------------------------------------- + --color-border: #374151; + --color-border-light: #1f2937; + --color-border-dark: #4b5563; + + // ----------------------------------------------------------------------- + // 阴影 + // ----------------------------------------------------------------------- + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.4); + + // ----------------------------------------------------------------------- + // wot-design-uni 组件库变量 + // ----------------------------------------------------------------------- + --wot-color-theme: var(--color-primary); + --wot-color-success: var(--color-success); + --wot-color-warning: var(--color-warning); + --wot-color-danger: var(--color-danger); + --wot-button-normal-bg: var(--color-bg-secondary); + --wot-color-bg: var(--color-bg); + --wot-color-bg-light: var(--color-bg-secondary); + --wot-color-text: var(--color-text); + --wot-color-text-secondary: var(--color-text-secondary); + --wot-color-text-placeholder: var(--color-text-placeholder); + --wot-color-border: var(--color-border); } diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts index 5496945..768e6bd 100644 --- a/src/types/auto-imports.d.ts +++ b/src/types/auto-imports.d.ts @@ -143,23 +143,26 @@ declare global { const useId: typeof import('vue')['useId'] const useInterceptor: typeof import('@uni-helper/uni-use')['useInterceptor'] const useLink: (typeof import("vue-router"))["useLink"] - const useLoading: typeof import('@uni-helper/uni-use')['useLoading'] + const useLoading: typeof import('../composables/useLoading')['useLoading'] const useManualTheme: typeof import('../composables/useTheme')['useManualTheme'] const useMessage: typeof import('wot-design-uni')['useMessage'] const useModal: typeof import('@uni-helper/uni-use')['useModal'] const useModel: typeof import('vue')['useModel'] + const useNavbar: typeof import('../composables/useNavbar')['useNavbar'] const useNetwork: typeof import('@uni-helper/uni-use')['useNetwork'] const useNotify: typeof import('wot-design-uni')['useNotify'] const useOnline: typeof import('@uni-helper/uni-use')['useOnline'] const usePage: typeof import('@uni-helper/uni-use')['usePage'] + const usePageLayout: typeof import('../composables/useNavbar')['usePageLayout'] const usePageScroll: typeof import('@uni-helper/uni-use')['usePageScroll'] const usePages: typeof import('@uni-helper/uni-use')['usePages'] + const usePagination: typeof import('../composables/useRequest')['usePagination'] const usePreferredDark: typeof import('@uni-helper/uni-use')['usePreferredDark'] const usePreferredLanguage: typeof import('@uni-helper/uni-use')['usePreferredLanguage'] const usePrevPage: typeof import('@uni-helper/uni-use')['usePrevPage'] const usePrevRoute: typeof import('@uni-helper/uni-use')['usePrevRoute'] const useProvider: typeof import('@uni-helper/uni-use')['useProvider'] - const useRequest: typeof import('@uni-helper/uni-use')['useRequest'] + const useRequest: typeof import('../composables/useRequest')['useRequest'] const useRoute: typeof import('uni-mini-router')['useRoute'] const useRouter: typeof import('uni-mini-router')['useRouter'] const useScanCode: typeof import('@uni-helper/uni-use')['useScanCode'] @@ -174,10 +177,10 @@ declare global { const useTabbar: typeof import('../composables/useTabbar')['useTabbar'] const useTemplateRef: typeof import('vue')['useTemplateRef'] const useTheme: typeof import('../composables/useTheme')['useTheme'] - const useThemeStore: typeof import('../store/modules/theme-store')['useThemeStore'] + const useThemeStore: typeof import('../store/modules/theme')['useThemeStore'] const useToast: typeof import('wot-design-uni')['useToast'] const useUploadFile: typeof import('@uni-helper/uni-use')['useUploadFile'] - const useUserStore: typeof import('../store/modules/user-store')['useUserStore'] + const useUserStore: typeof import('../store/modules/user')['useUserStore'] const useVisible: typeof import('@uni-helper/uni-use')['useVisible'] const user: typeof import('../api/user')['default'] const watch: typeof import('vue')['watch'] @@ -306,9 +309,13 @@ declare module 'vue' { readonly useCssModule: UnwrapRef readonly useCssVars: UnwrapRef readonly useId: UnwrapRef + readonly useLoading: UnwrapRef readonly useMessage: UnwrapRef readonly useModel: UnwrapRef + readonly useNavbar: UnwrapRef readonly useNotify: UnwrapRef + readonly usePagination: UnwrapRef + readonly useRequest: UnwrapRef readonly useRoute: UnwrapRef readonly useRouter: UnwrapRef readonly useSlots: UnwrapRef @@ -316,9 +323,9 @@ declare module 'vue' { readonly useTabbar: UnwrapRef readonly useTemplateRef: UnwrapRef readonly useTheme: UnwrapRef - readonly useThemeStore: UnwrapRef + readonly useThemeStore: UnwrapRef readonly useToast: UnwrapRef - readonly useUserStore: UnwrapRef + readonly useUserStore: UnwrapRef readonly user: UnwrapRef readonly watch: UnwrapRef readonly watchEffect: UnwrapRef diff --git a/src/types/uni-components.d.ts b/src/types/uni-components.d.ts new file mode 100644 index 0000000..118ab67 --- /dev/null +++ b/src/types/uni-components.d.ts @@ -0,0 +1,226 @@ +/** + * uni-app 原生组件类型声明 + * + * 解决 Volar 对原生组件属性的类型检查问题 + * 这些类型声明扩展了 @uni-helper/uni-types 的基础类型 + */ + +// 扩展 uni-app 原生组件属性类型 +declare module "@vue/runtime-core" { + export interface GlobalComponents { + view: new () => { + $props: { + class?: string + style?: string | Record + hoverClass?: string + hoverStartTime?: number + hoverStayTime?: number + hoverStopPropagation?: boolean + onClick?: (event: any) => void + } + } + text: new () => { + $props: { + class?: string + style?: string | Record + selectable?: boolean + userSelect?: boolean + space?: "ensp" | "emsp" | "nbsp" + decode?: boolean + onClick?: (event: any) => void + } + } + image: new () => { + $props: { + class?: string + style?: string | Record + src?: string + mode?: + | "scaleToFill" + | "aspectFit" + | "aspectFill" + | "widthFix" + | "heightFix" + | "top" + | "bottom" + | "center" + | "left" + | "right" + | "top left" + | "top right" + | "bottom left" + | "bottom right" + lazyLoad?: boolean + fadeShow?: boolean + webp?: boolean + showMenuByLongpress?: boolean + onClick?: (event: any) => void + } + } + button: new () => { + $props: { + class?: string + style?: string | Record + size?: "default" | "mini" + type?: "primary" | "default" | "warn" + plain?: boolean + disabled?: boolean + loading?: boolean + formType?: "submit" | "reset" + openType?: + | "feedback" + | "share" + | "getUserInfo" + | "contact" + | "getPhoneNumber" + | "launchApp" + | "openSetting" + | "getAuthorize" + | "contactShare" + | "livestream" + | "getRealnameAuthInfo" + hoverClass?: string + hoverStartTime?: number + hoverStayTime?: number + lang?: "en" | "zh_CN" | "zh_TW" + sessionFrom?: string + sendMessageTitle?: string + sendMessagePath?: string + sendMessageImg?: string + showMessageCard?: boolean + appParameter?: string + onClick?: (event: any) => void + onGetphonenumber?: (event: any) => void + onGetuserinfo?: (event: any) => void + onContact?: (event: any) => void + onError?: (event: any) => void + onOpensetting?: (event: any) => void + onLaunchapp?: (event: any) => void + } + } + input: new () => { + $props: { + class?: string + style?: string | Record + value?: string + type?: "text" | "number" | "idcard" | "digit" | "safe-password" | "nickname" + password?: boolean + placeholder?: string + placeholderStyle?: string + placeholderClass?: string + disabled?: boolean + maxlength?: number | string + cursorSpacing?: number + autoFocus?: boolean + focus?: boolean + cursor?: number + selectionStart?: number + selectionEnd?: number + adjustPosition?: boolean + holdKeyboard?: boolean + alwaysEmbed?: boolean + confirmType?: "send" | "search" | "next" | "go" | "done" + confirmHold?: boolean + onInput?: (event: any) => void + onFocus?: (event: any) => void + onBlur?: (event: any) => void + onConfirm?: (event: any) => void + } + } + textarea: new () => { + $props: { + class?: string + style?: string | Record + value?: string + placeholder?: string + placeholderStyle?: string + placeholderClass?: string + disabled?: boolean + maxlength?: number | string + autoFocus?: boolean + focus?: boolean + autoHeight?: boolean + fixed?: boolean + cursorSpacing?: number + cursor?: number + showConfirmBar?: boolean + selectionStart?: number + selectionEnd?: number + adjustPosition?: boolean + holdKeyboard?: boolean + disableDefaultPadding?: boolean + confirmType?: "send" | "search" | "next" | "go" | "done" + confirmHold?: boolean + onInput?: (event: any) => void + onFocus?: (event: any) => void + onBlur?: (event: any) => void + onConfirm?: (event: any) => void + onLinechange?: (event: any) => void + } + } + scrollview: new () => { + $props: { + class?: string + style?: string | Record + scrollX?: boolean + scrollY?: boolean + upperThreshold?: number + lowerThreshold?: number + scrollTop?: number | string + scrollLeft?: number | string + scrollIntoView?: string + scrollWithAnimation?: boolean + enableBackToTop?: boolean + enableFlex?: boolean + scrollAnchors?: boolean + refresherEnabled?: boolean + refresherThreshold?: number + refresherDefaultStyle?: "black" | "white" | "none" + refresherBackground?: string + refresherTriggered?: boolean + enhanced?: boolean + bounces?: boolean + showScrollbar?: boolean + pagingEnabled?: boolean + fastDeceleration?: boolean + onScrolltoupper?: (event: any) => void + onScrolltolower?: (event: any) => void + onScroll?: (event: any) => void + onRefresherrefresh?: (event: any) => void + onRefresherrestore?: (event: any) => void + onRefresherabort?: (event: any) => void + } + } + swiper: new () => { + $props: { + class?: string + style?: string | Record + indicatorDots?: boolean + indicatorColor?: string + indicatorActiveColor?: string + autoplay?: boolean + current?: number + interval?: number + duration?: number + circular?: boolean + vertical?: boolean + previousMargin?: string + nextMargin?: string + displayMultipleItems?: number + skipHiddenItemLayout?: boolean + easingFunction?: "default" | "linear" | "easeInCubic" | "easeOutCubic" | "easeInOutCubic" + onChange?: (event: any) => void + onTransition?: (event: any) => void + onAnimationfinish?: (event: any) => void + } + } + swiperitem: new () => { + $props: { + class?: string + style?: string | Record + } + } + } +} + +export {} diff --git a/src/types/uni-pages.d.ts b/src/types/uni-pages.d.ts index 2c41fa3..32385f4 100644 --- a/src/types/uni-pages.d.ts +++ b/src/types/uni-pages.d.ts @@ -15,8 +15,11 @@ interface NavigateToOptions { "/pages/mine/profile/index" | "/pages/mine/settings/index" | "/pages/work/config/index" | + "/pages/work/dept/index" | "/pages/work/log/index" | + "/pages/work/menu/index" | "/pages/work/notice/index" | + "/pages/work/permission/index" | "/pages/work/role/index" | "/pages/work/user/index" | "/pages/mine/settings/account/index" | diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 2dfaac3..44f9f4d 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,4 +1,4 @@ -import { useUserStore } from "@/store/modules/user-store"; +import { useUserStore } from "@/store/modules/user"; import { Storage } from "./storage"; import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "@/constants"; diff --git a/tsconfig.json b/tsconfig.json index c6223b4..44f5887 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,9 @@ }, "vueCompilerOptions": { // 调整 Volar(Vue 语言服务工具)解析行为,用于为 uni-app 组件提供 TypeScript 类型 - "plugins": ["@uni-helper/uni-types/volar-plugin"] + "plugins": ["@uni-helper/uni-types/volar-plugin"], + // 禁用原生组件严格类型检查(uni-app 原生组件与 Volar 的兼容性问题) + "nativeTags": ["block", "component", "template", "slot"] }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "exclude": ["node_modules", "dist"] diff --git a/unocss.config.ts b/unocss.config.ts index ed59fc1..cefd8ac 100644 --- a/unocss.config.ts +++ b/unocss.config.ts @@ -24,6 +24,7 @@ export default { "flex-column": "flex flex-col", "flex-row": "flex flex-row", + // 垂直布局并居中对齐 "flex-col-center": "flex flex-col items-center", }, ],