feat: 新增对话置顶功能,优化对话列表排序和更新逻辑
This commit is contained in:
parent
230f69bf16
commit
b420f7f69c
@ -661,6 +661,7 @@ class ThreadResponse(BaseModel):
|
|||||||
user_id: str
|
user_id: str
|
||||||
agent_id: str
|
agent_id: str
|
||||||
title: str | None = None
|
title: str | None = None
|
||||||
|
is_pinned: bool = False
|
||||||
created_at: str
|
created_at: str
|
||||||
updated_at: str
|
updated_at: str
|
||||||
|
|
||||||
@ -728,6 +729,7 @@ async def delete_thread(
|
|||||||
|
|
||||||
class ThreadUpdate(BaseModel):
|
class ThreadUpdate(BaseModel):
|
||||||
title: str | None = None
|
title: str | None = None
|
||||||
|
is_pinned: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
@chat.put("/thread/{thread_id}", response_model=ThreadResponse)
|
@chat.put("/thread/{thread_id}", response_model=ThreadResponse)
|
||||||
@ -741,6 +743,7 @@ async def update_thread(
|
|||||||
return await update_thread_view(
|
return await update_thread_view(
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
title=thread_update.title,
|
title=thread_update.title,
|
||||||
|
is_pinned=thread_update.is_pinned,
|
||||||
db=db,
|
db=db,
|
||||||
current_user_id=str(current_user.id),
|
current_user_id=str(current_user.id),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -203,20 +203,56 @@ class ConversationRepository:
|
|||||||
limit: int | None = None,
|
limit: int | None = None,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[Conversation]:
|
) -> list[Conversation]:
|
||||||
query = select(Conversation).where(Conversation.status == status)
|
"""List conversations with pinned conversations always included first.
|
||||||
|
|
||||||
|
The limit applies only to non-pinned conversations to ensure pinned
|
||||||
|
conversations are always visible in the list.
|
||||||
|
"""
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
|
base_conditions = [Conversation.status == status]
|
||||||
if user_id:
|
if user_id:
|
||||||
query = query.where(Conversation.user_id == str(user_id))
|
base_conditions.append(Conversation.user_id == str(user_id))
|
||||||
if agent_id:
|
if agent_id:
|
||||||
query = query.where(Conversation.agent_id == agent_id)
|
base_conditions.append(Conversation.agent_id == agent_id)
|
||||||
|
|
||||||
query = query.order_by(Conversation.updated_at.desc())
|
# First, get all pinned conversations (no limit)
|
||||||
|
pinned_query = (
|
||||||
|
select(Conversation)
|
||||||
|
.where(*base_conditions)
|
||||||
|
.where(Conversation.is_pinned == True)
|
||||||
|
.order_by(Conversation.updated_at.desc())
|
||||||
|
)
|
||||||
|
result = await self.db.execute(pinned_query)
|
||||||
|
pinned_conversations = list(result.scalars().all())
|
||||||
|
|
||||||
if limit:
|
# Then, get non-pinned conversations with limit/offset
|
||||||
query = query.limit(limit).offset(offset)
|
remaining_limit = None
|
||||||
|
remaining_offset = offset
|
||||||
|
|
||||||
result = await self.db.execute(query)
|
if limit is not None:
|
||||||
return list(result.scalars().all())
|
# Calculate how many slots are taken by pinned conversations
|
||||||
|
pinned_count = len(pinned_conversations)
|
||||||
|
if pinned_count >= limit:
|
||||||
|
# All slots taken by pinned conversations
|
||||||
|
return pinned_conversations[:limit]
|
||||||
|
remaining_limit = limit - pinned_count
|
||||||
|
|
||||||
|
if remaining_limit is not None and remaining_limit > 0:
|
||||||
|
non_pinned_query = (
|
||||||
|
select(Conversation)
|
||||||
|
.where(*base_conditions)
|
||||||
|
.where(Conversation.is_pinned == False)
|
||||||
|
.order_by(Conversation.updated_at.desc())
|
||||||
|
.limit(remaining_limit)
|
||||||
|
.offset(remaining_offset)
|
||||||
|
)
|
||||||
|
result = await self.db.execute(non_pinned_query)
|
||||||
|
non_pinned_conversations = list(result.scalars().all())
|
||||||
|
else:
|
||||||
|
non_pinned_conversations = []
|
||||||
|
|
||||||
|
return pinned_conversations + non_pinned_conversations
|
||||||
|
|
||||||
async def update_conversation(
|
async def update_conversation(
|
||||||
self,
|
self,
|
||||||
@ -224,6 +260,7 @@ class ConversationRepository:
|
|||||||
title: str | None = None,
|
title: str | None = None,
|
||||||
status: str | None = None,
|
status: str | None = None,
|
||||||
metadata: dict | None = None,
|
metadata: dict | None = None,
|
||||||
|
is_pinned: bool | None = None,
|
||||||
) -> Conversation | None:
|
) -> Conversation | None:
|
||||||
conversation = await self.get_conversation_by_thread_id(thread_id)
|
conversation = await self.get_conversation_by_thread_id(thread_id)
|
||||||
if not conversation:
|
if not conversation:
|
||||||
@ -234,6 +271,8 @@ class ConversationRepository:
|
|||||||
conversation.title = normalized_title
|
conversation.title = normalized_title
|
||||||
if status is not None:
|
if status is not None:
|
||||||
conversation.status = status
|
conversation.status = status
|
||||||
|
if is_pinned is not None:
|
||||||
|
conversation.is_pinned = is_pinned
|
||||||
|
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
current_metadata = conversation.extra_metadata or {}
|
current_metadata = conversation.extra_metadata or {}
|
||||||
|
|||||||
@ -177,6 +177,7 @@ async def list_threads_view(
|
|||||||
"user_id": conv.user_id,
|
"user_id": conv.user_id,
|
||||||
"agent_id": conv.agent_id,
|
"agent_id": conv.agent_id,
|
||||||
"title": conv.title,
|
"title": conv.title,
|
||||||
|
"is_pinned": bool(conv.is_pinned),
|
||||||
"created_at": conv.created_at.isoformat(),
|
"created_at": conv.created_at.isoformat(),
|
||||||
"updated_at": conv.updated_at.isoformat(),
|
"updated_at": conv.updated_at.isoformat(),
|
||||||
}
|
}
|
||||||
@ -201,13 +202,14 @@ async def delete_thread_view(
|
|||||||
async def update_thread_view(
|
async def update_thread_view(
|
||||||
*,
|
*,
|
||||||
thread_id: str,
|
thread_id: str,
|
||||||
title: str | None,
|
title: str | None = None,
|
||||||
|
is_pinned: bool | None = None,
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
current_user_id: str,
|
current_user_id: str,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
conv_repo = ConversationRepository(db)
|
conv_repo = ConversationRepository(db)
|
||||||
await require_user_conversation(conv_repo, thread_id, str(current_user_id))
|
await require_user_conversation(conv_repo, thread_id, str(current_user_id))
|
||||||
updated_conv = await conv_repo.update_conversation(thread_id, title=title)
|
updated_conv = await conv_repo.update_conversation(thread_id, title=title, is_pinned=is_pinned)
|
||||||
if not updated_conv:
|
if not updated_conv:
|
||||||
raise HTTPException(status_code=500, detail="更新失败")
|
raise HTTPException(status_code=500, detail="更新失败")
|
||||||
return {
|
return {
|
||||||
@ -215,6 +217,7 @@ async def update_thread_view(
|
|||||||
"user_id": updated_conv.user_id,
|
"user_id": updated_conv.user_id,
|
||||||
"agent_id": updated_conv.agent_id,
|
"agent_id": updated_conv.agent_id,
|
||||||
"title": updated_conv.title,
|
"title": updated_conv.title,
|
||||||
|
"is_pinned": bool(updated_conv.is_pinned),
|
||||||
"created_at": updated_conv.created_at.isoformat(),
|
"created_at": updated_conv.created_at.isoformat(),
|
||||||
"updated_at": updated_conv.updated_at.isoformat(),
|
"updated_at": updated_conv.updated_at.isoformat(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -217,6 +217,7 @@ class Conversation(Base):
|
|||||||
agent_id = Column(String(64), index=True, nullable=False, comment="Agent ID")
|
agent_id = Column(String(64), index=True, nullable=False, comment="Agent ID")
|
||||||
title = Column(String(255), nullable=True, comment="Conversation title")
|
title = Column(String(255), nullable=True, comment="Conversation title")
|
||||||
status = Column(String(20), default="active", comment="Status: active/archived/deleted")
|
status = Column(String(20), default="active", comment="Status: active/archived/deleted")
|
||||||
|
is_pinned = Column(Boolean, default=False, nullable=False, index=True, comment="Is pinned to top")
|
||||||
created_at = Column(DateTime, default=utc_now_naive, comment="Creation time")
|
created_at = Column(DateTime, default=utc_now_naive, comment="Creation time")
|
||||||
updated_at = Column(DateTime, default=utc_now_naive, onupdate=utc_now_naive, comment="Update time")
|
updated_at = Column(DateTime, default=utc_now_naive, onupdate=utc_now_naive, comment="Update time")
|
||||||
extra_metadata = Column(JSON, nullable=True, comment="Additional metadata")
|
extra_metadata = Column(JSON, nullable=True, comment="Additional metadata")
|
||||||
@ -235,6 +236,7 @@ class Conversation(Base):
|
|||||||
"agent_id": self.agent_id,
|
"agent_id": self.agent_id,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"status": self.status,
|
"status": self.status,
|
||||||
|
"is_pinned": bool(self.is_pinned),
|
||||||
"created_at": format_utc_datetime(self.created_at),
|
"created_at": format_utc_datetime(self.created_at),
|
||||||
"updated_at": format_utc_datetime(self.updated_at),
|
"updated_at": format_utc_datetime(self.updated_at),
|
||||||
"metadata": self.extra_metadata or {},
|
"metadata": self.extra_metadata or {},
|
||||||
|
|||||||
@ -313,13 +313,13 @@ export const threadApi = {
|
|||||||
* 更新对话线程
|
* 更新对话线程
|
||||||
* @param {string} threadId - 对话线程ID
|
* @param {string} threadId - 对话线程ID
|
||||||
* @param {string} title - 对话标题
|
* @param {string} title - 对话标题
|
||||||
* @param {string} description - 对话描述
|
* @param {boolean} is_pinned - 是否置顶
|
||||||
* @returns {Promise} - 更新结果
|
* @returns {Promise} - 更新结果
|
||||||
*/
|
*/
|
||||||
updateThread: (threadId, title, description) =>
|
updateThread: (threadId, title, is_pinned) =>
|
||||||
apiPut(`/api/chat/thread/${threadId}`, {
|
apiPut(`/api/chat/thread/${threadId}`, {
|
||||||
title,
|
title,
|
||||||
description
|
is_pinned
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
@select-chat="selectChat"
|
@select-chat="selectChat"
|
||||||
@delete-chat="deleteChat"
|
@delete-chat="deleteChat"
|
||||||
@rename-chat="renameChat"
|
@rename-chat="renameChat"
|
||||||
|
@toggle-pin="togglePinChat"
|
||||||
@toggle-sidebar="toggleSidebar"
|
@toggle-sidebar="toggleSidebar"
|
||||||
@open-agent-modal="openAgentModal"
|
@open-agent-modal="openAgentModal"
|
||||||
@load-more-chats="loadMoreChats"
|
@load-more-chats="loadMoreChats"
|
||||||
@ -618,8 +619,12 @@ const loadMoreChats = async () => {
|
|||||||
const offset = threads.value.length
|
const offset = threads.value.length
|
||||||
const fetchedThreads = await threadApi.getThreads(targetAgentId, 100, offset)
|
const fetchedThreads = await threadApi.getThreads(targetAgentId, 100, offset)
|
||||||
if (fetchedThreads && fetchedThreads.length > 0) {
|
if (fetchedThreads && fetchedThreads.length > 0) {
|
||||||
threads.value = [...threads.value, ...fetchedThreads]
|
// 去除重复的置顶对话(后端每次都返回所有置顶对话)
|
||||||
hasMoreChats.value = fetchedThreads.length >= 100
|
const existingIds = new Set(threads.value.map((t) => t.id))
|
||||||
|
const newThreads = fetchedThreads.filter((t) => !existingIds.has(t.id))
|
||||||
|
|
||||||
|
threads.value = [...threads.value, ...newThreads]
|
||||||
|
hasMoreChats.value = newThreads.length >= 100
|
||||||
} else {
|
} else {
|
||||||
hasMoreChats.value = false
|
hasMoreChats.value = false
|
||||||
}
|
}
|
||||||
@ -675,25 +680,43 @@ const deleteThread = async (threadId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新线程标题
|
// 更新线程标题
|
||||||
const updateThread = async (threadId, title) => {
|
const updateThread = async (threadId, title, is_pinned) => {
|
||||||
if (!threadId || !title) return
|
if (!threadId) return
|
||||||
|
|
||||||
const normalizedTitle = String(title).replace(/\s+/g, ' ').trim().slice(0, 255)
|
if (title) {
|
||||||
if (!normalizedTitle) return
|
const normalizedTitle = String(title).replace(/\s+/g, ' ').trim().slice(0, 255)
|
||||||
|
if (!normalizedTitle) return
|
||||||
|
|
||||||
chatState.isRenamingThread = true
|
chatState.isRenamingThread = true
|
||||||
try {
|
try {
|
||||||
await threadApi.updateThread(threadId, normalizedTitle)
|
await threadApi.updateThread(threadId, normalizedTitle, is_pinned)
|
||||||
const thread = threads.value.find((t) => t.id === threadId)
|
const thread = threads.value.find((t) => t.id === threadId)
|
||||||
if (thread) {
|
if (thread) {
|
||||||
thread.title = normalizedTitle
|
thread.title = normalizedTitle
|
||||||
|
if (is_pinned !== undefined) {
|
||||||
|
thread.is_pinned = is_pinned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update thread:', error)
|
||||||
|
handleChatError(error, 'update')
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
chatState.isRenamingThread = false
|
||||||
|
}
|
||||||
|
} else if (is_pinned !== undefined) {
|
||||||
|
// 只更新置顶状态
|
||||||
|
try {
|
||||||
|
await threadApi.updateThread(threadId, null, is_pinned)
|
||||||
|
const thread = threads.value.find((t) => t.id === threadId)
|
||||||
|
if (thread) {
|
||||||
|
thread.is_pinned = is_pinned
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update thread pin status:', error)
|
||||||
|
handleChatError(error, 'update')
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update thread:', error)
|
|
||||||
handleChatError(error, 'update')
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
chatState.isRenamingThread = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1372,6 +1395,27 @@ const renameChat = async (data) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const togglePinChat = async (chatId) => {
|
||||||
|
const chat = chatsList.value.find((c) => c.id === chatId)
|
||||||
|
if (!chat) return
|
||||||
|
try {
|
||||||
|
// 保存当前选中的对话ID
|
||||||
|
const prevChatId = currentChatId.value
|
||||||
|
|
||||||
|
await updateThread(chatId, null, !chat.is_pinned)
|
||||||
|
|
||||||
|
// 刷新对话列表
|
||||||
|
await loadChatsList()
|
||||||
|
|
||||||
|
// 恢复当前选中的对话
|
||||||
|
if (prevChatId) {
|
||||||
|
chatState.currentThreadId = prevChatId
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleChatError(error, 'pin')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSendMessage = async ({ image } = {}) => {
|
const handleSendMessage = async ({ image } = {}) => {
|
||||||
const text = userInput.value.trim()
|
const text = userInput.value.trim()
|
||||||
if ((!text && !image) || !currentAgent.value || isProcessing.value) return
|
if ((!text && !image) || !currentAgent.value || isProcessing.value) return
|
||||||
|
|||||||
@ -85,7 +85,9 @@
|
|||||||
<!-- 文件内容 Modal -->
|
<!-- 文件内容 Modal -->
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:open="modalVisible"
|
v-model:open="modalVisible"
|
||||||
width="80%"
|
width="800px"
|
||||||
|
:style="{ maxWidth: '90vw', top: '5vh' }"
|
||||||
|
:bodyStyle="{ maxHeight: '90vh', overflow: 'auto' }"
|
||||||
:footer="null"
|
:footer="null"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
@cancel="closeModal"
|
@cancel="closeModal"
|
||||||
|
|||||||
@ -36,31 +36,42 @@
|
|||||||
class="conversation-item"
|
class="conversation-item"
|
||||||
:class="{ active: currentChatId === chat.id }"
|
:class="{ active: currentChatId === chat.id }"
|
||||||
@click="selectChat(chat)"
|
@click="selectChat(chat)"
|
||||||
|
@click.middle="handleMiddleClickDelete(chat.id)"
|
||||||
>
|
>
|
||||||
<div class="conversation-title">{{ chat.title || '新的对话' }}</div>
|
<div class="conversation-title">
|
||||||
|
<Pin v-if="chat.is_pinned" :size="14" class="pin-icon" />
|
||||||
|
<span>{{ chat.title || '新的对话' }}</span>
|
||||||
|
</div>
|
||||||
<div class="actions-mask"></div>
|
<div class="actions-mask"></div>
|
||||||
<div class="conversation-actions">
|
<div class="conversation-actions">
|
||||||
<a-dropdown :trigger="['click']" @click.stop>
|
<a-dropdown :trigger="['click']" @click.stop>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu>
|
<a-menu>
|
||||||
|
<a-menu-item
|
||||||
|
key="pin"
|
||||||
|
@click.stop="togglePin(chat.id)"
|
||||||
|
:icon="h(chat.is_pinned ? PinOff : Pin, { size: 14 })"
|
||||||
|
>
|
||||||
|
{{ chat.is_pinned ? '取消置顶' : '置顶' }}
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-item
|
<a-menu-item
|
||||||
key="rename"
|
key="rename"
|
||||||
@click.stop="renameChat(chat.id)"
|
@click.stop="renameChat(chat.id)"
|
||||||
:icon="h(EditOutlined)"
|
:icon="h(Pencil, { size: 14 })"
|
||||||
>
|
>
|
||||||
重命名
|
重命名
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item
|
<a-menu-item
|
||||||
key="delete"
|
key="delete"
|
||||||
@click.stop="deleteChat(chat.id)"
|
@click.stop="deleteChat(chat.id)"
|
||||||
:icon="h(DeleteOutlined)"
|
:icon="h(Trash2, { size: 14 })"
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</template>
|
</template>
|
||||||
<a-button type="text" class="more-btn" @click.stop>
|
<a-button type="text" class="more-btn" @click.stop>
|
||||||
<MoreOutlined />
|
<MoreVertical :size="16" />
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</div>
|
</div>
|
||||||
@ -84,9 +95,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, h, ref } from 'vue'
|
import { computed, h, ref } from 'vue'
|
||||||
import { DeleteOutlined, EditOutlined, MoreOutlined } from '@ant-design/icons-vue'
|
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import { PanelLeftClose, MessageSquarePlus, LoaderCircle } from 'lucide-vue-next'
|
import { PanelLeftClose, MessageSquarePlus, LoaderCircle, Pin, PinOff, Pencil, Trash2, MoreVertical } from 'lucide-vue-next'
|
||||||
import dayjs, { parseToShanghai } from '@/utils/time'
|
import dayjs, { parseToShanghai } from '@/utils/time'
|
||||||
import { useChatUIStore } from '@/stores/chatUI'
|
import { useChatUIStore } from '@/stores/chatUI'
|
||||||
import { useInfoStore } from '@/stores/info'
|
import { useInfoStore } from '@/stores/info'
|
||||||
@ -142,14 +152,19 @@ const emit = defineEmits([
|
|||||||
'select-chat',
|
'select-chat',
|
||||||
'delete-chat',
|
'delete-chat',
|
||||||
'rename-chat',
|
'rename-chat',
|
||||||
|
'toggle-pin',
|
||||||
'toggle-sidebar',
|
'toggle-sidebar',
|
||||||
'open-agent-modal',
|
'open-agent-modal',
|
||||||
'load-more-chats'
|
'load-more-chats'
|
||||||
])
|
])
|
||||||
|
|
||||||
// 按时间倒序排列的对话列表
|
// 按置顶和时间倒序排列的对话列表
|
||||||
const sortedChats = computed(() => {
|
const sortedChats = computed(() => {
|
||||||
return [...props.chatsList].sort((a, b) => {
|
return [...props.chatsList].sort((a, b) => {
|
||||||
|
// 置顶的排在前面
|
||||||
|
if (a.is_pinned !== b.is_pinned) {
|
||||||
|
return a.is_pinned ? -1 : 1
|
||||||
|
}
|
||||||
const dateA = parseToShanghai(b.created_at)
|
const dateA = parseToShanghai(b.created_at)
|
||||||
const dateB = parseToShanghai(a.created_at)
|
const dateB = parseToShanghai(a.created_at)
|
||||||
if (!dateA || !dateB) return 0
|
if (!dateA || !dateB) return 0
|
||||||
@ -169,6 +184,10 @@ const deleteChat = (chatId) => {
|
|||||||
emit('delete-chat', chatId)
|
emit('delete-chat', chatId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMiddleClickDelete = (chatId) => {
|
||||||
|
emit('delete-chat', chatId)
|
||||||
|
}
|
||||||
|
|
||||||
const renameChat = async (chatId) => {
|
const renameChat = async (chatId) => {
|
||||||
try {
|
try {
|
||||||
const chat = props.chatsList.find((c) => c.id === chatId)
|
const chat = props.chatsList.find((c) => c.id === chatId)
|
||||||
@ -215,6 +234,10 @@ const toggleCollapse = () => {
|
|||||||
const handleLoadMore = () => {
|
const handleLoadMore = () => {
|
||||||
emit('load-more-chats')
|
emit('load-more-chats')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const togglePin = (chatId) => {
|
||||||
|
emit('toggle-pin', chatId)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@ -364,6 +387,14 @@ const handleLoadMore = () => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.pin-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--main-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions-mask {
|
.actions-mask {
|
||||||
@ -391,6 +422,9 @@ const handleLoadMore = () => {
|
|||||||
.more-btn {
|
.more-btn {
|
||||||
color: var(--gray-600);
|
color: var(--gray-600);
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--main-500);
|
color: var(--main-500);
|
||||||
|
|||||||
@ -536,9 +536,12 @@ const form = reactive({
|
|||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const filteredServers = computed(() => {
|
const filteredServers = computed(() => {
|
||||||
if (!searchQuery.value) return servers.value
|
const sorted = [...servers.value].sort((a, b) => {
|
||||||
|
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||||
|
})
|
||||||
|
if (!searchQuery.value) return sorted
|
||||||
const q = searchQuery.value.toLowerCase()
|
const q = searchQuery.value.toLowerCase()
|
||||||
return servers.value.filter(
|
return sorted.filter(
|
||||||
(s) => s.name.toLowerCase().includes(q) || (s.description || '').toLowerCase().includes(q)
|
(s) => s.name.toLowerCase().includes(q) || (s.description || '').toLowerCase().includes(q)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -945,10 +948,6 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
&.disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-icon {
|
.server-icon {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user