feat(campaign): live plan timeline preview in create form
Above the plan editor, adapt each plan entry (offset, scenario, count) into WindowTimeline markers coloured per scenario with a count badge, re-rendering live as the form changes via Form.useWatch. Empty plan shows a placeholder. Purely form-local, no backend change. (v0.6 ticket 09)
This commit is contained in:
parent
f4db90a989
commit
b295f7370d
@ -17,6 +17,7 @@ import {
|
||||
import { passRateColor } from '../utils/colors'
|
||||
import { shortDateTime } from '../utils/date'
|
||||
import { deriveTimeScale, acceleratedDuration, formatScale } from '../utils/campaignTime'
|
||||
import WindowTimeline, { type TimelineMarker } from '../components/WindowTimeline'
|
||||
import { useResource } from '../hooks/useResource'
|
||||
import { usePolling } from '../hooks/usePolling'
|
||||
import { useTabStore } from '../stores/tabStore'
|
||||
@ -99,6 +100,25 @@ export default function CampaignsPage() {
|
||||
const campaigns = data?.campaigns ?? []
|
||||
const targets = data?.targets ?? []
|
||||
const scenarios = data?.scenarios ?? []
|
||||
const scenarioNames = Object.fromEntries(scenarios.map((s) => [s.id, s.name]))
|
||||
|
||||
const wPlan = (Form.useWatch('plan', form) as PlanFormEntry[] | undefined) ?? []
|
||||
const planMarkers: TimelineMarker[] = wPlan
|
||||
.map((e, i) => ({ e, i }))
|
||||
.filter(({ e }) => e && e.scenario_id)
|
||||
.map(({ e, i }) => {
|
||||
const name = scenarioNames[e.scenario_id as string] ?? (e.scenario_id as string).slice(0, 8)
|
||||
const count = e.count ?? 1
|
||||
const offsetHours = e.offset_hours ?? 0
|
||||
return {
|
||||
key: `plan-${i}`,
|
||||
offsetSeconds: offsetHours * 3600,
|
||||
colorKey: e.scenario_id as string,
|
||||
label: name,
|
||||
badge: count > 1 ? `×${count}` : undefined,
|
||||
tooltip: `${name} · +${offsetHours}h · ${count} 次`,
|
||||
}
|
||||
})
|
||||
|
||||
// Poll the list while this tab is active and a campaign is still working —
|
||||
// compressed dev-line campaigns change fast. Stop once all are terminal.
|
||||
@ -412,6 +432,25 @@ export default function CampaignsPage() {
|
||||
{acceleratedDuration(wWindow, derivedScale)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 4, color: colors.textSecondary }}>计划预览</div>
|
||||
<div style={{
|
||||
border: `1px solid ${colors.border}`, borderRadius: 8,
|
||||
padding: '12px 8px', marginBottom: 12,
|
||||
}}>
|
||||
{planMarkers.length > 0 ? (
|
||||
<WindowTimeline
|
||||
windowSeconds={wWindow}
|
||||
markers={planMarkers}
|
||||
colorLabels={scenarioNames}
|
||||
showLegend
|
||||
/>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', color: colors.textMuted, padding: '12px 0' }}>
|
||||
选择场景并设置偏移后,将在此按时间轴预览计划
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 8, color: colors.textSecondary }}>活动计划(在窗口的哪些时段跑哪个场景、跑几次)</div>
|
||||
<Form.List name="plan" rules={[{ validator: async (_, plan) => { if (!plan || plan.length < 1) return Promise.reject(new Error('至少一条计划条目')) } }]}>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user