From 4f484f1d0014de9a43b42d32d3ed5043578a0c66 Mon Sep 17 00:00:00 2001 From: francis-fh Date: Wed, 17 Jun 2026 21:48:26 +0800 Subject: [PATCH] 111 --- config/routes.ts | 5 + src/components/EnterpriseHeader/index.tsx | 11 +- .../PersonalCenter/Showcase/index.less | 27 +- .../PersonalCenter/Showcase/index.tsx | 252 ++++++++--------- .../Management/EnterpriseShowcase/index.less | 153 ++++++++++ .../Management/EnterpriseShowcase/index.tsx | 266 ++++++++++++++++++ src/services/jobportal/user.ts | 18 ++ src/utils/fileUrl.ts | 15 + 8 files changed, 606 insertions(+), 141 deletions(-) create mode 100644 src/pages/Management/EnterpriseShowcase/index.less create mode 100644 src/pages/Management/EnterpriseShowcase/index.tsx create mode 100644 src/utils/fileUrl.ts diff --git a/config/routes.ts b/config/routes.ts index 84f6daf..ad448ae 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -230,6 +230,11 @@ export default [ }, ], }, + { + path: '/management/enterprise-showcase',// 企业风采 + layout: false, + component: './Management/EnterpriseShowcase', + }, { path: '*', layout: false, diff --git a/src/components/EnterpriseHeader/index.tsx b/src/components/EnterpriseHeader/index.tsx index 3e88b66..35a1b2a 100644 --- a/src/components/EnterpriseHeader/index.tsx +++ b/src/components/EnterpriseHeader/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import { createPortal } from 'react-dom'; -import { HomeOutlined } from '@ant-design/icons'; +import { HomeOutlined, CameraOutlined } from '@ant-design/icons'; +import { history } from '@umijs/max'; import portalLogo from '@/assets/logo.png'; import './index.less'; @@ -19,6 +20,10 @@ const EnterpriseHeader: React.FC = () => { window.open(PORTAL_HOME_URL, '_blank'); }; + const handleGoShowcase = () => { + history.push('/management/enterprise-showcase'); + }; + const header = (
@@ -28,6 +33,10 @@ const EnterpriseHeader: React.FC = () => {
+
+ + 企业风采 +
返回首页 diff --git a/src/pages/JobPortal/PersonalCenter/Showcase/index.less b/src/pages/JobPortal/PersonalCenter/Showcase/index.less index cb4f35f..f8584e8 100644 --- a/src/pages/JobPortal/PersonalCenter/Showcase/index.less +++ b/src/pages/JobPortal/PersonalCenter/Showcase/index.less @@ -109,25 +109,30 @@ height: 200px; overflow: hidden; background: #000; + position: relative; + cursor: pointer; .gallery-video { width: 100%; height: 100%; object-fit: contain; } - } - .ant-card-actions { - border-radius: 0 0 @jp-radius @jp-radius; + .video-play-mask { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.35); + opacity: 0; + transition: opacity 0.3s; + color: #fff; + font-size: 40px; + } - > li > span { - cursor: pointer; - - &:hover { - .anticon { - color: #ff7875; - } - } + &:hover .video-play-mask { + opacity: 1; } } } diff --git a/src/pages/JobPortal/PersonalCenter/Showcase/index.tsx b/src/pages/JobPortal/PersonalCenter/Showcase/index.tsx index 11f4923..f23a0ad 100644 --- a/src/pages/JobPortal/PersonalCenter/Showcase/index.tsx +++ b/src/pages/JobPortal/PersonalCenter/Showcase/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Upload, Button, @@ -7,30 +7,37 @@ import { Spin, Empty, message, + Typography, Modal, - Typography } from 'antd'; import { UploadOutlined, ArrowLeftOutlined, CameraOutlined, VideoCameraOutlined, - DeleteOutlined } from '@ant-design/icons'; -import type { UploadFile } from 'antd/es/upload/interface'; import { history } from '@umijs/max'; import JobPortalHeader from '@/components/JobPortalHeader'; +import { uploadShowcaseFile, getShowcaseFileList } from '@/services/jobportal/user'; +import { getFileDisplayUrl } from '@/utils/fileUrl'; import './index.less'; const { Title, Text } = Typography; -/** 从缓存获取用户ID */ +interface ShowcaseItem { + uid: string; + type: string; + url: string; + createTime: string; +} + +/** 从 getInfoCache 缓存获取 userId */ const getUserIdFromCache = (): number | null => { try { - const cached = localStorage.getItem('userInfo'); + const cached = localStorage.getItem('getInfoCache'); if (cached) { - const userInfo = JSON.parse(cached); - return userInfo?.userId || null; + const data = JSON.parse(cached); + return data?.user?.userId || null; } } catch (error) { console.error('读取缓存用户信息失败:', error); @@ -38,27 +45,78 @@ const getUserIdFromCache = (): number | null => { return null; }; +/** 根据文件名后缀推断类型 */ +const guessType = (filename: string): string => { + const ext = filename.split('.').pop()?.toLowerCase() || ''; + const videoExts = ['mp4', 'mov', 'avi', 'wmv', 'flv', 'mkv', 'webm', 'm4v']; + if (videoExts.includes(ext)) return 'video/mp4'; + return 'image/jpeg'; +}; + +/** 格式化时间 */ +const formatTime = (time?: string): string => { + if (!time) return ''; + try { + const date = new Date(time); + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + } catch { + return time; + } +}; + +/** 服务器文件 → 展示条目 */ +const serverFileToItem = (row: { + id: number; + fileUrl: string; + createTime?: string; +}): ShowcaseItem => ({ + uid: `server-${row.id}`, + type: guessType(row.fileUrl), + url: getFileDisplayUrl(row.fileUrl), + createTime: formatTime(row.createTime), +}); + +const isVideo = (type: string): boolean => type.startsWith('video/'); + const ShowcasePage: React.FC = () => { - const [uploadedFiles, setUploadedFiles] = useState([]); + const [items, setItems] = useState([]); const [uploading, setUploading] = useState(false); - const [previewVisible, setPreviewVisible] = useState(false); - const [previewFile, setPreviewFile] = useState(null); + const [loading, setLoading] = useState(true); + const [previewItem, setPreviewItem] = useState(null); const userId = getUserIdFromCache(); - const isVideo = (file: UploadFile): boolean => { - const type = file.type || ''; - return type.startsWith('video/'); - }; + const fetchFileList = useCallback(async () => { + if (!userId) { + setLoading(false); + return; + } + try { + setLoading(true); + const response = await getShowcaseFileList(userId); + if (response?.code === 200 || response?.code === 0) { + const rows = response.rows || response.data || []; + setItems(rows.map(serverFileToItem)); + } + } catch (error) { + console.error('获取文件列表失败:', error); + } finally { + setLoading(false); + } + }, [userId]); - const isImage = (file: UploadFile): boolean => { - const type = file.type || ''; - return type.startsWith('image/'); - }; + useEffect(() => { + fetchFileList(); + }, [fetchFileList]); - /** 自定义上传逻辑 */ const customUpload = async (options: any) => { - const { file, onSuccess, onError, onProgress } = options; + const { file, onSuccess, onError } = options; if (!userId) { message.error('请先登录'); @@ -66,33 +124,17 @@ const ShowcasePage: React.FC = () => { return; } - const formData = new FormData(); - formData.append('file', file as File); - try { setUploading(true); - const response = await fetch( - `/app/file/upload?bussinessid=${userId}`, - { - method: 'POST', - body: formData, - } - ); + const response = await uploadShowcaseFile(userId, file as File); - if (response.ok) { - const result = await response.json(); - if (result.code === 200 || result.code === 0) { - message.success(`${file.name} 上传成功`); - onSuccess(result, file); - // 将上传成功的文件添加到展示列表 - setUploadedFiles((prev) => [...prev, file]); - } else { - message.error(result.msg || `${file.name} 上传失败`); - onError(new Error(result.msg || '上传失败')); - } + if (response?.code === 200 || response?.code === 0) { + message.success(`${file.name} 上传成功`); + onSuccess(response, file); + await fetchFileList(); } else { - message.error(`${file.name} 上传失败`); - onError(new Error('上传失败')); + message.error(response?.msg || `${file.name} 上传失败`); + onError(new Error(response?.msg || '上传失败')); } } catch (error) { console.error('上传失败:', error); @@ -103,26 +145,12 @@ const ShowcasePage: React.FC = () => { } }; - /** 上传前校验:仅允许图片和视频 */ const beforeUpload = (file: File): boolean => { - const isImageOrVideo = file.type.startsWith('image/') || file.type.startsWith('video/'); - if (!isImageOrVideo) { + const isValid = file.type.startsWith('image/') || file.type.startsWith('video/'); + if (!isValid) { message.error('只支持上传图片和视频文件!'); - return false; } - return true; - }; - - /** 移除已上传文件 */ - const handleRemove = (file: UploadFile) => { - setUploadedFiles((prev) => prev.filter((f) => f.uid !== file.uid)); - message.info('已移除'); - }; - - /** 预览文件 */ - const handlePreview = (file: UploadFile) => { - setPreviewFile(file); - setPreviewVisible(true); + return isValid; }; const handleBack = () => { @@ -133,27 +161,21 @@ const ShowcasePage: React.FC = () => {
- +
- {/* 返回按钮 */}
-
- {/* 页面标题 */}
我的风采 上传你的个人照片和视频,展示你的风采
{/* 上传区域 */} - +
{ {/* 已上传内容展示 */} - {uploadedFiles.length === 0 ? ( + {items.length === 0 ? ( {
已上传内容 - 共 {uploadedFiles.length} 个文件 + 共 {items.length} 条记录
- {uploadedFiles.map((file, index) => ( + {items.map((item) => ( - ))}
)} - - {/* 视频预览弹窗 */} - setPreviewVisible(false)} - width={800} - title={previewFile?.name || '预览'} - > - {previewFile && isVideo(previewFile) && ( -
+ + {/* 视频放大预览 */} + setPreviewItem(null)} + width={900} + > + {previewItem && ( +
); }; diff --git a/src/pages/Management/EnterpriseShowcase/index.less b/src/pages/Management/EnterpriseShowcase/index.less new file mode 100644 index 0000000..f6f0df4 --- /dev/null +++ b/src/pages/Management/EnterpriseShowcase/index.less @@ -0,0 +1,153 @@ +.enterprise-showcase-page { + min-height: 100vh; + padding-top: 72px; + padding-bottom: 40px; + background: #f0f2f5; + + .es-page-content { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; + box-sizing: border-box; + } + + .es-back-button { + margin-bottom: 8px; + } + + .es-page-header { + margin-bottom: 24px; + + h2 { + margin-bottom: 8px; + } + } + + .es-upload-card { + margin-bottom: 24px; + border-radius: 8px; + } + + .es-upload-section { + .es-upload-area { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + border: 2px dashed #d9d9d9; + border-radius: 12px; + background: #fafafa; + cursor: pointer; + transition: all 0.3s; + + &:hover { + border-color: #1890ff; + background: #e6f4ff; + } + + .es-upload-icons { + display: flex; + gap: 24px; + margin-bottom: 16px; + + .es-upload-icon { + font-size: 40px; + color: #1890ff; + } + } + + .es-upload-text { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + margin-bottom: 20px; + } + } + } + + .es-showcase-gallery { + .es-gallery-header { + display: flex; + align-items: baseline; + gap: 12px; + margin-bottom: 16px; + + h4 { + margin-bottom: 0; + } + } + + .es-gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; + } + + .es-gallery-card { + border-radius: 8px; + overflow: hidden; + + .es-image-cover { + height: 200px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background: #f5f5f5; + + .es-gallery-image { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .es-video-cover { + height: 200px; + overflow: hidden; + background: #000; + position: relative; + cursor: pointer; + + .es-gallery-video { + width: 100%; + height: 100%; + object-fit: contain; + } + + .es-video-play-mask { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.35); + opacity: 0; + transition: opacity 0.3s; + color: #fff; + font-size: 40px; + } + + &:hover .es-video-play-mask { + opacity: 1; + } + } + } + } +} + +@media (max-width: 768px) { + .enterprise-showcase-page { + padding-top: 64px; + + .es-page-content { + padding: 0 12px; + } + + .es-gallery-grid { + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)) !important; + } + } +} diff --git a/src/pages/Management/EnterpriseShowcase/index.tsx b/src/pages/Management/EnterpriseShowcase/index.tsx new file mode 100644 index 0000000..aee8b9f --- /dev/null +++ b/src/pages/Management/EnterpriseShowcase/index.tsx @@ -0,0 +1,266 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Upload, + Button, + Card, + Image, + Spin, + Empty, + message, + Typography, + Modal, +} from 'antd'; +import { + UploadOutlined, + CameraOutlined, + VideoCameraOutlined, + ArrowLeftOutlined, +} from '@ant-design/icons'; +import { history } from '@umijs/max'; +import { uploadShowcaseFile, getShowcaseFileList } from '@/services/jobportal/user'; +import { getFileDisplayUrl } from '@/utils/fileUrl'; +import EnterpriseHeader from '@/components/EnterpriseHeader'; +import './index.less'; + +const { Title, Text } = Typography; + +interface ShowcaseItem { + uid: string; + type: string; + url: string; + createTime: string; +} + +/** 从 getInfoCache 缓存获取 userId */ +const getUserIdFromCache = (): number | null => { + try { + const cached = localStorage.getItem('getInfoCache'); + if (cached) { + const data = JSON.parse(cached); + return data?.user?.userId || null; + } + } catch (error) { + console.error('读取缓存用户信息失败:', error); + } + return null; +}; + +/** 根据文件名后缀推断类型 */ +const guessType = (filename: string): string => { + const ext = filename.split('.').pop()?.toLowerCase() || ''; + const videoExts = ['mp4', 'mov', 'avi', 'wmv', 'flv', 'mkv', 'webm', 'm4v']; + if (videoExts.includes(ext)) return 'video/mp4'; + return 'image/jpeg'; +}; + +/** 格式化时间 */ +const formatTime = (time?: string): string => { + if (!time) return ''; + try { + const date = new Date(time); + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + } catch { + return time; + } +}; + +/** 服务器文件 → 展示条目 */ +const serverFileToItem = (row: { + id: number; + fileUrl: string; + createTime?: string; +}): ShowcaseItem => ({ + uid: `server-${row.id}`, + type: guessType(row.fileUrl), + url: getFileDisplayUrl(row.fileUrl), + createTime: formatTime(row.createTime), +}); + +const isVideo = (type: string): boolean => type.startsWith('video/'); + +const EnterpriseShowcase: React.FC = () => { + const [items, setItems] = useState([]); + const [uploading, setUploading] = useState(false); + const [loading, setLoading] = useState(true); + const [previewItem, setPreviewItem] = useState(null); + + const userId = getUserIdFromCache(); + + const fetchFileList = useCallback(async () => { + if (!userId) { + setLoading(false); + return; + } + try { + setLoading(true); + const response = await getShowcaseFileList(userId); + if (response?.code === 200 || response?.code === 0) { + const rows = response.rows || response.data || []; + setItems(rows.map(serverFileToItem)); + } + } catch (error) { + console.error('获取文件列表失败:', error); + } finally { + setLoading(false); + } + }, [userId]); + + useEffect(() => { + fetchFileList(); + }, [fetchFileList]); + + const customUpload = async (options: any) => { + const { file, onSuccess, onError } = options; + + if (!userId) { + message.error('请先登录'); + onError(new Error('未登录')); + return; + } + + try { + setUploading(true); + const response = await uploadShowcaseFile(userId, file as File); + + if (response?.code === 200 || response?.code === 0) { + message.success(`${file.name} 上传成功`); + onSuccess(response, file); + await fetchFileList(); + } else { + message.error(response?.msg || `${file.name} 上传失败`); + onError(new Error(response?.msg || '上传失败')); + } + } catch (error) { + console.error('上传失败:', error); + message.error(`${file.name} 上传失败`); + onError(error as Error); + } finally { + setUploading(false); + } + }; + + const beforeUpload = (file: File): boolean => { + const isValid = file.type.startsWith('image/') || file.type.startsWith('video/'); + if (!isValid) { + message.error('只支持上传图片和视频文件!'); + } + return isValid; + }; + + return ( +
+ + +
+
+ +
+ +
+ 企业风采 + 上传企业照片和视频,展示企业风采 +
+ + {/* 上传区域 */} + +
+ +
+
+ + +
+
+ 点击或拖拽上传 + 支持图片和视频文件,大小不限 +
+ +
+
+
+
+ + {/* 已上传内容展示 */} + {items.length === 0 ? ( + + ) : ( +
+
+ 已上传内容 + 共 {items.length} 条记录 +
+
+ {items.map((item) => ( + setPreviewItem(item)}> +
+ ) : ( +
+ 企业风采图片 }} + /> +
+ ) + } + > + 上传时间:{item.createTime}} /> + + ))} +
+
+ )} +
+
+ + {/* 视频放大预览 */} + setPreviewItem(null)} + width={900} + > + {previewItem && ( + +
+ ); +}; + +export default EnterpriseShowcase; diff --git a/src/services/jobportal/user.ts b/src/services/jobportal/user.ts index df64aef..49812b3 100644 --- a/src/services/jobportal/user.ts +++ b/src/services/jobportal/user.ts @@ -136,3 +136,21 @@ export async function deleteMessage(ids: number, options?: Record) }); } +/** 上传我的风采文件 POST /app/file/upload */ +export async function uploadShowcaseFile(userId: number, file: File) { + const formData = new FormData(); + formData.append('file', file); + return request(`/app/file/upload?bussinessid=${userId}`, { + method: 'POST', + data: formData, + }); +} + +/** 获取我的风采文件列表 GET /app/file/list */ +export async function getShowcaseFileList(userId: number) { + return request('/app/file/list', { + method: 'GET', + params: { bussinessid: userId }, + }); +} + diff --git a/src/utils/fileUrl.ts b/src/utils/fileUrl.ts new file mode 100644 index 0000000..673c11e --- /dev/null +++ b/src/utils/fileUrl.ts @@ -0,0 +1,15 @@ +/** + * 文件展示 URL 拼接工具 + * 开发环境走相对路径,生产环境拼接固定域名 + */ + +const PROD_DOMAIN = 'http://39.98.44.136:6024'; + +/** 根据服务端返回的 fileUrl 字段拼出可用于 /