初始化

This commit is contained in:
xiaoyi
2024-01-27 19:53:17 +08:00
commit 07dbe71c31
840 changed files with 119152 additions and 0 deletions

3
chat/.commitlintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}

11
chat/.editorconfig Normal file
View File

@@ -0,0 +1,11 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

6
chat/.env.example Normal file
View File

@@ -0,0 +1,6 @@
# 本地链接生产# xxx填写你的后端服务地址后面/api勿删除
VITE_GLOB_API_URL=https://xxx/api
VITE_GLOB_OPEN_LONG_REPLY=false
VITE_GLOB_APP_PWA=false

2
chat/.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
docker-compose
kubernetes

12
chat/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,12 @@
module.exports = {
root: true,
extends: ['@antfu'],
rules: {
'no-tabs': 0,
},
globals: {
WeixinJSBridge: false,
wx: false,
loginCount: true,
},
}

17
chat/.gitattributes vendored Normal file
View File

@@ -0,0 +1,17 @@
"*.vue" eol=lf
"*.js" eol=lf
"*.ts" eol=lf
"*.jsx" eol=lf
"*.tsx" eol=lf
"*.cjs" eol=lf
"*.cts" eol=lf
"*.mjs" eol=lf
"*.mts" eol=lf
"*.json" eol=lf
"*.html" eol=lf
"*.css" eol=lf
"*.less" eol=lf
"*.scss" eol=lf
"*.sass" eol=lf
"*.styl" eol=lf
"*.md" eol=lf

30
chat/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment variables files
/service/.env
.env

1
chat/.npmrc Normal file
View File

@@ -0,0 +1 @@
strict-peer-dependencies=false

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

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

25
chat/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Web App",
"url": "http://localhost:1002",
"webRoot": "${workspaceFolder}"
},
{
"type": "node",
"request": "launch",
"name": "Launch Service Server",
"runtimeExecutable": "${workspaceFolder}/service/node_modules/.bin/esno",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/service/src/index.ts",
"outFiles": ["${workspaceFolder}/service/**/*.js"],
"envFile": "${workspaceFolder}/service/.env"
}
]
}

66
chat/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,66 @@
{
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"json",
"jsonc",
"json5",
"yaml",
"yml",
"markdown"
],
"cSpell.words": [
"antfu",
"axios",
"bumpp",
"chatgpt",
"chenzhaoyu",
"commitlint",
"davinci",
"dockerhub",
"esno",
"GPTAPI",
"highlightjs",
"hljs",
"iconify",
"katex",
"katexmath",
"linkify",
"logprobs",
"mdhljs",
"mila",
"nodata",
"OPENAI",
"pinia",
"Popconfirm",
"rushstack",
"Sider",
"tailwindcss",
"traptitech",
"tsup",
"Typecheck",
"unplugin",
"VITE",
"vueuse",
"Zhao"
],
"i18n-ally.enabledParsers": [
"ts"
],
"i18n-ally.sortKeys": true,
"i18n-ally.keepFulfilled": true,
"i18n-ally.localesPaths": [
"src/locales"
],
"i18n-ally.keystyle": "nested",
"vue.codeActions.enabled": false
}

1
chat/config/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './proxy'

16
chat/config/proxy.ts Normal file
View File

@@ -0,0 +1,16 @@
import type { ProxyOptions } from 'vite'
export function createViteProxy(isOpenProxy: boolean, viteEnv: ImportMetaEnv) {
if (!isOpenProxy)
return
const proxy: Record<string, string | ProxyOptions> = {
'/api': {
target: viteEnv.VITE_APP_API_BASE_URL,
changeOrigin: true,
rewrite: path => path.replace('/api/', '/'),
},
}
return proxy
}

63
chat/electron/appMenu.js Normal file
View File

@@ -0,0 +1,63 @@
// appMenu.js
const { Menu, app, Tray } = require('electron');
const path = require('path')
function configureAppMenu(mainWindow) {
let tray = new Tray(path.join(__dirname, '../icons/16x16.png'));
// tray.setToolTip('Nine Ai');
const template = [
{
label: 'NineAi',
submenu: [
{
label: '退出应用',
accelerator: 'CmdOrCtrl+Q',
click: () => {
app.quit();
},
}
],
}
];
if (process.platform === 'darwin') {
template.unshift({
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteandmatchstyle' },
{ role: 'delete' },
{ role: 'selectall' }
]
})
}
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});
tray.on('right-click', () => {
const contextMenuWindows = Menu.buildFromTemplate([
{
label: '退出应用',
click: () => {
app.quit();
},
},
]);
tray.popUpContextMenu(contextMenuWindows);
})
}
module.exports = { configureAppMenu };

View File

@@ -0,0 +1,40 @@
// ipcManager.js
const { ipcMain, BrowserWindow, app } = require('electron');
function handleIpc(mainWindow) {
ipcMain.handle('minimizeWindow', () => {
const mainWindow = BrowserWindow.getFocusedWindow();
mainWindow?.minimize();
});
ipcMain.handle('maxmizeWindow', () => {
const win = BrowserWindow.getFocusedWindow();
if (win) {
if (process.platform === 'darwin') mainWindow?.setFullScreen(true);
else mainWindow?.maximize();
}
});
ipcMain.handle('closeWindow', () => {
// const mainWindow = BrowserWindow.getFocusedWindow();
// mainWindow?.close();
app.quit()
});
ipcMain.handle('unmaximizeWindow', () => {
const win = BrowserWindow.getFocusedWindow();
if (win) {
if (process.platform === 'darwin') mainWindow?.setFullScreen(false);
else mainWindow?.unmaximize();
}
});
ipcMain.on('check-window-maximized', (event) => {
const win = BrowserWindow.getFocusedWindow();
if (win) {
event.reply('window-maximized-status', win.isFullScreen());
}
});
}
module.exports = { handleIpc };

28
chat/electron/main.js Normal file
View File

@@ -0,0 +1,28 @@
// main.js
const { app, BrowserWindow } = require('electron');
const { createMainWindow } = require('./windowManager');
const { handleIpc } = require('./ipcManager');
const { registerShortcuts } = require('./shortcutManager');
const { configureAppMenu } = require('./appMenu');
app.commandLine.appendSwitch('--ignore-certificate-errors', 'true');
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors');
app.whenReady().then(() => {
const mainWindow = createMainWindow();
handleIpc(mainWindow);
registerShortcuts(mainWindow);
configureAppMenu(mainWindow);
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});

View File

@@ -0,0 +1,5 @@
if(process.platform === 'darwin') {
require('./darwin.js')
}else if( process.platform === "win32") {
require('./win32.js')
}

View File

@@ -0,0 +1,19 @@
// shortcutManager.js
const { globalShortcut, BrowserWindow } = require('electron');
let isWindowVisible = true;
function registerShortcuts(mainWindow) {
globalShortcut.register('Ctrl+L', () => {
if (mainWindow && !mainWindow.isFullScreen()) {
if (isWindowVisible) {
mainWindow.hide();
} else {
mainWindow.show();
}
isWindowVisible = !isWindowVisible;
}
});
}
module.exports = { registerShortcuts };

View File

@@ -0,0 +1,51 @@
// windowManager.js
const { BrowserWindow, globalShortcut, clipboard, app } = require('electron');
const { handleIpc } = require('./ipcManager');
let mainWindow = null;
let isWindowVisible = true;
let lastClipboardContent = '';
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1300,
height: 860,
minWidth: 1300,
minHeight: 820,
center: true,
frame: false,
show: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
nodeIntegrationInWorker: true,
webSecurity: true,
allowRunningInsecureContent: false,
},
})
if (app.isPackaged) {
// mainWindow.loadFile(filePath)
mainWindow.loadURL('https://ai.jiangly.com')
}
else {
mainWindow.loadURL('http://127.0.0.1:1002')
// mainWindow.loadURL('https://ai.jiangly.com')
mainWindow.webContents.openDevTools()
}
mainWindow.on('show', () => {
const clipboardContent = clipboard.readText();
if (clipboardContent === lastClipboardContent) return;
mainWindow.webContents.send('clipboard-content', clipboardContent);
lastClipboardContent = clipboardContent;
});
globalShortcut.register('CommandOrControl+Shift+i', () => {
mainWindow.webContents.openDevTools();
});
return mainWindow;
}
module.exports = { createMainWindow };

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
chat/icons/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

BIN
chat/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
chat/icons/icon.icns Normal file

Binary file not shown.

BIN
chat/icons/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
chat/icons/nine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

26
chat/icons/nineai.svg Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="31px" height="31px" viewBox="0 0 31 31" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>资源 1</title>
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
<stop stop-color="#3EABFE" offset="0%"></stop>
<stop stop-color="#1D72FC" offset="100%"></stop>
</linearGradient>
<linearGradient x1="71.4302648%" y1="100%" x2="26.9138414%" y2="9.11560472%" id="linearGradient-2">
<stop stop-color="#3CA6FD" offset="0%"></stop>
<stop stop-color="#FFBE03" offset="100%"></stop>
</linearGradient>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="界面1" transform="translate(-25.000000, -46.000000)" fill-rule="nonzero">
<g id="logo" transform="translate(16.000000, 37.000000)">
<g id="资源-1" transform="translate(9.000000, 9.000000)">
<ellipse id="椭圆形" fill="#FFBE03" cx="27" cy="2.93478261" rx="3" ry="2.93478261"></ellipse>
<rect id="矩形" fill="#FFBE03" x="24" y="8" width="6" height="20.2173913" rx="3"></rect>
<path d="M7.99026784,2.31267003 C9.645947,2.30268356 10.9962358,3.63678109 11.0062223,5.29246025 C11.0062955,5.30459765 11.006295,5.31673539 11.0062207,5.32887279 L10.8749747,26.793631 C10.8646911,28.4754713 9.50375408,29.8362956 7.82191294,29.8464398 C6.16623378,29.8564263 4.815945,28.5223288 4.80595854,26.8666496 C4.80588533,26.8545122 4.80588583,26.8423745 4.80596004,26.8302371 L4.93720605,5.36547891 C4.94748964,3.68363862 6.30842671,2.32281429 7.99026784,2.31267003 Z" id="矩形" fill="url(#linearGradient-1)" transform="translate(7.906090, 16.079555) rotate(20.840000) translate(-7.906090, -16.079555) "></path>
<path d="M8,7.60145625 C10.5,4.5 11.8885328,7.3271602 13.6676092,11.030157 L20.9271128,26.4330613 C22.862394,30.2320617 31,30.5 30,24 C28.5,26 27.2029532,24.5719625 26.5961043,23.3019718 C26.1915384,22.4553113 23.2754425,16.1657663 17.8478165,4.43333678 C14.5,-2 9,2 8,7.60145625 Z" id="路径" fill="url(#linearGradient-2)"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

204
chat/index.html Normal file
View File

