RequirementListSection.vue 4.66 KB
<template>
  <section class="req-section">
    <header class="req-hdr">
      <h2>需求列表</h2>
      <div class="hdr-meta">
        <span v-if="data" class="meta">
          共 {{ data.items.length }} 项 · 当前 rev {{ data.current_rev }} · 项目状态
          <span :class="['pill', `s-${data.project_status}`]">{{ data.project_status }}</span>
        </span>
        <button class="primary" @click="goNew">+ 新增需求</button>
      </div>
    </header>

    <div v-if="error" class="err">{{ error }}</div>

    <div v-if="loading && !data" class="empty-mini">加载中…</div>
    <table v-else-if="data && data.items.length" class="rt">
      <thead>
        <tr>
          <th>rev</th><th>时间</th><th>来源</th><th>触发</th><th>需求摘要</th><th>状态</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="it in data.items" :key="it.rev" :class="{ current: it.is_current }">
          <td><code>r{{ it.rev }}</code></td>
          <td class="ts">{{ formatTime(it.ts) }}</td>
          <td><code>{{ it.by }}</code></td>
          <td>{{ humanTrigger(it.trigger) }}</td>
          <td class="text-cell" :title="it.text">{{ truncate(it.text, 100) }}</td>
          <td>
            <span v-if="it.is_current" :class="['pill', `s-${it.status}`]">{{ it.status }}</span>
            <span v-else class="pill older">已被新需求覆盖</span>
          </td>
        </tr>
      </tbody>
    </table>
    <div v-else class="empty-mini">尚无需求记录。</div>
  </section>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { useRouter } from 'vue-router';

const props = defineProps({ pid: { type: String, required: true } });
const router = useRouter();

const data = ref(null);
const error = ref('');
const loading = ref(false);
let pollTimer = null;

async function fetchList() {
  loading.value = true;
  try {
    const r = await fetch(`/api/devloop/projects/${props.pid}/requirements`);
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    data.value = await r.json();
    error.value = '';
  } catch (e) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
}

function goNew() {
  router.push(`/devloop/${props.pid}/new-requirement`);
}

function formatTime(iso) {
  if (!iso) return '—';
  try {
    const d = new Date(iso);
    return `${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
  } catch { return iso; }
}
function truncate(s, n) {
  if (!s) return '—';
  return s.length > n ? s.slice(0, n) + '…' : s;
}
function humanTrigger(t) {
  return ({ project_create: '初始', human_feedback: '人类反馈' }[t]) || t;
}

onMounted(() => {
  fetchList();
  pollTimer = setInterval(fetchList, 5000);
});
onBeforeUnmount(() => { if (pollTimer) clearInterval(pollTimer); });
</script>

<style scoped>
.req-section { background:#fff; border:1px solid #e7e7eb; border-radius:8px; padding:16px; }
.req-hdr { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
.req-hdr h2 { font-size:14px; font-weight:600; margin:0; color:#555; }
.hdr-meta { display:flex; align-items:center; gap:14px; }
.meta { font-size:12px; color:#888; }
.err { color:#c00; font-size:12px; padding:8px; background:#fee; border-radius:4px; margin-bottom:8px; }
.empty-mini { color:#888; padding:24px; text-align:center; font-size:13px; }

button.primary { background:#0a6; color:#fff; border:1px solid #0a6; padding:5px 14px; border-radius:5px; cursor:pointer; font-size:12px; }
button.primary:hover { background:#085; }

.rt { width:100%; border-collapse:collapse; font-size:13px; }
.rt th, .rt td { padding:8px 10px; border-bottom:1px solid #f0f0f0; text-align:left; vertical-align:top; }
.rt th { background:#fafafa; color:#666; font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:0.04em; }
.rt tr.current { background:#f5fbf7; }
.rt code { background:#f4f4f6; padding:1px 5px; border-radius:3px; font-family: ui-monospace, monospace; font-size:11px; }
.rt .ts { white-space:nowrap; color:#666; font-size:12px; }
.rt .text-cell { max-width:480px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.pill { font-size:11px; padding:2px 8px; border-radius:10px; font-weight:600; background:#eee; color:#555; }
.pill.older { background:#fafafa; color:#999; font-weight:400; }
.pill.s-draft { background:#eef; color:#446; }
.pill.s-running { background:#fff3cd; color:#7a5; }
.pill.s-shipped { background:#d4f4dd; color:#0a6; }
.pill.s-shipped-final { background:#0a6; color:#fff; }
.pill.s-abandoned { background:#fbe; color:#922; }
.pill.s-l2_blocked { background:#fdd; color:#a44; }
</style>