(function () {
  'use strict';
  const NS = (window.CreditsAdmin = window.CreditsAdmin || {});
  const { useState, useRef, useEffect } = React;

/* projects/credits-admin/prototype.jsx
 * 积分发放的运营后台 · 可交互原型
 * 需求来源：办公小浣熊 2.0（补充版）「（5）运营后台配置要求」
 *
 * 交互范围：两组「二选一」Tab 真实切换、账号类/行为类事件切换、选项联动置灰、
 * 列表 → 详情 → 表单跳转、手动发放二次确认、启用开关。
 * 文本输入不做真实录入，改用预置示例 chip 切换（足以驱动右侧用户端预览联动）。
 */
/* global React */

const C = {
  brand: '#8E6BF2', brandSoft: '#F3EFFD',
  text: '#0A0A06', sub: '#57574A', mute: '#8A8A80',
  line: 'rgba(10,10,6,0.09)', lineSoft: 'rgba(10,10,6,0.055)',
  bg: '#F6F6F4', card: '#FFFFFF',
  green: '#12A05C', greenSoft: '#E7F6EE',
  amber: '#B26A00', amberSoft: '#FDF1DF',
  grayChip: '#F1F1EE',
};
const FONT = '"Inter","PingFang SC",-apple-system,system-ui,sans-serif';
const DISABLED = { opacity: 0.42, pointerEvents: 'none' };

/* ═════════════ 基础控件（可交互） ═════════════ */

function Btn({ children, kind = 'ghost', size = 'md', onClick, style, disabled }) {
  const [hover, setHover] = useState(false);
  const pad = size === 'sm' ? '5px 11px' : '8px 16px';
  const map = {
    primary: { background: hover ? '#7E58EE' : C.brand, color: '#fff', border: '1px solid transparent' },
    ghost: { background: hover ? '#F7F7F5' : '#fff', color: C.sub, border: '1px solid ' + C.line },
    soft: { background: hover ? '#EBE4FC' : C.brandSoft, color: C.brand, border: '1px solid rgba(142,107,242,0.22)' },
    danger: { background: hover ? '#FDF1DF' : '#fff', color: C.amber, border: '1px solid rgba(178,106,0,0.28)' },
  };
  return (
    <button type="button" disabled={disabled} onClick={onClick}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
        borderRadius: 7, padding: pad, fontSize: size === 'sm' ? 12 : 13, fontWeight: 600,
        fontFamily: FONT, cursor: disabled ? 'not-allowed' : 'pointer', whiteSpace: 'nowrap',
        transition: 'background .12s', ...map[kind], ...(disabled ? { opacity: 0.5 } : null), ...style,
      }}>{children}</button>);
}

function Pill({ children, tone = 'gray' }) {
  const map = {
    gray: { background: C.grayChip, color: C.mute },
    green: { background: C.greenSoft, color: C.green },
    amber: { background: C.amberSoft, color: C.amber },
    brand: { background: C.brandSoft, color: C.brand },
  };
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 5, padding: '3px 9px',
      borderRadius: 20, fontSize: 11.5, fontWeight: 600, whiteSpace: 'nowrap', ...map[tone],
    }}>{children}</span>);
}

function Dot({ on }) {
  return <span style={{ width: 6, height: 6, borderRadius: 6, background: on ? C.green : '#C4C4BC', display: 'inline-block' }} />;
}

function Switch({ on, onClick, disabled }) {
  return (
    <span onClick={disabled ? null : onClick} title={disabled ? '活动已结束，开关不可再操作' : null} style={{
      width: 34, height: 20, borderRadius: 20,
      background: disabled ? '#EAEAE4' : (on ? C.brand : '#D8D8D2'),
      display: 'inline-flex', alignItems: 'center', padding: 2, boxSizing: 'border-box',
      justifyContent: on ? 'flex-end' : 'flex-start', cursor: disabled ? 'not-allowed' : 'pointer',
      transition: 'background .16s', flexShrink: 0, opacity: disabled ? 0.6 : 1,
    }}>
      <span style={{ width: 16, height: 16, borderRadius: 16, background: '#fff', boxShadow: '0 1px 2px rgba(0,0,0,0.18)' }} />
    </span>);
}

function Radio({ on, children, onClick }) {
  return (
    <span onClick={onClick} style={{
      display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13,
      color: on ? C.text : C.sub, cursor: 'pointer', userSelect: 'none',
    }}>
      <span style={{
        width: 14, height: 14, borderRadius: 14, boxSizing: 'border-box', flexShrink: 0,
        border: '1.5px solid ' + (on ? C.brand : '#CFCFC8'),
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
      }}>
        {on && <span style={{ width: 7, height: 7, borderRadius: 7, background: C.brand }} />}
      </span>
      {children}
    </span>);
}

function Check({ on, children, onClick }) {
  return (
    <span onClick={onClick} style={{
      display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 13,
      color: on ? C.text : C.sub, cursor: 'pointer', userSelect: 'none',
    }}>
      <span style={{
        width: 14, height: 14, borderRadius: 4, boxSizing: 'border-box', flexShrink: 0,
        border: '1.5px solid ' + (on ? C.brand : '#CFCFC8'), background: on ? C.brand : '#fff',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
      }}>
        {on && <svg width="9" height="9" viewBox="0 0 10 10" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 5.2l2 2L8 3" /></svg>}
      </span>
      {children}
    </span>);
}

/* 只读输入框（原型不做真实录入） */
function Input({ value, placeholder, w = 260, suffix, icon }) {
  return (
    <span title="原型中文本输入不可编辑" style={{
      display: 'inline-flex', alignItems: 'center', gap: 6, width: w, boxSizing: 'border-box',
      height: 32, padding: '0 10px', borderRadius: 7, background: '#fff',
      border: '1px solid ' + C.line, fontSize: 13, color: value ? C.text : '#B4B4AC',
    }}>
      {icon}
      <span style={{ flex: 1, overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>{value || placeholder}</span>
      {suffix && <span style={{ fontSize: 12, color: C.mute }}>{suffix}</span>}
    </span>);
}

/* 真实可选的下拉 */
function Select({ value, options = [], onChange, w = 180, placeholder }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const off = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('pointerdown', off, true);
    return () => document.removeEventListener('pointerdown', off, true);
  }, [open]);
  return (
    <span ref={ref} style={{ position: 'relative', display: 'inline-block', width: w }}>
      <span onClick={() => options.length && setOpen(o => !o)} style={{
        display: 'inline-flex', alignItems: 'center', width: '100%', boxSizing: 'border-box',
        height: 32, padding: '0 10px', borderRadius: 7, background: '#fff',
        border: '1px solid ' + (open ? C.brand : C.line), fontSize: 13,
        color: value ? C.text : '#B4B4AC', cursor: options.length ? 'pointer' : 'default',
      }}>
        <span style={{ flex: 1, overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>{value || placeholder}</span>
        <svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="#9A9A90" strokeWidth="1.8" strokeLinecap="round"
          style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .14s' }}><path d="M2 4l4 4 4-4" /></svg>
      </span>
      {open && (
        <div style={{
          position: 'absolute', top: '100%', left: 0, marginTop: 4, minWidth: '100%', zIndex: 40,
          background: '#fff', borderRadius: 9, padding: 4, boxSizing: 'border-box',
          boxShadow: '0 10px 30px rgba(0,0,0,0.14), 0 0 0 1px rgba(10,10,6,0.06)',
        }}>
          {options.map(o => (
            <div key={o} onClick={() => { onChange && onChange(o); setOpen(false); }} style={{
              padding: '7px 9px', borderRadius: 6, fontSize: 12.5, cursor: 'pointer',
              background: o === value ? C.brandSoft : 'transparent',
              color: o === value ? C.brand : C.sub, fontWeight: o === value ? 600 : 400, whiteSpace: 'nowrap',
            }}>{o}</div>
          ))}
        </div>
      )}
    </span>);
}

/* 日期时间选择器：不选 = 不设置，选中即设置，随时可清空。
 * 值格式统一为 'YYYY-MM-DD HH:mm'，空串代表未设置。 */
const WEEK_CN = ['一', '二', '三', '四', '五', '六', '日'];
const pad2 = (n) => (n < 10 ? '0' : '') + n;

function parseDT(v) {
  const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/.exec(v || '');
  if (!m) return null;
  return { y: +m[1], mo: +m[2], d: +m[3], h: +m[4], mi: +m[5] };
}

function DateTimePicker({ value, onChange, placeholder = '不设置', w = 190 }) {
  const [open, setOpen] = useState(false);
  const parsed = parseDT(value);
  const today = new Date();
  const [view, setView] = useState(() => (parsed
    ? { y: parsed.y, mo: parsed.mo }
    : { y: today.getFullYear(), mo: today.getMonth() + 1 }));
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const off = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('pointerdown', off, true);
    return () => document.removeEventListener('pointerdown', off, true);
  }, [open]);
  useEffect(() => { if (open && parsed) setView({ y: parsed.y, mo: parsed.mo }); }, [open]);

  const emit = (d) => onChange && onChange(d);
  const time = parsed ? { h: parsed.h, mi: parsed.mi } : { h: 0, mi: 0 };
  const pick = (day) => emit(view.y + '-' + pad2(view.mo) + '-' + pad2(day) + ' ' + pad2(time.h) + ':' + pad2(time.mi));
  const setTime = (h, mi) => {
    const base = parsed || { y: view.y, mo: view.mo, d: today.getDate() };
    emit(base.y + '-' + pad2(base.mo) + '-' + pad2(base.d) + ' ' + pad2(h) + ':' + pad2(mi));
  };
  const shift = (n) => setView(v => {
    let mo = v.mo + n, y = v.y;
    if (mo < 1) { mo = 12; y -= 1; }
    if (mo > 12) { mo = 1; y += 1; }
    return { y, mo };
  });

  const first = new Date(view.y, view.mo - 1, 1);
  const lead = (first.getDay() + 6) % 7;               // 周一为一周起点
  const days = new Date(view.y, view.mo, 0).getDate();
  const cells = [];
  for (let i = 0; i < lead; i++) cells.push(null);
  for (let d = 1; d <= days; d++) cells.push(d);

  const navBtn = (label, onClick) => (
    <span onClick={onClick} style={{
      width: 22, height: 22, borderRadius: 6, display: 'inline-flex', alignItems: 'center',
      justifyContent: 'center', cursor: 'pointer', fontSize: 12, color: C.sub, userSelect: 'none',
    }}>{label}</span>
  );

  return (
    <span ref={ref} style={{ position: 'relative', display: 'inline-block', width: w }}>
      <span onClick={() => setOpen(o => !o)} style={{
        display: 'inline-flex', alignItems: 'center', gap: 6, width: '100%', boxSizing: 'border-box',
        height: 32, padding: '0 9px', borderRadius: 7, background: '#fff', cursor: 'pointer',
        border: '1px solid ' + (open ? C.brand : C.line), fontSize: 13,
        color: value ? C.text : '#B4B4AC',
      }}>
        <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke={value ? '#8A8A80' : '#C2C2BA'} strokeWidth="1.4" strokeLinecap="round">
          <rect x="2.2" y="3.2" width="11.6" height="10.6" rx="2" /><path d="M2.2 6.4h11.6M5.6 1.8v2.6M10.4 1.8v2.6" />
        </svg>
        <span style={{ flex: 1, overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>{value || placeholder}</span>
        {value && (
          <span onClick={(e) => { e.stopPropagation(); emit(''); setOpen(false); }} title="清空"
            style={{ display: 'inline-flex', color: '#B0B0A6', fontSize: 13, lineHeight: 1 }}>
            <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"><path d="M3 3l6 6M9 3l-6 6" /></svg>
          </span>
        )}
      </span>
      {open && (
        <div style={{
          position: 'absolute', top: '100%', left: 0, marginTop: 5, zIndex: 60, width: 236,
          background: '#fff', borderRadius: 10, padding: 10, boxSizing: 'border-box',
          boxShadow: '0 10px 30px rgba(0,0,0,0.14), 0 0 0 1px rgba(10,10,6,0.06)',
        }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
            {navBtn('‹', () => shift(-1))}
            <span style={{ fontSize: 12.5, color: C.text, fontWeight: 600 }}>{view.y} 年 {view.mo} 月</span>
            {navBtn('›', () => shift(1))}
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2 }}>
            {WEEK_CN.map(d => (
              <span key={d} style={{ textAlign: 'center', fontSize: 11, color: C.mute, lineHeight: '22px' }}>{d}</span>
            ))}
            {cells.map((d, i) => {
              const on = !!d && parsed && parsed.y === view.y && parsed.mo === view.mo && parsed.d === d;
              return (
                <span key={i} onClick={() => d && pick(d)} style={{
                  textAlign: 'center', lineHeight: '26px', fontSize: 12, borderRadius: 6,
                  cursor: d ? 'pointer' : 'default', userSelect: 'none',
                  background: on ? C.brand : 'transparent', color: on ? '#fff' : (d ? C.sub : 'transparent'),
                  fontWeight: on ? 600 : 400,
                }}>{d || '·'}</span>
              );
            })}
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 9, paddingTop: 9, borderTop: '1px solid ' + C.lineSoft }}>
            <span style={{ fontSize: 12, color: C.mute }}>时间</span>
            <select value={time.h} onChange={(e) => setTime(+e.target.value, time.mi)}
              style={{ height: 26, borderRadius: 6, border: '1px solid ' + C.line, fontSize: 12, color: C.text, background: '#fff' }}>
              {Array.from({ length: 24 }, (_, h) => <option key={h} value={h}>{pad2(h)}</option>)}
            </select>
            <span style={{ fontSize: 12, color: C.mute }}>:</span>
            <select value={time.mi} onChange={(e) => setTime(time.h, +e.target.value)}
              style={{ height: 26, borderRadius: 6, border: '1px solid ' + C.line, fontSize: 12, color: C.text, background: '#fff' }}>
              {Array.from({ length: 60 }, (_, m) => <option key={m} value={m}>{pad2(m)}</option>)}
            </select>
            <span style={{ flex: 1 }} />
            <span onClick={() => { emit(''); setOpen(false); }} style={{ fontSize: 12, color: C.mute, cursor: 'pointer' }}>清空</span>
            <span onClick={() => setOpen(false)} style={{ fontSize: 12, color: C.brand, fontWeight: 600, cursor: 'pointer' }}>确定</span>
          </div>
        </div>
      )}
    </span>);
}

