refactor: ♻️ 我的页面重构

This commit is contained in:
ray
2024-10-15 08:31:03 +08:00
parent 3dec53af38
commit c21b15d793
22 changed files with 6888 additions and 5319 deletions

View File

@@ -1,7 +1,7 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
# 项目运行的端口号
VITE_APP_PORT = 5173
VITE_APP_PORT = 4396
# API 基础路径,开发环境下的请求前缀
VITE_APP_BASE_API = '/dev-api'

View File

@@ -1,42 +1,23 @@
import globals from "globals"; // 全局变量配置
import pluginJs from "@eslint/js"; // JavaScript 的推荐配置
import tseslint from "typescript-eslint"; // TypeScript 配置
import pluginVue from "eslint-plugin-vue"; // Vue 配置
import globals from "globals";
import js from "@eslint/js";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";
// ESLint 核心插件
import pluginVue from "eslint-plugin-vue";
import pluginTypeScript from "@typescript-eslint/eslint-plugin";
// 动态读取 .eslintrc-auto-import.json 文件内容
const autoImportConfig = JSON.parse(
readFileSync(
resolve(dirname(fileURLToPath(import.meta.url)), ".eslintrc-auto-import.json"),
"utf-8",
),
);
// Prettier 插件及配置
import configPrettier from "eslint-config-prettier";
import pluginPrettier from "eslint-plugin-prettier";
// 解析器
import * as parserVue from "vue-eslint-parser";
import * as parserTypeScript from "@typescript-eslint/parser";
// 定义 ESLint 配置
export default [
{ files: ["**/*.{js,mjs,cjs,ts,vue}"] }, // 校验的文件类型
{
// 语言选项配置,定义全局变量
languageOptions: {
globals: {
...globals.browser, // 浏览器变量 (window, document 等)
...globals.node, // Node.js 变量 (process, require 等)
...autoImportConfig.globals, // 自动导入的全局变量
...{
uni: "readonly", // uni-app 全局对象
},
},
},
},
pluginJs.configs.recommended, // JavaScript 推荐配置
...tseslint.configs.recommended, // TypeScript 推荐配置
...pluginVue.configs["flat/essential"], // Vue 推荐配置
{ files: ["**/*.vue"], languageOptions: { parserOptions: { parser: tseslint.parser } } }, // 对 .vue 文件使用 TypeScript 解析器
// 添加忽略的文件或目录
// 通用 JavaScript 配置
{
...js.configs.recommended,
ignores: [
"/dist",
"/public",
@@ -46,28 +27,123 @@ export default [
"**/*.tsbuildinfo",
"/src/manifest.json",
],
languageOptions: {
globals: {
...globals.browser, // 浏览器变量 (window, document 等)
...globals.node, // Node.js 变量 (process, require 等)
},
},
plugins: {
prettier: pluginPrettier,
},
rules: {
...configPrettier.rules,
...pluginPrettier.configs.recommended.rules,
"no-debug": "off", // 禁止 debugger
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
], // 允许未使用的变量,以 _ 开头的变量不检查
"prettier/prettier": [
"error",
{
endOfLine: "auto", // 自动识别换行符
},
],
},
},
// 自定义规则
// TypeScript 配置
{
files: ["**/*.?([cm])ts"],
languageOptions: {
parser: parserTypeScript,
parserOptions: {
sourceType: "module",
},
},
plugins: {
"@typescript-eslint": pluginTypeScript,
},
rules: {
quotes: ["error", "double"], // 强制使用双引号
"quote-props": ["error", "always"], // 强制对象的属性名使用引号
semi: ["error", "always"], // 要求使用分号
indent: ["error", 2], // 使用两个空格进行缩进
"no-multiple-empty-lines": ["error", { max: 1 }], // 不允许多个空行
"no-trailing-spaces": "error", // 不允许行尾有空格
...pluginTypeScript.configs.strict.rules,
"@typescript-eslint/ban-types": "off", // 禁止特定类型
"@typescript-eslint/no-redeclare": "error", // 禁止重复声明
"@typescript-eslint/ban-ts-comment": "off", // 禁止特定注释
"@typescript-eslint/no-explicit-any": "off", // 禁止使用 any
"@typescript-eslint/prefer-as-const": "warn", // 使用 as const 替代 as 'const'
"@typescript-eslint/no-empty-function": "off", // 禁止空函数
"@typescript-eslint/no-non-null-assertion": "off", // 禁止非空断言
"@typescript-eslint/no-import-type-side-effects": "error", // 禁止导入类型产生副作用
"@typescript-eslint/explicit-module-boundary-types": "off", // 显式函数返回类型
"@typescript-eslint/consistent-type-imports": [
"error",
{ disallowTypeAnnotations: false, fixStyle: "inline-type-imports" },
], // 一致的类型导入
"@typescript-eslint/prefer-literal-enum-member": ["error", { allowBitwiseExpressions: true }], // 使用字面量枚举成员
"@typescript-eslint/no-empty-object-type": "off", // 禁止空对象类型
},
},
// TypeScript 规则
"@typescript-eslint/no-explicit-any": "off", // 禁用 no-explicit-any 规则,允许使用 any 类型
"@typescript-eslint/explicit-function-return-type": "off", // 不强制要求函数必须明确返回类型
"@typescript-eslint/no-empty-interface": "off", // 禁用 no-empty-interface 规则,允许空接口声明
"@typescript-eslint/no-empty-object-type": "off", // 允许空对象类型
// TypeScript 声明文件的特殊配置
{
files: ["**/*.d.ts"],
rules: {
"eslint-comments/no-unlimited-disable": "off",
"unused-imports/no-unused-vars": "off",
},
},
// Vue 规则
"vue/multi-word-component-names": "off", // 关闭多单词组件名称的限制
"vue/html-indent": ["error", 2], // Vue 模板中的 HTML 缩进使用两个空格
"vue/no-v-html": "off", // 允许使用 v-html (根据实际项目需要)
// JavaScript 配置(包含 commonjs
{
files: ["**/*.?([cm])js"],
rules: {
"@typescript-eslint/no-require-imports": "off", // 禁止 require
"@typescript-eslint/no-var-requires": "off", // 禁止 require
},
},
// Vue 文件配置
{
files: ["**/*.vue"],
languageOptions: {
parser: parserVue,
parserOptions: {
extraFileExtensions: [".vue"],
parser: "@typescript-eslint/parser",
sourceType: "module",
},
},
plugins: {
vue: pluginVue,
},
processor: pluginVue.processors[".vue"],
rules: {
...pluginVue.configs.base.rules, // Vue 基础配置
...pluginVue.configs["vue3-essential"].rules, // Vue3 基础配置
...pluginVue.configs["vue3-recommended"].rules, // Vue3 推荐配置
"no-undef": "off",
"no-unused-vars": "off",
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
"vue/require-explicit-emits": "off",
"vue/multi-word-component-names": "off",
"vue/no-setup-props-reactivity-loss": "off",
"vue/html-self-closing": [
"error",
{
html: {
void: "always",
normal: "always",
component: "always",
},
svg: "always",
math: "always",
},
], // 自闭合标签
},
},
];

