增加后台管理项目;

This commit is contained in:
karlcarlo
2025-12-12 03:29:09 +08:00
parent 420fed4bcf
commit 41d2d893d3
30 changed files with 4746 additions and 632 deletions

View File

@@ -1 +1 @@
177144 285640

View File

@@ -24,8 +24,27 @@ async def lifespan(app: FastAPI):
) # 批量注册模型 ) # 批量注册模型
print(f"🗄️ 数据库连接成功: {database_name}") print(f"🗄️ 数据库连接成功: {database_name}")
# 初始化GLM服务,确保默认角色存在
try:
from app.services.glm_service import get_glm_service
glm_service = get_glm_service()
await glm_service.ensure_default_role()
print(f"✅ GLM角色配置初始化完成")
except Exception as e:
print(f"⚠️ GLM角色配置初始化失败: {e}")
yield # 应用在此处进入正常运行状态,接收请求 yield # 应用在此处进入正常运行状态,接收请求
# 关闭GLM服务
try:
from app.services.glm_service import close_glm_service
close_glm_service()
print(f"GLM服务已关闭")
except Exception as e:
print(f"关闭GLM服务时出错: {e}")
# 关闭数据库连接 # 关闭数据库连接
await client.close() await client.close()
print(f"数据库连接关闭") print(f"数据库连接关闭")

View File

@@ -1,7 +1,8 @@
from .base import BaseOut from .base import BaseOut
from .message import Message from .message import Message
from .glm_role import GLMRole
models = [Message] models = [Message, GLMRole]
# 在所有模型导入后调用 model_rebuild 来解决循环导入问题 # 在所有模型导入后调用 model_rebuild 来解决循环导入问题
# Message.model_rebuild() # Message.model_rebuild()

View File

@@ -0,0 +1,56 @@
from datetime import datetime
from typing import Optional
from pydantic import Field
from beanie import Document
import arrow
class GLMRole(Document):
"""GLM角色配置模型"""
# 基本信息
name: str = Field(..., description="角色名称")
description: str = Field(..., description="角色描述")
skills: str = Field(default="", description="技能列表(多行文本字符串)")
restrictions: str = Field(default="", description="限制列表(多行文本字符串)")
# 状态管理
is_active: bool = Field(default=True, description="是否激活")
is_default: bool = Field(default=False, description="是否为默认角色")
# 时间戳
created_at: datetime = Field(default_factory=lambda: arrow.now().datetime)
updated_at: datetime = Field(default_factory=lambda: arrow.now().datetime)
class Settings:
name = "glm_roles" # MongoDB集合名称
indexes = [
[("is_active", 1)], # 按激活状态查询
[("is_default", 1)], # 按默认状态查询
[("created_at", -1)], # 按创建时间排序
]
def update_timestamp(self):
"""更新更新时间戳"""
self.updated_at = arrow.now().datetime
@classmethod
def from_role_config(cls, role_config: dict) -> "GLMRole":
"""从角色配置字典创建GLMRole实例"""
return cls(
name=role_config.get("name", ""),
description=role_config.get("description", ""),
skills=role_config.get("skills", ""),
restrictions=role_config.get("restrictions", ""),
is_active=True,
is_default=True,
)
def to_role_config_dict(self) -> dict:
"""转换为角色配置字典"""
return {
"name": self.name,
"description": self.description,
"skills": self.skills,
"restrictions": self.restrictions,
}

View File

