refactor: 暗黑模式适配调整

This commit is contained in:
Ray.Hao
2025-08-15 20:36:20 +08:00
parent d0baa003e1
commit 8b2ae01a9a
20 changed files with 490 additions and 447 deletions

99
.cursorrules Normal file
View File

@@ -0,0 +1,99 @@
## 样式CSS/SCSS与 UnoCSS 使用规范Cursor Rules
目标:在不牺牲可读性的前提下最大化复用,减少冗余,保证小程序/H5 多端一致、整洁大方。
### 1) 总原则
- 优先使用 UnoCSS 工具类与已定义的 shortcuts避免重复造轮子。
- 同一工具类组合在同一页面或多个页面中出现 ≥ 2 次时,提取为“语义化类名”。
- 禁止使用 `@apply`,禁止在 SCSS 中重复定义 UnoCSS 已提供的工具类或在 `unocss.config.ts` 中已存在的 shortcuts`flex`、`flex-start`、`flex-between`、`flex-col-center` 等)。
- 组件库样式覆盖和页面布局样式分离:页面/组件的布局与样式用普通类,覆盖 wot 组件样式用“外部样式类”。
### 2) 目录与作用域
- 页面私有样式:写在当前 `.vue` 的 `<style lang="scss" scoped>` 中。
- 多处复用的语义化样式:优先抽到所属页面/模块级样式;跨页面复用再考虑提到 `src/styles` 公共样式文件。
- 避免把零碎的、只出现一次的样式放到全局,保持全局样式轻量。
### 3) 覆盖 wot-design-uni 组件样式(外部样式类)
- 使用 `custom-class` 传入类名,禁止对 wot 组件使用 `class` 覆盖样式。
- 因为 SFC `scoped`,覆盖子组件需使用 `:deep()`,并加 `!important` 提升优先级。
- 在“自定义组件”中使用 wot 组件时,若样式穿透无效,按需开启 `styleIsolation: 'shared'`。
- 参考文档:[小程序使用外部样式类wot-design-uni](https://wot-design-uni.pages.dev/guide/common-problems.html#%E5%B0%8F%E7%A8%8B%E5%BA%8F%E4%BD%BF%E7%94%A8%E5%A4%96%E9%83%A8%E6%A0%B7%E5%BC%8F%E7%B1%BB)
示例(按钮):
```vue
<wd-button custom-class="logout-button" plain @click="handleLogout">退出登录</wd-button>
```
```scss
/* 页面/组件内 */
:deep(.logout-button) {
width: 100% !important;
height: 80rpx !important;
font-size: 32rpx !important;
font-weight: bold !important;
border-radius: 40rpx !important;
}
```
### 4) UnoCSS 约定与最小用法
- 预设、shortcuts 位于 `unocss.config.ts`,不要在 SCSS 中重复定义这些类。
- 常用 shortcuts已存在无需再自定义
- `flex-start`、`flex-between`、`flex-center`、`flex-col-center` 等。
- 可以自由组合 UnoCSS 工具类(如:`p-20rpx`、`px-24rpx`、`border-b` 等)。
- 不可读的长串组合若重复使用,抽成语义化类名。
- 复杂/冗长的原子类组合(例如包含 ≥4 个原子类,且会在项目内重复出现)一律改为“外部样式类”(语义化类名),以避免模板膨胀;模板仅保留必要的少量工具类。
- 简单、一次性且语义明确的区域优先使用 UnoCSS 原子类(例如单个 `p-30rpx`、`text-center`、`font-600` 等);
复杂/冗长≥4或会复用的组合抽为“外部样式类”。
示例(页面语义化类):
```vue
<view class="card-header">...</view>
```
```scss
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #e5e7eb;
}
:deep(.dark) .card-header { border-bottom-color: #374151; }
```
### 5) 语义化抽取的判定标准
- 满足以下任一条件即可抽取为语义化类名:
- 同一工具类组合在项目内出现 ≥ 2 次;
- 该区域具备明确的语义(如“卡片头部”、“工具项”等);
- 需要在暗色模式等主题下统一控制样式。
推荐命名:`kebab-case`,表达语义而非表现形式,例如:`card-header`、`tool-item`、`user-profile`。
### 6) 属性书写顺序(遵循项目 Linter
- 基本顺序:`display` → 布局(如 `flex-*`:先 `align-items`,后 `justify-content`)→ 尺寸(`width/height`)→ 间距(`margin/padding`)→ 背景 → 边框 → 圆角 → 阴影 → 过渡/变换。
- 避免无意义的属性覆盖;必要时使用 `!important`,仅用于外部样式类覆盖。
### 7) 深色模式
- 使用 `:deep(.dark) .your-class { ... }` 定义暗色样式,保持与明亮模式同一语义类名。
### 8) 示例对照(本项目中已落地)
- 将重复的工具项组合 `w-1/4 flex-col-center mb-30rpx` 抽取为:
- `tool-item`(语义化类,含列方向与对齐、宽度 25%、下间距)
- 将重复的卡片头部组合 `flex-between p-20rpx px-24rpx border-b ...` 抽取为:
- `card-header`(统一控制边距、分隔线与暗色模式)
- 将数据统计卡片 `flex p-30rpx mx-30rpx my-20rpx bg-white rounded-16rpx shadow-sm dark:bg-gray-800` 抽取为:
- `stats-card`、`stats-item`、`stats-divider`(统一控制布局/间距/暗色色板)
- 覆盖 wot 组件样式:
- 使用 `custom-class` + `:deep()` + `!important`,不要用 `class`。
### 9) 禁止项(务必遵守)
- 不要在 SCSS 中定义 `.flex`、`.flex-start`、`.flex-between` 等已由 UnoCSS/shortcuts 提供的类。
- 不要再使用 `@apply`。
- 不要把一次性的样式放到全局公共样式。
---
本规则文件旨在为 Cursor 自动补全与协作提供统一参考,遇到冲突以项目 Lint 规则与 `unocss.config.ts` 为准。

7
components.d.ts vendored
View File

@@ -21,9 +21,12 @@ declare module 'vue' {
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']
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']
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']
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']
@@ -38,11 +41,15 @@ declare module 'vue' {
WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default']
WdRadio: typeof import('wot-design-uni/components/wd-radio/wd-radio.vue')['default']
WdRadioGroup: typeof import('wot-design-uni/components/wd-radio-group/wd-radio-group.vue')['default']
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']
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']
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']
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']
}
}

View File

@@ -25,10 +25,9 @@
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";
import { useThemeStore } from "@/store";
import { useTabbar } from "@/composables/useTabbar";
import { useRouter } from "uni-mini-router";
import { useRouter, useRoute } from "uni-mini-router";
import { storeToRefs } from "pinia";
const router = useRouter();
@@ -47,11 +46,8 @@ onMounted(() => {
uni.hideTabBar();
// #endif
nextTick(() => {
if (route.name) {
const routeName = typeof route.name === "string" ? route.name : "";
if (routeName !== activeTabbar.value.name) {
setTabbarItemActive(routeName);
}
if (route.name && route.name !== activeTabbar.value.name) {
setTabbarItemActive(route.name);
}
});
});

View File

@@ -43,7 +43,14 @@
},
{
"path": "pages/mine/feedback/index",
"type": "page"
"type": "page",
"name": "feedback",
"style": {
"navigationBarTitleText": "意见反馈"
},
"meta": {
"requireAuth": true
}
},
{
"path": "pages/mine/profile/complete-profile",
@@ -117,5 +124,6 @@
}
]
},
"__esModule": true,
"subPackages": []
}
}

