master
LiuJiaNan 2026-08-22 09:30:25 +08:00
commit 1476e3276e
53 changed files with 2976 additions and 0 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# production
/dist
/demo
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea
yarn.lock

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# 微应用模板说明文档
## 在线文档
https://www.yuque.com/buhangjiecheshen-ymbtb/qc0093/gxdun1dphetcurko
## 安装依赖
项目依赖可通过 **yarn****npm** 进行安装:
```bash
# 使用 yarn
yarn
# 或使用 npm
npm i
```
## 开发服务&打包应用
```bash
# 启动开发服务
yarn serve:<env>
# 或
npm run serve:<env>
# 开发环境打包
yarn build:<env>
# 或
npm run build:<env>
```
## 路由配置&路由访问&自动化路由
所有页面必须放在`src/pages/container`目录下,启动访问页面请在浏览器地址栏输入`/<appIdentifier>/container/<你的路由页面文件名称>`
解释:
1. 所有页面组件命名为`index.js`或`index.jsx`,必须放在一个首字母大写的文件中。
2. `container`为固定路径访问格式
3. `<appIdentifier>`为应用的唯一标识符,也是应用路由的`basename`,在底座中用于区分其他应用。可在根目录 `jjb.config.js` 文件的 `appIdentifier` 节点中进行修改。
4. 自动化路由将根据`pages/container`中的路由页面文件自动生成路由树。
5. `id`匹配路由,文件夹命名`_id`
## 应用接口环境配置
应用接口环境相关配置在根目录 `jjb.config.js` 文件的 `environment` 节点中进行定义。
## 应用开发服务配置
应用开发服务相关配置在根目录 `jjb.config.js` 文件的 `server` 节点中进行定义。
## Babel 配置
应用的 `Babel` 配置在根目录 `jjb.babel.js` 文件中进行管理。
## 目录说明
1. `src/api/` 配置各个 store 模块的接口数据。
2. `src/components/` 全局公共组件。
3. `src/enumerate/` 全局各种枚举配置。
4. `src/pages/` 页面文件目录。
5. `src/main.js` 应用的入口文件。
## 核心依赖
1. `@cqsjjb/jjb-common-decorator`
1. 公共装饰器库,内部包含:
1. 按钮权限处理
2. antd/Table 控制
3. 文本重命名处理
4. 具体使用方式可参考各个模块的 `d.ts`
2. `@cqsjjb/jjb-common-lib`
1. 公共工具库,具体 API 使用请查看 `d.ts`
3. `@cqsjjb/jjb-dva-runtime`
1. 核心运行时,基于 `dvajs` 实现。
1. 应用核心依赖模块
2. 应用的自动化路由
3. `store` 模块接口数据处理
4. 均基于此依赖实现,具体使用方式请查看 `d.ts`
4. `@cqsjjb/jjb-react-admin-component`
1. 公共组件库,具体组件使用方式请查看 `d.ts`

49
eslint.config.js Normal file
View File

@ -0,0 +1,49 @@
import antfu from "@antfu/eslint-config";
export default antfu({
formatters: {
html: false,
css: true,
},
test: false,
typescript: true,
react: true,
vue: false,
markdown: false,
stylistic: {
semi: true,
quotes: "double",
},
overrides: {
react: {
"react/no-comment-textnodes": "off",
"react-hooks-extra/no-unnecessary-use-prefix": "off",
"react-hooks-extra/prefer-use-state-lazy-initialization": "off",
"react-hooks/exhaustive-deps": "off",
"react/no-implicit-key": "off",
},
javascript: {
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-alert": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-restricted-syntax": [
"error",
{
selector: "VariableDeclarator[id.name='pd']",
message: "不允许使用 pd请改用有语义化的变量名",
},
{
selector: "ObjectExpression > Property[key.name='pd']",
message: "不允许使用 pd请改用有语义化的变量名",
},
],
"no-unused-vars": ["error", { varsIgnorePattern: "^React$" }],
},
},
rules: {
"antfu/top-level-function": "off",
"node/prefer-global/process": "off",
"dot-notation": "off",
"linebreak-style": ["off", "windows"],
},
});

23
jjb.babel.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
compact: false,
// 插件
plugins: [
[
"@babel/plugin-proposal-decorators",
{
legacy: true,
},
],
],
// 预设
presets: [
["@babel/preset-env", {
targets: {
browsers: ["ie >= 10"],
},
}],
["@babel/preset-react", {
runtime: "automatic",
}],
],
};

74
jjb.config.js Normal file
View File

@ -0,0 +1,74 @@
module.exports = {
// 应用后端git地址部署上线需要
javaGit: "<git-url>",
// 应用后端仓库名称,部署上线需要
javaGitName: "<git-name>",
// 环境配置
environment: {
development: {
// 应用后端分支名称,部署上线需要
javaGitBranch: "<branch-name>",
// 接口服务地址
API_HOST: "https://gbs-gateway.qhdsafety.com",
},
production: {
// 应用后端分支名称,部署上线需要
javaGitBranch: "<branch-name>",
// 接口服务地址
API_HOST: "",
},
},
// 应用唯一标识符
appIdentifier: "proDispatch",
// 应用上下文注入全局变量
contextInject: {
// 应用Key
appKey: "",
},
// public/index.html注入全局变量
windowInject: {
// 应用标题
title: "微应用模板",
// 注入css链接集合
links: [],
element: {
root: {
// 挂载DOM元素ID
id: "root",
},
},
// 注入js链接集合
scripts: [],
},
// 开发服务
server: {
// 监听端口号
port: "8088",
// 服务地址
host: "127.0.0.1",
// 是否自动打开浏览器
open: true,
},
// 框架
framework: {
// ant-design
antd: {
// 全局antd-class-name前缀
"ant-prefix": "micro-temp",
// 全局字体
"fontFamily": "PingFangSC-Regular",
// 全局主题色
"colorPrimary": "#1677ff",
// 全局圆角
"borderRadius": 2,
},
},
// webpack
webpackConfig: {
// 单页面插件
htmlWebpackPluginOption: {
// 自动注入编译后的文件到public/index.html中
inject: true,
},
},
};

10
jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"~/*": ["*"]
},
"jsx": "react"
},
"include": ["src"]
}

50
package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "micro-app",
"version": "2.0.0",
"description": "建教帮微应用模板",
"author": "JJB",
"license": "MIT",
"main": "index.js",
"scripts": {
"serve": "node node_modules/@cqsjjb/scripts/webpack.dev.server.js",
"build": "node node_modules/@cqsjjb/scripts/webpack.build.js",
"push": "jjb-cmd push java production",
"clean-cache": "rimraf node_modules/.cache/webpack",
"serve:development": "cross-env NODE_ENV=development npm run serve",
"serve:production": "cross-env NODE_ENV=production npm run serve",
"build:development": "cross-env NODE_ENV=development npm run build",
"build:production": "cross-env NODE_ENV=production npm run build",
"code-optimization": "node node_modules/@cqsjjb/scripts/code-optimization.js",
"lint": "eslint --ext .js,.jsx,.tsx --fix src"
},
"dependencies": {
"@ahooksjs/use-url-state": "^3.5.1",
"@ant-design/icons": "^5.6.1",
"@ant-design/pro-components": "^2.8.10",
"@cqsjjb/jjb-common-decorator": "latest",
"@cqsjjb/jjb-common-lib": "latest",
"@cqsjjb/jjb-dva-runtime": "latest",
"@cqsjjb/jjb-react-admin-component": "latest",
"ahooks": "^3.9.5",
"antd": "^5.27.6",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"mockjs": "^1.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"throttle-debounce": "^5.0.2",
"zy-react-library": "^1.3.31"
},
"devDependencies": {
"@antfu/eslint-config": "^5.4.1",
"@babel/plugin-proposal-decorators": "^7.19.3",
"@cqsjjb/scripts": "2.0.0-alpha-1",
"@eslint-react/eslint-plugin": "^2.2.2",
"cross-env": "^7.0.3",
"eslint": "^9.37.0",
"eslint-plugin-format": "^1.0.2",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.23",
"typescript": "^5.9.3"
}
}

55
public/index.html Normal file
View File

@ -0,0 +1,55 @@
<!--BEGIN-->
<!--<% var { env: $env, process: $process, mode: $mode, builtInfo: $builtInfo, links: $links, redirectLogin: $redirectLogin, framework: $framework, scripts: $scripts, element: $element } = htmlWebpackPlugin.options %>-->
<!--<% var { appKey: $appKey, antd: $antd, basename: $basename, API_HOST: $API_HOST } = $process %>-->
<!--<% var { ['ant-prefix']: $antPrefix, fontFamily: $fontFamily, colorPrimary: $colorPrimary, borderRadius: $borderRadius } = $antd %>-->
<!--NED-->
<!DOCTYPE html>
<html lang="zh">
<head data-built-info="<%= $builtInfo %>">
<meta charset="UTF-8"/>
<meta name="renderer" content="webkit"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,Chrome=1"/>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no,viewport-fit=cover">
<meta charset="UTF-8" name="referrer" content="strict-origin-when-cross-origin"/>
<% for (const item of $links) { %>
<link type="text/css" rel="stylesheet" href="<%= item %>"></link>
<% } %>
<title>--</title>
<script>
(function () {
const APP_ENV = {
antd: {
'ant-prefix': '<%= $antPrefix %>',
fontFamily: '<%= $fontFamily %>',
colorPrimary: '<%= $colorPrimary %>',
borderRadius: parseInt('<%= $borderRadius %>')
},
appKey: '<%= $appKey %>',
basename: '<%= $basename %>',
API_HOST: '<%= $API_HOST %>'
};
APP_ENV.API_HOST = sessionStorage.API_HOST || APP_ENV.API_HOST || window.location.origin;
window.process = {
env: { app: APP_ENV },
NODE_ENV: '<%= $mode %>'
};
window.__JJB_ENVIRONMENT__ = {
API_HOST: APP_ENV.API_HOST,
redirect: '<%= $redirectLogin %>',
FRAMEWORK: APP_ENV.antd
};
})();
</script>
<!-- SCRIPTS -->
<% for (const item of $scripts) { %>
<script src="<%= item %>" type="text/javascript"></script>
<% } %>
</head>
<body >
<!-- NOSCRIPT -->
<noscript>此网页需要开启JavaScript功能。</noscript>
<!-- MAIN -->
<% const { root } = $element; %>
<div id="<%= root.id %>" style="width: 100%; height: 100%; position: relative;overflow-y: auto"></div>
</body>
</html>

24
router.md Normal file
View File

@ -0,0 +1,24 @@
# 生产派工管理路由规划
## 派工工单管理
### 人员派工
- 生产派工
`/proDispatch/container/personnelDispatch/productionDispatch`
- 清扫派工
`/proDispatch/container/personnelDispatch/cleaningDispatch`
### 车辆派工
- 生产派工
`/proDispatch/container/vehicleDispatch/productionDispatch`
- 清扫派工
`/proDispatch/container/vehicleDispatch/cleaningDispatch`
## 设备管理
- 内部设备管理
`/proDispatch/container/deviceManagement/internalDevice`
- 相关方设备管理
`/proDispatch/container/deviceManagement/partnerDevice`

View File

@ -0,0 +1,22 @@
import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
// 当前接口地址仅用于原型阶段的本地请求拦截,收到正式接口文档后需按文档替换。
export const internalDeviceList = declareRequest(
"internalDeviceLoading",
"Post > @/production-dispatch/device/internal/list",
);
export const internalDeviceSync = declareRequest(
"internalDeviceSyncLoading",
"Post > @/production-dispatch/device/internal/sync",
);
export const partnerDeviceList = declareRequest(
"partnerDeviceLoading",
"Post > @/production-dispatch/device/partner/list",
);
export const partnerDeviceSync = declareRequest(
"partnerDeviceSyncLoading",
"Post > @/production-dispatch/device/partner/sync",
);