@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="UTF-8">
<!-- <link rel="icon" type="image/svg+xml" href="/favicon.svg"> -->
<meta content="yes" name="apple-mobile-web-app-capable" />
<link rel="apple-touch-icon" href="/favicon.ico">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
<title>Ai Loading</title>
</head>
<body class="dark:bg-black">
<div id="app">
<style>
* {
box-sizing: border-box;
}
.main-container {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
body {
background: #fff;
}
.loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
z-index: 9999;
}
.loading-text {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
text-align: center;
width: 100%;
height: 100px;
line-height: 100px;
color: #fff;
}
.loading-text span {
display: inline-block;
margin: 0 5px;
font-family: "Quattrocento Sans", sans-serif;
}
.loading-text span:nth-child(1) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0s infinite linear alternate;
animation: blur-text 1.5s 0s infinite linear alternate;
}
.loading-text span:nth-child(2) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.2s infinite linear alternate;
animation: blur-text 1.5s 0.2s infinite linear alternate;
}
.loading-text span:nth-child(3) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.4s infinite linear alternate;
animation: blur-text 1.5s 0.4s infinite linear alternate;
}
.loading-text span:nth-child(4) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.6s infinite linear alternate;
animation: blur-text 1.5s 0.6s infinite linear alternate;
}
.loading-text span:nth-child(5) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.8s infinite linear alternate;
animation: blur-text 1.5s 0.8s infinite linear alternate;
}
.loading-text span:nth-child(6) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 1s infinite linear alternate;
animation: blur-text 1.5s 1s infinite linear alternate;
}
.loading-text span:nth-child(7) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 1.2s infinite linear alternate;
animation: blur-text 1.5s 1.2s infinite linear alternate;
}
@-webkit-keyframes blur-text {
0% {
filter: blur(0px);
}
100% {
filter: blur(4px);
}
}
@keyframes blur-text {
0% {
filter: blur(0px);
}
100% {
filter: blur(4px);
}
}
@media (prefers-color-scheme: dark) {
body {
background: #121212;
}
}
</style>
<div class="main-container ">
<div class="loading">
<div class="loading-text" id="loading-text">
<span class="loading-text-words">L</span>
<span class="loading-text-words">O</span>
<span class="loading-text-words">A</span>
<span class="loading-text-words">D</span>
<span class="loading-text-words">I</span>
<span class="loading-text-words">N</span>
<span class="loading-text-words">G</span>
</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script>
const now = new Date()
const hour = now.getHours()
const words = document.querySelectorAll('.loading-text-words')
const theme = localStorage.theme
if (theme) {
if (theme === 'dark') {
document.body.style.backgroundColor = '#24272e'
for (let i = 0; i < words.length; i++) {
const word = words[i]
word.style.color = '#fff'
}
}
if (theme === 'light') {
document.body.style.backgroundColor = '#fff'
for (let i = 0; i < words.length; i++) {
const word = words[i]
word.style.color = '#24272e'
}
}
}
else {
if (!theme && hour >= 6 && hour <= 20) {
document.body.style.backgroundColor = '#fff'
for (let i = 0; i < words.length; i++) {
const word = words[i]
word.style.color = '#24272e'
}
}
else {
document.body.style.backgroundColor = '#24272e'
for (let i = 0; i < words.length; i++) {
const word = words[i]
word.style.color = '#fff'
}
}
}
console.log(
"%c本项目作者----小易联系QQ805239273",
"background-color:rgb(30,30,30);border-radius:4px;font-size:12px;padding:4px;color:rgb(220,208,129);"
)
</script>
</body>
</html>
<script>document.querySelector('html').classList.toggle('loading')
</script>

21
chat/license Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Snine
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

148
chat/package.json Normal file
View File

@@ -0,0 +1,148 @@
{
"name": "chatgpt-cooper",
"version": "2.3.0",
"private": true,
"description": "ChatGPT Cooper",
"author": "Snine <J_longyan@163.com>",
"keywords": [
"chatgpt-cooper",
"chatgpt",
"chatbot",
"vue",
"nestjs"
],
"main": "electron/main.js",
"scripts": {
"start:h": "pnpm run -C service dev",
"start:f": "vite",
"all": "npm-run-all --parallel start:h start:f",
"dev": "vite",
"build-check": "run-p type-check build-only",
"preview": "vite preview",
"build": "vite build --mode=production",
"type-check": "vue-tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"bootstrap": "pnpm install && pnpm run common:prepare",
"start": "pnpm dev && electron .",
"ele": "electron .",
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml",
"pack:mac": "NPM_CONFIG_ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ electron-builder build --mac",
"pack:win": "NPM_CONFIG_ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ electron-builder build --win --ia32"
},
"dependencies": {
"@electron/remote": "^2.1.0",
"@icon-park/vue-next": "^1.4.2",
"@traptitech/markdown-it-katex": "^3.6.0",
"@types/dom-to-image": "^2.6.4",
"@types/file-saver": "^2.0.5",
"@vicons/ionicons5": "^0.12.0",
"@vueuse/core": "^9.13.0",
"@vueuse/electron": "^10.2.1",
"@vueuse/integrations": "^10.2.0",
"@vueuse/motion": "^2.0.0",
"add": "^2.0.6",
"clientjs": "^0.2.1",
"date-fns": "^2.30.0",
"dom-to-image": "^2.6.0",
"file-saver": "^2.0.5",
"highlight.js": "^11.7.0",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"katex": "^0.16.4",
"markdown-it": "^13.0.1",
"marked": "^4.3.0",
"markmap-common": "0.14.2",
"markmap-lib": "0.14.4",
"markmap-view": "0.14.4",
"naive-ui": "^2.34.3",
"pinia": "^2.0.33",
"qrcode": "^1.5.3",
"v-viewer": "3.0.11",
"vue": "^3.2.47",
"vue-clipboard3": "^2.0.0",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@antfu/eslint-config": "^0.35.3",
"@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4",
"@iconify/vue": "^4.1.0",
"@types/crypto-js": "^4.1.1",
"@types/katex": "^0.16.0",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/node": "^18.14.6",
"@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13",
"axios": "^1.3.4",
"crypto-js": "^4.1.1",
"electron": "^25.3.1",
"electron-builder": "^24.4.0",
"eslint": "^8.35.0",
"husky": "^8.0.3",
"less": "^4.1.3",
"lint-staged": "^13.1.2",
"markdown-it-link-attributes": "^4.0.1",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.21",
"rimraf": "^4.2.0",
"tailwindcss": "^3.2.7",
"typescript": "~4.9.5",
"vite": "^4.2.0",
"vite-plugin-pwa": "^0.14.4",
"vue-tsc": "^1.2.0"
},
"lint-staged": {
"*.{ts,tsx,vue}": [
"pnpm lint:fix"
]
},
"build": {
"productName": "NineAi",
"appId": "ai.jiangly.com",
"icon": "icons/icon.icns",
"directories": {
"output": "build"
},
"mac": {
"target": "dmg",
"icon": "icons/icon.icns"
},
"dmg": {
"backgroundColor": "#fff",
"contents": [
{
"x": 410,
"y": 150,
"type": "link",
"path": "/Applications"
},
{
"x": 130,
"y": 150,
"type": "file"
}
]
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"ia32"
]
}
]
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"perMachine": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}

8562
chat/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
chat/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
chat/public/favicon.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" width="1em" height="1em"><path d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
chat/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
chat/resources/icon.icns Normal file

Binary file not shown.

BIN
chat/resources/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
chat/resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

91
chat/src/App.vue Normal file
View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import { NConfigProvider, NGlobalStyle, dateZhCN } from 'naive-ui'
import { computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ClientJS } from 'clientjs'
import { ss } from './utils/storage'
import { NaiveProvider } from '@/components/common'
import { useTheme } from '@/hooks/useTheme'
import { useLanguage } from '@/hooks/useLanguage'
import { useAuthStore, useGlobalStoreWithOut, useChatStore } from '@/store'
const client = new ClientJS()
const chatStore = useChatStore()
// Get the client's fingerprint id
const fingerprint = client.getFingerprint()
const authStore = useAuthStore()
const useGlobalStore = useGlobalStoreWithOut()
const router = useRouter()
useGlobalStore.updateFingerprint(fingerprint)
const { theme, lightThemeOverrides, darkThemeOverrides } = useTheme()
const { language } = useLanguage()
const homePath = computed(() => authStore.globalConfig?.clientHomePath)
const faviconPath = computed(() => authStore.globalConfig?.clientFavoIconPath || '/favicon.svg')
const isAutoOpenNotice = computed(() => Number(authStore.globalConfig?.isAutoOpenNotice) === 1)
async function loadBaiduCode() {
const baiduCode: any = authStore.globalConfig?.baiduCode || ''
if (!baiduCode)
return
const scriptElem = document.createElement('script')
const escapedCode = baiduCode.replace(/<script[\s\S]*?>([\s\S]*?)<\/script>/gi, '$1')
scriptElem.innerHTML = escapedCode
document.head.appendChild(scriptElem)
}
function setDocumentTitle() {
document.title = authStore.globalConfig?.siteName || 'AI'
}
const themeOverrides = computed(() => {
const config = !theme.value ? lightThemeOverrides : darkThemeOverrides
return config
})
function goHome() {
homePath.value && router.push(homePath.value)
}
function noticeInit() {
const showNotice = ss.get('showNotice')
if (!showNotice && isAutoOpenNotice.value) {
useGlobalStore.updateNoticeDialog(true)
}
else {
if (Date.now() > Number(showNotice) && isAutoOpenNotice.value)
useGlobalStore.updateNoticeDialog(true)
}
}
/* 动态设置网站ico svg格式 */
const link = document.createElement('link')
link.rel = 'shortcut icon'
link.href = faviconPath.value
link.type = 'image/svg+xml'
document.getElementsByTagName('head')[0].appendChild(link)
onMounted(async () => {
goHome()
await chatStore.getBaseModelConfig()
loadBaiduCode()
setDocumentTitle()
noticeInit()
})
</script>
<template>
<NConfigProvider
class="h-full "
:theme="theme"
:theme-overrides="themeOverrides"
:locale="language"
:date-locale="dateZhCN"
preflight-style-disabled
>
<NaiveProvider>
<RouterView />
</NaiveProvider>
<NGlobalStyle />
</NConfigProvider>
</template>

44
chat/src/api/appStore.ts Normal file
View File

@@ -0,0 +1,44 @@
import { get, post } from '@/utils/request'
/* 查询app分组 */
export function fetchQueryAppCatsAPI<T>(): Promise<T> {
return get<T>({ url: '/app/queryCats' })
}
/* 查询全量app列表 */
export function fetchQueryAppsAPI<T>(): Promise<T> {
return get<T>({
url: '/app/list',
})
}
/* 查询个人app列表 */
export function fetchQueryMineAppsAPI<T>(): Promise<T> {
return get<T>({
url: '/app/mineApps',
})
}
/* 收藏app */
export function fetchCollectAppAPI<T>(data: { appId: number }): Promise<T> {
return post<T>({ url: '/app/collect', data })
}
/* 收藏app */
export function fetchCustomAppAPI<T>(data: any): Promise<T> {
return post<T>({ url: '/app/customApp', data })
}
/* 删除app */
export function fetchDelMineAppAPI<T>(data: any): Promise<T> {
return post<T>({ url: '/app/delMineApp', data })
}
/* 查询全量app列表 */
export function fetchQueryOneCatAPI<T>(data): Promise<T> {
return get<T>({
url: '/app/queryOneCat',
data
})
}

30
chat/src/api/balance.ts Normal file
View File

@@ -0,0 +1,30 @@
import { get, post } from '@/utils/request'
/* get rechargeLog */
export function fetchGetRechargeLogAPI<T>(data: { page?: number; size?: number }): Promise<T> {
return get<T>({
url: '/balance/rechargeLog',
data,
})
}
/* query balance */
export function fetchGetBalanceQueryAPI<T>(): Promise<T> {
return get<T>({
url: '/balance/query',
})
}
/* log invite link count */
export function fetchVisitorCountAPI<T>(): Promise<T> {
return get<T>({
url: '/balance/getVisitorCount',
})
}
/* log invite link count */
export function fetchSyncVisitorDataAPI<T>(): Promise<T> {
return post<T>({
url: '/balance/inheritVisitorData',
})
}

