web端打通oss资源上传
This commit is contained in:
@@ -17,6 +17,7 @@ import HqAccountsPage from './pages/HqAccountsPage';
|
||||
import CitiesPage from './pages/CitiesPage';
|
||||
import StoreMediaPage from './pages/StoreMediaPage';
|
||||
import ProductsPage from './pages/ProductsPage';
|
||||
import ResourcesPage from './pages/ResourcesPage';
|
||||
|
||||
function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||
if (!getToken()) return <Navigate to="/login" replace />;
|
||||
@@ -41,6 +42,7 @@ export default function App() {
|
||||
<Route path="/stores" element={<StoresPage />} />
|
||||
<Route path="/store-accounts" element={<StoreAccountsPage />} />
|
||||
<Route path="/store-media" element={<StoreMediaPage />} />
|
||||
<Route path="/resources" element={<ResourcesPage />} />
|
||||
<Route path="/partners" element={<PartnersPage />} />
|
||||
<Route path="/cities" element={<CitiesPage />} />
|
||||
<Route path="/partner-accounts" element={<PartnerAccountsPage />} />
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Image, Input, Space, Upload, message } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import type { UploadProps } from 'antd';
|
||||
import { uploadFileToOss, type OssMediaType, type UploadFileResult } from '../lib/upload';
|
||||
|
||||
type OssUploadProps = {
|
||||
value?: string;
|
||||
onChange?: (url: string) => void;
|
||||
onUploaded?: (result: UploadFileResult) => void;
|
||||
bizType: string;
|
||||
mediaType?: OssMediaType;
|
||||
accept?: string;
|
||||
maxSizeMb?: number;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_MAX_MB = 10;
|
||||
|
||||
export default function OssUpload({
|
||||
value,
|
||||
onChange,
|
||||
onUploaded,
|
||||
bizType,
|
||||
mediaType = 'IMAGE',
|
||||
accept,
|
||||
maxSizeMb = DEFAULT_MAX_MB,
|
||||
placeholder = '上传后自动填入,或手动粘贴 URL',
|
||||
}: OssUploadProps) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const resolvedAccept =
|
||||
accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? undefined : 'image/*');
|
||||
|
||||
const customRequest: UploadProps['customRequest'] = async ({ file, onSuccess, onError }) => {
|
||||
const raw = file as File;
|
||||
if (raw.size > maxSizeMb * 1024 * 1024) {
|
||||
const err = new Error(`文件不能超过 ${maxSizeMb}MB`);
|
||||
message.error(err.message);
|
||||
onError?.(err);
|
||||
return;
|
||||
}
|
||||
setUploading(true);
|
||||
try {
|
||||
const result = await uploadFileToOss(raw, { bizType, mediaType });
|
||||
onChange?.(result.url);
|
||||
onUploaded?.(result);
|
||||
if (result.mock) {
|
||||
message.info('当前为 Mock OSS,已使用占位 URL');
|
||||
} else {
|
||||
message.success('上传成功');
|
||||
}
|
||||
onSuccess?.(result);
|
||||
} catch (e) {
|
||||
const err = e instanceof Error ? e : new Error('上传失败');
|
||||
message.error(err.message);
|
||||
onError?.(err);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||
{value && mediaType === 'IMAGE' && (
|
||||
<Image src={value} width={120} height={120} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||
)}
|
||||
{value && mediaType === 'VIDEO' && (
|
||||
<video src={value} controls style={{ maxWidth: '100%', maxHeight: 160, borderRadius: 4 }} />
|
||||
)}
|
||||
<Space wrap>
|
||||
<Upload
|
||||
accept={resolvedAccept}
|
||||
showUploadList={false}
|
||||
customRequest={customRequest}
|
||||
disabled={uploading}
|
||||
>
|
||||
<Button icon={<UploadOutlined />} loading={uploading}>
|
||||
{mediaType === 'VIDEO' ? '上传视频' : mediaType === 'FILE' ? '上传文件' : '上传图片'}
|
||||
</Button>
|
||||
</Upload>
|
||||
</Space>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CarOutlined,
|
||||
SafetyOutlined,
|
||||
LogoutOutlined,
|
||||
CloudUploadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { clearAuth, request, type HqProfile } from '../lib/api';
|
||||
|
||||
@@ -22,6 +23,7 @@ const MENU_ITEMS: MenuProps['items'] = [
|
||||
{ key: '/users', icon: <UserOutlined />, label: '用户' },
|
||||
{ key: '/products', icon: <ShoppingOutlined />, label: '商品' },
|
||||
{ key: '/orders', icon: <ShoppingOutlined />, label: '订单' },
|
||||
{ key: '/resources', icon: <CloudUploadOutlined />, label: 'OSS 资源库' },
|
||||
{
|
||||
key: 'stores-group',
|
||||
icon: <ShopOutlined />,
|
||||
|
||||
@@ -42,6 +42,33 @@ export const PARTNER_BILL_STATUS_LABELS: Record<string, string> = {
|
||||
export const MEDIA_TYPE_LABELS: Record<string, string> = {
|
||||
IMAGE: '图片',
|
||||
VIDEO: '视频',
|
||||
FILE: '文件',
|
||||
};
|
||||
|
||||
export const RESOURCE_OWNER_TYPE_LABELS: Record<string, string> = {
|
||||
PRODUCT: '商品',
|
||||
STORE: '门店',
|
||||
PARTNER: '合伙人',
|
||||
USER: '用户',
|
||||
ORDER: '订单',
|
||||
HQ: '总部',
|
||||
};
|
||||
|
||||
export const RESOURCE_BIZ_TYPE_LABELS: Record<string, string> = {
|
||||
COVER: '封面',
|
||||
ENV: '环境图',
|
||||
CONTRACT: '合同',
|
||||
CAROUSEL: '轮播',
|
||||
DETAIL: '详情',
|
||||
AVATAR: '头像',
|
||||
QRCODE: '二维码',
|
||||
SIGN_PHOTO: '签收照',
|
||||
VIDEO: '视频',
|
||||
};
|
||||
|
||||
export const RESOURCE_STATUS_LABELS: Record<string, string> = {
|
||||
ACTIVE: '有效',
|
||||
DELETED: '已删除',
|
||||
};
|
||||
|
||||
export const PRODUCT_STATUS_LABELS: Record<string, string> = {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { apiBase, CLIENT_APP, getToken, clearAuth } from './api';
|
||||
|
||||
export type OssMediaType = 'IMAGE' | 'VIDEO' | 'FILE';
|
||||
|
||||
export type UploadFileResult = {
|
||||
url: string;
|
||||
ossKey: string;
|
||||
bucket: string;
|
||||
mock: boolean;
|
||||
};
|
||||
|
||||
/** 经 API 服务端转存 OSS,避免浏览器直传跨域 */
|
||||
export async function uploadFileToOss(
|
||||
file: File,
|
||||
options: { bizType: string; mediaType?: OssMediaType },
|
||||
): Promise<UploadFileResult> {
|
||||
const mediaType = options.mediaType ?? (file.type.startsWith('video/') ? 'VIDEO' : 'IMAGE');
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('bizType', options.bizType);
|
||||
formData.append('mediaType', mediaType);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'X-Client-App': CLIENT_APP,
|
||||
};
|
||||
const token = getToken();
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`${apiBase}/common/resources/upload`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.code === 401) {
|
||||
clearAuth();
|
||||
window.location.href = '/login';
|
||||
throw new Error('未登录');
|
||||
}
|
||||
if (json.code !== 0) throw new Error(json.message || '上传失败');
|
||||
|
||||
const data = json.data as UploadFileResult;
|
||||
return {
|
||||
url: data.url,
|
||||
ossKey: data.ossKey,
|
||||
bucket: data.bucket,
|
||||
mock: data.mock,
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import { request, type Paginated } from '../lib/api';
|
||||
import { AROMA_TYPE_LABELS, PRODUCT_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import OssUpload from '../components/OssUpload';
|
||||
|
||||
type Row = {
|
||||
id: string;
|
||||
@@ -114,7 +115,9 @@ export default function ProductsPage() {
|
||||
<Select options={Object.entries(PRODUCT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="sortOrder" label="排序"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
|
||||
<Form.Item name="coverUrl" label="封面 URL"><Input placeholder="https://..." /></Form.Item>
|
||||
<Form.Item name="coverUrl" label="封面">
|
||||
<OssUpload bizType="COVER" mediaType="IMAGE" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
@@ -142,7 +145,9 @@ export default function ProductsPage() {
|
||||
<Select options={Object.entries(PRODUCT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="sortOrder" label="排序"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
|
||||
<Form.Item name="coverUrl" label="封面 URL"><Input placeholder="https://..." /></Form.Item>
|
||||
<Form.Item name="coverUrl" label="封面">
|
||||
<OssUpload bizType="COVER" mediaType="IMAGE" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import { request, type Paginated } from '../lib/api';
|
||||
import { MEDIA_TYPE_LABELS, fmtTime } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import OssUpload from '../components/OssUpload';
|
||||
|
||||
type Row = {
|
||||
id: string; mediaType: string; url: string; sortOrder: number; createdAt: string;
|
||||
@@ -102,7 +103,13 @@ export default function StoreMediaPage() {
|
||||
<Form.Item name="mediaType" label="类型" rules={[{ required: true }]} initialValue="IMAGE">
|
||||
<Select options={Object.entries(MEDIA_TYPE_LABELS).map(([value, label]) => ({ value, label }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="url" label="资源 URL" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.mediaType !== cur.mediaType}>
|
||||
{({ getFieldValue }) => (
|
||||
<Form.Item name="url" label="资源" rules={[{ required: true }]}>
|
||||
<OssUpload bizType="ENV" mediaType={getFieldValue('mediaType') || 'IMAGE'} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item name="sortOrder" label="排序" initialValue={0}><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
@@ -116,7 +123,13 @@ export default function StoreMediaPage() {
|
||||
}}>
|
||||
<Form form={editForm} layout="vertical">
|
||||
<Form.Item name="mediaType" label="类型"><Select options={Object.entries(MEDIA_TYPE_LABELS).map(([value, label]) => ({ value, label }))} /></Form.Item>
|
||||
<Form.Item name="url" label="资源 URL" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.mediaType !== cur.mediaType}>
|
||||
{({ getFieldValue }) => (
|
||||
<Form.Item name="url" label="资源" rules={[{ required: true }]}>
|
||||
<OssUpload bizType="ENV" mediaType={getFieldValue('mediaType') || 'IMAGE'} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item name="sortOrder" label="排序"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import { request, type Paginated } from '../lib/api';
|
||||
import { STORE_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import OssUpload from '../components/OssUpload';
|
||||
|
||||
type StoreRow = {
|
||||
id: string;
|
||||
@@ -128,16 +129,18 @@ export default function StoresPage() {
|
||||
<Descriptions.Item label="ID">{String(detail.id)}</Descriptions.Item>
|
||||
<Descriptions.Item label="地址">{String(detail.province)}{String(detail.cityName)}{String(detail.district)}{String(detail.address)}</Descriptions.Item>
|
||||
<Descriptions.Item label="核销数">{String(detail.redeemCount ?? 0)}</Descriptions.Item>
|
||||
{detail.coverUrl && (
|
||||
{detail.coverUrl ? (
|
||||
<Descriptions.Item label="封面">
|
||||
<Image src={String(detail.coverUrl)} width={120} />
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
) : null}
|
||||
</Descriptions>
|
||||
<Form form={editForm} layout="vertical">
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item name="phone" label="电话" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item name="coverUrl" label="封面图 URL"><Input /></Form.Item>
|
||||
<Form.Item name="coverUrl" label="封面图">
|
||||
<OssUpload bizType="COVER" mediaType="IMAGE" />
|
||||
</Form.Item>
|
||||
<Form.Item name="intro" label="介绍"><Input.TextArea rows={4} /></Form.Item>
|
||||
<Form.Item name="district" label="区县"><Input /></Form.Item>
|
||||
<Form.Item name="address" label="详细地址"><Input /></Form.Item>
|
||||
@@ -164,7 +167,9 @@ export default function StoresPage() {
|
||||
<Form.Item name="phone" label="门店电话" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item name="address" label="详细地址" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item name="district" label="区县"><Input /></Form.Item>
|
||||
<Form.Item name="coverUrl" label="封面图 URL"><Input /></Form.Item>
|
||||
<Form.Item name="coverUrl" label="封面图">
|
||||
<OssUpload bizType="COVER" mediaType="IMAGE" />
|
||||
</Form.Item>
|
||||
<Form.Item name="intro" label="介绍"><Input.TextArea rows={3} /></Form.Item>
|
||||
<Form.Item name="accountPhone" label="店长手机"><Input placeholder="默认同门店电话" /></Form.Item>
|
||||
<Form.Item name="accountName" label="店长姓名"><Input placeholder="默认同门店名" /></Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user