├── .dockerignore ├── .vscode └── settings.json ├── public ├── favicon.ico └── assets │ └── images │ ├── login_bg.png │ ├── login_user.png │ ├── site_name.png │ ├── site_config.png │ ├── login_form_bg.png │ ├── login_password.png │ └── site_mode.svg ├── .eslintignore ├── src ├── assets │ └── images │ │ ├── login_bg.png │ │ ├── login_user.png │ │ ├── site_name.png │ │ ├── login_form_bg.png │ │ ├── site_config.png │ │ ├── login_password.png │ │ └── site_mode.svg ├── components │ ├── OnlineStatus │ │ ├── index.less │ │ └── index.tsx │ ├── LonLatUnit │ │ └── index.tsx │ ├── SelectLang │ │ ├── index.less │ │ └── index.tsx │ ├── CardList │ │ ├── index.less │ │ └── index.tsx │ ├── HeaderDropdown │ │ ├── index.less │ │ └── index.tsx │ ├── Country │ │ ├── index.tsx │ │ └── renderHelper.tsx │ ├── BaseContainer │ │ └── index.tsx │ ├── FormItem │ │ └── index.tsx │ ├── RightContent │ │ ├── index.tsx │ │ ├── index.less │ │ └── AvatarDropdown.tsx │ ├── FormField │ │ └── index.tsx │ ├── ConfirmModal │ │ └── index.ts │ ├── typings.d.ts │ ├── CollisionWarningList │ │ └── index.tsx │ ├── CreateSendModal │ │ └── index.tsx │ ├── ParameterInfo │ │ └── index.tsx │ └── BaseProTable │ │ └── index.tsx ├── services │ ├── index.ts │ ├── event │ │ ├── dnpw.ts │ │ ├── sds.ts │ │ ├── clc.ts │ │ ├── icw.ts │ │ ├── rsm.ts │ │ └── rsi.ts │ ├── api.ts │ ├── system │ │ └── edge.ts │ ├── errorStatus.ts │ ├── config │ │ ├── query.ts │ │ ├── log.ts │ │ ├── maintenance.ts │ │ ├── business.ts │ │ └── map.ts │ ├── device │ │ ├── radar.ts │ │ ├── camera.ts │ │ ├── model.ts │ │ ├── spat.ts │ │ ├── lidar.ts │ │ └── device.ts │ └── request.ts ├── pages │ ├── maintenanceManagement │ │ ├── mapConfig │ │ │ ├── ConfigDetails │ │ │ │ └── index.less │ │ │ ├── ConfigPreview │ │ │ │ └── index.less │ │ │ └── ConfigList │ │ │ │ └── index.tsx │ │ ├── businessConfig │ │ │ ├── ConfigList │ │ │ │ └── components │ │ │ │ │ ├── CreateRSUConfigModal │ │ │ │ │ └── index.less │ │ │ │ │ └── SelectDeviceModal.tsx │ │ │ └── ConfigDetails │ │ │ │ └── index.tsx │ │ ├── LogConfig │ │ │ └── index.tsx │ │ └── RSUInfoQuery │ │ │ └── InfoQueryList │ │ │ └── components │ │ │ └── CreateQueryModal.tsx │ ├── deviceManagement │ │ ├── RSUManagement │ │ │ ├── DeviceList │ │ │ │ ├── index.less │ │ │ │ ├── index.tsx │ │ │ │ └── components │ │ │ │ │ └── NotRegisteredList │ │ │ │ │ └── index.tsx │ │ │ └── DeviceDetails │ │ │ │ └── components │ │ │ │ ├── CPULineChart.tsx │ │ │ │ ├── MemoryLineChart.tsx │ │ │ │ ├── DiskLineChart.tsx │ │ │ │ └── NetworkLineChart.tsx │ │ └── RSUModelManagement │ │ │ ├── index.tsx │ │ │ └── components │ │ │ └── CreateModelModal.tsx │ ├── 404.tsx │ ├── eventManagement │ │ ├── vulnerableRoadUser │ │ │ ├── VRUCWDetails │ │ │ │ └── index.tsx │ │ │ └── VRUCWList │ │ │ │ └── index.tsx │ │ ├── intersectionCollisionWarning │ │ │ ├── ICWDetails │ │ │ │ └── index.tsx │ │ │ └── ICWList │ │ │ │ └── index.tsx │ │ ├── SensorDataSharing │ │ │ └── index.tsx │ │ ├── CooperativeLaneChange │ │ │ └── index.tsx │ │ ├── DoNotPassWarning │ │ │ └── index.tsx │ │ ├── roadsideSafetyMessage │ │ │ ├── RSMDetails │ │ │ │ └── index.tsx │ │ │ └── RSMList │ │ │ │ └── index.tsx │ │ └── roadSideInformation │ │ │ └── RSIList │ │ │ └── index.tsx │ ├── systemConfiguration │ │ └── EdgeSiteConfig │ │ │ ├── index.less │ │ │ └── components │ │ │ ├── UpdateSiteNameModal.tsx │ │ │ └── UpdateSiteConfigModal.tsx │ └── user │ │ └── Login │ │ └── index.tsx ├── manifest.json ├── access.ts ├── utils │ ├── storage.test.js │ ├── storage.ts │ ├── i18n.ts │ └── index.ts ├── shims.ant-pro-columns.d.ts ├── typings.d.ts ├── service-worker.js └── app.tsx ├── .prettierrc.js ├── .stylelintrc.js ├── e2e ├── utils │ ├── detail.ts │ ├── index.ts │ ├── road_simulator.ts │ ├── global.ts │ └── form.ts ├── storage │ └── userStorageState.json ├── api │ └── login.spec.ts ├── login.spec.ts ├── examples │ └── example.spec.ts └── pages │ ├── login.spec.ts │ ├── event │ ├── Dnpw.spec.ts │ ├── Clc.spec.ts │ ├── Sds.spec.ts │ ├── Vrucw.spec.ts │ ├── Icw.spec.ts │ ├── Rsi.spec.ts │ └── Rsm.spec.ts │ ├── maintenance │ ├── Query.spec.ts │ ├── Log.spec.ts │ ├── Maintenance.spec.ts │ ├── Map.spec.ts │ └── Business.spec.ts │ └── device │ ├── Rsumodel.spec.ts │ ├── Camera.spec.ts │ └── Ladar.spec.ts ├── tests ├── setupTests.js └── run-tests.js ├── jsconfig.json ├── docs ├── technology-stack.md ├── edgeview.md ├── i18n-introduction.md ├── how-to-develop.md └── catalog-tree.md ├── jest.config.js ├── .github ├── workflows │ ├── commitlint.yml │ ├── dprint-check-action.yml │ ├── lint.yml │ ├── sync.yml │ ├── unit-test.yml │ ├── test-e2e-aio.yml │ └── ci.yml └── ISSUE_TEMPLATE │ ├── feature-request.md │ └── bug-report.md ├── .editorconfig ├── .prettierignore ├── dprint.json ├── config ├── proxy.ts ├── defaultSettings.ts ├── config.dev.ts └── config.ts ├── deploy ├── init_env.sh ├── edgeview.conf └── nginx.conf ├── Dockerfile ├── tests-examples └── examples │ └── example.spec.ts ├── .gitignore ├── Gruntfile.js ├── .eslintrc.js ├── tsconfig.json ├── mock ├── log.ts ├── maintenance.ts ├── query.ts ├── user.ts ├── model.ts ├── mapConfig.ts ├── rsm.ts ├── eventInfo.ts ├── radar.ts ├── camera.ts ├── device.ts └── parameterConfig.ts └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": ["src/locales"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | .history 5 | public 6 | dist 7 | .umi 8 | mock -------------------------------------------------------------------------------- /src/assets/images/login_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/src/assets/images/login_bg.png -------------------------------------------------------------------------------- /src/assets/images/login_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/src/assets/images/login_user.png -------------------------------------------------------------------------------- /src/assets/images/site_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/src/assets/images/site_name.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.prettier, 5 | }; 6 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.stylelint, 5 | }; 6 | -------------------------------------------------------------------------------- /public/assets/images/login_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/public/assets/images/login_bg.png -------------------------------------------------------------------------------- /public/assets/images/login_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/public/assets/images/login_user.png -------------------------------------------------------------------------------- /public/assets/images/site_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/public/assets/images/site_name.png -------------------------------------------------------------------------------- /src/assets/images/login_form_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/src/assets/images/login_form_bg.png -------------------------------------------------------------------------------- /src/assets/images/site_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/src/assets/images/site_config.png -------------------------------------------------------------------------------- /public/assets/images/site_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/public/assets/images/site_config.png -------------------------------------------------------------------------------- /src/assets/images/login_password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/src/assets/images/login_password.png -------------------------------------------------------------------------------- /public/assets/images/login_form_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/public/assets/images/login_form_bg.png -------------------------------------------------------------------------------- /public/assets/images/login_password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-v2x/edgeview/HEAD/public/assets/images/login_password.png -------------------------------------------------------------------------------- /src/components/OnlineStatus/index.less: -------------------------------------------------------------------------------- 1 | .dots { 2 | display: inline-block; 3 | width: 8px; 4 | height: 8px; 5 | border-radius: 50%; 6 | } 7 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | // API 更新时间: 4 | // API 唯一标识: 5 | import * as api from './api'; 6 | export default { 7 | api, 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/LonLatUnit/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LonLatUnit: React.FC<{ data: number }> = ({ data }) => <>{data / Math.pow(10, 7)} °; 4 | 5 | export default LonLatUnit; 6 | -------------------------------------------------------------------------------- /e2e/utils/detail.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from '@playwright/test'; 2 | 3 | export const clickBackToListBtn = async (page: Page, delay = 1000) => { 4 | await page.locator('.ant-page-header #backButton').click({ delay }); 5 | }; 6 | -------------------------------------------------------------------------------- /src/pages/maintenanceManagement/mapConfig/ConfigDetails/index.less: -------------------------------------------------------------------------------- 1 | .send :global .ant-pro-table .ant-pro-card-body { 2 | padding-bottom: 0; 3 | 4 | .ant-pro-table-list-toolbar-container { 5 | padding-top: 0; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/maintenanceManagement/mapConfig/ConfigPreview/index.less: -------------------------------------------------------------------------------- 1 | .preview { 2 | width: 100%; 3 | height: 100%; 4 | 5 | :global .ant-pro-card-body { 6 | width: 100%; 7 | height: 100%; 8 | padding: 0; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/setupTests.js: -------------------------------------------------------------------------------- 1 | // do some test init 2 | 3 | const localStorageMock = { 4 | getItem: jest.fn(), 5 | setItem: jest.fn(), 6 | removeItem: jest.fn(), 7 | clear: jest.fn(), 8 | }; 9 | 10 | global.localStorage = localStorageMock; 11 | -------------------------------------------------------------------------------- /src/components/SelectLang/index.less: -------------------------------------------------------------------------------- 1 | .dropdown_icon { 2 | display: inline-flex; 3 | align-items: center; 4 | justify-content: center; 5 | padding: 12px; 6 | font-size: 18px; 7 | vertical-align: middle; 8 | cursor: pointer; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/CardList/index.less: -------------------------------------------------------------------------------- 1 | .list { 2 | color: rgba(0, 0, 0, 0.85); 3 | font-weight: 400; 4 | font-size: 16px; 5 | line-height: 22px; 6 | 7 | > span:first-child { 8 | color: #70727a; 9 | white-space: nowrap; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edgeview", 3 | "short_name": "edgeview", 4 | "display": "standalone", 5 | "start_url": "./?utm_source=homescreen", 6 | "theme_color": "#002140", 7 | "background_color": "#001529", 8 | "icons": [] 9 | } 10 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "baseUrl": ".", 7 | "paths": { 8 | "edge-src/*": ["./src/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/services/event/dnpw.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | export async function overtakingWarningList(params: API.PageParams) { 4 | return request>(`/v1/rsi_dnps`, { 5 | method: 'GET', 6 | params, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/services/event/sds.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | export async function sensorDataSharingList(params: API.PageParams) { 4 | return request>(`/v1/rsi_sdss`, { 5 | method: 'GET', 6 | params, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /docs/technology-stack.md: -------------------------------------------------------------------------------- 1 | # 技术栈 2 | 3 | EdgeView 是基于 React 开发的。主要适配 `React > 10.0` 4 | 5 | ## 构建项目的 Web 框架 6 | 7 | 使用[umi](https://github.com/umijs/umi)作为 React 应用框架 8 | 9 | ## UI 框架 10 | 11 | 使用[antd-pro](https://github.com/ant-design/ant-design-pro)作为 UI 框架 12 | -------------------------------------------------------------------------------- /src/services/event/clc.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | export async function cooperativeLaneChangeList(params: API.PageParams) { 4 | return request>(`/v1/rsi_clcs`, { 5 | method: 'GET', 6 | params, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/services/event/icw.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | export async function intersectionCollisionWarningList(params: API.PageParams) { 4 | return request>(`/v1/rsi_cwms`, { 5 | method: 'GET', 6 | params, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | verbose: false, 4 | extraSetupFiles: ['./tests/setupTests.js'], 5 | globals: { 6 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 7 | localStorage: null, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/services/event/rsm.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // 事件信息列表 4 | export async function roadSideMessageList(params: API.PageParams) { 5 | return request>(`/v1/rsms`, { 6 | method: 'GET', 7 | params, 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /docs/edgeview.md: -------------------------------------------------------------------------------- 1 | ## EdgeView - 边缘云控平台 2 | 3 | EdgeView 是 OpenV2X 组织下的一个用于作为设备管理的模块。 4 | 5 | > License 使用 Apache License Version 2.0 6 | 7 | 1. [技术栈](./technology-stack.md) 8 | 2. [目录树介绍](./catalog-tree.md) 9 | 3. [如何开发](./how-to-develop.md) 10 | 4. [国际化](./i18n-introduction.md) 11 | -------------------------------------------------------------------------------- /src/pages/deviceManagement/RSUManagement/DeviceList/index.less: -------------------------------------------------------------------------------- 1 | .rsu_tabs { 2 | background-color: #fff; 3 | border-radius: 2px; 4 | 5 | :global .ant-tabs-nav { 6 | margin-bottom: 0; 7 | 8 | .ant-tabs-nav-wrap { 9 | padding-left: 20px; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | on: [pull_request] 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | with: 10 | fetch-depth: 0 11 | - uses: wagoid/commitlint-github-action@v5 12 | -------------------------------------------------------------------------------- /.github/workflows/dprint-check-action.yml: -------------------------------------------------------------------------------- 1 | name: 'dprint-check-action' 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | style: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - uses: dprint/check@v2.1 12 | with: 13 | dprint-version: 0.15.0 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /src/access.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://umijs.org/zh-CN/plugins/plugin-access 3 | * */ 4 | export default function access(initialState: { currentUser?: API.CurrentUser | undefined }) { 5 | const { currentUser } = initialState || {}; 6 | return { 7 | canAdmin: currentUser && currentUser.access === 'admin', 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/storage.test.js: -------------------------------------------------------------------------------- 1 | import { getToken, setToken } from './storage'; 2 | 3 | describe('test localStorage', () => { 4 | it('set token', () => { 5 | const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ8'; 6 | setToken(token); 7 | 8 | expect(getToken()).toBe(token); 9 | localStorage.clear(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: 'check-eslint-stylelint-action' 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | eslint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Install modules 11 | run: yarn 12 | - name: Run ESLint 13 | run: yarn lint:js && yarn lint:style 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | LICENSE 17 | .eslintcache 18 | *.lock 19 | yarn-error.log 20 | .history 21 | CNAME 22 | /build 23 | /public -------------------------------------------------------------------------------- /src/shims.ant-pro-columns.d.ts: -------------------------------------------------------------------------------- 1 | import type { ProColumnGroupType, SearchConfig } from '@ant-design/pro-table'; 2 | 3 | declare module '@ant-design/pro-table' { 4 | export declare type TableProColumns = Omit< 5 | ProColumnGroupType, 6 | 'search' 7 | > & { 8 | search?: boolean | SearchConfig; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dprint.dev/schemas/v0.json", 3 | "lineWidth": 100, 4 | "incremental": false, 5 | "newLineKind": "lf", 6 | "markdown": { 7 | "lineWidth": 100, 8 | "newLineKind": "lf", 9 | "deno": true 10 | }, 11 | "includes": ["**/*.{md}"], 12 | "excludes": [], 13 | "plugins": ["https://plugins.dprint.dev/markdown-0.8.0.wasm"] 14 | } 15 | -------------------------------------------------------------------------------- /src/components/HeaderDropdown/index.less: -------------------------------------------------------------------------------- 1 | @import (reference) '~antd/es/style/themes/index'; 2 | 3 | .container > * { 4 | background-color: @popover-bg; 5 | border-radius: 4px; 6 | box-shadow: @shadow-1-down; 7 | } 8 | 9 | @media screen and (max-width: @screen-xs) { 10 | .container { 11 | width: 100% !important; 12 | } 13 | .container > * { 14 | border-radius: 0 !important; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Sync To Gitee 2 | on: push 3 | jobs: 4 | sync: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: wearerequired/git-mirror-action@master 8 | env: 9 | SSH_PRIVATE_KEY: ${{ secrets.GITEE_PRIVATE_KEY }} 10 | with: 11 | source-repo: "https://github.com/open-v2x/edgeview.git" 12 | destination-repo: "git@gitee.com:open-v2x/edgeview.git" -------------------------------------------------------------------------------- /e2e/storage/userStorageState.json: -------------------------------------------------------------------------------- 1 | { 2 | "cookies": [], 3 | "origins": [ 4 | { 5 | "origin": "http://localhost:8000", 6 | "localStorage": [ 7 | { 8 | "name": "v2x_edgeview_token", 9 | "value": "bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjkwMjU1NTAsInN1YiI6IjEifQ.9dILzt1UkcL90vmN0fxMjbgOQ5Aar9MSmsvmZ-jM-Sw" 10 | } 11 | ] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import request from './request'; 2 | 3 | // 登录 4 | // 登录后刷新 token 5 | export async function login(body: API.LoginParams) { 6 | return request('/v1/login', { 7 | method: 'POST', 8 | data: body, 9 | }); 10 | } 11 | 12 | // 获取登录用户信息 13 | export async function currentUser() { 14 | return request('/v1/users/me', { 15 | method: 'GET', 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Country/index.tsx: -------------------------------------------------------------------------------- 1 | import { countries } from 'edge-src/services/device/device'; 2 | import { ProFormCascader } from '@ant-design/pro-form'; 3 | 4 | const Country: React.FC = (props) => { 5 | return ( 6 | await countries()} 10 | /> 11 | ); 12 | }; 13 | 14 | export default Country; 15 | -------------------------------------------------------------------------------- /src/services/event/rsi.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // 事件信息列表 4 | export async function eventInfoList(params: API.PageParams) { 5 | return request>(`/v1/events`, { 6 | method: 'GET', 7 | params, 8 | }); 9 | } 10 | 11 | // 事件信息详情 12 | export async function eventInfoDetail(id: number) { 13 | return request(`/v1/events/${id}`, { 14 | method: 'GET', 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | const V2X = 'v2x_edgeview_'; 2 | 3 | export const clearStorage = () => { 4 | const locale = localStorage.getItem('umi_locale'); 5 | localStorage.clear(); 6 | sessionStorage.clear(); 7 | if (locale) { 8 | localStorage.setItem('umi_locale', locale); 9 | } 10 | }; 11 | 12 | const token = `${V2X}token`; 13 | export const getToken = () => localStorage.getItem(token); 14 | export const setToken = (data: string) => localStorage.setItem(token, data); 15 | -------------------------------------------------------------------------------- /src/services/system/edge.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | export async function systemConfig(id: number) { 4 | return request(`/v1/system_configs/${id}`, { 5 | method: 'GET', 6 | }); 7 | } 8 | 9 | export async function updateSystemConfig( 10 | data: System.UpdateEdgeNameParams | System.UpdateEdgeConfigParams, 11 | ) { 12 | return request('/v1/system_configs', { 13 | method: 'POST', 14 | data, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /e2e/utils/index.ts: -------------------------------------------------------------------------------- 1 | // generate Integers in a range 2 | export const generateIntNum = ({ min = 0, max = 10 }) => { 3 | return Math.floor(Math.random() * (max - min) + min); 4 | }; 5 | 6 | // generate pure numbers 7 | export const generatePureNumber = (length = 6) => { 8 | return Math.random().toString(10).substr(2, length); 9 | }; 10 | 11 | // generate numbers and letters 12 | export const generateNumLetter = (length = 6) => { 13 | return Math.random().toString(36).substr(2, length); 14 | }; 15 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { history } from 'umi'; 3 | import { Button, Result } from 'antd'; 4 | 5 | const NoFoundPage: React.FC = () => ( 6 | {t('Sorry, the page you visited does not exist')}} 10 | extra={ 11 | 14 | } 15 | /> 16 | ); 17 | 18 | export default NoFoundPage; 19 | -------------------------------------------------------------------------------- /config/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 3 | * ------------------------------- 4 | * The agent cannot take effect in the production environment 5 | * so there is no configuration of the production environment 6 | * For details, please see 7 | * https://pro.ant.design/docs/deploy 8 | */ 9 | export default { 10 | dev: { 11 | '/api': { 12 | target: 'http://47.100.126.13:28300/api', 13 | changeOrigin: true, 14 | pathRewrite: { '^/api': '' }, 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import zhCN from 'edge-src/locales/zh-CN.json'; 4 | import enUS from 'edge-src/locales/en-US.json'; 5 | 6 | const resources = { 7 | 'zh-CN': { translation: zhCN }, 8 | 'en-US': { translation: enUS }, 9 | }; 10 | 11 | i18n.use(initReactI18next).init({ 12 | resources, 13 | lng: localStorage.getItem('umi_locale') || 'zh-CN', 14 | interpolation: { 15 | escapeValue: false, 16 | }, 17 | }); 18 | 19 | export default i18n; 20 | -------------------------------------------------------------------------------- /config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { Settings as LayoutSettings } from '@ant-design/pro-layout'; 2 | 3 | const Settings: LayoutSettings & { 4 | pwa?: boolean; 5 | logo?: string; 6 | } = { 7 | title: 'V2X 路侧设备管理平台', 8 | headerHeight: 52, 9 | primaryColor: '#1890ff', 10 | layout: 'side', 11 | contentWidth: 'Fluid', 12 | navTheme: 'dark', 13 | fixedHeader: true, 14 | fixSiderbar: true, 15 | splitMenus: false, 16 | pwa: false, 17 | colorWeak: false, 18 | iconfontUrl: '/assets/font/iconfont.js', 19 | }; 20 | 21 | export default Settings; 22 | -------------------------------------------------------------------------------- /src/services/errorStatus.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'antd'; 2 | 3 | export default function errorStatus(code: number | undefined, msg: string, detail: any) { 4 | switch (code) { 5 | case 1062: 6 | case 1406: 7 | return message.error(t(`error.${code}`, { msg: msg })); 8 | case 1116: 9 | const { intersection_id, phase_id } = detail; 10 | return message.error( 11 | t(`error.${code}`, { intersectionId: intersection_id, phaseId: phase_id }), 12 | ); 13 | default: 14 | return message.error(msg); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /deploy/init_env.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | FILE_PATH=/var/www/edgeview 4 | 5 | # shellcheck disable=SC2010 6 | for file_name in $(ls $FILE_PATH |grep "umi.*.js") 7 | do 8 | file=${FILE_PATH}/${file_name} 9 | sed -i "s#APISERVER#${API_SERVER}#g" "$file" 10 | done 11 | 12 | # shellcheck disable=SC2010 13 | for file_name in $(ls $FILE_PATH |grep "p__eventManagement__roadSideInformation__RSIDetails.*.js") 14 | do 15 | file=${FILE_PATH}/${file_name} 16 | sed -i "s#AMAPKEY#${MAP_KEY}#g" "$file" 17 | done 18 | 19 | ./docker-entrypoint.sh 20 | 21 | nginx -g "daemon off;" 22 | -------------------------------------------------------------------------------- /config/config.dev.ts: -------------------------------------------------------------------------------- 1 | // https://umijs.org/config/ 2 | import { defineConfig } from 'umi'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | // https://github.com/zthxxx/react-dev-inspector 7 | 'react-dev-inspector/plugins/umi/react-inspector', 8 | ], 9 | // https://github.com/zthxxx/react-dev-inspector#inspector-loader-props 10 | inspectorConfig: { 11 | exclude: [], 12 | babelPlugins: [], 13 | babelOptions: {}, 14 | }, 15 | define: { 16 | 'process.env.API_SERVER': '/api', 17 | 'process.env.AMAP_KEY': 'a7a90e05a37d3f6bf76d4a9032fc9129', 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/OnlineStatus/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Space } from 'antd'; 3 | 4 | import styles from './index.less'; 5 | 6 | type OnlineStatusType = { 7 | status: boolean; 8 | statusName: React.ReactNode; 9 | }; 10 | 11 | const OnlineStatus: React.FC = ({ status, statusName }) => { 12 | const color = status ? '#52C41A' : 'rgba(0, 0, 0, 0.25)'; 13 | 14 | return ( 15 | 16 | 17 | {statusName} 18 | 19 | ); 20 | }; 21 | 22 | export default OnlineStatus; 23 | -------------------------------------------------------------------------------- /e2e/api/login.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { saveUserStorageState } from '../utils/global'; 3 | 4 | export default test.describe('Login Api', () => { 5 | const username = 'admin'; 6 | const password = 'dandelion'; 7 | const baseURL = 'http://localhost:8000'; 8 | 9 | test('login api', async ({ page, request }) => { 10 | const loginRes: any = await request.post(`${baseURL}/v1/login`, { 11 | data: { 12 | username, 13 | password, 14 | }, 15 | }); 16 | expect(loginRes.ok()).toBeTruthy(); 17 | await saveUserStorageState(page); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /e2e/login.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { saveUserStorageState } from '../utils/global'; 3 | 4 | export default test.describe('Login Api', () => { 5 | const username = 'admin'; 6 | const password = 'dandelion'; 7 | const baseURL = 'http://localhost:8000'; 8 | 9 | test('login api', async ({ page, request }) => { 10 | const loginRes: any = await request.post(`${baseURL}/v1/login`, { 11 | data: { 12 | username, 13 | password, 14 | }, 15 | }); 16 | expect(loginRes.ok()).toBeTruthy(); 17 | await saveUserStorageState(page); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/pages/eventManagement/vulnerableRoadUser/VRUCWDetails/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { history } from 'umi'; 3 | import BaseContainer from 'edge-src/components/BaseContainer'; 4 | import CollisionWarningDetails from 'edge-src/components/CollisionWarningDetails'; 5 | 6 | const VRUCWDetails: React.FC = ({ location: { state } }) => { 7 | if (!state) { 8 | history.goBack(); 9 | } 10 | 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default VRUCWDetails; 19 | -------------------------------------------------------------------------------- /src/pages/eventManagement/intersectionCollisionWarning/ICWDetails/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { history } from 'umi'; 3 | import BaseContainer from 'edge-src/components/BaseContainer'; 4 | import CollisionWarningDetails from 'edge-src/components/CollisionWarningDetails'; 5 | 6 | const ICWDetails: React.FC = ({ location: { state } }) => { 7 | if (!state) { 8 | history.goBack(); 9 | } 10 | 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default ICWDetails; 19 | -------------------------------------------------------------------------------- /src/pages/eventManagement/intersectionCollisionWarning/ICWList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { history } from 'umi'; 3 | import BaseContainer from 'edge-src/components/BaseContainer'; 4 | import CollisionWarningList from 'edge-src/components/CollisionWarningList'; 5 | 6 | const ICWList: React.FC = () => { 7 | return ( 8 | 9 | 12 | history.push({ pathname: `/event/icw/details`, state: row }) 13 | } 14 | /> 15 | 16 | ); 17 | }; 18 | 19 | export default ICWList; 20 | -------------------------------------------------------------------------------- /src/pages/eventManagement/vulnerableRoadUser/VRUCWList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { history } from 'umi'; 3 | import BaseContainer from 'edge-src/components/BaseContainer'; 4 | import CollisionWarningList from 'edge-src/components/CollisionWarningList'; 5 | 6 | const VRUCWList: React.FC = () => { 7 | return ( 8 | 9 | 12 | history.push({ pathname: `/event/vrucw/details`, state: row }) 13 | } 14 | /> 15 | 16 | ); 17 | }; 18 | 19 | export default VRUCWList; 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine AS builder 2 | 3 | COPY ./ /root/edgeview/ 4 | WORKDIR /root/edgeview 5 | RUN yarn config set registry http://registry.npmmirror.com \ 6 | && yarn config set network-timeout 300000 \ 7 | && yarn install \ 8 | && yarn run build 9 | 10 | # Step2. Put into nginx 11 | FROM nginx:1.21.1-alpine 12 | 13 | ARG REPO_URL 14 | ARG BRANCH 15 | ARG COMMIT_REF 16 | LABEL repo-url=$REPO_URL 17 | LABEL branch=$BRANCH 18 | LABEL commit-ref=$COMMIT_REF 19 | 20 | RUN mkdir /etc/nginx/edgeview 21 | 22 | COPY --from=builder /root/edgeview/dist /var/www/edgeview 23 | COPY ./deploy/init_env.sh /init_env.sh 24 | WORKDIR / 25 | CMD ["sh", "init_env.sh"] 26 | -------------------------------------------------------------------------------- /e2e/utils/road_simulator.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page } from '@playwright/test'; 2 | const clientId = 'R328328'; 3 | const password = 'password'; 4 | // const rsu_simulator_Url = 'http://47.100.126.13:6688' 5 | 6 | export const connectMqtt = async (page: Page) => { 7 | await page.fill('#clientId', clientId); 8 | await page.fill('#password', password); 9 | await page.click('#connectButton'); 10 | const locator = page.locator('#state'); 11 | await expect(locator).toHaveText('connected'); 12 | }; 13 | // 勾选复选框 14 | export const checkDataset = async (page: Page, name: string) => { 15 | await page.click(`xpath=//td[contains(text(), "${name}")]/preceding-sibling::td[1]`); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/Country/renderHelper.tsx: -------------------------------------------------------------------------------- 1 | import type { FormInstance } from 'antd'; 2 | import Country from '.'; 3 | 4 | export const renderAreaFormItem = ( 5 | _: any, 6 | { type, defaultRender, ...rest }: any, 7 | form: FormInstance, 8 | ) => { 9 | if (type === 'form') { 10 | return null; 11 | } 12 | const status = form.getFieldValue('state'); 13 | if (status !== 'open') { 14 | return ; 15 | } 16 | return defaultRender(_); 17 | }; 18 | 19 | export const renderAreaFormatName = (data: any) => { 20 | const { countryName = '', provinceName = '', cityName = '', areaName = '' } = data || {}; 21 | return `${countryName}${provinceName}${cityName}${areaName}`; 22 | }; 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: FEATURE REQUEST 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ## Is your feature request related to a problem? Please describe. 10 | 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## Describe the solution you'd like 14 | 15 | A clear and concise description of what you want to happen. 16 | 17 | ## Describe alternatives you've considered 18 | 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | ## Additional context 22 | 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /e2e/examples/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('homepage has title and links to intro page', async ({ page }) => { 4 | await page.goto('https://playwright.dev/'); 5 | 6 | // Expect a title "to contain" a substring. 7 | await expect(page).toHaveTitle(/Playwright/); 8 | 9 | // create a locator 10 | const getStarted = page.getByRole('link', { name: 'Get started' }); 11 | 12 | // Expect an attribute "to be strictly equal" to the value. 13 | await expect(getStarted).toHaveAttribute('href', '/docs/intro'); 14 | 15 | // Click the get started link. 16 | await getStarted.click(); 17 | 18 | // Expects the URL to contain intro. 19 | await expect(page).toHaveURL(/.*intro/); 20 | }); 21 | -------------------------------------------------------------------------------- /tests-examples/examples/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('homepage has title and links to intro page', async ({ page }) => { 4 | await page.goto('https://playwright.dev/'); 5 | 6 | // Expect a title "to contain" a substring. 7 | await expect(page).toHaveTitle(/Playwright/); 8 | 9 | // create a locator 10 | const getStarted = page.getByRole('link', { name: 'Get started' }); 11 | 12 | // Expect an attribute "to be strictly equal" to the value. 13 | await expect(getStarted).toHaveAttribute('href', '/docs/intro'); 14 | 15 | // Click the get started link. 16 | await getStarted.click(); 17 | 18 | // Expects the URL to contain intro. 19 | await expect(page).toHaveURL(/.*intro/); 20 | }); 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | 12 | # misc 13 | .DS_Store 14 | npm-debug.log* 15 | yarn-error.log 16 | 17 | /coverage 18 | .idea 19 | yarn.lock 20 | package-lock.json 21 | pnpm-lock.yaml 22 | *bak 23 | 24 | 25 | # visual studio code 26 | .history 27 | *.log 28 | functions/* 29 | .temp/** 30 | 31 | # umi 32 | .umi 33 | .umi-production 34 | 35 | # screenshot 36 | screenshot 37 | .firebase 38 | .eslintcache 39 | 40 | build 41 | 42 | # e2e 43 | /e2e/test-results/ 44 | /e2e/playwright-report/ 45 | /e2e/storage/ 46 | /playwright/.cache/ 47 | -------------------------------------------------------------------------------- /docs/i18n-introduction.md: -------------------------------------------------------------------------------- 1 | # 国际化 2 | 3 | - 框架支持国际化,默认支持英文、中文 4 | 5 | ## 代码位置 6 | 7 | - 英文:`src/locales/en-US.json` 8 | - 中文:`src/locales/zh-CN.json` 9 | 10 | ## 如何使用 11 | 12 | - 代码中的需要国际化展示的字符串均使用英文,使用命令行完成字符采集后,无需更新 en.json 文件,只需要修改 zh.json 中对应的中文即可完成国际化的操作 13 | - 对于需要国际化的字符串,使用`t`函数即可 14 | 15 | - 国际化写法为`t('Action')` 16 | - 注意,英文是大小写相关的 17 | - `t`函数支持带有参数的字符串 18 | 19 | - 参数使用`{}`标识,如 20 | 21 | ```javascript 22 | confirmContext = () => 23 | t('Are you sure to { action }?', { 24 | action: this.actionName || this.title, 25 | }); 26 | ``` 27 | 28 | - 采集 29 | 30 | ```shell 31 | grunt 32 | ``` 33 | 34 | - 采集后,`en-US.json`与`zh-CN.json`文件会自动更新 35 | 36 | - 更新中文 37 | - 采集后,直接在`zh-CN.json`中更新相应的中文翻译即可 38 | -------------------------------------------------------------------------------- /src/components/HeaderDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import type { DropDownProps } from 'antd/es/dropdown'; 2 | import { Dropdown } from 'antd'; 3 | import React from 'react'; 4 | import classNames from 'classnames'; 5 | import styles from './index.less'; 6 | 7 | export type HeaderDropdownProps = { 8 | overlayClassName?: string; 9 | overlay: React.ReactNode | (() => React.ReactNode) | any; 10 | placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; 11 | } & Omit; 12 | 13 | const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( 14 | 15 | ); 16 | 17 | export default HeaderDropdown; 18 | -------------------------------------------------------------------------------- /src/components/BaseContainer/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageContainer } from '@ant-design/pro-layout'; 2 | import { Button } from 'antd'; 3 | import { history } from 'umi'; 4 | 5 | type BaseContainerType = { 6 | children: React.ReactNode; 7 | back?: boolean; 8 | }; 9 | 10 | const BaseContainer: React.FC = ({ children, back = false }) => { 11 | return ( 12 | history.goBack()}> 18 | {t('Back')} 19 | , 20 | ] 21 | : [] 22 | } 23 | > 24 | {children} 25 | 26 | ); 27 | }; 28 | 29 | export default BaseContainer; 30 | -------------------------------------------------------------------------------- /src/pages/systemConfiguration/EdgeSiteConfig/index.less: -------------------------------------------------------------------------------- 1 | .edge_site { 2 | height: calc(100vh - 164px); 3 | 4 | .site { 5 | padding: 45px 82px 45px 44px; 6 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 7 | 8 | &_icon { 9 | width: 40px; 10 | } 11 | 12 | &_info { 13 | margin: 0 auto 0 10px; 14 | padding-right: 10%; 15 | 16 | &_name { 17 | color: rgba(0, 0, 0, 0.85); 18 | font-weight: 400; 19 | font-size: 14px; 20 | line-height: 20px; 21 | } 22 | 23 | &_desc { 24 | margin-top: 10px; 25 | color: #979797; 26 | font-weight: 400; 27 | font-size: 14px; 28 | line-height: 20px; 29 | 30 | span { 31 | color: rgba(0, 0, 0, 0.85); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/deviceManagement/RSUManagement/DeviceList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tabs } from 'antd'; 3 | import BaseContainer from 'edge-src/components/BaseContainer'; 4 | import RegisteredList from './components/RegisteredList'; 5 | import NotRegisteredList from './components/NotRegisteredList'; 6 | 7 | import styles from './index.less'; 8 | 9 | const DeviceList: React.FC = () => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default DeviceList; 25 | -------------------------------------------------------------------------------- /src/pages/deviceManagement/RSUManagement/DeviceDetails/components/CPULineChart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Line } from '@ant-design/charts'; 3 | 4 | const CPULineChart: React.FC<{ list: Device.CPURunningInfo[] }> = ({ list }) => { 5 | const config: any = { 6 | data: list.map(({ time, load, uti }) => { 7 | const date = new Date(+time); 8 | const timeString = `${date.getHours()}:${date.getMinutes()}`; 9 | return { time: timeString, uti, value: load, category: t('CPU Load') }; 10 | }), 11 | xField: 'time', 12 | yField: 'value', 13 | seriesField: 'category', 14 | color: ['#5b8ff9'], 15 | }; 16 | return ( 17 | <> 18 |
19 | {t('CPU Core Count')}:{list[0]?.uti || 0} 20 |
21 | 22 | 23 | ); 24 | }; 25 | 26 | export default CPULineChart; 27 | -------------------------------------------------------------------------------- /src/pages/deviceManagement/RSUManagement/DeviceDetails/components/MemoryLineChart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Line } from '@ant-design/charts'; 3 | 4 | const MemoryLineChart: React.FC<{ list: Device.MemRunningInfo[] }> = ({ list }) => { 5 | const data: { time: string; value: number; category: string }[] = []; 6 | list.map(({ time, total, used }) => { 7 | const date = new Date(+time); 8 | const timeString = `${date.getHours()}:${date.getMinutes()}`; 9 | data.push( 10 | { time: timeString, value: total, category: t('Total Memory (M)') }, 11 | { time: timeString, value: used, category: t('Stored Memory (M)') }, 12 | ); 13 | }); 14 | const config: any = { 15 | data, 16 | xField: 'time', 17 | yField: 'value', 18 | seriesField: 'category', 19 | }; 20 | return ; 21 | }; 22 | 23 | export default MemoryLineChart; 24 | -------------------------------------------------------------------------------- /src/services/config/query.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // RSU 信息查询列表 4 | export async function infoQueryList(params: API.PageParams) { 5 | return request>(`/v1/rsu_queries`, { 6 | method: 'GET', 7 | params, 8 | }); 9 | } 10 | 11 | // 下发 RSU 信息查询指令 12 | export async function createQueryInstruction(data: Config.CreateQueryParams) { 13 | return request(`/v1/rsu_queries`, { 14 | method: 'POST', 15 | data, 16 | }); 17 | } 18 | 19 | // RSU 信息查询详情 20 | export async function infoQueryDetails(id: number) { 21 | return request>(`/v1/rsu_queries/${id}`, { 22 | method: 'GET', 23 | }); 24 | } 25 | 26 | // 删除信息查询 27 | export async function deleteInfoQuery(id: number) { 28 | return request(`/v1/rsu_queries/${id}`, { 29 | method: 'DELETE', 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/services/config/log.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // RSU 日志下发配置列表 4 | export async function logConfigList(params: API.PageParams) { 5 | return request>(`/v1/rsu_logs`, { 6 | method: 'GET', 7 | params, 8 | }); 9 | } 10 | 11 | // 创建 RSU 日志下发配置 12 | export async function createLogConfig(data: Config.CreateLogConfigParams) { 13 | return request(`/v1/rsu_logs`, { 14 | method: 'POST', 15 | data, 16 | }); 17 | } 18 | 19 | // 编辑 RSU 日志下发配置 20 | export async function updateLogConfig(id: number, data: Config.CreateLogConfigParams) { 21 | return request(`/v1/rsu_logs/${id}`, { 22 | method: 'PUT', 23 | data, 24 | }); 25 | } 26 | 27 | // 删除 RSU 日志下发配置 28 | export async function deleteLogConfig(id: number) { 29 | return request(`/v1/rsu_logs/${id}`, { 30 | method: 'DELETE', 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // 表格 select 选项格式化 2 | export const statusOptionFormat = ( 3 | data: Record | string[], 4 | ): Record => { 5 | const result = {}; 6 | if (Array.isArray(data)) { 7 | data.map((value: string, index: number) => (result[index] = { text: value })); 8 | } else { 9 | Object.keys(data).map((key) => (result[key] = { text: data[key] })); 10 | } 11 | return result; 12 | }; 13 | 14 | export const downloadFile = (url: string, name: string) => { 15 | const a = document.createElement('a'); 16 | a.setAttribute('href', url); 17 | a.setAttribute('download', name); 18 | document.body.appendChild(a); 19 | a.click(); 20 | a.remove(); 21 | }; 22 | 23 | export const dataFormat = (data: number, unit?: string | React.ReactNode) => { 24 | if (data) { 25 | return `${Math.round(data * 100) / 100} ${unit || ''}`; 26 | } 27 | return '-'; 28 | }; 29 | -------------------------------------------------------------------------------- /src/pages/deviceManagement/RSUManagement/DeviceDetails/components/DiskLineChart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Line } from '@ant-design/charts'; 3 | 4 | const DiskLineChart: React.FC<{ list: Device.DiskRunningInfo[] }> = ({ list }) => { 5 | const data: { time: string; value: number; category: string }[] = []; 6 | list.map(({ time, rxByte, wxByte }) => { 7 | const date = new Date(+time); 8 | const timeString = `${date.getHours()}:${date.getMinutes()}`; 9 | data.push( 10 | { time: timeString, value: rxByte, category: t('Disk Data Read Per Second (K)') }, 11 | { time: timeString, value: wxByte, category: t('Disk Data Written Per Second (K)') }, 12 | ); 13 | }); 14 | const config: any = { 15 | data, 16 | xField: 'time', 17 | yField: 'value', 18 | seriesField: 'category', 19 | }; 20 | return ; 21 | }; 22 | 23 | export default DiskLineChart; 24 | -------------------------------------------------------------------------------- /src/pages/deviceManagement/RSUManagement/DeviceDetails/components/NetworkLineChart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Line } from '@ant-design/charts'; 3 | 4 | const NetworkLineChart: React.FC<{ list: Device.NetRunningInfo[] }> = ({ list }) => { 5 | const data: { time: string; value: number; category: string }[] = []; 6 | list.map(({ time, read, write }) => { 7 | const date = new Date(+time); 8 | const timeString = `${date.getHours()}:${date.getMinutes()}`; 9 | data.push( 10 | { time: timeString, value: read, category: t('Disk Data Read Per Second (K)') }, 11 | { time: timeString, value: write, category: t('Disk Data Written Per Second (K)') }, 12 | ); 13 | }); 14 | const config: any = { 15 | data, 16 | xField: 'time', 17 | yField: 'value', 18 | seriesField: 'category', 19 | }; 20 | return ; 21 | }; 22 | 23 | export default NetworkLineChart; 24 | -------------------------------------------------------------------------------- /src/components/CardList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Col, Row } from 'antd'; 3 | import classNames from 'classnames'; 4 | 5 | import styles from './index.less'; 6 | 7 | type BasicInfoType = { 8 | infoMap: InfoMapType[]; 9 | info: Record; 10 | md?: number; 11 | xl?: number; 12 | }; 13 | 14 | const CardList: React.FC = ({ infoMap, info, md = 12, xl = 8 }) => { 15 | return ( 16 | 17 | {infoMap.map(({ key, label, block, render }) => ( 18 | 25 | {label}: 26 | {render?.(info) || info[key] || '-'} 27 | 28 | ))} 29 | 30 | ); 31 | }; 32 | 33 | export default CardList; 34 | -------------------------------------------------------------------------------- /src/services/config/maintenance.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // RSU 运维配置列表 4 | export async function maintenanceConfigList(params: API.PageParams) { 5 | return request>(`/v1/mngs`, { 6 | method: 'GET', 7 | params, 8 | }); 9 | } 10 | 11 | // 编辑 RSU 运维配置 12 | export async function updateMaintenanceConfig(id: number, data: Config.MaintenanceItem) { 13 | return request(`/v1/mngs/${id}`, { 14 | method: 'PUT', 15 | data, 16 | }); 17 | } 18 | 19 | // 下发 RSU 运维配置 20 | export async function sendMaintenanceConfig(id: number) { 21 | return request(`/v1/mngs/${id}/down`, { 22 | method: 'POST', 23 | }); 24 | } 25 | 26 | // 复制 RSU 运维配置 27 | export async function copyMaintenanceConfig(id: number, data: { rsus: number[] }) { 28 | return request(`/v1/mngs/${id}/copy`, { 29 | method: 'POST', 30 | data, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/FormItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProForm from '@ant-design/pro-form'; 3 | import FormField from '../FormField'; 4 | import type { FormGroupType } from '../typings'; 5 | 6 | const FormItem: React.FC<{ items: FormGroupType[] }> = ({ items }) => { 7 | return ( 8 | <> 9 | {items.map(({ key, title, children, components, hidden }) => 10 | hidden ? null : ( 11 | 12 | {children?.map(({ components: comp, hidden: hid, ...item }) => { 13 | return hid ? null : ( 14 |
15 | {comp || } 16 |
17 | ); 18 | })} 19 | {components} 20 |
21 | ), 22 | )} 23 | 24 | ); 25 | }; 26 | 27 | export default FormItem; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: BUG REPORT 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Describe the bug 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | 15 | Steps to reproduce the behavior: 16 | 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | ## Expected behavior 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | ## Screenshots 27 | 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | ## Log Information 31 | 32 | If applicable, add more detailed log information to help explain your problem. 33 | 34 | ## Desktop (please complete the following information): 35 | 36 | - OS: [e.g. iOS] 37 | - Browser [e.g. chrome, safari] 38 | - Version [e.g. 22] 39 | 40 | ## Additional context 41 | 42 | Add any other context about the problem here. 43 | -------------------------------------------------------------------------------- /src/components/RightContent/index.tsx: -------------------------------------------------------------------------------- 1 | import { Space } from 'antd'; 2 | import React from 'react'; 3 | import { useModel } from 'umi'; 4 | import { SelectLang } from '../SelectLang'; 5 | import Avatar from './AvatarDropdown'; 6 | import styles from './index.less'; 7 | export type SiderTheme = 'light' | 'dark'; 8 | 9 | const GlobalHeaderRight: React.FC = () => { 10 | const { initialState } = useModel('@@initialState'); 11 | 12 | if (!initialState || !initialState.settings) { 13 | return null; 14 | } 15 | 16 | const { navTheme, layout } = initialState.settings; 17 | let className = styles.right; 18 | 19 | if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') { 20 | className = `${styles.right} ${styles.dark}`; 21 | } 22 | 23 | return ( 24 | 25 | 26 |
27 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | export default GlobalHeaderRight; 34 | -------------------------------------------------------------------------------- /src/services/device/radar.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // 雷达列表 4 | export async function radarList({ 5 | countryName, 6 | ...params 7 | }: API.PageParams & { countryName?: string[]; areaCode?: string }) { 8 | if (countryName?.length) { 9 | params.areaCode = countryName[countryName.length - 1]; 10 | } 11 | return request>(`/v1/radars`, { 12 | method: 'GET', 13 | params, 14 | }); 15 | } 16 | 17 | // 创建雷达 18 | export async function createRadar(data: Device.CreateCameraParams) { 19 | return request(`/v1/radars`, { 20 | method: 'POST', 21 | data, 22 | }); 23 | } 24 | 25 | // 编辑雷达 26 | export async function updateRadar(id: number, data: Device.CreateCameraParams) { 27 | return request(`/v1/radars/${id}`, { 28 | method: 'PUT', 29 | data, 30 | }); 31 | } 32 | 33 | // 删除雷达 34 | export async function deleteRadar(id: number) { 35 | return request(`/v1/radars/${id}`, { 36 | method: 'DELETE', 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/services/device/camera.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // 摄像头列表 4 | export async function cameraList({ 5 | countryName, 6 | ...params 7 | }: API.PageParams & { countryName?: string[]; areaCode?: string }) { 8 | if (countryName?.length) { 9 | params.areaCode = countryName[countryName.length - 1]; 10 | } 11 | return request>(`/v1/cameras`, { 12 | method: 'GET', 13 | params, 14 | }); 15 | } 16 | 17 | // 创建摄像头 18 | export async function createCamera(data: Device.CreateCameraParams) { 19 | return request(`/v1/cameras`, { 20 | method: 'POST', 21 | data, 22 | }); 23 | } 24 | 25 | // 编辑摄像头 26 | export async function updateCamera(id: number, data: Device.CreateCameraParams) { 27 | return request(`/v1/cameras/${id}`, { 28 | method: 'PUT', 29 | data, 30 | }); 31 | } 32 | 33 | // 删除摄像头 34 | export async function deleteCamera(id: number) { 35 | return request(`/v1/cameras/${id}`, { 36 | method: 'DELETE', 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/services/device/model.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // RSU 型号列表 4 | export async function modelList(params: API.PageParams) { 5 | return request>(`/v1/rsu_models`, { 6 | method: 'GET', 7 | params, 8 | }); 9 | } 10 | 11 | // 添加 RSU 型号 12 | export async function createModel(body: Device.ModelListItem) { 13 | return request(`/v1/rsu_models`, { 14 | method: 'POST', 15 | data: body, 16 | }); 17 | } 18 | 19 | // RSU 型号详情 20 | export async function modelInfo(id: number) { 21 | return request(`/v1/rsu_models/${id}`, { 22 | method: 'GET', 23 | }); 24 | } 25 | 26 | // 编辑 RSU 型号 27 | export async function updateModel(id: number, body: Device.ModelListItem) { 28 | return request(`/v1/rsu_models/${id}`, { 29 | method: 'PUT', 30 | data: body, 31 | }); 32 | } 33 | 34 | // 删除 RSU 型号 35 | export async function deleteModel(id: number) { 36 | return request(`/v1/rsu_models/${id}`, { 37 | method: 'DELETE', 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/services/config/business.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // RSU 参数配置列表 4 | export async function parameterConfigList(params: API.PageParams) { 5 | return request>(`/v1/rsu_configs`, { 6 | method: 'GET', 7 | params, 8 | }); 9 | } 10 | 11 | // 创建 RSU 参数配置 12 | export async function createParameterConfig(data: any) { 13 | return request(`/v1/rsu_configs`, { 14 | method: 'POST', 15 | data, 16 | }); 17 | } 18 | 19 | // RSU 参数配置详情 20 | export async function parameterConfigInfo(id: number) { 21 | return request(`/v1/rsu_configs/${id}`, { 22 | method: 'GET', 23 | }); 24 | } 25 | 26 | // 编辑 RSU 参数配置 27 | export async function updateParameterConfig(id: number, data: any) { 28 | return request(`/v1/rsu_configs/${id}`, { 29 | method: 'PUT', 30 | data, 31 | }); 32 | } 33 | 34 | // 删除 RSU 参数配置 35 | export async function deleteParameterConfig(id: number) { 36 | return request(`/v1/rsu_configs/${id}`, { 37 | method: 'DELETE', 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /e2e/pages/login.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { checkErrorMsg, saveUserStorageState } from '../utils/global'; 3 | 4 | const username = 'admin'; 5 | const password = 'dandelion'; 6 | 7 | export default test.describe('The Login page', () => { 8 | test('successfully login and check menu', async ({ page }) => { 9 | await page.goto('/user/login'); 10 | 11 | await page.fill('#username', username); 12 | await page.fill('#password', password); 13 | const submitBtn = page.locator('.ant-form button.ant-btn'); 14 | await submitBtn.click(); 15 | await expect(page).toHaveURL('/device/rsu'); 16 | 17 | // Save signed-in state to 'userStorageState.json'. 18 | await saveUserStorageState(page); 19 | }); 20 | 21 | test('successfully error username and password', async ({ page }) => { 22 | await page.goto('/user/login'); 23 | 24 | await page.fill('#username', `${username}0`); 25 | await page.fill('#password', `${password}0`); 26 | const submitBtn = page.locator('.ant-form button.ant-btn'); 27 | await submitBtn.click(); 28 | await checkErrorMsg(page); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /deploy/edgeview.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | root /var/www/edgeview; 4 | access_log /var/log/nginx/access.log; 5 | error_log /var/log/nginx/error.log; 6 | 7 | index index.html index.htm; 8 | 9 | # 处理静态文件 10 | location /favicon.png { 11 | root /var/www/edgeview; 12 | } 13 | location /logo.png { 14 | root /var/www/edgeview; 15 | } 16 | location /index.html { 17 | root /var/www/edgeview; 18 | } 19 | 20 | location ~ ^\/assets\/.*$ { 21 | root /var/www/edgeview; 22 | } 23 | 24 | location ~ ^\/uploadData\/.*$ { 25 | root /var/www/edgeview; 26 | } 27 | 28 | # 后端api反向代理 29 | location /api/ { 30 | proxy_pass http://localhost:28300/api/; 31 | proxy_buffering off; 32 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 33 | proxy_set_header X-Forwarded-Proto $scheme; 34 | proxy_set_header X-Forwarded-Host $host; 35 | proxy_set_header X-Real-IP $remote_addr; 36 | } 37 | 38 | # 无效url重定向至首页 39 | location / { 40 | try_files $uri $uri/ /index.html; 41 | } 42 | } -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | # 1.setup node env 2 | # 2.unit test 3 | 4 | name: unit-test 5 | 6 | on: 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | # This ensures that previous jobs for the PR are canceled when the PR is updated. 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.head_ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | jest-run: 18 | runs-on: ubuntu-20.04 19 | # let's make sure our tests pass on Chrome browser 20 | name: Jest 21 | steps: 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Setup node env and build 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 14.17.6 31 | 32 | - name: Install dependencies 33 | run: | 34 | if [ -e yarn.lock ]; then 35 | yarn install --frozen-lockfile 36 | elif [ -e package-lock.json ]; then 37 | npm ci 38 | else 39 | npm i 40 | fi 41 | 42 | - name: Unit test 43 | run: yarn test .test.js 44 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.initConfig({ 3 | i18next: { 4 | dev: { 5 | src: ['src/**/*.{tsx,ts}'], 6 | dest: 'src', 7 | options: { 8 | lngs: ['en-US', 'zh-CN'], 9 | removeUnusedKeys: true, 10 | // sort: true, 11 | keySeparator: false, 12 | nsSeparator: false, 13 | interpolation: { 14 | prefix: '{{', 15 | suffix: '}}', 16 | }, 17 | resource: { 18 | loadPath: 'src/locales/{{lng}}.json', 19 | savePath: 'locales/{{lng}}.json', 20 | }, 21 | func: { 22 | list: ['t', 't.html'], 23 | extensions: ['.ts', '.tsx'], 24 | }, 25 | defaultValue: (lng, ns, key) => { 26 | if (lng === 'zh-CN') { 27 | return ''; 28 | } 29 | return key; 30 | }, 31 | }, 32 | }, 33 | }, 34 | }); 35 | 36 | // Load the plugin that provides the "i18next" task. 37 | grunt.loadNpmTasks('i18next-scanner'); 38 | 39 | // Default task(s). 40 | grunt.registerTask('default', ['i18next']); 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/FormField/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ProFormText, 4 | ProFormTextArea, 5 | ProFormCascader, 6 | ProFormSelect, 7 | ProFormRadio, 8 | ProFormDigit, 9 | ProFormUploadButton, 10 | } from '@ant-design/pro-form'; 11 | import type { FormItemType } from '../typings'; 12 | 13 | const components = { 14 | text: ProFormText, 15 | password: ProFormText.Password, 16 | textarea: ProFormTextArea, 17 | cascader: ProFormCascader, 18 | select: ProFormSelect, 19 | radio: ProFormRadio.Group, 20 | digit: ProFormDigit, 21 | uploadButton: ProFormUploadButton, 22 | }; 23 | const Field: React.FC<{ item: FormItemType }> = ({ 24 | item: { type = 'text', required = false, width = 'lg', ...props }, 25 | }) => { 26 | const Component = components[type]; 27 | return ; 28 | }; 29 | 30 | const FormField: React.FC<{ items: FormItemType[] }> = ({ items }) => { 31 | return ( 32 | <> 33 | {items.map((item) => ( 34 | 35 | ))} 36 | 37 | ); 38 | }; 39 | 40 | export type FieldType = keyof typeof components; 41 | export default FormField; 42 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | globals: { 4 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, 5 | page: true, 6 | REACT_APP_ENV: true, 7 | }, 8 | rules: { 9 | camelcase: 'warn', 10 | 'react/prop-types': 'warn', 11 | 'class-methods-use-this': 'off', 12 | 'react/prefer-stateless-function': 'warn', 13 | 'no-plusplus': 'warn', 14 | 'no-param-reassign': 'warn', 15 | 'react/jsx-props-no-spreading': 'warn', 16 | 'react/static-property-placement': 'warn', 17 | 'prefer-destructuring': 'warn', 18 | 'no-use-before-define': 'warn', 19 | 'react/forbid-prop-types': 'warn', 20 | 'react/no-array-index-key': 'warn', 21 | 'react/require-default-props': 'warn', 22 | 'consistent-return': 'warn', 23 | 'no-underscore-dangle': 'warn', 24 | 'no-unused-expressions': 'warn', 25 | 'no-empty': [ 26 | 2, 27 | { 28 | allowEmptyCatch: true, 29 | }, 30 | ], 31 | 'react/destructuring-assignment': 'warn', 32 | 'import/prefer-default-export': 'off', 33 | 'no-nested-ternary': 0, 34 | 'no-console': 'error', 35 | 'global-require': 'off', 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/services/device/spat.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // spat 列表 4 | export async function spatList({ 5 | countryName, 6 | ...params 7 | }: API.PageParams & { countryName?: string[]; areaCode?: string }) { 8 | if (countryName?.length) { 9 | params.areaCode = countryName[countryName.length - 1]; 10 | } 11 | return request>(`/v1/spats`, { 12 | method: 'GET', 13 | params, 14 | }); 15 | } 16 | 17 | // 创建 spat 18 | export async function createSpat(data: Device.CreateSpatItem) { 19 | return request(`/v1/spats`, { 20 | method: 'POST', 21 | data, 22 | }); 23 | } 24 | 25 | // 编辑 spat 26 | export async function updateSpat(id: number, data: Device.CreateSpatItem) { 27 | return request(`/v1/spats/${id}`, { 28 | method: 'PUT', 29 | data, 30 | }); 31 | } 32 | 33 | // 删除 spat 34 | export async function deleteSpat(id: number) { 35 | return request(`/v1/spats/${id}`, { 36 | method: 'DELETE', 37 | }); 38 | } 39 | 40 | // 启用激光雷达 41 | export async function enabledSpat(id: number, data: Device.CreateSpatItem) { 42 | return request(`/v1/spats/${id}`, { 43 | method: 'POST', 44 | data, 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/services/device/lidar.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // 激光雷达列表 4 | export async function lidarList({ 5 | countryName, 6 | ...params 7 | }: API.PageParams & { countryName?: string[]; areaCode?: string }) { 8 | if (countryName?.length) { 9 | params.areaCode = countryName[countryName.length - 1]; 10 | } 11 | return request>(`/v1/lidars`, { 12 | method: 'GET', 13 | params, 14 | }); 15 | } 16 | 17 | // 创建激光雷达 18 | export async function createLidar(data: Device.CreateLidarItem) { 19 | return request(`/v1/lidars`, { 20 | method: 'POST', 21 | data, 22 | }); 23 | } 24 | 25 | // 编辑激光雷达 26 | export async function updateLidar(id: number, data: Device.CreateLidarItem) { 27 | return request(`/v1/lidars/${id}`, { 28 | method: 'PUT', 29 | data, 30 | }); 31 | } 32 | 33 | // 删除雷达 34 | export async function deleteLidar(id: number) { 35 | return request(`/v1/lidars/${id}`, { 36 | method: 'DELETE', 37 | }); 38 | } 39 | 40 | // 启用激光雷达 41 | export async function enabledLidar(id: number, data: Device.CreateLidarItem) { 42 | return request(`/v1/lidars/${id}`, { 43 | method: 'POST', 44 | data, 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "esnext", 6 | "lib": ["esnext", "dom"], 7 | "sourceMap": true, 8 | "baseUrl": ".", 9 | "jsx": "react-jsx", 10 | "resolveJsonModule": true, 11 | "allowSyntheticDefaultImports": true, 12 | "moduleResolution": "node", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "allowJs": true, 18 | "skipLibCheck": true, 19 | "experimentalDecorators": true, 20 | "strict": true, 21 | "paths": { 22 | "edge-src/*": ["./src/*"], 23 | "@@/*": ["./src/.umi/*"] 24 | } 25 | }, 26 | "include": [ 27 | "mock/**/*", 28 | "src/**/*", 29 | "playwright.config.ts", 30 | "tests/**/*", 31 | "test/**/*", 32 | "__test__/**/*", 33 | "typings/**/*", 34 | "config/**/*", 35 | ".eslintrc.js", 36 | ".stylelintrc.js", 37 | ".prettierrc.js", 38 | "jest.config.js", 39 | "mock/*", 40 | "e2e/**/*", 41 | "tests-examples/**/*" 42 | ], 43 | "exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"] 44 | } 45 | -------------------------------------------------------------------------------- /mock/log.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | const genList = (pageNum: number, pageSize: number) => { 4 | const data: Config.LogListItem[] = []; 5 | 6 | for (let i = 0; i < pageSize; i += 1) { 7 | const index = (pageNum - 1) * 10 + i + 1; 8 | data.push({ 9 | id: index, 10 | uploadUrl: 'http://172.16.1.167:8000', 11 | userId: `username ${index + 1}`, 12 | createTime: '2022-03-14 23:12:00', 13 | }); 14 | } 15 | data.reverse(); 16 | return data; 17 | }; 18 | 19 | let tableDataSource = genList(1, 40); 20 | 21 | function getList(req: Request, res: Response, u: string) { 22 | let realUrl = u; 23 | if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { 24 | realUrl = req.url; 25 | } 26 | const { pageNum = 1, pageSize = 10 } = req.query; 27 | 28 | let dataSource = [...tableDataSource].slice( 29 | ((pageNum as number) - 1) * (pageSize as number), 30 | (pageNum as number) * (pageSize as number), 31 | ); 32 | 33 | const result = { 34 | data: { 35 | data: dataSource, 36 | total: tableDataSource.length, 37 | }, 38 | }; 39 | 40 | return res.json(result); 41 | } 42 | 43 | export default { 44 | 'GET /api/log': getList, 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/ConfirmModal/index.ts: -------------------------------------------------------------------------------- 1 | import { message, Modal } from 'antd'; 2 | import type { ActionType } from '@ant-design/pro-table'; 3 | 4 | type confirmModalParams = { 5 | id: number; 6 | content: string; // modal 提示内容 7 | modalFn: (id: number, params?: any) => void; 8 | actionRef: React.MutableRefObject; 9 | title?: string; // modal 标题 10 | successMsg?: string; // modal 操作成功的提示信息 11 | params?: number | Record; // 额外参数 12 | }; 13 | export const confirmModal = async ({ 14 | id, 15 | params, 16 | title = t('Delete'), 17 | content, 18 | successMsg = t('{{value}} successfully', { value: t('Deleted') }), 19 | modalFn, 20 | actionRef, 21 | }: confirmModalParams) => { 22 | Modal.confirm({ 23 | title, 24 | content, 25 | cancelButtonProps: { id: 'cancelButton' }, 26 | okButtonProps: { id: 'okButton' }, 27 | onOk() { 28 | return new Promise(async (resolve) => { 29 | try { 30 | await modalFn(id, params); 31 | resolve(true); 32 | message.success(successMsg); 33 | actionRef.current?.reload?.(); 34 | } catch { 35 | resolve(false); 36 | } 37 | }); 38 | }, 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/typings.d.ts: -------------------------------------------------------------------------------- 1 | import type { FieldType } from './FormField'; 2 | 3 | type FormItemType = { 4 | type?: FieldType; 5 | required?: boolean; 6 | width?: number | 'lg' | 'sm' | 'md' | 'xl' | 'xs'; 7 | name?: NamePath; 8 | label?: string | React.ReactNode; 9 | tooltip?: LabelTooltipType; 10 | fieldProps?: FieldProps & 11 | InputProps & 12 | RadioGroupProps & 13 | InputNumberProps & 14 | CascaderProps & 15 | SelectProps; 16 | disabled?: boolean; 17 | min?: number | Number.MIN_SAFE_INTEGER; 18 | rules?: Rule[]; 19 | options?: (string | number | CheckboxOptionType)[] & (DefaultOptionType[] | string[]); 20 | valueEnum?: 21 | | ProSchemaValueEnumObj 22 | | ProSchemaValueEnumMap 23 | | ((row: Record) => ProSchemaValueEnumObj | ProSchemaValueEnumMap) 24 | | undefined; 25 | request?: ProFieldRequestData; 26 | title?: string; 27 | icon?: React.ReactNode; 28 | max?: number; 29 | components?: React.ReactNode | React.ReactNode[]; 30 | hidden?: boolean; 31 | }; 32 | type FormGroupType = { 33 | key: string; 34 | title?: string; 35 | children?: FormItemType[]; 36 | components?: React.ReactNode | React.ReactNode[]; 37 | hidden?: boolean; 38 | }; 39 | -------------------------------------------------------------------------------- /deploy/nginx.conf: -------------------------------------------------------------------------------- 1 | include /etc/nginx/modules-enabled/*.conf; 2 | 3 | user nginx; 4 | worker_processes auto; 5 | 6 | pid /var/run/nginx.pid; 7 | 8 | 9 | events { 10 | worker_connections 1024; 11 | multi_accept on; 12 | } 13 | 14 | 15 | http { 16 | 17 | tcp_nodelay on; 18 | client_max_body_size 0; 19 | types_hash_max_size 2048; 20 | proxy_request_buffering off; 21 | server_tokens off; 22 | 23 | include /etc/nginx/mime.types; 24 | default_type application/octet-stream; 25 | 26 | log_format main '$remote_addr - $remote_user [$time_local] "$request_time" ' 27 | '"$upstream_response_time" "$request" ' 28 | '$status $body_bytes_sent "$http_referer" ' 29 | '"$http_user_agent" "$http_x_forwarded_for"'; 30 | access_log /var/log/nginx/access.log main; 31 | error_log /var/log/nginx/error.log; 32 | 33 | sendfile on; 34 | tcp_nopush on; 35 | 36 | keepalive_timeout 65; 37 | 38 | gzip on; 39 | gzip_static on; 40 | gzip_disable "msie6"; 41 | 42 | gzip_vary on; 43 | gzip_proxied any; 44 | gzip_comp_level 6; 45 | gzip_buffers 16 8k; 46 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 47 | 48 | include /etc/nginx/conf.d/*.conf; 49 | } -------------------------------------------------------------------------------- /tests/run-tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const { spawn } = require('child_process'); 4 | const { kill } = require('cross-port-killer'); 5 | 6 | const env = Object.create(process.env); 7 | env.BROWSER = 'none'; 8 | env.TEST = true; 9 | env.UMI_UI = 'none'; 10 | env.PROGRESS = 'none'; 11 | // flag to prevent multiple test 12 | let once = false; 13 | 14 | const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['run', 'serve'], { 15 | env, 16 | }); 17 | 18 | startServer.stderr.on('data', (data) => { 19 | // eslint-disable-next-line 20 | console.log(data.toString()); 21 | }); 22 | 23 | startServer.on('exit', () => { 24 | kill(process.env.PORT || 8000); 25 | }); 26 | 27 | console.log('Starting development server for e2e tests...'); 28 | startServer.stdout.on('data', (data) => { 29 | console.log(data.toString()); 30 | // hack code , wait umi 31 | if (!once && data.toString().indexOf('Serving your umi project!') >= 0) { 32 | // eslint-disable-next-line 33 | once = true; 34 | console.log('Development server is started, ready to run tests.'); 35 | const testCmd = spawn( 36 | /^win/.test(process.platform) ? 'npm.cmd' : 'npm', 37 | ['run', 'playwright'], 38 | { 39 | stdio: 'inherit', 40 | }, 41 | ); 42 | testCmd.on('exit', (code) => { 43 | console.log(code); 44 | startServer.kill(); 45 | process.exit(code); 46 | }); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /e2e/pages/event/Dnpw.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import config from '../../../playwright.config'; 3 | import { setQuerySelectValue } from '../../utils/form'; 4 | import { gotoPageAndExpectUrl, useUserStorageState } from '../../utils/global'; 5 | import { checkDataset, connectMqtt } from '../../utils/road_simulator'; 6 | 7 | import { checkEmptyTable, checkTableRowLength } from '../../utils/table'; 8 | 9 | test.describe('The Sds Page', () => { 10 | const pageUrl = '/event/dnpw'; 11 | const baseURL = config.use?.baseURL; 12 | 13 | // Use signed-in state of 'userStorageState.json'. 14 | useUserStorageState(); 15 | test.beforeEach(async ({ page }) => { 16 | await gotoPageAndExpectUrl(page, pageUrl); 17 | }); 18 | 19 | test('用路侧模拟器发送[逆向超车预警]数据', async ({ page }) => { 20 | await page.goto(`${baseURL}:6688`); 21 | await connectMqtt(page); 22 | await checkDataset(page, 'DNP_track'); 23 | await checkDataset(page, 'msg_VIR_DNP'); 24 | // 直到loading结束再点击发送按钮 25 | await page.locator('#loading-DNP_track').waitFor({ state: 'hidden' }); 26 | await page.click('#publishDataSetButton'); 27 | await page.waitForTimeout(10000); // 发送10s数据后停止发送 28 | await page.click('#publishDataSetButton'); 29 | }); 30 | 31 | test('成功接收到数据', async ({ page }) => { 32 | page.reload(); 33 | 34 | await checkTableRowLength(page, 3); 35 | }); 36 | 37 | test('成功通过下拉框筛选数据', async ({ page }) => { 38 | await setQuerySelectValue(page, '#info', 2); 39 | await checkEmptyTable(page); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /config/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi'; 2 | import defaultSettings from './defaultSettings'; 3 | import proxy from './proxy'; 4 | import routes from './routes'; 5 | 6 | const { REACT_APP_ENV } = process.env; 7 | const path = require('path'); 8 | 9 | const root = (p: string) => { 10 | return path.resolve(__dirname, `../${p}`); 11 | }; 12 | 13 | export default defineConfig({ 14 | hash: true, 15 | antd: {}, 16 | dva: { hmr: true }, 17 | layout: { 18 | locale: true, 19 | siderWidth: 300, 20 | ...defaultSettings, 21 | }, 22 | locale: { 23 | antd: true, 24 | baseNavigator: true, 25 | }, 26 | dynamicImport: { 27 | loading: '@ant-design/pro-layout/es/PageLoading', 28 | }, 29 | targets: { ie: 11 }, 30 | routes, 31 | theme: { 32 | 'root-entry-name': 'variable', 33 | }, 34 | // esbuild is father build tools 35 | // https://umijs.org/plugins/plugin-esbuild 36 | esbuild: {}, 37 | title: false, 38 | ignoreMomentLocale: true, 39 | proxy: proxy[REACT_APP_ENV || 'dev'], 40 | define: { 41 | 'process.env.API_SERVER': 'APISERVER', 42 | 'process.env.AMAP_KEY': 'AMAPKEY', 43 | }, 44 | manifest: { basePath: '/' }, 45 | // Fast Refresh 热更新 46 | fastRefresh: {}, 47 | nodeModulesTransform: { type: 'none' }, 48 | mfsu: {}, 49 | webpack5: {}, 50 | exportStatic: {}, 51 | extraBabelPlugins: [ 52 | [ 53 | 'istanbul', 54 | { 55 | useInlineSourceMaps: false, 56 | }, 57 | ], 58 | ], 59 | alias: { 60 | 'edge-src': root('src'), 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Deprecated] Move to https://github.com/open-v2x/omega 2 | 3 | # edgeview 4 | 5 | ## Environment Prepare 6 | 7 | Install `node_modules`: 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | or 14 | 15 | ```bash 16 | yarn 17 | ``` 18 | 19 | ## Provided Scripts 20 | 21 | ### Start project 22 | 23 | ```bash 24 | npm start 25 | ``` 26 | 27 | ### Build project 28 | 29 | ```bash 30 | npm run build 31 | ``` 32 | 33 | ### Check code style 34 | 35 | ```bash 36 | npm run lint 37 | ``` 38 | 39 | You can also use script to auto fix some lint error: 40 | 41 | ```bash 42 | npm run lint:fix 43 | ``` 44 | 45 | ### Test code 46 | 47 | ```bash 48 | npm test 49 | ``` 50 | 51 | ## Run e2e at local device 52 | 53 | ### 1. Running all the Tests 54 | 55 | ```bash 56 | yarn playwright:test 57 | ``` 58 | 59 | ### 1.1 Running a single test file 60 | 61 | ```bash 62 | npx playwright test ${test file name} 63 | ``` 64 | 65 | ### 1.2 Running tests on a specific project 66 | 67 | View document on [playwright.dev/docs/running-tests](https://playwright.dev/docs/running-tests). 68 | 69 | ```bash 70 | npx playwright test ${test file name} --project=chromium 71 | ``` 72 | 73 | ### 1.3 Debugging all the Tests 74 | 75 | ```bash 76 | yarn playwright:test-debug 77 | ``` 78 | 79 | ### 2. View HTML Test Reports 80 | 81 | ```bash 82 | yarn playwright:show-report 83 | ``` 84 | 85 | ## More 86 | 87 | You can view full document on our [official website](https://pro.ant.design). And welcome any 88 | feedback in our [github](https://github.com/ant-design/ant-design-pro). 89 | -------------------------------------------------------------------------------- /e2e/pages/event/Clc.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import config from '../../../playwright.config'; 3 | import { setQuerySelectValue } from '../../utils/form'; 4 | import { gotoPageAndExpectUrl, useUserStorageState } from '../../utils/global'; 5 | import { checkDataset, connectMqtt } from '../../utils/road_simulator'; 6 | 7 | import { checkEmptyTable, checkTableRowLength } from '../../utils/table'; 8 | 9 | test.describe('The Sds Page', () => { 10 | const pageUrl = '/event/clc'; 11 | const baseURL = config.use?.baseURL; 12 | 13 | // Use signed-in state of 'userStorageState.json'. 14 | useUserStorageState(); 15 | test.beforeEach(async ({ page }) => { 16 | await gotoPageAndExpectUrl(page, pageUrl); 17 | }); 18 | 19 | test('用路侧模拟器发送[协作换道]数据', async ({ page }) => { 20 | await page.goto(`${baseURL}:6688`); 21 | await connectMqtt(page); 22 | await checkDataset(page, 'CLC_track'); 23 | await checkDataset(page, 'msg_VIR_CLC'); 24 | // 直到loading结束再点击发送按钮 25 | await page.locator('#loading-CLC_track').waitFor({ state: 'hidden' }); 26 | await page.click('#publishDataSetButton'); 27 | await page.waitForTimeout(10000); // 发送10s数据后停止发送 28 | await page.click('#publishDataSetButton'); 29 | }); 30 | 31 | test('成功接收到数据', async ({ page }) => { 32 | page.reload(); 33 | 34 | await checkTableRowLength(page, 3); 35 | }); 36 | 37 | test('成功通过下拉框筛选数据', async ({ page }) => { 38 | page.goto(pageUrl); 39 | await setQuerySelectValue(page, '#info', 2); 40 | await checkEmptyTable(page); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /e2e/pages/event/Sds.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import config from '../../../playwright.config'; 3 | import { setQuerySelectValue } from '../../utils/form'; 4 | import { gotoPageAndExpectUrl, useUserStorageState } from '../../utils/global'; 5 | import { checkDataset, connectMqtt } from '../../utils/road_simulator'; 6 | import { checkEmptyTable, checkTableRowLength } from '../../utils/table'; 7 | 8 | test.describe('The Sds Page', () => { 9 | const pageUrl = '/event/sds'; 10 | const baseURL = config.use?.baseURL; 11 | 12 | // Use signed-in state of 'userStorageState.json'. 13 | useUserStorageState(); 14 | test.beforeEach(async ({ page }) => { 15 | await gotoPageAndExpectUrl(page, pageUrl); 16 | }); 17 | 18 | test('用路侧模拟器发送[数据共享]数据', async ({ page }) => { 19 | await page.goto(`${baseURL}:6688`); 20 | await connectMqtt(page); 21 | await checkDataset(page, 'SDS_track'); 22 | await checkDataset(page, 'msg_VIR_SDS'); 23 | // 直到loading结束再点击发送按钮 24 | await page.locator('#loading-SDS_track').waitFor({ state: 'hidden' }); 25 | await page.click('#publishDataSetButton'); 26 | await page.waitForTimeout(10000); // 发送10s数据后停止发送 27 | await page.click('#publishDataSetButton'); 28 | }); 29 | 30 | test('成功接收到数据', async ({ page }) => { 31 | page.goto(pageUrl); 32 | await checkTableRowLength(page); 33 | }); 34 | 35 | test('成功通过下拉框筛选数据', async ({ page }) => { 36 | page.goto(pageUrl); 37 | await setQuerySelectValue(page, '#equipmentType'); 38 | await checkEmptyTable(page); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/pages/systemConfiguration/EdgeSiteConfig/components/UpdateSiteNameModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'antd'; 3 | import type { FormGroupType } from 'edge-src/components/typings'; 4 | import Modal from 'edge-src/components/Modal'; 5 | import FormItem from 'edge-src/components/FormItem'; 6 | import { updateSystemConfig } from 'edge-src/services/system/edge'; 7 | 8 | const UpdateSiteNameModal: React.FC = ({ 9 | name = '', 10 | success, 11 | }) => { 12 | const formItems: FormGroupType[] = [ 13 | { 14 | key: 'name', 15 | children: [ 16 | { 17 | required: true, 18 | name: 'name', 19 | label: t('Edge Site Name'), 20 | rules: [{ required: true, message: t('Please enter an edge site name') }], 21 | }, 22 | ], 23 | }, 24 | ]; 25 | return ( 26 | 30 | {t('Modify')} 31 | 32 | } 33 | width={500} 34 | modalProps={{ maskClosable: false }} 35 | submitForm={async (values) => { 36 | await updateSystemConfig(values); 37 | success(); 38 | }} 39 | successMsg={t('{{value}} successfully', { value: t('Modify') })} 40 | params={{ name }} 41 | request={async () => ({ name })} 42 | > 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default UpdateSiteNameModal; 49 | -------------------------------------------------------------------------------- /mock/maintenance.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { parse } from 'url'; 3 | 4 | const genList = (pageNum: number, pageSize: number) => { 5 | const data: Config.MaintenanceListItem[] = []; 6 | 7 | for (let i = 0; i < pageSize; i += 1) { 8 | const index = (pageNum - 1) * 10 + i + 1; 9 | data.push({ 10 | id: index, 11 | name: `test ${index}`, 12 | sn: 'rsd12345677321111', 13 | heartbeat: 60, 14 | operatingState: 60, 15 | log: 60, 16 | }); 17 | } 18 | data.reverse(); 19 | return data; 20 | }; 21 | 22 | let tableDataSource = genList(1, 40); 23 | 24 | function getList(req: Request, res: Response, u: string) { 25 | let realUrl = u; 26 | if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { 27 | realUrl = req.url; 28 | } 29 | const { pageNum = 1, pageSize = 10 } = req.query; 30 | const params = parse(realUrl, true).query as unknown as API.PageParams & 31 | Config.MaintenanceListItem & { filter: any }; 32 | 33 | let dataSource = [...tableDataSource].slice( 34 | ((pageNum as number) - 1) * (pageSize as number), 35 | (pageNum as number) * (pageSize as number), 36 | ); 37 | 38 | if (params.name) { 39 | dataSource = dataSource.filter((data) => data?.name?.includes(params.name || '')); 40 | } 41 | 42 | const result = { 43 | data: { 44 | data: dataSource, 45 | total: params.name ? dataSource.length : tableDataSource.length, 46 | }, 47 | }; 48 | 49 | return res.json(result); 50 | } 51 | 52 | export default { 53 | 'GET /api/maintenance': getList, 54 | }; 55 | -------------------------------------------------------------------------------- /mock/query.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { parse } from 'url'; 3 | 4 | const genList = (pageNum: number, pageSize: number) => { 5 | const data: Config.QueryListItem[] = []; 6 | 7 | for (let i = 0; i < pageSize; i += 1) { 8 | const index = (pageNum - 1) * 10 + i + 1; 9 | data.push({ 10 | id: index, 11 | rsu: 'RSU1、RSU2', 12 | type: ['status', 'statistics', 'device'][(index - 1) % 3], 13 | interval: ['hour', 'day', 'week', 'now'][(index - 1) % 4], 14 | time: '2022-03-14 23:12:00', 15 | }); 16 | } 17 | data.reverse(); 18 | return data; 19 | }; 20 | 21 | let tableDataSource = genList(1, 40); 22 | 23 | function getList(req: Request, res: Response, u: string) { 24 | let realUrl = u; 25 | if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { 26 | realUrl = req.url; 27 | } 28 | const { pageNum = 1, pageSize = 10 } = req.query; 29 | const params = parse(realUrl, true).query as unknown as API.PageParams & 30 | Config.QueryListItem & { filter: any }; 31 | 32 | let dataSource = [...tableDataSource].slice( 33 | ((pageNum as number) - 1) * (pageSize as number), 34 | (pageNum as number) * (pageSize as number), 35 | ); 36 | 37 | if (params.rsu) { 38 | dataSource = dataSource.filter((data) => data?.rsu?.includes(params.rsu || '')); 39 | } 40 | 41 | const result = { 42 | data: { 43 | data: dataSource, 44 | total: params.rsu ? dataSource.length : tableDataSource.length, 45 | }, 46 | }; 47 | 48 | return res.json(result); 49 | } 50 | 51 | export default { 52 | 'GET /api/query': getList, 53 | }; 54 | -------------------------------------------------------------------------------- /src/services/request.ts: -------------------------------------------------------------------------------- 1 | import { extend } from 'umi-request'; 2 | import { getToken } from 'edge-src/utils/storage'; 3 | import { message } from 'antd'; 4 | import { clearStorage } from './../utils/storage'; 5 | import { history } from 'umi'; 6 | import errorStatus from './errorStatus'; 7 | 8 | const errorHandler = (error: any) => { 9 | const { response } = error; 10 | if (response.status === 401) { 11 | clearStorage(); 12 | history.push('/user/login'); 13 | return Promise.reject(error); 14 | } else if (response.status != 200) { 15 | if (response.headers.get('Content-Type').includes('application/json')) { 16 | response.json().then((res: { detail: any }) => { 17 | const { detail } = res || {}; 18 | if (detail) { 19 | const { code, msg, detail: d } = detail; 20 | errorStatus(code, msg || detail, d); 21 | } 22 | }); 23 | } else { 24 | message.error(response.statusText); 25 | } 26 | return Promise.reject(error); 27 | } 28 | }; 29 | 30 | const request = extend({ 31 | prefix: process.env.API_SERVER, 32 | errorHandler, 33 | }); 34 | 35 | request.interceptors.request.use((url: string, { params, ...options }: any) => { 36 | const { current, pageSize, ...param } = params || {}; 37 | if (options.method === 'get') { 38 | if (current) { 39 | param.pageNum = current; 40 | } 41 | if (pageSize) { 42 | param.pageSize = pageSize; 43 | } 44 | } 45 | return { 46 | options: { 47 | ...options, 48 | params: param, 49 | headers: { Authorization: getToken() }, 50 | }, 51 | }; 52 | }); 53 | 54 | export default request; 55 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'slash2'; 2 | declare module '*.css'; 3 | declare module '*.less'; 4 | declare module '*.scss'; 5 | declare module '*.sass'; 6 | declare module '*.svg'; 7 | declare module '*.png'; 8 | declare module '*.jpg'; 9 | declare module '*.jpeg'; 10 | declare module '*.gif'; 11 | declare module '*.bmp'; 12 | declare module '*.tiff'; 13 | declare module 'omit.js'; 14 | declare module 'numeral'; 15 | declare module '@antv/data-set'; 16 | declare module 'mockjs'; 17 | declare module 'react-fittext'; 18 | declare module 'bizcharts-plugin-slider'; 19 | 20 | // preview.pro.ant.design only do not use in your production ; 21 | // preview.pro.ant.design Dedicated environment variable, please do not use it in your project. 22 | declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined; 23 | 24 | declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false; 25 | 26 | type RouterMatchTypes = { 27 | location: { 28 | query: Record; 29 | state: unknown; 30 | }; 31 | match: { 32 | params: Record; 33 | }; 34 | }; 35 | 36 | type InfoMapType = { 37 | key: string; 38 | label: string; 39 | block?: boolean; 40 | render?: (data: any) => void; 41 | span?: number; 42 | unit?: string; 43 | }; 44 | 45 | type CreateModalProps = { 46 | editId?: number; // 编辑id 47 | editInfo?: { id: number } & Record< 48 | string, 49 | string | number | boolean | Record[] 50 | >; // 编辑信息 51 | isDetails?: boolean; // 是否详情 52 | success: () => void; // 创建或编辑成功回调 53 | }; 54 | 55 | declare function t(key: string, { [key as string]: string }?): string & React.ReactNode; 56 | -------------------------------------------------------------------------------- /mock/user.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | const waitTime = (time: number = 100) => { 4 | return new Promise((resolve) => { 5 | setTimeout(() => { 6 | resolve(true); 7 | }, time); 8 | }); 9 | }; 10 | 11 | const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env; 12 | 13 | /** 14 | * 当前用户的权限,如果为空代表没登录 15 | * current user access, if is '', user need login 16 | * 如果是 pro 的预览,默认是有权限的 17 | */ 18 | let access = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ? 'admin' : ''; 19 | 20 | const getAccess = () => { 21 | return access; 22 | }; 23 | 24 | // 代码中会兼容本地 service mock 以及部署站点的静态数据 25 | export default { 26 | 'POST /api/login/account': async (req: Request, res: Response) => { 27 | const { password, username, type } = req.body; 28 | await waitTime(2000); 29 | if (password === '99cloud' && username === 'admin') { 30 | res.send({ 31 | status: 'ok', 32 | type, 33 | currentAuthority: 'admin', 34 | }); 35 | access = 'admin'; 36 | return; 37 | } 38 | if (password === '99cloud' && username === 'user') { 39 | res.send({ 40 | status: 'ok', 41 | type, 42 | currentAuthority: 'user', 43 | }); 44 | access = 'user'; 45 | return; 46 | } 47 | if (type === 'mobile') { 48 | res.send({ 49 | status: 'ok', 50 | type, 51 | currentAuthority: 'admin', 52 | }); 53 | access = 'admin'; 54 | return; 55 | } 56 | 57 | res.send({ 58 | status: 'error', 59 | type, 60 | currentAuthority: 'guest', 61 | }); 62 | access = 'guest'; 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/pages/maintenanceManagement/businessConfig/ConfigList/components/CreateRSUConfigModal/index.less: -------------------------------------------------------------------------------- 1 | .template :global .ant-pro-form-group { 2 | .ant-pro-form-group-title { 3 | position: relative; 4 | padding-left: 22px; 5 | color: rgba(0, 0, 0, 0.85); 6 | font-weight: 400; 7 | font-size: 18px; 8 | line-height: 25px; 9 | 10 | &::before { 11 | position: absolute; 12 | top: 2px; 13 | left: 0; 14 | border-color: transparent transparent transparent #1d6fe4; 15 | border-style: solid; 16 | border-width: 10px 0 10px 13px; 17 | content: ''; 18 | } 19 | } 20 | 21 | > .ant-space, 22 | > .ant-space > .ant-space-item { 23 | width: 100%; 24 | 25 | .ant-pro-table .ant-pro-card-body { 26 | padding: 0; 27 | 28 | .ant-pro-table-list-toolbar-container { 29 | padding-top: 10px; 30 | 31 | .ant-pro-form-group-title { 32 | margin-bottom: 0; 33 | } 34 | } 35 | } 36 | } 37 | 38 | > .ant-space.ant-pro-form-group-container { 39 | flex-wrap: wrap !important; 40 | } 41 | 42 | &:nth-child(3) > .ant-space .ant-space-item { 43 | width: calc(50% - 16px); 44 | 45 | .parameter-info { 46 | height: 100%; 47 | margin: 0 0 20px !important; 48 | 49 | .ant-pro-card-body { 50 | padding: 18px 20px 10px; 51 | } 52 | } 53 | 54 | .ant-form-item { 55 | padding-right: 10px; 56 | 57 | .ant-form-item-label { 58 | min-width: 145px; 59 | } 60 | 61 | .pro-field-lg { 62 | width: 100%; 63 | } 64 | } 65 | 66 | &:nth-child(2) { 67 | height: 334px; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /mock/model.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { parse } from 'url'; 3 | 4 | const genList = (current: number, pageSize: number) => { 5 | const data: Device.ModelListItem[] = []; 6 | 7 | for (let i = 0; i < pageSize; i += 1) { 8 | const index = (current - 1) * 10 + i + 1; 9 | data.push({ 10 | id: index, 11 | modelName: `型号名称 ${index}`, 12 | manufacturer: `厂商名称 ${index}`, 13 | describe: '1234567890', 14 | createTime: '2022-03-14 23:12:00', 15 | }); 16 | } 17 | data.reverse(); 18 | return data; 19 | }; 20 | 21 | let tableDataSource = genList(1, 40); 22 | 23 | function getList(req: Request, res: Response, u: string) { 24 | let realUrl = u; 25 | if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { 26 | realUrl = req.url; 27 | } 28 | const { current = 1, pageSize = 10 } = req.query; 29 | const params = parse(realUrl, true).query as unknown as API.PageParams & 30 | Device.ModelListItem & { filter: any }; 31 | 32 | let dataSource = [...tableDataSource].slice( 33 | ((current as number) - 1) * (pageSize as number), 34 | (current as number) * (pageSize as number), 35 | ); 36 | 37 | if (params.modelName) { 38 | dataSource = dataSource.filter((data) => data?.modelName?.includes(params.modelName || '')); 39 | } 40 | 41 | const result = { 42 | data: dataSource, 43 | total: params.modelName ? dataSource.length : tableDataSource.length, 44 | success: true, 45 | pageSize, 46 | current: parseInt(`${params.pageNum}`, 10) || 1, 47 | }; 48 | 49 | return res.json(result); 50 | } 51 | 52 | export default { 53 | 'GET /api/model/list': getList, 54 | }; 55 | -------------------------------------------------------------------------------- /mock/mapConfig.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { parse } from 'url'; 3 | 4 | const genList = (current: number, pageSize: number) => { 5 | const data: Config.MapListItem[] = []; 6 | 7 | for (let i = 0; i < pageSize; i += 1) { 8 | const index = (current - 1) * 10 + i + 1; 9 | data.push({ 10 | id: index, 11 | mapName: `设备名称${index}`, 12 | mapArea: '江苏省/南京市/江宁区', 13 | mapLocation: `十字路口${index}`, 14 | status: i % 10 > 6 ? 1 : 0, 15 | number: i % 10, 16 | }); 17 | } 18 | data.reverse(); 19 | return data; 20 | }; 21 | 22 | let tableDataSource = genList(1, 40); 23 | 24 | function getList(req: Request, res: Response, u: string) { 25 | let realUrl = u; 26 | if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { 27 | realUrl = req.url; 28 | } 29 | const { current = 1, pageSize = 10 } = req.query; 30 | const params = parse(realUrl, true).query as unknown as API.PageParams & 31 | Config.MapListItem & { filter: any }; 32 | 33 | let dataSource = [...tableDataSource].slice( 34 | ((current as number) - 1) * (pageSize as number), 35 | (current as number) * (pageSize as number), 36 | ); 37 | 38 | if (params.mapName) { 39 | dataSource = dataSource.filter((data) => data?.mapName?.includes(params.mapName || '')); 40 | } 41 | 42 | const result = { 43 | data: dataSource, 44 | total: params.mapName ? dataSource.length : tableDataSource.length, 45 | success: true, 46 | pageSize, 47 | current: parseInt(`${params.pageNum}`, 10) || 1, 48 | }; 49 | 50 | return res.json(result); 51 | } 52 | 53 | export default { 54 | 'GET /api/map/list': getList, 55 | }; 56 | -------------------------------------------------------------------------------- /src/pages/maintenanceManagement/businessConfig/ConfigDetails/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { history, useRequest } from 'umi'; 3 | import ProCard from '@ant-design/pro-card'; 4 | import BaseContainer from 'edge-src/components/BaseContainer'; 5 | import CardList from 'edge-src/components/CardList'; 6 | import ParameterInfo from 'edge-src/components/ParameterInfo'; 7 | import ParameterDeviceList from 'edge-src/components/ParameterDeviceList'; 8 | import { parameterConfigInfo } from 'edge-src/services/config/business'; 9 | 10 | // 基本信息 11 | type BasicInfoType = { 12 | name: string; 13 | }; 14 | const BasicInfo: React.FC<{ basicInfo: BasicInfoType | undefined }> = ({ basicInfo = {} }) => { 15 | const infoMap = [ 16 | { 17 | key: 'name', 18 | label: t('Configuration Name'), 19 | }, 20 | ]; 21 | return ( 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | const ConfigDetails: React.FC = ({ match: { params } }) => { 29 | if (!+params.id) { 30 | history.goBack(); 31 | } 32 | 33 | const { data } = useRequest( 34 | () => { 35 | return parameterConfigInfo(+params.id); 36 | }, 37 | { formatResult: (res) => res }, 38 | ); 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default ConfigDetails; 52 | -------------------------------------------------------------------------------- /e2e/pages/event/Vrucw.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import config from '../../../playwright.config'; 3 | import { setQuerySelectValue } from '../../utils/form'; 4 | import { gotoPageAndExpectUrl, useUserStorageState } from '../../utils/global'; 5 | import { checkDataset, connectMqtt } from '../../utils/road_simulator'; 6 | 7 | import { checkEmptyTable, checkTableRowLength } from '../../utils/table'; 8 | 9 | test.describe('The Sds Page', () => { 10 | const pageUrl = '/event/vrucw'; 11 | const baseURL = config.use?.baseURL; 12 | 13 | // Use signed-in state of 'userStorageState.json'. 14 | useUserStorageState(); 15 | test.beforeEach(async ({ page }) => { 16 | await gotoPageAndExpectUrl(page, pageUrl); 17 | }); 18 | 19 | test('用路侧模拟器发送[弱势交通参与者碰撞预警]数据', async ({ page }) => { 20 | await page.goto(`${baseURL}:6688`); 21 | await connectMqtt(page); 22 | await checkDataset(page, 'VPTC_CW_track_stright'); 23 | await checkDataset(page, 'VPTC_CW_track_turn'); 24 | // 直到loading结束再点击发送按钮 25 | await page.locator('#loading-VPTC_CW_track_stright').waitFor({ state: 'hidden' }); 26 | await page.locator('#loading-VPTC_CW_track_turn').waitFor({ state: 'hidden' }); 27 | await page.click('#publishDataSetButton'); 28 | await page.waitForTimeout(10000); // 发送10s数据后停止发送 29 | await page.click('#publishDataSetButton'); 30 | }); 31 | 32 | test('成功接收到数据', async ({ page }) => { 33 | page.reload(); 34 | 35 | await checkTableRowLength(page, 3); 36 | }); 37 | 38 | test('成功通过下拉框筛选数据', async ({ page }) => { 39 | page.goto(pageUrl); 40 | await setQuerySelectValue(page, '#collisionType', 1); 41 | checkEmptyTable(page); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /mock/rsm.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { parse } from 'url'; 3 | 4 | const genList = (pageNum: number, pageSize: number) => { 5 | const data: Event.RSMListItem[] = []; 6 | 7 | for (let i = 0; i < pageSize; i += 1) { 8 | const index = (pageNum - 1) * 10 + i + 1; 9 | data.push({ 10 | id: index, 11 | targetId: index, 12 | participantType: ['unknown', 'motor', 'non-motor', 'pedestrian', 'rsu'][index % 4], 13 | dataSource: 'RSU', 14 | lng: 33.23423423, 15 | lat: 122.23423423, 16 | createTime: '2022-03-14 23:12:00', 17 | }); 18 | } 19 | data.reverse(); 20 | return data; 21 | }; 22 | 23 | let tableDataSource = genList(1, 40); 24 | 25 | function getList(req: Request, res: Response, u: string) { 26 | let realUrl = u; 27 | if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { 28 | realUrl = req.url; 29 | } 30 | const { pageNum = 1, pageSize = 10 } = req.query; 31 | const params = parse(realUrl, true).query as unknown as API.PageParams & 32 | Event.RSMListItem & { filter: any }; 33 | 34 | let dataSource = [...tableDataSource].slice( 35 | ((pageNum as number) - 1) * (pageSize as number), 36 | (pageNum as number) * (pageSize as number), 37 | ); 38 | 39 | if (params.participantType) { 40 | dataSource = dataSource.filter((data) => data?.participantType === params.participantType); 41 | } 42 | 43 | const result = { 44 | data: { 45 | data: dataSource, 46 | total: params.participantType ? dataSource.length : tableDataSource.length, 47 | }, 48 | }; 49 | 50 | return res.json(result); 51 | } 52 | 53 | export default { 54 | 'GET /api/rsm': getList, 55 | }; 56 | -------------------------------------------------------------------------------- /e2e/pages/maintenance/Query.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import { clickBackToListBtn } from '../../utils/detail'; 3 | import { globalModalSubmitBtn, setQuerySelectValue, setSelectValue } from '../../utils/form'; 4 | import { 5 | checkDetailUrl, 6 | checkSuccessMsg, 7 | gotoPageAndExpectUrl, 8 | useUserStorageState, 9 | } from '../../utils/global'; 10 | import { 11 | clickConfirmModalOkBtn, 12 | clickCreateBtn, 13 | clickDeleteTextBtn, 14 | clickDetailTextBtn, 15 | } from '../../utils/table'; 16 | 17 | test.describe('The Query Page', () => { 18 | const pageUrl = '/maintenance/query'; 19 | 20 | // Use signed-in state of 'userStorageState.json'. 21 | useUserStorageState(); 22 | 23 | test.beforeEach(async ({ page }) => { 24 | await gotoPageAndExpectUrl(page, pageUrl); 25 | }); 26 | 27 | test('successfully create query', async ({ page }) => { 28 | await clickCreateBtn(page); 29 | 30 | await setSelectValue(page, 'queryType', '#queryType_list'); 31 | await setSelectValue(page, 'timeType', '#timeType_list'); 32 | await setSelectValue(page, 'rsus', '#rsus_list'); 33 | 34 | await globalModalSubmitBtn(page); 35 | await checkSuccessMsg(page); 36 | }); 37 | 38 | test('successfully view detail', async ({ page }) => { 39 | await setQuerySelectValue(page, '#rsuId'); 40 | await clickDetailTextBtn(page); 41 | await checkDetailUrl(page, pageUrl); 42 | await clickBackToListBtn(page); 43 | }); 44 | 45 | test('successfully delete query', async ({ page }) => { 46 | await setQuerySelectValue(page, '#rsuId'); 47 | await clickDeleteTextBtn(page); 48 | await clickConfirmModalOkBtn(page); 49 | await checkSuccessMsg(page); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /mock/eventInfo.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { parse } from 'url'; 3 | 4 | const genList = (pageNum: number, pageSize: number) => { 5 | const data: Event.EventInfoListItem[] = []; 6 | 7 | for (let i = 0; i < pageSize; i += 1) { 8 | const index = (pageNum - 1) * 10 + i + 1; 9 | data.push({ 10 | id: index, 11 | name: `事件名称 ${index}`, 12 | category: '异常路况', 13 | eventType: '交通事故', 14 | countryName: '中国', 15 | provinceName: '江苏省', 16 | cityName: '南京市', 17 | areaName: '江宁区', 18 | address: '秣周东路交叉路口', 19 | time: '2022-03-14 23:12:00', 20 | }); 21 | } 22 | data.reverse(); 23 | return data; 24 | }; 25 | 26 | let tableDataSource = genList(1, 40); 27 | 28 | function getList(req: Request, res: Response, u: string) { 29 | let realUrl = u; 30 | if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { 31 | realUrl = req.url; 32 | } 33 | const { pageNum = 1, pageSize = 10 } = req.query; 34 | const params = parse(realUrl, true).query as unknown as API.PageParams & 35 | Event.EventInfoListItem & { filter: any }; 36 | 37 | let dataSource = [...tableDataSource].slice( 38 | ((pageNum as number) - 1) * (pageSize as number), 39 | (pageNum as number) * (pageSize as number), 40 | ); 41 | 42 | if (params.name) { 43 | dataSource = dataSource.filter((data) => data?.name?.includes(params.name || '')); 44 | } 45 | 46 | const result = { 47 | data: { 48 | data: dataSource, 49 | total: params.name ? dataSource.length : tableDataSource.length, 50 | }, 51 | }; 52 | 53 | return res.json(result); 54 | } 55 | 56 | export default { 57 | 'GET /api/event': getList, 58 | }; 59 | -------------------------------------------------------------------------------- /mock/radar.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { parse } from 'url'; 3 | 4 | const genList = (current: number, pageSize: number) => { 5 | const data: Device.CameraListItem[] = []; 6 | 7 | for (let i = 0; i < pageSize; i += 1) { 8 | const index = (current - 1) * 10 + i + 1; 9 | data.push({ 10 | id: index, 11 | deviceName: `test ${index}`, 12 | code: 'KV1233', 13 | deviceId: `rsd1234567732111${index}`, 14 | status: i % 10 > 6 ? 1 : 0, 15 | updateTime: '2022-03-14 23:12:00', 16 | }); 17 | } 18 | data.reverse(); 19 | return data; 20 | }; 21 | 22 | let tableDataSource = genList(1, 40); 23 | 24 | function getList(req: Request, res: Response, u: string) { 25 | let realUrl = u; 26 | if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { 27 | realUrl = req.url; 28 | } 29 | const { current = 1, pageSize = 10 } = req.query; 30 | const params = parse(realUrl, true).query as unknown as API.PageParams & 31 | Device.CameraListItem & { filter: any }; 32 | 33 | let dataSource = [...tableDataSource].slice( 34 | ((current as number) - 1) * (pageSize as number), 35 | (current as number) * (pageSize as number), 36 | ); 37 | 38 | if (params.deviceName) { 39 | dataSource = dataSource.filter((data) => data?.deviceName?.includes(params.deviceName || '')); 40 | } 41 | 42 | const result = { 43 | data: dataSource, 44 | total: params.deviceName ? dataSource.length : tableDataSource.length, 45 | success: true, 46 | pageSize, 47 | current: parseInt(`${params.pageNum}`, 10) || 1, 48 | }; 49 | 50 | return res.json(result); 51 | } 52 | 53 | export default { 54 | 'GET /api/radar/list': getList, 55 | }; 56 | -------------------------------------------------------------------------------- /mock/camera.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { parse } from 'url'; 3 | 4 | const genList = (current: number, pageSize: number) => { 5 | const data: Device.CameraListItem[] = []; 6 | 7 | for (let i = 0; i < pageSize; i += 1) { 8 | const index = (current - 1) * 10 + i + 1; 9 | data.push({ 10 | id: index, 11 | deviceName: `设备名称 ${index}`, 12 | code: 'KV1233', 13 | deviceId: `rsd1234567732111${index}`, 14 | status: i % 10 > 6 ? 1 : 0, 15 | updateTime: '2022-03-14 23:12:00', 16 | }); 17 | } 18 | data.reverse(); 19 | return data; 20 | }; 21 | 22 | let tableDataSource = genList(1, 40); 23 | 24 | function getCameraList(req: Request, res: Response, u: string) { 25 | let realUrl = u; 26 | if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { 27 | realUrl = req.url; 28 | } 29 | const { current = 1, pageSize = 10 } = req.query; 30 | const params = parse(realUrl, true).query as unknown as API.PageParams & 31 | Device.CameraListItem & { filter: any }; 32 | 33 | let dataSource = [...tableDataSource].slice( 34 | ((current as number) - 1) * (pageSize as number), 35 | (current as number) * (pageSize as number), 36 | ); 37 | 38 | if (params.deviceName) { 39 | dataSource = dataSource.filter((data) => data?.deviceName?.includes(params.deviceName || '')); 40 | } 41 | 42 | const result = { 43 | data: dataSource, 44 | total: params.deviceName ? dataSource.length : tableDataSource.length, 45 | success: true, 46 | pageSize, 47 | current: parseInt(`${params.pageNum}`, 10) || 1, 48 | }; 49 | 50 | return res.json(result); 51 | } 52 | 53 | export default { 54 | 'GET /api/camera/list': getCameraList, 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/RightContent/index.less: -------------------------------------------------------------------------------- 1 | @import (reference) '~antd/es/style/themes/index'; 2 | 3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025); 4 | 5 | .menu { 6 | :global(.anticon) { 7 | margin-right: 8px; 8 | } 9 | 10 | :global(.ant-dropdown-menu-item) { 11 | min-width: 120px; 12 | } 13 | } 14 | 15 | .right { 16 | display: flex; 17 | float: right; 18 | height: 48px; 19 | margin-left: auto; 20 | overflow: hidden; 21 | 22 | .action { 23 | display: flex; 24 | align-items: center; 25 | height: 48px; 26 | padding: 0 12px; 27 | cursor: pointer; 28 | transition: all 0.3s; 29 | 30 | > span { 31 | vertical-align: middle; 32 | } 33 | 34 | &:hover { 35 | background: @pro-header-hover-bg; 36 | } 37 | 38 | &:global(.opened) { 39 | background: @pro-header-hover-bg; 40 | } 41 | } 42 | 43 | .search { 44 | padding: 0 12px; 45 | 46 | &:hover { 47 | background: transparent; 48 | } 49 | } 50 | 51 | .account { 52 | .avatar { 53 | margin-right: 8px; 54 | color: #fff; 55 | vertical-align: top; 56 | background: #c4c9d6; 57 | } 58 | } 59 | } 60 | 61 | .dark { 62 | .action { 63 | &:hover { 64 | background: #252a3d; 65 | } 66 | 67 | &:global(.opened) { 68 | background: #252a3d; 69 | } 70 | } 71 | } 72 | 73 | @media only screen and (max-width: @screen-md) { 74 | :global(.ant-divider-vertical) { 75 | vertical-align: unset; 76 | } 77 | 78 | .name { 79 | display: none; 80 | } 81 | 82 | .right { 83 | position: absolute; 84 | top: 0; 85 | right: 12px; 86 | 87 | .account { 88 | .avatar { 89 | margin-right: 0; 90 | } 91 | } 92 | 93 | .search { 94 | display: none; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /mock/device.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { parse } from 'url'; 3 | 4 | const genList = (current: number, pageSize: number) => { 5 | const data: Device.DeviceListItem[] = []; 6 | 7 | for (let i = 0; i < pageSize; i += 1) { 8 | const index = (current - 1) * 10 + i + 1; 9 | data.push({ 10 | id: index, 11 | name: `设备名称 ${index}`, 12 | code: `KV1233${index}`, 13 | serialNumber: `rsd1234567732111${index}`, 14 | location: '江苏省南京市江宁区', 15 | onlineStatus: i % 10 > 5 ? 1 : 0, 16 | status: i % 10 > 6 ? 1 : 0, 17 | sendStatus: i % 10 > 5 ? 1 : 0, 18 | createTime: '2022-03-14 23:12:00', 19 | }); 20 | } 21 | data.reverse(); 22 | return data; 23 | }; 24 | 25 | let tableDataSource = genList(1, 40); 26 | 27 | function getRsuList(req: Request, res: Response, u: string) { 28 | let realUrl = u; 29 | if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { 30 | realUrl = req.url; 31 | } 32 | const { current = 1, pageSize = 10 } = req.query; 33 | const params = parse(realUrl, true).query as unknown as API.PageParams & 34 | Device.DeviceListItem & { filter: any }; 35 | 36 | let dataSource = [...tableDataSource].slice( 37 | ((current as number) - 1) * (pageSize as number), 38 | (current as number) * (pageSize as number), 39 | ); 40 | 41 | if (params.name) { 42 | dataSource = dataSource.filter((data) => data?.name?.includes(params.name || '')); 43 | } 44 | 45 | const result = { 46 | data: dataSource, 47 | total: params.name ? dataSource.length : tableDataSource.length, 48 | success: true, 49 | pageSize, 50 | current: parseInt(`${params.pageNum}`, 10) || 1, 51 | }; 52 | 53 | return res.json(result); 54 | } 55 | 56 | export default { 57 | 'GET /api/rsu/list': getRsuList, 58 | }; 59 | -------------------------------------------------------------------------------- /src/assets/images/site_mode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | moshi 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/assets/images/site_mode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | moshi 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /e2e/pages/event/Icw.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import config from '../../../playwright.config'; 3 | import { setQuerySelectValue } from '../../utils/form'; 4 | import { gotoPageAndExpectUrl, useUserStorageState } from '../../utils/global'; 5 | import { checkDataset, connectMqtt } from '../../utils/road_simulator'; 6 | import { checkTableRowLength, getTableTotal } from '../../utils/table'; 7 | 8 | test.describe('The Sds Page', () => { 9 | const pageUrl = '/event/icw'; 10 | const baseURL = config.use?.baseURL; 11 | 12 | // Use signed-in state of 'userStorageState.json'. 13 | useUserStorageState(); 14 | test.beforeEach(async ({ page }) => { 15 | await gotoPageAndExpectUrl(page, pageUrl); 16 | }); 17 | 18 | test('用路侧模拟器发送[交叉路口碰撞预警]数据', async ({ page }) => { 19 | await page.goto(`${baseURL}:6688`); 20 | await connectMqtt(page); 21 | await checkDataset(page, 'ICW_track'); 22 | await checkDataset(page, 'CLC_track'); //如果只发送交叉路口碰撞预警数据,下拉框筛选可能选不出数据或者数据总数不变。 23 | //所以要发送一些别的数据,使得下拉框筛选后数据总数减少以此验证下拉框筛选数据是有效的。 24 | await checkDataset(page, 'msg_VIR_CLC'); 25 | 26 | // 直到loading结束再点击发送按钮 27 | await page.locator('#loading-ICW_track').waitFor({ state: 'hidden' }); 28 | await page.locator('#loading-CLC_track').waitFor({ state: 'hidden' }); 29 | await page.click('#publishDataSetButton'); 30 | await page.waitForTimeout(10000); // 发送10s数据后停止发送 31 | await page.click('#publishDataSetButton'); 32 | }); 33 | 34 | test('成功接收到数据', async ({ page }) => { 35 | await checkTableRowLength(page, 3); 36 | }); 37 | 38 | test('成功通过下拉框筛选数据', async ({ page }) => { 39 | const res_before = await getTableTotal(page); 40 | await setQuerySelectValue(page, '#collisionType', 1); 41 | const res_after = await getTableTotal(page); 42 | expect(Number(res_before)).toBeGreaterThan(Number(res_after)); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/pages/deviceManagement/RSUManagement/DeviceList/components/NotRegisteredList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import type { ActionType, TableProColumns } from '@ant-design/pro-table'; 3 | import BaseProTable from 'edge-src/components/BaseProTable'; 4 | import { deleteTemporaryDevice, notRegisterDeviceList } from 'edge-src/services/device/device'; 5 | import CreateDeviceModal from '../CreateDeviceModal'; 6 | import { Divider } from 'antd'; 7 | import { confirmModal } from 'edge-src/components/ConfirmModal'; 8 | 9 | const RegisteredList: React.FC = () => { 10 | const actionRef = useRef(); 11 | const columns: TableProColumns[] = [ 12 | { 13 | title: t('RSU Name'), 14 | dataIndex: 'rsuName', 15 | search: true, 16 | }, 17 | { 18 | title: t('Serial Number'), 19 | dataIndex: 'rsuEsn', 20 | search: true, 21 | }, 22 | { 23 | title: t('Protocol Version'), 24 | dataIndex: 'version', 25 | }, 26 | { 27 | title: t('Operate'), 28 | fixed: 'right', 29 | valueType: 'option', 30 | render: (_, row) => [ 31 | actionRef.current?.reload()} 36 | />, 37 | , 38 | 41 | confirmModal({ 42 | id: row.id, 43 | content: t('Are you sure you want to delete this device?'), 44 | modalFn: deleteTemporaryDevice, 45 | actionRef, 46 | }) 47 | } 48 | > 49 | {t('Delete')} 50 | , 51 | ], 52 | }, 53 | ]; 54 | return ; 55 | }; 56 | 57 | export default RegisteredList; 58 | -------------------------------------------------------------------------------- /src/pages/maintenanceManagement/businessConfig/ConfigList/components/SelectDeviceModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, message, Modal } from 'antd'; 3 | import { PlusOutlined } from '@ant-design/icons'; 4 | import { deviceList } from 'edge-src/services/device/device'; 5 | import ParameterDeviceList from 'edge-src/components/ParameterDeviceList'; 6 | 7 | type SelectDeviceProps = { 8 | defaultSelectedIds: number[]; 9 | selected: (data: Device.DeviceListItem[]) => void; 10 | }; 11 | 12 | const SelectDeviceModal: React.FC = ({ defaultSelectedIds, selected }) => { 13 | const [isVisible, setIsVisible] = useState(false); 14 | const [selectedData, setSelectedData] = useState([]); 15 | const confirm = () => { 16 | if (!selectedData.length) { 17 | message.warn(t('Please choose an RSU device')); 18 | return; 19 | } 20 | 21 | selected(selectedData.filter((item) => !defaultSelectedIds.includes(item.id))); 22 | setIsVisible(false); 23 | }; 24 | 25 | return ( 26 | <> 27 | 35 | setIsVisible(false)} 44 | > 45 | setSelectedData(rows), 51 | }} 52 | /> 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default SelectDeviceModal; 59 | -------------------------------------------------------------------------------- /mock/parameterConfig.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { parse } from 'url'; 3 | 4 | const genList = (current: number, pageSize: number) => { 5 | const data: Config.ParameterListItem[] = []; 6 | 7 | for (let i = 0; i < pageSize; i += 1) { 8 | const index = (current - 1) * 10 + i + 1; 9 | data.push({ 10 | id: index, 11 | templateName: `test模板${index}`, 12 | bsm: { 13 | samplingMethod: '全局采样', 14 | samplingRate: 12, 15 | upsideCap: 1200, 16 | }, 17 | rsi: { 18 | amount: 200, 19 | }, 20 | rsm: { 21 | upsideCap: 1200, 22 | downsideCap: 1200, 23 | }, 24 | spat: { 25 | upsideCap: 1200, 26 | downsideCap: 1200, 27 | }, 28 | }); 29 | } 30 | data.reverse(); 31 | return data; 32 | }; 33 | 34 | let tableDataSource = genList(1, 20); 35 | 36 | function getList(req: Request, res: Response, u: string) { 37 | let realUrl = u; 38 | if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { 39 | realUrl = req.url; 40 | } 41 | const { current = 1, pageSize = 10 } = req.query; 42 | const params = parse(realUrl, true).query as unknown as API.PageParams & 43 | Config.ParameterListItem & { filter: any }; 44 | 45 | let dataSource = [...tableDataSource].slice( 46 | ((current as number) - 1) * (pageSize as number), 47 | (current as number) * (pageSize as number), 48 | ); 49 | 50 | if (params.templateName) { 51 | dataSource = dataSource.filter((data) => 52 | data?.templateName?.includes(params.templateName || ''), 53 | ); 54 | } 55 | 56 | const result = { 57 | data: dataSource, 58 | total: params.templateName ? dataSource.length : tableDataSource.length, 59 | success: true, 60 | pageSize, 61 | current: parseInt(`${params.pageNum}`, 10) || 1, 62 | }; 63 | 64 | return res.json(result); 65 | } 66 | 67 | export default { 68 | 'GET /api/parameter/list': getList, 69 | }; 70 | -------------------------------------------------------------------------------- /src/services/device/device.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // RSU 设备列表 4 | export async function deviceList({ 5 | countryName, 6 | ...params 7 | }: API.PageParams & { countryName?: string[]; areaCode?: string }) { 8 | if (countryName?.length) { 9 | params.areaCode = countryName[countryName.length - 1]; 10 | } 11 | return request>(`/v1/rsus`, { 12 | method: 'GET', 13 | params, 14 | }); 15 | } 16 | 17 | // 未注册 RSU 设备列表 18 | export async function notRegisterDeviceList(params: API.PageParams) { 19 | return request>(`/v1/rsu_tmps`, { 20 | method: 'GET', 21 | params, 22 | }); 23 | } 24 | 25 | export async function countries() { 26 | return request(`/v1/countries`, { 27 | method: 'get', 28 | params: { cascade: true }, 29 | }); 30 | } 31 | 32 | // 添加 RSU 设备 33 | export async function createDevice(data: Device.CreateDeviceParams) { 34 | return request(`/v1/rsus`, { 35 | method: 'POST', 36 | data, 37 | }); 38 | } 39 | 40 | // RSU 设备详情 41 | export async function deviceInfo(id: number) { 42 | return request(`/v1/rsus/${id}`, { 43 | method: 'GET', 44 | }); 45 | } 46 | 47 | // RSU 设备详情-运行信息 48 | export async function runningInfo(id: number) { 49 | return request(`/v1/rsus/${id}/running`, { 50 | method: 'GET', 51 | }); 52 | } 53 | 54 | // 编辑 RSU 设备 55 | export async function updateDevice(id: number, data: Device.CreateDeviceParams) { 56 | return request(`/v1/rsus/${id}`, { 57 | method: 'PATCH', 58 | data, 59 | }); 60 | } 61 | 62 | // 删除 RSU 设备 63 | export async function deleteDevice(id: number) { 64 | return request(`/v1/rsus/${id}`, { 65 | method: 'DELETE', 66 | }); 67 | } 68 | 69 | // 删除未注册 RSU 设备 70 | export async function deleteTemporaryDevice(id: number) { 71 | return request(`/v1/rsu_tmps/${id}`, { 72 | method: 'DELETE', 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /e2e/pages/event/Rsi.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import config from '../../../playwright.config'; 3 | import { clickBackToListBtn } from '../../utils/detail'; 4 | import { setQuerySelectValue } from '../../utils/form'; 5 | import { checkDetailUrl, gotoPageAndExpectUrl, useUserStorageState } from '../../utils/global'; 6 | import { checkDataset, connectMqtt } from '../../utils/road_simulator'; 7 | import { clickDetailTextBtn, getTableTotal } from '../../utils/table'; 8 | test.describe('The Rsi Page', () => { 9 | const pageUrl = '/event/rsi'; 10 | const baseURL = config.use?.baseURL; 11 | 12 | // Use signed-in state of 'userStorageState.json'. 13 | useUserStorageState(); 14 | test.beforeEach(async ({ page }) => { 15 | await gotoPageAndExpectUrl(page, pageUrl); 16 | }); 17 | 18 | test('用路侧模拟器发送[路侧单元信息]数据并成功收到消息', async ({ page }) => { 19 | //先统计列表有多少数据再用模拟器发送消息 20 | await page.goto(pageUrl); 21 | const rows_init = await getTableTotal(page); 22 | await page.goto(`${baseURL}:6688`); 23 | await connectMqtt(page); 24 | await checkDataset(page, 'RSI_data'); 25 | 26 | // 直到loading结束再点击发送按钮 27 | await page.locator('#loading-RSI_data').waitFor({ state: 'hidden' }); 28 | await page.click('#publishDataSetButton'); 29 | await page.waitForTimeout(5000); // 发送5s数据后停止发送 30 | await page.click('#publishDataSetButton'); 31 | 32 | await page.goto(pageUrl); 33 | const rows_after = await getTableTotal(page); // 经过模拟器发送后表格数据应该比原来多 34 | expect(Number(rows_init)).toBeLessThan(Number(rows_after)); 35 | }); 36 | 37 | test('成功通过下拉框筛选数据', async ({ page }) => { 38 | const res_before = await getTableTotal(page); 39 | await setQuerySelectValue(page, '#eventType', 1); 40 | const res_after = await getTableTotal(page); 41 | expect(Number(res_before)).toBeGreaterThan(Number(res_after)); 42 | }); 43 | 44 | test('成功查看详情', async ({ page }) => { 45 | await clickDetailTextBtn(page); 46 | await checkDetailUrl(page, pageUrl); 47 | await clickBackToListBtn(page); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /e2e/pages/maintenance/Log.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import { generateNumLetter } from '../../utils'; 3 | import { globalModalSubmitBtn, setModalFormItemValue, setSelectValue } from '../../utils/form'; 4 | import { checkSuccessMsg, gotoPageAndExpectUrl, useUserStorageState } from '../../utils/global'; 5 | 6 | import { 7 | clickConfirmModalOkBtn, 8 | clickCreateBtn, 9 | clickDeleteTextBtn, 10 | clickEditBtn, 11 | } from '../../utils/table'; 12 | 13 | test.describe('The Log Page', () => { 14 | const randomNumLetter = generateNumLetter(); 15 | const pageUrl = '/maintenance/log'; 16 | 17 | // Use signed-in state of 'userStorageState.json'. 18 | useUserStorageState(); 19 | 20 | test.beforeEach(async ({ page }) => { 21 | await gotoPageAndExpectUrl(page, pageUrl); 22 | }); 23 | 24 | test('successfully create log', async ({ page }) => { 25 | await clickCreateBtn(page); 26 | 27 | await setModalFormItemValue(page, '#uploadUrl', randomNumLetter); 28 | await setModalFormItemValue(page, '#userId', randomNumLetter); 29 | await setModalFormItemValue(page, '#password', randomNumLetter); 30 | await setSelectValue(page, 'transprotocal', '#transprotocal_list'); 31 | await setSelectValue(page, 'rsus', '#rsus_list'); 32 | 33 | await globalModalSubmitBtn(page); 34 | await checkSuccessMsg(page); 35 | }); 36 | 37 | test('successfully edit log', async ({ page }) => { 38 | await clickEditBtn(page); 39 | 40 | await setModalFormItemValue(page, '#uploadUrl', `update_${randomNumLetter}`); 41 | await setModalFormItemValue(page, '#userId', `update_${randomNumLetter}`); 42 | await setModalFormItemValue(page, '#password', `update_${randomNumLetter}`); 43 | await setSelectValue(page, 'transprotocal', '#transprotocal_list'); 44 | 45 | await globalModalSubmitBtn(page); 46 | await checkSuccessMsg(page); 47 | }); 48 | 49 | test('successfully delete log', async ({ page }) => { 50 | await clickDeleteTextBtn(page); 51 | await clickConfirmModalOkBtn(page); 52 | await checkSuccessMsg(page); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/services/config/map.ts: -------------------------------------------------------------------------------- 1 | import request from '../request'; 2 | 3 | // MAP 配置列表 4 | export async function mapConfigList({ 5 | countryName, 6 | ...params 7 | }: API.PageParams & { countryName?: string[]; areaCode?: string }) { 8 | if (countryName?.length) { 9 | params.areaCode = countryName[countryName.length - 1]; 10 | } 11 | return request>(`/v1/maps`, { 12 | method: 'GET', 13 | params, 14 | }); 15 | } 16 | 17 | // 添加 MAP 配置 18 | export async function createMapConfig(data: Config.CreateMapConfigParams) { 19 | return request(`/v1/maps`, { 20 | method: 'POST', 21 | data, 22 | }); 23 | } 24 | 25 | // MAP 配置详情 26 | export async function mapConfigInfo(id: number) { 27 | return request(`/v1/maps/${id}`, { 28 | method: 'GET', 29 | }); 30 | } 31 | 32 | // MAP 配置已绑定 RSU 列表 33 | export async function mapRSUList({ mapId, ...params }: API.PageParams & { mapId: number }) { 34 | return request>(`/v1/maps/${mapId}/rsus`, { 35 | method: 'GET', 36 | params, 37 | }); 38 | } 39 | 40 | // 添加 MAP 绑定 RSU 41 | export async function createMapRSU(id: number, data: { rusId: number[] }) { 42 | return request(`/v1/maps/${id}/rsus`, { 43 | method: 'POST', 44 | data, 45 | }); 46 | } 47 | 48 | // 删除 MAP 绑定的 RSU 49 | export async function deleteMapRSU(id: number, rsuId: number) { 50 | return request(`/v1/maps/${id}/rsus/${rsuId}`, { 51 | method: 'DELETE', 52 | }); 53 | } 54 | 55 | // 编辑 MAP 配置 56 | export async function updateMapConfig(id: number, data: Config.CreateMapConfigParams) { 57 | return request(`/v1/maps/${id}`, { 58 | method: 'PUT', 59 | data, 60 | }); 61 | } 62 | 63 | // 删除 MAP 配置 64 | export async function deleteMapConfig(id: number) { 65 | return request(`/v1/maps/${id}`, { 66 | method: 'DELETE', 67 | }); 68 | } 69 | 70 | // 下载 MAP 配置 71 | export async function downloadMapConfig(id: number) { 72 | return request(`/v1/maps/${id}/data`, { 73 | method: 'GET', 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /e2e/utils/global.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from '@playwright/test'; 2 | import { expect, test } from '@playwright/test'; 3 | const username = 'admin'; 4 | const password = 'dandelion'; 5 | 6 | // Save signed-in state to 'userStorageState.json'. 7 | export const saveUserStorageState = async (page: Page) => { 8 | await page.context().storageState({ path: 'e2e/storage/userStorageState.json' }); 9 | }; 10 | export const Login = async (page: Page) => { 11 | await page.goto('/user/login'); 12 | await page.fill('#username', username); 13 | await page.fill('#password', password); 14 | const submitBtn = page.locator('.ant-form button.ant-btn'); 15 | await submitBtn.click(); 16 | await expect(page).toHaveURL('/device/rsu'); 17 | 18 | // Save signed-in state to 'userStorageState.json'. 19 | await saveUserStorageState(page); 20 | }; 21 | export const gotoPageAndExpectUrl = async (page: Page, url: string) => { 22 | await Login(page); 23 | await page.goto(url); 24 | await expect(page).toHaveURL(new RegExp(url)); 25 | }; 26 | 27 | // Use signed-in state of 'userStorageState.json'. 28 | export const useUserStorageState = () => { 29 | test.use({ storageState: 'e2e/storage/userStorageState.json' }); 30 | }; 31 | 32 | export const checkSuccessMsg = async (page: Page) => { 33 | return await expect(page.locator('.ant-message .ant-message-success')).toBeVisible(); 34 | }; 35 | 36 | export const checkErrorMsg = async (page: Page) => { 37 | return await expect(page.locator('.ant-message .ant-message-error')).toBeVisible(); 38 | }; 39 | 40 | export const checkDetaillWindow = async (page: Page) => { 41 | return await expect(page.locator('.ant-modal-content')).toBeVisible(); 42 | }; 43 | 44 | export const closePopWindow = async (page: Page) => { 45 | await page.locator('.ant-modal-close-x').click(); 46 | }; 47 | 48 | export const uploadFile = async (page: Page, selector: string, file_path: string) => { 49 | await page.setInputFiles(selector, file_path); 50 | }; 51 | 52 | export const checkDetailUrl = async (page: Page, pageUrl: string) => { 53 | await expect(page).toHaveURL(new RegExp(`${pageUrl}/details`)); 54 | }; 55 | -------------------------------------------------------------------------------- /.github/workflows/test-e2e-aio.yml: -------------------------------------------------------------------------------- 1 | # 1. build temporary image: openv2x/edgeview:temp 2 | # 2. use openv2x/edgeview:temp replace openv2x/edgeview:latest in docker-compose-service.yaml 3 | # 3. all-in-one deploy 4 | # 4. run e2e test 5 | 6 | name: front-e2e-test 7 | 8 | on: 9 | pull_request: 10 | branches: 11 | - master 12 | # This ensures that previous jobs for the branch are canceled when the branch is updated. 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.head_ref }} 15 | cancel-in-progress: true 16 | env: 17 | OPENV2X_EXTERNAL_IP: 127.0.0.1 18 | OPENV2X_REDIS_ROOT: password 19 | OPENV2X_MARIADB_ROOT: password 20 | OPENV2X_MARIADB_DANDELION: password 21 | OPENV2X_EMQX_ROOT: password 22 | jobs: 23 | front-e2e-test: 24 | runs-on: ubuntu-20.04 25 | steps: 26 | - name: Check out code into the Go module directory 27 | uses: actions/checkout@v2 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: build temporary image 32 | run: | 33 | docker build -t openv2x/edgeview:temp . 34 | 35 | - name: deploy service 36 | run: | 37 | sudo rm -rf openv2x-aio-master.tar.gz && wget https://openv2x.oss-ap-southeast-1.aliyuncs.com/deploy/master/openv2x-aio-master.tar.gz 38 | sudo rm -rf src && tar zxvf openv2x-aio-master.tar.gz 39 | sudo sed -i "s/8084/8085/" src/deploy/docker-compose-pre.yaml && sudo sed -i "s/8084/8085/" src/deploy/docker-compose-service.yaml 40 | sed -i "s#openv2x/edgeview:latest#openv2x/edgeview:temp#" src/deploy/docker-compose-service.yaml 41 | cd src && chmod +x ./install.sh && sudo -E bash ./install.sh 42 | 43 | - name: Install dependencies 44 | run: npm install 45 | - name: Install Playwright Browsers 46 | run: npx playwright install --with-deps 47 | - name: Run Playwright tests 48 | run: npx playwright test -g event 49 | - uses: actions/upload-artifact@v3 50 | if: always() 51 | with: 52 | name: playwright-report 53 | path: e2e/playwright-report 54 | retention-days: 30 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/components/CollisionWarningList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import type { ActionType, TableProColumns } from '@ant-design/pro-table'; 3 | import BaseProTable from '../BaseProTable'; 4 | import { statusOptionFormat } from 'edge-src/utils'; 5 | import { ICWCollisionTypeOptions } from 'edge-src/utils/constants'; 6 | import { intersectionCollisionWarningList } from 'edge-src/services/event/icw'; 7 | import LonLatUnit from '../LonLatUnit'; 8 | 9 | type CollisionWarningProps = { 10 | type: 'ICW' | 'VRUCW'; 11 | navigator: (data: Event.ICWListItem) => void; 12 | }; 13 | 14 | const CollisionWarningList: React.FC = ({ type, navigator }) => { 15 | const actionRef = useRef(); 16 | const columns: TableProColumns[] = [ 17 | { title: t('ID'), dataIndex: 'id' }, 18 | { 19 | title: t('Sensor Longitude'), 20 | dataIndex: ['sensorPos', 'lon'], 21 | render: (_, { sensorPos: { lon } }) => , 22 | }, 23 | { 24 | title: t('Sensor Latitude'), 25 | dataIndex: ['sensorPos', 'lat'], 26 | render: (_, { sensorPos: { lat } }) => , 27 | }, 28 | { 29 | title: t('Collision Type'), 30 | dataIndex: 'collisionType', 31 | valueType: 'select', 32 | valueEnum: statusOptionFormat(ICWCollisionTypeOptions), 33 | search: true, 34 | }, 35 | { title: t('Millisecond Time'), dataIndex: 'secMark' }, 36 | { 37 | title: t('Operate'), 38 | width: 160, 39 | fixed: 'right', 40 | valueType: 'option', 41 | render: (_, row) => [ 42 | navigator(row)}> 43 | {t('Details')} 44 | , 45 | ], 46 | }, 47 | { 48 | title: t('Reporting Time'), 49 | dataIndex: 'createTime', 50 | sorter: true, 51 | }, 52 | ]; 53 | return ( 54 | 60 | ); 61 | }; 62 | 63 | export default CollisionWarningList; 64 | -------------------------------------------------------------------------------- /e2e/pages/event/Rsm.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import config from '../../../playwright.config'; 3 | import { clickBackToListBtn } from '../../utils/detail'; 4 | import { setQuerySelectValue } from '../../utils/form'; 5 | import { checkDetailUrl, gotoPageAndExpectUrl, useUserStorageState } from '../../utils/global'; 6 | import { checkDataset, connectMqtt } from '../../utils/road_simulator'; 7 | import { clickDetailTextBtn, getTableTotal } from '../../utils/table'; 8 | test.describe('The Rsm Page', () => { 9 | const pageUrl = '/event/rsm'; 10 | const baseURL = config.use?.baseURL; 11 | 12 | // Use signed-in state of 'userStorageState.json'. 13 | useUserStorageState(); 14 | test.beforeEach(async ({ page }) => { 15 | await gotoPageAndExpectUrl(page, pageUrl); 16 | }); 17 | 18 | test('用路侧模拟器发送[路侧安全消息]数据并成功收到消息', async ({ page }) => { 19 | //先统计列表有多少数据再用模拟器发送消息 20 | await page.goto(pageUrl); 21 | const rows_init = await getTableTotal(page); 22 | await page.goto(`${baseURL}:6688`); 23 | await connectMqtt(page); 24 | await checkDataset(page, 'video_track'); 25 | await checkDataset(page, 'radar_track'); 26 | 27 | // 直到loading结束再点击发送按钮 28 | await page.locator('#loading-video_track').waitFor({ state: 'hidden' }); 29 | await page.locator('#loading-radar_track').waitFor({ state: 'hidden' }); 30 | await page.click('#publishDataSetButton'); 31 | await page.waitForTimeout(5000); // 发送5s数据后停止发送 32 | await page.click('#publishDataSetButton'); 33 | 34 | await page.goto(pageUrl); 35 | const rows_after = await getTableTotal(page); // 经过模拟器发送后表格数据应该比原来多 36 | expect(Number(rows_init)).toBeLessThan(Number(rows_after)); 37 | }); 38 | 39 | test('成功通过下拉框筛选数据', async ({ page }) => { 40 | const res_before = await getTableTotal(page); 41 | await setQuerySelectValue(page, '#ptcType', 1); 42 | const res_after = await getTableTotal(page); 43 | expect(Number(res_before)).toBeGreaterThan(Number(res_after)); 44 | }); 45 | 46 | test('成功查看详情', async ({ page }) => { 47 | await clickDetailTextBtn(page); 48 | await checkDetailUrl(page, pageUrl); 49 | await clickBackToListBtn(page); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/pages/eventManagement/SensorDataSharing/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import type { ActionType, TableProColumns } from '@ant-design/pro-table'; 3 | import BaseContainer from 'edge-src/components/BaseContainer'; 4 | import BaseProTable from 'edge-src/components/BaseProTable'; 5 | import { DSDEquipmentTypeOptions } from 'edge-src/utils/constants'; 6 | import { statusOptionFormat } from 'edge-src/utils'; 7 | import { sensorDataSharingList } from 'edge-src/services/event/sds'; 8 | import LonLatUnit from 'edge-src/components/LonLatUnit'; 9 | 10 | const SensorDataSharing: React.FC = () => { 11 | const actionRef = useRef(); 12 | const columns: TableProColumns[] = [ 13 | { title: t('ID'), dataIndex: 'id' }, 14 | { title: t('Message Number'), dataIndex: 'msgID' }, 15 | { 16 | title: t('Equipment Type'), 17 | dataIndex: 'equipmentType', 18 | valueType: 'select', 19 | valueEnum: statusOptionFormat(DSDEquipmentTypeOptions), 20 | search: true, 21 | }, 22 | { 23 | title: t('Sensor Longitude'), 24 | dataIndex: ['sensorPos', 'lon'], 25 | render: (_, { sensorPos: { lon } }) => , 26 | }, 27 | { 28 | title: t('Sensor Latitude'), 29 | dataIndex: ['sensorPos', 'lat'], 30 | render: (_, { sensorPos: { lat } }) => , 31 | }, 32 | { title: t('Millisecond Time'), dataIndex: 'secMark' }, 33 | { title: t('Vehicle ID'), dataIndex: 'egoID' }, 34 | { 35 | title: t('Vehicle Longitude'), 36 | dataIndex: ['egoPos', 'lon'], 37 | render: (_, { egoPos: { lon } }) => , 38 | }, 39 | { 40 | title: t('Vehicle Latitude'), 41 | dataIndex: ['egoPos', 'lat'], 42 | render: (_, { egoPos: { lat } }) => , 43 | }, 44 | { 45 | title: t('Reporting Time'), 46 | dataIndex: 'createTime', 47 | sorter: true, 48 | }, 49 | ]; 50 | return ( 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default SensorDataSharing; 58 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | /* eslint-disable no-underscore-dangle */ 3 | /* globals workbox */ 4 | workbox.core.setCacheNameDetails({ 5 | prefix: 'antd-pro', 6 | suffix: 'v5', 7 | }); 8 | // Control all opened tabs ASAP 9 | workbox.clientsClaim(); 10 | 11 | /** 12 | * Use precaching list generated by workbox in build process. 13 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching 14 | */ 15 | workbox.precaching.precacheAndRoute(self.__precacheManifest || []); 16 | 17 | /** 18 | * Register a navigation route. 19 | * https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route 20 | */ 21 | workbox.routing.registerNavigationRoute('/index.html'); 22 | 23 | /** 24 | * Use runtime cache: 25 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute 26 | * 27 | * Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc. 28 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies 29 | */ 30 | 31 | /** Handle API requests */ 32 | workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst()); 33 | 34 | /** Handle third party requests */ 35 | workbox.routing.registerRoute( 36 | /^https:\/\/gw\.alipayobjects\.com\//, 37 | workbox.strategies.networkFirst(), 38 | ); 39 | workbox.routing.registerRoute( 40 | /^https:\/\/cdnjs\.cloudflare\.com\//, 41 | workbox.strategies.networkFirst(), 42 | ); 43 | workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst()); 44 | 45 | /** Response to client after skipping waiting with MessageChannel */ 46 | addEventListener('message', (event) => { 47 | const replyPort = event.ports[0]; 48 | const message = event.data; 49 | if (replyPort && message && message.type === 'skip-waiting') { 50 | event.waitUntil( 51 | self.skipWaiting().then( 52 | () => { 53 | replyPort.postMessage({ 54 | error: null, 55 | }); 56 | }, 57 | (error) => { 58 | replyPort.postMessage({ 59 | error, 60 | }); 61 | }, 62 | ), 63 | ); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /src/pages/eventManagement/CooperativeLaneChange/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import type { ActionType, TableProColumns } from '@ant-design/pro-table'; 3 | import BaseContainer from 'edge-src/components/BaseContainer'; 4 | import BaseProTable from 'edge-src/components/BaseProTable'; 5 | import { CoordinationInfoTypeOptions, DriveBehaviorTypeOptions } from 'edge-src/utils/constants'; 6 | import { dataFormat, statusOptionFormat } from 'edge-src/utils'; 7 | import { cooperativeLaneChangeList } from 'edge-src/services/event/clc'; 8 | import LonLatUnit from 'edge-src/components/LonLatUnit'; 9 | 10 | const CooperativeLaneChange: React.FC = () => { 11 | const actionRef = useRef(); 12 | const columns: TableProColumns[] = [ 13 | { title: t('ID'), dataIndex: 'id' }, 14 | { title: t('Message Number'), dataIndex: 'msgID' }, 15 | { title: t('Millisecond Time'), dataIndex: 'secMark' }, 16 | { 17 | title: t('Longitude'), 18 | dataIndex: ['refPos', 'lon'], 19 | render: (_, { refPos: { lon } }) => , 20 | }, 21 | { 22 | title: t('Latitude'), 23 | dataIndex: ['refPos', 'lat'], 24 | render: (_, { refPos: { lat } }) => , 25 | }, 26 | { title: t('Vehicle ID'), dataIndex: 'vehID' }, 27 | { 28 | title: t('Driving Behavior'), 29 | dataIndex: ['driveSuggestion', 'suggestion'], 30 | valueType: 'select', 31 | valueEnum: statusOptionFormat(DriveBehaviorTypeOptions), 32 | }, 33 | { 34 | title: t('Request Valid Time'), 35 | dataIndex: ['driveSuggestion', 'lifeTime'], 36 | render: (_, { driveSuggestion: { lifeTime } }) => dataFormat(lifeTime * 10, 'ms'), 37 | }, 38 | { 39 | title: t('Coordination Information'), 40 | dataIndex: 'info', 41 | valueType: 'select', 42 | valueEnum: statusOptionFormat(CoordinationInfoTypeOptions), 43 | search: true, 44 | }, 45 | { 46 | title: t('Reporting Time'), 47 | dataIndex: 'createTime', 48 | sorter: true, 49 | }, 50 | ]; 51 | return ( 52 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default CooperativeLaneChange; 59 | -------------------------------------------------------------------------------- /src/pages/deviceManagement/RSUModelManagement/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import type { ActionType, TableProColumns } from '@ant-design/pro-table'; 3 | import { Divider } from 'antd'; 4 | import BaseContainer from 'edge-src/components/BaseContainer'; 5 | import BaseProTable from 'edge-src/components/BaseProTable'; 6 | import CreateModelModal from './components/CreateModelModal'; 7 | import { modelList, deleteModel } from 'edge-src/services/device/model'; 8 | import { confirmModal } from 'edge-src/components/ConfirmModal'; 9 | 10 | const RSUModelManagement: React.FC = () => { 11 | const actionRef = useRef(); 12 | 13 | const columns: TableProColumns[] = [ 14 | { 15 | title: t('Model Name'), 16 | dataIndex: 'name', 17 | ellipsis: true, 18 | search: true, 19 | }, 20 | { 21 | title: t('Manufacturer Name'), 22 | dataIndex: 'manufacturer', 23 | ellipsis: true, 24 | search: true, 25 | }, 26 | { 27 | title: t('Describe'), 28 | dataIndex: 'desc', 29 | ellipsis: true, 30 | }, 31 | { 32 | title: t('Creation Time'), 33 | dataIndex: 'createTime', 34 | }, 35 | { 36 | title: t('Operate'), 37 | width: 160, 38 | fixed: 'right', 39 | valueType: 'option', 40 | render: (_, row) => [ 41 | actionRef.current?.reload()} />, 42 | , 43 | 46 | confirmModal({ 47 | id: row.id, 48 | content: t('Are you sure you want to delete this model?'), 49 | modalFn: deleteModel, 50 | actionRef, 51 | }) 52 | } 53 | > 54 | {t('Delete')} 55 | , 56 | ], 57 | }, 58 | ]; 59 | return ( 60 | 61 | [ 66 | actionRef.current?.reload()} />, 67 | ]} 68 | /> 69 | 70 | ); 71 | }; 72 | 73 | export default RSUModelManagement; 74 | -------------------------------------------------------------------------------- /src/components/CreateSendModal/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'antd'; 3 | import { PlusOutlined } from '@ant-design/icons'; 4 | import type { FormGroupType } from 'edge-src/components/typings'; 5 | import { deviceList } from 'edge-src/services/device/device'; 6 | import FormItem from '../FormItem'; 7 | import { createMapRSU } from 'edge-src/services/config/map'; 8 | import Modal from '../Modal'; 9 | import { copyMaintenanceConfig } from 'edge-src/services/config/maintenance'; 10 | 11 | const fetchDeviceList = async () => { 12 | const { data } = await deviceList({ pageNum: 1, pageSize: -1 }); 13 | return data.map(({ id, rsuName, rsuEsn }: Device.DeviceListItem) => ({ 14 | label: `${rsuName}(Esn: ${rsuEsn})`, 15 | value: id, 16 | })); 17 | }; 18 | 19 | type CreateSendModalProps = CreateModalProps & { 20 | type: 'map' | 'rsu'; 21 | id: number; 22 | }; 23 | 24 | const CreateSendModal: React.FC = ({ type, id, success }) => { 25 | const formItems: FormGroupType[] = [ 26 | { 27 | key: 'rsus', 28 | children: [ 29 | { 30 | type: 'select', 31 | required: true, 32 | name: 'rsus', 33 | label: t('RSU'), 34 | request: fetchDeviceList, 35 | fieldProps: { mode: 'multiple' }, 36 | rules: [{ required: true, message: t('Please select an RSU device') }], 37 | }, 38 | ], 39 | }, 40 | ]; 41 | return ( 42 | : ''} 48 | type={type === 'map' ? 'primary' : 'link'} 49 | > 50 | {type === 'map' ? t('Down') : t('Copy')} 51 | 52 | } 53 | width={500} 54 | modalProps={{ maskClosable: false }} 55 | submitForm={async (values) => { 56 | await { map: createMapRSU, rsu: copyMaintenanceConfig }[type]?.(id, values); 57 | success(); 58 | }} 59 | successMsg={t('{{value}} successfully', { 60 | value: type === 'map' ? t('Delivered') : t('Copy configuration'), 61 | })} 62 | > 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default CreateSendModal; 69 | -------------------------------------------------------------------------------- /src/pages/maintenanceManagement/LogConfig/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import type { ActionType, TableProColumns } from '@ant-design/pro-table'; 3 | import { Divider } from 'antd'; 4 | import BaseContainer from 'edge-src/components/BaseContainer'; 5 | import BaseProTable from 'edge-src/components/BaseProTable'; 6 | import CreateLogConfigModal from './components/CreateLogConfigModal'; 7 | import { deleteLogConfig, logConfigList } from 'edge-src/services/config/log'; 8 | import { confirmModal } from 'edge-src/components/ConfirmModal'; 9 | 10 | const LogConfig: React.FC = () => { 11 | const actionRef = useRef(); 12 | 13 | const columns: TableProColumns[] = [ 14 | { 15 | title: t('ID'), 16 | dataIndex: 'id', 17 | }, 18 | { 19 | title: t('Log Upload Address'), 20 | dataIndex: 'uploadUrl', 21 | }, 22 | { 23 | title: t('Log Server Username'), 24 | dataIndex: 'userId', 25 | }, 26 | { 27 | title: t('Server Type'), 28 | dataIndex: 'transprotocal', 29 | }, 30 | { 31 | title: t('Creation Time'), 32 | dataIndex: 'createTime', 33 | }, 34 | { 35 | title: t('Operate'), 36 | width: 160, 37 | fixed: 'right', 38 | valueType: 'option', 39 | render: (_, row) => [ 40 | actionRef.current?.reload()} 44 | />, 45 | , 46 | 49 | confirmModal({ 50 | id: row.id, 51 | content: t('Are you sure you want to delete this configuration?'), 52 | modalFn: deleteLogConfig, 53 | actionRef, 54 | }) 55 | } 56 | > 57 | {t('Delete')} 58 | , 59 | ], 60 | }, 61 | ]; 62 | return ( 63 | 64 | [ 70 | actionRef.current?.reload()} />, 71 | ]} 72 | /> 73 | 74 | ); 75 | }; 76 | 77 | export default LogConfig; 78 | -------------------------------------------------------------------------------- /docs/how-to-develop.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | ## 环境准备 4 | 5 | - OpenV2X 边缘云控平台 (edgeview) 开发需要准备后端环境。环境准备可以使用 OpenV2X All-in-One 6 | [快速安装](https://github.com/open-v2x/docs/blob/master/docs/v2x-quick-install.md) 7 | 8 | - 环境准备完成后,即可登录边缘云控平台进行设备配置,具体配置参考 9 | [v2x-quick-start](https://github.com/open-v2x/docs/blob/master/docs/v2x-quick-start.md#4-edgeportal-%E5%92%8C-centralportal-%E7%9A%84%E5%BF%AB%E9%80%9F%E8%81%94%E5%8A%A8) 10 | 11 | ## 依赖准备 12 | 13 | - node 环境 14 | 15 | - package.json 中要求:`"node": ">=14.17.0"` 16 | - 验证 nodejs 版本,请确认 nodejs 版本符合要求 17 | 18 | ```shell 19 | node -v 20 | ``` 21 | > 推荐使用 [nvm](https://github.com/nvm-sh/nvm) 管理 node 版本 22 | 23 | - yarn 24 | 25 | - 安装 yarn 26 | 27 | ```shell 28 | npm install -g yarn 29 | ``` 30 | 31 | - 安装依赖包 32 | 33 | - 在项目根目录下执行,即`package.json`同级,需要耐心等待安装完成 34 | 35 | ```shell 36 | yarn install 37 | ``` 38 | 39 | > 如果下载出现意外请设置国内镜像 40 | 41 | > `npm config set registry http://registry.npmmirror.com` 42 | 43 | > 或者 44 | 45 | > `yarn config set registry http://registry.npmmirror.com` 46 | 47 | - 准备好可用的后端 48 | 49 | - 准备好可访问的后端,举个例子: 50 | - 修改`config/proxy.ts`中的相应配置: 51 | 52 | ```javascript 53 | dev: { 54 | '/api': { 55 | target: 'http://47.100.126.13:8080/api', 56 | changeOrigin: true, 57 | pathRewrite: { 58 | '^/api': '', 59 | }, 60 | }, 61 | }, 62 | ``` 63 | 64 | - 搭建完成 65 | 66 | - 在项目根目录下执行,即`package.json`同级 67 | 68 | ```shell 69 | yarn start 70 | ``` 71 | 72 | - 然后会自动启动浏览器打开:`http://localhost:8000`,即可查看到平台页面。 73 | 74 | ## 生产环境使用的前端包 75 | 76 | - 具备符合要求的`nodejs`与`yarn` 77 | - 在项目根目录下执行,即`package.json`同级 78 | 79 | ```shell 80 | yarn build 81 | ``` 82 | 83 | - 打包后的文件在`dist`目录,交给部署相关人员即可。 84 | 85 | ## 如何构建镜像 86 | 87 | ```js 88 | docker build -t openv2x/edgeview:latest . 89 | ``` 90 | 91 | ## 如何启用镜像 92 | 93 | ```js 94 | docker run -d -p <映射端口>:80 -e API_SERVER='http:///api' -e MAP_KEY=<高德地图key> -v <绝对路径>/deploy/edgeview.conf:/etc/nginx/conf.d/default.conf -v <绝对路径>/deploy/nginx.conf:/etc/nginx/nginx.conf --name=edgeview openv2x/edgeview:latest 95 | ``` 96 | 97 | ## 如何贡献代码 98 | 99 | 参考 [v2x_contribution](https://github.com/open-v2x/docs/blob/master/docs/v2x_contribution-zh_CN.md) 100 | 提交 PR 贡献代码。 101 | -------------------------------------------------------------------------------- /src/pages/deviceManagement/RSUModelManagement/components/CreateModelModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createModel, modelInfo, updateModel } from 'edge-src/services/device/model'; 3 | import FormItem from 'edge-src/components/FormItem'; 4 | import type { FormGroupType } from 'edge-src/components/typings'; 5 | import Modal from 'edge-src/components/Modal'; 6 | 7 | const CreateModelModal: React.FC = ({ editId, success }) => { 8 | const formItems: FormGroupType[] = [ 9 | { 10 | key: 'name', 11 | children: [ 12 | { 13 | required: true, 14 | name: 'name', 15 | label: t('RSU Device Model'), 16 | tooltip: t('RSU_NAME_TIP'), 17 | fieldProps: { maxLength: 64 }, 18 | rules: [ 19 | { required: true, message: t('Please enter the RSU device model') }, 20 | { pattern: /^[\u4e00-\u9fa5a-zA-Z0-9_-]+$/, message: t('RSU_NAME_VALIDATE_MSG') }, 21 | ], 22 | }, 23 | { 24 | required: true, 25 | name: 'manufacturer', 26 | label: t('Manufacturer'), 27 | tooltip: t('RSU_MANUFACTURER_TIP'), 28 | fieldProps: { maxLength: 64 }, 29 | rules: [ 30 | { required: true, message: t('Please enter device manufacturer') }, 31 | { 32 | pattern: /^[\u4e00-\u9fa5a-zA-Z0-9_]+$/, 33 | message: t('RSU_MANUFACTURER_VALIDATE_MSG'), 34 | }, 35 | ], 36 | }, 37 | ], 38 | }, 39 | { 40 | key: 'desc', 41 | children: [ 42 | { 43 | type: 'textarea', 44 | width: 912, 45 | name: 'desc', 46 | label: t('Describe'), 47 | fieldProps: { autoSize: { minRows: 3, maxRows: 5 } }, 48 | }, 49 | ], 50 | }, 51 | ]; 52 | return ( 53 | { 57 | if (editId) { 58 | await updateModel(editId, values); 59 | } else { 60 | await createModel(values); 61 | } 62 | success(); 63 | }} 64 | editId={editId} 65 | request={async ({ id }) => await modelInfo(id)} 66 | > 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default CreateModelModal; 73 | -------------------------------------------------------------------------------- /src/pages/eventManagement/DoNotPassWarning/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import type { ActionType, TableProColumns } from '@ant-design/pro-table'; 3 | import BaseContainer from 'edge-src/components/BaseContainer'; 4 | import BaseProTable from 'edge-src/components/BaseProTable'; 5 | import { CoordinationInfoTypeOptions, DriveBehaviorTypeOptions } from 'edge-src/utils/constants'; 6 | import { dataFormat, statusOptionFormat } from 'edge-src/utils'; 7 | import { overtakingWarningList } from 'edge-src/services/event/dnpw'; 8 | import LonLatUnit from 'edge-src/components/LonLatUnit'; 9 | 10 | const RoadSideCoordination: React.FC = () => { 11 | const actionRef = useRef(); 12 | const columns: TableProColumns[] = [ 13 | { 14 | title: t('ID'), 15 | dataIndex: 'id', 16 | }, 17 | { 18 | title: t('Message Number'), 19 | dataIndex: 'msgID', 20 | }, 21 | { 22 | title: t('Millisecond Time'), 23 | dataIndex: 'secMark', 24 | }, 25 | { 26 | title: t('Longitude'), 27 | dataIndex: ['refPos', 'lon'], 28 | render: (_, { refPos: { lon } }) => , 29 | }, 30 | { 31 | title: t('Latitude'), 32 | dataIndex: ['refPos', 'lat'], 33 | render: (_, { refPos: { lat } }) => , 34 | }, 35 | { 36 | title: t('Target ID'), 37 | dataIndex: 'vehID', 38 | }, 39 | { 40 | title: t('Driving Behavior'), 41 | dataIndex: ['driveSuggestion', 'suggestion'], 42 | valueType: 'select', 43 | valueEnum: statusOptionFormat(DriveBehaviorTypeOptions), 44 | }, 45 | { 46 | title: t('Request Valid Time'), 47 | dataIndex: ['driveSuggestion', 'lifeTime'], 48 | render: (_, { driveSuggestion: { lifeTime } }) => dataFormat(lifeTime * 10, 'ms'), 49 | }, 50 | { 51 | title: t('Coordination Information'), 52 | dataIndex: 'info', 53 | valueType: 'select', 54 | valueEnum: statusOptionFormat(CoordinationInfoTypeOptions), 55 | search: true, 56 | }, 57 | { 58 | title: t('Reporting Time'), 59 | dataIndex: 'createTime', 60 | sorter: true, 61 | }, 62 | ]; 63 | return ( 64 | 65 | 66 | 67 | ); 68 | }; 69 | 70 | export default RoadSideCoordination; 71 | -------------------------------------------------------------------------------- /src/pages/eventManagement/roadsideSafetyMessage/RSMDetails/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { history } from 'umi'; 3 | import ProCard from '@ant-design/pro-card'; 4 | import BaseContainer from 'edge-src/components/BaseContainer'; 5 | import CardList from 'edge-src/components/CardList'; 6 | import { DataSourceOptions, ParticipantTypeOptions } from 'edge-src/utils/constants'; 7 | import { dataFormat } from 'edge-src/utils'; 8 | import LonLatUnit from 'edge-src/components/LonLatUnit'; 9 | 10 | // 基本信息 11 | const BasicInfo: React.FC<{ basicInfo: Event.RSMListItem | undefined }> = ({ basicInfo = {} }) => { 12 | const infoMap = [ 13 | { 14 | key: 'id', 15 | label: t('Message ID'), 16 | }, 17 | { 18 | key: 'ptcId', 19 | label: t('Target ID'), 20 | }, 21 | { 22 | key: 'ptcType', 23 | label: t('Participant Type'), 24 | render: ({ ptcType }: Event.RSMListItem) => ParticipantTypeOptions[ptcType], 25 | }, 26 | { 27 | key: 'source', 28 | label: t('Data Sources'), 29 | render: ({ source }: Event.RSMListItem) => DataSourceOptions[source], 30 | }, 31 | { 32 | key: 'secMark', 33 | label: t('Millisecond Time'), 34 | }, 35 | { 36 | key: 'speed', 37 | label: t('Speed'), 38 | render: ({ speed }: Event.RSMListItem) => dataFormat(speed * 0.02 * 3.6, 'km/h'), 39 | }, 40 | { 41 | key: 'heading', 42 | label: t('Heading'), 43 | render: ({ heading }: Event.RSMListItem) => dataFormat(heading * 0.0125, '°'), 44 | }, 45 | { 46 | key: 'lon', 47 | label: t('Longitude'), 48 | render: ({ lon }: Event.RSMListItem) => , 49 | }, 50 | { 51 | key: 'lat', 52 | label: t('Latitude'), 53 | render: ({ lat }: Event.RSMListItem) => , 54 | }, 55 | { 56 | key: 'createTime', 57 | label: t('Reporting Time'), 58 | }, 59 | ]; 60 | return ( 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | const RSMDetails: React.FC = ({ location: { state } }) => { 68 | if (!state) { 69 | history.goBack(); 70 | } 71 | 72 | return ( 73 | 74 | 75 | 76 | ); 77 | }; 78 | 79 | export default RSMDetails; 80 | -------------------------------------------------------------------------------- /e2e/pages/maintenance/Maintenance.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import { generatePureNumber } from '../../utils'; 3 | import { globalModalSubmitBtn, setModalFormItemValue, setSelectValue } from '../../utils/form'; 4 | import { checkSuccessMsg, gotoPageAndExpectUrl, useUserStorageState } from '../../utils/global'; 5 | 6 | import { 7 | checkTableItemContainValue, 8 | checkTableItemEqualValue, 9 | clickCopyBtn, 10 | clickDownTextBtn, 11 | clickEditBtn, 12 | searchItemAndQuery, 13 | } from '../../utils/table'; 14 | 15 | test.describe('The Maintenance Page', () => { 16 | const M_NameVal = 'RSU01'; 17 | const M_rsuEsn = 'R328328'; 18 | const randomNum = generatePureNumber(); 19 | const pageUrl = '/maintenance/maintenance'; 20 | 21 | // Use signed-in state of 'userStorageState.json'. 22 | useUserStorageState(); 23 | 24 | test.beforeEach(async ({ page }) => { 25 | await gotoPageAndExpectUrl(page, pageUrl); 26 | }); 27 | 28 | test('successfully edit maintenance', async ({ page }) => { 29 | await searchItemAndQuery(page, '#rsuName', M_NameVal); 30 | await clickEditBtn(page); 31 | 32 | await setModalFormItemValue(page, '#hbRate', randomNum); 33 | await setModalFormItemValue(page, '#runningInfoRate', randomNum); 34 | await setSelectValue(page, 'logLevel', '#logLevel_list'); 35 | await setSelectValue(page, 'reboot', '#reboot_list'); 36 | 37 | await globalModalSubmitBtn(page); 38 | await checkSuccessMsg(page); 39 | }); 40 | 41 | test('successfully down RSU maintenance configuration', async ({ page }) => { 42 | await searchItemAndQuery(page, '#rsuName', M_NameVal); 43 | await clickDownTextBtn(page); 44 | await checkSuccessMsg(page); 45 | }); 46 | 47 | test('successfully copy RSU maintenance configuration', async ({ page }) => { 48 | await searchItemAndQuery(page, '#rsuName', M_NameVal); 49 | await clickCopyBtn(page); 50 | await setSelectValue(page, 'rsus', '#rsus_list'); 51 | await globalModalSubmitBtn(page); 52 | await checkSuccessMsg(page); 53 | }); 54 | 55 | test('successfully query via rsuEsn', async ({ page }) => { 56 | await searchItemAndQuery(page, '#rsuEsn', M_rsuEsn); 57 | await checkTableItemEqualValue(page, M_rsuEsn, 2); 58 | }); 59 | 60 | test('successfully query via rsuName', async ({ page }) => { 61 | await searchItemAndQuery(page, '#rsuName', M_NameVal); 62 | await checkTableItemContainValue(page, M_NameVal, 1); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /e2e/pages/device/Rsumodel.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import { generateNumLetter } from '../../utils'; 3 | import { globalModalSubmitBtn, setModalFormItemValue } from '../../utils/form'; 4 | import { checkSuccessMsg, gotoPageAndExpectUrl, useUserStorageState } from '../../utils/global'; 5 | import { 6 | checkTableItemContainValue, 7 | clickConfirmModalOkBtn, 8 | clickCreateBtn, 9 | clickDeleteTextBtn, 10 | clickEditBtn, 11 | searchItemAndQuery, 12 | } from '../../utils/table'; 13 | 14 | test.describe('The RsuModel Page', () => { 15 | const randomNumLetter = generateNumLetter(); 16 | const rsumodelNameVal = `model_name_${1}`; 17 | const manufacturerVal = `C_${randomNumLetter}`; 18 | const descVal = 'test description info'; 19 | const pageUrl = '/device/model'; 20 | 21 | // Use signed-in state of 'userStorageState.json'. 22 | useUserStorageState(); 23 | 24 | test.beforeEach(async ({ page }) => { 25 | await gotoPageAndExpectUrl(page, pageUrl); 26 | }); 27 | 28 | test('successfully create rsumodel', async ({ page }) => { 29 | await clickCreateBtn(page); 30 | 31 | await setModalFormItemValue(page, '#name', rsumodelNameVal); 32 | await setModalFormItemValue(page, '#manufacturer', manufacturerVal); 33 | await setModalFormItemValue(page, '#desc', descVal); 34 | 35 | await globalModalSubmitBtn(page); 36 | await checkSuccessMsg(page); 37 | }); 38 | 39 | test('successfully query via rsuModelName', async ({ page }) => { 40 | await searchItemAndQuery(page, '#name', rsumodelNameVal); 41 | await checkTableItemContainValue(page, rsumodelNameVal, 1); 42 | }); 43 | 44 | test('successfully query via manufacturer', async ({ page }) => { 45 | await searchItemAndQuery(page, '#manufacturer', manufacturerVal); 46 | await checkTableItemContainValue(page, manufacturerVal, 2); 47 | }); 48 | 49 | test('successfully edit rsumodel', async ({ page }) => { 50 | await searchItemAndQuery(page, '#name', rsumodelNameVal); 51 | await clickEditBtn(page); 52 | 53 | await setModalFormItemValue(page, '#name', `update_${rsumodelNameVal}`); 54 | 55 | await globalModalSubmitBtn(page); 56 | await checkSuccessMsg(page); 57 | }); 58 | 59 | test('successfully delete rsumodel', async ({ page }) => { 60 | await searchItemAndQuery(page, '#name', `update_${rsumodelNameVal}`); 61 | await clickDeleteTextBtn(page); 62 | await clickConfirmModalOkBtn(page); 63 | await checkSuccessMsg(page); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Set up QEMU 15 | uses: docker/setup-qemu-action@v2 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v2 18 | - name: Login to DockerHub 19 | uses: docker/login-action@v2 20 | with: 21 | username: ${{ secrets.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_TOKEN }} 23 | - name: Build and push 24 | uses: docker/build-push-action@v3 25 | with: 26 | context: . 27 | platforms: linux/amd64,linux/arm64 28 | push: true 29 | tags: openv2x/edgeview:latest 30 | build-args: | 31 | GIT_BRANCH=${{ github.ref_name }} 32 | REPO_URL=https://github.com/open-v2x/edgeview 33 | GIT_COMMIT=${{ github.sha }} 34 | 35 | acr: 36 | needs: docker 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: sync image to ACR 40 | uses: appleboy/ssh-action@master 41 | with: 42 | host: ${{ secrets.FQ_HOST }} 43 | username: ${{ secrets.FQ_USERNAME }} 44 | key: ${{ secrets.FQ_KEY }} 45 | port: ${{ secrets.FQ_PORT }} 46 | script: | 47 | set -e 48 | 49 | (docker images | grep none | awk '{print $3}' | xargs -I{} docker rmi -f {}) || true 50 | docker pull openv2x/edgeview:latest 51 | docker tag openv2x/edgeview:latest registry.cn-shanghai.aliyuncs.com/openv2x/edgeview:latest 52 | docker push registry.cn-shanghai.aliyuncs.com/openv2x/edgeview:latest 53 | 54 | ssh: 55 | needs: acr 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: apply to all-in-one environment 59 | uses: appleboy/ssh-action@master 60 | with: 61 | host: ${{ secrets.AIO_MASTER_HOST }} 62 | username: ${{ secrets.AIO_MASTER_USERNAME }} 63 | key: ${{ secrets.AIO_MASTER_KEY }} 64 | port: ${{ secrets.AIO_MASTER_PORT }} 65 | script: | 66 | set -e 67 | rm -rf openv2x-aio-master.tar.gz && wget https://openv2x.oss-ap-southeast-1.aliyuncs.com/deploy/master/openv2x-aio-master.tar.gz 68 | rm -rf src && tar zxvf openv2x-aio-master.tar.gz 69 | cd src 70 | sh install.sh 71 | -------------------------------------------------------------------------------- /src/pages/user/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { history, useModel } from 'umi'; 3 | import { ProFormText, LoginForm } from '@ant-design/pro-form'; 4 | import { login } from 'edge-src/services/api'; 5 | import classNames from 'classnames'; 6 | import { setToken } from 'edge-src/utils/storage'; 7 | import { SelectLang } from 'edge-src/components/SelectLang'; 8 | 9 | import imgUser from 'edge-src/assets/images/login_user.png'; 10 | import imgPwd from 'edge-src/assets/images/login_password.png'; 11 | 12 | import styles from './index.less'; 13 | 14 | const Login: React.FC = () => { 15 | const { initialState, setInitialState } = useModel('@@initialState'); 16 | 17 | const fetchUserInfo = async () => { 18 | const userInfo = await initialState?.fetchUserInfo?.(); 19 | if (userInfo) { 20 | await setInitialState((s) => ({ ...s, currentUser: userInfo })); 21 | } 22 | }; 23 | 24 | const handleSubmit = async (values: API.LoginParams) => { 25 | const { access_token: accessToken, token_type: tokenType } = await login(values); 26 | const token = `${tokenType} ${accessToken}`; 27 | setToken(token); 28 | await fetchUserInfo(); 29 | if (!history) return; 30 | const { query } = history.location; 31 | const { redirect } = query as { redirect: string }; 32 | history.replace(redirect || '/'); 33 | }; 34 | 35 | return ( 36 |
37 |
38 | 39 |
40 | {t('OpenV2X Edge Portal')}} 42 | onFinish={async (values) => { 43 | await handleSubmit(values as API.LoginParams); 44 | }} 45 | > 46 |

