🎨 更新环境配置,添加 Dockerfile,优化代码结构
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
FROM golang:alpine as builder
|
FROM golang:alpine as builder
|
||||||
|
|
||||||
WORKDIR /go/src/git.echol.cn/loser/Go-Web-Template/server
|
WORKDIR /go/src/git.echol.cn/loser/Go-Web-Template/server/server
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN go env -w GO111MODULE=on \
|
RUN go env -w GO111MODULE=on \
|
||||||
@@ -18,14 +18,14 @@ ENV TZ=Asia/Shanghai
|
|||||||
RUN apk update && apk add --no-cache tzdata openntpd \
|
RUN apk update && apk add --no-cache tzdata openntpd \
|
||||||
&& ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
&& ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
WORKDIR /go/src/git.echol.cn/loser/Go-Web-Template/server
|
WORKDIR /go/src/git.echol.cn/loser/Go-Web-Template/server/server
|
||||||
|
|
||||||
COPY --from=0 /go/src/git.echol.cn/loser/Go-Web-Template/server/server ./
|
COPY --from=0 /go/src/git.echol.cn/loser/Go-Web-Template/server/server/server ./server
|
||||||
COPY --from=0 /go/src/git.echol.cn/loser/Go-Web-Template/server/resource ./resource/
|
COPY --from=0 /go/src/git.echol.cn/loser/Go-Web-Template/server/server/resource ./resource/
|
||||||
COPY --from=0 /go/src/git.echol.cn/loser/Go-Web-Template/server/config.docker.yaml ./
|
COPY --from=0 /go/src/git.echol.cn/loser/Go-Web-Template/server/server/config.docker.yaml ./
|
||||||
|
|
||||||
# 挂载目录:如果使用了sqlite数据库,容器命令示例:docker run -d -v /宿主机路径/gva.db:/go/src/git.echol.cn/loser/Go-Web-Template/server/gva.db -p 8888:8888 --name gva-server-v1 gva-server:1.0
|
# 挂载目录:如果使用了sqlite数据库,容器命令示例:docker run -d -v /宿主机路径/gva.db:/go/src/git.echol.cn/loser/Go-Web-Template/servergva.db -p 8888:8888 --name gva-server-v1 gva-server:1.0
|
||||||
# VOLUME ["/go/src/git.echol.cn/loser/Go-Web-Template/server"]
|
# VOLUME ["/go/src/git.echol.cn/loser/Go-Web-Template/server/server"]
|
||||||
|
|
||||||
EXPOSE 8888
|
EXPOSE 8888
|
||||||
ENTRYPOINT ./server -c config.docker.yaml
|
ENTRYPOINT ./server -c config.docker.yaml
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
│ └── response
|
│ └── response
|
||||||
├── packfile
|
├── packfile
|
||||||
├── resource
|
├── resource
|
||||||
|
│ ├── excel
|
||||||
│ ├── page
|
│ ├── page
|
||||||
│ └── template
|
│ └── template
|
||||||
├── router
|
├── router
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
| `--response` | 出参结构体 | 返回给前端的数据结构体 |
|
| `--response` | 出参结构体 | 返回给前端的数据结构体 |
|
||||||
| `packfile` | 静态文件打包 | 静态文件打包 |
|
| `packfile` | 静态文件打包 | 静态文件打包 |
|
||||||
| `resource` | 静态资源文件夹 | 负责存放静态文件 |
|
| `resource` | 静态资源文件夹 | 负责存放静态文件 |
|
||||||
|
| `--excel` | excel导入导出默认路径 | excel导入导出默认路径 |
|
||||||
| `--page` | 页面静态资源目录 | 历史页面资源输出目录 |
|
| `--page` | 页面静态资源目录 | 历史页面资源输出目录 |
|
||||||
| `--template` | 模板 | 模板文件夹,存放的是代码生成器的模板 |
|
| `--template` | 模板 | 模板文件夹,存放的是代码生成器的模板 |
|
||||||
| `router` | 路由层 | 路由层 |
|
| `router` | 路由层 | 路由层 |
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
commonReq "git.echol.cn/loser/Go-Web-Template/server/model/common/request"
|
commonReq "git.echol.cn/loser/Go-Web-Template/server/model/common/request"
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
"git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
||||||
commonRes "git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
commonRes "git.echol.cn/loser/Go-Web-Template/server/model/common/response"
|
||||||
|
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
||||||
|
"git.echol.cn/loser/Go-Web-Template/server/utils/upload"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -32,7 +34,13 @@ func (b *FileUploadAndDownloadApi) UploadFile(c *gin.Context) {
|
|||||||
response.FailWithMessage("接收文件失败", c)
|
response.FailWithMessage("接收文件失败", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file, err = fileUploadAndDownloadService.UploadFile(header, noSave, classId) // 文件上传后拿到文件路径
|
// 从系统 JWT 中获取管理员 ID
|
||||||
|
var adminID uint
|
||||||
|
if claims := utils.GetUserInfo(c); claims != nil {
|
||||||
|
adminID = claims.BaseClaims.ID
|
||||||
|
}
|
||||||
|
uploadCtx := upload.UploadContext{Source: upload.SourceAdmin, UserID: adminID}
|
||||||
|
file, err = fileUploadAndDownloadService.UploadFile(header, noSave, classId, uploadCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("上传文件失败!", zap.Error(err))
|
global.GVA_LOG.Error("上传文件失败!", zap.Error(err))
|
||||||
response.FailWithMessage("上传文件失败", c)
|
response.FailWithMessage("上传文件失败", c)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type Local struct {
|
type Local struct {
|
||||||
Path string `mapstructure:"path" json:"path" yaml:"path"` // 本地文件访问路径
|
Path string `mapstructure:"path" json:"path" yaml:"path"` // 本地文件访问路径(相对路径)
|
||||||
StorePath string `mapstructure:"store-path" json:"store-path" yaml:"store-path"` // 本地文件存储路径
|
StorePath string `mapstructure:"store-path" json:"store-path" yaml:"store-path"` // 本地文件存储路径
|
||||||
|
BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"` // 文件访问域名前缀,如 https://api.wanjia.ai
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/core/internal"
|
"git.echol.cn/loser/Go-Web-Template/server/core/internal"
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
"git.echol.cn/loser/Go-Web-Template/server/global"
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
"git.echol.cn/loser/Go-Web-Template/server/utils"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Zap 获取 zap.Logger
|
// Zap 获取 zap.Logger
|
||||||
|
|||||||
@@ -5847,6 +5847,14 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"config.Excel": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dir": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"config.HuaWeiObs": {
|
"config.HuaWeiObs": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -6373,6 +6381,9 @@ const docTemplate = `{
|
|||||||
"email": {
|
"email": {
|
||||||
"$ref": "#/definitions/config.Email"
|
"$ref": "#/definitions/config.Email"
|
||||||
},
|
},
|
||||||
|
"excel": {
|
||||||
|
"$ref": "#/definitions/config.Excel"
|
||||||
|
},
|
||||||
"hua-wei-obs": {
|
"hua-wei-obs": {
|
||||||
"$ref": "#/definitions/config.HuaWeiObs"
|
"$ref": "#/definitions/config.HuaWeiObs"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5839,6 +5839,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"config.Excel": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dir": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"config.HuaWeiObs": {
|
"config.HuaWeiObs": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -6365,6 +6373,9 @@
|
|||||||
"email": {
|
"email": {
|
||||||
"$ref": "#/definitions/config.Email"
|
"$ref": "#/definitions/config.Email"
|
||||||
},
|
},
|
||||||
|
"excel": {
|
||||||
|
"$ref": "#/definitions/config.Excel"
|
||||||
|
},
|
||||||
"hua-wei-obs": {
|
"hua-wei-obs": {
|
||||||
"$ref": "#/definitions/config.HuaWeiObs"
|
"$ref": "#/definitions/config.HuaWeiObs"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -216,6 +216,11 @@ definitions:
|
|||||||
description: 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用
|
description: 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
config.Excel:
|
||||||
|
properties:
|
||||||
|
dir:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
config.HuaWeiObs:
|
config.HuaWeiObs:
|
||||||
properties:
|
properties:
|
||||||
access-key:
|
access-key:
|
||||||
@@ -592,6 +597,8 @@ definitions:
|
|||||||
type: array
|
type: array
|
||||||
email:
|
email:
|
||||||
$ref: '#/definitions/config.Email'
|
$ref: '#/definitions/config.Email'
|
||||||
|
excel:
|
||||||
|
$ref: '#/definitions/config.Excel'
|
||||||
hua-wei-obs:
|
hua-wei-obs:
|
||||||
$ref: '#/definitions/config.HuaWeiObs'
|
$ref: '#/definitions/config.HuaWeiObs'
|
||||||
jwt:
|
jwt:
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ package global
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
"github.com/qiniu/qmgo"
|
"github.com/qiniu/qmgo"
|
||||||
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/utils/timer"
|
"git.echol.cn/loser/Go-Web-Template/server/utils/timer"
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ func bizModel() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package initialize
|
package initialize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/config"
|
"git.echol.cn/loser/Go-Web-Template/server/config"
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/global"
|
"git.echol.cn/loser/Go-Web-Template/server/global"
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/initialize/internal"
|
"git.echol.cn/loser/Go-Web-Template/server/initialize/internal"
|
||||||
@@ -38,6 +40,8 @@ func initPgSqlDatabase(p config.Pgsql) *gorm.DB {
|
|||||||
sqlDB, _ := db.DB()
|
sqlDB, _ := db.DB()
|
||||||
sqlDB.SetMaxIdleConns(p.MaxIdleConns)
|
sqlDB.SetMaxIdleConns(p.MaxIdleConns)
|
||||||
sqlDB.SetMaxOpenConns(p.MaxOpenConns)
|
sqlDB.SetMaxOpenConns(p.MaxOpenConns)
|
||||||
|
sqlDB.SetConnMaxLifetime(30 * time.Minute)
|
||||||
|
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,10 +51,11 @@ func Routers() *gin.Engine {
|
|||||||
// Router.Static("/assets", "./dist/assets") // dist里面的静态资源
|
// Router.Static("/assets", "./dist/assets") // dist里面的静态资源
|
||||||
// Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面
|
// Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面
|
||||||
|
|
||||||
|
// 跨域,如需跨域可以打开下面的注释
|
||||||
|
Router.Use(middleware.Cors()) // 直接放行全部跨域请求(必须在 StaticFS 之前注册,否则静态文件不带 CORS 头)
|
||||||
|
// Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求
|
||||||
Router.StaticFS(global.GVA_CONFIG.Local.StorePath, justFilesFilesystem{http.Dir(global.GVA_CONFIG.Local.StorePath)})
|
Router.StaticFS(global.GVA_CONFIG.Local.StorePath, justFilesFilesystem{http.Dir(global.GVA_CONFIG.Local.StorePath)})
|
||||||
// Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件")
|
// Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件")
|
||||||
// 跨域,如需跨域可以打开下面的注释
|
|
||||||
Router.Use(middleware.Cors()) // 直接放行全部跨域请求
|
|
||||||
// Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求
|
// Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求
|
||||||
// global.GVA_LOG.Info("use middleware cors")
|
// global.GVA_LOG.Info("use middleware cors")
|
||||||
docs.SwaggerInfo.BasePath = global.GVA_CONFIG.System.RouterPrefix
|
docs.SwaggerInfo.BasePath = global.GVA_CONFIG.System.RouterPrefix
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 占位方法,保证文件可以正确加载,避免go空变量检测报错,请勿删除。
|
|
||||||
func holder(routers ...*gin.RouterGroup) {
|
func holder(routers ...*gin.RouterGroup) {
|
||||||
_ = routers
|
_ = routers
|
||||||
_ = router.RouterGroupApp
|
_ = router.RouterGroupApp
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package initialize
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.echol.cn/loser/Go-Web-Template/server/task"
|
"git.echol.cn/loser/Go-Web-Template/server/task"
|
||||||
|
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (a *ApiCreator) New() mcp.Tool {
|
|||||||
mcp.WithDescription(`创建后端API记录,用于AI编辑器自动添加API接口时自动创建对应的API权限记录。
|
mcp.WithDescription(`创建后端API记录,用于AI编辑器自动添加API接口时自动创建对应的API权限记录。
|
||||||
|
|
||||||
**重要限制:**
|
**重要限制:**
|
||||||
- 当使用模块自动生成功能且 needCreatedModules=true 时,模块创建会自动生成 API 权限,不应调用此工具
|
- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成API权限,不应调用此工具
|
||||||
- 仅在以下情况使用:1) 单独创建API(不涉及模块创建);2) AI编辑器自动添加API;3) router下的文件产生路径变化时`),
|
- 仅在以下情况使用:1) 单独创建API(不涉及模块创建);2) AI编辑器自动添加API;3) router下的文件产生路径变化时`),
|
||||||
mcp.WithString("path",
|
mcp.WithString("path",
|
||||||
mcp.Required(),
|
mcp.Required(),
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func (m *MenuCreator) New() mcp.Tool {
|
|||||||
mcp.WithDescription(`创建前端菜单记录,用于AI编辑器自动添加前端页面时自动创建对应的菜单项。
|
mcp.WithDescription(`创建前端菜单记录,用于AI编辑器自动添加前端页面时自动创建对应的菜单项。
|
||||||
|
|
||||||
**重要限制:**
|
**重要限制:**
|
||||||
- 当使用模块自动生成功能且 needCreatedModules=true 时,模块创建会自动生成菜单项,不应调用此工具
|
- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成菜单项,不应调用此工具
|
||||||
- 仅在以下情况使用:1) 单独创建菜单(不涉及模块创建);2) AI编辑器自动添加前端页面时`),
|
- 仅在以下情况使用:1) 单独创建菜单(不涉及模块创建);2) AI编辑器自动添加前端页面时`),
|
||||||
mcp.WithNumber("parentId",
|
mcp.WithNumber("parentId",
|
||||||
mcp.Description("父菜单ID,0表示根菜单"),
|
mcp.Description("父菜单ID,0表示根菜单"),
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ func OperationRecord() gin.HandlerFunc {
|
|||||||
strings.Contains(c.Writer.Header().Get("Cache-Control"), "must-revalidate, post-check=0, pre-check=0") ||
|
strings.Contains(c.Writer.Header().Get("Cache-Control"), "must-revalidate, post-check=0, pre-check=0") ||
|
||||||
strings.Contains(c.Writer.Header().Get("Content-Type"), "application/force-download") ||
|
strings.Contains(c.Writer.Header().Get("Content-Type"), "application/force-download") ||
|
||||||
strings.Contains(c.Writer.Header().Get("Content-Type"), "application/octet-stream") ||
|
strings.Contains(c.Writer.Header().Get("Content-Type"), "application/octet-stream") ||
|
||||||
|
strings.Contains(c.Writer.Header().Get("Content-Type"), "application/vnd.ms-excel") ||
|
||||||
strings.Contains(c.Writer.Header().Get("Content-Type"), "application/download") ||
|
strings.Contains(c.Writer.Header().Get("Content-Type"), "application/download") ||
|
||||||
strings.Contains(c.Writer.Header().Get("Content-Disposition"), "attachment") ||
|
strings.Contains(c.Writer.Header().Get("Content-Disposition"), "attachment") ||
|
||||||
strings.Contains(c.Writer.Header().Get("Content-Transfer-Encoding"), "binary") {
|
strings.Contains(c.Writer.Header().Get("Content-Transfer-Encoding"), "binary") {
|
||||||
|
|||||||
@@ -93,9 +93,9 @@ func (e *FileUploadAndDownloadService) GetFileRecordInfoList(info commonReq.ExaA
|
|||||||
//@param: header *multipart.FileHeader, noSave string
|
//@param: header *multipart.FileHeader, noSave string
|
||||||
//@return: file model.ExaFileUploadAndDownload, err error
|
//@return: file model.ExaFileUploadAndDownload, err error
|
||||||
|
|
||||||
func (e *FileUploadAndDownloadService) UploadFile(header *multipart.FileHeader, noSave string, classId int) (file common.ExaFileUploadAndDownload, err error) {
|
func (e *FileUploadAndDownloadService) UploadFile(header *multipart.FileHeader, noSave string, classId int, uploadCtx upload.UploadContext) (file common.ExaFileUploadAndDownload, err error) {
|
||||||
oss := upload.NewOss()
|
oss := upload.NewOss()
|
||||||
filePath, key, uploadErr := oss.UploadFile(header)
|
filePath, key, uploadErr := oss.UploadFile(header, uploadCtx)
|
||||||
if uploadErr != nil {
|
if uploadErr != nil {
|
||||||
return file, uploadErr
|
return file, uploadErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,28 +9,24 @@ import (
|
|||||||
type SysParamsService struct{}
|
type SysParamsService struct{}
|
||||||
|
|
||||||
// CreateSysParams 创建参数记录
|
// CreateSysParams 创建参数记录
|
||||||
// Author [Mr.奇淼](https://github.com/pixelmaxQm)
|
|
||||||
func (sysParamsService *SysParamsService) CreateSysParams(sysParams *system.SysParams) (err error) {
|
func (sysParamsService *SysParamsService) CreateSysParams(sysParams *system.SysParams) (err error) {
|
||||||
err = global.GVA_DB.Create(sysParams).Error
|
err = global.GVA_DB.Create(sysParams).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSysParams 删除参数记录
|
// DeleteSysParams 删除参数记录
|
||||||
// Author [Mr.奇淼](https://github.com/pixelmaxQm)
|
|
||||||
func (sysParamsService *SysParamsService) DeleteSysParams(ID string) (err error) {
|
func (sysParamsService *SysParamsService) DeleteSysParams(ID string) (err error) {
|
||||||
err = global.GVA_DB.Delete(&system.SysParams{}, "id = ?", ID).Error
|
err = global.GVA_DB.Delete(&system.SysParams{}, "id = ?", ID).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSysParamsByIds 批量删除参数记录
|
// DeleteSysParamsByIds 批量删除参数记录
|
||||||
// Author [Mr.奇淼](https://github.com/pixelmaxQm)
|
|
||||||
func (sysParamsService *SysParamsService) DeleteSysParamsByIds(IDs []string) (err error) {
|
func (sysParamsService *SysParamsService) DeleteSysParamsByIds(IDs []string) (err error) {
|
||||||
err = global.GVA_DB.Delete(&[]system.SysParams{}, "id in ?", IDs).Error
|
err = global.GVA_DB.Delete(&[]system.SysParams{}, "id in ?", IDs).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSysParams 更新参数记录
|
// UpdateSysParams 更新参数记录
|
||||||
// Author [Mr.奇淼](https://github.com/pixelmaxQm)
|
|
||||||
func (sysParamsService *SysParamsService) UpdateSysParams(sysParams system.SysParams) (err error) {
|
func (sysParamsService *SysParamsService) UpdateSysParams(sysParams system.SysParams) (err error) {
|
||||||
err = global.GVA_DB.Model(&system.SysParams{}).Where("id = ?", sysParams.ID).Updates(&sysParams).Error
|
err = global.GVA_DB.Model(&system.SysParams{}).Where("id = ?", sysParams.ID).Updates(&sysParams).Error
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VITE_BASE_API=/api
|
VITE_BASE_API=http://localhost:8888
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VITE_BASE_API=/api
|
VITE_BASE_API=https://api.wanjia.ai
|
||||||
|
|||||||
2
web-admin/.gitignore
vendored
2
web-admin/.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
#logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
|
|||||||
26
web-admin/Dockerfile
Normal file
26
web-admin/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# ---- deps ----
|
||||||
|
FROM node:20-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# ---- build ----
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG VITE_BASE_API=/api
|
||||||
|
ENV VITE_BASE_API=$VITE_BASE_API
|
||||||
|
|
||||||
|
RUN pnpm exec vite build
|
||||||
|
|
||||||
|
# ---- runtime ----
|
||||||
|
FROM nginx:alpine AS runner
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
33
web-admin/nginx.conf
Normal file
33
web-admin/nginx.conf
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# 文件上传大小限制 50MB
|
||||||
|
client_max_body_size 50m;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# 将 /api/ 请求反代到后端(去掉 /api 前缀,与 vite dev proxy 行为一致)
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://server:8888/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_read_timeout 35s;
|
||||||
|
proxy_send_timeout 35s;
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# SPA fallback:所有非文件路径返回 index.html
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 静态资源缓存
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.14.0",
|
"react-router-dom": "^7.14.0",
|
||||||
"recharts": "^3.8.1",
|
"recharts": "^3.8.1",
|
||||||
"zustand": "^5.0.12"
|
"zustand": "^5.0.12"
|
||||||
|
|||||||
3974
web-admin/pnpm-lock.yaml
generated
Normal file
3974
web-admin/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,123 +1,606 @@
|
|||||||
import { Card, Col, List, Progress, Row, Statistic, Tag, Typography } from 'antd'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { moduleList } from '@/features/discovery/systemInventory'
|
import {
|
||||||
import { flattenMenus } from '@/lib/menu'
|
Avatar,
|
||||||
import { useAuthStore } from '@/store/auth'
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Image,
|
||||||
|
Row,
|
||||||
|
Skeleton,
|
||||||
|
Statistic,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
message,
|
||||||
|
Drawer,
|
||||||
|
Input,
|
||||||
|
Space,
|
||||||
|
Upload,
|
||||||
|
} from 'antd'
|
||||||
|
import {
|
||||||
|
AlertOutlined,
|
||||||
|
BarChartOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
MessageOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
RiseOutlined,
|
||||||
|
StarOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { dashboardApi, feedbackAdminApi, uploadFile, getMediaUrl } from '@/lib/api'
|
||||||
|
import type { DashboardFeedbackPendingItem, DashboardTopCreator } from '@/types/system'
|
||||||
|
|
||||||
export function DashboardPage() {
|
const { Text, Title } = Typography
|
||||||
const menus = useAuthStore((state) => state.menus)
|
|
||||||
const user = useAuthStore((state) => state.user)
|
|
||||||
|
|
||||||
const flatMenus = flattenMenus(menus)
|
// ── 快速回复 Drawer ──────────────────────────────────────────
|
||||||
const implementedCount = moduleList.filter((item) => item.status === 'ready').length
|
const QUICK_REPLIES: { label: string; value: string }[] = [
|
||||||
const partialCount = moduleList.filter((item) => item.status === 'partial').length
|
{ label: '正在处理', value: '您好,我们已收到您的反馈,正在为您处理,请耐心等待。' },
|
||||||
const allowedModules = flatMenus.length
|
{ label: '已解决', value: '您好,您反馈的问题已解决,如还有疑问欢迎继续联系我们。' },
|
||||||
const allEndpoints = moduleList.reduce((total, item) => total + item.endpoints.length, 0)
|
{ label: '无法复现', value: '您好,我们尝试复现您描述的问题但未能成功,请提供更多截图或操作步骤以便排查。' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function QuickReplyDrawer({
|
||||||
|
item,
|
||||||
|
onClose,
|
||||||
|
onReplied,
|
||||||
|
}: {
|
||||||
|
item: DashboardFeedbackPendingItem | null
|
||||||
|
onClose: () => void
|
||||||
|
onReplied: () => void
|
||||||
|
}) {
|
||||||
|
const [content, setContent] = useState('')
|
||||||
|
const [images, setImages] = useState<string[]>([])
|
||||||
|
const [sending, setSending] = useState(false)
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
|
||||||
|
const detailQuery = useQuery({
|
||||||
|
queryKey: ['feedback-detail', item?.id],
|
||||||
|
queryFn: () => feedbackAdminApi.getById(item!.id),
|
||||||
|
enabled: !!item,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!item || (!content.trim() && images.length === 0)) return
|
||||||
|
setSending(true)
|
||||||
|
try {
|
||||||
|
await feedbackAdminApi.reply(item.id, {
|
||||||
|
content: content.trim() || undefined,
|
||||||
|
images: images.length > 0 ? images : undefined,
|
||||||
|
})
|
||||||
|
message.success('回复成功')
|
||||||
|
setContent('')
|
||||||
|
setImages([])
|
||||||
|
onReplied()
|
||||||
|
onClose()
|
||||||
|
} catch {
|
||||||
|
message.error('回复失败')
|
||||||
|
} finally {
|
||||||
|
setSending(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-stack">
|
<Drawer
|
||||||
<Card className="glass-panel page-panel">
|
title={
|
||||||
<div className="hero-grid">
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<div>
|
<Avatar src={item?.userAvatar} size={28} />
|
||||||
<Tag color="processing" style={{ marginBottom: 12 }}>
|
<span style={{ fontSize: 14, fontWeight: 600 }}>
|
||||||
React 管理后台重构中
|
{item?.userNickname} · {item?.title}
|
||||||
</Tag>
|
</span>
|
||||||
<Typography.Title level={2} style={{ marginBottom: 12 }}>
|
{item && <Badge count={item.unreadCount} />}
|
||||||
{user?.nickName || user?.userName || '管理员'},欢迎回来
|
|
||||||
</Typography.Title>
|
|
||||||
<Typography.Paragraph className="text-muted" style={{ maxWidth: 720, marginBottom: 20 }}>
|
|
||||||
新后台已经基于真实菜单树和后端接口协议接管路由结构。当前仪表盘会直接反映已有菜单权限、模块覆盖进度和需要继续迁移的能力。
|
|
||||||
</Typography.Paragraph>
|
|
||||||
<Row gutter={[16, 16]}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Card bordered={false} style={{ background: 'var(--accent-soft)' }}>
|
|
||||||
<Statistic title="当前角色默认首页" value={user?.authority?.defaultRouter || 'dashboard'} />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<Card bordered={false} style={{ background: 'var(--success-soft)' }}>
|
|
||||||
<Statistic title="可访问内部菜单" value={allowedModules} />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
</div>
|
||||||
<Card bordered={false} style={{ background: 'rgba(16, 37, 66, 0.04)' }}>
|
}
|
||||||
<Typography.Title level={4}>重构覆盖率</Typography.Title>
|
open={!!item}
|
||||||
<Progress
|
onClose={onClose}
|
||||||
percent={Math.round((implementedCount / moduleList.length) * 100)}
|
width={520}
|
||||||
strokeColor="#d16f3f"
|
footer={
|
||||||
/>
|
<div style={{ display: 'flex', gap: 8, flexDirection: 'column' }}>
|
||||||
<List
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||||
|
{QUICK_REPLIES.map((r) => (
|
||||||
|
<Button
|
||||||
|
key={r.label}
|
||||||
size="small"
|
size="small"
|
||||||
style={{ marginTop: 16 }}
|
onClick={() =>
|
||||||
dataSource={[
|
setContent((prev) => (prev.trim() ? `${prev}\n${r.value}` : r.value))
|
||||||
`已接入模块 ${implementedCount} 个`,
|
}
|
||||||
`待继续迁移 ${partialCount} 个`,
|
>
|
||||||
`模块登记总数 ${moduleList.length} 个`,
|
{r.label}
|
||||||
`清点出的接口触点 ${allEndpoints} 个`,
|
</Button>
|
||||||
]}
|
))}
|
||||||
renderItem={(item) => <List.Item>{item}</List.Item>}
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<Input.TextArea
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
placeholder="输入回复内容..."
|
||||||
|
autoSize={{ minRows: 2, maxRows: 5 }}
|
||||||
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
</div>
|
<Upload
|
||||||
</Card>
|
showUploadList={false}
|
||||||
<div className="metric-grid">
|
accept="image/*"
|
||||||
<div className="metric-card">
|
customRequest={async ({ file, onSuccess, onError }) => {
|
||||||
<Typography.Text className="text-muted">模块总数</Typography.Text>
|
setUploading(true)
|
||||||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
try {
|
||||||
{moduleList.length}
|
const url = await uploadFile(file as File)
|
||||||
</Typography.Title>
|
setImages((prev) => [...prev, url])
|
||||||
</div>
|
onSuccess?.(url)
|
||||||
<div className="metric-card">
|
} catch {
|
||||||
<Typography.Text className="text-muted">已接入</Typography.Text>
|
onError?.(new Error('上传失败'))
|
||||||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
message.error('图片上传失败')
|
||||||
{implementedCount}
|
} finally {
|
||||||
</Typography.Title>
|
setUploading(false)
|
||||||
</div>
|
}
|
||||||
<div className="metric-card">
|
}}
|
||||||
<Typography.Text className="text-muted">待继续</Typography.Text>
|
>
|
||||||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
<Button icon={<UploadOutlined />} loading={uploading} size="small" />
|
||||||
{partialCount}
|
</Upload>
|
||||||
</Typography.Title>
|
<Button type="primary" size="small" loading={sending} onClick={handleSend}>
|
||||||
</div>
|
发送
|
||||||
<div className="metric-card">
|
</Button>
|
||||||
<Typography.Text className="text-muted">已登记接口触点</Typography.Text>
|
|
||||||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
|
||||||
{allEndpoints}
|
|
||||||
</Typography.Title>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card className="glass-panel page-panel" title="模块清单">
|
{images.length > 0 && (
|
||||||
<div className="catalog-grid">
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||||
{moduleList.map((item) => (
|
<Image.PreviewGroup>
|
||||||
<div className="catalog-card" key={item.name}>
|
{images.map((url, i) => (
|
||||||
<div className="section-heading" style={{ marginBottom: 8 }}>
|
<div key={i} style={{ position: 'relative' }}>
|
||||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
<Image
|
||||||
{item.title}
|
src={url}
|
||||||
</Typography.Title>
|
width={56}
|
||||||
<span className={`status-dot ${item.status}`}></span>
|
height={56}
|
||||||
|
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setImages((prev) => prev.filter((_, idx) => idx !== i))}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: -4,
|
||||||
|
right: -4,
|
||||||
|
border: 'none',
|
||||||
|
background: 'rgba(0,0,0,0.5)',
|
||||||
|
color: '#fff',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 10,
|
||||||
|
lineHeight: '16px',
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Image.PreviewGroup>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{detailQuery.isLoading ? (
|
||||||
|
<Skeleton active />
|
||||||
|
) : detailQuery.data?.data ? (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxHeight: 'calc(100vh - 280px)', overflowY: 'auto' }}>
|
||||||
|
{detailQuery.data.data.messages.map((msg) => (
|
||||||
|
<div
|
||||||
|
key={msg.id}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: msg.senderType === 'admin' ? 'row-reverse' : 'row',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar src={msg.senderAvatar ? getMediaUrl(msg.senderAvatar) : undefined} size={32} style={{ flexShrink: 0 }}>
|
||||||
|
{msg.senderName[0]}
|
||||||
|
</Avatar>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: '75%',
|
||||||
|
background: msg.senderType === 'admin' ? 'var(--accent-soft)' : 'rgba(16,37,66,0.06)',
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: '8px 12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 4 }}>
|
||||||
|
{msg.senderName}
|
||||||
|
</div>
|
||||||
|
{msg.content && <div style={{ fontSize: 13 }}>{msg.content}</div>}
|
||||||
|
{msg.images && msg.images.length > 0 && (
|
||||||
|
<Image.PreviewGroup>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 }}>
|
||||||
|
{msg.images.map((url, i) => (
|
||||||
|
<Image
|
||||||
|
key={i}
|
||||||
|
src={getMediaUrl(url)}
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
style={{ objectFit: 'cover', borderRadius: 6 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Image.PreviewGroup>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Typography.Paragraph className="text-muted" style={{ minHeight: 66 }}>
|
|
||||||
{item.summary}
|
|
||||||
</Typography.Paragraph>
|
|
||||||
<Tag>{item.group}</Tag>
|
|
||||||
<Tag bordered={false}>{item.endpoints.length} 个接口触点</Tag>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
) : null}
|
||||||
<Card className="glass-panel page-panel" title="当前角色菜单">
|
</Drawer>
|
||||||
<List
|
)
|
||||||
dataSource={flatMenus.filter((item) => !item.hidden)}
|
}
|
||||||
renderItem={(item) => (
|
|
||||||
<List.Item>
|
// ── 通用 Section 标题 ────────────────────────────────────────
|
||||||
<List.Item.Meta
|
function SectionHeader({ icon, title, extra }: { icon: React.ReactNode; title: string; extra?: React.ReactNode }) {
|
||||||
title={item.meta.title}
|
return (
|
||||||
description={`${item.name} · ${item.fullPath}`}
|
<div
|
||||||
/>
|
style={{
|
||||||
</List.Item>
|
display: 'flex',
|
||||||
)}
|
alignItems: 'center',
|
||||||
/>
|
justifyContent: 'space-between',
|
||||||
</Card>
|
marginBottom: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<span style={{ color: 'var(--accent)', display: 'flex' }}>{icon}</span>
|
||||||
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
</div>
|
||||||
|
{extra}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 指标卡 ────────────────────────────────────────────────────
|
||||||
|
function MetricCard({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
suffix,
|
||||||
|
loading,
|
||||||
|
highlight,
|
||||||
|
sub,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
value: number | string
|
||||||
|
suffix?: string
|
||||||
|
loading?: boolean
|
||||||
|
highlight?: boolean
|
||||||
|
sub?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
bordered={false}
|
||||||
|
style={{
|
||||||
|
background: highlight ? 'var(--accent-soft)' : 'var(--surface)',
|
||||||
|
borderRadius: 12,
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
bodyStyle={{ padding: '16px 20px' }}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Skeleton active paragraph={{ rows: 1 }} title={false} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Statistic
|
||||||
|
title={<Text style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{title}</Text>}
|
||||||
|
value={value}
|
||||||
|
suffix={suffix}
|
||||||
|
valueStyle={{ fontSize: 26, fontWeight: 700 }}
|
||||||
|
/>
|
||||||
|
{sub && (
|
||||||
|
<Text style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{sub}</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 主组件 ────────────────────────────────────────────────────
|
||||||
|
export function DashboardPage() {
|
||||||
|
const [selectedFeedback, setSelectedFeedback] = useState<DashboardFeedbackPendingItem | null>(null)
|
||||||
|
|
||||||
|
const { data, isLoading, refetch, isRefetching } = useQuery({
|
||||||
|
queryKey: ['dashboard-stats'],
|
||||||
|
queryFn: () => dashboardApi.getStats(),
|
||||||
|
refetchInterval: 15_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const stats = data?.data
|
||||||
|
|
||||||
|
const handleReplied = () => {
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-stack" style={{ gap: 20 }}>
|
||||||
|
{/* 顶部刷新 */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Title level={4} style={{ margin: 0 }}>
|
||||||
|
运营大屏
|
||||||
|
</Title>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
loading={isRefetching}
|
||||||
|
onClick={() => refetch()}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── 用户数据 ── */}
|
||||||
|
<Card className="glass-panel page-panel" bodyStyle={{ padding: '20px 24px' }}>
|
||||||
|
<SectionHeader icon={<UserOutlined />} title="用户数据" />
|
||||||
|
<Row gutter={[12, 12]}>
|
||||||
|
<Col xs={12} sm={8} md={4}>
|
||||||
|
<MetricCard title="注册用户总数" value={stats?.users.total ?? 0} loading={isLoading} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={8} md={4}>
|
||||||
|
<MetricCard title="创作者数" value={stats?.users.creatorCount ?? 0} loading={isLoading} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={8} md={4}>
|
||||||
|
<MetricCard
|
||||||
|
title="当前在线"
|
||||||
|
value={stats?.users.onlineCount ?? 0}
|
||||||
|
loading={isLoading}
|
||||||
|
highlight
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={8} md={4}>
|
||||||
|
<MetricCard
|
||||||
|
title="今日新增"
|
||||||
|
value={stats?.users.todayNew ?? 0}
|
||||||
|
loading={isLoading}
|
||||||
|
sub={`本周 +${stats?.users.weekNew ?? 0}`}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={8} md={4}>
|
||||||
|
<MetricCard
|
||||||
|
title="累计消息数"
|
||||||
|
value={stats?.users.totalMessages ?? 0}
|
||||||
|
loading={isLoading}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={8} md={4}>
|
||||||
|
<MetricCard
|
||||||
|
title="累计对话数"
|
||||||
|
value={stats?.users.totalChats ?? 0}
|
||||||
|
loading={isLoading}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* ── 角色卡 + 创作者 并排 ── */}
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{/* 角色卡模块 */}
|
||||||
|
<Col xs={24} lg={10}>
|
||||||
|
<Card className="glass-panel page-panel" style={{ height: '100%' }} bodyStyle={{ padding: '20px 24px' }}>
|
||||||
|
<SectionHeader icon={<BookOutlined />} title="角色卡" />
|
||||||
|
<Row gutter={[12, 12]}>
|
||||||
|
<Col span={8}>
|
||||||
|
<MetricCard title="角色卡总数" value={stats?.characters.total ?? 0} loading={isLoading} />
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<MetricCard title="已发布" value={stats?.characters.published ?? 0} loading={isLoading} />
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<MetricCard
|
||||||
|
title="待审核"
|
||||||
|
value={stats?.characters.pendingReview ?? 0}
|
||||||
|
loading={isLoading}
|
||||||
|
highlight={!!stats?.characters.pendingReview}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<MetricCard
|
||||||
|
title="今日新增"
|
||||||
|
value={stats?.characters.todayNew ?? 0}
|
||||||
|
loading={isLoading}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<MetricCard
|
||||||
|
title="本周新增"
|
||||||
|
value={stats?.characters.weekNew ?? 0}
|
||||||
|
loading={isLoading}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* 创作者模块 */}
|
||||||
|
<Col xs={24} lg={14}>
|
||||||
|
<Card className="glass-panel page-panel" style={{ height: '100%' }} bodyStyle={{ padding: '20px 24px' }}>
|
||||||
|
<SectionHeader icon={<StarOutlined />} title="创作者" />
|
||||||
|
<Row gutter={[12, 12]}>
|
||||||
|
<Col xs={12} sm={8} md={6}>
|
||||||
|
<MetricCard title="创作者总数" value={stats?.creators.total ?? 0} loading={isLoading} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={8} md={6}>
|
||||||
|
<MetricCard title="签约创作者" value={stats?.creators.contracted ?? 0} loading={isLoading} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={8} md={6}>
|
||||||
|
<MetricCard
|
||||||
|
title="今日新增"
|
||||||
|
value={stats?.creators.todayNew ?? 0}
|
||||||
|
loading={isLoading}
|
||||||
|
sub={`本周 +${stats?.creators.weekNew ?? 0}`}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={8} md={6}>
|
||||||
|
<MetricCard
|
||||||
|
title="待审创作申请"
|
||||||
|
value={stats?.creators.pendingApplications ?? 0}
|
||||||
|
loading={isLoading}
|
||||||
|
highlight={!!stats?.creators.pendingApplications}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* ── 热门创作者排行 ── */}
|
||||||
|
<Card className="glass-panel page-panel" bodyStyle={{ padding: '20px 24px' }}>
|
||||||
|
<SectionHeader icon={<RiseOutlined />} title="热门创作者排行(按角色卡游玩量)" />
|
||||||
|
<Table<DashboardTopCreator>
|
||||||
|
size="small"
|
||||||
|
loading={isLoading}
|
||||||
|
dataSource={stats?.creators.topCreators ?? []}
|
||||||
|
pagination={false}
|
||||||
|
rowKey="userId"
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: '排名',
|
||||||
|
width: 56,
|
||||||
|
render: (_, __, idx) => (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontWeight: 700,
|
||||||
|
color: idx < 3 ? 'var(--accent)' : 'var(--text-secondary)',
|
||||||
|
fontSize: idx < 3 ? 16 : 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{idx + 1}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创作者',
|
||||||
|
dataIndex: 'nickName',
|
||||||
|
render: (name: string, row: DashboardTopCreator) => (
|
||||||
|
<Space>
|
||||||
|
<Avatar src={row.avatar ? getMediaUrl(row.avatar) : undefined} size={28}>
|
||||||
|
{name[0]}
|
||||||
|
</Avatar>
|
||||||
|
<Text>{name}</Text>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '角色卡数',
|
||||||
|
dataIndex: 'charCount',
|
||||||
|
width: 100,
|
||||||
|
render: (v: number) => <Tag>{v}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '总游玩量',
|
||||||
|
dataIndex: 'totalUseCount',
|
||||||
|
width: 120,
|
||||||
|
render: (v: number) => (
|
||||||
|
<Text strong style={{ color: 'var(--accent)' }}>
|
||||||
|
{v.toLocaleString()}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* ── 用户反馈 ── */}
|
||||||
|
<Card className="glass-panel page-panel" bodyStyle={{ padding: '20px 24px' }}>
|
||||||
|
<SectionHeader
|
||||||
|
icon={<MessageOutlined />}
|
||||||
|
title="用户反馈"
|
||||||
|
extra={
|
||||||
|
<Space size={4}>
|
||||||
|
<Tag color="processing">待回复 {stats?.feedback.adminUnread ?? 0}</Tag>
|
||||||
|
<Tag color="default">开放 {stats?.feedback.open ?? 0}</Tag>
|
||||||
|
<Tag color="success">今日已关 {stats?.feedback.closedToday ?? 0}</Tag>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Table<DashboardFeedbackPendingItem>
|
||||||
|
size="small"
|
||||||
|
loading={isLoading}
|
||||||
|
dataSource={stats?.feedback.pendingItems ?? []}
|
||||||
|
pagination={false}
|
||||||
|
rowKey="id"
|
||||||
|
locale={{ emptyText: '暂无待回复工单' }}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: '用户',
|
||||||
|
dataIndex: 'userNickname',
|
||||||
|
width: 140,
|
||||||
|
render: (name: string, row: DashboardFeedbackPendingItem) => (
|
||||||
|
<Space>
|
||||||
|
<Avatar src={row.userAvatar} size={24}>
|
||||||
|
{name[0]}
|
||||||
|
</Avatar>
|
||||||
|
<Text style={{ fontSize: 12 }}>{name}</Text>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '工单标题',
|
||||||
|
dataIndex: 'title',
|
||||||
|
render: (title: string) => <Text style={{ fontSize: 13 }}>{title}</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '未读',
|
||||||
|
dataIndex: 'unreadCount',
|
||||||
|
width: 70,
|
||||||
|
render: (v: number) => <Badge count={v} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
width: 150,
|
||||||
|
render: (v: string) => (
|
||||||
|
<Text style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
|
||||||
|
{new Date(v).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 80,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<AlertOutlined />}
|
||||||
|
onClick={() => setSelectedFeedback(row)}
|
||||||
|
>
|
||||||
|
回复
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* ── 系统统计 ── */}
|
||||||
|
<Card className="glass-panel page-panel" bodyStyle={{ padding: '20px 24px' }}>
|
||||||
|
<SectionHeader icon={<BarChartOutlined />} title="系统汇总" />
|
||||||
|
<Row gutter={[12, 12]}>
|
||||||
|
<Col xs={12} md={6}>
|
||||||
|
<MetricCard title="总对话会话数" value={stats?.system.totalConversations ?? 0} loading={isLoading} />
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} md={6}>
|
||||||
|
<MetricCard title="反馈工单总数" value={stats?.feedback.total ?? 0} loading={isLoading} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 快捷回复 Drawer */}
|
||||||
|
<QuickReplyDrawer
|
||||||
|
item={selectedFeedback}
|
||||||
|
onClose={() => setSelectedFeedback(null)}
|
||||||
|
onReplied={handleReplied}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,159 @@ export const moduleCatalog: Record<string, ModuleDescriptor> = {
|
|||||||
features: ['用户列表', '新建用户', '重置密码', '角色切换'],
|
features: ['用户列表', '新建用户', '重置密码', '角色切换'],
|
||||||
endpoints: ['/user/getUserList', '/user/admin_register', '/user/setUserInfo', '/user/resetPassword'],
|
endpoints: ['/user/getUserList', '/user/admin_register', '/user/setUserInfo', '/user/resetPassword'],
|
||||||
},
|
},
|
||||||
|
appManage: {
|
||||||
|
name: 'appManage',
|
||||||
|
title: '应用管理',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '聚合用户运营、创作者管理、内容管理和运行配置能力。',
|
||||||
|
features: ['用户运营', '创作者管理', '内容管理', '运行配置', '风险审阅'],
|
||||||
|
endpoints: ['/admin/user/list', '/admin/creator/applications', '/admin/characters', '/admin/conversations', '/admin/risk/users', '/admin/risk/events', '/admin/risk/traces/lookup'],
|
||||||
|
},
|
||||||
|
appUserManage: {
|
||||||
|
name: 'appUserManage',
|
||||||
|
title: '用户运营',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '承载前台用户管理、对话留痕和风险审阅入口,用于聚合用户侧运营能力。',
|
||||||
|
features: ['用户管理入口', '对话日志入口', '风险账号审阅', '风险事件审阅', '用户资料维护', '会话留痕审计'],
|
||||||
|
endpoints: ['/admin/user/list', '/admin/user/:id', '/admin/user/:id/reset-password', '/admin/conversations', '/admin/risk/users', '/admin/risk/events', '/admin/risk/traces/lookup'],
|
||||||
|
},
|
||||||
|
appUserList: {
|
||||||
|
name: 'appUserList',
|
||||||
|
title: '用户管理',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '管理前台用户资料、启用状态、创作者标识与密码重置。',
|
||||||
|
features: ['关键词检索', '状态切换', '资料编辑', '密码重置'],
|
||||||
|
endpoints: ['/admin/user/list', '/admin/user/:id', '/admin/user/:id/reset-password', '/admin/user/:id/toggle-enable'],
|
||||||
|
},
|
||||||
|
appCreatorManage: {
|
||||||
|
name: 'appCreatorManage',
|
||||||
|
title: '创作者管理分组',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '承载创作者申请审核和创作者列表入口。',
|
||||||
|
features: ['创作者申请处理', '创作者列表入口', '创作者身份维护'],
|
||||||
|
endpoints: ['/admin/creator/applications', '/admin/creators', '/admin/creators/:id/revoke'],
|
||||||
|
},
|
||||||
|
appCreatorManageList: {
|
||||||
|
name: 'appCreatorManageList',
|
||||||
|
title: '创作者管理',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '管理创作者申请审核、创作者列表和权限撤销。',
|
||||||
|
features: ['申请审核', '创作者列表', '权限撤销'],
|
||||||
|
endpoints: ['/admin/creator/applications', '/admin/creator/applications/:id/approve', '/admin/creator/applications/:id/reject', '/admin/creators'],
|
||||||
|
},
|
||||||
|
appCharacterReview: {
|
||||||
|
name: 'appCharacterReview',
|
||||||
|
title: '角色卡审核(旧分组)',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '旧分组占位。角色卡审核入口已并入内容管理。',
|
||||||
|
features: ['旧分组保留', '兼容历史配置'],
|
||||||
|
endpoints: ['/admin/characters', '/admin/characters/:id/approve', '/admin/characters/:id/reject'],
|
||||||
|
},
|
||||||
|
appCharacterReviewList: {
|
||||||
|
name: 'appCharacterReviewList',
|
||||||
|
title: '角色卡审核',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '审核角色卡内容并执行通过、拒绝和删除操作。',
|
||||||
|
features: ['待审列表', '审核详情', '通过与拒绝', '删除角色卡'],
|
||||||
|
endpoints: ['/admin/characters', '/admin/characters/:id', '/admin/characters/:id/approve', '/admin/characters/:id/reject'],
|
||||||
|
},
|
||||||
|
appCharacterManage: {
|
||||||
|
name: 'appCharacterManage',
|
||||||
|
title: '内容管理',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '承载角色卡审核与角色卡全量管理入口,用于聚合内容治理能力。',
|
||||||
|
features: ['角色卡审核入口', '角色卡管理入口', '内容上架治理', '内容存量治理'],
|
||||||
|
endpoints: ['/admin/characters', '/admin/characters/:id', '/admin/characters/:id/approve', '/admin/characters/:id/reject'],
|
||||||
|
},
|
||||||
|
appCharacterManageList: {
|
||||||
|
name: 'appCharacterManageList',
|
||||||
|
title: '角色卡管理',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '查看角色卡全量列表并执行删除等存量治理动作。',
|
||||||
|
features: ['角色卡全量列表', '筛选检索', '删除处理'],
|
||||||
|
endpoints: ['/admin/characters', '/admin/characters/:id', '/admin/characters/:id/delete'],
|
||||||
|
},
|
||||||
|
appAIConfig: {
|
||||||
|
name: 'appAIConfig',
|
||||||
|
title: 'AI模型配置分组',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '承载模型供应商配置入口,用于区分应用运行配置。',
|
||||||
|
features: ['模型配置入口', '默认模型治理', '模型同步'],
|
||||||
|
endpoints: ['/admin/ai-config', '/admin/ai-config/:id/test', '/admin/ai-config/:id/fetch-models'],
|
||||||
|
},
|
||||||
|
appAIConfigList: {
|
||||||
|
name: 'appAIConfigList',
|
||||||
|
title: 'AI模型配置',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '管理模型供应商、默认模型、请求参数和可用模型同步。',
|
||||||
|
features: ['配置列表', '新增与编辑', '默认配置切换', '模型拉取与连通性测试'],
|
||||||
|
endpoints: ['/admin/ai-config', '/admin/ai-config/:id/set-default', '/admin/ai-config/:id/test', '/admin/ai-config/:id/fetch-models'],
|
||||||
|
},
|
||||||
|
appSystemConfig: {
|
||||||
|
name: 'appSystemConfig',
|
||||||
|
title: '系统配置分组',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '承载应用级通知与系统配置入口。',
|
||||||
|
features: ['系统配置入口', '邮件配置入口', '通知能力维护'],
|
||||||
|
endpoints: ['/admin/email-config', '/admin/email-config/test'],
|
||||||
|
},
|
||||||
|
appEmailConfig: {
|
||||||
|
name: 'appEmailConfig',
|
||||||
|
title: '邮件配置',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '管理 SMTP 参数、默认收件人和测试邮件发送。',
|
||||||
|
features: ['SMTP 配置', '启用开关', '默认收件人', '测试邮件'],
|
||||||
|
endpoints: ['/admin/email-config', '/admin/email-config/test'],
|
||||||
|
},
|
||||||
|
appConversationLog: {
|
||||||
|
name: 'appConversationLog',
|
||||||
|
title: '对话日志(旧分组)',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '旧分组占位。对话日志入口已并入用户运营。',
|
||||||
|
features: ['旧分组保留', '兼容历史配置'],
|
||||||
|
endpoints: ['/admin/conversations', '/admin/conversations/:id'],
|
||||||
|
},
|
||||||
|
appConversationLogList: {
|
||||||
|
name: 'appConversationLogList',
|
||||||
|
title: '对话日志',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '查看对话会话、消息内容和删除处理记录。',
|
||||||
|
features: ['会话列表', '消息明细', '删除记录'],
|
||||||
|
endpoints: ['/admin/conversations', '/admin/conversations/:id', '/admin/conversations/:id/delete'],
|
||||||
|
},
|
||||||
|
appRiskAccountList: {
|
||||||
|
name: 'appRiskAccountList',
|
||||||
|
title: '风险账号审阅',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '查看触发风险事件的账号摘要,并更新人工审阅状态和处理备注。',
|
||||||
|
features: ['风险账号列表', '风险分排序', '最近风险摘要', '人工状态更新'],
|
||||||
|
endpoints: ['/admin/risk/users', '/admin/risk/users/:id/status'],
|
||||||
|
},
|
||||||
|
appRiskEventList: {
|
||||||
|
name: 'appRiskEventList',
|
||||||
|
title: '风险事件审阅',
|
||||||
|
group: '应用管理',
|
||||||
|
status: 'ready',
|
||||||
|
summary: '查看风险事件明细、命中摘录和来源信息,并记录处理结论。',
|
||||||
|
features: ['风险事件列表', '事件类型筛选', '详情展开', '人工状态更新'],
|
||||||
|
endpoints: ['/admin/risk/events', '/admin/risk/events/:id/status', '/admin/risk/traces/lookup'],
|
||||||
|
},
|
||||||
dictionary: {
|
dictionary: {
|
||||||
name: 'dictionary',
|
name: 'dictionary',
|
||||||
title: '字典管理',
|
title: '字典管理',
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ const iconMap: Record<string, React.ReactNode> = {
|
|||||||
loginLog: <AppstoreOutlined />,
|
loginLog: <AppstoreOutlined />,
|
||||||
systemTools: <CodeOutlined />,
|
systemTools: <CodeOutlined />,
|
||||||
common: <BookOutlined />,
|
common: <BookOutlined />,
|
||||||
|
appManage: <AppstoreOutlined />,
|
||||||
|
appUserManage: <UserOutlined />,
|
||||||
|
appCreatorManage: <UserOutlined />,
|
||||||
|
appCharacterReview: <BookOutlined />,
|
||||||
|
appCharacterManage: <BookOutlined />,
|
||||||
|
appAIConfig: <SettingOutlined />,
|
||||||
|
appSystemConfig: <SettingOutlined />,
|
||||||
|
appConversationLog: <BookOutlined />,
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMenuItems(menus: AppMenu[]): ItemType[] {
|
function buildMenuItems(menus: AppMenu[]): ItemType[] {
|
||||||
@@ -89,6 +97,16 @@ export function AdminShell({ children }: Props) {
|
|||||||
|
|
||||||
const menuItems = useMemo(() => buildMenuItems(menus), [menus])
|
const menuItems = useMemo(() => buildMenuItems(menus), [menus])
|
||||||
|
|
||||||
|
const defaultOpenKeys = useMemo(() => {
|
||||||
|
const parts = selectedKey.split('/')
|
||||||
|
const keys: string[] = []
|
||||||
|
for (let i = 1; i < parts.length; i++) {
|
||||||
|
keys.push(parts.slice(0, i).join('/'))
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}, [selectedKey])
|
||||||
|
|
||||||
|
|
||||||
const handleMenuClick = ({ key }: { key: string }) => {
|
const handleMenuClick = ({ key }: { key: string }) => {
|
||||||
const target = flattenMenus(menus).find((menu) => menu.fullPathKey === key)
|
const target = flattenMenus(menus).find((menu) => menu.fullPathKey === key)
|
||||||
if (!target) {
|
if (!target) {
|
||||||
@@ -146,6 +164,8 @@ export function AdminShell({ children }: Props) {
|
|||||||
theme="dark"
|
theme="dark"
|
||||||
inlineIndent={16}
|
inlineIndent={16}
|
||||||
selectedKeys={[selectedKey]}
|
selectedKeys={[selectedKey]}
|
||||||
|
defaultOpenKeys={defaultOpenKeys}
|
||||||
|
motion={false}
|
||||||
items={menuItems}
|
items={menuItems}
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
style={{ width: '100%', background: 'transparent', borderInlineEnd: 'none' }}
|
style={{ width: '100%', background: 'transparent', borderInlineEnd: 'none' }}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import type { DataNode } from 'antd/es/tree'
|
|||||||
import { DeleteOutlined, EditOutlined, FolderAddOutlined, UploadOutlined } from '@ant-design/icons'
|
import { DeleteOutlined, EditOutlined, FolderAddOutlined, UploadOutlined } from '@ant-design/icons'
|
||||||
import { formatDate } from '@/lib/date'
|
import { formatDate } from '@/lib/date'
|
||||||
import { http } from '@/lib/http'
|
import { http } from '@/lib/http'
|
||||||
import { mediaLibraryApi } from '@/lib/api'
|
import { getMediaUrl, mediaLibraryApi } from '@/lib/api'
|
||||||
import type { AttachmentCategory, MediaFileRecord } from '@/types/system'
|
import type { AttachmentCategory, MediaFileRecord } from '@/types/system'
|
||||||
|
|
||||||
type SearchFormValues = {
|
type SearchFormValues = {
|
||||||
@@ -257,7 +257,7 @@ export function MediaLibraryPage() {
|
|||||||
<Image
|
<Image
|
||||||
width={56}
|
width={56}
|
||||||
height={56}
|
height={56}
|
||||||
src={record.url}
|
src={getMediaUrl(record.url)}
|
||||||
style={{ objectFit: 'cover', borderRadius: 12 }}
|
style={{ objectFit: 'cover', borderRadius: 12 }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -17,6 +17,131 @@ const componentOptions: ComponentOption[] = [
|
|||||||
{ value: 'features/menus/MenuManagementPage', label: 'MenuManagementPage · 菜单管理', routeName: 'menu' },
|
{ value: 'features/menus/MenuManagementPage', label: 'MenuManagementPage · 菜单管理', routeName: 'menu' },
|
||||||
{ value: 'features/apis/ApiManagementPage', label: 'ApiManagementPage · API 管理', routeName: 'api' },
|
{ value: 'features/apis/ApiManagementPage', label: 'ApiManagementPage · API 管理', routeName: 'api' },
|
||||||
{ value: 'features/users/UserManagementPage', label: 'UserManagementPage · 用户管理', routeName: 'user' },
|
{ value: 'features/users/UserManagementPage', label: 'UserManagementPage · 用户管理', routeName: 'user' },
|
||||||
|
{
|
||||||
|
value: 'features/appManage/AppManageLandingPage',
|
||||||
|
label: 'AppManageLandingPage · 应用管理总览',
|
||||||
|
routeName: 'appManage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appUserManage',
|
||||||
|
label: 'ModuleLandingPage · 用户运营分组',
|
||||||
|
routeName: 'appUserManage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/AppUserManagementPage',
|
||||||
|
label: 'AppUserManagementPage · 用户管理',
|
||||||
|
routeName: 'appUserList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appCreatorManage',
|
||||||
|
label: 'ModuleLandingPage · 创作者管理分组',
|
||||||
|
routeName: 'appCreatorManage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/CreatorManagementPage',
|
||||||
|
label: 'CreatorManagementPage · 创作者管理',
|
||||||
|
routeName: 'appCreatorManageList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appSigningApplication',
|
||||||
|
label: 'ModuleLandingPage · 签约申请分组',
|
||||||
|
routeName: 'appSigningApplication',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/SigningApplicationPage',
|
||||||
|
label: 'SigningApplicationPage · 签约申请审核',
|
||||||
|
routeName: 'appSigningApplicationList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appFanLevelConfig',
|
||||||
|
label: 'ModuleLandingPage · 粉丝等级配置分组',
|
||||||
|
routeName: 'appFanLevelConfig',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/FanLevelConfigPage',
|
||||||
|
label: 'FanLevelConfigPage · 粉丝等级配置',
|
||||||
|
routeName: 'appFanLevelConfigList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appCreatorLevelConfig',
|
||||||
|
label: 'ModuleLandingPage · 创作者等级配置分组',
|
||||||
|
routeName: 'appCreatorLevelConfig',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/CreatorLevelConfigPage',
|
||||||
|
label: 'CreatorLevelConfigPage · 创作者等级配置',
|
||||||
|
routeName: 'appCreatorLevelConfigList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appCharacterReview',
|
||||||
|
label: 'ModuleLandingPage · 角色卡审核旧分组',
|
||||||
|
routeName: 'appCharacterReview',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/CharacterReviewPage',
|
||||||
|
label: 'CharacterReviewPage · 角色卡审核',
|
||||||
|
routeName: 'appCharacterReviewList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appCharacterManage',
|
||||||
|
label: 'ModuleLandingPage · 内容管理分组',
|
||||||
|
routeName: 'appCharacterManage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/CharacterManagementPage',
|
||||||
|
label: 'CharacterManagementPage · 角色卡管理',
|
||||||
|
routeName: 'appCharacterManageList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appLeaderboardConfig',
|
||||||
|
label: 'ModuleLandingPage · 榜单配置分组',
|
||||||
|
routeName: 'appLeaderboardConfig',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/LeaderboardConfigPage',
|
||||||
|
label: 'LeaderboardConfigPage · 榜单配置',
|
||||||
|
routeName: 'appLeaderboardConfigList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/AIConfigManagementPage',
|
||||||
|
label: 'AIConfigManagementPage · AI模型配置',
|
||||||
|
routeName: 'appAIConfigList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appAIConfig',
|
||||||
|
label: 'ModuleLandingPage · AI模型配置分组',
|
||||||
|
routeName: 'appAIConfig',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appSystemConfig',
|
||||||
|
label: 'ModuleLandingPage · 应用系统配置分组',
|
||||||
|
routeName: 'appSystemConfig',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/EmailConfigPage',
|
||||||
|
label: 'EmailConfigPage · 邮件配置',
|
||||||
|
routeName: 'appEmailConfig',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appConversationLog',
|
||||||
|
label: 'ModuleLandingPage · 对话日志旧分组',
|
||||||
|
routeName: 'appConversationLog',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/ConversationLogPage',
|
||||||
|
label: 'ConversationLogPage · 对话日志',
|
||||||
|
routeName: 'appConversationLogList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/RiskAccountReviewPage',
|
||||||
|
label: 'RiskAccountReviewPage · 风险账号审阅',
|
||||||
|
routeName: 'appRiskAccountList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/RiskEventReviewPage',
|
||||||
|
label: 'RiskEventReviewPage · 风险事件审阅',
|
||||||
|
routeName: 'appRiskEventList',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'features/dictionaries/DictionaryManagementPage',
|
value: 'features/dictionaries/DictionaryManagementPage',
|
||||||
label: 'DictionaryManagementPage · 字典管理',
|
label: 'DictionaryManagementPage · 字典管理',
|
||||||
@@ -58,6 +183,26 @@ const componentOptions: ComponentOption[] = [
|
|||||||
{ value: 'features/mcp/McpTestPage', label: 'McpTestPage · MCP Tools 管理', routeName: 'mcpTest' },
|
{ value: 'features/mcp/McpTestPage', label: 'McpTestPage · MCP Tools 管理', routeName: 'mcpTest' },
|
||||||
{ value: 'features/mcp/McpToolPage', label: 'McpToolPage · MCP Tools 模板', routeName: 'mcpTool' },
|
{ value: 'features/mcp/McpToolPage', label: 'McpToolPage · MCP Tools 模板', routeName: 'mcpTool' },
|
||||||
{ value: 'features/media/MediaLibraryPage', label: 'MediaLibraryPage · 媒体库', routeName: 'upload' },
|
{ value: 'features/media/MediaLibraryPage', label: 'MediaLibraryPage · 媒体库', routeName: 'upload' },
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appAnnouncementManage',
|
||||||
|
label: 'ModuleLandingPage · 公告管理分组',
|
||||||
|
routeName: 'appAnnouncementManage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/AnnouncementManagePage',
|
||||||
|
label: 'AnnouncementManagePage · 公告管理',
|
||||||
|
routeName: 'appAnnouncementManageList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/discovery/ModuleLandingPage:appFeedbackManage',
|
||||||
|
label: 'ModuleLandingPage · 反馈工单分组',
|
||||||
|
routeName: 'appFeedbackManage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'features/appManage/FeedbackManagePage',
|
||||||
|
label: 'FeedbackManagePage · 反馈工单管理',
|
||||||
|
routeName: 'appFeedbackManageList',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const componentRouteNameMap = new Map(componentOptions.map((item) => [item.value, item.routeName]))
|
const componentRouteNameMap = new Map(componentOptions.map((item) => [item.value, item.routeName]))
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ const groupMetaMap: Record<string, { title: string; summary: string }> = {
|
|||||||
title: 'MCP',
|
title: 'MCP',
|
||||||
summary: 'MCP 服务监听、对外地址与鉴权头。',
|
summary: 'MCP 服务监听、对外地址与鉴权头。',
|
||||||
},
|
},
|
||||||
|
autocode: {
|
||||||
|
title: '代码生成',
|
||||||
|
summary: '代码生成根目录、模块名与目标路径。',
|
||||||
|
},
|
||||||
|
excel: {
|
||||||
|
title: 'Excel',
|
||||||
|
summary: 'Excel 导出目录。',
|
||||||
|
},
|
||||||
'disk-list': {
|
'disk-list': {
|
||||||
title: '磁盘监控',
|
title: '磁盘监控',
|
||||||
summary: '服务器状态页磁盘挂载点来源。',
|
summary: '服务器状态页磁盘挂载点来源。',
|
||||||
|
|||||||
@@ -145,16 +145,20 @@ img {
|
|||||||
color: rgba(255, 255, 255, 0.96) !important;
|
color: rgba(255, 255, 255, 0.96) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav-menu.ant-menu-dark .ant-menu-item-selected,
|
.admin-nav-menu.ant-menu-dark .ant-menu-item-selected {
|
||||||
.admin-nav-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title {
|
|
||||||
background:
|
background:
|
||||||
linear-gradient(135deg, rgba(209, 111, 63, 0.95), rgba(235, 146, 96, 0.86)) !important;
|
linear-gradient(135deg, rgba(209, 111, 63, 0.95), rgba(235, 146, 96, 0.86)) !important;
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
box-shadow: 0 12px 24px rgba(209, 111, 63, 0.22);
|
box-shadow: 0 12px 24px rgba(209, 111, 63, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav-menu.ant-menu-dark .ant-menu-item-selected::after,
|
/* 父级 SubMenu 不显示子项选中时的高亮 */
|
||||||
.admin-nav-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title::after {
|
.admin-nav-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title {
|
||||||
|
color: rgba(255, 255, 255, 0.65) !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav-menu.ant-menu-dark .ant-menu-item-selected::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,37 @@
|
|||||||
import { http } from './http'
|
import { http } from './http'
|
||||||
import type {
|
import type {
|
||||||
|
AIConfigRecord,
|
||||||
|
AIConfigTestResult,
|
||||||
|
AppUserDetail,
|
||||||
|
AppUserRecord,
|
||||||
ApiRecord,
|
ApiRecord,
|
||||||
|
BannerItem,
|
||||||
ApiGroupsPayload,
|
ApiGroupsPayload,
|
||||||
ApiTokenRecord,
|
ApiTokenRecord,
|
||||||
AttachmentCategory,
|
AttachmentCategory,
|
||||||
AuthorityButtonSelection,
|
AuthorityButtonSelection,
|
||||||
Authority,
|
Authority,
|
||||||
CaptchaInfo,
|
CaptchaInfo,
|
||||||
|
CharacterAdminItem,
|
||||||
|
CharacterTraceLookupResult,
|
||||||
|
ConversationDetail,
|
||||||
|
ConversationLogPageResult,
|
||||||
|
CreatorApplicationPageResult,
|
||||||
|
CreatorIdentity,
|
||||||
|
CreatorPageResult,
|
||||||
|
CreatorAdminDetail,
|
||||||
|
CreatorWallet,
|
||||||
|
DashboardStats,
|
||||||
Dictionary,
|
Dictionary,
|
||||||
|
HomepageConfig,
|
||||||
DictionaryDetail,
|
DictionaryDetail,
|
||||||
|
EmailConfigRecord,
|
||||||
InitCheckResult,
|
InitCheckResult,
|
||||||
InitDBPayload,
|
InitDBPayload,
|
||||||
|
InviteCode,
|
||||||
|
InviteCodeUsageItem,
|
||||||
|
AdminUserExpLogItem,
|
||||||
|
LevelExpConfig,
|
||||||
LoginLog,
|
LoginLog,
|
||||||
LoginResult,
|
LoginResult,
|
||||||
McpContent,
|
McpContent,
|
||||||
@@ -22,11 +43,29 @@ import type {
|
|||||||
MenuNode,
|
MenuNode,
|
||||||
OperationRecord,
|
OperationRecord,
|
||||||
PagePayload,
|
PagePayload,
|
||||||
|
PointsConfig,
|
||||||
|
PointsTransaction,
|
||||||
|
RechargePackage,
|
||||||
|
RiskAccountPageResult,
|
||||||
|
RiskEventPageResult,
|
||||||
ServerState,
|
ServerState,
|
||||||
|
ShopItem,
|
||||||
|
UserShopItem,
|
||||||
|
FanLevelConfig,
|
||||||
|
CreatorLevelExpConfig,
|
||||||
|
LeaderboardConfig,
|
||||||
|
SigningApplicationPageResult,
|
||||||
SyncApiPayload,
|
SyncApiPayload,
|
||||||
SysErrorRecord,
|
SysErrorRecord,
|
||||||
SysParam,
|
SysParam,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
|
ActivityPageResult,
|
||||||
|
Activity,
|
||||||
|
ContestSubmission,
|
||||||
|
AnnouncementItem,
|
||||||
|
CreateAnnouncementDto,
|
||||||
|
AdminFeedbackListItem,
|
||||||
|
FeedbackDetailResponse,
|
||||||
} from '@/types/system'
|
} from '@/types/system'
|
||||||
|
|
||||||
export const authApi = {
|
export const authApi = {
|
||||||
@@ -165,6 +204,114 @@ export const systemApi = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const appUserAdminApi = {
|
||||||
|
getUserList(params: { page: number; pageSize: number; keyword?: string; status?: string }) {
|
||||||
|
return http.get<PagePayload<AppUserRecord>>('/admin/user/list', { params })
|
||||||
|
},
|
||||||
|
getUserById(id: number) {
|
||||||
|
return http.get<AppUserRecord>(`/admin/user/${id}`)
|
||||||
|
},
|
||||||
|
updateUser(id: number, payload: { nickName?: string; email?: string; avatar?: string; enable?: boolean }) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/user/${id}`, payload)
|
||||||
|
},
|
||||||
|
deleteUser(id: number) {
|
||||||
|
return http.delete<Record<string, never>>(`/admin/user/${id}`)
|
||||||
|
},
|
||||||
|
resetPassword(id: number, payload: { newPassword: string }) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/user/${id}/reset-password`, payload)
|
||||||
|
},
|
||||||
|
toggleEnable(id: number) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/user/${id}/toggle-enable`)
|
||||||
|
},
|
||||||
|
getUserDetail(id: number) {
|
||||||
|
return http.get<AppUserDetail>(`/admin/user/${id}/detail`)
|
||||||
|
},
|
||||||
|
adjustPoints(userId: number, payload: { amount: number; reason: string; walletType: string }) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/points/adjust/${userId}`, payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiConfigApi = {
|
||||||
|
getList() {
|
||||||
|
return http.get<AIConfigRecord[]>('/admin/ai-config')
|
||||||
|
},
|
||||||
|
getById(id: number) {
|
||||||
|
return http.get<AIConfigRecord>(`/admin/ai-config/${id}`)
|
||||||
|
},
|
||||||
|
create(payload: {
|
||||||
|
name: string
|
||||||
|
provider: string
|
||||||
|
baseUrl: string
|
||||||
|
apiKey?: string
|
||||||
|
defaultModel?: string
|
||||||
|
maxTokens?: number
|
||||||
|
contextLimit?: number
|
||||||
|
presetPrompt?: string
|
||||||
|
settings?: Record<string, unknown>
|
||||||
|
extraHeaders?: Record<string, unknown>
|
||||||
|
pointsPerMessage?: number
|
||||||
|
scope?: string
|
||||||
|
}) {
|
||||||
|
return http.post<AIConfigRecord>('/admin/ai-config', payload)
|
||||||
|
},
|
||||||
|
update(
|
||||||
|
id: number,
|
||||||
|
payload: Partial<{
|
||||||
|
name: string
|
||||||
|
base_url: string
|
||||||
|
api_key: string
|
||||||
|
default_model: string
|
||||||
|
max_tokens: number
|
||||||
|
context_limit: number
|
||||||
|
preset_prompt: string
|
||||||
|
settings: Record<string, unknown>
|
||||||
|
extra_headers: Record<string, unknown>
|
||||||
|
is_active: boolean
|
||||||
|
is_default: boolean
|
||||||
|
scope: string
|
||||||
|
points_per_message: number
|
||||||
|
}>,
|
||||||
|
) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/ai-config/${id}`, payload)
|
||||||
|
},
|
||||||
|
delete(id: number) {
|
||||||
|
return http.delete<Record<string, never>>(`/admin/ai-config/${id}`)
|
||||||
|
},
|
||||||
|
setDefault(id: number) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/ai-config/${id}/set-default`)
|
||||||
|
},
|
||||||
|
setActive(id: number, active: boolean) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/ai-config/${id}/set-active`, { active })
|
||||||
|
},
|
||||||
|
test(id: number, payload: {
|
||||||
|
baseUrl: string
|
||||||
|
apiKey?: string
|
||||||
|
provider?: string
|
||||||
|
model?: string
|
||||||
|
mode?: 'connectivity' | 'message'
|
||||||
|
prompt?: string
|
||||||
|
maxTokens?: number
|
||||||
|
extraHeaders?: Record<string, string>
|
||||||
|
}) {
|
||||||
|
return http.post<AIConfigTestResult>(`/admin/ai-config/${id}/test`, payload)
|
||||||
|
},
|
||||||
|
fetchModels(id: number) {
|
||||||
|
return http.get<{ models: string[] }>(`/admin/ai-config/${id}/fetch-models`)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const emailConfigApi = {
|
||||||
|
get() {
|
||||||
|
return http.get<EmailConfigRecord>('/admin/email-config')
|
||||||
|
},
|
||||||
|
update(payload: EmailConfigRecord) {
|
||||||
|
return http.put<Record<string, never>>('/admin/email-config', payload)
|
||||||
|
},
|
||||||
|
test(to: string) {
|
||||||
|
return http.post<Record<string, never>>('/admin/email-config/test', { to })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const apiRegistryApi = {
|
export const apiRegistryApi = {
|
||||||
getApiList(payload: Record<string, unknown>) {
|
getApiList(payload: Record<string, unknown>) {
|
||||||
return http.post<PagePayload<ApiRecord>>('/api/getApiList', payload)
|
return http.post<PagePayload<ApiRecord>>('/api/getApiList', payload)
|
||||||
@@ -382,3 +529,411 @@ export const mcpApi = {
|
|||||||
return http.post<Record<string, never>>('/mcp/createTool', payload)
|
return http.post<Record<string, never>>('/mcp/createTool', payload)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const characterReviewApi = {
|
||||||
|
getList(params: { page: number; pageSize: number; auditStatus?: string; unpublishStatus?: string; keyword?: string }) {
|
||||||
|
return http.get<PagePayload<CharacterAdminItem>>('/admin/characters', { params })
|
||||||
|
},
|
||||||
|
getById(id: number) {
|
||||||
|
return http.get<CharacterAdminItem>(`/admin/characters/${id}`)
|
||||||
|
},
|
||||||
|
approve(id: number) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/characters/${id}/approve`)
|
||||||
|
},
|
||||||
|
reject(id: number, note: string) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/characters/${id}/reject`, { note })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const creatorAdminApi = {
|
||||||
|
listApplications(params: { page: number; pageSize: number; status?: string; keyword?: string }) {
|
||||||
|
return http.get<CreatorApplicationPageResult>('/admin/creator/applications', { params })
|
||||||
|
},
|
||||||
|
approveApplication(id: number, payload?: { identityId?: number }) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/creator/applications/${id}/approve`, payload)
|
||||||
|
},
|
||||||
|
rejectApplication(id: number, note: string) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/creator/applications/${id}/reject`, { note })
|
||||||
|
},
|
||||||
|
listCreators(params: { page: number; pageSize: number; keyword?: string }) {
|
||||||
|
return http.get<CreatorPageResult>('/admin/creators', { params })
|
||||||
|
},
|
||||||
|
getCreatorDetail(userId: number) {
|
||||||
|
return http.get<CreatorAdminDetail>(`/admin/creators/${userId}/detail`)
|
||||||
|
},
|
||||||
|
revokeCreator(userId: number) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/creators/${userId}/revoke`)
|
||||||
|
},
|
||||||
|
setCreatorMark(userId: number, payload: { markName?: string; clear?: boolean }) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/creators/${userId}/set-mark`, payload)
|
||||||
|
},
|
||||||
|
setCreatorIdentity(userId: number, identityId: number | null) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/creators/${userId}/set-identity`, { identityId })
|
||||||
|
},
|
||||||
|
setContracted(userId: number, contracted: boolean) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/creators/${userId}/set-contracted`, { contracted })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const characterManageApi = {
|
||||||
|
getList(params: { page: number; pageSize: number; auditStatus?: string; unpublishStatus?: string; keyword?: string }) {
|
||||||
|
return characterReviewApi.getList(params)
|
||||||
|
},
|
||||||
|
getById(id: number) {
|
||||||
|
return characterReviewApi.getById(id)
|
||||||
|
},
|
||||||
|
approve(id: number) {
|
||||||
|
return characterReviewApi.approve(id)
|
||||||
|
},
|
||||||
|
reject(id: number, note: string) {
|
||||||
|
return characterReviewApi.reject(id, note)
|
||||||
|
},
|
||||||
|
deleteCharacter(id: number) {
|
||||||
|
return http.delete<Record<string, never>>(`/admin/characters/${id}`)
|
||||||
|
},
|
||||||
|
setRecommended(id: number, value: boolean) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/characters/${id}/set-recommended`, { value })
|
||||||
|
},
|
||||||
|
setHot(id: number, value: boolean) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/characters/${id}/set-hot`, { value })
|
||||||
|
},
|
||||||
|
approveUnpublish(id: number) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/characters/${id}/approve-unpublish`)
|
||||||
|
},
|
||||||
|
rejectUnpublish(id: number, note: string) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/characters/${id}/reject-unpublish`, { note })
|
||||||
|
},
|
||||||
|
directUnpublish(id: number, note: string) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/characters/${id}/unpublish`, { note })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const conversationLogApi = {
|
||||||
|
listConversations(params: { page: number; pageSize: number; keyword?: string }) {
|
||||||
|
return http.get<ConversationLogPageResult>('/admin/conversations', { params })
|
||||||
|
},
|
||||||
|
getConversationDetail(id: number) {
|
||||||
|
return http.get<ConversationDetail>(`/admin/conversations/${id}`)
|
||||||
|
},
|
||||||
|
deleteConversation(id: number) {
|
||||||
|
return http.delete<Record<string, never>>(`/admin/conversations/${id}`)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const riskReviewApi = {
|
||||||
|
getRiskUsers(params: { page: number; pageSize: number; keyword?: string; riskStatus?: string }) {
|
||||||
|
return http.get<RiskAccountPageResult>('/admin/risk/users', { params })
|
||||||
|
},
|
||||||
|
getRiskEvents(params: {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
keyword?: string
|
||||||
|
riskStatus?: string
|
||||||
|
eventType?: string
|
||||||
|
userId?: number
|
||||||
|
}) {
|
||||||
|
return http.get<RiskEventPageResult>('/admin/risk/events', { params })
|
||||||
|
},
|
||||||
|
updateRiskUserStatus(id: number, payload: { status: string; note?: string }) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/risk/users/${id}/status`, payload)
|
||||||
|
},
|
||||||
|
updateRiskEventStatus(id: number, payload: { status: string; note?: string }) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/risk/events/${id}/status`, payload)
|
||||||
|
},
|
||||||
|
lookupTrace(payload: { traceCode?: string; content?: string }) {
|
||||||
|
return http.post<CharacterTraceLookupResult>('/admin/risk/traces/lookup', payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 积分管理 API ──────────────────────────────────────────
|
||||||
|
export const pointsAdminApi = {
|
||||||
|
getConfig() {
|
||||||
|
return http.get<PointsConfig>('/admin/points/config')
|
||||||
|
},
|
||||||
|
updateConfig(payload: PointsConfig) {
|
||||||
|
return http.put<Record<string, never>>('/admin/points/config', payload)
|
||||||
|
},
|
||||||
|
getPackages(params: { page: number; pageSize: number; status?: string }) {
|
||||||
|
return http.get<PagePayload<RechargePackage>>('/admin/points/packages', { params })
|
||||||
|
},
|
||||||
|
createPackage(payload: Omit<RechargePackage, 'id' | 'createdAt' | 'updatedAt'>) {
|
||||||
|
return http.post<RechargePackage>('/admin/points/packages', payload)
|
||||||
|
},
|
||||||
|
updatePackage(id: number, payload: Partial<Omit<RechargePackage, 'id' | 'createdAt' | 'updatedAt'>>) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/points/packages/${id}`, payload)
|
||||||
|
},
|
||||||
|
deletePackage(id: number) {
|
||||||
|
return http.delete<Record<string, never>>(`/admin/points/packages/${id}`)
|
||||||
|
},
|
||||||
|
getTransactions(params: {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
keyword?: string
|
||||||
|
userId?: string
|
||||||
|
type?: string
|
||||||
|
walletType?: string
|
||||||
|
}) {
|
||||||
|
return http.get<PagePayload<PointsTransaction>>('/admin/points/transactions', { params })
|
||||||
|
},
|
||||||
|
adjustPoints(payload: { userId: number; walletType: string; amount: number; description: string }) {
|
||||||
|
return http.post<Record<string, never>>('/admin/points/adjust', payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 邀请码管理 API ────────────────────────────────────────
|
||||||
|
export const inviteCodeAdminApi = {
|
||||||
|
getCodes(params: { page: number; pageSize: number; keyword?: string }) {
|
||||||
|
return http.get<PagePayload<InviteCode>>('/admin/invite/codes', { params })
|
||||||
|
},
|
||||||
|
createCode(payload: { code?: string; maxUsage: number; expiredAt?: string; remark?: string }) {
|
||||||
|
return http.post<InviteCode>('/admin/invite/codes', payload)
|
||||||
|
},
|
||||||
|
updateCode(id: number, payload: { maxUsage?: number; expiredAt?: string; status?: string; remark?: string }) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/invite/codes/${id}`, payload)
|
||||||
|
},
|
||||||
|
deleteCode(id: number) {
|
||||||
|
return http.delete<Record<string, never>>(`/admin/invite/codes/${id}`)
|
||||||
|
},
|
||||||
|
getUsages(params: { page: number; pageSize: number; keyword?: string }) {
|
||||||
|
return http.get<PagePayload<InviteCodeUsageItem>>('/admin/invite/usages', { params })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 用户等级配置 API ──────────────────────────────────────
|
||||||
|
export const levelAdminApi = {
|
||||||
|
// 获取全量等级配置(包含升级经验阈值 + 各事件经验值)
|
||||||
|
getConfig() {
|
||||||
|
return http.get<LevelExpConfig>('/admin/levels/config')
|
||||||
|
},
|
||||||
|
// 更新全量等级配置
|
||||||
|
updateConfig(payload: LevelExpConfig) {
|
||||||
|
return http.put<Record<string, never>>('/admin/levels/config', payload)
|
||||||
|
},
|
||||||
|
// 经验流水记录
|
||||||
|
getExpLogs(params: { page: number; pageSize: number; keyword?: string; userId?: number; sourceType?: string }) {
|
||||||
|
return http.get<PagePayload<AdminUserExpLogItem>>('/admin/levels/exp-logs', { params })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 首页配置 API ─────────────────────────────────────────
|
||||||
|
export const homepageConfigApi = {
|
||||||
|
get() {
|
||||||
|
return http.get<HomepageConfig>('/admin/homepage/config')
|
||||||
|
},
|
||||||
|
update(payload: { banners: BannerItem[]; recommendedCharIds: string; recommendedCreatorIds: string; isEnabled: boolean }) {
|
||||||
|
return http.put<Record<string, never>>('/admin/homepage/config', payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 创作者钱包 API ────────────────────────────────────────
|
||||||
|
export const creatorWalletAdminApi = {
|
||||||
|
getWallets(params: { page: number; pageSize: number; keyword?: string }) {
|
||||||
|
return http.get<PagePayload<CreatorWallet>>('/admin/creator/wallets', { params })
|
||||||
|
},
|
||||||
|
adjustWallet(payload: { userId: number; amount: number; description: string }) {
|
||||||
|
return http.post<Record<string, never>>('/admin/creator/wallets/adjust', payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 创作者身份管理 API ────────────────────────────────────
|
||||||
|
export const creatorIdentityAdminApi = {
|
||||||
|
list() {
|
||||||
|
return http.get<CreatorIdentity[]>('/admin/creator-identities')
|
||||||
|
},
|
||||||
|
create(payload: { name: string; description?: string; color?: string; sort?: number }) {
|
||||||
|
return http.post<CreatorIdentity>('/admin/creator-identities', payload)
|
||||||
|
},
|
||||||
|
update(id: number, payload: { name: string; description?: string; color?: string; isActive: boolean; sort?: number }) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/creator-identities/${id}`, payload)
|
||||||
|
},
|
||||||
|
delete(id: number) {
|
||||||
|
return http.delete<Record<string, never>>(`/admin/creator-identities/${id}`)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 关于我们配置 API ────────────────────────────────────────
|
||||||
|
export interface AppAboutConfig {
|
||||||
|
id?: number
|
||||||
|
appName: string
|
||||||
|
version: string
|
||||||
|
slogan: string
|
||||||
|
description: string
|
||||||
|
privacyUrl: string
|
||||||
|
termsUrl: string
|
||||||
|
supportEmail: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appAboutApi = {
|
||||||
|
get() {
|
||||||
|
return http.get<AppAboutConfig>('/admin/system/about')
|
||||||
|
},
|
||||||
|
update(payload: Omit<AppAboutConfig, 'id'>) {
|
||||||
|
return http.put<Record<string, never>>('/admin/system/about', payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 粉丝等级配置 API ──────────────────────────────────────────
|
||||||
|
export const fanLevelAdminApi = {
|
||||||
|
getList() {
|
||||||
|
return http.get<FanLevelConfig[]>('/admin/fan-level-configs')
|
||||||
|
},
|
||||||
|
update(level: number, payload: Omit<FanLevelConfig, 'level'>) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/fan-level-configs/${level}`, payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 创作者等级配置 API ────────────────────────────────────────
|
||||||
|
export const creatorLevelAdminApi = {
|
||||||
|
getConfig() {
|
||||||
|
return http.get<CreatorLevelExpConfig>('/admin/creator-level-config')
|
||||||
|
},
|
||||||
|
updateConfig(payload: CreatorLevelExpConfig) {
|
||||||
|
return http.put<Record<string, never>>('/admin/creator-level-config', payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 签约申请管理 API ──────────────────────────────────────────
|
||||||
|
export const signingAdminApi = {
|
||||||
|
getList(params: { page: number; pageSize: number; status?: string; keyword?: string }) {
|
||||||
|
return http.get<SigningApplicationPageResult>('/admin/creator/signing-applications', { params })
|
||||||
|
},
|
||||||
|
approve(id: number) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/creator/signing-applications/${id}/approve`)
|
||||||
|
},
|
||||||
|
reject(id: number, note: string) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/creator/signing-applications/${id}/reject`, { note })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 榜单配置 API ────────────────────────────────────────────
|
||||||
|
export const leaderboardAdminApi = {
|
||||||
|
getConfig() {
|
||||||
|
return http.get<LeaderboardConfig>('/admin/characters/leaderboard-config')
|
||||||
|
},
|
||||||
|
updateConfig(payload: LeaderboardConfig) {
|
||||||
|
return http.put<Record<string, never>>('/admin/characters/leaderboard-config', payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 仪表盘统计 API ─────────────────────────────────────────
|
||||||
|
export const dashboardApi = {
|
||||||
|
getStats() {
|
||||||
|
return http.get<DashboardStats>('/admin/dashboard/stats')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
export function getMediaUrl(url: string | undefined | null): string {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) return url
|
||||||
|
const baseURL = (import.meta.env.VITE_BASE_API as string) || ''
|
||||||
|
try {
|
||||||
|
const origin = new URL(baseURL).origin
|
||||||
|
return `${origin}/${url.startsWith('/') ? url.slice(1) : url}`
|
||||||
|
} catch {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadFile(file: File): Promise<string> {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('file', file)
|
||||||
|
const res = await http.post<{ file: { url: string } }>(
|
||||||
|
'/fileUploadAndDownload/upload',
|
||||||
|
form,
|
||||||
|
{ headers: { 'Content-Type': 'multipart/form-data' } },
|
||||||
|
)
|
||||||
|
return getMediaUrl(res.data.file.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 兑换商店管理 API ────────────────────────────────────────
|
||||||
|
export const shopAdminApi = {
|
||||||
|
listItems(params: { page: number; pageSize: number; category?: string; status?: string; keyword?: string }) {
|
||||||
|
return http.get<PagePayload<ShopItem>>('/admin/shop/items', { params })
|
||||||
|
},
|
||||||
|
createItem(payload: Omit<ShopItem, 'id' | 'createdAt'>) {
|
||||||
|
return http.post<ShopItem>('/admin/shop/items', payload)
|
||||||
|
},
|
||||||
|
updateItem(id: number, payload: Partial<Omit<ShopItem, 'id' | 'createdAt'>>) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/shop/items/${id}`, payload)
|
||||||
|
},
|
||||||
|
deleteItem(id: number) {
|
||||||
|
return http.delete<Record<string, never>>(`/admin/shop/items/${id}`)
|
||||||
|
},
|
||||||
|
listUserItems(params: { page: number; pageSize: number; status?: string }) {
|
||||||
|
return http.get<PagePayload<UserShopItem>>('/admin/shop/user-items', { params })
|
||||||
|
},
|
||||||
|
reviewUserItem(id: number, payload: { status: string; reviewNote?: string }) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/shop/user-items/${id}/review`, payload)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 活动管理 API ──────────────────────────────────────────
|
||||||
|
export const activityAdminApi = {
|
||||||
|
list(params: { page: number; pageSize: number; type?: string; status?: string; keyword?: string }) {
|
||||||
|
return http.get<ActivityPageResult>('/admin/activities', { params })
|
||||||
|
},
|
||||||
|
create(payload: Omit<Activity, 'id' | 'createdAt' | 'participantCount'>) {
|
||||||
|
return http.post<Activity>('/admin/activities', payload)
|
||||||
|
},
|
||||||
|
update(id: number, payload: Partial<Activity>) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/activities/${id}`, payload)
|
||||||
|
},
|
||||||
|
delete(id: number) {
|
||||||
|
return http.delete<Record<string, never>>(`/admin/activities/${id}`)
|
||||||
|
},
|
||||||
|
publish(id: number) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/activities/${id}/publish`)
|
||||||
|
},
|
||||||
|
end(id: number) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/activities/${id}/end`)
|
||||||
|
},
|
||||||
|
settle(id: number) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/activities/${id}/settle`)
|
||||||
|
},
|
||||||
|
getParticipants(id: number, params: { page: number; pageSize: number }) {
|
||||||
|
return http.get(`/admin/activities/${id}/participants`, { params })
|
||||||
|
},
|
||||||
|
getSubmissions(id: number, params: { page: number; pageSize: number; status?: string }) {
|
||||||
|
return http.get<{ list: ContestSubmission[]; total: number }>(`/admin/activities/${id}/submissions`, { params })
|
||||||
|
},
|
||||||
|
reviewSubmission(id: number, subId: number, payload: { status: string; reviewNote?: string }) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/activities/${id}/submissions/${subId}/review`, payload)
|
||||||
|
},
|
||||||
|
pinSubmission(id: number, subId: number, pin: boolean) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/activities/${id}/submissions/${subId}/pin`, { pin })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 公告管理 API ──────────────────────────────────────────
|
||||||
|
export const announcementAdminApi = {
|
||||||
|
getList(params: { page: number; pageSize: number; keyword?: string; status?: string }) {
|
||||||
|
return http.get<PagePayload<AnnouncementItem>>('/admin/announcement/list', { params })
|
||||||
|
},
|
||||||
|
getById(id: number) {
|
||||||
|
return http.get<AnnouncementItem>(`/admin/announcement/${id}`)
|
||||||
|
},
|
||||||
|
create(payload: CreateAnnouncementDto) {
|
||||||
|
return http.post<AnnouncementItem>('/admin/announcement', payload)
|
||||||
|
},
|
||||||
|
update(id: number, payload: Partial<CreateAnnouncementDto>) {
|
||||||
|
return http.put<Record<string, never>>(`/admin/announcement/${id}`, payload)
|
||||||
|
},
|
||||||
|
delete(id: number) {
|
||||||
|
return http.delete<Record<string, never>>(`/admin/announcement/${id}`)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 用户反馈工单管理 API ──────────────────────────────────────
|
||||||
|
export const feedbackAdminApi = {
|
||||||
|
getList(params: { page: number; pageSize: number; status?: string; keyword?: string; userId?: number }) {
|
||||||
|
return http.get<PagePayload<AdminFeedbackListItem>>('/admin/feedback/list', { params })
|
||||||
|
},
|
||||||
|
getById(id: number) {
|
||||||
|
return http.get<FeedbackDetailResponse>(`/admin/feedback/${id}`)
|
||||||
|
},
|
||||||
|
reply(id: number, payload: { content?: string; images?: string[] }) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/feedback/${id}/reply`, payload)
|
||||||
|
},
|
||||||
|
close(id: number) {
|
||||||
|
return http.post<Record<string, never>>(`/admin/feedback/${id}/close`)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,161 @@ export type UserInfo = BaseEntity & {
|
|||||||
originSetting?: Record<string, unknown>
|
originSetting?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AppUserRecord = BaseEntity & {
|
||||||
|
uuid?: string
|
||||||
|
username: string
|
||||||
|
nickName?: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
avatar?: string
|
||||||
|
status?: string
|
||||||
|
enable: boolean
|
||||||
|
isAdmin?: boolean
|
||||||
|
isCreator?: boolean
|
||||||
|
level?: number
|
||||||
|
experience?: number
|
||||||
|
chatCount?: number
|
||||||
|
messageCount?: number
|
||||||
|
lastLoginAt?: string | null
|
||||||
|
lastLoginIp?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppUserDetail = {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
email: string
|
||||||
|
avatar: string
|
||||||
|
enable: boolean
|
||||||
|
isCreator: boolean
|
||||||
|
level: number
|
||||||
|
experience: number
|
||||||
|
createdAt: string
|
||||||
|
lastLoginAt: string | null
|
||||||
|
// 积分
|
||||||
|
normalPoints: number
|
||||||
|
paidBalance: number
|
||||||
|
freeBalance: number
|
||||||
|
creatorPoints: number
|
||||||
|
totalRecharged: number
|
||||||
|
totalConsumed: number
|
||||||
|
// 邀请
|
||||||
|
inviteCode: string
|
||||||
|
inviteCount: number
|
||||||
|
invitedBy: string
|
||||||
|
// 统计
|
||||||
|
chatCount: number
|
||||||
|
messageCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RiskReviewStatus = 'none' | 'pending_review' | 'reviewed'
|
||||||
|
|
||||||
|
export type RiskAccountRecord = {
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
avatar: string
|
||||||
|
enable: boolean
|
||||||
|
riskScore: number
|
||||||
|
riskEventCount: number
|
||||||
|
lastRiskAt: string | null
|
||||||
|
lastRiskType: string
|
||||||
|
lastRiskReason: string
|
||||||
|
riskStatus: RiskReviewStatus
|
||||||
|
riskNote: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RiskAccountPageResult = {
|
||||||
|
list: RiskAccountRecord[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RiskEventRecord = {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
characterId?: number | null
|
||||||
|
characterName: string
|
||||||
|
conversationId?: number | null
|
||||||
|
eventType: string
|
||||||
|
riskLabel: string
|
||||||
|
riskScore: number
|
||||||
|
excerpt: string
|
||||||
|
detail: string
|
||||||
|
sourceIp: string
|
||||||
|
userAgent: string
|
||||||
|
status: RiskReviewStatus
|
||||||
|
reviewedBy?: number | null
|
||||||
|
reviewedAt?: string | null
|
||||||
|
reviewNote: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RiskEventPageResult = {
|
||||||
|
list: RiskEventRecord[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CharacterTraceLookupResult = {
|
||||||
|
traceCode: string
|
||||||
|
traceType: string
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
characterId: number
|
||||||
|
characterName: string
|
||||||
|
conversationId?: number | null
|
||||||
|
sourceIp: string
|
||||||
|
userAgent: string
|
||||||
|
carrierField: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AIConfigRecord = BaseEntity & {
|
||||||
|
name: string
|
||||||
|
provider: string
|
||||||
|
baseUrl: string
|
||||||
|
apiKey?: string
|
||||||
|
defaultModel?: string
|
||||||
|
maxTokens?: number
|
||||||
|
contextLimit?: number
|
||||||
|
models?: string[] | null
|
||||||
|
presetPrompt?: string
|
||||||
|
settings?: Record<string, unknown> | null
|
||||||
|
extraHeaders?: Record<string, unknown> | null
|
||||||
|
isActive: boolean
|
||||||
|
isDefault: boolean
|
||||||
|
scope?: string
|
||||||
|
pointsPerMessage?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EmailConfigRecord = {
|
||||||
|
id?: number
|
||||||
|
host?: string
|
||||||
|
port?: number
|
||||||
|
from?: string
|
||||||
|
nickname?: string
|
||||||
|
secret?: string
|
||||||
|
isSsl?: boolean
|
||||||
|
isLoginAuth?: boolean
|
||||||
|
to?: string
|
||||||
|
enabled?: boolean
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AIConfigTestResult = {
|
||||||
|
success: boolean
|
||||||
|
latency: number
|
||||||
|
error?: string
|
||||||
|
content?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type MenuMeta = {
|
export type MenuMeta = {
|
||||||
title: string
|
title: string
|
||||||
icon?: string
|
icon?: string
|
||||||
@@ -307,8 +462,403 @@ export type McpToolTemplateRequest = {
|
|||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CharacterAdminItem = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
avatar: string
|
||||||
|
creator: string
|
||||||
|
creatorNickName: string
|
||||||
|
displayIntro: string
|
||||||
|
tags: string[] | null
|
||||||
|
auditStatus: 'none' | 'pending' | 'approved' | 'rejected' | 'update_pending'
|
||||||
|
auditNote: string
|
||||||
|
auditRequestedAt: string | null
|
||||||
|
auditReviewedAt: string | null
|
||||||
|
isPublic: boolean
|
||||||
|
isRecommended: boolean
|
||||||
|
isHot: boolean
|
||||||
|
unpublishStatus: 'none' | 'pending'
|
||||||
|
unpublishNote: string
|
||||||
|
unpublishRequestedAt: string | null
|
||||||
|
deleteStatus: 'none' | 'pending'
|
||||||
|
deleteNote: string
|
||||||
|
deleteRequestedAt: string | null
|
||||||
|
useCount: number
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CharacterAdminPageResult = {
|
||||||
|
list: CharacterAdminItem[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreatorApplicationItem = {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
avatar: string
|
||||||
|
status: 'pending' | 'approved' | 'rejected'
|
||||||
|
reason: string
|
||||||
|
note: string
|
||||||
|
reviewedAt: string | null
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreatorApplicationPageResult = {
|
||||||
|
list: CreatorApplicationItem[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreatorItem = {
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
avatar: string
|
||||||
|
email: string
|
||||||
|
level?: number
|
||||||
|
isContracted: boolean
|
||||||
|
creatorMarkId?: number | null
|
||||||
|
creatorMarkName?: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreatorPageResult = {
|
||||||
|
list: CreatorItem[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreatorAdminDetail = {
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
avatar: string
|
||||||
|
email: string
|
||||||
|
level: number
|
||||||
|
experience: number
|
||||||
|
isCreator: boolean
|
||||||
|
creatorMarkId?: number | null
|
||||||
|
creatorMarkName?: string
|
||||||
|
createdAt: string
|
||||||
|
lastLoginAt: string | null
|
||||||
|
walletBalance: number
|
||||||
|
walletEarned: number
|
||||||
|
walletRedeemed: number
|
||||||
|
normalBalance: number
|
||||||
|
characterCount: number
|
||||||
|
publishedCount: number
|
||||||
|
chatCount: number
|
||||||
|
messageCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConversationLogItem = {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
characterId: number
|
||||||
|
characterName: string
|
||||||
|
title: string
|
||||||
|
aiProvider: string
|
||||||
|
model: string
|
||||||
|
messageCount: number
|
||||||
|
tokenCount: number
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConversationLogPageResult = {
|
||||||
|
list: ConversationLogItem[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageItem = {
|
||||||
|
id: number
|
||||||
|
role: 'user' | 'assistant' | 'system'
|
||||||
|
content: string
|
||||||
|
tokenCount: number
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConversationDetail = ConversationLogItem & {
|
||||||
|
messages: MessageItem[]
|
||||||
|
}
|
||||||
|
|
||||||
export type AutoMcpToolRequest = McpToolTemplateRequest
|
export type AutoMcpToolRequest = McpToolTemplateRequest
|
||||||
|
|
||||||
|
// ── 创作者身份 ─────────────────────────────────────────────
|
||||||
|
export type CreatorIdentity = {
|
||||||
|
ID: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
color: string
|
||||||
|
icon?: string
|
||||||
|
isActive: boolean
|
||||||
|
sort: number
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 积分管理 ──────────────────────────────────────────────
|
||||||
|
export type InviteRewardTier = {
|
||||||
|
minCount: number // 邀请人已邀请 >= minCount 人时启用此档(0 = 基础档)
|
||||||
|
inviterReward: number // 邀请人获得积分
|
||||||
|
inviteeReward: number // 被邀请人获得积分
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PointsConfig = {
|
||||||
|
// 基础
|
||||||
|
pointDeductionPerMessage: number
|
||||||
|
rechargeRatio: number
|
||||||
|
normalRegisterReward: number
|
||||||
|
// 邀请(基础固定奖励,被阶梯覆盖时仍作备用)
|
||||||
|
inviteRegisterReward: number
|
||||||
|
inviteRechargeCommission: number
|
||||||
|
inviteRewardTiers: InviteRewardTier[] // 阶梯奖励配置
|
||||||
|
// 创作者
|
||||||
|
creatorRewardConsumePoints: number
|
||||||
|
creatorRewardPoints: number
|
||||||
|
creatorRewardRatio: number
|
||||||
|
// 星光币兑换灵感点
|
||||||
|
creatorToNormalRate: number // 1 星光币 = N 灵感点
|
||||||
|
creatorToNormalMinExchange: number // 最小兑换量(星光币)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RechargePackage = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
price: number
|
||||||
|
points: number
|
||||||
|
bonusPoints: number
|
||||||
|
status: 'active' | 'inactive'
|
||||||
|
sort: number
|
||||||
|
description: string
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PointsTransaction = {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
walletType: string
|
||||||
|
type: string
|
||||||
|
amount: number
|
||||||
|
balance: number
|
||||||
|
description: string
|
||||||
|
orderId: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 邀请码管理 ────────────────────────────────────────────
|
||||||
|
export type InviteCode = {
|
||||||
|
id: number
|
||||||
|
code: string
|
||||||
|
creatorId: number
|
||||||
|
creatorUsername: string
|
||||||
|
creatorNickName: string
|
||||||
|
maxUsage: number
|
||||||
|
usedCount: number
|
||||||
|
expiredAt: string | null
|
||||||
|
status: 'active' | 'inactive'
|
||||||
|
remark: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InviteRewardDetail = {
|
||||||
|
rewardAmount: number
|
||||||
|
rewardDescription: string
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InviteCodeUsageItem = {
|
||||||
|
id: number
|
||||||
|
inviteCode: string
|
||||||
|
inviterUserId: number
|
||||||
|
inviterUsername: string
|
||||||
|
inviterNickName: string
|
||||||
|
inviteeUserId: number
|
||||||
|
inviteeUsername: string
|
||||||
|
inviteeNickName: string
|
||||||
|
usedAt: string
|
||||||
|
ipAddress: string
|
||||||
|
inviterReward?: InviteRewardDetail
|
||||||
|
inviteeReward?: InviteRewardDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 用户等级 ──────────────────────────────────────────────
|
||||||
|
// LevelConfig 保留供旧代码引用,不对应实际后端 CRUD(后端已改为统一 config)
|
||||||
|
export type LevelConfig = {
|
||||||
|
id: number
|
||||||
|
level: number
|
||||||
|
requiredExp: number
|
||||||
|
name: string
|
||||||
|
color: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LevelExpConfig 与后端 LevelConfigResponse / UpdateLevelConfigRequest 完全对齐
|
||||||
|
export type LevelExpConfig = {
|
||||||
|
levelUpExpRules: number[] // 升级所需经验阈值数组,levelUpExpRules[0] = Lv1→Lv2 所需经验
|
||||||
|
eventExpRegister: number
|
||||||
|
eventExpDailyLogin: number
|
||||||
|
eventExpSendMessage: number
|
||||||
|
eventExpCreateCharacter: number
|
||||||
|
eventExpCreateComment: number
|
||||||
|
eventExpCharacterLiked: number
|
||||||
|
dailyCapSendMessage: number
|
||||||
|
dailyCapCreateComment: number
|
||||||
|
dailyCapCharacterLiked: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminUserExpLogItem = {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
sourceType: string
|
||||||
|
sourceRef: string
|
||||||
|
pointTxId?: number
|
||||||
|
pointsDelta: number
|
||||||
|
expDelta: number
|
||||||
|
expBefore: number
|
||||||
|
expAfter: number
|
||||||
|
levelBefore: number
|
||||||
|
levelAfter: number
|
||||||
|
remark: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 创作者钱包 ────────────────────────────────────────────
|
||||||
|
export type CreatorWallet = {
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
avatar: string
|
||||||
|
balance: number
|
||||||
|
totalEarned: number
|
||||||
|
totalRedeemed: number
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 首页配置 ──────────────────────────────────────────────
|
||||||
|
export type BannerItem = {
|
||||||
|
imageUrl: string
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
jumpUrl: string
|
||||||
|
sort: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HomepageConfig = {
|
||||||
|
id?: number
|
||||||
|
banners: BannerItem[]
|
||||||
|
recommendedCharIds: string
|
||||||
|
recommendedCreatorIds: string
|
||||||
|
isEnabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 兑换商店 ────────────────────────────────────────────────
|
||||||
|
export type ShopItem = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
category: string
|
||||||
|
scope: string
|
||||||
|
unlockType: string
|
||||||
|
currencyType: string
|
||||||
|
price: number
|
||||||
|
needReview: boolean
|
||||||
|
status: string
|
||||||
|
sort: number
|
||||||
|
coverImage: string
|
||||||
|
resourceUrl: string
|
||||||
|
minLevel: number
|
||||||
|
stockLimit: number
|
||||||
|
exchangeCount: number
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserShopItem = {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
shopItemId: number
|
||||||
|
status: string
|
||||||
|
reviewNote: string
|
||||||
|
exchangedAt: string
|
||||||
|
itemName: string
|
||||||
|
coverImage: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 粉丝等级配置 ────────────────────────────────────────────
|
||||||
|
export type FanLevelConfig = {
|
||||||
|
level: number
|
||||||
|
name: string
|
||||||
|
minFollowDays: number
|
||||||
|
minPoints: number
|
||||||
|
minConvCount: number
|
||||||
|
degradeLevel: number
|
||||||
|
degradeCondition: string
|
||||||
|
degradeConvCount: number
|
||||||
|
degradeDays: number
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 创作者等级配置 ──────────────────────────────────────────
|
||||||
|
export type CreatorLevelExpConfig = {
|
||||||
|
levelUpExpRules: number[]
|
||||||
|
events: {
|
||||||
|
publishCharacter: number
|
||||||
|
characterUsed: number
|
||||||
|
characterLiked: number
|
||||||
|
characterFavorited: number
|
||||||
|
followCreator: number
|
||||||
|
}
|
||||||
|
dailyCaps: {
|
||||||
|
publishCharacter: number
|
||||||
|
characterUsed: number
|
||||||
|
characterLiked: number
|
||||||
|
characterFavorited: number
|
||||||
|
followCreator: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 榜单配置 ────────────────────────────────────────────────
|
||||||
|
export type LeaderboardConfig = {
|
||||||
|
weeklySortBy: 'likes' | 'plays' | 'favorites'
|
||||||
|
monthlySortBy: 'likes' | 'plays' | 'favorites'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 签约申请 ──────────────────────────────────────────────
|
||||||
|
export type SigningApplicationItem = {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
avatar: string
|
||||||
|
reason: string
|
||||||
|
portfolio: string
|
||||||
|
status: 'pending' | 'approved' | 'rejected'
|
||||||
|
note: string
|
||||||
|
reviewedAt: string | null
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SigningApplicationPageResult = {
|
||||||
|
list: SigningApplicationItem[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
export type ModuleStatus = 'ready' | 'partial' | 'planned'
|
export type ModuleStatus = 'ready' | 'partial' | 'planned'
|
||||||
|
|
||||||
export type ModuleDescriptor = {
|
export type ModuleDescriptor = {
|
||||||
@@ -320,3 +870,261 @@ export type ModuleDescriptor = {
|
|||||||
features: string[]
|
features: string[]
|
||||||
endpoints: string[]
|
endpoints: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 活动管理类型 ──────────────────────────────────────────
|
||||||
|
export type ActivityType = 'task' | 'contest' | 'checkin' | 'ranking' | 'lottery'
|
||||||
|
export type ActivityStatus = 'draft' | 'active' | 'ended' | 'cancelled'
|
||||||
|
|
||||||
|
export interface RewardTier {
|
||||||
|
rankFrom: number
|
||||||
|
rankTo: number
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RewardPool {
|
||||||
|
currency: 'inspiration' | 'stardust'
|
||||||
|
amount: number
|
||||||
|
tiers?: RewardTier[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContestExtraConfig {
|
||||||
|
requireSubmissionReview: boolean
|
||||||
|
voteEnabled: boolean
|
||||||
|
voteCostType: 'free' | 'inspiration' | 'stardust'
|
||||||
|
voteCostAmount: number
|
||||||
|
maxVotesPerUser: number
|
||||||
|
maxSubmissionsPerUser: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RankingDimension {
|
||||||
|
metric: string
|
||||||
|
weight: number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RankingExtraConfig {
|
||||||
|
dimensions: RankingDimension[]
|
||||||
|
scoreFormula: string
|
||||||
|
rankBy: 'character' | 'user'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LotteryExtraConfig {
|
||||||
|
uiStyle: 'wheel' | 'grid' | 'flip'
|
||||||
|
drawCostType: 'inspiration' | 'stardust' | 'condition'
|
||||||
|
drawCostAmount: number
|
||||||
|
maxDrawsPerUser: number
|
||||||
|
freeDraws: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckinMilestone {
|
||||||
|
day: number
|
||||||
|
rewardType: string
|
||||||
|
rewardAmount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckinExtraConfig {
|
||||||
|
cycleDays: number
|
||||||
|
milestones: CheckinMilestone[]
|
||||||
|
dailyRewardType: string
|
||||||
|
dailyRewardAmount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActivityTask {
|
||||||
|
id?: number
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
taskType: string
|
||||||
|
targetCount: number
|
||||||
|
rewardType: string
|
||||||
|
rewardAmount: number
|
||||||
|
rewardAmountMin?: number
|
||||||
|
rewardAmountMax?: number
|
||||||
|
rewardExp?: number
|
||||||
|
rewardExpMin?: number
|
||||||
|
rewardExpMax?: number
|
||||||
|
rewardTargetId?: number
|
||||||
|
sortOrder: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LotteryPrize {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
prizeType: 'inspiration' | 'stardust' | 'badge' | 'physical'
|
||||||
|
prizeValue: number
|
||||||
|
totalCount: number
|
||||||
|
weight: number
|
||||||
|
image: string
|
||||||
|
sortOrder: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Activity {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
type: ActivityType
|
||||||
|
status: ActivityStatus
|
||||||
|
coverImage: string
|
||||||
|
description: string
|
||||||
|
rules: string
|
||||||
|
startAt: string
|
||||||
|
endAt: string
|
||||||
|
maxParticipants: number
|
||||||
|
isFeatured: boolean
|
||||||
|
rewardPool: RewardPool
|
||||||
|
extraConfig: ContestExtraConfig | RankingExtraConfig | LotteryExtraConfig | CheckinExtraConfig | null
|
||||||
|
participantCount: number
|
||||||
|
createdAt: string
|
||||||
|
tasks?: ActivityTask[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActivityPageResult {
|
||||||
|
list: Activity[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContestSubmission {
|
||||||
|
id: number
|
||||||
|
activityId: number
|
||||||
|
userId: number
|
||||||
|
nickname: string
|
||||||
|
characterId: number
|
||||||
|
characterName: string
|
||||||
|
characterCover: string
|
||||||
|
voteCount: number
|
||||||
|
isPinned: boolean
|
||||||
|
reviewStatus: 'pending' | 'approved' | 'rejected'
|
||||||
|
reviewNote: string
|
||||||
|
submittedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 公告管理 ──────────────────────────────────────────────
|
||||||
|
export type AnnouncementItem = {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
contentFormat: 'markdown' | 'html'
|
||||||
|
status: 'on' | 'off'
|
||||||
|
startAt?: string
|
||||||
|
endAt?: string
|
||||||
|
priority: number
|
||||||
|
popupOnHome: boolean
|
||||||
|
allowClose: boolean
|
||||||
|
allowSnoozeToday: boolean
|
||||||
|
createdBy: number
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateAnnouncementDto = {
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
contentFormat: 'markdown' | 'html'
|
||||||
|
status: 'on' | 'off'
|
||||||
|
startAt?: string
|
||||||
|
endAt?: string
|
||||||
|
priority: number
|
||||||
|
popupOnHome: boolean
|
||||||
|
allowClose: boolean
|
||||||
|
allowSnoozeToday: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 用户反馈工单 ────────────────────────────────────────────
|
||||||
|
export type FeedbackMessageItem = {
|
||||||
|
id: number
|
||||||
|
senderType: 'user' | 'admin'
|
||||||
|
senderId: number
|
||||||
|
senderName: string
|
||||||
|
senderAvatar: string
|
||||||
|
content: string
|
||||||
|
images: string[]
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FeedbackDetailResponse = {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
status: 'open' | 'closed'
|
||||||
|
createdAt: string
|
||||||
|
closedAt?: string
|
||||||
|
messages: FeedbackMessageItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminFeedbackListItem = {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
status: 'open' | 'closed'
|
||||||
|
userId: number
|
||||||
|
userNickname: string
|
||||||
|
userAvatar: string
|
||||||
|
lastMessageAt: string
|
||||||
|
adminUnreadCount: number
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 仪表盘统计 ──────────────────────────────────────────────
|
||||||
|
export type DashboardUserStats = {
|
||||||
|
total: number
|
||||||
|
creatorCount: number
|
||||||
|
onlineCount: number
|
||||||
|
todayNew: number
|
||||||
|
weekNew: number
|
||||||
|
totalMessages: number
|
||||||
|
totalChats: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DashboardCharacterStats = {
|
||||||
|
total: number
|
||||||
|
published: number
|
||||||
|
pendingReview: number
|
||||||
|
todayNew: number
|
||||||
|
weekNew: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DashboardTopCreator = {
|
||||||
|
userId: number
|
||||||
|
nickName: string
|
||||||
|
avatar: string
|
||||||
|
charCount: number
|
||||||
|
totalUseCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DashboardCreatorStats = {
|
||||||
|
total: number
|
||||||
|
contracted: number
|
||||||
|
todayNew: number
|
||||||
|
weekNew: number
|
||||||
|
pendingApplications: number
|
||||||
|
topCreators: DashboardTopCreator[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DashboardFeedbackPendingItem = {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
userNickname: string
|
||||||
|
userAvatar: string
|
||||||
|
unreadCount: number
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DashboardFeedbackStats = {
|
||||||
|
total: number
|
||||||
|
open: number
|
||||||
|
adminUnread: number
|
||||||
|
closedToday: number
|
||||||
|
pendingItems: DashboardFeedbackPendingItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DashboardSystemStats = {
|
||||||
|
totalConversations: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DashboardStats = {
|
||||||
|
users: DashboardUserStats
|
||||||
|
characters: DashboardCharacterStats
|
||||||
|
creators: DashboardCreatorStats
|
||||||
|
feedback: DashboardFeedbackStats
|
||||||
|
system: DashboardSystemStats
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user