投诉功能开发
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:
@@ -84,6 +84,10 @@ export default [
|
||||
path: '/job-portal/personal-center/interviews', // 我的面试邀约
|
||||
component: './JobPortal/PersonalCenter/Interviews',
|
||||
},
|
||||
{
|
||||
path: '/job-portal/personal-center/complaints', // 我的投诉
|
||||
component: './JobPortal/PersonalCenter/Complaints',
|
||||
},
|
||||
{
|
||||
path: '/job-portal/message', // 消息通知页面
|
||||
component: './JobPortal/Message',
|
||||
@@ -204,6 +208,11 @@ export default [
|
||||
path: '/management/showcase-review',
|
||||
component: './Management/Showcase',
|
||||
},
|
||||
{
|
||||
name: '投诉管理',
|
||||
path: '/management/complaint',
|
||||
component: './Complaint',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
182
src/components/JobComplaintModal/index.tsx
Normal file
182
src/components/JobComplaintModal/index.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Select,
|
||||
Input,
|
||||
Button,
|
||||
message,
|
||||
Spin,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import type { DictValueEnumObj } from '@/components/DictTag';
|
||||
import { getJobPortalUserId, ensureJobPortalLogin } from '@/utils/jobPortalAuth';
|
||||
import { submitJobComplaint } from '@/services/jobportal/user';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface JobComplaintModalProps {
|
||||
open: boolean;
|
||||
jobId: string | number;
|
||||
onCancel: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const JobComplaintModal: React.FC<JobComplaintModalProps> = ({
|
||||
open,
|
||||
jobId,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [complaintTypeEnum, setComplaintTypeEnum] = useState<DictValueEnumObj>({});
|
||||
const [loadingDict, setLoadingDict] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// 加载投诉类型字典
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setLoadingDict(true);
|
||||
getDictValueEnum('complaint_type', false, true)
|
||||
.then((data) => {
|
||||
setComplaintTypeEnum(data);
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('加载投诉类型失败');
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingDict(false);
|
||||
});
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// 打开时重置表单
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 检查登录状态
|
||||
if (!ensureJobPortalLogin('提交投诉')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getJobPortalUserId();
|
||||
if (!userId) {
|
||||
message.warning('无法获取用户信息,请稍后重试');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
const res = await submitJobComplaint({
|
||||
userId: String(userId),
|
||||
jobId: String(jobId),
|
||||
complaintType: values.complaintType,
|
||||
complaintContent: values.complaintContent,
|
||||
contactPhone: values.contactPhone,
|
||||
});
|
||||
|
||||
if (res?.code === 200) {
|
||||
message.success('投诉提交成功');
|
||||
form.resetFields();
|
||||
onSuccess?.();
|
||||
onCancel();
|
||||
} else {
|
||||
message.error(res?.msg || '投诉提交失败');
|
||||
}
|
||||
} catch (error) {
|
||||
// 表单校验失败不提示
|
||||
if (error && typeof error === 'object' && 'errorFields' in error) {
|
||||
return;
|
||||
}
|
||||
console.error('提交投诉失败:', error);
|
||||
message.error('投诉提交失败,请重试');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 手机号校验规则
|
||||
const phoneRules = [
|
||||
{ required: true, message: '请输入联系方式' },
|
||||
{
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: '请输入正确的11位手机号码',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="岗位投诉"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
width={600}
|
||||
destroyOnClose
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" loading={submitting} onClick={handleSubmit}>
|
||||
确认投诉
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Spin spinning={loadingDict} tip="加载投诉类型...">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Form.Item
|
||||
name="complaintType"
|
||||
label="投诉类型"
|
||||
rules={[{ required: true, message: '请选择投诉类型' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择投诉类型"
|
||||
options={Object.entries(complaintTypeEnum).map(([key, val]) => ({
|
||||
value: key,
|
||||
label: (val as any).label || (val as any).text || key,
|
||||
}))}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="complaintContent"
|
||||
label="投诉描述"
|
||||
rules={[{ required: true, message: '请输入投诉描述' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={6}
|
||||
placeholder="请详细描述您要投诉的内容..."
|
||||
maxLength={2000}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="contactPhone"
|
||||
label="联系方式"
|
||||
rules={phoneRules}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入您的手机号码"
|
||||
maxLength={11}
|
||||
style={{ maxWidth: 300 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobComplaintModal;
|
||||
182
src/pages/Complaint/index.tsx
Normal file
182
src/pages/Complaint/index.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { Switch, message, Modal } from 'antd';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import DictTag from '@/components/DictTag';
|
||||
import { getJobComplaintList, jobUp, jobDown, companyUp, companyDown, type JobComplaint } from '@/services/cms/jobComplaint';
|
||||
|
||||
const ComplaintManagement: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [complaintTypeEnum, setComplaintTypeEnum] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
getDictValueEnum('complaint_type', false, true).then((data) => {
|
||||
setComplaintTypeEnum(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleJobToggle = async (record: JobComplaint) => {
|
||||
try {
|
||||
const isUp = record.jobStatus == 0;
|
||||
const res = isUp ? await jobDown(record.jobId) : await jobUp(record.jobId);
|
||||
if (res.code === 200) {
|
||||
message.success(isUp ? '岗位下架成功' : '岗位上架成功');
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
message.error(res.msg || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompanyToggle = async (record: JobComplaint) => {
|
||||
const isUp = record.companyStatus == 0;
|
||||
if (isUp) {
|
||||
Modal.confirm({
|
||||
title: '确认禁用企业',
|
||||
content: '禁用企业将禁用该企业下所有已发布的岗位,将不再对求职者展示。',
|
||||
okText: '确认禁用',
|
||||
cancelText: '取消',
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => doCompanyToggle(record, isUp),
|
||||
});
|
||||
} else {
|
||||
await doCompanyToggle(record, isUp);
|
||||
}
|
||||
};
|
||||
|
||||
const doCompanyToggle = async (record: JobComplaint, isUp: boolean) => {
|
||||
try {
|
||||
const res = isUp ? await companyDown(record.companyId) : await companyUp(record.companyId);
|
||||
if (res.code === 200) {
|
||||
message.success(isUp ? '企业下架成功' : '企业上架成功');
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
message.error(res.msg || '操作失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ProColumns<JobComplaint>[] = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
hideInSearch: true,
|
||||
render: (_, __, index) => (
|
||||
<span style={{ color: '#1890ff' }}>{index + 1}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '投诉者昵称',
|
||||
dataIndex: 'name',
|
||||
valueType: 'text',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '被投诉的企业',
|
||||
dataIndex: 'companyName',
|
||||
valueType: 'text',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '被投诉的岗位',
|
||||
dataIndex: 'jobTitle',
|
||||
valueType: 'text',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '投诉类型',
|
||||
dataIndex: 'complaintType',
|
||||
valueType: 'select',
|
||||
align: 'center',
|
||||
width: 120,
|
||||
valueEnum: complaintTypeEnum,
|
||||
render: (_, record) => {
|
||||
return <DictTag enums={complaintTypeEnum} value={record.complaintType} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '投诉描述',
|
||||
dataIndex: 'complaintContent',
|
||||
valueType: 'text',
|
||||
align: 'center',
|
||||
width: 250,
|
||||
ellipsis: true,
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '投诉时间',
|
||||
dataIndex: 'createTime',
|
||||
valueType: 'dateTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '岗位启用',
|
||||
dataIndex: 'jobStatus',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
hideInSearch: true,
|
||||
render: (_, record) => (
|
||||
<Switch
|
||||
checked={record.jobStatus == 0}
|
||||
onChange={() => handleJobToggle(record)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '企业启用',
|
||||
dataIndex: 'companyStatus',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
hideInSearch: true,
|
||||
render: (_, record) => (
|
||||
<Switch
|
||||
checked={record.companyStatus == 0}
|
||||
onChange={() => handleCompanyToggle(record)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div style={{ width: '100%', float: 'right' }}>
|
||||
<ProTable<JobComplaint>
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
search={{ labelWidth: 100 }}
|
||||
request={(params) => {
|
||||
const { current, pageSize, ...rest } = params;
|
||||
return getJobComplaintList({
|
||||
currentPage: current,
|
||||
pageSize,
|
||||
...rest,
|
||||
}).then((res) => {
|
||||
const result = {
|
||||
data: res.rows,
|
||||
total: res.total,
|
||||
success: true,
|
||||
};
|
||||
return result;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComplaintManagement;
|
||||
@@ -22,7 +22,8 @@ import {
|
||||
UserOutlined,
|
||||
TrophyOutlined,
|
||||
FireOutlined,
|
||||
StarOutlined
|
||||
StarOutlined,
|
||||
FlagOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { Radar } from '@ant-design/charts';
|
||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||
@@ -35,6 +36,7 @@ import { isJobPortalLoggedIn } from '@/utils/jobPortalAuth';
|
||||
import { favoriteJob, unfavoriteJob, applyJob, browseJob, getJobDetail } from '@/services/jobportal/user';
|
||||
import { ensureJobPortalLogin, requireJobPortalUserId } from '@/utils/jobPortalAuth';
|
||||
import { getDictLabel, resolveIndustryLabel } from '@/utils/jobPortalDict';
|
||||
import JobComplaintModal from '@/components/JobComplaintModal';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import { getCmsIndustryTreeList } from '@/services/classify/industry';
|
||||
import type { DictValueEnumObj } from '@/components/DictTag';
|
||||
@@ -190,6 +192,7 @@ const JobDetailPage: React.FC = () => {
|
||||
]);
|
||||
const [scaleEnum, setScaleEnum] = useState<DictValueEnumObj>({});
|
||||
const [industryTree, setIndustryTree] = useState<any[]>([]);
|
||||
const [complaintVisible, setComplaintVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
@@ -450,7 +453,7 @@ const JobDetailPage: React.FC = () => {
|
||||
{/* 职位头部信息 */}
|
||||
<Card className="job-header-card">
|
||||
<Row gutter={24}>
|
||||
<Col span={18}>
|
||||
<Col span={16}>
|
||||
<div className="job-title-section">
|
||||
<Space direction="vertical" size={8}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px', flexWrap: 'wrap' }}>
|
||||
@@ -458,25 +461,6 @@ const JobDetailPage: React.FC = () => {
|
||||
<Title level={2} className="job-title" style={{ marginBottom: 0 }}>{jobDetail.title}</Title>
|
||||
<Text className="job-salary-display">{jobDetail.salary}</Text>
|
||||
</Space>
|
||||
<Button
|
||||
icon={isFavorited ? <HeartFilled /> : <HeartOutlined />}
|
||||
danger={isFavorited}
|
||||
onClick={handleCollect}
|
||||
size="middle"
|
||||
style={{
|
||||
borderRadius: '6px',
|
||||
fontWeight: 500,
|
||||
height: '32px',
|
||||
padding: '0 12px',
|
||||
fontSize: '14px',
|
||||
...(isFavorited ? {} : {
|
||||
borderColor: '#1890ff',
|
||||
color: '#1890ff'
|
||||
})
|
||||
}}
|
||||
>
|
||||
{isFavorited ? '已收藏' : '收藏'}
|
||||
</Button>
|
||||
</div>
|
||||
<Space size={16}>
|
||||
<Text className="job-meta">
|
||||
@@ -497,17 +481,33 @@ const JobDetailPage: React.FC = () => {
|
||||
</Space>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="job-actions">
|
||||
<Col span={8}>
|
||||
<div className="job-actions" style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
|
||||
<Button
|
||||
icon={isFavorited ? <HeartFilled /> : <HeartOutlined />}
|
||||
danger={isFavorited}
|
||||
onClick={handleCollect}
|
||||
size="large"
|
||||
style={isFavorited ? {} : { borderColor: '#1890ff', color: '#1890ff' }}
|
||||
>
|
||||
{isFavorited ? '已收藏' : '收藏'}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
disabled={originalJobData?.isApply === 1}
|
||||
onClick={handleApply}
|
||||
>
|
||||
{originalJobData?.isApply === 1 ? '已投递' : '立即申请'}
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<FlagOutlined />}
|
||||
size="large"
|
||||
onClick={() => setComplaintVisible(true)}
|
||||
>
|
||||
投诉
|
||||
</Button>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -643,6 +643,13 @@ const JobDetailPage: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* 岗位投诉弹窗 */}
|
||||
<JobComplaintModal
|
||||
open={complaintVisible}
|
||||
jobId={originalJobData?.jobId || originalJobData?.id || id || ''}
|
||||
onCancel={() => setComplaintVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ import { favoriteJob, unfavoriteJob, applyJob, browseJob } from '@/services/jobp
|
||||
import { ensureJobPortalLogin, requireJobPortalUserId } from '@/utils/jobPortalAuth';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import { getDictLabel, findTreeLabelById, resolveIndustryLabel } from '@/utils/jobPortalDict';
|
||||
import JobComplaintModal from '@/components/JobComplaintModal';
|
||||
import { getJobTitleTreeSelect } from '@/services/common/jobTitle';
|
||||
import { getCmsIndustryTreeList } from '@/services/classify/industry';
|
||||
import type { DictValueEnumObj } from '@/components/DictTag';
|
||||
@@ -213,6 +214,7 @@ const JobListPage: React.FC = () => {
|
||||
{ item: '年龄', score: 0 },
|
||||
{ item: '工作地', score: 0 }
|
||||
]);
|
||||
const [complaintVisible, setComplaintVisible] = useState(false);
|
||||
// 格式化薪资显示
|
||||
const formatSalary = (minSalary: number, maxSalary: number) => {
|
||||
if (minSalary && maxSalary) {
|
||||
@@ -837,10 +839,16 @@ const JobListPage: React.FC = () => {
|
||||
>
|
||||
{selectedJob?.isApply === 1 ? '已投递' : '立即申请'}
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<FlagOutlined />}
|
||||
onClick={() => setComplaintVisible(true)}
|
||||
>
|
||||
投诉
|
||||
</Button>
|
||||
</Space>
|
||||
<Space className="action-icons" size={16}>
|
||||
<QrcodeOutlined title="微信扫码分享" />
|
||||
<FlagOutlined title="举报" />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
@@ -987,6 +995,13 @@ const JobListPage: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* 岗位投诉弹窗 */}
|
||||
<JobComplaintModal
|
||||
open={complaintVisible}
|
||||
jobId={selectedJob?.jobId || selectedJob?.id || ''}
|
||||
onCancel={() => setComplaintVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
52
src/pages/JobPortal/PersonalCenter/Complaints/index.less
Normal file
52
src/pages/JobPortal/PersonalCenter/Complaints/index.less
Normal file
@@ -0,0 +1,52 @@
|
||||
@import '../../theme.less';
|
||||
|
||||
.complaints-page {
|
||||
min-height: 100vh;
|
||||
background-color: @jp-bg-page;
|
||||
|
||||
.page-content {
|
||||
.jp-page-container();
|
||||
padding: 24px 20px 40px;
|
||||
|
||||
.back-button .ant-btn {
|
||||
color: @jp-primary;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
color: @jp-primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header .ant-typography {
|
||||
color: @jp-text-primary;
|
||||
}
|
||||
|
||||
.job-list .job-card {
|
||||
.jp-card-base();
|
||||
margin-bottom: 16px;
|
||||
|
||||
.complaint-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.complaint-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.complaint-meta {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.complaint-content {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.complaint-footer {
|
||||
color: @jp-text-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/pages/JobPortal/PersonalCenter/Complaints/index.tsx
Normal file
185
src/pages/JobPortal/PersonalCenter/Complaints/index.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Tag,
|
||||
Typography,
|
||||
Button,
|
||||
Empty,
|
||||
message,
|
||||
Spin,
|
||||
Modal,
|
||||
Pagination,
|
||||
} from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
WarningOutlined,
|
||||
ClockCircleOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { history } from '@umijs/max';
|
||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||
import { getMyComplaintList, deleteComplaint, type JobComplaint } from '@/services/cms/jobComplaint';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import type { DictValueEnumObj } from '@/components/DictTag';
|
||||
import './index.less';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
const ComplaintsPage: React.FC = () => {
|
||||
const [complaints, setComplaints] = useState<JobComplaint[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [pageSize] = useState<number>(10);
|
||||
const [complaintTypeEnum, setComplaintTypeEnum] = useState<DictValueEnumObj>({});
|
||||
|
||||
useEffect(() => {
|
||||
getDictValueEnum('complaint_type', false, true).then((data) => {
|
||||
setComplaintTypeEnum(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchComplaints = async (page: number = 1) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await getMyComplaintList({ currentPage: page, pageSize });
|
||||
if (res?.code === 200) {
|
||||
setComplaints(res.rows);
|
||||
setTotal(res.total);
|
||||
} else {
|
||||
message.error(res?.msg || '获取投诉列表失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('获取投诉列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchComplaints();
|
||||
}, []);
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
fetchComplaints(page);
|
||||
};
|
||||
|
||||
const handleDelete = (record: JobComplaint) => {
|
||||
Modal.confirm({
|
||||
title: '确认撤销投诉',
|
||||
content: `确定要撤销对「${record.jobTitle}」的投诉吗?`,
|
||||
okText: '确认撤销',
|
||||
cancelText: '取消',
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await deleteComplaint(record.id);
|
||||
if (res.code === 200) {
|
||||
message.success('撤销投诉成功');
|
||||
fetchComplaints(currentPage);
|
||||
} else {
|
||||
message.error(res.msg || '撤销失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('撤销失败,请重试');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getComplaintTypeLabel = (type: number): string => {
|
||||
const item = complaintTypeEnum[type];
|
||||
return (item as any)?.label || (item as any)?.text || String(type);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
history.push('/job-portal/personal-center');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="complaints-page">
|
||||
<JobPortalHeader showSearch={false} showHotJobs={false} />
|
||||
|
||||
<Spin spinning={loading}>
|
||||
<div className="page-content">
|
||||
<div className="back-button">
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={handleBack}>
|
||||
返回个人中心
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="page-header">
|
||||
<Title level={2}>
|
||||
<WarningOutlined style={{ color: '#ff4d4f', marginRight: 8 }} />
|
||||
我的投诉
|
||||
</Title>
|
||||
<Text type="secondary">共 {total} 条记录</Text>
|
||||
</div>
|
||||
|
||||
{complaints.length === 0 && !loading ? (
|
||||
<Empty description="暂无投诉记录" style={{ marginTop: '100px' }} />
|
||||
) : (
|
||||
<>
|
||||
<div className="job-list">
|
||||
{complaints.map((item) => (
|
||||
<Card key={item.id} className="job-card">
|
||||
<Row gutter={16} align="middle">
|
||||
<Col span={20}>
|
||||
<div className="complaint-info">
|
||||
<div className="complaint-header">
|
||||
<Title level={4} className="complaint-title">
|
||||
{item.jobTitle}
|
||||
</Title>
|
||||
<Tag color="orange">{getComplaintTypeLabel(item.complaintType)}</Tag>
|
||||
</div>
|
||||
<div className="complaint-meta">
|
||||
<Text className="complaint-company">{item.companyName}</Text>
|
||||
</div>
|
||||
<div className="complaint-content">
|
||||
<Paragraph ellipsis={{ rows: 2 }} type="secondary">
|
||||
{item.complaintContent}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className="complaint-footer">
|
||||
<Text type="secondary">
|
||||
<ClockCircleOutlined /> {item.createTime}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={4} style={{ textAlign: 'right' }}>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
撤销投诉
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
{total > pageSize && (
|
||||
<div style={{ textAlign: 'center', marginTop: 24 }}>
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComplaintsPage;
|
||||
@@ -136,7 +136,8 @@ const PersonalCenter: React.FC = () => {
|
||||
applications: 0,
|
||||
favorites: 0,
|
||||
footprints: 0,
|
||||
appointments: 0
|
||||
appointments: 0,
|
||||
complaints: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -149,6 +150,7 @@ const PersonalCenter: React.FC = () => {
|
||||
favorites: Number(response.data.ysc) || 0,
|
||||
footprints: Number(response.data.yzj) || 0,
|
||||
appointments: Number(response.data.yyy) || 0,
|
||||
complaints: Number(response.data.yts) || 0,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -268,7 +270,7 @@ const PersonalCenter: React.FC = () => {
|
||||
|
||||
<section className="stats-section">
|
||||
<Row gutter={0}>
|
||||
<Col span={8}>
|
||||
<Col span={6}>
|
||||
<div
|
||||
className="stat-item"
|
||||
onClick={() => history.push('/job-portal/personal-center/applications')}
|
||||
@@ -277,7 +279,7 @@ const PersonalCenter: React.FC = () => {
|
||||
<div className="stat-label">投递</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Col span={6}>
|
||||
<div
|
||||
className="stat-item"
|
||||
onClick={() => history.push('/job-portal/personal-center/favorites')}
|
||||
@@ -286,7 +288,7 @@ const PersonalCenter: React.FC = () => {
|
||||
<div className="stat-label">收藏</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Col span={6}>
|
||||
<div
|
||||
className="stat-item"
|
||||
onClick={() => history.push('/job-portal/personal-center/footprints')}
|
||||
@@ -295,12 +297,15 @@ const PersonalCenter: React.FC = () => {
|
||||
<div className="stat-label">足迹</div>
|
||||
</div>
|
||||
</Col>
|
||||
{/* <Col span={6}>
|
||||
<div className="stat-item stat-item-last">
|
||||
<div className="stat-number">{statistics.appointments}</div>
|
||||
<div className="stat-label">预约</div>
|
||||
<Col span={6}>
|
||||
<div
|
||||
className="stat-item"
|
||||
onClick={() => history.push('/job-portal/personal-center/complaints')}
|
||||
>
|
||||
<div className="stat-number">{statistics.complaints || 0}</div>
|
||||
<div className="stat-label">投诉</div>
|
||||
</div>
|
||||
</Col> */}
|
||||
</Col>
|
||||
</Row>
|
||||
</section>
|
||||
|
||||
|
||||
95
src/services/cms/jobComplaint.ts
Normal file
95
src/services/cms/jobComplaint.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
/** 投诉列表查询参数 */
|
||||
export interface JobComplaintListParams {
|
||||
pageSize?: number;
|
||||
currentPage?: number;
|
||||
complaintType?: string;
|
||||
complaintStatus?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** 投诉记录 */
|
||||
export interface JobComplaint {
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
id: number;
|
||||
userId: number;
|
||||
jobId: number;
|
||||
complaintType: number;
|
||||
complaintContent: string;
|
||||
evidenceUrl: string;
|
||||
contactPhone: string;
|
||||
complaintStatus: number;
|
||||
handleUserId: number | null;
|
||||
handleContent: string;
|
||||
handleTime: string | null;
|
||||
companyId: number;
|
||||
companyName: string;
|
||||
jobTitle: string;
|
||||
name: string;
|
||||
/** 岗位状态:0=上架,1=下架 */
|
||||
jobStatus?: number;
|
||||
/** 企业状态:0=上架,1=下架 */
|
||||
companyStatus?: number;
|
||||
}
|
||||
|
||||
/** 投诉列表分页结果 */
|
||||
export interface JobComplaintPageResult {
|
||||
total: number;
|
||||
rows: JobComplaint[];
|
||||
code: number;
|
||||
msg: string;
|
||||
data: null;
|
||||
}
|
||||
|
||||
/** 查询投诉列表(管理端) */
|
||||
export async function getJobComplaintList(params?: JobComplaintListParams) {
|
||||
return request<JobComplaintPageResult>('/api/cms/jobComplaint/list', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 岗位上架 */
|
||||
export async function jobUp(jobId: number) {
|
||||
return request<API.Result>(`/api/cms/job/jobUp/${jobId}`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
||||
|
||||
/** 岗位下架 */
|
||||
export async function jobDown(jobId: number) {
|
||||
return request<API.Result>(`/api/cms/job/jobDown/${jobId}`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
||||
|
||||
/** 企业上架 */
|
||||
export async function companyUp(companyId: number) {
|
||||
return request<API.Result>(`/api/cms/company/companyUp/${companyId}`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
||||
|
||||
/** 企业下架 */
|
||||
export async function companyDown(companyId: number) {
|
||||
return request<API.Result>(`/api/cms/company/companyDown/${companyId}`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询我的投诉列表(求职者端) */
|
||||
export async function getMyComplaintList(params?: JobComplaintListParams) {
|
||||
return request<JobComplaintPageResult>('/api/cms/jobComplaint/listSelf', {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 撤销投诉 */
|
||||
export async function deleteComplaint(ids: number | string) {
|
||||
return request<API.Result>(`/api/cms/jobComplaint/${ids}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
@@ -168,3 +168,17 @@ export async function deleteShowcaseFile(id: number) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 提交岗位投诉 POST /cms/jobComplaint */
|
||||
export async function submitJobComplaint(params: {
|
||||
userId: string;
|
||||
jobId: string;
|
||||
complaintType: string;
|
||||
complaintContent: string;
|
||||
contactPhone: string;
|
||||
}) {
|
||||
return request<API.Result>('/api/cms/jobComplaint', {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
1
src/types/jobportal/user.d.ts
vendored
1
src/types/jobportal/user.d.ts
vendored
@@ -72,6 +72,7 @@ declare namespace API {
|
||||
ysc?: string; // 收藏
|
||||
yzj?: string; // 足迹
|
||||
yyy?: string; // 预约
|
||||
yts?: string; // 投诉
|
||||
}
|
||||
|
||||
export interface AppUserYhscResult {
|
||||
|
||||
Reference in New Issue
Block a user