859 lines
21 KiB
Vue
Raw Normal View History

2025-06-02 21:36:36 +08:00
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { getPageDetail, updateViewCount } from '../services/api'
import TheNavbar from '../components/TheNavbar.vue'
const route = useRoute()
const loading = ref(true)
const error = ref(false)
const page = ref<any>(null)
// 计算附件列表
const attachmentList = computed(() => {
if (!page.value || !page.value.multiAttachments) return [];
try {
const parsed = JSON.parse(page.value.multiAttachments);
// 过滤无效的附件项
return Array.isArray(parsed) ? parsed.filter(item => item && (item.url || item.name)) : [];
} catch (e) {
console.error('解析附件列表失败:', e);
return [];
}
});
onMounted(async () => {
try {
const formatId = route.query.Id as string
if (!formatId) {
console.error('缺少必要的formatId参数');
error.value = true;
loading.value = false;
return;
}
console.log('开始获取表单详情formatId:', formatId);
// 获取表单数据
const result = await getPageDetail('form', formatId);
console.log('表单详情请求结果:', result);
if (result && result.code === 200 && result.data) {
page.value = result.data;
console.log('成功获取表单详情:', page.value);
// 更新浏览量
try {
const viewResult = await updateViewCount('form', formatId) as any;
console.log('更新浏览量结果:', viewResult);
// 确认服务器端更新成功
if (viewResult && viewResult.code === 200) {
// 如果后端返回了新的浏览量数据
if (viewResult.data && typeof viewResult.data.viewCount === 'number') {
page.value.viewCount = viewResult.data.viewCount;
}
// 否则本地递增浏览量
else if (typeof page.value.viewCount === 'number' || typeof page.value.viewCount === 'string') {
page.value.viewCount = Number(page.value.viewCount || 0) + 1;
} else {
page.value.viewCount = 1; // 初始浏览量
}
console.log('浏览量更新为:', page.value.viewCount);
} else {
console.warn('浏览量更新API返回错误:', viewResult?.msg || '未知错误');
}
} catch (e) {
console.error('更新浏览量失败:', e);
// 出错时也尝试本地增加浏览量
if (typeof page.value.viewCount === 'number' || typeof page.value.viewCount === 'string') {
page.value.viewCount = Number(page.value.viewCount || 0) + 1;
console.log('API出错本地更新浏览量为:', page.value.viewCount);
}
}
// 检查返回的数据是否确实是表单类型
if (page.value.pageType && page.value.pageType !== 'form') {
console.error(`错误: 请求的是表单(form),但返回的是${page.value.pageType}类型的内容`);
// 不再尝试重新获取,而是直接显示错误
error.value = true;
loading.value = false;
return;
}
// 处理可能的中文乱码问题
if (page.value.title && /\\u|%/.test(page.value.title)) {
try {
page.value.title = decodeURIComponent(page.value.title);
} catch (e) {
console.error('标题解码失败:', e);
}
}
// 检查内容是否为HTML格式
if (page.value.content) {
console.log('内容类型:', typeof page.value.content);
console.log('内容前50个字符:', page.value.content.substring(0, 50));
// 处理可能的中文乱码问题
if (/\\u|%/.test(page.value.content)) {
try {
page.value.content = decodeURIComponent(page.value.content);
} catch (e) {
console.error('内容解码失败:', e);
}
}
// 确保内容是HTML字符串
if (typeof page.value.content !== 'string') {
try {
page.value.content = JSON.stringify(page.value.content);
} catch (e) {
console.error('内容格式转换失败:', e);
}
}
} else {
console.warn('页面内容为空');
page.value.content = '<p>暂无内容</p>';
}
// 处理附件信息
if (page.value.attachmentUrl) {
console.log('附件URL:', page.value.attachmentUrl);
// 处理可能的附件URL乱码
if (/\\u|%/.test(page.value.attachmentUrl)) {
try {
page.value.attachmentUrl = decodeURIComponent(page.value.attachmentUrl);
} catch (e) {
console.error('附件URL解码失败:', e);
}
}
}
if (page.value.multiAttachments) {
try {
// 处理可能的多附件数据乱码
let attachmentsStr = page.value.multiAttachments;
if (/\\u|%/.test(attachmentsStr)) {
try {
attachmentsStr = decodeURIComponent(attachmentsStr);
} catch (e) {
console.error('多附件数据解码失败:', e);
}
}
const attachments = JSON.parse(attachmentsStr);
console.log('多附件数据:', attachments);
// 处理附件数组中的每个附件URL
if (Array.isArray(attachments)) {
attachments.forEach((attachment, index) => {
if (attachment.url && /\\u|%/.test(attachment.url)) {
try {
attachment.url = decodeURIComponent(attachment.url);
} catch (e) {
console.error(`附件${index}URL解码失败:`, e);
}
}
});
// 更新页面的多附件数据
page.value.multiAttachments = JSON.stringify(attachments);
}
} catch (e) {
console.error('解析多附件数据失败:', e);
}
}
// 更新页面标题
document.title = `${page.value.title || '表单详情'} - 表单下载`;
} else {
console.error('获取表单详情失败:', result?.msg || '未知错误');
error.value = true;
}
} catch (e) {
console.error('获取表单详情异常:', e);
error.value = true;
} finally {
loading.value = false;
}
})
// 格式化日期
function formatDate(dateStr: string): string {
if (!dateStr) return '未知'
const date = new Date(dateStr)
return `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())}`
}
// 数字补零
function padZero(num: number): string {
return num < 10 ? `0${num}` : `${num}`
}
// 获取附件图标类
function getAttachmentIconClass(filename: string): string {
if (!filename) return 'icon-file'
const ext = filename.split('.').pop()?.toLowerCase() || ''
switch (ext) {
case 'pdf': return 'icon-pdf'
case 'doc':
case 'docx': return 'icon-word'
case 'xls':
case 'xlsx': return 'icon-excel'
case 'ppt':
case 'pptx': return 'icon-ppt'
case 'zip':
case 'rar': return 'icon-archive'
case 'png':
case 'jpg':
case 'jpeg':
case 'gif': return 'icon-image'
default: return 'icon-file'
}
}
// 从URL中获取文件名
function getFileName(url: string): string {
if (!url) return '未知文件'
return url.split('/').pop() || '未知文件'
}
// 获取附件完整URL - 修复版本
function getAttachmentUrl(url: string): string {
if (!url) return '#'
// 如果已经是完整URL则直接返回
if (url.startsWith('http://') || url.startsWith('https://')) {
return url
}
// 处理可能的格式问题
let processedUrl = url;
// 替换反斜杠为正斜杠
processedUrl = processedUrl.replace(/\\/g, '/');
// 确保不存在连续的双斜杠
while (processedUrl.includes('//')) {
processedUrl = processedUrl.replace('//', '/');
}
// 去除开头的斜杠,因为后续我们会添加
if (processedUrl.startsWith('/')) {
processedUrl = processedUrl.substring(1);
}
// 检查是否已经包含profile前缀
if (processedUrl.startsWith('profile/')) {
return `${import.meta.env.VITE_APP_BASE_API}/${processedUrl}`;
}
// 检查是否为年月日格式的路径
const datePattern = /^\d{4}\/\d{2}\/\d{2}\//;
if (datePattern.test(processedUrl)) {
// 如果不包含files/master前缀但符合日期格式则添加
if (!processedUrl.startsWith('files/master/')) {
processedUrl = `files/master/${processedUrl}`;
}
}
// 返回完整的URL
return `${import.meta.env.VITE_APP_BASE_API}/profile/${processedUrl}`;
}
// 添加一个直接文件下载处理函数,处理点击下载事件
function handleDownload(url: string, filename: string) {
const fullUrl = getAttachmentUrl(url);
// 显示下载中状态
const downloadStatus = ref('');
downloadStatus.value = '下载中...';
// 使用fetch API下载文件
fetch(fullUrl)
.then(response => {
if (!response.ok) {
throw new Error(`下载失败: ${response.status}`);
}
return response.blob();
})
.then(blob => {
// 创建临时下载链接
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = filename || getFileName(url);
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
downloadStatus.value = '下载成功';
})
.catch(error => {
console.error('下载文件时出错:', error, fullUrl);
downloadStatus.value = '下载失败';
// 如果下载失败,尝试直接打开链接
window.open(fullUrl, '_blank');
});
}
// 后台管理中的附件删除处理函数
// 1. 首先在FileUpload组件中添加更健壮的文件路径处理函数
function normalizeFilePath(url:any) {
if (!url) return '';
// 去除基础URL前缀
let path = url;
const baseUrl = import.meta.env.VITE_APP_BASE_API;
if (path.startsWith(baseUrl)) {
path = path.substring(baseUrl.length);
}
// 规范化路径
path = path.replace(/\\/g, '/');
// 处理/profile/前缀
if (path.startsWith('/profile/')) {
path = path.substring('/profile/'.length);
}
// 确保没有多余的斜杠
while (path.includes('//')) {
path = path.replace('//', '/');
}
// 去除开头的斜杠
if (path.startsWith('/')) {
path = path.substring(1);
}
return path;
}
// 2. 提取文件ID以便更精确地删除文件
function extractFileId(url) {
if (!url) return '';
// 尝试匹配文件格式 [年月日时分秒]_[随机字符串].[扩展名]
const match = url.match(/(\d{8}|\d{14})_[a-zA-Z0-9]+\.[a-zA-Z0-9]+$/);
if (match && match[0]) {
return match[0];
}
// 如果无法提取特定格式,则返回文件名
const fileName = url.split('/').pop();
return fileName || '';
}
// 3. 优化删除文件的函数,使用多种策略尝试删除文件
function handleDelete(index) {
if (index < 0 || index >= fileList.value.length) return;
const file = fileList.value[index];
if (!file || !file.url) return;
proxy.$modal.confirm('确定要删除该文件吗?').then(() => {
proxy.$modal.loading("删除文件中,请稍候...");
// 获取所有可能的文件路径表示形式
const normalizedPath = normalizeFilePath(file.url);
const fileId = extractFileId(file.url);
console.log("删除文件信息:");
console.log("- 原始URL:", file.url);
console.log("- 规范化路径:", normalizedPath);
console.log("- 文件ID:", fileId);
// 尝试策略1: 使用文件ID删除
deleteFileUsingId(fileId, index, () => {
// 尝试策略2: 使用规范化路径删除
deleteFileUsingPath(normalizedPath, index, () => {
// 尝试策略3: 使用原始URL删除
deleteFileUsingOriginalUrl(file.url, index, () => {
// 所有策略都失败时,直接从前端移除
console.warn("所有删除策略均失败,仅从前端移除文件");
removeFileFromList(index);
});
});
});
});
}
// 4. 使用文件ID尝试删除
function deleteFileUsingId(fileId, index, onFail) {
console.log("尝试通过文件ID删除:", fileId);
proxy.$http.post('/profile/delete-by-id', {
fileId: fileId
}).then(res => {
if (res.data && res.data.code === 200) {
removeFileFromList(index);
proxy.$modal.msgSuccess("文件删除成功");
} else {
console.warn("通过ID删除失败:", res.data?.msg);
onFail && onFail();
}
}).catch(err => {
console.error("通过ID删除请求失败:", err);
onFail && onFail();
});
}
// 5. 使用规范化路径尝试删除
function deleteFileUsingPath(path, index, onFail) {
console.log("尝试通过规范化路径删除:", path);
proxy.$http.post('/common/deleteFile', {
filePath: path
}).then(res => {
if (res.data && res.data.code === 200) {
removeFileFromList(index);
proxy.$modal.msgSuccess("文件删除成功");
} else {
console.warn("通过规范化路径删除失败:", res.data?.msg);
onFail && onFail();
}
}).catch(err => {
console.error("通过规范化路径删除请求失败:", err);
onFail && onFail();
});
}
// 6. 使用原始URL尝试删除
function deleteFileUsingOriginalUrl(url, index, onFail) {
console.log("尝试通过原始URL删除:", url);
proxy.$http.get('/common/deleteFile', {
params: { filePath: url }
}).then(res => {
if (res.data && res.data.code === 200) {
removeFileFromList(index);
proxy.$modal.msgSuccess("文件删除成功");
} else {
console.warn("通过原始URL删除失败:", res.data?.msg);
onFail && onFail();
}
}).catch(err => {
console.error("通过原始URL删除请求失败:", err);
onFail && onFail();
});
}
// 7. 从前端列表移除文件
function removeFileFromList(index) {
fileList.value.splice(index, 1);
emit("update:modelValue", listToString(fileList.value));
proxy.$modal.closeLoading();
}
</script>
<template>
<div class="form-container">
<TheNavbar />
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<div v-else-if="error" class="error">
<div class="container">
<h3>内容加载失败</h3>
<p>无法找到该表单内容或发生网络错误</p>
<router-link to="/" class="btn-home">返回首页</router-link>
</div>
</div>
<div v-else class="content">
<div class="container">
<div class="content-card">
<div class="header">
<div class="form-category">表单下载</div>
<h2>{{ page.title }}</h2>
<div class="meta">
<span><i class="icon-time"></i>{{ formatDate(page.createTime) }}</span>
<span><i class="icon-eye"></i>浏览次数: {{ page.viewCount }}</span>
<span v-if="page.author"><i class="icon-user"></i>作者: {{ page.author }}</span>
</div>
</div>
<div class="body" v-html="page.content"></div>
<!-- 附件列表 -->
<div class="attachments" v-if="attachmentList.length > 0">
<h3 class="attachments-title">附件下载</h3>
<ul class="attachment-list">
<li v-for="(item, index) in attachmentList" :key="index" class="attachment-item">
<span class="attachment-icon" :class="getAttachmentIconClass(item.name || item.url)"></span>
<span class="attachment-name">{{ item.name || getFileName(item.url) }}</span>
<a @click.prevent="handleDownload(item.url, item.name || getFileName(item.url))"
class="download-btn" href="javascript:void(0)">下载</a>
</li>
</ul>
</div>
<div class="footer">
<router-link to="/hasfjform" class="btn-more">查看更多表单</router-link>
<router-link to="/" class="btn-home">返回首页</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.form-container {
min-height: 100vh;
background-color: var(--color-background);
display: flex;
flex-direction: column;
width: 100%;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 50vh;
font-size: 1rem;
color: var(--color-text-light);
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: var(--color-primary);
animation: spin 1s ease-in-out infinite;
margin-bottom: 1rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error {
text-align: center;
padding: 4rem 0;
}
.error h3 {
margin-bottom: 1rem;
color: var(--color-danger);
font-size: 1.5rem;
}
.error p {
margin-bottom: 2rem;
color: var(--color-text-light);
}
.content {
padding: 0;
flex: 1;
background-color: var(--color-background);
width: 100%;
}
.content-card {
background-color: white;
border-radius: 0;
box-shadow: none;
padding: 2.5rem;
overflow: hidden;
max-width: 100%;
margin: 0;
}
.content .header {
text-align: center;
margin-bottom: 2.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--color-border-light);
position: relative;
}
.form-category {
display: inline-block;
background-color: var(--color-primary);
color: white;
padding: 0.3rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
margin-bottom: 0.8rem;
}
.content .header h2 {
margin-bottom: 1.5rem;
color: var(--color-text);
font-size: 1.8rem;
font-weight: 600;
}
.content .meta {
color: var(--color-text-light);
font-size: 0.9rem;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 1.5rem;
}
.content .meta span {
display: flex;
align-items: center;
}
.icon-time::before {
content: '🕒';
margin-right: 5px;
}
.icon-eye::before {
content: '👁️';
margin-right: 5px;
}
.icon-user::before {
content: '👤';
margin-right: 5px;
}
.content .body {
line-height: 1.8;
color: var(--color-text);
font-size: 1.1rem;
}
.content .body img {
max-width: 100%;
height: auto;
display: block;
margin: 1rem auto;
border-radius: 5px;
}
.content .body p {
margin-bottom: 1rem;
}
.content .body h1,
.content .body h2,
.content .body h3,
.content .body h4,
.content .body h5,
.content .body h6 {
margin: 1.5rem 0 1rem;
color: var(--color-text);
}
.content .body table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
.content .body table th,
.content .body table td {
border: 1px solid #eee;
padding: 0.5rem;
text-align: left;
}
.content .body table th {
background-color: #f8f9fa;
}
.attachments {
background-color: #f9f9f9;
border-radius: 8px;
margin-top: 2rem;
padding: 1.5rem;
}
.attachments-title {
margin-bottom: 1.2rem;
color: var(--color-text);
font-size: 1.2rem;
font-weight: 600;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
padding-bottom: 0.8rem;
}
.attachment-list {
list-style: none;
margin: 0;
padding: 0;
}
.attachment-item {
display: flex;
align-items: center;
padding: 0.8rem 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.attachment-item:last-child {
border-bottom: none;
}
.attachment-icon {
width: 24px;
height: 24px;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
margin-right: 12px;
}
.attachment-name {
flex: 1;
color: var(--color-text);
font-size: 0.95rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.download-btn {
background-color: var(--color-primary);
color: white;
border: none;
border-radius: 4px;
padding: 0.4rem 0.8rem;
font-size: 0.85rem;
text-decoration: none;
transition: background-color 0.3s, transform 0.3s;
cursor: pointer;
}
.download-btn:hover {
background-color: var(--color-primary-dark);
transform: translateY(-2px);
}
.content .footer {
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px dashed #eee;
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.btn-home, .btn-more {
display: inline-block;
padding: 0.6rem 1.5rem;
background-color: var(--color-primary);
color: white;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.3s, transform 0.3s;
font-size: 0.9rem;
}
.btn-home:hover, .btn-more:hover {
background-color: #0069d9;
transform: translateY(-2px);
}
.btn-more {
background-color: var(--color-success);
}
.btn-more:hover {
background-color: #218838;
}
@media (max-width: 768px) {
.content {
padding: 0;
}
.content-card {
padding: 1.5rem;
}
.content .header h2 {
font-size: 1.5rem;
}
.content .body {
font-size: 0.95rem;
}
.attachments {
padding: 1.2rem;
}
}
@media (max-width: 576px) {
.content {
padding: 0;
}
.content-card {
padding: 1rem;
border-radius: 0;
}
.content .header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
}
.content .header h2 {
font-size: 1.3rem;
margin-bottom: 0.8rem;
}
.content .meta {
font-size: 0.8rem;
gap: 1rem;
}
.content .body {
font-size: 0.9rem;
}
.attachments {
padding: 1rem;
}
.attachment-item {
padding: 0.6rem 0;
}
.content .footer {
margin-top: 2rem;
padding-top: 1rem;
}
.btn-home, .btn-more {
padding: 0.5rem 1.2rem;
font-size: 0.85rem;
}
}
@media (min-width: 1200px) {
.content-card {
border-radius: 0;
max-width: 100%;
}
.content .body {
padding: 0;
}
.content {
padding: 0;
}
}
</style>