/* ============================================================
   汇能科技仓库 ERP v3 — 通用 React 组件
   Modal / StockBadge / EmptyState / Toast / Shell(含移动端导航+用户信息)
   / ScannerModal(摄像头条码扫描) / RoleBadge / StatusBadge / PageLoader
   ============================================================ */

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

/* ----- 图标（内联 SVG）----- */
const Icon = {
  Dashboard: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="9" /><rect x="14" y="3" width="7" height="5" /><rect x="14" y="12" width="7" height="9" /><rect x="3" y="16" width="7" height="5" /></svg>),
  Box: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /><polyline points="3.27 6.96 12 12.01 20.73 6.96" /><line x1="12" y1="22.08" x2="12" y2="12" /></svg>),
  Warehouse: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 21V9l9-6 9 6v12" /><path d="M9 21V12h6v9" /><path d="M9 21h6" /></svg>),
  Down: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 5v14" /><polyline points="19 12 12 19 5 12" /></svg>),
  Up: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19V5" /><polyline points="5 12 12 5 19 12" /></svg>),
  Stock: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 12V8H6a2 2 0 0 1-2-2c0-1.1.9-2 2-2h12v4" /><path d="M4 6v12c0 1.1.9 2 2 2h14v-4" /><path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z" /></svg>),
  Report: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3v18h18" /><path d="M7 14l4-4 4 4 5-5" /></svg>),
  Plus: () => (<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>),
  Edit: () => (<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" /></svg>),
  Trash: () => (<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" /><path d="M10 11v6" /><path d="M14 11v6" /></svg>),
  Close: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>),
  Check: () => (<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12" /></svg>),
  Search: () => (<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" /></svg>),
  Download: () => (<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" 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>),
  Warn: () => (<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" /><line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" /></svg>),
  Camera: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" /><circle cx="12" cy="13" r="4" /></svg>),
  Settings: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" 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 0 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 0 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 0 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 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /></svg>),
  Logout: () => (<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" 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>),
  Users: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" 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>),
  Clipboard: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" /><rect x="8" y="2" width="8" height="4" rx="1" /></svg>),
  Truck: () => (<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="1" y="3" width="15" height="13" /><polygon points="16 8 20 8 23 11 23 16 16 16 16 8" /><circle cx="5.5" cy="18.5" r="2.5" /><circle cx="18.5" cy="18.5" r="2.5" /></svg>),
};

/* ----- 模态框 ----- */
function Modal({ title, onClose, children, footer, size, className }) {
  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose && onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose]);

  return (
    <div className={'modal-mask' + (className ? ' ' + className : '')} onMouseDown={(e) => { if (e.target === e.currentTarget) onClose && onClose(); }}>
      <div className={'modal' + (size === 'lg' ? ' modal-lg' : '')}>
        <div className="modal-header">
          <div className="modal-title">{title}</div>
          <button className="btn btn-ghost btn-icon" onClick={onClose} aria-label="关闭"><Icon.Close /></button>
        </div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-footer">{footer}</div>}
      </div>
    </div>
  );
}

/* ----- 库存徽章 ----- */
function StockBadge({ value, threshold = 10 }) {
  let cls = 'stock-ok';
  let label = '正常';
  if (value <= 0) { cls = 'stock-zero'; label = '缺货'; }
  else if (value < threshold) { cls = 'stock-low'; label = '低库存'; }
  else if (value > threshold * 5) { cls = 'stock-high'; label = '充足'; }
  return <span className={'stock-cell ' + cls} title={label}>{fmtNum(value)}</span>;
}

/* ----- 角色 / 单据状态徽标 ----- */
function RoleBadge({ role }) {
  return <span className={'badge ' + (role === 'admin' ? 'badge-admin' : 'badge-manager')}>{role === 'admin' ? '管理员' : '仓管员'}</span>;
}
function StatusBadge({ status }) {
  return <span className={'badge ' + (status === 'pending' ? 'badge-pending' : 'badge-approved')}>{status === 'pending' ? '待审核' : '已确认'}</span>;
}
function SourceBadge({ source }) {
  if (source !== 'scan') return null;
  return <span className="badge badge-scan">📷 扫码录入</span>;
}

/* ----- 空状态 ----- */
function EmptyState({ icon = '📦', title = '暂无数据', hint = '' }) {
  return (
    <div className="table-empty">
      <div className="icon">{icon}</div>
      <div style={{ fontSize: 14, marginBottom: 4 }}>{title}</div>
      {hint && <div style={{ fontSize: 12 }}>{hint}</div>}
    </div>
  );
}

/* ----- 整页加载态 ----- */
function PageLoader({ text = '加载中…' }) {
  return <div className="full-page-loading"><div className="spinner" /><div>{text}</div></div>;
}

/* ----- Toast 提示 ----- */
function Toast({ message, type = 'info', onClose }) {
  useEffect(() => {
    const t = setTimeout(() => onClose && onClose(), 2600);
    return () => clearTimeout(t);
  }, [onClose]);
  return (
    <div style={{
      position: 'fixed', top: 24, right: 24, zIndex: 300, maxWidth: '90vw',
      background: type === 'success' ? 'var(--color-success)' : type === 'error' ? 'var(--color-danger)' : 'var(--color-primary)',
      color: 'white', padding: '10px 16px', borderRadius: 8,
      boxShadow: 'var(--shadow-lg)', fontSize: 14, fontWeight: 500,
      display: 'flex', alignItems: 'center', gap: 8
    }}>
      {type === 'success' && <Icon.Check />}
      {message}
    </div>
  );
}

/* ----- 侧边栏 + 顶栏壳（含移动端底部导航 + 用户信息/退出登录）----- */
function Shell({ active, title, subtitle, actions, children, onScanClick }) {
  const user = window.Api ? window.Api.getUser() : null;
  const isAdmin = user && user.role === 'admin';
  const [showUserSheet, setShowUserSheet] = useState(false);

  const navGroups = [
    { group: '总览', items: [{ key: 'dashboard', name: '仪表盘', href: 'index.html', icon: <Icon.Dashboard /> }] },
    { group: '基础数据', items: [
      { key: 'products', name: '货品档案', href: 'products.html', icon: <Icon.Box /> },
      { key: 'warehouses', name: '仓库管理', href: 'warehouses.html', icon: <Icon.Warehouse /> },
      { key: 'suppliers', name: '供应商管理', href: 'suppliers.html', icon: <Icon.Truck /> },
    ]},
    { group: '业务单据', items: [
      { key: 'inbound', name: '入库管理', href: 'inbound.html', icon: <Icon.Down /> },
      { key: 'outbound', name: '出库管理', href: 'outbound.html', icon: <Icon.Up /> },
      { key: 'stocktake', name: '库存盘点', href: 'stocktake.html', icon: <Icon.Clipboard /> },
    ]},
    { group: '查询与报表', items: [
      { key: 'inventory', name: '实时库存', href: 'inventory.html', icon: <Icon.Stock /> },
      { key: 'reports', name: '收发存报表', href: 'reports.html', icon: <Icon.Report /> },
    ]},
    { group: '系统', items: [
      { key: 'dictionary', name: '数据字典', href: 'dictionary.html', icon: <Icon.Clipboard /> },
      { key: 'settings', name: '系统设置', href: 'settings.html', icon: <Icon.Settings /> },
    ]},
  ];

  const mobileTabs = [
    { key: 'dashboard', name: '首页', href: 'index.html', icon: <Icon.Dashboard /> },
    { key: 'inbound', name: '入库', href: 'inbound.html', icon: <Icon.Down /> },
    { key: 'scan', name: '扫码', isScan: true, icon: <Icon.Camera /> },
    { key: 'outbound', name: '出库', href: 'outbound.html', icon: <Icon.Up /> },
    { key: 'inventory', name: '库存', href: 'inventory.html', icon: <Icon.Stock /> },
  ];

  return (
    <div className="app">
      {/* 移动端顶部条：侧边栏在窄屏被隐藏，用户信息/退出登录/系统设置从这里进 */}
      <div className="mobile-topbar">
        <div className="brand-mini"><span className="logo">HN</span>汇能科技 ERP</div>
        {user && <button className="user-btn" onClick={() => setShowUserSheet(true)}>{(user.name || '?').slice(0, 1)}</button>}
      </div>

      <aside className="sidebar">
        <div className="brand">
          <div className="brand-logo">HN</div>
          <div className="brand-text">
            <div className="brand-name">汇能科技</div>
            <div className="brand-tag">WAREHOUSE ERP</div>
          </div>
        </div>
        <nav>
          {navGroups.map(g => (
            <div key={g.group} className="nav-group">
              <div className="nav-group-title">{g.group}</div>
              {g.items.map(it => (
                <a key={it.key} href={it.href} className={'nav-item' + (active === it.key ? ' active' : '')}>
                  <span className="icon">{it.icon}</span>
                  <span>{it.name}</span>
                </a>
              ))}
            </div>
          ))}
        </nav>
        {user && (
          <div className="sidebar-user">
            <div className="avatar">{(user.name || '?').slice(0, 1)}</div>
            <div className="info">
              <div className="name">{user.name}</div>
              <div className="role">{user.role === 'admin' ? '管理员' : '仓管员'}</div>
            </div>
            <button className="logout-btn" title="退出登录" onClick={() => window.Api.logout()}><Icon.Logout /></button>
          </div>
        )}
      </aside>

      <main className="main">
        <div className="page-header">
          <div>
            <div className="page-title">{title}</div>
            {subtitle && <div className="page-subtitle">{subtitle}</div>}
          </div>
          {actions && <div className="page-actions">{actions}</div>}
        </div>
        {children}
      </main>

      <nav className="bottom-nav">
        {mobileTabs.map(t => t.isScan ? (
          <a key={t.key} href="#" onClick={(e) => { e.preventDefault(); onScanClick && onScanClick(); }}>
            <span className="fab-scan">{t.icon}</span>
          </a>
        ) : (
          <a key={t.key} href={t.href} className={active === t.key ? 'active' : ''}>
            {t.icon}<span>{t.name}</span>
          </a>
        ))}
      </nav>

      {showUserSheet && user && (
        <div className="mobile-user-sheet" onMouseDown={(e) => { if (e.target === e.currentTarget) setShowUserSheet(false); }}>
          <div className="sheet-panel">
            <div className="sheet-row"><b>{user.name}</b>&nbsp;<span style={{ color: 'var(--color-text-tertiary)', fontSize: 12 }}>{user.role === 'admin' ? '管理员' : '仓管员'}</span></div>
            <a className="sheet-row sheet-link" href="products.html">货品档案</a>
            <a className="sheet-row sheet-link" href="warehouses.html">仓库管理</a>
            <a className="sheet-row sheet-link" href="suppliers.html">供应商管理</a>
            <a className="sheet-row sheet-link" href="reports.html">收发存报表</a>
            <a className="sheet-row sheet-link" href="stocktake.html">库存盘点</a>
            <a className="sheet-row sheet-link" href="dictionary.html">数据字典</a>
            <a className="sheet-row sheet-link" href="settings.html">系统设置</a>
            <div className="sheet-row sheet-danger" onClick={() => window.Api.logout()}>退出登录</div>
            <button className="btn" style={{ width: '100%', justifyContent: 'center', marginTop: 8 }} onClick={() => setShowUserSheet(false)}>关闭</button>
          </div>
        </div>
      )}
    </div>
  );
}

/* ----- 摄像头条码扫描 Hook（供 ScannerModal 及扫码清单弹窗共用）-----
   用法：const { error, torchOn, toggleTorch } = useBarcodeScanner(videoRef, onDetect);
   onDetect 会在识别到新条码时被调用（同一条码 1.2s 内去重，避免连续触发） */
function useBarcodeScanner(videoRef, onDetect) {
  const readerRef = useRef(null);
  const lastCodeRef = useRef({ code: '', time: 0 });
  const [error, setError] = useState('');
  const [torchOn, setTorchOn] = useState(false);
  const onDetectRef = useRef(onDetect);
  onDetectRef.current = onDetect;

  useEffect(() => {
    if (!window.ZXingBrowser && !window.ZXing) {
      setError('条码识别库未加载，请检查 vendor/zxing.min.js 是否正确引入');
      return;
    }
    const ZX = window.ZXingBrowser || window.ZXing;
    const hints = new Map();
    const reader = new ZX.BrowserMultiFormatReader(hints, { delayBetweenScanAttempts: 150 });
    readerRef.current = reader;
    let stopped = false;

    reader.decodeFromConstraints(
      { video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } } },
      videoRef.current,
      (result) => {
        if (stopped || !result) return;
        const code = result.getText();
        const now = Date.now();
        if (code === lastCodeRef.current.code && now - lastCodeRef.current.time < 1200) return;
        lastCodeRef.current = { code, time: now };
        if (navigator.vibrate) navigator.vibrate(60);
        onDetectRef.current && onDetectRef.current(code);
      }
    ).then((controls) => {
      readerRef.current._controls = controls;
    }).catch((err) => {
      setError('无法打开摄像头：' + (err && err.message ? err.message : String(err)) + '（请检查浏览器摄像头权限，且必须是 HTTPS 或 localhost）');
    });

    return () => {
      stopped = true;
      try { readerRef.current && readerRef.current._controls && readerRef.current._controls.stop(); } catch (e) {}
      try {
        const s = videoRef.current && videoRef.current.srcObject;
        if (s) s.getTracks().forEach(t => t.stop());
      } catch (e) {}
    };
  }, []);

  const toggleTorch = async () => {
    try {
      const s = videoRef.current.srcObject;
      const track = s && s.getVideoTracks()[0];
      if (!track) return;
      const capabilities = track.getCapabilities ? track.getCapabilities() : {};
      if (!capabilities.torch) { setError('该设备/浏览器不支持手电筒'); return; }
      await track.applyConstraints({ advanced: [{ torch: !torchOn }] });
      setTorchOn(!torchOn);
    } catch (e) { setError('切换手电筒失败：' + e.message); }
  };

  return { error, torchOn, toggleTorch };
}

