feat: 更新应用名称和页面路由结构

refactor: 重构用户信息管理和登录流程

fix: 修复头像上传和密码修改功能

style: 优化登录页面和主题设置界面

docs: 更新主题系统文档说明

chore: 移除无用配置文件和更新依赖项
This commit is contained in:
zhangtao
2025-08-13 01:35:24 +08:00
parent e013c0aad2
commit 82840e410f
30 changed files with 421 additions and 775 deletions
+2 -2
View File
@@ -18,7 +18,7 @@ const FileAPI = {
*
* @param filePath
*/
upload(filePath: string): Promise<UploadFilePath> {
upload(filePath: string): Promise<UploadFileResult> {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: this.uploadUrl,
@@ -29,7 +29,7 @@ const FileAPI = {
},
formData: {},
success: (response) => {
const resData = JSON.parse(response.data) as ApiResponse<UploadFilePath>;
const resData = JSON.parse(response.data) as ApiResponse<UploadFileResult>;
// 业务状态码 00000 表示成功
if (resData.code === ApiCode.SUCCESS) {
resolve(resData.data);
+14 -50
View File
@@ -21,8 +21,8 @@ const UserAPI = {
* @param body
* @returns 上传后的文件路径
*/
uploadCurrentUserAvatar(body: any): Promise<UploadFilePath> {
return request<UploadFilePath>({
uploadCurrentUserAvatar(body: any): Promise<UploadFileResult> {
return request<UploadFileResult>({
url: `${USER_BASE_URL}/current/avatar/upload`,
method: "POST",
data: body,
@@ -145,7 +145,7 @@ const UserAPI = {
*/
deleteUser(ids: number[]): Promise<ApiResponse> {
return request<ApiResponse>({
url: `/system/user/delete`,
url: `${USER_BASE_URL}/delete`,
method: "DELETE",
data: ids,
});
@@ -213,7 +213,7 @@ export interface UserInfo {
avatar?: string;
email?: string;
mobile?: string;
gender?: string;
gender?: number;
password?: string;
menus?: MenuTable[];
dept?: deptTreeType;
@@ -289,16 +289,16 @@ export interface positionSelectorType {
/* 个人中心用户信息表单 */
export interface UserProfileForm {
id?: number;
name: string;
gender: number;
mobile: string;
email: string;
username: string;
dept_name: string;
positions: positionSelectorType[];
roles: roleSelectorType[];
avatar: string;
created_at: string;
name?: string;
gender?: number;
mobile?: string;
email?: string;
username?: string;
dept_name?: string;
positions?: positionSelectorType[];
roles?: roleSelectorType[];
avatar?: string;
created_at?: string;
}
/* 修改密码表单 */
@@ -313,39 +313,3 @@ export interface ResetPasswordForm {
id: number;
password: string;
}
/* 修改手机表单 */
export interface MobileBindingForm {
/** 手机号 */
mobile?: string;
/** 验证码 */
code?: string;
}
/* 修改邮箱表单 */
export interface EmailBindingForm {
/** 邮箱 */
email?: string;
/** 验证码 */
code?: string;
}
/* 微信手机号授权数据 */
export interface WechatPhoneData {
/** 微信授权码 */
code: string;
/** 加密数据 */
encryptedData?: string;
/** 初始向量 */
iv?: string;
}
/* 手机号获取结果 */
export interface PhoneNumberResult {
/** 手机号 */
phoneNumber: string;
/** 纯手机号(去除+86 */
purePhoneNumber?: string;
/** 国家代码 */
countryCode?: string;
}
+2 -2
View File
@@ -7,8 +7,8 @@ export interface TabbarItem {
}
const tabbarItems = ref<TabbarItem[]>([
{ name: "home", value: null, active: true, title: "首页", icon: "home" },
{ name: "work", value: null, active: true, title: "工作台", icon: "work" },
{ name: "index", value: null, active: true, title: "首页", icon: "home" },
{ name: "work", value: null, active: false, title: "工作台", icon: "app" },
{ name: "mine", value: null, active: false, title: "我的", icon: "user" },
]);
+19 -1
View File
@@ -25,6 +25,24 @@ 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",
@@ -114,7 +132,6 @@ export function useTheme() {
theme.value = systemTheme;
if (!hasUserSet.value) {
followSystem.value = true;
console.log("首次启动,使用系统主题:", theme.value);
} else {
console.log("跟随系统主题:", theme.value);
}
@@ -162,6 +179,7 @@ export function useTheme() {
return {
theme: computed(() => theme.value),
isDark,
colorColumns,
followSystem: computed(() => followSystem.value),
hasUserSet: computed(() => hasUserSet.value),
currentThemeColor: computed(() => currentThemeColor.value),
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "vue-uniapp-template",
"name": "FastApp",
"appid": "",
"description": "FastApp",
"versionName": "2.0.0",
+5 -8
View File
@@ -1,15 +1,9 @@
{
"easycom": {
"autoscan": true,
"custom": {
"^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue"
}
},
"pages": [
{
"path": "pages/index/index",
"type": "home",
"name": "home",
"name": "index",
"style": {
"navigationStyle": "custom"
},
@@ -31,7 +25,7 @@
{
"path": "pages/work/index",
"type": "page",
"name": "work",
"name": "index",
"style": {
"navigationBarTitleText": "工作台"
},
@@ -106,6 +100,9 @@
{
"pagePath": "pages/index/index"
},
{
"pagePath": "pages/work/index"
},
{
"pagePath": "pages/mine/index"
}
+5 -5
View File
@@ -25,7 +25,7 @@
<!-- 通知公告 -->
<wd-notice-bar
text="vue-uniapp-template 是一个基于 Vue3 + UniApp 的前端模板项目,提供了一套完整的前端解决方案,包括登录、权限、字典、接口请求、状态管理、页面布局、组件封装等功能。"
text="fastapp 是一个基于 Vue3 + UniApp 的前端模板项目,提供了一套完整的前端解决方案,包括登录、权限、字典、接口请求、状态管理、页面布局、组件封装等功能。"
color="#34D19D"
type="info"
>
@@ -80,7 +80,7 @@
</view>
</template>
<view class="w-full h-300px mb-40rpx">
<view class="w-full h-240px mb-40rpx">
<qiun-data-charts type="area" :chartData="chartData" :opts="chartOpts" />
</view>
</wd-card>
@@ -89,6 +89,7 @@
<script setup lang="ts">
import { dayjs } from "wot-design-uni";
import { useRouter } from "vue-router";
// 定义访问统计数据类型
interface VisitStatsVO {
@@ -140,7 +141,7 @@ const chartOpts = ref({
// 日期范围
const recentDaysRange = ref(7);
const swiperList = ref(["https://www.youlai.tech/storage/blog/banner9.png"]);
const swiperList = ref(["/static/images/banner.png"]);
// 快捷导航列表
const navList = reactive([
@@ -248,7 +249,6 @@ const loadVisitTrendData = () => {
// 数据范围变化
const handleDataRangeChange = ({ value }: { value: number }) => {
console.log("handleDataRangeChange", value);
recentDaysRange.value = value;
loadVisitTrendData();
};
@@ -273,7 +273,7 @@ onShow(() => {
<route lang="json">
{
"name": "home",
"name": "index",
"style": { "navigationStyle": "custom" },
"layout": "tabbar"
}
+88 -75
View File
@@ -4,7 +4,11 @@
<image src="/static/images/login-bg.svg" mode="aspectFill" class="login-bg" />
<!-- Logo和标题区域 -->
<view class="header"></view>
<view class="header">
<image src="/static/logo.png" class="logo" mode="aspectFit" />
<text class="title">FastApp管理系统</text>
<text class="subtitle">欢迎使用移动端管理平台</text>
</view>
<view class="login-card">
<view class="form-wrap">
@@ -23,6 +27,7 @@
class="form-input input-transparent"
placeholder="请输入用户名"
placeholder-class="input-placeholder"
@confirm="handleAccountLogin"
/>
</view>
<view class="divider"></view>
@@ -38,9 +43,11 @@
<input
v-model="loginFormData.password"
class="form-input input-transparent"
:type="showPassword ? 'text' : 'password'"
type="text"
:password="showPassword ? false : true"
placeholder="请输入密码"
placeholder-class="input-placeholder"
@confirm="handleAccountLogin"
/>
<wd-icon
:name="showPassword ? 'eye-open' : 'eye-close'"
@@ -64,9 +71,10 @@
type="text"
placeholder="请输入验证码"
placeholder-class="input-placeholder"
@confirm="handleAccountLogin"
/>
<view class="captcha-img">
<image :src="captchaImg" class="captcha-img" />
<view class="captcha-img" @click="getLoginCaptcha">
<image :src="captchaImg" class="captcha-img" mode="aspectFit" />
</view>
</view>
<view class="divider"></view>
@@ -75,9 +83,10 @@
<button
class="login-btn"
:disabled="loading"
:style="loading ? 'opacity: 0.7;' : ''"
:style="{ opacity: loading ? 0.7 : 1 }"
@click="handleAccountLogin"
>
<wd-loading v-if="loading" size="20" color="#fff" />
{{ loading ? "登录中..." : "账号登录" }}
</button>
@@ -145,7 +154,8 @@ 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, onMounted } from "vue";
import { debounce } from "@/utils";
import { computed, onMounted, ref } from "vue";
import AuthAPI, { type LoginFormData } from "@/api/auth";
const loginFormRef = ref();
@@ -166,9 +176,9 @@ const isDarkMode = computed(() => theme.value === "dark");
const loginFormData = ref<LoginFormData>({
username: "admin",
password: "123456",
captcha_key: "",
captcha: "",
remember: true,
captcha_key: "",
remember: true, // 添加 remember 属性,默认值设为 false
});
// 获取重定向参数
@@ -183,10 +193,15 @@ onLoad((options) => {
const getLoginCaptcha = async () => {
try {
const result = await AuthAPI.getCaptcha();
captchaImg.value = result.captcha_key;
toast.success("验证码发送成功");
captchaImg.value = result.img_base;
loginFormData.value.captcha_key = result.key;
// 只有在需要验证码时才清空输入框
if (result.enable) {
loginFormData.value.captcha = "";
}
} catch (error: any) {
toast.error(error?.message || "验证码发送失败");
toast.error(error?.message || "验证码获取失败");
captchaImg.value = "";
}
};
@@ -203,12 +218,29 @@ onMounted(() => {
getLoginCaptcha();
});
// 账号密码登录处理
const handleAccountLogin = () => {
// 统一的登录成功处理
const handleLoginSuccess = async (result: any) => {
// 记住用户名
await userStore.getInfo();
toast.success("登录成功");
setTimeout(() => {
if (!userStore.isUserInfoComplete() || result.isNewUser) {
uni.navigateTo({
url: `/pages/mine/profile/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
} else {
uni.reLaunch({ url: redirect.value });
}
}, 1000);
};
// 账号密码登录处理(防抖处理)
const handleAccountLogin = debounce(() => {
if (loading.value) return;
// 表单验证
if (!loginFormData.value.username) {
if (!loginFormData.value.username.trim()) {
toast.error("请输入用户名");
return;
}
@@ -216,37 +248,28 @@ const handleAccountLogin = () => {
toast.error("请输入密码");
return;
}
if (loginFormData.value.captcha_key && !loginFormData.value.captcha) {
if (loginFormData.value.captcha_key && !loginFormData.value.captcha.trim()) {
toast.error("请输入验证码");
return;
}
loading.value = true;
userStore
.login(loginFormData.value)
.then(() => userStore.getInfo())
.then(() => {
toast.success("登录成功");
// 账号密码登录直接跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
}, 1000);
})
.then(handleLoginSuccess)
.catch((error) => {
toast.error(error?.message || "登录失败");
// 登录失败后刷新验证码
getLoginCaptcha();
})
.finally(() => {
loading.value = false;
});
};
}, 500);
// 微信一键登录(通过手机号)
const handleWechatPhoneLogin = async (e: any) => {
// 微信一键登录(通过手机号)- 防抖处理
const handleWechatPhoneLogin = debounce(async (e: any) => {
if (loading.value || authState.value.isLogining) return;
loading.value = true;
@@ -257,26 +280,8 @@ const handleWechatPhoneLogin = async (e: any) => {
// 调用登录接口
const result: any = await userStore.loginWithWxPhone(phoneData);
// 获取用户信息
await userStore.getInfo();
toast.success("登录成功");
// 检查是否为新用户或信息不完整
if (result.isNewUser || !userStore.isUserInfoComplete()) {
// 跳转到完善信息页面
setTimeout(() => {
uni.navigateTo({
url: `/pages/mine/profile/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
}, 1000);
} else {
// 跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
}, 1000);
}
// 使用统一的成功处理
await handleLoginSuccess(result);
} catch (error: any) {
if (error.message === "用户拒绝授权") {
toast.error("您已拒绝授权获取手机号");
@@ -287,10 +292,10 @@ const handleWechatPhoneLogin = async (e: any) => {
} finally {
loading.value = false;
}
};
}, 500);
// 微信授权登录处理
const handleWechatLogin = async () => {
// 微信授权登录处理 - 防抖处理
const handleWechatLogin = debounce(async () => {
if (loading.value) return;
loading.value = true;
@@ -301,27 +306,8 @@ const handleWechatLogin = async () => {
// 尝试使用微信授权登录接口
const result: any = await userStore.loginWithWxCode(code);
// 获取用户信息
await userStore.getInfo();
toast.success("登录成功");
// 检查用户信息是否完整
if (result.isNewUser || !userStore.isUserInfoComplete()) {
// 如果信息不完整,跳转到完善信息页面
setTimeout(() => {
uni.navigateTo({
url: `/pages/mine/profile/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
}, 1000);
} else {
// 否则直接跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
}, 1000);
}
// 使用统一的成功处理
await handleLoginSuccess(result);
// #endif
// #ifndef MP-WEIXIN
@@ -332,7 +318,7 @@ const handleWechatLogin = async () => {
} finally {
loading.value = false;
}
};
}, 500);
// 跳转到用户协议页面
const navigateToUserAgreement = () => {
@@ -365,10 +351,35 @@ const navigateToPrivacy = () => {
position: absolute;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
}
.loading-overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
}
.loading-content {
display: flex;
flex-direction: column;
gap: 20rpx;
align-items: center;
}
.loading-text {
font-size: 28rpx;
color: #fff;
}
.header {
z-index: 2;
display: flex;
@@ -478,6 +489,7 @@ input:-webkit-autofill:active {
input.form-input,
input.input-transparent {
-webkit-appearance: none;
appearance: none;
background: none !important;
background-color: transparent !important;
border: none !important;
@@ -661,6 +673,7 @@ input.input-transparent {
@supports (-webkit-appearance: none) {
input {
-webkit-appearance: none;
appearance: none;
background: transparent !important;
background-color: transparent !important;
}
+4 -4
View File
@@ -159,15 +159,15 @@ const handleSubmit = async () => {
setTimeout(() => {
uni.navigateBack();
}, 1500);
} catch (_error) {
toast.error("提交失败,请重试");
} catch (error: any) {
toast.error(error?.message || "提交失败,请重试");
} finally {
submitting.value = false;
}
}
} catch (_error) {
} catch (error: any) {
// 表单验证失败
console.log("表单验证失败");
console.log("表单验证失败", error);
}
};
+2 -2
View File
@@ -13,11 +13,11 @@
</view>
<view class="user-details">
<block v-if="isLogin">
<view class="nickname">{{ userInfo!.nickname || "匿名用户" }}</view>
<view class="name">{{ userInfo!.name || "匿名用户" }}</view>
<view class="user-id">ID: {{ userInfo?.username || "0000000" }}</view>
</block>
<block v-else>
<view class="login-prompt">立即登录获取更多功能</view>
<view class="login-prompt">立即登录</view>
<wd-button
custom-class="btn-login"
size="small"
@@ -39,7 +39,7 @@
<view class="nickname-section">
<view class="section-title">昵称</view>
<input
v-model="profileForm.nickname"
v-model="profileForm.name"
type="nickname"
class="nickname-input"
placeholder="请输入昵称"
@@ -80,10 +80,10 @@
<text class="required">*</text>
</view>
<wd-input
v-model="profileForm.nickname"
v-model="profileForm.name"
placeholder="请输入昵称"
prop="nickname"
:rules="rules.nickname"
prop="name"
:rules="rules.name"
custom-class="nickname-input"
/>
</view>
@@ -98,28 +98,16 @@
</view>
<!-- #endif -->
<!-- 手机号授权 -->
<!-- 手机号输入 -->
<view class="form-section">
<view class="section-title">
手机号
<text class="required">*</text>
</view>
<view v-if="!profileForm.mobile" class="phone-auth">
<button
class="phone-auth-btn"
open-type="getPhoneNumber"
:disabled="phoneAuthLoading"
@getphonenumber="onGetPhoneNumber"
>
<wd-icon name="phone" size="20" color="#165DFF" />
<text class="auth-text">{{ phoneAuthLoading ? "授权中..." : "授权获取手机号" }}</text>
</button>
</view>
<view v-else class="phone-display">
<wd-icon name="phone" size="20" color="#52c41a" />
<text class="phone-number">{{ formatPhoneNumber(profileForm.mobile) }}</text>
<text class="change-phone" @click="changePhone">更换</text>
</view>
<view class="section-title">手机号</view>
<wd-input
v-model="profileForm.mobile"
placeholder="请输入手机号"
type="number"
:maxlength="11"
custom-class="mobile-input"
/>
</view>
</wd-form>
</view>
@@ -157,9 +145,10 @@
import { ref, reactive, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { useToast } from "wot-design-uni";
import type { FormRules } from "wot-design-uni/components/wd-form/types";
import { useUserStore } from "@/store/modules/user.store";
import UserAPI, { type UserProfileForm } from "@/api/user";
import FileAPI, { type FileInfo } from "@/api/file";
import UserAPI, { type UserInfo } from "@/api/user";
import FileAPI from "@/api/file";
const toast = useToast();
const userStore = useUserStore();
@@ -168,31 +157,33 @@ const userStore = useUserStore();
const redirect = ref("/pages/index/index");
// 表单数据
const profileForm = reactive<UserProfileForm & { mobile?: string }>({
nickname: "",
avatar: "",
const profileForm = reactive({
name: "",
gender: 1,
mobile: "",
email: "",
username: "",
dept_name: "",
positions: [] as UserInfo["positions"],
roles: [] as UserInfo["roles"],
avatar: "",
created_at: "",
});
// 表单验证规则
const rules = {
nickname: [
{ required: true, message: "请输入昵称" },
{ required: true, min: 2, max: 20, message: "昵称长度为2-20个字符" },
],
const rules: FormRules = {
name: [{ required: true, message: "请输入昵称" }],
};
// 状态管理
const loading = ref(false);
const phoneAuthLoading = ref(false);
const cropperVisible = ref(false);
const originalImageSrc = ref("");
const profileFormRef = ref();
// 计算属性
const canComplete = computed(() => {
return profileForm.nickname && profileForm.mobile;
return profileForm.name && profileForm.name.length >= 2;
});
// 页面加载
@@ -204,10 +195,15 @@ onLoad((options: any) => {
// 如果用户已有部分信息,预填充
const userInfo = userStore.userInfo;
if (userInfo) {
profileForm.nickname = userInfo.nickname || "";
profileForm.name = userInfo.name || "";
profileForm.avatar = userInfo.avatar || "";
// 注意:UserInfo接口可能没有gender字段,这里需要安全访问
profileForm.gender = (userInfo as any).gender || 1;
profileForm.gender = userInfo.gender || 1;
profileForm.email = userInfo.email || "";
profileForm.username = userInfo.username || "";
profileForm.dept_name = userInfo.dept_name || "";
profileForm.positions = userInfo.positions || [];
profileForm.roles = userInfo.roles || [];
profileForm.created_at = userInfo.created_at || "";
}
});
@@ -219,15 +215,14 @@ const onChooseAvatar = async (e: any) => {
// 上传头像到服务器
uni.showLoading({ title: "上传中..." });
const fileInfo: FileInfo = await FileAPI.upload(avatarUrl);
profileForm.avatar = fileInfo.url;
const fileInfo: UploadFileResult = await FileAPI.upload(avatarUrl);
profileForm.avatar = fileInfo.file_url;
uni.hideLoading();
uni.showToast({ title: "头像上传成功", icon: "success" });
} catch (error) {
} catch (error: any) {
uni.hideLoading();
console.error("头像上传失败:", error);
uni.showToast({ title: "头像上传失败", icon: "error" });
uni.showToast({ title: error?.message || "头像上传失败", icon: "error" });
}
};
@@ -272,8 +267,8 @@ const handleAvatarConfirm = async (event: any) => {
const { tempFilePath } = event;
toast.loading("上传中...");
const fileInfo: FileInfo = await FileAPI.upload(tempFilePath);
profileForm.avatar = fileInfo.url;
const fileInfo: UploadFileResult = await FileAPI.upload(tempFilePath);
profileForm.avatar = fileInfo.file_url;
toast.success("头像上传成功");
} catch (error) {
@@ -282,45 +277,6 @@ const handleAvatarConfirm = async (event: any) => {
}
};
// 获取手机号授权
const onGetPhoneNumber = async (e: any) => {
console.log("手机号授权回调:", e);
if (e.detail.errMsg === "getPhoneNumber:ok") {
phoneAuthLoading.value = true;
try {
// 调用后端接口解析手机号
const phoneData = await UserAPI.getPhoneNumber({
code: e.detail.code,
encryptedData: e.detail.encryptedData,
iv: e.detail.iv,
});
profileForm.mobile = phoneData.phoneNumber;
toast.success("手机号授权成功");
} catch (error: any) {
console.error("手机号授权失败:", error);
toast.error(error?.message || "手机号授权失败");
} finally {
phoneAuthLoading.value = false;
}
} else {
toast.error("手机号授权失败");
}
};
// 更换手机号
const changePhone = () => {
profileForm.mobile = "";
};
// 格式化手机号显示
const formatPhoneNumber = (phone: string) => {
if (!phone) return "";
return phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
};
// 完成信息填写
const handleComplete = async () => {
try {
@@ -328,27 +284,22 @@ const handleComplete = async () => {
const { valid } = await profileFormRef.value.validate();
if (!valid) return;
if (!profileForm.mobile) {
toast.error("请先授权获取手机号");
return;
}
loading.value = true;
// 更新用户信息
await UserAPI.updateProfile({
nickname: profileForm.nickname,
avatar: profileForm.avatar,
await UserAPI.updateCurrentUserInfo({
name: profileForm.name,
gender: profileForm.gender,
mobile: profileForm.mobile || "",
email: profileForm.email || "",
username: profileForm.username || "",
dept_name: profileForm.dept_name || "",
positions: profileForm.positions || [],
roles: profileForm.roles || [],
avatar: profileForm.avatar || "",
created_at: profileForm.created_at || "",
});
// 如果有手机号,绑定手机号
if (profileForm.mobile) {
await UserAPI.bindMobile({
mobile: profileForm.mobile,
});
}
// 重新获取用户信息
await userStore.getInfo();
+46 -22
View File
@@ -21,7 +21,7 @@
/>
</view>
</wd-cell>
<wd-cell title="昵称" :value="userProfile.nickname" is-link @click="handleOpenDialog()" />
<wd-cell title="昵称" :value="userProfile.name" is-link @click="handleOpenDialog()" />
<wd-cell
title="性别"
:value="userProfile.gender === 1 ? '男' : userProfile.gender === 2 ? '女' : '未知'"
@@ -29,9 +29,9 @@
@click="handleOpenDialog()"
/>
<wd-cell title="用户名" :value="userProfile.username" />
<wd-cell title="部门" :value="userProfile.deptName" />
<wd-cell title="角色" :value="userProfile.roleNames" />
<wd-cell title="创建日期" :value="userProfile.createTime" />
<wd-cell title="部门" :value="userProfile.dept_name" />
<wd-cell title="角色" :value="userProfile.roleNames?.join(', ')" />
<wd-cell title="创建日期" :value="userProfile.created_at" />
</wd-cell-group>
</wd-card>
@@ -43,12 +43,12 @@
<wd-form ref="userProfileFormRef" :model="userProfileForm" custom-class="edit-form">
<wd-cell-group border>
<wd-input
v-model="userProfileForm.nickname"
v-model="userProfileForm.name"
label="昵称"
label-width="160rpx"
placeholder="请输入昵称"
prop="nickname"
:rules="rules.nickname"
prop="name"
:rules="rules.name"
/>
<wd-cell title="性别" title-width="160rpx" center prop="gender" :rules="rules.gender">
<wd-radio-group v-model="userProfileForm.gender" shape="button" class="ef-radio-group">
@@ -65,17 +65,18 @@
</view>
</template>
<script setup lang="ts">
import UserAPI, { type UserProfileVO, UserProfileForm } from "@/api/user";
import FileAPI, { type FileInfo } from "@/api/file";
import UserAPI, { type UserInfo } from "@/api/user";
import FileAPI from "@/api/file";
import { checkLogin } from "@/utils/auth";
import type { FormRules } from "wot-design-uni/components/wd-form/types";
const originalSrc = ref<string>(""); //选取的原图路径
const avatarShow = ref<boolean>(false); //显示头像裁剪
const userProfile = ref<UserProfileVO>(); //用户信息
const userProfile = ref<UserInfo>(); //用户信息
/** 加载用户信息 */
const loadUserProfile = async () => {
userProfile.value = await UserAPI.getProfile();
userProfile.value = await UserAPI.getCurrentUserInfo();
};
// 头像选择
@@ -91,12 +92,21 @@ function avatarUpload() {
// 头像裁剪完成
function handleAvatarConfirm(event: any) {
const { tempFilePath } = event;
FileAPI.upload(tempFilePath).then((fileInfo: FileInfo) => {
const avatarForm: UserProfileForm = {
avatar: fileInfo.url,
FileAPI.upload(tempFilePath).then((fileInfo: UploadFileResult) => {
const avatarForm = {
name: userProfile.value?.name || "",
gender: userProfile.value?.gender || 1,
mobile: userProfile.value?.mobile || "",
email: userProfile.value?.email || "",
username: userProfile.value?.username || "",
dept_name: userProfile.value?.dept_name || "",
positions: userProfile.value?.positions || [],
roles: userProfile.value?.roles || [],
avatar: fileInfo.file_url,
created_at: userProfile.value?.created_at || "",
};
// 头像路径保存至后端
UserAPI.updateProfile(avatarForm).then(() => {
UserAPI.updateCurrentUserInfo(avatarForm).then(() => {
uni.showToast({ title: "头像上传成功", icon: "none" });
loadUserProfile();
});
@@ -104,16 +114,19 @@ function handleAvatarConfirm(event: any) {
}
// 本页面中所有的校验规则
const rules = reactive({
nickname: [{ required: true, message: "请填写昵称" }],
const rules: FormRules = {
name: [{ required: true, message: "请填写昵称" }],
gender: [{ required: true, message: "请选择性别" }],
});
};
const dialog = reactive({
visible: false,
});
const userProfileForm = reactive<UserProfileForm>({});
const userProfileForm = reactive<{
name?: string;
gender?: number;
}>({});
const userProfileFormRef = ref();
/**
@@ -123,15 +136,26 @@ const userProfileFormRef = ref();
const handleOpenDialog = () => {
dialog.visible = true;
// 初始化表单数据
userProfileForm.nickname = userProfile.value?.nickname;
userProfileForm.gender = userProfile.value?.gender;
userProfileForm.name = userProfile.value?.name || "";
userProfileForm.gender = userProfile.value?.gender || 1;
};
// 提交表单
function handleSubmit() {
userProfileFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
if (valid) {
UserAPI.updateProfile(userProfileForm).then(() => {
UserAPI.updateCurrentUserInfo({
name: userProfileForm.name,
gender: userProfileForm.gender,
mobile: userProfile.value?.mobile || "",
email: userProfile.value?.email || "",
username: userProfile.value?.username || "",
dept_name: userProfile.value?.dept_name || "",
positions: userProfile.value?.positions || [],
roles: userProfile.value?.roles || [],
avatar: userProfile.value?.avatar || "",
created_at: userProfile.value?.created_at || "",
}).then(() => {
uni.showToast({ title: "账号资料修改成功", icon: "none" });
dialog.visible = false;
loadUserProfile();
+19 -193
View File
@@ -11,18 +11,6 @@
is-link
@click="handleOpenDialog(DialogType.PASSWORD)"
/>
<wd-cell
title="绑定手机"
:value="userProfile?.mobile"
is-link
@click="handleOpenDialog(DialogType.MOBILE)"
/>
<wd-cell
title="绑定邮箱"
:value="userProfile?.email ? userProfile.email : '未绑定邮箱'"
is-link
@click="handleOpenDialog(DialogType.EMAIL)"
/>
</wd-cell-group>
</wd-card>
@@ -36,33 +24,33 @@
>
<wd-cell-group border>
<wd-input
v-model="passwordChangeForm.oldPassword"
v-model="passwordChangeForm.old_password"
label="原密码"
label-width="160rpx"
show-password
clearable
placeholder="请输入原密码"
prop="oldPassword"
prop="old_password"
:rules="rules.oldPassword"
/>
<wd-input
v-model="passwordChangeForm.newPassword"
v-model="passwordChangeForm.new_password"
label="新密码"
label-width="160rpx"
show-password
clearable
placeholder="请输入新密码"
prop="newPassword"
prop="new_password"
:rules="rules.newPassword"
/>
<wd-input
v-model="passwordChangeForm.confirmPassword"
v-model="passwordChangeForm.confirm_password"
label="确认密码"
label-width="160rpx"
show-password
clearable
placeholder="请确认新密码"
prop="confirmPassword"
prop="confirm_password"
:rules="rules.confirmPassword"
/>
</wd-cell-group>
@@ -70,102 +58,17 @@
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
</view>
</wd-form>
<wd-form
v-if="dialog.type === DialogType.MOBILE"
ref="mobileBindingFormRef"
:model="mobileBindingForm"
custom-class="edit-form"
>
<wd-cell-group border>
<wd-input
v-model="mobileBindingForm.mobile"
label="手机号码"
label-width="160rpx"
clearable
placeholder="请输入手机号码"
prop="mobile"
:rules="rules.mobile"
/>
<wd-input
v-model="mobileBindingForm.code"
label="验证码"
label-width="160rpx"
clearable
placeholder="请输入验证码"
prop="code"
:rules="rules.code"
>
<template #suffix>
<wd-button
plain
:disabled="mobileCountdown > 0"
@click="handleSendVerificationCode('MOBILE')"
>
{{ mobileCountdown > 0 ? `${mobileCountdown}s后重新发送` : "发送验证码" }}
</wd-button>
</template>
</wd-input>
</wd-cell-group>
<view class="p-6">
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
</view>
</wd-form>
<wd-form
v-if="dialog.type === DialogType.EMAIL"
ref="emailBindingFormRef"
:model="emailBindingForm"
custom-class="edit-form"
>
<wd-cell-group border>
<wd-input
v-model="emailBindingForm.email"
label="邮箱"
label-width="160rpx"
clearable
placeholder="请输入邮箱"
prop="email"
:rules="rules.email"
/>
<wd-input
v-model="emailBindingForm.code"
label="验证码"
label-width="160rpx"
clearable
placeholder="请输入验证码"
prop="code"
:rules="rules.code"
>
<template #suffix>
<wd-button
plain
:disabled="emailCountdown > 0"
@click="handleSendVerificationCode('EMAIL')"
>
{{ emailCountdown > 0 ? `${emailCountdown}s后重新发送` : "发送验证码" }}
</wd-button>
</template>
</wd-input>
</wd-cell-group>
<view class="p-6">
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
</view>
</wd-form>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import UserAPI, {
PasswordChangeForm,
MobileBindingForm,
EmailBindingForm,
UserProfileVO,
} from "@/api/user";
import UserAPI, { PasswordChangeForm, UserInfo } from "@/api/user";
const validatorConfirmPassword = (value: string) => {
if (!value) {
return Promise.reject("请确认密码");
} else {
if (value !== passwordChangeForm.newPassword) {
if (value !== passwordChangeForm.new_password) {
return Promise.reject("两次输入的密码不一致");
} else {
return Promise.resolve();
@@ -190,8 +93,6 @@ const rules = reactive({
enum DialogType {
PASSWORD = "password",
MOBILE = "mobile",
EMAIL = "email",
}
const dialog = reactive({
@@ -199,19 +100,13 @@ const dialog = reactive({
type: "" as DialogType, // 修改账号资料,修改密码、绑定手机、绑定邮箱
});
const userProfile = ref<UserProfileVO>(); //用户信息
const passwordChangeForm = reactive<PasswordChangeForm>({});
const mobileBindingForm = reactive<MobileBindingForm>({});
const emailBindingForm = reactive<EmailBindingForm>({});
const userProfile = ref<UserInfo>(); //用户信息
const passwordChangeForm = reactive<PasswordChangeForm>({
old_password: "",
new_password: "",
confirm_password: "",
});
const passwordChangeFormRef = ref();
const mobileBindingFormRef = ref();
const emailBindingFormRef = ref();
const mobileCountdown = ref(0);
const mobileTimer = ref<ReturnType<typeof setInterval> | null>(null);
const emailCountdown = ref(0);
const emailTimer = ref<ReturnType<typeof setTimeout> | null>(null);
// 处理返回按钮点击
const handleBack = () => {
@@ -220,7 +115,7 @@ const handleBack = () => {
/** 加载用户信息 */
const loadUserProfile = async () => {
userProfile.value = await UserAPI.getProfile();
userProfile.value = await UserAPI.getCurrentUserInfo();
};
/**
@@ -232,59 +127,10 @@ const handleOpenDialog = (type: DialogType) => {
dialog.visible = true;
switch (type) {
case DialogType.PASSWORD:
passwordChangeForm.oldPassword = "";
passwordChangeForm.newPassword = "";
passwordChangeForm.confirmPassword = "";
passwordChangeForm.old_password = "";
passwordChangeForm.new_password = "";
passwordChangeForm.confirm_password = "";
break;
case DialogType.MOBILE:
mobileBindingForm.mobile = "";
mobileBindingForm.code = "";
break;
case DialogType.EMAIL:
emailBindingForm.email = "";
emailBindingForm.code = "";
break;
}
};
/**
* 发送验证码
*
* @param contactType 联系方式类型 MOBILE: 手机号码 EMAIL: 邮箱
*/
const handleSendVerificationCode = async (contactType: string) => {
if (contactType === "MOBILE") {
mobileBindingFormRef.value.validate("mobile").then(({ valid }: { valid: boolean }) => {
if (valid) {
UserAPI.sendVerificationCode(mobileBindingForm.mobile!, "MOBILE").then(() => {
uni.showToast({ title: "验证码已发送", icon: "none" });
mobileCountdown.value = 60;
mobileTimer.value = setInterval(() => {
if (mobileCountdown.value > 0) {
mobileCountdown.value -= 1;
} else {
clearInterval(mobileTimer.value!);
}
}, 1000);
});
}
});
} else if (contactType === "EMAIL") {
emailBindingFormRef.value.validate("email").then(({ valid }: { valid: boolean }) => {
if (valid) {
UserAPI.sendVerificationCode(emailBindingForm.email!, "EMAIL").then(() => {
uni.showToast({ title: "验证码已发送", icon: "none" });
emailCountdown.value = 60;
emailTimer.value = setInterval(() => {
if (emailCountdown.value > 0) {
emailCountdown.value -= 1;
} else {
clearInterval(emailTimer.value!);
}
}, 1000);
});
}
});
}
};
@@ -293,32 +139,12 @@ function handleSubmit() {
if (dialog.type === DialogType.PASSWORD) {
passwordChangeFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
if (valid) {
UserAPI.changePassword(passwordChangeForm).then(() => {
UserAPI.changeCurrentUserPassword(passwordChangeForm).then(() => {
uni.showToast({ title: "密码修改成功", icon: "none" });
dialog.visible = false;
});
}
});
} else if (dialog.type === DialogType.MOBILE) {
mobileBindingFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
if (valid) {
UserAPI.bindMobile(mobileBindingForm).then(() => {
uni.showToast({ title: "手机号绑定成功", icon: "none" });
dialog.visible = false;
loadUserProfile();
});
}
});
} else if (dialog.type === DialogType.EMAIL) {
emailBindingFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
if (valid) {
UserAPI.bindEmail(emailBindingForm).then(() => {
uni.showToast({ title: "邮箱绑定成功", icon: "none" });
dialog.visible = false;
loadUserProfile();
});
}
});
}
}
@@ -160,6 +160,7 @@ const startTest = async () => {
// #ifndef H5
await request({
url: "/api/v1/auth/captcha",
method: "GET",
timeout: 5000,
});
// #endif
+1 -1
View File
@@ -8,7 +8,7 @@
<route lang="json">
{
"name": "work",
"name": "index",
"style": {
"navigationBarTitleText": "工作台"
},
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 2.5 MiB

+6 -3
View File
@@ -6,7 +6,7 @@ import AuthAPI, {
type LogoutBody,
} from "@/api/auth";
import UserAPI, { type UserInfo } from "@/api/user";
import { setAccessToken, clearTokens } from "@/utils/auth";
import { getAccessToken, setAccessToken, clearTokens } from "@/utils/auth";
import { getUserInfo, setUserInfo } from "@/utils/storage";
import { USER_INFO_KEY } from "@/constants";
import { Storage } from "@/utils/storage";
@@ -76,9 +76,12 @@ export const useUserStore = defineStore("user", () => {
};
// 登出
const logout = async (data: LogoutBody) => {
const logout = async () => {
try {
await AuthAPI.logout(data); // 调用后台注销接口
const logoutBody: LogoutBody = {
token: getAccessToken(),
};
await AuthAPI.logout(logoutBody); // 调用后台注销接口
} catch (error) {
console.error("登出失败", error);
} finally {
+1 -1
View File
@@ -7,5 +7,5 @@ body,
}
.app-container {
padding: 0 10rpx;
padding: 10rpx 10rpx;
}
+1 -1
View File
@@ -56,7 +56,7 @@ declare global {
/**
* 上传文件返回
*/
interface UploadFilePath {
interface UploadFileResult {
file_path: string;
file_name: string;
origin_name: string;
+59
View File
@@ -0,0 +1,59 @@
// Type definitions for vite-plugin-uni-pages
// This file provides type definitions for the uni-pages plugin
declare module '@uni-helper/vite-plugin-uni-pages' {
import { Plugin } from 'vite'
export interface UniPagesOptions {
/**
* Subpackage directory
* @default 'src/pages'
*/
subPackages?: string[]
/**
* Global style file
*/
globalStyle?: Record<string, any>
/**
* Tabbar configuration
*/
tabBar?: Record<string, any>
/**
* Whether to merge pages.json
* @default true
*/
mergePages?: boolean
/**
* Whether to use uni-cloud-router
* @default false
*/
uniCloudRouter?: boolean
}
export default function UniPages(options?: UniPagesOptions): Plugin
}
// Augment Uni namespace for type-safe routing
declare global {
namespace UniNamespace {
interface NavigateToOptions {
url: string
}
interface RedirectToOptions {
url: string
}
interface SwitchTabOptions {
url: string
}
interface ReLaunchOptions {
url: string
}
}
}
+3 -3
View File
@@ -23,8 +23,8 @@ import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "@/constants";
* 获取访问令牌
* @returns 返回访问令牌,如果不存在则返回null
*/
export function getAccessToken(): string | null {
return Storage.get<string>(ACCESS_TOKEN_KEY) || null;
export function getAccessToken(): string {
return Storage.get<string>(ACCESS_TOKEN_KEY);
}
/**
@@ -132,7 +132,7 @@ export function requireLogin(): void {
if (!accessToken || !userStore.userInfo) {
// 清除可能存在的无效状态
clearTokens();
userStore.logout({ token: accessToken || "" });
userStore.logout();
// 跳转到登录页
uni.reLaunch({