remove new-ui files
@ -131,10 +131,13 @@ const XunFei = Platform("XunFei")
|
||||
const QWen = Platform("QWen")
|
||||
|
||||
type SystemConfig struct {
|
||||
Title string `json:"title"`
|
||||
AdminTitle string `json:"admin_title"`
|
||||
Logo string `json:"logo"`
|
||||
InitPower int `json:"init_power"` // 新用户注册赠送算力值
|
||||
Title string `json:"title"`
|
||||
AdminTitle string `json:"admin_title"`
|
||||
Logo string `json:"logo"`
|
||||
InitPower int `json:"init_power"` // 新用户注册赠送算力值
|
||||
DailyPower int `json:"daily_power"` // 每日赠送算力
|
||||
InvitePower int `json:"invite_power"` // 邀请新用户赠送算力值
|
||||
VipMonthPower int `json:"vip_month_power"` // VIP 会员每月赠送的算力值
|
||||
|
||||
RegisterWays []string `json:"register_ways"` // 注册方式:支持手机,邮箱注册
|
||||
EnabledRegister bool `json:"enabled_register"` // 是否开放注册
|
||||
@ -143,11 +146,8 @@ type SystemConfig struct {
|
||||
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
|
||||
PowerPrice float64 `json:"power_price"` // 算力单价
|
||||
|
||||
OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间
|
||||
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
|
||||
OrderPayInfoText string `json:"order_pay_info_text"` // 订单支付页面说明文字
|
||||
InvitePower int `json:"invite_power"` // 邀请新用户赠送算力值
|
||||
VipMonthPower int `json:"vip_month_power"` // VIP 会员每月赠送的算力值
|
||||
OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间
|
||||
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
|
||||
|
||||
MjPower int `json:"mj_power"` // MJ 绘画消耗算力
|
||||
SdPower int `json:"sd_power"` // SD 绘画消耗算力
|
||||
|
@ -5,8 +5,6 @@ import (
|
||||
"chatplus/handler"
|
||||
"chatplus/service/oss"
|
||||
"chatplus/store/model"
|
||||
"chatplus/store/vo"
|
||||
"chatplus/utils"
|
||||
"chatplus/utils/resp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@ -48,50 +46,3 @@ func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
|
||||
resp.SUCCESS(c, file)
|
||||
}
|
||||
|
||||
func (h *UploadHandler) List(c *gin.Context) {
|
||||
userId := 0
|
||||
var items []model.File
|
||||
var files = make([]vo.File, 0)
|
||||
h.db.Where("user_id = ?", userId).Find(&items)
|
||||
if len(items) > 0 {
|
||||
for _, v := range items {
|
||||
var file vo.File
|
||||
err := utils.CopyObject(v, &file)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
file.CreatedAt = v.CreatedAt.Unix()
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, files)
|
||||
}
|
||||
|
||||
// Remove remove files
|
||||
func (h *UploadHandler) Remove(c *gin.Context) {
|
||||
userId := 0
|
||||
id := h.GetInt(c, "id", 0)
|
||||
var file model.File
|
||||
tx := h.db.Where("user_id = ? AND id = ?", userId, id).First(&file)
|
||||
if tx.Error != nil || file.Id == 0 {
|
||||
resp.ERROR(c, "file not existed")
|
||||
return
|
||||
}
|
||||
|
||||
// remove database
|
||||
tx = h.db.Model(&model.File{}).Delete("id = ?", id)
|
||||
if tx.Error != nil || tx.RowsAffected == 0 {
|
||||
resp.ERROR(c, "failed to update database")
|
||||
return
|
||||
}
|
||||
// remove files
|
||||
objectKey := file.ObjKey
|
||||
if objectKey == "" {
|
||||
objectKey = file.URL
|
||||
}
|
||||
_ = h.uploaderManager.GetUploadHandler().Delete(objectKey)
|
||||
resp.SUCCESS(c)
|
||||
}
|
||||
|
@ -384,8 +384,6 @@ func main() {
|
||||
fx.Provide(admin.NewUploadHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) {
|
||||
s.Engine.POST("/api/admin/upload", h.Upload)
|
||||
s.Engine.GET("/api/admin/upload/list", h.List)
|
||||
s.Engine.GET("/api/admin/upload/remove", h.Remove)
|
||||
}),
|
||||
|
||||
// 系统管理员
|
||||
|
@ -1,2 +0,0 @@
|
||||
dist
|
||||
node_modules
|
@ -1,27 +0,0 @@
|
||||
/* 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",
|
||||
},
|
||||
};
|
21
new-ui/.gitignore
vendored
@ -1,21 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# ChatGPT-Plus
|
||||
|
||||
重构UI
|
@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "chatgpt-plus",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"clear": "rimraf node_modules rimraf -g */node_modules rimraf -g */*/node_modules",
|
||||
"dev": "pnpm --filter=@chatgpt-plus-projects/* run dev",
|
||||
"build": "pnpm --filter=@chatgpt-plus-projects/* run build"
|
||||
},
|
||||
"keywords": [],
|
||||
"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"
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "@chatgpt-plus/packages",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.7",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.8"
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import axios from "axios";
|
||||
import tokenHandler from "./token";
|
||||
|
||||
const { _tokenData, refreshToken, setCurRequest } = tokenHandler();
|
||||
|
||||
const createInstance = (baseURL: string) => {
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
timeout: 10000,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
if (config.url !== _tokenData.get("lastRequest")) {
|
||||
refreshToken();
|
||||
}
|
||||
if (config.method === "post") {
|
||||
setCurRequest(config.url);
|
||||
config.headers["request-id"] = _tokenData.get("__token");
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
export default createInstance;
|
@ -1,13 +0,0 @@
|
||||
import { getUUID } from "../utils";
|
||||
|
||||
const _tokenData = new Map();
|
||||
export default function tokenHandler() {
|
||||
const refreshToken = () => {
|
||||
_tokenData.set("__token", getUUID());
|
||||
_tokenData.set("lastRequest", null);
|
||||
};
|
||||
const setCurRequest = (curRequest?: string) => {
|
||||
_tokenData.set("lastRequest", curRequest);
|
||||
};
|
||||
return { _tokenData, refreshToken, setCurRequest };
|
||||
}
|
14
new-ui/packages/type.d.ts
vendored
@ -1,14 +0,0 @@
|
||||
export interface BaseResponse<T> {
|
||||
code: number;
|
||||
data?: T;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ListResponse<T = Record<string, unknown>> {
|
||||
items: T[];
|
||||
page: number;
|
||||
page_size: number;
|
||||
total: number;
|
||||
total_page: number
|
||||
}
|
||||
|
@ -1,57 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
packages:
|
||||
- 'packages/**'
|
||||
- 'projects/**'
|
@ -1,3 +0,0 @@
|
||||
VITE_PROXY_BASE_URL="/api"
|
||||
VITE_TARGET_URL="http://localhost:5678"
|
||||
VITE_SOCKET_IO_URL="http://localhost:8899"
|
@ -1,2 +0,0 @@
|
||||
VITE_PROXY_BASE_URL="/api"
|
||||
VITE_TARGET_URL="http://172.22.11.2:5678"
|
@ -1,2 +0,0 @@
|
||||
VITE_PROXY_BASE_URL=""
|
||||
VITE_TARGET_URL="/"
|
@ -1,28 +0,0 @@
|
||||
/* 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",
|
||||
"@vue/eslint-config-prettier",
|
||||
],
|
||||
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",
|
||||
},
|
||||
};
|
30
new-ui/projects/admin/.gitignore
vendored
@ -1,30 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
# vue-admin
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
pnpm lint
|
||||
```
|
9
new-ui/projects/admin/env.d.ts
vendored
@ -1,9 +0,0 @@
|
||||
/// <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;
|
@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>极客AI助手-控制台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "@chatgpt-plus-projects/admin",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview --mode preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.54.6",
|
||||
"@chatgpt-plus/packages": "workspace:^1.0.0",
|
||||
"echarts": "^5.5.0",
|
||||
"md-editor-v3": "^2.2.1",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@tsconfig/node20": "^20.1.2",
|
||||
"@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",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"less": "^4.2.0",
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"typescript": "~5.3.0",
|
||||
"vite": "^5.0.11",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 31 KiB |
@ -1,21 +0,0 @@
|
||||
<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;
|
||||
}
|
||||
</style>
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
Before Width: | Height: | Size: 276 B |
@ -1,39 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { Message, type SwitchInstance } from "@arco-design/web-vue";
|
||||
import type { BaseResponse } from "@chatgpt-plus/packages/type";
|
||||
|
||||
type OriginProps = SwitchInstance["$props"];
|
||||
|
||||
interface Props extends /* @vue-ignore */ OriginProps {
|
||||
modelValue: boolean | string | number;
|
||||
api: (params?: any) => Promise<BaseResponse<any>>;
|
||||
onSuccess?: (res?: any) => void;
|
||||
}
|
||||
|
||||
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 {
|
||||
const res = await props.api({ ...params, value: !_value.value });
|
||||
Message.success("操作成功");
|
||||
props?.onSuccess?.(res);
|
||||
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,153 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import {IconDown, IconExport} from "@arco-design/web-vue/es/icon";
|
||||
import {useAuthStore} from "@/stores/auth";
|
||||
import useState from "@/composables/useState";
|
||||
import Logo from "/images/logo.png";
|
||||
import avatar from "/images/user-info.jpg";
|
||||
import donateImg from "/images/wechat-pay.png";
|
||||
|
||||
import SystemMenu from "./SystemMenu.vue";
|
||||
import PageWrapper from "./PageWrapper.vue";
|
||||
import {getConfig} from "@/views/System/api";
|
||||
import {onMounted, ref} from "vue";
|
||||
|
||||
const logoWidth = "200px";
|
||||
const authStore = useAuthStore();
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const system = ref({})
|
||||
const reload = async () => {
|
||||
system.value = (await getConfig({key: "system"})).data;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ALayout class="custom-layout">
|
||||
<ALayoutHeader class="custom-layout-header">
|
||||
<div class="logo">
|
||||
<img :src="Logo" alt="logo"/>
|
||||
<span>{{ system?.admin_title }}</span>
|
||||
</div>
|
||||
<div class="action">
|
||||
<ADropdown>
|
||||
<ASpace align="center" :size="4">
|
||||
<a-avatar class="user-avatar" :size="30">
|
||||
<img :src="avatar"/>
|
||||
</a-avatar>
|
||||
<IconDown/>
|
||||
</ASpace>
|
||||
<template #content>
|
||||
<a
|
||||
class="dropdown-link"
|
||||
href="https://github.com/yangjian102621/chatgpt-plus"
|
||||
target="_blank"
|
||||
>
|
||||
<ADoption value="1">
|
||||
<template #icon>
|
||||
<icon-github/>
|
||||
</template>
|
||||
<span>{{ system?.title }}</span>
|
||||
</ADoption>
|
||||
</a>
|
||||
<ADoption value="2" @click="setVisible(true)">
|
||||
<template #icon>
|
||||
<icon-wechatpay/>
|
||||
</template>
|
||||
<span>打赏作者</span>
|
||||
</ADoption>
|
||||
</template>
|
||||
<template #footer>
|
||||
<APopconfirm content="确认退出?" position="bl" @ok="authStore.logout">
|
||||
<AButton status="warning" class="logout-area">
|
||||
<ASpace align="center">
|
||||
<IconExport size="16"/>
|
||||
<span>退出登录</span>
|
||||
</ASpace>
|
||||
</AButton>
|
||||
</APopconfirm>
|
||||
</template>
|
||||
</ADropdown>
|
||||
</div>
|
||||
</ALayoutHeader>
|
||||
<ALayout>
|
||||
<SystemMenu :width="logoWidth"/>
|
||||
<ALayoutContent>
|
||||
<PageWrapper>
|
||||
<slot/>
|
||||
</PageWrapper>
|
||||
</ALayoutContent>
|
||||
</ALayout>
|
||||
</ALayout>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
class="donate-dialog"
|
||||
width="400px"
|
||||
title="请作者喝杯咖啡"
|
||||
:footer="false"
|
||||
>
|
||||
<a-alert :closable="false" :show-icon="false">
|
||||
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
|
||||
</a-alert>
|
||||
<p>
|
||||
<a-image :src="donateImg"/>
|
||||
</p>
|
||||
</a-modal>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.custom-layout {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-neutral-2);
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
width: v-bind("logoWidth");
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
padding: 0 12px;
|
||||
flex: 1;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.donate-dialog {
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.logout-area {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
min-width: 80px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
@ -1,49 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { Message } from "@arco-design/web-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"]>(() => {
|
||||
const TOKEN = JSON.parse(localStorage.getItem(__AUTH_KEY))?.token;
|
||||
return {
|
||||
accept: "image/*",
|
||||
action: uploadUrl,
|
||||
name: "file",
|
||||
headers: { [__AUTH_KEY]: TOKEN },
|
||||
showFileList: false,
|
||||
};
|
||||
});
|
||||
|
||||
const handleChange = (_, file: FileItem) => {
|
||||
if (file?.response) {
|
||||
emits("update:modelValue", file?.response?.data?.url);
|
||||
Message.success("上传成功");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a-upload v-bind="uploadProps" style="width: 100%" @change="handleChange">
|
||||
<template #upload-button>
|
||||
<a-input-group style="width: 100%">
|
||||
<a-input
|
||||
:model-value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
readonly
|
||||
allow-clear
|
||||
@clear.stop="emits('update:modelValue')"
|
||||
/>
|
||||
<a-button type="primary" style="width: 100px">
|
||||
<icon-cloud />
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</template>
|
||||
</a-upload>
|
||||
</template>
|
@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<div class="page-wrapper">
|
||||
<div class="page-content">
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.page-wrapper {
|
||||
height: calc(100vh - 60px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f7f8fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
.page-content {
|
||||
margin: 12px;
|
||||
padding: 12px;
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
@ -1,10 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { hasPermission } from "@/directives/permission";
|
||||
defineProps<{
|
||||
permission: string | string[] | true;
|
||||
}>();
|
||||
</script>
|
||||
<template>
|
||||
<slot v-if="hasPermission(permission)" />
|
||||
<slot v-else name="none" />
|
||||
</template>
|
@ -1,113 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, type PropType } from "vue";
|
||||
import { getDefaultFormData, useComponentConfig } from "./utils";
|
||||
import { ValueType } from "./type.d";
|
||||
import type { SearchTableColumns, SearchColumns } from "./type";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
columns: {
|
||||
type: Array as PropType<SearchTableColumns[]>,
|
||||
default: () => [],
|
||||
},
|
||||
submitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue", "request"]);
|
||||
|
||||
const size = "small";
|
||||
|
||||
const collapsed = ref(false);
|
||||
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set(value) {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
|
||||
const searchColumns = computed(() => {
|
||||
return props.columns?.filter((item) => item.dataIndex && item.search) as (SearchColumns & {
|
||||
dataIndex: string;
|
||||
})[];
|
||||
});
|
||||
|
||||
const optionsEvent = {
|
||||
onReset: () => {
|
||||
formData.value = getDefaultFormData(props.columns);
|
||||
emits("request");
|
||||
},
|
||||
onSearch: () => emits("request"),
|
||||
onCollapse: (value: boolean) => {
|
||||
collapsed.value = value ?? !collapsed.value;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<AForm
|
||||
v-if="searchColumns?.length"
|
||||
class="search-form-conteiner"
|
||||
:model="formData"
|
||||
:size="size"
|
||||
:label-col-props="{ span: 0 }"
|
||||
:wrapper-col-props="{ span: 24 }"
|
||||
@submit="optionsEvent.onSearch"
|
||||
>
|
||||
<AGrid
|
||||
:cols="{ md: 1, lg: 3, xl: 4, xxl: 5 }"
|
||||
:row-gap="12"
|
||||
:col-gap="12"
|
||||
:collapsed="collapsed"
|
||||
>
|
||||
<AGridItem
|
||||
v-for="item in searchColumns"
|
||||
:key="item.dataIndex"
|
||||
style="transition: all 0.3s ease-in-out"
|
||||
>
|
||||
<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"
|
||||
v-bind="useComponentConfig(size, item)"
|
||||
/>
|
||||
</slot>
|
||||
</AFormItem>
|
||||
</AGridItem>
|
||||
<AGridItem>
|
||||
<ASpace>
|
||||
<slot name="search-options" :option="optionsEvent">
|
||||
<AButton type="primary" html-type="submit" :size="size" :loading="submitting">
|
||||
<icon-search />
|
||||
<span>查询</span>
|
||||
</AButton>
|
||||
<AButton :size="size" @click="optionsEvent.onReset" :loading="submitting">
|
||||
<icon-refresh />
|
||||
<span>重置</span>
|
||||
</AButton>
|
||||
</slot>
|
||||
</ASpace>
|
||||
</AGridItem>
|
||||
<AGridItem suffix>
|
||||
<ASpace class="flex-end">
|
||||
<slot name="search-extra" />
|
||||
</ASpace>
|
||||
</AGridItem>
|
||||
</AGrid>
|
||||
</AForm>
|
||||
</template>
|
||||
<style scoped>
|
||||
.search-form-conteiner {
|
||||
padding: 8px 0px 0px;
|
||||
}
|
||||
.flex-end {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
</style>
|
@ -1,95 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onActivated } from "vue";
|
||||
import useAsyncTable from "./useAsyncTable";
|
||||
import FormSection from "./FormSection.vue";
|
||||
import type { SearchTableProps } from "./type";
|
||||
import { useTableScroll, getDefaultFormData, useRequestParams } from "./utils";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
|
||||
const props = defineProps<SearchTableProps>();
|
||||
const formData = ref({ ...getDefaultFormData(props.columns) });
|
||||
const tableContainerRef = ref<HTMLElement>();
|
||||
|
||||
// 表格请求参数
|
||||
const requestParams = computed(() => ({
|
||||
...useRequestParams(props.columns, formData.value),
|
||||
...props.params,
|
||||
}));
|
||||
|
||||
const [tableConfig, getList] = useAsyncTable(props.request, requestParams);
|
||||
|
||||
const _columns = computed(() => {
|
||||
return props.columns
|
||||
.filter((item) => !item.hideInTable)
|
||||
.map((item) => ({
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
...item,
|
||||
}));
|
||||
});
|
||||
|
||||
const handleSearch = async (tips?: boolean) => {
|
||||
tips && Message.success("操作成功");
|
||||
await getList();
|
||||
};
|
||||
|
||||
onActivated(handleSearch);
|
||||
</script>
|
||||
<template>
|
||||
<div class="search-table">
|
||||
<div class="search-table-header">
|
||||
<div class="search-table-header-option">
|
||||
<div>
|
||||
<slot name="header-title">{{ props.headerTitle }}</slot>
|
||||
</div>
|
||||
<div class="header-option">
|
||||
<slot name="header-option" :formData="formData" :reload="handleSearch" />
|
||||
</div>
|
||||
</div>
|
||||
<FormSection
|
||||
v-model="formData"
|
||||
: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>
|
||||
<div ref="tableContainerRef" class="search-table-container">
|
||||
<ATable
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
...tableConfig,
|
||||
...props,
|
||||
scroll: useTableScroll(_columns),
|
||||
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>
|
||||
.search-table {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
.search-table-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.search-table-header {
|
||||
background: #fff;
|
||||
z-index: 2;
|
||||
}
|
||||
.search-table-header-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
@ -1,49 +0,0 @@
|
||||
import type { Component } from "vue";
|
||||
import type { JsxElement } from "typescript";
|
||||
import {
|
||||
DatePicker,
|
||||
Input,
|
||||
InputNumber,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Select,
|
||||
Switch,
|
||||
type TableColumnData,
|
||||
} from "@arco-design/web-vue";
|
||||
import type { TableOriginalProps, TableRequest } from "./useAsyncTable";
|
||||
|
||||
type Object = Record<string, unknown>;
|
||||
|
||||
export enum ValueType {
|
||||
"input" = Input,
|
||||
"select" = Select,
|
||||
"number" = InputNumber,
|
||||
"date" = DatePicker,
|
||||
"range" = RangePicker,
|
||||
"radio" = RadioGroup,
|
||||
"switch" = Switch,
|
||||
}
|
||||
|
||||
export type SearchConfig = {
|
||||
valueType?: keyof typeof ValueType;
|
||||
fieldProps?: Object;
|
||||
render?: Component | JsxElement;
|
||||
slotsName?: string;
|
||||
defaultValue?: any;
|
||||
transform?: (value) => Record<string, any>;
|
||||
};
|
||||
|
||||
export interface SearchTableColumns extends TableColumnData {
|
||||
search?: SearchConfig;
|
||||
hideInTable?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type SearchColumns = SearchTableColumns & { search: SearchConfig };
|
||||
|
||||
export interface SearchTableProps extends /* @vue-ignore */ TableOriginalProps {
|
||||
request: TableRequest<Object>;
|
||||
params?: Object;
|
||||
columns: SearchTableColumns[];
|
||||
headerTitle?: string;
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
import { computed, onMounted, reactive, unref, type Ref } from "vue";
|
||||
import type { TableInstance } from "@arco-design/web-vue";
|
||||
import type { BaseResponse, ListResponse } from "@chatgpt-plus/packages/type";
|
||||
|
||||
export type TableOriginalProps = TableInstance["$props"];
|
||||
export type TableRequest<T extends Record<string, unknown>> = (params?: any) => Promise<BaseResponse<ListResponse<T>>>
|
||||
export type TableReturn = [TableOriginalProps, () => Promise<void>];
|
||||
function useAsyncTable<T extends Record<string, unknown>>(
|
||||
request: TableRequest<T>,
|
||||
params?: Ref<Record<string, unknown>>
|
||||
): TableReturn {
|
||||
const paginationState = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const tableState = reactive({
|
||||
loading: false,
|
||||
data: []
|
||||
})
|
||||
|
||||
const tableConfig = computed<TableOriginalProps>(() => {
|
||||
return {
|
||||
...tableState,
|
||||
rowKey: "id",
|
||||
pagination: {
|
||||
...paginationState,
|
||||
showTotal: true,
|
||||
showPageSize: true,
|
||||
},
|
||||
onPageChange: (page) => {
|
||||
paginationState.current = page;
|
||||
getTableData();
|
||||
},
|
||||
onPageSizeChange(pageSize) {
|
||||
paginationState.pageSize = pageSize;
|
||||
getTableData();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const getTableData = async () => {
|
||||
tableState.loading = true
|
||||
try {
|
||||
const { data } = await request({
|
||||
...unref(params ?? {}),
|
||||
page: paginationState.current,
|
||||
page_size: paginationState.pageSize,
|
||||
});
|
||||
tableState.data = (data as any)?.items;
|
||||
paginationState.total = (data as any)?.total;
|
||||
} finally {
|
||||
tableState.loading = false
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(getTableData);
|
||||
|
||||
return [tableConfig, getTableData] as TableReturn;
|
||||
}
|
||||
|
||||
export default useAsyncTable;
|
@ -1,44 +0,0 @@
|
||||
import type { SearchTableColumns, SearchColumns } from "./type";
|
||||
|
||||
export function useTableScroll(columns: SearchTableColumns[]) {
|
||||
const x = columns.reduce((prev, curr) => {
|
||||
const width = curr.hideInTable ? 0 : curr.width ?? 150;
|
||||
return prev + width;
|
||||
}, 0);
|
||||
return { x };
|
||||
}
|
||||
|
||||
export function getDefaultFormData(columns: SearchTableColumns[]) {
|
||||
return columns?.reduce((field, curr) => {
|
||||
if (curr.dataIndex && curr?.search?.defaultValue) {
|
||||
field[curr.dataIndex] = curr.search.defaultValue;
|
||||
}
|
||||
return field;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function useRequestParams(
|
||||
columns: SearchTableColumns[],
|
||||
originFormData: Record<string, any>
|
||||
) {
|
||||
const filterFormData = columns?.reduce((prev, curr) => {
|
||||
if (!curr.dataIndex || !curr.search) {
|
||||
return prev;
|
||||
}
|
||||
if (curr?.search?.transform) {
|
||||
const filters = curr.search.transform(originFormData[curr.dataIndex]);
|
||||
return Object.assign(prev, filters);
|
||||
}
|
||||
return Object.assign(prev, { [curr.dataIndex]: originFormData[curr.dataIndex] });
|
||||
}, {});
|
||||
return filterFormData as Record<string, any>;
|
||||
}
|
||||
|
||||
export function useComponentConfig(size: string, item: SearchColumns) {
|
||||
return {
|
||||
size,
|
||||
placeholder: item.search.valueType === "range" ? ["开始时间", "结束时间"] : item.title,
|
||||
allowClear: true,
|
||||
...(item.search.fieldProps ?? {}),
|
||||
};
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onActivated } from "vue";
|
||||
import useAsyncTable from "./useAsyncTable";
|
||||
import { useTableScroll } from "@/components/SearchTable/utils";
|
||||
import { Message, type TableColumnData } 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?: TableColumnData[];
|
||||
}
|
||||
|
||||
const props = defineProps<SimpleTable>();
|
||||
|
||||
// 表格请求参数
|
||||
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-table">
|
||||
<div class="simple-header">
|
||||
<a-space class="flex-end">
|
||||
<slot name="header" v-bind="{ reload: handleSearch }" />
|
||||
</a-space>
|
||||
</div>
|
||||
<div ref="tableContainerRef" class="simple-table-container">
|
||||
<ATable
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
...tableConfig,
|
||||
...props,
|
||||
scroll: useTableScroll(_columns || []),
|
||||
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: 8px 0px 16px;
|
||||
}
|
||||
.flex-end {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
</style>
|
@ -1,43 +0,0 @@
|
||||
import { computed, onMounted, reactive, unref } from "vue";
|
||||
import type { TableInstance } from "@arco-design/web-vue";
|
||||
import type { BaseResponse } from "@chatgpt-plus/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;
|
@ -1,64 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, h } from "vue";
|
||||
import type { Component, PropType } from "vue";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import { SubMenu, MenuItem } from "@arco-design/web-vue";
|
||||
const CustomMenuItem: Component = defineComponent({
|
||||
props: {
|
||||
tree: {
|
||||
type: Array as PropType<RouteRecordRaw[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup: (props) => {
|
||||
return () =>
|
||||
props.tree?.map((item) => {
|
||||
const _icon = item.meta?.icon ? h(item.meta.icon) : undefined;
|
||||
const hasChildren = Array.isArray(item.children) && item.children.length;
|
||||
if (hasChildren) {
|
||||
return h(
|
||||
SubMenu,
|
||||
{ title: item.meta.title, key: item.name },
|
||||
{
|
||||
default: () => h(CustomMenuItem, { tree: item.children }),
|
||||
icon: () => _icon,
|
||||
}
|
||||
);
|
||||
}
|
||||
return h(
|
||||
MenuItem,
|
||||
{ key: item.name },
|
||||
{ default: () => item.meta.title, icon: () => _icon }
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import router from "@/router";
|
||||
import menu from "@/router/menu";
|
||||
import { hasPermission } from "@/directives/permission";
|
||||
defineProps({
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: 200,
|
||||
},
|
||||
});
|
||||
const route = useRoute();
|
||||
const goto = (name: string) => router.push({ name });
|
||||
|
||||
const selectedKeys = computed(() => [route.name]);
|
||||
|
||||
const showMenu = computed(() => menu.filter((item: any) => hasPermission(item.meta?.permission)));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ALayoutSider :style="{ width, height: '100%' }">
|
||||
<AMenu :selectedKeys="selectedKeys" auto-open-selected @menu-item-click="goto">
|
||||
<CustomMenuItem :tree="showMenu" />
|
||||
</AMenu>
|
||||
</ALayoutSider>
|
||||
</template>
|
@ -1,44 +0,0 @@
|
||||
import usePopup, { type Config } from "./usePopup";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import type { Component } from "vue";
|
||||
import type { BaseResponse } from "@chatgpt-plus/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 });
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
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;
|
@ -1,26 +0,0 @@
|
||||
import { ref } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
import type { BaseResponse } from "@chatgpt-plus/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
|
@ -1,10 +0,0 @@
|
||||
import { ref, type Ref } from "vue";
|
||||
function useState<T>(defaultValue?: T): [Ref<T>, (newValue: T) => void] {
|
||||
const state = ref<T>(defaultValue) as Ref<T>;
|
||||
const setState = (newValue: T) => {
|
||||
state.value = newValue;
|
||||
};
|
||||
return [state, setState];
|
||||
}
|
||||
|
||||
export default useState;
|
@ -1,32 +0,0 @@
|
||||
import { ref, reactive, unref } from "vue";
|
||||
import type { BaseResponse } from "@chatgpt-plus/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) {
|
||||
return Promise.reject({ validateErrors: hasError });
|
||||
}
|
||||
|
||||
const { data, code, message } = await api({ ...formData ?? {}, ...unref(params) });
|
||||
if (code) {
|
||||
return Promise.reject({ requestErrors: message })
|
||||
}
|
||||
|
||||
return Promise.resolve({ formData, data });
|
||||
} catch (err) {
|
||||
return Promise.reject({ errors: err });
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return { formRef, formData, handleSubmit, submitting };
|
||||
}
|
||||
|
||||
export default useSubmit;
|
@ -1,32 +0,0 @@
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
// 判断操作权限
|
||||
export function hasPermission(permissionTag: string | string[] | boolean) {
|
||||
const authStore = useAuthStore();
|
||||
const { is_super_admin, permissions = [] } = authStore;
|
||||
if (is_super_admin) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(permissionTag)) {
|
||||
return permissionTag.every((tag) => permissions.includes(tag));
|
||||
}
|
||||
if (typeof permissionTag === "string") {
|
||||
return permissions.includes(permissionTag);
|
||||
}
|
||||
return permissionTag;
|
||||
}
|
||||
|
||||
function checkPermission(el, binding) {
|
||||
if (!hasPermission(binding.value)) {
|
||||
el.parentNode && el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
export const permission = {
|
||||
mounted(el, binding) {
|
||||
checkPermission(el, binding);
|
||||
},
|
||||
updated(el, binding) {
|
||||
checkPermission(el, binding);
|
||||
},
|
||||
};
|
@ -1,72 +0,0 @@
|
||||
import router from "@/router";
|
||||
import { Notification } from "@arco-design/web-vue";
|
||||
import createInstance from "@chatgpt-plus/packages/request"
|
||||
import type { BaseResponse } from "@chatgpt-plus/packages/type";
|
||||
|
||||
export const uploadUrl = import.meta.env.VITE_PROXY_BASE_URL + "/api/admin/upload";
|
||||
|
||||
export const instance = createInstance(import.meta.env.VITE_PROXY_BASE_URL)
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
const TOKEN = JSON.parse(localStorage.getItem(__AUTH_KEY))?.token
|
||||
config.headers[__AUTH_KEY] = TOKEN;
|
||||
config.headers["Authorization"] = TOKEN;
|
||||
return config;
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
const { data }: { data: BaseResponse<unknown> } = response
|
||||
if (data && typeof data === "object" && data.code > 0) {
|
||||
switch (data.code) {
|
||||
case 400: {
|
||||
localStorage.removeItem(__AUTH_KEY);
|
||||
router.push({ name: "Login" })
|
||||
break;
|
||||
}
|
||||
case 403: {
|
||||
router.replace({ name: "403" })
|
||||
break;
|
||||
}
|
||||
}
|
||||
Notification.error(data.message ?? '未知错误')
|
||||
}
|
||||
return { data, response } as any;
|
||||
},
|
||||
(error) => {
|
||||
const STATUS_CODE: any = {
|
||||
401: {
|
||||
msg: error.response.data || "没有操作权限!",
|
||||
event: null,
|
||||
},
|
||||
500: {
|
||||
msg: error.response.data || "系统正在部署升级中,请稍后再试!",
|
||||
event: null,
|
||||
},
|
||||
};
|
||||
|
||||
const statusCodeEvent = STATUS_CODE?.[error.response.status];
|
||||
|
||||
if (statusCodeEvent) {
|
||||
Notification.error(statusCodeEvent.msg);
|
||||
statusCodeEvent.event?.();
|
||||
}
|
||||
|
||||
if (error.message.indexOf("timeout") !== -1) {
|
||||
Notification.error("连接超时");
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
function http<T = any>(config: any): Promise<BaseResponse<T>> {
|
||||
return instance(config).then((res) => {
|
||||
return res.data;
|
||||
}) as unknown as Promise<BaseResponse<T>>;
|
||||
}
|
||||
|
||||
export function originHttp<T = any>(config: any) {
|
||||
return instance<T>(config as any).then((res) => res);
|
||||
}
|
||||
|
||||
export default http;
|
@ -1,39 +0,0 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const userLogin = (data) => {
|
||||
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) => {
|
||||
return http({
|
||||
url: "/api/admin/user/loginLog",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const captcha = () => {
|
||||
return http({
|
||||
url: "/api/admin/login/captcha",
|
||||
method: "get",
|
||||
});
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import ArcoVue from "@arco-design/web-vue";
|
||||
import ArcoVueIcon from "@arco-design/web-vue/es/icon";
|
||||
import "@arco-design/web-vue/dist/arco.css";
|
||||
import PermissionRender from "@/components/PermissionRender.vue";
|
||||
import { permission } from "@/directives/permission";
|
||||
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.use(ArcoVue);
|
||||
app.use(ArcoVueIcon);
|
||||
|
||||
app.component("PermissionRender", PermissionRender);
|
||||
app.directive("permission", permission);
|
||||
|
||||
app.mount("#app");
|
||||
app.config.warnHandler = (msg, vm, trace) => {
|
||||
if (msg.includes('Invalid prop name: "key" is a reserved property.')) {
|
||||
// 如果警告信息包含我们要屏蔽的内容,则不执行任何操作
|
||||
return;
|
||||
}
|
||||
console.warn(`[Vue warn]: ${msg}${trace}`);
|
||||
};
|
||||
|
||||
export default app;
|
@ -1,65 +0,0 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import CustomLayout from '@/components/CustomLayout.vue'
|
||||
import { hasPermission } from "@/directives/permission";
|
||||
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: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: CustomLayout,
|
||||
redirect: () => menu[0].path,
|
||||
children: [
|
||||
{
|
||||
path: "403",
|
||||
name: "403",
|
||||
component: () => import("@/views/NoPermission.vue"),
|
||||
},
|
||||
...menu
|
||||
]
|
||||
},
|
||||
...whiteListRoutes
|
||||
]
|
||||
})
|
||||
|
||||
const whiteList = [...whiteListRoutes.map((i) => i.name), "403"];
|
||||
|
||||
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) {
|
||||
authStore.$reset();
|
||||
next({ name: "Login" });
|
||||
return;
|
||||
}
|
||||
if (to.meta.permission) {
|
||||
next(!hasPermission(to.meta.permission) ? { name: "403" } : undefined);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
export default router
|
@ -1,143 +0,0 @@
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import {
|
||||
IconUser,
|
||||
IconDashboard,
|
||||
IconOrderedList,
|
||||
IconHeartFill,
|
||||
IconCodeSandbox,
|
||||
IconCodeSquare,
|
||||
IconMessage,
|
||||
IconSettings,
|
||||
IconLock,
|
||||
IconCodepen,
|
||||
IconWechatpay,
|
||||
IconRobot,
|
||||
} from "@arco-design/web-vue/es/icon";
|
||||
|
||||
import system from "./system";
|
||||
|
||||
const menu: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/dashboard",
|
||||
name: "Dashboard",
|
||||
meta: {
|
||||
title: "仪表盘",
|
||||
icon: IconDashboard,
|
||||
},
|
||||
component: () => import("@/views/DashboardView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/user",
|
||||
name: "User",
|
||||
meta: {
|
||||
title: "用户管理",
|
||||
icon: IconUser,
|
||||
permission: "api_admin_user_list",
|
||||
},
|
||||
component: () => import("@/views/User/UserContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/role",
|
||||
name: "Role",
|
||||
meta: {
|
||||
title: "角色模型",
|
||||
icon: IconCodeSandbox,
|
||||
permission: "api_admin_role_list",
|
||||
},
|
||||
component: () => import("@/views/Role/RoleContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/chatModel",
|
||||
name: "ChatModel",
|
||||
meta: {
|
||||
title: "语言模型",
|
||||
icon: IconCodepen,
|
||||
permission: "api_admin_model_list",
|
||||
},
|
||||
component: () => import("@/views/ChatModel/ChatModelContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/product",
|
||||
name: "Product",
|
||||
meta: {
|
||||
title: "充值产品",
|
||||
icon: IconWechatpay,
|
||||
permission: "api_admin_product_list",
|
||||
},
|
||||
component: () => import("@/views/Product/ProductContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/apiKey",
|
||||
name: "ApiKey",
|
||||
meta: {
|
||||
title: "APIKEY",
|
||||
icon: IconLock,
|
||||
permission: "api_admin_apikey_list",
|
||||
},
|
||||
component: () => import("@/views/ApiKey/ApiKeyContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/order",
|
||||
name: "Order",
|
||||
meta: {
|
||||
title: "充值订单",
|
||||
icon: IconOrderedList,
|
||||
permission: "api_admin_order_list",
|
||||
},
|
||||
component: () => import("@/views/Order/OrderContainer.vue"),
|
||||
},
|
||||
|
||||
{
|
||||
path: "/reward",
|
||||
name: "Reward",
|
||||
meta: {
|
||||
title: "众筹管理",
|
||||
icon: IconHeartFill,
|
||||
permission: "api_admin_reward_list",
|
||||
},
|
||||
component: () => import("@/views/Reward/RewardContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/functions",
|
||||
name: "Functions",
|
||||
meta: {
|
||||
title: "函数管理",
|
||||
icon: IconCodeSquare,
|
||||
permission: "api_admin_function_list",
|
||||
},
|
||||
component: () => import("@/views/Functions/FunctionsContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/chats",
|
||||
name: "Chats",
|
||||
meta: {
|
||||
title: "对话管理",
|
||||
icon: IconMessage,
|
||||
permission: "api_admin_chat_list",
|
||||
},
|
||||
component: () => import("@/views/Chats/ChatsContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/system",
|
||||
name: "System",
|
||||
meta: {
|
||||
title: "网站设置",
|
||||
icon: IconSettings,
|
||||
permission: "api_admin_config_get",
|
||||
},
|
||||
component: () => import("@/views/System/SystemContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "/sys",
|
||||
name: "Sys",
|
||||
meta: {
|
||||
title: "系统设置",
|
||||
icon: IconRobot,
|
||||
},
|
||||
redirect: () => system[0].path,
|
||||
children: system
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
export default menu;
|
@ -1,40 +0,0 @@
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
const system: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "admin",
|
||||
name: "SysAdmin",
|
||||
meta: {
|
||||
title: "系统管理员",
|
||||
permission: "api_admin_sysUser_list",
|
||||
},
|
||||
component: () => import("@/views/SysAdmin/SysAdminContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "permission",
|
||||
name: "SysPermission",
|
||||
meta: {
|
||||
title: "权限配置",
|
||||
permission: "api_admin_sysPermission_list",
|
||||
},
|
||||
component: () => import("@/views/SysPermission/SysPermissionContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "role",
|
||||
name: "SysRole",
|
||||
meta: {
|
||||
title: "角色管理",
|
||||
permission: "api_admin_sysRole_list",
|
||||
},
|
||||
component: () => import("@/views/SysRole/SysRoleContainer.vue"),
|
||||
},
|
||||
{
|
||||
path: "loginLog",
|
||||
name: "LoginLog",
|
||||
meta: {
|
||||
title: "登录日志",
|
||||
},
|
||||
component: () => import("@/views/LoginLog.vue"),
|
||||
},
|
||||
]
|
||||
|
||||
export default system
|
8
new-ui/projects/admin/src/router/type.d.ts
vendored
@ -1,8 +0,0 @@
|
||||
import 'vue-router'
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
title?: string
|
||||
icon?: any
|
||||
permission?: boolean | string | string[]
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { userLogin, userLogout } from '@/http/login'
|
||||
import router from '@/router'
|
||||
|
||||
const defaultState: {
|
||||
token: string
|
||||
is_super_admin?: boolean;
|
||||
permissions?: string[]
|
||||
} = {
|
||||
token: null,
|
||||
is_super_admin: false,
|
||||
permissions: []
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore({
|
||||
id: Symbol(__AUTH_KEY).toString(),
|
||||
state: () => ({ ...defaultState }),
|
||||
actions: {
|
||||
init() {
|
||||
this.$state = JSON.parse(localStorage.getItem(__AUTH_KEY));
|
||||
},
|
||||
async login(params: any) {
|
||||
try {
|
||||
const { data } = await userLogin(params)
|
||||
if (data) {
|
||||
this.$state = data;
|
||||
localStorage.setItem(__AUTH_KEY, JSON.stringify(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,139 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { getList, save, deleting, setStatus } from "./api";
|
||||
import ApiKeyForm from "./ApiKeyForm.vue";
|
||||
import useCustomFormPopup from "@/composables/useCustomFormPopup";
|
||||
import { Message, type TableColumnData } from "@arco-design/web-vue";
|
||||
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
|
||||
import { dateFormat } from "@chatgpt-plus/packages/utils";
|
||||
// table 配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: "所属平台",
|
||||
dataIndex: "platform",
|
||||
},
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "name",
|
||||
},
|
||||
{
|
||||
title: "key",
|
||||
dataIndex: "value",
|
||||
slotName: "value",
|
||||
},
|
||||
{
|
||||
title: "API URL",
|
||||
dataIndex: "api_url",
|
||||
slotName: "value",
|
||||
},
|
||||
{
|
||||
title: "用途",
|
||||
dataIndex: "type",
|
||||
},
|
||||
{
|
||||
title: "使用代理",
|
||||
dataIndex: "use_proxy",
|
||||
slotName: "proxy",
|
||||
align: "center",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "最后使用时间",
|
||||
dataIndex: "last_used_at",
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(record.last_used_at);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "启用状态",
|
||||
dataIndex: "enabled",
|
||||
slotName: "status",
|
||||
align: "center",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
slotName: "action",
|
||||
width: 120,
|
||||
fixed: "right",
|
||||
},
|
||||
];
|
||||
|
||||
// 新增编辑
|
||||
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>
|
||||
<SimpleTable :columns="columns" :request="getList">
|
||||
<template #header="{ reload }">
|
||||
<a-space>
|
||||
<a-button @click="popup({ reload })" size="small" type="primary"
|
||||
><template #icon> <icon-plus /> </template>新增
|
||||
</a-button>
|
||||
<a-button type="primary" status="success" href="https://gpt.bemore.lol" target="_blank">
|
||||
<template #icon>
|
||||
<icon-link />
|
||||
</template>
|
||||
购买API-KEY
|
||||
</a-button>
|
||||
</a-space>
|
||||
</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 #value="{ record, column }">
|
||||
<a-typography-text copyable ellipsis style="margin: 0">
|
||||
{{ record[column.dataIndex] }}
|
||||
</a-typography-text>
|
||||
</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>
|
@ -1,172 +0,0 @@
|
||||
<template>
|
||||
<a-alert type="warning">
|
||||
<div class="warning">
|
||||
<div>注意:如果是百度文心一言平台,API-KEY 为 APIKey|SecretKey,中间用竖线(|)连接</div>
|
||||
<div>注意:如果是讯飞星火大模型,API-KEY 为 AppId|APIKey|APISecret,中间用竖线(|)连接</div>
|
||||
</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-select
|
||||
v-model="form.platform"
|
||||
placeholder="请输入所属平台"
|
||||
:options="platformOptions"
|
||||
@change="handlePlatformChange"
|
||||
/>
|
||||
</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"
|
||||
@change="handlePlatformChange"
|
||||
/>
|
||||
</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-space>
|
||||
<a-switch v-model="form.use_proxy" />
|
||||
<a-tooltip
|
||||
content="是否使用代理访问 API URL,OpenAI 官方API需要开启代理访问"
|
||||
position="right"
|
||||
>
|
||||
<icon-info-circle-fill />
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</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",
|
||||
},
|
||||
];
|
||||
|
||||
const platformOptions = [
|
||||
{
|
||||
label: "【OpenAI】ChatGPT",
|
||||
value: "OpenAI",
|
||||
api_url: "https://gpt.bemore.lol/v1/chat/completions",
|
||||
img_url: "https://gpt.bemore.lol/v1/images/generations",
|
||||
},
|
||||
{
|
||||
label: "【讯飞】星火大模型",
|
||||
value: "XunFei",
|
||||
api_url: "wss://spark-api.xf-yun.com/{version}/chat",
|
||||
},
|
||||
{
|
||||
label: "【清华智普】ChatGLM",
|
||||
value: "ChatGLM",
|
||||
api_url: "https://open.bigmodel.cn/api/paas/v3/model-api/{model}/sse-invoke",
|
||||
},
|
||||
{
|
||||
label: "【百度】文心一言",
|
||||
value: "Baidu",
|
||||
api_url: "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{model}",
|
||||
},
|
||||
{
|
||||
label: "【微软】Azure",
|
||||
value: "Azure",
|
||||
api_url:
|
||||
"https://chat-bot-api.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2023-05-15",
|
||||
},
|
||||
{
|
||||
label: "【阿里】千义通问",
|
||||
value: "QWen",
|
||||
api_url: "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation",
|
||||
},
|
||||
];
|
||||
|
||||
const handlePlatformChange = () => {
|
||||
const obj = platformOptions.find((item) => item.value === form.value.platform);
|
||||
if (obj) {
|
||||
form.value.api_url = form.value.type === "img" ? obj.img_url : obj.api_url;
|
||||
}
|
||||
};
|
||||
</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>
|
@ -1,30 +0,0 @@
|
||||
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`,
|
||||
method: "post",
|
||||
data: { id },
|
||||
});
|
||||
};
|
||||
export const setStatus = (data) => {
|
||||
return http({
|
||||
url: `/api/admin/apikey/set`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
@ -1,114 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { getList, save, deleting, setStatus } from "./api";
|
||||
import ChatModelForm from "./ChatModelForm.vue";
|
||||
import useCustomFormPopup from "@/composables/useCustomFormPopup";
|
||||
import { Message, type TableColumnData } from "@arco-design/web-vue";
|
||||
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
|
||||
import { dateFormat } from "@chatgpt-plus/packages/utils";
|
||||
// table 配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
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",
|
||||
width: 120,
|
||||
fixed: "right",
|
||||
},
|
||||
];
|
||||
|
||||
// 新增编辑
|
||||
const popup = useCustomFormPopup(ChatModelForm, save, {
|
||||
popupProps: (arg) => ({ title: arg[0].record ? "编辑模型" : "新增模型" }),
|
||||
});
|
||||
|
||||
// 删除
|
||||
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>
|
||||
<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" type="primary"
|
||||
><template #icon> <icon-plus /> </template>新增</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>
|
@ -1,94 +0,0 @@
|
||||
<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-select v-model="form.platform" placeholder="请输入所属平台" :options="platformOptions" />
|
||||
</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-space>
|
||||
<a-input-number v-model="form.weight" placeholder="请输入对话权重" />
|
||||
<a-tooltip content="对话权重,每次对话扣减多少次对话额度" position="right">
|
||||
<icon-info-circle-fill />
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</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 platformOptions = [
|
||||
{ label: "【OpenAI】ChatGPT", value: "OpenAI" },
|
||||
{ label: "【讯飞】星火大模型", value: "XunFei" },
|
||||
{ label: "【清华智普】ChatGLM", value: "ChatGLM" },
|
||||
{ label: "【百度】文心一言", value: "Baidu" },
|
||||
{ label: "【微软】Azure", value: "Azure" },
|
||||
{ label: "【阿里】通义千问", value: "QWen" },
|
||||
];
|
||||
</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>
|
@ -1,30 +0,0 @@
|
||||
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`,
|
||||
method: "post",
|
||||
data: { id },
|
||||
});
|
||||
};
|
||||
export const setStatus = (data) => {
|
||||
return http({
|
||||
url: `/api/admin/model/set`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
@ -1,150 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, h } from "vue";
|
||||
import { Message, Modal } from "@arco-design/web-vue";
|
||||
import { dateFormat } from "@chatgpt-plus/packages/utils";
|
||||
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 chatColumns: 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: "created_at",
|
||||
title: "创建时间",
|
||||
search: {
|
||||
valueType: "range",
|
||||
},
|
||||
render: ({ record }) => dateFormat(record.created_at),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
fixed: "right",
|
||||
slotName: "actions",
|
||||
},
|
||||
];
|
||||
|
||||
const messageColumns: SearchTableColumns[] = [
|
||||
{
|
||||
dataIndex: "user_id",
|
||||
title: "账户ID",
|
||||
search: {
|
||||
valueType: "input",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "username",
|
||||
title: "账户",
|
||||
},
|
||||
{
|
||||
dataIndex: "title",
|
||||
title: "标题",
|
||||
search: {
|
||||
valueType: "input",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "msg_num",
|
||||
title: "消息数量",
|
||||
},
|
||||
{
|
||||
dataIndex: "token",
|
||||
title: "消耗算力",
|
||||
},
|
||||
{
|
||||
dataIndex: "created_at",
|
||||
title: "创建时间",
|
||||
search: {
|
||||
valueType: "range",
|
||||
},
|
||||
render: ({ record }) => dateFormat(record.created_at),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
fixed: "right",
|
||||
slotName: "actions",
|
||||
},
|
||||
];
|
||||
|
||||
const tabsList = [
|
||||
{ key: "1", title: "对话列表", api: getList, columns: chatColumns },
|
||||
{ key: "2", title: "消息记录", api: message, columns: messageColumns },
|
||||
];
|
||||
|
||||
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.chat_id, reload)"
|
||||
>
|
||||
<a-link status="danger">删除</a-link>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</SearchTable>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
@ -1,94 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import { dateFormat } from "@chatgpt-plus/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>
|
@ -1,34 +0,0 @@
|
||||
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,171 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import http from "@/http/config";
|
||||
import { ref, nextTick } from "vue";
|
||||
import * as echarts from "echarts/core";
|
||||
import { GridComponent, TitleComponent } from "echarts/components";
|
||||
import { LineChart } from "echarts/charts";
|
||||
import { UniversalTransition } from "echarts/features";
|
||||
import { CanvasRenderer } from "echarts/renderers";
|
||||
|
||||
const icons = {
|
||||
users: "icon-user",
|
||||
chats: "icon-wechat",
|
||||
tokens: "icon-computer",
|
||||
income: "icon-wechatpay",
|
||||
};
|
||||
const dataSet = {
|
||||
users: "今日新增用户",
|
||||
chats: "今日新增对话",
|
||||
tokens: "今日消耗 Tokens",
|
||||
income: "今日入账",
|
||||
};
|
||||
|
||||
const getData = () => {
|
||||
http({
|
||||
url: "api/admin/dashboard/stats",
|
||||
method: "get",
|
||||
}).then((res) => {
|
||||
data.value = res.data;
|
||||
handeChartData(res.data.chart);
|
||||
});
|
||||
};
|
||||
getData();
|
||||
// 图表
|
||||
const chartTitle = {
|
||||
historyMessage: "对话",
|
||||
orders: "订单",
|
||||
users: "用户数",
|
||||
};
|
||||
echarts.use([GridComponent, LineChart, CanvasRenderer, UniversalTransition, TitleComponent]);
|
||||
const chartDomRefs = [];
|
||||
const chartData = ref({});
|
||||
const data = ref<Record<string, number>>({});
|
||||
const handeChartData = (data) => {
|
||||
const _chartData = {};
|
||||
for (let key in data) {
|
||||
const type = data[key];
|
||||
_chartData[key] = {
|
||||
series: [],
|
||||
xAxis: [],
|
||||
};
|
||||
for (let date in type) {
|
||||
_chartData[key].series.push(type[date]);
|
||||
_chartData[key].xAxis.push(date);
|
||||
}
|
||||
nextTick(() => {
|
||||
const myChart = echarts.init(chartDomRefs.pop());
|
||||
myChart.setOption(createOption(_chartData[key], key));
|
||||
});
|
||||
}
|
||||
chartData.value = _chartData;
|
||||
};
|
||||
const createOption = (data, key) => {
|
||||
const { xAxis, series } = data;
|
||||
return {
|
||||
title: {
|
||||
left: "center",
|
||||
text: chartTitle[key],
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: xAxis,
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: series,
|
||||
type: "line",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<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"><component :is="icons[key]" /> </span>
|
||||
<span class="count"
|
||||
><a-statistic :extra="value" :value="data[key]" :precision="0"
|
||||
/></span>
|
||||
</div>
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<a-grid
|
||||
:cols="{ xs: 1, sm: 1, md: 1, lg: 2, xl: 2, xxl: 3 }"
|
||||
:colGap="12"
|
||||
:rowGap="16"
|
||||
class="grid"
|
||||
>
|
||||
<a-grid-item v-for="(value, key, index) in chartData" :key="key">
|
||||
<div
|
||||
:ref="
|
||||
(el) => {
|
||||
chartDomRefs[index] = el;
|
||||
}
|
||||
"
|
||||
class="chartDom"
|
||||
>
|
||||
{{ key }}
|
||||
</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;
|
||||
background: #f3f3f3;
|
||||
}
|
||||
}
|
||||
}
|
||||
.chart {
|
||||
margin-top: 15px;
|
||||
.chartDom {
|
||||
width: 450px;
|
||||
height: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,89 +0,0 @@
|
||||
<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 type="primary" @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>
|