1
src/api/global/index.js Normal file
View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,32 @@
import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
// 当前接口地址仅用于原型阶段的本地请求拦截,收到正式接口文档后需按文档替换。
export const productionDispatchList = declareRequest(
"productionDispatchLoading",
"Post > @/production-dispatch/personnel/production/list",
);
export const productionDispatchApproval = declareRequest(
"productionDispatchApprovalLoading",
"Post > @/production-dispatch/personnel/production/approval",
);
export const productionDispatchSync = declareRequest(
"productionDispatchSyncLoading",
"Post > @/production-dispatch/personnel/production/sync",
);
export const cleaningDispatchList = declareRequest(
"cleaningDispatchLoading",
"Post > @/production-dispatch/personnel/cleaning/list",
);
export const cleaningDispatchApproval = declareRequest(
"cleaningDispatchApprovalLoading",
"Post > @/production-dispatch/personnel/cleaning/approval",
);
export const cleaningDispatchSync = declareRequest(
"cleaningDispatchSyncLoading",
"Post > @/production-dispatch/personnel/cleaning/sync",
);

View File

@ -0,0 +1,32 @@
import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
// 当前接口地址仅用于原型阶段的本地请求拦截,收到正式接口文档后需按文档替换。
export const productionDispatchList = declareRequest(
"productionDispatchLoading",
"Post > @/production-dispatch/vehicle/production/list",
);
export const productionDispatchApproval = declareRequest(
"productionDispatchApprovalLoading",
"Post > @/production-dispatch/vehicle/production/approval",
);
export const productionDispatchSync = declareRequest(
"productionDispatchSyncLoading",
"Post > @/production-dispatch/vehicle/production/sync",
);
export const cleaningDispatchList = declareRequest(
"cleaningDispatchLoading",
"Post > @/production-dispatch/vehicle/cleaning/list",
);
export const cleaningDispatchApproval = declareRequest(
"cleaningDispatchApprovalLoading",
"Post > @/production-dispatch/vehicle/cleaning/approval",
);
export const cleaningDispatchSync = declareRequest(
"cleaningDispatchSyncLoading",
"Post > @/production-dispatch/vehicle/cleaning/sync",
);

View File

@ -0,0 +1,57 @@
import { Button, Descriptions, Divider, Modal } from "antd";
export default function DeviceDetailModal(props) {
const { onClose, record, unitLabel } = props;
return (
<Modal
title={`${record.deviceName} 设备详情`}
width={720}
open
footer={<Button onClick={onClose}>关闭</Button>}
maskClosable={false}
onCancel={onClose}
>
<Divider orientation="left">设备基础信息</Divider>
<Descriptions
bordered
column={1}
items={[
{ key: "deviceNo", label: "设备编号", children: record.deviceNo },
{ key: "deviceName", label: "设备名称", children: record.deviceName },
{ key: "deviceType", label: "设备类型", children: record.deviceType },
{ key: "unit", label: unitLabel, children: record.unit },
{ key: "status", label: "设备状态", children: record.status },
{ key: "updateTime", label: "更新时间", children: record.updateTime },
{ key: "sourceSystem", label: "来源系统", children: record.sourceSystem },
]}
styles={{ label: { width: 200 } }}
/>
<Divider orientation="left">定位绑定信息</Divider>
<Descriptions
bordered
column={1}
items={[
{ key: "gpsNo", label: "定位设备编号", children: record.gpsNo },
{ key: "bindingObject", label: "绑定对象", children: record.bindingObject },
{ key: "relatedVehicle", label: "关联车辆", children: record.relatedVehicle },
{ key: "location", label: "当前位置", children: record.location },
]}
styles={{ label: { width: 200 } }}
/>
<Divider orientation="left">维护边界</Divider>
<Descriptions
bordered
column={1}
items={[
{
key: "maintenanceBoundary",
label: "维护说明",
children: "设备主数据由生产派工调度系统同步,双控系统只用于监管查询、详情查看和定位绑定展示。",
},
]}
styles={{ label: { width: 200 } }}
/>
</Modal>
);
}

View File

@ -0,0 +1,131 @@
import { Button, Tag } from "antd";
import { useState } from "react";
import Page from "zy-react-library/components/Page";
import Search from "zy-react-library/components/Search";
import Table from "zy-react-library/components/Table";
import { FORM_ITEM_RENDER_ENUM } from "zy-react-library/enum/formItemRender";
import useTable from "zy-react-library/hooks/useTable";
import DeviceDetailModal from "~/components/DeviceDetailModal";
import SummaryMetrics from "~/components/SummaryMetrics";
import SyncExportActions from "~/components/SyncExportActions";
const selectItems = values => [...new Set(values)].map(value => ({ bianma: value, name: value }));
export default function DeviceManagementList(props) {
const { listRequest, requestSync, scope, syncLoading } = props;
const [form] = Search.useForm();
const [currentRecord, setCurrentRecord] = useState({});
const [detailModalOpen, setDetailModalOpen] = useState(false);
const { tableProps, getData } = useTable(listRequest, { form });
const partner = scope === "partner";
const currentList = tableProps.dataSource || [];
const locatedCount = currentList.filter(item => item.gpsNo && item.gpsNo !== "-").length;
const normalCount = currentList.filter(item => item.status === "正常").length;
const unitLabel = partner ? "相关方单位" : "所属单位";
const unitItems = partner ? ["相关方清扫一队", "相关方清扫二队"] : ["流机公司", "清扫二队"];
const statusItems = partner ? ["正常", "维修", "停用"] : ["正常"];
return (
<Page isShowAllAction={false}>
<SummaryMetrics
items={[
{
label: "设备台账",
value: tableProps.pagination?.total || 0,
note: `当前页 ${currentList.length}`,
},
{ label: "定位绑定", value: locatedCount, note: "查看终端与绑定对象", type: "blue" },
{ label: "正常设备", value: normalCount, note: "同步状态正常", type: "green" },
{ label: "维护边界", value: "只读", note: "双控系统不维护设备主数据", type: "orange" },
]}
/>
<Search
form={form}
options={[
{ name: "keyword", label: "关键词", placeholder: "设备编号、设备名称、绑定对象" },
{
name: "deviceType",
label: "设备类型",
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(["定位终端", "车辆"]),
componentProps: { allowClear: true },
},
{
name: "unit",
label: unitLabel,
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(unitItems),
componentProps: { allowClear: true },
},
{
name: "status",
label: "设备状态",
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(statusItems),
componentProps: { allowClear: true },
},
]}
onFinish={getData}
/>
<Table
toolBarRender={() => (
<SyncExportActions
syncText="同步设备"
syncMessage="已同步内部与相关方设备台账"
syncLoading={syncLoading}
requestSync={requestSync}
onSyncSuccess={getData}
exportUrl={`/production-dispatch/device/${scope}/export`}
exportName={partner ? "相关方设备管理" : "内部设备管理"}
getExportParams={() => form.getFieldsValue()}
/>
)}
columns={[
{ title: "设备编号", dataIndex: "deviceNo", render: (_, record) => <strong>{record.deviceNo}</strong> },
{ title: "设备名称", dataIndex: "deviceName" },
{ title: "设备类型", dataIndex: "deviceType" },
{ title: unitLabel, dataIndex: "unit" },
{ title: "绑定对象", dataIndex: "bindingObject" },
{
title: "设备状态",
dataIndex: "status",
render: (_, record) => <Tag color={record.status === "正常" ? "green" : "red"}>{record.status}</Tag>,
},
{ title: "更新时间", dataIndex: "updateTime" },
{
title: "操作",
fixed: "right",
width: 80,
render: (_, record) => (
<Button
type="link"
onClick={() => {
setCurrentRecord(record);
setDetailModalOpen(true);
}}
>
详情
</Button>
),
},
]}
showIndexColumn={false}
{...tableProps}
/>
{detailModalOpen && (
<DeviceDetailModal
record={currentRecord}
unitLabel={unitLabel}
onClose={() => {
setDetailModalOpen(false);
setCurrentRecord({});
}}
/>
)}
</Page>
);
}

View File

