feat: 完成修改用户名功能

This commit is contained in:
Litrix 2024-12-23 20:30:06 +08:00
parent 364dc4560f
commit 4f3dc11101
6 changed files with 135 additions and 61 deletions

View File

@ -1,3 +1,4 @@
import { REQUEST_BASE_URL } from '@/env';
import {
userInfoRespSchema,
userInfoRespSchemaNullable,
@ -13,7 +14,7 @@ import { ElMessage } from 'element-plus';
import type { z } from 'zod';
export const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_REQUEST_BASE_URL,
baseURL: REQUEST_BASE_URL,
});
// 自动添加token到请求中.
axiosInstance.interceptors.request.use((config) => {
@ -32,11 +33,8 @@ axiosInstance.interceptors.response.use((response) => {
return response;
});
export type RawResp = Record<string, unknown>;
const errorMessage = (
description: string = '请求错误',
code: string | number | undefined,
msg: string,
) => `${description}(${code})${msg}`;
const errorMessage = (description: string, code: string | number | undefined, msg: string) =>
`${description}(${code})${msg}`;
interface RequestOptions extends Override<AxiosRequestConfig, { method?: Method }, 'url'> {
errorDescription?: string;
@ -44,7 +42,7 @@ interface RequestOptions extends Override<AxiosRequestConfig, { method?: Method
export function parseRawResp<T extends AnyRespSchema>(
schema: T,
raw: RawResp | undefined,
errorDescription?: string,
errorDescription: string,
): SucceedRespOf<z.infer<T>> | undefined {
if (!raw) return;
const resp = schema.parse(raw);
@ -59,14 +57,16 @@ export async function request<T extends AnyRespSchema>(
schema: T,
options: RequestOptions = {},
) {
const { errorDescription = '请求错误' } = options;
return parseRawResp(
schema,
await axiosInstance
.request<RawResp>(Object.assign(options, { url }))
.then((r) => r.data)
.catch((err: AxiosError): undefined => {
ElMessage.error(errorMessage(options.errorDescription, err.code, err.message));
ElMessage.error(errorMessage(errorDescription, err.code, err.message));
}),
errorDescription,
);
}
export async function getUserInfo(): Promise<SucceedUserInfoResp>;

View File

@ -41,7 +41,7 @@
</template>
</el-input>
</el-form-item>
<el-form-item prop="verifyCode">
<el-form-item v-if="!DEV" prop="verifyCode">
<verify-input v-model="loginFormData" :disabled="logining" />
</el-form-item>
<el-form-item style="margin-bottom: 0">
@ -105,7 +105,7 @@
</template>
</el-input>
</el-form-item>
<el-form-item prop="verifyCode">
<el-form-item v-if="!DEV" prop="verifyCode">
<verify-input v-model="registerFormData" :disabled="registering" />
</el-form-item>
<el-form-item>
@ -151,9 +151,12 @@ export interface LoginRegisterDialogProps {
handleLogin: (params: LoginParams) => Promise<boolean>;
handleRegister: (params: RegisterParams) => Promise<boolean>;
}
export const usernameLength = 3;
export const passwordLength = usernameLength;
</script>
<script lang="ts" setup>
import VerifyInput, { type VerifyImagePath } from '@/components/app/VerifyInput.vue';
import { DEV } from '@/env';
import { useMediaStore } from '@/stores/media';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { storeToRefs } from 'pinia';
@ -174,18 +177,17 @@ watch(show, (show) => {
loginFormRef.value?.resetFields();
registerFormRef.value?.resetFields();
});
const inputLength = 3;
const loginFormRules = reactive<FormRules<typeof loginFormData>>({
username: [
{ required: true, message: '请输入用户名' },
{ min: inputLength, message: `用户名长度不能小于${inputLength}` },
{ min: usernameLength, message: `用户名长度不能小于${usernameLength}` },
],
password: [
{ required: true, message: '请输入密码' },
{ min: inputLength, message: `密码长度不能小于${inputLength}` },
{ min: passwordLength, message: `密码长度不能小于${passwordLength}` },
],
verifyCode: [
{ required: true, message: '请输入验证码' },
{ required: !DEV, message: '请输入验证码' },
{ pattern: /^[0-9A-Za-z]{4}$/, message: '验证码不符合格式' },
],
});
@ -196,18 +198,19 @@ async function submitLoginForm() {
} catch (e) {
return;
}
let key: string;
if (typeof loginFormData.verifyImage === 'string') {
ElMessage.error('请获取验证码');
return;
if (!DEV) {
ElMessage.error('请获取验证码');
return;
}
key = '';
} else {
({ key } = loginFormData.verifyImage);
}
logining.value = true;
try {
const {
username,
password,
verifyImage: { key },
verifyCode: code,
} = loginFormData;
const { username, password, verifyCode: code } = loginFormData;
if (!(await handleLogin({ username, password, key, code }))) return;
ElMessage.success('登录成功');
show.value = false;
@ -235,8 +238,8 @@ const registerFormRules = reactive<FormRules<typeof registerFormData>>({
callback('请输入密码');
} else if (value !== registerFormData.password) {
callback('密码不一致');
} else if (value.length < inputLength) {
callback(`密码长度不能小于${inputLength}`);
} else if (value.length < passwordLength) {
callback(`密码长度不能小于${passwordLength}`);
} else {
callback();
}
@ -251,18 +254,19 @@ async function submitRegisterForm() {
} catch {
return;
}
let key: string;
if (typeof registerFormData.verifyImage === 'string') {
ElMessage.error('请输入验证码');
return;
if (!DEV) {
ElMessage.error('请获取验证码');
return;
}
key = '';
} else {
({ key } = registerFormData.verifyImage);
}
registering.value = true;
try {
const {
username,
password,
verifyCode: code,
verifyImage: { key },
} = registerFormData;
const { username, password, verifyCode: code } = registerFormData;
if (!(await handleRegister({ username, password, code, key, auth: 1 }))) return;
ElMessage.success('注册成功');
show.value = false;

4
src/env.ts Normal file
View File

@ -0,0 +1,4 @@
// export const DEV = import.meta.env.DEV;
export const DEV = false;
export const REQUEST_BASE_URL = import.meta.env.VITE_REQUEST_BASE_URL;
export const WEBSOCKET_BASE_URL = import.meta.env.VITE_WEBSOCKET_BASE_URL;

View File

@ -14,10 +14,7 @@
v-loading="loading"
:element-loading-text="loadingText"
class="gobang-list-page-main flex-col"
:class="[
!rooms.length ? 'justify-center items-center' : '',
{ '!flex': !rooms.length },
]"
:class="[!rooms.length ? 'justify-center items-center' : '', { '!flex': !rooms.length }]"
>
<template v-if="rooms.length || !loading">
<template v-if="!rooms.length">
@ -153,10 +150,7 @@ export function useGobangSocket(
) {
return useGameSocket<Request, Resp>()(
Object.assign(options, {
url:
import.meta.env.VITE_DEBUG === 'true'
? `ws://172.16.114.84:58080/chess/${useUserStore().token}`
: `wss://wzpmc.cn:18080/chess/${useUserStore().token}`,
url: `${WEBSOCKET_BASE_URL}/chess/${useUserStore().token}`,
relations,
}),
);
@ -171,6 +165,7 @@ const playerCountMap: Record<RoomState, string> = {
<script lang="ts" setup>
import CreateGobangRoomDialog from '@/components/gobang/CreateGobangRoomDialog.vue';
import GobangHeader from '@/components/gobang/GobangHeader.vue';
import { WEBSOCKET_BASE_URL } from '@/env';
import router from '@/router';
import type { UserInfo } from '@/schemas/response';
import { useUserStore } from '@/stores/user';

View File

@ -2,38 +2,60 @@
<div>
<h4 class="p">编辑资料</h4>
<el-divider />
<div class="p flex flex-col items-center gap-10px">
<el-avatar :size="100" :src="getAvatarURL(userStore.userInfo!.avatar)" />
<el-upload
:show-file-list="false"
:http-request="upload"
@success="onUploadSuccess"
@error="onUploadError"
>
<div class="p outer">
<el-avatar class="self-center" :size="100" :src="getAvatarURL(userStore.userInfo!.avatar)" />
<el-upload class="self-center" :show-file-list="false" :http-request="upload">
<template #trigger>
<el-link type="primary" :icon="Upload" :underline="false" :limit="1">上传头像</el-link>
</template>
</el-upload>
</div>
<el-divider />
<div class="p outer">
<el-form
ref="formRef"
class="flex flex-col items-stretch"
hide-required-asterisk
@submit.prevent
:model="data"
:rules="rules"
>
<el-form-item prop="username" label="用户名">
<el-input v-model="data.username" />
</el-form-item>
<el-form-item class="self-center">
<el-button type="primary" native-type="submit" @click="submit" :loading="submitting">
保存设置
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<style lang="scss" scoped>
.p {
padding: 0 var(--main-h-padding);
}
.outer {
@apply flex flex-col gap-10px;
}
.el-form {
padding: 0 100px;
}
</style>
<script lang="ts">
import { getAvatarURL, request } from '@/api';
import { usernameLength } from '@/components/app/LoginRegisterDialog.vue';
import { ordinarySchema, uploadAvatarRespSchema } from '@/schemas/response';
import { useUserStore } from '@/stores/user';
import { Upload } from '@element-plus/icons-vue';
import {
ElMessage,
type UploadInstance,
type UploadProps,
type FormInstance,
type FormRules,
type UploadRequestHandler,
} from 'element-plus';
import { defineComponent, ref } from 'vue';
import { defineComponent, reactive, ref } from 'vue';
import type { RouteLocationNormalized } from 'vue-router';
function validate({ params: { id } }: RouteLocationNormalized) {
if (!useUserStore().isSelf(id as string)) {
@ -48,29 +70,79 @@ export default defineComponent({
</script>
<script setup lang="ts">
const userStore = useUserStore();
const uploadRef = ref<UploadInstance>();
const upload: UploadRequestHandler = async ({ file }) => {
if (!file.type.startsWith('image')) {
ElMessage.error('只能上传图片');
return;
}
if (file.size > 1024 * 1024) {
throw new Error('文件大小不能超过1MB');
ElMessage.error('文件大小不能超过1MB');
return;
}
const formdata = new FormData();
formdata.append('file', file);
const uploadResp = await request('/api/user/avatar', uploadAvatarRespSchema, {
method: 'post',
data: formdata,
errorDescription: '上传头像失败',
});
if (!uploadResp) return;
const changeResp = await request('/api/user/avatar', ordinarySchema, {
method: 'put',
params: { code: uploadResp.data },
errorDescription: '上传头像失败',
});
if (!changeResp) return;
await userStore.updateSelfUserInfo();
ElMessage.success('头像上传成功');
};
const onUploadSuccess: UploadProps['onSuccess'] = (...args) => {
console.log(args);
};
const onUploadError: UploadProps['onError'] = (err) => {
ElMessage.error(err.message);
};
const formRef = ref<FormInstance>();
const data = reactive({
username: userStore.userInfo!.name,
});
const rules = reactive<FormRules<typeof data>>({
username: [
{ required: true, message: '请输入用户名' },
{ min: usernameLength, message: `用户名长度不能小于${usernameLength}` },
],
});
const submitting = ref(false);
async function submit() {
try {
await formRef.value?.validate();
} catch {
return;
}
submitting.value = true;
const { username } = data;
let changed = false,
allSucceed = true;
try {
if (username !== userStore.userInfo!.name) {
const resp = await request('/api/user/rename', ordinarySchema, {
method: 'put',
data: {
id: userStore.userInfo!.id,
newName: username,
},
errorDescription: '修改用户名失败',
});
if (resp) {
changed = true;
} else {
allSucceed = false;
}
}
} finally {
if (changed) {
await userStore.updateSelfUserInfo();
if (allSucceed) {
ElMessage.success('修改成功');
}
} else if (allSucceed) {
ElMessage.info('没有要修改的信息');
}
submitting.value = false;
}
}
</script>

View File

@ -40,7 +40,7 @@
</el-button>
</el-card>
<div class="flex gap-10px">
<el-card class="aside w-200px">
<el-card class="aside w-200px self-start">
<el-menu class="menu" router :default-active="$route.fullPath">
<h4 class="mb-5px text-center">个人中心</h4>
<el-divider />
@ -49,7 +49,6 @@
<el-divider />
<el-menu-item :index="`/user/${$route.params.id}/edit`">编辑信息</el-menu-item>
</template>
<el-divider />
</el-menu>
</el-card>
<el-card class="main flex-1">