feat: 添加图片外链
This commit is contained in:
parent
20293b6283
commit
3bcff30458
9
config/config.prod.ts
Normal file
9
config/config.prod.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { defineConfig } from '@umijs/max';
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
'process.env': {
|
||||
API_HOST_URL: 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
});
|
69
src/components/Banner/Upload.tsx
Normal file
69
src/components/Banner/Upload.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Image, Upload } from 'antd';
|
||||
import { LoadingOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import type { GetProp, UploadProps } from 'antd';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IFormUpload {
|
||||
value?: string;
|
||||
onChange?: (val: string) => void;
|
||||
}
|
||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
||||
|
||||
const getBase64 = (img: FileType, callback: (url: string) => void) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => callback(reader.result as string));
|
||||
reader.readAsDataURL(img);
|
||||
};
|
||||
|
||||
const FormUpload: React.FC<IFormUpload> = ({ onChange }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [imageUrl, setImageUrl] = useState('');
|
||||
|
||||
const handleUpload = async (info: any) => {
|
||||
if (info.file.status === 'uploading') {
|
||||
setLoading(true);
|
||||
return;
|
||||
}
|
||||
if (info.file.status === 'done') {
|
||||
getBase64(info.file.originFileObj as FileType, (url) => {
|
||||
setLoading(false);
|
||||
setImageUrl(url);
|
||||
});
|
||||
}
|
||||
onChange?.(info);
|
||||
};
|
||||
|
||||
const uploadButton = (
|
||||
<button style={{ border: 0, background: 'none' }} type="button">
|
||||
{loading ? <LoadingOutlined /> : <PlusOutlined />}
|
||||
<div style={{ marginTop: 8 }}>上传图片</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{imageUrl ? (
|
||||
<div className={styles.bannerItem}>
|
||||
<Image width="100%" height="100%" src={imageUrl} />
|
||||
<div className={styles.operation} onClick={() => setImageUrl('')}>
|
||||
<CloseOutlined />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Upload
|
||||
accept=".png,.jpg,.jpeg,.gif,.webp,.svg"
|
||||
name="file"
|
||||
listType="picture-circle"
|
||||
className={`avatar-uploader uploadBtn`}
|
||||
showUploadList={false}
|
||||
onChange={handleUpload}
|
||||
>
|
||||
{uploadButton}
|
||||
</Upload>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormUpload;
|
@ -3,29 +3,30 @@
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
.bannerItem {
|
||||
border-radius: 10px;
|
||||
// overflow: hidden;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bannerItem {
|
||||
border-radius: 10px;
|
||||
// overflow: hidden;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
.operation {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: red;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
color: white;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
&:hover {
|
||||
.operation {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: red;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
color: white;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
&:hover {
|
||||
.operation {
|
||||
display: block;
|
||||
}
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +1,126 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import { LoadingOutlined, PlusOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { Image, Upload } from 'antd';
|
||||
import type { UploadProps } from 'antd';
|
||||
import { Image, Popconfirm, Table, Button, Modal, Form, Input } from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import FormUpload from './Upload';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IItemImage {
|
||||
id: number;
|
||||
url: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
interface IBanner {
|
||||
type FieldType = {
|
||||
file?: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
export interface IBanner {
|
||||
type: number;
|
||||
dataSource: IItemImage[];
|
||||
onChange: (info: any, type: number) => void;
|
||||
onChange: (fileInfo: any, params: { type: number; path: string }) => void;
|
||||
onDelFile: (id: number) => void;
|
||||
}
|
||||
|
||||
const Banner: React.FC<IBanner> = ({ type, dataSource, onChange, onDelFile }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dataList, setDataList] = useState<IItemImage[]>(dataSource);
|
||||
|
||||
const handleChange: UploadProps['onChange'] = async (info) => {
|
||||
if (info.file.status === 'uploading') {
|
||||
setLoading(true);
|
||||
return;
|
||||
}
|
||||
if (info.file.status === 'done') {
|
||||
onChange?.(info, type);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const [open, setOpen] = useState<boolean>();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
setDataList(dataSource);
|
||||
}, [dataSource]);
|
||||
|
||||
const uploadButton = (
|
||||
<button style={{ border: 0, background: 'none' }} type="button">
|
||||
{loading ? <LoadingOutlined /> : <PlusOutlined />}
|
||||
<div style={{ marginTop: 8 }}>上传图片</div>
|
||||
</button>
|
||||
);
|
||||
const columns: TableColumnsType = [
|
||||
{
|
||||
title: '图片',
|
||||
dataIndex: 'url',
|
||||
width: '300px',
|
||||
render: (_: any, record: any) => {
|
||||
return (
|
||||
<>
|
||||
<Image width={180} height={100} src={record.url} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '跳转链接',
|
||||
align: 'center',
|
||||
dataIndex: 'path',
|
||||
},
|
||||
{
|
||||
title: '上传时间',
|
||||
align: 'center',
|
||||
dataIndex: 'createTime',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
dataIndex: 'operation',
|
||||
render: (_: any, record: any) =>
|
||||
dataSource.length >= 1 ? (
|
||||
<Popconfirm
|
||||
title="Sure to delete?"
|
||||
onConfirm={() => onDelFile((record as IItemImage).id)}
|
||||
>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
|
||||
const handleOk = () => {
|
||||
const { file, path } = form.getFieldsValue() || {};
|
||||
onChange?.(file, {
|
||||
type,
|
||||
path,
|
||||
});
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image.PreviewGroup
|
||||
preview={{
|
||||
onChange: (current, prev) =>
|
||||
console.log(`current index: ${current}, prev index: ${prev}`),
|
||||
}}
|
||||
>
|
||||
<div className={styles.bannerWrapper}>
|
||||
{dataList.map((item) => {
|
||||
return (
|
||||
<div key={item.id} className={styles.bannerItem}>
|
||||
<Image width="100%" height={250} src={item.url} />
|
||||
<div className={styles.operation} onClick={() => onDelFile(item.id)}>
|
||||
<CloseOutlined />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Image.PreviewGroup>
|
||||
<div className={styles.uploadWrapper}>
|
||||
<Upload
|
||||
accept=".png,.jpg,.jpeg,.gif,.webp,.svg"
|
||||
name="file"
|
||||
listType="picture-circle"
|
||||
className={`avatar-uploader uploadBtn`}
|
||||
showUploadList={false}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{uploadButton}
|
||||
</Upload>
|
||||
<div className={styles.header}>
|
||||
<Button type="primary" onClick={() => setOpen(true)}>
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
<Modal
|
||||
title="Basic Modal"
|
||||
open={open}
|
||||
centered
|
||||
width={600}
|
||||
okText="提交"
|
||||
cancelText="取消"
|
||||
onOk={handleOk}
|
||||
onCancel={() => setOpen(false)}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
name="basic"
|
||||
labelCol={{ span: 4 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
style={{ maxWidth: 600 }}
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label="图片"
|
||||
name="file"
|
||||
rules={[{ required: true, message: 'Please input your username!' }]}
|
||||
>
|
||||
<FormUpload />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType> label="跳转链接" name="path">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Table bordered dataSource={dataList} columns={columns} rowClassName="editable-row" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Card } from 'antd';
|
||||
import { Card, Tabs } from 'antd';
|
||||
import { useModel } from '@umijs/max';
|
||||
import Banner from '@/components/Banner';
|
||||
import { getBanners, deleteBanner, uploadBanner } from '@/services/ant-design-pro/api';
|
||||
import styles from './index.less';
|
||||
import { requestUpload } from '@/utils/upload';
|
||||
import { bannerTypes } from '@/constant';
|
||||
|
||||
@ -32,12 +31,11 @@ const Page = () => {
|
||||
* @param info 文件内容
|
||||
* @param type 轮播图类型
|
||||
*/
|
||||
const handleFileUpload = async (info: any, type: number) => {
|
||||
const file = info.file.originFileObj;
|
||||
const fileName = info.file.name;
|
||||
const handleFileUpload = async (fileInfo: any, params: { type: number; path: string }) => {
|
||||
const file = fileInfo.file.originFileObj;
|
||||
const fileName = fileInfo.file.name;
|
||||
const url = await requestUpload(file as Blob, fileName);
|
||||
await uploadBanner({ url, type });
|
||||
await getBanners();
|
||||
await uploadBanner({ url, ...params });
|
||||
getBannerList();
|
||||
};
|
||||
|
||||
@ -50,6 +48,25 @@ const Page = () => {
|
||||
getBannerList();
|
||||
};
|
||||
|
||||
const items = useMemo(() => {
|
||||
return bannerTypes.map((itemTab) => {
|
||||
const dataSource = dataList.filter((_item) => itemTab.type === _item.type);
|
||||
return {
|
||||
key: itemTab.type + '',
|
||||
label: itemTab.name,
|
||||
children: (
|
||||
<Banner
|
||||
key={itemTab.type}
|
||||
dataSource={dataSource}
|
||||
type={itemTab.type}
|
||||
onDelFile={handleDelImage}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
}, [dataList]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageContainer>
|
||||
@ -64,20 +81,7 @@ const Page = () => {
|
||||
: 'background-image: linear-gradient(75deg, #FBFDFF 0%, #F5F7FF 100%)',
|
||||
}}
|
||||
>
|
||||
{bannerTypes.map((item) => {
|
||||
const dataSource = dataList.filter((_item) => item.type === _item.type);
|
||||
return (
|
||||
<div className={styles.bannerBox} key={item.type}>
|
||||
<h3>{item.name}</h3>
|
||||
<Banner
|
||||
dataSource={dataSource}
|
||||
type={item.type}
|
||||
onDelFile={handleDelImage}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Tabs type="card" defaultActiveKey="1" items={items} />
|
||||
</Card>
|
||||
</PageContainer>
|
||||
</>
|
||||
|
@ -1,22 +1,8 @@
|
||||
import { Footer } from '@/components';
|
||||
import { login } from '@/services/ant-design-pro/api';
|
||||
import { getFakeCaptcha } from '@/services/ant-design-pro/login';
|
||||
import {
|
||||
AlipayCircleOutlined,
|
||||
LockOutlined,
|
||||
MobileOutlined,
|
||||
TaobaoCircleOutlined,
|
||||
UserOutlined,
|
||||
WeiboCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
LoginForm,
|
||||
ProFormCaptcha,
|
||||
ProFormCheckbox,
|
||||
ProFormText,
|
||||
} from '@ant-design/pro-components';
|
||||
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { LoginForm, ProFormText } from '@ant-design/pro-components';
|
||||
import { FormattedMessage, history, SelectLang, useIntl, useModel, Helmet } from '@umijs/max';
|
||||
import { Alert, message, Tabs } from 'antd';
|
||||
import { Alert, message } from 'antd';
|
||||
import Settings from '../../../../config/defaultSettings';
|
||||
import React, { useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
@ -58,18 +44,6 @@ const useStyles = createStyles(({ token }) => {
|
||||
};
|
||||
});
|
||||
|
||||
const ActionIcons = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlipayCircleOutlined key="AlipayCircleOutlined" className={styles.action} />
|
||||
<TaobaoCircleOutlined key="TaobaoCircleOutlined" className={styles.action} />
|
||||
<WeiboCircleOutlined key="WeiboCircleOutlined" className={styles.action} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Lang = () => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user