11
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
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:
353
src/pages/JobPortal/LiveRecruitment/index.less
Normal file
353
src/pages/JobPortal/LiveRecruitment/index.less
Normal file
@@ -0,0 +1,353 @@
|
||||
@import '../theme.less';
|
||||
|
||||
.live-recruitment-page {
|
||||
min-height: 100vh;
|
||||
background: @jp-bg-page;
|
||||
}
|
||||
|
||||
// ===================== Banner =====================
|
||||
.live-banner {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 40%, #0f3460 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -60%;
|
||||
right: -15%;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(24, 144, 255, 0.15) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -40%;
|
||||
left: -10%;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(102, 126, 234, 0.12) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.live-banner-inner {
|
||||
.jp-page-container();
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding-top: 36px;
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
|
||||
.banner-text {
|
||||
.banner-title {
|
||||
color: #fff;
|
||||
margin: 0 0 8px;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2px;
|
||||
|
||||
.banner-icon {
|
||||
margin-right: 12px;
|
||||
font-size: 32px;
|
||||
color: #69b1ff;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-subtitle {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.banner-search {
|
||||
width: 480px;
|
||||
max-width: 100%;
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.search-type-select {
|
||||
width: 90px;
|
||||
flex-shrink: 0;
|
||||
height: 40px;
|
||||
|
||||
.ant-select-selector {
|
||||
height: 40px !important;
|
||||
line-height: 38px !important;
|
||||
border-radius: 24px 0 0 24px !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-right: none;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
|
||||
.ant-select-selection-item {
|
||||
line-height: 38px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.ant-select-selection-search-input {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
height: 40px;
|
||||
border-radius: 0 24px 24px 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
|
||||
.ant-input {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input-prefix {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(24, 144, 255, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: @jp-primary;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
border-color: @jp-primary;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== Stats Bar =====================
|
||||
.live-stats-bar {
|
||||
background: @jp-bg-card;
|
||||
border-bottom: 1px solid @jp-border;
|
||||
|
||||
.stats-inner {
|
||||
.jp-page-container();
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
|
||||
.stat-icon {
|
||||
color: #ff4d4f;
|
||||
font-size: 16px;
|
||||
|
||||
&.pulse {
|
||||
animation: livePulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: @jp-text-secondary;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: @jp-primary;
|
||||
font-weight: 600;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== Content =====================
|
||||
.live-content {
|
||||
padding: 28px 20px 48px;
|
||||
|
||||
.live-container {
|
||||
.jp-page-container();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== Card =====================
|
||||
.live-card {
|
||||
border-radius: @jp-radius-lg !important;
|
||||
overflow: hidden;
|
||||
border: 1px solid @jp-border !important;
|
||||
box-shadow: @jp-shadow-card;
|
||||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 12px 32px rgba(24, 144, 255, 0.18);
|
||||
border-color: @jp-primary-border !important;
|
||||
|
||||
.live-card-cover {
|
||||
.cover-play-icon {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.live-card-action {
|
||||
.action-text {
|
||||
color: @jp-primary;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Card cover (top decorated area)
|
||||
.live-card-cover {
|
||||
height: 120px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
padding: 14px 16px;
|
||||
|
||||
.cover-play-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.6);
|
||||
font-size: 48px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
opacity: 0;
|
||||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
}
|
||||
|
||||
// Card body
|
||||
.live-card-body {
|
||||
padding: 18px 20px 16px;
|
||||
|
||||
.live-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: @jp-text-primary;
|
||||
line-height: 1.5;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.live-company {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid lighten(@jp-border, 3%);
|
||||
|
||||
.company-icon {
|
||||
color: @jp-primary;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
color: @jp-text-secondary;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.live-time-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
padding: 8px 12px;
|
||||
background: @jp-primary-light;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: @jp-primary;
|
||||
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
color: @jp-primary;
|
||||
}
|
||||
|
||||
.time-separator {
|
||||
color: @jp-text-muted;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.live-card-action {
|
||||
.action-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: @jp-text-secondary;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== Pagination =====================
|
||||
.live-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
// Empty state
|
||||
.live-empty {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
// ===================== Animations =====================
|
||||
@keyframes livePulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.3);
|
||||
}
|
||||
}
|
||||
247
src/pages/JobPortal/LiveRecruitment/index.tsx
Normal file
247
src/pages/JobPortal/LiveRecruitment/index.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Col,
|
||||
Row,
|
||||
Select,
|
||||
Spin,
|
||||
Typography,
|
||||
Input,
|
||||
Pagination,
|
||||
Empty,
|
||||
message,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlayCircleOutlined,
|
||||
SearchOutlined,
|
||||
EnvironmentOutlined,
|
||||
ClockCircleOutlined,
|
||||
RightOutlined,
|
||||
FireOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||
import { getLiveList, type LiveStreamItem } from '@/services/jobportal/live';
|
||||
import './index.less';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
/** 卡片渐变色盘,按 id 取模分配 */
|
||||
const COVER_COLORS = [
|
||||
'#667eea',
|
||||
'#f093fb',
|
||||
'#4facfe',
|
||||
'#43e97b',
|
||||
'#fa709a',
|
||||
'#a18cd1',
|
||||
'#30cfd0',
|
||||
'#f6d365',
|
||||
'#f5576c',
|
||||
'#667eea',
|
||||
'#764ba2',
|
||||
'#f093fb',
|
||||
];
|
||||
|
||||
const PAGE_SIZE = 9;
|
||||
|
||||
const getCoverColor = (id: number) => COVER_COLORS[id % COVER_COLORS.length];
|
||||
|
||||
const LiveRecruitmentPage: React.FC = () => {
|
||||
const [list, setList] = useState<LiveStreamItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [pageNum, setPageNum] = useState(1);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [searchType, setSearchType] = useState<'title' | 'companyName'>('title');
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params: { title?: string; companyName?: string } = {};
|
||||
if (keyword) {
|
||||
params[searchType] = keyword;
|
||||
}
|
||||
const response = await getLiveList(params);
|
||||
if (response?.code === 200) {
|
||||
// 前端分页
|
||||
const rows = response.rows || [];
|
||||
const start = (pageNum - 1) * PAGE_SIZE;
|
||||
setList(rows.slice(start, start + PAGE_SIZE));
|
||||
setTotal(response.total || rows.length);
|
||||
} else {
|
||||
message.error(response?.msg || '获取直播列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取直播列表失败:', error);
|
||||
message.error('获取直播列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [pageNum, keyword, searchType]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchList();
|
||||
}, [fetchList]);
|
||||
|
||||
const handleSearch = () => {
|
||||
setPageNum(1);
|
||||
setKeyword(searchValue.trim());
|
||||
};
|
||||
|
||||
const handleCardClick = (item: LiveStreamItem) => {
|
||||
if (item.liveUrl) {
|
||||
window.open(item.liveUrl, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
};
|
||||
|
||||
/** 格式化时间,只保留前 16 位 (yyyy-MM-dd HH:mm) */
|
||||
const formatTime = (time?: string): string => {
|
||||
if (!time) return '';
|
||||
return time.length >= 16 ? time.substring(0, 16) : time;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="live-recruitment-page">
|
||||
<JobPortalHeader showSearch={false} showHotJobs={false} />
|
||||
|
||||
{/* 顶部 Banner */}
|
||||
<div className="live-banner">
|
||||
<div className="live-banner-inner">
|
||||
<div className="banner-text">
|
||||
<Title level={2} className="banner-title">
|
||||
<PlayCircleOutlined className="banner-icon" />
|
||||
直播带岗
|
||||
</Title>
|
||||
<Text className="banner-subtitle">
|
||||
实时在线,直面心仪企业,在线互动,快速了解岗位详情
|
||||
</Text>
|
||||
</div>
|
||||
<div className="banner-search">
|
||||
<div className="search-row">
|
||||
<Select
|
||||
value={searchType}
|
||||
onChange={(value) => setSearchType(value)}
|
||||
size="large"
|
||||
className="search-type-select"
|
||||
options={[
|
||||
{ value: 'title', label: '标题' },
|
||||
{ value: 'companyName', label: '公司' },
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
placeholder={searchType === 'title' ? '搜索直播标题...' : '搜索公司名称...'}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
allowClear
|
||||
size="large"
|
||||
className="search-input"
|
||||
prefix={<SearchOutlined />}
|
||||
suffix={
|
||||
<div className="search-btn" onClick={handleSearch}>
|
||||
<RightOutlined />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计栏 */}
|
||||
<div className="live-stats-bar">
|
||||
<div className="stats-inner">
|
||||
<div className="stat-item">
|
||||
<FireOutlined className="stat-icon pulse" />
|
||||
<span className="stat-label">直播带岗</span>
|
||||
<span className="stat-value">{total} 场</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 卡片列表 */}
|
||||
<div className="live-content">
|
||||
<div className="live-container">
|
||||
<Spin spinning={loading}>
|
||||
{list.length > 0 ? (
|
||||
<>
|
||||
<Row gutter={[20, 20]}>
|
||||
{list.map((item) => (
|
||||
<Col xs={24} sm={12} lg={8} key={item.id}>
|
||||
<Card
|
||||
className="live-card"
|
||||
hoverable
|
||||
bodyStyle={{ padding: 0 }}
|
||||
onClick={() => handleCardClick(item)}
|
||||
>
|
||||
{/* 卡片顶部装饰 */}
|
||||
<div
|
||||
className="live-card-cover"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${getCoverColor(item.id)}, ${getCoverColor(item.id)}dd)`,
|
||||
}}
|
||||
>
|
||||
<PlayCircleOutlined className="cover-play-icon" />
|
||||
</div>
|
||||
|
||||
{/* 卡片内容 */}
|
||||
<div className="live-card-body">
|
||||
<Title
|
||||
level={5}
|
||||
className="live-title"
|
||||
ellipsis={{ rows: 2 }}
|
||||
>
|
||||
{item.title}
|
||||
</Title>
|
||||
<div className="live-company">
|
||||
<EnvironmentOutlined className="company-icon" />
|
||||
<Text ellipsis className="company-name">
|
||||
{item.companyName}
|
||||
</Text>
|
||||
</div>
|
||||
{(item.startTime || item.endTime) && (
|
||||
<div className="live-time-range">
|
||||
<ClockCircleOutlined />
|
||||
<span>{formatTime(item.startTime) || '待定'}</span>
|
||||
<span className="time-separator">—</span>
|
||||
<span>{formatTime(item.endTime) || '待定'}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="live-card-action">
|
||||
<span className="action-text">
|
||||
进入直播间 <RightOutlined />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<div className="live-pagination">
|
||||
<Pagination
|
||||
current={pageNum}
|
||||
pageSize={PAGE_SIZE}
|
||||
total={total}
|
||||
showSizeChanger={false}
|
||||
showTotal={(t) => `共 ${t} 场直播`}
|
||||
onChange={(page) => setPageNum(page)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
!loading && (
|
||||
<Empty
|
||||
className="live-empty"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={keyword ? '没有找到相关直播' : '暂无直播安排'}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveRecruitmentPage;
|
||||
@@ -169,6 +169,15 @@ const PersonalCenter: React.FC = () => {
|
||||
hasArrow: !isRealNameVerified,
|
||||
color: isRealNameVerified ? '#52c41a' : '#8c8c8c'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '在线面试',
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
status: '',
|
||||
hasArrow: true,
|
||||
color: '#1890ff',
|
||||
link: 'https://mc.repohub.cn/pro.html'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '通知与提醒',
|
||||
@@ -180,8 +189,8 @@ const PersonalCenter: React.FC = () => {
|
||||
], [isRealNameVerified]);
|
||||
|
||||
const handleServiceClick = (service: any) => {
|
||||
if (service.hasArrow) {
|
||||
// 预留跳转
|
||||
if (service.link) {
|
||||
window.open(service.link, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user