753 lines
28 KiB
Vue
753 lines
28 KiB
Vue
<template>
|
||
<view class="file-preview-container" :style="themeColor">
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<!-- 微信小程序:web-view 是原生组件,必须用 cover-view 盖在上面;cover-view 不支持 iconfont,改用 cover-image PNG -->
|
||
<cover-view class="mp-header">
|
||
<cover-view class="mp-header-inner">
|
||
<cover-view class="nav-btn back-btn" @click="goBack">
|
||
<cover-image src="/static/img/icon-back.png" class="nav-icon"></cover-image>
|
||
</cover-view>
|
||
<cover-view class="header-title">{{ headerTitle }}</cover-view>
|
||
<button class="nav-btn share-btn" open-type="share" hover-class="none">
|
||
<cover-image src="/static/img/icon-share.png" class="nav-icon"></cover-image>
|
||
</button>
|
||
</cover-view>
|
||
<!-- 状态栏占位(navigationStyle custom 时,避免被时间/电量遮挡) -->
|
||
<cover-view class="mp-status-bar" :style="{ height: statusBarHeight + 'px' }"></cover-view>
|
||
</cover-view>
|
||
<!-- #endif -->
|
||
|
||
<!-- #ifndef MP-WEIXIN -->
|
||
<!-- 非小程序:普通 view + iconfont 即可 -->
|
||
<view class="header">
|
||
<view class="nav-btn back-btn" @click="goBack">
|
||
<text class="iconfont icon-back"></text>
|
||
</view>
|
||
<view class="header-title">{{ headerTitle }}</view>
|
||
<button class="nav-btn share-btn" open-type="share" hover-class="none" @click.stop>
|
||
<text class="iconfont icon-fenxiang"></text>
|
||
</button>
|
||
</view>
|
||
<!-- #endif -->
|
||
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<!-- 微信小程序 web-view 是全屏原生组件,父容器 padding/margin 对它无效,必须在它前面放一个等高的占位 view 把它顶下去 -->
|
||
<view class="mp-content-spacer" :style="{ height: headerTotalHeightPx + 'px' }"></view>
|
||
<!-- #endif -->
|
||
|
||
<view class="content">
|
||
<!-- PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 图片预览分支
|
||
aikefu 测试对话附件 type='image' / AI 侧 attachments.image 跳转时传 fileType=image -->
|
||
<view v-if="fileType === 'image'" class="image-viewer">
|
||
<image v-if="fileUrl" :src="fileUrl" mode="aspectFit" class="image-element" @click="previewImageFullscreen" />
|
||
<view v-else class="error-message">{{ $lang('file-preview.file-preview.fileUrlNotExist') }}</view>
|
||
</view>
|
||
|
||
<!-- PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 音频预览分支
|
||
AI 侧 attachments.audio 跳转时传 fileType=audio, 用 createInnerAudioContext 播放 -->
|
||
<view v-else-if="fileType === 'audio'" class="audio-viewer">
|
||
<view v-if="fileUrl" class="audio-player-card">
|
||
<view class="audio-icon">
|
||
<text class="iconfont icon-voice"></text>
|
||
</view>
|
||
<view class="audio-info">
|
||
<view class="audio-title">{{ fileName || $lang('file-preview.file-preview.audioDefaultTitle') }}</view>
|
||
<view class="audio-status">{{ audioPlaying ? $lang('file-preview.file-preview.audioPlaying') : $lang('file-preview.file-preview.audioPaused') }}</view>
|
||
<view class="audio-progress">
|
||
<text class="audio-time">{{ formatAudioTime(audioCurrentTime) }}</text>
|
||
<view class="audio-progress-bar">
|
||
<view class="audio-progress-fill" :style="{ width: audioProgressPct + '%' }"></view>
|
||
</view>
|
||
<text class="audio-time">{{ formatAudioTime(audioDuration) }}</text>
|
||
</view>
|
||
</view>
|
||
<button class="audio-play-btn" @click="toggleAudioPlay">
|
||
<text class="iconfont" :class="audioPlaying ? 'icon-pause' : 'icon-play'"></text>
|
||
</button>
|
||
</view>
|
||
<view v-else class="error-message">{{ $lang('file-preview.file-preview.audioUrlNotExist') }}</view>
|
||
<!-- #ifdef H5 -->
|
||
<view class="h5-action-bar">
|
||
<view class="h5-action-tip">如无法播放,请使用下方按钮:</view>
|
||
<view class="h5-action-btns">
|
||
<button class="h5-action-btn primary" @click="openInNewTab">新标签页打开</button>
|
||
<button class="h5-action-btn" @click="triggerDownload">下载文件</button>
|
||
</view>
|
||
</view>
|
||
<!-- #endif -->
|
||
</view>
|
||
|
||
<!-- PDF 文件预览 -->
|
||
<view v-if="fileType === 'pdf'" class="file-viewer">
|
||
<!-- #ifdef H5 -->
|
||
<!-- H5 端:web-view 渲染为 iframe,若被 X-Frame-Options/CORS 拦截则显示空白;额外提供"新标签页打开"与"下载"按钮兜底 -->
|
||
<view class="h5-action-bar">
|
||
<view class="h5-action-tip">如页面空白,请使用下方按钮:</view>
|
||
<view class="h5-action-btns">
|
||
<button class="h5-action-btn primary" @click="openInNewTab">新标签页打开</button>
|
||
<button class="h5-action-btn" @click="triggerDownload">下载文件</button>
|
||
</view>
|
||
</view>
|
||
<!-- #endif -->
|
||
<web-view v-if="fileUrl" :src="fileUrl"></web-view>
|
||
<view v-else class="error-message">{{ $lang('file-preview.file-preview.fileUrlNotExist') }}</view>
|
||
</view>
|
||
|
||
<!-- Word 文件预览 -->
|
||
<view v-else-if="fileType === 'word'" class="file-viewer">
|
||
<!-- #ifdef H5 -->
|
||
<view class="h5-action-bar">
|
||
<view class="h5-action-tip">浏览器可能无法直接内嵌预览 Word,建议下载后查看:</view>
|
||
<view class="h5-action-btns">
|
||
<button class="h5-action-btn primary" @click="triggerDownload">下载文件</button>
|
||
<button class="h5-action-btn" @click="openInNewTab">新标签页打开</button>
|
||
</view>
|
||
</view>
|
||
<!-- #endif -->
|
||
<web-view v-if="fileUrl" :src="fileUrl"></web-view>
|
||
<view v-else class="error-message">{{ $lang('file-preview.file-preview.fileUrlNotExist') }}</view>
|
||
</view>
|
||
|
||
<!-- 视频文件预览 -->
|
||
<view v-else-if="fileType === 'video'" class="video-viewer">
|
||
<video v-if="fileUrl" id="filePreviewVideo" :src="fileUrl" controls class="video-player"></video>
|
||
<view v-else class="error-message">{{ $lang('file-preview.file-preview.videoUrlNotExist') }}</view>
|
||
</view>
|
||
|
||
<!-- 其他文件类型 -->
|
||
<view v-else class="file-viewer">
|
||
<!-- #ifdef H5 -->
|
||
<view v-if="fileUrl" class="h5-action-bar">
|
||
<view class="h5-action-tip">{{ $lang('file-preview.file-preview.unsupportedFileType') }}</view>
|
||
<view class="h5-action-btns">
|
||
<button class="h5-action-btn primary" @click="triggerDownload">下载文件</button>
|
||
<button class="h5-action-btn" @click="openInNewTab">新标签页打开</button>
|
||
</view>
|
||
</view>
|
||
<view v-else class="error-message">{{ $lang('file-preview.file-preview.fileUrlNotExist') }}</view>
|
||
<!-- #endif -->
|
||
<!-- #ifndef H5 -->
|
||
<view class="error-message">{{ $lang('file-preview.file-preview.unsupportedFileType') }}</view>
|
||
<!-- #endif -->
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
fileName: '',
|
||
fileUrl: '',
|
||
fileType: '',
|
||
statusBarHeight: 20,
|
||
windowWidth: 375,
|
||
// PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 音频播放引擎
|
||
audioCtx: null,
|
||
audioPlaying: false,
|
||
audioCurrentTime: 0,
|
||
audioDuration: 0
|
||
};
|
||
},
|
||
computed: {
|
||
audioProgressPct() {
|
||
if (!this.audioDuration || this.audioDuration <= 0) return 0;
|
||
return Math.min(100, (this.audioCurrentTime / this.audioDuration) * 100);
|
||
}
|
||
},
|
||
computed: {
|
||
// #ifdef MP-WEIXIN
|
||
// 整个自定义 header 高度(状态栏 + 标题栏 88rpx),单位 px —— 用于占位 view 和内容区下移
|
||
headerTotalHeightPx() {
|
||
const rpxToPx = this.windowWidth / 750;
|
||
return this.statusBarHeight + Math.round(88 * rpxToPx);
|
||
},
|
||
// #endif
|
||
// 页面标题栏显示的短标题(避免超长挤压分享按钮;对用户无意义的tmp名统一显示通用"文件预览")
|
||
headerTitle() {
|
||
const name = (this.fileName || '').trim();
|
||
// 如果是临时文件名(tmp_ 开头 或 带路径hash的长串),统一显示通用"文件预览"
|
||
if (/^tmp_/i.test(name) || (name.length > 30 && /[a-f0-9]{20,}/i.test(name))) {
|
||
return this.$lang('file-preview.file-preview.title');
|
||
}
|
||
// 否则显示原始文件名(列表里传入的,如"乐瑞固企业介绍",本身就短)
|
||
if (name) {
|
||
// JS 层保险截断(超过16字 + …),应对 cover-view CSS 省略号不生效
|
||
if (name.length > 16) return name.slice(0, 16) + '…';
|
||
return name;
|
||
}
|
||
return this.$lang('file-preview.file-preview.title');
|
||
},
|
||
// 分享的真实标题(好友收到的卡片显示真实文件名,不截断)
|
||
shareTitle() {
|
||
const name = (this.fileName || '').trim();
|
||
if (name && !/^tmp_/i.test(name)) return name;
|
||
return this.$lang('file-preview.file-preview.title');
|
||
},
|
||
// 分享路径:只带文件三要素,不挂推荐码/关系
|
||
sharePath() {
|
||
const query = [];
|
||
if (this.fileName) query.push(`fileName=${encodeURIComponent(this.fileName)}`);
|
||
if (this.fileUrl) query.push(`fileUrl=${encodeURIComponent(this.fileUrl)}`);
|
||
if (this.fileType) query.push(`fileType=${encodeURIComponent(this.fileType)}`);
|
||
return `/pages_tool/file-preview/file-preview${query.length ? '?' + query.join('&') : ''}`;
|
||
},
|
||
themeColor() {
|
||
if (this.$shop && this.$shop.themeColor) {
|
||
return `--theme-color: ${this.$shop.themeColor};`;
|
||
}
|
||
return '';
|
||
}
|
||
},
|
||
onLoad(option) {
|
||
// 解析传递的参数
|
||
if (option.fileName) {
|
||
this.fileName = decodeURIComponent(option.fileName);
|
||
}
|
||
if (option.fileUrl) {
|
||
this.fileUrl = decodeURIComponent(option.fileUrl);
|
||
}
|
||
if (option.fileType) {
|
||
this.fileType = decodeURIComponent(option.fileType);
|
||
} else {
|
||
this.inferFileType();
|
||
}
|
||
// #ifdef MP-WEIXIN
|
||
const sys = uni.getSystemInfoSync();
|
||
this.statusBarHeight = sys.statusBarHeight || 20;
|
||
this.windowWidth = sys.windowWidth || 375;
|
||
// #endif
|
||
},
|
||
onReady() {
|
||
// 视频加载后手动触发一次播放占位(去掉 autoplay 防自动播放被系统拦截)
|
||
// #ifndef H5
|
||
if (this.fileType === 'video' && this.fileUrl) {
|
||
try {
|
||
const ctx = uni.createVideoContext('filePreviewVideo', this);
|
||
if (ctx && typeof ctx.play === 'function') { /* 留给用户点击,不自动播 */ }
|
||
} catch (e) {}
|
||
}
|
||
// #endif
|
||
},
|
||
// PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 离开页面销毁音频 ctx
|
||
onUnload() {
|
||
if (this.audioCtx) {
|
||
try { this.audioCtx.stop(); this.audioCtx.destroy(); } catch (e) {}
|
||
this.audioCtx = null;
|
||
}
|
||
},
|
||
// #ifdef MP-WEIXIN
|
||
// 微信小程序:右上角胶囊"分享给朋友" / button[open-type=share] 触发
|
||
onShareAppMessage() {
|
||
return {
|
||
title: this.shareTitle,
|
||
path: this.sharePath
|
||
};
|
||
},
|
||
// 微信小程序:分享到朋友圈
|
||
onShareTimeline() {
|
||
return {
|
||
title: this.shareTitle,
|
||
query: this.sharePath.split('?')[1] || ''
|
||
};
|
||
},
|
||
// #endif
|
||
methods: {
|
||
inferFileType() {
|
||
// PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 加 image / audio 识别
|
||
const classify = (ext) => {
|
||
if (['pdf'].includes(ext)) return 'pdf';
|
||
if (['doc', 'docx'].includes(ext)) return 'word';
|
||
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'].includes(ext)) return 'video';
|
||
if (['mp3', 'wav', 'aac', 'm4a', 'ogg', 'flac'].includes(ext)) return 'audio';
|
||
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'].includes(ext)) return 'image';
|
||
return null;
|
||
};
|
||
if (this.fileUrl) {
|
||
const clean = String(this.fileUrl).split('?')[0];
|
||
const ext = clean.split('.').pop().toLowerCase();
|
||
const t = classify(ext);
|
||
if (t) this.fileType = t;
|
||
} else if (this.fileName) {
|
||
const ext = this.fileName.split('.').pop().toLowerCase();
|
||
const t = classify(ext);
|
||
if (t) this.fileType = t;
|
||
}
|
||
},
|
||
goBack() {
|
||
const pages = getCurrentPages();
|
||
if (pages && pages.length > 1) {
|
||
uni.navigateBack();
|
||
} else {
|
||
uni.switchTab({ url: '/pages/index/index' }).catch(() => {
|
||
uni.navigateBack();
|
||
});
|
||
}
|
||
},
|
||
// #ifdef H5
|
||
// H5 端:新标签页打开文件(绕过 iframe X-Frame-Options/CORS 限制)
|
||
openInNewTab() {
|
||
if (!this.fileUrl) return;
|
||
try {
|
||
window.open(this.fileUrl, '_blank', 'noopener,noreferrer');
|
||
} catch (e) {
|
||
uni.showToast({ title: '打开失败,请稍后重试', icon: 'none' });
|
||
}
|
||
},
|
||
// H5 端:通过 <a download> 触发浏览器下载(最稳定的跨浏览器方案)
|
||
triggerDownload() {
|
||
if (!this.fileUrl) return;
|
||
try {
|
||
const link = document.createElement('a');
|
||
link.href = this.fileUrl;
|
||
link.target = '_blank';
|
||
link.rel = 'noopener noreferrer';
|
||
if (this.fileName) {
|
||
// 仅提示浏览器用这个名字;受 CORS 影响,跨域文件时 download 属性可能被忽略,
|
||
// 此时浏览器会直接按服务器返回的文件名/直接显示,但不会丢失文件本身
|
||
link.download = this.fileName;
|
||
}
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
} catch (e) {
|
||
// 兜底:新标签页打开
|
||
this.openInNewTab();
|
||
}
|
||
},
|
||
// #endif
|
||
|
||
// PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 图片分支 — 点击全屏预览
|
||
previewImageFullscreen() {
|
||
if (!this.fileUrl) return;
|
||
// #ifdef H5 || MP-WEIXIN
|
||
uni.previewImage({ urls: [this.fileUrl], current: this.fileUrl });
|
||
// #endif
|
||
},
|
||
|
||
// PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 音频分支 — 切换播放/暂停
|
||
toggleAudioPlay() {
|
||
if (!this.fileUrl) {
|
||
uni.showToast({ title: '音频地址为空', icon: 'none' });
|
||
return;
|
||
}
|
||
if (this.audioPlaying) {
|
||
this._pauseAudio();
|
||
} else {
|
||
this._playAudio();
|
||
}
|
||
},
|
||
|
||
_ensureAudioCtx() {
|
||
if (this.audioCtx) return this.audioCtx;
|
||
try {
|
||
const ctx = uni.createInnerAudioContext();
|
||
ctx.src = this.fileUrl;
|
||
ctx.onTimeUpdate(() => {
|
||
this.audioCurrentTime = ctx.currentTime || 0;
|
||
if (ctx.duration && !isNaN(ctx.duration)) {
|
||
this.audioDuration = ctx.duration;
|
||
}
|
||
});
|
||
ctx.onEnded(() => {
|
||
this.audioPlaying = false;
|
||
this.audioCurrentTime = 0;
|
||
});
|
||
ctx.onError((e) => {
|
||
this.audioPlaying = false;
|
||
uni.showToast({ title: '音频播放失败', icon: 'none' });
|
||
console.error('[file-preview audio] error:', e);
|
||
});
|
||
this.audioCtx = ctx;
|
||
return ctx;
|
||
} catch (e) {
|
||
uni.showToast({ title: '音频引擎初始化失败', icon: 'none' });
|
||
console.error('[file-preview audio] createInnerAudioContext failed:', e);
|
||
return null;
|
||
}
|
||
},
|
||
|
||
_playAudio() {
|
||
const ctx = this._ensureAudioCtx();
|
||
if (!ctx) return;
|
||
if (this.audioCurrentTime > 0) {
|
||
try { ctx.seek(this.audioCurrentTime); } catch (e) { /* ignore */ }
|
||
}
|
||
this.audioPlaying = true;
|
||
try {
|
||
ctx.play();
|
||
} catch (e) {
|
||
this.audioPlaying = false;
|
||
uni.showToast({ title: '音频播放失败', icon: 'none' });
|
||
}
|
||
},
|
||
|
||
_pauseAudio() {
|
||
if (this.audioCtx) {
|
||
try { this.audioCtx.pause(); } catch (e) { /* ignore */ }
|
||
}
|
||
this.audioPlaying = false;
|
||
},
|
||
|
||
// 00:00 格式
|
||
formatAudioTime(seconds) {
|
||
if (!seconds || isNaN(seconds)) return '00:00';
|
||
const total = Math.floor(seconds);
|
||
const mm = String(Math.floor(total / 60)).padStart(2, '0');
|
||
const ss = String(total % 60).padStart(2, '0');
|
||
return mm + ':' + ss;
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.file-preview-container {
|
||
min-height: 100vh;
|
||
background-color: #f5f5f5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
/* =================== 非 MP 端 header =================== */
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 100rpx;
|
||
padding: 0 30rpx;
|
||
background-color: #fff;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
flex-shrink: 0;
|
||
z-index: 10;
|
||
|
||
.nav-btn {
|
||
flex-shrink: 0;
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0;
|
||
background: transparent;
|
||
line-height: 1;
|
||
border: none;
|
||
margin: 0;
|
||
&::after { border: none; }
|
||
}
|
||
|
||
.back-btn {
|
||
.icon-back {
|
||
font-size: 36rpx;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.header-title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
text-align: center;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
margin: 0 20rpx;
|
||
}
|
||
|
||
.share-btn {
|
||
.icon-fenxiang {
|
||
font-size: 34rpx;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* =================== MP 端 header (cover-view 浮层) =================== */
|
||
/* #ifdef MP-WEIXIN */
|
||
.mp-header {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 9999;
|
||
background-color: #ffffff;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
|
||
.mp-status-bar {
|
||
width: 100%;
|
||
}
|
||
|
||
.mp-header-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 88rpx;
|
||
padding: 0 20rpx;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.nav-btn {
|
||
flex-shrink: 0;
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0;
|
||
background: transparent;
|
||
line-height: 1;
|
||
border: none;
|
||
margin: 0;
|
||
&::after { border: none; }
|
||
}
|
||
|
||
.nav-icon {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
display: block;
|
||
}
|
||
|
||
.header-title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
text-align: center;
|
||
margin: 0 16rpx;
|
||
/* cover-view 下 ellipsis 支持有限,JS 层(headerTitle computed)已经做了截断,这里再冗余防守 */
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
}
|
||
/* #endif */
|
||
|
||
/* =================== 内容区 =================== */
|
||
.content {
|
||
flex: 1;
|
||
/* #ifdef MP-WEIXIN */
|
||
/* 给浮层 header(statusBar+88rpx)留位置,避免内容顶上去 */
|
||
padding-top: calc(var(--mp-header-h, 120rpx));
|
||
/* #endif */
|
||
padding-left: 20rpx;
|
||
padding-right: 20rpx;
|
||
padding-bottom: 20rpx;
|
||
box-sizing: border-box;
|
||
|
||
.file-viewer {
|
||
width: 100%;
|
||
min-height: 80vh;
|
||
background-color: #fff;
|
||
border-radius: 12rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
|
||
.error-message {
|
||
width: 100%;
|
||
height: 80vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
/* #ifdef H5 */
|
||
/* H5 兜底操作栏(PDF/Word 被 X-Frame-Options 拦截时显示)*/
|
||
.h5-action-bar {
|
||
padding: 24rpx 28rpx 20rpx;
|
||
background: linear-gradient(180deg, #fffbe6 0%, #ffffff 100%);
|
||
border-bottom: 1rpx solid #f0e9c8;
|
||
|
||
.h5-action-tip {
|
||
font-size: 26rpx;
|
||
color: #8a6d1b;
|
||
margin-bottom: 18rpx;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.h5-action-btns {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
|
||
.h5-action-btn {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
line-height: 72rpx;
|
||
padding: 0;
|
||
font-size: 28rpx;
|
||
border-radius: 36rpx;
|
||
background: #f2f3f5;
|
||
color: #333;
|
||
border: 1rpx solid #e2e4e8;
|
||
margin: 0;
|
||
|
||
&::after { border: none; }
|
||
|
||
&.primary {
|
||
background: linear-gradient(90deg, #ff7e47 0%, #f73131 100%);
|
||
color: #fff;
|
||
border-color: transparent;
|
||
font-weight: 600;
|
||
box-shadow: 0 4rpx 12rpx rgba(247, 49, 49, 0.25);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/* #endif */
|
||
}
|
||
|
||
.video-viewer {
|
||
width: 100%;
|
||
min-height: 80vh;
|
||
background-color: #000;
|
||
border-radius: 12rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
|
||
.video-player {
|
||
width: 100%;
|
||
height: 80vh;
|
||
}
|
||
|
||
.error-message {
|
||
width: 100%;
|
||
height: 80vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
background-color: #000;
|
||
}
|
||
}
|
||
|
||
/* PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 图片预览 */
|
||
.image-viewer {
|
||
width: 100%;
|
||
min-height: 80vh;
|
||
background-color: #000;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
|
||
.image-element {
|
||
width: 100%;
|
||
max-height: 80vh;
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
/* PR-FRONTEND-MEDIA-UPLOAD (2026-09-16): 音频预览 */
|
||
.audio-viewer {
|
||
width: 100%;
|
||
min-height: 80vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.audio-player-card {
|
||
width: 90%;
|
||
max-width: 560rpx;
|
||
padding: 40rpx 32rpx;
|
||
background-color: #fff;
|
||
border-radius: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||
|
||
.audio-icon {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #6b7c93 0%, #4a5a72 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
margin-right: 24rpx;
|
||
.iconfont {
|
||
font-size: 48rpx;
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
.audio-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
|
||
.audio-title {
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: #2c3e50;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.audio-status {
|
||
font-size: 22rpx;
|
||
color: #8a9fb8;
|
||
}
|
||
.audio-progress {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
margin-top: 8rpx;
|
||
.audio-time {
|
||
font-size: 22rpx;
|
||
color: #8a9fb8;
|
||
flex-shrink: 0;
|
||
min-width: 60rpx;
|
||
text-align: center;
|
||
}
|
||
.audio-progress-bar {
|
||
flex: 1;
|
||
height: 6rpx;
|
||
background-color: #e8edf3;
|
||
border-radius: 3rpx;
|
||
overflow: hidden;
|
||
.audio-progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #6b7c93 0%, #4a5a72 100%);
|
||
transition: width 0.1s linear;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.audio-play-btn {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #6b7c93 0%, #4a5a72 100%);
|
||
border: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-left: 16rpx;
|
||
flex-shrink: 0;
|
||
padding: 0;
|
||
line-height: 1;
|
||
&::after { border: none; }
|
||
.iconfont {
|
||
font-size: 36rpx;
|
||
color: #fff;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|