/* 表单行：左标签 + 右内容 + 可选说明。
 * 标签只与「控件那一行」对齐，补充说明不参与垂直居中计算 ——
 * 整行始终 flex-start，控件行统一 32px 行高，标签用等高的 lineHeight 与之对齐。
 * align="top" 用于内容本身是多行卡片 / 表格的场景，标签贴首行顶部。 */
const ROW_H = 32;

function Field({ label, required, hint, children, align = 'center', dim, pad = 0 }) {
  const top = align === 'top';
  return (
    <div style={{ display: 'flex', gap: 16, padding: '11px 0', alignItems: 'flex-start', ...(dim ? { opacity: 0.5 } : null) }}>
      {/* 标签行高恒等于控件行高，靠 line-height 让文字光学居中于首行；
          不要用 padding 顶下去 —— 那样盒子中心对上了，文字中心反而是歪的。 */}
      <div style={{
        width: 116, flexShrink: 0, textAlign: 'right', fontSize: 13, color: C.sub,
        lineHeight: ROW_H + 'px', paddingTop: pad,
      }}>
        {required && <span style={{ color: '#E0533D', marginRight: 3 }}>*</span>}{label}
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        {top ? children : (
          <div style={{ minHeight: ROW_H, display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>{children}</div>
        )}
        {hint && <div style={{ fontSize: 11.5, color: C.mute, marginTop: 6, lineHeight: 1.6 }}>{hint}</div>}
      </div>
    </div>);
}

function Group({ index, title, desc, children, id }) {
  return (
    <div id={id} style={{ background: C.card, border: '1px solid ' + C.lineSoft, borderRadius: 12, padding: '16px 22px 18px', marginBottom: 14 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 9, marginBottom: 4 }}>
        <span style={{ width: 19, height: 19, borderRadius: 19, background: C.brandSoft, color: C.brand, fontSize: 11, fontWeight: 700, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>{index}</span>
        <span style={{ fontSize: 14, fontWeight: 700, color: C.text }}>{title}</span>
        {desc && <span style={{ fontSize: 12, color: C.mute }}>{desc}</span>}
      </div>
      <div style={{ borderTop: '1px solid ' + C.lineSoft, marginTop: 10, paddingTop: 2 }}>{children}</div>
    </div>);
}

function WayTabs({ items, active, onChange }) {
  return (
    <div style={{ display: 'inline-flex', gap: 4, padding: 3, background: '#F1F1EE', borderRadius: 9 }}>
      {items.map((t) => {
        const on = t === active;
        return (
          <span key={t} onClick={() => onChange && onChange(t)} style={{
            display: 'inline-flex', alignItems: 'center', gap: 6,
            padding: '5px 14px', borderRadius: 7, fontSize: 12.5, fontWeight: 600, cursor: 'pointer',
            background: on ? '#fff' : 'transparent', color: on ? C.brand : C.mute,
            border: '1px solid ' + (on ? C.brand : 'transparent'),
            boxShadow: on ? '0 1px 3px rgba(0,0,0,0.08)' : 'none', transition: 'background .12s',
          }}>
            <span style={{
              width: 13, height: 13, borderRadius: 13, boxSizing: 'border-box', flexShrink: 0,
              border: '1.5px solid ' + (on ? C.brand : '#CFCFC8'), background: on ? C.brand : '#fff',
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            }}>
              {on && <svg width="8" height="8" viewBox="0 0 10 10" fill="none" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 5.2l2 2L8 3" /></svg>}
            </span>
            {t}
          </span>);
      })}
    </div>);
}

/* 界面里只允许出现两类说明，且样式各有专属含义：
 *   gray  = 产品规则说明，面向运营，是后台上线后真实存在的文案
 *   amber = 风险警示，只用于不可逆操作与校验失败（发放不可撤销、结束不可恢复、无法启用）
 * 第三类「需求背景 / 设计取舍 / 原型导览」一律不进界面，收在右上角「设计说明」侧栏。 */
function Note({ children, tone = 'gray' }) {
  const bg = tone === 'amber' ? C.amberSoft : '#FAFAF8';
  const bd = tone === 'amber' ? 'rgba(178,106,0,0.18)' : C.lineSoft;
  const fg = tone === 'amber' ? C.amber : C.mute;
  return <div style={{ background: bg, border: '1px solid ' + bd, borderRadius: 8, padding: '6px 12px', fontSize: 12, lineHeight: 1.7, color: fg }}>{children}</div>;
}

/* 预置示例切换 chip（替代真实文本输入） */
function Presets({ label, options, value, onChange }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 7, flexWrap: 'wrap' }}>
      <span style={{ fontSize: 11, color: '#B0B0A6' }}>{label}</span>
      {options.map(o => (
        <span key={o} onClick={() => onChange(o)} style={{
          padding: '3px 9px', borderRadius: 20, fontSize: 11, cursor: 'pointer',
          border: '1px solid ' + (o === value ? 'rgba(142,107,242,0.4)' : C.line),
          background: o === value ? C.brandSoft : '#fff',
          color: o === value ? C.brand : C.mute, fontWeight: o === value ? 600 : 400,
        }}>{o}</span>
      ))}
    </div>);
}

/* ═════════════ 数据 ═════════════ */

const SEED = [
  { id: 'c1', name: '老用户回馈', crowdWay: 'filter', crowdText: '条件圈人 · T 之前注册', trigWay: 'event', evGroup: 'account', event: '登录', trigText: '事件触发 · 登录', amount: 3000, sent: '128406', hit: '131204', state: 'on', win: '08-06 12:00 → 11-06 12:00' },
  { id: 'c2', name: '新人注册礼包', crowdWay: 'filter', crowdText: '条件圈人 · 全量用户', trigWay: 'event', evGroup: 'account', event: '注册成功', trigText: '事件触发 · 注册成功', amount: 3000, sent: '9872', hit: '9872', state: 'on', win: '08-06 12:00 → 长期' },
  { id: 'c3', name: '桌面端登录奖励', crowdWay: 'filter', crowdText: '条件圈人 · 全量用户', trigWay: 'event', evGroup: 'account', event: '登录', trigText: '事件触发 · 登录 · 限桌面端', amount: 3000, sent: '4015', hit: '18330', state: 'on', win: '长期有效' },
  { id: 'c4', name: '数据分析首次体验奖', crowdWay: 'filter', crowdText: '条件圈人 · 免费版', trigWay: 'event', evGroup: 'behavior', event: '跑通数据分析', trigText: '事件触发 · 跑通数据分析', amount: 500, sent: '1204', hit: '62481', state: 'on', win: '立即生效 → 09-10 00:00' },
  { id: 'c8', name: '9 月开学季激励', crowdWay: 'filter', crowdText: '条件圈人 · 教育版', trigWay: 'event', evGroup: 'behavior', event: '生成 PPT', trigText: '事件触发 · 生成 PPT', amount: 1000, sent: '—', hit: '8420', state: 'on', win: '09-01 00:00 → 09-30 00:00', notStarted: true },
  { id: 'c5', name: '7 月故障补偿', crowdWay: 'roster', crowdText: '名单指定 · 上传 328 人', trigWay: 'direct', evGroup: 'account', event: '', trigText: '即时直发', amount: 2000, sent: '328', hit: '328', state: 'done', win: '一次性发放 · 07-28 15:00' },
  { id: 'c7', name: '7 月 PPT 体验激励', crowdWay: 'filter', crowdText: '条件圈人 · 免费版', trigWay: 'event', evGroup: 'behavior', event: '生成 PPT', trigText: '事件触发 · 生成 PPT', amount: 500, sent: '—', hit: '—', state: 'off', win: '07-01 00:00 → 07-31 00:00', expired: true },
  { id: 'c6', name: '流失付费用户召回', crowdWay: 'filter', crowdText: '条件圈人 · 90 天内购买过', trigWay: 'direct', evGroup: 'account', event: '', trigText: '即时直发', amount: 1500, sent: '—', hit: '12880', state: 'off', win: '一次性发放' },
];

const BEHAVIORS = [
  ['付费成功', '订单支付成功回调返回时，订阅与加油包均算'],
  ['生成 PPT', '生成成功并返回结果时'],
  ['跑通数据分析', '完整生成并返回数据分析报告时'],
  ['上传文件到知识库', '文件上传并解析入库成功时'],
  ['完成任务规划', '任务规划执行完成并返回结果时'],
  ['创建文档', '成功创建并保存文档时'],
];

/* 「登录」与原「指定端登录」是同一个事件：后者等于前者把端从「不限端」收紧，
 * 且「不限端」本就是端多选里的一个取值，两条并列纯属冗余，已合并为一条。 */
const ACCOUNT_EVENTS = [
  ['注册成功', 'P0', '账号创建成功的那一刻', '注册失败或中途放弃不产生记录'],
  ['登录', 'P0', '活动期内在所选端完成登录（默认不限端），Web 端按首次访问任一后端接口计', '活动期内一直不登录则不发放；端不匹配或版本低于下限也不触发，换端或升级后再次登录可补发'],
];

/* ═════════════ 设计说明侧栏 ═════════════ */

const DOCS = {
  list: {
    title: '活动列表',
    body: [
      ['界面依据', '❶「是否启用：运营可随时手动关停，关停后立即停止发放，重新打开即恢复，不需要改动其他字段」—— 所以开关直接放在行内，不必进详情。'],
      ['不设独立的状态列', '进行中 / 未启用 与操作列的开关完全重复，故不单列。开关表达运营意图，活动窗口列表达时间进展 —— 只有开关看不出来的「未开始」（已启用但没到生效时间）与「已结束」才在窗口列上挂标签。'],
      ['状态怎么算', '不落库，按开关与窗口实时算，优先级从上往下：手动结束过 → 已结束；开着且过了截止时间 → 已结束；开关关着 → 未启用；没到生效时间 → 未开始；其余 → 进行中。未启用的活动到了截止时间「依然是未启用」，它从未开始过，没有「结束」可言。'],
      ['启用时校验时间区间', '启用的前提是时间区间仍然有效：不能完全落在过去；开始时间可以在过去（启用后立即生效），但结束时间绝对不能在过去，否则启用即是空操作。校验不通过时拦截并提示去改时间。'],
      ['本期三类奖励', '老用户回馈、新人注册礼包、桌面端登录奖励在这里就是三条普通配置，没有特殊地位 —— 这正是后台化的意义。'],
      ['原型导览', '列表页可点任意一行进入活动详情，点「编辑」进入配置表单，行内开关可直接启停。幂等口径写死在后端、不出现在界面上：同一条配置对同一用户永远只发一次。'],
      ['列表不放发放数据', '已发放人数属于 ❻ 发放数据（P1，不进本期），且它是个随时间变化的动态值，摆在列表里既做不准也帮不上配置决策。要看进度进详情页。'],
    ],
    warn: null,
  },
  form: {
    title: '活动配置表单',
    body: [
      ['为什么是长表单', '五组字段做成同一页，而不是分步向导 —— 运营常改的只是其中一两项，分步会强制走完全流程。'],
      ['奖励名称的提示', 'PRD 明确要求：该字段旁需给出说明文案，告知运营「此名称会直接展示在用户的到账弹窗中」，避免填入内部口径的名称。'],
      ['两组二选一', '人群范围（条件圈人 / 名单指定）与触发方式（事件触发 / 即时直发），一条配置只能各选其一，不混用。做成 Tab 而非并列展开，从视觉上杜绝「两边都填了算谁的」。'],
      ['行为类事件是两维', '动作决定「做什么算数」，统计口径决定「怎么算达标」，两者必须同时选。选「账号历史首次」时「累计次数 N」置灰而非隐藏，切回另一种口径才不会漏填。'],
      ['有效期只读', '写死为自发放起 1 年。注意区分：活动生效/截止时间管的是「这条配置还发不发」，有效期管的是「已经发出去的积分什么时候过期」。'],
      ['界面里没有设计说明', '界面内只出现两类文字：灰底是产品规则说明（后台上线后真实存在的文案），琥珀底是风险警示（只用于不可逆操作与校验失败）。需求背景、设计取舍、原型导览一律不进界面，全部收在这个侧栏里。'],
      ['「登录」的收紧档位', '「登录」默认不限端不限版本，是最宽的一档；勾选具体端并填版本下限即可逐级收紧。本期的桌面端登录奖励就是这样一条配置：端选桌面端，桌面端版本下限填技术侧确定的版本号。'],
      ['右侧预览不是装饰', '奖励名称同时是弹窗里的来源文案与积分明细的事件说明，填错了用户直接可见。弹窗内容全部由后端下发的发放事件数据驱动，前端不写死任何一类奖励的判断分支。'],
      ['版本下限按端分设', '桌面端 / iOS / Android 的版本号是三条彼此独立的序列，共用一个输入框会让「2.6.0」这种值同时套到三个端上，语义是错的。因此勾选哪些端，就出现哪些端的版本下限，各填各的。Web 端始终最新、无版本概念，只作只读说明出现。'],
      ['启用后仍可编辑', '窗口时间与发放额度在活动启用后依然允许修改，不做字段锁定。改动仅对之后的发放生效，已到账积分不追溯调整 —— 同一活动下早发与晚发的用户额度可能不同，这一不一致是可接受的。'],
    ],
    warn: '预估命中人数为本稿新增（PRD 未覆盖），已定为 P1、不进本期。二期落地前，条件圈人只能靠运营自己估。',
  },
  detail: {
    title: '活动详情',
    body: [
      ['已结束活动只读', '已结束后不再允许编辑，页面上也不提供任何操作入口。窗口已过，改任何字段都不会再发出积分，编辑是空操作；而且已结束活动是财务对账与风控复盘的凭据，事后可改等于历史可篡改。要再做一轮，新建一条配置即可。'],
      ['结束是独立动作', '不通过把截止时间改到过去来提前结束。那样语义含糊，还会和「延长活动」的操作混在一起误触。结束不可逆；只想临时停，用启用开关关停。'],
      ['为什么没有触达率', '触达率的分母是「命中人群」，而懒发放下这个集合随时间变化，界定不了，算出来的百分比是假精度。详情页只保留能精确计数的指标：已发放人数、已发放积分、剩余补发窗口。'],
      ['明细只记已发放', '不设「未发放」状态。懒发放下命中人群是动态的 —— 今天没登录的人明天登录就发了，谁算「未发放」无法界定，硬造这个状态只会给运营一个假的分母。表里只有真实发生过的发放记录。'],
      ['异常账号', '已注销、已封禁账号不发放也不补发，因为没有发生发放，明细里不出现这些记录。'],
    ],
    warn: '整个详情页为本稿新增。PRD 只写了「怎么配」，没写「发完怎么看」—— 缺了这屏，运营发完积分就失去全部反馈，既无法复盘也无法排查投诉。',
  },
};

const GLOBAL_DOC = [
  ['幂等键写死为「手机号 + 活动 ID」', '不作为运营可配置项。跨端、跨设备、多次触发均不重复发放；注销后用同一手机号重新注册视为同一用户。'],
  ['有效期写死为自发放起 1 年', '不开放运营配置，与积分体系「奖励积分」有效期口径一致。'],
  ['到账弹窗由发放事件数据驱动', '前端不写死任何一类奖励的判断分支。新增一类奖励，运营在后台配完就能弹窗，前端零改动。'],
  ['叠加规则', '同一用户命中多条配置时各条独立到账、可叠加、不合并，每笔各自写积分明细、各自弹一次窗。'],
];

/* 2026-08-07 已拍板，原「待确认」三条转为明确口径 */
const DECISIONS = [
  ['启用后允许编辑窗口时间与发放额度', 'P0',
    '不做字段锁定。改动即时生效，仅对之后的发放起作用；已到账的积分不追溯调整。同一活动下早发与晚发的用户拿到的额度可能不同，这一不一致是可接受的。'],
  ['条件圈人支持预估命中人数', 'P1',
    '不进本期，作为二期能力。后端需具备「不实际发放、先跑一次人群计算」的能力。二期落地前，条件圈人只能靠运营自己估。'],
  ['不设审批流，靠保存并启用的二次确认兜底', 'P0',
    '单人操作即可发出。两种触发方式都在「保存并启用」时拦一道二次确认，提示内容分写：即时直发要把「人数 × 每人积分 = 总额」三个数算出来并写明不可撤销；事件触发给不出总额，转而讲清活动窗口、每人额度与「触发即到账」，并说明需要停止时用启用开关关停。'],
];

function DocDrawer({ page, open, onClose }) {
  const d = DOCS[page] || DOCS.list;
  return (
    <>
      <div onClick={onClose} style={{
        position: 'absolute', inset: 0, background: 'rgba(10,10,6,0.28)', zIndex: 300,
        opacity: open ? 1 : 0, pointerEvents: open ? 'auto' : 'none', transition: 'opacity .2s',
      }} />
      <div style={{
        position: 'absolute', top: 0, right: 0, bottom: 0, width: 420, zIndex: 301,
        background: '#fff', boxShadow: '-8px 0 40px rgba(0,0,0,0.14)', fontFamily: FONT,
        transform: open ? 'none' : 'translateX(100%)', transition: 'transform .24s cubic-bezier(.32,.72,0,1)',
        display: 'flex', flexDirection: 'column',
      }}>
        <div style={{ padding: '18px 22px 14px', borderBottom: '1px solid ' + C.lineSoft, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <div>
            <div style={{ fontSize: 11, color: C.brand, fontWeight: 600, letterSpacing: '0.3px' }}>设计说明 · 当前页面</div>
            <div style={{ fontSize: 16, fontWeight: 700, marginTop: 3 }}>{d.title}</div>
            <div style={{ fontSize: 11, color: C.mute, marginTop: 4 }}>本层不属于后台界面，是给评审看的需求背景与设计取舍。</div>
          </div>
          <span onClick={onClose} style={{ fontSize: 18, color: '#B4B4AC', cursor: 'pointer', lineHeight: 1, padding: 4 }}>×</span>
        </div>

        <div style={{ flex: 1, overflowY: 'auto', padding: '16px 22px 30px' }}>
          {d.warn && (
            <div style={{ marginBottom: 16 }}>
              <Note tone="amber"><b>本稿新增</b><br />{d.warn}</Note>
            </div>
          )}
          {d.body.map((r) => (
            <div key={r[0]} style={{ marginBottom: 15 }}>
              <div style={{ fontSize: 12.5, fontWeight: 700, color: C.text, marginBottom: 5 }}>{r[0]}</div>
              <div style={{ fontSize: 12.5, color: C.sub, lineHeight: 1.8 }}>{r[1]}</div>
            </div>
          ))}

          <div style={{ marginTop: 26, paddingTop: 18, borderTop: '1px solid ' + C.lineSoft }}>
            <div style={{ fontSize: 12.5, fontWeight: 700, marginBottom: 4 }}>界面上看不到，但必须知道的后端口径</div>
            <div style={{ fontSize: 11.5, color: C.mute, marginBottom: 12 }}>以下四条写死在后端，不作为运营可配置项。</div>
            {GLOBAL_DOC.map((r) => (
              <div key={r[0]} style={{ display: 'flex', gap: 9, marginBottom: 11 }}>
                <span style={{ width: 5, height: 5, borderRadius: 5, background: C.brand, marginTop: 7, flexShrink: 0 }} />
                <div>
                  <div style={{ fontSize: 12.5, fontWeight: 600, color: C.text }}>{r[0]}</div>
                  <div style={{ fontSize: 12, color: C.sub, lineHeight: 1.75, marginTop: 3 }}>{r[1]}</div>
                </div>
              </div>
            ))}
          </div>

          <div style={{ marginTop: 24, paddingTop: 18, borderTop: '1px solid ' + C.lineSoft }}>
            <div style={{ fontSize: 12.5, fontWeight: 700, marginBottom: 3 }}>已确认口径</div>
            <div style={{ fontSize: 11.5, color: C.mute, marginBottom: 12 }}>2026-08-07 拍板，原三条待确认已有结论。</div>
            {DECISIONS.map((d, i) => (
              <div key={i} style={{ display: 'flex', gap: 9, marginBottom: 12 }}>
                <span style={{
                  width: 16, height: 16, borderRadius: 16, background: C.greenSoft, color: C.green,
                  fontSize: 10, fontWeight: 700, display: 'inline-flex', alignItems: 'center',
                  justifyContent: 'center', flexShrink: 0, marginTop: 3,
                }}>✓</span>
                <div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
                    <span style={{ fontSize: 12.5, fontWeight: 600, color: C.text }}>{d[0]}</span>
                    <Pill tone={d[1] === 'P0' ? 'brand' : 'gray'}>{d[1]}</Pill>
                  </div>
                  <div style={{ fontSize: 12, color: C.sub, lineHeight: 1.75, marginTop: 3 }}>{d[2]}</div>
                </div>
              </div>
            ))}
          </div>

          <div style={{ marginTop: 24, fontSize: 11.5, color: '#B0B0A6', lineHeight: 1.7 }}>
            内容来源：办公小浣熊 2.0（补充版）·「（5）运营后台配置要求」<br />
            原型说明：文本输入不做真实录入，积分数额用预置示例切换。
          </div>
        </div>
      </div>
    </>);
}

/* ═════════════ 页面 ①：活动列表 ═════════════ */

/* 不设独立的「状态」列：进行中 / 未启用 与操作列的开关完全重复。
 * 开关表达运营意图，活动窗口列表达时间进展 —— 只有开关看不出来的
 * 「未开始」「已结束」才在窗口列上挂一个小标签。 */
const HEAD = ['活动名称', '人群范围', '触发方式', '发放额度', '活动窗口', '操作'];
const COLW = [172, 200, 186, 92, 250, 116];

function ListPage({ rows, onToggle, onOpen, onNew }) {
  const [hover, setHover] = useState(null);
  const shown = rows;
  return (
    <>
      <div style={{ background: '#fff', border: '1px solid ' + C.lineSoft, borderRadius: 12, overflow: 'hidden' }}>
        <div style={{ display: 'flex', padding: '0 18px', height: 38, alignItems: 'center', borderBottom: '1px solid ' + C.lineSoft, background: '#FCFCFA' }}>
          {HEAD.map((h, i) => <div key={h} style={{ width: COLW[i], fontSize: 11.5, color: C.mute, fontWeight: 600 }}>{h}</div>)}
        </div>
        {shown.map((r, i) => (
          <div key={r.id}
            onMouseEnter={() => setHover(r.id)} onMouseLeave={() => setHover(null)}
            onClick={() => onOpen(r, 'detail')}
            style={{
              display: 'flex', padding: '0 18px', height: 52, alignItems: 'center', cursor: 'pointer',
              background: hover === r.id ? '#FBFAFF' : '#fff',
              borderBottom: i === shown.length - 1 ? 'none' : '1px solid ' + C.lineSoft,
            }}>
            <div style={{ width: COLW[0], fontSize: 12.5, fontWeight: 600, paddingRight: 8, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', color: hover === r.id ? C.brand : C.text }}>{r.name}</div>
            <div style={{ width: COLW[1], fontSize: 12, color: C.sub, paddingRight: 8, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} title={r.crowdText}>{r.crowdText}</div>
            <div style={{ width: COLW[2], fontSize: 12, color: C.sub, paddingRight: 8, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} title={r.trigText}>{r.trigText}</div>
            <div style={{ width: COLW[3], fontSize: 12.5, fontWeight: 600 }}>{r.amount}</div>
            <div style={{ width: COLW[4], fontSize: 11.5, color: C.mute, display: 'flex', alignItems: 'center', gap: 6, paddingRight: 8 }}>
              <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.win}</span>
              {r.state === 'done' && <Pill tone="gray">已结束</Pill>}
              {r.state === 'on' && !r.notStarted && <Pill tone="green"><Dot on />进行中</Pill>}
            </div>
            <div style={{ width: COLW[5], display: 'flex', gap: 10, alignItems: 'center', fontSize: 12, color: C.brand, fontWeight: 600 }}
              onClick={(e) => e.stopPropagation()}>
              {/* 已结束活动只读：窗口已过，改任何字段都不会再发出积分，
                  且它是财务对账与风控复盘的凭据，不可事后篡改。 */}
              {r.state === 'done'
                ? <>
                  <span style={{ cursor: 'pointer' }} onClick={() => onOpen(r, 'detail')}>查看</span>
                  <span style={{ color: '#DEDED8' }}>|</span>
                  <Switch on={false} disabled />
                </>
                : <>
                  <span style={{ cursor: 'pointer' }} onClick={() => onOpen(r, 'form')}>编辑</span>
                  <span style={{ color: '#DEDED8' }}>|</span>
                  <Switch on={r.state === 'on'} onClick={() => onToggle(r.id)} />
                </>}
            </div>
          </div>
        ))}
        {!shown.length && <div style={{ padding: '40px 0', textAlign: 'center', fontSize: 12.5, color: C.mute }}>暂无活动</div>}
      </div>

      <div style={{ marginTop: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <div style={{ fontSize: 12, color: C.mute }}>
          共 {rows.length} 条活动 · 其中 {rows.filter(r => r.state === 'on').length} 条已启用
        </div>
        <div style={{ display: 'flex', gap: 6 }}>
          {['1', '2', '›'].map((p, i) => (
            <span key={p} style={{
              minWidth: 26, height: 26, borderRadius: 6, fontSize: 12, border: '1px solid ' + C.line,
              background: i === 0 ? C.brand : '#fff', color: i === 0 ? '#fff' : C.sub,
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            }}>{p}</span>
          ))}
        </div>
      </div>

    </>);
}

/* ═════════════ 用户端到账弹窗预览 ═════════════ */

/* 版本下限：按端分别设置。
 * 桌面端 / iOS / Android 三端的版本号是彼此独立的序列，共用一个输入框没有意义；
 * Web 端不存在版本概念（始终最新），只作为一条只读说明出现，不占配置位。 */
const CLIENT_ENDS = ['桌面端', 'iOS', 'Android'];
const VER_OPTS = {
  '桌面端': ['不限版本', '2.4.0', '2.5.0', '2.6.0'],
  'iOS': ['不限版本', '1.8.0', '1.9.0', '2.0.0'],
  'Android': ['不限版本', '1.7.0', '1.8.0', '1.9.0'],
};

function VersionFloors({ devs, vers, onChange }) {
  /* 不限端时不设版本下限：既然不区分端，就没有「哪个端的版本」可言。
     若允许「不限端 + 桌面端 ≥ 2.6.0」，语义上等于既不限端又对某端设门槛，自相矛盾。 */
  const unlimited = devs.indexOf('不限端') > -1;
  const picked = unlimited ? [] : CLIENT_ENDS.filter(d => devs.indexOf(d) > -1);
  const webOnly = !unlimited && !picked.length && devs.indexOf('Web') > -1;

  if (!picked.length) {
    return (
      <div style={{ maxWidth: 460 }}>
        <Note>{unlimited
          ? '已选「不限端」，不区分端也就不区分版本，本项无需设置。需要限定版本时，请先勾选具体的端。'
          : webOnly
            ? '已选端只有 Web。Web 端始终运行最新版本，不存在版本下限，本项无需设置。'
            : '请先在上方勾选端。选中桌面端 / iOS / Android 后，这里会按端分别出现版本下限。'}</Note>
      </div>);
  }
  return (
    <div style={{ maxWidth: 460 }}>
      {picked.map((d, i) => (
        <div key={d} style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: i ? 8 : 0 }}>
          <span style={{
            width: 62, flexShrink: 0, fontSize: 12, color: C.sub, textAlign: 'right', lineHeight: '32px',
          }}>{d}</span>
          <Select w={150} value={vers[d] || '不限版本'} options={VER_OPTS[d]}
            onChange={v => onChange(Object.assign({}, vers, { [d]: v === '不限版本' ? '' : v }))} />
          <span style={{ fontSize: 11.5, color: C.mute, lineHeight: '32px' }}>
            {vers[d] ? d + ' ' + vers[d] + ' 及以上可触发' : '该端不限版本'}
          </span>
        </div>
      ))}
      {devs.indexOf('Web') > -1 && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 8, opacity: 0.55 }}>
          <span style={{ width: 62, flexShrink: 0, fontSize: 12, color: C.sub, textAlign: 'right', lineHeight: '32px' }}>Web</span>
          <span style={{ fontSize: 11.5, color: C.mute, lineHeight: '32px' }}>始终为最新版本，无版本下限概念，不参与设置</span>
        </div>
      )}
    </div>);
}

function ToastPreview({ name, amount }) {
  return (
    <div style={{
      width: 262, borderRadius: 14, background: '#fff', boxSizing: 'border-box',
      border: '1px solid ' + C.line, boxShadow: '0 12px 32px rgba(0,0,0,0.10)',
      padding: '18px 20px 20px', textAlign: 'center', position: 'relative',
    }}>
      <div style={{ position: 'absolute', right: 12, top: 10, fontSize: 14, color: '#B4B4AC' }}>×</div>
      <div style={{ width: 40, height: 40, borderRadius: 40, background: C.greenSoft, margin: '4px auto 10px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke={C.green} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 10.5l4 4 8-8" /></svg>
      </div>
      <div style={{ fontSize: 15, fontWeight: 700 }}>积分已到账</div>
      <div style={{ fontSize: 12.5, color: C.brand, fontWeight: 600, marginTop: 12 }}>{name || '（未填写奖励名称）'}</div>
      <div style={{ fontSize: 13.5, fontWeight: 600, marginTop: 4 }}>{amount} 积分已到账</div>
      <div style={{ fontSize: 11.5, color: C.mute, marginTop: 4 }}>有效期至 2027-08-06</div>
      <div style={{ fontSize: 11.5, color: C.mute, marginTop: 12, lineHeight: 1.6 }}>可前往 设置 - 资源用量<br />查看积分明细</div>
      <div style={{ marginTop: 14, height: 34, borderRadius: 8, background: C.brand, color: '#fff', fontSize: 13, fontWeight: 600, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>我知道了</div>
    </div>);
}

/* ═════════════ 页面 ②：配置表单 ═════════════ */

function FormPage({ f, set }) {
  const isEvent = f.trigWay === 'event';
  const isAccount = f.evGroup === 'account';
  const canScopeDevice = isAccount && f.event === '登录';
  const nOff = f.stat === '账号历史首次';

  return (
    <div style={{ display: 'flex', gap: 20, alignItems: 'flex-start' }}>
      <div style={{ flex: 1, minWidth: 0 }}>

        {/* ❶ 叫什么 */}
        <Group index="1" title="活动基础信息" desc="名称、活动窗口与启用状态">
          <Field label="奖励名称" required
            hint="⚠️ 此名称会直接展示在用户的到账弹窗中，并作为积分明细的事件说明，请勿填写内部口径名称。">
            <Input value={f.name} placeholder="请输入奖励名称" w={280} />
          </Field>
          <Field label="活动生效时间" dim={!isEvent}
            hint={isEvent
              ? '两端都可以不设：不设开始即保存后立即生效，不设截止即一直发放到手动关停。'
                + '精确到分钟，窗口之外不再发放；活动启用后仍可改，改动即时生效，已到账的积分不受影响。'
              : '即时直发是一次性发放，在保存并启用的那一刻算人群、发完即止，不存在持续发放的窗口，该项不适用。'}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', ...(isEvent ? null : { pointerEvents: 'none' }) }}>
              <DateTimePicker value={f.winStart} placeholder="不设开始时间" w={190}
                onChange={(v) => set({ winStart: v })} />
              <span style={{ fontSize: 12, color: C.mute }}>至</span>
              <DateTimePicker value={f.winEnd} placeholder="不设截止时间" w={190}
                onChange={(v) => set({ winEnd: v })} />
              <span style={{ fontSize: 11.5, color: C.mute }}>
                {!isEvent ? '即时直发不适用'
                  : !f.winStart && !f.winEnd ? '当前为长期有效'
                    : !f.winStart ? '保存后立即生效'
                      : !f.winEnd ? '到期前持续发放' : ''}
              </span>
            </div>
          </Field>
          <Field label="是否启用">
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <Switch on={f.enabled} onClick={() => set({ enabled: !f.enabled })} />
              <span style={{ fontSize: 12, color: f.enabled ? C.green : C.mute, fontWeight: 600 }}>{f.enabled ? '已启用' : '已关停'}</span>
              <span style={{ fontSize: 12, color: C.mute }}>关停后立即停止发放，重新打开即恢复，其他字段无需改动</span>
            </div>
          </Field>
          {f.expired && (
            <div style={{ marginLeft: 132, marginBottom: 6, maxWidth: 600 }}>
              <Note tone="amber">该活动的结束时间已在当前时间之前，保存后仍无法启用。请先把结束时间改到之后。</Note>
            </div>
          )}
        </Group>

        {/* ❷ 发给谁 */}
        <Group index="2" title="人群范围" desc="两种方式二选一，不混用">
          <div style={{ padding: '10px 0 4px', marginLeft: 132 }}>
            <WayTabs items={['方式 A · 条件圈人', '方式 B · 名单指定']}
              active={f.crowdWay === 'filter' ? '方式 A · 条件圈人' : '方式 B · 名单指定'}
              onChange={v => set({ crowdWay: v.indexOf('条件圈人') > -1 ? 'filter' : 'roster' })} />
          </div>

          {f.crowdWay === 'filter' ? (
            <>
              <Field label="会员身份" hint="「全部用户」与其余四项互斥；不填即不限身份。">
                <div style={{ display: 'flex', gap: 16, alignItems: 'center', height: 32, flexWrap: 'wrap' }}>
                  {['全部用户', '免费版', '个人升级版', '教育版', '企业版'].map(p => (
                    <Check key={p} on={f.plans.indexOf(p) > -1} onClick={() => {
                      if (p === '全部用户') return set({ plans: f.plans.indexOf(p) > -1 ? [] : ['全部用户'] });
                      const next = f.plans.filter(x => x !== '全部用户');
                      set({ plans: next.indexOf(p) > -1 ? next.filter(x => x !== p) : next.concat(p) });
                    }}>{p}</Check>
                  ))}
                </div>
              </Field>
              <Field label="注册时间" hint="三种模式三选一，均精确到分钟；不填即不限注册时间。">
                <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
                  <Select value={f.regMode} w={148} onChange={v => set({ regMode: v })}
                    options={['不限注册时间', '早于某个时间点', '晚于某个时间点', '介于两个时间点之间']} />
                  {f.regMode !== '不限注册时间' && <Input value="2026-08-06 12:00" w={168} />}
                  {f.regMode === '介于两个时间点之间' && <><span style={{ fontSize: 12, color: C.mute }}>至</span><Input value="2026-11-06 12:00" w={168} /></>}
                </div>
              </Field>
              <Field label="端使用情况" align="top"
                hint="端可多选，默认不限端，「不限端」与其余四项互斥；Windows 与 Mac 合并计为「桌面端」，不区分操作系统。">
                <div style={{ display: 'flex', gap: 16, alignItems: 'center', height: 32, flexWrap: 'wrap' }}>
                  {['不限端', 'Web', '桌面端', 'iOS', 'Android'].map(d => (
                    <Check key={d} on={f.devScope.indexOf(d) > -1} onClick={() => {
                      if (d === '不限端') return set({ devScope: f.devScope.indexOf(d) > -1 ? [] : ['不限端'] });
                      const next = f.devScope.filter(x => x !== '不限端');
                      set({ devScope: next.indexOf(d) > -1 ? next.filter(x => x !== d) : next.concat(d) });
                    }}>{d}</Check>
                  ))}
                </div>
                <div style={{ display: 'flex', gap: 10, alignItems: 'center', marginTop: 8 }}>
                  <Select value={f.devCond} w={116} onChange={v => set({ devCond: v })} options={['登录过', '未登录过']} />
                  <Select value={f.devWin} w={116} onChange={v => set({ devWin: v })} options={['不限时间', '最近 7 天', '最近 30 天', '最近 90 天']} />
                </div>
              </Field>
              <Field label="付费历史" align="top"
                hint="类型可多选，默认不限类型，「不限类型」与其余两项互斥；已全额退款的订单不计入「购买过」。">
                <div style={{ display: 'flex', gap: 16, alignItems: 'center', height: 32, flexWrap: 'wrap' }}>
                  {['不限类型', '订阅', '加油包'].map(d => (
                    <Check key={d} on={f.payType.indexOf(d) > -1} onClick={() => {
                      if (d === '不限类型') return set({ payType: f.payType.indexOf(d) > -1 ? [] : ['不限类型'] });
                      const next = f.payType.filter(x => x !== '不限类型');
                      set({ payType: next.indexOf(d) > -1 ? next.filter(x => x !== d) : next.concat(d) });
                    }}>{d}</Check>
                  ))}
                </div>
                <div style={{ display: 'flex', gap: 10, alignItems: 'center', marginTop: 8 }}>
                  <Select value={f.payCond} w={116} onChange={v => set({ payCond: v })} options={['购买过', '未购买过']} />
                  <Select value={f.payWin} w={116} onChange={v => set({ payWin: v })} options={['不限时间', '最近 30 天', '最近 90 天', '最近 180 天']} />
                </div>
              </Field>
              <div style={{ marginLeft: 132, marginTop: 6, maxWidth: 600 }}>
                <Note>
                  上面两处「最近 N 天」从判定发放的那一刻往前推：即时直发按点击「保存并启用」并确认的时刻算，全体命中用户共用同一个窗口；
                  事件触发按每个用户触发事件的时刻各自计算，因此不同用户的窗口不同。
                </Note>
              </div>
              <div style={{ marginLeft: 132, marginTop: 10, display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
                <Btn kind="soft" size="sm" onClick={() => set({ estimated: true })}>预估命中人数</Btn>
                <span style={{ fontSize: 12, color: C.sub }}>
                  {f.estimated
                    ? <>预估命中 <b style={{ color: C.text }}>62481</b> 人 · 多个条件取交集，全部留空则命中全量用户</>
                    : <>多个条件之间取交集，全部留空则命中全量用户</>}
                </span>
              </div>
            </>
          ) : (
            <>
              <Field label="上传名单" align="top" pad={11}
                hint="支持 CSV / Excel。用于补偿、内测、白名单等无法用条件描述的场景；此类发放同样走统一发放事件，到账弹窗与积分明细口径不变。">
                <div style={{
                  border: '1.5px dashed rgba(142,107,242,0.4)', borderRadius: 10, background: '#FCFBFF',
                  padding: '18px 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
                }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
                    <span style={{ width: 34, height: 34, borderRadius: 8, background: C.brandSoft, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                      <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke={C.brand} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M8 11V3M5 6l3-3 3 3M3 12v1.5h10V12" /></svg>
                    </span>
                    <div>
                      <div style={{ fontSize: 13, fontWeight: 600 }}>compensation_0728.csv</div>
                      <div style={{ fontSize: 11.5, color: C.mute, marginTop: 3 }}>解析成功 328 条 · 去重后 328 人 · 3 条格式异常已跳过</div>
                    </div>
                  </div>
                  <div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
                    <Btn size="sm">下载模板</Btn>
                    <Btn size="sm">重新上传</Btn>
                  </div>
                </div>
              </Field>
              <Field label="名单格式" align="top">
                <div style={{ border: '1px solid ' + C.lineSoft, borderRadius: 9, overflow: 'hidden', maxWidth: 520 }}>
                  <div style={{ display: 'flex', background: '#FCFCFA', borderBottom: '1px solid ' + C.lineSoft, padding: '7px 12px' }}>
                    {['列名', '是否必填', '说明'].map((h, i) => <div key={h} style={{ width: [86, 118, 230][i], fontSize: 11.5, color: C.mute, fontWeight: 600 }}>{h}</div>)}
                  </div>
                  {[
                    ['手机号', '与用户 ID 二选一', '与幂等键「手机号 + 活动 ID」对齐，优先按此列匹配'],
                    ['用户 ID', '与手机号二选一', '供无手机号或只掌握 ID 的场景使用'],
                  ].map((r, i) => (
                    <div key={r[0]} style={{ display: 'flex', padding: '9px 12px', borderTop: i ? '1px solid ' + C.lineSoft : 'none' }}>
                      <div style={{ width: 86, fontSize: 12, fontWeight: 600 }}>{r[0]}</div>
                      <div style={{ width: 118, fontSize: 12, color: C.sub }}>{r[1]}</div>
                      <div style={{ width: 230, fontSize: 11.5, color: C.mute, lineHeight: 1.6 }}>{r[2]}</div>
                    </div>
                  ))}
                </div>
                <div style={{ marginTop: 8, maxWidth: 520 }}>
                  <Note>首行为表头，仅两列；两列都填时以手机号为准，名单内重复的同一用户按一条计，不重复发放。</Note>
                </div>
              </Field>
            </>
          )}
        </Group>

        {/* ❸ 什么时候发 */}
        <Group index="3" title="触发方式" desc="两种方式二选一，一条配置只挂一个事件">
          <div style={{ padding: '10px 0 4px', marginLeft: 132 }}>
            <WayTabs items={['方式 A · 事件触发（懒发放）', '方式 B · 即时直发']}
              active={isEvent ? '方式 A · 事件触发（懒发放）' : '方式 B · 即时直发'}
              onChange={v => set({ trigWay: v.indexOf('事件触发') > -1 ? 'event' : 'direct' })} />
          </div>

          {isEvent ? (
            <>
              <Field label="事件分组" hint="一条配置只挂一个事件。想同时激励多个动作，需要建多条配置。">
                <div style={{ display: 'flex', gap: 22, height: 32, alignItems: 'center' }}>
                  <Radio on={isAccount} onClick={() => set({ evGroup: 'account' })}>账号类事件</Radio>
                  <Radio on={!isAccount} onClick={() => set({ evGroup: 'behavior' })}>行为类事件（动作 + 统计口径）</Radio>
                </div>
              </Field>

              {isAccount ? (
                <>
                  <Field label="选择事件" align="top" pad={5}>
                    <div style={{ border: '1px solid ' + C.lineSoft, borderRadius: 9, overflow: 'hidden', maxWidth: 600 }}>
                      {ACCOUNT_EVENTS.map((e, i) => {
                        const on = f.event === e[0];
                        return (
                          <div key={e[0]} onClick={() => set({ event: e[0] })} style={{
                            display: 'flex', gap: 10, padding: '10px 12px', cursor: 'pointer',
                            borderTop: i ? '1px solid ' + C.lineSoft : 'none',
                            background: on ? '#FBFAFF' : '#fff',
                          }}>
                            <div style={{ paddingTop: 1 }}><Radio on={on} /></div>
                            <div style={{ flex: 1 }}>
                              <span style={{ fontSize: 12.5, fontWeight: 600, color: on ? C.brand : C.text }}>{e[0]}</span>
                              <div style={{ fontSize: 11.5, color: C.sub, marginTop: 4, lineHeight: 1.6 }}>触发时点：{e[2]}</div>
                              <div style={{ fontSize: 11.5, color: C.mute, marginTop: 2, lineHeight: 1.6 }}>不触发：{e[3]}</div>
                            </div>
                          </div>);
                      })}
                    </div>
                  </Field>
                  <Field label="端" dim={!canScopeDevice}
                    hint={canScopeDevice ? '端可多选，默认不限端。选中具体端即把「登录」收紧为该端登录才触发。桌面端不区分 Windows / Mac。' : '「注册成功」不涉及端与版本，该项不适用。'}>
                    <div style={{ display: 'flex', gap: 16, alignItems: 'center', height: 32, flexWrap: 'wrap', ...(canScopeDevice ? null : { pointerEvents: 'none' }) }}>
                      {['不限端', 'Web', '桌面端', 'iOS', 'Android'].map(d => (
                        <Check key={d} on={f.devs.indexOf(d) > -1} onClick={() => {
                          if (d === '不限端') return set({ devs: f.devs.indexOf(d) > -1 ? [] : ['不限端'] });
                          const next = f.devs.filter(x => x !== '不限端');
                          set({ devs: next.indexOf(d) > -1 ? next.filter(x => x !== d) : next.concat(d) });
                        }}>{d}</Check>
                      ))}
                    </div>
                  </Field>
                  <Field label="版本下限" align="top" dim={!canScopeDevice}
                    hint={canScopeDevice ? '仅在限定了具体端时才需要设置：各端版本号是彼此独立的序列，需分别填写，均为选填，不填即该端不限版本。Web 端无版本概念，不参与设置。' : null}>
                    <span style={canScopeDevice ? null : { pointerEvents: 'none' }}>
                      <VersionFloors devs={f.devs} vers={f.vers} onChange={v => set({ vers: v })} />
                    </span>
                  </Field>
                  <div style={{ marginLeft: 132, marginTop: 8, maxWidth: 600 }}>
                    <Note>事件触发只在活动生效与截止时间之间有效，截止之后再发生同一事件不再到账。</Note>
                  </div>
                </>
              ) : (
                <>
                  <Field label="选择动作" align="top" pad={3} hint="一条配置只挂一个动作。已创建后再删除不扣减计数；失败或中途取消不算达成。">
                    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, maxWidth: 620 }}>
                      {BEHAVIORS.map(b => {
                        const on = f.behavior === b[0];
                        return (
                          <div key={b[0]} onClick={() => set({ behavior: b[0] })} style={{
                            width: 196, boxSizing: 'border-box', padding: '9px 11px', borderRadius: 9, cursor: 'pointer',
                            border: '1px solid ' + (on ? 'rgba(142,107,242,0.45)' : C.lineSoft),
                            background: on ? '#FBFAFF' : '#fff',
                          }}>
                            <div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
                              <Radio on={on} />
                              <span style={{ fontSize: 12.5, fontWeight: 600, color: on ? C.brand : C.text }}>{b[0]}</span>
                            </div>
                            <div style={{ fontSize: 11, color: C.mute, marginTop: 5, lineHeight: 1.55, paddingLeft: 21 }}>{b[1]}</div>
                          </div>);
                      })}
                    </div>
                  </Field>
                  <Field label="统计口径" align="top" pad={5}>
                    <div style={{ display: 'flex', gap: 10, maxWidth: 620 }}>
                      {[
                        { t: '活动期间累计 N 次', w: '活动生效时间 → 截止时间', c: '窗口内累计达成 N 次时触发，填 1 即代表活动期内做过一次', u: '活动期内的行为激励，此前用没用过不影响' },
                        { t: '账号历史首次', w: '账号注册至今全生命周期', c: '仅对从未达成过该动作的用户生效，其第一次达成时触发', u: '引导没碰过该功能的用户尝试，老手不重复拿' },
                      ].map(o => {
                        const on = f.stat === o.t;
                        return (
                          <div key={o.t} onClick={() => set({ stat: o.t })} style={{
                            flex: 1, padding: '11px 13px', borderRadius: 10, cursor: 'pointer',
                            border: '1px solid ' + (on ? 'rgba(142,107,242,0.45)' : C.lineSoft),
                            background: on ? '#FBFAFF' : '#fff',
                          }}>
                            <div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
                              <Radio on={on} />
                              <span style={{ fontSize: 12.5, fontWeight: 600, color: on ? C.brand : C.text }}>{o.t}</span>
                            </div>
                            <div style={{ fontSize: 11, color: C.mute, marginTop: 7, lineHeight: 1.7 }}>
                              统计窗口：{o.w}<br />触发条件：{o.c}<br />典型用法：{o.u}
                            </div>
                          </div>);
                      })}
                    </div>
                  </Field>
                  <Field label="累计次数 N" dim={nOff}
                    hint={nOff ? '当前口径为「账号历史首次」，该项不适用，切回「活动期间累计 N 次」后可填。' : '正整数，填 1 即代表活动期内做过一次。'}>
                    <span style={nOff ? { pointerEvents: 'none' } : null}>
                      <Select value={nOff ? '' : f.n} placeholder="请选择次数" w={130} onChange={v => set({ n: v })} options={['1 次', '3 次', '5 次', '10 次']} />
                    </span>
                  </Field>
                  <div style={{ marginLeft: 132, marginTop: 8, maxWidth: 620 }}>
                    <Note>无论选哪种口径，同一条配置对同一用户仍只发一次，达标即发，之后继续做不再触发。</Note>
                  </div>
                </>
              )}
            </>
          ) : (
            <Field label="发放方式" align="top"
              hint="保存并启用即发放，不设独立的发放动作，命中人群一次性到账，不依赖用户行为。若用户当时不在前台，到账弹窗顺延到其下一次登录后展示。">
              <div style={{
                maxWidth: 600, padding: '14px 16px', borderRadius: 10,
                border: '1px solid ' + C.lineSoft, background: '#FCFCFA',
                display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
              }}>
                <div>
                  <div style={{ fontSize: 12.5, fontWeight: 600 }}>
                    待发放人群：{f.crowdWay === 'roster' ? '名单指定 · 328 人' : '条件圈人 · 预估 62481 人'}
                  </div>
                  <div style={{ fontSize: 11.5, color: C.mute, marginTop: 4 }}>
                    每人 {f.amount} 积分 · 预计总发放 {((f.crowdWay === 'roster' ? 328 : 62481) * parseInt(f.amount, 10))} 积分
                  </div>
                </div>
                <span style={{ fontSize: 11.5, color: C.mute, textAlign: 'right', maxWidth: 190 }}>
                  点击底部「保存并启用」后弹出二次确认，确认即发放
                </span>
              </div>
            </Field>
          )}
        </Group>

        {/* ❹ 发多少 */}
        <Group index="4" title="发放额度" desc="固定数额，不按会员身份分档">
          <Field label="积分数额" required align="top"
            hint="必填正整数。一条配置只有一个固定数额，命中人群的所有用户拿到的积分数一致，不按身份分档；需要差异化时建多条配置，各自圈各自的人群。活动启用后仍可修改，改动仅对之后的发放生效，已到账积分不追溯调整。">
            <Input value={f.amount} w={140} suffix="积分" />
            <Presets label="切换示例：" value={f.amount} options={['500', '1500', '2000', '3000']} onChange={v => set({ amount: v })} />
          </Field>
          <Field label="叠加规则" align="top">
            <div style={{ maxWidth: 600 }}>
              <Note>同一用户同时命中多条配置时，各条独立到账、可叠加，不做合并。每笔各自写入积分明细，并各自弹一次到账弹窗。</Note>
            </div>
          </Field>
        </Group>

        {/* ❺ 能用多久 */}
        <Group index="5" title="积分有效期" desc="后端写死，不开放配置">
          <Field label="有效期" align="top">
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <span style={{ opacity: 0.55 }}><Input value="自发放时刻起 1 年" w={180} /></span>
              <Pill tone="gray">不可编辑</Pill>
            </div>
            <div style={{ fontSize: 11.5, color: C.mute, marginTop: 8, lineHeight: 1.7, maxWidth: 600 }}>
              与积分体系「奖励积分」有效期口径一致，写死在后端。<br />
              注意区分两个时间：<b style={{ color: C.sub }}>活动生效 / 截止时间</b>管的是「这条配置还发不发」，
              <b style={{ color: C.sub }}>有效期</b>管的是「已经发出去的积分什么时候过期」。活动关闭后停止发放，但已到账积分按各自的 1 年有效期正常使用。
            </div>
          </Field>
        </Group>
      </div>

      {/* 右侧：用户端预览 */}
      <div style={{ width: 300, flexShrink: 0, position: 'sticky', top: 0 }}>
        <div style={{ background: '#fff', border: '1px solid ' + C.lineSoft, borderRadius: 12, padding: '14px 16px 18px' }}>
          <div style={{ fontSize: 12.5, fontWeight: 700, marginBottom: 3 }}>用户端预览</div>
          <div style={{ fontSize: 11, color: C.mute, lineHeight: 1.6, marginBottom: 14 }}>
            标题、来源、数额、有效期文案全部由后端下发的发放事件数据驱动，前端不写死任何一类奖励的判断分支。
          </div>
          <div style={{ display: 'flex', justifyContent: 'center' }}>
            <ToastPreview name={f.name} amount={f.amount} />
          </div>
          <div style={{ marginTop: 14 }}>
            <Note>这是保存并启用后，命中用户实际会看到的到账弹窗。奖励名称与积分数额填错，用户直接可见。</Note>
          </div>
        </div>
      </div>
    </div>);
}

/* ═════════════ 页面 ③：活动详情 ═════════════ */

/* 明细只记录「真实发生过的发放」。
 * 不设「未发放」状态：懒发放下命中人群是动态的，今天没登录的人明天登录就发了，
 * 谁算未发放无法界定，硬造这个状态只会给运营一个假的分母。 */
const DETAIL_ROWS = [
  ['138****4021', 'u_88213', '2026-08-06 12:04', '3000'],
  ['139****7756', 'u_90114', '2026-08-06 12:07', '3000'],
  ['186****2093', 'u_90887', '2026-08-06 12:11', '3000'],
  ['150****3348', 'u_91002', '2026-08-06 12:15', '3000'],
  ['177****6610', 'u_91240', '2026-08-06 12:19', '3000'],
];
const DW = [124, 108, 168, 90];

function DetailPage({ c, onEdit }) {
  const rows = DETAIL_ROWS;
  const done = c.state === 'done';
  return (
    <>
      <div style={{ display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
        {/* 只保留可精确计数的指标。
            原「触达率」与「命中人群」已删除：懒发放下命中人群随时间变化，
            分母界定不了，百分比是假精度，与明细「不设未发放口径」的原则冲突。 */}
        {[
          ['已发放人数', c.sent, '截至 2026-08-07 12:00'],
          ['已发放积分', '385218000', '每人 ' + c.amount + ' 积分'],
          ['剩余补发窗口', '61 天', '截止 2026-11-06 12:00'],
        ].map(s => (
          <div key={s[0]} style={{ flex: '1 1 180px', background: '#fff', border: '1px solid ' + C.lineSoft, borderRadius: 11, padding: '13px 15px' }}>
            <div style={{ fontSize: 11.5, color: C.mute }}>{s[0]}</div>
            <div style={{ fontSize: 20, fontWeight: 700, margin: '5px 0 4px', letterSpacing: '-0.4px' }}>{s[1]}</div>
            <div style={{ fontSize: 11, color: C.mute }}>{s[2]}</div>
          </div>
        ))}
      </div>

      <div style={{ display: 'flex', gap: 12, alignItems: 'flex-start', flexWrap: 'wrap' }}>
        <div style={{ width: 258, flexShrink: 0, background: '#fff', border: '1px solid ' + C.lineSoft, borderRadius: 12, padding: '14px 16px' }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
            <span style={{ fontSize: 12.5, fontWeight: 700 }}>配置摘要</span>
            {done
              ? <Pill tone="gray">只读</Pill>
              : <span onClick={onEdit} style={{ fontSize: 11.5, color: C.brand, fontWeight: 600, cursor: 'pointer' }}>编辑 ›</span>}
          </div>
          {[
            ['奖励名称', c.name],
            ['活动窗口', c.win],
            ['人群范围', c.crowdText],
            ['触发方式', c.trigText],
            ['发放额度', c.amount + ' 积分 / 人'],
            ['有效期', '自发放起 1 年'],
          ].map(r => (
            <div key={r[0]} style={{ display: 'flex', gap: 8, padding: '6px 0', borderTop: '1px solid ' + C.lineSoft }}>
              <div style={{ width: 62, flexShrink: 0, fontSize: 11.5, color: C.mute }}>{r[0]}</div>
              <div style={{ fontSize: 11.5, color: C.sub, lineHeight: 1.55 }}>{r[1]}</div>
            </div>
          ))}
        </div>

        <div style={{ flex: 1, minWidth: 420, background: '#fff', border: '1px solid ' + C.lineSoft, borderRadius: 12, overflow: 'hidden' }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', gap: 10, flexWrap: 'wrap' }}>
            <span style={{ fontSize: 12.5, fontWeight: 700 }}>发放明细</span>
            <div style={{ display: 'flex', gap: 8 }}>
              <Input placeholder="按手机号 / 用户 ID 查询" w={180} />
            </div>
          </div>
          <div style={{ display: 'flex', padding: '0 16px', height: 34, alignItems: 'center', background: '#FCFCFA', borderTop: '1px solid ' + C.lineSoft, borderBottom: '1px solid ' + C.lineSoft }}>
            {['手机号', '用户 ID', '发放时间', '积分'].map((h, i) => <div key={h} style={{ width: DW[i], fontSize: 11.5, color: C.mute, fontWeight: 600 }}>{h}</div>)}
          </div>
          {rows.map((r, i) => (
            <div key={r[1]} style={{ display: 'flex', padding: '0 16px', height: 40, alignItems: 'center', borderBottom: i === rows.length - 1 ? 'none' : '1px solid ' + C.lineSoft }}>
              {r.map((cell, j) => (
                <div key={j} style={{ width: DW[j], fontSize: 12, fontWeight: j === 0 || j === 3 ? 600 : 400, color: C.sub }}>{cell}</div>
              ))}
            </div>
          ))}
          <div style={{ padding: '10px 16px', borderTop: '1px solid ' + C.lineSoft, fontSize: 11.5, color: C.mute }}>
            仅记录已实际发放的用户；懒发放下命中人群随时间变化，不设「未发放」口径。
          </div>
        </div>
      </div>
    </>);
}

/* ═════════════ 弹窗 ═════════════ */

function Modal({ children, onClose }) {
  return (
    <>
      <div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(10,10,6,0.36)', zIndex: 200 }} />
      <div style={{
        position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%,-50%)', zIndex: 201,
        width: 388, background: '#fff', borderRadius: 14, padding: '22px 24px 18px',
        boxShadow: '0 20px 60px rgba(0,0,0,0.22)', fontFamily: FONT, boxSizing: 'border-box',
      }}>{children}</div>
    </>);
}

/* ═════════════ 主应用 ═════════════ */

const NAV = [
  { g: '积分运营', items: ['发放活动', '兑换码管理', '积分明细查询'] },
  { g: '基础配置', items: ['积分单价表', '套餐与加油包'] },
];

const BLANK = {
  name: '新人注册礼包', enabled: true, winStart: '', winEnd: '',
  crowdWay: 'filter', plans: ['免费版'], regMode: '晚于某个时间点',
  devScope: ['桌面端'], devCond: '未登录过', devWin: '不限时间',
  payType: ['不限类型'], payCond: '未购买过', payWin: '最近 90 天',
  estimated: false,
  trigWay: 'event', evGroup: 'account', event: '注册成功', devs: ['桌面端'],
  vers: { '桌面端': '2.6.0', 'iOS': '', 'Android': '' },
  behavior: '跑通数据分析', stat: '账号历史首次', n: '',
  amount: '3000',
};

/* 列表「活动窗口」列的展示文案：两端可各自缺省；
 * 即时直发是一次性发放，不存在窗口，只记发放时刻。 */
function winText(start, end, trigWay) {
  const short = (v) => (v || '').slice(5);            // 'YYYY-MM-DD HH:mm' → 'MM-DD HH:mm'
  if (trigWay === 'direct') return '一次性发放';
  if (!start && !end) return '长期有效';
  if (!end) return short(start) + ' → 长期';
  if (!start) return '立即生效 → ' + short(end);
  return short(start) + ' → ' + short(end);
}

/* 反向解析列表文案，供编辑态回填（原型里年份统一按 2026 处理） */
function winPart(win, i) {
  const parts = String(win || '').split(' → ');
  const v = parts[i];
  if (!v || v === '长期' || v === '长期有效' || v === '立即生效') return '';
  return '2026-' + v;
}

function fromRow(r) {
  return Object.assign({}, BLANK, {
    name: r.name, enabled: r.state === 'on', crowdWay: r.crowdWay,
    winStart: winPart(r.win, 0), winEnd: winPart(r.win, 1),
    trigWay: r.trigWay, evGroup: r.evGroup,
    event: r.evGroup === 'account' && r.event ? r.event : '注册成功',
    behavior: r.evGroup === 'behavior' ? r.event : '跑通数据分析',
    devs: ['桌面端'],
    vers: { '桌面端': '2.6.0', 'iOS': '', 'Android': '' },
    amount: String(r.amount), estimated: true, expired: !!r.expired,
  });
}

function CreditsAdminApp() {
  const [rows, setRows] = useState(SEED);
  const [page, setPage] = useState('list');       // list | form | detail
  const [current, setCurrent] = useState(null);   // 当前活动（详情/编辑）
  const [form, setForm] = useState(BLANK);
  const [isNew, setIsNew] = useState(true);
  const [modal, setModal] = useState(null);       // null | confirm | sent | saved
  const [doc, setDoc] = useState(false);
  const scroller = useRef(null);

  const set = (patch) => setForm(f => Object.assign({}, f, patch));
  const goTop = () => { if (scroller.current) scroller.current.scrollTop = 0; };

  const open = (row, target) => {
    setCurrent(row);
    if (target === 'form') { setForm(fromRow(row)); setIsNew(false); setPage('form'); }
    else setPage('detail');
    goTop();
  };
  const newCampaign = () => { setForm(BLANK); setIsNew(true); setCurrent(null); setPage('form'); goTop(); };
  /* 未启用的活动到了截止时间，状态依然是「未启用」，不会自动变成「已结束」——
     它从未开始过，没有「结束」可言。但启用时要校验时间区间是否仍然有效：
     开始时间可以在过去（立刻生效），结束时间绝对不能在过去，否则启用即刻失效。 */
  const toggle = (id) => {
    const row = rows.find(r => r.id === id);
    if (!row) return;
    if (row.state === 'off' && row.expired) { setCurrent(row); setModal('expired'); return; }
    setRows(rs => rs.map(r => r.id === id ? Object.assign({}, r, { state: r.state === 'on' ? 'off' : 'on' }) : r));
  };

  /* 结束活动是一个独立动作，不通过把截止时间改到过去来实现 ——
     后者语义含糊，且与「延长活动」的操作混在一起容易误操作。 */
  const endCampaign = () => {
    if (!current) return;
    setRows(rs => rs.map(r => r.id === current.id ? Object.assign({}, r, { state: 'done' }) : r));
    setCurrent(Object.assign({}, current, { state: 'done' }));
    setModal(null);
  };


  const save = (nextModal) => {
    const crowdText = form.crowdWay === 'roster' ? '名单指定 · 上传 328 人'
      : '条件圈人 · ' + (form.plans.length ? form.plans.join(' / ') : '全量用户');
    const notStarted = form.trigWay === 'event' && !!form.winStart
      && new Date(form.winStart.replace(' ', 'T')) > new Date();
    const trigText = form.trigWay === 'direct' ? '即时直发'
      : '事件触发 · ' + (form.evGroup === 'account' ? form.event : form.behavior);
    if (isNew) {
      const row = {
        id: 'n' + Date.now(), name: form.name, crowdWay: form.crowdWay, crowdText,
        trigWay: form.trigWay, evGroup: form.evGroup,
        event: form.evGroup === 'account' ? form.event : form.behavior, trigText,
        amount: parseInt(form.amount, 10), sent: form.trigWay === 'direct' ? '—' : '0',
        hit: '62481', state: form.enabled ? 'on' : 'off', notStarted,
        win: winText(form.winStart, form.winEnd, form.trigWay),
      };
      setRows(rs => [row].concat(rs));
    } else if (current) {
      setRows(rs => rs.map(r => r.id === current.id ? Object.assign({}, r, {
        name: form.name, crowdWay: form.crowdWay, crowdText, trigWay: form.trigWay,
        evGroup: form.evGroup, trigText, amount: parseInt(form.amount, 10),
        win: winText(form.winStart, form.winEnd, form.trigWay), notStarted,
        state: form.enabled ? 'on' : (r.state === 'done' ? 'done' : 'off'),
      }) : r));
    }
    setModal(nextModal || 'saved');
  };

  const title = page === 'list' ? '发放活动'
    : page === 'form' ? (isNew ? '新建活动' : '编辑活动 · ' + form.name)
      : (current ? current.name : '活动详情');
  const crumb = page === 'list' ? '积分运营' : '积分运营 / 发放活动';

  return (
    /* 外层留出顶部留白露出画布背景，供左上角面包屑落位；
       后台本体收成一张浮起的卡片，不再与面包屑争同一块像素。 */
    <div style={{
      position: 'fixed', inset: 0, boxSizing: 'border-box',
      padding: '60px 22px 22px',
      background: 'linear-gradient(180deg,#F5F3EE 0%, #EEEAE2 100%)',
      fontFamily: FONT,
    }}>
      <div style={{
        position: 'relative', height: '100%', display: 'flex', background: C.bg,
        color: C.text, overflow: 'hidden', borderRadius: 14,
        boxShadow: '0 12px 40px rgba(0,0,0,0.10), 0 0 0 1px rgba(10,10,6,0.07)',
      }}>
      {/* 左侧导航 */}
      <div style={{ width: 186, flexShrink: 0, background: '#fff', borderRight: '1px solid ' + C.lineSoft, padding: '14px 10px', boxSizing: 'border-box' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '2px 8px 16px' }}>
          <span style={{ width: 22, height: 22, borderRadius: 6, background: C.brand, color: '#fff', fontSize: 12, fontWeight: 700, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>浣</span>
          <span style={{ fontSize: 13, fontWeight: 700 }}>运营后台</span>
        </div>
        {NAV.map(sec => (
          <div key={sec.g} style={{ marginBottom: 14 }}>
            <div style={{ fontSize: 10.5, color: '#B0B0A6', fontWeight: 600, letterSpacing: '0.5px', padding: '0 8px 6px' }}>{sec.g}</div>
            {sec.items.map(it => {
              const on = it === '发放活动';
              return (
                <div key={it} onClick={() => { if (on) { setPage('list'); goTop(); } }} style={{
                  display: 'flex', alignItems: 'center', padding: '7px 8px', borderRadius: 7, marginBottom: 2,
                  background: on ? C.brandSoft : 'transparent', color: on ? C.brand : C.sub,
                  fontSize: 12.5, fontWeight: on ? 600 : 400, cursor: on ? 'pointer' : 'default',
                }}
                title={on ? '' : '原型未实现'}>{it}</div>);
            })}
          </div>
        ))}
      </div>

      {/* 主体 */}
      <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
        <div style={{
          height: 58, flexShrink: 0, borderBottom: '1px solid ' + C.lineSoft, background: '#fff',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 24px', gap: 12,
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 12, minWidth: 0 }}>
            {page !== 'list' && (
              <span onClick={() => { setPage('list'); goTop(); }} style={{
                width: 26, height: 26, borderRadius: 7, border: '1px solid ' + C.line, cursor: 'pointer',
                display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
              }}>
                <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke={C.sub} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M7.5 2L3.5 6l4 4" /></svg>
              </span>
            )}
            <div style={{ minWidth: 0 }}>
              <div style={{ fontSize: 11, color: C.mute, marginBottom: 2 }}>{crumb}</div>
              {/* 状态胶囊放在标题这一行内，与标题基线齐平；
                  若作为标题块的兄弟节点，它会被居中到「面包屑 + 标题」两行的中间，看起来偏高。 */}
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
                <div style={{ fontSize: 15, fontWeight: 700, letterSpacing: '-0.2px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{title}</div>
                {page === 'detail' && current && (
                  current.state === 'on' ? (current.notStarted
                    ? <Pill tone="gray"><Dot />已启用 · 未开始</Pill>
                    : <Pill tone="green"><Dot on />进行中</Pill>)
                    : current.state === 'off' ? <Pill tone="gray"><Dot />未启用</Pill> : <Pill tone="gray">已结束</Pill>
                )}
              </div>
            </div>
          </div>

          <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
            {page === 'list' && <>
              <Input placeholder="搜索活动名称" w={160} icon={<svg width="12" height="12" viewBox="0 0 14 14" fill="none" stroke="#9A9A90" strokeWidth="1.6"><circle cx="6" cy="6" r="4.2" /><path d="M9.2 9.2L12 12" strokeLinecap="round" /></svg>} />
              <Btn kind="primary" onClick={newCampaign}>+ 新建活动</Btn>
            </>}
            {page === 'detail' && current && current.state !== 'done' && <>
              <Btn size="sm" onClick={() => open(current, 'form')}>编辑</Btn>
              {current.state === 'on' && <Btn kind="danger" size="sm" onClick={() => setModal('end')}>结束活动</Btn>}
            </>}
            <span style={{ width: 1, height: 20, background: C.lineSoft }} />
            <Btn kind="soft" size="sm" onClick={() => setDoc(true)}>设计说明 ▸</Btn>
            <Btn size="sm" onClick={() => { setRows(SEED); setPage('list'); setForm(BLANK); goTop(); }}>重置</Btn>
          </div>
        </div>

        <div ref={scroller} style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '18px 24px 40px' }}>
          {page === 'list' && <ListPage rows={rows} onToggle={toggle} onOpen={open} onNew={newCampaign} />}
          {page === 'form' && <FormPage f={form} set={set} />}
          {page === 'detail' && current && <DetailPage c={current} onEdit={() => open(current, 'form')} />}
        </div>

        {page === 'form' && (
          <div style={{ flexShrink: 0, borderTop: '1px solid ' + C.lineSoft, background: '#fff', padding: '12px 24px', display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
            <Btn onClick={() => { setPage('list'); goTop(); }}>取消</Btn>
            <Btn kind="primary" onClick={() => form.enabled ? setModal('confirm') : save()}>
              {form.enabled ? '保存并启用' : '保存'}
            </Btn>
          </div>
        )}
      </div>

      {/* 保存并启用 · 二次确认：两种触发方式共用同一层拦截，提示内容按方式分写 */}
      {modal === 'confirm' && (
        <Modal onClose={() => setModal(null)}>
          {form.trigWay === 'direct' ? (
            <>
              <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 10 }}>确认保存并立即发放？</div>
              <div style={{ fontSize: 12.5, color: C.sub, lineHeight: 1.8 }}>
                启用即发放，本次将向 <b style={{ color: C.text }}>{form.crowdWay === 'roster' ? '328' : '62481'}</b> 名用户各发放 <b style={{ color: C.text }}>{form.amount}</b> 积分，
                合计 <b style={{ color: C.text }}>{((form.crowdWay === 'roster' ? 328 : 62481) * parseInt(form.amount, 10))}</b> 积分。
              </div>
              <div style={{ marginTop: 12 }}>
                <Note tone="amber">发放为不可撤销操作，到账后不支持批量回收。同一用户在本条配置下只会收到一次。</Note>
              </div>
            </>
          ) : (
            <>
              <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 10 }}>确认保存并启用？</div>
              <div style={{ fontSize: 12.5, color: C.sub, lineHeight: 1.8 }}>
                启用后不会立刻发放，活动窗口内符合条件的用户触发「{form.trigWay === 'event' && form.evGroup === 'account' ? form.event : form.behavior}」时逐个到账，
                每人 <b style={{ color: C.text }}>{form.amount}</b> 积分。
                活动窗口：<b style={{ color: C.text }}>{winText(form.winStart, form.winEnd)}</b>。
              </div>
              <div style={{ marginTop: 12 }}>
                <Note tone="amber">单笔到账后不可撤销，也不支持批量回收。发放总量取决于实际触发人数，无法预先锁定；需要停止时用启用开关关停。</Note>
              </div>
            </>
          )}
          <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 16 }}>
            <Btn size="sm" onClick={() => setModal(null)}>再看看</Btn>
            <Btn kind="primary" size="sm" onClick={() => save(form.trigWay === 'direct' ? 'sent' : 'saved')}>
              {form.trigWay === 'direct' ? '确认发放' : '确认启用'}
            </Btn>
          </div>
        </Modal>
      )}

      {/* 发放成功 */}
      {modal === 'sent' && (
        <Modal onClose={() => setModal(null)}>
          <div style={{ textAlign: 'center', padding: '6px 0 2px' }}>
            <div style={{ width: 42, height: 42, borderRadius: 42, background: C.greenSoft, margin: '0 auto 12px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <svg width="21" height="21" viewBox="0 0 20 20" fill="none" stroke={C.green} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 10.5l4 4 8-8" /></svg>
            </div>
            <div style={{ fontSize: 15, fontWeight: 700 }}>发放已提交</div>
            <div style={{ fontSize: 12.5, color: C.sub, lineHeight: 1.8, marginTop: 8 }}>
              后台正在批量入账，可在活动详情页查看发放进度与逐条明细。<br />
              不在前台的用户，到账弹窗顺延到其下一次登录后展示。
            </div>
            <div style={{ marginTop: 16 }}><Btn kind="primary" onClick={() => { setModal(null); setPage('list'); goTop(); }} style={{ width: '100%' }}>知道了</Btn></div>
          </div>
        </Modal>
      )}

      {/* 结束活动 · 二次确认 */}
      {modal === 'end' && current && (
        <Modal onClose={() => setModal(null)}>
          <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 10 }}>确认结束「{current.name}」？</div>
          <div style={{ fontSize: 12.5, color: C.sub, lineHeight: 1.8 }}>
            {current.notStarted
              ? <>该活动尚未开始发放，结束后将不再启动。</>
              : <>结束后立即停止发放。</>}
            活动转为<b style={{ color: C.text }}>只读</b>，不可再编辑，也无法重新开启。
            {!current.notStarted && <>已到账的积分不受影响，按各自的 1 年有效期正常使用。</>}
          </div>
          <div style={{ marginTop: 12 }}>
            <Note tone="amber">若只是想临时停一下，请用列表里的启用开关关停 —— 关停可以随时再打开，结束不可逆。</Note>
          </div>
          <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 16 }}>
            <Btn size="sm" onClick={() => setModal(null)}>取消</Btn>
            <Btn kind="primary" size="sm" onClick={endCampaign}>确认结束</Btn>
          </div>
        </Modal>
      )}

      {/* 启用校验：结束时间已在过去 */}
      {modal === 'expired' && current && (
        <Modal onClose={() => setModal(null)}>
          <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 10 }}>无法启用「{current.name}」</div>
          <div style={{ fontSize: 12.5, color: C.sub, lineHeight: 1.8 }}>
            该活动的结束时间是 <b style={{ color: C.text }}>{current.win.split(' → ')[1] || '长期'}</b>，已经在当前时间之前。
            启用后不会有任何用户触发，等于空操作。
          </div>
          <div style={{ marginTop: 12 }}>
            <Note tone="amber">
              开始时间可以在过去（启用后立即生效），但结束时间必须晚于当前时间。请先把结束时间改到之后再启用。
            </Note>
          </div>
          <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 16 }}>
            <Btn size="sm" onClick={() => setModal(null)}>知道了</Btn>
            <Btn kind="primary" size="sm" onClick={() => { setModal(null); open(current, 'form'); }}>去修改时间</Btn>
          </div>
        </Modal>
      )}

      {/* 保存成功 */}
      {modal === 'saved' && (
        <Modal onClose={() => { setModal(null); setPage('list'); goTop(); }}>
          <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 8 }}>{isNew ? '活动已创建' : '配置已保存'}</div>
          <div style={{ fontSize: 12.5, color: C.sub, lineHeight: 1.8 }}>
            「{form.name}」{form.enabled ? '已启用，命中人群将按配置的触发方式陆续到账。' : '已保存但未启用，打开开关后才会开始发放。'}
          </div>
          <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 16 }}>
            <Btn kind="primary" size="sm" onClick={() => { setModal(null); setPage('list'); goTop(); }}>返回列表</Btn>
          </div>
        </Modal>
      )}

      <DocDrawer page={page} open={doc} onClose={() => setDoc(false)} />
      </div>
    </div>);
}

  Object.assign(NS, { CreditsAdminApp });
})();
