770 lines
17 KiB
Vue
770 lines
17 KiB
Vue
<template>
|
||
<view class="app-update-page">
|
||
<app-top-push-notice />
|
||
<view class="tabback">
|
||
<view class="rtjt" @tap="goBack">←</view>
|
||
<view class="name">{{ $i18n.t('softwareUpdate') }}</view>
|
||
<view class="tabback-placeholder" />
|
||
</view>
|
||
<view class="tabback-spacer" />
|
||
|
||
<scroll-view class="main-scroll" scroll-y :show-scrollbar="false">
|
||
<view class="main-pad">
|
||
<view class="panel">
|
||
<view class="ios-head">
|
||
<view class="ios-icon-wrap">
|
||
<image
|
||
class="ios-icon"
|
||
src="/static/newlogo.png"
|
||
mode="aspectFill"
|
||
/>
|
||
</view>
|
||
<view class="ios-head-main">
|
||
<text class="ios-title">{{ headlineTitle }}</text>
|
||
<text class="ios-subtitle">{{ headlineSubtitle }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<rich-text
|
||
v-if="updateContentHtml"
|
||
class="ios-body-rich"
|
||
:nodes="updateContentHtml"
|
||
/>
|
||
<text v-else class="ios-body preline">{{ mainBodyText }}</text>
|
||
</view>
|
||
<view class="scroll-tail" />
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view class="footer-bar">
|
||
<text class="footer-version">
|
||
{{ $i18n.t('appUpdateCurrentVersionLabel') }}
|
||
<text class="footer-version-num">{{ currentVersionDisplay }}</text>
|
||
</text>
|
||
<view
|
||
class="footer-btn"
|
||
:class="{ 'footer-btn--disabled': footerDisabled }"
|
||
@tap="onFooterClick"
|
||
>
|
||
<text class="footer-btn-text">{{ footerBtnText }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 下载进度(真实进度,完成后直接调起安装,无「完成」提示) -->
|
||
<view v-if="updateProgressVisible" class="progress-mask" @tap.stop="">
|
||
<view class="progress-card" @tap.stop="">
|
||
<view class="progress-card-glow" />
|
||
<view class="progress-top">
|
||
<view class="progress-pct-huge">
|
||
<text class="pct-value">{{ updateProgress }}</text>
|
||
<text class="pct-unit">%</text>
|
||
</view>
|
||
<text class="progress-headline">{{ $i18n.t('appUpdateDownloading') }}</text>
|
||
<text class="progress-stage">{{ $i18n.t('appUpdateStageDownload') }}</text>
|
||
</view>
|
||
|
||
<view class="progress-track-shell">
|
||
<view class="progress-track-bg">
|
||
<view class="progress-fill" :style="{ width: progressWidth }">
|
||
<view class="progress-shimmer" />
|
||
<view class="progress-fill-head" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<text class="progress-hint">{{ $i18n.t('appUpdateInProgress') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
getCurrentAppVersionInfo,
|
||
pickPackageSizeLabel,
|
||
extractLatestApkRecord,
|
||
normalizeVersionString,
|
||
pickDownloadUrl
|
||
} from '@/common/utils/appVersion.js'
|
||
import {
|
||
readGlobalVersionFromStorage,
|
||
syncGlobalVersionToLocalStorage
|
||
} from '@/common/config/appVersionConfig.js'
|
||
|
||
export default {
|
||
name: 'AppUpdatePage',
|
||
data() {
|
||
return {
|
||
hasUpdate: false,
|
||
currentVersion: '',
|
||
newVersionName: '',
|
||
newVersionIntro: '',
|
||
updateContentHtml: '',
|
||
downloadUrl: '',
|
||
packageSizeLabel: '',
|
||
updateProgressVisible: false,
|
||
updateProgress: 0,
|
||
updateRunning: false,
|
||
_downloadTask: null
|
||
}
|
||
},
|
||
computed: {
|
||
progressWidth() {
|
||
return `${Math.min(100, Math.max(0, this.updateProgress))}%`
|
||
},
|
||
headlineTitle() {
|
||
const brand = this.$i18n.t('appBrandName')
|
||
if (this.hasUpdate && this.newVersionName) {
|
||
return `${brand} ${this.newVersionName}`
|
||
}
|
||
return `${brand} ${this.currentVersion}`
|
||
},
|
||
headlineSubtitle() {
|
||
if (this.hasUpdate) {
|
||
return (
|
||
this.packageSizeLabel ||
|
||
this.$i18n.t('appUpdatePackageSizeFallback')
|
||
)
|
||
}
|
||
return this.$i18n.t('appUpdateAlreadyLatest')
|
||
},
|
||
mainBodyText() {
|
||
if (this.updateContentHtml) {
|
||
return ''
|
||
}
|
||
if (this.hasUpdate) {
|
||
return (
|
||
this.newVersionIntro ||
|
||
this.$i18n.t('appUpdateDefaultNewIntro')
|
||
)
|
||
}
|
||
return (
|
||
this.$i18n.t('appUpdateHeroLatestHint') +
|
||
'\n\n' +
|
||
this.$i18n.t('appUpdateCurrentOptimizations') +
|
||
'\n' +
|
||
this.$i18n.t('appUpdateCurrentBuildNotes')
|
||
)
|
||
},
|
||
footerDisabled() {
|
||
return !this.hasUpdate || this.updateRunning
|
||
},
|
||
footerBtnText() {
|
||
if (this.updateRunning) return this.$i18n.t('appUpdateInProgress')
|
||
return this.hasUpdate
|
||
? this.$i18n.t('appUpdateNow')
|
||
: this.$i18n.t('appUpdateBtnAlreadyUpdated')
|
||
},
|
||
/** 底部展示的当前版本(与接口对比用的本地版本一致) */
|
||
currentVersionDisplay() {
|
||
const v = (this.currentVersion || '').trim()
|
||
return v || '—'
|
||
}
|
||
},
|
||
onShow() {
|
||
this.refreshVersionState()
|
||
},
|
||
mounted() {
|
||
uni.$on('languageChanged', this.onLang)
|
||
},
|
||
beforeDestroy() {
|
||
uni.$off('languageChanged', this.onLang)
|
||
this.abortActiveDownload()
|
||
},
|
||
methods: {
|
||
onLang() {
|
||
this.$forceUpdate()
|
||
},
|
||
goBack() {
|
||
if (this.updateRunning) return
|
||
uni.navigateBack()
|
||
},
|
||
applyPendingSource(pending) {
|
||
if (!pending || typeof pending !== 'object') return
|
||
const size = pickPackageSizeLabel(pending)
|
||
this.packageSizeLabel = size || this.$i18n.t('appUpdatePackageSizeFallback')
|
||
this.newVersionName =
|
||
pending.versionName != null && pending.versionName !== ''
|
||
? String(pending.versionName)
|
||
: pending.versionCode != null && Number(pending.versionCode) > 0
|
||
? String(pending.versionCode)
|
||
: ''
|
||
const intro =
|
||
pending.changelog != null && String(pending.changelog).trim() !== ''
|
||
? String(pending.changelog).trim()
|
||
: this.$i18n.t('appUpdateDefaultNewIntro')
|
||
this.newVersionIntro = intro
|
||
this.updateContentHtml =
|
||
pending.updateContentHtml != null &&
|
||
String(pending.updateContentHtml).trim() !== ''
|
||
? String(pending.updateContentHtml).trim()
|
||
: ''
|
||
this.downloadUrl = pickDownloadUrl(pending)
|
||
},
|
||
clearNoUpdateUi() {
|
||
this.hasUpdate = false
|
||
this.newVersionName = ''
|
||
this.newVersionIntro = ''
|
||
this.updateContentHtml = ''
|
||
this.downloadUrl = ''
|
||
this.packageSizeLabel = ''
|
||
},
|
||
/** 请求最新包信息:接口 `version` 与当前安装包版本一致则已是最新,否则可更新 */
|
||
async refreshVersionState() {
|
||
// 对比必须以「当前运行包」为准(plus.runtime / 小程序线上版),不能用本地缓存优先:
|
||
// 否则装新 APK 后若 APP_GLOBAL_VERSION_INFO 仍为旧值,会一直误判可更新。
|
||
syncGlobalVersionToLocalStorage()
|
||
const runtimeInfo = getCurrentAppVersionInfo()
|
||
const localVer = normalizeVersionString(runtimeInfo.versionName)
|
||
const storedFallback = normalizeVersionString(readGlobalVersionFromStorage() || '')
|
||
this.currentVersion = localVer || storedFallback
|
||
try {
|
||
const res = await this.$http.get('/app/apkVersion/latest')
|
||
if (!res || Number(res.code) !== 200) {
|
||
this.clearNoUpdateUi()
|
||
return
|
||
}
|
||
const record = extractLatestApkRecord(res)
|
||
if (!record || !record.versionName) {
|
||
this.clearNoUpdateUi()
|
||
return
|
||
}
|
||
const remoteVer = normalizeVersionString(record.versionName)
|
||
if (remoteVer === localVer) {
|
||
this.hasUpdate = false
|
||
this.newVersionName = ''
|
||
this.newVersionIntro = ''
|
||
this.downloadUrl = ''
|
||
this.packageSizeLabel = pickPackageSizeLabel(record) || ''
|
||
const html =
|
||
record.updateContentHtml != null &&
|
||
String(record.updateContentHtml).trim() !== ''
|
||
? String(record.updateContentHtml).trim()
|
||
: ''
|
||
this.updateContentHtml = html
|
||
return
|
||
}
|
||
this.hasUpdate = true
|
||
this.applyPendingSource(record)
|
||
} catch (e) {
|
||
console.warn('[app-update] apkVersion/latest', e)
|
||
this.clearNoUpdateUi()
|
||
}
|
||
},
|
||
abortActiveDownload() {
|
||
if (this._downloadTask && typeof this._downloadTask.abort === 'function') {
|
||
try {
|
||
this._downloadTask.abort()
|
||
} catch (e) {}
|
||
}
|
||
this._downloadTask = null
|
||
},
|
||
_finishDownloadFailed() {
|
||
this.updateProgressVisible = false
|
||
this.updateRunning = false
|
||
this.updateProgress = 0
|
||
this._downloadTask = null
|
||
uni.showToast({
|
||
title: this.$i18n.t('appUpdateDownloadFailed'),
|
||
icon: 'none',
|
||
duration: 2500
|
||
})
|
||
},
|
||
/** Android App:真实下载进度;完成后直接调起安装,无完成 Toast */
|
||
startAndroidApkDownload(url) {
|
||
this.abortActiveDownload()
|
||
this.updateRunning = true
|
||
this.updateProgressVisible = true
|
||
this.updateProgress = 0
|
||
const task = uni.downloadFile({
|
||
url,
|
||
success: (res) => {
|
||
this._downloadTask = null
|
||
if (res.statusCode !== 200 || !res.tempFilePath) {
|
||
this._finishDownloadFailed()
|
||
return
|
||
}
|
||
this.updateProgress = 100
|
||
this.updateProgressVisible = false
|
||
this.updateRunning = false
|
||
// #ifdef APP-PLUS
|
||
try {
|
||
plus.runtime.install(
|
||
res.tempFilePath,
|
||
{},
|
||
() => {},
|
||
(e) => {
|
||
const msg =
|
||
e && e.message
|
||
? e.message
|
||
: this.$i18n.t('appUpdateInstallFailed')
|
||
uni.showToast({ title: msg, icon: 'none', duration: 3000 })
|
||
}
|
||
)
|
||
} catch (err) {
|
||
uni.showToast({
|
||
title: this.$i18n.t('appUpdateInstallFailed'),
|
||
icon: 'none',
|
||
duration: 2500
|
||
})
|
||
}
|
||
// #endif
|
||
},
|
||
fail: () => {
|
||
this._finishDownloadFailed()
|
||
}
|
||
})
|
||
this._downloadTask = task
|
||
if (task && typeof task.onProgressUpdate === 'function') {
|
||
task.onProgressUpdate((r) => {
|
||
const p =
|
||
typeof r.progress === 'number'
|
||
? r.progress
|
||
: r.totalBytesExpectedToWrite > 0
|
||
? Math.round(
|
||
(100 * r.totalBytesWritten) /
|
||
r.totalBytesExpectedToWrite
|
||
)
|
||
: 0
|
||
this.updateProgress = Math.min(100, Math.max(0, Math.round(p)))
|
||
})
|
||
}
|
||
},
|
||
startUpdateFlow() {
|
||
if (this.updateRunning) return
|
||
const url = (this.downloadUrl || '').trim()
|
||
if (!url) {
|
||
uni.showToast({
|
||
title: this.$i18n.t('appUpdateNoDownloadUrl'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
// #ifdef APP-PLUS
|
||
try {
|
||
if (uni.getSystemInfoSync().platform === 'android') {
|
||
this.startAndroidApkDownload(url)
|
||
return
|
||
}
|
||
} catch (e) {}
|
||
// #endif
|
||
this.openExternal(url)
|
||
},
|
||
openExternal(url) {
|
||
const u = String(url).trim()
|
||
if (!u) return
|
||
// #ifdef APP-PLUS
|
||
try {
|
||
plus.runtime.openURL(u)
|
||
} catch (e) {
|
||
uni.showToast({ title: this.$i18n.t('appUpdateNoDownloadUrl'), icon: 'none' })
|
||
}
|
||
// #endif
|
||
// #ifdef H5
|
||
try {
|
||
if (typeof window !== 'undefined') window.open(u, '_blank')
|
||
} catch (e) {
|
||
uni.showToast({ title: this.$i18n.t('appUpdateNoDownloadUrl'), icon: 'none' })
|
||
}
|
||
// #endif
|
||
// #ifdef MP-WEIXIN
|
||
uni.setClipboardData({
|
||
data: u,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: this.$i18n.t('appUpdateLinkCopied'),
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
// #endif
|
||
},
|
||
onFooterClick() {
|
||
if (!this.hasUpdate || this.updateRunning) return
|
||
this.startUpdateFlow()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
/* 黑白灰 + 主题色 #3996FD:字阶与对比统一 */
|
||
$theme: #3996fd;
|
||
$ink: #0a0a0a;
|
||
$ink-strong: #141414;
|
||
$ink-body: #3a3a3a;
|
||
$ink-muted: #6b6b6b;
|
||
$ink-sub: #8c8c8c;
|
||
$ink-hint: #a3a3a3;
|
||
$page-bg: #f0f0f0;
|
||
$surface: #ffffff;
|
||
$line: #e8e8e8;
|
||
|
||
.app-update-page {
|
||
min-height: 100vh;
|
||
background-color: $page-bg;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.tabback {
|
||
width: 750rpx;
|
||
height: 204rpx;
|
||
background: $surface;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 28rpx;
|
||
padding-top: 52rpx;
|
||
box-sizing: border-box;
|
||
border-bottom: 1px solid $line;
|
||
z-index: 999;
|
||
|
||
.name {
|
||
font-size: 34rpx;
|
||
color: $ink-strong;
|
||
font-weight: 600;
|
||
letter-spacing: -0.5rpx;
|
||
}
|
||
|
||
.rtjt {
|
||
font-size: 38rpx;
|
||
color: $ink-strong;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.tabback-placeholder {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
}
|
||
}
|
||
|
||
.tabback-spacer {
|
||
width: 100%;
|
||
height: 204rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.main-scroll {
|
||
flex: 1;
|
||
height: 0;
|
||
min-height: 0;
|
||
margin-top: 30rpx;
|
||
}
|
||
|
||
.main-pad {
|
||
padding: 0 32rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.panel {
|
||
background: $surface;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx 28rpx 36rpx;
|
||
box-sizing: border-box;
|
||
border: 1rpx solid $line;
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.scroll-tail {
|
||
height: calc(200rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.ios-head {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.ios-icon-wrap {
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
border-radius: 28rpx;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
background: #ebebeb;
|
||
border: 1rpx solid $line;
|
||
}
|
||
|
||
.ios-icon {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.ios-head-main {
|
||
flex: 1;
|
||
margin-left: 24rpx;
|
||
min-width: 0;
|
||
padding-top: 4rpx;
|
||
}
|
||
|
||
.ios-title {
|
||
display: block;
|
||
font-size: 38rpx;
|
||
font-weight: 700;
|
||
color: $ink;
|
||
line-height: 1.2;
|
||
letter-spacing: -0.8rpx;
|
||
}
|
||
|
||
.ios-subtitle {
|
||
display: block;
|
||
margin-top: 12rpx;
|
||
font-size: 26rpx;
|
||
font-weight: 400;
|
||
color: $ink-muted;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.ios-body {
|
||
display: block;
|
||
margin-top: 36rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 400;
|
||
color: $ink-body;
|
||
line-height: 1.7;
|
||
text-align: left;
|
||
}
|
||
|
||
.preline {
|
||
white-space: pre-line;
|
||
}
|
||
|
||
/* rich-text 与正文对齐;内部标签字号由 HTML 决定时可被 rich-text 默认样式覆盖 */
|
||
.ios-body-rich {
|
||
display: block;
|
||
margin-top: 36rpx;
|
||
font-size: 28rpx;
|
||
color: $ink-body;
|
||
line-height: 1.7;
|
||
text-align: left;
|
||
word-break: break-word;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.footer-bar {
|
||
flex-shrink: 0;
|
||
padding: 16rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
|
||
background: $surface;
|
||
border-top: 1rpx solid $line;
|
||
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.04);
|
||
z-index: 998;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.footer-version {
|
||
display: block;
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
font-weight: 400;
|
||
color: $ink-muted;
|
||
line-height: 1.5;
|
||
margin-bottom: 16rpx;
|
||
letter-spacing: 0.2rpx;
|
||
}
|
||
|
||
.footer-version-num {
|
||
font-weight: 600;
|
||
color: $ink-strong;
|
||
margin-left: 8rpx;
|
||
}
|
||
|
||
.footer-btn {
|
||
height: 96rpx;
|
||
border-radius: 48rpx;
|
||
background: $ink;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.footer-btn--disabled {
|
||
background: #d0d0d0;
|
||
}
|
||
|
||
.footer-btn-text {
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
letter-spacing: 0.5rpx;
|
||
}
|
||
|
||
.footer-btn--disabled .footer-btn-text {
|
||
color: #ffffff;
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.progress-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 2000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 48rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.progress-card {
|
||
position: relative;
|
||
width: 100%;
|
||
max-width: 620rpx;
|
||
overflow: hidden;
|
||
border-radius: 28rpx;
|
||
padding: 52rpx 40rpx 44rpx;
|
||
box-sizing: border-box;
|
||
background: $surface;
|
||
border: 1rpx solid $line;
|
||
box-shadow: 0 24rpx 64rpx rgba(0, 0, 0, 0.18);
|
||
}
|
||
|
||
.progress-card-glow {
|
||
position: absolute;
|
||
top: -100rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 400rpx;
|
||
height: 200rpx;
|
||
background: radial-gradient(
|
||
ellipse at center,
|
||
rgba(57, 150, 253, 0.12) 0%,
|
||
transparent 65%
|
||
);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.progress-top {
|
||
position: relative;
|
||
z-index: 1;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.progress-pct-huge {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.pct-value {
|
||
font-size: 88rpx;
|
||
font-weight: 700;
|
||
color: $ink;
|
||
line-height: 0.9;
|
||
letter-spacing: -3rpx;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.pct-unit {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: $ink-sub;
|
||
line-height: 1.2;
|
||
margin-left: 6rpx;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.progress-headline {
|
||
display: block;
|
||
text-align: center;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: $ink-strong;
|
||
letter-spacing: -0.3rpx;
|
||
}
|
||
|
||
.progress-stage {
|
||
display: block;
|
||
text-align: center;
|
||
margin-top: 14rpx;
|
||
font-size: 24rpx;
|
||
color: $ink-muted;
|
||
line-height: 1.5;
|
||
min-height: 40rpx;
|
||
}
|
||
|
||
.progress-track-shell {
|
||
position: relative;
|
||
z-index: 1;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.progress-track-bg {
|
||
position: relative;
|
||
height: 20rpx;
|
||
border-radius: 10rpx;
|
||
background: #e3e3e3;
|
||
box-shadow: inset 0 2rpx 5rpx rgba(0, 0, 0, 0.08);
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.progress-fill {
|
||
position: relative;
|
||
height: 100%;
|
||
border-radius: 10rpx;
|
||
background: linear-gradient(90deg, #5ba3fc 0%, $theme 45%, #2b7de0 100%);
|
||
box-shadow:
|
||
0 0 16rpx rgba(57, 150, 253, 0.35),
|
||
inset 0 1rpx 0 rgba(255, 255, 255, 0.25);
|
||
transition: width 0.1s linear, background 0.35s ease, box-shadow 0.35s ease;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-shimmer {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
bottom: 0;
|
||
width: 55%;
|
||
background: linear-gradient(
|
||
90deg,
|
||
rgba(255, 255, 255, 0) 0%,
|
||
rgba(255, 255, 255, 0.45) 45%,
|
||
rgba(255, 255, 255, 0) 90%
|
||
);
|
||
animation: progress-shine 1.35s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes progress-shine {
|
||
0% {
|
||
transform: translateX(-100%);
|
||
}
|
||
100% {
|
||
transform: translateX(220%);
|
||
}
|
||
}
|
||
|
||
.progress-fill-head {
|
||
position: absolute;
|
||
top: 50%;
|
||
right: 2rpx;
|
||
transform: translateY(-50%);
|
||
width: 8rpx;
|
||
height: 55%;
|
||
border-radius: 6rpx;
|
||
background: rgba(255, 255, 255, 0.85);
|
||
box-shadow: 0 0 14rpx rgba(255, 255, 255, 0.75);
|
||
}
|
||
|
||
.progress-hint {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: block;
|
||
text-align: center;
|
||
font-size: 22rpx;
|
||
color: $ink-hint;
|
||
letter-spacing: 0.5rpx;
|
||
}
|
||
</style>
|