feat: 更换应用框架为ElectronFofge

This commit is contained in:
duanshuwen
2025-10-12 14:12:29 +08:00
parent 02b85f3251
commit ebac04b786
67 changed files with 7935 additions and 6104 deletions

18
src/App.vue Normal file
View File

@@ -0,0 +1,18 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script setup lang="ts">
import { useCounterStore } from "@stores/counter";
// 使用 Pinia store
const counterStore = useCounterStore();
</script>
<style scoped>
#app {
@apply min-h-screen bg-gray-100;
}
</style>

View File

@@ -1,9 +1,3 @@
<script setup lang="ts">
import { reactive } from 'vue'
const versions = reactive({ ...window.electron.process.versions })
</script>
<template>
<ul class="versions">
<li class="electron-version">Electron v{{ versions.electron }}</li>
@@ -11,3 +5,9 @@ const versions = reactive({ ...window.electron.process.versions })
<li class="node-version">Node v{{ versions.node }}</li>
</ul>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const versions = reactive({ ...window.electron.process.versions })
</script>

2
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
declare module "@stores/counter";
declare module "@utils/request";

12
src/index.css Normal file
View File

@@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
sans-serif;
margin: auto;
max-width: 38rem;
padding: 2rem;
}

60
src/main.ts Normal file
View File

@@ -0,0 +1,60 @@
import { app, BrowserWindow } from "electron";
import path from "node:path";
import started from "electron-squirrel-startup";
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) {
app.quit();
}
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
autoHideMenuBar: true,
resizable: false, // 禁止拖拽放大缩小
maximizable: false, // 禁止最大化
minimizable: true, // 允许最小化
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
});
// and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
mainWindow.loadFile(
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
);
}
// Open the DevTools.
// mainWindow.webContents.openDevTools();
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on("ready", createWindow);
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// 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.

View File

@@ -1,88 +0,0 @@
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
let mainWindow: BrowserWindow | null = null
function createWindow(): void {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
resizable: false, // 禁止拖拽放大缩小
maximizable: false, // 禁止最大化
minimizable: true, // 允许最小化
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow.on('ready-to-show', () => {
mainWindow?.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
// IPC test
ipcMain.on('ping', () => console.log('pong'))
// 监听登录成功事件,恢复窗口拖拽功能
ipcMain.on('login-success', () => {
if (mainWindow) {
mainWindow.setResizable(true)
mainWindow.setMaximizable(true)
console.log('窗口拖拽功能已恢复')
}
})
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// 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 require them here.

0
src/preload.ts Normal file
View File

View File

@@ -1,8 +0,0 @@
import { ElectronAPI } from '@electron-toolkit/preload'
declare global {
interface Window {
electron: ElectronAPI
api: unknown
}
}

View File

@@ -1,39 +0,0 @@
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
const api = {}
const handleSend = async (vue_params: any) => {
let fallback = await ipcRenderer.invoke('sent-event', vue_params)
return fallback
}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
contextBridge.exposeInMainWorld('myApi', {
handleSend: handleSend,
// 能暴露的不仅仅是函数,我们还可以暴露变量
// 最小化
windowMin: () => ipcRenderer.send('window-min'),
// 最大化
windowMax: () => ipcRenderer.send('window-max'),
// 关闭窗口
windowClose: () => ipcRenderer.send('window-close')
})
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
}

45
src/renderer.ts Normal file
View File

@@ -0,0 +1,45 @@
/**
* This file will automatically be loaded by vite and run in the "renderer" context.
* To learn more about the differences between the "main" and the "renderer" context in
* Electron, visit:
*
* https://electronjs.org/docs/tutorial/process-model
*
* By default, Node.js integration in this file is disabled. When enabling Node.js integration
* in a renderer process, please be aware of potential security implications. You can read
* more about security risks here:
*
* https://electronjs.org/docs/tutorial/security
*
* To enable Node.js integration in this file, open up `main.ts` and enable the `nodeIntegration`
* flag:
*
* ```
* // Create the browser window.
* mainWindow = new BrowserWindow({
* width: 800,
* height: 600,
* webPreferences: {
* nodeIntegration: true
* }
* });
* ```
*/
import "./index.css";
import { createApp } from "vue";
import { createPinia } from "pinia";
import router from "./router";
import App from "./App.vue";
// 创建 Vue 应用实例
const app = createApp(App);
// 使用 Pinia 状态管理
app.use(createPinia());
// 使用 Vue Router
app.use(router);
// 挂载应用到 DOM
app.mount("#app");

View File

@@ -1,17 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,35 +0,0 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { useMainStore } from "@store";
const store = useMainStore();
onMounted(() => {
console.log("Vue应用已挂载路由和Pinia已集成", store.count);
});
// import Versions from './components/Versions.vue'
// const ipcHandle = (): void => window.electron.ipcRenderer.send('ping')
</script>
<template>
<!-- <img alt="logo" class="logo" src="./assets/electron.svg" />
<div class="creator">Powered by electron-vite</div>
<div class="text">
Build an Electron app with
<span class="vue">Vue</span>
and
<span class="ts">TypeScript</span>
</div>
<p class="tip">Please try pressing <code>F12</code> to open the devTool</p>
<div class="actions">
<div class="action">
<a href="https://electron-vite.org/" target="_blank" rel="noreferrer">Documentation</a>
</div>
<div class="action">
<a target="_blank" rel="noreferrer" @click="ipcHandle">Send IPC</a>
</div>
</div>
<Versions /> -->
<router-view />
</template>

View File

@@ -1,10 +0,0 @@
<svg viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="64" cy="64" r="64" fill="#2F3242"/>
<ellipse cx="63.9835" cy="23.2036" rx="4.48794" ry="4.495" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path d="M51.3954 39.5028C52.3733 39.6812 53.3108 39.033 53.4892 38.055C53.6676 37.0771 53.0194 36.1396 52.0414 35.9612L51.3954 39.5028ZM28.6153 43.5751L30.1748 44.4741L30.1748 44.4741L28.6153 43.5751ZM28.9393 60.9358C29.4332 61.7985 30.5329 62.0976 31.3957 61.6037C32.2585 61.1098 32.5575 60.0101 32.0636 59.1473L28.9393 60.9358ZM37.6935 66.7457C37.025 66.01 35.8866 65.9554 35.1508 66.6239C34.415 67.2924 34.3605 68.4308 35.029 69.1666L37.6935 66.7457ZM53.7489 81.7014L52.8478 83.2597L53.7489 81.7014ZM96.9206 89.515C97.7416 88.9544 97.9526 87.8344 97.3919 87.0135C96.8313 86.1925 95.7113 85.9815 94.8904 86.5422L96.9206 89.515ZM52.0414 35.9612C46.4712 34.9451 41.2848 34.8966 36.9738 35.9376C32.6548 36.9806 29.0841 39.1576 27.0559 42.6762L30.1748 44.4741C31.5693 42.0549 34.1448 40.3243 37.8188 39.4371C41.5009 38.5479 46.1547 38.5468 51.3954 39.5028L52.0414 35.9612ZM27.0559 42.6762C24.043 47.9029 25.2781 54.5399 28.9393 60.9358L32.0636 59.1473C28.6579 53.1977 28.1088 48.0581 30.1748 44.4741L27.0559 42.6762ZM35.029 69.1666C39.6385 74.24 45.7158 79.1355 52.8478 83.2597L54.6499 80.1432C47.8081 76.1868 42.0298 71.5185 37.6935 66.7457L35.029 69.1666ZM52.8478 83.2597C61.344 88.1726 70.0465 91.2445 77.7351 92.3608C85.359 93.4677 92.2744 92.6881 96.9206 89.515L94.8904 86.5422C91.3255 88.9767 85.4902 89.849 78.2524 88.7982C71.0793 87.7567 62.809 84.8612 54.6499 80.1432L52.8478 83.2597ZM105.359 84.9077C105.359 81.4337 102.546 78.6127 99.071 78.6127V82.2127C100.553 82.2127 101.759 83.4166 101.759 84.9077H105.359ZM99.071 78.6127C95.5956 78.6127 92.7831 81.4337 92.7831 84.9077H96.3831C96.3831 83.4166 97.5892 82.2127 99.071 82.2127V78.6127ZM92.7831 84.9077C92.7831 88.3817 95.5956 91.2027 99.071 91.2027V87.6027C97.5892 87.6027 96.3831 86.3988 96.3831 84.9077H92.7831ZM99.071 91.2027C102.546 91.2027 105.359 88.3817 105.359 84.9077H101.759C101.759 86.3988 100.553 87.6027 99.071 87.6027V91.2027Z" fill="#A2ECFB"/>
<path d="M91.4873 65.382C90.8456 66.1412 90.9409 67.2769 91.7002 67.9186C92.4594 68.5603 93.5951 68.465 94.2368 67.7058L91.4873 65.382ZM99.3169 43.6354L97.7574 44.5344L99.3169 43.6354ZM84.507 35.2412C83.513 35.2282 82.6967 36.0236 82.6838 37.0176C82.6708 38.0116 83.4661 38.8279 84.4602 38.8409L84.507 35.2412ZM74.9407 39.8801C75.9127 39.6716 76.5315 38.7145 76.323 37.7425C76.1144 36.7706 75.1573 36.1517 74.1854 36.3603L74.9407 39.8801ZM53.7836 46.3728L54.6847 47.931L53.7836 46.3728ZM25.5491 80.9047C25.6932 81.8883 26.6074 82.5688 27.5911 82.4247C28.5747 82.2806 29.2552 81.3664 29.1111 80.3828L25.5491 80.9047ZM94.2368 67.7058C97.8838 63.3907 100.505 58.927 101.752 54.678C103.001 50.4213 102.9 46.2472 100.876 42.7365L97.7574 44.5344C99.1494 46.9491 99.3603 50.0419 98.2974 53.6644C97.2323 57.2945 94.9184 61.3223 91.4873 65.382L94.2368 67.7058ZM100.876 42.7365C97.9119 37.5938 91.7082 35.335 84.507 35.2412L84.4602 38.8409C91.1328 38.9278 95.7262 41.0106 97.7574 44.5344L100.876 42.7365ZM74.1854 36.3603C67.4362 37.8086 60.0878 40.648 52.8826 44.8146L54.6847 47.931C61.5972 43.9338 68.5948 41.2419 74.9407 39.8801L74.1854 36.3603ZM52.8826 44.8146C44.1366 49.872 36.9669 56.0954 32.1491 62.3927C27.3774 68.63 24.7148 75.2115 25.5491 80.9047L29.1111 80.3828C28.4839 76.1026 30.4747 70.5062 35.0084 64.5802C39.496 58.7143 46.2839 52.7889 54.6847 47.931L52.8826 44.8146Z" fill="#A2ECFB"/>
<path d="M49.0825 87.2295C48.7478 86.2934 47.7176 85.8059 46.7816 86.1406C45.8455 86.4753 45.358 87.5055 45.6927 88.4416L49.0825 87.2295ZM78.5635 96.4256C79.075 95.5732 78.7988 94.4675 77.9464 93.9559C77.0941 93.4443 75.9884 93.7205 75.4768 94.5729L78.5635 96.4256ZM79.5703 85.1795C79.2738 86.1284 79.8027 87.1379 80.7516 87.4344C81.7004 87.7308 82.71 87.2019 83.0064 86.2531L79.5703 85.1795ZM84.3832 64.0673H82.5832H84.3832ZM69.156 22.5301C68.2477 22.1261 67.1838 22.535 66.7799 23.4433C66.3759 24.3517 66.7848 25.4155 67.6931 25.8194L69.156 22.5301ZM45.6927 88.4416C47.5994 93.7741 50.1496 98.2905 53.2032 101.505C56.2623 104.724 59.9279 106.731 63.9835 106.731V103.131C61.1984 103.131 58.4165 101.765 55.8131 99.0249C53.2042 96.279 50.8768 92.2477 49.0825 87.2295L45.6927 88.4416ZM63.9835 106.731C69.8694 106.731 74.8921 102.542 78.5635 96.4256L75.4768 94.5729C72.0781 100.235 68.0122 103.131 63.9835 103.131V106.731ZM83.0064 86.2531C85.0269 79.7864 86.1832 72.1831 86.1832 64.0673H82.5832C82.5832 71.8536 81.4723 79.0919 79.5703 85.1795L83.0064 86.2531ZM86.1832 64.0673C86.1832 54.1144 84.4439 44.922 81.4961 37.6502C78.5748 30.4436 74.3436 24.8371 69.156 22.5301L67.6931 25.8194C71.6364 27.5731 75.3846 32.1564 78.1598 39.0026C80.9086 45.7836 82.5832 54.507 82.5832 64.0673H86.1832Z" fill="#A2ECFB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M103.559 84.9077C103.559 82.4252 101.55 80.4127 99.071 80.4127C96.5924 80.4127 94.5831 82.4252 94.5831 84.9077C94.5831 87.3902 96.5924 89.4027 99.071 89.4027C101.55 89.4027 103.559 87.3902 103.559 84.9077V84.9077Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.8143 89.4027C31.2929 89.4027 33.3023 87.3902 33.3023 84.9077C33.3023 82.4252 31.2929 80.4127 28.8143 80.4127C26.3357 80.4127 24.3264 82.4252 24.3264 84.9077C24.3264 87.3902 26.3357 89.4027 28.8143 89.4027V89.4027V89.4027Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.8501 68.0857C62.6341 68.5652 60.451 67.1547 59.9713 64.9353C59.4934 62.7159 60.9007 60.5293 63.1167 60.0489C65.3326 59.5693 67.5157 60.9798 67.9954 63.1992C68.4742 65.4186 67.066 67.6052 64.8501 68.0857Z" fill="#A2ECFB"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -1,182 +0,0 @@
body {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background-color: #fff;
/* background-image: url('./wavy-lines.svg');
background-size: cover; */
user-select: none;
}
code {
font-weight: 600;
padding: 3px 5px;
border-radius: 2px;
background-color: var(--color-background-mute);
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 85%;
}
#app {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-bottom: 80px;
}
.logo {
margin-bottom: 20px;
-webkit-user-drag: none;
height: 128px;
width: 128px;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 1.2em #6988e6aa);
}
.creator {
font-size: 14px;
line-height: 16px;
color: var(--ev-c-text-2);
font-weight: 600;
margin-bottom: 10px;
}
.text {
font-size: 28px;
color: var(--ev-c-text-1);
font-weight: 700;
line-height: 32px;
text-align: center;
margin: 0 10px;
padding: 16px 0;
}
.tip {
font-size: 16px;
line-height: 24px;
color: var(--ev-c-text-2);
font-weight: 600;
}
.vue {
background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
}
.ts {
background: -webkit-linear-gradient(315deg, #3178c6 45%, #f0dc4e);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
}
.actions {
display: flex;
padding-top: 32px;
margin: -6px;
flex-wrap: wrap;
justify-content: flex-start;
}
.action {
flex-shrink: 0;
padding: 6px;
}
.action a {
cursor: pointer;
text-decoration: none;
display: inline-block;
border: 1px solid transparent;
text-align: center;
font-weight: 600;
white-space: nowrap;
border-radius: 20px;
padding: 0 20px;
line-height: 38px;
font-size: 14px;
border-color: var(--ev-button-alt-border);
color: var(--ev-button-alt-text);
background-color: var(--ev-button-alt-bg);
}
.action a:hover {
border-color: var(--ev-button-alt-hover-border);
color: var(--ev-button-alt-hover-text);
background-color: var(--ev-button-alt-hover-bg);
}
.versions {
position: absolute;
bottom: 30px;
margin: 0 auto;
padding: 15px 0;
font-family: 'Menlo', 'Lucida Console', monospace;
display: inline-flex;
overflow: hidden;
align-items: center;
border-radius: 22px;
background-color: #202127;
backdrop-filter: blur(24px);
}
.versions li {
display: block;
float: left;
border-right: 1px solid var(--ev-c-gray-1);
padding: 0 20px;
font-size: 14px;
line-height: 14px;
opacity: 0.8;
&:last-child {
border: none;
}
}
@media (max-width: 720px) {
.text {
font-size: 20px;
}
}
@media (max-width: 620px) {
.versions {
display: none;
}
}
@media (max-width: 350px) {
.tip,
.actions {
display: none;
}
}
.fz-14 {
font-size: 14px;
}
.fz-22 {
font-size: 22px;
}
.mb-20 {
margin-bottom: 20px;
}

View File

@@ -1,4 +0,0 @@
/* /src/asstes/css/tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,25 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1422 800" opacity="0.3">
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="oooscillate-grad">
<stop stop-color="hsl(206, 75%, 49%)" stop-opacity="1" offset="0%"></stop>
<stop stop-color="hsl(331, 90%, 56%)" stop-opacity="1" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke-width="1" stroke="url(#oooscillate-grad)" fill="none" stroke-linecap="round">
<path d="M 0 448 Q 355.5 -100 711 400 Q 1066.5 900 1422 448" opacity="0.05"></path>
<path d="M 0 420 Q 355.5 -100 711 400 Q 1066.5 900 1422 420" opacity="0.11"></path>
<path d="M 0 392 Q 355.5 -100 711 400 Q 1066.5 900 1422 392" opacity="0.18"></path>
<path d="M 0 364 Q 355.5 -100 711 400 Q 1066.5 900 1422 364" opacity="0.24"></path>
<path d="M 0 336 Q 355.5 -100 711 400 Q 1066.5 900 1422 336" opacity="0.30"></path>
<path d="M 0 308 Q 355.5 -100 711 400 Q 1066.5 900 1422 308" opacity="0.37"></path>
<path d="M 0 280 Q 355.5 -100 711 400 Q 1066.5 900 1422 280" opacity="0.43"></path>
<path d="M 0 252 Q 355.5 -100 711 400 Q 1066.5 900 1422 252" opacity="0.49"></path>
<path d="M 0 224 Q 355.5 -100 711 400 Q 1066.5 900 1422 224" opacity="0.56"></path>
<path d="M 0 196 Q 355.5 -100 711 400 Q 1066.5 900 1422 196" opacity="0.62"></path>
<path d="M 0 168 Q 355.5 -100 711 400 Q 1066.5 900 1422 168" opacity="0.68"></path>
<path d="M 0 140 Q 355.5 -100 711 400 Q 1066.5 900 1422 140" opacity="0.75"></path>
<path d="M 0 112 Q 355.5 -100 711 400 Q 1066.5 900 1422 112" opacity="0.81"></path>
<path d="M 0 84 Q 355.5 -100 711 400 Q 1066.5 900 1422 84" opacity="0.87"></path>
<path d="M 0 56 Q 355.5 -100 711 400 Q 1066.5 900 1422 56" opacity="0.94"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,15 +0,0 @@
import './assets/main.css'
import './assets/tailwind.css'
import 'element-plus/dist/index.css'
import ElementPlus from 'element-plus'
import router from './router'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.use(router)
app.use(createPinia())
app.mount('#app')

View File

@@ -1,26 +0,0 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'login',
component: () => import('../views/login/index.vue')
},
{
path: '/home',
name: 'home',
component: () => import('../views/home/index.vue')
},
{
path: '/about',
name: 'about',
component: () => import('../views/about/index.vue')
}
]
const router = createRouter({
history: createWebHashHistory(), // hash模式
routes // 路由配置规则数组
})
export default router

View File

@@ -1,13 +0,0 @@
import { defineStore } from 'pinia'
export const useMainStore = defineStore('main', {
state: () => ({
count: 0
}),
getters: {},
actions: {
increment() {
this.count++
}
}
})

View File

@@ -1,7 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style scoped></style>

View File

@@ -1,107 +0,0 @@
<template>
<main class="h-screen flex flex-col">
<!-- 标题栏 -->
<div class="header bg-gray-300 h-10 w-full flex justify-end items-center" style="-webkit-app-region: drag;">
<span @click="toMin"
class="mx-4 px-2 py-1 border border-gray-800 cursor-pointer hover:bg-gray-400 transition-colors"
style="-webkit-app-region: no-drag;">最小化</span>
<span @click="toBig"
class="mx-4 px-2 py-1 border border-gray-800 cursor-pointer hover:bg-gray-400 transition-colors"
style="-webkit-app-region: no-drag;">最大化</span>
<span @click="toClose"
class="mx-4 px-2 py-1 border border-gray-800 cursor-pointer hover:bg-red-400 transition-colors"
style="-webkit-app-region: no-drag;">关闭</span>
</div>
<!-- 主要内容区域 -->
<div class="flex-1 flex flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-8">
<h1 class="text-4xl font-bold text-gray-800 mb-8">Tailwind CSS 示例</h1>
<!-- 卡片示例 -->
<div class="max-w-md w-full bg-white rounded-lg shadow-lg p-6 mb-6">
<h2 class="text-2xl font-semibold text-gray-700 mb-4">卡片组件</h2>
<p class="text-gray-600 mb-4">这是一个使用 Tailwind CSS 样式的卡片组件示例</p>
<button class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded transition-colors">
点击按钮
</button>
</div>
<!-- 网格布局示例 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 w-full max-w-4xl">
<div class="bg-red-100 p-4 rounded-lg text-center">
<div class="text-red-600 text-2xl mb-2">🎨</div>
<h3 class="font-semibold text-red-800">设计</h3>
<p class="text-red-600 text-sm">美观的界面设计</p>
</div>
<div class="bg-green-100 p-4 rounded-lg text-center">
<div class="text-green-600 text-2xl mb-2"></div>
<h3 class="font-semibold text-green-800">性能</h3>
<p class="text-green-600 text-sm">快速响应体验</p>
</div>
<div class="bg-purple-100 p-4 rounded-lg text-center">
<div class="text-purple-600 text-2xl mb-2">🔧</div>
<h3 class="font-semibold text-purple-800">功能</h3>
<p class="text-purple-600 text-sm">丰富的功能特性</p>
</div>
</div>
<!-- 响应式按钮组 -->
<div class="flex flex-wrap gap-2 mt-6">
<button
class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors">
主要按钮
</button>
<button
class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-md text-sm font-medium transition-colors">
次要按钮
</button>
<button
class="border border-indigo-500 text-indigo-500 hover:bg-indigo-50 px-4 py-2 rounded-md text-sm font-medium transition-colors">
边框按钮
</button>
</div>
</div>
</main>
</template>
<script setup>
import { onMounted } from "vue";
// 检查是否在Electron环境中
const isElectron = typeof window !== 'undefined' && window.myApi;
onMounted(async () => {
if (isElectron) {
let res = await window.myApi.handleSend("liaoruiruirurirui");
console.log(res);
} else {
console.log('在浏览器环境中运行Electron API不可用');
}
});
const toMin = () => {
if (isElectron) {
window.myApi.windowMin();
} else {
console.log('最小化功能仅在Electron环境中可用');
}
};
const toBig = () => {
if (isElectron) {
window.myApi.windowMax();
} else {
console.log('最大化功能仅在Electron环境中可用');
}
};
const toClose = () => {
if (isElectron) {
window.myApi.windowClose();
} else {
console.log('关闭功能仅在Electron环境中可用');
}
};
</script>
<style scoped></style>

View File

@@ -1,112 +0,0 @@
**产品需求文档:综合登录系统**
**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,284 +0,0 @@
<template>
<div
class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4"
>
<div class="w-full max-w-md">
<!-- 登录卡片 -->
<div class="bg-white rounded-lg shadow-xl p-8">
<!-- Logo和标题 -->
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-gray-900 mb-2">China Fellou Plus</h1>
<div class="text-sm text-gray-600">
您的隐私对我们很重要我们确保您的数据是安全和保密的
</div>
</div>
<div class="login flex items-start justify-center">
<!-- 二维码登录 -->
<div class="left text-center">
<div class="bg-gray-100 rounded-lg p-8 mb-4">
<div class="fz-22 mb-20">手机扫码登录</div>
<div class="w-48 h-48 bg-white rounded-lg mx-auto flex items-center justify-center">
<div class="qr text-center mt-20">
<VueQrcode value="https://www.1stg.me" :width="195" :margin="3" />
<!-- 二维码失效 -->
<div class="qrcode-error">
<p>二维码已失效</p>
<el-button type="info" round @click="refreshQrCode">刷新</el-button>
</div>
</div>
</div>
</div>
<p class="fz-14">打开<el-text type="success">微信</el-text>扫一扫登录</p>
</div>
<!-- 账号密码登录 -->
<div class="right text-center">
<div class="fz-22 mb-20">密码登录</div>
<el-form
class="mt-20"
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
@keyup.enter="handleLogin"
>
<!-- 用户名/邮箱/手机号 -->
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入手机号"
size="large"
clearable
>
<template #prefix>
<el-icon>
<User />
</el-icon>
</template>
</el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
size="large"
show-password
clearable
>
<template #prefix>
<el-icon>
<Lock />
</el-icon>
</template>
</el-input>
</el-form-item>
<!-- 图形验证码 -->
<el-form-item v-if="showCaptcha" prop="captcha">
<div class="flex space-x-2">
<el-input
v-model="loginForm.captcha"
placeholder="请输入验证码"
size="large"
clearable
>
<template #prefix>
<el-icon>
<Key />
</el-icon>
</template>
</el-input>
<div
class="w-24 h-10 bg-gray-200 rounded flex items-center justify-center cursor-pointer"
@click="refreshCaptcha"
>
<span class="text-sm font-mono">{{ captchaCode }}</span>
</div>
</div>
</el-form-item>
<!-- 记住我和忘记密码 -->
<div class="flex items-center justify-between">
<el-checkbox v-model="loginForm.remember"> 记住我 </el-checkbox>
<el-button type="text" size="small" @click="handleForgotPassword">
忘记密码
</el-button>
</div>
<!-- 登录按钮 -->
<el-form-item>
<el-button
type="primary"
size="large"
class="w-full"
:loading="loading"
@click="handleLogin"
>
登录
</el-button>
</el-form-item>
</el-form>
<!-- 用户协议 -->
<div class="text-center fz-14">
登录即表示您同意
<el-link type="primary" :underline="false">用户协议</el-link>
<el-link type="primary" :underline="false">隐私政策</el-link>
</div>
</div>
</div>
</div>
<!-- Copyright -->
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { User, Lock, Key } from '@element-plus/icons-vue'
import VueQrcode from 'vue-qrcode'
// 路由实例
const router = useRouter()
// 登录表单
const loginFormRef = ref()
const loginForm = reactive({
username: '',
password: '',
captcha: '',
remember: false
})
// 登录规则(必填验证)
const loginRules = {
username: [{ required: true, message: '请输入用户名/邮箱/手机号', trigger: 'blur' }],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
],
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
}
// 加载状态
const loading = ref(false)
// 验证码相关
const showCaptcha = ref(false)
const captchaCode = ref('8K9M')
const loginAttempts = ref(0)
// 刷新验证码
const refreshCaptcha = (): void => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let code = ''
for (let i = 0; i < 4; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length))
}
captchaCode.value = code
}
// 处理登录
const handleLogin = async (): Promise<void> => {
try {
await loginFormRef.value.validate()
loading.value = true
// 模拟登录请求
setTimeout(() => {
loading.value = false
loginAttempts.value++
// 模拟登录成功
if (loginForm.username === 'admin' && loginForm.password === '123456') {
ElMessage.success('登录成功')
// 登录成功后,跳转到首页
router.push({ name: 'home' })
// 登录成功后,通知主进程
if (window.electron && window.electron.ipcRenderer) {
window.electron.ipcRenderer.send('login-success')
}
} else {
ElMessage.error('用户名或密码错误')
refreshCaptcha()
}
}, 1500)
} catch (error) {
console.error('验证失败:', error)
}
}
// 处理忘记密码
const handleForgotPassword = (): void => {
ElMessage.info('忘记密码功能开发中...')
}
// 页面加载时聚焦到用户名输入框
onMounted(() => {
const usernameInput = document.querySelector('input[placeholder="请输入用户名/邮箱/手机号"]')
if (usernameInput) {
;(usernameInput as HTMLInputElement).focus()
}
})
</script>
<style scoped lang="scss">
.login {
margin: 40px auto;
}
.left {
padding-right: 60px;
border-right: 1px solid #f2f2f2;
}
.right {
padding-left: 60px;
}
.qr {
width: 195px;
height: 195px;
border: 1px solid #e5e8ec;
border-radius: 8px;
box-shadow: none;
background-color: #fff;
position: relative;
overflow: hidden;
}
.qrcode-error {
margin: 14px;
width: 167px;
height: 167px;
background: hsla(0, 0%, 100%, 0.95);
font-family: PingFang SC;
font-weight: 400;
letter-spacing: 0;
position: absolute;
left: 0;
top: 0;
z-index: 1001;
p {
font-family: PingFang SC;
font-size: 16px;
font-weight: 500;
line-height: 16px;
letter-spacing: 0;
color: #111;
margin-top: 38px;
margin-bottom: 20px;
}
}
</style>

26
src/router/index.ts Normal file
View File

@@ -0,0 +1,26 @@
import { createRouter, createWebHistory } from "vue-router";
const routes = [
{
path: "/",
name: "Login",
component: () => import("@/views/login/index.vue"),
},
{
path: "/home",
name: "Home",
component: () => import("@/views/home/index.vue"),
},
{
path: "/about",
name: "About",
component: () => import("@/views/about/index.vue"),
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;

22
src/stores/counter.ts Normal file
View File

@@ -0,0 +1,22 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = 0
}
return { count, doubleCount, increment, decrement, reset }
})

44
src/views/about/index.vue Normal file
View File

@@ -0,0 +1,44 @@
<template>
<div class="about">
<h2 class="text-2xl font-bold mb-4">关于我们</h2>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-lg font-semibold mb-4">智念科技 AI</h3>
<p class="mb-4">
这是一个基于 Electron + Vue 3 + TypeScript 的桌面应用程序
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-gray-50 p-4 rounded">
<h4 class="font-semibold mb-2">技术栈</h4>
<ul class="list-disc list-inside space-y-1">
<li>Electron</li>
<li>Vue 3</li>
<li>TypeScript</li>
<li>Vue Router</li>
<li>Pinia</li>
<li>Tailwind CSS</li>
</ul>
</div>
<div class="bg-gray-50 p-4 rounded">
<h4 class="font-semibold mb-2">功能特性</h4>
<ul class="list-disc list-inside space-y-1">
<li>现代化 UI 界面</li>
<li>状态管理</li>
<li>路由导航</li>
<li>响应式设计</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// 关于页面组件
</script>
<style scoped>
.about {
max-width: 800px;
margin: 0 auto;
}
</style>

45
src/views/home/index.vue Normal file
View File

@@ -0,0 +1,45 @@
<template>
<div class="home">
<h2 class="text-2xl font-bold mb-4">欢迎使用智念科技 AI</h2>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-lg font-semibold mb-4">计数器示例</h3>
<div class="flex items-center space-x-4 mb-4">
<button
@click="counterStore.decrement"
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded"
>
-
</button>
<span class="text-xl font-bold">{{ counterStore.count }}</span>
<button
@click="counterStore.increment"
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
>
+
</button>
</div>
<div class="mb-4">
<p>双倍值: {{ counterStore.doubleCount }}</p>
</div>
<button
@click="counterStore.reset"
class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded"
>
重置
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useCounterStore } from "@/stores/counter";
const counterStore = useCounterStore();
</script>
<style scoped>
.home {
max-width: 600px;
margin: 0 auto;
}
</style>

32
src/views/login/index.vue Normal file
View File

@@ -0,0 +1,32 @@
<template>
<div
class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4"
>
<div class="w-full max-w-md">
<!-- 登录卡片 -->
<div class="bg-white rounded-lg shadow-xl p-8">
<!-- Logo和标题 -->
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-gray-900 mb-2">
China Fellou Plus
</h1>
<div class="text-sm text-gray-600">
您的隐私对我们很重要我们确保您的数据是安全和保密的
</div>
</div>
<div class="login flex items-start justify-center"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { useRouter } from "vue-router";
// 路由实例
const router = useRouter();
</script>
<style scoped lang="scss"></style>