|
|
@@ -0,0 +1,208 @@
|
|
|
+<template>
|
|
|
+ <div v-if="show" class="pl-mask" @click.stop>
|
|
|
+ <div class="pl-card" @click.stop>
|
|
|
+ <div class="pl-body">
|
|
|
+ <div class="pl-ring-wrap" :style="{ width: ringPx + 'px', height: ringPx + 'px' }">
|
|
|
+ <svg class="pl-ring-svg" :viewBox="viewBox" aria-hidden="true">
|
|
|
+ <defs>
|
|
|
+ <linearGradient :id="progressGradientId" x1="12" y1="18" x2="88" y2="82" gradientUnits="userSpaceOnUse">
|
|
|
+ <stop offset="0%" class="pl-grad-stop pl-grad-stop--start" />
|
|
|
+ <stop offset="52%" class="pl-grad-stop pl-grad-stop--mid" />
|
|
|
+ <stop offset="100%" class="pl-grad-stop pl-grad-stop--end" />
|
|
|
+ </linearGradient>
|
|
|
+ </defs>
|
|
|
+ <circle class="pl-ring-track" :cx="cx" :cy="cy" :r="radius" fill="none" :stroke-width="strokeW" />
|
|
|
+ <circle
|
|
|
+ class="pl-ring-progress"
|
|
|
+ :cx="cx"
|
|
|
+ :cy="cy"
|
|
|
+ :r="radius"
|
|
|
+ fill="none"
|
|
|
+ :stroke="`url(#${progressGradientId})`"
|
|
|
+ :stroke-width="strokeW"
|
|
|
+ stroke-linecap="round"
|
|
|
+ :stroke-dasharray="dashArray"
|
|
|
+ :stroke-dashoffset="dashOffset"
|
|
|
+ :transform="`rotate(-90 ${cx} ${cy})`"
|
|
|
+ />
|
|
|
+ </svg>
|
|
|
+ <div class="pl-percent-wrap">
|
|
|
+ <span class="pl-percent">{{ displayPercent }}%</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <p class="pl-title">
|
|
|
+ {{ title }}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <div class="pl-divider" />
|
|
|
+ <button type="button" class="pl-cancel" @click="onCancel">
|
|
|
+ <span class="pl-cancel__text">{{ cancelText }}</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { computed } from "vue";
|
|
|
+
|
|
|
+/** 避免同页多个实例时 defs id 冲突 */
|
|
|
+const progressGradientId = `pl-ring-grad-${Math.random().toString(36).slice(2, 10)}`;
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ show: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ percent: {
|
|
|
+ type: Number,
|
|
|
+ default: 0
|
|
|
+ },
|
|
|
+ title: {
|
|
|
+ type: String,
|
|
|
+ default: "上传中"
|
|
|
+ },
|
|
|
+ cancelText: {
|
|
|
+ type: String,
|
|
|
+ default: "取消上传"
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+const emit = defineEmits(["cancel", "update:show"]);
|
|
|
+
|
|
|
+const displayPercent = computed(() => {
|
|
|
+ const n = Number(props.percent);
|
|
|
+ if (Number.isNaN(n)) return 0;
|
|
|
+ return Math.round(Math.min(100, Math.max(0, n)));
|
|
|
+});
|
|
|
+
|
|
|
+/** SVG 环形:与 Element 主题色对齐 */
|
|
|
+const vb = 100;
|
|
|
+const cx = vb / 2;
|
|
|
+const cy = vb / 2;
|
|
|
+const radius = 38;
|
|
|
+const strokeW = 7;
|
|
|
+const viewBox = `0 0 ${vb} ${vb}`;
|
|
|
+const ringPx = 118;
|
|
|
+
|
|
|
+const circumference = 2 * Math.PI * radius;
|
|
|
+
|
|
|
+const dashArray = computed(() => `${circumference} ${circumference}`);
|
|
|
+
|
|
|
+const dashOffset = computed(() => {
|
|
|
+ const p = displayPercent.value / 100;
|
|
|
+ return circumference * (1 - p);
|
|
|
+});
|
|
|
+
|
|
|
+function onCancel() {
|
|
|
+ emit("cancel");
|
|
|
+ emit("update:show", false);
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.pl-mask {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ z-index: 10000;
|
|
|
+ box-sizing: border-box;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 24px;
|
|
|
+ background: rgb(0 0 0 / 50%);
|
|
|
+}
|
|
|
+.pl-card {
|
|
|
+ box-sizing: border-box;
|
|
|
+ width: 100%;
|
|
|
+ max-width: 320px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: var(--el-bg-color, #ffffff);
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: var(--el-box-shadow, 0 12px 32px rgb(0 0 0 / 12%));
|
|
|
+}
|
|
|
+.pl-body {
|
|
|
+ box-sizing: border-box;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 32px 24px 24px;
|
|
|
+}
|
|
|
+.pl-ring-wrap {
|
|
|
+ position: relative;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.pl-ring-svg {
|
|
|
+ display: block;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+.pl-ring-track {
|
|
|
+ stroke: var(--el-color-primary-light-8, #e8ecfc);
|
|
|
+}
|
|
|
+.pl-ring-progress {
|
|
|
+ transition: stroke-dashoffset 0.35s ease;
|
|
|
+}
|
|
|
+.pl-grad-stop--start {
|
|
|
+ stop-color: var(--el-color-primary-light-5, #a3b9fc);
|
|
|
+}
|
|
|
+.pl-grad-stop--mid {
|
|
|
+ stop-color: var(--el-color-primary, #6c8ff8);
|
|
|
+}
|
|
|
+.pl-grad-stop--end {
|
|
|
+ stop-color: var(--el-color-primary-dark-2, #4a6fd6);
|
|
|
+}
|
|
|
+.pl-percent-wrap {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+.pl-percent {
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: 600;
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
+ line-height: 1.2;
|
|
|
+ color: var(--el-color-primary, #6c8ff8);
|
|
|
+}
|
|
|
+.pl-title {
|
|
|
+ margin: 20px 0 0;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 500;
|
|
|
+ line-height: 1.4;
|
|
|
+ color: var(--el-text-color-primary, #303133);
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.pl-divider {
|
|
|
+ height: 1px;
|
|
|
+ margin: 0;
|
|
|
+ background: var(--el-border-color-lighter, #ebeef5);
|
|
|
+}
|
|
|
+.pl-cancel {
|
|
|
+ box-sizing: border-box;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 100%;
|
|
|
+ padding: 14px 20px 16px;
|
|
|
+ margin: 0;
|
|
|
+ font: inherit;
|
|
|
+ cursor: pointer;
|
|
|
+ background: transparent;
|
|
|
+ border: none;
|
|
|
+ &:hover .pl-cancel__text {
|
|
|
+ color: var(--el-color-primary-light-3, #8aa6fa);
|
|
|
+ }
|
|
|
+ &:active .pl-cancel__text {
|
|
|
+ color: var(--el-color-primary-dark-2, #4a6fd6);
|
|
|
+ }
|
|
|
+}
|
|
|
+.pl-cancel__text {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 500;
|
|
|
+ line-height: 1.4;
|
|
|
+ color: var(--el-color-primary, #6c8ff8);
|
|
|
+ transition: color 0.15s ease;
|
|
|
+}
|
|
|
+</style>
|