From da47f98fe9c6c8439fea04e90b72c1c760c258b6 Mon Sep 17 00:00:00 2001
From: "Ray.Hao" <1490493387@qq.com>
Date: Fri, 6 Mar 2026 13:24:34 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=87=AA=E5=AE=9A?=
=?UTF-8?q?=E4=B9=89=E5=AF=BC=E8=88=AA=E6=A0=8F=E7=BB=84=E4=BB=B6=E5=92=8C?=
=?UTF-8?q?=E7=9B=B8=E5=85=B3=20composables?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
components.d.ts | 1 +
package.json | 2 +-
src/api/user.ts | 19 +-
src/components/custom-navbar/index.vue | 207 ++++++++++
src/composables/useLoading.ts | 141 +++++++
src/composables/useNavbar.ts | 242 ++++++++++++
src/composables/useRequest.ts | 302 ++++++++++++++
src/composables/useTabbar.ts | 154 ++++++--
src/pages.json | 31 +-
src/pages/index/index.vue | 525 +++++++++++++++----------
src/pages/login/index.vue | 8 +-
src/pages/mine/index.vue | 24 +-
src/pages/work/dept/index.vue | 363 +++++++++++++++++
src/pages/work/menu/index.vue | 39 ++
src/pages/work/permission/index.vue | 39 ++
src/store/index.ts | 4 +-
src/store/modules/theme-store.ts | 89 -----
src/store/modules/theme.ts | 119 ++++++
src/store/modules/user-store.ts | 155 --------
src/store/modules/user.ts | 212 ++++++++++
src/styles/_mixins.scss | 169 ++++++++
src/styles/_variables.scss | 46 +++
src/styles/index.scss | 87 +++-
src/styles/theme.scss | 149 +++++--
src/types/auto-imports.d.ts | 19 +-
src/types/uni-components.d.ts | 226 +++++++++++
src/types/uni-pages.d.ts | 3 +
src/utils/auth.ts | 2 +-
tsconfig.json | 4 +-
unocss.config.ts | 1 +
30 files changed, 2831 insertions(+), 551 deletions(-)
create mode 100644 src/components/custom-navbar/index.vue
create mode 100644 src/composables/useLoading.ts
create mode 100644 src/composables/useNavbar.ts
create mode 100644 src/composables/useRequest.ts
create mode 100644 src/pages/work/dept/index.vue
create mode 100644 src/pages/work/menu/index.vue
create mode 100644 src/pages/work/permission/index.vue
delete mode 100644 src/store/modules/theme-store.ts
create mode 100644 src/store/modules/theme.ts
delete mode 100644 src/store/modules/user-store.ts
create mode 100644 src/store/modules/user.ts
create mode 100644 src/styles/_mixins.scss
create mode 100644 src/styles/_variables.scss
create mode 100644 src/types/uni-components.d.ts
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
-
+
+
+
-
-
+
+
-
-
-
-
-
- {{ item.title }}
-
-
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
-
-
-
- 通知公告
-
-
-
+
+
+
+
+ 通知公告
+
+
+
+
-
-
-
-
-
-
- 访客数
- {{ visitStatsData.todayUvCount }}
-
+
+
+
+
+
+ 访客数
+ {{ visitStatsData.todayUvCount }}
-
-
-
-
-
-
- 浏览量
- {{ visitStatsData.todayPvCount }}
-
+
+
+
+ 浏览量
+ {{ visitStatsData.todayPvCount }}
-
-
+
+
-
-
-
- 访问趋势
-
+
+
+
+
+
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+{
+ "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",
},
],