引入tailwind css,调整样式

This commit is contained in:
RockYang
2024-12-24 11:07:04 +08:00
parent fb403bde8b
commit 274cff71b1
51 changed files with 1892 additions and 2393 deletions

View File

@@ -3,9 +3,9 @@
## v4.1.8
- 功能优化:**UI 全新改版,支持主题切换**。 :rocket: :rocket: :rocket:
- Bug 修复:修复音 Luma API 更新导致任务响应解析失败的错误
- Bug修复修复音 Luma API 更新导致任务响应解析失败的错误
- 功能优化:支持 Suno v4.0 模型支持
- Bug 修复:修复 Suno 已完成任务删除失败的 错误
- Bug修复修复 Suno 已完成任务删除失败的 错误
- 功能新增:支持 OpenAI 实时语音通话功能,目前已经支持按次收费,支持管理员设置每次实时语音通话的算力消耗
## v4.1.7

588
web/package-lock.json generated
View File

@@ -45,10 +45,13 @@
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"autoprefixer": "^10.4.20",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"postcss": "^8.4.49",
"stylus": "^0.58.1",
"stylus-loader": "^7.0.0",
"tailwindcss": "^3.4.17",
"webpack": "^5.90.3"
}
},
@@ -66,6 +69,18 @@
"node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -1850,6 +1865,102 @@
"deprecated": "Use @eslint/object-schema instead",
"dev": true
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
@@ -2027,6 +2138,16 @@
"ws": "^8.18.0"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.28",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
@@ -3723,6 +3844,12 @@
}
]
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -3805,7 +3932,7 @@
},
"node_modules/autoprefixer": {
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
"resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.20.tgz",
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
"dev": true,
"funding": [
@@ -4232,6 +4359,15 @@
"node": ">=6"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -5722,6 +5858,12 @@
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"dev": true
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
@@ -5739,6 +5881,12 @@
"node": ">=8"
}
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
},
"node_modules/dns-packet": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
@@ -5862,6 +6010,12 @@
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
"dev": true
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/easy-stack": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz",
@@ -6941,6 +7095,34 @@
}
}
},
"node_modules/foreground-child": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.0.tgz",
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/foreground-child/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
@@ -7851,6 +8033,21 @@
"node": ">=0.10.0"
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/javascript-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
@@ -7886,6 +8083,15 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"bin": {
"jiti": "bin/jiti.js"
}
},
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
@@ -9219,6 +9425,15 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
@@ -9445,6 +9660,12 @@
"node": ">=6"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true
},
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -9555,6 +9776,37 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true
},
"node_modules/path-scurry/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
@@ -9587,6 +9839,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pinia": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.6.tgz",
@@ -9637,6 +9898,15 @@
}
}
},
"node_modules/pirates": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.6.tgz",
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -9682,7 +9952,7 @@
},
"node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"funding": [
{
@@ -9802,6 +10072,101 @@
"postcss": "^8.2.15"
}
},
"node_modules/postcss-import": {
"version": "15.1.0",
"resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
"resolve": "^1.1.7"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/postcss-js": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"dependencies": {
"camelcase-css": "^2.0.1"
},
"engines": {
"node": "^12 || ^14 || >= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": "^8.4.21"
}
},
"node_modules/postcss-load-config": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"lilconfig": "^3.0.0",
"yaml": "^2.3.4"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"postcss": ">=8.0.9",
"ts-node": ">=9.0.0"
},
"peerDependenciesMeta": {
"postcss": {
"optional": true
},
"ts-node": {
"optional": true
}
}
},
"node_modules/postcss-load-config/node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/postcss-load-config/node_modules/yaml": {
"version": "2.6.1",
"resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.6.1.tgz",
"integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==",
"dev": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/postcss-loader": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz",
@@ -10019,6 +10384,31 @@
"postcss": "^8.1.0"
}
},
"node_modules/postcss-nested": {
"version": "6.2.0",
"resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"postcss-selector-parser": "^6.1.1"
},
"engines": {
"node": ">=12.0"
},
"peerDependencies": {
"postcss": "^8.2.14"
}
},
"node_modules/postcss-normalize-charset": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz",
@@ -10599,6 +10989,15 @@
"node": ">=0.10.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"dependencies": {
"pify": "^2.3.0"
}
},
"node_modules/read-pkg": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -11504,6 +11903,21 @@
"node": ">=8"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -11515,6 +11929,19 @@
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-eof": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
@@ -11619,6 +12046,90 @@
"node": ">= 8"
}
},
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
"glob": "^10.3.10",
"lines-and-columns": "^1.1.6",
"mz": "^2.7.0",
"pirates": "^4.0.1",
"ts-interface-checker": "^0.1.9"
},
"bin": {
"sucrase": "bin/sucrase",
"sucrase-node": "bin/sucrase-node"
},
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/sucrase/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/sucrase/node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/sucrase/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sucrase/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sucrase/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -11717,6 +12228,55 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/tailwindcss": {
"version": "3.4.17",
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.6.0",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.6",
"lilconfig": "^3.1.3",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.1.1",
"postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.2",
"postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8",
"sucrase": "^3.35.0"
},
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tailwindcss/node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -11961,6 +12521,12 @@
"tslib": "2"
}
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true
},
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
@@ -12925,6 +13491,24 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@@ -45,10 +45,13 @@
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"autoprefixer": "^10.4.20",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"postcss": "^8.4.49",
"stylus": "^0.58.1",
"stylus-loader": "^7.0.0",
"tailwindcss": "^3.4.17",
"webpack": "^5.90.3"
},
"eslintConfig": {

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

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

View File

@@ -1,29 +1,29 @@
<template>
<el-config-provider>
<router-view/>
<router-view />
</el-config-provider>
</template>
<script setup>
import {ElConfigProvider} from 'element-plus';
import {onMounted, ref, watch} from "vue";
import {checkSession, getClientId, getSystemInfo} from "@/store/cache";
import {isChrome, isMobile} from "@/utils/libs";
import {showMessageInfo} from "@/utils/dialog";
import {useSharedStore} from "@/store/sharedata";
import {getUserToken} from "@/store/session";
import { ElConfigProvider } from "element-plus";
import { onMounted, ref, watch } from "vue";
import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
import { isChrome, isMobile } from "@/utils/libs";
import { showMessageInfo } from "@/utils/dialog";
import { useSharedStore } from "@/store/sharedata";
import { getUserToken } from "@/store/session";
const debounce = (fn, delay) => {
let timer
let timer;
return (...args) => {
if (timer) {
clearTimeout(timer)
clearTimeout(timer);
}
timer = setTimeout(() => {
fn(...args)
}, delay)
}
}
fn(...args);
}, delay);
};
};
const _ResizeObserver = window.ResizeObserver;
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
@@ -31,63 +31,69 @@ window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
callback = debounce(callback, 200);
super(callback);
}
}
};
const store = useSharedStore()
const store = useSharedStore();
onMounted(() => {
// 获取系统参数
getSystemInfo().then((res) => {
const link = document.createElement('link')
link.rel = 'shortcut icon'
link.href = res.data.logo
document.head.appendChild(link)
})
const link = document.createElement("link");
link.rel = "shortcut icon";
link.href = res.data.logo;
document.head.appendChild(link);
});
if (!isChrome() && !isMobile()) {
showMessageInfo("建议使用 Chrome 浏览器以获得最佳体验。")
showMessageInfo("建议使用 Chrome 浏览器以获得最佳体验。");
}
checkSession().then(() => {
store.setIsLogin(true)
}).catch(()=>{})
})
checkSession()
.then(() => {
store.setIsLogin(true);
})
.catch(() => {});
watch(() => store.isLogin, (val) => {
if (val) {
connect()
}
})
// 设置主题
document.documentElement.setAttribute("data-theme", store.theme);
});
const handler = ref(0)
// 初始化 websocket 连接
const connect = () => {
let host = process.env.VUE_APP_WS_HOST
if (host === '') {
if (location.protocol === 'https:') {
host = 'wss://' + location.host;
} else {
host = 'ws://' + location.host;
watch(
() => store.isLogin,
(val) => {
if (val) {
connect();
}
}
const clientId = getClientId()
const _socket = new WebSocket(host + `/api/ws?client_id=${clientId}`,["token",getUserToken()]);
_socket.addEventListener('open', () => {
console.log('WebSocket 连接')
);
const handler = ref(0);
// 初始化 websocket 连接
const connect = () => {
let host = process.env.VUE_APP_WS_HOST;
if (host === "") {
if (location.protocol === "https:") {
host = "wss://" + location.host;
} else {
host = "ws://" + location.host;
}
}
const clientId = getClientId();
const _socket = new WebSocket(host + `/api/ws?client_id=${clientId}`, ["token", getUserToken()]);
_socket.addEventListener("open", () => {
console.log("WebSocket 已连接");
handler.value = setInterval(() => {
if (_socket.readyState === WebSocket.OPEN) {
_socket.send(JSON.stringify({"type":"ping"}))
_socket.send(JSON.stringify({ type: "ping" }));
}
},5000)
})
_socket.addEventListener('close', () => {
clearInterval(handler.value)
connect()
}, 5000);
});
store.setSocket(_socket)
}
_socket.addEventListener("close", () => {
clearInterval(handler.value);
connect();
});
store.setSocket(_socket);
};
</script>
<style lang="stylus">
html, body {
margin: 0;
@@ -137,4 +143,5 @@ html, body {
color #07C160
}
@import '@/assets/iconfont/iconfont.css'
</style>

View File

@@ -16,29 +16,18 @@
height: 100%;
}
.el-aside {
//background-color: $sideBgColor;
padding 10px
width var(--el-aside-width, 320px)
.media-page {
display: flex
flex-flow: column
//background-color: $sideBgColor
border-radius 10px
padding 10px 0
.search-box {
flex-wrap: wrap
margin-bottom: 10px
// padding: 10px 0;
// .search-input {
// --el-input-bg-color: #363535
// --el-input-border-color: #464545
// --el-input-focus-border-color:#b0a0f8
// --el-input-hover-border-color: #2DA39A
// box-shadow: none
// }
}
//
@@ -58,16 +47,14 @@
width: 100%
justify-content: flex-start
padding: 8px 12px
//border-bottom: 1px solid #3c3c3c
//border: 1px solid #3c3c3c
cursor: pointer
// border: 1px solid #3c3c3c
border: 1px solid var(--theme-bg-color)
margin-bottom 6px
border-radius 5px
&:hover {
// background-color :rgba(239, 241, 246, 0.64);
border: 1px solid var(--border-active);
border: 1px solid var(--border-active);
}
.avatar {
@@ -115,10 +102,9 @@
}
.chat-list-item.active {
background-color :var(--theme-bg);
box-shadow: 0px 3px 9px rgba(112,144,176,0.12);
border: 1px solid var(--shadow-color);
background-color :var(--theme-bg);
box-shadow: 0 3px 9px rgba(112, 144, 176, 0.12);
border: 1px solid var(--border-active);
}
}
}
@@ -148,7 +134,7 @@
color var(--el-text-color-primary)
.chat-config {
height 30px
height 50px
padding 10px 30px
display flex
justify-content center

View File

@@ -3,7 +3,7 @@
--text-secondary: #8a939d;
--el-color-primary: rgb(107, 80, 225);
--theme-textcolor-normal:#b0a0f8;
--el-border-radius-base: 8px;
--el-border-radius-base: 5px;
--el-color-primary-light-5:rgb(107, 85, 255);
--el-color-primary-light-3:rgb(78, 51, 254);
--theme-btn-color:rgba(117, 81, 255, 1)
@@ -35,7 +35,8 @@
// #e7e7e8
}
.el-dialog{
--el-border-radius-base: 20px;
//--el-border-radius-base: calc(var(--el-component-size) / 2);
--el-dialog-border-radius: 10px
}
.login-box{
--el-component-size: 48px;
@@ -90,7 +91,11 @@
line-height: 28px;
}
.el-button--primary{
border-radius: 8px;
border-radius: 5px;
}
.el-button {
height auto
}
/* */
::-webkit-scrollbar {
@@ -117,10 +122,10 @@
}
//.el-message-box
.el-message-box{
--el-messagebox-border-radius:18px
--el-messagebox-border-radius: 10px
}
.el-message-box__container{
border-top: 1px solid #dbd3f4;
//border-top: 1px solid #dbd3f4;
padding-top: 7px;
.el-message-box__message{
--text-color:var(--theme-text-color-primary)

View File

@@ -43,6 +43,20 @@
display flex
align-items center
flex-direction column
.icon-expand {
font-size 24px
margin-bottom 10px
cursor pointer
color var(--text-color)
}
.icon-colspan {
font-size 18px
margin-left 3px
cursor pointer
color var(--text-color)
}
}
.menu-list-collapse{
.flex-center-col{
@@ -63,14 +77,9 @@
background: transparent !important;
}
}
.el-icon{
margin: 0 4px;
width 26px !important;
height: 26px !important;
}
.menu-title{
font-size: 15px !important;
margin-bottom: 0px !important;
margin-bottom: 0 !important;
}
}
@@ -117,11 +126,15 @@
}
&.active{
color: var(--text-color);
font-weight: 600;
filter: none !important;
.el-icon{
background: rgba(79, 89, 102, .122);
}
filter: invert(100%);
}
}
}
.bot{

View File

@@ -82,7 +82,6 @@
.task-list-box {
background: var(--chat-bg);
width 100%
padding 10px
color var(--text-theme-color)
overflow-x hidden

View File

@@ -195,7 +195,7 @@
.task-list-box {
background: var(--chat-bg);
width 100%
padding 0 10px 10px 10px
//padding 0 10px 10px 10px
color var(--text-theme-color)
overflow-x hidden

View File

@@ -84,7 +84,6 @@
.task-list-box {
background: var(--chat-bg);
width 100%
padding 0 10px 10px 10px
color: var(--text-theme-color)
overflow-x hidden

View File

@@ -1,5 +1,3 @@
.index-page {
margin: 0
overflow hidden
@@ -7,9 +5,8 @@
display flex
justify-content center
align-items baseline
padding-top 158px
min-height: 75vh
background: var(--theme-bg) !important
height: 100vh

View File

@@ -251,7 +251,6 @@
.text {
margin-right 10px
color #000
}
}
@@ -352,7 +351,8 @@
border-radius 5px
padding 5px 10px
cursor pointer
color #000
color: var(--theme-text-color-primary)
background-color var(--btn-bg)
&:hover {
opacity: 0.7

View File

@@ -170,6 +170,14 @@ body {
.content-collapse {
left: 65px;
}
.el-table {
width: 100%;
.el-table__body-header {
height 40px
}
}
}
.w-100 {

View File

@@ -146,7 +146,7 @@
font-size 12px
padding 2px 5px
background-color var(--sm-btn-bg)
color: #fff
color: #fff
}
}
@@ -193,7 +193,7 @@
.list-box {
padding 0 0 0 20px
padding 20px
.item {
display flex
flex-flow row
@@ -301,11 +301,13 @@
.right {
min-width 350px;
font-size 14px
padding 0 15px
padding 0 0 0 15px
display flex
justify-content right
.tools {
display flex
justify-content left
justify-content right
align-items center
flex-flow row
height 90px
@@ -313,9 +315,9 @@
.btn-publish {
padding 2px 10px
.text {
margin-right 10px
}
// .text {
// margin-right 10px
// }
}
.btn-icon {
@@ -395,11 +397,12 @@
.btn {
margin-right 10px
color: #000
color: var((--theme-text-color-primary))
border none
border-radius 5px
padding 5px 10px
cursor pointer
background: var(--btn-bg)
&:hover {

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -4,7 +4,7 @@
--text-fb:#fff;
--text-color: rgba(255, 255, 255, 1) !important; //
--normal-color: rgba(163, 174, 208, 1); //
--el-text-color-primary: #fff;
--el-text-color-primary: #fff;
p, h1, h2, h3, h4, h5, h6, article {
// color: var(--text-color) !important;
font-family: $font-regular;
@@ -25,6 +25,7 @@
--card-bg:#252d58;
--card-bg-table: rgba(17, 28, 68, 1);
--theme-bg:rgb(13, 20, 53);
--theme-bg-color: rgb(13, 20, 53);
--theme-bg-all:rgb(13, 20, 53);
--sign-bg: rgba(27, 37, 75, 1);
--text-theme-color: #fff;
@@ -40,8 +41,8 @@
--el-bg-color:#141a36;
--el-fill-color-blank: rgba(17, 28, 68, 1);
--el-fill-color-light: rgba(86, 86, 95, .2);
--el-color-primary-light-9:rgba(86, 86, 95, .2);
--chat-wel-bg:#2d2f388a;
--el-color-primary-light-9:rgba(86, 86, 95, .2);
--chat-wel-bg:#2d2f388a;
--theme-text-color-secondary: #a3aed0;
// --el-pagination-button-bg-color: rgba(86,86,95,0.2);
@@ -67,4 +68,16 @@
--chat-content-bg-list:rgba(86, 86, 95, .2);
--hover-deep-color:#30323c;
// --theme-text-tertiary: #e1e1e1;
}
//
--btn-bg: rgba(86, 86, 95, .5);
.el-table {
//
--el-fill-color-darker: rgba(100, 100, 100, .5);
--el-border-color-darker: #73767a;
--el-table-border-color: rgba(100, 100, 100, .5);
--el-table-row-hover-bg-color: rgba(16, 21, 43, .8);
--el-table-current-row-bg-color: rgba(16, 21, 43, .8);
}
}

View File

@@ -28,6 +28,7 @@
--card-bg:#fff;
--theme-bg:linear-gradient(88deg, #fff3f3 1.44%, #e7e8ff);
--theme-bg-all:#f5f7fd;
--theme-bg-color: #f5f7fd;
--sign-bg: rgba(244, 247, 254, 1);
--text-theme-color: rgba(43, 54, 116, 1)
--text-color-primary: rgba(67, 24, 255, 1);
@@ -43,10 +44,13 @@
--chat-content-bg:#f5f7fc;
--chat-list-bg: #0302020a;
--chat-content-bg-list:#fff;
--chat-wel-bg:rgba(247, 247, 248, 1);
--el-pagination-button-bg-color: rgba(86,86,95,0.2);
--hover-deep-color:#fff;
--chat-content-bg-list: #fff;
--chat-wel-bg: rgba(247, 247, 248, 1);
--el-pagination-button-bg-color: rgba(86, 86, 95, 0.2);
--hover-deep-color: #fff;
//
--btn-bg: rgba(100, 100, 100, .1);
}

View File

@@ -1,10 +1,8 @@
/* 在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份。 */
@font-face {
font-family: 'iconfont'; /* Project id 4125778 */
src: url('//at.alicdn.com/t/c/font_4125778_t6hhjiqu67.woff2?t=1733221702650') format('woff2'),
url('//at.alicdn.com/t/c/font_4125778_t6hhjiqu67.woff?t=1733221702650') format('woff'),
url('//at.alicdn.com/t/c/font_4125778_t6hhjiqu67.ttf?t=1733221702650') format('truetype');
font-family: "iconfont"; /* Project id 4125778 */
src: url('iconfont.woff2?t=1734934068681') format('woff2'),
url('iconfont.woff?t=1734934068681') format('woff'),
url('iconfont.ttf?t=1734934068681') format('truetype');
}
.iconfont {
@@ -15,6 +13,154 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-redeem:before {
content: "\e61a";
}
.icon-login:before {
content: "\e636";
}
.icon-present:before {
content: "\e648";
}
.icon-icon-warning:before {
content: "\e671";
}
.icon-help:before {
content: "\e64a";
}
.icon-success:before {
content: "\e61e";
}
.icon-error:before {
content: "\e64e";
}
.icon-house:before {
content: "\e619";
}
.icon-vip4:before {
content: "\e684";
}
.icon-vip1:before {
content: "\f90b";
}
.icon-vip2:before {
content: "\fabb";
}
.icon-vip3:before {
content: "\10135";
}
.icon-conversation:before {
content: "\e617";
}
.icon-arrow-down:before {
content: "\e615";
}
.icon-arrow-up:before {
content: "\e616";
}
.icon-refresh:before {
content: "\e90c";
}
.icon-refresh-bold:before {
content: "\e614";
}
.icon-copy:before {
content: "\e720";
}
.icon-new-chat:before {
content: "\e613";
}
.icon-expand:before {
content: "\e7a0";
}
.icon-colspan:before {
content: "\e79e";
}
.icon-question:before {
content: "\e8e9";
}
.icon-AIduihua_jihuo:before {
content: "\e6bb";
}
.icon-MidJourney:before {
content: "\e60e";
}
.icon-stable-diffusion:before {
content: "\e60f";
}
.icon-info:before {
content: "\e6a0";
}
.icon-more-horizontal:before {
content: "\e60d";
}
.icon-xinghao:before {
content: "\e8d6";
}
.icon-plus:before {
content: "\e61f";
}
.icon-plus-circle:before {
content: "\e822";
}
.icon-taiyang:before {
content: "\e60b";
}
.icon-yueliang:before {
content: "\e679";
}
.icon-prev-page:before {
content: "\e8ef";
}
.icon-next-page:before {
content: "\e8f0";
}
.icon-search:before {
content: "\e618";
}
.icon-sub_menu:before {
content: "\e75e";
}
.icon-google:before {
content: "\ea0c";
}
.icon-linggan:before {
content: "\e641";
}
@@ -419,6 +565,3 @@
content: "\e66f";
}
.icon-house:before {
content: "\e619";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,265 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "3624396",
"name": "兑换码",
"font_class": "redeem",
"unicode": "e61a",
"unicode_decimal": 58906
},
{
"icon_id": "8760110",
"name": "login",
"font_class": "login",
"unicode": "e636",
"unicode_decimal": 58934
},
{
"icon_id": "29565277",
"name": "礼物",
"font_class": "present",
"unicode": "e648",
"unicode_decimal": 58952
},
{
"icon_id": "13519527",
"name": "警告",
"font_class": "icon-warning",
"unicode": "e671",
"unicode_decimal": 58993
},
{
"icon_id": "145466",
"name": "帮助",
"font_class": "help",
"unicode": "e64a",
"unicode_decimal": 58954
},
{
"icon_id": "1951950",
"name": "成功",
"font_class": "success",
"unicode": "e61e",
"unicode_decimal": 58910
},
{
"icon_id": "6204756",
"name": "失败",
"font_class": "error",
"unicode": "e64e",
"unicode_decimal": 58958
},
{
"icon_id": "3916695",
"name": "首页",
"font_class": "house",
"unicode": "e619",
"unicode_decimal": 58905
},
{
"icon_id": "10583159",
"name": "会员",
"font_class": "vip4",
"unicode": "e684",
"unicode_decimal": 59012
},
{
"icon_id": "23942994",
"name": "会员",
"font_class": "vip1",
"unicode": "f90b",
"unicode_decimal": 63755
},
{
"icon_id": "24111538",
"name": "会员",
"font_class": "vip2",
"unicode": "fabb",
"unicode_decimal": 64187
},
{
"icon_id": "37305926",
"name": "会员VIP",
"font_class": "vip3",
"unicode": "10135",
"unicode_decimal": 65845
},
{
"icon_id": "41134022",
"name": "新会话",
"font_class": "conversation",
"unicode": "e617",
"unicode_decimal": 58903
},
{
"icon_id": "17411805",
"name": "Arrow Down",
"font_class": "arrow-down",
"unicode": "e615",
"unicode_decimal": 58901
},
{
"icon_id": "17411857",
"name": "Arrow Up",
"font_class": "arrow-up",
"unicode": "e616",
"unicode_decimal": 58902
},
{
"icon_id": "7736305",
"name": "refresh",
"font_class": "refresh",
"unicode": "e90c",
"unicode_decimal": 59660
},
{
"icon_id": "1391302",
"name": "Refresh",
"font_class": "refresh-bold",
"unicode": "e614",
"unicode_decimal": 58900
},
{
"icon_id": "19418384",
"name": "copy",
"font_class": "copy",
"unicode": "e720",
"unicode_decimal": 59168
},
{
"icon_id": "20584689",
"name": "New Chat",
"font_class": "new-chat",
"unicode": "e613",
"unicode_decimal": 58899
},
{
"icon_id": "23995596",
"name": "收起展开-展开",
"font_class": "expand",
"unicode": "e7a0",
"unicode_decimal": 59296
},
{
"icon_id": "23995626",
"name": "收起展开-收起",
"font_class": "colspan",
"unicode": "e79e",
"unicode_decimal": 59294
},
{
"icon_id": "1727527",
"name": "306问号-线性圆框",
"font_class": "question",
"unicode": "e8e9",
"unicode_decimal": 59625
},
{
"icon_id": "35446270",
"name": "AI对话_激活",
"font_class": "AIduihua_jihuo",
"unicode": "e6bb",
"unicode_decimal": 59067
},
{
"icon_id": "39584617",
"name": "MidJourney-copy",
"font_class": "MidJourney",
"unicode": "e60e",
"unicode_decimal": 58894
},
{
"icon_id": "42109955",
"name": "stable-diffusion",
"font_class": "stable-diffusion",
"unicode": "e60f",
"unicode_decimal": 58895
},
{
"icon_id": "1227734",
"name": "info",
"font_class": "info",
"unicode": "e6a0",
"unicode_decimal": 59040
},
{
"icon_id": "159969",
"name": "more",
"font_class": "more-horizontal",
"unicode": "e60d",
"unicode_decimal": 58893
},
{
"icon_id": "8434022",
"name": "星号",
"font_class": "xinghao",
"unicode": "e8d6",
"unicode_decimal": 59606
},
{
"icon_id": "831577",
"name": "plus",
"font_class": "plus",
"unicode": "e61f",
"unicode_decimal": 58911
},
{
"icon_id": "6151285",
"name": "plus-circle",
"font_class": "plus-circle",
"unicode": "e822",
"unicode_decimal": 59426
},
{
"icon_id": "15056491",
"name": "太阳",
"font_class": "taiyang",
"unicode": "e60b",
"unicode_decimal": 58891
},
{
"icon_id": "40094190",
"name": "月亮-copy",
"font_class": "yueliang",
"unicode": "e679",
"unicode_decimal": 59001
},
{
"icon_id": "1727538",
"name": "上一页",
"font_class": "prev-page",
"unicode": "e8ef",
"unicode_decimal": 59631
},
{
"icon_id": "1727540",
"name": "下一页",
"font_class": "next-page",
"unicode": "e8f0",
"unicode_decimal": 59632
},
{
"icon_id": "2488134",
"name": "搜索",
"font_class": "search",
"unicode": "e618",
"unicode_decimal": 58904
},
{
"icon_id": "9845558",
"name": "sub_menu",
"font_class": "sub_menu",
"unicode": "e75e",
"unicode_decimal": 59230
},
{
"icon_id": "11983544",
"name": "google",
"font_class": "google",
"unicode": "ea0c",
"unicode_decimal": 59916
},
{
"icon_id": "15330210",
"name": "创意灵感",

Binary file not shown.

View File

@@ -1,27 +1,22 @@
<template>
<div class="theme-box" @click="toggleTheme">
<span class="iconfont icon-yueliang">{{
themePage === "light" ? "&#xe679;" : "&#xe60b;"
}}</span>
<span class="iconfont icon-yueliang">{{ themePage === "light" ? "&#xe679;" : "&#xe60b;" }}</span>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { useSharedStore } from "@/store/sharedata";
// 定义主题状态,初始值从 localStorage 获取
const themePage = ref(localStorage.getItem("theme") || "light");
const store = useSharedStore();
const themePage = ref(store.theme || "light");
// 切换主题函数
const toggleTheme = () => {
themePage.value = themePage.value === "light" ? "dark" : "light";
document.documentElement.setAttribute("data-theme", themePage.value); // 设置 HTML 的 data-theme 属性
localStorage.setItem("theme", themePage.value); // 保存主题到 localStorage
store.setTheme(themePage.value); // 保存主题
};
onMounted(() => {
document.documentElement.setAttribute("data-theme", themePage.value);
});
</script>
<style lang="stylus" scoped>
@@ -30,7 +25,7 @@ onMounted(() => {
z-index :111
position: fixed;
right: 40px;
bottom: 262px;
bottom: 150px;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 50%;

View File

@@ -1,23 +1,11 @@
<template>
<div
class="user-bill"
v-loading="loading"
element-loading-background="rgba(255,255,255,.3)"
>
<div class="user-bill" v-loading="loading" element-loading-background="rgba(255,255,255,.3)">
<el-row v-if="items.length > 0">
<el-table
:data="items"
:row-key="(row) => row.id"
table-layout="auto"
border
>
<el-table :data="items" :row-key="(row) => row.id" table-layout="auto" border>
<el-table-column prop="order_no" label="订单号">
<template #default="scope">
<span>{{ scope.row.order_no }}</span>
<el-icon
class="copy-order-no"
:data-clipboard-text="scope.row.order_no"
>
<el-icon class="copy-order-no" :data-clipboard-text="scope.row.order_no">
<DocumentCopy />
</el-icon>
</template>
@@ -33,16 +21,14 @@
<el-table-column prop="pay_name" label="支付名称" />
<el-table-column label="支付时间">
<template #default="scope">
<span v-if="scope.row['pay_time']">{{
dateFormat(scope.row["pay_time"])
}}</span>
<span v-if="scope.row['pay_time']">{{ dateFormat(scope.row["pay_time"]) }}</span>
<el-tag v-else>未支付</el-tag>
</template>
</el-table-column>
</el-table>
</el-row>
<el-empty :image-size="100" v-else :image="nodata" description="暂无数据" />
<div class="pagination">
<div class="pagination pb-5">
<el-pagination
v-if="total > 0"
background

View File

@@ -12,22 +12,14 @@
<div class="breadcrumb">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item v-for="item in breadcrumb">{{
item.title
}}</el-breadcrumb-item>
<el-breadcrumb-item v-for="item in breadcrumb" :key="item.title">{{ item.title }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="header-right">
<div class="header-user-con">
<!-- 切换主题 -->
<el-switch
style="margin-right: 10px"
v-model="dark"
inline-prompt
:active-action-icon="Moon"
:inactive-action-icon="Sunny"
@change="changeTheme"
/>
<el-switch style="margin-right: 10px" v-model="dark" inline-prompt :active-action-icon="Moon"
:inactive-action-icon="Sunny" @change="changeTheme"/>
<!-- 用户名下拉菜单 -->
<el-dropdown class="user-name" :hide-on-click="true" trigger="click">
<span class="el-dropdown-link">
@@ -38,9 +30,7 @@
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<i class="iconfont icon-version"></i> 当前版本{{ version }}
</el-dropdown-item>
<el-dropdown-item><i class="iconfont icon-version"></i> 当前版本{{ version }}</el-dropdown-item>
<el-dropdown-item divided @click="logout">
<i class="iconfont icon-logout"></i>
<span>退出登录</span>
@@ -53,21 +43,14 @@
</div>
</template>
<script setup>
import { onMounted, ref, watch } from "vue";
import { getMenuItems, useSidebarStore } from "@/store/sidebar";
import { useRouter } from "vue-router";
import {
ArrowDown,
ArrowRight,
Expand,
Fold,
Moon,
Sunny
} from "@element-plus/icons-vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { removeAdminToken } from "@/store/session";
import { useSharedStore } from "@/store/sharedata";
import {onMounted, ref, watch} from "vue";
import {getMenuItems, useSidebarStore} from "@/store/sidebar";
import {useRouter} from "vue-router";
import {ArrowDown, ArrowRight, Expand, Fold, Moon, Sunny} from "@element-plus/icons-vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {removeAdminToken} from "@/store/session";
import {useSharedStore} from "@/store/sharedata";
const version = ref(process.env.VUE_APP_VERSION);
const avatar = ref("/images/user-info.jpg");
@@ -76,17 +59,17 @@ const router = useRouter();
const breadcrumb = ref([]);
const store = useSharedStore();
const dark = ref(store.adminTheme === "dark");
const theme = ref(store.adminTheme);
const dark = ref(store.theme === "dark");
const theme = ref(store.theme);
watch(
() => store.adminTheme,
() => store.theme,
(val) => {
theme.value = val;
}
);
const changeTheme = () => {
store.setAdminTheme(dark.value ? "dark" : "light");
store.setTheme(dark.value ? "dark" : "light");
};
router.afterEach((to) => {
@@ -107,7 +90,7 @@ const initBreadCrumb = (path) => {
if (items[i].index === path) {
breadcrumb.value.push({
title: items[i].title,
path: items[i].index
path: items[i].index,
});
break;
}
@@ -121,11 +104,11 @@ const initBreadCrumb = (path) => {
if (subs[j].index === path) {
breadcrumb.value.push({
title: items[i].title,
path: items[i].index
path: items[i].index,
});
breadcrumb.value.push({
title: subs[j].title,
path: subs[j].index
path: subs[j].index,
});
bk = true;
break;

View File

@@ -70,8 +70,8 @@ httpGet('/api/admin/config/get?key=system').then(res => {
ElMessage.error("加载系统配置失败: " + e.message)
})
const store = useSharedStore()
const theme = ref(store.adminTheme)
watch(() => store.adminTheme, (val) => {
const theme = ref(store.theme)
watch(() => store.theme, (val) => {
theme.value = val
})
const items = [

View File

@@ -42,8 +42,8 @@ import {useSharedStore} from "@/store/sharedata";
import {ref, watch} from "vue";
const store = useSharedStore()
const theme = ref(store.adminTheme)
watch(() => store.adminTheme, (val) => {
const theme = ref(store.theme)
watch(() => store.theme, (val) => {
theme.value = val
})
const router = useRouter();

View File

@@ -14,6 +14,8 @@ import App from "./App.vue";
import { useThemeStore } from "@/store/theme";
import { createPinia } from "pinia";
import "animate.css/animate.min.css";
import "@/assets/css/tailwind.css";
import {
ActionSheet,
Badge,
@@ -59,7 +61,7 @@ import {
Tabs,
Tag,
TextEllipsis,
Uploader
Uploader,
} from "vant";
import { router } from "@/router";
import "v3-waterfall/dist/style.css";

View File

@@ -1,77 +1,73 @@
import {defineStore} from 'pinia';
import Storage from 'good-storage'
import { defineStore } from "pinia";
import Storage from "good-storage";
export const useSharedStore = defineStore('shared', {
state: () => ({
showLoginDialog: false,
chatListStyle: Storage.get("chat_list_style","chat"),
chatStream: Storage.get("chat_stream",true),
socket: {conn:null, handlers:{}},
mobileTheme: Storage.get("mobile_theme", "light"),
adminTheme: Storage.get("admin_theme", "light"),
isLogin: false
}),
getters: {},
actions: {
setShowLoginDialog(value) {
this.showLoginDialog = value;
},
setChatListStyle(value) {
this.chatListStyle = value;
Storage.set("chat_list_style", value);
},
setChatStream(value) {
this.chatStream = value;
Storage.set("chat_stream", value);
},
setSocket(value) {
for (const key in this.socket.handlers) {
this.setMessageHandler(value, this.socket.handlers[key])
}
this.socket.conn = value
},
addMessageHandler(key, callback) {
if (!this.socket.handlers[key]) {
this.socket.handlers[key] = callback;
}
this.setMessageHandler(this.socket.conn, callback)
},
setMessageHandler(conn, callback) {
if (!conn) {
return
}
conn.addEventListener('message', (event) => {
try {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
callback(JSON.parse(String(reader.result)))
}
}
} catch (e) {
console.warn(e)
}
})
},
removeMessageHandler(key) {
if (this.socket.conn && this.socket.conn.readyState === WebSocket.OPEN) {
this.socket.conn.removeEventListener('message', this.socket.handlers[key])
}
delete this.socket.handlers[key]
},
setMobileTheme(theme) {
this.mobileTheme = theme
Storage.set("mobile_theme", theme)
},
setAdminTheme(theme) {
this.adminTheme = theme
Storage.set("admin_theme", theme)
},
setIsLogin(value) {
this.isLogin = value
}
export const useSharedStore = defineStore("shared", {
state: () => ({
showLoginDialog: false,
chatListStyle: Storage.get("chat_list_style", "chat"),
chatStream: Storage.get("chat_stream", true),
socket: { conn: null, handlers: {} },
theme: Storage.get("theme", "light"),
isLogin: false,
}),
getters: {},
actions: {
setShowLoginDialog(value) {
this.showLoginDialog = value;
},
setChatListStyle(value) {
this.chatListStyle = value;
Storage.set("chat_list_style", value);
},
setChatStream(value) {
this.chatStream = value;
Storage.set("chat_stream", value);
},
setSocket(value) {
for (const key in this.socket.handlers) {
this.setMessageHandler(value, this.socket.handlers[key]);
}
this.socket.conn = value;
},
addMessageHandler(key, callback) {
if (!this.socket.handlers[key]) {
this.socket.handlers[key] = callback;
}
this.setMessageHandler(this.socket.conn, callback);
},
setMessageHandler(conn, callback) {
if (!conn) {
return;
}
conn.addEventListener("message", (event) => {
try {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
callback(JSON.parse(String(reader.result)));
};
}
} catch (e) {
console.warn(e);
}
});
},
removeMessageHandler(key) {
if (this.socket.conn && this.socket.conn.readyState === WebSocket.OPEN) {
this.socket.conn.removeEventListener("message", this.socket.handlers[key]);
}
delete this.socket.handlers[key];
},
setTheme(theme) {
this.theme = theme;
document.documentElement.setAttribute("data-theme", theme); // 设置 HTML 的 data-theme 属性
Storage.set("theme", theme);
},
setIsLogin(value) {
this.isLogin = value;
},
},
});

View File

@@ -11,13 +11,7 @@
</el-button>
<div class="search-box">
<el-input
v-model="chatName"
placeholder="搜索会话"
@keyup="searchChat($event)"
style=""
class="search-input"
>
<el-input v-model="chatName" placeholder="搜索会话" @keyup="searchChat($event)" style="" class="search-input">
<template #prefix>
<el-icon class="el-input__icon">
<Search />
@@ -25,96 +19,62 @@
</template>
</el-input>
</div>
<div class="content" :style="{ height: leftBoxHeight + 'px' }">
<el-row v-for="chat in chatList" :key="chat.chat_id">
<div
:class="
chat.chat_id === chatId
? 'chat-list-item active'
: 'chat-list-item'
"
@click="loadChat(chat)"
>
<el-image :src="chat.icon" class="avatar" />
<span class="chat-title-input" v-if="chat.edit">
<el-input
v-model="tmpChatTitle"
size="small"
@keydown="titleKeydown($event, chat)"
:id="'chat-' + chat.chat_id"
@blur="editConfirm(chat)"
@click="stopPropagation($event)"
placeholder="请输入标题"
/>
</span>
<span v-else class="chat-title">{{ chat.title }}</span>
<span class="chat-opt">
<el-dropdown trigger="click">
<span
class="el-dropdown-link"
<el-scrollbar :height="{ height: +'px' }">
<div class="content">
<el-row v-for="chat in chatList" :key="chat.chat_id">
<div :class="chat.chat_id === chatId ? 'chat-list-item active' : 'chat-list-item'" @click="loadChat(chat)">
<el-image :src="chat.icon" class="avatar" />
<span class="chat-title-input" v-if="chat.edit">
<el-input
v-model="tmpChatTitle"
size="small"
@keydown="titleKeydown($event, chat)"
:id="'chat-' + chat.chat_id"
@blur="editConfirm(chat)"
@click="stopPropagation($event)"
>
<el-icon><More /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
:icon="Edit"
@click="editChatTitle(chat)"
>重命名</el-dropdown-item
>
<el-dropdown-item
:icon="Delete"
style="
--el-text-color-regular: var(--el-color-danger);
--el-dropdown-menuItem-hover-fill: #f8e1de;
--el-dropdown-menuItem-hover-color: var(
--el-color-danger
);
"
@click="removeChat(chat)"
>删除</el-dropdown-item
>
<el-dropdown-item :icon="Share" @click="shareChat(chat)"
>分享</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
</el-row>
</div>
placeholder="请输入标题"
/>
</span>
<span v-else class="chat-title">{{ chat.title }}</span>
<span class="chat-opt">
<el-dropdown trigger="click">
<span class="el-dropdown-link" @click="stopPropagation($event)">
<el-icon><More /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="Edit" @click="editChatTitle(chat)">重命名</el-dropdown-item>
<el-dropdown-item
:icon="Delete"
style="
--el-text-color-regular: var(--el-color-danger);
--el-dropdown-menuItem-hover-fill: #f8e1de;
--el-dropdown-menuItem-hover-color: var(--el-color-danger);
"
@click="removeChat(chat)"
>删除</el-dropdown-item
>
<el-dropdown-item :icon="Share" @click="shareChat(chat)">分享</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
</el-row>
</div>
</el-scrollbar>
</div>
<div class="tool-box">
<el-button type="primary" size="small" @click="clearAllChats">
<i class="iconfont icon-clear"></i> 清除所有对话
</el-button>
<el-button type="primary" size="small" @click="clearAllChats"> <i class="iconfont icon-clear"></i> 清除所有对话 </el-button>
</div>
</el-aside>
<el-main
v-loading="loading"
element-loading-background="rgba(122, 122, 122, 0.3)"
>
<el-main v-loading="loading" element-loading-background="rgba(122, 122, 122, 0.3)">
<div class="chat-container">
<div class="chat-config">
<el-select
v-model="roleId"
filterable
placeholder="角色"
@change="_newChat"
class="role-select"
style="width: 150px"
>
<el-option
v-for="item in roles"
:key="item.id"
:label="item.name"
:value="item.id"
>
<el-select v-model="roleId" filterable placeholder="角色" @change="_newChat" class="role-select" style="width: 150px">
<el-option v-for="item in roles" :key="item.id" :label="item.name" :value="item.id">
<div class="role-option">
<el-image :src="item.icon"></el-image>
<span>{{ item.name }}</span>
@@ -122,43 +82,21 @@
</el-option>
</el-select>
<el-select
v-model="modelID"
filterable
placeholder="模型"
@change="_newChat"
:disabled="disableModel"
style="width: 150px"
>
<el-option
v-for="item in models"
:key="item.id"
:label="item.name"
:value="item.id"
>
<el-select v-model="modelID" filterable placeholder="模型" @change="_newChat" :disabled="disableModel" style="width: 150px">
<el-option v-for="item in models" :key="item.id" :label="item.name" :value="item.id">
<span>{{ item.name }}</span>
<el-tag
style="margin-left: 5px; position: relative; top: -2px"
type="info"
size="small"
>{{ item.power }}算力
</el-tag>
<el-tag style="margin-left: 5px; position: relative; top: -2px" type="info" size="small">{{ item.power }}算力 </el-tag>
</el-option>
</el-select>
<div class="flex-center">
<el-dropdown :hide-on-click="false" trigger="click">
<span class="setting"
><i class="iconfont icon-plugin"></i
></span>
<span class="setting"><i class="iconfont icon-plugin"></i></span>
<template #dropdown>
<el-dropdown-menu class="tools-dropdown">
<el-checkbox-group v-model="toolSelected">
<el-dropdown-item v-for="item in tools" :key="item.id">
<el-checkbox :value="item.id" :label="item.label" />
<el-tooltip
:content="item.description"
placement="right"
>
<el-tooltip :content="item.description" placement="right">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
</el-dropdown-item>
@@ -175,27 +113,13 @@
<div>
<div id="container" :style="{ height: mainWinHeight + 'px' }">
<div
class="chat-box"
id="chat-box"
:style="{ height: chatBoxHeight + 'px' }"
>
<div class="chat-box" id="chat-box" :style="{ height: chatBoxHeight + 'px' }">
<div v-if="showHello">
<welcome @send="autofillPrompt" />
</div>
<div v-for="item in chatData" :key="item.id" v-else>
<chat-prompt
v-if="item.type === 'prompt'"
:data="item"
:list-style="listStyle"
/>
<chat-reply
v-else-if="item.type === 'reply'"
:data="item"
@regen="reGenerate"
:read-only="false"
:list-style="listStyle"
/>
<chat-prompt v-if="item.type === 'prompt'" :data="item" :list-style="listStyle" />
<chat-reply v-else-if="item.type === 'reply'" :data="item" @regen="reGenerate" :read-only="false" :list-style="listStyle" />
</div>
<back-top :right="30" :bottom="155" />
@@ -259,56 +183,27 @@
</div> -->
<div class="flex little-btns">
<span class="tool-item-btn" @click="realtimeChat">
<el-tooltip
class="box-item"
effect="dark"
:content="
'实时语音对话,每次消耗' +
config.advance_voice_power +
'算力'
"
>
<el-tooltip class="box-item" effect="dark" :content="'实时语音对话,每次消耗' + config.advance_voice_power + '算力'">
<i class="iconfont icon-mic-bold"></i>
</el-tooltip>
</span>
<span class="tool-item-btn" v-if="isLogin">
<el-tooltip
class="box-item"
effect="dark"
content="上传附件"
>
<file-select
v-if="isLogin"
:user-id="loginUser.id"
@selected="insertFile"
/>
<el-tooltip class="box-item" effect="dark" content="上传附件">
<file-select v-if="isLogin" :user-id="loginUser.id" @selected="insertFile" />
</el-tooltip>
</span>
</div>
<div class="flex little-btns">
<span class="send-btn tool-item-btn">
<!-- showStopGenerate -->
<el-button
type="info"
v-if="showStopGenerate"
@click="stopGenerate"
plain
>
<el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain>
<el-icon>
<VideoPause />
</el-icon>
</el-button>
<el-button
@click="sendMessage"
style="color: #754ff6"
v-else
>
<el-tooltip
class="box-item"
effect="dark"
content="发送"
>
<el-button @click="sendMessage" style="color: #754ff6" v-else>
<el-tooltip class="box-item" effect="dark" content="发送">
<el-icon><Promotion /></el-icon>
</el-tooltip>
</el-button>
@@ -328,21 +223,14 @@
</el-main>
</el-container>
<el-dialog
v-model="showNotice"
:show-close="true"
class="notice-dialog"
title="网站公告"
>
<el-dialog v-model="showNotice" :show-close="true" class="notice-dialog" title="网站公告">
<div class="notice">
<div v-html="notice"></div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="notShow" type="primary"
>我知道了不再显示</el-button
>
<el-button @click="notShow" type="primary">我知道了不再显示</el-button>
</span>
</template>
</el-dialog>
@@ -361,11 +249,7 @@
/>
</el-dialog> -->
<el-dialog
v-model="showConversationDialog"
title="实时语音通话"
:fullscreen="true"
>
<el-dialog v-model="showConversationDialog" title="实时语音通话" :fullscreen="true">
<div v-loading="!frameLoaded">
<iframe
style="width: 100%; height: calc(100vh - 100px); border: none"
@@ -381,17 +265,7 @@
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import ChatPrompt from "@/components/ChatPrompt.vue";
import ChatReply from "@/components/ChatReply.vue";
import {
Delete,
Edit,
InfoFilled,
More,
Plus,
Promotion,
Search,
Share,
VideoPause
} from "@element-plus/icons-vue";
import { Delete, Edit, InfoFilled, More, Plus, Promotion, Search, Share, VideoPause } from "@element-plus/icons-vue";
import "highlight.js/styles/a11y-dark.css";
import { isMobile, randString, removeArrayItem, UUID } from "@/utils/libs";
import { ElMessage, ElMessageBox } from "element-plus";
@@ -492,7 +366,7 @@ const md = require("markdown-it")({
breaks: true,
html: true,
linkify: true,
typographer: true
typographer: true,
});
// 获取系统公告
httpGet("/api/config/get?key=notice")
@@ -561,7 +435,7 @@ onMounted(() => {
id: randString(32),
icon: chatRole["icon"],
prompt: prePrompt,
content: data.body
content: data.body,
});
isNewMsg.value = false;
lineBuffer.value = data.body;
@@ -583,16 +457,14 @@ onMounted(() => {
httpPost("/api/chat/tokens", {
text: "",
model: getModelValue(modelID.value),
chat_id: chatId.value
chat_id: chatId.value,
})
.then((res) => {
reply["created_at"] = new Date().getTime();
reply["tokens"] = res.data;
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
document
.getElementById("chat-box")
.scrollTo(0, document.getElementById("chat-box").scrollHeight);
document.getElementById("chat-box").scrollTo(0, document.getElementById("chat-box").scrollHeight);
});
})
.catch(() => {});
@@ -606,9 +478,7 @@ onMounted(() => {
}
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
document
.getElementById("chat-box")
.scrollTo(0, document.getElementById("chat-box").scrollHeight);
document.getElementById("chat-box").scrollTo(0, document.getElementById("chat-box").scrollHeight);
localStorage.setItem("chat_id", chatId.value);
});
});
@@ -736,10 +606,7 @@ const newChat = () => {
disableModel.value = true;
}
// 已有新开的会话
if (
newChatItem.value !== null &&
newChatItem.value["role_id"] === roles.value[0]["role_id"]
) {
if (newChatItem.value !== null && newChatItem.value["role_id"] === roles.value[0]["role_id"]) {
return;
}
@@ -757,7 +624,7 @@ const newChat = () => {
model_id: modelID.value,
title: "",
edit: false,
removing: false
removing: false,
};
showStopGenerate.value = false;
loadChatHistory(chatId.value);
@@ -818,7 +685,7 @@ const editConfirm = function (chat) {
httpPost("/api/chat/update", {
chat_id: chat.chat_id,
title: tmpChatTitle.value
title: tmpChatTitle.value,
})
.then(() => {
chat.title = tmpChatTitle.value;
@@ -833,18 +700,14 @@ const removeChat = function (chat) {
ElMessageBox.confirm(`该操作会删除"${chat.title}"`, "删除聊天", {
confirmButtonText: "删除",
cancelButtonText: "取消",
type: "warning"
type: "warning",
})
.then(() => {
httpGet("/api/chat/remove?chat_id=" + chat.chat_id)
.then(() => {
chatList.value = removeArrayItem(
chatList.value,
chat,
function (e1, e2) {
return e1.id === e2.id;
}
);
chatList.value = removeArrayItem(chatList.value, chat, function (e1, e2) {
return e1.id === e2.id;
});
// 重置会话
_newChat();
})
@@ -867,9 +730,7 @@ const enableInput = () => {
const onInput = (e) => {
// 根据输入的内容自动计算输入框的行数
const lineHeight = parseFloat(
window.getComputedStyle(inputRef.value).lineHeight
);
const lineHeight = parseFloat(window.getComputedStyle(inputRef.value).lineHeight);
textHeightRef.value.style.width = inputRef.value.clientWidth + "px"; // 设定宽度和 textarea 相同
const lines = Math.floor(textHeightRef.value.clientHeight / lineHeight);
inputRef.value.scrollTo(0, inputRef.value.scrollHeight);
@@ -936,13 +797,11 @@ const sendMessage = function () {
icon: loginUser.value.avatar,
content: content,
model: getModelValue(modelID.value),
created_at: new Date().getTime() / 1000
created_at: new Date().getTime() / 1000,
});
nextTick(() => {
document
.getElementById("chat-box")
.scrollTo(0, document.getElementById("chat-box").scrollHeight);
document.getElementById("chat-box").scrollTo(0, document.getElementById("chat-box").scrollHeight);
});
showHello.value = false;
@@ -957,8 +816,8 @@ const sendMessage = function () {
chat_id: chatId.value,
content: content,
tools: toolSelected.value,
stream: stream.value
}
stream: stream.value,
},
})
);
tmpChatTitle.value = content;
@@ -976,7 +835,7 @@ const clearAllChats = function () {
dangerouslyUseHTMLString: true,
showClose: true,
closeOnClickModal: false,
center: false
center: false,
})
.then(() => {
httpGet("/api/chat/clear")
@@ -1009,7 +868,7 @@ const loadChatHistory = function (chatId) {
type: "reply",
id: randString(32),
icon: _role["icon"],
content: _role["hello_msg"]
content: _role["hello_msg"],
});
return;
}
@@ -1022,9 +881,7 @@ const loadChatHistory = function (chatId) {
}
nextTick(() => {
document
.getElementById("chat-box")
.scrollTo(0, document.getElementById("chat-box").scrollHeight);
document.getElementById("chat-box").scrollTo(0, document.getElementById("chat-box").scrollHeight);
});
})
.catch((e) => {
@@ -1049,7 +906,7 @@ const reGenerate = function (prompt) {
type: "prompt",
id: randString(32),
icon: loginUser.value.avatar,
content: text
content: text,
});
store.socket.conn.send(
JSON.stringify({
@@ -1061,8 +918,8 @@ const reGenerate = function (prompt) {
chat_id: chatId.value,
content: text,
tools: toolSelected.value,
stream: stream.value
}
stream: stream.value,
},
})
);
};
@@ -1077,11 +934,7 @@ const searchChat = function (e) {
if (e.keyCode === 13) {
const items = [];
for (let i = 0; i < allChats.value.length; i++) {
if (
allChats.value[i].title
.toLowerCase()
.indexOf(chatName.value.toLowerCase()) !== -1
) {
if (allChats.value[i].title.toLowerCase().indexOf(chatName.value.toLowerCase()) !== -1) {
items.push(allChats.value[i]);
}
}
@@ -1095,12 +948,7 @@ const shareChat = (chat) => {
return ElMessage.error("请先选中一个会话");
}
const url =
location.protocol +
"//" +
location.host +
"/chat/export?chat_id=" +
chat.chat_id;
const url = location.protocol + "//" + location.host + "/chat/export?chat_id=" + chat.chat_id;
window.open(url, "_blank");
};
@@ -1124,11 +972,7 @@ const insertFile = (file) => {
files.value.push(file);
};
const removeFile = (file) => {
files.value = removeArrayItem(
files.value,
file,
(v1, v2) => v1.url === v2.url
);
files.value = removeArrayItem(files.value, file, (v1, v2) => v1.url === v2.url);
};
// 实时语音对话

View File

@@ -11,13 +11,8 @@
<el-form-item label="图片质量">
<template #default>
<div class="form-item-inner">
<el-select v-model="params.quality" style="width: 176px">
<el-option
v-for="v in qualities"
:label="v.name"
:value="v.value"
:key="v.value"
/>
<el-select v-model="params.quality" style="width: 150px">
<el-option v-for="v in qualities" :label="v.name" :value="v.value" :key="v.value" />
</el-select>
</div>
</template>
@@ -28,13 +23,8 @@
<el-form-item label="图片尺寸">
<template #default>
<div class="form-item-inner">
<el-select v-model="params.size" style="width: 176px">
<el-option
v-for="v in sizes"
:label="v"
:value="v"
:key="v"
/>
<el-select v-model="params.size" style="width: 150px">
<el-option v-for="v in sizes" :label="v" :value="v" :key="v" />
</el-select>
</div>
</template>
@@ -45,19 +35,10 @@
<el-form-item label="图片样式">
<template #default>
<div class="form-item-inner">
<el-select v-model="params.style" style="width: 176px">
<el-option
v-for="v in styles"
:label="v.name"
:value="v.value"
:key="v.value"
/>
<el-select v-model="params.style" style="width: 150px">
<el-option v-for="v in styles" :label="v.name" :value="v.value" :key="v.value" />
</el-select>
<el-tooltip
content="生动使模型倾向于生成超真实和戏剧性的图像"
raw-content
placement="right"
>
<el-tooltip content="生动使模型倾向于生成超真实和戏剧性的图像" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@@ -80,17 +61,8 @@
</div>
<el-row class="text-info">
<el-button
class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<i
class="iconfont icon-chuangzuo"
style="margin-right: 5px"
></i>
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating">
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i>
<span>生成专业绘画指令</span>
</el-button>
</el-row>
@@ -98,12 +70,8 @@
<div class="text-info">
<el-row :gutter="10">
<el-text type="primary"
>每次绘图消耗
<el-text type="warning"
>{{ dallPower }}算力</el-text
></el-text
>每次绘图消耗 <el-text type="warning">{{ dallPower }}算力</el-text></el-text
>
&nbsp;&nbsp;
<el-text type="primary"
>当前可用
<el-text type="warning"> {{ power }}算力</el-text>
@@ -113,21 +81,16 @@
</el-form>
</div>
<div class="submit-btn">
<el-button type="primary" :dark="false" round @click="generate">
立即生成
</el-button>
<el-button type="primary" :dark="false" round @click="generate"> 立即生成 </el-button>
</div>
</div>
<div class="task-list-box">
<div
class="task-list-inner"
:style="{ height: listBoxHeight + 'px' }"
>
<div class="task-list-box pl-6 pr-6 pb-4 pt-4">
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
<div class="job-list-box">
<h2>任务列表</h2>
<h2 class="text-xl">任务列表</h2>
<task-list :list="runningJobs" />
<template v-if="finishedJobs.length > 0">
<h2>创作记录</h2>
<h2 class="text-xl">创作记录</h2>
<div class="finish-job-list">
<div v-if="finishedJobs.length > 0">
<v3-waterfall
@@ -170,22 +133,12 @@
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover
title="错误详情"
trigger="click"
:width="250"
:content="slotProp.item['err_msg']"
placement="top"
>
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button
type="danger"
@click="removeImage(slotProp.item)"
>删除</el-button
>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
</div>
</div>
</div>
@@ -203,43 +156,21 @@
<div class="remove">
<el-tooltip content="删除" placement="top">
<el-button
type="danger"
:icon="Delete"
@click="removeImage(slotProp.item)"
circle
/>
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle />
</el-tooltip>
<el-tooltip
content="取消分享"
placement="top"
v-if="slotProp.item.publish"
>
<el-button
type="warning"
@click="publishImage(slotProp.item, false)"
circle
>
<el-tooltip content="取消分享" placement="top" v-if="slotProp.item.publish">
<el-button type="warning" @click="publishImage(slotProp.item, false)" circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
</el-tooltip>
<el-tooltip content="分享" placement="top" v-else>
<el-button
type="success"
@click="publishImage(slotProp.item, true)"
circle
>
<el-button type="success" @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
</el-tooltip>
<el-tooltip content="复制提示词" placement="top">
<el-button
type="info"
circle
class="copy-prompt"
:data-clipboard-text="slotProp.item.prompt"
>
<el-button type="info" circle class="copy-prompt" :data-clipboard-text="slotProp.item.prompt">
<i class="iconfont icon-file"></i>
</el-button>
</el-tooltip>
@@ -255,12 +186,7 @@
</template>
</v3-waterfall>
</div>
<el-empty
:image-size="100"
:image="nodata"
description="暂无记录"
v-else
/>
<el-empty :image-size="100" :image="nodata" description="暂无记录" v-else />
</div>
</template>
@@ -318,19 +244,19 @@ window.onresize = () => {
};
const qualities = [
{ name: "标准", value: "standard" },
{ name: "高清", value: "hd" }
{ name: "高清", value: "hd" },
];
const sizes = ["1024x1024", "1792x1024", "1024x1792"];
const styles = [
{ name: "生动", value: "vivid" },
{ name: "自然", value: "natural" }
{ name: "自然", value: "natural" },
];
const params = ref({
client_id: getClientId(),
quality: "standard",
size: "1024x1024",
style: "vivid",
prompt: ""
prompt: "",
});
const finishedJobs = ref([]);
@@ -417,17 +343,14 @@ const fetchFinishJobs = () => {
loading.value = true;
page.value = page.value + 1;
httpGet(
`/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`
)
httpGet(`/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`)
.then((res) => {
if (res.data.items.length < pageSize.value) {
isOver.value = true;
}
const imageList = res.data.items;
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] =
imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
}
if (page.value === 1) {
finishedJobs.value = imageList;
@@ -469,7 +392,7 @@ const removeImage = (item) => {
ElMessageBox.confirm("此操作将会删除任务和图片,继续操作码?", "删除提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning"
type: "warning",
})
.then(() => {
httpGet("/api/dall/remove", { id: item.id })

View File

@@ -3,64 +3,24 @@
<div class="tab-box">
<div class="flex-center-col big-top-title xxx">
<div class="flex-center-col" @click="isCollapse = !isCollapse">
<el-icon v-if="isCollapse" class="openicon">
<svg
t="1733138242826"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1853"
id="mx_n_1733138242827"
width="200"
height="200"
>
<path
d="M715 267c135.31 0 245 109.69 245 245S850.31 757 715 757H309C173.69 757 64 647.31 64 512s109.69-245 245-245h406zM309 367c-80.081 0-145 64.919-145 145s64.919 145 145 145 145-64.919 145-145-64.919-145-145-145z"
fill="#754ff6"
p-id="1854"
></path>
</svg>
</el-icon>
<el-tooltip content="展开菜单" placement="right" v-if="isCollapse">
<i class="iconfont icon-expand"></i>
</el-tooltip>
</div>
<div class="flex" :class="{ 'top-collapse': !isCollapse }">
<div class="top-avatar flex">
<span class="title" v-if="!isCollapse">GeekAI</span>
<img
v-if="loginUser.id"
:src="!!loginUser.avatar ? loginUser.avatar : avatarImg"
alt=""
:class="{ marr: !isCollapse }"
/>
<img v-if="loginUser.id" :src="!!loginUser.avatar ? loginUser.avatar : avatarImg" alt="" :class="{ marr: !isCollapse }" />
</div>
<div class="menuIcon xxx" @click="isCollapse = !isCollapse">
<el-icon v-if="!isCollapse" class="openicon">
<svg
t="1733138405307"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2064"
width="200"
height="200"
>
<path
d="M715 267c135.31 0 245 109.69 245 245S850.31 757 715 757H309C173.69 757 64 647.31 64 512s109.69-245 245-245h406z m0 100c-80.081 0-145 64.919-145 145s64.919 145 145 145 145-64.919 145-145-64.919-145-145-145z"
fill="#754ff6"
p-id="2065"
></path>
</svg>
</el-icon>
<el-tooltip content="关闭菜单" placement="right" v-if="!isCollapse">
<i class="iconfont icon-colspan"></i>
</el-tooltip>
</div>
</div>
</div>
<div
class="menu-list"
:style="{ width: isCollapse ? '65px' : '170px' }"
:class="{ 'menu-list-collapse': !isCollapse }"
>
<div class="menu-list" :style="{ width: isCollapse ? '65px' : '170px' }" :class="{ 'menu-list-collapse': !isCollapse }">
<ul>
<li
class="menu-list-item flex-center-col"
@@ -69,11 +29,11 @@
@click="changeNav(item)"
:class="item.url === curPath ? 'active' : ''"
>
<el-image :src="item.icon" class="el-icon" />
<div
class="menu-title"
:class="{ 'menu-title-collapse': !isCollapse }"
>
<span v-if="item.icon.startsWith('icon')" :class="{ 'mr-1 ml-2': !isCollapse }">
<i class="iconfont" :class="item.icon"></i>
</span>
<el-image :src="item.icon" class="el-icon ml-1" v-else />
<div class="menu-title" :class="{ 'menu-title-collapse': !isCollapse }">
{{ item.name }}
</div>
</li>
@@ -91,11 +51,7 @@
<div class="bot" :style="{ width: isCollapse ? '65px' : '170px' }">
<div class="bot-line"></div>
<el-popover
v-if="moreNavs.length > 0"
placement="right-end"
trigger="hover"
>
<el-popover v-if="moreNavs.length > 0" placement="right-end" trigger="hover">
<template #reference>
<li class="menu-list-item flex-center-col">
<el-icon><CirclePlus /></el-icon>
@@ -110,28 +66,21 @@
:class="{
active: item.url === curPath,
moreTitle: index !== 3 && index !== 4,
twoTittle: index === 3 || index === 4
twoTittle: index === 3 || index === 4,
}"
>
<a @click="changeNav(item)">
<el-image
:src="item.icon"
style="width: 20px; height: 20px"
/>
<span
:class="item.url === curPath ? 'title active' : 'title'"
>{{ item.name }}</span
>
<span v-if="item.icon.startsWith('icon')">
<i class="iconfont" :class="item.icon"></i>
</span>
<el-image :src="item.icon" style="width: 20px; height: 20px" v-else />
<span :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</span>
</a>
</li>
</ul>
</template>
</el-popover>
<el-popover
placement="right-end"
trigger="hover"
v-if="loginUser.id"
>
<el-popover placement="right-end" trigger="hover" v-if="loginUser.id">
<template #reference>
<li class="menu-list-item flex-center-col">
<el-icon><Setting /></el-icon>
@@ -145,9 +94,7 @@
<el-icon>
<UserFilled />
</el-icon>
<span class="username title">{{
loginUser.nickname
}}</span>
<span class="username title">{{ loginUser.nickname }}</span>
</div>
</li>
<li>
@@ -160,12 +107,7 @@
</template>
</el-popover>
<li class="menu-bot-item">
<a
:href="gitURL"
class="link-button"
target="_blank"
v-if="!license.de_copy && !isCollapse"
>
<a :href="gitURL" class="link-button" target="_blank" v-if="!license.de_copy && !isCollapse">
<i class="iconfont icon-github"></i>
</a>
@@ -189,12 +131,7 @@
@click="showNoticeLogin = true"
></div>
<div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
<el-button
@click="router.push('/login')"
class="btn-go animate__animated animate__pulse animate__infinite"
round
>登录</el-button
>
<el-button @click="router.push('/login')" class="btn-go animate__animated animate__pulse animate__infinite" round>登录</el-button>
</div>
<!-- <div class="content custom-scroll"> -->
<div class="content custom-scroll">
@@ -206,35 +143,27 @@
</div>
<!-- </div> -->
</el-scrollbar>
<config-dialog
v-if="loginUser.id"
:show="showConfigDialog"
@hide="showConfigDialog = false"
/>
<config-dialog v-if="loginUser.id" :show="showConfigDialog" @hide="showConfigDialog = false" />
<el-dialog v-model="showNoticeLogin">
<el-result icon="warning" title="未登录" sub-title="登录后解锁功能">
<template #extra>
<el-button type="primary" @click="router.push('/login')">登录</el-button>
</template>
</el-result>
</el-dialog>
</div>
<el-dialog v-model="showNoticeLogin">
<el-result icon="warning" title="未登录" sub-title="登录后解锁功能">
<template #extra>
<el-button type="primary" @click="router.push('/login')"
>登录</el-button
>
</template>
</el-result>
</el-dialog>
</template>
<script setup>
import { CirclePlus, Setting } from "@element-plus/icons-vue";
import { CirclePlus, Setting, UserFilled } from "@element-plus/icons-vue";
import ThemeChange from "@/components/ThemeChange.vue";
import avatarImg from "@/assets/img/avatar.jpg";
import { useRouter } from "vue-router";
import { onMounted, ref, watch } from "vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { UserFilled } from "@element-plus/icons-vue";
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
import { removeUserToken } from "@/store/session";
import LoginDialog from "@/components/LoginDialog.vue";
import { useSharedStore } from "@/store/sharedata";
import ConfigDialog from "@/components/UserInfoDialog.vue";
import { showMessageError } from "@/utils/dialog";
@@ -249,7 +178,6 @@ const curPath = ref();
const title = ref("");
const showNoticeLogin = ref(false);
// const mainWinHeight = window.innerHeight - 50;
/**
* 从路径名中提取第一个路径段
@@ -276,15 +204,9 @@ const getFirstPathSegment = (url) => {
}
};
const loginUser = ref({});
const mainWinHeight = loginUser.value.id
? window.innerHeight
: window.innerHeight;
const version = ref(process.env.VUE_APP_VERSION);
const routerViewKey = ref(0);
const showConfigDialog = ref(false);
const license = ref({ de_copy: true });
const docsURL = ref(process.env.VUE_APP_DOCS_URL);
const gitURL = ref(process.env.VUE_APP_GIT_URL);
const store = useSharedStore();
@@ -380,12 +302,6 @@ const logout = function () {
ElMessage.error("注销失败!");
});
};
const loginCallback = () => {
init();
// 刷新组件
routerViewKey.value += 1;
};
</script>
<style lang="stylus" scoped>

View File

@@ -1,297 +0,0 @@
<template>
<div class="home">
<div class="header">
<div class="banner">
<div class="logo">
<el-image :src="logo" @click="router.push('/')" />
</div>
<div class="title">
<span>{{ title }}</span>
</div>
</div>
<div class="navbar">
<el-tooltip
v-if="!license.de_copy"
class="box-item"
effect="light"
content="部署文档"
placement="bottom"
>
<a :href="docsURL" class="link-button" target="_blank">
<i class="iconfont icon-book"></i>
</a>
</el-tooltip>
<el-tooltip
v-if="!license.de_copy"
class="box-item"
effect="light"
content="项目源码"
placement="bottom"
>
<a
href="https://github.com/yangjian102621/chatgpt-plus"
class="link-button"
target="_blank"
>
<i class="iconfont icon-github"></i>
</a>
</el-tooltip>
<el-dropdown
:hide-on-click="true"
class="user-info"
trigger="click"
v-if="loginUser.id"
>
<span class="el-dropdown-link">
<el-image :src="loginUser.avatar" />
</span>
<template #dropdown>
<el-dropdown-menu class="user-info-menu">
<el-dropdown-item @click="showConfigDialog = true">
<el-icon>
<UserFilled />
</el-icon>
<span class="username">{{ loginUser.nickname }}</span>
</el-dropdown-item>
<div v-if="!license.de_copy">
<el-dropdown-item>
<i class="iconfont icon-book"></i>
<a :href="docsURL" target="_blank"> 用户手册 </a>
</el-dropdown-item>
<el-dropdown-item>
<i class="iconfont icon-github"></i>
<a :href="gitURL" target="_blank"> GeekAI {{ version }} </a>
</el-dropdown-item>
</div>
<el-divider style="margin: 2px 0" />
<el-dropdown-item @click="logout">
<i class="iconfont icon-logout"></i>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div v-else>
<el-button
size="small"
color="#21aa93"
@click="store.setShowLoginDialog(true)"
round
>登录</el-button
>
</div>
</div>
</div>
<div class="main">
<div class="navigator">
<ul class="nav-items">
<li v-for="item in mainNavs" :key="item.url">
<el-tooltip effect="light" :content="item.name" placement="right">
<a
@click="changeNav(item)"
:class="item.url === curPath ? 'active' : ''"
>
<el-image :src="item.icon" style="width: 30px; height: 30px" />
</a>
</el-tooltip>
<span :class="item.url === curPath ? 'title active' : 'title'">{{
item.name
}}</span>
</li>
<el-popover
v-if="moreNavs.length > 0"
placement="right-end"
trigger="hover"
>
<template #reference>
<li>
<a class="active">
<el-image
src="/images/menu/more.png"
style="width: 30px; height: 30px"
/>
</a>
</li>
</template>
<template #default>
<ul class="more-menus">
<li
v-for="item in moreNavs"
:key="item.url"
:class="item.url === curPath ? 'active' : ''"
>
<a @click="changeNav(item)">
<el-image
:src="item.icon"
style="width: 20px; height: 20px"
/>
<span
:class="item.url === curPath ? 'title active' : 'title'"
>{{ item.name }}</span
>
</a>
</li>
</ul>
</template>
</el-popover>
</ul>
</div>
<div
class="content custom-scroll"
:style="{ height: mainWinHeight + 'px' }"
>
<router-view :key="routerViewKey" v-slot="{ Component }">
<transition name="move" mode="out-in">
<component :is="Component"></component>
</transition>
</router-view>
</div>
</div>
<login-dialog
:show="show"
@hide="store.setShowLoginDialog(false)"
@success="loginCallback"
/>
<config-dialog
v-if="loginUser.id"
:show="showConfigDialog"
@hide="showConfigDialog = false"
/>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
import { onMounted, ref, watch } from "vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { UserFilled } from "@element-plus/icons-vue";
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
import { removeUserToken } from "@/store/session";
import LoginDialog from "@/components/LoginDialog.vue";
import { useSharedStore } from "@/store/sharedata";
import ConfigDialog from "@/components/UserInfoDialog.vue";
import { showMessageError } from "@/utils/dialog";
const router = useRouter();
const logo = ref("");
const mainNavs = ref([]);
const moreNavs = ref([]);
const curPath = ref(router.currentRoute.value.path);
const title = ref("");
const mainWinHeight = window.innerHeight - 50;
const loginUser = ref({});
const version = ref(process.env.VUE_APP_VERSION);
const routerViewKey = ref(0);
const showConfigDialog = ref(false);
const license = ref({ de_copy: true });
const docsURL = ref(process.env.VUE_APP_DOCS_URL);
const gitURL = ref(process.env.VUE_APP_GIT_URL);
const store = useSharedStore();
const show = ref(false);
watch(
() => store.showLoginDialog,
(newValue) => {
show.value = newValue;
}
);
// 监听路由变化
router.beforeEach((to, from, next) => {
curPath.value = to.path;
next();
});
if (curPath.value === "/external") {
curPath.value = router.currentRoute.value.query.url;
}
const changeNav = (item) => {
curPath.value = item.url;
if (item.url.indexOf("http") !== -1) {
// 外部链接
router.push({ name: "ExternalLink", query: { url: item.url } });
} else {
router.push(item.url);
}
};
onMounted(() => {
getSystemInfo()
.then((res) => {
logo.value = res.data.logo;
title.value = res.data.title;
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
});
// 获取菜单
httpGet("/api/menu/list")
.then((res) => {
mainNavs.value = res.data;
// 根据窗口的高度计算应该显示多少菜单
const rows = Math.floor((window.innerHeight - 100) / 90);
if (res.data.length > rows) {
mainNavs.value = res.data.slice(0, rows);
moreNavs.value = res.data.slice(rows);
}
})
.catch((e) => {
ElMessage.error("获取系统菜单失败:" + e.message);
});
getLicenseInfo()
.then((res) => {
license.value = res.data;
})
.catch((e) => {
license.value = { de_copy: false };
showMessageError("获取 License 配置:" + e.message);
});
init();
});
const init = () => {
checkSession()
.then((user) => {
loginUser.value = user;
})
.catch(() => {});
};
const logout = function () {
httpGet("/api/user/logout")
.then(() => {
removeUserToken();
store.setShowLoginDialog(true);
store.setIsLogin(false);
loginUser.value = {};
// 刷新组件
routerViewKey.value += 1;
})
.catch(() => {
ElMessage.error("注销失败!");
});
};
const loginCallback = () => {
init();
// 刷新组件
routerViewKey.value += 1;
};
</script>
<style lang="stylus" scoped>
@import "@/assets/css/custom-scroll.styl"
@import "@/assets/css/home.styl"
</style>

View File

@@ -18,20 +18,8 @@
<div class="param-line pt">
<el-row :gutter="10">
<el-col :span="8" v-for="item in rates" :key="item.value">
<div
class="flex-col items-center"
:class="
item.value === params.rate
? 'grid-content active'
: 'grid-content'
"
@click="changeRate(item)"
>
<el-image
class="icon"
:src="item.img"
fit="cover"
></el-image>
<div class="flex-col items-center" :class="item.value === params.rate ? 'grid-content active' : 'grid-content'" @click="changeRate(item)">
<el-image class="icon" :src="item.img" fit="cover"></el-image>
<div class="text">{{ item.text }}</div>
</div>
</el-col>
@@ -42,23 +30,10 @@
<el-form-item label="图片画质">
<template #default>
<div class="form-item-inner flex-row items-center">
<el-select
v-model="params.quality"
placeholder="请选择"
style="width: 175px"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
>
</el-option>
<el-select v-model="params.quality" placeholder="请选择" style="width: 150px">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
<el-tooltip
content="生成的图片质量,质量越好出图越慢"
placement="right"
>
<el-tooltip content="生成的图片质量,质量越好出图越慢" placement="right">
<el-icon>
<InfoFilled />
</el-icon>
@@ -70,11 +45,7 @@
<div class="param-line pt">
<span>模型选择</span>
<el-tooltip
content="MJ: 偏真实通用模型 <br/>NIJI: 偏动漫风格、适用于二次元模型"
raw-content
placement="right"
>
<el-tooltip content="MJ: 偏真实通用模型 <br/>NIJI: 偏动漫风格、适用于二次元模型" raw-content placement="right">
<el-icon>
<InfoFilled />
</el-icon>
@@ -83,12 +54,7 @@
<div class="param-line pt">
<el-row :gutter="10">
<el-col :span="12" v-for="item in models" :key="item.value">
<div
:class="
item.value === params.model ? 'model active' : 'model'
"
@click="changeModel(item)"
>
<div :class="item.value === params.model ? 'model active' : 'model'" @click="changeModel(item)">
<el-image :src="item.img" fit="cover"></el-image>
<div class="text">{{ item.text }}</div>
</div>
@@ -101,11 +67,7 @@
<template #default>
<div class="form-item-inner">
<el-switch v-model="params.tile" inactive-color="#464649" />
<el-tooltip
content="重复:--tile参数释义生成可用作重复平铺的图像以创建无缝图案。"
raw-content
placement="right"
>
<el-tooltip content="重复:--tile参数释义生成可用作重复平铺的图像以创建无缝图案。" raw-content placement="right">
<el-icon>
<InfoFilled />
</el-icon>
@@ -138,12 +100,7 @@
<el-form-item label="创意度">
<template #default>
<div class="form-item-inner">
<el-slider
v-model.number="params.chaos"
:max="100"
:step="1"
style="width: 180px"
/>
<el-slider v-model.number="params.chaos" :max="100" :step="1" style="width: 180px" />
<el-tooltip
content="参数用法:--chaos 或--c取值范围: 0-100 <br/> 取值越高结果越发散,反之则稳定收敛<br /> 默认值0最为精准稳定"
raw-content
@@ -162,13 +119,7 @@
<el-form-item label="风格化">
<template #default>
<div class="form-item-inner">
<el-slider
v-model.number="params.stylize"
:min="0"
:max="1000"
:step="1"
style="width: 180px"
/>
<el-slider v-model.number="params.stylize" :min="0" :max="1000" :step="1" style="width: 180px" />
<el-tooltip
content="风格化:--stylize 或 --s范围 1-1000默认值100 <br/>高取值会产生非常艺术化但与提示关联性较低的图像"
raw-content
@@ -188,11 +139,7 @@
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.seed" />
<el-tooltip
content="随机种子:--seed默认值0表示随机产生 <br/>使用相同的种子参数和描述将产生相似的图像"
raw-content
placement="right"
>
<el-tooltip content="随机种子:--seed默认值0表示随机产生 <br/>使用相同的种子参数和描述将产生相似的图像" raw-content placement="right">
<el-icon>
<InfoFilled />
</el-icon>
@@ -204,25 +151,18 @@
</el-form>
</div>
</div>
<div class="task-list-box">
<div class="task-list-box pl-6 pr-6 pb-4">
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
<div class="extra-params">
<el-form>
<el-tabs
v-model="activeName"
class="title-tabs"
@tabChange="tabChange"
>
<el-tabs v-model="activeName" class="title-tabs" @tabChange="tabChange">
<el-tab-pane label="文生图" name="txt2img">
<div class="prompt-box">
<div class="param-line pt">
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>提示词</span>
<el-tooltip
content="输入你想要的内容,用逗号分割"
placement="right"
>
<el-tooltip content="输入你想要的内容,用逗号分割" placement="right">
<el-icon>
<InfoFilled />
</el-icon>
@@ -244,13 +184,7 @@
</div>
<el-row class="text-info">
<el-button
class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating">
<i class="iconfont icon-chuangzuo"></i>
<span>生成专业绘画指令</span>
</el-button>
@@ -260,10 +194,7 @@
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>不希望出现的内容可选</span>
<el-tooltip
content="不想出现在图片上的元素(例如:树,建筑)"
placement="right"
>
<el-tooltip content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
<el-icon>
<InfoFilled />
</el-icon>
@@ -284,33 +215,16 @@
</div>
</el-tab-pane>
<el-tab-pane label="图生图" name="img2img">
<div class="text">
图生图以某张图片为底稿参考来创作绘画生成类似风格或类型图像支持
PNG JPG 格式图片
</div>
<div class="text">图生图以某张图片为底稿参考来创作绘画生成类似风格或类型图像支持 PNG JPG 格式图片</div>
<div class="param-line">
<div class="img-inline">
<div class="img-list-box">
<div
class="img-item"
v-for="imgURL in imgList"
:key="imgURL"
>
<div class="img-item" v-for="imgURL in imgList" :key="imgURL">
<el-image :src="imgURL" fit="cover" />
<el-button
type="danger"
:icon="Delete"
@click="removeUploadImage(imgURL)"
circle
/>
<el-button type="danger" :icon="Delete" @click="removeUploadImage(imgURL)" circle />
</div>
</div>
<el-upload
class="img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadImg"
>
<el-upload class="img-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadImg">
<el-icon class="uploader-icon">
<Plus />
</el-icon>
@@ -322,12 +236,7 @@
<el-form-item label="参考权重:">
<template #default>
<div class="form-item-inner">
<el-slider
v-model.number="params.iw"
:max="1"
:step="0.01"
style="width: 180px"
/>
<el-slider v-model.number="params.iw" :max="1" :step="0.01" style="width: 180px" />
<el-tooltip
content="使用图像权重参数--iw来调整图像 URL 与文本的重要性 <br/>权重较高时意味着图像提示将对完成的作业产生更大的影响"
raw-content
@@ -347,10 +256,7 @@
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>提示词</span>
<el-tooltip
content="输入你想要的内容,用逗号分割"
placement="right"
>
<el-tooltip content="输入你想要的内容,用逗号分割" placement="right">
<el-icon>
<InfoFilled />
</el-icon>
@@ -372,13 +278,7 @@
</div>
<el-row class="text-info">
<el-button
class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating">
<i class="iconfont icon-chuangzuo"></i>
<span>生成专业绘画指令</span>
</el-button>
@@ -388,10 +288,7 @@
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>不希望出现的内容可选</span>
<el-tooltip
content="不想出现在图片上的元素(例如:树,建筑)"
placement="right"
>
<el-tooltip content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
<el-icon>
<InfoFilled />
</el-icon>
@@ -413,31 +310,15 @@
</el-tab-pane>
<el-tab-pane label="融图" name="blend">
<div class="text">
请上传两张以上的图片最多不超过五张超过五张图片请使用图生图功能
</div>
<div class="text">请上传两张以上的图片最多不超过五张超过五张图片请使用图生图功能</div>
<div class="img-inline">
<div class="img-list-box">
<div
class="img-item"
v-for="imgURL in imgList"
:key="imgURL"
>
<div class="img-item" v-for="imgURL in imgList" :key="imgURL">
<el-image :src="imgURL" fit="cover" />
<el-button
type="danger"
:icon="Delete"
@click="removeUploadImage(imgURL)"
circle
/>
<el-button type="danger" :icon="Delete" @click="removeUploadImage(imgURL)" circle />
</div>
</div>
<el-upload
class="img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadImg"
>
<el-upload class="img-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadImg">
<el-icon class="uploader-icon">
<Plus />
</el-icon>
@@ -446,31 +327,15 @@
</el-tab-pane>
<el-tab-pane label="换脸" name="swapFace">
<div class="text">
请上传两张有脸部的图片用左边图片的脸替换右边图片的脸
</div>
<div class="text">请上传两张有脸部的图片用左边图片的脸替换右边图片的脸</div>
<div class="img-inline">
<div class="img-list-box">
<div
class="img-item"
v-for="imgURL in imgList"
:key="imgURL"
>
<div class="img-item" v-for="imgURL in imgList" :key="imgURL">
<el-image :src="imgURL" fit="cover" />
<el-button
type="danger"
:icon="Delete"
@click="removeUploadImage(imgURL)"
circle
/>
<el-button type="danger" :icon="Delete" @click="removeUploadImage(imgURL)" circle />
</div>
</div>
<el-upload
class="img-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadImg"
>
<el-upload class="img-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadImg">
<el-icon class="uploader-icon">
<Plus />
</el-icon>
@@ -485,29 +350,17 @@
</el-badge>
</template>
<div class="text">
注意只有于 niji6 v6
模型支持一致性功能如果选择其他模型此功能将会生成失败
</div>
<div class="text">注意只有于 niji6 v6 模型支持一致性功能如果选择其他模型此功能将会生成失败</div>
<div class="param-line">
<el-form-item label="角色一致性:" prop="cref">
<el-input
v-model="params.cref"
placeholder="请输入图片URL或者上传图片"
style="
--el-input-focus-border-color: #b0a0f8;
max-width: 500px;
width: 100%;
"
style="--el-input-focus-border-color: #b0a0f8; max-width: 500px; width: 100%"
size="small"
>
<template #append>
<el-upload
:auto-upload="true"
:show-file-list="false"
@click="beforeUpload('cref')"
:http-request="uploadImg"
>
<el-upload :auto-upload="true" :show-file-list="false" @click="beforeUpload('cref')" :http-request="uploadImg">
<el-icon class="uploader-icon">
<UploadFilled />
</el-icon>
@@ -522,20 +375,11 @@
<el-input
v-model="params.sref"
placeholder="请输入图片URL或者上传图片"
style="
--el-input-focus-border-color: #b0a0f8;
max-width: 500px;
width: 100%;
"
style="--el-input-focus-border-color: #b0a0f8; max-width: 500px; width: 100%"
size="small"
>
<template #append>
<el-upload
:auto-upload="true"
:show-file-list="false"
@click="beforeUpload('sref')"
:http-request="uploadImg"
>
<el-upload :auto-upload="true" :show-file-list="false" @click="beforeUpload('sref')" :http-request="uploadImg">
<el-icon class="uploader-icon">
<UploadFilled />
</el-icon>
@@ -549,17 +393,8 @@
<el-form-item label="参考权重:">
<template #default>
<div class="form-item-inner">
<el-slider
v-model.number="params.cw"
:max="100"
:step="1"
style="width: 180px"
/>
<el-tooltip
content="取值范围 0-100 <br/>默认值100参考原图的脸部、头发和衣服<br/>0则表示只换脸"
raw-content
placement="right"
>
<el-slider v-model.number="params.cw" :max="100" :step="1" style="width: 180px" />
<el-tooltip content="取值范围 0-100 <br/>默认值100参考原图的脸部、头发和衣服<br/>0则表示只换脸" raw-content placement="right">
<el-icon>
<InfoFilled />
</el-icon>
@@ -574,10 +409,7 @@
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>提示词</span>
<el-tooltip
content="输入你想要的内容,用逗号分割"
placement="right"
>
<el-tooltip content="输入你想要的内容,用逗号分割" placement="right">
<el-icon>
<InfoFilled />
</el-icon>
@@ -600,10 +432,7 @@
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>不希望出现的内容可选</span>
<el-tooltip
content="不想出现在图片上的元素(例如:树,建筑)"
placement="right"
>
<el-tooltip content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
<el-icon>
<InfoFilled />
</el-icon>
@@ -629,30 +458,24 @@
<el-text type="primary"
>每次绘图消耗
<el-text type="warning">{{ mjPower }}算力;</el-text>
&nbsp;&nbsp; U/V 操作消耗<el-text type="warning"
>{{ mjActionPower }}算力;</el-text
> </el-text
&nbsp;&nbsp; U/V 操作消耗<el-text type="warning">{{ mjActionPower }}算力;</el-text> </el-text
>&nbsp;&nbsp;
<el-text type="primary"
>当前可用算力<el-text type="warning">{{
power
}}</el-text></el-text
>当前可用算力<el-text type="warning">{{ power }}</el-text></el-text
>
</el-row>
<div class="submit-btn">
<el-button type="primary" :dark="false" @click="generate" round
>立即生成</el-button
>
<el-button type="primary" :dark="false" @click="generate" round>立即生成</el-button>
</div>
</el-form>
</div>
<div class="job-list-box">
<h2>任务列表</h2>
<h2 class="text-xl">任务列表</h2>
<task-list :list="runningJobs" />
<template v-if="finishedJobs.length > 0">
<h2>创作记录</h2>
<h2 class="text-xl">创作记录</h2>
<div class="finish-job-list">
<div v-if="finishedJobs.length > 0">
<v3-waterfall
@@ -695,22 +518,12 @@
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover
title="错误详情"
trigger="click"
:width="250"
:content="slotProp.item['err_msg']"
placement="top"
>
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button
type="danger"
@click="removeImage(slotProp.item)"
>删除</el-button
>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
</div>
</div>
</div>
@@ -741,12 +554,7 @@
<a @click="upscale(4, slotProp.item)">U4</a>
</li>
<li class="show-prompt">
<el-popover
placement="left"
title="提示词"
:width="240"
trigger="hover"
>
<el-popover placement="left" title="提示词" :width="240" trigger="hover">
<template #reference>
<el-icon>
<ChromeFilled />
@@ -756,12 +564,7 @@
<template #default>
<div class="mj-list-item-prompt">
<span>{{ slotProp.item.prompt }}</span>
<el-icon
class="copy-prompt-mj"
:data-clipboard-text="
slotProp.item.prompt
"
>
<el-icon class="copy-prompt-mj" :data-clipboard-text="slotProp.item.prompt">
<DocumentCopy />
</el-icon>
</div>
@@ -789,48 +592,23 @@
</div>
</div>
<div
class="remove"
v-if="slotProp.item.progress === 100"
>
<div class="remove" v-if="slotProp.item.progress === 100">
<el-tooltip content="删除任务" placement="top">
<el-button
type="danger"
:icon="Delete"
@click="removeImage(slotProp.item)"
circle
/>
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle />
</el-tooltip>
<el-tooltip
content="取消发布"
placement="top"
v-if="slotProp.item.publish"
>
<el-button
type="warning"
@click="publishImage(slotProp.item, false)"
circle
>
<el-tooltip content="取消发布" placement="top" v-if="slotProp.item.publish">
<el-button type="warning" @click="publishImage(slotProp.item, false)" circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
</el-tooltip>
<el-tooltip content="发布图片" placement="top" v-else>
<el-button
type="success"
@click="publishImage(slotProp.item, true)"
circle
>
<el-button type="success" @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
</el-tooltip>
<el-tooltip content="复制提示词" placement="top">
<el-button
type="success"
class="copy-prompt-mj"
:data-clipboard-text="slotProp.item.prompt"
circle
>
<el-button type="success" class="copy-prompt-mj" :data-clipboard-text="slotProp.item.prompt" circle>
<el-icon><DocumentCopy /></el-icon>
</el-button>
</el-tooltip>
@@ -846,12 +624,7 @@
</template>
</v3-waterfall>
</div>
<el-empty
:image-size="100"
:image="nodata"
description="暂无记录"
v-else
/>
<el-empty :image-size="100" :image="nodata" description="暂无记录" v-else />
</div>
</template>
@@ -877,15 +650,7 @@
<script setup>
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import {
ChromeFilled,
Delete,
DocumentCopy,
InfoFilled,
Picture,
Plus,
UploadFilled
} from "@element-plus/icons-vue";
import { ChromeFilled, Delete, DocumentCopy, InfoFilled, Picture, Plus, UploadFilled } from "@element-plus/icons-vue";
import nodata from "@/assets/img/no-data.png";
import Compressor from "compressorjs";
import { httpGet, httpPost } from "@/utils/http";
@@ -930,14 +695,14 @@ const rates = [
css: "size16-9",
value: "16:9",
text: "16:9",
img: "/images/mj/rate_16_9.png"
img: "/images/mj/rate_16_9.png",
},
{
css: "size9-16",
value: "9:16",
text: "9:16",
img: "/images/mj/rate_9_16.png"
}
img: "/images/mj/rate_9_16.png",
},
];
const models = [
{ text: "写实模式MJ-6.1", value: " --v 6.1", img: "/images/mj/mj-v6.png" },
@@ -950,33 +715,33 @@ const models = [
{
text: "动漫风-niji5 可爱",
value: " --niji 5 --style cute",
img: "/images/mj/nj1.jpg"
img: "/images/mj/nj1.jpg",
},
{
text: "动漫风-niji5 风景",
value: " --niji 5 --style scenic",
img: "/images/mj/nj2.jpg"
img: "/images/mj/nj2.jpg",
},
{ text: "动漫风-niji6", value: " --niji 6", img: "/images/mj/nj3.jpg" }
{ text: "动漫风-niji6", value: " --niji 6", img: "/images/mj/nj3.jpg" },
];
const options = [
{
value: 0,
label: "默认"
label: "默认",
},
{
value: 0.25,
label: "普通"
label: "普通",
},
{
value: 0.5,
label: "清晰"
label: "清晰",
},
{
value: 1,
label: "高清"
}
label: "高清",
},
];
const router = useRouter();
@@ -997,7 +762,7 @@ const initParams = {
quality: 0,
cref: "",
sref: "",
cw: 0
cw: 0,
};
const params = ref(copyObj(initParams));
@@ -1086,7 +851,7 @@ const fetchRunningJobs = () => {
dangerouslyUseHTMLString: true,
message: `任务ID${jobs[i]["task_id"]}<br />原因:${jobs[i]["err_msg"]}`,
type: "error",
duration: 0
duration: 0,
});
if (jobs[i].type === "image") {
power.value += mjPower.value;
@@ -1114,19 +879,15 @@ const fetchFinishJobs = () => {
loading.value = true;
page.value = page.value + 1;
// 获取已完成的任务
httpGet(
`/api/mj/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`
)
httpGet(`/api/mj/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`)
.then((res) => {
const jobs = res.data.items;
for (let i = 0; i < jobs.length; i++) {
if (jobs[i]["img_url"] !== "") {
if (jobs[i].type === "upscale" || jobs[i].type === "swapFace") {
jobs[i]["thumb_url"] =
jobs[i]["img_url"] + "?imageView2/1/w/480/h/600/q/75";
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/600/q/75";
} else {
jobs[i]["thumb_url"] =
jobs[i]["img_url"] + "?imageView2/1/w/480/h/480/q/75";
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/480/q/75";
}
} else {
jobs[i]["thumb_url"] = "/images/img-placeholder.jpg";
@@ -1196,7 +957,7 @@ const uploadImg = (file) => {
},
error(err) {
console.log(err.message);
}
},
});
};
@@ -1249,7 +1010,7 @@ const send = (url, index, item) => {
message_id: item.message_id,
message_hash: item.hash,
session_id: getSessionId(),
prompt: item.prompt
prompt: item.prompt,
})
.then(() => {
ElMessage.success("任务推送成功,请耐心等待任务执行...");
@@ -1265,7 +1026,7 @@ const removeImage = (item) => {
ElMessageBox.confirm("此操作将会删除任务和图片,继续操作码?", "删除提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning"
type: "warning",
})
.then(() => {
httpGet("/api/mj/remove", { id: item.id, user_id: item.user_id })
@@ -1291,7 +1052,7 @@ const publishImage = (item, action) => {
httpGet("/api/mj/publish", {
id: item.id,
action: action,
user_id: item.user_id
user_id: item.user_id,
})
.then(() => {
ElMessage.success(text + "成功");

View File

@@ -11,19 +11,10 @@
<el-form-item label="采样方法">
<template #default>
<div class="form-item-inner">
<el-select v-model="params.sampler" style="width: 176px">
<el-option
v-for="item in samplers"
:label="item"
:value="item"
:key="item"
/>
<el-select v-model="params.sampler" style="width: 150px">
<el-option v-for="item in samplers" :label="item" :value="item" :key="item" />
</el-select>
<el-tooltip
content="出图效果比较好的一般是 Euler 和 DPM 系列算法"
raw-content
placement="right"
>
<el-tooltip content="出图效果比较好的一般是 Euler 和 DPM 系列算法" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@@ -37,22 +28,10 @@
<el-form-item label="采样调度">
<template #default>
<div class="form-item-inner">
<el-select
v-model="params.scheduler"
style="width: 176px"
>
<el-option
v-for="item in schedulers"
:label="item"
:value="item"
:key="item"
/>
<el-select v-model="params.scheduler" style="width: 150px">
<el-option v-for="item in schedulers" :label="item" :value="item" :key="item" />
</el-select>
<el-tooltip
content="推荐自动或者 Karras"
raw-content
placement="right"
>
<el-tooltip content="推荐自动或者 Karras" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@@ -68,16 +47,10 @@
<div class="form-item-inner">
<el-row :gutter="20">
<el-col :span="12">
<el-input
v-model.number="params.width"
placeholder="图片宽度"
/>
<el-input v-model.number="params.width" placeholder="图片宽度" />
</el-col>
<el-col :span="12">
<el-input
v-model.number="params.height"
placeholder="图片高度"
/>
<el-input v-model.number="params.height" placeholder="图片高度" />
</el-col>
</el-row>
</div>
@@ -90,11 +63,7 @@
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.steps" />
<el-tooltip
content="值越大则代表细节越多,同时也意味着出图速度越慢"
raw-content
placement="right"
>
<el-tooltip content="值越大则代表细节越多,同时也意味着出图速度越慢" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@@ -109,11 +78,7 @@
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.cfg_scale" />
<el-tooltip
content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果"
raw-content
placement="right"
>
<el-tooltip content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@@ -128,21 +93,13 @@
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.seed" />
<el-tooltip
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
raw-content
placement="right"
>
<el-tooltip content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
</el-tooltip>
<el-tooltip
content="使用随机数"
raw-content
placement="right"
>
<el-tooltip content="使用随机数" raw-content placement="right">
<el-icon @click="params.seed = -1" class="info-icon">
<Orange />
</el-icon>
@@ -156,16 +113,8 @@
<el-form-item label="高清修复">
<template #default>
<div class="form-item-inner">
<el-switch
v-model="params.hd_fix"
style="--el-switch-on-color: #47fff1"
size="large"
/>
<el-tooltip
content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节"
raw-content
placement="right"
>
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1" size="large" />
<el-tooltip content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节" raw-content placement="right">
<el-icon style="margin-left: 10px; top: 12px">
<InfoFilled />
</el-icon>
@@ -180,20 +129,8 @@
<el-form-item label="重绘幅度">
<template #default>
<div class="form-item-inner">
<el-slider
v-model.number="params.hd_redraw_rate"
:max="1"
:step="0.1"
style="
width: 180px;
--el-slider-main-bg-color: #47fff1;
"
/>
<el-tooltip
content="决定算法对图像内容的影响程度<br />较大的值将得到越有创意的图像"
raw-content
placement="right"
>
<el-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1" style="width: 180px; --el-slider-main-bg-color: #47fff1" />
<el-tooltip content="决定算法对图像内容的影响程度<br />较大的值将得到越有创意的图像" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@@ -207,22 +144,10 @@
<el-form-item label="放大算法">
<template #default>
<div class="form-item-inner">
<el-select
v-model="params.hd_scale_alg"
style="width: 176px"
>
<el-option
v-for="item in scaleAlg"
:label="item"
:value="item"
:key="item"
/>
<el-select v-model="params.hd_scale_alg" style="width: 176px">
<el-option v-for="item in scaleAlg" :label="item" :value="item" :key="item" />
</el-select>
<el-tooltip
content="高清修复放大算法主流算法有Latent和ESRGAN_4x"
raw-content
placement="right"
>
<el-tooltip content="高清修复放大算法主流算法有Latent和ESRGAN_4x" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@@ -237,11 +162,7 @@
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.hd_scale" />
<el-tooltip
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
raw-content
placement="right"
>
<el-tooltip content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@@ -256,11 +177,7 @@
<template #default>
<div class="form-item-inner">
<el-input v-model.number="params.hd_steps" />
<el-tooltip
content="重绘迭代步数如果设置为0则设置跟原图相同的迭代步数"
raw-content
placement="right"
>
<el-tooltip content="重绘迭代步数如果设置为0则设置跟原图相同的迭代步数" raw-content placement="right">
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
@@ -284,77 +201,48 @@
</div>
<el-row class="text-info">
<el-button
class="generate-btn"
size="small"
@click="generatePrompt"
color="#5865f2"
:disabled="isGenerating"
>
<i
class="iconfont icon-chuangzuo"
style="margin-right: 5px"
></i>
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating">
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i>
<span>生成专业绘画指令</span>
</el-button>
</el-row>
<div class="param-line pt">
<span>反向提示词</span>
<el-tooltip
content="不希望出现的元素,下面给了默认的起手式"
placement="right"
>
<el-tooltip content="不希望出现的元素,下面给了默认的起手式" placement="right">
<el-icon class="info-icon">
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
<div class="param-line">
<el-input
v-model="params.neg_prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
placeholder="反向提示词"
/>
<el-input v-model="params.neg_prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea" placeholder="反向提示词" />
</div>
<div class="text-info">
<el-row :gutter="10">
<el-col :span="12">
<el-text type="primary"
>单次绘图消耗
<el-text type="warning">{{ sdPower }}算力;</el-text>
</el-text>
</el-col>
<el-col :span="12">
<el-text type="primary"
>当前可用
<el-text type="warning">
{{ power }}算力</el-text
></el-text
>
</el-col>
<el-text type="primary"
>单次绘图消耗
<el-text type="warning">{{ sdPower }}算力</el-text>
</el-text>
<el-text type="primary"
>当前可用 <el-text type="warning"> {{ power }}算力</el-text></el-text
>
</el-row>
</div>
</el-form>
</div>
<div class="submit-btn">
<el-button type="primary" :dark="false" round @click="generate"
>立即生成</el-button
>
<el-button type="primary" :dark="false" round @click="generate">立即生成</el-button>
</div>
</div>
<div class="task-list-box">
<div
class="task-list-inner"
:style="{ height: listBoxHeight + 'px' }"
>
<div class="task-list-box pl-6 pr-6 pb-4 pt-4">
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
<div class="job-list-box">
<h2>任务列表</h2>
<h2 class="text-xl">任务列表</h2>
<task-list :list="runningJobs" />
<template v-if="finishedJobs.length > 0">
<h2>创作记录</h2>
<h2 class="text-xl">创作记录</h2>
<div class="finish-job-list">
<div v-if="finishedJobs.length > 0">
<v3-waterfall
@@ -377,55 +265,25 @@
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover
title="错误详情"
trigger="click"
:width="250"
:content="slotProp.item['err_msg']"
placement="top"
>
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button
type="danger"
@click="removeImage(slotProp.item)"
>删除</el-button
>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
</div>
</div>
</div>
</template>
</el-image>
<div v-else>
<el-image
:src="slotProp.item['img_thumb']"
@click="showTask(slotProp.item)"
fit="cover"
loading="lazy"
/>
<el-image :src="slotProp.item['img_thumb']" @click="showTask(slotProp.item)" fit="cover" loading="lazy" />
<div class="remove">
<el-button
type="danger"
:icon="Delete"
@click="removeImage(slotProp.item)"
circle
/>
<el-button
type="warning"
v-if="slotProp.item.publish"
@click="publishImage(slotProp.item, false)"
circle
>
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle />
<el-button type="warning" v-if="slotProp.item.publish" @click="publishImage(slotProp.item, false)" circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
<el-button
type="success"
v-else
@click="publishImage(slotProp.item, true)"
circle
>
<el-button type="success" v-else @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
</div>
@@ -441,12 +299,7 @@
</template>
</v3-waterfall>
</div>
<el-empty
:image-size="100"
v-else
:image="nodata"
description="暂无记录"
/>
<el-empty :image-size="100" v-else :image="nodata" description="暂无记录" />
</div>
</template>
@@ -459,17 +312,10 @@
</div>
<!-- 任务详情弹框 -->
<el-dialog
v-model="showTaskDialog"
title="绘画任务详情"
:fullscreen="true"
>
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
<el-row :gutter="20">
<el-col :span="16">
<div
class="img-container"
:style="{ maxHeight: fullImgHeight + 'px' }"
>
<div class="img-container" :style="{ maxHeight: fullImgHeight + 'px' }">
<el-image :src="item['img_url']" fit="contain" />
</div>
</el-col>
@@ -479,10 +325,7 @@
<el-divider> 正向提示词 </el-divider>
<div class="prompt">
<span>{{ item.prompt }}</span>
<el-icon
class="copy-prompt-sd"
:data-clipboard-text="item.prompt"
>
<el-icon class="copy-prompt-sd" :data-clipboard-text="item.prompt">
<DocumentCopy />
</el-icon>
</div>
@@ -492,10 +335,7 @@
<el-divider> 反向提示词 </el-divider>
<div class="prompt">
<span>{{ item.params.neg_prompt }}</span>
<el-icon
class="copy-prompt-sd"
:data-clipboard-text="item.params.neg_prompt"
>
<el-icon class="copy-prompt-sd" :data-clipboard-text="item.params.neg_prompt">
<DocumentCopy />
</el-icon>
</div>
@@ -511,9 +351,7 @@
<div class="info-line">
<div class="wrapper">
<label>图片尺寸</label>
<div class="item-value">
{{ item.params.width }} x {{ item.params.height }}
</div>
<div class="item-value">{{ item.params.width }} x {{ item.params.height }}</div>
</div>
</div>
@@ -572,9 +410,7 @@
</div>
<div class="copy-params">
<el-button type="primary" round @click="copyParams(item)"
>画一张同款的</el-button
>
<el-button type="primary" round @click="copyParams(item)">画一张同款的</el-button>
</div>
</div>
</el-col>
@@ -586,12 +422,7 @@
<script setup>
import { nextTick, onMounted, onUnmounted, ref } from "vue";
import {
Delete,
DocumentCopy,
InfoFilled,
Orange
} from "@element-plus/icons-vue";
import { Delete, DocumentCopy, InfoFilled, Orange } from "@element-plus/icons-vue";
import nodata from "@/assets/img/no-data.png";
import { httpGet, httpPost } from "@/utils/http";
@@ -623,15 +454,7 @@ resizeElement();
window.onresize = () => {
resizeElement();
};
const samplers = [
"Euler a",
"DPM++ 2S a",
"DPM++ 2M",
"DPM++ SDE",
"DPM++ 2M SDE",
"UniPC",
"Restart"
];
const samplers = ["Euler a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SDE", "UniPC", "Restart"];
const schedulers = ["Automatic", "Karras", "Exponential", "Uniform"];
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"];
const params = ref({
@@ -649,8 +472,7 @@ const params = ref({
hd_scale_alg: scaleAlg[0],
hd_steps: 0,
prompt: "",
neg_prompt:
"nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet"
neg_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
});
const runningJobs = ref([]);
@@ -745,17 +567,14 @@ const fetchFinishJobs = () => {
loading.value = true;
page.value = page.value + 1;
httpGet(
`/api/sd/jobs?finish=1&page=${page.value}&page_size=${pageSize.value}`
)
httpGet(`/api/sd/jobs?finish=1&page=${page.value}&page_size=${pageSize.value}`)
.then((res) => {
if (res.data.items.length < pageSize.value) {
isOver.value = true;
}
const imageList = res.data.items;
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] =
imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
}
if (page.value === 1) {
finishedJobs.value = imageList;
@@ -812,7 +631,7 @@ const removeImage = (item) => {
ElMessageBox.confirm("此操作将会删除任务和图片,继续操作码?", "删除提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning"
type: "warning",
})
.then(() => {
httpGet("/api/sd/remove", { id: item.id })

View File

@@ -2,7 +2,7 @@
<div class="page-images-wall">
<div class="inner custom-scroll">
<div class="header">
<h2>AI 绘画作品墙</h2>
<h2 class="text-xl pt-4 pb-4">AI 绘画作品墙</h2>
<div class="settings">
<el-radio-group v-model="imgType" @change="changeImgType">
<el-radio value="mj" size="large">MidJourney</el-radio>
@@ -11,11 +11,7 @@
</el-radio-group>
</div>
</div>
<div
class="waterfall"
:style="{ height: listBoxHeight + 'px' }"
id="waterfall-box"
>
<div class="waterfall" :style="{ height: listBoxHeight + 'px' }" id="waterfall-box">
<v3-waterfall
v-if="imgType === 'mj'"
id="waterfall"
@@ -54,24 +50,14 @@
</el-image>
</div>
<div class="opt">
<el-tooltip
class="box-item"
content="复制提示词"
placement="top"
>
<el-icon
class="copy-prompt-wall"
:data-clipboard-text="slotProp.item.prompt"
>
<el-tooltip class="box-item" content="复制提示词" placement="top">
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
<DocumentCopy />
</el-icon>
</el-tooltip>
<el-tooltip class="box-item" content="画同款" placement="top">
<i
class="iconfont icon-palette-pen"
@click="drawSameMj(slotProp.item)"
></i>
<i class="iconfont icon-palette-pen" @click="drawSameMj(slotProp.item)"></i>
</el-tooltip>
</div>
</div>
@@ -116,15 +102,8 @@
</el-image>
</div>
<div class="opt">
<el-tooltip
class="box-item"
content="复制提示词"
placement="top"
>
<el-icon
class="copy-prompt-wall"
:data-clipboard-text="slotProp.item.prompt"
>
<el-tooltip class="box-item" content="复制提示词" placement="top">
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
<DocumentCopy />
</el-icon>
</el-tooltip>
@@ -149,11 +128,7 @@
<template #default="slotProp">
<div class="list-item">
<div class="image">
<el-image
:src="slotProp.item['img_thumb']"
loading="lazy"
@click="showTask(slotProp.item)"
>
<el-image :src="slotProp.item['img_thumb']" loading="lazy" @click="showTask(slotProp.item)">
<template #placeholder>
<div class="image-slot">正在加载图片</div>
</template>
@@ -189,10 +164,7 @@
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
<el-row :gutter="20">
<el-col :span="16">
<div
class="img-container"
:style="{ maxHeight: fullImgHeight + 'px' }"
>
<div class="img-container" :style="{ maxHeight: fullImgHeight + 'px' }">
<el-image :src="item['img_url']" fit="contain">
<template #placeholder>
<div class="image-slot">正在加载图片</div>
@@ -214,10 +186,7 @@
<el-divider> 正向提示词 </el-divider>
<div class="prompt">
<span>{{ item.prompt }}</span>
<el-icon
class="copy-prompt-wall"
:data-clipboard-text="item.prompt"
>
<el-icon class="copy-prompt-wall" :data-clipboard-text="item.prompt">
<DocumentCopy />
</el-icon>
</div>
@@ -227,10 +196,7 @@
<el-divider> 反向提示词 </el-divider>
<div class="prompt">
<span>{{ item.params.negative_prompt }}</span>
<el-icon
class="copy-prompt-wall"
:data-clipboard-text="item.params.negative_prompt"
>
<el-icon class="copy-prompt-wall" :data-clipboard-text="item.params.negative_prompt">
<DocumentCopy />
</el-icon>
</div>
@@ -246,9 +212,7 @@
<div class="info-line">
<div class="wrapper">
<label>图片尺寸</label>
<div class="item-value">
{{ item.params.width }} x {{ item.params.height }}
</div>
<div class="item-value">{{ item.params.width }} x {{ item.params.height }}</div>
</div>
</div>
@@ -305,9 +269,7 @@
</div>
<div class="copy-params">
<el-button type="primary" round @click="drawSameSd(item)"
>画一张同款的</el-button
>
<el-button type="primary" round @click="drawSameSd(item)">画一张同款的</el-button>
</div>
</div>
</el-col>
@@ -329,7 +291,7 @@ import BackTop from "@/components/BackTop.vue";
const data = ref({
mj: [],
sd: [],
dall: []
dall: [],
});
const loading = ref(true);
const isOver = ref(false);
@@ -384,8 +346,7 @@ const getNext = () => {
// 生成缩略图
const imageList = res.data.items;
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] =
imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
}
if (data.value[imgType.value].length === 0) {
data.value[imgType.value] = imageList;
@@ -427,7 +388,7 @@ const changeImgType = () => {
data.value = {
mj: [],
sd: [],
dall: []
dall: [],
};
loading.value = true;
isOver.value = false;
@@ -443,7 +404,7 @@ const router = useRouter();
const drawSameSd = (row) => {
router.push({
name: "image-sd",
params: { copyParams: JSON.stringify(row.params) }
params: { copyParams: JSON.stringify(row.params) },
});
};

View File

@@ -12,22 +12,12 @@
</div>
<div class="menu-item">
<span v-if="!license.de_copy">
<el-tooltip
v-if="!license.de_copy"
class="box-item"
content="部署文档"
placement="bottom"
>
<a :href="docsURL" class="link-button" target="_blank">
<el-tooltip v-if="!license.de_copy" class="box-item" content="部署文档" placement="bottom">
<a :href="docsURL" class="link-button mr-2" target="_blank">
<i class="iconfont icon-book"></i>
</a>
</el-tooltip>
<el-tooltip
v-if="!license.de_copy"
class="box-item"
content="项目源码"
placement="bottom"
>
<el-tooltip v-if="!license.de_copy" class="box-item" content="项目源码" placement="bottom">
<a :href="gitURL" class="link-button" target="_blank">
<i class="iconfont icon-github"></i>
</a>
@@ -41,38 +31,25 @@
<el-button @click="router.push('/register')" class="shadow" round
>注册</el-button
> -->
<el-button
@click="router.push('/login')"
class="btn-go animate__animated animate__pulse animate__infinite"
round
>登录/注册</el-button
>
<el-button @click="router.push('/login')" class="btn-go animate__animated animate__pulse animate__infinite" round>登录/注册</el-button>
</span>
</div>
</el-menu>
</div>
<div class="content">
<div style="height: 158px"></div>
<h1 class="animate__animated animate__backInDown">
{{ title }}
</h1>
<div class="msg-text cursor-ani">
<span
v-for="(char, index) in displayedChars"
:key="index"
:style="{ color: rainbowColor(index) }"
>
<span v-for="(char, index) in displayedChars" :key="index" :style="{ color: rainbowColor(index) }">
{{ char }}
</span>
</div>
<div class="navs animate__animated animate__backInDown">
<el-space wrap :size="14">
<div
v-for="item in navs"
:key="item.url"
class="nav-item-box"
@click="router.push(item.url)"
>
<div v-for="item in navs" :key="item.url" class="nav-item-box" @click="router.push(item.url)">
<i :class="'iconfont ' + iconMap[item.url]"></i>
<div>{{ item.name }}</div>
</div>
@@ -121,7 +98,7 @@ const iconMap = ref({
"/apps": "icon-app",
"/member": "icon-vip-user",
"/invite": "icon-share",
"/luma": "icon-luma"
"/luma": "icon-luma",
});
const displayedChars = ref([]);
@@ -194,6 +171,5 @@ const rainbowColor = (index) => {
</script>
<style lang="stylus" scoped>
@import '@/assets/iconfont/iconfont.css'
@import "@/assets/css/index.styl"
</style>

View File

@@ -1,49 +1,32 @@
<template>
<div>
<div
class="member custom-scroll"
v-loading="loading"
element-loading-background="rgba(255,255,255,.3)"
:element-loading-text="loadingText"
>
<div class="member custom-scroll" v-loading="loading" element-loading-background="rgba(255,255,255,.3)" :element-loading-text="loadingText">
<div class="inner">
<div class="user-profile">
<user-profile :key="profileKey" />
<el-row class="user-opt" :gutter="20">
<el-col :span="12">
<el-button type="primary" @click="showBindEmailDialog = true"
>绑定邮箱</el-button
>
<el-button type="primary" @click="showBindEmailDialog = true">绑定邮箱</el-button>
</el-col>
<el-col :span="12">
<el-button type="primary" @click="showBindMobileDialog = true"
>绑定手机</el-button
>
<el-button type="primary" @click="showBindMobileDialog = true">绑定手机</el-button>
</el-col>
<el-col :span="12">
<el-button type="primary" @click="showThirdLoginDialog = true"
>第三方登录</el-button
>
<el-button type="primary" @click="showThirdLoginDialog = true">第三方登录</el-button>
</el-col>
<el-col :span="12">
<el-button type="primary" @click="showPasswordDialog = true"
>修改密码</el-button
>
<el-button type="primary" @click="showPasswordDialog = true">修改密码</el-button>
</el-col>
<el-col :span="24">
<el-button type="primary" @click="showRedeemVerifyDialog = true"
>卡密兑换
</el-button>
<el-button type="primary" @click="showRedeemVerifyDialog = true">卡密兑换 </el-button>
</el-col>
</el-row>
</div>
<div class="product-box">
<div class="info" v-if="orderPayInfoText !== ''">
<el-alert type="success" show-icon :closable="false" effect="dark">
<strong>说明:</strong> {{ vipInfoText }}
</el-alert>
<el-alert type="success" show-icon :closable="false" effect="dark"> <strong>说明:</strong> {{ vipInfoText }} </el-alert>
</div>
<el-row v-if="list.length > 0" :gutter="20" class="list-box">
@@ -68,9 +51,7 @@
</div>
<div class="info-line">
<span class="label">有效期</span>
<span class="expire" v-if="item.days > 0"
>{{ item.days }}</span
>
<span class="expire" v-if="item.days > 0">{{ item.days }}</span>
<span class="expire" v-else>长期有效</span>
</div>
@@ -80,41 +61,20 @@
</div>
<div class="pay-way">
<span
type="primary"
v-for="payWay in payWays"
@click="pay(item, payWay)"
:key="payWay"
>
<el-button
v-if="payWay.pay_type === 'alipay'"
color="#15A6E8"
circle
>
<span type="primary" v-for="payWay in payWays" @click="pay(item, payWay)" :key="payWay">
<el-button v-if="payWay.pay_type === 'alipay'" color="#15A6E8" circle>
<i class="iconfont icon-alipay"></i>
</el-button>
<el-button v-else-if="payWay.pay_type === 'qqpay'" circle>
<i class="iconfont icon-qq"></i>
</el-button>
<el-button
v-else-if="payWay.pay_type === 'paypal'"
class="paypal"
round
>
<el-button v-else-if="payWay.pay_type === 'paypal'" class="paypal" round>
<i class="iconfont icon-paypal"></i>
</el-button>
<el-button
v-else-if="payWay.pay_type === 'jdpay'"
color="#E1251B"
circle
>
<el-button v-else-if="payWay.pay_type === 'jdpay'" color="#E1251B" circle>
<i class="iconfont icon-jd-pay"></i>
</el-button>
<el-button
v-else-if="payWay.pay_type === 'douyin'"
class="douyin"
circle
>
<el-button v-else-if="payWay.pay_type === 'douyin'" class="douyin" circle>
<i class="iconfont icon-douyin"></i>
</el-button>
<el-button v-else circle class="wechat" color="#67C23A">
@@ -136,41 +96,14 @@
</div>
</div>
<password-dialog
v-if="isLogin"
:show="showPasswordDialog"
@hide="showPasswordDialog = false"
/>
<bind-mobile
v-if="isLogin"
:show="showBindMobileDialog"
@hide="showBindMobileDialog = false"
/>
<bind-email
v-if="isLogin"
:show="showBindEmailDialog"
@hide="showBindEmailDialog = false"
/>
<third-login
v-if="isLogin"
:show="showThirdLoginDialog"
@hide="showThirdLoginDialog = false"
/>
<redeem-verify
v-if="isLogin"
:show="showRedeemVerifyDialog"
@hide="redeemCallback"
/>
<password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false" />
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" @hide="showBindMobileDialog = false" />
<bind-email v-if="isLogin" :show="showBindEmailDialog" @hide="showBindEmailDialog = false" />
<third-login v-if="isLogin" :show="showThirdLoginDialog" @hide="showThirdLoginDialog = false" />
<redeem-verify v-if="isLogin" :show="showRedeemVerifyDialog" @hide="redeemCallback" />
</div>
<el-dialog
v-model="showDialog"
:show-close="false"
:close-on-click-modal="false"
hide-footer
width="auto"
class="pay-dialog"
>
<el-dialog v-model="showDialog" :show-close="false" :close-on-click-modal="false" hide-footer width="auto" class="pay-dialog">
<div v-if="qrImg !== ''">
<div class="product-info">
请使用微信扫码支付<span class="price">{{ price }}</span>
@@ -178,12 +111,8 @@
<el-image :src="qrImg" fit="cover" />
</div>
<div style="padding-bottom: 10px; text-align: center">
<el-button type="success" @click="payCallback(true)"
>支付成功</el-button
>
<el-button type="danger" @click="payCallback(false)"
>支付失败</el-button
>
<el-button type="success" @click="payCallback(true)">支付成功</el-button>
<el-button type="danger" @click="payCallback(false)">支付失败</el-button>
</div>
</el-dialog>
</div>
@@ -289,24 +218,20 @@ const pay = (product, payWay) => {
pay_type: payWay.pay_type,
user_id: user.value.id,
host: host,
device: "jump"
device: "jump",
})
.then((res) => {
showDialog.value = true;
loading.value = false;
if (payWay.pay_way === "wechat") {
price.value = Number(product.discount);
QRCode.toDataURL(
res.data,
{ width: 300, height: 300, margin: 2 },
(error, url) => {
if (error) {
console.error(error);
} else {
qrImg.value = url;
}
QRCode.toDataURL(res.data, { width: 300, height: 300, margin: 2 }, (error, url) => {
if (error) {
console.error(error);
} else {
qrImg.value = url;
}
);
});
} else {
window.open(res.data, "_blank");
}

View File

@@ -4,12 +4,7 @@
<div class="inner">
<div class="list-box">
<div class="handle-box">
<el-input
v-model="query.model"
placeholder="模型"
class="handle-input mr10"
clearable
></el-input>
<el-input v-model="query.model" placeholder="模型" class="handle-input mr10" clearable></el-input>
<el-date-picker
v-model="query.date"
type="daterange"
@@ -19,36 +14,23 @@
value-format="YYYY-MM-DD"
style="margin: 0 10px; width: 200px"
/>
<el-button type="primary" :icon="Search" @click="fetchData"
>搜索</el-button
>
<el-button type="primary" :icon="Search" @click="fetchData">搜索</el-button>
</div>
<el-row v-if="items.length > 0">
<el-table
:data="items"
:row-key="(row) => row.id"
table-layout="auto"
border
>
<el-table :data="items" :row-key="(row) => row.id" table-layout="auto" border>
<el-table-column prop="username" label="用户" width="130px" />
<el-table-column prop="model" label="模型" width="130px" />
<el-table-column prop="type" label="类型">
<template #default="scope">
<el-tag size="small" :type="tagColors[scope.row.type]">{{
scope.row.type_str
}}</el-tag>
<el-tag size="small" :type="tagColors[scope.row.type]">{{ scope.row.type_str }}</el-tag>
</template>
</el-table-column>
<el-table-column label="数额">
<template #default="scope">
<div>
<el-text type="success" v-if="scope.row.mark === 1"
>+{{ scope.row.amount }}</el-text
>
<el-text type="danger" v-if="scope.row.mark === 0"
>-{{ scope.row.amount }}</el-text
>
<el-text type="success" v-if="scope.row.mark === 1">+{{ scope.row.amount }}</el-text>
<el-text type="danger" v-if="scope.row.mark === 0">-{{ scope.row.amount }}</el-text>
</div>
</template>
</el-table-column>
@@ -75,12 +57,7 @@
/>
</div>
</el-row>
<el-empty
:image-size="100"
v-else
:image="nodata"
description="暂无数据"
/>
<el-empty :image-size="100" v-else :image="nodata" description="暂无数据" />
</div>
</div>
</div>
@@ -105,16 +82,9 @@ const loading = ref(false);
const listBoxHeight = window.innerHeight - 87;
const query = ref({
model: "",
date: []
date: [],
});
const tagColors = ref([
"primary",
"success",
"primary",
"danger",
"info",
"warning"
]);
const tagColors = ref(["primary", "success", "primary", "danger", "info", "warning"]);
onMounted(() => {
checkSession()
@@ -139,7 +109,7 @@ const fetchData = () => {
model: query.value.model,
date: query.value.date,
page: page.value,
page_size: pageSize.value
page_size: pageSize.value,
})
.then((res) => {
if (res.data) {
@@ -173,7 +143,7 @@ const fetchData = () => {
margin-top: 20px;
border-radius: 10px;
.handle-box {
padding 20px 0
padding 0 20px 20px 0
.el-input {
max-width 150px

View File

@@ -6,48 +6,27 @@
<el-tooltip content="定义模式" placement="top">
<black-switch v-model:value="custom" size="large" />
</el-tooltip>
<el-tooltip
content="请上传6-60秒的原始音频检测到人声的音频将仅设为私人音频。"
placement="bottom-end"
>
<el-upload
class="avatar-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadAudio"
accept=".wav,.mp3"
>
<el-tooltip content="请上传6-60秒的原始音频检测到人声的音频将仅设为私人音频。" placement="bottom-end">
<el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadAudio" accept=".wav,.mp3">
<el-button class="upload-music" round type="primary">
<i class="iconfont icon-upload"></i>
<span>上传音乐</span>
</el-button>
</el-upload>
</el-tooltip>
<black-select
v-model:value="data.model"
:options="models"
placeholder="请选择模型"
style="width: 100px"
/>
<black-select v-model:value="data.model" :options="models" placeholder="请选择模型" style="width: 100px" />
</div>
<div class="params">
<div class="pure-music">
<span class="switch"
><black-switch v-model:value="data.instrumental" size="default"
/></span>
<span class="switch"><black-switch v-model:value="data.instrumental" size="default" /></span>
<span class="text">纯音乐</span>
</div>
<div v-if="custom">
<div class="item-group" v-if="!data.instrumental">
<div class="label">
<span class="text">歌词</span>
<el-popover
placement="right"
:width="200"
trigger="hover"
content="自己写歌词或寻求 AI 的帮助。使用两节歌词8 行)可获得最佳效果。"
>
<el-popover placement="right" :width="200" trigger="hover" content="自己写歌词或寻求 AI 的帮助。使用两节歌词8 行)可获得最佳效果。">
<template #reference>
<el-icon>
<InfoFilled />
@@ -55,21 +34,9 @@
</template>
</el-popover>
</div>
<div
class="item"
v-loading="isGenerating"
element-loading-text="正在生成歌词..."
element-loading-background="rgba(122, 122, 122, 0.8)"
>
<black-input
v-model:value="data.lyrics"
type="textarea"
:rows="10"
:placeholder="promptPlaceholder"
/>
<button class="btn btn-lyric" @click="createLyric">
生成歌词
</button>
<div class="item" v-loading="isGenerating" element-loading-text="正在生成歌词..." element-loading-background="rgba(122, 122, 122, 0.8)">
<black-input v-model:value="data.lyrics" type="textarea" :rows="10" :placeholder="promptPlaceholder" />
<button class="btn btn-lyric" @click="createLyric">生成歌词</button>
</div>
</div>
@@ -90,24 +57,12 @@
</el-popover>
</div>
<div class="item">
<black-input
v-model:value="data.tags"
type="textarea"
:maxlength="120"
:rows="3"
placeholder="请输入音乐风格,多个风格之间用英文逗号隔开..."
/>
<black-input v-model:value="data.tags" type="textarea" :maxlength="120" :rows="3" placeholder="请输入音乐风格,多个风格之间用英文逗号隔开..." />
</div>
<div class="tag-select">
<div class="inner">
<span
class="tag"
@click="selectTag(tag)"
v-for="tag in tags"
:key="tag.value"
>{{ tag.label }}</span
>
<span class="tag" @click="selectTag(tag)" v-for="tag in tags" :key="tag.value">{{ tag.label }}</span>
</div>
</div>
</div>
@@ -115,12 +70,7 @@
<div class="item-group">
<div class="label">
<span class="text">歌曲名称</span>
<el-popover
placement="right"
:width="200"
trigger="hover"
content="给你的歌曲起一个标题,以便于分享、发现和组织。"
>
<el-popover placement="right" :width="200" trigger="hover" content="给你的歌曲起一个标题,以便于分享、发现和组织。">
<template #reference>
<el-icon>
<InfoFilled />
@@ -129,12 +79,7 @@
</el-popover>
</div>
<div class="item">
<black-input
v-model:value="data.title"
type="textarea"
:rows="1"
placeholder="请输入歌曲名称..."
/>
<black-input v-model:value="data.title" type="textarea" :rows="1" placeholder="请输入歌曲名称..." />
</div>
</div>
</div>
@@ -156,24 +101,14 @@
</el-popover>
</div>
<div class="item">
<black-input
v-model:value="data.prompt"
type="textarea"
:rows="10"
placeholder="例如:一首关于爱情的摇滚歌曲..."
/>
<black-input v-model:value="data.prompt" type="textarea" :rows="10" placeholder="例如:一首关于爱情的摇滚歌曲..." />
</div>
</div>
<div class="ref-song" v-if="refSong">
<div class="label">
<span class="text">续写</span>
<el-popover
placement="right"
:width="200"
trigger="hover"
content="输入额外的歌词,根据您之前的歌词来扩展歌曲。"
>
<el-popover placement="right" :width="200" trigger="hover" content="输入额外的歌词,根据您之前的歌词来扩展歌曲。">
<template #reference>
<el-icon>
<InfoFilled />
@@ -186,17 +121,9 @@
<div class="song">
<el-image :src="refSong.cover_url" fit="cover" />
<span class="title">{{ refSong.title }}</span>
<el-button
type="info"
@click="removeRefSong"
size="small"
:icon="Delete"
circle
/>
</div>
<div class="extend-secs">
<input v-model="refSong.extend_secs" type="text" /> 秒开始续写
<el-button type="info" @click="removeRefSong" size="small" :icon="Delete" circle />
</div>
<div class="extend-secs"> <input v-model="refSong.extend_secs" type="text" /> 秒开始续写</div>
</div>
</div>
@@ -208,11 +135,7 @@
</div>
</div>
</div>
<div
class="right-box"
v-loading="loading"
element-loading-background="rgba(100,100,100,0.3)"
>
<div class="right-box" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)">
<div class="list-box" v-if="!noData">
<div v-for="item in list" :key="item.id">
<div class="item" v-if="item.progress === 100">
@@ -227,12 +150,8 @@
</div>
<div class="center">
<div class="title">
<a :href="'/song/' + item.song_id" target="_blank">{{
item.title
}}</a>
<span class="model" v-if="item.major_model_version">{{
item.major_model_version
}}</span>
<a :href="'/song/' + item.song_id" target="_blank">{{ item.title }}</a>
<span class="model" v-if="item.major_model_version">{{ item.major_model_version }}</span>
<span class="model" v-if="item.type === 4">用户上传</span>
<span class="model" v-if="item.type === 3">
<i class="iconfont icon-mp3"></i>
@@ -246,50 +165,31 @@
<div class="tags" v-if="item.tags">{{ item.tags }}</div>
</div>
<div class="right">
<div class="tools">
<div class="tools grid grid-flow-row auto-rows-max">
<el-tooltip content="以当前歌曲为素材继续创作" placement="top">
<button class="btn" @click="extend(item)">续写</button>
</el-tooltip>
<button class="btn btn-publish">
<span class="text">发布</span>
<black-switch
v-model:value="item.publish"
@change="publishJob(item)"
size="small"
/>
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
</button>
<el-tooltip content="下载歌曲" placement="top">
<button class="btn btn-icon" @click="download(item)">
<i
class="iconfont icon-download"
v-if="!item.downloading"
></i>
<el-image
src="/images/loading.gif"
class="downloading"
fit="cover"
v-else
/>
<i class="iconfont icon-download" v-if="!item.downloading"></i>
<el-image src="/images/loading.gif" class="downloading" fit="cover" v-else />
</button>
</el-tooltip>
<el-tooltip
content="获取完整歌曲"
placement="top"
v-if="item.ref_song"
>
<el-tooltip content="获取完整歌曲" placement="top" v-if="item.ref_song">
<button class="btn btn-icon" @click="merge(item)">
<i class="iconfont icon-concat"></i>
</button>
</el-tooltip>
<el-tooltip content="复制歌曲链接" placement="top">
<button
class="btn btn-icon copy-link"
:data-clipboard-text="getShareURL(item)"
>
<button class="btn btn-icon copy-link" :data-clipboard-text="getShareURL(item)">
<i class="iconfont icon-share1"></i>
</button>
</el-tooltip>
@@ -329,12 +229,7 @@
</div>
</div>
</div>
<el-empty
:image-size="100"
:image="nodata"
description="没有任何作品,赶紧去创作吧!"
v-else
/>
<el-empty :image-size="100" :image="nodata" description="没有任何作品,赶紧去创作吧!" v-else />
<div class="pagination">
<el-pagination
@@ -351,37 +246,20 @@
</div>
<div class="music-player" v-if="showPlayer">
<music-player
:songs="playList"
ref="playerRef"
:show-close="true"
@close="showPlayer = false"
/>
<music-player :songs="playList" ref="playerRef" :show-close="true" @close="showPlayer = false" />
</div>
</div>
<black-dialog
v-model:show="showDialog"
title="修改歌曲"
@cancal="showDialog = false"
@confirm="updateSong"
:width="500 + 'px'"
>
<black-dialog v-model:show="showDialog" title="修改歌曲" @cancal="showDialog = false" @confirm="updateSong" :width="500 + 'px'">
<form class="form">
<div class="form-item">
<div class="label">歌曲名称</div>
<input class="input" v-model="editData.title" type="text" />
<el-input v-model="editData.title" type="text" />
</div>
<div class="form-item">
<div class="label">封面图片</div>
<el-upload
class="avatar-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="uploadCover"
accept=".png,.jpg,.jpeg,.bmp"
>
<el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadCover" accept=".png,.jpg,.jpeg,.bmp">
<el-avatar :src="editData.cover" shape="square" :size="100" />
</el-upload>
</div>
@@ -393,23 +271,23 @@
<script setup>
import nodata from "@/assets/img/no-data.png";
import {nextTick, onMounted, onUnmounted, ref, watch} from "vue";
import {Delete, InfoFilled} from "@element-plus/icons-vue";
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { Delete, InfoFilled } from "@element-plus/icons-vue";
import BlackSelect from "@/components/ui/BlackSelect.vue";
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
import BlackInput from "@/components/ui/BlackInput.vue";
import MusicPlayer from "@/components/MusicPlayer.vue";
import {compact} from "lodash";
import {httpDownload, httpGet, httpPost} from "@/utils/http";
import {showMessageError, showMessageOK} from "@/utils/dialog";
import {checkSession, getClientId} from "@/store/cache";
import {ElMessage, ElMessageBox} from "element-plus";
import {formatTime, replaceImg} from "@/utils/libs";
import { compact } from "lodash";
import { httpDownload, httpGet, httpPost } from "@/utils/http";
import { showMessageError, showMessageOK } from "@/utils/dialog";
import { checkSession, getClientId } from "@/store/cache";
import { ElMessage, ElMessageBox } from "element-plus";
import { formatTime, replaceImg } from "@/utils/libs";
import Clipboard from "clipboard";
import BlackDialog from "@/components/ui/BlackDialog.vue";
import Compressor from "compressorjs";
import Generating from "@/components/ui/Generating.vue";
import {useSharedStore} from "@/store/sharedata";
import { useSharedStore } from "@/store/sharedata";
// const winHeight = ref(window.innerHeight - 50);
const winHeight = ref(window.innerHeight - 20);
@@ -417,8 +295,8 @@ const winHeight = ref(window.innerHeight - 20);
const custom = ref(false);
const models = ref([
{ label: "v3.0", value: "chirp-v3-0" },
{label: "v3.5", value: "chirp-v3-5"},
{label: "v4.0", value: "chirp-v4"}
{ label: "v3.5", value: "chirp-v3-5" },
{ label: "v4.0", value: "chirp-v4" },
]);
const tags = ref([
{ label: "女声", value: "female vocals" },
@@ -436,7 +314,7 @@ const tags = ref([
{ label: "钢琴", value: "piano" },
{ label: "小提琴", value: "violin" },
{ label: "贝斯", value: "bass" },
{ label: "嘻哈", value: "hip hop" }
{ label: "嘻哈", value: "hip hop" },
]);
const data = ref({
client_id: getClientId(),
@@ -448,7 +326,7 @@ const data = ref({
instrumental: false,
ref_task_id: "",
extend_secs: 0,
ref_song_id: ""
ref_song_id: "",
});
const loading = ref(false);
const noData = ref(true);
@@ -605,7 +483,7 @@ const uploadAudio = (file) => {
httpPost("/api/suno/create", {
audio_url: res.data.url,
title: res.data.name,
type: 4
type: 4,
})
.then(() => {
fetchData(1);
@@ -680,16 +558,14 @@ const selectTag = (tag) => {
if (data.value.tags.length + tag.value.length >= 119) {
return;
}
data.value.tags = compact([...data.value.tags.split(","), tag.value]).join(
","
);
data.value.tags = compact([...data.value.tags.split(","), tag.value]).join(",");
};
const removeJob = (item) => {
ElMessageBox.confirm("此操作将会删除任务相关文件,继续操作码?", "删除提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning"
type: "warning",
})
.then(() => {
httpGet("/api/suno/remove", { id: item.id })
@@ -737,7 +613,7 @@ const uploadCover = (file) => {
},
error(err) {
console.log(err.message);
}
},
});
};

View File

@@ -4,7 +4,7 @@
<el-button type="primary" :icon="Plus" @click="addRole">新增</el-button>
</div>
<el-row>
<el-table :data="tableData" :border="parentBorder" style="width: 100%">
<el-table :data="tableData" border>
<el-table-column type="expand">
<template #default="props">
<div>
@@ -28,34 +28,19 @@
<el-table-column label="绑定模型" prop="model_name" />
<el-table-column label="启用状态">
<template #default="scope">
<el-switch
v-model="scope.row['enable']"
@change="roleSet('enable', scope.row)"
/>
<el-switch v-model="scope.row['enable']" @change="roleSet('enable', scope.row)" />
</template>
</el-table-column>
<el-table-column label="应用图标" prop="icon">
<template #default="scope">
<el-image
:src="scope.row.icon"
style="width: 45px; height: 45px; border-radius: 50%"
/>
<el-image :src="scope.row.icon" style="width: 45px; height: 45px; border-radius: 50%" />
</template>
</el-table-column>
<el-table-column label="打招呼信息" prop="hello_msg" />
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button
size="small"
type="primary"
@click="rowEdit(scope.$index, scope.row)"
>编辑</el-button
>
<el-popconfirm
title="确定要删除当前应用吗?"
@confirm="removeRole(scope.row)"
:width="200"
>
<el-button size="small" type="primary" @click="rowEdit(scope.$index, scope.row)">编辑</el-button>
<el-popconfirm title="确定要删除当前应用吗?" @confirm="removeRole(scope.row)" :width="200">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
@@ -65,35 +50,14 @@
</el-table>
</el-row>
<el-dialog
v-model="showDialog"
:title="optTitle"
:close-on-click-modal="false"
width="50%"
>
<el-form
:model="role"
label-width="120px"
ref="formRef"
label-position="left"
:rules="rules"
>
<el-dialog v-model="showDialog" :title="optTitle" :close-on-click-modal="false" width="50%">
<el-form :model="role" label-width="120px" ref="formRef" label-position="left" :rules="rules">
<el-form-item label="应用名称:" prop="name">
<el-input v-model="role.name" autocomplete="off" />
</el-form-item>
<el-form-item label="应用分类:" prop="tid">
<el-select
v-model="role.tid"
filterable
placeholder="请选择分类"
clearable
>
<el-option
v-for="item in appTypes"
:key="item.id"
:label="item.name"
:value="item.id"
/>
<el-select v-model="role.tid" filterable placeholder="请选择分类" clearable>
<el-option v-for="item in appTypes" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
@@ -104,30 +68,14 @@
<el-form-item label="应用图标:" prop="icon">
<el-input v-model="role.icon">
<template #append>
<el-upload
:auto-upload="true"
:show-file-list="false"
:http-request="uploadImg"
>
上传
</el-upload>
<el-upload :auto-upload="true" :show-file-list="false" :http-request="uploadImg"> 上传 </el-upload>
</template>
</el-input>
</el-form-item>
<el-form-item label="绑定模型:" prop="model_id">
<el-select
v-model="role.model_id"
filterable
placeholder="请选择模型"
clearable
>
<el-option
v-for="item in models"
:key="item.id"
:label="item.name"
:value="item.id"
/>
<el-select v-model="role.model_id" filterable placeholder="请选择模型" clearable>
<el-option v-for="item in models" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
@@ -141,12 +89,7 @@
<el-table-column label="对话应用" width="120">
<template #default="scope">
<el-select v-model="scope.row.role" placeholder="Role">
<el-option
v-for="value in messageRoles"
:key="value"
:label="value"
:value="value"
/>
<el-option v-for="value in messageRoles" :key="value" :label="value" :value="value" />
</el-select>
</template>
</el-table-column>
@@ -155,11 +98,7 @@
<div class="context-msg-key">
<span>对话内容</span>
<span class="fr">
<el-button
type="primary"
@click="addContext"
size="small"
>
<el-button type="primary" @click="addContext" size="small">
<el-icon>
<Plus />
</el-icon>
@@ -171,38 +110,17 @@
<template #default="scope">
<div class="context-msg-content">
<el-input
type="textarea"
:rows="3"
v-model="scope.row.content"
autocomplete="off"
v-loading="isGenerating"
/>
<el-input type="textarea" :rows="3" v-model="scope.row.content" autocomplete="off" v-loading="isGenerating" />
<span class="remove-item">
<el-tooltip
effect="dark"
content="删除当前行"
placement="right"
>
<el-tooltip effect="dark" content="删除当前行" placement="right">
<el-button circle type="danger" size="small">
<el-icon @click="removeContext(scope.$index)"
><Delete
/></el-icon>
<el-icon @click="removeContext(scope.$index)"><Delete /></el-icon>
</el-button>
</el-tooltip>
<el-popover
placement="right"
:width="400"
trigger="click"
>
<el-popover placement="right" :width="400" trigger="click">
<template #reference>
<el-button
type="primary"
circle
size="small"
class="icon-btn"
>
<el-button type="primary" circle size="small" class="icon-btn">
<i class="iconfont icon-linggan"></i>
</el-button>
</template>
@@ -214,16 +132,8 @@
placeholder="请您输入要 AI实现的目标任务或者需要AI扮演的角色"
/>
<el-row class="text-line">
<el-text type="info" size="small"
>使用 AI 生成 System 预设指令</el-text
>
<el-button
class="generate-btn"
size="small"
@click="generatePrompt(scope.row)"
color="#5865f2"
:disabled="isGenerating"
>
<el-text type="info" size="small">使用 AI 生成 System 预设指令</el-text>
<el-button class="generate-btn" size="small" @click="generatePrompt(scope.row)" color="#5865f2" :disabled="isGenerating">
<i class="iconfont icon-chuangzuo"></i>
<span>立即生成</span>
</el-button>
@@ -278,11 +188,9 @@ const rules = reactive({
icon: [{ required: true, message: "请输入应用图标", trigger: "blur" }],
sort: [
{ required: true, message: "请输入排序数字", trigger: "blur" },
{ type: "number", message: "请输入有效数字" }
{ type: "number", message: "请输入有效数字" },
],
hello_msg: [
{ required: true, message: "请输入打招呼信息", trigger: "change" }
]
hello_msg: [{ required: true, message: "请输入打招呼信息", trigger: "change" }],
});
const appTypes = ref([]);
@@ -339,9 +247,7 @@ const fetchData = () => {
return;
}
const sortedData = Array.from(from.children).map((row) =>
row.querySelector(".sort").getAttribute("data-id")
);
const sortedData = Array.from(from.children).map((row) => row.querySelector(".sort").getAttribute("data-id"));
const ids = [];
const sorts = [];
sortedData.forEach((id, index) => {
@@ -350,12 +256,10 @@ const fetchData = () => {
tableData.value[index].sort_num = index + 1;
});
httpPost("/api/admin/role/sort", { ids: ids, sorts: sorts }).catch(
(e) => {
ElMessage.error("排序失败:" + e.message);
}
);
}
httpPost("/api/admin/role/sort", { ids: ids, sorts: sorts }).catch((e) => {
ElMessage.error("排序失败:" + e.message);
});
},
});
};
@@ -363,7 +267,7 @@ const roleSet = (filed, row) => {
httpPost("/api/admin/role/set", {
id: row.id,
filed: filed,
value: row[filed]
value: row[filed],
})
.then(() => {
ElMessage.success("操作成功!");
@@ -448,7 +352,7 @@ const uploadImg = (file) => {
},
error(e) {
ElMessage.error("上传失败:" + e.message);
}
},
});
};

View File

@@ -20,22 +20,22 @@
</div>
</template>
<script setup>
import { useSidebarStore } from "@/store/sidebar";
import { useTagsStore } from "@/store/tags";
import {useSidebarStore} from "@/store/sidebar";
import {useTagsStore} from "@/store/tags";
import AdminHeader from "@/components/admin/AdminHeader.vue";
import AdminSidebar from "@/components/admin/AdminSidebar.vue";
import AdminTags from "@/components/admin/AdminTags.vue";
import { useRouter } from "vue-router";
import { checkAdminSession } from "@/store/cache";
import { ref, watch } from "vue";
import { useSharedStore } from "@/store/sharedata";
import {useRouter} from "vue-router";
import {checkAdminSession} from "@/store/cache";
import {ref, watch} from "vue";
import {useSharedStore} from "@/store/sharedata";
const sidebar = useSidebarStore();
const tags = useTagsStore();
const isLogin = ref(false);
const contentHeight = window.innerHeight - 80;
const store = useSharedStore();
const theme = ref(store.adminTheme);
const theme = ref(store.theme);
// 获取会话信息
const router = useRouter();
@@ -48,7 +48,7 @@ checkAdminSession()
});
watch(
() => store.adminTheme,
() => store.theme,
(val) => {
theme.value = val;
}
@@ -56,7 +56,5 @@ watch(
</script>
<style scoped lang="stylus">
// @import '@/assets/css/color-dark.styl';
@import '@/assets/css/main.styl';
@import '@/assets/iconfont/iconfont.css';
</style>

View File

@@ -1,12 +1,11 @@
<template>
<div class="container menu" v-loading="loading">
<div class="handle-box">
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
</div>
<el-row>
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
<el-table :data="items" :row-key="(row) => row.id" table-layout="auto" border style="width: 100%">
<el-table-column prop="name" label="菜单名称">
<template #default="scope">
<span class="sort" :data-id="scope.row.id">
@@ -17,13 +16,16 @@
</el-table-column>
<el-table-column prop="icon" label="菜单图标">
<template #default="scope">
<el-image class="menu-icon" :src="scope.row.icon"/>
<span v-if="scope.row.icon.startsWith('icon')">
<i class="iconfont" :class="scope.row.icon" style="font-size: 30px"></i>
</span>
<el-image class="menu-icon" :src="scope.row.icon" v-else />
</template>
</el-table-column>
<el-table-column prop="url" label="菜单URL"/>
<el-table-column prop="url" label="菜单URL" />
<el-table-column prop="enabled" label="启用状态">
<template #default="scope">
<el-switch v-model="scope.row['enabled']" @change="enable(scope.row)"/>
<el-switch v-model="scope.row['enabled']" @change="enable(scope.row)" />
</template>
</el-table-column>
<el-table-column label="操作" width="180">
@@ -39,26 +41,28 @@
</el-table>
</el-row>
<el-dialog
v-model="showDialog"
:title="title"
:close-on-click-modal="false"
>
<el-dialog v-model="showDialog" :title="title" :close-on-click-modal="false">
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
<el-form-item label="菜单名称" prop="name">
<el-input v-model="item.name" autocomplete="off"/>
<el-input v-model="item.name" autocomplete="off" />
</el-form-item>
<el-form-item label="菜单图标" prop="icon">
<el-form-item>
<template #label>
<div class="label-title">
开放注册
<el-tooltip effect="dark" content="可以填写 iconfont 图标名称也可以自己上传图片" raw-content placement="right">
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input v-model="item.icon" placeholder="菜单图标地址">
<template #append>
<el-upload
:auto-upload="true"
:show-file-list="false"
:http-request="uploadImg"
>
<el-upload :auto-upload="true" :show-file-list="false" :http-request="uploadImg">
<el-icon class="uploader-icon">
<UploadFilled/>
<UploadFilled />
</el-icon>
</el-upload>
</template>
@@ -66,141 +70,149 @@
</el-form-item>
<el-form-item label="菜单URL" prop="url">
<el-input v-model="item.url" autocomplete="off"/>
<el-input v-model="item.url" autocomplete="off" />
</el-form-item>
<el-form-item label="启用状态" prop="enable">
<el-switch v-model="item.enabled"/>
<el-switch v-model="item.enabled" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="save">提交</el-button>
</span>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="save">提交</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {onMounted, reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {dateFormat, removeArrayItem} from "@/utils/libs";
import {Plus, UploadFilled} from "@element-plus/icons-vue";
import {Sortable} from "sortablejs";
import { onMounted, reactive, ref } from "vue";
import { httpGet, httpPost } from "@/utils/http";
import { ElMessage } from "element-plus";
import { dateFormat, removeArrayItem } from "@/utils/libs";
import { InfoFilled, Plus, UploadFilled } from "@element-plus/icons-vue";
import { Sortable } from "sortablejs";
import Compressor from "compressorjs";
// 变量定义
const items = ref([])
const item = ref({})
const showDialog = ref(false)
const title = ref("")
const items = ref([]);
const item = ref({});
const showDialog = ref(false);
const title = ref("");
const rules = reactive({
name: [{required: true, message: '请输入菜单名称', trigger: 'change',}],
icon: [{required: true, message: '请上传菜单图标', trigger: 'change',}],
url: [{required: true, message: '请输入菜单地址', trigger: 'change',}],
})
const loading = ref(true)
const formRef = ref(null)
name: [{ required: true, message: "请输入菜单名称", trigger: "change" }],
icon: [{ required: true, message: "请上传菜单图标", trigger: "change" }],
url: [{ required: true, message: "请输入菜单地址", trigger: "change" }],
});
const loading = ref(true);
const formRef = ref(null);
const fetchData = () => {
// 获取数据
httpGet('/api/admin/menu/list').then((res) => {
if (res.data) {
// 初始化数据
const arr = res.data;
for (let i = 0; i < arr.length; i++) {
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
httpGet("/api/admin/menu/list")
.then((res) => {
if (res.data) {
// 初始化数据
const arr = res.data;
for (let i = 0; i < arr.length; i++) {
arr[i].last_used_at = dateFormat(arr[i].last_used_at);
}
items.value = arr;
}
items.value = arr
}
loading.value = false
}).catch(() => {
ElMessage.error("获取数据失败");
})
}
loading.value = false;
})
.catch(() => {
ElMessage.error("获取数据失败");
});
};
onMounted(() => {
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
fetchData()
const drawBodyWrapper = document.querySelector(".el-table__body tbody");
fetchData();
// 初始化拖动排序插件
Sortable.create(drawBodyWrapper, {
sort: true,
animation: 500,
onEnd({newIndex, oldIndex, from}) {
onEnd({ newIndex, oldIndex, from }) {
if (oldIndex === newIndex) {
return
return;
}
const sortedData = Array.from(from.children).map(row => row.querySelector('.sort').getAttribute('data-id'));
const ids = []
const sorts = []
const sortedData = Array.from(from.children).map((row) => row.querySelector(".sort").getAttribute("data-id"));
const ids = [];
const sorts = [];
sortedData.forEach((id, index) => {
ids.push(parseInt(id))
sorts.push(index + 1)
items.value[index].sort_num = index + 1
})
ids.push(parseInt(id));
sorts.push(index + 1);
items.value[index].sort_num = index + 1;
});
httpPost("/api/admin/menu/sort", {ids: ids, sorts: sorts}).catch(e => {
ElMessage.error("排序失败" + e.message)
})
}
})
})
httpPost("/api/admin/menu/sort", { ids: ids, sorts: sorts }).catch((e) => {
ElMessage.error("排序失败" + e.message);
});
},
});
});
const add = function () {
title.value = "新增菜单"
showDialog.value = true
item.value = {}
}
title.value = "新增菜单";
showDialog.value = true;
item.value = {};
};
const edit = function (row) {
title.value = "修改菜单"
showDialog.value = true
item.value = row
}
title.value = "修改菜单";
showDialog.value = true;
item.value = row;
};
const save = function () {
formRef.value.validate((valid) => {
if (valid) {
showDialog.value = false
showDialog.value = false;
if (!item.value.id) {
item.value.sort_num = items.value.length + 1
item.value.sort_num = items.value.length + 1;
}
httpPost('/api/admin/menu/save', item.value).then(() => {
ElMessage.success('操作成功!')
fetchData()
}).catch((e) => {
ElMessage.error('操作失败,' + e.message)
})
httpPost("/api/admin/menu/save", item.value)
.then(() => {
ElMessage.success("操作成功");
fetchData();
})
.catch((e) => {
ElMessage.error("操作失败" + e.message);
});
} else {
return false
return false;
}
})
}
});
};
const enable = (row) => {
httpPost('/api/admin/menu/enable', {id: row.id, enabled: row.enabled}).then(() => {
ElMessage.success("操作成功")
}).catch(e => {
ElMessage.error("操作失败" + e.message)
})
}
httpPost("/api/admin/menu/enable", { id: row.id, enabled: row.enabled })
.then(() => {
ElMessage.success("操作成功");
})
.catch((e) => {
ElMessage.error("操作失败" + e.message);
});
};
const remove = function (row) {
httpGet('/api/admin/menu/remove?id=' + row.id).then(() => {
ElMessage.success("删除成功")
items.value = removeArrayItem(items.value, row, (v1, v2) => {
return v1.id === v2.id
httpGet("/api/admin/menu/remove?id=" + row.id)
.then(() => {
ElMessage.success("删除成功");
items.value = removeArrayItem(items.value, row, (v1, v2) => {
return v1.id === v2.id;
});
})
}).catch((e) => {
ElMessage.error("删除失败" + e.message)
})
}
.catch((e) => {
ElMessage.error("删除失败" + e.message);
});
};
// 图片上传
const uploadImg = (file) => {
@@ -209,35 +221,32 @@ const uploadImg = (file) => {
quality: 0.6,
success(result) {
const formData = new FormData();
formData.append('file', result, result.name);
formData.append("file", result, result.name);
// 执行上传操作
httpPost('/api/admin/upload', formData).then((res) => {
item.value.icon = res.data.url
ElMessage.success('上传成功')
}).catch((e) => {
ElMessage.error('上传失败:' + e.message)
})
httpPost("/api/admin/upload", formData)
.then((res) => {
item.value.icon = res.data.url;
ElMessage.success("上传成功");
})
.catch((e) => {
ElMessage.error("上传失败:" + e.message);
});
},
error(e) {
ElMessage.error('上传失败:' + e.message)
ElMessage.error("上传失败:" + e.message);
},
});
};
</script>
<style lang="stylus" scoped>
<style lang="stylus">
@import "@/assets/css/admin/form.styl"
@import "@/assets/css/main.styl"
.menu {
.opt-box {
padding-bottom: 10px;
display flex;
justify-content flex-end
.el-icon {
margin-right: 5px;
}
.handle-box {
margin-bottom 20px
}
.menu-icon {
width 36px
height 36px
@@ -255,5 +264,9 @@ const uploadImg = (file) => {
width: 100%
}
.el-table .cell {
height: 100% !important;
line-height: 1 !important;
}
}
</style>
</style>

View File

@@ -1,41 +1,20 @@
<template>
<div class="container user-list" v-loading="loading">
<div class="handle-box">
<el-input
v-model="query.username"
placeholder="账号"
class="handle-input mr10"
></el-input>
<el-button type="primary" :icon="Search" @click="handleSearch"
>搜索</el-button
>
<el-button type="success" :icon="Plus" @click="addUser"
>新增用户</el-button
>
<el-button type="danger" :icon="Delete" @click="multipleDelete"
>删除</el-button
>
<el-input v-model="query.username" placeholder="账号" class="handle-input mr10"></el-input>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button type="success" :icon="Plus" @click="addUser">新增用户</el-button>
<el-button type="danger" :icon="Delete" @click="multipleDelete">删除</el-button>
</div>
<el-row>
<el-table
:data="users.items"
border
class="table"
:row-key="(row) => row.id"
@selection-change="handleSelectionChange"
table-layout="auto"
>
<el-table :data="users.items" border class="table" :row-key="(row) => row.id" @selection-change="handleSelectionChange" table-layout="auto">
<el-table-column type="selection" width="38"></el-table-column>
<el-table-column prop="id" label="ID" />
<el-table-column label="账号">
<template #default="scope">
<span>{{ scope.row.username }}</span>
<el-image
v-if="scope.row.vip"
:src="vipImg"
style="height: 20px; position: relative; top: 5px; left: 5px"
/>
<el-image v-if="scope.row.vip" :src="vipImg" style="height: 20px; position: relative; top: 5px; left: 5px" />
</template>
</el-table-column>
<el-table-column prop="mobile" label="手机" />
@@ -50,9 +29,7 @@
</el-table-column>
<el-table-column label="过期时间">
<template #default="scope">
<span v-if="scope.row['expired_time']">{{
scope.row["expired_time"]
}}</span>
<span v-if="scope.row['expired_time']">{{ scope.row["expired_time"] }}</span>
<el-tag v-else>长期有效</el-tag>
</template>
</el-table-column>
@@ -66,24 +43,9 @@
<el-table-column fixed="right" label="操作" width="200">
<template #default="scope">
<el-button-group class="ml-4">
<el-button
size="small"
type="primary"
@click="userEdit(scope.row)"
>编辑</el-button
>
<el-button
size="small"
type="danger"
@click="removeUser(scope.row)"
>删除</el-button
>
<el-button
size="small"
type="success"
@click="resetPass(scope.row)"
>重置密码</el-button
>
<el-button size="small" type="primary" @click="userEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="removeUser(scope.row)">删除</el-button>
<el-button size="small" type="success" @click="resetPass(scope.row)">重置密码</el-button>
</el-button-group>
</template>
</el-table-column>
@@ -102,18 +64,8 @@
</div>
</el-row>
<el-dialog
v-model="showUserEditDialog"
:title="title"
:close-on-click-modal="false"
width="50%"
>
<el-form
:model="user"
label-width="100px"
ref="userEditFormRef"
:rules="rules"
>
<el-dialog v-model="showUserEditDialog" :title="title" :close-on-click-modal="false" width="50%">
<el-form :model="user" label-width="100px" ref="userEditFormRef" :rules="rules">
<el-form-item label="账号" prop="username">
<el-input v-model="user.username" autocomplete="off" />
</el-form-item>
@@ -124,18 +76,10 @@
<el-input v-model="user.email" autocomplete="off" />
</el-form-item>
<el-form-item v-if="add" label="密码" prop="password">
<el-input
v-model="user.password"
autocomplete="off"
placeholder="8-16"
/>
<el-input v-model="user.password" autocomplete="off" placeholder="8-16" />
</el-form-item>
<el-form-item label="剩余算力" prop="power">
<el-input
v-model.number="user.power"
autocomplete="off"
placeholder="0"
/>
<el-input v-model.number="user.power" autocomplete="off" placeholder="0" />
</el-form-item>
<el-form-item label="有效期" prop="expired_time">
@@ -150,34 +94,14 @@
</el-form-item>
<el-form-item label="聊天角色" prop="chat_roles">
<el-select
v-model="user.chat_roles"
multiple
:filterable="true"
placeholder="选择聊天角色多选"
>
<el-option
v-for="item in roles"
:key="item.key"
:label="item.name"
:value="item.key"
/>
<el-select v-model="user.chat_roles" multiple :filterable="true" placeholder="选择聊天角色多选">
<el-option v-for="item in roles" :key="item.key" :label="item.name" :value="item.key" />
</el-select>
</el-form-item>
<el-form-item label="模型权限" prop="chat_models">
<el-select
v-model="user.chat_models"
multiple
:filterable="true"
placeholder="选择AI模型多选"
>
<el-option
v-for="item in models"
:key="item.id"
:label="item.name"
:value="item.id"
/>
<el-select v-model="user.chat_models" multiple :filterable="true" placeholder="选择AI模型多选">
<el-option v-for="item in models" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
@@ -201,12 +125,7 @@
<el-dialog v-model="showResetPassDialog" title="重置密码" width="50%">
<el-form label-width="100px" ref="userEditFormRef">
<el-form-item label="账户">
<el-input
v-model="pass.username"
autocomplete="off"
readonly
disabled
/>
<el-input v-model="pass.username" autocomplete="off" readonly disabled />
</el-form-item>
<el-form-item label="新密码">
@@ -252,17 +171,15 @@ const rules = reactive({
return !(value.length > 16 || value.length < 8);
},
message: "密码必须为8-16",
trigger: "blur"
}
trigger: "blur",
},
],
calls: [
{ required: true, message: "请输入提问次数" },
{ type: "number", message: "请输入有效数字" }
{ type: "number", message: "请输入有效数字" },
],
chat_roles: [
{ required: true, message: "请选择聊天角色", trigger: "change" }
],
chat_models: [{ required: true, message: "请选择AI模型", trigger: "change" }]
chat_roles: [{ required: true, message: "请选择聊天角色", trigger: "change" }],
chat_models: [{ required: true, message: "请选择AI模型", trigger: "change" }],
});
const loading = ref(true);
@@ -317,15 +234,11 @@ const handleSearch = () => {
// 删除用户
const removeUser = function (user) {
ElMessageBox.confirm(
"此操作将会永久删除用户信息和聊天记录确认操作吗?",
"警告",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}
)
ElMessageBox.confirm("此操作将会永久删除用户信息和聊天记录确认操作吗?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
httpGet("/api/admin/user/remove", { id: user.id })
.then(() => {
@@ -385,15 +298,11 @@ const handleSelectionChange = function (rows) {
};
const multipleDelete = function () {
ElMessageBox.confirm(
"此操作将会永久删除用户信息和聊天记录确认操作吗?",
"警告",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}
)
ElMessageBox.confirm("此操作将会永久删除用户信息和聊天记录确认操作吗?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
loading.value = true;
httpGet("/api/admin/user/remove", { ids: userIds.value })

View File

@@ -22,9 +22,9 @@ import {useSharedStore} from "@/store/sharedata";
const active = ref('home')
const store = useSharedStore()
const theme = ref(store.mobileTheme)
const theme = ref(store.theme)
watch(() => store.mobileTheme, (val) => {
watch(() => store.theme, (val) => {
theme.value = val
})

View File

@@ -125,7 +125,7 @@
<van-cell-group inset>
<van-field name="switch" label="暗黑主题">
<template #input>
<van-switch v-model="dark" @change="(val) => store.setMobileTheme(val?'dark':'light')"/>
<van-switch v-model="dark" @change="(val) => store.setTheme(val?'dark':'light')"/>
</template>
</van-field>
@@ -189,7 +189,7 @@ const isLogin = ref(false)
const showSettings = ref(false)
const store = useSharedStore()
const stream = ref(store.chatStream)
const dark = ref(store.mobileTheme === 'dark')
const dark = ref(store.theme === 'dark')
onMounted(() => {
checkSession().then(user => {

7
web/tailwind.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -9,14 +9,14 @@ module.exports = defineConfig({
configureWebpack: {
// disable performance hints
performance: {
hints: false
hints: false,
},
plugins: [new webpack.optimize.MinChunkSizePlugin({ minChunkSize: 10000 })],
resolve: {
alias: {
"@": path.resolve(__dirname, "src")
}
}
"@": path.resolve(__dirname, "src"),
},
},
},
publicPath: "/",
@@ -29,8 +29,8 @@ module.exports = defineConfig({
proxy: {
"/static/upload/": {
target: process.env.VUE_APP_API_HOST,
changeOrigin: true
}
}
}
changeOrigin: true,
},
},
},
});