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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9288
									
								
								new-ui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -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>
 | 
			
		||||