View File

@@ -131,6 +131,8 @@
"unocss-preset-weapp": "^0.62.2",
"unplugin-auto-import": "^0.18.3",
"vite": "5.2.8",
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "^1.0.24"
}
},
"packageManager": "pnpm@9.2.0+sha512.98a80fd11c2e7096747762304106432b3ddc67dcf54b5a8c01c93f68a2cd5e05e6821849522a06fb76284d41a2660d5e334f2ee3bbf29183bf2e739b1dafa771"
}

11618
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import request from "@/utils/request";
class AuthAPI {
const AuthAPI = {
/**
* 登录接口
*
@@ -8,37 +8,40 @@ class AuthAPI {
* @param password 密码
* @returns 返回 token
*/
static login(username: string, password: string): Promise<LoginResult> {
login(data: LoginFormData): Promise<LoginResult> {
console.log("data", data);
return request<LoginResult>({
url: "/api/v1/auth/login",
method: "POST",
data: {
username,
password,
},
data: data,
header: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
}
},
/**
* 登出接口
*/
static logout(): Promise<void> {
return request<void>({
logout(): Promise<void> {
return request({
url: "/api/v1/auth/logout",
method: "DELETE",
});
}
}
},
};
export default AuthAPI;
/** 登录响应 */
export interface LoginResult {
/** 访问token */
accessToken?: string;
accessToken: string;
/** token 类型 */
tokenType?: string;
}
export interface LoginFormData {
username: string;
password: string;
}

View File

@@ -2,19 +2,19 @@ import request from "@/utils/request";
const USER_BASE_URL = "/api/v1/users";
class UserAPI {
const UserAPI = {
/**
* 获取当前登录用户信息
*
* @returns 登录用户昵称、头像信息,包括角色和权限
*/
static getUserInfo(): Promise<UserInfo> {
getUserInfo(): Promise<UserInfo> {
return request<UserInfo>({
url: `${USER_BASE_URL}/me`,
method: "GET",
});
}
}
},
};
export default UserAPI;
/** 登录用户信息 */

2
src/env.d.ts vendored
View File

@@ -1,7 +1,7 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import { DefineComponent } from "vue";
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -14,13 +14,13 @@
}
},
{
"path": "pages/workbench/index",
"path": "pages/work/index",
"style": {
"navigationBarTitleText": "工作台"
}
},
{
"path": "pages/profile/index",
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
@@ -50,16 +50,16 @@
"selectedIconPath": "static/tabbar/home-active.png"
},
{
"pagePath": "pages/workbench/index",
"pagePath": "pages/work/index",
"text": "工作台",
"iconPath": "static/tabbar/workbench.png",
"selectedIconPath": "static/tabbar/workbench-active.png"
"iconPath": "static/tabbar/work.png",
"selectedIconPath": "static/tabbar/work-active.png"
},
{
"pagePath": "pages/profile/index",
"pagePath": "pages/mine/index",
"text": "我的",
"iconPath": "static/tabbar/profile.png",
"selectedIconPath": "static/tabbar/profile-active.png"
"iconPath": "static/tabbar/mine.png",
"selectedIconPath": "static/tabbar/mine-active.png"
}
]
}

