111
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled

This commit is contained in:
francis-fh
2026-06-17 19:56:39 +08:00
parent 17c1c89639
commit be90f4026b
5 changed files with 446 additions and 3 deletions

View File

@@ -76,6 +76,10 @@ export default [
path: '/job-portal/personal-center/footprints',// 访问足迹岗位列表
component: './JobPortal/PersonalCenter/Footprints',
},
{
path: '/job-portal/personal-center/showcase',// 我的风采
component: './JobPortal/PersonalCenter/Showcase',
},
{
path: '/job-portal/message',// 消息通知页面
component: './JobPortal/Message',

View File

@@ -0,0 +1,148 @@
@import '../../theme.less';
.showcase-page {
min-height: 100vh;
background: @jp-bg-page;
padding-bottom: 40px;
.page-content {
.jp-page-container();
margin-top: 20px;
padding: 0 20px;
box-sizing: border-box;
}
.back-button {
margin-bottom: 16px;
}
.page-header {
margin-bottom: 24px;
h2 {
margin-bottom: 8px;
}
}
.upload-card {
.jp-card-base();
margin-bottom: 24px;
}
.upload-section {
.upload-area {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
border: 2px dashed @jp-border;
border-radius: @jp-radius-lg;
background: @jp-bg-page;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: @jp-primary;
background: @jp-primary-light;
}
.upload-icons {
display: flex;
gap: 24px;
margin-bottom: 16px;
.upload-icon {
font-size: 40px;
color: @jp-primary;
}
}
.upload-text {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
margin-bottom: 20px;
}
}
}
.showcase-gallery {
.gallery-header {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: 16px;
h4 {
margin-bottom: 0;
}
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.gallery-card {
.jp-card-base();
overflow: hidden;
.image-cover {
height: 200px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
.gallery-image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.video-cover {
height: 200px;
overflow: hidden;
background: #000;
.gallery-video {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.ant-card-actions {
border-radius: 0 0 @jp-radius @jp-radius;
> li > span {
cursor: pointer;
&:hover {
.anticon {
color: #ff7875;
}
}
}
}
}
}
}
@media (max-width: 768px) {
.showcase-page {
.page-content {
padding: 0 12px;
margin-top: 12px;
}
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)) !important;
}
}
}

View File

@@ -0,0 +1,277 @@
import React, { useState } from 'react';
import {
Upload,
Button,
Card,
Image,
Spin,
Empty,
message,
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 './index.less';
const { Title, Text } = Typography;
/** 从缓存获取用户ID */
const getUserIdFromCache = (): number | null => {
try {
const cached = localStorage.getItem('userInfo');
if (cached) {
const userInfo = JSON.parse(cached);
return userInfo?.userId || null;
}
} catch (error) {
console.error('读取缓存用户信息失败:', error);
}
return null;
};
const ShowcasePage: React.FC = () => {
const [uploadedFiles, setUploadedFiles] = useState<UploadFile[]>([]);
const [uploading, setUploading] = useState(false);
const [previewVisible, setPreviewVisible] = useState(false);
const [previewFile, setPreviewFile] = useState<UploadFile | null>(null);
const userId = getUserIdFromCache();
const isVideo = (file: UploadFile): boolean => {
const type = file.type || '';
return type.startsWith('video/');
};
const isImage = (file: UploadFile): boolean => {
const type = file.type || '';
return type.startsWith('image/');
};
/** 自定义上传逻辑 */
const customUpload = async (options: any) => {
const { file, onSuccess, onError, onProgress } = options;
if (!userId) {
message.error('请先登录');
onError(new Error('未登录'));
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,
}
);
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 || '上传失败'));
}
} else {
message.error(`${file.name} 上传失败`);
onError(new Error('上传失败'));
}
} catch (error) {
console.error('上传失败:', error);
message.error(`${file.name} 上传失败`);
onError(error as Error);
} finally {
setUploading(false);
}
};
/** 上传前校验:仅允许图片和视频 */
const beforeUpload = (file: File): boolean => {
const isImageOrVideo = file.type.startsWith('image/') || file.type.startsWith('video/');
if (!isImageOrVideo) {
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);
};
const handleBack = () => {
history.push('/job-portal/personal-center');
};
return (
<div className="showcase-page">
<JobPortalHeader showSearch={false} showHotJobs={false} />
<Spin spinning={uploading}>
<div className="page-content">
{/* 返回按钮 */}
<div className="back-button">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={handleBack}
>
</Button>
</div>
{/* 页面标题 */}
<div className="page-header">
<Title level={2}></Title>
<Text type="secondary"></Text>
</div>
{/* 上传区域 */}
<Card className="upload-card" bordered={false}>
<div className="upload-section">
<Upload
customRequest={customUpload}
beforeUpload={beforeUpload}
showUploadList={false}
accept="image/*,video/*"
multiple
>
<div className="upload-area">
<div className="upload-icons">
<CameraOutlined className="upload-icon" />
<VideoCameraOutlined className="upload-icon" />
</div>
<div className="upload-text">
<Text strong></Text>
<Text type="secondary"></Text>
</div>
<Button type="primary" icon={<UploadOutlined />}>
</Button>
</div>
</Upload>
</div>
</Card>
{/* 已上传内容展示 */}
{uploadedFiles.length === 0 ? (
<Empty
description="暂无上传内容,快去上传吧"
style={{ marginTop: '60px' }}
/>
) : (
<div className="showcase-gallery">
<div className="gallery-header">
<Title level={4}></Title>
<Text type="secondary"> {uploadedFiles.length} </Text>
</div>
<div className="gallery-grid">
{uploadedFiles.map((file, index) => (
<Card
key={file.uid || index}
className="gallery-card"
hoverable
cover={
isVideo(file) ? (
<div className="video-cover">
<video
src={URL.createObjectURL(file.originFileObj as File)}
controls
className="gallery-video"
/>
</div>
) : (
<div className="image-cover">
<Image
src={URL.createObjectURL(file.originFileObj as File)}
alt={file.name}
className="gallery-image"
preview={{
mask: <CameraOutlined />,
}}
/>
</div>
)
}
actions={[
<DeleteOutlined
key="delete"
onClick={() => handleRemove(file)}
style={{ color: '#ff4d4f' }}
/>,
]}
>
<Card.Meta
title={file.name}
description={
<Text type="secondary">
{file.size
? file.size > 1024 * 1024
? `${(file.size / 1024 / 1024).toFixed(2)} MB`
: `${(file.size / 1024).toFixed(2)} KB`
: ''}
</Text>
}
/>
</Card>
))}
</div>
</div>
)}
{/* 视频预览弹窗 */}
<Modal
open={previewVisible}
footer={null}
onCancel={() => setPreviewVisible(false)}
width={800}
title={previewFile?.name || '预览'}
>
{previewFile && isVideo(previewFile) && (
<video
src={URL.createObjectURL(previewFile.originFileObj as File)}
controls
style={{ width: '100%' }}
/>
)}
{previewFile && isImage(previewFile) && (
<Image
src={URL.createObjectURL(previewFile.originFileObj as File)}
alt={previewFile.name}
style={{ width: '100%' }}
/>
)}
</Modal>
</div>
</Spin>
</div>
);
};
export default ShowcasePage;

View File

@@ -16,7 +16,8 @@ import {
RightOutlined,
SafetyCertificateOutlined,
BellOutlined,
FileTextOutlined
FileTextOutlined,
CameraOutlined
} from '@ant-design/icons';
import { history } from '@umijs/max';
import JobPortalHeader from '@/components/JobPortalHeader';
@@ -169,6 +170,15 @@ const PersonalCenter: React.FC = () => {
hasArrow: !isRealNameVerified,
color: isRealNameVerified ? '#52c41a' : '#8c8c8c'
},
{
id: 5,
title: '我的风采',
icon: <CameraOutlined />,
status: '',
hasArrow: true,
color: '#722ed1',
link: '/job-portal/personal-center/showcase'
},
{
id: 2,
title: '在线面试',
@@ -190,7 +200,11 @@ const PersonalCenter: React.FC = () => {
const handleServiceClick = (service: any) => {
if (service.link) {
if (service.link.startsWith('http')) {
window.open(service.link, '_blank');
} else {
history.push(service.link);
}
}
};