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