react實戰系列 —— 我的儀表盤(bizcharts、antd、moment)

彭加李發表於2022-04-30

其他章節請看:

react實戰 系列

My Dashboard

上一篇我們在 spug 專案中模仿”任務計劃“模組實現一個類似的一級導航頁面(”My任務計劃“),本篇,我們將模仿“Dashboard”來實現一個儀表盤“My Dashboard”。

主要涉及 antd 的 GridCardDescriptions等元件、bizcharts 的使用、moment 日期庫和頁面適配。

:實現的程式碼在上一篇的基礎上展開。

Dashboard

介面如下:
mydashboard-1.png

裡面用到了:

  • antd 的 GridCardDescriptions 描述列表 (文字長度不同,有時會感覺沒對齊)
  • bizcharts 中的折線圖、柱狀圖
  • moment(日期相關的庫),比如按天、按月、最近 30 天都很方便

My Dashboard

最終效果

mydashboard-2.png

無需許可權即可訪問:
mydashboard-3.png

全屏效果:
mydashboard-4.png

實現的程式碼

安裝兩個依賴包:

  • @antv/data-set,柱狀圖和餅狀圖需要使用
  • bx-tooltip,自定義 bizcharts 中的 tooltip。折線圖和柱狀圖的 tooltip 都使用了。
spug-study> 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.
spug-study> 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.

package.json 變動如下:

"dependencies": {
  "@antv/data-set": "^0.11.8",
}
"devDependencies": {
  "bx-tooltip": "^0.1.6",
}

增強表格元件

spug 中封裝的表格元件,不支援 style和 size。替換一行,以及增加一行:

// src/components/TableCard.js

- <div ref={rootRef} className={styles.tableCard}>
+ <div ref={rootRef} className={styles.tableCard} style={{...props.customStyles}}>

<Table
+ size={props.size}

準備 mock 資料

將 mydashboard 模組的的 mock 專門放入一個檔案,並在 mock/index.js 中引入。

// src\mock\index.js

+ import './mydashboard'

// src\mock\mydashboard.js

import Mock from 'mockjs'

// 開發環境引入 mock
if (process.env.NODE_ENV === 'development') {
    
Mock.mock('/api/mdashboard/occupancy_rate/', 'get', () => (
    {"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', () => ({
    "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() },
]
}))
}

路由配置

配置 /mdashboard/mydashboard 兩個路由:

// src\App.js

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

class App extends Component {
  render() {
    return (
      <Switch>
      //  無需許可權
      + <Route path="/mdashboard" exact component={MDashboard} />
        <Route path="/" exact component={Login} />
        <Route path="/ssh" exact component={WebSSH} />
        <Route component={Layout} />
      </Switch>
    );
  }
}
// src\routes.js

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

export default [
  {icon: <DesktopOutlined/>, title: '工作臺', path: '/home', component: HomeIndex},
  {
    icon: <DashboardOutlined/>,
    title: 'Dashboard',
    auth: 'dashboard.dashboard.view',
    path: '/dashboard',
    component: DashboardIndex
  },
+ // 我的儀表盤
+ {
+   icon: <DashboardOutlined />,
+   title: 'MyDashboard',
+   auth: 'mydashboard.mydashboard.view',
+   path: '/mydashboard',
+   component: MyDashboardIndex
+ },

新建儀表盤元件。一個需要許可權訪問,另一個無需許可權即可訪問,故將儀表盤提取成一個單獨的檔案:

// src\pages\mdashboard\Dashboard.js

import React from 'react';
export default function () {
  return (
    <div>儀表盤</div>
  )
}
// src\pages\mdashboard\index.js

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

export default function () {
  return (
    <section>
      //  AuthDiv 是 spug 封裝的與許可權相關的元件
      <AuthDiv auth="testdashboard.testdashboard.view">
        <p>需要許可權才能訪問</p>
        <Dashboard />
      </AuthDiv>
    </section>
  )
}
// src\pages\mdashboard\tIndex.js

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

export default function () {
  return (
    <section>
        <p>無需許可權也能訪問</p>
        <Dashboard />
    </section>
  )
}

重啟服務,倘若能訪問,說明一切就緒,只差儀表盤核心程式碼。

訪問 /mydashboard
mydashboard-5.png

訪問 /mdashboard
mydashboard-6.png

儀表盤的核心程式碼

樣式
// 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%;
    }
}
表格(水果資訊)
// 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 => {
    return <Descriptions>
      <Descriptions.Item label="真資料">{record.time}</Descriptions.Item>
      <Descriptions.Item label="假資料">xxx</Descriptions.Item>
      <Descriptions.Item label="假資料xxx">xxxxxx</Descriptions.Item>
      <Descriptions.Item label="假資料xx">xxxxxxxxxxxxxxx</Descriptions.Item>
      <Descriptions.Item label="假資料xx">xxx</Descriptions.Item>
      <Descriptions.Item label="假資料xxxxxx">
        xxxxx xxxxx xxxxxxxxxx xxxxxxxxx
      </Descriptions.Item>
    </Descriptions>
  }

  render() {
    console.log('this.props.tableHeight', this.props.tableHeight, 'y', this.props.tableHeight * this.scrollRadio)
    return (
      <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} />
    )
  }
}

