|
|
@@ -2,6 +2,7 @@
|
|
|
import { computed, nextTick, ref } from 'vue'
|
|
|
import type { AgentAssignment, AgentGoal } from '@shared/agentTasks'
|
|
|
import type { WorkspaceAgent } from '@shared/workspace'
|
|
|
+import AppIcon from '@/components/common/AppIcon.vue'
|
|
|
const props = defineProps<{
|
|
|
goal?: AgentGoal
|
|
|
assignments: AgentAssignment[]
|
|
|
@@ -11,6 +12,7 @@ const props = defineProps<{
|
|
|
defineEmits<{ stop: []; resume: [] }>()
|
|
|
const labels: Record<string, string> = { pending: '待执行', running: '进行中', review: '待复核', complete: '已完成', failed: '执行失败', blocked: '遇到阻碍', cancelling: '正在停止', cancelled: '已停止', waiting: '需要补充信息' }
|
|
|
const completed = computed(() => props.assignments.filter(a => a.status === 'complete').length)
|
|
|
+const progress = computed(() => props.assignments.length ? Math.round(completed.value / props.assignments.length * 100) : props.goal?.status === 'complete' ? 100 : 0)
|
|
|
const editable = computed(() => !!props.saveCriteria && !!props.goal && ['running', 'review', 'waiting'].includes(props.goal.status))
|
|
|
const editing = ref(false), saving = ref(false), draft = ref(''), error = ref('')
|
|
|
const editor = ref<HTMLTextAreaElement>(), editButton = ref<HTMLButtonElement>()
|
|
|
@@ -47,16 +49,35 @@ async function save() {
|
|
|
</script>
|
|
|
<template>
|
|
|
<section v-if="goal || assignments.length" class="task-plan" aria-label="目标进度">
|
|
|
- <header>
|
|
|
- <strong>交付进展</strong>
|
|
|
- <span v-if="goal">{{ labels[goal.status] }}</span>
|
|
|
- <small v-if="assignments.length">{{ completed }}/{{ assignments.length }} 项完成</small>
|
|
|
- <button v-if="goal && ['running','review','waiting'].includes(goal.status)" type="button" @click="$emit('stop')">停止任务</button>
|
|
|
- <button v-if="goal && ['blocked','waiting','cancelled'].includes(goal.status)" type="button" @click="$emit('resume')">继续任务</button>
|
|
|
+ <header class="task-plan__header">
|
|
|
+ <span class="task-plan__icon"><AppIcon name="files" :size="17" /></span>
|
|
|
+ <div class="task-plan__heading">
|
|
|
+ <strong>交付进展</strong>
|
|
|
+ <span v-if="goal" class="task-plan__status"><i />{{ labels[goal.status] }}</span>
|
|
|
+ </div>
|
|
|
+ <strong v-if="assignments.length" class="task-plan__count">{{ completed }}/{{ assignments.length }} <small>项完成</small></strong>
|
|
|
+ <div class="task-plan__actions">
|
|
|
+ <button v-if="goal && ['running','review','waiting'].includes(goal.status)" type="button" @click="$emit('stop')">停止任务</button>
|
|
|
+ <button v-if="goal && ['blocked','waiting','cancelled'].includes(goal.status)" type="button" @click="$emit('resume')">继续任务</button>
|
|
|
+ </div>
|
|
|
</header>
|
|
|
- <p v-if="goal?.result" class="result">{{ goal.result }}</p>
|
|
|
+
|
|
|
+ <div v-if="assignments.length" class="task-plan__progress" role="progressbar" aria-label="交付完成进度" aria-valuemin="0" aria-valuemax="100" :aria-valuenow="progress">
|
|
|
+ <span v-for="item in assignments" :key="item.id" :class="{ complete: item.status === 'complete', active: ['running','review'].includes(item.status), failed: ['failed','blocked'].includes(item.status) }" />
|
|
|
+ </div>
|
|
|
+ <div v-if="assignments.length" class="task-plan__steps" aria-label="任务步骤">
|
|
|
+ <div v-for="(item, index) in assignments" :key="item.id" :class="{ complete: item.status === 'complete', active: ['running','review'].includes(item.status), failed: ['failed','blocked'].includes(item.status) }">
|
|
|
+ <span><AppIcon v-if="item.status === 'complete'" name="check" :size="11" /><template v-else>{{ index + 1 }}</template></span>
|
|
|
+ <strong>{{ item.title }}</strong>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="goal?.result" class="task-plan__result">
|
|
|
+ <small>最新结果</small>
|
|
|
+ <p class="result">{{ goal.result }}</p>
|
|
|
+ </div>
|
|
|
<details>
|
|
|
- <summary>任务与验收详情</summary>
|
|
|
+ <summary><AppIcon name="file" :size="15" /><span>任务与验收详情</span><AppIcon class="task-plan__chevron" name="chevron-down" :size="14" /></summary>
|
|
|
<div class="plan-details">
|
|
|
<p v-if="goal" class="objective">{{ goal.objective }}</p>
|
|
|
<ol v-if="assignments.length">
|
|
|
@@ -85,12 +106,83 @@ async function save() {
|
|
|
</section>
|
|
|
</template>
|
|
|
<style scoped>
|
|
|
-.task-plan{width:100%;max-width:760px;margin:0 auto 8px;border:1px solid var(--line);border-radius:12px;padding:0 14px;font-size:13px;box-sizing:border-box}
|
|
|
-header{display:flex;align-items:center;gap:10px;min-height:44px;flex-wrap:wrap}header>span{color:var(--text-secondary)}header>small{margin-right:auto}header>button{margin-left:auto}
|
|
|
-summary{min-height:44px;align-content:center;cursor:pointer;color:var(--text-secondary)}small{font-size:12px;color:var(--text-secondary)}
|
|
|
-button{min-height:44px;padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--text-primary);font:inherit;cursor:pointer}button:hover:not(:disabled){background:var(--surface-hover)}button:disabled{opacity:.55;cursor:default}
|
|
|
-.plan-details{max-height:32vh;overflow:auto;padding-bottom:12px}.result{max-height:12vh;overflow:auto;margin:0;padding:8px 0}.objective{color:var(--text-secondary)}
|
|
|
-ol,.criteria{padding-left:18px;margin-top:0}li{padding:7px 0}li div{display:flex;gap:12px;justify-content:space-between}li div span{white-space:nowrap;color:var(--text-secondary)}p{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.6}.criteria span{font-size:12px;color:var(--text-secondary)}
|
|
|
-label{display:grid;gap:8px}textarea{width:100%;box-sizing:border-box;resize:vertical;padding:10px;border:1px solid var(--line);border-radius:8px;background:var(--surface);color:var(--text-primary);font:inherit;line-height:1.5}.criteria-actions{display:flex;gap:8px;margin-top:10px}.criteria-error{color:var(--danger)}
|
|
|
-summary:focus-visible,button:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
|
+.task-plan {
|
|
|
+ width: 100%;
|
|
|
+ max-width: 760px;
|
|
|
+ margin: 0 auto 10px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 15px;
|
|
|
+ background: var(--surface-raised);
|
|
|
+ box-shadow: 0 6px 22px color-mix(in srgb, var(--text-primary) 5%, transparent);
|
|
|
+ box-sizing: border-box;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.task-plan__header { display: flex; min-height: 58px; align-items: center; gap: 11px; padding: 8px 14px; }
|
|
|
+.task-plan__icon { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 10px; background: var(--surface-soft); color: var(--text-secondary); }
|
|
|
+.task-plan__heading { display: flex; min-width: 0; align-items: center; gap: 8px; }
|
|
|
+.task-plan__heading > strong { color: var(--text-primary); font-size: 14px; font-weight: 650; white-space: nowrap; }
|
|
|
+.task-plan__status { display: inline-flex; align-items: center; gap: 5px; color: var(--text-secondary); font-size: 11px; white-space: nowrap; }
|
|
|
+.task-plan__status i { width: 6px; height: 6px; border-radius: 50%; background: var(--success); box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 12%, transparent); }
|
|
|
+.task-plan__count { margin-left: auto; color: var(--text-primary); font-size: 13px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
|
+.task-plan__count small { margin-left: 2px; color: var(--text-muted); font-size: 11px; font-weight: 450; }
|
|
|
+.task-plan__actions { display: flex; gap: 6px; }
|
|
|
+.task-plan__actions:empty { display: none; }
|
|
|
+.task-plan__actions button { min-height: 40px; padding-inline: 10px; border-color: transparent; color: var(--text-secondary); }
|
|
|
+.task-plan__progress { display: grid; gap: 5px; grid-template-columns: repeat(auto-fit, minmax(28px, 1fr)); padding: 0 14px; }
|
|
|
+.task-plan__progress span { height: 3px; border-radius: 999px; background: var(--line-strong); }
|
|
|
+.task-plan__progress span.complete { background: var(--workflow-accent); }
|
|
|
+.task-plan__progress span.active { background: color-mix(in srgb, var(--workflow-accent) 52%, var(--line)); }
|
|
|
+.task-plan__progress span.failed { background: var(--danger); }
|
|
|
+.task-plan__steps { display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); padding: 10px 14px 13px; }
|
|
|
+.task-plan__steps > div { display: flex; min-width: 0; align-items: center; gap: 7px; color: var(--text-muted); }
|
|
|
+.task-plan__steps > div > span { display: grid; width: 20px; height: 20px; flex: 0 0 20px; place-items: center; border: 1px solid var(--line-strong); border-radius: 50%; background: var(--surface); color: var(--text-muted); font-size: 10px; font-variant-numeric: tabular-nums; }
|
|
|
+.task-plan__steps > div.complete > span { border-color: var(--workflow-accent); background: var(--workflow-accent); color: var(--workflow-on-accent); }
|
|
|
+.task-plan__steps > div.active > span { border-color: var(--workflow-accent); color: var(--workflow-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--workflow-accent) 14%, transparent); }
|
|
|
+.task-plan__steps > div.failed > span { border-color: var(--danger); color: var(--danger); }
|
|
|
+.task-plan__steps strong { min-width: 0; overflow: hidden; font-size: 11px; font-weight: 520; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
+.task-plan__steps .complete strong,
|
|
|
+.task-plan__steps .active strong { color: var(--text-secondary); }
|
|
|
+.task-plan__result { margin: 0 14px 12px; padding: 11px 12px; border-radius: 10px; background: var(--surface-soft); }
|
|
|
+.task-plan__result > small { display: block; margin-bottom: 3px; color: var(--text-muted); font-size: 10px; font-weight: 620; }
|
|
|
+.result { max-height: 12vh; margin: 0; overflow: auto; color: var(--text-secondary); font-size: 12px; line-height: 1.6; }
|
|
|
+details { border-top: 1px solid var(--line); }
|
|
|
+summary { display: flex; min-height: 46px; align-items: center; gap: 8px; padding: 0 14px; color: var(--text-secondary); cursor: pointer; list-style: none; }
|
|
|
+summary::-webkit-details-marker { display: none; }
|
|
|
+summary > span { flex: 1; font-size: 12px; font-weight: 520; }
|
|
|
+.task-plan__chevron { transition: transform 160ms ease; }
|
|
|
+details[open] .task-plan__chevron { transform: rotate(180deg); }
|
|
|
+.plan-details { max-height: 34vh; overflow: auto; padding: 2px 14px 14px; }
|
|
|
+.objective { color: var(--text-secondary); }
|
|
|
+ol,.criteria { padding-left: 20px; margin-top: 0; }
|
|
|
+li { padding: 7px 0; }
|
|
|
+li div { display: flex; gap: 12px; justify-content: space-between; }
|
|
|
+li div span { color: var(--text-secondary); white-space: nowrap; }
|
|
|
+p { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; }
|
|
|
+.criteria span { color: var(--text-secondary); font-size: 12px; }
|
|
|
+button { min-height: 44px; padding: 8px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); color: var(--text-primary); cursor: pointer; font: inherit; }
|
|
|
+button:hover:not(:disabled) { background: var(--surface-hover); }
|
|
|
+button:disabled { cursor: default; opacity: .55; }
|
|
|
+label { display: grid; gap: 8px; }
|
|
|
+textarea { width: 100%; padding: 10px; border: 1px solid var(--line); border-radius: 9px; outline: 0; background: var(--surface); box-sizing: border-box; color: var(--text-primary); font: inherit; line-height: 1.5; resize: vertical; }
|
|
|
+textarea:focus { border-color: var(--workflow-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--workflow-accent) 14%, transparent); }
|
|
|
+.criteria-actions { display: flex; gap: 8px; margin-top: 10px; }
|
|
|
+.criteria-error { color: var(--danger); }
|
|
|
+summary:focus-visible,button:focus-visible { outline: 2px solid var(--workflow-accent); outline-offset: 2px; }
|
|
|
+
|
|
|
+@media (max-width: 600px) {
|
|
|
+ .task-plan { border-radius: 14px; }
|
|
|
+ .task-plan__header { min-height: 56px; padding-inline: 12px; }
|
|
|
+ .task-plan__icon { display: none; }
|
|
|
+ .task-plan__heading { align-items: flex-start; flex-direction: column; gap: 1px; }
|
|
|
+ .task-plan__actions button { min-width: 44px; padding-inline: 8px; font-size: 12px; }
|
|
|
+ .task-plan__progress { padding-inline: 12px; }
|
|
|
+ .task-plan__steps { grid-auto-columns: minmax(78px, 1fr); grid-auto-flow: column; grid-template-columns: none; gap: 8px; padding-inline: 12px; overflow-x: auto; scrollbar-width: none; }
|
|
|
+ .task-plan__steps::-webkit-scrollbar { display: none; }
|
|
|
+ .task-plan__result { margin-inline: 12px; }
|
|
|
+ summary { padding-inline: 12px; }
|
|
|
+ .plan-details { padding-inline: 12px; }
|
|
|
+}
|
|
|
+
|
|
|
+@media (prefers-reduced-motion: reduce) { .task-plan__chevron { transition: none; } }
|
|
|
</style>
|