黄婧婧 發表於 2022-4-30 20:38:00

react实战系列 —— 我的仪表盘(bizcharts、antd、moment)

<blockquote>
<p>其他章节请看:</p>
<p>react实战 系列</p>
</blockquote>
<h2 id="my-dashboard">My Dashboard</h2>
<p>上一篇我们在 spug 项目中模仿”任务计划“模块实现一个类似的一级导航页面(”My任务计划“),本篇,我们将模仿“Dashboard”来实现一个仪表盘“My Dashboard”。</p>
<p>主要涉及 antd 的 <code>Grid</code>、<code>Card</code>、<code>Descriptions</code>等组件、<code>bizcharts</code> 的使用、<code>moment</code> 日期库和页面适配。</p>
<p><em>注</em>:实现的代码在上一篇的基础上展开。</p>
<h3 id="dashboard">Dashboard</h3>
<p>界面如下:<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094855_mydashboard-1.png"></p>
<p>里面用到了:</p>
<ul>
<li>antd 的 <code>Grid</code>、<code>Card</code>、<code>Descriptions</code> 描述列表 (文字长度不同,有时会感觉没对齐)</li>
<li><code>bizcharts</code> 中的折线图、柱状图</li>
<li><code>moment</code>(日期相关的库),比如按天、按月、最近 30 天都很方便</li>
</ul>
<h3 id="my-dashboard-1">My Dashboard</h3>
<h4 id="最终效果">最终效果</h4>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094859_mydashboard-2.png"></p>
<p>无需权限即可访问:<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094903_mydashboard-3.png"></p>
<p>全屏效果:<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094907_mydashboard-4.png"></p>
<h3 id="实现的代码">实现的代码</h3>
<h4 id="安装两个依赖包">安装两个依赖包:</h4>
<ul>
<li><code>@antv/data-set</code>,柱状图和饼状图需要使用</li>
<li><code>bx-tooltip</code>,自定义 bizcharts 中的 tooltip。折线图和柱状图的 tooltip 都使用了。</li>
</ul>
<pre><code class="language-javascript">spug-study&gt; npm i @antv/data-set

added 31 packages, and audited 1820 packages in 26s

107 packages are looking for funding
run `npm fund` for details

33 vulnerabilities (1 low, 16 moderate, 15 high, 1 critical)

To address issues that do not require attention, run:      
npm audit fix

To address all issues (including breaking changes), run:   
npm audit fix --force

Run `npm audit` for details.
</code></pre>
<pre><code class="language-javascript">spug-study&gt; npm i -D bx-tooltip

added 1 package, and audited 1821 packages in 9s

107 packages are looking for funding
run `npm fund` for details

33 vulnerabilities (1 low, 16 moderate, 15 high, 1 critical)

To address issues that do not require attention, run:
npm audit fix

To address all issues (including breaking changes), run:
npm audit fix --force

Run `npm audit` for details.
</code></pre>
<p>package.json 变动如下:</p>
<pre><code class="language-javascript">"dependencies": {
"@antv/data-set": "^0.11.8",
}
"devDependencies": {
"bx-tooltip": "^0.1.6",
}
</code></pre>
<h4 id="增强表格组件">增强表格组件</h4>
<p>spug 中封装的表格组件,不支持 style和 size。替换一行,以及增加一行:</p>
<pre><code class="language-javascript">// src/components/TableCard.js

- &lt;div ref={rootRef} className={styles.tableCard}&gt;
+ &lt;div ref={rootRef} className={styles.tableCard} style={{...props.customStyles}}&gt;

&lt;Table
+ size={props.size}
</code></pre>
<h4 id="准备-mock-数据">准备 mock 数据</h4>
<p>将 mydashboard 模块的的 mock 专门放入一个文件,并在 <code>mock/index.js</code> 中引入。</p>
<pre><code class="language-javascript">// src\mock\index.js

+ import './mydashboard'

</code></pre>
<pre><code class="language-javascript">// src\mock\mydashboard.js

import Mock from 'mockjs'

