BoardPage.vue 3.97 KB
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { Badge, Button, Card, Input, Select, Textarea } from '@drama/ui'

import { api, getToken } from '../api.js'

const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const tasks = ref(null)
const message = ref('')
const form = reactive({ title: '', spec: '', template: 'piecework', reward: '', deadline: '', acceptance: '' })
// 一次填写对应一个幂等键:提交失败后重试不会重复扣积分,成功后换新键。
let idempotencyKey = crypto.randomUUID()

const BADGE = { open: 'info', claimed: 'warning', in_contest: 'warning', submitted: 'warning', reviewing: 'warning', rework: 'warning', disputed: 'danger', settled: 'success', pending_approval: 'neutral', cancelled: 'neutral', refunded: 'neutral' }
const tasksPath = `/api/voko/agents/${route.params.id}/tasks`

async function load() {
  tasks.value = (await api('GET', tasksPath)).tasks
}

async function publish() {
  message.value = ''
  try {
    await api('POST', tasksPath, {
      title: form.title,
      spec: form.spec,
      template: form.template,
      reward: Number(form.reward),
      deadline: new Date(form.deadline).toISOString(),
      acceptance: form.acceptance.split('\n').map((line) => line.trim()).filter((line) => line !== '').map((text, index) => ({ id: `c${index + 1}`, text })),
    }, idempotencyKey)
    idempotencyKey = crypto.randomUUID()
    Object.assign(form, { title: '', spec: '', reward: '', deadline: '', acceptance: '' })
    message.value = t('board.published')
    await load()
  } catch (err) {
    message.value = err instanceof Error ? err.message : String(err)
  }
}

onMounted(async () => {
  if (getToken() === null) {
    await router.push({ path: '/login', query: { next: route.fullPath } })
    return
  }
  await load()
})
</script>

<template>
  <h1>{{ t('board.title') }}</h1>
  <p><router-link to="/agents">{{ t('board.back') }}</router-link></p>

  <Card class="voko-board-card">
    <h2>{{ t('board.publishTitle') }}</h2>
    <p>{{ t('board.publishHint') }}</p>
    <label class="voko-field">{{ t('board.taskTitle') }}<Input v-model="form.title" /></label>
    <label class="voko-field">{{ t('board.spec') }}<Textarea v-model="form.spec" /></label>
    <div class="voko-field">
      <span>{{ t('board.template') }}</span>
      <Select v-model="form.template" :aria-label="t('board.template')" :options="[{ value: 'piecework', label: t('board.templates.piecework') }, { value: 'contest', label: t('board.templates.contest') }]" />
    </div>
    <label class="voko-field">{{ t('board.reward_label') }}<Input v-model="form.reward" type="number" /></label>
    <label class="voko-field">{{ t('board.deadline_label') }}<Input v-model="form.deadline" type="datetime-local" /></label>
    <label class="voko-field">{{ t('board.acceptance') }}<Textarea v-model="form.acceptance" :rows="3" /></label>
    <p v-if="message" role="status">{{ message }}</p>
    <Button :disabled="form.title === '' || form.spec === '' || form.reward === '' || form.deadline === '' || form.acceptance.trim() === ''" @click="publish">{{ t('board.publish') }}</Button>
  </Card>

  <template v-if="tasks">
    <p v-if="tasks.length === 0">{{ t('board.empty') }}</p>
    <Card v-for="task in tasks" :key="task.id" class="voko-board-card">
      <h3><router-link :to="`/agents/${route.params.id}/tasks/${task.id}`">{{ task.title }}</router-link> <Badge :variant="BADGE[task.status]">{{ t(`board.status.${task.status}`) }}</Badge></h3>
      <p>{{ t(`board.templates.${task.template}`) }} · {{ t('board.reward', { reward: task.reward }) }} · {{ t('board.rarity', { rarity: task.rarity }) }} · {{ t('board.deadline', { deadline: new Date(task.deadline).toLocaleString() }) }}</p>
      <p class="voko-spec">{{ task.spec }}</p>
    </Card>
  </template>
</template>

<style scoped>
.voko-board-card {
  margin-bottom: var(--drama-space-md);
}
.voko-spec {
  white-space: pre-wrap;
}
</style>