33
chat/src/api/chatLog.ts Normal file
View File

@@ -0,0 +1,33 @@
import { get, post } from '@/utils/request'
/* 删除对话记录 */
export function fetchDelChatLogAPI<T>(data: { id: number }): Promise<T> {
return post<T>({
url: '/chatlog/del',
data,
})
}
/* 删除一组对话记录 */
export function fetchDelChatLogByGroupIdAPI<T>(data: { groupId: number }): Promise<T> {
return post<T>({
url: '/chatlog/delByGroupId',
data,
})
}
/* 查询x组对话信息 */
export function fetchQueryChatLogListAPI<T>(data: { groupId: number }): Promise<T> {
return get<T>({
url: '/chatlog/chatList',
data,
})
}
/* 查询单个应用的对话信息 */
export function fetchQueryChatLogByAppIdAPI<T>(data: { page?: number; size?: number; appId: number }): Promise<T> {
return get<T>({
url: '/chatlog/byAppId',
data,
})
}

17
chat/src/api/config.ts Normal file
View File

@@ -0,0 +1,17 @@
import { get } from '@/utils/request'
/* query globle config */
export function fetchQueryConfigAPI<T>(data: any) {
return get<T>({
url: '/config/queryFronet',
data,
})
}
/* query globle menu */
export function fetchQueryMenuAPI<T>(data: any) {
return get<T>({
url: '/menu/list',
data,
})
}

17
chat/src/api/crami.ts Normal file
View File

@@ -0,0 +1,17 @@
import { get, post } from '@/utils/request'
/* use crami */
export function fetchUseCramiAPI<T>(data: { code: string }): Promise<T> {
return post<T>({
url: '/crami/useCrami',
data,
})
}
/* get all crami package */
export function fetchGetPackageAPI<T>(data: { status: number; type?: number; size?: number }): Promise<T> {
return get<T>({
url: '/crami/queryAllPackage',
data,
})
}

8
chat/src/api/global.ts Normal file
View File

@@ -0,0 +1,8 @@
import { get } from '@/utils/request'
/* get notice */
export function fetchGetGlobalNoticeAPI<T>(): Promise<T> {
return get<T>({
url: '/config/notice',
})
}

43
chat/src/api/group.ts Normal file
View File

@@ -0,0 +1,43 @@
import { get, post } from '@/utils/request'
/* 创建新的对话组 */
export function fetchCreateGroupAPI<T>(data?: { appId?: number }): Promise<T> {
return post<T>({
url: '/group/create',
data,
})
}
/* 查询对话组列表 */
export function fetchQueryGroupAPI<T>(): Promise<T> {
return get<T>({ url: '/group/query' })
}
/* 修改对话组 */
export function fetchUpdateGroupAPI<T>(data?: {
groupId?: number
title?: string
isSticky?: boolean,
config?: string
}): Promise<T> {
return post<T>({
url: '/group/update',
data,
})
}
/* 删除对话组 */
export function fetchDelGroupAPI<T>(data?: { groupId: number }): Promise<T> {
return post<T>({
url: '/group/del',
data,
})
}
/* 删除全部对话组 */
export function fetchDelAllGroupAPI<T>(data?: { groupId: number }): Promise<T> {
return post<T>({
url: '/group/delAll',
data,
})
}

163
chat/src/api/index.ts Normal file
View File

