clip-path绘制倾斜角裁剪的矩形占比条;基于svg实现仪表盘弧线占比图。

页面效果

切图


代码实现:

html 复制代码
<!--  庭审分布:右侧实时庭审信息 -->
<template>
  <div class="trial-info">
    <div class="first-title">实时庭审信息</div>
    <div class="title-decoration"></div>
    <div class="trial-info-content">
      <!-- 当前庭审情况 -->
      <div class="card">
        <div class="second-title">
          <span class="second-title__icon"></span>
          <span class="second-title__text">当前庭审情况</span>
        </div>
        <div class="total-wrap">
          <span class="total-value">{{ dataInfo.total }}</span>
          <span class="total-text">今日庭审总数</span>
        </div>
        <div class="bar-wrap">
          <span class="bar-item waiting" :style="{ width: getWidth('waiting') }"></span>
          <span class="bar-item inProgress" :style="{ width: getWidth('inProgress') }"></span>
          <span class="bar-item endEd" :style="{ width: getWidth('endEd') }"></span>
        </div>

        <div class="trial-status">
          <div class="trial-status-left">
            <span class="icon-ring"></span>
            <span class="status-text">等待中</span>
          </div>
          <div class="trial-status-right">
            <span class="status-value">{{ dataInfo.waiting }}</span>
            <span class="status-percent">{{ getWidth('waiting') }}</span>
          </div>
        </div>
        <div class="split-line"></div>
        <div class="trial-status">
          <div class="trial-status-left">
            <span class="icon-ring green"></span>
            <span class="status-text">进行中</span>
          </div>
          <div class="trial-status-right">
            <span class="status-value">{{ dataInfo.inProgress }}</span>
            <span class="status-percent">{{ getWidth('inProgress') }}</span>
          </div>
        </div>
        <div class="split-line"></div>
        <div class="trial-status">
          <div class="trial-status-left">
            <span class="icon-ring gray"></span>
            <span class="status-text">已结束</span>
          </div>
          <div class="trial-status-right">
            <span class="status-value">{{ dataInfo.endEd }}</span>
            <span class="status-percent">{{ getWidth('endEd') }}</span>
          </div>
        </div>
      </div>

      <div class="scheduling-saturation">
        <div class="second-title">
          <span class="second-title__icon"></span>
          <span class="second-title__text">当前排期饱和度</span>
        </div>
        <div class="scheduling-saturation-content">
          <div class="left-part">
            <div class="left-part__icon"></div>
            <div class="left-part__text">已排期</div>
            <div class="left-part__value">{{ schedulingData.scheduled.num }}</div>
            <div class="left-part__percent">{{ schedulingData.scheduled.percent }}%</div>
          </div>
          <div class="middle-part">
            <div class="middle-part-top">
              <svg width="124" height="62" viewBox="-2 -2 126 64" style="width: 100%;height: 100%;">
                <defs>
                  <linearGradient id="arcGradient1" x1="0%" y1="0%" x2="100%" y2="0%">
                    <stop offset="0%" stop-color="rgba(52, 68, 98, 0)" />
                    <stop offset="30%" stop-color="rgba(144, 182, 253, 1)" />
                    <stop offset="100%" stop-color="rgba(108, 163, 255, 1)" />
                  </linearGradient>
                  <linearGradient id="arcGradient2" x1="100%" y1="0%" x2="0%" y2="0%">
                    <stop offset="0%" stop-color="rgba(52, 68, 98, 0)" />
                    <stop offset="30%" stop-color="rgba(211, 84, 57, 1)" />
                    <stop offset="100%" stop-color="rgba(255, 124, 96, 1)" />
                  </linearGradient>
                </defs>
                <!-- 圆弧路径 -->
                <path id="arcPath1" :d="path1" fill="none" stroke="url(#arcGradient1)" stroke-width="2" />
                <path id="arcPath2" :d="path2" fill="none" stroke="url(#arcGradient2)" stroke-width="2" />
                <!-- 添加分割线 -->
                <path id="linePath" :d="splitLine" fill="none" stroke="white" stroke-width="2" />
              </svg>
            </div>
          </div>
          <div class="right-part">
            <div class="right-part__icon"></div>
            <div class="right-part__text">已排期</div>
            <div class="right-part__value">{{ schedulingData.notScheduled.num }}</div>
            <div class="right-part__percent">{{ schedulingData.notScheduled.percent }}%</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const dataInfo = ref({
  total: 0,
  waiting: 0,
  inProgress: 0,
  endEd: 0
})