View File

@@ -1,40 +1,78 @@
<template>
<view class="flex-col items-center">
<input v-model="username" placeholder="请输入用户名" />
<input v-model="password" placeholder="请输入密码" type="password" />
<button class="mt-5" @click="handleLogin">登录</button>
<view class="login">
<image class="logo" src="@/static/logo.png" style="width: 100px; height: 100px" />
<view class="mt-10 flex-center">
<wd-form ref="loginFormRef" :model="loginFormData">
<wd-cell-group border>
<wd-input
v-model="loginFormData.username"
label="用户名"
label-width="100px"
prop="username"
clearable
placeholder="请输入用户名"
:rules="[{ required: true, message: '请填写用户名' }]"
/>
<wd-input
v-model="loginFormData.password"
label="密码"
label-width="100px"
prop="password"
show-password
clearable
placeholder="请输入密码"
:rules="[{ required: true, message: '请填写密码' }]"
/>
</wd-cell-group>
<view class="footer">
<wd-button size="large" type="primary" block @click="handleLogin">提交</wd-button>
</view>
</wd-form>
</view>
</view>
</template>
<script lang="ts" setup>
import { type LoginFormData } from "@/api/auth";
import { useUserStore } from "@/store/modules/user";
// 登录表单
const username = ref("admin");
const password = ref("123456");
const loginFormRef = ref();
const loginFormData = ref<LoginFormData>({
username: "admin",
password: "123456",
});
// 使用 pinia
const userStore = useUserStore();
// 登录处理
const handleLogin = async () => {
await userStore.login(username.value, password.value);
if (userStore.token) {
await userStore.getUserInfo(); // 登录成功后获取用户信息
uni.showToast({ title: "登录成功", icon: "success" });
uni.navigateBack(); // 登录成功后返回上一页
} else {
uni.showToast({ title: "登录失败", icon: "none" });
}
const handleLogin = () => {
loginFormRef.value
.validate()
.then(async ({ valid }: { valid: boolean }) => {
if (valid) {
try {
userStore.login(loginFormData.value).then(() => {
uni.showToast({ title: "登录成功", icon: "success" });
uni.navigateBack();
});
} catch (error: any) {
console.log("登录失败", error.message);
}
}
})
.error(({ errors }: { errors: any }) => {
console.log("errors", errors);
});
};
</script>
<style scoped>
input {
width: 80%;
padding: 10px;
margin-top: 16px;
border: 1px solid #ccc;
<style lang="scss" scoped>
.login {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100vh - 50px);
}
</style>

103
src/pages/mine/index.vue Normal file
View File

@@ -0,0 +1,103 @@
<template>
<view class="mine">
<!-- 个人资料 -->
<view class="header">
<view class="flex items-center">
<image
class="w-100rpx h-100rpx rounded-full"
:src="isLogin ? userInfo!.avatar : defaultAvatar"
/>
<text class="ml-20rpx text-32rpx">{{
isLogin ? userInfo!.nickname : "您还未登录,请先登录"
}}</text>
</view>
<view>
<text v-if="isLogin" @click="goToLoginPage"> 个人信息 <wd-icon name="arrow-right" /> </text>
<text v-else @click="goToLoginPage"> 去登录 <wd-icon name="arrow-right" /> </text>
</view>
</view>
<view class="relative -top-[50px] p-[40rpx]">
<!-- 功能操作 -->
<wd-grid clickable class="rounded-[10rpx] bg-white">
<wd-grid-item :to="{ path: '/group' }" icon="setting" text="交流群" />
<wd-grid-item :to="{ path: '/customer-service' }" icon="setting" text="在线客服" />
<wd-grid-item :to="{ path: '/feedback' }" icon="setting" text="反馈社区" />
<wd-grid-item :to="{ path: '/like-us' }" icon="setting" text="点赞我们" />
</wd-grid>
<!-- 菜单列表 -->
<view class="mt50rpx">
<wd-cell-group border>
<wd-cell title="编辑资料" icon="setting" @click="goToEditProfile">
<wd-icon name="arrow-right" />
</wd-cell>
<wd-cell title="常见问题" icon="setting" @click="goToFAQ">
<wd-icon name="arrow-right" />
</wd-cell>
<wd-cell title="关于我们" icon="setting" @click="goToAbout">
<wd-icon name="arrow-right" />
</wd-cell>
<wd-cell title="应用设置" icon="setting" @click="goToSettings">
<wd-icon name="arrow-right" />
</wd-cell>
<wd-cell title="退出登录" icon="setting" clickable @click="handleLogout">
<wd-icon name="arrow-right" />
</wd-cell>
</wd-cell-group>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { useUserStore } from "@/store/modules/user";
const userStore = useUserStore();
const userInfo = computed(() => userStore.userInfo);
const isLogin = computed(() => !!userInfo.value);
const defaultAvatar = "/static/images/default-avatar.png";
const goToLoginPage = () => {
uni.navigateTo({ url: "/pages/login/index" });
};
const goToProfile = () => {
uni.navigateTo({ url: "/pages/profile/index" });
};
const goToEditProfile = () => {
uni.navigateTo({ url: "/pages/edit-profile/index" });
};
const goToFAQ = () => {
uni.navigateTo({ url: "/pages/faq/index" });
};
const goToAbout = () => {
uni.navigateTo({ url: "/pages/about/index" });
};
const goToSettings = () => {
uni.navigateTo({ url: "/pages/settings/index" });
};
const handleLogout = () => {
userStore.logout().then(() => {
uni.showToast({ title: "已退出登录", icon: "success" });
});
};
</script>
<style lang="scss" scoped>
.mine {
height: 100%;
background: #f5f2f2;
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 50rpx 20rpx 100rpx 20rpx;
color: #fff;
background: linear-gradient(60deg, #165dff, #6aa1ff);
}
}
</style>

View File

@@ -1,39 +0,0 @@
<template>
<view class="flex-center flex-col">
<text class="text-blue font-bold text-lg">我的</text>
<!-- 判断是否已登录 -->
<template v-if="isLoggedIn">
<image :src="userInfo?.avatar" class="w100 h100 mb-5 rounded-full" />
<text class="text-lg font-bold">{{ userInfo?.nickname }}</text>
<button @click="handleLogout" class="mt-5">退出登录</button>
</template>
<!-- 未登录时显示去登录按钮 -->
<template v-else>
<text>您还未登录请先登录</text>
<button @click="goToLoginPage" class="mt-5">去登录</button>
</template>
</view>
</template>
<script lang="ts" setup>
import { useUserStore } from "@/store/modules/user";
// 使用 pinia
const userStore = useUserStore();
const isLoggedIn = computed(() => userStore.token);
const userInfo = computed(() => userStore.userInfo);
// 跳转到登录页面
const goToLoginPage = () => {
uni.navigateTo({ url: "/pages/login/index" });
};
// 退出登录处理
const handleLogout = async () => {
await userStore.logout();
uni.showToast({ title: "已退出登录", icon: "success" });
};
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,48 +1,48 @@
import { defineStore } from "pinia";
import AuthAPI from "@/api/auth";
import UserAPI, { UserInfo } from "@/api/user";
import { setToken, clearToken } from "@/utils/auth";
import AuthAPI, { type LoginFormData } from "@/api/auth";
import UserAPI, { type UserInfo } from "@/api/user";
import { setToken, getUserInfo, setUserInfo, clearAll } from "@/utils/cache";
export const useUserStore = defineStore("user", () => {
// 确保 token 是响应式的
const token = ref<string>(uni.getStorageSync("token") || "");
const userInfo = ref<UserInfo | null>(null);
const userInfo = ref<UserInfo | null>(getUserInfo());
// 登录
const login = (username: string, password: string) => {
AuthAPI.login(username, password)
.then((res) => {
token.value = ` ${res.accessToken}`; // Bearer token
setToken(token.value);
})
.catch((error) => {
if (error.action === "TOKEN_INVALID") {
// 处理 Token 失效,比如弹出登录提示框,而不是直接跳转
console.log("Token 已失效,请重新登录");
// 例如uni.navigateTo({ url: '/pages/login/index' });
} else {
// 处理其他错误
console.error("请求失败", error.message);
}
});
const login = (data: LoginFormData) => {
return new Promise((resolve, reject) => {
AuthAPI.login(data)
.then((data) => {
setToken(data.accessToken);
getInfo();
resolve(data);
})
.catch((error) => {
console.error("登录失败", error);
reject(error); // 将错误抛出
});
});
};
// 获取用户信息
const getUserInfo = async () => {
const info = await UserAPI.getUserInfo();
userInfo.value = info;
const getInfo = () => {
UserAPI.getUserInfo().then((data) => {
setUserInfo(data);
userInfo.value = data;
});
};
// 登出
const logout = async () => {
await AuthAPI.logout();
userInfo.value = null;
token.value = ""; // 清空 token
clearToken(); // 从本地缓存移除 token
try {
await AuthAPI.logout(); // 调用后台注销接口
} catch (error) {
console.error("登出失败", error);
} finally {
clearAll(); // 清除本地的 token 和用户信息缓存
userInfo.value = null; // 清空用户信息
}
};
return {
token,
userInfo,
login,
logout,

View File

@@ -1,19 +0,0 @@
const TOKEN_KEY = "app-token";
function getToken(): string {
return uni.getStorageSync(TOKEN_KEY) || "";
}
function setToken(token: string) {
return uni.setStorageSync(TOKEN_KEY, token);
}
function clearToken() {
return uni.removeStorageSync(TOKEN_KEY);
}
function isLogin(): boolean {
return !!getToken();
}
export { getToken, setToken, clearToken, isLogin };

38
src/utils/cache.ts Normal file
View File

@@ -0,0 +1,38 @@
const TOKEN_KEY = "app-token";
const USER_INFO_KEY = "user-info";
// 设置 token
export function setToken(token: string) {
uni.setStorageSync(TOKEN_KEY, token);
}
// 获取 token
export function getToken(): string {
return uni.getStorageSync(TOKEN_KEY) || "";
}
// 清除 token
export function clearToken() {
uni.removeStorageSync(TOKEN_KEY);
}
// 设置用户信息
export function setUserInfo(userInfo: any) {
uni.setStorageSync(USER_INFO_KEY, userInfo);
}
// 获取用户信息
export function getUserInfo(): any {
return uni.getStorageSync(USER_INFO_KEY) || null;
}
// 清除用户信息
export function clearUserInfo() {
uni.removeStorageSync(USER_INFO_KEY);
}
// 清除所有缓存信息
export function clearAll() {
clearToken();
clearUserInfo();
}

View File

@@ -1,4 +1,4 @@
import { getToken, clearToken } from "@/utils/auth";
import { getToken } from "@/utils/cache";
import { ResultCodeEnum } from "@/enums/ResultCodeEnum";
export default function request<T>(options: UniApp.RequestOptions): Promise<T> {
@@ -14,7 +14,7 @@ export default function request<T>(options: UniApp.RequestOptions): Promise<T> {
url: `${baseApi}${options.url}`,
header: {
...options.header,
Authorization: getToken() ? `Bearer ${getToken()}` : "", // 添加 Bearer 前缀
Authorization: getToken() ? `Bearer ${getToken()}` : "",
},
success: (response) => {
console.log("success response", response);
@@ -32,8 +32,6 @@ export default function request<T>(options: UniApp.RequestOptions): Promise<T> {
duration: 2000,
});
clearToken();
// 此处不强制跳转到登录页,可以让调用方决定下一步操作
reject({
message: resData.msg || "令牌无效或过期",
@@ -46,7 +44,6 @@ export default function request<T>(options: UniApp.RequestOptions): Promise<T> {
uni.showToast({
title: resData.msg || "业务处理失败",
icon: "none",
duration: 2000,
});
reject({
message: resData.msg || "业务处理失败",

View File

@@ -1,4 +1,4 @@
import { defineConfig, UserConfig, ConfigEnv, loadEnv } from "vite";
import { defineConfig, type UserConfig, type ConfigEnv, loadEnv } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
import AutoImport from "unplugin-auto-import/vite";