@@ -0,0 +1,163 @@
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
import { get, post } from '@/utils/request'
import { useSettingStore } from '@/store'
/* 流失对话聊天 */
export function fetchChatAPIProcess<T = any>(
params: {
prompt: string
appId?: number
options?: { conversationId?: string; parentMessageId?: string; temperature: number }
signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
return post<T>({
url: '/chatgpt/chat-process',
data: { prompt: params.prompt, appId: params?.appId, options: params.options },
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})
}
/* 获取个人信息 */
export function fetchGetInfo<T>() {
return get<T>({ url: '/auth/getInfo' })
}
/* 注册 */
export function fetchRegisterAPI<T>(data: { username: string;password: string;email: string }): Promise<T> {
return post<T>({ url: '/auth/register', data }) as Promise<T>
}
/* 注册 */
export function fetchRegisterByPhoneAPI<T>(data: { username: string;password: string; phone: string; phoneCode: string }): Promise<T> {
return post<T>({ url: '/auth/registerByPhone', data }) as Promise<T>
}
/* 登录 */
export function fetchLoginAPI<T>(data: { username: string; password: string }): Promise<T> {
return post<T>({ url: '/auth/login', data }) as Promise<T>
}
/* 手机号登录 */
export function fetchLoginByPhoneAPI<T>(data: { phone: string; password: string }): Promise<T> {
return post<T>({ url: '/auth/loginByPhone', data }) as Promise<T>
}
/* 修改个人信息 */
export function fetchUpdateInfoAPI<T>(data: { username?: string; avatar?: string }): Promise<T> {
return post<T>({ url: '/user/update', data }) as Promise<T>
}
/* 获取个人绘画记录 */
export function fetchGetChatLogDraw<T>(data: { model: string }): Promise<T> {
return get<T>({ url: '/chatLog/draw', data }) as Promise<T>
}
/* 获取所有绘画记录 */
export function fetchGetAllChatLogDraw<T>(data: { size: number; rec: number; model: string }): Promise<T> {
return get<T>({ url: '/chatLog/drawAll', data }) as Promise<T>
}
/* chatgpt的dall-e2绘画 */
export function fetchChatDraw<T>(data: { prompt: string;n: number;size: string }): Promise<T> {
return post<T>({ url: '/chatgpt/chat-draw', data }) as Promise<T>
}
/* 修改密码 */
export function fetchUpdatePasswordAPI<T>(data: { oldPassword?: string;password?: string }): Promise<T> {
return post<T>({ url: '/auth/updatePassword', data }) as Promise<T>
}
/* 同步对话 */
export function fetchGetchatSyncApi<T = any>(
params: {
prompt: string
options?: { conversationId?: string; parentMessageId?: string; temperature: number }
signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
return post<T>({
url: '/chatgpt/chat-sync',
data: { prompt: params.prompt, options: params.options },
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})
}
/* 获取mind绘画联想词 */
export function fetchGetchatMindApi<T = any>(
params: {
prompt: string
options?: { conversationId?: string; parentMessageId?: string; temperature: number }
signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
return post<T>({
url: '/chatgpt/chat-mind',
data: { prompt: params.prompt, options: params.options },
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})
}
/* 获取MJ绘画联想词 */
export function fetchGetMjPromptAssociateApi<T>(data: { prompt: string }): Promise<T> {
return post<T>({ url: '/chatgpt/mj-associate', data }) as Promise<T>
}
/* 获取MJ绘画联想词 */
export function fetchGetMjPromptFanyiApi<T>(data: { prompt: string }): Promise<T> {
return post<T>({ url: '/chatgpt/mj-fy', data }) as Promise<T>
}
/* 获取我得绘制列表 */
export function fetchMidjourneyDrawList<T>(data: { page?: number; size?: number }): Promise<T> {
return get<T>({ url: '/midjourney/drawList', data }) as Promise<T>
}
/* 获取Mj提示词 */
export function fetchMidjourneyPromptList<T>(): Promise<T> {
return get<T>({ url: '/midjourney/queryPrompts' }) as Promise<T>
}
/* 获取Mj完整提示词 */
export function fetchMidjourneyFullPrompt<T>(data: any): Promise<T> {
return get<T>({ url: '/midjourney/getFullPrompt', data }) as Promise<T>
}
/* 删除MJ绘画记录 */
export function fetchDownloadImg<T>(data: { id: number }): Promise<T> {
return post<T>({ url: '/midjourney/delete', data }) as Promise<T>
}
/* 获取我得绘制列表 */
export function fetchMidjourneyGetList<T>(data: { page?: number; size?: number; rec: number }): Promise<T> {
return get<T>({ url: '/midjourney/getList', data }) as Promise<T>
}
/* 推荐图片 */
export function fetchRecDraw<T>(data: { id: number }): Promise<T> {
return post<T>({ url: '/midjourney/rec', data }) as Promise<T>
}
/* 获取图片验证码 */
export function fetchCaptchaImg<T>(data: { color: string }): Promise<T> {
return post<T>({ url: '/auth/captcha', data }) as Promise<T>
}
/* 发送手机验证码 */
export function fetchSendSms<T>(data: { phone: string; captchaId: string; captchaCode: string }): Promise<T> {
return post<T>({ url: '/auth/sendPhoneCode', data }) as Promise<T>
}
/* 获取九宫格设置 */
export function fetchGetChatBoxList<T>() {
return get<T>({ url: '/chatgpt/queryChatBoxFrontend' })
}
/* 获取快问设置 */
export function fetchGetChatPreList<T>() {
return get<T>({ url: '/chatgpt/queryChatPreList' })
}

52
chat/src/api/mjDraw.ts Normal file
View File

@@ -0,0 +1,52 @@
import { get, post } from '@/utils/request'
/* mj draw */
export function fetchMjDtawAPI<T>(data: { prompt: string }): Promise<T> {
return post<T>({
url: '/mj/draw',
data,
})
}
/* mj upscale Img */
export function fetchUpscaleSingleImgAPI<T>(data: { message_id: string; orderId: number }): Promise<T> {
return post<T>({
url: '/mj/upscaleSingleImg',
data,
})
}
/* mj variation img */
export function fetchVariationSingleImgAPI<T>(data: { message_id: string; orderId: number }): Promise<T> {
return post<T>({
url: '/mj/variationSingleImg',
data,
})
}
/* mj fanyi */
export function fetchTranslateAPI<T>(data: { text: string }): Promise<T> {
return get<T>({
url: '/fanyi/translate',
data,
})
}
/* 提交一个绘画任务 */
export function fetchDrawTaskAPI<T>(data: { prompt?: string; imgUrl?: string; extraParam?: string; drawId?: number; action?: number; orderId?: number }): Promise<T> {
return post<T>({
url: '/queue/addMjDrawQueue',
data,
})
}
/* 代理图片 */
export function fetchProxyImgAPI<T>(data: { url: string }): Promise<T> {
return get<T>({
url: '/midjourney/proxy',
data,
headers: {
responseType: 'arraybuffer'
}
})
}

15
chat/src/api/models.ts Normal file
View File

@@ -0,0 +1,15 @@
import { get } from '@/utils/request'
/* query models list */
export function fetchQueryModelsListAPI<T>() {
return get<T>({
url: '/models/list',
})
}
/* query base model config */
export function fetchModelBaseConfigAPI<T>() {
return get<T>({
url: '/models/baseConfig',
})
}

17
chat/src/api/order.ts Normal file
View File

@@ -0,0 +1,17 @@
import { get, post } from '@/utils/request'
/* order buy */
export function fetchOrderBuyAPI<T>(data: { goodsId: number; payType?: string }): Promise<T> {
return post<T>({
url: '/order/buy',
data,
})
}
/* order query */
export function fetchOrderQueryAPI<T>(data: { orderId: string }): Promise<T> {
return get<T>({
url: '/order/queryByOrderId',
data,
})
}

37
chat/src/api/sales.ts Normal file
View File

@@ -0,0 +1,37 @@
import { get, post } from '@/utils/request'
/* query sales account */
export function fetchSalesAccountAPI<T>(): Promise<T> {
return get<T>({
url: '/sales/mineAccount',
})
}
/* query sales records */
export function fetchSalesRecordsAPI<T>(data: { page?: number; size?: number }): Promise<T> {
return get<T>({
url: '/sales/mineRecords',
data,
})
}
/* query sales order */
export function fetchSalesOrderAPI<T>(data: { page?: number; size?: number }): Promise<T> {
return get<T>({
url: '/sales/drawMoneyOrder',
data,
})
}
/* salce appfor money */
export function fetchAppforMoneyAPI<T>(data: {
withdrawalAmount: number | null
withdrawalChannels: number | null
contactInformation: string
remark?: string
}): Promise<T> {
return post<T>({
url: '/sales/appForMoney',
data,
})
}

15
chat/src/api/signin.ts Normal file
View File

@@ -0,0 +1,15 @@
import { get, post } from '@/utils/request'
/* sign in */
export function fetchSignInAPI<T>(): Promise<T> {
return post<T>({
url: '/signin/sign',
})
}
/* sign log */
export function fetchSignLogAPI<T>(): Promise<T> {
return get<T>({
url: '/signin/signinLog',
})
}

5
chat/src/api/types.ts Normal file
View File

@@ -0,0 +1,5 @@
export interface ResData {
success: boolean
message: string
data: any
}

104
chat/src/api/user.ts Normal file
View File

@@ -0,0 +1,104 @@
import { get, post } from '@/utils/request'
/* gen inviteCode */
export function fetchGenInviteCodeAPI<T>(): Promise<T> {
return post<T>({
url: '/user/genInviteCode',
})
}
/* get inviteRecord */
export function fetchGetInviteRecordAPI<T>(data: { page?: number; size?: number }): Promise<T> {
return get<T>({
url: '/user/inviteRecord',
data,
})
}
/* get wechat-login senceStr */
export function fetchGetQRSceneStrAPI<T>(
data: { invitedBy?: string },
): Promise<T> {
return post<T>({
url: '/official/getQRSceneStr',
data,
})
}
/* get wechat-login qr url */
export function fetchGetQRCodeAPI<T>(
data: { sceneStr: string },
): Promise<T> {
return get<T>({
url: '/official/getQRCode',
data,
})
}
/* login by scenceStr */
export function fetchLoginBySceneStrAPI<T>(
data: { sceneStr: string },
): Promise<T> {
return post<T>({
url: '/official/loginBySceneStr',
data,
})
}
/* login by code */
export function fetchLoginByCodeAPI<T>(
data: { code: string },
): Promise<T> {
return post<T>({
url: '/official/loginByCode',
data,
})
}
/* get wx registery config */
export function fetchGetJsapiTicketAPI<T>(
data: { url: string },
): Promise<T> {
return post<T>({
url: '/official/getJsapiTicket',
data,
})
}
/* get wechat-login senceStr */
export function fetchGetQRSceneStrByBindAPI<T>(): Promise<T> {
return post<T>({
url: '/official/getQRSceneStrByBind',
})
}
/* bind wx by scenceStr */
export function fetchBindWxBySceneStrAPI<T>(
data: { sceneStr: string },
): Promise<T> {
return post<T>({
url: '/official/bindWxBySceneStr',
data,
})
}
/* get wx rediriect login url */
export function fetchWxLoginRedirectAPI<T>(
data: { url: string },
): Promise<T> {
return post<T>({
url: '/official/getRedirectUrl',
data,
})
}
/* log invite link count */
export function fetchInviteCodeAPI<T>(
data: { code: string },
): Promise<T> {
return get<T>({
url: '/user/inviteLink',
data,
})
}

BIN
chat/src/assets/alipay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
chat/src/assets/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
chat/src/assets/badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
chat/src/assets/fail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#292F33" d="M3.651 29.852L29.926 3.576c.391-.391 2.888 2.107 2.497 2.497L6.148 32.349c-.39.391-2.888-2.107-2.497-2.497z"/><path fill="#66757F" d="M30.442 4.051L4.146 30.347l.883.883L31.325 4.934z"/><path fill="#E1E8ED" d="M34.546 2.537l-.412-.412-.671-.671c-.075-.075-.165-.123-.255-.169-.376-.194-.844-.146-1.159.169l-2.102 2.102.495.495.883.883 1.119 1.119 2.102-2.102c.391-.391.391-1.024 0-1.414zM5.029 31.23l-.883-.883-.495-.495-2.209 2.208c-.315.315-.363.783-.169 1.159.046.09.094.18.169.255l.671.671.412.412c.391.391 1.024.391 1.414 0l2.208-2.208-1.118-1.119z"/><path fill="#F5F8FA" d="M31.325 4.934l2.809-2.809-.671-.671c-.075-.075-.165-.123-.255-.169l-2.767 2.767.884.882zM4.146 30.347L1.273 33.22c.046.09.094.18.169.255l.671.671 2.916-2.916-.883-.883z"/><path d="M28.897 14.913l1.542-.571.6-2.2c.079-.29.343-.491.644-.491.3 0 .564.201.643.491l.6 2.2 1.542.571c.262.096.435.346.435.625s-.173.529-.435.625l-1.534.568-.605 2.415c-.074.296-.341.505-.646.505-.306 0-.573-.209-.647-.505l-.605-2.415-1.534-.568c-.262-.096-.435-.346-.435-.625 0-.278.173-.528.435-.625M11.961 5.285l2.61-.966.966-2.61c.16-.433.573-.72 1.035-.72.461 0 .874.287 1.035.72l.966 2.61 2.609.966c.434.161.721.573.721 1.035 0 .462-.287.874-.721 1.035l-2.609.966-.966 2.61c-.161.433-.574.72-1.035.72-.462 0-.875-.287-1.035-.72l-.966-2.61-2.61-.966c-.433-.161-.72-.573-.72-1.035.001-.462.288-.874.72-1.035M24.13 20.772l1.383-.512.512-1.382c.085-.229.304-.381.548-.381.244 0 .463.152.548.381l.512 1.382 1.382.512c.23.085.382.304.382.548 0 .245-.152.463-.382.548l-1.382.512-.512 1.382c-.085.229-.304.381-.548.381-.245 0-.463-.152-.548-.381l-.512-1.382-1.383-.512c-.229-.085-.381-.304-.381-.548 0-.245.152-.463.381-.548" fill="#FFAC33"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1706003306703" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="26508" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M484.118261 266.562783l-193.446957 101.776695a40.893217 40.893217 0 0 0-26.045217 39.41287v208.372869a36.730435 36.730435 0 0 0 20.301913 37.030957l206.436174 108.710956a39.991652 39.991652 0 0 0 40.626087 1.113044c22.405565-11.853913 211.656348-112.161391 211.656348-112.161391a36.10713 36.10713 0 0 0 15.582608-29.862957c0.367304-20.813913 0-213.23687 0-213.236869a36.964174 36.964174 0 0 0-18.654608-35.283479l-209.452522-110.825739a34.148174 34.148174 0 0 0-25.6-4.852869 39.168 39.168 0 0 0-11.130435 4.36313z" fill="#87B3FF" p-id="26509"></path><path d="M308.424348 406.984348l202.963478 101.665391 0.545391 213.214609-203.508869-107.163826z" fill="#D3E3FF" p-id="26510"></path><path d="M712.325565 415.376696l-163.550608 82.721391v47.749565l163.550608-82.721391z" fill="#186CFF" p-id="26511"></path><path d="M512 100.173913a411.826087 411.826087 0 1 0 411.826087 411.826087A412.293565 412.293565 0 0 0 512 100.173913m0-100.173913A512 512 0 1 1 0 512 512 512 0 0 1 512 0z" fill="#ECF3FF" p-id="26512"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1706003418289" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="26651" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M0 0h1024v1024H0z" fill="#1296db" fill-opacity="0" p-id="26652"></path><path d="M512 65.6L898.6 288.8v446.5L512 958.5 125.4 735.2V288.8L512 65.6m0-65.6L68.5 256v512l443.4 256 443.4-256V256L512 0z" fill="#1296db" p-id="26653"></path><path d="M338.5 602.8l-0.4 141.1c0 1.8-1 3.5-2.6 4.4-1.6 1-3.6 1-5.2-0.1l-122.8-70.8c-1.6-0.9-2.6-2.7-2.6-4.5l0.4-140.9c0-1.8 1-3.6 2.6-4.5 1.6-0.9 3.6-0.9 5.2 0l122.7 70.9c1.7 0.9 2.7 2.6 2.7 4.4z m0.6-183.4l-0.4 141c0 1.8-1 3.6-2.6 4.5-1.6 0.9-3.5 0.9-5.1-0.1L208.2 494c-1.6-0.9-2.6-2.7-2.6-4.5l0.4-141c0-1.8 1-3.6 2.6-4.5 1.6-0.9 3.6-0.9 5.2 0L336.6 414.9c1.5 1 2.5 2.7 2.5 4.5zM498.2 695l-0.4 141c0 1.8-1 3.5-2.6 4.5-1.6 0.9-3.5 0.9-5.1 0l-122.8-70.9c-1.6-0.9-2.6-2.6-2.6-4.5l0.4-141c0-1.8 1-3.6 2.6-4.5 1.6-0.9 3.6-0.9 5.2 0l122.8 70.9c1.5 1 2.5 2.7 2.5 4.5z m187-92.2l0.4 141.1c0 4 4.4 6.4 7.8 4.4l122.8-70.8c1.6-0.9 2.6-2.7 2.6-4.5l-0.5-141c0-1.8-1-3.6-2.6-4.5-1.6-0.9-3.6-0.9-5.2 0l-122.8 70.9c-1.5 0.9-2.5 2.6-2.5 4.4z m-0.5-183.4l0.4 141c0 1.9 1 3.6 2.6 4.5 1.6 0.9 3.6 0.9 5.2-0.1L815.6 494c1.6-0.9 2.6-2.7 2.6-4.5l-0.4-141c0-1.8-1-3.6-2.6-4.5-1.6-0.9-3.6-0.9-5.2 0l-122.8 70.9c-1.5 1-2.5 2.7-2.5 4.5zM525.6 695l0.3 141c0 4 4.3 6.4 7.7 4.5l122.8-70.9c1.6-0.9 2.6-2.6 2.6-4.5l-0.4-141c0-1.8-1-3.6-2.6-4.5-1.6-0.9-3.6-0.9-5.2 0l-122.8 70.9c-1.5 1-2.4 2.7-2.4 4.5z m-10.4-394.5l121.9-70.9c1.6-0.9 2.6-2.6 2.6-4.5s-1-3.5-2.6-4.5l-122.7-70.8c-1.6-0.9-3.5-0.9-5.1 0l-122 70.9c-1.6 0.9-2.6 2.6-2.6 4.4 0 1.8 1 3.6 2.6 4.5l122.8 70.9c1.5 1 3.5 1 5.1 0z m-158.7 92.2l121.9-70.9c1.6-0.9 2.6-2.6 2.6-4.5 0-1.8-1-3.5-2.6-4.4L355.7 242c-1.6-1-3.6-1-5.2 0L228.6 312.9c-1.6 0.9-2.6 2.6-2.6 4.4 0 1.8 1 3.6 2.6 4.5l122.8 70.9c1.6 1 3.6 1 5.1 0z m316.2 6.8l126.3-73.4c1.7-0.9 2.7-2.7 2.7-4.7s-1-3.7-2.7-4.7l-127.2-73.4c-1.6-1-3.7-1-5.3 0l-126.3 73.4c-1.7 1-2.7 2.7-2.7 4.7 0 1.9 1 3.7 2.7 4.7l127.2 73.4c1.6 0.9 3.6 0.9 5.3 0z m-13.6 19.9l-0.4 141c0 1.8-1 3.5-2.6 4.5-1.6 0.9-3.6 0.9-5.2-0.1L528.2 494c-1.6-0.9-2.6-2.7-2.6-4.5l0.4-141c0-4 4.3-6.4 7.8-4.4L656.6 415c1.5 0.9 2.5 2.6 2.5 4.4z m-294.4 0l0.3 141c0 1.8 1 3.6 2.6 4.5s3.6 0.9 5.2 0L495.6 494c1.6-0.9 2.6-2.7 2.6-4.5l-0.3-141c0-1.8-1-3.6-2.6-4.5-1.6-0.9-3.6-0.9-5.2 0l-122.8 70.9c-1.6 1-2.6 2.7-2.6 4.5z m150.5 247l121.9-70.9c1.6-0.9 2.6-2.6 2.6-4.5 0-1.8-1-3.5-2.6-4.4l-122.7-70.9c-1.6-0.9-3.5-0.9-5.1 0l-122 70.9c-1.6 0.9-2.5 2.6-2.6 4.4 0 1.9 1 3.6 2.6 4.5l122.8 70.9c1.5 0.9 3.5 0.9 5.1 0z" fill="#1296db" p-id="26654"></path></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#9AAAB4" d="M27.388 24.642L24.56 27.47l-4.95-4.95 2.828-2.828z"/><path fill="#66757F" d="M34.683 29.11l-5.879-5.879c-.781-.781-2.047-.781-2.828 0l-2.828 2.828c-.781.781-.781 2.047 0 2.828l5.879 5.879c1.562 1.563 4.096 1.563 5.658 0 1.56-1.561 1.559-4.094-.002-5.656z"/><circle fill="#8899A6" cx="13.586" cy="13.669" r="13.5"/><circle fill="#BBDDF5" cx="13.586" cy="13.669" r="9.5"/></svg>

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
chat/src/assets/img-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
chat/src/assets/market.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
chat/src/assets/qianbao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1 @@
[]

BIN
chat/src/assets/wechat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

BIN
chat/src/assets/wxpay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -0,0 +1,130 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { useAppStore } from '@/store'
const props = withDefaults(defineProps<Props>(), {
gap: 10,
progress: 0,
tips: '',
words: ['L', 'O', 'A', 'D', 'I', 'N', 'G']
})
const appStore = useAppStore()
const theme = computed(() => appStore.theme)
const loadingTextColor = computed(() => theme.value === 'dark' ? '#fff' : '#000')
interface Props {
gap?: number
progress?: number
tips?: string
bgColor?: string
words?: any
}
// const words = ref<string[]>(['L', 'O', 'A', 'D', 'I', 'N', 'G'])
</script>
<template>
<div class="loading" :style="{ background: props.bgColor }">
<div class="loading-text">
<span v-for="item in props.words" :key="item" :style="{ margin: `0 ${props.gap}px`, color: loadingTextColor }" class="loading-text-words">{{ item }}</span>
</div>
<div v-if="!tips && props.progress" class="progress">
绘制进度 {{ props.progress }}%
</div>
<div v-if="tips" class="progress">
{{ props.tips }}
</div>
</div>
</template>
<style scoped>
.loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
user-select: none;
}
.progress{
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
.loading-text {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
text-align: center;
width: 100%;
height: 110px;
line-height: 100px;
}
.loading-text span {
display: inline-block;
margin: 0 5px;
color: #fff;
font-family: "Quattrocento Sans", sans-serif;
}
.loading-text span:nth-child(1) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0s infinite linear alternate;
animation: blur-text 1.5s 0s infinite linear alternate;
}
.loading-text span:nth-child(2) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.2s infinite linear alternate;
animation: blur-text 1.5s 0.2s infinite linear alternate;
}
.loading-text span:nth-child(3) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.4s infinite linear alternate;
animation: blur-text 1.5s 0.4s infinite linear alternate;
}
.loading-text span:nth-child(4) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.6s infinite linear alternate;
animation: blur-text 1.5s 0.6s infinite linear alternate;
}
.loading-text span:nth-child(5) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 0.8s infinite linear alternate;
animation: blur-text 1.5s 0.8s infinite linear alternate;
}
.loading-text span:nth-child(6) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 1s infinite linear alternate;
animation: blur-text 1.5s 1s infinite linear alternate;
}
.loading-text span:nth-child(7) {
filter: blur(0px);
-webkit-animation: blur-text 1.5s 1.2s infinite linear alternate;
animation: blur-text 1.5s 1.2s infinite linear alternate;
}
@-webkit-keyframes blur-text {
0% {
filter: blur(0px);
}
100% {
filter: blur(4px);
}
}
@keyframes blur-text {
0% {
filter: blur(0px);
}
100% {
filter: blur(4px);
}
}
</style>

