feat: 新增隐私政策页面并优化设置模块
refactor: 重构用户信息存储和主题管理逻辑 fix: 修正登录类型字段缺失问题 style: 统一页面导航栏样式和布局 docs: 更新README和文档内容 chore: 清理无用代码和资源文件 perf: 优化网络请求和错误处理逻辑 test: 更新API测试用例 build: 更新依赖版本和构建配置 ci: 调整CI/CD脚本配置
@@ -22,4 +22,5 @@ class OnlineOutSchema(BaseModel):
|
||||
os: Optional[str] = Field(default=None, description='操作系统')
|
||||
browser: Optional[str] = Field(default=None, description='浏览器')
|
||||
login_time: Optional[DateTimeStr] = Field(default=None, description='登录时间')
|
||||
login_type: Optional[str] = Field(default=None, description='登录类型 PC端 | 移动端')
|
||||
|
||||
|
||||
@@ -83,12 +83,12 @@ class LoginService:
|
||||
user = await UserCRUD(auth).update_last_login_crud(id=user.id)
|
||||
|
||||
# 创建token
|
||||
token = await cls.create_token_service(request=request, redis=redis, user=user)
|
||||
token = await cls.create_token_service(request=request, redis=redis, user=user, login_type=login_form.login_type)
|
||||
|
||||
return token
|
||||
|
||||
@classmethod
|
||||
async def create_token_service(cls, request: Request, redis: Redis, user: UserModel) -> JWTOutSchema:
|
||||
async def create_token_service(cls, request: Request, redis: Redis, user: UserModel, login_type: str) -> JWTOutSchema:
|
||||
"""
|
||||
创建访问令牌和刷新令牌
|
||||
|
||||
@@ -129,6 +129,7 @@ class LoginService:
|
||||
os=user_agent.os.family,
|
||||
browser = user_agent.browser.family,
|
||||
login_time=user.last_login.isoformat() if isinstance(user.last_login, datetime) else str(user.last_login),
|
||||
login_type=login_type
|
||||
).model_dump_json()
|
||||
|
||||
access_token = create_access_token(payload=JWTPayloadSchema(
|
||||
|
||||
@@ -58,7 +58,7 @@ async def get_current_user(
|
||||
# 解析token
|
||||
payload = decode_access_token(token)
|
||||
if not payload or not hasattr(payload, 'is_refresh') or payload.is_refresh:
|
||||
raise CustomException(msg="非法凭证", status_code=401)
|
||||
raise CustomException(msg="非法凭证", code=10401, status_code=401)
|
||||
|
||||
online_user_info = payload.sub
|
||||
# 从Redis中获取用户信息
|
||||
@@ -66,23 +66,23 @@ async def get_current_user(
|
||||
|
||||
session_id = user_info.get("session_id")
|
||||
if not session_id:
|
||||
raise CustomException(msg="认证已失效", status_code=401)
|
||||
raise CustomException(msg="认证已失效", code=10401, status_code=401)
|
||||
|
||||
# 检查用户是否在线
|
||||
online_ok = await RedisCURD(redis).exists(key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}')
|
||||
if not online_ok:
|
||||
raise CustomException(msg="认证已失效", status_code=401)
|
||||
raise CustomException(msg="认证已失效", code=10401, status_code=401)
|
||||
|
||||
auth = AuthSchema(db=db)
|
||||
username = user_info.get("user_name")
|
||||
if not username:
|
||||
raise CustomException(msg="认证已失效", status_code=401)
|
||||
raise CustomException(msg="认证已失效", code=10401, status_code=401)
|
||||
# 获取用户信息
|
||||
user = await UserCRUD(auth).get_by_username_crud(username=username)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在", status_code=401)
|
||||
raise CustomException(msg="用户不存在", code=10401, status_code=401)
|
||||
if not user.status:
|
||||
raise CustomException(msg="用户已被停用", status_code=401)
|
||||
raise CustomException(msg="用户已被停用", code=10401, status_code=401)
|
||||
|
||||
# 设置请求上下文
|
||||
request.scope["user_id"] = user.id
|
||||
@@ -156,11 +156,11 @@ class AuthPermission:
|
||||
# 严格模式:要求所有权限都满足
|
||||
if not all(perm in user_permissions for perm in self.permissions):
|
||||
logger.error(f"用户缺少所需的权限: {self.permissions}")
|
||||
raise CustomException(msg="无权限操作", status_code=403)
|
||||
raise CustomException(msg="无权限操作", code=10403, status_code=403)
|
||||
else:
|
||||
# 非严格模式:满足任一权限即可
|
||||
if not any(perm in user_permissions for perm in self.permissions):
|
||||
logger.error(f"用户缺少任何所需的权限: {self.permissions}")
|
||||
raise CustomException(msg="无权限操作", status_code=403)
|
||||
raise CustomException(msg="无权限操作", code=10403, status_code=403)
|
||||
|
||||
return auth
|
||||
|
||||
@@ -55,6 +55,7 @@ class CustomOAuth2PasswordRequestForm(OAuth2PasswordRequestForm):
|
||||
password: str = Form(),
|
||||
captcha_key: Optional[str] = Form(default=""),
|
||||
captcha: Optional[str] = Form(default=""),
|
||||
login_type: Optional[str] = Form(default="PC端", description="PC端 | 移动端")
|
||||
):
|
||||
super().__init__(
|
||||
grant_type=grant_type,
|
||||
@@ -62,10 +63,11 @@ class CustomOAuth2PasswordRequestForm(OAuth2PasswordRequestForm):
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
username=username,
|
||||
password=password
|
||||
password=password,
|
||||
)
|
||||
self.captcha_key = captcha_key
|
||||
self.captcha = captcha
|
||||
self.login_type = login_type
|
||||
|
||||
|
||||
# OAuth2认证配置
|
||||
@@ -88,7 +90,7 @@ def create_access_token(payload: JWTPayloadSchema) -> str:
|
||||
def decode_access_token(token: str) -> JWTPayloadSchema:
|
||||
"""解析JWT访问令牌"""
|
||||
if not token:
|
||||
raise CustomException(msg="认证不存在,请重新登录", status_code=401)
|
||||
raise CustomException(msg="认证不存在,请重新登录", code=10401, status_code=401)
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
@@ -99,15 +101,15 @@ def decode_access_token(token: str) -> JWTPayloadSchema:
|
||||
|
||||
online_user_info = payload.get("sub")
|
||||
if not online_user_info:
|
||||
raise CustomException(msg="无效认证,请重新登录", status_code=401)
|
||||
raise CustomException(msg="无效认证,请重新登录", code=10401, status_code=401)
|
||||
|
||||
return JWTPayloadSchema(**payload)
|
||||
|
||||
except (jwt.InvalidSignatureError, jwt.DecodeError):
|
||||
raise CustomException(msg="无效认证,请重新登录", status_code=401)
|
||||
raise CustomException(msg="无效认证,请重新登录", code=10401, status_code=401)
|
||||
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise CustomException(msg="认证已过期,请重新登录", status_code=401)
|
||||
raise CustomException(msg="认证已过期,请重新登录", code=10401, status_code=401)
|
||||
|
||||
except jwt.InvalidTokenError:
|
||||
raise CustomException(msg="token已失效,请重新登录", status_code=401)
|
||||
raise CustomException(msg="token已失效,请重新登录", code=10401, status_code=401)
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"id": 8,
|
||||
"config_name": "帮助文档",
|
||||
"config_key": "sys_help_doc",
|
||||
"config_value": "https://service.fastapiadmin.com/site/index.html",
|
||||
"config_value": "https://service.fastapiadmin.com",
|
||||
"config_type": true,
|
||||
"description": "帮助文档",
|
||||
"creator_id": 1
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 3.0 MiB |
@@ -1,3 +1,5 @@
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
|
||||
# 环境
|
||||
VITE_APP_ENV=development
|
||||
|
||||
@@ -17,6 +19,5 @@ VITE_APP_PORT=5180
|
||||
# 超时时间
|
||||
VITE_TIMEOUT=10000
|
||||
|
||||
|
||||
# WebSocket 服务器的 URL,不配置默认关闭 WebSocket
|
||||
#VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws
|
||||
VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws
|
||||
@@ -1,3 +1,5 @@
|
||||
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||
|
||||
# 环境
|
||||
VITE_APP_ENV=production
|
||||
|
||||
@@ -17,4 +19,4 @@ VITE_APP_PORT=5180
|
||||
VITE_TIMEOUT=10000
|
||||
|
||||
# WebSocket 服务器的 URL,不配置默认关闭 WebSocket
|
||||
#VITE_APP_WS_ENDPOINT= ws://localhost:4096/ws
|
||||
VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws
|
||||
@@ -93,6 +93,68 @@
|
||||
"watchSyncEffect": true,
|
||||
"DirectiveBinding": true,
|
||||
"MaybeRef": true,
|
||||
"MaybeRefOrGetter": true
|
||||
"MaybeRefOrGetter": true,
|
||||
"CommonUtil": true,
|
||||
"Storage": true,
|
||||
"acceptHMRUpdate": true,
|
||||
"applyThemeOnPageShow": true,
|
||||
"applyThemeToMiniProgram": true,
|
||||
"auth": true,
|
||||
"checkLogin": true,
|
||||
"clearAll": true,
|
||||
"clearToken": true,
|
||||
"clearTokens": true,
|
||||
"clearUserInfo": true,
|
||||
"createPinia": true,
|
||||
"createRouter": true,
|
||||
"debounce": true,
|
||||
"defineStore": true,
|
||||
"dept": true,
|
||||
"dict": true,
|
||||
"file": true,
|
||||
"getAccessToken": true,
|
||||
"getActivePinia": true,
|
||||
"getDarkerColor": true,
|
||||
"getLighterColor": true,
|
||||
"getRefreshToken": true,
|
||||
"getToken": true,
|
||||
"getUserInfo": true,
|
||||
"isLoggedIn": true,
|
||||
"log": true,
|
||||
"mapActions": true,
|
||||
"mapGetters": true,
|
||||
"mapState": true,
|
||||
"mapStores": true,
|
||||
"mapWritableState": true,
|
||||
"menu": true,
|
||||
"notice": true,
|
||||
"request": true,
|
||||
"requireLogin": true,
|
||||
"role": true,
|
||||
"setAccessToken": true,
|
||||
"setActivePinia": true,
|
||||
"setMapStoreSuffix": true,
|
||||
"setRefreshToken": true,
|
||||
"setToken": true,
|
||||
"setUserInfo": true,
|
||||
"setupStore": true,
|
||||
"store": true,
|
||||
"storeToRefs": true,
|
||||
"useDictStore": true,
|
||||
"useMessage": true,
|
||||
"useNotify": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useThemeStore": true,
|
||||
"useToast": true,
|
||||
"useUserStore": true,
|
||||
"user": true,
|
||||
"config": true,
|
||||
"extendedColorOptions": true,
|
||||
"themeColorOptions": true,
|
||||
"useStomp": true,
|
||||
"useTabbar": true,
|
||||
"useTheme": true,
|
||||
"useWechat": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-recommended",
|
||||
"stylelint-config-recommended-scss",
|
||||
"stylelint-config-recommended-vue/scss",
|
||||
"stylelint-config-html/vue",
|
||||
"stylelint-config-recess-order"
|
||||
],
|
||||
"extends": ["stylelint-config-recommended", "stylelint-config-recommended-scss", "stylelint-config-recommended-vue/scss", "stylelint-config-html/vue", "stylelint-config-recess-order"],
|
||||
"plugins": ["stylelint-prettier"],
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
## 项目介绍
|
||||
# 项目介绍
|
||||
|
||||
基于 uni-app + Vue 3 + TypeScript 移动端跨平台开发模板,集成了 ESLint、Prettier、Stylelint、Husky 和 Commitlint 等工具,确保代码规范与质量。
|
||||
|
||||
## 项目截图
|
||||
|
||||

|
||||

|
||||
|
||||
## 项目文档
|
||||
|
||||
@@ -14,51 +14,20 @@
|
||||
|
||||
安装依赖
|
||||
|
||||
```
|
||||
```node
|
||||
pnpm install
|
||||
```
|
||||
|
||||
h5启动
|
||||
|
||||
```
|
||||
```node
|
||||
pnpm run dev:h5
|
||||
```
|
||||
|
||||
构建
|
||||
|
||||
```
|
||||
```node
|
||||
pnpm run build:h5
|
||||
```
|
||||
|
||||
访问 [http://localhost:4096](http://localhost:4096)
|
||||
|
||||
## 组件结构
|
||||
|
||||
项目组件分为以下几类:
|
||||
|
||||
- **基础组件**:`src/components` 下的通用组件
|
||||
- **业务组件**:`src/components/business` 下的业务相关组件
|
||||
- `WechatProfile.vue`: 微信小程序环境下的头像、昵称和性别选择组件
|
||||
|
||||
使用业务组件:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<WechatProfile v-model="profileData" @change="onProfileChange" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { WechatProfile } from "@/components/business";
|
||||
|
||||
const profileData = ref({
|
||||
avatar: "",
|
||||
nickname: "",
|
||||
gender: 1,
|
||||
});
|
||||
|
||||
const onProfileChange = (data) => {
|
||||
console.log("个人资料变更:", data);
|
||||
};
|
||||
</script>
|
||||
```
|
||||
访问 [http://localhost:5180/app](http://localhost:5180/app)
|
||||
|
||||
@@ -8,7 +8,10 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CuDateQuery: typeof import('./src/components/cu-date-query/index.vue')['default']
|
||||
CuDict: typeof import('./src/components/cu-dict/index.vue')['default']
|
||||
CuDictLabel: typeof import('./src/components/cu-dict-label/index.vue')['default']
|
||||
CuPicker: typeof import('./src/components/cu-picker/index.vue')['default']
|
||||
DataTree: typeof import('./src/components/data-tree/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']
|
||||
@@ -17,29 +20,35 @@ declare module 'vue' {
|
||||
QiunDataCharts: typeof import('./src/components/qiun-data-charts/qiun-data-charts.vue')['default']
|
||||
QiunError: typeof import('./src/components/qiun-error/qiun-error.vue')['default']
|
||||
QiunLoading: typeof import('./src/components/qiun-loading/qiun-loading.vue')['default']
|
||||
TodoItem: typeof import('./src/components/todo/TodoItem.vue')['default']
|
||||
TodoList: typeof import('./src/components/todo/TodoList.vue')['default']
|
||||
WdBadge: typeof import('wot-design-uni/components/wd-badge/wd-badge.vue')['default']
|
||||
WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default']
|
||||
WdCard: typeof import('wot-design-uni/components/wd-card/wd-card.vue')['default']
|
||||
WdCell: typeof import('wot-design-uni/components/wd-cell/wd-cell.vue')['default']
|
||||
WdCellGroup: typeof import('wot-design-uni/components/wd-cell-group/wd-cell-group.vue')['default']
|
||||
WdCheckbox: typeof import('wot-design-uni/components/wd-checkbox/wd-checkbox.vue')['default']
|
||||
WdCollapse: typeof import('wot-design-uni/components/wd-collapse/wd-collapse.vue')['default']
|
||||
WdCollapseItem: typeof import('wot-design-uni/components/wd-collapse-item/wd-collapse-item.vue')['default']
|
||||
WdConfigProvider: typeof import('wot-design-uni/components/wd-config-provider/wd-config-provider.vue')['default']
|
||||
WdDivider: typeof import('wot-design-uni/components/wd-divider/wd-divider.vue')['default']
|
||||
WdField: typeof import('wot-design-uni/components/wd-field/wd-field.vue')['default']
|
||||
WdForm: typeof import('wot-design-uni/components/wd-form/wd-form.vue')['default']
|
||||
WdFormItem: typeof import('wot-design-uni/components/wd-form-item/wd-form-item.vue')['default']
|
||||
WdFormItem: (typeof import("wot-design-uni/components/wd-form-item/wd-form-item.vue"))["default"]
|
||||
WdGrid: typeof import('wot-design-uni/components/wd-grid/wd-grid.vue')['default']
|
||||
WdGridItem: typeof import('wot-design-uni/components/wd-grid-item/wd-grid-item.vue')['default']
|
||||
WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default']
|
||||
WdImage: typeof import('wot-design-uni/components/wd-image/wd-image.vue')['default']
|
||||
WdImg: typeof import('wot-design-uni/components/wd-img/wd-img.vue')['default']
|
||||
WdImgCropper: typeof import('wot-design-uni/components/wd-img-cropper/wd-img-cropper.vue')['default']
|
||||
WdImge: typeof import('wot-design-uni/components/wd-imge/wd-imge.vue')['default']
|
||||
WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default']
|
||||
WdLoading: typeof import('wot-design-uni/components/wd-loading/wd-loading.vue')['default']
|
||||
WdMessageBox: typeof import('wot-design-uni/components/wd-message-box/wd-message-box.vue')['default']
|
||||
WdNavbar: typeof import('wot-design-uni/components/wd-navbar/wd-navbar.vue')['default']
|
||||
WdNoticeBar: typeof import('wot-design-uni/components/wd-notice-bar/wd-notice-bar.vue')['default']
|
||||
WdNotify: typeof import('wot-design-uni/components/wd-notify/wd-notify.vue')['default']
|
||||
WdPasswordInput: typeof import('wot-design-uni/components/wd-password-input/wd-password-input.vue')['default']
|
||||
WdPasswordInput: (typeof import("wot-design-uni/components/wd-password-input/wd-password-input.vue"))["default"]
|
||||
WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default']
|
||||
WdProgress: typeof import('wot-design-uni/components/wd-progress/wd-progress.vue')['default']
|
||||
WdRadio: typeof import('wot-design-uni/components/wd-radio/wd-radio.vue')['default']
|
||||
@@ -47,13 +56,16 @@ declare module 'vue' {
|
||||
WdStatusTip: typeof import('wot-design-uni/components/wd-status-tip/wd-status-tip.vue')['default']
|
||||
WdSwiper: typeof import('wot-design-uni/components/wd-swiper/wd-swiper.vue')['default']
|
||||
WdSwitch: typeof import('wot-design-uni/components/wd-switch/wd-switch.vue')['default']
|
||||
WdTab: typeof import('wot-design-uni/components/wd-tab/wd-tab.vue')['default']
|
||||
WdTabbar: typeof import('wot-design-uni/components/wd-tabbar/wd-tabbar.vue')['default']
|
||||
WdTabbarItem: typeof import('wot-design-uni/components/wd-tabbar-item/wd-tabbar-item.vue')['default']
|
||||
WdTabs: typeof import('wot-design-uni/components/wd-tabs/wd-tabs.vue')['default']
|
||||
WdTag: typeof import('wot-design-uni/components/wd-tag/wd-tag.vue')['default']
|
||||
WdText: typeof import('wot-design-uni/components/wd-text/wd-text.vue')['default']
|
||||
WdTextarea: typeof import('wot-design-uni/components/wd-textarea/wd-textarea.vue')['default']
|
||||
WdTitle: typeof import('wot-design-uni/components/wd-title/wd-title.vue')['default']
|
||||
WdTitle: (typeof import("wot-design-uni/components/wd-title/wd-title.vue"))["default"]
|
||||
WdToast: typeof import('wot-design-uni/components/wd-toast/wd-toast.vue')['default']
|
||||
WdUpload: typeof import('wot-design-uni/components/wd-upload/wd-upload.vue')['default']
|
||||
WdView: typeof import('wot-design-uni/components/wd-view/wd-view.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,7 @@ const {
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<wd-config-provider
|
||||
:theme="theme"
|
||||
:theme-vars="themeVars"
|
||||
custom-style="min-height: 100vh"
|
||||
:class="{ 'wot-theme-dark': theme === 'dark' }"
|
||||
>
|
||||
<wd-config-provider :theme="theme" :theme-vars="themeVars" custom-style="min-height: 100vh" :class="{ 'wot-theme-dark': theme === 'dark' }">
|
||||
<!-- 页面内容 -->
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
@@ -49,12 +44,7 @@ const {
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<wd-config-provider
|
||||
:theme="theme"
|
||||
:theme-vars="themeVars"
|
||||
custom-style="background-color: #f5f5f5;min-height: 100vh"
|
||||
:class="{ 'wot-theme-dark': theme === 'dark' }"
|
||||
>
|
||||
<wd-config-provider :theme="theme" :theme-vars="themeVars" custom-style="background-color: #f5f5f5;min-height: 100vh" :class="{ 'wot-theme-dark': theme === 'dark' }">
|
||||
<!-- 页面内容 -->
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
@@ -170,6 +160,7 @@ const THEME_COLOR_STORAGE_KEY = "app_theme_color";
|
||||
```
|
||||
|
||||
主题设置会在以下情况自动保存:
|
||||
|
||||
- 切换主题模式时
|
||||
- 设置主题色时
|
||||
- 重置主题时
|
||||
|
||||
@@ -29,55 +29,55 @@ pnpm add - uni-mini-router
|
||||
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
import { createRouter } from 'uni-mini-router'
|
||||
import { pages, subPackages } from 'virtual:uni-pages'
|
||||
import { createRouter } from "uni-mini-router";
|
||||
import { pages, subPackages } from "virtual:uni-pages";
|
||||
|
||||
// 生成路由配置
|
||||
function generateRoutes() {
|
||||
const routes = pages.map((page) => {
|
||||
const newPath = `/${page.path}`
|
||||
return { ...page, path: newPath }
|
||||
})
|
||||
const newPath = `/${page.path}`;
|
||||
return { ...page, path: newPath };
|
||||
});
|
||||
|
||||
// 处理分包路由
|
||||
if (subPackages && subPackages.length > 0) {
|
||||
subPackages.forEach((subPackage) => {
|
||||
const subRoutes = subPackage.pages.map((page: any) => {
|
||||
const newPath = `/${subPackage.root}/${page.path}`
|
||||
return { ...page, path: newPath }
|
||||
})
|
||||
routes.push(...subRoutes)
|
||||
})
|
||||
const newPath = `/${subPackage.root}/${page.path}`;
|
||||
return { ...page, path: newPath };
|
||||
});
|
||||
routes.push(...subRoutes);
|
||||
});
|
||||
}
|
||||
|
||||
return routes
|
||||
return routes;
|
||||
}
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
routes: generateRoutes(),
|
||||
})
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
```
|
||||
|
||||
### 3. 在main.ts中挂载路由
|
||||
|
||||
```typescript
|
||||
// src/main.ts
|
||||
import { createSSRApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { createSSRApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
const app = createSSRApp(App);
|
||||
|
||||
// 使用路由
|
||||
app.use(router)
|
||||
app.use(router);
|
||||
|
||||
return {
|
||||
app
|
||||
}
|
||||
app,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -87,22 +87,22 @@ export function createApp() {
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
"vue",
|
||||
{
|
||||
from: 'uni-mini-router',
|
||||
imports: ['createRouter', 'useRouter', 'useRoute']
|
||||
}
|
||||
from: "uni-mini-router",
|
||||
imports: ["createRouter", "useRouter", "useRoute"],
|
||||
},
|
||||
],
|
||||
dts: 'src/auto-imports.d.ts'
|
||||
})
|
||||
]
|
||||
})
|
||||
dts: "src/auto-imports.d.ts",
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## 三、路由基本用法
|
||||
@@ -112,55 +112,55 @@ export default defineConfig({
|
||||
uni-mini-router提供了多种导航方法:
|
||||
|
||||
```typescript
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
// 字符串路径导航
|
||||
router.push('/pages/index/index')
|
||||
router.push("/pages/index/index");
|
||||
|
||||
// 对象导航(通过路径)
|
||||
router.push({ path: '/pages/index/index' })
|
||||
router.push({ path: "/pages/index/index" });
|
||||
|
||||
// 对象导航(通过名称)
|
||||
router.push({ name: 'index' })
|
||||
router.push({ name: "index" });
|
||||
|
||||
// 携带参数
|
||||
router.push({
|
||||
path: '/pages/detail/index',
|
||||
query: { id: 10 }
|
||||
})
|
||||
path: "/pages/detail/index",
|
||||
query: { id: 10 },
|
||||
});
|
||||
|
||||
// 通过名称 + 参数
|
||||
router.push({
|
||||
name: 'detail',
|
||||
params: { id: 10 }
|
||||
})
|
||||
name: "detail",
|
||||
params: { id: 10 },
|
||||
});
|
||||
|
||||
// Tab页面导航
|
||||
router.pushTab('/pages/home/index')
|
||||
router.pushTab("/pages/home/index");
|
||||
|
||||
// 关闭当前页面并跳转
|
||||
router.replace('/pages/index/index')
|
||||
router.replace("/pages/index/index");
|
||||
|
||||
// 关闭所有页面并跳转
|
||||
router.replaceAll('/pages/index/index')
|
||||
router.replaceAll("/pages/index/index");
|
||||
|
||||
// 返回上一级
|
||||
router.back()
|
||||
router.back();
|
||||
|
||||
// 返回多级
|
||||
router.back(2)
|
||||
router.back(2);
|
||||
```
|
||||
|
||||
### 2. 获取和使用路由信息
|
||||
|
||||
```typescript
|
||||
const route = useRoute()
|
||||
const route = useRoute();
|
||||
|
||||
// 访问当前路由信息
|
||||
console.log(route.path) // 当前路由路径
|
||||
console.log(route.name) // 当前路由名称
|
||||
console.log(route.query) // 查询参数
|
||||
console.log(route.params) // 路由参数
|
||||
console.log(route.path); // 当前路由路径
|
||||
console.log(route.name); // 当前路由名称
|
||||
console.log(route.query); // 查询参数
|
||||
console.log(route.params); // 路由参数
|
||||
```
|
||||
|
||||
### 3. 接收页面参数
|
||||
@@ -194,24 +194,24 @@ uni-mini-router提供了全局导航守卫功能,可以在路由跳转前后
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.log('路由跳转:', from.path, '->', to.path)
|
||||
console.log("路由跳转:", from.path, "->", to.path);
|
||||
|
||||
// 检查是否需要登录
|
||||
if (to.meta && to.meta.requireAuth) {
|
||||
// 检查登录状态
|
||||
const isLoggedIn = uni.getStorageSync('token')
|
||||
const isLoggedIn = uni.getStorageSync("token");
|
||||
|
||||
if (!isLoggedIn) {
|
||||
// 未登录,跳转到登录页
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
next('/pages/login/index')
|
||||
return
|
||||
uni.showToast({ title: "请先登录", icon: "none" });
|
||||
next("/pages/login/index");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 继续导航
|
||||
next()
|
||||
})
|
||||
next();
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 全局后置守卫
|
||||
@@ -219,10 +219,10 @@ router.beforeEach((to, from, next) => {
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
router.afterEach((to, from) => {
|
||||
console.log('路由跳转完成:', to.path)
|
||||
console.log("路由跳转完成:", to.path);
|
||||
|
||||
// 可以在这里做一些统计或记录
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 路由元数据配置
|
||||
@@ -255,45 +255,45 @@ router.afterEach((to, from) => {
|
||||
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
import { createRouter } from 'uni-mini-router'
|
||||
import { createRouter } from "uni-mini-router";
|
||||
|
||||
const router = createRouter({
|
||||
routes: generateRoutes()
|
||||
})
|
||||
routes: generateRoutes(),
|
||||
});
|
||||
|
||||
// 全局前置守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 检查页面是否需要登录
|
||||
if (to.meta && to.meta.requireAuth) {
|
||||
const token = uni.getStorageSync('token')
|
||||
const token = uni.getStorageSync("token");
|
||||
|
||||
if (!token) {
|
||||
// 显示登录提示
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '该功能需要登录后使用',
|
||||
confirmText: '去登录',
|
||||
cancelText: '返回',
|
||||
title: "提示",
|
||||
content: "该功能需要登录后使用",
|
||||
confirmText: "去登录",
|
||||
cancelText: "返回",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 记住原来要去的页面
|
||||
uni.setStorageSync('redirect', to.fullPath)
|
||||
next('/pages/login/index')
|
||||
uni.setStorageSync("redirect", to.fullPath);
|
||||
next("/pages/login/index");
|
||||
} else {
|
||||
// 取消则返回首页
|
||||
next('/pages/index/index')
|
||||
next("/pages/index/index");
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 继续导航
|
||||
next()
|
||||
})
|
||||
next();
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
```
|
||||
|
||||
### 2. 登录成功后跳转回原页面
|
||||
@@ -301,21 +301,21 @@ export default router
|
||||
```vue
|
||||
<!-- src/pages/login/index.vue -->
|
||||
<script setup>
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
function handleLogin() {
|
||||
// 模拟登录请求
|
||||
setTimeout(() => {
|
||||
// 登录成功,存储token
|
||||
uni.setStorageSync('token', 'user_token_example')
|
||||
uni.setStorageSync("token", "user_token_example");
|
||||
|
||||
// 获取之前要去的页面
|
||||
const redirect = uni.getStorageSync('redirect') || '/pages/index/index'
|
||||
uni.removeStorageSync('redirect')
|
||||
const redirect = uni.getStorageSync("redirect") || "/pages/index/index";
|
||||
uni.removeStorageSync("redirect");
|
||||
|
||||
// 跳转回原来的页面
|
||||
router.replaceAll(redirect)
|
||||
}, 1000)
|
||||
router.replaceAll(redirect);
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -336,25 +336,25 @@ function handleLogin() {
|
||||
|
||||
```typescript
|
||||
// 传递复杂对象
|
||||
const complexData = { name: 'product', details: { id: 1, features: ['a', 'b'] } }
|
||||
const complexData = { name: "product", details: { id: 1, features: ["a", "b"] } };
|
||||
|
||||
// 方法1: JSON序列化 + URL编码
|
||||
router.push({
|
||||
path: '/pages/detail/index',
|
||||
query: { data: encodeURIComponent(JSON.stringify(complexData)) }
|
||||
})
|
||||
path: "/pages/detail/index",
|
||||
query: { data: encodeURIComponent(JSON.stringify(complexData)) },
|
||||
});
|
||||
|
||||
// 接收页面
|
||||
onLoad((option) => {
|
||||
if (option.data) {
|
||||
try {
|
||||
const data = JSON.parse(decodeURIComponent(option.data))
|
||||
console.log(data)
|
||||
const data = JSON.parse(decodeURIComponent(option.data));
|
||||
console.log(data);
|
||||
} catch (e) {
|
||||
console.error('参数解析错误', e)
|
||||
console.error("参数解析错误", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 方法2: 对于非常大的数据,考虑使用全局状态管理或本地存储
|
||||
```
|
||||
@@ -391,12 +391,12 @@ uni-mini-router自动支持小程序的分包加载特性,可以在pages.json
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.log(`[Router] ${from.path || '初始页面'} -> ${to.path}`, {
|
||||
console.log(`[Router] ${from.path || "初始页面"} -> ${to.path}`, {
|
||||
params: to.params,
|
||||
query: to.query,
|
||||
})
|
||||
next()
|
||||
})
|
||||
});
|
||||
next();
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 常见问题解决
|
||||
|
||||
@@ -54,6 +54,9 @@ export default [
|
||||
ApiResponse: "readonly", // 统一响应数据类型
|
||||
creatorType: "readonly", // 创建人类型
|
||||
UploadFileResult: "readonly", // 上传文件返回类型
|
||||
Todo: "readonly", // 待办事项
|
||||
TodoState: "readonly", // 待办事项状态
|
||||
plus: true, // HTML5+ 运行时环境
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
"@uni-helper/uni-use": "^0.19.14",
|
||||
"@vueuse/core": "9.13.0",
|
||||
"pinia": "^2.2.2",
|
||||
"vue": "^3.4.21",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"wot-design-uni": "^1.9.1"
|
||||
},
|
||||
|
||||
@@ -41,6 +41,9 @@ export default defineUniPages({
|
||||
{
|
||||
pagePath: "pages/index/index",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/work/index",
|
||||
},
|
||||
{
|
||||
pagePath: "pages/mine/index",
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 3.0 MiB |
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useThemeStore } from "@/store";
|
||||
|
||||
const { initTheme } = useTheme();
|
||||
// 主题初始化
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
onLaunch(() => {
|
||||
// 初始化主题
|
||||
initTheme();
|
||||
themeStore.initTheme();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
@@ -18,4 +19,25 @@ onHide(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss">
|
||||
/* H5 环境样式变量设置 */
|
||||
:root {
|
||||
--primary-color: #165dff;
|
||||
--primary-color-light: #94bfff;
|
||||
--primary-color-dark: #0e3c9b;
|
||||
}
|
||||
|
||||
/* 小程序环境样式变量设置 */
|
||||
page {
|
||||
--primary-color: #165dff;
|
||||
--primary-color-light: #94bfff;
|
||||
--primary-color-dark: #0e3c9b;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
/* 动态加载小程序主题色的钩子 */
|
||||
/* 用于通过小程序原生API获取主题色并应用 */
|
||||
.theme-container {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,9 +13,8 @@ const AuthAPI = {
|
||||
return request<LoginResult>({
|
||||
url: `${AUTH_BASE_URL}/login`,
|
||||
method: "POST",
|
||||
headers: { [ApiHeader.KEY]: ApiHeader.FORM },
|
||||
header: { [ApiHeader.KEY]: ApiHeader.FORM },
|
||||
data: body,
|
||||
skipAuth: true,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -40,7 +39,6 @@ const AuthAPI = {
|
||||
return request<CaptchaInfo>({
|
||||
url: `${AUTH_BASE_URL}/captcha/get`,
|
||||
method: "GET",
|
||||
skipAuth: true,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -56,45 +54,10 @@ const AuthAPI = {
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 微信小程序手机号授权登录
|
||||
* @param data 包含code、encryptedData、iv等手机号相关数据
|
||||
* @returns 登录结果
|
||||
*/
|
||||
loginByWxMiniAppPhone(data: WxLoginData): Promise<LoginResult> {
|
||||
return request<LoginResult>({
|
||||
url: `${AUTH_BASE_URL}/wx/miniapp/phone-login`,
|
||||
method: "POST",
|
||||
data,
|
||||
skipAuth: true,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 微信小程序授权登录 (仅使用code获取OpenID)
|
||||
* @param code 微信登录凭证
|
||||
* @returns 登录结果
|
||||
*/
|
||||
loginByWxMiniAppCode(code: string): Promise<LoginResult> {
|
||||
return request<LoginResult>({
|
||||
url: `${AUTH_BASE_URL}/wx/miniapp/code-login`,
|
||||
method: "POST",
|
||||
data: { code },
|
||||
skipAuth: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default AuthAPI;
|
||||
|
||||
export interface WxLoginData {
|
||||
code: string;
|
||||
encryptedData?: string;
|
||||
iv?: string;
|
||||
phoneCode?: string;
|
||||
}
|
||||
|
||||
/** 登录表单数据 */
|
||||
export interface LoginFormData {
|
||||
username: string;
|
||||
@@ -102,6 +65,7 @@ export interface LoginFormData {
|
||||
captcha_key: string;
|
||||
captcha: string;
|
||||
remember: boolean;
|
||||
login_type: string;
|
||||
}
|
||||
|
||||
// 刷新令牌
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getToken } from "@/utils/storage";
|
||||
import { getAccessToken } from "@/utils/auth";
|
||||
import { ApiCode } from "@/enums/api-code.enum";
|
||||
|
||||
// H5 使用 VITE_APP_BASE_API 作为代理路径,其他平台使用 VITE_API_BASE_URL 作为请求路径
|
||||
@@ -25,12 +25,12 @@ const FileAPI = {
|
||||
filePath: filePath,
|
||||
name: "file",
|
||||
header: {
|
||||
Authorization: getToken() ? `Bearer ${getToken()}` : "",
|
||||
Authorization: getAccessToken() ? `Bearer ${getAccessToken()}` : "",
|
||||
},
|
||||
formData: {},
|
||||
success: (response) => {
|
||||
const resData = JSON.parse(response.data) as ApiResponse<UploadFileResult>;
|
||||
// 业务状态码 00000 表示成功
|
||||
// 业务状态码 0 表示成功
|
||||
if (resData.code === ApiCode.SUCCESS) {
|
||||
resolve(resData.data);
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from "@/utils/request";
|
||||
import { ApiHeader } from "@/enums/api-header.enum";
|
||||
|
||||
const USER_BASE_URL = "/system/user";
|
||||
|
||||
@@ -26,7 +27,7 @@ const UserAPI = {
|
||||
url: `${USER_BASE_URL}/current/avatar/upload`,
|
||||
method: "POST",
|
||||
data: body,
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
header: { [ApiHeader.KEY]: ApiHeader.MULTIPART },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -213,7 +214,7 @@ export interface UserInfo {
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
mobile?: string;
|
||||
gender?: number;
|
||||
gender?: string;
|
||||
password?: string;
|
||||
menus?: MenuTable[];
|
||||
dept?: deptTreeType;
|
||||
@@ -290,7 +291,7 @@ export interface positionSelectorType {
|
||||
export interface UserProfileForm {
|
||||
id?: number;
|
||||
name?: string;
|
||||
gender?: number;
|
||||
gender?: string;
|
||||
mobile?: string;
|
||||
email?: string;
|
||||
username?: string;
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,6 +0,0 @@
|
||||
# uCharts JSSDK说明
|
||||
|
||||
1、如不使用uCharts组件,可直接引用u-charts.js,打包编译后会`自动压缩`,压缩后体积约为`120kb`。
|
||||
2、如果120kb的体积仍需压缩,请手到uCharts官网通过在线定制选择您需要的图表。
|
||||
3、config-ucharts.js为uCharts组件的用户配置文件,升级前请`自行备份config-ucharts.js`文件,以免被强制覆盖。
|
||||
4、config-echarts.js为ECharts组件的用户配置文件,升级前请`自行备份config-echarts.js`文件,以免被强制覆盖。
|
||||
@@ -1,345 +1,48 @@
|
||||
import type { ConfigProviderThemeVars } from "wot-design-uni";
|
||||
import { useThemeStore } from "@/store/modules/theme.store";
|
||||
|
||||
// 定义主题色选项
|
||||
export interface ThemeColorOption {
|
||||
name: string;
|
||||
value: string;
|
||||
primary: string;
|
||||
}
|
||||
|
||||
// 预定义的主题色选项
|
||||
export const themeColorOptions: ThemeColorOption[] = [
|
||||
{ name: "默认蓝", value: "blue", primary: "#4D7FFF" },
|
||||
{ name: "活力橙", value: "orange", primary: "#FF7D00" },
|
||||
{ name: "薄荷绿", value: "green", primary: "#07C160" },
|
||||
{ name: "樱花粉", value: "pink", primary: "#FF69B4" },
|
||||
{ name: "紫罗兰", value: "purple", primary: "#8A2BE2" },
|
||||
{ name: "朱砂红", value: "red", primary: "#FF4757" },
|
||||
{ name: "天空蓝", value: "sky", primary: "#00BFFF" },
|
||||
{ name: "柠檬黄", value: "yellow", primary: "#FFD700" },
|
||||
];
|
||||
|
||||
// 扩展的颜色选项,用于自定义颜色选择器
|
||||
export const extendedColorOptions: ThemeColorOption[] = [
|
||||
...themeColorOptions,
|
||||
{ name: "珊瑚红", value: "coral", primary: "#FF6B6B" },
|
||||
{ name: "薄荷绿", value: "mint", primary: "#4ECDC4" },
|
||||
{ name: "黄金黄", value: "gold", primary: "#FFD166" },
|
||||
{ name: "经典红", value: "classic-red", primary: "#CD5C5C" },
|
||||
{ name: "自然绿", value: "nature-green", primary: "#228B22" },
|
||||
{ name: "天蓝色", value: "sky-blue", primary: "#1890FF" },
|
||||
{ name: "青绿色", value: "teal", primary: "#0FC6C2" },
|
||||
{ name: "深紫色", value: "deep-purple", primary: "#722ED1" },
|
||||
];
|
||||
|
||||
export interface ThemeState {
|
||||
theme: "light" | "dark";
|
||||
isDark: boolean;
|
||||
followSystem: boolean;
|
||||
hasUserSet: boolean;
|
||||
currentThemeColor: ThemeColorOption;
|
||||
showThemeColorSheet: boolean;
|
||||
}
|
||||
import { useThemeStore, ThemeMode, ThemeColorOption } from "@/store";
|
||||
|
||||
export function useTheme() {
|
||||
// 状态定义
|
||||
const theme = ref<"light" | "dark">("light");
|
||||
const followSystem = ref(true); // 是否跟随系统主题
|
||||
const hasUserSet = ref(false); // 用户是否手动设置过主题
|
||||
const currentThemeColor = ref<ThemeColorOption>(themeColorOptions[0]);
|
||||
const showThemeColorSheet = ref(false);
|
||||
const store = useThemeStore();
|
||||
|
||||
// 主题变量
|
||||
const themeVars = reactive<ConfigProviderThemeVars>({
|
||||
darkBackground: "#0f0f0f",
|
||||
darkBackground2: "#1a1a1a",
|
||||
darkBackground3: "#242424",
|
||||
darkBackground4: "#2f2f2f",
|
||||
darkBackground5: "#3d3d3d",
|
||||
darkBackground6: "#4a4a4a",
|
||||
darkBackground7: "#606060",
|
||||
darkColor: "#ffffff",
|
||||
darkColor2: "#e0e0e0",
|
||||
darkColor3: "#a0a0a0",
|
||||
colorTheme: themeColorOptions[0].primary,
|
||||
});
|
||||
|
||||
// 计算属性
|
||||
const isDark = computed(() => theme.value === "dark");
|
||||
|
||||
// 主题状态
|
||||
const themeState = computed(() => ({
|
||||
theme: theme.value,
|
||||
isDark: isDark.value,
|
||||
followSystem: followSystem.value,
|
||||
hasUserSet: hasUserSet.value,
|
||||
currentThemeColor: currentThemeColor.value,
|
||||
showThemeColorSheet: showThemeColorSheet.value,
|
||||
}));
|
||||
|
||||
/* 手动切换主题 */
|
||||
function toggleTheme(mode?: "light" | "dark") {
|
||||
theme.value = mode || (theme.value === "light" ? "dark" : "light");
|
||||
hasUserSet.value = true;
|
||||
followSystem.value = false;
|
||||
setNavigationBarColor();
|
||||
saveThemeSettings();
|
||||
|
||||
// 实时应用主题变化
|
||||
applyThemeModeToApp();
|
||||
/**
|
||||
* 切换暗黑模式
|
||||
* @param mode 指定主题模式,不传则自动切换
|
||||
*/
|
||||
function toggleTheme(mode?: ThemeMode) {
|
||||
store.toggleTheme(mode);
|
||||
}
|
||||
|
||||
/* 实时应用主题模式到应用 */
|
||||
function applyThemeModeToApp() {
|
||||
// 更新CSS变量
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.setAttribute("data-theme", theme.value);
|
||||
/**
|
||||
* 设置主题色
|
||||
* @param option 主题色选项
|
||||
*/
|
||||
function setThemeColor(option: ThemeColorOption) {
|
||||
store.setCurrentThemeColor(option);
|
||||
}
|
||||
|
||||
// 更新导航栏颜色
|
||||
setNavigationBarColor();
|
||||
}
|
||||
|
||||
/* 设置是否跟随系统主题 */
|
||||
function setFollowSystem(follow: boolean) {
|
||||
followSystem.value = follow;
|
||||
if (follow) {
|
||||
hasUserSet.value = false;
|
||||
initTheme();
|
||||
}
|
||||
saveThemeSettings();
|
||||
|
||||
// 实时应用主题变化
|
||||
applyThemeModeToApp();
|
||||
}
|
||||
|
||||
/* 设置主题色 */
|
||||
function setCurrentThemeColor(color: ThemeColorOption) {
|
||||
currentThemeColor.value = color;
|
||||
themeVars.colorTheme = color.primary;
|
||||
hasUserSet.value = true;
|
||||
saveThemeSettings();
|
||||
|
||||
// 实时应用主题色到全局
|
||||
applyThemeColorToApp(color.primary);
|
||||
}
|
||||
|
||||
/* 设置自定义主题色 */
|
||||
function setCustomThemeColor(color: string) {
|
||||
const customTheme: ThemeColorOption = {
|
||||
name: "自定义",
|
||||
value: "custom",
|
||||
primary: color,
|
||||
};
|
||||
setCurrentThemeColor(customTheme);
|
||||
}
|
||||
|
||||
/* 重置主题 */
|
||||
function resetTheme() {
|
||||
setCurrentThemeColor(themeColorOptions[0]);
|
||||
theme.value = "light";
|
||||
followSystem.value = true;
|
||||
hasUserSet.value = false;
|
||||
initTheme();
|
||||
}
|
||||
|
||||
/* 保存主题设置到本地存储 */
|
||||
function saveThemeSettings() {
|
||||
try {
|
||||
uni.setStorageSync("theme_settings", {
|
||||
theme: theme.value,
|
||||
currentThemeColor: currentThemeColor.value,
|
||||
followSystem: followSystem.value,
|
||||
hasUserSet: hasUserSet.value,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("保存主题设置失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/* 从本地存储加载主题设置 */
|
||||
function loadThemeSettings() {
|
||||
try {
|
||||
const settings = uni.getStorageSync("theme_settings");
|
||||
if (settings) {
|
||||
if (settings.theme && (settings.theme === "light" || settings.theme === "dark")) {
|
||||
theme.value = settings.theme;
|
||||
}
|
||||
if (settings.currentThemeColor) {
|
||||
const savedColor =
|
||||
themeColorOptions.find((c) => c.value === settings.currentThemeColor.value) ||
|
||||
extendedColorOptions.find((c) => c.value === settings.currentThemeColor.value) ||
|
||||
settings.currentThemeColor;
|
||||
if (savedColor) {
|
||||
currentThemeColor.value = savedColor;
|
||||
themeVars.colorTheme = savedColor.primary;
|
||||
}
|
||||
}
|
||||
followSystem.value = settings.followSystem !== false;
|
||||
hasUserSet.value = settings.hasUserSet === true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("加载主题设置失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/* 获取系统主题 */
|
||||
function getSystemTheme(): "light" | "dark" {
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
const appBaseInfo = uni.getAppBaseInfo();
|
||||
if (appBaseInfo?.theme) {
|
||||
return appBaseInfo.theme as "light" | "dark";
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
if (systemInfo?.theme) {
|
||||
return systemInfo.theme as "light" | "dark";
|
||||
}
|
||||
// #endif
|
||||
} catch (error) {
|
||||
console.warn("获取系统主题失败:", error);
|
||||
}
|
||||
return "light";
|
||||
}
|
||||
|
||||
/* 设置导航栏颜色 */
|
||||
function setNavigationBarColor() {
|
||||
// #ifndef H5
|
||||
uni.setNavigationBarColor({
|
||||
frontColor: theme.value === "light" ? "#000000" : "#ffffff",
|
||||
backgroundColor: theme.value === "light" ? "#ffffff" : "#000000",
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
|
||||
/* 实时应用主题色到应用 */
|
||||
function applyThemeColorToApp(color: string) {
|
||||
// 更新Wot Design组件库主题色
|
||||
themeVars.colorTheme = color;
|
||||
|
||||
// 更新CSS变量
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.style.setProperty("--wot-color-theme", color);
|
||||
document.documentElement.style.setProperty("--primary-color", color);
|
||||
document.documentElement.style.setProperty("--primary-color-light", color + "20");
|
||||
document.documentElement.style.setProperty("--primary-color-dark", color);
|
||||
}
|
||||
|
||||
// 同步到主题存储
|
||||
const themeStore = useThemeStore();
|
||||
themeStore.setPrimaryColor(color);
|
||||
|
||||
// 通过事件总线通知全局主题变化
|
||||
uni.$emit("theme-color-changed", color);
|
||||
|
||||
// 强制刷新页面样式
|
||||
setTimeout(() => {
|
||||
uni.$emit("force-theme-refresh");
|
||||
}, 50);
|
||||
}
|
||||
|
||||
/* 初始化主题 */
|
||||
/**
|
||||
* 初始化主题
|
||||
*/
|
||||
function initTheme() {
|
||||
if (hasUserSet.value && !followSystem.value) {
|
||||
setNavigationBarColor();
|
||||
applyThemeColorToApp(currentThemeColor.value.primary);
|
||||
return;
|
||||
store.initTheme();
|
||||
}
|
||||
|
||||
const systemTheme = getSystemTheme();
|
||||
if (!hasUserSet.value || followSystem.value) {
|
||||
theme.value = systemTheme;
|
||||
}
|
||||
|
||||
setNavigationBarColor();
|
||||
applyThemeColorToApp(currentThemeColor.value.primary);
|
||||
}
|
||||
|
||||
/* 主题色选择器相关 */
|
||||
function openThemeColorPicker() {
|
||||
showThemeColorSheet.value = true;
|
||||
}
|
||||
|
||||
function closeThemeColorPicker() {
|
||||
showThemeColorSheet.value = false;
|
||||
}
|
||||
|
||||
function selectThemeColor(option: ThemeColorOption) {
|
||||
setCurrentThemeColor(option);
|
||||
closeThemeColorPicker();
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onBeforeMount(() => {
|
||||
loadThemeSettings();
|
||||
initTheme();
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
if (uni.onThemeChange) {
|
||||
uni.onThemeChange((res: any) => {
|
||||
if (followSystem.value) {
|
||||
theme.value = res.theme;
|
||||
setNavigationBarColor();
|
||||
}
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
if (uni.onThemeChange) {
|
||||
uni.onThemeChange((res: any) => {
|
||||
if (followSystem.value) {
|
||||
theme.value = res.theme;
|
||||
setNavigationBarColor();
|
||||
}
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理监听器
|
||||
if (uni.offThemeChange) {
|
||||
uni.offThemeChange(() => {});
|
||||
}
|
||||
});
|
||||
// 注意:全局主题管理已在App.vue中处理
|
||||
// 包括:系统主题监听、导航栏颜色同步等
|
||||
// 组件中一般不需要再调用initTheme(),除非有特殊需求
|
||||
|
||||
return {
|
||||
// 状态
|
||||
theme: computed(() => theme.value),
|
||||
isDark,
|
||||
followSystem: computed(() => followSystem.value),
|
||||
hasUserSet: computed(() => hasUserSet.value),
|
||||
currentThemeColor: computed(() => currentThemeColor.value),
|
||||
showThemeColorSheet: computed(() => showThemeColorSheet.value),
|
||||
themeState,
|
||||
theme: computed(() => store.theme),
|
||||
isDark: computed(() => store.isDark),
|
||||
currentThemeColor: computed(() => store.currentThemeColor),
|
||||
themeVars: computed(() => store.themeVars),
|
||||
|
||||
// 主题选项
|
||||
themeColorOptions,
|
||||
extendedColorOptions,
|
||||
|
||||
// 主题变量
|
||||
themeVars,
|
||||
themeColorOptions: computed(() => store.themeColorOptions),
|
||||
|
||||
// 方法
|
||||
toggleTheme,
|
||||
setFollowSystem,
|
||||
setCurrentThemeColor,
|
||||
setCustomThemeColor,
|
||||
resetTheme,
|
||||
initTheme,
|
||||
|
||||
// 主题色选择器
|
||||
openThemeColorPicker,
|
||||
closeThemeColorPicker,
|
||||
selectThemeColor,
|
||||
|
||||
// 工具方法
|
||||
saveThemeSettings,
|
||||
loadThemeSettings,
|
||||
toggleTheme,
|
||||
setThemeColor,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
/**
|
||||
* 微信授权服务
|
||||
* 处理微信登录授权、获取用户信息等功能
|
||||
*/
|
||||
|
||||
import { ref } from "vue";
|
||||
import { getAccessToken } from "@/utils/auth";
|
||||
|
||||
export function useWechat() {
|
||||
// 定义微信授权状态
|
||||
const authState = ref({
|
||||
isLogining: false,
|
||||
authDenied: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取微信登录凭证code
|
||||
* @returns Promise 返回登录凭证code
|
||||
*/
|
||||
const getLoginCode = (): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.login({
|
||||
provider: "weixin",
|
||||
success: (res) => {
|
||||
if (res.code) {
|
||||
resolve(res.code);
|
||||
} else {
|
||||
reject(new Error("获取微信登录凭证失败"));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
reject(new Error("当前环境不支持微信登录"));
|
||||
// #endif
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取微信用户手机号
|
||||
* @param e 微信授权返回的事件对象
|
||||
* @returns Promise 返回包含手机号加密数据的对象
|
||||
*/
|
||||
const getPhoneNumber = (
|
||||
e: any
|
||||
): Promise<{ code: string; encryptedData?: string; iv?: string }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
authState.value.isLogining = true;
|
||||
|
||||
// 判断授权是否成功
|
||||
if (e.detail.errMsg !== "getPhoneNumber:ok") {
|
||||
authState.value.isLogining = false;
|
||||
authState.value.authDenied = true;
|
||||
reject(new Error("用户拒绝授权"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取登录凭证code
|
||||
getLoginCode()
|
||||
.then((code) => {
|
||||
// 在微信小程序环境下,可以获取encryptedData和iv
|
||||
// #ifdef MP-WEIXIN
|
||||
resolve({
|
||||
code,
|
||||
encryptedData: e.detail.encryptedData,
|
||||
iv: e.detail.iv,
|
||||
});
|
||||
// #endif
|
||||
|
||||
// 其他环境或新版本接口
|
||||
// #ifndef MP-WEIXIN
|
||||
resolve({
|
||||
code,
|
||||
// 新版本接口在e.detail.code中包含手机号获取凭证
|
||||
...(e.detail.code ? { phoneCode: e.detail.code } : {}),
|
||||
});
|
||||
// #endif
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
})
|
||||
.finally(() => {
|
||||
authState.value.isLogining = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查会话有效性
|
||||
* @returns Promise 返回会话是否有效
|
||||
*/
|
||||
const checkSession = (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const token = getAccessToken();
|
||||
|
||||
if (!token) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用后端接口验证token有效性
|
||||
uni.request({
|
||||
url: "/api/v1/auth/check-session",
|
||||
method: "GET",
|
||||
header: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
success: (res: any) => {
|
||||
if (res.statusCode === 200 && res.data.valid) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户头像昵称
|
||||
* 注意:此接口已于2021年弃用,仅作为兼容保留
|
||||
* 推荐使用button组件的open-type="chooseAvatar"让用户选择头像
|
||||
*/
|
||||
const getUserProfile = (): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.getUserProfile({
|
||||
desc: "用于完善用户资料",
|
||||
success: (res) => {
|
||||
resolve(res.userInfo);
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
reject(new Error("当前环境不支持获取用户信息"));
|
||||
// #endif
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
authState,
|
||||
getLoginCode,
|
||||
getPhoneNumber,
|
||||
checkSession,
|
||||
getUserProfile,
|
||||
};
|
||||
}
|
||||
@@ -3,9 +3,14 @@
|
||||
* 包括 localStorage、sessionStorage 等各种存储的键名
|
||||
*/
|
||||
|
||||
// 🔐 用户认证相关
|
||||
export const ACCESS_TOKEN_KEY = "access_token";
|
||||
export const REFRESH_TOKEN_KEY = "refresh_token";
|
||||
// 访问token
|
||||
export const APP_ACCESS_TOKEN_KEY = "appAccessToken";
|
||||
|
||||
// 📊 用户缓存相关
|
||||
export const USER_INFO_KEY = "user_info";
|
||||
// 刷新token
|
||||
export const APP_REFRESH_TOKEN_KEY = "appRefreshToken";
|
||||
|
||||
// 用户缓存相关
|
||||
export const APP_USER_INFO = "appUserInfo";
|
||||
|
||||
// 全局配置相关
|
||||
export const APP_THEME_KEY = "appTheme";
|
||||
|
||||
@@ -20,12 +20,12 @@ export const enum ApiCode {
|
||||
/**
|
||||
* 未授权访问
|
||||
*/
|
||||
UNAUTHORIZED = 401,
|
||||
UNAUTHORIZED = 10403,
|
||||
|
||||
/**
|
||||
* 令牌已过期
|
||||
*/
|
||||
TOKEN_EXPIRED = 403,
|
||||
TOKEN_EXPIRED = 10401,
|
||||
|
||||
/**
|
||||
* 参数校验失败
|
||||
|
||||
@@ -15,8 +15,11 @@
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules": {},
|
||||
/* 应用发布信息 */
|
||||
"distribute": {
|
||||
/* android打包配置 */
|
||||
"android": {
|
||||
"permissions": [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
@@ -36,11 +39,15 @@
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios": {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs": {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp": {},
|
||||
/* 小程序特有相关:"appid": "wx99a151dc43d2637b" */
|
||||
"mp-weixin": {
|
||||
"appid": "微信开发者ID",
|
||||
"setting": {
|
||||
@@ -50,12 +57,15 @@
|
||||
"darkmode": true,
|
||||
"themeLocation": "theme.json"
|
||||
},
|
||||
/* 抖音小程序特有相关 */
|
||||
"mp-alipay": {
|
||||
"usingComponents": true
|
||||
},
|
||||
/* 百度小程序特有相关 */
|
||||
"mp-baidu": {
|
||||
"usingComponents": true
|
||||
},
|
||||
/* 字节跳动小程序特有相关 */
|
||||
"mp-toutiao": {
|
||||
"usingComponents": true
|
||||
},
|
||||
|
||||
@@ -25,25 +25,38 @@
|
||||
{
|
||||
"path": "pages/work/index",
|
||||
"type": "page",
|
||||
"name": "index",
|
||||
"name": "work",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作台"
|
||||
"navigationStyle": "custom"
|
||||
},
|
||||
"layout": "tabbar",
|
||||
"meta": {
|
||||
"requireAuth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/about/index",
|
||||
"type": "page"
|
||||
"type": "page",
|
||||
"name": "about",
|
||||
"style": {
|
||||
"navigationBarTitleText": "关于我们"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/faq/index",
|
||||
"type": "page"
|
||||
"type": "page",
|
||||
"name": "faq",
|
||||
"style": {
|
||||
"navigationBarTitleText": "常见问题"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/feedback/index",
|
||||
"type": "page"
|
||||
"type": "page",
|
||||
"name": "feedback",
|
||||
"style": {
|
||||
"navigationBarTitleText": "问题反馈"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/profile/complete-profile",
|
||||
@@ -55,23 +68,54 @@
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/index",
|
||||
"type": "page"
|
||||
"type": "page",
|
||||
"name": "settings",
|
||||
"style": {
|
||||
"navigationBarTitleText": "设置"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/account/index",
|
||||
"type": "page"
|
||||
"type": "page",
|
||||
"name": "account",
|
||||
"style": {
|
||||
"navigationBarTitleText": "账号和安全"
|
||||
},
|
||||
"layout": "tabbar"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/agreement/index",
|
||||
"type": "page"
|
||||
"type": "page",
|
||||
"name": "agreement",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户协议"
|
||||
},
|
||||
"layout": "tabbar"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/network/index",
|
||||
"type": "page"
|
||||
"type": "page",
|
||||
"name": "network",
|
||||
"style": {
|
||||
"navigationBarTitleText": "网络测试"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/privacy/index",
|
||||
"type": "page",
|
||||
"name": "privacy",
|
||||
"style": {
|
||||
"navigationBarTitleText": "隐私政策"
|
||||
},
|
||||
"layout": "tabbar"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/theme/index",
|
||||
"type": "page"
|
||||
"type": "page",
|
||||
"name": "theme",
|
||||
"style": {
|
||||
"navigationBarTitleText": "主题设置"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
@@ -100,6 +144,9 @@
|
||||
{
|
||||
"pagePath": "pages/index/index"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/work/index"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/index"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<view style="width: 100%; height: var(--status-bar-height)" />
|
||||
<view class="app-container">
|
||||
<wd-swiper
|
||||
v-model:current="current"
|
||||
@@ -14,12 +15,13 @@
|
||||
v-for="(item, index) in navList"
|
||||
:key="index"
|
||||
use-slot
|
||||
@click="handleNavClick(item)"
|
||||
link-type="navigateTo"
|
||||
:url="item.url"
|
||||
>
|
||||
<view class="p-2">
|
||||
<image class="w-72rpx h-72rpx rounded-8rpx" :src="item.icon" />
|
||||
</view>
|
||||
<view class="text-sm text-center">{{ item.title }}</view>
|
||||
<view class="text">{{ item.title }}</view>
|
||||
</wd-grid-item>
|
||||
</wd-grid>
|
||||
|
||||
@@ -38,9 +40,9 @@
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<wd-grid :column="2" :gutter="2">
|
||||
<wd-grid-item use-slot custom-class="h-80px">
|
||||
<wd-grid-item use-slot custom-class="custom-item">
|
||||
<view class="flex justify-start pl-5">
|
||||
<view class="flex items-center">
|
||||
<view class="flex-center">
|
||||
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/visitor.png" />
|
||||
<view class="ml-5 text-left">
|
||||
<view class="font-bold">访客数</view>
|
||||
@@ -49,9 +51,9 @@
|
||||
</view>
|
||||
</view>
|
||||
</wd-grid-item>
|
||||
<wd-grid-item use-slot custom-class="h-80px">
|
||||
<wd-grid-item use-slot custom-class="custom-item">
|
||||
<view class="flex justify-start pl-5">
|
||||
<view class="flex items-center">
|
||||
<view class="flex-center">
|
||||
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/browser.png" />
|
||||
<view class="ml-5 text-left">
|
||||
<view class="font-bold">浏览量</view>
|
||||
@@ -64,11 +66,11 @@
|
||||
|
||||
<wd-card>
|
||||
<template #title>
|
||||
<view class="flex justify-between items-center">
|
||||
<view class="flex-between">
|
||||
<view>访问趋势</view>
|
||||
<view>
|
||||
<wd-radio-group
|
||||
v-model="recentDaysRange"
|
||||
:value="recentDaysRange"
|
||||
shape="button"
|
||||
inline
|
||||
@change="handleDataRangeChange"
|
||||
@@ -80,7 +82,7 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="w-full h-240px mb-40rpx">
|
||||
<view class="w-full h-360px mb-40rpx">
|
||||
<qiun-data-charts type="area" :chartData="chartData" :opts="chartOpts" />
|
||||
</view>
|
||||
</wd-card>
|
||||
@@ -89,7 +91,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dayjs } from "wot-design-uni";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
// 定义访问统计数据类型
|
||||
interface VisitStatsVO {
|
||||
@@ -101,7 +102,6 @@ interface VisitStatsVO {
|
||||
totalPvCount: number;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
const current = ref<number>(0);
|
||||
|
||||
const visitStatsData = ref<VisitStatsVO>({
|
||||
@@ -148,35 +148,29 @@ const navList = reactive([
|
||||
{
|
||||
icon: "/static/icons/user.png",
|
||||
title: "用户管理",
|
||||
url: "/pages/work/index",
|
||||
url: "/pages/work/user/index",
|
||||
prem: "sys:user:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/role.png",
|
||||
title: "角色管理",
|
||||
url: "/pages/work/index",
|
||||
url: "/pages/work/role/index",
|
||||
prem: "sys:role:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/notice.png",
|
||||
title: "通知公告",
|
||||
url: "/pages/work/index",
|
||||
url: "/pages/work/notice/index",
|
||||
prem: "sys:notice:query",
|
||||
},
|
||||
{
|
||||
icon: "/static/icons/setting.png",
|
||||
title: "系统配置",
|
||||
url: "/pages/work/index",
|
||||
url: "/pages/work/config/index",
|
||||
prem: "sys:config:query",
|
||||
},
|
||||
]);
|
||||
|
||||
// 处理导航点击
|
||||
function handleNavClick(item: any) {
|
||||
// 使用路由系统进行导航,这样会触发路由守卫
|
||||
router.push({ path: item.url });
|
||||
}
|
||||
|
||||
// 生成静态的访问趋势数据
|
||||
const generateStaticTrendData = (days: number) => {
|
||||
const dates = [];
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<!-- Logo和标题区域 -->
|
||||
<view class="header">
|
||||
<image src="/static/logo.png" class="logo" mode="aspectFit" />
|
||||
<wd-img src="/static/logo.png" class="logo" mode="aspectFit" />
|
||||
<text class="title">FastApp管理系统</text>
|
||||
<text class="subtitle">欢迎使用移动端管理平台</text>
|
||||
</view>
|
||||
@@ -28,13 +28,13 @@
|
||||
prop="username"
|
||||
class="form-input input-transparent"
|
||||
placeholder="请输入用户名"
|
||||
clear-trigger="focus"
|
||||
placeholder-class="input-placeholder"
|
||||
clearable
|
||||
:rules="[{ required: true, message: '请输入用户名', trigger: 'blur' }]"
|
||||
clear-trigger="focus"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- <view class="divider"></view> -->
|
||||
<!-- <wd-divider /> -->
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<view class="form-item">
|
||||
@@ -57,7 +57,7 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- <view class="divider"></view> -->
|
||||
<!-- <wd-divider /> -->
|
||||
|
||||
<!-- 验证码输入框 -->
|
||||
<view v-if="captchaState.enable" class="form-item">
|
||||
@@ -91,7 +91,7 @@
|
||||
<text>点击加载</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- <view class="divider"></view> -->
|
||||
<!-- <wd-divider /> -->
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<view class="footer">
|
||||
@@ -121,15 +121,16 @@
|
||||
<view class="phone-login-title">微信一键登录</view>
|
||||
<view class="phone-login-subtitle">授权后将获取您的手机号</view>
|
||||
|
||||
<button
|
||||
<wd-button
|
||||
class="wechat-phone-btn"
|
||||
:disabled="loading"
|
||||
open-type="getPhoneNumber"
|
||||
size="large"
|
||||
@getphonenumber="handleWechatPhoneLogin"
|
||||
>
|
||||
<wd-icon name="weixin" size="24" color="#ffffff" />
|
||||
<text>微信一键登录</text>
|
||||
</button>
|
||||
</wd-button>
|
||||
|
||||
<!-- 切换登录方式 -->
|
||||
<view class="switch-login-type" @click="loginType = 'account'">
|
||||
@@ -148,17 +149,19 @@
|
||||
|
||||
<view class="wechat-login" @click="handleWechatLogin">
|
||||
<view class="wechat-icon-wrapper">
|
||||
<image src="/static/icons/weixin.png" class="wechat-icon" />
|
||||
<wd-img src="/static/icons/weixin.png" class="wechat-icon" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部协议 -->
|
||||
<view class="agreement">
|
||||
<text class="text">登录即同意</text>
|
||||
<text class="link" @click="navigateToUserAgreement">《用户协议》</text>
|
||||
<text class="text">和</text>
|
||||
<text class="link" @click="navigateToPrivacy">《隐私政策》</text>
|
||||
<wd-checkbox v-model="loginFormData.remember">
|
||||
<wd-text text="我已阅读并同意"></wd-text>
|
||||
<wd-text type="primary" text="《用户协议》" @click="navigateToUserAgreement"></wd-text>
|
||||
<wd-text text="和"></wd-text>
|
||||
<wd-text type="primary" text="《隐私政策》" @click="navigateToPrivacy"></wd-text>
|
||||
</wd-checkbox>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -168,18 +171,17 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from "@/store/modules/user.store";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useUserStore } from "@/store";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { useWechat } from "@/composables/useWechat";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { computed, ref, reactive, watch } from "vue";
|
||||
import AuthAPI, { type LoginFormData, type CaptchaInfo } from "@/api/auth";
|
||||
|
||||
const loginFormRef = ref();
|
||||
const toast = useToast();
|
||||
const loading = ref(false);
|
||||
const userStore = useUserStore();
|
||||
const loginType = ref<"account" | "phone">("account");
|
||||
const { authState, getLoginCode, getPhoneNumber } = useWechat();
|
||||
const { theme } = useTheme();
|
||||
|
||||
// 登录表单数据
|
||||
@@ -189,6 +191,7 @@ const loginFormData = ref<LoginFormData>({
|
||||
captcha: "",
|
||||
captcha_key: "",
|
||||
remember: true,
|
||||
login_type: "移动端",
|
||||
});
|
||||
|
||||
// 验证码状态
|
||||
@@ -201,9 +204,6 @@ const captchaState = reactive<CaptchaInfo>({
|
||||
// 防重复请求标志
|
||||
const isCaptchaLoading = ref(false);
|
||||
|
||||
// 使用store的loading状态
|
||||
const loading = computed(() => userStore.isLoggingIn || authState.value.isLogining);
|
||||
|
||||
// 表单验证
|
||||
const isFormValid = computed(() => {
|
||||
const { username, password, captcha } = loginFormData.value;
|
||||
@@ -237,25 +237,6 @@ const getLoginCaptcha = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 统一的错误处理
|
||||
const handleLoginError = (error: any, loginType: string) => {
|
||||
const message = error?.message || `${loginType}登录失败`;
|
||||
|
||||
// 根据错误类型显示不同提示
|
||||
if (message.includes("验证码")) {
|
||||
toast.error("验证码错误,请重新输入");
|
||||
getLoginCaptcha(); // 刷新验证码
|
||||
} else if (message.includes("用户不存在") || message.includes("密码错误")) {
|
||||
toast.error("用户名或密码错误");
|
||||
} else if (message.includes("拒绝授权")) {
|
||||
toast.error("您已拒绝授权");
|
||||
} else {
|
||||
toast.error(message);
|
||||
}
|
||||
|
||||
console.error(`${loginType}登录失败:`, error);
|
||||
};
|
||||
|
||||
// 账号密码登录
|
||||
const handleAccountLogin = async () => {
|
||||
if (!isFormValid.value) {
|
||||
@@ -272,67 +253,33 @@ const handleAccountLogin = async () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
await userStore.login(loginFormData.value);
|
||||
toast.success("登录成功");
|
||||
|
||||
// 登录成功后跳转到mine页面,确保用户信息及时更新
|
||||
uni.switchTab({ url: "/pages/mine/index" });
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: redirect.value });
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
handleLoginError(error, "账号密码");
|
||||
toast.error(error?.message || "登录失败");
|
||||
getLoginCaptcha(); // 刷新验证码
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 微信一键登录(通过手机号)
|
||||
const handleWechatPhoneLogin = async (e: any) => {
|
||||
if (!e.detail?.encryptedData) {
|
||||
toast.error("获取手机号失败");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const phoneData = await getPhoneNumber(e);
|
||||
await userStore.loginWithWxPhone(phoneData);
|
||||
toast.success("登录成功");
|
||||
|
||||
// 登录成功后跳转到mine页面,确保用户信息及时更新
|
||||
uni.switchTab({ url: "/pages/mine/index" });
|
||||
} catch (error: any) {
|
||||
handleLoginError(error, "微信手机号");
|
||||
}
|
||||
const handleWechatPhoneLogin = async () => {
|
||||
toast.error("微信一键登录功能开发中...");
|
||||
};
|
||||
|
||||
// 微信授权登录
|
||||
const handleWechatLogin = async () => {
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
const code = await getLoginCode();
|
||||
await userStore.loginWithWxCode(code);
|
||||
toast.success("登录成功");
|
||||
|
||||
// 登录成功后跳转到mine页面,确保用户信息及时更新
|
||||
uni.switchTab({ url: "/pages/mine/index" });
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
toast.error("当前环境不支持微信登录");
|
||||
// #endif
|
||||
} catch (error: any) {
|
||||
handleLoginError(error, "微信授权");
|
||||
}
|
||||
toast.error("微信授权登录功能开发中...");
|
||||
};
|
||||
|
||||
// 监听验证码状态变化
|
||||
watch(
|
||||
() => captchaState.enable,
|
||||
(newVal) => {
|
||||
if (newVal && !captchaState.img_base) {
|
||||
getLoginCaptcha();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 是否暗黑模式
|
||||
const isDarkMode = computed(() => theme.value === "dark");
|
||||
|
||||
@@ -345,10 +292,26 @@ const navigateToPrivacy = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/settings/privacy/index" });
|
||||
};
|
||||
|
||||
// 页面加载时获取验证码
|
||||
onLoad(() => {
|
||||
// 强制清除输入框背景色
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
const inputs = document.querySelectorAll("input");
|
||||
inputs.forEach((input) => {
|
||||
input.style.backgroundColor = "transparent";
|
||||
input.style.boxShadow = "none";
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// 仅使用 query 作为重定向来源
|
||||
const redirect = ref("/pages/index/index");
|
||||
onLoad((options) => {
|
||||
uni.setNavigationBarTitle({ title: "登录" });
|
||||
getLoginCaptcha();
|
||||
const fromQuery = options && options.redirect ? decodeURIComponent(options.redirect) : "";
|
||||
if (fromQuery && fromQuery !== "/pages/login/index") {
|
||||
redirect.value = fromQuery;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -359,6 +322,7 @@ onLoad(() => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--wot-color-bg-container);
|
||||
}
|
||||
@@ -367,35 +331,10 @@ onLoad(() => {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
}
|
||||
.header {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
@@ -429,7 +368,7 @@ onLoad(() => {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 95%;
|
||||
width: 90%;
|
||||
margin-top: 80rpx;
|
||||
overflow: hidden;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
@@ -504,8 +443,6 @@ input:-webkit-autofill:active {
|
||||
.form-item input,
|
||||
input.form-input,
|
||||
input.input-transparent {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: none !important;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
@@ -712,14 +649,4 @@ input.input-transparent {
|
||||
background-image: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 修复Android Chrome输入框背景色问题 */
|
||||
@supports (-webkit-appearance: none) {
|
||||
input {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,393 +1,179 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<view class="app-container dark:text-[var(--wot-dark-color)]">
|
||||
<wd-navbar
|
||||
title="关于我们"
|
||||
left-arrow
|
||||
safe-area-inset-top
|
||||
placeholder
|
||||
safe-area-inset-top
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 顶部品牌区域 -->
|
||||
<view class="brand-section">
|
||||
<view class="brand-content">
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<view class="about-header">
|
||||
<wd-img
|
||||
:src="webLogo"
|
||||
:width="120"
|
||||
:height="120"
|
||||
round
|
||||
custom-class="brand-logo"
|
||||
width="120rpx"
|
||||
height="120rpx"
|
||||
src="/static/logo.png"
|
||||
mode="aspectFit"
|
||||
class="about-logo"
|
||||
/>
|
||||
<view class="brand-info">
|
||||
<view class="brand-title-wrapper">
|
||||
<text class="brand-title">{{ webTitle }}</text>
|
||||
<wd-tag type="primary" size="small" plain custom-class="version-tag">
|
||||
v{{ version }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
<text class="brand-subtitle">{{ webDescription }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 技术栈 -->
|
||||
<view class="section">
|
||||
<wd-card title="技术栈" custom-style="margin: 20rpx;">
|
||||
<view class="tech-stack-container">
|
||||
<view class="tech-category">
|
||||
<view class="category-title">
|
||||
<wd-icon name="computer" size="24" color="#409eff" />
|
||||
<text class="category-text">前端技术</text>
|
||||
</view>
|
||||
<wd-grid :column="4" clickable custom-class="tech-grid">
|
||||
<wd-grid-item
|
||||
v-for="tech in frontendTechs"
|
||||
:key="tech.name"
|
||||
:icon="tech.icon"
|
||||
:text="tech.name"
|
||||
/>
|
||||
</wd-grid>
|
||||
</view>
|
||||
|
||||
<view class="tech-category">
|
||||
<view class="category-title">
|
||||
<wd-icon name="server" size="24" color="#52c41a" />
|
||||
<text class="category-text">后端技术</text>
|
||||
</view>
|
||||
<wd-grid :column="4" clickable custom-class="tech-grid">
|
||||
<wd-grid-item
|
||||
v-for="tech in backendTechs"
|
||||
:key="tech.name"
|
||||
:icon="tech.icon"
|
||||
:text="tech.name"
|
||||
/>
|
||||
</wd-grid>
|
||||
<view class="about-meta">
|
||||
<text class="app-title">{{ webTitle }}</text>
|
||||
<text class="app-version">版本 {{ version }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</wd-card>
|
||||
</view>
|
||||
|
||||
<!-- 资源链接 -->
|
||||
<view class="section">
|
||||
<wd-card title="资源链接" custom-style="margin: 20rpx;">
|
||||
<wd-cell-group>
|
||||
<wd-cell title="帮助文档" label="查看完整开发文档" is-link @click="openLink(helpDoc)">
|
||||
<template #icon>
|
||||
<view class="link-icon-wrapper doc">
|
||||
<wd-icon name="document" size="28" color="#409eff" />
|
||||
<!-- 公司信息区域 -->
|
||||
<wd-card title="项目概述" custom-style="margin-top: 20rpx">
|
||||
<view class="company-section">
|
||||
<view class="section-title">
|
||||
<wd-icon name="home" size="18" />
|
||||
<text>{{ webTitle }}</text>
|
||||
</view>
|
||||
<text class="section-subtitle">{{ webDescription }}</text>
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<wd-card title="技术站" custom-style="margin-top: 20rpx">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="后台" icon="desktop">
|
||||
<view slot="label" class="project-desc">
|
||||
基于 FastAPI + Uvicorn + SQLAlchemy 构建的RBAC管理系统
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="用户协议" label="了解使用条款" is-link @click="openLink(webClause)">
|
||||
<template #icon>
|
||||
<view class="link-icon-wrapper agreement">
|
||||
<wd-icon name="agreement" size="28" color="#52c41a" />
|
||||
|
||||
<wd-cell title="web前端" icon="mobile">
|
||||
<view slot="label" class="project-desc">
|
||||
基于 Vue3 + Vite6 + TypeScript + Element-Plus + Pinia 构建的中后台管理模板
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="隐私政策" label="保护您的隐私" is-link @click="openLink(webPrivacy)">
|
||||
<template #icon>
|
||||
<view class="link-icon-wrapper privacy">
|
||||
<wd-icon name="privacy" size="28" color="#faad14" />
|
||||
|
||||
<wd-cell title="移动端" icon="server" label="">
|
||||
<view slot="label" class="project-desc">
|
||||
基于 uni-app + Vite6 + Vue 3 + TypeScript + Wot Design Uni + Pinia 构建的移动端应用模板
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="开源代码" label="访问GitHub仓库" is-link @click="openLink(gitCode)">
|
||||
<template #icon>
|
||||
<view class="link-icon-wrapper github">
|
||||
<wd-icon name="github" size="28" color="#333" />
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
</view>
|
||||
|
||||
<!-- 版权信息 -->
|
||||
<view class="copyright-section">
|
||||
<wd-divider content-position="center" custom-style="margin: 40rpx 0;">
|
||||
<text class="divider-text">版权信息</text>
|
||||
</wd-divider>
|
||||
<view class="copyright-content">
|
||||
<view class="copyright-card">
|
||||
<wd-icon name="copyright" size="32" color="#909399" />
|
||||
<text class="copyright-text">{{ copyright }}</text>
|
||||
<text class="record-text">{{ keepRecord }}</text>
|
||||
<view class="copyright-links">
|
||||
<text class="copyright-link" @click="openLink(webClause)">用户协议</text>
|
||||
<text class="copyright-separator">|</text>
|
||||
<text class="copyright-link" @click="openLink(webPrivacy)">隐私政策</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 联系方式 -->
|
||||
<wd-card title="联系我们" custom-style="margin-top: 20rpx">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="联系邮箱" value="948080782@qq.com" icon="mail" ellipsis />
|
||||
<wd-cell title="官方网站" :value="helpDoc" is-link :to="helpDoc" icon="link" ellipsis />
|
||||
<wd-cell title="源码地址" :value="gitCode" is-link :to="gitCode" icon="github" ellipsis />
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!-- 底部版权信息 -->
|
||||
<view class="about-footer">
|
||||
<text class="copyright">{{ copyright }}</text>
|
||||
<text class="copyright copyright-sub">{{ keepRecord }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
// 系统配置数据
|
||||
const webTitle = ref("FastAPI Vue3 Admin");
|
||||
const webDescription = ref("FastAPI Vue3 Admin 是完全开源的权限管理系统");
|
||||
const version = ref("2.0.0");
|
||||
const keepRecord = ref("陕ICP备2025069493号-1");
|
||||
const copyright = ref("Copyright © 2025-2026 service.fastapiadmin.com 版权所有");
|
||||
const webLogo = ref("http://service.fastapiadmin.com/api/v1/static/image/logo.png");
|
||||
const helpDoc = ref("http://service.fastapiadmin.com/docs/index.html");
|
||||
const webClause = ref("https://github.com/1014TaoTao/fastapi_vue3_admin/blob/master/LICENSE");
|
||||
const webPrivacy = ref("https://github.com/1014TaoTao/fastapi_vue3_admin/blob/master/LICENSE");
|
||||
const helpDoc = ref("http://service.fastapiadmin.com");
|
||||
const gitCode = ref("https://github.com/1014TaoTao/fastapi_vue3_admin.git");
|
||||
|
||||
// 技术栈数据
|
||||
const frontendTechs = ref([
|
||||
{ name: "Vue 3", icon: "code", color: "#4fc08d" },
|
||||
{ name: "TypeScript", icon: "code", color: "#3178c6" },
|
||||
{ name: "Wot Design", icon: "code", color: "#409eff" },
|
||||
{ name: "uni-app", icon: "code", color: "#007aff" },
|
||||
]);
|
||||
|
||||
const backendTechs = ref([
|
||||
{ name: "FastAPI", icon: "link", color: "#009688" },
|
||||
{ name: "Python", icon: "code", color: "#3776ab" },
|
||||
{ name: "MySQL", icon: "code", color: "#4479a1" },
|
||||
{ name: "Redis", icon: "code", color: "#dc382d" },
|
||||
]);
|
||||
onMounted(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
version.value = uni.getSystemInfoSync().appVersion;
|
||||
// #endif
|
||||
});
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const openLink = (url: string) => {
|
||||
// #ifdef H5
|
||||
window.open(url, "_blank");
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openURL(url);
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: "链接已复制",
|
||||
icon: "success",
|
||||
duration: 2000,
|
||||
});
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "about",
|
||||
"style": {
|
||||
"navigationBarTitleText": "关于我们"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 40rpx;
|
||||
background: var(--wot-color-bg-secondary);
|
||||
}
|
||||
|
||||
.brand-section {
|
||||
padding: 60rpx 0;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.brand-content {
|
||||
.about-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.brand-info {
|
||||
margin-top: 24rpx;
|
||||
text-align: center;
|
||||
.about-logo {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
.about-meta {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 36rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
.app-version {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.company-section {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
|
||||
:deep(.wd-icon) {
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
padding-left: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.copyright-section {
|
||||
padding: 0 32rpx;
|
||||
margin-top: 60rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.copyright-content {
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.copyright-card {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
background: var(--wot-color-bg);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.copyright-text {
|
||||
margin: 20rpx 0 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
.record-text {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
.copyright-links {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.copyright-link {
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.copyright-separator {
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-text-third);
|
||||
}
|
||||
|
||||
:deep(.wd-grid-item__content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.wd-cell__left) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.architecture-detail {
|
||||
padding: 0 24rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
flex-shrink: 0;
|
||||
margin-right: 16rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
color: var(--wot-color-text-secondary);
|
||||
}
|
||||
|
||||
:deep(.architecture-grid) {
|
||||
margin: 24rpx;
|
||||
}
|
||||
|
||||
.tech-stack-container {
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.tech-category {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 8rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.category-text {
|
||||
margin-left: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
:deep(.tech-grid) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.wd-cell__body) {
|
||||
padding: 28rpx 32rpx;
|
||||
}
|
||||
|
||||
:deep(.wd-cell__title) {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
:deep(.wd-cell__label) {
|
||||
.project-desc {
|
||||
margin-top: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.link-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
.about-footer {
|
||||
padding: 32rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link-icon-wrapper.doc {
|
||||
background: linear-gradient(135deg, #409eff, #66b1ff);
|
||||
.copyright {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.link-icon-wrapper.agreement {
|
||||
background: linear-gradient(135deg, #52c41a, #73d13d);
|
||||
}
|
||||
|
||||
.link-icon-wrapper.privacy {
|
||||
background: linear-gradient(135deg, #faad14, #ffc53d);
|
||||
}
|
||||
|
||||
.link-icon-wrapper.github {
|
||||
background: linear-gradient(135deg, #333, #666);
|
||||
.copyright-sub {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<template>
|
||||
<view class="faq-container">
|
||||
<wd-navbar
|
||||
title="常见问题"
|
||||
left-arrow
|
||||
safe-area-inset-top
|
||||
placeholder
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
<wd-navbar title="常见问题" left-arrow placeholder safe-area-inset-top @click-left="handleBack" />
|
||||
|
||||
<scroll-view class="content-scroll" scroll-y :scroll-top="scrollTop" @scroll="handleScroll">
|
||||
<view class="content-wrapper">
|
||||
@@ -21,7 +15,7 @@
|
||||
<view class="support-grid">
|
||||
<view class="support-item" @tap="handleContact('email')">
|
||||
<view class="support-icon">
|
||||
<wd-icon name="email" size="40rpx" color="#0083f0" />
|
||||
<wd-icon name="mail" size="40rpx" color="#0083f0" />
|
||||
</view>
|
||||
<view class="support-info">
|
||||
<text class="support-label">邮箱支持</text>
|
||||
@@ -55,24 +49,16 @@
|
||||
<wd-collapse v-model="activeNames" accordion>
|
||||
<wd-collapse-item title="常见问题" name="faq">
|
||||
<view class="faq-list">
|
||||
<view
|
||||
v-for="(item, index) in faqList"
|
||||
:key="index"
|
||||
class="faq-item"
|
||||
:class="{ 'active': currentFaq === index }"
|
||||
@tap="toggleFaq(index)"
|
||||
>
|
||||
<view v-for="(item, index) in faqList" :key="index" class="faq-item" :class="{
|
||||
active: currentFaq === index,
|
||||
}" @tap="toggleFaq(index)">
|
||||
<view class="faq-header">
|
||||
<view class="faq-question">
|
||||
<wd-icon name="question-filled" size="28rpx" color="#ff9500" />
|
||||
<text class="question-text">{{ item.question }}</text>
|
||||
</view>
|
||||
<wd-icon
|
||||
name="arrow-down-bold"
|
||||
size="24rpx"
|
||||
color="#999"
|
||||
:custom-class="currentFaq === index ? 'rotate-180' : ''"
|
||||
/>
|
||||
<wd-icon name="arrow-down-bold" size="24rpx" color="#999"
|
||||
:custom-class="currentFaq === index ? 'rotate-180' : ''" />
|
||||
</view>
|
||||
<view v-if="currentFaq === index" class="faq-answer">
|
||||
<text class="answer-text">{{ item.answer }}</text>
|
||||
@@ -83,11 +69,7 @@
|
||||
|
||||
<wd-collapse-item title="系统功能" name="features">
|
||||
<view class="feature-list">
|
||||
<view
|
||||
v-for="(feature, index) in featureList"
|
||||
:key="index"
|
||||
class="feature-item"
|
||||
>
|
||||
<view v-for="(feature, index) in featureList" :key="index" class="feature-item">
|
||||
<view class="feature-icon">
|
||||
<wd-icon :name="feature.icon" size="48rpx" :color="feature.color" />
|
||||
</view>
|
||||
@@ -106,18 +88,14 @@
|
||||
<text class="guide-title">快速开始</text>
|
||||
</view>
|
||||
<view class="guide-steps">
|
||||
<view
|
||||
v-for="(step, index) in guideSteps"
|
||||
:key="index"
|
||||
class="guide-step"
|
||||
>
|
||||
<view v-for="(step, index) in guideSteps" :key="index" class="guide-step">
|
||||
<view class="step-indicator">
|
||||
<view class="step-number">{{ index + 1 }}</view>
|
||||
<view class="step-line" v-if="index < guideSteps.length - 1"></view>
|
||||
<view v-if="index < guideSteps.length - 1" class="step-line"></view>
|
||||
</view>
|
||||
<view class="step-content">
|
||||
<text class="step-title">{{ step.title }}</text>
|
||||
<text class="step-desc" v-if="step.desc">{{ step.desc }}</text>
|
||||
<text v-if="step.desc" class="step-desc">{{ step.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -146,21 +124,25 @@ const scrollTop = ref<number>(0);
|
||||
const faqList = ref([
|
||||
{
|
||||
question: "如何重置密码?",
|
||||
answer: '在登录页面点击"忘记密码",按照提示操作即可重置密码。如果无法收到验证码,请检查邮箱或手机号是否正确,或联系客服协助处理。'
|
||||
answer:
|
||||
"在登录页面点击 '忘记密码',按照提示操作即可重置密码。如果无法收到验证码,请检查邮箱或手机号是否正确,或联系客服协助处理。",
|
||||
},
|
||||
{
|
||||
question: "数据如何备份?",
|
||||
answer: '系统会自动定期备份数据,您也可以在"系统设置-数据管理"中手动导出数据。建议每月进行一次完整数据备份,确保数据安全。'
|
||||
answer:
|
||||
"系统会自动定期备份数据,您也可以在'系统设置-数据管理'中手动导出数据。建议每月进行一次完整数据备份,确保数据安全。",
|
||||
},
|
||||
{
|
||||
question: "支持哪些浏览器?",
|
||||
answer: '推荐使用Chrome 90+、Firefox 88+、Safari 14+、Edge 90+等现代浏览器。IE浏览器仅支持IE11及以上版本。'
|
||||
answer:
|
||||
"推荐使用Chrome 90+、Firefox 88+、Safari 14+、Edge 90+等现代浏览器。IE浏览器仅支持IE11及以上版本。",
|
||||
},
|
||||
{
|
||||
question: "如何联系客服?",
|
||||
answer: "您可以通过以下方式联系客服:1. 客服热线 400-123-4567(工作日9:00-18:00);2. 邮箱 support@example.com(7×24小时);3. 在线客服(工作日9:00-18:00)。"
|
||||
}
|
||||
])
|
||||
answer:
|
||||
"您可以通过以下方式联系客服:1. 客服热线 400-123-4567(工作日9:00-18:00);2. 邮箱 support@example.com(7×24小时);3. 在线客服(工作日9:00-18:00)。",
|
||||
},
|
||||
]);
|
||||
|
||||
// 系统功能数据
|
||||
const featureList = ref([
|
||||
@@ -168,65 +150,65 @@ const featureList = ref([
|
||||
name: "用户管理",
|
||||
desc: "支持用户注册、登录、权限管理等功能",
|
||||
icon: "user",
|
||||
color: "#0083f0"
|
||||
color: "#0083f0",
|
||||
},
|
||||
{
|
||||
name: "数据统计",
|
||||
desc: "提供实时数据分析和可视化报表",
|
||||
icon: "chart",
|
||||
color: "#00c250"
|
||||
color: "#00c250",
|
||||
},
|
||||
{
|
||||
name: "文件管理",
|
||||
desc: "支持文件上传、下载、分类管理",
|
||||
icon: "folder",
|
||||
color: "#ff9500"
|
||||
color: "#ff9500",
|
||||
},
|
||||
{
|
||||
name: "消息通知",
|
||||
desc: "实时消息推送和系统通知",
|
||||
icon: "notification",
|
||||
color: "#ff3b30"
|
||||
}
|
||||
])
|
||||
color: "#ff3b30",
|
||||
},
|
||||
]);
|
||||
|
||||
// 使用指南数据
|
||||
const guideSteps = ref([
|
||||
{
|
||||
title: "注册账号并登录系统",
|
||||
desc: "使用手机号或邮箱注册账号,完成实名认证"
|
||||
desc: "使用手机号或邮箱注册账号,完成实名认证",
|
||||
},
|
||||
{
|
||||
title: "完善个人或企业信息",
|
||||
desc: "填写基本信息,设置安全问题和密保邮箱"
|
||||
desc: "填写基本信息,设置安全问题和密保邮箱",
|
||||
},
|
||||
{
|
||||
title: "根据需求配置功能模块",
|
||||
desc: "选择需要的功能模块,设置相关参数"
|
||||
desc: "选择需要的功能模块,设置相关参数",
|
||||
},
|
||||
{
|
||||
title: "开始使用各项功能",
|
||||
desc: "完成初始化设置,开始使用系统各项功能"
|
||||
}
|
||||
])
|
||||
desc: "完成初始化设置,开始使用系统各项功能",
|
||||
},
|
||||
]);
|
||||
|
||||
// 方法定义
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const handleScroll = (e: any) => {
|
||||
scrollTop.value = e.detail.scrollTop
|
||||
}
|
||||
scrollTop.value = e.detail.scrollTop;
|
||||
};
|
||||
|
||||
const toggleFaq = (index: number) => {
|
||||
currentFaq.value = currentFaq.value === index ? null : index
|
||||
}
|
||||
currentFaq.value = currentFaq.value === index ? null : index;
|
||||
};
|
||||
|
||||
const handleContact = (type: "email" | "phone") => {
|
||||
if (type === "email") {
|
||||
// #ifdef H5
|
||||
window.location.href = "mailto:support@example.com"
|
||||
window.location.href = "mailto:support@example.com";
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({
|
||||
@@ -234,34 +216,41 @@ const handleContact = (type: "email" | "phone") => {
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: "邮箱已复制",
|
||||
icon: "success"
|
||||
})
|
||||
}
|
||||
})
|
||||
icon: "success",
|
||||
});
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
} else if (type === "phone") {
|
||||
// #ifdef H5
|
||||
window.location.href = "tel:400-123-4567"
|
||||
window.location.href = "tel:400-123-4567";
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: "400-123-4567"
|
||||
})
|
||||
phoneNumber: "400-123-4567",
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 可以在这里添加页面埋点或初始化逻辑
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "faq",
|
||||
"style": {
|
||||
"navigationBarTitleText": "常见问题"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
<style lang="scss" scoped>
|
||||
.faq-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
background: var(--wot-color-bg);
|
||||
|
||||
.content-scroll {
|
||||
@@ -536,8 +525,8 @@ onMounted(() => {
|
||||
.guide-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 48rpx;
|
||||
padding: 32rpx;
|
||||
margin-top: 48rpx;
|
||||
background: var(--wot-color-bg);
|
||||
border-radius: 16rpx;
|
||||
|
||||
@@ -577,8 +566,8 @@ onMounted(() => {
|
||||
|
||||
.support-card,
|
||||
.faq-section :deep(.wd-collapse) {
|
||||
background: #2c2c2e;
|
||||
color: var(--wot-color-text);
|
||||
background: #2c2c2e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="意见反馈" left-arrow @click-left="handleBack" />
|
||||
<wd-navbar title="意见反馈" left-text="返回" left-arrow placeholder safe-area-inset-top right-text="首页"
|
||||
@click-left="handleBack" @click-right="handleClickRight" />
|
||||
|
||||
123
|
||||
<wd-text size="small">选填,最多上传3张图片</wd-text>
|
||||
<wd-form ref="formRef" :model="formData" :rules="rules">
|
||||
<wd-form ref="formRef" :model="formData" :rules="rules" errorType="toast">
|
||||
<!-- 问题类型选择 -->
|
||||
<wd-form-item label="问题类型" prop="feedbackType">
|
||||
<wd-radio-group v-model="formData.feedbackType" inline>
|
||||
<wd-cell-group title="问题类型" border>
|
||||
<wd-radio-group v-model="formData.feedbackType" inline prop="feedbackType">
|
||||
<wd-radio v-for="item in feedbackTypes" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
|
||||
<!-- 问题描述 -->
|
||||
<wd-form-item label="问题描述" prop="description">
|
||||
<wd-textarea
|
||||
v-model="formData.description"
|
||||
placeholder="请详细描述您遇到的问题或建议..."
|
||||
:maxlength="120"
|
||||
show-word-limit
|
||||
/>
|
||||
</wd-form-item>
|
||||
<wd-textarea v-model="formData.description" label="问题描述" prop="description" placeholder="请详细描述您遇到的问题或建议..."
|
||||
:maxlength="120" show-word-limit />
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<wd-form-item label="相关截图" prop="fileList">
|
||||
<wd-upload
|
||||
v-model="formData.fileList"
|
||||
:max-count="3"
|
||||
:before-read="beforeRead"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</wd-form-item>
|
||||
<wd-upload v-model="formData.fileList" label="相关截图" prop="fileList" :max-count="3" :before-read="beforeRead"
|
||||
@delete="handleDelete" />
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<wd-form-item label="联系方式" prop="contact">
|
||||
<wd-input v-model="formData.contact" placeholder="请输入您的手机号或邮箱" clearable />
|
||||
<wd-input v-model="formData.contact" label="联系方式" prop="contact" placeholder="请输入您的手机号或邮箱" clearable
|
||||
:border="false" />
|
||||
<wd-text size="small">选填,便于我们与您联系</wd-text>
|
||||
</wd-form-item>
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-btn">
|
||||
<wd-button type="primary" block :loading="submitting" @click="handleSubmit">
|
||||
<view class="footer">
|
||||
<wd-button type="primary" size="large" :loading="submitting" block @click="handleSubmit">
|
||||
提交反馈
|
||||
</wd-button>
|
||||
</view>
|
||||
@@ -175,8 +159,22 @@ const handleSubmit = async () => {
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
|
||||
// 首页
|
||||
const handleClickRight = () => {
|
||||
uni.switchTab({
|
||||
url: "/pages/index/index",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "feedback",
|
||||
"style": {
|
||||
"navigationBarTitleText": "问题反馈"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.wd-form-item) {
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
</view>
|
||||
<view class="user-details">
|
||||
<block v-if="isLogin">
|
||||
<view class="name">{{ userInfo!.name || "匿名用户" }}</view>
|
||||
<view class="user-id">账号: {{ userInfo?.username || "user" }}</view>
|
||||
<view class="nickname">{{ userInfo!.name || "匿名用户" }}</view>
|
||||
<view class="user-id">ID: {{ userInfo?.username || "0000000" }}</view>
|
||||
</block>
|
||||
<block v-else>
|
||||
<view class="login-prompt">立即登录</view>
|
||||
<view class="login-prompt">立即登录获取更多功能</view>
|
||||
<wd-button
|
||||
custom-class="btn-login"
|
||||
size="small"
|
||||
@@ -33,64 +33,64 @@
|
||||
<wd-icon name="setting1" size="22" color="#333" />
|
||||
</view>
|
||||
<view v-if="isLogin" class="action-btn" @click="navigateToSection('messages')">
|
||||
<wd-badge v-if="true" modelValue="99+">
|
||||
<wd-icon name="notification" size="22" color="#333" />
|
||||
<view v-if="true" class="badge">2</view>
|
||||
</wd-badge>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<view class="stats-container">
|
||||
<view class="stat-item" @click="navigateToSection('wallet')">
|
||||
<view class="stat-value">0.00</view>
|
||||
<view class="stat-label">我的余额</view>
|
||||
<view class="stats-card">
|
||||
<view class="stats-item" @click="navigateToSection('wallet')">
|
||||
<view class="mb-8rpx text-36rpx font-600">0.00</view>
|
||||
<view class="text-26rpx text-gray-500">我的余额</view>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="stat-item" @click="navigateToSection('favorites')">
|
||||
<view class="stat-value">0</view>
|
||||
<view class="stat-label">我的收藏</view>
|
||||
<view class="stats-divider"></view>
|
||||
<view class="stats-item" @click="navigateToSection('favorites')">
|
||||
<view class="mb-8rpx text-36rpx font-600">0</view>
|
||||
<view class="text-26rpx text-gray-500">我的收藏</view>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="stat-item" @click="navigateToSection('history')">
|
||||
<view class="stat-value">0</view>
|
||||
<view class="stat-label">浏览历史</view>
|
||||
<view class="stats-divider"></view>
|
||||
<view class="stats-item" @click="navigateToSection('history')">
|
||||
<view class="mb-8rpx text-36rpx font-600">0</view>
|
||||
<view class="text-26rpx text-gray-500">浏览历史</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 常用工具 -->
|
||||
<view class="card-container">
|
||||
<view class="card-header">
|
||||
<view class="card-title">
|
||||
<wd-icon name="tools" size="18" :color="currentThemeColor.primary" />
|
||||
<text>常用工具</text>
|
||||
<view class="flex-start">
|
||||
<wd-icon name="tools" size="18" :color="currentThemeColor" />
|
||||
<text class="ml-12rpx text-28rpx font-600">常用工具</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tools-grid">
|
||||
<view class="flex flex-wrap p-20rpx pt-20rpx pb-10rpx">
|
||||
<view class="tool-item" @click="navigateToProfile">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="user" size="24" :color="currentThemeColor.primary" />
|
||||
<wd-icon name="user" size="24" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="tool-label">个人资料</view>
|
||||
<view class="text-24rpx">个人资料</view>
|
||||
</view>
|
||||
|
||||
<view class="tool-item" @click="navigateToFAQ">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="help-circle" size="24" :color="currentThemeColor.primary" />
|
||||
<wd-icon name="help-circle" size="24" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="tool-label">常见问题</view>
|
||||
<view class="text-24rpx">常见问题</view>
|
||||
</view>
|
||||
<view class="tool-item" @click="handleQuestionFeedback">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="check-circle" size="24" :color="currentThemeColor.primary" />
|
||||
<wd-icon name="check-circle" size="24" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="tool-label">问题反馈</view>
|
||||
<view class="text-24rpx">问题反馈</view>
|
||||
</view>
|
||||
<view class="tool-item" @click="navigateToAbout">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="info-circle" size="24" :color="currentThemeColor.primary" />
|
||||
<wd-icon name="info-circle" size="24" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="tool-label">关于我们</view>
|
||||
<view class="text-24rpx">关于我们</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -98,60 +98,57 @@
|
||||
<!-- 推荐服务 -->
|
||||
<view class="card-container">
|
||||
<view class="card-header">
|
||||
<view class="card-title">
|
||||
<wd-icon name="star" size="18" :color="currentThemeColor.primary" />
|
||||
<text>推荐服务</text>
|
||||
<view class="flex-start">
|
||||
<wd-icon name="star" size="18" :color="currentThemeColor" />
|
||||
<text class="ml-12rpx text-28rpx font-600">推荐服务</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="services-list">
|
||||
<view>
|
||||
<view class="service-item" @click="navigateToSection('services', 'vip')">
|
||||
<view class="service-left">
|
||||
<view class="flex-start">
|
||||
<view class="service-icon">
|
||||
<wd-icon name="dong" size="22" :color="currentThemeColor.primary" />
|
||||
<wd-icon name="dong" size="22" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="service-info">
|
||||
<view class="service-name">会员中心</view>
|
||||
<view class="service-desc">解锁更多特权</view>
|
||||
<view>
|
||||
<view class="text-28rpx font-500">会员中心</view>
|
||||
<view class="mt-8rpx text-24rpx text-gray-500">解锁更多特权</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="14" color="#999" />
|
||||
<wd-icon name="arrow-right" size="14" />
|
||||
</view>
|
||||
<view class="service-item" @click="navigateToSection('services', 'coupon')">
|
||||
<view class="service-left">
|
||||
<view class="flex-start">
|
||||
<view class="service-icon">
|
||||
<wd-icon name="discount" size="22" :color="currentThemeColor.primary" />
|
||||
<wd-icon name="discount" size="22" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="service-info">
|
||||
<view class="service-name">优惠券</view>
|
||||
<view class="service-desc">查看我的优惠券</view>
|
||||
<view>
|
||||
<view class="text-28rpx font-500">优惠券</view>
|
||||
<view class="mt-8rpx text-24rpx text-gray-500">查看我的优惠券</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="14" color="#999" />
|
||||
<wd-icon name="arrow-right" size="14" />
|
||||
</view>
|
||||
<view class="service-item" @click="navigateToSection('services', 'invite')">
|
||||
<view class="service-left">
|
||||
<view
|
||||
class="service-item service-item-last"
|
||||
@click="navigateToSection('services', 'invite')"
|
||||
>
|
||||
<view class="flex-start">
|
||||
<view class="service-icon">
|
||||
<wd-icon name="share" size="22" :color="currentThemeColor.primary" />
|
||||
<wd-icon name="share" size="22" :color="currentThemeColor" />
|
||||
</view>
|
||||
<view class="service-info">
|
||||
<view class="service-name">邀请有礼</view>
|
||||
<view class="service-desc">邀请好友得奖励</view>
|
||||
<view>
|
||||
<view class="text-28rpx font-500">邀请有礼</view>
|
||||
<view class="mt-8rpx text-24rpx text-gray-500">邀请好友得奖励</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="14" color="#999" />
|
||||
<wd-icon name="arrow-right" size="14" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录按钮 -->
|
||||
<view v-if="isLogin" class="logout-btn-wrap">
|
||||
<wd-button
|
||||
class="w-full h-80rpx rounded-40rpx font-bold text-32rpx"
|
||||
plain
|
||||
@click="handleLogout"
|
||||
>
|
||||
退出登录
|
||||
</wd-button>
|
||||
<view v-if="isLogin" class="p-30rpx">
|
||||
<wd-button custom-class="logout-button" plain @click="handleLogout">退出登录</wd-button>
|
||||
</view>
|
||||
|
||||
<wd-toast />
|
||||
@@ -159,29 +156,29 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { useUserStore } from "@/store/modules/user.store";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useRouter } from "uni-mini-router";
|
||||
import { useUserStore, useThemeStore } from "@/store";
|
||||
import { computed } from "vue";
|
||||
|
||||
const toast = useToast();
|
||||
const userStore = useUserStore();
|
||||
const { currentThemeColor } = useTheme();
|
||||
const themeStore = useThemeStore();
|
||||
const currentThemeColor = computed(() => themeStore.themeVars.colorTheme);
|
||||
const userInfo = computed(() => userStore.userInfo);
|
||||
const isLogin = computed(() => !!userInfo.value);
|
||||
const defaultAvatar = "/static/images/default-avatar.png";
|
||||
const isLoading = ref(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 登录
|
||||
const navigateToLoginPage = () => {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const currentPagePath = `/${currentPage.route}`;
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/login/index?redirect=${encodeURIComponent(currentPagePath)}`,
|
||||
});
|
||||
router.push({ path: "/pages/login/index", query: { redirect: currentPagePath } });
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
@@ -204,27 +201,27 @@ const navigateToProfile = () => {
|
||||
navigateToLoginPage();
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: "/pages/mine/profile/index" });
|
||||
router.push({ path: "/pages/mine/profile/index" });
|
||||
};
|
||||
|
||||
// 常见问题
|
||||
const navigateToFAQ = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/faq/index" });
|
||||
router.push({ path: "/pages/mine/faq/index" });
|
||||
};
|
||||
|
||||
// 关于我们
|
||||
const navigateToAbout = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/about/index" });
|
||||
router.push({ path: "/pages/mine/about/index" });
|
||||
};
|
||||
|
||||
// 设置
|
||||
const navigateToSettings = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/settings/index" });
|
||||
router.push({ path: "/pages/mine/settings/index" });
|
||||
};
|
||||
|
||||
// 问题反馈
|
||||
const handleQuestionFeedback = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/feedback/index" });
|
||||
router.push({ path: "/pages/mine/feedback/index" });
|
||||
};
|
||||
|
||||
// 导航到各个板块
|
||||
@@ -299,7 +296,7 @@ watch(
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
height: 240rpx;
|
||||
background: linear-gradient(to bottom, var(--wot-color-theme), var(--primary-color-light));
|
||||
background: linear-gradient(135deg, var(--wot-color-theme, #165dff) 0%, #667eea 100%);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
@@ -357,160 +354,48 @@ watch(
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -6rpx;
|
||||
right: -6rpx;
|
||||
z-index: 2;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 6rpx;
|
||||
font-size: 20rpx;
|
||||
line-height: 32rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: var(--wot-color-danger);
|
||||
border: 2rpx solid #fff;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数据统计
|
||||
.stats-container {
|
||||
display: flex;
|
||||
padding: 30rpx 20rpx;
|
||||
margin: 20rpx 30rpx;
|
||||
background: var(--wot-color-bg-container);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: var(--wot-card-shadow);
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.stat-value {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 26rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
margin: 0 20rpx;
|
||||
background-color: var(--wot-color-border);
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片容器通用样式
|
||||
// 卡片容器
|
||||
.card-container {
|
||||
margin: 24rpx 30rpx;
|
||||
overflow: hidden;
|
||||
background: var(--wot-color-bg-container);
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: var(--wot-card-shadow);
|
||||
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// 卡片头部
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
border-bottom: 1rpx solid var(--wot-color-border);
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
text {
|
||||
margin-left: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
border-bottom: 1rpx solid #e5e7eb;
|
||||
}
|
||||
|
||||
.card-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
text {
|
||||
margin-right: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 暗色模式下的卡片样式
|
||||
:deep(.dark) .card-container {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
// 订单状态
|
||||
.order-status {
|
||||
display: flex;
|
||||
padding: 30rpx 0 20rpx;
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.status-icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.status-badge {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
right: -10rpx;
|
||||
z-index: 2;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 6rpx;
|
||||
font-size: 20rpx;
|
||||
line-height: 32rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-color: #ff4d4f;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
:deep(.dark) .card-header {
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具网格
|
||||
.tools-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 20rpx 0 10rpx;
|
||||
|
||||
// 工具项
|
||||
.tool-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 25%;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
// 工具图标
|
||||
.tool-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -518,44 +403,80 @@ watch(
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
margin-bottom: 12rpx;
|
||||
background-color: var(--wot-color-bg-light);
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 18rpx;
|
||||
transition: transform 0.2s;
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.tool-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
}
|
||||
// 暗色模式下的工具图标样式
|
||||
:deep(.dark) .tool-icon {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
// 服务列表
|
||||
.services-list {
|
||||
// 数据统计卡片(减少原子类堆叠)
|
||||
.stats-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
margin: 20rpx 30rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:deep(.dark) .stats-card {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-divider {
|
||||
width: 1px;
|
||||
margin: 0 20rpx;
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
:deep(.dark) .stats-divider {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
// 服务项
|
||||
.service-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid var(--wot-color-border);
|
||||
transition: background-color 0.2s;
|
||||
border-bottom: 1rpx solid #e5e7eb;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:active {
|
||||
background-color: var(--wot-color-bg-light);
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&.service-item-last {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.service-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// 暗色模式下的服务项样式
|
||||
:deep(.dark) .service-item {
|
||||
border-bottom-color: #374151;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 服务图标
|
||||
.service-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -563,29 +484,27 @@ watch(
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-right: 20rpx;
|
||||
background-color: var(--wot-color-bg-light);
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.service-info {
|
||||
.service-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: var(--wot-color-text);
|
||||
// 暗色模式下的服务图标样式
|
||||
:deep(.dark) .service-icon {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.service-desc {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 登录按钮样式
|
||||
:deep(.btn-login) {
|
||||
font-size: 24rpx !important;
|
||||
border-radius: 20rpx !important;
|
||||
}
|
||||
|
||||
// 退出登录按钮
|
||||
.logout-btn-wrap {
|
||||
padding: 30rpx;
|
||||
// 退出登录按钮样式
|
||||
:deep(.logout-button) {
|
||||
width: 100% !important;
|
||||
height: 80rpx !important;
|
||||
font-size: 32rpx !important;
|
||||
font-weight: bold !important;
|
||||
border-radius: 40rpx !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -159,7 +159,7 @@ const redirect = ref("/pages/index/index");
|
||||
// 表单数据
|
||||
const profileForm = reactive({
|
||||
name: "",
|
||||
gender: 1,
|
||||
gender: "0",
|
||||
mobile: "",
|
||||
email: "",
|
||||
username: "",
|
||||
@@ -197,7 +197,7 @@ onLoad((options: any) => {
|
||||
if (userInfo) {
|
||||
profileForm.name = userInfo.name || "";
|
||||
profileForm.avatar = userInfo.avatar || "";
|
||||
profileForm.gender = userInfo.gender || 1;
|
||||
profileForm.gender = userInfo.gender || "0";
|
||||
profileForm.email = userInfo.email || "";
|
||||
profileForm.username = userInfo.username || "";
|
||||
profileForm.dept_name = userInfo.dept_name || "";
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="个人信息" left-arrow @click-left="handleBack" />
|
||||
<wd-navbar
|
||||
title="个人信息"
|
||||
left-arrow
|
||||
placeholder
|
||||
safe-area-inset-top
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<wd-card v-if="userProfile" custom-style="margin-top: 20rpx">
|
||||
<wd-cell-group border>
|
||||
@@ -24,13 +30,27 @@
|
||||
<wd-cell title="昵称" :value="userProfile.name" is-link @click="handleOpenDialog()" />
|
||||
<wd-cell
|
||||
title="性别"
|
||||
:value="userProfile.gender === 1 ? '男' : userProfile.gender === 2 ? '女' : '未知'"
|
||||
:value="userProfile.gender === '0' ? '男' : userProfile.gender === '1' ? '女' : '未知'"
|
||||
is-link
|
||||
@click="handleOpenDialog()"
|
||||
/>
|
||||
<wd-cell title="用户名" :value="userProfile.username" />
|
||||
<wd-cell v-if="userProfile" title="状态" center>
|
||||
<wd-tag :type="userProfile.status ? 'success' : 'danger'">
|
||||
{{ userProfile.status ? "启用" : "停用" }}
|
||||
</wd-tag>
|
||||
</wd-cell>
|
||||
<wd-cell v-if="userProfile" title="是否超管" center>
|
||||
<wd-tag plain :type="userProfile.is_superuser ? 'primary' : 'default'">
|
||||
{{ userProfile.is_superuser ? "是" : "否" }}
|
||||
</wd-tag>
|
||||
</wd-cell>
|
||||
<wd-cell title="手机号" :value="userProfile.mobile" />
|
||||
<wd-cell title="邮箱" :value="userProfile.email" />
|
||||
<wd-cell title="部门" :value="userProfile.dept_name" />
|
||||
<wd-cell title="角色" :value="userProfile.roleNames?.join(', ')" />
|
||||
<wd-cell title="角色" :value="userProfile.roles?.map((item) => item.name).join(', ')" />
|
||||
<wd-cell title="岗位" :value="userProfile.positions?.map((item) => item.name).join(', ')" />
|
||||
<wd-cell title="备注" :value="userProfile.description" />
|
||||
<wd-cell title="创建日期" :value="userProfile.created_at" />
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
@@ -52,12 +72,13 @@
|
||||
/>
|
||||
<wd-cell title="性别" title-width="160rpx" center prop="gender" :rules="rules.gender">
|
||||
<wd-radio-group v-model="userProfileForm.gender" shape="button" class="ef-radio-group">
|
||||
<wd-radio :value="1">男</wd-radio>
|
||||
<wd-radio :value="2">女</wd-radio>
|
||||
<wd-radio :value="0">男</wd-radio>
|
||||
<wd-radio :value="1">女</wd-radio>
|
||||
<wd-radio :value="2">未知</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
<view class="p-6">
|
||||
<view class="footer">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
@@ -95,7 +116,7 @@ function handleAvatarConfirm(event: any) {
|
||||
FileAPI.upload(tempFilePath).then((fileInfo: UploadFileResult) => {
|
||||
const avatarForm = {
|
||||
name: userProfile.value?.name || "",
|
||||
gender: userProfile.value?.gender || 1,
|
||||
gender: userProfile.value?.gender || "0",
|
||||
mobile: userProfile.value?.mobile || "",
|
||||
email: userProfile.value?.email || "",
|
||||
username: userProfile.value?.username || "",
|
||||
@@ -125,7 +146,7 @@ const dialog = reactive({
|
||||
|
||||
const userProfileForm = reactive<{
|
||||
name?: string;
|
||||
gender?: number;
|
||||
gender?: string;
|
||||
}>({});
|
||||
const userProfileFormRef = ref();
|
||||
|
||||
@@ -137,7 +158,7 @@ const handleOpenDialog = () => {
|
||||
dialog.visible = true;
|
||||
// 初始化表单数据
|
||||
userProfileForm.name = userProfile.value?.name || "";
|
||||
userProfileForm.gender = userProfile.value?.gender || 1;
|
||||
userProfileForm.gender = userProfile.value?.gender || "0";
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
@@ -207,16 +228,19 @@ function handleBack() {
|
||||
:deep(.wd-cell__body) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
|
||||
.img {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 50%;
|
||||
|
||||
.img-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -229,6 +253,7 @@ function handleBack() {
|
||||
|
||||
.edit-form {
|
||||
padding-top: 40rpx;
|
||||
|
||||
.ef-radio-group {
|
||||
line-height: 1;
|
||||
text-align: left;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="账号和安全" left-arrow @click-left="handleBack" />
|
||||
<wd-navbar
|
||||
title="账号和安全"
|
||||
left-arrow
|
||||
placeholder
|
||||
safe-area-inset-top
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<wd-cell-group border>
|
||||
@@ -93,6 +99,8 @@ const rules = reactive({
|
||||
|
||||
enum DialogType {
|
||||
PASSWORD = "password",
|
||||
MOBILE = "mobile",
|
||||
EMAIL = "email",
|
||||
}
|
||||
|
||||
const dialog = reactive({
|
||||
@@ -152,4 +160,12 @@ onMounted(() => {
|
||||
loadUserProfile();
|
||||
});
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "account",
|
||||
"style": { "navigationBarTitleText": "账号和安全" },
|
||||
"layout": "tabbar"
|
||||
}
|
||||
</route>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="用户协议" left-arrow @click-left="handleBack" />
|
||||
<wd-navbar
|
||||
title="用户协议"
|
||||
left-arrow
|
||||
placeholder
|
||||
safe-area-inset-top
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<view class="flex-col-center py-4">
|
||||
@@ -80,5 +86,12 @@ const handleAgree = () => {
|
||||
}, 1500);
|
||||
};
|
||||
</script>
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "agreement",
|
||||
"style": { "navigationBarTitleText": "用户协议" },
|
||||
"layout": "tabbar"
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,31 +1,18 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="设置" left-arrow @click-left="handleBack" />
|
||||
|
||||
<wd-cell-group custom-style="margin-top: 20rpx">
|
||||
<wd-cell v-if="isLogin" title="个人资料" icon="user" is-link @click="navigateToProfile" />
|
||||
<wd-cell
|
||||
v-if="isLogin"
|
||||
title="账号和安全"
|
||||
icon="secured"
|
||||
is-link
|
||||
@click="navigateToAccount"
|
||||
/>
|
||||
<wd-cell v-if="isLogin" title="账号和安全" icon="secured" is-link @click="navigateToAccount" />
|
||||
<wd-cell title="主题设置" icon="setting1" is-link @click="navigateToTheme" />
|
||||
<wd-cell title="用户协议" icon="user" is-link @click="navigateToUserAgreement" />
|
||||
<wd-cell title="隐私政策" icon="folder" is-link @click="navigateToPrivacy" />
|
||||
<wd-cell title="关于我们" icon="info-circle" is-link @click="navigateToAbout" />
|
||||
<wd-cell title="进入官网" icon="internet" is-link @click="navigateToOfficialWebsite" />
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-cell-group custom-style="margin-top:40rpx">
|
||||
<wd-cell title="网络测试" icon="wifi" is-link @click="navigateToNetworkTest" />
|
||||
<wd-cell
|
||||
title="清空缓存"
|
||||
icon="delete1"
|
||||
:value="cacheSize"
|
||||
clickable
|
||||
@click="handleClearCache"
|
||||
/>
|
||||
<wd-cell title="清空缓存" icon="delete1" :value="cacheSize" clickable @click="handleClearCache" />
|
||||
</wd-cell-group>
|
||||
|
||||
<view v-if="isLogin" class="logout-section">
|
||||
@@ -83,6 +70,13 @@ const navigateToUserAgreement = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 隐私政策
|
||||
const navigateToPrivacy = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/privacy/index",
|
||||
});
|
||||
};
|
||||
|
||||
// 关于我们
|
||||
const navigateToAbout = () => {
|
||||
uni.navigateTo({
|
||||
@@ -92,15 +86,13 @@ const navigateToAbout = () => {
|
||||
|
||||
// 进入官网
|
||||
const navigateToOfficialWebsite = () => {
|
||||
uni.navigateTo({
|
||||
url: "https://service.fastapiadmin.com",
|
||||
});
|
||||
// plus.runtime.openURL("https://service.fastapiadmin.com");
|
||||
window.open("https://service.fastapiadmin.com", "_blank");
|
||||
};
|
||||
|
||||
// 网络测试
|
||||
const navigateToNetworkTest = () => {
|
||||
// #ifdef H5
|
||||
window.open("https://service.fastapiadmin.com", "_blank");
|
||||
uni.navigateTo({ url: "/pages/mine/settings/network/index" });
|
||||
};
|
||||
|
||||
// 是否正在清理
|
||||
@@ -201,16 +193,21 @@ const handleLogout = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 检查登录状态
|
||||
onLoad(() => {
|
||||
getCacheSize();
|
||||
});
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "settings",
|
||||
"style": {
|
||||
"navigationBarTitleText": "设置"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logout-section {
|
||||
display: flex;
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar title="网络测试" left-arrow @click-left="handleBack" />
|
||||
<view class="app-container dark:text-[var(--wot-dark-color)]">
|
||||
<wd-navbar
|
||||
title="网络测试"
|
||||
left-arrow
|
||||
placeholder
|
||||
safe-area-inset-top
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 网络状态展示 -->
|
||||
<wd-card title="网络状态" custom-style="margin: 20rpx">
|
||||
<wd-card custom-style="margin: 20rpx">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="网络状态">
|
||||
<wd-tag :type="networkType ? 'success' : 'danger'" size="small">
|
||||
@@ -207,6 +213,15 @@ onBeforeUnmount(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "network",
|
||||
"style": {
|
||||
"navigationBarTitleText": "网络测试"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mr-10 {
|
||||
margin-right: 10rpx;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-navbar
|
||||
title="隐私政策"
|
||||
left-arrow
|
||||
placeholder
|
||||
safe-area-inset-top
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<view class="flex-col-center py-4">
|
||||
<text class="text-xl font-bold mb-2">隐私政策</text>
|
||||
<text class="text-sm text-gray-500">更新日期:2024年3月15日</text>
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<wd-collapse v-model="activeNames" accordion>
|
||||
<wd-collapse-item
|
||||
v-for="(section, index) in privacyContent"
|
||||
:key="index"
|
||||
:title="section.title"
|
||||
:name="String(index)"
|
||||
>
|
||||
<view class="py-3 px-4">
|
||||
<text class="text-base leading-relaxed text-gray-600">{{ section.content }}</text>
|
||||
</view>
|
||||
</wd-collapse-item>
|
||||
</wd-collapse>
|
||||
|
||||
<view class="mt-6 px-4">
|
||||
<wd-button type="primary" block @click="handleAgree">我已阅读并同意</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const activeNames = ref(["0"]); // 默认展开第一项
|
||||
|
||||
const privacyContent = ref([
|
||||
{
|
||||
title: "1. 信息收集",
|
||||
content:
|
||||
"我们可能收集您的基本信息(如设备信息、操作日志等)以提供更好的服务体验。我们承诺对这些信息进行严格保密,并只用于改善产品服务。",
|
||||
},
|
||||
{
|
||||
title: "2. 信息使用",
|
||||
content:
|
||||
"收集的信息将用于:优化用户体验、提供客户支持、发送重要通知、保障账号安全等。未经您的同意,我们不会向第三方分享您的个人信息。",
|
||||
},
|
||||
{
|
||||
title: "3. 信息安全",
|
||||
content:
|
||||
"我们采用业界标准的安全技术和程序来保护您的个人信息,防止未经授权的访问、使用或泄露。我们定期审查信息收集、存储和处理实践。",
|
||||
},
|
||||
{
|
||||
title: "4. Cookie 使用",
|
||||
content:
|
||||
"我们使用 Cookie 和类似技术来提供、保护和改进我们的产品和服务。这些技术帮助我们了解用户行为,告诉我们哪些功能最受欢迎。",
|
||||
},
|
||||
{
|
||||
title: "5. 第三方服务",
|
||||
content:
|
||||
"我们的应用可能包含第三方服务。这些第三方服务有自己的隐私政策,我们建议您查看这些政策。我们不对第三方的隐私实践负责。",
|
||||
},
|
||||
{
|
||||
title: "6. 未成年人保护",
|
||||
content:
|
||||
"我们非常重视对未成年人个人信息的保护。若您是未成年人,请在监护人指导下使用我们的服务。如果您是监护人,当您对您所监护的未成年人的个人信息有疑问时,请联系我们。",
|
||||
},
|
||||
{
|
||||
title: "7. 隐私政策更新",
|
||||
content:
|
||||
"我们可能会不时更新本隐私政策。当我们更新隐私政策时,我们会在本页面上发布更新后的版本并修改更新日期。建议您定期查看本页面。",
|
||||
},
|
||||
]);
|
||||
|
||||
// 同意协议
|
||||
const handleAgree = () => {
|
||||
uni.showToast({
|
||||
title: "感谢您的支持",
|
||||
icon: "success",
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
</script>
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "privacy",
|
||||
"style": { "navigationBarTitleText": "隐私政策" },
|
||||
"layout": "tabbar"
|
||||
}
|
||||
</route>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,118 +1,142 @@
|
||||
<template>
|
||||
<!-- 内容区域 -->
|
||||
<view class="app-container">
|
||||
<wd-navbar title="主题设置" left-arrow @click-left="handleBack" />
|
||||
|
||||
<view class="app-container dark:text-[var(--wot-dark-color)]">
|
||||
<wd-navbar
|
||||
title="主题设置"
|
||||
left-arrow
|
||||
placeholder
|
||||
safe-area-inset-top
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
<!-- 页面标题 -->
|
||||
<wd-card custom-class="page-header">
|
||||
<view class="page-header">
|
||||
<text class="page-title">主题设置</text>
|
||||
<view class="page-subtitle">个性化您的应用外观</view>
|
||||
</wd-card>
|
||||
<text class="page-subtitle">个性化您的应用外观</text>
|
||||
</view>
|
||||
|
||||
<!-- 暗黑模式设置 -->
|
||||
<wd-card class="mb-3">
|
||||
<view class="flex-between py-2">
|
||||
<view>
|
||||
<text class="font-medium">暗黑模式</text>
|
||||
</view>
|
||||
<wd-switch v-model:model-value="isDark" active-color="var(--wot-color-theme)" @change="toggleTheme" />
|
||||
<wd-card class="setting-section">
|
||||
<view class="section-header">
|
||||
<wd-icon name="moon" size="20" :color="isDarkMode ? '#FFD700' : '#666'" />
|
||||
<text class="section-title">外观模式</text>
|
||||
</view>
|
||||
<wd-cell title="暗黑模式" :value="isDarkMode ? '已开启' : '已关闭'">
|
||||
<wd-switch :model-value="isDarkMode" @change="handleToggleDarkMode" />
|
||||
</wd-cell>
|
||||
</wd-card>
|
||||
|
||||
<!-- 跟随系统主题 -->
|
||||
<wd-card class="mb-3">
|
||||
<view class="flex-between py-2">
|
||||
<view>
|
||||
<text class="font-medium">跟随系统</text>
|
||||
<!-- 主题色设置 -->
|
||||
<wd-card class="setting-section">
|
||||
<view class="section-header">
|
||||
<wd-icon name="palette" size="20" color="#666" />
|
||||
<text class="section-title">主题色彩</text>
|
||||
</view>
|
||||
<wd-switch :model-value="followSystem" active-color="var(--wot-color-theme)" @change="setFollowSystem" />
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<!-- 主题色选择 -->
|
||||
<wd-card title="主题色" class="mb-3">
|
||||
<!-- 预设颜色选择 -->
|
||||
<view class="color-section">
|
||||
<text class="color-label">预设主题色</text>
|
||||
<view class="color-grid">
|
||||
<view v-for="item in themeColorOptions" :key="item.value" class="color-item"
|
||||
:class="{ active: currentThemeColor.value === item.value }" @click="handleSelectColor(item)">
|
||||
<view class="color-box" :style="{ backgroundColor: item.primary }">
|
||||
<wd-icon v-if="currentThemeColor.value === item.value" name="check" size="16" color="#fff" />
|
||||
<view
|
||||
v-for="(color, index) in themeColorOptions"
|
||||
:key="index"
|
||||
class="color-item"
|
||||
:class="{ active: currentThemeColor === color.primary }"
|
||||
@click="handleSelectColor(color)"
|
||||
>
|
||||
<view
|
||||
class="color-preview"
|
||||
:style="{
|
||||
backgroundColor: color.primary,
|
||||
border:
|
||||
currentThemeColor === color.primary ? '3px solid #fff' : '1px solid #e0e0e0',
|
||||
}"
|
||||
>
|
||||
<text v-if="currentThemeColor === color.primary" class="check-icon">✓</text>
|
||||
</view>
|
||||
<text class="color-name">{{ color.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 当前主题色显示 -->
|
||||
<view class="current-theme-section">
|
||||
<view class="current-theme-item">
|
||||
<text class="current-theme-label">当前主题色</text>
|
||||
<view class="current-theme-value">
|
||||
<view
|
||||
class="current-color-preview"
|
||||
:style="{ backgroundColor: currentThemeColor }"
|
||||
></view>
|
||||
<text class="current-color-text">{{ currentThemeColor }}</text>
|
||||
</view>
|
||||
<text class="color-label">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<!-- 自定义颜色 -->
|
||||
<wd-card class="mb-3">
|
||||
<view class="flex-between items-center py-2" @click="showCustomColorPopup = true">
|
||||
<view class="flex-start gap-2 items-center">
|
||||
<wd-icon name="edit" size="20" :color="currentThemeColor.primary" />
|
||||
<view>
|
||||
<text class="font-medium">自定义颜色</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-start gap-2 items-center">
|
||||
<view class="color-box small" :style="{ backgroundColor: currentThemeColor.primary }"></view>
|
||||
<text class="text-sm text-gray-500 font-mono">{{ currentThemeColor.primary }}</text>
|
||||
<wd-icon name="arrow-right" size="14" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
<wd-cell title="自定义颜色" is-link @click="showCustomInput">
|
||||
<wd-icon name="edit" size="16" color="#999" />
|
||||
</wd-cell>
|
||||
</wd-card>
|
||||
|
||||
<!-- 预览效果 -->
|
||||
<wd-card title="预览效果" class="mb-3">
|
||||
<view class="py-4">
|
||||
<view class="flex-start gap-3 mb-4">
|
||||
<wd-button type="primary" size="small">主要按钮</wd-button>
|
||||
<wd-button type="primary" plain size="small">次要按钮</wd-button>
|
||||
<wd-tag type="primary">标签</wd-tag>
|
||||
</view>
|
||||
<view class="preview-card" :style="{ backgroundColor: currentThemeColor.primary + '20' }">
|
||||
<text class="text-sm text-gray-600">当前主题色预览</text>
|
||||
<view class="mt-2 h-8 rounded" :style="{ backgroundColor: currentThemeColor.primary }"></view>
|
||||
</view>
|
||||
<!-- 预览区域 -->
|
||||
<wd-card class="setting-section">
|
||||
<view class="section-header">
|
||||
<wd-icon name="eye" size="20" />
|
||||
<text class="section-title">效果预览</text>
|
||||
</view>
|
||||
|
||||
<wd-divider />
|
||||
|
||||
<wd-grid :column="2" border>
|
||||
<wd-grid-item use-slot>
|
||||
<wd-button size="small" type="primary">按钮</wd-button>
|
||||
</wd-grid-item>
|
||||
<wd-grid-item use-slot>
|
||||
<wd-button size="small" plain :style="{ borderColor: currentThemeColor }">边框</wd-button>
|
||||
</wd-grid-item>
|
||||
<wd-grid-item use-slot>
|
||||
<wd-text class="preview-text" :style="{ color: currentThemeColor }" text="文本"></wd-text>
|
||||
</wd-grid-item>
|
||||
|
||||
<wd-grid-item use-slot>
|
||||
<wd-tag type="primary" mark>标签</wd-tag>
|
||||
</wd-grid-item>
|
||||
</wd-grid>
|
||||
</wd-card>
|
||||
|
||||
<!-- 重置按钮 -->
|
||||
<view class="mt-5 mx-3">
|
||||
<wd-button plain block :disabled="currentThemeColor.value === themeColorOptions[0].value &&
|
||||
theme === 'light' &&
|
||||
followSystem
|
||||
" @click="handleReset">
|
||||
恢复默认
|
||||
</wd-button>
|
||||
</view>
|
||||
<!-- 操作按钮 -->
|
||||
<wd-card class="action-section">
|
||||
<wd-button size="large" block @click="handleResetTheme">重置为默认主题</wd-button>
|
||||
</wd-card>
|
||||
|
||||
<!-- 自定义颜色弹窗 -->
|
||||
<wd-popup v-model="showCustomColorPopup" position="bottom" closeable>
|
||||
<!-- 自定义颜色输入弹窗 -->
|
||||
<wd-popup v-model="showCustomColorInput" position="bottom" :safe-area-inset-bottom="true">
|
||||
<view class="custom-color-popup">
|
||||
<view class="text-center mb-5">
|
||||
<text class="text-lg font-bold">自定义主题色</text>
|
||||
<text class="block text-sm text-gray-500 mt-1">输入任意 HEX 颜色值</text>
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">自定义主题色</text>
|
||||
<wd-icon name="close" size="20" color="#999" @click="showCustomColorInput = false" />
|
||||
</view>
|
||||
|
||||
<view class="mb-5">
|
||||
<view class="color-preview-large mb-4" :style="{ backgroundColor: customColor }"></view>
|
||||
<wd-divider />
|
||||
|
||||
<view class="mb-3">
|
||||
<text class="text-sm font-medium mb-2 block">颜色值</text>
|
||||
<wd-input v-model="customColor" placeholder="例如: #FF6B6B 或 #F00" clearable :maxlength="7" />
|
||||
<view class="color-input-section">
|
||||
<view class="input-label">请输入十六进制颜色值</view>
|
||||
<view class="input-container">
|
||||
<view class="color-preview-small" :style="{ backgroundColor: customColor }"></view>
|
||||
<wd-input
|
||||
v-model="customColor"
|
||||
placeholder="例如: #165DFF"
|
||||
:maxlength="7"
|
||||
class="color-input"
|
||||
/>
|
||||
</view>
|
||||
<view class="input-tip">支持格式:#RGB 或 #RRGGBB</view>
|
||||
</view>
|
||||
|
||||
<view class="grid grid-cols-6 gap-2 mb-3">
|
||||
<view v-for="color in quickColors" :key="color" class="w-10 h-10 rounded cursor-pointer"
|
||||
:style="{ backgroundColor: color }" @click="customColor = color"></view>
|
||||
</view>
|
||||
<wd-divider />
|
||||
|
||||
<text class="input-tip">支持 #RRGGBB 或 #RGB 格式</text>
|
||||
</view>
|
||||
|
||||
<view class="flex gap-2">
|
||||
<wd-button type="info" block @click="showCustomColorPopup = false">取消</wd-button>
|
||||
<wd-button type="primary" block :disabled="!isValidColor(customColor)" @click="applyCustomColor">
|
||||
应用
|
||||
</wd-button>
|
||||
<view class="popup-actions">
|
||||
<wd-button type="info" size="large" @click="showCustomColorInput = false">取消</wd-button>
|
||||
<wd-button type="primary" size="large" @click="applyCustomColor">应用</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
@@ -120,155 +144,147 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { onShow, onLoad } from "@dcloudio/uni-app";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import type { ThemeColorOption } from "@/composables/useTheme";
|
||||
|
||||
const {
|
||||
theme,
|
||||
currentThemeColor,
|
||||
themeColorOptions,
|
||||
toggleTheme,
|
||||
selectThemeColor,
|
||||
resetTheme,
|
||||
setCustomThemeColor,
|
||||
followSystem,
|
||||
setFollowSystem,
|
||||
isDark,
|
||||
} = useTheme();
|
||||
// 使用主题组合函数
|
||||
const { isDark, themeVars, themeColorOptions, toggleTheme, setThemeColor } = useTheme();
|
||||
|
||||
// 自定义颜色相关
|
||||
const showCustomColorPopup = ref(false);
|
||||
const customColor = ref(currentThemeColor.value.primary);
|
||||
// 创建响应式的计算属性
|
||||
const isDarkMode = computed(() => isDark.value);
|
||||
|
||||
// 快速颜色选择
|
||||
const quickColors = [
|
||||
"#FF6B6B",
|
||||
"#4ECDC4",
|
||||
"#45B7D1",
|
||||
"#96CEB4",
|
||||
"#FECA57",
|
||||
"#FF9FF3",
|
||||
"#54A0FF",
|
||||
"#5F27CD",
|
||||
"#00D2D3",
|
||||
"#FF9F43",
|
||||
"#10AC84",
|
||||
"#EE5A24",
|
||||
"#009432",
|
||||
"#0652DD",
|
||||
"#9980FA",
|
||||
];
|
||||
// 自定义颜色输入
|
||||
const customColor = ref("");
|
||||
const showCustomColorInput = ref(false);
|
||||
|
||||
// 动态设置页面标题
|
||||
onLoad(() => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: "主题设置",
|
||||
});
|
||||
// 当前选中的主题色
|
||||
const currentThemeColor = computed(() => {
|
||||
return themeVars.value.colorTheme || themeColorOptions.value[0].primary;
|
||||
});
|
||||
|
||||
// 监听主题变化,确保实时生效
|
||||
onShow(() => {
|
||||
// 强制应用当前主题色
|
||||
setTimeout(() => {
|
||||
customColor.value = currentThemeColor.value.primary;
|
||||
}, 50);
|
||||
});
|
||||
// 选择预设颜色
|
||||
const handleSelectColor = (color: (typeof themeColorOptions.value)[0]) => {
|
||||
setThemeColor(color);
|
||||
customColor.value = color.primary;
|
||||
|
||||
// 监听主题更新事件
|
||||
onMounted(() => {
|
||||
uni.$on('theme-color-changed', (color: string) => {
|
||||
customColor.value = color;
|
||||
});
|
||||
// 提示
|
||||
uni.showToast({
|
||||
title: "主题色已更新",
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off('theme-color-changed');
|
||||
});
|
||||
|
||||
// 验证颜色格式
|
||||
const isValidColor = (color: string): boolean => {
|
||||
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color);
|
||||
// 显示自定义颜色输入
|
||||
const showCustomInput = () => {
|
||||
showCustomColorInput.value = true;
|
||||
customColor.value = currentThemeColor.value;
|
||||
};
|
||||
|
||||
// 应用自定义颜色
|
||||
const applyCustomColor = () => {
|
||||
if (!isValidColor(customColor.value)) {
|
||||
// 验证颜色格式
|
||||
const colorRegex = /^#([0-9A-F]{6}|[0-9A-F]{3})$/i;
|
||||
if (!colorRegex.test(customColor.value)) {
|
||||
uni.showToast({
|
||||
title: "请输入正确的颜色格式",
|
||||
title: "请输入有效的颜色值",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setCustomThemeColor(customColor.value);
|
||||
showCustomColorPopup.value = false;
|
||||
uni.showToast({
|
||||
title: "主题颜色已更新",
|
||||
icon: "success",
|
||||
});
|
||||
// 转换3位颜色值为6位
|
||||
let color = customColor.value;
|
||||
if (color.length === 4) {
|
||||
color = "#" + color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
|
||||
}
|
||||
|
||||
// 强制刷新当前页面样式
|
||||
setTimeout(() => {
|
||||
uni.$emit('theme-updated');
|
||||
}, 50);
|
||||
// 创建自定义主题色选项
|
||||
const customColorOption = {
|
||||
name: "自定义",
|
||||
value: "custom",
|
||||
primary: color,
|
||||
};
|
||||
|
||||
// 选择主题色
|
||||
const handleSelectColor = (colorOption: ThemeColorOption) => {
|
||||
selectThemeColor(colorOption);
|
||||
uni.showToast({
|
||||
title: "主题色已更新",
|
||||
icon: "success",
|
||||
});
|
||||
setThemeColor(customColorOption);
|
||||
showCustomColorInput.value = false;
|
||||
|
||||
// 强制刷新当前页面样式
|
||||
setTimeout(() => {
|
||||
customColor.value = currentThemeColor.value.primary;
|
||||
uni.$emit('theme-updated');
|
||||
}, 50);
|
||||
// 提示
|
||||
uni.showToast({
|
||||
title: "自定义主题色已应用",
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
};
|
||||
|
||||
// 重置主题
|
||||
const handleReset = () => {
|
||||
// 重置为默认主题
|
||||
const handleResetTheme = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要恢复默认主题吗?",
|
||||
title: "确认重置",
|
||||
content: "确定要重置为默认主题吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
resetTheme();
|
||||
customColor.value = currentThemeColor.value.primary;
|
||||
uni.showToast({
|
||||
title: "已恢复默认",
|
||||
icon: "success",
|
||||
});
|
||||
setThemeColor(themeColorOptions.value[0]);
|
||||
customColor.value = themeColorOptions.value[0].primary;
|
||||
|
||||
// 强制刷新当前页面样式
|
||||
setTimeout(() => {
|
||||
uni.$emit('theme-updated');
|
||||
}, 50);
|
||||
uni.showToast({
|
||||
title: "已重置为默认主题",
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 处理返回按钮点击
|
||||
// 切换暗黑模式
|
||||
const handleToggleDarkMode = () => {
|
||||
toggleTheme();
|
||||
nextTick(() => {
|
||||
uni.showToast({
|
||||
title: `已切换到${isDarkMode.value ? "暗黑" : "浅色"}模式`,
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
|
||||
// 页面显示时确保主题色同步
|
||||
onShow(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
// 页面显示时更新自定义颜色值
|
||||
onShow(() => {
|
||||
customColor.value = currentThemeColor.value.primary;
|
||||
});
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "theme",
|
||||
"style": {
|
||||
"navigationBarTitleText": "主题设置"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 基础布局
|
||||
.page-header {
|
||||
margin-top: 20rpx;
|
||||
padding: 40rpx 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, var(--wot-color-theme) 0%, var(--primary-color-light) 100%);
|
||||
background: linear-gradient(135deg, var(--wot-color-theme, #165dff) 0%, #667eea 100%);
|
||||
border-radius: 16rpx;
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
@@ -284,210 +300,195 @@ onShow(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx 30rpx 20rpx;
|
||||
border-bottom: 1rpx solid var(--wot-color-border, #f0f0f0);
|
||||
|
||||
.section-title {
|
||||
margin-left: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text, #333);
|
||||
}
|
||||
}
|
||||
|
||||
// 颜色选择区域
|
||||
.color-section {
|
||||
padding: 30rpx;
|
||||
|
||||
.color-label {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--wot-color-text-secondary, #666);
|
||||
}
|
||||
|
||||
.color-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24rpx;
|
||||
padding: 32rpx 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.color-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
width: calc(25% - 15rpx);
|
||||
padding: 10rpx;
|
||||
cursor: pointer;
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
color: white;
|
||||
|
||||
.color-label {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.color-box {
|
||||
position: relative;
|
||||
.color-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-bottom: 8rpx;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.small {
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.ring-2 {
|
||||
box-shadow:
|
||||
0 0 0 2rpx var(--wot-color-theme),
|
||||
0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.color-label {
|
||||
.check-icon {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.color-name {
|
||||
font-size: 22rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
transition: color 0.2s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.active .color-preview {
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:active .color-preview {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-color-popup {
|
||||
padding: 48rpx 40rpx;
|
||||
background-color: var(--wot-color-bg);
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
// 当前主题色显示
|
||||
.current-theme-section {
|
||||
padding: 30rpx;
|
||||
|
||||
.color-preview-large {
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
border: 2rpx solid var(--wot-color-border);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
padding: 24rpx;
|
||||
border: 2rpx solid var(--wot-color-border);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.input-tip {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-6 {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
.current-theme-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.current-theme-label {
|
||||
font-size: 28rpx;
|
||||
color: var(--wot-color-text-secondary, #666);
|
||||
}
|
||||
|
||||
.flex-start {
|
||||
.current-theme-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.py-4 {
|
||||
padding-top: 32rpx;
|
||||
padding-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: var(--wot-color-secondary);
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: var(--wot-color-secondary);
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family:
|
||||
"ui-monospace", SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.w-8 {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
.w-10 {
|
||||
width: 80rpx;
|
||||
}
|
||||
|
||||
.h-8 {
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.h-10 {
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
.current-color-preview {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 2rpx solid var(--wot-color-border, #f0f0f0);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.rounded-full {
|
||||
border-radius: 50%;
|
||||
.current-color-text {
|
||||
margin-left: 10rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ring-2 {
|
||||
--tw-ring-offset-width: 2px;
|
||||
// 效果预览
|
||||
.preview-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ring-offset-2 {
|
||||
--tw-ring-offset-width: 2px;
|
||||
.preview-border {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 200rpx;
|
||||
height: 60rpx;
|
||||
font-size: 26rpx;
|
||||
color: var(--wot-color-text-secondary, #666);
|
||||
border: 2rpx solid;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.ring-current {
|
||||
--tw-ring-color: currentColor;
|
||||
// 自定义颜色弹窗
|
||||
.custom-color-popup {
|
||||
padding: 40rpx 30rpx;
|
||||
background: var(--wot-popup-bg-color, #fff);
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text, #333);
|
||||
}
|
||||
}
|
||||
|
||||
.shadow-sm {
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
.color-input-section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.input-label {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--wot-color-text-secondary, #666);
|
||||
}
|
||||
|
||||
.transition-all {
|
||||
transition: all 0.2s ease;
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
.color-preview-small {
|
||||
flex-shrink: 0;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 2rpx solid var(--wot-color-border, #f0f0f0);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.duration-200 {
|
||||
transition-duration: 200ms;
|
||||
.color-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ease-in-out {
|
||||
transition-timing-function: ease-in-out;
|
||||
.input-tip {
|
||||
margin-left: 80rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-text-placeholder, #999);
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,309 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-status-tip type="search" tip="建设中..." />
|
||||
<view class="workbench-container">
|
||||
<!-- 顶部统计卡片 -->
|
||||
<view class="stats-section">
|
||||
<view class="stats-grid">
|
||||
<view v-for="(stat, index) in statsData" :key="index" class="stat-card">
|
||||
<view class="stat-icon" :style="{ backgroundColor: stat.color + '20' }">
|
||||
<wd-icon :name="stat.icon" :color="stat.color" size="24" />
|
||||
</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-number">{{ stat.number }}</text>
|
||||
<text class="stat-label">{{ stat.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<view class="quick-actions-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">快捷入口</text>
|
||||
<text class="section-subtitle">常用功能一键直达</text>
|
||||
</view>
|
||||
<view class="actions-grid">
|
||||
<view class="action-item" v-for="(action, index) in quickActions" :key="index" @tap="handleQuickAction(action)">
|
||||
<view class="action-icon" :style="{ backgroundColor: action.color + '15' }">
|
||||
<wd-icon :name="action.icon" :color="action.color" size="28" />
|
||||
</view>
|
||||
<text class="action-name">{{ action.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待办事项 -->
|
||||
<view class="todo-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">待办事项</text>
|
||||
<text class="section-subtitle">{{ pendingTodos.length }} 项待处理</text>
|
||||
</view>
|
||||
<view class="todo-list">
|
||||
<view class="todo-item" v-for="(todo, index) in pendingTodos" :key="index" @tap="handleTodoItem(todo)">
|
||||
<view class="todo-priority" :class="'priority-' + todo.priority">
|
||||
<view class="priority-dot"></view>
|
||||
</view>
|
||||
<view class="todo-content">
|
||||
<text class="todo-title">{{ todo.title }}</text>
|
||||
<text class="todo-time">{{ todo.time }}</text>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" color="#999" size="16" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="pendingTodos.length === 0" class="empty-todos">
|
||||
<wd-icon name="smile" color="#999" size="48" />
|
||||
<text class="empty-text">暂无待办事项</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
// 统计数据
|
||||
const statsData = ref([
|
||||
{ icon: "user", number: "1,234", label: "用户总数", color: "#165DFF" },
|
||||
{ icon: "chart-bar", number: "856", label: "今日活跃", color: "#00B42A" },
|
||||
{ icon: "calendar", number: "42", label: "待处理订单", color: "#FF7D00" },
|
||||
{ icon: "warning", number: "8", label: "系统告警", color: "#F53F3F" },
|
||||
]);
|
||||
|
||||
// 快捷入口
|
||||
const quickActions = ref([
|
||||
{ name: "用户管理", icon: "user", color: "#165DFF", path: "/pages/work/user/index" },
|
||||
{ name: "角色管理", icon: "usergroup", color: "#00B42A", path: "/pages/work/role/index" },
|
||||
{ name: "菜单管理", icon: "app", color: "#FF7D00", path: "/pages/work/menu/index" },
|
||||
{ name: "部门管理", icon: "fork", color: "#FFC53D", path: "/pages/work/department/index" },
|
||||
{ name: "岗位管理", icon: "user-avatar", color: "#52C41A", path: "/pages/work/job/index" },
|
||||
{ name: "日志管理", icon: "link", color: "#13C2C2", path: "/pages/work/log/index" },
|
||||
{ name: "系统配置", icon: "setting", color: "#722ED1", path: "/pages/work/settings/index" },
|
||||
{ name: "字典管理", icon: "books", color: "#FA541C", path: "/pages/work/dictionary/index" },
|
||||
{ name: "任务管理", icon: "clock", color: "#F5222D", path: "/pages/work/task/index" },
|
||||
{ name: "通知公告", icon: "notification", color: "#FAAD14", path: "/pages/work/notice/index" },
|
||||
{ name: "帮助中心", icon: "help", color: "#2F54EB", path: "/pages/work/help/index" },
|
||||
{ name: "系统监控", icon: "dashboard", color: "#B37FEB", path: "/pages/work/monitor/index" },
|
||||
]);
|
||||
|
||||
// 待办事项
|
||||
const pendingTodos = ref([
|
||||
{ title: "审核新用户注册申请", time: "2小时前", priority: "high", type: "user" },
|
||||
{ title: "处理订单退款请求", time: "3小时前", priority: "medium", type: "order" },
|
||||
{ title: "更新系统权限配置", time: "1天前", priority: "low", type: "config" },
|
||||
{ title: "回复用户反馈", time: "2天前", priority: "medium", type: "feedback" },
|
||||
]);
|
||||
|
||||
// 处理方法
|
||||
const handleQuickAction = (action: any) => {
|
||||
uni.navigateTo({
|
||||
url: action.path,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTodoItem = (todo: any) => {
|
||||
uni.showToast({
|
||||
title: `处理:${todo.title}`,
|
||||
icon: "none",
|
||||
});
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const loadWorkbenchData = async () => {
|
||||
// 这里可以添加实际的API调用
|
||||
console.log("加载工作台数据...");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadWorkbenchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "index",
|
||||
"name": "work",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作台"
|
||||
"navigationStyle": "custom"
|
||||
},
|
||||
"layout": "tabbar",
|
||||
"meta": {
|
||||
"requireAuth": true
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss" scoped>
|
||||
.workbench-container {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.quick-actions-section,
|
||||
.todo-section,
|
||||
.recent-section {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-bottom: 16rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.action-name {
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.todo-priority {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.priority-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.priority-high .priority-dot {
|
||||
background-color: #f53f3f;
|
||||
}
|
||||
|
||||
.priority-medium .priority-dot {
|
||||
background-color: #ff7d00;
|
||||
}
|
||||
|
||||
.priority-low .priority-dot {
|
||||
background-color: #00b42a;
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.todo-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.todo-time {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.empty-todos {
|
||||
padding: 60rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
display: block;
|
||||
margin-top: 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
// 深色模式适配
|
||||
:deep(.dark) .workbench-container {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
:deep(.dark) .stat-card,
|
||||
:deep(.dark) .quick-actions-section,
|
||||
:deep(.dark) .todo-section {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
:deep(.dark) .todo-item {
|
||||
background-color: #333;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,9 @@ import { createRouter } from "uni-mini-router";
|
||||
function generateRoutes() {
|
||||
const routes = pages.map((page: { path: string; [key: string]: any }) => {
|
||||
const newPath = `/${page.path}`;
|
||||
return { ...page, path: newPath };
|
||||
// 透传 meta 字段(如果 pages.json 中定义了)
|
||||
const meta = page.meta ?? undefined;
|
||||
return { ...page, path: newPath, meta };
|
||||
});
|
||||
|
||||
// 处理分包路由
|
||||
@@ -14,7 +16,8 @@ function generateRoutes() {
|
||||
subPackages.forEach((subPackage: { root: string; pages: any[] }) => {
|
||||
const subRoutes = subPackage.pages.map((page: any) => {
|
||||
const newPath = `/${subPackage.root}/${page.path}`;
|
||||
return { ...page, path: newPath };
|
||||
const meta = page.meta ?? undefined;
|
||||
return { ...page, path: newPath, meta };
|
||||
});
|
||||
routes.push(...subRoutes);
|
||||
});
|
||||
@@ -62,8 +65,8 @@ router.beforeEach((to, from, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
console.log("路由跳转完成:", to.path);
|
||||
router.afterEach((to, from) => {
|
||||
console.log("🎯 afterEach 钩子触发:", { to, from });
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 背景渐变 -->
|
||||
<defs>
|
||||
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#94BFFF" />
|
||||
<stop offset="100%" stop-color="#165DFF" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 图形渐变 -->
|
||||
<linearGradient id="shapeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.3" />
|
||||
<stop offset="100%" stop-color="#ffffff" stop-opacity="0.15" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 透明背景,不完全填充 -->
|
||||
<rect width="100%" height="50%" fill="url(#bgGradient)" />
|
||||
|
||||
<!-- 左侧方块装饰 -->
|
||||
<rect x="100" y="150" width="120" height="120" rx="15" fill="url(#shapeGradient)" transform="rotate(-10, 160, 210)" opacity="0.7" />
|
||||
<rect x="190" y="90" width="80" height="80" rx="10" fill="url(#shapeGradient)" transform="rotate(15, 230, 130)" opacity="0.6" />
|
||||
<rect x="60" y="250" width="100" height="100" rx="10" fill="url(#shapeGradient)" transform="rotate(-5, 110, 300)" opacity="0.5" />
|
||||
|
||||
<!-- 右侧圆形装饰 -->
|
||||
<circle cx="750" cy="150" r="60" fill="url(#shapeGradient)" opacity="0.7" />
|
||||
<circle cx="820" cy="230" r="90" fill="url(#shapeGradient)" opacity="0.5" />
|
||||
<circle cx="690" cy="250" r="40" fill="url(#shapeGradient)" opacity="0.6" />
|
||||
|
||||
<!-- 底部波浪 -->
|
||||
<path d="M0,900 C200,800 350,950 550,870 C750,790 850,900 1000,850 L1000,1000 L0,1000 Z" fill="#ffffff" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,50 +1,103 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { applyThemeToMiniProgram } from "@/utils/theme";
|
||||
import type { ConfigProviderThemeVars } from "wot-design-uni";
|
||||
import { useStorage } from "@uni-helper/uni-use";
|
||||
|
||||
// 从缓存获取主题色
|
||||
const getThemeColor = (): string => {
|
||||
const savedColor = uni.getStorageSync("themeColor");
|
||||
return savedColor || "#165DFF"; // 默认Arco蓝色
|
||||
};
|
||||
// 主题色选项接口
|
||||
export interface ThemeColorOption {
|
||||
name: string;
|
||||
value: string;
|
||||
primary: string;
|
||||
}
|
||||
|
||||
// 保存主题色到缓存
|
||||
const setThemeColorCache = (color: string) => {
|
||||
uni.setStorageSync("themeColor", color);
|
||||
};
|
||||
// 主题类型
|
||||
export type ThemeMode = "light" | "dark";
|
||||
|
||||
export const useThemeStore = defineStore("appTheme", () => {
|
||||
const theme = useStorage<ThemeMode>("app-theme", "light");
|
||||
|
||||
// 预定义的主题色选项
|
||||
const themeColorOptions: ThemeColorOption[] = [
|
||||
{ name: "默认蓝", value: "blue", primary: "#4D7FFF" },
|
||||
{ name: "活力橙", value: "orange", primary: "#FF7D00" },
|
||||
{ name: "薄荷绿", value: "green", primary: "#07C160" },
|
||||
{ name: "樱花粉", value: "pink", primary: "#FF69B4" },
|
||||
{ name: "紫罗兰", value: "purple", primary: "#8A2BE2" },
|
||||
{ name: "朱砂红", value: "red", primary: "#FF4757" },
|
||||
];
|
||||
|
||||
export const useThemeStore = defineStore("theme", () => {
|
||||
// 主题色
|
||||
const primaryColor = ref<string>(getThemeColor());
|
||||
const currentThemeColor = useStorage<ThemeColorOption>("app-theme-color", themeColorOptions[0]);
|
||||
|
||||
// 设置主题色
|
||||
const setPrimaryColor = (color: string) => {
|
||||
primaryColor.value = color;
|
||||
setThemeColorCache(color);
|
||||
// 主题变量(响应式对象)
|
||||
const themeVars: ConfigProviderThemeVars = 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,
|
||||
});
|
||||
|
||||
// 检测运行环境,区分处理
|
||||
if (typeof document !== "undefined") {
|
||||
// H5环境
|
||||
document.documentElement.style.setProperty("--primary-color", color);
|
||||
document.documentElement.style.setProperty("--wot-color-theme", color);
|
||||
// 计算属性
|
||||
const isDark = computed(() => theme.value === "dark");
|
||||
|
||||
// 设置简单的衍生色(不依赖外部工具函数)
|
||||
document.documentElement.style.setProperty("--primary-color-light", color + "80"); // 添加透明度
|
||||
document.documentElement.style.setProperty("--primary-color-dark", color);
|
||||
// 切换主题, 指定主题模式,不传则自动切换
|
||||
const toggleTheme = (mode?: ThemeMode) => {
|
||||
theme.value = mode || (theme.value === "light" ? "dark" : "light");
|
||||
setNavigationBarColor();
|
||||
};
|
||||
|
||||
// 设置导航栏颜色
|
||||
const setNavigationBarColor = () => {
|
||||
// 只在非H5环境下调用setNavigationBarColor
|
||||
if (process.env.UNI_PLATFORM !== "h5") {
|
||||
console.log("设置导航栏颜色", theme.value);
|
||||
uni.setNavigationBarColor({
|
||||
frontColor: theme.value === "light" ? "#000000" : "#ffffff",
|
||||
backgroundColor: theme.value === "light" ? "#ffffff" : "#000000",
|
||||
});
|
||||
} else {
|
||||
// 小程序环境
|
||||
applyThemeToMiniProgram(color);
|
||||
console.log("H5环境下跳过设置导航栏颜色");
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化,应用主题色
|
||||
// 设置主题色
|
||||
const setCurrentThemeColor = (color: ThemeColorOption) => {
|
||||
currentThemeColor.value = color;
|
||||
themeVars.colorTheme = color.primary;
|
||||
console.log("主题色已设置:", color.name);
|
||||
};
|
||||
|
||||
// 初始化主题
|
||||
const initTheme = () => {
|
||||
setPrimaryColor(primaryColor.value);
|
||||
// 更新主题变量中的颜色
|
||||
themeVars.colorTheme = currentThemeColor.value.primary;
|
||||
|
||||
// 设置导航栏颜色
|
||||
nextTick(() => {
|
||||
setNavigationBarColor();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
primaryColor,
|
||||
setPrimaryColor,
|
||||
// 状态
|
||||
theme,
|
||||
currentThemeColor,
|
||||
themeVars,
|
||||
themeColorOptions,
|
||||
|
||||
// 计算属性
|
||||
isDark,
|
||||
|
||||
// 方法
|
||||
toggleTheme,
|
||||
setCurrentThemeColor,
|
||||
setNavigationBarColor,
|
||||
initTheme,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { defineStore } from "pinia";
|
||||
import AuthAPI, {
|
||||
type LoginFormData,
|
||||
type WxLoginData,
|
||||
type LoginResult,
|
||||
type LogoutBody,
|
||||
} from "@/api/auth";
|
||||
import UserAPI, { type UserInfo } from "@/api/user";
|
||||
import { getAccessToken, setAccessToken, clearTokens } from "@/utils/auth";
|
||||
import { getUserInfo, setUserInfo } from "@/utils/storage";
|
||||
import { USER_INFO_KEY } from "@/constants";
|
||||
import { Storage } from "@/utils/storage";
|
||||
import AuthAPI, { type LoginFormData, type LoginResult, type LogoutBody } from "@/api/auth";
|
||||
import {
|
||||
getAccessToken,
|
||||
setAccessToken,
|
||||
setRefreshToken,
|
||||
clearAll,
|
||||
getUserInfo,
|
||||
setUserInfo,
|
||||
} from "@/utils/auth";
|
||||
|
||||
export const useUserStore = defineStore("user_info", () => {
|
||||
export const useUserStore = defineStore("appUserInfo", () => {
|
||||
const userInfo = ref<UserInfo | undefined>(getUserInfo());
|
||||
const isLoggingIn = ref(false);
|
||||
|
||||
@@ -23,6 +22,7 @@ export const useUserStore = defineStore("user_info", () => {
|
||||
try {
|
||||
const result = await loginFn();
|
||||
setAccessToken(result.access_token);
|
||||
setRefreshToken(result.refresh_token);
|
||||
|
||||
// 登录成功后获取用户信息
|
||||
await getInfo();
|
||||
@@ -41,16 +41,6 @@ export const useUserStore = defineStore("user_info", () => {
|
||||
return handleLogin(() => AuthAPI.login(data), "账号密码");
|
||||
};
|
||||
|
||||
// 微信基础授权登录
|
||||
const loginWithWxCode = async (code: string) => {
|
||||
return handleLogin(() => AuthAPI.loginByWxMiniAppCode(code), "微信授权");
|
||||
};
|
||||
|
||||
// 微信手机号授权登录
|
||||
const loginWithWxPhone = async (data: WxLoginData) => {
|
||||
return handleLogin(() => AuthAPI.loginByWxMiniAppPhone(data), "微信手机号");
|
||||
};
|
||||
|
||||
// 获取用户信息
|
||||
const getInfo = async () => {
|
||||
try {
|
||||
@@ -69,14 +59,13 @@ export const useUserStore = defineStore("user_info", () => {
|
||||
const logout = async () => {
|
||||
try {
|
||||
const logoutBody: LogoutBody = {
|
||||
token: getAccessToken(),
|
||||
token: getAccessToken() || "",
|
||||
};
|
||||
await AuthAPI.logout(logoutBody); // 调用后台注销接口
|
||||
} catch (error) {
|
||||
console.error("登出失败", error);
|
||||
} finally {
|
||||
clearTokens(); // 清除本地的 token
|
||||
Storage.remove(USER_INFO_KEY); // 清除用户信息缓存
|
||||
clearAll(); // 清除本地的 token
|
||||
userInfo.value = undefined; // 清空用户信息
|
||||
// 跳转到登录页面
|
||||
uni.reLaunch({
|
||||
@@ -95,8 +84,6 @@ export const useUserStore = defineStore("user_info", () => {
|
||||
userInfo,
|
||||
isLoggingIn,
|
||||
login,
|
||||
loginWithWxCode,
|
||||
loginWithWxPhone,
|
||||
logout,
|
||||
getInfo,
|
||||
isUserInfoComplete,
|
||||
|
||||
@@ -8,4 +8,255 @@ body,
|
||||
|
||||
.app-container {
|
||||
padding: 10rpx 10rpx;
|
||||
:deep(.custom-item) {
|
||||
height: 80px !important;
|
||||
}
|
||||
:deep(.wd-card) {
|
||||
margin: 10rpx 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局样式变量
|
||||
*/
|
||||
|
||||
/*
|
||||
* 主题颜色 - 会被theme.ts中的动态设置覆盖
|
||||
* 这里作为默认值和IDE提示
|
||||
*/
|
||||
:root {
|
||||
/* 主色 */
|
||||
--primary-color: #165dff;
|
||||
--primary-color-light: #94bfff;
|
||||
--primary-color-dark: #0e3c9b;
|
||||
|
||||
/* 功能色 */
|
||||
--success-color: #0fc6c2;
|
||||
--warning-color: #ff7d00;
|
||||
--danger-color: #f5222d;
|
||||
--info-color: #86909c;
|
||||
|
||||
/* 文字颜色 */
|
||||
--text-primary: #1d2129;
|
||||
--text-regular: #4e5969;
|
||||
--text-secondary: #86909c;
|
||||
--text-placeholder: #c9cdd4;
|
||||
--text-inverse: #ffffff;
|
||||
|
||||
/* 边框颜色 */
|
||||
--border-color: #e5e6eb;
|
||||
--border-light: #f2f3f5;
|
||||
|
||||
/* 背景颜色 */
|
||||
--bg-white: #ffffff;
|
||||
--bg-light: #f2f3f5;
|
||||
--bg-gray: #f7f8fa;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题相关的通用类
|
||||
*/
|
||||
|
||||
/* 主色文本 */
|
||||
.text-primary {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* 主色背景 */
|
||||
.bg-primary {
|
||||
color: #fff;
|
||||
background-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* 主色边框 */
|
||||
.border-primary {
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* 主色按钮样式 */
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* 圆角按钮 */
|
||||
.btn-rounded {
|
||||
border-radius: 45rpx !important;
|
||||
}
|
||||
|
||||
/* 次级按钮 */
|
||||
.btn-secondary {
|
||||
color: var(--primary-color);
|
||||
background-color: #fff;
|
||||
border: 1px solid var(--primary-color);
|
||||
border-radius: 8rpx;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(22, 93, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字体大小
|
||||
*/
|
||||
.font-xs {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.font-sm {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.font-md {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.font-lg {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.font-xl {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 边距辅助类
|
||||
*/
|
||||
.mt-10 {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.mt-20 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.mt-30 {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
.mt-40 {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.mb-10 {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.mb-20 {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.mb-30 {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
.mb-40 {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.ml-10 {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
.ml-20 {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
.ml-30 {
|
||||
margin-left: 30rpx;
|
||||
}
|
||||
.ml-40 {
|
||||
margin-left: 40rpx;
|
||||
}
|
||||
|
||||
.mr-10 {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
.mr-20 {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.mr-30 {
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
.mr-40 {
|
||||
margin-right: 40rpx;
|
||||
}
|
||||
|
||||
.p-10 {
|
||||
padding: 10rpx;
|
||||
}
|
||||
.p-20 {
|
||||
padding: 20rpx;
|
||||
}
|
||||
.p-30 {
|
||||
padding: 30rpx;
|
||||
}
|
||||
.p-40 {
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 布局辅助类
|
||||
*/
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.flex-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.justify-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/**
|
||||
* 其他辅助类
|
||||
*/
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rounded-sm {
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
.rounded {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.rounded-lg {
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.rounded-xl {
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
.rounded-full {
|
||||
border-radius: 9999rpx;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,13 @@ declare global {
|
||||
const applyThemeToMiniProgram: typeof import('../utils/theme')['applyThemeToMiniProgram']
|
||||
const auth: typeof import('../api/auth')['default']
|
||||
const checkLogin: typeof import('../utils/auth')['checkLogin']
|
||||
const clearAll: typeof import('../utils/storage')['clearAll']
|
||||
const clearAll: typeof import('../utils/auth')['clearAll']
|
||||
const clearToken: typeof import('../utils/storage')['clearToken']
|
||||
const clearTokens: typeof import('../utils/auth')['clearTokens']
|
||||
const clearUserInfo: typeof import('../utils/auth')['clearUserInfo']
|
||||
const colorColumns: typeof import('../composables/useTheme')['colorColumns']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const config: typeof import('../api/config')['default']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const createRouter: typeof import('uni-mini-router')['createRouter']
|
||||
@@ -27,6 +30,8 @@ declare global {
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const dept: typeof import('../api/dept')['default']
|
||||
const dict: typeof import('../api/dict')['default']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const extendedColorOptions: typeof import('../composables/useTheme')['extendedColorOptions']
|
||||
const file: typeof import('../api/file')['default']
|
||||
@@ -34,9 +39,11 @@ declare global {
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getDarkerColor: typeof import('../utils/colorUtils')['getDarkerColor']
|
||||
const getLighterColor: typeof import('../utils/colorUtils')['getLighterColor']
|
||||
const getRefreshToken: typeof import('../utils/auth')['getRefreshToken']
|
||||
const getToken: typeof import('../utils/storage')['getToken']
|
||||
const getUserInfo: typeof import('../utils/storage')['getUserInfo']
|
||||
const getUserInfo: typeof import('../utils/auth')['getUserInfo']
|
||||
const guessSerializerType: typeof import('@uni-helper/uni-use')['guessSerializerType']
|
||||
const h: typeof import('vue')['h']
|
||||
const initTheme: typeof import('../composables/useTheme')['initTheme']
|
||||
@@ -46,13 +53,16 @@ declare global {
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const log: typeof import('../api/log')['default']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const menu: typeof import('../api/menu')['default']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const notice: typeof import('../api/notice')['default']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites']
|
||||
const onBackPress: typeof import('@dcloudio/uni-app')['onBackPress']
|
||||
@@ -102,13 +112,14 @@ declare global {
|
||||
const requireLogin: typeof import('../utils/auth')['requireLogin']
|
||||
const resetTheme: typeof import('../composables/useTheme')['resetTheme']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const role: typeof import('../api/role')['default']
|
||||
const setAccessToken: typeof import('../utils/auth')['setAccessToken']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const setRefreshToken: typeof import('../utils/auth')['setRefreshToken']
|
||||
const setThemeColor: typeof import('../composables/useTheme')['setThemeColor']
|
||||
const setToken: typeof import('../utils/storage')['setToken']
|
||||
const setUserInfo: typeof import('../utils/storage')['setUserInfo']
|
||||
const setUserInfo: typeof import('../utils/auth')['setUserInfo']
|
||||
const setupStore: typeof import('../store/index')['setupStore']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
@@ -138,6 +149,7 @@ declare global {
|
||||
const useClipboardData: typeof import('@uni-helper/uni-use')['useClipboardData']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useDictStore: typeof import('../store/modules/dict.store')['useDictStore']
|
||||
const useDownloadFile: typeof import('@uni-helper/uni-use')['useDownloadFile']
|
||||
const useGlobalData: typeof import('@uni-helper/uni-use')['useGlobalData']
|
||||
const useId: typeof import('vue')['useId']
|
||||
@@ -201,12 +213,11 @@ declare module 'vue' {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly Storage: UnwrapRef<typeof import('../utils/storage')['Storage']>
|
||||
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||
readonly applyThemeOnPageShow: UnwrapRef<typeof import('../utils/theme')['applyThemeOnPageShow']>
|
||||
readonly applyThemeToMiniProgram: UnwrapRef<typeof import('../utils/theme')['applyThemeToMiniProgram']>
|
||||
readonly auth: UnwrapRef<typeof import('../api/auth')['default']>
|
||||
readonly checkLogin: UnwrapRef<typeof import('../utils/auth')['checkLogin']>
|
||||
readonly clearAll: UnwrapRef<typeof import('../utils/storage')['clearAll']>
|
||||
readonly clearAll: UnwrapRef<typeof import('../utils/auth')['clearAll']>
|
||||
readonly clearTokens: UnwrapRef<typeof import('../utils/auth')['clearTokens']>
|
||||
readonly clearUserInfo: UnwrapRef<typeof import('../utils/auth')['clearUserInfo']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
|
||||
@@ -217,15 +228,13 @@ declare module 'vue' {
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly extendedColorOptions: UnwrapRef<typeof import('../composables/useTheme')['extendedColorOptions']>
|
||||
readonly file: UnwrapRef<typeof import('../api/file')['default']>
|
||||
readonly getAccessToken: UnwrapRef<typeof import('../utils/auth')['getAccessToken']>
|
||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getRefreshToken: UnwrapRef<typeof import('../utils/auth')['getRefreshToken']>
|
||||
readonly getToken: UnwrapRef<typeof import('../utils/storage')['getToken']>
|
||||
readonly getUserInfo: UnwrapRef<typeof import('../utils/storage')['getUserInfo']>
|
||||
readonly getUserInfo: UnwrapRef<typeof import('../utils/auth')['getUserInfo']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isLoggedIn: UnwrapRef<typeof import('../utils/auth')['isLoggedIn']>
|
||||
@@ -289,15 +298,13 @@ declare module 'vue' {
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setRefreshToken: UnwrapRef<typeof import('../utils/auth')['setRefreshToken']>
|
||||
readonly setToken: UnwrapRef<typeof import('../utils/storage')['setToken']>
|
||||
readonly setUserInfo: UnwrapRef<typeof import('../utils/storage')['setUserInfo']>
|
||||
readonly setUserInfo: UnwrapRef<typeof import('../utils/auth')['setUserInfo']>
|
||||
readonly setupStore: UnwrapRef<typeof import('../store/index')['setupStore']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly store: UnwrapRef<typeof import('../store/index')['store']>
|
||||
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
|
||||
readonly themeColorOptions: UnwrapRef<typeof import('../composables/useTheme')['themeColorOptions']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
@@ -321,7 +328,6 @@ declare module 'vue' {
|
||||
readonly useThemeStore: UnwrapRef<typeof import('../store/modules/theme.store')['useThemeStore']>
|
||||
readonly useToast: UnwrapRef<typeof import('wot-design-uni')['useToast']>
|
||||
readonly useUserStore: UnwrapRef<typeof import('../store/modules/user.store')['useUserStore']>
|
||||
readonly useWechat: UnwrapRef<typeof import('../composables/useWechat')['useWechat']>
|
||||
readonly user: UnwrapRef<typeof import('../api/user')['default']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by vite-plugin-uni-pages
|
||||
|
||||
interface NavigateToOptions {
|
||||
url: "/pages/index/index" |
|
||||
"/pages/login/index" |
|
||||
"/pages/mine/index" |
|
||||
"/pages/work/index" |
|
||||
"/pages/mine/about/index" |
|
||||
"/pages/mine/faq/index" |
|
||||
"/pages/mine/feedback/index" |
|
||||
"/pages/mine/profile/complete-profile" |
|
||||
"/pages/mine/profile/index" |
|
||||
"/pages/mine/settings/index" |
|
||||
"/pages/mine/settings/account/index" |
|
||||
"/pages/mine/settings/agreement/index" |
|
||||
"/pages/mine/settings/network/index" |
|
||||
"/pages/mine/settings/privacy/index" |
|
||||
"/pages/mine/settings/theme/index";
|
||||
}
|
||||
interface RedirectToOptions extends NavigateToOptions {}
|
||||
|
||||
interface SwitchTabOptions {
|
||||
url: "/pages/index/index" | "/pages/work/index" | "/pages/mine/index"
|
||||
}
|
||||
|
||||
type ReLaunchOptions = NavigateToOptions | SwitchTabOptions;
|
||||
|
||||
declare interface Uni {
|
||||
navigateTo(options: UniNamespace.NavigateToOptions & NavigateToOptions): void;
|
||||
redirectTo(options: UniNamespace.RedirectToOptions & RedirectToOptions): void;
|
||||
switchTab(options: UniNamespace.SwitchTabOptions & SwitchTabOptions): void;
|
||||
reLaunch(options: UniNamespace.ReLaunchOptions & ReLaunchOptions): void;
|
||||
}
|
||||
@@ -1,59 +1,59 @@
|
||||
// Type definitions for vite-plugin-uni-pages
|
||||
// This file provides type definitions for the uni-pages plugin
|
||||
|
||||
declare module '@uni-helper/vite-plugin-uni-pages' {
|
||||
import { Plugin } from 'vite'
|
||||
declare module "@uni-helper/vite-plugin-uni-pages" {
|
||||
import { Plugin } from "vite";
|
||||
|
||||
export interface UniPagesOptions {
|
||||
/**
|
||||
* Subpackage directory
|
||||
* @default 'src/pages'
|
||||
*/
|
||||
subPackages?: string[]
|
||||
subPackages?: string[];
|
||||
|
||||
/**
|
||||
* Global style file
|
||||
*/
|
||||
globalStyle?: Record<string, any>
|
||||
globalStyle?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* Tabbar configuration
|
||||
*/
|
||||
tabBar?: Record<string, any>
|
||||
tabBar?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* Whether to merge pages.json
|
||||
* @default true
|
||||
*/
|
||||
mergePages?: boolean
|
||||
mergePages?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use uni-cloud-router
|
||||
* @default false
|
||||
*/
|
||||
uniCloudRouter?: boolean
|
||||
uniCloudRouter?: boolean;
|
||||
}
|
||||
|
||||
export default function UniPages(options?: UniPagesOptions): Plugin
|
||||
export default function UniPages(options?: UniPagesOptions): Plugin;
|
||||
}
|
||||
|
||||
// Augment Uni namespace for type-safe routing
|
||||
declare global {
|
||||
namespace UniNamespace {
|
||||
interface NavigateToOptions {
|
||||
url: string
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface RedirectToOptions {
|
||||
url: string
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface SwitchTabOptions {
|
||||
url: string
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface ReLaunchOptions {
|
||||
url: string
|
||||
url: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,56 @@
|
||||
import { useUserStore } from "@/store/modules/user.store";
|
||||
import { useUserStore } from "@/store";
|
||||
import { Storage } from "./storage";
|
||||
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "@/constants";
|
||||
import { APP_ACCESS_TOKEN_KEY, APP_REFRESH_TOKEN_KEY, APP_USER_INFO } from "@/constants";
|
||||
|
||||
/**
|
||||
* 获取访问令牌
|
||||
* @returns 返回访问令牌,如果不存在则返回null
|
||||
*/
|
||||
export function getAccessToken(): string {
|
||||
return Storage.get<string>(ACCESS_TOKEN_KEY);
|
||||
// 获取访问 token
|
||||
export function getAccessToken(): string | null {
|
||||
return Storage.get<string>(APP_ACCESS_TOKEN_KEY) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置访问令牌
|
||||
* @param token 访问令牌
|
||||
*/
|
||||
// 设置 token
|
||||
export function setAccessToken(token: string): void {
|
||||
Storage.set(ACCESS_TOKEN_KEY, token);
|
||||
return Storage.set(APP_ACCESS_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取刷新令牌
|
||||
* @returns 返回刷新令牌,如果不存在则返回null
|
||||
*/
|
||||
// 获取刷新 token
|
||||
export function getRefreshToken(): string | null {
|
||||
return Storage.get<string>(REFRESH_TOKEN_KEY) || null;
|
||||
return Storage.get<string>(APP_REFRESH_TOKEN_KEY) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置刷新令牌
|
||||
* @param token 刷新令牌
|
||||
*/
|
||||
// 设置刷新 token
|
||||
export function setRefreshToken(token: string): void {
|
||||
Storage.set(REFRESH_TOKEN_KEY, token);
|
||||
return Storage.set(APP_REFRESH_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有令牌
|
||||
*/
|
||||
export function clearTokens(): void {
|
||||
Storage.remove(ACCESS_TOKEN_KEY);
|
||||
Storage.remove(REFRESH_TOKEN_KEY);
|
||||
// 清除 token
|
||||
export function clearTokens() {
|
||||
Storage.remove(APP_ACCESS_TOKEN_KEY);
|
||||
Storage.remove(APP_REFRESH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户登录状态,未登录则跳转到登录页面
|
||||
* @param silent 是否静默检查,不跳转登录页面
|
||||
* @returns 返回用户是否已登录
|
||||
*/
|
||||
// 获取用户信息
|
||||
export function getUserInfo<T = any>(): T | undefined {
|
||||
return Storage.get<T>(APP_USER_INFO);
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
export function setUserInfo(userInfo: any): void {
|
||||
return Storage.set(APP_USER_INFO, userInfo);
|
||||
}
|
||||
|
||||
// 清除用户信息
|
||||
export function clearUserInfo() {
|
||||
return Storage.remove(APP_USER_INFO);
|
||||
}
|
||||
|
||||
// 清除所有缓存信息
|
||||
export function clearAll() {
|
||||
Storage.remove(APP_ACCESS_TOKEN_KEY);
|
||||
Storage.remove(APP_REFRESH_TOKEN_KEY);
|
||||
Storage.remove(APP_USER_INFO);
|
||||
}
|
||||
|
||||
// 检查用户登录状态,未登录则跳转到登录页面
|
||||
export function checkLogin(silent: boolean = false): boolean {
|
||||
const userStore = useUserStore();
|
||||
const accessToken = getAccessToken();
|
||||
@@ -97,17 +101,12 @@ export function checkLogin(silent: boolean = false): boolean {
|
||||
return isLoggedIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已登录(静默检查,不跳转)
|
||||
* @returns 返回用户是否已登录
|
||||
*/
|
||||
// 检查用户是否已登录(静默检查,不跳转)
|
||||
export function isLoggedIn(): boolean {
|
||||
return checkLogin(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制用户登录,清除无效的登录状态
|
||||
*/
|
||||
// 强制用户登录,清除无效的登录状态
|
||||
export function requireLogin(): void {
|
||||
const userStore = useUserStore();
|
||||
const accessToken = getAccessToken();
|
||||
|
||||
@@ -1,53 +1,18 @@
|
||||
import { getAccessToken } from "./auth";
|
||||
|
||||
// 请求配置
|
||||
interface RequestOptions<T = any> {
|
||||
url: string;
|
||||
method: "GET" | "POST" | "PUT" | "DELETE";
|
||||
data?: T;
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
responseType?: "text" | "arraybuffer" | "blob" | "json";
|
||||
skipAuth?: boolean; // 标记是否跳过认证
|
||||
}
|
||||
import { ApiCode } from "@/enums/api-code.enum";
|
||||
|
||||
import { getAccessToken, clearAll } from "@/utils/auth";
|
||||
/**
|
||||
* 请求拦截器 - 添加 Authorization 头
|
||||
*/
|
||||
function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// 构建请求头
|
||||
const header = Object.assign({}, options.headers || {});
|
||||
|
||||
// 检查是否需要添加认证令牌
|
||||
if (!options.skipAuth) {
|
||||
const accessToken = getAccessToken();
|
||||
if (accessToken) {
|
||||
header["Authorization"] = `Bearer ${accessToken}`;
|
||||
} else {
|
||||
// 需要认证但没有令牌,跳转到登录页
|
||||
// 防止循环跳转:检查当前页面是否已经是登录页
|
||||
const currentPages = getCurrentPages();
|
||||
const currentPage = currentPages[currentPages.length - 1];
|
||||
if (!currentPage || !currentPage.route || !currentPage.route.includes("login")) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/login/index",
|
||||
function request<T>(options: UniApp.RequestOptions): Promise<T> {
|
||||
uni.showLoading({
|
||||
title: "加载中...",
|
||||
});
|
||||
}
|
||||
return reject(new Error("请先登录"));
|
||||
}
|
||||
}
|
||||
|
||||
// 根据平台决定URL前缀
|
||||
let requestUrl = options.url;
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序环境,使用完整URL
|
||||
requestUrl = `${import.meta.env.VITE_API_BASE_URL}${options.url}`;
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// 非微信小程序环境,使用代理前缀
|
||||
requestUrl = `${import.meta.env.VITE_APP_BASE_API}${options.url}`;
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// H5 使用 VITE_APP_BASE_API 作为代理路径,其他平台使用 VITE_API_BASE_URL 作为请求路径
|
||||
let baseApi = import.meta.env.VITE_API_BASE_URL;
|
||||
// #ifdef H5
|
||||
baseApi = import.meta.env.VITE_APP_BASE_API;
|
||||
// #endif
|
||||
|
||||
let timeout = Number(import.meta.env.VITE_TIMEOUT);
|
||||
@@ -57,38 +22,64 @@ function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
|
||||
// 统一处理请求
|
||||
uni.request({
|
||||
url: requestUrl,
|
||||
url: `${baseApi}${options.url}`,
|
||||
method: options.method,
|
||||
data: options.data,
|
||||
header: header,
|
||||
header: {
|
||||
...options.header,
|
||||
Authorization: getAccessToken() ? `Bearer ${getAccessToken()}` : "",
|
||||
},
|
||||
timeout: timeout,
|
||||
responseType: options.responseType,
|
||||
success: (res: any) => {
|
||||
success: (res: UniApp.RequestSuccessCallbackResult) => {
|
||||
const result = res.data as ApiResponse<T>;
|
||||
// 请求成功
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(res.data.data);
|
||||
if (result.code === ApiCode.SUCCESS) {
|
||||
resolve(result.data);
|
||||
}
|
||||
// 未授权错误
|
||||
else if (res.statusCode === 401) {
|
||||
// 防止循环跳转:检查当前页面是否已经是登录页
|
||||
const currentPages = getCurrentPages();
|
||||
const currentPage = currentPages[currentPages.length - 1];
|
||||
if (!currentPage || !currentPage.route || !currentPage.route.includes("login")) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/login/index",
|
||||
// 令牌过期
|
||||
else if (result.code === ApiCode.TOKEN_EXPIRED) {
|
||||
// 清除所有缓存
|
||||
clearAll();
|
||||
// 跳转登录
|
||||
uni.reLaunch({ url: "/pages/login/index" });
|
||||
// 提示
|
||||
uni.showToast({
|
||||
title: result.msg || "登录已过期,请重新登录",
|
||||
icon: "error",
|
||||
});
|
||||
reject(new Error(result.msg || "登录已过期,请重新登录"));
|
||||
}
|
||||
reject(new Error(res.data.message || "未授权,请重新登录"));
|
||||
// 未授权访问
|
||||
else if (result.code === ApiCode.UNAUTHORIZED) {
|
||||
uni.showToast({
|
||||
title: result.msg || "未授权访问",
|
||||
icon: "error",
|
||||
});
|
||||
reject(new Error(result.msg || "未授权访问"));
|
||||
}
|
||||
// 其他错误
|
||||
else {
|
||||
const errorMsg = res.data.message || `请求失败: ${res.statusCode}`;
|
||||
reject(new Error(errorMsg));
|
||||
uni.showToast({
|
||||
title: result.msg || "请求失败",
|
||||
icon: "error",
|
||||
});
|
||||
reject(new Error(result.msg || "请求失败"));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
fail: (err: UniApp.GeneralCallbackResult) => {
|
||||
console.log("请求失败", err);
|
||||
uni.showToast({
|
||||
title: "网络请求失败",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
reject(new Error(err.errMsg || "网络请求失败"));
|
||||
},
|
||||
complete: () => {
|
||||
// 请求完成,隐藏loading
|
||||
uni.hideLoading();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,42 +31,3 @@ export const Storage = {
|
||||
get,
|
||||
remove,
|
||||
};
|
||||
|
||||
// 为了向后兼容,导出具体的函数
|
||||
import { ACCESS_TOKEN_KEY, USER_INFO_KEY } from "@/constants";
|
||||
|
||||
/**
|
||||
* 获取令牌
|
||||
*/
|
||||
export function getToken(): string | null {
|
||||
return Storage.get<string>(ACCESS_TOKEN_KEY) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置令牌
|
||||
*/
|
||||
export function setToken(token: string): void {
|
||||
Storage.set(ACCESS_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export function getUserInfo<T = any>(): T | undefined {
|
||||
return Storage.get<T>(USER_INFO_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
export function setUserInfo(userInfo: any): void {
|
||||
Storage.set(USER_INFO_KEY, userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有数据
|
||||
*/
|
||||
export function clearAll(): void {
|
||||
Storage.remove(ACCESS_TOKEN_KEY);
|
||||
Storage.remove(USER_INFO_KEY);
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* 小程序主题工具类
|
||||
* 用于解决小程序环境中CSS变量不能动态设置的问题
|
||||
*/
|
||||
|
||||
// 注入小程序环境的全局样式
|
||||
export function applyThemeToMiniProgram(primaryColor: string) {
|
||||
// 确保在小程序环境中执行
|
||||
if (typeof document !== "undefined") return;
|
||||
|
||||
try {
|
||||
// 设置TabBar样式
|
||||
uni.setTabBarStyle({
|
||||
color: "#000000",
|
||||
selectedColor: primaryColor,
|
||||
backgroundColor: "#ffffff",
|
||||
borderStyle: "black",
|
||||
});
|
||||
|
||||
console.log("小程序主题色已应用:", primaryColor);
|
||||
} catch (error) {
|
||||
console.error("应用小程序主题色失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 在页面展示时应用主题
|
||||
export function applyThemeOnPageShow(primaryColor: string) {
|
||||
// 各平台小程序可能需要不同处理
|
||||
const platform = uni.getSystemInfoSync().platform;
|
||||
console.log(`当前平台: ${platform}, 应用主题色: ${primaryColor}`);
|
||||
|
||||
// 某些平台可能需要特定处理
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"target": "esnext",
|
||||
"allowJs": false,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"lib": ["esnext", "dom"],
|
||||
"types": ["@dcloudio/types", "@uni-helper/uni-types", "wot-design-uni/global", "./src/types/vite-plugin-uni-pages"]
|
||||
"types": ["@dcloudio/types", "@uni-helper/uni-types", "wot-design-uni/global"]
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
// 调整 Volar(Vue 语言服务工具)解析行为,用于为 uni-app 组件提供 TypeScript 类型
|
||||
"plugins": ["@uni-helper/uni-types/volar-plugin"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"exclude": ["node_modules", "dist", "src/components/u-charts/*.js"]
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -17,12 +17,13 @@ interface NavigateToOptions {
|
||||
"/pages/mine/settings/account/index" |
|
||||
"/pages/mine/settings/agreement/index" |
|
||||
"/pages/mine/settings/network/index" |
|
||||
"/pages/mine/settings/privacy/index" |
|
||||
"/pages/mine/settings/theme/index";
|
||||
}
|
||||
interface RedirectToOptions extends NavigateToOptions {}
|
||||
|
||||
interface SwitchTabOptions {
|
||||
url: "/pages/index/index" | "/pages/mine/index"
|
||||
url: "/pages/index/index" | "/pages/work/index" | "/pages/mine/index"
|
||||
}
|
||||
|
||||
type ReLaunchOptions = NavigateToOptions | SwitchTabOptions;
|
||||
|
||||
@@ -16,6 +16,7 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
|
||||
host: true,
|
||||
port: Number(env.VITE_APP_PORT),
|
||||
open: true,
|
||||
// 代理配置只在 H5(浏览器)开发时生效。 其他平台(如小程序、App)在开发时不使用 Vite 的开发服务器,它们直接运行在各自的环境中。
|
||||
proxy: {
|
||||
[env.VITE_APP_BASE_API]: {
|
||||
changeOrigin: true,
|
||||
@@ -36,7 +37,15 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
|
||||
// make sure put it before `Uni()`
|
||||
UnoCss(),
|
||||
UniLayouts(),
|
||||
UniPages(),
|
||||
UniPages({
|
||||
dts: "src/types/uni-pages.d.ts",
|
||||
subPackages: ["src/subPages"],
|
||||
/**
|
||||
* 排除的页面,相对于 dir 和 subPackages
|
||||
* @default []
|
||||
*/
|
||||
exclude: ["**/components/**/*.*"],
|
||||
}),
|
||||
|
||||
Components({
|
||||
resolvers: [WotResolver()],
|
||||
|
||||
|
After Width: | Height: | Size: 666 KiB |
|
After Width: | Height: | Size: 573 KiB |
|
After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 248 KiB |
@@ -46,4 +46,5 @@ export interface OnlineUserTable {
|
||||
os: string;
|
||||
browser: string;
|
||||
login_time: string;
|
||||
login_type: string;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface LoginFormData {
|
||||
captcha_key: string;
|
||||
captcha: string;
|
||||
remember: boolean;
|
||||
login_type: string;
|
||||
}
|
||||
|
||||
// 刷新令牌
|
||||
|
||||
@@ -16,8 +16,13 @@ export const enum ResultEnum {
|
||||
EXCEPTION = -1,
|
||||
|
||||
/**
|
||||
* 访问令牌无效或过期
|
||||
* 未授权访问
|
||||
*/
|
||||
ACCESS_TOKEN_INVALID = 401,
|
||||
UNAUTHORIZED = 10403,
|
||||
|
||||
/**
|
||||
* 令牌已过期
|
||||
*/
|
||||
TOKEN_EXPIRED = 10401,
|
||||
|
||||
}
|
||||
|
||||
@@ -85,12 +85,15 @@ httpRequest.interceptors.response.use((response: AxiosResponse<ApiResponse>) =>
|
||||
}
|
||||
}
|
||||
|
||||
if (data?.status_code === ResultEnum.ACCESS_TOKEN_INVALID) {
|
||||
if (data?.status_code === ResultEnum.TOKEN_EXPIRED) {
|
||||
await redirectToLogin("登录已过期,请重新登录");
|
||||
return Promise.reject(new Error(data.msg));
|
||||
} else if (data?.code === ResultEnum.ERROR) {
|
||||
ElMessage.error(data.msg || "请求错误");
|
||||
return Promise.reject(new Error(data.msg || "请求错误"));
|
||||
} else if (data?.code === ResultEnum.UNAUTHORIZED) {
|
||||
ElMessage.error(data.msg || "暂无权限");
|
||||
return Promise.reject(new Error(data.msg || "请求错误"));
|
||||
} else if (data?.code === ResultEnum.EXCEPTION) {
|
||||
ElMessage.error(data.msg || "服务异常");
|
||||
return Promise.reject(new Error(data.msg || "服务异常"));
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'index')?.show" type="index" fixed label="序号" min-width="60" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'session_id')?.show" key="session_id" label="会话编号" prop="session_id" min-width="250" show-overflow-tooltip/>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'login_type')?.show" key="login_type" label="登录类型" prop="login_type" min-width="100" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'ipaddr')?.show" key="ipaddr" label="IP地址" prop="ipaddr" min-width="150" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-text>{{ scope.row.ipaddr }}</el-text>
|
||||
@@ -103,7 +104,7 @@ defineOptions({
|
||||
});
|
||||
|
||||
import OnlineAPI, { type OnlineUserPageQuery, type OnlineUserTable } from "@/api/monitor/online";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const total = ref(0);
|
||||
@@ -119,6 +120,7 @@ const tableColumns = ref([
|
||||
{ label: '选择框', prop: 'selection', show: true },
|
||||
{ label: '序号', prop: 'index', show: true },
|
||||
{ label: '会话编号', prop: 'session_id', show: true },
|
||||
{ label: '登录类型', prop: 'login_type', show: true },
|
||||
{ label: '登录名称', prop: 'name', show: true },
|
||||
{ label: '用户账号', prop: 'user_name', show: true },
|
||||
{ label: '主机', prop: 'ipaddr', show: true },
|
||||
|
||||
@@ -145,7 +145,8 @@ const loginForm = reactive<LoginFormData>({
|
||||
password: "",
|
||||
captcha: "",
|
||||
captcha_key: "",
|
||||
remember: true
|
||||
remember: true,
|
||||
login_type: "PC端",
|
||||
});
|
||||
|
||||
const captchaState = reactive<CaptchaInfo>({
|
||||
|
||||