支持故事logo,增加默认logo

This commit is contained in:
jiangh277
2025-07-25 20:25:28 +08:00
parent 04dde093a8
commit 56a0042011
6 changed files with 324 additions and 73 deletions

View File

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