export default ComTable

折線圖(居住趨勢)
// 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 [loading, setLoading] = useState(true);
  // 本月第一天 —— 本月最後一天
  // const [duration, setDuration] = useState([moment().startOf('month'), moment().endOf('month')]);
  // 最近三十天
  const [duration, setDuration] = useState([moment().subtract(29, 'days'), moment()]);
  const [res, setRes] = useState([]);

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

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

  // bx-tooltip外掛的使用
  const [BxChart, CustTooltip] = useCustTooltip.create(Chart, Tooltip);

  return (
    // headStyle、bodyStyle 在這裡都是用於適配(響應式)
    <Card className="trendBox" loading={loading} title="居住趨勢" headStyle={store.cardTitleStyle} bodyStyle={{ height: props.cardBodyHeight }} extra={(
      <div>
        <DatePicker.RangePicker allowClear={false} style={{ width: 250 }} value={duration} onChange={val => setDuration(val)} />
      </div>
    )}>

      <BxChart height={props.cardBodyHeight * chartHeightRatio} data={res} padding={[30, 120, 20, 60]}
        // 座標軸展示不完整
        scale={{ month: { range: [0.05, 0.99] }, per: { alias: '居住率', range: [0, 0.95], minTickInterval: 10, max: 100, min: 0 } }}
        // 強制適應(PS:只會對寬度有響應式,高度沒有)
        forceFit
      >
        <Legend position="right-center" allowAllCanceled={true} itemFormatter={val => {
          const maxNum = 10
          return val.length > maxNum ? val.split('').slice(0, maxNum - 3).join('') + '...' : val
        }} />
        {/* x 座標格式化 */}
        <Axis name="month" label={{
          formatter(text, item, index) {
            // 格式化:2022-01-01 -> 0101
            return `${text.split('-').slice(1).join('')}`;
          }
        }} />

        <Axis name="per" title />

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

        <Geom type="line" position="month*per"
          // 兩條線
          size={2}
          // 使線條平滑
          // shape={"smooth"} 
          color={"city"}
        />
      </BxChart>
    </Card>
  )
}
餅狀圖(統計蘋果和梨子)
// 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 => {
                val = val * 100 + '%';
                return val;
            },
        },
    };
    function getXY(c, { index: idx = 0, field = 'percent', radius = 0.5 }) {
        const d = c.get('data');
        if (idx > d.length) return;
        const scales = c.get('scales');
        let sum = 0;
        for (let i = 0; i < idx + 1; i++) {
            let val = d[i][field];
            if (i === idx) {
                val = val / 2;
            }
            sum += val;
        }
        const pt = {
            y: scales[field].scale(sum),
            x: radius,
        };
        const coord = c.get('coord');
        let xy = coord.convert(pt);
        return xy;
    }
    return (
        <section>
            <Text>統計蘋果和梨子</Text>
            <Chart
                height={props.chartHeight}
                // 內容顯示不完整(見 bizcharts 實戰部分)
                padding={[20, 150, 20, 40]}
                data={dv}
                scale={cols}
                forceFit
                onGetG2Instance={c => {
                    const xy = getXY(c, { index: 0 });
                    c.showTooltip(xy);
                }}
            >
                <Legend position="right-center" />
                <Coord type="theta" radius={1} />
                <Axis name="percent" />
                <Tooltip
                    showTitle={false}
                    itemTpl='<li><span style="background-color:{color};" class="g2-tooltip-marker"></span>{name}: {value}</li>'
                />
                <Geom
                    type="intervalStack"
                    position="percent"
                    color="item"
                    tooltip={[
                        'item*percent',
                        (item, percent) => {
                            // 處理 33.33333333% -> 33.33
                            percent = (percent * 100).toFixed(2) + '%';
                            return {
                                name: item,
                                value: percent,
                            };
                        },
                    ]}
                    style={{
                        lineWidth: 1,
                        stroke: '#fff',
                    }}
                >
                    <Label
                        content="count"
                        formatter={(val, item) => {
                            return item.point.item + ': ' + val;
                        }}
                    />
                </Geom>
            </Chart>
        </section>
    );
}
柱狀圖(堆疊柱狀圖)
// 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 [BxChart, CustTooltip] = 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 (
    <section>
      <Text>堆疊柱狀圖</Text>
      <BxChart height={props.barHeight} data={dv} padding={[30, 80, 20, 40]} forceFit>
        <Legend position="right-center" />
        <Coord />
        <Axis
          name="State"
          label={{
            offset: 12,
            formatter(text, item, index) {
              // 最多顯示 10 個,多餘省略。詳細的在 tooltip 中顯示
              const maxNum = 10
              return text.length > maxNum ? text.split('').slice(0, maxNum - 3).join('') + '...' : text
            }
          }}
        />
        <CustTooltip enterable >
          {(title, items) => {
            return <div>
              {
                items.map((x, i) => {
                  // 取得原始資料
                  let oData = x.point._origin
                  return <div>
                    {Object.is(i, 0) && <Title level={5}>{oData.State}</Title>}
                    <section style={{ marginTop: '20px' }}>
                      <Space direction="vertical" size={2}>
                        <Text style={{ color: x.color, fontWeight: 'bold' }}>{oData['比例']}:{oData['百分總計']}%</Text>
                        <Text>good數量:{oData['good']}</Text>
                        <Text>bad數量:{oData['bad']}</Text>
                        <Text>總數量:{oData['Total']}</Text>
                      </Space>
                    </section>
                  </div>
                })
              }
            </div>
          }}
        </CustTooltip>
        <Geom
          type="intervalStack"
          position="State*百分總計"
          color={"比例"}
        >
        </Geom>
      </BxChart>
    </section>
  );
}
store.js
// 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 = () => {
    this.isFetching = true;
    http.get('/api/mdashboard/table')
      // todo 介面格式或許會調整 
      .then(res => this.records = res)
      .finally(() => 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()
Dashboard.js
// 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(() => {
    // 響應式
    window.addEventListener("resize", function(){
      // padding,用於留點間距出來
      store.boxHeight = window.innerHeight - store.padding * 2
    }, false);
  }, [])

  return (
    // Fragment 用於包裹多個元素,卻不會被渲染到 dom
    <Fragment>
      {/* 使用單一的一組 Row 和 Col 柵格元件,就可以建立一個基本的柵格系統,所有列(Col)必須放在 Row 內。 */}
      <Row style={{ marginBottom: store.xys16 }}>
        <Col span={24}>
          {/* 可以省略 px */}
          {/* 如果將字型和padding 改為響應式,height 設定或不設定還是有差別的,設定 height 會更準確 */}
          <Card bodyStyle={{display: 'flex', height: store.TitleHeight, justifyContent: 'center', padding: store.xys12, fontSize: store.xys36, fontWeight: 700,  }}>
            <Text>My Dashboard 我的儀表盤</Text>
          </Card>
        </Col>
      </Row>
      <Row gutter={16}>
        <Col span={8}>
          {/* gutter:水平垂直間距都是 響應式 16  */}
          <Row gutter={[store.xys16, store.xys16]}>
            {/* 24 柵格系統。 */}
            <Col span={24}>
              {/* 垂直居中 */}
              <Card bodyStyle={{ display: 'flex', height: store.todayCardHeight, alignItems: 'center'}}>
                {/* 文字大小 */}
                <span>
                <Text style={{ fontSize: store.xys16}}>
                  執行為綠色,否則為灰色:
                  <span className={Styles.react}></span>
                  <span>執行</span>
                </Text>
                </span>
              </Card>
            </Col>
            <Col span={24}>
              <Card title="餅圖+描述列表+柱狀圖" headStyle={store.cardTitleStyle} bodyStyle={{height: store.statisticBodyHeight}}>
                <Piechart chartHeight={store.pieBoxHeight}/>
                <Divider style={{margin: `${store.xys12}px 0`}}/>
                {/* Descriptions描述列表,常見於詳情頁的資訊展示。這裡總是顯示兩列。 */}
                {/* spug 中“Dashboard”的“最近30天登入”是用的就是Descriptions,缺點是不像 table 對齊。當文字長度不同,會看起來錯亂。 */}
                {/* 樣式,用於適配,即垂直居中 */}
                <Descriptions column={2} style={{display: 'flex', alignItems: 'center', minHeight: store.xys78}}>
                  <Descriptions.Item label="Descriptions">描述列表</Descriptions.Item>
                  <Descriptions.Item label="梨子">5個</Descriptions.Item>
                  <Descriptions.Item label="購買時間">2022-04-21</Descriptions.Item>
                  <Descriptions.Item label="購買途徑">
                    <Text
                      style={{ width: 100 }}
                      ellipsis={{ tooltip: '看不完整就將滑鼠移上來' }}>
                      看不完整就將滑鼠移上來
                      {/* 超A、超B、超C、超D, */}
                    </Text>
                  </Descriptions.Item>
                </Descriptions>
                <Divider style={{margin: `${store.xys12}px 0`}}/>
                <CusBarChart barHeight={store.barBoxHeight}/>
              </Card>
            </Col>
          </Row>
        </Col>
        <Col span={16} >
          <Row gutter={[store.xys16, store.xys16]}>
            <Col span={24}>
              <AlarmTrend cardBodyHeight={store.trendBodyBodyHeight}/>
            </Col>
            <Col span={24}>
              <CusTable tableHeight={store.configTableHeight}/>
            </Col>
          </Row>
        </Col>
      </Row>
    </Fragment>
  )
})
index.js
// 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 (
    <section className={styles.tdashboardBox}>
      <AuthDiv auth="testdashboard.testdashboard.view">
        <Dashboard />
      </AuthDiv>
    </section>
  )
}
tIndex.js
// 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 (
    <section className={styles.tdashboardBox} style={{padding: `${store.padding}px 16px`, backgroundColor: 'rgb(125 164 222)', height: '100vh'}}>
      <Dashboard/>
    </section>
  )
}

