feat: 优化主题管理和登录流程

refactor(theme): 重构主题管理逻辑,增加自定义颜色选项和持久化存储
refactor(login): 优化登录流程,增加防重复提交和错误处理
fix(request): 修复请求循环跳转问题,移除publicRequest方法
style: 更新页面样式和布局,改进用户体验
docs: 移除无用注释,更新代码文档
This commit is contained in:
zhangtao
2025-08-15 02:06:05 +08:00
parent da425d62d1
commit 66b5514463
19 changed files with 749 additions and 275 deletions
-1
View File
@@ -5,7 +5,6 @@ import { useTheme } from "@/composables/useTheme";
const { initTheme } = useTheme();
onLaunch(() => {
console.log("App Launch");
// 初始化主题
initTheme();
});
+10 -7
View File
@@ -1,6 +1,7 @@
import request, { publicRequest } from "@/utils/request";
import request from "@/utils/request";
import { ApiHeader } from "@/enums/api-header.enum";
const AUTH_BASE_URL = "/api/v1/system/auth";
const AUTH_BASE_URL = "/system/auth";
const AuthAPI = {
/**
@@ -12,10 +13,9 @@ const AuthAPI = {
return request<LoginResult>({
url: `${AUTH_BASE_URL}/login`,
method: "POST",
headers: {
"Content-Type": "multipart/form-data",
},
headers: { [ApiHeader.KEY]: ApiHeader.FORM },
data: body,
skipAuth: true,
});
},
@@ -40,6 +40,7 @@ const AuthAPI = {
return request<CaptchaInfo>({
url: `${AUTH_BASE_URL}/captcha/get`,
method: "GET",
skipAuth: true,
});
},
@@ -62,10 +63,11 @@ const AuthAPI = {
* @returns 登录结果
*/
loginByWxMiniAppPhone(data: WxLoginData): Promise<LoginResult> {
return publicRequest<LoginResult>({
return request<LoginResult>({
url: `${AUTH_BASE_URL}/wx/miniapp/phone-login`,
method: "POST",
data,
skipAuth: true,
});
},
@@ -75,10 +77,11 @@ const AuthAPI = {
* @returns 登录结果
*/
loginByWxMiniAppCode(code: string): Promise<LoginResult> {
return publicRequest<LoginResult>({
return request<LoginResult>({
url: `${AUTH_BASE_URL}/wx/miniapp/code-login`,
method: "POST",
data: { code },
skipAuth: true,
});
},
};
+1 -1
View File
@@ -11,7 +11,7 @@ const FileAPI = {
/**
* 文件上传地址
*/
uploadUrl: baseApi + "/api/v1/files/upload",
uploadUrl: baseApi + "/files/upload",
/**
* 上传文件
+1 -1
View File
@@ -1,6 +1,6 @@
import request from "@/utils/request";
const USER_BASE_URL = "/api/v1/system/user";
const USER_BASE_URL = "/system/user";
const UserAPI = {
/**
@@ -915,7 +915,7 @@ export default {
.exec((res) => {
if (res[0]) {
const canvas = res[0].node;
const ctx = canvas.getContext("2d");
const ctx = canvas.getContext("2d", { willReadFrequently: false });
cfu.option[cid].context = ctx;
cfu.option[cid].rotateLock = cfu.option[cid].rotate;
if (
+209 -60
View File
@@ -1,4 +1,5 @@
import type { ConfigProviderThemeVars } from "wot-design-uni";
import { useThemeStore } from "@/store/modules/theme.store";
// 定义主题色选项
export interface ThemeColorOption {
@@ -15,8 +16,32 @@ export const themeColorOptions: ThemeColorOption[] = [
{ 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;
}
export function useTheme() {
// 状态定义
const theme = ref<"light" | "dark">("light");
@@ -25,24 +50,7 @@ export function useTheme() {
const currentThemeColor = ref<ThemeColorOption>(themeColorOptions[0]);
const showThemeColorSheet = ref(false);
const colorColumns = [
{ value: "#165DFF", label: "蓝色" },
{ value: "#0FC6C2", label: "青绿色" },
{ value: "#722ED1", label: "紫色" },
{ value: "#F5222D", label: "红色" },
{ value: "#FA8C16", label: "橙色" },
{ value: "#FADB14", label: "黄色" },
{ value: "#52C41A", label: "绿色" },
{ value: "#EB2F96", label: "粉色" },
{ value: "#13C2C2", label: "青色" },
{ value: "#1890FF", label: "天蓝色" },
{ value: "#CD5C5C", label: "经典红" },
{ value: "#228B22", label: "自然绿" },
{ value: "#FF6B6B", label: "珊瑚红" },
{ value: "#4ECDC4", label: "薄荷绿" },
{ value: "#FFD166", label: "黄金黄色" },
];
// 主题变量
const themeVars = reactive<ConfigProviderThemeVars>({
darkBackground: "#0f0f0f",
darkBackground2: "#1a1a1a",
@@ -60,11 +68,36 @@ export function useTheme() {
// 计算属性
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; // 不再跟随系统
hasUserSet.value = true;
followSystem.value = false;
setNavigationBarColor();
saveThemeSettings();
// 实时应用主题变化
applyThemeModeToApp();
}
/* 实时应用主题模式到应用 */
function applyThemeModeToApp() {
// 更新CSS变量
if (typeof document !== "undefined") {
document.documentElement.setAttribute("data-theme", theme.value);
}
// 更新导航栏颜色
setNavigationBarColor();
}
@@ -73,124 +106,240 @@ export function useTheme() {
followSystem.value = follow;
if (follow) {
hasUserSet.value = false;
initTheme(); // 重新获取系统主题
initTheme();
}
}
saveThemeSettings();
/* 设置导航栏颜色 */
function setNavigationBarColor() {
uni.setNavigationBarColor({
frontColor: theme.value === "light" ? "#000000" : "#ffffff",
backgroundColor: theme.value === "light" ? "#ffffff" : "#000000",
});
// 实时应用主题变化
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
// 微信小程序使用 getAppBaseInfo
const appBaseInfo = uni.getAppBaseInfo();
if (appBaseInfo && appBaseInfo.theme) {
if (appBaseInfo?.theme) {
return appBaseInfo.theme as "light" | "dark";
}
// #endif
// #ifndef MP-WEIXIN
// 其他平台使用 getSystemInfoSync
const systemInfo = uni.getSystemInfoSync();
if (systemInfo && systemInfo.theme) {
if (systemInfo?.theme) {
return systemInfo.theme as "light" | "dark";
}
// #endif
} catch (error) {
console.warn("获取系统主题失败:", error);
}
return "light"; // 默认返回 light
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) {
console.log("使用用户设置的主题:", theme.value);
setNavigationBarColor();
applyThemeColorToApp(currentThemeColor.value.primary);
return;
}
// 获取系统主题
const systemTheme = getSystemTheme();
// 如果是首次启动或跟随系统,使用系统主题
if (!hasUserSet.value || followSystem.value) {
theme.value = systemTheme;
if (!hasUserSet.value) {
followSystem.value = true;
} else {
console.log("跟随系统主题:", theme.value);
}
}
setNavigationBarColor();
applyThemeColorToApp(currentThemeColor.value.primary);
}
/* 打开主题色选择 */
/* 主题色选择器相关 */
function openThemeColorPicker() {
showThemeColorSheet.value = true;
}
/* 关闭主题色选择 */
function closeThemeColorPicker() {
showThemeColorSheet.value = false;
}
/* 选择主题色 */
function selectThemeColor(option: ThemeColorOption) {
setCurrentThemeColor(option);
closeThemeColorPicker();
}
// 检查函数是否存在的工具函数
const isFunction = (fn: any): boolean => typeof fn === "function";
// 生命周期
onBeforeMount(() => {
loadThemeSettings();
initTheme();
if (isFunction(uni.onThemeChange)) {
uni.onThemeChange((res) => {
toggleTheme(res.theme);
// #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 (isFunction(uni.offThemeChange)) {
uni.offThemeChange((res) => {
toggleTheme(res.theme);
});
// 清理监听器
if (uni.offThemeChange) {
uni.offThemeChange(() => {});
}
});
return {
// 状态
theme: computed(() => theme.value),
isDark,
colorColumns,
followSystem: computed(() => followSystem.value),
hasUserSet: computed(() => hasUserSet.value),
currentThemeColor: computed(() => currentThemeColor.value),
showThemeColorSheet,
themeVars,
showThemeColorSheet: computed(() => showThemeColorSheet.value),
themeState,
// 主题选项
themeColorOptions,
initTheme,
extendedColorOptions,
// 主题变量
themeVars,
// 方法
toggleTheme,
setFollowSystem,
setCurrentThemeColor,
setCustomThemeColor,
resetTheme,
initTheme,
// 主题色选择器
openThemeColorPicker,
closeThemeColorPicker,
selectThemeColor,
// 工具方法
saveThemeSettings,
loadThemeSettings,
};
}
+15
View File
@@ -0,0 +1,15 @@
/**
* API 请求头相关枚举封装
*/
export const enum ApiHeader {
KEY = "Content-Type",
/* 表单数据 */
FORM = "application/x-www-form-urlencoded",
/* JSON 数据 */
JSON = "application/json",
/* 多部分数据 */
MULTIPART = "multipart/form-data",
}
+80 -46
View File
@@ -92,8 +92,8 @@
<!-- 登录按钮 -->
<button
class="login-btn"
:disabled="loading"
:style="{ opacity: loading ? 0.7 : 1 }"
:disabled="loading || !isFormValid"
:style="{ opacity: loading || !isFormValid ? 0.7 : 1 }"
@click="handleAccountLogin"
>
<wd-loading v-if="loading" size="20" color="#fff" />
@@ -163,12 +163,11 @@ import { useUserStore } from "@/store/modules/user.store";
import { useToast } from "wot-design-uni";
import { useWechat } from "@/composables/useWechat";
import { useTheme } from "@/composables/useTheme";
import { computed, ref, reactive } from "vue";
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 showPassword = ref(false);
const loginType = ref<"account" | "phone">("account");
@@ -186,7 +185,7 @@ const loginFormData = ref<LoginFormData>({
// 验证码状态
const captchaState = reactive<CaptchaInfo>({
enable: false, // 默认不启用,等待后端确认
enable: false,
key: "",
img_base: "",
});
@@ -194,14 +193,22 @@ 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;
return !!(username?.trim() && password && (!captchaState.enable || (captcha && captcha.trim())));
});
// 获取验证码
const getLoginCaptcha = async () => {
if (isCaptchaLoading.value) return;
isCaptchaLoading.value = true;
try {
const result = await AuthAPI.getCaptcha();
// 确保数据结构正确
if (result && typeof result === "object") {
captchaState.enable = Boolean(result.enable);
captchaState.key = result.key || "";
@@ -222,74 +229,102 @@ const getLoginCaptcha = async () => {
}
};
// 修复后的账号密码登录
const handleAccountLogin = async () => {
if (loading.value) return;
// 统一的错误处理
const handleLoginError = (error: any, loginType: string) => {
const message = error?.message || `${loginType}登录失败`;
if (!loginFormData.value.username.trim()) {
toast.error("请输入用户名");
return;
// 根据错误类型显示不同提示
if (message.includes("验证码")) {
toast.error("验证码错误,请重新输入");
getLoginCaptcha(); // 刷新验证码
} else if (message.includes("用户不存在") || message.includes("密码错误")) {
toast.error("用户名或密码错误");
} else if (message.includes("拒绝授权")) {
toast.error("您已拒绝授权");
} else {
toast.error(message);
}
if (!loginFormData.value.password) {
toast.error("请输入密码");
return;
}
if (captchaState.enable && !loginFormData.value.captcha.trim()) {
toast.error("请输入验证码");
return;
console.error(`${loginType}登录失败:`, error);
};
// 账号密码登录
const handleAccountLogin = async () => {
if (!isFormValid.value) {
if (!loginFormData.value.username.trim()) {
toast.error("请输入用户名");
return;
}
if (!loginFormData.value.password) {
toast.error("请输入密码");
return;
}
if (captchaState.enable && !loginFormData.value.captcha.trim()) {
toast.error("请输入验证码");
return;
}
}
try {
loading.value = true;
await userStore.login(loginFormData.value);
toast.success("登录成功");
// 登录成功后跳转到mine页面,确保用户信息及时更新
uni.switchTab({ url: "/pages/mine/index" });
} catch (error: any) {
toast.error(error?.message || "登录失败");
// 登录失败后刷新验证码
getLoginCaptcha();
} finally {
loading.value = false;
handleLoginError(error, "账号密码");
}
};
// 微信一键登录(通过手机号)
const handleWechatPhoneLogin = async (e: any) => {
if (loading.value || authState.value.isLogining) return;
loading.value = true;
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) {
if (error.message === "用户拒绝授权") {
toast.error("您已拒绝授权获取手机号");
} else {
toast.error(error?.message || "登录失败");
}
} finally {
loading.value = false;
handleLoginError(error, "微信手机号");
}
};
// 微信授权登录
const handleWechatLogin = async () => {
if (loading.value) return;
loading.value = true;
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) {
toast.error(error?.message || "微信登录失败");
} finally {
loading.value = false;
handleLoginError(error, "微信授权");
}
};
// 监听验证码状态变化
watch(
() => captchaState.enable,
(newVal) => {
if (newVal && !captchaState.img_base) {
getLoginCaptcha();
}
}
);
// 是否暗黑模式
const isDarkMode = computed(() => theme.value === "dark");
@@ -302,9 +337,9 @@ const navigateToPrivacy = () => {
uni.navigateTo({ url: "/pages/mine/settings/privacy/index" });
};
// 页面加载时获取验证码 - 使用UniApp的onLoad生命周期
// 页面加载时获取验证码
onLoad(() => {
// 只在需要时获取验证码
uni.setNavigationBarTitle({ title: "用户登录" });
getLoginCaptcha();
});
</script>
@@ -316,7 +351,6 @@ onLoad(() => {
flex-direction: column;
align-items: center;
height: 100%;
min-height: 100vh;
overflow: hidden;
background-color: var(--wot-color-bg-container);
}
@@ -359,7 +393,7 @@ onLoad(() => {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 120rpx;
margin-top: 80rpx;
}
.logo {
@@ -387,7 +421,7 @@ onLoad(() => {
z-index: 2;
display: flex;
flex-direction: column;
width: 90%;
width: 95%;
margin-top: 80rpx;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.9);
+39 -11
View File
@@ -14,7 +14,7 @@
<view class="user-details">
<block v-if="isLogin">
<view class="name">{{ userInfo!.name || "匿名用户" }}</view>
<view class="user-id">ID: {{ userInfo?.username || "0000000" }}</view>
<view class="user-id">账号: {{ userInfo?.username || "user" }}</view>
</block>
<block v-else>
<view class="login-prompt">立即登录</view>
@@ -62,33 +62,33 @@
<view class="card-container">
<view class="card-header">
<view class="card-title">
<wd-icon name="tools" size="18" :color="currentThemeColor" />
<wd-icon name="tools" size="18" :color="currentThemeColor.primary" />
<text>常用工具</text>
</view>
</view>
<view class="tools-grid">
<view class="tool-item" @click="navigateToProfile">
<view class="tool-icon">
<wd-icon name="user" size="24" :color="currentThemeColor" />
<wd-icon name="user" size="24" :color="currentThemeColor.primary" />
</view>
<view class="tool-label">个人资料</view>
</view>
<view class="tool-item" @click="navigateToFAQ">
<view class="tool-icon">
<wd-icon name="help-circle" size="24" :color="currentThemeColor" />
<wd-icon name="help-circle" size="24" :color="currentThemeColor.primary" />
</view>
<view class="tool-label">常见问题</view>
</view>
<view class="tool-item" @click="handleQuestionFeedback">
<view class="tool-icon">
<wd-icon name="check-circle" size="24" :color="currentThemeColor" />
<wd-icon name="check-circle" size="24" :color="currentThemeColor.primary" />
</view>
<view class="tool-label">问题反馈</view>
</view>
<view class="tool-item" @click="navigateToAbout">
<view class="tool-icon">
<wd-icon name="info-circle" size="24" :color="currentThemeColor" />
<wd-icon name="info-circle" size="24" :color="currentThemeColor.primary" />
</view>
<view class="tool-label">关于我们</view>
</view>
@@ -99,7 +99,7 @@
<view class="card-container">
<view class="card-header">
<view class="card-title">
<wd-icon name="star" size="18" :color="currentThemeColor" />
<wd-icon name="star" size="18" :color="currentThemeColor.primary" />
<text>推荐服务</text>
</view>
</view>
@@ -107,7 +107,7 @@
<view class="service-item" @click="navigateToSection('services', 'vip')">
<view class="service-left">
<view class="service-icon">
<wd-icon name="dong" size="22" :color="currentThemeColor" />
<wd-icon name="dong" size="22" :color="currentThemeColor.primary" />
</view>
<view class="service-info">
<view class="service-name">会员中心</view>
@@ -119,7 +119,7 @@
<view class="service-item" @click="navigateToSection('services', 'coupon')">
<view class="service-left">
<view class="service-icon">
<wd-icon name="discount" size="22" :color="currentThemeColor" />
<wd-icon name="discount" size="22" :color="currentThemeColor.primary" />
</view>
<view class="service-info">
<view class="service-name">优惠券</view>
@@ -131,7 +131,7 @@
<view class="service-item" @click="navigateToSection('services', 'invite')">
<view class="service-left">
<view class="service-icon">
<wd-icon name="share" size="22" :color="currentThemeColor" />
<wd-icon name="share" size="22" :color="currentThemeColor.primary" />
</view>
<view class="service-info">
<view class="service-name">邀请有礼</view>
@@ -163,7 +163,7 @@ 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 } from "vue";
import { computed, ref, watch } from "vue";
const toast = useToast();
const userStore = useUserStore();
@@ -171,6 +171,7 @@ const { currentThemeColor } = useTheme();
const userInfo = computed(() => userStore.userInfo);
const isLogin = computed(() => !!userInfo.value);
const defaultAvatar = "/static/images/default-avatar.png";
const isLoading = ref(false);
// 登录
const navigateToLoginPage = () => {
@@ -246,7 +247,34 @@ onShow(() => {
uni.$emit("updateTabbar", "mine");
}
}
// 每次显示页面时都检查并刷新用户信息
loadUserInfo();
});
// 加载用户信息
const loadUserInfo = async () => {
if (isLogin.value) {
isLoading.value = true;
try {
await userStore.getInfo();
} catch (error) {
console.error("获取用户信息失败", error);
} finally {
isLoading.value = false;
}
}
};
// 监听用户信息变化,确保数据及时更新
watch(
() => userInfo.value,
() => {},
{
deep: true,
immediate: true,
}
);
</script>
<route lang="json">
+318 -56
View File
@@ -12,83 +12,154 @@
<!-- 暗黑模式设置 -->
<wd-card class="mb-3">
<view class="flex-between py-2">
<text>暗黑模式</text>
<wd-switch :model-value="theme === 'dark'" @change="toggleTheme" />
<view>
<text class="font-medium">暗黑模式</text>
</view>
<wd-switch v-model:model-value="isDark" active-color="var(--wot-color-theme)" @change="toggleTheme" />
</view>
</wd-card>
<!-- 跟随系统主题 -->
<wd-card class="mb-3">
<view class="flex-between py-2">
<view>
<text class="font-medium">跟随系统</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-grid">
<view
v-for="item in colorColumns"
:key="item.value"
class="color-item"
:class="{ active: currentThemeColor === item.value }"
@click="setThemeColor(item.value)"
>
<view class="color-box" :style="{ backgroundColor: item.value }">
<wd-icon v-if="currentThemeColor === item.value" name="check" size="16" color="#fff" />
<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>
<text class="color-label">{{ item.label }}</text>
<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" />
<text>自定义颜色</text>
<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 }"></view>
<text class="text-sm text-gray-500">{{ currentThemeColor }}</text>
<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-card>
<!-- 预览效果 -->
<wd-card title="预览效果" class="mb-3">
<view class="py-2">
<view class="flex-start gap-2">
<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>
</view>
</wd-card>
<!-- 重置按钮 -->
<view class="mt-5 mx-3">
<wd-button plain block @click="handleReset">恢复默认</wd-button>
<wd-button plain block :disabled="currentThemeColor.value === themeColorOptions[0].value &&
theme === 'light' &&
followSystem
" @click="handleReset">
恢复默认
</wd-button>
</view>
<!-- 自定义颜色弹窗 -->
<wd-popup v-model="showCustomColorPopup" position="bottom" closeable>
<view class="custom-color-popup">
<view class="text-center mb-5"><text class="text-lg font-bold">自定义主题色</text></view>
<view class="mb-5">
<view class="color-preview-large" :style="{ backgroundColor: customColor }"></view>
<wd-input v-model="customColor" placeholder="请输入颜色值,如 #FF6B6B" clearable />
<text class="input-tip">支持 HEX 格式颜色值</text>
<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>
<view class="mb-5">
<view class="color-preview-large mb-4" :style="{ backgroundColor: customColor }"></view>
<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>
<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>
<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 @click="applyCustomColor">应用</wd-button>
<wd-button type="primary" block :disabled="!isValidColor(customColor)" @click="applyCustomColor">
应用
</wd-button>
</view>
</view>
</wd-popup>
</view>
</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, colorColumns, toggleTheme, setThemeColor, resetTheme } =
useTheme();
const {
theme,
currentThemeColor,
themeColorOptions,
toggleTheme,
selectThemeColor,
resetTheme,
setCustomThemeColor,
followSystem,
setFollowSystem,
isDark,
} = useTheme();
// 自定义颜色相关
const showCustomColorPopup = ref(false);
const customColor = ref(currentThemeColor.value);
const customColor = ref(currentThemeColor.value.primary);
// 快速颜色选择
const quickColors = [
"#FF6B6B",
"#4ECDC4",
"#45B7D1",
"#96CEB4",
"#FECA57",
"#FF9FF3",
"#54A0FF",
"#5F27CD",
"#00D2D3",
"#FF9F43",
"#10AC84",
"#EE5A24",
"#009432",
"#0652DD",
"#9980FA",
];
// 动态设置页面标题
onLoad(() => {
@@ -97,11 +168,33 @@ onLoad(() => {
});
});
// 监听主题变化,确保实时生效
onShow(() => {
// 强制应用当前主题色
setTimeout(() => {
customColor.value = currentThemeColor.value.primary;
}, 50);
});
// 监听主题更新事件
onMounted(() => {
uni.$on('theme-color-changed', (color: string) => {
customColor.value = color;
});
});
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 applyCustomColor = () => {
const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
if (!colorRegex.test(customColor.value)) {
if (!isValidColor(customColor.value)) {
uni.showToast({
title: "请输入正确的颜色格式",
icon: "none",
@@ -109,12 +202,32 @@ const applyCustomColor = () => {
return;
}
setThemeColor(customColor.value);
setCustomThemeColor(customColor.value);
showCustomColorPopup.value = false;
uni.showToast({
title: "主题颜色已更新",
icon: "success",
});
// 强制刷新当前页面样式
setTimeout(() => {
uni.$emit('theme-updated');
}, 50);
};
// 选择主题色
const handleSelectColor = (colorOption: ThemeColorOption) => {
selectThemeColor(colorOption);
uni.showToast({
title: "主题色已更新",
icon: "success",
});
// 强制刷新当前页面样式
setTimeout(() => {
customColor.value = currentThemeColor.value.primary;
uni.$emit('theme-updated');
}, 50);
};
// 重置主题
@@ -125,11 +238,16 @@ const handleReset = () => {
success: (res) => {
if (res.confirm) {
resetTheme();
customColor.value = currentThemeColor.value;
customColor.value = currentThemeColor.value.primary;
uni.showToast({
title: "已恢复默认",
icon: "success",
});
// 强制刷新当前页面样式
setTimeout(() => {
uni.$emit('theme-updated');
}, 50);
}
},
});
@@ -142,12 +260,12 @@ const handleBack = () => {
// 页面显示时更新自定义颜色值
onShow(() => {
customColor.value = currentThemeColor.value;
customColor.value = currentThemeColor.value.primary;
});
</script>
<style lang="scss" scoped>
.page-header {
padding: 40rpx 20rpx;
margin-top: 20rpx;
text-align: center;
background: linear-gradient(135deg, var(--wot-color-theme) 0%, var(--primary-color-light) 100%);
@@ -169,63 +287,207 @@ onShow(() => {
.color-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx 20rpx;
gap: 24rpx;
padding: 32rpx 0;
}
.color-item {
display: flex;
flex-direction: column;
gap: 8rpx;
gap: 16rpx;
align-items: center;
padding: 8rpx;
cursor: pointer;
border-radius: 16rpx;
transition: all 0.2s ease;
&.active .color-box {
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.15);
transform: scale(1.1);
&.active {
color: white;
.color-label {
color: white;
}
}
&:active {
transform: scale(0.95);
}
.color-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
border-radius: 12rpx;
transition: all 0.3s ease;
width: 64rpx;
height: 64rpx;
border-radius: 50%;
&.small {
width: 40rpx;
height: 40rpx;
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 {
font-size: 22rpx;
font-size: 24rpx;
color: var(--wot-color-text-secondary);
text-align: center;
white-space: nowrap;
transition: color 0.2s ease;
}
}
.custom-color-popup {
padding: 40rpx 30rpx;
background-color: var(--wot-color-bg-container);
padding: 48rpx 40rpx;
background-color: var(--wot-color-bg);
border-radius: 32rpx 32rpx 0 0;
.color-preview-large {
width: 100%;
height: 120rpx;
margin-bottom: 30rpx;
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: 15rpx;
margin-top: 12rpx;
font-size: 24rpx;
color: var(--wot-color-text-placeholder);
text-align: center;
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 {
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-start {
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 {
border-radius: 8rpx;
}
.rounded-full {
border-radius: 50%;
}
.ring-2 {
--tw-ring-offset-width: 2px;
}
.ring-offset-2 {
--tw-ring-offset-width: 2px;
}
.ring-current {
--tw-ring-color: currentColor;
}
.shadow-sm {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.transition-all {
transition: all 0.2s ease;
}
.duration-200 {
transition-duration: 200ms;
}
.ease-in-out {
transition-timing-function: ease-in-out;
}
.block {
display: block;
}
</style>
+1
View File
@@ -26,6 +26,7 @@ export const useThemeStore = defineStore("theme", () => {
if (typeof document !== "undefined") {
// H5环境
document.documentElement.style.setProperty("--primary-color", color);
document.documentElement.style.setProperty("--wot-color-theme", color);
// 设置简单的衍生色(不依赖外部工具函数)
document.documentElement.style.setProperty("--primary-color-light", color + "80"); // 添加透明度
+32 -31
View File
@@ -13,52 +13,52 @@ import { Storage } from "@/utils/storage";
export const useUserStore = defineStore("user", () => {
const userInfo = ref<UserInfo | undefined>(getUserInfo());
const isLoggingIn = ref(false);
// 统一的登录处理方法
const handleLogin = async (loginFn: () => Promise<LoginResult>, loginType: string) => {
if (isLoggingIn.value) return;
isLoggingIn.value = true;
try {
const result = await loginFn();
setAccessToken(result.access_token);
// 登录成功后获取用户信息
await getInfo();
return result;
} catch (error: any) {
console.error(`${loginType}登录失败`, error);
throw error;
} finally {
isLoggingIn.value = false;
}
};
// 账号密码登录
const login = async (data: LoginFormData) => {
return new Promise((resolve, reject) => {
AuthAPI.login(data)
.then((data: LoginResult) => {
setAccessToken(data.access_token);
resolve(data);
})
.catch((error) => {
console.error("登录失败", error);
reject(error);
});
});
return handleLogin(() => AuthAPI.login(data), "账号密码");
};
// 微信基础授权登录
const loginWithWxCode = async (code: string) => {
try {
const data = await AuthAPI.loginByWxMiniAppCode(code);
setAccessToken(data.access_token);
return data;
} catch (error: any) {
console.error("微信授权登录失败", error);
throw error;
}
return handleLogin(() => AuthAPI.loginByWxMiniAppCode(code), "微信授权");
};
// 微信手机号授权登录
const loginWithWxPhone = async (data: WxLoginData): Promise<any> => {
try {
const result = await AuthAPI.loginByWxMiniAppPhone(data);
setAccessToken(result.access_token);
return result;
} catch (error: any) {
console.error("微信手机号登录失败", error);
throw error;
}
const loginWithWxPhone = async (data: WxLoginData) => {
return handleLogin(() => AuthAPI.loginByWxMiniAppPhone(data), "微信手机号");
};
// 获取用户信息
const getInfo = async () => {
try {
const userInfo = await UserAPI.getCurrentUserInfo();
setUserInfo(userInfo);
return userInfo;
const userInfoData = await UserAPI.getCurrentUserInfo();
setUserInfo(userInfoData);
// 确保响应式数据更新
userInfo.value = userInfoData;
return userInfoData;
} catch (error) {
console.error("获取用户信息失败", error);
return null;
@@ -93,6 +93,7 @@ export const useUserStore = defineStore("user", () => {
return {
userInfo,
isLoggingIn,
login,
loginWithWxCode,
loginWithWxPhone,
+2 -1
View File
@@ -28,6 +28,7 @@ declare global {
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope']
const extendedColorOptions: typeof import('../composables/useTheme')['extendedColorOptions']
const file: typeof import('../api/file')['default']
const getAccessToken: typeof import('../utils/auth')['getAccessToken']
const getActivePinia: typeof import('pinia')['getActivePinia']
@@ -216,6 +217,7 @@ 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']>
@@ -277,7 +279,6 @@ declare module 'vue' {
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly publicRequest: UnwrapRef<typeof import('../utils/request')['publicRequest']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
-17
View File
@@ -2,23 +2,6 @@ import { useUserStore } from "@/store/modules/user.store";
import { Storage } from "./storage";
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "@/constants";
/**
* 认证工具函数
*
* 使用示例:
*
* 1. 检查登录状态并自动跳转:
* if (!checkLogin()) return; // 未登录会自动跳转到登录页
*
* 2. 静默检查登录状态:
* if (!isLoggedIn()) {
* // 处理未登录逻辑,不会自动跳转
* }
*
* 3. 强制要求登录:
* requireLogin(); // 清除无效状态并跳转到登录页
*/
/**
* 获取访问令牌
* @returns 返回访问令牌,如果不存在则返回null
+12 -16
View File
@@ -26,9 +26,14 @@ function request<T = any>(options: RequestOptions): Promise<T> {
header["Authorization"] = `Bearer ${accessToken}`;
} else {
// 需要认证但没有令牌,跳转到登录页
uni.navigateTo({
url: "/pages/login/index",
});
// 防止循环跳转:检查当前页面是否已经是登录页
const currentPages = getCurrentPages();
const currentPage = currentPages[currentPages.length - 1];
if (!currentPage || !currentPage.route || !currentPage.route.includes("login")) {
uni.navigateTo({
url: "/pages/login/index",
});
}
return reject(new Error("请先登录"));
}
}
@@ -65,8 +70,10 @@ function request<T = any>(options: RequestOptions): Promise<T> {
}
// 未授权错误
else if (res.statusCode === 401) {
// 如果需要认证且未授权,跳转到登录页
if (!options.skipAuth) {
// 防止循环跳转:检查当前页面是否已经是登录页
const currentPages = getCurrentPages();
const currentPage = currentPages[currentPages.length - 1];
if (!currentPage || !currentPage.route || !currentPage.route.includes("login")) {
uni.navigateTo({
url: "/pages/login/index",
});
@@ -86,15 +93,4 @@ function request<T = any>(options: RequestOptions): Promise<T> {
});
}
/**
* 无需认证的请求
* @param options 请求配置
*/
export function publicRequest<T = any>(options: RequestOptions): Promise<T> {
return request<T>({
...options,
skipAuth: true,
});
}
export default request;