@@ -1,12 +1,18 @@
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Optional from typing import List, Optional
import httpx import httpx
import os import os
import logging
from app.services.glm_service import get_glm_service, GLMService
from app.models.base import BaseOut
# 创建API路由,使用/glm前缀 # 创建API路由,使用/glm前缀
router = APIRouter(prefix="/glm", tags=["GLM聊天"]) router = APIRouter(prefix="/glm", tags=["GLM聊天"])
logger = logging.getLogger(__name__)
# 数据模型 # 数据模型
class Message(BaseModel): class Message(BaseModel):
@@ -27,73 +33,79 @@ class ChatResponse(BaseModel):
class RoleConfig(BaseModel): class RoleConfig(BaseModel):
name: str name: str
description: str description: str
skills: List[str] skills: str # 多行文本字符串
restrictions: List[str] restrictions: str # 多行文本字符串
# 默认角色配置 # 默认角色配置(使用字符串格式)
DEFAULT_ROLE_CONFIG = RoleConfig( DEFAULT_ROLE_CONFIG = RoleConfig(
name="兜兜", name="兜兜",
description="你是兜兜,1989年出生,有一个5岁的女儿(2018年出生)。你是一位对用户负责、选品严格的宝妈客服。当用户给你发来疑问时,你总能第一时间回复,并以亲切温暖的风格与用户交流。", description="你是兜兜,1989年出生,有一个5岁的女儿(2018年出生)。你是一位对用户负责、选品严格的宝妈客服。当用户给你发来疑问时,你总能第一时间回复,并以亲切温暖的风格与用户交流。",
skills=[ skills="亲切温暖的沟通\n轻松友好的语气\n简洁明了的描述\n亲昵且口语化的用词\n乱码话术重复问询",
"亲切温暖的沟通", restrictions="回复必须基于提供的知识库中的内容\n每条回复应保持在80字以内,使用口语化语言\n回复中可以适当加入表情符号,但不要过度使用\n只讨论与宝妈相关的主题,如育儿、选品、团购等\n始终以用户的需求和满意度为优先",
"轻松友好的语气",
"简洁明了的描述",
"亲昵且口语化的用词",
"乱码话术重复问询",
],
restrictions=[
"回复必须基于提供的知识库中的内容",
"每条回复应保持在80字以内,使用口语化语言",
"回复中可以适当加入表情符号,但不要过度使用",
"只讨论与宝妈相关的主题,如育儿、选品、团购等",
"始终以用户的需求和满意度为优先",
],
) )
# 全局变量存储角色配置和对话历史 # 全局变量存储对话历史(暂时保持内存存储,根据用户要求不持久化)
current_role_config = DEFAULT_ROLE_CONFIG
conversations = {} conversations = {}
def build_system_prompt(role_config: RoleConfig) -> str: def build_system_prompt(role_config: dict) -> str:
"""构建系统提示词""" """构建系统提示词
Args:
role_config: 角色配置字典,包含name, description, skills, restrictions字段
Returns:
系统提示词字符串
"""
# 解析技能字符串为列表
skills_list = [
skill.strip() for skill in role_config["skills"].split("\n") if skill.strip()
]
restrictions_list = [
restriction.strip()
for restriction in role_config["restrictions"].split("\n")
if restriction.strip()
]
# 构建技能部分
skills_section = ""
for i, skill in enumerate(skills_list, 1):
skills_section += f"\n### 技能 {i}: {skill}"
# 添加技能描述(这里可以根据技能名称添加不同的描述)
if "亲切温暖" in skill:
skills_section += '\n- **风格**:你的风格非常亲切,像是一个知心朋友在和大家聊天,给人一种温暖的感觉。你经常使用"宝"、"美妈"等亲昵的称呼,拉近了与用户的距离。'
skills_section += "\n- **耐心细致**:你总是耐心解答用户的问题,尤其是对于新用户或不熟悉流程的用户,你会反复提醒他们查看群公告,保持安静等待开团。"
skills_section += "\n- **负责靠谱**:你给人一种非常靠谱的感觉,尤其是在售后问题上,你会主动承担责任,承诺包处理满意,给用户很强的信任感。"
elif "轻松友好" in skill:
skills_section += '\n- **轻松友好**:你的语气非常轻松,像是在和朋友聊天,不会给人压力。比如"不着急哦"、"爱你!"等表达方式,让人感到轻松愉快。'
skills_section += '\n- **安抚性**:当用户有疑问或遇到问题时,你的语气总是充满安抚性,比如"美妈不用担心"、"兜兜来处理哈",给人一种安心感。'
skills_section += '\n- **鼓励性**:你经常用"不着急"、"等开团的时候会有统一说明"等话语,鼓励用户耐心等待,保持积极的心态。'
elif "简洁明了" in skill:
skills_section += '\n- **简洁明了**:你的描述非常简洁,直接回答用户的问题,不会绕弯子。比如"贝亲是咱们深度合作品牌,经常上团的",直接给出了明确的答案。'
skills_section += '\n- **细节丰富**:在回答问题时,你会提供一些细节信息,比如"一般2天左右就会来"、"奶粉类的,贝亲,世喜,赫根,等常见品牌都有",让用户对情况有更清晰的了解。'
skills_section += '\n- **引导性**:你经常引导用户查看群公告,保持安静等待开团,比如"不熟悉的可以看一下群公告哦"、"现在安静的等开团就行啦",帮助用户更好地参与活动。'
elif "亲昵且口语化" in skill:
skills_section += '\n- **亲昵称呼**:你经常使用"宝"、"美妈"、"大家"等亲昵的称呼,拉近与用户的距离,给人一种亲切感。'
skills_section += '\n- **口语化**:你的用词非常口语化,像是日常聊天,比如"不着急哦"、"爱你!"、"兜兜来处理哈",给人一种轻松自然的感觉,称呼自己为"兜兜"或者"我"。'
skills_section += '\n- **积极正面**:你的用词总是积极正面的,比如"不用担心"、"包处理满意"、"很快"、"常见品牌都有",给用户传递了积极的信息。'
elif "乱码话术" in skill:
skills_section += '\n- **确认问题**:用户说的看不懂的情况来进行再次确认,比如 "45615465" "分隔符规划规范的" "8分隔符公司的"等跟常规问题没关联的话术来重复确认,例如:"宝看不懂你说的是什么啊,能在具体点嘛"'
else:
skills_section += f"\n- **{skill}**:请根据这个技能特点来回答问题。"
# 构建限制部分
restrictions_section = "\n".join(
[f"- {restriction}" for restriction in restrictions_list]
)
prompt = f"""# 角色 prompt = f"""# 角色
{role_config.description} {role_config["description"]}
## 技能 ## 技能{skills_section}
### 技能 1: 亲切温暖的沟通
- **风格**:你的风格非常亲切,像是一个知心朋友在和大家聊天,给人一种温暖的感觉。你经常使用"宝"、"美妈"等亲昵的称呼,拉近了与用户的距离。
- **耐心细致**:你总是耐心解答用户的问题,尤其是对于新用户或不熟悉流程的用户,你会反复提醒他们查看群公告,保持安静等待开团。
- **负责靠谱**:你给人一种非常靠谱的感觉,尤其是在售后问题上,你会主动承担责任,承诺包处理满意,给用户很强的信任感。
### 技能 2: 轻松友好的语气
- **轻松友好**:你的语气非常轻松,像是在和朋友聊天,不会给人压力。比如"不着急哦"、"爱你!"等表达方式,让人感到轻松愉快。
- **安抚性**:当用户有疑问或遇到问题时,你的语气总是充满安抚性,比如"美妈不用担心"、"兜兜来处理哈",给人一种安心感。
- **鼓励性**:你经常用"不着急"、"等开团的时候会有统一说明"等话语,鼓励用户耐心等待,保持积极的心态。
### 技能 3: 简洁明了的描述
- **简洁明了**:你的描述非常简洁,直接回答用户的问题,不会绕弯子。比如"贝亲是咱们深度合作品牌,经常上团的",直接给出了明确的答案。
- **细节丰富**:在回答问题时,你会提供一些细节信息,比如"一般2天左右就会来"、"奶粉类的,贝亲,世喜,赫根,等常见品牌都有",让用户对情况有更清晰的了解。
- **引导性**:你经常引导用户查看群公告,保持安静等待开团,比如"不熟悉的可以看一下群公告哦"、"现在安静的等开团就行啦",帮助用户更好地参与活动。
### 技能 4: 亲昵且口语化的用词
- **亲昵称呼**:你经常使用"宝"、"美妈"、"大家"等亲昵的称呼,拉近与用户的距离,给人一种亲切感。
- **口语化**:你的用词非常口语化,像是日常聊天,比如"不着急哦"、"爱你!"、"兜兜来处理哈",给人一种轻松自然的感觉,称呼自己为"兜兜"或者"我"。
- **积极正面**:你的用词总是积极正面的,比如"不用担心"、"包处理满意"、"很快"、"常见品牌都有",给用户传递了积极的信息。
### 技能 5: 乱码话术重复问询
- **确认问题**:用户说的看不懂的情况来进行再次确认,比如 "45615465" "分隔符规划规范的" "8分隔符公司的"等跟常规问题没关联的话术来重复确认,例如:"宝看不懂你说的是什么啊,能在具体点嘛"
## 限制 ## 限制
- 回复必须基于提供的知识库中的内容。 {restrictions_section}
- 每条回复应保持在80字以内,使用口语化语言。
- 回复中可以适当加入表情符号,但不要过度使用。
- 只讨论与宝妈相关的主题,如育儿、选品、团购等。
- 始终以用户的需求和满意度为优先。
- 在回答问题时,确保信息准确无误,避免误导用户。
- 保持沟通的亲切性和温暖感,但不要过度使用亲昵称呼,以免显得过于随意。
请按照以上角色设定和限制来回答用户的问题。""" 请按照以上角色设定和限制来回答用户的问题。"""
return prompt return prompt
@@ -145,22 +157,42 @@ async def root():
return {"message": "DeepSeek AI Chat Assistant API"} return {"message": "DeepSeek AI Chat Assistant API"}
@router.get("/role") @router.get("/role", response_model=BaseOut)
async def get_role_config(): async def get_role_config(service: GLMService = Depends(get_glm_service)):
"""获取当前角色配置""" """获取当前角色配置"""
return current_role_config try:
role = await service.get_current_role()
return BaseOut(code=200, message="ok", result=role.to_role_config_dict())
except Exception as e:
logger.error(f"获取角色配置失败: {e}")
return BaseOut(code=500, message=f"获取角色配置失败: {str(e)}", result=None)
@router.put("/role") @router.put("/role", response_model=BaseOut)
async def update_role_config(role_config: RoleConfig): async def update_role_config(
role_config: RoleConfig, service: GLMService = Depends(get_glm_service)
):
"""更新角色配置""" """更新角色配置"""
global current_role_config try:
current_role_config = role_config # 将Pydantic模型转换为字典
return {"message": "角色配置已更新", "role_config": current_role_config} role_data = role_config.dict()
updated_role = await service.update_role(role_data)
return BaseOut(
code=200,
message="角色配置已更新",
result={
"message": "角色配置已更新",
"role_config": updated_role.to_role_config_dict(),
},
)
except Exception as e:
logger.error(f"更新角色配置失败: {e}")
return BaseOut(code=500, message=f"更新角色配置失败: {str(e)}", result=None)
@router.post("/chat", response_model=ChatResponse) @router.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest): async def chat(request: ChatRequest, service: GLMService = Depends(get_glm_service)):
"""处理聊天请求""" """处理聊天请求"""
# 创建或获取对话历史 # 创建或获取对话历史
if request.conversation_id and request.conversation_id in conversations: if request.conversation_id and request.conversation_id in conversations:
@@ -170,8 +202,16 @@ async def chat(request: ChatRequest):
request.conversation_id = f"conv_{len(conversations) + 1}" request.conversation_id = f"conv_{len(conversations) + 1}"
conversations[request.conversation_id] = conversation_history conversations[request.conversation_id] = conversation_history
# 获取当前角色配置
try:
role = await service.get_current_role()
system_prompt = build_system_prompt(role.to_role_config_dict())
except Exception as e:
logger.error(f"获取角色配置失败: {e}")
raise HTTPException(status_code=500, detail=f"获取角色配置失败: {str(e)}")
# 构建消息列表 # 构建消息列表
messages = [{"role": "system", "content": build_system_prompt(current_role_config)}] messages = [{"role": "system", "content": system_prompt}]
# 添加历史消息 # 添加历史消息
for msg in conversation_history[-10:]: # 保留最近10条历史 for msg in conversation_history[-10:]: # 保留最近10条历史
@@ -204,3 +244,24 @@ async def get_conversation(conversation_id: str):
"conversation_id": conversation_id, "conversation_id": conversation_id,
"messages": conversations[conversation_id], "messages": conversations[conversation_id],
} }
@router.post("/role/reset", response_model=BaseOut)
async def reset_role_config(service: GLMService = Depends(get_glm_service)):
"""重置角色配置为默认值"""
try:
# 使用 DEFAULT_ROLE_CONFIG 更新角色
role_data = DEFAULT_ROLE_CONFIG.dict()
updated_role = await service.update_role(role_data)
return BaseOut(
code=200,
message="角色配置已重置为默认值",
result={
"message": "角色配置已重置为默认值",
"role_config": updated_role.to_role_config_dict(),
},
)
except Exception as e:
logger.error(f"重置角色配置失败: {e}")
return BaseOut(code=500, message=f"重置角色配置失败: {str(e)}", result=None)

View File

