DevLoopDashboard.vue 4.49 KB
<template>
  <div class="devloop-dashboard">
    <DashboardHeader
      :pid="pid"
      :state="state"
      @ops="goOps"
      @new-requirement="goNewRequirement"
      @signoff="onSignOff"
      @abandon="onAbandon"
    />

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

    <div v-if="state" class="grid">
      <section class="loop-status">
        <LoopStatusBar :state="state" />
      </section>

      <div class="reqs">
        <RequirementListSection :pid="pid" />
      </div>

      <section class="dag">
        <h2>Units DAG</h2>
        <UnitsDAG
          :units="state.units || []"
          @select="onUnitClick"
        />
      </section>

      <section class="live">
        <h2>Live agent stream</h2>
        <LiveAgentStream :pid="pid" />
      </section>

      <section class="diff">
        <h2>PRD changes</h2>
        <PrdDiffPanel :pid="pid" :state="state" />
      </section>
    </div>

    <UnitDrawer
      v-if="drawerUnit"
      :pid="pid"
      :unit-id="drawerUnit"
      @close="drawerUnit = null"
    />

    <FeedbackModal
      v-if="showFeedback"
      :pid="pid"
      @close="showFeedback = false"
      @submitted="onFeedbackSubmitted"
    />
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { useRouter } from 'vue-router';
import DashboardHeader from '@/components/devloop/DashboardHeader.vue';
import LoopStatusBar from '@/components/devloop/LoopStatusBar.vue';
import UnitsDAG from '@/components/devloop/UnitsDAG.vue';
import LiveAgentStream from '@/components/devloop/LiveAgentStream.vue';
import PrdDiffPanel from '@/components/devloop/PrdDiffPanel.vue';
import FeedbackModal from '@/components/devloop/FeedbackModal.vue';
import UnitDrawer from '@/components/devloop/UnitDrawer.vue';
import RequirementListSection from '@/components/devloop/RequirementListSection.vue';

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

const state = ref(null);
const error = ref('');
const showFeedback = ref(false);
const drawerUnit = ref(null);
let pollTimer = null;

async function fetchState() {
  try {
    const r = await fetch(`/api/devloop/projects/${props.pid}/state`);
    if (!r.ok) {
      error.value = r.status === 404 ? `项目不存在: ${props.pid}` : `读 state 失败: ${r.status}`;
      state.value = null;
      return;
    }
    error.value = '';
    state.value = await r.json();
  } catch (e) {
    error.value = e.message;
  }
}

function goOps(tab = 'bugs') {
  router.push(`/devloop/${props.pid}/ops/${tab}`);
}

function openFeedback() { showFeedback.value = true; }
function goNewRequirement() { router.push(`/devloop/${props.pid}/new-requirement`); }

function onUnitClick(unitId) { drawerUnit.value = unitId; }

async function onSignOff() {
  if (!confirm('确认 Sign Off? 此后将无法再提交反馈。')) return;
  const r = await fetch(`/api/devloop/projects/${props.pid}/sign-off`, {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ by: 'human' }),
  });
  const j = await r.json();
  if (!r.ok) alert('Sign off failed: ' + (j.error || r.status));
  await fetchState();
}

async function onAbandon() {
  const reason = prompt('Abandon 原因 (可空):', 'no longer needed');
  if (reason === null) return;
  const r = await fetch(`/api/devloop/projects/${props.pid}/abandon`, {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ by: 'human', reason }),
  });
  const j = await r.json();
  if (!r.ok) alert('Abandon failed: ' + (j.error || r.status));
  await fetchState();
}

function onFeedbackSubmitted() { showFeedback.value = false; fetchState(); }

onMounted(async () => {
  await fetchState();
  pollTimer = setInterval(fetchState, 2500);
});

onBeforeUnmount(() => {
  if (pollTimer) clearInterval(pollTimer);
});
</script>

<style scoped>
.devloop-dashboard { max-width: 1280px; margin: 0 auto; padding: 16px 24px; }
.error-banner { background:#fee; border:1px solid #fcc; color:#c00; padding:8px 12px; border-radius:6px; margin:8px 0; }
.grid { display:grid; grid-template-columns: 1fr 1fr; gap:16px; margin-top:16px; }
.grid section { background:#fff; border:1px solid #e7e7eb; border-radius:8px; padding:16px; min-height: 200px; }
.grid section.live, .grid section.diff { min-height: 280px; }
.grid h2 { font-size:14px; font-weight:600; margin:0 0 8px 0; color:#555; }
.loop-status, .dag, .reqs { grid-column: 1 / -1; }
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
</style>