/* ----- OCR 文字识别（本地免费引擎 Tesseract.js，识别包装上印刷的品牌/型号文字）-----
   用法：const text = await runOcr(canvasOrImageElement, onProgress);
   仅在实际调用时才会加载 OCR 引擎和语言包（约 5-8MB，只加载一次，浏览器会缓存），
   不会拖慢平时的页面加载速度。 */
async function runOcr(imageSource, onProgress) {
  if (!window.Tesseract) throw new Error('OCR 引擎未加载，请检查 vendor/tesseract/tesseract.min.js 是否正确引入');
  const worker = await window.Tesseract.createWorker('chi_sim+eng', 1, {
    workerPath: 'vendor/tesseract/worker.min.js',
    corePath: 'vendor/tesseract/tesseract-core-lstm.wasm.js',
    langPath: 'vendor/tesseract/lang',
    logger: (m) => {
      if (onProgress && m.status === 'recognizing text') onProgress(Math.round((m.progress || 0) * 100));
    },
  });
  try {
    const { data } = await worker.recognize(imageSource);
    return (data.text || '').trim();
  } finally {
    await worker.terminate();
  }
}

/* ----- 独立的"拍照识别文字"弹窗：自带摄像头，用于货品档案等没有现成摄像头画面的场景 -----
   用法：<OcrCaptureModal onClose={..} onResult={(text)=>{...}} /> */
function OcrCaptureModal({ onClose, onResult, title = '拍照识别文字' }) {
  const videoRef = useRef(null);
  const canvasRef = useRef(null);
  const [error, setError] = useState('');
  const [phase, setPhase] = useState('camera'); // camera | captured | recognizing | done
  const [progress, setProgress] = useState(0);
  const [text, setText] = useState('');

  useEffect(() => {
    let stream;
    navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } } })
      .then((s) => { stream = s; if (videoRef.current) videoRef.current.srcObject = s; })
      .catch((err) => setError('无法打开摄像头：' + (err && err.message ? err.message : String(err)) + '（请检查摄像头权限，且必须是 HTTPS 或 localhost）'));
    return () => { if (stream) stream.getTracks().forEach(t => t.stop()); };
  }, []);

  const capture = () => {
    const video = videoRef.current, canvas = canvasRef.current;
    canvas.width = video.videoWidth; canvas.height = video.videoHeight;
    canvas.getContext('2d').drawImage(video, 0, 0);
    setPhase('captured');
  };

  const recognize = async () => {
    setPhase('recognizing'); setProgress(0); setError('');
    try {
      const result = await runOcr(canvasRef.current, setProgress);
      setText(result);
      setPhase('done');
    } catch (e) {
      setError(e.message);
      setPhase('captured');
    }
  };

  const use = () => { onResult(text); onClose(); };

  return (
    <div className="scan-modal">
      <Modal title={title} onClose={onClose}>
        <div className="scan-viewport" style={{ display: phase === 'camera' ? 'block' : 'none' }}>
          <video ref={videoRef} muted playsInline autoPlay />
          <div className="scan-hint">对准包装上的文字，尽量让文字占满取景框、光线充足</div>
        </div>
        <canvas ref={canvasRef} style={{ width: '100%', borderRadius: 'var(--radius)', display: phase === 'camera' ? 'none' : 'block' }} />
        {error && <div className="alert alert-warning" style={{ marginTop: 10 }}><Icon.Warn />{error}</div>}

        {phase === 'recognizing' && (
          <div style={{ textAlign: 'center', padding: '16px 0', fontSize: 13, color: 'var(--color-text-secondary)' }}>
            <div className="spinner" style={{ margin: '0 auto 8px' }} />
            识别中… {progress}%
          </div>
        )}

        {phase === 'done' && (
          <div className="form-group full" style={{ marginTop: 12 }}>
            <label className="form-label">识别结果（可编辑修正后再使用）</label>
            <textarea className="form-textarea" rows={4} value={text} onChange={(e) => setText(e.target.value)} />
          </div>
        )}

        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 14 }}>
          {phase === 'camera' && <button className="btn btn-primary" onClick={capture}><Icon.Camera />拍照</button>}
          {phase === 'captured' && <>
            <button className="btn" onClick={() => setPhase('camera')}>重拍</button>
            <button className="btn btn-primary" onClick={recognize}>开始识别</button>
          </>}
          {phase === 'done' && <>
            <button className="btn" onClick={() => setPhase('camera')}>重拍</button>
            <button className="btn btn-primary" onClick={use}>使用识别结果</button>
          </>}
        </div>
      </Modal>
    </div>
  );
}

