97054a9699
- user 端:课程/活动/报名/测评/政策/调研 - 后端已并入 server-core/app/training,/api/* 走 opc.pinesound.cn
79 lines
2.9 KiB
React
79 lines
2.9 KiB
React
import React from 'react';
|
||
import Skeleton from '@/components/Skeleton';
|
||
|
||
/* ---------- 管理页通用 hook:拉取 + loading + 错误 + 重载 ---------- */
|
||
export function useOps(fn) {
|
||
const [data, setData] = React.useState(null);
|
||
const [err, setErr] = React.useState('');
|
||
const [loading, setLoading] = React.useState(true);
|
||
const reload = React.useCallback(() => {
|
||
setLoading(true);
|
||
setErr('');
|
||
fn()
|
||
.then((d) => { setData(d); setErr(''); })
|
||
.catch((e) => { setData(null); setErr(e && e.message ? e.message : '加载失败'); })
|
||
.finally(() => setLoading(false));
|
||
}, [fn]);
|
||
React.useEffect(() => { reload(); }, [reload]);
|
||
return { data, err, loading, reload };
|
||
}
|
||
|
||
/* ---------- 预约状态徽章 ---------- */
|
||
const STATUS_MAP = {
|
||
pending: { label: '待确认', cls: 's-pending' },
|
||
confirmed: { label: '已确认', cls: 's-confirmed' },
|
||
arrived: { label: '已到场', cls: 's-arrived' },
|
||
converted: { label: '已转化', cls: 's-converted' }
|
||
};
|
||
export function StatusBadge({ status }) {
|
||
const m = STATUS_MAP[status] || STATUS_MAP.pending;
|
||
return <span className={`st-badge ${m.cls}`}>{m.label}</span>;
|
||
}
|
||
export const BOOKING_STATUSES = ['pending', 'confirmed', 'arrived', 'converted'];
|
||
|
||
/* ---------- 统计卡片 ---------- */
|
||
export function StatCard({ label, value, icon, accent }) {
|
||
return (
|
||
<div className={`ad-stat${accent ? ' acc' : ''}`}>
|
||
<div className="ad-stat-icon"><i className={icon} /></div>
|
||
<div className="ad-stat-num">{value}</div>
|
||
<div className="ad-stat-label">{label}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ---------- 加载 / 错误占位 ---------- */
|
||
export function OpsState({ loading, err, children }) {
|
||
if (loading) return <Skeleton.Rows n={4} gap={14} last="55%" style={{ marginTop: 8 }} />;
|
||
if (err) return <div className="ad-err">{err}</div>;
|
||
return children || null;
|
||
}
|
||
|
||
/* ---------- 时间工具 ---------- */
|
||
export const toLocalInput = (iso) => {
|
||
if (!iso) return '';
|
||
const d = new Date(iso);
|
||
const p = (n) => String(n).padStart(2, '0');
|
||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
|
||
};
|
||
export const fromLocalInput = (s) => (s ? new Date(s).toISOString() : '');
|
||
|
||
export const fmtDate = (iso) => {
|
||
if (!iso) return '—';
|
||
const d = new Date(iso);
|
||
const p = (n) => String(n).padStart(2, '0');
|
||
return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||
};
|
||
|
||
/* ---------- 导出 CSV ---------- */
|
||
export function downloadCSV(filename, head, rows) {
|
||
const esc = (v) => `"${String(v == null ? '' : v).replace(/"/g, '""')}"`;
|
||
const csv = [head.map(esc).join(','), ...rows.map((r) => r.map(esc).join(','))].join('\n');
|
||
const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' });
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = filename;
|
||
a.click();
|
||
URL.revokeObjectURL(a.href);
|
||
}
|