const { useState, useEffect, useRef, useCallback, useMemo } = React;

/* ============================================================
   SVG 图标（管理后台用到的子集，与请柬页一致）
   ============================================================ */
const Icon = {
  lock: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></svg>),
  logout: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><polyline points="16 17 21 12 16 7" /><line x1="21" y1="12" x2="9" y2="12" /></svg>),
  barChart: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="20" x2="12" y2="10" /><line x1="18" y1="20" x2="18" y2="4" /><line x1="6" y1="20" x2="6" y2="16" /></svg>),
  message: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /></svg>),
  musicNote: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M9 18V5l12-2v13" /><circle cx="6" cy="18" r="3" /><circle cx="18" cy="16" r="3" /></svg>),
  rotate: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="1 4 1 10 7 10" /><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" /></svg>),
  heart: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" /></svg>),
  users: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></svg>),
  eye: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /><circle cx="12" cy="12" r="3" /></svg>),
  eyeOff: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" /><line x1="1" y1="1" x2="23" y2="23" /></svg>),
  settings: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /></svg>),
  edit: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 20h9" /><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" /></svg>),
  trash: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6" /><path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6" /><path d="M10 11v6M14 11v6" /><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" /></svg>),
  pin: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" /></svg>),
  download: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></svg>),
  rsvp: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></svg>),
  external: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></svg>),
  plus: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>),
  image: (<svg viewBox="0 0 24 24" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /></svg>),
};

const RSVP_SUBMITTED_KEY = 'wedding_rsvp_submitted_v1';

/* ============================================================
   Toast 提示动画：保存/修改等操作的成功与失败反馈
   ============================================================ */
const Toast = {
  _push: null, // 由 ToastHost 挂载时注入
  show(msg, type) { this._push && this._push(msg, type || 'success'); },
  success(msg) { this.show(msg, 'success'); },
  error(msg) { this.show(msg, 'error'); },
};

function ToastHost() {
  const [items, setItems] = useState([]);
  useEffect(() => {
    // 注入全局推送函数：新提示入栈，2.6s 后自动出栈
    Toast._push = (msg, type) => {
      const id = Date.now() + Math.random();
      setItems(list => [...list, { id, msg, type }]);
      setTimeout(() => setItems(list => list.filter(t => t.id !== id)), 2600);
    };
    return () => { Toast._push = null; };
  }, []);
  return (
    <div className="toast-container">
      {items.map(t => (
        <div key={t.id} className={`toast toast-${t.type}`}>
          {t.type === 'success' ? (
            <svg viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12" /></svg>
          ) : (
            <svg viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" /></svg>
          )}
          <span>{t.msg}</span>
        </div>
      ))}
    </div>
  );
}
const NAV_ITEMS = [
  { key: 'overview', label: '概览', icon: Icon.barChart },
  { key: 'rsvp', label: 'RSVP管理', icon: Icon.rsvp },
  { key: 'blessings', label: '留言管理', icon: Icon.message },
  { key: 'content', label: '内容编辑', icon: Icon.edit },
  { key: 'music', label: '背景音乐', icon: Icon.musicNote },
  { key: 'settings', label: '设置', icon: Icon.settings },
];

/* ============================================================
   登录卡片
   ============================================================ */
