Browse Source

Merge branch 'manage-cicle' into online

liushengqiang 2 years ago
parent
commit
4969ea73fa

+ 10 - 1
src/router/routes-common.ts

@@ -272,11 +272,20 @@ export const rootRouter = [
     }
   },
   {
+    path: '/subject-echarts',
+    name: 'subject-echarts',
+    component: () => import('@/views/subject-echarts/index'),
+    meta: {
+      title: '声部图表'
+    }
+  },
+  {
     path: '/:pathMatch(.*)*',
     component: () => import('@/views/404'),
     meta: {
       title: '404',
       platform: paymentType
     }
-  }
+  },
+  
 ]

+ 32 - 0
src/views/subject-echarts/component/CircleProgress.module.less

@@ -0,0 +1,32 @@
+.circle {
+    transform: rotate(-90deg);
+}
+
+.circle-main-box {
+    position: relative;
+    display: block;
+    margin: 0 auto;
+}
+
+.count-num {
+    position: absolute;
+    left: 0;
+    top: 21Px;
+    align-items: center;
+    justify-content: center;
+    display: flex;
+    font-size: 22Px;
+    color: #333;
+    user-select: none;
+    width: 100%;
+    text-align: center;
+}
+.des{
+    position: absolute;
+    left: 0;
+    top: 47Px;
+    font-size: 12px;
+    color: #777;
+    width: 100%;
+    text-align: center;
+}

+ 131 - 0
src/views/subject-echarts/component/CircleProgress.tsx

@@ -0,0 +1,131 @@
+import { defineComponent } from 'vue'
+import styles from './CircleProgress.module.less'
+
+export default defineComponent({
+  name: 'CircleProgress',
+  data() {
+    return {
+      now: 0,
+      timer: null as any
+    }
+  },
+  props: {
+    // 进度值
+    value: {
+      type: Number,
+      default: 0
+    },
+    // 尺寸
+    size: {
+      type: Number,
+      default: 120
+    },
+    // 边框粗细
+    strokeWidth: {
+      type: Number,
+      default: 10
+    },
+    // 进度条颜色
+    color: {
+      type: String,
+      default: 'rgba(153,202,251,1)'
+    },
+    // 动画执行时间
+    duration: {
+      type: Number,
+      default: 1000
+    },
+    des:{
+        type: String,
+        default: '总达标率'
+    }
+  },
+  watch:{
+    value(value) {
+      this.now = 0
+      this.$nextTick(() => {
+        this.run()
+      })
+    }
+  },
+  computed: {
+    percentage() {
+      return this.value
+    },
+    countDown() {
+      return this.now
+    },
+    // 圆心x轴坐标
+    cx() {
+      return this.size / 2
+    },
+    // 圆心y轴坐标
+    cy() {
+      return this.size / 2
+    },
+    // 半径
+    radius() {
+      return (this.size - this.strokeWidth) / 2
+    },
+    // 圆周长
+    circumference() {
+      return 2 * Math.PI * this.radius
+    },
+    // 进度长度
+    progress() {
+      return (1 - this.now / 100) * this.circumference
+    }
+  },
+  mounted() {
+    this.run()
+  },
+  methods: {
+    run() {
+      if (this.value == 0) return
+      let t = this.duration / this.value
+      this.timer = setInterval(() => {
+        if (this.now >= this.value) {
+          this.now = this.value
+          return clearInterval(this.timer)
+        }
+        this.now++
+      }, t)
+    }
+  },
+  render() {
+    return (
+      <div class={styles['circle-main']}>
+        <div
+          class={styles['circle-main-box']}
+          style={[{ width: this.size + 'px', height: this.size + 'px' }]}
+        >
+          <svg width={this.size} height={this.size} class={styles['circle']}>
+            <circle
+              r={this.radius}
+              cx={this.cx}
+              cy={this.cy}
+              fill="transparent"
+              stroke="#FFE7DF"
+              stroke-width={this.strokeWidth}
+            />
+            <circle
+              r={this.radius}
+              cx={this.cx}
+              cy={this.cy}
+              fill="transparent"
+              stroke={this.color}
+              stroke-width={this.strokeWidth}
+              stroke-linecap="round"
+              stroke-dasharray={this.circumference}
+              stroke-dashoffset={this.progress}
+            />
+          </svg>
+          <span class={styles['count-num']}>
+            {this.countDown}<span class={styles.countType}>%</span>
+          </span>
+          <span class={styles.des}>{this.des}</span>
+        </div>
+      </div>
+    )
+  }
+})

