diff --git a/apps/voko-web/src/api.js b/apps/voko-web/src/api.js index df8c4a3..568ccfb 100644 --- a/apps/voko-web/src/api.js +++ b/apps/voko-web/src/api.js @@ -46,3 +46,20 @@ export async function api(method, path, body, idempotencyKey) { if (!response.ok) throw new Error(json.error.message) return json } + +/** + * 带登录态下载一个文件并交给浏览器保存(交付文件需要鉴权,不能直接用链接)。 + * @param {string} path + * @param {string} filename + */ +export async function download(path, filename) { + const token = getToken() + const response = await fetch(path, { headers: token === null ? {} : { authorization: `Bearer ${token}` } }) + if (!response.ok) throw new Error((await response.json()).error.message) + const url = URL.createObjectURL(await response.blob()) + const link = document.createElement('a') + link.href = url + link.download = filename + link.click() + URL.revokeObjectURL(url) +} diff --git a/apps/voko-web/src/i18n/messages.js b/apps/voko-web/src/i18n/messages.js index f4f5cb8..3c3c2b0 100644 --- a/apps/voko-web/src/i18n/messages.js +++ b/apps/voko-web/src/i18n/messages.js @@ -108,6 +108,8 @@ export const messages = { noSubmissions: 'Nothing visible yet. Submissions are shown to the publisher and the submitter; contest entries stay sealed until the deadline.', comments: 'Comments', round: 'Round {round}', + download: 'Download', + downloadFile: 'Download {filename}', none: 'None yet.', }, world: { @@ -290,6 +292,8 @@ export const messages = { noSubmissions: '暂无可见的交付。交付只对发布方和提交者本人可见;擂台赛的提交在截止前封存。', comments: '评论', round: '第 {round} 轮', + download: '下载', + downloadFile: '下载 {filename}', none: '暂无。', }, world: { diff --git a/apps/voko-web/src/pages/TaskPage.vue b/apps/voko-web/src/pages/TaskPage.vue index bf2b647..00c0214 100644 --- a/apps/voko-web/src/pages/TaskPage.vue +++ b/apps/voko-web/src/pages/TaskPage.vue @@ -2,9 +2,9 @@ import { onMounted, ref } from 'vue' import { useI18n } from 'vue-i18n' import { useRoute, useRouter } from 'vue-router' -import { Badge, Card } from '@drama/ui' +import { Badge, Button, Card } from '@drama/ui' -import { api, getToken } from '../api.js' +import { api, download, getToken } from '../api.js' const { t } = useI18n() const route = useRoute() @@ -14,6 +14,19 @@ const error = ref('') const when = (/** @type {string} */ iso) => new Date(iso).toLocaleString() +/** @param {{ file_id: string, filename: string }} artifact */ +async function save(artifact) { + error.value = '' + try { + await download(`/api/voko/agents/${route.params.id}/files/${artifact.file_id}`, artifact.filename) + } catch (err) { + error.value = err instanceof Error ? err.message : String(err) + } +} + +/** @param {number} bytes */ +const size = (bytes) => (bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`) + onMounted(async () => { if (getToken() === null) { await router.push({ path: '/login', query: { next: route.fullPath } }) @@ -49,7 +62,15 @@ onMounted(async () => {
{{ t('task.round', { round: item.round }) }} · {{ when(item.created_at) }}
{{ item.summary }}
- +