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>
|