BIN
src/views/subject-echarts/image/icon-backup.png


BIN
src/views/subject-echarts/image/icon-ensemble.png


+ 212 - 0
src/views/subject-echarts/index.module.less

@@ -0,0 +1,212 @@
+.subjectEcharts {
+    padding: 12px 12px 14px 12px;
+    font-family: 'DINA';
+}
+
+.container {
+    position: relative;
+    padding: 14px 12px;
+    border-radius: 10px;
+    background-color: #fff;
+    margin-bottom: 12px;
+}
+.emtry{
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translateX(-50%);
+    font-size: 14px;
+    color: #AAAAAA;
+}
+
+.head {
+    display: flex;
+    font-size: 15px;
+    color: #333;
+    padding-bottom: 14px;
+
+    .headLeft {
+        display: flex;
+        align-items: center;
+
+        .icon {
+            display: block;
+            width: 18px;
+            height: 18px;
+            margin-right: 8px;
+        }
+    }
+
+    .headRight {
+        margin-left: auto;
+        display: flex;
+        align-items: center;
+
+        &>div {
+            font-size: 10px;
+            border-radius: 12px;
+            border: 1px solid;
+            margin: 0 3px;
+            line-height: 18px;
+            width: 40px;
+            text-align: center;
+        }
+    }
+}
+.headLabelWrap{
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 10px;
+    font-size: 12px;
+    .headLabel{
+        margin: 0 6px;
+        display: flex;
+        align-items: center;
+    }
+    .headLabelDot{
+        width: 4px;
+        height: 4px;
+        border-radius: 50%;
+        margin-right: 3px;
+    }
+}
+
+.content {
+    display: flex;
+    align-items: center;
+    padding: 8px 2px;
+}
+
+.echartsMain {
+    margin: 0 -12px;
+    height: 202px;
+}
+
+.items {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    margin-left: 17px;
+    justify-content: space-between;
+
+    .item {
+        .itemNum {
+            display: flex;
+            justify-content: flex-end;
+            align-items: center;
+            font-weight: 400;
+            font-size: 22px;
+            color: #333;
+            margin-bottom: 3px;
+            white-space: nowrap;
+
+            .rect {
+                width: 8px;
+                height: 8px;
+                background: #FF8057;
+                border-radius: 2px;
+                margin-right: 6px;
+            }
+        }
+
+        .itemTitle {
+            font-size: 12px;
+            color: #777;
+            white-space: nowrap;
+        }
+    }
+
+    .line {
+        padding: 0 10px;
+        border-left: 1px solid;
+        border-right: 1px solid;
+        border-image: linear-gradient(to bottom, #f9f9f9, #eaeaea, #f9f9f9) 1;
+    }
+}
+
+.subjectWrap {
+    padding: 0;
+}
+
+.listItem {
+    position: relative;
+    display: flex;
+    align-items: center;
+    padding: 10px 15px 10px 27px;
+    border: 1px solid transparent;
+    &:not(:last-child) {
+        border-bottom: 1px dashed #EAEAEA;
+    }
+
+    .itemLeft {
+        border-right: 1px solid;
+        border-image: linear-gradient(to bottom, #f9f9f9, #eaeaea, #f9f9f9) 1;
+
+        .subjectName {
+            width: 54px;
+            font-size: 13px;
+            font-weight: 600;
+            color: #333;
+            line-height: 18px;
+            margin-bottom: 3px;
+            white-space: nowrap;
+        }
+
+        .subjectType {
+            font-size: 10px;
+            font-weight: 400;
+            color: #666;
+            line-height: 14px;
+        }
+    }
+
+    .dot {
+        position: absolute;
+        left: 15px;
+        top: 15px;
+        width: 4px;
+        height: 4px;
+        background: #AAAAAA;
+        border-radius: 50%;
+    }
+}
+
+.listItemActive {
+    background-color: rgba(255,128,87,0.09);
+    border: 1px solid #FF8057 !important;
+    border-radius: 10px;
+}
+
+.listitems {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    margin-left: 17px;
+    justify-content: space-between;
+
+    .item {
+
+        .itemNum {
+            display: flex;
+            justify-content: flex-end;
+            align-items: center;
+            font-size: 16px;
+            color: #333;
+            margin-bottom: 3px;
+            white-space: nowrap;
+        }
+
+        .itemTitle {
+            font-size: 10px;
+            color: #777;
+            white-space: nowrap;
+        }
+    }
+
+    .line {
+        border-left: 1px solid;
+        border-right: 1px solid;
+        border-image: linear-gradient(to bottom, #f9f9f9, #eaeaea, #f9f9f9) 1;
+    }
+}

+ 396 - 0
src/views/subject-echarts/index.tsx

@@ -0,0 +1,396 @@
+import { computed, defineComponent, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
+import styles from './index.module.less'
+import CircleProgress from './component/CircleProgress'
+import iconBackup from './image/icon-backup.png'
+import iconEnsemble from './image/icon-ensemble.png'
+
+import * as echarts from 'echarts'
+import { listenerMessage, postMessage, removeListenerMessage } from '@/helpers/native-message'
+
+type EChartsOption = echarts.EChartsOption
+interface ISubjectItem {
+  /** 声部名称 */
+  subjectName: string
+  /**达标率 */
+  practiceRate: number
+  /** 达标人数 */
+  passNum: number
+  /** 未达标人数 */
+  noPassNum: number
+  /** 非会员 */
+  noMemberNum: number
+}
+const symbolData: any = {
+  type: 'line',
+  symbol: 'circle',
+  symbolSize: 6,
+  triggerLineEvent: true
+}
+export default defineComponent({
+  name: 'subject-echarts',
+  setup() {
+    const activeData = reactive({
+      index: -1,
+      sum: {
+        /**达标率 */
+        practiceRate: 0,
+        /**达标人数 */
+        passNum: 0,
+        /**未达标人数 */
+        noPassNum: 0,
+        /**未会员 */
+        noMemberNum: 0
+      },
+      practiceThisWeeks: [] as ISubjectItem[],
+      colors: [
+        {
+          color: '#FF8057',
+          borderColor: 'rgba(255,128,87,0.5)',
+          text: '达标率',
+          select: true,
+          key: 'practiceRate'
+        },
+        {
+          color: '#2FC58D',
+          borderColor: 'rgba(47,197,141,0.5)',
+          text: '达标',
+          select: true,
+          key: 'passNum'
+        },
+        {
+          color: '#4A99FF',
+          borderColor: 'rgba(74,153,255,0.5)',
+          text: '未达标',
+          select: true,
+          key: 'noPassNum'
+        },
+        {
+          color: '#9884BA',
+          borderColor: 'rgba(152,132,186,0.5)',
+          text: '非会员',
+          select: true,
+          key: 'noMemberNum'
+        }
+      ]
+    })
+    const subjects = computed(() => {
+      return activeData.practiceThisWeeks.map((n) => n.subjectName)
+    })
+    let myChart: echarts.ECharts
+    const handleInit = (data: any) => {
+      const { content } = data
+      if (!content) return
+      if (myChart) {
+        myChart.dispose()
+      }
+      activeData.sum = content.sum || {}
+      activeData.practiceThisWeeks = content.practiceThisWeeks || []
+      const chartDom = document.getElementById('subjectEcharts')!
+      myChart = echarts.init(chartDom)
+      const option: EChartsOption = {
+        tooltip: {
+          trigger: 'axis',
+          showContent: false,
+          axisPointer: {
+            type: 'line',
+            lineStyle: {
+              width: 30,
+              type: 'solid',
+              opacity: 0.2,
+            },
+          }
+        },
+        grid: {
+          left: 8,
+          top: 5,
+          right: 5,
+          bottom: 5,
+          containLabel: true
+        },
+        legend: {
+          type: 'plain',
+          align: 'auto',
+          itemGap: 10,
+          itemWidth: 6,
+          itemStyle: {
+            opacity: 0
+          },
+          lineStyle: {
+            width: 0,
+            opacity: 0
+          },
+          textStyle: {
+            opacity: 0
+          }
+        },
+        xAxis: {
+          type: 'category',
+          axisLine: {
+            show: false
+          },
+          axisLabel: {
+            interval: 0,
+            color: '#333',
+            fontSize: 9
+          },
+          axisTick: {
+            alignWithLabel: true,
+            show: false
+          },
+          // boundaryGap: false,
+          data: subjects.value
+        },
+        yAxis: [
+          {
+            type: 'value',
+            position: 'left',
+            axisLine: {
+              show: false,
+              lineStyle: {
+                color: '#999'
+              }
+            },
+            splitLine: {
+              show: true, // 是否显示y轴分割线
+              lineStyle: {
+                color: ['rgba(249, 234, 220, 1)'] // 分隔线颜色。
+              }
+            },
+            min: 0,
+            splitNumber: 5
+          },
+          {
+            type: 'value',
+            position: 'right',
+            splitLine: {
+              show: true, // 是否显示y轴分割线
+              lineStyle: {
+                color: ['rgba(249, 234, 220, 1)'] // 分隔线颜色。
+              }
+            },
+            axisLine: {
+              show: false,
+              lineStyle: {
+                color: '#999'
+              }
+            },
+            interval: 20,
+            min: 0,
+            max: 100,
+            splitNumber: 5,
+            axisLabel: {
+              formatter: function (value: number, index: number) {
+                return value + '%'
+              }
+            }
+          }
+        ],
+        series: ['practiceRate', 'passNum', 'noPassNum', 'noMemberNum'].map(
+          (key: string, index: number) => {
+            return {
+              name: activeData.colors[index].text,
+              color: activeData.colors[index].color,
+              ...symbolData,
+              yAxisIndex: index === 0 ? 1 : 0,
+              data: activeData.practiceThisWeeks.map((n) => n[key])
+            }
+          }
+        )
+      }
+      let maxNum = Math.max(...(option.series as any[]).map((n) => n.data).flat())
+      let minNum = Math.min(...(option.series as any[]).map((n) => n.data).flat())
+      maxNum = Math.ceil(maxNum / 9.5) * 10
+      minNum = Math.floor(minNum / 12) * 10
+      ;(option.yAxis as any[])[0].interval = Math.ceil(Math.ceil(maxNum) / 5)
+      ;(option.yAxis as any[])[0].max = Math.ceil(Math.ceil(maxNum) / 5) * 5
+      option && myChart.setOption(option)
+      myChart.on('highlight', function (params: any) {
+        activeData.index = params.batch[0].dataIndex
+      })
+    }
+    const handleAction = (index: number) => {
+      activeData.index = index
+      myChart?.dispatchAction({
+        type: 'showTip',
+        seriesIndex: 0,
+        dataIndex: index
+      })
+    }
+    const changeLegend = (item: any) => {
+      myChart?.dispatchAction({
+        type: item.select ? 'legendUnSelect' : 'legendSelect',
+        // 图例名称
+        name: item.text
+      })
+      item.select = !item.select
+    }
+    const handleMock = () => {
+      // console.log('handleMock1232')
+      const resulst = {
+        sum: {
+          noMemberNum: Math.floor(Math.random() * 1000),
+          noPassNum: Math.floor(Math.random() * 1000),
+          passNum: Math.floor(Math.random() * 1000),
+          practiceRate: Math.ceil(Math.random() * 100)
+        },
+        practiceThisWeeks: new Array(Math.ceil(Math.random() * 9)).fill(1).map((n, i) => {
+          return {
+            /** 声部名称 */
+          subjectName: '声部' + (i + 1),
+          /**达标率 */
+          practiceRate: Math.ceil(Math.random() * 100),
+          /** 达标人数 */
+          passNum: Math.floor(Math.random() * 1000),
+          /** 未达标人数 */
+          noPassNum: Math.floor(Math.random() * 1000),
+          /** 非会员 */
+          noMemberNum: Math.floor(Math.random() * 1000)
+          }
+        })
+      }
+      console.log(resulst)
+      handleInit({ content: resulst })
+    }
+    onMounted(() => {
+      handleMock()
+      listenerMessage('setAccomanyEcharts', handleInit)
+      postMessage({
+        api: 'setAccomanyEcharts'
+      })
+    })
+    onBeforeUnmount(() => {
+      removeListenerMessage('setAccomanyEcharts', handleInit)
+    })
+    const goto = () => {
+      const url = location.origin + location.pathname + `#/exercise-record`
+      console.log('🚀 ~ url:', url)
+      postMessage({
+        api: 'openWebView',
+        content: {
+          url: url,
+          orientation: 1
+        }
+      })
+    }
+    return () => (
+      <div class={styles.subjectEcharts}>
+        <div class={[styles.container, styles.ensemble]}>
+          <div class={styles.head}>
+            <div
+              class={styles.headLeft}
+            >
+              <img class={styles.icon} src={iconEnsemble} />
+              <div>总体情况</div>
+            </div>
+          </div>
+
+          <div class={styles.content} onClick={() => goto()}>
+            <CircleProgress
+              value={Number(activeData.sum.practiceRate)}
+              size={80}
+              color="#FF8057"
+              strokeWidth={6}
+            />
+            <div class={styles.items}>
+              <div class={styles.item}>
+                <div class={styles.itemNum}>
+                  <span class={styles.rect}></span>
+                  <span style={{ color: '#FF8057' }}>{activeData.sum.passNum}</span>
+                </div>
+                <div class={styles.itemTitle}>达标人数</div>
+              </div>
+              <div class={[styles.item, styles.line]}>
+                <div class={styles.itemNum}>
+                  <span class={styles.rect} style={{ background: '#FFE7DF' }}></span>
+                  <span>{activeData.sum.noPassNum}</span>
+                </div>
+                <div class={styles.itemTitle}>未达标人数</div>
+              </div>
+              <div class={styles.item}>
+                <div class={styles.itemNum}>
+                  <span>{activeData.sum.noMemberNum}</span>
+                </div>
+                <div class={styles.itemTitle}>非会员人数</div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class={[styles.container, styles.ensemble]}>
+          <div class={styles.head}>
+            <div class={styles.headLeft}>
+              <img class={styles.icon} src={iconBackup} />
+              <div>声部情况</div>
+            </div>
+            <div class={styles.headRight}>
+              {activeData.colors.map((c) => (
+                <div
+                  style={
+                    c.select
+                      ? { color: '#fff', borderColor: c.color, backgroundColor: c.color }
+                      : { color: c.color, borderColor: c.borderColor }
+                  }
+                  onClick={() => changeLegend(c)}
+                >
+                  {c.text}
+                </div>
+              ))}
+            </div>
+          </div>
+          <div class={styles.headLabelWrap}>
+            {activeData.colors.map((c, cIndex) => {
+              return (
+                <div class={styles.headLabel} style={{ display: c.select ? '' : 'none' }}>
+                  <div
+                    class={styles.headLabelDot}
+                    style={{ background: c.color, color: c.color }}
+                  ></div>
+                  <div style={{ color: c.color }}>
+                    {activeData.sum[c.key] || 0}
+                    {cIndex === 0 ? '%' : '人'}
+                  </div>
+                </div>
+              )
+            })}
+          </div>
+
+          <div id="subjectEcharts" class={styles.echartsMain}></div>
+          {!activeData.practiceThisWeeks.length && <div class={styles.emtry}>暂无练习记录</div>}
+        </div>
+
+        <div class={[styles.container, styles.subjectWrap]}>
+          {activeData.practiceThisWeeks.map((item, index: number) => (
+            <div
+              class={[styles.listItem, index === activeData.index ? styles.listItemActive : '']}
+              onClick={() => handleAction(index)}
+            >
+              <div class={styles.dot}></div>
+              <div class={styles.itemLeft}>
+                <div class={styles.subjectName}>{item.subjectName}</div>
+                <div class={styles.subjectType}>声部</div>
+              </div>
+              <div class={styles.listitems}>
+                {activeData.colors.map((c, index: number) => {
+                  return (
+                    <div class={styles.item}>
+                      <div class={styles.itemNum}>
+                        <span style={{ color: c.color }}>
+                          {item[c.key]}
+                          {index === 0 ? '%' : ''}
+                        </span>
+                      </div>
+                      <div class={styles.itemTitle}>
+                        {c.text}
+                        {index === 0 ? '' : '人数'}
+                      </div>
+                    </div>
+                  )
+                })}
+              </div>
+            </div>
+          ))}
+        </div>
+      </div>
+    )
+  }
+})