@ -0,0 +1,310 @@
import { Button, Descriptions, Divider, message, Modal, Steps, Tag } from "antd";
import FormBuilder from "zy-react-library/components/FormBuilder";
import Table from "zy-react-library/components/Table";
import { FORM_ITEM_RENDER_ENUM } from "zy-react-library/enum/formItemRender";
import DispatchMap from "~/components/DispatchMap";
import styles from "./index.module.less";
const productionWorkflowNodes = [
"公务公司审核",
"港务公司计划员审核",
"港务公司业务部长审核",
"流机计划员审核",
"流机业务部长审核",
"流机计划员分发",
"流机调度员下达",
];
const cleaningWorkflowNodes = ["任务同步", "双控审核", "作业前确认", "派工下达", "作业后确认", "归档"];
function getCurrentWorkflowIndex(record, workflowNodes, cleaning) {
if (/已下达|已完成|已通过/.test(record.approvalStatus) && !/审核中|待审/.test(record.approvalStatus)) {
return workflowNodes.length - 1;
}
const currentIndex = workflowNodes.findIndex(node => record.currentRole.includes(node) || node.includes(record.currentRole));
if (currentIndex >= 0) {
return currentIndex;
}
if (cleaning && /现场值守人/.test(record.currentRole)) {
return 2;
}
return record.approvalStatus === "待审" ? 0 : 1;
}
function getApprovalResultColor(value) {
if (/异常|驳回/.test(value)) {
return "red";
}
if (/处理中|待审/.test(value)) {
return "gold";
}
return "green";
}
function getModalTitle(record, cleaning, vehicle) {
if (cleaning && vehicle) {
return `${record.id} 车辆清扫派工详情`;
}
if (cleaning) {
return `${record.id} 清扫派工详情`;
}
if (vehicle) {
return `${record.planNo} 车辆派工详情`;
}
return `${record.planNo} 派工详情`;
}
function getWorkflowDescription(index, currentIndex) {
if (index < currentIndex) {
return "已处理";
}
if (index === currentIndex) {
return "当前节点";
}
return "待流转";
}
export default function DispatchDetailModal(props) {
const {
approvalLoading,
approvalMode,
businessType,
objectType,
onClose,
onSuccess,
record,
requestApproval,
} = props;
const [form] = FormBuilder.useForm();
const cleaning = businessType === "cleaning";
const vehicle = objectType === "vehicle";
const approvalObject = cleaning ? record.cleaningPeople : record.personnel;
const workflowNodes = cleaning ? cleaningWorkflowNodes : productionWorkflowNodes;
const currentWorkflowIndex = getCurrentWorkflowIndex(record, workflowNodes, cleaning);
const canApprove = approvalMode
&& record.approvalOwner === "双控系统审批"
&& ["待审", "审核中"].includes(record.approvalStatus)
&& !/未配置|未匹配/.test(`${record.currentRole} ${approvalObject}`);
const handleApproval = async (values) => {
const { success } = await requestApproval({ id: record.id, ...values });
if (success) {
message.success(values.approvalResult === "PASS" ? "审批已通过" : "审批已驳回");
onSuccess();
}
};
return (
<Modal
title={getModalTitle(record, cleaning, vehicle)}
width={960}
open
footer={canApprove ? undefined : <Button onClick={onClose}>关闭</Button>}
confirmLoading={approvalLoading}
maskClosable={false}
okText="提交审批"
onOk={canApprove ? form.submit : undefined}
onCancel={onClose}
>
{canApprove && (
<>
<Divider orientation="left">审批处理</Divider>
<FormBuilder
form={form}
loading={approvalLoading}
values={{ approvalResult: "PASS" }}
span={24}
labelCol={{ span: 6 }}
showActionButtons={false}
options={[
{
name: "approvalResult",
label: "审批结果",
render: FORM_ITEM_RENDER_ENUM.RADIO,
items: [
{ bianma: "PASS", name: "通过" },
{ bianma: "REJECT", name: "驳回" },
],
},
{
name: "rejectReason",
label: "驳回原因",
render: FORM_ITEM_RENDER_ENUM.TEXTAREA,
dependencies: ["approvalResult"],
hidden: values => values.approvalResult !== "REJECT",
required: values => values.approvalResult === "REJECT",
rules: [{ whitespace: true, message: "请输入驳回原因" }],
},
]}
onFinish={handleApproval}
/>
</>
)}
<Divider orientation="left">位置与轨迹</Divider>
<DispatchMap latitude={record.latitude} longitude={record.longitude} trackPoints={record.trackPoints} />
<Descriptions
bordered
column={1}
items={[
{ key: "position", label: "当前位置", children: record.position },
{ key: "locationTime", label: "定位时间", children: record.locationTime },
{ key: "trackStatus", label: "轨迹状态", children: record.trackStatus },
{ key: "accessState", label: "门禁状态", children: record.accessState },
{ key: "videoState", label: "视频状态", children: record.videoState },
{ key: "progress", label: "执行进度", children: record.progress },
]}
styles={{ label: { width: 200 } }}
/>
<Divider orientation="left">基础信息</Divider>
<Descriptions
bordered
column={1}
items={[
{ key: "id", label: "监管派工单号", children: record.id },
{ key: "sourceNo", label: "来源派工单号", children: record.sourceNo },
{ key: "sourceSystem", label: "来源系统", children: record.sourceSystem },
{ key: "businessType", label: "业务类别", children: record.businessType },
{ key: "objectType", label: "派工对象", children: record.objectType },
{ key: "dispatchMode", label: "派工方式", children: record.dispatchMode },
{ key: "orderStatus", label: "工单状态", children: record.orderStatus },
]}
styles={{ label: { width: 200 } }}
/>
{cleaning && (
<>
<Divider orientation="left">清扫派工作业信息</Divider>
<Descriptions
bordered
column={1}
items={[
{ key: "id", label: "工单编号", children: record.id },
{ key: "workDate", label: "作业日期", children: record.workDate },
{ key: "workSource", label: "工单来源", children: record.workSource },
{ key: "companyName", label: "公司名称", children: record.companyName },
{ key: "workCompany", label: "作业公司", children: record.workCompany },
{ key: "workProject", label: "作业项目", children: record.workProject },
{ key: "workProcess", label: "作业过程", children: record.workProcess },
{ key: "workPlace", label: "作业地点", children: record.workPlace },
{ key: "description", label: "工单说明", children: record.description },
{ key: "orderStatus", label: "工单状态", children: record.orderStatus },
{ key: "settlementStatus", label: "结算状态", children: record.settlementStatus },
{ key: "timeout", label: "作业是否超时", children: record.timeout },
{ key: "evaluation", label: "评价", children: record.evaluation },
]}
styles={{ label: { width: 200 } }}
/>
<Divider orientation="left">清扫执行信息</Divider>
<Descriptions
bordered
column={1}
items={[
{ key: "inspector", label: "流机现场巡视员", children: record.inspector },
{ key: "cleaningPeople", label: "清扫人员", children: record.cleaningPeople },
{ key: "markerName", label: "标识名称", children: record.markerName },
{ key: "deviceName", label: "设备名称", children: record.deviceName },
]}
styles={{ label: { width: 200 } }}
/>
</>
)}
{!cleaning && (
<>
<Divider orientation="left">生产派工作业信息</Divider>
<Descriptions
bordered
column={1}
items={[
{ key: "planNo", label: "计划号", children: record.planNo },
{ key: "carUnit", label: "用车单位", children: record.carUnit },
{ key: "applyDept", label: "申请用车部门", children: record.applyDept },
{ key: "responsibleDept", label: "现场负责部门", children: record.responsibleDept },
{ key: "responsiblePerson", label: "现场负责人", children: record.responsiblePerson },
{ key: "expectedArrivalTime", label: "预到位时间", children: record.expectedArrivalTime },
{ key: "shift", label: "白/夜班", children: record.shift },
{ key: "workPlace", label: "作业地点", children: record.workPlace },
{ key: "arrivalPlace", label: "到位地点", children: record.arrivalPlace },
{ key: "workContent", label: "作业内容", children: record.workContent },
{ key: "workProcess", label: "作业过程", children: record.workProcess },
{ key: "cargoName", label: "作业货名", children: record.cargoName },
{ key: "voyage", label: "船名航次", children: record.voyage },
]}
styles={{ label: { width: 200 } }}
/>
</>
)}
{!cleaning && vehicle && (
<>
<Divider orientation="left">车辆/设备信息</Divider>
<Descriptions
bordered
column={1}
items={[
{ key: "requestedEquipment", label: "申请设备", children: record.requestedEquipment },
{ key: "fulfilledEquipment", label: "兑现设备", children: record.fulfilledEquipment },
{ key: "licenseNo", label: "车牌号", children: record.licenseNo },
{ key: "vehicleType", label: "车辆类型", children: record.vehicleType },
{ key: "driver", label: "司机", children: record.driver },
{ key: "personnel", label: "关联人员", children: record.personnel },
]}
styles={{ label: { width: 200 } }}
/>
</>
)}
{!cleaning && !vehicle && (
<>
<Divider orientation="left">人员信息</Divider>
<Descriptions
bordered
column={1}
items={[
{ key: "personnel", label: "派工人员", children: record.personnel },
{ key: "personId", label: "人员ID", children: record.personId },
{ key: "licenseNo", label: "关联车辆", children: record.licenseNo },
{ key: "requestedEquipment", label: "申请设备", children: record.requestedEquipment },
{ key: "fulfilledEquipment", label: "兑现设备", children: record.fulfilledEquipment },
]}
styles={{ label: { width: 200 } }}
/>
</>
)}
<Divider orientation="left">审批与执行链路</Divider>
<div className={styles.workflowSteps}>
<Steps
current={currentWorkflowIndex}
items={workflowNodes.map((node, index) => ({
title: node,
description: getWorkflowDescription(index, currentWorkflowIndex),
}))}
labelPlacement="vertical"
responsive={false}
size="small"
/>
</div>
<Divider orientation="left">处理记录</Divider>
<Table
columns={[
{ title: "处理时间", dataIndex: "time" },
{ title: "节点", dataIndex: "node" },
{
title: "结果",
dataIndex: "result",
render: (_, record) => <Tag color={getApprovalResultColor(record.result)}>{record.result}</Tag>,
},
{ title: "意见", dataIndex: "opinion" },
]}
dataSource={(record.approvalRecords || []).map(([time, node, result, opinion], index) => ({
id: `${record.id}-${index}`,
time,
node,
result,
opinion,
}))}
disabledResizer
options={false}
pagination={false}
showIndexColumn={false}
/>
</Modal>
);
}

View File

@ -0,0 +1,9 @@
.workflowSteps {
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 8px;
:global(.micro-temp-steps) {
min-width: 900px;
}
}

View File

@ -0,0 +1,211 @@
import { Button, Space, Tag } from "antd";
import { useState } from "react";
import Page from "zy-react-library/components/Page";
import Search from "zy-react-library/components/Search";
import Table from "zy-react-library/components/Table";
import { FORM_ITEM_RENDER_ENUM } from "zy-react-library/enum/formItemRender";
import useTable from "zy-react-library/hooks/useTable";
import DispatchDetailModal from "~/components/DispatchDetailModal";
import SummaryMetrics from "~/components/SummaryMetrics";
import SyncExportActions from "~/components/SyncExportActions";
const statusColor = {
待审: "gold",
审核中: "orange",
已通过: "green",
已驳回: "red",
已下达: "green",
};
const orderStatusColor = {
待下达: "gold",
作业中: "blue",
已完成: "green",
};
const selectItems = values => [...new Set(values)].map(value => ({ bianma: value, name: value }));
export default function CleaningDispatchList(props) {
const {
approvalLoading,
listRequest,
objectType,
requestApproval,
requestSync,
syncLoading,
} = props;
const [form] = Search.useForm();
const [currentRecord, setCurrentRecord] = useState({});
const [detailModalOpen, setDetailModalOpen] = useState(false);
const [approvalMode, setApprovalMode] = useState(false);
const { tableProps, getData } = useTable(listRequest, { form });
const personnel = objectType === "personnel";
const currentList = tableProps.dataSource || [];
const pendingCount = currentList.filter(item => ["待审", "审核中"].includes(item.approvalStatus)).length;
const dualApprovalCount = currentList.filter(item => item.approvalOwner === "双控系统审批").length;
const activeCount = currentList.filter(item => ["待下达", "作业中"].includes(item.orderStatus)).length;
const closeDetailModal = () => {
setApprovalMode(false);
setDetailModalOpen(false);
setCurrentRecord({});
};
const openDetailModal = (record, approve = false) => {
setApprovalMode(approve);
setCurrentRecord(record);
setDetailModalOpen(true);
};
return (
<Page isShowAllAction={false}>
<SummaryMetrics
items={[
{
label: "当前菜单工单",
value: tableProps.pagination?.total || 0,
note: `当前页 ${currentList.length}`,
},
{ label: "双控审批", value: dualApprovalCount, note: "仅有权限节点可操作", type: "blue" },
{ label: "执行监管", value: activeCount, note: "位置、轨迹和工单状态联动展示", type: "green" },
{ label: "待处理", value: pendingCount, note: "包含待审和审核中工单", type: "orange" },
]}
/>
<Search
form={form}
options={[
{ name: "keyword", label: "关键词", placeholder: "工单编号、作业地点、清扫人员、设备名称" },
{
name: "workSource",
label: "工单来源",
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(["临时清扫", "清扫计划"]),
componentProps: { allowClear: true },
},
{
name: "workCompany",
label: "作业公司",
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(["相关方清扫一队", "清扫二队"]),
componentProps: { allowClear: true },
},
{
name: "orderStatus",
label: "工单状态",
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(["待下达", "作业中", "已完成"]),
componentProps: { allowClear: true },
},
{
name: "settlementStatus",
label: "结算状态",
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(["未结算", "已结算"]),
componentProps: { allowClear: true },
},
{
name: "timeout",
label: "作业是否超时",
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(["是", "否"]),
componentProps: { allowClear: true },
},
]}
onFinish={getData}
/>
<Table
toolBarRender={() => (
<SyncExportActions
syncText="同步工单"
syncMessage="已同步派工和实时作业信息"
syncLoading={syncLoading}
requestSync={requestSync}
onSyncSuccess={getData}
exportUrl={`/production-dispatch/${objectType}/cleaning/export`}
exportName={`${personnel ? "人员" : "车辆"}清扫派工`}
getExportParams={() => form.getFieldsValue()}
/>
)}
columns={[
{ title: "工单编号", dataIndex: "id", render: (_, record) => <strong>{record.id}</strong> },
{ title: "作业日期", dataIndex: "workDate" },
{ title: "工单来源", dataIndex: "workSource" },
{ title: "公司名称", dataIndex: "companyName" },
{ title: "作业公司", dataIndex: "workCompany" },
{ title: "作业项目", dataIndex: "workProject" },
{ title: "作业过程", dataIndex: "workProcess" },
{ title: "作业地点", dataIndex: "workPlace" },
{ title: "流机现场巡视员", dataIndex: "inspector" },
{ title: "清扫人员", dataIndex: "cleaningPeople" },
{ title: "标识名称", dataIndex: "markerName" },
{ title: "设备名称", dataIndex: "deviceName" },
{ title: "工单说明", dataIndex: "description" },
{
title: "工单状态",
dataIndex: "orderStatus",
render: (_, record) => (
<Tag color={orderStatusColor[record.orderStatus]}>{record.orderStatus}</Tag>
),
},
{ title: "结算状态", dataIndex: "settlementStatus" },
{
title: "作业是否超时",
dataIndex: "timeout",
render: (_, record) => <Tag color={record.timeout === "是" ? "red" : "green"}>{record.timeout}</Tag>,
},
{ title: "评价", dataIndex: "evaluation" },
{
title: "审批归属",
dataIndex: "approvalOwner",
render: (_, record) => (
<Tag color={record.approvalOwner === "双控系统审批" ? "blue" : "cyan"}>{record.approvalOwner}</Tag>
),
},
{ title: "当前审批角色", dataIndex: "currentRole" },
{
title: "审批状态",
dataIndex: "approvalStatus",
render: (_, record) => <Tag color={statusColor[record.approvalStatus]}>{record.approvalStatus}</Tag>,
},
{
title: "操作",
fixed: "right",
width: 150,
render: (_, record) => {
const canApprove = record.approvalOwner === "双控系统审批"
&& ["待审", "审核中"].includes(record.approvalStatus)
&& !/未配置|未匹配/.test(`${record.currentRole} ${record.cleaningPeople}`);
return (
<Space>
<Button type="link" onClick={() => openDetailModal(record)}>详情</Button>
{canApprove && <Button type="link" onClick={() => openDetailModal(record, true)}>审批</Button>}
</Space>
);
},
},
]}
{...tableProps}
/>
{detailModalOpen && (
<DispatchDetailModal
approvalMode={approvalMode}
approvalLoading={approvalLoading}
businessType="cleaning"
objectType={objectType}
record={currentRecord}
requestApproval={requestApproval}
onClose={closeDetailModal}
onSuccess={() => {
closeDetailModal();
getData();
}}
/>
)}
</Page>
);
}