View File

@@ -0,0 +1,3 @@
import TitleBar from './titleBar.vue'
export { TitleBar }

View File

@@ -0,0 +1,164 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useIpcRenderer } from '@vueuse/electron'
defineProps<{ title?: string }>()
import { useGlobalStore } from '@/store'
const ipcRenderer = useIpcRenderer()
const isFullScreen = ref(false)
const globalStore = useGlobalStore()
const checkIfWindowIsMaximized = () => {
ipcRenderer.send('check-window-maximized');
};
const handleMaximizedStatus: any = (_: Event, isMaximized: any) => {
isFullScreen.value = isMaximized
};
onMounted(() => {
ipcRenderer.on('window-maximized-status', handleMaximizedStatus);
ipcRenderer.on('clipboard-content', clipboardHandle);
checkIfWindowIsMaximized();
});
onUnmounted(() => {
ipcRenderer.removeListener('window-maximized-status', handleMaximizedStatus);
});
/* 关闭窗口 */
const closeWindow = () => {
ipcRenderer.invoke('closeWindow')
}
/* 最大化最小化窗口 */
const maxmizeMainWin = () => {
ipcRenderer.invoke( isFullScreen.value ? 'unmaximizeWindow' : 'maxmizeWindow')
isFullScreen.value = !isFullScreen.value
}
/* 最小化窗口 */
const minimizeMainWindow = () => {
ipcRenderer.invoke('minimizeWindow')
}
/* 处理粘贴内容 */
const clipboardHandle = (event: any, content: any) => {
globalStore.updateClipboardText(content)
}
</script>
<template>
<div class="wrapper">
<div class="btn close-btn" @click="closeWindow" />
<div v-if="isFullScreen" class="btn disabled" />
<div v-if="!isFullScreen" class="btn min-btn" @click="minimizeMainWindow" />
<div class="btn max-btn" @click="maxmizeMainWin" />
</div>
</template>
<style scoped>
body {
margin: 0;
}
.wrapper {
margin-top: 8px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
}
.btn {
width: 14px;
height: 14px;
border-radius: 50%;
margin-right: 6px;
position: relative;
overflow: hidden;
cursor: pointer;
}
.btn:last-child {
margin-right: 0;
}
.btn:before,
.btn:after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 1px;
opacity: 0;
transition: all 300ms ease-in-out;
}
.close-btn {
background: #FF5D5B;
border: 1px solid #CF544D;
}
.min-btn {
background: #FFBB39;
border: 1px solid #CFA64E;
}
.disabled{
background: #cccccc;
}
.max-btn {
background: #00CD4E;
border: 1px solid #0EA642;
}
/* Close btn */
.close-btn:before,
.close-btn:after {
width: 1px;
height: 70%;
background: #460100;
}
.close-btn:before {
transform: translate(-50%, -50%) rotate(45deg);
}
.close-btn:after {
transform: translate(-50%, -50%) rotate(-45deg);
}
/* min btn */
.min-btn:before {
width: 70%;
height: 1px;
background: #460100;
}
/* max btn */
.max-btn:before {
width: 50%;
height: 50%;
background: #024D0F;
}
.max-btn:after {
width: 1px;
height: 90%;
transform: translate(-50%, -50%) rotate(-135deg);
background: #00CD4E;
}
/* Hover function */
.wrapper:hover .btn:before,
.wrapper:hover .btn:after {
top: 50%;
opacity: 1;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import { PlayBack } from '@vicons/ionicons5'
import { NIcon } from 'naive-ui'
import { useRouter } from 'vue-router'
import { computed } from 'vue'
import { useAppStore } from '@/store'
interface Props {
title?: string
des?: string
padding?: number
}
const props = withDefaults(defineProps<Props>(), {
title: '',
des: '',
padding: 4,
})
const appStore = useAppStore()
const darkMode = computed(() => appStore.theme === 'dark')
const router = useRouter()
</script>
<template>
<div class="flex border-b border-[#ebebeb] dark:border-[#ffffff17] py-4 w-full" :class="[`px-${props.padding}`]">
<div class="pt-1 mr-2 cursor-pointer">
<NIcon size="16" class="text-primary" @click="router.push('/')">
<PlayBack />
</NIcon>
</div>
<div>
<b :class="[darkMode ? 'text-[#fff]' : 'text-[#555]']" class="text-lg ">{{ props.title }}</b>
<div :class="[darkMode ? 'text-[#fff]' : 'text-[#626569]']" class="text-truncate text-[#626569] mt-1">
{{ props.des }}
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,235 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
const props = defineProps({
/* 图片地址 如果不是同源跨域 传入base64 */
src: String,
/* 图片 高度 宽度 不传就是用图片宽高、如果是缩略图 使用尺寸导出到原始尺寸 */
width: Number,
height: Number,
/* 允许的画布最大宽度 限制区域 */
max: {
type: Number,
default: 500,
},
/* 导出蒙版的底色背景色 */
exportMaskBackgroundColor: {
type: String,
default: 'black',
},
/* 导出蒙版的绘制颜色 */
exportMaskColor: {
type: String,
default: 'white',
},
penColor: {
type: String,
default: 'white',
},
penWidth: {
type: Number,
default: 20,
},
updateFileInfo: Function,
})
// TODO 如果动态变更了线宽颜色等 在导出的时候没有记录每一步的线宽 而是使用了最后的
const canvas = ref<any>(null)
const backgroundCanvas = ref<any>(null)
const paths = ref<any>([])
let isDrawing = false
let currentPath: any = []
const baseImage: any = new Image()
const isEraserEnabled = ref(false)
const computedWidth = ref(0)
const computedHeight = ref(0)
const scaleRatio = ref(0)
onMounted(() => {
const ctx: any = canvas.value.getContext('2d')
const backgroundCtx = backgroundCanvas.value?.getContext('2d')
baseImage.src = props.src
baseImage.onload = () => {
const ratio = Math.min(props.max / baseImage.width, props.max / baseImage.height)
scaleRatio.value = ratio
computedWidth.value = props.width || (ratio < 1 ? baseImage.width * ratio : baseImage.width)
computedHeight.value = props.height || (ratio < 1 ? baseImage.height * ratio : baseImage.height)
props.updateFileInfo?.({
width: baseImage.width,
height: baseImage.height,
scaleRatio: ratio.toFixed(3),
})
canvas.value.width = computedWidth.value
backgroundCanvas.value.width = computedWidth.value
canvas.value.height = computedHeight.value
backgroundCanvas.value.height = computedHeight.value
// ctx.drawImage(baseImage, 0, 0, computedWidth.value, computedHeight.value);
backgroundCtx.drawImage(baseImage, 0, 0, computedWidth.value, computedHeight.value)
}
canvas.value.addEventListener('mousedown', startDrawing)
canvas.value.addEventListener('mousemove', draw)
canvas.value.addEventListener('mouseup', stopDrawing)
})
/* 开始绘制 */
const startDrawing = (e: any) => {
isDrawing = true
const ctx = canvas.value.getContext('2d')
ctx.beginPath()
ctx.moveTo(e.offsetX, e.offsetY)
currentPath = [{ type: isEraserEnabled.value ? 'erase' : 'draw', x: e.offsetX, y: e.offsetY }]
}
/* 绘制过程 */
const draw = (e: any) => {
if (!isDrawing)
return
const ctx = canvas.value.getContext('2d')
ctx.lineTo(e.offsetX, e.offsetY)
if (isEraserEnabled.value) {
// 橡皮擦模式:清除画布上的内容
ctx.globalCompositeOperation = 'destination-out'
ctx.lineWidth = props.penWidth * 2 // 橡皮擦宽度可以调整
}
else {
// 正常绘制模式
ctx.globalCompositeOperation = 'source-over'
ctx.strokeStyle = props.penColor
ctx.lineWidth = props.penWidth
}
ctx.stroke()
currentPath.push({ type: isEraserEnabled.value ? 'erase' : 'draw', x: e.offsetX, y: e.offsetY })
}
/* 完成单次绘制 */
const stopDrawing = () => {
isDrawing = false
paths.value.push([...currentPath, { type: 'end' }])
currentPath = []
}
/* 获取Base图片 */
const exportImage = (): Promise<string> => {
return new Promise((resolve, reject) => {
const exportCanvas = document.createElement('canvas')
const image: any = baseImage
exportCanvas.width = image.width
exportCanvas.height = image.height
const exportCtx = exportCanvas.getContext('2d')
if (exportCtx) {
exportCtx.fillStyle = props.exportMaskBackgroundColor
exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height)
exportCtx.beginPath()
const xRatio = image.width / computedWidth.value
const yRatio = image.height / computedHeight.value
exportCtx.beginPath()
paths.value.forEach((pathArr: any[]) => {
pathArr.forEach((path, index) => {
if (path.type === 'begin' || path.type === 'draw') {
if (index === 0 || pathArr[index - 1].type !== path.type)
exportCtx.beginPath()
exportCtx.lineTo(path.x * xRatio, path.y * yRatio)
exportCtx.strokeStyle = props.exportMaskColor
exportCtx.lineWidth = props.penWidth * xRatio
}
if (path.type === 'erase') {
if (index === 0 || pathArr[index - 1].type !== path.type)
exportCtx.beginPath()
exportCtx.lineTo(path.x * xRatio, path.y * yRatio)
exportCtx.strokeStyle = props.exportMaskBackgroundColor // 擦除路径使用的颜色(黑色)
}
// 每当一个 'draw' 或 'erase' 类型的路径结束时,结束当前的路径
if (index < pathArr.length - 1 && pathArr[index + 1].type !== path.type)
exportCtx.stroke()
})
// 如果最后一个路径元素是 'draw' 或 'erase',确保路径被结束
if (pathArr[pathArr.length - 1].type !== 'begin')
exportCtx.stroke()
})
const base64Image = exportCanvas.toDataURL('image/png')
resolve(base64Image)
}
else {
reject(new Error('无法获取canvas的2D渲染上下文'))
}
})
}
/* 清空画布并重置 */
function clear() {
paths.value = []
const ctx = canvas.value.getContext('2d')
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height)
}
/* 获取绘制后的蒙版图片 */
async function getBase() {
return await exportImage()
}
/* 返回上一步 */
function undo() {
if (paths.value.length > 0) {
paths.value.pop()
redrawCanvas()
}
}
/* 重新绘制 */
function redrawCanvas() {
const ctx = canvas.value.getContext('2d')
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height)
ctx.drawImage(baseImage, 0, 0, computedWidth.value, computedHeight.value)
paths.value.forEach((pathArr: any[]) => {
pathArr.forEach((path, index) => {
if (index === 0 || pathArr[index - 1].type !== path.type)
ctx.beginPath()
if (path.type === 'erase') {
ctx.globalCompositeOperation = 'destination-out'
ctx.strokeStyle = 'rgba(0,0,0,0)'
}
else {
ctx.globalCompositeOperation = 'source-over'
ctx.strokeStyle = 'white'
}
ctx.lineWidth = path.type === 'erase' ? props.penWidth * 2 : props.penWidth
ctx.lineTo(path.x, path.y)
ctx.stroke()
if (index === pathArr.length - 1 || pathArr[index + 1].type !== path.type)
ctx.closePath()
})
})
ctx.globalCompositeOperation = 'source-over'
}
/* 切换橡皮擦模式 */
const toggleEraser = () => {
isEraserEnabled.value = !isEraserEnabled.value
}
defineExpose({
getBase,
undo,
clear,
toggleEraser,
})
</script>
<template>
<div class="relative w-full h-full ">
<canvas ref="backgroundCanvas" class="absolute left-0 top-0" :width="width" :height="height" />
<canvas ref="canvas" class="absolute left-0 top-0" :width="width" :height="height" />
</div>
</template>
<style scoped>
canvas {
border: 1px solid #ddd;
}
</style>