@@ -0,0 +1,125 @@
"""
GLM聊天服务,包含角色配置管理
"""
import logging
from typing import Optional
from app.models.glm_role import GLMRole
logger = logging.getLogger(__name__)
class GLMService:
"""GLM聊天服务,包含角色配置管理"""
def __init__(self):
self._current_role: Optional[GLMRole] = None
async def get_current_role(self) -> GLMRole:
"""获取当前激活的角色配置"""
if self._current_role is None:
self._current_role = await self._load_or_create_role()
return self._current_role
async def update_role(self, role_data: dict) -> GLMRole:
"""更新角色配置
Args:
role_data: 角色配置数据,包含name, description, skills, restrictions字段
Returns:
更新后的GLMRole实例
"""
role = await self.get_current_role()
# 更新字段
if "name" in role_data:
role.name = role_data["name"]
if "description" in role_data:
role.description = role_data["description"]
if "skills" in role_data:
role.skills = role_data["skills"]
if "restrictions" in role_data:
role.restrictions = role_data["restrictions"]
role.update_timestamp()
await role.save()
# 更新缓存
self._current_role = role
logger.info(f"角色配置已更新: {role.name}")
return role
async def _load_or_create_role(self) -> GLMRole:
"""加载或创建默认角色"""
# 1. 尝试获取激活的角色
active_role = await GLMRole.find_one(GLMRole.is_active == True)
if active_role:
logger.info(f"加载激活角色: {active_role.name}")
return active_role
# 2. 尝试获取默认角色
default_role = await GLMRole.find_one(GLMRole.is_default == True)
if default_role:
default_role.is_active = True
await default_role.save()
logger.info(f"加载默认角色并激活: {default_role.name}")
return default_role
# 3. 创建默认角色
logger.info("创建默认角色配置")
return await self._create_default_role()
async def _create_default_role(self) -> GLMRole:
"""创建默认角色配置"""
# 导入DEFAULT_ROLE_CONFIG
from app.routes.glm_routes import DEFAULT_ROLE_CONFIG
role = GLMRole(
name=DEFAULT_ROLE_CONFIG.name,
description=DEFAULT_ROLE_CONFIG.description,
skills=DEFAULT_ROLE_CONFIG.skills,
restrictions=DEFAULT_ROLE_CONFIG.restrictions,
is_active=True,
is_default=True,
)
await role.save()
logger.info(f"默认角色创建成功: {role.name}")
return role
async def ensure_default_role(self):
"""确保存在默认角色配置(应用启动时调用)"""
try:
role = await self._load_or_create_role()
logger.info(f"默认角色已确保存在: {role.name}")
return role
except Exception as e:
logger.error(f"确保默认角色存在时出错: {e}")
raise
async def close(self):
"""关闭服务,清理资源"""
self._current_role = None
logger.info("GLM服务已关闭")
# 全局服务实例(单例模式)
_glm_service: Optional[GLMService] = None
def get_glm_service() -> GLMService:
"""获取GLM服务实例(单例)"""
global _glm_service
if _glm_service is None:
_glm_service = GLMService()
return _glm_service
def close_glm_service():
"""关闭GLM服务"""
global _glm_service
if _glm_service:
_glm_service.close()
_glm_service = None

View File

@@ -1,12 +1,12 @@
{ {
"access_token": "TfPE_kJUsbokpSRqeeDNW5-S-lREfLNrpITU42JkTaHhjDqasI6dHAkZ4_63A1-qTmZb9W1KzqZZ9d11QFQMV1hhBK7M_33Qt5kU8rHyyXxj_2raPgEG2UIexe3txVNhjLY872z8R0HXB_pwiacTgJuYJtJvgPRum0G9taHftuoh6Pwcy_tiewe5jlhwdjy5SDOOMUO8LzadhRAHrx0xGQ", "access_token": "cB51r2D8h73vFEY2WOUOpT7oKOAsTrYzh5kb_xcXTbLsw69U-Wxy9KnM_ealJEef_XdrPyqXcc82IYiJFIFVnwEXy65wF3xQmtm3JxvGq293eSDnNvuxWWRFjoQSf7ani47d_GMAELYoaLcM4S7kFPJxoq7kZz0g2dizOQ0vKiApNgQs3B_nxqm8cAuCgZuSw0JJDPAYZyci4lnRCVe0ww",
"fetch_time": 1765394181.8370981, "fetch_time": 1765476407.16114,
"expires_in": 7200, "expires_in": 7200,
"expiry_time": 1765400181.8370981, "expiry_time": 1765482407.16114,
"ticket_data": { "ticket_data": {
"ticket": "EVSxsqHHAbWPlLvl91IqbA==", "ticket": "fkOdR8Y0baUUdG33FhsIfg==",
"fetch_time": 1765394182.1750567, "fetch_time": 1765476407.4518044,
"expires_in": 7200, "expires_in": 7200,
"expiry_time": 1765401382.1750567 "expiry_time": 1765483607.4518046
} }
} }

View File

@@ -0,0 +1,32 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2022,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

13
backstage/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理系统 - 会话存档管理</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

32
backstage/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "backstage",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint ."
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"axios": "^1.13.2",
"daisyui": "^5.5.5",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.3.0",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"vite": "^7.2.4"
}
}

2380
backstage/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

26
backstage/src/App.jsx Normal file
View File

