This commit is contained in:
2026-08-08 18:29:00 +08:00
parent fcd797a1fd
commit 2c983e826d

View File

@@ -63,8 +63,33 @@
</el-form-item>
<el-form-item label="任务进度">
<div class="progress-input-wrap">
<el-slider v-model="form.progress" :min="0" :max="100" :step="5" show-input />
<!-- 海浪液位进度条:水位高度 = 进度值,浪尖从左往右涌动;点击/拖动轨道或键盘均可调节 -->
<div class="gauge-progress" :class="{ 'is-dragging': dragging, 'is-hovering': hovering }"
:style="{ '--progress-color': progressColor }">
<div ref="trackRef" class="gp-track" role="slider" tabindex="0" :aria-valuemin="0" :aria-valuemax="100"
:aria-valuenow="form.progress" @mousedown="onTrackMouseDown" @touchstart.prevent="onTrackTouchStart"
@mousemove="onTrackMouseMove" @mouseenter="hovering = true" @mouseleave="hovering = false"
@keydown.left.prevent="changeStep(-5)" @keydown.right.prevent="changeStep(5)"
@keydown.home.prevent="form.progress = 0" @keydown.end.prevent="form.progress = 100">
<div class="gp-clip">
<!-- 液体铺满全宽,水位随进度上涨 -->
<div class="gp-liquid" :class="{ 'is-empty': form.progress === 0 }" :style="{ height: form.progress + '%' }">
<div class="gp-sheen"></div>
<svg class="gp-waves gp-waves-back" viewBox="0 0 280 14" preserveAspectRatio="none" aria-hidden="true">
<path d="M0,7 C12,0 23,14 35,7 C47,0 58,14 70,7 C82,0 93,14 105,7 C117,0 128,14 140,7 C152,0 163,14 175,7 C187,0 198,14 210,7 C222,0 233,14 245,7 C257,0 268,14 280,7 L280,14 L0,14 Z"></path>
</svg>
<svg class="gp-waves gp-waves-front" viewBox="0 0 300 12" preserveAspectRatio="none" aria-hidden="true">
<path d="M0,6 C8,0 17,12 25,6 C33,0 42,12 50,6 C58,0 67,12 75,6 C83,0 92,12 100,6 C108,0 117,12 125,6 C133,0 142,12 150,6 C158,0 167,12 175,6 C183,0 192,12 200,6 C208,0 217,12 225,6 C233,0 242,12 250,6 C258,0 267,12 275,6 C283,0 292,12 300,6 L300,12 L0,12 Z"></path>
</svg>
</div>
</div>
<!-- 气泡跟随光标水平位置,悬停/拖动时显示 -->
<div class="gp-bubble" :style="{ left: bubbleX + '%' }">{{ form.progress }}%</div>
</div>
<div class="gp-meta">
<span class="gp-pct">{{ form.progress }}</span>
<span class="gp-stage">{{ progressStage }}</span>
</div>
</div>
</el-form-item>
@@ -131,6 +156,91 @@ const handleCustomerChange = () => {
form.operator_id = null;
};
// 任务进度颜色:按进度值取语义色;填充主色通过 CSS transition 平滑过渡,跨阶段变色无硬切
const progressColor = computed(() => {
const p = form.progress;
if (p >= 100) return "#10b981"; // 已完成 - 绿
if (p >= 75) return "#409eff"; // 快完成 - 蓝
if (p >= 40) return "#e6a23c"; // 进行中 - 橙
return "#f56c6c"; // 刚开始 - 红
});
// 进度阶段文案
const progressStage = computed(() => {
const p = form.progress;
if (p >= 100) return "已完成";
if (p >= 75) return "接近完成";
if (p >= 40) return "推进中";
return "刚开始";
});
// --- 可拖动进度条交互 ---
const dragging = ref(false);
const hovering = ref(false);
const bubbleX = ref(50); // 气泡跟随光标的水平位置(轨道内百分比)
const trackRef = ref(null);
const setProgressFromClientX = (clientX) => {
const el = trackRef.value;
if (!el) return;
const rect = el.getBoundingClientRect();
const ratio = (clientX - rect.left) / rect.width;
const pct = Math.min(100, Math.max(0, Math.round(ratio * 100)));
form.progress = pct;
};
// 气泡水平位置跟随光标,钳制在轨道两端以内避免溢出
const updateBubbleX = (clientX) => {
const el = trackRef.value;
if (!el) return;
const rect = el.getBoundingClientRect();
const ratio = (clientX - rect.left) / rect.width;
bubbleX.value = Math.min(94, Math.max(6, ratio * 100));
};
const onTrackMouseMove = (e) => updateBubbleX(e.clientX);
const onTrackMouseDown = (e) => {
e.preventDefault();
setProgressFromClientX(e.clientX);
updateBubbleX(e.clientX);
dragging.value = true;
const move = (ev) => {
setProgressFromClientX(ev.clientX);
updateBubbleX(ev.clientX);
};
const up = () => {
dragging.value = false;
window.removeEventListener("mousemove", move);
window.removeEventListener("mouseup", up);
};
window.addEventListener("mousemove", move);
window.addEventListener("mouseup", up);
};
const onTrackTouchStart = (e) => {
const touch = e.touches[0];
setProgressFromClientX(touch.clientX);
updateBubbleX(touch.clientX);
dragging.value = true;
const move = (ev) => {
ev.preventDefault();
setProgressFromClientX(ev.touches[0].clientX);
updateBubbleX(ev.touches[0].clientX);
};
const up = () => {
dragging.value = false;
window.removeEventListener("touchmove", move);
window.removeEventListener("touchend", up);
};
window.addEventListener("touchmove", move, { passive: false });
window.addEventListener("touchend", up);
};
const changeStep = (delta) => {
form.progress = Math.min(100, Math.max(0, form.progress + delta));
};
// 状态变为已完成(2)或已完结(3)时,自动将进度设为100
watch(
() => form.status,
@@ -283,8 +393,242 @@ const handleSubmit = async () => {
border-radius: var(--radius-sm);
}
.progress-input-wrap {
padding: 0 4px;
/* ===== 海浪液位进度条:水位随进度上涨,浪尖从左往右涌动 ===== */
.gauge-progress {
--progress-color: #4f46e5; /* 兜底色,实际由行内样式注入 */
display: flex;
align-items: center;
gap: 16px;
padding: 10px 0 6px;
width: 100%;
}
/* --- 轨道:玻璃胶囊舱 + 四分刻度线(刻度没入水中部分被液面覆盖) --- */
.gp-track {
position: relative;
flex: 1;
height: 26px;
margin: 0 16px;
border-radius: 999px;
cursor: pointer;
outline: none;
background-image:
linear-gradient(90deg, transparent calc(25% - 0.5px), rgba(15, 23, 42, 0.08) 25%, transparent calc(25% + 0.5px)),
linear-gradient(90deg, transparent calc(50% - 0.5px), rgba(15, 23, 42, 0.08) 50%, transparent calc(50% + 0.5px)),
linear-gradient(90deg, transparent calc(75% - 0.5px), rgba(15, 23, 42, 0.08) 75%, transparent calc(75% + 0.5px)),
linear-gradient(180deg, #e2e7f0 0%, #edf0f7 45%, #f5f7fb 100%);
box-shadow:
inset 0 2px 5px rgba(15, 23, 42, 0.14),
inset 0 -1px 0 rgba(255, 255, 255, 0.9),
0 1px 2px rgba(255, 255, 255, 0.7);
transition: box-shadow 0.2s ease;
}
.gp-track:hover {
box-shadow:
inset 0 2px 6px rgba(15, 23, 42, 0.18),
inset 0 -1px 0 rgba(255, 255, 255, 0.9),
0 1px 2px rgba(255, 255, 255, 0.7);
}
.gp-track:focus-visible {
box-shadow:
0 0 0 3px color-mix(in srgb, var(--progress-color) 32%, transparent),
inset 0 2px 5px rgba(15, 23, 42, 0.14);
}
/* 裁剪层:液体与海浪只允许出现在胶囊舱内(气泡在裁剪层之外,不被裁掉) */
.gp-clip {
position: absolute;
inset: 0;
border-radius: 999px;
overflow: hidden;
}
/* --- 液体:铺满全宽,水位高度 = 进度值 --- */
.gp-liquid {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: var(--progress-color); /* 主色可过渡:跨阶段变色平滑无硬切 */
background-image:
linear-gradient(180deg, rgba(255, 255, 255, 0.30) 0%, rgba(255, 255, 255, 0) 45%), /* 近水面提亮 */
linear-gradient(0deg, rgba(0, 0, 0, 0.14) 0%, rgba(0, 0, 0, 0) 40%), /* 水深处压暗 */
linear-gradient(90deg, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0) 40%); /* 左浅右深 → 颜色渐变 */
box-shadow: 0 3px 12px -2px color-mix(in srgb, var(--progress-color) 55%, transparent); /* 底部同色系辉光 */
transition: height 0.22s ease, background-color 0.35s ease, box-shadow 0.35s ease;
}
/* 进度为 0 时隐藏水体 */
.gp-liquid.is-empty {
display: none;
}
/* 流光扫过:一道斜向高光周期性掠过水体 */
.gp-sheen {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.gp-sheen::before {
content: "";
position: absolute;
top: -25%;
bottom: -25%;
left: -45%;
width: 34%;
background: linear-gradient(105deg, transparent 0%, rgba(255, 255, 255, 0.45) 50%, transparent 100%);
transform: skewX(-12deg);
animation: sheenSweep 3.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes sheenSweep {
0% { left: -45%; }
45% { left: 120%; }
100% { left: 120%; }
}
/* --- 海浪:前后两层浪尖横跨液面,视差涌动(从左往右) --- */
.gp-waves {
position: absolute;
left: 0;
width: 200%; /* 两个浪形周期,平移 50% 即可无缝循环 */
pointer-events: none;
animation: waveDriftX linear infinite;
}
.gp-waves path {
transition: fill 0.35s ease;
}
/* 后层涌浪:主色半透明,幅面更高、流动更慢(约 2 个浪峰横跨轨道) */
.gp-waves-back {
top: -8px;
height: 14px;
opacity: 0.55;
animation-duration: 5s;
animation-delay: -1.6s; /* 错开相位 */
}
.gp-waves-back path {
fill: var(--progress-color);
}
/* 前层浪花:主色调白的泡沫色,贴着液面、流动更快(约 3 个浪峰横跨轨道) */
.gp-waves-front {
top: -7px;
height: 12px;
animation-duration: 3.2s;
}
.gp-waves-front path {
fill: color-mix(in srgb, var(--progress-color) 35%, #ffffff);
}
@keyframes waveDriftX {
from { transform: translateX(-50%); }
to { transform: translateX(0); }
}
/* 拖动时:水位即时响应(无过渡延迟),海浪加速涌动 */
.gauge-progress.is-dragging .gp-liquid {
transition: none;
}
.gauge-progress.is-dragging .gp-waves-front {
animation-duration: 1.6s;
}
.gauge-progress.is-dragging .gp-waves-back {
animation-duration: 2.6s;
}
/* --- 气泡:跟随光标的弹性提示 --- */
.gp-bubble {
position: absolute;
bottom: calc(100% + 8px);
transform: translateX(-50%) translateY(6px) scale(0.9);
transform-origin: bottom center;
background: var(--progress-color);
color: #fff;
font-size: 12px;
font-weight: 700;
line-height: 1;
padding: 6px 10px;
border-radius: 7px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
box-shadow: 0 6px 16px -4px color-mix(in srgb, var(--progress-color) 65%, transparent);
transition: opacity 0.18s ease, transform 0.22s cubic-bezier(0.34, 1.56, 0.64, 1), background-color 0.35s ease, left 0.08s linear;
}
.gp-bubble::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: var(--progress-color);
transition: border-top-color 0.35s ease;
}
.gauge-progress.is-hovering .gp-bubble,
.gauge-progress.is-dragging .gp-bubble {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
/* --- 读数与阶段徽标 --- */
.gp-meta {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
min-width: 118px;
}
.gp-pct {
font-family: "Bahnschrift", "DIN Alternate", "Segoe UI", sans-serif;
font-size: 27px;
font-weight: 600;
font-variant-numeric: tabular-nums;
letter-spacing: -0.5px;
line-height: 1;
color: var(--text-primary, #1f2937);
}
.gp-pct::after {
content: "%";
font-size: 13px;
font-weight: 600;
color: var(--text-muted, #9ca3af);
margin-left: 2px;
}
.gp-stage {
font-size: 12px;
font-weight: 600;
line-height: 1;
padding: 5px 10px;
border-radius: 999px;
white-space: nowrap;
color: var(--progress-color);
background: color-mix(in srgb, var(--progress-color) 10%, #ffffff);
border: 1px solid color-mix(in srgb, var(--progress-color) 32%, transparent);
transition: color 0.35s ease, background-color 0.35s ease, border-color 0.35s ease;
}
/* 尊重系统“减弱动态效果”设置 */
@media (prefers-reduced-motion: reduce) {
.gp-waves,
.gp-sheen::before {
animation: none;
}
}
:deep(.el-form-item__label) {