update a lot

This commit is contained in:
Wenjie Zhang 2025-03-25 05:40:07 +08:00
parent b7533a0111
commit dbf5000ac7
12 changed files with 1984 additions and 138 deletions

View File

@ -1,53 +1,39 @@
from src.agents.registry import BaseAgent from src.agents.chatbot import ChatbotAgent, ChatbotConfiguration
from src.agents.chatbot.graph import ChatbotAgent
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import AIMessageChunk, ToolMessage
class AgentManager: class AgentManager:
def __init__(self): def __init__(self):
self.agents = {} self.agents = {}
def add_agent(self, agent_id, agent): def add_agent(self, agent_id, agent_class, configuration_class):
self.agents[agent_id] = { self.agents[agent_id] = {
"agent": agent, "agent_class": agent_class,
"configuration": agent.configuration "configuration_class": configuration_class
} }
def agent_cli(agent: BaseAgent, config: RunnableConfig = None): def get_runnable_agent(self, agent_id, **kwargs):
config = config or {} agent_class = self.get_agent(agent_id)
if "configurable" not in config: configuration_class = self.get_configuration(agent_id)
config["configurable"] = {} configuration = configuration_class(**kwargs)
return agent_class(configuration)
while True: def get_agent(self, agent_id):
user_input = input("\nUser: ") return self.agents[agent_id]["agent_class"]
if user_input.lower() in ["quit", "exit", "q"]:
print("Goodbye!")
break
stream_flag = False def get_configuration(self, agent_id):
for msg, metadata in agent.stream_messages([{"role": "user", "content": user_input}], config): return self.agents[agent_id]["configuration_class"]
if isinstance(msg, AIMessageChunk):
content = msg.content or msg.tool_calls
if not content:
if stream_flag == True:
print()
stream_flag = False
continue
if stream_flag == False and content: agent_manager = AgentManager()
print(f"AI: {content}", end="", flush=True) agent_manager.add_agent("chatbot", ChatbotAgent, ChatbotConfiguration)
stream_flag = True
continue
elif content: __all__ = ["agent_manager"]
print(f"{content}", end="", flush=True)
if isinstance(msg, ToolMessage):
print(f"Tool: {msg.content}")
def get_agents(): if __name__ == "__main__":
agent_manager = AgentManager() agent = agent_manager.get_agent("chatbot")
agent_manager.add_agent("chatbot", ChatbotAgent()) conf = agent_manager.get_configuration("chatbot")
return agent_manager.agents agent_info = {
"name": agent.name,
"description": agent.description,
}
print(agent_info)

View File

@ -1,3 +1,4 @@
from .graph import ChatbotAgent from .graph import ChatbotAgent
from .configuration import ChatbotConfiguration
__all__ = ["ChatbotAgent"] __all__ = ["ChatbotAgent", "ChatbotConfiguration"]

View File

@ -1,7 +1,5 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List
from langchain_core.messages import SystemMessage
from langchain_core.tools import Tool from langchain_core.tools import Tool
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
@ -9,20 +7,19 @@ from src import config
from src.models import select_model from src.models import select_model
from src.agents.registry import Configuration from src.agents.registry import Configuration
def get_default_llm():
return select_model(config).chat_open_ai
def get_default_tools():
from langchain_community.tools.tavily_search import TavilySearchResults
return [TavilySearchResults(max_results=10)]
def get_default_requirements(): def get_default_requirements():
return ["TAVILY_API_KEY"] return ["TAVILY_API_KEY"]
@dataclass(kw_only=True) @dataclass(kw_only=True)
class ChatbotConfiguration(Configuration): class ChatbotConfiguration(Configuration):
name: str = "chatbot"
description: str = "A chatbot that can answer questions and help with tasks."
requirements: list[str] = field(default_factory=get_default_requirements) requirements: list[str] = field(default_factory=get_default_requirements)
tools: list[Tool] = field(default_factory=get_default_tools) llm: ChatOpenAI | None = None
llm: ChatOpenAI = field(default_factory=get_default_llm) model_provider: str = "siliconflow"
model_name: str = "Qwen/Qwen2.5-72B-Instruct"
def __post_init__(self):
# TODO 需要确保这里的模型是支持 tools 的
if self.llm is None:
self.llm = select_model(config=config,
model_provider=self.model_provider,
model_name=self.model_name).chat_open_ai

View File

@ -3,7 +3,6 @@ import uuid
from typing import Any from typing import Any
from datetime import datetime from datetime import datetime
from langchain_core.messages import AIMessageChunk, ToolMessage
from langchain_core.runnables import RunnableConfig from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, START, END from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode, tools_condition from langgraph.prebuilt import ToolNode, tools_condition
@ -13,53 +12,56 @@ from src.agents.registry import State, BaseAgent
from src.agents.chatbot.configuration import ChatbotConfiguration from src.agents.chatbot.configuration import ChatbotConfiguration
class ChatbotAgent(BaseAgent): class ChatbotAgent(BaseAgent):
name = "chatbot"
description = "A chatbot that can answer questions and help with tasks."
_graph_cache = None _graph_cache = None
def __init__(self, configuration: ChatbotConfiguration = None): def __init__(self, configuration: ChatbotConfiguration = None):
super().__init__(configuration) super().__init__(configuration)
self.configuration = configuration or ChatbotConfiguration() self.configuration = configuration or ChatbotConfiguration()
self.llm = configuration.llm self.llm = self.configuration.llm
def llm_call(self, state: State) -> dict[str, Any]: def _get_tools(self, config: RunnableConfig):
tools = self.configuration.tools """根据配置获取工具"""
system_prompt = { tools = []
"role": "system", if not config:
"content": ( return tools
f"Current time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
)
}
messages = [system_prompt] + state["messages"]
model = self.llm.bind_tools(tools)
res = model.invoke(messages) if config.get("configurable", {}).get("use_web", None):
from langchain_community.tools.tavily_search import TavilySearchResults
tools.append(TavilySearchResults(max_results=10))
return tools
def llm_call(self, state: State, config: RunnableConfig) -> dict[str, Any]:
model = self.llm.bind_tools(self._get_tools(config))
res = model.invoke(state["messages"])
return {"messages": [res]} return {"messages": [res]}
def get_graph(self): def get_graph(self, config: RunnableConfig = None):
"""构建图""" """构建图"""
if ChatbotAgent._graph_cache is None: workflow = StateGraph(State)
workflow = StateGraph(State) workflow.add_node("chatbot", self.llm_call)
workflow.add_node("chatbot", self.llm_call) workflow.add_node("tools", ToolNode(tools=self._get_tools(config)))
workflow.add_node("tools", ToolNode(tools=self.configuration.tools)) workflow.add_edge(START, "chatbot")
workflow.add_edge(START, "chatbot") workflow.add_conditional_edges(
workflow.add_conditional_edges( "chatbot",
"chatbot", tools_condition,
tools_condition, )
) workflow.add_edge("tools", "chatbot")
workflow.add_edge("tools", "chatbot") workflow.add_edge("chatbot", END)
workflow.add_edge("chatbot", END)
graph = workflow.compile(checkpointer=MemorySaver()) graph = workflow.compile(checkpointer=MemorySaver())
ChatbotAgent._graph_cache = graph return graph
return ChatbotAgent._graph_cache
def stream_values(self, messages: list[str], config: RunnableConfig = None): def stream_values(self, messages: list[str], config: RunnableConfig = None):
graph = self.get_graph() graph = self.get_graph(config)
for event in graph.stream({"messages": messages}, stream_mode="values", config=config): for event in graph.stream({"messages": messages}, stream_mode="values", config=config):
yield event["messages"] yield event["messages"]
def stream_messages(self, messages: list[str], config: RunnableConfig = None): def stream_messages(self, messages: list[str], config: RunnableConfig = None):
graph = self.get_graph() graph = self.get_graph(config)
for msg, metadata in graph.stream({"messages": messages}, stream_mode="messages", config=config): for msg, metadata in graph.stream({"messages": messages}, stream_mode="messages", config=config):
msg_type = msg.type msg_type = msg.type
@ -67,14 +69,13 @@ class ChatbotAgent(BaseAgent):
if not return_keys or msg_type in return_keys: if not return_keys or msg_type in return_keys:
yield msg, metadata yield msg, metadata
def main(): def main():
agent = ChatbotAgent(ChatbotConfiguration()) agent = ChatbotAgent(ChatbotConfiguration())
thread_id = str(uuid.uuid4()) thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}} config = {"configurable": {"thread_id": thread_id}}
from src.agents import agent_cli from src.agents.utils import agent_cli
agent_cli(agent, config) agent_cli(agent, config)

