feat: 调整项目结构
This commit is contained in:
85
.trae/documents/实现简约美观的登录页(Vue + Tailwind).md
Normal file
85
.trae/documents/实现简约美观的登录页(Vue + Tailwind).md
Normal file
@@ -0,0 +1,85 @@
|
||||
## 页面需求
|
||||
|
||||
* 输入:账号、密码;按钮:登录
|
||||
|
||||
* 交互:输入校验(必填、长度/格式)、回车提交、禁用按钮(加载中/校验失败)、错误提示、明文/密文切换
|
||||
|
||||
* 布局:简约卡片居中、品牌标题、副文、响应式(桌面)、浅色渐变背景
|
||||
|
||||
## 现状与集成点
|
||||
|
||||
* 已存在页面骨架:`src/views/login/index.vue`(中心卡片+标题,待补表单)
|
||||
|
||||
* 路由:登录路由目前注释(`src/router/index.ts`),默认首页为 `/` 的 `BrowserLayout`(标签页界面)
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. 表单与校验
|
||||
|
||||
* 在 `src/views/login/index.vue` 中添加账号/密码输入框与登录按钮
|
||||
|
||||
* 使用 `ref`/`reactive` 管理表单;规则:
|
||||
|
||||
* 账号:必填、长度 4-32、允许邮箱/手机号/用户名(简单格式校验)
|
||||
|
||||
* 密码:必填、长度 ≥ 6
|
||||
|
||||
* 按钮禁用条件:`!valid || loading`
|
||||
|
||||
* 支持 `Enter` 键触发提交
|
||||
|
||||
* 密码框添加“显示/隐藏”切换
|
||||
|
||||
1. 反馈与跳转
|
||||
|
||||
* 登录过程显示 `loading`(按钮文案切换为“登录中…”)
|
||||
|
||||
* 失败显示错误提示(红色文本);成功暂时跳转到 `/`(浏览器标签页界面)
|
||||
|
||||
* 留出接口占位:`login(account, password)`(后续接入真实 API)
|
||||
|
||||
1. 样式
|
||||
|
||||
* Tailwind 构建简约风格:卡片圆角阴影、输入框边框、聚焦效果、按钮主色(蓝色)
|
||||
|
||||
* 响应式:移动端输入宽度 100%,桌面端 `max-w-md` 保持卡片紧凑
|
||||
|
||||
1. 路由接入
|
||||
|
||||
* 在 `src/router/index.ts` 增加登录路由:`/login` → `views/login/index.vue`
|
||||
|
||||
* 暂不改默认首页;如需登录守卫,可后续将 `/login` 作为默认或在未认证时重定向到 `/login`
|
||||
|
||||
## 代码改动概览(不执行,仅说明)
|
||||
|
||||
* `src/views/login/index.vue`
|
||||
|
||||
* 模板:两个输入框(账号、密码)、显示/隐藏密码按钮、登录按钮、错误提示区域
|
||||
|
||||
* 逻辑:`form`、`errors`、`validate()`、`onSubmit()`、`loading`、`togglePassword()`;成功后:`router.push('/')`
|
||||
|
||||
* 样式:Tailwind 类名添加于容器/卡片/输入/按钮
|
||||
|
||||
* `src/router/index.ts`
|
||||
|
||||
* 添加:
|
||||
|
||||
```ts
|
||||
{ path: '/login', name: 'Login', component: () => import('@/views/login/index.vue') }
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
* 输入为空时按钮禁用;填入合法后启用
|
||||
|
||||
* 按 `Enter` 能提交;失败提示文案显示;成功跳转到首页
|
||||
|
||||
* 移动端/桌面端居中显示良好
|
||||
|
||||
## 后续可选
|
||||
|
||||
* 接入真实认证 API(axios),保存 token(Pinia/LocalStorage),路由守卫控制访问
|
||||
|
||||
* 增加“记住我”、忘记密码、国际化
|
||||
|
||||
请确认以上方案,确认后我将直接补全 `index.vue` 表单与交互,并注册 `/login` 路由。
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>智念科技 AI</title>
|
||||
<title>NIANXX</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -47,6 +47,7 @@ export class TabManager {
|
||||
this.win.removeBrowserView(view)
|
||||
this.activeId = null
|
||||
}
|
||||
// @ts-ignore
|
||||
view.webContents.destroy()
|
||||
this.views.delete(tabId)
|
||||
this.win.webContents.send('tab-closed', { tabId })
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Tray, Menu } from 'electron'
|
||||
import path from 'path'
|
||||
|
||||
const createTray = (app, win) => {
|
||||
const createTray = (app: Electron.App, win: Electron.BrowserWindow) => {
|
||||
let tray = new Tray(path.join(__dirname, '../public/favicon.ico'))
|
||||
|
||||
tray.setToolTip('示例平台') // 鼠标放在托盘图标上的提示信息
|
||||
@@ -4,17 +4,17 @@ import { ipcMain, BrowserWindow } from 'electron'
|
||||
ipcMain.on('window-min', (event) => {
|
||||
const webContent = event.sender
|
||||
const win = BrowserWindow.fromWebContents(webContent)
|
||||
win.minimize()
|
||||
win?.minimize()
|
||||
})
|
||||
|
||||
// 最大化
|
||||
ipcMain.on('window-max', (event) => {
|
||||
const webContent = event.sender
|
||||
const win = BrowserWindow.fromWebContents(webContent)
|
||||
if (win.isMaximized()) {
|
||||
if (win?.isMaximized()) {
|
||||
win.unmaximize()
|
||||
} else {
|
||||
win.maximize()
|
||||
win?.maximize()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -22,5 +22,5 @@ ipcMain.on('window-max', (event) => {
|
||||
ipcMain.on('window-close', (event) => {
|
||||
const webContent = event.sender
|
||||
const win = BrowserWindow.fromWebContents(webContent)
|
||||
win.close()
|
||||
win?.close()
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { TabManager } from './tab-manager'
|
||||
import { TabManager } from '../controller/tab-manager'
|
||||
|
||||
export const registerTabIpc = (tabs: TabManager) => {
|
||||
ipcMain.handle('tab:create', (_e, url?: string) => tabs.create(url))
|
||||
12
src/main.ts
12
src/main.ts
@@ -1,8 +1,9 @@
|
||||
import { app, BrowserWindow, ipcMain, shell } from "electron";
|
||||
import path from "node:path";
|
||||
import started from "electron-squirrel-startup";
|
||||
import { TabManager } from './main/tab-manager'
|
||||
import { registerTabIpc } from './main/ipc'
|
||||
import { TabManager } from './controller/tab-manager'
|
||||
import { registerTabIpc } from './ipc'
|
||||
import "./controller/window-size-controll";
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (started) {
|
||||
@@ -16,8 +17,8 @@ const createWindow = () => {
|
||||
width: 900,
|
||||
height: 670,
|
||||
autoHideMenuBar: true,
|
||||
frame: false,
|
||||
windowButtonVisibility: false,
|
||||
// frame: false,
|
||||
// windowButtonVisibility: false,
|
||||
resizable: true,
|
||||
maximizable: true,
|
||||
minimizable: true,
|
||||
@@ -47,6 +48,7 @@ const createWindow = () => {
|
||||
});
|
||||
|
||||
// and load the index.html of the app.
|
||||
// @ts-ignore
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
@@ -88,4 +90,4 @@ app.on("activate", () => {
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and import them here.
|
||||
import "./controller/changeWindowSize.js";
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
|
||||
const routes = [
|
||||
// {
|
||||
// path: "/",
|
||||
// name: "Login",
|
||||
// component: () => import("@/views/login/index.vue"),
|
||||
// },
|
||||
{
|
||||
path: "/",
|
||||
name: "Login",
|
||||
component: () => import("@/views/login/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/browser",
|
||||
name: "Browser",
|
||||
component: () => import("@/views/browser/BrowserLayout.vue"),
|
||||
},
|
||||
|
||||
@@ -8,25 +8,94 @@
|
||||
<!-- Logo和标题 -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">
|
||||
China Fellou Plus
|
||||
NIANXX
|
||||
</h1>
|
||||
<div class="text-sm text-gray-600">
|
||||
您的隐私对我们很重要,我们确保您的数据是安全和保密的
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login flex items-start justify-center"></div>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">账号</label>
|
||||
<input
|
||||
class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-indigo-400"
|
||||
type="text"
|
||||
v-model.trim="form.account"
|
||||
placeholder="请输入账号"
|
||||
@keyup.enter="onSubmit"
|
||||
/>
|
||||
<p v-if="errors.account" class="mt-1 text-xs text-red-500">{{ errors.account }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">密码</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
class="flex-1 px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-indigo-400"
|
||||
:type="showPwd ? 'text' : 'password'"
|
||||
v-model.trim="form.password"
|
||||
placeholder="请输入密码"
|
||||
@keyup.enter="onSubmit"
|
||||
/>
|
||||
<button
|
||||
class="px-3 py-2 text-sm border rounded bg-gray-100 hover:bg-gray-200"
|
||||
@click="togglePassword"
|
||||
>{{ showPwd ? '隐藏' : '显示' }}</button>
|
||||
</div>
|
||||
<p v-if="errors.password" class="mt-1 text-xs text-red-500">{{ errors.password }}</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-full py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700 disabled:bg-indigo-300"
|
||||
:disabled="!valid || loading"
|
||||
@click="onSubmit"
|
||||
>{{ loading ? '登录中…' : '登录' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ref, reactive } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
// 路由实例
|
||||
const router = useRouter();
|
||||
const form = reactive({ account: "", password: "" });
|
||||
const errors = reactive<{ account?: string; password?: string }>({});
|
||||
const loading = ref(false);
|
||||
const showPwd = ref(false);
|
||||
|
||||
const validate = () => {
|
||||
errors.account = undefined;
|
||||
errors.password = undefined;
|
||||
if (!form.account) errors.account = "请输入账号";
|
||||
else if (form.account.length < 4 || form.account.length > 32) errors.account = "账号长度需在 4-32 之间";
|
||||
if (!form.password) errors.password = "请输入密码";
|
||||
else if (form.password.length < 6) errors.password = "密码长度不少于 6 位";
|
||||
return !errors.account && !errors.password;
|
||||
};
|
||||
|
||||
const valid = ref(false);
|
||||
|
||||
const recalc = () => {
|
||||
valid.value = validate();
|
||||
};
|
||||
|
||||
const togglePassword = () => {
|
||||
showPwd.value = !showPwd.value;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
recalc();
|
||||
if (!valid.value || loading.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
await new Promise((r) => setTimeout(r, 600));
|
||||
router.push("/");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped></style>
|
||||
|
||||
Reference in New Issue
Block a user