Commit 0ff8a5ba authored by Bill's avatar Bill

feaat: 会员预警

parent ad313276
......@@ -759,6 +759,11 @@ const MemberRoute: RouterChild = {
name: '会员预警',
routes: [
{
path: '/memberCenter/memberAbility/memberWarning/dashboard',
name: '预警工作台',
component: '@/pages/member/memberWarning/dashboard'
},
{
path: '/memberCenter/memberAbility/memberWarning/query',
name: '预警查询',
component: '@/pages/member/memberWarning/warningQuery'
......
......@@ -55,6 +55,7 @@
"@ctrl/tinycolor": "^3.4.0",
"@formily/antd": "^1.3.3",
"@formily/antd-components": "^1.3.3",
"@turf/turf": "^6.4.0",
"@types/crypto-js": "^4.0.1",
"@umijs/hooks": "^1.9.3",
"@umijs/plugin-esbuild": "^1.0.1",
......@@ -63,7 +64,7 @@
"antd-img-crop": "^3.12.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"bignumber.js": "^9.0.1",
"bizcharts": "^4.0.14",
"bizcharts": "^4.1.10",
"copy-to-clipboard": "^3.3.1",
"crypto-js": "^4.0.0",
"god": "^0.2.9",
......
......@@ -118,7 +118,7 @@ const useGetAuth = () => {
/**
* 只要当前能力拥有他们其中一个准入路由,那么就代表有权限,
* @review 这里是有问题的,不应该对某个中心做判断,应该在home 进来的时候就
* @review 这里是否有最优解 不应该对某个中心做判断,应该在home 进来的时候就获取所有layout的权限,但貌似时间复杂度是一样的
*/
const hasAbility = useCallback((abilityName: AbilityNameType) => {
/** 这里本来想写正则的, 可是没想到好的方案, 之前直接判断模块前缀的话,没有添加子集菜单同样也没有权限 */
......
......@@ -197,7 +197,7 @@ export const memberSchema: ISchema = {
type: 'string',
'x-component': 'Search',
'x-component-props': {
placeholder: '搜索会员名称1',
placeholder: '搜索会员名称',
tip: '输入 会员名称 进行搜索',
advanced: false,
},
......
import { ColumnsType } from 'antd/es/table';
/**
* 列表页column
*/
const listColumns: ColumnsType<any> = [
{
title: '序号',
dataIndex: 'no',
},
{
title: '预警条件',
dataIndex: 'memberName',
},
{
title: '预警提示语',
dataIndex: 'project',
},
{
title: '预警提示语',
dataIndex: 'notice',
},
]
export default listColumns;
......@@ -60,4 +60,47 @@ const querySchema: ISchema = {
},
},
};
export const handleFormSchema: ISchema = {
type: 'object',
properties: {
layout: {
type: 'string',
"x-component": 'mega-layout',
"x-component-props": {
labelCol: 4,
wrapperCol: 20,
labelAlign: 'left'
},
properties: {
name: {
title: '会员名称',
type: 'string'
},
project: {
title: '预警项目',
type: 'string'
},
tips: {
title: '预警提示语',
type: 'string'
},
date: {
title: '预警日期',
type: 'string'
},
level: {
title: '预警级别',
type: 'string'
},
solution: {
title: '处理方案',
type: 'textarea'
}
}
}
}
}
export default querySchema
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
.item {
padding: 8px;
font-size: 12px;
line-height: 12px;
border: 1px solid #E3E4E5;
color: #5C626A;
text-align: center;
margin-right: 8px;
}
}
import React from 'react';
import styles from './index.less';
type Options = {
label: string,
value: string | number,
}
interface Iprops {
options: Options,
value?: string | number,
onChange?: () => void
}
const CustomizeRadio: React.FC<Iprops> = (props: Iprops) => {
const { options, value, onChange } = props;
return (
<div className={styles.container}>
{
[1,2,3,4].map((_item) => {
return (
<div className={styles.item} key={_item}>
全部(6)
</div>
)
})
}
</div>
)
}
export default CustomizeRadio
import React from 'react';
import { Row, Col } from 'antd';
import OverViewCard from './OverViewCard';
const OverView = () => {
return (
<Row gutter={[16, 16]} justify="space-between">
<Col span={6}>
<OverViewCard type="warn" title={"今日预警"} total={12} first={4} second={3} third={5}/>
</Col>
<Col span={6}>
<OverViewCard type="primary" title={"今日预警"} total={12} first={4} second={3} third={5}/>
</Col>
<Col span={6}>
<OverViewCard type="success" title={"今日预警"} total={12} first={4} second={3} third={5}/>
</Col>
<Col span={6}>
<OverViewCard type="default" title={"今日预警"} total={12} first={4} second={3} third={5}/>
</Col>
</Row>
)
}
export default OverView
import React from 'react';
import { Card } from 'antd';
import styles from './index.less';
import cx from 'classnames';
interface Iprops {
icon?: any,
title: string,
total: number,
/** 一级 */
first: number,
second: number,
third: number,
loading?: boolean,
type: "warn" | "primary" | "success" | 'default',
}
const OverViewCard: React.FC<Iprops> = (props: Iprops) => {
const { loading, title, total, first, second, third, type } = props;
return (
<Card title={title} loading={loading}>
<div className={styles.section}>
<div className={cx(styles.icon, styles[type])}></div>
<span className={styles.total}>{total}</span>
</div>
<div className={styles.progress}>
<div className={cx(styles.first, styles.progressItem)} style={{flex: first}}>{first}</div>
<div className={cx(styles.second, styles.progressItem)} style={{flex: second}}>{second}</div>
<div className={cx(styles.third, styles.progressItem)} style={{flex: third}}>{third}</div>
</div>
</Card>
)
}
export default OverViewCard;
.section {
display: flex;
flex-direction: row;
align-items: center;
.icon {
width: 48px;
height: 48px;
border-radius: 50%;
}
.warn {
background-color: #FFF8EB;
}
.primary {
background-color: #E9F3FF;
}
.success {
background-color: #DAF3EE;
}
.default {
background-color: #F4F5F7;
}
.total {
margin-left: @margin-md;
font-size: 24px;
color: #303133;
}
}
.progress {
display: flex;
flex-direction: row;
margin-top: 16px;
.progressItem {
height: 24px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&::after {
content: "";
width: 100%;
height: 2px;
}
}
.first {
color: #D32F2F;
&::after {
background-color: #D32F2F;
}
}
.second {
color: #EA8000;
&::after {
background-color: #EA8000;
}
}
.third {
color: #2266EE;
&::after {
background-color: #2266EE;
}
}
}
// import OverViewCard from "./OverViewCard";
import OverView from './OverView';
export {
OverView,
// OverViewCard
}
.container {
padding: 12px 0;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
background: #F7F8FA;
height: 64px;
.count {
font-size: 16px;
color: #1f2c3d;
}
.name {
color: #91959B;
font-size: 12px;
}
&:hover {
background-color: #EBEDF0;
cursor: pointer;
}
}
import React from 'react';
import styles from './index.less';
const ProjectItem = () => {
return (
<div className={styles.container}>
<span className={styles.count}>3</span>
<span className={styles.name}>采购合同到期</span>
</div>
)
}
export default ProjectItem
import StatusTag, { StatusTagProps } from '@/components/StatusTag';
import React from 'react';
interface Iprops {
type?: Pick<StatusTagProps, "type">['type'],
alert: string,
content: string,
}
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
.card {
display: flex;
flex-direction: row;
// padding: 0 @padding-md;
.chartContainer {
display: flex;
flex-direction: column;
width: 504px;
padding: 0 @padding-md;
border-right: 1px solid red;
.header {
padding: @padding-md 0;
.title {
font-size: 16px;
color: #252D37;
line-height: 16px;
}
}
.chart {
width: 100%;
padding-bottom: 16px;
.selectData {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
.province {
color: #91959B;
font-size: 12;
}
.count {
margin-left: 58px;
color: #303133;
font-size: 24px;
line-height: 24px;
}
}
}
}
.section {
padding: @padding-md;
// margin: 0 -8px;
margin-right: -16px;
flex: 1;
}
}
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();
const WarningArea = () => {
const [mapData, setMapData] = useState(undefined)
useEffect(() => {
const dataUrl =
'https://gw.alipayobjects.com/os/bmw-prod/d4652bc5-e971-4bca-a48c-5d8ad10b3d91.json';
fetch(dataUrl)
.then(res => res.json())
.then(d => {
console.log(d);
const feas = d.features.filter(feat => feat.properties.name).map(v => {
return {
...v,
properties: {
...v.properties,
size: Math.floor(Math.random() * 300),
},
};
});
const res = { ...d, features: feas };
setMapData(res)
});
}, []);
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}>
<div className={styles.chartContainer}>
<header className={styles.header}>
<div className={styles.title}>预警区域</div>
</header>
<div className={styles.chart}>
<div className={styles.selectData}>
<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>
</div>
</div>
<div className={styles.section}>
<CustomizeRadio />
</div>
</div>
</Card>
)
}
export default WarningArea
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 WarningArea from './components/WarningArea';
import ProjectItem from './components/ProjectItem';
const Dashboard = () => {
return (
<PageHeaderWrapper
title={'预警工作台'}
>
<OverView />
<Row gutter={[16,16]}>
<Col span={6}>
<Card title="今日预警记录">
<Record alert={"合同到期"} content="江南皮革厂倒闭了" />
</Card>
</Col>
<Col span={12}>
<WarningArea />
</Col>
<Col span={6}>
<Card title="预警项目">
<Row gutter={[16, 16]}>
<Col span={12}>
<ProjectItem />
</Col>
<Col span={12}>
<ProjectItem />
</Col>
<Col span={12}>
<ProjectItem />
</Col>
<Col span={12}>
<ProjectItem />
</Col>
</Row>
</Card>
</Col>
</Row>
</PageHeaderWrapper>
)
}
export default Dashboard;
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import { Card, Input, Select, Table } from 'antd';
import ruleColumns from '../common/columns/ruleColumns';
import { PublicApi } from '@/services/api';
import { ColumnsType } from 'antd/es/table';
import { useDebounceFn } from '@umijs/hooks';
......
import React from 'react';
import { Card, Space, Button } from 'antd'
import React, { useState } from 'react';
import { Card, Space, Button, Drawer } from 'antd'
import tobeHandleColumns from '../common/columns/tobeHandleColumns';
import querySchema from '../common/schema/tobeHandleQuerySchema';
import querySchema, { handleFormSchema } from '../common/schema/tobeHandleQuerySchema';
import useFetchList from '../../memberEvaluate/hooks/useFetchList';
import { FORM_FILTER_PATH } from '@/formSchema/const';
import { useStateFilterSearchLinkageEffect } from '@/formSchema/effects/useFilterSearch';
......@@ -9,24 +9,45 @@ import { PublicApi } from '@/services/api';
import CustomizeQueryList from '../../components/CustomizeQueryList';
import { Link } from 'umi';
import useColumns from '../../memberRectification/common/hooks/useColumns';
import NiceForm from '@/components/NiceForm';
import { createFormActions } from '@formily/antd';
import { useToggle } from '@umijs/hooks';
interface Iprops {};
type submitType = {
name: string,
project: string,
tips: string,
date: string,
level: string
solution: string,
}
const actions = createFormActions()
const List: React.FC<Iprops> = (props: Iprops) => {
const { fetchListData } = useFetchList();
const { state: visible, toggle } = useToggle();
const [recordData, setRecordData] = useState<null>(null)
const { columns } = useColumns(tobeHandleColumns, [
{
title: '操作',
render: (text, record, index) => {
return (
<div>
<a>处理</a>
<a onClick={() => handleClick(record)}>处理</a>
</div>
)
}
}
])
const handleClick = (record: any) => {
setRecordData(record);
toggle(true)
}
const handleFetch = async (params) => {
// const result = fetchListData(PublicApi.getMemberAbilitySubPage, params);
return {
......@@ -35,17 +56,48 @@ const List: React.FC<Iprops> = (props: Iprops) => {
}
}
const onSubmit = (value: submitType) => {
console.log(value);
toggle(false)
}
return (
<Card>
<CustomizeQueryList
columns={columns}
schema={querySchema}
fetchListData={handleFetch}
effects={($, actions) => {
useStateFilterSearchLinkageEffect($, actions, 'name', FORM_FILTER_PATH);
}}
/>
</Card>
<>
<Card>
<CustomizeQueryList
columns={columns}
schema={querySchema}
fetchListData={handleFetch}
effects={($, actions) => {
useStateFilterSearchLinkageEffect($, actions, 'name', FORM_FILTER_PATH);
}}
/>
</Card>
<Drawer
visible={visible}
title="预警处理"
width={600}
onClose={() => toggle(false)}
footer={
<div
style={{
textAlign: 'right',
}}
>
<Space align="end">
<Button onClick={() => toggle(false)}>取消</Button>
<Button onClick={() => actions.submit()} type="primary">确定</Button>
</Space>
</div>
}
>
<NiceForm
schema={handleFormSchema}
actions={actions}
/>
</Drawer>
</>
)
}
export default List
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