// 开发环境引入 mock
if (process.env.NODE_ENV === 'development') {
   
Mock.mock('/api/mdashboard/occupancy_rate/', 'get', () =&gt; (
    {"data": [ {
      month: "2022-01-01",
      city: "城市-名字很长很长很长",
      happiness: 10,
      per: 90,
      msg1: '信息xxx'
    },
    {
      month: "2022-01-01",
      city: "城市B",
      per: 30,
      happiness: 50,
      msg1: '信息xxx'
    },
    {
      month: "2022-02-01",
      city: "城市-名字很长很长很长",
      happiness: 20,
      per: 40,
      msg1: '信息xxx'
    },
   
    {
      month: "2022-02-01",
      city: "城市B",
      happiness: 20,
      per: 60,
      msg1: '信息xxx'
    },
    {
      month: "2022-03-01",
      city: "城市-名字很长很长很长",
      happiness: 30,
      per: 80,
      msg1: '信息xxx'
    },], "error": ""}

))

let mIdSeed = 1;
Mock.mock('/api/mdashboard/table', 'get', () =&gt; ({
    "data": [{ "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
    { "id": mIdSeed++, "name": "苹果" + mIdSeed, address: '场地' +mIdSeed, time: new Date().toLocaleTimeString() },
]
}))
}
</code></pre>
<h4 id="路由配置">路由配置</h4>
<p>配置 <code>/mdashboard</code> 和 <code>/mydashboard</code> 两个路由:</p>
<pre><code class="language-javascript">// src\App.js

+ import MDashboard from './pages/mdashboard/tIndex';

class App extends Component {
render() {
    return (
      &lt;Switch&gt;
      //无需权限
      + &lt;Route path="/mdashboard" exact component={MDashboard} /&gt;
      &lt;Route path="/" exact component={Login} /&gt;
      &lt;Route path="/ssh" exact component={WebSSH} /&gt;
      &lt;Route component={Layout} /&gt;
      &lt;/Switch&gt;
    );
}
}
</code></pre>
<pre><code class="language-javascript">// src\routes.js

+ import MyDashboardIndex from './pages/mdashboard';

export default [
{icon: &lt;DesktopOutlined/&gt;, title: '工作台', path: '/home', component: HomeIndex},
{
    icon: &lt;DashboardOutlined/&gt;,
    title: 'Dashboard',
    auth: 'dashboard.dashboard.view',
    path: '/dashboard',
    component: DashboardIndex
},
+ // 我的仪表盘
+ {
+   icon: &lt;DashboardOutlined /&gt;,
+   title: 'MyDashboard',
+   auth: 'mydashboard.mydashboard.view',
+   path: '/mydashboard',
+   component: MyDashboardIndex
+ },
</code></pre>
<p>新建仪表盘组件。一个需要权限访问,另一个无需权限即可访问,故将仪表盘提取成一个单独的文件:</p>
<pre><code class="language-javascript">// src\pages\mdashboard\Dashboard.js

import React from 'react';
export default function () {
return (
    &lt;div&gt;仪表盘&lt;/div&gt;
)
}
</code></pre>
<pre><code class="language-javascript">// src\pages\mdashboard\index.js

import React from 'react';
import { AuthDiv } from 'components';
import Dashboard from './Dashboard';

export default function () {
return (
    &lt;section&gt;
      //AuthDiv 是 spug 封装的与权限相关的组件
      &lt;AuthDiv auth="testdashboard.testdashboard.view"&gt;
      &lt;p&gt;需要权限才能访问&lt;/p&gt;
      &lt;Dashboard /&gt;
      &lt;/AuthDiv&gt;
    &lt;/section&gt;
)
}
</code></pre>
<pre><code class="language-javascript">// src\pages\mdashboard\tIndex.js

import React from 'react';
import Dashboard from './Dashboard';

export default function () {
return (
    &lt;section&gt;
      &lt;p&gt;无需权限也能访问&lt;/p&gt;
      &lt;Dashboard /&gt;
    &lt;/section&gt;
)
}
</code></pre>
<p>重启服务,倘若能访问,说明一切就绪,只差仪表盘核心代码。</p>
<p>访问 <code>/mydashboard</code>:<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094912_mydashboard-5.png"></p>
<p>访问 <code>/mdashboard</code>:<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094916_mydashboard-6.png"></p>
<h4 id="仪表盘的核心代码">仪表盘的核心代码</h4>
<h5 id="样式">样式</h5>
<pre><code class="language-javascript">// src\pages\mdashboard\index.module.less

.tdashboardBox {
    .react{
      width: 10px;
      height: 10px;
      display: inline-block;
      background: #52c41a; /* #00000040 */
      margin-left: 30px;
      margin-right: 10px;
    }

    // 参考:src\components\index.module.less 中 global
    :global(.trendBox .ant-card-head-wrapper) {
      width: 100%;
    }
}
</code></pre>
<h5 id="表格水果信息">表格(水果信息)</h5>
<pre><code class="language-javascript">// src\pages\mdashboard\Table.js

import React from 'react';
import { observer } from 'mobx-react';
import { Descriptions } from 'antd';
import { TableCard } from 'components';
import store from './store';

@observer
class ComTable extends React.Component {
// 默认值
static defaultProps = {
    tableHeight: 353
}

// scrollY 以外的高度
excludeScrollY = 120;
componentDidMount() {
    store.fetchRecords()
}

columns = [{
    title: 'id',
    dataIndex: 'id',
},{
    title: '名称',
    dataIndex: 'name',
}, {
    title: '生产地',
    dataIndex: 'address',
}, {
    title: '时间',
    dataIndex: 'time',
}];

handleExpand = record =&gt; {
    return &lt;Descriptions&gt;
      &lt;Descriptions.Item label="真数据"&gt;{record.time}&lt;/Descriptions.Item&gt;
      &lt;Descriptions.Item label="假数据"&gt;xxx&lt;/Descriptions.Item&gt;
      &lt;Descriptions.Item label="假数据xxx"&gt;xxxxxx&lt;/Descriptions.Item&gt;
      &lt;Descriptions.Item label="假数据xx"&gt;xxxxxxxxxxxxxxx&lt;/Descriptions.Item&gt;
      &lt;Descriptions.Item label="假数据xx"&gt;xxx&lt;/Descriptions.Item&gt;
      &lt;Descriptions.Item label="假数据xxxxxx"&gt;
      xxxxx xxxxx xxxxxxxxxx xxxxxxxxx
      &lt;/Descriptions.Item&gt;
    &lt;/Descriptions&gt;
}

render() {
    console.log('this.props.tableHeight', this.props.tableHeight, 'y', this.props.tableHeight * this.scrollRadio)
    return (
      &lt;TableCard
      customStyles={{height: this.props.tableHeight}}
      title="水果信息"
      tKey="mt"
      rowKey="id"
      loading={store.isFetching}
      dataSource={store.dataSource}
      onReload={store.fetchRecords}
      actions={[]}
      scroll={{ y: this.props.tableHeight- this.excludeScrollY}}
      expandable={{
          expandedRowRender: this.handleExpand,
          expandRowByClick: true
      }}
      size={'middle'}
      // 设为 false 时不展示和进行分页
      pagination={false}
      columns={this.columns} /&gt;
    )
}
}

export default ComTable

</code></pre>
<h5 id="折线图居住趋势">折线图(居住趋势)</h5>
<pre><code class="language-javascript">// src\pages\mdashboard\Trend.js

import React, { useState, useEffect } from 'react';
import { Card, DatePicker, Modal } from 'antd';
import { Chart, Geom, Axis, Tooltip, Legend } from 'bizcharts';
import { http } from 'libs';
import styles from './index.module.less'
// 日期相关的库,比如最近30天等
import moment from 'moment';

/*
bizcharts 官网:
通过bx-tooltip插件自定义
为了满足更灵活多变的Tooltip自定义需求,提供bx-tooltip插件来实现ReactNode渲染,摆脱HTML模板的繁琐和死板
*/
import useCustTooltip from 'bx-tooltip';
import { Typography, Space } from 'antd';
import store from './store'

export default function (props = { cardBodyHeight: 450 }) {
// chart 高度占比
const chartHeightRatio = 0.888

const { Text, Link, Title } = Typography;
const = useState(true);
// 本月第一天 —— 本月最后一天
// const = useState();
// 最近三十天
const = useState();
const = useState([]);

useEffect(() =&gt; {
    const strDuration = duration.map(x =&gt; x.format('YYYY-MM-DD'))

    setLoading(true);
    http.get('/api/mdashboard/occupancy_rate/', { duration: strDuration })
      .then(res =&gt; {
      setRes(res)
      })
      .finally(() =&gt; setLoading(false))
}, )

// bx-tooltip插件的使用
const = useCustTooltip.create(Chart, Tooltip);

return (
    // headStyle、bodyStyle 在这里都是用于适配(响应式)
    &lt;Card className="trendBox" loading={loading} title="居住趋势" headStyle={store.cardTitleStyle} bodyStyle={{ height: props.cardBodyHeight }} extra={(
      &lt;div&gt;
      &lt;DatePicker.RangePicker allowClear={false} style={{ width: 250 }} value={duration} onChange={val =&gt; setDuration(val)} /&gt;
      &lt;/div&gt;
    )}&gt;

      &lt;BxChart height={props.cardBodyHeight * chartHeightRatio} data={res} padding={}
      // 坐标轴展示不完整
      scale={{ month: { range: }, per: { alias: '居住率', range: , minTickInterval: 10, max: 100, min: 0 } }}
      // 强制适应(PS:只会对宽度有响应式,高度没有)
      forceFit
      &gt;
      &lt;Legend position="right-center" allowAllCanceled={true} itemFormatter={val =&gt; {
          const maxNum = 10
          return val.length &gt; maxNum ? val.split('').slice(0, maxNum - 3).join('') + '...' : val
      }} /&gt;
      {/* x 坐标格式化 */}
      &lt;Axis name="month" label={{
          formatter(text, item, index) {
            // 格式化:2022-01-01 -&gt; 0101
            return `${text.split('-').slice(1).join('')}`;
          }
      }} /&gt;

      &lt;Axis name="per" title /&gt;

      {/* 自定义 tooltip */}
      &lt;CustTooltip enterable &gt;
          {(title, items) =&gt; {
            return &lt;div&gt;
            {
                items.map((x, i) =&gt; {
                  let oData = x.point._origin
                  return &lt;div&gt;
                  {Object.is(i, 0) &amp;&amp; &lt;Title level={5}&gt;{oData.month}&lt;/Title&gt;}
                  &lt;section style={{ marginTop: '20px' }}&gt;
                      &lt;Title style={{ color: x.color, fontWeight: 'bold' }} level={5}&gt;{oData.city}&lt;/Title&gt;
                      &lt;Space direction="vertical" size={2}&gt;
                        &lt;Text&gt;幸福指数:{oData.happiness}&lt;/Text&gt;
                        &lt;Link href="hello" target="_blank"&gt;
                        跳转
                        &lt;/Link&gt;
                        &lt;Link onClick={() =&gt; {
                        Modal.info({
                            title: 'title',
                            content: oData.msg1
                        });
                        }}&gt;
                        详情
                        &lt;/Link&gt;
                      &lt;/Space&gt;
                  &lt;/section&gt;
                  &lt;/div&gt;
                })
            }
            &lt;/div&gt;
          }}
      &lt;/CustTooltip&gt;

      &lt;Geom type="line" position="month*per"
          // 两条线
          size={2}
          // 使线条平滑
          // shape={"smooth"}
          color={"city"}
      /&gt;
      &lt;/BxChart&gt;
    &lt;/Card&gt;
)
}
</code></pre>
<h5 id="饼状图统计苹果和梨子">饼状图(统计苹果和梨子)</h5>
<pre><code class="language-javascript">// src\pages\mdashboard\PieChart.js

import React from 'react';
import { Typography} from 'antd';
import {
    Chart,
    Geom,
    Axis,
    Tooltip,
    Coord,
    Label,
    Legend
} from 'bizcharts';
import DataSet from '@antv/data-set';

// chartHeight 默认高度 250px ,用于适配
export default function (props = {chartHeight: 250}) {
    const { Text } = Typography;

    const { DataView } = DataSet;
    const data = [
      {
            item: '苹果',
            count: 10,
      },
      {
            item: '梨子',
            count: 20,
      },
    ];
    const dv = new DataView();
    dv.source(data).transform({
      type: 'percent',
      field: 'count',
      dimension: 'item',
      as: 'percent',
    });
    const cols = {
      percent: {
            formatter: val =&gt; {
                val = val * 100 + '%';
                return val;
            },
      },
    };
    function getXY(c, { index: idx = 0, field = 'percent', radius = 0.5 }) {
      const d = c.get('data');
      if (idx &gt; d.length) return;
      const scales = c.get('scales');
      let sum = 0;
      for (let i = 0; i &lt; idx + 1; i++) {
            let val = d;
            if (i === idx) {
                val = val / 2;
            }
            sum += val;
      }
      const pt = {
            y: scales.scale(sum),
            x: radius,
      };
      const coord = c.get('coord');
      let xy = coord.convert(pt);
      return xy;
    }
    return (
      &lt;section&gt;
            &lt;Text&gt;统计苹果和梨子&lt;/Text&gt;
            &lt;Chart
                height={props.chartHeight}
                // 内容显示不完整(见 bizcharts 实战部分)
                padding={}
                data={dv}
                scale={cols}
                forceFit
                onGetG2Instance={c =&gt; {
                  const xy = getXY(c, { index: 0 });
                  c.showTooltip(xy);
                }}
            &gt;
                &lt;Legend position="right-center" /&gt;
                &lt;Coord type="theta" radius={1} /&gt;
                &lt;Axis name="percent" /&gt;
                &lt;Tooltip
                  showTitle={false}
                  itemTpl='&lt;li&gt;&lt;span style="background-color:{color};" class="g2-tooltip-marker"&gt;&lt;/span&gt;{name}: {value}&lt;/li&gt;'
                /&gt;
                &lt;Geom
                  type="intervalStack"
                  position="percent"
                  color="item"
                  tooltip={[
                        'item*percent',
                        (item, percent) =&gt; {
                            // 处理 33.33333333% -&gt; 33.33
                            percent = (percent * 100).toFixed(2) + '%';
                            return {
                              name: item,
                              value: percent,
                            };
                        },
                  ]}
                  style={{
                        lineWidth: 1,
                        stroke: '#fff',
                  }}
                &gt;
                  &lt;Label
                        content="count"
                        formatter={(val, item) =&gt; {
                            return item.point.item + ': ' + val;
                        }}
                  /&gt;
                &lt;/Geom&gt;
            &lt;/Chart&gt;
      &lt;/section&gt;
    );
}
</code></pre>
<h5 id="柱状图堆叠柱状图">柱状图(堆叠柱状图)</h5>
<pre><code class="language-javascript">// src\pages\mdashboard\BarChart.js

import React from "react";
import { Typography, Space } from 'antd'
import {
Chart,
Geom,
Axis,
Tooltip,
Coord,
Legend,
} from "bizcharts";
import useCustTooltip from 'bx-tooltip';
import DataSet from "@antv/data-set";

export default function (props = {barHeight: 240}) {
const = useCustTooltip.create(Chart, Tooltip);
const { Text,Title } = Typography;
const retains = ["State", '总比例', 'bad', 'good', 'Total']
const fields = ["好的比例", "坏的比例"]
const data = [
    {
      State: "苹果(红富士、糖心苹果)",
      good: 50,
      bad: 150,
      Total: 200,
      好的比例: 25,
      坏的比例: 75,
      总比例: 100
    },
    {
      State: "梨子(香梨)",
      good: 75,
      bad: 125,
      Total: 200,
      好的比例: 37.5,
      坏的比例: 62.5,
      总比例: 100
    },
];

const ds = new DataSet();
const dv = ds.createView().source(data);

dv.transform({
    type: "fold",
    fields: fields,
    key: "比例",
    value: "百分总计",
    retains: retains // 保留字段集,默认为除fields以外的所有字段
});

return (
    &lt;section&gt;
      &lt;Text&gt;堆叠柱状图&lt;/Text&gt;
      &lt;BxChart height={props.barHeight} data={dv} padding={} forceFit&gt;
      &lt;Legend position="right-center" /&gt;
      &lt;Coord /&gt;
      &lt;Axis
          name="State"
          label={{
            offset: 12,
            formatter(text, item, index) {
            // 最多显示 10 个,多余省略。详细的在 tooltip 中显示
            const maxNum = 10
            return text.length &gt; maxNum ? text.split('').slice(0, maxNum - 3).join('') + '...' : text
            }
          }}
      /&gt;
      &lt;CustTooltip enterable &gt;
          {(title, items) =&gt; {
            return &lt;div&gt;
            {
                items.map((x, i) =&gt; {
                  // 取得原始数据
                  let oData = x.point._origin
                  return &lt;div&gt;
                  {Object.is(i, 0) &amp;&amp; &lt;Title level={5}&gt;{oData.State}&lt;/Title&gt;}
                  &lt;section style={{ marginTop: '20px' }}&gt;
                      &lt;Space direction="vertical" size={2}&gt;
                        &lt;Text style={{ color: x.color, fontWeight: 'bold' }}&gt;{oData['比例']}:{oData['百分总计']}%&lt;/Text&gt;
                        &lt;Text&gt;good数量:{oData['good']}&lt;/Text&gt;
                        &lt;Text&gt;bad数量:{oData['bad']}&lt;/Text&gt;
                        &lt;Text&gt;总数量:{oData['Total']}&lt;/Text&gt;
                      &lt;/Space&gt;
                  &lt;/section&gt;
                  &lt;/div&gt;
                })
            }
            &lt;/div&gt;
          }}
      &lt;/CustTooltip&gt;
      &lt;Geom
          type="intervalStack"
          position="State*百分总计"
          color={"比例"}
      &gt;
      &lt;/Geom&gt;
      &lt;/BxChart&gt;
    &lt;/section&gt;
);
}
</code></pre>
<h5 id="storejs">store.js</h5>
<pre><code class="language-javascript">// src\pages\mdashboard\store.js

import { observable, computed } from 'mobx';
import http from 'libs/http';

const PADDING = 16
class Store {
// 表格数据
@observable records = [];

// 是否正在请求数据
@observable isFetching = false;

// 数据源
@computed get dataSource() {
    return this.records
}

fetchRecords = () =&gt; {
    this.isFetching = true;
    http.get('/api/mdashboard/table')
      // todo 接口格式或许会调整
      .then(res =&gt; this.records = res)
      .finally(() =&gt; this.isFetching = false)
};

/* 适配相关 */
// 盒子高度,padding 用于给顶部和底部留点空隙。
// 由于笔者没有设计,所以先用 px 实现,之后在在将固定高度改为响应式,937 是固定高度实现后测量出的高度。
@observable baseBoxHeight = 937 - PADDING
@observable padding = PADDING
// 需要用 this 调用 padding 变量,即 `this.padding`
@observable boxHeight = window.innerHeight - this.padding * 2

// 饼图高度比例
@observable pieBoxRatio = 0.20

// 柱状图高度比例
@observable barBoxRatio = 0.23

// “My Dashboard 我的仪表盘”
@computed get TitleHeight() {
    const ratio = 80 / this.baseBoxHeight
    return this.boxHeight * ratio
}
// 运行card高度
@computed get todayCardHeight() {
    const ratio = 75 / this.baseBoxHeight
    return this.boxHeight * ratio
}

// “饼图+描述列表+柱状图” body 高度
@computed get statisticBodyHeight() {
    const ratio = 660 / this.baseBoxHeight
    return this.boxHeight * ratio
}

// 居住趋势 body 的
@computed get trendBodyBodyHeight() {
    const ratio = 385 / this.baseBoxHeight
    return this.boxHeight * ratio
}

// 水果信息高度
@computed get configTableHeight() {
    const ratio = 353 / this.baseBoxHeight
    return this.boxHeight * ratio
}

// xys16 得用 computed 才会联动。下面这种写法不会联动
// @observable xys16 = (16 / this.baseBoxHeight) * this.boxHeight
@computed get xys16() {
    return (16 / this.baseBoxHeight) * this.boxHeight
}

@computed get xys12() {
    return (12 / this.baseBoxHeight) * this.boxHeight
}

@computed get xys36() {
    return (36 / this.baseBoxHeight) * this.boxHeight
}

@computed get xys24() {
    return (24 / this.baseBoxHeight) * this.boxHeight
}

@computed get xys78() {
    return (78 / this.baseBoxHeight) * this.boxHeight
}

@computed get pieBoxHeight() {
    return this.pieBoxRatio * this.boxHeight
}

@computed get barBoxHeight() {
    return this.barBoxRatio * this.boxHeight
}

// card 的 header
@computed get cardTitleStyle() {
    const cardTitleRatio = 57 / this.baseBoxHeight
    return { display: 'flex', height: this.boxHeight * cardTitleRatio, alignItems: 'center', justifyContent: 'center' }
}
/* /适配相关 */
}

export default new Store()
</code></pre>
<h5 id="dashboardjs">Dashboard.js</h5>
<pre><code class="language-javascript">// src\pages\mdashboard\Dashboard.js


import React, {useEffect, Fragment} from 'react';
import { Row, Col, Card, Descriptions, Typography, Divider } from 'antd';
import AlarmTrend from './Trend';
import Piechart from './PieChart'
import CusTable from './Table';
import CusBarChart from './BarChart';
import Styles from './index.module.less'
import { observer } from 'mobx-react';
import store from './store'

export default observer(function () {
// Typography排版
const { Text } = Typography;

useEffect(() =&gt; {
    // 响应式
    window.addEventListener("resize", function(){
      // padding,用于留点间距出来
      store.boxHeight = window.innerHeight - store.padding * 2
    }, false);
}, [])

return (
    // Fragment 用于包裹多个元素,却不会被渲染到 dom
    &lt;Fragment&gt;
      {/* 使用单一的一组 Row 和 Col 栅格组件,就可以创建一个基本的栅格系统,所有列(Col)必须放在 Row 内。 */}
      &lt;Row style={{ marginBottom: store.xys16 }}&gt;
      &lt;Col span={24}&gt;
          {/* 可以省略 px */}
          {/* 如果将字体和padding 改为响应式,height 设置或不设置还是有差别的,设置 height 会更准确 */}
          &lt;Card bodyStyle={{display: 'flex', height: store.TitleHeight, justifyContent: 'center', padding: store.xys12, fontSize: store.xys36, fontWeight: 700,}}&gt;
            &lt;Text&gt;My Dashboard 我的仪表盘&lt;/Text&gt;
          &lt;/Card&gt;
      &lt;/Col&gt;
      &lt;/Row&gt;
      &lt;Row gutter={16}&gt;
      &lt;Col span={8}&gt;
          {/* gutter:水平垂直间距都是 响应式 16*/}
          &lt;Row gutter={}&gt;
            {/* 24 栅格系统。 */}
            &lt;Col span={24}&gt;
            {/* 垂直居中 */}
            &lt;Card bodyStyle={{ display: 'flex', height: store.todayCardHeight, alignItems: 'center'}}&gt;
                {/* 文字大小 */}
                &lt;span&gt;
                &lt;Text style={{ fontSize: store.xys16}}&gt;
                  运行为绿色,否则为灰色:
                  &lt;span className={Styles.react}&gt;&lt;/span&gt;
                  &lt;span&gt;运行&lt;/span&gt;
                &lt;/Text&gt;
                &lt;/span&gt;
            &lt;/Card&gt;
            &lt;/Col&gt;
            &lt;Col span={24}&gt;
            &lt;Card title="饼图+描述列表+柱状图" headStyle={store.cardTitleStyle} bodyStyle={{height: store.statisticBodyHeight}}&gt;
                &lt;Piechart chartHeight={store.pieBoxHeight}/&gt;
                &lt;Divider style={{margin: `${store.xys12}px 0`}}/&gt;
                {/* Descriptions描述列表,常见于详情页的信息展示。这里总是显示两列。 */}
                {/* spug 中“Dashboard”的“最近30天登录”是用的就是Descriptions,缺点是不像 table 对齐。当文字长度不同,会看起来错乱。 */}
                {/* 样式,用于适配,即垂直居中 */}
                &lt;Descriptions column={2} style={{display: 'flex', alignItems: 'center', minHeight: store.xys78}}&gt;
                  &lt;Descriptions.Item label="Descriptions"&gt;描述列表&lt;/Descriptions.Item&gt;
                  &lt;Descriptions.Item label="梨子"&gt;5个&lt;/Descriptions.Item&gt;
                  &lt;Descriptions.Item label="购买时间"&gt;2022-04-21&lt;/Descriptions.Item&gt;
                  &lt;Descriptions.Item label="购买途径"&gt;
                  &lt;Text
                      style={{ width: 100 }}
                      ellipsis={{ tooltip: '看不完整就将鼠标移上来' }}&gt;
                      看不完整就将鼠标移上来
                      {/* 超A、超B、超C、超D, */}
                  &lt;/Text&gt;
                  &lt;/Descriptions.Item&gt;
                &lt;/Descriptions&gt;
                &lt;Divider style={{margin: `${store.xys12}px 0`}}/&gt;
                &lt;CusBarChart barHeight={store.barBoxHeight}/&gt;
            &lt;/Card&gt;
            &lt;/Col&gt;
          &lt;/Row&gt;
      &lt;/Col&gt;
      &lt;Col span={16} &gt;
          &lt;Row gutter={}&gt;
            &lt;Col span={24}&gt;
            &lt;AlarmTrend cardBodyHeight={store.trendBodyBodyHeight}/&gt;
            &lt;/Col&gt;
            &lt;Col span={24}&gt;
            &lt;CusTable tableHeight={store.configTableHeight}/&gt;
            &lt;/Col&gt;
          &lt;/Row&gt;
      &lt;/Col&gt;
      &lt;/Row&gt;
    &lt;/Fragment&gt;
)
})
</code></pre>
<h5 id="indexjs">index.js</h5>
<pre><code class="language-javascript">// src\pages\mdashboard\index.js

import React from 'react';
import { AuthDiv } from 'components';
import Dashboard from './Dashboard';
import styles from './index.module.less'

export default function () {
return (
    &lt;section className={styles.tdashboardBox}&gt;
      &lt;AuthDiv auth="testdashboard.testdashboard.view"&gt;
      &lt;Dashboard /&gt;
      &lt;/AuthDiv&gt;
    &lt;/section&gt;
)
}
</code></pre>
<h5 id="tindexjs">tIndex.js</h5>
<pre><code class="language-javascript">// src\pages\mdashboard\tIndex.js

// 无需权限即可访问

import React from 'react';
import Dashboard from './Dashboard';
import store from './store';
import styles from './index.module.less'

export default function () {
return (
    &lt;section className={styles.tdashboardBox} style={{padding: `${store.padding}px 16px`, backgroundColor: 'rgb(125 164 222)', height: '100vh'}}&gt;
      &lt;Dashboard/&gt;
    &lt;/section&gt;
)
}
</code></pre>
<p>重启服务,效果如下:</p>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094907_mydashboard-4.png"></p>
<h3 id="bizcharts">bizcharts</h3>
<p>bizcharts 是阿里的一个图表组件库。</p>
<p><em>注</em>:spug 项目中使用的版本是 <code>3.x</code>。参考文档时不要搞错。</p>
<h4 id="api文档">API文档</h4>
<p>上面我们安装的其中一个依赖包 <code>bx-tooltip</code> 就来自这里。</p>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094830_bizcharts1.png"></p>
<h4 id="实战">实战</h4>
<p>实战其实就是一些 bizcharts 使用上的一些<code>答疑</code>。例如“内容显示不完整”,有可能就是因为 padding 的原因。</p>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094837_bizcharts2.png"></p>
<h4 id="图表示例">图表示例</h4>
<p>例如我们使用的<code>堆叠柱状图</code>的用法示例就参考这里:</p>
<p><img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094842_bizcharts3.png"></p>
<p>点击进入示例,修改左边源码,右侧显示也会<strong>同步</strong>,非常方便我们在线研究和学习:<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094851_bizcharts5.png"></p>
<h3 id="高度自适应">高度自适应</h3>
<p>bizcharts 有宽度自适应,但没有实现高度的自适应。<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094846_bizcharts4.png"></p>
<p>笔者高度自适应的做法:将高度全部改为百分比。<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430094919_mydashboard-7.png"></p>
<p>具体做法如下:</p>
<ol>
<li>由于没有设计,故先用固定像素实现界面</li>
<li>取得浏览器的窗口高度 window.innerHeight,笔者这里是 937</li>
<li>将“标签盒子”、“卡片头部高度”、卡片 body 部分等全部改为百分比</li>
</ol>
<p>核心代码如下:</p>
<pre><code class="language-javascript">// src\pages\mdashboard\store.js

const PADDING = 16
class Store {

@observable baseBoxHeight = 937 - PADDING

@observable padding = PADDING

// 仪表盘盒子高度
@observable boxHeight = window.innerHeight - this.padding * 2

// 饼图高度比例。根据之前的效果算出来的
@observable pieBoxRatio = 0.20

// 柱状图高度比例
@observable barBoxRatio = 0.23

// “My Dashboard 我的仪表盘” 高度
@computed get TitleHeight() {
    const ratio = 80 / this.baseBoxHeight
    return this.boxHeight * ratio
}
// 运行card高度
@computed get todayCardHeight() {
    const ratio = 75 / this.baseBoxHeight
    return this.boxHeight * ratio
}

// “饼图+描述列表+柱状图” body 高度
@computed get statisticBodyHeight() {
    const ratio = 660 / this.baseBoxHeight
    return this.boxHeight * ratio
}

// 居住趋势 body 的高度
@computed get trendBodyBodyHeight() {
    const ratio = 385 / this.baseBoxHeight
    return this.boxHeight * ratio
}

// xys16 得用 computed 才会联动。下面这种写法不会联动
@computed get xys16() {
    return (16 / this.baseBoxHeight) * this.boxHeight
}

// 饼状图盒子高度
@computed get pieBoxHeight() {
    return this.pieBoxRatio * this.boxHeight
}


// card 的 header 比例
@computed get cardTitleStyle() {
    const cardTitleRatio = 57 / this.baseBoxHeight
    return { display: 'flex', height: this.boxHeight * cardTitleRatio, alignItems: 'center', justifyContent: 'center' }
}
}
</code></pre>
<h3 id="问题">问题</h3>
<p>实现过程中出现如下<strong>两个</strong>问题:一个是折线图的 Y 轴<code>乱序</code>,一个是堆叠柱状图有一节<code>空白</code>。<br>
<img src="https://images.cnblogs.com/cnblogs_com/blogs/665957/galleries/2127271/o_220430115749_mydashboard-8.png"></p>
<p>原因是<strong>值</strong>不小心弄成了<code>字符串</code>,改为<code>数字</code>类型即可。</p>
<blockquote>
<p>其他章节请看:</p>
<p>react实战 系列</p>
</blockquote>


</div>
<div id="MySignature" role="contentinfo">
    <section class="m-reprintStatement" style="white-space:normal;/* 防止break-all与nowrap矛盾 */word-break:break-all;">
    作者:彭加李<br>
    出处:https://www.cnblogs.com/pengjiali/p/16211183.html<br>
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
</section><br><br>
来源:https://www.cnblogs.com/pengjiali/p/16211183.html
頁: [1]
查看完整版本: react实战系列 —— 我的仪表盘(bizcharts、antd、moment)