Commit aecf0e3f authored by Bill's avatar Bill

feat: 添加预警工作台

parent d9a26a8e
......@@ -61,6 +61,7 @@
"@umijs/plugin-esbuild": "^1.0.1",
"@umijs/preset-react": "1.x",
"@umijs/test": "^3.2.0",
"antd": "^4.16.6",
"antd-img-crop": "^3.12.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"bignumber.js": "^9.0.1",
......
......@@ -39,7 +39,10 @@ export const commonColumns = [
title: '整改结果',
dataIndex: 'agreeResultName',
render: (text, record) => {
return record.agreeResult ? record.agreeResultName : '';
if (record.agreeResult === null) {
return ''
}
return record.agreeResultName;
}
},
{
......
import React, { useState, useEffect } from 'react';
import {
Chart,
Area,
Axis,
Coordinate,
registerAnimation,
} from 'bizcharts';
import Point from 'bizcharts/lib/geometry/Point';
import { Annotation } from 'bizcharts/lib';
import { registerShape } from '@antv/g2/lib';
// 自定义Shape 部分
registerShape('point', 'pointer', {
draw(cfg, container) {
const group = container.addGroup();
console.log(cfg.y)
const center = this.parsePoint({ x: 0, y: 0 }); // 获取极坐标系下画布中心点
const start = this.parsePoint({ x: 0, y: 0.5 }); // 获取极坐标系下起始点
// 绘制指针
const line = group.addShape('line', {
attrs: {
x1: center.x,
y1: center.y,
x2: cfg.x,
y2: cfg.y,
stroke: '#c0c4cc',
lineWidth: 5,
lineCap: 'round',
},
});
group.addShape('circle', {
attrs: {
x: center.x,
y: center.y,
r: 9.75,
stroke: '#c0c4cc',
lineWidth: 4.5,
fill: '#fff',
},
});
const preAngle = this.preAngle || 0;
const angle1 = Math.atan((start.y - center.y) / (start.x - center.x));
const angle = (Math.PI - 2 * (angle1)) * cfg.points[0].x;
if (group.cfg.animable) {
group.animate((ratio) => {
group.resetMatrix();
group.rotateAtPoint(center.x, center.y, preAngle + (angle - preAngle) * ratio);
}, 300);
} else {
group.rotateAtPoint(center.x, center.y, angle);
}
this.preAngle = angle;
return group;
},
});
// registerAnimation('cust-animation', (shape) => {
// console.log('cust-animation', shape)
// })
const scale = {
value: {
min: 0,
max: 1,
tickInterval: 0.1,
formatter: v => v * 100
}
}
const AnnotationArc = () => {
const [data, setData] = useState([{ value: 0.56 }]);
useEffect(() => {
setTimeout(() => {
setData([{ value: 0.20 }])
}, 1000)
}, [])
return (
<Chart
height={255}
data={data}
scale={scale}
autoFit
>
<Coordinate
type="polar"
radius={0.75}
startAngle={(-12 / 10) * Math.PI}
endAngle={(2 / 10) * Math.PI}
/>
<Axis name="1" />
<Axis
name="value"
line={null}
label={null}
subTickLine={null}
tickLine={null}
grid={null}
/>
<Point
position="value*1"
color="#1890FF"
shape="pointer"
/>
<Annotation.Arc
start={[0, 1]}
end={[1, 1]}
style={{
stroke: '#1fBF87',
lineWidth: 18,
lineDash: null,
}}
/>
<Annotation.Arc
start={[0.25, 1]}
end={[1, 1]}
style={{
stroke: '#A0D911',
lineWidth: 18,
lineDash: null,
}}
/>
<Annotation.Arc
start={[0.5, 1]}
end={[1, 1]}
style={{
stroke: '#F7A128',
lineWidth: 18,
lineDash: null,
}}
/>
<Annotation.Arc
start={[0.75, 1]}
end={[1, 1]}
style={{
stroke: '#e05a55',
lineWidth: 18,
lineDash: null,
}}
/>
</Chart>
)
}
export default AnnotationArc;
import React, { useMemo } from 'react';
import AnnotationArc from './annotationArc';
import CustomizeCard from '../CustomizeCard';
import styles from './index.less'
const DashboardContainer = () => {
return (
<CustomizeCard title="单笔订单金额超出单次合作金额" bodyStyle={{height: '312px', padding: '0'}}>
<div className={styles.section}>
<AnnotationArc />
<div className={styles.tips}>
<p className={styles.text}>超出金额</p>
<p className={styles.money}>69万</p>
<p className={styles.level}>一级风险</p>
</div>
</div>
</CustomizeCard>
)
}
export default DashboardContainer
.section {
position: relative;
.tips {
position: absolute;
bottom: -40px;
left: 50%;
margin-left: -36px;
text-align: center;
}
.text {
margin-bottom: 8px;
font-size: 12px;
color: #91959b;
}
.money {
font-size: 32PX;
line-height: 32PX;
margin-bottom: 8px;
color: #e05a55;
}
.level {
color: #e05a55;
font-size: 12px;
margin-bottom: 0;
}
}
import Container from './container';
export default Container
.card {
:global {
.ant-card-head {
.ant-card-head-title {
font-size: 16px;
font-weight: 600;
line-height: 16px;
color: #252d37;
}
}
}
}
import React, { useMemo } from 'react';
import { Card } from 'antd';
import styles from './index.less';
type PropsNew = React.ComponentProps<typeof Card>
const CustomizeCard: React.FC<PropsNew> = (props: PropsNew) => {
const { title, loading, extra, children, ...rest } = props;
return (
<div className={styles.card}>
<Card
bordered={false}
title={title}
loading={loading}
extra={extra}
{...rest}
>
{children}
</Card>
</div>
)
}
CustomizeCard.defaultProps = {
loading: false,
extra: null
}
export default CustomizeCard
......@@ -4,12 +4,20 @@
flex-wrap: wrap;
.item {
padding: 8px;
padding: 6px 8px;
font-size: 12px;
line-height: 12px;
border: 1px solid #E3E4E5;
color: #5C626A;
text-align: center;
margin-right: 8px;
margin-bottom: 8PX;
cursor: pointer;
}
.active {
// background-color: @primary-color;
color: @primary-color;
border-color: @primary-color;
}
}
import React from 'react';
import React, { useState } from 'react';
import styles from './index.less';
import cx from 'classnames';
type Options = {
export type Options = {
label: string,
value: string | number,
} & {
[key: string]: any
}
interface Iprops {
options: Options,
options: Options[],
value?: string | number,
onChange?: () => void
onChange?: ((_item: Options) => void) | null
}
const CustomizeRadio: React.FC<Iprops> = (props: Iprops) => {
const { options, value, onChange } = props;
const [activeKey, setActiveKey] = useState<Options['value']>(null)
const handleCheck = (_item: Options) => {
console.log(_item);
if (!("value" in props)) {
setActiveKey(_item.value)
}
onChange?.(_item);
}
return (
<div className={styles.container}>
{
[1,2,3,4].map((_item) => {
options.map((_item) => {
const _itemClassName = cx(styles.item, {
[styles.active]: activeKey === _item.value
})
return (
<div className={styles.item} key={_item}>
全部(6)
<div className={_itemClassName} key={_item.value} onClick={() => handleCheck(_item)}>
{`${_item.label}(${_item.count})`}
</div>
)
})
......
import React from 'react';
import {
Chart,
Interval,
Tooltip,
Axis,
Coordinate,
Interaction,
Legend,
// getTheme
} from 'bizcharts';
function Labelline() {
const data = [
{ item: '事例一', count: 40, percent: 0.4 },
{ item: '事例二', count: 21, percent: 0.21 },
{ item: '事例三', count: 17, percent: 0.17 },
{ item: '事例四', count: 13, percent: 0.13 },
{ item: '事例五', count: 9, percent: 0.09 },
];
const cols = {
percent: {
formatter: val => {
val = val * 100 + '%';
return val;
},
},
};
return (
<Chart height={300} data={data} scale={cols} autoFit>
<Legend visible={false} /> /
<Coordinate type="theta" radius={0.75} />
<Tooltip showTitle={false} />
<Axis visible={false} />
<Interval
position="percent"
adjust="stack"
color="item"
style={{
lineWidth: 1,
stroke: '#fff',
}}
label={['count', {
content: (data) => {
return `${data.item}: ${data.percent * 100}%`;
},
}]}
state={{
selected: {
style: (t) => {
// const res = getTheme().geometries.interval.rect.selected.style(t);
return { fill: 'red' }
}
}
}}
/>
<Interaction type='element-single-selected' />
</Chart>
);
}
export default Labelline
import React, { useMemo } from 'react';
import CustomizeCard from '../CustomizeCard';
import StatusLabel from '../StatusLabel';
import styles from './index.less'
import Chart from './Chart'
const MemberEvaluateScoreContainer = () => {
const options = [
{ label: '非常满意' },
{ label: '满意' },
{ label: '一般' },
{ label: '不满意' },
{ label: '非常不满意' },
]
return (
<CustomizeCard
title="会员考评分值"
bodyStyle={{height: '312px', padding: '0'}}
extra={
<StatusLabel options={options}/>
}
>
<Chart />
</CustomizeCard>
)
}
export default MemberEvaluateScoreContainer
......@@ -4,7 +4,8 @@ import OverViewCard from './OverViewCard';
const OverView = () => {
return (
<Row gutter={[16, 16]} justify="space-between">
// <Row gutter={[16, 16]} justify="space-between">
<>
<Col span={6}>
<OverViewCard type="warn" title={"今日预警"} total={12} first={4} second={3} third={5}/>
</Col>
......@@ -17,7 +18,8 @@ const OverView = () => {
<Col span={6}>
<OverViewCard type="default" title={"今日预警"} total={12} first={4} second={3} third={5}/>
</Col>
</Row>
</>
// </Row>
)
}
......
import StatusTag, { StatusTagProps } from '@/components/StatusTag';
import React from 'react';
import RecordList from "./recordList";
import RecordItem from "./recordItem";
interface Iprops {
type?: Pick<StatusTagProps, "type">['type'],
alert: string,
content: string,
export {
RecordItem,
RecordList
}
const Record: React.FC<Iprops> = (props: Iprops) => {
const { type, alert, content } = props;
return (
<div>
<StatusTag type={type} title={alert} />
<p>{content}</p>
</div>
)
}
Record.defaultProps = {
type: 'primary'
}
export default Record
import StatusTag, { StatusTagProps } from '@/components/StatusTag';
import React from 'react';
interface Iprops {
type?: Pick<StatusTagProps, "type">['type'],
alert: string,
content: string,
}
const RecordItem: React.FC<Iprops> = (props: Iprops) => {
const { type, alert, content } = props;
return (
<div>
<StatusTag type={type} title={alert} />
<p style={{margin: '0'}}>{content}</p>
</div>
)
}
RecordItem.defaultProps = {
type: 'primary'
}
export default RecordItem
import React from 'react';
import RecordItem from './recordItem';
const RecordList = () => {
return (
<div>
{
[1,2,3,4,5,6,7,8,9,10,11].map((_item) => {
return (
<div key={_item} style={{marginBottom: '24px'}}>
<RecordItem type="danger" alert="合同到期" content="江南皮革厂倒闭了" />
</div>
)
})
}
</div>
)
}
export default RecordList;
.container {
display: flex;
.item {
color: #5c626a;
font-size: 12px;
}
}
.row {
flex-direction: row;
align-items: center;
.item {
margin-right: @margin-md;
}
}
.column {
flex-direction: column;
}
.circle {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: @margin-sm;
}
.square {
width: 8px;
height: 8px;
background-color: red;
display: inline-block;
margin-right: @margin-sm;
}
import React from 'react'
import styles from './index.less';
import cx from 'classnames';
type Options = {
label: string,
color?: string | null,
}
interface Iprops {
direction?: 'row' | 'column'
type?: 'circle' | 'square',
options: Options[]
}
const colors = ["#1fbf87", "#4b8bfa", "#5d7092", "#f7a12b", "#e05a55", "#fff", "#000"]
const StatusLabel: React.FC<Iprops> = (props: Iprops) => {
const { direction, type, options } = props;
const containerCx = cx(styles.container, styles[direction])
return (
<div className={containerCx}>
{
options.map((_item, index) => {
return (
<div className={styles.item}>
<span className={cx(styles[type])} style={_item.color ? {background: _item.color} : {background: colors[index] || '#fff'}}></span>
<span>{_item.label}</span>
</div>
)
})
}
{/* <div className={styles.item}>
<span className={cx(styles[type])}></span>
<span>满意</span>
</div> */}
</div>
)
}
StatusLabel.defaultProps = {
direction: "row",
type: 'square'
}
export default StatusLabel
......@@ -8,7 +8,7 @@
flex-direction: column;
width: 504px;
padding: 0 @padding-md;
border-right: 1px solid red;
border-right: 1px solid #f4f5f7;
.header {
padding: @padding-md 0;
......@@ -22,13 +22,14 @@
.chart {
width: 100%;
padding-bottom: 16px;
height: 408px;
// padding-bottom: 16px;
.selectData {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
margin-bottom: 24px;
.province {
color: #91959B;
font-size: 12;
......@@ -48,5 +49,11 @@
// margin: 0 -8px;
margin-right: -16px;
flex: 1;
.recordList {
margin-top: 8px;
height: 384px;
overflow-y: scroll;
}
}
}
import styles from './index.less'
import React, { useState, useEffect } from 'react';
import { Card } from 'antd';
import {
Chart,
Tooltip,
Interaction,
} from 'bizcharts';
import Coord from 'bizcharts/lib/components/Coordinate';
import Geom from 'bizcharts/lib/geometry'
import * as turf from '@turf/turf';
import DataSet from '@antv/data-set';
import CustomizeRadio from '../CustomizeRadio'
function keepMapRatio(mapData, c, type) {
if (mapData && turf) {
// 获取数据外接矩形,计算宽高比
const bbox = turf.bbox(mapData);
const width = bbox[2] - bbox[0];
const height = bbox[3] - bbox[1];
const ratio = height / width;
const cWidth = c.width;
const cHeight = c.height;
const cRatio = cHeight / cWidth;
let scale = {};
if (cRatio >= ratio) {
const halfDisRatio = (cRatio - ratio) / 2 / cRatio;
scale = {
x: {
range: [0, 1],
},
y: {
range: [halfDisRatio, 1 - halfDisRatio],
},
};
} else {
const halfDisRatio = ((1 / cRatio - 1 / ratio) / 2) * cRatio;
scale = {
y: {
range: [0, 1],
},
x: {
range: [halfDisRatio, 1 - halfDisRatio],
},
};
}
const curScaleXRange = c.getScaleByField('x').range;
const curScaleYRange = c.getScaleByField('y').range;
console.log(curScaleYRange, scale.y.range);
if (
curScaleXRange[0] !== scale.x.range[0] ||
curScaleXRange[1] !== scale.x.range[1] ||
curScaleYRange[0] !== scale.y.range[0] ||
curScaleYRange[1] !== scale.y.range[1]
) {
setTimeout(() => {
c.scale(scale);
c.render(true);
}, 1);
}
}
}
const colors = '#075A84,#3978A4,#6497C0,#91B6D7,#C0D6EA,#F2F7F8'
.split(',')
.reverse();
import MapChart from './mapChart';
import CustomizeRadio, { Options } from '../CustomizeRadio'
import { RecordList } from '../Record';
const WarningArea = () => {
const [mapData, setMapData] = useState(undefined)
const [warningOptions, setWarningOptions] = useState<Options[]>([
{
label: '全部',
dataIndex: 'all',
value: 0,
count: 6
},
{
label: '一级',
dataIndex: 'first',
value: 1,
count: 6
},
{
label: '二级',
dataIndex: 'second',
value: 2,
count: 6
},
{
label: '三级',
dataIndex: 'second',
value: 3,
count: 6
}
]);
useEffect(() => {
const dataUrl =
'https://gw.alipayobjects.com/os/bmw-prod/d4652bc5-e971-4bca-a48c-5d8ad10b3d91.json';
......@@ -96,34 +56,6 @@ const WarningArea = () => {
});
}, []);
let bgView;
let interval;
let min = 0;
if (mapData) {
// data set
const ds = new DataSet();
// draw the map
const dv = ds
.createView('back')
.source(mapData, {
type: 'GeoJSON',
})
.transform({
type: 'geo.projection',
projection: 'geoMercator',
as: ['x', 'y', 'centroidX', 'centroidY'],
});
bgView = new DataSet.View().source(dv.rows);
}
const scale = {
x: { sync: true },
y: { sync: true },
};
return (
<Card bodyStyle={{padding: 0}}>
<div className={styles.card}>
......@@ -136,54 +68,14 @@ const WarningArea = () => {
<span className={styles.province}>广东省</span>
<span className={styles.count}>12</span>
</div>
<div style={{ width: '472px', background: '#FAFBFC'}}>
<Chart
// 清空默认的坐标轴legend组件
pure
height={336}
scale={scale}
// 不支持dataSet数据格式了
data={bgView ? bgView.rows : bgView}
autoFit
placeholder={<div>Loading</div>}
padding={[16, 0]}
onAfterRender={(e, c) => {
keepMapRatio(mapData, c, "rerender")
}}
>
<Coord reflect="y" />
<Tooltip title="name" />
<Geom
type="polygon"
position="x*y"
color={['centroidY', '#777090-#493398']}
tooltip={[
'name*properties',
(t, p) => {
return {
//自定义 tooltip 上显示的 title 显示内容等。
name: 'Size',
title: t,
value: p.size,
};
},
]}
state={{
selected: {
style: (t) => {
return { fill: 'purple', stroke: '#ccc',lineWidth:1 }
}
}
}}
/>
<Interaction type='element-single-selected' />
</Chart>
</div>
<MapChart mapData={mapData} />
</div>
</div>
<div className={styles.section}>
<CustomizeRadio />
<CustomizeRadio options={warningOptions} />
<div className={styles.recordList}>
<RecordList />
</div>
</div>
</div>
</Card>
......
import React, { useState } from 'react';
import {
Chart,
Tooltip,
Interaction,
} from 'bizcharts';
import Coord from 'bizcharts/lib/components/Coordinate';
import Geom from 'bizcharts/lib/geometry'
import * as turf from '@turf/turf';
import DataSet from '@antv/data-set';
interface Iprops {
mapData: any,
}
function keepMapRatio(mapData, c, type) {
if (mapData && turf) {
// 获取数据外接矩形,计算宽高比
const bbox = turf.bbox(mapData);
const width = bbox[2] - bbox[0];
const height = bbox[3] - bbox[1];
const ratio = height / width;
const cWidth = c.width;
const cHeight = c.height;
const cRatio = cHeight / cWidth;
let scale = {};
if (cRatio >= ratio) {
const halfDisRatio = (cRatio - ratio) / 2 / cRatio;
scale = {
x: {
range: [0, 1],
},
y: {
range: [halfDisRatio, 1 - halfDisRatio],
},
};
} else {
const halfDisRatio = ((1 / cRatio - 1 / ratio) / 2) * cRatio;
scale = {
y: {
range: [0, 1],
},
x: {
range: [halfDisRatio, 1 - halfDisRatio],
},
};
}
const curScaleXRange = c.getScaleByField('x').range;
const curScaleYRange = c.getScaleByField('y').range;
console.log(curScaleYRange, scale.y.range);
if (
curScaleXRange[0] !== scale.x.range[0] ||
curScaleXRange[1] !== scale.x.range[1] ||
curScaleYRange[0] !== scale.y.range[0] ||
curScaleYRange[1] !== scale.y.range[1]
) {
setTimeout(() => {
c.scale(scale);
c.render(true);
}, 1);
}
}
}
const colors = '#075A84,#3978A4,#6497C0,#91B6D7,#C0D6EA,#F2F7F8'
.split(',')
.reverse();
const MapChart: React.FC<Iprops> = (props: Iprops) => {
// const [mapData, setMapData] = useState(undefined)
const { mapData } = props;
let bgView;
let interval;
let min = 0;
if (mapData) {
// data set
const ds = new DataSet();
// draw the map
const dv = ds
.createView('back')
.source(mapData, {
type: 'GeoJSON',
})
.transform({
type: 'geo.projection',
projection: 'geoMercator',
as: ['x', 'y', 'centroidX', 'centroidY'],
});
bgView = new DataSet.View().source(dv.rows);
}
const scale = {
x: { sync: true },
y: { sync: true },
};
return (
<div style={{ background: '#FAFBFC'}}>
<Chart
// 清空默认的坐标轴legend组件
pure
height={336}
scale={scale}
// 不支持dataSet数据格式了
data={bgView ? bgView.rows : bgView}
autoFit
placeholder={<div>Loading</div>}
padding={[0, 0]}
onAfterRender={(e, c) => {
keepMapRatio(mapData, c, "rerender")
}}
>
<Coord reflect="y" />
<Tooltip title="name" />
<Geom
type="polygon"
position="x*y"
color={['centroidY', '#777090-#493398']}
tooltip={[
'name*properties',
(t, p) => {
return {
//自定义 tooltip 上显示的 title 显示内容等。
name: 'Size',
title: t,
value: p.size,
};
},
]}
state={{
selected: {
style: (t) => {
return { fill: 'purple', stroke: '#ccc',lineWidth:1 }
}
}
}}
/>
<Interaction type='element-single-selected' />
</Chart>
</div>
)
}
export default MapChart;
......@@ -2,27 +2,33 @@ import React from 'react';
import { Row, Col, Card } from 'antd';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import { OverView } from './components/OverVIew';
import Record from './components/Record';
import { RecordList } from './components/Record';
import WarningArea from './components/WarningArea';
import ProjectItem from './components/ProjectItem';
// import AnnotationArc from './components/AnnotationArc';
import CustomizeCard from './components/CustomizeCard';
import AnnotationArc from './components/AnnotationArc';
import MemberEvaluateScoreContainer from './components/MemberEvaluateScore/container';
const Dashboard = () => {
return (
<PageHeaderWrapper
title={'预警工作台'}
>
<OverView />
<Row gutter={[16,16]}>
<OverView />
<Col span={6}>
<Card title="今日预警记录">
<Record alert={"合同到期"} content="江南皮革厂倒闭了" />
</Card>
<CustomizeCard title="今日预警记录" bodyStyle={{padding: '0'}} >
<div style={{height: '384px', overflowY: 'scroll', margin: '8px 0 16px 16px'}}>
<RecordList />
</div>
</CustomizeCard>
</Col>
<Col span={12}>
<WarningArea />
</Col>
<Col span={6}>
<Card title="预警项目">
<CustomizeCard title="预警项目" headStyle={{color: 'red'}} bodyStyle={{height: '408px'}}>
<Row gutter={[16, 16]}>
<Col span={12}>
<ProjectItem />
......@@ -37,7 +43,13 @@ const Dashboard = () => {
<ProjectItem />
</Col>
</Row>
</Card>
</CustomizeCard>
</Col>
<Col span={8}>
<AnnotationArc />
</Col>
<Col span={8}>
<MemberEvaluateScoreContainer />
</Col>
</Row>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment