管理端修改

This commit is contained in:
tianmo committed 2026-09-30 18:01:35 +08:00
1 parent 5fe7f5d476
commit b652132957
8 files changed
+257 -7

No files matched your search

@@ -16,6 +16,12 @@ function toInputValue(field: FieldDef, raw: unknown): string {
const d = new Date(String(raw));
return isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10);
}
// 金额:库里是分,表单里给元
if (field.money) {
const fen = Number(raw);
if (!Number.isFinite(fen)) return '';
return (fen / 100).toFixed(2);
}
return String(raw);
}
@@ -26,7 +32,10 @@ function buildPayload(fields: FieldDef[], form: FormState): Row {
const value = form[field.name];
if (value === undefined) continue;
if (value === '') {
payload[field.name] = field.type === 'number' ? 0 : null;
payload[field.name] = field.type === 'number' || field.money ? 0 : null;
} else if (field.money) {
// 元 → 分,四舍五入到整数分,避免浮点误差(1.2 元 → 120 分)
payload[field.name] = Math.round(Number(value) * 100);
} else if (field.type === 'number' || field.type === 'select') {
payload[field.name] = Number(value);
} else {
@@ -39,6 +48,7 @@ function buildPayload(fields: FieldDef[], form: FormState): Row {
function displayCell(field: FieldDef, row: Row): string {
const raw = row[field.name];
if (raw === null || raw === undefined || raw === '') return '—';
if (field.money) return `${fenToYuan(raw)}`;
if (field.type === 'select' && field.options) {
const hit = field.options.find((o) => o.value === Number(raw));
return hit ? hit.label : String(raw);
@@ -47,6 +57,14 @@ function displayCell(field: FieldDef, row: Row): string {
return String(raw);
}
/** 分 → 元。金额一律固定两位小数,不省 .00(1 元显示成 1.00 元) */
function fenToYuan(fen: unknown): string {
if (fen === null || fen === undefined || fen === '') return '—';
const n = Number(fen);
if (!Number.isFinite(n)) return String(fen);
return (n / 100).toFixed(2);
}
export function ResourceTable({ config }: { config: ResourceConfig }) {
const [items, setItems] = useState<Row[]>([]);
const [total, setTotal] = useState(0);
@@ -54,6 +72,8 @@ export function ResourceTable({ config }: { config: ResourceConfig }) {
const [keyword, setKeyword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
/** 静态资源域名,配了卡片视图时才需要(用来拼封面图地址) */
const [staticBase, setStaticBase] = useState('');
const [formOpen, setFormOpen] = useState(false);
const [editing, setEditing] = useState<Row | null>(null);
@@ -84,6 +104,16 @@ export function ResourceTable({ config }: { config: ResourceConfig }) {
load(1, '');
}, [config.key, load]);
useEffect(() => {
if (!config.card) return;
api
.siteInfo()
.then((site) => setStaticBase(site.staticBaseUrl.replace(/\/+$/, '')))
.catch(() => {
// 取不到就只是不显示封面图,不影响列表本身
});
}, [config.card]);
const openCreate = () => {
const initial: FormState = {};
for (const f of config.fields) {
@@ -154,6 +184,96 @@ export function ResourceTable({ config }: { config: ResourceConfig }) {
<div className="py-10 text-center text-sm text-gray-400">加载中…</div>
) : items.length === 0 ? (
<Empty />
) : config.card ? (
/* 卡片视图:短字段挤一行,长字段按标签-值显示(超过两行截断) */
/*
两个旋钮:
minmax(290px) —— 决定一行放几张:约 1500px 宽时正好 5 张
zoom: 0.8 —— 整张卡(文字/图标/内边距一起)等比缩放
用 zoom 而不是单独改字号,是因为长宽要一起缩;只改宽度会变成「更窄但一样高」。
*/
<div className="grid grid-cols-[repeat(auto-fill,minmax(290px,1fr))] gap-3">
{items.map((row) => {
const card = config.card!;
const coverPath = card.cover(row);
const coverUrl = coverPath && staticBase ? `${staticBase}/${coverPath}` : '';
const allFields: FieldDef[] = [
{ name: 'id', label: 'ID', type: 'number' },
...config.fields,
];
const inline = allFields.filter((f) => card.inlineFields.includes(f.name));
// 底栏已经单独显示了 footerField,别再在正文里重复一遍
const rest = allFields.filter(
(f) => !card.inlineFields.includes(f.name) && f.name !== card.footerField?.name,
);
return (
<div
key={String(row.id)}
style={{ zoom: 0.8 }}
className="flex flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm transition-shadow hover:shadow-md"
>
{coverUrl && (
/* 9:5 与课程封面原始像素(900×500)一致,配合 contain 保证图片完整不裁剪 */
<img
src={coverUrl}
alt=""
loading="lazy"
className="aspect-[9/5] w-full bg-[#15457b] object-contain"
/>
)}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-gray-100 px-3 py-2 text-xs text-gray-700">
{inline.map((f) => (
<span key={f.name}>
<span className="text-gray-400">{f.label} </span>
{displayCell(f, row)}
</span>
))}
</div>
{/* 值一律单行截断(悬停看全文),换行会让卡片高度不可控 */}
<div className="flex-1 space-y-1 px-3 py-2">
{rest.map((f) => {
const text = displayCell(f, row);
return (
<div key={f.name} className="flex gap-2 text-xs leading-relaxed">
<span className="w-14 shrink-0 text-gray-400">{f.label}</span>
<span className="min-w-0 flex-1 truncate text-gray-700" title={text}>
{text}
</span>
</div>
);
})}
</div>
<div className="flex items-center justify-between gap-2 border-t border-gray-100 px-3 py-2">
{card.footerField ? (
<span className="text-base font-medium text-gray-900">
<span className="text-xs text-gray-400">{card.footerField.label} </span>
{card.footerField.money
? `${fenToYuan(row[card.footerField.name])} 元`
: displayCell(
{ name: card.footerField.name, label: card.footerField.label, type: 'text' },
row,
)}
</span>
) : (
<span />
)}
<div className="flex gap-2">
<Button variant="ghost" onClick={() => openEdit(row)}>
编辑
</Button>
<Button variant="danger" onClick={() => remove(row)}>
删除
</Button>
</div>
</div>
</div>
);
})}
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
@@ -261,4 +381,3 @@ export function ResourceTable({ config }: { config: ResourceConfig }) {
</div>
);
}