1. 程式人生 > 其它 >繁瑣實用系列(一):vue實現github活躍圖

繁瑣實用系列(一):vue實現github活躍圖

技術標籤:繁瑣不難實用Vue功能元件vue.js

根據一些實用、不難但是繁瑣的功能,推出一個這個系列,方便大家以後直接拿來就用。

緣由:

最新有老師看見其他網站有仿github活躍圖的效果,讓我安排一下。

去github仔細看了看,以一個想法是看看echarts有沒有類似的,找了一會發現有個類似的,但是修改成本較大,果斷造輪子。

效果圖:

思路:

1.先排除座標說明,先繪製最裡面的每個小格子。

第一個格子的時間:上一年今天如果不是週一,那麼推到下一個週一。

最後一個格子的時間:今天。

格子的數量:最後一個格子時間 - 第一個格子時間 。

最後一個列的個數:今天的星期數 或 格子總數量除以7取餘。

格子的顏色:格子是用for迴圈變數的陣列資料,設定每個格子data時候,帶上提交次數屬性,然後寫個根據數量獲取不同顏色的方法。

滑鼠移入的內容:元件採用的iview的tooltip。根據專案整合的ui庫可更改,或者手寫一個也不難。

2.繪製x,y軸座標值。

y座標代表星期數,比較簡單,直接新增格子容器節點的上面就行。

x座標代表月份, 並非每列都有月份,只會在當前月份的第一列顯示月份。

月份第一個列判斷依據:每個格子我都賦予了具體的日期,判斷前一列是否有不相同月份的日期,如果有不同的日期設定一個變數為true,如果沒有重新設定成false,然後根據這個變數來決定本列是不是月份第一列。

3.繪製legend。

位置使用flex佈局就行,不做過多解釋。

左側的slider用的iview。

程式碼:

<template>
  <div class="submission-chart">
    <div class="calendar">
      <div class="weeks">
        <div class="week">週二</div>
        <div class="week">週四</div>
        <div class="week">週六</div>
      </div>
      <div class="column" v-for="(columnData, columnIndex) in dateData" :key="columnIndex">
        <div class="title">{{columnData.title}}</div>
        <div 
          class="date-wrapper" 
          v-for="(dateData, dateIndex) in columnData.data" 
          :key="dateIndex" 
          :style="`background:${getColor(dateData.number)};`" 
        >
        <Tooltip placement="top" :delay="300" :content="`${dateData.date}:${dateData.number}次通過`">
          <div class="date"></div>
        </Tooltip>
        </div>
      </div>
    </div>
    <div class="operation">
      <div class="slider">
        <div class="slider-desc">0</div>
        <div style="width:120px;">
         <Slider :value="sliderValue" :max="12" range :tip-format="sliderFormat" @on-change="sliderChange"></Slider>
        </div>
        <div class="slider-desc">12+</div>
      </div>
      <div class="legend">
        <div class="level-desc">少</div>
        <div class="level level-1"></div>
        <div class="level level-2"></div>
        <div class="level level-3"></div>
        <div class="level level-4"></div>
        <div class="level level-5"></div>
        <div class="level-desc">多</div>
      </div>
    </div>
  </div>
</template>

