From 959dea89982762e4e221b175204f7b191cf757f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=AF=E8=BE=89?= Date: Mon, 6 Jul 2026 21:13:15 +0800 Subject: [PATCH 01/12] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Management/Project/edit.tsx | 186 ++++++++++++ src/pages/Management/Project/index.tsx | 373 +++++++++++++++++++++++++ src/services/cms/project.ts | 54 ++++ src/types/cms/project.d.ts | 44 +++ 4 files changed, 657 insertions(+) create mode 100644 src/pages/Management/Project/edit.tsx create mode 100644 src/pages/Management/Project/index.tsx create mode 100644 src/services/cms/project.ts create mode 100644 src/types/cms/project.d.ts diff --git a/src/pages/Management/Project/edit.tsx b/src/pages/Management/Project/edit.tsx new file mode 100644 index 0000000..2cce074 --- /dev/null +++ b/src/pages/Management/Project/edit.tsx @@ -0,0 +1,186 @@ +import React, { useEffect, useState } from 'react'; +import { message, Form } from 'antd'; +import { ModalForm, ProFormText, ProFormTextArea, ProFormSelect, ProFormTreeSelect } from '@ant-design/pro-components'; +import { getProjectDetail, addProject, updateProject } from '@/services/cms/project'; +import { getDictValueEnum } from '@/services/system/dict'; +import { getCmsIndustryTreeList } from '@/services/classify/industry'; + +interface EditProjectFormProps { + open: boolean; + values?: API.Project.ProjectItem; + mode?: 'edit' | 'view'; + onCancel: () => void; + onSuccess?: () => void; +} + +const EditProjectForm: React.FC = ({ open, values, mode, onCancel, onSuccess }) => { + const [form] = Form.useForm(); + const isView = mode === 'view'; + const isEdit = !!values?.projectId && !isView; + const [regionOptions, setRegionOptions] = useState<{ label: string; value: string }[]>([]); + + const title = isView ? '项目详情' : isEdit ? '编辑项目' : '新建项目'; + + // 加载字典数据 + useEffect(() => { + // 地区字典(与发布岗位工作区县同一个字典) + getDictValueEnum('area', true, true).then((data) => { + const opts = Object.entries(data || {}).map(([k, v]: [string, any]) => ({ label: v.text, value: v.text })); + setRegionOptions(opts); + }).catch(() => {}); + }, []); + + useEffect(() => { + if (open && values?.projectId) { + getProjectDetail(values.projectId).then((res) => { + if (res.code === 200 && res.data) { + form?.setFieldsValue?.(res.data); + } + }); + } else if (open && !values?.projectId) { + form?.resetFields?.(); + } + }, [open, values]); + + const handleSubmit = async (formValues: API.Project.ProjectItem) => { + try { + let res; + if (isEdit) { + res = await updateProject({ ...formValues, projectId: values?.projectId }); + } else { + res = await addProject(formValues); + } + if (res.code === 200) { + message.success(isEdit ? '修改成功' : '新增成功'); + onSuccess?.(); + return true; + } else { + message.error(res.msg || '操作失败'); + return false; + } + } catch { + message.error('操作失败,请重试'); + return false; + } + }; + + return ( + + + + + + getCmsIndustryTreeList().then((res) => res.data)} + fieldProps={{ + suffixIcon: null, + filterTreeNode: true, + showSearch: true, + popupMatchSelectWidth: false, + labelInValue: false, + autoClearSearchValue: true, + multiple: false, + treeNodeFilterProp: 'label', + fieldNames: { label: 'label', value: 'label', children: 'children' }, + }} + /> + + + + + + + ); +}; + +export default EditProjectForm; diff --git a/src/pages/Management/Project/index.tsx b/src/pages/Management/Project/index.tsx new file mode 100644 index 0000000..34f32b7 --- /dev/null +++ b/src/pages/Management/Project/index.tsx @@ -0,0 +1,373 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { useAccess } from '@umijs/max'; +import { Button, message, Modal, Switch, Tag } from 'antd'; +import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; +import { PlusOutlined, DeleteOutlined, FormOutlined, MessageOutlined } from '@ant-design/icons'; +import { getProjectList, deleteProject, updateProject, getProjectComments, replyComment } from '@/services/cms/project'; +import { getDictValueEnum } from '@/services/system/dict'; +import { getCmsIndustryTreeList } from '@/services/classify/industry'; +import EditProjectForm from './edit'; + +const statusMap: Record = { + '0': { text: '草稿', color: 'default' }, + '1': { text: '已发布', color: 'green' }, +}; + +const ProjectMgmt: React.FC = () => { + const access = useAccess(); + const actionRef = useRef(); + const [editModalOpen, setEditModalOpen] = useState(false); + const [editValues, setEditValues] = useState(); + const [viewModalOpen, setViewModalOpen] = useState(false); + const [viewValues, setViewValues] = useState(); + const [dictEnumMap, setDictEnumMap] = useState>>({}); + const [commentModalOpen, setCommentModalOpen] = useState(false); + const [commentProjectId, setCommentProjectId] = useState(); + const [comments, setComments] = useState([]); + const [replyTexts, setReplyTexts] = useState>({}); + const [industryLabelMap, setIndustryLabelMap] = useState>({}); + + // 加载字典数据 + useEffect(() => { + getDictValueEnum('area', true, true).then((data) => { + const textMap: Record = {}; + Object.values(data || {}).forEach((v: any) => { textMap[v.text] = { text: v.text }; }); + setDictEnumMap((prev) => ({ ...prev, region: textMap })); + }).catch(() => {}); + // 加载行业树,构建 id->label 映射 + getCmsIndustryTreeList().then((res) => { + const map: Record = {}; + const walk = (nodes: any[]) => { + nodes.forEach((n: any) => { + if (n.id && n.label) map[String(n.id)] = n.label; + if (n.children) walk(n.children); + }); + }; + walk(res?.data || []); + setIndustryLabelMap(map); + }).catch(() => {}); + }, []); + + const handleRemove = async (ids: string) => { + Modal.confirm({ + title: '确认删除', + content: '确定要删除选中的项目吗?', + onOk: async () => { + const hide = message.loading('正在删除'); + try { + const res = await deleteProject(ids); + hide(); + if (res.code === 200) { + message.success('删除成功'); + actionRef.current?.reload(); + } else { + message.error(res.msg || '删除失败'); + } + } catch { + hide(); + message.error('删除失败,请重试'); + } + }, + }); + }; + + const loadComments = async (projectId: number) => { + setCommentProjectId(projectId); + try { + const res = await getProjectComments(projectId); + if (res.code === 200) { + setComments(res.data || []); + } + } catch { message.error('加载评论失败'); } + setCommentModalOpen(true); + }; + + const handleReply = async (commentId: number) => { + const reply = replyTexts[commentId]?.trim(); + if (!reply) { message.warning('请输入回复内容'); return; } + try { + const res = await replyComment({ commentId, reply }); + if (res.code === 200) { + message.success('回复成功'); + setReplyTexts((prev) => ({ ...prev, [commentId]: '' })); + if (commentProjectId) loadComments(commentProjectId); + } else { message.error(res.msg || '回复失败'); } + } catch { message.error('回复失败'); } + }; + + const handleStatusChange = async (record: API.Project.ProjectItem, checked: boolean) => { + const newStatus = checked ? '1' : '0'; + try { + const res = await updateProject({ ...record, status: newStatus }); + if (res.code === 200) { + message.success(checked ? '已发布' : '已下架'); + actionRef.current?.reload(); + } else { + message.error(res.msg || '状态更新失败'); + } + } catch { + message.error('状态更新失败'); + } + }; + + const columns: ProColumns[] = [ + { + title: '项目名称', + dataIndex: 'title', + valueType: 'text', + align: 'center', + width: 200, + }, + { + title: '行业', + dataIndex: 'industry', + valueType: 'treeSelect', + align: 'center', + width: 120, + render: (_, record) => record.industry || '-', + fieldProps: { + showSearch: true, + filterTreeNode: true, + treeNodeFilterProp: 'label', + fieldNames: { label: 'label', value: 'label', children: 'children' }, + }, + request: async () => { + const res = await getCmsIndustryTreeList(); + return res?.data || []; + }, + }, + { + title: '地区', + dataIndex: 'region', + valueType: 'select', + align: 'center', + width: 100, + render: (_, record) => { + const val = dictEnumMap.region?.[record.region || '']; + return val?.text || record.region || '-'; + }, + valueEnum: dictEnumMap.region, + }, + { + title: '合作方式', + dataIndex: 'cooperationMode', + valueType: 'text', + align: 'center', + width: 120, + render: (_, record) => record.cooperationMode || '-', + }, + { + title: '联系人', + dataIndex: 'contactName', + valueType: 'text', + align: 'center', + width: 100, + hideInSearch: true, + render: (_, record) => record.contactName || '-', + }, + { + title: '联系电话', + dataIndex: 'contactPhone', + valueType: 'text', + align: 'center', + width: 130, + hideInSearch: true, + render: (_, record) => record.contactPhone || '-', + }, + { + title: '状态', + dataIndex: 'status', + valueType: 'select', + align: 'center', + width: 80, + valueEnum: { '0': '草稿', '1': '已发布' }, + render: (_, record) => { + const st = statusMap[record.status || '0']; + return ( + handleStatusChange(record, checked)} + checkedChildren="发布" + unCheckedChildren="草稿" + /> + ); + }, + }, + { + title: '创建时间', + dataIndex: 'createTime', + valueType: 'text', + align: 'center', + width: 160, + hideInSearch: true, + }, + { + title: '操作', + key: 'action', + align: 'center', + width: 180, + hideInSearch: true, + render: (_, record) => ( + <> + + + {access.hasPerms('cms:project:edit') && ( + + )} + {access.hasPerms('cms:project:remove') && ( + + )} + + ), + }, + ]; + + return ( + <> + + actionRef={actionRef} + rowKey="projectId" + columns={columns} + headerTitle="项目管理" + request={async (params) => { + const { current, pageSize, ...searchParams } = params; + const response = await getProjectList({ + ...searchParams, + pageNum: current, + pageSize, + }); + return { + data: response.rows || [], + total: response.total || 0, + success: true, + }; + }} + search={{ + labelWidth: 'auto', + }} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + toolBarRender={() => [ + access.hasPerms('cms:project:add') && ( + + ), + ]} + /> + + {/* 编辑/新建弹窗 */} + { + setEditModalOpen(false); + setEditValues(undefined); + }} + onSuccess={() => { + setEditModalOpen(false); + setEditValues(undefined); + actionRef.current?.reload(); + }} + /> + + {/* 查看详情弹窗 */} + { + setViewModalOpen(false); + setViewValues(undefined); + }} + /> + + {/* 评论管理弹窗 */} + { setCommentModalOpen(false); setComments([]); setReplyTexts({}); }} + footer={null} + width={700} + > + {comments.length === 0 ? ( +
暂无评论
+ ) : ( +
+ {comments.map((c: any) => ( +
+
+ {c.userName || '匿名用户'} + {c.createTime} +
+
{c.content}
+ {c.reply ? ( +
+ 已回复 ({c.replyTime}): +
{c.reply}
+
+ ) : ( +
+ setReplyTexts((prev) => ({ ...prev, [c.commentId]: e.target.value }))} + /> + +
+ )} +
+ ))} +
+ )} +
+ + ); +}; + +export default ProjectMgmt; diff --git a/src/services/cms/project.ts b/src/services/cms/project.ts new file mode 100644 index 0000000..b8c5b08 --- /dev/null +++ b/src/services/cms/project.ts @@ -0,0 +1,54 @@ +import { request } from '@umijs/max'; + +// 查询项目列表 +export async function getProjectList(params?: API.Project.ListParams) { + return request('/api/cms/project/list', { + method: 'GET', + params, + }); +} + +// 获取项目详情 +export async function getProjectDetail(id: number | string) { + return request(`/api/cms/project/${id}`, { + method: 'GET', + }); +} + +// 新增项目 +export async function addProject(data: Partial) { + return request('/api/cms/project', { + method: 'POST', + data, + }); +} + +// 修改项目 +export async function updateProject(data: Partial) { + return request('/api/cms/project', { + method: 'PUT', + data, + }); +} + +// 删除项目 +export async function deleteProject(ids: string) { + return request(`/api/cms/project/${ids}`, { + method: 'DELETE', + }); +} + +// 获取项目评论列表 +export async function getProjectComments(projectId: number | string) { + return request(`/api/cms/project/comments/${projectId}`, { + method: 'GET', + }); +} + +// 回复评论 +export async function replyComment(data: { commentId: number; reply: string }) { + return request('/api/cms/project/comment/reply', { + method: 'PUT', + data, + }); +} diff --git a/src/types/cms/project.d.ts b/src/types/cms/project.d.ts new file mode 100644 index 0000000..7928b50 --- /dev/null +++ b/src/types/cms/project.d.ts @@ -0,0 +1,44 @@ +declare namespace API.Project { + export interface ProjectResult { + total: number; + rows: ProjectItem[]; + code: number; + msg: string; + } + + export interface ProjectDetailResult { + code: number; + msg: string; + data: ProjectItem; + } + + export interface ProjectItem { + projectId?: number; + title: string; + description?: string; + requiredPersonnel?: string; + cooperationMode?: string; + industry?: string; + region?: string; + contactName?: string; + contactPhone?: string; + contactEmail?: string; + status?: string; + createBy?: string; + createTime?: string; + updateBy?: string; + updateTime?: string; + remark?: string; + } + + export interface ListParams { + pageNum?: number; + pageSize?: number; + searchValue?: string; + title?: string; + industry?: string; + region?: string; + cooperationMode?: string; + status?: string; + } +} From 450e46dda06df502e6c323475b0a7dc9caeca670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=AF=E8=BE=89?= Date: Tue, 7 Jul 2026 15:41:35 +0800 Subject: [PATCH 02/12] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BDbug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/cms/project.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/cms/project.d.ts b/src/types/cms/project.d.ts index 7928b50..48ac85e 100644 --- a/src/types/cms/project.d.ts +++ b/src/types/cms/project.d.ts @@ -24,6 +24,7 @@ declare namespace API.Project { contactPhone?: string; contactEmail?: string; status?: string; + companyId?: number; createBy?: string; createTime?: string; updateBy?: string; From 0ad9de30460e083dc9a6ed188f1cdbf0d3b49ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=AF=E8=BE=89?= Date: Tue, 7 Jul 2026 16:34:52 +0800 Subject: [PATCH 03/12] 11 --- src/pages/Management/List/edit.tsx | 70 +++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/pages/Management/List/edit.tsx b/src/pages/Management/List/edit.tsx index 3eddf99..0662de6 100644 --- a/src/pages/Management/List/edit.tsx +++ b/src/pages/Management/List/edit.tsx @@ -9,12 +9,13 @@ import { ProDescriptions, } from '@ant-design/pro-components'; import { Form, Button, Input, TreeSelect, Switch } from 'antd'; -import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons'; +import { PlusOutlined, MinusCircleOutlined, EnvironmentOutlined } from '@ant-design/icons'; import React, { useEffect, useRef, useState } from 'react'; import { DictValueEnumObj } from '@/components/DictTag'; import { getCmsCompanyList } from '@/services/company/list'; import { getCmsJobDetail } from '@/services/Management/list'; import { getJobTitleTreeSelect } from '@/services/common/jobTitle'; +import ProFromMap from '@/components/ProFromMap'; export type ListFormProps = { onCancel: (flag?: boolean, formVals?: unknown) => void; @@ -42,6 +43,8 @@ const listEdit: React.FC = (props) => { const [jobDetail, setJobDetail] = useState(null); const [loading, setLoading] = useState(false); const [jobCategoryTreeData, setJobCategoryTreeData] = useState([]); + const [mapOpen, setMapOpen] = useState(false); + const [mapViewInfo, setMapViewInfo] = useState({}); const { educationEnum, experienceEnum, areaEnum, jobTypeEnum } = props; const { mode = props.values ? 'edit' : 'create' } = props; useEffect(() => { @@ -52,6 +55,18 @@ const listEdit: React.FC = (props) => { ...props.values, jobLocationAreaCode: String(props.values.jobLocationAreaCode || ''), }); + // 初始化地图位置信息(编辑时回显) + if (props.values.latitude || props.values.longitude) { + setMapViewInfo({ + address: props.values.jobLocation || '', + latitude: props.values.latitude, + longitude: props.values.longitude, + }); + } else { + setMapViewInfo({}); + } + } else { + setMapViewInfo({}); } // 加载岗位标签树数据 getJobTitleTreeSelect().then((res) => { @@ -95,6 +110,25 @@ const listEdit: React.FC = (props) => { form.resetFields(); }; + const handleMapSelect = (result: any) => { + if (result.location) { + const { lat, lng } = result.location; + form.setFieldValue('latitude', lat); + form.setFieldValue('longitude', lng); + form.setFieldValue('jobLocation', result.address); + setMapViewInfo({ + address: result.address, + latitude: lat, + longitude: lng, + }); + setMapOpen(false); + } + }; + + const handleMapCancel = () => { + setMapOpen(false); + }; + const handleFinish = async (values: Record) => { const payload: API.ManagementList.Manage = { jobTitle: values.jobTitle, @@ -119,6 +153,8 @@ const listEdit: React.FC = (props) => { })), jobCategory: values.jobCategory, isUrgent: values.isUrgent ? 1 : 0, + latitude: values.latitude, + longitude: values.longitude, }; if (mode === 'edit' && values.jobId) { payload.jobId = values.jobId; @@ -430,6 +466,37 @@ const listEdit: React.FC = (props) => { /> + +
+ +
+ {mapViewInfo.address ? ( + <> + 地址:{mapViewInfo.address} + {mapViewInfo.latitude ? ( + + 经纬度:{mapViewInfo.latitude},{mapViewInfo.longitude} + + ) : null} + + ) : ( + 可选:点击按钮在地图上选择工作地点 + )} +
+
+
+