View File

@ -0,0 +1,220 @@
import { Button, Space, Tag } from "antd";
import { useState } from "react";
import Page from "zy-react-library/components/Page";
import Search from "zy-react-library/components/Search";
import Table from "zy-react-library/components/Table";
import { FORM_ITEM_RENDER_ENUM } from "zy-react-library/enum/formItemRender";
import useTable from "zy-react-library/hooks/useTable";
import DispatchDetailModal from "~/components/DispatchDetailModal";
import styles from "~/components/ManagementList/index.module.less";
import SummaryMetrics from "~/components/SummaryMetrics";
import SyncExportActions from "~/components/SyncExportActions";
const statusColor = {
待审: "gold",
审核中: "orange",
已通过: "green",
已驳回: "red",
已下达: "green",
};
const selectItems = values => [...new Set(values)].map(value => ({ bianma: value, name: value }));
export default function ProductionDispatchList(props) {
const {
approvalLoading,
listRequest,
objectType,
requestApproval,
requestSync,
syncLoading,
} = props;
const [form] = Search.useForm();
const [currentRecord, setCurrentRecord] = useState({});
const [detailModalOpen, setDetailModalOpen] = useState(false);
const [approvalMode, setApprovalMode] = useState(false);
const { tableProps, getData } = useTable(listRequest, { form });
const personnel = objectType === "personnel";
const carUnitItems = personnel
? ["杂货公司东港作业部", "东港业务部", "西港业务部"]
: ["西港业务部", "东港业务部"];
const currentRoleItems = personnel
? ["港务公司计划员审核", "审批角色未配置", "已归档"]
: ["流机调度员下达", "港务公司业务部长审核", "已归档"];
const currentList = tableProps.dataSource || [];
const pendingCount = currentList.filter(item => ["待审", "审核中"].includes(item.approvalStatus)).length;
const dualApprovalCount = currentList.filter(item => item.approvalOwner === "双控系统审批").length;
const activeCount = currentList.filter(item => ["待下达", "作业中"].includes(item.orderStatus)).length;
const closeDetailModal = () => {
setApprovalMode(false);
setDetailModalOpen(false);
setCurrentRecord({});
};
const openDetailModal = (record, approve = false) => {
setApprovalMode(approve);
setCurrentRecord(record);
setDetailModalOpen(true);
};
return (
<Page isShowAllAction={false}>
<SummaryMetrics
items={[
{
label: "当前菜单工单",
value: tableProps.pagination?.total || 0,
note: `当前页 ${currentList.length}`,
},
{
label: "双控审批",
value: dualApprovalCount,
note: personnel ? "当前仅展示审批归属与状态" : "仅有权限节点可操作",
type: "blue",
},
{
label: "执行监管",
value: activeCount,
note: personnel ? "位置、轨迹和状态联动展示" : "位置、轨迹和工单状态联动展示",
type: "green",
},
{ label: "待处理", value: pendingCount, note: "包含待审和审核中工单", type: "orange" },
]}
/>
<Search
form={form}
options={[
{ name: "keyword", label: "关键词", placeholder: "计划号、地点、人员、车辆、申请人" },
{
name: "carUnit",
label: "用车单位",
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(carUnitItems),
componentProps: { allowClear: true },
},
{
name: "shift",
label: "白/夜班",
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(["白班", "夜班"]),
componentProps: { allowClear: true },
},
{
name: "approvalStatus",
label: "审批状态",
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(["待审", "审核中", "已通过", "已驳回", "已下达"]),
componentProps: { allowClear: true },
},
{
name: "currentRole",
label: "当前审批角色",
render: FORM_ITEM_RENDER_ENUM.SELECT,
items: selectItems(currentRoleItems),
componentProps: { allowClear: true },
},
]}
onFinish={getData}
/>
<Table
toolBarRender={() => (
<SyncExportActions
syncText="同步工单"
syncMessage="已同步派工和实时作业信息"
syncLoading={syncLoading}
requestSync={requestSync}
onSyncSuccess={getData}
exportUrl={`/production-dispatch/${objectType}/production/export`}
exportName={`${personnel ? "人员" : "车辆"}生产派工`}
getExportParams={() => form.getFieldsValue()}
/>
)}
columns={[
{
title: "计划号",
width: 200,
dataIndex: "planNo",
render: (_, record) => (
<div className={styles.planNo}>
<strong>{record.planNo}</strong>
<span>{record.sourceNo}</span>
</div>
),
},
{ title: "当前审批角色", dataIndex: "currentRole" },
{ title: "用车单位", dataIndex: "carUnit" },
{ title: "申请用车部门", dataIndex: "applyDept" },
{ title: "现场负责部门", dataIndex: "responsibleDept" },
{ title: "现场负责人", dataIndex: "responsiblePerson" },
{ title: "预到位时间", dataIndex: "expectedArrivalTime" },
{ title: "白/夜班", dataIndex: "shift" },
{ title: "作业地点", dataIndex: "workPlace" },
{ title: "到位地点", dataIndex: "arrivalPlace" },
{ title: "作业内容", dataIndex: "workContent" },
{ title: "作业过程", dataIndex: "workProcess" },
{ title: "作业货名", dataIndex: "cargoName" },
{ title: "船名航次", dataIndex: "voyage" },
{ title: "人员姓名", dataIndex: "personnel", hidden: !personnel },
{ title: "人员ID", dataIndex: "personId", hidden: !personnel },
{ title: "申请设备", dataIndex: "requestedEquipment", hidden: personnel },
{ title: "兑现设备", dataIndex: "fulfilledEquipment", hidden: personnel },
{ title: "车牌号", dataIndex: "licenseNo", hidden: personnel },
{ title: "车辆类型", dataIndex: "vehicleType", hidden: personnel },
{ title: "司机", dataIndex: "driver", hidden: personnel },
{ title: "申请人", dataIndex: "applicant" },
{ title: "申请时间", dataIndex: "applyTime" },
{
title: "审批归属",
dataIndex: "approvalOwner",
render: (_, record) => (
<Tag color={record.approvalOwner === "双控系统审批" ? "blue" : "cyan"}>{record.approvalOwner}</Tag>
),
},
{
title: "审批状态",
dataIndex: "approvalStatus",
render: (_, record) => <Tag color={statusColor[record.approvalStatus]}>{record.approvalStatus}</Tag>,
},
{
title: "操作",
fixed: "right",
width: 150,
render: (_, record) => {
const canApprove = record.approvalOwner === "双控系统审批"
&& ["待审", "审核中"].includes(record.approvalStatus)
&& !/未配置|未匹配/.test(`${record.currentRole} ${record.personnel}`);
return (
<Space>
<Button type="link" onClick={() => openDetailModal(record)}>详情</Button>
{canApprove && <Button type="link" onClick={() => openDetailModal(record, true)}>审批</Button>}
</Space>
);
},
},
]}
{...tableProps}
/>
{detailModalOpen && (
<DispatchDetailModal
approvalMode={approvalMode}
approvalLoading={approvalLoading}
businessType="production"
objectType={objectType}
record={currentRecord}
requestApproval={requestApproval}
onClose={closeDetailModal}
onSuccess={() => {
closeDetailModal();
getData();
}}
/>
)}
</Page>
);
}

View File

@ -0,0 +1,9 @@
import CleaningDispatchList from "./CleaningDispatchList";
import ProductionDispatchList from "./ProductionDispatchList";
export default function DispatchList(props) {
if (props.businessType === "production") {
return <ProductionDispatchList {...props} />;
}
return <CleaningDispatchList {...props} />;
}

View File

@ -0,0 +1,109 @@
import { message, Spin } from "antd";
import { useEffect, useState } from "react";
import CesiumMap from "zy-react-library/components/Map/CesiumMap";
import { dynamicLoadCss, dynamicLoadJs } from "zy-react-library/utils";
import styles from "./index.module.less";
import "./index.less";
const CESIUM_SCRIPT_URL = "https://cesium.com/downloads/cesiumjs/releases/1.91/Build/Cesium/Cesium.js";
const CESIUM_STYLE_URL = "https://cesium.com/downloads/cesiumjs/releases/1.91/Build/Cesium/Widgets/widgets.css";
const EMPTY_TRACK_POINTS = [];
let cesiumResourcePromise;
async function loadCesiumResources() {
if (window.Cesium) {
return;
}
if (!cesiumResourcePromise) {
cesiumResourcePromise = (async () => {
if (window?.base?.loadDynamicResource) {
await window.base.loadDynamicResource({
url: CESIUM_SCRIPT_URL,
type: "script",
attr: { type: "text/javascript" },
});
await window.base.loadDynamicResource({
url: CESIUM_STYLE_URL,
type: "link",
attr: { rel: "stylesheet", type: "text/css" },
});
return;
}
await dynamicLoadJs(CESIUM_SCRIPT_URL);
await dynamicLoadCss(CESIUM_STYLE_URL);
})();
}
await cesiumResourcePromise;
}
export default function DispatchMap(props) {
const { latitude, longitude, trackPoints = EMPTY_TRACK_POINTS } = props;
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
let viewer;
const initMap = async () => {
try {
await loadCesiumResources();
if (cancelled) {
return;
}
const map = new CesiumMap();
viewer = map.init().viewer;
const centerLongitude = Number(longitude || window.mapLongitude);
const centerLatitude = Number(latitude || window.mapLatitude);
map.flyTo({ longitude: centerLongitude, latitude: centerLatitude, height: 30000 });
const hasPosition = longitude !== undefined && longitude !== null && longitude !== ""
&& latitude !== undefined && latitude !== null && latitude !== "";
if (hasPosition) {
map.addMarkPoint({ longitude: Number(longitude), latitude: Number(latitude) });
}
const positions = trackPoints
.filter(point => Array.isArray(point) && point.length >= 2)
.map(point => [Number(point[0]), Number(point[1])])
.filter(point => point.every(Number.isFinite))
.map(point => window.Cesium.Cartesian3.fromDegrees(Number(point[0]), Number(point[1])));
if (positions.length > 1) {
viewer.entities.add({
polyline: {
positions,
width: 4,
clampToGround: true,
material: window.Cesium.Color.fromCssColorString("#1677ff"),
},
});
}
}
catch {
message.error("地图加载失败");
}
finally {
if (!cancelled) {
setLoading(false);
}
}
};
initMap();
return () => {
cancelled = true;
if (viewer && !viewer.isDestroyed()) {
viewer.destroy();
}
};
}, [latitude, longitude, trackPoints]);
return (
<div className={styles.mapWrapper}>
<Spin spinning={loading} tip="地图加载中">
<div id="map_container" className={styles.map} />
</Spin>
</div>
);
}

