Merge remote-tracking branch 'origin/ui' into ui
2
gpt-vue/.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
dist
|
||||
node_modules
|
27
gpt-vue/.eslintrc.cjs
Normal file
@ -0,0 +1,27 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
parser: "@typescript-eslint/parser",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
plugins: ["vue", "@typescript-eslint"],
|
||||
rules: {
|
||||
"prettier/prettier": "warn",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"no-undef": "off",
|
||||
},
|
||||
};
|
9
gpt-vue/.prettierrc.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true
|
||||
}
|
@ -12,6 +12,16 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@tsconfig/node20": "^20.1.2",
|
||||
"@types/node": "^20.11.10",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"prettier": "^3.2.5",
|
||||
"rimraf": "^5.0.5"
|
||||
}
|
||||
}
|
||||
|
@ -12,5 +12,8 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.6.7",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.8"
|
||||
}
|
||||
}
|
||||
|
@ -2,4 +2,56 @@ import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
export const getUUID = () => {
|
||||
return uuidV4();
|
||||
};
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
export function dateFormat(timestamp: number, format?: string) {
|
||||
if (!timestamp) {
|
||||
return '';
|
||||
} else if (timestamp < 9680917502) {
|
||||
timestamp = timestamp * 1000;
|
||||
}
|
||||
let year, month, day, HH, mm, ss;
|
||||
let time = new Date(timestamp);
|
||||
let timeDate;
|
||||
year = time.getFullYear(); // 年
|
||||
month = time.getMonth() + 1; // 月
|
||||
day = time.getDate(); // 日
|
||||
HH = time.getHours(); // 时
|
||||
mm = time.getMinutes(); // 分
|
||||
ss = time.getSeconds(); // 秒
|
||||
|
||||
month = month < 10 ? '0' + month : month;
|
||||
day = day < 10 ? '0' + day : day;
|
||||
HH = HH < 10 ? '0' + HH : HH; // 时
|
||||
mm = mm < 10 ? '0' + mm : mm; // 分
|
||||
ss = ss < 10 ? '0' + ss : ss; // 秒
|
||||
|
||||
switch (format) {
|
||||
case 'yyyy':
|
||||
timeDate = String(year);
|
||||
break;
|
||||
case 'yyyy-MM':
|
||||
timeDate = year + '-' + month;
|
||||
break;
|
||||
case 'yyyy-MM-dd':
|
||||
timeDate = year + '-' + month + '-' + day;
|
||||
break;
|
||||
case 'yyyy/MM/dd':
|
||||
timeDate = year + '/' + month + '/' + day;
|
||||
break;
|
||||
case 'yyyy-MM-dd HH:mm:ss':
|
||||
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss;
|
||||
break;
|
||||
case 'HH:mm:ss':
|
||||
timeDate = HH + ':' + mm + ':' + ss;
|
||||
break;
|
||||
case 'MM':
|
||||
timeDate = String(month);
|
||||
break;
|
||||
default:
|
||||
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss;
|
||||
break;
|
||||
}
|
||||
return timeDate;
|
||||
}
|
@ -8,6 +8,36 @@ importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@rushstack/eslint-patch':
|
||||
specifier: ^1.3.3
|
||||
version: 1.7.2
|
||||
'@tsconfig/node20':
|
||||
specifier: ^20.1.2
|
||||
version: 20.1.2
|
||||
'@types/node':
|
||||
specifier: ^20.11.10
|
||||
version: 20.11.24
|
||||
'@vue/eslint-config-typescript':
|
||||
specifier: ^12.0.0
|
||||
version: 12.0.0(eslint-plugin-vue@9.22.0)(eslint@8.57.0)(typescript@5.3.3)
|
||||
'@vue/tsconfig':
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1
|
||||
eslint:
|
||||
specifier: ^8.49.0
|
||||
version: 8.57.0
|
||||
eslint-config-prettier:
|
||||
specifier: ^9.1.0
|
||||
version: 9.1.0(eslint@8.57.0)
|
||||
eslint-plugin-prettier:
|
||||
specifier: ^5.1.3
|
||||
version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5)
|
||||
eslint-plugin-vue:
|
||||
specifier: ^9.17.0
|
||||
version: 9.22.0(eslint@8.57.0)
|
||||
prettier:
|
||||
specifier: ^3.2.5
|
||||
version: 3.2.5
|
||||
rimraf:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
@ -22,6 +52,11 @@ importers:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
|
||||
devDependencies:
|
||||
'@types/uuid':
|
||||
specifier: ^9.0.8
|
||||
version: 9.0.8
|
||||
|
||||
projects/vue-admin:
|
||||
dependencies:
|
||||
'@arco-design/web-vue':
|
||||
@ -30,6 +65,9 @@ importers:
|
||||
'@gpt-vue/packages':
|
||||
specifier: workspace:^1.0.0
|
||||
version: link:../../packages
|
||||
md-editor-v3:
|
||||
specifier: ^2.2.1
|
||||
version: 2.11.3(vue@3.4.21)
|
||||
pinia:
|
||||
specifier: ^2.1.7
|
||||
version: 2.1.7(typescript@5.3.3)(vue@3.4.21)
|
||||
@ -840,6 +878,11 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@pkgr/core@0.1.1:
|
||||
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@rollup/rollup-android-arm-eabi@4.12.0:
|
||||
resolution: {integrity: sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==}
|
||||
cpu: [arm]
|
||||
@ -975,6 +1018,10 @@ packages:
|
||||
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
|
||||
dev: true
|
||||
|
||||
/@types/uuid@9.0.8:
|
||||
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
@ -1661,6 +1708,36 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/eslint-config-prettier@9.1.0(eslint@8.57.0):
|
||||
resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
eslint: '>=7.0.0'
|
||||
dependencies:
|
||||
eslint: 8.57.0
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5):
|
||||
resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
'@types/eslint': '>=8.0.0'
|
||||
eslint: '>=8.0.0'
|
||||
eslint-config-prettier: '*'
|
||||
prettier: '>=3.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/eslint':
|
||||
optional: true
|
||||
eslint-config-prettier:
|
||||
optional: true
|
||||
dependencies:
|
||||
eslint: 8.57.0
|
||||
eslint-config-prettier: 9.1.0(eslint@8.57.0)
|
||||
prettier: 3.2.5
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.8.8
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-vue@9.22.0(eslint@8.57.0):
|
||||
resolution: {integrity: sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
@ -1779,6 +1856,10 @@ packages:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
dev: true
|
||||
|
||||
/fast-diff@1.3.0:
|
||||
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
||||
dev: true
|
||||
|
||||
/fast-glob@3.3.2:
|
||||
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
@ -2189,6 +2270,14 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/md-editor-v3@2.11.3(vue@3.4.21):
|
||||
resolution: {integrity: sha512-SCfS4qMy0HldFdplcIGUMCpSv8qkNWkYShSdv2gTHeViKduA34zV89BOrWcqls2EZSlvt2n3G7nHRzYUvJjDKw==}
|
||||
peerDependencies:
|
||||
vue: ^3.2.47
|
||||
dependencies:
|
||||
vue: 3.4.21(typescript@5.3.3)
|
||||
dev: false
|
||||
|
||||
/memorystream@0.3.1:
|
||||
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
@ -2442,6 +2531,19 @@ packages:
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dev: true
|
||||
|
||||
/prettier-linter-helpers@1.0.0:
|
||||
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dependencies:
|
||||
fast-diff: 1.3.0
|
||||
dev: true
|
||||
|
||||
/prettier@3.2.5:
|
||||
resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
dev: false
|
||||
@ -2663,6 +2765,14 @@ packages:
|
||||
resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
|
||||
dev: true
|
||||
|
||||
/synckit@0.8.8:
|
||||
resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@pkgr/core': 0.1.1
|
||||
tslib: 2.6.2
|
||||
dev: true
|
||||
|
||||
/text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
dev: true
|
||||
|
@ -1,14 +1,28 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript'
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript",
|
||||
"@vue/eslint-config-prettier",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
parser: "@typescript-eslint/parser",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
plugins: ["vue", "@typescript-eslint"],
|
||||
rules: {
|
||||
"prettier/prettier": "warn",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"no-undef": "off",
|
||||
},
|
||||
};
|
||||
|
9
gpt-vue/projects/vue-admin/.prettierrc.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true
|
||||
}
|
8
gpt-vue/projects/vue-admin/env.d.ts
vendored
@ -1 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare const __AUTH_KEY: string;
|
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
<title>ChatPlus-Ai</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
@ -14,6 +14,7 @@
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.54.6",
|
||||
"@gpt-vue/packages": "workspace:^1.0.0",
|
||||
"md-editor-v3": "^2.2.1",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5"
|
||||
@ -24,6 +25,7 @@
|
||||
"@types/node": "^20.11.10",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^8.49.0",
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 66 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/alipay.jpg
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/artist.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/dou_yin.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/elon_musk.jpg
Normal file
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 20 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/girl_friend.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/good_comment.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/gpt.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/kong_zi.jpg
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/lu_xun.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/mid_journey.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/programmer.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/psychiatrist.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/red_book.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/seller.jpg
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/steve_jobs.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/teacher.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/translator.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/user.png
Normal file
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 19 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/yi_yan.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/chat.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/logo.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/mic.gif
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/mj.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/reward.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/sd.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/user-info.jpg
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/vip.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/wechat-pay.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/wx.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
gpt-vue/projects/vue-admin/public/left-img.png
Normal file
After Width: | Height: | Size: 31 KiB |
@ -1,3 +0,0 @@
|
||||
VITE_PROXY_BASE_URL="/api"
|
||||
VITE_TARGET_URL="http://172.22.11.2:5678"
|
||||
VITE_SOCKET_IO_URL="http://172.28.1.3:8899"
|
@ -1,3 +0,0 @@
|
||||
VITE_PROXY_BASE_URL=""
|
||||
VITE_TARGET_URL="/"
|
||||
VITE_SOCKET_IO_URL="/"
|
@ -1,3 +1,31 @@
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
<style>
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 1px;
|
||||
box-shadow: inset 0 0 5px #0000000d;
|
||||
background: #d9d9d9;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 5px #0000000d;
|
||||
border-radius: 1px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.public-bg {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(133deg, #ffffff 0%, #dde8fe 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
37
gpt-vue/projects/vue-admin/src/components/ConfirmSwitch.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { Message, type SwitchInstance } from "@arco-design/web-vue";
|
||||
import type { BaseResponse } from "@gpt-vue/packages/type";
|
||||
|
||||
type OriginProps = SwitchInstance["$props"];
|
||||
|
||||
interface Props extends /* @vue-ignore */ OriginProps {
|
||||
modelValue: boolean | string | number;
|
||||
api: (params?: any) => Promise<BaseResponse<any>>;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
|
||||
const _value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => {
|
||||
emits("update:modelValue", v);
|
||||
},
|
||||
});
|
||||
|
||||
const onBeforeChange = async (params) => {
|
||||
try {
|
||||
await props.api({ ...params, value: !_value.value });
|
||||
Message.success("操作成功");
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a-switch v-bind="{ ...props, ...$attrs }" v-model="_value" :before-change="onBeforeChange" />
|
||||
</template>
|
@ -1,15 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { IconDown, IconExport } from "@arco-design/web-vue/es/icon";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import Logo from "/images/logo.png";
|
||||
|
||||
import SystemMenu from "./SystemMenu.vue";
|
||||
import PageWrapper from "./PageWrapper.vue";
|
||||
|
||||
const logoWidth = "200px";
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
<template>
|
||||
<ALayout class="custom-layout">
|
||||
<ALayoutHeader class="custom-layout-header">
|
||||
<div class="logo"></div>
|
||||
<div class="logo">
|
||||
<img :src="Logo" alt="logo" />
|
||||
<span>ChatPlus 控制台</span>
|
||||
</div>
|
||||
<div class="action">
|
||||
<ADropdown>
|
||||
<ASpace align="center" :size="4">
|
||||
@ -20,7 +26,11 @@ const logoWidth = "200px";
|
||||
<ADoption value="changeOwnPwd">更改密码</ADoption>
|
||||
</template>
|
||||
<template #footer>
|
||||
<APopconfirm content="确认退出?" position="br">
|
||||
<APopconfirm
|
||||
content="确认退出?"
|
||||
position="br"
|
||||
@ok="authStore.logout"
|
||||
>
|
||||
<ASpace align="center" class="logout-area">
|
||||
<IconExport size="16" />
|
||||
<span>退出</span>
|
||||
@ -52,10 +62,15 @@ const logoWidth = "200px";
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-neutral-2);
|
||||
.logo {
|
||||
display: block;
|
||||
display: flex;
|
||||
width: v-bind("logoWidth");
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
.action {
|
||||
display: flex;
|
||||
|
@ -33,9 +33,9 @@ const formData = computed({
|
||||
});
|
||||
|
||||
const searchColumns = computed(() => {
|
||||
return props.columns?.filter(
|
||||
(item) => item.dataIndex && item.search
|
||||
) as (SearchColumns & { dataIndex: string })[];
|
||||
return props.columns?.filter((item) => item.dataIndex && item.search) as (SearchColumns & {
|
||||
dataIndex: string;
|
||||
})[];
|
||||
});
|
||||
|
||||
const optionsEvent = {
|
||||
@ -60,7 +60,7 @@ const optionsEvent = {
|
||||
@submit="optionsEvent.onSearch"
|
||||
>
|
||||
<AGrid
|
||||
:cols="{ md: 1, lg: 2, xl: 3, xxl: 5 }"
|
||||
:cols="{ md: 1, lg: 3, xl: 4, xxl: 5 }"
|
||||
:row-gap="12"
|
||||
:col-gap="12"
|
||||
:collapsed="collapsed"
|
||||
@ -70,36 +70,24 @@ const optionsEvent = {
|
||||
:key="item.dataIndex"
|
||||
style="transition: all 0.3s ease-in-out"
|
||||
>
|
||||
<AFormItem :field="item.dataIndex" :label="(item.title as string)">
|
||||
<AFormItem :field="item.dataIndex" :label="item.title as string">
|
||||
<slot :name="item.search.slotsName">
|
||||
<component
|
||||
v-model="formData[item.dataIndex]"
|
||||
:is="
|
||||
ValueType[item.search.valueType ?? 'input'] ??
|
||||
item.search.render
|
||||
"
|
||||
:is="ValueType[item.search.valueType ?? 'input'] ?? item.search.render"
|
||||
v-bind="useComponentConfig(size, item)"
|
||||
/>
|
||||
</slot>
|
||||
</AFormItem>
|
||||
</AGridItem>
|
||||
<AGridItem suffix>
|
||||
<ASpace>
|
||||
<ASpace class="flex-end">
|
||||
<slot name="search-options" :option="optionsEvent">
|
||||
<AButton
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
:size="size"
|
||||
:loading="submitting"
|
||||
>
|
||||
<AButton type="primary" html-type="submit" :size="size" :loading="submitting">
|
||||
<icon-search />
|
||||
<span>查询</span>
|
||||
</AButton>
|
||||
<AButton
|
||||
:size="size"
|
||||
@click="optionsEvent.onReset"
|
||||
:loading="submitting"
|
||||
>
|
||||
<AButton :size="size" @click="optionsEvent.onReset" :loading="submitting">
|
||||
<icon-refresh />
|
||||
<span>重置</span>
|
||||
</AButton>
|
||||
@ -114,4 +102,8 @@ const optionsEvent = {
|
||||
.search-form-conteiner {
|
||||
padding: 16px 0;
|
||||
}
|
||||
.flex-end {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
</style>
|
||||
|
@ -40,11 +40,7 @@ onActivated(handleSearch);
|
||||
<slot name="header-title">{{ props.headerTitle }}</slot>
|
||||
</div>
|
||||
<div class="header-option">
|
||||
<slot
|
||||
name="header-option"
|
||||
:formData="formData"
|
||||
:reload="handleSearch"
|
||||
/>
|
||||
<slot name="header-option" :formData="formData" :reload="handleSearch" />
|
||||
</div>
|
||||
</div>
|
||||
<FormSection
|
||||
@ -52,7 +48,11 @@ onActivated(handleSearch);
|
||||
:columns="columns"
|
||||
:submitting="(tableConfig.loading as boolean)"
|
||||
@request="handleSearch"
|
||||
/>
|
||||
>
|
||||
<template v-for="slot in Object.keys($slots)" #[slot]="config">
|
||||
<slot :name="slot" v-bind="{ ...config, reload: handleSearch }" />
|
||||
</template>
|
||||
</FormSection>
|
||||
<div ref="tableContainerRef" class="search-table-container">
|
||||
<ATable
|
||||
v-bind="{
|
||||
|
@ -46,10 +46,10 @@ function useAsyncTable<T extends Record<string, unknown>>(
|
||||
const { data } = await request({
|
||||
...unref(params ?? {}),
|
||||
page: paginationState.current,
|
||||
pageSize: paginationState.pageSize,
|
||||
page_size: paginationState.pageSize,
|
||||
});
|
||||
tableState.data = data?.items;
|
||||
paginationState.total = data.total;
|
||||
tableState.data = (data as any)?.items;
|
||||
paginationState.total = (data as any)?.total;
|
||||
} finally {
|
||||
tableState.loading = false
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onActivated } from "vue";
|
||||
import useAsyncTable from "./useAsyncTable";
|
||||
import { useTableScroll } from "@/components/SearchTable/utils";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import type { TableRequest, TableOriginalProps } from "./useAsyncTable";
|
||||
|
||||
interface SimpleTable extends /* @vue-ignore */ TableOriginalProps {
|
||||
request: TableRequest<Record<string, unknown>>;
|
||||
params?: Record<string, unknown>;
|
||||
columns?: TableOriginalProps["columns"];
|
||||
}
|
||||
|
||||
const props = defineProps<SimpleTable>();
|
||||
const tableContainerRef = ref<HTMLElement>();
|
||||
|
||||
// 表格请求参数
|
||||
const [tableConfig, getList] = useAsyncTable(props.request, props.params);
|
||||
|
||||
const _columns = computed(() => {
|
||||
return props.columns?.map((item) => ({
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
...item,
|
||||
}));
|
||||
});
|
||||
|
||||
const handleSearch = async (tips?: boolean) => {
|
||||
tips && Message.success("操作成功");
|
||||
await getList();
|
||||
};
|
||||
|
||||
onActivated(handleSearch);
|
||||
</script>
|
||||
<template>
|
||||
<div class="simple-header">
|
||||
<a-space>
|
||||
<slot name="header" v-bind="{ reload: handleSearch }" />
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="simple-table">
|
||||
<div ref="tableContainerRef" class="simple-table-container">
|
||||
<ATable
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
...tableConfig,
|
||||
...props,
|
||||
scroll: useTableScroll(_columns || [], tableContainerRef as HTMLElement),
|
||||
columns: _columns
|
||||
}"
|
||||
>
|
||||
<template v-for="slot in Object.keys($slots)" #[slot]="config">
|
||||
<slot :name="slot" v-bind="{ ...config, reload: handleSearch }" />
|
||||
</template>
|
||||
</ATable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.simple-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.simple-table-container {
|
||||
flex: 1;
|
||||
}
|
||||
.simple-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.simple-header {
|
||||
padding: 16px 0;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,43 @@
|
||||
import { computed, onMounted, reactive, unref } from "vue";
|
||||
import type { TableInstance } from "@arco-design/web-vue";
|
||||
import type { BaseResponse } from "@gpt-vue/packages/type";
|
||||
|
||||
export type TableOriginalProps = TableInstance["$props"];
|
||||
export type TableRequest<T extends Record<string, unknown>> = (
|
||||
params?: any
|
||||
) => Promise<BaseResponse<T[]>>;
|
||||
export type TableReturn = [TableOriginalProps, () => Promise<void>];
|
||||
function useAsyncTable<T extends Record<string, unknown>>(
|
||||
request: TableRequest<T>,
|
||||
params?: Record<string, unknown>
|
||||
): TableReturn {
|
||||
const tableState = reactive<{ loading: Boolean; data: T[] }>({
|
||||
loading: false,
|
||||
data: [],
|
||||
});
|
||||
|
||||
const tableConfig = computed<TableOriginalProps>(() => {
|
||||
return {
|
||||
...tableState,
|
||||
rowKey: "id",
|
||||
};
|
||||
});
|
||||
|
||||
const getTableData = async () => {
|
||||
tableState.loading = true;
|
||||
try {
|
||||
const { data } = await request({
|
||||
...unref(params ?? {}),
|
||||
});
|
||||
tableState.data = data as any;
|
||||
} finally {
|
||||
tableState.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(getTableData);
|
||||
|
||||
return [tableConfig, getTableData] as TableReturn;
|
||||
}
|
||||
|
||||
export default useAsyncTable;
|
@ -0,0 +1,44 @@
|
||||
import usePopup, { type Config } from "./usePopup";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import type { Component } from "vue";
|
||||
import type { BaseResponse } from "@gpt-vue/packages/type";
|
||||
interface Arg {
|
||||
reload?: () => void;
|
||||
record?: Record<string, any>;
|
||||
}
|
||||
|
||||
export default function (
|
||||
node: Component,
|
||||
api: (params?: any) => Promise<BaseResponse<any>>,
|
||||
config?: Config
|
||||
): (arg: Arg) => void {
|
||||
const nodeProps = (arg: Arg[]) => {
|
||||
return {
|
||||
data: arg[0].record || {},
|
||||
...config.nodeProps?.(arg),
|
||||
};
|
||||
};
|
||||
|
||||
const popupProps = (arg: Arg[], getExposed) => {
|
||||
return {
|
||||
width: 750,
|
||||
maskClosable: false,
|
||||
onBeforeOk: async () => {
|
||||
const exposed = getExposed();
|
||||
const validateRes = await exposed?.formRef.value.validate();
|
||||
if (validateRes) {
|
||||
return false;
|
||||
}
|
||||
const { code } = await api(exposed?.form.value);
|
||||
if (code === 0) {
|
||||
Message.success("操作成功");
|
||||
}
|
||||
arg[0]?.reload?.();
|
||||
return code === 0;
|
||||
},
|
||||
...config.popupProps?.(arg, getExposed),
|
||||
};
|
||||
};
|
||||
|
||||
return usePopup(node, { nodeProps, popupProps });
|
||||
}
|
37
gpt-vue/projects/vue-admin/src/composables/usePopup.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { h } from "vue";
|
||||
import type { Component, ComponentInternalInstance } from "vue";
|
||||
import { Modal, Drawer } from "@arco-design/web-vue";
|
||||
import type { ModalConfig, DrawerConfig } from "@arco-design/web-vue";
|
||||
import app from "@/main";
|
||||
|
||||
export interface Config {
|
||||
nodeProps?: (...arg: any) => Record<string, any>;
|
||||
popupProps?: (
|
||||
arg: any[],
|
||||
exposed: () => ComponentInternalInstance["exposed"]
|
||||
) => Omit<ModalConfig | DrawerConfig, "content"> & {
|
||||
[key: string]: any;
|
||||
};
|
||||
type?: "drawer" | "modal";
|
||||
}
|
||||
|
||||
const component = {
|
||||
modal: Modal,
|
||||
drawer: Drawer,
|
||||
};
|
||||
function usePopup(node: Component, config: Config) {
|
||||
const { nodeProps, popupProps, type = "modal" } = config;
|
||||
|
||||
return (...arg: any[]) => {
|
||||
const content = h(node, nodeProps ? nodeProps(arg) : {});
|
||||
const popupNode = component[type];
|
||||
// 获取全局组件的上下文
|
||||
popupNode._context = app._context;
|
||||
popupNode.open({
|
||||
content: () => content,
|
||||
...popupProps?.(arg, () => content?.component?.exposed as any),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default usePopup;
|
26
gpt-vue/projects/vue-admin/src/composables/useRequest.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ref } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import type { BaseResponse } from "@gpt-vue/packages/type";
|
||||
|
||||
type Request<T> = (params?: any) => Promise<BaseResponse<T>>
|
||||
function useRequest<T>(request: Request<T>) {
|
||||
const result = ref<T>()
|
||||
const loading = ref(false)
|
||||
|
||||
const requestData = async (params?: any) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await request(params)
|
||||
result.value = res.data
|
||||
return Promise.resolve(res)
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return [requestData, result, loading] as [Request<T>, Ref<T>, Ref<boolean>]
|
||||
}
|
||||
|
||||
export default useRequest
|
29
gpt-vue/projects/vue-admin/src/composables/useSubmit.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { ref, reactive, unref } from "vue";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import type { BaseResponse } from "@gpt-vue/packages/type";
|
||||
function useSubmit<T extends Record<string, any> = Record<string, any>, R = any>(defaultData?: T) {
|
||||
const formRef = ref();
|
||||
const formData = reactive<T | Record<string, any>>({ ...defaultData ?? {} });
|
||||
const submitting = ref(false);
|
||||
|
||||
const handleSubmit = async (api: (params?: any) => Promise<BaseResponse<R>>, params) => {
|
||||
submitting.value = true;
|
||||
try {
|
||||
const hasError = await formRef.value?.validate();
|
||||
if (!hasError) {
|
||||
const { data, message } = await api({ ...formData ?? {}, ...unref(params) });
|
||||
Message.success(message);
|
||||
return Promise.resolve({ formData, data });
|
||||
}
|
||||
return Promise.reject(false);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return { formRef, formData, handleSubmit, submitting };
|
||||
}
|
||||
|
||||
export default useSubmit;
|
@ -1,12 +1,15 @@
|
||||
import router from "@/router";
|
||||
import { Notification } from "@arco-design/web-vue";
|
||||
import createInstance from "@gpt-vue/packages/request"
|
||||
import type { BaseResponse } from "@gpt-vue/packages/type";
|
||||
|
||||
export const uploadUrl = import.meta.env.VITE_PROXY_BASE_URL + "/common/upload/minio";
|
||||
export const uploadUrl = import.meta.env.VITE_PROXY_BASE_URL + "/api/upload";
|
||||
|
||||
export const instance = createInstance()
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
config.headers[__AUTH_KEY] = localStorage.getItem(__AUTH_KEY);
|
||||
config.headers["Authorization"] = localStorage.getItem(__AUTH_KEY);
|
||||
return config;
|
||||
});
|
||||
|
||||
@ -14,6 +17,10 @@ instance.interceptors.response.use(
|
||||
(response) => {
|
||||
const { data }: { data: BaseResponse<unknown> } = response
|
||||
if (data && typeof data === "object" && data.code !== 0) {
|
||||
if (data.code === 400) {
|
||||
localStorage.removeItem(__AUTH_KEY);
|
||||
router.push({ name: "Login" })
|
||||
}
|
||||
Notification.error(data.message ?? '未知错误')
|
||||
}
|
||||
return { data, response } as any;
|
||||
|
35
gpt-vue/projects/vue-admin/src/http/login.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const userLogin = (data: {
|
||||
username: string;
|
||||
password: string;
|
||||
}) => {
|
||||
return http({
|
||||
url: "/api/admin/login",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const userLogout = () => {
|
||||
return http({
|
||||
url: "/api/admin/logout",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
export const getSession = () => {
|
||||
return http({
|
||||
url: "/api/admin/session",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const loginLog = (params?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/user/loginLog",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
@ -1,7 +1,21 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import CustomLayout from '@/components/CustomLayout.vue'
|
||||
import menu from './menu'
|
||||
|
||||
const whiteListRoutes = [
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: () => import("@/views/LoginView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "404",
|
||||
component: () => import("@/views/NotFound.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
@ -12,12 +26,28 @@ const router = createRouter({
|
||||
redirect: () => menu[0].path,
|
||||
children: menu
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "404",
|
||||
component: () => import("@/views/NotFound.vue"),
|
||||
},
|
||||
...whiteListRoutes
|
||||
]
|
||||
})
|
||||
|
||||
const whiteList = whiteListRoutes.map((i) => i.name);
|
||||
|
||||
router.beforeEach((to, _, next) => {
|
||||
const authStore = useAuthStore();
|
||||
authStore.init()
|
||||
if (typeof to.name === "string" && whiteList.includes(to.name)) {
|
||||
if (authStore.token && to.name === "Login") {
|
||||
next({ path: menu[0].path });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if (!authStore.token) {
|
||||
next({ name: "Login" });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
export default router
|
||||
|
@ -2,35 +2,126 @@ import {
|
||||
IconUser,
|
||||
IconDashboard,
|
||||
IconOrderedList,
|
||||
IconCalendar,
|
||||
IconHeartFill,
|
||||
IconCodeSquare,
|
||||
IconMessage,
|
||||
IconSettings,
|
||||
IconUserGroup,
|
||||
IconLock,
|
||||
IconCodepen,
|
||||
IconWechatpay,
|
||||
} from "@arco-design/web-vue/es/icon";
|
||||
|
||||
const menu = [
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
path: "/dashboard",
|
||||
name: "Dashboard",
|
||||
meta: {
|
||||
title: "仪表盘",
|
||||
icon: IconDashboard
|
||||
icon: IconDashboard,
|
||||
},
|
||||
component: () => import('@/views/DashboardView.vue')
|
||||
component: () => import("@/views/DashboardView.vue"),
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
name: 'User',
|
||||
path: "/user",
|
||||
name: "User",
|
||||
meta: {
|
||||
title: "用户管理",
|
||||
icon: IconUser,
|
||||
},
|
||||
component: () => import('@/views/User/UserContainer.vue')
|
||||
component: () => import("@/views/User/UserContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: '/order',
|
||||
name: 'Order',
|
||||
path: "/Role",
|
||||
name: "Role",
|
||||
meta: {
|
||||
title: "角色管理",
|
||||
icon: IconUserGroup,
|
||||
},
|
||||
component: () => import("@/views/Role/RoleContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/ChatModel",
|
||||
name: "ChatModel",
|
||||
meta: {
|
||||
title: "语言模型",
|
||||
icon: IconCodepen,
|
||||
},
|
||||
component: () => import("@/views/ChatModel/ChatModelContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/Product",
|
||||
name: "Product",
|
||||
meta: {
|
||||
title: "充值产品",
|
||||
icon: IconWechatpay,
|
||||
},
|
||||
component: () => import("@/views/Product/ProductContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/ApiKey",
|
||||
name: "ApiKey",
|
||||
meta: {
|
||||
title: "APIKEY",
|
||||
icon: IconLock,
|
||||
},
|
||||
component: () => import("@/views/ApiKey/ApiKeyContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/order",
|
||||
name: "Order",
|
||||
meta: {
|
||||
title: "充值订单",
|
||||
icon: IconOrderedList,
|
||||
},
|
||||
component: () => import('@/views/Order/OrderContainer.vue')
|
||||
component: () => import("@/views/Order/OrderContainer.vue"),
|
||||
},
|
||||
|
||||
{
|
||||
path: "/reward",
|
||||
name: "Reward",
|
||||
meta: {
|
||||
title: "众筹管理",
|
||||
icon: IconHeartFill,
|
||||
},
|
||||
component: () => import("@/views/Reward/RewardContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/functions",
|
||||
name: "Functions",
|
||||
meta: {
|
||||
title: "函数管理",
|
||||
icon: IconCodeSquare,
|
||||
},
|
||||
component: () => import("@/views/Functions/FunctionsContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/chats",
|
||||
name: "Chats",
|
||||
meta: {
|
||||
title: "对话管理",
|
||||
icon: IconMessage,
|
||||
},
|
||||
component: () => import("@/views/Chats/ChatsContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/system",
|
||||
name: "System",
|
||||
meta: {
|
||||
title: "系统设置",
|
||||
icon: IconSettings,
|
||||
},
|
||||
component: () => import("@/views/System/SystemContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/loginLog",
|
||||
name: "LoginLog",
|
||||
meta: {
|
||||
title: "登录日志",
|
||||
icon: IconCalendar,
|
||||
},
|
||||
component: () => import("@/views/LoginLog.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
|
42
gpt-vue/projects/vue-admin/src/stores/auth.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { userLogin, userLogout } from '@/http/login'
|
||||
import router from '@/router'
|
||||
|
||||
export const useAuthStore = defineStore({
|
||||
id: __AUTH_KEY,
|
||||
state: () => ({ token: null } as { token: string | null }),
|
||||
actions: {
|
||||
init() {
|
||||
this.$state.token = localStorage.getItem(__AUTH_KEY);
|
||||
},
|
||||
async login(params: any) {
|
||||
try {
|
||||
const { data } = await userLogin(params)
|
||||
if (data) {
|
||||
this.$state.token = data;
|
||||
localStorage.setItem(__AUTH_KEY, data)
|
||||
Message.success('登录成功');
|
||||
router.replace({ name: 'home' })
|
||||
return Promise.resolve(data)
|
||||
}
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
},
|
||||
async logout() {
|
||||
try {
|
||||
await userLogout()
|
||||
if (this.$state.token) {
|
||||
localStorage.removeItem(__AUTH_KEY)
|
||||
this.$reset()
|
||||
}
|
||||
Message.success('退出成功');
|
||||
router.push({ name: 'Login' })
|
||||
return Promise.resolve(true)
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
@ -1,4 +1,122 @@
|
||||
<script lang="ts" setup></script>
|
||||
<script lang="ts" setup>
|
||||
import { getList, save, deleting, setStatus } from "./api";
|
||||
import { ref } from "vue";
|
||||
import ApiKeyForm from "./ApiKeyForm.vue";
|
||||
import useCustomFormPopup from "@/composables/useCustomFormPopup";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
|
||||
import { dateFormat } from "@gpt-vue/packages/utils";
|
||||
// table 配置
|
||||
const columns = [
|
||||
{
|
||||
title: "所属平台",
|
||||
dataIndex: "platform",
|
||||
},
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "name",
|
||||
},
|
||||
{
|
||||
title: "key",
|
||||
dataIndex: "value",
|
||||
},
|
||||
{
|
||||
title: "用途",
|
||||
dataIndex: "type",
|
||||
},
|
||||
{
|
||||
title: "使用代理",
|
||||
dataIndex: "use_proxy",
|
||||
slotName: "proxy",
|
||||
},
|
||||
{
|
||||
title: "最后使用时间",
|
||||
dataIndex: "last_used_at",
|
||||
render: ({ record }) => {
|
||||
return dateFormat(record.last_used_at);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "启用状态",
|
||||
dataIndex: "enabled",
|
||||
slotName: "status",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
slotName: "action",
|
||||
},
|
||||
];
|
||||
|
||||
// 数据
|
||||
const tableData = ref([]);
|
||||
const getData = () => {
|
||||
getList().then(({ code, data }) => {
|
||||
if (code === 0) {
|
||||
tableData.value = data;
|
||||
}
|
||||
});
|
||||
};
|
||||
getData();
|
||||
|
||||
// 新增编辑
|
||||
const popup = useCustomFormPopup(ApiKeyForm, save, {
|
||||
popupProps: (arg) => ({ title: arg[0].record ? "编辑ApiKey" : "新增ApiKey" }),
|
||||
});
|
||||
|
||||
// 删除
|
||||
const handleDelete = ({ id }, reload) => {
|
||||
deleting(id).then(({ code }) => {
|
||||
if (code === 0) {
|
||||
Message.success("操作成功");
|
||||
reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 状态
|
||||
const handleStatusChange = ({ filed, value, record, reload }) => {
|
||||
setStatus({
|
||||
id: record.id,
|
||||
value,
|
||||
filed,
|
||||
}).then(({ code }) => {
|
||||
if (code === 0) {
|
||||
Message.success("操作成功");
|
||||
reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div></div>
|
||||
<SimpleTable :columns="columns" :request="getList">
|
||||
<template #action="{ record, reload }">
|
||||
<a-link @click="popup({ record, reload })">编辑</a-link>
|
||||
<a-popconfirm content="确定删除?" @ok="handleDelete(record, reload)">
|
||||
<a-link status="danger">删除</a-link>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
<template #header="{ reload }">
|
||||
<a-button @click="popup({ reload })" size="small"><icon-plus />新增</a-button>
|
||||
</template>
|
||||
<template #status="{ record, reload }">
|
||||
<a-switch
|
||||
v-model="record.enabled"
|
||||
@change="
|
||||
(value) => {
|
||||
handleStatusChange({ filed: 'enabled', value, record, reload });
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #proxy="{ record, reload }">
|
||||
<a-switch
|
||||
v-model="record.use_proxy"
|
||||
@change="
|
||||
(value) => {
|
||||
handleStatusChange({ filed: 'use_proxy', value, record, reload });
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</SimpleTable>
|
||||
</template>
|
||||
|
119
gpt-vue/projects/vue-admin/src/views/ApiKey/ApiKeyForm.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<a-alert type="warning">
|
||||
<div class="warning">
|
||||
{{
|
||||
`注意:如果是百度文心一言平台,API-KEY 为 APIKey|SecretKey,中间用竖线(|)连接\n注意:如果是讯飞星火大模型,API-KEY 为 AppId|APIKey|APISecret,中间用竖线(|)连接`
|
||||
}}
|
||||
</div>
|
||||
</a-alert>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:style="{ width: '600px', 'margin-top': '10px' }"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<a-form-item
|
||||
field="platform"
|
||||
label="所属平台"
|
||||
:rules="[{ required: true, message: '请输入所属平台' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
>
|
||||
<a-input v-model="form.platform" placeholder="请输入所属平台" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="name"
|
||||
label="名称"
|
||||
:rules="[{ required: true, message: '请输入名称' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
showable
|
||||
>
|
||||
<a-input v-model="form.name" placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="type"
|
||||
label="用途"
|
||||
:rules="[{ required: true, message: '请输入用途' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
>
|
||||
<a-select v-model="form.type" placeholder="请输入用途" :options="typeOPtions"> </a-select>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="value"
|
||||
label="API KEY"
|
||||
:rules="[{ required: true, message: '请输入API KEY' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
>
|
||||
<a-input v-model="form.value" placeholder="请输入API KEY" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="api_url"
|
||||
label="API URL"
|
||||
:rules="[{ required: true, message: '请输入API URL' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
>
|
||||
<a-input v-model="form.api_url" placeholder="请输入API URL" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="use_proxy" label="使用代理">
|
||||
<a-switch v-model="form.use_proxy" />
|
||||
<a-tooltip
|
||||
content="是否使用代理访问 API URL,OpenAI 官方API需要开启代理访问"
|
||||
position="right"
|
||||
>
|
||||
<icon-info-circle-fill />
|
||||
</a-tooltip>
|
||||
</a-form-item>
|
||||
<a-form-item field="enable" label="启用状态">
|
||||
<a-switch v-model="form.enable" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineExpose, defineProps } from "vue";
|
||||
const props = defineProps({
|
||||
data: {},
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const form = ref({});
|
||||
if (props.data?.id) {
|
||||
form.value = Object.assign({}, props.data);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
formRef,
|
||||
form,
|
||||
});
|
||||
|
||||
const typeOPtions = [
|
||||
{
|
||||
label: "聊天",
|
||||
value: "chart",
|
||||
},
|
||||
{
|
||||
label: "绘图",
|
||||
value: "img",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.content-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 350px;
|
||||
}
|
||||
.content-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 350px;
|
||||
svg {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.warning {
|
||||
color: #e6a23c;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
29
gpt-vue/projects/vue-admin/src/views/ApiKey/api.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const getList = (params?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/apikey/list",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
export const save = (data?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/apikey/save",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
export const deleting = (id: string | number) => {
|
||||
return http({
|
||||
url: `/api/admin/apikey/remove?id=${id}`,
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
export const setStatus = (data) => {
|
||||
return http({
|
||||
url: `/api/admin/apikey/set`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
@ -1,4 +1,122 @@
|
||||
<script lang="ts" setup></script>
|
||||
<script lang="ts" setup>
|
||||
import { getList, save, deleting, setStatus } from "./api";
|
||||
import { ref } from "vue";
|
||||
import ChatModelForm from "./ChatModelForm.vue";
|
||||
import useCustomFormPopup from "@/composables/useCustomFormPopup";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
|
||||
import { dateFormat } from "@gpt-vue/packages/utils";
|
||||
// table 配置
|
||||
const columns = [
|
||||
{
|
||||
title: "所属平台",
|
||||
dataIndex: "platform",
|
||||
},
|
||||
{
|
||||
title: "模型名称",
|
||||
dataIndex: "name",
|
||||
},
|
||||
{
|
||||
title: "模型值",
|
||||
dataIndex: "value",
|
||||
},
|
||||
{
|
||||
title: "对话权重",
|
||||
dataIndex: "weight",
|
||||
},
|
||||
{
|
||||
title: "启用状态",
|
||||
dataIndex: "enabled",
|
||||
slotName: "status",
|
||||
},
|
||||
{
|
||||
title: "开放状态",
|
||||
dataIndex: "open",
|
||||
slotName: "open",
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "created_at",
|
||||
render: ({ record }) => {
|
||||
return dateFormat(record.created_at);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
slotName: "action",
|
||||
},
|
||||
];
|
||||
|
||||
// 数据
|
||||
const tableData = ref([]);
|
||||
const getData = () => {
|
||||
getList().then(({ code, data }) => {
|
||||
if (code === 0) {
|
||||
tableData.value = data;
|
||||
}
|
||||
});
|
||||
};
|
||||
getData();
|
||||
|
||||
// 新增编辑
|
||||
const popup = useCustomFormPopup(ChatModelForm, save, {
|
||||
popupProps: (arg) => ({ title: arg[0].record ? "编辑ApiKey" : "新增ApiKey" }),
|
||||
});
|
||||
|
||||
// 删除
|
||||
const handleDelete = ({ id }, reload) => {
|
||||
deleting(id).then(({ code }) => {
|
||||
if (code === 0) {
|
||||
Message.success("操作成功");
|
||||
reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 状态
|
||||
const handleStatusChange = ({ filed, value, record, reload }) => {
|
||||
setStatus({
|
||||
id: record.id,
|
||||
value,
|
||||
filed,
|
||||
}).then(({ code }) => {
|
||||
if (code === 0) {
|
||||
Message.success("操作成功");
|
||||
reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div></div>
|
||||
<SimpleTable :columns="columns" :request="getList">
|
||||
<template #action="{ record, reload }">
|
||||
<a-link @click="popup({ record, reload })">编辑</a-link>
|
||||
<a-popconfirm content="确定删除?" @ok="handleDelete(record, reload)">
|
||||
<a-link status="danger">删除</a-link>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
<template #header="{ reload }">
|
||||
<a-button @click="popup({ reload })" size="small"><icon-plus />新增</a-button>
|
||||
</template>
|
||||
<template #status="{ record, reload }">
|
||||
<a-switch
|
||||
v-model="record.enabled"
|
||||
@change="
|
||||
(value) => {
|
||||
handleStatusChange({ filed: 'enabled', value, record, reload });
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #open="{ record, reload }">
|
||||
<a-switch
|
||||
v-model="record.open"
|
||||
@change="
|
||||
(value) => {
|
||||
handleStatusChange({ filed: 'open', value, record, reload });
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</SimpleTable>
|
||||
</template>
|
||||
|
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
|
||||
<a-form-item
|
||||
field="platform"
|
||||
label="所属平台"
|
||||
:rules="[{ required: true, message: '请输入所属平台' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
>
|
||||
<a-input v-model="form.platform" placeholder="请输入所属平台" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="name"
|
||||
label="名称"
|
||||
:rules="[{ required: true, message: '请输入名称' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
showable
|
||||
>
|
||||
<a-input v-model="form.name" placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="value"
|
||||
label="模型值"
|
||||
:rules="[{ required: true, message: '请输入名称' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
showable
|
||||
>
|
||||
<a-input v-model="form.value" placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="weight"
|
||||
label="对话权重"
|
||||
:rules="[{ required: true, message: '请输入对话权重' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
showable
|
||||
>
|
||||
<a-input-number v-model="form.weight" placeholder="请输入对话权重" />
|
||||
<a-tooltip content="对话权重,每次对话扣减多少次对话额度" position="right">
|
||||
<icon-info-circle-fill />
|
||||
</a-tooltip>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="open" label="开放状态代理">
|
||||
<a-switch v-model="form.open" />
|
||||
</a-form-item>
|
||||
<a-form-item field="enabled" label="启用状态">
|
||||
<a-switch v-model="form.enabled" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineExpose, defineProps } from "vue";
|
||||
const props = defineProps({
|
||||
data: {},
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const form = ref({});
|
||||
if (props.data?.id) {
|
||||
form.value = Object.assign({}, props.data);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
formRef,
|
||||
form,
|
||||
});
|
||||
|
||||
const typeOPtions = [
|
||||
{
|
||||
label: "聊天",
|
||||
value: "chart",
|
||||
},
|
||||
{
|
||||
label: "绘图",
|
||||
value: "img",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.content-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 350px;
|
||||
}
|
||||
.content-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 350px;
|
||||
svg {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
29
gpt-vue/projects/vue-admin/src/views/ChatModel/api.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const getList = (params?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/model/list",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
export const save = (data?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/model/save",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
export const deleting = (id: string | number) => {
|
||||
return http({
|
||||
url: `/api/admin/model/remove?id=${id}`,
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
export const setStatus = (data) => {
|
||||
return http({
|
||||
url: `/api/admin/model/set`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
110
gpt-vue/projects/vue-admin/src/views/Chats/ChatsContainer.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, h } from "vue";
|
||||
import { Message, Modal } from "@arco-design/web-vue";
|
||||
import SearchTable from "@/components/SearchTable/SearchTable.vue";
|
||||
import type { SearchTableColumns } from "@/components/SearchTable/type";
|
||||
import app from "@/main";
|
||||
import { getList, message, remove } from "./api";
|
||||
import ChatsLogs from "./ChatsLogs.vue";
|
||||
|
||||
const columns: SearchTableColumns[] = [
|
||||
{
|
||||
dataIndex: "user_id",
|
||||
title: "账户ID",
|
||||
search: {
|
||||
valueType: "input",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "username",
|
||||
title: "账户",
|
||||
},
|
||||
{
|
||||
dataIndex: "title",
|
||||
title: "标题",
|
||||
search: {
|
||||
valueType: "input",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "model",
|
||||
title: "模型",
|
||||
search: {
|
||||
valueType: "input",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "msg_num",
|
||||
title: "消息数量",
|
||||
},
|
||||
{
|
||||
dataIndex: "token",
|
||||
title: "消耗算力",
|
||||
},
|
||||
{
|
||||
dataIndex: "username",
|
||||
title: "账户",
|
||||
},
|
||||
{
|
||||
dataIndex: "created_at",
|
||||
title: "创建时间",
|
||||
search: {
|
||||
valueType: "range",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
fixed: "right",
|
||||
slotName: "actions",
|
||||
},
|
||||
];
|
||||
|
||||
const tabsList = [
|
||||
{ key: "1", title: "对话列表", api: getList, columns },
|
||||
{ key: "2", title: "消息记录", api: message, columns },
|
||||
];
|
||||
|
||||
const activeKey = ref(tabsList[0].key);
|
||||
|
||||
const handleRemove = async (chat_id, reload) => {
|
||||
await remove({ chat_id });
|
||||
Message.success("删除成功");
|
||||
await reload();
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCheck = (record) => {
|
||||
if (activeKey.value === "1") {
|
||||
Modal._context = app._context;
|
||||
Modal.info({
|
||||
title: "对话详情",
|
||||
width: 800,
|
||||
content: () => h(ChatsLogs, { id: record.chat_id }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
Modal.info({
|
||||
title: "消息详情",
|
||||
content: record.content,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a-tabs v-model:active-key="activeKey" lazy-load justify>
|
||||
<a-tab-pane v-for="item in tabsList" :key="item.key" :title="item.title">
|
||||
<SearchTable :request="item.api" :columns="item.columns">
|
||||
<template #actions="{ record, reload }">
|
||||
<a-link @click="handleCheck(record)">查看</a-link>
|
||||
<a-popconfirm
|
||||
content="是否删除?"
|
||||
position="left"
|
||||
type="warning"
|
||||
:on-before-ok="() => handleRemove(record.id, reload)"
|
||||
>
|
||||
<a-link status="danger">删除</a-link>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</SearchTable>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
94
gpt-vue/projects/vue-admin/src/views/Chats/ChatsLogs.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import { dateFormat } from "@gpt-vue/packages/utils";
|
||||
import useRequest from "@/composables/useRequest";
|
||||
import { history } from "./api";
|
||||
|
||||
const props = defineProps({
|
||||
id: String,
|
||||
});
|
||||
|
||||
const [getData, data, loading] = useRequest(history);
|
||||
onMounted(async () => {
|
||||
await getData({ chat_id: props.id });
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<template v-if="loading">
|
||||
<div class="custom-skeleton">
|
||||
<a-skeleton-shape />
|
||||
<div style="flex: 1">
|
||||
<a-skeleton-line :rows="2" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="item in data" :key="item.id">
|
||||
<div class="item-container" :class="item.type">
|
||||
<div class="left">
|
||||
<a-avatar shape="square">
|
||||
<img :src="item.icon" />
|
||||
</a-avatar>
|
||||
</div>
|
||||
<a-space class="right" direction="vertical">
|
||||
<div>{{ item.content }}</div>
|
||||
<a-space>
|
||||
<div class="code">
|
||||
<icon-clock-circle />
|
||||
{{ dateFormat(item.created_at) }}
|
||||
</div>
|
||||
<div class="code">算力消耗: {{ item.tokens }}</div>
|
||||
<a-typography-text
|
||||
v-if="item.type === 'reply'"
|
||||
copyable
|
||||
:copy-delay="1000"
|
||||
:copy-text="item.content"
|
||||
@copy="Message.success('复制成功')"
|
||||
>
|
||||
<template #copy-icon>
|
||||
<a-button class="code" size="mini">
|
||||
<icon-copy />
|
||||
</a-button>
|
||||
</template>
|
||||
<template #copy-tooltip>复制回答</template>
|
||||
</a-typography-text>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.item-container {
|
||||
display: flex;
|
||||
padding: 20px 10px;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
box-sizing: border-box;
|
||||
align-items: flex-start;
|
||||
&.reply {
|
||||
background: #f7f7f8;
|
||||
}
|
||||
.left {
|
||||
width: 40px;
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.code {
|
||||
background-color: #e7e7e8;
|
||||
color: #888;
|
||||
padding: 3px 5px;
|
||||
margin-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
.custom-skeleton {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
34
gpt-vue/projects/vue-admin/src/views/Chats/api.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const getList = (data) => {
|
||||
return http({
|
||||
url: "/api/admin/chat/list",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const message = (data) => {
|
||||
return http({
|
||||
url: "/api/admin/chat/message",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const history = (params) => {
|
||||
return http({
|
||||
url: "/api/admin/chat/history",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const remove = (params) => {
|
||||
return http({
|
||||
url: "/api/admin/chat/remove",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
@ -1,4 +1,86 @@
|
||||
<script lang="ts" setup></script>
|
||||
<script lang="ts" setup>
|
||||
import http from "@/http/config";
|
||||
import { ref } from "vue";
|
||||
|
||||
const dataSet = {
|
||||
users: "今日新增用户",
|
||||
chats: "今日新增对话",
|
||||
tokens: "今日消耗 Tokens",
|
||||
income: "今日入账",
|
||||
};
|
||||
|
||||
const icons = {
|
||||
users: "icon-user",
|
||||
chats: "icon-wechat",
|
||||
tokens: "icon-computer",
|
||||
income: "icon-wechatpay",
|
||||
};
|
||||
|
||||
const data = ref<Record<string, number>>({});
|
||||
const getData = () => {
|
||||
http({
|
||||
url: "api/admin/dashboard/stats",
|
||||
method: "get",
|
||||
}).then((res) => {
|
||||
data.value = res.data;
|
||||
});
|
||||
};
|
||||
getData();
|
||||
</script>
|
||||
<template>
|
||||
<div></div>
|
||||
<div class="dashboard">
|
||||
<a-grid :cols="{ xs: 1, sm: 1, md: 2, lg: 3, xl: 4 }" :colGap="12" :rowGap="16" class="grid">
|
||||
<a-grid-item v-for="(value, key) in dataSet" :key="key">
|
||||
<div class="data-card">
|
||||
<span :class="key" class="icon"><icon-user /></span>
|
||||
<span class="count"
|
||||
><a-statistic :extra="value" :value="data[key]" :precision="0"
|
||||
/></span>
|
||||
</div>
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.dashboard {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
.grid {
|
||||
width: 100%;
|
||||
}
|
||||
.data-card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 0 0 25%;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
.icon {
|
||||
display: inline-block;
|
||||
font-size: 50px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
color: #fff;
|
||||
}
|
||||
.users {
|
||||
background: #2d8cf0;
|
||||
}
|
||||
.chats {
|
||||
background: #64d572;
|
||||
}
|
||||
.tokens {
|
||||
background: #f25e43;
|
||||
}
|
||||
.income {
|
||||
background: #f25e43;
|
||||
}
|
||||
.count {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import { Message, type TableColumnData } from "@arco-design/web-vue";
|
||||
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
|
||||
import ConfirmSwitch from "@/components/ConfirmSwitch.vue";
|
||||
import usePopup from "@/composables/usePopup";
|
||||
import { getList, remove, setStatus, save } from "./api";
|
||||
import FunctionsForm from "./FunctionsForm.vue";
|
||||
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
dataIndex: "name",
|
||||
title: "函数名称",
|
||||
},
|
||||
{
|
||||
dataIndex: "label",
|
||||
title: "函数别名",
|
||||
},
|
||||
{
|
||||
dataIndex: "description",
|
||||
title: "功能描述",
|
||||
},
|
||||
{
|
||||
dataIndex: "enabled",
|
||||
title: "启用状态",
|
||||
slotName: "switch",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
slotName: "actions",
|
||||
fixed: "right",
|
||||
},
|
||||
];
|
||||
|
||||
const openFormModal = usePopup(FunctionsForm, {
|
||||
nodeProps: ([_, record]) => ({ record }),
|
||||
popupProps: ([reload, record], exposed) => ({
|
||||
title: `${record?.id ? "编辑" : "新增"}函数`,
|
||||
width: "800px",
|
||||
onBeforeOk: async (done) => {
|
||||
await exposed()?.handleSubmit(save, {
|
||||
id: record?.id,
|
||||
parameters: exposed()?.parameters(),
|
||||
});
|
||||
await reload();
|
||||
done(true);
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const handleRemove = async (id, reload) => {
|
||||
await remove({ id });
|
||||
Message.success("删除成功");
|
||||
await reload();
|
||||
return true;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<SimpleTable :request="getList" :columns="columns" :pagination="false">
|
||||
<template #header="{ reload }">
|
||||
<a-button @click="openFormModal(reload, {})">
|
||||
<template #icon> <icon-plus /> </template>
|
||||
新增
|
||||
</a-button>
|
||||
</template>
|
||||
<template #switch="{ record, column }">
|
||||
<ConfirmSwitch
|
||||
v-model="record[column.dataIndex]"
|
||||
:api="(p) => setStatus({ ...p, id: record.id, filed: 'enabled' })"
|
||||
/>
|
||||
</template>
|
||||
<template #exchange="{ record }">
|
||||
<a-tag v-if="record.exchange.calls > 0">聊天{{ record.exchange.calls }}次</a-tag>
|
||||
<a-tag v-else-if="record.exchange.img_calls > 0" color="green"
|
||||
>绘图{{ record.exchange.img_calls }}次</a-tag
|
||||
>
|
||||
</template>
|
||||
<template #actions="{ record, reload }">
|
||||
<a-link @click="openFormModal(reload, record)">编辑</a-link>
|
||||
<a-popconfirm
|
||||
content="是否删除?"
|
||||
position="left"
|
||||
type="warning"
|
||||
:on-before-ok="() => handleRemove(record.id, reload)"
|
||||
>
|
||||
<a-link status="danger">删除</a-link>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</SimpleTable>
|
||||
</template>
|
@ -0,0 +1,81 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watchEffect } from "vue";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import useSubmit from "@/composables/useSubmit";
|
||||
import FunctionsFormTable from "./FunctionsFormTable.vue";
|
||||
import { token } from "./api";
|
||||
import translateTableData from "./translateTableData";
|
||||
|
||||
const props = defineProps({
|
||||
record: Object,
|
||||
});
|
||||
|
||||
const tableData = ref([]);
|
||||
const { formRef, formData, handleSubmit, submitting } = useSubmit({
|
||||
name: "",
|
||||
label: "",
|
||||
description: "",
|
||||
action: "",
|
||||
token: "",
|
||||
parameters: {},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: "请输入函数名称" }],
|
||||
label: [{ required: true, message: "请输入函数标签" }],
|
||||
description: [{ required: true, message: "请输入函数功能描述" }],
|
||||
};
|
||||
|
||||
const generateToken = async () => {
|
||||
const { data } = await token({});
|
||||
Message.success("生成 Token 成功");
|
||||
formData.token = data;
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
Object.assign(formData, props.record ?? {});
|
||||
tableData.value = translateTableData.get(formData.parameters);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
handleSubmit,
|
||||
parameters: () => translateTableData.set(tableData.value),
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<a-spin :loading="submitting" style="width: 100%">
|
||||
<a-form ref="formRef" :model="formData" auto-label-width :rules="rules">
|
||||
<a-form-item field="name" label="函数名称">
|
||||
<a-input v-model="formData.name" placeholder="函数名称最好为英文" />
|
||||
</a-form-item>
|
||||
<a-form-item field="label" label="函数标签">
|
||||
<a-input v-model="formData.label" placeholder="函数的中文名称" />
|
||||
</a-form-item>
|
||||
<a-form-item field="description" label="功能描述">
|
||||
<a-input v-model="formData.description" placeholder="函数的中文名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="parameters" label="函数参数">
|
||||
<FunctionsFormTable v-model="tableData" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="action" label="API 地址">
|
||||
<a-input v-model="formData.action" placeholder="该函数实现的API地址,可以是第三方服务API" />
|
||||
</a-form-item>
|
||||
<a-form-item field="token" label="API Token">
|
||||
<a-input-group style="width: 100%">
|
||||
<a-input v-model="formData.token" placeholder="API授权Token" />
|
||||
<a-tooltip
|
||||
content="只有本地服务才可以使用自动生成Token第三方服务请填写第三方服务API Token"
|
||||
>
|
||||
<a-button type="primary" @click="generateToken">生成Token</a-button>
|
||||
</a-tooltip>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item field="enabled" label="启用状态">
|
||||
<a-switch v-model="formData.enabled" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</template>
|
@ -0,0 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
|
||||
const handleCreateRow = () => {
|
||||
emits("update:modelValue", [
|
||||
...(props.modelValue ?? []),
|
||||
{ name: "", type: "", description: "", required: false },
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRemoveRow = (index) => {
|
||||
emits(
|
||||
"update:modelValue",
|
||||
props.modelValue.filter((_, i) => i !== index)
|
||||
);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-table :data="modelValue" size="small" style="width: 100%" :pagination="false">
|
||||
<template #columns>
|
||||
<a-table-column title="参数名称" :width="120">
|
||||
<template #cell="scope">
|
||||
<a-input v-model="scope.record.name" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="参数类型" :width="150">
|
||||
<template #cell="scope">
|
||||
<a-select
|
||||
v-model="scope.record.type"
|
||||
placeholder="参数类型"
|
||||
:options="['string', 'number']"
|
||||
/>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="参数描述">
|
||||
<template #cell="scope">
|
||||
<a-input v-model="scope.record.description" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
|
||||
<a-table-column title="必填参数" :width="100" align="center">
|
||||
<template #cell="scope">
|
||||
<a-checkbox v-model="scope.record.required" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
|
||||
<a-table-column title="操作" :width="80">
|
||||
<template #cell="scope">
|
||||
<a-button
|
||||
status="danger"
|
||||
shape="circle"
|
||||
@click="handleRemoveRow(scope.rowIndex)"
|
||||
size="small"
|
||||
>
|
||||
<icon-delete />
|
||||
</a-button>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-button type="primary" @click="handleCreateRow">
|
||||
<template #icon><icon-plus /></template>
|
||||
<span>新增参数</span>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
41
gpt-vue/projects/vue-admin/src/views/Functions/api.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const getList = (params) => {
|
||||
return http({
|
||||
url: "/api/admin/function/list",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const save = (data) => {
|
||||
return http({
|
||||
url: "/api/admin/function/save",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const remove = (params) => {
|
||||
return http({
|
||||
url: "/api/admin/function/remove",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const setStatus = (data) => {
|
||||
return http({
|
||||
url: "/api/admin/function/set",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const token = (params) => {
|
||||
return http({
|
||||
url: "/api/admin/function/token",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
const get = (origin) => {
|
||||
const { properties, required, type } = origin;
|
||||
if (type === "object") {
|
||||
const array = Object.keys(properties).reduce((prev, name) => {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
name,
|
||||
...properties[name],
|
||||
required: required.includes(name),
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
return array;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const set = (tableData) => {
|
||||
const properties = tableData.reduce((prev, curr) => {
|
||||
if (curr.name) {
|
||||
return {
|
||||
...prev,
|
||||
[curr.name]: {
|
||||
description: curr.description,
|
||||
type: curr.type,
|
||||
},
|
||||
};
|
||||
}
|
||||
return prev
|
||||
}, {});
|
||||
const required = tableData.filter((i) => i.required).map((i) => i.name);
|
||||
return { properties, required, type: "object" }
|
||||
}
|
||||
|
||||
export default { get, set }
|
29
gpt-vue/projects/vue-admin/src/views/LoginLog.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { dateFormat } from "@gpt-vue/packages/utils";
|
||||
import SearchTable from "@/components/SearchTable/SearchTable.vue";
|
||||
import type { SearchTableColumns } from "@/components/SearchTable/type";
|
||||
import { loginLog } from "@/http/login";
|
||||
|
||||
const columns: SearchTableColumns[] = [
|
||||
{
|
||||
dataIndex: "username",
|
||||
title: "用户名",
|
||||
},
|
||||
{
|
||||
dataIndex: "login_ip",
|
||||
title: "登录IP",
|
||||
},
|
||||
{
|
||||
dataIndex: "login_address",
|
||||
title: "登录地址",
|
||||
},
|
||||
{
|
||||
dataIndex: "created_at",
|
||||
title: "登陆时间",
|
||||
render: ({ record }) => dateFormat(record.created_at),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<SearchTable :request="loginLog" :columns="columns" />
|
||||
</template>
|
162
gpt-vue/projects/vue-admin/src/views/LoginView.vue
Normal file
@ -0,0 +1,162 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import useRequest from "@/composables/useRequest";
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const [loginRequest, _, loading] = useRequest(authStore.login);
|
||||
const formData = reactive({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
async function handleSubmit({ errors, values }: any) {
|
||||
if (errors) return;
|
||||
await loginRequest({
|
||||
...values,
|
||||
...route.query,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="bg">
|
||||
<div class="content">
|
||||
<!-- 左侧图片 -->
|
||||
<span class="left">
|
||||
<img src="/left-img.png" alt="" style="width: 468px" />
|
||||
</span>
|
||||
<!-- 表单 -->
|
||||
<div class="right-content">
|
||||
<div class="form-box">
|
||||
<div class="title">ChatGPT Plus Admin</div>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
class="form"
|
||||
size="medium"
|
||||
auto-label-width
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-form-item
|
||||
field="username"
|
||||
label="账号"
|
||||
hide-label
|
||||
hide-asterisk
|
||||
:rules="[{ required: true, message: '请输入您的账号' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入您的账号"
|
||||
class="input"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="password"
|
||||
label="密码"
|
||||
hide-label
|
||||
hide-asterisk
|
||||
:rules="[{ required: true, message: '请输入您的密码' }]"
|
||||
>
|
||||
<a-input-password
|
||||
v-model="formData.password"
|
||||
placeholder="请输入您的密码"
|
||||
class="input"
|
||||
>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
<a-form-item hide-label>
|
||||
<a-button :disabled="loading" html-type="submit" long type="primary" class="sign-in-btn">
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.bg {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: linear-gradient(133deg, #ffffff 0%, #dde8fe 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 1080px;
|
||||
height: 557px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.left {
|
||||
width: 540px;
|
||||
height: 557px;
|
||||
background: #2670fe;
|
||||
border-radius: 16px 0 0 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right-content {
|
||||
width: 540px;
|
||||
height: 557px;
|
||||
background: #ffffff;
|
||||
border-radius: 0 16px 16px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #333333;
|
||||
line-height: 35px;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.form-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 438px;
|
||||
padding: 58px 0;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form {
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.input {
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
height: 60px;
|
||||
border: 2px solid #e5e6eb;
|
||||
}
|
||||
|
||||
.captcha-image {
|
||||
//width: 100%;
|
||||
//height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sign-in-btn {
|
||||
height: 60px;
|
||||
font-weight: 500;
|
||||
line-height: 33px;
|
||||
font-size: 28px;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
}
|
||||
</style>
|
@ -1,9 +1,9 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const getList = (params?: Record<string, unknown>) => {
|
||||
export const getList = (data?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/admin/order/list",
|
||||
methods: "get",
|
||||
params
|
||||
url: "/api/admin/order/list",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
@ -1,4 +1,118 @@
|
||||
<script lang="ts" setup></script>
|
||||
<script lang="ts" setup>
|
||||
import { getList, save, deleting, setStatus } from "./api";
|
||||
import { ref } from "vue";
|
||||
import ProductForm from "./ProductForm.vue";
|
||||
import useCustomFormPopup from "@/composables/useCustomFormPopup";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
|
||||
import { dateFormat } from "@gpt-vue/packages/utils";
|
||||
// table 配置
|
||||
const columns = [
|
||||
{
|
||||
title: "产品名称",
|
||||
dataIndex: "name",
|
||||
},
|
||||
{
|
||||
title: "产品价格",
|
||||
dataIndex: "price",
|
||||
},
|
||||
{
|
||||
title: "优惠金额",
|
||||
dataIndex: "discount",
|
||||
},
|
||||
{
|
||||
title: "有效期(天)",
|
||||
dataIndex: "days",
|
||||
},
|
||||
{
|
||||
title: "对话次数",
|
||||
dataIndex: "calls",
|
||||
},
|
||||
{
|
||||
title: "绘图次数",
|
||||
dataIndex: "img_calls",
|
||||
},
|
||||
{
|
||||
title: "销量",
|
||||
dataIndex: "sales",
|
||||
},
|
||||
{
|
||||
title: "启用状态",
|
||||
dataIndex: "enabled",
|
||||
slotName: "status",
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updated_at",
|
||||
render: ({ record }) => {
|
||||
return dateFormat(record.updated_at);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
slotName: "action",
|
||||
},
|
||||
];
|
||||
|
||||
// 数据
|
||||
const tableData = ref([]);
|
||||
const getData = () => {
|
||||
getList().then(({ code, data }) => {
|
||||
if (code === 0) {
|
||||
tableData.value = data;
|
||||
}
|
||||
});
|
||||
};
|
||||
getData();
|
||||
|
||||
// 新增编辑
|
||||
const popup = useCustomFormPopup(ProductForm, save, {
|
||||
popupProps: (arg) => ({ title: arg[0].record ? "编辑产品" : "新增产品" }),
|
||||
});
|
||||
|
||||
// 删除
|
||||
const handleDelete = ({ id }, reload) => {
|
||||
deleting(id).then(({ code }) => {
|
||||
if (code === 0) {
|
||||
Message.success("操作成功");
|
||||
reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 状态
|
||||
const handleStatusChange = ({ value, record, reload }) => {
|
||||
setStatus({
|
||||
id: record.id,
|
||||
enabled: value,
|
||||
}).then(({ code }) => {
|
||||
if (code === 0) {
|
||||
Message.success("操作成功");
|
||||
reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div></div>
|
||||
<SimpleTable :columns="columns" :request="getList">
|
||||
<template #action="{ record, reload }">
|
||||
<a-link @click="popup({ record, reload })">编辑</a-link>
|
||||
<a-popconfirm content="确定删除?" @ok="handleDelete(record, reload)">
|
||||
<a-link status="danger">删除</a-link>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
<template #header="{ reload }">
|
||||
<a-button @click="popup({ reload })" size="small"><icon-plus />新增</a-button>
|
||||
</template>
|
||||
<template #status="{ record, reload }">
|
||||
<a-switch
|
||||
v-model="record.enabled"
|
||||
@change="
|
||||
(value) => {
|
||||
handleStatusChange({ value, record, reload });
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</SimpleTable>
|
||||
</template>
|
||||
|
87
gpt-vue/projects/vue-admin/src/views/Product/ProductForm.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
|
||||
<a-form-item
|
||||
field="name"
|
||||
label="产品名称"
|
||||
:rules="[{ required: true, message: '请输入产品名称' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
>
|
||||
<a-input v-model="form.name" placeholder="请输入产品名称" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="price"
|
||||
label="产品价格"
|
||||
:rules="[{ required: true, message: '请输入产品价格' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
showable
|
||||
>
|
||||
<a-input-number v-model="form.price" placeholder="请输入产品价格" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="discount"
|
||||
label="优惠金额"
|
||||
:rules="[{ required: true, message: '请输入优惠金额' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
showable
|
||||
>
|
||||
<a-input-number v-model="form.discount" placeholder="请输入优惠金额" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="days"
|
||||
label="有效期(天)"
|
||||
:rules="[{ required: true, message: '请输入有效期(天)' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
showable
|
||||
>
|
||||
<a-input-number v-model="form.days" placeholder="请输入有效期(天)" />
|
||||
</a-form-item>
|
||||
<a-form-item field="calls" label="对话次数" :validate-trigger="['change', 'input']" showable>
|
||||
<a-input-number v-model="form.calls" placeholder="请输入对话次数" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="img_calls"
|
||||
label="绘图次数"
|
||||
:validate-trigger="['change', 'input']"
|
||||
showable
|
||||
>
|
||||
<a-input-number v-model="form.img_calls" placeholder="请输入绘图次数" />
|
||||
</a-form-item>
|
||||
<a-form-item field="enabled" label="启用状态">
|
||||
<a-switch v-model="form.enabled" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineExpose, defineProps } from "vue";
|
||||
const props = defineProps({
|
||||
data: {},
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const form = ref({});
|
||||
if (props.data?.id) {
|
||||
form.value = Object.assign({}, props.data);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
formRef,
|
||||
form,
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.content-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 350px;
|
||||
}
|
||||
.content-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 350px;
|
||||
svg {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
29
gpt-vue/projects/vue-admin/src/views/Product/api.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const getList = (params?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/product/list",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
export const save = (data?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/product/save",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
export const deleting = (id: string | number) => {
|
||||
return http({
|
||||
url: `/api/admin/product/remove?id=${id}`,
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
export const setStatus = (data) => {
|
||||
return http({
|
||||
url: `/api/admin/product/enable`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import { Message, type TableColumnData } from "@arco-design/web-vue";
|
||||
import { dateFormat } from "@gpt-vue/packages/utils";
|
||||
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
|
||||
import { getList, remove } from "./api";
|
||||
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
dataIndex: "username",
|
||||
title: "用户",
|
||||
},
|
||||
{
|
||||
dataIndex: "tx_id",
|
||||
title: "转账单号",
|
||||
},
|
||||
{
|
||||
dataIndex: "amount",
|
||||
title: "转账金额",
|
||||
},
|
||||
{
|
||||
dataIndex: "remark",
|
||||
title: "备注",
|
||||
},
|
||||
{
|
||||
dataIndex: "created_at",
|
||||
title: "转账时间",
|
||||
render: ({ record }) => dateFormat(record.created_at),
|
||||
},
|
||||
{
|
||||
title: "核销时间",
|
||||
slotName: "updated_at",
|
||||
},
|
||||
{
|
||||
title: "兑换详情",
|
||||
slotName: "exchange",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
slotName: "actions",
|
||||
fixed: "right",
|
||||
width: 80,
|
||||
},
|
||||
];
|
||||
|
||||
const handleRemove = async (id, reload) => {
|
||||
await remove({ id });
|
||||
Message.success("删除成功");
|
||||
await reload();
|
||||
return true;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<SimpleTable :request="getList" :columns="columns">
|
||||
<template #updated_at="{ record }">
|
||||
<span v-if="record.status">{{ dateFormat(record.updated_at) }}</span>
|
||||
<a-tag v-else color="blue">未核销</a-tag>
|
||||
</template>
|
||||
<template #exchange="{ record }">
|
||||
<a-tag v-if="record.exchange.calls > 0">聊天{{ record.exchange.calls }}次</a-tag>
|
||||
<a-tag v-else-if="record.exchange.img_calls > 0" color="green"
|
||||
>绘图{{ record.exchange.img_calls }}次</a-tag
|
||||
>
|
||||
</template>
|
||||
<template #actions="{ record, reload }">
|
||||
<a-popconfirm
|
||||
content="是否删除?"
|
||||
position="left"
|
||||
type="warning"
|
||||
:on-before-ok="() => handleRemove(record.id, reload)"
|
||||
>
|
||||
<a-link status="danger">删除</a-link>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</SimpleTable>
|
||||
</template>
|
17
gpt-vue/projects/vue-admin/src/views/Reward/api.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const getList = (params?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/reward/list",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const remove = (params?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/reward/remove",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
@ -1,4 +1,127 @@
|
||||
<script lang="ts" setup></script>
|
||||
<script lang="ts" setup>
|
||||
import { getList, save, deleting, setStatus } from "./api";
|
||||
import { reactive, ref } from "vue";
|
||||
import RoleForm from "./RoleForm.vue";
|
||||
import useCustomFormPopup from "@/composables/useCustomFormPopup";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
|
||||
// table 配置
|
||||
const columns = [
|
||||
{
|
||||
title: "角色名称",
|
||||
dataIndex: "name",
|
||||
},
|
||||
{
|
||||
title: "角色标识",
|
||||
dataIndex: "key",
|
||||
},
|
||||
{
|
||||
title: "启用状态",
|
||||
dataIndex: "enable",
|
||||
slotName: "status",
|
||||
},
|
||||
{
|
||||
title: "角色图标",
|
||||
dataIndex: "icon",
|
||||
slotName: "icon",
|
||||
},
|
||||
{
|
||||
title: "打招呼信息",
|
||||
dataIndex: "hello_msg",
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
slotName: "action",
|
||||
},
|
||||
];
|
||||
|
||||
const expandable = reactive({
|
||||
title: "",
|
||||
width: 80,
|
||||
});
|
||||
|
||||
// 数据
|
||||
const tableData = ref([]);
|
||||
const getData = () => {
|
||||
getList().then(({ code, data }) => {
|
||||
if (code === 0) {
|
||||
tableData.value = data;
|
||||
}
|
||||
});
|
||||
};
|
||||
getData();
|
||||
|
||||
//展开行table
|
||||
const expandColumns = [
|
||||
{
|
||||
dataIndex: "role",
|
||||
title: "对话角色",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
dataIndex: "content",
|
||||
title: "对话内容",
|
||||
},
|
||||
];
|
||||
|
||||
// 新增编辑
|
||||
const popup = useCustomFormPopup(RoleForm, save, {
|
||||
popupProps: (arg) => ({ title: arg[0].record ? "编辑角色" : "新增角色" }),
|
||||
});
|
||||
|
||||
// 删除
|
||||
const handleDelete = ({ id }, reload) => {
|
||||
deleting(id).then(({ code }) => {
|
||||
if (code === 0) {
|
||||
Message.success("操作成功");
|
||||
reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 状态
|
||||
const handleStatusChange = ({ value, record, reload }) => {
|
||||
setStatus({
|
||||
id: record.id,
|
||||
value,
|
||||
filed: "enable",
|
||||
}).then(({ code }) => {
|
||||
if (code === 0) {
|
||||
Message.success("操作成功");
|
||||
reload();
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div></div>
|
||||
<SimpleTable :columns="columns" :request="getList" :expandable="expandable">
|
||||
<template #expand-row="{ record }">
|
||||
<a-table :columns="expandColumns" :data="record.context || []" :pagination="false"></a-table>
|
||||
</template>
|
||||
<template #action="{ record, reload }">
|
||||
<a-link @click="popup({ record, reload })">编辑</a-link>
|
||||
<a-popconfirm content="确定删除?" @ok="handleDelete(record, reload)">
|
||||
<a-link status="danger">删除</a-link>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
<template #header="{ reload }">
|
||||
<a-button @click="popup({ reload })" size="small"><icon-plus />新增</a-button>
|
||||
</template>
|
||||
<template #status="{ record, reload }">
|
||||
<a-switch
|
||||
v-model="record.enable"
|
||||
@change="
|
||||
(value) => {
|
||||
handleStatusChange({ value, record, reload });
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #icon="{ record }">
|
||||
<a-avatar>
|
||||
<img alt="avatar" :src="record.icon" />
|
||||
</a-avatar>
|
||||
</template>
|
||||
</SimpleTable>
|
||||
</template>
|
||||
|
116
gpt-vue/projects/vue-admin/src/views/Role/RoleForm.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
|
||||
<a-form-item
|
||||
field="name"
|
||||
label="角色名称"
|
||||
:rules="[{ required: true, message: '请输入角色名称' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
>
|
||||
<a-input v-model="form.name" placeholder="请输入角色名称" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="key"
|
||||
label="角色标志"
|
||||
:rules="[{ required: true, message: '请输入角色标志' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
showable
|
||||
>
|
||||
<a-input v-model="form.key" placeholder="请输入角色标志" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="icon"
|
||||
label="角色图标"
|
||||
:rules="[{ required: true, message: '请输入角色图标' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
>
|
||||
<a-input v-model="form.icon" placeholder="请输入角色图标" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="hello_msg"
|
||||
label="打招呼信息"
|
||||
:rules="[{ required: true, message: '请输入打招呼信息' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
>
|
||||
<a-input v-model="form.hello_msg" placeholder="请输入打招呼信息" />
|
||||
</a-form-item>
|
||||
<a-form-item field="username" label="上下文信息" :validate-trigger="['change', 'input']">
|
||||
<a-table :data="form.context || []" :pagination="false">
|
||||
<template #columns>
|
||||
<a-table-column title="对话角色">
|
||||
<template #cell="{ record }">
|
||||
<a-input v-model="record.role" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column width="350">
|
||||
<template #title>
|
||||
<div class="content-title">
|
||||
<span>对话内容</span>
|
||||
<a-button @click="addContext" type="primary">增加一行</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell="{ record }">
|
||||
<div class="content-cell">
|
||||
<a-input v-model="record.content" /><icon-minus-circle
|
||||
@click="removeContext(record)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-form-item>
|
||||
<a-form-item field="enable" label="启用状态">
|
||||
<a-switch v-model="form.enable" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineExpose, defineProps } from "vue";
|
||||
const props = defineProps({
|
||||
data: {},
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const form = ref({});
|
||||
if (props.data?.id) {
|
||||
form.value = Object.assign({}, props.data);
|
||||
}
|
||||
|
||||
const addContext = () => {
|
||||
form.value.context = form.value.context || [];
|
||||
form.value.context.push({
|
||||
role: "",
|
||||
content: "",
|
||||
});
|
||||
};
|
||||
|
||||
const removeContext = (record) => {
|
||||
const index = form.value.context.findIndex((item) => {
|
||||
return item === record;
|
||||
});
|
||||
if (index > -1) {
|
||||
form.value.context.splice(index, 1);
|
||||
}
|
||||
};
|
||||
defineExpose({
|
||||
formRef,
|
||||
form,
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.content-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 350px;
|
||||
}
|
||||
.content-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 350px;
|
||||
svg {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
29
gpt-vue/projects/vue-admin/src/views/Role/api.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const getList = (params?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/role/list",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
export const save = (data?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/role/save",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
export const deleting = (id: string | number) => {
|
||||
return http({
|
||||
url: `/api/admin/role/remove?id=${id}`,
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
export const setStatus = (data) => {
|
||||
return http({
|
||||
url: `/api/admin/role/set`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
155
gpt-vue/projects/vue-admin/src/views/System/SystemBaseConfig.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import useSubmit from "@/composables/useSubmit";
|
||||
import useRequest from "@/composables/useRequest";
|
||||
import { getConfig, modelList, save } from "./api";
|
||||
import SystemUploader from "./SystemUploader.vue";
|
||||
|
||||
const { formRef, formData: system, handleSubmit, submitting } = useSubmit({});
|
||||
|
||||
const [getModelOptions, modelOptions, modelOptionsLoading] = useRequest(modelList);
|
||||
|
||||
const rules = {
|
||||
title: [{ required: true, message: "请输入网站标题" }],
|
||||
admin_title: [{ required: true, message: "请输入控制台标题" }],
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
await handleSubmit(
|
||||
() =>
|
||||
save({
|
||||
key: "system",
|
||||
config: system,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
Message.success("保存成功");
|
||||
};
|
||||
|
||||
const reload = async () => {
|
||||
const { data } = await getConfig({ key: "system" });
|
||||
data && Object.assign(system, data);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
getModelOptions();
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
<a-form ref="formRef" :model="system" :rules="rules" auto-label-width :disabled="submitting">
|
||||
<a-form-item label="网站标题" field="title">
|
||||
<a-input v-model="system['title']" />
|
||||
</a-form-item>
|
||||
<a-form-item label="控制台标题" field="admin_title">
|
||||
<a-input v-model="system['admin_title']" />
|
||||
</a-form-item>
|
||||
<a-form-item label="注册赠送对话次数" field="user_init_calls">
|
||||
<a-input v-model.number="system['init_chat_calls']" placeholder="新用户注册赠送对话次数" />
|
||||
</a-form-item>
|
||||
<a-form-item label="注册赠送绘图次数" field="init_img_calls">
|
||||
<a-input v-model.number="system['init_img_calls']" placeholder="新用户注册赠送绘图次数" />
|
||||
</a-form-item>
|
||||
<a-form-item label="邀请赠送对话次数" field="invite_chat_calls">
|
||||
<a-input
|
||||
v-model.number="system['invite_chat_calls']"
|
||||
placeholder="邀请新用户注册赠送对话次数"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="邀请赠送绘图次数" field="invite_img_calls">
|
||||
<a-input
|
||||
v-model.number="system['invite_img_calls']"
|
||||
placeholder="邀请新用户注册赠送绘图次数"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="VIP每月对话次数" field="vip_month_calls">
|
||||
<a-input v-model.number="system['vip_month_calls']" placeholder="VIP用户每月赠送对话次数" />
|
||||
</a-form-item>
|
||||
<a-form-item label="VIP每月绘图次数" field="vip_month_img_calls">
|
||||
<a-input
|
||||
v-model.number="system['vip_month_img_calls']"
|
||||
placeholder="VIP用户每月赠送绘图次数"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="开放注册" field="enabled_register">
|
||||
<a-space>
|
||||
<a-switch v-model="system['enabled_register']" />
|
||||
<a-tooltip content="关闭注册之后只能通过管理后台添加用户" position="right">
|
||||
<icon-info-circle-fill size="18" />
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<a-form-item label="注册方式" field="register_ways">
|
||||
<a-checkbox-group v-model="system['register_ways']">
|
||||
<a-checkbox value="mobile">手机注册</a-checkbox>
|
||||
<a-checkbox value="email">邮箱注册</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="启用众筹功能" field="enabled_reward">
|
||||
<a-space>
|
||||
<a-switch v-model="system['enabled_reward']" />
|
||||
<a-tooltip content="如果关闭次功能将不在用户菜单显示众筹二维码" position="right">
|
||||
<icon-info-circle-fill size="18" />
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<template v-if="system['enabled_reward']">
|
||||
<a-form-item label="单次对话价格" field="chat_call_price">
|
||||
<a-input v-model="system['chat_call_price']" placeholder="众筹金额跟对话次数的兑换比例" />
|
||||
</a-form-item>
|
||||
<a-form-item label="单次绘图价格" field="img_call_price">
|
||||
<a-input v-model="system['img_call_price']" placeholder="众筹金额跟绘图次数的兑换比例" />
|
||||
</a-form-item>
|
||||
<a-form-item label="收款二维码" field="reward_img">
|
||||
<SystemUploader v-model="system['reward_img']" placeholder="众筹收款二维码地址" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="微信客服二维码" field="wechat_card_url">
|
||||
<SystemUploader v-model="system['wechat_card_url']" placeholder="微信客服二维码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="订单超时时间" field="order_pay_timeout">
|
||||
<a-space style="width: 100%">
|
||||
<a-input
|
||||
v-model.number="system['order_pay_timeout']"
|
||||
placeholder="单位:秒"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<a-tooltip position="right">
|
||||
<icon-info-circle-fill size="18" />
|
||||
<template #content> 系统会定期清理超时未支付的订单<br />默认值:900秒 </template>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<a-form-item label="会员充值说明" field="order_pay_info_text">
|
||||
<a-textarea
|
||||
v-model="system['order_pay_info_text']"
|
||||
:autosize="{ minRows: 3, maxRows: 10 }"
|
||||
placeholder="请输入会员充值说明文字,比如介绍会员计划"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="默认AI模型" field="default_models">
|
||||
<a-space style="width: 100%">
|
||||
<a-select
|
||||
v-model="system['default_models']"
|
||||
multiple
|
||||
:filterable="true"
|
||||
placeholder="选择AI模型,多选"
|
||||
:options="modelOptions"
|
||||
:loading="modelOptionsLoading"
|
||||
:field-names="{ value: 'value', label: 'name' }"
|
||||
style="width: 100%"
|
||||
>
|
||||
</a-select>
|
||||
<a-tooltip content="新用户注册默认开通的 AI 模型" position="right">
|
||||
<icon-info-circle-fill size="18" />
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="submitting" @click="handleSave">提交</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</template>
|
148
gpt-vue/projects/vue-admin/src/views/System/SystemChatConfig.vue
Normal file
@ -0,0 +1,148 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import useSubmit from "@/composables/useSubmit";
|
||||
import { getConfig, save } from "./api";
|
||||
|
||||
const {
|
||||
formRef,
|
||||
formData: chat,
|
||||
handleSubmit,
|
||||
submitting,
|
||||
} = useSubmit({
|
||||
open_ai: { temperature: 1, max_tokens: 1024 },
|
||||
azure: { temperature: 1, max_tokens: 1024 },
|
||||
chat_gml: { temperature: 0.95, max_tokens: 1024 },
|
||||
baidu: { temperature: 0.95, max_tokens: 1024 },
|
||||
xun_fei: { temperature: 0.5, max_tokens: 1024 },
|
||||
context_deep: 0,
|
||||
enable_context: true,
|
||||
enable_history: true,
|
||||
dall_api_url: "",
|
||||
});
|
||||
|
||||
const rules = {
|
||||
init_chat_calls: [{ required: true, message: "请输入赠送对话次数" }],
|
||||
user_img_calls: [{ required: true, message: "请输入赠送绘图次数" }],
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
await handleSubmit(
|
||||
() =>
|
||||
save({
|
||||
key: "chat",
|
||||
config: chat,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
Message.success("保存成功");
|
||||
};
|
||||
|
||||
const reload = async () => {
|
||||
const { data } = await getConfig({ key: "chat" });
|
||||
data && Object.assign(chat, data);
|
||||
};
|
||||
|
||||
onMounted(reload);
|
||||
</script>
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
<a-form ref="formRef" :model="chat" :rules="rules" auto-label-width :disabled="submitting">
|
||||
<a-form-item label="开启聊天上下文">
|
||||
<a-switch v-model="chat['enable_context']" />
|
||||
</a-form-item>
|
||||
<a-form-item label="保存聊天记录">
|
||||
<a-switch v-model="chat['enable_history']" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="会话上下文深度"
|
||||
extra="会话上下文深度:在老会话中继续会话,默认加载多少条聊天记录作为上下文。如果设置为 0
|
||||
则不加载聊天记录,仅仅使用当前角色的上下文。该配置参数最好设置需要为偶数,否则将无法兼容百度的
|
||||
API。"
|
||||
>
|
||||
<a-input-number v-model="chat['context_deep']" :min="0" :max="10" />
|
||||
</a-form-item>
|
||||
|
||||
<a-divider content-position="center">OpenAI</a-divider>
|
||||
<a-form-item
|
||||
label="模型创意度"
|
||||
extra="值越大 AI 回答越发散,值越小回答越保守,建议保持默认值"
|
||||
>
|
||||
<a-slider v-model="chat['open_ai']['temperature']" :max="2" :step="0.1" />
|
||||
</a-form-item>
|
||||
<a-form-item label="最大响应长度">
|
||||
<a-input
|
||||
v-model.number="chat['open_ai']['max_tokens']"
|
||||
placeholder="回复的最大字数,最大4096"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider content-position="center">Azure</a-divider>
|
||||
<a-form-item
|
||||
label="模型创意度"
|
||||
extra="值越大 AI 回答越发散,值越小回答越保守,建议保持默认值"
|
||||
>
|
||||
<a-slider v-model="chat['azure']['temperature']" :max="2" :step="0.1" />
|
||||
</a-form-item>
|
||||
<a-form-item label="最大响应长度">
|
||||
<a-input
|
||||
v-model.number="chat['azure']['max_tokens']"
|
||||
placeholder="回复的最大字数,最大4096"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider content-position="center">ChatGLM</a-divider>
|
||||
<a-form-item
|
||||
label="模型创意度"
|
||||
extra="值越大 AI 回答越发散,值越小回答越保守,建议保持默认值"
|
||||
>
|
||||
<a-slider v-model="chat['chat_gml']['temperature']" :max="1" :step="0.01" />
|
||||
</a-form-item>
|
||||
<a-form-item label="最大响应长度">
|
||||
<a-input
|
||||
v-model.number="chat['chat_gml']['max_tokens']"
|
||||
placeholder="回复的最大字数,最大4096"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider content-position="center">文心一言</a-divider>
|
||||
<a-form-item
|
||||
label="模型创意度"
|
||||
extra="值越大 AI 回答越发散,值越小回答越保守,建议保持默认值"
|
||||
>
|
||||
<a-slider v-model="chat['baidu']['temperature']" :max="1" :step="0.01" />
|
||||
</a-form-item>
|
||||
<a-form-item label="最大响应长度">
|
||||
<a-input
|
||||
v-model.number="chat['baidu']['max_tokens']"
|
||||
placeholder="回复的最大字数,最大4096"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider content-position="center">讯飞星火</a-divider>
|
||||
<a-form-item
|
||||
label="模型创意度"
|
||||
extra="值越大 AI 回答越发散,值越小回答越保守,建议保持默认值"
|
||||
>
|
||||
<a-slider v-model="chat['xun_fei']['temperature']" :max="1" :step="0.1" />
|
||||
</a-form-item>
|
||||
<a-form-item label="最大响应长度">
|
||||
<a-input
|
||||
v-model.number="chat['xun_fei']['max_tokens']"
|
||||
placeholder="回复的最大字数,最大4096"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-divider content-position="center">AI绘图</a-divider>
|
||||
<a-form-item label="DALL-E3出图数量">
|
||||
<a-input
|
||||
v-model.number="chat['dall_img_num']"
|
||||
placeholder="调用 DALL E3 API 传入的出图数量"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="submitting" @click="handleSave">提交</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</template>
|
@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import SystemBaseConfig from "./SystemBaseConfig.vue";
|
||||
import SystemChatConfig from "./SystemChatConfig.vue";
|
||||
import SystemNoticeConfig from "./SystemNoticeConfig.vue";
|
||||
const tabsList = [
|
||||
{ key: "1", title: "基本设置", components: SystemBaseConfig },
|
||||
{ key: "2", title: "模型设置", components: SystemChatConfig },
|
||||
{ key: "3", title: "公告设置", components: SystemNoticeConfig },
|
||||
];
|
||||
|
||||
const activeKey = ref(tabsList[0].key);
|
||||
</script>
|
||||
<template>
|
||||
<a-tabs v-model:active-key="activeKey" lazy-load justify>
|
||||
<a-tab-pane v-for="item in tabsList" :key="item.key" :title="item.title">
|
||||
<div class="system-config-wrapper">
|
||||
<component :is="item.components" />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
<style scoped>
|
||||
.system-config-wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,67 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import MdEditor from "md-editor-v3";
|
||||
import "md-editor-v3/lib/style.css";
|
||||
import http from "@/http/config";
|
||||
import useSubmit from "@/composables/useSubmit";
|
||||
import { getConfig, save } from "./api";
|
||||
|
||||
const { formRef, formData, handleSubmit, submitting } = useSubmit({
|
||||
content: "",
|
||||
updated: true,
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
await handleSubmit(
|
||||
() =>
|
||||
save({
|
||||
key: "notice",
|
||||
config: formData,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
Message.success("保存成功");
|
||||
};
|
||||
|
||||
const reload = async () => {
|
||||
const { data } = await getConfig({ key: "notice" });
|
||||
data && Object.assign(formData, data);
|
||||
};
|
||||
|
||||
const onUploadImg = (files, callback) => {
|
||||
Promise.all(
|
||||
files.map((file) => {
|
||||
return new Promise((rev, rej) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file, file.name);
|
||||
http({
|
||||
url: `/api/upload`,
|
||||
data: formData,
|
||||
})
|
||||
.then((res) => rev(res))
|
||||
.catch((e) => rej(e));
|
||||
});
|
||||
})
|
||||
)
|
||||
.then((res) => {
|
||||
Message.success({ content: "上传成功", duration: 500 });
|
||||
callback(res.map((item) => item.data.url));
|
||||
})
|
||||
.catch((e) => {
|
||||
Message.error("图片上传失败:" + e.message);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(reload);
|
||||
</script>
|
||||
<template>
|
||||
<a-form ref="formRef" :model="formData" auto-label-width :disabled="submitting">
|
||||
<a-form-item>
|
||||
<md-editor v-model="formData.content" @on-upload-img="onUploadImg" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="submitting" @click="handleSave">提交</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import type { UploadInstance, FileItem } from "@arco-design/web-vue";
|
||||
import { uploadUrl } from "@/http/config";
|
||||
|
||||
defineProps({
|
||||
modelValue: String,
|
||||
placeholder: String,
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
|
||||
const uploadProps = computed<UploadInstance["$props"]>(() => ({
|
||||
action: uploadUrl,
|
||||
name: "file",
|
||||
headers: { [__AUTH_KEY]: localStorage.getItem(__AUTH_KEY) },
|
||||
showFileList: false,
|
||||
}));
|
||||
|
||||
const handleChange = (_, file: FileItem) => {
|
||||
console.log(file.response);
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a-space>
|
||||
<a-input-group>
|
||||
<a-input :model-value="modelValue" :placeholder="placeholder" readonly />
|
||||
<a-upload v-bind="uploadProps" @change="handleChange">
|
||||
<template #upload-button>
|
||||
<a-button type="primary">
|
||||
<icon-cloud />
|
||||
</a-button>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-input-group>
|
||||
</a-space>
|
||||
</template>
|
25
gpt-vue/projects/vue-admin/src/views/System/api.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const getConfig = (params) => {
|
||||
return http({
|
||||
url: "/api/admin/config/get",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const save = (data) => {
|
||||
return http({
|
||||
url: "/api/admin/config/update",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const modelList = (params) => {
|
||||
return http({
|
||||
url: "/api/admin/model/list",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
@ -1,4 +1,88 @@
|
||||
<script lang="ts" setup></script>
|
||||
<script lang="ts" setup>
|
||||
import SearchTable from "@/components/SearchTable/SearchTable.vue";
|
||||
import type { SearchTableColumns } from "@/components/SearchTable/type";
|
||||
import { getList, save as saveApi, deletApi, resetPassword } from "./api";
|
||||
import UserForm from "./UserForm.vue";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import { dateFormat } from "@gpt-vue/packages/utils";
|
||||
import UserPassword from "./UserPassword.vue";
|
||||
import useCustomFormPopup from "@/composables/useCustomFormPopup";
|
||||
const columns: SearchTableColumns[] = [
|
||||
{
|
||||
title: "账号",
|
||||
dataIndex: "username",
|
||||
search: {
|
||||
valueType: "input",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "剩余对话次数",
|
||||
dataIndex: "calls",
|
||||
},
|
||||
{
|
||||
title: "剩余绘图次数",
|
||||
dataIndex: "img_calls",
|
||||
},
|
||||
{
|
||||
title: "累计消耗tokens",
|
||||
dataIndex: "total_tokens",
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
render: ({ record }) => {
|
||||
return record.status ? "正常" : "停用";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "过期时间",
|
||||
dataIndex: "expired_time",
|
||||
render: ({ record }) => {
|
||||
return dateFormat(record.expired_time);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "注册时间",
|
||||
dataIndex: "created_at",
|
||||
render: ({ record }) => {
|
||||
return dateFormat(record.created_at);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
slotName: "actions",
|
||||
},
|
||||
];
|
||||
|
||||
//弹窗
|
||||
const editModal = useCustomFormPopup(UserForm, saveApi, {
|
||||
popupProps: (arg) => ({ title: arg[0].record ? "编辑用户" : "新增用户" }),
|
||||
});
|
||||
const password = useCustomFormPopup(UserPassword, resetPassword, {
|
||||
popupProps: (arg) => ({ title: "重置密码" }),
|
||||
});
|
||||
|
||||
const handleDelete = async ({ id }: { id: string }, reload) => {
|
||||
const res = await deletApi(id);
|
||||
if (res.code === 0) {
|
||||
Message.success("操作成功");
|
||||
reload();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div></div>
|
||||
<SearchTable :request="getList" :columns="columns">
|
||||
<template #actions="{ record, reload }">
|
||||
<a-link @click="editModal({ record, reload })">编辑</a-link>
|
||||
<a-popconfirm content="确定删除?" @ok="handleDelete(record, reload)">
|
||||
<a-link status="danger">删除</a-link>
|
||||
</a-popconfirm>
|
||||
<a-link @click="password({ record, reload })">重置密码</a-link>
|
||||
</template>
|
||||
<template #search-extra="{ reload }">
|
||||
<a-button @click="editModal({ reload })" status="success" size="small"
|
||||
><icon-plus />新增用户</a-button
|
||||
>
|
||||
</template>
|
||||
</SearchTable>
|
||||
</template>
|
||||
|
112
gpt-vue/projects/vue-admin/src/views/User/UserForm.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
|
||||
<a-form-item
|
||||
field="username"
|
||||
label="账号"
|
||||
:rules="[{ required: true, message: '请输入账号' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
>
|
||||
<a-input v-model="form.username" placeholder="请输入账号" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="!props.data.id"
|
||||
field="password"
|
||||
label="密码"
|
||||
:rules="[{ required: true, message: '请输入密码' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
showable
|
||||
>
|
||||
<a-input v-model="form.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="calls"
|
||||
label="对话次数"
|
||||
:rules="[{ required: true, message: '请输入对话次数' }]"
|
||||
>
|
||||
<a-input-number v-model="form.calls" placeholder="请输入对话次数" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="img_calls"
|
||||
label="绘图次数"
|
||||
:rules="[{ required: true, message: '请输入绘图次数' }]"
|
||||
>
|
||||
<a-input-number v-model="form.img_calls" placeholder="请输入绘图次数" />
|
||||
</a-form-item>
|
||||
<a-form-item field="expired_time" label="有效期">
|
||||
<a-date-picker v-model="form.expired_time" placeholder="请选择有效期" />
|
||||
</a-form-item>
|
||||
<a-form-item field="chat_roles" label="聊天角色">
|
||||
<a-select
|
||||
:field-names="{ value: 'key', label: 'name' }"
|
||||
v-model="form.chat_roles"
|
||||
placeholder="请选择聊天角色"
|
||||
multiple
|
||||
:options="roleOption"
|
||||
:rules="[{ required: true, message: '请选择聊天角色' }]"
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item field="chat_models" label="模型角色">
|
||||
<a-select
|
||||
:field-names="{ value: 'value', label: 'name' }"
|
||||
v-model="form.chat_models"
|
||||
placeholder="请选择模型角色"
|
||||
multiple
|
||||
:options="modalOption"
|
||||
:rules="[{ required: true, message: '请选择模型角色' }]"
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item field="status" label="启用状态">
|
||||
<a-switch v-model="form.status" />
|
||||
</a-form-item>
|
||||
<a-form-item field="vip" label="开通VIP">
|
||||
<a-switch v-model="form.vip" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineExpose, defineProps } from "vue";
|
||||
import { getModel, getRole } from "./api";
|
||||
const props = defineProps({
|
||||
data: {},
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const form = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
calls: "",
|
||||
img_calls: "",
|
||||
expired_time: "",
|
||||
chat_roles: [],
|
||||
chat_models: [],
|
||||
status: false,
|
||||
vip: false,
|
||||
});
|
||||
if (props.data?.id) {
|
||||
form.value = Object.assign({}, props.data);
|
||||
if (form.value.expired_time === 0) {
|
||||
form.value.expired_time = "";
|
||||
}
|
||||
}
|
||||
|
||||
//拿选项
|
||||
const modalOption = ref([]);
|
||||
const roleOption = ref([]);
|
||||
const getOption = (api, container) => {
|
||||
api().then(({ code, data }) => {
|
||||
if (code === 0) {
|
||||
container.value = data;
|
||||
}
|
||||
});
|
||||
};
|
||||
getOption(getModel, modalOption);
|
||||
getOption(getRole, roleOption);
|
||||
|
||||
defineExpose({
|
||||
formRef,
|
||||
form,
|
||||
});
|
||||
</script>
|
44
gpt-vue/projects/vue-admin/src/views/User/UserPassword.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
|
||||
<a-form-item
|
||||
field="username"
|
||||
label="账号"
|
||||
:rules="[{ required: true, message: 'name is required' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
:disabled="true"
|
||||
>
|
||||
<a-input v-model="form.username" placeholder="请输入账号" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="password"
|
||||
label="新密码"
|
||||
:rules="[{ required: true, message: 'password is required' }]"
|
||||
:validate-trigger="['change', 'input']"
|
||||
showable
|
||||
>
|
||||
<a-input v-model="form.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineExpose, defineProps } from "vue";
|
||||
import { getModel, getRole } from "./api";
|
||||
const props = defineProps({
|
||||
data: {},
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const form = ref({
|
||||
id: "",
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
form.value.id = props.data.id;
|
||||
form.value.username = props.data.username;
|
||||
|
||||
defineExpose({
|
||||
formRef,
|
||||
form,
|
||||
});
|
||||
</script>
|
46
gpt-vue/projects/vue-admin/src/views/User/api.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const getList = (params?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/user/list",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
export const save = (data?: Record<string, unknown>) => {
|
||||
return http({
|
||||
url: "/api/admin/user/save",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const deletApi = (id: string | number) => {
|
||||
return http({
|
||||
url: `/api/admin/user/remove?id=${id}`,
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
export const getRole = () => {
|
||||
return http({
|
||||
url: `/api/admin/role/list`,
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
export const getModel = () => {
|
||||
return http({
|
||||
url: `/api/admin/model/list`,
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
export const resetPassword = (data) => {
|
||||
return http({
|
||||
url: `/api/admin/user/resetPass`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|