feat: DSL 磁力聚合 — 搜索/解析/预览/投递 + cilisousuo 源 + 中英双语 + Docker 部署

This commit is contained in:
ice-a
2026-10-16 18:53:45 +08:00
commit d6999104bf
64 changed files with 16524 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
<script setup lang="ts">
import type { ClientsResponse, TorrentView } from '~/types/api'
const props = defineProps<{ torrent: TorrentView }>()
const clients = ref<ClientsResponse | null>(null)
const loading = ref(false)
const sending = ref(false)
const selectedId = ref('')
const dir = ref('')
const statuses = computed(() => clients.value?.statuses || [])
const onlineStatuses = computed(() => statuses.value.filter((s) => s.online))
const current = computed(() => statuses.value.find((s) => s.id === selectedId.value) || null)
async function load() {
loading.value = true
try {
clients.value = await $fetch<ClientsResponse>('/api/clients')
const firstOnline = clients.value.statuses.find((s) => s.online)
if (!selectedId.value) selectedId.value = (firstOnline || clients.value.statuses[0])?.id || ''
syncDir()
} catch (err) {
pushToast('err', '读取下载器配置失败', errorMessage(err))
} finally {
loading.value = false
}
}
function syncDir() {
const cfg = clients.value?.configs.find((c) => c.id === selectedId.value)
dir.value = cfg?.downloadDir || ''
}
watch(selectedId, syncDir)
async function send() {
if (!selectedId.value) return
sending.value = true
try {
const res = await $fetch<{ ok: boolean; message: string }>('/api/send', {
method: 'POST',
body: {
clientId: selectedId.value,
infoHash: props.torrent.infoHash,
magnet: props.torrent.magnet || undefined,
name: props.torrent.name,
downloadDir: dir.value || undefined
}
})
pushToast('ok', '投递成功', res.message)
refreshHealth()
} catch (err) {
pushToast('err', '投递失败', errorMessage(err))
} finally {
sending.value = false
}
}
onMounted(load)
</script>
<template>
<div class="panel">
<div class="panel-hd">
<span class="label">SEND TO LOCAL CLIENT</span>
<span class="mono small" :class="onlineStatuses.length ? 'ok' : 'err'">
{{ loading ? '探测中…' : `${onlineStatuses.length}/${statuses.length} 在线` }}
</span>
<span class="spacer" />
<button class="btn btn-sm btn-ghost" :disabled="loading" @click="load">重新探测</button>
<NuxtLink to="/settings" class="btn btn-sm btn-ghost">配置</NuxtLink>
</div>
<div class="panel-bd stack sm">
<div v-if="!statuses.length" class="note warn">
还没有配置任何下载器。去「设置」里填 qBittorrent / Transmission / aria2 的地址和账号密码。
</div>
<template v-else>
<div class="row wrap">
<div
v-for="s in statuses"
:key="s.id"
class="pill"
:class="s.online ? 'on' : 'off'"
:title="s.error || s.endpoint"
>
<i class="dot" />
{{ s.name }}
<span class="mute">{{ s.version ? `v${s.version}` : s.online ? '' : '离线' }}</span>
</div>
</div>
<div class="grid-2">
<label class="field">
<span class="label">目标下载器</span>
<select v-model="selectedId" class="select">
<option v-for="s in statuses" :key="s.id" :value="s.id">
{{ s.name }}({{ s.online ? '在线' : '离线' }})
</option>
</select>
</label>
<label class="field">
<span class="label">下载目录(留空用客户端默认)</span>
<input v-model="dir" class="input" placeholder="D:\\Downloads" />
</label>
</div>
<div v-if="current && !current.online" class="note err">{{ current.error || '该下载器当前不可用' }}</div>
<div class="row">
<button class="btn btn-primary" :disabled="sending || !selectedId" @click="send">
<span v-if="sending" class="spin" />
{{ sending ? '投递中…' : '投递到本机下载器' }}
</button>
<span class="mute small">种子交给本机客户端下载,服务器不中转文件内容</span>
</div>
</template>
</div>
</div>
</template>