View File

@ -0,0 +1,5 @@
.cesium-viewer-toolbar,
.cesium-viewer-fullscreenContainer,
.cesium-infoBox-visible {
display: none !important;
}

View File

@ -0,0 +1,13 @@
.mapWrapper {
width: 100%;
margin-bottom: 12px;
overflow: hidden;
border: 1px solid #f0f0f0;
border-radius: 2px;
}
.map {
position: relative;
width: 100%;
height: 240px;
}

View File

@ -0,0 +1,9 @@
.planNo {
display: grid;
gap: 3px;
span {
color: #8c8c8c;
font-size: 12px;
}
}

View File

@ -0,0 +1,17 @@
import styles from "./index.module.less";
export default function SummaryMetrics(props) {
const { items } = props;
return (
<section className={styles.metrics}>
{items.map(item => (
<div key={item.label} className={`${styles.metric} ${styles[item.type] || ""}`}>
<span>{item.label}</span>
<strong>{item.value}</strong>
<small>{item.note}</small>
</div>
))}
</section>
);
}

View File

@ -0,0 +1,49 @@
.metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.metric {
display: grid;
gap: 8px;
min-width: 0;
padding: 16px;
border: 1px solid #e5e7eb;
border-top: 3px solid #64748b;
border-radius: 6px;
background: #fff;
span,
small {
color: #667085;
}
span {
font-size: 13px;
font-weight: 600;
}
strong {
color: #1d2939;
font-size: 26px;
line-height: 1;
}
small {
line-height: 1.5;
}
}
.blue {
border-top-color: #1677ff;
}
.green {
border-top-color: #389e0d;
}
.orange {
border-top-color: #d97706;
}

View File

@ -0,0 +1,43 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, message, Space } from "antd";
import ExportIcon from "zy-react-library/components/Icon/ExportIcon";
import useDownloadBlob from "zy-react-library/hooks/useDownloadBlob";
export default function SyncExportActions(props) {
const {
exportName,
exportUrl,
getExportParams,
onSyncSuccess,
requestSync,
syncLoading,
syncMessage,
syncText,
} = props;
const { loading: exportLoading, downloadBlob } = useDownloadBlob();
const handleSync = async () => {
const { success } = await requestSync();
if (success) {
message.success(syncMessage);
onSyncSuccess();
}
};
return (
<Space>
<Button ghost type="primary" icon={<SyncOutlined />} loading={syncLoading} onClick={handleSync}>
{syncText}
</Button>
<Button
ghost
type="primary"
icon={<ExportIcon />}
loading={exportLoading}
onClick={() => downloadBlob(exportUrl, { name: exportName, params: getExportParams() })}
>
导出
</Button>
</Space>
);
}

1
src/components/index.js Normal file
View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,5 @@
/**
* 全局常量定义
*/
export {};

View File

@ -0,0 +1,8 @@
/**
* 全局上下文定义
*/
import React from "react";
// 获取antd全局静态方法
export const InjectContext = React.createContext({});

View File

@ -0,0 +1,10 @@
/**
* 全局数据状态管理模块定义
*/
import { defineNamespace } from "@cqsjjb/jjb-dva-runtime";
export const NS_GLOBAL = defineNamespace("global");
export const NS_DEVICE_MANAGEMENT = defineNamespace("deviceManagement");
export const NS_PERSONNEL_DISPATCH = defineNamespace("personnelDispatch");
export const NS_VEHICLE_DISPATCH = defineNamespace("vehicleDispatch");

56
src/main.js Normal file
View File

@ -0,0 +1,56 @@
import { setJJBCommonAntdMessage } from "@cqsjjb/jjb-common-lib";
import { setup } from "@cqsjjb/jjb-dva-runtime";
import { message } from "antd";
import dayjs from "dayjs";
import { getFileUrlFromServer } from "zy-react-library/utils";
import "dayjs/locale/zh-cn";
require("antd/dist/reset.css");
require("zy-react-library/css/common.less");
if (process.env.NODE_ENV === "development") {
require("~/mock");
}
dayjs.locale("zh-cn");
setJJBCommonAntdMessage(message);
const app = setup();
getFileUrlFromServer();
window.mapLongitude = "119.69457721306945";
window.mapLatitude = "39.940504336846665";
window.mapBaiDuKey = "OElqFYoKiAH8KFtph8ftLKF5NlNrbCUr";
// 非底座环境运行
if (!window.__POWERED_BY_QIANKUN__) {
// 云组件默认依赖
window.__coreLib = {};
window.__coreLib.React = require("react");
window.__coreLib.ReactDOM = require("react-dom");
window.__coreLib.jjbCommonLib = require("@cqsjjb/jjb-common-lib");
}
/**
* @description 挂载
* @param props {{ setGlobalState: ({ rendered: boolean }) => void }}
* @returns {Promise<*>} ''
*/
export const mount = async (props) => {
// 云组件默认依赖
window.__coreLib.React = require("react");
window.__coreLib.ReactDOM = require("react-dom");
window.__coreLib.jjbCommonLib = require("@cqsjjb/jjb-common-lib");
app.mount(props);
};
/**
* @description 卸载
* @param props {object}
* @returns {Promise<*>} ''
*/
export const unmount = async props => app.unmount(props);
/**
* @description 启动
* @param props
*/
export const bootstrap = async props => app.bootstrap(props);

View File

@ -0,0 +1,101 @@
import Mock from "mockjs";
// 当前没有内部设备接口文档,以下数据仅用于还原交互原型,接入真实接口后删除本拦截文件。
const internalDeviceList = [
{
id: "SB-NB-001",
scope: "内部",
deviceNo: "SB-NB-001",
deviceName: "定位终端PT-3107",
deviceType: "定位终端",
unit: "流机公司",
bindingObject: "冀C-3107",
status: "正常",
updateTime: "2026-07-24 10:12",
gpsNo: "GPS-PT-3107",
relatedVehicle: "装载机L-07",
location: "东港3号泊位",
sourceSystem: "流机公司智能调度平台",
},
{
id: "SB-NB-014",
scope: "内部",
deviceNo: "SB-NB-014",
deviceName: "装载机L-07",
deviceType: "车辆",
unit: "流机公司",
bindingObject: "司机李明",
status: "正常",
updateTime: "2026-07-24 10:10",
gpsNo: "GPS-L-07",
relatedVehicle: "冀C-3107",
location: "东港3号泊位",
sourceSystem: "流机公司智能调度平台",
},
{
id: "SB-NB-022",
scope: "内部",
deviceNo: "SB-NB-022",
deviceName: "清扫车QS-22",
deviceType: "车辆",
unit: "清扫二队",
bindingObject: "东港堆场南路",
status: "正常",
updateTime: "2026-07-24 09:58",
gpsNo: "GPS-QS-22",
relatedVehicle: "冀C-QS22",
location: "东港堆场南路",
sourceSystem: "流机公司智能调度平台",
},
{
id: "SB-NB-031",
scope: "内部",
deviceNo: "SB-NB-031",
deviceName: "牵引车T-09",
deviceType: "车辆",
unit: "流机公司",
bindingObject: "郭涛",
status: "正常",
updateTime: "2026-07-24 12:03",
gpsNo: "GPS-T-09",
relatedVehicle: "冀C-6309",
location: "东港5号堆场",
sourceSystem: "流机公司智能调度平台",
},
];
function getRequestBody(options) {
try {
return JSON.parse(options.body || "{}");
}
catch {
return {};
}
}
Mock.mock(/\/production-dispatch\/device\/internal\/list$/, "post", (options) => {
const body = getRequestBody(options);
const keyword = body.keyword?.trim();
const filteredList = internalDeviceList.filter((record) => {
if (keyword && ![
record.deviceNo,
record.deviceName,
record.bindingObject,
].some(value => value.includes(keyword))) {
return false;
}
return ["deviceType", "unit", "status"]
.every(key => !body[key] || record[key] === body[key]);
});
const pageIndex = Number(body.pageIndex || 1);
const pageSize = Number(body.pageSize || 20);
const start = (pageIndex - 1) * pageSize;
return {
success: true,
totalCount: filteredList.length,
data: filteredList.slice(start, start + pageSize),
};
});
Mock.mock(/\/production-dispatch\/device\/internal\/sync$/, "post", () => ({ success: true }));

View File

@ -0,0 +1,86 @@
import Mock from "mockjs";
// 当前没有相关方设备接口文档,以下数据仅用于还原交互原型,接入真实接口后删除本拦截文件。
const partnerDeviceList = [
{
id: "SB-XG-011",
scope: "相关方",
deviceNo: "SB-XG-011",
deviceName: "相关方定位卡R-008",
deviceType: "定位终端",
unit: "相关方清扫一队",
bindingObject: "张敏",
status: "正常",
updateTime: "2026-07-24 09:42",
gpsNo: "GPS-R-008",
relatedVehicle: "-",
location: "隔离封闭区2号门",
sourceSystem: "流机公司智能调度平台",
},
{
id: "SB-XG-019",
scope: "相关方",
deviceNo: "SB-XG-019",
deviceName: "吸污车QS-08",
deviceType: "车辆",
unit: "相关方清扫一队",
bindingObject: "冀C-QS08",
status: "维修",
updateTime: "2026-07-24 09:25",
gpsNo: "GPS-QS-08",
relatedVehicle: "冀C-QS08",
location: "隔离封闭区2号门排水沟",
sourceSystem: "流机公司智能调度平台",
},
{
id: "SB-XG-027",
scope: "相关方",
deviceNo: "SB-XG-027",
deviceName: "洒水车SW-03",
deviceType: "车辆",
unit: "相关方清扫二队",
bindingObject: "冀C-SW03",
status: "停用",
updateTime: "2026-07-24 08:18",
gpsNo: "GPS-SW-03",
relatedVehicle: "冀C-SW03",
location: "西港维修点",
sourceSystem: "流机公司智能调度平台",
},
];
function getRequestBody(options) {
try {
return JSON.parse(options.body || "{}");
}
catch {
return {};
}
}
Mock.mock(/\/production-dispatch\/device\/partner\/list$/, "post", (options) => {
const body = getRequestBody(options);
const keyword = body.keyword?.trim();
const filteredList = partnerDeviceList.filter((record) => {
if (keyword && ![
record.deviceNo,
record.deviceName,
record.bindingObject,
].some(value => value.includes(keyword))) {
return false;
}
return ["deviceType", "unit", "status"]
.every(key => !body[key] || record[key] === body[key]);
});
const pageIndex = Number(body.pageIndex || 1);
const pageSize = Number(body.pageSize || 20);
const start = (pageIndex - 1) * pageSize;
return {
success: true,
totalCount: filteredList.length,
data: filteredList.slice(start, start + pageSize),
};
});
Mock.mock(/\/production-dispatch\/device\/partner\/sync$/, "post", () => ({ success: true }));

20
src/mock/index.js Normal file
View File

