dev_1.2
tangjie 2026-07-21 18:03:20 +08:00
parent 305208d91b
commit 2f68c75e8e
13 changed files with 1415 additions and 286 deletions

View File

@ -10,9 +10,9 @@ module.exports = {
javaGitBranch: "dev",
// 本地联调 safetyEval-servicecontext-path: /safetyEval默认端口 8095
// 可通过环境变量覆盖: SAFETY_EVAL_API_HOST=http://192.168.x.x:8095
API_HOST: "https://gbs-gateway.qhdsafety.com",
//API_HOST: "https://gbs-gateway.qhdsafety.com",
//API_HOST: "http://192.168.0.150", //太浅
//API_HOST: "http://192.168.0.152",
API_HOST: "http://192.168.0.152",
},
production: {
// 应用后端分支名称,部署上线需要

View File

@ -1,18 +0,0 @@
import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
export const customerPage = declareRequest(
"customerLoading",
"Get > /safetyEval/customer/page",
'customerList: [] | res.data?.records || [] & customerTotal: 0 | res.data?.total || 0',
);
export const customerSave = declareRequest(
"customerSaveLoading",
"Post > @/safetyEval/customer/save",
);
export const customerDetail = declareRequest(
"customerDetailLoading",
"Get > /safetyEval/customer/detail/{id}",
'customerDetail: {} | res.data || {}',
);

View File

@ -16,4 +16,50 @@ export const createWpsDoc = declareRequest(
"createDocLoading",
"Post > @/safetyEval-h5/wsp/doc/create",
'wpsDoc: {} | res.data || {}',
);
export const customerPage = declareRequest(
"customerLoading",
"Get > /safetyEval/institution/eval-customer/page",
'customerPage: [] | res.data',
);
export const customerSave = declareRequest(
"customerSaveLoading",
"Post > @/safetyEval/institution/eval-customer/save",
);
export const customerModify = declareRequest(
"customerModifyLoading",
"Post > @/safetyEval/institution/eval-customer/modify",
);
export const customerDetail = declareRequest(
"customerDetailLoading",
"Get > /safetyEval/institution/eval-customer/get",
);
export const customerDelete = declareRequest(
"customerDeleteLoading",
"Post > @/safetyEval/institution/eval-customer/delete",
);
export const customerProjectPage = declareRequest(
"customerProjectLoading",
"Get > /safetyEval/institution/eval-customer/project/page",
);
export const evalProjectPage = declareRequest(
"evalProjectLoading",
"Get > /safetyEval/institution/eval-project/page",
);
export const evalProjectSave = declareRequest(
"evalProjectSaveLoading",
"Post > @/safetyEval/institution/eval-project/save",
);
export const evalProjectDetail = declareRequest(
"evalProjectDetailLoading",
"Get > /safetyEval/institution/eval-project/get",
);

View File

@ -8,6 +8,7 @@ const BaiduMapPicker = ({ visible, onCancel, onConfirm }) => {
const markerRef = useRef(null);
const [loaded, setLoaded] = useState(false);
const [selectedPoint, setSelectedPoint] = useState(null);
const [selectedAddress, setSelectedAddress] = useState('');
// 动态加载百度地图脚本
useEffect(() => {
@ -48,12 +49,21 @@ const BaiduMapPicker = ({ visible, onCancel, onConfirm }) => {
map.addEventListener('click', (e) => {
const point = e.point;
setSelectedPoint(point);
setSelectedAddress('');
if (markerRef.current) {
map.removeOverlay(markerRef.current);
}
const marker = new window.BMap.Marker(point);
map.addOverlay(marker);
markerRef.current = marker;
// 逆地理编码获取地址
const geocoder = new window.BMap.Geocoder();
geocoder.getLocation(point, (result) => {
if (result?.address) {
setSelectedAddress(result.address);
}
});
});
// 搜索自动完成
@ -69,6 +79,7 @@ const BaiduMapPicker = ({ visible, onCancel, onConfirm }) => {
const poi = results.getPoi(0);
const point = poi.point;
setSelectedPoint(point);
setSelectedAddress(poi.address || poi.title || '');
map.centerAndZoom(point, 15);
if (markerRef.current) {
map.removeOverlay(markerRef.current);
@ -96,6 +107,7 @@ const BaiduMapPicker = ({ visible, onCancel, onConfirm }) => {
if (!visible) {
setLoaded(false);
setSelectedPoint(null);
setSelectedAddress('');
markerRef.current = null;
mapInstanceRef.current = null;
}
@ -106,9 +118,11 @@ const BaiduMapPicker = ({ visible, onCancel, onConfirm }) => {
message.warning('请在地图上选择位置');
return;
}
onConfirm({
lng: selectedPoint.lng,
lat: selectedPoint.lat,
address: selectedAddress,
});
};

View File

@ -28,7 +28,6 @@ export const EXPERT_STATUS_MAP = {
pending: { label: "待核验", color: "blue" },
arranging: { label: "待安排", color: "warning" },
passed: { label: "已核验", color: "success" },
};
/** 公示状态 */
@ -48,10 +47,9 @@ export const FILING_STATUS_MAP = {
export const FILING_STATUS_MAP_CHANGE = {
1: { label: "已备案", color: "success" },
3: { label: "已打回", color: "error" },
4: { label: "变更审核中", color: "processing" },
};
/** 变更状态 */
@ -90,7 +88,7 @@ export const GENDER_MAP = {
/** 审核结果 */
export const REVIEW_RESULT_OPTIONS = [
{ label: "通过", value: 1 },
{ label: "不通过", value: 3 },
];
@ -143,26 +141,26 @@ export const CERT_TYPE_MAP = {
/** 风险预警中心 — 预警大类映射 */
export const RISK_TYPE_MAP = {
"资质预警": { label: "资质预警", color: "error" },
"项目预警": { label: "项目预警", color: "warning" },
资质预警: { label: "资质预警", color: "error" },
项目预警: { label: "项目预警", color: "warning" },
};
/** 风险预警中心 — 处理阶段映射 */
export const STAGE_MAP = {
"未处理": { label: "未处理", color: "error" },
"已发机构": { label: "已发机构", color: "warning" },
"待复核": { label: "待复核", color: "processing" },
"已闭环": { label: "已闭环", color: "success" },
未处理: { label: "未处理", color: "error" },
已发机构: { label: "已发机构", color: "warning" },
待复核: { label: "待复核", color: "processing" },
已闭环: { label: "已闭环", color: "success" },
};
/** 风险预警中心 — 机构端状态映射 */
export const ORG_STATUS_MAP = {
"未发送": { label: "未发送", color: "error" },
"机构未读": { label: "机构未读", color: "warning" },
"已提交材料": { label: "已提交材料", color: "processing" },
"处理中": { label: "处理中", color: "processing" },
"已提交说明": { label: "已提交说明", color: "processing" },
"整改完成": { label: "整改完成", color: "success" },
未发送: { label: "未发送", color: "error" },
机构未读: { label: "机构未读", color: "warning" },
已提交材料: { label: "已提交材料", color: "processing" },
处理中: { label: "处理中", color: "processing" },
已提交说明: { label: "已提交说明", color: "processing" },
整改完成: { label: "整改完成", color: "success" },
};
/** 风险预警中心 — 预警子类选项 */
@ -176,4 +174,143 @@ export const RISK_SUBTYPE_OPTIONS = [
{ label: "从业告知异常", value: "从业告知异常" },
{ label: "现场勘查异常", value: "现场勘查异常" },
{ label: "过程控制/归档异常", value: "过程控制/归档异常" },
];
export const district = [
{
code: "500101",
name: "万州区",
},
{
code: "500102",
name: "涪陵区",
},
{
code: "500103",
name: "渝中区",
},
{
code: "500104",
name: "大渡口区",
},
{
code: "500105",
name: "江北区",
},
{
code: "500106",
name: "沙坪坝区",
},
{
code: "500107",
name: "九龙坡区",
},
{
code: "500108",
name: "南岸区",
},
{
code: "500109",
name: "北碚区",
},
{
code: "500110",
name: "綦江区",
},
{
code: "500111",
name: "大足区",
},
{
code: "500112",
name: "渝北区",
},
{
code: "500113",
name: "巴南区",
},
{
code: "500114",
name: "黔江区",
},
{
code: "500115",
name: "长寿区",
},
{
code: "500116",
name: "江津区",
},
{
code: "500117",
name: "合川区",
},
{
code: "500118",
name: "永川区",
},
{
code: "500119",
name: "南川区",
},
{
code: "500120",
name: "璧山区",
},
{
code: "500151",
name: "铜梁区",
},
{
code: "500152",
name: "潼南区",
},
{
code: "500153",
name: "荣昌区",
},
{
code: "500154",
name: "开州区",
},
{
code: "500155",
name: "梁平区",
},
{
code: "500156",
name: "武隆区",
},
];
/** 企业状态1正常经营 2停产整改 3停业 4注销 */
export const ENTERPRISE_STATUS_OPTIONS = [
{ label: "正常经营", value: 1 },
{ label: "停产整改", value: 2 },
{ label: "停业", value: 3 },
{ label: "注销", value: 4 },
];
/** 企业规模LARGE 大型 MEDIUM 中型 SMALL 小型 MICRO 微型 */
export const ENTERPRISE_SCALE_OPTIONS = [
{ label: "大型", value: "LARGE" },
{ label: "中型", value: "MEDIUM" },
{ label: "小型", value: "SMALL" },
{ label: "微型", value: "MICRO" },
];
/** 评价类型选项 */
export const EVAL_TYPE_OPTIONS = [
{ label: "安全预评价", value: "PRE" },
{ label: "安全设施竣工验收评价", value: "ACCEPT" },
{ label: "安全现状评价", value: "STATUS" },
];
/** 登记半径选项 */
export const CHECKIN_RADIUS_OPTIONS = [
{ label: "100 米", value: 100 },
{ label: "200 米", value: 200 },
{ label: "300 米", value: 300 },
{ label: "500 米", value: 500 },
];

View File

@ -154,6 +154,12 @@ export const PROFESSIONAL_LEVEL_OPTIONS = [
},
];
export const PROFESSIONAL_LEVEL_OPTIONS_MAP =
PROFESSIONAL_LEVEL_OPTIONS.reduce(
(acc, cur) => ({ ...acc, [cur.value]: cur.label }),
{},
);
/** 学历类型 */
export const EDUCATION_TYPE_OPTIONS = [
{ label: "全日制", value: "全日制" },

View File

@ -23,4 +23,3 @@ export const NS_QUAL_EXPERT = defineNamespace("qualExpert");
export const NS_DRIVER = defineNamespace("driver");
export const NS_RISK_CENTER = defineNamespace("riskCenter");
export const NS_SAFETY_EVAL_BUSINESS = defineNamespace("safetyEvalBusiness");
export const NS_CUSTOMER = defineNamespace("customer");

View File

@ -206,6 +206,11 @@ const menuItems = [
label: "我的项目",
icon: <FileTextOutlined />,
},
{
key: "/safetyEval/container/SafetyEvalBusiness/EvalProject/List",
label: "安评项目管理",
icon: <FileTextOutlined />,
},
{
key: "/safetyEval/container/SafetyEvalBusiness/CustomerManage",
label: "安评客户管理",

View File

@ -1,150 +1,211 @@
import React, { useState, useCallback } from "react";
import { Form, Table, Button, Modal, Input, Select, Row, Col, message, Descriptions } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import React, { useState, useCallback, useEffect } from "react";
import {
Form,
Table,
Button,
Modal,
Input,
Select,
Row,
Col,
message,
Descriptions,
Flex,
Space,
} from "antd";
import {
PlusOutlined,
} from "@ant-design/icons";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm";
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { NS_CUSTOMER } from "~/enumerate/namespace";
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import BaiduMapPicker from "~/components/BaiduMapPicker";
import { tools } from "@cqsjjb/jjb-common-lib";
import {
district,
ENTERPRISE_STATUS_OPTIONS,
ENTERPRISE_SCALE_OPTIONS,
} from "~/enumerate/constant";
import { QUALIFICATION_INDUSTRY_OPTIONS } from "~/enumerate/enterpriseOptions";
import { phoneRule, creditCodeRule } from "~/utils/validators";
const MOCK_LIST = [
{
id: 1, name: "重庆华安安全科技有限公司", contact: "周经理", phone: "138****5621",
region: "渝北区", projectCount: 6,
creditCode: "91500112MA5UQ001", industry: "化工生产", address: "重庆市渝北区化工园区东路18号",
status: "正常经营", location: "重庆市渝北区 · 东经106.63° / 北纬29.72°",
principal: "周海峰", principalPhone: "138****5621", legalPerson: "周海峰", legalPhone: "138****5621", scale: "中型企业",
},
{
id: 2, name: "重庆鼎信安全咨询有限公司", contact: "刘总", phone: "139****1120",
region: "南岸区", projectCount: 4,
creditCode: "91500108MA61A1120", industry: "建筑施工", address: "重庆市南岸区茶园新区通江大道96号",
status: "正常经营", location: "重庆市南岸区 · 东经106.66° / 北纬29.48°",
principal: "刘志强", principalPhone: "139****1120", legalPerson: "刘志强", legalPhone: "139****1120", scale: "小型企业",
},
{
id: 3, name: "重庆安环检测技术有限公司", contact: "陈主任", phone: "136****9088",
region: "江北区", projectCount: 9,
creditCode: "91500105MA60K9088", industry: "其他工贸", address: "重庆市江北区港城工业园A区",
status: "正常经营", location: "重庆市江北区 · 东经106.65° / 北纬29.63°",
principal: "陈海林", principalPhone: "136****9088", legalPerson: "陈海林", legalPhone: "136****9088", scale: "中型企业",
},
{
id: 4, name: "重庆环宇投资有限公司", contact: "罗经理", phone: "135****8710",
region: "沙坪坝区", projectCount: 3,
creditCode: "91500106MA60K9099", industry: "危险化学品经营", address: "重庆市沙坪坝区三峡广场88号",
status: "正常经营", location: "重庆市沙坪坝区 · 东经106.46° / 北纬29.56°",
principal: "罗建国", principalPhone: "135****8710", legalPerson: "罗建国", legalPhone: "135****8710", scale: "大型企业",
},
];
const PLATFORM_ENTERPRISES = [
{ name: "重庆华安安全科技有限公司", code: "91500112MA5UQ001", region: "渝北区", industry: "化工生产", status: "正常经营", principal: "周海峰", legal: "周海峰" },
{ name: "重庆鼎信安全咨询有限公司", code: "91500108MA61A1120", region: "南岸区", industry: "建筑施工", status: "正常经营", principal: "刘志强", legal: "刘志强" },
{ name: "重庆安环检测技术有限公司", code: "91500105MA60K9088", region: "江北区", industry: "其他工贸", status: "正常经营", principal: "陈海林", legal: "陈海林" },
];
const REGION_OPTIONS = ["渝北区", "南岸区", "江北区", "沙坪坝区", "九龙坡区", "北碚区", "巴南区"];
const INDUSTRY_OPTIONS = ["化工生产", "危险化学品经营", "金属非金属矿山", "金属冶炼", "建筑施工", "加油站", "其他工贸"];
const STATUS_OPTIONS = ["正常经营", "停产整改", "停业", "注销"];
const SCALE_OPTIONS = ["大型企业", "中型企业", "小型企业", "微型企业"];
const { router } = tools;
const CustomerManage = (props) => {
const [searchForm] = Form.useForm();
const [modalForm] = Form.useForm();
const [dataSource, setDataSource] = useState(MOCK_LIST);
const [dataSource, setDataSource] = useState([]);
const [total, setTotal] = useState(0);
const [mapPickerVisible, setMapPickerVisible] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [detailOpen, setDetailOpen] = useState(false);
const [currentDetail, setCurrentDetail] = useState(null);
const [createMode, setCreateMode] = useState("platform"); // "platform" | "new"
const [selectedPlatform, setSelectedPlatform] = useState(null);
const [editingId, setEditingId] = useState(null);
const {customerLoading, customerModifyLoading, customerSaveLoading}= props.safetyEvalBusiness;
const handleSearch = useCallback((values) => {
const { name, contact, region } = values || {};
const filtered = MOCK_LIST.filter((item) => {
if (name && !item.name.includes(name)) return false;
if (contact && !item.contact.includes(contact)) return false;
if (region && item.region !== region) return false;
return true;
});
setDataSource(filtered);
const getData = async (pagination) => {
const params = {
...router.query,
current: pagination?.current || router.query.current || 1,
size: pagination?.size || router.query.size || 10,
};
const res = await props.customerPage(params);
if (res?.success !== false) {
setDataSource(res?.data || []);
setTotal(res?.total || 0);
}
};
const handleMapConfirm = (location) => {
const { lng, lat, address } = location;
modalForm.setFieldsValue({ longitude: lng, latitude: lat, businessAddress: address });
setMapPickerVisible(false);
};
useEffect(() => {
searchForm.setFieldsValue(router.query);
getData();
}, []);
const handleReset = useCallback(() => {
searchForm.resetFields();
setDataSource(MOCK_LIST);
}, [searchForm]);
const handleSearch = (values) => {
router.query = { ...router.query, ...values, current: 1, size: 10 };
getData();
};
const handleOpenCreate = useCallback(() => {
setCreateMode("platform");
setSelectedPlatform(null);
const handleReset = (values) => {
searchForm.resetFields();
router.query = { ...values, current: 1, size: 10 };
getData();
};
const handlePageChange = (pagination) => {
router.query = {
...router.query,
current: pagination.current,
size: pagination.pageSize,
};
getData(pagination);
};
const handleOpenCreate = () => {
setEditingId(null);
setCurrentDetail(null);
modalForm.resetFields();
setModalOpen(true);
}, [modalForm]);
};
const handlePlatformSelect = useCallback((name) => {
const ep = PLATFORM_ENTERPRISES.find((e) => e.name === name);
if (ep) {
setSelectedPlatform(ep);
modalForm.setFieldsValue({
creditCode: ep.code,
region: ep.region,
industry: ep.industry,
status: ep.status,
principal: ep.principal,
legalPerson: ep.legal,
});
}
}, [modalForm]);
const handleOpenEdit = async (record) => {
const res = await props.customerDetail({ id: record.id });
if (res?.success !== false) {
setEditingId(record.id);
setCurrentDetail(res?.data || {});
modalForm.setFieldsValue(res?.data || {});
setModalOpen(true);
}
};
const handleCreateSubmit = useCallback(() => {
modalForm.validateFields().then((values) => {
const newCustomer = {
id: dataSource.length + 1,
name: createMode === "platform" ? values.platformEnterprise : values.name,
contact: values.contact || "待完善",
phone: values.phone || "—",
region: values.region || "待完善",
projectCount: 0,
creditCode: values.creditCode || "",
industry: values.industry || "",
address: values.address || "",
status: values.status || "正常经营",
location: values.location || "",
principal: values.principal || "",
principalPhone: values.phone || "",
legalPerson: values.legalPerson || "",
legalPhone: values.legalPhone || "",
scale: values.scale || "",
};
setDataSource((prev) => [newCustomer, ...prev]);
setModalOpen(false);
message.success("安评客户已新建");
}).catch(() => {});
}, [modalForm, dataSource.length, createMode]);
const handleViewDetail = async (record) => {
const res = await props.customerDetail({ id: record.id });
if (res?.success !== false) {
setCurrentDetail(res?.data || {});
setDetailOpen(true);
}
};
const handleViewDetail = useCallback((record) => {
setCurrentDetail(record);
setDetailOpen(true);
}, []);
const handleDelete = async (record) => {
Modal.confirm({
title: "确认删除",
content: "确定要删除这个客户吗?",
onOk: async () => {
await props.customerDelete({ data: record.id });
message.success("删除成功");
getData();
},
});
};
const handleCreateSubmit = () => {
modalForm.validateFields().then(async (values) => {
if (editingId) {
await props.customerModify({ ...values, id: editingId });
} else {
await props.customerSave(values);
}
message.success(editingId ? "修改成功" : "创建成功");
setModalOpen(false);
getData();
});
};
const columns = [
{ title: "序号", width: 60, render: (_, __, index) => index + 1 },
{ title: "客户名称", dataIndex: "name", ellipsis: true },
{ title: "客户联系人", dataIndex: "contact", width: 120 },
{ title: "联系人电话", dataIndex: "phone", width: 130 },
{ title: "属地", dataIndex: "region", width: 100 },
{ title: "客户名称", dataIndex: "customerName", ellipsis: true, width: 220 },
{ title: "统一社会信用代码", dataIndex: "creditCode", width: 220 },
{ title: "客户联系人", dataIndex: "principalName", width: 150 },
{ title: "联系人电话", dataIndex: "principalPhone", width: 160 },
{
title: "服务项目数", dataIndex: "projectCount", width: 110,
title: "属地",
dataIndex: "districtName",
width: 150,
},
{
title: "企业状态",
dataIndex: "enterpriseStatusCode",
width: 100,
render: (status) => {
const statusOption = ENTERPRISE_STATUS_OPTIONS.find(
(opt) => opt.value === status,
);
return statusOption?.label || status;
},
},
{
title: "服务项目数",
dataIndex: "projectCount",
width: 110,
render: (count) => (
<Button type="link" size="small" style={{ padding: 0 }}>{count}</Button>
<Button type="link" size="small" style={{ padding: 0 }}>
{count || 0}
</Button>
),
},
{
title: "操作", width: 80, fixed: "right",
title: "操作",
width: 260,
fixed: "right",
render: (_, record) => (
<Button type="link" size="small" onClick={() => handleViewDetail(record)}>查看</Button>
<Space>
<Button
type="link"
size="small"
onClick={() => handleViewDetail(record)}
>
查看
</Button>
<Button
type="link"
size="small"
onClick={() => handleOpenEdit(record)}
>
编辑
</Button>
<Button
type="link"
size="small"
danger
onClick={() => handleDelete(record)}
>
删除
</Button>
</Space>
),
},
];
@ -153,7 +214,11 @@ const CustomerManage = (props) => {
<PageLayout
title="安评客户管理"
extra={
<Button type="primary" icon={<PlusOutlined />} onClick={handleOpenCreate}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleOpenCreate}
>
新建客户
</Button>
}
@ -164,18 +229,33 @@ const CustomerManage = (props) => {
<SearchForm
form={searchForm}
loading={false}
loading={customerLoading}
formLine={[
<Form.Item key="name" name="name">
<ControlWrapper.Input label="客户名称" placeholder="关键字查询" allowClear />
<Form.Item key="customerName" name="customerName">
<ControlWrapper.Input
label="客户名称"
placeholder="关键字查询"
allowClear
/>
</Form.Item>,
<Form.Item key="contact" name="contact">
<ControlWrapper.Input label="客户联系人" placeholder="关键字查询" allowClear />
<Form.Item key="principalName" name="principalName">
<ControlWrapper.Input
label="主要负责人"
placeholder="关键字查询"
allowClear
/>
</Form.Item>,
<Form.Item key="region" name="region">
<ControlWrapper.Select label="属地" placeholder="全部" allowClear style={{ width: "100%" }}>
{REGION_OPTIONS.map((r) => (
<Select.Option key={r} value={r}>{r}</Select.Option>
<Form.Item key="districtCode" name="districtCode">
<ControlWrapper.Select
label="属地"
placeholder="全部"
allowClear
style={{ width: "100%" }}
>
{district.map((opt) => (
<Select.Option key={opt.code} value={opt.code}>
{opt.name}
</Select.Option>
))}
</ControlWrapper.Select>
</Form.Item>,
@ -185,86 +265,104 @@ const CustomerManage = (props) => {
style={{ marginBottom: 16 }}
/>
<div style={{ fontSize: 13, color: "#999", marginBottom: 12 }}>
服务项目数可进入该客户的评价项目列表操作栏仅查看客户详情
</div>
<Table
rowKey="id"
columns={columns}
dataSource={dataSource}
loading={false}
scroll={{ y: props.scrollY }}
loading={customerLoading}
scroll={{ y: props.scrollY, x: 1200 }}
pagination={{
total: dataSource.length,
total,
current: router.query.current || 1,
pageSize: router.query.size || 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
pageSize: 10,
}}
onChange={handlePageChange}
/>
{/* 新建客户弹窗 */}
{/* 新建/编辑客户弹窗 */}
<Modal
title="新建客户"
title={editingId ? "编辑客户" : "新建客户"}
open={modalOpen}
onCancel={() => setModalOpen(false)}
onOk={handleCreateSubmit}
confirmLoading={editingId ? customerModifyLoading : customerSaveLoading}
width={800}
destroyOnHidden
>
<div style={{ fontSize: 13, color: "#666", marginBottom: 16 }}>
优先从平台企业库选择平台没有时可新建客户信息
</div>
<Form form={modalForm} layout="vertical">
<Form.Item label="建档方式">
<Select
value={createMode}
onChange={(v) => { setCreateMode(v); setSelectedPlatform(null); modalForm.resetFields(); }}
>
<Select.Option value="platform">选择平台已有企业</Select.Option>
<Select.Option value="new">新建客户信息</Select.Option>
</Select>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="customerName"
label="客户名称"
rules={[{ required: true, message: "请输入客户名称" }]}
>
<Input placeholder="请输入" maxLength={100} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="creditCode" label="统一社会信用代码" rules={[creditCodeRule(false)]}>
<Input placeholder="请输入" maxLength={18} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="districtCode" label="属地">
<Select placeholder="请选择" allowClear>
{district.map((opt) => (
<Select.Option key={opt.code} value={opt.code}>
{opt.name}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="industryCode" label="所属行业">
<Select placeholder="请选择" allowClear>
{QUALIFICATION_INDUSTRY_OPTIONS.map((opt) => (
<Select.Option key={opt.value} value={opt.value}>
{opt.label}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item label="经营地址">
<Flex>
<Form.Item name="businessAddress" noStyle>
<Input placeholder="请输入经营地址" disabled maxLength={200} />
</Form.Item>
<Button onClick={() => setMapPickerVisible(true)}>选择</Button>
</Flex>
</Form.Item>
{createMode === "platform" ? (
<Form.Item
name="platformEnterprise"
label="平台企业"
rules={[{ required: true, message: "请选择平台企业" }]}
>
<Select
placeholder="请选择平台企业"
showSearch
optionFilterProp="children"
onChange={handlePlatformSelect}
>
{PLATFORM_ENTERPRISES.map((ep) => (
<Select.Option key={ep.name} value={ep.name}>{ep.name}</Select.Option>
))}
</Select>
</Form.Item>
) : (
<Form.Item
name="name"
label="客户名称"
rules={[{ required: true, message: "请输入客户名称" }]}
>
<Input placeholder="输入客户名称" />
</Form.Item>
)}
<Row gutter={16}>
<Col span={12}>
<Form.Item name="creditCode" label="统一社会信用代码">
<Input placeholder="请输入" />
<Form.Item name="enterpriseStatusCode" label="企业状态">
<Select placeholder="请选择" allowClear>
{ENTERPRISE_STATUS_OPTIONS.map((opt) => (
<Select.Option key={opt.value} value={opt.value}>
{opt.label}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="region" label="属地">
<Form.Item name="enterpriseScaleCode" label="企业规模">
<Select placeholder="请选择" allowClear>
{REGION_OPTIONS.map((r) => (
<Select.Option key={r} value={r}>{r}</Select.Option>
{ENTERPRISE_SCALE_OPTIONS.map((opt) => (
<Select.Option key={opt.value} value={opt.value}>
{opt.label}
</Select.Option>
))}
</Select>
</Form.Item>
@ -273,78 +371,36 @@ const CustomerManage = (props) => {
<Row gutter={16}>
<Col span={12}>
<Form.Item name="industry" label="所属行业">
<Select placeholder="请选择" allowClear>
{INDUSTRY_OPTIONS.map((r) => (
<Select.Option key={r} value={r}>{r}</Select.Option>
))}
</Select>
<Form.Item name="principalName" label="主要负责人">
<Input placeholder="请输入" maxLength={50} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="status" label="企业状态">
<Select placeholder="请选择" allowClear>
{STATUS_OPTIONS.map((r) => (
<Select.Option key={r} value={r}>{r}</Select.Option>
))}
</Select>
<Form.Item name="principalPhone" label="负责人电话" rules={[phoneRule("负责人电话", false)]}>
<Input placeholder="请输入" maxLength={20} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="address" label="经营地址">
<Input placeholder="请输入经营地址" />
<Form.Item name="legalRepresentative" label="法定代表人">
<Input placeholder="请输入" maxLength={50} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="location" label="地理位置">
<Input placeholder="定位后自动生成" />
<Form.Item name="legalRepresentativePhone" label="法人电话" rules={[phoneRule("法人电话", false)]}>
<Input placeholder="请输入" maxLength={20} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="contact" label="客户联系人">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item name="longitude" label="经度" noStyle></Form.Item>
</Col>
<Col span={12}>
<Form.Item name="phone" label="联系人电话">
<Input placeholder="请输入" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="principal" label="主要负责人">
<Input placeholder="请输入" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="legalPerson" label="法定代表人">
<Input placeholder="请输入" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="legalPhone" label="法人电话">
<Input placeholder="请输入" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="scale" label="企业规模">
<Select placeholder="请选择" allowClear>
{SCALE_OPTIONS.map((r) => (
<Select.Option key={r} value={r}>{r}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item name="latitude" label="纬度" noStyle></Form.Item>
</Col>
</Row>
</Form>
@ -352,7 +408,7 @@ const CustomerManage = (props) => {
{/* 客户详情弹窗 */}
<Modal
title={`客户详情 - ${currentDetail?.name || ""}`}
title={`客户详情 - ${currentDetail?.customerName || ""}`}
open={detailOpen}
onCancel={() => setDetailOpen(false)}
footer={<Button onClick={() => setDetailOpen(false)}>关闭</Button>}
@ -361,48 +417,127 @@ const CustomerManage = (props) => {
>
{currentDetail && (
<>
<Descriptions column={2} bordered size="small" style={{ marginBottom: 16 }}>
<Descriptions.Item label="客户名称">{currentDetail.name}</Descriptions.Item>
<Descriptions.Item label="企业状态">
{currentDetail.status}
<Descriptions
column={2}
bordered
size="small"
style={{ marginBottom: 16 }}
>
<Descriptions.Item label="客户名称">
{currentDetail.customerName}
</Descriptions.Item>
<Descriptions.Item label="统一社会信用代码">
{currentDetail.creditCode}
</Descriptions.Item>
<Descriptions.Item label="企业状态">
{(() => {
const statusOption = ENTERPRISE_STATUS_OPTIONS.find(
(opt) => opt.value === currentDetail.enterpriseStatusCode,
);
return (
statusOption?.label || currentDetail.enterpriseStatusName
);
})()}
</Descriptions.Item>
<Descriptions.Item label="企业规模">
{(() => {
const scaleOption = ENTERPRISE_SCALE_OPTIONS.find(
(opt) => opt.value === currentDetail.enterpriseScaleCode,
);
return (
scaleOption?.label || currentDetail.enterpriseScaleName
);
})()}
</Descriptions.Item>
<Descriptions.Item label="所属行业">
{currentDetail.industryName || currentDetail.industryCode}
</Descriptions.Item>
<Descriptions.Item label="属地">
{currentDetail.districtName}
</Descriptions.Item>
<Descriptions.Item label="联系人">
{currentDetail.principalName}
</Descriptions.Item>
<Descriptions.Item label="联系电话">
{currentDetail.principalPhone}
</Descriptions.Item>
<Descriptions.Item label="主要负责人">
{currentDetail.principalName}
</Descriptions.Item>
<Descriptions.Item label="负责人电话">
{currentDetail.principalPhone}
</Descriptions.Item>
<Descriptions.Item label="法定代表人">
{currentDetail.legalRepresentative}
</Descriptions.Item>
<Descriptions.Item label="法人电话">
{currentDetail.legalRepresentativePhone}
</Descriptions.Item>
<Descriptions.Item label="经营地址" span={2}>
{currentDetail.businessAddress}
</Descriptions.Item>
<Descriptions.Item label="经度">
{currentDetail.longitude}
</Descriptions.Item>
<Descriptions.Item label="纬度">
{currentDetail.latitude}
</Descriptions.Item>
<Descriptions.Item label="统一社会信用代码">{currentDetail.creditCode}</Descriptions.Item>
<Descriptions.Item label="所属区域">重庆市</Descriptions.Item>
<Descriptions.Item label="联系人">{currentDetail.contact}</Descriptions.Item>
<Descriptions.Item label="联系电话">{currentDetail.phone}</Descriptions.Item>
<Descriptions.Item label="经营地址" span={2}>{currentDetail.address}</Descriptions.Item>
</Descriptions>
<h4 style={{ margin: "12px 0 8px" }}>安全评价项目列表</h4>
<Table
rowKey="id"
dataSource={[
{ id: 1, name: `${currentDetail.name}安全现状评价`, type: "安全现状评价", date: "2026-06-15", status: "已完成" },
{ id: 2, name: `${currentDetail.name}安全验收评价`, type: "安全验收评价", date: "2023-07-20", status: "已归档" },
{ id: 3, name: `${currentDetail.name}安全预评价`, type: "安全预评价", date: "2020-08-12", status: "已归档" },
{
id: 1,
name: `${currentDetail.customerName}安全现状评价`,
type: "安全现状评价",
date: "2026-06-15",
status: "已完成",
},
{
id: 2,
name: `${currentDetail.customerName}安全验收评价`,
type: "安全验收评价",
date: "2023-07-20",
status: "已归档",
},
{
id: 3,
name: `${currentDetail.customerName}安全预评价`,
type: "安全预评价",
date: "2020-08-12",
status: "已归档",
},
]}
columns={[
{ title: "项目名称", dataIndex: "name" },
{ title: "评价类别", dataIndex: "type", width: 120 },
{ title: "报告完成日期", dataIndex: "date", width: 130 },
{
title: "项目状态", dataIndex: "status", width: 100,
title: "项目状态",
dataIndex: "status",
width: 100,
fixed: "right",
render: (s) => <span style={{ color: "#52c41a" }}>{s}</span>,
},
]}
pagination={false}
scroll={{ x: 1200,y: props.scrollY }}
size="small"
/>
</>
)}
</Modal>
<BaiduMapPicker
visible={mapPickerVisible}
onCancel={() => setMapPickerVisible(false)}
onConfirm={handleMapConfirm}
/>
</PageLayout>
);
};
export default Connect(
[NS_CUSTOMER],
[NS_SAFETY_EVAL_BUSINESS],
true,
)(AntdTableFuncControl(CustomerManage));
)(AntdTableFuncControl(CustomerManage));

View File

@ -0,0 +1,269 @@
import React, { useState, useEffect } from "react";
import {
Modal,
TreeSelect,
Input,
Table,
Button,
Tag,
Row,
Col,
Space,
} from "antd";
import { CloseOutlined } from "@ant-design/icons";
import { NS_STAFF_INFO, NS_ORG_DEPARTMENT } from "~/enumerate/namespace";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { QUALIFICATION_INDUSTRY_OPTIONS_MAP, PROFESSIONAL_LEVEL_OPTIONS_MAP, TITLE_LEVEL_MAP } from "~/enumerate/enterpriseOptions";
const StaffPickerModal = (props) => {
const { open, onCancel, onConfirm, selectedIds = [] } = props;
const { staffInfoList: dataSource, staffInfoTotal: total, staffInfoLoading: loading } = props.staffInfo || {};
const { orgDepartmentTreeData } = props.orgDepartment || {};
const [deptId, setDeptId] = useState(undefined);
const [keyword, setKeyword] = useState("");
const [page, setPage] = useState({ current: 1, size: 10 });
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
// 用 Map 存储所有已选行,翻页后不丢失
const [selectedRowMap, setSelectedRowMap] = useState({});
useEffect(() => {
if (open) {
props.orgDepartmentTree();
// 从父组件恢复已选数据
const initialIds = selectedIds;
const initialRows = props.selectedParticipants || [];
setSelectedRowKeys(initialIds);
const map = {};
initialRows.forEach((r) => { map[r.id] = r; });
setSelectedRowMap(map);
fetchData({ current: 1, size: 10 });
}
}, [open]);
const fetchData = (pagination) => {
const params = {
current: pagination?.current || page.current,
size: pagination?.size || page.size,
employmentStatusCode: 1,
};
if (deptId) params.deptId = deptId;
if (keyword) params.userName = keyword;
props.staffInfoList(params);
setPage(params);
};
const handleSearch = () => {
fetchData({ current: 1, size: 10 });
};
const handleTableChange = (pagination) => {
fetchData(pagination);
};
const handleSelectChange = (rowKeys, rows) => {
setSelectedRowKeys(rowKeys);
// rows 仅包含当前页的行,用 Map 合并新旧数据
setSelectedRowMap((prev) => {
const next = { ...prev };
// 移除取消勾选的项
Object.keys(next).forEach((key) => {
if (!rowKeys.includes(key)) {
delete next[key];
}
});
// 添加/更新当前页勾选的行
rows.forEach((row) => {
next[row.id] = row;
});
return next;
});
};
const handleRemoveSelected = (record) => {
const newKeys = selectedRowKeys.filter((id) => id !== record.id);
setSelectedRowKeys(newKeys);
setSelectedRowMap((prev) => {
const next = { ...prev };
delete next[record.id];
return next;
});
};
const handleConfirm = () => {
onConfirm(Object.values(selectedRowMap));
};
const selectedRows = Object.values(selectedRowMap);
const columns = [
{
title: "用户名称",
dataIndex: "userName",
width: 200,
render: (text, record) => {
const name = record.userName || "-";
return (
<Space size={4} wrap align="start">
<span style={{ maxWidth: 100, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", display: "inline-block" }} title={name}>{name}</span>
{record.technicalDirectorFlag && (
<Tag color="blue" style={{ marginRight: 0, fontSize: 11 }}>技术</Tag>
)}
{record.processControlLeaderFlag && (
<Tag color="purple" style={{ marginRight: 0, fontSize: 11 }}>过程</Tag>
)}
</Space>
);
},
},
{
title: "资质范围",
dataIndex: "qualScope",
ellipsis: true,
render: (code) => {
const status = QUALIFICATION_INDUSTRY_OPTIONS_MAP[code];
return status || "-";
},
},
{
title: "职业等级",
dataIndex: "professionalLevelCode",
ellipsis: true,
render: (code) => {
const status = PROFESSIONAL_LEVEL_OPTIONS_MAP[code];
return status || "-";
},
},
{
title: "职称",
dataIndex: "titleCode",
width: 80,
ellipsis: true,
render: (code) => {
const status = TITLE_LEVEL_MAP[code];
return status || "-";
},
},
];
return (
<Modal
title="选择项目参与人员"
open={open}
onCancel={onCancel}
onOk={handleConfirm}
width={1000}
destroyOnHidden
>
<Row gutter={16}>
<Col span={18}>
<Space style={{ marginBottom: 12, width: "100%" }} size={12}>
<TreeSelect
placeholder="请选择部门"
allowClear
treeData={orgDepartmentTreeData}
fieldNames={{ label: "deptName", value: "id", key: "id" }}
treeDefaultExpandAll
showSearch
style={{ width: 240 }}
value={deptId}
onChange={(value) => {
setDeptId(value);
setTimeout(() => handleSearch(), 0);
}}
/>
<Input
placeholder="搜索用户名称"
allowClear
style={{ width: 240 }}
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onPressEnter={handleSearch}
/>
<Button type="primary" onClick={handleSearch}>搜索</Button>
</Space>
<div style={{ marginBottom: 8, fontSize: 12, color: "#666" }}>
<Tag color="blue" style={{ fontSize: 11, marginRight: 4 }}>技术</Tag>
<Tag color="purple" style={{ fontSize: 11, margin: "0 4px 0 12px" }}>过程</Tag>
</div>
<Table
rowKey="id"
columns={columns}
dataSource={dataSource}
loading={loading}
size="small"
rowSelection={{
selectedRowKeys,
preserveSelectedRowKeys: true,
onChange: handleSelectChange,
}}
pagination={{
total,
current: page.current,
pageSize: page.size,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `${t}`,
onChange: (cur, size) => handleTableChange({ current: cur, size }),
}}
scroll={{ y: 400, x: 600 }}
/>
</Col>
<Col span={6}>
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 8 }}>
已选人员{selectedRows.length}
</div>
<div
style={{
border: "1px solid #d9d9d9",
borderRadius: 6,
padding: 8,
minHeight: 300,
maxHeight: 460,
overflow: "auto",
}}
>
{selectedRows.length === 0 && (
<div style={{ color: "#999", fontSize: 13, textAlign: "center", paddingTop: 120 }}>
请从左侧勾选人员
</div>
)}
{selectedRows.map((item) => (
<div
key={item.id}
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "6px 8px",
borderBottom: "1px solid #f0f0f0",
}}
>
<div style={{ fontSize: 13, maxWidth: 140, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={item.userName}>{item.userName}</div>
<Button
type="text"
size="small"
danger
icon={<CloseOutlined />}
onClick={() => handleRemoveSelected(item)}
/>
</div>
))}
</div>
</Col>
</Row>
</Modal>
);
};
export default Connect(
[NS_STAFF_INFO, NS_ORG_DEPARTMENT],
true,
)(StaffPickerModal);

View File

@ -0,0 +1,340 @@
import React, { useState, useEffect } from "react";
import {
Form,
Button,
Input,
Select,
Row,
Col,
Card,
message,
DatePicker,
Flex,
Tag,
} from "antd";
import dayjs from "dayjs";
import {tools} from "@cqsjjb/jjb-common-lib";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import BaiduMapPicker from "~/components/BaiduMapPicker";
import { district } from "~/enumerate/constant";
import { phoneRule } from "~/utils/validators";
import StaffPickerModal from "./StaffPickerModal";
import {
EVAL_TYPE_OPTIONS,
CHECKIN_RADIUS_OPTIONS,
} from "~/enumerate/constant";
const { router } = tools;
const EvalProjectCreate = (props) => {
const [form] = Form.useForm();
const [mapPickerVisible, setMapPickerVisible] = useState(false);
const [staffPickerVisible, setStaffPickerVisible] = useState(false);
const [selectedParticipants, setSelectedParticipants] = useState([]);
const [detailLoading, setDetailLoading] = useState(false);
const projectId = router.query?.id;
const isView = projectId;
const { evalProjectSaveLoading, evalProjectDetailLoading, customerPage: customerList } = props.safetyEvalBusiness;
useEffect(() => {
props.customerPage({ current: 1, size: 999 });
}, []);
useEffect(() => {
if (!projectId) return;
(async () => {
setDetailLoading(true);
try {
const res = await props.evalProjectDetail({ id: projectId });
if (res?.success !== false && res?.data) {
const data = res.data;
form.setFieldsValue({
...data,
planStartDate: data.planStartDate ? dayjs(data.planStartDate) : undefined,
planEndDate: data.planEndDate ? dayjs(data.planEndDate) : undefined,
});
if (data.participants?.length) {
setSelectedParticipants(
data.participants.map((p) => ({ id: p.id, userName: p.name, ...p })),
);
}
}
} finally {
setDetailLoading(false);
}
})();
}, [projectId]);
const handleMapConfirm = (location) => {
const { lng, lat, address } = location;
form.setFieldsValue({
longitude: lng,
latitude: lat,
customerAddress: address,
});
setMapPickerVisible(false);
};
const handleSubmit = () => {
form.validateFields().then(async (values) => {
const { planStartDate, planEndDate, ...rest } = values;
const params = {
...rest,
planStartDate: planStartDate?.format("YYYY-MM-DD"),
planEndDate: planEndDate?.format("YYYY-MM-DD"),
participants: selectedParticipants.map((item) => ({
id: item.id,
name: item.userName,
})),
};
if (isView) {
params.id = projectId;
await props.evalProjectSave(params);
message.success("修改成功");
} else {
await props.evalProjectSave(params);
message.success("创建成功");
}
props.history.goBack();
});
};
const handleStaffConfirm = (rows) => {
setSelectedParticipants(rows);
setStaffPickerVisible(false);
};
return (
<PageLayout
title={isView ? "查看安全评价项目" : "新建安全评价项目"}
history={props.history}
previous
footer={
!isView && <Flex gap={24}>
<Button onClick={() => props.history.goBack()}>取消</Button>
<Button
type="primary"
onClick={handleSubmit}
loading={detailLoading || evalProjectSaveLoading}
>
提交并返回项目列表
</Button>
</Flex>
}
loading={detailLoading}
>
<Form form={form} layout="vertical" disabled={isView}>
<Card title="项目基本信息" style={{ marginBottom: 24 }}>
<Row gutter={24}>
<Col span={12}>
<Form.Item
name="projectName"
label="项目名称"
rules={[{ required: true, message: "请输入项目名称" }]}
>
<Input placeholder="请输入项目名称" maxLength={100} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="项目编号" name="projectNo">
<Input placeholder="提交后自动生成" disabled />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item
name="evalTypeCode"
label="评价类型"
rules={[{ required: true, message: "请选择评价类型" }]}
>
<Select placeholder="请选择" allowClear>
{EVAL_TYPE_OPTIONS.map((opt) => (
<Select.Option key={opt.value} value={opt.value}>
{opt.label}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="customerId"
label="被评价客户"
rules={[{ required: true, message: "请选择被评价客户" }]}
>
<Select
placeholder="请选择"
allowClear
showSearch
optionFilterProp="label"
onChange={(value, option) => {
const customer = customerList?.find((item) => item.id === value);
console.log(customer);
form.setFieldsValue({
customerContactName: customer?.principalName || "",
customerContactPhone: customer?.principalPhone || "",
});
}}
>
{(customerList || []).map((item) => (
<Select.Option key={item.id} value={item.id}>
{item.customerName}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item name="customerContactName" label="客户负责人">
<Input placeholder="请输入客户负责人" maxLength={50} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="customerContactPhone"
label="联系电话"
rules={[phoneRule("联系电话", false)]}
>
<Input placeholder="请输入客户负责人联系电话" maxLength={20} />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item name="districtCode" label="项目所在地">
<Select
placeholder="请选择"
allowClear
showSearch
optionFilterProp="label"
>
{district.map((opt) => (
<Select.Option key={opt.code} value={opt.code}>
{opt.name}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="projectLeaderName" label="项目负责人">
<Input placeholder="请输入" maxLength={50} />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={12}>
<Form.Item name="planStartDate" label="项目开始时间">
<DatePicker style={{ width: "100%" }} placeholder="请选择" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="planEndDate" label="项目完成时间">
<DatePicker style={{ width: "100%" }} placeholder="请选择" />
</Form.Item>
</Col>
</Row>
</Card>
<Card title="客户所在地与打卡定位" style={{ marginBottom: 24 }}>
<Form.Item label="客户所在地" style={{ marginBottom: 16 }}>
<Flex>
<Form.Item name="customerAddress" noStyle>
<Input
placeholder="请选择客户或打开地图定位"
disabled
maxLength={200}
style={{ width: 400 }}
/>
</Form.Item>
<Button
onClick={() => setMapPickerVisible(true)}
style={{ marginLeft: 8 }}
>
选择地图位置
</Button>
</Flex>
</Form.Item>
<Row gutter={24}>
<Col span={6}>
<Form.Item name="longitude" label="经度">
<Input placeholder="自动获取" disabled />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="latitude" label="纬度">
<Input placeholder="自动获取" disabled />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name="checkinRadiusMeters" label="打卡有效范围">
<Select placeholder="请选择" allowClear>
{CHECKIN_RADIUS_OPTIONS.map((opt) => (
<Select.Option key={opt.value} value={opt.value}>
{opt.label}
</Select.Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
</Card>
<Card title="项目参与人员">
<Form.Item label="参与人员" required>
<Input
placeholder="点击选择参与人员"
readOnly
value={
selectedParticipants.length
? `已选 ${selectedParticipants.length}`
: ""
}
onClick={() => setStaffPickerVisible(true)}
style={{ cursor: "pointer", width: 400 }}
/>
{selectedParticipants.length > 0 && (
<Flex wrap style={{ marginTop: 8, gap: 4 }}>
{selectedParticipants.map((item) => (
<Tag
key={item.id}
closable={!isView}
onClose={() =>
setSelectedParticipants((prev) =>
prev.filter((r) => r.id !== item.id),
)
}
>
{item.userName}
</Tag>
))}
</Flex>
)}
</Form.Item>
</Card>
</Form>
<StaffPickerModal
open={staffPickerVisible}
onCancel={() => setStaffPickerVisible(false)}
onConfirm={handleStaffConfirm}
selectedIds={selectedParticipants.map((item) => item.id)}
selectedParticipants={selectedParticipants}
/>
<BaiduMapPicker
visible={mapPickerVisible}
onCancel={() => setMapPickerVisible(false)}
onConfirm={handleMapConfirm}
/>
</PageLayout>
);
};
export default Connect([NS_SAFETY_EVAL_BUSINESS], true)(EvalProjectCreate);

View File

@ -0,0 +1,187 @@
import React, { useState, useEffect } from "react";
import {
Form,
Table,
Button,
Input,
Select,
Space,
Tag,
} from "antd";
import {
PlusOutlined,
} from "@ant-design/icons";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm";
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { tools } from "@cqsjjb/jjb-common-lib";
const { router } = tools;
const EvalProject = (props) => {
const [searchForm] = Form.useForm();
const [dataSource, setDataSource] = useState([]);
const [total, setTotal] = useState(0);
const { evalProjectLoading } = props.safetyEvalBusiness;
const getData = async (pagination) => {
const params = {
...router.query,
current: pagination?.current || router.query.current || 1,
size: pagination?.size || router.query.size || 10,
};
const res = await props.evalProjectPage(params);
if (res?.success !== false) {
setDataSource(res?.data || []);
setTotal(res?.total || 0);
}
};
useEffect(() => {
searchForm.setFieldsValue(router.query);
getData();
}, []);
const handleSearch = (values) => {
router.query = { ...router.query, ...values, current: 1, size: 10 };
getData();
};
const handleReset = (values) => {
searchForm.resetFields();
router.query = { ...values, current: 1, size: 10 };
getData();
};
const handlePageChange = (pagination) => {
router.query = {
...router.query,
current: pagination.current,
size: pagination.pageSize,
};
getData(pagination);
};
const columns = [
{ title: "序号", width: 60, render: (_, __, index) => index + 1 },
{
title: "项目名称",
dataIndex: "projectName",
ellipsis: true,
width: 220,
render: (text, record) => (
<div>
<div>{text}</div>
<div style={{ fontSize: 12, color: "#999" }}>
{record.projectNo} · {record.evalTypeName}
</div>
</div>
),
},
{ title: "项目负责人", dataIndex: "projectLeaderName", width: 120 },
{ title: "客户负责人", dataIndex: "customerContactName", width: 120 },
{ title: "项目开始时间", dataIndex: "planStartDate", width: 120 },
{ title: "项目结束时间", dataIndex: "planEndDate", width: 120 },
{
title: "项目阶段",
dataIndex: "projectPhaseName",
width: 100,
render: (phase) => {
const color = phase === "延期" ? "error" : phase === "正常" ? "success" : undefined;
return <Tag color={color}>{phase || "-"}</Tag>;
},
},
{
title: "操作",
width: 100,
fixed: "right",
render: (_, record) => (
<Space>
<Button
type="link"
size="small"
onClick={() => props.history.push(`Create?id=${record.id}`)}
>
查看
</Button>
</Space>
),
},
];
return (
<PageLayout
title="安评项目管理"
extra={
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => props.history.push("Create")}
>
新建项目
</Button>
}
>
<div style={{ fontSize: 13, color: "#666", marginBottom: 16 }}>
管理机构全部安全评价项目各业务节点可独立维护不以系统顺序替代机构线下实操
</div>
<SearchForm
form={searchForm}
loading={false}
formLine={[
<Form.Item key="projectName" name="projectName">
<ControlWrapper.Input
label="项目名称"
placeholder="关键字查询"
allowClear
/>
</Form.Item>,
<Form.Item key="projectLeaderName" name="projectLeaderName">
<ControlWrapper.Input
label="项目负责人"
placeholder="姓名"
allowClear
/>
</Form.Item>,
<Form.Item key="customerContactName" name="customerContactName">
<ControlWrapper.Input
label="客户负责人"
placeholder="姓名"
allowClear
/>
</Form.Item>,
]}
onFinish={handleSearch}
onReset={handleReset}
style={{ marginBottom: 16 }}
/>
<Table
rowKey="id"
columns={columns}
dataSource={dataSource}
loading={evalProjectLoading}
scroll={{ y: props.scrollY, x: 1000 }}
pagination={{
total,
current: router.query.current || 1,
pageSize: router.query.size || 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
}}
onChange={handlePageChange}
/>
</PageLayout>
);
};
export default Connect(
[NS_SAFETY_EVAL_BUSINESS],
true,
)(AntdTableFuncControl(EvalProject));

View File

@ -0,0 +1,9 @@
function EvalProject(props) {
return (
props.children
);
}
export default EvalProject;