View File

@@ -0,0 +1,338 @@
<script lang="ts" setup>
import { onMounted, ref, computed, onUnmounted, getCurrentInstance, watch, nextTick } from 'vue'
import { throttle } from '@/utils/functions/throttle'
import { SvgIcon } from '@/components/common'
import { copyText } from '@/utils/format'
import { useMessage, NPopover } from 'naive-ui'
import { useAuthStore } from '@/store'
import { useRouter } from 'vue-router'
const authStore = useAuthStore()
interface Props {
dataList: FileItem[]
scaleWidth?: number
isDrawLike?: boolean
usePropmpt?: boolean
copyPropmpt?: boolean
gap?: number
preOrigin?: boolean
}
interface FileInfo {
width: number
height: number
cosUrl: string
thumbImg: string
size: string
filename: string
}
interface FileItem {
id: number
fileInfo: FileInfo
prompt: string
fullPrompt?: string
originUrl?: string
}
interface Emit {
(ev: 'loadMore'): void
(ev: 'usePropmptDraw', prompt: string): void
}
const props = withDefaults(defineProps<Props>(),{
gap: 5,
})
const emit = defineEmits<Emit>()
const $viewerApi = getCurrentInstance()?.appContext.config.globalProperties.$viewerApi
const ms = useMessage()
const boxRefs = ref<any>({})
const otherInfoContainerHeight = ref(0)
const realWidth = ref(160)
const realColumn = ref(0)
const loadComplete = ref<number[]>([])
const wapperRef = ref<HTMLDivElement | null>(null)
const wapperHeigth = ref(0)
const isLogin = computed(() => authStore.isLogin)
const width = computed(() => {
return props.scaleWidth? Number(props.scaleWidth) * 2 + props.gap + 150 : 150
});
const router = useRouter()
/* 拿到图片高度 对定位top和right 新的一轮去插入最小值的那一列 贪心算法即可 */
function compilerContainer() {
calcHeight()
compilerColumn()
const columns = realColumn.value
const itemWidth = realWidth.value
const cacheHeight = <any>[]
props.dataList.forEach((item, index) => {
const { width, height } = item.fileInfo
const bi = itemWidth / width
const boxheight = height * bi + props.gap + otherInfoContainerHeight.value
const currentBox = boxRefs.value[item.id]
if (cacheHeight.length < columns) {
currentBox.style.top = '0px'
currentBox.style.left = `${(itemWidth + props.gap) * index}px`
cacheHeight.push(boxheight)
} else {
const minHeight = Math.min.apply(null, cacheHeight)
const minIndex = cacheHeight.findIndex((t: number) => t === minHeight)
currentBox.style.top = `${minHeight + 0}px`
currentBox.style.left = `${minIndex * (realWidth.value + props.gap)}px`
cacheHeight[minIndex] += boxheight
}
})
wapperHeigth.value = Math.max(...cacheHeight) + 100
}
function setItemRefs(el: HTMLDivElement, item: FileItem) {
if (el && item) {
boxRefs.value[item.id] = el;
}
}
/* 通过额外展示的信息计算有没有除了图片意外额外的高度 eg 图片100px 额外显示其他信息30px cacheHeight的高度在图片的基础上需要+30 */
function calcHeight() {
const { showName = 0, showOther = 0 } = {}
otherInfoContainerHeight.value = [showName, showOther].filter(t => t).length * 15
}
watch(() => props.scaleWidth, (val) => {
handleResizeThrottled()
})
watch(() => props.dataList, (val) => {
if (!val) return;
nextTick(() => {
handleResizeThrottled()
})
}, { immediate: true })
/* 计算放多少列比较合理,并计算最终单个图片的宽 */
function compilerColumn() {
if (!wapperRef.value)
return
const containerWidth = wapperRef.value.clientWidth
/* 计算按目前宽度最多可以是几列 */
realColumn.value = Math.floor(containerWidth / width.value)
const surplus = containerWidth - realColumn.value * width.value // 剩下的多余空间
/* 计算如果给了左右间距那么作业间距需要占多少宽度 */
const positionWith = ((realColumn.value - 1) * props.gap) // 设置的right 需要padding的值
/* 总宽度减去right的宽度如果是负数考虑要不要cloumn-1 那么图片真实宽度就会比传入的宽度大 */
if (surplus - positionWith < 0) {
realColumn.value -= 1
}
/* 图片宽度*列 + right的间距 不管大于小于总宽 多的或者少的那部分都平分给列容器 保证总宽是100% */
realWidth.value = Math.floor((containerWidth - positionWith) / realColumn.value)
}
function imgLoadSuccess(e: any, item: FileItem) {
loadComplete.value.push(item.id)
}
function imgLoadError(e: any, item: FileItem) {
loadComplete.value.push(item.id)
}
function handleCopy(item: any) {
if (!isLogin.value) {
return authStore.setLoginDialog(true)
}
const { prompt } = item
copyText({ text: prompt })
ms.success('复制prompt成功')
}
function drawLike(item: any) {
router.push(`/midjourney?mjId=${item.id}`)
}
function usePropmptDraw(item: FileItem){
const { prompt } = item
emit('usePropmptDraw', prompt)
}
function handlePreview(item: any) {
const { fileInfo } = item
const { cosUrl } = fileInfo
$viewerApi({ options: {}, images: [cosUrl] })
}
const realHeight = computed(() => (item) => {
const { fileInfo } = item
const { width, height } = fileInfo
return height / (width / realWidth.value)
})
const handleResizeThrottled = throttle(function (this: any) {
compilerContainer()
}, 200)
onMounted(async () => {
window.addEventListener('resize', handleResizeThrottled)
const container: any = document.getElementById('footer')
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
emit('loadMore')
}
})
})
observer.observe(container)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResizeThrottled)
})
</script>
<template>
<div class=" min-h-full overflow-hidden flex flex-col">
<div class="flex-1 min-h-full p-4 relative">
<div id="wapper" ref="wapperRef" class="wapper" :style="{ height: `${wapperHeigth}px` }">
<div v-for="(item, index) in dataList" :id="item.id" :key="index" :ref="(el) => setItemRefs(el, item)"
class="wapper-item" :style="{ width: `${realWidth}px` }">
<transition name="img" :css="true">
<img :id="item.id" class="item-file rounded-sm"
:style="{ width: `${realWidth}px`, height: `${realHeight(item)}px` }" :src="preOrigin ? item.fileInfo.cosUrl : item.fileInfo.thumbImg"
loading="lazy" @load="imgLoadSuccess($event, item)" @error="imgLoadError($event, item)"
@click="handlePreview(item)" />
</transition>
<div class="menu p-2 text-[#cbd5e1]">
<div class="prompt">
{{ item.fullPrompt }}
</div>
<div class="flex justify-end items-end space-x-2">
<n-popover trigger="hover" v-if="isDrawLike" >
<template #trigger>
<button
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
@click.stop="drawLike(item)">
<span class="text-sm dark:text-slate-400">
<SvgIcon icon="fluent:draw-image-24-regular" class="text-sm" />
</span>
</button>
</template>
<span>画同款</span>
</n-popover>
<n-popover trigger="hover" v-if="usePropmpt" >
<template #trigger>
<button
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
@click.stop="usePropmptDraw(item)">
<span class="text-sm dark:text-slate-400">
<SvgIcon icon="fluent:draw-image-24-regular" class="text-sm" />
</span>
</button>
</template>
<span>使用当前画同款</span>
</n-popover>
<n-popover trigger="hover" v-if="copyPropmpt">
<template #trigger>
<button
class="flex h-5 w-8 items-center justify-center rounded border transition hover:bg-[#666161] dark:border-neutral-700 dark:hover:bg-[#33373c]"
@click.stop="handleCopy(item)">
<span class="text-sm dark:text-slate-400">
<SvgIcon icon="tabler:copy" class="text-sm" />
</span>
</button>
</template>
<span>复制提示词</span>
</n-popover>
</div>
</div>
<div class="item-loading" v-if="!loadComplete.includes(item.id)"
:style="{ width: `${realWidth}px`, height: `${realHeight(item)}px` }"></div>
</div>
<div id="footer" class="w-full absolute bottom-[350px]" />
</div>
</div>
</div>
</template>
<style lang="less">
.market {
}
.wapper {
width: 100%;
position: relative;
height: 100%;
padding-bottom: 20px;
&-item {
z-index: 10;
overflow: hidden;
position: absolute;
transition: all 0.5s;
cursor: pointer;
&:hover {
.menu {
transition: transform 0.3s ease-in-out;
transform: translateY(-10px);
}
img {
transform: scale(1.1);
}
}
.menu {
position: absolute;
bottom: 0;
width: 94%;
left: 3%;
max-height: 70%;
height: 100px;
transform: translateY(100%);
background-color: #090b15;
opacity: 0.8;
transition: all .1s cubic-bezier(0.68, -0.55, 0.265, 1.55);
border-radius: 10px;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
.prompt {
height: 50px;
overflow: hidden;
}
}
img {
user-select: none;
cursor: pointer;
transition: all .6s cubic-bezier(0.19, 1, 0.22, 1);
border-radius: 6px;
}
.item-loading {
background: url(../../assets/img-bg.png) no-repeat center center;
filter: blur(20px);
position: absolute;
top: 0;
}
}
}
.img-enter-active,
.img-leave-active {
transition: transform .3s;
}
.img-enter,
.img-leave-to {
transform: scale(.6);
opacity: 0;
}
</style>

