refactor: replace ant-design components with shadcn/ui and update toast notifications

This commit migrates from Ant Design Vue components to Shadcn/Vue components across multiple files in the web-gold frontend application. Key changes include:

- Replaced ant-design-vue imports with shadcn/ui components (Dialog, Button, Input, etc.)
- Swapped ant-design-vue message/toast system for vue-sonner toast notifications
- Updated icon usage from ant-design icons to lucide icons via @iconify/vue
- Removed unused token refresh logic that was incorrectly implemented client-side
- Applied consistent styling updates to match new component library

The token refresh functionality was removed since it should be handled server-side through axios interceptors rather than client-side intervals.
This commit is contained in:
2026-03-16 02:41:26 +08:00
parent 52c3b5489d
commit 110fe62404
8 changed files with 632 additions and 736 deletions

View File

@@ -12,13 +12,13 @@
<div class="header-content">
<div v-if="selectedConversation" class="header-nav">
<button class="nav-back" @click="backToList">
<LeftOutlined />
<Icon icon="lucide:arrow-left" class="size-4" />
<span>返回列表</span>
</button>
</div>
<div class="header-title-group">
<h2 class="modal-title">
<HistoryOutlined class="title-icon" />
<Icon icon="lucide:history" class="title-icon" />
{{ selectedConversation ? '对话详情' : '历史记录' }}
</h2>
<p v-if="!selectedConversation" class="modal-subtitle">
@@ -27,7 +27,7 @@
</div>
</div>
<button class="close-btn" @click="handleClose">
<CloseOutlined />
<Icon icon="lucide:x" class="size-4" />
</button>
</header>
@@ -35,107 +35,113 @@
<div class="modal-body">
<!-- Conversation List View -->
<div v-if="!selectedConversation" class="list-view">
<a-spin :spinning="loading">
<!-- Empty State -->
<div v-if="groupedConversations.length === 0 && !loading" class="empty-state">
<div class="empty-illustration">
<div class="illustration-ring"></div>
<div class="illustration-ring delay-1"></div>
<HistoryOutlined class="illustration-icon" />
</div>
<h3 class="empty-title">暂无历史记录</h3>
<p class="empty-desc">开始对话后记录将保存在这里</p>
</div>
<!-- Loading State -->
<div v-if="loading" class="loading-state">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-primary" />
</div>
<!-- Grouped Conversations -->
<div v-else class="conversation-groups">
<div
v-for="(group, groupIndex) in groupedConversations"
:key="group.label"
class="conversation-group"
:style="{ '--group-index': groupIndex }"
>
<div class="group-header">
<span class="group-dot"></span>
<span class="group-label">{{ group.label }}</span>
<span class="group-count">{{ group.items.length }}</span>
</div>
<div class="group-items">
<button
v-for="(item, itemIndex) in group.items"
:key="item.id"
class="conversation-item"
:style="{ '--item-index': itemIndex }"
@click="selectConversation(item)"
>
<div class="item-indicator"></div>
<div class="item-content">
<h4 class="item-title">{{ item.name || '未命名会话' }}</h4>
<p class="item-preview">{{ item.preview || '暂无预览内容' }}</p>
<!-- Empty State -->
<div v-else-if="groupedConversations.length === 0" class="empty-state">
<div class="empty-illustration">
<div class="illustration-ring"></div>
<div class="illustration-ring delay-1"></div>
<Icon icon="lucide:history" class="illustration-icon" />
</div>
<h3 class="empty-title">暂无历史记录</h3>
<p class="empty-desc">开始对话后记录将保存在这里</p>
</div>
<!-- Grouped Conversations -->
<div v-else class="conversation-groups">
<div
v-for="(group, groupIndex) in groupedConversations"
:key="group.label"
class="conversation-group"
:style="{ '--group-index': groupIndex }"
>
<div class="group-header">
<span class="group-dot"></span>
<span class="group-label">{{ group.label }}</span>
<span class="group-count">{{ group.items.length }}</span>
</div>
<div class="group-items">
<button
v-for="(item, itemIndex) in group.items"
:key="item.id"
class="conversation-item"
:style="{ '--item-index': itemIndex }"
@click="selectConversation(item)"
>
<div class="item-indicator"></div>
<div class="item-content">
<h4 class="item-title">{{ item.name || '未命名会话' }}</h4>
<p class="item-preview">{{ item.preview || '暂无预览内容' }}</p>
</div>
<div class="item-meta">
<span class="meta-time">
<Icon icon="lucide:clock" class="size-3" />
{{ formatTime(item.updatedAt || item.createdAt) }}
</span>
<div class="meta-arrow">
<Icon icon="lucide:chevron-right" class="size-4" />
</div>
<div class="item-meta">
<span class="meta-time">
<ClockCircleOutlined />
{{ formatTime(item.updatedAt || item.createdAt) }}
</span>
<div class="meta-arrow">
<RightOutlined />
</div>
</div>
</button>
</div>
</div>
</button>
</div>
</div>
</a-spin>
</div>
</div>
<!-- Message Detail View -->
<div v-else class="detail-view">
<a-spin :spinning="messageLoading">
<div v-if="messageList.length === 0 && !messageLoading" class="empty-state">
<CommentOutlined class="empty-icon-single" />
<p>暂无消息记录</p>
</div>
<!-- Loading State -->
<div v-if="messageLoading" class="loading-state">
<Icon icon="lucide:loader-2" class="size-8 animate-spin text-primary" />
</div>
<div v-else class="message-timeline">
<div
v-for="(msg, index) in messageList"
:key="msg.id"
class="timeline-item"
:style="{ '--msg-index': index }"
>
<!-- User Message -->
<div class="message-block message-block--user">
<div class="message-avatar">
<UserOutlined />
</div>
<div class="message-body">
<div class="message-header">
<span class="message-author"></span>
<span class="message-time">{{ formatTime(msg.createdAt) }}</span>
</div>
<div class="message-content">{{ msg.query }}</div>
</div>
<div v-else-if="messageList.length === 0" class="empty-state">
<Icon icon="lucide:message-circle" class="empty-icon-single" />
<p>暂无消息记录</p>
</div>
<div v-else class="message-timeline">
<div
v-for="(msg, index) in messageList"
:key="msg.id"
class="timeline-item"
:style="{ '--msg-index': index }"
>
<!-- User Message -->
<div class="message-block message-block--user">
<div class="message-avatar">
<Icon icon="lucide:user" class="size-4" />
</div>
<div class="message-body">
<div class="message-header">
<span class="message-author"></span>
<span class="message-time">{{ formatTime(msg.createdAt) }}</span>
</div>
<div class="message-content">{{ msg.query }}</div>
</div>
</div>
<!-- AI Response -->
<div class="message-block message-block--ai">
<div class="message-avatar">
<RobotOutlined />
</div>
<div class="message-body">
<div class="message-header">
<span class="message-author">AI 助手</span>
<button class="copy-btn" @click="copyContent(msg.answer)" title="复制">
<CopyOutlined />
</button>
</div>
<div class="message-content">{{ msg.answer }}</div>
<!-- AI Response -->
<div class="message-block message-block--ai">
<div class="message-avatar">
<Icon icon="lucide:bot" class="size-4" />
</div>
<div class="message-body">
<div class="message-header">
<span class="message-author">AI 助手</span>
<button class="copy-btn" @click="copyContent(msg.answer)" title="复制">
<Icon icon="lucide:copy" class="size-3" />
</button>
</div>
<div class="message-content">{{ msg.answer }}</div>
</div>
</div>
</div>
</a-spin>
</div>
</div>
</div>
</div>
@@ -146,18 +152,8 @@
<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 { toast } from 'vue-sonner'
import { Icon } from '@iconify/vue'
import dayjs from 'dayjs'
import { getConversations, getMessages } from '@/api/agent'
import { copyToClipboard } from '@/utils/clipboard'
@@ -253,7 +249,7 @@ const handleClose = () => {
const copyContent = async (content) => {
const success = await copyToClipboard(content)
success ? message.success('已复制到剪贴板') : message.error('复制失败')
success ? toast.success('已复制到剪贴板') : toast.error('复制失败')
}
const formatTime = (timestamp) => {
@@ -444,6 +440,16 @@ watch(() => props.visible, (val) => {
}
}
// ========================================
// Loading State
// ========================================
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 60px 20px;
}
// ========================================
// Empty State
// ========================================