@ -0,0 +1,20 @@
import "./deviceManagement/internalDevice";
import "./deviceManagement/partnerDevice";
import "./personnelDispatch/productionDispatch";
import "./personnelDispatch/cleaningDispatch";
import "./vehicleDispatch/productionDispatch";
import "./vehicleDispatch/cleaningDispatch";
// MockJS 1.1.0 不会把 open 后设置的 responseType 传给未命中的原生 XHR导致 Cesium 无法解码地图瓦片。
Object.defineProperty(window.XMLHttpRequest.prototype, "responseType", {
configurable: true,
get() {
return this.custom?.xhr?.responseType || this.custom?.responseType || "";
},
set(value) {
this.custom.responseType = value;
if (this.custom.xhr) {
this.custom.xhr.responseType = value;
}
},
});

View File

@ -0,0 +1,154 @@
import Mock from "mockjs";
// 当前没有人员清扫派工接口文档,以下数据仅用于还原交互原型,接入真实接口后删除本拦截文件。
const cleaningDispatchList = [
{
id: "QSGD-260724-006",
sourceNo: "QSGD-260724-006",
sourceSystem: "流机公司智能调度平台",
businessType: "清扫派工",
objectType: "人员",
dispatchMode: "临时派工",
workDate: "2026-07-24",
workSource: "临时清扫",
companyName: "秦港股份",
workCompany: "相关方清扫一队",
workProject: "清扫",
workProcess: "吸污",
workPlace: "隔离封闭区2号门周边",
inspector: "孙晓",
cleaningPeople: "张敏、李兵、何强、王磊",
markerName: "QS-08",
deviceName: "吸污车",
description: "临时清扫与吸污作业,作业前后需港务公司现场值守人确认",
orderStatus: "待下达",
settlementStatus: "未结算",
timeout: "否",
evaluation: "-",
currentRole: "公务公司审核",
approvalOwner: "双控系统审批",
approvalStatus: "待审",
applicant: "港务值守员",
applyTime: "2026-07-24 09:05",
position: "隔离封闭区2号门",
locationTime: "未开始定位",
onlineStatus: "离线",
trackStatus: "未生成",
progress: "20%",
accessState: "领取任务并持定位卡后可入区",
videoState: "吸污车监控待推送",
approvalRecords: [["2026-07-24 09:07", "派工单同步", "已接收", "等待公务公司审核"]],
},
{
id: "QSGD-260724-012",
sourceNo: "QSGD-260724-012",
sourceSystem: "流机公司智能调度平台",
businessType: "清扫派工",
objectType: "人员",
dispatchMode: "计划派工",
workDate: "2026-07-24",
workSource: "清扫计划",
companyName: "秦港股份",
workCompany: "清扫二队",
workProject: "道路清扫",
workProcess: "机扫巡检",
workPlace: "东港堆场南路",
inspector: "郭涵",
cleaningPeople: "王磊、刘雪",
markerName: "QS-22",
deviceName: "清扫车",
description: "道路机扫及现场巡视",
orderStatus: "已完成",
settlementStatus: "已结算",
timeout: "否",
evaluation: "满意",
currentRole: "已归档",
approvalOwner: "派工系统审批",
approvalStatus: "已通过",
applicant: "清扫计划员",
applyTime: "2026-07-24 06:55",
position: "东港堆场南路",
locationTime: "2026-07-24 09:58:00",
onlineStatus: "在线",
trackStatus: "轨迹可回放",
progress: "100%",
accessState: "任务已结束,人员不可再次入区",
videoState: "视频可回放",
approvalRecords: [["2026-07-24 07:05", "流机调度员下达", "已下达", "派工系统审批完成后同步"]],
},
];
function getRequestBody(options) {
try {
return JSON.parse(options.body || "{}");
}
catch {
return {};
}
}
Mock.mock(/\/production-dispatch\/personnel\/cleaning\/list$/, "post", (options) => {
const body = getRequestBody(options);
const keyword = body.keyword?.trim();
const filteredList = cleaningDispatchList.filter((record) => {
if (keyword && ![
record.id,
record.workPlace,
record.cleaningPeople,
record.deviceName,
].some(value => value.includes(keyword))) {
return false;
}
return ["workSource", "workCompany", "orderStatus", "settlementStatus", "timeout"]
.every(key => !body[key] || record[key] === body[key]);
});
const pageIndex = Number(body.pageIndex || 1);
const pageSize = Number(body.pageSize || 20);
const start = (pageIndex - 1) * pageSize;
return {
success: true,
totalCount: filteredList.length,
data: filteredList.slice(start, start + pageSize),
};
});
Mock.mock(/\/production-dispatch\/personnel\/cleaning\/sync$/, "post", () => ({ success: true }));
Mock.mock(/\/production-dispatch\/personnel\/cleaning\/approval$/, "post", (options) => {
const body = getRequestBody(options);
const record = cleaningDispatchList.find(item => item.id === body.id);
const canApprove = record
&& record.approvalOwner === "双控系统审批"
&& ["待审", "审核中"].includes(record.approvalStatus)
&& !/未配置|未匹配/.test(`${record.currentRole} ${record.cleaningPeople}`);
if (!canApprove) {
return { success: false, errMessage: "当前清扫派工单不可审批" };
}
if (!["PASS", "REJECT"].includes(body.approvalResult)) {
return { success: false, errMessage: "请选择审批结果" };
}
if (body.approvalResult === "REJECT" && !body.rejectReason?.trim()) {
return { success: false, errMessage: "请输入驳回原因" };
}
const approved = body.approvalResult === "PASS";
record.approvalStatus = approved ? "已通过" : "已驳回";
record.rejectReason = approved ? "" : body.rejectReason.trim();
const approvalRecord = [
new Date().toLocaleString("sv-SE", { hour12: false }).slice(0, 16),
record.currentRole,
approved ? "通过" : "驳回",
approved ? "审批通过" : record.rejectReason,
];
const processingRecordIndex = record.approvalRecords.findIndex(item => item[2] === "处理中");
if (processingRecordIndex >= 0) {
record.approvalRecords[processingRecordIndex] = approvalRecord;
}
else {
record.approvalRecords.push(approvalRecord);
}
return { success: true, data: record };
});

View File

@ -0,0 +1,223 @@
import Mock from "mockjs";
// 当前没有人员生产派工接口文档,以下数据仅用于还原交互原型,接入真实接口后删除本拦截文件。
const productionDispatchList = [
{
id: "QG-SC-RY-20260724-001",
sourceNo: "PGD-SC-260724-0815",
sourceSystem: "流机公司智能调度平台",
businessType: "生产派工",
objectType: "人员",
dispatchMode: "自动派工",
planNo: "JH-DG-0724-015",
currentRole: "港务公司计划员审核",
carUnit: "杂货公司东港作业部",
applyDept: "东港生产调度室",
responsibleDept: "东港现场保障组",
responsiblePerson: "王海",
expectedArrivalTime: "2026-07-24 10:00",
shift: "白班",
workPlace: "东港3号泊位",
arrivalPlace: "3号泊位东侧集结点",
workContent: "装卸保障与现场辅助作业",
workProcess: "到位确认、作业执行、反馈记录",
cargoName: "矿石",
voyage: "QHD-0724-A",
personnel: "李明、赵强、刘洋、韩涛",
personId: "DK-RY-10021、DK-RY-10046、DK-RY-10108、DK-RY-10177",
licenseNo: "冀C-3107、冀C-3112",
requestedEquipment: "装载机2台、对讲机6部",
fulfilledEquipment: "装载机L-07、L-12",
applicant: "刘静",
applyTime: "2026-07-24 08:15",
approvalOwner: "双控系统审批",
approvalStatus: "审核中",
orderStatus: "待下达",
position: "东港3号泊位东侧集结点",
longitude: 119.694577,
latitude: 39.940504,
trackPoints: [
[119.6908, 39.9386],
[119.6924, 39.9394],
[119.694577, 39.940504],
],
locationTime: "2026-07-24 09:42:16",
onlineStatus: "在线",
trackStatus: "轨迹已生成",
accessState: "派工有效,可进入隔离封闭区域",
videoState: "移动监控已推送",
progress: "35%",
approvalRecords: [
["2026-07-24 08:20", "公务公司审核", "通过", "用工信息完整"],
["2026-07-24 09:10", "港务公司计划员审核", "处理中", "等待当前节点确认"],
],
},
{
id: "QG-SC-RY-20260724-013",
sourceNo: "PGD-SC-260724-1028",
sourceSystem: "流机公司智能调度平台",
businessType: "生产派工",
objectType: "人员",
dispatchMode: "临时派工",
planNo: "JH-DG-0724-031",
currentRole: "审批角色未配置",
carUnit: "东港业务部",
applyDept: "东港计划室",
responsibleDept: "现场维修配合组",
responsiblePerson: "邓凯",
expectedArrivalTime: "2026-07-24 13:00",
shift: "白班",
workPlace: "东港维修区",
arrivalPlace: "维修区南门",
workContent: "设备检修配合作业",
workProcess: "停机确认、维修配合、验收反馈",
cargoName: "-",
voyage: "-",
personnel: "派工人员未匹配",
personId: "-",
licenseNo: "流机07号装载机",
requestedEquipment: "维修工具包A3",
fulfilledEquipment: "-",
applicant: "陈立",
applyTime: "2026-07-24 10:28",
approvalOwner: "双控系统审批",
approvalStatus: "待审",
orderStatus: "待下达",
position: "东港维修区",
longitude: 119.6819,
latitude: 39.9358,
trackPoints: [],
locationTime: "未开始定位",
onlineStatus: "离线",
trackStatus: "未生成",
accessState: "未完成审批,暂不可入区",
videoState: "未推送",
progress: "8%",
approvalRecords: [["2026-07-24 10:30", "同步校验", "异常", "人员ID未匹配当前审批角色未配置"]],
},
{
id: "QG-SC-RY-20260724-021",
sourceNo: "PGD-SC-260724-1136",
sourceSystem: "流机公司智能调度平台",
businessType: "生产派工",
objectType: "人员",
dispatchMode: "自动派工",
planNo: "JH-XG-0724-041",
currentRole: "已归档",
carUnit: "西港业务部",
applyDept: "西港生产计划室",
responsibleDept: "西港现场保障组",
responsiblePerson: "周强",
expectedArrivalTime: "2026-07-24 14:30",
shift: "白班",
workPlace: "西港主干路",
arrivalPlace: "西港2号门",
workContent: "短倒转运保障",
workProcess: "人员到位、连续作业、结束反馈",
cargoName: "煤炭",
voyage: "XG-0724-C",
personnel: "陈勇、孙凯、马宁",
personId: "DK-RY-10203、DK-RY-10211、DK-RY-10245",
licenseNo: "冀C-5021、冀C-5022、冀C-5025",
requestedEquipment: "对讲机3部",
fulfilledEquipment: "对讲机3部",
applicant: "周强",
applyTime: "2026-07-24 11:36",
approvalOwner: "派工系统审批",
approvalStatus: "已下达",
orderStatus: "作业中",
position: "西港主干路北段",
longitude: 119.6426,
latitude: 39.9284,
trackPoints: [
[119.6378, 39.9256],
[119.6401, 39.9271],
[119.6426, 39.9284],
],
locationTime: "2026-07-24 14:58:41",
onlineStatus: "在线",
trackStatus: "轨迹记录中",
accessState: "派工有效,可进入隔离封闭区域",
videoState: "无移动监控",
progress: "68%",
approvalRecords: [["2026-07-24 12:04", "流机调度员下达", "已下达", "派工系统审批完成后同步"]],
},
];
function getRequestBody(options) {
try {
return JSON.parse(options.body || "{}");
}
catch {
return {};
}
}
Mock.setup({ timeout: "300-600" });
Mock.mock(/\/production-dispatch\/personnel\/production\/list$/, "post", (options) => {
const body = getRequestBody(options);
const keyword = body.keyword?.trim();
const filteredList = productionDispatchList.filter((record) => {
if (keyword && ![
record.planNo,
record.workPlace,
record.personnel,
record.licenseNo,
record.applicant,
].some(value => value.includes(keyword))) {
return false;
}
return ["carUnit", "shift", "approvalStatus", "currentRole"]
.every(key => !body[key] || record[key] === body[key]);
});
const pageIndex = Number(body.pageIndex || 1);
const pageSize = Number(body.pageSize || 20);
const start = (pageIndex - 1) * pageSize;
return {
success: true,
totalCount: filteredList.length,
data: filteredList.slice(start, start + pageSize),
};
});
Mock.mock(/\/production-dispatch\/personnel\/production\/sync$/, "post", () => ({ success: true }));
Mock.mock(/\/production-dispatch\/personnel\/production\/approval$/, "post", (options) => {
const body = getRequestBody(options);
const record = productionDispatchList.find(item => item.id === body.id);
const canApprove = record
&& record.approvalOwner === "双控系统审批"
&& ["待审", "审核中"].includes(record.approvalStatus)
&& !/未配置|未匹配/.test(`${record.currentRole} ${record.personnel}`);
if (!canApprove) {
return { success: false, errMessage: "当前派工单不可审批" };
}
if (!["PASS", "REJECT"].includes(body.approvalResult)) {
return { success: false, errMessage: "请选择审批结果" };
}
if (body.approvalResult === "REJECT" && !body.rejectReason?.trim()) {
return { success: false, errMessage: "请输入驳回原因" };
}
const approved = body.approvalResult === "PASS";
record.approvalStatus = approved ? "已通过" : "已驳回";
record.rejectReason = approved ? "" : body.rejectReason.trim();
const approvalRecord = [
new Date().toLocaleString("sv-SE", { hour12: false }).slice(0, 16),
record.currentRole,
approved ? "通过" : "驳回",
approved ? "审批通过" : record.rejectReason,
];
const processingRecordIndex = record.approvalRecords.findIndex(item => item[2] === "处理中");
if (processingRecordIndex >= 0) {
record.approvalRecords[processingRecordIndex] = approvalRecord;
}
else {
record.approvalRecords.push(approvalRecord);
}
return { success: true, data: record };
});

View File

@ -0,0 +1,157 @@
import Mock from "mockjs";
// 当前没有车辆清扫派工接口文档,以下数据仅用于还原交互原型,接入真实接口后删除本拦截文件。
const cleaningDispatchList = [
{
id: "QSGD-260724-018",
sourceNo: "QSGD-260724-018",
sourceSystem: "流机公司智能调度平台",
businessType: "清扫派工",
objectType: "车辆",
dispatchMode: "计划派工",
workDate: "2026-07-24",
workSource: "清扫计划",
companyName: "秦港股份",
workCompany: "清扫二队",
workProject: "道路清扫",
workProcess: "机扫",
workPlace: "东港堆场南路至5号门",
inspector: "郭涵",
cleaningPeople: "王磊、刘雪",
markerName: "冀C-QS22",
deviceName: "清扫车QS-22",
description: "按计划完成道路清扫",
orderStatus: "作业中",
settlementStatus: "未结算",
timeout: "否",
evaluation: "-",
currentRole: "流机调度员下达",
approvalOwner: "派工系统审批",
approvalStatus: "已下达",
applicant: "清扫计划员",
applyTime: "2026-07-24 07:20",
position: "东港堆场南路",
locationTime: "2026-07-24 10:16:29",
onlineStatus: "在线",
trackStatus: "轨迹记录中",
progress: "74%",
accessState: "派工有效,可进入隔离封闭区域",
videoState: "移动监控已推送",
approvalRecords: [["2026-07-24 07:36", "流机调度员下达", "已下达", "派工系统审批完成后同步"]],
},
{
id: "QSGD-260724-031",
sourceNo: "QSGD-260724-031",
sourceSystem: "流机公司智能调度平台",
businessType: "清扫派工",
objectType: "车辆",
dispatchMode: "临时派工",
workDate: "2026-07-24",
workSource: "临时清扫",
companyName: "秦港股份",
workCompany: "相关方清扫一队",
workProject: "吸污",
workProcess: "吸污",
workPlace: "隔离封闭区2号门排水沟",
inspector: "孙晓",
cleaningPeople: "张敏、李兵",
markerName: "冀C-QS08",
deviceName: "吸污车QS-08",
description: "排水沟吸污,作业后需现场确认",
orderStatus: "待下达",
settlementStatus: "未结算",
timeout: "否",
evaluation: "-",
currentRole: "港务公司现场值守人确认",
approvalOwner: "双控系统审批",
approvalStatus: "审核中",
applicant: "港务值守员",
applyTime: "2026-07-24 10:05",
position: "隔离封闭区2号门排水沟",
locationTime: "2026-07-24 10:26:18",
onlineStatus: "在线",
trackStatus: "轨迹已生成",
progress: "28%",
accessState: "等待作业前现场确认",
videoState: "移动监控已推送",
approvalRecords: [
["2026-07-24 10:07", "公务公司审核", "通过", "临时清扫申请有效"],
["2026-07-24 10:22", "港务公司现场值守人确认", "处理中", "等待作业前确认"],
],
},
];
function getRequestBody(options) {
try {
return JSON.parse(options.body || "{}");
}
catch {
return {};
}
}
Mock.mock(/\/production-dispatch\/vehicle\/cleaning\/list$/, "post", (options) => {
const body = getRequestBody(options);
const keyword = body.keyword?.trim();
const filteredList = cleaningDispatchList.filter((record) => {
if (keyword && ![
record.id,
record.workPlace,
record.cleaningPeople,
record.deviceName,
].some(value => value.includes(keyword))) {
return false;
}
return ["workSource", "workCompany", "orderStatus", "settlementStatus", "timeout"]
.every(key => !body[key] || record[key] === body[key]);
});
const pageIndex = Number(body.pageIndex || 1);
const pageSize = Number(body.pageSize || 20);
const start = (pageIndex - 1) * pageSize;
return {
success: true,
totalCount: filteredList.length,
data: filteredList.slice(start, start + pageSize),
};
});
Mock.mock(/\/production-dispatch\/vehicle\/cleaning\/sync$/, "post", () => ({ success: true }));
Mock.mock(/\/production-dispatch\/vehicle\/cleaning\/approval$/, "post", (options) => {
const body = getRequestBody(options);
const record = cleaningDispatchList.find(item => item.id === body.id);
const canApprove = record
&& record.approvalOwner === "双控系统审批"
&& ["待审", "审核中"].includes(record.approvalStatus)
&& !/未配置|未匹配/.test(`${record.currentRole} ${record.cleaningPeople}`);
if (!canApprove) {
return { success: false, errMessage: "当前车辆清扫派工单不可审批" };
}
if (!["PASS", "REJECT"].includes(body.approvalResult)) {
return { success: false, errMessage: "请选择审批结果" };
}
if (body.approvalResult === "REJECT" && !body.rejectReason?.trim()) {
return { success: false, errMessage: "请输入驳回原因" };
}
const approved = body.approvalResult === "PASS";
record.approvalStatus = approved ? "已通过" : "已驳回";
record.rejectReason = approved ? "" : body.rejectReason.trim();
const approvalRecord = [
new Date().toLocaleString("sv-SE", { hour12: false }).slice(0, 16),
record.currentRole,
approved ? "通过" : "驳回",
approved ? "审批通过" : record.rejectReason,
];
const processingRecordIndex = record.approvalRecords.findIndex(item => item[2] === "处理中");
if (processingRecordIndex >= 0) {
record.approvalRecords[processingRecordIndex] = approvalRecord;
}
else {
record.approvalRecords.push(approvalRecord);
}
return { success: true, data: record };
});

View File

@ -0,0 +1,211 @@
import Mock from "mockjs";
// 当前没有车辆生产派工接口文档,以下数据仅用于还原交互原型,接入真实接口后删除本拦截文件。
const productionDispatchList = [
{
id: "QG-SC-CL-20260724-002",
sourceNo: "PGD-SC-260724-0832",
planNo: "JH-XG-0724-022",
sourceSystem: "流机公司智能调度平台",
businessType: "生产派工",
objectType: "车辆",
dispatchMode: "自动派工",
currentRole: "流机调度员下达",
carUnit: "西港业务部",
applyDept: "西港生产计划室",
responsibleDept: "西港现场保障组",
responsiblePerson: "李海军",
expectedArrivalTime: "2026-07-24 09:30",
shift: "白班",
workPlace: "西港主干路",
arrivalPlace: "西港2号门",
workContent: "短倒转运保障",
workProcess: "车辆到位、连续作业、结束反馈",
cargoName: "煤炭",
voyage: "XG-0724-C",
requestedEquipment: "自卸车3台",
fulfilledEquipment: "自卸车X-21、X-22、X-25",
personnel: "陈勇、孙凯、马宁",
personId: "DK-RY-10203、DK-RY-10211、DK-RY-10245",
licenseNo: "冀C-5021、冀C-5022、冀C-5025",
vehicleType: "自卸车",
driver: "陈勇、孙凯、马宁",
applicant: "周强",
applyTime: "2026-07-24 08:32",
approvalOwner: "派工系统审批",
approvalStatus: "已下达",
orderStatus: "作业中",
position: "西港主干路北段",
locationTime: "2026-07-24 10:24:08",
onlineStatus: "在线",
trackStatus: "轨迹记录中",
progress: "68%",
accessState: "派工有效,可进入隔离封闭区域",
videoState: "移动监控已推送",
approvalRecords: [["2026-07-24 08:54", "流机调度员下达", "已下达", "派工系统审批完成后同步"]],
},
{
id: "QG-SC-CL-20260724-017",
sourceNo: "PGD-SC-260724-1048",
planNo: "JH-DG-0724-038",
sourceSystem: "流机公司智能调度平台",
businessType: "生产派工",
objectType: "车辆",
dispatchMode: "临时派工",
currentRole: "港务公司业务部长审核",
carUnit: "东港业务部",
applyDept: "东港生产调度室",
responsibleDept: "东港现场保障组",
responsiblePerson: "韩冰",
expectedArrivalTime: "2026-07-24 12:30",
shift: "白班",
workPlace: "东港5号堆场",
arrivalPlace: "5号堆场西门",
workContent: "倒运车辆临时支援",
workProcess: "派车确认、安全交底、作业反馈",
cargoName: "钢材",
voyage: "DG-0724-H",
requestedEquipment: "牵引车2台、平板车2台",
fulfilledEquipment: "牵引车T-09、T-12",
personnel: "郭涛、吴建",
personId: "DK-RY-10305、DK-RY-10361",
licenseNo: "冀C-6309、冀C-6312",
vehicleType: "牵引车",
driver: "郭涛、吴建",
applicant: "赵一鸣",
applyTime: "2026-07-24 10:48",
approvalOwner: "双控系统审批",
approvalStatus: "审核中",
orderStatus: "待下达",
position: "东港5号堆场西门",
locationTime: "2026-07-24 12:06:22",
onlineStatus: "在线",
trackStatus: "轨迹已生成",
progress: "41%",
accessState: "审批中,等待下达后放行",
videoState: "移动监控待推送",
approvalRecords: [
["2026-07-24 10:55", "公务公司审核", "通过", "车辆资质有效"],
["2026-07-24 11:40", "港务公司计划员审核", "通过", "作业窗口符合计划"],
["2026-07-24 12:02", "港务公司业务部长审核", "处理中", "等待当前节点确认"],
],
},
{
id: "QG-SC-CL-20260724-029",
sourceNo: "PGD-SC-260724-1510",
planNo: "JH-XG-0724-068",
sourceSystem: "流机公司智能调度平台",
businessType: "生产派工",
objectType: "车辆",
dispatchMode: "自动派工",
currentRole: "已归档",
carUnit: "西港业务部",
applyDept: "西港生产计划室",
responsibleDept: "西港现场保障组",
responsiblePerson: "曹俊",
expectedArrivalTime: "2026-07-24 16:00",
shift: "夜班",
workPlace: "西港煤场南路",
arrivalPlace: "煤场南路检查口",
workContent: "夜班倒运保障",
workProcess: "车辆入区、转运、离区确认",
cargoName: "煤炭",
voyage: "XG-0724-N",
requestedEquipment: "自卸车4台",
fulfilledEquipment: "自卸车X-31、X-32、X-33、X-36",
personnel: "沈旭、李杨、贾鹏、刘帅",
personId: "DK-RY-10511、DK-RY-10512、DK-RY-10515、DK-RY-10519",
licenseNo: "冀C-5031、冀C-5032、冀C-5033、冀C-5036",
vehicleType: "自卸车",
driver: "沈旭、李杨、贾鹏、刘帅",
applicant: "马琳",
applyTime: "2026-07-24 15:10",
approvalOwner: "派工系统审批",
approvalStatus: "已通过",
orderStatus: "待下达",
position: "西港煤场南路检查口",
locationTime: "未开始定位",
onlineStatus: "离线",
trackStatus: "未生成",
progress: "0%",
accessState: "待派工系统下达",
videoState: "未推送",
approvalRecords: [["2026-07-24 15:22", "流机计划员审核", "已通过", "派工系统审批完成后同步"]],
},
];
function getRequestBody(options) {
try {
return JSON.parse(options.body || "{}");
}
catch {
return {};
}
}
Mock.mock(/\/production-dispatch\/vehicle\/production\/list$/, "post", (options) => {
const body = getRequestBody(options);
const keyword = body.keyword?.trim();
const filteredList = productionDispatchList.filter((record) => {
if (keyword && ![
record.planNo,
record.workPlace,
record.personnel,
record.licenseNo,
record.applicant,
].some(value => value.includes(keyword))) {
return false;
}
return ["carUnit", "shift", "approvalStatus", "currentRole"]
.every(key => !body[key] || record[key] === body[key]);
});
const pageIndex = Number(body.pageIndex || 1);
const pageSize = Number(body.pageSize || 20);
const start = (pageIndex - 1) * pageSize;
return {
success: true,
totalCount: filteredList.length,
data: filteredList.slice(start, start + pageSize),
};
});
Mock.mock(/\/production-dispatch\/vehicle\/production\/sync$/, "post", () => ({ success: true }));
Mock.mock(/\/production-dispatch\/vehicle\/production\/approval$/, "post", (options) => {
const body = getRequestBody(options);
const record = productionDispatchList.find(item => item.id === body.id);
const canApprove = record
&& record.approvalOwner === "双控系统审批"
&& ["待审", "审核中"].includes(record.approvalStatus)
&& !/未配置|未匹配/.test(`${record.currentRole} ${record.personnel}`);
if (!canApprove) {
return { success: false, errMessage: "当前车辆派工单不可审批" };
}
if (!["PASS", "REJECT"].includes(body.approvalResult)) {
return { success: false, errMessage: "请选择审批结果" };
}
if (body.approvalResult === "REJECT" && !body.rejectReason?.trim()) {
return { success: false, errMessage: "请输入驳回原因" };
}
const approved = body.approvalResult === "PASS";
record.approvalStatus = approved ? "已通过" : "已驳回";
record.rejectReason = approved ? "" : body.rejectReason.trim();
const approvalRecord = [
new Date().toLocaleString("sv-SE", { hour12: false }).slice(0, 16),
record.currentRole,
approved ? "通过" : "驳回",
approved ? "审批通过" : record.rejectReason,
];
const processingRecordIndex = record.approvalRecords.findIndex(item => item[2] === "处理中");
if (processingRecordIndex >= 0) {
record.approvalRecords[processingRecordIndex] = approvalRecord;
}
else {
record.approvalRecords.push(approvalRecord);
}
return { success: true, data: record };
});