{t('Platform Login')}

47 | , 52 | }} 53 | placeholder={t('Username')} 54 | rules={[{ required: true, message: t('Please input your username') }]} 55 | /> 56 | , 61 | }} 62 | placeholder={t('Password')} 63 | rules={[{ required: true, message: t('Please input your password') }]} 64 | /> 65 |
66 |
67 | ); 68 | }; 69 | 70 | export default Login; 71 | -------------------------------------------------------------------------------- /src/components/RightContent/AvatarDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; 3 | import { Avatar, Menu, Spin } from 'antd'; 4 | import { history, useModel } from 'umi'; 5 | import { stringify } from 'querystring'; 6 | import HeaderDropdown from '../HeaderDropdown'; 7 | import styles from './index.less'; 8 | import type { MenuInfo } from 'rc-menu/lib/interface'; 9 | import { clearStorage } from 'edge-src/utils/storage'; 10 | 11 | export type GlobalHeaderRightProps = { 12 | menu?: boolean; 13 | }; 14 | 15 | /** 16 | * 退出登录,并且将当前的 url 保存 17 | */ 18 | const loginOut = () => { 19 | clearStorage(); 20 | const { query = {}, search, pathname } = history.location; 21 | const { redirect } = query; 22 | if (window.location.pathname !== '/user/login' && !redirect) { 23 | history.replace({ 24 | pathname: '/user/login', 25 | search: stringify({ redirect: pathname + search }), 26 | }); 27 | } 28 | }; 29 | 30 | const AvatarDropdown: React.FC = () => { 31 | const { initialState, setInitialState } = useModel('@@initialState'); 32 | 33 | const onMenuClick = useCallback( 34 | (event: MenuInfo) => { 35 | const { key } = event; 36 | if (key === 'logout') { 37 | setInitialState((s) => ({ ...s, currentUser: undefined })); 38 | loginOut(); 39 | return; 40 | } 41 | history.push(`/account/${key}`); 42 | }, 43 | [setInitialState], 44 | ); 45 | 46 | const loading = ( 47 | 48 | 49 | 50 | ); 51 | 52 | if (!initialState) { 53 | return loading; 54 | } 55 | 56 | const { currentUser } = initialState; 57 | 58 | if (!currentUser || !currentUser.username) { 59 | return loading; 60 | } 61 | 62 | const menuHeaderDropdown = ( 63 | 64 | 65 | 66 | {t('Logout')} 67 | 68 | 69 | ); 70 | return ( 71 | 72 | 73 | } /> 74 | {currentUser.username} 75 | 76 | 77 | ); 78 | }; 79 | 80 | export default AvatarDropdown; 81 | -------------------------------------------------------------------------------- /e2e/pages/maintenance/Map.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import { generateNumLetter } from '../../utils'; 3 | import { globalModalSubmitBtn, setCascaderValue, setModalFormItemValue } from '../../utils/form'; 4 | import { 5 | checkDetailUrl, 6 | checkSuccessMsg, 7 | gotoPageAndExpectUrl, 8 | uploadFile, 9 | useUserStorageState, 10 | } from '../../utils/global'; 11 | 12 | import { clickBackToListBtn } from '../../utils/detail'; 13 | import { 14 | clickConfirmModalOkBtn, 15 | clickCreateBtn, 16 | clickDeleteTextBtn, 17 | clickDetailTextBtn, 18 | clickEditBtn, 19 | searchItemAndQuery, 20 | } from '../../utils/table'; 21 | 22 | test.describe('The MAP Page', () => { 23 | const randomNumLetter = generateNumLetter(); 24 | const mapNameVal = `map_name_${1}`; 25 | const provinceNameVal = [0, 1, 2, 4]; 26 | const addressVal = `address ${randomNumLetter}`; 27 | const descVal = 'test description info'; 28 | const pageUrl = '/maintenance/map'; 29 | 30 | // Use signed-in state of 'userStorageState.json'. 31 | useUserStorageState(); 32 | 33 | test.beforeEach(async ({ page }) => { 34 | await gotoPageAndExpectUrl(page, pageUrl); 35 | }); 36 | 37 | test('successfully create map', async ({ page }) => { 38 | await clickCreateBtn(page); 39 | 40 | await setModalFormItemValue(page, '#name', mapNameVal); 41 | await setModalFormItemValue(page, '#address', addressVal); 42 | await setModalFormItemValue(page, '#desc', descVal); 43 | await setCascaderValue(page, 'province', provinceNameVal); 44 | await uploadFile(page, '#data', './e2e/testdata/MapExample.json'); 45 | 46 | await globalModalSubmitBtn(page); 47 | await checkSuccessMsg(page); 48 | }); 49 | 50 | test('successfully edit map', async ({ page }) => { 51 | await searchItemAndQuery(page, '#name', mapNameVal); 52 | await clickEditBtn(page); 53 | 54 | await setModalFormItemValue(page, '#name', `update_${mapNameVal}`); 55 | 56 | await globalModalSubmitBtn(page); 57 | await checkSuccessMsg(page); 58 | }); 59 | 60 | test('successfully view map detail', async ({ page }) => { 61 | await searchItemAndQuery(page, '#name', `update_${mapNameVal}`); 62 | await clickDetailTextBtn(page); 63 | await checkDetailUrl(page, pageUrl); 64 | await clickBackToListBtn(page); 65 | }); 66 | 67 | test('successfully delete map', async ({ page }) => { 68 | await searchItemAndQuery(page, '#name', `update_${mapNameVal}`); 69 | await clickDeleteTextBtn(page); 70 | await clickConfirmModalOkBtn(page); 71 | await checkSuccessMsg(page); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/pages/eventManagement/roadSideInformation/RSIList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { history } from 'umi'; 3 | import type { ActionType, TableProColumns } from '@ant-design/pro-table'; 4 | import BaseContainer from 'edge-src/components/BaseContainer'; 5 | import BaseProTable from 'edge-src/components/BaseProTable'; 6 | import { eventInfoList } from 'edge-src/services/event/rsi'; 7 | import { EventClassOptions, EventSourceOptions, EventTypeOptions } from 'edge-src/utils/constants'; 8 | import { dataFormat, statusOptionFormat } from 'edge-src/utils'; 9 | 10 | const RSIList: React.FC = () => { 11 | const actionRef = useRef(); 12 | const columns: TableProColumns[] = [ 13 | { 14 | title: t('Alert ID'), 15 | dataIndex: 'id', 16 | }, 17 | { 18 | title: t('Event Duration'), 19 | dataIndex: 'duration', 20 | }, 21 | { 22 | title: t('Event Classification'), 23 | dataIndex: 'eventClass', 24 | valueType: 'select', 25 | valueEnum: statusOptionFormat(EventClassOptions), 26 | }, 27 | { 28 | title: t('Event Type'), 29 | dataIndex: 'eventType', 30 | valueType: 'select', 31 | valueEnum: statusOptionFormat(EventTypeOptions), 32 | search: true, 33 | }, 34 | { 35 | title: t('Event Source'), 36 | dataIndex: 'eventSource', 37 | valueType: 'select', 38 | valueEnum: statusOptionFormat(EventSourceOptions), 39 | }, 40 | { 41 | title: t('Event Confidence'), 42 | dataIndex: 'eventConfidence', 43 | }, 44 | { 45 | title: t('Occurrence Area Radius'), 46 | dataIndex: 'eventRadius', 47 | render: (_, { eventRadius }: Event.RSIListItem) => dataFormat(eventRadius / 10, 'm'), 48 | }, 49 | { 50 | title: t('Event Description'), 51 | dataIndex: 'eventDescription', 52 | }, 53 | { 54 | title: t('Event Priority'), 55 | dataIndex: 'eventPriority', 56 | }, 57 | { 58 | title: t('Reporting Time'), 59 | dataIndex: 'createTime', 60 | sorter: true, 61 | }, 62 | { 63 | title: t('Operate'), 64 | width: 160, 65 | fixed: 'right', 66 | valueType: 'option', 67 | render: (_, row) => [ 68 | history.push(`/event/rsi/details/${row.id}`)}> 69 | {t('Details')} 70 | , 71 | ], 72 | }, 73 | ]; 74 | return ( 75 | 76 | 77 | 78 | ); 79 | }; 80 | 81 | export default RSIList; 82 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import type { Settings as LayoutSettings } from '@ant-design/pro-layout'; 2 | import { PageLoading, ProBreadcrumb } from '@ant-design/pro-layout'; 3 | import { history } from 'umi'; 4 | import type { RequestConfig, RunTimeLayoutConfig } from 'umi'; 5 | import RightContent from 'edge-src/components/RightContent'; 6 | import defaultSettings from '../config/defaultSettings'; 7 | import { currentUser as queryCurrentUser } from './services/api'; 8 | import i18n from './utils/i18n'; 9 | import { getToken } from './utils/storage'; 10 | 11 | window.t = i18n.t; 12 | 13 | const loginPath = '/user/login'; 14 | 15 | export const request: RequestConfig = { 16 | errorConfig: { 17 | adaptor: (resData) => { 18 | return { 19 | ...resData, 20 | success: resData.code === 0, 21 | errorMessage: resData.msg, 22 | }; 23 | }, 24 | }, 25 | }; 26 | 27 | /** 获取用户信息比较慢的时候会展示一个 loading */ 28 | export const initialStateConfig = { 29 | loading: , 30 | }; 31 | 32 | /** 33 | * @see https://umijs.org/zh-CN/plugins/plugin-initial-state 34 | * */ 35 | export async function getInitialState(): Promise<{ 36 | settings?: Partial; 37 | currentUser?: API.CurrentUser; 38 | loading?: boolean; 39 | fetchUserInfo?: () => Promise; 40 | }> { 41 | const fetchUserInfo = async () => { 42 | try { 43 | const data = await queryCurrentUser(); 44 | return { username: data?.username }; 45 | } catch (error) { 46 | history.push(loginPath); 47 | } 48 | return undefined; 49 | }; 50 | // 如果是登录页面且未登录,不执行 51 | const { pathname } = history.location; 52 | if (!pathname.startsWith(loginPath) && getToken()) { 53 | const currentUser = await fetchUserInfo(); 54 | return { 55 | fetchUserInfo, 56 | currentUser, 57 | settings: defaultSettings, 58 | }; 59 | } 60 | return { 61 | fetchUserInfo, 62 | settings: defaultSettings, 63 | }; 64 | } 65 | 66 | // ProLayout 支持的api https://procomponents.ant.design/components/layout 67 | export const layout: RunTimeLayoutConfig = ({ initialState }) => { 68 | return { 69 | disableContentMargin: false, 70 | menuHeaderRender: () =>

{t('OpenV2X Edge Portal')}

, 71 | rightContentRender: () => , 72 | headerContentRender: () => { 73 | return ; 74 | }, 75 | onPageChange: () => { 76 | const { location } = history; 77 | // 如果没有登录,重定向到 login 78 | if (!initialState?.currentUser && location.pathname !== loginPath) { 79 | history.push(loginPath); 80 | } 81 | }, 82 | ...initialState?.settings, 83 | }; 84 | }; 85 | -------------------------------------------------------------------------------- /src/components/SelectLang/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { getLocale, setLocale } from 'umi'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { Menu, Dropdown } from 'antd'; 5 | import type { DropDownProps } from 'antd/es/dropdown'; 6 | 7 | import styles from './index.less'; 8 | 9 | interface HeaderDropdownProps extends DropDownProps { 10 | overlayClassName?: string; 11 | placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; 12 | } 13 | interface DefaultLangConfigType { 14 | lang: string; 15 | label: string; 16 | icon: string; 17 | title: string; 18 | } 19 | 20 | const defaultLangConfig: DefaultLangConfigType[] = [ 21 | { lang: 'en-US', label: 'English', icon: '🇺🇸', title: 'Language' }, 22 | { lang: 'zh-CN', label: '简体中文', icon: '🇨🇳', title: '语言' }, 23 | ]; 24 | 25 | const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( 26 | 27 | ); 28 | 29 | export const SelectLang: React.FC = () => { 30 | const [selectedLang, setSelectedLang] = useState(() => getLocale()); 31 | const { i18n } = useTranslation(); 32 | 33 | const changeLang = ({ key }: { key: string }): void => { 34 | setLocale(key); 35 | setSelectedLang(getLocale()); 36 | i18n.changeLanguage(key); 37 | }; 38 | 39 | const langMenu = ( 40 | 41 | {defaultLangConfig.map((localeObj) => { 42 | return ( 43 | 44 | 45 | {localeObj.icon || '🌐'} 46 | 47 | {localeObj.label} 48 | 49 | ); 50 | })} 51 | 52 | ); 53 | 54 | return ( 55 | 56 | 57 | 58 | 69 | 70 | 71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/pages/maintenanceManagement/RSUInfoQuery/InfoQueryList/components/CreateQueryModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { FormGroupType } from 'edge-src/components/typings'; 3 | import Modal from 'edge-src/components/Modal'; 4 | import FormItem from 'edge-src/components/FormItem'; 5 | import { QueryIntervalOptions, QueryTypeOptions } from 'edge-src/utils/constants'; 6 | import { deviceList } from 'edge-src/services/device/device'; 7 | import { createQueryInstruction } from 'edge-src/services/config/query'; 8 | import { statusOptionFormat } from 'edge-src/utils'; 9 | 10 | const fetchDeviceList = async () => { 11 | const { data } = await deviceList({ pageNum: 1, pageSize: -1 }); 12 | return data.map(({ id, rsuName, rsuEsn }: Device.DeviceListItem) => ({ 13 | label: `${rsuName}(Esn: ${rsuEsn})`, 14 | value: id, 15 | })); 16 | }; 17 | 18 | const CreateQueryModal: React.FC = ({ success }) => { 19 | const formItems: FormGroupType[] = [ 20 | { 21 | key: 'queryType', 22 | children: [ 23 | { 24 | type: 'select', 25 | required: true, 26 | name: 'queryType', 27 | label: t('Query Information Type'), 28 | valueEnum: statusOptionFormat(QueryTypeOptions), 29 | rules: [{ required: true, message: t('Please select the query information type') }], 30 | }, 31 | ], 32 | }, 33 | { 34 | key: 'timeType', 35 | children: [ 36 | { 37 | type: 'select', 38 | required: true, 39 | name: 'timeType', 40 | label: t('Query Information Time Interval'), 41 | valueEnum: statusOptionFormat(QueryIntervalOptions), 42 | rules: [ 43 | { required: true, message: t('Please select the query information time interval') }, 44 | ], 45 | }, 46 | ], 47 | }, 48 | { 49 | key: 'rsus', 50 | children: [ 51 | { 52 | type: 'select', 53 | required: true, 54 | name: 'rsus', 55 | label: t('Query RSU'), 56 | request: fetchDeviceList, 57 | fieldProps: { mode: 'multiple' }, 58 | rules: [{ required: true, message: t('Please select an RSU device') }], 59 | }, 60 | ], 61 | }, 62 | ]; 63 | return ( 64 | { 69 | await createQueryInstruction(values); 70 | success(); 71 | }} 72 | successMsg={t('The query command was issued successfully')} 73 | > 74 | 75 | 76 | ); 77 | }; 78 | 79 | export default CreateQueryModal; 80 | -------------------------------------------------------------------------------- /e2e/pages/maintenance/Business.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import { generatePureNumber } from '../../utils'; 3 | import { globalModalSubmitBtn, setModalFormItemValue } from '../../utils/form'; 4 | import { 5 | checkDetailUrl, 6 | checkSuccessMsg, 7 | gotoPageAndExpectUrl, 8 | useUserStorageState, 9 | } from '../../utils/global'; 10 | 11 | import { clickBackToListBtn } from '../../utils/detail'; 12 | import { 13 | clickConfirmModalOkBtn, 14 | clickCreateBtn, 15 | clickDeleteTextBtn, 16 | clickDetailTextBtn, 17 | clickEditBtn, 18 | searchItemAndQuery, 19 | } from '../../utils/table'; 20 | 21 | test.describe('The Business Page', () => { 22 | const randomNum = generatePureNumber(); 23 | const businessNameVal = `business_name_${1}`; 24 | const pageUrl = '/maintenance/business'; 25 | 26 | // Use signed-in state of 'userStorageState.json'. 27 | useUserStorageState(); 28 | 29 | test.beforeEach(async ({ page }) => { 30 | await gotoPageAndExpectUrl(page, pageUrl); 31 | }); 32 | 33 | test('successfully create Business', async ({ page }) => { 34 | await clickCreateBtn(page); 35 | 36 | await setModalFormItemValue(page, '#name', businessNameVal); 37 | 38 | await page.check('text="全局采样"'); 39 | await setModalFormItemValue(page, '#bsm_sampleRate', randomNum); 40 | await setModalFormItemValue(page, '#bsm_upLimit', randomNum); 41 | await setModalFormItemValue(page, '#rsm_upLimit', randomNum); 42 | await setModalFormItemValue(page, '#map_upLimit', randomNum); 43 | await setModalFormItemValue(page, '#spat_upLimit', randomNum); 44 | await page.click('text="配置 RSU"'); 45 | await page.click(':nth-match(.ant-checkbox-wrapper, 2)'); // 选择第一个 RSU 46 | await page.click('.ant-modal-footer > button:nth-child(2)'); // 配置 RSU 的确定按钮 47 | 48 | await globalModalSubmitBtn(page); 49 | await checkSuccessMsg(page); 50 | }); 51 | 52 | test('successfully edit business', async ({ page }) => { 53 | await searchItemAndQuery(page, '#name', businessNameVal); 54 | await clickEditBtn(page); 55 | 56 | await setModalFormItemValue(page, '#name', `update_${businessNameVal}`); 57 | 58 | await globalModalSubmitBtn(page); 59 | await checkSuccessMsg(page); 60 | }); 61 | 62 | test('successfully view detail', async ({ page }) => { 63 | await searchItemAndQuery(page, '#name', `update_${businessNameVal}`); 64 | await clickDetailTextBtn(page); 65 | await checkDetailUrl(page, pageUrl); 66 | await clickBackToListBtn(page); 67 | }); 68 | 69 | test('successfully delete business', async ({ page }) => { 70 | await searchItemAndQuery(page, '#name', `update_${businessNameVal}`); 71 | await clickDeleteTextBtn(page); 72 | await clickConfirmModalOkBtn(page); 73 | await checkSuccessMsg(page); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/pages/maintenanceManagement/mapConfig/ConfigList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { history } from 'umi'; 3 | import type { ActionType, TableProColumns } from '@ant-design/pro-table'; 4 | import { Divider } from 'antd'; 5 | import BaseContainer from 'edge-src/components/BaseContainer'; 6 | import BaseProTable from 'edge-src/components/BaseProTable'; 7 | import CreateMapConfigModal from './components/CreateMapConfigModal'; 8 | import { deleteMapConfig, mapConfigList } from 'edge-src/services/config/map'; 9 | import { confirmModal } from 'edge-src/components/ConfirmModal'; 10 | import { renderAreaFormatName, renderAreaFormItem } from 'edge-src/components/Country/renderHelper'; 11 | 12 | const ConfigList: React.FC = () => { 13 | const actionRef = useRef(); 14 | const columns: TableProColumns[] = [ 15 | { 16 | title: t('MAP Name'), 17 | dataIndex: 'name', 18 | ellipsis: true, 19 | search: true, 20 | }, 21 | { 22 | title: t('MAP Area'), 23 | dataIndex: 'countryName', 24 | ellipsis: true, 25 | render: (_, row) => renderAreaFormatName(row), 26 | renderFormItem: renderAreaFormItem, 27 | search: true, 28 | }, 29 | { 30 | title: t('MAP Location'), 31 | dataIndex: 'address', 32 | ellipsis: true, 33 | }, 34 | { 35 | title: t('Number Of Releases'), 36 | dataIndex: 'amount', 37 | }, 38 | { 39 | title: t('Operate'), 40 | width: 180, 41 | fixed: 'right', 42 | valueType: 'option', 43 | render: (_, row) => [ 44 | actionRef.current?.reload()} 48 | />, 49 | , 50 | history.push(`/maintenance/map/details/${row.id}`)}> 51 | {t('Details')} 52 | , 53 | , 54 | 57 | confirmModal({ 58 | id: row.id, 59 | content: t('Are you sure you want to delete this configuration?'), 60 | modalFn: deleteMapConfig, 61 | actionRef, 62 | }) 63 | } 64 | > 65 | {t('Delete')} 66 | , 67 | ], 68 | }, 69 | ]; 70 | return ( 71 | 72 | [ 77 | actionRef.current?.reload()} />, 78 | ]} 79 | /> 80 | 81 | ); 82 | }; 83 | 84 | export default ConfigList; 85 | -------------------------------------------------------------------------------- /src/pages/systemConfiguration/EdgeSiteConfig/components/UpdateSiteConfigModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'antd'; 3 | import type { FormGroupType } from 'edge-src/components/typings'; 4 | import Modal from 'edge-src/components/Modal'; 5 | import FormItem from 'edge-src/components/FormItem'; 6 | import { IPReg } from 'edge-src/utils/constants'; 7 | import { updateSystemConfig } from 'edge-src/services/system/edge'; 8 | 9 | type UpdateSiteConfigProps = CreateModalProps & { 10 | info?: { 11 | host: string; 12 | port: number; 13 | username: string; 14 | password: string; 15 | }; 16 | }; 17 | 18 | const UpdateSiteConfigModal: React.FC = ({ info = {}, success }) => { 19 | const tipStyle = { 20 | fontWeight: '400', 21 | color: 'rgba(0, 0, 0, 0.85)', 22 | lineHeight: '20px', 23 | }; 24 | const formItems: FormGroupType[] = [ 25 | { 26 | key: 'host', 27 | children: [ 28 | { 29 | required: true, 30 | name: 'host', 31 | label: 'MQTT Host', 32 | rules: [ 33 | { required: true, message: t('Please enter host') }, 34 | { pattern: IPReg, message: t('Incorrect host format') }, 35 | ], 36 | }, 37 | { 38 | required: true, 39 | name: 'port', 40 | label: 'MQTT Port', 41 | rules: [{ required: true, message: t('Please enter port') }], 42 | }, 43 | ], 44 | }, 45 | { 46 | key: 'username', 47 | children: [ 48 | { 49 | required: true, 50 | name: 'username', 51 | label: `MQTT ${t('Username')}`, 52 | rules: [{ required: true, message: t('Please enter username') }], 53 | }, 54 | { 55 | type: 'password', 56 | required: true, 57 | name: 'password', 58 | label: `MQTT ${t('Password')}`, 59 | rules: [{ required: true, message: t('Please enter password') }], 60 | }, 61 | ], 62 | }, 63 | ]; 64 | return ( 65 | 69 | {t('Configure')} 70 | 71 | } 72 | modalProps={{ maskClosable: false }} 73 | submitForm={async (values) => { 74 | await updateSystemConfig({ mqtt_config: values }); 75 | success(); 76 | }} 77 | successMsg={t('{{value}} successfully', { value: t('Modify') })} 78 | params={info.host ? { host: info.host } : undefined} 79 | request={async () => info} 80 | > 81 |

{t('Please enter the relevant information of the MQTT server')}

82 | 83 |
84 | ); 85 | }; 86 | 87 | export default UpdateSiteConfigModal; 88 | -------------------------------------------------------------------------------- /src/pages/eventManagement/roadsideSafetyMessage/RSMList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { history } from 'umi'; 3 | import type { ActionType, TableProColumns } from '@ant-design/pro-table'; 4 | import BaseContainer from 'edge-src/components/BaseContainer'; 5 | import BaseProTable from 'edge-src/components/BaseProTable'; 6 | import { roadSideMessageList } from 'edge-src/services/event/rsm'; 7 | import { DataSourceOptions, ParticipantTypeOptions } from 'edge-src/utils/constants'; 8 | import { dataFormat, statusOptionFormat } from 'edge-src/utils'; 9 | import LonLatUnit from 'edge-src/components/LonLatUnit'; 10 | 11 | const RSMList: React.FC = () => { 12 | const actionRef = useRef(); 13 | const columns: TableProColumns[] = [ 14 | { 15 | title: t('Message ID'), 16 | dataIndex: 'id', 17 | }, 18 | { 19 | title: t('Target ID'), 20 | dataIndex: 'ptcId', 21 | }, 22 | { 23 | title: t('Participant Type'), 24 | dataIndex: 'ptcType', 25 | valueType: 'select', 26 | valueEnum: statusOptionFormat(ParticipantTypeOptions), 27 | search: true, 28 | }, 29 | { 30 | title: t('Data Sources'), 31 | dataIndex: 'source', 32 | valueType: 'select', 33 | valueEnum: statusOptionFormat(DataSourceOptions), 34 | }, 35 | { 36 | title: t('Millisecond Time'), 37 | dataIndex: 'secMark', 38 | }, 39 | { 40 | title: t('Speed'), 41 | dataIndex: 'speed', 42 | render: (_, { speed }: Event.RSMListItem) => dataFormat(speed * 0.02 * 3.6, 'km/h'), 43 | }, 44 | { 45 | title: t('Heading'), 46 | dataIndex: 'heading', 47 | render: (_, { heading }: Event.RSMListItem) => dataFormat(heading * 0.0125, '°'), 48 | }, 49 | { 50 | title: t('Longitude'), 51 | dataIndex: 'lon', 52 | render: (_, { lon }: Event.RSMListItem) => , 53 | }, 54 | { 55 | title: t('Latitude'), 56 | dataIndex: 'lat', 57 | render: (_, { lat }: Event.RSMListItem) => , 58 | }, 59 | { 60 | title: t('Reporting Time'), 61 | dataIndex: 'createTime', 62 | sorter: true, 63 | }, 64 | { 65 | title: t('Operate'), 66 | width: 160, 67 | fixed: 'right', 68 | valueType: 'option', 69 | render: (_, row) => [ 70 | 73 | history.push({ 74 | pathname: `/event/rsm/details`, 75 | state: row, 76 | }) 77 | } 78 | > 79 | {t('Details')} 80 | , 81 | ], 82 | }, 83 | ]; 84 | return ( 85 | 86 | 87 | 88 | ); 89 | }; 90 | 91 | export default RSMList; 92 | -------------------------------------------------------------------------------- /e2e/utils/form.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from '@playwright/test'; 2 | import { clickQuerySearchBtn } from './table'; 3 | 4 | export const formItemSelect = (page: Page, selector: string) => { 5 | return page.locator(`.antd-form-item-${selector}`).locator('.ant-select'); 6 | }; 7 | 8 | // 创建和编辑时选择的下拉框 9 | export const setSelectValue = async ( 10 | page: Page, 11 | selector: string, 12 | detail_selector: string, 13 | nthChild: number = 1, 14 | ) => { 15 | await formItemSelect(page, selector).click(); 16 | await page 17 | .locator(`${detail_selector} + .rc-virtual-list`) 18 | .locator(`.rc-virtual-list-holder-inner > div:nth-child(${nthChild})`) 19 | .click(); 20 | }; 21 | 22 | // 查询时选择的下拉框 23 | export const setQuerySelectValue = async (page: Page, selector: string, nthchild: number = 1) => { 24 | await page.click(selector); 25 | await page.locator(`.rc-virtual-list-holder-inner > div:nth-child(${nthchild})`).click(); 26 | await clickQuerySearchBtn(page); 27 | await page.waitForTimeout(1000); 28 | }; 29 | 30 | export const formItemCascader = (page: Page, selector: string) => { 31 | return page.locator(`.antd-form-item-${selector}`).locator('.ant-cascader'); 32 | }; 33 | 34 | export const setCascaderItemValue = async (page: Page, index: number = 0) => { 35 | await page 36 | .locator(`.ant-cascader-dropdown`) 37 | .locator('.ant-cascader-menu-item') 38 | .nth(index) 39 | .click(); 40 | }; 41 | 42 | // indexs: 级联菜单下标列表 43 | export const setCascaderValue = async (page: Page, selector: string, indexs: number[]) => { 44 | await formItemCascader(page, selector).click(); 45 | for (let i = 0; i < indexs.length; i++) { 46 | await setCascaderItemValue(page, indexs[i]); 47 | } 48 | }; 49 | 50 | export const globalModal = (page: Page) => { 51 | const modal = page.locator('.ant-modal-wrap .ant-modal'); 52 | return modal; 53 | }; 54 | 55 | export const globalModalFormItem = (page: Page, selector: string) => { 56 | const item = globalModal(page).locator(selector); 57 | return item; 58 | }; 59 | 60 | export const setModalFormItemValue = async (page: Page, selector: string, value: string) => { 61 | const item = globalModalFormItem(page, selector); 62 | await item.fill(value); 63 | }; 64 | 65 | export const globalModalFormItems = (page: Page, selectors: string[]) => { 66 | const list: any = []; 67 | selectors.map((selector) => { 68 | const item = globalModalFormItem(page, selector); 69 | list.push(item); 70 | }); 71 | return list; 72 | }; 73 | 74 | export const globalModalSubmitBtn = async (page: Page) => { 75 | const footer = globalModal(page).locator('.ant-modal-footer'); 76 | const btn = footer.locator('#submitButton'); 77 | await btn.click(); 78 | }; 79 | 80 | export const globalModalCancelBtn = async (page: Page) => { 81 | const footer = globalModal(page).locator('.ant-modal-footer'); 82 | const btn = footer.locator('#cancelButton'); 83 | await btn.click(); 84 | }; 85 | -------------------------------------------------------------------------------- /e2e/pages/device/Camera.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import { generateIntNum, generateNumLetter, generatePureNumber } from '../../utils'; 3 | import { globalModalSubmitBtn, setModalFormItemValue, setSelectValue } from '../../utils/form'; 4 | import { 5 | checkDetaillWindow, 6 | checkSuccessMsg, 7 | closePopWindow, 8 | gotoPageAndExpectUrl, 9 | useUserStorageState, 10 | } from '../../utils/global'; 11 | import { 12 | clickConfirmModalOkBtn, 13 | clickCreateBtn, 14 | clickDeleteTextBtn, 15 | clickDetailTextBtn, 16 | clickEditBtn, 17 | searchItemAndQuery, 18 | } from '../../utils/table'; 19 | 20 | test.describe('The Camera Page', () => { 21 | const randomNumLetter = generateNumLetter(); 22 | const randomNum = generatePureNumber(); 23 | const cameraNameVal = `camera_name_${1}`; 24 | const camernSnVal = `C_${randomNumLetter}`; 25 | const videoUrl = generateNumLetter(); 26 | const lng = generateIntNum({ max: 180 }); 27 | const lat = generateIntNum({ max: 90 }); 28 | const descVal = 'test description info'; 29 | const pageUrl = '/device/camera'; 30 | 31 | // Use signed-in state of 'userStorageState.json'. 32 | useUserStorageState(); 33 | 34 | test.beforeEach(async ({ page }) => { 35 | await gotoPageAndExpectUrl(page, pageUrl); 36 | }); 37 | 38 | test('successfully create camera', async ({ page }) => { 39 | await clickCreateBtn(page); 40 | 41 | await setModalFormItemValue(page, '#name', cameraNameVal); 42 | await setModalFormItemValue(page, '#sn', camernSnVal); 43 | await setModalFormItemValue(page, '#streamUrl', videoUrl); 44 | await setModalFormItemValue(page, '#lng', String(lng)); 45 | await setModalFormItemValue(page, '#lat', String(lat)); 46 | await setModalFormItemValue(page, '#elevation', randomNum); 47 | await setModalFormItemValue(page, '#towards', randomNum); 48 | await setModalFormItemValue(page, '#desc', descVal); 49 | await setSelectValue(page, 'rsuId', '#rsuId_list'); 50 | 51 | await globalModalSubmitBtn(page); 52 | await checkSuccessMsg(page); 53 | }); 54 | 55 | test('successfully edit camera', async ({ page }) => { 56 | await searchItemAndQuery(page, '#name', cameraNameVal); 57 | await clickEditBtn(page); 58 | 59 | await setModalFormItemValue(page, '#name', `update_${cameraNameVal}`); 60 | 61 | await globalModalSubmitBtn(page); 62 | await checkSuccessMsg(page); 63 | }); 64 | 65 | test('successfully view detail', async ({ page }) => { 66 | await searchItemAndQuery(page, '#name', `update_${cameraNameVal}`); 67 | await clickDetailTextBtn(page); 68 | await checkDetaillWindow(page); 69 | await closePopWindow(page); 70 | }); 71 | 72 | test('successfully delete camera', async ({ page }) => { 73 | await searchItemAndQuery(page, '#name', `update_${cameraNameVal}`); 74 | await clickDeleteTextBtn(page); 75 | await clickConfirmModalOkBtn(page); 76 | await checkSuccessMsg(page); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/components/ParameterInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProCard from '@ant-design/pro-card'; 3 | import { Col, Row } from 'antd'; 4 | import { SampleModeOptions } from 'edge-src/utils/constants'; 5 | 6 | type ParameterType = { 7 | key: string; 8 | label: string; 9 | span: number; 10 | render?: (data: string) => string; 11 | unit?: string; 12 | }; 13 | 14 | const ParameterInfo: React.FC<{ parameterInfo: Config.ParameterInfo | undefined }> = ({ 15 | parameterInfo = {}, 16 | }) => { 17 | const upsideCap: ParameterType[] = [ 18 | { 19 | key: 'upLimit', 20 | label: t('Forwarding Limit'), 21 | span: 24, 22 | }, 23 | ]; 24 | const rsi: ParameterType[] = [ 25 | { 26 | key: 'upFilters', 27 | label: t('Filter Rules'), 28 | span: 24, 29 | render: (upFilters: string) => upFilters && JSON.stringify(upFilters), 30 | unit: '', 31 | }, 32 | ]; 33 | const spat: ParameterType[] = [...upsideCap, ...rsi]; 34 | const bsm: ParameterType[] = [ 35 | { 36 | key: 'sampleMode', 37 | label: t('Sampling Method'), 38 | span: 12, 39 | render: (sampleMode: string) => SampleModeOptions[sampleMode], 40 | unit: '', 41 | }, 42 | { 43 | key: 'sampleRate', 44 | label: t('Sampling Rate'), 45 | span: 12, 46 | }, 47 | ...upsideCap, 48 | ...rsi, 49 | ]; 50 | 51 | const infoMap = [ 52 | { 53 | title: t('BSM_CONFIG_DETAILS'), 54 | groupKey: 'bsmConfig', 55 | children: bsm, 56 | }, 57 | { 58 | title: t('RSI_CONFIG_DETAILS'), 59 | groupKey: 'rsiConfig', 60 | children: rsi, 61 | }, 62 | { 63 | title: t('RSM_CONFIG_DETAILS'), 64 | groupKey: 'rsmConfig', 65 | children: spat, 66 | }, 67 | { 68 | title: t('MAP_CONFIG_DETAILS'), 69 | groupKey: 'mapConfig', 70 | children: spat, 71 | }, 72 | { 73 | title: t('SPAT_CONFIG_DETAILS'), 74 | groupKey: 'spatConfig', 75 | children: spat, 76 | }, 77 | ]; 78 | return ( 79 | 80 | {infoMap.map(({ title, groupKey, children }) => ( 81 | 82 |
{title}
83 | 84 | {children.map(({ key, span, label, render, unit }) => { 85 | const value = parameterInfo[groupKey]?.[key]; 86 | return ( 87 | 88 | {label}: 89 | {render?.(value) || value || '-'} 90 | {value ? unit ?? ` ${t('bars/s')}` : ''} 91 | 92 | ); 93 | })} 94 | 95 |
96 | ))} 97 |
98 | ); 99 | }; 100 | 101 | export default ParameterInfo; 102 | -------------------------------------------------------------------------------- /e2e/pages/device/Ladar.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import { generateIntNum, generateNumLetter, generatePureNumber } from '../../utils'; 3 | import { globalModalSubmitBtn, setModalFormItemValue, setSelectValue } from '../../utils/form'; 4 | import { 5 | checkDetaillWindow, 6 | checkSuccessMsg, 7 | closePopWindow, 8 | gotoPageAndExpectUrl, 9 | useUserStorageState, 10 | } from '../../utils/global'; 11 | import { 12 | clickConfirmModalOkBtn, 13 | clickCreateBtn, 14 | clickDeleteTextBtn, 15 | clickDetailTextBtn, 16 | clickEditBtn, 17 | searchItemAndQuery, 18 | } from '../../utils/table'; 19 | 20 | test.describe('The Ladar Page', () => { 21 | const randomNumLetter = generateNumLetter(); 22 | const randomNum = generatePureNumber(); 23 | const ladarNameVal = `ladar_name_${1}`; 24 | const ladarnSnVal = `C_${randomNumLetter}`; 25 | const lng = generateIntNum({ max: 180 }); 26 | const lat = generateIntNum({ max: 90 }); 27 | const descVal = 'test description info'; 28 | const pageUrl = '/device/radar'; 29 | const ladarIPVal = [ 30 | generateIntNum({ max: 256 }), 31 | generateIntNum({ max: 256 }), 32 | generateIntNum({ max: 256 }), 33 | generateIntNum({ max: 256 }), 34 | ].join('.'); 35 | // Use signed-in state of 'userStorageState.json'. 36 | useUserStorageState(); 37 | 38 | test.beforeEach(async ({ page }) => { 39 | await gotoPageAndExpectUrl(page, pageUrl); 40 | }); 41 | 42 | test('successfully create ladar', async ({ page }) => { 43 | await clickCreateBtn(page); 44 | 45 | await setModalFormItemValue(page, '#name', ladarNameVal); 46 | await setModalFormItemValue(page, '#sn', ladarnSnVal); 47 | await setModalFormItemValue(page, '#lng', String(lng)); 48 | await setModalFormItemValue(page, '#lat', String(lat)); 49 | await setModalFormItemValue(page, '#elevation', randomNum); 50 | await setModalFormItemValue(page, '#towards', randomNum); 51 | await setModalFormItemValue(page, '#radarIP', ladarIPVal); 52 | await setModalFormItemValue(page, '#desc', descVal); 53 | await setSelectValue(page, 'rsuId', '#rsuId_list'); 54 | 55 | await globalModalSubmitBtn(page); 56 | await checkSuccessMsg(page); 57 | }); 58 | 59 | test('successfully edit ladar', async ({ page }) => { 60 | await searchItemAndQuery(page, '#name', ladarNameVal); 61 | await clickEditBtn(page); 62 | 63 | await setModalFormItemValue(page, '#name', `update_${ladarNameVal}`); 64 | 65 | await globalModalSubmitBtn(page); 66 | await checkSuccessMsg(page); 67 | }); 68 | 69 | test('successfully view ladar detail', async ({ page }) => { 70 | await searchItemAndQuery(page, '#name', `update_${ladarNameVal}`); 71 | await clickDetailTextBtn(page); 72 | await checkDetaillWindow(page); 73 | await closePopWindow(page); 74 | }); 75 | 76 | test('successfully delete ladar', async ({ page }) => { 77 | await searchItemAndQuery(page, '#name', `update_${ladarNameVal}`); 78 | await clickDeleteTextBtn(page); 79 | await clickConfirmModalOkBtn(page); 80 | await checkSuccessMsg(page); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/components/BaseProTable/index.tsx: -------------------------------------------------------------------------------- 1 | import ProTable from '@ant-design/pro-table'; 2 | import type { ActionType, TableProColumns } from '@ant-design/pro-table'; 3 | import type { OptionConfig, ToolBarProps } from '@ant-design/pro-table/lib/components/ToolBar'; 4 | import type { TableProps } from 'antd'; 5 | import type { ExpandableConfig } from 'antd/lib/table/interface'; 6 | 7 | type BaseProTableType = { 8 | columns: TableProColumns[]; 9 | bordered?: boolean; 10 | actionRef?: React.Ref | undefined; 11 | dataSource?: any[]; 12 | params?: Record; 13 | request?: (params: any) => Promise>; 14 | rowSelection?: 15 | | (TableProps['rowSelection'] & { 16 | alwaysShowAlert?: boolean; 17 | }) 18 | | false; 19 | rowKey?: string; 20 | search?: false | { labelWidth: number }; 21 | pagination?: { pageSize: number } | false; 22 | scroll?: { x: number }; 23 | headerTitle?: string | React.ReactNode; 24 | toolBarRender?: ToolBarProps['toolBarRender'] | false; 25 | options?: false | OptionConfig; 26 | expandable?: ExpandableConfig; 27 | }; 28 | 29 | const BaseProTable: React.FC = (props) => { 30 | const { 31 | bordered = false, 32 | columns = [], 33 | actionRef, 34 | dataSource, 35 | request, 36 | params, 37 | rowSelection, 38 | rowKey = 'id', 39 | search = { labelWidth: 0 }, 40 | pagination = { pageSize: 10 }, 41 | scroll, 42 | headerTitle, 43 | toolBarRender, 44 | options, 45 | expandable, 46 | } = props; 47 | 48 | /** 49 | * @description: 获取处理后的columns 50 | * @return {*} 51 | */ 52 | const getColumns = () => { 53 | const newColumns = columns 54 | .filter((c) => !c.hidden) 55 | .map((col) => { 56 | return { 57 | ...col, 58 | search: col.search || false, 59 | }; 60 | }); 61 | 62 | return newColumns; 63 | }; 64 | 65 | const getPagination = () => { 66 | if (pagination) { 67 | const newPagination = { 68 | defaultPageSize: pagination.pageSize, 69 | }; 70 | return newPagination; 71 | } 72 | return pagination; 73 | }; 74 | 75 | return ( 76 | { 82 | if (createTime) { 83 | param.sortDir = createTime === 'ascend' ? 'asc' : 'desc'; 84 | } 85 | const res = await request?.(param); 86 | return { data: res?.data, total: res?.total, success: true }; 87 | }} 88 | params={params} 89 | rowSelection={rowSelection} 90 | rowKey={rowKey} 91 | search={search} 92 | pagination={getPagination()} 93 | scroll={scroll} 94 | headerTitle={headerTitle} 95 | toolBarRender={toolBarRender} 96 | options={options} 97 | expandable={expandable} 98 | /> 99 | ); 100 | }; 101 | 102 | export default BaseProTable; 103 | -------------------------------------------------------------------------------- /docs/catalog-tree.md: -------------------------------------------------------------------------------- 1 | # 目录介绍 2 | 3 | ```js 4 | ├── dist // 默认的 build 输出目录 5 | ├── config // 全局配置文件 6 | │ ├── config.dev.ts // 开发环境配置 7 | │ ├── config.ts // 生产环境配置 8 | │ ├── defaultSettings.ts // 项目默认 layout 配置 9 | │ ├── proxy.ts // 开发环境后端 server 配置 10 | │ ├── routes.ts // 项目路由配置 11 | ├── deploy // 部署相关配置文件 12 | ├── docs // 文档 13 | ├── mock // mock 数据 14 | ├── public // 公共的文件(如 image、favicon 等) 15 | ├── src // 源码目录 16 | │ ├── components // 公共组件 17 | │ ├── locales // 国际化文件 18 | │ ├── pages // 页面模块 19 | │ ├── services // api 20 | │ ├── utils // 工具类 21 | │ ├── access.ts // access 22 | │ ├── app.tsx // 入口文件 23 | │ ├── global.less // 全局样式 24 | │ ├── manafest.json // 应用元数据 25 | │ ├── service-worker.js // service-worker 26 | │ ├── typings.d.ts // ts 类型声明 27 | ├── tests // 源码目录 28 | ├── .editorconfig // IDE格式规范 29 | ├── .eslintignore // eslint忽略 30 | ├── .eslintrc // eslint配置文件 31 | ├── .gitignore // git忽略 32 | ├── .prettierignore // prettierc忽略 33 | ├── .prettierrc // prettierc配置文件 34 | ├── .stylelintignore // stylelint忽略 35 | ├── .stylelintrc // stylelint配置文件 36 | ├── Dockerfile // stylelint配置文件 37 | ├── dprint.json // github action 代码规范检测 38 | ├── Gruntfile.js // grunt 配置文件,用于扫描生产国际化文件 39 | ├── jest.config.js // 单元测试配置文件 40 | ├── jsconfig.json // javascript 辅助配置文件 41 | ├── LICENSE // license 42 | ├── package.json // package 43 | ├── playwright.config.ts // e2e 测试配置文件 44 | ├── README.md // README 45 | ├── tsconfig.json // typescript 配置文件 46 | ``` 47 | 48 | ## 文件命名规范 49 | 50 | ### 文件命名 51 | 52 | - 属于 components 文件夹下的子文件夹,使用大写字母开头的 PascalBase 风格 53 | - 如果是组件文件,则使用 PascalCase,如 MyComponent.ts 54 | - 如果该文件夹内是一个组件,则组件主入口命名为 index,需要有 index.ts,该文件夹命名使用 PascalCase。如: 55 | 56 | ```js 57 | src / components / Button / index.ts; 58 | ``` 59 | 60 | ```js 61 | -[src] - 62 | [pages] - 63 | [layout] - 64 | [components] - 65 | [Sidebar] - 66 | index.ts - 67 | Item.ts - 68 | SidebarItem.ts - 69 | AppMain.ts - 70 | index.ts - 71 | Navbar.ts; 72 | ``` 73 | 74 | - 其他情况文件夹使用 CamelCase, 单复数视情况 75 | 76 | ### 图片命名 77 | 78 | 1. 图片名称必须小写,禁止使用特殊字符、中文 79 | 2. 使用英文或英文缩写,禁止特殊字符,禁止使用拼音 80 | 3. 名称间隔使用\_符号 81 | 4. 命名需要能体现图片的大概用途 82 | 5. 禁止文件名和实际图片内容不符 83 | 6. icon 命名禁止出现拼音 84 | 85 | 常用示例: 86 | 87 | ```js 88 | bg.jpg; //背景图片 89 | mod_bg.jpg; //模块背景 90 | ``` 91 | --------------------------------------------------------------------------------