Browse Source
chore: remove unused dependencies from package.json chore: merge all-blocks chore: remove unused mock filespull/11531/head
158 changed files with 20 additions and 13384 deletions
@ -1,210 +0,0 @@ |
|||
import dayjs from 'dayjs'; |
|||
import type { Request, Response } from 'express'; |
|||
import type { AnalysisData, DataItem, RadarData } from '../src/pages/dashboard/analysis/data'; |
|||
|
|||
// mock data
|
|||
const visitData: DataItem[] = []; |
|||
const beginDay = new Date().getTime(); |
|||
|
|||
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]; |
|||
for (let i = 0; i < fakeY.length; i += 1) { |
|||
visitData.push({ |
|||
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'), |
|||
y: fakeY[i], |
|||
}); |
|||
} |
|||
|
|||
const visitData2 = []; |
|||
const fakeY2 = [1, 6, 4, 8, 3, 7, 2]; |
|||
for (let i = 0; i < fakeY2.length; i += 1) { |
|||
visitData2.push({ |
|||
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'), |
|||
y: fakeY2[i], |
|||
}); |
|||
} |
|||
|
|||
const salesData = []; |
|||
for (let i = 0; i < 12; i += 1) { |
|||
salesData.push({ |
|||
x: `${i + 1}月`, |
|||
y: Math.floor(Math.random() * 1000) + 200, |
|||
}); |
|||
} |
|||
const searchData = []; |
|||
for (let i = 0; i < 50; i += 1) { |
|||
searchData.push({ |
|||
index: i + 1, |
|||
keyword: `搜索关键词-${i}`, |
|||
count: Math.floor(Math.random() * 1000), |
|||
range: Math.floor(Math.random() * 100), |
|||
status: Math.floor((Math.random() * 10) % 2), |
|||
}); |
|||
} |
|||
const salesTypeData = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 4544, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 3321, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 3113, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 2341, |
|||
}, |
|||
{ |
|||
x: '母婴产品', |
|||
y: 1231, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 1231, |
|||
}, |
|||
]; |
|||
|
|||
const salesTypeDataOnline = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 244, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 321, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 311, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 41, |
|||
}, |
|||
{ |
|||
x: '母婴产品', |
|||
y: 121, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 111, |
|||
}, |
|||
]; |
|||
|
|||
const salesTypeDataOffline = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 99, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 188, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 344, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 255, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 65, |
|||
}, |
|||
]; |
|||
|
|||
const offlineData = []; |
|||
for (let i = 0; i < 10; i += 1) { |
|||
offlineData.push({ |
|||
name: `Stores ${i}`, |
|||
cvr: Math.ceil(Math.random() * 9) / 10, |
|||
}); |
|||
} |
|||
const offlineChartData = []; |
|||
for (let i = 0; i < 20; i += 1) { |
|||
const date = dayjs(new Date().getTime() + 1000 * 60 * 30 * i).format('HH:mm'); |
|||
offlineChartData.push({ |
|||
date, |
|||
type: '客流量', |
|||
value: Math.floor(Math.random() * 100) + 10, |
|||
}); |
|||
offlineChartData.push({ |
|||
date, |
|||
type: '支付笔数', |
|||
value: Math.floor(Math.random() * 100) + 10, |
|||
}); |
|||
} |
|||
|
|||
const radarOriginData = [ |
|||
{ |
|||
name: '个人', |
|||
ref: 10, |
|||
koubei: 8, |
|||
output: 4, |
|||
contribute: 5, |
|||
hot: 7, |
|||
}, |
|||
{ |
|||
name: '团队', |
|||
ref: 3, |
|||
koubei: 9, |
|||
output: 6, |
|||
contribute: 3, |
|||
hot: 1, |
|||
}, |
|||
{ |
|||
name: '部门', |
|||
ref: 4, |
|||
koubei: 1, |
|||
output: 6, |
|||
contribute: 5, |
|||
hot: 7, |
|||
}, |
|||
]; |
|||
|
|||
const radarData: RadarData[] = []; |
|||
const radarTitleMap = { |
|||
ref: '引用', |
|||
koubei: '口碑', |
|||
output: '产量', |
|||
contribute: '贡献', |
|||
hot: '热度', |
|||
}; |
|||
radarOriginData.forEach((item) => { |
|||
Object.keys(item).forEach((key) => { |
|||
if (key !== 'name') { |
|||
radarData.push({ |
|||
name: item.name, |
|||
label: radarTitleMap[key as 'ref'], |
|||
value: item[key as 'ref'], |
|||
}); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
const getFakeChartData: AnalysisData = { |
|||
visitData, |
|||
visitData2, |
|||
salesData, |
|||
searchData, |
|||
offlineData, |
|||
offlineChartData, |
|||
salesTypeData, |
|||
salesTypeDataOnline, |
|||
salesTypeDataOffline, |
|||
radarData, |
|||
}; |
|||
|
|||
const fakeChartData = (_: Request, res: Response) => { |
|||
return res.json({ |
|||
data: getFakeChartData, |
|||
}); |
|||
}; |
|||
|
|||
export default { |
|||
'GET /api/fake_analysis_chart_data': fakeChartData, |
|||
}; |
|||
@ -1,418 +0,0 @@ |
|||
import dayjs from 'dayjs'; |
|||
import type { Request, Response } from 'express'; |
|||
import type { DataItem, OfflineDataType } from '../src/pages/dashboard/workplace/data.d'; |
|||
|
|||
export type SearchDataType = { |
|||
index: number; |
|||
keyword: string; |
|||
count: number; |
|||
range: number; |
|||
status: number; |
|||
}; |
|||
|
|||
// mock data
|
|||
const visitData: DataItem[] = []; |
|||
const beginDay = new Date().getTime(); |
|||
|
|||
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]; |
|||
for (let i = 0; i < fakeY.length; i += 1) { |
|||
visitData.push({ |
|||
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'), |
|||
y: fakeY[i], |
|||
}); |
|||
} |
|||
|
|||
const visitData2: DataItem[] = []; |
|||
const fakeY2 = [1, 6, 4, 8, 3, 7, 2]; |
|||
for (let i = 0; i < fakeY2.length; i += 1) { |
|||
visitData2.push({ |
|||
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'), |
|||
y: fakeY2[i], |
|||
}); |
|||
} |
|||
|
|||
const salesData: DataItem[] = []; |
|||
for (let i = 0; i < 12; i += 1) { |
|||
salesData.push({ |
|||
x: `${i + 1}月`, |
|||
y: Math.floor(Math.random() * 1000) + 200, |
|||
}); |
|||
} |
|||
const searchData: SearchDataType[] = []; |
|||
for (let i = 0; i < 50; i += 1) { |
|||
searchData.push({ |
|||
index: i + 1, |
|||
keyword: `搜索关键词-${i}`, |
|||
count: Math.floor(Math.random() * 1000), |
|||
range: Math.floor(Math.random() * 100), |
|||
status: Math.floor((Math.random() * 10) % 2), |
|||
}); |
|||
} |
|||
const salesTypeData = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 4544, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 3321, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 3113, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 2341, |
|||
}, |
|||
{ |
|||
x: '母婴产品', |
|||
y: 1231, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 1231, |
|||
}, |
|||
]; |
|||
|
|||
const salesTypeDataOnline = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 244, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 321, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 311, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 41, |
|||
}, |
|||
{ |
|||
x: '母婴产品', |
|||
y: 121, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 111, |
|||
}, |
|||
]; |
|||
|
|||
const salesTypeDataOffline = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 99, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 188, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 344, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 255, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 65, |
|||
}, |
|||
]; |
|||
|
|||
const offlineData: OfflineDataType[] = []; |
|||
for (let i = 0; i < 10; i += 1) { |
|||
offlineData.push({ |
|||
name: `Stores ${i}`, |
|||
cvr: Math.ceil(Math.random() * 9) / 10, |
|||
}); |
|||
} |
|||
const offlineChartData: DataItem[] = []; |
|||
for (let i = 0; i < 20; i += 1) { |
|||
offlineChartData.push({ |
|||
x: new Date().getTime() + 1000 * 60 * 30 * i, |
|||
y1: Math.floor(Math.random() * 100) + 10, |
|||
y2: Math.floor(Math.random() * 100) + 10, |
|||
}); |
|||
} |
|||
|
|||
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 avatars2 = [ |
|||
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/psOgztMplJMGpVEqfcgF.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/ZpBqSxLxVEXfcUNoPKrz.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/laiEnJdGHVOhJrUShBaJ.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/UrQsqscbKEpNuJcvBZBu.png', |
|||
]; |
|||
|
|||
const getNotice = (_: Request, res: Response) => { |
|||
res.json({ |
|||
data: [ |
|||
{ |
|||
id: 'xxx1', |
|||
title: titles[0], |
|||
logo: avatars[0], |
|||
description: '那是一种内在的东西,他们到达不了,也无法触及的', |
|||
updatedAt: new Date(), |
|||
member: '科学搬砖组', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx2', |
|||
title: titles[1], |
|||
logo: avatars[1], |
|||
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的', |
|||
updatedAt: new Date('2017-07-24'), |
|||
member: '全组都是吴彦祖', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx3', |
|||
title: titles[2], |
|||
logo: avatars[2], |
|||
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆', |
|||
updatedAt: new Date(), |
|||
member: '中二少女团', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx4', |
|||
title: titles[3], |
|||
logo: avatars[3], |
|||
description: '那时候我只会想自己想要什么,从不想自己拥有什么', |
|||
updatedAt: new Date('2017-07-23'), |
|||
member: '程序员日常', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx5', |
|||
title: titles[4], |
|||
logo: avatars[4], |
|||
description: '凛冬将至', |
|||
updatedAt: new Date('2017-07-23'), |
|||
member: '高逼格设计天团', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx6', |
|||
title: titles[5], |
|||
logo: avatars[5], |
|||
description: '生命就像一盒巧克力,结果往往出人意料', |
|||
updatedAt: new Date('2017-07-23'), |
|||
member: '骗你来学计算机', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
], |
|||
}); |
|||
}; |
|||
|
|||
const getActivities = (_: Request, res: Response) => { |
|||
res.json({ |
|||
data: [ |
|||
{ |
|||
id: 'trend-1', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '曲丽丽', |
|||
avatar: avatars2[0], |
|||
}, |
|||
group: { |
|||
name: '高逼格设计天团', |
|||
link: 'http://github.com/', |
|||
}, |
|||
project: { |
|||
name: '六月迭代', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '在 @{group} 新建项目 @{project}', |
|||
}, |
|||
{ |
|||
id: 'trend-2', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '付小小', |
|||
avatar: avatars2[1], |
|||
}, |
|||
group: { |
|||
name: '高逼格设计天团', |
|||
link: 'http://github.com/', |
|||
}, |
|||
project: { |
|||
name: '六月迭代', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '在 @{group} 新建项目 @{project}', |
|||
}, |
|||
{ |
|||
id: 'trend-3', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '林东东', |
|||
avatar: avatars2[2], |
|||
}, |
|||
group: { |
|||
name: '中二少女团', |
|||
link: 'http://github.com/', |
|||
}, |
|||
project: { |
|||
name: '六月迭代', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '在 @{group} 新建项目 @{project}', |
|||
}, |
|||
{ |
|||
id: 'trend-4', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '周星星', |
|||
avatar: avatars2[4], |
|||
}, |
|||
project: { |
|||
name: '5 月日常迭代', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '将 @{project} 更新至已发布状态', |
|||
}, |
|||
{ |
|||
id: 'trend-5', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '朱偏右', |
|||
avatar: avatars2[3], |
|||
}, |
|||
project: { |
|||
name: '工程效能', |
|||
link: 'http://github.com/', |
|||
}, |
|||
comment: { |
|||
name: '留言', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '在 @{project} 发布了 @{comment}', |
|||
}, |
|||
{ |
|||
id: 'trend-6', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '乐哥', |
|||
avatar: avatars2[5], |
|||
}, |
|||
group: { |
|||
name: '程序员日常', |
|||
link: 'http://github.com/', |
|||
}, |
|||
project: { |
|||
name: '品牌迭代', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '在 @{group} 新建项目 @{project}', |
|||
}, |
|||
], |
|||
}); |
|||
}; |
|||
|
|||
const radarOriginData = [ |
|||
{ |
|||
name: '个人', |
|||
ref: 10, |
|||
koubei: 8, |
|||
output: 4, |
|||
contribute: 5, |
|||
hot: 7, |
|||
}, |
|||
{ |
|||
name: '团队', |
|||
ref: 3, |
|||
koubei: 9, |
|||
output: 6, |
|||
contribute: 3, |
|||
hot: 1, |
|||
}, |
|||
{ |
|||
name: '部门', |
|||
ref: 4, |
|||
koubei: 1, |
|||
output: 6, |
|||
contribute: 5, |
|||
hot: 7, |
|||
}, |
|||
]; |
|||
|
|||
const radarData: any[] = []; |
|||
const radarTitleMap = { |
|||
ref: '引用', |
|||
koubei: '口碑', |
|||
output: '产量', |
|||
contribute: '贡献', |
|||
hot: '热度', |
|||
}; |
|||
radarOriginData.forEach((item) => { |
|||
Object.keys(item).forEach((key) => { |
|||
if (key !== 'name') { |
|||
radarData.push({ |
|||
name: item.name, |
|||
label: radarTitleMap[key as 'ref'], |
|||
value: item[key as 'ref'], |
|||
}); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
const getChartData = (_: Request, res: Response) => { |
|||
res.json({ |
|||
data: { |
|||
visitData, |
|||
visitData2, |
|||
salesData, |
|||
searchData, |
|||
offlineData, |
|||
offlineChartData, |
|||
salesTypeData, |
|||
salesTypeDataOnline, |
|||
salesTypeDataOffline, |
|||
radarData, |
|||
}, |
|||
}); |
|||
}; |
|||
|
|||
export default { |
|||
'GET /api/project/notice': getNotice, |
|||
'GET /api/activities': getActivities, |
|||
'GET /api/fake_workplace_chart_data': getChartData, |
|||
}; |
|||
@ -1,69 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
avatarHolder: { |
|||
marginBottom: '24px', |
|||
textAlign: 'center', |
|||
'& > img': { width: '104px', height: '104px', marginBottom: '20px' }, |
|||
}, |
|||
name: { |
|||
marginBottom: '4px', |
|||
color: token.colorTextHeading, |
|||
fontWeight: '500', |
|||
fontSize: '20px', |
|||
lineHeight: '28px', |
|||
}, |
|||
detail: { |
|||
p: { |
|||
position: 'relative', |
|||
marginBottom: '8px', |
|||
paddingLeft: '26px', |
|||
'&:last-child': { |
|||
marginBottom: '0', |
|||
}, |
|||
}, |
|||
i: { |
|||
position: 'absolute', |
|||
top: '4px', |
|||
left: '0', |
|||
width: '14px', |
|||
height: '14px', |
|||
}, |
|||
}, |
|||
tagsTitle: { |
|||
marginBottom: '12px', |
|||
color: token.colorTextHeading, |
|||
fontWeight: '500', |
|||
}, |
|||
teamTitle: { |
|||
marginBottom: '12px', |
|||
color: token.colorTextHeading, |
|||
fontWeight: '500', |
|||
}, |
|||
tags: { |
|||
'.ant-tag': { marginBottom: '8px' }, |
|||
}, |
|||
team: { |
|||
'.ant-avatar': { marginRight: '12px' }, |
|||
a: { |
|||
display: 'block', |
|||
marginBottom: '24px', |
|||
overflow: 'hidden', |
|||
color: token.colorText, |
|||
whiteSpace: 'nowrap', |
|||
textOverflow: 'ellipsis', |
|||
wordBreak: 'break-all', |
|||
transition: 'color 0.3s', |
|||
'&:hover': { |
|||
color: token.colorPrimary, |
|||
}, |
|||
}, |
|||
}, |
|||
tabsCard: { |
|||
'.ant-card-head': { padding: '0 16px' }, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,249 +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 = [ |
|||
'付小小', |
|||
'曲丽丽', |
|||
'林东东', |
|||
'周星星', |
|||
'吴加好', |
|||
'朱偏右', |
|||
'鱼酱', |
|||
'乐哥', |
|||
'谭小仪', |
|||
'仲尼', |
|||
]; |
|||
|
|||
// 当前用户信息
|
|||
const currentUseDetail = { |
|||
name: 'Serati Ma', |
|||
avatar: |
|||
'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png', |
|||
userid: '00000001', |
|||
email: 'antdesign@alipay.com', |
|||
signature: '海纳百川,有容乃大', |
|||
title: '交互专家', |
|||
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED', |
|||
tags: [ |
|||
{ |
|||
key: '0', |
|||
label: '很有想法的', |
|||
}, |
|||
{ |
|||
key: '1', |
|||
label: '专注设计', |
|||
}, |
|||
{ |
|||
key: '2', |
|||
label: '辣~', |
|||
}, |
|||
{ |
|||
key: '3', |
|||
label: '大长腿', |
|||
}, |
|||
{ |
|||
key: '4', |
|||
label: '川妹子', |
|||
}, |
|||
{ |
|||
key: '5', |
|||
label: '海纳百川', |
|||
}, |
|||
], |
|||
notice: [ |
|||
{ |
|||
id: 'xxx1', |
|||
title: titles[0], |
|||
logo: avatars[0], |
|||
description: '那是一种内在的东西,他们到达不了,也无法触及的', |
|||
updatedAt: new Date(), |
|||
member: '科学搬砖组', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx2', |
|||
title: titles[1], |
|||
logo: avatars[1], |
|||
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的', |
|||
updatedAt: new Date('2017-07-24'), |
|||
member: '全组都是吴彦祖', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx3', |
|||
title: titles[2], |
|||
logo: avatars[2], |
|||
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆', |
|||
updatedAt: new Date(), |
|||
member: '中二少女团', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx4', |
|||
title: titles[3], |
|||
logo: avatars[3], |
|||
description: '那时候我只会想自己想要什么,从不想自己拥有什么', |
|||
updatedAt: new Date('2017-07-23'), |
|||
member: '程序员日常', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx5', |
|||
title: titles[4], |
|||
logo: avatars[4], |
|||
description: '凛冬将至', |
|||
updatedAt: new Date('2017-07-23'), |
|||
member: '高逼格设计天团', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx6', |
|||
title: titles[5], |
|||
logo: avatars[5], |
|||
description: '生命就像一盒巧克力,结果往往出人意料', |
|||
updatedAt: new Date('2017-07-23'), |
|||
member: '骗你来学计算机', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
], |
|||
notifyCount: 12, |
|||
unreadCount: 11, |
|||
country: 'China', |
|||
geographic: { |
|||
province: { |
|||
label: '浙江省', |
|||
key: '330000', |
|||
}, |
|||
city: { |
|||
label: '杭州市', |
|||
key: '330100', |
|||
}, |
|||
}, |
|||
address: '西湖区工专路 77 号', |
|||
phone: '0752-268888888', |
|||
}; |
|||
|
|||
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(Date.now() - 1000 * 60 * 60 * 2 * i).getTime(), |
|||
createdAt: new Date(Date.now() - 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 || 5; |
|||
|
|||
const result = fakeList(count); |
|||
return res.json({ |
|||
data: { |
|||
list: result, |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
// 获取用户信息
|
|||
function getCurrentUser(_req: Request, res: Response) { |
|||
return res.json({ |
|||
data: currentUseDetail, |
|||
}); |
|||
} |
|||
|
|||
export default { |
|||
'GET /api/fake_list_Detail': getFakeList, |
|||
// 支持值为 Object 和 Array
|
|||
'GET /api/currentUserDetail': getCurrentUser, |
|||
}; |
|||
@ -1,43 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
filterCardList: { |
|||
marginBottom: '-24px', |
|||
'.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,31 +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', |
|||
display: 'flex', |
|||
gap: '8px', |
|||
alignItems: 'center', |
|||
'& > em': { |
|||
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'; |
|||
export type ApplicationsProps = { |
|||
data: { |
|||
content?: string; |
|||
updatedAt?: any; |
|||
avatar?: string; |
|||
owner?: string; |
|||
href?: string; |
|||
}; |
|||
}; |
|||
const ArticleListContent: React.FC<ApplicationsProps> = ({ |
|||
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,14 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
articleList: { |
|||
'.ant-list-item:first-child': { paddingTop: '0' }, |
|||
}, |
|||
listItemMetaTitle: { |
|||
color: token.colorTextHeading, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,70 +0,0 @@ |
|||
import { LikeOutlined, MessageFilled, StarTwoTone } from '@ant-design/icons'; |
|||
import { useRequest } from '@umijs/max'; |
|||
import { List, Tag } from 'antd'; |
|||
import React from 'react'; |
|||
import type { ListItemDataType } from '../../data.d'; |
|||
import { queryFakeList } from '../../service'; |
|||
import ArticleListContent from '../ArticleListContent'; |
|||
import useStyles from './index.style'; |
|||
|
|||
const Articles: React.FC = () => { |
|||
const { styles } = useStyles(); |
|||
const IconText: React.FC<{ |
|||
icon: React.ReactNode; |
|||
text: React.ReactNode; |
|||
}> = ({ icon, text }) => ( |
|||
<span> |
|||
{icon} {text} |
|||
</span> |
|||
); |
|||
|
|||
// 获取tab列表数据
|
|||
const { data: listData } = useRequest(() => { |
|||
return queryFakeList({ |
|||
count: 30, |
|||
}); |
|||
}); |
|||
return ( |
|||
<List<ListItemDataType> |
|||
size="large" |
|||
className={styles.articleList} |
|||
rowKey="id" |
|||
itemLayout="vertical" |
|||
dataSource={listData?.list || []} |
|||
style={{ |
|||
margin: '0 -24px', |
|||
}} |
|||
renderItem={(item) => ( |
|||
<List.Item |
|||
key={item.id} |
|||
actions={[ |
|||
<IconText key="star" icon={<StarTwoTone />} text={item.star} />, |
|||
<IconText key="like" icon={<LikeOutlined />} text={item.like} />, |
|||
<IconText |
|||
key="message" |
|||
icon={<MessageFilled />} |
|||
text={item.message} |
|||
/>, |
|||
]} |
|||
> |
|||
<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> |
|||
)} |
|||
/> |
|||
); |
|||
}; |
|||
export default Articles; |
|||
@ -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,89 +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 = (styles: any, size?: SizeType | 'mini') => |
|||
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(styles, size); |
|||
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(styles, size); |
|||
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,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,65 +0,0 @@ |
|||
import { useRequest } from '@umijs/max'; |
|||
import { Card, List } from 'antd'; |
|||
import dayjs from 'dayjs'; |
|||
import relativeTime from 'dayjs/plugin/relativeTime'; |
|||
import React from 'react'; |
|||
import type { ListItemDataType } from '../../data.d'; |
|||
import { queryFakeList } from '../../service'; |
|||
import AvatarList from '../AvatarList'; |
|||
import useStyles from './index.style'; |
|||
|
|||
dayjs.extend(relativeTime); |
|||
const Projects: React.FC = () => { |
|||
const { styles } = useStyles(); |
|||
// 获取tab列表数据
|
|||
const { data: listData } = useRequest(() => { |
|||
return queryFakeList({ |
|||
count: 30, |
|||
}); |
|||
}); |
|||
return ( |
|||
<List<ListItemDataType> |
|||
className={styles.coverCardList} |
|||
rowKey="id" |
|||
grid={{ |
|||
gutter: 24, |
|||
xxl: 3, |
|||
xl: 2, |
|||
lg: 2, |
|||
md: 2, |
|||
sm: 2, |
|||
xs: 1, |
|||
}} |
|||
dataSource={listData?.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={item.subDescription} |
|||
/> |
|||
<div className={styles.cardItemContent}> |
|||
<span>{dayjs(item.updatedAt).fromNow()}</span> |
|||
<div className={styles.avatarList}> |
|||
<AvatarList size="small"> |
|||
{item.members.map((member) => ( |
|||
<AvatarList.Item |
|||
key={`${item.id}-avatar-${member.id}`} |
|||
src={member.avatar} |
|||
tips={member.name} |
|||
/> |
|||
))} |
|||
</AvatarList> |
|||
</div> |
|||
</div> |
|||
</Card> |
|||
</List.Item> |
|||
)} |
|||
/> |
|||
); |
|||
}; |
|||
export default Projects; |
|||
@ -1,75 +0,0 @@ |
|||
export type tabKeyType = 'articles' | 'applications' | 'projects'; |
|||
export interface TagType { |
|||
key: string; |
|||
label: string; |
|||
} |
|||
|
|||
export type GeographicType = { |
|||
province: { |
|||
label: string; |
|||
key: string; |
|||
}; |
|||
city: { |
|||
label: string; |
|||
key: string; |
|||
}; |
|||
}; |
|||
|
|||
export type NoticeType = { |
|||
id: string; |
|||
title: string; |
|||
logo: string; |
|||
description: string; |
|||
updatedAt: string; |
|||
member: string; |
|||
href: string; |
|||
memberLink: string; |
|||
}; |
|||
|
|||
export type CurrentUser = { |
|||
name: string; |
|||
avatar: string; |
|||
userid: string; |
|||
notice: NoticeType[]; |
|||
email: string; |
|||
signature: string; |
|||
title: string; |
|||
group: string; |
|||
tags: TagType[]; |
|||
notifyCount: number; |
|||
unreadCount: number; |
|||
country: string; |
|||
geographic: GeographicType; |
|||
address: string; |
|||
phone: string; |
|||
}; |
|||
|
|||
export type Member = { |
|||
avatar: string; |
|||
name: string; |
|||
id: string; |
|||
}; |
|||
|
|||
export type 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,14 +0,0 @@ |
|||
import { request } from '@umijs/max'; |
|||
import type { CurrentUser, ListItemDataType } from './data.d'; |
|||
|
|||
export async function queryCurrent(): Promise<{ data: CurrentUser }> { |
|||
return request('/api/currentUserDetail'); |
|||
} |
|||
|
|||
export async function queryFakeList(params: { |
|||
count: number; |
|||
}): Promise<{ data: { list: ListItemDataType[] } }> { |
|||
return request('/api/fake_list_Detail', { |
|||
params, |
|||
}); |
|||
} |
|||
@ -1,79 +0,0 @@ |
|||
import type { Request, Response } from 'express'; |
|||
|
|||
const city = require('./geographic/city.json'); |
|||
const province = require('./geographic/province.json'); |
|||
|
|||
function getProvince(_: Request, res: Response) { |
|||
return res.json({ |
|||
data: province, |
|||
}); |
|||
} |
|||
|
|||
function getCity(req: Request, res: Response) { |
|||
return res.json({ |
|||
data: city[req.params.province], |
|||
}); |
|||
} |
|||
|
|||
function getCurrentUse(_req: Request, res: Response) { |
|||
return res.json({ |
|||
data: { |
|||
name: 'Serati Ma', |
|||
avatar: |
|||
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', |
|||
userid: '00000001', |
|||
email: 'antdesign@alipay.com', |
|||
signature: '海纳百川,有容乃大', |
|||
title: '交互专家', |
|||
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED', |
|||
tags: [ |
|||
{ |
|||
key: '0', |
|||
label: '很有想法的', |
|||
}, |
|||
{ |
|||
key: '1', |
|||
label: '专注设计', |
|||
}, |
|||
{ |
|||
key: '2', |
|||
label: '辣~', |
|||
}, |
|||
{ |
|||
key: '3', |
|||
label: '大长腿', |
|||
}, |
|||
{ |
|||
key: '4', |
|||
label: '川妹子', |
|||
}, |
|||
{ |
|||
key: '5', |
|||
label: '海纳百川', |
|||
}, |
|||
], |
|||
notifyCount: 12, |
|||
unreadCount: 11, |
|||
country: 'China', |
|||
geographic: { |
|||
province: { |
|||
label: '浙江省', |
|||
key: '330000', |
|||
}, |
|||
city: { |
|||
label: '杭州市', |
|||
key: '330100', |
|||
}, |
|||
}, |
|||
address: '西湖区工专路 77 号', |
|||
phone: '0752-268888888', |
|||
}, |
|||
}); |
|||
} |
|||
// 代码中会兼容本地 service mock 以及部署站点的静态数据
|
|||
export default { |
|||
// 支持值为 Object 和 Array
|
|||
'GET /api/accountSettingCurrentUser': getCurrentUse, |
|||
'GET /api/geographic/province': getProvince, |
|||
'GET /api/geographic/city/:province': getCity, |
|||
}; |
|||
@ -1,39 +0,0 @@ |
|||
import { Input } from 'antd'; |
|||
import React from 'react'; |
|||
import useStyles from './index.style'; |
|||
|
|||
type PhoneViewProps = { |
|||
value?: string; |
|||
onChange?: (value: string) => void; |
|||
}; |
|||
const PhoneView: React.FC<PhoneViewProps> = (props) => { |
|||
const { styles } = useStyles(); |
|||
const { value, onChange } = props; |
|||
let values = ['', '']; |
|||
if (value) { |
|||
values = value.split('-'); |
|||
} |
|||
return ( |
|||
<> |
|||
<Input |
|||
className={styles.area_code} |
|||
value={values[0]} |
|||
onChange={(e) => { |
|||
if (onChange) { |
|||
onChange(`${e.target.value}-${values[1]}`); |
|||
} |
|||
}} |
|||
/> |
|||
<Input |
|||
className={styles.phone_number} |
|||
onChange={(e) => { |
|||
if (onChange) { |
|||
onChange(`${values[0]}-${e.target.value}`); |
|||
} |
|||
}} |
|||
value={values[1]} |
|||
/> |
|||
</> |
|||
); |
|||
}; |
|||
export default PhoneView; |
|||
@ -1,234 +0,0 @@ |
|||
import { UploadOutlined } from '@ant-design/icons'; |
|||
import { |
|||
ProForm, |
|||
ProFormDependency, |
|||
ProFormFieldSet, |
|||
ProFormSelect, |
|||
ProFormText, |
|||
ProFormTextArea, |
|||
} from '@ant-design/pro-components'; |
|||
import { useRequest } from '@umijs/max'; |
|||
import { Button, Input, message, Upload } from 'antd'; |
|||
import React from 'react'; |
|||
import { queryCity, queryCurrent, queryProvince } from '../service'; |
|||
import useStyles from './index.style'; |
|||
|
|||
const validatorPhone = ( |
|||
_rule: any, |
|||
value: string[], |
|||
callback: (message?: string) => void, |
|||
) => { |
|||
if (!value[0]) { |
|||
callback('Please input your area code!'); |
|||
} |
|||
if (!value[1]) { |
|||
callback('Please input your phone number!'); |
|||
} |
|||
callback(); |
|||
}; |
|||
|
|||
const BaseView: React.FC = () => { |
|||
const { styles } = useStyles(); |
|||
// 头像组件 方便以后独立,增加裁剪之类的功能
|
|||
const AvatarView = ({ avatar }: { avatar: string }) => ( |
|||
<> |
|||
<div className={styles.avatar_title}>头像</div> |
|||
<div className={styles.avatar}> |
|||
<img src={avatar} alt="avatar" /> |
|||
</div> |
|||
<Upload showUploadList={false}> |
|||
<div className={styles.button_view}> |
|||
<Button> |
|||
<UploadOutlined /> |
|||
更换头像 |
|||
</Button> |
|||
</div> |
|||
</Upload> |
|||
</> |
|||
); |
|||
const { data: currentUser, loading } = useRequest(() => { |
|||
return queryCurrent(); |
|||
}); |
|||
const getAvatarURL = () => { |
|||
if (currentUser) { |
|||
if (currentUser.avatar) { |
|||
return currentUser.avatar; |
|||
} |
|||
const url = |
|||
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png'; |
|||
return url; |
|||
} |
|||
return ''; |
|||
}; |
|||
const handleFinish = async () => { |
|||
message.success('更新基本信息成功'); |
|||
}; |
|||
return ( |
|||
<div className={styles.baseView}> |
|||
{loading ? null : ( |
|||
<> |
|||
<div className={styles.left}> |
|||
<ProForm |
|||
layout="vertical" |
|||
onFinish={handleFinish} |
|||
submitter={{ |
|||
searchConfig: { |
|||
submitText: '更新基本信息', |
|||
}, |
|||
render: (_, dom) => dom[1], |
|||
}} |
|||
initialValues={{ |
|||
...currentUser, |
|||
phone: currentUser?.phone.split('-'), |
|||
}} |
|||
hideRequiredMark |
|||
> |
|||
<ProFormText |
|||
width="md" |
|||
name="email" |
|||
label="邮箱" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: '请输入您的邮箱!', |
|||
}, |
|||
]} |
|||
/> |
|||
<ProFormText |
|||
width="md" |
|||
name="name" |
|||
label="昵称" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: '请输入您的昵称!', |
|||
}, |
|||
]} |
|||
/> |
|||
<ProFormTextArea |
|||
name="profile" |
|||
label="个人简介" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: '请输入个人简介!', |
|||
}, |
|||
]} |
|||
placeholder="个人简介" |
|||
/> |
|||
<ProFormSelect |
|||
width="sm" |
|||
name="country" |
|||
label="国家/地区" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: '请输入您的国家或地区!', |
|||
}, |
|||
]} |
|||
options={[ |
|||
{ |
|||
label: '中国', |
|||
value: 'China', |
|||
}, |
|||
]} |
|||
/> |
|||
|
|||
<ProForm.Group title="所在省市" size={8}> |
|||
<ProFormSelect |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: '请输入您的所在省!', |
|||
}, |
|||
]} |
|||
width="sm" |
|||
fieldProps={{ |
|||
labelInValue: true, |
|||
}} |
|||
name="province" |
|||
request={async () => { |
|||
return queryProvince().then(({ data }) => { |
|||
return data.map((item) => { |
|||
return { |
|||
label: item.name, |
|||
value: item.id, |
|||
}; |
|||
}); |
|||
}); |
|||
}} |
|||
/> |
|||
<ProFormDependency name={['province']}> |
|||
{({ province }) => { |
|||
return ( |
|||
<ProFormSelect |
|||
params={{ |
|||
key: province?.value, |
|||
}} |
|||
name="city" |
|||
width="sm" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: '请输入您的所在城市!', |
|||
}, |
|||
]} |
|||
disabled={!province} |
|||
request={async () => { |
|||
if (!province?.key) { |
|||
return []; |
|||
} |
|||
return queryCity(province.key || '').then( |
|||
({ data }) => { |
|||
return data.map((item) => { |
|||
return { |
|||
label: item.name, |
|||
value: item.id, |
|||
}; |
|||
}); |
|||
}, |
|||
); |
|||
}} |
|||
/> |
|||
); |
|||
}} |
|||
</ProFormDependency> |
|||
</ProForm.Group> |
|||
<ProFormText |
|||
width="md" |
|||
name="address" |
|||
label="街道地址" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: '请输入您的街道地址!', |
|||
}, |
|||
]} |
|||
/> |
|||
<ProFormFieldSet |
|||
name="phone" |
|||
label="联系电话" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: '请输入您的联系电话!', |
|||
}, |
|||
{ |
|||
validator: validatorPhone, |
|||
}, |
|||
]} |
|||
> |
|||
<Input className={styles.area_code} /> |
|||
<Input className={styles.phone_number} /> |
|||
</ProFormFieldSet> |
|||
</ProForm> |
|||
</div> |
|||
<div className={styles.right}> |
|||
<AvatarView avatar={getAvatarURL()} /> |
|||
</div> |
|||
</> |
|||
)} |
|||
</div> |
|||
); |
|||
}; |
|||
export default BaseView; |
|||
@ -1,48 +0,0 @@ |
|||
import { |
|||
AlipayOutlined, |
|||
DingdingOutlined, |
|||
TaobaoOutlined, |
|||
} from '@ant-design/icons'; |
|||
import { List } from 'antd'; |
|||
import React from 'react'; |
|||
|
|||
const BindingView: React.FC = () => { |
|||
const getData = () => [ |
|||
{ |
|||
title: '绑定淘宝', |
|||
description: '当前未绑定淘宝账号', |
|||
actions: [<a key="Bind">绑定</a>], |
|||
avatar: <TaobaoOutlined className="taobao" />, |
|||
}, |
|||
{ |
|||
title: '绑定支付宝', |
|||
description: '当前未绑定支付宝账号', |
|||
actions: [<a key="Bind">绑定</a>], |
|||
avatar: <AlipayOutlined className="alipay" />, |
|||
}, |
|||
{ |
|||
title: '绑定钉钉', |
|||
description: '当前未绑定钉钉账号', |
|||
actions: [<a key="Bind">绑定</a>], |
|||
avatar: <DingdingOutlined className="dingding" />, |
|||
}, |
|||
]; |
|||
|
|||
return ( |
|||
<List |
|||
itemLayout="horizontal" |
|||
dataSource={getData()} |
|||
renderItem={(item) => ( |
|||
<List.Item actions={item.actions}> |
|||
<List.Item.Meta |
|||
avatar={item.avatar} |
|||
title={item.title} |
|||
description={item.description} |
|||
/> |
|||
</List.Item> |
|||
)} |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
export default BindingView; |
|||
@ -1,60 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
baseView: { |
|||
display: 'flex', |
|||
paddingTop: '12px', |
|||
'.ant-legacy-form-item .ant-legacy-form-item-control-wrapper': { |
|||
width: '100%', |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenXL}px)`]: { |
|||
flexDirection: 'column-reverse', |
|||
}, |
|||
}, |
|||
left: { |
|||
minWidth: '224px', |
|||
maxWidth: '448px', |
|||
}, |
|||
right: { |
|||
flex: '1', |
|||
paddingLeft: '104px', |
|||
[`@media screen and (max-width: ${token.screenXL}px)`]: { |
|||
display: 'flex', |
|||
flexDirection: 'column', |
|||
alignItems: 'center', |
|||
maxWidth: '448px', |
|||
padding: '20px', |
|||
}, |
|||
}, |
|||
avatar_title: { |
|||
height: '22px', |
|||
marginBottom: '8px', |
|||
color: token.colorTextHeading, |
|||
fontSize: token.fontSize, |
|||
lineHeight: '22px', |
|||
[`@media screen and (max-width: ${token.screenXL}px)`]: { |
|||
display: 'none', |
|||
}, |
|||
}, |
|||
avatar: { |
|||
width: '144px', |
|||
height: '144px', |
|||
marginBottom: '12px', |
|||
overflow: 'hidden', |
|||
img: { width: '100%' }, |
|||
}, |
|||
button_view: { |
|||
width: '144px', |
|||
textAlign: 'center', |
|||
}, |
|||
area_code: { |
|||
width: '72px', |
|||
}, |
|||
phone_number: { |
|||
width: '214px', |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,44 +0,0 @@ |
|||
import { List, Switch } from 'antd'; |
|||
import React from 'react'; |
|||
|
|||
type Unpacked<T> = T extends (infer U)[] ? U : T; |
|||
|
|||
const NotificationView: React.FC = () => { |
|||
const getData = () => { |
|||
const Action = ( |
|||
<Switch checkedChildren="开" unCheckedChildren="关" defaultChecked /> |
|||
); |
|||
return [ |
|||
{ |
|||
title: '用户消息', |
|||
description: '其他用户的消息将以站内信的形式通知', |
|||
actions: [Action], |
|||
}, |
|||
{ |
|||
title: '系统消息', |
|||
description: '系统消息将以站内信的形式通知', |
|||
actions: [Action], |
|||
}, |
|||
{ |
|||
title: '待办任务', |
|||
description: '待办任务将以站内信的形式通知', |
|||
actions: [Action], |
|||
}, |
|||
]; |
|||
}; |
|||
|
|||
const data = getData(); |
|||
return ( |
|||
<List<Unpacked<typeof data>> |
|||
itemLayout="horizontal" |
|||
dataSource={data} |
|||
renderItem={(item) => ( |
|||
<List.Item actions={item.actions}> |
|||
<List.Item.Meta title={item.title} description={item.description} /> |
|||
</List.Item> |
|||
)} |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
export default NotificationView; |
|||
@ -1,60 +0,0 @@ |
|||
import { List } from 'antd'; |
|||
import React from 'react'; |
|||
|
|||
type Unpacked<T> = T extends (infer U)[] ? U : T; |
|||
|
|||
const passwordStrength = { |
|||
strong: <span className="strong">强</span>, |
|||
medium: <span className="medium">中</span>, |
|||
weak: <span className="weak">弱 Weak</span>, |
|||
}; |
|||
|
|||
const SecurityView: React.FC = () => { |
|||
const getData = () => [ |
|||
{ |
|||
title: '账户密码', |
|||
description: ( |
|||
<> |
|||
当前密码强度: |
|||
{passwordStrength.strong} |
|||
</> |
|||
), |
|||
actions: [<a key="Modify">修改</a>], |
|||
}, |
|||
{ |
|||
title: '密保手机', |
|||
description: `已绑定手机:138****8293`, |
|||
actions: [<a key="Modify">修改</a>], |
|||
}, |
|||
{ |
|||
title: '密保问题', |
|||
description: '未设置密保问题,密保问题可有效保护账户安全', |
|||
actions: [<a key="Set">设置</a>], |
|||
}, |
|||
{ |
|||
title: '备用邮箱', |
|||
description: `已绑定邮箱:ant***sign.com`, |
|||
actions: [<a key="Modify">修改</a>], |
|||
}, |
|||
{ |
|||
title: 'MFA 设备', |
|||
description: '未绑定 MFA 设备,绑定后,可以进行二次确认', |
|||
actions: [<a key="bind">绑定</a>], |
|||
}, |
|||
]; |
|||
|
|||
const data = getData(); |
|||
return ( |
|||
<List<Unpacked<typeof data>> |
|||
itemLayout="horizontal" |
|||
dataSource={data} |
|||
renderItem={(item) => ( |
|||
<List.Item actions={item.actions}> |
|||
<List.Item.Meta title={item.title} description={item.description} /> |
|||
</List.Item> |
|||
)} |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
export default SecurityView; |
|||
@ -1,43 +0,0 @@ |
|||
export type TagType = { |
|||
key: string; |
|||
label: string; |
|||
}; |
|||
|
|||
export type GeographicItemType = { |
|||
name: string; |
|||
id: string; |
|||
}; |
|||
|
|||
export type GeographicType = { |
|||
province: GeographicItemType; |
|||
city: GeographicItemType; |
|||
}; |
|||
|
|||
export type NoticeType = { |
|||
id: string; |
|||
title: string; |
|||
logo: string; |
|||
description: string; |
|||
updatedAt: string; |
|||
member: string; |
|||
href: string; |
|||
memberLink: string; |
|||
}; |
|||
|
|||
export type CurrentUser = { |
|||
name: string; |
|||
avatar: string; |
|||
userid: string; |
|||
notice: NoticeType[]; |
|||
email: string; |
|||
signature: string; |
|||
title: string; |
|||
group: string; |
|||
tags: TagType[]; |
|||
notifyCount: number; |
|||
unreadCount: number; |
|||
country: string; |
|||
geographic: GeographicType; |
|||
address: string; |
|||
phone: string; |
|||
}; |
|||
File diff suppressed because it is too large
@ -1,138 +0,0 @@ |
|||
[ |
|||
{ |
|||
"name": "北京市", |
|||
"id": "110000" |
|||
}, |
|||
{ |
|||
"name": "天津市", |
|||
"id": "120000" |
|||
}, |
|||
{ |
|||
"name": "河北省", |
|||
"id": "130000" |
|||
}, |
|||
{ |
|||
"name": "山西省", |
|||
"id": "140000" |
|||
}, |
|||
{ |
|||
"name": "内蒙古自治区", |
|||
"id": "150000" |
|||
}, |
|||
{ |
|||
"name": "辽宁省", |
|||
"id": "210000" |
|||
}, |
|||
{ |
|||
"name": "吉林省", |
|||
"id": "220000" |
|||
}, |
|||
{ |
|||
"name": "黑龙江省", |
|||
"id": "230000" |
|||
}, |
|||
{ |
|||
"name": "上海市", |
|||
"id": "310000" |
|||
}, |
|||
{ |
|||
"name": "江苏省", |
|||
"id": "320000" |
|||
}, |
|||
{ |
|||
"name": "浙江省", |
|||
"id": "330000" |
|||
}, |
|||
{ |
|||
"name": "安徽省", |
|||
"id": "340000" |
|||
}, |
|||
{ |
|||
"name": "福建省", |
|||
"id": "350000" |
|||
}, |
|||
{ |
|||
"name": "江西省", |
|||
"id": "360000" |
|||
}, |
|||
{ |
|||
"name": "山东省", |
|||
"id": "370000" |
|||
}, |
|||
{ |
|||
"name": "河南省", |
|||
"id": "410000" |
|||
}, |
|||
{ |
|||
"name": "湖北省", |
|||
"id": "420000" |
|||
}, |
|||
{ |
|||
"name": "湖南省", |
|||
"id": "430000" |
|||
}, |
|||
{ |
|||
"name": "广东省", |
|||
"id": "440000" |
|||
}, |
|||
{ |
|||
"name": "广西壮族自治区", |
|||
"id": "450000" |
|||
}, |
|||
{ |
|||
"name": "海南省", |
|||
"id": "460000" |
|||
}, |
|||
{ |
|||
"name": "重庆市", |
|||
"id": "500000" |
|||
}, |
|||
{ |
|||
"name": "四川省", |
|||
"id": "510000" |
|||
}, |
|||
{ |
|||
"name": "贵州省", |
|||
"id": "520000" |
|||
}, |
|||
{ |
|||
"name": "云南省", |
|||
"id": "530000" |
|||
}, |
|||
{ |
|||
"name": "西藏自治区", |
|||
"id": "540000" |
|||
}, |
|||
{ |
|||
"name": "陕西省", |
|||
"id": "610000" |
|||
}, |
|||
{ |
|||
"name": "甘肃省", |
|||
"id": "620000" |
|||
}, |
|||
{ |
|||
"name": "青海省", |
|||
"id": "630000" |
|||
}, |
|||
{ |
|||
"name": "宁夏回族自治区", |
|||
"id": "640000" |
|||
}, |
|||
{ |
|||
"name": "新疆维吾尔自治区", |
|||
"id": "650000" |
|||
}, |
|||
{ |
|||
"name": "台湾省", |
|||
"id": "710000" |
|||
}, |
|||
{ |
|||
"name": "香港特别行政区", |
|||
"id": "810000" |
|||
}, |
|||
{ |
|||
"name": "澳门特别行政区", |
|||
"id": "820000" |
|||
} |
|||
] |
|||
@ -1,108 +0,0 @@ |
|||
import { GridContent } from '@ant-design/pro-components'; |
|||
import { Menu } from 'antd'; |
|||
import React, { useLayoutEffect, useRef, useState } from 'react'; |
|||
import BaseView from './components/base'; |
|||
import BindingView from './components/binding'; |
|||
import NotificationView from './components/notification'; |
|||
import SecurityView from './components/security'; |
|||
import useStyles from './style.style'; |
|||
|
|||
type SettingsStateKeys = 'base' | 'security' | 'binding' | 'notification'; |
|||
type SettingsState = { |
|||
mode: 'inline' | 'horizontal'; |
|||
selectKey: SettingsStateKeys; |
|||
}; |
|||
const Settings: React.FC = () => { |
|||
const { styles } = useStyles(); |
|||
const menuMap: Record<string, React.ReactNode> = { |
|||
base: '基本设置', |
|||
security: '安全设置', |
|||
binding: '账号绑定', |
|||
notification: '新消息通知', |
|||
}; |
|||
const [initConfig, setInitConfig] = useState<SettingsState>({ |
|||
mode: 'inline', |
|||
selectKey: 'base', |
|||
}); |
|||
const dom = useRef<HTMLDivElement>(null); |
|||
const resize = () => { |
|||
requestAnimationFrame(() => { |
|||
if (!dom.current) { |
|||
return; |
|||
} |
|||
let mode: 'inline' | 'horizontal' = 'inline'; |
|||
const { offsetWidth } = dom.current; |
|||
if (dom.current.offsetWidth < 641 && offsetWidth > 400) { |
|||
mode = 'horizontal'; |
|||
} |
|||
if (window.innerWidth < 768 && offsetWidth > 400) { |
|||
mode = 'horizontal'; |
|||
} |
|||
setInitConfig({ |
|||
...initConfig, |
|||
mode: mode as SettingsState['mode'], |
|||
}); |
|||
}); |
|||
}; |
|||
useLayoutEffect(() => { |
|||
if (dom.current) { |
|||
window.addEventListener('resize', resize); |
|||
resize(); |
|||
} |
|||
return () => { |
|||
window.removeEventListener('resize', resize); |
|||
}; |
|||
}, []); |
|||
const getMenu = () => { |
|||
return Object.keys(menuMap).map((item) => ({ |
|||
key: item, |
|||
label: menuMap[item], |
|||
})); |
|||
}; |
|||
const renderChildren = () => { |
|||
const { selectKey } = initConfig; |
|||
switch (selectKey) { |
|||
case 'base': |
|||
return <BaseView />; |
|||
case 'security': |
|||
return <SecurityView />; |
|||
case 'binding': |
|||
return <BindingView />; |
|||
case 'notification': |
|||
return <NotificationView />; |
|||
default: |
|||
return null; |
|||
} |
|||
}; |
|||
return ( |
|||
<GridContent> |
|||
<div |
|||
className={styles.main} |
|||
ref={(ref) => { |
|||
if (ref) { |
|||
dom.current = ref; |
|||
} |
|||
}} |
|||
> |
|||
<div className={styles.leftMenu}> |
|||
<Menu |
|||
mode={initConfig.mode} |
|||
selectedKeys={[initConfig.selectKey]} |
|||
onClick={({ key }) => { |
|||
setInitConfig({ |
|||
...initConfig, |
|||
selectKey: key as SettingsStateKeys, |
|||
}); |
|||
}} |
|||
items={getMenu()} |
|||
/> |
|||
</div> |
|||
<div className={styles.right}> |
|||
<div className={styles.title}>{menuMap[initConfig.selectKey]}</div> |
|||
{renderChildren()} |
|||
</div> |
|||
</div> |
|||
</GridContent> |
|||
); |
|||
}; |
|||
export default Settings; |
|||
@ -1,20 +0,0 @@ |
|||
import { request } from '@umijs/max'; |
|||
import type { CurrentUser, GeographicItemType } from './data'; |
|||
|
|||
export async function queryCurrent(): Promise<{ data: CurrentUser }> { |
|||
return request('/api/accountSettingCurrentUser'); |
|||
} |
|||
|
|||
export async function queryProvince(): Promise<{ data: GeographicItemType[] }> { |
|||
return request('/api/geographic/province'); |
|||
} |
|||
|
|||
export async function queryCity( |
|||
province: string, |
|||
): Promise<{ data: GeographicItemType[] }> { |
|||
return request(`/api/geographic/city/${province}`); |
|||
} |
|||
|
|||
export async function query() { |
|||
return request('/api/users'); |
|||
} |
|||
@ -1,74 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
main: { |
|||
display: 'flex', |
|||
width: '100%', |
|||
height: '100%', |
|||
paddingTop: '16px', |
|||
paddingBottom: '16px', |
|||
backgroundColor: token.colorBgContainer, |
|||
'.ant-list-split .ant-list-item:last-child': { |
|||
borderBottom: `1px solid ${token.colorSplit}`, |
|||
}, |
|||
'.ant-list-item': { paddingTop: '14px', paddingBottom: '14px' }, |
|||
[`@media screen and (max-width: ${token.screenMD}px)`]: { |
|||
flexDirection: 'column', |
|||
}, |
|||
}, |
|||
leftMenu: { |
|||
width: '224px', |
|||
borderRight: `${token.lineWidth}px solid ${token.colorSplit}`, |
|||
'.ant-menu-inline': { border: 'none' }, |
|||
'.ant-menu-horizontal': { fontWeight: 'bold' }, |
|||
[`@media screen and (max-width: ${token.screenMD}px)`]: { |
|||
width: '100%', |
|||
border: 'none', |
|||
}, |
|||
}, |
|||
right: { |
|||
flex: '1', |
|||
padding: '8px 40px', |
|||
[`@media screen and (max-width: ${token.screenMD}px)`]: { |
|||
padding: '40px', |
|||
}, |
|||
}, |
|||
title: { |
|||
marginBottom: '12px', |
|||
color: token.colorTextHeading, |
|||
fontWeight: '500', |
|||
fontSize: '20px', |
|||
lineHeight: '28px', |
|||
}, |
|||
taobao: { |
|||
display: 'block', |
|||
color: '#ff4000', |
|||
fontSize: '48px', |
|||
lineHeight: '48px', |
|||
borderRadius: token.borderRadius, |
|||
}, |
|||
dingding: { |
|||
margin: '2px', |
|||
padding: '6px', |
|||
color: '#fff', |
|||
fontSize: '32px', |
|||
lineHeight: '32px', |
|||
backgroundColor: '#2eabff', |
|||
borderRadius: token.borderRadius, |
|||
}, |
|||
alipay: { |
|||
color: '#2eabff', |
|||
fontSize: '48px', |
|||
lineHeight: '48px', |
|||
borderRadius: token.borderRadius, |
|||
}, |
|||
':global': { |
|||
'font.strong': { color: token.colorSuccess }, |
|||
'font.medium': { color: token.colorWarning }, |
|||
'font.weak': { color: token.colorError }, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,210 +0,0 @@ |
|||
import dayjs from 'dayjs'; |
|||
import type { Request, Response } from 'express'; |
|||
import type { AnalysisData, DataItem, RadarData } from './data.d'; |
|||
|
|||
// mock data
|
|||
const visitData: DataItem[] = []; |
|||
const beginDay = Date.now(); |
|||
|
|||
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]; |
|||
for (let i = 0; i < fakeY.length; i += 1) { |
|||
visitData.push({ |
|||
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'), |
|||
y: fakeY[i], |
|||
}); |
|||
} |
|||
|
|||
const visitData2 = []; |
|||
const fakeY2 = [1, 6, 4, 8, 3, 7, 2]; |
|||
for (let i = 0; i < fakeY2.length; i += 1) { |
|||
visitData2.push({ |
|||
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'), |
|||
y: fakeY2[i], |
|||
}); |
|||
} |
|||
|
|||
const salesData = []; |
|||
for (let i = 0; i < 12; i += 1) { |
|||
salesData.push({ |
|||
x: `${i + 1}月`, |
|||
y: Math.floor(Math.random() * 1000) + 200, |
|||
}); |
|||
} |
|||
const searchData = []; |
|||
for (let i = 0; i < 50; i += 1) { |
|||
searchData.push({ |
|||
index: i + 1, |
|||
keyword: `搜索关键词-${i}`, |
|||
count: Math.floor(Math.random() * 1000), |
|||
range: Math.floor(Math.random() * 100), |
|||
status: Math.floor((Math.random() * 10) % 2), |
|||
}); |
|||
} |
|||
const salesTypeData = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 4544, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 3321, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 3113, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 2341, |
|||
}, |
|||
{ |
|||
x: '母婴产品', |
|||
y: 1231, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 1231, |
|||
}, |
|||
]; |
|||
|
|||
const salesTypeDataOnline = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 244, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 321, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 311, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 41, |
|||
}, |
|||
{ |
|||
x: '母婴产品', |
|||
y: 121, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 111, |
|||
}, |
|||
]; |
|||
|
|||
const salesTypeDataOffline = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 99, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 188, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 344, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 255, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 65, |
|||
}, |
|||
]; |
|||
|
|||
const offlineData = []; |
|||
for (let i = 0; i < 10; i += 1) { |
|||
offlineData.push({ |
|||
name: `Stores ${i}`, |
|||
cvr: Math.ceil(Math.random() * 9) / 10, |
|||
}); |
|||
} |
|||
const offlineChartData = []; |
|||
for (let i = 0; i < 20; i += 1) { |
|||
const date = dayjs(Date.now() + 1000 * 60 * 30 * i).format('HH:mm'); |
|||
offlineChartData.push({ |
|||
date, |
|||
type: '客流量', |
|||
value: Math.floor(Math.random() * 100) + 10, |
|||
}); |
|||
offlineChartData.push({ |
|||
date, |
|||
type: '支付笔数', |
|||
value: Math.floor(Math.random() * 100) + 10, |
|||
}); |
|||
} |
|||
|
|||
const radarOriginData = [ |
|||
{ |
|||
name: '个人', |
|||
ref: 10, |
|||
koubei: 8, |
|||
output: 4, |
|||
contribute: 5, |
|||
hot: 7, |
|||
}, |
|||
{ |
|||
name: '团队', |
|||
ref: 3, |
|||
koubei: 9, |
|||
output: 6, |
|||
contribute: 3, |
|||
hot: 1, |
|||
}, |
|||
{ |
|||
name: '部门', |
|||
ref: 4, |
|||
koubei: 1, |
|||
output: 6, |
|||
contribute: 5, |
|||
hot: 7, |
|||
}, |
|||
]; |
|||
|
|||
const radarData: RadarData[] = []; |
|||
const radarTitleMap = { |
|||
ref: '引用', |
|||
koubei: '口碑', |
|||
output: '产量', |
|||
contribute: '贡献', |
|||
hot: '热度', |
|||
}; |
|||
radarOriginData.forEach((item) => { |
|||
Object.keys(item).forEach((key) => { |
|||
if (key !== 'name') { |
|||
radarData.push({ |
|||
name: item.name, |
|||
label: radarTitleMap[key as 'ref'], |
|||
value: item[key as 'ref'], |
|||
}); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
const getFakeChartData: AnalysisData = { |
|||
visitData, |
|||
visitData2, |
|||
salesData, |
|||
searchData, |
|||
offlineData, |
|||
offlineChartData, |
|||
salesTypeData, |
|||
salesTypeDataOnline, |
|||
salesTypeDataOffline, |
|||
radarData, |
|||
}; |
|||
|
|||
const fakeChartData = (_: Request, res: Response) => { |
|||
return res.json({ |
|||
data: getFakeChartData, |
|||
}); |
|||
}; |
|||
|
|||
export default { |
|||
'GET /api/fake_analysis_chart_data': fakeChartData, |
|||
}; |
|||
@ -1,75 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.chartCard { |
|||
position: relative; |
|||
.chartTop { |
|||
position: relative; |
|||
width: 100%; |
|||
overflow: hidden; |
|||
} |
|||
.chartTopMargin { |
|||
margin-bottom: 12px; |
|||
} |
|||
.chartTopHasMargin { |
|||
margin-bottom: 20px; |
|||
} |
|||
.metaWrap { |
|||
float: left; |
|||
} |
|||
.avatar { |
|||
position: relative; |
|||
top: 4px; |
|||
float: left; |
|||
margin-right: 20px; |
|||
img { |
|||
border-radius: 100%; |
|||
} |
|||
} |
|||
.meta { |
|||
height: 22px; |
|||
color: @text-color-secondary; |
|||
font-size: @font-size-base; |
|||
line-height: 22px; |
|||
} |
|||
.action { |
|||
position: absolute; |
|||
top: 4px; |
|||
right: 0; |
|||
line-height: 1; |
|||
cursor: pointer; |
|||
} |
|||
.total { |
|||
height: 38px; |
|||
margin-top: 4px; |
|||
margin-bottom: 0; |
|||
overflow: hidden; |
|||
color: @heading-color; |
|||
font-size: 30px; |
|||
line-height: 38px; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
word-break: break-all; |
|||
} |
|||
.content { |
|||
position: relative; |
|||
width: 100%; |
|||
margin-bottom: 12px; |
|||
} |
|||
.contentFixed { |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
} |
|||
.footer { |
|||
margin-top: 8px; |
|||
padding-top: 9px; |
|||
border-top: 1px solid @border-color-split; |
|||
& > * { |
|||
position: relative; |
|||
} |
|||
} |
|||
.footerMargin { |
|||
margin-top: 20px; |
|||
} |
|||
} |
|||
@ -1,77 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
chartCard: { |
|||
position: 'relative', |
|||
}, |
|||
chartTop: { |
|||
position: 'relative', |
|||
width: '100%', |
|||
overflow: 'hidden', |
|||
}, |
|||
chartTopMargin: { |
|||
marginBottom: '12px', |
|||
}, |
|||
chartTopHasMargin: { |
|||
marginBottom: '20px', |
|||
}, |
|||
metaWrap: { |
|||
float: 'left', |
|||
}, |
|||
avatar: { |
|||
position: 'relative', |
|||
top: '4px', |
|||
float: 'left', |
|||
marginRight: '20px', |
|||
img: { borderRadius: '100%' }, |
|||
}, |
|||
meta: { |
|||
height: '22px', |
|||
color: token.colorTextSecondary, |
|||
fontSize: token.fontSize, |
|||
lineHeight: '22px', |
|||
}, |
|||
action: { |
|||
position: 'absolute', |
|||
top: '4px', |
|||
right: '0', |
|||
lineHeight: '1', |
|||
cursor: 'pointer', |
|||
}, |
|||
total: { |
|||
height: '38px', |
|||
marginTop: '4px', |
|||
marginBottom: '0', |
|||
overflow: 'hidden', |
|||
color: token.colorTextHeading, |
|||
fontSize: '30px', |
|||
lineHeight: '38px', |
|||
whiteSpace: 'nowrap', |
|||
textOverflow: 'ellipsis', |
|||
wordBreak: 'break-all', |
|||
}, |
|||
content: { |
|||
position: 'relative', |
|||
width: '100%', |
|||
marginBottom: '12px', |
|||
}, |
|||
contentFixed: { |
|||
position: 'absolute', |
|||
bottom: '0', |
|||
left: '0', |
|||
width: '100%', |
|||
}, |
|||
footer: { |
|||
marginTop: '8px', |
|||
paddingTop: '9px', |
|||
borderTop: `1px solid ${token.colorSplit}`, |
|||
'& > *': { position: 'relative' }, |
|||
}, |
|||
footerMargin: { |
|||
marginTop: '20px', |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,17 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.field { |
|||
margin: 0; |
|||
overflow: hidden; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
.label, |
|||
.number { |
|||
font-size: @font-size-base; |
|||
line-height: 22px; |
|||
} |
|||
.number { |
|||
margin-left: 8px; |
|||
color: @heading-color; |
|||
} |
|||
} |
|||
@ -1,22 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
field: { |
|||
margin: '0', |
|||
overflow: 'hidden', |
|||
whiteSpace: 'nowrap', |
|||
textOverflow: 'ellipsis', |
|||
}, |
|||
label: { |
|||
fontSize: token.fontSize, |
|||
lineHeight: '22px', |
|||
}, |
|||
number: { |
|||
marginLeft: '8px', |
|||
color: token.colorTextHeading, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,17 +0,0 @@ |
|||
import React from 'react'; |
|||
import useStyles from './index.style'; |
|||
export type FieldProps = { |
|||
label: React.ReactNode; |
|||
value: React.ReactNode; |
|||
style?: React.CSSProperties; |
|||
}; |
|||
const Field: React.FC<FieldProps> = ({ label, value, ...rest }) => { |
|||
const { styles } = useStyles(); |
|||
return ( |
|||
<div className={styles.field} {...rest}> |
|||
<span className={styles.label}>{label}</span> |
|||
<span className={styles.number}>{value}</span> |
|||
</div> |
|||
); |
|||
}; |
|||
export default Field; |
|||
@ -1,225 +0,0 @@ |
|||
import React, { Component } from 'react'; |
|||
import autoHeight from '../../../../monitor/components/Charts/autoHeight'; |
|||
|
|||
/* eslint no-return-assign: 0 */ |
|||
/* eslint no-mixed-operators: 0 */ |
|||
// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90
|
|||
|
|||
export type WaterWaveProps = { |
|||
title: React.ReactNode; |
|||
color?: string; |
|||
height?: number; |
|||
percent: number; |
|||
style?: React.CSSProperties; |
|||
}; |
|||
class WaterWave extends Component<WaterWaveProps> { |
|||
state = { |
|||
radio: 1, |
|||
}; |
|||
timer: number = 0; |
|||
root: HTMLDivElement | undefined | null = null; |
|||
node: HTMLCanvasElement | undefined | null = null; |
|||
componentDidMount() { |
|||
this.renderChart(); |
|||
this.resize(); |
|||
window.addEventListener( |
|||
'resize', |
|||
() => { |
|||
requestAnimationFrame(() => this.resize()); |
|||
}, |
|||
{ |
|||
passive: true, |
|||
}, |
|||
); |
|||
} |
|||
componentDidUpdate(props: WaterWaveProps) { |
|||
const { percent } = this.props; |
|||
if (props.percent !== percent) { |
|||
// 不加这个会造成绘制缓慢
|
|||
this.renderChart('update'); |
|||
} |
|||
} |
|||
componentWillUnmount() { |
|||
cancelAnimationFrame(this.timer); |
|||
if (this.node) { |
|||
this.node.innerHTML = ''; |
|||
} |
|||
window.removeEventListener('resize', this.resize); |
|||
} |
|||
resize = () => { |
|||
if (this.root) { |
|||
const { height = 1 } = this.props; |
|||
const { offsetWidth } = this.root.parentNode as HTMLElement; |
|||
this.setState({ |
|||
radio: offsetWidth < height ? offsetWidth / height : 1, |
|||
}); |
|||
} |
|||
}; |
|||
renderChart(type?: string) { |
|||
const { percent, color = '#1890FF' } = this.props; |
|||
const data = percent / 100; |
|||
cancelAnimationFrame(this.timer); |
|||
if (!this.node || (data !== 0 && !data)) { |
|||
return; |
|||
} |
|||
const canvas = this.node; |
|||
const ctx = canvas.getContext('2d'); |
|||
if (!ctx) { |
|||
return; |
|||
} |
|||
const canvasWidth = canvas.width; |
|||
const canvasHeight = canvas.height; |
|||
const radius = canvasWidth / 2; |
|||
const lineWidth = 2; |
|||
const cR = radius - lineWidth; |
|||
ctx.beginPath(); |
|||
ctx.lineWidth = lineWidth * 2; |
|||
const axisLength = canvasWidth - lineWidth; |
|||
const unit = axisLength / 8; |
|||
const range = 0.2; // 振幅
|
|||
let currRange = range; |
|||
const xOffset = lineWidth; |
|||
let sp = 0; // 周期偏移量
|
|||
let currData = 0; |
|||
const waveupsp = 0.005; // 水波上涨速度
|
|||
|
|||
let arcStack: number[][] = []; |
|||
const bR = radius - lineWidth; |
|||
const circleOffset = -(Math.PI / 2); |
|||
let circleLock = true; |
|||
for ( |
|||
let i = circleOffset; |
|||
i < circleOffset + 2 * Math.PI; |
|||
i += 1 / (8 * Math.PI) |
|||
) { |
|||
arcStack.push([radius + bR * Math.cos(i), radius + bR * Math.sin(i)]); |
|||
} |
|||
const cStartPoint = arcStack.shift() as number[]; |
|||
ctx.strokeStyle = color; |
|||
ctx.moveTo(cStartPoint[0], cStartPoint[1]); |
|||
const drawSin = () => { |
|||
if (!ctx) { |
|||
return; |
|||
} |
|||
ctx.beginPath(); |
|||
ctx.save(); |
|||
const sinStack = []; |
|||
for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) { |
|||
const x = sp + (xOffset + i) / unit; |
|||
const y = Math.sin(x) * currRange; |
|||
const dx = i; |
|||
const dy = 2 * cR * (1 - currData) + (radius - cR) - unit * y; |
|||
ctx.lineTo(dx, dy); |
|||
sinStack.push([dx, dy]); |
|||
} |
|||
const startPoint = sinStack.shift() as number[]; |
|||
ctx.lineTo(xOffset + axisLength, canvasHeight); |
|||
ctx.lineTo(xOffset, canvasHeight); |
|||
ctx.lineTo(startPoint[0], startPoint[1]); |
|||
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight); |
|||
gradient.addColorStop(0, '#ffffff'); |
|||
gradient.addColorStop(1, color); |
|||
ctx.fillStyle = gradient; |
|||
ctx.fill(); |
|||
ctx.restore(); |
|||
}; |
|||
const render = () => { |
|||
if (!ctx) { |
|||
return; |
|||
} |
|||
ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
|||
if (circleLock && type !== 'update') { |
|||
if (arcStack.length) { |
|||
const temp = arcStack.shift() as number[]; |
|||
ctx.lineTo(temp[0], temp[1]); |
|||
ctx.stroke(); |
|||
} else { |
|||
circleLock = false; |
|||
ctx.lineTo(cStartPoint[0], cStartPoint[1]); |
|||
ctx.stroke(); |
|||
arcStack = []; |
|||
ctx.globalCompositeOperation = 'destination-over'; |
|||
ctx.beginPath(); |
|||
ctx.lineWidth = lineWidth; |
|||
ctx.arc(radius, radius, bR, 0, 2 * Math.PI, true); |
|||
ctx.beginPath(); |
|||
ctx.save(); |
|||
ctx.arc(radius, radius, radius - 3 * lineWidth, 0, 2 * Math.PI, true); |
|||
ctx.restore(); |
|||
ctx.clip(); |
|||
ctx.fillStyle = color; |
|||
} |
|||
} else { |
|||
if (data >= 0.85) { |
|||
if (currRange > range / 4) { |
|||
const t = range * 0.01; |
|||
currRange -= t; |
|||
} |
|||
} else if (data <= 0.1) { |
|||
if (currRange < range * 1.5) { |
|||
const t = range * 0.01; |
|||
currRange += t; |
|||
} |
|||
} else { |
|||
if (currRange <= range) { |
|||
const t = range * 0.01; |
|||
currRange += t; |
|||
} |
|||
if (currRange >= range) { |
|||
const t = range * 0.01; |
|||
currRange -= t; |
|||
} |
|||
} |
|||
if (data - currData > 0) { |
|||
currData += waveupsp; |
|||
} |
|||
if (data - currData < 0) { |
|||
currData -= waveupsp; |
|||
} |
|||
sp += 0.07; |
|||
drawSin(); |
|||
} |
|||
this.timer = requestAnimationFrame(render); |
|||
}; |
|||
render(); |
|||
} |
|||
render() { |
|||
const { radio } = this.state; |
|||
const { percent, title, height = 1 } = this.props; |
|||
return ( |
|||
<div |
|||
ref={(n) => { |
|||
this.root = n; |
|||
}} |
|||
style={{ |
|||
transform: `scale(${radio})`, |
|||
}} |
|||
> |
|||
<div |
|||
style={{ |
|||
width: height, |
|||
height, |
|||
overflow: 'hidden', |
|||
}} |
|||
> |
|||
<canvas |
|||
ref={(n) => { |
|||
this.node = n; |
|||
}} |
|||
width={height * 2} |
|||
height={height * 2} |
|||
/> |
|||
</div> |
|||
<div |
|||
style={{ |
|||
width: height, |
|||
}} |
|||
> |
|||
{title && <span>{title}</span>} |
|||
<h4>{percent}%</h4> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
export default autoHeight()(WaterWave); |
|||
@ -1,19 +0,0 @@ |
|||
.miniChart { |
|||
position: relative; |
|||
width: 100%; |
|||
.chartContent { |
|||
position: absolute; |
|||
bottom: -28px; |
|||
width: 100%; |
|||
> div { |
|||
margin: 0 -5px; |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
.chartLoading { |
|||
position: absolute; |
|||
top: 16px; |
|||
left: 50%; |
|||
margin-left: -7px; |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(() => { |
|||
return { |
|||
miniChart: { |
|||
position: 'relative', |
|||
width: '100%', |
|||
}, |
|||
chartContent: { |
|||
position: 'absolute', |
|||
bottom: '-28px', |
|||
width: '100%', |
|||
'> div': { margin: '0 -5px', overflow: 'hidden' }, |
|||
}, |
|||
chartLoading: { |
|||
position: 'absolute', |
|||
top: '16px', |
|||
left: '50%', |
|||
marginLeft: '-7px', |
|||
}, |
|||
}; |
|||
}); |
|||
export default useStyles; |
|||
@ -1,13 +0,0 @@ |
|||
import numeral from 'numeral'; |
|||
import ChartCard from './ChartCard'; |
|||
import Field from './Field'; |
|||
|
|||
const yuan = (val: number | string) => `¥ ${numeral(val).format('0,0')}`; |
|||
|
|||
const Charts = { |
|||
yuan, |
|||
ChartCard, |
|||
Field, |
|||
}; |
|||
|
|||
export { Charts as default, yuan, ChartCard, Field }; |
|||
@ -1,68 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.numberInfo { |
|||
.suffix { |
|||
margin-left: 4px; |
|||
color: @text-color; |
|||
font-size: 16px; |
|||
font-style: normal; |
|||
} |
|||
.numberInfoTitle { |
|||
margin-bottom: 16px; |
|||
color: @text-color; |
|||
font-size: @font-size-lg; |
|||
transition: all 0.3s; |
|||
} |
|||
.numberInfoSubTitle { |
|||
height: 22px; |
|||
overflow: hidden; |
|||
color: @text-color-secondary; |
|||
font-size: @font-size-base; |
|||
line-height: 22px; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
word-break: break-all; |
|||
} |
|||
.numberInfoValue { |
|||
margin-top: 4px; |
|||
overflow: hidden; |
|||
font-size: 0; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
word-break: break-all; |
|||
& > span { |
|||
display: inline-block; |
|||
height: 32px; |
|||
margin-right: 32px; |
|||
color: @heading-color; |
|||
font-size: 24px; |
|||
line-height: 32px; |
|||
} |
|||
.subTotal { |
|||
margin-right: 0; |
|||
color: @text-color-secondary; |
|||
font-size: @font-size-lg; |
|||
vertical-align: top; |
|||
.anticon { |
|||
margin-left: 4px; |
|||
font-size: 12px; |
|||
transform: scale(0.82); |
|||
} |
|||
:global { |
|||
.anticon-caret-up { |
|||
color: @red-6; |
|||
} |
|||
.anticon-caret-down { |
|||
color: @green-6; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.numberInfolight { |
|||
.numberInfoValue { |
|||
& > span { |
|||
color: @text-color; |
|||
} |
|||
} |
|||
} |
|||
@ -1,56 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
suffix: { |
|||
marginLeft: '4px', |
|||
color: token.colorText, |
|||
fontSize: '16px', |
|||
fontStyle: 'normal', |
|||
}, |
|||
numberInfoTitle: { |
|||
marginBottom: '16px', |
|||
color: token.colorText, |
|||
fontSize: token.fontSizeLG, |
|||
transition: 'all 0.3s', |
|||
}, |
|||
numberInfoSubTitle: { |
|||
height: '22px', |
|||
overflow: 'hidden', |
|||
color: token.colorTextSecondary, |
|||
fontSize: token.fontSize, |
|||
lineHeight: '22px', |
|||
whiteSpace: 'nowrap', |
|||
textOverflow: 'ellipsis', |
|||
wordBreak: 'break-all', |
|||
}, |
|||
numberInfoValue: { |
|||
marginTop: '4px', |
|||
overflow: 'hidden', |
|||
fontSize: '0', |
|||
whiteSpace: 'nowrap', |
|||
textOverflow: 'ellipsis', |
|||
wordBreak: 'break-all', |
|||
'& > span': { color: token.colorText }, |
|||
}, |
|||
subTotal: { |
|||
marginRight: '0', |
|||
color: token.colorTextSecondary, |
|||
fontSize: token.fontSizeLG, |
|||
verticalAlign: 'top', |
|||
}, |
|||
anticon: { |
|||
marginLeft: '4px', |
|||
fontSize: '12px', |
|||
transform: 'scale(0.82)', |
|||
}, |
|||
'anticon-caret-up': { |
|||
color: token['red-6'], |
|||
}, |
|||
'anticon-caret-down': { |
|||
color: token['green-6'], |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,79 +0,0 @@ |
|||
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons'; |
|||
import classNames from 'classnames'; |
|||
import React from 'react'; |
|||
import useStyles from './index.style'; |
|||
export type NumberInfoProps = { |
|||
title?: React.ReactNode | string; |
|||
subTitle?: React.ReactNode | string; |
|||
total?: React.ReactNode | string; |
|||
status?: 'up' | 'down'; |
|||
theme?: string; |
|||
gap?: number; |
|||
subTotal?: number; |
|||
suffix?: string; |
|||
style?: React.CSSProperties; |
|||
}; |
|||
const NumberInfo: React.FC<NumberInfoProps> = ({ |
|||
theme, |
|||
title, |
|||
subTitle, |
|||
total, |
|||
subTotal, |
|||
status, |
|||
suffix, |
|||
gap, |
|||
...rest |
|||
}) => { |
|||
const { styles } = useStyles(); |
|||
return ( |
|||
<div |
|||
className={classNames({ |
|||
[styles[`numberInfo${theme}` as keyof typeof styles]]: !!theme, |
|||
})} |
|||
{...rest} |
|||
> |
|||
{title && ( |
|||
<div |
|||
className={styles.numberInfoTitle} |
|||
title={typeof title === 'string' ? title : ''} |
|||
> |
|||
{title} |
|||
</div> |
|||
)} |
|||
{subTitle && ( |
|||
<div |
|||
className={styles.numberInfoSubTitle} |
|||
title={typeof subTitle === 'string' ? subTitle : ''} |
|||
> |
|||
{subTitle} |
|||
</div> |
|||
)} |
|||
<div |
|||
className={styles.numberInfoValue} |
|||
style={ |
|||
gap |
|||
? { |
|||
marginTop: gap, |
|||
} |
|||
: {} |
|||
} |
|||
> |
|||
<span> |
|||
{total} |
|||
{suffix && <em className={styles.suffix}>{suffix}</em>} |
|||
</span> |
|||
{(status || subTotal) && ( |
|||
<span className={styles.subTotal}> |
|||
{subTotal} |
|||
{status && status === 'up' ? ( |
|||
<CaretUpOutlined /> |
|||
) : ( |
|||
<CaretDownOutlined /> |
|||
)} |
|||
</span> |
|||
)} |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
export default NumberInfo; |
|||
@ -1,9 +0,0 @@ |
|||
import { Spin } from 'antd'; |
|||
|
|||
// loading components from code split
|
|||
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
|
|||
export default () => ( |
|||
<div style={{ paddingTop: 100, textAlign: 'center' }}> |
|||
<Spin size="large" /> |
|||
</div> |
|||
); |
|||
@ -1,67 +0,0 @@ |
|||
import { Pie } from '@ant-design/plots'; |
|||
import { Card, Segmented, Typography } from 'antd'; |
|||
import numeral from 'numeral'; |
|||
import React from 'react'; |
|||
import type { DataItem } from '../data.d'; |
|||
import useStyles from '../style.style'; |
|||
|
|||
const { Text } = Typography; |
|||
const ProportionSales = ({ |
|||
dropdownGroup, |
|||
salesType, |
|||
loading, |
|||
salesPieData, |
|||
handleChangeSalesType, |
|||
}: { |
|||
loading: boolean; |
|||
dropdownGroup: React.ReactNode; |
|||
salesType: 'all' | 'online' | 'stores'; |
|||
salesPieData: DataItem[]; |
|||
handleChangeSalesType?: (value: 'all' | 'online' | 'stores') => void; |
|||
}) => { |
|||
const { styles } = useStyles(); |
|||
return ( |
|||
<Card |
|||
loading={loading} |
|||
className={styles.salesCard} |
|||
variant="borderless" |
|||
title="销售额类别占比" |
|||
style={{ |
|||
height: '100%', |
|||
}} |
|||
extra={ |
|||
<div className={styles.salesCardExtra}> |
|||
{dropdownGroup} |
|||
<Segmented |
|||
className={styles.salesTypeRadio} |
|||
value={salesType} |
|||
onChange={handleChangeSalesType} |
|||
options={[ |
|||
{ label: '全部渠道', value: 'all' }, |
|||
{ label: '线上', value: 'online' }, |
|||
{ label: '门店', value: 'stores' }, |
|||
]} |
|||
size="middle" |
|||
/> |
|||
</div> |
|||
} |
|||
> |
|||
<Text>销售额</Text> |
|||
<Pie |
|||
height={340} |
|||
radius={0.8} |
|||
innerRadius={0.5} |
|||
angleField="y" |
|||
colorField="x" |
|||
data={salesPieData as any} |
|||
legend={false} |
|||
label={{ |
|||
position: 'spider', |
|||
text: (item: { x: number; y: number }) => |
|||
`${item.x}: ${numeral(item.y).format('0,0')}`, |
|||
}} |
|||
/> |
|||
</Card> |
|||
); |
|||
}; |
|||
export default ProportionSales; |
|||
@ -1,225 +0,0 @@ |
|||
import { Column } from '@ant-design/plots'; |
|||
import { Button, Card, Col, DatePicker, Row, Tabs } from 'antd'; |
|||
import type { RangePickerProps } from 'antd/es/date-picker'; |
|||
import numeral from 'numeral'; |
|||
import type { DataItem } from '../data.d'; |
|||
import useStyles from '../style.style'; |
|||
|
|||
export type TimeType = 'today' | 'week' | 'month' | 'year'; |
|||
const { RangePicker } = DatePicker; |
|||
|
|||
const rankingListData: { |
|||
title: string; |
|||
total: number; |
|||
}[] = []; |
|||
|
|||
for (let i = 0; i < 7; i += 1) { |
|||
rankingListData.push({ |
|||
title: `工专路 ${i} 号店`, |
|||
total: 323234, |
|||
}); |
|||
} |
|||
|
|||
const SalesCard = ({ |
|||
rangePickerValue, |
|||
salesData, |
|||
isActive, |
|||
handleRangePickerChange, |
|||
loading, |
|||
selectDate, |
|||
}: { |
|||
rangePickerValue: RangePickerProps['value']; |
|||
isActive: (key: TimeType) => string; |
|||
salesData: DataItem[]; |
|||
loading: boolean; |
|||
handleRangePickerChange: RangePickerProps['onChange']; |
|||
selectDate: (key: TimeType) => void; |
|||
}) => { |
|||
const { styles } = useStyles(); |
|||
return ( |
|||
<Card |
|||
loading={loading} |
|||
variant="borderless" |
|||
styles={{ |
|||
body: { |
|||
padding: loading ? 24 : 0, |
|||
}, |
|||
}} |
|||
> |
|||
<Tabs |
|||
className={styles.salesCard} |
|||
tabBarExtraContent={ |
|||
<div className={styles.salesExtraWrap}> |
|||
<div className={styles.salesExtra}> |
|||
<Button |
|||
type="text" |
|||
className={isActive('today')} |
|||
onClick={() => selectDate('today')} |
|||
> |
|||
今日 |
|||
</Button> |
|||
<Button |
|||
type="text" |
|||
className={isActive('week')} |
|||
onClick={() => selectDate('week')} |
|||
> |
|||
本周 |
|||
</Button> |
|||
<Button |
|||
type="text" |
|||
className={isActive('month')} |
|||
onClick={() => selectDate('month')} |
|||
> |
|||
本月 |
|||
</Button> |
|||
<Button |
|||
type="text" |
|||
className={isActive('year')} |
|||
onClick={() => selectDate('year')} |
|||
> |
|||
本年 |
|||
</Button> |
|||
</div> |
|||
<RangePicker |
|||
value={rangePickerValue} |
|||
onChange={handleRangePickerChange} |
|||
variant="filled" |
|||
style={{ |
|||
width: 256, |
|||
}} |
|||
/> |
|||
</div> |
|||
} |
|||
size="large" |
|||
tabBarStyle={{ |
|||
marginBottom: 24, |
|||
}} |
|||
items={[ |
|||
{ |
|||
key: 'sales', |
|||
label: '销售额', |
|||
children: ( |
|||
<Row> |
|||
<Col xl={16} lg={12} md={12} sm={24} xs={24}> |
|||
<div className={styles.salesBar}> |
|||
<Column |
|||
height={300} |
|||
data={salesData} |
|||
xField="x" |
|||
yField="y" |
|||
paddingBottom={12} |
|||
axis={{ |
|||
x: { |
|||
title: false, |
|||
}, |
|||
y: { |
|||
title: false, |
|||
gridLineDash: null, |
|||
gridStroke: '#ccc', |
|||
}, |
|||
}} |
|||
scale={{ |
|||
x: { paddingInner: 0.4 }, |
|||
}} |
|||
tooltip={{ |
|||
name: '销售量', |
|||
channel: 'y', |
|||
}} |
|||
/> |
|||
</div> |
|||
</Col> |
|||
<Col xl={8} lg={12} md={12} sm={24} xs={24}> |
|||
<div className={styles.salesRank}> |
|||
<h4 className={styles.rankingTitle}>门店销售额排名</h4> |
|||
<ul className={styles.rankingList}> |
|||
{rankingListData.map((item, i) => ( |
|||
<li key={item.title}> |
|||
<span |
|||
className={`${styles.rankingItemNumber} ${ |
|||
i < 3 ? styles.rankingItemNumberActive : '' |
|||
}`}
|
|||
> |
|||
{i + 1} |
|||
</span> |
|||
<span |
|||
className={styles.rankingItemTitle} |
|||
title={item.title} |
|||
> |
|||
{item.title} |
|||
</span> |
|||
<span>{numeral(item.total).format('0,0')}</span> |
|||
</li> |
|||
))} |
|||
</ul> |
|||
</div> |
|||
</Col> |
|||
</Row> |
|||
), |
|||
}, |
|||
{ |
|||
key: 'views', |
|||
label: '访问量', |
|||
children: ( |
|||
<Row> |
|||
<Col xl={16} lg={12} md={12} sm={24} xs={24}> |
|||
<div className={styles.salesBar}> |
|||
<Column |
|||
height={300} |
|||
data={salesData} |
|||
xField="x" |
|||
yField="y" |
|||
paddingBottom={12} |
|||
axis={{ |
|||
x: { |
|||
title: false, |
|||
}, |
|||
y: { |
|||
title: false, |
|||
}, |
|||
}} |
|||
scale={{ |
|||
x: { paddingInner: 0.4 }, |
|||
}} |
|||
tooltip={{ |
|||
name: '访问量', |
|||
channel: 'y', |
|||
}} |
|||
/> |
|||
</div> |
|||
</Col> |
|||
<Col xl={8} lg={12} md={12} sm={24} xs={24}> |
|||
<div className={styles.salesRank}> |
|||
<h4 className={styles.rankingTitle}>门店访问量排名</h4> |
|||
<ul className={styles.rankingList}> |
|||
{rankingListData.map((item, i) => ( |
|||
<li key={item.title}> |
|||
<span |
|||
className={`${ |
|||
i < 3 |
|||
? styles.rankingItemNumberActive |
|||
: styles.rankingItemNumber |
|||
}`}
|
|||
> |
|||
{i + 1} |
|||
</span> |
|||
<span |
|||
className={styles.rankingItemTitle} |
|||
title={item.title} |
|||
> |
|||
{item.title} |
|||
</span> |
|||
<span>{numeral(item.total).format('0,0')}</span> |
|||
</li> |
|||
))} |
|||
</ul> |
|||
</div> |
|||
</Col> |
|||
</Row> |
|||
), |
|||
}, |
|||
]} |
|||
/> |
|||
</Card> |
|||
); |
|||
}; |
|||
export default SalesCard; |
|||
@ -1,37 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.trendItem { |
|||
display: inline-block; |
|||
font-size: @font-size-base; |
|||
line-height: 22px; |
|||
|
|||
.up, |
|||
.down { |
|||
position: relative; |
|||
top: 1px; |
|||
margin-left: 4px; |
|||
span { |
|||
font-size: 12px; |
|||
transform: scale(0.83); |
|||
} |
|||
} |
|||
.up { |
|||
color: @red-6; |
|||
} |
|||
.down { |
|||
top: -1px; |
|||
color: @green-6; |
|||
} |
|||
|
|||
&.trendItemGrey .up, |
|||
&.trendItemGrey .down { |
|||
color: @text-color; |
|||
} |
|||
|
|||
&.reverseColor .up { |
|||
color: @green-6; |
|||
} |
|||
&.reverseColor .down { |
|||
color: @red-6; |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
trendItem: { |
|||
display: 'inline-block', |
|||
fontSize: token.fontSize, |
|||
lineHeight: '22px', |
|||
}, |
|||
up: { |
|||
color: token['red-6'], |
|||
}, |
|||
down: { |
|||
top: '-1px', |
|||
color: token['green-6'], |
|||
}, |
|||
trendItemGrey: { |
|||
up: { |
|||
color: token.colorText, |
|||
}, |
|||
down: { |
|||
color: token.colorText, |
|||
}, |
|||
}, |
|||
reverseColor: { |
|||
up: { color: token['green-6'] }, |
|||
down: { color: token['red-6'] }, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,47 +0,0 @@ |
|||
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons'; |
|||
import classNames from 'classnames'; |
|||
import React from 'react'; |
|||
import useStyles from './index.style'; |
|||
|
|||
export type TrendProps = { |
|||
colorful?: boolean; |
|||
flag: 'up' | 'down'; |
|||
style?: React.CSSProperties; |
|||
reverseColor?: boolean; |
|||
className?: string; |
|||
children?: React.ReactNode; |
|||
}; |
|||
|
|||
const Trend: React.FC<TrendProps> = ({ |
|||
colorful = true, |
|||
reverseColor = false, |
|||
flag, |
|||
children, |
|||
className, |
|||
...rest |
|||
}) => { |
|||
const { styles } = useStyles(); |
|||
const classString = classNames( |
|||
styles.trendItem, |
|||
{ |
|||
[styles.trendItemGrey]: !colorful, |
|||
[styles.reverseColor]: reverseColor && colorful, |
|||
}, |
|||
className, |
|||
); |
|||
return ( |
|||
<div |
|||
{...rest} |
|||
className={classString} |
|||
title={typeof children === 'string' ? children : ''} |
|||
> |
|||
<span>{children}</span> |
|||
{flag && ( |
|||
<span className={styles[flag]}> |
|||
{flag === 'up' ? <CaretUpOutlined /> : <CaretDownOutlined />} |
|||
</span> |
|||
)} |
|||
</div> |
|||
); |
|||
}; |
|||
export default Trend; |
|||
@ -1,45 +0,0 @@ |
|||
export interface DataItem { |
|||
[field: string]: string | number | number[] | null | undefined; |
|||
} |
|||
export interface VisitDataType { |
|||
x: string; |
|||
y: number; |
|||
} |
|||
|
|||
export type SearchDataType = { |
|||
index: number; |
|||
keyword: string; |
|||
count: number; |
|||
range: number; |
|||
status: number; |
|||
}; |
|||
|
|||
export type OfflineDataType = { |
|||
name: string; |
|||
cvr: number; |
|||
}; |
|||
|
|||
export interface OfflineChartData { |
|||
date: number; |
|||
type: number; |
|||
value: number; |
|||
} |
|||
|
|||
export type RadarData = { |
|||
name: string; |
|||
label: string; |
|||
value: number; |
|||
}; |
|||
|
|||
export interface AnalysisData { |
|||
visitData: DataItem[]; |
|||
visitData2: DataItem[]; |
|||
salesData: DataItem[]; |
|||
searchData: DataItem[]; |
|||
offlineData: OfflineDataType[]; |
|||
offlineChartData: DataItem[]; |
|||
salesTypeData: DataItem[]; |
|||
salesTypeDataOnline: DataItem[]; |
|||
salesTypeDataOffline: DataItem[]; |
|||
radarData: RadarData[]; |
|||
} |
|||
@ -1,157 +0,0 @@ |
|||
import { EllipsisOutlined } from '@ant-design/icons'; |
|||
import { GridContent } from '@ant-design/pro-components'; |
|||
import { useRequest } from '@umijs/max'; |
|||
import { Col, Dropdown, Row } from 'antd'; |
|||
import type { RangePickerProps } from 'antd/es/date-picker'; |
|||
import type { Dayjs } from 'dayjs'; |
|||
import type { FC } from 'react'; |
|||
import { Suspense, useState } from 'react'; |
|||
import IntroduceRow from './components/IntroduceRow'; |
|||
import OfflineData from './components/OfflineData'; |
|||
import PageLoading from './components/PageLoading'; |
|||
import ProportionSales from './components/ProportionSales'; |
|||
import type { TimeType } from './components/SalesCard'; |
|||
import SalesCard from './components/SalesCard'; |
|||
import TopSearch from './components/TopSearch'; |
|||
import type { AnalysisData } from './data.d'; |
|||
import { fakeChartData } from './service'; |
|||
import useStyles from './style.style'; |
|||
import { getTimeDistance } from './utils/utils'; |
|||
|
|||
type RangePickerValue = RangePickerProps['value']; |
|||
type AnalysisProps = { |
|||
dashboardAndanalysis: AnalysisData; |
|||
loading: boolean; |
|||
}; |
|||
type SalesType = 'all' | 'online' | 'stores'; |
|||
const Analysis: FC<AnalysisProps> = () => { |
|||
const { styles } = useStyles(); |
|||
const [salesType, setSalesType] = useState<SalesType>('all'); |
|||
const [currentTabKey, setCurrentTabKey] = useState<string>(''); |
|||
const [rangePickerValue, setRangePickerValue] = useState<RangePickerValue>( |
|||
getTimeDistance('year'), |
|||
); |
|||
const { loading, data } = useRequest(fakeChartData); |
|||
const selectDate = (type: TimeType) => { |
|||
setRangePickerValue(getTimeDistance(type)); |
|||
}; |
|||
const handleRangePickerChange = (value: RangePickerValue) => { |
|||
setRangePickerValue(value); |
|||
}; |
|||
const isActive = (type: TimeType) => { |
|||
if (!rangePickerValue) { |
|||
return ''; |
|||
} |
|||
const value = getTimeDistance(type); |
|||
if (!value) { |
|||
return ''; |
|||
} |
|||
if (!rangePickerValue[0] || !rangePickerValue[1]) { |
|||
return ''; |
|||
} |
|||
if ( |
|||
rangePickerValue[0].isSame(value[0] as Dayjs, 'day') && |
|||
rangePickerValue[1].isSame(value[1] as Dayjs, 'day') |
|||
) { |
|||
return styles.currentDate; |
|||
} |
|||
return ''; |
|||
}; |
|||
|
|||
let salesPieData: any; |
|||
if (salesType === 'all') { |
|||
salesPieData = data?.salesTypeData; |
|||
} else { |
|||
salesPieData = |
|||
salesType === 'online' |
|||
? data?.salesTypeDataOnline |
|||
: data?.salesTypeDataOffline; |
|||
} |
|||
|
|||
const dropdownGroup = ( |
|||
<span className={styles.iconGroup}> |
|||
<Dropdown |
|||
menu={{ |
|||
items: [ |
|||
{ |
|||
key: '1', |
|||
label: '操作一', |
|||
}, |
|||
{ |
|||
key: '2', |
|||
label: '操作二', |
|||
}, |
|||
], |
|||
}} |
|||
placement="bottomRight" |
|||
> |
|||
<EllipsisOutlined /> |
|||
</Dropdown> |
|||
</span> |
|||
); |
|||
const handleChangeSalesType = (value: SalesType) => { |
|||
setSalesType(value); |
|||
}; |
|||
const handleTabChange = (key: string) => { |
|||
setCurrentTabKey(key); |
|||
}; |
|||
const activeKey = currentTabKey || data?.offlineData[0]?.name || ''; |
|||
return ( |
|||
<GridContent> |
|||
<Suspense fallback={<PageLoading />}> |
|||
<IntroduceRow loading={loading} visitData={data?.visitData || []} /> |
|||
</Suspense> |
|||
|
|||
<Suspense fallback={null}> |
|||
<SalesCard |
|||
rangePickerValue={rangePickerValue} |
|||
salesData={data?.salesData || []} |
|||
isActive={isActive} |
|||
handleRangePickerChange={handleRangePickerChange} |
|||
loading={loading} |
|||
selectDate={selectDate} |
|||
/> |
|||
</Suspense> |
|||
|
|||
<Row |
|||
gutter={24} |
|||
style={{ |
|||
marginTop: 24, |
|||
}} |
|||
> |
|||
<Col xl={12} lg={24} md={24} sm={24} xs={24}> |
|||
<Suspense fallback={null}> |
|||
<TopSearch |
|||
loading={loading} |
|||
visitData2={data?.visitData2 || []} |
|||
searchData={data?.searchData || []} |
|||
dropdownGroup={dropdownGroup} |
|||
/> |
|||
</Suspense> |
|||
</Col> |
|||
<Col xl={12} lg={24} md={24} sm={24} xs={24}> |
|||
<Suspense fallback={null}> |
|||
<ProportionSales |
|||
dropdownGroup={dropdownGroup} |
|||
salesType={salesType} |
|||
loading={loading} |
|||
salesPieData={salesPieData || []} |
|||
handleChangeSalesType={handleChangeSalesType} |
|||
/> |
|||
</Suspense> |
|||
</Col> |
|||
</Row> |
|||
|
|||
<Suspense fallback={null}> |
|||
<OfflineData |
|||
activeKey={activeKey} |
|||
loading={loading} |
|||
offlineData={data?.offlineData || []} |
|||
offlineChartData={data?.offlineChartData || []} |
|||
handleTabChange={handleTabChange} |
|||
/> |
|||
</Suspense> |
|||
</GridContent> |
|||
); |
|||
}; |
|||
export default Analysis; |
|||
@ -1,6 +0,0 @@ |
|||
import { request } from '@umijs/max'; |
|||
import type { AnalysisData } from './data'; |
|||
|
|||
export async function fakeChartData(): Promise<{ data: AnalysisData }> { |
|||
return request('/api/fake_analysis_chart_data'); |
|||
} |
|||
@ -1,189 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.iconGroup { |
|||
span.anticon { |
|||
margin-left: 16px; |
|||
color: @text-color-secondary; |
|||
cursor: pointer; |
|||
transition: color 0.32s; |
|||
&:hover { |
|||
color: @text-color; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.rankingList { |
|||
margin: 25px 0 0; |
|||
padding: 0; |
|||
list-style: none; |
|||
li { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-top: 16px; |
|||
zoom: 1; |
|||
&::before, |
|||
&::after { |
|||
display: table; |
|||
content: ' '; |
|||
} |
|||
&::after { |
|||
clear: both; |
|||
height: 0; |
|||
font-size: 0; |
|||
visibility: hidden; |
|||
} |
|||
span { |
|||
color: @text-color; |
|||
font-size: 14px; |
|||
line-height: 22px; |
|||
} |
|||
.rankingItemNumber { |
|||
display: inline-block; |
|||
width: 20px; |
|||
height: 20px; |
|||
margin-top: 1.5px; |
|||
margin-right: 16px; |
|||
font-weight: 600; |
|||
font-size: 12px; |
|||
line-height: 20px; |
|||
text-align: center; |
|||
background-color: @tag-default-bg; |
|||
border-radius: 20px; |
|||
&.active { |
|||
color: #fff; |
|||
background-color: #314659; |
|||
} |
|||
} |
|||
.rankingItemTitle { |
|||
flex: 1; |
|||
margin-right: 8px; |
|||
overflow: hidden; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.salesExtra { |
|||
display: inline-block; |
|||
margin-right: 24px; |
|||
a { |
|||
margin-left: 24px; |
|||
color: @text-color; |
|||
&:hover { |
|||
color: @primary-color; |
|||
} |
|||
&.currentDate { |
|||
color: @primary-color; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.salesCard { |
|||
.salesBar { |
|||
padding: 0 0 32px 32px; |
|||
} |
|||
.salesRank { |
|||
padding: 0 32px 32px 72px; |
|||
} |
|||
:global { |
|||
.ant-tabs-bar, |
|||
.ant-tabs-nav-wrap { |
|||
padding-left: 16px; |
|||
.ant-tabs-nav .ant-tabs-tab { |
|||
padding-top: 16px; |
|||
padding-bottom: 14px; |
|||
line-height: 24px; |
|||
} |
|||
} |
|||
.ant-tabs-extra-content { |
|||
padding-right: 24px; |
|||
line-height: 55px; |
|||
} |
|||
.ant-card-head { |
|||
position: relative; |
|||
} |
|||
.ant-card-head-title { |
|||
align-items: normal; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.salesCardExtra { |
|||
height: inherit; |
|||
} |
|||
|
|||
.salesTypeRadio { |
|||
position: absolute; |
|||
right: 54px; |
|||
bottom: 12px; |
|||
} |
|||
|
|||
.offlineCard { |
|||
:global { |
|||
.ant-tabs-ink-bar { |
|||
bottom: auto; |
|||
} |
|||
.ant-tabs-bar { |
|||
border-bottom: none; |
|||
} |
|||
.ant-tabs-nav-container-scrolling { |
|||
padding-right: 40px; |
|||
padding-left: 40px; |
|||
} |
|||
.ant-tabs-tab-prev-icon::before { |
|||
position: relative; |
|||
left: 6px; |
|||
} |
|||
.ant-tabs-tab-next-icon::before { |
|||
position: relative; |
|||
right: 6px; |
|||
} |
|||
.ant-tabs-tab-active h4 { |
|||
color: @primary-color; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.trendText { |
|||
margin-left: 8px; |
|||
color: @heading-color; |
|||
} |
|||
|
|||
@media screen and (max-width: @screen-lg) { |
|||
.salesExtra { |
|||
display: none; |
|||
} |
|||
|
|||
.rankingList { |
|||
li { |
|||
span:first-child { |
|||
margin-right: 8px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media screen and (max-width: @screen-md) { |
|||
.rankingTitle { |
|||
margin-top: 16px; |
|||
} |
|||
|
|||
.salesCard .salesBar { |
|||
padding: 16px; |
|||
} |
|||
} |
|||
|
|||
@media screen and (max-width: @screen-sm) { |
|||
.salesExtraWrap { |
|||
display: none; |
|||
} |
|||
|
|||
.salesCard { |
|||
:global { |
|||
.ant-tabs-content { |
|||
padding-top: 30px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,160 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
iconGroup: { |
|||
'span.anticon': { |
|||
marginLeft: '16px', |
|||
color: token.colorTextSecondary, |
|||
cursor: 'pointer', |
|||
transition: 'color 0.32s', |
|||
'&:hover': { |
|||
color: token.colorText, |
|||
}, |
|||
}, |
|||
}, |
|||
rankingList: { |
|||
margin: '25px 0 0', |
|||
padding: '0', |
|||
listStyle: 'none', |
|||
li: { |
|||
display: 'flex', |
|||
alignItems: 'center', |
|||
marginTop: '16px', |
|||
zoom: '1', |
|||
'&::before, &::after': { |
|||
display: 'table', |
|||
content: "' '", |
|||
}, |
|||
'&::after': { |
|||
clear: 'both', |
|||
height: '0', |
|||
fontSize: '0', |
|||
visibility: 'hidden', |
|||
}, |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenLG}px)`]: { |
|||
li: { |
|||
'span:first-child': { marginRight: '8px' }, |
|||
}, |
|||
}, |
|||
}, |
|||
rankingItemNumber: { |
|||
display: 'inline-block', |
|||
width: '20px', |
|||
height: '20px', |
|||
marginTop: '1.5px', |
|||
marginRight: '16px', |
|||
fontWeight: '600', |
|||
fontSize: '12px', |
|||
lineHeight: '20px', |
|||
textAlign: 'center', |
|||
borderRadius: '20px', |
|||
backgroundColor: token.colorBgContainerDisabled, |
|||
}, |
|||
rankingItemTitle: { |
|||
flex: '1', |
|||
marginRight: '8px', |
|||
overflow: 'hidden', |
|||
whiteSpace: 'nowrap', |
|||
textOverflow: 'ellipsis', |
|||
}, |
|||
rankingItemNumberActive: { |
|||
display: 'inline-block', |
|||
width: '20px', |
|||
height: '20px', |
|||
marginTop: '1.5px', |
|||
marginRight: '16px', |
|||
fontWeight: '600', |
|||
fontSize: '12px', |
|||
lineHeight: '20px', |
|||
textAlign: 'center', |
|||
borderRadius: '20px', |
|||
color: '#fff', |
|||
backgroundColor: token.colorBgSpotlight, |
|||
}, |
|||
salesExtra: { |
|||
display: 'inline-block', |
|||
marginRight: '24px', |
|||
a: { |
|||
marginLeft: '24px', |
|||
color: token.colorText, |
|||
'&:hover': { |
|||
color: token.colorPrimary, |
|||
}, |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenLG}px)`]: { |
|||
display: 'none', |
|||
}, |
|||
}, |
|||
currentDate: { |
|||
color: token.colorPrimary, |
|||
fontWeight: 'bold', |
|||
}, |
|||
salesBar: { |
|||
padding: '0 0 32px 32px', |
|||
[`@media screen and (max-width: ${token.screenMD}px)`]: { |
|||
padding: '16px', |
|||
}, |
|||
}, |
|||
salesRank: { |
|||
padding: '0 32px 32px 72px', |
|||
}, |
|||
salesCard: { |
|||
'.ant-tabs-bar, .ant-tabs-nav-wrap': { |
|||
paddingLeft: '16px', |
|||
'.ant-tabs-nav .ant-tabs-tab': { |
|||
paddingTop: '16px', |
|||
paddingBottom: '14px', |
|||
lineHeight: '24px', |
|||
}, |
|||
}, |
|||
'.ant-tabs-extra-content': { paddingRight: '24px', lineHeight: '55px' }, |
|||
'.ant-card-head': { position: 'relative' }, |
|||
'.ant-card-head-title': { alignItems: 'normal' }, |
|||
[`@media screen and (max-width: ${token.screenMD}px)`]: { |
|||
padding: '16px', |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenSM}px)`]: { |
|||
'.ant-tabs-content': { |
|||
paddingTop: '30px', |
|||
}, |
|||
}, |
|||
}, |
|||
salesCardExtra: { |
|||
height: 'inherit', |
|||
}, |
|||
salesTypeRadio: { |
|||
position: 'absolute', |
|||
right: '54px', |
|||
bottom: '12px', |
|||
}, |
|||
offlineCard: { |
|||
'.ant-tabs-ink-bar': { bottom: 'auto' }, |
|||
'.ant-tabs-bar': { borderBottom: 'none' }, |
|||
'.ant-tabs-nav-container-scrolling': { |
|||
paddingRight: '40px', |
|||
paddingLeft: '40px', |
|||
}, |
|||
'.ant-tabs-tab-prev-icon::before': { position: 'relative', left: '6px' }, |
|||
'.ant-tabs-tab-next-icon::before': { position: 'relative', right: '6px' }, |
|||
'.ant-tabs-tab-active h4': { color: token.colorPrimary }, |
|||
}, |
|||
trendText: { |
|||
marginLeft: '8px', |
|||
color: token.colorTextHeading, |
|||
}, |
|||
rankingTitle: { |
|||
[`@media screen and (max-width: ${token.screenMD}px)`]: { |
|||
marginTop: '16px', |
|||
}, |
|||
}, |
|||
salesExtraWrap: { |
|||
[`@media screen and (max-width: ${token.screenSM}px)`]: { |
|||
display: 'none', |
|||
}, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,33 +0,0 @@ |
|||
import React from 'react'; |
|||
import { yuan } from '../components/Charts'; |
|||
/** 减少使用 dangerouslySetInnerHTML */ |
|||
export default class Yuan extends React.Component<{ |
|||
children: string | number; |
|||
}> { |
|||
main: HTMLSpanElement | undefined | null = null; |
|||
|
|||
componentDidMount() { |
|||
this.renderToHtml(); |
|||
} |
|||
|
|||
componentDidUpdate() { |
|||
this.renderToHtml(); |
|||
} |
|||
|
|||
renderToHtml = () => { |
|||
const { children } = this.props; |
|||
if (this.main) { |
|||
this.main.innerHTML = yuan(children); |
|||
} |
|||
}; |
|||
|
|||
render() { |
|||
return ( |
|||
<span |
|||
ref={(ref) => { |
|||
this.main = ref; |
|||
}} |
|||
/> |
|||
); |
|||
} |
|||
} |
|||
@ -1,57 +0,0 @@ |
|||
import type { RangePickerProps } from 'antd/es/date-picker'; |
|||
import dayjs from 'dayjs'; |
|||
|
|||
type RangePickerValue = RangePickerProps['value']; |
|||
|
|||
export function fixedZero(val: number) { |
|||
return val * 1 < 10 ? `0${val}` : val; |
|||
} |
|||
|
|||
export function getTimeDistance( |
|||
type: 'today' | 'week' | 'month' | 'year', |
|||
): RangePickerValue { |
|||
const now = new Date(); |
|||
const oneDay = 1000 * 60 * 60 * 24; |
|||
|
|||
if (type === 'today') { |
|||
now.setHours(0); |
|||
now.setMinutes(0); |
|||
now.setSeconds(0); |
|||
return [dayjs(now), dayjs(now.getTime() + (oneDay - 1000))]; |
|||
} |
|||
|
|||
if (type === 'week') { |
|||
let day = now.getDay(); |
|||
now.setHours(0); |
|||
now.setMinutes(0); |
|||
now.setSeconds(0); |
|||
|
|||
if (day === 0) { |
|||
day = 6; |
|||
} else { |
|||
day -= 1; |
|||
} |
|||
|
|||
const beginTime = now.getTime() - day * oneDay; |
|||
|
|||
return [dayjs(beginTime), dayjs(beginTime + (7 * oneDay - 1000))]; |
|||
} |
|||
const year = now.getFullYear(); |
|||
|
|||
if (type === 'month') { |
|||
const month = now.getMonth(); |
|||
const nextDate = dayjs(now).add(1, 'months'); |
|||
const nextYear = nextDate.year(); |
|||
const nextMonth = nextDate.month(); |
|||
|
|||
return [ |
|||
dayjs(`${year}-${fixedZero(month + 1)}-01 00:00:00`), |
|||
dayjs( |
|||
dayjs(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() - |
|||
1000, |
|||
), |
|||
]; |
|||
} |
|||
|
|||
return [dayjs(`${year}-01-01 00:00:00`), dayjs(`${year}-12-31 23:59:59`)]; |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
import type { Request, Response } from 'express'; |
|||
import mockjs from 'mockjs'; |
|||
|
|||
const getTags = (_: Request, res: Response) => { |
|||
return res.json({ |
|||
data: mockjs.mock({ |
|||
'list|100': [{ name: '@city', 'value|1-100': 150, 'type|0-2': 1 }], |
|||
}), |
|||
}); |
|||
}; |
|||
|
|||
export default { |
|||
'GET /api/tags': getTags, |
|||
}; |
|||
@ -1,51 +0,0 @@ |
|||
.activeChart { |
|||
position: relative; |
|||
} |
|||
.activeChartGrid { |
|||
p { |
|||
position: absolute; |
|||
top: 80px; |
|||
} |
|||
p:last-child { |
|||
top: 115px; |
|||
} |
|||
} |
|||
.activeChartLegend { |
|||
position: relative; |
|||
height: 20px; |
|||
margin-top: 8px; |
|||
font-size: 0; |
|||
line-height: 20px; |
|||
span { |
|||
display: inline-block; |
|||
width: 33.33%; |
|||
font-size: 12px; |
|||
text-align: center; |
|||
} |
|||
span:first-child { |
|||
text-align: left; |
|||
} |
|||
span:last-child { |
|||
text-align: right; |
|||
} |
|||
} |
|||
.dashedLine { |
|||
position: relative; |
|||
top: -70px; |
|||
left: -3px; |
|||
height: 1px; |
|||
|
|||
.line { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
background-image: linear-gradient(to right, transparent 50%, #e9e9e9 50%); |
|||
background-size: 6px; |
|||
} |
|||
} |
|||
|
|||
.dashedLine:last-child { |
|||
top: -36px; |
|||
} |
|||
@ -1,48 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(() => { |
|||
return { |
|||
activeChart: { |
|||
position: 'relative', |
|||
}, |
|||
activeChartGrid: { |
|||
p: { position: 'absolute', top: '80px' }, |
|||
'p:last-child': { top: '115px' }, |
|||
}, |
|||
activeChartLegend: { |
|||
position: 'relative', |
|||
height: '20px', |
|||
marginTop: '8px', |
|||
fontSize: '0', |
|||
lineHeight: '20px', |
|||
span: { |
|||
display: 'inline-block', |
|||
width: '33.33%', |
|||
fontSize: '12px', |
|||
textAlign: 'center', |
|||
}, |
|||
'span:first-child': { textAlign: 'left' }, |
|||
'span:last-child': { textAlign: 'right' }, |
|||
}, |
|||
dashedLine: { |
|||
position: 'relative', |
|||
top: '-70px', |
|||
left: '-3px', |
|||
height: '1px', |
|||
}, |
|||
line: { |
|||
position: 'absolute', |
|||
top: '0', |
|||
left: '0', |
|||
width: '100%', |
|||
height: '100%', |
|||
backgroundImage: |
|||
'linear-gradient(to right, transparent 50%, #e9e9e9 50%)', |
|||
backgroundSize: '6px', |
|||
}, |
|||
'dashedLine:last-child': { |
|||
top: '-36px', |
|||
}, |
|||
}; |
|||
}); |
|||
export default useStyles; |
|||
@ -1,93 +0,0 @@ |
|||
import { Area } from '@ant-design/plots'; |
|||
import { Statistic } from 'antd'; |
|||
import { useCallback, useEffect, useRef, useState } from 'react'; |
|||
import useStyles from './index.style'; |
|||
|
|||
function fixedZero(val: number) { |
|||
return val * 1 < 10 ? `0${val}` : val; |
|||
} |
|||
function getActiveData() { |
|||
const activeData = []; |
|||
for (let i = 0; i < 24; i += 1) { |
|||
activeData.push({ |
|||
x: `${fixedZero(i)}:00`, |
|||
y: Math.floor(Math.random() * 200) + i * 50, |
|||
}); |
|||
} |
|||
return activeData; |
|||
} |
|||
|
|||
const ActiveChart = () => { |
|||
const timerRef = useRef<number | null>(null); |
|||
const requestRef = useRef<number | null>(null); |
|||
const { styles } = useStyles(); |
|||
const [activeData, setActiveData] = useState<{ x: string; y: number }[]>([]); |
|||
const loopData = useCallback(() => { |
|||
requestRef.current = requestAnimationFrame(() => { |
|||
timerRef.current = window.setTimeout(() => { |
|||
setActiveData(getActiveData()); |
|||
loopData(); |
|||
}, 2000); |
|||
}); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
loopData(); |
|||
return () => { |
|||
clearTimeout(timerRef.current as number); |
|||
if (requestRef.current) { |
|||
cancelAnimationFrame(requestRef.current); |
|||
} |
|||
}; |
|||
}, [loopData]); |
|||
|
|||
return ( |
|||
<div className={styles.activeChart}> |
|||
<Statistic title="目标评估" value="有望达到预期" /> |
|||
<div |
|||
style={{ |
|||
marginTop: 32, |
|||
}} |
|||
> |
|||
<Area |
|||
padding={[0, 0, 0, 0]} |
|||
xField="x" |
|||
axis={false} |
|||
yField="y" |
|||
height={84} |
|||
style={{ |
|||
fill: 'linear-gradient(-90deg, white 0%, #6294FA 100%)', |
|||
fillOpacity: 0.6, |
|||
}} |
|||
data={activeData} |
|||
/> |
|||
</div> |
|||
{activeData && ( |
|||
<div> |
|||
<div className={styles.activeChartGrid}> |
|||
<p>{[...activeData].sort()[activeData.length - 1]?.y + 200} 亿元</p> |
|||
<p> |
|||
{[...activeData].sort()[Math.floor(activeData.length / 2)]?.y}{' '} |
|||
亿元 |
|||
</p> |
|||
</div> |
|||
<div className={styles.dashedLine}> |
|||
<div className={styles.line} /> |
|||
</div> |
|||
<div className={styles.dashedLine}> |
|||
<div className={styles.line} /> |
|||
</div> |
|||
</div> |
|||
)} |
|||
{activeData && ( |
|||
<div className={styles.activeChartLegend}> |
|||
<span>00:00</span> |
|||
<span>{activeData[Math.floor(activeData.length / 2)]?.x}</span> |
|||
<span>{activeData[activeData.length - 1]?.x}</span> |
|||
</div> |
|||
)} |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default ActiveChart; |
|||
@ -1,225 +0,0 @@ |
|||
import React, { Component } from 'react'; |
|||
import autoHeight from '../autoHeight'; |
|||
|
|||
/* eslint no-return-assign: 0 */ |
|||
/* eslint no-mixed-operators: 0 */ |
|||
// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90
|
|||
|
|||
export type WaterWaveProps = { |
|||
title: React.ReactNode; |
|||
color?: string; |
|||
height?: number; |
|||
percent: number; |
|||
style?: React.CSSProperties; |
|||
}; |
|||
class WaterWave extends Component<WaterWaveProps> { |
|||
state = { |
|||
radio: 1, |
|||
}; |
|||
timer: number = 0; |
|||
root: HTMLDivElement | undefined | null = null; |
|||
node: HTMLCanvasElement | undefined | null = null; |
|||
componentDidMount() { |
|||
this.renderChart(); |
|||
this.resize(); |
|||
window.addEventListener( |
|||
'resize', |
|||
() => { |
|||
requestAnimationFrame(() => this.resize()); |
|||
}, |
|||
{ |
|||
passive: true, |
|||
}, |
|||
); |
|||
} |
|||
componentDidUpdate(props: WaterWaveProps) { |
|||
const { percent } = this.props; |
|||
if (props.percent !== percent) { |
|||
// 不加这个会造成绘制缓慢
|
|||
this.renderChart('update'); |
|||
} |
|||
} |
|||
componentWillUnmount() { |
|||
cancelAnimationFrame(this.timer); |
|||
if (this.node) { |
|||
this.node.innerHTML = ''; |
|||
} |
|||
window.removeEventListener('resize', this.resize); |
|||
} |
|||
resize = () => { |
|||
if (this.root) { |
|||
const { height = 1 } = this.props; |
|||
const { offsetWidth } = this.root.parentNode as HTMLElement; |
|||
this.setState({ |
|||
radio: offsetWidth < height ? offsetWidth / height : 1, |
|||
}); |
|||
} |
|||
}; |
|||
renderChart(type?: string) { |
|||
const { percent, color = '#1890FF' } = this.props; |
|||
const data = percent / 100; |
|||
cancelAnimationFrame(this.timer); |
|||
if (!this.node || (data !== 0 && !data)) { |
|||
return; |
|||
} |
|||
const canvas = this.node; |
|||
const ctx = canvas.getContext('2d'); |
|||
if (!ctx) { |
|||
return; |
|||
} |
|||
const canvasWidth = canvas.width; |
|||
const canvasHeight = canvas.height; |
|||
const radius = canvasWidth / 2; |
|||
const lineWidth = 2; |
|||
const cR = radius - lineWidth; |
|||
ctx.beginPath(); |
|||
ctx.lineWidth = lineWidth * 2; |
|||
const axisLength = canvasWidth - lineWidth; |
|||
const unit = axisLength / 8; |
|||
const range = 0.2; // 振幅
|
|||
let currRange = range; |
|||
const xOffset = lineWidth; |
|||
let sp = 0; // 周期偏移量
|
|||
let currData = 0; |
|||
const waveupsp = 0.005; // 水波上涨速度
|
|||
|
|||
let arcStack: number[][] = []; |
|||
const bR = radius - lineWidth; |
|||
const circleOffset = -(Math.PI / 2); |
|||
let circleLock = true; |
|||
for ( |
|||
let i = circleOffset; |
|||
i < circleOffset + 2 * Math.PI; |
|||
i += 1 / (8 * Math.PI) |
|||
) { |
|||
arcStack.push([radius + bR * Math.cos(i), radius + bR * Math.sin(i)]); |
|||
} |
|||
const cStartPoint = arcStack.shift() as number[]; |
|||
ctx.strokeStyle = color; |
|||
ctx.moveTo(cStartPoint[0], cStartPoint[1]); |
|||
const drawSin = () => { |
|||
if (!ctx) { |
|||
return; |
|||
} |
|||
ctx.beginPath(); |
|||
ctx.save(); |
|||
const sinStack = []; |
|||
for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) { |
|||
const x = sp + (xOffset + i) / unit; |
|||
const y = Math.sin(x) * currRange; |
|||
const dx = i; |
|||
const dy = 2 * cR * (1 - currData) + (radius - cR) - unit * y; |
|||
ctx.lineTo(dx, dy); |
|||
sinStack.push([dx, dy]); |
|||
} |
|||
const startPoint = sinStack.shift() as number[]; |
|||
ctx.lineTo(xOffset + axisLength, canvasHeight); |
|||
ctx.lineTo(xOffset, canvasHeight); |
|||
ctx.lineTo(startPoint[0], startPoint[1]); |
|||
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight); |
|||
gradient.addColorStop(0, '#ffffff'); |
|||
gradient.addColorStop(1, color); |
|||
ctx.fillStyle = gradient; |
|||
ctx.fill(); |
|||
ctx.restore(); |
|||
}; |
|||
const render = () => { |
|||
if (!ctx) { |
|||
return; |
|||
} |
|||
ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
|||
if (circleLock && type !== 'update') { |
|||
if (arcStack.length) { |
|||
const temp = arcStack.shift() as number[]; |
|||
ctx.lineTo(temp[0], temp[1]); |
|||
ctx.stroke(); |
|||
} else { |
|||
circleLock = false; |
|||
ctx.lineTo(cStartPoint[0], cStartPoint[1]); |
|||
ctx.stroke(); |
|||
arcStack = []; |
|||
ctx.globalCompositeOperation = 'destination-over'; |
|||
ctx.beginPath(); |
|||
ctx.lineWidth = lineWidth; |
|||
ctx.arc(radius, radius, bR, 0, 2 * Math.PI, true); |
|||
ctx.beginPath(); |
|||
ctx.save(); |
|||
ctx.arc(radius, radius, radius - 3 * lineWidth, 0, 2 * Math.PI, true); |
|||
ctx.restore(); |
|||
ctx.clip(); |
|||
ctx.fillStyle = color; |
|||
} |
|||
} else { |
|||
if (data >= 0.85) { |
|||
if (currRange > range / 4) { |
|||
const t = range * 0.01; |
|||
currRange -= t; |
|||
} |
|||
} else if (data <= 0.1) { |
|||
if (currRange < range * 1.5) { |
|||
const t = range * 0.01; |
|||
currRange += t; |
|||
} |
|||
} else { |
|||
if (currRange <= range) { |
|||
const t = range * 0.01; |
|||
currRange += t; |
|||
} |
|||
if (currRange >= range) { |
|||
const t = range * 0.01; |
|||
currRange -= t; |
|||
} |
|||
} |
|||
if (data - currData > 0) { |
|||
currData += waveupsp; |
|||
} |
|||
if (data - currData < 0) { |
|||
currData -= waveupsp; |
|||
} |
|||
sp += 0.07; |
|||
drawSin(); |
|||
} |
|||
this.timer = requestAnimationFrame(render); |
|||
}; |
|||
render(); |
|||
} |
|||
render() { |
|||
const { radio } = this.state; |
|||
const { percent, title, height = 1 } = this.props; |
|||
return ( |
|||
<div |
|||
ref={(n) => { |
|||
this.root = n; |
|||
}} |
|||
style={{ |
|||
transform: `scale(${radio})`, |
|||
}} |
|||
> |
|||
<div |
|||
style={{ |
|||
width: height, |
|||
height, |
|||
overflow: 'hidden', |
|||
}} |
|||
> |
|||
<canvas |
|||
ref={(n) => { |
|||
this.node = n; |
|||
}} |
|||
width={height * 2} |
|||
height={height * 2} |
|||
/> |
|||
</div> |
|||
<div |
|||
style={{ |
|||
width: height, |
|||
}} |
|||
> |
|||
{title && <span>{title}</span>} |
|||
<h4>{percent}%</h4> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
export default autoHeight()(WaterWave); |
|||
@ -1,78 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
export type IReactComponent<P = any> = |
|||
| React.ComponentClass<P> |
|||
| React.ClassicComponentClass<P>; |
|||
|
|||
function computeHeight(node: HTMLDivElement) { |
|||
const { style } = node; |
|||
style.height = '100%'; |
|||
const totalHeight = parseInt(`${getComputedStyle(node).height}`, 10); |
|||
const padding = |
|||
parseInt(`${getComputedStyle(node).paddingTop}`, 10) + |
|||
parseInt(`${getComputedStyle(node).paddingBottom}`, 10); |
|||
return totalHeight - padding; |
|||
} |
|||
|
|||
function getAutoHeight(n: HTMLDivElement) { |
|||
if (!n) { |
|||
return 0; |
|||
} |
|||
|
|||
const node = n; |
|||
|
|||
let height = computeHeight(node); |
|||
const parentNode = node.parentNode as HTMLDivElement; |
|||
if (parentNode) { |
|||
height = computeHeight(parentNode); |
|||
} |
|||
|
|||
return height; |
|||
} |
|||
|
|||
type AutoHeightProps = { |
|||
height?: number; |
|||
}; |
|||
|
|||
function autoHeight() { |
|||
return <P extends AutoHeightProps>( |
|||
WrappedComponent: React.ComponentClass<P> | React.FC<P>, |
|||
): React.ComponentClass<P> => { |
|||
class AutoHeightComponent extends React.Component<P & AutoHeightProps> { |
|||
state = { |
|||
computedHeight: 0, |
|||
}; |
|||
|
|||
root: HTMLDivElement | null = null; |
|||
|
|||
componentDidMount() { |
|||
const { height } = this.props; |
|||
if (!height && this.root) { |
|||
let h = getAutoHeight(this.root); |
|||
this.setState({ computedHeight: h }); |
|||
if (h < 1) { |
|||
h = getAutoHeight(this.root); |
|||
this.setState({ computedHeight: h }); |
|||
} |
|||
} |
|||
} |
|||
|
|||
handleRoot = (node: HTMLDivElement) => { |
|||
this.root = node; |
|||
}; |
|||
|
|||
render() { |
|||
const { height } = this.props; |
|||
const { computedHeight } = this.state; |
|||
const h = height || computedHeight; |
|||
return ( |
|||
<div ref={this.handleRoot}> |
|||
{h > 0 && <WrappedComponent {...this.props} height={h} />} |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
return AutoHeightComponent; |
|||
}; |
|||
} |
|||
export default autoHeight; |
|||
@ -1,153 +0,0 @@ |
|||
import { PageLoading } from '@ant-design/pro-components'; |
|||
import { HeatmapLayer, MapboxScene, PointLayer } from '@antv/l7-react'; |
|||
import * as React from 'react'; |
|||
|
|||
const colors = [ |
|||
'#eff3ff', |
|||
'#c6dbef', |
|||
'#9ecae1', |
|||
'#6baed6', |
|||
'#4292c6', |
|||
'#2171b5', |
|||
'#084594', |
|||
]; |
|||
export default class MonitorMap extends React.Component { |
|||
state = { |
|||
data: null, |
|||
grid: null, |
|||
loading: false, |
|||
}; |
|||
|
|||
public async componentDidMount() { |
|||
const [geoData, gridData] = await Promise.all([ |
|||
fetch( |
|||
'https://gw.alipayobjects.com/os/bmw-prod/c5dba875-b6ea-4e88-b778-66a862906c93.json', |
|||
).then((d) => d.json()), |
|||
fetch( |
|||
'https://gw.alipayobjects.com/os/bmw-prod/8990e8b4-c58e-419b-afb9-8ea3daff2dd1.json', |
|||
).then((d) => d.json()), |
|||
]); |
|||
this.setState({ |
|||
data: geoData, |
|||
grid: gridData, |
|||
loading: true, |
|||
}); |
|||
} |
|||
|
|||
public render() { |
|||
const { data, grid, loading } = this.state; |
|||
return loading === false ? ( |
|||
<PageLoading /> |
|||
) : ( |
|||
<MapboxScene |
|||
map={{ |
|||
center: [110.19382669582967, 50.258134], |
|||
pitch: 0, |
|||
style: 'blank', |
|||
zoom: 1, |
|||
}} |
|||
style={{ |
|||
position: 'relative', |
|||
width: '100%', |
|||
height: '452px', |
|||
}} |
|||
> |
|||
{grid && ( |
|||
<HeatmapLayer |
|||
key="1" |
|||
source={{ |
|||
data: grid, |
|||
transforms: [ |
|||
{ |
|||
type: 'hexagon', |
|||
size: 800000, |
|||
field: 'capacity', |
|||
method: 'sum', |
|||
}, |
|||
], |
|||
}} |
|||
color={{ |
|||
values: '#ddd', |
|||
}} |
|||
shape={{ |
|||
values: 'hexagon', |
|||
}} |
|||
style={{ |
|||
coverage: 0.7, |
|||
opacity: 0.8, |
|||
}} |
|||
/> |
|||
)} |
|||
{data && [ |
|||
<PointLayer |
|||
key="2" |
|||
options={{ |
|||
autoFit: true, |
|||
}} |
|||
source={{ |
|||
data, |
|||
}} |
|||
scale={{ |
|||
values: { |
|||
color: { |
|||
field: 'cum_conf', |
|||
type: 'quantile', |
|||
}, |
|||
size: { |
|||
field: 'cum_conf', |
|||
type: 'log', |
|||
}, |
|||
}, |
|||
}} |
|||
color={{ |
|||
field: 'cum_conf', |
|||
values: colors, |
|||
}} |
|||
shape={{ |
|||
values: 'circle', |
|||
}} |
|||
active={{ |
|||
option: { |
|||
color: '#0c2c84', |
|||
}, |
|||
}} |
|||
size={{ |
|||
field: 'cum_conf', |
|||
values: [0, 30], |
|||
}} |
|||
style={{ |
|||
opacity: 0.8, |
|||
}} |
|||
/>, |
|||
<PointLayer |
|||
key="5" |
|||
source={{ |
|||
data, |
|||
}} |
|||
color={{ |
|||
values: '#fff', |
|||
}} |
|||
shape={{ |
|||
field: 'Short_Name_ZH', |
|||
values: 'text', |
|||
}} |
|||
filter={{ |
|||
field: 'cum_conf', |
|||
values: (v) => { |
|||
return v > 2000; |
|||
}, |
|||
}} |
|||
size={{ |
|||
values: 12, |
|||
}} |
|||
style={{ |
|||
opacity: 1, |
|||
strokeOpacity: 1, |
|||
strokeWidth: 0, |
|||
}} |
|||
/>, |
|||
]} |
|||
</MapboxScene> |
|||
); |
|||
} |
|||
} |
|||
@ -1,5 +0,0 @@ |
|||
export type TagType = { |
|||
name: string; |
|||
value: number; |
|||
type: string; |
|||
}; |
|||
@ -1,6 +0,0 @@ |
|||
import { request } from '@umijs/max'; |
|||
import type { TagType } from './data'; |
|||
|
|||
export async function queryTags(): Promise<{ data: { list: TagType[] } }> { |
|||
return request('/api/tags'); |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.mapChart { |
|||
height: 452px; |
|||
padding-top: 24px; |
|||
img { |
|||
display: inline-block; |
|||
max-width: 100%; |
|||
max-height: 437px; |
|||
} |
|||
} |
|||
|
|||
.pieCard :global(.pie-stat) { |
|||
font-size: 24px !important; |
|||
} |
|||
|
|||
@media screen and (max-width: @screen-lg) { |
|||
.mapChart { |
|||
height: auto; |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
mapChart: { |
|||
height: '452px', |
|||
paddingTop: '24px', |
|||
img: { display: 'inline-block', maxWidth: '100%', maxHeight: '437px' }, |
|||
[`@media screen and (max-width: ${token.screenLG}px)`]: { |
|||
height: 'auto', |
|||
}, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,410 +0,0 @@ |
|||
import dayjs from 'dayjs'; |
|||
import type { Request, Response } from 'express'; |
|||
import type { DataItem, OfflineDataType, SearchDataType } from './data.d'; |
|||
|
|||
// mock data
|
|||
const visitData: DataItem[] = []; |
|||
const beginDay = Date.now(); |
|||
|
|||
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]; |
|||
for (let i = 0; i < fakeY.length; i += 1) { |
|||
visitData.push({ |
|||
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'), |
|||
y: fakeY[i], |
|||
}); |
|||
} |
|||
|
|||
const visitData2: DataItem[] = []; |
|||
const fakeY2 = [1, 6, 4, 8, 3, 7, 2]; |
|||
for (let i = 0; i < fakeY2.length; i += 1) { |
|||
visitData2.push({ |
|||
x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'), |
|||
y: fakeY2[i], |
|||
}); |
|||
} |
|||
|
|||
const salesData: DataItem[] = []; |
|||
for (let i = 0; i < 12; i += 1) { |
|||
salesData.push({ |
|||
x: `${i + 1}月`, |
|||
y: Math.floor(Math.random() * 1000) + 200, |
|||
}); |
|||
} |
|||
const searchData: SearchDataType[] = []; |
|||
for (let i = 0; i < 50; i += 1) { |
|||
searchData.push({ |
|||
index: i + 1, |
|||
keyword: `搜索关键词-${i}`, |
|||
count: Math.floor(Math.random() * 1000), |
|||
range: Math.floor(Math.random() * 100), |
|||
status: Math.floor((Math.random() * 10) % 2), |
|||
}); |
|||
} |
|||
const salesTypeData = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 4544, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 3321, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 3113, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 2341, |
|||
}, |
|||
{ |
|||
x: '母婴产品', |
|||
y: 1231, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 1231, |
|||
}, |
|||
]; |
|||
|
|||
const salesTypeDataOnline = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 244, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 321, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 311, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 41, |
|||
}, |
|||
{ |
|||
x: '母婴产品', |
|||
y: 121, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 111, |
|||
}, |
|||
]; |
|||
|
|||
const salesTypeDataOffline = [ |
|||
{ |
|||
x: '家用电器', |
|||
y: 99, |
|||
}, |
|||
{ |
|||
x: '食用酒水', |
|||
y: 188, |
|||
}, |
|||
{ |
|||
x: '个护健康', |
|||
y: 344, |
|||
}, |
|||
{ |
|||
x: '服饰箱包', |
|||
y: 255, |
|||
}, |
|||
{ |
|||
x: '其他', |
|||
y: 65, |
|||
}, |
|||
]; |
|||
|
|||
const offlineData: OfflineDataType[] = []; |
|||
for (let i = 0; i < 10; i += 1) { |
|||
offlineData.push({ |
|||
name: `Stores ${i}`, |
|||
cvr: Math.ceil(Math.random() * 9) / 10, |
|||
}); |
|||
} |
|||
const offlineChartData: DataItem[] = []; |
|||
for (let i = 0; i < 20; i += 1) { |
|||
offlineChartData.push({ |
|||
x: Date.now() + 1000 * 60 * 30 * i, |
|||
y1: Math.floor(Math.random() * 100) + 10, |
|||
y2: Math.floor(Math.random() * 100) + 10, |
|||
}); |
|||
} |
|||
|
|||
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 avatars2 = [ |
|||
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/psOgztMplJMGpVEqfcgF.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/ZpBqSxLxVEXfcUNoPKrz.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/laiEnJdGHVOhJrUShBaJ.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/UrQsqscbKEpNuJcvBZBu.png', |
|||
]; |
|||
|
|||
const getNotice = (_: Request, res: Response) => { |
|||
res.json({ |
|||
data: [ |
|||
{ |
|||
id: 'xxx1', |
|||
title: titles[0], |
|||
logo: avatars[0], |
|||
description: '那是一种内在的东西,他们到达不了,也无法触及的', |
|||
updatedAt: new Date(), |
|||
member: '科学搬砖组', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx2', |
|||
title: titles[1], |
|||
logo: avatars[1], |
|||
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的', |
|||
updatedAt: new Date('2017-07-24'), |
|||
member: '全组都是吴彦祖', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx3', |
|||
title: titles[2], |
|||
logo: avatars[2], |
|||
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆', |
|||
updatedAt: new Date(), |
|||
member: '中二少女团', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx4', |
|||
title: titles[3], |
|||
logo: avatars[3], |
|||
description: '那时候我只会想自己想要什么,从不想自己拥有什么', |
|||
updatedAt: new Date('2017-07-23'), |
|||
member: '程序员日常', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx5', |
|||
title: titles[4], |
|||
logo: avatars[4], |
|||
description: '凛冬将至', |
|||
updatedAt: new Date('2017-07-23'), |
|||
member: '高逼格设计天团', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
{ |
|||
id: 'xxx6', |
|||
title: titles[5], |
|||
logo: avatars[5], |
|||
description: '生命就像一盒巧克力,结果往往出人意料', |
|||
updatedAt: new Date('2017-07-23'), |
|||
member: '骗你来学计算机', |
|||
href: '', |
|||
memberLink: '', |
|||
}, |
|||
], |
|||
}); |
|||
}; |
|||
|
|||
const getActivities = (_: Request, res: Response) => { |
|||
res.json({ |
|||
data: [ |
|||
{ |
|||
id: 'trend-1', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '曲丽丽', |
|||
avatar: avatars2[0], |
|||
}, |
|||
group: { |
|||
name: '高逼格设计天团', |
|||
link: 'http://github.com/', |
|||
}, |
|||
project: { |
|||
name: '六月迭代', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '在 @{group} 新建项目 @{project}', |
|||
}, |
|||
{ |
|||
id: 'trend-2', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '付小小', |
|||
avatar: avatars2[1], |
|||
}, |
|||
group: { |
|||
name: '高逼格设计天团', |
|||
link: 'http://github.com/', |
|||
}, |
|||
project: { |
|||
name: '六月迭代', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '在 @{group} 新建项目 @{project}', |
|||
}, |
|||
{ |
|||
id: 'trend-3', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '林东东', |
|||
avatar: avatars2[2], |
|||
}, |
|||
group: { |
|||
name: '中二少女团', |
|||
link: 'http://github.com/', |
|||
}, |
|||
project: { |
|||
name: '六月迭代', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '在 @{group} 新建项目 @{project}', |
|||
}, |
|||
{ |
|||
id: 'trend-4', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '周星星', |
|||
avatar: avatars2[4], |
|||
}, |
|||
project: { |
|||
name: '5 月日常迭代', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '将 @{project} 更新至已发布状态', |
|||
}, |
|||
{ |
|||
id: 'trend-5', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '朱偏右', |
|||
avatar: avatars2[3], |
|||
}, |
|||
project: { |
|||
name: '工程效能', |
|||
link: 'http://github.com/', |
|||
}, |
|||
comment: { |
|||
name: '留言', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '在 @{project} 发布了 @{comment}', |
|||
}, |
|||
{ |
|||
id: 'trend-6', |
|||
updatedAt: new Date(), |
|||
user: { |
|||
name: '乐哥', |
|||
avatar: avatars2[5], |
|||
}, |
|||
group: { |
|||
name: '程序员日常', |
|||
link: 'http://github.com/', |
|||
}, |
|||
project: { |
|||
name: '品牌迭代', |
|||
link: 'http://github.com/', |
|||
}, |
|||
template: '在 @{group} 新建项目 @{project}', |
|||
}, |
|||
], |
|||
}); |
|||
}; |
|||
|
|||
const radarOriginData = [ |
|||
{ |
|||
name: '个人', |
|||
ref: 10, |
|||
koubei: 8, |
|||
output: 4, |
|||
contribute: 5, |
|||
hot: 7, |
|||
}, |
|||
{ |
|||
name: '团队', |
|||
ref: 3, |
|||
koubei: 9, |
|||
output: 6, |
|||
contribute: 3, |
|||
hot: 1, |
|||
}, |
|||
{ |
|||
name: '部门', |
|||
ref: 4, |
|||
koubei: 1, |
|||
output: 6, |
|||
contribute: 5, |
|||
hot: 7, |
|||
}, |
|||
]; |
|||
|
|||
const radarData: any[] = []; |
|||
const radarTitleMap = { |
|||
ref: '引用', |
|||
koubei: '口碑', |
|||
output: '产量', |
|||
contribute: '贡献', |
|||
hot: '热度', |
|||
}; |
|||
radarOriginData.forEach((item) => { |
|||
Object.keys(item).forEach((key) => { |
|||
if (key !== 'name') { |
|||
radarData.push({ |
|||
name: item.name, |
|||
label: radarTitleMap[key as 'ref'], |
|||
value: item[key as 'ref'], |
|||
}); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
const getChartData = (_: Request, res: Response) => { |
|||
res.json({ |
|||
data: { |
|||
visitData, |
|||
visitData2, |
|||
salesData, |
|||
searchData, |
|||
offlineData, |
|||
offlineChartData, |
|||
salesTypeData, |
|||
salesTypeDataOnline, |
|||
salesTypeDataOffline, |
|||
radarData, |
|||
}, |
|||
}); |
|||
}; |
|||
|
|||
export default { |
|||
'GET /api/project/notice': getNotice, |
|||
'GET /api/activities': getActivities, |
|||
'GET /api/fake_workplace_chart_data': getChartData, |
|||
}; |
|||
@ -1,16 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.linkGroup { |
|||
padding: 20px 0 8px 24px; |
|||
font-size: 0; |
|||
& > a { |
|||
display: inline-block; |
|||
width: 25%; |
|||
margin-bottom: 13px; |
|||
color: @text-color; |
|||
font-size: @font-size-base; |
|||
&:hover { |
|||
color: @primary-color; |
|||
} |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
linkGroup: { |
|||
fontSize: '0', |
|||
'& > a': { |
|||
display: 'inline-block', |
|||
width: '25%', |
|||
marginBottom: '13px', |
|||
color: token.colorText, |
|||
fontSize: token.fontSize, |
|||
'&:hover': { |
|||
color: token.colorPrimary, |
|||
}, |
|||
}, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,38 +0,0 @@ |
|||
import { PlusOutlined } from '@ant-design/icons'; |
|||
import { Button } from 'antd'; |
|||
import React, { createElement } from 'react'; |
|||
import useStyles from './index.style'; |
|||
export type EditableLink = { |
|||
title: string; |
|||
href: string; |
|||
id?: string; |
|||
}; |
|||
type EditableLinkGroupProps = { |
|||
onAdd: () => void; |
|||
links: EditableLink[]; |
|||
linkElement: any; |
|||
}; |
|||
const EditableLinkGroup: React.FC<EditableLinkGroupProps> = (props) => { |
|||
const { styles } = useStyles(); |
|||
const { links = [], linkElement = 'a', onAdd = () => {} } = props; |
|||
return ( |
|||
<div className={styles.linkGroup}> |
|||
{links.map((link) => |
|||
createElement( |
|||
linkElement, |
|||
{ |
|||
key: `linkGroup-item-${link.id || link.title}`, |
|||
to: link.href, |
|||
href: link.href, |
|||
}, |
|||
link.title, |
|||
), |
|||
)} |
|||
<Button size="small" type="primary" ghost onClick={onAdd}> |
|||
<PlusOutlined /> 添加 |
|||
</Button> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default EditableLinkGroup; |
|||
@ -1,111 +0,0 @@ |
|||
export interface DataItem { |
|||
[field: string]: string | number | number[] | null | undefined; |
|||
} |
|||
export interface TagType { |
|||
key: string; |
|||
label: string; |
|||
} |
|||
|
|||
export type SearchDataType = { |
|||
index: number; |
|||
keyword: string; |
|||
count: number; |
|||
range: number; |
|||
status: number; |
|||
}; |
|||
|
|||
export type OfflineDataType = { |
|||
name: string; |
|||
cvr: number; |
|||
}; |
|||
|
|||
export interface RadarData { |
|||
name: string; |
|||
label: string; |
|||
value: number; |
|||
} |
|||
|
|||
export type AnalysisData = { |
|||
visitData: VisitDataType[]; |
|||
visitData2: VisitDataType[]; |
|||
salesData: VisitDataType[]; |
|||
searchData: SearchDataType[]; |
|||
offlineData: OfflineDataType[]; |
|||
offlineChartData: OfflineChartData[]; |
|||
salesTypeData: VisitDataType[]; |
|||
salesTypeDataOnline: VisitDataType[]; |
|||
salesTypeDataOffline: VisitDataType[]; |
|||
radarData: DataItem[]; |
|||
}; |
|||
|
|||
export type GeographicType = { |
|||
province: { |
|||
label: string; |
|||
key: string; |
|||
}; |
|||
city: { |
|||
label: string; |
|||
key: string; |
|||
}; |
|||
}; |
|||
|
|||
export type NoticeType = { |
|||
id: string; |
|||
title: string; |
|||
logo: string; |
|||
description: string; |
|||
updatedAt: string; |
|||
member: string; |
|||
href: string; |
|||
memberLink: string; |
|||
}; |
|||
|
|||
export type CurrentUser = { |
|||
name: string; |
|||
avatar: string; |
|||
userid: string; |
|||
notice: NoticeType[]; |
|||
email: string; |
|||
signature: string; |
|||
title: string; |
|||
group: string; |
|||
tags: TagType[]; |
|||
notifyCount: number; |
|||
unreadCount: number; |
|||
country: string; |
|||
geographic: GeographicType; |
|||
address: string; |
|||
phone: string; |
|||
}; |
|||
|
|||
export type Member = { |
|||
avatar: string; |
|||
name: string; |
|||
id: string; |
|||
}; |
|||
|
|||
export type ActivitiesType = { |
|||
id: string; |
|||
updatedAt: string; |
|||
user: { |
|||
link?: string; |
|||
name: string; |
|||
avatar: string; |
|||
}; |
|||
group: { |
|||
name: string; |
|||
link: string; |
|||
}; |
|||
project: { |
|||
name: string; |
|||
link: string; |
|||
}; |
|||
|
|||
template: string; |
|||
}; |
|||
|
|||
export type RadarDataType = { |
|||
label: string; |
|||
name: string; |
|||
value: number; |
|||
}; |
|||
@ -1,286 +0,0 @@ |
|||
import { Radar } from '@ant-design/plots'; |
|||
import { PageContainer } from '@ant-design/pro-components'; |
|||
import { Link, useRequest } from '@umijs/max'; |
|||
import { Avatar, Card, Col, List, Row, Skeleton, Statistic } from 'antd'; |
|||
import dayjs from 'dayjs'; |
|||
import relativeTime from 'dayjs/plugin/relativeTime'; |
|||
import type { FC } from 'react'; |
|||
import EditableLinkGroup from './components/EditableLinkGroup'; |
|||
import type { ActivitiesType, CurrentUser } from './data.d'; |
|||
import { fakeChartData, queryActivities, queryProjectNotice } from './service'; |
|||
import useStyles from './style.style'; |
|||
|
|||
dayjs.extend(relativeTime); |
|||
|
|||
const links = [ |
|||
{ |
|||
title: '操作一', |
|||
href: '', |
|||
}, |
|||
{ |
|||
title: '操作二', |
|||
href: '', |
|||
}, |
|||
{ |
|||
title: '操作三', |
|||
href: '', |
|||
}, |
|||
{ |
|||
title: '操作四', |
|||
href: '', |
|||
}, |
|||
{ |
|||
title: '操作五', |
|||
href: '', |
|||
}, |
|||
{ |
|||
title: '操作六', |
|||
href: '', |
|||
}, |
|||
]; |
|||
const PageHeaderContent: FC<{ |
|||
currentUser: Partial<CurrentUser>; |
|||
}> = ({ currentUser }) => { |
|||
const { styles } = useStyles(); |
|||
const loading = currentUser && Object.keys(currentUser).length; |
|||
if (!loading) { |
|||
return ( |
|||
<Skeleton |
|||
avatar |
|||
paragraph={{ |
|||
rows: 1, |
|||
}} |
|||
active |
|||
/> |
|||
); |
|||
} |
|||
return ( |
|||
<div className={styles.pageHeaderContent}> |
|||
<div className={styles.avatar}> |
|||
<Avatar size="large" src={currentUser.avatar} /> |
|||
</div> |
|||
<div className={styles.content}> |
|||
<div className={styles.contentTitle}> |
|||
早安, |
|||
{currentUser.name} |
|||
,祝你开心每一天! |
|||
</div> |
|||
<div> |
|||
{currentUser.title} |{currentUser.group} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
const ExtraContent: FC<Record<string, any>> = () => { |
|||
const { styles } = useStyles(); |
|||
return ( |
|||
<div className={styles.extraContent}> |
|||
<div className={styles.statItem}> |
|||
<Statistic title="项目数" value={56} /> |
|||
</div> |
|||
<div className={styles.statItem}> |
|||
<Statistic title="团队内排名" value={8} suffix="/ 24" /> |
|||
</div> |
|||
<div className={styles.statItem}> |
|||
<Statistic title="项目访问" value={2223} /> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
const Workplace: FC = () => { |
|||
const { styles } = useStyles(); |
|||
const { loading: projectLoading, data: projectNotice = [] } = |
|||
useRequest(queryProjectNotice); |
|||
const { loading: activitiesLoading, data: activities = [] } = |
|||
useRequest(queryActivities); |
|||
const { data } = useRequest(fakeChartData); |
|||
const renderActivities = (item: ActivitiesType) => { |
|||
const events = item.template.split(/@\{([^{}]*)\}/gi).map((key) => { |
|||
if (item[key as keyof ActivitiesType]) { |
|||
const value = item[key as 'user']; |
|||
return ( |
|||
<a href={value?.link} key={value?.name}> |
|||
{value.name} |
|||
</a> |
|||
); |
|||
} |
|||
return key; |
|||
}); |
|||
return ( |
|||
<List.Item key={item.id}> |
|||
<List.Item.Meta |
|||
avatar={<Avatar src={item.user.avatar} />} |
|||
title={ |
|||
<span> |
|||
<a className={styles.username}>{item.user.name}</a> |
|||
|
|||
<span className={styles.event}>{events}</span> |
|||
</span> |
|||
} |
|||
description={ |
|||
<span className={styles.datetime} title={item.updatedAt}> |
|||
{dayjs(item.updatedAt).fromNow()} |
|||
</span> |
|||
} |
|||
/> |
|||
</List.Item> |
|||
); |
|||
}; |
|||
|
|||
return ( |
|||
<PageContainer |
|||
content={ |
|||
<PageHeaderContent |
|||
currentUser={{ |
|||
avatar: |
|||
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', |
|||
name: '吴彦祖', |
|||
userid: '00000001', |
|||
email: 'antdesign@alipay.com', |
|||
signature: '海纳百川,有容乃大', |
|||
title: '交互专家', |
|||
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED', |
|||
}} |
|||
/> |
|||
} |
|||
extraContent={<ExtraContent />} |
|||
> |
|||
<Row gutter={24}> |
|||
<Col xl={16} lg={24} md={24} sm={24} xs={24}> |
|||
<Card |
|||
className={styles.projectList} |
|||
style={{ |
|||
marginBottom: 24, |
|||
}} |
|||
title="进行中的项目" |
|||
variant="borderless" |
|||
extra={<Link to="/">全部项目</Link>} |
|||
loading={projectLoading} |
|||
> |
|||
{projectNotice.map((item) => ( |
|||
<Card.Grid className={styles.projectGrid} key={item.id}> |
|||
<Card.Meta |
|||
title={ |
|||
<div className={styles.cardTitle}> |
|||
<Avatar size="small" src={item.logo} /> |
|||
<Link to={item.href || '/'}>{item.title}</Link> |
|||
</div> |
|||
} |
|||
description={item.description} |
|||
style={{ |
|||
width: '100%', |
|||
}} |
|||
/> |
|||
<div className={styles.projectItemContent}> |
|||
<Link to={item.memberLink || '/'}>{item.member || ''}</Link> |
|||
{item.updatedAt && ( |
|||
<span className={styles.datetime} title={item.updatedAt}> |
|||
{dayjs(item.updatedAt).fromNow()} |
|||
</span> |
|||
)} |
|||
</div> |
|||
</Card.Grid> |
|||
))} |
|||
</Card> |
|||
<Card |
|||
styles={{ |
|||
body: { |
|||
padding: activitiesLoading ? 16 : 0, |
|||
}, |
|||
}} |
|||
variant="borderless" |
|||
className={styles.activeCard} |
|||
title="动态" |
|||
loading={activitiesLoading} |
|||
> |
|||
<List<ActivitiesType> |
|||
loading={activitiesLoading} |
|||
renderItem={(item) => renderActivities(item)} |
|||
dataSource={activities} |
|||
className={styles.activitiesList} |
|||
size="large" |
|||
/> |
|||
</Card> |
|||
</Col> |
|||
<Col xl={8} lg={24} md={24} sm={24} xs={24}> |
|||
<Card |
|||
style={{ |
|||
marginBottom: 24, |
|||
}} |
|||
title="快速开始 / 便捷导航" |
|||
variant="borderless" |
|||
> |
|||
<EditableLinkGroup |
|||
onAdd={() => {}} |
|||
links={links} |
|||
linkElement={Link} |
|||
/> |
|||
</Card> |
|||
<Card |
|||
style={{ |
|||
marginBottom: 24, |
|||
}} |
|||
variant="borderless" |
|||
title="XX 指数" |
|||
loading={data?.radarData?.length === 0} |
|||
> |
|||
<Radar |
|||
height={343} |
|||
data={data?.radarData || []} |
|||
xField="label" |
|||
colorField="name" |
|||
yField="value" |
|||
shapeField="smooth" |
|||
area={{ |
|||
style: { |
|||
fillOpacity: 0.4, |
|||
}, |
|||
}} |
|||
axis={{ |
|||
y: { |
|||
gridStrokeOpacity: 0.5, |
|||
}, |
|||
}} |
|||
legend={{ |
|||
color: { |
|||
position: 'bottom', |
|||
layout: { justifyContent: 'center' }, |
|||
}, |
|||
}} |
|||
/> |
|||
</Card> |
|||
<Card |
|||
styles={{ |
|||
body: { |
|||
paddingTop: 12, |
|||
paddingBottom: 12, |
|||
}, |
|||
}} |
|||
variant="borderless" |
|||
title="团队" |
|||
loading={projectLoading} |
|||
> |
|||
<div className={styles.members}> |
|||
<Row gutter={48}> |
|||
{projectNotice.map((item) => { |
|||
return ( |
|||
<Col span={12} key={`members-item-${item.id}`}> |
|||
<a> |
|||
<Avatar src={item.logo} size="small" /> |
|||
<span className={styles.member}> |
|||
{item.member.substring(0, 3)} |
|||
</span> |
|||
</a> |
|||
</Col> |
|||
); |
|||
})} |
|||
</Row> |
|||
</div> |
|||
</Card> |
|||
</Col> |
|||
</Row> |
|||
</PageContainer> |
|||
); |
|||
}; |
|||
export default Workplace; |
|||
@ -1,14 +0,0 @@ |
|||
import { request } from '@umijs/max'; |
|||
import type { ActivitiesType, AnalysisData, NoticeType } from './data'; |
|||
|
|||
export async function queryProjectNotice(): Promise<{ data: NoticeType[] }> { |
|||
return request('/api/project/notice'); |
|||
} |
|||
|
|||
export async function queryActivities(): Promise<{ data: ActivitiesType[] }> { |
|||
return request('/api/activities'); |
|||
} |
|||
|
|||
export async function fakeChartData(): Promise<{ data: AnalysisData }> { |
|||
return request('/api/fake_workplace_chart_data'); |
|||
} |
|||
@ -1,251 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.textOverflow() { |
|||
overflow: hidden; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
word-break: break-all; |
|||
} |
|||
|
|||
// mixins for clearfix |
|||
// ------------------------ |
|||
.clearfix() { |
|||
zoom: 1; |
|||
&::before, |
|||
&::after { |
|||
display: table; |
|||
content: ' '; |
|||
} |
|||
&::after { |
|||
clear: both; |
|||
height: 0; |
|||
font-size: 0; |
|||
visibility: hidden; |
|||
} |
|||
} |
|||
|
|||
.activitiesList { |
|||
padding: 0 24px 8px 24px; |
|||
|
|||
.username { |
|||
color: @text-color; |
|||
} |
|||
.event { |
|||
font-weight: normal; |
|||
} |
|||
} |
|||
|
|||
.pageHeaderContent { |
|||
display: flex; |
|||
.avatar { |
|||
flex: 0 1 72px; |
|||
& > span { |
|||
display: block; |
|||
width: 72px; |
|||
height: 72px; |
|||
border-radius: 72px; |
|||
} |
|||
} |
|||
.content { |
|||
position: relative; |
|||
top: 4px; |
|||
flex: 1 1 auto; |
|||
margin-left: 24px; |
|||
color: @text-color-secondary; |
|||
line-height: 22px; |
|||
.contentTitle { |
|||
margin-bottom: 12px; |
|||
color: @heading-color; |
|||
font-weight: 500; |
|||
font-size: 20px; |
|||
line-height: 28px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.extraContent { |
|||
.clearfix(); |
|||
|
|||
float: right; |
|||
white-space: nowrap; |
|||
.statItem { |
|||
position: relative; |
|||
display: inline-block; |
|||
padding: 0 32px; |
|||
> p:first-child { |
|||
margin-bottom: 4px; |
|||
color: @text-color-secondary; |
|||
font-size: @font-size-base; |
|||
line-height: 22px; |
|||
} |
|||
> p { |
|||
margin: 0; |
|||
color: @heading-color; |
|||
font-size: 30px; |
|||
line-height: 38px; |
|||
> span { |
|||
color: @text-color-secondary; |
|||
font-size: 20px; |
|||
} |
|||
} |
|||
&::after { |
|||
position: absolute; |
|||
top: 8px; |
|||
right: 0; |
|||
width: 1px; |
|||
height: 40px; |
|||
background-color: @border-color-split; |
|||
content: ''; |
|||
} |
|||
&:last-child { |
|||
padding-right: 0; |
|||
&::after { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.members { |
|||
a { |
|||
display: block; |
|||
height: 24px; |
|||
margin: 12px 0; |
|||
color: @text-color; |
|||
transition: all 0.3s; |
|||
.textOverflow(); |
|||
.member { |
|||
margin-left: 12px; |
|||
font-size: @font-size-base; |
|||
line-height: 24px; |
|||
vertical-align: top; |
|||
} |
|||
&:hover { |
|||
color: @primary-color; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.projectList { |
|||
:global { |
|||
.ant-card-meta-description { |
|||
height: 44px; |
|||
overflow: hidden; |
|||
color: @text-color-secondary; |
|||
line-height: 22px; |
|||
} |
|||
} |
|||
.cardTitle { |
|||
font-size: 0; |
|||
a { |
|||
display: inline-block; |
|||
height: 24px; |
|||
margin-left: 12px; |
|||
color: @heading-color; |
|||
font-size: @font-size-base; |
|||
line-height: 24px; |
|||
vertical-align: top; |
|||
&:hover { |
|||
color: @primary-color; |
|||
} |
|||
} |
|||
} |
|||
.projectGrid { |
|||
width: 33.33%; |
|||
} |
|||
.projectItemContent { |
|||
display: flex; |
|||
height: 20px; |
|||
margin-top: 8px; |
|||
overflow: hidden; |
|||
font-size: 12px; |
|||
line-height: 20px; |
|||
.textOverflow(); |
|||
a { |
|||
display: inline-block; |
|||
flex: 1 1 0; |
|||
color: @text-color-secondary; |
|||
.textOverflow(); |
|||
&:hover { |
|||
color: @primary-color; |
|||
} |
|||
} |
|||
.datetime { |
|||
flex: 0 0 auto; |
|||
float: right; |
|||
color: @disabled-color; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.datetime { |
|||
color: @disabled-color; |
|||
} |
|||
|
|||
@media screen and (max-width: @screen-xl) and (min-width: @screen-lg) { |
|||
.activeCard { |
|||
margin-bottom: 24px; |
|||
} |
|||
.members { |
|||
margin-bottom: 0; |
|||
} |
|||
.extraContent { |
|||
margin-left: -44px; |
|||
.statItem { |
|||
padding: 0 16px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media screen and (max-width: @screen-lg) { |
|||
.activeCard { |
|||
margin-bottom: 24px; |
|||
} |
|||
.members { |
|||
margin-bottom: 0; |
|||
} |
|||
.extraContent { |
|||
float: none; |
|||
margin-right: 0; |
|||
.statItem { |
|||
padding: 0 16px; |
|||
text-align: left; |
|||
&::after { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media screen and (max-width: @screen-md) { |
|||
.extraContent { |
|||
margin-left: -16px; |
|||
} |
|||
.projectList { |
|||
.projectGrid { |
|||
width: 50%; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media screen and (max-width: @screen-sm) { |
|||
.pageHeaderContent { |
|||
display: block; |
|||
.content { |
|||
margin-left: 0; |
|||
} |
|||
} |
|||
.extraContent { |
|||
.statItem { |
|||
float: none; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media screen and (max-width: @screen-xs) { |
|||
.projectList { |
|||
.projectGrid { |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
|||
@ -1,215 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
activitiesList: { |
|||
padding: 0, |
|||
}, |
|||
username: { |
|||
color: token.colorText, |
|||
}, |
|||
event: { |
|||
fontWeight: 'normal', |
|||
}, |
|||
pageHeaderContent: { |
|||
display: 'flex', |
|||
[`@media screen and (max-width: ${token.screenSM}px)`]: { |
|||
display: 'block', |
|||
}, |
|||
}, |
|||
avatar: { |
|||
flex: '0 1 72px', |
|||
'& > span': { |
|||
display: 'block', |
|||
width: '72px', |
|||
height: '72px', |
|||
borderRadius: '72px', |
|||
}, |
|||
}, |
|||
content: { |
|||
position: 'relative', |
|||
top: '4px', |
|||
flex: '1 1 auto', |
|||
marginLeft: '24px', |
|||
color: token.colorTextSecondary, |
|||
lineHeight: '22px', |
|||
[`@media screen and (max-width: ${token.screenSM}px)`]: { |
|||
marginLeft: '0', |
|||
}, |
|||
}, |
|||
contentTitle: { |
|||
marginBottom: '12px', |
|||
color: token.colorTextHeading, |
|||
fontWeight: '500', |
|||
fontSize: '20px', |
|||
lineHeight: '28px', |
|||
}, |
|||
extraContent: { |
|||
zoom: '1', |
|||
'&::before, &::after': { display: 'table', content: "' '" }, |
|||
'&::after': { |
|||
clear: 'both', |
|||
height: '0', |
|||
fontSize: '0', |
|||
visibility: 'hidden', |
|||
}, |
|||
float: 'right', |
|||
whiteSpace: 'nowrap', |
|||
[`@media screen and (max-width: ${token.screenXL}px) and (min-width: @screen-lg)`]: |
|||
{ |
|||
marginLeft: '-44px', |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenLG}px)`]: { |
|||
float: 'none', |
|||
marginRight: '0', |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenMD}px)`]: { |
|||
marginLeft: '-16px', |
|||
}, |
|||
}, |
|||
statItem: { |
|||
position: 'relative', |
|||
display: 'inline-block', |
|||
padding: '0 32px', |
|||
'> p:first-child': { |
|||
marginBottom: '4px', |
|||
color: token.colorTextSecondary, |
|||
fontSize: token.fontSize, |
|||
lineHeight: '22px', |
|||
}, |
|||
'> p': { |
|||
margin: '0', |
|||
color: token.colorTextHeading, |
|||
fontSize: '30px', |
|||
lineHeight: '38px', |
|||
'> span': { |
|||
color: token.colorTextSecondary, |
|||
fontSize: '20px', |
|||
}, |
|||
}, |
|||
'&::after': { |
|||
position: 'absolute', |
|||
top: '8px', |
|||
right: '0', |
|||
width: '1px', |
|||
height: '40px', |
|||
backgroundColor: token.colorSplit, |
|||
content: "''", |
|||
}, |
|||
'&:last-child': { |
|||
paddingRight: '0', |
|||
'&::after': { |
|||
display: 'none', |
|||
}, |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenXL}px) and (min-width: @screen-lg)`]: |
|||
{ |
|||
padding: '0 16px', |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenLG}px)`]: { |
|||
padding: '0 16px', |
|||
textAlign: 'left', |
|||
'&::after': { |
|||
display: 'none', |
|||
}, |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenSM}px)`]: { float: 'none' }, |
|||
}, |
|||
members: { |
|||
a: { |
|||
display: 'block', |
|||
height: '24px', |
|||
margin: '12px 0', |
|||
color: token.colorText, |
|||
transition: 'all 0.3s', |
|||
overflow: 'hidden', |
|||
whiteSpace: 'nowrap', |
|||
textOverflow: 'ellipsis', |
|||
wordBreak: 'break-all', |
|||
'&:hover': { |
|||
color: token.colorPrimary, |
|||
}, |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenXL}px) and (min-width: @screen-lg)`]: |
|||
{ |
|||
marginBottom: '0', |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenLG}px)`]: { |
|||
marginBottom: '0', |
|||
}, |
|||
}, |
|||
member: { |
|||
marginLeft: '12px', |
|||
fontSize: token.fontSize, |
|||
lineHeight: '24px', |
|||
verticalAlign: 'top', |
|||
}, |
|||
projectList: { |
|||
'.ant-card-meta-description': { |
|||
height: '44px', |
|||
overflow: 'hidden', |
|||
color: token.colorTextSecondary, |
|||
lineHeight: '22px', |
|||
}, |
|||
}, |
|||
cardTitle: { |
|||
fontSize: '0', |
|||
a: { |
|||
display: 'inline-block', |
|||
height: '24px', |
|||
marginLeft: '12px', |
|||
color: token.colorTextHeading, |
|||
fontSize: token.fontSize, |
|||
lineHeight: '24px', |
|||
verticalAlign: 'top', |
|||
'&:hover': { |
|||
color: token.colorPrimary, |
|||
}, |
|||
}, |
|||
}, |
|||
projectGrid: { |
|||
width: '33.33%', |
|||
[`@media screen and (max-width: ${token.screenMD}px)`]: { width: '50%' }, |
|||
[`@media screen and (max-width: ${token.screenXS}px)`]: { width: '100%' }, |
|||
}, |
|||
projectItemContent: { |
|||
display: 'flex', |
|||
height: '20px', |
|||
marginTop: '8px', |
|||
overflow: 'hidden', |
|||
fontSize: '12px', |
|||
gap: 'epx', |
|||
lineHeight: '20px', |
|||
whiteSpace: 'nowrap', |
|||
textOverflow: 'ellipsis', |
|||
wordBreak: 'break-all', |
|||
a: { |
|||
display: 'inline-block', |
|||
flex: '1 1 0', |
|||
color: token.colorTextSecondary, |
|||
overflow: 'hidden', |
|||
whiteSpace: 'nowrap', |
|||
textOverflow: 'ellipsis', |
|||
wordBreak: 'break-all', |
|||
'&:hover': { |
|||
color: token.colorPrimary, |
|||
}, |
|||
}, |
|||
}, |
|||
datetime: { |
|||
flex: '0 0 auto', |
|||
color: token.colorTextDisabled, |
|||
}, |
|||
activeCard: { |
|||
[`@media screen and (max-width: ${token.screenXL}px) and (min-width: @screen-lg)`]: |
|||
{ |
|||
marginBottom: '24px', |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenLG}px)`]: { |
|||
marginBottom: '24px', |
|||
}, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,17 +0,0 @@ |
|||
import { Link } from '@umijs/max'; |
|||
import { Button, Card, Result } from 'antd'; |
|||
|
|||
export default () => ( |
|||
<Card variant="borderless"> |
|||
<Result |
|||
status="403" |
|||
title="403" |
|||
subTitle="Sorry, you are not authorized to access this page." |
|||
extra={ |
|||
<Link to="/"> |
|||
<Button type="primary">Back to home</Button> |
|||
</Link> |
|||
} |
|||
/> |
|||
</Card> |
|||
); |
|||
@ -1,17 +0,0 @@ |
|||
import { Link } from '@umijs/max'; |
|||
import { Button, Card, Result } from 'antd'; |
|||
|
|||
export default () => ( |
|||
<Card variant="borderless"> |
|||
<Result |
|||
status="404" |
|||
title="404" |
|||
subTitle="Sorry, the page you visited does not exist." |
|||
extra={ |
|||
<Link to="/"> |
|||
<Button type="primary">Back Home</Button> |
|||
</Link> |
|||
} |
|||
/> |
|||
</Card> |
|||
); |
|||
@ -1,17 +0,0 @@ |
|||
import { Link } from '@umijs/max'; |
|||
import { Button, Card, Result } from 'antd'; |
|||
|
|||
export default () => ( |
|||
<Card variant="borderless"> |
|||
<Result |
|||
status="500" |
|||
title="500" |
|||
subTitle="Sorry, something went wrong." |
|||
extra={ |
|||
<Link to="/"> |
|||
<Button type="primary">Back Home</Button> |
|||
</Link> |
|||
} |
|||
/> |
|||
</Card> |
|||
); |
|||
@ -1,7 +0,0 @@ |
|||
import type { Request, Response } from 'express'; |
|||
|
|||
export default { |
|||
'POST /api/advancedForm': (_: Request, res: Response) => { |
|||
res.send({ data: { message: 'Ok' } }); |
|||
}, |
|||
}; |
|||
@ -1,268 +0,0 @@ |
|||
import { PlusOutlined } from '@ant-design/icons'; |
|||
import { Button, Divider, Input, message, Popconfirm, Table } from 'antd'; |
|||
import type { FC } from 'react'; |
|||
import React, { useState } from 'react'; |
|||
import useStyles from '../style.style'; |
|||
|
|||
type TableFormDateType = { |
|||
key: string; |
|||
workId?: string; |
|||
name?: string; |
|||
department?: string; |
|||
isNew?: boolean; |
|||
editable?: boolean; |
|||
}; |
|||
type TableFormProps = { |
|||
value?: TableFormDateType[]; |
|||
onChange?: (value: TableFormDateType[]) => void; |
|||
}; |
|||
const TableForm: FC<TableFormProps> = ({ value, onChange }) => { |
|||
const { styles } = useStyles(); |
|||
const [clickedCancel, setClickedCancel] = useState(false); |
|||
const [loading, setLoading] = useState(false); |
|||
const [index, setIndex] = useState(0); |
|||
const [cacheOriginData, setCacheOriginData] = useState<Record<string, any>>( |
|||
{}, |
|||
); |
|||
const [data, setData] = useState(value); |
|||
const getRowByKey = (key: string, newData?: TableFormDateType[]) => |
|||
(newData || data)?.filter((item) => item.key === key)[0]; |
|||
const toggleEditable = ( |
|||
e: React.MouseEvent | React.KeyboardEvent, |
|||
key: string, |
|||
) => { |
|||
e.preventDefault(); |
|||
const newData = data?.map((item) => ({ |
|||
...item, |
|||
})); |
|||
const target = getRowByKey(key, newData); |
|||
if (target) { |
|||
// 进入编辑状态时保存原始数据
|
|||
if (!target.editable) { |
|||
cacheOriginData[key] = { |
|||
...target, |
|||
}; |
|||
setCacheOriginData(cacheOriginData); |
|||
} |
|||
target.editable = !target.editable; |
|||
setData(newData); |
|||
} |
|||
}; |
|||
const newMember = () => { |
|||
const newData = |
|||
data?.map((item) => ({ |
|||
...item, |
|||
})) || []; |
|||
newData.push({ |
|||
key: `NEW_TEMP_ID_${index}`, |
|||
workId: '', |
|||
name: '', |
|||
department: '', |
|||
editable: true, |
|||
isNew: true, |
|||
}); |
|||
setIndex(index + 1); |
|||
setData(newData); |
|||
}; |
|||
const remove = (key: string) => { |
|||
const newData = data?.filter( |
|||
(item) => item.key !== key, |
|||
) as TableFormDateType[]; |
|||
setData(newData); |
|||
if (onChange) { |
|||
onChange(newData); |
|||
} |
|||
}; |
|||
const handleFieldChange = ( |
|||
e: React.ChangeEvent<HTMLInputElement>, |
|||
fieldName: keyof TableFormDateType, |
|||
key: string, |
|||
) => { |
|||
const newData = [...(data as TableFormDateType[])]; |
|||
const target = getRowByKey(key, newData); |
|||
if (target?.[fieldName]) { |
|||
target[fieldName as 'key'] = e.target.value; |
|||
setData(newData); |
|||
} |
|||
}; |
|||
const saveRow = (e: React.MouseEvent | React.KeyboardEvent, key: string) => { |
|||
e.persist(); |
|||
setLoading(true); |
|||
setTimeout(() => { |
|||
if (clickedCancel) { |
|||
setClickedCancel(false); |
|||
return; |
|||
} |
|||
const target = getRowByKey(key) || ({} as any); |
|||
if (!target.workId || !target.name || !target.department) { |
|||
message.error('请填写完整成员信息。'); |
|||
(e.target as HTMLInputElement).focus(); |
|||
setLoading(false); |
|||
return; |
|||
} |
|||
delete target.isNew; |
|||
toggleEditable(e, key); |
|||
if (onChange) { |
|||
onChange(data as TableFormDateType[]); |
|||
} |
|||
setLoading(false); |
|||
}, 500); |
|||
}; |
|||
const handleKeyPress = (e: React.KeyboardEvent, key: string) => { |
|||
if (e.key === 'Enter') { |
|||
saveRow(e, key); |
|||
} |
|||
}; |
|||
const cancel = (e: React.MouseEvent, key: string) => { |
|||
setClickedCancel(true); |
|||
e.preventDefault(); |
|||
const newData = [...(data as TableFormDateType[])]; |
|||
// 编辑前的原始数据
|
|||
let cacheData = []; |
|||
cacheData = newData.map((item) => { |
|||
if (item.key === key) { |
|||
if (cacheOriginData[key]) { |
|||
const originItem = { |
|||
...item, |
|||
...cacheOriginData[key], |
|||
editable: false, |
|||
}; |
|||
delete cacheOriginData[key]; |
|||
setCacheOriginData(cacheOriginData); |
|||
return originItem; |
|||
} |
|||
} |
|||
return item; |
|||
}); |
|||
setData(cacheData); |
|||
setClickedCancel(false); |
|||
}; |
|||
const columns = [ |
|||
{ |
|||
title: '成员姓名', |
|||
dataIndex: 'name', |
|||
key: 'name', |
|||
width: '20%', |
|||
render: (text: string, record: TableFormDateType) => { |
|||
if (record.editable) { |
|||
return ( |
|||
<Input |
|||
value={text} |
|||
autoFocus |
|||
onChange={(e) => handleFieldChange(e, 'name', record.key)} |
|||
onKeyPress={(e) => handleKeyPress(e, record.key)} |
|||
placeholder="成员姓名" |
|||
/> |
|||
); |
|||
} |
|||
return text; |
|||
}, |
|||
}, |
|||
{ |
|||
title: '工号', |
|||
dataIndex: 'workId', |
|||
key: 'workId', |
|||
width: '20%', |
|||
render: (text: string, record: TableFormDateType) => { |
|||
if (record.editable) { |
|||
return ( |
|||
<Input |
|||
value={text} |
|||
onChange={(e) => handleFieldChange(e, 'workId', record.key)} |
|||
onKeyPress={(e) => handleKeyPress(e, record.key)} |
|||
placeholder="工号" |
|||
/> |
|||
); |
|||
} |
|||
return text; |
|||
}, |
|||
}, |
|||
{ |
|||
title: '所属部门', |
|||
dataIndex: 'department', |
|||
key: 'department', |
|||
width: '40%', |
|||
render: (text: string, record: TableFormDateType) => { |
|||
if (record.editable) { |
|||
return ( |
|||
<Input |
|||
value={text} |
|||
onChange={(e) => handleFieldChange(e, 'department', record.key)} |
|||
onKeyPress={(e) => handleKeyPress(e, record.key)} |
|||
placeholder="所属部门" |
|||
/> |
|||
); |
|||
} |
|||
return text; |
|||
}, |
|||
}, |
|||
{ |
|||
title: '操作', |
|||
key: 'action', |
|||
render: (_text: string, record: TableFormDateType) => { |
|||
if (!!record.editable && loading) { |
|||
return null; |
|||
} |
|||
if (record.editable) { |
|||
if (record.isNew) { |
|||
return ( |
|||
<span> |
|||
<a onClick={(e) => saveRow(e, record.key)}>添加</a> |
|||
<Divider type="vertical" /> |
|||
<Popconfirm |
|||
title="是否要删除此行?" |
|||
onConfirm={() => remove(record.key)} |
|||
> |
|||
<a>删除</a> |
|||
</Popconfirm> |
|||
</span> |
|||
); |
|||
} |
|||
return ( |
|||
<span> |
|||
<a onClick={(e) => saveRow(e, record.key)}>保存</a> |
|||
<Divider type="vertical" /> |
|||
<a onClick={(e) => cancel(e, record.key)}>取消</a> |
|||
</span> |
|||
); |
|||
} |
|||
return ( |
|||
<span> |
|||
<a onClick={(e) => toggleEditable(e, record.key)}>编辑</a> |
|||
<Divider type="vertical" /> |
|||
<Popconfirm |
|||
title="是否要删除此行?" |
|||
onConfirm={() => remove(record.key)} |
|||
> |
|||
<a>删除</a> |
|||
</Popconfirm> |
|||
</span> |
|||
); |
|||
}, |
|||
}, |
|||
]; |
|||
return ( |
|||
<> |
|||
<Table<TableFormDateType> |
|||
loading={loading} |
|||
columns={columns} |
|||
dataSource={data} |
|||
pagination={false} |
|||
rowClassName={(record) => (record.editable ? styles.editable : '')} |
|||
/> |
|||
<Button |
|||
style={{ |
|||
width: '100%', |
|||
marginTop: 16, |
|||
marginBottom: 8, |
|||
}} |
|||
type="dashed" |
|||
onClick={newMember} |
|||
> |
|||
<PlusOutlined /> |
|||
新增成员 |
|||
</Button> |
|||
</> |
|||
); |
|||
}; |
|||
export default TableForm; |
|||
@ -1,8 +0,0 @@ |
|||
import { request } from '@umijs/max'; |
|||
|
|||
export async function fakeSubmitForm(params: any) { |
|||
return request('/api/advancedForm', { |
|||
method: 'POST', |
|||
data: params, |
|||
}); |
|||
} |
|||
@ -1,65 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.card { |
|||
margin-bottom: 24px; |
|||
|
|||
:global { |
|||
.ant-legacy-form-item .ant-legacy-form-item-control-wrapper { |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.errorIcon { |
|||
margin-right: 24px; |
|||
color: @error-color; |
|||
cursor: pointer; |
|||
|
|||
span.anticon { |
|||
margin-right: 4px; |
|||
} |
|||
} |
|||
|
|||
.errorPopover { |
|||
:global { |
|||
.ant-popover-inner-content { |
|||
min-width: 256px; |
|||
max-height: 290px; |
|||
padding: 0; |
|||
overflow: auto; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.errorListItem { |
|||
padding: 8px 16px; |
|||
list-style: none; |
|||
border-bottom: 1px solid @border-color-split; |
|||
cursor: pointer; |
|||
transition: all 0.3s; |
|||
&:hover { |
|||
background: @item-active-bg; |
|||
} |
|||
&:last-child { |
|||
border: 0; |
|||
} |
|||
.errorIcon { |
|||
float: left; |
|||
margin-top: 4px; |
|||
margin-right: 12px; |
|||
padding-bottom: 22px; |
|||
color: @error-color; |
|||
} |
|||
.errorField { |
|||
margin-top: 2px; |
|||
color: @text-color-secondary; |
|||
font-size: 12px; |
|||
} |
|||
} |
|||
|
|||
.editable { |
|||
td { |
|||
padding-top: 13px !important; |
|||
padding-bottom: 12.5px !important; |
|||
} |
|||
} |
|||
@ -1,48 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
card: { |
|||
marginBottom: '24px', |
|||
'.ant-legacy-form-item .ant-legacy-form-item-control-wrapper': { |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
errorIcon: { |
|||
marginRight: '12px', |
|||
color: token.colorError, |
|||
cursor: 'pointer', |
|||
'span.anticon': { marginRight: '4px' }, |
|||
float: 'left', |
|||
marginTop: '4px', |
|||
paddingBottom: '22px', |
|||
}, |
|||
errorPopover: { |
|||
'.ant-popover-inner-content': { |
|||
minWidth: '256px', |
|||
maxHeight: '290px', |
|||
padding: '0', |
|||
overflow: 'auto', |
|||
}, |
|||
}, |
|||
errorListItem: { |
|||
padding: '8px 16px', |
|||
listStyle: 'none', |
|||
borderBottom: `1px solid ${token.colorSplit}`, |
|||
cursor: 'pointer', |
|||
transition: 'all 0.3s', |
|||
'&:hover': { background: token.colorBgTextActive }, |
|||
'&:last-child': { border: '0' }, |
|||
}, |
|||
errorField: { |
|||
marginTop: '2px', |
|||
color: token.colorTextSecondary, |
|||
fontSize: '12px', |
|||
}, |
|||
editable: { |
|||
td: { paddingTop: '13px', paddingBottom: '12.5px' }, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,7 +0,0 @@ |
|||
import type { Request, Response } from 'express'; |
|||
|
|||
export default { |
|||
'POST /api/basicForm': (_: Request, res: Response) => { |
|||
res.send({ data: { message: 'Ok' } }); |
|||
}, |
|||
}; |
|||
@ -1,8 +0,0 @@ |
|||
import { request } from '@umijs/max'; |
|||
|
|||
export async function fakeSubmitForm(params: any) { |
|||
return request('/api/basicForm', { |
|||
method: 'POST', |
|||
data: params, |
|||
}); |
|||
} |
|||
@ -1,6 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.optional { |
|||
color: @text-color-secondary; |
|||
font-style: normal; |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
optional: { |
|||
color: token.colorTextSecondary, |
|||
fontStyle: 'normal', |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,7 +0,0 @@ |
|||
import type { Request, Response } from 'express'; |
|||
|
|||
export default { |
|||
'POST /api/stepForm': (_: Request, res: Response) => { |
|||
res.send({ data: { message: 'Ok' } }); |
|||
}, |
|||
}; |
|||
@ -1,9 +0,0 @@ |
|||
export interface StepDataType { |
|||
payAccount: string; |
|||
receiverAccount: string; |
|||
receiverName: string; |
|||
amount: string; |
|||
receiverMode: string; |
|||
} |
|||
|
|||
export type CurrentTypes = 'base' | 'confirm' | 'result'; |
|||
@ -1,8 +0,0 @@ |
|||
import { request } from '@umijs/max'; |
|||
|
|||
export async function fakeSubmitForm(params: any) { |
|||
return request('/api/stepForm', { |
|||
method: 'POST', |
|||
data: params, |
|||
}); |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.card { |
|||
margin-bottom: 24px; |
|||
} |
|||
|
|||
.result { |
|||
max-width: 560px; |
|||
margin: 0 auto; |
|||
padding: 24px 0 8px; |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(() => { |
|||
return { |
|||
card: { |
|||
marginBottom: '24px', |
|||
}, |
|||
result: { |
|||
maxWidth: '560px', |
|||
margin: '0 auto', |
|||
padding: '24px 0 8px', |
|||
}, |
|||
}; |
|||
}); |
|||
export default useStyles; |
|||
@ -1,165 +0,0 @@ |
|||
import type { Request, Response } from 'express'; |
|||
import type { BasicListItemDataType } from './data.d'; |
|||
|
|||
const titles = [ |
|||
'Alipay', |
|||
'Angular', |
|||
'Ant Design', |
|||
'Ant Design Pro', |
|||
'Bootstrap', |
|||
'React', |
|||
'Vue', |
|||
'Webpack', |
|||
]; |
|||
const avatars = [ |
|||
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
|
|||
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
|
|||
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
|
|||
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
|
|||
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
|
|||
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
|
|||
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
|
|||
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
|
|||
]; |
|||
|
|||
const covers = [ |
|||
'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png', |
|||
'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png', |
|||
]; |
|||
const desc = [ |
|||
'那是一种内在的东西, 他们到达不了,也无法触及的', |
|||
'希望是一个好东西,也许是最好的,好东西是不会消亡的', |
|||
'生命就像一盒巧克力,结果往往出人意料', |
|||
'城镇中有那么多的酒馆,她却偏偏走进了我的酒馆', |
|||
'那时候我只会想自己想要什么,从不想自己拥有什么', |
|||
]; |
|||
|
|||
const user = [ |
|||
'付小小', |
|||
'曲丽丽', |
|||
'林东东', |
|||
'周星星', |
|||
'吴加好', |
|||
'朱偏右', |
|||
'鱼酱', |
|||
'乐哥', |
|||
'谭小仪', |
|||
'仲尼', |
|||
]; |
|||
|
|||
function fakeList(count: number): BasicListItemDataType[] { |
|||
const list = []; |
|||
for (let i = 0; i < count; i += 1) { |
|||
list.push({ |
|||
id: `fake-list-${i}`, |
|||
owner: user[i % 10], |
|||
title: 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(Date.now() - 1000 * 60 * 60 * 2 * i).getTime(), |
|||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2 * i).getTime(), |
|||
subDescription: desc[i % 5], |
|||
description: |
|||
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。', |
|||
activeUser: Math.ceil(Math.random() * 100000) + 100000, |
|||
newUser: Math.ceil(Math.random() * 1000) + 1000, |
|||
star: Math.ceil(Math.random() * 100) + 100, |
|||
like: Math.ceil(Math.random() * 100) + 100, |
|||
message: Math.ceil(Math.random() * 10) + 10, |
|||
content: |
|||
'段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。', |
|||
members: [ |
|||
{ |
|||
avatar: |
|||
'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png', |
|||
name: '曲丽丽', |
|||
id: 'member1', |
|||
}, |
|||
{ |
|||
avatar: |
|||
'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png', |
|||
name: '王昭君', |
|||
id: 'member2', |
|||
}, |
|||
{ |
|||
avatar: |
|||
'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png', |
|||
name: '董娜娜', |
|||
id: 'member3', |
|||
}, |
|||
], |
|||
}); |
|||
} |
|||
|
|||
return list; |
|||
} |
|||
|
|||
let sourceData: BasicListItemDataType[] = []; |
|||
|
|||
function getFakeList(req: Request, res: Response) { |
|||
const params = req.query as any; |
|||
|
|||
const count = Number(params.count) * 1 || 20; |
|||
|
|||
const result = fakeList(count); |
|||
sourceData = result; |
|||
return res.json({ |
|||
data: { |
|||
list: result, |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
function postFakeList(req: Request, res: Response) { |
|||
const { /* url = '', */ body } = req; |
|||
// const params = getUrlParams(url);
|
|||
const { method, id } = body; |
|||
// const count = (params.count * 1) || 20;
|
|||
let result = sourceData || []; |
|||
|
|||
switch (method) { |
|||
case 'delete': |
|||
result = result.filter((item) => item.id !== id); |
|||
break; |
|||
case 'update': |
|||
result.forEach((item, i) => { |
|||
if (item.id === id) { |
|||
result[i] = { ...item, ...body }; |
|||
} |
|||
}); |
|||
break; |
|||
case 'post': |
|||
result.unshift({ |
|||
...body, |
|||
id: `fake-list-${result.length}`, |
|||
createdAt: Date.now(), |
|||
}); |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
|
|||
return res.json({ |
|||
data: { |
|||
list: result, |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
export default { |
|||
'GET /api/get_list': getFakeList, |
|||
'POST /api/post_fake_list': postFakeList, |
|||
}; |
|||
@ -1,29 +0,0 @@ |
|||
export type Member = { |
|||
avatar: string; |
|||
name: string; |
|||
id: string; |
|||
}; |
|||
|
|||
export type BasicListItemDataType = { |
|||
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,50 +0,0 @@ |
|||
import { request } from '@umijs/max'; |
|||
import type { BasicListItemDataType } from './data.d'; |
|||
|
|||
type ParamsType = { |
|||
count?: number; |
|||
} & Partial<BasicListItemDataType>; |
|||
|
|||
export async function queryFakeList( |
|||
params: ParamsType, |
|||
): Promise<{ data: { list: BasicListItemDataType[] } }> { |
|||
return request('/api/get_list', { |
|||
params, |
|||
}); |
|||
} |
|||
|
|||
export async function removeFakeList( |
|||
params: ParamsType, |
|||
): Promise<{ data: { list: BasicListItemDataType[] } }> { |
|||
return request('/api/post_fake_list', { |
|||
method: 'POST', |
|||
data: { |
|||
...params, |
|||
method: 'delete', |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
export async function addFakeList( |
|||
params: ParamsType, |
|||
): Promise<{ data: { list: BasicListItemDataType[] } }> { |
|||
return request('/api/post_fake_list', { |
|||
method: 'POST', |
|||
data: { |
|||
...params, |
|||
method: 'post', |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
export async function updateFakeList( |
|||
params: ParamsType, |
|||
): Promise<{ data: { list: BasicListItemDataType[] } }> { |
|||
return request('/api/post_fake_list', { |
|||
method: 'POST', |
|||
data: { |
|||
...params, |
|||
method: 'update', |
|||
}, |
|||
}); |
|||
} |
|||
@ -1,141 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(({ token }) => { |
|||
return { |
|||
standardList: { |
|||
'.ant-card-head': { borderBottom: 'none' }, |
|||
'.ant-card-head-title': { padding: '24px 0', lineHeight: '32px' }, |
|||
'.ant-card-extra': { padding: '24px 0' }, |
|||
'.ant-list-pagination': { marginTop: '24px', textAlign: 'right' }, |
|||
'.ant-avatar-lg': { width: '48px', height: '48px', lineHeight: '48px' }, |
|||
[`@media screen and (max-width: ${token.screenXS}px)`]: { |
|||
'.ant-list-item-content': { |
|||
display: 'block', |
|||
flex: 'none', |
|||
width: '100%', |
|||
}, |
|||
'.ant-list-item-action': { |
|||
marginLeft: '0', |
|||
}, |
|||
}, |
|||
}, |
|||
headerInfo: { |
|||
position: 'relative', |
|||
textAlign: 'center', |
|||
'& > span': { |
|||
display: 'inline-block', |
|||
marginBottom: '4px', |
|||
color: token.colorTextSecondary, |
|||
fontSize: token.fontSize, |
|||
lineHeight: '22px', |
|||
}, |
|||
'& > p': { |
|||
margin: '0', |
|||
color: token.colorTextHeading, |
|||
fontSize: '24px', |
|||
lineHeight: '32px', |
|||
}, |
|||
'& > em': { |
|||
position: 'absolute', |
|||
top: '0', |
|||
right: '0', |
|||
width: '1px', |
|||
height: '56px', |
|||
backgroundColor: token.colorSplit, |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenSM}px)`]: { |
|||
marginBottom: '16px', |
|||
'& > em': { |
|||
display: 'none', |
|||
}, |
|||
}, |
|||
}, |
|||
listContent: { |
|||
fontSize: '0', |
|||
[`@media screen and (max-width: ${token.screenXS}px)`]: { |
|||
marginLeft: '0', |
|||
'& > div': { |
|||
marginLeft: '0', |
|||
}, |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenMD}px)`]: { |
|||
'& > div': { |
|||
display: 'block', |
|||
}, |
|||
'& > div:last-child': { |
|||
top: '0', |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenLG}px) and (min-width: @screen-md)`]: |
|||
{ |
|||
'& > div': { |
|||
display: 'block', |
|||
}, |
|||
'& > div:last-child': { |
|||
top: '0', |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenXL}px)`]: { |
|||
'& > div': { |
|||
marginLeft: '24px', |
|||
}, |
|||
'& > div:last-child': { |
|||
top: '0', |
|||
}, |
|||
}, |
|||
'@media screen and (max-width: 1400px)': { |
|||
textAlign: 'right', |
|||
'& > div:last-child': { |
|||
top: '0', |
|||
}, |
|||
}, |
|||
}, |
|||
listContentItem: { |
|||
display: 'inline-block', |
|||
marginLeft: '40px', |
|||
color: token.colorTextSecondary, |
|||
fontSize: token.fontSize, |
|||
verticalAlign: 'middle', |
|||
'> span': { lineHeight: '20px' }, |
|||
'> p': { marginTop: '4px', marginBottom: '0', lineHeight: '22px' }, |
|||
}, |
|||
extraContentSearch: { |
|||
width: '272px', |
|||
marginLeft: '16px', |
|||
[`@media screen and (max-width: ${token.screenSM}px)`]: { |
|||
width: '100%', |
|||
marginLeft: '0', |
|||
}, |
|||
}, |
|||
listCard: { |
|||
[`@media screen and (max-width: ${token.screenXS}px)`]: { |
|||
'.ant-card-head-title': { |
|||
overflow: 'open', |
|||
}, |
|||
}, |
|||
[`@media screen and (max-width: ${token.screenMD}px)`]: { |
|||
'.ant-radio-group': { |
|||
display: 'block', |
|||
marginBottom: '8px', |
|||
}, |
|||
}, |
|||
}, |
|||
standardListForm: { |
|||
'.ant-form-item': { |
|||
marginBottom: '12px', |
|||
'&:last-child': { |
|||
marginBottom: '32px', |
|||
paddingTop: '4px', |
|||
}, |
|||
}, |
|||
}, |
|||
formResult: { |
|||
width: '100%', |
|||
"[class^='title']": { marginBottom: '8px' }, |
|||
}, |
|||
}; |
|||
}); |
|||
|
|||
export default useStyles; |
|||
@ -1,6 +0,0 @@ |
|||
import { createStyles } from 'antd-style'; |
|||
|
|||
const useStyles = createStyles(() => { |
|||
return {}; |
|||
}); |
|||
export default useStyles; |
|||
@ -1,125 +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(Date.now() - 1000 * 60 * 60 * 2 * i).getTime(), |
|||
createdAt: new Date(Date.now() - 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[]; |
|||
}; |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue