支持故事logo,增加默认logo
This commit is contained in:
@@ -5,10 +5,13 @@ import {
|
||||
ProFormText,
|
||||
ProFormTextArea,
|
||||
} from '@ant-design/pro-components';
|
||||
import { Button, Result } from 'antd';
|
||||
import React, { FC } from 'react';
|
||||
import type {StoryType} from '../data.d';
|
||||
import { Button, message, Result, Upload, Radio } from 'antd';
|
||||
import React, { FC, useState } from 'react';
|
||||
import ImgCrop from 'antd-img-crop';
|
||||
import type { StoryType } from '../data.d';
|
||||
import useStyles from '../style.style';
|
||||
import { defaultIcons } from '@/utils/commonConstant';
|
||||
|
||||
type OperationModalProps = {
|
||||
done: boolean;
|
||||
open: boolean;
|
||||
@@ -17,22 +20,106 @@ type OperationModalProps = {
|
||||
onSubmit: (values: StoryType) => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const OperationModal: FC<OperationModalProps> = (props) => {
|
||||
const { styles } = useStyles();
|
||||
const { done, open, current, onDone, onSubmit, children } = props;
|
||||
|
||||
// 图标状态管理
|
||||
const [iconType, setIconType] = useState<'default' | 'upload'>(
|
||||
current?.logo ? 'upload' : 'default',
|
||||
);
|
||||
const [selectedIcon, setSelectedIcon] = useState<string | null>(
|
||||
current?.logo || defaultIcons[0],
|
||||
);
|
||||
const [iconPreview, setIconPreview] = useState<string | null>(
|
||||
current?.logo || null,
|
||||
);
|
||||
const [fileList, setFileList] = useState<any[]>([]); // 控制上传图像展示
|
||||
|
||||
// 图标上传逻辑
|
||||
const beforeUpload = (file: File) => {
|
||||
const isValidType = file.type === 'image/png' || file.type === 'image/jpeg';
|
||||
|
||||
if (!isValidType) {
|
||||
message.error('仅支持 PNG 或 JPEG 格式');
|
||||
return false;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 强制设置裁剪尺寸为 40x40
|
||||
canvas.width = 40;
|
||||
canvas.height = 40;
|
||||
ctx.drawImage(img, 0, 0, 40, 40);
|
||||
|
||||
// 生成 Base64 图像
|
||||
const base64 = canvas.toDataURL('image/png');
|
||||
setSelectedIcon(base64);
|
||||
setIconPreview(base64);
|
||||
setFileList([
|
||||
{
|
||||
uid: '-1',
|
||||
name: 'icon.png',
|
||||
status: 'done',
|
||||
url: base64,
|
||||
originFileObj: new File([dataURLtoBlob(base64)], 'icon.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
};
|
||||
img.onerror = () => {
|
||||
message.error('图像加载失败');
|
||||
};
|
||||
img.src = e.target?.result as string;
|
||||
};
|
||||
reader.onerror = () => {
|
||||
message.error('读取图像失败');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
return false; // 阻止自动上传
|
||||
};
|
||||
|
||||
// Base64 → Blob 转换工具函数
|
||||
const dataURLtoBlob = (dataurl: string) => {
|
||||
const arr = dataurl.split(',');
|
||||
const mime = arr[0].match(/:(.*?);/)[1];
|
||||
const bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
const u8arr = new Uint8Array(n);
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
return new Blob([u8arr], { type: mime });
|
||||
};
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalForm<StoryType>
|
||||
open={open}
|
||||
title={done ? null : `任务${current ? '编辑' : '添加'}`}
|
||||
title={done ? null : `故事${current ? '编辑' : '添加'}`}
|
||||
className={styles.standardListForm}
|
||||
width={640}
|
||||
onFinish={async (values) => {
|
||||
onSubmit(values);
|
||||
onSubmit({
|
||||
...values,
|
||||
logo: selectedIcon || '',
|
||||
});
|
||||
}}
|
||||
initialValues={{
|
||||
...current,
|
||||
logo: current?.logo ? 'upload' : 'default',
|
||||
}}
|
||||
initialValues={current}
|
||||
submitter={{
|
||||
render: (_, dom) => (done ? null : dom),
|
||||
}}
|
||||
@@ -42,8 +129,8 @@ const OperationModal: FC<OperationModalProps> = (props) => {
|
||||
destroyOnClose: true,
|
||||
bodyStyle: done
|
||||
? {
|
||||
padding: '72px 0',
|
||||
}
|
||||
padding: '72px 0',
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
>
|
||||
@@ -51,15 +138,145 @@ const OperationModal: FC<OperationModalProps> = (props) => {
|
||||
<>
|
||||
<ProFormText
|
||||
name="title"
|
||||
label="任务名称"
|
||||
label="故事名称"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入任务名称',
|
||||
message: '请输入故事名称',
|
||||
},
|
||||
]}
|
||||
placeholder="请输入"
|
||||
/>
|
||||
|
||||
{/* 图标选择方式 */}
|
||||
<ProFormText
|
||||
name="logo"
|
||||
label="图标选择"
|
||||
hidden
|
||||
rules={[{ required: true, message: '请选择图标' }]}
|
||||
fieldProps={{
|
||||
value: iconType,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{ fontWeight: 'bold' }}>图标选择方式</span>
|
||||
<Radio.Group
|
||||
value={iconType}
|
||||
onChange={(e) => {
|
||||
const type = e.target.value;
|
||||
setIconType(type);
|
||||
if (type === 'default') {
|
||||
setSelectedIcon(defaultIcons[0]);
|
||||
setIconPreview(defaultIcons[0]);
|
||||
setFileList([]);
|
||||
} else {
|
||||
setSelectedIcon(null);
|
||||
setIconPreview(null);
|
||||
}
|
||||
}}
|
||||
style={{ display: 'flex', gap: 16, marginTop: 8 }}
|
||||
>
|
||||
<Radio value="default">选择系统图标</Radio>
|
||||
<Radio value="upload">上传图标</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
|
||||
{/* 默认图标库 */}
|
||||
{iconType === 'default' && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{ fontWeight: 'bold' }}>选择图标</span>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginTop: 8 }}>
|
||||
{defaultIcons.map((icon, index) => (
|
||||
<img
|
||||
key={index}
|
||||
src={icon}
|
||||
alt="icon"
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
cursor: 'pointer',
|
||||
border: selectedIcon === icon ? '2px solid #1890ff' : 'none',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedIcon(icon);
|
||||
setIconPreview(icon);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 图标上传 + 裁剪 */}
|
||||
{iconType === 'upload' && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<span style={{ fontWeight: 'bold' }}>上传图标(40x40)</span>
|
||||
<ImgCrop
|
||||
rotationSlider
|
||||
aspect={1} // 强制 1:1 宽高比
|
||||
modalTitle="裁剪图像"
|
||||
quality={0.8}
|
||||
onModalOk={() => {
|
||||
// 裁剪完成后自动更新 fileList 和 Base64 数据
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (img.width !== 40 || img.height !== 40) {
|
||||
message.error('裁剪图像尺寸必须为 40x40 像素');
|
||||
setIconPreview(null);
|
||||
setFileList([]);
|
||||
return;
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 40;
|
||||
canvas.height = 40;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx?.drawImage(img, 0, 0, 40, 40);
|
||||
const base64 = canvas.toDataURL('image/png');
|
||||
setSelectedIcon(base64);
|
||||
setIconPreview(base64);
|
||||
setFileList([
|
||||
{
|
||||
uid: '-1',
|
||||
name: 'icon.png',
|
||||
status: 'done',
|
||||
url: base64,
|
||||
originFileObj: new File([dataURLtoBlob(base64)], 'icon.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
};
|
||||
img.src = iconPreview;
|
||||
}}
|
||||
>
|
||||
<Upload
|
||||
name="icon"
|
||||
listType="picture-card"
|
||||
showUploadList={false}
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={({ fileList }) => {
|
||||
setFileList(fileList);
|
||||
}}
|
||||
fileList={fileList}
|
||||
style={{ marginTop: 8 }}
|
||||
>
|
||||
{iconPreview ? (
|
||||
<img
|
||||
src={iconPreview}
|
||||
alt="icon"
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ fontSize: 20 }}>+</div>
|
||||
)}
|
||||
</Upload>
|
||||
</ImgCrop>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 其他表单项 */}
|
||||
<ProFormDateTimePicker
|
||||
name="createTime"
|
||||
label="开始时间"
|
||||
@@ -76,13 +293,14 @@ const OperationModal: FC<OperationModalProps> = (props) => {
|
||||
}}
|
||||
placeholder="请选择"
|
||||
/>
|
||||
|
||||
<ProFormSelect
|
||||
name="ownerId"
|
||||
label="任务负责人"
|
||||
label="故事负责人"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择任务负责人',
|
||||
message: '请选择故事负责人',
|
||||
},
|
||||
]}
|
||||
options={[
|
||||
@@ -97,6 +315,7 @@ const OperationModal: FC<OperationModalProps> = (props) => {
|
||||
]}
|
||||
placeholder="请选择管理员"
|
||||
/>
|
||||
|
||||
<ProFormTextArea
|
||||
name="description"
|
||||
label="产品描述"
|
||||
@@ -113,7 +332,7 @@ const OperationModal: FC<OperationModalProps> = (props) => {
|
||||
<Result
|
||||
status="success"
|
||||
title="操作成功"
|
||||
subTitle="一系列的信息描述,很短同样也可以带标点。"
|
||||
subTitle={`${current?.instanceId ? '编辑' : '创建'}成功`}
|
||||
extra={
|
||||
<Button type="primary" onClick={onDone}>
|
||||
知道了
|
||||
@@ -125,4 +344,5 @@ const OperationModal: FC<OperationModalProps> = (props) => {
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperationModal;
|
||||
|
||||
Reference in New Issue
Block a user