feat: 添加图片外链

This commit is contained in:
luchang 2024-05-23 15:53:10 +08:00
parent 20293b6283
commit 3bcff30458
6 changed files with 229 additions and 126 deletions

9
config/config.prod.ts Normal file
View File

@ -0,0 +1,9 @@
import { defineConfig } from '@umijs/max';
export default defineConfig({
define: {
'process.env': {
API_HOST_URL: 'http://localhost:3000',
},
},
});

View 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;

View File

@ -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;
}
}
}

View File

@ -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" />
</>
);
};

View File

@ -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>
</>

View File

@ -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();