feat: 初始化项目

This commit is contained in:
DEV_DSW
2025-09-22 17:05:21 +08:00
parent 1347e31f83
commit 329fc3eb0e
21 changed files with 9462 additions and 1 deletions

11
.env.development Normal file
View File

@@ -0,0 +1,11 @@
# 环境
VITE_ENV = development
# 接口地址开关控制器
VITE_DEV_URL = 0
# 项目h5域名
VITE_BASE_URL_API = https://h5.gogpay.cn/h5-test/app/love-calculator-frontend
# 应用ID
VITE_APP_ID = ZN-AI

11
.env.production Normal file
View File

@@ -0,0 +1,11 @@
# 环境
VITE_ENV = production
# 接口地址开关控制器
VITE_DEV_URL = 0
# 项目h5域名
VITE_BASE_URL_API = https://h5.gogpay.cn/h5-test/app/love-calculator-frontend
# 应用ID
VITE_APP_ID = ZN-AI

11
.env.staging Normal file
View File

@@ -0,0 +1,11 @@
# 环境
VITE_ENV = staging
# 接口地址开关控制器
VITE_DEV_URL = 0
# 项目h5域名
VITE_BASE_URL_API = https://h5.gogpay.cn/h5-test/app/love-calculator-frontend
# 应用ID
VITE_APP_ID = ZN-AI

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

112
LOGIN-README.md Normal file
View File

@@ -0,0 +1,112 @@
**产品需求文档:综合登录系统**
**1. 文档概述**
- **产品名称:** 统一认证与登录系统
- **功能模块:** 登录页 (Login Page)
- **文档目的:** 定义登录功能的详细需求,涵盖账号密码登录、验证码校验、二维码登录及相关的用户体验细节,确保开发实现准确无误。
- **目标用户:** 所有需要访问系统权限的注册用户(包括新用户、老用户、遗忘密码用户)。
- **核心目标:**
1. **安全可靠:** 保障用户账号安全,防御机器攻击。
2. **便捷高效:** 提供多种登录方式,减少用户操作成本。
3. **清晰引导:** 界面文案清晰,对各类情况给予明确反馈和引导。
**2. 功能需求详述**
**2.1. 账号密码登录(主流程)**
- **输入字段:**
- **用户名/邮箱/手机号:**
- 类型:单行输入框。
- 提示文案(Placeholder): “请输入用户名/邮箱/手机号”。
- 规则:支持三者之一即可,后端需做兼容判断。前端可做初步格式校验(如邮箱含'@',手机号为 11 位数字),但最终有效性由后端验证。
- **密码:**
- 类型:密码输入框(内容默认隐藏)。
- 提示文案:“请输入密码”。
- 功能:右侧需提供“眼睛”图标,点击可切换明文/密文显示。
- **图形验证码:**
- 显示逻辑非首次输入时触发。为了提高体验可在用户密码输入框失焦blur后、或连续输错 1 次密码后,再出现验证码输入框。
- 组件:一个输入框 + 一个实时图形验证码图片。
- 图片:需提供“刷新”图标,点击可无刷新切换新的验证码。图片应清晰可辨,但具备一定的抗机器识别能力。
- 提示文案:“请输入验证码”。
- **辅助功能:**
- **记住我:**
- 组件复选框Checkbox
- 功能:勾选后,下次访问登录页时自动填充用户名(切勿记住密码)。
- 默认状态:不勾选。
- **忘记密码:**
- 组件:文字链接。
- 位置:位于密码框右侧或下方。
- 功能:点击后跳转至密码找回流程页。
- **主要操作按钮:**
- **“登录”按钮:**
- 状态:
- 默认态:可点击。
- 加载态:用户点击后,按钮变为禁用状态并显示“登录中...”加载动画,防止重复提交。
- 功能:触发登录逻辑。
**2.2. 二维码登录(便捷流程)**
- **切换入口:**
- 在登录框顶部或侧边提供 Tab 页或文字链接,如“账号密码登录” | “二维码登录”,用于切换两种登录方式。
- **二维码显示区域:**
- 初始状态页面应动态生成一个唯一的二维码QR Code并配有文案提示“请使用【APP 名称】扫描二维码登录”。
- 自动刷新:该二维码应有时效性(例如 2 分钟。倒计时在二维码旁可视化显示01:45。过期后自动刷新生成新的二维码。
- 手动刷新:提供“刷新”按钮,用户可手动点击更换二维码。
- **登录状态轮询:**
- 一旦二维码生成前端即开始向后台轮询Polling其状态如每 2 秒一次)。
- 状态反馈:
- 待扫描:显示初始状态。
- 已扫描,待确认:二维码仍显示,下方文案变为“请在手机上确认登录”。
- 登录成功:轮询停止,前端跳转至登录后页面(或首页)。
- 过期/失败:二维码区域变灰,显示“二维码已失效”,并自动刷新生成新的二维码。
**2.3. 异常与交互流程**
- **输入校验(前端 + 后端):**
- 字段为空点击登录后对未填写的必填字段进行红色高亮提示并显示文案“XXX 不能为空”。
- 格式错误在输入框失焦blur时即可进行初步校验如邮箱格式错误即时提示“邮箱格式不正确”。
- 验证码错误:提示“验证码错误”,并自动刷新验证码图片。
- 账号或密码错误:切勿明确提示是“账号错误”还是“密码错误”,统一提示:“用户名或密码错误”,以防黑客枚举用户名。同时,刷新验证码。
- **安全限制:**
- 连续输错密码 N 次(如 5 次)后,除图形验证码外,应触发更严格的验证(如滑块验证、短信验证码)或直接锁定账号一段时间(如 15 分钟),并明确提示用户:“密码错误次数过多,请 15 分钟后再试”。
- **加载与反馈:**
- 任何网络请求都必须有加载状态(如按钮 Loading防止用户重复操作。
- 所有错误提示都应清晰、友好,用红色字体在表单项附近或页面顶部固定区域显示。
**3. 用户体验UX重点**
1. **默认焦点:** 页面加载后,自动将光标聚焦到“用户名”输入框。
2. **键盘操作:** 在最后一项输入框(验证码或密码)按`Enter`键应触发登录操作。
3. **密码可见性:** “眼睛”图标是行业标准,显著降低用户输错密码的概率。
4. **智能验证码:** 不要一开始就展示验证码,而是在系统怀疑有风险(如输错一次)时再出现,优化善良用户的体验。
5. **状态反馈:** 二维码登录的每种状态(待扫描、已确认、已过期)都必须有明确的视觉和文案反馈,让用户知其所以然。
6. **链路闭环:** “忘记密码”和“注册”入口必须清晰可见,为登录失败的用户提供清晰的出路。
**4. 非功能性需求**
1. **性能:** 页面加载速度快,登录接口响应时间应小于 500ms。
2. **安全性:**
- 密码传输必须使用 HTTPS 并加密(如 SHA256 加盐)。
- 防暴力破解:后端需实施限流策略。
- Token 管理:登录成功后颁发的 Token 需有有效期并支持刷新机制。
3. **兼容性:** 支持主流浏览器Chrome, Firefox, Safari, Edge及移动端浏览器。
**5. 输出物建议**
1. **PRD 文档:** 即本文档,用于详细阐述逻辑和规则。
2. **线框图Wireframe** 绘制登录页的布局,标注各元素和交互点。
3. **高保真原型High-Fidelity Mockup** 由 UI 设计师输出,明确视觉样式、颜色、字体等。
4. **交互原型Interactive Prototype** 使用 Figma、Axure 等工具制作可点击的原型,演示登录成功、失败、切换等所有流程。

View File

@@ -1,3 +1,6 @@
# zn-ai # zn-ai
员工PC端应用 员工 PC 端应用
技术栈Vue3 + Electron + ElementPlus + TypeScript + Vite
包管理工具Yarn

31
controller/tray.js Normal file
View File

@@ -0,0 +1,31 @@
// 创建系统托盘
const { Tray, Menu } = require("electron");
const path = require("path");
function createTray(app, win) {
let tray = new Tray(path.join(__dirname, "../public/favicon.ico"));
tray.setToolTip("示例平台"); // 鼠标放在托盘图标上的提示信息
tray.on("click", (e) => {
if (e.shiftKey) {
app.quit();
} else {
win.show();
}
});
tray.setContextMenu(
Menu.buildFromTemplate([
{
label: "退出",
click: () => {
// 先把用户的登录状态和用户的登录信息给清楚掉,再退出
app.quit();
},
},
])
);
}
module.exports = createTray;

46
electron/main.js Normal file
View File

@@ -0,0 +1,46 @@
const { app, ipcMain, BrowserWindow } = require("electron");
const { join } = require("path");
const createTray = require("../controller/tray");
// 映射页面
const env = app.isPackaged ? "production" : "development";
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
frame: false, // 不要自带的窗口
webPreferences: {
preload: join(__dirname, "./preload/index.js"),
},
});
if (env === "development") {
win.loadURL("http://localhost:5173");
// 打开开发工具 { mode: "detach" }
// win.webContents.openDevTools();
} else {
win.loadFile("../dist/index.html");
}
// 系统托盘
createTray(app, win);
};
ipcMain.handle("sent-event", (event, params) => {
console.log(params);
return "1111";
});
app.whenReady().then(() => {
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});

17
index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' 'unsafe-inline';"
/>
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8939
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

58
package.json Normal file
View File

@@ -0,0 +1,58 @@
{
"name": "ZN-AI",
"private": true,
"version": "1.0.0",
"type": "commonjs",
"main": "electron/main.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"electron": "nodemon --exec electron . --watch ./ --ext .js,.html,.css,.vue",
"electron:dev": "npm-run-all --parallel dev electron",
"electron:build": "vite build && electron-builder"
},
"build": {
"productName": "ZN-AI",
"appId": "ZN-AI",
"asar": true,
"copyright": "Copyright © 2022 zhinian",
"directories": {
"output": "dist_electron/${version}"
},
"files": [
"dist",
"electron"
],
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"mac": {
"category": "your.app.category.type"
},
"win": {
"icon": "./electron/log.ico",
"target": [
{
"target": "nsis",
"arch": [
"ia32"
]
}
]
},
"linux": {}
},
"dependencies": {
"vue": "^3.5.21"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"electron": "^38.1.2",
"electron-builder": "^26.0.12",
"nodemon": "^3.1.10",
"npm-run-all": "^4.1.5",
"vite": "^7.1.6"
}
}

11
preload/index.js Normal file
View File

@@ -0,0 +1,11 @@
const { contextBridge, ipcRenderer } = require("electron");
const handleSend = async (vue_params) => {
let fallback = await ipcRenderer.invoke("sent-event", vue_params);
return fallback;
};
contextBridge.exposeInMainWorld("myApi", {
handleSend: handleSend,
// 能暴露的不仅仅是函数,我们还可以暴露变量
});

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

38
src/App.vue Normal file
View File

@@ -0,0 +1,38 @@
<script setup>
import { onMounted } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
onMounted(async () => {
let res = await window.myApi.handleSend('liaoruiruirurirui')
console.log(res)
})
</script>
<template>
<div>
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Vite + Vue" />
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

1
src/assets/vue.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,43 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

5
src/main.js Normal file
View File

@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

79
src/style.css Normal file
View File

@@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

17
vite.config.js Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from "vite";
import { resolve } from "path";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
base: "./",
manifest: true, //配置后才能让编译后的vue路径被正确识别
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
},
},
optimizeDeps: {
exclude: ["electron"], // 告诉 Vite 排除预构建 electron不然会出现 __diranme is not defined
},
});