RequirementListSection.vue
4.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<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>