Story排版修改
This commit is contained in:
159
src/pages/story/_mock.ts
Normal file
159
src/pages/story/_mock.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import type { BasicListItemDataType } from './data.d';
|
||||
|
||||
const titles = [
|
||||
'Alipay',
|
||||
'Angular',
|
||||
'Ant Design',
|
||||
'Ant Design Pro',
|
||||
'Bootstrap',
|
||||
'React',
|
||||
'Vue',
|
||||
'Webpack',
|
||||
];
|
||||
const avatars = [
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
|
||||
];
|
||||
|
||||
const covers = [
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png',
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png',
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png',
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
|
||||
];
|
||||
const desc = [
|
||||
'那是一种内在的东西, 他们到达不了,也无法触及的',
|
||||
'希望是一个好东西,也许是最好的,好东西是不会消亡的',
|
||||
'生命就像一盒巧克力,结果往往出人意料',
|
||||
'城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
|
||||
'那时候我只会想自己想要什么,从不想自己拥有什么',
|
||||
];
|
||||
|
||||
const user = [
|
||||
'付小小',
|
||||
'曲丽丽',
|
||||
'林东东',
|
||||
'周星星',
|
||||
'吴加好',
|
||||
'朱偏右',
|
||||
'鱼酱',
|
||||
'乐哥',
|
||||
'谭小仪',
|
||||
'仲尼',
|
||||
];
|
||||
|
||||
function fakeList(count: number): BasicListItemDataType[] {
|
||||
const list = [];
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
list.push({
|
||||
id: `fake-list-${i}`,
|
||||
owner: user[i % 10],
|
||||
title: '',
|
||||
avatar: avatars[i % 8],
|
||||
cover: parseInt(`${i / 4}`, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
|
||||
status: ['active', 'exception', 'normal'][i % 3] as
|
||||
| 'normal'
|
||||
| 'exception'
|
||||
| 'active'
|
||||
| 'success',
|
||||
percent: Math.ceil(Math.random() * 50) + 50,
|
||||
logo: avatars[i % 8],
|
||||
href: 'https://ant.design',
|
||||
updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
|
||||
createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
|
||||
subDescription: desc[i % 5],
|
||||
description:
|
||||
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
|
||||
activeUser: Math.ceil(Math.random() * 100000) + 100000,
|
||||
newUser: Math.ceil(Math.random() * 1000) + 1000,
|
||||
star: Math.ceil(Math.random() * 100) + 100,
|
||||
like: Math.ceil(Math.random() * 100) + 100,
|
||||
message: Math.ceil(Math.random() * 10) + 10,
|
||||
content:
|
||||
'段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
|
||||
members: [
|
||||
{
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
|
||||
name: '曲丽丽',
|
||||
id: 'member1',
|
||||
},
|
||||
{
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
|
||||
name: '王昭君',
|
||||
id: 'member2',
|
||||
},
|
||||
{
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
|
||||
name: '董娜娜',
|
||||
id: 'member3',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
let sourceData: BasicListItemDataType[] = [];
|
||||
|
||||
function getFakeList(req: Request, res: Response) {
|
||||
const params = req.query as any;
|
||||
|
||||
const count = Number(params.count) * 1 || 20;
|
||||
|
||||
const result = fakeList(count);
|
||||
sourceData = result;
|
||||
return res.json({
|
||||
data: {
|
||||
list: result,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function postFakeList(req: Request, res: Response) {
|
||||
const { /* url = '', */ body } = req;
|
||||
// const params = getUrlParams(url);
|
||||
const { method, id } = body;
|
||||
// const count = (params.count * 1) || 20;
|
||||
let result = sourceData || [];
|
||||
|
||||
switch (method) {
|
||||
case 'delete':
|
||||
result = result.filter((item) => item.id !== id);
|
||||
break;
|
||||
case 'update':
|
||||
result.forEach((item, i) => {
|
||||
if (item.id === id) {
|
||||
result[i] = { ...item, ...body };
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'post':
|
||||
result.unshift({
|
||||
...body,
|
||||
id: `fake-list-${result.length}`,
|
||||
createdAt: new Date().getTime(),
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return res.json({
|
||||
data: {
|
||||
list: result,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
'GET /api/get_list': getFakeList,
|
||||
'POST /api/post_fake_list': postFakeList,
|
||||
};
|
||||
63
src/pages/story/components/AddSubTimeLineItemModal.tsx
Normal file
63
src/pages/story/components/AddSubTimeLineItemModal.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Form, Modal, Input, TimePicker, Button } from 'antd';
|
||||
|
||||
interface SubItem {
|
||||
time: string;
|
||||
title: string;
|
||||
description: string;
|
||||
cover?: string | null;
|
||||
}
|
||||
|
||||
interface ModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onOk: (values: SubItem) => void;
|
||||
}
|
||||
|
||||
const AddSubTimeLineItemModal: React.FC<ModalProps> = ({ visible, onCancel, onOk }) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
onOk(values);
|
||||
} catch (error) {
|
||||
console.error('表单校验失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
title="添加子时间点"
|
||||
onCancel={() => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
}}
|
||||
footer={[
|
||||
<Button key="back" onClick={onCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleOk}>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="vertical" name="subTimeLineItemForm">
|
||||
<Form.Item label="时间(HH:mm)" name="time" rules={[{ required: true, message: '请输入时间' }]}>
|
||||
<Input placeholder="例如:14:30" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="子标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
|
||||
<Input placeholder="请输入子标题" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="子描述" name="description">
|
||||
<Input.TextArea rows={4} placeholder="请输入子时间点描述" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddSubTimeLineItemModal;
|
||||
200
src/pages/story/components/AddTimeLineItemModal.tsx
Normal file
200
src/pages/story/components/AddTimeLineItemModal.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
// src/pages/list/basic-list/components/AddTimeLineItemModal.tsx
|
||||
import chinaRegion, { code2Location } from '@/commonConstant/chinaRegion';
|
||||
import { addStoryItem } from '@/pages/story/service';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { useRequest } from '@umijs/max';
|
||||
import { Button, Cascader, DatePicker, Form, Input, message, Modal, Upload } from 'antd';
|
||||
import ImgCrop from 'antd-img-crop';
|
||||
import dayjs from 'dayjs';
|
||||
import moment from 'moment';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
interface ModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onOk: () => void;
|
||||
storyId: string | number | undefined;
|
||||
initialValues?: any;
|
||||
storyItemId?: string; // 是否根节点
|
||||
option: 'add' | 'edit' | 'addSubItem' | 'editSubItem';
|
||||
}
|
||||
|
||||
const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
onOk,
|
||||
storyId,
|
||||
initialValues,
|
||||
storyItemId,
|
||||
option,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [fileList, setFileList] = useState<any[]>([]);
|
||||
const [imageList, setImageList] = useState<any[]>(initialValues?.images || []);
|
||||
useEffect(() => {
|
||||
if (initialValues && option === 'edit') {
|
||||
form.setFieldsValue({
|
||||
title: initialValues.title,
|
||||
storyItemTime: initialValues.date ? moment(initialValues.date) : undefined,
|
||||
location: initialValues.location,
|
||||
description: initialValues.description,
|
||||
cover: initialValues.cover ? [{ url: initialValues.cover }] : [],
|
||||
images: initialValues.images?.map((url) => ({ url })) || [],
|
||||
});
|
||||
}
|
||||
}, [initialValues, option]);
|
||||
const { run: submitItem, loading } = useRequest((newItem) => addStoryItem(newItem), {
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
console.log(data);
|
||||
if (data.code === 200) {
|
||||
onOk();
|
||||
message.success(initialValues ? '时间点已更新' : '时间点已保存');
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error('保存失败');
|
||||
console.error('保存失败:', error);
|
||||
},
|
||||
});
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const location = code2Location(values.location);
|
||||
const newItem = {
|
||||
...values,
|
||||
id: initialValues?.id || Date.now(),
|
||||
storyItemTime: dayjs(values.date).format('YYYY-MM-DDTHH:mm:ss'),
|
||||
masterItemId: initialValues.masterItemId,
|
||||
subItems: initialValues?.subItems || [],
|
||||
storyInstanceId: storyId,
|
||||
location,
|
||||
};
|
||||
delete newItem.cover;
|
||||
delete newItem.images;
|
||||
// 构建 FormData
|
||||
const formData = new FormData();
|
||||
|
||||
// 添加 storyItem 作为 JSON 字符串
|
||||
formData.append('storyItem', JSON.stringify(newItem));
|
||||
|
||||
// 添加封面文件
|
||||
if (fileList.length > 0 && fileList[0].originFileObj instanceof File) {
|
||||
formData.append('cover', fileList[0].originFileObj);
|
||||
}
|
||||
console.log(imageList);
|
||||
if (imageList.length > 0) {
|
||||
imageList.forEach((file) => {
|
||||
if (file.originFileObj && file.originFileObj instanceof File) {
|
||||
formData.append('images', file.originFileObj);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 提交
|
||||
submitItem(formData);
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error);
|
||||
message.error('请检查表单内容');
|
||||
}
|
||||
};
|
||||
|
||||
const uploadCoverProps = {
|
||||
beforeUpload: (file) => {
|
||||
// 确保 originFileObj 是真正的 File 对象
|
||||
return false; // 阻止自动上传
|
||||
},
|
||||
onChange: ({ fileList }) => {
|
||||
// 确保 originFileObj 是真正的 File 对象
|
||||
const updatedFileList = fileList.map((file) => {
|
||||
if (file.originFileObj && !(file.originFileObj instanceof File)) {
|
||||
file.originFileObj = new File([file.originFileObj], file.name, { type: file.type });
|
||||
}
|
||||
return file;
|
||||
});
|
||||
setFileList(updatedFileList);
|
||||
},
|
||||
listType: 'picture',
|
||||
maxCount: 1,
|
||||
defaultFileList: initialValues?.cover ? [{ url: initialValues.cover }] : [],
|
||||
};
|
||||
|
||||
const uploadImagesProps = {
|
||||
beforeUpload: () => false,
|
||||
onChange: ({ fileList }) => {
|
||||
const updatedFileList = fileList.map((file) => {
|
||||
if (file.originFileObj && !(file.originFileObj instanceof File)) {
|
||||
file.originFileObj = new File([file.originFileObj], file.name, { type: file.type });
|
||||
}
|
||||
return file;
|
||||
});
|
||||
setImageList(updatedFileList);
|
||||
},
|
||||
listType: 'picture-card',
|
||||
multiple: true,
|
||||
defaultFileList: initialValues?.images?.map((url) => ({ url })),
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
title={initialValues ? '编辑时间点' : '添加时间点'}
|
||||
onCancel={() => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
}}
|
||||
footer={[
|
||||
<Button key="back" onClick={onCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleOk} loading={loading}>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
{['editSubItem', 'addSubItem'].includes( option) && <Form.Item label={'主时间点'}>{storyItemId}</Form.Item>}
|
||||
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
|
||||
<Input placeholder="请输入标题" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="时间" name="date" rules={[{ required: true, message: '请选择时间' }]}>
|
||||
<DatePicker
|
||||
showTime={{ defaultValue: dayjs('00:00:00', 'HH:mm:ss') }}
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="位置" name="location" rules={[{ required: false }]}>
|
||||
<Cascader
|
||||
options={chinaRegion}
|
||||
fieldNames={{ label: 'title', value: 'value', children: 'children' }}
|
||||
placeholder="请选择省/市/区"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea rows={4} placeholder="请输入时间点描述" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="封面图" name="cover">
|
||||
<ImgCrop aspect={1} quality={0.5} rotationSlider>
|
||||
<Upload {...uploadCoverProps} maxCount={1}>
|
||||
<Button icon={<UploadOutlined />}>上传封面</Button>
|
||||
</Upload>
|
||||
</ImgCrop>
|
||||
</Form.Item>
|
||||
|
||||
{/* 新增:时刻图库 */}
|
||||
<Form.Item label="时刻图库(多图)" name="images">
|
||||
<Upload {...uploadImagesProps} maxCount={5}>
|
||||
<Button size={'small'} icon={<UploadOutlined />}>
|
||||
上传多图
|
||||
</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTimeLineItemModal;
|
||||
348
src/pages/story/components/OperationModal.tsx
Normal file
348
src/pages/story/components/OperationModal.tsx
Normal file
@@ -0,0 +1,348 @@
|
||||
import {
|
||||
ModalForm,
|
||||
ProFormDateTimePicker,
|
||||
ProFormSelect,
|
||||
ProFormText,
|
||||
ProFormTextArea,
|
||||
} from '@ant-design/pro-components';
|
||||
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 '@/commonConstant/commonConstant';
|
||||
|
||||
type OperationModalProps = {
|
||||
done: boolean;
|
||||
open: boolean;
|
||||
current: Partial<StoryType> | undefined;
|
||||
onDone: () => void;
|
||||
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 ? '编辑' : '添加'}`}
|
||||
className={styles.standardListForm}
|
||||
width={640}
|
||||
onFinish={async (values) => {
|
||||
onSubmit({
|
||||
...values,
|
||||
logo: selectedIcon || '',
|
||||
});
|
||||
}}
|
||||
initialValues={{
|
||||
...current,
|
||||
logo: current?.logo ? 'upload' : 'default',
|
||||
}}
|
||||
submitter={{
|
||||
render: (_, dom) => (done ? null : dom),
|
||||
}}
|
||||
trigger={<>{children}</>}
|
||||
modalProps={{
|
||||
onCancel: () => onDone(),
|
||||
destroyOnClose: true,
|
||||
bodyStyle: done
|
||||
? {
|
||||
padding: '72px 0',
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
>
|
||||
{!done ? (
|
||||
<>
|
||||
<ProFormText
|
||||
name="title"
|
||||
label="故事名称"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
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="开始时间"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择开始时间',
|
||||
},
|
||||
]}
|
||||
fieldProps={{
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
placeholder="请选择"
|
||||
/>
|
||||
|
||||
<ProFormSelect
|
||||
name="ownerId"
|
||||
label="故事负责人"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择故事负责人',
|
||||
},
|
||||
]}
|
||||
options={[
|
||||
{
|
||||
label: '付晓晓',
|
||||
value: 'xiao',
|
||||
},
|
||||
{
|
||||
label: '周毛毛',
|
||||
value: 'mao',
|
||||
},
|
||||
]}
|
||||
placeholder="请选择管理员"
|
||||
/>
|
||||
|
||||
<ProFormTextArea
|
||||
name="description"
|
||||
label="产品描述"
|
||||
rules={[
|
||||
{
|
||||
message: '请输入至少五个字符的产品描述!',
|
||||
min: 5,
|
||||
},
|
||||
]}
|
||||
placeholder="请输入至少五个字符"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Result
|
||||
status="success"
|
||||
title="操作成功"
|
||||
subTitle={`${current?.instanceId ? '编辑' : '创建'}成功`}
|
||||
extra={
|
||||
<Button type="primary" onClick={onDone}>
|
||||
知道了
|
||||
</Button>
|
||||
}
|
||||
className={styles.formResult}
|
||||
/>
|
||||
)}
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperationModal;
|
||||
97
src/pages/story/components/SubTimeLineItemModal.tsx
Normal file
97
src/pages/story/components/SubTimeLineItemModal.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
// file: SubTimeLineItemModal.tsx
|
||||
import React, { useEffect } from 'react';
|
||||
import { Form, Modal, Input, DatePicker, Upload, Button } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import moment from 'moment';
|
||||
|
||||
interface ModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onOk: (values: any) => void;
|
||||
initialValues?: any;
|
||||
}
|
||||
|
||||
const SubTimeLineItemModal: React.FC<ModalProps> = ({ visible, onCancel, onOk, initialValues }) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
form.setFieldsValue({
|
||||
title: initialValues.title,
|
||||
date: initialValues.date ? moment(initialValues.date) : undefined,
|
||||
location: initialValues.location,
|
||||
description: initialValues.description,
|
||||
cover: initialValues.cover,
|
||||
});
|
||||
}
|
||||
}, [initialValues]);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const newItem = {
|
||||
...values,
|
||||
id: initialValues?.id || Date.now(),
|
||||
cover:
|
||||
form.getFieldValue('cover') ||
|
||||
(initialValues && initialValues.cover),
|
||||
date: values.date.format('YYYY-MM-DD'),
|
||||
};
|
||||
onOk(newItem);
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadProps = {
|
||||
beforeUpload: () => false,
|
||||
onChange: ({ fileList }) => {},
|
||||
listType: 'picture' as const,
|
||||
maxCount: 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
title={initialValues ? '编辑时间点' : '添加时间点'}
|
||||
onCancel={() => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
}}
|
||||
footer={[
|
||||
<Button key="back" onClick={onCancel}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleOk}>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} layout="vertical" name="timeLineItemForm">
|
||||
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
|
||||
<Input placeholder="请输入标题" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="时间" name="date" rules={[{ required: true, message: '请选择时间' }]}>
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="地点" name="location">
|
||||
<Input placeholder="请输入地点" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea rows={4} placeholder="请输入时间点描述" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="封面图片" name="cover">
|
||||
<Upload {...uploadProps} maxCount={1}>
|
||||
<Button icon={<UploadOutlined />}>上传图片</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubTimeLineItemModal;
|
||||
182
src/pages/story/components/TimelineItem/TimelineItem.tsx
Normal file
182
src/pages/story/components/TimelineItem/TimelineItem.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import {DeleteOutlined, DownOutlined, EditOutlined, PlusOutlined, UpOutlined} from '@ant-design/icons';
|
||||
import {useIntl, useRequest} from '@umijs/max';
|
||||
import { Button, Card, Popconfirm, message } from 'antd';
|
||||
import React, {useState} from 'react';
|
||||
import {queryStoryItemImages, removeStoryItem} from '../../service';
|
||||
import useStyles from './index.style';
|
||||
import {StoryItem} from "@/pages/story/data";
|
||||
import TimelineImage from "@/components/TimelineImage";
|
||||
import TimelineItemDrawer from '../TimelineItemDrawer';
|
||||
|
||||
const TimelineItem: React.FC<{
|
||||
item: StoryItem;
|
||||
handleOption: (item: StoryItem, option: 'add' | 'edit' | 'addSubItem' | 'editSubItem') => void;
|
||||
refresh: () => void;
|
||||
}> = ({ item, handleOption, refresh }) => {
|
||||
const { styles } = useStyles();
|
||||
const intl = useIntl();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [showActions, setShowActions] = useState(false);
|
||||
const [subItemsExpanded, setSubItemsExpanded] = useState(false);
|
||||
const [openDetail, setOpenDetail] = useState(false)
|
||||
const { data: imagesList } = useRequest(
|
||||
async () => {
|
||||
return await queryStoryItemImages(item.instanceId);
|
||||
},
|
||||
);
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
if (!item.instanceId) return;
|
||||
const response = await removeStoryItem(item.instanceId);
|
||||
if (response.code === 200) {
|
||||
message.success(intl.formatMessage({ id: 'story.deleteSuccess' }));
|
||||
refresh();
|
||||
} else {
|
||||
message.error(intl.formatMessage({ id: 'story.deleteFailed' }));
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(intl.formatMessage({ id: 'story.deleteFailed' }));
|
||||
}
|
||||
};
|
||||
const toggleDescription = () => {
|
||||
setExpanded(!expanded);
|
||||
};
|
||||
|
||||
const toggleSubItems = () => {
|
||||
setSubItemsExpanded(!subItemsExpanded);
|
||||
};
|
||||
|
||||
const displayedDescription = expanded
|
||||
? item.description
|
||||
: item.description?.substring(0, 100) + (item.description && item.description.length > 100 ? '...' : '');
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.timelineItem}
|
||||
title={item.title}
|
||||
onMouseEnter={() => setShowActions(true)}
|
||||
onMouseLeave={() => setShowActions(false)}
|
||||
onClick={() => setOpenDetail(true)}
|
||||
extra={
|
||||
<div
|
||||
className={styles.actions}
|
||||
>
|
||||
{showActions && (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOption(item, 'editSubItem');
|
||||
}}
|
||||
aria-label={intl.formatMessage({ id: 'story.edit' })}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOption(item, 'addSubItem');
|
||||
}}
|
||||
aria-label={intl.formatMessage({ id: 'story.addSubItem' })}
|
||||
/>
|
||||
<Popconfirm
|
||||
title={intl.formatMessage({ id: 'story.deleteConfirm' })}
|
||||
description={intl.formatMessage({ id: 'story.deleteConfirmDescription' })}
|
||||
onConfirm={(e) => {
|
||||
e?.stopPropagation();
|
||||
handleDelete()
|
||||
}}
|
||||
okText={intl.formatMessage({ id: 'story.yes' })}
|
||||
cancelText={intl.formatMessage({ id: 'story.no' })}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={intl.formatMessage({ id: 'story.delete' })}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
// onClick={() => onDetail(item)}
|
||||
hoverable
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.date}>
|
||||
{item.storyItemTime} {item.location ? `创建于${item.location}` : ''}
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
{displayedDescription}
|
||||
{item.description && item.description.length > 100 && (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleDescription();
|
||||
}}
|
||||
>
|
||||
{expanded
|
||||
? intl.formatMessage({ id: 'story.showLess' })
|
||||
: intl.formatMessage({ id: 'story.showMore' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{imagesList && imagesList.length > 0 && (
|
||||
<>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', marginBottom: '20px' }}>
|
||||
{imagesList.map((imageInstanceId, index) => (
|
||||
<TimelineImage
|
||||
key={imageInstanceId + index}
|
||||
title={imageInstanceId}
|
||||
imageInstanceId={imageInstanceId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{item.subItems && item.subItems.length > 0 && (
|
||||
<div className={styles.subItems}>
|
||||
<div
|
||||
className={styles.subItemsHeader}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleSubItems();
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'story.subItems' })} ({item.subItems.length})
|
||||
</span>
|
||||
{subItemsExpanded ? <UpOutlined /> : <DownOutlined />}
|
||||
</div>
|
||||
{subItemsExpanded && (
|
||||
<div className={styles.subItemsList}>
|
||||
{item.subItems.map((subItem) => (
|
||||
<div key={subItem.id} className={styles.subItem}>
|
||||
<div className={styles.subItemDate}>
|
||||
{item.storyItemTime} {item.location ? `创建于${item.location}` : ''}
|
||||
</div>
|
||||
<div className={styles.subItemContent}>
|
||||
{subItem.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TimelineItemDrawer
|
||||
storyItem={item}
|
||||
open={openDetail}
|
||||
setOpen={setOpenDetail}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineItem;
|
||||
38
src/pages/story/components/TimelineItem/index.css
Normal file
38
src/pages/story/components/TimelineItem/index.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.timeline-item {
|
||||
margin-bottom: 24px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: box-shadow 0.3s;
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-event-container {
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.timeline-content-text {
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-timeline-wrapper {
|
||||
margin-top: 16px;
|
||||
background-color: #f9f9f9;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sub-timeline-item {
|
||||
padding: 8px 0;
|
||||
transition: background-color 0.2s;
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
90
src/pages/story/components/TimelineItem/index.style.ts
Normal file
90
src/pages/story/components/TimelineItem/index.style.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
const useStyles = createStyles(({ token }) => {
|
||||
return {
|
||||
timelineItem: {
|
||||
marginBottom: '20px',
|
||||
boxShadow: token.boxShadow,
|
||||
borderRadius: token.borderRadius,
|
||||
transition: 'all 0.3s',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
alignItems: 'center',
|
||||
height: '24px',
|
||||
width: '120px',
|
||||
},
|
||||
content: {
|
||||
padding: '10px 0',
|
||||
},
|
||||
cover: {
|
||||
width: '100%',
|
||||
height: '200px',
|
||||
overflow: 'hidden',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '15px',
|
||||
img: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
},
|
||||
},
|
||||
date: {
|
||||
fontSize: '14px',
|
||||
color: token.colorTextSecondary,
|
||||
marginBottom: '10px',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
description: {
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.6',
|
||||
color: token.colorText,
|
||||
marginBottom: '15px',
|
||||
'.ant-btn-link': {
|
||||
padding: '0 4px',
|
||||
},
|
||||
},
|
||||
subItems: {
|
||||
borderTop: `1px dashed ${token.colorBorder}`,
|
||||
paddingTop: '15px',
|
||||
marginTop: '15px',
|
||||
},
|
||||
subItemsHeader: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold',
|
||||
color: token.colorTextHeading,
|
||||
marginBottom: '10px',
|
||||
padding: '5px 0',
|
||||
},
|
||||
subItemsList: {
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
subItem: {
|
||||
display: 'flex',
|
||||
marginBottom: '10px',
|
||||
'&:last-child': {
|
||||
marginBottom: 0,
|
||||
},
|
||||
},
|
||||
subItemDate: {
|
||||
fontWeight: 'bold',
|
||||
minWidth: '100px',
|
||||
color: token.colorTextSecondary,
|
||||
},
|
||||
subItemContent: {
|
||||
flex: 1,
|
||||
color: token.colorText,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default useStyles;
|
||||
139
src/pages/story/components/TimelineItemDrawer.tsx
Normal file
139
src/pages/story/components/TimelineItemDrawer.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import TimelineImage from '@/components/TimelineImage';
|
||||
import AddTimeLineItemModal from '@/pages/story/components/AddTimeLineItemModal';
|
||||
import SubTimeLineItemModal from '@/pages/story/components/SubTimeLineItemModal';
|
||||
import { StoryItem } from '@/pages/story/data';
|
||||
import { queryStoryItemImages } from '@/pages/story/service';
|
||||
import { EditOutlined, PlusCircleOutlined } from '@ant-design/icons';
|
||||
import { useRequest } from '@umijs/max';
|
||||
import { Button, Drawer, Space } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
interface Props {
|
||||
storyItem: StoryItem;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const TimelineItemDrawer = (props: Props) => {
|
||||
const { storyItem, open, setOpen } = props;
|
||||
const [editModalVisible, setEditModalVisible] = React.useState(false);
|
||||
const [openAddSubItemModal, setOpenAddSubItemModal] = React.useState(false);
|
||||
const [openEditMainItemModal, setOpenEditMainItemModal] = React.useState(false);
|
||||
const { data: imagesList, run } = useRequest(
|
||||
async (itemId) => {
|
||||
return await queryStoryItemImages(itemId);
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
console.log(storyItem);
|
||||
run(storyItem.instanceId);
|
||||
}
|
||||
}, [open]);
|
||||
const closeDrawer = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
const handleEditMainItem = (updatedItem: any) => {
|
||||
const mergedEvent = {
|
||||
...storyItem,
|
||||
...updatedItem,
|
||||
};
|
||||
setOpenEditMainItemModal(false);
|
||||
};
|
||||
const handleAddSubItem = () => {
|
||||
setOpenAddSubItemModal(false);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{/* 主时间点详情抽屉 */}
|
||||
<Drawer
|
||||
width={1000}
|
||||
placement="right"
|
||||
onClose={() => {
|
||||
closeDrawer();
|
||||
}}
|
||||
open={open}
|
||||
title={storyItem.title}
|
||||
footer={
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<PlusCircleOutlined />}
|
||||
type="primary"
|
||||
onClick={() => setOpenAddSubItemModal(true)}
|
||||
>
|
||||
添加子时间点
|
||||
</Button>
|
||||
<Button icon={<EditOutlined />} onClick={() => setEditModalVisible(true)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button onClick={closeDrawer}>关闭</Button>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<strong>描述:</strong> {storyItem.description}
|
||||
</p>
|
||||
<p>
|
||||
<strong>日期:</strong> {storyItem.storyItemTime}
|
||||
</p>
|
||||
<p>
|
||||
<strong>位置:</strong> {storyItem.location}
|
||||
</p>
|
||||
{/* 封面图 */}
|
||||
{storyItem.coverInstanceId && (
|
||||
<>
|
||||
<p>
|
||||
<strong>封面图:</strong>
|
||||
</p>
|
||||
<TimelineImage
|
||||
title={storyItem.title + 'cover'}
|
||||
imageInstanceId={storyItem.coverInstanceId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 时刻图库 */}
|
||||
{imagesList && imagesList.length > 0 && (
|
||||
<>
|
||||
<p>
|
||||
<strong>时刻图库:</strong>
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', marginBottom: '20px' }}>
|
||||
{imagesList.map((imageInstanceId, index) => (
|
||||
<TimelineImage
|
||||
key={imageInstanceId + index}
|
||||
title={imageInstanceId}
|
||||
imageInstanceId={imageInstanceId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 添加子时间点模态框 */}
|
||||
<AddTimeLineItemModal
|
||||
visible={openAddSubItemModal}
|
||||
onOk={handleAddSubItem}
|
||||
onCancel={() => setOpenAddSubItemModal(false)}
|
||||
lineId={storyItem.storyInstanceId}
|
||||
storyItemId={storyItem.instanceId}
|
||||
/>
|
||||
|
||||
{/* 编辑主时间点模态框 */}
|
||||
<SubTimeLineItemModal
|
||||
visible={openEditMainItemModal}
|
||||
onOk={handleEditMainItem}
|
||||
onCancel={() => setOpenEditMainItemModal(false)}
|
||||
initialValues={storyItem}
|
||||
/>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineItemDrawer;
|
||||
80
src/pages/story/data.d.ts
vendored
Normal file
80
src/pages/story/data.d.ts
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
export type Member = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type BasicListItemDataType = {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
title: string;
|
||||
avatar: string;
|
||||
cover: string;
|
||||
status: 'normal' | 'exception' | 'active' | 'success';
|
||||
percent: number;
|
||||
logo: string;
|
||||
href: string;
|
||||
body?: any;
|
||||
updatedAt: number;
|
||||
createdAt: number;
|
||||
subDescription: string;
|
||||
description: string;
|
||||
activeUser: number;
|
||||
newUser: number;
|
||||
star: number;
|
||||
like: number;
|
||||
message: number;
|
||||
content: string;
|
||||
members: Member[];
|
||||
};
|
||||
export interface StoryType {
|
||||
id?: number;
|
||||
instanceId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
createTime?: string;
|
||||
ownerId?: string;
|
||||
updatedId?: string;
|
||||
updateTime?: string;
|
||||
logo?: string;
|
||||
}
|
||||
export interface BaseResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
||||
export interface ErrorResponse extends BaseResponse {
|
||||
data: null;
|
||||
code: -1,
|
||||
message: "请求失败"
|
||||
}
|
||||
export interface StoryItem {
|
||||
id?: number;
|
||||
instanceId: string;
|
||||
title: string;
|
||||
storyInstanceId: string;
|
||||
masterItemId?: string;
|
||||
description: string;
|
||||
storyItemTime: string; // YYYY-MM-DD
|
||||
createTime: string; // YYYY-MM-DD
|
||||
updateTime: string; // YYYY-MM-DD
|
||||
location?: string;
|
||||
coverInstanceId?: string; // 封面图
|
||||
images?: string[]; // 多张图片
|
||||
subItems?: StoryItem[];
|
||||
isRoot: number;
|
||||
}
|
||||
export interface AddStoryItem extends StoryItem{
|
||||
file: FormData;
|
||||
}
|
||||
export interface TimelineEvent {
|
||||
id?: number;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string; // YYYY-MM-DD
|
||||
time?: string; // HH:mm (可选)
|
||||
location?: string;
|
||||
cover?: string; // 封面图
|
||||
images?: string[]; // 多张图片
|
||||
subItems?: TimelineEvent[];
|
||||
}
|
||||
129
src/pages/story/detail.tsx
Normal file
129
src/pages/story/detail.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import AddTimeLineItemModal from '@/pages/story/components/AddTimeLineItemModal';
|
||||
import TimelineItem from '@/pages/story/components/TimelineItem/TimelineItem';
|
||||
import { StoryItem } from '@/pages/story/data';
|
||||
import { countStoryItem, queryStoryDetail, queryStoryItem } from '@/pages/story/service';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { history, useIntl, useRequest } from '@umijs/max';
|
||||
import { FloatButton, Spin, Timeline } from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import './index.css';
|
||||
import useStyles from './style.style';
|
||||
|
||||
interface TimelineItemProps {
|
||||
children: React.ReactNode; // 修正:使用 ReactNode 更通用
|
||||
// label: string
|
||||
}
|
||||
|
||||
const Index = () => {
|
||||
const { id: lineId } = useParams<{ id: string }>();
|
||||
const { styles } = useStyles();
|
||||
const intl = useIntl();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [items, setItems] = useState<TimelineItemProps[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasMoreNew, setHasMoreNew] = useState(true);
|
||||
const [hasMoreOld, setHasMoreOld] = useState(true);
|
||||
const [openAddItemModal, setOpenAddItemModal] = useState(false);
|
||||
const [currentItem, setCurrentItem] = useState<StoryItem>();
|
||||
const [currentOption, setCurrentOption] = useState<'add' | 'edit' | 'addSubItem' | 'editSubItem'>();
|
||||
|
||||
const { data: storyItemList, run } = useRequest(
|
||||
() => {
|
||||
return queryStoryItem(lineId ?? '');
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
},
|
||||
);
|
||||
const {
|
||||
data: detail,
|
||||
run: queryDetail,
|
||||
loading: queryDetailLoading,
|
||||
} = useRequest(() => {
|
||||
return queryStoryDetail(lineId ?? '');
|
||||
});
|
||||
const { data: count, run: queryCount } = useRequest(() => {
|
||||
return countStoryItem(lineId ?? '');
|
||||
});
|
||||
// 初始化加载数据
|
||||
useEffect(() => {
|
||||
run();
|
||||
}, [lineId]);
|
||||
useEffect(() => {
|
||||
if (!storyItemList?.length) return;
|
||||
console.log(storyItemList);
|
||||
let timelineItems = storyItemList; //handleStoryItemList(storyItemList);
|
||||
// 转换为 Timeline 组件需要的格式
|
||||
const formattedItems = timelineItems.map((item: StoryItem) => ({
|
||||
children: (
|
||||
<TimelineItem
|
||||
item={item}
|
||||
handleOption={(item: StoryItem, option: 'add' | 'edit' | 'addSubItem' | 'editSubItem') => {
|
||||
setCurrentItem(item);
|
||||
setCurrentOption(option)
|
||||
setOpenAddItemModal(true);
|
||||
}}
|
||||
refresh={() => {
|
||||
run();
|
||||
queryCount();
|
||||
queryDetail();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
setItems(formattedItems);
|
||||
}, [storyItemList]);
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
onBack={() => history.push('/story')}
|
||||
title={queryDetailLoading ? '加载中' : `${detail?.title} ${count ? `共${count}个时刻` : ``}`}
|
||||
>
|
||||
<div className="timeline" ref={containerRef}>
|
||||
{!hasMoreOld && <div style={{ textAlign: 'center', color: '#999' }}>没有更老的内容</div>}
|
||||
{loading && <Spin style={{ display: 'block', margin: '20px auto' }} />}
|
||||
<Timeline items={items} mode={'left'} />
|
||||
{loading && <Spin style={{ display: 'block', margin: '20px auto' }} />}
|
||||
{!hasMoreNew && <div style={{ textAlign: 'center', color: '#999' }}>没有更新的内容</div>}
|
||||
</div>
|
||||
<FloatButton onClick={() => {
|
||||
setCurrentOption('add');
|
||||
setCurrentItem({
|
||||
coverInstanceId: "",
|
||||
createTime: "",
|
||||
description: "",
|
||||
id: 0,
|
||||
images: [],
|
||||
instanceId: "",
|
||||
isRoot: 0,
|
||||
location: "",
|
||||
masterItemId: "",
|
||||
storyInstanceId: "",
|
||||
storyItemTime: "",
|
||||
subItems: [],
|
||||
title: "",
|
||||
updateTime: ""
|
||||
});
|
||||
setOpenAddItemModal(true);
|
||||
}} />
|
||||
<AddTimeLineItemModal
|
||||
visible={openAddItemModal}
|
||||
initialValues={currentItem}
|
||||
option={currentOption}
|
||||
onCancel={() => {
|
||||
setOpenAddItemModal(false);
|
||||
}}
|
||||
onOk={() => {
|
||||
setOpenAddItemModal(false);
|
||||
run();
|
||||
}}
|
||||
storyId={lineId}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
140
src/pages/story/index.css
Normal file
140
src/pages/story/index.css
Normal file
@@ -0,0 +1,140 @@
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
overflow-y: auto;
|
||||
max-height: 80vh;
|
||||
scroll-behavior: smooth;
|
||||
transition: transform 0.3s ease;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.timeline::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 主时间线容器 */
|
||||
.timeline-event-container {
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.timeline-event-container:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
/* 主时间线内容 */
|
||||
.timeline-event {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
border-left: 2px solid #f1f1f1;
|
||||
margin-bottom: 20px;
|
||||
background-color: transparent;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-event:hover {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
|
||||
.timeline-event.minimized {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 展开按钮样式 */
|
||||
.expand-trigger {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 10;
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.expand-trigger:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 子时间线动画容器 */
|
||||
.sub-timeline-wrapper {
|
||||
margin-top: 12px;
|
||||
animation: zoomIn 0.4s ease forwards;
|
||||
background-color: #fafafa;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #ddd;
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
from {
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-timeline {
|
||||
max-width: 100%;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.timeline-item.expanded {
|
||||
transform: scale(0.95);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sub-timeline {
|
||||
transform-origin: top left;
|
||||
animation: zoomIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
background-color: #f1f1f1;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 20px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
margin: 5px;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-content-text {
|
||||
padding-left: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.timeline-content h2 {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.timeline-content span {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
237
src/pages/story/index.tsx
Normal file
237
src/pages/story/index.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import { DownOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
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 useStyles from './style.style';
|
||||
|
||||
/*const RadioButton = Radio.Button;
|
||||
const RadioGroup = Radio.Group;*/
|
||||
const { Search } = Input;
|
||||
|
||||
const ListContent = ({
|
||||
data: { ownerId, updatedId, createTime, updateTime, status },
|
||||
}: {
|
||||
data: StoryType;
|
||||
}) => {
|
||||
const { styles } = useStyles();
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.listContentItem}>
|
||||
<span>创建人</span>
|
||||
<p>{ownerId}</p>
|
||||
</div>
|
||||
<div className={styles.listContentItem}>
|
||||
<span>最近更新人</span>
|
||||
<p>{updatedId}</p>
|
||||
</div>
|
||||
<div className={styles.listContentItem}>
|
||||
<span>节点数</span>
|
||||
<p>{111}</p>
|
||||
</div>
|
||||
<div className={styles.listContentItem}>
|
||||
<span>开始时间</span>
|
||||
<p>{createTime}</p>
|
||||
</div>
|
||||
<div className={styles.listContentItem}>
|
||||
<span>更新时间</span>
|
||||
<p>{updateTime}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const BasicList: FC = () => {
|
||||
const { styles } = useStyles();
|
||||
const [done, setDone] = useState<boolean>(false);
|
||||
const [open, setVisible] = useState<boolean>(false);
|
||||
const [current, setCurrent] = useState<Partial<StoryType> | undefined>(undefined);
|
||||
const {
|
||||
data: listData,
|
||||
loading,
|
||||
run,
|
||||
} = useRequest((storyName?: string) => {
|
||||
return queryTimelineList({
|
||||
count: 50,
|
||||
storyName,
|
||||
});
|
||||
});
|
||||
const { run: postRun } = useRequest(
|
||||
(method, params) => {
|
||||
if (method === 'remove') {
|
||||
return deleteStory(params);
|
||||
}
|
||||
if (method === 'update') {
|
||||
return updateStory(params);
|
||||
}
|
||||
return addStory(params);
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
run();
|
||||
},
|
||||
},
|
||||
);
|
||||
const list = listData || [];
|
||||
const paginationProps = {
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSize: 5,
|
||||
total: list.length,
|
||||
};
|
||||
const showEditModal = (item: StoryType) => {
|
||||
setVisible(true);
|
||||
setCurrent(item);
|
||||
};
|
||||
const deleteItem = (id: string) => {
|
||||
postRun('remove', {
|
||||
instanceId: id,
|
||||
});
|
||||
};
|
||||
const editAndDelete = (key: string | number, currentItem: StoryType) => {
|
||||
console.log(currentItem);
|
||||
if (key === 'edit') showEditModal(currentItem);
|
||||
else if (key === 'delete') {
|
||||
Modal.confirm({
|
||||
title: '删除故事',
|
||||
content: '确定删除该故事吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => deleteItem(currentItem.instanceId ?? ''),
|
||||
});
|
||||
}
|
||||
};
|
||||
const extraContent = (
|
||||
<div>
|
||||
{/*<RadioGroup defaultValue="all">
|
||||
<RadioButton value="all">全部</RadioButton>
|
||||
<RadioButton value="progress">进行中</RadioButton>
|
||||
<RadioButton value="waiting">等待中</RadioButton>
|
||||
</RadioGroup>*/}
|
||||
<Search
|
||||
className={styles.extraContentSearch}
|
||||
placeholder="请输入"
|
||||
onSearch={(value) => {
|
||||
run(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const MoreBtn: React.FC<{
|
||||
item: StoryType;
|
||||
}> = ({ item }) => (
|
||||
<Dropdown
|
||||
menu={{
|
||||
onClick: ({ key }) => editAndDelete(key, item),
|
||||
items: [
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑',
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<a>
|
||||
更多 <DownOutlined />
|
||||
</a>
|
||||
</Dropdown>
|
||||
);
|
||||
const handleDone = () => {
|
||||
setDone(false);
|
||||
setVisible(false);
|
||||
setCurrent({});
|
||||
};
|
||||
const handleSubmit = (values: StoryType) => {
|
||||
setDone(true);
|
||||
const method = current?.instanceId ? 'update' : 'add';
|
||||
postRun(method, { ...current, ...values });
|
||||
run();
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<PageContainer title={"Timeline"}>
|
||||
<div className={styles.standardList}>
|
||||
<Card
|
||||
className={styles.listCard}
|
||||
variant={undefined}
|
||||
style={{
|
||||
marginTop: 24,
|
||||
}}
|
||||
bodyStyle={{
|
||||
padding: '0 32px 40px 32px',
|
||||
}}
|
||||
extra={extraContent}
|
||||
>
|
||||
<List
|
||||
size="large"
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={paginationProps}
|
||||
dataSource={list}
|
||||
renderItem={(item: StoryType) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<a
|
||||
key="edit"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
showEditModal(item);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</a>,
|
||||
<MoreBtn key="more" item={item} />,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar src={item.logo} shape="square" size="large" />}
|
||||
title={
|
||||
<a
|
||||
onClick={() => {
|
||||
history.push(`/timeline/${item.instanceId}`);
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
}
|
||||
description={item.description}
|
||||
/>
|
||||
<ListContent data={item} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</PageContainer>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
添加
|
||||
</Button>
|
||||
<OperationModal
|
||||
done={done}
|
||||
open={open}
|
||||
current={current}
|
||||
onDone={handleDone}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default BasicList;
|
||||
95
src/pages/story/service.ts
Normal file
95
src/pages/story/service.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { request } from '@umijs/max';
|
||||
import { StoryItem, StoryType } from './data.d';
|
||||
import {CommonResponse} from "@/types/common";
|
||||
|
||||
type ParamsType = {
|
||||
count?: number;
|
||||
instanceId?: string;
|
||||
storyName?: string;
|
||||
} & Partial<StoryType>;
|
||||
|
||||
export async function queryTimelineList(
|
||||
params: ParamsType,
|
||||
): Promise<{ data: StoryType[] }> {
|
||||
return await request('/story/owner/test11', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
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[] } }> {
|
||||
return request('/story/add', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
...params,
|
||||
method: 'post',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateStory(params: ParamsType): Promise<{ data: { list: StoryType[] } }> {
|
||||
return await request(`/story/${params.instanceId}`, {
|
||||
method: 'PUT',
|
||||
data: {
|
||||
...params,
|
||||
method: 'put',
|
||||
},
|
||||
});
|
||||
}
|
||||
export async function queryStoryDetail(itemId: string): Promise<{ data: StoryType }> {
|
||||
return request(`/story/${itemId}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
export async function addStoryItem(params: FormData): Promise<any> {
|
||||
return request(`/story/item`, {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
requestType: 'form',
|
||||
getResponse: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function queryStoryItem(storyInstanceId: string): Promise<{ data: StoryItem[] }> {
|
||||
return request(`/story/item/list`, {
|
||||
method: 'GET',
|
||||
params: {
|
||||
storyInstanceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function queryStoryItemDetail(itemId: string): Promise<{ data: StoryItem }> {
|
||||
return request(`/story/item/${itemId}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
export async function countStoryItem(storyInstanceId: string): Promise<{ data: StoryItem }> {
|
||||
return request(`/story/item/count/${storyInstanceId}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
export async function queryStoryItemImages(itemId: string): Promise<{ data: string[] }> {
|
||||
return request(`/story/item/images/${itemId}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
export async function removeStoryItem(itemId: string): Promise<CommonResponse<string>> {
|
||||
return request(`/story/item/${itemId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchImage(imageInstanceId: string): Promise<any> {
|
||||
return request(`/file/image/${imageInstanceId}`, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
getResponse: true,
|
||||
});
|
||||
}
|
||||
228
src/pages/story/style.style.ts
Normal file
228
src/pages/story/style.style.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
const useStyles = createStyles(({ token }) => {
|
||||
return {
|
||||
standardList: {
|
||||
'.ant-card-head': { borderBottom: 'none' },
|
||||
'.ant-card-head-title': { padding: '24px 0', lineHeight: '32px' },
|
||||
'.ant-card-extra': { padding: '24px 0' },
|
||||
'.ant-list-pagination': { marginTop: '24px', textAlign: 'right' },
|
||||
'.ant-avatar-lg': { width: '48px', height: '48px', lineHeight: '48px' },
|
||||
[`@media screen and (max-width: ${token.screenXS}px)`]: {
|
||||
'.ant-list-item-content': {
|
||||
display: 'block',
|
||||
flex: 'none',
|
||||
width: '100%',
|
||||
},
|
||||
'.ant-list-item-action': {
|
||||
marginLeft: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
headerInfo: {
|
||||
position: 'relative',
|
||||
textAlign: 'center',
|
||||
'& > span': {
|
||||
display: 'inline-block',
|
||||
marginBottom: '4px',
|
||||
color: token.colorTextSecondary,
|
||||
fontSize: token.fontSize,
|
||||
lineHeight: '22px',
|
||||
},
|
||||
'& > p': {
|
||||
margin: '0',
|
||||
color: token.colorTextHeading,
|
||||
fontSize: '24px',
|
||||
lineHeight: '32px',
|
||||
},
|
||||
'& > em': {
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
right: '0',
|
||||
width: '1px',
|
||||
height: '56px',
|
||||
backgroundColor: token.colorSplit,
|
||||
},
|
||||
[`@media screen and (max-width: ${token.screenSM}px)`]: {
|
||||
marginBottom: '16px',
|
||||
'& > em': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
listContent: {
|
||||
fontSize: '0',
|
||||
[`@media screen and (max-width: ${token.screenXS}px)`]: {
|
||||
marginLeft: '0',
|
||||
'& > div': {
|
||||
marginLeft: '0',
|
||||
},
|
||||
},
|
||||
[`@media screen and (max-width: ${token.screenMD}px)`]: {
|
||||
'& > div': {
|
||||
display: 'block',
|
||||
},
|
||||
'& > div:last-child': {
|
||||
top: '0',
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
[`@media screen and (max-width: ${token.screenLG}px) and (min-width: @screen-md)`]: {
|
||||
'& > div': {
|
||||
display: 'block',
|
||||
},
|
||||
'& > div:last-child': {
|
||||
top: '0',
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
[`@media screen and (max-width: ${token.screenXL}px)`]: {
|
||||
'& > div': {
|
||||
marginLeft: '24px',
|
||||
},
|
||||
'& > div:last-child': {
|
||||
top: '0',
|
||||
},
|
||||
},
|
||||
'@media screen and (max-width: 1400px)': {
|
||||
textAlign: 'right',
|
||||
'& > div:last-child': {
|
||||
top: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
listContentItem: {
|
||||
display: 'inline-block',
|
||||
marginLeft: '40px',
|
||||
color: token.colorTextSecondary,
|
||||
fontSize: token.fontSize,
|
||||
verticalAlign: 'middle',
|
||||
'> span': { lineHeight: '20px' },
|
||||
'> p': { marginTop: '4px', marginBottom: '0', lineHeight: '22px' },
|
||||
},
|
||||
extraContentSearch: {
|
||||
width: '272px',
|
||||
marginLeft: '16px',
|
||||
[`@media screen and (max-width: ${token.screenSM}px)`]: {
|
||||
width: '100%',
|
||||
marginLeft: '0',
|
||||
},
|
||||
},
|
||||
listCard: {
|
||||
[`@media screen and (max-width: ${token.screenXS}px)`]: {
|
||||
'.ant-card-head-title': {
|
||||
overflow: 'open',
|
||||
},
|
||||
},
|
||||
[`@media screen and (max-width: ${token.screenMD}px)`]: {
|
||||
'.ant-radio-group': {
|
||||
display: 'block',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
},
|
||||
},
|
||||
standardListForm: {
|
||||
'.ant-form-item': {
|
||||
marginBottom: '12px',
|
||||
'&:last-child': {
|
||||
marginBottom: '32px',
|
||||
paddingTop: '4px',
|
||||
},
|
||||
},
|
||||
},
|
||||
formResult: {
|
||||
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,
|
||||
},
|
||||
storyList: {
|
||||
padding: '24px',
|
||||
},
|
||||
toolbar: {
|
||||
marginBottom: '24px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
storyDetail: {
|
||||
padding: '12px',
|
||||
},
|
||||
pageHeader: {
|
||||
padding: 0,
|
||||
marginBottom: '24px',
|
||||
},
|
||||
card: {
|
||||
maxWidth: '800px',
|
||||
margin: '0 auto',
|
||||
},
|
||||
date: {
|
||||
fontSize: '16px',
|
||||
color: token.colorTextSecondary,
|
||||
marginBottom: '20px',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
cover: {
|
||||
width: '100%',
|
||||
height: '300px',
|
||||
overflow: 'hidden',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '20px',
|
||||
img: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
},
|
||||
},
|
||||
description: {
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.8',
|
||||
color: token.colorText,
|
||||
whiteSpace: 'pre-wrap',
|
||||
},
|
||||
subItems: {
|
||||
marginTop: '20px',
|
||||
},
|
||||
subItem: {
|
||||
display: 'flex',
|
||||
marginBottom: '15px',
|
||||
padding: '10px',
|
||||
backgroundColor: token.colorFillAlter,
|
||||
borderRadius: '4px',
|
||||
'&:last-child': {
|
||||
marginBottom: 0,
|
||||
},
|
||||
},
|
||||
subItemDate: {
|
||||
fontWeight: 'bold',
|
||||
minWidth: '120px',
|
||||
color: token.colorTextSecondary,
|
||||
},
|
||||
subItemContent: {
|
||||
flex: 1,
|
||||
color: token.colorText,
|
||||
},
|
||||
timelineItem: {
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: token.boxShadow,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default useStyles;
|
||||
6
src/pages/story/utils/utils.style.ts
Normal file
6
src/pages/story/utils/utils.style.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
const useStyles = createStyles(() => {
|
||||
return {};
|
||||
});
|
||||
export default useStyles;
|
||||
Reference in New Issue
Block a user