Files
ChatLab/src/pages/settings/components/API/DataSourceAddModal.vue
T
2026-04-22 23:46:31 +08:00

288 lines
10 KiB
Vue

<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useApiServerStore, type DataSource, type RemoteSession } from '@/stores/apiServer'
const props = defineProps<{
open: boolean
manageSource?: DataSource
subscribedRemoteIds?: Set<string>
}>()
const emit = defineEmits<{
'update:open': [value: boolean]
sourceAdded: []
sessionsAdded: [sourceId: string, sessions: Array<{ name: string; remoteSessionId: string }>]
}>()
const { t } = useI18n()
const store = useApiServerStore()
const isManageMode = computed(() => !!props.manageSource)
const formData = ref({
name: '',
baseUrl: '',
token: '',
intervalMinutes: 60,
})
const remoteSessions = ref<RemoteSession[]>([])
const selectedSessionIds = ref<Set<string>>(new Set())
const discovering = ref(false)
const discoveryError = ref('')
watch(
() => props.open,
async (val) => {
if (val) {
if (isManageMode.value && props.manageSource) {
formData.value = {
baseUrl: props.manageSource.baseUrl,
token: props.manageSource.token,
intervalMinutes: props.manageSource.intervalMinutes,
}
} else {
formData.value = { name: '', baseUrl: '', token: '', intervalMinutes: 60 }
}
remoteSessions.value = []
selectedSessionIds.value = new Set()
discoveryError.value = ''
if (isManageMode.value) {
await discoverSessions()
}
}
},
{ immediate: true }
)
const availableSessions = computed(() => remoteSessions.value.filter((s) => !props.subscribedRemoteIds?.has(s.id)))
const allSelected = computed(
() => availableSessions.value.length > 0 && selectedSessionIds.value.size === availableSessions.value.length
)
function toggleSelectAll() {
if (allSelected.value) {
selectedSessionIds.value = new Set()
} else {
selectedSessionIds.value = new Set(availableSessions.value.map((s) => s.id))
}
}
function toggleSession(id: string) {
if (props.subscribedRemoteIds?.has(id)) return
const next = new Set(selectedSessionIds.value)
if (next.has(id)) next.delete(id)
else next.add(id)
selectedSessionIds.value = next
}
function closeModal() {
emit('update:open', false)
}
async function discoverSessions() {
if (!formData.value.baseUrl) return
discovering.value = true
discoveryError.value = ''
remoteSessions.value = []
selectedSessionIds.value = new Set()
try {
remoteSessions.value = await store.fetchRemoteSessions(formData.value.baseUrl, formData.value.token)
} catch (err: any) {
discoveryError.value = err.message || t('settings.api.dataSources.discovery.error')
} finally {
discovering.value = false
}
}
async function handleSubmit() {
if (isManageMode.value) {
if (selectedSessionIds.value.size === 0 || !props.manageSource) return
const sessions = remoteSessions.value
.filter((s) => selectedSessionIds.value.has(s.id))
.map((s) => ({ name: s.name, remoteSessionId: s.id }))
emit('sessionsAdded', props.manageSource.id, sessions)
closeModal()
} else {
const ds = await store.addDataSource({
name: formData.value.name || undefined,
baseUrl: formData.value.baseUrl,
token: formData.value.token,
intervalMinutes: formData.value.intervalMinutes,
})
if (ds && selectedSessionIds.value.size > 0) {
const sessions = remoteSessions.value
.filter((s) => selectedSessionIds.value.has(s.id))
.map((s) => ({ name: s.name, remoteSessionId: s.id }))
await store.addImportSessions(ds.id, sessions)
}
emit('sourceAdded')
closeModal()
}
}
function formatMessageCount(count?: number): string {
if (count === undefined) return '-'
if (count >= 10000) return `${(count / 10000).toFixed(1)}w`
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`
return String(count)
}
</script>
<template>
<UModal :open="open" @update:open="emit('update:open', $event)">
<template #content>
<div class="p-6" style="min-width: 480px; max-height: 80vh; overflow-y: auto">
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
{{
isManageMode
? t('settings.api.dataSources.edit.manageSessions')
: t('settings.api.dataSources.form.modalTitle')
}}
</h3>
<div class="space-y-4">
<!-- New data source: show all fields -->
<template v-if="!isManageMode">
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">
{{ t('settings.api.dataSources.form.name') }}
</label>
<UInput
v-model="formData.name"
class="w-full"
:placeholder="t('settings.api.dataSources.form.namePlaceholder')"
/>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">
{{ t('settings.api.dataSources.discovery.baseUrl') }}
</label>
<UInput
v-model="formData.baseUrl"
class="w-full"
:placeholder="t('settings.api.dataSources.discovery.baseUrlPlaceholder')"
/>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">
{{ t('settings.api.dataSources.form.token') }}
</label>
<UInput
v-model="formData.token"
class="w-full"
:placeholder="t('settings.api.dataSources.form.tokenPlaceholder')"
/>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300">
{{ t('settings.api.dataSources.form.interval') }}
</label>
<UInput v-model.number="formData.intervalMinutes" type="number" :min="1" class="w-full" />
</div>
<div class="flex items-center gap-2">
<UButton
color="primary"
variant="soft"
:loading="discovering"
:disabled="!formData.baseUrl"
@click="discoverSessions"
>
<UIcon name="i-heroicons-magnifying-glass" class="mr-1 h-4 w-4" />
{{ t('settings.api.dataSources.discovery.browse') }}
</UButton>
</div>
</template>
<!-- Error -->
<div
v-if="discoveryError"
class="rounded-md bg-red-50 p-3 text-xs text-red-600 dark:bg-red-900/20 dark:text-red-400"
>
{{ discoveryError }}
</div>
<!-- Session list -->
<div v-if="remoteSessions.length > 0">
<div class="mb-2 flex items-center justify-between">
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">
{{ t('settings.api.dataSources.discovery.found', { count: remoteSessions.length }) }}
</span>
<button
class="text-xs text-blue-500 hover:text-blue-700 dark:hover:text-blue-400"
@click="toggleSelectAll"
>
{{
allSelected
? t('settings.api.dataSources.discovery.deselectAll')
: t('settings.api.dataSources.discovery.selectAll')
}}
</button>
</div>
<div class="max-h-64 overflow-y-auto rounded-lg border border-gray-200 dark:border-gray-600">
<div
v-for="session in remoteSessions"
:key="session.id"
class="flex items-center gap-3 border-b border-gray-100 px-3 py-2 last:border-0"
:class="
subscribedRemoteIds?.has(session.id)
? 'opacity-60'
: 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800'
"
@click="toggleSession(session.id)"
>
<UCheckbox
:model-value="subscribedRemoteIds?.has(session.id) || selectedSessionIds.has(session.id)"
:disabled="subscribedRemoteIds?.has(session.id)"
@click.stop
@update:model-value="toggleSession(session.id)"
/>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 truncate text-sm font-medium text-gray-900 dark:text-white">
{{ session.name }}
<span
v-if="subscribedRemoteIds?.has(session.id)"
class="rounded-full bg-green-100 px-1.5 py-0.5 text-[10px] text-green-700 dark:bg-green-900/30 dark:text-green-400"
>
{{ t('settings.api.dataSources.edit.subscribed') }}
</span>
</div>
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
<span class="rounded bg-gray-100 px-1 dark:bg-gray-700">{{ session.platform }}</span>
<span>{{ session.type }}</span>
<span v-if="session.messageCount !== undefined">
{{ formatMessageCount(session.messageCount) }}
{{ t('settings.api.dataSources.discovery.messages') }}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-end gap-2 pt-2">
<UButton variant="soft" @click="closeModal">{{ t('common.cancel') }}</UButton>
<UButton
v-if="isManageMode"
color="primary"
:disabled="selectedSessionIds.size === 0"
@click="handleSubmit"
>
{{ t('settings.api.dataSources.discovery.subscribe', { count: selectedSessionIds.size }) }}
</UButton>
<UButton v-else color="primary" :disabled="!formData.baseUrl" @click="handleSubmit">
{{ t('settings.api.dataSources.addBtn') }}
</UButton>
</div>
</div>
</div>
</template>
</UModal>
</template>