dify历史记录

This commit is contained in:
2026-02-25 18:21:25 +08:00
parent 0efca50be3
commit 2e93211697
13 changed files with 1328 additions and 74 deletions

View File

@@ -85,3 +85,34 @@ export async function sendChatStream(options) {
signal: ctrl ? ctrl.signal : undefined
})
}
/**
* 获取会话列表
* @param {Object} params - 请求参数
* @param {number} params.agentId - 智能体ID
* @param {string} [params.lastId] - 上一页最后一条记录ID
* @param {number} [params.limit] - 返回条数默认20
*/
export function getConversations(params) {
return request({
url: `${BASE_URL}/dify/conversations`,
method: 'get',
params
})
}
/**
* 获取会话历史消息
* @param {Object} params - 请求参数
* @param {number} params.agentId - 智能体ID
* @param {string} params.conversationId - 会话ID
* @param {string} [params.firstId] - 当前页第一条记录ID
* @param {number} [params.limit] - 返回条数默认20
*/
export function getMessages(params) {
return request({
url: `${BASE_URL}/dify/messages`,
method: 'get',
params
})
}

View File

@@ -18,9 +18,14 @@
<span class="agent-category">{{ agent?.categoryName || '通用' }}</span>
</div>
</div>
<button class="close-btn" @click="handleClose">
<CloseOutlined />
</button>
<div class="header-actions">
<button class="history-btn" @click="openHistory" title="历史记录">
<HistoryOutlined />
</button>
<button class="close-btn" @click="handleClose">
<CloseOutlined />
</button>
</div>
</header>
<!-- Main Content -->
@@ -139,13 +144,20 @@
<p class="footer-tip"> Enter 发送Shift + Enter 换行</p>
</footer>
</div>
<!-- History Drawer -->
<HistoryPanel
:visible="historyVisible"
:agent-id="agent?.id"
@close="closeHistory"
/>
</div>
</transition>
</teleport>
</template>
<script setup>
import { ref, watch, nextTick, computed } from 'vue'
import { ref, watch, nextTick } from 'vue'
import {
CloseOutlined,
RobotOutlined,
@@ -154,11 +166,13 @@ import {
ThunderboltFilled,
EditOutlined,
SendOutlined,
LoadingOutlined
LoadingOutlined,
HistoryOutlined
} from '@ant-design/icons-vue'
import { message, Modal } from 'ant-design-vue'
import { sendChatStream } from '@/api/agent'
import { copyToClipboard } from '@/utils/clipboard'
import HistoryPanel from './HistoryPanel.vue'
const props = defineProps({
visible: { type: Boolean, default: false },
@@ -179,6 +193,9 @@ const textareaRef = ref(null)
const resultBodyRef = ref(null)
const abortController = ref(null)
// History State
const historyVisible = ref(false)
// Methods
const handleClose = (force = false) => {
// 正在生成时,不允许关闭
@@ -234,42 +251,10 @@ const handleGenerate = async () => {
isGenerating.value = true
generatedContent.value = ''
inputText.value = ''
if (textareaRef.value) textareaRef.value.style.height = 'auto'
abortController.value = new AbortController()
try {
await sendChatStream({
agentId: props.agent?.id,
content: prompt,
modelMode: modelMode.value,
ctrl: abortController.value,
onMessage: (result) => {
if (result.event === 'message' && result.content) {
generatedContent.value += result.content
scrollToBottom()
} else if (result.event === 'error') {
message.error(result.errorMessage || '生成出错')
}
},
onError: (error) => {
console.error('生成失败:', error)
message.error('生成失败,请重试')
if (!generatedContent.value) isGenerating.value = false
},
onClose: () => {
isGenerating.value = false
abortController.value = null
}
})
emit('send', { agentId: props.agent?.id, content: prompt, modelMode: modelMode.value })
} catch (error) {
if (error.name !== 'AbortError') {
message.error('生成失败,请重试')
}
isGenerating.value = false
}
await executeStreamRequest(prompt)
emit('send', { agentId: props.agent?.id, content: prompt, modelMode: modelMode.value })
}
const handleKeyDown = (e) => {
@@ -281,25 +266,17 @@ const handleKeyDown = (e) => {
const handleCopy = async (content) => {
const success = await copyToClipboard(content)
if (success) {
message.success('已复制')
} else {
message.error('复制失败')
}
success ? message.success('已复制') : message.error('复制失败')
}
const handleRegenerate = async () => {
if (!currentInput.value || isGenerating.value) return
generatedContent.value = ''
isGenerating.value = true
usedProMode.value = modelMode.value === 'pro'
// 执行流式请求
const executeStreamRequest = async (prompt) => {
abortController.value = new AbortController()
try {
await sendChatStream({
agentId: props.agent?.id,
content: currentInput.value,
content: prompt,
modelMode: modelMode.value,
ctrl: abortController.value,
onMessage: (result) => {
@@ -325,6 +302,14 @@ const handleRegenerate = async () => {
}
}
const handleRegenerate = async () => {
if (!currentInput.value || isGenerating.value) return
generatedContent.value = ''
isGenerating.value = true
usedProMode.value = modelMode.value === 'pro'
await executeStreamRequest(currentInput.value)
}
const resetTask = () => {
generatedContent.value = ''
currentInput.value = ''
@@ -336,6 +321,15 @@ const resetTask = () => {
nextTick(() => textareaRef.value?.focus())
}
// History Methods
const openHistory = () => {
historyVisible.value = true
}
const closeHistory = () => {
historyVisible.value = false
}
const scrollToBottom = () => {
nextTick(() => {
if (resultBodyRef.value) {
@@ -886,4 +880,30 @@ watch(() => props.visible, (val) => {
.drawer-fade-leave-to {
opacity: 0;
}
// Header Actions
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.history-btn {
width: 36px;
height: 36px;
border-radius: 10px;
border: none;
background: transparent;
color: @text-light;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: #f1f5f9;
color: @primary;
}
}
</style>

View File

@@ -0,0 +1,595 @@
<template>
<transition name="history-slide">
<div v-if="visible" class="history-panel">
<!-- Header -->
<header class="panel-header">
<div class="header-left">
<button v-if="selectedConversation" class="back-btn" @click="backToList">
<LeftOutlined />
</button>
<h3 class="header-title">{{ selectedConversation ? '对话详情' : '历史记录' }}</h3>
</div>
<button class="close-btn" @click="handleClose">
<CloseOutlined />
</button>
</header>
<!-- Content -->
<div class="panel-content">
<!-- Conversation List -->
<div v-if="!selectedConversation" class="conversation-view">
<a-spin :spinning="loading">
<!-- Empty State -->
<div v-if="groupedConversations.length === 0 && !loading" class="empty-state">
<div class="empty-visual">
<div class="visual-ring"></div>
<HistoryOutlined class="empty-icon" />
</div>
<p class="empty-text">暂无历史记录</p>
<p class="empty-hint">开始对话后记录将保存在这里</p>
</div>
<!-- Grouped List -->
<div v-else class="conversation-groups">
<div
v-for="group in groupedConversations"
:key="group.label"
class="conversation-group"
>
<div class="group-header">
<span class="group-label">{{ group.label }}</span>
<span class="group-count">{{ group.items.length }}</span>
</div>
<div class="group-list">
<button
v-for="item in group.items"
:key="item.id"
class="conversation-card"
@click="selectConversation(item)"
>
<div class="card-main">
<h4 class="card-title">{{ item.name || '未命名会话' }}</h4>
<p class="card-preview">{{ item.preview || '暂无预览' }}</p>
</div>
<div class="card-meta">
<span class="meta-time">
<ClockCircleOutlined />
{{ formatTime(item.updatedAt || item.createdAt) }}
</span>
<RightOutlined class="meta-arrow" />
</div>
</button>
</div>
</div>
</div>
</a-spin>
</div>
<!-- Message Detail -->
<div v-else class="message-view">
<a-spin :spinning="messageLoading">
<div v-if="messageList.length === 0 && !messageLoading" class="empty-state">
<CommentOutlined class="empty-icon" />
<p>暂无消息</p>
</div>
<div v-else class="message-list">
<div v-for="(msg, index) in messageList" :key="msg.id" class="message-item">
<div class="message-bubble message-bubble--user">
<div class="bubble-header">
<UserOutlined class="bubble-avatar" />
<span class="bubble-label">你的问题</span>
<span class="bubble-time">{{ formatTime(msg.createdAt) }}</span>
</div>
<p class="bubble-content">{{ msg.query }}</p>
</div>
<div class="message-bubble message-bubble--ai">
<div class="bubble-header">
<RobotOutlined class="bubble-avatar" />
<span class="bubble-label">AI 回答</span>
<button class="bubble-copy" @click="copyContent(msg.answer)">
<CopyOutlined />
</button>
</div>
<p class="bubble-content">{{ msg.answer }}</p>
</div>
</div>
</div>
</a-spin>
</div>
</div>
</div>
</transition>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import {
CloseOutlined,
LeftOutlined,
RightOutlined,
HistoryOutlined,
ClockCircleOutlined,
CopyOutlined,
UserOutlined,
RobotOutlined,
CommentOutlined
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
import { getConversations, getMessages } from '@/api/agent'
import { copyToClipboard } from '@/utils/clipboard'
const props = defineProps({
visible: { type: Boolean, default: false },
agentId: { type: [String, Number], default: null }
})
const emit = defineEmits(['close'])
// State
const loading = ref(false)
const conversationList = ref([])
const selectedConversation = ref(null)
const messageList = ref([])
const messageLoading = ref(false)
// 按日期分组
const groupedConversations = computed(() => {
const today = dayjs().startOf('day')
const yesterday = dayjs().subtract(1, 'day').startOf('day')
const week = dayjs().subtract(7, 'day').startOf('day')
const groups = {
today: { label: '今天', items: [] },
yesterday: { label: '昨天', items: [] },
week: { label: '近7天', items: [] },
older: { label: '更早', items: [] }
}
conversationList.value.forEach(item => {
const date = dayjs((item.updatedAt || item.createdAt) * 1000)
if (date.isAfter(today)) {
groups.today.items.push(item)
} else if (date.isAfter(yesterday)) {
groups.yesterday.items.push(item)
} else if (date.isAfter(week)) {
groups.week.items.push(item)
} else {
groups.older.items.push(item)
}
})
return Object.values(groups).filter(g => g.items.length > 0)
})
// Methods
const loadConversations = async () => {
if (!props.agentId) return
loading.value = true
try {
const res = await getConversations({ agentId: props.agentId, limit: 50 })
if (res.code === 0) {
conversationList.value = res.data?.data || []
}
} catch (e) {
console.error('加载会话列表失败:', e)
} finally {
loading.value = false
}
}
const selectConversation = async (conversation) => {
selectedConversation.value = conversation
messageLoading.value = true
messageList.value = []
try {
const res = await getMessages({
agentId: props.agentId,
conversationId: conversation.id,
limit: 50
})
if (res.code === 0) {
messageList.value = (res.data?.data || []).reverse()
}
} catch (e) {
console.error('加载消息详情失败:', e)
} finally {
messageLoading.value = false
}
}
const backToList = () => {
selectedConversation.value = null
messageList.value = []
}
const handleClose = () => {
emit('close')
backToList()
}
const copyContent = async (content) => {
const success = await copyToClipboard(content)
success ? message.success('已复制') : message.error('复制失败')
}
const formatTime = (timestamp) => {
if (!timestamp) return ''
const date = dayjs(timestamp * 1000)
const now = dayjs()
if (date.isSame(now, 'day')) {
return date.format('HH:mm')
} else if (date.isSame(now.subtract(1, 'day'), 'day')) {
return '昨天 ' + date.format('HH:mm')
} else if (date.isAfter(now.subtract(7, 'day'))) {
return date.format('dddd HH:mm')
} else {
return date.format('MM-DD HH:mm')
}
}
// Watch
watch(() => props.visible, (val) => {
if (val) {
backToList()
loadConversations()
}
})
</script>
<style scoped lang="less">
@primary: #6366f1;
@primary-light: rgba(99, 102, 241, 0.1);
@surface: #ffffff;
@bg: #f8fafc;
@text: #1e293b;
@text-light: #64748b;
@text-muted: #94a3b8;
@border: #e2e8f0;
@radius: 12px;
.history-panel {
position: absolute;
inset: 0;
background: @bg;
z-index: 20;
display: flex;
flex-direction: column;
}
// Header
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12px);
border-bottom: 1px solid @border;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.back-btn {
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
background: transparent;
color: @text-light;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: #f1f5f9;
color: @text;
}
}
.header-title {
font-size: 15px;
font-weight: 600;
color: @text;
margin: 0;
}
.close-btn {
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
background: transparent;
color: @text-light;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: #fee2e2;
color: #ef4444;
}
}
// Content
.panel-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
// Empty State
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
}
.empty-visual {
position: relative;
width: 80px;
height: 80px;
margin-bottom: 24px;
}
.visual-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px dashed @border;
animation: ringRotate 20s linear infinite;
}
@keyframes ringRotate {
to { transform: rotate(360deg); }
}
.empty-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 32px;
color: @text-muted;
}
.empty-text {
font-size: 15px;
font-weight: 500;
color: @text;
margin: 0 0 6px;
}
.empty-hint {
font-size: 13px;
color: @text-muted;
margin: 0;
}
// Conversation Groups
.conversation-groups {
display: flex;
flex-direction: column;
gap: 20px;
}
.conversation-group {
// Group styling
}
.group-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding: 0 4px;
}
.group-label {
font-size: 12px;
font-weight: 600;
color: @text-light;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.group-count {
font-size: 11px;
font-weight: 500;
color: @text-muted;
background: #f1f5f9;
padding: 2px 8px;
border-radius: 10px;
}
.group-list {
display: flex;
flex-direction: column;
gap: 8px;
}
// Conversation Card
.conversation-card {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: @surface;
border: 1px solid @border;
border-radius: @radius;
cursor: pointer;
transition: all 0.2s;
text-align: left;
&:hover {
border-color: @primary;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1);
transform: translateY(-1px);
.meta-arrow {
color: @primary;
transform: translateX(2px);
}
}
}
.card-main {
flex: 1;
min-width: 0;
}
.card-title {
font-size: 14px;
font-weight: 600;
color: @text;
margin: 0 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-preview {
font-size: 12px;
color: @text-muted;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
flex-shrink: 0;
margin-left: 12px;
}
.meta-time {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: @text-muted;
}
.meta-arrow {
font-size: 10px;
color: @text-muted;
transition: all 0.2s;
}
// Message View
.message-view {
// Message view container
}
.message-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.message-item {
display: flex;
flex-direction: column;
gap: 12px;
}
.message-bubble {
border-radius: @radius;
overflow: hidden;
&--user {
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
border: 1px solid @border;
}
&--ai {
background: linear-gradient(135deg, @primary-light, rgba(139, 92, 246, 0.08));
border: 1px solid rgba(99, 102, 241, 0.2);
}
}
.bubble-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.bubble-avatar {
font-size: 14px;
color: @text-light;
}
.bubble-label {
font-size: 11px;
font-weight: 600;
color: @text-light;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.bubble-time {
font-size: 10px;
color: @text-muted;
margin-left: auto;
}
.bubble-copy {
margin-left: auto;
width: 24px;
height: 24px;
border: none;
background: transparent;
color: @text-muted;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&:hover {
background: rgba(99, 102, 241, 0.1);
color: @primary;
}
}
.bubble-content {
margin: 0;
padding: 12px 14px;
font-size: 13px;
line-height: 1.7;
color: @text;
white-space: pre-wrap;
word-break: break-word;
}
// Transition
.history-slide-enter-active,
.history-slide-leave-active {
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}
.history-slide-enter-from,
.history-slide-leave-to {
transform: translateX(100%);
}
</style>

View File

@@ -19,7 +19,6 @@
type="text"
placeholder="搜索智能体..."
class="search-input"
@keydown.enter="handleSearch"
/>
<transition name="fade">
<button v-if="searchKeyword" class="search-clear" @click="clearSearch">
@@ -34,7 +33,7 @@
<div class="category-scroll" ref="categoryScrollRef">
<div class="category-track">
<button
v-for="category in visibleCategories"
v-for="category in categories"
:key="category.id"
:data-category-id="category.id"
class="category-chip"
@@ -46,10 +45,54 @@
</button>
</div>
</div>
<!-- 展开按钮 -->
<button
ref="expandTriggerRef"
class="expand-trigger"
:class="{ 'expand-trigger--active': showCategoryPanel }"
@click="toggleCategoryPanel"
>
<span class="expand-icon">
<AppstoreOutlined />
</span>
</button>
</div>
</div>
</header>
<!-- 分类展开面板 - Teleport body -->
<Teleport to="body">
<!-- 遮罩层 -->
<transition name="mask">
<div v-if="showCategoryPanel" class="category-mask" @click="showCategoryPanel = false"></div>
</transition>
<!-- 面板 -->
<transition name="panel">
<div v-if="showCategoryPanel" class="category-panel" :style="panelStyle">
<div class="panel-header">
<span class="panel-title">快速选择</span>
<button class="panel-close" @click="showCategoryPanel = false">
<CloseOutlined />
</button>
</div>
<div class="panel-grid">
<button
v-for="category in categories"
:key="category.id"
class="panel-chip"
:class="{ 'panel-chip--active': activeCategory === category.id }"
@click="handleCategoryFromPanel(category.id)"
>
<span class="panel-chip-name">{{ category.name }}</span>
<span class="panel-chip-count">{{ category.count }}</span>
</button>
</div>
</div>
</transition>
</Teleport>
<!-- 主内容区域 -->
<main class="agents-main">
<a-spin :spinning="loading" class="loading-spinner">
@@ -128,7 +171,8 @@ import {
RobotOutlined,
CloseOutlined,
ArrowRightOutlined,
MessageOutlined
MessageOutlined,
AppstoreOutlined
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue'
@@ -142,6 +186,14 @@ const searchKeyword = ref('')
const chatDrawerVisible = ref(false)
const currentAgent = ref(null)
const categoryScrollRef = ref(null)
const expandTriggerRef = ref(null)
const showCategoryPanel = ref(false)
const panelTop = ref(0)
// 面板样式
const panelStyle = computed(() => ({
top: `${panelTop.value}px`
}))
// 智能体列表数据
const agentList = ref([])
@@ -166,9 +218,6 @@ const categories = computed(() => {
return cats
})
// 可见分类(用于展示)
const visibleCategories = computed(() => categories.value)
// 过滤后的列表
const filteredAgentList = computed(() => {
let list = agentList.value
@@ -242,8 +291,18 @@ const handleCategoryChange = (categoryId) => {
scrollCategoryIntoView(categoryId)
}
const handleSearch = () => {
// 搜索逻辑通过 computed 自动处理
const toggleCategoryPanel = () => {
if (!showCategoryPanel.value && expandTriggerRef.value) {
const rect = expandTriggerRef.value.getBoundingClientRect()
panelTop.value = rect.bottom + 8
}
showCategoryPanel.value = !showCategoryPanel.value
}
const handleCategoryFromPanel = (categoryId) => {
activeCategory.value = categoryId
scrollCategoryIntoView(categoryId)
showCategoryPanel.value = false
}
const clearSearch = () => {
@@ -334,6 +393,7 @@ onMounted(() => {
padding: 48px 0 32px;
background: var(--surface-primary);
border-bottom: 1px solid var(--border-light);
z-index: 10;
}
.header-decoration {
@@ -456,6 +516,9 @@ onMounted(() => {
.category-filter {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
position: relative;
}
.category-scroll {
@@ -832,4 +895,206 @@ onMounted(() => {
.fade-leave-to {
opacity: 0;
}
// ============================================
// 展开按钮
// ============================================
.expand-trigger {
flex-shrink: 0;
width: 34px;
height: 34px;
border: 1px solid var(--border-subtle);
border-radius: 10px;
background: var(--surface-elevated);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-tertiary);
transition: all var(--transition-fast);
&:hover {
border-color: var(--text-tertiary);
color: var(--text-secondary);
background: var(--surface-muted);
}
&--active {
background: var(--text-primary);
border-color: var(--text-primary);
color: white;
}
}
.expand-icon {
font-size: 14px;
}
</style>
<!-- 全局样式 - 用于 Teleport body 的元素 -->
<style lang="less">
.category-panel {
position: fixed;
left: 50%;
transform: translateX(-50%);
width: 320px;
max-width: calc(100vw - 48px);
background: #FFFFFF;
border-radius: 14px;
box-shadow: 0 12px 32px rgba(0,0,0,0.12), 0 4px 12px rgba(0,0,0,0.08);
border: 1px solid #F0F0F0;
z-index: 1001;
overflow: hidden;
}
.category-panel .panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid #F0F0F0;
}
.category-panel .panel-title {
font-size: 13px;
font-weight: 600;
color: #171717;
}
.category-panel .panel-close {
width: 24px;
height: 24px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #A3A3A3;
font-size: 10px;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.category-panel .panel-close:hover {
background: #F5F5F5;
color: #525252;
}
.category-panel .panel-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
padding: 12px;
max-height: 280px;
overflow-y: auto;
}
.category-panel .panel-grid::-webkit-scrollbar {
width: 4px;
}
.category-panel .panel-grid::-webkit-scrollbar-thumb {
background: #E5E5E5;
border-radius: 2px;
}
.category-panel .panel-chip {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border: 1px solid #F0F0F0;
border-radius: 10px;
background: #FFFFFF;
cursor: pointer;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.category-panel .panel-chip:hover:not(.panel-chip--active) {
border-color: #E5E5E5;
background: #F5F5F5;
}
.category-panel .panel-chip--active {
background: #171717;
border-color: #171717;
}
.category-panel .panel-chip--active .panel-chip-name,
.category-panel .panel-chip--active .panel-chip-count {
color: white;
}
.category-panel .panel-chip--active .panel-chip-count {
background: rgba(255,255,255,0.2);
}
.category-panel .panel-chip-name {
font-size: 13px;
font-weight: 500;
color: #171717;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.category-panel .panel-chip-count {
font-size: 11px;
font-weight: 600;
padding: 2px 6px;
border-radius: 6px;
background: #F5F5F5;
color: #525252;
flex-shrink: 0;
margin-left: 8px;
}
.category-mask {
position: fixed;
inset: 0;
z-index: 1000;
background: transparent;
}
// 面板动画 - 全局
.panel-enter-active {
animation: panelIn 0.2s ease-out;
}
.panel-leave-active {
animation: panelOut 0.15s ease-in;
}
@keyframes panelIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(-8px) scale(0.96);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
}
@keyframes panelOut {
from {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateX(-50%) translateY(-8px) scale(0.96);
}
}
.mask-enter-active,
.mask-leave-active {
transition: opacity 0.15s ease;
}
.mask-enter-from,
.mask-leave-to {
opacity: 0;
}
</style>

View File

@@ -292,7 +292,10 @@ const handleFileSelectWrapper = (e: Event) => {
}
onMounted(async () => {
await voiceStore.refresh()
await Promise.all([
voiceStore.refresh(),
userStore.fetchUserProfile()
])
})
</script>