重啟服務,效果如下:

mydashboard-4.png

bizcharts

bizcharts 是阿里的一個圖表元件庫。

:spug 專案中使用的版本是 3.x。參考文件時不要搞錯。

API文件

上面我們安裝的其中一個依賴包 bx-tooltip 就來自這裡。

bizcharts1.png

實戰

實戰其實就是一些 bizcharts 使用上的一些答疑。例如“內容顯示不完整”,有可能就是因為 padding 的原因。

bizcharts2.png

圖表示例

例如我們使用的堆疊柱狀圖的用法示例就參考這裡:

bizcharts3.png

點選進入示例,修改左邊原始碼,右側顯示也會同步,非常方便我們線上研究和學習:
bizcharts4.png

高度自適應

bizcharts 有寬度自適應,但沒有實現高度的自適應。
bizcharts5.png

筆者高度自適應的做法:將高度全部改為百分比。
mydashboard-7.png

具體做法如下:

  1. 由於沒有設計,故先用固定畫素實現介面
  2. 取得瀏覽器的視窗高度 window.innerHeight,筆者這裡是 937
  3. 將“標籤盒子”、“卡片頭部高度”、卡片 body 部分等全部改為百分比

核心程式碼如下:

// 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' }
  }
}

問題

實現過程中出現如下兩個問題:一個是折線圖的 Y 軸亂序,一個是堆疊柱狀圖有一節空白
mydashboard-8.png

原因是不小心弄成了字串,改為數字型別即可。

其他章節請看:

react實戰 系列

相關文章