Story排版修改
This commit is contained in:
parent
63ae33288d
commit
141e8d9818
|
@ -73,10 +73,10 @@ export default [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timeline',
|
name: '故事',
|
||||||
icon: 'smile',
|
icon: 'smile',
|
||||||
path: '/timeline',
|
path: '/story',
|
||||||
component: './list/basic-list',
|
component: './story',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '图库',
|
name: '图库',
|
||||||
|
@ -86,7 +86,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/timeline/:id',
|
path: '/timeline/:id',
|
||||||
component: './list/basic-list/detail',
|
component: './story/detail',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
@ -122,64 +122,6 @@ export default [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/list',
|
|
||||||
icon: 'table',
|
|
||||||
name: 'list',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/list/search',
|
|
||||||
name: 'search-list',
|
|
||||||
component: './list/search',
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/list/search',
|
|
||||||
redirect: '/list/search/articles',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'articles',
|
|
||||||
icon: 'smile',
|
|
||||||
path: '/list/search/articles',
|
|
||||||
component: './list/search/articles',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'projects',
|
|
||||||
icon: 'smile',
|
|
||||||
path: '/list/search/projects',
|
|
||||||
component: './list/search/projects',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'applications',
|
|
||||||
icon: 'smile',
|
|
||||||
path: '/list/search/applications',
|
|
||||||
component: './list/search/applications',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/list',
|
|
||||||
redirect: '/list/table-list',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'table-list',
|
|
||||||
icon: 'smile',
|
|
||||||
path: '/list/table-list',
|
|
||||||
component: './table-list',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'basic-list',
|
|
||||||
icon: 'smile',
|
|
||||||
path: '/list/basic-list',
|
|
||||||
component: './list/basic-list',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'card-list',
|
|
||||||
icon: 'smile',
|
|
||||||
path: '/list/card-list',
|
|
||||||
component: './list/card-list',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
name: 'profile',
|
name: 'profile',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { fetchImage } from '@/pages/list/basic-list/service';
|
import { fetchImage } from '@/pages/story/service';
|
||||||
import { useRequest } from '@umijs/max';
|
import { useRequest } from '@umijs/max';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
@ -18,6 +18,8 @@ const useFetchImageUrl = (imageInstanceId: string) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (response) {
|
if (response) {
|
||||||
setImageUrl(URL.createObjectURL(response));
|
setImageUrl(URL.createObjectURL(response));
|
||||||
|
} else {
|
||||||
|
setImageUrl("error");
|
||||||
}
|
}
|
||||||
}, [response]);
|
}, [response]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -35,12 +35,11 @@ const TimelineImage: React.FC<Props> = (props) => {
|
||||||
const [previewVisible, setPreviewVisible] = useState(false);
|
const [previewVisible, setPreviewVisible] = useState(false);
|
||||||
|
|
||||||
// 构建预览列表
|
// 构建预览列表
|
||||||
const previewList = imageList.map(item => ({
|
imageList.map(item => ({
|
||||||
src: item.instanceId,
|
src: item.instanceId,
|
||||||
title: item.imageName
|
title: item.imageName
|
||||||
}));
|
}));
|
||||||
|
// 预览配置
|
||||||
// 预览配置
|
|
||||||
const previewConfig = {
|
const previewConfig = {
|
||||||
visible: previewVisible,
|
visible: previewVisible,
|
||||||
onVisibleChange: (visible: boolean) => setPreviewVisible(visible),
|
onVisibleChange: (visible: boolean) => setPreviewVisible(visible),
|
||||||
|
|
|
@ -64,4 +64,13 @@ export default {
|
||||||
'pages.searchTable.tenThousand': '万',
|
'pages.searchTable.tenThousand': '万',
|
||||||
'pages.searchTable.batchDeletion': '批量删除',
|
'pages.searchTable.batchDeletion': '批量删除',
|
||||||
'pages.searchTable.batchApproval': '批量审批',
|
'pages.searchTable.batchApproval': '批量审批',
|
||||||
|
"story.deleteSuccess": "删除成功",
|
||||||
|
"story.deleteFailed": "删除失败",
|
||||||
|
"story.deleteConfirm": "确认删除",
|
||||||
|
"story.deleteConfirmDescription": "确定要删除这个故事项吗?此操作不可撤销。",
|
||||||
|
"story.yes": "是",
|
||||||
|
"story.no": "否",
|
||||||
|
"story.edit": "编辑",
|
||||||
|
"story.addSubItem": "添加子项",
|
||||||
|
"story.delete": "删除"
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,133 +0,0 @@
|
||||||
import { Badge, Col, Row, Timeline } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import TimelineImage from '@/components/TimelineImage';
|
|
||||||
import TimelineItemDrawer from '@/pages/list/basic-list/components/TimelineItemDrawer';
|
|
||||||
import { StoryItem, TimelineEvent } from '@/pages/list/basic-list/data';
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
interface TimelineItemProps {
|
|
||||||
event: StoryItem;
|
|
||||||
onUpdate?: (updatedEvent: TimelineEvent) => void; // 数据更新回调
|
|
||||||
}
|
|
||||||
|
|
||||||
const TimelineItem = ({ event: initialEvent, onUpdate }: TimelineItemProps) => {
|
|
||||||
const [openMainDrawer, setOpenMainDrawer] = React.useState(false);
|
|
||||||
const [expanded, setExpanded] = React.useState(false); // 控制子项展开状态
|
|
||||||
|
|
||||||
const showMainDrawer = () => {
|
|
||||||
setOpenMainDrawer(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`timeline-item ${expanded ? 'expanded' : ''}`}>
|
|
||||||
{/* 主时间线容器 */}
|
|
||||||
<div className={`timeline-event-container ${expanded ? 'expanded' : ''}`}>
|
|
||||||
<div className={`timeline-event ${expanded ? 'minimized' : ''}`}>
|
|
||||||
<Row gutter={24} align="middle">
|
|
||||||
<Col span={6}>
|
|
||||||
<TimelineImage
|
|
||||||
width={200}
|
|
||||||
height={200}
|
|
||||||
title={initialEvent.title}
|
|
||||||
imageInstanceId={initialEvent.coverInstanceId}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col span={16} offset={2}>
|
|
||||||
<div className="timeline-content-text" onClick={showMainDrawer}>
|
|
||||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>{initialEvent.title}</h2>
|
|
||||||
<p style={{ marginBottom: '0.5rem' }}>{initialEvent.description}</p>
|
|
||||||
<div style={{ color: '#666', lineHeight: '1.6' }}>
|
|
||||||
<span>故事时间:{initialEvent.storyItemTime}</span>
|
|
||||||
<br />
|
|
||||||
<span>创建时间:{initialEvent.createTime}</span>
|
|
||||||
<br />
|
|
||||||
<span>更新时间:{initialEvent.updateTime}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{/* 子时间点徽章 */}
|
|
||||||
{initialEvent.subItems && initialEvent.subItems.length > 0 && (
|
|
||||||
<Badge
|
|
||||||
count={`${initialEvent.subItems.length} 个子时间点`}
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#1890ff',
|
|
||||||
position: 'absolute',
|
|
||||||
top: 10,
|
|
||||||
right: 10,
|
|
||||||
transform: 'translate(50%, -50%)', // 修复超出边界
|
|
||||||
padding: '0 6px',
|
|
||||||
borderRadius: 4,
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 12,
|
|
||||||
height: 20,
|
|
||||||
lineHeight: '20px',
|
|
||||||
textAlign: 'center',
|
|
||||||
minWidth: 60,
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setExpanded(!expanded);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 子时间线列表 */}
|
|
||||||
{initialEvent.subItems && initialEvent.subItems.length > 0 && expanded && (
|
|
||||||
<div className="sub-timeline-wrapper">
|
|
||||||
<Timeline
|
|
||||||
mode="left"
|
|
||||||
items={initialEvent.subItems.map((sub) => ({
|
|
||||||
children: (
|
|
||||||
<div
|
|
||||||
className="sub-timeline-item"
|
|
||||||
style={{
|
|
||||||
fontSize: '12px',
|
|
||||||
padding: '8px 0',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'background-color 0.2s',
|
|
||||||
':hover': { backgroundColor: '#f5f5f5' }, // 添加 hover 效果
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Row>
|
|
||||||
<Col span={4}>
|
|
||||||
<TimelineImage title={sub.title} imageInstanceId={sub.coverInstanceId} />
|
|
||||||
</Col>
|
|
||||||
<Col span={18} offset={2}>
|
|
||||||
<div className="timeline-content-text" onClick={showMainDrawer}>
|
|
||||||
<h2 style={{ fontSize: '1.2rem', fontWeight: 'normal' }}>{sub.title}</h2>
|
|
||||||
<p style={{ marginBottom: '0.5rem' }}>{sub.description}</p>
|
|
||||||
<div style={{ color: '#999', lineHeight: '1.6' }}>
|
|
||||||
<span>故事时间:{sub.storyItemTime}</span>
|
|
||||||
<br />
|
|
||||||
<span>创建时间:{sub.createTime}</span>
|
|
||||||
<br />
|
|
||||||
<span>更新时间:{sub.updateTime}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 主时间点详情抽屉 */}
|
|
||||||
<TimelineItemDrawer
|
|
||||||
storyItem={initialEvent}
|
|
||||||
open={openMainDrawer}
|
|
||||||
setOpen={setOpenMainDrawer}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TimelineItem;
|
|
|
@ -1,246 +0,0 @@
|
||||||
import AddTimeLineItemModal from '@/pages/list/basic-list/components/AddTimeLineItemModal';
|
|
||||||
import TimelineItem from '@/pages/list/basic-list/components/TimelineItem/TimelineItem';
|
|
||||||
import {StoryItem, TimelineEvent} from '@/pages/list/basic-list/data';
|
|
||||||
import {queryStoryItem} from '@/pages/list/basic-list/service';
|
|
||||||
import {PageContainer} from '@ant-design/pro-components';
|
|
||||||
import {useRequest} from '@umijs/max';
|
|
||||||
import {FloatButton, Spin, Timeline} from 'antd';
|
|
||||||
import {debounce} from 'lodash';
|
|
||||||
import React, {useEffect, useRef, useState} from 'react';
|
|
||||||
import {useParams} from 'react-router';
|
|
||||||
import './index.css';
|
|
||||||
import {handleStoryItemList} from "@/utils";
|
|
||||||
|
|
||||||
interface TimelineItemProps {
|
|
||||||
children: React.ReactNode; // 修正:使用 ReactNode 更通用
|
|
||||||
// label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX = 5;
|
|
||||||
const Index = () => {
|
|
||||||
const {id: lineId} = useParams<{ id: string }>();
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [items, setItems] = useState<TimelineItemProps[]>([]);
|
|
||||||
const lastScrollTopRef = useRef<number>(0);
|
|
||||||
const isScrollingUpRef = useRef<boolean>(false);
|
|
||||||
const isScrollingDownRef = useRef<boolean>(false); // 是否向下滚动
|
|
||||||
const isFetchingRef = useRef(false); // 防止重复请求
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [hasMoreNew, setHasMoreNew] = useState(true);
|
|
||||||
const [hasMoreOld, setHasMoreOld] = useState(true);
|
|
||||||
const [loadCount, setLoadCount] = useState(0);
|
|
||||||
const [openAddItemModal, setOpenAddItemModal] = useState(false);
|
|
||||||
|
|
||||||
const {data: storyItemList, run} = useRequest(() => {
|
|
||||||
return queryStoryItem(lineId ?? '');
|
|
||||||
}, {
|
|
||||||
manual: true,
|
|
||||||
});
|
|
||||||
// 初始化加载数据
|
|
||||||
useEffect(() => {
|
|
||||||
run();
|
|
||||||
}, [lineId]);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!storyItemList?.length) return;
|
|
||||||
let timelineItems = handleStoryItemList(storyItemList);
|
|
||||||
// 转换为 Timeline 组件需要的格式
|
|
||||||
const formattedItems = timelineItems.map((item: StoryItem) => ({
|
|
||||||
children: <TimelineItem event={item} onUpdate={updateTimelineItem}/>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setItems(formattedItems);
|
|
||||||
}, [storyItemList]);
|
|
||||||
const handleScroll = () => {
|
|
||||||
const container = containerRef.current;
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const {scrollTop} = container;
|
|
||||||
|
|
||||||
// 判断滚动方向
|
|
||||||
if (scrollTop < lastScrollTopRef.current) {
|
|
||||||
isScrollingUpRef.current = true;
|
|
||||||
isScrollingDownRef.current = false;
|
|
||||||
loadMoreOldData();
|
|
||||||
} else if (scrollTop > lastScrollTopRef.current) {
|
|
||||||
isScrollingUpRef.current = false;
|
|
||||||
isScrollingDownRef.current = true;
|
|
||||||
loadMoreNewData();
|
|
||||||
}
|
|
||||||
|
|
||||||
lastScrollTopRef.current = scrollTop;
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadMoreOldData = async () => {
|
|
||||||
if (loadCount >= MAX) {
|
|
||||||
setHasMoreOld(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (loading || isFetchingRef.current) return;
|
|
||||||
isFetchingRef.current = true;
|
|
||||||
setLoading(true);
|
|
||||||
console.log('loadMoreData');
|
|
||||||
|
|
||||||
const container = containerRef.current;
|
|
||||||
|
|
||||||
let scrollTopBeforeLoad = 0;
|
|
||||||
let beforeScrollHeight = 0;
|
|
||||||
|
|
||||||
if (isScrollingUpRef.current && container) {
|
|
||||||
scrollTopBeforeLoad = container.scrollTop;
|
|
||||||
beforeScrollHeight = container.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const newItems = [
|
|
||||||
{
|
|
||||||
title: 'New Event Top 1',
|
|
||||||
description: 'Description from top',
|
|
||||||
date: new Date().toDateString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'New Event Top 2',
|
|
||||||
description: 'Description from top',
|
|
||||||
date: new Date().toDateString(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const newTimelineItems = newItems.map((item, index) => ({
|
|
||||||
children: <TimelineItem event={{...item, id: items.length + index}}/>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setItems((prev) => [...newTimelineItems, ...prev]);
|
|
||||||
setLoadCount((prev) => prev + 1);
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (container && isScrollingUpRef.current) {
|
|
||||||
const afterScrollHeight = container.scrollHeight;
|
|
||||||
const newContentHeight = afterScrollHeight - beforeScrollHeight;
|
|
||||||
|
|
||||||
container.scrollTop = scrollTopBeforeLoad + newContentHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
isFetchingRef.current = false;
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
const loadMoreNewData = async () => {
|
|
||||||
if (loadCount >= MAX) {
|
|
||||||
setHasMoreNew(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (loading || isFetchingRef.current) return;
|
|
||||||
isFetchingRef.current = true;
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const container = containerRef.current;
|
|
||||||
|
|
||||||
let scrollTopBeforeLoad = 0;
|
|
||||||
|
|
||||||
if (isScrollingDownRef.current && container) {
|
|
||||||
scrollTopBeforeLoad = container.scrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const newItems = [
|
|
||||||
{
|
|
||||||
title: 'New Event Bottom 1',
|
|
||||||
description: 'New data from bottom',
|
|
||||||
date: new Date().toDateString(),
|
|
||||||
subItems: [
|
|
||||||
{
|
|
||||||
title: 'New sub1 Event Bottom 1.1',
|
|
||||||
description: 'New data from bottom',
|
|
||||||
date: new Date().toDateString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'New Event Bottom 1.1',
|
|
||||||
description: 'New data from bottom',
|
|
||||||
date: new Date().toDateString(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'New Event Bottom 2',
|
|
||||||
description: 'New data from bottom',
|
|
||||||
date: new Date().toDateString(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const newTimelineItems = newItems.map((item, index) => ({
|
|
||||||
children: <TimelineItem event={{...item, id: items.length + index}}/>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setItems((prev) => [...prev, ...newTimelineItems]);
|
|
||||||
setLoadCount((prev) => prev + 1);
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (container && isScrollingDownRef.current) {
|
|
||||||
// 不需要调整位置,因为是追加在最后
|
|
||||||
}
|
|
||||||
|
|
||||||
isFetchingRef.current = false;
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedHandleScroll = debounce(handleScroll, 500);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const container = containerRef.current;
|
|
||||||
// if (container) {
|
|
||||||
// container.addEventListener('scroll', debouncedHandleScroll);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return () => {
|
|
||||||
// if (container) {
|
|
||||||
// container.removeEventListener('scroll', debouncedHandleScroll);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
/*useEffect(() => {
|
|
||||||
const container = containerRef.current;
|
|
||||||
if (container) {
|
|
||||||
container.scrollTop = container.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [items]);
|
|
||||||
*/
|
|
||||||
const updateTimelineItem = (updatedItem: TimelineEvent) => {
|
|
||||||
const storageKey = `timelineItems_${updatedItem.id}`;
|
|
||||||
localStorage.setItem(storageKey, JSON.stringify(updatedItem));
|
|
||||||
|
|
||||||
// 刷新整个时间线
|
|
||||||
const storedItems = localStorage.getItem(`timelineItems_${lineId}`);
|
|
||||||
const items = storedItems ? JSON.parse(storedItems) : [];
|
|
||||||
|
|
||||||
setItems(items.map((item) => (item.id === updatedItem.id ? updatedItem : item)));
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<PageContainer title={lineId}>
|
|
||||||
<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={() => setOpenAddItemModal(true)}/>
|
|
||||||
<AddTimeLineItemModal
|
|
||||||
visible={openAddItemModal}
|
|
||||||
onCancel={() => {
|
|
||||||
setOpenAddItemModal(false);
|
|
||||||
}}
|
|
||||||
onOk={() => {
|
|
||||||
setOpenAddItemModal(false);
|
|
||||||
run();
|
|
||||||
}}
|
|
||||||
lineId={lineId}
|
|
||||||
isRoot={true}
|
|
||||||
/>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Index;
|
|
|
@ -1,119 +0,0 @@
|
||||||
import type { Request, Response } from 'express';
|
|
||||||
import type { CardListItemDataType } 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): CardListItemDataType[] {
|
|
||||||
const list = [];
|
|
||||||
for (let i = 0; i < count; i += 1) {
|
|
||||||
list.push({
|
|
||||||
id: `fake-list-${i}`,
|
|
||||||
owner: user[i % 10],
|
|
||||||
title: titles[i % 8],
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFakeList(req: Request, res: Response) {
|
|
||||||
const params = req.query as any;
|
|
||||||
|
|
||||||
const count = Number(params.count) * 1 || 20;
|
|
||||||
|
|
||||||
const result = fakeList(count);
|
|
||||||
return res.json({
|
|
||||||
data: {
|
|
||||||
list: result,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
'GET /api/card_fake_list': getFakeList,
|
|
||||||
};
|
|
|
@ -1,29 +0,0 @@
|
||||||
export type Member = {
|
|
||||||
avatar: string;
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CardListItemDataType = {
|
|
||||||
id: string;
|
|
||||||
owner: 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[];
|
|
||||||
};
|
|
|
@ -1,104 +0,0 @@
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
|
||||||
import { useRequest } from '@umijs/max';
|
|
||||||
import { Button, Card, List, Typography } from 'antd';
|
|
||||||
import type { CardListItemDataType } from './data.d';
|
|
||||||
import { queryFakeList } from './service';
|
|
||||||
import useStyles from './style.style';
|
|
||||||
const { Paragraph } = Typography;
|
|
||||||
const CardList = () => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const { data, loading } = useRequest(() => {
|
|
||||||
return queryFakeList({
|
|
||||||
count: 8,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const list = data?.list || [];
|
|
||||||
const content = (
|
|
||||||
<div className={styles.pageHeaderContent}>
|
|
||||||
<p>
|
|
||||||
段落示意:蚂蚁金服务设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,
|
|
||||||
提供跨越设计与开发的体验解决方案。
|
|
||||||
</p>
|
|
||||||
<div className={styles.contentLink}>
|
|
||||||
<a>
|
|
||||||
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/MjEImQtenlyueSmVEfUD.svg" />{' '}
|
|
||||||
快速开始
|
|
||||||
</a>
|
|
||||||
<a>
|
|
||||||
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/NbuDUAuBlIApFuDvWiND.svg" />{' '}
|
|
||||||
产品简介
|
|
||||||
</a>
|
|
||||||
<a>
|
|
||||||
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/ohOEPSYdDTNnyMbGuyLb.svg" />{' '}
|
|
||||||
产品文档
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
const extraContent = (
|
|
||||||
<div className={styles.extraImg}>
|
|
||||||
<img
|
|
||||||
alt="这是一个标题"
|
|
||||||
src="https://gw.alipayobjects.com/zos/rmsportal/RzwpdLnhmvDJToTdfDPe.png"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
const nullData: Partial<CardListItemDataType> = {};
|
|
||||||
return (
|
|
||||||
<PageContainer content={content} extraContent={extraContent}>
|
|
||||||
<div className={styles.cardList}>
|
|
||||||
<List<Partial<CardListItemDataType>>
|
|
||||||
rowKey="id"
|
|
||||||
loading={loading}
|
|
||||||
grid={{
|
|
||||||
gutter: 16,
|
|
||||||
xs: 1,
|
|
||||||
sm: 2,
|
|
||||||
md: 3,
|
|
||||||
lg: 3,
|
|
||||||
xl: 4,
|
|
||||||
xxl: 4,
|
|
||||||
}}
|
|
||||||
dataSource={[nullData, ...list]}
|
|
||||||
renderItem={(item) => {
|
|
||||||
if (item && item.id) {
|
|
||||||
return (
|
|
||||||
<List.Item key={item.id}>
|
|
||||||
<Card
|
|
||||||
hoverable
|
|
||||||
className={styles.card}
|
|
||||||
actions={[<a key="option1">操作一</a>, <a key="option2">操作二</a>]}
|
|
||||||
>
|
|
||||||
<Card.Meta
|
|
||||||
avatar={<img alt="" className={styles.cardAvatar} src={item.avatar} />}
|
|
||||||
title={<a>{item.title}</a>}
|
|
||||||
description={
|
|
||||||
<Paragraph
|
|
||||||
className={styles.item}
|
|
||||||
ellipsis={{
|
|
||||||
rows: 3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.description}
|
|
||||||
</Paragraph>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</List.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<List.Item>
|
|
||||||
<Button type="dashed" className={styles.newButton}>
|
|
||||||
<PlusOutlined /> 新增产品
|
|
||||||
</Button>
|
|
||||||
</List.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default CardList;
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import type { CardListItemDataType } from './data.d';
|
|
||||||
|
|
||||||
export async function queryFakeList(params: {
|
|
||||||
count: number;
|
|
||||||
}): Promise<{ data: { list: CardListItemDataType[] } }> {
|
|
||||||
return request('/api/card_fake_list', {
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
card: {
|
|
||||||
'.ant-card-meta-title': {
|
|
||||||
marginBottom: '12px',
|
|
||||||
'& > a': {
|
|
||||||
display: 'inline-block',
|
|
||||||
maxWidth: '100%',
|
|
||||||
color: token.colorTextHeading,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.ant-card-body:hover': {
|
|
||||||
'.ant-card-meta-title > a': {
|
|
||||||
color: token.colorPrimary,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
item: {
|
|
||||||
height: '64px',
|
|
||||||
},
|
|
||||||
cardList: {
|
|
||||||
'.ant-list .ant-list-item-content-single': { maxWidth: '100%' },
|
|
||||||
},
|
|
||||||
extraImg: {
|
|
||||||
width: '155px',
|
|
||||||
marginTop: '-20px',
|
|
||||||
textAlign: 'center',
|
|
||||||
img: { width: '100%' },
|
|
||||||
[`@media screen and (max-width: ${token.screenMD}px)`]: {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
newButton: {
|
|
||||||
width: '100%',
|
|
||||||
height: '201px',
|
|
||||||
color: token.colorTextSecondary,
|
|
||||||
backgroundColor: token.colorBgContainer,
|
|
||||||
borderColor: token.colorBorder,
|
|
||||||
},
|
|
||||||
cardAvatar: {
|
|
||||||
width: '48px',
|
|
||||||
height: '48px',
|
|
||||||
borderRadius: '48px',
|
|
||||||
},
|
|
||||||
cardDescription: {
|
|
||||||
overflow: 'hidden',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
wordBreak: 'break-all',
|
|
||||||
},
|
|
||||||
pageHeaderContent: {
|
|
||||||
position: 'relative',
|
|
||||||
[`@media screen and (max-width: ${token.screenSM}px)`]: {
|
|
||||||
paddingBottom: '30px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
contentLink: {
|
|
||||||
marginTop: '16px',
|
|
||||||
a: {
|
|
||||||
marginRight: '32px',
|
|
||||||
img: {
|
|
||||||
width: '24px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
img: { marginRight: '8px', verticalAlign: 'middle' },
|
|
||||||
[`@media screen and (max-width: ${token.screenLG}px)`]: {
|
|
||||||
a: {
|
|
||||||
marginRight: '16px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[`@media screen and (max-width: ${token.screenSM}px)`]: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: '-4px',
|
|
||||||
left: '0',
|
|
||||||
width: '1000px',
|
|
||||||
a: {
|
|
||||||
marginRight: '16px',
|
|
||||||
},
|
|
||||||
img: {
|
|
||||||
marginRight: '4px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(() => {
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
export default useStyles;
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { DefaultOptionType } from 'antd/es/select';
|
|
||||||
|
|
||||||
export const categoryOptions: DefaultOptionType[] = Array.from({ length: 12 }).map((_, index) => ({
|
|
||||||
value: `cat${index + 1}`,
|
|
||||||
label: `类目${index + 1}`,
|
|
||||||
}));
|
|
|
@ -1,118 +0,0 @@
|
||||||
import type { Request, Response } from 'express';
|
|
||||||
import type { ListItemDataType } 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): ListItemDataType[] {
|
|
||||||
const list = [];
|
|
||||||
for (let i = 0; i < count; i += 1) {
|
|
||||||
list.push({
|
|
||||||
id: `fake-list-${i}`,
|
|
||||||
owner: user[i % 10],
|
|
||||||
title: titles[i % 8],
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFakeList(req: Request, res: Response) {
|
|
||||||
const params: any = req.query;
|
|
||||||
|
|
||||||
const count = params.count * 1 || 20;
|
|
||||||
|
|
||||||
const result = fakeList(count);
|
|
||||||
return res.json({
|
|
||||||
data: {
|
|
||||||
list: result,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
'GET /api/fake_list': getFakeList,
|
|
||||||
};
|
|
|
@ -1,60 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
standardFormRow: {
|
|
||||||
display: 'flex',
|
|
||||||
marginBottom: '16px',
|
|
||||||
paddingBottom: '16px',
|
|
||||||
borderBottom: `1px dashed ${token.colorSplit}`,
|
|
||||||
'.ant-form-item, .ant-legacy-form-item': { marginRight: '24px' },
|
|
||||||
'.ant-form-item-label, .ant-legacy-form-item-label': {
|
|
||||||
label: {
|
|
||||||
marginRight: '0',
|
|
||||||
color: token.colorText,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.ant-form-item-label, .ant-legacy-form-item-label, .ant-form-item-control, .ant-legacy-form-item-control':
|
|
||||||
{ padding: '0', lineHeight: '32px' },
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
flex: '0 0 auto',
|
|
||||||
marginRight: '24px',
|
|
||||||
color: token.colorTextHeading,
|
|
||||||
fontSize: token.fontSize,
|
|
||||||
textAlign: 'right',
|
|
||||||
'& > span': {
|
|
||||||
display: 'inline-block',
|
|
||||||
height: '32px',
|
|
||||||
lineHeight: '32px',
|
|
||||||
'&::after': {
|
|
||||||
content: "':'",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: '1 1 0',
|
|
||||||
'.ant-form-item, .ant-legacy-form-item': {
|
|
||||||
'&:last-child': {
|
|
||||||
marginRight: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
standardFormRowLast: {
|
|
||||||
marginBottom: '0',
|
|
||||||
paddingBottom: '0',
|
|
||||||
border: 'none',
|
|
||||||
},
|
|
||||||
standardFormRowBlock: {
|
|
||||||
'.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
|
|
||||||
{ display: 'block' },
|
|
||||||
},
|
|
||||||
standardFormRowGrid: {
|
|
||||||
'.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
|
|
||||||
{ display: 'block' },
|
|
||||||
'.ant-form-item-label, .ant-legacy-form-item-label': { float: 'left' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,37 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
|
||||||
import useStyles from './index.style';
|
|
||||||
type StandardFormRowProps = {
|
|
||||||
title?: string;
|
|
||||||
last?: boolean;
|
|
||||||
block?: boolean;
|
|
||||||
grid?: boolean;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
};
|
|
||||||
const StandardFormRow: React.FC<StandardFormRowProps> = ({
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
last,
|
|
||||||
block,
|
|
||||||
grid,
|
|
||||||
...rest
|
|
||||||
}) => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const cls = classNames(styles.standardFormRow, {
|
|
||||||
[styles.standardFormRowBlock]: block,
|
|
||||||
[styles.standardFormRowLast]: last,
|
|
||||||
[styles.standardFormRowGrid]: grid,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className={cls} {...rest}>
|
|
||||||
{title && (
|
|
||||||
<div className={styles.label}>
|
|
||||||
<span>{title}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.content}>{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default StandardFormRow;
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
tagSelect: {
|
|
||||||
position: 'relative',
|
|
||||||
maxHeight: '32px',
|
|
||||||
marginLeft: '-8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
lineHeight: '32px',
|
|
||||||
transition: 'all 0.3s',
|
|
||||||
userSelect: 'none',
|
|
||||||
'.ant-tag': {
|
|
||||||
marginRight: '24px',
|
|
||||||
padding: '0 8px',
|
|
||||||
fontSize: token.fontSize,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
trigger: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '0',
|
|
||||||
right: '0',
|
|
||||||
'span.anticon': { fontSize: '12px' },
|
|
||||||
},
|
|
||||||
expanded: {
|
|
||||||
maxHeight: '200px',
|
|
||||||
transition: 'all 0.3s',
|
|
||||||
},
|
|
||||||
hasExpandTag: {
|
|
||||||
paddingRight: '50px',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,136 +0,0 @@
|
||||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
|
||||||
import { Tag } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useMergedState } from 'rc-util';
|
|
||||||
import React, { FC, useState } from 'react';
|
|
||||||
import useStyles from './index.style';
|
|
||||||
const { CheckableTag } = Tag;
|
|
||||||
export interface TagSelectOptionProps {
|
|
||||||
value: string | number;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
checked?: boolean;
|
|
||||||
onChange?: (value: string | number, state: boolean) => void;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
const TagSelectOption: React.FC<TagSelectOptionProps> & {
|
|
||||||
isTagSelectOption: boolean;
|
|
||||||
} = ({ children, checked, onChange, value }) => (
|
|
||||||
<CheckableTag
|
|
||||||
checked={!!checked}
|
|
||||||
key={value}
|
|
||||||
onChange={(state) => onChange && onChange(value, state)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CheckableTag>
|
|
||||||
);
|
|
||||||
|
|
||||||
TagSelectOption.isTagSelectOption = true;
|
|
||||||
|
|
||||||
type TagSelectOptionElement = React.ReactElement<TagSelectOptionProps, typeof TagSelectOption>;
|
|
||||||
|
|
||||||
export interface TagSelectProps {
|
|
||||||
onChange?: (value: (string | number)[]) => void;
|
|
||||||
expandable?: boolean;
|
|
||||||
value?: (string | number)[];
|
|
||||||
defaultValue?: (string | number)[];
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
hideCheckAll?: boolean;
|
|
||||||
actionsText?: {
|
|
||||||
expandText?: React.ReactNode;
|
|
||||||
collapseText?: React.ReactNode;
|
|
||||||
selectAllText?: React.ReactNode;
|
|
||||||
};
|
|
||||||
className?: string;
|
|
||||||
Option?: TagSelectOptionProps;
|
|
||||||
children?: TagSelectOptionElement | TagSelectOptionElement[];
|
|
||||||
}
|
|
||||||
const TagSelect: FC<TagSelectProps> & {
|
|
||||||
Option: typeof TagSelectOption;
|
|
||||||
} = (props) => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const { children, hideCheckAll = false, className, style, expandable, actionsText = {} } = props;
|
|
||||||
const [expand, setExpand] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [value, setValue] = useMergedState<(string | number)[]>(props.defaultValue || [], {
|
|
||||||
value: props.value,
|
|
||||||
defaultValue: props.defaultValue,
|
|
||||||
onChange: props.onChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isTagSelectOption = (node: TagSelectOptionElement) =>
|
|
||||||
node &&
|
|
||||||
node.type &&
|
|
||||||
(node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption');
|
|
||||||
const getAllTags = () => {
|
|
||||||
const childrenArray = React.Children.toArray(children) as TagSelectOptionElement[];
|
|
||||||
const checkedTags = childrenArray
|
|
||||||
.filter((child) => isTagSelectOption(child))
|
|
||||||
.map((child) => child.props.value);
|
|
||||||
return checkedTags || [];
|
|
||||||
};
|
|
||||||
const onSelectAll = (checked: boolean) => {
|
|
||||||
let checkedTags: (string | number)[] = [];
|
|
||||||
if (checked) {
|
|
||||||
checkedTags = getAllTags();
|
|
||||||
}
|
|
||||||
setValue(checkedTags);
|
|
||||||
};
|
|
||||||
const handleTagChange = (tag: string | number, checked: boolean) => {
|
|
||||||
const checkedTags: (string | number)[] = [...(value || [])];
|
|
||||||
const index = checkedTags.indexOf(tag);
|
|
||||||
if (checked && index === -1) {
|
|
||||||
checkedTags.push(tag);
|
|
||||||
} else if (!checked && index > -1) {
|
|
||||||
checkedTags.splice(index, 1);
|
|
||||||
}
|
|
||||||
setValue(checkedTags);
|
|
||||||
};
|
|
||||||
const checkedAll = getAllTags().length === value?.length;
|
|
||||||
const { expandText = '展开', collapseText = '收起', selectAllText = '全部' } = actionsText;
|
|
||||||
const cls = classNames(styles.tagSelect, className, {
|
|
||||||
[styles.hasExpandTag]: expandable,
|
|
||||||
[styles.expanded]: expand,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className={cls} style={style}>
|
|
||||||
{hideCheckAll ? null : (
|
|
||||||
<CheckableTag checked={checkedAll} key="tag-select-__all__" onChange={onSelectAll}>
|
|
||||||
{selectAllText}
|
|
||||||
</CheckableTag>
|
|
||||||
)}
|
|
||||||
{children &&
|
|
||||||
React.Children.map(children, (child: TagSelectOptionElement) => {
|
|
||||||
if (isTagSelectOption(child)) {
|
|
||||||
return React.cloneElement(child, {
|
|
||||||
key: `tag-select-${child.props.value}`,
|
|
||||||
value: child.props.value,
|
|
||||||
checked: value && value.indexOf(child.props.value) > -1,
|
|
||||||
onChange: handleTagChange,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
})}
|
|
||||||
{expandable && (
|
|
||||||
<a
|
|
||||||
className={styles.trigger}
|
|
||||||
onClick={() => {
|
|
||||||
setExpand(!expand);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{expand ? (
|
|
||||||
<>
|
|
||||||
{collapseText} <UpOutlined />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{expandText}
|
|
||||||
<DownOutlined />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
TagSelect.Option = TagSelectOption;
|
|
||||||
export default TagSelect;
|
|
|
@ -1,33 +0,0 @@
|
||||||
export type Member = {
|
|
||||||
avatar: string;
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Params {
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListItemDataType {
|
|
||||||
id: string;
|
|
||||||
owner: 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[];
|
|
||||||
}
|
|
|
@ -1,214 +0,0 @@
|
||||||
import {
|
|
||||||
DownloadOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
EllipsisOutlined,
|
|
||||||
ShareAltOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { useRequest } from '@umijs/max';
|
|
||||||
import { Avatar, Card, Col, Dropdown, Form, List, Row, Select, Tooltip } from 'antd';
|
|
||||||
import numeral from 'numeral';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
import { categoryOptions } from '../../mock';
|
|
||||||
import StandardFormRow from './components/StandardFormRow';
|
|
||||||
import TagSelect from './components/TagSelect';
|
|
||||||
import type { ListItemDataType } from './data.d';
|
|
||||||
import { queryFakeList } from './service';
|
|
||||||
import useStyles from './style.style';
|
|
||||||
export function formatWan(val: number) {
|
|
||||||
const v = val * 1;
|
|
||||||
if (!v || Number.isNaN(v)) return '';
|
|
||||||
let result: React.ReactNode = val;
|
|
||||||
if (val > 10000) {
|
|
||||||
result = (
|
|
||||||
<span>
|
|
||||||
{Math.floor(val / 10000)}
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
top: -2,
|
|
||||||
fontSize: 14,
|
|
||||||
fontStyle: 'normal',
|
|
||||||
marginLeft: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
万
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
const formItemLayout = {
|
|
||||||
wrapperCol: {
|
|
||||||
xs: {
|
|
||||||
span: 24,
|
|
||||||
},
|
|
||||||
sm: {
|
|
||||||
span: 16,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const CardInfo: React.FC<{
|
|
||||||
activeUser: React.ReactNode;
|
|
||||||
newUser: React.ReactNode;
|
|
||||||
}> = ({ activeUser, newUser }) => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
return (
|
|
||||||
<div className={styles.cardInfo}>
|
|
||||||
<div>
|
|
||||||
<p>活跃用户</p>
|
|
||||||
<p>{activeUser}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>新增用户</p>
|
|
||||||
<p>{newUser}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export const Applications: FC<Record<string, any>> = () => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const { data, loading, run } = useRequest((values: any) => {
|
|
||||||
console.log('form data', values);
|
|
||||||
return queryFakeList({
|
|
||||||
count: 8,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const list = data?.list || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.filterCardList}>
|
|
||||||
<Card bordered={false}>
|
|
||||||
<Form
|
|
||||||
onValuesChange={(_, values) => {
|
|
||||||
run(values);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StandardFormRow
|
|
||||||
title="所属类目"
|
|
||||||
block
|
|
||||||
style={{
|
|
||||||
paddingBottom: 11,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form.Item name="category">
|
|
||||||
<TagSelect expandable>
|
|
||||||
{categoryOptions.map((category) => (
|
|
||||||
<TagSelect.Option value={category.value!} key={category.value}>
|
|
||||||
{category.label}
|
|
||||||
</TagSelect.Option>
|
|
||||||
))}
|
|
||||||
</TagSelect>
|
|
||||||
</Form.Item>
|
|
||||||
</StandardFormRow>
|
|
||||||
<StandardFormRow title="其它选项" grid last>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col lg={8} md={10} sm={10} xs={24}>
|
|
||||||
<Form.Item {...formItemLayout} name="author" label="作者">
|
|
||||||
<Select
|
|
||||||
placeholder="不限"
|
|
||||||
style={{
|
|
||||||
maxWidth: 200,
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: '王昭君',
|
|
||||||
value: 'lisa',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col lg={8} md={10} sm={10} xs={24}>
|
|
||||||
<Form.Item {...formItemLayout} name="rate" label="好评度">
|
|
||||||
<Select
|
|
||||||
placeholder="不限"
|
|
||||||
style={{
|
|
||||||
maxWidth: 200,
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: '优秀',
|
|
||||||
value: 'good',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '普通',
|
|
||||||
value: 'normal',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</StandardFormRow>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
<br />
|
|
||||||
<List<ListItemDataType>
|
|
||||||
rowKey="id"
|
|
||||||
grid={{
|
|
||||||
gutter: 16,
|
|
||||||
xs: 1,
|
|
||||||
sm: 2,
|
|
||||||
md: 3,
|
|
||||||
lg: 3,
|
|
||||||
xl: 4,
|
|
||||||
xxl: 4,
|
|
||||||
}}
|
|
||||||
loading={loading}
|
|
||||||
dataSource={list}
|
|
||||||
renderItem={(item) => (
|
|
||||||
<List.Item key={item.id}>
|
|
||||||
<Card
|
|
||||||
hoverable
|
|
||||||
bodyStyle={{
|
|
||||||
paddingBottom: 20,
|
|
||||||
}}
|
|
||||||
actions={[
|
|
||||||
<Tooltip key="download" title="下载">
|
|
||||||
<DownloadOutlined />
|
|
||||||
</Tooltip>,
|
|
||||||
<Tooltip key="edit" title="编辑">
|
|
||||||
<EditOutlined />
|
|
||||||
</Tooltip>,
|
|
||||||
<Tooltip title="分享" key="share">
|
|
||||||
<ShareAltOutlined />
|
|
||||||
</Tooltip>,
|
|
||||||
<Dropdown
|
|
||||||
key="ellipsis"
|
|
||||||
menu={{
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
key: '1',
|
|
||||||
title: '1st menu item',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '2',
|
|
||||||
title: '2st menu item',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EllipsisOutlined />
|
|
||||||
</Dropdown>,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Card.Meta avatar={<Avatar size="small" src={item.avatar} />} title={item.title} />
|
|
||||||
<div>
|
|
||||||
<CardInfo
|
|
||||||
activeUser={formatWan(item.activeUser)}
|
|
||||||
newUser={numeral(item.newUser).format('0,0')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default Applications;
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import type { ListItemDataType, Params } from './data.d';
|
|
||||||
|
|
||||||
export async function queryFakeList(
|
|
||||||
params: Params,
|
|
||||||
): Promise<{ data: { list: ListItemDataType[] } }> {
|
|
||||||
return request('/api/fake_list', {
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
filterCardList: {
|
|
||||||
'.ant-card-meta-content': { marginTop: '0' },
|
|
||||||
'.ant-card-meta-avatar': { fontSize: '0' },
|
|
||||||
'.ant-list .ant-list-item-content-single': { maxWidth: '100%' },
|
|
||||||
},
|
|
||||||
cardInfo: {
|
|
||||||
marginTop: '16px',
|
|
||||||
marginLeft: '40px',
|
|
||||||
zoom: '1',
|
|
||||||
'&::before, &::after': { display: 'table', content: "' '" },
|
|
||||||
'&::after': {
|
|
||||||
clear: 'both',
|
|
||||||
height: '0',
|
|
||||||
fontSize: '0',
|
|
||||||
visibility: 'hidden',
|
|
||||||
},
|
|
||||||
'& > div': {
|
|
||||||
position: 'relative',
|
|
||||||
float: 'left',
|
|
||||||
width: '50%',
|
|
||||||
textAlign: 'left',
|
|
||||||
p: {
|
|
||||||
margin: '0',
|
|
||||||
fontSize: '24px',
|
|
||||||
lineHeight: '32px',
|
|
||||||
},
|
|
||||||
'p:first-child': {
|
|
||||||
marginBottom: '4px',
|
|
||||||
color: token.colorTextSecondary,
|
|
||||||
fontSize: '12px',
|
|
||||||
lineHeight: '20px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(() => {
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
export default useStyles;
|
|
|
@ -1,118 +0,0 @@
|
||||||
import type { Request, Response } from 'express';
|
|
||||||
import type { ListItemDataType } 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): ListItemDataType[] {
|
|
||||||
const list = [];
|
|
||||||
for (let i = 0; i < count; i += 1) {
|
|
||||||
list.push({
|
|
||||||
id: `fake-list-${Math.random().toString(36).slice(2, 6)}${i}`,
|
|
||||||
owner: user[i % 10],
|
|
||||||
title: titles[i % 8],
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFakeList(req: Request, res: Response) {
|
|
||||||
const params: any = req.query;
|
|
||||||
|
|
||||||
const count = params.count * 1 || 20;
|
|
||||||
|
|
||||||
const result = fakeList(count);
|
|
||||||
return res.json({
|
|
||||||
data: {
|
|
||||||
list: result,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
'GET /api/fake_list': getFakeList,
|
|
||||||
};
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
description: {
|
|
||||||
maxWidth: '720px',
|
|
||||||
lineHeight: '22px',
|
|
||||||
},
|
|
||||||
extra: {
|
|
||||||
marginTop: '16px',
|
|
||||||
color: token.colorTextSecondary,
|
|
||||||
lineHeight: '22px',
|
|
||||||
'& > em': {
|
|
||||||
marginLeft: '16px',
|
|
||||||
color: token.colorTextDisabled,
|
|
||||||
fontStyle: 'normal',
|
|
||||||
},
|
|
||||||
[`@media screen and (max-width: ${token.screenXS}px)`]: {
|
|
||||||
'& > em': {
|
|
||||||
display: 'block',
|
|
||||||
marginTop: '8px',
|
|
||||||
marginLeft: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { Avatar } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import React from 'react';
|
|
||||||
import useStyles from './index.style';
|
|
||||||
type ArticleListContentProps = {
|
|
||||||
data: {
|
|
||||||
content: React.ReactNode;
|
|
||||||
updatedAt: number;
|
|
||||||
avatar: string;
|
|
||||||
owner: string;
|
|
||||||
href: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const ArticleListContent: React.FC<ArticleListContentProps> = ({
|
|
||||||
data: { content, updatedAt, avatar, owner, href },
|
|
||||||
}) => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.description}>{content}</div>
|
|
||||||
<div className={styles.extra}>
|
|
||||||
<Avatar src={avatar} size="small" />
|
|
||||||
<a href={href}>{owner}</a> 发布在 <a href={href}>{href}</a>
|
|
||||||
<em>{dayjs(updatedAt).format('YYYY-MM-DD HH:mm')}</em>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default ArticleListContent;
|
|
|
@ -1,62 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
standardFormRow: {
|
|
||||||
display: 'flex',
|
|
||||||
width: '100%',
|
|
||||||
marginBottom: '16px',
|
|
||||||
paddingBottom: '16px',
|
|
||||||
borderBottom: `1px dashed ${token.colorSplit}`,
|
|
||||||
'.ant-form-item, .ant-legacy-form-item': { marginRight: '24px' },
|
|
||||||
'.ant-form-item-label, .ant-legacy-form-item-label': {
|
|
||||||
label: {
|
|
||||||
marginRight: '0',
|
|
||||||
color: token.colorText,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.ant-form-item-label, .ant-legacy-form-item-label, .ant-form-item-control, .ant-legacy-form-item-control':
|
|
||||||
{ padding: '0', lineHeight: '32px' },
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
flex: '0 0 auto',
|
|
||||||
marginRight: '24px',
|
|
||||||
color: token.colorTextHeading,
|
|
||||||
fontSize: token.fontSize,
|
|
||||||
textAlign: 'right',
|
|
||||||
'& > span': {
|
|
||||||
display: 'inline-block',
|
|
||||||
height: '32px',
|
|
||||||
lineHeight: '32px',
|
|
||||||
'&::after': {
|
|
||||||
content: "':'",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: '1 1 0',
|
|
||||||
'.ant-form-item, .ant-legacy-form-item': {
|
|
||||||
'&:last-child': {
|
|
||||||
display: 'block',
|
|
||||||
marginRight: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
standardFormRowLast: {
|
|
||||||
marginBottom: '0',
|
|
||||||
paddingBottom: '0',
|
|
||||||
border: 'none',
|
|
||||||
},
|
|
||||||
standardFormRowBlock: {
|
|
||||||
'.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
|
|
||||||
{ display: 'block' },
|
|
||||||
},
|
|
||||||
standardFormRowGrid: {
|
|
||||||
'.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
|
|
||||||
{ display: 'block' },
|
|
||||||
'.ant-form-item-label, .ant-legacy-form-item-label': { float: 'left' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,37 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
|
||||||
import useStyles from './index.style';
|
|
||||||
type StandardFormRowProps = {
|
|
||||||
title?: string;
|
|
||||||
last?: boolean;
|
|
||||||
block?: boolean;
|
|
||||||
grid?: boolean;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
const StandardFormRow: React.FC<StandardFormRowProps> = ({
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
last,
|
|
||||||
block,
|
|
||||||
grid,
|
|
||||||
...rest
|
|
||||||
}) => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const cls = classNames(styles.standardFormRow, {
|
|
||||||
[styles.standardFormRowBlock]: block,
|
|
||||||
[styles.standardFormRowLast]: last,
|
|
||||||
[styles.standardFormRowGrid]: grid,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className={cls} {...rest}>
|
|
||||||
{title && (
|
|
||||||
<div className={styles.label}>
|
|
||||||
<span>{title}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.content}>{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default StandardFormRow;
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
tagSelect: {
|
|
||||||
position: 'relative',
|
|
||||||
maxHeight: '32px',
|
|
||||||
marginLeft: '-8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
lineHeight: '32px',
|
|
||||||
transition: 'all 0.3s',
|
|
||||||
userSelect: 'none',
|
|
||||||
'.ant-tag': {
|
|
||||||
marginRight: '24px',
|
|
||||||
padding: '0 8px',
|
|
||||||
fontSize: token.fontSize,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
trigger: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '0',
|
|
||||||
right: '0',
|
|
||||||
'span.anticon': { fontSize: '12px' },
|
|
||||||
},
|
|
||||||
expanded: {
|
|
||||||
maxHeight: '200px',
|
|
||||||
transition: 'all 0.3s',
|
|
||||||
},
|
|
||||||
hasExpandTag: {
|
|
||||||
paddingRight: '50px',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,136 +0,0 @@
|
||||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
|
||||||
import { Tag } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useMergedState } from 'rc-util';
|
|
||||||
import React, { FC, useState } from 'react';
|
|
||||||
import useStyles from './index.style';
|
|
||||||
const { CheckableTag } = Tag;
|
|
||||||
export interface TagSelectOptionProps {
|
|
||||||
value: string | number;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
checked?: boolean;
|
|
||||||
onChange?: (value: string | number, state: boolean) => void;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
const TagSelectOption: React.FC<TagSelectOptionProps> & {
|
|
||||||
isTagSelectOption: boolean;
|
|
||||||
} = ({ children, checked, onChange, value }) => (
|
|
||||||
<CheckableTag
|
|
||||||
checked={!!checked}
|
|
||||||
key={value}
|
|
||||||
onChange={(state) => onChange && onChange(value, state)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CheckableTag>
|
|
||||||
);
|
|
||||||
|
|
||||||
TagSelectOption.isTagSelectOption = true;
|
|
||||||
|
|
||||||
type TagSelectOptionElement = React.ReactElement<TagSelectOptionProps, typeof TagSelectOption>;
|
|
||||||
|
|
||||||
export interface TagSelectProps {
|
|
||||||
onChange?: (value: (string | number)[]) => void;
|
|
||||||
expandable?: boolean;
|
|
||||||
value?: (string | number)[];
|
|
||||||
defaultValue?: (string | number)[];
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
hideCheckAll?: boolean;
|
|
||||||
actionsText?: {
|
|
||||||
expandText?: React.ReactNode;
|
|
||||||
collapseText?: React.ReactNode;
|
|
||||||
selectAllText?: React.ReactNode;
|
|
||||||
};
|
|
||||||
className?: string;
|
|
||||||
Option?: TagSelectOptionProps;
|
|
||||||
children?: TagSelectOptionElement | TagSelectOptionElement[];
|
|
||||||
}
|
|
||||||
const TagSelect: FC<TagSelectProps> & {
|
|
||||||
Option: typeof TagSelectOption;
|
|
||||||
} = (props) => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const { children, hideCheckAll = false, className, style, expandable, actionsText = {} } = props;
|
|
||||||
const [expand, setExpand] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [value, setValue] = useMergedState<(string | number)[]>(props.defaultValue || [], {
|
|
||||||
value: props.value,
|
|
||||||
defaultValue: props.defaultValue,
|
|
||||||
onChange: props.onChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isTagSelectOption = (node: TagSelectOptionElement) =>
|
|
||||||
node &&
|
|
||||||
node.type &&
|
|
||||||
(node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption');
|
|
||||||
const getAllTags = () => {
|
|
||||||
const childrenArray = React.Children.toArray(children) as TagSelectOptionElement[];
|
|
||||||
const checkedTags = childrenArray
|
|
||||||
.filter((child) => isTagSelectOption(child))
|
|
||||||
.map((child) => child.props.value);
|
|
||||||
return checkedTags || [];
|
|
||||||
};
|
|
||||||
const onSelectAll = (checked: boolean) => {
|
|
||||||
let checkedTags: (string | number)[] = [];
|
|
||||||
if (checked) {
|
|
||||||
checkedTags = getAllTags();
|
|
||||||
}
|
|
||||||
setValue(checkedTags);
|
|
||||||
};
|
|
||||||
const handleTagChange = (tag: string | number, checked: boolean) => {
|
|
||||||
const checkedTags: (string | number)[] = [...(value || [])];
|
|
||||||
const index = checkedTags.indexOf(tag);
|
|
||||||
if (checked && index === -1) {
|
|
||||||
checkedTags.push(tag);
|
|
||||||
} else if (!checked && index > -1) {
|
|
||||||
checkedTags.splice(index, 1);
|
|
||||||
}
|
|
||||||
setValue(checkedTags);
|
|
||||||
};
|
|
||||||
const checkedAll = getAllTags().length === value?.length;
|
|
||||||
const { expandText = '展开', collapseText = '收起', selectAllText = '全部' } = actionsText;
|
|
||||||
const cls = classNames(styles.tagSelect, className, {
|
|
||||||
[styles.hasExpandTag]: expandable,
|
|
||||||
[styles.expanded]: expand,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className={cls} style={style}>
|
|
||||||
{hideCheckAll ? null : (
|
|
||||||
<CheckableTag checked={checkedAll} key="tag-select-__all__" onChange={onSelectAll}>
|
|
||||||
{selectAllText}
|
|
||||||
</CheckableTag>
|
|
||||||
)}
|
|
||||||
{children &&
|
|
||||||
React.Children.map(children, (child: TagSelectOptionElement) => {
|
|
||||||
if (isTagSelectOption(child)) {
|
|
||||||
return React.cloneElement(child, {
|
|
||||||
key: `tag-select-${child.props.value}`,
|
|
||||||
value: child.props.value,
|
|
||||||
checked: value && value.indexOf(child.props.value) > -1,
|
|
||||||
onChange: handleTagChange,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
})}
|
|
||||||
{expandable && (
|
|
||||||
<a
|
|
||||||
className={styles.trigger}
|
|
||||||
onClick={() => {
|
|
||||||
setExpand(!expand);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{expand ? (
|
|
||||||
<>
|
|
||||||
{collapseText} <UpOutlined />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{expandText}
|
|
||||||
<DownOutlined />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
TagSelect.Option = TagSelectOption;
|
|
||||||
export default TagSelect;
|
|
|
@ -1,32 +0,0 @@
|
||||||
export type Member = {
|
|
||||||
avatar: string;
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Params {
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
export interface ListItemDataType {
|
|
||||||
id: string;
|
|
||||||
owner: 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[];
|
|
||||||
}
|
|
|
@ -1,242 +0,0 @@
|
||||||
import { LikeOutlined, LoadingOutlined, MessageOutlined, StarOutlined } from '@ant-design/icons';
|
|
||||||
import { useRequest } from '@umijs/max';
|
|
||||||
import { Button, Card, Col, Form, List, Row, Select, Tag } from 'antd';
|
|
||||||
import { DefaultOptionType } from 'antd/es/select';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import { categoryOptions } from '../../mock';
|
|
||||||
import ArticleListContent from './components/ArticleListContent';
|
|
||||||
import StandardFormRow from './components/StandardFormRow';
|
|
||||||
import TagSelect from './components/TagSelect';
|
|
||||||
import type { ListItemDataType } from './data.d';
|
|
||||||
import { queryFakeList } from './service';
|
|
||||||
import useStyles from './style.style';
|
|
||||||
|
|
||||||
const FormItem = Form.Item;
|
|
||||||
|
|
||||||
const pageSize = 5;
|
|
||||||
|
|
||||||
const Articles: FC = () => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
const { styles } = useStyles();
|
|
||||||
|
|
||||||
const { data, reload, loading, loadMore, loadingMore } = useRequest(
|
|
||||||
() => {
|
|
||||||
return queryFakeList({
|
|
||||||
count: pageSize,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loadMore: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const list = data?.list || [];
|
|
||||||
|
|
||||||
const setOwner = () => {
|
|
||||||
form.setFieldsValue({
|
|
||||||
owner: ['wzj'],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const owners = [
|
|
||||||
{
|
|
||||||
id: 'wzj',
|
|
||||||
name: '我自己',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'wjh',
|
|
||||||
name: '吴家豪',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'zxx',
|
|
||||||
name: '周星星',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'zly',
|
|
||||||
name: '赵丽颖',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ym',
|
|
||||||
name: '姚明',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const IconText: React.FC<{
|
|
||||||
type: string;
|
|
||||||
text: React.ReactNode;
|
|
||||||
}> = ({ type, text }) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'star-o':
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<StarOutlined style={{ marginRight: 8 }} />
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case 'like-o':
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<LikeOutlined style={{ marginRight: 8 }} />
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case 'message':
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<MessageOutlined style={{ marginRight: 8 }} />
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formItemLayout = {
|
|
||||||
wrapperCol: {
|
|
||||||
xs: { span: 24 },
|
|
||||||
sm: { span: 24 },
|
|
||||||
md: { span: 12 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadMoreDom = list.length > 0 && (
|
|
||||||
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
|
||||||
<Button onClick={loadMore} style={{ paddingLeft: 48, paddingRight: 48 }}>
|
|
||||||
{loadingMore ? (
|
|
||||||
<span>
|
|
||||||
<LoadingOutlined /> 加载中...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
'加载更多'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ownerOptions = useMemo<DefaultOptionType[]>(
|
|
||||||
() =>
|
|
||||||
owners.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
})),
|
|
||||||
[owners],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card bordered={false}>
|
|
||||||
<Form
|
|
||||||
layout="inline"
|
|
||||||
form={form}
|
|
||||||
initialValues={{
|
|
||||||
owner: ['wjh', 'zxx'],
|
|
||||||
}}
|
|
||||||
onValuesChange={reload}
|
|
||||||
>
|
|
||||||
<StandardFormRow title="所属类目" block style={{ paddingBottom: 11 }}>
|
|
||||||
<FormItem name="category">
|
|
||||||
<TagSelect expandable>
|
|
||||||
{categoryOptions.map((category) => (
|
|
||||||
<TagSelect.Option value={category.value!} key={category.value}>
|
|
||||||
{category.label}
|
|
||||||
</TagSelect.Option>
|
|
||||||
))}
|
|
||||||
</TagSelect>
|
|
||||||
</FormItem>
|
|
||||||
</StandardFormRow>
|
|
||||||
<StandardFormRow title="owner" grid>
|
|
||||||
<FormItem name="owner" noStyle>
|
|
||||||
<Select
|
|
||||||
mode="multiple"
|
|
||||||
placeholder="选择 owner"
|
|
||||||
style={{ minWidth: '6rem' }}
|
|
||||||
options={ownerOptions}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<a className={styles.selfTrigger} onClick={setOwner}>
|
|
||||||
只看自己的
|
|
||||||
</a>
|
|
||||||
</StandardFormRow>
|
|
||||||
<StandardFormRow title="其它选项" grid last>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col xl={8} lg={10} md={12} sm={24} xs={24}>
|
|
||||||
<FormItem {...formItemLayout} label="活跃用户" name="user">
|
|
||||||
<Select
|
|
||||||
placeholder="不限"
|
|
||||||
style={{ maxWidth: 200, width: '100%' }}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: '李三',
|
|
||||||
value: 'lisa',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col xl={8} lg={10} md={12} sm={24} xs={24}>
|
|
||||||
<FormItem {...formItemLayout} label="好评度" name="rate">
|
|
||||||
<Select
|
|
||||||
placeholder="不限"
|
|
||||||
style={{ maxWidth: 200, width: '100%' }}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: '优秀',
|
|
||||||
value: 'good',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</StandardFormRow>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
style={{ marginTop: 24 }}
|
|
||||||
bordered={false}
|
|
||||||
bodyStyle={{ padding: '8px 32px 32px 32px' }}
|
|
||||||
>
|
|
||||||
<List<ListItemDataType>
|
|
||||||
size="large"
|
|
||||||
loading={loading}
|
|
||||||
rowKey="id"
|
|
||||||
itemLayout="vertical"
|
|
||||||
loadMore={loadMoreDom}
|
|
||||||
dataSource={list}
|
|
||||||
renderItem={(item) => (
|
|
||||||
<List.Item
|
|
||||||
key={item.id}
|
|
||||||
actions={[
|
|
||||||
<IconText key="star" type="star-o" text={item.star} />,
|
|
||||||
<IconText key="like" type="like-o" text={item.like} />,
|
|
||||||
<IconText key="message" type="message" text={item.message} />,
|
|
||||||
]}
|
|
||||||
extra={<div className={styles.listItemExtra} />}
|
|
||||||
>
|
|
||||||
<List.Item.Meta
|
|
||||||
title={
|
|
||||||
<a className={styles.listItemMetaTitle} href={item.href}>
|
|
||||||
{item.title}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<span>
|
|
||||||
<Tag>Ant Design</Tag>
|
|
||||||
<Tag>设计语言</Tag>
|
|
||||||
<Tag>蚂蚁金服</Tag>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ArticleListContent data={item} />
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Articles;
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import type { ListItemDataType, Params } from './data.d';
|
|
||||||
|
|
||||||
export async function queryFakeList(
|
|
||||||
params: Params,
|
|
||||||
): Promise<{ data: { list: ListItemDataType[] } }> {
|
|
||||||
return request('/api/fake_list', {
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
listItemMetaTitle: {
|
|
||||||
color: token.colorTextHeading,
|
|
||||||
},
|
|
||||||
listItemExtra: {
|
|
||||||
width: '272px',
|
|
||||||
height: '1px',
|
|
||||||
[`@media screen and (max-width: ${token.screenLG}px)`]: {
|
|
||||||
width: '0',
|
|
||||||
height: '1px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
selfTrigger: {
|
|
||||||
marginLeft: '12px',
|
|
||||||
[`@media screen and (max-width: ${token.screenXS}px)`]: {
|
|
||||||
display: 'block',
|
|
||||||
marginLeft: '0',
|
|
||||||
},
|
|
||||||
[`@media screen and (max-width: ${token.screenMD}px)`]: {
|
|
||||||
display: 'block',
|
|
||||||
marginLeft: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,81 +0,0 @@
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
|
||||||
import { history, Outlet, useLocation, useMatch } from '@umijs/max';
|
|
||||||
import { Input } from 'antd';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
|
|
||||||
type SearchProps = {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabList = [
|
|
||||||
{
|
|
||||||
key: 'articles',
|
|
||||||
tab: '文章',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'projects',
|
|
||||||
tab: '项目',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'applications',
|
|
||||||
tab: '应用',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const Search: FC<SearchProps> = () => {
|
|
||||||
const location = useLocation();
|
|
||||||
let match = useMatch(location.pathname);
|
|
||||||
const handleTabChange = (key: string) => {
|
|
||||||
const url =
|
|
||||||
match?.pathname === '/' ? '' : match?.pathname.substring(0, match.pathname.lastIndexOf('/'));
|
|
||||||
switch (key) {
|
|
||||||
case 'articles':
|
|
||||||
history.push(`${url}/articles`);
|
|
||||||
break;
|
|
||||||
case 'applications':
|
|
||||||
history.push(`${url}/applications`);
|
|
||||||
break;
|
|
||||||
case 'projects':
|
|
||||||
history.push(`${url}/projects`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFormSubmit = (value: string) => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTabKey = () => {
|
|
||||||
const tabKey = location.pathname.substring(location.pathname.lastIndexOf('/') + 1);
|
|
||||||
if (tabKey && tabKey !== '/') {
|
|
||||||
return tabKey;
|
|
||||||
}
|
|
||||||
return 'articles';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer
|
|
||||||
content={
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<Input.Search
|
|
||||||
placeholder="请输入"
|
|
||||||
enterButton="搜索"
|
|
||||||
size="large"
|
|
||||||
onSearch={handleFormSubmit}
|
|
||||||
style={{ maxWidth: 522, width: '100%' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
tabList={tabList}
|
|
||||||
tabActiveKey={getTabKey()}
|
|
||||||
onTabChange={handleTabChange}
|
|
||||||
>
|
|
||||||
<Outlet />
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Search;
|
|
|
@ -1,118 +0,0 @@
|
||||||
import type { Request, Response } from 'express';
|
|
||||||
import type { ListItemDataType } 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): ListItemDataType[] {
|
|
||||||
const list = [];
|
|
||||||
for (let i = 0; i < count; i += 1) {
|
|
||||||
list.push({
|
|
||||||
id: `fake-list-${i}`,
|
|
||||||
owner: user[i % 10],
|
|
||||||
title: titles[i % 8],
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFakeList(req: Request, res: Response) {
|
|
||||||
const params: any = req.query;
|
|
||||||
|
|
||||||
const count = params.count * 1 || 20;
|
|
||||||
|
|
||||||
const result = fakeList(count);
|
|
||||||
return res.json({
|
|
||||||
data: {
|
|
||||||
list: result,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
'GET /api/fake_list': getFakeList,
|
|
||||||
};
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
avatarList: {
|
|
||||||
display: 'inline-block',
|
|
||||||
ul: { display: 'inline-block', marginLeft: '8px', fontSize: '0' },
|
|
||||||
},
|
|
||||||
avatarItem: {
|
|
||||||
display: 'inline-block',
|
|
||||||
width: token.controlHeight,
|
|
||||||
height: token.controlHeight,
|
|
||||||
marginLeft: '-8px',
|
|
||||||
fontSize: token.fontSize,
|
|
||||||
'.ant-avatar': { border: `1px solid ${token.colorBorder}` },
|
|
||||||
},
|
|
||||||
avatarItemLarge: {
|
|
||||||
width: token.controlHeightLG,
|
|
||||||
height: token.controlHeightLG,
|
|
||||||
},
|
|
||||||
avatarItemSmall: {
|
|
||||||
width: token.controlHeightSM,
|
|
||||||
height: token.controlHeightSM,
|
|
||||||
},
|
|
||||||
avatarItemMini: {
|
|
||||||
width: '20px',
|
|
||||||
height: '20px',
|
|
||||||
'.ant-avatar': {
|
|
||||||
width: '20px',
|
|
||||||
height: '20px',
|
|
||||||
lineHeight: '20px',
|
|
||||||
'.ant-avatar-string': {
|
|
||||||
fontSize: '12px',
|
|
||||||
lineHeight: '18px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,79 +0,0 @@
|
||||||
import { Avatar, Tooltip } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
|
||||||
import useStyles from './index.style';
|
|
||||||
export declare type SizeType = number | 'small' | 'default' | 'large';
|
|
||||||
export type AvatarItemProps = {
|
|
||||||
tips: React.ReactNode;
|
|
||||||
src: string;
|
|
||||||
size?: SizeType;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
export type AvatarListProps = {
|
|
||||||
Item?: React.ReactElement<AvatarItemProps>;
|
|
||||||
size?: SizeType;
|
|
||||||
maxLength?: number;
|
|
||||||
excessItemsStyle?: React.CSSProperties;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
children: React.ReactElement<AvatarItemProps> | React.ReactElement<AvatarItemProps>[];
|
|
||||||
};
|
|
||||||
const avatarSizeToClassName = (size: SizeType | 'mini', styles: any) =>
|
|
||||||
classNames(styles.avatarItem, {
|
|
||||||
[styles.avatarItemLarge]: size === 'large',
|
|
||||||
[styles.avatarItemSmall]: size === 'small',
|
|
||||||
[styles.avatarItemMini]: size === 'mini',
|
|
||||||
});
|
|
||||||
|
|
||||||
const Item: React.FC<AvatarItemProps> = ({ src, size, tips, onClick = () => {} }) => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
|
|
||||||
const cls = avatarSizeToClassName(size!, styles);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className={cls} onClick={onClick}>
|
|
||||||
{tips ? (
|
|
||||||
<Tooltip title={tips}>
|
|
||||||
<Avatar
|
|
||||||
src={src}
|
|
||||||
size={size}
|
|
||||||
style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Avatar src={src} size={size} />
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const AvatarList: React.FC<AvatarListProps> & {
|
|
||||||
Item: typeof Item;
|
|
||||||
} = ({ children, size, maxLength = 5, excessItemsStyle, ...other }) => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const numOfChildren = React.Children.count(children);
|
|
||||||
const numToShow = maxLength >= numOfChildren ? numOfChildren : maxLength;
|
|
||||||
const childrenArray = React.Children.toArray(children) as React.ReactElement<AvatarItemProps>[];
|
|
||||||
|
|
||||||
const childrenWithProps = childrenArray.slice(0, numToShow).map((child) =>
|
|
||||||
React.cloneElement(child, {
|
|
||||||
size,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (numToShow < numOfChildren) {
|
|
||||||
const cls = avatarSizeToClassName(size!, styles);
|
|
||||||
childrenWithProps.push(
|
|
||||||
<li key="exceed" className={cls}>
|
|
||||||
<Avatar size={size} style={excessItemsStyle}>{`+${numOfChildren - maxLength}`}</Avatar>
|
|
||||||
</li>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div {...other} className={styles.avatarList}>
|
|
||||||
<ul> {childrenWithProps} </ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
AvatarList.Item = Item;
|
|
||||||
export default AvatarList;
|
|
|
@ -1,62 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
standardFormRow: {
|
|
||||||
display: 'flex',
|
|
||||||
width: '100%',
|
|
||||||
marginBottom: '16px',
|
|
||||||
paddingBottom: '16px',
|
|
||||||
borderBottom: `1px dashed ${token.colorSplit}`,
|
|
||||||
'.ant-form-item, .ant-legacy-form-item': { marginRight: '24px' },
|
|
||||||
'.ant-form-item-label, .ant-legacy-form-item-label': {
|
|
||||||
label: {
|
|
||||||
marginRight: '0',
|
|
||||||
color: token.colorText,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.ant-form-item-label, .ant-legacy-form-item-label, .ant-form-item-control, .ant-legacy-form-item-control':
|
|
||||||
{ padding: '0', lineHeight: '32px' },
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
flex: '0 0 auto',
|
|
||||||
marginRight: '24px',
|
|
||||||
color: token.colorTextHeading,
|
|
||||||
fontSize: token.fontSize,
|
|
||||||
textAlign: 'right',
|
|
||||||
'& > span': {
|
|
||||||
display: 'inline-block',
|
|
||||||
height: '32px',
|
|
||||||
lineHeight: '32px',
|
|
||||||
'&::after': {
|
|
||||||
content: "':'",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: '1 1 0',
|
|
||||||
'.ant-form-item, .ant-legacy-form-item': {
|
|
||||||
'&:last-child': {
|
|
||||||
display: 'block',
|
|
||||||
marginRight: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
standardFormRowLast: {
|
|
||||||
marginBottom: '0',
|
|
||||||
paddingBottom: '0',
|
|
||||||
border: 'none',
|
|
||||||
},
|
|
||||||
standardFormRowBlock: {
|
|
||||||
'.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
|
|
||||||
{ display: 'block' },
|
|
||||||
},
|
|
||||||
standardFormRowGrid: {
|
|
||||||
'.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
|
|
||||||
{ display: 'block' },
|
|
||||||
'.ant-form-item-label, .ant-legacy-form-item-label': { float: 'left' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,37 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import React from 'react';
|
|
||||||
import useStyles from './index.style';
|
|
||||||
type StandardFormRowProps = {
|
|
||||||
title?: string;
|
|
||||||
last?: boolean;
|
|
||||||
block?: boolean;
|
|
||||||
grid?: boolean;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
const StandardFormRow: React.FC<StandardFormRowProps> = ({
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
last,
|
|
||||||
block,
|
|
||||||
grid,
|
|
||||||
...rest
|
|
||||||
}) => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const cls = classNames(styles.standardFormRow, {
|
|
||||||
[styles.standardFormRowBlock]: block,
|
|
||||||
[styles.standardFormRowLast]: last,
|
|
||||||
[styles.standardFormRowGrid]: grid,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className={cls} {...rest}>
|
|
||||||
{title && (
|
|
||||||
<div className={styles.label}>
|
|
||||||
<span>{title}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.content}>{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default StandardFormRow;
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
tagSelect: {
|
|
||||||
position: 'relative',
|
|
||||||
maxHeight: '32px',
|
|
||||||
marginLeft: '-8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
lineHeight: '32px',
|
|
||||||
transition: 'all 0.3s',
|
|
||||||
userSelect: 'none',
|
|
||||||
'.ant-tag': {
|
|
||||||
marginRight: '24px',
|
|
||||||
padding: '0 8px',
|
|
||||||
fontSize: token.fontSize,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
trigger: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '0',
|
|
||||||
right: '0',
|
|
||||||
'span.anticon': { fontSize: '12px' },
|
|
||||||
},
|
|
||||||
expanded: {
|
|
||||||
maxHeight: '200px',
|
|
||||||
transition: 'all 0.3s',
|
|
||||||
},
|
|
||||||
hasExpandTag: {
|
|
||||||
paddingRight: '50px',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,136 +0,0 @@
|
||||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
|
||||||
import { Tag } from 'antd';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useMergedState } from 'rc-util';
|
|
||||||
import React, { FC, useState } from 'react';
|
|
||||||
import useStyles from './index.style';
|
|
||||||
const { CheckableTag } = Tag;
|
|
||||||
export interface TagSelectOptionProps {
|
|
||||||
value: string | number;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
checked?: boolean;
|
|
||||||
onChange?: (value: string | number, state: boolean) => void;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
const TagSelectOption: React.FC<TagSelectOptionProps> & {
|
|
||||||
isTagSelectOption: boolean;
|
|
||||||
} = ({ children, checked, onChange, value }) => (
|
|
||||||
<CheckableTag
|
|
||||||
checked={!!checked}
|
|
||||||
key={value}
|
|
||||||
onChange={(state) => onChange && onChange(value, state)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CheckableTag>
|
|
||||||
);
|
|
||||||
|
|
||||||
TagSelectOption.isTagSelectOption = true;
|
|
||||||
|
|
||||||
type TagSelectOptionElement = React.ReactElement<TagSelectOptionProps, typeof TagSelectOption>;
|
|
||||||
|
|
||||||
export interface TagSelectProps {
|
|
||||||
onChange?: (value: (string | number)[]) => void;
|
|
||||||
expandable?: boolean;
|
|
||||||
value?: (string | number)[];
|
|
||||||
defaultValue?: (string | number)[];
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
hideCheckAll?: boolean;
|
|
||||||
actionsText?: {
|
|
||||||
expandText?: React.ReactNode;
|
|
||||||
collapseText?: React.ReactNode;
|
|
||||||
selectAllText?: React.ReactNode;
|
|
||||||
};
|
|
||||||
className?: string;
|
|
||||||
Option?: TagSelectOptionProps;
|
|
||||||
children?: TagSelectOptionElement | TagSelectOptionElement[];
|
|
||||||
}
|
|
||||||
const TagSelect: FC<TagSelectProps> & {
|
|
||||||
Option: typeof TagSelectOption;
|
|
||||||
} = (props) => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const { children, hideCheckAll = false, className, style, expandable, actionsText = {} } = props;
|
|
||||||
const [expand, setExpand] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [value, setValue] = useMergedState<(string | number)[]>(props.defaultValue || [], {
|
|
||||||
value: props.value,
|
|
||||||
defaultValue: props.defaultValue,
|
|
||||||
onChange: props.onChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isTagSelectOption = (node: TagSelectOptionElement) =>
|
|
||||||
node &&
|
|
||||||
node.type &&
|
|
||||||
(node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption');
|
|
||||||
const getAllTags = () => {
|
|
||||||
const childrenArray = React.Children.toArray(children) as TagSelectOptionElement[];
|
|
||||||
const checkedTags = childrenArray
|
|
||||||
.filter((child) => isTagSelectOption(child))
|
|
||||||
.map((child) => child.props.value);
|
|
||||||
return checkedTags || [];
|
|
||||||
};
|
|
||||||
const onSelectAll = (checked: boolean) => {
|
|
||||||
let checkedTags: (string | number)[] = [];
|
|
||||||
if (checked) {
|
|
||||||
checkedTags = getAllTags();
|
|
||||||
}
|
|
||||||
setValue(checkedTags);
|
|
||||||
};
|
|
||||||
const handleTagChange = (tag: string | number, checked: boolean) => {
|
|
||||||
const checkedTags: (string | number)[] = [...(value || [])];
|
|
||||||
const index = checkedTags.indexOf(tag);
|
|
||||||
if (checked && index === -1) {
|
|
||||||
checkedTags.push(tag);
|
|
||||||
} else if (!checked && index > -1) {
|
|
||||||
checkedTags.splice(index, 1);
|
|
||||||
}
|
|
||||||
setValue(checkedTags);
|
|
||||||
};
|
|
||||||
const checkedAll = getAllTags().length === value?.length;
|
|
||||||
const { expandText = '展开', collapseText = '收起', selectAllText = '全部' } = actionsText;
|
|
||||||
const cls = classNames(styles.tagSelect, className, {
|
|
||||||
[styles.hasExpandTag]: expandable,
|
|
||||||
[styles.expanded]: expand,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className={cls} style={style}>
|
|
||||||
{hideCheckAll ? null : (
|
|
||||||
<CheckableTag checked={checkedAll} key="tag-select-__all__" onChange={onSelectAll}>
|
|
||||||
{selectAllText}
|
|
||||||
</CheckableTag>
|
|
||||||
)}
|
|
||||||
{children &&
|
|
||||||
React.Children.map(children, (child: TagSelectOptionElement) => {
|
|
||||||
if (isTagSelectOption(child)) {
|
|
||||||
return React.cloneElement(child, {
|
|
||||||
key: `tag-select-${child.props.value}`,
|
|
||||||
value: child.props.value,
|
|
||||||
checked: value && value.indexOf(child.props.value) > -1,
|
|
||||||
onChange: handleTagChange,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
})}
|
|
||||||
{expandable && (
|
|
||||||
<a
|
|
||||||
className={styles.trigger}
|
|
||||||
onClick={() => {
|
|
||||||
setExpand(!expand);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{expand ? (
|
|
||||||
<>
|
|
||||||
{collapseText} <UpOutlined />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{expandText}
|
|
||||||
<DownOutlined />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
TagSelect.Option = TagSelectOption;
|
|
||||||
export default TagSelect;
|
|
|
@ -1,32 +0,0 @@
|
||||||
export type Member = {
|
|
||||||
avatar: string;
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Params {
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
export interface ListItemDataType {
|
|
||||||
id: string;
|
|
||||||
owner: 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[];
|
|
||||||
}
|
|
|
@ -1,162 +0,0 @@
|
||||||
import { useRequest } from '@umijs/max';
|
|
||||||
import { Card, Col, Form, List, Row, Select, Typography } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
||||||
import type { FC } from 'react';
|
|
||||||
import { categoryOptions } from '../../mock';
|
|
||||||
import AvatarList from './components/AvatarList';
|
|
||||||
import StandardFormRow from './components/StandardFormRow';
|
|
||||||
import TagSelect from './components/TagSelect';
|
|
||||||
import type { ListItemDataType } from './data.d';
|
|
||||||
import { queryFakeList } from './service';
|
|
||||||
import useStyles from './style.style';
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
const FormItem = Form.Item;
|
|
||||||
const { Paragraph } = Typography;
|
|
||||||
const getKey = (id: string, index: number) => `${id}-${index}`;
|
|
||||||
const Projects: FC = () => {
|
|
||||||
const { styles } = useStyles();
|
|
||||||
const { data, loading, run } = useRequest((values: any) => {
|
|
||||||
console.log('form data', values);
|
|
||||||
return queryFakeList({
|
|
||||||
count: 8,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const list = data?.list || [];
|
|
||||||
const cardList = list && (
|
|
||||||
<List<ListItemDataType>
|
|
||||||
rowKey="id"
|
|
||||||
loading={loading}
|
|
||||||
grid={{
|
|
||||||
gutter: 16,
|
|
||||||
xs: 1,
|
|
||||||
sm: 2,
|
|
||||||
md: 3,
|
|
||||||
lg: 3,
|
|
||||||
xl: 4,
|
|
||||||
xxl: 4,
|
|
||||||
}}
|
|
||||||
dataSource={list}
|
|
||||||
renderItem={(item) => (
|
|
||||||
<List.Item>
|
|
||||||
<Card className={styles.card} hoverable cover={<img alt={item.title} src={item.cover} />}>
|
|
||||||
<Card.Meta
|
|
||||||
title={<a>{item.title}</a>}
|
|
||||||
description={
|
|
||||||
<Paragraph
|
|
||||||
ellipsis={{
|
|
||||||
rows: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.subDescription}
|
|
||||||
</Paragraph>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className={styles.cardItemContent}>
|
|
||||||
<span>{dayjs(item.updatedAt).fromNow()}</span>
|
|
||||||
<div className={styles.avatarList}>
|
|
||||||
<AvatarList size="small">
|
|
||||||
{item.members.map((member, i) => (
|
|
||||||
<AvatarList.Item
|
|
||||||
key={getKey(item.id, i)}
|
|
||||||
src={member.avatar}
|
|
||||||
tips={member.name}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</AvatarList>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const formItemLayout = {
|
|
||||||
wrapperCol: {
|
|
||||||
xs: {
|
|
||||||
span: 24,
|
|
||||||
},
|
|
||||||
sm: {
|
|
||||||
span: 16,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className={styles.coverCardList}>
|
|
||||||
<Card bordered={false}>
|
|
||||||
<Form
|
|
||||||
layout="inline"
|
|
||||||
onValuesChange={(_, values) => {
|
|
||||||
// 表单项变化时请求数据
|
|
||||||
// 模拟查询表单生效
|
|
||||||
run(values);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StandardFormRow
|
|
||||||
title="所属类目"
|
|
||||||
block
|
|
||||||
style={{
|
|
||||||
paddingBottom: 11,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormItem name="category">
|
|
||||||
<TagSelect expandable>
|
|
||||||
{categoryOptions.map((category) => (
|
|
||||||
<TagSelect.Option value={category.value!} key={category.value}>
|
|
||||||
{category.label}
|
|
||||||
</TagSelect.Option>
|
|
||||||
))}
|
|
||||||
</TagSelect>
|
|
||||||
</FormItem>
|
|
||||||
</StandardFormRow>
|
|
||||||
<StandardFormRow title="其它选项" grid last>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col lg={8} md={10} sm={10} xs={24}>
|
|
||||||
<FormItem {...formItemLayout} label="作者" name="author">
|
|
||||||
<Select
|
|
||||||
placeholder="不限"
|
|
||||||
style={{
|
|
||||||
maxWidth: 200,
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: '王昭君',
|
|
||||||
value: 'lisa',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
<Col lg={8} md={10} sm={10} xs={24}>
|
|
||||||
<FormItem {...formItemLayout} label="好评度" name="rate">
|
|
||||||
<Select
|
|
||||||
placeholder="不限"
|
|
||||||
style={{
|
|
||||||
maxWidth: 200,
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: '优秀',
|
|
||||||
value: 'good',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '普通',
|
|
||||||
value: 'normal',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</StandardFormRow>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
<div className={styles.cardList}>{cardList}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default Projects;
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import type { ListItemDataType, Params } from './data';
|
|
||||||
|
|
||||||
export async function queryFakeList(
|
|
||||||
params: Params,
|
|
||||||
): Promise<{ data: { list: ListItemDataType[] } }> {
|
|
||||||
return request('/api/fake_list', {
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { createStyles } from 'antd-style';
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ token }) => {
|
|
||||||
return {
|
|
||||||
card: {
|
|
||||||
'.ant-card-meta-title': {
|
|
||||||
marginBottom: '4px',
|
|
||||||
'& > a': {
|
|
||||||
display: 'inline-block',
|
|
||||||
maxWidth: '100%',
|
|
||||||
color: token.colorTextHeading,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'.ant-card-meta-description': {
|
|
||||||
height: '44px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
lineHeight: '22px',
|
|
||||||
},
|
|
||||||
'&:hover': {
|
|
||||||
'.ant-card-meta-title > a': {
|
|
||||||
color: token.colorPrimary,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cardItemContent: {
|
|
||||||
display: 'flex',
|
|
||||||
height: '20px',
|
|
||||||
marginTop: '16px',
|
|
||||||
marginBottom: '-4px',
|
|
||||||
lineHeight: '20px',
|
|
||||||
'& > span': {
|
|
||||||
flex: '1',
|
|
||||||
color: token.colorTextSecondary,
|
|
||||||
fontSize: '12px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
avatarList: {
|
|
||||||
flex: '0 1 auto',
|
|
||||||
},
|
|
||||||
cardList: {
|
|
||||||
marginTop: '24px',
|
|
||||||
},
|
|
||||||
coverCardList: {
|
|
||||||
'.ant-list .ant-list-item-content-single': { maxWidth: '100%' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useStyles;
|
|
|
@ -1,176 +0,0 @@
|
||||||
import type { Request, Response } from 'express';
|
|
||||||
import { parse } from 'url';
|
|
||||||
import type { TableListItem, TableListParams } from './data.d';
|
|
||||||
|
|
||||||
// mock tableListDataSource
|
|
||||||
const genList = (current: number, pageSize: number) => {
|
|
||||||
const tableListDataSource: TableListItem[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < pageSize; i += 1) {
|
|
||||||
const index = (current - 1) * 10 + i;
|
|
||||||
tableListDataSource.push({
|
|
||||||
key: index,
|
|
||||||
disabled: i % 6 === 0,
|
|
||||||
href: 'https://ant.design',
|
|
||||||
avatar: [
|
|
||||||
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
|
|
||||||
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
|
|
||||||
][i % 2],
|
|
||||||
name: `TradeCode ${index}`,
|
|
||||||
owner: '曲丽丽',
|
|
||||||
desc: '这是一段描述',
|
|
||||||
callNo: Math.floor(Math.random() * 1000),
|
|
||||||
status: (Math.floor(Math.random() * 10) % 4).toString(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
createdAt: new Date(),
|
|
||||||
progress: Math.ceil(Math.random() * 100),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
tableListDataSource.reverse();
|
|
||||||
return tableListDataSource;
|
|
||||||
};
|
|
||||||
|
|
||||||
let tableListDataSource = genList(1, 100);
|
|
||||||
|
|
||||||
function getRule(req: Request, res: Response, u: string) {
|
|
||||||
let realUrl = u;
|
|
||||||
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
|
|
||||||
realUrl = req.url;
|
|
||||||
}
|
|
||||||
const { current = 1, pageSize = 10 } = req.query;
|
|
||||||
const params = parse(realUrl, true).query as unknown as TableListParams;
|
|
||||||
|
|
||||||
let dataSource = [...tableListDataSource].slice(
|
|
||||||
((current as number) - 1) * (pageSize as number),
|
|
||||||
(current as number) * (pageSize as number),
|
|
||||||
);
|
|
||||||
if (params.sorter) {
|
|
||||||
const sorter = JSON.parse(params.sorter as any);
|
|
||||||
dataSource = dataSource.sort((prev: any, next: any) => {
|
|
||||||
let sortNumber = 0;
|
|
||||||
Object.keys(sorter).forEach((key) => {
|
|
||||||
if (sorter[key] === 'descend') {
|
|
||||||
if (prev[key] - next[key] > 0) {
|
|
||||||
sortNumber += -1;
|
|
||||||
} else {
|
|
||||||
sortNumber += 1;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (prev[key] - next[key] > 0) {
|
|
||||||
sortNumber += 1;
|
|
||||||
} else {
|
|
||||||
sortNumber += -1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return sortNumber;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (params.filter) {
|
|
||||||
const filter = JSON.parse(params.filter as any) as Record<string, string[]>;
|
|
||||||
if (Object.keys(filter).length > 0) {
|
|
||||||
dataSource = dataSource.filter((item) => {
|
|
||||||
return Object.keys(filter).some((key) => {
|
|
||||||
if (!filter[key]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (filter[key].includes(`${item[key as 'status']}`)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.name) {
|
|
||||||
dataSource = dataSource.filter((data) => data.name.includes(params.name || ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalPageSize = 10;
|
|
||||||
if (params.pageSize) {
|
|
||||||
finalPageSize = parseInt(`${params.pageSize}`, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
data: dataSource,
|
|
||||||
total: tableListDataSource.length,
|
|
||||||
success: true,
|
|
||||||
pageSize: finalPageSize,
|
|
||||||
current: parseInt(`${params.currentPage}`, 10) || 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
return res.json(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
function postRule(req: Request, res: Response, u: string, b: Request) {
|
|
||||||
let realUrl = u;
|
|
||||||
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
|
|
||||||
realUrl = req.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = (b && b.body) || req.body;
|
|
||||||
const { name, desc, key } = body;
|
|
||||||
|
|
||||||
switch (req.method) {
|
|
||||||
/* eslint no-case-declarations:0 */
|
|
||||||
case 'DELETE':
|
|
||||||
tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
|
|
||||||
break;
|
|
||||||
case 'POST':
|
|
||||||
(() => {
|
|
||||||
const i = Math.ceil(Math.random() * 10000);
|
|
||||||
const newRule = {
|
|
||||||
key: tableListDataSource.length,
|
|
||||||
href: 'https://ant.design',
|
|
||||||
avatar: [
|
|
||||||
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
|
|
||||||
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
|
|
||||||
][i % 2],
|
|
||||||
name,
|
|
||||||
owner: '曲丽丽',
|
|
||||||
desc,
|
|
||||||
callNo: Math.floor(Math.random() * 1000),
|
|
||||||
status: (Math.floor(Math.random() * 10) % 2).toString(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
createdAt: new Date(),
|
|
||||||
progress: Math.ceil(Math.random() * 100),
|
|
||||||
};
|
|
||||||
tableListDataSource.unshift(newRule);
|
|
||||||
return res.json(newRule);
|
|
||||||
})();
|
|
||||||
return;
|
|
||||||
|
|
||||||
case 'PUT':
|
|
||||||
(() => {
|
|
||||||
let newRule = {};
|
|
||||||
tableListDataSource = tableListDataSource.map((item) => {
|
|
||||||
if (item.key === key) {
|
|
||||||
newRule = { ...item, desc, name };
|
|
||||||
return { ...item, desc, name };
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
return res.json(newRule);
|
|
||||||
})();
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
list: tableListDataSource,
|
|
||||||
pagination: {
|
|
||||||
total: tableListDataSource.length,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
res.json(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
'GET /api/rule': getRule,
|
|
||||||
'POST /api/rule': postRule,
|
|
||||||
'DELETE /api/rule': postRule,
|
|
||||||
'PUT /api/rule': postRule,
|
|
||||||
};
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { Modal } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
type CreateFormProps = {
|
|
||||||
modalVisible: boolean;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
onCancel: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CreateForm: React.FC<CreateFormProps> = (props) => {
|
|
||||||
const { modalVisible, onCancel } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
destroyOnClose
|
|
||||||
title="新建规则"
|
|
||||||
open={modalVisible}
|
|
||||||
onCancel={() => onCancel()}
|
|
||||||
footer={null}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateForm;
|
|
|
@ -1,159 +0,0 @@
|
||||||
import {
|
|
||||||
ProFormDateTimePicker,
|
|
||||||
ProFormRadio,
|
|
||||||
ProFormSelect,
|
|
||||||
ProFormText,
|
|
||||||
ProFormTextArea,
|
|
||||||
StepsForm,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { Modal } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import type { TableListItem } from '../data';
|
|
||||||
|
|
||||||
export type FormValueType = {
|
|
||||||
target?: string;
|
|
||||||
template?: string;
|
|
||||||
type?: string;
|
|
||||||
time?: string;
|
|
||||||
frequency?: string;
|
|
||||||
} & Partial<TableListItem>;
|
|
||||||
|
|
||||||
export type UpdateFormProps = {
|
|
||||||
onCancel: (flag?: boolean, formVals?: FormValueType) => void;
|
|
||||||
onSubmit: (values: FormValueType) => Promise<void>;
|
|
||||||
updateModalVisible: boolean;
|
|
||||||
values: Partial<TableListItem>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UpdateForm: React.FC<UpdateFormProps> = (props) => {
|
|
||||||
return (
|
|
||||||
<StepsForm
|
|
||||||
stepsProps={{
|
|
||||||
size: 'small',
|
|
||||||
}}
|
|
||||||
stepsFormRender={(dom, submitter) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
width={640}
|
|
||||||
bodyStyle={{
|
|
||||||
padding: '32px 40px 48px',
|
|
||||||
}}
|
|
||||||
destroyOnClose
|
|
||||||
title="规则配置"
|
|
||||||
open={props.updateModalVisible}
|
|
||||||
footer={submitter}
|
|
||||||
onCancel={() => {
|
|
||||||
props.onCancel();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{dom}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
onFinish={props.onSubmit}
|
|
||||||
>
|
|
||||||
<StepsForm.StepForm
|
|
||||||
initialValues={{
|
|
||||||
name: props.values.name,
|
|
||||||
desc: props.values.desc,
|
|
||||||
}}
|
|
||||||
title="基本信息"
|
|
||||||
>
|
|
||||||
<ProFormText
|
|
||||||
name="name"
|
|
||||||
label="规则名称"
|
|
||||||
width="md"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入规则名称!',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<ProFormTextArea
|
|
||||||
name="desc"
|
|
||||||
width="md"
|
|
||||||
label="规则描述"
|
|
||||||
placeholder="请输入至少五个字符"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请输入至少五个字符的规则描述!',
|
|
||||||
min: 5,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</StepsForm.StepForm>
|
|
||||||
<StepsForm.StepForm
|
|
||||||
initialValues={{
|
|
||||||
target: '0',
|
|
||||||
template: '0',
|
|
||||||
}}
|
|
||||||
title="配置规则属性"
|
|
||||||
>
|
|
||||||
<ProFormSelect
|
|
||||||
name="target"
|
|
||||||
width="md"
|
|
||||||
label="监控对象"
|
|
||||||
valueEnum={{
|
|
||||||
0: '表一',
|
|
||||||
1: '表二',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="template"
|
|
||||||
width="md"
|
|
||||||
label="规则模板"
|
|
||||||
valueEnum={{
|
|
||||||
0: '规则模板一',
|
|
||||||
1: '规则模板二',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormRadio.Group
|
|
||||||
name="type"
|
|
||||||
label="规则类型"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
value: '0',
|
|
||||||
label: '强',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: '1',
|
|
||||||
label: '弱',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</StepsForm.StepForm>
|
|
||||||
<StepsForm.StepForm
|
|
||||||
initialValues={{
|
|
||||||
type: '1',
|
|
||||||
frequency: 'month',
|
|
||||||
}}
|
|
||||||
title="设定调度周期"
|
|
||||||
>
|
|
||||||
<ProFormDateTimePicker
|
|
||||||
name="time"
|
|
||||||
width="md"
|
|
||||||
label="开始时间"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择开始时间!',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<ProFormSelect
|
|
||||||
name="frequency"
|
|
||||||
label="监控对象"
|
|
||||||
width="md"
|
|
||||||
valueEnum={{
|
|
||||||
month: '月',
|
|
||||||
week: '周',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</StepsForm.StepForm>
|
|
||||||
</StepsForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UpdateForm;
|
|
|
@ -1,36 +0,0 @@
|
||||||
export type TableListItem = {
|
|
||||||
key: number;
|
|
||||||
disabled?: boolean;
|
|
||||||
href: string;
|
|
||||||
avatar: string;
|
|
||||||
name: string;
|
|
||||||
owner: string;
|
|
||||||
desc: string;
|
|
||||||
callNo: number;
|
|
||||||
status: string;
|
|
||||||
updatedAt: Date;
|
|
||||||
createdAt: Date;
|
|
||||||
progress: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TableListPagination = {
|
|
||||||
total: number;
|
|
||||||
pageSize: number;
|
|
||||||
current: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TableListData = {
|
|
||||||
list: TableListItem[];
|
|
||||||
pagination: Partial<TableListPagination>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TableListParams = {
|
|
||||||
status?: string;
|
|
||||||
name?: string;
|
|
||||||
desc?: string;
|
|
||||||
key?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
currentPage?: number;
|
|
||||||
filter?: Record<string, any[]>;
|
|
||||||
sorter?: Record<string, any>;
|
|
||||||
};
|
|
|
@ -1,324 +0,0 @@
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
|
||||||
import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components';
|
|
||||||
import {
|
|
||||||
FooterToolbar,
|
|
||||||
ModalForm,
|
|
||||||
PageContainer,
|
|
||||||
ProDescriptions,
|
|
||||||
ProFormText,
|
|
||||||
ProFormTextArea,
|
|
||||||
ProTable,
|
|
||||||
} from '@ant-design/pro-components';
|
|
||||||
import { Button, Drawer, Input, message } from 'antd';
|
|
||||||
import React, { useRef, useState } from 'react';
|
|
||||||
import type { FormValueType } from './components/UpdateForm';
|
|
||||||
import UpdateForm from './components/UpdateForm';
|
|
||||||
import type { TableListItem, TableListPagination } from './data';
|
|
||||||
import { addRule, removeRule, rule, updateRule } from './service';
|
|
||||||
/**
|
|
||||||
* 添加节点
|
|
||||||
*
|
|
||||||
* @param fields
|
|
||||||
*/
|
|
||||||
|
|
||||||
const handleAdd = async (fields: TableListItem) => {
|
|
||||||
const hide = message.loading('正在添加');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await addRule({ ...fields });
|
|
||||||
hide();
|
|
||||||
message.success('添加成功');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
hide();
|
|
||||||
message.error('添加失败请重试!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 更新节点
|
|
||||||
*
|
|
||||||
* @param fields
|
|
||||||
*/
|
|
||||||
|
|
||||||
const handleUpdate = async (fields: FormValueType, currentRow?: TableListItem) => {
|
|
||||||
const hide = message.loading('正在配置');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateRule({
|
|
||||||
...currentRow,
|
|
||||||
...fields,
|
|
||||||
});
|
|
||||||
hide();
|
|
||||||
message.success('配置成功');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
hide();
|
|
||||||
message.error('配置失败请重试!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* 删除节点
|
|
||||||
*
|
|
||||||
* @param selectedRows
|
|
||||||
*/
|
|
||||||
|
|
||||||
const handleRemove = async (selectedRows: TableListItem[]) => {
|
|
||||||
const hide = message.loading('正在删除');
|
|
||||||
if (!selectedRows) return true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await removeRule({
|
|
||||||
key: selectedRows.map((row) => row.key),
|
|
||||||
});
|
|
||||||
hide();
|
|
||||||
message.success('删除成功,即将刷新');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
hide();
|
|
||||||
message.error('删除失败,请重试');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const TableList: React.FC = () => {
|
|
||||||
/** 新建窗口的弹窗 */
|
|
||||||
const [createModalVisible, handleModalVisible] = useState<boolean>(false);
|
|
||||||
/** 分布更新窗口的弹窗 */
|
|
||||||
|
|
||||||
const [updateModalVisible, handleUpdateModalVisible] = useState<boolean>(false);
|
|
||||||
const [showDetail, setShowDetail] = useState<boolean>(false);
|
|
||||||
const actionRef = useRef<ActionType>();
|
|
||||||
const [currentRow, setCurrentRow] = useState<TableListItem>();
|
|
||||||
const [selectedRowsState, setSelectedRows] = useState<TableListItem[]>([]);
|
|
||||||
/** 国际化配置 */
|
|
||||||
|
|
||||||
const columns: ProColumns<TableListItem>[] = [
|
|
||||||
{
|
|
||||||
title: '规则名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
tip: '规则名称是唯一的 key',
|
|
||||||
render: (dom, entity) => {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentRow(entity);
|
|
||||||
setShowDetail(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{dom}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '描述',
|
|
||||||
dataIndex: 'desc',
|
|
||||||
valueType: 'textarea',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '服务调用次数',
|
|
||||||
dataIndex: 'callNo',
|
|
||||||
sorter: true,
|
|
||||||
hideInForm: true,
|
|
||||||
renderText: (val: string) => `${val}万`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
dataIndex: 'status',
|
|
||||||
hideInForm: true,
|
|
||||||
valueEnum: {
|
|
||||||
0: {
|
|
||||||
text: '关闭',
|
|
||||||
status: 'Default',
|
|
||||||
},
|
|
||||||
1: {
|
|
||||||
text: '运行中',
|
|
||||||
status: 'Processing',
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
text: '已上线',
|
|
||||||
status: 'Success',
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
text: '异常',
|
|
||||||
status: 'Error',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '上次调度时间',
|
|
||||||
sorter: true,
|
|
||||||
dataIndex: 'updatedAt',
|
|
||||||
valueType: 'dateTime',
|
|
||||||
renderFormItem: (item, { defaultRender, ...rest }, form) => {
|
|
||||||
const status = form.getFieldValue('status');
|
|
||||||
|
|
||||||
if (`${status}` === '0') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (`${status}` === '3') {
|
|
||||||
return <Input {...rest} placeholder="请输入异常原因!" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultRender(item);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
dataIndex: 'option',
|
|
||||||
valueType: 'option',
|
|
||||||
render: (_, record) => [
|
|
||||||
<a
|
|
||||||
key="config"
|
|
||||||
onClick={() => {
|
|
||||||
handleUpdateModalVisible(true);
|
|
||||||
setCurrentRow(record);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
配置
|
|
||||||
</a>,
|
|
||||||
<a key="subscribeAlert" href="https://procomponents.ant.design/">
|
|
||||||
订阅警报
|
|
||||||
</a>,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer>
|
|
||||||
<ProTable<TableListItem, TableListPagination>
|
|
||||||
headerTitle="查询表格"
|
|
||||||
actionRef={actionRef}
|
|
||||||
rowKey="key"
|
|
||||||
search={{
|
|
||||||
labelWidth: 120,
|
|
||||||
}}
|
|
||||||
toolBarRender={() => [
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
key="primary"
|
|
||||||
onClick={() => {
|
|
||||||
handleModalVisible(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusOutlined /> 新建
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
request={rule}
|
|
||||||
columns={columns}
|
|
||||||
rowSelection={{
|
|
||||||
onChange: (_, selectedRows) => {
|
|
||||||
setSelectedRows(selectedRows);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{selectedRowsState?.length > 0 && (
|
|
||||||
<FooterToolbar
|
|
||||||
extra={
|
|
||||||
<div>
|
|
||||||
已选择{' '}
|
|
||||||
<a
|
|
||||||
style={{
|
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedRowsState.length}
|
|
||||||
</a>{' '}
|
|
||||||
项
|
|
||||||
<span>
|
|
||||||
服务调用次数总计 {selectedRowsState.reduce((pre, item) => pre + item.callNo!, 0)} 万
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
await handleRemove(selectedRowsState);
|
|
||||||
setSelectedRows([]);
|
|
||||||
actionRef.current?.reloadAndRest?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
批量删除
|
|
||||||
</Button>
|
|
||||||
<Button type="primary">批量审批</Button>
|
|
||||||
</FooterToolbar>
|
|
||||||
)}
|
|
||||||
<ModalForm
|
|
||||||
title="新建规则"
|
|
||||||
width="400px"
|
|
||||||
open={createModalVisible}
|
|
||||||
onVisibleChange={handleModalVisible}
|
|
||||||
onFinish={async (value) => {
|
|
||||||
const success = await handleAdd(value as TableListItem);
|
|
||||||
if (success) {
|
|
||||||
handleModalVisible(false);
|
|
||||||
if (actionRef.current) {
|
|
||||||
actionRef.current.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProFormText
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '规则名称为必填项',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
width="md"
|
|
||||||
name="name"
|
|
||||||
/>
|
|
||||||
<ProFormTextArea width="md" name="desc" />
|
|
||||||
</ModalForm>
|
|
||||||
<UpdateForm
|
|
||||||
onSubmit={async (value) => {
|
|
||||||
const success = await handleUpdate(value, currentRow);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
handleUpdateModalVisible(false);
|
|
||||||
setCurrentRow(undefined);
|
|
||||||
|
|
||||||
if (actionRef.current) {
|
|
||||||
actionRef.current.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onCancel={() => {
|
|
||||||
handleUpdateModalVisible(false);
|
|
||||||
setCurrentRow(undefined);
|
|
||||||
}}
|
|
||||||
updateModalVisible={updateModalVisible}
|
|
||||||
values={currentRow || {}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Drawer
|
|
||||||
width={600}
|
|
||||||
open={showDetail}
|
|
||||||
onClose={() => {
|
|
||||||
setCurrentRow(undefined);
|
|
||||||
setShowDetail(false);
|
|
||||||
}}
|
|
||||||
closable={false}
|
|
||||||
>
|
|
||||||
{currentRow?.name && (
|
|
||||||
<ProDescriptions<TableListItem>
|
|
||||||
column={2}
|
|
||||||
title={currentRow?.name}
|
|
||||||
request={async () => ({
|
|
||||||
data: currentRow || {},
|
|
||||||
})}
|
|
||||||
params={{
|
|
||||||
id: currentRow?.name,
|
|
||||||
}}
|
|
||||||
columns={columns as ProDescriptionsItemProps<TableListItem>[]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableList;
|
|
|
@ -1,56 +0,0 @@
|
||||||
// @ts-ignore
|
|
||||||
/* eslint-disable */
|
|
||||||
import { request } from '@umijs/max';
|
|
||||||
import { TableListItem } from './data';
|
|
||||||
|
|
||||||
/** 获取规则列表 GET /api/rule */
|
|
||||||
export async function rule(
|
|
||||||
params: {
|
|
||||||
// query
|
|
||||||
/** 当前的页码 */
|
|
||||||
current?: number;
|
|
||||||
/** 页面的容量 */
|
|
||||||
pageSize?: number;
|
|
||||||
},
|
|
||||||
options?: { [key: string]: any },
|
|
||||||
) {
|
|
||||||
return request<{
|
|
||||||
data: TableListItem[];
|
|
||||||
/** 列表的内容总数 */
|
|
||||||
total?: number;
|
|
||||||
success?: boolean;
|
|
||||||
}>('/api/rule', {
|
|
||||||
method: 'GET',
|
|
||||||
params: {
|
|
||||||
...params,
|
|
||||||
},
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 新建规则 PUT /api/rule */
|
|
||||||
export async function updateRule(data: { [key: string]: any }, options?: { [key: string]: any }) {
|
|
||||||
return request<TableListItem>('/api/rule', {
|
|
||||||
data,
|
|
||||||
method: 'PUT',
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 新建规则 POST /api/rule */
|
|
||||||
export async function addRule(data: { [key: string]: any }, options?: { [key: string]: any }) {
|
|
||||||
return request<TableListItem>('/api/rule', {
|
|
||||||
data,
|
|
||||||
method: 'POST',
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 删除规则 DELETE /api/rule */
|
|
||||||
export async function removeRule(data: { key: number[] }, options?: { [key: string]: any }) {
|
|
||||||
return request<Record<string, any>>('/api/rule', {
|
|
||||||
data,
|
|
||||||
method: 'DELETE',
|
|
||||||
...(options || {}),
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
// src/pages/list/basic-list/components/AddTimeLineItemModal.tsx
|
// src/pages/list/basic-list/components/AddTimeLineItemModal.tsx
|
||||||
import { addStoryItem } from '@/pages/list/basic-list/service';
|
|
||||||
import chinaRegion, { code2Location } from '@/commonConstant/chinaRegion';
|
import chinaRegion, { code2Location } from '@/commonConstant/chinaRegion';
|
||||||
|
import { addStoryItem } from '@/pages/story/service';
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import { useRequest } from '@umijs/max';
|
import { useRequest } from '@umijs/max';
|
||||||
import { Button, Cascader, DatePicker, Form, Input, message, Modal, Upload } from 'antd';
|
import { Button, Cascader, DatePicker, Form, Input, message, Modal, Upload } from 'antd';
|
||||||
|
@ -13,24 +13,26 @@ interface ModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onOk: () => void;
|
onOk: () => void;
|
||||||
lineId: string | number | undefined;
|
storyId: string | number | undefined;
|
||||||
initialValues?: any;
|
initialValues?: any;
|
||||||
isRoot: boolean; // 是否根节点
|
storyItemId?: string; // 是否根节点
|
||||||
|
option: 'add' | 'edit' | 'addSubItem' | 'editSubItem';
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
onCancel,
|
onCancel,
|
||||||
onOk,
|
onOk,
|
||||||
lineId,
|
storyId,
|
||||||
initialValues,
|
initialValues,
|
||||||
isRoot = true,
|
storyItemId,
|
||||||
|
option,
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [fileList, setFileList] = useState<any[]>([]);
|
const [fileList, setFileList] = useState<any[]>([]);
|
||||||
const [imageList, setImageList] = useState<any[]>(initialValues?.images || []);
|
const [imageList, setImageList] = useState<any[]>(initialValues?.images || []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialValues) {
|
if (initialValues && option === 'edit') {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
title: initialValues.title,
|
title: initialValues.title,
|
||||||
storyItemTime: initialValues.date ? moment(initialValues.date) : undefined,
|
storyItemTime: initialValues.date ? moment(initialValues.date) : undefined,
|
||||||
|
@ -40,7 +42,7 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
||||||
images: initialValues.images?.map((url) => ({ url })) || [],
|
images: initialValues.images?.map((url) => ({ url })) || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [initialValues]);
|
}, [initialValues, option]);
|
||||||
const { run: submitItem, loading } = useRequest((newItem) => addStoryItem(newItem), {
|
const { run: submitItem, loading } = useRequest((newItem) => addStoryItem(newItem), {
|
||||||
manual: true,
|
manual: true,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
@ -64,9 +66,9 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
||||||
...values,
|
...values,
|
||||||
id: initialValues?.id || Date.now(),
|
id: initialValues?.id || Date.now(),
|
||||||
storyItemTime: dayjs(values.date).format('YYYY-MM-DDTHH:mm:ss'),
|
storyItemTime: dayjs(values.date).format('YYYY-MM-DDTHH:mm:ss'),
|
||||||
masterItemId: lineId,
|
masterItemId: initialValues.masterItemId,
|
||||||
subItems: initialValues?.subItems || [],
|
subItems: initialValues?.subItems || [],
|
||||||
isRoot: isRoot ? 1 : 0,
|
storyInstanceId: storyId,
|
||||||
location,
|
location,
|
||||||
};
|
};
|
||||||
delete newItem.cover;
|
delete newItem.cover;
|
||||||
|
@ -151,7 +153,7 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
{!isRoot && <Form.Item label={'主时间点'}>{lineId}</Form.Item>}
|
{['editSubItem', 'addSubItem'].includes( option) && <Form.Item label={'主时间点'}>{storyItemId}</Form.Item>}
|
||||||
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
|
<Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}>
|
||||||
<Input placeholder="请输入标题" />
|
<Input placeholder="请输入标题" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@ -185,7 +187,9 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
||||||
{/* 新增:时刻图库 */}
|
{/* 新增:时刻图库 */}
|
||||||
<Form.Item label="时刻图库(多图)" name="images">
|
<Form.Item label="时刻图库(多图)" name="images">
|
||||||
<Upload {...uploadImagesProps} maxCount={5}>
|
<Upload {...uploadImagesProps} maxCount={5}>
|
||||||
<Button size={"small"} icon={<UploadOutlined />}>上传多图</Button>
|
<Button size={'small'} icon={<UploadOutlined />}>
|
||||||
|
上传多图
|
||||||
|
</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,8 +1,8 @@
|
||||||
import TimelineImage from '@/components/TimelineImage';
|
import TimelineImage from '@/components/TimelineImage';
|
||||||
import AddTimeLineItemModal from '@/pages/list/basic-list/components/AddTimeLineItemModal';
|
import AddTimeLineItemModal from '@/pages/story/components/AddTimeLineItemModal';
|
||||||
import SubTimeLineItemModal from '@/pages/list/basic-list/components/SubTimeLineItemModal';
|
import SubTimeLineItemModal from '@/pages/story/components/SubTimeLineItemModal';
|
||||||
import { StoryItem } from '@/pages/list/basic-list/data';
|
import { StoryItem } from '@/pages/story/data';
|
||||||
import { queryStoryItemImages } from '@/pages/list/basic-list/service';
|
import { queryStoryItemImages } from '@/pages/story/service';
|
||||||
import { EditOutlined, PlusCircleOutlined } from '@ant-design/icons';
|
import { EditOutlined, PlusCircleOutlined } from '@ant-design/icons';
|
||||||
import { useRequest } from '@umijs/max';
|
import { useRequest } from '@umijs/max';
|
||||||
import { Button, Drawer, Space } from 'antd';
|
import { Button, Drawer, Space } from 'antd';
|
||||||
|
@ -30,7 +30,7 @@ const TimelineItemDrawer = (props: Props) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
console.log(storyItem);
|
console.log(storyItem);
|
||||||
run(storyItem.itemId);
|
run(storyItem.instanceId);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
const closeDrawer = () => {
|
const closeDrawer = () => {
|
||||||
|
@ -120,8 +120,8 @@ const TimelineItemDrawer = (props: Props) => {
|
||||||
visible={openAddSubItemModal}
|
visible={openAddSubItemModal}
|
||||||
onOk={handleAddSubItem}
|
onOk={handleAddSubItem}
|
||||||
onCancel={() => setOpenAddSubItemModal(false)}
|
onCancel={() => setOpenAddSubItemModal(false)}
|
||||||
lineId={storyItem.itemId}
|
lineId={storyItem.storyInstanceId}
|
||||||
isRoot={false}
|
storyItemId={storyItem.instanceId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 编辑主时间点模态框 */}
|
{/* 编辑主时间点模态框 */}
|
|
@ -50,14 +50,14 @@ export interface ErrorResponse extends BaseResponse {
|
||||||
}
|
}
|
||||||
export interface StoryItem {
|
export interface StoryItem {
|
||||||
id?: number;
|
id?: number;
|
||||||
itemId?: string;
|
instanceId: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
storyInstanceId: string;
|
||||||
masterItemId?: string;
|
masterItemId?: string;
|
||||||
description: string;
|
description: string;
|
||||||
storyItemTime: string; // YYYY-MM-DD
|
storyItemTime: string; // YYYY-MM-DD
|
||||||
createTime: string; // YYYY-MM-DD
|
createTime: string; // YYYY-MM-DD
|
||||||
updateTime: string; // YYYY-MM-DD
|
updateTime: string; // YYYY-MM-DD
|
||||||
time?: string; // HH:mm (可选)
|
|
||||||
location?: string;
|
location?: string;
|
||||||
coverInstanceId?: string; // 封面图
|
coverInstanceId?: string; // 封面图
|
||||||
images?: string[]; // 多张图片
|
images?: string[]; // 多张图片
|
|
@ -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;
|
|
@ -1,5 +1,6 @@
|
||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
import { StoryItem, StoryType } from './data.d';
|
import { StoryItem, StoryType } from './data.d';
|
||||||
|
import {CommonResponse} from "@/types/common";
|
||||||
|
|
||||||
type ParamsType = {
|
type ParamsType = {
|
||||||
count?: number;
|
count?: number;
|
||||||
|
@ -40,7 +41,11 @@ export async function updateStory(params: ParamsType): Promise<{ data: { list: S
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
export async function queryStoryDetail(itemId: string): Promise<{ data: StoryType }> {
|
||||||
|
return request(`/story/${itemId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
export async function addStoryItem(params: FormData): Promise<any> {
|
export async function addStoryItem(params: FormData): Promise<any> {
|
||||||
return request(`/story/item`, {
|
return request(`/story/item`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -50,26 +55,36 @@ export async function addStoryItem(params: FormData): Promise<any> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function queryStoryItem(masterItemId: string): Promise<{ data: StoryItem[] }> {
|
export async function queryStoryItem(storyInstanceId: string): Promise<{ data: StoryItem[] }> {
|
||||||
return request(`/story/item/list`, {
|
return request(`/story/item/list`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: {
|
params: {
|
||||||
masterItemId,
|
storyInstanceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function queryStoryItemDetail(itemId: string): Promise<{ data: StoryItem[] }> {
|
export async function queryStoryItemDetail(itemId: string): Promise<{ data: StoryItem }> {
|
||||||
return request(`/story/item/${itemId}`, {
|
return request(`/story/item/${itemId}`, {
|
||||||
method: 'GET',
|
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[] }> {
|
export async function queryStoryItemImages(itemId: string): Promise<{ data: string[] }> {
|
||||||
return request(`/story/item/images/${itemId}`, {
|
return request(`/story/item/images/${itemId}`, {
|
||||||
method: 'GET',
|
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> {
|
export async function fetchImage(imageInstanceId: string): Promise<any> {
|
||||||
return request(`/file/image/${imageInstanceId}`, {
|
return request(`/file/image/${imageInstanceId}`, {
|
|
@ -148,6 +148,80 @@ const useStyles = createStyles(({ token }) => {
|
||||||
gap: 8,
|
gap: 8,
|
||||||
marginBottom: 16,
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue