fix: improve session card favorite UX
This commit is contained in:
@@ -67,16 +67,6 @@
|
|||||||
{{ statusLabel(s.status) }}
|
{{ statusLabel(s.status) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="id-preview mono">会话 ID: <span class="id-highlight">{{ (s.id || '').substring(0, 15) }}...</span></span>
|
<span class="id-preview mono">会话 ID: <span class="id-highlight">{{ (s.id || '').substring(0, 15) }}...</span></span>
|
||||||
<button class="name-btn-card" @click.stop="openNameEditor(s)" title="自定义名称">✏️</button>
|
|
||||||
<button
|
|
||||||
class="fav-btn-card"
|
|
||||||
:class="{ 'is-fav': s.is_favorite }"
|
|
||||||
:disabled="favoritePending"
|
|
||||||
@click.stop="toggleFavorite(s)"
|
|
||||||
:title="s.is_favorite ? '取消收藏' : '收藏'"
|
|
||||||
:aria-label="s.is_favorite ? '取消收藏此会话' : '收藏此会话'"
|
|
||||||
:aria-pressed="String(s.is_favorite)"
|
|
||||||
>{{ s.is_favorite ? '★' : '☆' }}</button>
|
|
||||||
</div>
|
</div>
|
||||||
<h4 class="card-summary">{{ s.custom_name || s.summary || '(无消息)' }}</h4>
|
<h4 class="card-summary">{{ s.custom_name || s.summary || '(无消息)' }}</h4>
|
||||||
<div v-if="s.custom_name" class="card-original-summary">{{ s.summary }}</div>
|
<div v-if="s.custom_name" class="card-original-summary">{{ s.summary }}</div>
|
||||||
@@ -85,21 +75,40 @@
|
|||||||
<span class="path-text">{{ s.cwd || '-' }}</span>
|
<span class="path-text">{{ s.cwd || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-meta">
|
<div class="card-side">
|
||||||
<div class="meta-col">
|
<div class="card-meta">
|
||||||
<span class="meta-col-label">对应版本</span>
|
<div class="meta-col">
|
||||||
<span class="meta-col-val">{{ s.cli_version || s.model || '-' }}</span>
|
<span class="meta-col-label">对应版本</span>
|
||||||
|
<span class="meta-col-val">{{ s.cli_version || s.model || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-col">
|
||||||
|
<span class="meta-col-label">工具调用</span>
|
||||||
|
<span class="meta-col-val calls" :class="callsClass(s.tool_call_count)">{{ s.tool_call_count || 0 }} 次</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-col">
|
||||||
|
<span class="meta-col-label">开始时间</span>
|
||||||
|
<span class="meta-col-val time">{{ formatTimestamp(s.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-col">
|
||||||
|
<button class="detail-btn" @click.stop="goToDetailPage(s)" title="进入详情页">详情 →</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-col">
|
<div class="card-actions">
|
||||||
<span class="meta-col-label">工具调用</span>
|
<button class="action-btn" @click.stop="openNameEditor(s)" title="编辑名称" aria-label="编辑自定义名称">
|
||||||
<span class="meta-col-val calls" :class="callsClass(s.tool_call_count)">{{ s.tool_call_count || 0 }} 次</span>
|
<span class="icon">✏️</span><span class="text">编辑</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="meta-col">
|
<button
|
||||||
<span class="meta-col-label">开始时间</span>
|
class="action-btn fav"
|
||||||
<span class="meta-col-val time">{{ formatTimestamp(s.created_at) }}</span>
|
:class="{ 'is-fav': s.is_favorite }"
|
||||||
</div>
|
:disabled="isFavoritePending(s)"
|
||||||
<div class="meta-col">
|
@click.stop="toggleFavorite(s)"
|
||||||
<button class="detail-btn" @click.stop="goToDetailPage(s)" title="进入详情页">详情 →</button>
|
:title="s.is_favorite ? '取消收藏' : '收藏'"
|
||||||
|
:aria-label="s.is_favorite ? '取消收藏此会话' : '收藏此会话'"
|
||||||
|
:aria-pressed="String(s.is_favorite)"
|
||||||
|
>
|
||||||
|
<span class="icon">{{ s.is_favorite ? '★' : '☆' }}</span>
|
||||||
|
<span class="text">{{ isFavoritePending(s) ? '处理中' : (s.is_favorite ? '已收藏' : '收藏') }}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,7 +146,7 @@
|
|||||||
<button
|
<button
|
||||||
class="fav-btn-drawer"
|
class="fav-btn-drawer"
|
||||||
:class="{ 'is-fav': drawerSession.is_favorite }"
|
:class="{ 'is-fav': drawerSession.is_favorite }"
|
||||||
:disabled="favoritePending"
|
:disabled="isFavoritePending(drawerSession)"
|
||||||
@click.stop="toggleFavorite(drawerSession)"
|
@click.stop="toggleFavorite(drawerSession)"
|
||||||
:title="drawerSession.is_favorite ? '取消收藏' : '收藏'"
|
:title="drawerSession.is_favorite ? '取消收藏' : '收藏'"
|
||||||
:aria-label="drawerSession.is_favorite ? '取消收藏此会话' : '收藏此会话'"
|
:aria-label="drawerSession.is_favorite ? '取消收藏此会话' : '收藏此会话'"
|
||||||
@@ -276,7 +285,18 @@ const statusOpts = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const favoriteOnly = ref(false)
|
const favoriteOnly = ref(false)
|
||||||
const favoritePending = ref(false) // global lock: one favorite mutation at a time
|
const favoritePendingMap = ref({})
|
||||||
|
|
||||||
|
function isFavoritePending(s) {
|
||||||
|
return !!favoritePendingMap.value[`${s.engine}:${s.id}`]
|
||||||
|
}
|
||||||
|
function setFavoritePending(s, val) {
|
||||||
|
if (val) {
|
||||||
|
favoritePendingMap.value[`${s.engine}:${s.id}`] = true
|
||||||
|
} else {
|
||||||
|
delete favoritePendingMap.value[`${s.engine}:${s.id}`]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const engineDist = computed(() => {
|
const engineDist = computed(() => {
|
||||||
const dist = {}
|
const dist = {}
|
||||||
@@ -494,8 +514,8 @@ async function generateDrawerTitle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleFavorite(s) {
|
function toggleFavorite(s) {
|
||||||
if (!s || favoritePending.value) return
|
if (!s || isFavoritePending(s)) return
|
||||||
favoritePending.value = true
|
setFavoritePending(s, true)
|
||||||
|
|
||||||
const action = s.is_favorite ? unfavoriteSession : favoriteSession
|
const action = s.is_favorite ? unfavoriteSession : favoriteSession
|
||||||
action(s.engine, s.id)
|
action(s.engine, s.id)
|
||||||
@@ -508,14 +528,23 @@ function toggleFavorite(s) {
|
|||||||
drawerSession.value.is_favorite = result.is_favorite
|
drawerSession.value.is_favorite = result.is_favorite
|
||||||
drawerSession.value.favorited_at = result.favorited_at
|
drawerSession.value.favorited_at = result.favorited_at
|
||||||
}
|
}
|
||||||
page.value = 1
|
if (favoriteOnly.value && !result.is_favorite) {
|
||||||
return loadSessions()
|
const idx = sessions.value.findIndex(x => x.id === s.id && x.engine === s.engine)
|
||||||
|
if (idx !== -1) {
|
||||||
|
sessions.value.splice(idx, 1)
|
||||||
|
total.value = Math.max(0, total.value - 1)
|
||||||
|
if (displayedSessions.value.length === 0 && page.value > 1) {
|
||||||
|
page.value--
|
||||||
|
return loadSessions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
ElMessage.error('收藏操作失败: ' + (err.message || ''))
|
ElMessage.error('收藏操作失败: ' + (err.message || ''))
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
favoritePending.value = false
|
setFavoritePending(s, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,12 +620,22 @@ onMounted(loadSessions)
|
|||||||
.id-highlight { color: var(--accent-light); }
|
.id-highlight { color: var(--accent-light); }
|
||||||
.name-btn-card { background: none; border: none; font-size: 12px; cursor: pointer; opacity: 0.4; padding: 0 2px; }
|
.name-btn-card { background: none; border: none; font-size: 12px; cursor: pointer; opacity: 0.4; padding: 0 2px; }
|
||||||
.name-btn-card:hover { opacity: 1; }
|
.name-btn-card:hover { opacity: 1; }
|
||||||
.card-summary { font-size: 13px; font-weight: 500; color: var(--text-primary); line-height: 1.4; margin-bottom: 6px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
.card-summary { font-size: 1rem; font-weight: 700; color: var(--text-primary); line-height: 1.4; margin-bottom: 6px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
.card-original-summary { font-size: 11px; color: var(--text-muted); margin-bottom: 6px; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
|
.card-original-summary { font-size: 11px; color: var(--text-muted); margin-bottom: 6px; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
.card-path { display: flex; align-items: center; gap: 4px; font-size: 10px; color: var(--text-muted); }
|
.card-path { display: flex; align-items: center; gap: 4px; font-size: 10px; color: var(--text-muted); }
|
||||||
.path-ico { font-size: 12px; }
|
.path-ico { font-size: 12px; }
|
||||||
.path-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: 'SF Mono', monospace; }
|
.path-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: 'SF Mono', monospace; }
|
||||||
|
|
||||||
|
.card-side { display: flex; align-items: center; gap: 16px; flex-shrink: 0; }
|
||||||
|
.card-actions { display: flex; gap: 8px; flex-shrink: 0; align-items: center; }
|
||||||
|
.action-btn { display: inline-flex; align-items: center; justify-content: center; gap: 4px; min-width: 44px; min-height: 44px; padding: 4px 12px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; background: transparent; transition: all 0.12s; border: 1px solid var(--border); color: var(--text-secondary); }
|
||||||
|
.action-btn:hover { border-color: var(--accent); color: var(--accent-light); }
|
||||||
|
.action-btn:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.action-btn.fav { border-color: var(--border); }
|
||||||
|
.action-btn.fav:hover { border-color: #f59e0b; color: #f59e0b; }
|
||||||
|
.action-btn.fav.is-fav { border-color: #f59e0b; color: #f59e0b; background: rgba(245,158,11,0.1); }
|
||||||
|
.action-btn:disabled { opacity: 0.5; cursor: not-allowed; border-color: var(--border) !important; color: var(--text-muted) !important; background: transparent !important; }
|
||||||
|
|
||||||
.card-meta { display: flex; align-items: center; gap: 16px; flex-shrink: 0; }
|
.card-meta { display: flex; align-items: center; gap: 16px; flex-shrink: 0; }
|
||||||
.meta-col { text-align: center; }
|
.meta-col { text-align: center; }
|
||||||
.meta-col-label { display: block; font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
|
.meta-col-label { display: block; font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
|
||||||
@@ -703,7 +742,9 @@ onMounted(loadSessions)
|
|||||||
.card-main { padding-right: 0; width: 100%; }
|
.card-main { padding-right: 0; width: 100%; }
|
||||||
.card-tags { gap: 4px; }
|
.card-tags { gap: 4px; }
|
||||||
.id-preview { flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
.id-preview { flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.card-side { flex-direction: column; align-items: flex-end; width: 100%; gap: 12px; }
|
||||||
.card-meta { width: 100%; justify-content: space-around; }
|
.card-meta { width: 100%; justify-content: space-around; }
|
||||||
|
.card-actions { width: 100%; justify-content: flex-end; }
|
||||||
.drawer { max-width: 100%; }
|
.drawer { max-width: 100%; }
|
||||||
}
|
}
|
||||||
.footer-btn-detail {
|
.footer-btn-detail {
|
||||||
|
|||||||
Reference in New Issue
Block a user