diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index cde004d..8b36025 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -388,7 +388,9 @@ export async function uploadDocumentToServer( let responseData; try { responseData = await response.json(); - // console.log('【调试】JSON响应解析成功:', responseData); + // console.log('【上传调试】服务器原始JSON响应:', responseData); + // console.log('【上传调试】响应类型:', typeof responseData); + // console.log('【上传调试】响应keys:', Object.keys(responseData)); } catch (jsonError) { console.error('【调试】JSON解析失败:', jsonError); return { @@ -396,15 +398,21 @@ export async function uploadDocumentToServer( status: 500 }; } - + const extractedData = extractApiData(responseData); - // console.log('【调试】提取的数据:', extractedData); - + // console.log('【上传调试】提取后的数据:', extractedData); + // console.log('【上传调试】提取数据详情:', { + // hasData: !!extractedData, + // success: extractedData?.success, + // hasResult: !!extractedData?.result, + // error: extractedData?.error + // }); + if (!extractedData) { - console.error('【调试】无法提取数据'); + console.error('【调试】无法提取数据,原始响应:', JSON.stringify(responseData)); return { error: '处理上传响应失败', status: 500 }; } - + // console.log('【调试】上传成功,返回数据'); return { data: extractedData }; } catch (fetchError) { diff --git a/app/components/layout/Layout.tsx b/app/components/layout/Layout.tsx index 698a3d7..93ca52c 100644 --- a/app/components/layout/Layout.tsx +++ b/app/components/layout/Layout.tsx @@ -38,21 +38,28 @@ export function Layout({ children, userRole = 'developer' as UserRole, frontendJ const [selectedApp, setSelectedApp] = useState(''); const matches = useMatches() as Match[]; const location = useLocation(); - + // 检查当前路径是否应该隐藏侧边栏 const noLayoutPaths = ['/login', '/']; const shouldHideSidebar = noLayoutPaths.includes(location.pathname); - + // 检查当前路由是否应该隐藏默认面包屑 - const shouldHideBreadcrumb = shouldHideSidebar || matches.some(match => + const shouldHideBreadcrumb = shouldHideSidebar || matches.some(match => match.handle && match.handle.hideBreadcrumb === true ); - + // 从sessionStorage中获取侧边栏状态和reviewType useEffect(() => { + // 检查是否为移动端 + const isMobile = window.innerWidth <= 768; + // 从localStorage获取侧边栏状态 const savedState = localStorage.getItem('sidebarCollapsed'); - if (savedState) { + + // 移动端默认收起,桌面端使用保存的状态 + if (isMobile) { + setSidebarCollapsed(true); + } else if (savedState) { setSidebarCollapsed(savedState === 'true'); } diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index 0a7dbfc..79f4e04 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -39,8 +39,24 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec const [isLoading, setIsLoading] = useState(true); // 添加加载状态 const [menuItems, setMenuItems] = useState([]); // 动态菜单项 const [isLoadingRoutes, setIsLoadingRoutes] = useState(true); // 路由加载状态 + const [isMobile, setIsMobile] = useState(false); // 移动端检测 const navigate = useNavigate(); + // 移动端检测 + useEffect(() => { + const checkMobile = () => { + const mobile = window.innerWidth <= 768; // 768px以下视为移动端 + setIsMobile(mobile); + }; + + // 初始检测 + checkMobile(); + + // 监听窗口大小变化 + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + // 获取用户路由权限 useEffect(() => { const fetchUserRoutes = async () => { @@ -254,8 +270,26 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec // }) return ( -
-
+ <> + {/* 移动端遮罩层 */} + {isMobile && !collapsed && ( +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onToggle(); + } + }} + /> + )} + +
+
{ navigate('/'); @@ -396,6 +430,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selec {!collapsed && 操作手册}
-
+
+ ); } \ No newline at end of file diff --git a/app/components/reviews/ReviewTabs.tsx b/app/components/reviews/ReviewTabs.tsx index 02f3e56..f9e46ef 100644 --- a/app/components/reviews/ReviewTabs.tsx +++ b/app/components/reviews/ReviewTabs.tsx @@ -10,7 +10,7 @@ import { UploadArea, type UploadAreaRef } from '~/components/ui/UploadArea'; import { Button } from '~/components/ui/Button'; import { toastService } from '~/components/ui/Toast'; // import { DOCUMENT_URL } from "~/api/axios-client"; -import { uploadFileToBinary, uploadDocumentToServer } from '~/api/files/files-upload'; +import { uploadContractTemplate } from '~/api/files/files-upload'; interface ReviewTabsProps { activeTab: string; @@ -22,6 +22,7 @@ interface ReviewTabsProps { path?: string; auditStatus?: number; type?: string; + comparisonId?: number; }; onConfirmResults: () => void; jwtToken?: string | null; @@ -117,23 +118,32 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi const handleTemplateFilesSelected = (files: FileList) => { try { if (files.length > 0) { - // 验证文件类型,只允许PDF文件 + // 验证文件类型,允许PDF和Word文件 const validFiles: File[] = []; let hasInvalidFiles = false; - + Array.from(files).forEach(file => { - if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { + const fileName = file.name.toLowerCase(); + const isValidType = + file.type === 'application/pdf' || + fileName.endsWith('.pdf') || + file.type === 'application/msword' || + file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || + fileName.endsWith('.doc') || + fileName.endsWith('.docx'); + + if (isValidType) { validFiles.push(file); } else { hasInvalidFiles = true; console.error(`无效的文件类型: ${file.name}, 类型: ${file.type}`); } }); - + if (hasInvalidFiles) { - toastService.error('只能上传PDF格式的文件'); + toastService.error('只能上传PDF或Word格式的文件'); } - + if (validFiles.length > 0) { setSelectedTemplateFiles(validFiles); } @@ -152,57 +162,42 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi return; } + // 验证必需的参数 + if (!fileInfo.id) { + toastService.error('文档ID不能为空'); + return; + } + try { setIsUploading(true); - - // 这里可以调用上传API - let binaryData: ArrayBuffer; - try { - binaryData = await uploadFileToBinary(selectedTemplateFiles[0]); - } catch (error) { - console.error('上传文件失败:', error); - throw new Error(`文件 ${selectedTemplateFiles[0].name} 转换失败: ${error instanceof Error ? error.message : '未知错误'}`); + + console.log('【重新上传模板】开始上传:', { + fileName: selectedTemplateFiles[0].name, + documentId: fileInfo.id, + comparisonId: fileInfo.comparisonId + }); + + // 调用专门的合同模板上传接口 + const uploadResult = await uploadContractTemplate( + selectedTemplateFiles[0], // file: File 对象 + fileInfo.id, // documentId: 合同文件的id + fileInfo.comparisonId, // comparisonId: 模板预览文件的id (可选) + jwtToken || undefined // jwtToken + ); + + console.log('【重新上传模板】上传结果:', uploadResult); + + if (uploadResult.error) { + throw new Error(uploadResult.error); } - // const uploadInfo = { - // binaryData, - // fileName: selectedTemplateFiles[0].name, - // fileType: 'pdf', - // documentType: '1', - // priority: 'normal', - // documentNumber: null, - // remark: null, - // isTestDocument: false, - // documentId: fileInfo - // }; - // console.log('uploadInfo',uploadInfo); - - const uploadResult = await uploadDocumentToServer( - binaryData, - selectedTemplateFiles[0].name, - 'pdf', //file_type 文件类型:pdf - '1', //fileType(type_id) 合同id:1 - 'normal', //priority 优先级:normal - null, //document_number 文档编号 - null, //remark 备注 - false, //is_test_document 是否为测试文档:false - fileInfo.id, //document_id 主文档id - true, //is_reupload 是否为重新上传:true - jwtToken || undefined, //jwtToken - ); - // console.log('重新上传合同模板',uploadResult); - - if (uploadResult.error) { - throw new Error(uploadResult.error); - } - toastService.success('模板文件上传成功,结构比对数据将会发生更新,即将返回上一页...'); await new Promise(resolve => setTimeout(resolve, 2000)); - + handleCloseReuploadModal(); handleBack(); - + } catch (error) { console.error('上传模板文件失败:', error); toastService.error(`上传失败: ${error instanceof Error ? error.message : '未知错误'}`); @@ -335,21 +330,21 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi

请选择新的模板文件用于结构比对。

- 注意:只支持PDF格式的文件,上传后将替换当前的比对模板。 + 注意:支持PDF和Word格式的文件,上传后将替换当前的比对模板。

- 支持格式:PDF + 支持格式:PDF、DOC、DOCX } disabled={isUploading} @@ -360,38 +355,50 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi

已选择的文件:

- {selectedTemplateFiles.map((file, index) => ( -
-
- -
-
- {file.name} -
-
- {formatFileSize(file.size)} + {selectedTemplateFiles.map((file, index) => { + // 根据文件类型确定图标和颜色 + const fileName = file.name.toLowerCase(); + let fileIcon = 'ri-file-pdf-line'; + let iconColor = 'text-red-500'; + + if (fileName.endsWith('.doc') || fileName.endsWith('.docx')) { + fileIcon = 'ri-file-word-2-line'; + iconColor = 'text-blue-600'; + } + + return ( +
+
+ +
+
+ {file.name} +
+
+ {formatFileSize(file.size)} +
+
- -
- ))} + ); + })}
)} diff --git a/app/routes/cross-checking._index.tsx b/app/routes/cross-checking._index.tsx index a553ef2..a5aa7d1 100644 --- a/app/routes/cross-checking._index.tsx +++ b/app/routes/cross-checking._index.tsx @@ -218,9 +218,10 @@ export default function CrossCheckingIndex() { const dateTo = searchParams.get('dateTo') || ''; const navigate = useNavigate(); const fetcher = useFetcher(); - + // 状态管理 const [isDeleting, setIsDeleting] = useState(false); + const [hasAutoOpened, setHasAutoOpened] = useState(false); // 标记是否已自动打开模态框 const [modalState, setModalState] = useState<{ isOpen: boolean; title: string; @@ -280,7 +281,13 @@ export default function CrossCheckingIndex() { // 处理文档查看 - 导航到评查详情页 const handleViewFile = (fileId: string) => { - navigate(`/cross-checking/result?id=${fileId}&tId=${currentTaskInfo?.taskId}&previousRoute=crossChecking`); + const params = new URLSearchParams({ + id: fileId, + tId: currentTaskInfo?.taskId?.toString() || '', + tName: currentTaskInfo?.taskName || '', + previousRoute: 'crossChecking' + }); + navigate(`/cross-checking/result?${params.toString()}`); }; // 存储当前任务信息用于分页 @@ -465,11 +472,38 @@ export default function CrossCheckingIndex() { + // 检测URL参数,自动打开模态框 + useEffect(() => { + const openModal = searchParams.get('openModal'); + const taskId = searchParams.get('taskId'); + const taskName = searchParams.get('taskName'); + + if (openModal === 'true' && taskId && !hasAutoOpened) { + console.log('[自动打开模态框] taskId:', taskId, 'taskName:', taskName); + + // 标记已自动打开,防止重复触发 + setHasAutoOpened(true); + + // 清除URL参数,避免刷新页面时再次打开 + const newParams = new URLSearchParams(searchParams); + newParams.delete('openModal'); + newParams.delete('taskId'); + newParams.delete('taskName'); + setSearchParams(newParams, { replace: true }); + + // 延迟自动打开模态框,确保状态已更新 + setTimeout(() => { + handleViewResult(Number(taskId), taskName || '任务详情'); + }, 100); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchParams, hasAutoOpened]); + // 监听fetcher状态变化 - 删除操作 useEffect(() => { if (fetcher.data && fetcher.state === 'idle' && isDeleting) { setIsDeleting(false); - + const data = fetcher.data as { result?: boolean; message?: string }; if (data.result) { toastService.success(data.message || '操作成功'); @@ -533,16 +567,7 @@ export default function CrossCheckingIndex() { dataIndex: "taskName" as keyof CrossCheckingTask, key: "taskName", align: "left" as const, - width: "16%", - render: (value: string, record: CrossCheckingTask) => ( - - ) + width: "16%" }, { title: "评查开始时间", diff --git a/app/routes/cross-checking.result.tsx b/app/routes/cross-checking.result.tsx index 4852aa7..d2ab6f3 100644 --- a/app/routes/cross-checking.result.tsx +++ b/app/routes/cross-checking.result.tsx @@ -23,7 +23,7 @@ */ import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { useNavigate, useLoaderData } from "@remix-run/react"; import crossCheckingStyles from "~/styles/cross-checking-result.css?url"; import { getReviewPoints, updateReviewResult} from "~/api/evaluation_points/reviews"; @@ -185,6 +185,7 @@ export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const id = url.searchParams.get('id') || undefined; const taskId = url.searchParams.get('tId') || undefined; + const taskName = url.searchParams.get('tName') || undefined; const previousRoute = url.searchParams.get('previousRoute') || ''; // console.log("id-------",id); if (!id) { @@ -225,7 +226,9 @@ export async function loader({ request }: LoaderFunctionArgs) { scoring_proposals: reviewData.scoring_proposals || [], userInfo: userInfo, jwtToken: frontendJWT, // 传递JWT token - isProposer: isProposer + isProposer: isProposer, + taskId: taskId, // 传递任务ID + taskName: taskName // 传递任务名称 }); } else { console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2)); @@ -294,14 +297,27 @@ export async function action({ request }: ActionFunctionArgs) { } export default function CrossCheckingResult() { + console.log('[组件] CrossCheckingResult 渲染'); + const navigate = useNavigate(); const loaderData = useLoaderData(); - const { document, reviewPoints, statistics, reviewInfo, scoring_proposals, jwtToken, userInfo, isProposer } = loaderData; + const { document, reviewPoints, statistics, reviewInfo, scoring_proposals, jwtToken, userInfo, isProposer, taskId, taskName } = loaderData; const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态 const [reviewData, setReviewData] = useState(null); const [activeReviewPointResultId, setActiveReviewPointResultId] = useState(null); const [targetPage, setTargetPage] = useState(undefined); const [localScoringProposals, setLocalScoringProposals] = useState(scoring_proposals || []); // 本地状态管理scoringProposals + + // 使用ref来跟踪loading状态,避免不必要的重新渲染 + const isProcessingRef = useRef(false); + + // 添加组件挂载/卸载日志 + useEffect(() => { + console.log('[组件] CrossCheckingResult 挂载'); + return () => { + console.log('[组件] CrossCheckingResult 卸载'); + }; + }, []); // 同步外部scoring_proposals到本地状态 useEffect(() => { @@ -309,9 +325,9 @@ export default function CrossCheckingResult() { }, [scoring_proposals]); // 处理意见提交成功的回调 - const handleOpinionSubmitted = (newProposal: ScoringProposal) => { + const handleOpinionSubmitted = useCallback((newProposal: ScoringProposal) => { setLocalScoringProposals(prev => [...prev, newProposal]); - }; + }, []); // loader 数据加载出错 useEffect(()=>{ @@ -371,7 +387,7 @@ export default function CrossCheckingResult() { }, [document, reviewPoints, statistics, reviewInfo]); - const handleReviewPointSelect = (reviewPointId: string, page?: number) => { + const handleReviewPointSelect = useCallback((reviewPointId: string, page?: number) => { // 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发 if (reviewPointId === activeReviewPointResultId && page) { setTargetPage(undefined); @@ -385,7 +401,7 @@ export default function CrossCheckingResult() { setActiveReviewPointResultId(reviewPointId); setTargetPage(page); } - }; + }, [activeReviewPointResultId]); // 处理评审点状态变更 @@ -500,28 +516,38 @@ export default function CrossCheckingResult() { * 2. 根据结果弹出确认模态框 * 3. 用户确认后更新文档状态并跳转 */ - const handleConfirmResults = async (event?: React.MouseEvent) => { - // 阻止默认行为,防止页面刷新 + const handleConfirmResults = useCallback(async (event?: React.MouseEvent) => { + console.log('[完成评查] 开始处理'); + + // 首先阻止默认行为和事件冒泡,防止页面刷新 if (event) { event.preventDefault(); event.stopPropagation(); } - + if (!document || !document.id) { toastService.error('文档数据不完整,无法确认评查结果'); return; } - + + // 使用ref防止重复点击,避免触发状态更新 + if (isProcessingRef.current) { + console.log('[完成评查] 正在处理中,跳过'); + return; + } + try { - setIsLoading(true); - - // 1. 先检查未投票 + console.log('[完成评查] 标记为处理中'); + isProcessingRef.current = true; + + // 1. 先检查未投票(不触发loading状态更新,避免重新渲染) + console.log('[完成评查] 开始检查未投票提案'); const checkRes = await checkProposalVotes(document.id, jwtToken); - console.log("checkRes", checkRes); - + console.log("[完成评查] 检查结果:", checkRes); + if (checkRes.error) { toastService.error(checkRes.error); - setIsLoading(false); + isProcessingRef.current = false; return; } @@ -551,7 +577,12 @@ export default function CrossCheckingResult() { modalMessage = '是否完成评查?'; } - // 4. 弹出模态框 + // 4. 重置处理状态标记,准备显示模态框(不触发状态更新) + console.log('[完成评查] 重置处理标记,准备显示模态框'); + isProcessingRef.current = false; + + // 5. 弹出模态框 + console.log('[完成评查] 显示确认模态框'); messageService.show({ title: '提示', message: modalMessage, @@ -559,43 +590,58 @@ export default function CrossCheckingResult() { confirmText: '确认', cancelText: '取消', onConfirm: async () => { + // 用户点击确认后才开始处理,此时才显示loading + console.log('[完成评查] 用户点击确认,开始更新状态'); setIsLoading(true); - const res = await confirmReviewResults(document.id, jwtToken); - setIsLoading(false); + try { + const res = await confirmReviewResults(document.id, jwtToken); - if (res.error) { - toastService.error(res.error); - return; + if (res.error) { + toastService.error(res.error); + setIsLoading(false); + return; + } + + toastService.success('评查结果已确认,文档审核状态已更新'); + // 跳转到交叉评查列表页,并带上任务信息以自动打开模态框 + const params = new URLSearchParams({ + openModal: 'true', + taskId: taskId || '', + taskName: taskName || '任务详情' + }); + navigate(`/cross-checking?${params.toString()}`); + } catch (error) { + console.error('确认评查结果失败:', error); + toastService.error('确认评查结果失败,请重试'); + setIsLoading(false); } - - toastService.success('评查结果已确认,文档审核状态已更新'); - // 注释掉自动跳转,让用户停留在当前页面 - navigate('/cross-checking'); + }, + onCancel: () => { + // 用户取消时不需要做任何处理 + console.log('[完成评查] 用户取消了确认操作'); } }); - - setIsLoading(false); } catch (error) { - setIsLoading(false); + isProcessingRef.current = false; toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`); } - }; + }, [document, jwtToken, navigate]); - // 构建自定义面包屑项 - const getBreadcrumbItems = () => { + // 构建自定义面包屑项 - 使用 useMemo 缓存 + const breadcrumbItems = useMemo(() => { const items = [ { title: "交叉评查详情", to: `/cross-checking/result?id=${document?.id}` } ]; - + // 添加前置路由 if (loaderData.previousRoute) { if (loaderData.previousRoute === 'crossChecking') { items.unshift({ title: "交叉评查", to: "/cross-checking" }); } } - + return items; - }; + }, [document?.id, loaderData.previousRoute]); return (
@@ -608,8 +654,8 @@ export default function CrossCheckingResult() { <> {/* 自定义面包屑 */}
- @@ -638,17 +684,29 @@ export default function CrossCheckingResult() { {/* 完成评查按钮 */} {isProposer && ( - )}
diff --git a/app/routes/cross-checking.upload.tsx b/app/routes/cross-checking.upload.tsx index 11079a9..b77c928 100644 --- a/app/routes/cross-checking.upload.tsx +++ b/app/routes/cross-checking.upload.tsx @@ -837,10 +837,9 @@ export default function CrossCheckingUpload() {
{/* 文件上传区域 */} -
- + {/* 上传框区域 */}
{/* 单案件导入 */} @@ -958,30 +957,29 @@ export default function CrossCheckingUpload() {
)} - {/* 完成按钮 */} -
- +
+ + -
- - -
- +
{/* 文件选择状态提示 */} {!canComplete && !isUploading && ( diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index 43b86db..f1363f1 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -130,6 +130,14 @@ async function handleFileUpload( jwtToken?: string, attachments?: File[] ): Promise { + // console.log('【handleFileUpload】开始上传:', { + // fileName, + // fileSize: binaryData.byteLength, + // documentType, + // hasAttachments: !!(attachments && attachments.length > 0), + // attachmentCount: attachments?.length || 0 + // }); + const response = await uploadDocumentToServer( binaryData, fileName, @@ -144,11 +152,20 @@ async function handleFileUpload( jwtToken, attachments ); - + + // console.log('【handleFileUpload】uploadDocumentToServer返回:', { + // hasError: !!response.error, + // hasData: !!response.data, + // error: response.error, + // dataKeys: response.data ? Object.keys(response.data) : [] + // }); + if (response.error || !response.data) { + console.error('【handleFileUpload】上传失败:', response.error); throw new Error(response.error || '上传失败'); } - + + // console.log('【handleFileUpload】返回数据:', response.data); return response.data; } @@ -1088,7 +1105,21 @@ export default function FilesUpload() { attachmentFiles ); - if (!uploadResp.result) throw new Error('主文件上传失败'); + // console.log('【合同上传】服务器响应数据:', uploadResp); + // console.log('【合同上传】响应详情:', { + // success: uploadResp.success, + // hasResult: !!uploadResp.result, + // error: uploadResp.error, + // fullResponse: JSON.stringify(uploadResp) + // }); + + if (!uploadResp.success) { + throw new Error(uploadResp.error || '上传失败,服务器返回success=false'); + } + + if (!uploadResp.result) { + throw new Error('主文件上传失败:服务器未返回文档信息'); + } const documentId = uploadResp.result.id; // 可选:模板上传 diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index 107fdc7..f176d8c 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -702,7 +702,8 @@ export default function ReviewDetails() { previousRoute: loaderData.previousRoute, path: document?.path, auditStatus: document?.auditStatus, - type: document?.type + type: document?.type, + comparisonId: comparison_document?.id ? Number(comparison_document.id) : undefined }} onConfirmResults={handleConfirmResults} jwtToken={frontendJWT} diff --git a/app/styles/main.css b/app/styles/main.css index 3c8c743..d95fe95 100644 --- a/app/styles/main.css +++ b/app/styles/main.css @@ -162,7 +162,31 @@ .sidebar.collapsed { @apply w-20; } - + + /* 移动端侧边栏遮罩层 */ + .sidebar-overlay { + @apply fixed inset-0 bg-black bg-opacity-50 z-[99]; + animation: fadeIn 0.3s ease-in-out; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + /* 移动端侧边栏样式 */ + .sidebar-mobile { + @apply shadow-[0_0_20px_rgba(0,0,0,0.15)]; + } + + .sidebar-mobile.collapsed { + transform: translateX(-100%); + } + .sidebar-menu { @apply flex-1 overflow-y-auto py-4 px-3; } @@ -277,7 +301,29 @@ @apply p-6; } } - + + /* 移动端响应式样式 (768px以下) */ + @media (max-width: 768px) { + /* 移动端主内容区域无左边距 */ + .main-content { + @apply ml-0; + } + + .main-content.sidebar-collapsed { + @apply ml-0; + } + + /* 移动端侧边栏默认隐藏 */ + .sidebar { + @apply w-[280px]; + } + + /* 移动端侧边栏折叠时完全隐藏 */ + .sidebar.collapsed { + @apply w-0; + } + } + @screen md { .sidebar-toggle { @apply block; diff --git a/app/styles/pages/home.css b/app/styles/pages/home.css index e028381..2116b07 100644 --- a/app/styles/pages/home.css +++ b/app/styles/pages/home.css @@ -163,4 +163,184 @@ background-position: center bottom; background-repeat: no-repeat; background-size: cover; + } + + .logout-button { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + background: transparent; + border: none; + color: #666; + cursor: pointer; + transition: all 0.2s; + border-radius: 4px; + } + + .logout-button:hover { + background-color: rgba(0, 104, 74, 0.05); + color: #00684a; + } + + .logout-button i { + font-size: 1.25rem; + } + + /* ===== 移动端响应式样式 ===== */ + @media (max-width: 768px) { + /* 头部样式调整 */ + .header { + padding: 0.5rem 0.75rem; + } + + .logo { + height: 40px; + margin-right: 0.5rem; + } + + .logo-text { + font-size: 1.2rem; + font-weight: 700; + } + + .logo-text-en { + font-size: 0.65rem; + margin-top: -0.15rem; + } + + /* 隐藏日期时间以节省空间 */ + .datetime { + display: none; + } + + .user-info { + gap: 0.5rem; + } + + .username { + display: none; /* 移动端隐藏用户名,只显示头像 */ + } + + .avatar { + width: 36px; + height: 36px; + } + + /* 主内容区域调整 */ + .index-main-content { + padding: 1rem 0; + } + + .index-main-content-container { + width: 95%; + padding: 1rem 0; + transform: translateY(-2rem); + } + + .welcome-text { + font-size: 1.3rem; + margin-bottom: 2.5rem; + padding: 0 1rem; + } + + /* 模块容器改为纵向排列 */ + .modules-container { + flex-direction: column; + gap: 1.25rem; + margin-bottom: 1.5rem; + align-items: center; + } + + /* 模块卡片调整 */ + .module-card { + width: 100%; + max-width: 340px; + height: 100px; + padding: 0 1.5rem; + gap: 1.25rem; + } + + .module-card img { + width: 48px !important; + height: 48px !important; + } + + .module-name { + font-size: 1.1rem; + } + + .logout-button { + padding: 0.4rem; + } + + .logout-button i { + font-size: 1.15rem; + } + } + + /* 超小屏幕 (手机竖屏) */ + @media (max-width: 480px) { + .header { + padding: 0.4rem 0.5rem; + } + + .logo { + height: 36px; + } + + .logo-text { + font-size: 1rem; + } + + .logo-text-en { + font-size: 0.6rem; + } + + .welcome-text { + font-size: 1.15rem; + margin-bottom: 2rem; + } + + .module-card { + max-width: 300px; + height: 90px; + padding: 0 1.25rem; + gap: 1rem; + } + + .module-card img { + width: 42px !important; + height: 42px !important; + } + + .module-name { + font-size: 1rem; + } + + .modules-container { + gap: 1rem; + } + } + + /* 平板横屏 */ + @media (min-width: 769px) and (max-width: 1024px) { + .index-main-content-container { + width: 85%; + transform: translateY(-5rem); + } + + .modules-container { + gap: 2rem; + } + + .module-card { + width: 260px; + height: 120px; + } + + .welcome-text { + font-size: 1.75rem; + margin-bottom: 4rem; + } } \ No newline at end of file