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:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"pid": 8432,
|
||||
"pid": 15780,
|
||||
"version": "0.9.9",
|
||||
"socketPath": "\\\\.\\pipe\\codegraph-ae68fa88b3ffb91a",
|
||||
"startedAt": 1781608072582
|
||||
"startedAt": 1781687361498
|
||||
}
|
||||
|
||||
@@ -92,6 +92,10 @@ export default [
|
||||
path: '/job-portal/policy/detail',// 政策详情
|
||||
component: './JobPortal/Policy/Detail',
|
||||
},
|
||||
{
|
||||
path: '/job-portal/live-recruitment',// 直播带岗
|
||||
component: './JobPortal/LiveRecruitment',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -211,6 +215,17 @@ export default [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'jobportal',
|
||||
path: '/jobportal',
|
||||
routes: [
|
||||
{
|
||||
name: '直播待岗管理',
|
||||
path: '/jobportal/live-mgmt',
|
||||
component: './Livemgmt',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
layout: false,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ReadOutlined,
|
||||
LoginOutlined,
|
||||
FundProjectionScreenOutlined,
|
||||
PlayCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { history, useLocation, useModel } from '@umijs/max';
|
||||
import {
|
||||
@@ -102,6 +103,7 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
|
||||
const isMessage = location.pathname.startsWith('/job-portal/message');
|
||||
const isPolicy = location.pathname.startsWith('/job-portal/policy');
|
||||
const isCareerRecommend = location.pathname.startsWith('/job-portal/career-recommendation');
|
||||
const isLiveRecruitment = location.pathname.startsWith('/job-portal/live-recruitment');
|
||||
|
||||
// 获取未读消息数量(仅登录用户)
|
||||
useEffect(() => {
|
||||
@@ -295,6 +297,14 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
|
||||
>
|
||||
政策
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => handleNavClick('/job-portal/live-recruitment')}
|
||||
className={`nav-btn${isLiveRecruitment ? ' active' : ''}`}
|
||||
>
|
||||
直播带岗
|
||||
</Button>
|
||||
<Badge count={unreadCount > 0 ? unreadCount : 0} offset={[8, 0]} size="small">
|
||||
<Button
|
||||
type="text"
|
||||
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
87
src/pages/Livemgmt/components/EditModal.tsx
Normal file
87
src/pages/Livemgmt/components/EditModal.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
ModalForm,
|
||||
ProFormText,
|
||||
ProFormTextArea,
|
||||
ProFormDateTimePicker,
|
||||
} from '@ant-design/pro-components';
|
||||
import { Form } from 'antd';
|
||||
import type { LiveStreamItem, CmsLiveForm } from '@/services/jobportal/live';
|
||||
|
||||
interface EditModalProps {
|
||||
open: boolean;
|
||||
values?: LiveStreamItem;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: CmsLiveForm) => Promise<void>;
|
||||
}
|
||||
|
||||
const EditModal: React.FC<EditModalProps> = ({ open, values, onCancel, onSubmit }) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
if (values) {
|
||||
form.setFieldsValue(values);
|
||||
}
|
||||
}
|
||||
}, [open, values, form]);
|
||||
|
||||
return (
|
||||
<ModalForm
|
||||
title={values ? '编辑直播带岗' : '新增直播带岗'}
|
||||
form={form}
|
||||
open={open}
|
||||
width={600}
|
||||
modalProps={{ destroyOnClose: true, onCancel }}
|
||||
onFinish={async (formValues) => {
|
||||
await onSubmit({ ...formValues, id: values?.id } as CmsLiveForm);
|
||||
}}
|
||||
>
|
||||
<ProFormText name="id" hidden />
|
||||
<ProFormText
|
||||
name="title"
|
||||
label="直播标题"
|
||||
rules={[{ required: true, message: '请输入直播标题' }]}
|
||||
placeholder="请输入直播标题"
|
||||
/>
|
||||
<ProFormText
|
||||
name="liveUrl"
|
||||
label="直播链接"
|
||||
rules={[{ required: true, message: '请输入直播链接' }]}
|
||||
placeholder="请输入直播链接"
|
||||
/>
|
||||
<ProFormText
|
||||
name="companyName"
|
||||
label="公司名称"
|
||||
placeholder="请输入公司名称"
|
||||
/>
|
||||
<ProFormText
|
||||
name="companyId"
|
||||
label="公司ID"
|
||||
placeholder="请输入公司ID"
|
||||
/>
|
||||
<ProFormDateTimePicker
|
||||
name="startTime"
|
||||
label="开始时间"
|
||||
width="md"
|
||||
rules={[{ required: true, message: '请选择开始时间' }]}
|
||||
fieldProps={{ format: 'YYYY-MM-DD HH:mm:ss' }}
|
||||
/>
|
||||
<ProFormDateTimePicker
|
||||
name="endTime"
|
||||
label="结束时间"
|
||||
width="md"
|
||||
rules={[{ required: true, message: '请选择结束时间' }]}
|
||||
fieldProps={{ format: 'YYYY-MM-DD HH:mm:ss' }}
|
||||
/>
|
||||
<ProFormTextArea
|
||||
name="description"
|
||||
label="直播描述"
|
||||
placeholder="请输入直播描述"
|
||||
/>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditModal;
|
||||
163
src/pages/Livemgmt/index.tsx
Normal file
163
src/pages/Livemgmt/index.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useAccess } from '@umijs/max';
|
||||
import { Button, message, Modal } from 'antd';
|
||||
import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { PlusOutlined, DeleteOutlined, FormOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
getCmsLiveList,
|
||||
addCmsLive,
|
||||
updateCmsLive,
|
||||
deleteCmsLive,
|
||||
} from '@/services/jobportal/live';
|
||||
import type { LiveStreamItem, CmsLiveListParams, CmsLiveForm } from '@/services/jobportal/live';
|
||||
import EditModal from './components/EditModal';
|
||||
|
||||
const LiveMgmtList: React.FC = () => {
|
||||
const access = useAccess();
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<LiveStreamItem>();
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该直播吗?',
|
||||
onOk: async () => {
|
||||
const res = await deleteCmsLive(id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
message.error(res.msg || '删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ProColumns<LiveStreamItem>[] = [
|
||||
{
|
||||
title: '直播标题',
|
||||
dataIndex: 'title',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '直播链接',
|
||||
dataIndex: 'liveUrl',
|
||||
ellipsis: true,
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '公司名称',
|
||||
dataIndex: 'companyName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'startTime',
|
||||
valueType: 'dateTime',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'endTime',
|
||||
valueType: 'dateTime',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
valueType: 'dateTime',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
render: (_, record) => [
|
||||
<Button
|
||||
key="edit"
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<FormOutlined />}
|
||||
hidden={!access.hasPerms('cms:live:edit')}
|
||||
onClick={() => {
|
||||
setCurrentRow(record);
|
||||
setModalVisible(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
hidden={!access.hasPerms('cms:live:remove')}
|
||||
onClick={() => handleDelete(record.id)}
|
||||
>
|
||||
删除
|
||||
</Button>,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ProTable<LiveStreamItem>
|
||||
headerTitle="直播待岗列表"
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
scroll={{ x: 'max-content' }}
|
||||
request={async (params) => {
|
||||
const res = await getCmsLiveList({
|
||||
current: params.current,
|
||||
pageSize: params.pageSize,
|
||||
title: params.title,
|
||||
companyName: params.companyName,
|
||||
} as CmsLiveListParams);
|
||||
return { data: res.rows, total: res.total, success: true };
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
hidden={!access.hasPerms('cms:live:add')}
|
||||
onClick={() => {
|
||||
setCurrentRow(undefined);
|
||||
setModalVisible(true);
|
||||
}}
|
||||
>
|
||||
新增
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
<EditModal
|
||||
open={modalVisible}
|
||||
values={currentRow}
|
||||
onCancel={() => {
|
||||
setModalVisible(false);
|
||||
setCurrentRow(undefined);
|
||||
}}
|
||||
onSubmit={async (values: CmsLiveForm) => {
|
||||
const res = values.id
|
||||
? await updateCmsLive(values)
|
||||
: await addCmsLive(values);
|
||||
if (res.code === 200) {
|
||||
message.success(values.id ? '修改成功' : '新增成功');
|
||||
setModalVisible(false);
|
||||
setCurrentRow(undefined);
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
message.error(res.msg || '操作失败');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveMgmtList;
|
||||
@@ -180,7 +180,7 @@ function ManagementList() {
|
||||
size="small"
|
||||
key="view-resume"
|
||||
icon={<AlignLeftOutlined/>}
|
||||
hidden={!access.hasPerms('area:business:List.add')}
|
||||
hidden={!access.hasPerms('cms:userworkexperiences:list')}
|
||||
onClick={() => {
|
||||
setCurrentRow(record);
|
||||
setResumeVisible(true);
|
||||
@@ -193,7 +193,7 @@ function ManagementList() {
|
||||
size="small"
|
||||
key="edit"
|
||||
icon={<FormOutlined/>}
|
||||
hidden={!access.hasPerms('area:business:List.add')}
|
||||
hidden={!access.hasPerms('cms:employeeConfirm:add')}
|
||||
onClick={() => {
|
||||
setCurrentRow(record);
|
||||
setHireVisible(true);
|
||||
@@ -207,7 +207,7 @@ function ManagementList() {
|
||||
size="small"
|
||||
key="edit"
|
||||
icon={<AlignLeftOutlined/>}
|
||||
hidden={!access.hasPerms('area:business:List.update')}
|
||||
// hidden={!access.hasPerms('area:business:List.update')}
|
||||
onClick={() => {
|
||||
setModalVisible(true);
|
||||
setCurrentRow(record);
|
||||
|
||||
110
src/services/jobportal/live.ts
Normal file
110
src/services/jobportal/live.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 直播带岗列表项 */
|
||||
export interface LiveStreamItem {
|
||||
id: number;
|
||||
title: string;
|
||||
liveUrl: string;
|
||||
description?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
companyId?: string;
|
||||
companyName: string;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/** 直播带岗列表查询参数 */
|
||||
export interface LiveListParams {
|
||||
title?: string;
|
||||
companyName?: string;
|
||||
}
|
||||
|
||||
/** 直播带岗列表响应 */
|
||||
export interface LiveListResult {
|
||||
code: number;
|
||||
msg?: string;
|
||||
total: number;
|
||||
rows: LiveStreamItem[];
|
||||
data?: any;
|
||||
}
|
||||
|
||||
/** 获取直播带岗列表 GET /app/live/list */
|
||||
export async function getLiveList(params?: LiveListParams) {
|
||||
return request<LiveListResult>('/api/app/live/list', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 管理端 CMS 接口 ====================
|
||||
|
||||
const CMS_BASE_URL = '/api/cms/live';
|
||||
|
||||
/** 管理端直播列表查询参数 */
|
||||
export interface CmsLiveListParams {
|
||||
title?: string;
|
||||
companyName?: string;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
current?: number;
|
||||
}
|
||||
|
||||
/** 管理端通用响应 */
|
||||
export interface CmsLiveCommonResult {
|
||||
code: number;
|
||||
msg?: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
/** 管理端直播列表响应 */
|
||||
export interface CmsLiveListResult {
|
||||
code: number;
|
||||
msg?: string;
|
||||
total: number;
|
||||
rows: LiveStreamItem[];
|
||||
data?: any;
|
||||
}
|
||||
|
||||
/** 管理端新增/编辑直播参数 */
|
||||
export interface CmsLiveForm {
|
||||
id?: number;
|
||||
title: string;
|
||||
liveUrl: string;
|
||||
description?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
companyId?: string;
|
||||
companyName?: string;
|
||||
}
|
||||
|
||||
/** 获取直播带岗管理列表 GET /api/cms/live/list */
|
||||
export async function getCmsLiveList(params?: CmsLiveListParams) {
|
||||
return request<CmsLiveListResult>(`${CMS_BASE_URL}/list`, {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 新增直播带岗 POST /api/cms/live */
|
||||
export async function addCmsLive(data: CmsLiveForm) {
|
||||
return request<CmsLiveCommonResult>(CMS_BASE_URL, {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改直播带岗 PUT /api/cms/live */
|
||||
export async function updateCmsLive(data: CmsLiveForm) {
|
||||
return request<CmsLiveCommonResult>(CMS_BASE_URL, {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除直播带岗 DELETE /api/cms/live/{id} */
|
||||
export async function deleteCmsLive(id: number) {
|
||||
return request<CmsLiveCommonResult>(`${CMS_BASE_URL}/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user