View File

@@ -1,5 +1,5 @@
<template>
<view class="app-container">
<view class="app-container dark:text-[var(--wot-dark-color)]">
<wd-swiper
v-model:current="current"
:list="swiperList"
@@ -89,6 +89,7 @@
<script setup lang="ts">
import { dayjs } from "wot-design-uni";
import { useRouter } from "uni-mini-router";
// 定义访问统计数据类型
interface VisitStatsVO {
@@ -257,18 +258,6 @@ onReady(() => {
loadVisitStatsData();
loadVisitTrendData();
});
onShow(() => {
// 确保 tabbar 状态正确
const pages = getCurrentPages();
if (pages.length > 0) {
const currentPage = pages[pages.length - 1];
if (currentPage.route === "pages/index/index") {
// 通过事件通知 tabbar 布局更新状态
uni.$emit("updateTabbar", "index");
}
}
});
</script>
<route lang="json">

View File

@@ -12,12 +12,7 @@
<wd-form v-if="loginType === 'account'" ref="loginFormRef" :model="loginFormData">
<!-- 用户名输入框 -->
<view class="form-item">
<wd-icon
name="user"
size="22"
:color="isDarkMode ? '#7AC5FF' : '#333'"
class="input-icon"
/>
<wd-icon name="user" size="22" class="input-icon" />
<input
v-model="loginFormData.username"
class="form-input input-transparent"
@@ -33,7 +28,7 @@
<input
v-model="loginFormData.password"
class="form-input input-transparent"
:type="showPassword ? 'text' : 'password'"
:password="!showPassword"
placeholder="请输入密码"
placeholder-class="input-placeholder"
/>
@@ -135,11 +130,12 @@ const loginFormData = ref<LoginData>({
password: "123456",
});
// 获取重定向参数
// 仅使用 query 作为重定向来源
const redirect = ref("/pages/index/index");
onLoad((options) => {
if (options && options.redirect) {
redirect.value = decodeURIComponent(options.redirect);
const fromQuery = options && options.redirect ? decodeURIComponent(options.redirect) : "";
if (fromQuery && fromQuery !== "/pages/login/index") {
redirect.value = fromQuery;
}
});
@@ -176,11 +172,11 @@ const handleAccountLogin = () => {
.then(() => {
toast.success("登录成功");
console.log("跳转链接", redirect.value);
// 账号密码登录直接跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
uni.reLaunch({ url: redirect.value });
}, 1000);
})
.catch((error) => {
@@ -211,16 +207,13 @@ const handleWechatPhoneLogin = async (e: any) => {
if (result.isNewUser || !userStore.isUserInfoComplete()) {
// 跳转到完善信息页面
setTimeout(() => {
uni.navigateTo({
url: `/pages/mine/profile/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
// 跳转完善资料redirect 保持在缓存中供目标页读取
uni.navigateTo({ url: "/pages/mine/profile/complete-profile" });
}, 1000);
} else {
// 跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
uni.reLaunch({ url: redirect.value });
}, 1000);
}
} catch (error: any) {
@@ -256,16 +249,12 @@ const handleWechatLogin = async () => {
if (result.isNewUser || !userStore.isUserInfoComplete()) {
// 如果信息不完整,跳转到完善信息页面
setTimeout(() => {
uni.navigateTo({
url: `/pages/mine/profile/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
uni.navigateTo({ url: "/pages/mine/profile/complete-profile" });
}, 1000);
} else {
// 否则直接跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
uni.reLaunch({ url: redirect.value });
}, 1000);
}
// #endif

View File

@@ -1,23 +1,29 @@
<template>
<view class="app-container">
<view class="app-container dark:text-[var(--wot-dark-color)]">
<wd-card>
<view class="flex items-center p-2">
<wd-img width="60px" height="60px" src="/static/logo.png" mode="aspectFit" class="mr-4" />
<view class="flex-1">
<text class="text-lg font-bold block mb-1">vue-uniapp-template</text>
<text class="text-xs text-gray-500">版本 {{ version }}</text>
<view class="about-header">
<wd-img
width="120rpx"
height="120rpx"
src="/static/logo.png"
mode="aspectFit"
class="about-logo"
/>
<view class="about-meta">
<text class="app-title">vue-uniapp-template</text>
<text class="app-version">版本 {{ version }}</text>
</view>
</view>
</wd-card>
<!-- 公司信息区域 -->
<wd-card custom-style="margin-top: 20rpx">
<view class="p-4">
<view class="flex items-center mb-3">
<wd-icon name="company" size="18" class="mr-2" />
<text class="text-base font-medium">有来开源组织</text>
<view class="company-section">
<view class="section-title">
<wd-icon name="company" size="18" />
<text>有来开源组织</text>
</view>
<text class="text-sm text-gray-500 block pl-6">专注于快速构建和高效开发的应用解决方案</text>
<text class="section-subtitle">专注于快速构建和高效开发的应用解决方案</text>
</view>
</wd-card>
@@ -25,19 +31,19 @@
<wd-card title="优质项目" custom-style="margin-top: 20rpx">
<wd-cell-group border>
<wd-cell title="vue3-element-admin" icon="desktop">
<view slot="label" class="text-xs text-gray-500 mt-1">
基于 Vue3 + Vite5 + TypeScript5 + Element-Plus + Pinia 构建的中后台管理模板
<view slot="label" class="project-desc">
基于 Vue3 + Vite7 + TypeScript5 + Element-Plus + Pinia 构建的中后台管理模板
</view>
</wd-cell>
<wd-cell title="vue-uniapp-template" icon="mobile">
<view slot="label" class="text-xs text-gray-500 mt-1">
<view slot="label" class="project-desc">
基于 uni-app + Vue 3 + TypeScript集成多种工具的移动端应用模板
</view>
</wd-cell>
<wd-cell title="youlai-boot" icon="server">
<view slot="label" class="text-xs text-gray-500 mt-1">
<view slot="label" class="project-desc">
基于 Spring Boot 3 + Vue 3 构建的前后端分离单体权限管理系统
</view>
</wd-cell>
@@ -54,9 +60,9 @@
</wd-card>
<!-- 底部版权信息 -->
<view class="text-center py-4">
<text class="block text-xs text-gray-400">Copyright © {{ getYear() }} 有来开源组织</text>
<text class="block text-xs text-gray-400 mt-1">All Rights Reserved</text>
<view class="about-footer">
<text class="copyright">Copyright © {{ getYear() }} 有来开源组织</text>
<text class="copyright copyright-sub">All Rights Reserved</text>
</view>
</view>
</template>
@@ -75,14 +81,73 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
:deep(.wd-card__header) {
font-size: 30rpx;
font-weight: 500;
color: var(--wot-color-text, #333);
.about-header {
display: flex;
align-items: center;
padding: 20rpx;
}
:deep(.wd-cell__icon) {
font-size: 36rpx;
color: var(--wot-color-theme, #409eff);
.about-logo {
margin-right: 16rpx;
}
.about-meta {
flex: 1;
}
.app-title {
display: block;
margin-bottom: 8rpx;
font-size: 32rpx;
font-weight: 700;
}
.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;
padding-left: 32rpx;
font-size: 24rpx;
color: #6b7280;
}
.project-desc {
margin-top: 8rpx;
font-size: 24rpx;
color: #6b7280;
}
.about-footer {
padding: 32rpx 0;
text-align: center;
}
.copyright {
display: block;
font-size: 24rpx;
color: #9ca3af;
}
.copyright-sub {
margin-top: 8rpx;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<view class="faq-container">
<view class="wechat">
<view class="tips">
<view class="app-container dark:text-[var(--wot-dark-color)]">
<view>
<view class="text-center font-600">
<text>长按关注有来技术公众号获取交流群二维码</text>
</view>
<view class="flex-center">
@@ -12,12 +12,12 @@
mode="aspectFit"
/>
</view>
<view>
<view class="mt-10rpx">
<text>如果交流群的二维码过期请加微信(</text>
<text :user-select="true" :selectable="true">haoxianrui</text>
<text>)并备注前端后端全栈以获取最新二维码</text>
</view>
<view>
<view class="mt-10rpx">
<text>为确保交流群质量防止营销广告人群混入我们采取了此措施望各位理解</text>
</view>
</view>
@@ -135,20 +135,4 @@ const pagesStr = ref<string>(`<pre style="background-color: #f9f9fa"><code>
onMounted(() => {});
</script>
<style lang="scss" scoped>
.faq-container {
min-height: 100vh;
background-color: #f5f5f5;
.wechat {
padding: 30rpx;
margin: 20px 0;
font-size: 14px;
color: var(--wot-card-content-color, rgba(0, 0, 0, 0.45));
background-color: #fff;
.tips {
font-weight: bold;
text-align: center;
}
}
}
</style>
<style lang="scss" scoped></style>

View File

@@ -1,11 +1,7 @@
<template>
<view class="app-container">
<wd-navbar title="意见反馈" left-arrow @click-left="handleBack" />
123
<view class="app-container dark:text-[var(--wot-dark-color)]">
<wd-text size="small">选填最多上传3张图片</wd-text>
<wd-form ref="formRef" :model="formData" :rules="rules">
<!-- 问题类型选择 -->
<wd-form-item label="问题类型" prop="feedbackType">
<wd-radio-group v-model="formData.feedbackType" inline>
<wd-radio v-for="item in feedbackTypes" :key="item.value" :value="item.value">
@@ -41,7 +37,7 @@
</wd-form-item>
<!-- 提交按钮 -->
<view class="submit-btn">
<view class="mx-30rpx mt-40rpx">
<wd-button type="primary" block :loading="submitting" @click="handleSubmit">
提交反馈
</wd-button>
@@ -50,19 +46,23 @@
</view>
</template>
<route lang="json">
{
"name": "feedback",
"style": {
"navigationBarTitleText": "意见反馈"
},
"meta": { "requireAuth": true }
}
</route>
<script setup lang="ts">
import { checkLogin } from "@/utils/auth";
import { useToast } from "wot-design-uni";
import { FormRules } from "wot-design-uni/components/wd-form/types";
const toast = useToast();
const formRef = ref();
// 检查登录状态
onLoad(() => {
if (!checkLogin()) return;
});
// 问题类型选项
const feedbackTypes = [
{ label: "功能异常", value: "bug" },
@@ -138,51 +138,31 @@ const handleDelete = (detail: { index: number }) => {
// 提交反馈
const handleSubmit = async () => {
// 表单验证
try {
const { valid } = await formRef.value.validate();
const { valid } = await formRef.value.validate();
if (valid) {
submitting.value = true;
try {
// TODO: 调用提交反馈的接口
await new Promise((resolve) => setTimeout(resolve, 1500)); // 模拟提交
toast.success("提交成功");
if (valid) {
submitting.value = true;
try {
// TODO: 调用提交反馈的接口
await new Promise((resolve) => setTimeout(resolve, 1500)); // 模拟提交
toast.success("提交成功");
// 重置表单
formRef.value.reset();
formData.feedbackType = "bug";
formData.description = "";
formData.fileList = [];
formData.contact = "";
// 重置表单
formRef.value.reset();
formData.feedbackType = "bug";
formData.description = "";
formData.fileList = [];
formData.contact = "";
// 延迟返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1500);
} catch (_error) {
toast.error("提交失败,请重试");
} finally {
submitting.value = false;
}
// 延迟返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1500);
} finally {
submitting.value = false;
}
} catch (_error) {
// 表单验证失败
console.log("表单验证失败");
}
};
// 返回
const handleBack = () => {
uni.navigateBack();
};
</script>
<style lang="scss" scoped>
:deep(.wd-form-item) {
margin-bottom: 12rpx;
}
.submit-btn {
margin: 40rpx 30rpx;
}
</style>
<style lang="scss" scoped></style>

View File

@@ -13,7 +13,9 @@
</view>
<view class="user-details">
<block v-if="isLogin">
<view class="nickname">{{ userInfo!.nickname || "匿名用户" }}</view>
<view class="nickname">
{{ userInfo!.nickname || "匿名用户" }}
</view>
<view class="user-id">ID: {{ userInfo?.username || "0000000" }}</view>
</block>
<block v-else>
@@ -41,56 +43,55 @@
</view>
<!-- 数据统计 -->
<view class="stats-container">
<view class="stat-item" @click="navigateToSection('wallet')">
<view class="stat-value">0.00</view>
<view class="">我的余额</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">
<view class="flex-start">
<wd-icon name="tools" size="18" :color="currentThemeColor" />
<text>常用工具</text>
<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" />
</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" />
</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" />
</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" />
</view>
<view class="tool-label">关于我们</view>
<view class="text-24rpx">关于我们</view>
</view>
</view>
</view>
@@ -98,44 +99,47 @@
<!-- 推荐服务 -->
<view class="card-container">
<view class="card-header">
<view class="card-title">
<view class="flex-start">
<wd-icon name="star" size="18" :color="currentThemeColor" />
<text>推荐服务</text>
<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" />
</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" />
</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" />
</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" />
</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" />
</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" />
@@ -144,14 +148,8 @@
</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 />
@@ -161,6 +159,7 @@
<script lang="ts" setup>
import { useToast } from "wot-design-uni";
import { useUserStore, useThemeStore } from "@/store";
import { useRouter } from "uni-mini-router";
const toast = useToast();
const userStore = useUserStore();
@@ -170,15 +169,14 @@ const userInfo = computed(() => userStore.userInfo);
const isLogin = computed(() => !!userInfo.value);
const defaultAvatar = "/static/images/default-avatar.png";
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 } });
};
// 退出登录
@@ -201,27 +199,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" });
};
// 导航到各个板块
@@ -233,18 +231,6 @@ const navigateToSection = (section: string, subSection?: string) => {
icon: "none",
});
};
onShow(() => {
// 确保 tabbar 状态正确
const pages = getCurrentPages();
if (pages.length > 0) {
const currentPage = pages[pages.length - 1];
if (currentPage.route === "pages/mine/index") {
// 通过事件通知 tabbar 布局更新状态
uni.$emit("updateTabbar", "mine");
}
}
});
</script>
<route lang="json">
@@ -269,7 +255,7 @@ onShow(() => {
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 {
@@ -349,213 +335,152 @@ onShow(() => {
}
}
// 数据统计
.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);
.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);
}
}
.card-action {
display: flex;
align-items: center;
text {
margin-right: 8rpx;
font-size: 24rpx;
color: #999;
}
}
}
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
}
// 订单状态
.order-status {
// 卡片头部
.card-header {
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;
}
}
.status-label {
font-size: 24rpx;
color: #666;
}
}
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #e5e7eb;
}
// 工具网格
.tools-grid {
// 暗色模式下的卡片样式
:deep(.dark) .card-container {
background-color: #1f2937;
}
:deep(.dark) .card-header {
border-bottom-color: #374151;
}
// 工具项
.tool-item {
display: flex;
flex-wrap: wrap;
padding: 20rpx 0 10rpx;
flex-direction: column;
align-items: center;
width: 25%;
margin-bottom: 30rpx;
}
.tool-item {
display: flex;
flex-direction: column;
align-items: center;
width: 25%;
margin-bottom: 30rpx;
// 工具图标
.tool-icon {
display: flex;
align-items: center;
justify-content: center;
width: 90rpx;
height: 90rpx;
margin-bottom: 12rpx;
background-color: #f3f4f6;
border-radius: 18rpx;
transition: transform 0.15s ease;
.tool-icon {
display: flex;
align-items: center;
justify-content: center;
width: 90rpx;
height: 90rpx;
margin-bottom: 12rpx;
background-color: var(--wot-color-bg-light);
border-radius: 18rpx;
transition: transform 0.2s;
&:active {
transform: scale(0.95);
}
}
.tool-label {
font-size: 24rpx;
color: var(--wot-color-text);
}
&:active {
transform: scale(0.95);
}
}
// 服务列表
.services-list {
.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;
&:active {
background-color: var(--wot-color-bg-light);
}
&:last-child {
border-bottom: none;
}
.service-left {
display: flex;
align-items: center;
.service-icon {
display: flex;
align-items: center;
justify-content: center;
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
background-color: var(--wot-color-bg-light);
border-radius: 16rpx;
}
.service-info {
.service-name {
font-size: 28rpx;
font-weight: 500;
color: var(--wot-color-text);
}
.service-desc {
margin-top: 8rpx;
font-size: 24rpx;
color: var(--wot-color-text-secondary);
}
}
}
}
// 暗色模式下的工具图标样式
:deep(.dark) .tool-icon {
background-color: #374151;
}
// 退出登录按钮
.logout-btn-wrap {
// 数据统计卡片(减少原子类堆叠)
.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 #e5e7eb;
transition: background-color 0.15s ease;
&:active {
background-color: #f3f4f6;
}
&.service-item-last {
border-bottom: none;
}
}
// 暗色模式下的服务项样式
:deep(.dark) .service-item {
border-bottom-color: #374151;
&:active {
background-color: rgba(255, 255, 255, 0.1);
}
}
// 服务图标
.service-icon {
display: flex;
align-items: center;
justify-content: center;
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
background-color: #f3f4f6;
border-radius: 16rpx;
}
// 暗色模式下的服务图标样式
:deep(.dark) .service-icon {
background-color: #374151;
}
// 登录按钮样式
:deep(.btn-login) {
font-size: 24rpx !important;
border-radius: 20rpx !important;
}
// 退出登录按钮样式
:deep(.logout-button) {
width: 100% !important;
height: 80rpx !important;
font-size: 32rpx !important;
font-weight: bold !important;
border-radius: 40rpx !important;
}
</style>

View File

@@ -1,7 +1,5 @@
<template>
<view class="app-container">
<wd-navbar title="完善个人信息" left-arrow @click-left="handleBack" />
<view class="app-container dark:text-[var(--wot-dark-color)]">
<!-- 头部标题 -->
<view class="header">
<view class="title">完善个人信息</view>
@@ -164,7 +162,7 @@ import FileAPI, { type FileInfo } from "@/api/file";
const toast = useToast();
const userStore = useUserStore();
// 页面参数
// 页面参数:使用 query 读取重定向
const redirect = ref("/pages/index/index");
// 表单数据
@@ -356,9 +354,7 @@ const handleComplete = async () => {
// 跳转到目标页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
uni.reLaunch({ url: redirect.value });
}, 1000);
} catch (error: any) {
console.error("完善信息失败:", error);
@@ -375,18 +371,11 @@ const handleSkip = () => {
content: "跳过信息完善可能会影响部分功能使用,确定要跳过吗?",
success: (res) => {
if (res.confirm) {
uni.reLaunch({
url: redirect.value,
});
uni.reLaunch({ url: redirect.value });
}
},
});
};
// 返回
function handleBack() {
uni.navigateBack();
}
</script>
<style lang="scss" scoped>

View File

@@ -1,7 +1,5 @@
<template>
<view class="app-container">
<wd-navbar title="个人信息" left-arrow @click-left="handleBack" />
<view class="app-container dark:text-[var(--wot-dark-color)]">
<wd-card v-if="userProfile" custom-style="margin-top: 20rpx">
<wd-cell-group border>
<wd-cell class="avatar-cell" title="头像" center is-link>

View File

@@ -1,5 +1,5 @@
<template>
<view class="app-container">
<view class="app-container dark:text-[var(--wot-dark-color)]">
<wd-card>
<wd-cell-group border>
<wd-cell

View File

@@ -1,5 +1,5 @@
<template>
<view class="app-container">
<view class="app-container dark:text-[var(--wot-dark-color)]">
<wd-collapse v-model="activeNames" accordion>
<wd-collapse-item
v-for="(section, index) in agreementContent"

View File

@@ -1,5 +1,5 @@
<template>
<view class="app-container dark-mode">
<view class="app-container dark:text-[var(--wot-dark-color)]">
<wd-cell-group>
<wd-cell v-if="isLogin" title="个人资料" icon="user" is-link @click="navigateToProfile" />
<wd-cell

View File

@@ -1,5 +1,5 @@
<template>
<view class="app-container">
<view class="app-container dark:text-[var(--wot-dark-color)]">
<!-- 网络状态展示 -->
<wd-card>
<wd-cell-group border>

View File

@@ -1,5 +1,5 @@
<template>
<view class="app-container">
<view class="app-container dark:text-[var(--wot-dark-color)]">
<wd-status-tip type="search" tip="建设中..." />
</view>
</template>

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);
});
@@ -29,8 +32,10 @@ const router = createRouter({
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 检查页面是否需要登录
if (to.meta && to.meta.requireAuth && !isLoggedIn()) {
const redirectPath = (to.path || "/pages/index/index") as string;
// 先取消本次导航,避免在异步对话框中遗漏 next 导致报错
next(false);
uni.showModal({
title: "提示",
content: "该功能需要登录后使用",
@@ -38,32 +43,20 @@ router.beforeEach((to, from, next) => {
cancelText: "返回",
success: (res) => {
if (res.confirm) {
// 记住原来要去的页面
uni.setStorageSync("redirect", to.fullPath);
// 使用 uni 原生导航而不是 router
uni.navigateTo({
url: "/pages/login/index",
});
} else {
// 取消则返回首页
uni.switchTab({
url: "/pages/index/index",
router.push({
path: "/pages/login/index",
query: { redirect: encodeURIComponent(redirectPath) },
});
}
},
// 确保在取消弹窗时也能调用 next
fail: () => {
next(false);
},
});
} else {
// 继续导航
next();
}
});
router.afterEach((to) => {
console.log("路由跳转完成:", to.path);
router.afterEach((to, from) => {
console.log("🎯 afterEach 钩子触发:", { to, from });
});
export default router;

View File

@@ -7,5 +7,5 @@ body,
}
.app-container {
padding: 0 10rpx;
padding: 10rpx;
}

21
src/types/uni-mini-router.d.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
// 扩展 uni-mini-router 路由类型,支持从 pages.json 注入的 meta 字段
// 该声明文件会被 ts 自动合并到库类型中
import "uni-mini-router";
declare module "uni-mini-router" {
/** 自定义路由元信息类型,可按需扩展 */
interface RouteMeta {
/** 是否需要登录 */
requireAuth?: boolean;
/** 其他自定义字段 */
[key: string]: any;
}
interface Route {
/** 来自 pages.json 的 meta 字段(动态生成时透传) */
meta?: RouteMeta;
}
}
export {};