View File

@ -1,12 +1,11 @@
from typing import Type, Annotated, Optional, TypedDict from typing import Type, Annotated, Optional, TypedDict
from abc import abstractmethod from abc import abstractmethod
from langchain_core.tools import BaseTool
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.graph.message import add_messages from langchain_core.messages import BaseMessage
from langchain_core.messages import BaseMessage, ToolMessage, AIMessage, filter_messages
from langgraph.graph.state import CompiledStateGraph from langgraph.graph.state import CompiledStateGraph
from langgraph.graph.message import add_messages
from dataclasses import dataclass from dataclasses import dataclass
@ -26,7 +25,8 @@ class Configuration:
""" """
定义一个基础 Configuration 各类 graph 继承 定义一个基础 Configuration 各类 graph 继承
""" """
llm: ChatOpenAI pass
class BaseAgent(): class BaseAgent():

37
src/agents/utils.py Normal file
View File

@ -0,0 +1,37 @@
from src.agents.registry import BaseAgent
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import AIMessageChunk, ToolMessage
def agent_cli(agent: BaseAgent, config: RunnableConfig = None):
config = config or {}
if "configurable" not in config:
config["configurable"] = {}
while True:
user_input = input("\nUser: ")
if user_input.lower() in ["quit", "exit", "q"]:
print("Goodbye!")
break
stream_flag = False
for msg, metadata in agent.stream_messages([{"role": "user", "content": user_input}], config):
if isinstance(msg, AIMessageChunk):
content = msg.content or msg.tool_calls
if not content:
if stream_flag == True:
print()
stream_flag = False
continue
if stream_flag == False and content:
print(f"AI: {content}", end="", flush=True)
stream_flag = True
continue
elif content:
print(f"{content}", end="", flush=True)
if isinstance(msg, ToolMessage):
print(f"Tool: {msg.content}")

View File

