项目功能开发
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:
冯辉
2026-07-06 21:13:15 +08:00
parent 5672bfcbc6
commit 959dea8998
4 changed files with 657 additions and 0 deletions

View File

@@ -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<EditProjectFormProps> = ({ 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 (
<ModalForm
title={title}
open={open}
form={form}
width={650}
modalProps={{
onCancel,
destroyOnClose: true,
maskClosable: !isView,
}}
readonly={isView}
submitTimeout={2000}
onFinish={handleSubmit}
submitter={
isView
? false
: {
searchConfig: {
submitText: isEdit ? '保存修改' : '创建项目',
},
}
}
>
<ProFormText
name="title"
label="项目名称"
placeholder="请输入项目名称"
rules={[{ required: true, message: '请输入项目名称' }]}
readonly={isView}
/>
<ProFormTextArea
name="description"
label="项目描述"
placeholder="请输入项目描述(背景、目标、内容、特点等)"
fieldProps={{ rows: 4 }}
readonly={isView}
/>
<ProFormTextArea
name="requiredPersonnel"
label="项目需求人员"
placeholder="请输入项目所需的人员类型、专业技能、工作经验等"
fieldProps={{ rows: 3 }}
readonly={isView}
/>
<ProFormSelect
name="cooperationMode"
label="合作方式"
placeholder="请选择合作方式"
readonly={isView}
options={[
{ label: '合作开发', value: '合作开发' },
{ label: '技术转让', value: '技术转让' },
{ label: '技术咨询', value: '技术咨询' },
{ label: '技术服务', value: '技术服务' },
{ label: '联合研究', value: '联合研究' },
{ label: '委托开发', value: '委托开发' },
]}
/>
<ProFormTreeSelect
name="industry"
label="行业"
placeholder="请选择行业"
readonly={isView}
allowClear
request={async () => 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' },
}}
/>
<ProFormSelect
name="region"
label="地区"
placeholder="请选择地区"
readonly={isView}
options={regionOptions}
fieldProps={{ showSearch: true }}
/>
<ProFormText
name="contactName"
label="联系人"
placeholder="请输入联系人姓名"
readonly={isView}
/>
<ProFormText
name="contactPhone"
label="联系电话"
placeholder="请输入联系电话"
readonly={isView}
/>
<ProFormText
name="contactEmail"
label="联系邮箱"
placeholder="请输入联系邮箱"
readonly={isView}
/>
<ProFormSelect
name="status"
label="状态"
placeholder="请选择状态"
readonly={isView}
options={[
{ label: '草稿', value: '0' },
{ label: '发布', value: '1' },
]}
initialValue="0"
/>
</ModalForm>
);
};
export default EditProjectForm;

View File

