支持故事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;
|
||||
|
||||
1
src/pages/list/basic-list/data.d.ts
vendored
1
src/pages/list/basic-list/data.d.ts
vendored
@@ -37,6 +37,7 @@ export interface StoryType {
|
||||
ownerId?: string;
|
||||
updatedId?: string;
|
||||
updateTime?: string;
|
||||
logo?: string;
|
||||
}
|
||||
export interface BaseResponse {
|
||||
code: number;
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import { DownOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { useRequest } from '@umijs/max';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Dropdown,
|
||||
Input,
|
||||
List,
|
||||
Modal,
|
||||
Radio,
|
||||
|
||||
} from 'antd';
|
||||
import { history, useRequest } from '@umijs/max';
|
||||
import { Avatar, Button, Card, Dropdown, Input, List, Modal, Radio } from 'antd';
|
||||
import type { FC } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import OperationModal from './components/OperationModal';
|
||||
import type {StoryType} from './data.d';
|
||||
import {addStory, deleteStory, queryTimelineList, updateStory} from './service';
|
||||
import type { StoryType } from './data.d';
|
||||
import { addStory, deleteStory, queryTimelineList, updateStory } from './service';
|
||||
import useStyles from './style.style';
|
||||
const RadioButton = Radio.Button;
|
||||
const RadioGroup = Radio.Group;
|
||||
|
||||
/*const RadioButton = Radio.Button;
|
||||
const RadioGroup = Radio.Group;*/
|
||||
const { Search } = Input;
|
||||
import { history } from '@umijs/max';
|
||||
|
||||
const ListContent = ({
|
||||
data: { ownerId, updatedId, createTime, updateTime, status },
|
||||
}: {
|
||||
@@ -62,9 +53,10 @@ export const BasicList: FC = () => {
|
||||
data: listData,
|
||||
loading,
|
||||
run,
|
||||
} = useRequest(() => {
|
||||
} = useRequest((storyName?: string) => {
|
||||
return queryTimelineList({
|
||||
count: 50,
|
||||
storyName,
|
||||
});
|
||||
});
|
||||
const { run: postRun } = useRequest(
|
||||
@@ -101,12 +93,12 @@ export const BasicList: FC = () => {
|
||||
});
|
||||
};
|
||||
const editAndDelete = (key: string | number, currentItem: StoryType) => {
|
||||
console.log(currentItem)
|
||||
console.log(currentItem);
|
||||
if (key === 'edit') showEditModal(currentItem);
|
||||
else if (key === 'delete') {
|
||||
Modal.confirm({
|
||||
title: '删除任务',
|
||||
content: '确定删除该任务吗?',
|
||||
title: '删除故事',
|
||||
content: '确定删除该故事吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => deleteItem(currentItem.instanceId ?? ''),
|
||||
@@ -115,14 +107,18 @@ export const BasicList: FC = () => {
|
||||
};
|
||||
const extraContent = (
|
||||
<div>
|
||||
<RadioGroup defaultValue="all">
|
||||
{/*<RadioGroup defaultValue="all">
|
||||
<RadioButton value="all">全部</RadioButton>
|
||||
<RadioButton value="progress">进行中</RadioButton>
|
||||
<RadioButton value="waiting">等待中</RadioButton>
|
||||
</RadioGroup>
|
||||
<Search className={styles.extraContentSearch} placeholder="请输入" onSearch={(value) => {
|
||||
run();
|
||||
}} />
|
||||
</RadioGroup>*/}
|
||||
<Search
|
||||
className={styles.extraContentSearch}
|
||||
placeholder="请输入"
|
||||
onSearch={(value) => {
|
||||
run(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const MoreBtn: React.FC<{
|
||||
@@ -156,16 +152,16 @@ export const BasicList: FC = () => {
|
||||
const handleSubmit = (values: StoryType) => {
|
||||
setDone(true);
|
||||
const method = current?.instanceId ? 'update' : 'add';
|
||||
postRun(method, {...current, ...values});
|
||||
postRun(method, { ...current, ...values });
|
||||
run();
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<PageContainer>
|
||||
<PageContainer title={"Timeline"}>
|
||||
<div className={styles.standardList}>
|
||||
<Card
|
||||
className={styles.listCard}
|
||||
variant={undefined}
|
||||
title="我的Timeline"
|
||||
style={{
|
||||
marginTop: 24,
|
||||
}}
|
||||
@@ -180,7 +176,7 @@ export const BasicList: FC = () => {
|
||||
loading={loading}
|
||||
pagination={paginationProps}
|
||||
dataSource={list}
|
||||
renderItem={(item:StoryType) => (
|
||||
renderItem={(item: StoryType) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<a
|
||||
@@ -197,7 +193,15 @@ export const BasicList: FC = () => {
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar src={item.logo} shape="square" size="large" />}
|
||||
title={<a onClick={() => {history.push(`/timeline/${item.instanceId}`)}}>{item.title}</a>}
|
||||
title={
|
||||
<a
|
||||
onClick={() => {
|
||||
history.push(`/timeline/${item.instanceId}`);
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
}
|
||||
description={item.description}
|
||||
/>
|
||||
<ListContent data={item} />
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
import { request } from '@umijs/max';
|
||||
import {AddStoryItem, BaseResponse, StoryItem, StoryType} from './data.d';
|
||||
import { StoryItem, StoryType } from './data.d';
|
||||
|
||||
type ParamsType = {
|
||||
count?: number;
|
||||
instanceId?: string;
|
||||
storyName?: string;
|
||||
} & Partial<StoryType>;
|
||||
|
||||
export async function queryTimelineList(
|
||||
params: ParamsType,
|
||||
): Promise<{ data: { data: StoryType[] } }> {
|
||||
): Promise<{ data: StoryType[] }> {
|
||||
return await request('/story/owner/test11', {
|
||||
params
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteStory(
|
||||
params: ParamsType,
|
||||
): Promise<{ data: { list: StoryType[] } }> {
|
||||
export async function deleteStory(params: ParamsType): Promise<{ data: { list: StoryType[] } }> {
|
||||
return request(`/story/${params.instanceId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export async function addStory(
|
||||
params: ParamsType,
|
||||
): Promise<{ data: { list: StoryType[] } }> {
|
||||
export async function addStory(params: ParamsType): Promise<{ data: { list: StoryType[] } }> {
|
||||
return request('/story/add', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
@@ -33,9 +30,8 @@ export async function addStory(
|
||||
},
|
||||
});
|
||||
}
|
||||
export async function updateStory(
|
||||
params: ParamsType,
|
||||
): Promise<{ data: { list: StoryType[] } }> {
|
||||
|
||||
export async function updateStory(params: ParamsType): Promise<{ data: { list: StoryType[] } }> {
|
||||
return await request(`/story/${params.instanceId}`, {
|
||||
method: 'PUT',
|
||||
data: {
|
||||
@@ -45,9 +41,7 @@ export async function updateStory(
|
||||
});
|
||||
}
|
||||
|
||||
export async function addStoryItem(
|
||||
params: FormData,
|
||||
): Promise<any> {
|
||||
export async function addStoryItem(params: FormData): Promise<any> {
|
||||
return request(`/story/item`, {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
@@ -55,9 +49,8 @@ export async function addStoryItem(
|
||||
getResponse: true,
|
||||
});
|
||||
}
|
||||
export async function queryStoryItem(
|
||||
masterItemId: string,
|
||||
): Promise<{ data: StoryItem[] }> {
|
||||
|
||||
export async function queryStoryItem(masterItemId: string): Promise<{ data: StoryItem[] }> {
|
||||
return request(`/story/item/list`, {
|
||||
method: 'GET',
|
||||
params: {
|
||||
@@ -65,26 +58,23 @@ export async function queryStoryItem(
|
||||
},
|
||||
});
|
||||
}
|
||||
export async function queryStoryItemDetail(
|
||||
itemId: string,
|
||||
): Promise<{ data: StoryItem[] }> {
|
||||
|
||||
export async function queryStoryItemDetail(itemId: string): Promise<{ data: StoryItem[] }> {
|
||||
return request(`/story/item/${itemId}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
export async function queryStoryItemImages(
|
||||
itemId: string,
|
||||
): Promise<{ data: string[] }> {
|
||||
|
||||
export async function queryStoryItemImages(itemId: string): Promise<{ data: string[] }> {
|
||||
return request(`/story/item/images/${itemId}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
export async function fetchImage(
|
||||
imageInstanceId: string,
|
||||
): Promise<any> {
|
||||
|
||||
export async function fetchImage(imageInstanceId: string): Promise<any> {
|
||||
return request(`/file/download/cover/${imageInstanceId}`, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
getResponse: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,6 +134,20 @@ const useStyles = createStyles(({ token }) => {
|
||||
width: '100%',
|
||||
"[class^='title']": { marginBottom: '8px' },
|
||||
},
|
||||
iconUploader: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
objectFit: 'contain',
|
||||
border: '1px dashed #ddd',
|
||||
borderRadius: 2,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
iconList: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user