setTimeout(() => {
  dataInfo.value = {
    total: 40,
    waiting: 25,
    inProgress: 10,
    endEd: 5
  }
}, 1000);

// 计算宽度
function getWidth(type) {
  if (dataInfo.value.total) {
    return ((dataInfo.value[type] / dataInfo.value.total) * 100).toFixed(0) + '%'
  } else {
    return '0%'
  }
}

const schedulingData = ref({
  total: 100,
  scheduled: {
    num: 60,
    percent: 60
  },
  notScheduled: {
    num: 40,
    percent: 40
  }
})

const path1 = ref('')
const path2 = ref('')
const splitLine = ref('')

function getData() {
  // 计算已排期部分对应的角度:已排期60%,对应半圆中的108度角
  const angle1 = schedulingData.value.scheduled.percent / 100 * 180
  /**
   * 实际效果:从左侧中点开始,顺时针向上绘制108度的圆弧
   * M 0,62:移动到起点(0,62) - 半圆最左侧中点
   * A 62,62 0 0,1:绘制圆弧
      62,62:x半径和y半径都是62
      0:旋转角度为0
      0:大弧标志为0(选择小弧):当给定起点、终点和半径后,理论上可以绘制出两个可能的圆弧(一个大弧和一个小弧)。大弧标志用于选择绘制哪一个。值为 0:表示选择小弧(较小的那个圆弧)。圆弧的角度小于或等于 180 度。值为 1:表示选择大弧(较大的那个圆弧)。圆弧的角度大于 180 度。
      1:顺时针方向绘制
   * ${62 - 62 * Math.cos(angle1 * Math.PI / 180)}:计算终点x坐标
      Math.cos(angle1 * Math.PI / 180):将角度转为弧度并计算余弦
      从圆心(62,62)向左偏移62*cos(角度)
   * ${62 - 62 * Math.sin(angle1 * Math.PI / 180)}:计算终点y坐标
      从圆心(62,62)向上偏移62*sin(角度)
   */
  path1.value = `M 0,62 A 62,62 0 0,1 ${62 - 62 * Math.cos(angle1 * Math.PI / 180)},${62 - 62 * Math.sin(angle1 * Math.PI / 180)}`

  // 计算未排期部分对应的角度:未排期40%,对应半圆中的72度角
  const angle2 = schedulingData.value.notScheduled.percent / 100 * 180 - 5
  /**
   * 实际效果:从右侧中点开始,逆时针向上绘制72度的圆弧
   * M 124,62:移动到起点(124,62) - 半圆最右侧中点
   * A 62,62 0 0,0:绘制圆弧
      0:大弧标志为0(选择小弧)
      0:逆时针方向绘制
   * ${62 + 62 * Math.cos(angle2 * Math.PI / 180)}:计算终点x坐标
      从圆心(62,62)向右偏移62*cos(角度)
   * ${62 - 62 * Math.sin(angle2 * Math.PI / 180)}:计算终点y坐标
      从圆心(62,62)向上偏移62*sin(角度)
   */
  path2.value = `M 124,62 A 62,62 0 0,0 ${62 + 62 * Math.cos(angle2 * Math.PI / 180)},${62 - 62 * Math.sin(angle2 * Math.PI / 180)}`

  // 计算分割线
  const updateSplitLine = () => {
    // 计算交汇点坐标(path1的终点,也是path2的起点)
    const intersectionX = 62 - 62 * Math.cos(angle1 * Math.PI / 180)
    const intersectionY = 62 - 62 * Math.sin(angle1 * Math.PI / 180)

    // 计算指向圆心的角度(法线方向)
    const centerX = 62
    const centerY = 62
    const angleToCenter = Math.atan2(centerY - intersectionY, centerX - intersectionX)

    // 计算分割线的两个端点(长度16px,垂直于圆弧)
    const lineLength = 16
    const halfLength = lineLength / 2

    // 计算分割线的起点和终点(沿着法线方向)
    const startX = intersectionX
    const startY = intersectionY
    const endX = intersectionX + lineLength * Math.cos(angleToCenter)
    const endY = intersectionY + lineLength * Math.sin(angleToCenter)

    // const startX = intersectionX - halfLength * Math.cos(angleToCenter)
    // const startY = intersectionY - halfLength * Math.sin(angleToCenter)
    // const endX = intersectionX + halfLength * Math.cos(angleToCenter)
    // const endY = intersectionY + halfLength * Math.sin(angleToCenter)

    splitLine.value = `M ${startX},${startY} L ${endX},${endY}`
  }

  // 更新分割线
  updateSplitLine()
}

getData()

setTimeout(() => {
  schedulingData.value = {
    total: 100,
    scheduled: {
      num: 30,
      percent: 30
    },
    notScheduled: {
      num: 70,
      percent: 70
    }
  }
  getData()
}, 2000);


</script>

<style lang="scss" scoped>
.trial-info {
  width: 348px;
  height: 466px;
  pointer-events: all;
  position: absolute;
  z-index: 4;
  top: 88px;
  right: 24px;
  border: 1.5px solid rgba(255, 255, 255, 0.1);
  border-radius: 12px;
  box-shadow: 0px 12px 24px 0px rgba(0, 0, 0, 0.3);
  background: radial-gradient(70.23% 49.93% at 94% 0%, rgba(116, 168, 255, 0.21), rgba(14, 63, 170, 0.16), rgba(16, 18, 27, 0))
    /* 警告:渐变使用了CSS不支持的旋转方式,可能无法按预期工作 */
    , radial-gradient(106.40% 87.66% at 0% 0%, rgba(204, 223, 255, 0.3), rgba(137, 164, 213, 0.11), rgba(35, 42, 59, 0))
    /* 警告:渐变使用了CSS不支持的旋转方式,可能无法按预期工作 */
    , rgba(35, 42, 59, 0.9);
  padding: 4px 12px 12px 12px;

  .first-title {
    width: 3.24rem;
    height: .32rem;
    background: url('~@/assets/images/BigScreen/panelPart/first-title-bg.png');
    background-size: 100% 100%;
    padding-left: .28rem;
    display: flex;
    align-items: center;

    color: rgba(255, 255, 255, 1);
    font-family: DingTalk JinBuTi;
    font-size: .16rem;
    font-weight: 400;
    line-height: .24rem;
  }

  .title-decoration {
    width: 3.24rem;
    height: .56rem;
    background: url('~@/assets/images/BigScreen/panelPart/title-decoration.png');
    background-size: 100% 100%;
  }

  &-content {
    width: 324px;
    height: 406px;
    position: absolute;
    left: 12px;
    top: 48px;

    .card {
      width: 100%;
      height: 230px;
      border-radius: 12px;
      background: rgba(0, 0, 0, 0.2);
      padding: 12px;
      margin-bottom: 12px;

      .total-wrap {
        height: 24px;
        display: flex;
        align-items: flex-end;
        margin-top: 16px;
        margin-bottom: 12px;

        .total-value {
          color: rgba(255, 255, 255, 1);
          font-family: Gilroy;
          font-size: 24px;
          font-weight: 400;
          line-height: 24px;
          margin-right: 8px;
        }

        .total-text {
          color: rgba(255, 255, 255, 0.7);
          font-family: Alibaba PuHuiTi;
          font-size: 14px;
          font-weight: 400;
          line-height: 20px;
        }
      }

      .bar-wrap {
        height: 4px;
        display: flex;

        .bar-item {
          height: 100%;
          background: linear-gradient(191.36deg, rgba(255, 255, 255, 1) -31.817%, rgba(93, 143, 255, 1) 53.805%);
          clip-path: polygon(0 0, 100% 0, calc(100% - 2px) 100%, 0 100%);
          transition: all 1.2s ease-out;

          &.inProgress {
            background: linear-gradient(23.81deg, rgba(205, 255, 231, 1) -0.562%, rgba(158, 255, 202, 1) 75.02%);
            transform: rotate(-180.00deg);
            clip-path: polygon(2px 0, 100% 0, calc(100% - 2px) 100%, 0 100%);
          }

          &.endEd {
            background: rgba(100, 111, 135, 1);
            clip-path: polygon(2px 0, 100% 0, 100% 100%, 0 100%);
          }
        }
      }

      .trial-status {
        height: 20px;
        margin-top: 12px;
        display: flex;
        justify-content: space-between;
        align-items: center;

        &-left {
          display: flex;
          align-items: center;

          .icon-ring {
            width: 8px;
            height: 8px;
            border: 1px solid rgba(93, 143, 255, 1);
            border-radius: 50%;
            margin-right: 8px;

            &.green {
              border: 1px solid rgba(158, 255, 202, 1);
            }

            &.gray {
              border: 1px solid rgba(100, 111, 135, 1);
            }
          }

          .status-text {
            color: rgba(255, 255, 255, 0.8);
            font-family: Alibaba PuHuiTi;
            font-size: 14px;
            font-weight: 400;
          }
        }

        &-right {
          display: flex;
          align-items: center;

          .status-value {
            color: rgba(255, 255, 255, 1);
            font-family: Gilroy;
            font-size: 16px;
            font-weight: 400;
          }

          .status-percent {
            width: 31px;
            color: rgba(255, 255, 255, 0.5);
            font-family: Gilroy;
            font-size: 16px;
            font-weight: 400;
            margin-left: 12px;
          }
        }
      }

      .split-line {
        height: 1px;
        background: rgba(255, 255, 255, 0.15);
        margin-top: 12px;
      }
    }

    .second-title {
      height: 24px;
      display: flex;
      align-items: center;

      &__icon {
        width: 24px;
        height: 24px;
        margin-right: 8px;
        background: url('~@/assets/images/BigScreen/panelPart/court-trial/icon-trial.png');
        background-size: 100% 100%;
      }

      &__text {
        color: rgba(255, 255, 255, 1);
        font-family: Alibaba PuHuiTi;
        font-size: 16px;
        font-weight: 400;
      }
    }

    .scheduling-saturation {
      height: 164px;
      padding: 12px;
      overflow: hidden;

      &-content {
        margin-top: 10px;
        height: calc(100% - 34px);
        display: flex;
        justify-content: space-between;

        .left-part,
        .right-part {
          width: 48px;
          height: 100%;

          &__icon {
            width: 48px;
            height: 53px;
            background: url('~@/assets/images/BigScreen/panelPart/court-trial/icon-scheduled.png');
            background-size: 100% 100%;
            margin-bottom: -6px;
          }

          &__text {
            color: rgba(255, 255, 255, 0.7);
            font-family: Alibaba PuHuiTi;
            font-size: 14px;
            font-weight: 400;
            line-height: 20px;
          }

          &__value {
            color: rgba(255, 255, 255, 1);
            font-family: Gilroy;
            font-size: 16px;
            font-weight: 400;
            line-height: 20px;
            margin-top: 2px;
          }

          &__percent {
            color: rgba(255, 255, 255, 0.5);
            font-family: Gilroy;
            font-size: 14px;
            font-weight: 400;
            line-height: 20px;
          }
        }

        .middle-part {
          width: 136px;
          height: 136px;
          background: url('~@/assets/images/BigScreen/panelPart/court-trial/dial-bg.png');
          background-size: 100% 100%;
          padding: 6px;
          margin-top: 8px;

          &-top {
            width: 100%;
            height: 50%;
          }

          #arcPath1,
          #arcPath2,
          #linePath {
            transition: all 0.5s ease-in-out;
          }
        }

        .right-part {

          &__text,
          &__value,
          &__percent {
            text-align: right;
          }
        }
      }
    }
  }
}
</style>
相关推荐
m0_738120721 小时前
渗透基础知识ctfshow——Web应用安全与防护(完结:第八章)
前端·python·sql·安全·web安全·网络安全
克里斯蒂亚诺更新1 小时前
uniapp适配H5和Android-apk实现获取当前位置经纬度并调用接口
android·前端·uni-app
宁&沉沦2 小时前
前端开发专用的 Cursor 四大模式「快捷切换 + 指令模板」,直接复制就能用,覆盖 90% 日常场景
前端·编辑器
Cloud Traveler2 小时前
用Calibre-Web把NAS上的电子书管起来:部署、配置与远程访问实战
前端
神明不懂浪漫2 小时前
【第一章】HTML(一)——HTML简述及常用标签
前端·javascript·css·html·css3
鹏程十八少2 小时前
5. 2026金三银四 吐血整理!Android高级UI 自定义view面试25题,覆盖90%大厂考点
前端·面试·前端框架
兄弟加油,别颓废了。2 小时前
XSS-Labs 前 5 关 超详细通关全解
前端·xss
telllong2 小时前
深入理解React Fiber架构:从栈调和到时间切片
前端·react.js·架构
英俊潇洒美少年2 小时前
React18 Hooks 项目重构为 Vue3 组合式API的坑
前端·javascript·重构