1
This commit is contained in:
12
src/App.vue
Normal file
12
src/App.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup>
|
||||
import AppTitlebar from "./components/AppTitlebar.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<AppTitlebar />
|
||||
<main class="min-h-0 flex-1 overflow-hidden">
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#2D79C7" stroke="none">
|
||||
<path d="M430 5109 c-130 -19 -248 -88 -325 -191 -53 -71 -83 -147 -96 -247
|
||||
-6 -49 -9 -813 -7 -2166 l3 -2090 22 -65 c54 -159 170 -273 328 -323 l70 -22
|
||||
2140 0 2140 0 66 23 c160 55 272 169 322 327 l22 70 0 2135 0 2135 -22 70
|
||||
c-49 157 -155 265 -319 327 l-59 23 -2115 1 c-1163 1 -2140 -2 -2170 -7z
|
||||
m3931 -2383 c48 -9 120 -26 160 -39 l74 -23 3 -237 c1 -130 0 -237 -2 -237 -3
|
||||
0 -26 14 -53 30 -61 38 -197 84 -310 106 -110 20 -293 15 -368 -12 -111 -39
|
||||
-175 -110 -175 -193 0 -110 97 -197 335 -300 140 -61 309 -146 375 -189 30
|
||||
-20 87 -68 126 -107 119 -117 164 -234 164 -426 0 -310 -145 -518 -430 -613
|
||||
-131 -43 -248 -59 -445 -60 -243 -1 -405 24 -577 90 l-68 26 0 242 c0 175 -3
|
||||
245 -12 254 -9 9 -9 12 0 12 7 0 12 -4 12 -9 0 -17 139 -102 223 -138 136 -57
|
||||
233 -77 382 -76 145 0 224 19 295 68 75 52 100 156 59 242 -41 84 -135 148
|
||||
-374 253 -367 161 -522 300 -581 520 -23 86 -23 253 -1 337 73 275 312 448
|
||||
682 492 109 13 401 6 506 -13z m-1391 -241 l0 -205 -320 0 -320 0 0 -915 0
|
||||
-915 -255 0 -255 0 0 915 0 915 -320 0 -320 0 0 205 0 205 895 0 895 0 0 -205z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
10
src/components/AppTitlebar.vue
Normal file
10
src/components/AppTitlebar.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<header
|
||||
class="flex h-9 shrink-0 items-center border-b border-cyan-500/10 bg-bg-base/80 px-3 backdrop-blur-sm select-none"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<span class="font-mono text-xs tracking-widest text-text-muted uppercase">
|
||||
aiclient
|
||||
</span>
|
||||
</header>
|
||||
</template>
|
||||
24
src/components/AuthLayout.vue
Normal file
24
src/components/AuthLayout.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
subtitle: { type: String, default: "" },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-bg flex h-full items-center justify-center overflow-auto p-6">
|
||||
<div class="relative z-10 w-full max-w-md">
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="gradient-text text-3xl font-bold tracking-tight">aiclient</h1>
|
||||
<p v-if="subtitle" class="mt-2 text-sm text-text-muted">{{ subtitle }}</p>
|
||||
</div>
|
||||
|
||||
<div class="glass-card rounded-xl p-6 md:p-8">
|
||||
<h2 class="mb-6 font-mono text-lg font-semibold text-slate-100">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
35
src/main.js
Normal file
35
src/main.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import PrimeVue from "primevue/config";
|
||||
import Aura from "@primeuix/themes/aura";
|
||||
import Button from "primevue/button";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Password from "primevue/password";
|
||||
import Message from "primevue/message";
|
||||
import Card from "primevue/card";
|
||||
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import "./styles/main.css";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Aura,
|
||||
options: {
|
||||
darkModeSelector: ".dark",
|
||||
cssLayer: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app.component("Button", Button);
|
||||
app.component("InputText", InputText);
|
||||
app.component("Password", Password);
|
||||
app.component("Message", Message);
|
||||
app.component("Card", Card);
|
||||
|
||||
app.mount("#app");
|
||||
22
src/main.ts
22
src/main.ts
@@ -1,22 +0,0 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
let greetInputEl: HTMLInputElement | null;
|
||||
let greetMsgEl: HTMLElement | null;
|
||||
|
||||
async function greet() {
|
||||
if (greetMsgEl && greetInputEl) {
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
greetMsgEl.textContent = await invoke("greet", {
|
||||
name: greetInputEl.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
greetInputEl = document.querySelector("#greet-input");
|
||||
greetMsgEl = document.querySelector("#greet-msg");
|
||||
document.querySelector("#greet-form")?.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
greet();
|
||||
});
|
||||
});
|
||||
44
src/router/index.js
Normal file
44
src/router/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { useAuthStore } from "../stores/auth";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: () => import("../views/HomeView.vue"),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: () => import("../views/LoginView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
name: "register",
|
||||
component: () => import("../views/RegisterView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
redirect: "/login",
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const auth = useAuthStore();
|
||||
|
||||
if (to.meta.requiresAuth && !auth.isAuthenticated) {
|
||||
return { name: "login" };
|
||||
}
|
||||
|
||||
if ((to.name === "login" || to.name === "register") && auth.isAuthenticated) {
|
||||
return { name: "home" };
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
101
src/stores/auth.js
Normal file
101
src/stores/auth.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
const USERS_KEY = "aiclient_users";
|
||||
const TOKEN_KEY = "aiclient_token";
|
||||
const USER_KEY = "aiclient_current_user";
|
||||
|
||||
function loadUsers() {
|
||||
try {
|
||||
const raw = localStorage.getItem(USERS_KEY);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveUsers(users) {
|
||||
localStorage.setItem(USERS_KEY, JSON.stringify(users));
|
||||
}
|
||||
|
||||
function createToken(username) {
|
||||
return btoa(`${username}:${Date.now()}`);
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
state: () => ({
|
||||
token: localStorage.getItem(TOKEN_KEY) || "",
|
||||
currentUser: (() => {
|
||||
try {
|
||||
const raw = localStorage.getItem(USER_KEY);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})(),
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isAuthenticated: (state) => Boolean(state.token),
|
||||
},
|
||||
|
||||
actions: {
|
||||
register({ username, password, confirmPassword }) {
|
||||
const name = username?.trim();
|
||||
if (!name || !password) {
|
||||
return { ok: false, message: "用户名和密码不能为空" };
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
return { ok: false, message: "两次输入的密码不一致" };
|
||||
}
|
||||
if (password.length < 6) {
|
||||
return { ok: false, message: "密码长度至少为 6 位" };
|
||||
}
|
||||
|
||||
const users = loadUsers();
|
||||
if (users.some((u) => u.username === name)) {
|
||||
return { ok: false, message: "用户名已存在" };
|
||||
}
|
||||
|
||||
users.push({ username: name, password });
|
||||
saveUsers(users);
|
||||
|
||||
return this.login({ username: name, password });
|
||||
},
|
||||
|
||||
login({ username, password }) {
|
||||
const name = username?.trim();
|
||||
if (!name || !password) {
|
||||
return { ok: false, message: "用户名和密码不能为空" };
|
||||
}
|
||||
|
||||
const users = loadUsers();
|
||||
const user = users.find((u) => u.username === name);
|
||||
if (!user || user.password !== password) {
|
||||
return { ok: false, message: "用户名或密码错误" };
|
||||
}
|
||||
|
||||
const token = createToken(name);
|
||||
const currentUser = { username: name };
|
||||
|
||||
this.token = token;
|
||||
this.currentUser = currentUser;
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(currentUser));
|
||||
|
||||
return { ok: true, message: "登录成功" };
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.token = "";
|
||||
this.currentUser = null;
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
return { ok: true, message: "已退出登录" };
|
||||
},
|
||||
|
||||
// 预留:后续对接 Tauri invoke / HTTP API
|
||||
async loginWithApi(_credentials) {
|
||||
return { ok: false, message: "API 登录尚未实现" };
|
||||
},
|
||||
},
|
||||
});
|
||||
144
src/styles.css
144
src/styles.css
@@ -1,144 +0,0 @@
|
||||
.logo.vite:hover {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
|
||||
.logo.typescript:hover {
|
||||
filter: drop-shadow(0 0 2em #2d79c7);
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.titlebar-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
height: calc(100vh - 36px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
}
|
||||
|
||||
.logo.tauri:hover {
|
||||
filter: drop-shadow(0 0 2em #24c8db);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
button:active {
|
||||
border-color: #396cd8;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#greet-input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.titlebar {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #24c8db;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
}
|
||||
button:active {
|
||||
background-color: #0f0f0f69;
|
||||
}
|
||||
}
|
||||
104
src/styles/main.css
Normal file
104
src/styles/main.css
Normal file
@@ -0,0 +1,104 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-bg-deep: #050810;
|
||||
--color-bg-base: #0b1220;
|
||||
--color-bg-elevated: #111827;
|
||||
--color-accent-cyan: #00e5ff;
|
||||
--color-accent-cyan-dim: #22d3ee;
|
||||
--color-accent-indigo: #6366f1;
|
||||
--color-accent-violet: #8b5cf6;
|
||||
--color-border-glow: rgba(0, 229, 255, 0.25);
|
||||
--color-text-muted: #94a3b8;
|
||||
--font-display: ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: ui-monospace, "Cascadia Code", "Segoe UI Mono", monospace;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-display);
|
||||
background-color: var(--color-bg-deep);
|
||||
color: #e2e8f0;
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.auth-bg {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(0, 229, 255, 0.12), transparent),
|
||||
radial-gradient(ellipse 60% 40% at 100% 100%, rgba(99, 102, 241, 0.1), transparent),
|
||||
linear-gradient(180deg, var(--color-bg-deep) 0%, var(--color-bg-base) 50%, var(--color-bg-deep) 100%);
|
||||
}
|
||||
|
||||
.auth-bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 229, 255, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 229, 255, 0.04) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
mask-image: radial-gradient(ellipse 70% 60% at 50% 40%, black 20%, transparent 75%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.auth-bg::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
left: 50%;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
transform: translateX(-50%);
|
||||
background: radial-gradient(circle, rgba(0, 229, 255, 0.08) 0%, transparent 70%);
|
||||
filter: blur(40px);
|
||||
pointer-events: none;
|
||||
animation: pulse-glow 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
border: 1px solid var(--color-border-glow);
|
||||
backdrop-filter: blur(16px);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(99, 102, 241, 0.08),
|
||||
0 24px 48px rgba(0, 0, 0, 0.45),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--color-accent-cyan) 0%, var(--color-accent-indigo) 55%, var(--color-accent-violet) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: var(--color-accent-cyan-dim);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.auth-link:hover {
|
||||
color: var(--color-accent-cyan);
|
||||
}
|
||||
37
src/views/HomeView.vue
Normal file
37
src/views/HomeView.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "../stores/auth";
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
function onLogout() {
|
||||
auth.logout();
|
||||
router.push({ name: "login" });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex h-full flex-col items-center justify-center gap-6 bg-bg-base p-8"
|
||||
style="
|
||||
background:
|
||||
radial-gradient(ellipse 60% 40% at 50% 0%, rgba(0, 229, 255, 0.08), transparent),
|
||||
var(--color-bg-base);
|
||||
"
|
||||
>
|
||||
<div class="text-center">
|
||||
<p class="font-mono text-xs tracking-widest text-text-muted uppercase">
|
||||
已连接
|
||||
</p>
|
||||
<h1 class="gradient-text mt-2 text-2xl font-bold">
|
||||
欢迎,{{ auth.currentUser?.username }}
|
||||
</h1>
|
||||
<p class="mt-3 max-w-sm text-sm text-text-muted">
|
||||
主界面开发中。当前为登录成功占位页。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button label="退出登录" severity="secondary" outlined @click="onLogout" />
|
||||
</div>
|
||||
</template>
|
||||
89
src/views/LoginView.vue
Normal file
89
src/views/LoginView.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "../stores/auth";
|
||||
import AuthLayout from "../components/AuthLayout.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const loading = ref(false);
|
||||
const feedback = ref({ severity: "", message: "" });
|
||||
|
||||
async function onSubmit() {
|
||||
feedback.value = { severity: "", message: "" };
|
||||
loading.value = true;
|
||||
|
||||
const result = auth.login({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
});
|
||||
|
||||
loading.value = false;
|
||||
|
||||
if (result.ok) {
|
||||
router.push({ name: "home" });
|
||||
return;
|
||||
}
|
||||
|
||||
feedback.value = { severity: "error", message: result.message };
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthLayout title="登录" subtitle="连接你的 AI 工作空间">
|
||||
<form class="flex flex-col gap-5" @submit.prevent="onSubmit">
|
||||
<Message
|
||||
v-if="feedback.message"
|
||||
:severity="feedback.severity"
|
||||
:closable="false"
|
||||
class="w-full"
|
||||
>
|
||||
{{ feedback.message }}
|
||||
</Message>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="login-username" class="font-mono text-xs text-text-muted uppercase">
|
||||
用户名
|
||||
</label>
|
||||
<InputText
|
||||
id="login-username"
|
||||
v-model="username"
|
||||
placeholder="请输入用户名"
|
||||
autocomplete="username"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="login-password" class="font-mono text-xs text-text-muted uppercase">
|
||||
密码
|
||||
</label>
|
||||
<Password
|
||||
id="login-password"
|
||||
v-model="password"
|
||||
placeholder="请输入密码"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
fluid
|
||||
input-class="w-full"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
label="登录"
|
||||
:loading="loading"
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<p class="text-center text-sm text-text-muted">
|
||||
还没有账号?
|
||||
<RouterLink to="/register" class="auth-link">立即注册</RouterLink>
|
||||
</p>
|
||||
</form>
|
||||
</AuthLayout>
|
||||
</template>
|
||||
105
src/views/RegisterView.vue
Normal file
105
src/views/RegisterView.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "../stores/auth";
|
||||
import AuthLayout from "../components/AuthLayout.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const confirmPassword = ref("");
|
||||
const loading = ref(false);
|
||||
const feedback = ref({ severity: "", message: "" });
|
||||
|
||||
async function onSubmit() {
|
||||
feedback.value = { severity: "", message: "" };
|
||||
loading.value = true;
|
||||
|
||||
const result = auth.register({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
confirmPassword: confirmPassword.value,
|
||||
});
|
||||
|
||||
loading.value = false;
|
||||
|
||||
if (result.ok) {
|
||||
router.push({ name: "home" });
|
||||
return;
|
||||
}
|
||||
|
||||
feedback.value = { severity: "error", message: result.message };
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthLayout title="注册" subtitle="创建你的 AI 客户端账号">
|
||||
<form class="flex flex-col gap-5" @submit.prevent="onSubmit">
|
||||
<Message
|
||||
v-if="feedback.message"
|
||||
:severity="feedback.severity"
|
||||
:closable="false"
|
||||
class="w-full"
|
||||
>
|
||||
{{ feedback.message }}
|
||||
</Message>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="reg-username" class="font-mono text-xs text-text-muted uppercase">
|
||||
用户名
|
||||
</label>
|
||||
<InputText
|
||||
id="reg-username"
|
||||
v-model="username"
|
||||
placeholder="请输入用户名"
|
||||
autocomplete="username"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="reg-password" class="font-mono text-xs text-text-muted uppercase">
|
||||
密码
|
||||
</label>
|
||||
<Password
|
||||
id="reg-password"
|
||||
v-model="password"
|
||||
placeholder="至少 6 位"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
fluid
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="reg-confirm" class="font-mono text-xs text-text-muted uppercase">
|
||||
确认密码
|
||||
</label>
|
||||
<Password
|
||||
id="reg-confirm"
|
||||
v-model="confirmPassword"
|
||||
placeholder="再次输入密码"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
fluid
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
label="注册"
|
||||
:loading="loading"
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<p class="text-center text-sm text-text-muted">
|
||||
已有账号?
|
||||
<RouterLink to="/login" class="auth-link">返回登录</RouterLink>
|
||||
</p>
|
||||
</form>
|
||||
</AuthLayout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user