1298 lines
31 KiB
Vue
Raw Normal View History

2025-06-02 21:36:36 +08:00
<script setup lang="ts">
2025-06-02 21:36:36 +08:00
import { ref, onMounted, computed, reactive } from 'vue'
2025-06-02 21:36:36 +08:00
import { useRoute } from 'vue-router'
2025-06-02 21:36:36 +08:00
import {
getPageDetail,
updateViewCount,
addFeedback,
listFeedback,
queryFeedbackDetail,
} from '../services/api'
2025-06-02 21:36:36 +08:00
import TheNavbar from '../components/TheNavbar.vue'
const route = useRoute()
const loading = ref(true)
const error = ref(false)
const page = ref<any>(null)
2025-06-02 21:36:36 +08:00
const feedbackForm = reactive({
legalArticle: '',
issueDescription: '',
userName: '',
phoneNumber: '',
idCardNumber: '',
contactInfoType: 1, // 默认手机号
})
const feedbackSubmitSuccess = ref(false)
const feedbackNoReturned = ref('')
// 处理内容,将#标题#格式的文本转换为可折叠卡片
const processedContent = computed(() => {
if (!page.value || !page.value.content) return ''
// 正则表达式匹配 #标题# 格式
const regex = /#([^#]+)#([\s\S]*?)(?=#[^#]+#|$)/g
let index = 0
// 替换为可折叠卡片HTML不使用内联onclick
const result = page.value.content.replace(
regex,
(match: string, title: string, content: string) => {
// 去除前后空白
title = title.trim()
content = content.trim()
const currentIndex = index++
return `
<div class="law-card" data-card-index="${currentIndex}">
<div class="law-card-header" data-card-index="${currentIndex}">
<span class="law-card-title">${title}</span>
<span class="law-card-toggle"></span>
</div>
<div class="law-card-content">
${content}
</div>
</div>
`
},
)
return result
})
// 展开或折叠卡片的处理函数
const expandedCards = ref<Set<number>>(new Set())
const toggleCard = (index: number) => {
const card = document.querySelector(`.law-card[data-card-index="${index}"]`)
if (card) {
if (expandedCards.value.has(index)) {
expandedCards.value.delete(index)
card.classList.remove('expanded')
} else {
expandedCards.value.add(index)
card.classList.add('expanded')
}
}
}
2025-06-02 21:36:36 +08:00
// 计算附件列表
const attachmentList = computed(() => {
2025-06-02 21:36:36 +08:00
if (!page.value || !page.value.multiAttachments) return []
2025-06-02 21:36:36 +08:00
try {
2025-06-02 21:36:36 +08:00
const parsed = JSON.parse(page.value.multiAttachments)
2025-06-02 21:36:36 +08:00
// 过滤无效的附件项
2025-06-02 21:36:36 +08:00
return Array.isArray(parsed) ? parsed.filter((item) => item && (item.url || item.name)) : []
2025-06-02 21:36:36 +08:00
} catch (e) {
2025-06-02 21:36:36 +08:00
console.error('解析附件列表失败:', e)
return []
2025-06-02 21:36:36 +08:00
}
2025-06-02 21:36:36 +08:00
})
2025-06-02 21:36:36 +08:00
onMounted(async () => {
try {
const formatId = route.query.Id as string
if (!formatId) {
2025-06-02 21:36:36 +08:00
console.error('缺少必要的formatId参数')
error.value = true
loading.value = false
return
2025-06-02 21:36:36 +08:00
}
2025-06-02 21:36:36 +08:00
console.log('开始获取表单详情formatId:', formatId)
2025-06-02 21:36:36 +08:00
// 获取表单数据
2025-06-02 21:36:36 +08:00
const result = await getPageDetail('form', formatId)
console.log('表单详情请求结果:', result)
2025-06-02 21:36:36 +08:00
if (result && result.code === 200 && result.data) {
2025-06-02 21:36:36 +08:00
page.value = result.data
console.log('成功获取表单详情:', page.value)
2025-06-02 21:36:36 +08:00
// 更新浏览量
try {
2025-06-02 21:36:36 +08:00
const viewResult = (await updateViewCount('form', formatId)) as any
console.log('更新浏览量结果:', viewResult)
2025-06-02 21:36:36 +08:00
// 确认服务器端更新成功
if (viewResult && viewResult.code === 200) {
// 如果后端返回了新的浏览量数据
if (viewResult.data && typeof viewResult.data.viewCount === 'number') {
2025-06-02 21:36:36 +08:00
page.value.viewCount = viewResult.data.viewCount
2025-06-02 21:36:36 +08:00
}
// 否则本地递增浏览量
2025-06-02 21:36:36 +08:00
else if (
typeof page.value.viewCount === 'number' ||
typeof page.value.viewCount === 'string'
) {
page.value.viewCount = Number(page.value.viewCount || 0) + 1
2025-06-02 21:36:36 +08:00
} else {
2025-06-02 21:36:36 +08:00
page.value.viewCount = 1 // 初始浏览量
2025-06-02 21:36:36 +08:00
}
2025-06-02 21:36:36 +08:00
console.log('浏览量更新为:', page.value.viewCount)
2025-06-02 21:36:36 +08:00
} else {
2025-06-02 21:36:36 +08:00
console.warn('浏览量更新API返回错误:', viewResult?.msg || '未知错误')
2025-06-02 21:36:36 +08:00
}
} catch (e) {
2025-06-02 21:36:36 +08:00
console.error('更新浏览量失败:', e)
2025-06-02 21:36:36 +08:00
// 出错时也尝试本地增加浏览量
if (typeof page.value.viewCount === 'number' || typeof page.value.viewCount === 'string') {
2025-06-02 21:36:36 +08:00
page.value.viewCount = Number(page.value.viewCount || 0) + 1
console.log('API出错本地更新浏览量为:', page.value.viewCount)
2025-06-02 21:36:36 +08:00
}
}
// 检查返回的数据是否确实是表单类型
if (page.value.pageType && page.value.pageType !== 'form') {
2025-06-02 21:36:36 +08:00
console.error(`错误: 请求的是表单(form),但返回的是${page.value.pageType}类型的内容`)
2025-06-02 21:36:36 +08:00
// 不再尝试重新获取,而是直接显示错误
2025-06-02 21:36:36 +08:00
error.value = true
loading.value = false
return
2025-06-02 21:36:36 +08:00
}
// 处理可能的中文乱码问题
if (page.value.title && /\\u|%/.test(page.value.title)) {
try {
2025-06-02 21:36:36 +08:00
page.value.title = decodeURIComponent(page.value.title)
2025-06-02 21:36:36 +08:00
} catch (e) {
2025-06-02 21:36:36 +08:00
console.error('标题解码失败:', e)
2025-06-02 21:36:36 +08:00
}
}
// 检查内容是否为HTML格式
if (page.value.content) {
2025-06-02 21:36:36 +08:00
console.log('内容类型:', typeof page.value.content)
console.log('内容前50个字符:', page.value.content.substring(0, 50))
2025-06-02 21:36:36 +08:00
// 处理可能的中文乱码问题
if (/\\u|%/.test(page.value.content)) {
try {
2025-06-02 21:36:36 +08:00
page.value.content = decodeURIComponent(page.value.content)
2025-06-02 21:36:36 +08:00
} catch (e) {
2025-06-02 21:36:36 +08:00
console.error('内容解码失败:', e)
2025-06-02 21:36:36 +08:00
}
}
// 确保内容是HTML字符串
if (typeof page.value.content !== 'string') {
try {
2025-06-02 21:36:36 +08:00
page.value.content = JSON.stringify(page.value.content)
2025-06-02 21:36:36 +08:00
} catch (e) {
2025-06-02 21:36:36 +08:00
console.error('内容格式转换失败:', e)
2025-06-02 21:36:36 +08:00
}
}
} else {
2025-06-02 21:36:36 +08:00
console.warn('页面内容为空')
page.value.content = '<p>暂无内容</p>'
2025-06-02 21:36:36 +08:00
}
// 处理附件信息
if (page.value.attachmentUrl) {
2025-06-02 21:36:36 +08:00
console.log('附件URL:', page.value.attachmentUrl)
2025-06-02 21:36:36 +08:00
// 处理可能的附件URL乱码
if (/\\u|%/.test(page.value.attachmentUrl)) {
try {
2025-06-02 21:36:36 +08:00
page.value.attachmentUrl = decodeURIComponent(page.value.attachmentUrl)
2025-06-02 21:36:36 +08:00
} catch (e) {
2025-06-02 21:36:36 +08:00
console.error('附件URL解码失败:', e)
2025-06-02 21:36:36 +08:00
}
}
}
if (page.value.multiAttachments) {
try {
// 处理可能的多附件数据乱码
2025-06-02 21:36:36 +08:00
let attachmentsStr = page.value.multiAttachments
2025-06-02 21:36:36 +08:00
if (/\\u|%/.test(attachmentsStr)) {
try {
2025-06-02 21:36:36 +08:00
attachmentsStr = decodeURIComponent(attachmentsStr)
2025-06-02 21:36:36 +08:00
} catch (e) {
2025-06-02 21:36:36 +08:00
console.error('多附件数据解码失败:', e)
2025-06-02 21:36:36 +08:00
}
}
2025-06-02 21:36:36 +08:00
const attachments = JSON.parse(attachmentsStr)
console.log('多附件数据:', attachments)
2025-06-02 21:36:36 +08:00
// 处理附件数组中的每个附件URL
if (Array.isArray(attachments)) {
attachments.forEach((attachment, index) => {
if (attachment.url && /\\u|%/.test(attachment.url)) {
try {
2025-06-02 21:36:36 +08:00
attachment.url = decodeURIComponent(attachment.url)
2025-06-02 21:36:36 +08:00
} catch (e) {
2025-06-02 21:36:36 +08:00
console.error(`附件${index}URL解码失败:`, e)
2025-06-02 21:36:36 +08:00
}
}
2025-06-02 21:36:36 +08:00
})
2025-06-02 21:36:36 +08:00
// 更新页面的多附件数据
2025-06-02 21:36:36 +08:00
page.value.multiAttachments = JSON.stringify(attachments)
2025-06-02 21:36:36 +08:00
}
} catch (e) {
2025-06-02 21:36:36 +08:00
console.error('解析多附件数据失败:', e)
2025-06-02 21:36:36 +08:00
}
}
// 更新页面标题
2025-06-02 21:36:36 +08:00
document.title = `${page.value.title || '表单详情'} - 表单下载`
2025-06-02 21:36:36 +08:00
} else {
2025-06-02 21:36:36 +08:00
console.error('获取表单详情失败:', result?.msg || '未知错误')
error.value = true
2025-06-02 21:36:36 +08:00
}
} catch (e) {
2025-06-02 21:36:36 +08:00
console.error('获取表单详情异常:', e)
error.value = true
2025-06-02 21:36:36 +08:00
} finally {
2025-06-02 21:36:36 +08:00
loading.value = false
2025-06-02 21:36:36 +08:00
}
})
2025-06-02 21:36:36 +08:00
// 添加一个初始化折叠卡片功能的方法
const initCollapsibleCards = () => {
console.log('初始化折叠卡片...')
// 使用事件委托在父元素上监听点击事件
document.querySelector('.body')?.addEventListener('click', (e) => {
const target = e.target as HTMLElement
const header = target.closest('.law-card-header')
if (header) {
const index = parseInt(header.getAttribute('data-card-index') || '-1')
if (index >= 0) {
toggleCard(index)
console.log('切换卡片状态:', index)
}
}
})
console.log('折叠卡片初始化完成')
}
// 在内容更新后初始化折叠卡片
onMounted(() => {
// 内容加载完成后初始化折叠卡片
setTimeout(() => {
initCollapsibleCards()
}, 1000)
})
2025-06-02 21:36:36 +08:00
// 格式化日期
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) {
2025-06-02 21:36:36 +08:00
case 'pdf':
return 'icon-pdf'
2025-06-02 21:36:36 +08:00
case 'doc':
2025-06-02 21:36:36 +08:00
case 'docx':
return 'icon-word'
2025-06-02 21:36:36 +08:00
case 'xls':
2025-06-02 21:36:36 +08:00
case 'xlsx':
return 'icon-excel'
2025-06-02 21:36:36 +08:00
case 'ppt':
2025-06-02 21:36:36 +08:00
case 'pptx':
return 'icon-ppt'
2025-06-02 21:36:36 +08:00
case 'zip':
2025-06-02 21:36:36 +08:00
case 'rar':
return 'icon-archive'
2025-06-02 21:36:36 +08:00
case 'png':
case 'jpg':
case 'jpeg':
2025-06-02 21:36:36 +08:00
case 'gif':
return 'icon-image'
default:
return 'icon-file'
2025-06-02 21:36:36 +08:00
}
}
// 从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
}
// 处理可能的格式问题
2025-06-02 21:36:36 +08:00
let processedUrl = url
2025-06-02 21:36:36 +08:00
// 替换反斜杠为正斜杠
2025-06-02 21:36:36 +08:00
processedUrl = processedUrl.replace(/\\/g, '/')
2025-06-02 21:36:36 +08:00
// 确保不存在连续的双斜杠
while (processedUrl.includes('//')) {
2025-06-02 21:36:36 +08:00
processedUrl = processedUrl.replace('//', '/')
2025-06-02 21:36:36 +08:00
}
// 去除开头的斜杠,因为后续我们会添加
if (processedUrl.startsWith('/')) {
2025-06-02 21:36:36 +08:00
processedUrl = processedUrl.substring(1)
2025-06-02 21:36:36 +08:00
}
// 检查是否已经包含profile前缀
if (processedUrl.startsWith('profile/')) {
2025-06-02 21:36:36 +08:00
return `${import.meta.env.VITE_APP_BASE_API}/${processedUrl}`
2025-06-02 21:36:36 +08:00
}
// 检查是否为年月日格式的路径
2025-06-02 21:36:36 +08:00
const datePattern = /^\d{4}\/\d{2}\/\d{2}\//
2025-06-02 21:36:36 +08:00
if (datePattern.test(processedUrl)) {
// 如果不包含files/master前缀但符合日期格式则添加
if (!processedUrl.startsWith('files/master/')) {
2025-06-02 21:36:36 +08:00
processedUrl = `files/master/${processedUrl}`
2025-06-02 21:36:36 +08:00
}
}
// 返回完整的URL
2025-06-02 21:36:36 +08:00
return `${import.meta.env.VITE_APP_BASE_API}/profile/${processedUrl}`
2025-06-02 21:36:36 +08:00
}
// 添加一个直接文件下载处理函数,处理点击下载事件
function handleDownload(url: string, filename: string) {
2025-06-02 21:36:36 +08:00
const fullUrl = getAttachmentUrl(url)
2025-06-02 21:36:36 +08:00
// 显示下载中状态
2025-06-02 21:36:36 +08:00
const downloadStatus = ref('')
downloadStatus.value = '下载中...'
2025-06-02 21:36:36 +08:00
// 使用fetch API下载文件
fetch(fullUrl)
2025-06-02 21:36:36 +08:00
.then((response) => {
2025-06-02 21:36:36 +08:00
if (!response.ok) {
2025-06-02 21:36:36 +08:00
throw new Error(`下载失败: ${response.status}`)
2025-06-02 21:36:36 +08:00
}
2025-06-02 21:36:36 +08:00
return response.blob()
2025-06-02 21:36:36 +08:00
})
2025-06-02 21:36:36 +08:00
.then((blob) => {
2025-06-02 21:36:36 +08:00
// 创建临时下载链接
2025-06-02 21:36:36 +08:00
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 = '下载成功'
2025-06-02 21:36:36 +08:00
})
2025-06-02 21:36:36 +08:00
.catch((error) => {
console.error('下载文件时出错:', error, fullUrl)
downloadStatus.value = '下载失败'
2025-06-02 21:36:36 +08:00
// 如果下载失败,尝试直接打开链接
2025-06-02 21:36:36 +08:00
window.open(fullUrl, '_blank')
})
2025-06-02 21:36:36 +08:00
}
2025-06-02 21:36:36 +08:00
const submitFeedback = async () => {
try {
// 前端验证
if (!feedbackForm.legalArticle.trim()) {
alert('有问题的表单或案例不能为空。')
return
}
if (!feedbackForm.issueDescription.trim()) {
alert('问题描述不能为空。')
return
}
2025-06-02 21:36:36 +08:00
2025-06-02 21:36:36 +08:00
if (feedbackForm.contactInfoType === 1 && !feedbackForm.phoneNumber.trim()) {
alert('手机号不能为空。')
return
} else if (feedbackForm.contactInfoType === 2 && !feedbackForm.idCardNumber.trim()) {
alert('身份证号不能为空。')
return
} else if (feedbackForm.contactInfoType === 3 && !feedbackForm.userName.trim()) {
alert('姓名不能为空。')
return
2025-06-02 21:36:36 +08:00
}
2025-06-02 21:36:36 +08:00
const dataToSend = { ...feedbackForm }
// 根据选择的联系方式类型设置对应的值,其他值置空
if (feedbackForm.contactInfoType === 1) {
dataToSend.idCardNumber = ''
dataToSend.userName = ''
} else if (feedbackForm.contactInfoType === 2) {
dataToSend.phoneNumber = ''
dataToSend.userName = ''
} else if (feedbackForm.contactInfoType === 3) {
dataToSend.phoneNumber = ''
dataToSend.idCardNumber = ''
2025-06-02 21:36:36 +08:00
}
2025-06-02 21:36:36 +08:00
const res = await addFeedback(dataToSend)
if (res && res.code === 200) {
feedbackSubmitSuccess.value = true
feedbackNoReturned.value = res.data?.feedbackNo || '未知'
// 清空表单
feedbackForm.legalArticle = ''
feedbackForm.issueDescription = ''
feedbackForm.userName = ''
feedbackForm.phoneNumber = ''
feedbackForm.idCardNumber = ''
feedbackForm.contactInfoType = 1
console.log('留言提交成功,查询编号:', feedbackNoReturned.value)
2025-06-02 21:36:36 +08:00
} else {
2025-06-02 21:36:36 +08:00
alert('提交留言失败:' + (res.msg || '未知错误'))
2025-06-02 21:36:36 +08:00
}
2025-06-02 21:36:36 +08:00
} catch (e) {
console.error('提交留言异常:', e)
alert('提交留言异常,请稍后再试')
}
2025-06-02 21:36:36 +08:00
}
2025-06-02 21:36:36 +08:00
const queryFeedback = async () => {
// 实现查询留言回复的逻辑
2025-06-02 21:36:36 +08:00
}
2025-06-02 21:36:36 +08:00
// 删除有类型错误的后台管理相关的函数,因为它们在前台页面不会被使用
2025-06-02 21:36:36 +08:00
</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>
2025-06-02 21:36:36 +08:00
<div class="body" v-html="processedContent"></div>
2025-06-02 21:36:36 +08:00
<!-- 附件列表 -->
<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">
2025-06-02 21:36:36 +08:00
<span
class="attachment-icon"
:class="getAttachmentIconClass(item.name || item.url)"
></span>
2025-06-02 21:36:36 +08:00
<span class="attachment-name">{{ item.name || getFileName(item.url) }}</span>
2025-06-02 21:36:36 +08:00
<a
@click.prevent="handleDownload(item.url, item.name || getFileName(item.url))"
class="download-btn"
href="javascript:void(0)"
>下载</a
>
2025-06-02 21:36:36 +08:00
</li>
</ul>
</div>
<div class="footer">
<router-link to="/hasfjform" class="btn-more">查看更多表单</router-link>
<router-link to="/" class="btn-home">返回首页</router-link>
</div>
2025-06-02 21:36:36 +08:00
<!-- 用户留言模块 -->
<div class="feedback-section">
<h3><i class="icon-chat"></i> 表单咨询与留言</h3>
<div v-if="!feedbackSubmitSuccess" class="feedback-form">
<p class="section-description">如果您对表单下载有任何疑问或建议欢迎在此留言</p>
<div class="form-group">
<label for="legalArticle">有问题的表单或案例:<span class="required">*</span></label>
<input
type="text"
id="legalArticle"
v-model="feedbackForm.legalArticle"
placeholder="如:公司设立登记申请表"
/>
</div>
<div class="form-group">
<label for="issueDescription">问题描述: <span class="required">*</span></label>
<textarea
id="issueDescription"
v-model="feedbackForm.issueDescription"
rows="5"
placeholder="请详细描述您的问题字数在10-500字之间"
required
></textarea>
</div>
<div class="form-group">
<label for="contactInfoType">联系方式类型:</label>
<select id="contactInfoType" v-model="feedbackForm.contactInfoType">
<option :value="1">手机号</option>
<option :value="2">身份证号</option>
<option :value="3">姓名</option>
</select>
</div>
<div class="form-group">
<label for="phoneNumber"
>手机号:
<span class="required" v-show="feedbackForm.contactInfoType === 1">*</span></label
>
<input
type="text"
id="phoneNumber"
v-model="feedbackForm.phoneNumber"
placeholder="请输入手机号"
:required="feedbackForm.contactInfoType === 1"
/>
</div>
<div class="form-group">
<label for="idCardNumber"
>身份证号:
<span class="required" v-show="feedbackForm.contactInfoType === 2">*</span></label
>
<input
type="text"
id="idCardNumber"
v-model="feedbackForm.idCardNumber"
placeholder="请输入身份证号"
:required="feedbackForm.contactInfoType === 2"
/>
</div>
<div class="form-group">
<label for="userNameContact"
>姓名:
<span class="required" v-show="feedbackForm.contactInfoType === 3">*</span></label
>
<input
type="text"
id="userNameContact"
v-model="feedbackForm.userName"
placeholder="请输入您的姓名"
:required="feedbackForm.contactInfoType === 3"
/>
</div>
<button @click="submitFeedback" class="submit-btn">提交留言</button>
</div>
<div v-else class="feedback-success">
<h4>留言提交成功</h4>
<p>
您的留言查询编号是<span class="feedback-no">{{ feedbackNoReturned }}</span>
</p>
<p>请牢记此编号以便查询回复</p>
<button
@click="((feedbackSubmitSuccess = false), (feedbackNoReturned = ''))"
class="submit-btn"
>
继续留言
</button>
</div>
</div>
2025-06-02 21:36:36 +08:00
</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 {
2025-06-02 21:36:36 +08:00
to {
transform: rotate(360deg);
}
2025-06-02 21:36:36 +08:00
}
.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;
}
2025-06-02 21:36:36 +08:00
/* 添加法律卡片相关样式 */
:deep(.law-card) {
margin: 1.5rem 0;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
background-color: #f9f9f9;
}
:deep(.law-card-header) {
padding: 1rem 1.5rem;
background-color: #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: background-color 0.3s;
user-select: none; /* 防止选中文本 */
}
:deep(.law-card-header:hover) {
background-color: #e0e0e0;
}
:deep(.law-card-title) {
font-weight: bold;
color: #333;
font-size: 1.1rem;
}
:deep(.law-card-toggle) {
transition: transform 0.3s;
}
:deep(.law-card-content) {
padding: 0;
max-height: 0;
overflow: hidden;
transition:
max-height 0.5s ease,
padding 0.5s ease;
}
:deep(.law-card.expanded .law-card-content) {
padding: 1.5rem;
max-height: 2000px;
}
:deep(.law-card.expanded .law-card-toggle) {
transform: rotate(180deg);
}
2025-06-02 21:36:36 +08:00
.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;
2025-06-02 21:36:36 +08:00
transition:
background-color 0.3s,
transform 0.3s;
2025-06-02 21:36:36 +08:00
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;
}
2025-06-02 21:36:36 +08:00
.btn-home,
.btn-more {
2025-06-02 21:36:36 +08:00
display: inline-block;
padding: 0.6rem 1.5rem;
background-color: var(--color-primary);
color: white;
text-decoration: none;
border-radius: 4px;
2025-06-02 21:36:36 +08:00
transition:
background-color 0.3s,
transform 0.3s;
2025-06-02 21:36:36 +08:00
font-size: 0.9rem;
}
2025-06-02 21:36:36 +08:00
.btn-home:hover,
.btn-more:hover {
2025-06-02 21:36:36 +08:00
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;
}
2025-06-02 21:36:36 +08:00
:deep(.law-card-title) {
font-size: 1rem;
}
2025-06-02 21:36:36 +08:00
}
@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;
}
2025-06-02 21:36:36 +08:00
.btn-home,
.btn-more {
2025-06-02 21:36:36 +08:00
padding: 0.5rem 1.2rem;
font-size: 0.85rem;
}
2025-06-02 21:36:36 +08:00
:deep(.law-card-header) {
padding: 0.8rem 1rem;
}
:deep(.law-card-title) {
font-size: 0.9rem;
}
:deep(.law-card.expanded .law-card-content) {
padding: 1rem;
}
2025-06-02 21:36:36 +08:00
}
@media (min-width: 1200px) {
.content-card {
border-radius: 0;
max-width: 100%;
}
.content .body {
padding: 0;
}
.content {
padding: 0;
}
}
2025-06-02 21:36:36 +08:00
.feedback-section {
background-color: #f9f9f9;
border-radius: 8px;
margin-top: 2rem;
padding: 2rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.feedback-section h3 {
font-size: 1.5rem;
color: var(--color-primary);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
}
.feedback-section .icon-chat::before {
content: '💬'; /* Chat bubble icon */
margin-right: 10px;
font-size: 1.8rem;
}
.feedback-section .icon-search::before {
content: '🔍'; /* Search icon */
margin-right: 10px;
font-size: 1.8rem;
}
.feedback-section .section-description {
color: var(--color-text-light);
margin-bottom: 1.5rem;
line-height: 1.6;
}
.feedback-form .form-group,
.feedback-query .form-group {
margin-bottom: 1rem;
}
.feedback-form label,
.feedback-query label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--color-text);
}
.feedback-form input[type='text'],
.feedback-form textarea,
.feedback-form select,
.feedback-query input[type='text'],
.feedback-query select {
width: calc(100% - 20px);
padding: 0.8rem 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
}
.feedback-form textarea {
resize: vertical;
}
.feedback-form .required,
.feedback-query .required {
color: var(--color-danger);
margin-left: 5px;
}
.feedback-form .submit-btn,
.feedback-query .submit-btn {
display: inline-block;
padding: 0.8rem 2rem;
background-color: var(--color-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
margin-top: 1rem;
}
.feedback-form .submit-btn:hover,
.feedback-query .submit-btn:hover {
background-color: #0069d9;
}
.feedback-query .submit-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.feedback-success {
text-align: center;
padding: 2rem;
background-color: #e6ffe6;
border: 1px solid #aaffaa;
border-radius: 8px;
color: #336633;
}
.feedback-success h4 {
color: #28a745;
margin-bottom: 1rem;
font-size: 1.3rem;
}
.feedback-success .feedback-no {
font-weight: bold;
color: var(--color-primary);
font-size: 1.5rem;
}
.feedback-success button {
margin-top: 1.5rem;
padding: 0.7rem 1.5rem;
background-color: var(--color-success);
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
transition: background-color 0.3s;
}
.feedback-success button:hover {
background-color: #218838;
}
.query-title {
margin-top: 3rem;
border-top: 1px dashed #eee;
padding-top: 2rem;
}
.query-result {
background-color: #e9f7fe;
border: 1px solid #b3e0ff;
border-radius: 8px;
padding: 1.5rem;
margin-top: 2rem;
}
.query-result h4 {
color: #007bff;
margin-bottom: 1rem;
font-size: 1.3rem;
padding-bottom: 0.8rem;
border-bottom: 1px dashed #cceeff;
}
.query-result .result-item {
margin-bottom: 0.8rem;
color: var(--color-text);
font-size: 0.95rem;
}
.query-result .result-item strong {
color: #333;
}
.query-result .admin-reply-content {
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 1rem;
margin-top: 0.5rem;
line-height: 1.6;
color: #555;
}
.error-message {
color: var(--color-danger);
margin-top: 1rem;
font-size: 0.9rem;
}
.status-pending {
color: orange;
font-weight: bold;
}
.status-replied {
color: green;
font-weight: bold;
}
.status-closed {
color: gray;
font-weight: bold;
}
@media (max-width: 768px) {
.feedback-section {
padding: 1.5rem;
}
}
@media (max-width: 576px) {
.feedback-section {
padding: 1rem;
}
.feedback-section h3 {
font-size: 1.3rem;
}
.feedback-section .icon-chat::before,
.feedback-section .icon-search::before {
font-size: 1.5rem;
}
.feedback-form input[type='text'],
.feedback-form textarea,
.feedback-form select,
.feedback-query input[type='text'],
.feedback-query select {
padding: 0.6rem 8px;
font-size: 0.9rem;
}
.feedback-form .submit-btn,
.feedback-query .submit-btn {
padding: 0.6rem 1.5rem;
font-size: 0.9rem;
}
.feedback-success h4 {
font-size: 1.1rem;
}
.feedback-success .feedback-no {
font-size: 1.3rem;
}
.query-result h4 {
font-size: 1.1rem;
}
.query-result .result-item {
font-size: 0.85rem;
}
}
2025-06-02 21:36:36 +08:00
</style>