View File

@@ -0,0 +1,20 @@
<script setup lang='ts'>
interface Emit {
(e: 'click'): void
}
const emit = defineEmits<Emit>()
function handleClick() {
emit('click')
}
</script>
<template>
<button
class="flex items-center justify-center w-10 h-8 transition rounded-md hover:bg-neutral-100 dark:hover:bg-[#414755]"
@click="handleClick"
>
<slot />
</button>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang='ts'>
import { computed } from 'vue'
import type { PopoverPlacement } from 'naive-ui'
import { NTooltip } from 'naive-ui'
import Button from './Button.vue'
interface Props {
tooltip?: string
placement?: PopoverPlacement
}
interface Emit {
(e: 'click'): void
}
const props = withDefaults(defineProps<Props>(), {
tooltip: '',
placement: 'bottom',
})
const emit = defineEmits<Emit>()
const showTooltip = computed(() => Boolean(props.tooltip))
function handleClick() {
emit('click')
}
</script>
<template>
<div v-if="showTooltip">
<NTooltip :placement="placement" trigger="hover">
<template #trigger>
<Button @click="handleClick">
<slot />
</Button>
</template>
{{ tooltip }}
</NTooltip>
</div>
<div v-else>
<Button @click="handleClick">
<slot />
</Button>
</div>
</template>

View File

@@ -0,0 +1,217 @@
<template>
<div>
<canvas ref="canvas" @click="handleClick" crossOrigin="anonymous"></canvas>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
// 定义接收的属性
const props = defineProps({
src: String,
selectColor: String,
maxSteps: Number,
updateFileInfo: Function
});
const canvas = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | null>(null);
let modifiedPixels = new Set<string>();
const history = ref<ImageData[]>([]);
const maxHistorySteps = ref(10)
watch(() => props.maxSteps, (val) => {
val && (maxHistorySteps.value = val)
}, { immediate: true })
// 初始化canvas
onMounted(() => {
if (canvas.value) {
ctx.value = canvas.value.getContext('2d', { willReadFrequently: true });
initCanvas();
}
});
// 监听src属性变化
watch(() => props.src, (newSrc) => {
if (newSrc) {
initCanvas();
}
});
// 初始化Canvas函数
function initCanvas(){
if (!ctx.value || !props.src) return;
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
canvas.value!.width = img.width;
canvas.value!.height = img.height;
props.updateFileInfo?.({
width: img.width,
height: img.height,
scaleRatio: 1,
})
ctx.value!.drawImage(img, 0, 0, img.width, img.height);
};
img.src = props.src;
};
// 获取下标
function pointToIndex (x: number, y: number) {
return (y * canvas.value!.width + x) * 4;
};
// 获取颜色
function getColor(x: number, y: number, imgData: Uint8ClampedArray) {
const i = pointToIndex(x, y);
return [
imgData[i],
imgData[i + 1],
imgData[i + 2],
imgData[i + 3],
];
};
function diff (color1: number[], color2: number[]) {
const sum = color1.reduce((sum, value, index) => sum + Math.abs(value - color2[index]), 0);
return sum;
};
// 修改颜色
function changeColor (initX: number, initY: number, targetColor: number[], clickColor: number[], imgData: Uint8ClampedArray){
// 保存当前状态
if (ctx.value && canvas.value) {
const currentImageData = ctx.value.getImageData(0, 0, canvas.value.width, canvas.value.height);
addAction(currentImageData)
}
const queue = [[initX, initY]];
while(queue.length) {
const [x, y] = queue.shift()!;
if(x < 0 || x >= canvas.value!.width || y < 0 || y >= canvas.value!.height) continue;
const curColor = getColor(x, y, imgData);
if(diff(curColor, clickColor) > 50) continue;
if(diff(curColor, targetColor) === 0) continue;
const i = pointToIndex(x, y);
imgData.set(targetColor, i);
modifiedPixels.add(x + "," + y);
queue.push([x+1, y]);
queue.push([x-1, y]);
queue.push([x, y+1]);
queue.push([x, y-1]);
}
};
/* 点选图片换色 */
function handleClick(e: MouseEvent){
if (!ctx.value || !canvas.value) return;
const x = e.offsetX;
const y = e.offsetY;
const imageData = ctx.value.getImageData(0, 0, canvas.value.width, canvas.value.height);
const clickColor = getColor(x, y, imageData.data);
const color = parseColor(props.selectColor);
changeColor(x, y, color, clickColor, imageData.data);
ctx.value.putImageData(imageData, 0, 0);
};
/* 获得base64 */
function exportToBase64WithCustomBackground() {
if (!ctx.value || !canvas.value) return '';
const originalImageData = ctx.value.getImageData(0, 0, canvas.value.width, canvas.value.height);
const copiedData = new Uint8ClampedArray(originalImageData.data);
for (let y = 0; y < canvas.value.height; y++) {
for (let x = 0; x < canvas.value.width; x++) {
const i = pointToIndex(x, y);
if (modifiedPixels.has(x + "," + y)) {
copiedData[i] = 255;
copiedData[i + 1] = 255;
copiedData[i + 2] = 255;
} else {
copiedData[i] = 0;
copiedData[i + 1] = 0;
copiedData[i + 2] = 0;
}
copiedData[i + 3] = 255;
}
}
const newImageData = new ImageData(copiedData, canvas.value.width, canvas.value.height);
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvas.value.width;
tempCanvas.height = canvas.value.height;
const tempCtx = tempCanvas.getContext('2d')!;
tempCtx.putImageData(newImageData, 0, 0);
return tempCanvas.toDataURL("image/png");
};
/* 格式化传入颜色 */
function parseColor(selectColor: string): number[]{
if (selectColor && selectColor.startsWith('#')) {
const extendedHex = selectColor.length === 4 ? '#' + selectColor[1] + selectColor[1] + selectColor[2] + selectColor[2] + selectColor[3] + selectColor[3] : selectColor;
const r = parseInt(extendedHex.slice(1, 3), 16);
const g = parseInt(extendedHex.slice(3, 5), 16);
const b = parseInt(extendedHex.slice(5, 7), 16);
return [r, g, b, 255];
}
else if (selectColor && selectColor.startsWith('rgb')) {
const rgbValues = selectColor
.replace(/rgba?\(/, '')
.replace(/\)/, '')
.split(',')
.map((num) => parseInt(num));
if (rgbValues.length === 3) rgbValues.push(255); // 如果没有 alpha添加一个默认的不透明度
return rgbValues;
}
return [0, 0, 0, 255];
};
/* 对外提供base64格式的遮罩 */
async function getBase(){
return await exportToBase64WithCustomBackground()
}
/* 返回上一步 */
function undo() {
if(history.value.length === 0 || !ctx.value || !canvas.value) return;
const previousState = history.value.pop();
ctx.value.putImageData(previousState.imageData, 0, 0);
modifiedPixels = new Set(previousState.currentModifiedPixels);
}
/* 清空画布 */
function clear() {
if (!ctx.value || !canvas.value) return;
// 直接清空画布
ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height);
// 重置修改记录和历史记录
modifiedPixels.clear();
history.value = []; // 如果您想保留初始状态,可以重置为 [initialState]
initCanvas();
}
/* 设置记录栈最大存储步数 防止存储过多 */
function setMaxHistorySteps(steps) {
maxHistorySteps.value = steps;
}
/* 检测、超出限制移除老的数据 */
function addAction(imageData) {
const currentModifiedPixels = new Set(modifiedPixels); // 创建 modifiedPixels 的一个副本
history.value.push({ imageData, currentModifiedPixels }); // 保存 imageData 和 modifiedPixels
if (history.value.length > maxHistorySteps.value) {
history.value.shift();
}
}
defineExpose({
getBase,
undo,
clear
})
</script>
<style>
/* 这里可以添加样式 */
</style>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { defineComponent, h } from 'vue'
import {
NDialogProvider,
NLoadingBarProvider,
NMessageProvider,
NNotificationProvider,
useDialog,
useLoadingBar,
useMessage,
useNotification,
} from 'naive-ui'
function registerNaiveTools() {
window.$loadingBar = useLoadingBar()
window.$dialog = useDialog()
window.$message = useMessage()
window.$notification = useNotification()
}
const NaiveProviderContent = defineComponent({
name: 'NaiveProviderContent',
setup() {
registerNaiveTools()
},
render() {
return h('div')
},
})
</script>
<template>
<NLoadingBarProvider>
<NDialogProvider>
<NNotificationProvider>
<NMessageProvider>
<slot />
<NaiveProviderContent />
</NMessageProvider>
</NNotificationProvider>
</NDialogProvider>
</NLoadingBarProvider>
</template>

View File

