Archived
Feat/activity l3 detail #1
@@ -709,30 +709,27 @@
|
|||||||
.g-actid .g-dh .chips > * { animation: none; }
|
.g-actid .g-dh .chips > * { animation: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ══ 二次确认改气泡弹层(2026-08-26 用户定:不要瘫在 box 下方的确认带)══
|
/* ══ 二次确认·轻气泡(2026-08-26 用户定:只问一句 + 确认/取消,不摆影响三必答)══
|
||||||
锚在页头右下、小三角指回触发钮;三必答从三栏收成竖排三行;
|
锚在页头右下、小三角指回触发钮;点气泡外的透明罩即取消;
|
||||||
阴影只给弹层(§10),危险确认沿用非实心 danger 钮(§11),进场 180ms 从触发点展开(§13) */
|
阴影只给弹层(§10),确认钮 danger 非实心(§11),进场 180ms 从触发点展开(§13) */
|
||||||
/* 注意:`.g-actvd/.g-actid > *` 的入场动画 forwards 填充会让每个区块长期保持独立层叠上下文,
|
/* 正 z-index 必须有:`.g-actvd/.g-actid > *` 的入场动画 forwards 填充会让每个区块长期保持独立层叠上下文,
|
||||||
气泡的 z-index 只在页头内部作数 —— 所以页头容器自己要有正 z-index,气泡才盖得住后面的区块 */
|
没有它,气泡的 z-index 只在页头内部作数,盖不住后面的区块 */
|
||||||
.g-actvd .g-dhwrap, .g-actid .g-dhwrap { position: relative; z-index: 30; }
|
.g-actvd .g-pahead, .g-actid .g-pahead { position: relative; z-index: 30; }
|
||||||
.g-actvd .g-ask, .g-actid .g-ask { position: absolute; top: calc(100% + 10px); right: 0; z-index: 40;
|
.g-actvd .g-popask-veil, .g-actid .g-popask-veil { position: fixed; inset: 0; z-index: 39; }
|
||||||
width: 372px; padding: 13px 15px 12px; gap: 10px;
|
.g-actvd .g-popask, .g-actid .g-popask { position: absolute; top: calc(100% + 10px); right: 0; z-index: 40;
|
||||||
|
width: 264px; padding: 13px 14px 12px; background: #fff;
|
||||||
border: 1px solid var(--tone-line, var(--cline)); border-radius: 12px;
|
border: 1px solid var(--tone-line, var(--cline)); border-radius: 12px;
|
||||||
box-shadow: 0 12px 32px rgba(22, 26, 34, .14), 0 2px 8px rgba(22, 26, 34, .08);
|
box-shadow: 0 12px 32px rgba(22, 26, 34, .14), 0 2px 8px rgba(22, 26, 34, .08);
|
||||||
animation: actvd-ask .18s cubic-bezier(.3, 0, .2, 1) both; transform-origin: top right; }
|
animation: actvd-ask .18s cubic-bezier(.3, 0, .2, 1) both; transform-origin: top right; }
|
||||||
@keyframes actvd-ask { from { opacity: 0; transform: translateY(-4px) scale(.97); } }
|
@keyframes actvd-ask { from { opacity: 0; transform: translateY(-4px) scale(.97); } }
|
||||||
/* 小三角:指回右上方的触发钮(左/上两条边旋转 45° 拼出) */
|
/* 小三角:指回右上方的触发钮(左/上两条边旋转 45° 拼出) */
|
||||||
.g-actvd .g-ask::before, .g-actid .g-ask::before { content: ""; position: absolute; top: -6px; right: 60px;
|
.g-actvd .g-popask::before, .g-actid .g-popask::before { content: ""; position: absolute; top: -6px; right: 60px;
|
||||||
width: 10px; height: 10px; background: #fff; transform: rotate(45deg);
|
width: 10px; height: 10px; background: #fff; transform: rotate(45deg);
|
||||||
border-left: 1px solid var(--tone-line, var(--cline)); border-top: 1px solid var(--tone-line, var(--cline)); }
|
border-left: 1px solid var(--tone-line, var(--cline)); border-top: 1px solid var(--tone-line, var(--cline)); }
|
||||||
/* 三必答竖排:标签定宽左、内容右,气泡窄也读得顺 */
|
.g-actvd .g-popask p, .g-actid .g-popask p { margin: 0; font-size: 13px; font-weight: 600; line-height: 1.55; color: var(--ink); }
|
||||||
.g-actvd .g-impact, .g-actid .g-impact { grid-template-columns: 1fr; border-radius: 8px; }
|
.g-actvd .g-popask-a, .g-actid .g-popask-a { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
||||||
.g-actvd .g-impact > div, .g-actid .g-impact > div { flex-direction: row; align-items: baseline; gap: 10px; padding: 7px 10px; }
|
|
||||||
.g-actvd .g-impact em, .g-actid .g-impact em { flex: none; width: 52px; }
|
|
||||||
.g-actvd .g-impact b, .g-actid .g-impact b { font-weight: 500; }
|
|
||||||
.g-actvd .g-ask-a .g-btn, .g-actid .g-ask-a .g-btn { height: 28px; }
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.g-actvd .g-ask, .g-actid .g-ask { animation: none; }
|
.g-actvd .g-popask, .g-actid .g-popask { animation: none; }
|
||||||
}
|
}
|
||||||
.g-fcard .mbtn { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--ink-4); cursor: pointer; }
|
.g-fcard .mbtn { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--ink-4); cursor: pointer; }
|
||||||
.g-fcard .mbtn:hover, .g-fcard .mbtn.on { border-color: var(--cline); background: #f3f6fa; color: var(--ink); }
|
.g-fcard .mbtn:hover, .g-fcard .mbtn.on { border-color: var(--cline); background: #f3f6fa; color: var(--ink); }
|
||||||
|
|||||||
@@ -283,6 +283,36 @@ function inTimeRange(at: string, from: string, to: string): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 轻量二次确认的规格(2026-08-26 用户定:只问一句 + 确认/取消 —— 影响三必答(AskBand)是治理台重度动作的规格,禁用活动/终止实例是轻动作,不摆) */
|
||||||
|
type PopAskSpec = { title: string; okLabel: string; danger?: boolean; onOk: () => void };
|
||||||
|
|
||||||
|
/** 气泡式二次确认 —— 锚在页头右下的小泡泡:一句话问清,确认/取消两钮;点气泡外的透明罩即取消 */
|
||||||
|
function PopAsk({ spec, onCancel }: { spec: PopAskSpec; onCancel: () => void }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="g-popask-veil" onClick={onCancel} />
|
||||||
|
<div className={'g-popask' + (spec.danger === true ? ' t-danger' : '')} role="alertdialog" aria-label={spec.title}>
|
||||||
|
<p>{spec.title}</p>
|
||||||
|
<div className="g-popask-a">
|
||||||
|
<button type="button" className="g-rowbtn" onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={'g-rowbtn' + (spec.danger === true ? ' danger' : '')}
|
||||||
|
onClick={() => {
|
||||||
|
spec.onOk();
|
||||||
|
onCancel();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{spec.okLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* L2 · 活动详情 —— 两个问题一屏答:「这个活动是什么」(定义,只读)
|
* L2 · 活动详情 —— 两个问题一屏答:「这个活动是什么」(定义,只读)
|
||||||
* 与「它跑得怎么样」(小统计 + 实例清单,点行进 L3)。
|
* 与「它跑得怎么样」(小统计 + 实例清单,点行进 L3)。
|
||||||
@@ -309,6 +339,8 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
|||||||
const [fState, setFState] = useState<InstanceState | ''>('');
|
const [fState, setFState] = useState<InstanceState | ''>('');
|
||||||
const [fFrom, setFFrom] = useState('');
|
const [fFrom, setFFrom] = useState('');
|
||||||
const [fTo, setFTo] = useState('');
|
const [fTo, setFTo] = useState('');
|
||||||
|
/* 轻量二次确认(2026-08-26 用户定):一句话 + 确认/取消的气泡,不走 AskBand 三必答 */
|
||||||
|
const [ask, setAsk] = useState<PopAskSpec | null>(null);
|
||||||
if (a === undefined) return <Note tone="warn">活动不存在 —— 它可能已被调整,返回活动清单看看。</Note>;
|
if (a === undefined) return <Note tone="warn">活动不存在 —— 它可能已被调整,返回活动清单看看。</Note>;
|
||||||
const st = status ?? a.status;
|
const st = status ?? a.status;
|
||||||
const instances = rows ?? demoInstances(a.owner);
|
const instances = rows ?? demoInstances(a.owner);
|
||||||
@@ -354,64 +386,66 @@ export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'g-actvd' + (a.hue === 'blue' ? '' : ` h-${a.hue}`)}>
|
<div className={'g-actvd' + (a.hue === 'blue' ? '' : ` h-${a.hue}`)}>
|
||||||
<PageHead
|
<div className="g-pahead">
|
||||||
ico={a.ico}
|
<PageHead
|
||||||
kicker={`${a.biz} · ${a.id}`}
|
ico={a.ico}
|
||||||
title={a.name}
|
kicker={`${a.biz} · ${a.id}`}
|
||||||
chips={
|
title={a.name}
|
||||||
<>
|
chips={
|
||||||
<Chip tone={st === 'active' ? 'ok' : 'mute'}>{st === 'active' ? '激活中' : '已禁用'}</Chip>
|
<>
|
||||||
{a.version !== undefined && <Chip ico="tag">v{a.version}</Chip>}
|
<Chip tone={st === 'active' ? 'ok' : 'mute'}>{st === 'active' ? '激活中' : '已禁用'}</Chip>
|
||||||
{/* 绑定的 LLM 上头版(2026-08-25 用户定):每个活动一个专属 agent */}
|
{a.version !== undefined && <Chip ico="tag">v{a.version}</Chip>}
|
||||||
<Chip ico="cpu">{a.llm}</Chip>
|
{/* 绑定的 LLM 上头版(2026-08-25 用户定):每个活动一个专属 agent */}
|
||||||
<Chip ico="user">{a.owner}</Chip>
|
<Chip ico="cpu">{a.llm}</Chip>
|
||||||
<Chip ico="calendar-days">创建于 {a.createdAt}</Chip>
|
<Chip ico="user">{a.owner}</Chip>
|
||||||
</>
|
<Chip ico="calendar-days">创建于 {a.createdAt}</Chip>
|
||||||
}
|
</>
|
||||||
desc="活动定义只读 —— 输入与输出在创建时定死,演进走版本迭代;验收在工作台,这里只看状态。"
|
}
|
||||||
acts={[
|
desc="活动定义只读 —— 输入与输出在创建时定死,演进走版本迭代;验收在工作台,这里只看状态。"
|
||||||
st === 'active'
|
acts={[
|
||||||
? {
|
st === 'active'
|
||||||
label: '禁用活动',
|
? {
|
||||||
ico: 'power',
|
label: '禁用活动',
|
||||||
danger: true,
|
ico: 'power',
|
||||||
ask: {
|
danger: true,
|
||||||
title: '确认禁用这个活动?',
|
onClick: () =>
|
||||||
note: '禁用后不能再为它创建实例;已在跑的实例不受影响。',
|
setAsk({
|
||||||
impact: { who: '该活动及其后续实例创建', undo: '可恢复 —— 随时重新激活', when: '确认后立即生效(本期前端演示,接口对接后落库)' },
|
title: '确认禁用这个活动?',
|
||||||
okLabel: '确认禁用',
|
okLabel: '确认禁用',
|
||||||
onOk: () => {
|
danger: true,
|
||||||
/* 规则(2026-08-25 用户定):有实例时接口会提示无法禁用 —— 本期前端先演出这个拦截 */
|
onOk: () => {
|
||||||
if (a.instances > 0) {
|
/* 规则(2026-08-25 用户定):有实例时接口会提示无法禁用 —— 本期前端先演出这个拦截 */
|
||||||
setBlocked(true);
|
if (a.instances > 0) {
|
||||||
} else {
|
setBlocked(true);
|
||||||
setStatus('disabled');
|
} else {
|
||||||
setBlocked(false);
|
setStatus('disabled');
|
||||||
}
|
setBlocked(false);
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label: '激活活动',
|
||||||
|
ico: 'zap',
|
||||||
|
primary: true,
|
||||||
|
onClick: () =>
|
||||||
|
setAsk({
|
||||||
|
title: '确认激活这个活动?',
|
||||||
|
okLabel: '确认激活',
|
||||||
|
onOk: () => {
|
||||||
|
setStatus('active');
|
||||||
|
setBlocked(false);
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}
|
{ label: '启动活动', ico: 'plus', disabled: true },
|
||||||
: {
|
/* 版本迭代入口先隐藏(2026-08-25 用户定):功能设计回头讨论,要做时把这行加回 acts
|
||||||
label: '激活活动',
|
{ label: '版本迭代', ico: 'git-branch', disabled: true },
|
||||||
ico: 'zap',
|
*/
|
||||||
primary: true,
|
]}
|
||||||
ask: {
|
/>
|
||||||
title: '确认激活这个活动?',
|
{ask !== null && <PopAsk spec={ask} onCancel={() => setAsk(null)} />}
|
||||||
note: '激活后可以为它创建实例。',
|
</div>
|
||||||
impact: { who: '该活动及其后续实例创建', undo: '可逆 —— 随时可以再次禁用', when: '确认后立即生效(本期前端演示,接口对接后落库)' },
|
|
||||||
okLabel: '确认激活',
|
|
||||||
onOk: () => {
|
|
||||||
setStatus('active');
|
|
||||||
setBlocked(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ label: '启动活动', ico: 'plus', disabled: true },
|
|
||||||
/* 版本迭代入口先隐藏(2026-08-25 用户定):功能设计回头讨论,要做时把这行加回 acts
|
|
||||||
{ label: '版本迭代', ico: 'git-branch', disabled: true },
|
|
||||||
*/
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
{blocked && (
|
{blocked && (
|
||||||
<Note tone="warn">
|
<Note tone="warn">
|
||||||
无法禁用:该活动还有 {a.instances} 个实例 —— 这条提示将来由接口返回(有实例时接口拒绝禁用),本期为前端占位演示。
|
无法禁用:该活动还有 {a.instances} 个实例 —— 这条提示将来由接口返回(有实例时接口拒绝禁用),本期为前端占位演示。
|
||||||
@@ -735,6 +769,8 @@ export function ActiPage({ id }: { id: string }) {
|
|||||||
/* 状态就地翻转(本期前端演示):终止暂落「失败」(归态接口语义未定),对接后走 mutation 由服务端重投影;
|
/* 状态就地翻转(本期前端演示):终止暂落「失败」(归态接口语义未定),对接后走 mutation 由服务端重投影;
|
||||||
term 记下终止时刻 —— 终止的过程面与「自己跑挂」不同(当前步记「已终止」,不是「附件缺失」那种失败原因) */
|
term 记下终止时刻 —— 终止的过程面与「自己跑挂」不同(当前步记「已终止」,不是「附件缺失」那种失败原因) */
|
||||||
const [term, setTerm] = useState<string | undefined>(undefined);
|
const [term, setTerm] = useState<string | undefined>(undefined);
|
||||||
|
/* 轻量二次确认(2026-08-26 用户定):一句话 + 确认/取消的气泡 */
|
||||||
|
const [ask, setAsk] = useState<PopAskSpec | null>(null);
|
||||||
if (a === undefined || inst === undefined) {
|
if (a === undefined || inst === undefined) {
|
||||||
return <Note tone="warn">实例不存在 —— 它不在该活动的清单里,从面包屑返回活动详情看看。</Note>;
|
return <Note tone="warn">实例不存在 —— 它不在该活动的清单里,从面包屑返回活动详情看看。</Note>;
|
||||||
}
|
}
|
||||||
@@ -754,7 +790,8 @@ export function ActiPage({ id }: { id: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'g-actid' + (a.hue === 'blue' ? '' : ` h-${a.hue}`)}>
|
<div className={'g-actid' + (a.hue === 'blue' ? '' : ` h-${a.hue}`)}>
|
||||||
<PageHead
|
<div className="g-pahead">
|
||||||
|
<PageHead
|
||||||
ico={a.ico}
|
ico={a.ico}
|
||||||
tone={INSTANCE_ST[state]}
|
tone={INSTANCE_ST[state]}
|
||||||
kicker={`${a.name} · 一次运行`}
|
kicker={`${a.name} · 一次运行`}
|
||||||
@@ -774,25 +811,27 @@ export function ActiPage({ id }: { id: string }) {
|
|||||||
}
|
}
|
||||||
desc="一次运行的完整视图:输入 / 过程 / 输出 / 状态 —— 实例不可编辑、不可删除,进行中的运行可终止。"
|
desc="一次运行的完整视图:输入 / 过程 / 输出 / 状态 —— 实例不可编辑、不可删除,进行中的运行可终止。"
|
||||||
acts={[
|
acts={[
|
||||||
/* 唯一动作(2026-08-26 用户定:实例不许删、不许编):进行中可终止,二次确认 */
|
/* 唯一动作(2026-08-26 用户定:实例不许删、不许编):进行中可终止,轻气泡二次确认 */
|
||||||
...(state === '进行中'
|
...(state === '进行中'
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: '终止活动',
|
label: '终止活动',
|
||||||
ico: 'power',
|
ico: 'power',
|
||||||
danger: true,
|
danger: true,
|
||||||
ask: {
|
onClick: () =>
|
||||||
title: `确认终止实例 ${inst.id}?`,
|
setAsk({
|
||||||
note: '进行中的步骤就地停下,不再产生后续动作;已经走完的步骤留痕保留。',
|
title: `确认终止实例 ${inst.id}?`,
|
||||||
impact: { who: '该实例当前与后续的全部动作', undo: '不可恢复 —— 只能重新启动一次新运行', when: '确认后立即生效(本期前端演示,接口对接后落库)' },
|
okLabel: '确认终止',
|
||||||
okLabel: '确认终止',
|
danger: true,
|
||||||
onOk: () => setTerm(fmtMinute(Date.now())),
|
onOk: () => setTerm(fmtMinute(Date.now())),
|
||||||
},
|
}),
|
||||||
} satisfies ActSpec,
|
} satisfies ActSpec,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
{ask !== null && <PopAsk spec={ask} onCancel={() => setAsk(null)} />}
|
||||||
|
</div>
|
||||||
{reason !== undefined && <Note tone="warn">{reason}</Note>}
|
{reason !== undefined && <Note tone="warn">{reason}</Note>}
|
||||||
{state === '待执行' && <Note>待执行:已排入队列,等待调度 —— 到点或由负责人手动起步。</Note>}
|
{state === '待执行' && <Note>待执行:已排入队列,等待调度 —— 到点或由负责人手动起步。</Note>}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ L1(模块首页 · 活动清单)需要一个**标签页**切换,因为活
|
|||||||
- 2026-08-26 · **L2 历史活动加检索(用户定)**:列表上方检索条 —— 条件下拉四选(实例 / 负责人 / 状态 / 启动时间),控件随条件换:实例=按实例号模糊输入、负责人=人员下拉(候选取自现有实例,接接口后换人员接口)、状态=五态下拉、启动时间=日期段(含两端整日);筛中显示「筛出 X / 共 Y」并可一键清空;分页按筛出的集合走。
|
- 2026-08-26 · **L2 历史活动加检索(用户定)**:列表上方检索条 —— 条件下拉四选(实例 / 负责人 / 状态 / 启动时间),控件随条件换:实例=按实例号模糊输入、负责人=人员下拉(候选取自现有实例,接接口后换人员接口)、状态=五态下拉、启动时间=日期段(含两端整日);筛中显示「筛出 X / 共 Y」并可一键清空;分页按筛出的集合走。
|
||||||
- 2026-08-26 · **L3 实例详情已落地(静态占位版,用户确认规划后实施)**:路由 `acti`(id = `活动id:实例号`),L2 行内「查看详情」接入。布局 = 主从 2:1(DESIGN-SPEC §9):左侧「动作过程记录」竖时间线是主角(五步节点流:输入接收 → 要素结构化 → 定价测算 → **负责人验收** → 输出归档 —— 验收永远留给人,对应人机分工),当前步呼吸高亮、未到达空心灰显、连接线成不间断竖轨;右栏输入(键值)/ 输出(文件行,下载随接口开放)。头部 = 状态丸 + 负责人/启动/结束·耗时/专属 LLM;驳回/失败出原因说明块;终止后的过程面与「自己跑挂」区分:当前步记「已终止」而非失败原因。数据仍是静态假数据。
|
- 2026-08-26 · **L3 实例详情已落地(静态占位版,用户确认规划后实施)**:路由 `acti`(id = `活动id:实例号`),L2 行内「查看详情」接入。布局 = 主从 2:1(DESIGN-SPEC §9):左侧「动作过程记录」竖时间线是主角(五步节点流:输入接收 → 要素结构化 → 定价测算 → **负责人验收** → 输出归档 —— 验收永远留给人,对应人机分工),当前步呼吸高亮、未到达空心灰显、连接线成不间断竖轨;右栏输入(键值)/ 输出(文件行,下载随接口开放)。头部 = 状态丸 + 负责人/启动/结束·耗时/专属 LLM;驳回/失败出原因说明块;终止后的过程面与「自己跑挂」区分:当前步记「已终止」而非失败原因。数据仍是静态假数据。
|
||||||
- 2026-08-26 · **实例不可删除、不可编辑(用户定,推翻 L3 原规划)**:L3 的编辑/删除入口撤掉,唯一动作是进行中的「终止活动」(二次确认);六功能里实例的「编辑/删除」两项就此划掉,待确认问题里的两条(编辑指什么 / 删除是否放开)同时关闭。
|
- 2026-08-26 · **实例不可删除、不可编辑(用户定,推翻 L3 原规划)**:L3 的编辑/删除入口撤掉,唯一动作是进行中的「终止活动」(二次确认);六功能里实例的「编辑/删除」两项就此划掉,待确认问题里的两条(编辑指什么 / 删除是否放开)同时关闭。
|
||||||
|
- 2026-08-26 · **二次确认的规格(用户定)**:活动模块的确认走**轻气泡**(锚在触发钮下方的小泡泡,只问一句 + 确认/取消,点罩即取消)—— 不摆影响三必答;三必答(AskBand)是治理台重度动作的规格,别处的确认带暂不动。
|
||||||
|
|
||||||
## 三、功能表(整理稿)
|
## 三、功能表(整理稿)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user