This commit is contained in:
fengchuanhn@gmail.com
2026-05-15 17:51:47 +08:00
parent bac4ced1fe
commit 584d4c47a1
23 changed files with 1952 additions and 304 deletions

View File

@@ -0,0 +1,37 @@
---
description: aiclient 项目总览与通用约定
alwaysApply: true
---
# aiclient 项目约定
## 技术栈
- **桌面壳**Tauri 2Rust单例、无边框、可拖动标题栏
- **前端**Vue 3 + Vite 6**纯 JavaScript**(不使用 TypeScript
- **UI**Tailwind CSS v4`@tailwindcss/vite`+ PrimeVue 4Aura 深色预设)
- **状态/路由**Pinia + Vue Router
## 目录结构
```
src/ # Vue 前端
main.js # 入口:插件与全局组件注册
App.vue # 布局:标题栏 + router-view
styles/main.css # Tailwind + 科技感主题令牌
router/ # 路由与守卫
stores/ # Piniaauth 等)
views/ # 页面
components/ # 可复用组件
src-tauri/ # Tauri Rust 与配置
```
## 常用命令
- `npm run dev` — 仅前端(端口 1420
- `npm run tauri dev` — 桌面开发
- `npm run build` / `npm run tauri build` — 构建(前端无 `tsc` 步骤)
## 通用原则
- 用户界面文案默认使用简体中文

View File

@@ -0,0 +1,51 @@
---
description: Vue 前端、路由、认证与 UI 规范
globs: src/**/*
alwaysApply: false
---
# 前端规范Vue 3
## 语言与构建
- 使用 `.js` / `.vue`**禁止**引入 TypeScript无 `tsconfig`、无 `.ts` 入口)
- 全局样式仅通过 `src/styles/main.css``@import "tailwindcss"` + `@theme` 令牌)
- PrimeVue 在 `src/main.js` 注册Aura 主题 + `html.dark` 深色模式
## 目录职责
| 路径 | 用途 |
|------|------|
| `views/` | 路由页面Login、Register、Home |
| `components/` | 布局与复用 UI`AppTitlebar`、`AuthLayout` |
| `stores/` | Pinia 状态(`auth.js` |
| `router/index.js` | 路由表 + `beforeEach` 守卫 |
## 路由
| 路径 | 组件 | 说明 |
|------|------|------|
| `/login` | LoginView | 未登录入口 |
| `/register` | RegisterView | 注册后自动登录 |
| `/` | HomeView | `meta.requiresAuth: true` |
守卫逻辑:
- 无 token 访问需登录页 → `/login`
- 已登录访问 `/login` 或 `/register` → `/`
- 未知路径 → `/login`
## UI / 科技感配色
- 设计令牌在 `styles/main.css` 的 `@theme``bg-deep`、`accent-cyan`、`accent-indigo` 等
- 登录/注册共用 `AuthLayout`:网格背景 + `glass-card` + `gradient-text`
- 布局类名示例:`bg-bg-base`、`text-text-muted`、`auth-link`
- 表单组件:`InputText`、`Password``fluid`、`toggle-mask`)、`Button`
- **Tauri 拖动**:标题栏保留 `data-tauri-drag-region`(见 `AppTitlebar.vue`),按钮/输入框勿加该属性
## 新增页面 checklist
1. 在 `router/index.js` 注册路由与 `meta`
2. 需登录则设 `requiresAuth: true`
3. 业务状态放 PiniaView 只做展示与提交

View File

@@ -0,0 +1,45 @@
---
description: Tauri 2 桌面壳、窗口与单例规范
globs: src-tauri/**/*
alwaysApply: false
---
# Tauri 桌面规范
## 窗口配置(`tauri.conf.json`
- 主窗口 `label`: `"main"`(单例回调中通过此 label 获取窗口)
- `decorations: false` — 无边框;`resizable: true`
- `identifier`: `com.aiclient.desktop`(避免以 `.app` 结尾)
## 单例运行
- 插件:`tauri-plugin-single-instance`
- 二次启动时:对 `main` 窗口 `unminimize` → `show` → `set_focus`**不**新建窗口
- 注册于 `src-tauri/src/lib.rs`,需 `use tauri::Manager`
```rust
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}))
```
## 权限(`capabilities/default.json`
- 拖动窗口需:`core:window:allow-start-dragging`
- 前端标题栏元素使用 `data-tauri-drag-region`(在 `AppTitlebar.vue`
## Rust 约定
- 入口逻辑在 `src/lib.rs``main.rs` 仅调用 `tauri_app_lib::run()`
- 新增 Tauri 命令:在 `lib.rs` 用 `#[tauri::command]` 定义,并在 `invoke_handler` 注册
- 前端调用:`import { invoke } from "@tauri-apps/api/core"`
## 开发注意
- Vite 固定端口 `1420`(见 `vite.config.js`),与 `tauri.conf.json` 的 `devUrl` 一致
- 修改 capabilities 或插件后需重新 `tauri dev` / `tauri build`

View File

@@ -1,46 +1,12 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="/src/styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri App</title>
<script type="module" src="/src/main.ts" defer></script>
<title>aiclient</title>
</head>
<body>
<header class="titlebar" data-tauri-drag-region>
<span class="titlebar-title">aiclient</span>
</header>
<main class="container">
<h1>Welcome to Tauri</h1>
<div class="row">
<a href="https://vite.dev" target="_blank">
<img src="/src/assets/vite.svg" class="logo vite" alt="Vite logo" />
</a>
<a href="https://tauri.app" target="_blank">
<img
src="/src/assets/tauri.svg"
class="logo tauri"
alt="Tauri logo"
/>
</a>
<a href="https://www.typescriptlang.org/docs" target="_blank">
<img
src="/src/assets/typescript.svg"
class="logo typescript"
alt="typescript logo"
/>
</a>
</div>
<p>Click on the Tauri logo to learn more about the framework</p>
<form class="row" id="greet-form">
<input id="greet-input" placeholder="Enter a name..." />
<button type="submit">Greet</button>
</form>
<p id="greet-msg"></p>
</main>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1225
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,30 @@
{
"name": "tauri-app",
"name": "aiclient",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@primeuix/themes": "^1.2.1",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2"
"@tauri-apps/plugin-opener": "^2",
"pinia": "^3.0.3",
"primevue": "^4.3.6",
"vue": "^3.5.17",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.11",
"@tauri-apps/cli": "^2",
"vite": "^6.0.3",
"typescript": "~5.6.2"
"@vitejs/plugin-vue": "^6.0.0",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11",
"vite": "^6.0.3"
}
}

View File

@@ -1,11 +1,5 @@
use tauri::Manager;
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@@ -17,7 +11,6 @@ pub fn run() {
}
}))
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

12
src/App.vue Normal file
View 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>

View File

@@ -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

View 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>

View 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
View 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");

View File

@@ -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
View 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
View 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 登录尚未实现" };
},
},
});

View File

@@ -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
View 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
View 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
View 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
View 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>

View File

@@ -1,23 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

25
vite.config.js Normal file
View File

@@ -0,0 +1,25 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import tailwindcss from "@tailwindcss/vite";
const host = process.env.TAURI_DEV_HOST;
export default defineConfig({
plugins: [vue(), tailwindcss()],
clearScreen: false,
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
ignored: ["**/src-tauri/**"],
},
},
});

View File

@@ -1,30 +0,0 @@
import { defineConfig } from "vite";
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vite.dev/config/
export default defineConfig(async () => ({
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent Vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell Vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));