项目中心-支付模块

This commit is contained in:
wangzzpeng
2026-09-30 08:04:16 +08:00
parent 6b88c3805f
commit ce8210fb3f
35 changed files with 2618 additions and 175 deletions

View File

@@ -0,0 +1,247 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { AppButton } from '@/components'
import { useOssStore } from '@/stores/modules/oss'
import { PREVIEW_TEXT_MAX_BYTES, formatBytes } from '@/utils/filePreview'
const ossStore = useOssStore()
const { visible, file, kind, objectUrl, textContent, loading, error, truncated, totalBytes } = storeToRefs(ossStore)
function onClose(): void {
ossStore.closePreview()
}
function onDownload(): void {
const target = file.value
if (!target?.url) return
const link = document.createElement('a')
link.href = target.url
link.download = target.name
link.target = '_blank'
link.rel = 'noopener'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
</script>
<template>
<Teleport to="body">
<div v-if="visible" class="preview-mask" @click.self="onClose">
<div class="preview-dialog">
<header class="preview-dialog__head">
<span class="preview-dialog__name" :title="file?.name">{{ file?.name }}</span>
<button class="preview-dialog__close" type="button" aria-label="关闭" @click="onClose">
×
</button>
</header>
<div class="preview-dialog__body">
<!-- 加载中 -->
<div v-if="loading" class="preview-dialog__center">
预览加载中…
</div>
<!-- 加载失败 / 无 ossId 等错误 -->
<div v-else-if="error" class="preview-dialog__center">
<p class="preview-dialog__msg">{{ error }}</p>
<AppButton v-if="file?.url" type="primary" @click="onDownload">
下载文件
</AppButton>
</div>
<!-- 不支持内联预览(office 等) -->
<div v-else-if="kind === 'unsupported'" class="preview-dialog__center">
<p class="preview-dialog__msg">
当前格式({{ (file?.name ?? '').split('.').pop() }})不支持在线预览,请下载后查看。
</p>
<AppButton v-if="file?.url" type="primary" @click="onDownload">
下载文件
</AppButton>
</div>
<!-- 图片 -->
<img
v-else-if="kind === 'image' && objectUrl"
:src="objectUrl"
class="preview-dialog__image"
alt="预览"
>
<!-- PDF(blob 已带 application/pdf,iframe 内联渲染) -->
<iframe
v-else-if="kind === 'pdf' && objectUrl"
:src="objectUrl"
class="preview-dialog__frame"
title="预览"
/>
<!-- 视频 -->
<video
v-else-if="kind === 'video' && objectUrl"
:src="objectUrl"
class="preview-dialog__media"
controls
/>
<!-- 音频 -->
<audio
v-else-if="kind === 'audio' && objectUrl"
:src="objectUrl"
class="preview-dialog__media"
controls
/>
<!-- 文本(大文件截断预览,避免卡死浏览器) -->
<div v-else-if="kind === 'text'" class="preview-dialog__text-wrap">
<p v-if="truncated" class="preview-dialog__hint">
<span>文件较大({{ formatBytes(totalBytes ?? 0) }}),仅预览前 {{ formatBytes(PREVIEW_TEXT_MAX_BYTES) }},完整内容请下载。</span>
<AppButton v-if="file?.url" type="primary" @click="onDownload">下载文件</AppButton>
</p>
<pre class="preview-dialog__text">{{ textContent }}</pre>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<style lang="less" scoped>
.preview-mask {
position: fixed;
inset: 0;
z-index: 2000;
.flex(row, center);
background: rgba(0, 0, 0, 0.55);
padding: @spacing-lg;
overflow: auto;
}
.preview-dialog {
width: min(960px, 100%);
max-height: calc(100vh - @spacing-xxl);
.flex(column, flex-start, stretch);
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-lg;
overflow: hidden;
&__head {
.flex(row, space-between, center);
flex-shrink: 0;
gap: @spacing-sm;
padding: @spacing-base @spacing-lg;
border-bottom: 1px solid @border-color;
}
&__name {
min-width: 0;
font-size: @font-size-base;
font-weight: 600;
color: @heading-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__close {
flex-shrink: 0;
width: 28px;
height: 28px;
border: none;
border-radius: @radius-base;
background: transparent;
color: @text-color-secondary;
font-size: 20px;
line-height: 1;
cursor: pointer;
transition: background 0.2s, color 0.2s;
&:hover {
background: @bg-color-light;
color: @text-color;
}
}
&__body {
flex: 1;
min-height: 0;
.flex(column, flex-start, stretch);
padding: @spacing-base;
overflow: auto;
}
&__center {
.flex(column, center, center);
gap: @spacing-base;
width: 100%;
min-height: 240px;
}
&__msg {
font-size: @font-size-base;
color: @text-color-secondary;
text-align: center;
}
&__image {
max-width: 100%;
max-height: 70vh;
margin: auto;
object-fit: contain;
border-radius: @radius-base;
}
&__frame {
flex: 1;
width: 100%;
min-height: 60vh;
border: none;
}
&__media {
width: 100%;
max-height: 70vh;
margin: auto;
}
&__text-wrap {
display: flex;
flex-direction: column;
width: 100%;
min-height: 0;
}
&__hint {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: @spacing-sm;
flex-shrink: 0;
margin: 0 0 @spacing-sm;
padding: @spacing-sm @spacing-base;
border-radius: @radius-base;
background: #fff7e6;
border: 1px solid #ffd591;
color: #ad6800;
font-size: @font-size-sm;
line-height: 1.6;
}
&__text {
width: 100%;
margin: 0;
padding: @spacing-base;
border: 1px solid @border-color;
border-radius: @radius-base;
background: @bg-color-light;
color: @text-color;
font-size: @font-size-sm;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
max-height: 70vh;
overflow: auto;
}
}
</style>