1
This commit is contained in:
37
.cursor/rules/aiclient-overview.mdc
Normal file
37
.cursor/rules/aiclient-overview.mdc
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
description: aiclient 项目总览与通用约定
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# aiclient 项目约定
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **桌面壳**:Tauri 2(Rust),单例、无边框、可拖动标题栏
|
||||
- **前端**:Vue 3 + Vite 6,**纯 JavaScript**(不使用 TypeScript)
|
||||
- **UI**:Tailwind CSS v4(`@tailwindcss/vite`)+ PrimeVue 4(Aura 深色预设)
|
||||
- **状态/路由**:Pinia + Vue Router
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/ # Vue 前端
|
||||
main.js # 入口:插件与全局组件注册
|
||||
App.vue # 布局:标题栏 + router-view
|
||||
styles/main.css # Tailwind + 科技感主题令牌
|
||||
router/ # 路由与守卫
|
||||
stores/ # Pinia(auth 等)
|
||||
views/ # 页面
|
||||
components/ # 可复用组件
|
||||
src-tauri/ # Tauri Rust 与配置
|
||||
```
|
||||
|
||||
## 常用命令
|
||||
|
||||
- `npm run dev` — 仅前端(端口 1420)
|
||||
- `npm run tauri dev` — 桌面开发
|
||||
- `npm run build` / `npm run tauri build` — 构建(前端无 `tsc` 步骤)
|
||||
|
||||
## 通用原则
|
||||
|
||||
- 用户界面文案默认使用简体中文
|
||||
51
.cursor/rules/frontend-vue.mdc
Normal file
51
.cursor/rules/frontend-vue.mdc
Normal 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. 业务状态放 Pinia,View 只做展示与提交
|
||||
45
.cursor/rules/tauri-desktop.mdc
Normal file
45
.cursor/rules/tauri-desktop.mdc
Normal 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`
|
||||
42
index.html
42
index.html
@@ -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
1225
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
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>
|
||||
@@ -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
25
vite.config.js
Normal 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/**"],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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/**"],
|
||||
},
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user