Files
dukang/apps/admin-web/src/pages/ResourcesPage.tsx
T
2026-08-04 21:38:49 +08:00

267 lines
8.8 KiB
TypeScript

import { useEffect, useState } from 'react';
import {
Button, Form, Image, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tag, Typography, message,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { request, type Paginated } from '../lib/api';
import {
MEDIA_TYPE_LABELS,
RESOURCE_BIZ_TYPE_LABELS,
RESOURCE_OWNER_TYPE_LABELS,
RESOURCE_STATUS_LABELS,
fmtTime,
} from '../lib/constants';
import OssUpload from '../components/OssUpload';
type Row = {
id: string;
ownerType: string;
ownerId: string;
bizType: string;
mediaType: string;
ossBucket: string;
ossKey: string;
url: string;
fileName?: string | null;
sortOrder: number;
status: string;
createdAt: string;
};
export default function ResourcesPage() {
const [form] = Form.useForm();
const [createForm] = Form.useForm();
const [filters, setFilters] = useState<Record<string, string>>({});
const [data, setData] = useState<Paginated<Row> | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [createOpen, setCreateOpen] = useState(false);
const [saving, setSaving] = useState(false);
async function reload(p = page, ps = pageSize, f = filters) {
setLoading(true);
try {
const qs = new URLSearchParams({ page: String(p), pageSize: String(ps) });
if (f.ownerType) qs.set('ownerType', f.ownerType);
if (f.ownerId) qs.set('ownerId', f.ownerId);
if (f.bizType) qs.set('bizType', f.bizType);
if (f.status) qs.set('status', f.status);
const res = await request<Paginated<Row>>(`/common/resources?${qs}`);
setData(res);
} finally {
setLoading(false);
}
}
useEffect(() => {
void reload();
}, []);
const columns: ColumnsType<Row> = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{
title: '归属',
width: 140,
render: (_, row) => (
<span>
{RESOURCE_OWNER_TYPE_LABELS[row.ownerType] || row.ownerType} / {row.ownerId}
</span>
),
},
{ title: '用途', dataIndex: 'bizType', width: 90, render: (v) => RESOURCE_BIZ_TYPE_LABELS[v] || v },
{ title: '类型', dataIndex: 'mediaType', width: 70, render: (v) => MEDIA_TYPE_LABELS[v] || v },
{
title: '预览',
dataIndex: 'url',
width: 90,
render: (url, row) =>
row.mediaType === 'IMAGE' ? (
<Image src={url} width={56} height={40} style={{ objectFit: 'cover' }} />
) : (
<a href={url} target="_blank" rel="noreferrer">
查看
</a>
),
},
{ title: 'URL', dataIndex: 'url', ellipsis: true },
{ title: 'OSS Key', dataIndex: 'ossKey', ellipsis: true, width: 160 },
{ title: '排序', dataIndex: 'sortOrder', width: 60 },
{
title: '状态',
dataIndex: 'status',
width: 80,
render: (s) => <Tag color={s === 'ACTIVE' ? 'green' : 'default'}>{RESOURCE_STATUS_LABELS[s] || s}</Tag>,
},
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
{
title: '操作',
width: 80,
render: (_, row) => (
<Popconfirm
title="确认删除该资源?"
onConfirm={async () => {
await request(`/common/resources/${row.id}`, { method: 'DELETE' });
message.success('已删除');
void reload();
}}
>
<Button type="link" size="small" danger>
删除
</Button>
</Popconfirm>
),
},
];
return (
<div>
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
<Typography.Title level={4} style={{ margin: 0 }}>
OSS 资源库
</Typography.Title>
<Button type="primary" onClick={() => setCreateOpen(true)}>
登记资源
</Button>
</Space>
<Form
form={form}
layout="inline"
style={{ marginBottom: 16 }}
onFinish={(v) => {
setFilters(v);
setPage(1);
void reload(1, pageSize, v);
}}
>
<Form.Item name="ownerType" label="归属类型">
<Select
allowClear
style={{ width: 120 }}
options={Object.entries(RESOURCE_OWNER_TYPE_LABELS).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item name="ownerId" label="归属 ID">
<Input allowClear style={{ width: 120 }} />
</Form.Item>
<Form.Item name="bizType" label="用途">
<Select
allowClear
style={{ width: 110 }}
options={Object.entries(RESOURCE_BIZ_TYPE_LABELS).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item name="status" label="状态">
<Select
allowClear
style={{ width: 100 }}
options={Object.entries(RESOURCE_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
查询
</Button>
</Form.Item>
</Form>
<Table
rowKey="id"
className="admin-table-nowrap"
loading={loading}
columns={columns}
dataSource={data?.items ?? []}
scroll={{ x: 1200 }}
pagination={{
current: page,
pageSize,
total: data?.total ?? 0,
showSizeChanger: true,
onChange: (p, ps) => {
setPage(p);
setPageSize(ps);
void reload(p, ps);
},
}}
/>
<Modal
title="登记 OSS 资源"
open={createOpen}
confirmLoading={saving}
onCancel={() => setCreateOpen(false)}
onOk={async () => {
const v = await createForm.validateFields();
setSaving(true);
try {
await request('/common/resources', {
method: 'POST',
body: JSON.stringify({
ownerType: v.ownerType,
ownerId: v.ownerId,
bizType: v.bizType,
mediaType: v.mediaType,
ossKey: v.ossKey,
url: v.url,
fileName: v.fileName,
sortOrder: v.sortOrder ?? 0,
}),
});
message.success('已登记');
setCreateOpen(false);
createForm.resetFields();
void reload();
} finally {
setSaving(false);
}
}}
width={520}
>
<Form form={createForm} layout="vertical" initialValues={{ mediaType: 'IMAGE', bizType: 'COVER', sortOrder: 0 }}>
<Form.Item name="ownerType" label="归属类型" rules={[{ required: true }]}>
<Select options={Object.entries(RESOURCE_OWNER_TYPE_LABELS).map(([value, label]) => ({ value, label }))} />
</Form.Item>
<Form.Item name="ownerId" label="归属 ID" rules={[{ required: true }]}>
<Input placeholder="商品/门店等业务 ID" />
</Form.Item>
<Form.Item name="bizType" label="用途" rules={[{ required: true }]}>
<Select options={Object.entries(RESOURCE_BIZ_TYPE_LABELS).map(([value, label]) => ({ value, label }))} />
</Form.Item>
<Form.Item name="mediaType" label="媒体类型" rules={[{ required: true }]}>
<Select
options={[
...Object.entries(MEDIA_TYPE_LABELS).map(([value, label]) => ({ value, label })),
{ value: 'FILE', label: '文件' },
]}
/>
</Form.Item>
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.mediaType !== cur.mediaType || prev.bizType !== cur.bizType}>
{({ getFieldValue }) => {
const mediaType = getFieldValue('mediaType') || 'IMAGE';
const bizType = getFieldValue('bizType') || 'COVER';
return (
<Form.Item name="url" label="资源" rules={[{ required: true }]}>
<OssUpload
bizType={bizType}
mediaType={mediaType}
onUploaded={(result) => {
createForm.setFieldsValue({ ossKey: result.ossKey, url: result.url });
}}
/>
</Form.Item>
);
}}
</Form.Item>
<Form.Item name="ossKey" label="OSS Key" rules={[{ required: true }]}>
<Input placeholder="上传后自动填入" />
</Form.Item>
<Form.Item name="fileName" label="文件名">
<Input />
</Form.Item>
<Form.Item name="sortOrder" label="排序">
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
</div>
);
}