From a2e532389e1ad552dde169ef85d8f199e823e1ff Mon Sep 17 00:00:00 2001 From: bin <719488417@qq.com> Date: Tue, 2 Dec 2025 14:05:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=A1=8C=E4=B8=BA=E8=AE=B0=E5=BD=95=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=B2=97=E4=BD=8D=E4=BF=A1=E6=81=AF=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Analysis/User/components/AreaStatics.tsx | 2 +- src/pages/Mobileusers/List/index.tsx | 42 ++- src/pages/Mobileusers/components/logList.tsx | 288 ++++++++++++++++++ .../JobInfo/detail.tsx | 119 ++++++++ .../JobInfo/index.tsx | 286 +++++++++++++++++ src/pages/ResumeLibrary/ResumeList/index.tsx | 2 +- src/services/Management/list.ts | 6 + src/services/mobileusers/list.ts | 6 + src/types/mobileusers/list.d.ts | 17 ++ 9 files changed, 759 insertions(+), 9 deletions(-) create mode 100644 src/pages/Mobileusers/components/logList.tsx create mode 100644 src/pages/RecruitmentDataCollection/JobInfo/detail.tsx create mode 100644 src/pages/RecruitmentDataCollection/JobInfo/index.tsx diff --git a/src/pages/Analysis/User/components/AreaStatics.tsx b/src/pages/Analysis/User/components/AreaStatics.tsx index 752f0f1..38b88bd 100644 --- a/src/pages/Analysis/User/components/AreaStatics.tsx +++ b/src/pages/Analysis/User/components/AreaStatics.tsx @@ -55,7 +55,7 @@ export default function AreaStatics({ data }) { height: 220, autoFit: true, style:{ - columnWidthRatio: 0.4, // 统一柱状图宽度比例 + columnWidthRatio: 0.4, }, color: color, xAxis: { diff --git a/src/pages/Mobileusers/List/index.tsx b/src/pages/Mobileusers/List/index.tsx index d1f3852..2a83aa1 100644 --- a/src/pages/Mobileusers/List/index.tsx +++ b/src/pages/Mobileusers/List/index.tsx @@ -8,6 +8,7 @@ import { EyeOutlined, UserOutlined } from '@ant-design/icons'; import { getDictValueEnum } from '@/services/system/dict'; import DictTag from '@/components/DictTag'; import ResumeDetail from '../components/detail'; +import BehaviorLogList from '../components/logList'; function MobileUserList() { const access = useAccess(); @@ -18,6 +19,11 @@ function MobileUserList() { const [currentRow, setCurrentRow] = useState(); const [modalVisible, setModalVisible] = useState(false); const [loading, setLoading] = useState(false); + + // 用户行为 + const [behaviorModalVisible, setBehaviorModalVisible] = useState(false); + const [selectedUserId, setSelectedUserId] = useState(''); + const [selectedUserName, setSelectedUserName] = useState(''); // 字典枚举值 const [sexEnum, setSexEnum] = useState([]); @@ -25,7 +31,7 @@ function MobileUserList() { const [politicalEnum, setPoliticalEnum] = useState([]); const [areaEnum, setAreaEnum] = useState([]); - // 获取字典数据 + useEffect(() => { getDictValueEnum('sys_user_sex', true).then((data) => { setSexEnum(data); @@ -59,19 +65,27 @@ function MobileUserList() { } }; - // 查看用户行为(待定功能) - const handleViewBehavior = (userId: any) => { - message.info('用户行为查看功能开发中'); - // TODO: 待后续开发 + // 查看用户行为( + const handleViewBehavior = (userId: any, userName: string) => { + setSelectedUserId(userId); + setSelectedUserName(userName || '未知用户'); + setBehaviorModalVisible(true); }; const columns: ProColumns[] = [ - { + { title: '用户名', dataIndex: 'name', valueType: 'text', align: 'center', }, + { + title: '手机号', + dataIndex: 'phone', + valueType: 'text', + align: 'center', + hideInSearch: true, + }, { title: '出生日期', dataIndex: 'birthDate', @@ -155,7 +169,7 @@ function MobileUserList() { key="behavior" icon={} hidden={!access.hasPerms('mobileusers:list:viewBehavior')} - onClick={() => handleViewBehavior(userId)} + onClick={() => handleViewBehavior(userId, record.name)} > 用户行为 @@ -191,6 +205,8 @@ function MobileUserList() { }} /> + + {/* 简历详情弹窗 */} + + {/* 用户行为记录弹窗 */} + { + setBehaviorModalVisible(false); + setSelectedUserId(''); + setSelectedUserName(''); + }} + /> ); } diff --git a/src/pages/Mobileusers/components/logList.tsx b/src/pages/Mobileusers/components/logList.tsx new file mode 100644 index 0000000..79cc1df --- /dev/null +++ b/src/pages/Mobileusers/components/logList.tsx @@ -0,0 +1,288 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Table, Card, Tag, Space, Typography, Spin, Empty } from 'antd'; +import { getBehaviorLog } from '@/services/mobileusers/list'; + +const { Text } = Typography; + +export type BehaviorLogProps = { + userId: string | number; + userName: string; + open: boolean; + onCancel: () => void; +}; + +interface BehaviorLogItem { + id: string; + opTime: string; + content: string; + userId: string; + [key: string]: any; +} + +const BehaviorLogList: React.FC = (props) => { + const { userId, userName, open, onCancel } = props; + + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState([]); + const [total, setTotal] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + // 获取行为日志数据 + const fetchBehaviorLog = async (page: number = 1, size: number = 10) => { + if (!userId) return; + + setLoading(true); + try { + const params = { + userId: String(userId), + current: page, + pageSize: size, + }; + + const res = await getBehaviorLog(params); + if (res.code === 200) { + setDataSource(res.rows || []); + setTotal(Number(res.total) || 0); + } else { + setDataSource([]); + setTotal(0); + } + } catch (error) { + console.error('获取用户行为日志失败:', error); + setDataSource([]); + setTotal(0); + } finally { + setLoading(false); + } + }; + + // 当userId或open变化时重新获取数据 + useEffect(() => { + if (open && userId) { + fetchBehaviorLog(currentPage, pageSize); + } + }, [open, userId, currentPage, pageSize]); + + // 处理分页变化 + const handleTableChange = (pagination: any) => { + setCurrentPage(pagination.current); + setPageSize(pagination.pageSize); + }; + + // 格式化时间显示 + const formatTime = (timeStr: string) => { + if (!timeStr) return '-'; + + try { + const date = new Date(timeStr); + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); + } catch (error) { + return timeStr; + } + }; + + // 根据内容获取标签颜色 + const getTagColor = (content: string) => { + if (!content) return 'default'; + + const lowerContent = content.toLowerCase(); + if (lowerContent.includes('登录') || lowerContent.includes('登出')) { + return 'blue'; + } else if (lowerContent.includes('浏览') || lowerContent.includes('查看')) { + return 'green'; + } else if (lowerContent.includes('提交') || lowerContent.includes('申请')) { + return 'purple'; + } else if (lowerContent.includes('修改') || lowerContent.includes('更新')) { + return 'orange'; + } else if (lowerContent.includes('删除') || lowerContent.includes('取消')) { + return 'red'; + } else { + return 'default'; + } + }; + + // 表格列定义 + const columns = [ + { + title: '序号', + key: 'index', + width: 100, + align: 'center', + render: (_: any, __: any, index: number) => { + return (currentPage - 1) * pageSize + index + 1; + }, + }, + { + title: '操作时间', + dataIndex: 'opTime', + key: 'opTime', + align: 'center', + width: 180, + render: (text: string) => ( + + {formatTime(text)} + + ), + sorter: (a: BehaviorLogItem, b: BehaviorLogItem) => { + return new Date(a.opTime).getTime() - new Date(b.opTime).getTime(); + }, + }, + { + title: '行为类型', + key: 'actionType', + align: 'center', + width: 180, + render: (_: any, record: BehaviorLogItem) => { + const content = record.content || ''; + let type = '其他'; + + if (content.includes('登录')) type = '登录'; + else if (content.includes('浏览')) type = '浏览'; + else if (content.includes('提交')) type = '提交'; + else if (content.includes('修改')) type = '修改'; + else if (content.includes('删除')) type = '删除'; + + return ( + + {type} + + ); + }, + }, + { + title: '操作行为', + dataIndex: 'content', + key: 'content', + render: (text: string) => ( +
+ + {text} + +
+ ), + }, + ]; + + return ( + + 用户行为记录 + + ({userName || '未知用户'}) + + + } + open={open} + onCancel={onCancel} + width={1100} + footer={null} + bodyStyle={{ padding: 0 }} + > + + + {/* 统计信息 */} + {dataSource.length > 0 && ( + + +
+ 用户ID: + {userId} +
+
+ 用户名: + {userName} +
+
+ 记录总数: + + {total} 条 + +
+
+ 最近活动: + + {dataSource.length > 0 ? formatTime(dataSource[0].opTime) : '暂无'} + +
+
+
+ )} + {dataSource.length > 0 ? ( + `共 ${total} 条记录,显示 ${range[0]}-${range[1]} 条`, + pageSizeOptions: ['10', '20', '30', '50'], + }} + onChange={handleTableChange} + size="middle" + scroll={{ y: 400 }} + locale={{ + emptyText: ( + + ), + }} + onRow={(record) => { + return { + style: { + cursor: 'pointer', + transition: 'all 0.2s', + }, + onMouseEnter: (e) => { + e.currentTarget.style.backgroundColor = '#f5f5f5'; + }, + onMouseLeave: (e) => { + e.currentTarget.style.backgroundColor = 'inherit'; + }, + }; + }} + /> + ) : ( + +

暂无行为记录

+ 该用户目前没有记录的操作行为 + + } + style={{ margin: '40px 0' }} + /> + )} + + + + ); +}; + +export default BehaviorLogList; diff --git a/src/pages/RecruitmentDataCollection/JobInfo/detail.tsx b/src/pages/RecruitmentDataCollection/JobInfo/detail.tsx new file mode 100644 index 0000000..b677914 --- /dev/null +++ b/src/pages/RecruitmentDataCollection/JobInfo/detail.tsx @@ -0,0 +1,119 @@ +import React, { useEffect } from 'react'; +import { Modal, Descriptions,Button } from 'antd'; +import { DictValueEnumObj } from '@/components/DictTag'; + +export type DetailModalProps = { + onCancel: () => void; + open: boolean; + values?: Partial; + educationEnum?: DictValueEnumObj; + experienceEnum?: DictValueEnumObj; + areaEnum?: DictValueEnumObj; +}; + +const DetailModal: React.FC = (props) => { + const { values } = props; + + // 可以在这里获取字典数据,如果传入的话 + // 或者通过 useEffect 请求 + + // 如果没有传入字典数据,可以在这里获取 + useEffect(() => { + if (props.open && !props.educationEnum) { + // 这里可以获取字典数据 + // getDictValueEnum('education', true, true).then(setEducationEnum); + } + }, [props.open]); + + const handleCancel = () => { + props.onCancel(); + }; + + // 如果没有数据,显示加载状态 + if (!values) { + return null; + } + + return ( + + 关闭 + + ]} + destroyOnHidden + > + + + {values.jobTitle || '-'} + + + + {values.companyName || '-'} + + + + {`${values.minSalary || 0} - ${values.maxSalary || 0} 元/月`} + + + + {props.educationEnum ? ( + {props.educationEnum[values.education as string] || values.education || '-'} + ) : ( + values.education || '-' + )} + + + + {props.experienceEnum ? ( + {props.experienceEnum[values.experience as string] || values.experience || '-'} + ) : ( + values.experience || '-' + )} + + + + {props.areaEnum ? ( + {props.areaEnum[values.jobLocationAreaCode as string] || values.jobLocationAreaCode || '-'} + ) : ( + values.jobLocationAreaCode || '-' + )} + + + + {values.vacancies || 0} 人 + + + + {values.jobLocation || '-'} + + + +
+ {values.description || '-'} +
+
+ + + {values.view || 0} + + + + {values.createTime || '-'} + + + {values.isPublish !== undefined && ( + + {values.isPublish === 1 ? '已发布' : '未发布'} + + )} +
+
+ ); +}; + +export default DetailModal; \ No newline at end of file diff --git a/src/pages/RecruitmentDataCollection/JobInfo/index.tsx b/src/pages/RecruitmentDataCollection/JobInfo/index.tsx new file mode 100644 index 0000000..a008fe1 --- /dev/null +++ b/src/pages/RecruitmentDataCollection/JobInfo/index.tsx @@ -0,0 +1,286 @@ +import React, { Fragment, useRef, useState, useEffect } from 'react'; +import { history, useAccess } from '@umijs/max'; +import { getCmsJobList, getJobTrend } from '@/services/Management/list'; +import { Button, DatePicker, FormInstance, message, Space, Card } from 'antd'; +import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; +import { AlignLeftOutlined } from '@ant-design/icons'; +import { Line } from '@ant-design/charts'; +import dayjs from 'dayjs'; +import { getDictValueEnum } from '@/services/system/dict'; + +import DictTag from '@/components/DictTag'; +import DetailModal from './detail'; + +const { RangePicker } = DatePicker; + +function JobStatistics() { + const access = useAccess(); + const formTableRef = useRef(); + const actionRef = useRef(); + + const [chartData, setChartData] = useState([]); + const [currentRow, setCurrentRow] = useState(); + const [detailVisible, setDetailVisible] = useState(false); + + const [educationEnum, setEducationEnum] = useState([]); + const [experienceEnum, setExperienceEnum] = useState([]); + const [areaEnum, setAreaEnum] = useState([]); + const [isPublishEnum, setIsPublishEnum] = useState([]); + + const [params, setParams] = useState({ + beginTime: dayjs().subtract(13, 'day').format('YYYY-MM-DD'), + endTime: dayjs().format('YYYY-MM-DD'), + }); + + const getPickerValue = () => { + return [dayjs(params.beginTime), dayjs(params.endTime)]; + }; + + const disabledDate = (current: dayjs.Dayjs) => { + const now = dayjs(); + return current.isAfter(now.endOf('day')); + }; + + // 初始化图表数据 + useEffect(() => { + getChartData(); + }, [params.beginTime, params.endTime]); + + useEffect(() => { + getDictValueEnum('education', true, true).then((data) => { + setEducationEnum(data); + }); + getDictValueEnum('experience', true, true).then((data) => { + setExperienceEnum(data); + }); + getDictValueEnum('area', true, true).then((data) => { + setAreaEnum(data); + }); + getDictValueEnum('is_publish', true).then((data) => { + setIsPublishEnum(data); + }); + }, []); + + const handleDateRangeChange = (dates: any, dateStrings: [string, string]) => { + if (dates && dates[0] && dates[1]) { + setParams(() => ({ + beginTime: dateStrings[0], + endTime: dateStrings[1], + })); + } + }; + async function getChartData() { + let { data } = await getJobTrend(params); + setChartData(data); // 暂时使用模拟数据 + } + + // 图表配置 + const chartConfig = { + data: chartData, + xField: 'storageTime', + yField: 'insertCount', + style: { + columnWidthRatio: 0.6, + }, + xAxis: { + label: { + autoHide: true, + autoRotate: false, + }, + }, + point: { + size: 4, + shape: 'circle', + }, + line: { + size: 3, + style: { + lineCap: 'round', + }, + }, + tooltip: { + items: [ + { + field: 'insertCount', + name: '录入数量', + valueFormatter: (v) => `${v}个`, + title: '日期', + }, + ], + }, + }; + + const columns: ProColumns[] = [ + { + title: '岗位采集日期', + dataIndex: 'collectionDate1', + hideInTable: true, + valueType: 'dateRange', + search: { + transform: (value) => { + return { + beginTime: value[0], + endTime: value[1], + }; + }, + }, + }, + { + title: '岗位名称', + dataIndex: 'jobTitle', + valueType: 'text', + align: 'center', + }, + { + title: '最小薪资(元/月)', + dataIndex: 'minSalary', + valueType: 'text', + align: 'center', + hideInSearch: true, + }, + { + title: '最大薪资(元/月)', + dataIndex: 'maxSalary', + valueType: 'text', + align: 'center', + hideInSearch: true, + }, + { + title: '单位名称', + dataIndex: 'companyName', + valueType: 'text', + align: 'center', + }, + { + title: '学历要求', + dataIndex: 'education', + valueType: 'select', + align: 'center', + valueEnum: educationEnum, + render: (_, record) => { + return ; + }, + hideInSearch: true, + }, + { + title: '经验要求', + dataIndex: 'experience', + valueType: 'select', + align: 'center', + valueEnum: experienceEnum, + render: (_, record) => { + return ; + }, + hideInSearch: true, + }, + { + title: '是否发布', + dataIndex: 'isPublish', + valueType: 'select', + align: 'center', + valueEnum: isPublishEnum, + render: (_, record) => { + return ; + }, + hideInSearch: true, + }, + { + title: '招聘人数', + dataIndex: 'vacancies', + valueType: 'text', + align: 'center', + hideInSearch: true, + }, + { + title: '浏览量', + dataIndex: 'view', + valueType: 'text', + align: 'center', + hideInSearch: true, + }, + { + title: '操作', + hideInSearch: true, + align: 'center', + dataIndex: 'jobId', + width: 100, + render: (jobId, record) => ( + + ), + }, + ]; + + return ( + + 📊 采集趋势分析} + style={{ + marginBottom: 16, + }} + extra={ +
+ 统计日期范围: + +
+ } + > +
+ +
+
+ +
+ + actionRef={actionRef} + formRef={formTableRef} + rowKey="jobId" + key="jobStatisticsList" + columns={columns} + request={(params) => { + return getCmsJobList(params as API.ManagementList.ListParams).then((res) => { + const result = { + data: res.rows, + total: res.total, + success: true, + }; + return result; + }); + }} + search={{ + labelWidth: 120, + }} + toolBarRender={false} + pagination={{ + pageSize: 10, + }} + /> +
+ + { + setDetailVisible(false); + setCurrentRow(undefined); + }} + values={currentRow} + /> +
+ ); +} + +export default JobStatistics; diff --git a/src/pages/ResumeLibrary/ResumeList/index.tsx b/src/pages/ResumeLibrary/ResumeList/index.tsx index 2c65bc5..63116b9 100644 --- a/src/pages/ResumeLibrary/ResumeList/index.tsx +++ b/src/pages/ResumeLibrary/ResumeList/index.tsx @@ -182,7 +182,7 @@ function ResumeList() { hidden={!access.hasPerms('resumeLibrary:resumeList:view')} onClick={() => handleViewDetail(userId)} > - 查看简历 + 查看人才信息 ), diff --git a/src/services/Management/list.ts b/src/services/Management/list.ts index f86e551..9015726 100644 --- a/src/services/Management/list.ts +++ b/src/services/Management/list.ts @@ -51,3 +51,9 @@ export async function exportCmsJobCandidates(ids: string) { }, }); } +export async function getJobTrend(params) { + return request(`/api/cms/jobTrend/list`, { + method: 'GET', + params: params + }); +} diff --git a/src/services/mobileusers/list.ts b/src/services/mobileusers/list.ts index 39e51dc..cd3ba08 100644 --- a/src/services/mobileusers/list.ts +++ b/src/services/mobileusers/list.ts @@ -14,6 +14,12 @@ export async function getCmsAppUser(userId?: string) { method: 'GET', }); } +export async function getBehaviorLog(params?:API.MobileUser.LogParams) { + return request(`/api/cms/behaviorLog/getList`, { + method: 'GET', + params + }); +} export async function exportCmsAppUserExport(params?: API.MobileUser.ListParams) { return downLoadXlsx( diff --git a/src/types/mobileusers/list.d.ts b/src/types/mobileusers/list.d.ts index 8aa3ceb..6bc9867 100644 --- a/src/types/mobileusers/list.d.ts +++ b/src/types/mobileusers/list.d.ts @@ -35,4 +35,21 @@ declare namespace API.MobileUser { education?: string; politicalAffiliation?: string; } + export interface LogParams { + userId?: string; + current?: string; + pageSize?: string; + } + export interface LogRow { + opTime?: string; + content?: string; + } + + export interface LogResult { + total: number; + rows: LogRow[]; + code: number; + msg: string; + } + }