feat: 大部分注册登录功能

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-24 18:02:42 +08:00
parent 5ab74ad9dd
commit 5294528148
23 changed files with 858 additions and 167 deletions
+10 -8
View File
@@ -1,11 +1,13 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { Toaster } from "vue-sonner";
import "vue-sonner/style.css";
</script>
<template>
<h1 class="">
Hello World!
<el-button type="primary">Primary</el-button>
</h1>
<div class="min-h-dvh">
<Toaster position="top-center" />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
<style scoped></style>
+3
View File
@@ -0,0 +1,3 @@
:root {
--el-color-primary: #2563eb !important;
}
+114
View File
@@ -0,0 +1,114 @@
<script setup lang="ts">
import { reactive, ref } from "vue";
import type {
ICommonResponse,
IUserLoginData,
IUserLoginRequest
} from "#shared/types";
import { useUserStore } from "~/stores";
type FormRef = {
validate: () => Promise<boolean>;
resetFields: () => void;
};
const emit = defineEmits<{
success: [response: ICommonResponse<IUserLoginData>];
}>();
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const formRef = ref<FormRef>();
const form = reactive<Required<IUserLoginRequest>>({
username: "",
password: ""
});
const rules = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }]
};
const submit = async () => {
const valid = await formRef.value?.validate().catch(() => false);
if (!valid) return;
const response = await userStore.login({
username: form.username.trim(),
password: form.password
});
if (response?.code === 0) {
emit("success", response);
}
};
const resetForm = () => {
formRef.value?.resetFields();
};
const goToRegister = () => {
router.replace({
query: {
...route.query,
_loginAction: "signUp"
}
});
};
defineExpose({
resetForm
});
</script>
<template>
<div class="w-full max-w-sm">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
@submit.prevent="submit"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="form.username"
autocomplete="username"
placeholder="请输入用户名"
size="large"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="form.password"
autocomplete="current-password"
placeholder="请输入密码"
show-password
size="large"
type="password"
@keyup.enter="submit"
/>
</el-form-item>
<el-button
class="w-full"
native-type="submit"
size="large"
type="primary"
:loading="userStore.loginLoading"
>
登录
</el-button>
</el-form>
<div class="mt-4 text-center text-sm">
没有账户
<span class="cursor-pointer text-blue-600" @click="goToRegister">
注册
</span>
</div>
</div>
</template>
+188
View File
@@ -0,0 +1,188 @@
<script setup lang="ts">
import { reactive, ref } from "vue";
import type {
ICommonResponse,
IUserRegisterData,
IUserRegisterRequest
} from "#shared/types";
import { useUserStore } from "~/stores";
type FormRef = {
validate: () => Promise<boolean>;
resetFields: () => void;
};
type RegisterForm = Required<
Pick<IUserRegisterRequest, "username" | "password" | "email">
> &
Pick<IUserRegisterRequest, "verification_code" | "aff_code"> & {
confirmPassword: string;
};
const emit = defineEmits<{
success: [response: ICommonResponse<IUserRegisterData>];
}>();
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const formRef = ref<FormRef>();
const form = reactive<RegisterForm>({
username: "",
password: "",
confirmPassword: "",
email: "",
verification_code: "",
aff_code: ""
});
const validateConfirmPassword = (
_rule: unknown,
value: string,
callback: (error?: Error) => void
) => {
if (!value) {
callback(new Error("请再次输入密码"));
return;
}
if (value !== form.password) {
callback(new Error("两次输入的密码不一致"));
return;
}
callback();
};
const rules = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
email: [
{ required: true, message: "请输入邮箱", trigger: "blur" },
{ type: "email", message: "邮箱格式不正确", trigger: ["blur", "change"] }
],
password: [
{ required: true, message: "请输入密码", trigger: "blur" },
{ min: 6, message: "密码至少 6 位", trigger: "blur" }
],
confirmPassword: [{ validator: validateConfirmPassword, trigger: "blur" }]
};
const submit = async () => {
const valid = await formRef.value?.validate().catch(() => false);
if (!valid) return;
const payload: IUserRegisterRequest = {
username: form.username.trim(),
password: form.password,
email: form.email.trim()
};
if (form.verification_code?.trim()) {
payload.verification_code = form.verification_code.trim();
}
if (form.aff_code?.trim()) {
payload.aff_code = form.aff_code.trim();
}
const response = await userStore.register(payload);
if (response?.code === 0) {
emit("success", response);
}
};
const resetForm = () => {
formRef.value?.resetFields();
};
const goToLogin = () => {
const { _loginAction, ...query } = route.query;
router.replace({ query });
};
defineExpose({
resetForm
});
</script>
<template>
<div class="w-full max-w-sm">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
@submit.prevent="submit"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="form.username"
autocomplete="username"
placeholder="请输入用户名"
size="large"
/>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input
v-model="form.email"
autocomplete="email"
placeholder="请输入邮箱"
size="large"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="form.password"
autocomplete="new-password"
placeholder="请输入密码"
show-password
size="large"
type="password"
/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="form.confirmPassword"
autocomplete="new-password"
placeholder="请再次输入密码"
show-password
size="large"
type="password"
@keyup.enter="submit"
/>
</el-form-item>
<el-form-item label="邮箱验证码" prop="verification_code">
<el-input
v-model="form.verification_code"
placeholder="如需验证请填写"
size="large"
/>
</el-form-item>
<el-form-item label="邀请码" prop="aff_code">
<el-input v-model="form.aff_code" placeholder="选填" size="large" />
</el-form-item>
<el-button
class="w-full"
native-type="submit"
size="large"
type="primary"
:loading="userStore.registerLoading"
>
注册
</el-button>
</el-form>
<div class="mt-4 text-center text-sm">
已有账户
<span class="cursor-pointer text-blue-600" @click="goToLogin">登录</span>
</div>
</div>
</template>
+5
View File
@@ -0,0 +1,5 @@
<template>
<div>
<slot />
</div>
</template>
+5
View File
@@ -0,0 +1,5 @@
<template>
<main class="min-h-dvh w-full flex items-center justify-center p-6">
<slot />
</main>
</template>
+5
View File
@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<div>123</div>
</template>
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
import LogInCom from "~/components/login/LogInCom.vue";
import SignUpCom from "~/components/login/SignUpCom.vue";
import { useUserStore } from "~/stores";
definePageMeta({
layout: "login-view"
});
const route = useRoute();
const userStore = useUserStore();
const { isOnline } = storeToRefs(userStore);
const isSignUp = computed(() => route.query._loginAction === "signUp");
onMounted(() => {
if (isOnline.value) {
navigateTo("/");
}
});
</script>
<template>
<div class="w-full flex justify-center">
<SignUpCom v-if="isSignUp" />
<LogInCom v-else />
</div>
</template>
+1
View File
@@ -0,0 +1 @@
export * from "./user_service";
+41
View File
@@ -0,0 +1,41 @@
import type {
ICommonResponse,
IUserLoginData,
IUserLoginRequest,
IUserLogoutData,
IUserRegisterData,
IUserRegisterRequest
} from "#shared/types";
export class UserService {
public static basePath = "/api/auth";
public static UserLogin(request: IUserLoginRequest) {
return $fetch<ICommonResponse<IUserLoginData>>(
`${UserService.basePath}/login`,
{
method: "POST",
body: request
}
);
}
public static UserRegister(request: IUserRegisterRequest) {
return $fetch<ICommonResponse<IUserRegisterData>>(
`${UserService.basePath}/register`,
{
method: "POST",
body: request
}
);
}
public static UserLogout() {
return $fetch<ICommonResponse<IUserLogoutData>>(
`${UserService.basePath}/logout`,
{
method: "GET"
}
);
}
}
+1
View File
@@ -0,0 +1 @@
export * from "./user";
+116
View File
@@ -0,0 +1,116 @@
import { toast } from "vue-sonner";
import type {
ICommonResponse,
IUserLoginData,
IUserLoginRequest,
IUserLogoutData,
IUserRegisterData,
IUserRegisterRequest
} from "#shared/types";
import { UserService } from "~/services";
export const useUserStore = defineStore("user", () => {
/** 用户登录状态 */
const isOnline = ref(false);
/** 用户信息 */
const userInfo = ref<IUserLoginData | null>(null);
/** 登录加载状态 */
const loginLoading = ref(false);
/** 注册加载状态 */
const registerLoading = ref(false);
/** 登出加载状态 */
const logoutLoading = ref(false);
/** 用户登录 */
const login = async (
request: IUserLoginRequest
): Promise<ICommonResponse<IUserLoginData> | null> => {
if (loginLoading.value || isOnline.value) return null;
loginLoading.value = true;
try {
const res = await UserService.UserLogin(request);
if (res.code === 0) {
isOnline.value = true;
userInfo.value = res.data;
toast.success(res.msg || "登录成功");
} else {
isOnline.value = false;
userInfo.value = null;
toast.warning(res.msg || "登录失败");
}
return res;
} catch (err) {
isOnline.value = false;
userInfo.value = null;
toast.error(getErrorMessage(err, "登录失败"));
return null;
} finally {
loginLoading.value = false;
}
};
/** 用户注册 */
const register = async (
request: IUserRegisterRequest
): Promise<ICommonResponse<IUserRegisterData> | null> => {
if (registerLoading.value || isOnline.value) return null;
registerLoading.value = true;
try {
const res = await UserService.UserRegister(request);
res.code === 0
? toast.success(res.msg || "注册成功")
: toast.warning(res.msg || "注册失败");
return res;
} catch (err) {
toast.error(getErrorMessage(err, "注册失败"));
return null;
} finally {
registerLoading.value = false;
}
};
/** 用户登出 */
const logout = async (): Promise<ICommonResponse<IUserLogoutData> | null> => {
if (logoutLoading.value || !isOnline.value) return null;
logoutLoading.value = true;
try {
const res = await UserService.UserLogout();
if (res.code === 0) {
isOnline.value = false;
userInfo.value = null;
toast.success(res.msg || "登出成功");
} else {
toast.warning(res.msg || "登出失败");
}
return res;
} catch (err) {
toast.error(getErrorMessage(err, "登出失败"));
return null;
} finally {
logoutLoading.value = false;
}
};
return {
isOnline,
userInfo,
loginLoading,
registerLoading,
logoutLoading,
login,
register,
logout
};
});
+27
View File
@@ -0,0 +1,27 @@
/** 获取错误消息 */
export const getErrorMessage = (error: unknown, fallbackMessage: string) => {
const fetchError = error as {
data?: unknown;
message?: string;
response?: {
_data?: unknown;
};
};
const errorData = fetchError.data ?? fetchError.response?._data;
if (typeof errorData === "string") {
return errorData;
}
if (
errorData &&
typeof errorData === "object" &&
"msg" in errorData &&
typeof errorData.msg === "string"
) {
return errorData.msg;
}
return fetchError.message || fallbackMessage;
};