@@ -0,0 +1,73 @@
<script setup lang="ts">
import { useQRCode } from '@vueuse/integrations/useQRCode'
/*
参考文档https://vueuse.org/integrations/useQRCode/
https://www.npmjs.com/package/qrcode#qr-code-options
*/
interface Props {
value?: string // 扫描后的文本或地址
size?: number // 二维码大小
color?: string // 二维码颜色Value must be in hex format (十六进制颜色值)
backgroundColor?: string // 二维码背景色Value must be in hex format (十六进制颜色值)
bordered?: boolean // 是否有边框
borderColor?: string // 边框颜色
scale?: number // 每个black dots多少像素
/*
纠错等级也叫纠错率,就是指二维码可以被遮挡后还能正常扫描,而这个能被遮挡的最大面积就是纠错率。
通常情况下二维码分为 4 个纠错级别L级 可纠正约 7% 错误、M级 可纠正约 15% 错误、Q级 可纠正约 25% 错误、H级 可纠正约30% 错误。
并不是所有位置都可以缺损,像最明显的三个角上的方框,直接影响初始定位。中间零散的部分是内容编码,可以容忍缺损。
当二维码的内容编码携带信息比较少的时候,也就是链接比较短的时候,设置不同的纠错等级,生成的图片不会发生变化。
*/
errorLevel?: string // 二维码纠错等级
}
const props = withDefaults(defineProps<Props>(), {
value: '',
size: 160,
color: '#000',
backgroundColor: '#FFF',
bordered: true,
borderColor: '#0505050f',
scale: 8,
errorLevel: 'H', // 可选 L M Q H
})
// `qrcode` will be a ref of data URL
const qrcode = useQRCode(props.value, {
errorCorrectionLevel: props.errorLevel,
type: 'image/png',
quality: 1,
margin: 3,
scale: props.scale, // 8px per modules(black dots)
color: {
dark: props.color, // 像素点颜色
light: props.backgroundColor, // 背景色
},
})
</script>
<template>
<div class="m-qrcode" :class="{ bordered }" :style="`width: ${size}px; height: ${size}px; border-color: ${borderColor};`">
<img :src="qrcode" class="u-qrcode" alt="QRCode">
</div>
</template>
<style lang="less" scoped>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.m-qrcode {
display: inline-block;
border-radius: 8px;
overflow: hidden;
.u-qrcode {
width: 100%;
height: 100%;
}
}
.bordered {
border-width: 1px;
border-style: solid;
}
</style>

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { NButton, NInput, useMessage } from 'naive-ui'
import { useSettingStore } from '@/store'
import type { SettingsState } from '@/store/modules/settings/helper'
import { t } from '@/locales'
const settingStore = useSettingStore()
const ms = useMessage()
const systemMessage = ref(settingStore.systemMessage ?? '')
function updateSettings(options: Partial<SettingsState>) {
settingStore.updateSetting(options)
ms.success(t('common.success'))
}
function handleReset() {
settingStore.resetSetting()
ms.success(t('common.success'))
window.location.reload()
}
</script>
<template>
<div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6">
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.role') }}</span>
<div class="flex-1">
<NInput v-model:value="systemMessage" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" />
</div>
<NButton size="tiny" text type="primary" @click="updateSettings({ systemMessage })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">&nbsp;</span>
<NButton size="small" @click="handleReset">
{{ $t('common.reset') }}
</NButton>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,213 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { NButton, NInput, NPopconfirm, NSelect, useMessage } from 'naive-ui'
import type { Language, Theme } from '@/store/modules/app/helper'
import { SvgIcon } from '@/components/common'
import { useAppStore, useAuthStore } from '@/store'
import { getCurrentDate } from '@/utils/functions'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { t } from '@/locales'
import { fetchUpdateInfoAPI } from '@/api/index'
import type { ResData } from '@/api/types'
const appStore = useAppStore()
const authStore = useAuthStore()
const { isMobile } = useBasicLayout()
const ms = useMessage()
const theme = computed(() => appStore.theme)
const userInfo = computed(() => authStore.userInfo)
const avatar = ref(userInfo.value.avatar ?? '')
const username = ref(userInfo.value.username ?? '')
const btnDisabled = ref(false)
const language = computed({
get() {
return appStore.language
},
set(value: Language) {
appStore.setLanguage(value)
},
})
const themeOptions: { label: string; key: Theme; icon: string }[] = [
{
label: 'Auto',
key: 'auto',
icon: 'ri:contrast-line',
},
{
label: 'Light',
key: 'light',
icon: 'ri:sun-foggy-line',
},
{
label: 'Dark',
key: 'dark',
icon: 'ri:moon-foggy-line',
},
]
const languageOptions: { label: string; key: Language; value: Language }[] = [
{ label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
// { label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
// { label: 'English', key: 'en-US', value: 'en-US' },
]
async function updateUserInfo(options: { avatar?: string; username?: string }) {
try {
btnDisabled.value = true
const res: ResData = await fetchUpdateInfoAPI(options)
btnDisabled.value = false
if (!res.success)
return ms.error(res.message)
ms.success(t('common.updateUserSuccess'))
authStore.getUserInfo()
}
catch (error) {
btnDisabled.value = false
}
}
function exportData(): void {
const date = getCurrentDate()
const data: string = localStorage.getItem('chatStorage') || '{}'
const jsonString: string = JSON.stringify(JSON.parse(data), null, 2)
const blob: Blob = new Blob([jsonString], { type: 'application/json' })
const url: string = URL.createObjectURL(blob)
const link: HTMLAnchorElement = document.createElement('a')
link.href = url
link.download = `chat-store_${date}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
function importData(event: Event): void {
const target = event.target as HTMLInputElement
if (!target || !target.files)
return
const file: File = target.files[0]
if (!file)
return
const reader: FileReader = new FileReader()
reader.onload = () => {
try {
const data = JSON.parse(reader.result as string)
localStorage.setItem('chatStorage', JSON.stringify(data))
ms.success(t('common.success'))
location.reload()
}
catch (error) {
ms.error(t('common.invalidFileFormat'))
}
}
reader.readAsText(file)
}
function clearData(): void {
localStorage.removeItem('chatStorage')
location.reload()
}
function handleImportButtonClick(): void {
const fileInput = document.getElementById('fileInput') as HTMLElement
if (fileInput)
fileInput.click()
}
</script>
<template>
<div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6">
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatarLink') }}</span>
<div class="flex-1">
<NInput v-model:value="avatar" placeholder="请填写头像地址" />
</div>
<NButton size="tiny" :disabled="btnDisabled" text type="primary" @click="updateUserInfo({ avatar })">
{{ $t('common.update') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.name') }}</span>
<div class="w-[200px]">
<NInput v-model:value="username" placeholder="请填写用户名" />
</div>
<NButton size="tiny" :disabled="btnDisabled" text type="primary" @click="updateUserInfo({ username })">
{{ $t('common.update') }}
</NButton>
</div>
<div
class="flex items-center space-x-4"
:class="isMobile && 'items-start'"
>
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.chatHistory') }}</span>
<div class="flex flex-wrap items-center gap-4">
<NButton size="small" @click="exportData">
<template #icon>
<SvgIcon icon="ri:download-2-fill" />
</template>
{{ $t('common.export') }}
</NButton>
<input id="fileInput" type="file" style="display:none" @change="importData">
<NButton size="small" @click="handleImportButtonClick">
<template #icon>
<SvgIcon icon="ri:upload-2-fill" />
</template>
{{ $t('common.import') }}
</NButton>
<NPopconfirm placement="bottom" @positive-click="clearData">
<template #trigger>
<NButton size="small">
<template #icon>
<SvgIcon icon="ri:close-circle-line" />
</template>
{{ $t('common.clear') }}
</NButton>
</template>
{{ $t('chat.clearHistoryConfirm') }}
</NPopconfirm>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.theme') }}</span>
<div class="flex flex-wrap items-center gap-4">
<template v-for="item of themeOptions" :key="item.key">
<NButton
size="small"
:type="item.key === theme ? 'primary' : undefined"
@click="appStore.setTheme(item.key)"
>
<template #icon>
<SvgIcon :icon="item.icon" />
</template>
</NButton>
</template>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.language') }}</span>
<div class="flex flex-wrap items-center gap-4">
<NSelect
style="width: 140px"
:value="language"
:options="languageOptions"
@update-value="value => appStore.setLanguage(value)"
/>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,66 @@
<script setup lang='ts'>
import { onMounted, ref } from 'vue'
import { NSpin } from 'naive-ui'
import { useAuthStore } from '@/store'
const authStore = useAuthStore()
const { userInfo, userBalance } = authStore
const loading = ref(false)
onMounted(async () => {
getInfo()
})
async function getInfo() {
try {
loading.value = true
await authStore.getUserInfo()
loading.value = false
}
catch (error) {
loading.value = false
}
}
</script>
<template>
<NSpin :show="loading">
<div class="p-4 space-y-5 min-h-[200px]">
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">用户邮箱</span>
<div class="w-[200px]">
{{ userInfo.email || "--" }}
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">用户姓名</span>
<div class="w-[200px]">
{{ userInfo.username || "--" }}
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">问答余额</span>
<div class="w-[200px]">
{{ userBalance.usesLeft || "0" }} 积分
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">绘画余额</span>
<div class="w-[200px]">
{{ userBalance.paintCount || "0" }} 积分
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">MJToken</span>
<div class="w-[200px]">
{{ userBalance.balance || "0" }} Token
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">使用金额</span>
<div class="w-[200px]">
{{ userBalance.useTokens || "0" }} Token
</div>
</div>
</div>
</NSpin>
</template>

View File

@@ -0,0 +1,64 @@
<script setup lang='ts'>
import { computed, ref } from 'vue'
import { NModal, NTabPane, NTabs } from 'naive-ui'
import General from './General.vue'
import Personal from './Personal.vue'
import { SvgIcon } from '@/components/common'
interface Props {
visible: boolean
}
interface Emit {
(e: 'update:visible', visible: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const active = ref('personalInfo')
const show = computed({
get() {
return props.visible
},
set(visible: boolean) {
emit('update:visible', visible)
},
})
</script>
<template>
<NModal v-model:show="show" title="个人中心" :auto-focus="false" preset="card" style="width: 95%; max-width: 640px">
<div>
<NTabs v-model:value="active" type="line" animated>
<NTabPane name="personalInfo" tab="personalInfo">
<template #tab>
<SvgIcon class="text-lg" icon="ri:file-user-line" />
<span class="ml-2">{{ $t('setting.personalInfo') }}</span>
</template>
<Personal />
</NTabPane>
<NTabPane name="General" tab="General">
<template #tab>
<SvgIcon class="text-lg" icon="ri:list-settings-line" />
<span class="ml-2">{{ $t('setting.general') }}</span>
</template>
<div class="min-h-[100px]">
<General />
</div>
</NTabPane>
<!-- <NTabPane name="Advanced" tab="Advanced">
<template #tab>
<SvgIcon class="text-lg" icon="ri:equalizer-line" />
<span class="ml-2">{{ $t('setting.advanced') }}</span>
</template>
<div class="min-h-[100px]">
<Advanced />
</div>
</NTabPane> -->
</NTabs>
</div>
</NModal>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang='ts'>
import { computed, useAttrs } from 'vue'
import { Icon } from '@iconify/vue'
interface Props {
icon?: string
}
defineProps<Props>()
const attrs = useAttrs()
const bindAttrs = computed<{ class: string; style: string }>(() => ({
class: (attrs.class as string) || '',
style: (attrs.style as string) || 'width: 1em, height: 1em',
}))
</script>
<template>
<Icon :icon="icon" v-bind="bindAttrs" />
</template>

Some files were not shown because too many files have changed in this diff Show More