function LoginCard({ onLogin }) {
  const [pwd, setPwd] = useState('');
  const [error, setError] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const inputRef = useRef(null);

  useEffect(() => { inputRef.current?.focus(); }, []);

  const handleSubmit = async () => {
    if (!pwd || submitting) return;
    setSubmitting(true);
    try {
      const { token } = await WeddingAPI.login(pwd);
      WeddingAPI.setToken(token);
      onLogin && onLogin();
    } catch (err) {
      setError(err.message);
      setSubmitting(false);
    }
  };

  return (
    <div className="login-scene">
      <div className="login-card">
        <div className="login-icon">{Icon.lock}</div>
        <div className="login-title">管理员登录</div>
        <div className="login-desc">婚礼请柬管理后台</div>
        <input
          ref={inputRef}
          type="password"
          className="login-input"
          placeholder="请输入管理员口令"
          value={pwd}
          onChange={(e) => setPwd(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
        />
        <div className="login-error">{error}</div>
        <button className="login-btn" onClick={handleSubmit} disabled={submitting}>
          {submitting ? '登录中…' : '登 录'}
        </button>
      </div>
    </div>
  );
}

/* ============================================================
   请柬内容编辑表单（PC 表单，保存后所有访客生效）
   ============================================================ */

/* 图片裁剪弹窗：按小程序端实际展示形状（拱形/圆形/横幅）选取可见区域，
   确认后在浏览器端用 canvas 裁切再上传，小程序端所见即所得 */
function CropModal({ src, mime, ratio, shape, label, rawFile, onCancel, onConfirm, onRaw }) {
  const boxW = ratio >= 1.5 ? 380 : ratio >= 1 ? 300 : 240;
  const boxH = Math.round(boxW / ratio);
  const [img, setImg] = useState(null); // { el, w, h }
  const [scale, setScale] = useState(0);
  const [minScale, setMinScale] = useState(1);
  const [pos, setPos] = useState({ x: 0, y: 0 });
  const [exporting, setExporting] = useState(false);
  const stageRef = useRef(null);
  const drag = useRef(null);
  const live = useRef({}); // 供非 React 事件（wheel）读取的最新状态

  // 加载图片并计算「恰好铺满裁剪框」的最小缩放
  useEffect(() => {
    const el = new Image();
    el.crossOrigin = 'anonymous';
    el.onload = () => {
      const ms = Math.max(boxW / el.naturalWidth, boxH / el.naturalHeight);
      setImg({ el, w: el.naturalWidth, h: el.naturalHeight });
      setMinScale(ms);
      setScale(ms);
      setPos({ x: (boxW - el.naturalWidth * ms) / 2, y: (boxH - el.naturalHeight * ms) / 2 });
    };
    el.onerror = () => { Toast.error('图片加载失败'); onCancel(); };
    el.src = src;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [src]);

  const clampPos = (p, s) => ({
    x: Math.min(0, Math.max(boxW - img.w * s, p.x)),
    y: Math.min(0, Math.max(boxH - img.h * s, p.y)),
  });

  // 以裁剪框中心为锚点缩放
  const zoomTo = (ns) => {
    if (!img) return;
    ns = Math.min(Math.max(ns, minScale), minScale * 5);
    const k = ns / scale;
    setPos(clampPos({ x: boxW / 2 - (boxW / 2 - pos.x) * k, y: boxH / 2 - (boxH / 2 - pos.y) * k }, ns));
    setScale(ns);
  };
  live.current = { scale, zoomTo };

  // React 的 onWheel 是 passive 的，无法 preventDefault，改为手动挂非被动监听
  useEffect(() => {
    const el = stageRef.current;
    if (!el) return;
    const handler = (e) => {
      e.preventDefault();
      const { scale: s, zoomTo: z } = live.current;
      z && z(s * (e.deltaY < 0 ? 1.08 : 0.93));
    };
    el.addEventListener('wheel', handler, { passive: false });
    return () => el.removeEventListener('wheel', handler);
  }, [img]);

  const onPointerDown = (e) => {
    drag.current = { x: e.clientX, y: e.clientY, pos };
    e.currentTarget.setPointerCapture(e.pointerId);
  };
  const onPointerMove = (e) => {
    if (!drag.current || !img) return;
    setPos(clampPos({
      x: drag.current.pos.x + e.clientX - drag.current.x,
      y: drag.current.pos.y + e.clientY - drag.current.y,
    }, scale));
  };
  const onPointerUp = () => { drag.current = null; };

  // canvas 按选区裁切导出（长边限制 1600px）
  const handleConfirm = () => {
    if (!img || exporting) return;
    setExporting(true);
    const cropW = boxW / scale;
    const cropH = boxH / scale;
    const k = Math.min(1, 1600 / Math.max(cropW, cropH));
    const canvas = document.createElement('canvas');
    canvas.width = Math.round(cropW * k);
    canvas.height = Math.round(cropH * k);
    canvas.getContext('2d').drawImage(img.el, -pos.x / scale, -pos.y / scale, cropW, cropH, 0, 0, canvas.width, canvas.height);
    const type = mime === 'image/png' ? 'image/png' : 'image/jpeg';
    canvas.toBlob((blob) => {
      setExporting(false);
      if (blob) onConfirm(blob, type);
      else Toast.error('导出失败，请重试');
    }, type, 0.92);
  };

  return (
    <div className="crop-mask" onClick={(e) => e.target === e.currentTarget && onCancel()}>
      <div className="crop-modal">
        <div className="crop-title">调整「{label || '图片'}」显示区域</div>
        <div className="crop-stage-wrap">
          <div
            ref={stageRef}
            className={`crop-stage shape-${shape || 'rect'}`}
            style={{ width: boxW, height: boxH }}
            onPointerDown={onPointerDown}
            onPointerMove={onPointerMove}
            onPointerUp={onPointerUp}
            onPointerCancel={onPointerUp}
          >
            {img && (
              <img
                src={src}
                alt=""
                draggable={false}
                style={{ width: img.w, height: img.h, transform: `translate(${pos.x}px, ${pos.y}px) scale(${scale})` }}
              />
            )}
          </div>
        </div>
        <div className="crop-zoom">
          <span>缩放</span>
          <input
            type="range"
            min={minScale}
            max={minScale * 5}
            step={((minScale * 5 - minScale) / 200) || 0.01}
            value={scale}
            onChange={(e) => zoomTo(parseFloat(e.target.value))}
          />
        </div>
        <div className="crop-tip">拖动图片调整位置，滚轮或滑杆缩放；框内即为小程序中的最终显示效果</div>
        <div className="crop-actions">
          {rawFile && <button className="btn btn-ghost" onClick={onRaw}>原图直传</button>}
          <button className="btn btn-outline" onClick={onCancel}>取消</button>
          <button className="btn btn-primary" onClick={handleConfirm} disabled={!img || exporting}>
            {exporting ? '导出中…' : '确认裁剪并上传'}
          </button>
        </div>
      </div>
    </div>
  );
}

/* 图片字段：传 ratio 时选择图片后先弹裁剪窗（按 ratio/shape 模拟小程序展示形状），确认后再上传；
   不传 ratio 表示小程序端按原图比例自适应展示（widthFix），直接上传原图不裁剪 */
function ImageField({ label, value, onChange, ratio, shape, hint }) {
  const [uploading, setUploading] = useState(false);
  const [crop, setCrop] = useState(null); // { src, mime, file|null }

  const handleFile = (e) => {
    const file = e.target.files[0];
    e.target.value = '';
    if (!file) return;
    if (!/^image\//.test(file.type)) { Toast.error('请选择图片文件'); return; }
    if (ratio) setCrop({ src: URL.createObjectURL(file), mime: file.type, file });
    else uploadRawFile(file);
  };

  const closeCrop = () => {
    if (crop && crop.file) URL.revokeObjectURL(crop.src);
    setCrop(null);
  };

  const doUpload = async (blob, mime) => {
    setUploading(true);
    try {
      const ext = mime === 'image/png' ? '.png' : '.jpg';
      const { url } = await WeddingAPI.uploadFile(new File([blob], `crop-${Date.now()}${ext}`, { type: mime }));
      onChange(url);
      Toast.success('图片已上传');
    } catch (err) { Toast.error('上传失败：' + err.message); }
    setUploading(false);
  };

  // 不裁剪，直接上传原图
  const uploadRawFile = async (file) => {
    setUploading(true);
    try {
      const { url } = await WeddingAPI.uploadFile(file);
      onChange(url);
      Toast.success('图片已上传');
    } catch (err) { Toast.error('上传失败：' + err.message); }
    setUploading(false);
  };

  const handleRaw = async () => {
    const file = crop.file;
    closeCrop();
    uploadRawFile(file);
  };

  return (
    <div className="form-field">
      <label>{label}</label>
      <div className="img-field-row">
        <div className="img-thumb">
          {value ? <img src={value} alt={label} /> : <span className="img-thumb-empty">未设置</span>}
        </div>
        <label className="upload-btn">
          {Icon.image}<span>{uploading ? '上传中…' : '上传图片'}</span>
          <input type="file" accept="image/*" style={{ display: 'none' }} onChange={handleFile} />
        </label>
        {value && ratio && (
          <button className="btn btn-outline btn-sm" onClick={() => setCrop({ src: value, mime: '', file: null })}>
            {Icon.edit}<span>调整裁剪</span>
          </button>
        )}
        {value && (
          <button className="btn btn-ghost btn-sm" onClick={() => onChange('')}>
            {Icon.trash}<span>移除</span>
          </button>
        )}
      </div>
      {hint && <p className="form-tip">{hint}</p>}
      {crop && (
        <CropModal
          src={crop.src}
          mime={crop.mime}
          ratio={ratio || 1}
          shape={shape}
          label={label}
          rawFile={crop.file}
          onRaw={handleRaw}
          onCancel={closeCrop}
          onConfirm={(blob, mime) => { closeCrop(); doUpload(blob, mime); }}
        />
      )}
    </div>
  );
}

/* 小程序效果实时预览：按 375px 屏宽 1:1 复刻小程序排版后整体缩放，
   封面/头像 object-fit: cover 与小程序 aspectFill 居中裁剪效果一致，故事图按原比例（widthFix）显示 */
const PV_TABS = [
  { key: 'cover', label: '封面' },
  { key: 'couple', label: '新人' },
  { key: 'story', label: '故事' },
];

function PhonePreview({ form }) {
  const [tab, setTab] = useState('cover');
  const parts = (form.date || '').split('-');
  const year = parts[0] || '';
  const monthDay = parts.length === 3 ? `${parts[1]}.${parts[2]}` : '';
  return (
    <div>
      <div className="preview-phone">
        <div className="preview-tabs">
          {PV_TABS.map(t => (
            <button key={t.key} className={`preview-tab ${tab === t.key ? 'active' : ''}`} onClick={() => setTab(t.key)}>
              {t.label}
            </button>
          ))}
        </div>
        <div className="preview-viewport">
          {tab === 'cover' && (
            <div className="preview-screen pv-cover">
              <div className="pv-frame">
                <div className="pv-photo-frame">
                  <div className="pv-photo">
                    {form.coverPhoto ? <img src={form.coverPhoto} alt="" /> : <div className="pv-photo-empty">Z & G</div>}
                  </div>
                </div>
                <div className="pv-wedding">WEDDING INVITATION</div>
                <div className="pv-names">
                  <div className="pv-name">{form.groom}</div>
                  <div className="pv-amp">&</div>
                  <div className="pv-name">{form.bride}</div>
                </div>
                <div className="pv-tagline">{form.tagline}</div>
                <div className="pv-date">
                  <div className="pv-year">{year}</div>
                  <div className="pv-date-num">{monthDay}</div>
                  <div className="pv-date-text">{form.lunarDate}</div>
                </div>
              </div>
            </div>
          )}
          {tab === 'couple' && (
            <div className="preview-screen pv-section">
              <div className="pv-sec-header">
                <div className="pv-sec-label">The Couple</div>
                <div className="pv-sec-title">新 人 介 绍</div>
                <div className="pv-sec-divider" />
              </div>
              <div className="pv-person">
                <div className="pv-avatar">{form.groomAvatar ? <img src={form.groomAvatar} alt="" /> : 'Z'}</div>
                <div className="pv-pinfo">
                  <div className="pv-pname">{form.groom}</div>
                  <div className="pv-prole">The Groom</div>
                  <div className="pv-pdesc">{form.groomDesc}</div>
                </div>
              </div>
              <div className="pv-and">&</div>
              <div className="pv-person right">
                <div className="pv-avatar">{form.brideAvatar ? <img src={form.brideAvatar} alt="" /> : 'G'}</div>
                <div className="pv-pinfo">
                  <div className="pv-pname">{form.bride}</div>
                  <div className="pv-prole">The Bride</div>
                  <div className="pv-pdesc">{form.brideDesc}</div>
                </div>
              </div>
            </div>
          )}
          {tab === 'story' && (
            <div className="preview-screen pv-section">
              <div className="pv-sec-header">
                <div className="pv-sec-label">Our Story</div>
                <div className="pv-sec-title">爱 情 故 事</div>
                <div className="pv-sec-divider" />
              </div>
              <div className="pv-timeline">
                {(form.story || []).map((item, i) => (
                  <div key={i} className="pv-story">
                    <div className="pv-sdate">
                      <div className="pv-syear">{item.year}</div>
                      <div className="pv-smonth">{item.month}</div>
                    </div>
                    <div className="pv-scontent">
                      {item.image && <div className="pv-simage"><img src={item.image} alt="" /></div>}
                      <div className="pv-stitle">{item.title}</div>
                      <div className="pv-sdesc">{item.desc}</div>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          )}
        </div>
      </div>
      <p className="preview-note">小程序实时预览，封面/头像按居中裁剪、故事图按原比例显示<br />与真实小程序排版一致</p>
    </div>
  );
}

function ContentForm({ config, onConfigChange }) {
  const [form, setForm] = useState(null);
  const [dirty, setDirty] = useState(false);
  const [saving, setSaving] = useState(false);

  // config 异步到位后初始化表单（只初始化一次）
  useEffect(() => {
    if (config && !form) setForm(config);
  }, [config, form]);

  if (!form) return <div className="empty-tip">加载中…</div>;

  const set = (k, v) => { setForm(f => ({ ...f, [k]: v })); setDirty(true); };
  const setStory = (i, patch) => {
    setForm(f => {
      const story = [...f.story];
      story[i] = { ...story[i], ...patch };
      return { ...f, story };
    });
    setDirty(true);
  };
  const addStory = () => {
    setForm(f => ({ ...f, story: [...f.story, { year: '202X', month: 'XX月', title: '新节点', desc: '', image: '' }] }));
    setDirty(true);
  };
  const removeStory = (i) => {
    if (form.story.length <= 1) return;
    if (!confirm('确定删除这个故事节点吗？')) return;
    setForm(f => ({ ...f, story: f.story.filter((_, idx) => idx !== i) }));
    setDirty(true);
  };

  const handleSave = async () => {
    if (saving) return;
    setSaving(true);
    try {
      await WeddingAPI.saveConfig(form);
      onConfigChange(form);
      setDirty(false);
      Toast.success('已保存，所有访客刷新生效');
    } catch (err) { Toast.error('保存失败：' + err.message); }
    setSaving(false);
  };

  const F = ({ label, k, placeholder }) => (
    <div className="form-field">
      <label>{label}</label>
      <input type="text" value={form[k] || ''} placeholder={placeholder || ''} onChange={(e) => set(k, e.target.value)} />
    </div>
  );

  return (
    <div className="content-layout">
    <div className="content-form">
      <div className="card">
        <h3 className="section-title">新人信息</h3>
        <div className="form-grid">
          <F label="新郎姓名" k="groom" />
          <F label="新娘姓名" k="bride" />
        </div>
        <div className="form-grid">
          <F label="新郎一句话" k="groomDesc" />
          <F label="新娘一句话" k="brideDesc" />
        </div>
        <F label="封面标语" k="tagline" />
      </div>

      <div className="card">
        <h3 className="section-title">婚礼信息</h3>
        <div className="form-grid">
          <div className="form-field">
            <label>日期</label>
            <input type="date" value={form.date || ''} onChange={(e) => set('date', e.target.value)} />
          </div>
          <div className="form-field">
            <label>时间</label>
            <input type="time" value={form.time || ''} onChange={(e) => set('time', e.target.value)} />
          </div>
        </div>
        <div className="form-grid">
          <F label="农历日期" k="lunarDate" placeholder="如：农历八月十七 · 星期日" />
          <F label="婚礼形式" k="venueSub" placeholder="如：草坪婚礼" />
        </div>
        <F label="酒店名称" k="venue" />
        <F label="详细地址" k="address" />
        <div className="form-grid">
          <F label="导航纬度" k="lat" placeholder="如：34.9267" />
          <F label="导航经度" k="lng" placeholder="如：113.5958" />
        </div>
        <p className="form-tip">导航坐标用于小程序「导航到现场」按钮，可从腾讯地图/高德地图拾取；留空则使用内置默认坐标</p>
      </div>

      <div className="card">
        <h3 className="section-title">封底文案</h3>
        <div className="form-field">
          <label>邀请语（可多行）</label>
          <textarea rows={2} value={form.closingText || ''} onChange={(e) => set('closingText', e.target.value)} />
        </div>
        <div className="form-grid">
          <F label="英文邀请语" k="closingEn" />
          <F label="城市" k="closingCity" />
        </div>
        <F label="封底大字" k="closingMonogram" placeholder="留空则自动取新郎/新娘姓名首字，如：楠 & 瑾" />
      </div>

      <div className="card">
        <h3 className="section-title">图片</h3>
        <div className="form-grid">
          <ImageField label="封面照" ratio={10 / 13} shape="arch" hint="拱形相框（宽:高=10:13），上传后可拖动调整显示区域" value={form.coverPhoto} onChange={(v) => set('coverPhoto', v)} />
          <ImageField label="新郎头像" ratio={1} shape="circle" hint="将以圆形显示" value={form.groomAvatar} onChange={(v) => set('groomAvatar', v)} />
          <ImageField label="新娘头像" ratio={1} shape="circle" hint="将以圆形显示" value={form.brideAvatar} onChange={(v) => set('brideAvatar', v)} />
        </div>
      </div>

      <div className="card">
        <h3 className="section-title">分享卡片</h3>
        <F label="分享标题" k="shareTitle" placeholder="留空则自动使用「新郎🤍新娘 的婚礼请柬」" />
        <ImageField label="分享配图" ratio={5 / 4} shape="rect" hint="转发给好友/群的卡片缩略图（宽:高=5:4，建议 500×400）；留空则使用小程序内置分享封面" value={form.shareImage} onChange={(v) => set('shareImage', v)} />
      </div>

      <div className="card">
        <h3 className="section-title">爱情故事时间线</h3>
        {form.story.map((item, i) => (
          <div key={i} className="story-node">
            <div className="form-grid form-grid-4">
              <div className="form-field">
                <label>年份</label>
                <input type="text" value={item.year} onChange={(e) => setStory(i, { year: e.target.value })} />
              </div>
              <div className="form-field">
                <label>月份</label>
                <input type="text" value={item.month} onChange={(e) => setStory(i, { month: e.target.value })} />
              </div>
              <div className="form-field" style={{ gridColumn: 'span 2' }}>
                <label>标题</label>
                <input type="text" value={item.title} onChange={(e) => setStory(i, { title: e.target.value })} />
              </div>
            </div>
            <div className="form-field">
              <label>故事描述</label>
              <textarea rows={2} value={item.desc} onChange={(e) => setStory(i, { desc: e.target.value })} />
            </div>
            <ImageField label="配图（可选）" hint="按原图比例显示（宽度撑满，高度自适应）" value={item.image} onChange={(v) => setStory(i, { image: v })} />
            <div style={{ textAlign: 'right' }}>
              <button className="btn btn-danger btn-sm" onClick={() => removeStory(i)}>
                {Icon.trash}<span>删除此节点</span>
              </button>
            </div>
          </div>
        ))}
        <button className="btn btn-outline" onClick={addStory}>
          {Icon.plus}<span>添加故事节点</span>
        </button>
      </div>

      <div className="form-save-bar">
        {dirty && <span className="form-dirty-tip">有未保存的修改</span>}
        <button className="btn btn-primary" onClick={handleSave} disabled={saving || !dirty}>
          {saving ? '保存中…' : '保存内容'}
        </button>
      </div>
    </div>
    <div className="preview-col">
      <PhonePreview form={form} />
    </div>
    </div>
  );
}

/* ============================================================
   RSVP 编辑弹窗：管理员修改某条回执（姓名/出席/人数/抵达/住宿/备注），
   校验规则与小程序提交接口一致（服务端 parseRsvpBody 会再校验一遍）
   ============================================================ */
function RsvpEditModal({ record, onClose, onSaved }) {
  const [f, setF] = useState({
    name: record.name || '',
    attend: record.attend === 'no' ? 'no' : 'yes',
    side: ['groom', 'bride', 'both'].includes(record.side) ? record.side : 'groom',
    maleCount: record.maleCount || 0,
    femaleCount: record.femaleCount || 0,
    childCount: record.childCount || 0,
    arrivalTime: record.arrivalTime || '',
    roomNeed: record.roomNeed || 0,
    note: record.note || '',
  });
  const [saving, setSaving] = useState(false);
  const upd = (k, v) => setF(prev => ({ ...prev, [k]: v }));
  const num = (v) => Math.max(0, Math.min(99, Number.parseInt(v, 10) || 0));

  const save = async () => {
    if (!f.name.trim()) { Toast.error('请填写姓名'); return; }
    if (f.attend === 'yes' && num(f.maleCount) + num(f.femaleCount) + num(f.childCount) === 0) {
      Toast.error('出席人数至少为 1');
      return;
    }
    setSaving(true);
    try {
      await WeddingAPI.updateRsvp(record.id, {
        name: f.name,
        attend: f.attend,
        side: f.side,
        maleCount: num(f.maleCount),
        femaleCount: num(f.femaleCount),
        childCount: num(f.childCount),
        arrivalTime: f.arrivalTime,
        roomNeed: num(f.roomNeed),
        note: f.note,
      });
      Toast.success('已保存');
      onSaved();
    } catch (err) {
      Toast.error('保存失败：' + err.message);
    } finally {
      setSaving(false);
    }
  };

  return (
    <div className="crop-mask" onClick={(e) => e.target === e.currentTarget && onClose()}>
      <div className="crop-modal rsvp-modal">
        <div className="crop-title">编辑 RSVP 回执</div>
        <div className="form-grid">
          <div className="form-field">
            <label>姓名</label>
            <input type="text" value={f.name} maxLength={20} onChange={(e) => upd('name', e.target.value)} />
          </div>
          <div className="form-field">
            <label>是否出席</label>
            <select value={f.attend} onChange={(e) => upd('attend', e.target.value)}>
              <option value="yes">参加</option>
              <option value="no">不参加</option>
            </select>
          </div>
          {f.attend === 'yes' && (
            <>
              <div className="form-field">
                <label>所属方</label>
                <select value={f.side} onChange={(e) => upd('side', e.target.value)}>
                  <option value="groom">新郎方</option>
                  <option value="bride">新娘方</option>
                  <option value="both">双方好友</option>
                </select>
              </div>
              <div className="form-field">
                <label>预计抵达日期</label>
                <input type="text" value={f.arrivalTime} placeholder="如 2026-10-01" maxLength={50}
                  onChange={(e) => upd('arrivalTime', e.target.value)} />
              </div>
              <div className="form-field">
                <label>男宾人数</label>
                <input type="number" min="0" max="99" value={f.maleCount} onChange={(e) => upd('maleCount', e.target.value)} />
              </div>
              <div className="form-field">
                <label>女宾人数</label>
                <input type="number" min="0" max="99" value={f.femaleCount} onChange={(e) => upd('femaleCount', e.target.value)} />
              </div>
              <div className="form-field">
                <label>儿童人数</label>
                <input type="number" min="0" max="99" value={f.childCount} onChange={(e) => upd('childCount', e.target.value)} />
              </div>
              <div className="form-field">
                <label>住宿房间数</label>
                <input type="number" min="0" max="10" value={f.roomNeed} onChange={(e) => upd('roomNeed', e.target.value)} />
              </div>
            </>
          )}
          <div className="form-field" style={{ gridColumn: '1 / -1' }}>
            <label>备注</label>
            <textarea rows="3" value={f.note} maxLength={200} placeholder="选填"
              onChange={(e) => upd('note', e.target.value)} />
          </div>
        </div>
        <div className="crop-actions" style={{ marginTop: 16 }}>
          <button className="btn btn-outline" onClick={onClose}>取消</button>
          <button className="btn btn-primary" onClick={save} disabled={saving}>
            {saving ? '保存中…' : '保存'}
          </button>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   管理后台面板（PC 布局）
   ============================================================ */
function AdminPanel({ config, onConfigChange, onLogout }) {
  const [tab, setTab] = useState('overview');
  const [newPassword, setNewPassword] = useState('');
  const [showPwdTip, setShowPwdTip] = useState(false);

  // 服务端数据：统计、留言（含待审核）、RSVP
  const [stats, setStats] = useState(null);
  const [blessList, setBlessList] = useState([]);
  const [rsvpAll, setRsvpAll] = useState([]);

  const refresh = useCallback(() => {
    Promise.all([
      WeddingAPI.getStats(),
      WeddingAPI.adminListBlessings(),
      WeddingAPI.adminListRsvp(),
    ]).then(([s, bl, rl]) => {
      setStats(s);
      setBlessList(bl);
      setRsvpAll(rl);
    }).catch((err) => {
      // token 失效（api.js 已清除）时退回登录页由 AdminApp 处理
      if (!WeddingAPI.getToken()) onLogout();
      else Toast.error('数据加载失败：' + err.message);
    });
  }, [onLogout]);

  useEffect(() => { refresh(); }, [refresh]);

  const musicUrl = config?.musicUrl || '';

  // ---------- 留言管理 ----------
  const [blessFilter, setBlessFilter] = useState('all');
  const filteredBlessings = useMemo(() => {
    let list = [...blessList];
    if (blessFilter !== 'all') list = list.filter(b => b.status === blessFilter);
    return list.sort((a, b) => {
      if (a.pinned && !b.pinned) return -1;
      if (!a.pinned && b.pinned) return 1;
      return b.id - a.id; // 新的在前
    });
  }, [blessList, blessFilter]);

  const togglePin = async (id) => {
    const current = blessList.find(b => b.id === id);
    if (!current) return;
    try {
      await WeddingAPI.updateBlessing(id, { pinned: !current.pinned });
      refresh();
      Toast.success(current.pinned ? '已取消置顶' : '已置顶');
    } catch (err) { Toast.error('操作失败：' + err.message); }
  };
  const toggleStatus = async (id) => {
    const current = blessList.find(b => b.id === id);
    if (!current) return;
    try {
      await WeddingAPI.updateBlessing(id, { status: current.status === 'approved' ? 'pending' : 'approved' });
      refresh();
      Toast.success(current.status === 'approved' ? '已隐藏该留言' : '已显示该留言');
    } catch (err) { Toast.error('操作失败：' + err.message); }
  };
  const deleteBlessing = async (id) => {
    if (!confirm('确定删除这条祝福吗？')) return;
    try {
      await WeddingAPI.deleteBlessing(id);
      refresh();
      Toast.success('已删除');
    } catch (err) { Toast.error('删除失败：' + err.message); }
  };

  // ---------- RSVP 管理 ----------
  const [rsvpFilter, setRsvpFilter] = useState('all');
  const filteredRsvp = useMemo(() => {
    let list = [...rsvpAll];
    if (rsvpFilter === 'yes') list = list.filter(r => r.attend === 'yes');
    else if (rsvpFilter === 'no') list = list.filter(r => r.attend === 'no');
    return list.sort((a, b) => b.id - a.id);
  }, [rsvpAll, rsvpFilter]);

  const deleteRsvp = async (id) => {
    if (!confirm('确定删除这条RSVP记录吗？')) return;
    try {
      await WeddingAPI.deleteRsvp(id);
      refresh();
      Toast.success('已删除');
    } catch (err) { Toast.error('删除失败：' + err.message); }
  };

  // 编辑弹窗：editingRsvp 为正在编辑的记录，null 表示关闭
  const [editingRsvp, setEditingRsvp] = useState(null);

  const resetRsvp = async () => {
    if (!confirm('确定重置所有RSVP数据吗？来宾再次打开时会重新弹出RSVP填写窗口。')) return;
    try {
      await Promise.all(rsvpAll.map(r => WeddingAPI.deleteRsvp(r.id)));
      refresh();
      Toast.success('已重置所有RSVP数据');
      try { localStorage.removeItem(RSVP_SUBMITTED_KEY); } catch (e) {}
    } catch (err) { Toast.error('重置失败：' + err.message); }
  };

  const exportCSV = () => {
    if (rsvpAll.length === 0) {
      Toast.success('暂无RSVP数据可导出');
      return;
    }
    const headers = ['序号', '姓名', '是否出席', '所属方', '男宾数', '女宾数', '儿童数', '总人数', '抵达时间', '房间需求', '留言/备注', '提交时间'];
    const sideMap = { groom: '新郎方', bride: '新娘方', both: '双方好友' };
    const attendMap = { yes: '参加', no: '不参加' };
    const rows = rsvpAll.map((r, i) => [
      i + 1,
      r.name || '',
      attendMap[r.attend] || r.attend,
      sideMap[r.side] || r.side || '',
      r.maleCount || 0,
      r.femaleCount || 0,
      r.childCount || 0,
      r.totalCount || 0,
      r.arrivalTime || '',
      r.roomNeed || 0,
      (r.note || '').replace(/\n/g, ' '),
      r.time || '',
    ]);
    const escape = (v) => {
      let s = String(v ?? '');
      // 防止 CSV 公式注入（Excel 会执行以 = + - @ 开头的单元格）
      if (/^[=+\-@\t\r]/.test(s)) s = "'" + s;
      if (s.includes(',') || s.includes('"') || s.includes('\n')) return '"' + s.replace(/"/g, '""') + '"';
      return s;
    };
    const csv = [headers, ...rows].map(row => row.map(escape).join(',')).join('\n');
    // 添加 BOM 以兼容 Excel 中文
    const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    const now = new Date();
    const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
    a.href = url;
    a.download = `婚礼RSVP名单_${dateStr}.csv`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  };

  // ---------- 背景音乐 ----------
  const saveMusicUrl = async (url) => {
    try {
      const next = { ...config, musicUrl: url };
      await WeddingAPI.saveConfig(next);
      onConfigChange(next);
      Toast.success('音乐已保存');
    } catch (err) { Toast.error('保存失败：' + err.message); }
  };
  const handleMusicBlur = (e) => {
    const url = e.target.value.trim();
    if (url !== musicUrl) saveMusicUrl(url);
  };
  const handleMusicUpload = async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    try {
      const { url } = await WeddingAPI.uploadFile(file);
      await saveMusicUrl(url);
    } catch (err) { Toast.error('上传失败：' + err.message); }
    e.target.value = '';
  };

  // ---------- 设置 ----------
  const handleResetData = async () => {
    if (!confirm('确定恢复所有内容为默认值？所有修改的文字、图片都会被清除（不影响祝福留言和 RSVP 数据）。')) return;
    try {
      const def = await WeddingAPI.resetConfig();
      onConfigChange(def);
      Toast.success('已恢复默认内容');
    } catch (err) { Toast.error('重置失败：' + err.message); }
  };

  const handleChangePwd = async () => {
    if (!newPassword || newPassword.length < 6) {
      Toast.error('新口令至少 6 位');
      return;
    }
    try {
      await WeddingAPI.changePassword(newPassword);
      setShowPwdTip(true);
      setTimeout(() => setShowPwdTip(false), 2000);
      setNewPassword('');
    } catch (err) { Toast.error('修改失败：' + err.message); }
  };

  const s = stats || {};
  const num = (v) => v ?? 0;
  const currentNav = NAV_ITEMS.find(n => n.key === tab);
  const sideText = (side) => side === 'groom' ? '新郎方' : side === 'bride' ? '新娘方' : side === 'both' ? '双方好友' : '-';

  return (
    <div className="admin-layout">
      {/* 侧边栏 */}
      <aside className="admin-sidebar">
        <div className="admin-brand">
          <div className="admin-brand-title">婚礼请柬</div>
          <div className="admin-brand-sub">Admin Dashboard</div>
        </div>
        <nav className="admin-nav">
          {NAV_ITEMS.map(n => (
            <button
              key={n.key}
              className={`admin-nav-item ${tab === n.key ? 'active' : ''}`}
              onClick={() => setTab(n.key)}
            >
              {n.icon}<span>{n.label}</span>
            </button>
          ))}
        </nav>
      </aside>

      {/* 内容区 */}
      <main className="admin-main">
        <div className="admin-topbar">
          <div className="admin-topbar-title">{currentNav?.label || ''}</div>
          <div className="admin-topbar-actions">
            <button className="btn btn-outline" onClick={onLogout}>
              {Icon.logout}<span>退出登录</span>
            </button>
          </div>
        </div>

        <div className="admin-content">
          {/* 概览 */}
          {tab === 'overview' && (
            <div>
              <div className="stat-grid">
                <div className="stat-card">
                  <div className="stat-num">{num(s.visitCount)}</div>
                  <div className="stat-label">访问次数</div>
                </div>
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-olive)' }}>{num(s.rsvpCount)}</div>
                  <div className="stat-label">RSVP回复</div>
                </div>
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-rose-deep)' }}>{num(s.total)}</div>
                  <div className="stat-label">祝福总数</div>
                </div>
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-gold-deep)' }}>{num(s.totalPeople)}</div>
                  <div className="stat-label">确认出席人数</div>
                </div>
              </div>

              <div className="stat-grid stat-grid-3">
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-olive)' }}>{num(s.attendYes)}</div>
                  <div className="stat-label">参加</div>
                </div>
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-terracotta)' }}>{num(s.attendNoCount)}</div>
                  <div className="stat-label">不参加</div>
                </div>
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-blue)' }}>{num(s.approved)}</div>
                  <div className="stat-label">已显示留言</div>
                </div>
              </div>

              <h3 className="section-title" style={{ marginTop: '28px' }}>出席构成</h3>
              <div className="stat-grid">
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-gold-deep)' }}>{num(s.maleTotal)}</div>
                  <div className="stat-label">男宾</div>
                </div>
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-rose-deep)' }}>{num(s.femaleTotal)}</div>
                  <div className="stat-label">女宾</div>
                </div>
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-blue)' }}>{num(s.childTotal)}</div>
                  <div className="stat-label">儿童</div>
                </div>
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-olive)' }}>{num(s.totalPeople)}</div>
                  <div className="stat-label">合计</div>
                </div>
              </div>

              <div className="stat-grid stat-grid-3">
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-blue)' }}>{num(s.groomSide)}</div>
                  <div className="stat-label">新郎方</div>
                </div>
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-rose-deep)' }}>{num(s.brideSide)}</div>
                  <div className="stat-label">新娘方</div>
                </div>
                <div className="stat-card">
                  <div className="stat-num" style={{ color: 'var(--c-olive)' }}>{num(s.bothSide)}</div>
                  <div className="stat-label">双方好友</div>
                </div>
              </div>

              <div className="quick-actions">
                <button className="btn btn-primary" onClick={() => setTab('rsvp')}>
                  {Icon.rsvp}<span>查看 RSVP 名单</span>
                </button>
                <button className="btn btn-outline" onClick={exportCSV}>
                  {Icon.download}<span>导出 RSVP CSV</span>
                </button>
              </div>
            </div>
          )}

          {/* RSVP 管理（表格） */}
          {tab === 'rsvp' && (
            <div>
              <div className="toolbar">
                <button className={`filter-btn ${rsvpFilter === 'all' ? 'active' : ''}`} onClick={() => setRsvpFilter('all')}>全部 ({num(s.rsvpCount)})</button>
                <button className={`filter-btn ${rsvpFilter === 'yes' ? 'active' : ''}`} onClick={() => setRsvpFilter('yes')}>参加 ({num(s.attendYes)})</button>
                <button className={`filter-btn ${rsvpFilter === 'no' ? 'active' : ''}`} onClick={() => setRsvpFilter('no')}>不参加 ({num(s.attendNoCount)})</button>
                <div className="toolbar-spacer" />
                <span className="section-desc">确认出席 <strong style={{ color: 'var(--c-olive)' }}>{num(s.totalPeople)}</strong> 人</span>
                <button className="btn btn-outline btn-sm" onClick={exportCSV}>
                  {Icon.download}<span>导出CSV</span>
                </button>
                <button className="btn btn-danger btn-sm" onClick={resetRsvp}>
                  {Icon.rotate}<span>重置所有RSVP</span>
                </button>
              </div>
              <div className="card table-wrap">
                {filteredRsvp.length === 0 ? (
                  <div className="empty-tip">暂无RSVP记录</div>
                ) : (
                  <table className="data-table">
                    <thead>
                      <tr>
                        <th>姓名</th>
                        <th>出席</th>
                        <th>所属方</th>
                        <th>人数（男/女/童）</th>
                        <th>抵达时间</th>
                        <th>住宿</th>
                        <th>备注</th>
                        <th>提交时间</th>
                        <th>操作</th>
                      </tr>
                    </thead>
                    <tbody>
                      {filteredRsvp.map(r => (
                        <tr key={r.id}>
                          <td>{r.name || '匿名来宾'}</td>
                          <td><span className={`badge badge-${r.attend}`}>{r.attend === 'yes' ? '参加' : '不参加'}</span></td>
                          <td>{r.attend === 'yes' ? sideText(r.side) : '-'}</td>
                          <td>{r.attend === 'yes' ? `${r.totalCount}（${r.maleCount}/${r.femaleCount}/${r.childCount || 0}）` : '-'}</td>
                          <td>{r.arrivalTime || '-'}</td>
                          <td>{r.roomNeed > 0 ? `${r.roomNeed} 间` : '-'}</td>
                          <td><div className="cell-wrap">{r.note || '-'}</div></td>
                          <td>{r.time}</td>
                          <td>
                            <div className="cell-actions">
                              <button className="btn btn-outline btn-sm" onClick={() => setEditingRsvp(r)}>
                                {Icon.edit}<span>编辑</span>
                              </button>
                              <button className="btn btn-danger btn-sm" onClick={() => deleteRsvp(r.id)}>
                                {Icon.trash}<span>删除</span>
                              </button>
                            </div>
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                )}
              </div>
            </div>
          )}

          {/* RSVP 编辑弹窗（任意标签页下均可挂载，仅 rsvp 页会触发） */}
          {editingRsvp && (
            <RsvpEditModal
              record={editingRsvp}
              onClose={() => setEditingRsvp(null)}
              onSaved={() => { setEditingRsvp(null); refresh(); }}
            />
          )}

          {/* 留言管理（两列卡片） */}
          {tab === 'blessings' && (
            <div>
              <div className="toolbar">
                <button className={`filter-btn ${blessFilter === 'all' ? 'active' : ''}`} onClick={() => setBlessFilter('all')}>全部 ({num(s.total)})</button>
                <button className={`filter-btn ${blessFilter === 'approved' ? 'active' : ''}`} onClick={() => setBlessFilter('approved')}>已通过 ({num(s.approved)})</button>
                <button className={`filter-btn ${blessFilter === 'pending' ? 'active' : ''}`} onClick={() => setBlessFilter('pending')}>待审核 ({num(s.pending)})</button>
              </div>
              {filteredBlessings.length === 0 ? (
                <div className="card"><div className="empty-tip">暂无数据</div></div>
              ) : (
                <div className="bless-grid">
                  {filteredBlessings.map(b => (
                    <div key={b.id} className={`bless-card ${b.pinned ? 'pinned' : ''}`}>
                      <div className="bless-card-head">
                        <span className="bless-name">{b.name}{b.pinned ? ' 📌' : ''}</span>
                        <span className="bless-time">{b.time}</span>
                      </div>
                      <div className="bless-text">{b.text}</div>
                      <div className="bless-actions">
                        <button className={`btn btn-ghost btn-sm ${b.pinned ? 'active' : ''}`} onClick={() => togglePin(b.id)}>
                          {Icon.pin}<span>{b.pinned ? '取消置顶' : '置顶'}</span>
                        </button>
                        <button className={`btn btn-ghost btn-sm ${b.status === 'approved' ? 'active' : ''}`} onClick={() => toggleStatus(b.id)}>
                          {b.status === 'approved' ? Icon.eye : Icon.eyeOff}
                          <span>{b.status === 'approved' ? '已显示' : '已隐藏'}</span>
                        </button>
                        <button className="btn btn-danger btn-sm" onClick={() => deleteBlessing(b.id)}>
                          {Icon.trash}<span>删除</span>
                        </button>
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </div>
          )}

          {/* 内容编辑（请柬内容表单） */}
          {tab === 'content' && (
            <ContentForm config={config} onConfigChange={onConfigChange} />
          )}

          {/* 背景音乐 */}
          {tab === 'music' && (
            <div className="form-card">
              <div className="form-field">
                <label>音乐链接</label>
                <input
                  type="text"
                  defaultValue={musicUrl}
                  key={musicUrl}
                  onBlur={handleMusicBlur}
                  placeholder="输入 mp3 链接"
                />
                <p className="form-tip">填入直链 mp3 地址，失焦后自动保存</p>
              </div>
              <div className="form-field">
                <label>上传音乐</label>
                <label className="upload-btn">
                  {Icon.musicNote}<span>选择本地 mp3 文件</span>
                  <input type="file" accept="audio/*" style={{ display: 'none' }} onChange={handleMusicUpload} />
                </label>
                <p className="form-tip">文件将上传到服务器，所有设备生效</p>
              </div>
              {musicUrl && (
                <div className="form-field">
                  <label>试听</label>
                  <audio controls src={musicUrl} style={{ width: '100%' }} />
                </div>
              )}
            </div>
          )}

          {/* 设置 */}
          {tab === 'settings' && (
            <div className="card" style={{ maxWidth: '640px' }}>
              <h3 className="section-title">数据管理</h3>
              <div className="setting-row">
                <div>
                  <div className="setting-title">恢复默认内容</div>
                  <div className="setting-desc">重置所有文字、图片为初始值（不影响祝福留言和 RSVP 数据）</div>
                </div>
                <button className="btn btn-danger" onClick={handleResetData}>
                  {Icon.rotate}<span>恢复默认</span>
                </button>
              </div>
              <div className="setting-row">
                <div>
                  <div className="setting-title">重置所有 RSVP</div>
                  <div className="setting-desc">清空已收到的所有RSVP回复，并让来宾再次打开时重新弹出填写窗口</div>
                </div>
                <button className="btn btn-danger" onClick={resetRsvp}>
                  {Icon.rotate}<span>重置RSVP</span>
                </button>
              </div>

              <h3 className="section-title" style={{ marginTop: '28px' }}>账号安全</h3>
              <div className="form-field">
                <label>修改管理员口令</label>
                <input
                  type="password"
                  value={newPassword}
                  onChange={(e) => setNewPassword(e.target.value)}
                  placeholder="输入新口令（至少 6 位）"
                />
                <div style={{ marginTop: '10px' }}>
                  <button className="btn btn-primary" onClick={handleChangePwd}>
                    {Icon.lock}<span>保存新口令</span>
                  </button>
                </div>
                {showPwdTip && <div className="pwd-ok-tip">口令已更新</div>}
              </div>
            </div>
          )}
        </div>
      </main>
    </div>
  );
}

/* ============================================================
   根组件：按登录态切换登录卡片 / 管理面板
   ============================================================ */
function AdminApp() {
  const [loggedIn, setLoggedIn] = useState(() => !!WeddingAPI.getToken());
  const [config, setConfig] = useState(null);

  // 登录后拉取请柬配置（音乐地址、恢复默认的数据源）
  useEffect(() => {
    if (!loggedIn) return;
    WeddingAPI.getConfig()
      .then(cfg => setConfig(cfg))
      .catch(() => { if (!WeddingAPI.getToken()) setLoggedIn(false); });
  }, [loggedIn]);

  const handleLogout = () => {
    WeddingAPI.setToken('');
    setLoggedIn(false);
  };

  if (!loggedIn) {
    return <LoginCard onLogin={() => setLoggedIn(true)} />;
  }
  return (
    <React.Fragment>
      <AdminPanel config={config} onConfigChange={setConfig} onLogout={handleLogout} />
      <ToastHost />
    </React.Fragment>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<AdminApp />);
