mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(provider-ui): 支持供应商备注内联编辑
在供应商列表中添加备注的内联编辑功能,点击备注文字或"添加备注" 即可直接编辑,支持 Enter 保存、Escape 取消和点击外部自动取消。 桌面端和移动端均已适配。
This commit is contained in:
@@ -202,11 +202,46 @@
|
|||||||
<ExternalLink class="w-3.5 h-3.5" />
|
<ExternalLink class="w-3.5 h-3.5" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 内联编辑备注 -->
|
||||||
|
<div
|
||||||
|
v-if="editingDescriptionId === provider.id"
|
||||||
|
data-desc-editor
|
||||||
|
class="flex items-center gap-1 max-w-[220px]"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="editingDescriptionValue"
|
||||||
|
v-auto-focus
|
||||||
|
class="flex-1 min-w-0 text-xs px-1.5 py-0.5 rounded border border-border bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||||
|
placeholder="输入备注..."
|
||||||
|
@keydown="handleDescriptionKeydown($event, provider)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="shrink-0 p-0.5 rounded hover:bg-muted text-primary"
|
||||||
|
title="保存"
|
||||||
|
@click="saveDescription($event, provider)"
|
||||||
|
>
|
||||||
|
<Check class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="shrink-0 p-0.5 rounded hover:bg-muted text-muted-foreground"
|
||||||
|
title="取消"
|
||||||
|
@click="cancelEditDescription($event)"
|
||||||
|
>
|
||||||
|
<X class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<span
|
<span
|
||||||
v-if="provider.description"
|
v-else-if="provider.description"
|
||||||
class="text-xs text-muted-foreground truncate block max-w-[200px]"
|
class="text-xs text-muted-foreground truncate block max-w-[200px] group/desc cursor-pointer hover:text-foreground/70 transition-colors"
|
||||||
:title="provider.description"
|
:title="provider.description"
|
||||||
>{{ provider.description }}</span>
|
@click="startEditDescription($event, provider)"
|
||||||
|
>{{ provider.description }} <Pencil class="w-3 h-3 inline-block opacity-0 group-hover/desc:opacity-50 transition-opacity" /></span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-xs text-muted-foreground cursor-pointer hover:text-foreground/70 transition-colors"
|
||||||
|
@click="startEditDescription($event, provider)"
|
||||||
|
>添加备注</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-3.5">
|
<TableCell class="py-3.5">
|
||||||
@@ -491,11 +526,46 @@
|
|||||||
{{ provider.is_active ? '活跃' : '停用' }}
|
{{ provider.is_active ? '活跃' : '停用' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 内联编辑备注 (移动端) -->
|
||||||
|
<div
|
||||||
|
v-if="editingDescriptionId === provider.id"
|
||||||
|
data-desc-editor
|
||||||
|
class="flex items-center gap-1 max-w-[180px]"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="editingDescriptionValue"
|
||||||
|
v-auto-focus
|
||||||
|
class="flex-1 min-w-0 text-xs px-1.5 py-0.5 rounded border border-border bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||||
|
placeholder="输入备注..."
|
||||||
|
@keydown="handleDescriptionKeydown($event, provider)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="shrink-0 p-0.5 rounded hover:bg-muted text-primary"
|
||||||
|
title="保存"
|
||||||
|
@click="saveDescription($event, provider)"
|
||||||
|
>
|
||||||
|
<Check class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="shrink-0 p-0.5 rounded hover:bg-muted text-muted-foreground"
|
||||||
|
title="取消"
|
||||||
|
@click="cancelEditDescription($event)"
|
||||||
|
>
|
||||||
|
<X class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<span
|
<span
|
||||||
v-if="provider.description"
|
v-else-if="provider.description"
|
||||||
class="text-xs text-muted-foreground truncate block max-w-[120px]"
|
class="text-xs text-muted-foreground truncate block max-w-[120px] group/desc cursor-pointer hover:text-foreground/70 transition-colors"
|
||||||
:title="provider.description"
|
:title="provider.description"
|
||||||
>{{ provider.description }}</span>
|
@click="startEditDescription($event, provider)"
|
||||||
|
>{{ provider.description }} <Pencil class="w-3 h-3 inline-block opacity-0 group-hover/desc:opacity-50 transition-opacity" /></span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-xs text-muted-foreground cursor-pointer hover:text-foreground/70 transition-colors"
|
||||||
|
@click="startEditDescription($event, provider)"
|
||||||
|
>添加备注</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-0.5 shrink-0"
|
class="flex items-center gap-0.5 shrink-0"
|
||||||
@@ -708,7 +778,10 @@ import {
|
|||||||
KeyRound,
|
KeyRound,
|
||||||
Loader2,
|
Loader2,
|
||||||
FilterX,
|
FilterX,
|
||||||
ExternalLink
|
ExternalLink,
|
||||||
|
Pencil,
|
||||||
|
Check,
|
||||||
|
X
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
import Badge from '@/components/ui/badge.vue'
|
import Badge from '@/components/ui/badge.vue'
|
||||||
@@ -746,6 +819,10 @@ import { formatBillingType } from '@/utils/format'
|
|||||||
import { type BalanceExtraItem } from '@/features/providers/auth-templates'
|
import { type BalanceExtraItem } from '@/features/providers/auth-templates'
|
||||||
import { formatBalanceExtraFromSchema, type CredentialsSchema } from '@/features/providers/auth-templates/schema-utils'
|
import { formatBalanceExtraFromSchema, type CredentialsSchema } from '@/features/providers/auth-templates/schema-utils'
|
||||||
|
|
||||||
|
const vAutoFocus = {
|
||||||
|
mounted: (el: HTMLElement) => el.focus()
|
||||||
|
}
|
||||||
|
|
||||||
const { error: showError, success: showSuccess } = useToast()
|
const { error: showError, success: showSuccess } = useToast()
|
||||||
const { confirmDanger } = useConfirm()
|
const { confirmDanger } = useConfirm()
|
||||||
|
|
||||||
@@ -786,6 +863,53 @@ const opsConfigDialogOpen = ref(false)
|
|||||||
const opsConfigProviderId = ref('')
|
const opsConfigProviderId = ref('')
|
||||||
const opsConfigProviderWebsite = ref('')
|
const opsConfigProviderWebsite = ref('')
|
||||||
|
|
||||||
|
// 内联编辑备注
|
||||||
|
const editingDescriptionId = ref<string | null>(null)
|
||||||
|
const editingDescriptionValue = ref('')
|
||||||
|
|
||||||
|
function startEditDescription(event: Event, provider: ProviderWithEndpointsSummary) {
|
||||||
|
event.stopPropagation()
|
||||||
|
editingDescriptionId.value = provider.id
|
||||||
|
editingDescriptionValue.value = provider.description || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEditDescription(event?: Event) {
|
||||||
|
event?.stopPropagation()
|
||||||
|
editingDescriptionId.value = null
|
||||||
|
editingDescriptionValue.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDescription(event: Event, provider: ProviderWithEndpointsSummary) {
|
||||||
|
event.stopPropagation()
|
||||||
|
const newValue = editingDescriptionValue.value.trim()
|
||||||
|
const oldValue = provider.description || ''
|
||||||
|
if (newValue === oldValue) {
|
||||||
|
cancelEditDescription()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateProvider(provider.id, { description: newValue || null })
|
||||||
|
provider.description = newValue || undefined
|
||||||
|
// 同步更新 providers 数组
|
||||||
|
const target = providers.value.find(p => p.id === provider.id)
|
||||||
|
if (target) {
|
||||||
|
target.description = newValue || undefined
|
||||||
|
}
|
||||||
|
cancelEditDescription()
|
||||||
|
} catch (err: any) {
|
||||||
|
showError(err.response?.data?.detail || '更新备注失败', '错误')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDescriptionKeydown(event: KeyboardEvent, provider: ProviderWithEndpointsSummary) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
saveDescription(event, provider)
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
cancelEditDescription(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 余额数据缓存 {providerId: ActionResultResponse}
|
// 余额数据缓存 {providerId: ActionResultResponse}
|
||||||
const balanceCache = ref<Record<string, ActionResultResponse>>({})
|
const balanceCache = ref<Record<string, ActionResultResponse>>({})
|
||||||
// 余额加载请求版本计数器(用于防止竞态条件)
|
// 余额加载请求版本计数器(用于防止竞态条件)
|
||||||
@@ -1379,6 +1503,14 @@ async function toggleProviderStatus(provider: ProviderWithEndpointsSummary) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 点击外部自动取消编辑备注
|
||||||
|
function handleGlobalClick(event: MouseEvent) {
|
||||||
|
if (!editingDescriptionId.value) return
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
if (target.closest('[data-desc-editor]')) return
|
||||||
|
cancelEditDescription()
|
||||||
|
}
|
||||||
|
|
||||||
// 用于触发倒计时更新的响应式计数器
|
// 用于触发倒计时更新的响应式计数器
|
||||||
const tickCounter = ref(0)
|
const tickCounter = ref(0)
|
||||||
let tickInterval: ReturnType<typeof setInterval> | null = null
|
let tickInterval: ReturnType<typeof setInterval> | null = null
|
||||||
@@ -1388,6 +1520,7 @@ onMounted(() => {
|
|||||||
loadPriorityMode()
|
loadPriorityMode()
|
||||||
loadGlobalModelList()
|
loadGlobalModelList()
|
||||||
loadArchitectureSchemas()
|
loadArchitectureSchemas()
|
||||||
|
document.addEventListener('click', handleGlobalClick, true)
|
||||||
// 每秒更新一次倒计时
|
// 每秒更新一次倒计时
|
||||||
tickInterval = setInterval(() => {
|
tickInterval = setInterval(() => {
|
||||||
tickCounter.value++
|
tickCounter.value++
|
||||||
@@ -1395,6 +1528,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleGlobalClick, true)
|
||||||
if (tickInterval) {
|
if (tickInterval) {
|
||||||
clearInterval(tickInterval)
|
clearInterval(tickInterval)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user