feat: 新增隐私政策页面并优化设置模块

refactor: 重构用户信息存储和主题管理逻辑

fix: 修正登录类型字段缺失问题

style: 统一页面导航栏样式和布局

docs: 更新README和文档内容

chore: 清理无用代码和资源文件

perf: 优化网络请求和错误处理逻辑

test: 更新API测试用例

build: 更新依赖版本和构建配置

ci: 调整CI/CD脚本配置
This commit is contained in:
zhangtao
2025-08-24 21:08:48 +08:00
parent b395a9bef7
commit 94a527ab6c
76 changed files with 2300 additions and 2698 deletions
@@ -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(
+8 -8
View File
@@ -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
+8 -6
View File
@@ -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)
+1 -1
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

+3 -2
View File
@@ -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
+3 -1
View File
@@ -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
+63 -1
View File
@@ -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 -7
View File
@@ -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": [
{
+6 -37
View File
@@ -1,10 +1,10 @@
## 项目介绍
# 项目介绍
基于 uni-app + Vue 3 + TypeScript 移动端跨平台开发模板,集成了 ESLint、Prettier、Stylelint、Husky 和 Commitlint 等工具,确保代码规范与质量。
## 项目截图
![](https://www.youlai.tech/storage/blog/2025/04/30/app.jpg)
![截图](https://www.youlai.tech/storage/blog/2025/04/30/app.jpg)
## 项目文档
@@ -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)
+15 -3
View File
@@ -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']
}
}
+3 -12
View File
@@ -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";
```
主题设置会在以下情况自动保存:
- 切换主题模式时
- 设置主题色时
- 重置主题时
+93 -93
View File
@@ -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. 常见问题解决
+3
View File
@@ -54,6 +54,9 @@ export default [
ApiResponse: "readonly", // 统一响应数据类型
creatorType: "readonly", // 创建人类型
UploadFileResult: "readonly", // 上传文件返回类型
Todo: "readonly", // 待办事项
TodoState: "readonly", // 待办事项状态
plus: true, // HTML5+ 运行时环境
},
},
},
+1 -1
View File
@@ -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"
},
+3
View File
@@ -41,6 +41,9 @@ export default defineUniPages({
{
pagePath: "pages/index/index",
},
{
pagePath: "pages/work/index",
},
{
pagePath: "pages/mine/index",
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

+26 -4
View File
@@ -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>
+2 -38
View File
@@ -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;
}
// 刷新令牌
+3 -3
View File
@@ -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 {
+4 -3
View File
@@ -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.
-201
View File
@@ -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`文件,以免被强制覆盖。
+28 -325
View File
@@ -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,
};
}
-159
View File
@@ -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,
};
}
+10 -5
View File
@@ -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";
+2 -2
View File
@@ -20,12 +20,12 @@ export const enum ApiCode {
/**
* 未授权访问
*/
UNAUTHORIZED = 401,
UNAUTHORIZED = 10403,
/**
* 令牌已过期
*/
TOKEN_EXPIRED = 403,
TOKEN_EXPIRED = 10401,
/**
* 参数校验失败
+10
View File
@@ -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
},
+57 -10
View File
@@ -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"
}
+15 -21
View File
@@ -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 = [];
+51 -124
View File
@@ -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>
+111 -325
View File
@@ -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>
+61 -72
View File
@@ -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.com7×24小时);3. 在线客服(工作日9:00-18:00)。"
}
])
answer:
"您可以通过以下方式联系客服:1. 客服热线 400-123-4567(工作日9:00-18:00);2. 邮箱 support@example.com7×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;
}
}
}
+29 -31
View File
@@ -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;
+152 -233
View File
@@ -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 || "";
+34 -9
View File
@@ -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>
+23 -26
View File
@@ -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>
+352 -351
View File
@@ -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",
});
// 36
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>
+294 -6
View File
@@ -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>
+7 -4
View File
@@ -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

+84 -31
View File
@@ -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,
};
});
+13 -26
View File
@@ -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,
+251
View File
@@ -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;
}
+19 -13
View File
@@ -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']>
+36
View File
@@ -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;
}
+12 -12
View File
@@ -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;
}
}
}
+40 -41
View File
@@ -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();
+53 -62
View File
@@ -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();
},
});
});
}
-39
View File
@@ -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);
}
-33
View File
@@ -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 -3
View File
@@ -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": {
// VolarVue 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"]
}
+2 -1
View File
@@ -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;
+10 -1
View File
@@ -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()],
Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

+1
View File
@@ -46,4 +46,5 @@ export interface OnlineUserTable {
os: string;
browser: string;
login_time: string;
login_type: string;
}
+1
View File
@@ -45,6 +45,7 @@ export interface LoginFormData {
captcha_key: string;
captcha: string;
remember: boolean;
login_type: string;
}
// 刷新令牌
+7 -2
View File
@@ -16,8 +16,13 @@ export const enum ResultEnum {
EXCEPTION = -1,
/**
* 访
* 访
*/
ACCESS_TOKEN_INVALID = 401,
UNAUTHORIZED = 10403,
/**
*
*/
TOKEN_EXPIRED = 10401,
}
+4 -1
View File
@@ -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 || "服务异常"));
+3 -1
View File
@@ -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>({