267 lines
8.8 KiB
TypeScript
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>
|
|
);
|
|
}
|