View File

@ -0,0 +1,17 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import DeviceManagementList from "~/components/DeviceManagementList";
import { NS_DEVICE_MANAGEMENT } from "~/enumerate/namespace";
function InternalDevice(props) {
return (
<DeviceManagementList
scope="internal"
listRequest={props["internalDeviceList"]}
requestSync={props["internalDeviceSync"]}
syncLoading={props.deviceManagement?.internalDeviceSyncLoading}
/>
);
}
export default Connect([NS_DEVICE_MANAGEMENT], true)(InternalDevice);

View File

@ -0,0 +1,17 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import DeviceManagementList from "~/components/DeviceManagementList";
import { NS_DEVICE_MANAGEMENT } from "~/enumerate/namespace";
function PartnerDevice(props) {
return (
<DeviceManagementList
scope="partner"
listRequest={props["partnerDeviceList"]}
requestSync={props["partnerDeviceSync"]}
syncLoading={props.deviceManagement?.partnerDeviceSyncLoading}
/>
);
}
export default Connect([NS_DEVICE_MANAGEMENT], true)(PartnerDevice);

View File

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

View File

@ -0,0 +1,31 @@
import { ImportCore } from "@cqsjjb/jjb-common-decorator/module";
import React from "react";
export default class Entry extends React.Component {
state = {
Component: undefined,
};
componentDidMount() {
if (process.env.app.appKey) {
ImportCore({
name: "$",
from: "https://cdn.cqjjb.cn/jcloud/use/plugin/b31c9840a57f11ef91cf7f3cabbb7484/latest",
}).then((res) => {
if (res.status) {
this.setState({ Component: res.module?.default });
}
});
}
}
render() {
const { Component } = this.state;
return (Component && process.env.app.appKey) && (
<Component
detail={{ componentKey: process.env.app.appKey }}
appKey={process.env.app.appKey}
/>
);
}
}

View File

@ -0,0 +1,20 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import DispatchList from "~/components/DispatchList";
import { NS_PERSONNEL_DISPATCH } from "~/enumerate/namespace";
function CleaningDispatch(props) {
return (
<DispatchList
businessType="cleaning"
objectType="personnel"
listRequest={props["cleaningDispatchList"]}
requestApproval={props["cleaningDispatchApproval"]}
requestSync={props["cleaningDispatchSync"]}
approvalLoading={props.personnelDispatch?.cleaningDispatchApprovalLoading}
syncLoading={props.personnelDispatch?.cleaningDispatchSyncLoading}
/>
);
}
export default Connect([NS_PERSONNEL_DISPATCH], true)(CleaningDispatch);

View File

@ -0,0 +1,20 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import DispatchList from "~/components/DispatchList";
import { NS_PERSONNEL_DISPATCH } from "~/enumerate/namespace";
function ProductionDispatch(props) {
return (
<DispatchList
businessType="production"
objectType="personnel"
listRequest={props["productionDispatchList"]}
requestApproval={props["productionDispatchApproval"]}
requestSync={props["productionDispatchSync"]}
approvalLoading={props.personnelDispatch?.productionDispatchApprovalLoading}
syncLoading={props.personnelDispatch?.productionDispatchSyncLoading}
/>
);
}
export default Connect([NS_PERSONNEL_DISPATCH], true)(ProductionDispatch);

View File

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

View File

@ -0,0 +1,20 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import DispatchList from "~/components/DispatchList";
import { NS_VEHICLE_DISPATCH } from "~/enumerate/namespace";
function CleaningDispatch(props) {
return (
<DispatchList
businessType="cleaning"
objectType="vehicle"
listRequest={props["cleaningDispatchList"]}
requestApproval={props["cleaningDispatchApproval"]}
requestSync={props["cleaningDispatchSync"]}
approvalLoading={props.vehicleDispatch?.cleaningDispatchApprovalLoading}
syncLoading={props.vehicleDispatch?.cleaningDispatchSyncLoading}
/>
);
}
export default Connect([NS_VEHICLE_DISPATCH], true)(CleaningDispatch);

View File

@ -0,0 +1,20 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import DispatchList from "~/components/DispatchList";
import { NS_VEHICLE_DISPATCH } from "~/enumerate/namespace";
function ProductionDispatch(props) {
return (
<DispatchList
businessType="production"
objectType="vehicle"
listRequest={props["productionDispatchList"]}
requestApproval={props["productionDispatchApproval"]}
requestSync={props["productionDispatchSync"]}
approvalLoading={props.vehicleDispatch?.productionDispatchApprovalLoading}
syncLoading={props.vehicleDispatch?.productionDispatchSyncLoading}
/>
);
}
export default Connect([NS_VEHICLE_DISPATCH], true)(ProductionDispatch);

View File

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

View File

@ -0,0 +1,100 @@
import { ImportCore } from "@cqsjjb/jjb-common-decorator/module";
import { theme as antdTheme, App, ConfigProvider } from "antd";
import language from "antd/locale/zh_CN";
import React from "react";
import { InjectContext } from "~/enumerate/context";
export default class Container extends React.Component {
state = window?.base?.themeConfig || {
algorithm: window.process.env.app.antd.algorithm,
borderRadius: window.process.env.app.antd.borderRadius,
colorPrimary: window.process.env.app.antd.colorPrimary,
};
get token() {
const {
colorPrimary,
borderRadius,
} = this.state;
return {
fontFamily: window.process.env.app.antd.fontFamily,
colorPrimary,
borderRadius,
};
}
get algorithm() {
return antdTheme[this.state.algorithm];
}
componentDidMount() {
if (window.__IN_BASE__) {
// eslint-disable-next-line react-web-api/no-leaked-event-listener
window.base.addEventListener("EVENT_THEME_CONTROL", (e) => {
const config = e.data;
this.setState({ [config.field]: config.value });
});
}
}
render() {
return (
<ConfigProvider
theme={{
token: this.token,
algorithm: this.algorithm,
}}
locale={language}
prefixCls={window.process.env.app.antd["ant-prefix"]}
>
<App style={{ height: "100%" }}>
<AppMiddle {...this.props} />
</App>
</ConfigProvider>
);
}
}
function AppMiddle(props) {
return (
<InjectContext.Provider value={App.useApp()}>
{process.env.NODE_ENV === "development"
? props.children
: (
<Interceptor>
{props.children}
</Interceptor>
)}
</InjectContext.Provider>
);
}
class Interceptor extends React.Component {
state = {
Component: undefined,
};
componentDidMount() {
if (process.env.app.appKey) {
ImportCore({
name: "$",
from: "https://cdn.cqjjb.cn/jcloud/use/plugin/b31c9840a57f11ef91cf7f3cabbb7484/latest",
}).then(async (res) => {
if (res.status) {
this.setState({ Component: res.module?.PageCover });
}
});
}
}
render() {
const { Component } = this.state;
return (Component && process.env.app.appKey && process.env.NODE_ENV === "development")
? (
<Component appKey={process.env.app.appKey}>
{this.props.children}
</Component>
)
: this.props.children;
}
}

8
src/pages/index.js Normal file
View File

@ -0,0 +1,8 @@
export default function () {
return (
<h1>
底座微应用模板技术文档
<a rel="noreferrer noopener" target="_blank" href="https://www.yuque.com/buhangjiecheshen-ymbtb/qc0093/gxdun1dphetcurko">https://www.yuque.com/buhangjiecheshen-ymbtb/qc0093/gxdun1dphetcurko</a>
</h1>
);
}

16
webstorm.config.js Normal file
View File

@ -0,0 +1,16 @@
"use strict";
const path = require("node:path");
function resolve(dir) {
return path.join(__dirname, ".", dir);
}
module.exports = {
context: path.resolve(__dirname, "./"),
resolve: {
extensions: [".js"],
alias: {
"~": resolve("src/"),
},
},
};