/* ----- 摄像头条码扫描弹窗（简单单次识别场景）-----
   用法：<ScannerModal onClose={..} onDetect={(code)=>{...}} /> */
function ScannerModal({ onClose, onDetect, title = '扫描条码' }) {
  const videoRef = useRef(null);
  const [manualCode, setManualCode] = useState('');
  const { error, torchOn, toggleTorch } = useBarcodeScanner(videoRef, onDetect);

  return (
    <div className="scan-modal">
      <Modal title={title} onClose={onClose}>
        <div className="scan-viewport">
          <video ref={videoRef} muted playsInline autoPlay />
          <div className="scan-frame" />
          <div className="scan-hint">将条码/二维码对准框内，自动识别</div>
        </div>
        {error && <div className="alert alert-warning" style={{ marginTop: 10 }}><Icon.Warn />{error}</div>}
        <div className="form-grid" style={{ marginTop: 14 }}>
          <div className="form-group full">
            <label className="form-label">识别失败？手动输入条码</label>
            <div style={{ display: 'flex', gap: 8 }}>
              <input className="form-input" placeholder="输入或粘贴条码后回车" value={manualCode}
                onChange={e => setManualCode(e.target.value)}
                onKeyDown={e => { if (e.key === 'Enter' && manualCode.trim()) { onDetect(manualCode.trim()); setManualCode(''); } }} />
              <button className="btn" onClick={() => { if (manualCode.trim()) { onDetect(manualCode.trim()); setManualCode(''); } }}>确认</button>
            </div>
          </div>
        </div>
        <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 10 }}>
          <button className="btn" onClick={toggleTorch}>{torchOn ? '关闭手电筒' : '打开手电筒'}</button>
        </div>
      </Modal>
    </div>
  );
}

/* ----- 工具：CSV 导出 ----- */
function downloadCSV(filename, rows) {
  const csv = '\uFEFF' + rows.map(r => r.map(cell => {
    const s = (cell == null ? '' : String(cell)).replace(/"/g, '""');
    return /[",\n]/.test(s) ? `"${s}"` : s;
  }).join(',')).join('\n');
  const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
}

/* ----- 格式化工具（与后端无关的纯前端展示格式化）----- */
function formatDate(iso) {
  if (!iso) return '—';
  const d = new Date(iso.replace(' ', 'T'));
  const p = (n) => String(n).padStart(2, '0');
  return `${d.getFullYear()}/${p(d.getMonth() + 1)}/${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
function formatDay(iso) {
  if (!iso) return '—';
  const d = new Date(iso.replace(' ', 'T'));
  const p = (n) => String(n).padStart(2, '0');
  return `${d.getFullYear()}/${p(d.getMonth() + 1)}/${p(d.getDate())}`;
}
function fmtMoney(n) {
  if (n == null) return '—';
  return '¥' + Number(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function fmtNum(n) {
  if (n == null) return '—';
  return Number(n).toLocaleString('zh-CN');
}

Object.assign(window, {
  Icon, Modal, StockBadge, EmptyState, Toast, Shell, ScannerModal, useBarcodeScanner, PageLoader,
  RoleBadge, StatusBadge, SourceBadge, downloadCSV, formatDate, formatDay, fmtMoney, fmtNum,
  runOcr, OcrCaptureModal,
  useState, useEffect, useRef, useMemo, useCallback
});