<script>
  import moment from 'moment'
  export default {
    name: 'submission-chart',
    data () {
      return {
        dateData: [],
        submissionRecord: {},
        sliderValue: [0, 12]
      }
    },
    props: {
      profile: {
        default: {},
        type: Object
      }
    },
    mounted () {
      this.formatProblemData()
      this.init()
    },
    methods: {
      init () {
        // 上一年資訊
        let prevYear = moment().format('YYYY') - 1
        let prevTodayFormatStr = prevYear + '-' + moment().format('MM-DD')
        let prevToday = moment(prevTodayFormatStr).format('YYYY-MM-DD')
        // 上年今日的是星期幾
        let prevTodayWeekNum = moment(prevToday).weekday() || 7
        // 初始日期(上年臨近的星期一)
        let firstMondayDate = prevTodayWeekNum > 1 ? moment(prevToday).add(8 - prevTodayWeekNum, 'days').format('YYYY-MM-DD') : prevToday
        // 初始日期至今日的天數,包括今日
        let days = moment().diff(moment(firstMondayDate), 'days') + 1
        // 每週天數
        let columns = 7
        // 最大列數(週數)
        let lineNums = Math.ceil(days / columns)
        // 繪製圖表的源資料
        let dateData = []
        let monthCN = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
        let startSliderNum = this.sliderValue[0]
        let endSliderNum = this.sliderValue[1]
        for (let i = 0; i < lineNums; i++) {
          // 最近一星期不一定滿的
          let weekColumn = (i === lineNums - 1 ? days % columns ? days % columns : columns : columns)
          // 開始計算title(月份的圖例)
          // 思路:第一列直接根據第一天的月份
          //      之後的嘛列數根據上一週的最後一天減去第一天的月份,如果大於1代表月份發生了改變,下一列的title顯示最新的月份
          let theWeekStartMonth = moment(firstMondayDate).add(i * 7, 'days').format('M')
          let theWeekEndMonth = moment(firstMondayDate).add(i * 7 + weekColumn, 'days').format('M')
          let title = (i === 0) ? monthCN[theWeekStartMonth - 1] : ''
          let ifSwitchMonth = false
          if (theWeekEndMonth - theWeekStartMonth) {
            ifSwitchMonth = true
          }
          if (i && dateData[i - 1].ifSwitchMonth) {
            title = monthCN[theWeekEndMonth - 1]
          }
          // 圖表源資料格式:columns:列數,title:列標題,ifSwitchMonth:月份是否發生改變,data:每格資料
          dateData.push({
            columns: weekColumn,
            title: title,
            ifSwitchMonth: ifSwitchMonth,
            data: []
          })
          for (let j = 0; j < dateData[i].columns; j++) {
            let date = moment(firstMondayDate).add(i * 7 + j, 'days').format('YYYY-MM-DD')
            let number = 0
            // 提交次數在slider範圍內再進行次數賦值
            if ((this.submissionRecord[date] >= startSliderNum && this.submissionRecord[date] < endSliderNum) ||
             (this.submissionRecord[date] > 12 && endSliderNum === 12)) {
              number = this.submissionRecord[date]
            }
            // number:提交次數,date:提交日期
            dateData[i].data.push({
              number: number,
              date: date
            })
          }
        }
        this.dateData = dateData
      },
      formatProblemData () {
        let submissionRecord = {}
        // let OIProblems = this.profile.oi_problems_status.problems || {}
        // // 格式化profile中oi的提交記錄資料,建立submissionRecord物件,將create_time作為key進行儲存
        // Object.keys(OIProblems).forEach(problemID => {
        //   if (OIProblems[problemID]['status'] === 0) {
        //     let date = moment(OIProblems[problemID]['create_time']).format('YYYY-MM-DD')
        //     // 第一次出現提交次數設定1,之後每次出現提交次數+1
        //     submissionRecord[date.toString()] = submissionRecord[date] ? ((submissionRecord[date])) + 1 : 1
        //   }
        // })

        // 處理你的業務邏輯
        // submissionRecord 最後的格式應為 {'2020-01-01':10, '2020-01-02': 11}
        this.submissionRecord = submissionRecord
      },
      getColor (number) {
        // level color
        // 左閉右開
        let color = '#EBEDF0'
        if (number >= 12) {
          color = '#196127'
        } else if (number >= 8) {
          color = '#239A3B'
        } else if (number >= 4) {
          color = '#7BC96F'
        } else if (number >= 1) {
          color = '#C6E48B'
        } else {
          color = '#EBEDF0'
        }
        return color
      },
      sliderFormat (val) {
        return '提交次數: ' + val
      },
      sliderChange (val) {
        // 沒有使用v-model繫結sliderValue而是採用回撥的原因
        // 1.拖拽1px sliderValue都會引起元件重繪,此元件計算嵌套了2個for迴圈,導致頁面出現卡頓slider不流暢的情況
        this.sliderValue = val
        this.init()
      }
    }
  }
</script>
<style lang="less" scoped>
  .submission-chart {
    width: 820px;
    height: 180px;
    background-color: #fff;
    margin: auto;
    margin-top: 20px;
    padding: 0px 0;
    font-size: 12px;

    .calendar {
      margin-left: 16px;
      margin-right: 16px;
      display: flex;

      .weeks {
        width: 30px;
        margin-right: 3px;
        margin-top: 22px;

        .week {
          margin-top: 13px;
          width: 30px;
          height: 14px;
        }
      }

      .column {
        width: 11px;
        margin-right: 3px;

        .title {
          width: 14px;
          height: 14px;
          margin-bottom: 8px;
          text-align: left;
          overflow: visible;
          white-space: nowrap;
        }

        .date-wrapper { 
          width: 11px;
          height: 11px;
          background: #EBEDF0;
          margin-bottom: 3px;

          .date {
            width: 11px;
            height: 11px;

            :hover {
              width: 13px;
              height: 13px;
            }
          }
        }
      }
    }
    
    .operation {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-top: 10px;

      .slider {
        display: flex;
        justify-content: center;
        align-items: center;
        width: 200px;

        .slider-desc {
          width: 11px;
          margin: 0 8px;
        }
      }

      .legend {
        display: flex;
        justify-content:center;
        align-items: center;

        .level-desc {
          margin-right: 6px;
          margin-left: 3px;
        }

        .level {
          margin-right: 3px;
          width: 11px;
          height: 11px;
        }

        .level-1 {
          background: #EBEDF0;
        }

        .level-2 {
          background: #C6E48B;
        }

        .level-3 {
          background: #7BC96F;
        }

        .level-4 {
          background: #239A3B;
        }

        .level-5 {
          background: #196127;
        }
      }
    }
  }
</style>

使用:

<submission-chart :profile="profile"/>