mirror of
https://gitee.com/myxzgzs/boyuehasfj-vue3-html.git
synced 2025-08-08 23:22:43 +08:00
859 lines
21 KiB
Vue
859 lines
21 KiB
Vue
![]() |
<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>
|