@@ -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<string, { text: string; color: string }> = {
'0': { text: '草稿', color: 'default' },
'1': { text: '已发布', color: 'green' },
};
const ProjectMgmt: React.FC = () => {
const access = useAccess();
const actionRef = useRef<ActionType>();
const [editModalOpen, setEditModalOpen] = useState(false);
const [editValues, setEditValues] = useState<API.Project.ProjectItem | undefined>();
const [viewModalOpen, setViewModalOpen] = useState(false);
const [viewValues, setViewValues] = useState<API.Project.ProjectItem | undefined>();
const [dictEnumMap, setDictEnumMap] = useState<Record<string, Record<string, { text: string }>>>({});
const [commentModalOpen, setCommentModalOpen] = useState(false);
const [commentProjectId, setCommentProjectId] = useState<number | undefined>();
const [comments, setComments] = useState<any[]>([]);
const [replyTexts, setReplyTexts] = useState<Record<number, string>>({});
const [industryLabelMap, setIndustryLabelMap] = useState<Record<string, string>>({});
// 加载字典数据
useEffect(() => {
getDictValueEnum('area', true, true).then((data) => {
const textMap: Record<string, { text: string }> = {};
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<string, string> = {};
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<API.Project.ProjectItem>[] = [
{
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 (
<Switch
checked={record.status === '1'}
onChange={(checked) => 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) => (
<>
<Button
type="link"
size="small"
icon={<MessageOutlined />}
onClick={() => loadComments(record.projectId!)}
>
</Button>
<Button
type="link"
size="small"
onClick={() => {
setViewValues(record);
setViewModalOpen(true);
}}
>
</Button>
{access.hasPerms('cms:project:edit') && (
<Button
type="link"
size="small"
icon={<FormOutlined />}
onClick={() => {
setEditValues(record);
setEditModalOpen(true);
}}
>
</Button>
)}
{access.hasPerms('cms:project:remove') && (
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleRemove(String(record.projectId))}
>
</Button>
)}
</>
),
},
];
return (
<>
<ProTable<API.Project.ProjectItem>
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') && (
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setEditValues(undefined);
setEditModalOpen(true);
}}
>
</Button>
),
]}
/>
{/* 编辑/新建弹窗 */}
<EditProjectForm
open={editModalOpen}
values={editValues}
onCancel={() => {
setEditModalOpen(false);
setEditValues(undefined);
}}
onSuccess={() => {
setEditModalOpen(false);
setEditValues(undefined);
actionRef.current?.reload();
}}
/>
{/* 查看详情弹窗 */}
<EditProjectForm
open={viewModalOpen}
values={viewValues}
mode="view"
onCancel={() => {
setViewModalOpen(false);
setViewValues(undefined);
}}
/>
{/* 评论管理弹窗 */}
<Modal
title="评论管理"
open={commentModalOpen}
onCancel={() => { setCommentModalOpen(false); setComments([]); setReplyTexts({}); }}
footer={null}
width={700}
>
{comments.length === 0 ? (
<div style={{ textAlign: 'center', color: '#999', padding: 40 }}></div>
) : (
<div style={{ maxHeight: 400, overflowY: 'auto' }}>
{comments.map((c: any) => (
<div key={c.commentId} style={{ borderBottom: '1px solid #f0f0f0', padding: '16px 0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<span style={{ fontWeight: 500, color: '#1a62ce' }}>{c.userName || '匿名用户'}</span>
<span style={{ color: '#999', fontSize: 12 }}>{c.createTime}</span>
</div>
<div style={{ color: '#333', marginBottom: 8 }}>{c.content}</div>
{c.reply ? (
<div style={{ background: '#f6ffed', padding: '8px 12px', borderRadius: 6, marginTop: 4 }}>
<span style={{ color: '#52c41a', fontSize: 12 }}> ({c.replyTime})</span>
<div style={{ color: '#333' }}>{c.reply}</div>
</div>
) : (
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<input
style={{ flex: 1, padding: '6px 12px', border: '1px solid #d9d9d9', borderRadius: 6 }}
placeholder="输入回复..."
value={replyTexts[c.commentId] || ''}
onChange={(e) => setReplyTexts((prev) => ({ ...prev, [c.commentId]: e.target.value }))}
/>
<Button size="small" type="primary" onClick={() => handleReply(c.commentId)}></Button>
</div>
)}
</div>
))}
</div>
)}
</Modal>
</>
);
};
export default ProjectMgmt;

View File

@@ -0,0 +1,54 @@
import { request } from '@umijs/max';
// 查询项目列表
export async function getProjectList(params?: API.Project.ListParams) {
return request<API.Project.ProjectResult>('/api/cms/project/list', {
method: 'GET',
params,
});
}
// 获取项目详情
export async function getProjectDetail(id: number | string) {
return request<API.Project.ProjectDetailResult>(`/api/cms/project/${id}`, {
method: 'GET',
});
}
// 新增项目
export async function addProject(data: Partial<API.Project.ProjectItem>) {
return request<API.Result>('/api/cms/project', {
method: 'POST',
data,
});
}
// 修改项目
export async function updateProject(data: Partial<API.Project.ProjectItem>) {
return request<API.Result>('/api/cms/project', {
method: 'PUT',
data,
});
}
// 删除项目
export async function deleteProject(ids: string) {
return request<API.Result>(`/api/cms/project/${ids}`, {
method: 'DELETE',
});
}
// 获取项目评论列表
export async function getProjectComments(projectId: number | string) {
return request<API.Result>(`/api/cms/project/comments/${projectId}`, {
method: 'GET',
});
}
// 回复评论
export async function replyComment(data: { commentId: number; reply: string }) {
return request<API.Result>('/api/cms/project/comment/reply', {
method: 'PUT',
data,
});
}

44
src/types/cms/project.d.ts vendored Normal file
View File

@@ -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;
}
}