|
@@ -0,0 +1,428 @@
|
|
|
|
|
+<!--
|
|
|
|
|
+ * @desc 合规教育-查看每一项
|
|
|
|
|
+ * @author linyuanjie
|
|
|
|
|
+ * @date 2023/9/21
|
|
|
|
|
+-->
|
|
|
|
|
+
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="compliance-detail-page">
|
|
|
|
|
+ <view class="detail-header">
|
|
|
|
|
+ <view class="header-label">合规教育文件</view>
|
|
|
|
|
+ <view class="header-title">
|
|
|
|
|
+ {{ fileName || '文件详情' }}
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="header-desc"> 点击下方文件可在线查看 </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-if="loading" class="state-box">
|
|
|
|
|
+ <view class="state-text">加载中...</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-else-if="!rulesItem" class="state-box">
|
|
|
|
|
+ <view class="empty-icon">📄</view>
|
|
|
|
|
+ <view class="state-text">未找到对应文件</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-else-if="fileList.length === 0" class="state-box">
|
|
|
|
|
+ <view class="empty-icon">📁</view>
|
|
|
|
|
+ <view class="state-text">暂无可查看文件</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view v-else class="file-section">
|
|
|
|
|
+ <view class="section-title">
|
|
|
|
|
+ <view class="title-main">文件列表</view>
|
|
|
|
|
+ <view class="title-count">共 {{ fileList.length }} 个</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="item in fileList"
|
|
|
|
|
+ :key="item.url"
|
|
|
|
|
+ class="file-card"
|
|
|
|
|
+ :class="{ 'file-card--unsupported': !isSupportedFile(item.url) }"
|
|
|
|
|
+ @click="handleReadFile(item)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="file-icon">
|
|
|
|
|
+ {{ getFileBadgeLabel(item.url) }}
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="file-content">
|
|
|
|
|
+ <view class="file-name">
|
|
|
|
|
+ {{ item.fileName }}
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="file-desc">
|
|
|
|
|
+ {{ getFileDesc(item.url) }}
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="file-arrow">›</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { computed, ref } from 'vue'
|
|
|
|
|
+
|
|
|
|
|
+import { onLoad } from '@dcloudio/uni-app'
|
|
|
|
|
+
|
|
|
|
|
+import { getDeptRegulationsListApi } from '@/services/modules/mine/common'
|
|
|
|
|
+import type {
|
|
|
|
|
+ ComplianceEducationFile,
|
|
|
|
|
+ DeptRegulationsItem,
|
|
|
|
|
+} from '@/services/modules/mine/common/type'
|
|
|
|
|
+
|
|
|
|
|
+type PreviewType = 'document' | 'image'
|
|
|
|
|
+
|
|
|
|
|
+const DOCUMENT_EXTENSIONS = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf'] as const
|
|
|
|
|
+const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'] as const
|
|
|
|
|
+
|
|
|
|
|
+const BASE_URL = import.meta.env.VITE_BASE_API as string
|
|
|
|
|
+
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+const fileName = ref('')
|
|
|
|
|
+const rulesItem = ref<DeptRegulationsItem | null>(null)
|
|
|
|
|
+
|
|
|
|
|
+const fileList = computed<ComplianceEducationFile[]>(() => {
|
|
|
|
|
+ return rulesItem.value?.fileUrl ?? []
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onLoad((options) => {
|
|
|
|
|
+ const routeFileName = decodeRouteParam(options?.fileName)
|
|
|
|
|
+
|
|
|
|
|
+ fileName.value = routeFileName
|
|
|
|
|
+
|
|
|
|
|
+ uni.setNavigationBarTitle({
|
|
|
|
|
+ title: routeFileName || '文件详情',
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ void getDeptRulesItem()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const getDeptRulesItem = async (): Promise<void> => {
|
|
|
|
|
+ if (!fileName.value) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '缺少文件名称',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getDeptRegulationsListApi()
|
|
|
|
|
+
|
|
|
|
|
+ if (res.code !== 0) {
|
|
|
|
|
+ rulesItem.value = null
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const list = Array.isArray(res.data) ? res.data : []
|
|
|
|
|
+
|
|
|
|
|
+ rulesItem.value =
|
|
|
|
|
+ list.find((item: DeptRegulationsItem) => {
|
|
|
|
|
+ return item.fileName === fileName.value
|
|
|
|
|
+ }) ?? null
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ rulesItem.value = null
|
|
|
|
|
+
|
|
|
|
|
+ console.error('[compliance-education-detail] getDeptRulesItem failed:', error)
|
|
|
|
|
+
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '加载失败,请稍后重试',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleReadFile = async (file: ComplianceEducationFile): Promise<void> => {
|
|
|
|
|
+ const fileType = getFileExtension(file.url)
|
|
|
|
|
+ const previewType = getPreviewType(fileType)
|
|
|
|
|
+
|
|
|
|
|
+ if (!previewType) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: `不支持${fileType || '该'}文件查看,请联系管理员`,
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const fileUrl = resolveFileUrl(file.url)
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (previewType === 'document') {
|
|
|
|
|
+ await openDocument(fileUrl, fileType)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ previewImage(fileUrl)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('[compliance-education-detail] open file failed:', error)
|
|
|
|
|
+
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '文件打开失败,请稍后重试',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const openDocument = (url: string, fileType: string): Promise<void> => {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ uni.downloadFile({
|
|
|
|
|
+ url,
|
|
|
|
|
+ success: (downloadRes) => {
|
|
|
|
|
+ if (downloadRes.statusCode !== 200) {
|
|
|
|
|
+ reject(downloadRes)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ uni.openDocument({
|
|
|
|
|
+ filePath: downloadRes.tempFilePath,
|
|
|
|
|
+ fileType,
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ resolve()
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: reject,
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: reject,
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const previewImage = (url: string): void => {
|
|
|
|
|
+ uni.previewImage({
|
|
|
|
|
+ urls: [url],
|
|
|
|
|
+ current: url,
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getPreviewType = (fileType: string): PreviewType | null => {
|
|
|
|
|
+ if ((DOCUMENT_EXTENSIONS as readonly string[]).includes(fileType)) {
|
|
|
|
|
+ return 'document'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ((IMAGE_EXTENSIONS as readonly string[]).includes(fileType)) {
|
|
|
|
|
+ return 'image'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const isSupportedFile = (url: string): boolean => {
|
|
|
|
|
+ return getPreviewType(getFileExtension(url)) !== null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getFileExtension = (url: string): string => {
|
|
|
|
|
+ const pureUrl = url.split(/[?#]/)[0] ?? ''
|
|
|
|
|
+ const lastDotIndex = pureUrl.lastIndexOf('.')
|
|
|
|
|
+
|
|
|
|
|
+ if (lastDotIndex === -1) {
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return pureUrl.slice(lastDotIndex + 1).toLowerCase()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const resolveFileUrl = (url: string): string => {
|
|
|
|
|
+ if (/^https?:\/\//i.test(url)) {
|
|
|
|
|
+ return url
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const normalizedBaseUrl = BASE_URL.replace(/\/$/, '')
|
|
|
|
|
+ const normalizedPath = url.startsWith('/') ? url : `/${url}`
|
|
|
|
|
+
|
|
|
|
|
+ return `${normalizedBaseUrl}${normalizedPath}`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getFileBadgeLabel = (url: string): string => {
|
|
|
|
|
+ const fileType = getFileExtension(url)
|
|
|
|
|
+
|
|
|
|
|
+ return fileType ? fileType.toUpperCase() : 'FILE'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getFileDesc = (url: string): string => {
|
|
|
|
|
+ const fileType = getFileExtension(url)
|
|
|
|
|
+ const previewType = getPreviewType(fileType)
|
|
|
|
|
+
|
|
|
|
|
+ if (previewType === 'document') {
|
|
|
|
|
+ return '点击后下载并打开文档'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (previewType === 'image') {
|
|
|
|
|
+ return '点击后预览图片'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return '暂不支持在线查看'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const decodeRouteParam = (value: unknown): string => {
|
|
|
|
|
+ if (typeof value !== 'string') {
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ return decodeURIComponent(value)
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return value
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.compliance-detail-page {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ padding: 32rpx;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ background: linear-gradient(180deg, #eef5ff 0%, #f7f8fa 360rpx), #f7f8fa;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-header {
|
|
|
|
|
+ padding: 34rpx 32rpx 38rpx;
|
|
|
|
|
+ border-radius: 28rpx;
|
|
|
|
|
+ background: linear-gradient(135deg, rgba(39, 111, 245, 0.96), rgba(72, 150, 255, 0.92)), #276ff5;
|
|
|
|
|
+ box-shadow: 0 20rpx 48rpx rgba(39, 111, 245, 0.22);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.header-label {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ height: 40rpx;
|
|
|
|
|
+ padding: 0 18rpx;
|
|
|
|
|
+ border-radius: 999rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.92);
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.16);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.header-title {
|
|
|
|
|
+ margin-top: 22rpx;
|
|
|
|
|
+ font-size: 38rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ line-height: 1.45;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.header-desc {
|
|
|
|
|
+ margin-top: 12rpx;
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.82);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-section {
|
|
|
|
|
+ margin-top: 30rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 18rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.title-main {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.title-count {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #8a94a6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-card {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ min-height: 132rpx;
|
|
|
|
|
+ padding: 26rpx 28rpx;
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ border-radius: 24rpx;
|
|
|
|
|
+ background: #ffffff;
|
|
|
|
|
+ box-shadow: 0 12rpx 36rpx rgba(20, 35, 60, 0.06);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-card:active {
|
|
|
|
|
+ transform: scale(0.985);
|
|
|
|
|
+ opacity: 0.92;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-card--unsupported {
|
|
|
|
|
+ opacity: 0.72;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-icon {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ width: 76rpx;
|
|
|
|
|
+ height: 76rpx;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ border-radius: 22rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #276ff5;
|
|
|
|
|
+ background: #edf4ff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-content {
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ margin-left: 22rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-name {
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ line-height: 1.45;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ display: -webkit-box;
|
|
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-desc {
|
|
|
|
|
+ margin-top: 8rpx;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ color: #8a94a6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-arrow {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ margin-left: 20rpx;
|
|
|
|
|
+ font-size: 48rpx;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ color: #b8c0cc;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.state-box {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ height: 420rpx;
|
|
|
|
|
+ margin-top: 28rpx;
|
|
|
|
|
+ border-radius: 24rpx;
|
|
|
|
|
+ background: #ffffff;
|
|
|
|
|
+ box-shadow: 0 12rpx 36rpx rgba(20, 35, 60, 0.05);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-icon {
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+ font-size: 64rpx;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.state-text {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #8a94a6;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|