mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-23 05:10:57 +00:00
feat: 优化主题管理和登录流程
refactor(theme): 重构主题管理逻辑,增加自定义颜色选项和持久化存储 refactor(login): 优化登录流程,增加防重复提交和错误处理 fix(request): 修复请求循环跳转问题,移除publicRequest方法 style: 更新页面样式和布局,改进用户体验 docs: 移除无用注释,更新代码文档
This commit is contained in:
@@ -5,7 +5,6 @@ import { useTheme } from "@/composables/useTheme";
|
||||
const { initTheme } = useTheme();
|
||||
|
||||
onLaunch(() => {
|
||||
console.log("App Launch");
|
||||
// 初始化主题
|
||||
initTheme();
|
||||
});
|
||||
|
||||
+10
-7
@@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ const FileAPI = {
|
||||
/**
|
||||
* 文件上传地址
|
||||
*/
|
||||
uploadUrl: baseApi + "/api/v1/files/upload",
|
||||
uploadUrl: baseApi + "/files/upload",
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"); // 添加透明度
|
||||
|
||||
@@ -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,
|
||||
|
||||
Vendored
+2
-1
@@ -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']>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user