小程序可以加载账单
This commit is contained in:
parent
9065c2dda3
commit
f1185057b8
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
spaces_around_operators = true
|
||||
max_line_length = 120
|
13
.eslintignore
Normal file
13
.eslintignore
Normal file
@ -0,0 +1,13 @@
|
||||
# don't ever lint node_modules
|
||||
node_modules
|
||||
|
||||
# don't lint build output (make sure it's set to your correct build folder name)
|
||||
build
|
||||
|
||||
# don't lint nyc coverage output
|
||||
coverage
|
||||
|
||||
.eslintrc.js
|
||||
vite.config.ts
|
||||
palette.js
|
||||
tailwind.config.js
|
38
.eslintrc.js
Normal file
38
.eslintrc.js
Normal file
@ -0,0 +1,38 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
commonjs: true,
|
||||
es6: true,
|
||||
amd: true
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json']
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking'
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.tsx', '**/*.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
420
.gitignore
vendored
420
.gitignore
vendored
@ -1,24 +1,418 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
node_modules
|
||||
# Debug Files
|
||||
*.sql
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# General
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
!*.svg # comment out if you don't need vector files
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/jetbrains+all
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains+all
|
||||
|
||||
### JetBrains+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### JetBrains+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/jetbrains+all
|
||||
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# pnpm lock file
|
||||
pnpm-lock.yaml
|
||||
|
18
.prettierignore
Normal file
18
.prettierignore
Normal file
@ -0,0 +1,18 @@
|
||||
*.min.js
|
||||
/node_modules
|
||||
/dist
|
||||
# OS
|
||||
.DS_Store
|
||||
.idea
|
||||
.editorconfig
|
||||
.npmrc
|
||||
package-lock.json
|
||||
# Ignored suffix
|
||||
*.log
|
||||
*.md
|
||||
*.svg
|
||||
*.png
|
||||
*ignore
|
||||
## Built-files
|
||||
.cache
|
||||
dist
|
20
.prettierrc.js
Normal file
20
.prettierrc.js
Normal file
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
quoteProps: 'as-needed',
|
||||
jsxSingleQuote: false,
|
||||
trailingComma: 'none',
|
||||
bracketSpacing: true,
|
||||
jsxBracketSameLine: false,
|
||||
arrowParens: 'avoid',
|
||||
rangeStart: 0,
|
||||
rangeEnd: Infinity,
|
||||
requirePragma: false,
|
||||
insertPragma: false,
|
||||
proseWrap: 'preserve',
|
||||
htmlWhitespaceSensitivity: 'css',
|
||||
endOfLine: 'lf'
|
||||
};
|
36
README.md
36
README.md
@ -1,30 +1,18 @@
|
||||
# React + TypeScript + Vite
|
||||
# 用电管理服务项目(前端项目)
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
该项目为华昌宝能发布的采用众包模式承接的小型Web服务系统项目。
|
||||
|
||||
Currently, two official plugins are available:
|
||||
项目采用以下技术框架:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
- React 18
|
||||
- React Router 6
|
||||
- Emotion
|
||||
- Tailwind CSS 3
|
||||
- Twin.macro
|
||||
- Ant Design 5
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
项目采用Vite 3编译打包,使用Typescript编写。
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
项目详细设计方案见[详细设计方案](https://kdocs.cn/l/cawe22YUV3bJ),该设计方案未经许可,禁止私自修改。
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default {
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
||||
项目任务分配与状态概览表见[任务概况](https://kdocs.cn/l/camrXvBMlCNs)。
|
||||
|
34
index.html
34
index.html
@ -1,13 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>华昌宝能</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>用电管理服务系统</title>
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
|
||||
<meta http-equiv="pragram" content="no-cache" />
|
||||
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate"/>
|
||||
<meta http-equiv="expires" content="0"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
8060
package-lock.json
generated
8060
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
109
package.json
109
package.json
@ -1,45 +1,84 @@
|
||||
{
|
||||
"name": "test",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"name": "trans_power_manage",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host",
|
||||
"local-mock": "PROXY_TARGET=local_mock vite --host",
|
||||
"window-local-mock": "set PROXY_TARGET=local_mock && vite --host",
|
||||
"remote": "PROXY_TARGET=remote vite --host",
|
||||
"window-remote": "set PROXY_TARGET=remote && vite --host",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd-mobile": "^5.34.0",
|
||||
"antd-mobile-icons": "^0.3.0",
|
||||
"ramda": "^0.29.1",
|
||||
"@ant-design/compatible": "^5.1.1",
|
||||
"@ant-design/icons": "^5.0.1",
|
||||
"@ant-design/pro-components": "^2.6.43",
|
||||
"@antv/g2": "^4.2.7",
|
||||
"@emotion/css": "^11.9.0",
|
||||
"@emotion/react": "^11.9.0",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@tanstack/react-query": "^4.3.9",
|
||||
"@tanstack/react-query-devtools": "^4.3.9",
|
||||
"@uiw/react-md-editor": "^3.14.5",
|
||||
"antd": "^5.1.6",
|
||||
"axios": "^0.27.2",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"bignumber.js": "^9.0.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"dayjs": "^1.11.2",
|
||||
"debounce-promise": "^3.1.2",
|
||||
"decamelize-keys": "^2.0.1",
|
||||
"decimal.js": "^10.3.1",
|
||||
"formik": "^2.2.9",
|
||||
"immer": "^9.0.14",
|
||||
"jspdf": "^2.5.1",
|
||||
"qs": "^6.10.3",
|
||||
"ramda": "^0.28.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.22.2"
|
||||
"react-router": "^6.10.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-to-print": "^2.14.15",
|
||||
"react-use": "^17.4.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"use-animate-number": "^1.0.5",
|
||||
"use-immer": "^0.7.0",
|
||||
"zustand": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.25",
|
||||
"@types/ramda": "^0.29.11",
|
||||
"@types/react": "^18.2.56",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.1.4"
|
||||
"@babel/core": "^7.20.12",
|
||||
"@emotion/babel-plugin": "^11.9.2",
|
||||
"@emotion/babel-plugin-jsx-pragmatic": "^0.1.5",
|
||||
"@ricons/fluent": "^0.12.0",
|
||||
"@ricons/utils": "^0.1.6",
|
||||
"@types/debounce-promise": "^3.1.4",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/ramda": "^0.28.13",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-router": "^5.1.18",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.26.0",
|
||||
"@typescript-eslint/parser": "^5.26.0",
|
||||
"@vitejs/plugin-react": "^1.3.2",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"clsx": "^1.1.1",
|
||||
"esbuild": "^0.14.44",
|
||||
"eslint": "^8.16.0",
|
||||
"html2pdf.js": "^0.10.1",
|
||||
"less": "^4.1.2",
|
||||
"postcss": "^8.4.14",
|
||||
"prettier": "^2.7.1",
|
||||
"tailwindcss": "^3.1.6",
|
||||
"twin.macro": "^3.4.1",
|
||||
"typescript": "^4.7.2",
|
||||
"vite": "^4.4.0",
|
||||
"vite-plugin-imp": "^2.2.0"
|
||||
},
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"chrome": "49",
|
||||
"ios": "10"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
"babelMacros": {
|
||||
"twin": {
|
||||
"preset": "emotion"
|
||||
}
|
||||
}
|
||||
}
|
31
src/App.tsx
31
src/App.tsx
@ -1,20 +1,17 @@
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
//@ts-nocheck
|
||||
import { suspense } from '@c/hoc/suspense';
|
||||
import { CentralSpin } from '@c/ui/CentralSpin';
|
||||
import { FC, Suspense } from 'react';
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
const UserReport = suspense('UserReport', () => import('@p/report'));
|
||||
const Test = suspense('Test', () => import('@p/test'));
|
||||
|
||||
const Recharge = React.lazy(() => import("@p/Recharge/index"))
|
||||
const RechargeRecords = React.lazy(() => import("@p/RechargeRecords/index"))
|
||||
const Return = React.lazy(() => import("@p/Return/index"))
|
||||
const App: FC = () => {
|
||||
/**
|
||||
* 定义应用的主体路由结构。
|
||||
* @returns 应用主体结构组件。
|
||||
*/
|
||||
export const App: FC = () => {
|
||||
return (
|
||||
<Suspense>
|
||||
<Routes>
|
||||
<Route path="/recharge" element={<Recharge />} />
|
||||
<Route path="/rechargeRecords" element={<RechargeRecords />} />
|
||||
<Route path="/return" element={<Return />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
<UserReport />
|
||||
);
|
||||
};
|
||||
|
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
@ -1,12 +0,0 @@
|
||||
import { Card } from "antd-mobile";
|
||||
import { FC } from "react";
|
||||
|
||||
const MeterInfo: FC = () => {
|
||||
return <Card title='256160646' >
|
||||
<div> 地址:xxxxx </div>
|
||||
<div> 金额:500 </div>
|
||||
<div> 商户:xxxxx </div>
|
||||
</Card>
|
||||
}
|
||||
|
||||
export default MeterInfo;
|
7
src/config/index.tsx
Normal file
7
src/config/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export const layout = { labelCol: { span: 8 }, wrapperCol: { span: 16 } };
|
||||
|
||||
export const disabledPark = [
|
||||
'/calculate/list', '/park/management', "/son-account", "/flow",
|
||||
"/park/registry", "/publicity/retrieval",
|
||||
"/sync/status", "/sync/setting", "/log"
|
||||
]
|
208
src/config/menu.tsx
Normal file
208
src/config/menu.tsx
Normal file
@ -0,0 +1,208 @@
|
||||
import React from 'react';
|
||||
import ArchiveSettings20Regular from '@ricons/fluent/lib/ArchiveSettings20Regular';
|
||||
import DataArea24Regular from '@ricons/fluent/lib/DataArea24Regular';
|
||||
import DocumentEdit24Regular from '@ricons/fluent/lib/DocumentEdit24Regular';
|
||||
import Glance24Regular from '@ricons/fluent/lib/Glance24Regular';
|
||||
import Pulse32Regular from '@ricons/fluent/lib/Pulse32Regular';
|
||||
import Settings32Regular from '@ricons/fluent/lib/Settings32Regular';
|
||||
import SignOut24Regular from '@ricons/fluent/lib/SignOut24Regular';
|
||||
import ClipboardDataBar32Regular from '@ricons/fluent/lib/ClipboardDataBar32Regular';
|
||||
import ContentView32Regular from '@ricons/fluent/lib/ContentView32Regular';
|
||||
import ArrowSyncCheckmark24Regular from '@ricons/fluent/lib/ArrowSyncCheckmark24Regular';
|
||||
import People16Regular from '@ricons/fluent/lib/People16Regular';
|
||||
import Account from "@ricons/fluent/PersonAccounts24Regular"
|
||||
import Sale from '@ricons/fluent/ShoppingBagTag24Regular'
|
||||
import 'twin.macro';
|
||||
import { PrivilegedItemType } from '@/shared/system';
|
||||
import Permission from '@ricons/fluent/lib/LockClosed12Regular'
|
||||
import Statement from '@ricons/fluent/lib/FormNew24Regular'
|
||||
import ReadingManagement from '@ricons/fluent/lib/Board16Filled'
|
||||
import Refund from '@ricons/fluent/lib/GroupReturn24Regular'
|
||||
import { ProjectOutlined } from '@ant-design/icons'
|
||||
|
||||
const MainMenuItems: PrivilegedItemType[] = [
|
||||
{
|
||||
label: '概要信息',
|
||||
icon: <Glance24Regular tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'glance',
|
||||
link: '/',
|
||||
any: [0, 1, 2]
|
||||
},
|
||||
{
|
||||
label: '子账号管理',
|
||||
icon: <People16Regular tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'son',
|
||||
link: '/son-account',
|
||||
any: [0]
|
||||
},
|
||||
{
|
||||
label: '设备管理',
|
||||
icon: <ProjectOutlined />,
|
||||
key: 'equipment',
|
||||
all: [0],
|
||||
children: [
|
||||
{ label: '卡管理', key: 'card', link: '/equipment/card', all: [0] },
|
||||
{ label: '通讯协议管理', key: 'message', link: '/equipment/message', all: [0] },
|
||||
{ label: '厂家管理', key: 'factory', link: '/equipment/factory', all: [0] },
|
||||
{ label: '电表设置', key: 'meter-setting', link: '/equipment/meter-setting', all: [0] },
|
||||
{ label: '电表操作', key: 'meter-operate', link: '/equipment/meter-operate', all: [0] },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '用户管理',
|
||||
icon: <ArchiveSettings20Regular tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'setup',
|
||||
all: [0],
|
||||
children: [
|
||||
{ label: '园区管理', key: 'setup-park', link: '/park/management', all: [0] },
|
||||
{ label: '建筑管理', key: 'building', link: '/park/building', all: [0] },
|
||||
{ label: '商户管理', key: 'setup-tenement', link: '/park/tenement', all: [0] },
|
||||
{ label: '园区表计管理', key: 'setup-kv04', link: '/park/04kv', all: [0] },
|
||||
{ label: '电表箱管理', key: 'meter-box', link: '/park/meter-box', all: [0] },
|
||||
{ label: '表计分摊管理', key: 'setup-pooled', link: '/park/pooled', all: [0] }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "抄表管理", icon: <ReadingManagement tw="w-4 h-4" onPointerEnterCapture={undefined}
|
||||
onPointerLeaveCapture={undefined}/>, key: 'readingManagement', all: [0], children: [
|
||||
{ label: '抄表记录', icon: <ClipboardDataBar32Regular tw="w-4 h-4" onPointerEnterCapture={undefined}
|
||||
onPointerLeaveCapture={undefined} />, key: 'reading', link: '/reading', all: [0] },
|
||||
{ label: '退补电量', icon: <Refund tw="w-4 h-4" onPointerEnterCapture={undefined}
|
||||
onPointerLeaveCapture={undefined} />, key: 'refund', link: '/refund', all: [0] },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '售电管理',
|
||||
icon: <Sale tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'electric',
|
||||
any: [0,],
|
||||
children: [
|
||||
{ label: '商户充值', key: 'electric-recharge', link: '/electric/recharge', any: [0] },
|
||||
{ label: '商户冲正', key: 'electric-reversal', link: '/electric/reversal', any: [0] },
|
||||
{ label: '商户退费', key: 'electric-return', link: '/electric/return', all: [0] }
|
||||
]
|
||||
},
|
||||
// { label: '商户充值', icon: <Payment28Regular tw="w-4 h-4" />, key: 'charge', link: '/charge', all: [0] },
|
||||
{
|
||||
label: '电费核算',
|
||||
icon: <DocumentEdit24Regular tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'calculate',
|
||||
link: '/calculate/list',
|
||||
all: [0]
|
||||
},
|
||||
{
|
||||
label: '企业电费核算',
|
||||
icon: <DocumentEdit24Regular tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'calculate-all',
|
||||
link: '/calculate/list-all',
|
||||
all: [2]
|
||||
},
|
||||
{
|
||||
label: '历史电费核算',
|
||||
icon: <DataArea24Regular tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'publicity',
|
||||
any: [0, 1, 2],
|
||||
children: [
|
||||
{ label: '往期电费核算检索', key: 'publicity-history', link: '/publicity/retrieval', any: [0, 1, 2] },
|
||||
{ label: '电费核算撤回审核', key: 'publicity-audit', link: '/publicity/audit', all: [2] }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '账务管理',
|
||||
icon: <Account tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'accounting',
|
||||
any: [0,],
|
||||
children: [
|
||||
{ label: '账务记录', key: 'accountingRecord', link: '/accounting/record', any: [0] },
|
||||
{ label: '账务余额查询', key: 'accountingBalance', link: '/accounting/balance', any: [0] },
|
||||
{ label: '余额导入', key: 'balanceExport', link: '/accounting/balanceExport', any: [0] },
|
||||
// { label: '结算记录检索', key: 'accountingFinish', link: '/accounting/finish', all: [0] }
|
||||
]
|
||||
},
|
||||
|
||||
{ label: '发票管理', icon: <ContentView32Regular tw="w-4 h-4" onPointerEnterCapture={undefined}
|
||||
onPointerLeaveCapture={undefined} />, key: 'invoice', all: [0],children: [
|
||||
{ label: '发票开具', key: 'invoice-notyet', link: '/invoice/notyet', any: [0] },
|
||||
{ label: '已开发票', key: 'invoice-already', link: '/invoice/already', any: [0] },
|
||||
] },
|
||||
{
|
||||
label: '同步管理',
|
||||
icon: <ArrowSyncCheckmark24Regular tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'sync',
|
||||
any: [0],
|
||||
children: [
|
||||
{ label: '近期同步任务状态', key: 'syncStatus', link: '/sync/status', any: [0, 2] },
|
||||
{ label: '同步设置', key: 'syncSetting', link: '/sync/setting', all: [0, 2] }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "流程设置",
|
||||
icon: <Settings32Regular tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'flow',
|
||||
link: '/flow',
|
||||
any: [0],
|
||||
},
|
||||
{
|
||||
label: "报表查询",
|
||||
icon: <Statement tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'statement',
|
||||
link: '/statement',
|
||||
any: [0],
|
||||
},
|
||||
{
|
||||
label: "日志查询",
|
||||
icon: <Statement tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'log',
|
||||
link: '/log',
|
||||
any: [0],
|
||||
},
|
||||
{
|
||||
label: '系统管理',
|
||||
icon: <Settings32Regular tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'setting',
|
||||
all: [2],
|
||||
children: [
|
||||
{ label: '系统账号管理', key: 'setting-accounts', link: '/account/management', all: [2] },
|
||||
{ label: '管理单位开户', key: 'setting-openAccount', link: '/account/open', all: [2] },
|
||||
{ label: '服务期限管理', key: 'setting-charge', link: '/account/charge', all: [2] }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '天神模式',
|
||||
icon: <Pulse32Regular tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'godMode',
|
||||
specific: '000',
|
||||
children: [
|
||||
{ label: '用户管理', key: 'gmUsers', link: '/god/user', specific: '000' },
|
||||
{ label: '园区管理', key: 'gmParks', link: '/god/park', specific: '000' },
|
||||
{ label: '报表管理', key: 'gmReport', link: '/god/report', specific: '000' },
|
||||
{
|
||||
key: 'gmSetup',
|
||||
specific: '000',
|
||||
label: '物业信息管理',
|
||||
children: [
|
||||
{ label: '商户管理', key: 'gmReport-tenement', link: '/god/specific/tenement', specific: '000' },
|
||||
{ label: '园区表计管理', key: 'gmReport-kv04', link: '/god/specific/04kv', specific: '000' },
|
||||
{ label: '抄表记录', key: 'gmReport-reading', link: '/god/specific/reading', specific: '000' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '权限设置',
|
||||
icon: <Permission tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'permission',
|
||||
any: [0],
|
||||
link: '/permission'
|
||||
},
|
||||
{
|
||||
label: '退出系统',
|
||||
icon: <SignOut24Regular tw="w-4 h-4" onPointerEnterCapture={undefined} onPointerLeaveCapture={undefined} />,
|
||||
key: 'quit',
|
||||
any: [0, 1, 2],
|
||||
link: '/logout'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
export default MainMenuItems;
|
21
src/exceptions/BaseError.ts
Normal file
21
src/exceptions/BaseError.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 加入了用于支持`instanceof`操作符的表示异常的基类。
|
||||
*
|
||||
* ! 所有用于表示各种异常的自定义异常类都必须继承自本类,不能直接继承`Error`类。
|
||||
*/
|
||||
export default class BaseError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
||||
// ! 以下这一段是一套继承Error类的时候的固定用法,用以支持instanceof操作符,所有继承Error类代表异常的异常类都必须原样复制。
|
||||
this.name = new.target.name;
|
||||
if (typeof (Error as any).captureStackTrace === 'function') {
|
||||
(Error as any).captureStackTrace(this, new.target);
|
||||
}
|
||||
if (typeof Object.setPrototypeOf === 'function') {
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
} else {
|
||||
(this as any).__proto__ = new.target.prototype;
|
||||
}
|
||||
}
|
||||
}
|
30
src/exceptions/NetworkError.ts
Normal file
30
src/exceptions/NetworkError.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import BaseError from './BaseError';
|
||||
|
||||
/**
|
||||
* 用于表示网络故障的异常类。
|
||||
*/
|
||||
export default class NetworkError extends BaseError {
|
||||
/**
|
||||
* 出现异常的编号
|
||||
*/
|
||||
private code: number;
|
||||
|
||||
constructor(code: number, message: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出现异常的错误编号
|
||||
*/
|
||||
get ErrCode(): number {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前异常的具体解释信息。
|
||||
*/
|
||||
get ErrMessage(): string {
|
||||
return this.message;
|
||||
}
|
||||
}
|
21
src/exceptions/RemoteServiceError.ts
Normal file
21
src/exceptions/RemoteServiceError.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import BaseError from './BaseError';
|
||||
|
||||
/**
|
||||
* 用于表示访问远程数据服务出现的错误,通常都是服务端主动报错。
|
||||
*/
|
||||
export default class RemoteServiceError extends BaseError {
|
||||
private code: number;
|
||||
|
||||
constructor(code: number, message: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
get ErrCode(): number {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
get ErrMessage(): string {
|
||||
return this.message;
|
||||
}
|
||||
}
|
15
src/favicon.svg
Normal file
15
src/favicon.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
7
src/logo.svg
Normal file
7
src/logo.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
33
src/main.less
Normal file
33
src/main.less
Normal file
@ -0,0 +1,33 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.anticon svg {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.ant-descriptions-item {
|
||||
vertical-align: baseline;
|
||||
.ant-descriptions-item-container {
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-thead .ant-table-cell {
|
||||
background-color: var(--color-primary-deep) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-primary-deep: #00694d;
|
||||
}
|
||||
|
||||
.ant-picker-ranges {
|
||||
margin-top: 0;
|
||||
}
|
58
src/main.tsx
58
src/main.tsx
@ -1,13 +1,49 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
//@ts-nocheck
|
||||
import { App } from '@/App';
|
||||
import GlobalStyles from '@/styles/GlobalStyles';
|
||||
import { queryClient } from '@q/query_client';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { App as AntApp, ConfigProvider } from 'antd';
|
||||
import zhCN from 'antd/es/locale/zh_CN';
|
||||
import 'babel-polyfill';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import arraySupport from 'dayjs/plugin/arraySupport';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import { enableAllPlugins } from 'immer';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './main.less';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
// 启用Dayjs库中相应的功能。
|
||||
dayjs.locale('zh-cn');
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(arraySupport);
|
||||
// 启用Immer库中的所有功能。
|
||||
enableAllPlugins();
|
||||
|
||||
// 我们将dayjs默认的toString方法和toJSON重写为format到指定格式
|
||||
dayjs.prototype.constructor.prototype.toString = dayjs.prototype.constructor.prototype.toJSON = function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
return this.format('YYYY-MM-DD HH:mm:ss');
|
||||
};
|
||||
|
||||
// 用于修补不存在ResizeObserver功能的代码
|
||||
if (!window.ResizeObserver) {
|
||||
window.ResizeObserver = import('resize-observer-polyfill').default;
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ConfigProvider locale={zhCN} theme={{ token: { colorPrimary: '#00B578' } }}>
|
||||
<AntApp>
|
||||
<BrowserRouter>
|
||||
<GlobalStyles />
|
||||
<App />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</BrowserRouter>
|
||||
</AntApp>
|
||||
</ConfigProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
@ -1,68 +0,0 @@
|
||||
import MeterInfo from "@c/MeterInfo";
|
||||
import { Button, Input, JumboTabs, NavBar, Space, Toast } from "antd-mobile";
|
||||
import { UserContactOutline } from "antd-mobile-icons";
|
||||
import { FC } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const back = () =>
|
||||
Toast.show({
|
||||
content: '点击了返回区域',
|
||||
duration: 1000,
|
||||
})
|
||||
|
||||
|
||||
|
||||
const Recharge: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const jumpToRecords = () => {
|
||||
console.log('跳转')
|
||||
navigate('/rechargeRecords')
|
||||
}
|
||||
|
||||
const right = (
|
||||
<div>
|
||||
<Space style={{ '--gap': '16px' }}>
|
||||
<div onClick={jumpToRecords}>充值记录</div>
|
||||
<div><UserContactOutline /></div>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
|
||||
return <div>
|
||||
<NavBar right={right} onBack={back}>
|
||||
充值电费
|
||||
</NavBar>
|
||||
<div style={{padding: '2vw'}}>
|
||||
<div style={{marginBottom: '2vw'}}> 请输入要充值的表号或公司名称 </div>
|
||||
<div style={{display: 'flex'}}>
|
||||
<Input placeholder='请输入要充值的表号或公司名称' clearable style={{flex: 1}} />
|
||||
<Button color='primary' fill='none'>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<MeterInfo />
|
||||
<div style={{marginTop: '4vw'}}>
|
||||
<div> 选择金额 </div>
|
||||
<JumboTabs>
|
||||
<JumboTabs.Tab title='100' description='' key='100'></JumboTabs.Tab>
|
||||
<JumboTabs.Tab title='200' description='' key='200'></JumboTabs.Tab>
|
||||
<JumboTabs.Tab title='500' description='' key='500'></JumboTabs.Tab>
|
||||
|
||||
</JumboTabs>
|
||||
<JumboTabs>
|
||||
<JumboTabs.Tab title='1000' description='' key='1000'></JumboTabs.Tab>
|
||||
<JumboTabs.Tab title='2000' description='' key='2000'></JumboTabs.Tab>
|
||||
<JumboTabs.Tab title='自定义' description='' key='自定义'></JumboTabs.Tab>
|
||||
</JumboTabs>
|
||||
<div style={{marginTop: '4vw'}}>
|
||||
<Input placeholder='请输入自定义金额' clearable />
|
||||
</div>
|
||||
<div style={{marginTop: '4vw'}}>
|
||||
本服务。。。。。。。。。。。。。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Recharge;
|
@ -1,47 +0,0 @@
|
||||
import { NavBar } from "antd-mobile";
|
||||
import { DownOutline } from "antd-mobile-icons";
|
||||
import { FC } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const RechargeRecords: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const back = () => {
|
||||
console.log('跳转')
|
||||
navigate(-1)
|
||||
}
|
||||
return <div>
|
||||
<NavBar onBack={back}>
|
||||
充值记录
|
||||
</NavBar>
|
||||
<div style={{padding: '4vw'}}>
|
||||
<div style={{marginTop: '2w'}}>
|
||||
2024 <DownOutline style={{marginLeft: '2vw'}} />
|
||||
</div>
|
||||
<div style={{marginTop: '3vw'}}>
|
||||
<div style={{display: 'flex', alignItems: 'center', marginTop: '3vw'}}>
|
||||
<div style={{flex: 1}}>
|
||||
<div >表号:5461132156</div>
|
||||
<div > 2024-03-07 13:00:00 </div>
|
||||
</div>
|
||||
<div>500</div>
|
||||
</div>
|
||||
<div style={{display: 'flex', alignItems: 'center', marginTop: '3vw'}}>
|
||||
<div style={{flex: 1}}>
|
||||
<div >表号:5461132156</div>
|
||||
<div > 2024-03-07 13:00:00 </div>
|
||||
</div>
|
||||
<div>500</div>
|
||||
</div>
|
||||
<div style={{display: 'flex', alignItems: 'center', marginTop: '3vw'}}>
|
||||
<div style={{flex: 1}}>
|
||||
<div >表号:5461132156</div>
|
||||
<div > 2024-03-07 13:00:00 </div>
|
||||
</div>
|
||||
<div>500</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default RechargeRecords;
|
@ -1,34 +0,0 @@
|
||||
import MeterInfo from "@c/MeterInfo";
|
||||
import { Button, Input, NavBar } from "antd-mobile";
|
||||
import { QuestionCircleOutline } from "antd-mobile-icons";
|
||||
import { FC } from "react";
|
||||
|
||||
const Return: FC = () => {
|
||||
return <div>
|
||||
<NavBar>
|
||||
退费
|
||||
</NavBar>
|
||||
<div style={{padding: '2vw'}}>
|
||||
<div style={{marginBottom: '2vw'}}> 请输入要退费的表号或公司名称 </div>
|
||||
<div style={{display: 'flex'}}>
|
||||
<Input placeholder='请输入要退费的表号或公司名称' clearable style={{flex: 1}} />
|
||||
<Button color='primary' fill='none'>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<MeterInfo />
|
||||
<div style={{marginTop: '4vw'}}>
|
||||
<div> 选择金额 </div>
|
||||
|
||||
<div style={{marginTop: '4vw'}}>
|
||||
<Input placeholder='最多可赎回100元' clearable />
|
||||
</div>
|
||||
<div style={{marginTop: '4vw'}}>
|
||||
预计手续费{2.5}元 <QuestionCircleOutline style={{marginLeft: '2vw'}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Return;
|
34
src/pages/errors/404.tsx
Normal file
34
src/pages/errors/404.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import Warning24Regular from '@ricons/fluent/lib/Warning24Regular';
|
||||
import { FC } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import 'twin.macro';
|
||||
|
||||
/**
|
||||
* 生成404 未找到页面。
|
||||
* @returns 404 未找到页面
|
||||
*/
|
||||
export const NotFoundPage: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div tw="w-full h-full flex flex-col justify-center items-center">
|
||||
<div tw="flex flex-row justify-center items-center mb-8">
|
||||
<div>
|
||||
<Warning24Regular tw="w-[36px] h-[36px] pt-1.5 text-red-5" onPointerEnterCapture={undefined}
|
||||
onPointerLeaveCapture={undefined} />
|
||||
</div>
|
||||
<h1 tw="text-3xl ml-4">请求的内容不存在!</h1>
|
||||
</div>
|
||||
<div tw="mt-2">
|
||||
点击
|
||||
<a
|
||||
onClick={() => navigate(-1)}
|
||||
tw="text-arcoblue-3 hover:text-arcoblue-5 active:text-arcoblue-7 cursor-pointer"
|
||||
>
|
||||
此处
|
||||
</a>
|
||||
返回系统。
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
8
src/pages/test/index.tsx
Normal file
8
src/pages/test/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
function Test() {
|
||||
return "test222222"
|
||||
}
|
||||
|
||||
|
||||
export { Test }
|
168
src/states/area_store.ts
Normal file
168
src/states/area_store.ts
Normal file
@ -0,0 +1,168 @@
|
||||
//@ts-nocheck
|
||||
import { SyncParamAction } from '@/shared/foundation';
|
||||
import { Area } from '@/shared/models';
|
||||
import { OptionProps } from '@arco-design/web-react/es/Select';
|
||||
import { notNil } from '@u/funcs';
|
||||
import {
|
||||
filter,
|
||||
find,
|
||||
includes,
|
||||
isEmpty,
|
||||
isNil,
|
||||
map,
|
||||
min,
|
||||
not,
|
||||
pluck,
|
||||
propEq,
|
||||
reduce,
|
||||
uniq
|
||||
} from 'ramda';
|
||||
import { StateSelector } from 'zustand';
|
||||
import { createStoreHook } from './store_creator';
|
||||
|
||||
/**
|
||||
* 记录应用中公用的行政区划信息的存储的类型结构。
|
||||
*/
|
||||
interface AreaStore {
|
||||
areas: Area[];
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
/**
|
||||
* 向当前应用的行政区划信息存储中加载新的行政区划信息。
|
||||
*/
|
||||
loadAreas: SyncParamAction<Area[]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用中公用的行政区划信息存储的初始状态。
|
||||
*/
|
||||
const initialAreaStore: AreaStore = {
|
||||
areas: []
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用中公用的行政区划信息存储。
|
||||
*/
|
||||
export const useAreaStore = createStoreHook<AreaStore & Actions>(
|
||||
set => ({
|
||||
...initialAreaStore,
|
||||
loadAreas: (areas: Area[]) =>
|
||||
set(state => {
|
||||
state.areas = areas;
|
||||
})
|
||||
}),
|
||||
{ debug: false }
|
||||
);
|
||||
|
||||
/**
|
||||
* 用于从行政区划信息Store中计算选取所有父级行政区划编号的选择器。
|
||||
*/
|
||||
export const allParentCodesSelector: StateSelector<AreaStore, Area[]> = state =>
|
||||
uniq(pluck('parent', state.areas));
|
||||
|
||||
/**
|
||||
* 用于从行政区划信息Store中计算选取所有的顶级行政区划的选择器。
|
||||
*/
|
||||
export const topAreasSelector: StateSelector<AreaStore, Area[]> = state => {
|
||||
const minLevel = reduce(
|
||||
(acc, elem: number) => min(acc, elem),
|
||||
Infinity,
|
||||
pluck<string, Area>('lev', state.areas)
|
||||
);
|
||||
return filter(propEq('lev', minLevel), state.areas);
|
||||
};
|
||||
|
||||
/**
|
||||
* 基于顶级行政区划选择器进一步形成用于下拉框选项的选择器。
|
||||
*/
|
||||
export const topAreaOptionsSelector: StateSelector<AreaStore, OptionProps[]> = state => {
|
||||
const parentCodes = allParentCodesSelector(state);
|
||||
const topAreas = topAreasSelector(state);
|
||||
return map(
|
||||
item => ({
|
||||
label: item.name,
|
||||
value: item.code,
|
||||
isLeaf: not(includes(item.code, parentCodes))
|
||||
}),
|
||||
topAreas
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于从行政区划信息Store中计算选取指定行政区划的子级行政区划的选择器。
|
||||
*/
|
||||
export const subAreasSelector: (parentCode: string) => StateSelector<AreaStore, Area[]> =
|
||||
parentCode => state => {
|
||||
return filter(propEq('parent', parentCode), state.areas);
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于将从行政区划信息Store中选出的子级行政区划转换为下拉框使用的选项集的选择器。
|
||||
*/
|
||||
export const subAreaOptionsSelector: (
|
||||
parentCode: string
|
||||
) => StateSelector<AreaStore, OptionProps[]> = parentCode => state => {
|
||||
const parentCodes = allParentCodesSelector(state);
|
||||
return map(
|
||||
(item: Area) => ({
|
||||
label: item.name,
|
||||
value: item.code,
|
||||
isLeaf: not(includes(item.code, parentCodes))
|
||||
}),
|
||||
filter(propEq('parent', parentCode), state.areas)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于生成具有默认值的行政区划级联列表用选项的选择器。
|
||||
*/
|
||||
export const topAreaOptionsWithDefaultSelector: (
|
||||
codes?: string[]
|
||||
) => StateSelector<AreaStore, OptionProps[]> = codes => state => {
|
||||
const topAreas = topAreaOptionsSelector(state);
|
||||
const parentCodes = allParentCodesSelector(state);
|
||||
const subAreaOptionsWithDefault = (parent: string) =>
|
||||
map<Area, OptionProps>(item => {
|
||||
const option: OptionProps = {
|
||||
label: item.name,
|
||||
value: item.code,
|
||||
isLeaf: not(includes(item.code, parentCodes))
|
||||
};
|
||||
return includes(item.code, parentCodes)
|
||||
? { ...option, children: subAreaOptionsWithDefault(item.code) }
|
||||
: option;
|
||||
}, filter<Area, Area[]>(propEq('parent', parent), state.areas) ?? []);
|
||||
return map(item => {
|
||||
return includes(item.value, codes ?? [])
|
||||
? { ...item, children: subAreaOptionsWithDefault(item.value) }
|
||||
: item;
|
||||
}, topAreas);
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于从行政区划信息Store中计算获取指定的行政区划信息用于级联选择的选择器。
|
||||
*/
|
||||
export const fullCascaderValueSelector: (
|
||||
code: string | null
|
||||
) => StateSelector<AreaStore, string[]> = code => state => {
|
||||
const result: string[] = isEmpty(code) || isNil(code) ? [] : [code];
|
||||
let current = find(propEq('code', code), state.areas);
|
||||
while (notNil(current)) {
|
||||
const parent = find(propEq('code', current.parent), state.areas);
|
||||
if (notNil(parent)) {
|
||||
result.unshift(parent.code);
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 从行政区划信息Store中获取指定行政区划是否被禁用的选择器。
|
||||
*/
|
||||
export const isAreaDisabledSelector: (code: string) => StateSelector<AreaStore, boolean> =
|
||||
code => state => {
|
||||
const current = find(propEq('code', code), state.areas);
|
||||
return current?.disabled ?? false;
|
||||
};
|
67
src/states/breadcrumb_store.ts
Normal file
67
src/states/breadcrumb_store.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { SyncAction, SyncParamAction } from '@/shared/foundation';
|
||||
import { reduceIndexed } from '@u/funcs';
|
||||
import { append, dropLast, equals, length } from 'ramda';
|
||||
import { createStoreHook } from './store_creator';
|
||||
|
||||
export interface BreadcrumbRecord {
|
||||
label: string;
|
||||
link?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于记录面包屑导航的存储。
|
||||
*/
|
||||
interface BreadcrumbStore {
|
||||
/**
|
||||
* 目前记录的各级面包屑。
|
||||
*/
|
||||
breadcrumbs: BreadcrumbRecord[];
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
/**
|
||||
* 向当前面包屑记录中推送一条新的记录。
|
||||
*/
|
||||
push: SyncParamAction<BreadcrumbRecord>;
|
||||
/**
|
||||
* 从当前面包屑记录中弹出最后一条记录。
|
||||
*/
|
||||
pop: SyncAction;
|
||||
/**
|
||||
* 删除当前全部面包屑记录,替换成新的记录。
|
||||
*/
|
||||
replace: SyncParamAction<BreadcrumbRecord>;
|
||||
};
|
||||
|
||||
const initialState: BreadcrumbStore = {
|
||||
breadcrumbs: []
|
||||
};
|
||||
|
||||
export const useBreadcrumbStore = createStoreHook<BreadcrumbStore & Actions>(set => ({
|
||||
...initialState,
|
||||
push: record =>
|
||||
set(st => {
|
||||
st.breadcrumbs = append(record, st.breadcrumbs);
|
||||
}),
|
||||
pop: () =>
|
||||
set(st => {
|
||||
st.breadcrumbs = dropLast(1, st.breadcrumbs);
|
||||
}),
|
||||
replace: record => set({ breadcrumbs: [record] })
|
||||
}));
|
||||
|
||||
/**
|
||||
* 用于生成一套最后一位面包屑不带链接的面包屑集合。
|
||||
*/
|
||||
export const breadcrumbsSelector: (state: BreadcrumbStore) => BreadcrumbRecord[] = state => {
|
||||
return reduceIndexed(
|
||||
(acc, elem, index, list) => {
|
||||
if (equals(index, length(list) - 1)) {
|
||||
return append({ label: elem.label }, acc);
|
||||
}
|
||||
return append(elem, acc);
|
||||
},
|
||||
[],
|
||||
state.breadcrumbs
|
||||
);
|
||||
};
|
139
src/states/layout_store.ts
Normal file
139
src/states/layout_store.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { SyncAction, SyncParamAction } from '@/shared/foundation';
|
||||
import { ReactElement, ReactFragment, ReactPortal } from 'react';
|
||||
import { StateSelector } from 'zustand';
|
||||
import { createStoreHook } from './store_creator';
|
||||
|
||||
export type WorkAreaType = ReactElement | ReactFragment | ReactPortal | HTMLElement;
|
||||
|
||||
/**
|
||||
* 用于记录应用中所有需要暂存的布局组件引用。
|
||||
*/
|
||||
interface LayoutStore {
|
||||
/**
|
||||
* 用于保存整个工作区的ref,即MainLayout中路由接口之外的部分。
|
||||
*/
|
||||
workAreaRef: WeakRef<WorkAreaType> | null;
|
||||
/**
|
||||
* 用于保存整个工作区的高度大小。
|
||||
*/
|
||||
workAreaHeight: number;
|
||||
/**
|
||||
* 页面主面板区域的高度。
|
||||
*/
|
||||
panelHeight: number;
|
||||
/**
|
||||
* 页面工具栏的高度。
|
||||
*/
|
||||
toolbarHeight: number;
|
||||
/**
|
||||
* 是否展示覆盖整个页面的遮罩层。
|
||||
*/
|
||||
maskVisible: boolean;
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
/**
|
||||
* 记录当前工作区组件。
|
||||
*/
|
||||
rememberWorkArea: SyncParamAction<WorkAreaType>;
|
||||
/**
|
||||
* 清除对于工作区组件的记录。
|
||||
*/
|
||||
forgotWorkArea: SyncAction;
|
||||
/**
|
||||
* 记录当前工作区组件的高度。
|
||||
*/
|
||||
memorizeWorkAreaHeight: SyncParamAction<number>;
|
||||
/**
|
||||
* 归零当前工作区组件的高度记录。
|
||||
*/
|
||||
resetWorkAreaHeight: SyncAction;
|
||||
/**
|
||||
* 记录当前工作区中主要工作面板的高度。
|
||||
*/
|
||||
memorizePanelHeight: SyncParamAction<number>;
|
||||
/**
|
||||
* 归零当前工作区中主要工作面板高度的记录。
|
||||
*/
|
||||
resetPanelHeight: SyncAction;
|
||||
/**
|
||||
* 记录当前工作区主要面板中工具栏的高度。
|
||||
*/
|
||||
memorizeToolbarHeight: SyncParamAction<number>;
|
||||
/**
|
||||
* 归零当前工作区主要工作面板中工具栏高度的记录。
|
||||
*/
|
||||
resetToolbarHeight: SyncAction;
|
||||
/**
|
||||
* 显示全局遮罩层。
|
||||
*/
|
||||
showMask: SyncAction;
|
||||
/**
|
||||
* 关闭全局遮罩层的显示。
|
||||
*/
|
||||
hideMask: SyncAction;
|
||||
};
|
||||
|
||||
const initialLayoutState: LayoutStore = {
|
||||
workAreaRef: null,
|
||||
workAreaHeight: 0,
|
||||
panelHeight: 0,
|
||||
toolbarHeight: 0,
|
||||
maskVisible: false
|
||||
};
|
||||
|
||||
export const useLayoutStore = createStoreHook<LayoutStore & Actions>(
|
||||
set => ({
|
||||
...initialLayoutState,
|
||||
rememberWorkArea: (workAreaRef: WorkAreaType) =>
|
||||
set(state => {
|
||||
state.workAreaRef = new WeakRef<WorkAreaType>(workAreaRef);
|
||||
}),
|
||||
forgotWorkArea: () =>
|
||||
set(state => {
|
||||
state.workAreaRef = null;
|
||||
}),
|
||||
memorizeWorkAreaHeight: (height: number) =>
|
||||
set(state => {
|
||||
state.workAreaHeight = height;
|
||||
}),
|
||||
resetWorkAreaHeight: () =>
|
||||
set(state => {
|
||||
state.workAreaHeight = 0;
|
||||
}),
|
||||
memorizePanelHeight: (height: number) =>
|
||||
set(state => {
|
||||
state.panelHeight = height;
|
||||
}),
|
||||
resetPanelHeight: () =>
|
||||
set(state => {
|
||||
state.panelHeight = 0;
|
||||
}),
|
||||
memorizeToolbarHeight: (height: number) =>
|
||||
set(state => {
|
||||
state.toolbarHeight = height;
|
||||
}),
|
||||
resetToolbarHeight: () =>
|
||||
set(state => {
|
||||
state.toolbarHeight = 0;
|
||||
}),
|
||||
showMask: () =>
|
||||
set(state => {
|
||||
state.maskVisible = true;
|
||||
}),
|
||||
hideMask: () =>
|
||||
set(state => {
|
||||
state.maskVisible = false;
|
||||
})
|
||||
}),
|
||||
{ debug: false }
|
||||
);
|
||||
|
||||
/**
|
||||
* 用于计算页面主面板上主表格的最大高度的选择器。
|
||||
*/
|
||||
export const tableHeightSelector: (
|
||||
additionalGaps: number,
|
||||
adjustHeight: number
|
||||
) => StateSelector<LayoutStore, number> = (additionalGaps, adjustHeight) => state =>
|
||||
state.panelHeight - state.toolbarHeight - 8 * 2 - 8 * additionalGaps - adjustHeight;
|
43
src/states/park_store.ts
Normal file
43
src/states/park_store.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { SyncAction, SyncParamAction } from '@/shared/foundation';
|
||||
import { createStoreHook } from './store_creator';
|
||||
import {ParkInfo} from "@/shared/model-park";
|
||||
|
||||
/**
|
||||
* 当前登录的用户信息以及会话信息状态存储类型。
|
||||
* ! 注意大部分用户信息已经转移到LocalStorage中存储,这里只保存一些辅助性的无关用户登录状态的内容。
|
||||
*/
|
||||
|
||||
type Actions = {
|
||||
/**
|
||||
* 登出当前用户,重置整个用户状态记录。
|
||||
*/
|
||||
clear: SyncAction;
|
||||
/**
|
||||
* 处理当前用户登录成功的请求,缓存用户信息。
|
||||
*/
|
||||
setPark: SyncParamAction<ParkInfo>;
|
||||
};
|
||||
|
||||
interface InitState {
|
||||
park?: ParkInfo
|
||||
}
|
||||
|
||||
const initialParkState: InitState = {
|
||||
park: undefined
|
||||
};
|
||||
|
||||
export const useParkStore = createStoreHook<InitState & Actions>(
|
||||
set => ({
|
||||
...initialParkState,
|
||||
clear: () =>
|
||||
set(st => {
|
||||
st.park = null;
|
||||
}),
|
||||
setPark: response =>
|
||||
set(st => {
|
||||
st.park = response
|
||||
}),
|
||||
|
||||
}),
|
||||
{ debug: false }
|
||||
);
|
92
src/states/session_store.ts
Normal file
92
src/states/session_store.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { SyncAction, SyncParamAction } from '@/shared/foundation';
|
||||
import {LoginResponse, logout} from '@q/session';
|
||||
import dayjs from 'dayjs';
|
||||
import { repeat } from 'ramda';
|
||||
import { createStoreHook } from './store_creator';
|
||||
// import {Simulate} from "react-dom/test-utils";
|
||||
// import reset = Simulate.reset;
|
||||
import useAsyncFn from "react-use/lib/useAsyncFn";
|
||||
|
||||
/**
|
||||
* 当前登录的用户信息以及会话信息状态存储类型。
|
||||
* ! 注意大部分用户信息已经转移到LocalStorage中存储,这里只保存一些辅助性的无关用户登录状态的内容。
|
||||
*/
|
||||
interface SessionStore {
|
||||
/**
|
||||
* 用于记录用户当前登录即将过期的提示信息是否已经显示过。
|
||||
* 三个元素的值分别对应10分钟内提示、5分钟内提示和1分钟内提示。
|
||||
* 值为true表示已经显示过,并不再重复显示。
|
||||
*/
|
||||
expireWarning: [boolean, boolean, boolean];
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
/**
|
||||
* 登出当前用户,重置整个用户状态记录。
|
||||
*/
|
||||
logout: SyncAction;
|
||||
/**
|
||||
* 处理当前用户登录成功的请求,缓存用户信息。
|
||||
*/
|
||||
login: SyncParamAction<LoginResponse>;
|
||||
/**
|
||||
* 用于设置哪个阶段的登录会话提示已经显示过了。
|
||||
*/
|
||||
warningDisplayed: SyncParamAction<number>;
|
||||
};
|
||||
|
||||
const initialOpeatorState: SessionStore = {
|
||||
expireWarning: [false, false, false]
|
||||
};
|
||||
|
||||
export const useSessionStore = createStoreHook<SessionStore & Actions>(
|
||||
set => ({
|
||||
...initialOpeatorState,
|
||||
logout: () =>
|
||||
set(st => {
|
||||
localStorage.clear();
|
||||
st.expireWarning = repeat(false, 3) as [boolean, boolean, boolean];
|
||||
}),
|
||||
login: response =>
|
||||
set(st => {
|
||||
localStorage.setItem('uid', response.session?.uid ?? '');
|
||||
localStorage.setItem('token', response.session?.token ?? null);
|
||||
const expiresAt = dayjs(response.session?.expiresAt ?? '2000-01-01 00:00:00').toISOString();
|
||||
localStorage.setItem('type', String(response.session?.type ?? -1));
|
||||
localStorage.setItem('expires', expiresAt);
|
||||
localStorage.setItem('name', response.session?.name ?? 'unknown');
|
||||
localStorage.setItem("menu", JSON.stringify(response?.roles || []))
|
||||
st.expireWarning = repeat(false, 3) as [boolean, boolean, boolean];
|
||||
}),
|
||||
warningDisplayed: index =>
|
||||
set(st => {
|
||||
st.expireWarning[index] = true;
|
||||
})
|
||||
}),
|
||||
{ debug: false }
|
||||
);
|
||||
|
||||
|
||||
type LongTimeNoOperate = {
|
||||
/** 有操作,重置 */
|
||||
reset: SyncAction;
|
||||
/** 无操作,退出 */
|
||||
logout: SyncAction
|
||||
};
|
||||
export const useLongTimeNoOperate = createStoreHook<{timeStamp: number} & LongTimeNoOperate>(
|
||||
set => ({
|
||||
timeStamp: Date.now(),
|
||||
reset: () => {
|
||||
set(st => {
|
||||
st.timeStamp = Date.now()
|
||||
})
|
||||
},
|
||||
logout: async () => {
|
||||
localStorage.clear();
|
||||
set(st => {st.timeStamp = Date.now()})
|
||||
const [state, doLogout] = useAsyncFn(logout);
|
||||
await doLogout()
|
||||
}
|
||||
}),
|
||||
{ debug: false }
|
||||
);
|
56
src/states/store_creator.ts
Normal file
56
src/states/store_creator.ts
Normal file
@ -0,0 +1,56 @@
|
||||
//@ts-nocheck
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { create, State, StateCreator, StoreApi, UseBoundStore } from 'zustand';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
interface EnhancedStoreType<StoreType> {
|
||||
use: {
|
||||
[key in keyof StoreType]: () => StoreType[key];
|
||||
};
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
type CreateStoreHookOptions = {
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 自动为一个Store Hook创建快速访问其中状态和Action的选择器。
|
||||
*/
|
||||
function createSelectors<StoreType extends State>(
|
||||
store: UseBoundStore<StoreApi<StoreType>>,
|
||||
debug?: boolean
|
||||
): UseBoundStore<StoreApi<StoreType>> & EnhancedStoreType<StoreType> {
|
||||
const initialState = store.getState();
|
||||
(store as unknown).use = {};
|
||||
|
||||
Object.keys(store.getState()).forEach(key => {
|
||||
const selector = (state: StoreType) => state[key as keyof StoreType];
|
||||
(store as unknown).use[key] = () => store(selector);
|
||||
});
|
||||
(store as unknown).reset = () => store.setState(initialState, true);
|
||||
|
||||
if (debug ?? false) {
|
||||
store.subscribe((current, previous) => {
|
||||
console.log('[状态调试]Action应用前: ', previous);
|
||||
console.log('[状态调试]Action应用后: ', current);
|
||||
});
|
||||
}
|
||||
|
||||
return store as UseBoundStore<StoreType> & EnhancedStoreType<StoreType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动嵌套使用Devtools和Immer中间件的Zustand创建Store Hook的函数。
|
||||
* 同时将会自动应用创建快速访问状态和Action的选择器。
|
||||
*/
|
||||
export const createStoreHook = <
|
||||
T extends State,
|
||||
Mps extends [StoreMutatorIdentifier, unknown][] = [],
|
||||
Mcs extends [StoreMutatorIdentifier, unknown][] = []
|
||||
>(
|
||||
initializer: StateCreator<T, [...Mps, ['zustand/immer', never]], Mcs>,
|
||||
options?: CreateStoreHookOptions
|
||||
): UseBoundStore<StoreApi<T>> & EnhancedStoreType<T> =>
|
||||
createSelectors(create<T>()(immer(initializer)), options?.debug ?? false);
|
22
src/styles/GlobalStyles.tsx
Normal file
22
src/styles/GlobalStyles.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Global } from '@emotion/react';
|
||||
import { Fragment } from 'react';
|
||||
import tw, { css, GlobalStyles as BaseStyle } from 'twin.macro';
|
||||
|
||||
const globalBaseStyle = css`
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
${tw`m-0 p-0 w-screen h-screen overflow-hidden antialiased bg-gray-2`};
|
||||
}
|
||||
`;
|
||||
|
||||
const GlobalStyles = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<BaseStyle />
|
||||
<Global styles={globalBaseStyle} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalStyles;
|
31
src/types/global.d.ts
vendored
Normal file
31
src/types/global.d.ts
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
import { css as cssImport } from '@emotion/react';
|
||||
import { CSSInterpolation } from '@emotion/serialize';
|
||||
import styledImport from '@emotion/styled';
|
||||
import 'twin.macro';
|
||||
|
||||
declare module 'twin.macro' {
|
||||
// The styled and css imports
|
||||
const styled: typeof styledImport;
|
||||
const css: typeof cssImport;
|
||||
}
|
||||
|
||||
declare module 'react' {
|
||||
// The css prop
|
||||
interface HTMLAttributes<T> extends DOMAttributes<T> {
|
||||
css?: CSSInterpolation;
|
||||
tw?: string;
|
||||
}
|
||||
// The inline svg css prop
|
||||
interface SVGProps<T> extends SVGProps<SVGSVGElement> {
|
||||
css?: CSSInterpolation;
|
||||
tw?: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare interface ImportMetaEnv {
|
||||
readonly VITE_MOCK_HOST: string;
|
||||
}
|
||||
|
||||
declare interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
@ -1,24 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES6",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ES6",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"noImplicitAny": false,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"jsxImportSource": "@emotion/react",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@c/*": [
|
||||
@ -50,6 +52,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"include": [
|
||||
"./src"
|
||||
]
|
||||
}
|
||||
|
115
vite.config.ts
115
vite.config.ts
@ -1,16 +1,33 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import { defaultTo, toUpper } from 'ramda';
|
||||
import { theme } from 'antd/lib';
|
||||
// @ts-ignore
|
||||
import path from 'path';
|
||||
// https://vitejs.dev/config/
|
||||
import {
|
||||
defaultTo,
|
||||
toUpper
|
||||
} from 'ramda';
|
||||
import {
|
||||
defineConfig,
|
||||
loadEnv
|
||||
} from 'vite';
|
||||
|
||||
import { convertLegacyToken } from '@ant-design/compatible/lib';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
const { defaultAlgorithm, defaultSeed } = theme;
|
||||
|
||||
const mapToken = defaultAlgorithm(defaultSeed);
|
||||
const v4Token = convertLegacyToken(mapToken);
|
||||
|
||||
type HostMapping = {
|
||||
Host: string;
|
||||
Path: string;
|
||||
};
|
||||
|
||||
const EnvHostMapping: { [key: string]: HostMapping } = {
|
||||
LOCAL: {
|
||||
Host: 'http://localhost:8000',
|
||||
// Host: 'http://1.92.72.5/api',
|
||||
// Host: 'http://1.92.72.5:8080/api',
|
||||
// Host: 'https://zgd.hbhcbn.com/wxApi',
|
||||
Path: ''
|
||||
},
|
||||
REMOTE: {
|
||||
@ -19,14 +36,21 @@ const EnvHostMapping: { [key: string]: HostMapping } = {
|
||||
},
|
||||
LOCAL_MOCK: {
|
||||
Host: 'http://127.0.0.1:4523',
|
||||
Path: '/m1/1411767-0-default'
|
||||
// Path: '/m1/1411767-0-default'
|
||||
Path: '/m1/4143821-0-default'
|
||||
}
|
||||
};
|
||||
export default (mode: string) => {
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default mode => {
|
||||
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
|
||||
const proxyHost: HostMapping = EnvHostMapping[toUpper(defaultTo('LOCAL')(process.env.PROXY_TARGET)).trim()];
|
||||
return defineConfig({
|
||||
plugins: [react()],
|
||||
esbuild: {
|
||||
define: {
|
||||
this: 'window'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@c': path.resolve(__dirname, 'src', 'components'),
|
||||
@ -41,24 +65,63 @@ export default (mode: string) => {
|
||||
'assets': path.resolve(__dirname, 'src', 'assets')
|
||||
}
|
||||
},
|
||||
envDir: './',
|
||||
server: {
|
||||
port: 8080,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: `${proxyHost.Host}`,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: path => path.replace(/^\/api/, proxyHost.Path)
|
||||
build: {
|
||||
minify: 'esbuild',
|
||||
terserOptions: { compress: true }
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
modifyVars: {
|
||||
...v4Token,
|
||||
'primary-color': '#00B578',
|
||||
'menu-bg': 'transparent',
|
||||
'menu-inline-submenu-bg': 'transparent',
|
||||
'menu-item-color': 'white',
|
||||
'table-header-bg': '#00B578',
|
||||
'table-header-color': 'white'
|
||||
},
|
||||
'/test': {
|
||||
target: `http://127.0.0.1:8081`,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: path => path.replace(/^\/test/, "")
|
||||
}
|
||||
javascriptEnabled: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
plugins: [
|
||||
react({
|
||||
babel: {
|
||||
plugins: [
|
||||
'babel-plugin-macros',
|
||||
[
|
||||
'@emotion/babel-plugin-jsx-pragmatic',
|
||||
{
|
||||
export: 'jsx',
|
||||
import: '__cssprop',
|
||||
module: '@emotion/react'
|
||||
}
|
||||
],
|
||||
['@babel/plugin-transform-react-jsx', { pragma: '__cssprop' }, 'twin.macro']
|
||||
]
|
||||
}
|
||||
})
|
||||
],
|
||||
envDir: './',
|
||||
publicDir: "/h5",
|
||||
base: './',
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: `${proxyHost.Host}`,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: path => path.replace(/^\/api/, proxyHost.Path)
|
||||
},
|
||||
'/test': {
|
||||
target: `http://127.0.0.1:8081`,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: path => path.replace(/^\/test/, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user