@ -1,10 +1,14 @@
import json import json
import asyncio import asyncio
import traceback import traceback
import uuid
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from fastapi.responses import StreamingResponse, Response from fastapi.responses import StreamingResponse
from src.core import HistoryManager from langchain_core.messages import AIMessageChunk
from src import executor, config, retriever from src import executor, config, retriever
from src.core import HistoryManager
from src.agents import agent_manager
from src.models import select_model from src.models import select_model
from src.utils.logging_config import logger from src.utils.logging_config import logger
@ -20,8 +24,8 @@ async def chat_get():
def chat_post( def chat_post(
query: str = Body(...), query: str = Body(...),
meta: dict = Body(None), meta: dict = Body(None),
history: list = Body(...), history: list | None = Body(None),
cur_res_id: str = Body(...)): thread_id: str | None = Body(None)):
model = select_model(config) model = select_model(config)
meta["server_model_name"] = model.model_name meta["server_model_name"] = model.model_name
@ -31,7 +35,6 @@ def chat_post(
def make_chunk(content=None, **kwargs): def make_chunk(content=None, **kwargs):
return json.dumps({ return json.dumps({
"response": content, "response": content,
"model_name": config.model_name,
"meta": meta, "meta": meta,
**kwargs **kwargs
}, ensure_ascii=False).encode('utf-8') + b"\n" }, ensure_ascii=False).encode('utf-8') + b"\n"
@ -76,7 +79,7 @@ def chat_post(
else: else:
content += delta.content or "" content += delta.content or ""
chunk = make_chunk(content=content, status="loading") chunk = make_chunk(content=delta.content, status="loading")
yield chunk yield chunk
logger.debug(f"Final response: {content}") logger.debug(f"Final response: {content}")
@ -118,3 +121,61 @@ async def call(query: str = Body(...), meta: dict = Body(None)):
logger.debug({"query": query, "response": response.content}) logger.debug({"query": query, "response": response.content})
return {"response": response.content} return {"response": response.content}
@chat.get("/agent")
async def get_agent():
agents = [{
"name": agent["agent_class"].name,
"description": agent["agent_class"].description
} for agent in agent_manager.agents.values()]
return {"agents": agents}
@chat.post("/agent/{agent_name}")
def chat_agent(agent_name: str,
query: str = Body(...),
meta: dict = Body({}),
history: list = Body(...),
thread_id: str | None = Body(None)):
meta["server_model_name"] = agent_name
agent = agent_manager.get_runnable_agent(agent_name)
history_manager = HistoryManager(history)
messages = history_manager.get_history_with_msg(query, max_rounds=meta.get('history_round'))
history_manager.add_user(query) # 注意这里使用原始查询
runnable_config = {
"configurable": {
"thread_id": thread_id or str(uuid.uuid4()),
"use_web": meta.get("use_web", False),
"return_keys": []
}
}
def make_chunk(content=None, **kwargs):
return json.dumps({
"response": content,
"model_name": agent_name,
"meta": meta,
**kwargs
}, ensure_ascii=False).encode('utf-8') + b"\n"
def stream_messages():
content = ""
yield make_chunk(status="waiting")
for msg, metadata in agent.stream_messages(messages, runnable_config):
# logger.debug(f"msg: {msg.model_dump()}, {metadata=}")
if isinstance(msg, AIMessageChunk) and msg.content != "<tool_call>":
content += msg.content
yield make_chunk(content=msg.content,
msg=msg.model_dump(),
metadata=metadata,
status="loading")
else:
yield make_chunk(msg=msg.model_dump(),
metadata=metadata,
status="loading")
yield make_chunk(status="finished", history=history_manager.update_ai(content))
return StreamingResponse(stream_messages(), media_type='application/json')

View File

@ -370,7 +370,7 @@ const updateMessage = (info) => {
try { try {
// text // text
if (info.text !== null && info.text !== undefined && info.text !== '') { if (info.text !== null && info.text !== undefined && info.text !== '') {
msg.text = info.text; msg.text += info.text;
} }
if (info.reasoning_content !== null && info.reasoning_content !== undefined && info.reasoning_content !== '') { if (info.reasoning_content !== null && info.reasoning_content !== undefined && info.reasoning_content !== '') {
@ -463,6 +463,7 @@ const fetchChatResponse = (user_input, cur_res_id) => {
history: conv.value.history, history: conv.value.history,
meta: meta, meta: meta,
cur_res_id: cur_res_id, cur_res_id: cur_res_id,
thread_id: conv.value.id.toString(),
}), }),
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -639,6 +640,12 @@ watch(
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header__left {
.close {
margin-right: 12px;
}
}
} }
.nav-btn { .nav-btn {
@ -652,6 +659,7 @@ watch(
font-size: 1rem; font-size: 1rem;
width: auto; width: auto;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
transition: background-color 0.3s;
.text { .text {
margin-left: 10px; margin-left: 10px;
@ -678,6 +686,7 @@ watch(
padding: 12px; padding: 12px;
z-index: 11; z-index: 11;
width: 280px; width: 280px;
transition: transform 0.3s ease, opacity 0.3s ease;
.flex-center { .flex-center {
display: flex; display: flex;
@ -1051,9 +1060,27 @@ watch(
@keyframes loading {0%,80%,100%{transform:scale(0.5);}40%{transform:scale(1);}} @keyframes loading {0%,80%,100%{transform:scale(0.5);}40%{transform:scale(1);}}
.slide-out-left{-webkit-animation:slide-out-left .2s cubic-bezier(.55,.085,.68,.53) both;animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both} .slide-out-left{-webkit-animation:slide-out-left .2s cubic-bezier(.55,.085,.68,.53) both;animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both}
.swing-in-top-fwd{-webkit-animation:swing-in-top-fwd .2s ease-out both;animation:swing-in-top-fwd .2s ease-out both} .swing-in-top-fwd {
@-webkit-keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}@keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}} -webkit-animation: swing-in-top-fwd 0.3s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
@-webkit-keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}@keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}} animation: swing-in-top-fwd 0.3s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
}
@keyframes swing-in-top-fwd {
0% {
-webkit-transform: rotateX(-100deg);
transform: rotateX(-100deg);
-webkit-transform-origin: top;
transform-origin: top;
opacity: 0;
}
100% {
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
-webkit-transform-origin: top;
transform-origin: top;
opacity: 1;
}
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideInUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes slideInUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

View File

@ -21,6 +21,8 @@ import {
StarFilled, StarFilled,
StarOutlined, StarOutlined,
ExclamationCircleOutlined, ExclamationCircleOutlined,
RobotOutlined,
RobotFilled,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
import { themeConfig } from '@/assets/theme' import { themeConfig } from '@/assets/theme'
import { useConfigStore } from '@/stores/config' import { useConfigStore } from '@/stores/config'
@ -73,6 +75,37 @@ onMounted(() => {
// 使 vue3 setup composition API // 使 vue3 setup composition API
const route = useRoute() const route = useRoute()
console.log(route) console.log(route)
//
const mainList = [{
name: '对话',
path: '/chat',
icon: MessageOutlined,
activeIcon: MessageFilled,
}, {
name: '智能体',
path: '/agent',
icon: RobotOutlined,
activeIcon: RobotFilled,
}, {
name: '图谱',
path: '/graph',
icon: ProjectOutlined,
activeIcon: ProjectFilled,
// hidden: !configStore.config.enable_knowledge_graph,
}, {
name: '知识库',
path: '/database',
icon: BookOutlined,
activeIcon: BookFilled,
// hidden: !configStore.config.enable_knowledge_base,
}, {
name: '工具',
path: '/tools',
icon: ToolOutlined,
activeIcon: ToolFilled,
}
]
</script> </script>
<template> <template>
@ -107,22 +140,18 @@ console.log(route)
</router-link> </router-link>
</div> </div>
<div class="nav"> <div class="nav">
<RouterLink to="/chat" class="nav-item" active-class="active"> <!-- 使用mainList渲染导航项 -->
<component class="icon" :is="route.path === '/chat' ? MessageFilled : MessageOutlined" /> <RouterLink
<span class="text">对话</span> v-for="(item, index) in mainList"
</RouterLink> :key="index"
<RouterLink to="/database" class="nav-item" active-class="active"> :to="item.path"
<component class="icon" :is="route.path.startsWith('/database') ? FolderFilled : FolderOutlined" /> v-show="!item.hidden"
<span class="text">知识</span> class="nav-item"
</RouterLink> active-class="active">
<RouterLink to="/graph" class="nav-item" active-class="active"> <component class="icon" :is="route.path.startsWith(item.path) ? item.activeIcon : item.icon" />
<component class="icon" :is="route.path.startsWith('/graph') ? ProjectFilled: ProjectOutlined" /> <span class="text">{{item.name}}</span>
<span class="text">图谱</span>
</RouterLink>
<RouterLink to="/tools" class="nav-item" active-class="active">
<component class="icon" :is="route.path.startsWith('/tools') ? ToolFilled: ToolOutlined" />
<span class="text">工具</span>
</RouterLink> </RouterLink>
<a-tooltip placement="right"> <a-tooltip placement="right">
<template #title>后端疑似没有正常启动或者正在繁忙中请刷新一下或者检查 docker logs api-dev</template> <template #title>后端疑似没有正常启动或者正在繁忙中请刷新一下或者检查 docker logs api-dev</template>
<div class="nav-item warning" v-if="!configStore.config._config_items"> <div class="nav-item warning" v-if="!configStore.config._config_items">
@ -140,7 +169,7 @@ console.log(route)
</span> </span>
</a> </a>
</div> </div>
<RouterLink class="nav-item setting" to="/setting" active-class="active"> <RouterLink class="nav-item setting" to="/setting" active-class="active">
<component class="icon" :is="route.path === '/setting' ? SettingFilled : SettingOutlined" /> <component class="icon" :is="route.path === '/setting' ? SettingFilled : SettingOutlined" />
</RouterLink> </RouterLink>
</div> </div>

View File

@ -30,6 +30,19 @@ const router = createRouter({
} }
] ]
}, },
{
path: '/agent',
name: 'agent',
component: AppLayout,
children: [
{
path: '',
name: 'AgentComp',
component: () => import('../views/AgentView.vue'),
meta: { keepAlive: true }
}
]
},
{ {
path: '/graph', path: '/graph',
name: 'graph', name: 'graph',

1694
web/src/views/AgentView.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="chat-container"> <div class="chat-container">
<div class="conversations" :class="['conversations', { 'is-open': state.isSidebarOpen }]"> <div class="conversations" :class="{ 'is-open': state.isSidebarOpen }">
<div class="actions"> <div class="actions">
<!-- <div class="action new" @click="addNewConv"><FormOutlined /></div> --> <!-- <div class="action new" @click="addNewConv"><FormOutlined /></div> -->
<span class="header-title">对话历史</span> <span class="header-title">对话历史</span>
@ -129,29 +129,24 @@ onMounted(() => {
position: relative; position: relative;
} }
.chat-container .conversations {
transition: width 0.3s ease, opacity 0.3s ease, flex 0.3s ease;
white-space: nowrap; /* 防止文本换行 */
overflow: hidden; /* 确保内容不溢出 */
}
.chat-container .conversations:not(.is-open) {
width: 0;
opacity: 0;
flex: 0 0 0;
}
.chat-container .conversations.is-open {
flex: 1 1 auto; /* 当侧边栏打开时,占据可用空间 */
width: 230px;
opacity: 1;
}
.conversations { .conversations {
width: 230px; /* 初始宽度 */ width: 230px;
max-width: 230px; max-width: 230px;
border-right: 1px solid var(--main-light-3); border-right: 1px solid var(--main-light-3);
max-height: 100%;
background-color: var(--bg-sider); background-color: var(--bg-sider);
transition: all 0.3s ease;
white-space: nowrap; /* 防止文本换行 */
overflow: hidden; /* 确保内容不溢出 */
&.is-open {
width: 230px;
}
&:not(.is-open) {
width: 0;
padding: 0;
overflow: hidden;
}
& .actions { & .actions {
height: var(--header-height); height: var(--header-height);
@ -238,7 +233,6 @@ onMounted(() => {
display: block; display: block;
} }
} }
} }
} }
@ -275,6 +269,12 @@ onMounted(() => {
height: 100%; height: 100%;
border-radius: 0 16px 16px 0; border-radius: 0 16px 16px 0;
box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.05); box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.05);
&:not(.is-open) {
width: 0;
padding: 0;
overflow: hidden;
}
} }
} }
</style> </style>