@@ -0,0 +1,26 @@
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import Layout from './components/layout/Layout'
import Dashboard from './pages/Dashboard'
import Messages from './pages/Messages'
import Settings from './pages/Settings'
import Login from './pages/Login'
function App() {
// 简单的认证检查(实际项目中应该使用更完善的认证方案)
const isAuthenticated = true // 暂时设为 true 以便测试
return (
<Router basename="/chat_bm">
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={isAuthenticated ? <Layout /> : <Navigate to="/login" />}>
<Route index element={<Dashboard />} />
<Route path="messages" element={<Messages />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</Router>
)
}
export default App

View File

@@ -0,0 +1,115 @@
import { Outlet, Link, useLocation } from 'react-router-dom'
import { useState } from 'react'
const Layout = () => {
const location = useLocation()
const [sidebarOpen, setSidebarOpen] = useState(true)
const navigation = [
{ name: '仪表板', href: '/', icon: '🏠', current: location.pathname === '/' },
{ name: '消息管理', href: '/messages', icon: '💬', current: location.pathname === '/messages' },
{ name: '系统设置', href: '/settings', icon: '⚙️', current: location.pathname === '/settings' },
]
return (
<div className="min-h-screen bg-base-100">
{/* 侧边栏 */}
<div className={`fixed inset-y-0 left-0 z-50 w-64 bg-base-200 shadow-xl transition-transform duration-300 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}`}>
<div className="flex h-16 items-center justify-between border-b border-base-300 px-4">
<div className="flex items-center">
<div className="text-2xl font-bold text-primary">后台管理</div>
</div>
<button
onClick={() => setSidebarOpen(false)}
className="btn btn-ghost btn-sm"
>
✕
</button>
</div>
<nav className="flex-1 space-y-1 p-4">
{navigation.map((item) => (
<Link
key={item.name}
to={item.href}
className={`flex items-center rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
item.current
? 'bg-primary text-primary-content'
: 'text-base-content hover:bg-base-300'
}`}
>
<span className="mr-3 text-lg">{item.icon}</span>
{item.name}
</Link>
))}
</nav>
<div className="border-t border-base-300 p-4">
<div className="flex items-center">
<div className="avatar placeholder">
<div className="bg-neutral text-neutral-content w-8 rounded-full">
<span className="text-xs">AD</span>
</div>
</div>
<div className="ml-3">
<p className="text-sm font-medium">管理员</p>
<p className="text-xs text-base-content/70">admin@example.com</p>
</div>
</div>
</div>
</div>
{/* 主内容区 */}
<div className={`transition-all duration-300 ${sidebarOpen ? 'ml-64' : 'ml-0'}`}>
{/* 顶部导航栏 */}
<header className="sticky top-0 z-40 flex h-16 items-center justify-between border-b border-base-300 bg-base-100 px-6 shadow-sm">
<div className="flex items-center">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="btn btn-ghost btn-sm mr-4"
>
{sidebarOpen ? '◀' : '▶'}
</button>
<h1 className="text-lg font-semibold">
{navigation.find(item => item.current)?.name || '后台管理系统'}
</h1>
</div>
<div className="flex items-center space-x-4">
<button className="btn btn-ghost btn-sm">
🔔
<span className="badge badge-xs badge-primary absolute -top-1 -right-1">3</span>
</button>
<div className="dropdown dropdown-end">
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm">
<div className="avatar placeholder">
<div className="bg-neutral text-neutral-content w-8 rounded-full">
<span className="text-xs">AD</span>
</div>
</div>
</div>
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow">
<li><a>个人资料</a></li>
<li><a>修改密码</a></li>
<li className="divider my-1"></li>
<li><a className="text-error">退出登录</a></li>
</ul>
</div>
</div>
</header>
{/* 页面内容 */}
<main className="p-6">
<Outlet />
</main>
{/* 页脚 */}
<footer className="border-t border-base-300 p-4 text-center text-sm text-base-content/70">
<p>© 2025 后台管理系统 - 会话存档管理 | 版本 1.0.0</p>
</footer>
</div>
</div>
)
}
export default Layout

View File

@@ -0,0 +1,35 @@
import { useState } from 'react'
const DocumentationViewer = () => {
const [library, setLibrary] = useState('react')
const [mode, setMode] = useState('code')
const [topic, setTopic] = useState('')
const [page, setPage] = useState(1)
const [loading, setLoading] = useState(false)
const [docs, setDocs] = useState(null)
const [error, setError] = useState('')
const libraries = [
{ id: 'react', name: 'React', description: '用于构建用户界面的 JavaScript 库' },
{ id: 'tailwindcss', name: 'Tailwind CSS', description: '实用优先的 CSS 框架' },
{ id: 'daisyui', name: 'DaisyUI', description: 'Tailwind CSS 组件库' },
{ id: 'vite', name: 'Vite', description: '下一代前端工具' },
{ id: 'axios', name: 'Axios', description: '基于 Promise 的 HTTP 客户端' },
]
const modes = [
{ id: 'code', name: '代码文档', description: 'API 参考和代码示例' },
{ id: 'info', name: '概念指南', description: '概念性指南和架构信息' },
]
const handleFetchDocs = async () => {
setLoading(true)
setError('')
setDocs(null)
try {
// 这里应该调用 Context7 MCP 工具
// 由于我们无法直接在前端调用 MCP 工具,这里模拟 API 调用
// 实际项目中应该通过后端代理调用 MCP 工具
// 模拟延迟

39
backstage/src/index.css Normal file
View File

@@ -0,0 +1,39 @@
@import "tailwindcss";
/* DaisyUI 主题配置 */
@plugin "daisyui" {
themes: light --default, dark --prefersdark;
}
/* 自定义样式 */
:root {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}

10
backstage/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@@ -0,0 +1,167 @@
const Dashboard = () => {
const stats = [
{ title: '总消息数', value: '1,234', change: '+12%', icon: '💬', color: 'bg-primary' },
{ title: '今日新增', value: '56', change: '+5%', icon: '📈', color: 'bg-success' },
{ title: '用户数', value: '89', change: '+8%', icon: '👥', color: 'bg-info' },
{ title: '加载成功率', value: '98%', change: '+2%', icon: '✅', color: 'bg-warning' },
]
const recentActivities = [
{ id: 1, user: '张三', action: '加载了会话存档', time: '10分钟前', type: 'success' },
{ id: 2, user: '李四', action: '查看了消息详情', time: '30分钟前', type: 'info' },
{ id: 3, user: '王五', action: '更新了系统设置', time: '1小时前', type: 'warning' },
{ id: 4, user: '赵六', action: '导出了消息数据', time: '2小时前', type: 'success' },
]
return (
<div className="space-y-6">
{/* 欢迎标题 */}
<div>
<h1 className="text-3xl font-bold">欢迎回来,管理员</h1>
<p className="text-base-content/70 mt-2">这里是会话存档管理系统的控制面板</p>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
{stats.map((stat, index) => (
<div key={index} className="card bg-base-100 shadow">
<div className="card-body">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-base-content/70">{stat.title}</p>
<p className="text-2xl font-bold mt-1">{stat.value}</p>
<p className="text-xs mt-1">
<span className="text-success">{stat.change}</span> 较昨日
</p>
</div>
<div className={`${stat.color} rounded-lg p-3`}>
<span className="text-2xl">{stat.icon}</span>
</div>
</div>
</div>
</div>
))}
</div>
{/* 快速操作 */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* 最近活动 */}
<div className="card bg-base-100 shadow">
<div className="card-body">
<h2 className="card-title">最近活动</h2>
<div className="space-y-4">
{recentActivities.map((activity) => (
<div key={activity.id} className="flex items-center justify-between border-b border-base-300 pb-3 last:border-0">
<div className="flex items-center">
<div className={`avatar placeholder ${activity.type === 'success' ? 'bg-success' : activity.type === 'warning' ? 'bg-warning' : 'bg-info'}`}>
<div className="text-white w-8 rounded-full">
<span className="text-xs">{activity.user.charAt(0)}</span>
</div>
</div>
<div className="ml-3">
<p className="font-medium">{activity.user}</p>
<p className="text-sm text-base-content/70">{activity.action}</p>
</div>
</div>
<span className="text-sm text-base-content/70">{activity.time}</span>
</div>
))}
</div>
</div>
</div>
{/* 快速操作卡片 */}
<div className="card bg-base-100 shadow">
<div className="card-body">
<h2 className="card-title">快速操作</h2>
<div className="grid grid-cols-2 gap-4">
<button className="btn btn-primary">
<span className="mr-2">📥</span>
加载存档
</button>
<button className="btn btn-outline">
<span className="mr-2">📊</span>
查看统计
</button>
<button className="btn btn-outline">
<span className="mr-2">🔍</span>
搜索消息
</button>
<button className="btn btn-outline">
<span className="mr-2">⚙️</span>
系统设置
</button>
</div>
<div className="divider my-4"></div>
<div>
<h3 className="font-medium mb-2">系统状态</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span>后端服务</span>
<span className="badge badge-success">运行中</span>
</div>
<div className="flex justify-between">
<span>数据库连接</span>
<span className="badge badge-success">正常</span>
</div>
<div className="flex justify-between">
<span>API 可用性</span>
<span className="badge badge-success">100%</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 消息类型分布 */}
<div className="card bg-base-100 shadow">
<div className="card-body">
<h2 className="card-title">消息类型分布</h2>
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>消息类型</th>
<th>数量</th>
<th>占比</th>
<th>趋势</th>
</tr>
</thead>
<tbody>
<tr>
<td>文本消息</td>
<td>856</td>
<td>69%</td>
<td className="text-success">↗ 增长</td>
</tr>
<tr>
<td>图片消息</td>
<td>234</td>
<td>19%</td>
<td className="text-warning">→ 稳定</td>
</tr>
<tr>
<td>文件消息</td>
<td>98</td>
<td>8%</td>
<td className="text-success">↗ 增长</td>
</tr>
<tr>
<td>语音消息</td>
<td>46</td>
<td>4%</td>
<td className="text-error">↘ 下降</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
)
}
export default Dashboard

View File

@@ -0,0 +1,143 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
const Login = () => {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const navigate = useNavigate()
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
if (!username || !password) {
setError('请输入用户名和密码')
return
}
setLoading(true)
// 模拟登录 API 调用
setTimeout(() => {
setLoading(false)
if (username === 'admin' && password === 'admin123') {
// 登录成功,跳转到首页
navigate('/')
} else {
setError('用户名或密码错误')
}
}, 1500)
}
return (
<div className="min-h-screen bg-gradient-to-br from-primary/10 to-secondary/10 flex items-center justify-center p-4">
<div className="card bg-base-100 shadow-2xl w-full max-w-md">
<div className="card-body">
{/* Logo 和标题 */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary text-primary-content text-2xl font-bold mb-4">
BS
</div>
<h1 className="text-3xl font-bold">后台管理系统</h1>
<p className="text-base-content/70 mt-2">会话存档管理平台</p>
</div>
{/* 登录表单 */}
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="alert alert-error">
<span>{error}</span>
</div>
)}
<div className="form-control">
<label className="label">
<span className="label-text">用户名</span>
</label>
<input
type="text"
className="input input-bordered"
placeholder="请输入用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={loading}
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">密码</span>
</label>
<input
type="password"
className="input input-bordered"
placeholder="请输入密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
/>
<label className="label">
<a href="#" className="label-text-alt link link-hover">忘记密码?</a>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer justify-start gap-3">
<input type="checkbox" className="checkbox checkbox-primary" defaultChecked />
<span className="label-text">记住我</span>
</label>
</div>
<div className="form-control mt-6">
<button
type="submit"
className="btn btn-primary"
disabled={loading}
>
{loading ? (
<>
<span className="loading loading-spinner loading-sm"></span>
登录中...
</>
) : (
'登录'
)}
</button>
</div>
</form>
{/* 分隔线 */}
<div className="divider my-6">或</div>
{/* 其他登录方式 */}
<div className="space-y-3">
<button className="btn btn-outline btn-block">
<span className="mr-2">👤</span>
使用访客账户登录
</button>
<button className="btn btn-outline btn-block">
<span className="mr-2">🔑</span>
使用 API Token 登录
</button>
</div>
{/* 底部信息 */}
<div className="mt-8 text-center text-sm text-base-content/70">
<p>默认账户:admin / admin123</p>
<p className="mt-2">© 2025 后台管理系统 | 版本 1.0.0</p>
</div>
</div>
</div>
{/* 背景装饰 */}
<div className="absolute inset-0 -z-10 overflow-hidden">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-primary/20 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-secondary/20 rounded-full blur-3xl"></div>
</div>
</div>
)
}
export default Login

View File

@@ -0,0 +1,265 @@
import { useState } from 'react'
const Messages = () => {
const [loading, setLoading] = useState(false)
const [messages, setMessages] = useState([
{ id: 1, from: '张三', content: '你好,我想了解一下产品详情', time: '2025-12-12 10:30', type: 'text', room: '单聊' },
{ id: 2, from: '李四', content: '这是会议纪要文件', time: '2025-12-12 09:15', type: 'file', room: '群聊-项目组' },
{ id: 3, from: '王五', content: '产品图片已发送', time: '2025-12-11 16:45', type: 'image', room: '单聊' },
{ id: 4, from: '赵六', content: '关于下周会议安排', time: '2025-12-11 14:20', type: 'text', room: '群聊-管理组' },
{ id: 5, from: '钱七', content: '语音消息', time: '2025-12-10 11:10', type: 'voice', room: '单聊' },
])
const handleLoadArchive = async () => {
setLoading(true)
// 模拟 API 调用
setTimeout(() => {
setLoading(false)
alert('会话存档加载成功!')
}, 1500)
}
const handleSearch = (e) => {
e.preventDefault()
// 搜索逻辑
alert('搜索功能开发中...')
}
return (
<div className="space-y-6">
{/* 页面标题和操作 */}
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
<div>
<h1 className="text-3xl font-bold">消息管理</h1>
<p className="text-base-content/70 mt-2">管理企业微信会话存档消息</p>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={handleLoadArchive}
disabled={loading}
className="btn btn-primary"
>
{loading ? (
<>
<span className="loading loading-spinner loading-sm"></span>
加载中...
</>
) : (
<>
<span className="mr-2">📥</span>
加载会话存档
</>
)}
</button>
<button className="btn btn-outline">
<span className="mr-2">📊</span>
查看统计
</button>
<button className="btn btn-outline">
<span className="mr-2">📤</span>
导出数据
</button>
</div>
</div>
{/* 搜索和过滤 */}
<div className="card bg-base-100 shadow">
<div className="card-body">
<form onSubmit={handleSearch} className="flex flex-col gap-4 md:flex-row">
<div className="flex-1">
<input
type="text"
placeholder="搜索消息内容、发送者..."
className="input input-bordered w-full"
/>
</div>
<div className="flex gap-2">
<select className="select select-bordered">
<option value="">所有消息类型</option>
<option value="text">文本消息</option>
<option value="image">图片消息</option>
<option value="file">文件消息</option>
<option value="voice">语音消息</option>
</select>
<select className="select select-bordered">
<option value="">所有聊天类型</option>
<option value="single">单聊</option>
<option value="group">群聊</option>
</select>
<button type="submit" className="btn btn-primary">
<span className="mr-2">🔍</span>
搜索
</button>
</div>
</form>
</div>
</div>
{/* 消息列表 */}
<div className="card bg-base-100 shadow">
<div className="card-body">
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>
<label>
<input type="checkbox" className="checkbox" />
</label>
</th>
<th>发送者</th>
<th>消息内容</th>
<th>消息类型</th>
<th>聊天类型</th>
<th>时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{messages.map((message) => (
<tr key={message.id}>
<td>
<label>
<input type="checkbox" className="checkbox" />
</label>
</td>
<td>
<div className="flex items-center gap-3">
<div className="avatar placeholder">
<div className="bg-neutral text-neutral-content w-8 rounded-full">
<span className="text-xs">{message.from.charAt(0)}</span>
</div>
</div>
<div>
<div className="font-bold">{message.from}</div>
</div>
</div>
</td>
<td>
<div className="max-w-xs truncate" title={message.content}>
{message.content}
</div>
</td>
<td>
<span className={`badge ${message.type === 'text' ? 'badge-info' : message.type === 'image' ? 'badge-success' : message.type === 'file' ? 'badge-warning' : 'badge-error'}`}>
{message.type === 'text' ? '文本' : message.type === 'image' ? '图片' : message.type === 'file' ? '文件' : '语音'}
</span>
</td>
<td>
<span className={`badge ${message.room.includes('群聊') ? 'badge-primary' : 'badge-secondary'}`}>
{message.room}
</span>
</td>
<td>{message.time}</td>
<td>
<div className="flex gap-2">
<button className="btn btn-ghost btn-xs">
<span className="mr-1">👁️</span>
查看
</button>
<button className="btn btn-ghost btn-xs">
<span className="mr-1">📋</span>
复制
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 分页 */}
<div className="flex items-center justify-between pt-4">
<div className="text-sm text-base-content/70">
显示 1-5 条,共 1234 条消息
</div>
<div className="join">
<button className="join-item btn btn-sm">«</button>
<button className="join-item btn btn-sm btn-active">1</button>
<button className="join-item btn btn-sm">2</button>
<button className="join-item btn btn-sm">3</button>
<button className="join-item btn btn-sm">4</button>
<button className="join-item btn btn-sm">»</button>
</div>
</div>
</div>
</div>
{/* 统计信息 */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
<div className="card bg-base-100 shadow">
<div className="card-body">
<h3 className="card-title">今日消息统计</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span>文本消息</span>
<span className="font-bold">45 条</span>
</div>
<div className="flex justify-between">
<span>图片消息</span>
<span className="font-bold">12 条</span>
</div>
<div className="flex justify-between">
<span>文件消息</span>
<span className="font-bold">8 条</span>
</div>
<div className="flex justify-between">
<span>语音消息</span>
<span className="font-bold">3 条</span>
</div>
</div>
</div>
</div>
<div className="card bg-base-100 shadow">
<div className="card-body">
<h3 className="card-title">加载状态</h3>
<div className="space-y-4">
<div>
<div className="flex justify-between mb-1">
<span>上次加载时间</span>
<span className="font-bold">2025-12-12 10:30</span>
</div>
<progress className="progress progress-primary w-full" value="100" max="100"></progress>
</div>
<div>
<div className="flex justify-between mb-1">
<span>加载成功率</span>
<span className="font-bold">98.5%</span>
</div>
<progress className="progress progress-success w-full" value="98.5" max="100"></progress>
</div>
</div>
</div>
</div>
<div className="card bg-base-100 shadow">
<div className="card-body">
<h3 className="card-title">快速操作</h3>
<div className="space-y-2">
<button className="btn btn-outline btn-block justify-start">
<span className="mr-2">🔄</span>
刷新消息列表
</button>
<button className="btn btn-outline btn-block justify-start">
<span className="mr-2">🧹</span>
清理旧数据
</button>
<button className="btn btn-outline btn-block justify-start">
<span className="mr-2">📋</span>
批量操作
</button>
<button className="btn btn-outline btn-block justify-start">
<span className="mr-2">⚙️</span>
消息设置
</button>
</div>
</div>
</div>
</div>
</div>
)
}
export default Messages

View File

@@ -0,0 +1,270 @@
import { useState } from 'react'
const Settings = () => {
const [apiConfig, setApiConfig] = useState({
baseUrl: 'http://localhost:8000/api',
timeout: 10000,
retryCount: 3,
})
const [systemSettings, setSystemSettings] = useState({
autoLoad: true,
loadInterval: 30,
keepHistory: 90,
theme: 'light',
})
const handleSaveApiConfig = () => {
alert('API 配置已保存!')
}
const handleSaveSystemSettings = () => {
alert('系统设置已保存!')
}
return (
<div className="space-y-6">
{/* 页面标题 */}
<div>
<h1 className="text-3xl font-bold">系统设置</h1>
<p className="text-base-content/70 mt-2">配置系统参数和 API 设置</p>
</div>
{/* API 配置 */}
<div className="card bg-base-100 shadow">
<div className="card-body">
<h2 className="card-title">API 配置</h2>
<p className="text-base-content/70">配置后端 API 连接参数</p>
<div className="divider my-4"></div>
<div className="space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text">API 基础地址</span>
</label>
<input
type="text"
className="input input-bordered"
value={apiConfig.baseUrl}
onChange={(e) => setApiConfig({...apiConfig, baseUrl: e.target.value})}
placeholder="http://localhost:8000/api"
/>
<label className="label">
<span className="label-text-alt">后端 API 服务的基础地址</span>
</label>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="form-control">
<label className="label">
<span className="label-text">请求超时时间(毫秒)</span>
</label>
<input
type="number"
className="input input-bordered"
value={apiConfig.timeout}
onChange={(e) => setApiConfig({...apiConfig, timeout: parseInt(e.target.value)})}
min="1000"
max="30000"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">重试次数</span>
</label>
<input
type="number"
className="input input-bordered"
value={apiConfig.retryCount}
onChange={(e) => setApiConfig({...apiConfig, retryCount: parseInt(e.target.value)})}
min="0"
max="10"
/>
</div>
</div>
<div className="form-control">
<label className="label cursor-pointer justify-start gap-3">
<input type="checkbox" className="toggle toggle-primary" defaultChecked />
<span className="label-text">启用请求日志</span>
</label>
<label className="label cursor-pointer justify-start gap-3">
<input type="checkbox" className="toggle toggle-primary" defaultChecked />
<span className="label-text">启用错误重试</span>
</label>
</div>
<div className="flex justify-end">
<button onClick={handleSaveApiConfig} className="btn btn-primary">
<span className="mr-2">💾</span>
保存 API 配置
</button>
</div>
</div>
</div>
</div>
{/* 系统设置 */}
<div className="card bg-base-100 shadow">
<div className="card-body">
<h2 className="card-title">系统设置</h2>
<p className="text-base-content/70">配置系统行为和外观</p>
<div className="divider my-4"></div>
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="form-control">
<label className="label">
<span className="label-text">自动加载间隔(分钟)</span>
</label>
<input
type="number"
className="input input-bordered"
value={systemSettings.loadInterval}
onChange={(e) => setSystemSettings({...systemSettings, loadInterval: parseInt(e.target.value)})}
min="1"
max="1440"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">历史数据保留天数</span>
</label>
<input
type="number"
className="input input-bordered"
value={systemSettings.keepHistory}
onChange={(e) => setSystemSettings({...systemSettings, keepHistory: parseInt(e.target.value)})}
min="1"
max="365"
/>
</div>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">主题设置</span>
</label>
<div className="flex gap-4">
<label className="flex cursor-pointer items-center gap-2">
<input
type="radio"
name="theme"
className="radio radio-primary"
checked={systemSettings.theme === 'light'}
onChange={() => setSystemSettings({...systemSettings, theme: 'light'})}
/>
<span>浅色主题</span>
</label>
<label className="flex cursor-pointer items-center gap-2">
<input
type="radio"
name="theme"
className="radio radio-primary"
checked={systemSettings.theme === 'dark'}
onChange={() => setSystemSettings({...systemSettings, theme: 'dark'})}
/>
<span>深色主题</span>
</label>
<label className="flex cursor-pointer items-center gap-2">
<input
type="radio"
name="theme"
className="radio radio-primary"
checked={systemSettings.theme === 'auto'}
onChange={() => setSystemSettings({...systemSettings, theme: 'auto'})}
/>
<span>自动切换</span>
</label>
</div>
</div>
<div className="form-control">
<label className="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
className="toggle toggle-primary"
checked={systemSettings.autoLoad}
onChange={(e) => setSystemSettings({...systemSettings, autoLoad: e.target.checked})}
/>
<span className="label-text">启用自动加载会话存档</span>
</label>
<label className="label cursor-pointer justify-start gap-3">
<input type="checkbox" className="toggle toggle-primary" defaultChecked />
<span className="label-text">启用消息通知</span>
</label>
<label className="label cursor-pointer justify-start gap-3">
<input type="checkbox" className="toggle toggle-primary" defaultChecked />
<span className="label-text">启用数据备份</span>
</label>
</div>
<div className="flex justify-end">
<button onClick={handleSaveSystemSettings} className="btn btn-primary">
<span className="mr-2">💾</span>
保存系统设置
</button>
</div>
</div>
</div>
</div>
{/* 系统信息 */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="card bg-base-100 shadow">
<div className="card-body">
<h2 className="card-title">系统信息</h2>
<div className="space-y-2">
<div className="flex justify-between">
<span>前端版本</span>
<span className="font-mono">1.0.0</span>
</div>
<div className="flex justify-between">
<span>React 版本</span>
<span className="font-mono">19.2.0</span>
</div>
<div className="flex justify-between">
<span>构建时间</span>
<span className="font-mono">2025-12-12</span>
</div>
<div className="flex justify-between">
<span>运行环境</span>
<span className="font-mono">Production</span>
</div>
</div>
</div>
</div>
<div className="card bg-base-100 shadow">
<div className="card-body">
<h2 className="card-title">危险操作</h2>
<p className="text-base-content/70 mb-4">请谨慎操作以下功能</p>
<div className="space-y-2">
<button className="btn btn-outline btn-error btn-block justify-start">
<span className="mr-2">🧹</span>
清理所有缓存
</button>
<button className="btn btn-outline btn-error btn-block justify-start">
<span className="mr-2">🔄</span>
重置所有设置
</button>
<button className="btn btn-outline btn-error btn-block justify-start">
<span className="mr-2">🗑️</span>
删除所有数据
</button>
</div>
<div className="mt-4 text-xs text-error">
⚠️ 注意:这些操作不可逆,请确认后再执行
</div>
</div>
</div>
</div>
</div>
)
}
export default Settings

View File

@@ -0,0 +1,123 @@
import axios from 'axios'
// API 基础配置
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'
// 创建 axios 实例
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
apiClient.interceptors.request.use(
(config) => {
// 可以在这里添加认证 token
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
apiClient.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
console.error('API 请求错误:', error)
if (error.response) {
// 服务器返回错误状态码
const { status, data } = error.response
switch (status) {
case 401:
// 未授权,跳转到登录页
localStorage.removeItem('auth_token')
window.location.href = '/login'
break
case 403:
console.error('权限不足:', data)
break
case 404:
console.error('资源不存在:', data)
break
case 500:
console.error('服务器内部错误:', data)
break
default:
console.error(`HTTP 错误 ${status}:`, data)
}
} else if (error.request) {
// 请求已发出但没有收到响应
console.error('网络错误,请检查网络连接')
} else {
// 请求配置错误
console.error('请求配置错误:', error.message)
}
return Promise.reject(error)
}
)
// 消息相关 API
export const messageApi = {
// 加载会话存档
loadMessages: (seq = 0, limit = 100) =>
apiClient.post('/messages/load', null, {
params: { seq, limit }
}),
// 获取消息列表
getMessages: (params = {}) =>
apiClient.get('/messages', { params }),
// 获取消息统计
getMessageStats: () =>
apiClient.get('/messages/stats'),
// 获取单个消息详情
getMessageById: (msgid) =>
apiClient.get(`/messages/${msgid}`),
}
// 认证相关 API
export const authApi = {
// 登录
login: (username, password) =>
apiClient.post('/auth/login', { username, password }),
// 获取用户信息
getUserInfo: () =>
apiClient.get('/auth/user'),
// 刷新 token
refreshToken: () =>
apiClient.post('/auth/refresh'),
}
// 系统相关 API
export const systemApi = {
// 获取系统状态
getSystemStatus: () =>
apiClient.get('/system/status'),
// 获取系统配置
getSystemConfig: () =>
apiClient.get('/system/config'),
// 更新系统配置
updateSystemConfig: (config) =>
apiClient.put('/system/config', config),
}
export default apiClient

17
backstage/vite.config.js Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
tailwindcss(),
react(),
],
base: '/chat_bm',
server: {
port: 3020,
host: '0.0.0.0',
allowedHosts: ['mp.yizhezhe.com']
}
})

View File

@@ -10,14 +10,13 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.18",
"@wecom/jssdk": "^2.3.3", "@wecom/jssdk": "^2.3.3",
"autoprefixer": "^10.4.22",
"axios": "^1.13.2", "axios": "^1.13.2",
"daisyui": "^5.5.5", "daisyui": "^5.5.5",
"postcss": "^8.5.6",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"tailwindcss": "^3.4.0" "tailwindcss": "^4.1.18"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",

719
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { useWeCom } from './hooks/useWeCom'; import { useWeCom } from './hooks/useWeCom';
import { workflowService, customerService } from './services/wecomService'; import { workflowService, customerService } from './services/wecomService';
import WorkflowProgress from './components/WorkflowProgress'; import WorkflowProgress from './components/WorkflowProgress';
import RolePromptDrawer from './components/RolePromptDrawer';
import { getStepNames } from './constants/workflowSteps'; import { getStepNames } from './constants/workflowSteps';
import { getSceneDescription } from './constants/wecomScenes'; import { getSceneDescription } from './constants/wecomScenes';
@@ -24,6 +25,9 @@ function App() {
// 执行计数 // 执行计数
const [executionCount, setExecutionCount] = useState(0); const [executionCount, setExecutionCount] = useState(0);
// 角色提示词抽屉状态
const [rolePromptDrawerOpen, setRolePromptDrawerOpen] = useState(false);
const messagesEndRef = useRef(null); const messagesEndRef = useRef(null);
const hasAutoExecutedRef = useRef(false); const hasAutoExecutedRef = useRef(false);
@@ -346,11 +350,21 @@ function App() {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button <button
className="hidden sm:inline px-3 py-1 bg-white/20 rounded text-sm hover:bg-white/30 transition-colors" className="hidden sm:inline px-3 py-1 bg-white/20 rounded text-sm hover:bg-white/30 transition-colors flex items-center space-x-1"
onClick={executeSmartReplyWorkflow} onClick={executeSmartReplyWorkflow}
disabled={workflowStatus.isRunning || !weCom.sdkReady} disabled={workflowStatus.isRunning || !weCom.sdkReady}
> >
{workflowStatus.isRunning ? '执行中...' : '执行工作流'} <span>{workflowStatus.isRunning ? '⏳' : '🔄️'}</span>
<span>{workflowStatus.isRunning ? '执行中...' : '执行工作流'}</span>
</button>
<button
className="hidden sm:inline px-3 py-1 bg-white/20 rounded text-sm hover:bg-white/30 transition-colors flex items-center space-x-1"
onClick={() => setRolePromptDrawerOpen(true)}
title="配置AI角色提示词"
>
<span>🤖</span>
<span>角色配置</span>
</button> </button>
{/* 移动端操作按钮 */} {/* 移动端操作按钮 */}
@@ -363,6 +377,13 @@ function App() {
> >
{workflowStatus.isRunning ? '⏳' : '🔄️'} {workflowStatus.isRunning ? '⏳' : '🔄️'}
</button> </button>
<button
className="w-8 h-8 flex items-center justify-center bg-white/20 rounded hover:bg-white/30 transition-colors"
onClick={() => setRolePromptDrawerOpen(true)}
title="角色配置"
>
🤖
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -439,33 +460,41 @@ function App() {
) : ( ) : (
// 聊天消息列表 // 聊天消息列表
<div className="max-w-4xl mx-auto space-y-3 sm:space-y-4"> <div className="max-w-4xl mx-auto space-y-3 sm:space-y-4">
{messages.map((message, index) => ( {messages.map((message, index) => {
<div // 确定justify类名
key={index} let justifyClass = 'justify-start'; // 默认左对齐
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} ${ if (message.role === 'user') {
message.role === 'system' ? 'justify-center' : '' justifyClass = 'justify-end'; // 用户消息右对齐
}`} } else if (message.role === 'system') {
> justifyClass = 'justify-center'; // 系统消息居中
<div className={`max-w-[85%] sm:max-w-[70%] rounded-2xl px-3 py-2 sm:px-4 sm:py-3 ${ }
message.role === 'user'
? 'bg-blue-600 text-white rounded-br-none' return (
: message.role === 'system' <div
? 'bg-gray-100 text-gray-700 border border-gray-200' key={index}
: 'bg-white border border-gray-200 rounded-bl-none shadow-sm' className={`flex ${justifyClass}`}
}`}> >
<div className="text-sm leading-relaxed break-words"> <div className={`max-w-[85%] sm:max-w-[70%] rounded-2xl px-3 py-2 sm:px-4 sm:py-3 ${
{message.content} message.role === 'user'
</div> ? 'bg-blue-600 text-white rounded-br-none'
{/* <div className={`text-xs mt-1 ${ : message.role === 'system'
message.role === 'user' ? 'text-blue-100' : ? 'bg-gray-100 text-gray-700 border border-gray-200'
message.role === 'system' ? 'text-gray-500' : : 'bg-white border border-gray-200 rounded-bl-none shadow-sm'
'text-gray-500'
}`}> }`}>
{message.timestamp} <div className="text-sm leading-relaxed break-words">
</div> */} {message.content}
</div>
{/* <div className={`text-xs mt-1 ${
message.role === 'user' ? 'text-blue-100' :
message.role === 'system' ? 'text-gray-500' :
'text-gray-500'
}`}>
{message.timestamp}
</div> */}
</div>
</div> </div>
</div> );
))} })}
{/* 加载状态 */} {/* 加载状态 */}
{workflowStatus.isRunning && ( {workflowStatus.isRunning && (
@@ -591,6 +620,12 @@ function App() {
</div> </div>
</footer> </footer>
)} )}
{/* 角色提示词配置抽屉 */}
<RolePromptDrawer
isOpen={rolePromptDrawerOpen}
onClose={() => setRolePromptDrawerOpen(false)}
/>
</div> </div>
); );
} }

View File

@@ -0,0 +1,448 @@
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://mp.yizhezhe.com/api';
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
const RolePromptDrawer = ({ isOpen, onClose }) => {
// 引用滚动容器
const drawerContentRef = useRef(null);
// 表单数据状态
const [formData, setFormData] = useState({
name: '',
description: '',
skills: '',
restrictions: ''
});
// 加载和保存状态
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isResetting, setIsResetting] = useState(false);
const [saveStatus, setSaveStatus] = useState({ type: null, message: '' });
// Popover确认状态
const [showResetConfirm, setShowResetConfirm] = useState(false);
// 加载角色配置
const loadRoleConfig = async () => {
setIsLoading(true);
try {
const response = await apiClient.get('/glm/role');
const result = response.data;
if (result.code === 200 && result.result) {
const roleConfig = result.result;
// 直接使用字符串(后端已返回字符串格式)
setFormData({
name: roleConfig.name || '',
description: roleConfig.description || '',
skills: roleConfig.skills || '',
restrictions: roleConfig.restrictions || ''
});
} else {
throw new Error(result.message || '加载角色配置失败');
}
} catch (error) {
console.error('加载角色配置失败:', error);
setSaveStatus({
type: 'error',
message: `加载失败: ${error.response?.data?.message || error.message}`
});
} finally {
setIsLoading(false);
}
};
// 保存角色配置
const handleSave = async (e) => {
e.preventDefault();
// 验证数据
if (!formData.name.trim() || !formData.description.trim()) {
setSaveStatus({ type: 'error', message: '角色名称和描述不能为空' });
return;
}
setIsSaving(true);
setSaveStatus({ type: 'loading', message: '保存中...' });
try {
// 直接发送字符串(后端期望字符串格式)
const roleConfigToSave = {
name: formData.name.trim(),
description: formData.description.trim(),
skills: formData.skills.trim(),
restrictions: formData.restrictions.trim()
};
const response = await apiClient.put('/glm/role', roleConfigToSave);
const result = response.data;
if (result.code === 200) {
setSaveStatus({ type: 'success', message: '保存成功!' });
// 滚动到顶部,让用户看到提示
if (drawerContentRef.current) {
drawerContentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
}
// 3秒后清除成功消息
setTimeout(() => {
setSaveStatus({ type: null, message: '' });
}, 3000);
} else {
throw new Error(result.message || '保存角色配置失败');
}
} catch (error) {
console.error('保存角色配置失败:', error);
setSaveStatus({
type: 'error',
message: `保存失败: ${error.response?.data?.message || error.message}`
});
} finally {
setIsSaving(false);
}
};
// 重置按钮点击 - 显示确认Popover
const handleResetButtonClick = () => {
setShowResetConfirm(true);
};
// 取消重置
const handleCancelReset = () => {
setShowResetConfirm(false);
};
// 确认重置到默认角色配置
const handleConfirmReset = async () => {
setShowResetConfirm(false);
setIsResetting(true);
setSaveStatus({ type: 'loading', message: '重置到默认配置中...' });
try {
const response = await apiClient.post('/glm/role/reset');
const result = response.data;
if (result.code === 200) {
setSaveStatus({ type: 'success', message: '已重置为默认角色配置!' });
// 重新加载角色配置
await loadRoleConfig();
// 滚动到顶部,让用户看到提示
if (drawerContentRef.current) {
drawerContentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
}
// 3秒后清除成功消息
setTimeout(() => {
setSaveStatus({ type: null, message: '' });
}, 3000);
} else {
throw new Error(result.message || '重置角色配置失败');
}
} catch (error) {
console.error('重置角色配置失败:', error);
setSaveStatus({
type: 'error',
message: `重置失败: ${error.response?.data?.message || error.message}`
});
} finally {
setIsResetting(false);
}
};
// 重置表单
const handleReset = () => {
loadRoleConfig();
setSaveStatus({ type: null, message: '' });
};
// 当抽屉打开时加载数据
useEffect(() => {
if (isOpen) {
loadRoleConfig();
setSaveStatus({ type: null, message: '' });
}
}, [isOpen]);
// 点击外部关闭Popover
useEffect(() => {
const handleClickOutside = (event) => {
if (showResetConfirm && !event.target.closest('.reset-confirm-container')) {
setShowResetConfirm(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [showResetConfirm]);
// ESC键关闭Popover
useEffect(() => {
const handleEscKey = (event) => {
if (event.key === 'Escape' && showResetConfirm) {
setShowResetConfirm(false);
}
};
document.addEventListener('keydown', handleEscKey);
return () => document.removeEventListener('keydown', handleEscKey);
}, [showResetConfirm]);
// 处理输入变化
const handleInputChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
if (!isOpen) return null;
return (
<div className="fixed right-0 top-0 h-full w-full lg:w-3/5 z-50 bg-white border-l border-gray-200 shadow-2xl overflow-y-auto">
<div ref={drawerContentRef} className="p-4 lg:p-6 h-full overflow-y-auto">
{/* 头部 */}
<div className="flex items-center justify-between mb-4 lg:mb-6 pb-3 lg:pb-4 border-b border-gray-200">
<div>
<h3 className="text-lg lg:text-xl font-bold text-gray-800">角色提示词配置</h3>
<p className="text-xs lg:text-sm text-gray-500 mt-1">配置AI角色的名称、描述、技能和限制</p>
</div>
<button
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
onClick={onClose}
disabled={isSaving}
title="关闭"
>
<svg className="w-4 h-4 lg:w-5 lg:h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* 状态提示 */}
{saveStatus.type && (
<div className={`mb-3 lg:mb-4 p-2 lg:p-3 rounded-lg ${
saveStatus.type === 'success' ? 'bg-green-50 border border-green-200 text-green-800' :
saveStatus.type === 'error' ? 'bg-red-50 border border-red-200 text-red-800' :
'bg-blue-50 border border-blue-200 text-blue-800'
}`}>
<div className="flex items-center">
{saveStatus.type === 'loading' && (
<span className="loading loading-spinner loading-xs lg:loading-sm mr-2"></span>
)}
<span className="text-xs lg:text-sm">{saveStatus.message}</span>
</div>
</div>
)}
{/* 加载状态 */}
{isLoading ? (
<div className="flex justify-center items-center h-48 lg:h-64">
<div className="text-center">
<span className="loading loading-spinner loading-md lg:loading-lg text-gray-400"></span>
<p className="mt-3 lg:mt-4 text-sm lg:text-base text-gray-500">加载角色配置中...</p>
</div>
</div>
) : (
/* 表单内容 */
<form onSubmit={handleSave} className="space-y-4 lg:space-y-6">
{/* 角色名称 */}
<div className="space-y-1 lg:space-y-2">
<label className="block text-xs lg:text-sm font-medium text-gray-700">
角色名称
</label>
<input
type="text"
className="w-full px-3 py-2 text-xs lg:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors focus:outline-none"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
placeholder="例如:兜兜"
required
disabled={isSaving}
/>
</div>
{/* 角色描述 */}
<div className="space-y-1 lg:space-y-2">
<label className="block text-xs lg:text-sm font-medium text-gray-700">
角色描述
<span className="text-gray-500 font-normal ml-1">(AI角色的背景、性格特点等)</span>
</label>
<textarea
className="w-full px-3 py-2 text-xs lg:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors h-32 lg:h-40 focus:outline-none"
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
placeholder="描述AI角色的背景、性格特点等..."
required
disabled={isSaving}
/>
</div>
{/* 技能列表 */}
<div className="space-y-1 lg:space-y-2">
<label className="block text-xs lg:text-sm font-medium text-gray-700">
技能列表
<span className="text-gray-500 font-normal ml-1">(每行输入一个技能)</span>
</label>
<textarea
className="w-full px-3 py-2 text-xs lg:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors font-mono h-36 lg:h-48 focus:outline-none"
value={formData.skills}
onChange={(e) => handleInputChange('skills', e.target.value)}
placeholder="亲切温暖的沟通&#10;轻松友好的语气&#10;简洁明了的描述"
disabled={isSaving}
/>
<p className="text-xs text-gray-500">
每行输入一个技能,空行会被自动过滤
</p>
</div>
{/* 限制列表 */}
<div className="space-y-1 lg:space-y-2">
<label className="block text-xs lg:text-sm font-medium text-gray-700">
限制列表
<span className="text-gray-500 font-normal ml-1">(每行输入一个限制)</span>
</label>
<textarea
className="w-full px-3 py-2 text-xs lg:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-200 focus:border-blue-200 transition-colors font-mono h-36 lg:h-48 focus:outline-none"
value={formData.restrictions}
onChange={(e) => handleInputChange('restrictions', e.target.value)}
placeholder="回复必须基于提供的知识库中的内容&#10;每条回复应保持在80字以内&#10;只讨论与宝妈相关的主题"
disabled={isSaving}
/>
<p className="text-xs text-gray-500">
每行输入一个限制,空行会被自动过滤
</p>
</div>
{/* 操作按钮 */}
<div className="flex justify-between items-center pt-4 lg:pt-6 border-t border-gray-200">
{/* 左边:重置到默认按钮 */}
<div className="relative reset-confirm-container">
<button
type="button"
className="px-3 py-1.5 lg:px-4 lg:py-2 text-xs lg:text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleResetButtonClick}
disabled={isSaving || isResetting}
>
{isResetting ? (
<span className="flex items-center">
<span className="loading loading-spinner loading-xs mr-1 lg:mr-2"></span>
重置中...
</span>
) : (
'重置到默认'
)}
</button>
{/* Popover确认气泡 */}
{showResetConfirm && (
<div className="absolute bottom-full mb-2 left-0 bg-white border border-gray-300 rounded-lg shadow-xl p-4 w-64 z-50">
{/* 三角形箭头 - 调整到左边 */}
<div className="absolute -bottom-2 left-4 w-4 h-4 bg-white border-r border-b border-gray-300 transform rotate-45"></div>
<p className="text-sm text-gray-700 mb-3">
确定要重置为默认角色配置吗?当前配置将会被覆盖。
</p>
<div className="flex justify-end space-x-2">
<button
type="button"
className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
onClick={handleCancelReset}
>
取消
</button>
<button
type="button"
className="px-3 py-1.5 text-xs font-medium text-white bg-orange-500 hover:bg-orange-600 rounded-lg transition-colors"
onClick={handleConfirmReset}
>
确定重置
</button>
</div>
</div>
)}
</div>
{/* 右边:其他操作按钮 */}
<div className="flex space-x-2 lg:space-x-3">
<button
type="button"
className="px-3 py-1.5 lg:px-4 lg:py-2 text-xs lg:text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
onClick={handleReset}
disabled={isSaving || isResetting}
>
重置
</button>
<button
type="button"
className="px-3 py-1.5 lg:px-4 lg:py-2 text-xs lg:text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
onClick={onClose}
disabled={isSaving || isResetting}
>
取消
</button>
<button
type="submit"
className="px-3 py-1.5 lg:px-4 lg:py-2 text-xs lg:text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isSaving || isResetting}
>
{isSaving ? (
<span className="flex items-center">
<span className="loading loading-spinner loading-xs mr-1 lg:mr-2"></span>
保存中...
</span>
) : (
'保存配置'
)}
</button>
</div>
</div>
</form>
)}
{/* 使用说明 */}
{!isLoading && (
<div className="mt-6 lg:mt-8 pt-4 lg:pt-6 border-t border-gray-200">
<div className="text-xs lg:text-sm text-gray-600">
<h4 className="font-medium mb-2 lg:mb-3 text-gray-700">使用说明</h4>
<ul className="space-y-1 lg:space-y-2">
<li className="flex items-start">
<span className="text-blue-500 mr-1 lg:mr-2 text-xs">•</span>
<span className="text-xs lg:text-sm">角色配置将影响AI的回复风格和内容限制</span>
</li>
<li className="flex items-start">
<span className="text-blue-500 mr-1 lg:mr-2 text-xs">•</span>
<span className="text-xs lg:text-sm">技能列表定义AI的能力特点,每行输入一个技能</span>
</li>
<li className="flex items-start">
<span className="text-blue-500 mr-1 lg:mr-2 text-xs">•</span>
<span className="text-xs lg:text-sm">限制列表确保AI回复符合业务要求,每行输入一个限制</span>
</li>
<li className="flex items-start">
<span className="text-blue-500 mr-1 lg:mr-2 text-xs">•</span>
<span className="text-xs lg:text-sm">保存后配置将立即生效,影响后续AI回复</span>
</li>
<li className="flex items-start">
<span className="text-orange-500 mr-1 lg:mr-2 text-xs">•</span>
<span className="text-xs lg:text-sm">点击"重置到默认"可将角色配置恢复为系统默认值</span>
</li>
</ul>
</div>
</div>
)}
</div>
</div>
);
};
export default RolePromptDrawer;

View File

@@ -1,6 +1,5 @@
@tailwind base; @import "tailwindcss";
@tailwind components; @plugin "daisyui";
@tailwind utilities;
/* 自定义滚动条样式 */ /* 自定义滚动条样式 */
::-webkit-scrollbar { ::-webkit-scrollbar {
@@ -41,7 +40,6 @@
} }
body { body {
@apply antialiased;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
@@ -50,12 +48,12 @@
/* 触摸友好的按钮 */ /* 触摸友好的按钮 */
@layer components { @layer components {
.touch-button { .touch-button {
@apply active:scale-95 transition-transform duration-150; transform: scale(0.95);
transition: transform 150ms;
} }
/* 移动端优化输入框 */ /* 移动端优化输入框 */
.mobile-input { .mobile-input {
@apply text-base;
font-size: 16px !important; /* 防止iOS缩放 */ font-size: 16px !important; /* 防止iOS缩放 */
} }
} }
@@ -85,7 +83,13 @@
/* 移动端优化模态框 */ /* 移动端优化模态框 */
.mobile-modal { .mobile-modal {
@apply fixed inset-0 z-50 flex items-center justify-center p-4; position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
} }

View File

@@ -1,14 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
daisyui: {
themes: ["light", "dark", "cupcake"],
},
}

View File

@@ -1,9 +1,13 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [
tailwindcss(),
react(),
],
base: '/chat_fe', base: '/chat_fe',
server: { server: {
port: 3010, port: 3010,