Compare commits

..

22 Commits

Author SHA1 Message Date
jiangwel 12b51f2b2b fix: 修复前端样式问题 2025-11-12 11:06:19 +08:00
jiangwel 1ff013ac47 chore: 更新模型设置模式及相关配置,移除冗余迁移功能 2025-11-11 16:48:15 +08:00
jiangwel c88bd58b6b chore: 添加模型设置模式及相关配置 2025-11-10 21:07:18 +08:00
jiangwel bddd515df2 feat: 添加关闭弹窗时未应用更改的确认提示 2025-11-10 21:07:18 +08:00
jiangwel b1c564ff2a feat(model): 添加模型设置模式切换功能
实现模型设置模式(手动/自动)切换功能,包括:
1. 新增SettingRepository及相关数据库操作
2. 添加模型模式相关常量定义
3. 实现模式切换API接口及业务逻辑
4. 添加数据库迁移脚本初始化模型设置
5. 更新swagger文档

feat(模型): 添加百智云自动模式配置功能

实现百智云模型自动模式的API Key和对话模型配置功能
重构相关代码,将BaiZhiCloud重命名为AutoMode
更新迁移脚本和模型设置接口

feat: 添加模型模式设置和自动模式支持

feat(系统设置): 重构系统设置模块,迁移至新表结构

- 新增system_settings表及相关迁移文件
- 将原setting表功能迁移至system_setting表
- 更新模型设置相关逻辑,支持自动模式配置
- 优化Makefile构建目标,分离pro和pro_consumer
- 更新API文档,修正模型设置相关接口路径

feat: 前端支持自动配置模型

feat: 在教程添加模型配置

feat: 在教程中添加配置模型

chore

feat: 修改前端样式

feat: 修改前端样式

feat: 前端流程跑通

feat: 跑通前端流程

feat: 优化接口
2025-11-10 21:07:18 +08:00
xiaomakuaiz e4dbfcb9fb
Merge pull request #1492 from KuaiYu95/fe/contribute
feat: 支持批量重新学习失败的文档
2025-11-10 19:33:56 +08:00
xiaomakuaiz 40c395400d
Merge pull request #1483 from coltea/feat-restudy
feat restudy
2025-11-10 19:33:43 +08:00
coltea b7cdec0d4a feat restudy 2025-11-10 14:13:09 +08:00
xiaomakuaiz 44126e0a11
Merge pull request #1493 from guanweiwang/hotfix/bug
fix: 拖拽组件输入文字 失焦问题
2025-11-10 12:14:36 +08:00
Gavan 3375fcb643 fix: 拖拽组件输入文字 失焦问题 2025-11-10 11:29:42 +08:00
yu.kuai ef3bae6336 feat: 支持批量重新学习失败的文档 2025-11-10 11:15:36 +08:00
xiaomakuaiz d9d3bc4911
Merge pull request #1491 from KuaiYu95/fe/editor-ui
feat: 编辑器部分组件样式更新
2025-11-10 10:37:29 +08:00
xiaomakuaiz 546062470b
Merge pull request #1478 from xiaomakuaiz/feature/summary-optimization
Feature/summary optimization
2025-11-10 10:37:10 +08:00
yu.kuai 5a3b23ac75 feat: 编辑器部分组件样式更新 2025-11-10 10:12:54 +08:00
xiaomakuaiz 35cd94e342
Merge pull request #1484 from guanweiwang/pref/landing_drag
pref: 优化和bug修复
2025-11-07 17:06:15 +08:00
monkeycode-ai f7c0fe273b Improve summary optimization with simplified aggregation
优化摘要生成逻辑:
1. 将chunk token限制从16KB提升到30KB,更合理地利用模型上下文
2. 简化摘要聚合逻辑,移除复杂的分批聚合,直接合并所有summaries
3. 保留fallback机制,当最终摘要生成失败时返回已聚合的摘要

这些改进确保了:
- 长文档能够更充分地被摘要(30KB vs 16KB)
- 代码更简洁,避免不必要的迭代聚合和额外LLM调用
- 即使最终摘要失败也能返回有用的结果

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-07 14:21:36 +08:00
Gavan 0175624c84 pref: 删除无用代码,提取公共拖拽函数,修复 footer 2025-11-07 12:00:48 +08:00
monkeycode-ai a6f4688b88 Run goimports on llm summary
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-06 20:07:30 +08:00
monkeycode-ai 575f51f0ea Simplify final summary aggregation
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-06 19:52:03 +08:00
monkeycode-ai 83f6853716 Iteratively reduce summary chunks
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-06 19:21:26 +08:00
monkeycode-ai 3dae8e8d01 Raise summary chunk limit to 16k
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-06 19:17:27 +08:00
monkeycode-ai 2e1e1848c4 Adjust summary chunking and concurrency
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-06 19:09:15 +08:00
96 changed files with 2359 additions and 3558 deletions

View File

@ -56,3 +56,11 @@ type NodePermissionEditReq struct {
type NodePermissionEditResp struct {
}
type NodeRestudyReq struct {
NodeIds []string `json:"node_ids" validate:"required,min=1"`
KbId string `json:"kb_id" validate:"required"`
}
type NodeRestudyResp struct {
}

View File

@ -8,12 +8,12 @@ package main
import (
"github.com/chaitin/panda-wiki/config"
mq2 "github.com/chaitin/panda-wiki/handler/mq"
mq3 "github.com/chaitin/panda-wiki/handler/mq"
"github.com/chaitin/panda-wiki/log"
"github.com/chaitin/panda-wiki/mq"
cache2 "github.com/chaitin/panda-wiki/repo/cache"
ipdb2 "github.com/chaitin/panda-wiki/repo/ipdb"
mq3 "github.com/chaitin/panda-wiki/repo/mq"
mq2 "github.com/chaitin/panda-wiki/repo/mq"
pg2 "github.com/chaitin/panda-wiki/repo/pg"
"github.com/chaitin/panda-wiki/store/cache"
"github.com/chaitin/panda-wiki/store/ipdb"
@ -49,11 +49,18 @@ func createApp() (*App, error) {
modelRepository := pg2.NewModelRepository(db, logger)
promptRepo := pg2.NewPromptRepo(db, logger)
llmUsecase := usecase.NewLLMUsecase(configConfig, ragService, conversationRepository, knowledgeBaseRepository, nodeRepository, modelRepository, promptRepo, logger)
ragmqHandler, err := mq2.NewRAGMQHandler(mqConsumer, logger, ragService, nodeRepository, knowledgeBaseRepository, llmUsecase, modelRepository)
mqProducer, err := mq.NewMQProducer(configConfig, logger)
if err != nil {
return nil, err
}
ragDocUpdateHandler, err := mq2.NewRagDocUpdateHandler(mqConsumer, logger, nodeRepository)
ragRepository := mq2.NewRAGRepository(mqProducer)
systemSettingRepo := pg2.NewSystemSettingRepo(db, logger)
modelUsecase := usecase.NewModelUsecase(modelRepository, nodeRepository, ragRepository, ragService, logger, configConfig, knowledgeBaseRepository, systemSettingRepo)
ragmqHandler, err := mq3.NewRAGMQHandler(mqConsumer, logger, ragService, nodeRepository, knowledgeBaseRepository, llmUsecase, modelUsecase)
if err != nil {
return nil, err
}
ragDocUpdateHandler, err := mq3.NewRagDocUpdateHandler(mqConsumer, logger, nodeRepository)
if err != nil {
return nil, err
}
@ -71,24 +78,17 @@ func createApp() (*App, error) {
geoRepo := cache2.NewGeoCache(cacheCache, db, logger)
authRepo := pg2.NewAuthRepo(db, logger, cacheCache)
statUseCase := usecase.NewStatUseCase(statRepository, nodeRepository, conversationRepository, appRepository, ipAddressRepo, geoRepo, authRepo, knowledgeBaseRepository, logger)
mqProducer, err := mq.NewMQProducer(configConfig, logger)
if err != nil {
return nil, err
}
ragRepository := mq3.NewRAGRepository(mqProducer)
userRepository := pg2.NewUserRepository(db, logger)
minioClient, err := s3.NewMinioClient(configConfig)
if err != nil {
return nil, err
}
systemSettingRepo := pg2.NewSystemSettingRepo(db, logger)
modelUsecase := usecase.NewModelUsecase(modelRepository, nodeRepository, ragRepository, ragService, logger, configConfig, knowledgeBaseRepository, systemSettingRepo)
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo, modelUsecase)
cronHandler, err := mq2.NewStatCronHandler(logger, statRepository, statUseCase, nodeUsecase)
cronHandler, err := mq3.NewStatCronHandler(logger, statRepository, statUseCase, nodeUsecase)
if err != nil {
return nil, err
}
mqHandlers := &mq2.MQHandlers{
mqHandlers := &mq3.MQHandlers{
RAGMQHandler: ragmqHandler,
RagDocUpdateHandler: ragDocUpdateHandler,
StatCronHandler: cronHandler,
@ -107,6 +107,6 @@ func createApp() (*App, error) {
type App struct {
MQConsumer mq.MQConsumer
Config *config.Config
MQHandlers *mq2.MQHandlers
StatCronHandler *mq2.CronHandler
MQHandlers *mq3.MQHandlers
StatCronHandler *mq3.CronHandler
}

View File

@ -70,11 +70,9 @@ func createApp() (*App, error) {
}
migrationNodeVersion := fns.NewMigrationNodeVersion(logger, nodeUsecase, knowledgeBaseUsecase, ragRepository)
migrationCreateBotAuth := fns.NewMigrationCreateBotAuth(logger)
migrationAddModelSettingMode := fns.NewMigrationAddModelSettingMode(logger, knowledgeBaseUsecase)
migrationFuncs := &migration.MigrationFuncs{
NodeMigration: migrationNodeVersion,
BotAuthMigration: migrationCreateBotAuth,
ModelSettingMigration: migrationAddModelSettingMode,
NodeMigration: migrationNodeVersion,
BotAuthMigration: migrationCreateBotAuth,
}
manager, err := migration.NewManager(db, logger, migrationFuncs)
if err != nil {

View File

@ -3,10 +3,10 @@ package consts
type AutoModeDefaultModel string
const (
AutoModeDefaultChatModel AutoModeDefaultModel = "qwen-vl-max-latest"
AutoModeDefaultChatModel AutoModeDefaultModel = "deepseek-chat"
AutoModeDefaultEmbeddingModel AutoModeDefaultModel = "bge-m3"
AutoModeDefaultRerankModel AutoModeDefaultModel = "bge-reranker-v2-m3"
AutoModeDefaultAnalysisModel AutoModeDefaultModel = "qwen2.5-3b"
AutoModeDefaultAnalysisModel AutoModeDefaultModel = "qwen2.5-3b-instruct"
AutoModeDefaultAnalysisVLModel AutoModeDefaultModel = "qwen3-vl-max"
)
@ -33,3 +33,7 @@ const (
ModelSettingModeManual ModelSettingMode = "manual"
ModelSettingModeAuto ModelSettingMode = "auto"
)
const (
AutoModeBaseURL = "https://model-square.app.baizhi.cloud/v1"
)

View File

@ -0,0 +1,7 @@
package consts
type SystemSettingKey string
const (
SystemSettingModelMode SystemSettingKey = "model_setting_mode"
)

View File

@ -2198,6 +2198,58 @@ const docTemplate = `{
}
}
},
"/api/v1/node/restudy": {
"post": {
"security": [
{
"bearerAuth": []
}
],
"description": "文档重新学习",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Node"
],
"summary": "文档重新学习",
"operationId": "v1-NodeRestudy",
"parameters": [
{
"description": "para",
"name": "param",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.NodeRestudyReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/v1.NodeRestudyResp"
}
}
}
]
}
}
}
}
},
"/api/v1/node/summary": {
"post": {
"security": [
@ -4033,6 +4085,17 @@ const docTemplate = `{
"LicenseEditionEnterprise"
]
},
"consts.ModelSettingMode": {
"type": "string",
"enum": [
"manual",
"auto"
],
"x-enum-varnames": [
"ModelSettingModeManual",
"ModelSettingModeAuto"
]
},
"consts.NodeAccessPerm": {
"type": "string",
"enum": [
@ -6298,9 +6361,17 @@ const docTemplate = `{
"description": "自定义对话模型名称",
"type": "string"
},
"is_manual_embedding_updated": {
"description": "手动模式下嵌入模型是否更新",
"type": "boolean"
},
"mode": {
"description": "模式: manual 或 auto",
"type": "string"
"allOf": [
{
"$ref": "#/definitions/consts.ModelSettingMode"
}
]
}
}
},
@ -8400,6 +8471,28 @@ const docTemplate = `{
}
}
},
"v1.NodeRestudyReq": {
"type": "object",
"required": [
"kb_id",
"node_ids"
],
"properties": {
"kb_id": {
"type": "string"
},
"node_ids": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
}
}
},
"v1.NodeRestudyResp": {
"type": "object"
},
"v1.ResetPasswordReq": {
"type": "object",
"required": [

View File

@ -2191,6 +2191,58 @@
}
}
},
"/api/v1/node/restudy": {
"post": {
"security": [
{
"bearerAuth": []
}
],
"description": "文档重新学习",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Node"
],
"summary": "文档重新学习",
"operationId": "v1-NodeRestudy",
"parameters": [
{
"description": "para",
"name": "param",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.NodeRestudyReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/v1.NodeRestudyResp"
}
}
}
]
}
}
}
}
},
"/api/v1/node/summary": {
"post": {
"security": [
@ -4026,6 +4078,17 @@
"LicenseEditionEnterprise"
]
},
"consts.ModelSettingMode": {
"type": "string",
"enum": [
"manual",
"auto"
],
"x-enum-varnames": [
"ModelSettingModeManual",
"ModelSettingModeAuto"
]
},
"consts.NodeAccessPerm": {
"type": "string",
"enum": [
@ -6291,9 +6354,17 @@
"description": "自定义对话模型名称",
"type": "string"
},
"is_manual_embedding_updated": {
"description": "手动模式下嵌入模型是否更新",
"type": "boolean"
},
"mode": {
"description": "模式: manual 或 auto",
"type": "string"
"allOf": [
{
"$ref": "#/definitions/consts.ModelSettingMode"
}
]
}
}
},
@ -8393,6 +8464,28 @@
}
}
},
"v1.NodeRestudyReq": {
"type": "object",
"required": [
"kb_id",
"node_ids"
],
"properties": {
"kb_id": {
"type": "string"
},
"node_ids": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
}
}
},
"v1.NodeRestudyResp": {
"type": "object"
},
"v1.ResetPasswordReq": {
"type": "object",
"required": [

View File

@ -131,6 +131,14 @@ definitions:
- LicenseEditionFree
- LicenseEditionContributor
- LicenseEditionEnterprise
consts.ModelSettingMode:
enum:
- manual
- auto
type: string
x-enum-varnames:
- ModelSettingModeManual
- ModelSettingModeAuto
consts.NodeAccessPerm:
enum:
- open
@ -1636,9 +1644,13 @@ definitions:
chat_model:
description: 自定义对话模型名称
type: string
is_manual_embedding_updated:
description: 手动模式下嵌入模型是否更新
type: boolean
mode:
allOf:
- $ref: '#/definitions/consts.ModelSettingMode'
description: '模式: manual 或 auto'
type: string
type: object
domain.ModelType:
enum:
@ -3019,6 +3031,21 @@ definitions:
$ref: '#/definitions/domain.NodeGroupDetail'
type: array
type: object
v1.NodeRestudyReq:
properties:
kb_id:
type: string
node_ids:
items:
type: string
minItems: 1
type: array
required:
- kb_id
- node_ids
type: object
v1.NodeRestudyResp:
type: object
v1.ResetPasswordReq:
properties:
id:
@ -4455,6 +4482,36 @@ paths:
summary: Recommend Nodes
tags:
- node
/api/v1/node/restudy:
post:
consumes:
- application/json
description: 文档重新学习
operationId: v1-NodeRestudy
parameters:
- description: para
in: body
name: param
required: true
schema:
$ref: '#/definitions/v1.NodeRestudyReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
$ref: '#/definitions/v1.NodeRestudyResp'
type: object
security:
- bearerAuth: []
summary: 文档重新学习
tags:
- Node
/api/v1/node/summary:
post:
consumes:

View File

@ -175,10 +175,3 @@ type SwitchModeReq struct {
type SwitchModeResp struct {
Message string `json:"message"`
}
// ModelModeSetting 模型配置结构体
type ModelModeSetting struct {
Mode string `json:"mode"` // 模式: manual 或 auto
AutoModeAPIKey string `json:"auto_mode_api_key"` // 百智云 API Key
ChatModel string `json:"chat_model"` // 自定义对话模型名称
}

View File

@ -2,18 +2,28 @@ package domain
import (
"time"
)
const (
SystemSettingModelMode = "model_setting_mode"
"github.com/chaitin/panda-wiki/consts"
)
// table: settings
type SystemSetting struct {
ID int `json:"id" gorm:"primary_key"`
Key string `json:"key"`
Value []byte `json:"value" gorm:"type:jsonb"` // JSON string
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID int `json:"id" gorm:"primary_key"`
Key consts.SystemSettingKey `json:"key"`
Value []byte `json:"value" gorm:"type:jsonb"` // JSON string
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (SystemSetting) TableName() string {
return "system_settings"
}
// ModelModeSetting 模型配置结构体
type ModelModeSetting struct {
Mode consts.ModelSettingMode `json:"mode"` // 模式: manual 或 auto
AutoModeAPIKey string `json:"auto_mode_api_key"` // 百智云 API Key
ChatModel string `json:"chat_model"` // 自定义对话模型名称
IsManualEmbeddingUpdated bool `json:"is_manual_embedding_updated"` // 手动模式下嵌入模型是否更新
}

View File

@ -20,20 +20,19 @@ type RAGMQHandler struct {
rag rag.RAGService
nodeRepo *pg.NodeRepository
kbRepo *pg.KnowledgeBaseRepository
modelRepo *pg.ModelRepository
llmUsecase *usecase.LLMUsecase
modelUsecase *usecase.ModelUsecase
}
func NewRAGMQHandler(consumer mq.MQConsumer, logger *log.Logger, rag rag.RAGService, nodeRepo *pg.NodeRepository, kbRepo *pg.KnowledgeBaseRepository, llmUsecase *usecase.LLMUsecase, modelRepo *pg.ModelRepository) (*RAGMQHandler, error) {
func NewRAGMQHandler(consumer mq.MQConsumer, logger *log.Logger, rag rag.RAGService, nodeRepo *pg.NodeRepository, kbRepo *pg.KnowledgeBaseRepository, llmUsecase *usecase.LLMUsecase, modelUsecase *usecase.ModelUsecase) (*RAGMQHandler, error) {
h := &RAGMQHandler{
consumer: consumer,
logger: logger.WithModule("mq.rag"),
rag: rag,
nodeRepo: nodeRepo,
kbRepo: kbRepo,
llmUsecase: llmUsecase,
modelRepo: modelRepo,
consumer: consumer,
logger: logger.WithModule("mq.rag"),
rag: rag,
nodeRepo: nodeRepo,
kbRepo: kbRepo,
llmUsecase: llmUsecase,
modelUsecase: modelUsecase,
}
if err := consumer.RegisterHandler(domain.VectorTaskTopic, h.HandleNodeContentVectorRequest); err != nil {
return nil, err

View File

@ -214,7 +214,7 @@ func (h *ModelHandler) GetProviderSupportedModelList(c echo.Context) error {
return h.NewResponseWithData(c, models)
}
// switch mode
// SwitchMode
//
// @Summary switch mode
// @Description switch model mode between manual and auto
@ -244,7 +244,7 @@ func (h *ModelHandler) SwitchMode(c echo.Context) error {
return h.NewResponseWithData(c, resp)
}
// get model mode setting
// GetModelModeSetting
//
// @Summary get model mode setting
// @Description get current model mode setting including mode, API key and chat model
@ -260,7 +260,7 @@ func (h *ModelHandler) GetModelModeSetting(c echo.Context) error {
// 如果获取失败,返回默认值(手动模式)
h.logger.Warn("failed to get model mode setting, return default", log.Error(err))
defaultSetting := domain.ModelModeSetting{
Mode: string(consts.ModelSettingModeManual),
Mode: consts.ModelSettingModeManual,
AutoModeAPIKey: "",
ChatModel: "",
}

View File

@ -47,6 +47,7 @@ func NewNodeHandler(
group.POST("/batch_move", h.BatchMoveNode)
group.GET("/recommend_nodes", h.RecommendNodes)
group.POST("/restudy", h.NodeRestudy)
// node permission
group.GET("/permission", h.NodePermission)
@ -384,3 +385,32 @@ func (h *NodeHandler) NodePermissionEdit(c echo.Context) error {
}
return h.NewResponseWithData(c, nil)
}
// NodeRestudy 文档重新学习
//
// @Tags Node
// @Summary 文档重新学习
// @Description 文档重新学习
// @ID v1-NodeRestudy
// @Accept json
// @Produce json
// @Security bearerAuth
// @Param param body v1.NodeRestudyReq true "para"
// @Success 200 {object} domain.Response{data=v1.NodeRestudyResp}
// @Router /api/v1/node/restudy [post]
func (h *NodeHandler) NodeRestudy(c echo.Context) error {
var req v1.NodeRestudyReq
if err := c.Bind(&req); err != nil {
return h.NewResponseWithError(c, "request params is invalid", err)
}
if err := c.Validate(req); err != nil {
return h.NewResponseWithError(c, "validate request params failed", err)
}
if err := h.usecase.NodeRestudy(c.Request().Context(), &req); err != nil {
return h.NewResponseWithError(c, "node restudy failed", err)
}
return h.NewResponseWithData(c, nil)
}

View File

@ -1,80 +0,0 @@
package fns
import (
"context"
"encoding/json"
"fmt"
"gorm.io/gorm"
"github.com/chaitin/panda-wiki/consts"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/log"
"github.com/chaitin/panda-wiki/usecase"
)
type MigrationAddModelSettingMode struct {
Name string
logger *log.Logger
kbUsecase *usecase.KnowledgeBaseUsecase
}
func NewMigrationAddModelSettingMode(logger *log.Logger, kbUsecase *usecase.KnowledgeBaseUsecase) *MigrationAddModelSettingMode {
return &MigrationAddModelSettingMode{
Name: "0003_add_model_setting_mode",
logger: logger,
kbUsecase: kbUsecase,
}
}
func (m *MigrationAddModelSettingMode) Execute(tx *gorm.DB) error {
ctx := context.Background()
// 检查是否已存在该设置
var existingSetting domain.SystemSetting
err := tx.WithContext(ctx).Where("key = ?", domain.SystemSettingModelMode).First(&existingSetting).Error
if err != nil && err != gorm.ErrRecordNotFound {
return fmt.Errorf("failed to check existing model_setting_mode setting: %w", err)
}
// 如果记录不存在,则创建新记录
if err == gorm.ErrRecordNotFound {
// 老用户设置手动模型, 新用户设置自动模式
mode := string(consts.ModelSettingModeManual)
kbList, err := m.kbUsecase.GetKnowledgeBaseList(ctx)
if err != nil {
return fmt.Errorf("get kb list failed: %w", err)
}
if len(kbList) == 0 {
mode = string(consts.ModelSettingModeAuto)
}
// 定义model_setting_mode的值结构
modelSettingValue := map[string]interface{}{
"mode": mode,
"auto_mode_api_key": "", // 默认没有api key
"chat_model": "", // 对话模型,默认为空
}
// 将值转换为JSON字节数组
valueBytes, err := json.Marshal(modelSettingValue)
if err != nil {
return fmt.Errorf("failed to marshal model setting value: %w", err)
}
// 创建setting记录
setting := &domain.SystemSetting{
Key: domain.SystemSettingModelMode,
Value: valueBytes,
Description: "Model setting mode configuration",
}
if err := tx.WithContext(ctx).Create(setting).Error; err != nil {
return fmt.Errorf("failed to create model_setting_mode setting: %w", err)
}
m.logger.Info("successfully created model_setting_mode setting")
} else {
m.logger.Info("model_setting_mode setting already exists, skipping creation")
}
return nil
}

View File

@ -7,5 +7,4 @@ import (
var ProviderSet = wire.NewSet(
NewMigrationNodeVersion,
NewMigrationCreateBotAuth,
NewMigrationAddModelSettingMode,
)

View File

@ -5,9 +5,8 @@ import (
)
type MigrationFuncs struct {
NodeMigration *fns.MigrationNodeVersion
BotAuthMigration *fns.MigrationCreateBotAuth
ModelSettingMigration *fns.MigrationAddModelSettingMode
NodeMigration *fns.MigrationNodeVersion
BotAuthMigration *fns.MigrationCreateBotAuth
}
func (mf *MigrationFuncs) GetMigrationFuncs() []MigrationFunc {
@ -20,9 +19,5 @@ func (mf *MigrationFuncs) GetMigrationFuncs() []MigrationFunc {
Name: mf.BotAuthMigration.Name,
Fn: mf.BotAuthMigration.Execute,
})
funcs = append(funcs, MigrationFunc{
Name: mf.ModelSettingMigration.Name,
Fn: mf.ModelSettingMigration.Execute,
})
return funcs
}

View File

@ -16,11 +16,11 @@ type SystemSettingRepo struct {
func NewSystemSettingRepo(db *pg.DB, logger *log.Logger) *SystemSettingRepo {
return &SystemSettingRepo{
db: db,
logger: logger.WithModule("repo.pg.setting"),
logger: logger.WithModule("repo.pg.system_setting"),
}
}
func (r *SystemSettingRepo) GetModelModeSetting(ctx context.Context, key string) (*domain.SystemSetting, error) {
func (r *SystemSettingRepo) GetSystemSetting(ctx context.Context, key string) (*domain.SystemSetting, error) {
var setting domain.SystemSetting
result := r.db.WithContext(ctx).Where("key = ?", key).First(&setting)
if result.Error != nil {
@ -30,6 +30,6 @@ func (r *SystemSettingRepo) GetModelModeSetting(ctx context.Context, key string)
return &setting, nil
}
func (r *SystemSettingRepo) UpdateModelModeSetting(ctx context.Context, key, value string) error {
func (r *SystemSettingRepo) UpdateSystemSetting(ctx context.Context, key, value string) error {
return r.db.WithContext(ctx).Model(&domain.SystemSetting{}).Where("key = ?", key).Update("value", value).Error
}

View File

@ -1,11 +0,0 @@
-- Create settings table
CREATE TABLE IF NOT EXISTS system_settings (
id SERIAL PRIMARY KEY,
key TEXT NOT NULL,
value JSONB NOT NULL,
description TEXT,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_system_settings_key ON settings (key);

View File

@ -0,0 +1,30 @@
-- Create settings table
CREATE TABLE IF NOT EXISTS system_settings (
id SERIAL PRIMARY KEY,
key TEXT NOT NULL,
value JSONB NOT NULL,
description TEXT,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_uniq_system_settings_key ON system_settings(key);
-- Insert model_setting_mode setting
-- If there are existing knowledge bases, set mode to 'manual', otherwise set to 'auto'
INSERT INTO system_settings (key, value, description)
SELECT
'model_setting_mode',
jsonb_build_object(
'mode', CASE
WHEN EXISTS (SELECT 1 FROM knowledge_bases LIMIT 1) THEN 'manual'
ELSE 'auto'
END,
'auto_mode_api_key', '',
'chat_model', '',
'is_manual_embedding_updated', false
),
'Model setting mode configuration'
WHERE NOT EXISTS (
SELECT 1 FROM system_settings WHERE key = 'model_setting_mode'
);

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
"math"
"slices"
"strings"
"time"
@ -16,8 +15,6 @@ import (
"github.com/cloudwego/eino/schema"
"github.com/pkoukk/tiktoken-go"
"github.com/samber/lo"
"github.com/samber/lo/parallel"
"golang.org/x/sync/semaphore"
"github.com/chaitin/panda-wiki/config"
"github.com/chaitin/panda-wiki/domain"
@ -39,6 +36,11 @@ type LLMUsecase struct {
modelkit *modelkit.ModelKit
}
const (
summaryChunkTokenLimit = 30720 // 30KB tokens per chunk
summaryMaxChunks = 4 // max chunks to process for summary
)
func NewLLMUsecase(config *config.Config, rag rag.RAGService, conversationRepo *pg.ConversationRepository, kbRepo *pg.KnowledgeBaseRepository, nodeRepo *pg.NodeRepository, modelRepo *pg.ModelRepository, promptRepo *pg.PromptRepo, logger *log.Logger) *LLMUsecase {
tiktoken.SetBpeLoader(&utils.Localloader{})
modelkit := modelkit.NewModelKit(logger.Logger)
@ -197,52 +199,59 @@ func (u *LLMUsecase) SummaryNode(ctx context.Context, model *domain.Model, name,
return "", err
}
chunks, err := u.SplitByTokenLimit(content, int(math.Floor(1024*32*0.95)))
chunks, err := u.SplitByTokenLimit(content, summaryChunkTokenLimit)
if err != nil {
return "", err
}
sem := semaphore.NewWeighted(int64(10))
summaries := parallel.Map(chunks, func(chunk string, _ int) string {
if err := sem.Acquire(ctx, 1); err != nil {
u.logger.Error("Failed to acquire semaphore for chunk: ", log.Error(err))
return ""
}
defer sem.Release(1)
summary, err := u.Generate(ctx, chatModel, []*schema.Message{
{
Role: "system",
Content: "你是文档总结助手请根据文档内容总结出文档的摘要。摘要是纯文本应该简洁明了不要超过160个字。",
},
{
Role: "user",
Content: fmt.Sprintf("文档名称:%s\n文档内容%s", name, chunk),
},
})
if err != nil {
u.logger.Error("Failed to generate summary for chunk: ", log.Error(err))
return ""
}
if strings.HasPrefix(summary, "<think>") {
// remove <think> body </think>
endIndex := strings.Index(summary, "</think>")
if endIndex != -1 {
summary = strings.TrimSpace(summary[endIndex+8:]) // 8 is length of "</think>"
}
}
return summary
})
// 使用lo.Filter处理错误
defeatSummary := lo.Filter(summaries, func(summary string, index int) bool {
return summary == ""
})
if len(defeatSummary) > 0 {
return "", fmt.Errorf("failed to generate summaries for all chunks: %d/%d", len(defeatSummary), len(chunks))
if len(chunks) > summaryMaxChunks {
u.logger.Debug("trim summary chunks for large document", log.String("node", name), log.Int("original_chunks", len(chunks)), log.Int("used_chunks", summaryMaxChunks))
chunks = chunks[:summaryMaxChunks]
}
contents, err := u.SplitByTokenLimit(strings.Join(summaries, "\n\n"), int(math.Floor(1024*32*0.95)))
if err != nil {
return "", err
summaries := make([]string, 0, len(chunks))
for idx, chunk := range chunks {
summary, err := u.requestSummary(ctx, chatModel, name, chunk)
if err != nil {
u.logger.Error("Failed to generate summary for chunk", log.Int("chunk_index", idx), log.Error(err))
continue
}
if summary == "" {
u.logger.Warn("Empty summary returned for chunk", log.Int("chunk_index", idx))
continue
}
summaries = append(summaries, summary)
}
if len(summaries) == 0 {
return "", fmt.Errorf("failed to generate summary for document %s", name)
}
// Join all summaries and generate final summary
joined := strings.Join(summaries, "\n\n")
finalSummary, err := u.requestSummary(ctx, chatModel, name, joined)
if err != nil {
u.logger.Error("Failed to generate final summary, using aggregated summaries", log.Error(err))
// Fallback: return the joined summaries directly
if len(joined) > 500 {
return joined[:500] + "...", nil
}
return joined, nil
}
return finalSummary, nil
}
func (u *LLMUsecase) trimThinking(summary string) string {
if !strings.HasPrefix(summary, "<think>") {
return summary
}
endIndex := strings.Index(summary, "</think>")
if endIndex == -1 {
return summary
}
return strings.TrimSpace(summary[endIndex+len("</think>"):])
}
func (u *LLMUsecase) requestSummary(ctx context.Context, chatModel model.BaseChatModel, name, content string) (string, error) {
summary, err := u.Generate(ctx, chatModel, []*schema.Message{
{
Role: "system",
@ -250,20 +259,13 @@ func (u *LLMUsecase) SummaryNode(ctx context.Context, model *domain.Model, name,
},
{
Role: "user",
Content: fmt.Sprintf("文档名称:%s\n文档内容%s", name, contents[0]),
Content: fmt.Sprintf("文档名称:%s\n文档内容%s", name, content),
},
})
if err != nil {
return "", err
}
if strings.HasPrefix(summary, "<think>") {
// remove <think> body </think>
endIndex := strings.Index(summary, "</think>")
if endIndex != -1 {
summary = strings.TrimSpace(summary[endIndex+8:]) // 8 is length of "</think>"
}
}
return summary, nil
return strings.TrimSpace(u.trimThinking(summary)), nil
}
func (u *LLMUsecase) SplitByTokenLimit(text string, maxTokens int) ([]string, error) {

View File

@ -47,9 +47,19 @@ func NewModelUsecase(modelRepo *pg.ModelRepository, nodeRepo *pg.NodeRepository,
}
func (u *ModelUsecase) Create(ctx context.Context, model *domain.Model) error {
var updatedEmbeddingModel bool
if model.Type == domain.ModelTypeEmbedding {
updatedEmbeddingModel = true
}
if err := u.modelRepo.Create(ctx, model); err != nil {
return err
}
// 模型更新成功后,如果更新嵌入模型,则触发记录更新
if updatedEmbeddingModel {
if _, err := u.updateModeSettingConfig(ctx, "", "", "", true); err != nil {
return err
}
}
return nil
}
@ -98,9 +108,19 @@ func (u *ModelUsecase) TriggerUpsertRecords(ctx context.Context) error {
}
func (u *ModelUsecase) Update(ctx context.Context, req *domain.UpdateModelReq) error {
var updatedEmbeddingModel bool
if req.Type == domain.ModelTypeEmbedding {
updatedEmbeddingModel = true
}
if err := u.modelRepo.Update(ctx, req); err != nil {
return err
}
// 模型更新成功后,如果更新嵌入模型,则触发记录更新
if updatedEmbeddingModel {
if _, err := u.updateModeSettingConfig(ctx, "", "", "", true); err != nil {
return err
}
}
return nil
}
@ -111,7 +131,7 @@ func (u *ModelUsecase) GetChatModel(ctx context.Context) (*domain.Model, error)
if err != nil {
u.logger.Error("get model mode setting failed, use manual mode", log.Error(err))
}
if err == nil && modelModeSetting.Mode == string(consts.ModelSettingModeAuto) && modelModeSetting.AutoModeAPIKey != "" {
if err == nil && modelModeSetting.Mode == consts.ModelSettingModeAuto && modelModeSetting.AutoModeAPIKey != "" {
modelName := modelModeSetting.ChatModel
if modelName == "" {
modelName = string(consts.AutoModeDefaultChatModel)
@ -120,7 +140,7 @@ func (u *ModelUsecase) GetChatModel(ctx context.Context) (*domain.Model, error)
Model: modelName,
Type: domain.ModelTypeChat,
IsActive: true,
BaseURL: "https://model-square.app.baizhi.cloud/v1",
BaseURL: consts.AutoModeBaseURL,
APIKey: modelModeSetting.AutoModeAPIKey,
Provider: domain.ModelProviderBrandBaiZhiCloud,
}
@ -156,7 +176,7 @@ func (u *ModelUsecase) SwitchMode(ctx context.Context, req *domain.SwitchModeReq
check, err := u.modelkit.CheckModel(ctx, &modelkitDomain.CheckModelReq{
Provider: string(domain.ModelProviderBrandBaiZhiCloud),
Model: modelName,
BaseURL: "https://model-square.app.baizhi.cloud/v1",
BaseURL: consts.AutoModeBaseURL,
APIKey: req.AutoModeAPIKey,
Type: string(domain.ModelTypeChat),
})
@ -180,18 +200,29 @@ func (u *ModelUsecase) SwitchMode(ctx context.Context, req *domain.SwitchModeReq
}
}
modelModeSetting, err := u.updateAutoModeSettingConfig(ctx, req.Mode, req.AutoModeAPIKey, req.ChatModel)
oldModelModeSetting, err := u.GetModelModeSetting(ctx)
if err != nil {
return err
}
return u.updateRAGModelsByMode(ctx, req.Mode, modelModeSetting.AutoModeAPIKey)
var isResetEmbeddingUpdateFlag = true
// 只有切换手动模式时重置isManualEmbeddingUpdated为false
if req.Mode == string(consts.ModelSettingModeManual) {
isResetEmbeddingUpdateFlag = false
}
modelModeSetting, err := u.updateModeSettingConfig(ctx, req.Mode, req.AutoModeAPIKey, req.ChatModel, isResetEmbeddingUpdateFlag)
if err != nil {
return err
}
return u.updateRAGModelsByMode(ctx, req.Mode, modelModeSetting.AutoModeAPIKey, oldModelModeSetting)
}
// updateAutoModeSettingConfig 读取当前设置并更新,然后持久化
func (u *ModelUsecase) updateAutoModeSettingConfig(ctx context.Context, mode, apiKey, chatModel string) (*domain.ModelModeSetting, error) {
// updateModeSettingConfig 读取当前设置并更新,然后持久化
func (u *ModelUsecase) updateModeSettingConfig(ctx context.Context, mode, apiKey, chatModel string, isManualEmbeddingUpdated bool) (*domain.ModelModeSetting, error) {
// 读取当前设置
setting, err := u.systemSettingRepo.GetModelModeSetting(ctx, domain.SystemSettingModelMode)
setting, err := u.systemSettingRepo.GetSystemSetting(ctx, string(consts.SystemSettingModelMode))
if err != nil {
return nil, fmt.Errorf("failed to get current model setting: %w", err)
}
@ -209,22 +240,24 @@ func (u *ModelUsecase) updateAutoModeSettingConfig(ctx context.Context, mode, ap
config.ChatModel = chatModel
}
if mode != "" {
config.Mode = mode
config.Mode = consts.ModelSettingMode(mode)
}
config.IsManualEmbeddingUpdated = isManualEmbeddingUpdated
// 持久化设置
updatedValue, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("failed to marshal updated model setting: %w", err)
}
if err := u.systemSettingRepo.UpdateModelModeSetting(ctx, domain.SystemSettingModelMode, string(updatedValue)); err != nil {
if err := u.systemSettingRepo.UpdateSystemSetting(ctx, string(consts.SystemSettingModelMode), string(updatedValue)); err != nil {
return nil, fmt.Errorf("failed to update model setting: %w", err)
}
return &config, nil
}
func (u *ModelUsecase) GetModelModeSetting(ctx context.Context) (domain.ModelModeSetting, error) {
setting, err := u.systemSettingRepo.GetModelModeSetting(ctx, domain.SystemSettingModelMode)
setting, err := u.systemSettingRepo.GetSystemSetting(ctx, string(consts.SystemSettingModelMode))
if err != nil {
return domain.ModelModeSetting{}, fmt.Errorf("failed to get model mode setting: %w", err)
}
@ -240,7 +273,14 @@ func (u *ModelUsecase) GetModelModeSetting(ctx context.Context) (domain.ModelMod
}
// updateRAGModelsByMode 根据模式更新 RAG 模型embedding、rerank、analysis、analysisVL
func (u *ModelUsecase) updateRAGModelsByMode(ctx context.Context, mode string, autoModeAPIKey string) error {
func (u *ModelUsecase) updateRAGModelsByMode(ctx context.Context, mode, autoModeAPIKey string, oldModelModeSetting domain.ModelModeSetting) error {
var isTriggerUpsertRecords = true
// 手动切换到手动模式, 根据IsManualEmbeddingUpdated字段决定
if oldModelModeSetting.Mode == consts.ModelSettingModeManual && mode == string(consts.ModelSettingModeManual) {
isTriggerUpsertRecords = oldModelModeSetting.IsManualEmbeddingUpdated
}
ragModelTypes := []domain.ModelType{
domain.ModelTypeEmbedding,
domain.ModelTypeRerank,
@ -248,7 +288,6 @@ func (u *ModelUsecase) updateRAGModelsByMode(ctx context.Context, mode string, a
domain.ModelTypeAnalysisVL,
}
var hasEmbeddingModel bool
for _, modelType := range ragModelTypes {
var model *domain.Model
@ -270,7 +309,7 @@ func (u *ModelUsecase) updateRAGModelsByMode(ctx context.Context, mode string, a
Model: modelName,
Type: modelType,
IsActive: true,
BaseURL: "https://model-square.app.baizhi.cloud/v1",
BaseURL: consts.AutoModeBaseURL,
APIKey: autoModeAPIKey,
Provider: domain.ModelProviderBrandBaiZhiCloud,
}
@ -285,15 +324,11 @@ func (u *ModelUsecase) updateRAGModelsByMode(ctx context.Context, mode string, a
}
u.logger.Info("successfully updated RAG model", log.String("model name: ", string(model.Model)))
}
// 检查是否有嵌入模型
if modelType == domain.ModelTypeEmbedding {
hasEmbeddingModel = true
}
}
// 如果有嵌入模型,触发记录更新
if hasEmbeddingModel {
// 触发记录更新
if isTriggerUpsertRecords {
u.logger.Info("embedding model updated, triggering upsert records")
return u.TriggerUpsertRecords(ctx)
}
return nil

View File

@ -576,3 +576,30 @@ func (u *NodeUsecase) SyncRagNodeStatus(ctx context.Context) error {
return nil
}
func (u *NodeUsecase) NodeRestudy(ctx context.Context, req *v1.NodeRestudyReq) error {
nodeReleases, err := u.nodeRepo.GetLatestNodeReleaseByNodeIDs(ctx, req.KbId, req.NodeIds)
if err != nil {
return fmt.Errorf("get latest node release failed: %w", err)
}
for _, nodeRelease := range nodeReleases {
if nodeRelease.DocID == "" {
continue
}
if err := u.ragRepo.AsyncUpdateNodeReleaseVector(ctx, []*domain.NodeReleaseVectorRequest{
{
KBID: nodeRelease.KBID,
NodeReleaseID: nodeRelease.ID,
Action: "upsert",
},
}); err != nil {
u.logger.Error("async update node release vector failed",
log.String("node_release_id", nodeRelease.ID),
log.Error(err))
continue
}
}
return nil
}

View File

@ -71,8 +71,8 @@ const Step1Model: React.FC<Step1ModelProps> = ({ ref }) => {
// 如果是 auto 模式,检查是否配置了 API key
if (modeSetting?.mode === 'auto') {
if (modeSetting.auto_mode_api_key === '') {
return Promise.reject(new Error('请完成模型配置'));
if (!modeSetting.auto_mode_api_key) {
return Promise.reject(new Error('请点击应用完成模型配置'));
}
} else {
// 手动模式检查
@ -82,7 +82,7 @@ const Step1Model: React.FC<Step1ModelProps> = ({ ref }) => {
!rerankModelData ||
!analysisModelData
) {
return Promise.reject(new Error('请配置必要的模型'));
return Promise.reject(new Error('请配置必要的模型后点击应用'));
}
}
} catch (error) {

View File

@ -1,5 +1,5 @@
import { FooterSetting } from '@/api/type';
import { useAppDispatch, useAppSelector } from '@/store';
import { Icon } from '@ctzhian/ui';
import {
closestCenter,
DndContext,
@ -18,22 +18,15 @@ import {
useSortable,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Box, Button, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { Box, IconButton, Stack, TextField } from '@mui/material';
import {
CSSProperties,
forwardRef,
HTMLAttributes,
useCallback,
useEffect,
useState,
} from 'react';
import {
Control,
Controller,
FieldErrors,
useFieldArray,
} from 'react-hook-form';
import { Control, Controller, FieldErrors } from 'react-hook-form';
import { BrandGroup } from '.';
export type ItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {
@ -122,7 +115,7 @@ const LinkItem = forwardRef<HTMLDivElement, LinkItemProps>(
size='small'
onClick={onRemove}
sx={{
color: 'text.auxiliary',
color: 'text.tertiary',
':hover': { color: 'error.main' },
flexShrink: 0,
width: '28px',
@ -395,7 +388,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
size='small'
onClick={handleRemove}
sx={{
color: 'text.auxiliary',
color: 'text.tertiary',
':hover': { color: 'error.main' },
flexShrink: 0,
width: '28px',

View File

@ -1,8 +1,8 @@
import UploadFile from '@/components/UploadFile';
import { DomainSocialMediaAccount } from '@/request/types';
import { Icon } from '@ctzhian/ui';
import {
Box,
FormControl,
IconButton,
MenuItem,
Select,
@ -11,7 +11,6 @@ import {
ToggleButton,
ToggleButtonGroup,
} from '@mui/material';
import { Icon } from '@ctzhian/ui';
import {
CSSProperties,
Dispatch,
@ -113,7 +112,7 @@ const Item = forwardRef<HTMLDivElement, SocialInfoProps>(
setIsEdit(true);
}}
sx={{
color: 'text.auxiliary',
color: 'text.tertiary',
':hover': { color: 'error.main' },
flexShrink: 0,
width: '28px',

View File

@ -0,0 +1,167 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
SortingStrategy,
} from '@dnd-kit/sortable';
import { Stack, SxProps, Theme } from '@mui/material';
import {
ComponentType,
CSSProperties,
Dispatch,
SetStateAction,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
export interface DragListProps<T extends { id?: string | null }> {
data: T[];
onChange: (data: T[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
SortableItemComponent: ComponentType<{
id: string;
item: T;
handleRemove: (id: string) => void;
handleUpdateItem: (item: T) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}>;
ItemComponent: ComponentType<{
isDragging?: boolean;
item: T;
style?: CSSProperties;
setIsEdit: Dispatch<SetStateAction<boolean>>;
handleUpdateItem?: (item: T) => void;
}>;
containerSx?: SxProps<Theme>;
sortingStrategy?: SortingStrategy;
direction?: 'row' | 'column';
gap?: number;
}
function DragList<T extends { id?: string | null }>({
data,
onChange,
setIsEdit,
SortableItemComponent,
ItemComponent,
containerSx,
sortingStrategy = rectSortingStrategy,
direction = 'row',
gap = 2,
}: DragListProps<T>) {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const dataRef = useRef(data);
// 保持 ref 与 data 同步
useEffect(() => {
dataRef.current = data;
}, [data]);
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const currentData = dataRef.current;
const oldIndex = currentData.findIndex(
item => (item.id || '') === active.id,
);
const newIndex = currentData.findIndex(
item => (item.id || '') === over!.id,
);
const newData = arrayMove(currentData, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const currentData = dataRef.current;
const newData = currentData.filter(item => (item.id || '') !== id);
onChange(newData);
},
[onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: T) => {
const currentData = dataRef.current;
const newData = currentData.map(item =>
(item.id || '') === (updatedItem.id || '') ? updatedItem : item,
);
onChange(newData);
},
[onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id || '')}
strategy={sortingStrategy}
>
<Stack
direction={direction}
flexWrap={'wrap'}
gap={gap}
sx={containerSx}
>
{data.map(item => (
<SortableItemComponent
key={item.id || ''}
id={item.id || ''}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<ItemComponent
isDragging
item={data.find(item => (item.id || '') === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
}
export default DragList;

View File

@ -1,11 +1,22 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemTypeProps } from './Item';
import { ComponentType } from 'react';
type SortableItemProps = ItemTypeProps & {};
export interface SortableItemProps<T extends { id?: string | null }> {
id: string;
item: T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ItemComponent: ComponentType<any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
function SortableItem<T extends { id?: string | null }>({
id,
item,
ItemComponent,
...rest
}: SortableItemProps<T>) {
const {
isDragging,
attributes,
@ -13,7 +24,7 @@ const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
@ -21,7 +32,7 @@ const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
};
return (
<FaqItem
<ItemComponent
ref={setNodeRef}
style={style}
withOpacity={isDragging}
@ -33,6 +44,6 @@ const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
{...rest}
/>
);
};
}
export default SortableItem;

View File

@ -0,0 +1,4 @@
export { default as DragList } from './DragList';
export type { DragListProps } from './DragList';
export type { SortableItemProps } from './SortableItem';

View File

@ -1,125 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item from './Item';
import SortableItem from './SortableItem';
type ItemType = {
id: string;
text: string;
type: 'contained' | 'outlined' | 'text';
href: string;
};
interface DragListProps {
data: ItemType[];
onChange: (data: ItemType[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: {
id: string;
text: string;
type: 'contained' | 'outlined' | 'text';
href: string;
}) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<SortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
isDragging
item={data.find(item => item.id === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default DragList;

View File

@ -0,0 +1,141 @@
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import {
CSSProperties,
Dispatch,
forwardRef,
HTMLAttributes,
SetStateAction,
} from 'react';
type HotSearchItem = {
id: string;
text: string;
};
export type HotSearchItemProps = Omit<
HTMLAttributes<HTMLDivElement>,
'onChange'
> & {
item: HotSearchItem;
withOpacity?: boolean;
isDragging?: boolean;
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>;
handleRemove?: (id: string) => void;
handleUpdateItem?: (item: HotSearchItem) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
};
const HotSearchItem = forwardRef<HTMLDivElement, HotSearchItemProps>(
(
{
item,
withOpacity,
isDragging,
style,
dragHandleProps,
handleRemove,
handleUpdateItem,
setIsEdit,
...props
},
ref,
) => {
const inlineStyles: CSSProperties = {
opacity: withOpacity ? '0.5' : '1',
borderRadius: '10px',
cursor: isDragging ? 'grabbing' : 'grab',
backgroundColor: '#ffffff',
width: '100%',
...style,
};
return (
<Box ref={ref} style={inlineStyles} {...props}>
<Stack
direction={'row'}
alignItems={'center'}
justifyContent={'space-between'}
gap={0.5}
sx={{
py: 1.5,
px: 1,
border: '1px solid',
borderColor: 'divider',
borderRadius: '10px',
}}
>
<Stack
direction={'column'}
gap={'20px'}
sx={{
flex: 1,
p: 1.5,
}}
>
<TextField
label='搜索关键词'
slotProps={{
inputLabel: {
shrink: true,
},
}}
sx={{
height: '36px',
'& .MuiOutlinedInput-root': {
height: '36px',
padding: '0 12px',
'& .MuiOutlinedInput-input': {
padding: '8px 0',
},
},
}}
fullWidth
placeholder='请输入搜索关键词'
variant='outlined'
value={item.text}
onChange={e => {
const updatedItem = { ...item, text: e.target.value };
handleUpdateItem?.(updatedItem);
setIsEdit(true);
}}
/>
</Stack>
<Stack
direction={'column'}
sx={{ justifyContent: 'space-between', alignSelf: 'stretch' }}
>
<IconButton
size='small'
onClick={e => {
e.stopPropagation();
handleRemove?.(item.id);
}}
sx={{
color: 'text.tertiary',
':hover': { color: 'error.main' },
width: '28px',
height: '28px',
}}
>
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
</IconButton>
<IconButton
size='small'
sx={{
cursor: 'grab',
color: 'text.secondary',
'&:hover': { color: 'primary.main' },
}}
{...(dragHandleProps as any)}
>
<Icon type='icon-drag' />
</IconButton>
</Stack>
</Stack>
</Box>
);
},
);
export default HotSearchItem;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import Item, { ItemProps } from './Item';
type FaqSortableItemProps = ItemProps & {};
const FaqSortableItem: FC<FaqSortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<Item
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default FaqSortableItem;

View File

@ -1,18 +1,21 @@
import React, { useState, useEffect } from 'react';
import { TextField, Chip, Autocomplete, Box } from '@mui/material';
import React, { useEffect, useRef, useMemo } from 'react';
import { TextField } from '@mui/material';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import type { ConfigProps } from '../type';
import { useForm, Controller } from 'react-hook-form';
import { useAppSelector } from '@/store';
import DragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import HotSearchItem from './HotSearchItem';
import UploadFile from '@/components/UploadFile';
import { DEFAULT_DATA } from '../../../constants';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
import { handleLandingConfigs, findConfigById } from '../../../utils';
import { Empty } from '@ctzhian/ui';
const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
const { appPreviewData } = useAppSelector(state => state.config);
const [inputValue, setInputValue] = useState('');
const { control, watch, setValue, subscribe } = useForm<
typeof DEFAULT_DATA.banner
>({
@ -24,6 +27,49 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
const debouncedDispatch = useDebounceAppPreviewData();
const btns = watch('btns') || [];
const hotSearch = watch('hot_search') || [];
// 使用 ref 来维护稳定的 ID 映射
const idMapRef = useRef<Map<number, string>>(new Map());
// 将string[]转换为对象数组用于显示,保持 ID 稳定
const hotSearchList = Array.isArray(hotSearch)
? hotSearch.map((text, index) => {
// 如果该索引没有 ID生成一个新的
if (!idMapRef.current.has(index)) {
idMapRef.current.set(
index,
`${Date.now()}-${index}-${Math.random()}`,
);
}
return {
id: idMapRef.current.get(index)!,
text: String(text),
};
})
: [];
// 清理不再使用的 ID并确保所有索引都有 ID
useEffect(() => {
const currentIndexes = new Set(hotSearch.map((_, index) => index));
// 清理不存在的索引
const keysToDelete: number[] = [];
idMapRef.current.forEach((_, key) => {
if (!currentIndexes.has(key)) {
keysToDelete.push(key);
}
});
keysToDelete.forEach(key => idMapRef.current.delete(key));
// 确保每个索引都有 ID
hotSearch.forEach((_, index) => {
if (!idMapRef.current.has(index)) {
idMapRef.current.set(index, `${Date.now()}-${index}-${Math.random()}`);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hotSearch.length]);
const handleAddButton = () => {
const nextId = `${Date.now()}`;
@ -33,6 +79,44 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
]);
};
const handleAddHotSearch = () => {
const newIndex = hotSearch.length;
const nextId = `${Date.now()}-${newIndex}-${Math.random()}`;
idMapRef.current.set(newIndex, nextId);
// 转换回string[]格式
setValue('hot_search', [...hotSearch, '']);
setIsEdit(true);
};
const handleHotSearchChange = (newList: { id: string; text: string }[]) => {
// 重建 ID 映射关系
const newIdMap = new Map<number, string>();
newList.forEach((item, index) => {
newIdMap.set(index, item.id);
});
idMapRef.current = newIdMap;
// 转换回string[]格式
setValue(
'hot_search',
newList.map(item => item.text),
);
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const HotSearchSortableItem = useMemo(
() => (props: any) => (
<SortableItem {...props} ItemComponent={HotSearchItem} />
),
[],
);
const ButtonSortableItem = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
const callback = subscribe({
formState: {
@ -59,6 +143,7 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
return () => {
callback();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subscribe]);
return (
@ -132,60 +217,18 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
render={({ field }) => <TextField {...field} placeholder='请输入' />}
/>
</CommonItem>
<CommonItem title='热门搜索'>
<Controller
control={control}
name='hot_search'
render={({ field }) => (
<Autocomplete
{...field}
value={field.value || []}
multiple
freeSolo
fullWidth
options={[]}
inputValue={inputValue}
onInputChange={(_, newInputValue) => setInputValue(newInputValue)}
onChange={(_, newValue) => {
setIsEdit(true);
const newValues = [...new Set(newValue as string[])];
field.onChange(newValues);
}}
onBlur={() => {
setIsEdit(true);
const trimmedValue = inputValue.trim();
if (trimmedValue && !field.value?.includes(trimmedValue)) {
field.onChange([...(field.value || []), trimmedValue]);
}
setInputValue('');
}}
renderValue={(value, getTagProps) => {
return value.map((option, index: number) => {
return (
<Chip
variant='outlined'
size='small'
label={
<Box sx={{ fontSize: '12px' }}>
{option as React.ReactNode}
</Box>
}
{...getTagProps({ index })}
key={index}
/>
);
});
}}
renderInput={params => (
<TextField
{...params}
placeholder='回车确认,填写下一个热门搜索'
variant='outlined'
/>
)}
/>
)}
/>
<CommonItem title='热门搜索' onAdd={handleAddHotSearch}>
{hotSearchList.length === 0 ? (
<Empty />
) : (
<DragList
data={hotSearchList}
onChange={handleHotSearchChange}
setIsEdit={setIsEdit}
SortableItemComponent={HotSearchSortableItem}
ItemComponent={HotSearchItem}
/>
)}
</CommonItem>
<CommonItem title='主按钮' onAdd={handleAddButton}>
<DragList
@ -195,6 +238,8 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
setIsEdit(true);
}}
setIsEdit={setIsEdit}
SortableItemComponent={ButtonSortableItem}
ItemComponent={Item}
/>
</CommonItem>
</StyledCommonWrapper>

View File

@ -1,114 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item from './Item';
import { DomainRecommendNodeListResp } from '@/request/types';
import SortableItem from './SortableItem';
interface DragListProps {
data: DomainRecommendNodeListResp[];
onChange: (data: DomainRecommendNodeListResp[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: DomainRecommendNodeListResp) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id!)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<SortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
// handleUpdateItem={handleUpdateItem}
// setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
isDragging
item={data.find(item => item.id === activeId)!}
// setIsEdit={setIsEdit}
// handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default DragList;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemProps } from './Item';
type SortableItemProps = ItemProps & {};
const FaqSortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id! });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default FaqSortableItem;

View File

@ -1,8 +1,10 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import BasicDocDragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { Empty } from '@ctzhian/ui';
import { useAppSelector } from '@/store';
@ -40,6 +42,12 @@ const BasicDocConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -116,13 +124,15 @@ const BasicDocConfig = ({ setIsEdit, id }: ConfigProps) => {
{nodes.length === 0 ? (
<Empty />
) : (
<BasicDocDragList
<DragList
data={nodes}
onChange={value => {
setIsEdit(true);
setValue('nodes', value);
}}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,113 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item, { type ItemType } from './Item';
import SortableItem from './SortableItem';
interface DragListProps {
data: ItemType[];
onChange: (data: ItemType[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: ItemType) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<SortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
isDragging
item={data.find(item => item.id === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default DragList;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import Item, { ItemProps } from './Item';
type SortableItemProps = ItemProps & {};
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<Item
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default SortableItem;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import DragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -36,6 +38,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -91,6 +99,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,120 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item from './Item';
import FaqSortableItem from './SortableItem';
type ItemType = {
id: string;
title: string;
url: string;
desc: string;
};
interface DragListProps {
data: ItemType[];
onChange: (data: ItemType[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: ItemType) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<FaqSortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
isDragging
item={data.find(item => item.id === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default DragList;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemProps } from './Item';
type FaqSortableItemProps = ItemProps & {};
const FaqSortableItem: FC<FaqSortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default FaqSortableItem;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import DragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -37,6 +39,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -121,6 +129,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,113 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item, { type ItemType } from './Item';
import SortableItem from './SortableItem';
interface DragListProps {
data: ItemType[];
onChange: (data: ItemType[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: ItemType) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<SortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
isDragging
item={data.find(item => item.id === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default DragList;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import DragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -34,6 +36,12 @@ const CaseConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -90,6 +98,8 @@ const CaseConfig = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,113 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item, { type ItemType } from './Item';
import SortableItem from './SortableItem';
interface DragListProps {
data: ItemType[];
onChange: (data: ItemType[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: ItemType) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<SortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
isDragging
item={data.find(item => item.id === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default DragList;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemTypeProps } from './Item';
type SortableItemProps = ItemTypeProps & {};
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default SortableItem;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import DragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -37,6 +39,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -93,6 +101,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,114 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item from './Item';
import { DomainRecommendNodeListResp } from '@/request/types';
import SortableItem from './SortableItem';
interface DragListProps {
data: DomainRecommendNodeListResp[];
onChange: (data: DomainRecommendNodeListResp[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: DomainRecommendNodeListResp) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id!)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<SortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
// handleUpdateItem={handleUpdateItem}
// setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
isDragging
item={data.find(item => item.id === activeId)!}
// setIsEdit={setIsEdit}
// handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default DragList;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import Item, { ItemProps } from './Item';
type SortableItemProps = ItemProps & {};
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id! });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<Item
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default SortableItem;

View File

@ -1,8 +1,10 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import BasicDocDragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import { Empty } from '@ctzhian/ui';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
@ -40,6 +42,12 @@ const DirDocConfig = ({ setIsEdit, id }: ConfigProps) => {
nodeRec(newList);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -117,13 +125,15 @@ const DirDocConfig = ({ setIsEdit, id }: ConfigProps) => {
{nodes.length === 0 ? (
<Empty />
) : (
<BasicDocDragList
<DragList
data={nodes}
onChange={value => {
setIsEdit(true);
setValue('nodes', value);
}}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,119 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import FaqItem from './FaqItem';
import FaqSortableItem from './FaqSortableItem';
type FaqItemType = {
id: string;
question: string;
link: string;
};
interface FaqDragListProps {
data: FaqItemType[];
onChange: (data: FaqItemType[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const FaqDragList: FC<FaqDragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: FaqItemType) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<FaqSortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<FaqItem
isDragging
item={data.find(item => item.id === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default FaqDragList;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { FaqItemProps } from './FaqItem';
type FaqSortableItemProps = FaqItemProps & {};
const FaqSortableItem: FC<FaqSortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default FaqSortableItem;

View File

@ -1,14 +1,15 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import FaqDragList from './FaqDragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import FaqItem from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
import { Empty } from '@ctzhian/ui';
import { DEFAULT_DATA } from '../../../constants';
import ColorPickerField from '../../components/ColorPickerField';
import { findConfigById, handleLandingConfigs } from '../../../utils';
const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
@ -35,6 +36,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const FaqSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={FaqItem} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -111,10 +118,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
{list.length === 0 ? (
<Empty />
) : (
<FaqDragList
<DragList
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={FaqSortableComponent}
ItemComponent={FaqItem}
/>
)}
</CommonItem>

View File

@ -1,113 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item, { type ItemType } from './Item';
import SortableItem from './SortableItem';
interface DragListProps {
data: ItemType[];
onChange: (data: ItemType[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: ItemType) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<SortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
isDragging
item={data.find(item => item.id === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default DragList;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import Item, { ItemProps } from './Item';
type SortableItemProps = ItemProps & {};
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<Item
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default SortableItem;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import DragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -34,6 +36,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -89,6 +97,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,113 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item, { type ItemType } from './Item';
import SortableItem from './SortableItem';
interface DragListProps {
data: ItemType[];
onChange: (data: ItemType[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: ItemType) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<SortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
isDragging
item={data.find(item => item.id === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default DragList;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemTypeProps } from './Item';
type SortableItemProps = ItemTypeProps & {};
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default SortableItem;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import DragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -34,6 +36,12 @@ const MetricsConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -90,6 +98,8 @@ const MetricsConfig = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,113 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item, { ItemType } from './Item';
import SortableItem from './SortableItem';
interface FaqDragListProps {
data: ItemType[];
onChange: (data: ItemType[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const FaqDragList: FC<FaqDragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: ItemType) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<SortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
handleUpdateItem={handleUpdateItem}
setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
isDragging
item={data.find(item => item.id === activeId)!}
setIsEdit={setIsEdit}
handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default FaqDragList;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import Item, { ItemProps } from './Item';
type SortableItemProps = ItemProps & {};
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<Item
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default SortableItem;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import FaqDragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -36,6 +38,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -88,10 +96,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
{list.length === 0 ? (
<Empty />
) : (
<FaqDragList
<DragList
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -1,114 +0,0 @@
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
rectSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import { Stack } from '@mui/material';
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
import Item from './Item';
import { DomainRecommendNodeListResp } from '@/request/types';
import SortableItem from './SortableItem';
interface DragListProps {
data: DomainRecommendNodeListResp[];
onChange: (data: DomainRecommendNodeListResp[]) => void;
setIsEdit: Dispatch<SetStateAction<boolean>>;
}
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
const [activeId, setActiveId] = useState<string | null>(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveId(event.active.id as string);
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = data.findIndex(item => item.id === active.id);
const newIndex = data.findIndex(item => item.id === over!.id);
const newData = arrayMove(data, oldIndex, newIndex);
onChange(newData);
}
setActiveId(null);
},
[data, onChange],
);
const handleDragCancel = useCallback(() => {
setActiveId(null);
}, []);
const handleRemove = useCallback(
(id: string) => {
const newData = data.filter(item => item.id !== id);
onChange(newData);
},
[data, onChange],
);
const handleUpdateItem = useCallback(
(updatedItem: DomainRecommendNodeListResp) => {
const newData = data.map(item =>
item.id === updatedItem.id ? updatedItem : item,
);
onChange(newData);
},
[data, onChange],
);
if (data.length === 0) return null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={data.map(item => item.id!)}
strategy={rectSortingStrategy}
>
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{data.map(item => (
<SortableItem
key={item.id}
id={item.id}
item={item}
handleRemove={handleRemove}
// handleUpdateItem={handleUpdateItem}
// setIsEdit={setIsEdit}
/>
))}
</Stack>
</SortableContext>
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
{activeId ? (
<Item
isDragging
item={data.find(item => item.id === activeId)!}
// setIsEdit={setIsEdit}
// handleUpdateItem={handleUpdateItem}
/>
) : null}
</DragOverlay>
</DndContext>
);
};
export default DragList;

View File

@ -1,38 +0,0 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemProps } from './Item';
type SortableItemProps = ItemProps & {};
const FaqSortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
const {
isDragging,
attributes,
listeners,
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id! });
const style = {
transform: CSS.Transform.toString(transform),
transition: transition || undefined,
};
return (
<FaqItem
ref={setNodeRef}
style={style}
withOpacity={isDragging}
dragHandleProps={{
...attributes,
...listeners,
}}
item={item}
{...rest}
/>
);
};
export default FaqSortableItem;

View File

@ -1,8 +1,10 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
import { Controller, useForm } from 'react-hook-form';
import DragList from './DragList';
import DragList from '../../components/DragList';
import SortableItem from '../../components/SortableItem';
import Item from './Item';
import { Empty } from '@ctzhian/ui';
import type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
@ -40,6 +42,12 @@ const SimpleDocConfigConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -124,6 +132,8 @@ const SimpleDocConfigConfig = ({ setIsEdit, id }: ConfigProps) => {
setValue('nodes', value);
}}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -50,7 +50,7 @@ const dark = {
text: {
primary: '#fff',
secondary: 'rgba(255,255,255,0.7)',
auxiliary: 'rgba(255,255,255,0.5)',
tertiary: 'rgba(255,255,255,0.5)',
disabled: 'rgba(255,255,255,0.26)',
slave: 'rgba(255,255,255,0.05)',
inverseAuxiliary: 'rgba(0,0,0,0.5)',

View File

@ -55,7 +55,7 @@ const light = {
text: {
primary: '#21222D',
secondary: 'rgba(33,34,35,0.7)',
auxiliary: 'rgba(33,34,35,0.5)',
tertiary: 'rgba(33,34,35,0.5)',
slave: 'rgba(33,34,35,0.3)',
disabled: 'rgba(33,34,35,0.2)',
inverse: '#FFFFFF',

View File

@ -7,7 +7,7 @@ import {
import RAG_SOURCES from '@/constant/rag';
import { treeSx } from '@/constant/styles';
import { postApiV1Node, putApiV1NodeDetail } from '@/request/Node';
import { ConstsNodeAccessPerm, ConstsNodeRagInfoStatus } from '@/request/types';
import { ConstsNodeAccessPerm } from '@/request/types';
import { useAppSelector } from '@/store';
import { AppContext, updateTree } from '@/utils/drag';
import { handleMultiSelect, updateAllParentStatus } from '@/utils/tree';
@ -522,20 +522,15 @@ const TreeItem = React.forwardRef<
gap={1}
sx={{ flexShrink: 0, fontSize: 12 }}
>
{item.type === 2 &&
item.rag_status &&
![
ConstsNodeRagInfoStatus.NodeRagStatusBasicSucceeded,
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceSucceeded,
].includes(item.rag_status) && (
<Tooltip title={item.rag_message}>
<StyledTag
color={RAG_SOURCES[item.rag_status].color as any}
>
{RAG_SOURCES[item.rag_status].name}
</StyledTag>
</Tooltip>
)}
{item.type === 2 && item.rag_status && (
<Tooltip title={item.rag_message}>
<StyledTag
color={RAG_SOURCES[item.rag_status].color as any}
>
{RAG_SOURCES[item.rag_status].name}
</StyledTag>
</Tooltip>
)}
{item.status === 1 && (
<StyledTag color='error'></StyledTag>
)}

View File

@ -3,7 +3,6 @@ import { useURLSearchParams } from '@/hooks';
import { ConstsUserRole } from '@/request/types';
import { useAppDispatch, useAppSelector } from '@/store';
import { setKbC, setKbId } from '@/store/slices/config';
import custom from '@/themes/custom';
import { Ellipsis, Icon, message } from '@ctzhian/ui';
import {
Box,
@ -117,7 +116,7 @@ const KBSelect = () => {
borderRadius: '5px',
bgcolor: 'background.paper3',
'&:hover': {
bgcolor: custom.selectedMenuItemBgColor,
bgcolor: 'rgba(50,72,242,0.1)',
},
}}
fullWidth

View File

@ -43,7 +43,7 @@ const AutoModelConfig = forwardRef<AutoModelConfigRef, AutoModelConfigProps>(
// 默认百智云 Chat 模型列表
const DEFAULT_BAIZHI_CLOUD_CHAT_MODELS: string[] = [
'deepseek-v3.1',
'deepseek-chat',
'deepseek-r1',
'kimi-k2-0711-preview',
'qwen-vl-max-latest',
@ -85,6 +85,9 @@ const AutoModelConfig = forwardRef<AutoModelConfigRef, AutoModelConfigProps>(
sx={{
flex: 1,
p: 2,
pl: 2,
pr: 0,
pt: 0,
overflow: 'hidden',
overflowY: 'auto',
}}
@ -121,7 +124,7 @@ const AutoModelConfig = forwardRef<AutoModelConfigRef, AutoModelConfigProps>(
}}
>
API Key PandaWiki
</Box>
</Box>
)}
@ -130,28 +133,31 @@ const AutoModelConfig = forwardRef<AutoModelConfigRef, AutoModelConfigProps>(
sx={{
display: 'flex',
alignItems: 'center',
fontSize: 14,
fontWeight: 'bold',
color: 'text.primary',
mb: 1.5,
justifyContent: 'space-between',
mb: '16px',
pt: '32px',
}}
>
<Box
sx={{
width: 3,
height: 14,
bgcolor: 'primary.main',
borderRadius: '2px',
mr: 1,
display: 'flex',
alignItems: 'center',
fontSize: 14,
fontWeight: 'bold',
color: 'text.primary',
}}
/>
API Key
</Box>
<Stack
direction={'row'}
alignItems={'center'}
justifyContent={'flex-end'}
>
>
<Box
sx={{
width: 4,
height: 10,
bgcolor: 'primary.main',
borderRadius: '30%',
mr: 1,
}}
/>
API Key
</Box>
<Box
component='a'
href='https://model-square.app.baizhi.cloud/token'
@ -167,7 +173,7 @@ const AutoModelConfig = forwardRef<AutoModelConfigRef, AutoModelConfigProps>(
<Icon type='icon-key' sx={{ fontSize: 14 }} />
API Key
</Box>
</Stack>
</Box>
<TextField
fullWidth
size='medium'
@ -199,7 +205,7 @@ const AutoModelConfig = forwardRef<AutoModelConfigRef, AutoModelConfigProps>(
</Box>
{!showTip && (
<Box sx={{ mt: 3 }}>
<Box sx={{ mt: 0 }}>
<Box
sx={{
display: 'flex',
@ -207,15 +213,16 @@ const AutoModelConfig = forwardRef<AutoModelConfigRef, AutoModelConfigProps>(
fontSize: 14,
fontWeight: 'bold',
color: 'text.primary',
mb: 1.5,
mb: '16px',
pt: '32px',
}}
>
<Box
sx={{
width: 3,
height: 14,
width: 4,
height: 10,
bgcolor: 'primary.main',
borderRadius: '2px',
borderRadius: '30%',
mr: 1,
}}
/>

View File

@ -8,7 +8,7 @@ import {
} from '@/request/Model';
import { GithubComChaitinPandaWikiDomainModelListItem } from '@/request/types';
import { addOpacityToColor } from '@/utils';
import { Icon, message } from '@ctzhian/ui';
import { Icon, message, Modal } from '@ctzhian/ui';
import {
Box,
Button,
@ -43,6 +43,7 @@ const ModelModal = lazy(() =>
export interface ModelConfigRef {
getAutoConfigFormData: () => { apiKey: string; selectedModel: string } | null;
handleClose: () => void;
}
interface ModelConfigProps {
@ -153,6 +154,26 @@ const ModelConfig = forwardRef<ModelConfigRef, ModelConfigProps>(
switchToAutoMode();
}, [autoSwitchToAutoMode, hasAutoSwitched, getModelList]);
// 处理关闭弹窗
const handleCloseModal = () => {
// 判断是否有未应用的更改
const hasUnappliedChanges = tempMode !== savedMode || hasConfigChanged;
if (hasUnappliedChanges) {
Modal.confirm({
title: '提示',
content: '有未应用的设置,是否确认关闭?',
onOk: () => {
onCloseModal();
},
okText: '确认',
cancelText: '取消',
});
} else {
onCloseModal();
}
};
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
getAutoConfigFormData: () => {
@ -161,6 +182,7 @@ const ModelConfig = forwardRef<ModelConfigRef, ModelConfigProps>(
}
return null;
},
handleClose: handleCloseModal,
}));
const handleSave = async () => {
@ -208,8 +230,14 @@ const ModelConfig = forwardRef<ModelConfigRef, ModelConfigProps>(
};
return (
<Stack gap={2}>
<Box sx={{ pl: 2, display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<Stack gap={0}>
<Box
sx={{
pl: 2,
display: 'flex',
alignItems: 'flex-start',
}}
>
<Box sx={{ flex: 1 }}>
<Box
sx={{
@ -218,15 +246,15 @@ const ModelConfig = forwardRef<ModelConfigRef, ModelConfigProps>(
fontSize: 14,
fontWeight: 'bold',
color: 'text.primary',
mb: 1,
mb: '16px',
}}
>
<Box
sx={{
width: 3,
height: 14,
width: 4,
height: 10,
bgcolor: 'primary.main',
borderRadius: '2px',
borderRadius: '30%',
mr: 1,
}}
/>

View File

@ -15,10 +15,10 @@ import {
Tooltip,
useTheme,
} from '@mui/material';
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef } from 'react';
import LottieIcon from '../LottieIcon';
import Member from './component/Member';
import ModelConfig from './component/ModelConfig';
import ModelConfig, { ModelConfigRef } from './component/ModelConfig';
const SystemTabs = [
{ label: '模型配置', id: 'model-config' },
@ -33,6 +33,7 @@ const System = () => {
const [open, setOpen] = useState(false);
const [activeTab, setActiveTab] = useState('model-config');
const dispatch = useAppDispatch();
const modelConfigRef = useRef<ModelConfigRef>(null);
const [chatModelData, setChatModelData] =
useState<GithubComChaitinPandaWikiDomainModelListItem | null>(null);
const [embeddingModelData, setEmbeddingModelData] =
@ -103,29 +104,73 @@ const System = () => {
open={open}
disableEnforceFocus={true}
footer={null}
onCancel={() => setOpen(false)}
onCancel={() => {
if (activeTab === 'model-config' && modelConfigRef.current) {
modelConfigRef.current.handleClose();
} else {
setOpen(false);
}
}}
>
<Tabs
value={activeTab}
onChange={(event, newValue) => setActiveTab(newValue)}
aria-label='system tabs'
sx={{ mb: 2 }}
sx={{
mb: 2,
ml: -2,
borderBottom: 1,
borderColor: 'divider',
'& .MuiTabs-indicator': {
display: 'none',
},
'& .MuiTab-root': {
minHeight: 48,
textTransform: 'none',
fontSize: '14px',
fontWeight: 400,
color: theme.palette.text.secondary,
position: 'relative',
'&.Mui-selected': {
color: theme.palette.primary.main,
fontWeight: 500,
},
'&.Mui-selected::after': {
content: '""',
position: 'absolute',
bottom: 0,
left: '50%',
transform: 'translateX(-50%)',
width: '40px',
height: '2px',
backgroundColor: theme.palette.primary.main,
zIndex: 1,
},
},
}}
>
{SystemTabs.map(tab => (
<Tab key={tab.id} label={tab.label} value={tab.id} />
))}
</Tabs>
{activeTab === 'user-management' && <Member />}
{activeTab === 'user-management' && (
<Box sx={{ ml: -2 }}>
<Member />
</Box>
)}
{activeTab === 'model-config' && (
<ModelConfig
onCloseModal={() => setOpen(false)}
chatModelData={chatModelData}
embeddingModelData={embeddingModelData}
rerankModelData={rerankModelData}
analysisModelData={analysisModelData}
analysisVLModelData={analysisVLModelData}
getModelList={getModelList}
/>
<Box sx={{ ml: -2 }}>
<ModelConfig
ref={modelConfigRef}
onCloseModal={() => setOpen(false)}
chatModelData={chatModelData}
embeddingModelData={embeddingModelData}
rerankModelData={rerankModelData}
analysisModelData={analysisModelData}
analysisVLModelData={analysisVLModelData}
getModelList={getModelList}
/>
</Box>
)}
</Modal>
</>

View File

@ -83,7 +83,7 @@ export default function ContributePreviewModal(
? '新增'
: '修改'}
</Box>
<Box sx={{ fontSize: 14, color: 'text.auxiliary', fontWeight: 400 }}>
<Box sx={{ fontSize: 14, color: 'text.tertiary', fontWeight: 400 }}>
{dayjs(row?.created_at).fromNow()}
</Box>
</Stack>

View File

@ -61,7 +61,7 @@ const MarkdownPreviewModal = ({
? '新增'
: '修改'}
</Box>
<Box sx={{ fontSize: 14, color: 'text.auxiliary', fontWeight: 400 }}>
<Box sx={{ fontSize: 14, color: 'text.tertiary', fontWeight: 400 }}>
{dayjs(row?.created_at).fromNow()}
</Box>
</Stack>

View File

@ -0,0 +1,151 @@
import { ITreeItem } from '@/api';
import Card from '@/components/Card';
import DragTree from '@/components/Drag/DragTree';
import { postApiV1NodeRestudy } from '@/request';
import { getApiV1NodeList } from '@/request/Node';
import {
ConstsNodeRagInfoStatus,
DomainNodeListItemResp,
} from '@/request/types';
import { useAppSelector } from '@/store';
import { convertToTree } from '@/utils/drag';
import { message, Modal } from '@ctzhian/ui';
import { Box, Checkbox, Stack } from '@mui/material';
import { useEffect, useMemo, useState } from 'react';
interface RagErrorReStartProps {
open: boolean;
defaultSelected?: string[];
onClose: () => void;
refresh: () => void;
}
const RagErrorReStart = ({
open,
defaultSelected = [],
onClose,
refresh,
}: RagErrorReStartProps) => {
const { kb_id } = useAppSelector(state => state.config);
const [selected, setSelected] = useState<string[]>([]);
const [treeList, setTreeList] = useState<ITreeItem[]>([]);
const [list, setList] = useState<DomainNodeListItemResp[]>([]);
const getData = () => {
getApiV1NodeList({ kb_id }).then(res => {
const ragErrorData =
res?.filter(
item =>
item.rag_info?.status &&
[
ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed,
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed,
].includes(item.rag_info.status),
) || [];
setList(ragErrorData);
setSelected(
defaultSelected.length > 0
? defaultSelected
: ragErrorData.map(it => it.id!),
);
const showTreeData = convertToTree(ragErrorData || []);
setTreeList(showTreeData);
});
};
const onSubmit = () => {
if (selected.length > 0) {
postApiV1NodeRestudy({
kb_id,
node_ids: [...selected],
}).then(() => {
message.success('正在重新学习');
setSelected([]);
onClose();
refresh();
});
} else {
message.error(
list.length > 0 ? '请选择要重新学习的文档' : '暂无学习失败的文档',
);
}
};
useEffect(() => {
if (open) {
getData();
}
}, [open, kb_id]);
const selectedTotal = useMemo(() => {
return list.filter(item => selected.includes(item.id!)).length;
}, [selected, list]);
return (
<Modal title='重新学习' open={open} onCancel={onClose} onOk={onSubmit}>
<Stack
direction='row'
component='label'
alignItems={'center'}
justifyContent={'space-between'}
gap={1}
sx={{
cursor: 'pointer',
borderRadius: '10px',
fontSize: 14,
}}
>
<Box>
<Box
component='span'
sx={{ color: 'text.tertiary', fontSize: 12, pl: 1 }}
>
{list.length} {selectedTotal}
</Box>
</Box>
<Stack direction='row' alignItems={'center'}>
<Box sx={{ color: 'text.tertiary', fontSize: 12 }}></Box>
<Checkbox
size='small'
sx={{
p: 0,
color: 'text.disabled',
width: '35px',
height: '35px',
}}
checked={selectedTotal === list.length}
onChange={() => {
setSelected(
selectedTotal === list.length ? [] : list.map(item => item.id!),
);
}}
/>
</Stack>
</Stack>
<Card sx={{ bgcolor: 'background.paper3', py: 1 }}>
<Stack
gap={0.25}
sx={{
fontSize: 14,
maxHeight: 'calc(100vh - 520px)',
overflowY: 'auto',
px: 2,
}}
>
<DragTree
ui='select'
readOnly
selected={selected}
data={treeList}
refresh={getData}
onSelectChange={ids => setSelected(ids)}
/>
</Stack>
</Card>
</Modal>
);
};
export default RagErrorReStart;

View File

@ -8,7 +8,11 @@ import {
} from '@/components/Drag/DragTree/TreeMenu';
import { useURLSearchParams } from '@/hooks';
import { getApiV1NodeList } from '@/request/Node';
import { ConstsCrawlerSource, DomainNodeListItemResp } from '@/request/types';
import {
ConstsCrawlerSource,
ConstsNodeRagInfoStatus,
DomainNodeListItemResp,
} from '@/request/types';
import { useAppDispatch, useAppSelector } from '@/store';
import { setIsRefreshDocList } from '@/store/slices/config';
import { addOpacityToColor } from '@/utils';
@ -32,6 +36,7 @@ import DocSearch from './component/DocSearch';
import DocStatus from './component/DocStatus';
import DocSummary from './component/DocSummary';
import MoveDocs from './component/MoveDocs';
import RagErrorReStart from './component/RagErrorReStart';
import Summary from './component/Summary';
const Content = () => {
@ -44,10 +49,15 @@ const Content = () => {
const search = searchParams.get('search') || '';
const [supportSelect, setBatchOpen] = useState(false);
const [ragErrorCount, setRagErrorCount] = useState(0);
const [ragErrorIds, setRagErrorIds] = useState<string[]>([]);
const [ragErrorOpen, setRagErrorOpen] = useState(false);
const [publish, setPublish] = useState({
// published: 0,
unpublished: 0,
});
const [publishIds, setPublishIds] = useState<string[]>([]);
const [publishOpen, setPublishOpen] = useState(false);
const [list, setList] = useState<DomainNodeListItemResp[]>([]);
const [selected, setSelected] = useState<string[]>([]);
const [data, setData] = useState<ITreeItem[]>([]);
@ -58,8 +68,6 @@ const Content = () => {
const [moreSummaryOpen, setMoreSummaryOpen] = useState(false);
const [moveOpen, setMoveOpen] = useState(false);
const [urlOpen, setUrlOpen] = useState(false);
const [publishIds, setPublishIds] = useState<string[]>([]);
const [publishOpen, setPublishOpen] = useState(false);
const [key, setKey] = useState<ConstsCrawlerSource | null>(null);
const [propertiesOpen, setPropertiesOpen] = useState(false);
const [isBatch, setIsBatch] = useState(false);
@ -119,6 +127,11 @@ const Content = () => {
setPublishIds([item.id]);
};
const handleRestudy = (item: ITreeItem) => {
setRagErrorOpen(true);
setRagErrorIds([item.id]);
};
const handleProperties = (item: ITreeItem) => {
setPropertiesOpen(true);
setOpraData(getOperationData(item));
@ -252,6 +265,19 @@ const Content = () => {
// },
]
: []),
...(item?.rag_status &&
[
ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed,
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed,
].includes(item.rag_status)
? [
{
label: '重新学习',
key: 'restudy',
onClick: () => handleRestudy(item),
},
]
: []),
...(!isEditing
? [{ label: '重命名', key: 'rename', onClick: renameItem }]
: []),
@ -308,8 +334,17 @@ const Content = () => {
setList(res || []);
setPublish({
unpublished: res.filter(it => it.status === 1).length,
// published: res.filter(it => it.status === 2).length,
});
setRagErrorCount(
res.filter(
it =>
it.rag_info?.status &&
[
ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed,
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed,
].includes(it.rag_info.status),
).length,
);
const collapsedAll = collapseAllFolders(convertToTree(res || []), true);
const next = openIds.size
? reopenFolders(collapsedAll, openIds)
@ -386,6 +421,29 @@ const Content = () => {
</Button>
</>
)}
{ragErrorCount > 0 && (
<>
<Box
sx={{
color: 'error.main',
fontSize: 12,
fontWeight: 'normal',
ml: 2,
}}
>
{ragErrorCount}
</Box>
<Button
size='small'
sx={{ minWidth: 0, p: 0, fontSize: 12 }}
onClick={() => {
setRagErrorOpen(true);
}}
>
</Button>
</>
)}
</Stack>
<Stack direction={'row'} alignItems={'center'} gap={2}>
<DocSearch />
@ -670,6 +728,15 @@ const Content = () => {
}}
refresh={getData}
/>
<RagErrorReStart
open={ragErrorOpen}
defaultSelected={ragErrorIds}
onClose={() => {
setRagErrorOpen(false);
setRagErrorIds([]);
}}
refresh={getData}
/>
<MoveDocs
open={moveOpen}
data={list}

View File

@ -0,0 +1,46 @@
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
import httpRequest, { ContentType, RequestParams } from "./httpClient";
import { DomainResponse, V1NodeRestudyReq, V1NodeRestudyResp } from "./types";
/**
* @description
*
* @tags NodeRestudy
* @name PostApiV1NodeRestudy
* @summary
* @request POST:/api/v1/node/restudy
* @secure
* @response `200` `(DomainResponse & {
data?: V1NodeRestudyResp,
})` OK
*/
export const postApiV1NodeRestudy = (
param: V1NodeRestudyReq,
params: RequestParams = {},
) =>
httpRequest<
DomainResponse & {
data?: V1NodeRestudyResp;
}
>({
path: `/api/v1/node/restudy`,
method: "POST",
body: param,
secure: true,
type: ContentType.Json,
format: "json",
...params,
});

View File

@ -10,6 +10,7 @@ export * from './Message'
export * from './Model'
export * from './Node'
export * from './NodePermission'
export * from './NodeRestudy'
export * from './Stat'
export * from './User'
export * from './types'

View File

@ -1644,12 +1644,39 @@ export interface V1NodePermissionResp {
visitable_groups?: DomainNodeGroupDetail[];
}
export interface V1NodeRestudyReq {
kb_id?: string;
node_ids?: string[];
}
export type V1NodeRestudyResp = Record<string, any>;
export interface V1ResetPasswordReq {
id: string;
/** @minLength 8 */
new_password: string;
}
export interface V1ShareNodeDetailResp {
content?: string;
created_at?: string;
creator_account?: string;
creator_id?: string;
editor_account?: string;
editor_id?: string;
id?: string;
kb_id?: string;
meta?: DomainNodeMeta;
name?: string;
parent_id?: string;
permissions?: DomainNodePermissions;
publisher_account?: string;
publisher_id?: string;
status?: DomainNodeStatus;
type?: DomainNodeType;
updated_at?: string;
}
export interface V1StatCountResp {
conversation_count?: number;
ip_count?: number;

View File

@ -1,6 +0,0 @@
import custom from './custom';
import dark from './dark';
import light from './light';
export { custom, dark, light };
export type ThemeColor = typeof light;

View File

@ -1,4 +0,0 @@
export default {
selectPopupBoxShadow: '0px 10px 20px 0px rgba(54,59,76,0.2)',
selectedMenuItemBgColor: 'rgba(50,72,242,0.1)',
};

View File

@ -51,7 +51,7 @@ const dark = {
text: {
primary: '#fff',
secondary: 'rgba(255,255,255,0.7)',
auxiliary: 'rgba(255,255,255,0.5)',
tertiary: 'rgba(255,255,255,0.5)',
disabled: 'rgba(255,255,255,0.26)',
slave: 'rgba(255,255,255,0.05)',
inverseAuxiliary: 'rgba(0,0,0,0.5)',

View File

@ -56,7 +56,7 @@ const light = {
text: {
primary: '#21222D',
secondary: 'rgba(33,34,35,0.7)',
auxiliary: 'rgba(33,34,35,0.5)',
tertiary: '#646a73',
slave: 'rgba(33,34,35,0.3)',
disabled: 'rgba(33,34,35,0.2)',
inverse: '#FFFFFF',

View File

@ -1,460 +0,0 @@
import { addOpacityToColor } from '@/utils';
import { custom, ThemeColor } from './color';
declare module '@mui/material/styles' {}
declare module '@mui/material/styles' {
interface TypeBackground {
paper0?: string;
paper2?: string;
chip?: string;
circle?: string;
hover?: string;
focus?: string;
disabled?: string;
}
}
const componentStyleOverrides = (theme: ThemeColor) => {
return {
MuiTabs: {
styleOverrides: {
root: {
borderRadius: '10px !important',
overflow: 'hidden',
minHeight: '36px',
height: '36px',
padding: '0px !important',
},
indicator: {
borderRadius: '0px !important',
overflow: 'hidden',
backgroundColor: '#21222D !important',
},
},
},
MuiTab: {
styleOverrides: {
root: {
borderRadius: '0px !important',
fontWeight: 'normal',
fontSize: '14px !important',
lineHeight: '34px',
padding: '0 16px !important',
},
},
},
MuiFormLabel: {
styleOverrides: {
asterisk: {
color: theme.error.main,
},
},
},
MuiButton: {
styleOverrides: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
root: ({ ownerState }: { ownerState: any }) => {
return {
height: '36px',
fontSize: 14,
lineHeight: '36px',
paddingLeft: '16px',
paddingRight: '16px',
boxShadow: 'none',
transition: 'all 0.2s ease-in-out',
borderRadius: '10px',
fontWeight: '400',
...(ownerState.variant === 'contained' && {
color: theme.text.inverse,
backgroundColor: theme.text.primary,
}),
...(ownerState.variant === 'text' && {}),
...(ownerState.variant === 'outlined' && {
color: theme.text.primary,
border: `1px solid ${theme.text.primary}`,
}),
...(ownerState.disabled === true && {
cursor: 'not-allowed !important',
}),
...(ownerState.size === 'small' && {
height: '32px',
lineHeight: '32px',
}),
'&:hover': {
boxShadow: 'none',
...(ownerState.variant === 'contained' && {
backgroundColor: addOpacityToColor(theme.text.primary, 0.9),
}),
...(ownerState.variant === 'text' && {
backgroundColor: theme.background.paper3,
}),
...(ownerState.variant === 'outlined' && {
backgroundColor: theme.background.paper3,
}),
...(ownerState.color === 'neutral' && {
color: theme.text.primary,
}),
},
};
},
startIcon: {
marginLeft: 0,
marginRight: 8,
'>*:nth-of-type(1)': {
fontSize: 14,
},
},
},
},
MuiTooltip: {
styleOverrides: {
tooltip: {
borderRadius: '10px',
maxWidth: '600px',
padding: '8px 16px',
backgroundColor: theme.text.primary,
fontSize: '12px',
lineHeight: '20px',
color: theme.primary.contrastText,
},
arrow: {
color: theme.text.primary,
},
},
},
MuiFormHelperText: {
styleOverrides: {
root: {
color: theme.error.main,
},
},
},
MuiFormControl: {
styleOverrides: {
root: {
'.MuiFormLabel-asterisk': {
color: theme.error.main,
},
},
},
},
MuiFormControlLabel: {
styleOverrides: {
root: {
marginLeft: '0 !important',
},
},
},
MuiTableBody: {
styleOverrides: {
root: {
'.MuiTableRow-root:hover': {
'.MuiTableCell-root:not(.cx-table-empty-td)': {
backgroundColor: theme.table.row.hoverColor,
overflowX: 'hidden',
'.primary-color': {
color: theme.primary.main,
},
'.no-title-url': {
color: `${theme.primary.main} !important`,
},
'.error-color': {
opacity: 1,
},
},
},
},
},
},
MuiCheckbox: {
styleOverrides: {
root: {
padding: 0,
svg: {
fontSize: '18px',
},
},
},
},
MuiTableCell: {
styleOverrides: {
root: {
background: theme.background.paper,
lineHeight: 1.5,
height: theme.table.cell.height,
fontSize: '14px',
paddingTop: '16px !important',
paddingBottom: '16px !important',
paddingLeft: 0,
'&:first-of-type': {
paddingLeft: '0px',
},
'&:not(:first-of-type)': {
paddingLeft: '0px',
},
'.MuiCheckbox-root': {
color: '#CCCCCC',
svg: {
fontSize: '16px',
},
'&.Mui-checked': {
color: theme.text.primary,
},
},
},
head: {
backgroundColor: theme.background.paper3,
color: theme.table.head.color,
fontSize: '12px',
height: theme.table.head.height,
paddingTop: '0 !important',
paddingBottom: '0 !important',
borderSpacing: '12px',
zIndex: 100,
},
body: {
borderBottom: '1px dashed',
borderColor: theme.table.cell.borderColor,
borderSpacing: '12px',
},
},
},
MuiPopover: {
styleOverrides: {
paper: {
borderRadius: '10px',
boxShadow: custom.selectPopupBoxShadow,
},
},
},
MuiMenu: {
styleOverrides: {
paper: {
padding: '4px',
borderRadius: '10px',
backgroundColor: theme.background.paper,
boxShadow: custom.selectPopupBoxShadow,
},
list: {
paddingTop: '0px !important',
paddingBottom: '0px !important',
},
},
defaultProps: {
elevation: 0,
},
},
MuiMenuItem: {
styleOverrides: {
root: {
height: '40px',
borderRadius: '5px',
':hover': {
backgroundColor: theme.background.paper3,
},
'&.Mui-selected': {
fontWeight: '500',
backgroundColor: `${custom.selectedMenuItemBgColor} !important`,
color: theme.primary.main,
},
},
},
},
MuiPaper: {
defaultProps: {
elevation: 1,
},
styleOverrides: {
root: ({ ownerState }: { ownerState: { elevation?: number } }) => {
return {
...(ownerState.elevation === 0 && {
backgroundColor: theme.background.paper2,
}),
...(ownerState.elevation === 2 && {
backgroundColor: theme.background.paper3,
}),
backgroundImage: 'none',
};
},
},
},
MuiChip: {
styleOverrides: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
root: ({ ownerState }: { ownerState: any }) => {
return {
height: '24px',
lineHeight: '24px',
borderRadius: '8px',
'.MuiChip-label': {
padding: '0 8px 0 4px',
},
...(ownerState.color === 'default' && {
backgroundColor: theme.background.chip,
borderColor: theme.text.disabled,
'.Mui-focusVisible': {
backgroundColor: theme.background.chip,
},
}),
...(ownerState.color === 'error' && {
backgroundColor: addOpacityToColor(theme.error.main, 0.1),
}),
};
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
label: ({ ownerState }: { ownerState: any }) => {
return {
padding: '0 14px',
fontSize: '14px',
lineHeight: '24px',
...(ownerState.color === 'default' && {
color: theme.text.primary,
}),
};
},
deleteIcon: {
fontSize: '14px',
color: theme.text.disabled,
},
},
},
MuiAppBar: {
defaultProps: {
elevation: 1,
},
},
MuiDialog: {
styleOverrides: {
root: {
'h2.MuiTypography-root button': {
marginRight: '2px',
},
'.MuiDialogActions-root': {
paddingTop: '24px',
button: {
width: '88px',
height: '36px !important',
},
'.MuiButton-text': {
width: 'auto',
minWidth: 'auto',
color: `${theme.text.primary} !important`,
},
},
},
container: {
height: '100vh',
bgcolor: theme.text.secondary,
backdropFilter: 'blur(5px)',
},
paper: {
pb: 1,
border: '1px solid',
borderColor: theme.divider,
borderRadius: '10px',
backgroundColor: theme.background.paper,
textarea: {
borderRadius: '8px 8px 0 8px',
},
},
},
},
MuiDialogTitle: {
styleOverrides: {
root: {
paddingTop: '24px',
'> button': {
top: '20px',
},
},
},
},
MuiAlert: {
styleOverrides: {
root: {
lineHeight: '22px',
paddingTop: '1px',
paddingBottom: '1px',
borderRadius: '10px',
boxShadow: 'none',
},
icon: {
padding: '10px 0',
},
standardInfo: {
backgroundColor: addOpacityToColor(theme.primary.main, 0.1),
color: theme.text.primary,
},
},
},
MuiRadio: {
styleOverrides: {
root: {
padding: 0,
marginRight: '8px',
},
},
},
MuiTextField: {
styleOverrides: {
root: {
label: {
color: theme.text.secondary,
},
'label.Mui-focused': {
color: theme.text.primary,
},
'& .MuiInputBase-input::placeholder': {
fontSize: '12px',
},
},
},
},
MuiInputBase: {
styleOverrides: {
root: {
borderRadius: '10px !important',
backgroundColor: theme.background.paper3,
'.MuiOutlinedInput-notchedOutline': {
borderColor: `${theme.background.paper3} !important`,
borderWidth: '1px !important',
},
'&.Mui-focused': {
'.MuiOutlinedInput-notchedOutline': {
borderColor: `${theme.text.primary} !important`,
borderWidth: '1px !important',
},
},
'&:hover': {
'.MuiOutlinedInput-notchedOutline': {
borderColor: `${theme.text.primary} !important`,
borderWidth: '1px !important',
},
},
input: {
height: '19px',
'&.Mui-disabled': {
color: `${theme.text.secondary} !important`,
WebkitTextFillColor: `${theme.text.secondary} !important`,
},
},
},
},
},
MuiSelect: {
styleOverrides: {
root: {
height: '36px',
borderRadius: '10px !important',
backgroundColor: theme.background.paper3,
},
select: {
paddingRight: '0 !important',
},
},
},
};
};
export default componentStyleOverrides;

View File

@ -25,7 +25,6 @@
"highlight.js": "^11.11.1",
"html-react-parser": "^5.2.5",
"html-to-image": "^1.11.13",
"import-in-the-middle": "^1.14.2",
"js-cookie": "^3.0.5",
"katex": "^0.16.22",
"markdown-it": "13.0.1",
@ -42,7 +41,6 @@
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"require-in-the-middle": "^7.5.2",
"uuid": "^11.1.0"
},
"devDependencies": {

View File

@ -1,7 +1,7 @@
import SSEClient from '@/utils/fetch';
import { Box, Divider, Stack } from '@mui/material';
import { Editor, useTiptap, UseTiptapReturn } from '@ctzhian/tiptap';
import { Modal } from '@ctzhian/ui';
import { Box, Divider, Stack } from '@mui/material';
import { useCallback, useEffect, useRef, useState } from 'react';
interface AIGenerateProps {
@ -126,7 +126,7 @@ const AIGenerate = ({
ml: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'text.auxiliary',
color: 'text.tertiary',
}}
>
@ -150,7 +150,7 @@ const AIGenerate = ({
ml: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'text.auxiliary',
color: 'text.tertiary',
}}
>

View File

@ -1,10 +1,10 @@
'use client';
import { V1NodeDetailResp } from '@/request/types';
import { Ellipsis, Icon } from '@ctzhian/ui';
import { Box, Button, Skeleton, Stack } from '@mui/material';
import { IconBaocun } from '@panda-wiki/icons';
import { Box, Button, IconButton, Skeleton, Stack } from '@mui/material';
import { Ellipsis, Icon, message } from '@ctzhian/ui';
import dayjs from 'dayjs';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';
import { useWrapContext } from '..';
interface HeaderProps {
@ -46,7 +46,7 @@ const Header = ({ edit, detail, handleSave }: HeaderProps) => {
onClick={() => setCatalogOpen(true)}
sx={{
cursor: 'pointer',
color: 'text.auxiliary',
color: 'text.tertiary',
':hover': {
color: 'text.primary',
},
@ -78,7 +78,7 @@ const Header = ({ edit, detail, handleSave }: HeaderProps) => {
direction={'row'}
alignItems={'center'}
gap={0.5}
sx={{ fontSize: 12, color: 'text.auxiliary' }}
sx={{ fontSize: 12, color: 'text.tertiary' }}
>
<IconBaocun sx={{ fontSize: 12 }} />
{nodeDetail?.updated_at ? (

View File

@ -1,7 +1,7 @@
'use client';
import { Box, Skeleton, Stack } from '@mui/material';
import { useTiptap } from '@ctzhian/tiptap';
import { Icon } from '@ctzhian/ui';
import { Box, Skeleton, Stack } from '@mui/material';
import { useState } from 'react';
import Header from './Header';
import Toolbar from './Toolbar';
@ -61,11 +61,11 @@ const LoadingEditorWrap = () => {
</Stack>
<Stack direction={'row'} alignItems={'center'} gap={2} sx={{ mb: 4 }}>
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
<Icon type='icon-a-shijian2' sx={{ color: 'text.auxiliary' }} />
<Icon type='icon-a-shijian2' sx={{ color: 'text.tertiary' }} />
<Skeleton variant='text' width={130} height={24} />
</Stack>
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
<Icon type='icon-ziti' sx={{ color: 'text.auxiliary' }} />
<Icon type='icon-ziti' sx={{ color: 'text.tertiary' }} />
<Skeleton variant='text' width={80} height={24} />
</Stack>
</Stack>

View File

@ -30,7 +30,7 @@ const HeadingIcon = [
const HeadingSx = [
{ fontSize: 14, fontWeight: 700, color: 'text.secondary' },
{ fontSize: 14, fontWeight: 400, color: 'text.auxiliary' },
{ fontSize: 14, fontWeight: 400, color: 'text.tertiary' },
{ fontSize: 14, fontWeight: 400, color: 'text.disabled' },
];
@ -117,7 +117,7 @@ const Toc = ({
sx={{
fontSize: 14,
fontWeight: 'bold',
color: 'text.auxiliary',
color: 'text.tertiary',
mb: 1,
p: 1,
pb: 0,

View File

@ -267,10 +267,10 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
gap={0.5}
sx={{
fontSize: 12,
color: 'text.auxiliary',
color: 'text.tertiary',
cursor: 'text',
':hover': {
color: 'text.auxiliary',
color: 'text.tertiary',
},
}}
>
@ -286,7 +286,7 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
direction={'row'}
alignItems={'center'}
gap={0.5}
sx={{ fontSize: 12, color: 'text.auxiliary' }}
sx={{ fontSize: 12, color: 'text.tertiary' }}
>
<IconZiti />
{characterCount}

View File

@ -18,7 +18,7 @@
"license": "ISC",
"packageManager": "pnpm@10.12.1",
"dependencies": {
"@ctzhian/tiptap": "^1.11.4",
"@ctzhian/tiptap": "^1.12.5",
"@ctzhian/ui": "^7.0.5",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",

View File

@ -125,7 +125,7 @@ const StyledTab = styled(Tab)(({ theme }) => ({
},
'&.Mui-selected': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.background.default,
color: theme.palette.primary.contrastText,
fontWeight: 500,
},
}));

View File

@ -97,199 +97,198 @@ const Footer = React.memo(
},
})}
>
{showBrand && (
<Box
pt={
customStyle?.footer_show_intro
<Box
pt={
customStyle?.footer_show_intro
? 5
: (footerSetting?.brand_groups?.length || 0) > 0
? 5
: (footerSetting?.brand_groups?.length || 0) > 0
? 5
: 0
}
>
{customStyle?.footer_show_intro !== false && (
<Box sx={{ mb: 3 }}>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={24}
/>
)}
<Box
sx={{
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'white',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={{
fontSize: 12,
lineHeight: '26px',
mt: 2,
color: 'rgba(255, 255, 255, 0.70)',
}}
>
{footerSetting.brand_desc}
</Box>
: 0
}
>
{customStyle?.footer_show_intro !== false && (
<Box sx={{ mb: 3 }}>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={24}
/>
)}
<Stack direction={'column'} gap={2.5} mt={2}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
color: alpha(theme.palette.text.primary, 0.7),
})}
gap={1}
onClick={() => {
setCurOverlayType(account.channel || '');
if (account.channel === 'phone') {
setPhoneData({
phone: account.phone || '',
text: account.text || '',
});
setOpen(true);
}
if (account.channel === 'wechat_oa') {
setWechatData({
src: account.icon || '',
text: account.text || '',
});
setOpen(true);
}
}}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
/>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
direction={'column'}
alignItems={'center'}
bgcolor={'#fff'}
p={1.5}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={83}
height={83}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: '#21222D',
maxWidth: '83px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Box>
)}
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{footerSetting?.brand_groups?.map((group, idx) => (
<Stack
gap={1}
key={group.name}
<Box
sx={{
fontSize: 14,
lineHeight: '22px',
width: 'calc(50% - 8px)',
...(idx > 1 && {
mt: 1,
}),
'& a:hover': {
color: 'primary.main',
},
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'white',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={{
fontSize: 12,
lineHeight: '26px',
mt: 2,
color: 'rgba(255, 255, 255, 0.70)',
}}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={2.5} mt={2}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
color: alpha(theme.palette.text.primary, 0.7),
})}
gap={1}
onClick={() => {
setCurOverlayType(account.channel || '');
if (account.channel === 'phone') {
setPhoneData({
phone: account.phone || '',
text: account.text || '',
});
setOpen(true);
}
if (account.channel === 'wechat_oa') {
setWechatData({
src: account.icon || '',
text: account.text || '',
});
setOpen(true);
}
}}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
/>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
direction={'column'}
alignItems={'center'}
bgcolor={'#fff'}
p={1.5}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={83}
height={83}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: '#21222D',
maxWidth: '83px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Box>
)}
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{footerSetting?.brand_groups?.map((group, idx) => (
<Stack
gap={1}
key={group.name}
sx={{
fontSize: 14,
lineHeight: '22px',
width: 'calc(50% - 8px)',
...(idx > 1 && {
mt: 1,
}),
'& a:hover': {
color: 'primary.main',
},
}}
>
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
color: '#ffffff',
}}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
color: '#ffffff',
}}
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
key={link.name}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
)}
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
{!(
customStyle?.footer_show_intro === false &&
footerSetting?.brand_groups?.length === 0
@ -478,241 +477,239 @@ const Footer = React.memo(
// }),
}}
>
{showBrand && (
<Box
py={
customStyle?.footer_show_intro
<Box
py={
customStyle?.footer_show_intro
? 6
: (footerSetting?.brand_groups?.length || 0) > 0
? 6
: (footerSetting?.brand_groups?.length || 0) > 0
? 6
: 0
: 0
}
>
<Stack
direction={'row'}
gap={10}
justifyContent={
customStyle?.footer_show_intro === false
? 'center'
: 'flex-start'
}
>
<Stack
direction={'row'}
gap={10}
justifyContent={
customStyle?.footer_show_intro === false
? 'center'
: 'flex-start'
}
>
{customStyle?.footer_show_intro !== false && (
<Stack
direction={'column'}
sx={{ width: '30%', minWidth: 200 }}
gap={3}
>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={36}
/>
)}
<Box
sx={{
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'text.primary',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 14,
lineHeight: '26px',
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={'26px'}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
'&:hover': {
color: theme.palette.primary.main,
},
'&:hover .popup': {
display: 'flex !important',
},
color: alpha(theme.palette.text.primary, 0.7),
cursor: 'default',
})}
gap={1}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
></IconWeixingongzhonghao>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
className={'popup'}
direction={'column'}
alignItems={'center'}
bgcolor={'#fff'}
p={1.5}
sx={theme => ({
position: 'absolute',
top: '40px',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={120}
height={120}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '120px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
{account.channel === 'phone' &&
account?.phone && (
<Stack
className={'popup'}
bgcolor={'#fff'}
px={1.5}
py={1}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
'0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
display={'none'}
zIndex={999}
>
{account.phone && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
textAlign: 'center',
}}
>
{account.phone}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Stack>
)}
{customStyle?.footer_show_intro !== false && (
<Stack
direction={'row'}
width={'100%'}
justifyContent={'flex-start'}
flexWrap='wrap'
direction={'column'}
sx={{ width: '30%', minWidth: 200 }}
gap={3}
>
{footerSetting?.brand_groups?.map(group => (
<Stack
gap={1.5}
key={group.name}
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={36}
/>
)}
<Box
sx={{
flex: '0 0 33.33%',
fontSize: 14,
lineHeight: '22px',
minWidth: '100px',
'& a:hover': {
color: 'primary.main',
},
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'text.primary',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 14,
lineHeight: '26px',
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={'26px'}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
'&:hover': {
color: theme.palette.primary.main,
},
'&:hover .popup': {
display: 'flex !important',
},
color: alpha(theme.palette.text.primary, 0.7),
cursor: 'default',
})}
gap={1}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
></IconWeixingongzhonghao>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
className={'popup'}
direction={'column'}
alignItems={'center'}
bgcolor={'#fff'}
p={1.5}
sx={theme => ({
position: 'absolute',
top: '40px',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={120}
height={120}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '120px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
{account.channel === 'phone' && account?.phone && (
<Stack
className={'popup'}
bgcolor={'#fff'}
px={1.5}
py={1}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
'0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
display={'none'}
zIndex={999}
>
{account.phone && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
textAlign: 'center',
}}
>
{account.phone}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Stack>
)}
<Stack
direction={'row'}
width={'100%'}
justifyContent={'flex-start'}
flexWrap='wrap'
>
{footerSetting?.brand_groups?.map(group => (
<Stack
gap={1.5}
key={group.name}
sx={{
flex: '0 0 33.33%',
fontSize: 14,
lineHeight: '22px',
minWidth: '100px',
'& a:hover': {
color: 'primary.main',
},
}}
>
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
color: 'text.primary',
}}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
color: 'text.primary',
}}
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
key={link.name}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
)}
</Stack>
</Box>
{!(
customStyle?.footer_show_intro === false &&
footerSetting?.brand_groups?.length === 0

View File

@ -94,197 +94,196 @@ const Footer = React.memo(
bgcolor: alpha(theme.palette.text.primary, 0.05),
})}
>
{showBrand && (
<Box
pt={
customStyle?.footer_show_intro
<Box
pt={
customStyle?.footer_show_intro
? 5
: (footerSetting?.brand_groups?.length || 0) > 0
? 5
: (footerSetting?.brand_groups?.length || 0) > 0
? 5
: 0
}
>
{customStyle?.footer_show_intro !== false && (
<Box sx={{ mb: 3 }}>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={24}
/>
)}
<Box
sx={{
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'text.primary',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 12,
lineHeight: '26px',
mt: 2,
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
: 0
}
>
{customStyle?.footer_show_intro !== false && (
<Box sx={{ mb: 3 }}>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={24}
/>
)}
<Stack direction={'column'} gap={2.5} mt={2}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
color: alpha(theme.palette.text.primary, 0.7),
})}
gap={1}
onClick={() => {
setCurOverlayType(account.channel || '');
if (account.channel === 'phone') {
setPhoneData({
phone: account.phone || '',
text: account.text || '',
});
setOpen(true);
}
if (account.channel === 'wechat_oa') {
setWechatData({
src: account.icon || '',
text: account.text || '',
});
setOpen(true);
}
}}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
/>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '12px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
direction={'column'}
alignItems={'center'}
p={1.5}
sx={theme => ({
position: 'absolute',
top: '40px',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={83}
height={83}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '83px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Box>
)}
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{footerSetting?.brand_groups?.map((group, idx) => (
<Stack
gap={1}
key={group.name}
<Box
sx={{
fontSize: 14,
lineHeight: '22px',
width: 'calc(50% - 8px)',
...(idx > 1 && {
mt: 1,
}),
'& a:hover': {
color: 'primary.main',
},
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
color: 'text.primary',
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 12,
lineHeight: '26px',
mt: 2,
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={2.5} mt={2}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
sx={theme => ({
position: 'relative',
color: alpha(theme.palette.text.primary, 0.7),
})}
gap={1}
onClick={() => {
setCurOverlayType(account.channel || '');
if (account.channel === 'phone') {
setPhoneData({
phone: account.phone || '',
text: account.text || '',
});
setOpen(true);
}
if (account.channel === 'wechat_oa') {
setWechatData({
src: account.icon || '',
text: account.text || '',
});
setOpen(true);
}
}}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '20px', color: 'inherit' }}
/>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '20px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '12px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
direction={'column'}
alignItems={'center'}
p={1.5}
sx={theme => ({
position: 'absolute',
top: '40px',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={83}
height={83}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '83px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Box>
)}
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
{footerSetting?.brand_groups?.map((group, idx) => (
<Stack
gap={1}
key={group.name}
sx={{
fontSize: 14,
lineHeight: '22px',
width: 'calc(50% - 8px)',
...(idx > 1 && {
mt: 1,
}),
'& a:hover': {
color: 'primary.main',
},
}}
>
<Box
sx={{
fontSize: 14,
lineHeight: '24px',
mb: 1,
color: 'text.primary',
}}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={{
fontSize: 14,
lineHeight: '24px',
mb: 1,
color: 'text.primary',
}}
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
key={link.name}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
)}
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
{!(
customStyle?.footer_show_intro === false &&
footerSetting?.brand_groups?.length === 0
@ -456,239 +455,237 @@ const Footer = React.memo(
width: '100%',
}}
>
{showBrand && (
<Box
py={
customStyle?.footer_show_intro
<Box
py={
customStyle?.footer_show_intro
? 6
: (footerSetting?.brand_groups?.length || 0) > 0
? 6
: (footerSetting?.brand_groups?.length || 0) > 0
? 6
: 0
: 0
}
>
<Stack
direction={'row'}
gap={10}
justifyContent={
customStyle?.footer_show_intro === false
? 'center'
: 'flex-start'
}
>
<Stack
direction={'row'}
gap={10}
justifyContent={
customStyle?.footer_show_intro === false
? 'center'
: 'flex-start'
}
>
{customStyle?.footer_show_intro !== false && (
<Stack
direction={'column'}
sx={{ width: '30%', minWidth: 200 }}
gap={3}
>
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={36}
/>
)}
<Box
sx={{
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 14,
lineHeight: '26px',
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={'26px'}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
alignItems='center'
sx={theme => ({
position: 'relative',
'&:hover': {
color: theme.palette.primary.main,
},
'&:hover .popup': {
display: 'flex !important',
},
color: alpha(theme.palette.text.primary, 0.7),
cursor: 'default',
})}
gap={1}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '18px', color: 'inherit' }}
></IconWeixingongzhonghao>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '16px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
className={'popup'}
direction={'column'}
alignItems={'center'}
p={1.5}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={120}
height={120}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '120px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
{account.channel === 'phone' &&
account?.phone && (
<Stack
className={'popup'}
px={1.5}
py={1}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
'0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
display={'none'}
zIndex={999}
>
{account.phone && (
<Box
sx={{
fontSize: '14px',
lineHeight: '16px',
color: 'text.primary',
textAlign: 'center',
}}
>
{account.phone}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Stack>
)}
{customStyle?.footer_show_intro !== false && (
<Stack
direction={'row'}
width={'100%'}
justifyContent={'flex-start'}
flexWrap='wrap'
direction={'column'}
sx={{ width: '30%', minWidth: 200 }}
gap={3}
>
{footerSetting?.brand_groups?.map(group => (
<Stack
gap={1.5}
key={group.name}
<Stack direction={'row'} alignItems={'center'} gap={1}>
{footerSetting?.brand_logo && (
<img
src={footerSetting.brand_logo}
alt='PandaWiki'
height={36}
/>
)}
<Box
sx={{
flex: '0 0 33.33%',
fontSize: 14,
lineHeight: '22px',
minWidth: '100px',
'& a:hover': {
color: 'primary.main',
},
fontWeight: 'bold',
lineHeight: '32px',
fontSize: 24,
}}
>
{footerSetting?.brand_name}
</Box>
</Stack>
{footerSetting?.brand_desc && (
<Box
sx={theme => ({
fontSize: 14,
lineHeight: '26px',
color: alpha(theme.palette.text.primary, 0.7),
})}
>
{footerSetting.brand_desc}
</Box>
)}
<Stack direction={'column'} gap={'26px'}>
{customStyle?.social_media_accounts?.map(
(account, index) => {
return (
<Stack
direction={'row'}
key={index}
alignItems='center'
sx={theme => ({
position: 'relative',
'&:hover': {
color: theme.palette.primary.main,
},
'&:hover .popup': {
display: 'flex !important',
},
color: alpha(theme.palette.text.primary, 0.7),
cursor: 'default',
})}
gap={1}
>
{account.channel === 'wechat_oa' && (
<IconWeixingongzhonghao
sx={{ fontSize: '18px', color: 'inherit' }}
></IconWeixingongzhonghao>
)}
{account.channel === 'phone' && (
<IconDianhua
sx={{ fontSize: '16px', color: 'inherit' }}
></IconDianhua>
)}
<Box
sx={{
lineHeight: '24px',
fontSize: '14px',
color: 'inherit',
}}
>
{account.text}
</Box>
{account.channel === 'wechat_oa' &&
(account?.text || account?.icon) && (
<Stack
className={'popup'}
direction={'column'}
alignItems={'center'}
p={1.5}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
' 0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
gap={1}
display={'none'}
zIndex={999}
>
{account.icon && (
<img
src={account.icon}
width={120}
height={120}
></img>
)}
{account.text && (
<Box
sx={{
fontSize: '12px',
lineHeight: '16px',
color: 'text.primary',
maxWidth: '120px',
textAlign: 'center',
}}
>
{account.text}
</Box>
)}
</Stack>
)}
{account.channel === 'phone' && account?.phone && (
<Stack
className={'popup'}
px={1.5}
py={1}
sx={theme => ({
position: 'absolute',
bottom: '100%',
transform: 'translateY(-10px)',
left: 0,
boxShadow:
'0px 4px 8px 0px ' +
alpha(theme.palette.text.primary, 0.25),
borderRadius: '4px',
bgcolor: theme.palette.background.default,
})}
display={'none'}
zIndex={999}
>
{account.phone && (
<Box
sx={{
fontSize: '14px',
lineHeight: '16px',
color: 'text.primary',
textAlign: 'center',
}}
>
{account.phone}
</Box>
)}
</Stack>
)}
</Stack>
);
},
)}
</Stack>
</Stack>
)}
<Stack
direction={'row'}
width={'100%'}
justifyContent={'flex-start'}
flexWrap='wrap'
>
{footerSetting?.brand_groups?.map(group => (
<Stack
gap={1.5}
key={group.name}
sx={{
flex: '0 0 33.33%',
fontSize: 14,
lineHeight: '22px',
minWidth: '100px',
'& a:hover': {
color: 'primary.main',
},
}}
>
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
}}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={{
fontSize: 16,
lineHeight: '24px',
mb: 1,
}}
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
key={link.name}
>
{group.name}
</Box>
{group.links?.map(link => (
<Box
sx={theme => ({
color: alpha(theme.palette.text.primary, 0.5),
})}
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
<Link
href={link?.url || ''}
target='_blank'
key={link.name}
>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
{link.name}
</Link>
</Box>
))}
</Stack>
))}
</Stack>
</Box>
)}
</Stack>
</Box>
{!(
customStyle?.footer_show_intro === false &&
footerSetting?.brand_groups?.length === 0

View File

@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@ctzhian/tiptap':
specifier: ^1.11.4
version: 1.11.4(e4f25edbbaea9249b1b10d38414e9a30)
specifier: ^1.12.5
version: 1.12.5(e4f25edbbaea9249b1b10d38414e9a30)
'@ctzhian/ui':
specifier: ^7.0.5
version: 7.0.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/icons-material@7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/utils@7.3.3(@types/react@19.2.2)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@ -262,9 +262,6 @@ importers:
html-to-image:
specifier: ^1.11.13
version: 1.11.13
import-in-the-middle:
specifier: ^1.14.2
version: 1.15.0
js-cookie:
specifier: ^3.0.5
version: 3.0.5
@ -313,9 +310,6 @@ importers:
remark-math:
specifier: ^6.0.0
version: 6.0.0
require-in-the-middle:
specifier: ^7.5.2
version: 7.5.2
uuid:
specifier: ^11.1.0
version: 11.1.0
@ -520,8 +514,8 @@ packages:
react: '>=16.9.0'
react-dom: '>=16.9.0'
'@ctzhian/tiptap@1.11.4':
resolution: {integrity: sha512-xJNTjlDCXF7cxSRw+4ki2VViKCKEWkB5pauvXz9Y8J3VAhN1J7Tt0uP26EGPDlUGIJ9dSdFpKIGM7JPLnU47fw==}
'@ctzhian/tiptap@1.12.5':
resolution: {integrity: sha512-ivWUnebIt1ECS0apUSss0U2tKk4QJYAH1/qE0Xl/AkYREbAMnI+McavmqDfN1R8alOadBO3L20Wq4rLbhldc1w==}
peerDependencies:
'@emotion/react': ^11
'@emotion/styled': ^11
@ -921,92 +915,78 @@ packages:
resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.3':
resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.3':
resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.3':
resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.3':
resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.3':
resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.3':
resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.4':
resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.4':
resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.4':
resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.4':
resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.4':
resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.4':
resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.4':
resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.4':
resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==}
@ -1191,28 +1171,24 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@15.4.6':
resolution: {integrity: sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@15.4.6':
resolution: {integrity: sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@15.4.6':
resolution: {integrity: sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@15.4.6':
resolution: {integrity: sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==}
@ -1519,67 +1495,56 @@ packages:
resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.52.5':
resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.52.5':
resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.52.5':
resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.52.5':
resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.52.5':
resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.52.5':
resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.52.5':
resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.52.5':
resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.52.5':
resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.52.5':
resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.52.5':
resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==}
@ -2413,49 +2378,41 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@ -6038,7 +5995,7 @@ snapshots:
- react-native
- typescript
'@ctzhian/tiptap@1.11.4(e4f25edbbaea9249b1b10d38414e9a30)':
'@ctzhian/tiptap@1.12.5(e4f25edbbaea9249b1b10d38414e9a30)':
dependencies:
'@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)