FeedbackModal.vue 3.03 KB
<template>
  <div class="modal-backdrop" @click.self="$emit('close')">
    <div class="modal">
      <header>
        <h2>+ NEW REQUIREMENT</h2>
        <button class="x" @click="$emit('close')">×</button>
      </header>
      <p class="hint">提交一段新的需求 / 反馈 → PM 进 incremental 模式,追加 PRD 章节并 bump rev。</p>
      <textarea
        v-model="text"
        :disabled="submitting"
        placeholder="例如:增加 toggle done 功能;或:把 POST /todos 改成支持 text+priority"
      />
      <div v-if="err" class="err">{{ err }}</div>
      <div v-if="ok" class="ok">{{ ok }}</div>
      <footer>
        <button class="btn cancel" @click="$emit('close')" :disabled="submitting">Cancel</button>
        <button class="btn submit" @click="submit" :disabled="!text.trim() || submitting">{{ submitting ? '...' : 'Submit' }}</button>
      </footer>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const props = defineProps({ pid: { type: String, required: true } });
const emit = defineEmits(['close', 'submitted']);

const text = ref('');
const submitting = ref(false);
const err = ref('');
const ok = ref('');

async function submit() {
  if (!text.value.trim()) return;
  submitting.value = true;
  err.value = ''; ok.value = '';
  try {
    const r = await fetch(`/api/devloop/projects/${props.pid}/feedback`, {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: text.value.trim(), by: 'human' }),
    });
    const j = await r.json();
    if (!r.ok) {
      err.value = j.error || `HTTP ${r.status}`;
    } else {
      ok.value = `PRD bumped: rev ${j.prd_rev_before} → ${j.prd_rev_after} · status=${j.status}`;
      setTimeout(() => emit('submitted'), 800);
    }
  } catch (e) {
    err.value = e.message;
  } finally {
    submitting.value = false;
  }
}
</script>

<style scoped>
.modal-backdrop {
  position:fixed; inset:0; background:rgba(0,0,0,.4); z-index:1000;
  display:flex; align-items:center; justify-content:center;
}
.modal {
  background:#fff; border-radius:8px; width:560px; max-width:90vw;
  padding:20px 24px; box-shadow:0 8px 32px rgba(0,0,0,.2);
}
header { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
h2 { margin:0; font-size:16px; }
.x { background:none; border:none; font-size:24px; cursor:pointer; color:#888; line-height:1; }
.hint { color:#888; font-size:12px; margin:0 0 12px 0; }
textarea {
  width:100%; min-height:120px; padding:10px; box-sizing:border-box;
  border:1px solid #ccc; border-radius:4px; font-family:inherit; resize:vertical;
}
.err { color:#c00; font-size:12px; margin-top:8px; }
.ok  { color:#070; font-size:12px; margin-top:8px; }
footer { margin-top:12px; display:flex; justify-content:flex-end; gap:8px; }
.btn { padding:6px 14px; border:none; border-radius:4px; cursor:pointer; font-size:13px; }
.btn:disabled { background:#ddd !important; color:#999 !important; cursor:not-allowed; }
.btn.cancel { background:#eee; color:#222; }
.btn.submit { background:#222; color:#fff; }
</style>