committed by
GitHub
198 changed files with 18357 additions and 929 deletions
@ -0,0 +1,30 @@ |
|||||
|
name: Deploy to GitHub Pages |
||||
|
|
||||
|
on: |
||||
|
push: |
||||
|
branches: |
||||
|
- all-blocks |
||||
|
|
||||
|
jobs: |
||||
|
build-and-deploy: |
||||
|
runs-on: ubuntu-latest |
||||
|
|
||||
|
steps: |
||||
|
- name: Checkout code |
||||
|
uses: actions/checkout@v4 |
||||
|
|
||||
|
- name: Install Bun |
||||
|
uses: oven-sh/setup-bun@v2 |
||||
|
|
||||
|
- name: Install dependencies with Bun |
||||
|
run: bun install |
||||
|
|
||||
|
- name: Build project |
||||
|
run: bun run build |
||||
|
|
||||
|
- name: Deploy to GitHub Pages |
||||
|
uses: peaceiris/actions-gh-pages@v4 |
||||
|
with: |
||||
|
github_token: ${{ secrets.GITHUB_TOKEN }} |
||||
|
publish_dir: ./dist |
||||
|
cname: preview.pro.ant.design |
||||
@ -0,0 +1 @@ |
|||||
|
legacy-peer-deps=true |
||||
@ -0,0 +1,210 @@ |
|||||
|
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, |
||||
|
}; |
||||
@ -0,0 +1,14 @@ |
|||||
|
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, |
||||
|
}; |
||||
@ -0,0 +1,418 @@ |
|||||
|
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, |
||||
|
}; |
||||
@ -0,0 +1,42 @@ |
|||||
|
import { createStyles } from 'antd-style'; |
||||
|
|
||||
|
const useStyles = createStyles(() => { |
||||
|
return { |
||||
|
colorWeak: { |
||||
|
filter: 'invert(80%)', |
||||
|
}, |
||||
|
'ant-layout': { |
||||
|
minHeight: '100vh', |
||||
|
}, |
||||
|
'ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed': { |
||||
|
left: 'unset', |
||||
|
}, |
||||
|
canvas: { |
||||
|
display: 'block', |
||||
|
}, |
||||
|
body: { |
||||
|
textRendering: 'optimizeLegibility', |
||||
|
WebkitFontSmoothing: 'antialiased', |
||||
|
MozOsxFontSmoothing: 'grayscale', |
||||
|
}, |
||||
|
'ul,ol': { |
||||
|
listStyle: 'none', |
||||
|
}, |
||||
|
'@media(max-width: 768px)': { |
||||
|
'ant-table': { |
||||
|
width: '100%', |
||||
|
overflowX: 'auto', |
||||
|
'&-thead > tr, &-tbody > tr': { |
||||
|
'> th, > td': { |
||||
|
whiteSpace: 'pre', |
||||
|
'> span': { |
||||
|
display: 'block', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
export default useStyles; |
||||
@ -1,7 +1,7 @@ |
|||||
import { Skeleton } from 'antd'; |
import { Skeleton } from 'antd'; |
||||
|
|
||||
const Loading: React.FC = () => ( |
const Loading: React.FC = () => ( |
||||
<Skeleton style={{ margin: '24px 40px' }} active /> |
<Skeleton style={{ margin: '24px 40px', height: '60vh' }} active /> |
||||
); |
); |
||||
|
|
||||
export default Loading; |
export default Loading; |
||||
|
|||||
@ -1,213 +0,0 @@ |
|||||
import { |
|
||||
ProFormDateTimePicker, |
|
||||
ProFormRadio, |
|
||||
ProFormSelect, |
|
||||
ProFormText, |
|
||||
ProFormTextArea, |
|
||||
StepsForm, |
|
||||
} from '@ant-design/pro-components'; |
|
||||
import { FormattedMessage, useIntl } from '@umijs/max'; |
|
||||
import { Modal } from 'antd'; |
|
||||
import React from 'react'; |
|
||||
|
|
||||
export type FormValueType = { |
|
||||
target?: string; |
|
||||
template?: string; |
|
||||
type?: string; |
|
||||
time?: string; |
|
||||
frequency?: string; |
|
||||
} & Partial<API.RuleListItem>; |
|
||||
|
|
||||
export type UpdateFormProps = { |
|
||||
onCancel: (flag?: boolean, formVals?: FormValueType) => void; |
|
||||
onSubmit: (values: FormValueType) => Promise<void>; |
|
||||
updateModalOpen: boolean; |
|
||||
values: Partial<API.RuleListItem>; |
|
||||
}; |
|
||||
|
|
||||
const UpdateForm: React.FC<UpdateFormProps> = (props) => { |
|
||||
const intl = useIntl(); |
|
||||
return ( |
|
||||
<StepsForm |
|
||||
stepsProps={{ |
|
||||
size: 'small', |
|
||||
}} |
|
||||
stepsFormRender={(dom, submitter) => { |
|
||||
return ( |
|
||||
<Modal |
|
||||
width={640} |
|
||||
styles={{ |
|
||||
body: { |
|
||||
padding: '32px 40px 48px', |
|
||||
}, |
|
||||
}} |
|
||||
destroyOnHidden |
|
||||
title={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.ruleConfig', |
|
||||
defaultMessage: '规则配置', |
|
||||
})} |
|
||||
open={props.updateModalOpen} |
|
||||
footer={submitter} |
|
||||
onCancel={() => { |
|
||||
props.onCancel(); |
|
||||
}} |
|
||||
> |
|
||||
{dom} |
|
||||
</Modal> |
|
||||
); |
|
||||
}} |
|
||||
onFinish={props.onSubmit} |
|
||||
> |
|
||||
<StepsForm.StepForm |
|
||||
initialValues={{ |
|
||||
name: props.values.name, |
|
||||
desc: props.values.desc, |
|
||||
}} |
|
||||
title={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.basicConfig', |
|
||||
defaultMessage: '基本信息', |
|
||||
})} |
|
||||
> |
|
||||
<ProFormText |
|
||||
name="name" |
|
||||
label={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.ruleName.nameLabel', |
|
||||
defaultMessage: '规则名称', |
|
||||
})} |
|
||||
width="md" |
|
||||
rules={[ |
|
||||
{ |
|
||||
required: true, |
|
||||
message: ( |
|
||||
<FormattedMessage |
|
||||
id="pages.searchTable.updateForm.ruleName.nameRules" |
|
||||
defaultMessage="请输入规则名称!" |
|
||||
/> |
|
||||
), |
|
||||
}, |
|
||||
]} |
|
||||
/> |
|
||||
<ProFormTextArea |
|
||||
name="desc" |
|
||||
width="md" |
|
||||
label={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.ruleDesc.descLabel', |
|
||||
defaultMessage: '规则描述', |
|
||||
})} |
|
||||
placeholder={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.ruleDesc.descPlaceholder', |
|
||||
defaultMessage: '请输入至少五个字符', |
|
||||
})} |
|
||||
rules={[ |
|
||||
{ |
|
||||
required: true, |
|
||||
message: ( |
|
||||
<FormattedMessage |
|
||||
id="pages.searchTable.updateForm.ruleDesc.descRules" |
|
||||
defaultMessage="请输入至少五个字符的规则描述!" |
|
||||
/> |
|
||||
), |
|
||||
min: 5, |
|
||||
}, |
|
||||
]} |
|
||||
/> |
|
||||
</StepsForm.StepForm> |
|
||||
<StepsForm.StepForm |
|
||||
initialValues={{ |
|
||||
target: '0', |
|
||||
template: '0', |
|
||||
}} |
|
||||
title={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.ruleProps.title', |
|
||||
defaultMessage: '配置规则属性', |
|
||||
})} |
|
||||
> |
|
||||
<ProFormSelect |
|
||||
name="target" |
|
||||
width="md" |
|
||||
label={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.object', |
|
||||
defaultMessage: '监控对象', |
|
||||
})} |
|
||||
valueEnum={{ |
|
||||
0: '表一', |
|
||||
1: '表二', |
|
||||
}} |
|
||||
/> |
|
||||
<ProFormSelect |
|
||||
name="template" |
|
||||
width="md" |
|
||||
label={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.ruleProps.templateLabel', |
|
||||
defaultMessage: '规则模板', |
|
||||
})} |
|
||||
valueEnum={{ |
|
||||
0: '规则模板一', |
|
||||
1: '规则模板二', |
|
||||
}} |
|
||||
/> |
|
||||
<ProFormRadio.Group |
|
||||
name="type" |
|
||||
label={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.ruleProps.typeLabel', |
|
||||
defaultMessage: '规则类型', |
|
||||
})} |
|
||||
options={[ |
|
||||
{ |
|
||||
value: '0', |
|
||||
label: '强', |
|
||||
}, |
|
||||
{ |
|
||||
value: '1', |
|
||||
label: '弱', |
|
||||
}, |
|
||||
]} |
|
||||
/> |
|
||||
</StepsForm.StepForm> |
|
||||
<StepsForm.StepForm |
|
||||
initialValues={{ |
|
||||
type: '1', |
|
||||
frequency: 'month', |
|
||||
}} |
|
||||
title={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.schedulingPeriod.title', |
|
||||
defaultMessage: '设定调度周期', |
|
||||
})} |
|
||||
> |
|
||||
<ProFormDateTimePicker |
|
||||
name="time" |
|
||||
width="md" |
|
||||
label={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.schedulingPeriod.timeLabel', |
|
||||
defaultMessage: '开始时间', |
|
||||
})} |
|
||||
rules={[ |
|
||||
{ |
|
||||
required: true, |
|
||||
message: ( |
|
||||
<FormattedMessage |
|
||||
id="pages.searchTable.updateForm.schedulingPeriod.timeRules" |
|
||||
defaultMessage="请选择开始时间!" |
|
||||
/> |
|
||||
), |
|
||||
}, |
|
||||
]} |
|
||||
/> |
|
||||
<ProFormSelect |
|
||||
name="frequency" |
|
||||
label={intl.formatMessage({ |
|
||||
id: 'pages.searchTable.updateForm.object', |
|
||||
defaultMessage: '监控对象', |
|
||||
})} |
|
||||
width="md" |
|
||||
valueEnum={{ |
|
||||
month: '月', |
|
||||
week: '周', |
|
||||
}} |
|
||||
/> |
|
||||
</StepsForm.StepForm> |
|
||||
</StepsForm> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default UpdateForm; |
|
||||
@ -0,0 +1,69 @@ |
|||||
|
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; |
||||
@ -0,0 +1,249 @@ |
|||||
|
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, |
||||
|
}; |
||||
@ -0,0 +1,43 @@ |
|||||
|
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; |
||||
@ -0,0 +1,128 @@ |
|||||
|
import { |
||||
|
DownloadOutlined, |
||||
|
EditOutlined, |
||||
|
EllipsisOutlined, |
||||
|
ShareAltOutlined, |
||||
|
} from '@ant-design/icons'; |
||||
|
import { useRequest } from '@umijs/max'; |
||||
|
import { Avatar, Card, Dropdown, List, Tooltip } from 'antd'; |
||||
|
import numeral from 'numeral'; |
||||
|
import React from 'react'; |
||||
|
import type { ListItemDataType } from '../../data.d'; |
||||
|
import { queryFakeList } from '../../service'; |
||||
|
import useStyles from './index.style'; |
||||
|
export function formatWan(val: number) { |
||||
|
const v = val * 1; |
||||
|
if (!v || Number.isNaN(v)) return ''; |
||||
|
let result: React.ReactNode = val; |
||||
|
if (val > 10000) { |
||||
|
result = ( |
||||
|
<span> |
||||
|
{Math.floor(val / 10000)} |
||||
|
<span |
||||
|
style={{ |
||||
|
position: 'relative', |
||||
|
top: -2, |
||||
|
fontSize: 14, |
||||
|
fontStyle: 'normal', |
||||
|
marginLeft: 2, |
||||
|
}} |
||||
|
> |
||||
|
万 |
||||
|
</span> |
||||
|
</span> |
||||
|
); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
const Applications: React.FC = () => { |
||||
|
const { styles: stylesApplications } = useStyles(); |
||||
|
// 获取tab列表数据
|
||||
|
const { data: listData } = useRequest(() => { |
||||
|
return queryFakeList({ |
||||
|
count: 30, |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
const CardInfo: React.FC<{ |
||||
|
activeUser: React.ReactNode; |
||||
|
newUser: React.ReactNode; |
||||
|
}> = ({ activeUser, newUser }) => ( |
||||
|
<div className={stylesApplications.cardInfo}> |
||||
|
<div> |
||||
|
<p>活跃用户</p> |
||||
|
<p>{activeUser}</p> |
||||
|
</div> |
||||
|
<div> |
||||
|
<p>新增用户</p> |
||||
|
<p>{newUser}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
return ( |
||||
|
<List<ListItemDataType> |
||||
|
rowKey="id" |
||||
|
className={stylesApplications.filterCardList} |
||||
|
grid={{ |
||||
|
gutter: 24, |
||||
|
xxl: 3, |
||||
|
xl: 2, |
||||
|
lg: 2, |
||||
|
md: 2, |
||||
|
sm: 2, |
||||
|
xs: 1, |
||||
|
}} |
||||
|
dataSource={listData?.list || []} |
||||
|
renderItem={(item) => ( |
||||
|
<List.Item key={item.id}> |
||||
|
<Card |
||||
|
hoverable |
||||
|
bodyStyle={{ |
||||
|
paddingBottom: 20, |
||||
|
}} |
||||
|
actions={[ |
||||
|
<Tooltip key="download" title="下载"> |
||||
|
<DownloadOutlined /> |
||||
|
</Tooltip>, |
||||
|
<Tooltip title="编辑" key="edit"> |
||||
|
<EditOutlined /> |
||||
|
</Tooltip>, |
||||
|
<Tooltip title="分享" key="share"> |
||||
|
<ShareAltOutlined /> |
||||
|
</Tooltip>, |
||||
|
<Dropdown |
||||
|
menu={{ |
||||
|
items: [ |
||||
|
{ |
||||
|
key: '1', |
||||
|
title: '1st menu item', |
||||
|
}, |
||||
|
{ |
||||
|
key: '2', |
||||
|
title: '2nd menu item', |
||||
|
}, |
||||
|
], |
||||
|
}} |
||||
|
key="ellipsis" |
||||
|
> |
||||
|
<EllipsisOutlined /> |
||||
|
</Dropdown>, |
||||
|
]} |
||||
|
> |
||||
|
<Card.Meta |
||||
|
avatar={<Avatar size="small" src={item.avatar} />} |
||||
|
title={item.title} |
||||
|
/> |
||||
|
<div> |
||||
|
<CardInfo |
||||
|
activeUser={formatWan(item.activeUser)} |
||||
|
newUser={numeral(item.newUser).format('0,0')} |
||||
|
/> |
||||
|
</div> |
||||
|
</Card> |
||||
|
</List.Item> |
||||
|
)} |
||||
|
/> |
||||
|
); |
||||
|
}; |
||||
|
export default Applications; |
||||
@ -0,0 +1,31 @@ |
|||||
|
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; |
||||
@ -0,0 +1,29 @@ |
|||||
|
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; |
||||
@ -0,0 +1,14 @@ |
|||||
|
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; |
||||
@ -0,0 +1,70 @@ |
|||||
|
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; |
||||
@ -0,0 +1,41 @@ |
|||||
|
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; |
||||
@ -0,0 +1,89 @@ |
|||||
|
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; |
||||
@ -0,0 +1,49 @@ |
|||||
|
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; |
||||
@ -0,0 +1,65 @@ |
|||||
|
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; |
||||
@ -0,0 +1,75 @@ |
|||||
|
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[]; |
||||
|
}; |
||||
@ -0,0 +1,278 @@ |
|||||
|
import { |
||||
|
ClusterOutlined, |
||||
|
ContactsOutlined, |
||||
|
HomeOutlined, |
||||
|
PlusOutlined, |
||||
|
} from '@ant-design/icons'; |
||||
|
import { GridContent } from '@ant-design/pro-components'; |
||||
|
import { useRequest } from '@umijs/max'; |
||||
|
import { |
||||
|
Avatar, |
||||
|
Card, |
||||
|
Col, |
||||
|
Divider, |
||||
|
Input, |
||||
|
type InputRef, |
||||
|
Row, |
||||
|
Tag, |
||||
|
} from 'antd'; |
||||
|
import React, { useRef, useState } from 'react'; |
||||
|
import useStyles from './Center.style'; |
||||
|
import Applications from './components/Applications'; |
||||
|
import Articles from './components/Articles'; |
||||
|
import Projects from './components/Projects'; |
||||
|
import type { CurrentUser, TagType, tabKeyType } from './data.d'; |
||||
|
import { queryCurrent } from './service'; |
||||
|
|
||||
|
const operationTabList = [ |
||||
|
{ |
||||
|
key: 'articles', |
||||
|
tab: ( |
||||
|
<span> |
||||
|
文章{' '} |
||||
|
<span |
||||
|
style={{ |
||||
|
fontSize: 14, |
||||
|
}} |
||||
|
> |
||||
|
(8) |
||||
|
</span> |
||||
|
</span> |
||||
|
), |
||||
|
}, |
||||
|
{ |
||||
|
key: 'applications', |
||||
|
tab: ( |
||||
|
<span> |
||||
|
应用{' '} |
||||
|
<span |
||||
|
style={{ |
||||
|
fontSize: 14, |
||||
|
}} |
||||
|
> |
||||
|
(8) |
||||
|
</span> |
||||
|
</span> |
||||
|
), |
||||
|
}, |
||||
|
{ |
||||
|
key: 'projects', |
||||
|
tab: ( |
||||
|
<span> |
||||
|
项目{' '} |
||||
|
<span |
||||
|
style={{ |
||||
|
fontSize: 14, |
||||
|
}} |
||||
|
> |
||||
|
(8) |
||||
|
</span> |
||||
|
</span> |
||||
|
), |
||||
|
}, |
||||
|
]; |
||||
|
const TagList: React.FC<{ |
||||
|
tags: CurrentUser['tags']; |
||||
|
}> = ({ tags }) => { |
||||
|
const { styles } = useStyles(); |
||||
|
const ref = useRef<InputRef | null>(null); |
||||
|
const [newTags, setNewTags] = useState<TagType[]>([]); |
||||
|
const [inputVisible, setInputVisible] = useState<boolean>(false); |
||||
|
const [inputValue, setInputValue] = useState<string>(''); |
||||
|
const showInput = () => { |
||||
|
setInputVisible(true); |
||||
|
if (ref.current) { |
||||
|
// eslint-disable-next-line no-unused-expressions
|
||||
|
ref.current?.focus(); |
||||
|
} |
||||
|
}; |
||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
||||
|
setInputValue(e.target.value); |
||||
|
}; |
||||
|
const handleInputConfirm = () => { |
||||
|
let tempsTags = [...newTags]; |
||||
|
if ( |
||||
|
inputValue && |
||||
|
tempsTags.filter((tag) => tag.label === inputValue).length === 0 |
||||
|
) { |
||||
|
tempsTags = [ |
||||
|
...tempsTags, |
||||
|
{ |
||||
|
key: `new-${tempsTags.length}`, |
||||
|
label: inputValue, |
||||
|
}, |
||||
|
]; |
||||
|
} |
||||
|
setNewTags(tempsTags); |
||||
|
setInputVisible(false); |
||||
|
setInputValue(''); |
||||
|
}; |
||||
|
return ( |
||||
|
<div className={styles.tags}> |
||||
|
<div className={styles.tagsTitle}>标签</div> |
||||
|
{(tags || []).concat(newTags).map((item) => ( |
||||
|
<Tag key={item.key}>{item.label}</Tag> |
||||
|
))} |
||||
|
{inputVisible && ( |
||||
|
<Input |
||||
|
ref={ref} |
||||
|
size="small" |
||||
|
style={{ |
||||
|
width: 78, |
||||
|
}} |
||||
|
value={inputValue} |
||||
|
onChange={handleInputChange} |
||||
|
onBlur={handleInputConfirm} |
||||
|
onPressEnter={handleInputConfirm} |
||||
|
/> |
||||
|
)} |
||||
|
{!inputVisible && ( |
||||
|
<Tag |
||||
|
onClick={showInput} |
||||
|
style={{ |
||||
|
borderStyle: 'dashed', |
||||
|
}} |
||||
|
> |
||||
|
<PlusOutlined /> |
||||
|
</Tag> |
||||
|
)} |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
const Center: React.FC = () => { |
||||
|
const { styles } = useStyles(); |
||||
|
const [tabKey, setTabKey] = useState<tabKeyType>('articles'); |
||||
|
|
||||
|
// 获取用户信息
|
||||
|
const { data: currentUser, loading } = useRequest(() => { |
||||
|
return queryCurrent(); |
||||
|
}); |
||||
|
|
||||
|
// 渲染用户信息
|
||||
|
const renderUserInfo = ({ |
||||
|
title, |
||||
|
group, |
||||
|
geographic, |
||||
|
}: Partial<CurrentUser>) => { |
||||
|
return ( |
||||
|
<div className={styles.detail}> |
||||
|
<p> |
||||
|
<ContactsOutlined |
||||
|
style={{ |
||||
|
marginRight: 8, |
||||
|
}} |
||||
|
/> |
||||
|
{title} |
||||
|
</p> |
||||
|
<p> |
||||
|
<ClusterOutlined |
||||
|
style={{ |
||||
|
marginRight: 8, |
||||
|
}} |
||||
|
/> |
||||
|
{group} |
||||
|
</p> |
||||
|
<p> |
||||
|
<HomeOutlined |
||||
|
style={{ |
||||
|
marginRight: 8, |
||||
|
}} |
||||
|
/> |
||||
|
{ |
||||
|
( |
||||
|
geographic || { |
||||
|
province: { |
||||
|
label: '', |
||||
|
}, |
||||
|
} |
||||
|
).province.label |
||||
|
} |
||||
|
{ |
||||
|
( |
||||
|
geographic || { |
||||
|
city: { |
||||
|
label: '', |
||||
|
}, |
||||
|
} |
||||
|
).city.label |
||||
|
} |
||||
|
</p> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
// 渲染tab切换
|
||||
|
const renderChildrenByTabKey = (tabValue: tabKeyType) => { |
||||
|
if (tabValue === 'projects') { |
||||
|
return <Projects />; |
||||
|
} |
||||
|
if (tabValue === 'applications') { |
||||
|
return <Applications />; |
||||
|
} |
||||
|
if (tabValue === 'articles') { |
||||
|
return <Articles />; |
||||
|
} |
||||
|
return null; |
||||
|
}; |
||||
|
return ( |
||||
|
<GridContent> |
||||
|
<Row gutter={24}> |
||||
|
<Col lg={7} md={24}> |
||||
|
<Card |
||||
|
bordered={false} |
||||
|
style={{ |
||||
|
marginBottom: 24, |
||||
|
}} |
||||
|
loading={loading} |
||||
|
> |
||||
|
{!loading && currentUser && ( |
||||
|
<> |
||||
|
<div className={styles.avatarHolder}> |
||||
|
<img alt="" src={currentUser.avatar} /> |
||||
|
<div className={styles.name}>{currentUser.name}</div> |
||||
|
<div>{currentUser?.signature}</div> |
||||
|
</div> |
||||
|
{renderUserInfo(currentUser)} |
||||
|
<Divider dashed /> |
||||
|
<TagList tags={currentUser.tags || []} /> |
||||
|
<Divider |
||||
|
style={{ |
||||
|
marginTop: 16, |
||||
|
}} |
||||
|
dashed |
||||
|
/> |
||||
|
<div className={styles.team}> |
||||
|
<div className={styles.teamTitle}>团队</div> |
||||
|
<Row gutter={36}> |
||||
|
{currentUser.notice?.map((item) => ( |
||||
|
<Col key={item.id} lg={24} xl={12}> |
||||
|
<a href={item.href}> |
||||
|
<Avatar size="small" src={item.logo} /> |
||||
|
{item.member} |
||||
|
</a> |
||||
|
</Col> |
||||
|
))} |
||||
|
</Row> |
||||
|
</div> |
||||
|
</> |
||||
|
)} |
||||
|
</Card> |
||||
|
</Col> |
||||
|
<Col lg={17} md={24}> |
||||
|
<Card |
||||
|
className={styles.tabsCard} |
||||
|
variant="borderless" |
||||
|
tabList={operationTabList} |
||||
|
activeTabKey={tabKey} |
||||
|
onTabChange={(_tabKey: string) => { |
||||
|
setTabKey(_tabKey as tabKeyType); |
||||
|
}} |
||||
|
> |
||||
|
{renderChildrenByTabKey(tabKey)} |
||||
|
</Card> |
||||
|
</Col> |
||||
|
</Row> |
||||
|
</GridContent> |
||||
|
); |
||||
|
}; |
||||
|
export default Center; |
||||
@ -0,0 +1,14 @@ |
|||||
|
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, |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
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, |
||||
|
}; |
||||
@ -0,0 +1,39 @@ |
|||||
|
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; |
||||
@ -0,0 +1,234 @@ |
|||||
|
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; |
||||
@ -0,0 +1,50 @@ |
|||||
|
import { |
||||
|
AlipayOutlined, |
||||
|
DingdingOutlined, |
||||
|
TaobaoOutlined, |
||||
|
} from '@ant-design/icons'; |
||||
|
import { List } from 'antd'; |
||||
|
import React, { Fragment } 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 ( |
||||
|
<Fragment> |
||||
|
<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> |
||||
|
)} |
||||
|
/> |
||||
|
</Fragment> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default BindingView; |
||||
@ -0,0 +1,60 @@ |
|||||
|
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; |
||||
@ -0,0 +1,46 @@ |
|||||
|
import { List, Switch } from 'antd'; |
||||
|
import React, { Fragment } 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 ( |
||||
|
<Fragment> |
||||
|
<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> |
||||
|
)} |
||||
|
/> |
||||
|
</Fragment> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default NotificationView; |
||||
@ -0,0 +1,60 @@ |
|||||
|
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; |
||||
@ -0,0 +1,43 @@ |
|||||
|
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
@ -0,0 +1,138 @@ |
|||||
|
[ |
||||
|
{ |
||||
|
"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" |
||||
|
} |
||||
|
] |
||||
@ -0,0 +1,108 @@ |
|||||
|
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; |
||||
@ -0,0 +1,20 @@ |
|||||
|
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'); |
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
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; |
||||
@ -0,0 +1,210 @@ |
|||||
|
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, |
||||
|
}; |
||||
@ -0,0 +1,75 @@ |
|||||
|
@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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
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; |
||||
@ -0,0 +1,109 @@ |
|||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||
|
import { Card } from 'antd'; |
||||
|
import type { CardProps } from 'antd/es/card'; |
||||
|
import classNames from 'classnames'; |
||||
|
import React from 'react'; |
||||
|
import useStyles from './index.style'; |
||||
|
|
||||
|
type totalType = () => React.ReactNode; |
||||
|
|
||||
|
export type ChartCardProps = { |
||||
|
title: React.ReactNode; |
||||
|
action?: React.ReactNode; |
||||
|
total?: React.ReactNode | number | (() => React.ReactNode | number); |
||||
|
footer?: React.ReactNode; |
||||
|
contentHeight?: number; |
||||
|
avatar?: React.ReactNode; |
||||
|
style?: React.CSSProperties; |
||||
|
} & CardProps; |
||||
|
|
||||
|
const ChartCard: React.FC<ChartCardProps> = (props) => { |
||||
|
const { styles } = useStyles(); |
||||
|
const renderTotal = (total?: number | totalType | React.ReactNode) => { |
||||
|
if (!total && total !== 0) { |
||||
|
return null; |
||||
|
} |
||||
|
let totalDom: React.ReactNode | null = null; |
||||
|
switch (typeof total) { |
||||
|
case 'undefined': |
||||
|
totalDom = null; |
||||
|
break; |
||||
|
case 'function': |
||||
|
totalDom = <div className={styles.total}>{total()}</div>; |
||||
|
break; |
||||
|
default: |
||||
|
totalDom = <div className={styles.total}>{total}</div>; |
||||
|
} |
||||
|
return totalDom; |
||||
|
}; |
||||
|
const renderContent = () => { |
||||
|
const { |
||||
|
contentHeight, |
||||
|
title, |
||||
|
avatar, |
||||
|
action, |
||||
|
total, |
||||
|
footer, |
||||
|
children, |
||||
|
loading, |
||||
|
} = props; |
||||
|
if (loading) { |
||||
|
return false; |
||||
|
} |
||||
|
return ( |
||||
|
<div className={styles.chartCard}> |
||||
|
<div |
||||
|
className={classNames(styles.chartTop, { |
||||
|
[styles.chartTopMargin]: !children && !footer, |
||||
|
})} |
||||
|
> |
||||
|
<div className={styles.avatar}>{avatar}</div> |
||||
|
<div className={styles.metaWrap}> |
||||
|
<div className={styles.meta}> |
||||
|
<span>{title}</span> |
||||
|
<span className={styles.action}>{action}</span> |
||||
|
</div> |
||||
|
{renderTotal(total)} |
||||
|
</div> |
||||
|
</div> |
||||
|
{children && ( |
||||
|
<div |
||||
|
className={styles.content} |
||||
|
style={{ |
||||
|
height: contentHeight || 'auto', |
||||
|
}} |
||||
|
> |
||||
|
<div className={contentHeight ? styles.contentFixed : undefined}> |
||||
|
{children} |
||||
|
</div> |
||||
|
</div> |
||||
|
)} |
||||
|
{footer && ( |
||||
|
<div |
||||
|
className={classNames(styles.footer, { |
||||
|
[styles.footerMargin]: !children, |
||||
|
})} |
||||
|
> |
||||
|
{footer} |
||||
|
</div> |
||||
|
)} |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const { loading = false, ...rest } = props; |
||||
|
return ( |
||||
|
<Card |
||||
|
loading={loading} |
||||
|
styles={{ |
||||
|
body: { |
||||
|
padding: '20px 24px 8px 24px', |
||||
|
}, |
||||
|
}} |
||||
|
{...rest} |
||||
|
> |
||||
|
{renderContent()} |
||||
|
</Card> |
||||
|
); |
||||
|
}; |
||||
|
export default ChartCard; |
||||
@ -0,0 +1,17 @@ |
|||||
|
@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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
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; |
||||
@ -0,0 +1,17 @@ |
|||||
|
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; |
||||
@ -0,0 +1,48 @@ |
|||||
|
import { Tooltip } from 'antd'; |
||||
|
import React from 'react'; |
||||
|
export type MiniProgressProps = { |
||||
|
target: number; |
||||
|
targetLabel?: string; |
||||
|
color?: string; |
||||
|
strokeWidth?: number; |
||||
|
percent?: number; |
||||
|
style?: React.CSSProperties; |
||||
|
}; |
||||
|
const MiniProgress: React.FC<MiniProgressProps> = ({ |
||||
|
targetLabel, |
||||
|
target, |
||||
|
color = 'rgb(19, 194, 194)', |
||||
|
strokeWidth, |
||||
|
percent, |
||||
|
}) => { |
||||
|
return ( |
||||
|
<div> |
||||
|
<Tooltip title={targetLabel}> |
||||
|
<div |
||||
|
style={{ |
||||
|
left: target ? `${target}%` : undefined, |
||||
|
}} |
||||
|
> |
||||
|
<span |
||||
|
style={{ |
||||
|
backgroundColor: color || undefined, |
||||
|
}} |
||||
|
/> |
||||
|
<span |
||||
|
style={{ |
||||
|
backgroundColor: color || undefined, |
||||
|
}} |
||||
|
/> |
||||
|
</div> |
||||
|
</Tooltip> |
||||
|
<div |
||||
|
style={{ |
||||
|
backgroundColor: color || undefined, |
||||
|
width: percent ? `${percent}%` : undefined, |
||||
|
height: strokeWidth || undefined, |
||||
|
}} |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
export default MiniProgress; |
||||
@ -0,0 +1,225 @@ |
|||||
|
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); |
||||
@ -0,0 +1,19 @@ |
|||||
|
.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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
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; |
||||
@ -0,0 +1,13 @@ |
|||||
|
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 }; |
||||
@ -0,0 +1,168 @@ |
|||||
|
import { InfoCircleOutlined } from '@ant-design/icons'; |
||||
|
import { Area, Column } from '@ant-design/plots'; |
||||
|
import { Col, Progress, Row, Tooltip } from 'antd'; |
||||
|
import numeral from 'numeral'; |
||||
|
import type { DataItem } from '../data.d'; |
||||
|
import useStyles from '../style.style'; |
||||
|
import Yuan from '../utils/Yuan'; |
||||
|
import { ChartCard, Field } from './Charts'; |
||||
|
import Trend from './Trend'; |
||||
|
|
||||
|
const topColResponsiveProps = { |
||||
|
xs: 24, |
||||
|
sm: 12, |
||||
|
md: 12, |
||||
|
lg: 12, |
||||
|
xl: 6, |
||||
|
style: { |
||||
|
marginBottom: 24, |
||||
|
}, |
||||
|
}; |
||||
|
const IntroduceRow = ({ |
||||
|
loading, |
||||
|
visitData, |
||||
|
}: { |
||||
|
loading: boolean; |
||||
|
visitData: DataItem[]; |
||||
|
}) => { |
||||
|
const { styles } = useStyles(); |
||||
|
return ( |
||||
|
<Row gutter={24}> |
||||
|
<Col {...topColResponsiveProps}> |
||||
|
<ChartCard |
||||
|
bordered={false} |
||||
|
title="总销售额" |
||||
|
action={ |
||||
|
<Tooltip title="指标说明"> |
||||
|
<InfoCircleOutlined /> |
||||
|
</Tooltip> |
||||
|
} |
||||
|
loading={loading} |
||||
|
total={() => <Yuan>126560</Yuan>} |
||||
|
footer={ |
||||
|
<Field |
||||
|
label="日销售额" |
||||
|
value={`¥${numeral(12423).format('0,0')}`} |
||||
|
/> |
||||
|
} |
||||
|
contentHeight={46} |
||||
|
> |
||||
|
<Trend |
||||
|
flag="up" |
||||
|
style={{ |
||||
|
marginRight: 16, |
||||
|
}} |
||||
|
> |
||||
|
周同比 |
||||
|
<span className={styles.trendText}>12%</span> |
||||
|
</Trend> |
||||
|
<Trend flag="down"> |
||||
|
日同比 |
||||
|
<span className={styles.trendText}>11%</span> |
||||
|
</Trend> |
||||
|
</ChartCard> |
||||
|
</Col> |
||||
|
|
||||
|
<Col {...topColResponsiveProps}> |
||||
|
<ChartCard |
||||
|
bordered={false} |
||||
|
loading={loading} |
||||
|
title="访问量" |
||||
|
action={ |
||||
|
<Tooltip title="指标说明"> |
||||
|
<InfoCircleOutlined /> |
||||
|
</Tooltip> |
||||
|
} |
||||
|
total={numeral(8846).format('0,0')} |
||||
|
footer={ |
||||
|
<Field label="日访问量" value={numeral(1234).format('0,0')} /> |
||||
|
} |
||||
|
contentHeight={46} |
||||
|
> |
||||
|
<Area |
||||
|
xField="x" |
||||
|
yField="y" |
||||
|
shapeField="smooth" |
||||
|
height={46} |
||||
|
axis={false} |
||||
|
style={{ |
||||
|
fill: 'linear-gradient(-90deg, white 0%, #975FE4 100%)', |
||||
|
fillOpacity: 0.6, |
||||
|
width: '100%', |
||||
|
}} |
||||
|
padding={-20} |
||||
|
data={visitData} |
||||
|
/> |
||||
|
</ChartCard> |
||||
|
</Col> |
||||
|
<Col {...topColResponsiveProps}> |
||||
|
<ChartCard |
||||
|
bordered={false} |
||||
|
loading={loading} |
||||
|
title="支付笔数" |
||||
|
action={ |
||||
|
<Tooltip title="指标说明"> |
||||
|
<InfoCircleOutlined /> |
||||
|
</Tooltip> |
||||
|
} |
||||
|
total={numeral(6560).format('0,0')} |
||||
|
footer={<Field label="转化率" value="60%" />} |
||||
|
contentHeight={46} |
||||
|
> |
||||
|
<Column |
||||
|
xField="x" |
||||
|
yField="y" |
||||
|
padding={-20} |
||||
|
axis={false} |
||||
|
height={46} |
||||
|
data={visitData} |
||||
|
scale={{ x: { paddingInner: 0.4 } }} |
||||
|
/> |
||||
|
</ChartCard> |
||||
|
</Col> |
||||
|
<Col {...topColResponsiveProps}> |
||||
|
<ChartCard |
||||
|
loading={loading} |
||||
|
bordered={false} |
||||
|
title="运营活动效果" |
||||
|
action={ |
||||
|
<Tooltip title="指标说明"> |
||||
|
<InfoCircleOutlined /> |
||||
|
</Tooltip> |
||||
|
} |
||||
|
total="78%" |
||||
|
footer={ |
||||
|
<div |
||||
|
style={{ |
||||
|
whiteSpace: 'nowrap', |
||||
|
overflow: 'hidden', |
||||
|
}} |
||||
|
> |
||||
|
<Trend |
||||
|
flag="up" |
||||
|
style={{ |
||||
|
marginRight: 16, |
||||
|
}} |
||||
|
> |
||||
|
周同比 |
||||
|
<span className={styles.trendText}>12%</span> |
||||
|
</Trend> |
||||
|
<Trend flag="down"> |
||||
|
日同比 |
||||
|
<span className={styles.trendText}>11%</span> |
||||
|
</Trend> |
||||
|
</div> |
||||
|
} |
||||
|
contentHeight={46} |
||||
|
> |
||||
|
<Progress |
||||
|
percent={78} |
||||
|
strokeColor={{ from: '#108ee9', to: '#87d068' }} |
||||
|
status="active" |
||||
|
/> |
||||
|
</ChartCard> |
||||
|
</Col> |
||||
|
</Row> |
||||
|
); |
||||
|
}; |
||||
|
export default IntroduceRow; |
||||
@ -0,0 +1,68 @@ |
|||||
|
@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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
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; |
||||
@ -0,0 +1,79 @@ |
|||||
|
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; |
||||
@ -0,0 +1,110 @@ |
|||||
|
import { Line, Tiny } from '@ant-design/plots'; |
||||
|
import { Card, Col, Row, Tabs } from 'antd'; |
||||
|
import type { DataItem, OfflineDataType } from '../data.d'; |
||||
|
import useStyles from '../style.style'; |
||||
|
import NumberInfo from './NumberInfo'; |
||||
|
|
||||
|
const CustomTab = ({ |
||||
|
data, |
||||
|
currentTabKey: currentKey, |
||||
|
}: { |
||||
|
data: OfflineDataType; |
||||
|
currentTabKey: string; |
||||
|
}) => ( |
||||
|
<Row |
||||
|
gutter={8} |
||||
|
style={{ |
||||
|
width: 138, |
||||
|
margin: '8px 0', |
||||
|
}} |
||||
|
> |
||||
|
<Col span={12}> |
||||
|
<NumberInfo |
||||
|
title={data.name} |
||||
|
subTitle="转化率" |
||||
|
gap={2} |
||||
|
total={`${data.cvr * 100}%`} |
||||
|
theme={currentKey !== data.name ? 'light' : undefined} |
||||
|
/> |
||||
|
</Col> |
||||
|
<Col |
||||
|
span={12} |
||||
|
style={{ |
||||
|
paddingTop: 36, |
||||
|
}} |
||||
|
> |
||||
|
<Tiny.Ring |
||||
|
height={60} |
||||
|
width={60} |
||||
|
percent={data.cvr} |
||||
|
color={['#E8EEF4', '#5FABF4']} |
||||
|
/> |
||||
|
</Col> |
||||
|
</Row> |
||||
|
); |
||||
|
|
||||
|
const OfflineData = ({ |
||||
|
activeKey, |
||||
|
loading, |
||||
|
offlineData, |
||||
|
offlineChartData, |
||||
|
handleTabChange, |
||||
|
}: { |
||||
|
activeKey: string; |
||||
|
loading: boolean; |
||||
|
offlineData: OfflineDataType[]; |
||||
|
offlineChartData: DataItem[]; |
||||
|
handleTabChange: (activeKey: string) => void; |
||||
|
}) => { |
||||
|
const { styles } = useStyles(); |
||||
|
return ( |
||||
|
<Card |
||||
|
loading={loading} |
||||
|
className={styles.offlineCard} |
||||
|
bordered={false} |
||||
|
style={{ |
||||
|
marginTop: 32, |
||||
|
}} |
||||
|
> |
||||
|
<Tabs |
||||
|
activeKey={activeKey} |
||||
|
onChange={handleTabChange} |
||||
|
items={offlineData.map((shop) => ({ |
||||
|
key: shop.name, |
||||
|
label: <CustomTab data={shop} currentTabKey={activeKey} />, |
||||
|
children: ( |
||||
|
<div |
||||
|
style={{ |
||||
|
padding: '0 24px', |
||||
|
}} |
||||
|
> |
||||
|
<Line |
||||
|
height={400} |
||||
|
data={offlineChartData} |
||||
|
xField="date" |
||||
|
yField="value" |
||||
|
colorField="type" |
||||
|
slider={{ x: true }} |
||||
|
axis={{ |
||||
|
x: { title: false }, |
||||
|
y: { |
||||
|
title: false, |
||||
|
gridLineDash: null, |
||||
|
gridStroke: '#ccc', |
||||
|
gridStrokeOpacity: 1, |
||||
|
}, |
||||
|
}} |
||||
|
legend={{ |
||||
|
color: { |
||||
|
layout: { justifyContent: 'center' }, |
||||
|
}, |
||||
|
}} |
||||
|
/> |
||||
|
</div> |
||||
|
), |
||||
|
}))} |
||||
|
/> |
||||
|
</Card> |
||||
|
); |
||||
|
}; |
||||
|
export default OfflineData; |
||||
@ -0,0 +1,9 @@ |
|||||
|
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> |
||||
|
); |
||||
@ -0,0 +1,67 @@ |
|||||
|
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; |
||||
@ -0,0 +1,225 @@ |
|||||
|
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; |
||||
@ -0,0 +1,181 @@ |
|||||
|
import { InfoCircleOutlined } from '@ant-design/icons'; |
||||
|
import { Area } from '@ant-design/plots'; |
||||
|
import { Card, Col, Row, Table, Tooltip } from 'antd'; |
||||
|
import numeral from 'numeral'; |
||||
|
import React from 'react'; |
||||
|
import type { DataItem } from '../data.d'; |
||||
|
import NumberInfo from './NumberInfo'; |
||||
|
import Trend from './Trend'; |
||||
|
|
||||
|
const TopSearch = ({ |
||||
|
loading, |
||||
|
visitData2, |
||||
|
searchData, |
||||
|
dropdownGroup, |
||||
|
}: { |
||||
|
loading: boolean; |
||||
|
visitData2: DataItem[]; |
||||
|
dropdownGroup: React.ReactNode; |
||||
|
searchData: DataItem[]; |
||||
|
}) => { |
||||
|
const columns = [ |
||||
|
{ |
||||
|
title: '排名', |
||||
|
dataIndex: 'index', |
||||
|
key: 'index', |
||||
|
}, |
||||
|
{ |
||||
|
title: '搜索关键词', |
||||
|
dataIndex: 'keyword', |
||||
|
key: 'keyword', |
||||
|
render: (text: React.ReactNode) => <a href="/">{text}</a>, |
||||
|
}, |
||||
|
{ |
||||
|
title: '用户数', |
||||
|
dataIndex: 'count', |
||||
|
key: 'count', |
||||
|
sorter: ( |
||||
|
a: { |
||||
|
count: number; |
||||
|
}, |
||||
|
b: { |
||||
|
count: number; |
||||
|
}, |
||||
|
) => a.count - b.count, |
||||
|
}, |
||||
|
{ |
||||
|
title: '周涨幅', |
||||
|
dataIndex: 'range', |
||||
|
key: 'range', |
||||
|
sorter: ( |
||||
|
a: { |
||||
|
range: number; |
||||
|
}, |
||||
|
b: { |
||||
|
range: number; |
||||
|
}, |
||||
|
) => a.range - b.range, |
||||
|
render: ( |
||||
|
text: React.ReactNode, |
||||
|
record: { |
||||
|
status: number; |
||||
|
}, |
||||
|
) => ( |
||||
|
<Trend flag={record.status === 1 ? 'down' : 'up'}> |
||||
|
<span |
||||
|
style={{ |
||||
|
marginRight: 4, |
||||
|
}} |
||||
|
> |
||||
|
{text}% |
||||
|
</span> |
||||
|
</Trend> |
||||
|
), |
||||
|
}, |
||||
|
]; |
||||
|
return ( |
||||
|
<Card |
||||
|
loading={loading} |
||||
|
bordered={false} |
||||
|
title="线上热门搜索" |
||||
|
extra={dropdownGroup} |
||||
|
style={{ |
||||
|
height: '100%', |
||||
|
}} |
||||
|
> |
||||
|
<Row gutter={68}> |
||||
|
<Col |
||||
|
sm={12} |
||||
|
xs={24} |
||||
|
style={{ |
||||
|
marginBottom: 24, |
||||
|
}} |
||||
|
> |
||||
|
<NumberInfo |
||||
|
subTitle={ |
||||
|
<span> |
||||
|
搜索用户数 |
||||
|
<Tooltip title="指标说明"> |
||||
|
<InfoCircleOutlined |
||||
|
style={{ |
||||
|
marginLeft: 8, |
||||
|
}} |
||||
|
/> |
||||
|
</Tooltip> |
||||
|
</span> |
||||
|
} |
||||
|
gap={8} |
||||
|
total={numeral(12321).format('0,0')} |
||||
|
status="up" |
||||
|
subTotal={17.1} |
||||
|
/> |
||||
|
<Area |
||||
|
xField="x" |
||||
|
yField="y" |
||||
|
shapeField="smooth" |
||||
|
height={45} |
||||
|
axis={false} |
||||
|
padding={-12} |
||||
|
style={{ |
||||
|
fill: 'linear-gradient(-90deg, white 0%, #6294FA 100%)', |
||||
|
fillOpacity: 0.4, |
||||
|
}} |
||||
|
data={visitData2} |
||||
|
/> |
||||
|
</Col> |
||||
|
<Col |
||||
|
sm={12} |
||||
|
xs={24} |
||||
|
style={{ |
||||
|
marginBottom: 24, |
||||
|
}} |
||||
|
> |
||||
|
<NumberInfo |
||||
|
subTitle={ |
||||
|
<span> |
||||
|
人均搜索次数 |
||||
|
<Tooltip title="指标说明"> |
||||
|
<InfoCircleOutlined |
||||
|
style={{ |
||||
|
marginLeft: 8, |
||||
|
}} |
||||
|
/> |
||||
|
</Tooltip> |
||||
|
</span> |
||||
|
} |
||||
|
total={2.7} |
||||
|
status="down" |
||||
|
subTotal={26.2} |
||||
|
gap={8} |
||||
|
/> |
||||
|
<Area |
||||
|
xField="x" |
||||
|
yField="y" |
||||
|
shapeField="smooth" |
||||
|
height={45} |
||||
|
padding={-12} |
||||
|
style={{ |
||||
|
fill: 'linear-gradient(-90deg, white 0%, #6294FA 100%)', |
||||
|
fillOpacity: 0.4, |
||||
|
}} |
||||
|
data={visitData2} |
||||
|
axis={false} |
||||
|
/> |
||||
|
</Col> |
||||
|
</Row> |
||||
|
<Table<any> |
||||
|
rowKey={(record) => record.index} |
||||
|
size="small" |
||||
|
columns={columns} |
||||
|
dataSource={searchData} |
||||
|
pagination={{ |
||||
|
style: { |
||||
|
marginBottom: 0, |
||||
|
}, |
||||
|
pageSize: 5, |
||||
|
}} |
||||
|
/> |
||||
|
</Card> |
||||
|
); |
||||
|
}; |
||||
|
export default TopSearch; |
||||
@ -0,0 +1,37 @@ |
|||||
|
@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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
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; |
||||
@ -0,0 +1,47 @@ |
|||||
|
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; |
||||
@ -0,0 +1,45 @@ |
|||||
|
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[]; |
||||
|
} |
||||
@ -0,0 +1,157 @@ |
|||||
|
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; |
||||
@ -0,0 +1,6 @@ |
|||||
|
import { request } from '@umijs/max'; |
||||
|
import type { AnalysisData } from './data'; |
||||
|
|
||||
|
export async function fakeChartData(): Promise<{ data: AnalysisData }> { |
||||
|
return request('/api/fake_analysis_chart_data'); |
||||
|
} |
||||
@ -0,0 +1,189 @@ |
|||||
|
@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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,160 @@ |
|||||
|
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; |
||||
@ -0,0 +1,33 @@ |
|||||
|
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; |
||||
|
}} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
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`)]; |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
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, |
||||
|
}; |
||||
@ -0,0 +1,51 @@ |
|||||
|
.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; |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
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; |
||||
@ -0,0 +1,93 @@ |
|||||
|
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; |
||||
@ -0,0 +1,225 @@ |
|||||
|
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); |
||||
@ -0,0 +1,78 @@ |
|||||
|
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; |
||||
@ -0,0 +1,153 @@ |
|||||
|
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> |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
export type TagType = { |
||||
|
name: string; |
||||
|
value: number; |
||||
|
type: string; |
||||
|
}; |
||||
@ -0,0 +1,197 @@ |
|||||
|
import { Gauge, Liquid, WordCloud } from '@ant-design/plots'; |
||||
|
import { GridContent } from '@ant-design/pro-components'; |
||||
|
import { useRequest } from '@umijs/max'; |
||||
|
import { Card, Col, Progress, Row, Statistic } from 'antd'; |
||||
|
import numeral from 'numeral'; |
||||
|
import type { FC } from 'react'; |
||||
|
import ActiveChart from './components/ActiveChart'; |
||||
|
import MonitorMap from './components/Map'; |
||||
|
import { queryTags } from './service'; |
||||
|
import useStyles from './style.style'; |
||||
|
|
||||
|
const { Countdown } = Statistic; |
||||
|
const deadline = Date.now() + 1000 * 60 * 60 * 24 * 2 + 1000 * 30; // Moment is also OK
|
||||
|
|
||||
|
const Monitor: FC = () => { |
||||
|
const { styles } = useStyles(); |
||||
|
const { loading, data } = useRequest(queryTags); |
||||
|
const wordCloudData = (data?.list || []).map((item) => { |
||||
|
return { |
||||
|
id: +Date.now(), |
||||
|
word: item.name, |
||||
|
weight: item.value, |
||||
|
}; |
||||
|
}); |
||||
|
return ( |
||||
|
<GridContent> |
||||
|
<Row gutter={24}> |
||||
|
<Col |
||||
|
xl={18} |
||||
|
lg={24} |
||||
|
md={24} |
||||
|
sm={24} |
||||
|
xs={24} |
||||
|
style={{ |
||||
|
marginBottom: 24, |
||||
|
}} |
||||
|
> |
||||
|
<Card title="活动实时交易情况" bordered={false}> |
||||
|
<Row> |
||||
|
<Col md={6} sm={12} xs={24}> |
||||
|
<Statistic |
||||
|
title="今日交易总额" |
||||
|
suffix="元" |
||||
|
value={numeral(124543233).format('0,0')} |
||||
|
/> |
||||
|
</Col> |
||||
|
<Col md={6} sm={12} xs={24}> |
||||
|
<Statistic title="销售目标完成率" value="92%" /> |
||||
|
</Col> |
||||
|
<Col md={6} sm={12} xs={24}> |
||||
|
<Countdown |
||||
|
title="活动剩余时间" |
||||
|
value={deadline} |
||||
|
format="HH:mm:ss:SSS" |
||||
|
/> |
||||
|
</Col> |
||||
|
<Col md={6} sm={12} xs={24}> |
||||
|
<Statistic |
||||
|
title="每秒交易总额" |
||||
|
suffix="元" |
||||
|
value={numeral(234).format('0,0')} |
||||
|
/> |
||||
|
</Col> |
||||
|
</Row> |
||||
|
<div className={styles.mapChart}> |
||||
|
<MonitorMap /> |
||||
|
</div> |
||||
|
</Card> |
||||
|
</Col> |
||||
|
<Col xl={6} lg={24} md={24} sm={24} xs={24}> |
||||
|
<Card |
||||
|
title="活动情况预测" |
||||
|
style={{ |
||||
|
marginBottom: 24, |
||||
|
}} |
||||
|
bordered={false} |
||||
|
> |
||||
|
<ActiveChart /> |
||||
|
</Card> |
||||
|
<Card |
||||
|
title="券核效率" |
||||
|
style={{ |
||||
|
marginBottom: 24, |
||||
|
}} |
||||
|
bodyStyle={{ |
||||
|
textAlign: 'center', |
||||
|
}} |
||||
|
bordered={false} |
||||
|
> |
||||
|
<Gauge |
||||
|
height={180} |
||||
|
data={ |
||||
|
{ |
||||
|
target: 80, |
||||
|
total: 100, |
||||
|
name: 'score', |
||||
|
thresholds: [20, 40, 60, 80, 100], |
||||
|
} as any |
||||
|
} |
||||
|
padding={-16} |
||||
|
style={{ |
||||
|
textContent: () => '优', |
||||
|
}} |
||||
|
meta={{ |
||||
|
color: { |
||||
|
range: [ |
||||
|
'#6395FA', |
||||
|
'#62DAAB', |
||||
|
'#657798', |
||||
|
'#F7C128', |
||||
|
'#1F8718', |
||||
|
], |
||||
|
}, |
||||
|
}} |
||||
|
/> |
||||
|
</Card> |
||||
|
</Col> |
||||
|
</Row> |
||||
|
<Row gutter={24}> |
||||
|
<Col |
||||
|
xl={12} |
||||
|
lg={24} |
||||
|
sm={24} |
||||
|
xs={24} |
||||
|
style={{ |
||||
|
marginBottom: 24, |
||||
|
}} |
||||
|
> |
||||
|
<Card title="各品类占比" bordered={false}> |
||||
|
<Row |
||||
|
style={{ |
||||
|
padding: '16px 0', |
||||
|
}} |
||||
|
> |
||||
|
<Col span={8}> |
||||
|
<Progress type="dashboard" percent={75} /> |
||||
|
</Col> |
||||
|
<Col span={8}> |
||||
|
<Progress type="dashboard" percent={48} /> |
||||
|
</Col> |
||||
|
<Col span={8}> |
||||
|
<Progress type="dashboard" percent={33} /> |
||||
|
</Col> |
||||
|
</Row> |
||||
|
</Card> |
||||
|
</Col> |
||||
|
<Col |
||||
|
xl={6} |
||||
|
lg={12} |
||||
|
sm={24} |
||||
|
xs={24} |
||||
|
style={{ |
||||
|
marginBottom: 24, |
||||
|
}} |
||||
|
> |
||||
|
<Card |
||||
|
title="热门搜索" |
||||
|
loading={loading} |
||||
|
bordered={false} |
||||
|
bodyStyle={{ |
||||
|
overflow: 'hidden', |
||||
|
}} |
||||
|
> |
||||
|
<WordCloud |
||||
|
data={wordCloudData} |
||||
|
height={162} |
||||
|
textField="word" |
||||
|
colorField="word" |
||||
|
layout={{ spiral: 'rectangular', fontSize: [10, 20] }} |
||||
|
/> |
||||
|
</Card> |
||||
|
</Col> |
||||
|
<Col |
||||
|
xl={6} |
||||
|
lg={12} |
||||
|
sm={24} |
||||
|
xs={24} |
||||
|
style={{ |
||||
|
marginBottom: 24, |
||||
|
}} |
||||
|
> |
||||
|
<Card |
||||
|
title="资源剩余" |
||||
|
bodyStyle={{ |
||||
|
textAlign: 'center', |
||||
|
fontSize: 0, |
||||
|
}} |
||||
|
bordered={false} |
||||
|
> |
||||
|
<Liquid height={160} percent={0.35} /> |
||||
|
</Card> |
||||
|
</Col> |
||||
|
</Row> |
||||
|
</GridContent> |
||||
|
); |
||||
|
}; |
||||
|
export default Monitor; |
||||
@ -0,0 +1,6 @@ |
|||||
|
import { request } from '@umijs/max'; |
||||
|
import type { TagType } from './data'; |
||||
|
|
||||
|
export async function queryTags(): Promise<{ data: { list: TagType[] } }> { |
||||
|
return request('/api/tags'); |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
@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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
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; |
||||
@ -0,0 +1,410 @@ |
|||||
|
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, |
||||
|
}; |
||||
@ -0,0 +1,16 @@ |
|||||
|
@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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
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; |
||||
@ -0,0 +1,38 @@ |
|||||
|
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; |
||||
@ -0,0 +1,111 @@ |
|||||
|
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; |
||||
|
}; |
||||
@ -0,0 +1,286 @@ |
|||||
|
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; |
||||
@ -0,0 +1,14 @@ |
|||||
|
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'); |
||||
|
} |
||||
@ -0,0 +1,251 @@ |
|||||
|
@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%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,215 @@ |
|||||
|
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; |
||||
@ -0,0 +1,17 @@ |
|||||
|
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> |
||||
|
); |
||||
@ -0,0 +1,17 @@ |
|||||
|
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> |
||||
|
); |
||||
@ -0,0 +1,17 @@ |
|||||
|
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> |
||||
|
); |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue