Files
training/website/src/components/pineAdmin.jsx
T
Pine 97054a9699 feat: 培训业务 Web(Vite+React)
- user 端:课程/活动/报名/测评/政策/调研
- 后端已并入 server-core/app/training,/api/* 走 opc.pinesound.cn
2026-08-23 22:32:42 +08:00

79 lines
2.9 KiB
React
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}