初始化
This commit is contained in:
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
/.vs
|
||||
/.vscode
|
||||
/obj
|
||||
12
CHANGELOG.md
Normal file
12
CHANGELOG.md
Normal file
@@ -0,0 +1,12 @@
|
||||
This file explains how Visual Studio created the project.
|
||||
|
||||
The following tools were used to generate this project:
|
||||
- create-vite
|
||||
|
||||
The following steps were used to generate this project:
|
||||
- Create vue project with create-vite: `npm init --yes vue@latest iotweb_vue -- --eslint `.
|
||||
- Updating `vite.config.js` with port.
|
||||
- Create project file (`iotweb_vue.esproj`).
|
||||
- Create `launch.json` to enable debugging.
|
||||
- Add project to solution.
|
||||
- Write this file.
|
||||
27
IoTWeb_Vue.sln
Normal file
27
IoTWeb_Vue.sln
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.13.35806.99 d17.13
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "iotweb_vue", "iotweb_vue.esproj", "{406D5E92-23E3-80CE-F70F-A3DFF817C28A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{406D5E92-23E3-80CE-F70F-A3DFF817C28A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{406D5E92-23E3-80CE-F70F-A3DFF817C28A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{406D5E92-23E3-80CE-F70F-A3DFF817C28A}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
||||
{406D5E92-23E3-80CE-F70F-A3DFF817C28A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{406D5E92-23E3-80CE-F70F-A3DFF817C28A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{406D5E92-23E3-80CE-F70F-A3DFF817C28A}.Release|Any CPU.Deploy.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {A5FBA450-6C1B-4B5A-A519-17597C100259}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
35
README.md
Normal file
35
README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# iotweb_vue
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
24
eslint.config.js
Normal file
24
eslint.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import globals from 'globals'
|
||||
import js from '@eslint/js'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{js,mjs,jsx,vue}'],
|
||||
},
|
||||
|
||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
js.configs.recommended,
|
||||
...pluginVue.configs['flat/essential'],
|
||||
])
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>inHaos IoT</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
11
iotweb_vue.esproj
Normal file
11
iotweb_vue.esproj
Normal file
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.2191419">
|
||||
<PropertyGroup>
|
||||
<StartupCommand>npm run dev</StartupCommand>
|
||||
<JavaScriptTestRoot>.\</JavaScriptTestRoot>
|
||||
<JavaScriptTestFramework>Vitest</JavaScriptTestFramework>
|
||||
<!-- Allows the build (or compile) script located on package.json to run on Build -->
|
||||
<ShouldRunBuildScript>false</ShouldRunBuildScript>
|
||||
<!-- Folder where production build objects will be placed -->
|
||||
<BuildOutputFolder>$(MSBuildProjectDirectory)\dist</BuildOutputFolder>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
13
iotweb_vue.esproj.user
Normal file
13
iotweb_vue.esproj.user
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebuggerFlavor>LaunchJsonDebugger</DebuggerFlavor>
|
||||
<LaunchJsonTarget>{
|
||||
"name": "localhost (Chrome)",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:4137",
|
||||
"webRoot": "E:\\Project\\IoTWeb_Vue"
|
||||
}</LaunchJsonTarget>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
6185
package-lock.json
generated
Normal file
6185
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "iotweb_vue",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"axios": "^1.8.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "^2.9.6",
|
||||
"jquery": "^3.7.1",
|
||||
"mqtt": "^5.13.0",
|
||||
"qs": "^6.14.0",
|
||||
"ssh2-sftp-client": "^12.0.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-simple-verify": "^1.1.0",
|
||||
"vuex": "^4.1.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-vue": "~10.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"unplugin-auto-import": "^19.1.1",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"vite": "^6.2.1",
|
||||
"vite-plugin-vue-devtools": "^7.7.2"
|
||||
}
|
||||
}
|
||||
14
public/config.js
Normal file
14
public/config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const config = {
|
||||
// http访问后端接口
|
||||
Api: "http://iot.uts-data.com:8086/api/",
|
||||
Ads: "http://iot.uts-data.com:8086/",
|
||||
Tcpip: "http://iot-manage.uts-data.com:5001/api/",
|
||||
|
||||
|
||||
// 本地调试接口
|
||||
/* Api: "https://localhost:44363/api/",
|
||||
Ads: "https://localhost:44363/",
|
||||
Tcpip: "http://iot-manage.uts-data.com:5001/api/",*/
|
||||
}
|
||||
|
||||
export default config
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
19
public/logo.svg
Normal file
19
public/logo.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW 2020 (64-Bit) -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="10.8373mm" height="10.8373mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 17.27 17.27"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil0 {fill:#008C8C}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="图层_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<path class="fil0" d="M15.51 13.37c0.46,-0.85 0.72,-1.83 0.72,-2.87 0,-3.34 -2.71,-6.05 -6.05,-6.05 -1.05,0 -2.04,0.27 -2.9,0.74l0 2.5c0.73,-0.75 1.76,-1.22 2.9,-1.22 2.23,0 4.04,1.81 4.04,4.04 0,2.23 -1.81,4.04 -4.04,4.04 -1.11,0 -2.12,-0.45 -2.85,-1.18 -0.73,-0.71 -0.89,-1.79 -0.93,-2.87l0 -6.65 0 -3.25 0 -0.07 0 -0 0 -0.22c0.71,-0.19 1.46,-0.29 2.23,-0.29 4.77,0 8.63,3.87 8.63,8.63 0,4.77 -3.87,8.63 -8.63,8.63 -4.77,0 -8.63,-3.87 -8.63,-8.63 0,-3.18 1.72,-5.95 4.27,-7.45l0 0.25 0 0 0 0.08 0 2.32 0 6.23c0,0.99 0.04,1.49 0.25,2.25 0.48,1.72 1.65,3.02 3.26,3.73 0.74,0.32 1.55,0.5 2.41,0.5 0.64,0 1.27,-0.1 1.85,-0.29 1.41,-0.63 2.61,-1.63 3.48,-2.89z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
37
public/logobig.svg
Normal file
37
public/logobig.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW 2020 (64-Bit) -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="34.0028mm" height="5.4187mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 183.31 29.21"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil1 {fill:#008C8C}
|
||||
.fil0 {fill:#008C8C;fill-rule:nonzero}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="图层_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<g id="_1914568022976">
|
||||
<path class="fil0" d="M112.71 17.12l7.7 -4.25c-0.43,-0.45 -0.89,-0.8 -1.36,-1.06 -0.47,-0.26 -1.22,-0.39 -2.24,-0.39 -1,0 -1.88,0.25 -2.63,0.75 -0.75,0.5 -1.33,1.18 -1.73,2.03 -0.4,0.85 -0.62,1.8 -0.6,2.83 0.02,0.86 0.27,1.69 0.67,2.47 0.4,0.77 0.99,1.42 1.73,1.89 0.75,0.48 1.66,0.73 2.68,0.72 1.56,-0.02 2.74,-0.46 3.51,-1.26 0.77,-0.8 1.32,-2.01 1.64,-3.61l2.84 0.4c-0.02,0.75 -0.2,1.53 -0.55,2.36 -0.35,0.82 -0.87,1.62 -1.56,2.36 -0.69,0.74 -1.56,1.36 -2.58,1.81 -1.02,0.46 -2.19,0.71 -3.49,0.71 -1.36,0 -2.65,-0.35 -3.86,-1.05 -1.21,-0.7 -2.19,-1.66 -2.91,-2.85 -0.72,-1.19 -1.08,-2.48 -1.08,-3.86 0,-1.4 0.24,-2.64 0.71,-3.71 0.47,-1.06 1.11,-1.95 1.89,-2.65 0.78,-0.69 1.65,-1.21 2.58,-1.55 0.93,-0.33 1.85,-0.5 2.75,-0.5 1.77,0 3.25,0.4 4.45,1.2 1.2,0.8 2.24,2.03 3.11,3.7l-10.43 5.79 -1.23 -2.29z"/>
|
||||
<path class="fil0" d="M92.27 9.1l3.09 0 4.53 12.01 4.51 -12.01 3.03 0 -5.41 14.33c-0.17,0.45 -0.48,0.82 -0.88,1.07 -0.4,0.25 -0.84,0.38 -1.31,0.38 -0.49,0 -0.95,-0.14 -1.36,-0.43 -0.41,-0.28 -0.67,-0.7 -0.86,-1.21l-5.35 -14.14z"/>
|
||||
<polygon class="fil0" points="86.8,24.88 86.8,9.12 89.64,9.12 89.64,24.88 "/>
|
||||
<path class="fil0" d="M86.53 5.65c0.01,-0.46 0.21,-0.85 0.54,-1.17 0.33,-0.31 0.72,-0.47 1.16,-0.47 0.45,0 0.84,0.17 1.17,0.49 0.33,0.32 0.5,0.73 0.5,1.22 0,0.52 -0.16,0.94 -0.47,1.24 -0.31,0.3 -0.69,0.46 -1.14,0.46 -0.48,0 -0.89,-0.17 -1.25,-0.49 -0.35,-0.32 -0.54,-0.76 -0.53,-1.28z"/>
|
||||
<polygon class="fil0" points="80.15,2.31 82.98,2.31 82.98,24.88 80.15,24.88 "/>
|
||||
<path class="fil0" d="M63.47 24.88l0 -9.43c0,-1.14 0.29,-2.2 0.87,-3.17 0.59,-0.97 1.39,-1.75 2.42,-2.32 1.02,-0.57 2.15,-0.9 3.37,-0.92 0.87,-0.02 1.83,0.22 2.86,0.67 1.03,0.45 1.91,1.18 2.64,2.18 0.74,1 1.11,2.28 1.11,3.82l0 9.18 -2.83 0 0 -9.11c0,-1.07 -0.35,-2.01 -1.06,-2.8 -0.71,-0.79 -1.61,-1.21 -2.71,-1.23 -1.05,-0.02 -1.94,0.37 -2.67,1.11 -0.73,0.74 -1.09,1.63 -1.09,2.66l0 9.37 -2.9 0z"/>
|
||||
<path class="fil1" d="M52.44 8.9c4.41,0 7.99,3.58 7.99,7.99 0,4.41 -3.58,7.99 -7.99,7.99 -2.8,0 -5.26,-1.44 -6.69,-3.61 -1.43,2.18 -3.89,3.61 -6.69,3.61 -4.41,0 -7.99,-3.58 -7.99,-7.99 0,-4.41 3.58,-7.99 7.99,-7.99 2.8,0 5.26,1.44 6.69,3.61 1.43,-2.18 3.89,-3.61 6.69,-3.61zm0 2.77c2.89,0 5.22,2.34 5.22,5.22 0,2.89 -2.34,5.22 -5.22,5.22 -2.89,0 -5.22,-2.34 -5.22,-5.22 0,-2.89 2.34,-5.22 5.22,-5.22zm-13.37 0c2.89,0 5.22,2.34 5.22,5.22 0,2.89 -2.34,5.22 -5.22,5.22 -2.89,0 -5.22,-2.34 -5.22,-5.22 0,-2.89 2.34,-5.22 5.22,-5.22z"/>
|
||||
<g>
|
||||
<path class="fil1" d="M140.49 19.22l1.82 0c0.16,0 0.31,0.13 0.34,0.29l0.49 2.63c0.03,0.16 -0.07,0.29 -0.23,0.29l-1.82 0c-0.16,0 -0.31,-0.13 -0.34,-0.29l-0.5 -2.63c-0.03,-0.16 0.08,-0.29 0.23,-0.29z"/>
|
||||
<path class="fil1" d="M129.14 9.06l6.05 0 0 -1.3c0,-0.16 0.13,-0.29 0.29,-0.29l2.55 0c0.16,0 0.29,0.13 0.29,0.29l0 1.3 6.05 0c0.16,0 0.29,0.13 0.29,0.29l0 2.61 -2.79 0 0 -1.12 -10.22 0 0 1.12 -2.79 0 0 -2.61c0,-0.16 0.13,-0.29 0.29,-0.29z"/>
|
||||
<path class="fil1" d="M128.71 23.12l6.48 0 0 -4.68 -6.44 0 0 -1.47c0,-0.16 0.13,-0.29 0.29,-0.29l6.15 0 0 -2.16 -6.44 0 0 -1.47c0,-0.16 0.13,-0.29 0.29,-0.29l15.43 0c0.16,0 0.29,0.13 0.29,0.29l0 1.47 -6.44 0 0 2.16 6.15 0c0.16,0 0.29,0.13 0.29,0.29l0 1.47 -6.44 0 0 4.68 6.48 0c0.16,0 0.29,0.13 0.29,0.29l0 1.47 -16.66 0 0 -1.47c0,-0.16 0.13,-0.29 0.29,-0.29z"/>
|
||||
<path class="fil1" d="M148.4 9.02l5.99 0 0 -1.25c0,-0.16 0.13,-0.29 0.29,-0.29l2.55 0c0.16,0 0.29,0.13 0.29,0.29l0 1.25 5.99 0c0.16,0 0.29,0.13 0.29,0.29l0 1.61 -6.27 0 0 4.6 6.36 0c0.19,0 0.34,0.15 0.34,0.34l0 1.89 -2.49 0 2.49 7.11 -2.86 0 -2.49 -7.11 -1.35 0 0 7.03 -3.12 0 0 -7.03 -1.35 0 -2.49 7.11 -2.85 0 2.49 -7.11 -2.49 0 0 -1.89c0,-0.19 0.15,-0.34 0.34,-0.34l6.36 0 0 -4.6 -6.27 0 0 -1.61c0,-0.16 0.13,-0.29 0.29,-0.29zm13.68 2.38l-2.28 0c-0.16,0 -0.33,0.13 -0.38,0.29l-1.09 3.31c-0.05,0.16 0.04,0.29 0.19,0.29l2.28 0c0.16,0 0.33,-0.13 0.38,-0.29l1.09 -3.31c0.05,-0.16 -0.03,-0.29 -0.19,-0.29zm-12.45 0l2.28 0c0.16,0 0.33,0.13 0.38,0.29l1.09 3.31c0.05,0.16 -0.04,0.29 -0.19,0.29l-2.28 0c-0.16,0 -0.33,-0.13 -0.38,-0.29l-1.09 -3.31c-0.05,-0.16 0.03,-0.29 0.19,-0.29z"/>
|
||||
<path class="fil1" d="M170.46 12.11l6.72 0c0.16,0 0.29,0.13 0.29,0.29l0 0.84c0,0.16 -0.13,0.29 -0.29,0.29l-6.81 0 -0.72 11.34 -2.88 0 0.98 -15.56c0.01,-0.16 0.15,-0.29 0.31,-0.29l0.1 0 2.49 0 6.75 0 -0.25 -1.25c-0.03,-0.16 0.07,-0.29 0.23,-0.29l2.13 0c0.16,0 0.31,0.13 0.35,0.29l0.25 1.25 2.76 0c0.16,0 0.29,0.13 0.29,0.29l0 1.61 -2.67 0 0.45 2.22 0.24 -0.67c0.06,-0.16 0.23,-0.29 0.39,-0.29l1.61 0 -1.54 4.4 1.67 8.29 -2.41 0c-0.16,0 -0.31,-0.13 -0.35,-0.29l-0.63 -3.11 -1.09 3.11c-0.06,0.16 -0.23,0.29 -0.39,0.29l-1.61 0 2.39 -6.83 -1.44 -7.12 -7.25 0 -0.08 1.19zm10.53 -4.67l1.54 0c0.16,0 0.29,0.13 0.29,0.29l0 0.68c0,0.16 -0.13,0.29 -0.29,0.29l-1.54 0c-0.16,0 -0.29,-0.13 -0.29,-0.29l0 -0.68c0,-0.16 0.13,-0.29 0.29,-0.29z"/>
|
||||
<path class="fil1" d="M172.98 19.71l0.99 0 1.09 -2.68 -1.11 0 -0.98 2.68zm2.64 -4.05l0 -0 1.96 0 -0 0 0.01 0 -0.49 1.36 -0.02 0c-0.34,0.98 -0.55,1.65 -1.06,3.03l-1.23 -0.06 -0 0.01 2.06 4.86 -2.44 0 -0.84 -1.99 -0.84 1.99 -2.44 0 2.06 -4.86 -0.01 -0.02 -1.17 0c-0.16,0 -0.24,-0.13 -0.18,-0.29l0.97 -2.67 -0.72 0c-0.16,0 -0.29,-0.13 -0.29,-0.29l0 -0.79c0,-0.16 0.13,-0.29 0.29,-0.29l1.21 0 0.44 -1.2c0.06,-0.16 0.23,-0.29 0.39,-0.29l1.42 0c0.16,0 0.24,0.13 0.18,0.29l-0.44 1.2 1.17 0z"/>
|
||||
</g>
|
||||
<path class="fil1" d="M26.23 22.62c0.78,-1.45 1.22,-3.1 1.22,-4.86 0,-5.65 -4.58,-10.23 -10.23,-10.23 -1.78,0 -3.44,0.45 -4.9,1.25l0 4.22c1.24,-1.28 2.98,-2.07 4.9,-2.07 3.77,0 6.83,3.06 6.83,6.83 0,3.77 -3.06,6.83 -6.83,6.83 -1.88,0 -3.58,-0.76 -4.82,-1.99 -1.23,-1.21 -1.51,-3.04 -1.57,-4.86l0 -11.24 0 -5.5 0 -0.12 0 -0 0 -0.38c1.2,-0.32 2.47,-0.49 3.78,-0.49 8.07,0 14.61,6.54 14.61,14.61 0,8.07 -6.54,14.61 -14.61,14.61 -8.07,0 -14.61,-6.54 -14.61,-14.61 0,-5.37 2.9,-10.07 7.22,-12.61l0 0.42 0 0 0 0.14 0 3.93 0 10.54c0,1.68 0.06,2.52 0.42,3.8 0.81,2.9 2.79,5.1 5.51,6.31 1.25,0.54 2.62,0.84 4.07,0.84 1.09,0 2.14,-0.17 3.13,-0.49 2.38,-1.06 4.42,-2.76 5.88,-4.89z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
671
src/App.vue
Normal file
671
src/App.vue
Normal file
@@ -0,0 +1,671 @@
|
||||
<template>
|
||||
<!-- 移除多余的包裹层 -->
|
||||
<div class="main-container">
|
||||
<!-- 登录后的布局 -->
|
||||
<template v-if="alreadyLogin">
|
||||
<!-- 侧边菜单 -->
|
||||
<div class="header-container">
|
||||
<el-menu ref="parentRef"
|
||||
mode="horizontal"
|
||||
:default-active="currentTitle.routes"
|
||||
@select="menuChange"
|
||||
ellipsis="true"
|
||||
class="horizontal-menu">
|
||||
|
||||
<!-- 菜单项 -->
|
||||
<template v-for="menu in filteredMenu" :key="menu.value">
|
||||
<el-sub-menu :index="menu.value"
|
||||
v-if="menu.options.some(item => hasPermission(item.label))">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<component :is="menu.icon" />
|
||||
</el-icon>
|
||||
<span>{{ menu.label }}</span>
|
||||
</template>
|
||||
<el-menu-item v-for="item in menu.options"
|
||||
:key="item.value"
|
||||
:index="item.value" >
|
||||
<el-icon>
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
{{ item.label }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
|
||||
</el-menu>
|
||||
<!-- 右侧功能区 -->
|
||||
<div class="right-functions" style="margin-left: auto; display: flex; align-items: center; padding-right: 20px;">
|
||||
<!-- 刷新 -->
|
||||
<el-button link size="large" style="font-size:22px; margin-right:15px"
|
||||
@click="refreshToHome"
|
||||
icon="Refresh" />
|
||||
|
||||
<!-- 主题切换 -->
|
||||
<el-button link size="large" style="font-size:22px; margin-right:15px"
|
||||
@click="toggleTheme"
|
||||
:icon="isDarkMode ? 'Moon' : 'Sunny'" />
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<el-popover v-model:visible="showPopover"
|
||||
placement="bottom-end"
|
||||
trigger="click">
|
||||
<template #reference>
|
||||
<!--<el-avatar class="user-avatar"
|
||||
:src="userAvatar" />-->
|
||||
<el-avatar><span style="font-size:16px">{{ userAvatar }}</span></el-avatar>
|
||||
</template>
|
||||
<div class="user-menu">
|
||||
<!--<el-button link @click="handleAction('profile')">个人中心</el-button>
|
||||
<el-divider />-->
|
||||
<el-button link @click="handleAction('logout')">退出登录</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content">
|
||||
<div class="router-container">
|
||||
<keep-alive :include="cachedViews">
|
||||
<router-view v-slot="{ Component }">
|
||||
<component :is="Component"
|
||||
:key="route.fullPath"
|
||||
class="router-view-container" />
|
||||
</router-view>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 未登录时的布局 -->
|
||||
<template v-else>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<!-- 底部 -->
|
||||
<!--<el-footer class="global-footer">Copyright © 2025 INHAOS IoT All Rights Reserved.</el-footer>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { provide, ref, onMounted, watchEffect, computed, reactive, inject, watch, onBeforeMount } from 'vue'
|
||||
import Vuex from 'vuex';
|
||||
import { ElMessage } from 'element-plus'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import en from 'element-plus/dist/locale/en.mjs'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import $ from 'jquery';
|
||||
import mqtt from 'mqtt'
|
||||
|
||||
const language = ref('zh-cn')
|
||||
const locale = computed(() => (language.value === 'zh-cn' ? zhCn : en))
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const toggle = () => {
|
||||
language.value = language.value === 'zh-cn' ? 'en' : 'zh-cn'
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
webToDark(true);
|
||||
} else {
|
||||
webToDark(false);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
const permissions = ref([])
|
||||
|
||||
// 权限检查方法
|
||||
const hasPermission = (label) => {
|
||||
return permissions.value.includes('全选') || permissions.value.includes(label)
|
||||
}
|
||||
|
||||
const checkLoginStatus = () => {
|
||||
alreadyLogin.value = localStorage.getItem('login') == 'true'
|
||||
userAvatar.value = getByCharWidth(localStorage.getItem('username'))
|
||||
userName.value = localStorage.getItem('username')
|
||||
const permissionString = localStorage.getItem('permission') || '[]'
|
||||
if (permissionString == "/全选") {
|
||||
permissions.value = ["全选"]
|
||||
} else if (permissionString) {
|
||||
permissions.value = permissionString.split('/').filter(s => s).map(s => `${s}`);
|
||||
} else {
|
||||
permissions.value = []
|
||||
}
|
||||
}
|
||||
provide('checkLoginStatus', checkLoginStatus)
|
||||
|
||||
const userAvatar = ref("")
|
||||
const userName = ref("")
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
localStorage.setItem('login', true)
|
||||
helloworld()
|
||||
// 初始检查登录状态
|
||||
checkLoginStatus()
|
||||
/*userAvatar.value = config.Ads + localStorage.getItem('avatar')*/
|
||||
/*userAvatar.value = getByCharWidth(localStorage.getItem('username'))
|
||||
userName.value = localStorage.getItem('username')*/
|
||||
// 主题初始化
|
||||
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handleColorSchemeChange = (e) => {
|
||||
webToDark(e.matches)
|
||||
}
|
||||
|
||||
// 初始设置
|
||||
webToDark(darkModeQuery.matches)
|
||||
|
||||
// 监听系统主题变化
|
||||
darkModeQuery.addEventListener('change', handleColorSchemeChange)
|
||||
|
||||
// 清理监听器
|
||||
return () => {
|
||||
darkModeQuery.removeEventListener('change', handleColorSchemeChange)
|
||||
}
|
||||
|
||||
findLabelByValue();
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
const helloworld = () => {
|
||||
try {
|
||||
if (getUrl() == "/login") {
|
||||
return
|
||||
}
|
||||
const apiload = 'Login/Helloooo?key=UserKey' + localStorage.getItem('username')
|
||||
const rs = $http.post(apiload).then(rs => {
|
||||
if (rs.data != 'allow') {
|
||||
refreshToHome()
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
const getUrl = () => {
|
||||
const url = window.location.href;
|
||||
// 找到最后一个 '/' 的位置
|
||||
const lastSlashIndex = url.lastIndexOf('/');
|
||||
// 获取最后一个 '/' 后面的内容
|
||||
return url.substring(lastSlashIndex);
|
||||
}
|
||||
|
||||
const alreadyLogin = ref(false)
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const showSidebar = ref(false)
|
||||
|
||||
const $http = inject('$http')
|
||||
const config = inject('config')
|
||||
const username = localStorage.getItem('username')
|
||||
|
||||
|
||||
const product = reactive({
|
||||
value: '',
|
||||
options: [],
|
||||
})
|
||||
|
||||
const currentTitle = reactive({
|
||||
label: '',
|
||||
routes: localStorage.getItem('url'),
|
||||
}) // 当前页面
|
||||
|
||||
const isCollapse = ref(false)
|
||||
const showPopover = ref(false)
|
||||
const noticeValue = ref(999)
|
||||
// 添加计算属性
|
||||
const filteredMenu = computed(() => {
|
||||
return menuValue.filter(menu => {
|
||||
if (!menu?.options) return false
|
||||
return menu.options.some(item =>
|
||||
item?.label && hasPermission(item.label) // 改为检查label
|
||||
)
|
||||
}).map(menu => ({
|
||||
...menu,
|
||||
options: menu.options.filter(item =>
|
||||
item?.label && hasPermission(item.label) // 改为检查label
|
||||
)
|
||||
}))
|
||||
})
|
||||
// 获取所有有权限的路由路径
|
||||
const allPermissionRoutes = computed(() => {
|
||||
let routes = [];
|
||||
filteredMenu.value.forEach(menu => {
|
||||
menu.options.forEach(option => {
|
||||
if (hasPermission(option.label)) {
|
||||
routes.push(option.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
return routes;
|
||||
});
|
||||
const menuValue = reactive([
|
||||
{
|
||||
label: "仪表盘",
|
||||
value: "dashboard",
|
||||
icon: "Menu",
|
||||
options: [
|
||||
{ label: "主页", value: "/home", icon: "House" },
|
||||
{ label: "日志", value: "/log", icon: "Clock" }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "系统操作",
|
||||
value: "system",
|
||||
icon: "Tools",
|
||||
options: [
|
||||
{ label: "用户管理", value: "/usermanage", icon: "User" },
|
||||
{ label: "机型管理", value: "/modelmanage", icon: "Grid" },
|
||||
{ label: "字典管理", value: "/dicmanage", icon: "Notebook" },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "设备管理",
|
||||
value: "quote",
|
||||
icon: "HelpFilled",
|
||||
options: [
|
||||
{ label: "设备注册", value: "/iotregister", icon: "Key" },
|
||||
{ label: "TCPIP测试", value: "/tcpiptest", icon: "Sort" },
|
||||
{ label: "设备记录", value: "/recorded", icon: "Connection" },
|
||||
]
|
||||
},
|
||||
]);
|
||||
|
||||
// 左侧返回首页按钮点击处理
|
||||
const handleLeftClick = (type) => {
|
||||
// type 可能的值: 'home' 按钮
|
||||
switch (type) {
|
||||
case 'home':
|
||||
menuChange("/home")
|
||||
break;
|
||||
default:
|
||||
console.log('跳转错误!');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单操作处理
|
||||
const handleAction = (action) => {
|
||||
// action 可能的值: 'profile' 个人中心 / 'logout' 退出登录
|
||||
showPopover.value = false
|
||||
switch (action) {
|
||||
case 'profile':
|
||||
//disDbDrop.value = true
|
||||
router.push('/user')
|
||||
break;
|
||||
case 'logout':
|
||||
localStorage.setItem('login', false)
|
||||
localStorage.removeItem('lastLoginTimestamp')
|
||||
router.push('/login')
|
||||
break;
|
||||
default:
|
||||
console.log('跳转错误!');
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
const refreshToHome = () => {
|
||||
$http.post('ConfigPY/RefreshConfig')
|
||||
router.push('/home')
|
||||
setTimeout(() => {
|
||||
location.reload(true);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
const parentRef = ref(null)
|
||||
// 菜单处理
|
||||
const menuChange = (val) => {
|
||||
if (currentTitle.routes == val) {
|
||||
return
|
||||
}
|
||||
currentTitle.label = val;
|
||||
currentTitle.routes = val;
|
||||
localStorage.setItem('url', val)
|
||||
router.push(val)
|
||||
}
|
||||
const collapseClick = () => {
|
||||
isCollapse.value = !isCollapse.value
|
||||
document.documentElement.clientWidth // 触发回流
|
||||
}
|
||||
|
||||
// 控制深色模式
|
||||
const isDarkMode = ref(false)
|
||||
const webToDark = (dark) => {
|
||||
// 获取 <html> 元素的类名
|
||||
let ishtmldark = null;
|
||||
if (document.documentElement.className.indexOf('dark') !== -1) {
|
||||
ishtmldark = false
|
||||
} else {
|
||||
ishtmldark = true
|
||||
}
|
||||
if (dark) {
|
||||
document.documentElement.setAttribute('theme-mode', 'dark')
|
||||
isDarkMode.value = true
|
||||
if (ishtmldark) {
|
||||
toggleDark()
|
||||
}
|
||||
} else {
|
||||
document.documentElement.removeAttribute('theme-mode')
|
||||
isDarkMode.value = false
|
||||
if (!ishtmldark) {
|
||||
toggleDark()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
const toggleTheme = () => {
|
||||
isDarkMode.value = !isDarkMode.value
|
||||
let root = document.documentElement;
|
||||
toggleDark()
|
||||
if (!isDarkMode.value) {
|
||||
// 如果当前是暗色主题,切换到亮色主题
|
||||
root.style.setProperty('--color-background', '#ffffff');
|
||||
root.style.setProperty('--color-background-soft', '#f8f8f8');
|
||||
root.style.setProperty('--color-background-mute', '#f2f2f2');
|
||||
root.style.setProperty('--color-border', 'rgba(60, 60, 60, 0.12)');
|
||||
root.style.setProperty('--color-border-hover', 'rgba(60, 60, 60, 0.29)');
|
||||
root.style.setProperty('--color-heading', '#2c3e50');
|
||||
root.style.setProperty('--color-text', '#2c3e50');
|
||||
root.setAttribute('data-theme', 'light');
|
||||
setTimeout(() => {
|
||||
root.removeAttribute('theme-mode');
|
||||
}, 20);
|
||||
} else {
|
||||
// 如果当前是亮色主题,切换到暗色主题
|
||||
root.style.setProperty('--color-background', '#242424');
|
||||
root.style.setProperty('--color-background-soft', '#222222');
|
||||
root.style.setProperty('--color-background-mute', '#282828');
|
||||
root.style.setProperty('--color-border', 'rgba(84, 84, 84, 0.48)');
|
||||
root.style.setProperty('--color-border-hover', 'rgba(84, 84, 84, 0.65)');
|
||||
root.style.setProperty('--color-heading', '#ffffff');
|
||||
root.style.setProperty('--color-text', 'rgba(235, 235, 235, 0.64)');
|
||||
root.setAttribute('data-theme', 'dark');
|
||||
setTimeout(() => {
|
||||
root.setAttribute('theme-mode', 'dark');
|
||||
}, 20);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 确定当前地址
|
||||
const findLabelByValue = () => {
|
||||
const url = window.location.href;
|
||||
// 找到最后一个 '/' 的位置
|
||||
const lastSlashIndex = url.lastIndexOf('/');
|
||||
// 获取最后一个 '/' 后面的内容
|
||||
return url.substring(lastSlashIndex);
|
||||
//const lastSegment = url.substring(lastSlashIndex);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 已打开的标签页
|
||||
const openedTabs = reactive([])
|
||||
// 需要缓存的组件名称
|
||||
const cachedViews = ref(new Set())
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeTab = computed({
|
||||
get: () => route.path,
|
||||
set: (val) => router.push(val)
|
||||
})
|
||||
|
||||
// 监听路由变化
|
||||
watch(() => route.path, (newPath) => {
|
||||
if (newPath === '/login') return
|
||||
|
||||
// 新增:同步更新当前激活菜单
|
||||
currentTitle.routes = newPath
|
||||
localStorage.setItem('url', newPath)
|
||||
|
||||
const existingTab = openedTabs.find(tab => tab.path == newPath)
|
||||
const routeName = route.matched[0]?.name
|
||||
|
||||
if (routeName) {
|
||||
if (!existingTab) {
|
||||
cachedViews.value.add(routeName)
|
||||
openedTabs.push({
|
||||
path: newPath,
|
||||
label: routeName || '未命名页面',
|
||||
name: routeName
|
||||
})
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 标签点击处理
|
||||
const handleTabClick = (tab) => {
|
||||
router.push(tab.paneName)
|
||||
}
|
||||
|
||||
const getByCharWidth = (str, maxWidth = 2) => {
|
||||
// 非空判断
|
||||
if (typeof str !== 'string' || !str) return ''
|
||||
|
||||
let width = 0;
|
||||
let result = '';
|
||||
|
||||
for (const char of str) {
|
||||
// 先转换字符为大写
|
||||
const upperChar = char.toUpperCase();
|
||||
// 判断字符宽度(基于原始字符)
|
||||
const isWideChar = /[\u4E00-\u9FA5\u3000-\u303F\uFF00-\uFFEF]/.test(char);
|
||||
const charWidth = isWideChar ? 2 : 1;
|
||||
|
||||
// 超出宽度限制则停止
|
||||
if (width + charWidth > maxWidth) break;
|
||||
|
||||
// 拼接处理后的字符
|
||||
result += upperChar;
|
||||
width += charWidth;
|
||||
|
||||
// 达到最大宽度则停止
|
||||
if (width >= maxWidth) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 标签关闭处理
|
||||
const handleTabRemove = (targetPath) => {
|
||||
//console.log(targetPath)
|
||||
const index = openedTabs.findIndex(tab => tab.path === targetPath)
|
||||
if (index != -1 && openedTabs.length > 1) {
|
||||
const removedTab = openedTabs.splice(index, 1)[0]
|
||||
|
||||
// 清理缓存
|
||||
if (removedTab.name) {
|
||||
cachedViews.value.delete(removedTab.name)
|
||||
}
|
||||
|
||||
// 如果关闭的是当前页面则跳转
|
||||
if (targetPath === route.path) {
|
||||
const lastTab = openedTabs[openedTabs.length - 1]
|
||||
menuChange(lastTab ? lastTab.path : '/home')
|
||||
}
|
||||
} else if (openedTabs.length == 1) {
|
||||
menuChange("/home")
|
||||
}
|
||||
}
|
||||
|
||||
const ajaxfile = async (form) => {
|
||||
// 参数验证
|
||||
if (!form.File || !form.File instanceof File) {
|
||||
throw new Error('文件对象无效!');
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建 FormData 对象并 append 文件
|
||||
const formData = new FormData();
|
||||
formData.append('File', form.File);
|
||||
formData.append('Folder', form.Folder);
|
||||
|
||||
// 设置请求配置
|
||||
const settings = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Cookie": "isCN=zh-cn"
|
||||
},
|
||||
body: formData
|
||||
};
|
||||
const token = localStorage.getItem('token');
|
||||
settings.headers['Authorization'] = `Bearer ${token}`;
|
||||
// 发送请求
|
||||
const response = await fetch(config.Api + 'FileUpload/UploadFile', settings);
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('error:', error);
|
||||
}
|
||||
};
|
||||
const ajax = async (api, form) => {
|
||||
try {
|
||||
const response = await $.ajax({
|
||||
url: api,
|
||||
type: "POST",
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(form)
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const mqttmsg = ref('')
|
||||
const client = mqtt.connect("ws://mqtt.uts-data.com:9002/mqtt", {
|
||||
clientId: 'mqttjs_' + Math.random().toString(16).substr(2, 8),
|
||||
username: "mqttwebclient",
|
||||
password: "2WnA99779Heg",
|
||||
protocolVersion: 5
|
||||
}).on("connect", (err) => { console.log(err); })
|
||||
.on("message", (topic, message) => {
|
||||
mqttmsg.value = message;
|
||||
console.log(mqttmsg.value);
|
||||
});
|
||||
|
||||
const mqttlink = (linkipNew, linkipOld) => {
|
||||
client.unsubscribe("emqtt/tcp/" + linkipOld, (err) => { });
|
||||
client.unsubscribe("emqtt/tcp/" + linkipNew, (err) => { });
|
||||
client.subscribe("emqtt/tcp/" + linkipNew, (err) => { });
|
||||
}
|
||||
|
||||
|
||||
provide('menuValue', menuValue)
|
||||
provide('mqttmsg', mqttmsg)
|
||||
provide('client', client)
|
||||
provide('mqttlink', mqttlink)
|
||||
provide('ajaxfile', ajaxfile)
|
||||
provide('ajax', ajax)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 新增header容器 */
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
padding: 0 10px;
|
||||
background: var(--el-bg-color);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
/* 调整水平菜单样式 */
|
||||
.horizontal-menu {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
border-bottom: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 菜单项对齐调整 */
|
||||
:deep(.el-menu--horizontal) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 右侧功能区新样式 */
|
||||
.right-functions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-left: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 按钮图标调整 */
|
||||
.right-functions .el-button {
|
||||
font-size: 24px;
|
||||
padding: 0 3px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.right-functions .el-button:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 用户头像 */
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.user-avatar:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.header-container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.right-functions {
|
||||
gap: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-menu--horizontal > .el-sub-menu) {
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 保持原有主内容区样式 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.router-container {
|
||||
height: calc(100vh - 64px);
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
86
src/assets/base.css
Normal file
86
src/assets/base.css
Normal file
@@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
37
src/assets/main.css
Normal file
37
src/assets/main.css
Normal file
@@ -0,0 +1,37 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
/*max-width: 1440px;*/
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
/*grid-template-columns: 1fr 1fr;*/
|
||||
padding: 0 ;
|
||||
}
|
||||
}
|
||||
105
src/axios.js
Normal file
105
src/axios.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import axios from 'axios';
|
||||
import config from '../public/config.js';
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: config.Api, // 设置基础URL
|
||||
timeout: 150000
|
||||
});
|
||||
|
||||
// 添加请求拦截器
|
||||
instance.interceptors.request.use(config => {
|
||||
// 设置默认的 Content-Type
|
||||
if (config.headers['Content-Type'] == undefined) {
|
||||
config.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
// 如果发送的数据是对象,则使用JSON库进行序列化
|
||||
if (config.data && typeof config.data === 'object') {
|
||||
config.data = JSON.stringify(config.data);
|
||||
}
|
||||
|
||||
// 从本地存储获取token
|
||||
const url = window.location.href;
|
||||
// 找到最后一个 '/' 的位置
|
||||
const lastSlashIndex = url.lastIndexOf('/');
|
||||
// 获取最后一个 '/' 后面的内容
|
||||
const lastSegment = url.substring(lastSlashIndex);
|
||||
// 如果是登录接口,则不添加token
|
||||
if (config.url !== 'Login/Login') {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
} else {
|
||||
handleTokenExpiration()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return config;
|
||||
}, error => {
|
||||
// 请求错误处理
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// 添加响应拦截器
|
||||
instance.interceptors.response.use(response => {
|
||||
// 对响应数据处理
|
||||
if (response.config.url !== 'Login/Login' && response.status === 200) {
|
||||
refreshToken();
|
||||
}
|
||||
return response;
|
||||
}, error => {
|
||||
// 对响应错误处理
|
||||
// 可以在这里添加例如token过期后的处理逻辑
|
||||
console.log(error)
|
||||
if (error.response && error.response.status == 401) {
|
||||
// token过期,跳转到登录页面
|
||||
handleTokenExpiration()
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
export default instance;
|
||||
|
||||
|
||||
// Token刷新方法
|
||||
async function refreshToken() {
|
||||
try {
|
||||
const username = localStorage.getItem('rememberedUsername')
|
||||
const password = localStorage.getItem('rememberedPassword')
|
||||
|
||||
if (!username || !password) {
|
||||
throw new Error('登录过期 请重新登录')
|
||||
}
|
||||
|
||||
const formdata = {
|
||||
username: localStorage.getItem('rememberedUsername'),
|
||||
password: localStorage.getItem('rememberedPassword')
|
||||
}
|
||||
instance.post('Login/Login', formdata)
|
||||
.then(response => {
|
||||
if (response.data.isok) {
|
||||
localStorage.setItem('token', response.data.response.accessToken);
|
||||
} else {
|
||||
throw new Error('token更新失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Token过期处理
|
||||
function handleTokenExpiration() {
|
||||
ElMessage.error('token失效 请重新登录')
|
||||
setTimeout(() => {
|
||||
localStorage.removeItem('lastLoginTimestamp')
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
}, 233);
|
||||
}
|
||||
44
src/components/HelloWorld.vue
Normal file
44
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
msg: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve successfully created a project with
|
||||
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.6rem;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
94
src/components/TheWelcome.vue
Normal file
94
src/components/TheWelcome.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup>
|
||||
import WelcomeItem from './WelcomeItem.vue'
|
||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||
import ToolingIcon from './icons/IconTooling.vue'
|
||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||
import CommunityIcon from './icons/IconCommunity.vue'
|
||||
import SupportIcon from './icons/IconSupport.vue'
|
||||
|
||||
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<DocumentationIcon />
|
||||
</template>
|
||||
<template #heading>Documentation</template>
|
||||
|
||||
Vue’s
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||
provides you with all information you need to get started.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<ToolingIcon />
|
||||
</template>
|
||||
<template #heading>Tooling</template>
|
||||
|
||||
This project is served and bundled with
|
||||
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||
recommended IDE setup is
|
||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||
+
|
||||
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
|
||||
you need to test your components and web pages, check out
|
||||
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
||||
and
|
||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||
/
|
||||
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||
|
||||
<br />
|
||||
|
||||
More instructions are available in
|
||||
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||
>.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<EcosystemIcon />
|
||||
</template>
|
||||
<template #heading>Ecosystem</template>
|
||||
|
||||
Get official tools and libraries for your project:
|
||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||
you need more resources, we suggest paying
|
||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||
a visit.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<CommunityIcon />
|
||||
</template>
|
||||
<template #heading>Community</template>
|
||||
|
||||
Got stuck? Ask your question on
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||
(our official Discord server), or
|
||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||
>StackOverflow</a
|
||||
>. You should also follow the official
|
||||
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||
Bluesky account or the
|
||||
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||
X account for latest news in the Vue world.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<SupportIcon />
|
||||
</template>
|
||||
<template #heading>Support Vue</template>
|
||||
|
||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||
us by
|
||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||
</WelcomeItem>
|
||||
</template>
|
||||
87
src/components/WelcomeItem.vue
Normal file
87
src/components/WelcomeItem.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
26
src/components/i18n.js
Normal file
26
src/components/i18n.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
import en from 'element-plus/es/locale/lang/en';
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
message: {
|
||||
hello: 'hello world'
|
||||
},
|
||||
el: en // Element Plus的英文翻译
|
||||
},
|
||||
zh: {
|
||||
message: {
|
||||
hello: '你好,世界'
|
||||
},
|
||||
el: zhCn // Element Plus的中文翻译
|
||||
}
|
||||
};
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'zh', // 设置默认语言
|
||||
fallbackLocale: 'en', // 设置备用语言
|
||||
messages, // 设置翻译信息
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
7
src/components/icons/IconCommunity.vue
Normal file
7
src/components/icons/IconCommunity.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
7
src/components/icons/IconDocumentation.vue
Normal file
7
src/components/icons/IconDocumentation.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
7
src/components/icons/IconEcosystem.vue
Normal file
7
src/components/icons/IconEcosystem.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
7
src/components/icons/IconSupport.vue
Normal file
7
src/components/icons/IconSupport.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
19
src/components/icons/IconTooling.vue
Normal file
19
src/components/icons/IconTooling.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
29
src/main.js
Normal file
29
src/main.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import config from '../public/config.js';
|
||||
import axios from './axios'
|
||||
import i18n from './components/i18n.js' // 导入i18n
|
||||
import './assets/main.css'
|
||||
import ElementPlus from 'element-plus'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
// axios挂载到全局属性
|
||||
app.config.globalProperties.$http = axios
|
||||
|
||||
// 提供依赖注入
|
||||
app.provide('config', config)
|
||||
app.provide('$http', axios)
|
||||
|
||||
app.use(router)
|
||||
app.use(ElementPlus, { locale: zhCn })
|
||||
app.use(i18n)
|
||||
app.mount('#app')
|
||||
110
src/pages/404/index.vue
Normal file
110
src/pages/404/index.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="interactive-container">
|
||||
<div class="error-card">
|
||||
<div class="liquid"></div>
|
||||
<div class="content">
|
||||
<h1>404</h1>
|
||||
<p>错误(404):当前页面未找到</p>
|
||||
<p>SYSTEM ERROR: PAGE NOT FOUND</p>
|
||||
<el-button type="success" plain round
|
||||
class="magnetic-btn" :icon="HomeFilled"
|
||||
@mouseenter="playHoverSound"
|
||||
@click="goHome">
|
||||
返回主页
|
||||
<span class="hover-effect"></span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { HomeFilled } from '@element-plus/icons-vue'
|
||||
const router = useRouter();
|
||||
|
||||
const goHome = () => {
|
||||
router.push('home');
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.interactive-container {
|
||||
height: 80%;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
height: 500px;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 25px 45px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.liquid {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
animation: rotate 10s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
inset: 2px;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 8rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1rem 0 3rem;
|
||||
}
|
||||
|
||||
.magnetic-btn {
|
||||
position: relative;
|
||||
padding: 1rem 2rem;
|
||||
border: 2px solid rgba(255,255,255,0.5);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.magnetic-btn:hover {
|
||||
}
|
||||
|
||||
.hover-effect {
|
||||
position: absolute;
|
||||
background: radial-gradient(circle at center, rgba(255,255,255,0.4) 0%, transparent 70%);
|
||||
transform: scale(0);
|
||||
transition: transform 0.3s ease;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.magnetic-btn:hover .hover-effect {
|
||||
transform: scale(2);
|
||||
}
|
||||
</style>
|
||||
563
src/pages/commanage/index.vue
Normal file
563
src/pages/commanage/index.vue
Normal file
@@ -0,0 +1,563 @@
|
||||
<template>
|
||||
<div class="company-management">
|
||||
<!-- 操作栏 -->
|
||||
<div class="header">
|
||||
<el-button type="primary" @click="handleAdd" :icon="Plus">添加企业</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 企业表格 -->
|
||||
<el-table :data="companies" border style="width: 100%" v-loading="loading" empty-text="暂无数据">
|
||||
<el-table-column label="操作" width="102">
|
||||
<template #default="{ row }">
|
||||
<el-button type="info" :icon="Edit" circle plain @click="handleEdit(row)" />
|
||||
<el-button type="danger" :icon="Delete" circle plain @click="handleDelete(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="code" label="公司代码" width="100" />
|
||||
<el-table-column prop="nameCn" label="中文名称" width="180" />
|
||||
<el-table-column prop="nameEn" label="英文名称" width="180" />
|
||||
<el-table-column prop="industry" label="所属行业" width="120" />
|
||||
<el-table-column prop="identity" label="身份" width="120" />
|
||||
<el-table-column prop="region" label="地区" width="120" />
|
||||
<!--<el-table-column prop="licenseCode" label="营业执照" width="220" />-->
|
||||
</el-table>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑企业' : '添加企业'"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
width="800px">
|
||||
<el-form :model="form"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
label-width="120px"
|
||||
label-position="right">
|
||||
|
||||
<el-form-item label="中文名称" prop="nameCn">
|
||||
<el-input v-model="form.nameCn" placeholder="请输入中文名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="英文名称" prop="nameEn">
|
||||
<el-input v-model="form.nameEn" placeholder="请输入英文名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属行业" prop="industry">
|
||||
<el-select v-model="form.industry" placeholder="请选择行业">
|
||||
<el-option v-for="item in industryOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="公司身份" prop="identity">
|
||||
<el-select v-model="form.identity" placeholder="请选择身份">
|
||||
<el-option v-for="item in identityOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属地区" prop="region">
|
||||
<el-select v-model="form.region" placeholder="请选择地区">
|
||||
<el-option v-for="(key,item) in regionOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!--
|
||||
<el-form-item label="营业执照代码" prop="licenseCode">
|
||||
<el-input v-model="form.licenseCode" placeholder="请输入营业执照代码" />
|
||||
</el-form-item>-->
|
||||
<!--<el-form-item label="营业执照" prop="licenseImage">
|
||||
<el-upload class="license-uploader"
|
||||
drag
|
||||
action="/api/upload"
|
||||
:show-file-list="false"
|
||||
:on-change="handleLicenseChange"
|
||||
:auto-upload="false">-->
|
||||
<!-- 预览区域 -->
|
||||
<!--<div v-if="form.licenseImage || form.licensePreview" class="image-preview">
|
||||
<img v-if="isImageFile(form.licenseImageFile)"
|
||||
:src="form.licensePreview || getFullUrl(form.licenseImage)"
|
||||
class="preview-image" />
|
||||
<div v-else class="file-preview">
|
||||
<el-icon size="33"><DocumentChecked /></el-icon>
|
||||
<div>营业执照文件 <span style="color: #409EFF">√已上传</span></div>
|
||||
<div class="file-name">{{ getFileName(form.licenseImage) }}</div>
|
||||
</div>
|
||||
<div class="preview-mask">
|
||||
<el-icon><UploadFilled /></el-icon>
|
||||
<div>点击更换文件</div>
|
||||
</div>
|
||||
</div>-->
|
||||
<!-- 未上传状态 -->
|
||||
<!--<template v-else>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
拖拽文件到此 或 <em>点击上传</em>
|
||||
</div>
|
||||
<div class="el-upload__tip">
|
||||
支持JPG/PNG/PDF格式,大小不超过5MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>-->
|
||||
|
||||
<el-form-item label="公司Logo" prop="logo">
|
||||
<el-upload class="logo-uploader"
|
||||
drag
|
||||
action="/api/upload"
|
||||
:show-file-list="false"
|
||||
:on-change="handleLogoChange"
|
||||
:auto-upload="false">
|
||||
<!-- 预览区域 -->
|
||||
<div v-if="form.logoaddress || form.logoPreview" class="image-preview">
|
||||
<img :src="form.logoPreview || getFullUrl(form.logoaddress)"
|
||||
class="preview-image" />
|
||||
<div class="preview-mask">
|
||||
<el-icon><UploadFilled /></el-icon>
|
||||
<div>点击更换Logo</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 未上传状态 -->
|
||||
<template v-else>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
拖拽图片到此 或 <em>点击上传</em>
|
||||
</div>
|
||||
<div class="el-upload__tip">
|
||||
支持JPG/PNG格式,大小不超过5MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, inject, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Delete, Edit, UploadFilled, DocumentChecked } from '@element-plus/icons-vue'
|
||||
|
||||
const $http = inject('$http')
|
||||
const ajaxfile = inject('ajaxfile');
|
||||
const config = inject('config');
|
||||
|
||||
// 数据列表
|
||||
const companies = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 对话框相关
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const currentCompanyId = ref(null)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 表单初始数据
|
||||
const defaultForm = {
|
||||
code: '',
|
||||
nameCn: '',
|
||||
nameEn: '',
|
||||
industry: '',
|
||||
identity: '',
|
||||
region: '',
|
||||
licenseCode: '',
|
||||
logoaddress: '',
|
||||
licenseImage: ''
|
||||
}
|
||||
|
||||
const form = reactive({ ...defaultForm })
|
||||
|
||||
// 选项数据
|
||||
const industryOptions = ref([]);
|
||||
const getIndustry = () => {
|
||||
try {
|
||||
const rs = $http.post('ConfigPY/GetSingleValue', {
|
||||
VarName: "行业分类",
|
||||
}).then(rs => {
|
||||
//console.log(JSON.parse(rs.data.response));
|
||||
industryOptions.value = JSON.parse(rs.data.response)
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const identityOptions = ref([]);
|
||||
const getIdentity = () => {
|
||||
try {
|
||||
const rs = $http.post('ConfigPY/GetSingleValue', {
|
||||
VarName: "公司身份",
|
||||
}).then(rs => {
|
||||
//console.log(JSON.parse(rs.data.response));
|
||||
identityOptions.value = JSON.parse(rs.data.response)
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const regionOptions = ref([]);
|
||||
const getRegion = () => {
|
||||
try {
|
||||
const rs = $http.post('ConfigPY/GetSingleValue', {
|
||||
VarName: "区域",
|
||||
}).then(rs => {
|
||||
//console.log(JSON.parse(rs.data.response));
|
||||
regionOptions.value = JSON.parse(rs.data.response)
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 验证规则
|
||||
const rules = reactive({
|
||||
nameCn: [
|
||||
{ required: true, message: '请输入中文名称', trigger: 'blur' }
|
||||
],
|
||||
industry: [
|
||||
{ required: true, message: '请选择所属行业', trigger: 'change' }
|
||||
],
|
||||
identity: [
|
||||
{ required: true, message: '请选择所属身份', trigger: 'change' }
|
||||
],
|
||||
region: [
|
||||
{ required: true, message: '请选择所属地区', trigger: 'change' }
|
||||
]/*,
|
||||
licenseCode: [
|
||||
{ required: true, message: '请输入营业执照代码', trigger: 'blur' }
|
||||
]*/
|
||||
})
|
||||
|
||||
// 获取企业列表
|
||||
const getCompanies = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const rs = await $http.post('Company/GetComInfo', {
|
||||
IsAll: true,
|
||||
ID: 0,
|
||||
})
|
||||
if (rs.data.isok) {
|
||||
companies.value = rs.data.response
|
||||
//console.log(companies.value);
|
||||
} else {
|
||||
ElMessage.error(rs.data.message)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取企业列表失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取完整图片URL
|
||||
const getFullUrl = (filename) => {
|
||||
//console.log(config.httpAds + `${filename}`);
|
||||
if (!filename) return '';
|
||||
// 如果已经是完整URL直接返回
|
||||
if (filename.startsWith('http')) return filename;
|
||||
// 根据实际存储路径拼接
|
||||
return config.Ads + `${filename}`;
|
||||
}
|
||||
|
||||
const beforeImageUpload = (file) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('文件大小不能超过5MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
const beforeImageUploadPDF = (file) => {
|
||||
const isImageOrPDF = file.type.startsWith('image/') || file.type === 'application/pdf';
|
||||
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||
|
||||
if (!isImageOrPDF) {
|
||||
ElMessage.error('只能上传图片文件或PDF文件!');
|
||||
return false;
|
||||
}
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('文件大小不能超过5MB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// 添加公司
|
||||
const handleAdd = () => {
|
||||
Object.assign(form, defaultForm);
|
||||
isEdit.value = false;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑公司
|
||||
const handleEdit = (row) => {
|
||||
Object.assign(form, row);
|
||||
isEdit.value = true;
|
||||
currentCompanyId.value = row.id;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除公司
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定删除该公司吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
//console.log(row.id);
|
||||
const rs = await $http.post('Company/DelCom', { Id: row.id });
|
||||
if (rs.data.isok) {
|
||||
ElMessage.success('删除成功');
|
||||
await getCompanies();
|
||||
} else {
|
||||
ElMessage.error(rs.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败');
|
||||
console.error(error);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
|
||||
const handleLogoChange = (file) => {
|
||||
form.logoFile = file.raw;
|
||||
// 生成本地预览图URL
|
||||
form.logoPreview = URL.createObjectURL(file.raw); // 新增预览字段
|
||||
};
|
||||
|
||||
const handleLicenseChange = (file) => {
|
||||
form.licenseImageFile = file.raw;
|
||||
// 生成本地预览图URL
|
||||
form.licensePreview = URL.createObjectURL(file.raw); // 新增预览字段
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
await formRef.value.validate();
|
||||
|
||||
try {
|
||||
// 上传Logo
|
||||
if (form.logoFile) {
|
||||
const logoFileData = {
|
||||
File: form.logoFile,
|
||||
Folder: "companylogo" // 根据实际存储目录调整
|
||||
};
|
||||
const logoUploadRes = await ajaxfile(logoFileData);
|
||||
if (logoUploadRes?.fileName) {
|
||||
form.logoaddress = "Uploads/companylogo/" + logoUploadRes.fileName;
|
||||
} else {
|
||||
ElMessage.error('上传Logo失败: ' + logoUploadRes);
|
||||
return;
|
||||
}
|
||||
} else if (form.logoaddress) { // 保留已有Logo逻辑
|
||||
form.logoaddress = form.logoaddress.substring(form.logoaddress.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
/* // 上传营业执照
|
||||
if (form.licenseImageFile) {
|
||||
const licenseFileData = {
|
||||
File: form.licenseImageFile,
|
||||
Folder: "businesslic" // 根据实际存储目录调整
|
||||
};
|
||||
const licenseUploadRes = await ajaxfile(licenseFileData);
|
||||
if (licenseUploadRes?.fileName) {
|
||||
form.licenseImage = licenseUploadRes.fileName;
|
||||
} else {
|
||||
ElMessage.error('上传营业执照失败: ' + licenseUploadRes);
|
||||
return;
|
||||
}
|
||||
} else if (form.licenseImage) { // 保留已有营业执照逻辑
|
||||
form.licenseImage = form.licenseImage.substring(form.licenseImage.lastIndexOf('/') + 1);
|
||||
}*/
|
||||
|
||||
// 提交表单数据
|
||||
const api = isEdit.value ? 'Company/EditCom' : 'Company/AddCom';
|
||||
const data = isEdit.value ? { ...form, id: currentCompanyId.value } : form;
|
||||
|
||||
const rs = await $http.post(api, data);
|
||||
if (rs.data.isok) {
|
||||
ElMessage.success(isEdit.value ? '修改成功' : '添加成功');
|
||||
dialogVisible.value = false;
|
||||
await getCompanies();
|
||||
} else {
|
||||
ElMessage.error(rs.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const isImageFile = (url) => {
|
||||
return /\.(jpe?g|png|gif|webp)$/i.test(url)
|
||||
}
|
||||
|
||||
const getFileName = (url) => {
|
||||
return url.split('/').pop()
|
||||
}
|
||||
// 其他操作函数(handleAdd/handleEdit/handleDelete)与公司管理页面逻辑类似
|
||||
// 此处省略,保持与公司管理页面相同逻辑即可
|
||||
|
||||
onMounted(() => {
|
||||
getCompanies()
|
||||
getIndustry()
|
||||
getIdentity()
|
||||
getRegion()
|
||||
})
|
||||
// 在组件卸载时释放对象URL
|
||||
onUnmounted(() => {
|
||||
if (form.logoPreview) URL.revokeObjectURL(form.logoPreview);
|
||||
if (form.licensePreview) URL.revokeObjectURL(form.licensePreview);
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.company-management {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.logo-uploader,
|
||||
.license-uploader {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.logo-uploader {
|
||||
width: 300px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.license-uploader {
|
||||
width: 300px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.logo-uploader:hover,
|
||||
.license-uploader:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.license-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 通用上传样式 */
|
||||
.logo-uploader :deep(.el-upload-dragger),
|
||||
.license-uploader :deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 预览区域公共样式 */
|
||||
.image-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 200px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 遮罩层样式 */
|
||||
.preview-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.image-preview:hover .preview-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.el-icon--upload {
|
||||
font-size: 36px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
498
src/pages/dicmanage/index.vue
Normal file
498
src/pages/dicmanage/index.vue
Normal file
@@ -0,0 +1,498 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div>字典名称</div>
|
||||
<el-select v-model="currentVarName"
|
||||
placeholder="请选择字典项"
|
||||
@change="handleDictChange">
|
||||
<el-option v-for="item in dictList"
|
||||
:key="item.varName"
|
||||
:label="item.varName"
|
||||
:value="item.varName" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div>字典修改</div>
|
||||
|
||||
<!-- 区域字典特殊表格 -->
|
||||
<el-table v-if="isRegionDict"
|
||||
:data="regionData"
|
||||
stripe
|
||||
style="width: 777px">
|
||||
<el-table-column prop="key" label="大区名称">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.key"
|
||||
placeholder="请输入大区名称" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="value" label="包含省份(点击修改省份/地区)" width="521">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" @click="showProvinceDialog(row)"
|
||||
link style="width: 100%; padding: 0; height: auto; min-height: 32px; white-space: normal; word-break: break-all; line-height: 1.5; display: inline-flex; align-items: center;">
|
||||
<span style="text-align: left;">
|
||||
{{ row.value.join(',') || '点击选择省份' }}
|
||||
</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
|
||||
<el-table-column label="操作" width="63">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger"
|
||||
:icon="Delete"
|
||||
circle
|
||||
@click="deleteRegion($index)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 普通字典表格 -->
|
||||
<el-table v-else
|
||||
:data="currentValues"
|
||||
stripe
|
||||
style="width: 777px">
|
||||
<el-table-column prop="value" label="字典值">
|
||||
<template #default="{ $index }">
|
||||
<el-input v-model="currentValues[$index]" placeholder="请输入" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="63">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger"
|
||||
:icon="Delete"
|
||||
circle
|
||||
@click="handleDelete($index)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="footer">
|
||||
<template v-if="isRegionDict">
|
||||
<el-button type="primary"
|
||||
:icon="Plus"
|
||||
plain
|
||||
round
|
||||
@click="addNewRegion">
|
||||
添加大区
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button type="primary"
|
||||
:icon="Plus"
|
||||
plain
|
||||
round
|
||||
@click="handleAdd">
|
||||
添加值
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<el-button type="success"
|
||||
:icon="FolderChecked"
|
||||
@click="handleSave">
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 省份选择对话框 -->
|
||||
<el-dialog v-model="provinceDialogVisible"
|
||||
title="选择省份/地区"
|
||||
width="50%">
|
||||
<el-checkbox-group v-model="selectedProvinces">
|
||||
<el-row :gutter="20">
|
||||
<el-col v-for="province in allProvinces"
|
||||
:key="province"
|
||||
:span="8"
|
||||
style="margin-bottom: 15px;">
|
||||
<el-checkbox :label="province"
|
||||
:value="province" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-checkbox-group>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="provinceDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmProvinceSelection">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, inject, computed } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Delete, Edit, UploadFilled, RefreshLeft, Upload, EditPen, FolderChecked } from '@element-plus/icons-vue';
|
||||
|
||||
const $http = inject('$http')
|
||||
const config = inject('config');
|
||||
|
||||
// 初始字典数据
|
||||
const dictList = reactive([]);
|
||||
// 新增响应式数据
|
||||
const provinceDialogVisible = ref(false);
|
||||
const selectedProvinces = ref([]);
|
||||
const currentEditingRegion = ref(null);
|
||||
const allProvinces = computed(() => {
|
||||
const provinceDict = dictList.find(d => d.varName == '省份地区');
|
||||
return provinceDict?.varValue || [];
|
||||
});
|
||||
// 获取字典
|
||||
const getDic = async () => {
|
||||
try {
|
||||
const response = await $http.post('ConfigPY/GetConfigString');
|
||||
const serverData = response.data.response || [];
|
||||
|
||||
//console.log(response);
|
||||
// 清空原有数据
|
||||
dictList.length = 0;
|
||||
|
||||
// 转换数据格式
|
||||
serverData.forEach(item => {
|
||||
dictList.push({
|
||||
varName: item.varName,
|
||||
varValue: tryParseJson(item.varValue) || []
|
||||
});
|
||||
});
|
||||
//console.log(dictList);
|
||||
// 默认选中第一个(如果存在)
|
||||
if (dictList.length > 0) {
|
||||
currentVarName.value = dictList[0].varName;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取字典失败:', error);
|
||||
ElMessage.error('字典数据加载失败');
|
||||
}
|
||||
};
|
||||
|
||||
// JSON安全解析方法
|
||||
const tryParseJson = (str) => {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const currentVarName = ref('');
|
||||
const currentDict = computed(() =>
|
||||
dictList.find(item => item.varName === currentVarName.value)
|
||||
);
|
||||
const currentValues = computed({
|
||||
get: () => currentDict.value?.varValue || [],
|
||||
set: (val) => {
|
||||
if (currentDict.value) {
|
||||
currentDict.value.varValue = val;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 处理字典项变化
|
||||
const handleDictChange = (val) => {
|
||||
if (!val) return;
|
||||
};
|
||||
// 判断是否是区域字典
|
||||
const isRegionDict = computed(() => currentVarName.value === '区域');
|
||||
|
||||
// 修改区域数据计算属性(移除setter)
|
||||
const regionData = computed(() => {
|
||||
if (!isRegionDict.value) return [];
|
||||
const regionDict = dictList.find(d => d.varName === '区域');
|
||||
return Object.entries(regionDict?.varValue || {}).map(([key, value]) => ({
|
||||
key,
|
||||
value: Array.isArray(value) ? value : [value]
|
||||
}));
|
||||
});
|
||||
|
||||
// 显示省份选择对话框
|
||||
const showProvinceDialog = (row) => {
|
||||
currentEditingRegion.value = row;
|
||||
selectedProvinces.value = [...row.value];
|
||||
provinceDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 修改省份选择确认方法
|
||||
const confirmProvinceSelection = async () => {
|
||||
try {
|
||||
if (!currentEditingRegion.value) return;
|
||||
|
||||
// 获取当前编辑的大区信息
|
||||
const currentKey = currentEditingRegion.value.key;
|
||||
const selectedProvincesList = selectedProvinces.value;
|
||||
|
||||
// 获取区域字典数据
|
||||
const regionDict = dictList.find(d => d.varName === '区域');
|
||||
if (!regionDict) return;
|
||||
|
||||
// 收集所有其他大区的省份映射
|
||||
const provinceMap = new Map();
|
||||
Object.entries(regionDict.varValue).forEach(([region, provinces]) => {
|
||||
if (region !== currentKey) {
|
||||
provinces.forEach(province => {
|
||||
provinceMap.set(province, region);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 检查重复省份
|
||||
const duplicates = [];
|
||||
selectedProvincesList.forEach(province => {
|
||||
if (provinceMap.has(province)) {
|
||||
duplicates.push({
|
||||
province,
|
||||
region: provinceMap.get(province)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 如果有重复则提示
|
||||
if (duplicates.length > 0) {
|
||||
const confirmMessage = `
|
||||
以下省份已在其他大区存在:<br>
|
||||
${duplicates.map(d => `${d.province} → ${d.region}`).join('<br>')}<br>
|
||||
是否确认继续保存?
|
||||
`;
|
||||
|
||||
await ElMessageBox.confirm(confirmMessage, '重复省份警告', {
|
||||
confirmButtonText: '强制保存',
|
||||
cancelButtonText: '返回修改',
|
||||
type: 'warning',
|
||||
dangerouslyUseHTMLString: true
|
||||
});
|
||||
}
|
||||
|
||||
// 执行保存操作
|
||||
regionDict.varValue = {
|
||||
...regionDict.varValue,
|
||||
[currentKey]: selectedProvincesList
|
||||
};
|
||||
provinceDialogVisible.value = false;
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error(error);
|
||||
}
|
||||
return; // 用户取消则中断流程
|
||||
}
|
||||
};
|
||||
/* const confirmProvinceSelection = () => {
|
||||
if (currentEditingRegion.value) {
|
||||
const regionDict = dictList.find(d => d.varName === '区域');
|
||||
if (regionDict) {
|
||||
regionDict.varValue = {
|
||||
...regionDict.varValue,
|
||||
[currentEditingRegion.value.key]: selectedProvinces.value
|
||||
};
|
||||
}
|
||||
}
|
||||
provinceDialogVisible.value = false;
|
||||
};*/
|
||||
|
||||
// 修改添加大区方法
|
||||
const addNewRegion = () => {
|
||||
const regionDict = dictList.find(d => d.varName === '区域');
|
||||
if (!regionDict) return;
|
||||
|
||||
// 生成唯一键名
|
||||
let newKey = "新增大区";
|
||||
let counter = 1;
|
||||
while (regionDict.varValue[newKey]) {
|
||||
newKey = `新增大区${counter++}`;
|
||||
}
|
||||
|
||||
// 直接修改原始数据
|
||||
regionDict.varValue = {
|
||||
...regionDict.varValue,
|
||||
[newKey]: []
|
||||
};
|
||||
};
|
||||
|
||||
// 修改删除大区方法
|
||||
const deleteRegion = (index) => {
|
||||
ElMessageBox.confirm('确定要删除该大区吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const regionDict = dictList.find(d => d.varName === '区域');
|
||||
if (!regionDict) return;
|
||||
|
||||
const keyToDelete = regionData.value[index].key;
|
||||
const newValue = { ...regionDict.varValue };
|
||||
delete newValue[keyToDelete];
|
||||
regionDict.varValue = newValue;
|
||||
});
|
||||
};
|
||||
|
||||
// 添加行
|
||||
const handleAdd = () => {
|
||||
if (!currentDict.value) return;
|
||||
currentDict.value.varValue.push('');
|
||||
};
|
||||
|
||||
// 删除行
|
||||
const handleDelete = (index) => {
|
||||
ElMessageBox.confirm('确定要删除该行吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
currentDict.value.varValue.splice(index, 1);
|
||||
});
|
||||
};
|
||||
|
||||
// 保存字典
|
||||
const saveDic = async (name, value) => {
|
||||
try {
|
||||
let filteredValue;
|
||||
|
||||
// 根据数据类型处理
|
||||
if (Array.isArray(value)) {
|
||||
// 处理普通数组格式
|
||||
filteredValue = value
|
||||
.map(item => String(item).trim())
|
||||
.filter(item => item !== "");
|
||||
|
||||
if (filteredValue.length === 0) {
|
||||
throw new Error("不能保存空字典项");
|
||||
}
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// 处理区域字典的对象格式
|
||||
filteredValue = Object.entries(value).reduce((acc, [key, values]) => {
|
||||
const filtered = values
|
||||
.map(item => String(item).trim())
|
||||
.filter(item => item !== "");
|
||||
|
||||
// 保留key即使值为空数组(如"其他")
|
||||
if (key === '其他' || filtered.length > 0) {
|
||||
acc[key] = filtered;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 检查是否为空对象
|
||||
if (Object.keys(filteredValue).length === 0) {
|
||||
throw new Error("至少需要保留一个有效区域");
|
||||
}
|
||||
} else {
|
||||
throw new Error("无效的字典格式");
|
||||
}
|
||||
|
||||
const valueJs = JSON.stringify(filteredValue);
|
||||
const rs = await $http.post('ConfigPY/SaveOrAddConfigString', {
|
||||
VarName: name,
|
||||
VarValue: valueJs
|
||||
});
|
||||
|
||||
return filteredValue;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 保存处理
|
||||
const handleSave = async () => {
|
||||
if (!currentDict.value) {
|
||||
ElMessage.warning('请先选择要修改的字典项');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'此操作将永久修改字典数据,是否继续?',
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确认保存',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
center: true,
|
||||
dangerouslyUseHTMLString: true,
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
saveDic(currentDict.value.varName, currentDict.value.varValue)
|
||||
.then(() => {
|
||||
done();
|
||||
ElMessage.success('保存成功');
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
ElMessage.error('保存失败');
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
// 处理区域字典的特殊结构
|
||||
let saveData = currentDict.value.varValue;
|
||||
|
||||
if (isRegionDict.value) {
|
||||
// 转换回原始对象格式
|
||||
saveData = regionData.value.reduce((acc, cur) => {
|
||||
acc[cur.key] = cur.value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// 保存并获取处理后的数据
|
||||
const filteredData = await saveDic(
|
||||
currentDict.value.varName,
|
||||
saveData
|
||||
);
|
||||
|
||||
// 更新前端数据
|
||||
currentDict.value.varValue = isRegionDict.value
|
||||
? { ...filteredData } // 对象格式
|
||||
: [...filteredData]; // 数组格式
|
||||
|
||||
} catch (error) {
|
||||
if (error === 'cancel') {
|
||||
//console.log('用户取消保存');
|
||||
} else if (error.message === '不能保存空字典项') {
|
||||
ElMessage.warning('字典项至少需要包含一个有效值');
|
||||
} else {
|
||||
ElMessage.error('保存失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getDic()
|
||||
// 初始化选择第一个
|
||||
if (dictList.length > 0) {
|
||||
currentVarName.value = dictList[0].varName;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 333px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 777px;
|
||||
display: flex;
|
||||
justify-content: space-between; /* 推荐方案 */
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.el-message-box {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
48
src/pages/home/index.vue
Normal file
48
src/pages/home/index.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div>Home</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const $http = inject('$http')
|
||||
const menuValue = inject('menuValue')
|
||||
|
||||
// 注入方法
|
||||
const checkLoginStatus = inject('checkLoginStatus');
|
||||
onMounted(() => {
|
||||
localStorage.setItem('url', '/home')
|
||||
checkLoginStatus();
|
||||
loginIndex()
|
||||
//handleSubmit()
|
||||
})
|
||||
|
||||
// 登录校验
|
||||
const loginIndex = () => {
|
||||
const permission = localStorage.getItem("permission")
|
||||
const permissionArray = permission
|
||||
? permission.split(',').filter(s => s)
|
||||
: [];
|
||||
// 获取第一个有权限的路由
|
||||
const firstRoute = getFirstPermissionRoute(permissionArray);
|
||||
// 跳转到第一个有权限的路由
|
||||
router.push(firstRoute);
|
||||
}
|
||||
const getFirstPermissionRoute = (permissionArray) => {
|
||||
for (const menu of menuValue) {
|
||||
for (const subItem of menu.options) {
|
||||
if (permissionArray.includes('/全选') || permissionArray.includes("/" + subItem.label)) {
|
||||
return subItem.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '/home'; // 默认页面
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
793
src/pages/iotregister/index.vue
Normal file
793
src/pages/iotregister/index.vue
Normal file
@@ -0,0 +1,793 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 机型选择 -->
|
||||
<div class="header">
|
||||
<el-select v-model="selectedDeviceId"
|
||||
placeholder="请选择机型"
|
||||
filterable
|
||||
size="large"
|
||||
@change="handleDeviceChange"
|
||||
style="width: 736px;">
|
||||
<el-option v-for="device in allDevices"
|
||||
:key="device.id"
|
||||
:label="device.deviceTypeName"
|
||||
:value="device.id">
|
||||
<span style="float: left; font-weight: 700">
|
||||
{{ device.deviceTypeName }}
|
||||
</span>
|
||||
|
||||
<span style="float: right; color: var(--el-text-color-secondary); font-size: 13px; line-height: 1.2;
|
||||
margin-top: 1px;margin-left: 8px;">
|
||||
<span style="display: block;">
|
||||
<span style="font-weight: 600;">
|
||||
{{ device.deviceClass }}
|
||||
</span>
|
||||
</span>
|
||||
<span style="display:block;width:220px">
|
||||
{{ truncateText(device.deviceInfo) }}
|
||||
</span>
|
||||
</span>
|
||||
<span style="float: right;">
|
||||
<el-tag type="success">
|
||||
{{ device.connTypeName }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<!-- Excel导入 -->
|
||||
<el-upload action=""
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="handleFileChange"
|
||||
style="flex-shrink: 0;"
|
||||
v-show="selectedDeviceId !== ''">
|
||||
<el-button type="success">
|
||||
<el-icon><DocumentAdd /></el-icon>
|
||||
导入Excel文件
|
||||
</el-button>
|
||||
</el-upload>
|
||||
|
||||
<el-button type="danger"
|
||||
v-show="tableData.length && !markID"
|
||||
@click="handleUpload"
|
||||
:disabled="uploadLoading">
|
||||
<el-icon v-if="uploadLoading"><Loading /></el-icon>
|
||||
<el-icon><Upload /></el-icon>
|
||||
上传数据
|
||||
</el-button>
|
||||
<el-button type="primary"
|
||||
v-show="markID && !AlreadyKey"
|
||||
@click="downLoadExcel"
|
||||
style="margin:0"
|
||||
:disabled="downloadLoading">
|
||||
<el-icon><Download /></el-icon>
|
||||
获取秘钥
|
||||
</el-button>
|
||||
<el-button type="primary"
|
||||
v-show="markID && AlreadyKey"
|
||||
@click="generateExcel"
|
||||
style="margin:0"
|
||||
:disabled="downloadLoading">
|
||||
<el-icon><DocumentAdd /></el-icon>
|
||||
导出表格
|
||||
</el-button>
|
||||
|
||||
<el-switch v-show="selectedDeviceId"
|
||||
v-model="showHistory"
|
||||
inline-prompt
|
||||
size="large"
|
||||
style="--el-switch-on-color: #E6A23C; --el-switch-off-color: #409EFF "
|
||||
@change="handleHistorySwitch"
|
||||
active-text="历史记录数据"
|
||||
inactive-text="手动生成数据" />
|
||||
</div>
|
||||
|
||||
<div class="form-generator" v-show="!showHistory && selectedDeviceId">
|
||||
<el-input v-model="formName"
|
||||
placeholder="设备名称前缀(必填)"
|
||||
clearable
|
||||
:disabled="!selectedDeviceId"
|
||||
style="width: 210px; margin-right: 12px;" />
|
||||
<el-input v-model.number="formCodeStart"
|
||||
placeholder="起始编号(整数)"
|
||||
type="number"
|
||||
:disabled="!selectedDeviceId"
|
||||
clearable
|
||||
style="width: 140px; margin-right: 12px;" />
|
||||
<el-input v-model.number="formCodeEnd"
|
||||
placeholder="结束编号(整数)"
|
||||
type="number"
|
||||
:disabled="!selectedDeviceId"
|
||||
clearable
|
||||
style="width: 140px; margin-right: 12px;" />
|
||||
<el-input v-model="formMO"
|
||||
placeholder="订单编号(可选)"
|
||||
:disabled="!selectedDeviceId"
|
||||
clearable
|
||||
style="width: 210px; margin-right: 12px;" />
|
||||
<el-input v-model="formRemark"
|
||||
placeholder="备注(可选)"
|
||||
:disabled="!selectedDeviceId"
|
||||
clearable
|
||||
style="width: 250px; margin-right: 12px;" />
|
||||
<el-button type="primary"
|
||||
@click="generateData"
|
||||
:disabled="!selectedDeviceId || !formName">
|
||||
<el-icon><MagicStick /></el-icon>
|
||||
生成数据
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 新增历史记录模块 -->
|
||||
<div class="form-generator" v-show="showHistory && selectedDeviceId">
|
||||
<div style="width:392px">
|
||||
<el-date-picker v-model="selectedDateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width:380px;margin-right:12px"
|
||||
:shortcuts="shortcuts" />
|
||||
</div>
|
||||
|
||||
<el-button type="primary"
|
||||
@click="fetchHistoryFiles"
|
||||
:loading="fileLoading">
|
||||
<el-icon><Search /></el-icon>
|
||||
查询
|
||||
</el-button>
|
||||
<el-select v-model="selectedFileFlag"
|
||||
placeholder="请选择文件序号"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 360px; margin-left: 12px;"
|
||||
@change="handleFileSelect">
|
||||
<el-option v-for="r in fileOptions"
|
||||
:key="r.ExcelBatchFlag"
|
||||
style="width: 444px;"
|
||||
:label="`${formatTime(r.CreateTime)} 共${ r.Count }条记录`"
|
||||
:value="r.ExcelBatchFlag">
|
||||
<span style="float: left; font-weight: 600;font-size:15px">
|
||||
{{ formatTime(r.CreateTime) }}
|
||||
</span>
|
||||
|
||||
<span style="float: right; font-size: 13px">
|
||||
记录数量:
|
||||
{{ r.Count }}
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 虚拟化表格 -->
|
||||
<el-auto-resizer v-if="tableData.length">
|
||||
<template #default="{ height, width }">
|
||||
<el-table-v2 :columns="columns"
|
||||
:data="tableData"
|
||||
:width="1555"
|
||||
:height="height"
|
||||
fixed
|
||||
row-key="id"
|
||||
:row-height="40"
|
||||
:sort-by="sortBy"
|
||||
@column-sort="onSort" />
|
||||
</template>
|
||||
</el-auto-resizer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted, h, watch } from 'vue'
|
||||
import { ElMessage, ElLoading, ElPopover } from 'element-plus'
|
||||
import * as XLSX from 'xlsx'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const $http = inject('$http')
|
||||
|
||||
const shortcuts = [
|
||||
{ text: '最近3天', value: () => [dayjs().subtract(2, 'day'), dayjs()] },
|
||||
{ text: '最近7天', value: () => [dayjs().subtract(6, 'day'), dayjs()] },
|
||||
{ text: '最近15天', value: () => [dayjs().subtract(14, 'day'), dayjs()] },
|
||||
{ text: '最近30天', value: () => [dayjs().subtract(29, 'day'), dayjs()] },
|
||||
{ text: '最近90天', value: () => [dayjs().subtract(89, 'day'), dayjs()] }
|
||||
]
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
key: 'DeviceName',
|
||||
dataKey: 'DeviceName',
|
||||
title: 'DeviceName',
|
||||
width: 250,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'MO',
|
||||
dataKey: 'MO',
|
||||
title: 'MO',
|
||||
width: 180,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'ClientID',
|
||||
dataKey: 'ClientID',
|
||||
title: 'ClientID',
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
key: 'MQTT_Login_Password',
|
||||
dataKey: 'MQTT_Login_Password',
|
||||
title: 'MQTT_Login_Password',
|
||||
width: 250,
|
||||
cellRenderer: ({ cellData }) => h(
|
||||
'div',
|
||||
{
|
||||
style: 'width: 100%; height: 100%; display: flex; align-items: center; padding: 0 8px;'
|
||||
},
|
||||
[
|
||||
h(
|
||||
ElPopover,
|
||||
{
|
||||
width: 420,
|
||||
trigger: 'click',
|
||||
content: cellData
|
||||
},
|
||||
{
|
||||
reference: () => h('span',
|
||||
{
|
||||
style: 'cursor: pointer; text-decoration: underline dotted;'
|
||||
},
|
||||
truncateText(cellData)
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'Key',
|
||||
dataKey: 'Key',
|
||||
title: 'Key',
|
||||
width: 250,
|
||||
cellRenderer: ({ cellData }) => h(
|
||||
'div',
|
||||
{
|
||||
style: 'width: 100%; height: 100%; display: flex; align-items: center; padding: 0 8px;'
|
||||
},
|
||||
[
|
||||
h(
|
||||
ElPopover,
|
||||
{
|
||||
width: 350,
|
||||
trigger: 'click',
|
||||
content: cellData
|
||||
},
|
||||
{
|
||||
reference: () => h('span',
|
||||
{
|
||||
style: 'cursor: pointer; text-decoration: underline dotted;'
|
||||
},
|
||||
truncateText(cellData)
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'Remark',
|
||||
dataKey: 'Remark',
|
||||
title: 'Remark',
|
||||
width: 350,
|
||||
},
|
||||
]
|
||||
|
||||
const allDevices = ref([])
|
||||
const selectedDeviceId = ref('')
|
||||
const tableData = ref([])
|
||||
const sortBy = ref({ key: 'DeviceName', order: 'asc' })
|
||||
|
||||
const formName = ref('')
|
||||
const formCodeStart = ref(1)
|
||||
const formCodeEnd = ref(100)
|
||||
const formMO = ref('')
|
||||
const formRemark = ref('')
|
||||
|
||||
const markID = ref('')
|
||||
const uploadLoading = ref(false)
|
||||
const downloadLoading = ref(false)
|
||||
const AlreadyKey = ref(false)
|
||||
|
||||
const showHistory = ref(false)
|
||||
const selectedDateRange = ref([])
|
||||
const fileOptions = ref([])
|
||||
const selectedFileFlag = ref('')
|
||||
const fileLoading = ref(false)
|
||||
|
||||
watch(() => tableData.value, (newVal) => {
|
||||
if (newVal.length > 0 && newVal[0]?.Key) {
|
||||
AlreadyKey.value = true
|
||||
} else {
|
||||
AlreadyKey.value = false
|
||||
}
|
||||
}, {
|
||||
deep: true,
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const handleDeviceChange = () => {
|
||||
// 清空表格数据
|
||||
tableData.value = []
|
||||
formName.value = ''
|
||||
selectedFileFlag.value = ''
|
||||
fileOptions.value = []
|
||||
markID.value = ''
|
||||
}
|
||||
|
||||
// 生成数据方法
|
||||
const generateData = () => {
|
||||
// 数据校验
|
||||
if (!selectedDeviceId.value) {
|
||||
ElMessage.warning('请先选择机型')
|
||||
return
|
||||
}
|
||||
if (!formName.value) {
|
||||
ElMessage.warning('请输入设备名称')
|
||||
return
|
||||
}
|
||||
|
||||
if (isNaN(formCodeStart.value) || isNaN(formCodeEnd.value)) {
|
||||
ElMessage.warning('起始码和结束码必须为数字')
|
||||
return
|
||||
}
|
||||
|
||||
if (formCodeEnd.value < formCodeStart.value) {
|
||||
ElMessage.warning('结束码不能小于起始码')
|
||||
return
|
||||
}
|
||||
|
||||
// 生成数据
|
||||
const start = parseInt(formCodeStart.value)
|
||||
const end = parseInt(formCodeEnd.value)
|
||||
const generatedData = []
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
generatedData.push({
|
||||
id: i,
|
||||
DeviceName: `${formName.value}-${i}`,
|
||||
MO: formMO.value,
|
||||
ClientID: '',
|
||||
MQTT_Login_Password: '',
|
||||
Key: '',
|
||||
Remark: formRemark.value,
|
||||
MachineType: selectedDeviceId.value
|
||||
})
|
||||
}
|
||||
|
||||
tableData.value = generatedData
|
||||
ElMessage.success(`成功生成 ${generatedData.length} 条数据`)
|
||||
}
|
||||
|
||||
|
||||
// 排序处理
|
||||
const onSort = ({ key, order }) => {
|
||||
sortBy.value = { key, order }
|
||||
tableData.value.sort((a, b) => {
|
||||
const aValue = a[key]
|
||||
const bValue = b[key]
|
||||
|
||||
if (order === 'asc') {
|
||||
return aValue.localeCompare(bValue)
|
||||
}
|
||||
return bValue.localeCompare(aValue)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理历史模式切换
|
||||
const handleHistorySwitch = (val) => {
|
||||
tableData.value = []
|
||||
markID.value = ''
|
||||
}
|
||||
|
||||
// 获取历史文件列表
|
||||
const fetchHistoryFiles = async () => {
|
||||
if (!selectedDateRange.value || selectedDateRange.value.length !== 2) {
|
||||
return ElMessage.warning('请先选择日期范围')
|
||||
}
|
||||
let loadingInstance = null
|
||||
try {
|
||||
// 创建全屏遮罩
|
||||
loadingInstance = ElLoading.service({
|
||||
lock: true,
|
||||
text: '正在加载记录,请稍候...',
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
fullscreen: true
|
||||
})
|
||||
fileLoading.value = true
|
||||
const rs = await $http.post('Deviceinfoes/GetAllBatchFlag', {
|
||||
MachineType: selectedDeviceId.value,
|
||||
StartTime: `${selectedDateRange.value[0]} 00:00:00`,
|
||||
EndTime: `${dayjs(selectedDateRange.value[1]).add(1, 'day').format('YYYY-MM-DD')} 00:00:00`
|
||||
})
|
||||
//console.log(rs.data)
|
||||
if (rs.data.isok) {
|
||||
fileOptions.value = rs.data.response.map(item => ({
|
||||
ExcelBatchFlag: item.excelBatchFlag,
|
||||
CreateTime: item.createTime,
|
||||
Count: item.count
|
||||
}))
|
||||
if (fileOptions.value.length > 0) {
|
||||
ElMessage.success(`找到 ${fileOptions.value.length} 条历史记录`)
|
||||
} else {
|
||||
ElMessage.warning(`无历史记录`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('获取历史文件失败: ' + (error.response?.data?.message || error.message))
|
||||
} finally {
|
||||
// 无论成功失败都关闭加载状态
|
||||
fileLoading.value = false
|
||||
if (loadingInstance) {
|
||||
loadingInstance.close() // 关闭全屏遮罩
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = async (batchFlag) => {
|
||||
let loadingInstance = null
|
||||
tableData.value = []
|
||||
if(!batchFlag) return
|
||||
try {
|
||||
// 创建全屏加载遮罩
|
||||
loadingInstance = ElLoading.service({
|
||||
lock: true,
|
||||
text: '正在加载数据,请稍候...',
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
fullscreen: true
|
||||
})
|
||||
|
||||
const rs = await $http.post('Deviceinfoes/GetDeviceinfosByBatchFlag', {
|
||||
FileBatchFlag: batchFlag
|
||||
})
|
||||
|
||||
if (rs.data.isok) {
|
||||
tableData.value = rs.data.response.map((item, index) => ({
|
||||
id: index,
|
||||
DeviceName: item.deviceName,
|
||||
MO: item.mo,
|
||||
ClientID: item.clientID,
|
||||
MQTT_Login_Password: item.mqtT_Login_Password,
|
||||
Key: item.key,
|
||||
Remark: item.remark
|
||||
}))
|
||||
markID.value = batchFlag
|
||||
if (tableData.value.length > 0) {
|
||||
ElMessage.success(`成功加载 ${tableData.value.length} 条记录`)
|
||||
} else {
|
||||
ElMessage.warning(`无记录`)
|
||||
}
|
||||
} else {
|
||||
// 处理接口返回的错误
|
||||
ElMessage.error(rs.data.message || '数据加载失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件数据加载失败:', error)
|
||||
// 优化错误提示,优先显示后端返回的错误信息
|
||||
const errorMsg = error.response?.data?.message || error.message
|
||||
ElMessage.error(`获取文件数据失败: ${errorMsg}`)
|
||||
} finally {
|
||||
// 确保总是关闭加载状态
|
||||
if (loadingInstance) {
|
||||
loadingInstance.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Excel文件处理(优化大数据量处理)
|
||||
const handleFileChange = (file) => {
|
||||
markID.value = ''
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const data = new Uint8Array(e.target.result)
|
||||
const workbook = XLSX.read(data, { type: 'array' })
|
||||
const worksheet = workbook.Sheets[workbook.SheetNames[0]]
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 })
|
||||
|
||||
if (jsonData.length < 2) {
|
||||
ElMessage.error('表格数据不完整')
|
||||
return
|
||||
}
|
||||
|
||||
// 表头验证
|
||||
const headers = jsonData[0]
|
||||
if (
|
||||
headers[0] !== 'DeviceName' ||
|
||||
headers[1] !== 'MO' ||
|
||||
headers[5] !== 'Remark'
|
||||
) {
|
||||
ElMessage.error('表格格式有误')
|
||||
return
|
||||
}
|
||||
|
||||
// 使用惰性生成器处理大数据
|
||||
const dataGenerator = function* () {
|
||||
for (let i = 1; i < jsonData.length; i++) {
|
||||
const row = jsonData[i]
|
||||
yield {
|
||||
id: i,
|
||||
DeviceName: row[0]?.toString().trim(),
|
||||
MO: row[1]?.toString().trim(),
|
||||
ClientID: '',
|
||||
MQTT_Login_Password: '',
|
||||
Key: '',
|
||||
Remark: row[5]?.toString().trim(),
|
||||
MachineType: selectedDeviceId.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedData = []
|
||||
for (const item of dataGenerator()) {
|
||||
if (item.DeviceName) {
|
||||
processedData.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (!processedData.length) {
|
||||
ElMessage.warning('未找到有效数据')
|
||||
return
|
||||
}
|
||||
//console.log(processedData)
|
||||
tableData.value = processedData
|
||||
ElMessage.success(`成功导入 ${processedData.length} 条数据`)
|
||||
}
|
||||
reader.readAsArrayBuffer(file.raw)
|
||||
}
|
||||
|
||||
const truncateText = (text) => {
|
||||
const maxLen = 30
|
||||
let total = 0
|
||||
const result = []
|
||||
|
||||
for (const char of text) {
|
||||
const code = char.charCodeAt(0)
|
||||
total += code > 255 ? 2 : 1
|
||||
if (total > maxLen) break
|
||||
result.push(char)
|
||||
}
|
||||
|
||||
return result.join('') + (total > maxLen ? '...' : '')
|
||||
}
|
||||
|
||||
// 上传数据处理
|
||||
const handleUpload = async () => {
|
||||
if (!tableData.value.length) {
|
||||
ElMessage.warning('请先导入数据')
|
||||
return
|
||||
} else if (tableData.value.length > 20000) {
|
||||
ElMessage.warning('每次导入数据不可大于20000行')
|
||||
return
|
||||
}
|
||||
|
||||
let loadingInstance = null
|
||||
try {
|
||||
uploadLoading.value = true
|
||||
loadingInstance = ElLoading.service({
|
||||
lock: true,
|
||||
text: '数据上传中...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
const rs = await $http.post('Deviceinfoes/AddDeviceinfo', tableData.value)
|
||||
console.log(rs)
|
||||
if (rs.data.isok) {
|
||||
markID.value = rs.data.response // 保存接口返回的标记ID
|
||||
ElMessage.success('数据上传成功')
|
||||
} else {
|
||||
ElMessage.error(rs.data.message || '上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('请求失败: ' + error.message)
|
||||
} finally {
|
||||
uploadLoading.value = false
|
||||
loadingInstance?.close()
|
||||
}
|
||||
}
|
||||
|
||||
// 下载Excel处理
|
||||
const downLoadExcel = async () => {
|
||||
if (!markID.value) {
|
||||
ElMessage.warning('请先上传数据')
|
||||
return
|
||||
}
|
||||
|
||||
let loadingInstance = null
|
||||
try {
|
||||
downloadLoading.value = true
|
||||
loadingInstance = ElLoading.service({
|
||||
lock: true,
|
||||
text: '文件生成中...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
// 调用新下载接口获取数据数组
|
||||
const rs = await $http.post('Deviceinfoes/GetDeviceinfosByBatchFlag', { FileBatchFlag: markID.value })
|
||||
console.log(rs)
|
||||
if (rs.data.isok && rs.data.response) {
|
||||
// 更新表格数据
|
||||
tableData.value = rs.data.response.map((item, index) => ({
|
||||
id: index,
|
||||
DeviceName: item.deviceName,
|
||||
MO: item.mo,
|
||||
ClientID: item.clientID,
|
||||
MQTT_Login_Password: item.mqtT_Login_Password,
|
||||
Key: item.key,
|
||||
Remark: item.remark
|
||||
}))
|
||||
|
||||
// 生成Excel文件
|
||||
//generateExcel(rs.data.response)
|
||||
} else {
|
||||
ElMessage.error(rs.data.message || '数据获取失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('请求失败: ' + error.message)
|
||||
} finally {
|
||||
downloadLoading.value = false
|
||||
loadingInstance?.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Excel生成函数
|
||||
const generateExcel = () => {
|
||||
let loadingInstance = null
|
||||
try {
|
||||
downloadLoading.value = true
|
||||
loadingInstance = ElLoading.service({
|
||||
lock: true,
|
||||
text: '数据加载中...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
const exportFields = ['DeviceName', 'MO', 'ClientID', 'MQTT_Login_Password', 'Key', 'Remark']
|
||||
|
||||
// 创建workbook
|
||||
const workbook = XLSX.utils.book_new()
|
||||
const worksheet = XLSX.utils.json_to_sheet(
|
||||
tableData.value.map(item => ({
|
||||
DeviceName: item.DeviceName,
|
||||
MO: item.MO,
|
||||
ClientID: item.ClientID,
|
||||
MQTT_Login_Password: item.MQTT_Login_Password,
|
||||
Key: item.Key,
|
||||
Remark: item.Remark
|
||||
}))
|
||||
)
|
||||
//const worksheet = XLSX.utils.json_to_sheet(tableData.value)
|
||||
|
||||
// 添加表头
|
||||
XLSX.utils.sheet_add_aoa(worksheet, [exportFields.map(field => {
|
||||
const col = columns.find(c => c.dataKey === field)
|
||||
return col ? col.title : field
|
||||
})], { origin: 'A1' })
|
||||
|
||||
// 设置列宽
|
||||
const colWidths = columns.filter(c => exportFields.includes(c.dataKey)).map(c => c.width / 8 || 20)
|
||||
worksheet['!cols'] = colWidths.map(w => ({ width: w }))
|
||||
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Device Info')
|
||||
XLSX.writeFile(workbook, `设备信息_${Date.now()}.xlsx`)
|
||||
|
||||
ElMessage.success('Excel文件生成成功')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('文件生成失败: ' + error.message)
|
||||
} finally {
|
||||
downloadLoading.value = false
|
||||
loadingInstance?.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取机型列表
|
||||
const getDevices = async () => {
|
||||
try {
|
||||
const rs = await $http.post('MachineTypes/GetMachineTypes', {
|
||||
IsAll: true,
|
||||
ID: 0,
|
||||
})
|
||||
//console.log(rs.data)
|
||||
if (rs.data.isok) {
|
||||
allDevices.value = rs.data.response
|
||||
.sort((a, b) => a.deviceTypeName.localeCompare(b.deviceTypeName))
|
||||
.map((d, index) => ({ ...d, oid: index })) // 添加唯一ID
|
||||
} else {
|
||||
ElMessage.error(rs.data.message)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('请求失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDevices()
|
||||
selectedDateRange.value = [
|
||||
dayjs().subtract(14, 'day').format('YYYY-MM-DD'),
|
||||
dayjs().format('YYYY-MM-DD')
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
const formatTime = (time) => {
|
||||
const date = new Date(time);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 12px;
|
||||
margin-left: 12px;
|
||||
height: calc(100vh - 160px); /* 保证容器有确定高度 */
|
||||
/* display: flex;*/
|
||||
/* flex-direction: column;*/
|
||||
}
|
||||
|
||||
.header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-table-v2 {
|
||||
flex-grow: 1;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.el-popover.el-popper {
|
||||
max-width: 400px;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.form-generator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.history-module {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.el-table-v2__header-cell) {
|
||||
font-weight: 600;
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
:deep(.el-table-v2__row-cell) {
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
20
src/pages/log/index.vue
Normal file
20
src/pages/log/index.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted } from 'vue';
|
||||
const $http = inject('$http')
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
555
src/pages/login/index.vue
Normal file
555
src/pages/login/index.vue
Normal file
@@ -0,0 +1,555 @@
|
||||
<template>
|
||||
<div style="height:100vh">
|
||||
<div class="container">
|
||||
<div class="logo-container">
|
||||
<!--<img src="../../../public/logobig.svg" style="width: 400px; height: auto; " />-->
|
||||
<div class="brand-logo">
|
||||
<div class="text-container">
|
||||
<div class="main-text">
|
||||
inHaos
|
||||
<span class="tm">TM</span>
|
||||
</div>
|
||||
<div class="sub-text">Everything done in house</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-container">
|
||||
<h1 style="margin:100px 0px 100px 0px; text-align:center">IoT设备管理平台</h1>
|
||||
<el-form :model="form"
|
||||
status-icon
|
||||
:disabled="isLocked"
|
||||
@submit.prevent="handleSubmit">
|
||||
<!-- 账号输入 -->
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="form.username"
|
||||
placeholder="请输入账号"
|
||||
clearable
|
||||
:prefix-icon="User" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
:prefix-icon="Lock" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 验证码区域 -->
|
||||
<el-form-item prop="code" class="captcha-container">
|
||||
<div class="captcha-input">
|
||||
<el-input v-model="form.code"
|
||||
placeholder="请输入验证码"
|
||||
:prefix-icon="Key"
|
||||
maxlength="4" />
|
||||
<img :src="captchaSrc"
|
||||
class="captcha-image"
|
||||
alt="验证码"
|
||||
@click="generateCaptcha" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 记住我 & 操作按钮 -->
|
||||
<el-form-item>
|
||||
<div class="form-actions">
|
||||
<el-checkbox v-model="form.remember">记住我</el-checkbox>
|
||||
<el-button type="primary"
|
||||
native-type="submit"
|
||||
:loading="loading"
|
||||
:disabled="submitDisabled">
|
||||
{{ isLocked ? `请等待${lockRemainTime}后重试` : '立即登录' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="errorAttempts > 0" class="error-tip">
|
||||
已错误尝试 {{ errorAttempts }} 次(5次后将锁定账号)
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted, computed, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User, Lock, Key } from '@element-plus/icons-vue'
|
||||
|
||||
|
||||
const checkLoginStatus = inject('checkLoginStatus');
|
||||
|
||||
const router = useRouter()
|
||||
const $http = inject('$http')
|
||||
|
||||
// 响应式状态
|
||||
const form = ref({
|
||||
username: localStorage.getItem('rememberedUsername') || '',
|
||||
password: localStorage.getItem('rememberedPassword') || '',
|
||||
remember: localStorage.getItem('rememberedUsername') ?
|
||||
Boolean(localStorage.getItem('rememberedUsername')) :
|
||||
true,
|
||||
code: ''
|
||||
})
|
||||
|
||||
|
||||
const isDarkMode = ref(window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
const loading = ref(false);
|
||||
const submitDisabled = ref(false);
|
||||
const isLocked = ref(false);
|
||||
const errorAttempts = ref(parseInt(localStorage.getItem('loginErrorAttempts')) || 0);
|
||||
const lockUntil = ref(parseInt(localStorage.getItem('loginLockUntil')) || 0);
|
||||
const captchaSrc = ref('');
|
||||
const captchaValue = ref('');
|
||||
|
||||
// 计算属性
|
||||
const lockRemainTime = computed(() => {
|
||||
if (!isLocked) return '00:00'
|
||||
const remain = (lockUntil.value - Date.now()) / 1000
|
||||
return `${Math.floor(remain / 60)}分${Math.floor(remain % 60)}秒`
|
||||
})
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.setItem('login', false)
|
||||
initAuthState()
|
||||
generateCaptcha()
|
||||
checkLoginStatus()
|
||||
checkAutoLoginStatus()
|
||||
})
|
||||
|
||||
// 方法实现
|
||||
const initAuthState = () => {
|
||||
if (lockUntil.value && Date.now() < lockUntil.value) {
|
||||
setupUnlockTimer(lockUntil.value - Date.now())
|
||||
}
|
||||
}
|
||||
|
||||
// 新增深色模式状态
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
isDarkMode.value = true;
|
||||
} else {
|
||||
isDarkMode.value = false;
|
||||
}
|
||||
|
||||
|
||||
const setupUnlockTimer = (duration) => {
|
||||
isLocked.value = true
|
||||
setTimeout(() => {
|
||||
isLocked.value = false
|
||||
errorAttempts.value = 0
|
||||
localStorage.removeItem('loginLockUntil')
|
||||
localStorage.removeItem('loginErrorAttempts')
|
||||
}, duration)
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
const generateCaptcha = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const chars = '0123456789';
|
||||
let captcha = '';
|
||||
canvas.width = 86;
|
||||
canvas.height = 30;
|
||||
|
||||
// 生成验证码字符串
|
||||
for (let i = 0; i < 4; i++) {
|
||||
captcha += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
captchaValue.value = captcha; // 存储验证码值用于验证
|
||||
|
||||
// 绘制背景
|
||||
ctx.fillStyle = isDarkMode.value ? '#242424' : '#FFFFFF';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 绘制噪点
|
||||
ctx.fillStyle = isDarkMode.value ? '#FFFFFF' : '#000000';
|
||||
for (let i = 0; i < 33; i++) {
|
||||
const x = Math.random() * canvas.width;
|
||||
const y = Math.random() * canvas.height;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 1, 0, Math.PI * 2, false);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// 绘制验证码
|
||||
ctx.font = '24px Arial';
|
||||
let xnd = 10;
|
||||
for (let i = 0; i < captcha.length; i++) {
|
||||
const angle = (Math.random() * 30) - 15; // -10度到+10度
|
||||
|
||||
// 保存当前状态
|
||||
ctx.save();
|
||||
|
||||
// 旋转并绘制文字
|
||||
ctx.translate(xnd, 20);
|
||||
ctx.rotate(angle * Math.PI / 180);
|
||||
ctx.fillText(captcha[i], 0, 0);
|
||||
|
||||
// 恢复状态
|
||||
ctx.restore();
|
||||
|
||||
// 更新下一个字符的x位置
|
||||
xnd += 15 + Math.abs(angle) / 2; // 根据角度调整字符间距
|
||||
}
|
||||
|
||||
// 添加5-10条干扰条纹
|
||||
ctx.strokeStyle = '#6F4A2F';
|
||||
const bug = Math.floor(Math.random() * 6) + 3
|
||||
for (let i = 0; i < bug; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
|
||||
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
|
||||
ctx.stroke();
|
||||
}
|
||||
// 将canvas转换为图片URL
|
||||
captchaSrc.value = canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (form.value.username == "WYSERVER") return
|
||||
if (isLocked.value || submitDisabled.value) return
|
||||
if (!validateForm()) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await $http.post('Login/Login', {
|
||||
username: form.value.username,
|
||||
password: form.value.password
|
||||
})
|
||||
|
||||
//console.log(response)
|
||||
handleLoginResponse(response.data)
|
||||
} catch (error) {
|
||||
handleLoginError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 新增表单验证方法
|
||||
const validateForm = () => {
|
||||
if (!form.value.username.trim()) {
|
||||
ElMessage.error('请输入账号!')
|
||||
return false
|
||||
}
|
||||
if (!form.value.password.trim()) {
|
||||
ElMessage.error('请输入密码!')
|
||||
return false
|
||||
}
|
||||
if (!form.value.code.trim()) {
|
||||
ElMessage.error('请输入验证码!')
|
||||
return false
|
||||
}
|
||||
if (form.value.code !== captchaValue.value) {
|
||||
ElMessage.error('验证码错误!')
|
||||
generateCaptcha()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
// 新增错误处理方法
|
||||
const handleLoginError = (error) => {
|
||||
console.error('登录请求失败:', error)
|
||||
ElMessage.error('网络请求失败,请检查网络连接')
|
||||
errorAttempts.value++
|
||||
}
|
||||
|
||||
|
||||
const handleLoginResponse = (data) => {
|
||||
if (data.isok) {
|
||||
handleLoginSuccess(data.response)
|
||||
} else {
|
||||
handleLoginFailure(data.message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoginSuccess = (userData) => {
|
||||
// 登录成功处理...
|
||||
errorAttempts.value = 0
|
||||
if (localStorage.getItem('ATTEMPTS_STORAGE_KEY')) {
|
||||
localStorage.removeItem(ATTEMPTS_STORAGE_KEY)
|
||||
}
|
||||
if (localStorage.getItem('LOCK_STORAGE_KEY')) {
|
||||
localStorage.removeItem(LOCK_STORAGE_KEY)
|
||||
}
|
||||
localStorage.removeItem('loginErrorAttempts')
|
||||
localStorage.setItem('lastLoginTimestamp', Date.now())
|
||||
// 存储token
|
||||
localStorage.setItem('token', userData.accessToken)
|
||||
localStorage.setItem('username', userData.userName)
|
||||
localStorage.setItem('login', true)
|
||||
localStorage.setItem('uid', userData.id)
|
||||
localStorage.setItem('permission', userData.permission)
|
||||
localStorage.setItem('realname', userData.realName)
|
||||
//localStorage.setItem('position', userData.position)
|
||||
//localStorage.setItem('comId', userData.comId)
|
||||
//localStorage.setItem('avatar', userData.avatar)
|
||||
|
||||
// 记住账号和密码
|
||||
if (form.value.remember) {
|
||||
localStorage.setItem('rememberedUsername', form.value.username)
|
||||
localStorage.setItem('rememberedPassword', form.value.password)
|
||||
} else {
|
||||
if (localStorage.getItem('rememberedUsername')) {
|
||||
localStorage.removeItem('rememberedPassword')
|
||||
}
|
||||
}
|
||||
ElMessage.success(`登录成功,欢迎:${userData.userName}!`)
|
||||
router.push('/home')
|
||||
}
|
||||
|
||||
const handleLoginFailure = (message) => {
|
||||
ElMessage.error(message)
|
||||
errorAttempts.value++
|
||||
localStorage.setItem('loginErrorAttempts', errorAttempts.value)
|
||||
|
||||
if (errorAttempts.value >= 5) {
|
||||
const lockUntil = Date.now() + 3600000
|
||||
lockUntil.value = lockUntil
|
||||
localStorage.setItem('loginLockUntil', lockUntil)
|
||||
setupUnlockTimer(3600000)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查7天内登录记录
|
||||
const checkAutoLoginStatus = () => {
|
||||
const lastLoginTimestamp = localStorage.getItem('lastLoginTimestamp')
|
||||
|
||||
// 检查是否在7天内登录过
|
||||
if (lastLoginTimestamp && (Date.now() - parseInt(lastLoginTimestamp)) < 7 * 24 * 60 * 60 * 1000) {
|
||||
// 如果是Admin账号或其它需要自动登录的账号
|
||||
// 自动填充验证码
|
||||
setTimeout(() => {
|
||||
form.value.code = captchaValue.value
|
||||
// 使用nextTick确保DOM更新后执行自动登录
|
||||
nextTick(() => {
|
||||
autoLogin()
|
||||
})
|
||||
}, 66)
|
||||
}
|
||||
}
|
||||
|
||||
// 自动登录功能
|
||||
const autoLogin = () => {
|
||||
if (!form.value.username || !form.value.password || !form.value.code) return
|
||||
handleSubmit()
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo-container,
|
||||
.login-container {
|
||||
flex: 1; /* 各占50% */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center; /* 水平居中 */
|
||||
justify-content: center; /* 垂直居中 */
|
||||
/*min-height: 100vh;*/
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*.login-container {
|
||||
background: var(--el-bg-color);*/ /* 保持与主题一致 */
|
||||
/*padding: 30px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
}*/
|
||||
|
||||
.login-title {
|
||||
margin: 2rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 基础样式保持桌面端布局 */
|
||||
/*.login-container {
|
||||
width: 400px;*/ /* 固定桌面端宽度 */
|
||||
/*margin: 0 auto;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
}*/
|
||||
|
||||
/* 外层容器样式 */
|
||||
div[style*="display: flex"] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 通用样式保持 */
|
||||
.captcha-container {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.captcha-input {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.captcha-image {
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-tip {
|
||||
color: var(--el-color-danger);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
div[style*="display: flex"] {
|
||||
flex-direction: column; /* 改为垂直布局 */
|
||||
padding: 100px;
|
||||
}
|
||||
|
||||
/* 图片容器调整 */
|
||||
div[style*="display: flex"] > div:first-child {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 图片样式调整 */
|
||||
img[src*="logobig.svg"] {
|
||||
width: 80% !important;
|
||||
max-width: 300px;
|
||||
margin: 0 auto !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 登录容器调整 */
|
||||
/*.login-container {
|
||||
width: 100% !important;
|
||||
margin: 30px auto 0;
|
||||
padding: 20px;
|
||||
box-shadow: none;
|
||||
}*/
|
||||
|
||||
/* 标题调整 */
|
||||
/*.login-container h1 {
|
||||
margin: 50px 0 !important;
|
||||
font-size: 1.5rem;
|
||||
}*/
|
||||
.main-text {
|
||||
font-size: 66px;
|
||||
}
|
||||
/* 验证码输入容器 */
|
||||
.captcha-input {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 验证码图片 */
|
||||
.captcha-image {
|
||||
width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo-container,
|
||||
.login-container {
|
||||
width: 100%;
|
||||
min-height: auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
max-width: 280px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
cursor: default; /* 禁用文本选择光标 */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.symbol-container {
|
||||
position: relative;
|
||||
margin-right: 25px;
|
||||
pointer-events: none; /* 禁用图形部分交互 */
|
||||
}
|
||||
|
||||
.text-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
pointer-events: none; /* 禁用文字部分交互 */
|
||||
}
|
||||
|
||||
.main-text {
|
||||
font-size: 88px;
|
||||
font-weight: 900; /* 加粗程度提升 */
|
||||
font-style: italic; /* 新增斜体效果 */
|
||||
margin-bottom: 5px;
|
||||
font-family: Arial, sans-serif; /* 确保斜体生效 */
|
||||
transform: skewX(-10deg);
|
||||
}
|
||||
|
||||
/* 其他保持原有样式 */
|
||||
.tm {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: -25px; /* 微调TM位置适配斜体 */
|
||||
font-style: normal; /* 新增斜体效果 */
|
||||
vertical-align: super;
|
||||
transform: skewX(10deg);
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
424
src/pages/modelmanage/index.vue
Normal file
424
src/pages/modelmanage/index.vue
Normal file
@@ -0,0 +1,424 @@
|
||||
<template>
|
||||
<div class="device-management">
|
||||
<!-- 操作栏 -->
|
||||
|
||||
<div class="header">
|
||||
<el-button type="primary" @click="handleAdd" :icon="Plus">添加机型</el-button>
|
||||
<el-input v-model="searchKeyword"
|
||||
placeholder="搜索机型名称/详细信息"
|
||||
style="width: 300px; margin-left: 15px"
|
||||
clearable>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<!-- 机型表格 -->
|
||||
<el-table :data="filteredDevices"
|
||||
border
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
empty-text="暂无数据">
|
||||
<el-table-column label="操作" :width="105">
|
||||
<template #default="{ row }">
|
||||
<el-button type="info" :icon="Edit" circle plain @click="handleEdit(row)" />
|
||||
<el-button type="danger" :icon="Delete" circle plain @click="handleDelete(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deviceTypeName" label="机型" width="180" sortable show-overflow-tooltip />
|
||||
<el-table-column prop="connTypeName"
|
||||
label="通讯方式"
|
||||
width="120"
|
||||
:filters="connTypeFilters"
|
||||
:filter-method="filterConnType">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="success">{{ row.connTypeName }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="deviceClass"
|
||||
label="机型类别"
|
||||
width="140"
|
||||
:filters="deviceClassFilters"
|
||||
:filter-method="filterDeviceClass">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="warning">{{ row.deviceClass }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deviceInfo" label="详细信息" />
|
||||
|
||||
</el-table>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑机型' : '添加机型'"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
width="800px">
|
||||
<el-form :model="form"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
label-width="120px"
|
||||
label-position="right">
|
||||
<el-form-item label="机型" prop="deviceTypeName">
|
||||
<el-input v-model="form.deviceTypeName" placeholder="请输入机型名称" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 修改为下拉选择 -->
|
||||
<el-form-item label="通讯方式" prop="connTypeName">
|
||||
<el-select v-model="form.connTypeName" placeholder="请选择通讯方式" clearable>
|
||||
<el-option v-for="item in communicatList"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 新增机型类别下拉 -->
|
||||
<el-form-item label="机型类别" prop="deviceClass">
|
||||
<el-select v-model="form.deviceClass" placeholder="请选择机型类别" clearable>
|
||||
<el-option v-for="item in typeList"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 在el-dialog中添加这个表单项 -->
|
||||
<el-form-item label="详细信息" prop="deviceInfo">
|
||||
<el-input v-model="form.deviceInfo"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 6, maxRows: 10 }"
|
||||
placeholder="请输入设备详细信息"
|
||||
show-word-limit
|
||||
maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, inject, watch, computed } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Delete, Edit, UploadFilled, Search } from '@element-plus/icons-vue';
|
||||
|
||||
const $http = inject('$http');
|
||||
const ajaxfile = inject('ajaxfile');
|
||||
const config = inject('config');
|
||||
|
||||
// 数据列表
|
||||
const allDevices = ref([]);
|
||||
const loading = ref(false);
|
||||
const companies = ref([]);
|
||||
|
||||
const nowDevice = localStorage.getItem('deviceTypeName');
|
||||
// 对话框相关
|
||||
const dialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const currentDeviceId = ref(null);
|
||||
const formRef = ref(null);
|
||||
|
||||
|
||||
// 表单数据
|
||||
const defaultForm = {
|
||||
deviceTypeName: '',
|
||||
connTypeName: '',
|
||||
deviceTypeName: '',
|
||||
connTypeName: '',
|
||||
deviceClass: '',
|
||||
deviceInfo: ''
|
||||
};
|
||||
|
||||
const form = reactive({ ...defaultForm });
|
||||
|
||||
// 验证规则
|
||||
const rules = reactive({
|
||||
deviceTypeName: [
|
||||
{ required: true, message: '机型不可为空', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在2到50个字符', trigger: 'blur' }
|
||||
],
|
||||
connTypeName: [
|
||||
{ required: true, message: '请选择通讯方式', trigger: 'change' }
|
||||
],
|
||||
deviceClass: [
|
||||
{ required: true, message: '请选择机型类别', trigger: 'change' }
|
||||
],
|
||||
deviceInfo: [
|
||||
{ max: 500, message: '不能超过500个字符', trigger: 'blur' }
|
||||
]
|
||||
});
|
||||
// 新增搜索相关逻辑
|
||||
const searchKeyword = ref('');
|
||||
const connTypeFilters = ref([]);
|
||||
const deviceClassFilters = ref([]);
|
||||
const connTypeFilterValues = ref([]);
|
||||
const deviceClassFilterValues = ref([]);
|
||||
// 过滤后的数据
|
||||
const filteredDevices = computed(() => {
|
||||
return allDevices.value.filter(device => {
|
||||
// 处理通讯方式筛选
|
||||
const connTypeMatch = connTypeFilterValues.value.length === 0 ||
|
||||
connTypeFilterValues.value.includes(device.connTypeName);
|
||||
|
||||
// 处理机型类别筛选
|
||||
const deviceClassMatch = deviceClassFilterValues.value.length === 0 ||
|
||||
deviceClassFilterValues.value.includes(device.deviceClass);
|
||||
|
||||
// 处理关键词搜索
|
||||
const keywordMatch = !searchKeyword.value ||
|
||||
device.deviceTypeName?.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
device.deviceInfo?.toLowerCase().includes(searchKeyword.value.toLowerCase());
|
||||
|
||||
return connTypeMatch && deviceClassMatch && keywordMatch;
|
||||
});
|
||||
});
|
||||
|
||||
// 列筛选相关
|
||||
const currentConnType = ref('');
|
||||
const currentDeviceClass = ref('');
|
||||
|
||||
const filterConnType = (value, row) => {
|
||||
return value ? row.connTypeName === value : true;
|
||||
};
|
||||
|
||||
const filterDeviceClass = (value, row) => {
|
||||
return value ? row.deviceClass === value : true;
|
||||
};
|
||||
|
||||
// 初始化筛选器选项
|
||||
const initFilters = () => {
|
||||
connTypeFilters.value = [...new Set(allDevices.value.map(d => d.connTypeName))].map(v => ({
|
||||
text: v,
|
||||
value: v
|
||||
}));
|
||||
|
||||
deviceClassFilters.value = [...new Set(allDevices.value.map(d => d.deviceClass))].map(v => ({
|
||||
text: v,
|
||||
value: v
|
||||
}));
|
||||
};
|
||||
|
||||
const communicatList = ref([]);
|
||||
const typeList = ref([]);
|
||||
// 获取列表
|
||||
const getCommunicat = async () => {
|
||||
try {
|
||||
const rs = await $http.post('ConfigPY/GetSingleValue', {
|
||||
VarName: "通讯方式",
|
||||
});
|
||||
communicatList.value = JSON.parse(rs.data.response);
|
||||
// 生成筛选选项
|
||||
connTypeFilters.value = [...new Set(communicatList.value)].map(item => ({
|
||||
text: item,
|
||||
value: item
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取通讯方式失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getType = async () => {
|
||||
try {
|
||||
const rs = await $http.post('ConfigPY/GetSingleValue', {
|
||||
VarName: "机型类型",
|
||||
});
|
||||
typeList.value = JSON.parse(rs.data.response);
|
||||
// 生成筛选选项
|
||||
deviceClassFilters.value = [...new Set(typeList.value)].map(item => ({
|
||||
text: item,
|
||||
value: item
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取机型类别失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取机型列表
|
||||
const getDevices = async () => {
|
||||
const rs = await $http.post('MachineTypes/GetMachineTypes', {
|
||||
IsAll: true,
|
||||
ID: 0,
|
||||
});
|
||||
//console.log(rs.data.response);
|
||||
if (rs.data.isok) {
|
||||
allDevices.value = rs.data.response.sort((a, b) => a.deviceTypeName.localeCompare(b.deviceTypeName));
|
||||
initFilters();
|
||||
} else {
|
||||
ElMessage.error(rs.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 修改提交方法
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
|
||||
const payload = {
|
||||
...form,
|
||||
// 保持和接口一致的字段名
|
||||
deviceClass: form.deviceClass || ''
|
||||
};
|
||||
|
||||
if (isEdit.value) payload.id = currentDeviceId.value;
|
||||
|
||||
const api = `MachineTypes/${isEdit.value ? 'EditMachineType' : 'AddMachineType'}`;
|
||||
const rs = await $http.post(api, payload);
|
||||
|
||||
if (rs.data.isok) {
|
||||
ElMessage.success(`${isEdit.value ? '修改' : '添加'}成功`);
|
||||
await getDevices();
|
||||
dialogVisible.value = false;
|
||||
} else {
|
||||
ElMessage.error(rs.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 修改打开对话框逻辑
|
||||
const handleAdd = () => {
|
||||
Object.assign(form, defaultForm);
|
||||
isEdit.value = false;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEdit = (row) => {
|
||||
Object.assign(form, row);
|
||||
isEdit.value = true;
|
||||
currentDeviceId.value = row.id;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除机型
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定删除该机型吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const rs = await $http.post('MachineTypes/DeleteMachineType', { id: row.id });
|
||||
if (rs.data.isok) {
|
||||
ElMessage.success('删除成功');
|
||||
await getDevices();
|
||||
} else {
|
||||
ElMessage.error(rs.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败');
|
||||
console.error(error);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getDevices()
|
||||
getCommunicat()
|
||||
getType()
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.device-management {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 拖拽区域样式 */
|
||||
.avatar-uploader :deep(.el-upload-dragger) {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
padding: 20px;
|
||||
border-radius: 5%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.avatar-uploader:hover :deep(.el-upload-dragger) {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* 头像预览样式 */
|
||||
.avatar-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 5%;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.avatar-mask:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 图标和文字样式 */
|
||||
.el-icon--upload {
|
||||
font-size: 36px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.el-upload__text em {
|
||||
color: var(--el-color-primary);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
82
src/pages/recorded/NT318V.vue
Normal file
82
src/pages/recorded/NT318V.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="form-container">
|
||||
<!-- 表单标题 -->
|
||||
<h3 class="form-title">NT-318V 设备记录</h3>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted, watch } from 'vue'
|
||||
import { ElMessage, ElLoading, ElPopover } from 'element-plus'
|
||||
|
||||
// 表单数据模型
|
||||
const formData = ref({
|
||||
deviceName: '',
|
||||
ipAddress: '',
|
||||
deviceModel: '',
|
||||
isActive: true,
|
||||
// 这里可以添加更多表单字段
|
||||
})
|
||||
|
||||
// 表单引用
|
||||
const deviceForm = ref(null)
|
||||
|
||||
// 获取表单初始数据(模拟)
|
||||
const fetchFormData = async () => {
|
||||
try {
|
||||
// TODO: 这里替换为实际的API调用
|
||||
// const response = await api.getDeviceConfig(selectedDeviceId)
|
||||
// formData.value = response.data
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取表单数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
// TODO: 替换为实际的提交逻辑
|
||||
console.log('提交的表单数据:', formData.value)
|
||||
|
||||
// 模拟API提交
|
||||
// await api.submitDeviceConfig(formData.value)
|
||||
|
||||
// 成功提示
|
||||
ElMessage.success('配置已成功保存')
|
||||
} catch (error) {
|
||||
console.error('表单提交失败:', error)
|
||||
ElMessage.error('提交失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
if (deviceForm.value) {
|
||||
deviceForm.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchFormData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-container {
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin-bottom: 24px;
|
||||
color: var(--el-text-color-primary);
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
122
src/pages/recorded/NT530R.vue
Normal file
122
src/pages/recorded/NT530R.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="form-container">
|
||||
<!-- 表单标题 -->
|
||||
<h3 class="form-title">NT530R 设备配置</h3>
|
||||
|
||||
<!-- 表单内容区 -->
|
||||
<el-form ref="deviceForm"
|
||||
:model="formData"
|
||||
label-position="top"
|
||||
label-width="120px">
|
||||
<el-form-item label="设备名称">
|
||||
<el-input v-model="formData.deviceName" placeholder="请输入设备名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="IP地址">
|
||||
<el-input v-model="formData.ipAddress" placeholder="请输入IP地址" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="设备型号">
|
||||
<el-select v-model="formData.deviceModel" placeholder="请选择型号">
|
||||
<el-option label="NT530R-A" value="A" />
|
||||
<el-option label="NT530R-B" value="B" />
|
||||
<el-option label="NT530R-Pro" value="Pro" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="formData.isActive" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 更多表单字段 -->
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm">提 交</el-button>
|
||||
<el-button @click="resetForm">重 置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted, watch } from 'vue'
|
||||
import { ElMessage, ElLoading, ElPopover } from 'element-plus'
|
||||
|
||||
// 表单数据模型
|
||||
const formData = ref({
|
||||
deviceName: '',
|
||||
ipAddress: '',
|
||||
deviceModel: '',
|
||||
isActive: true,
|
||||
// 这里可以添加更多表单字段
|
||||
})
|
||||
|
||||
// 表单引用
|
||||
const deviceForm = ref(null)
|
||||
|
||||
// 获取表单初始数据(模拟)
|
||||
const fetchFormData = async () => {
|
||||
try {
|
||||
// TODO: 这里替换为实际的API调用
|
||||
// const response = await api.getDeviceConfig(selectedDeviceId)
|
||||
// formData.value = response.data
|
||||
|
||||
// 模拟API延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
|
||||
// 模拟数据
|
||||
formData.value = {
|
||||
deviceName: 'NT530R-001',
|
||||
ipAddress: '192.168.1.100',
|
||||
deviceModel: 'Pro',
|
||||
isActive: true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取表单数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
// TODO: 替换为实际的提交逻辑
|
||||
console.log('提交的表单数据:', formData.value)
|
||||
|
||||
// 模拟API提交
|
||||
// await api.submitDeviceConfig(formData.value)
|
||||
|
||||
// 成功提示
|
||||
ElMessage.success('配置已成功保存')
|
||||
} catch (error) {
|
||||
console.error('表单提交失败:', error)
|
||||
ElMessage.error('提交失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
if (deviceForm.value) {
|
||||
deviceForm.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchFormData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-container {
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin-bottom: 24px;
|
||||
color: var(--el-text-color-primary);
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
162
src/pages/recorded/index.vue
Normal file
162
src/pages/recorded/index.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<el-select v-model="selectedDeviceId"
|
||||
placeholder="请选择机型"
|
||||
filterable
|
||||
size="large"
|
||||
@change="handleDeviceChange"
|
||||
style="width: 736px;">
|
||||
<el-option v-for="device in allDevices"
|
||||
:key="device.id"
|
||||
:label="device.deviceTypeName"
|
||||
:value="device.id">
|
||||
<span style="float: left; font-weight: 700">
|
||||
{{ device.deviceTypeName }}
|
||||
</span>
|
||||
<span style="float: right; color: var(--el-text-color-secondary); font-size: 13px; line-height: 1.2;
|
||||
margin-top: 1px;margin-left: 8px;">
|
||||
<span style="display: block;">
|
||||
<span style="font-weight: 600;">
|
||||
{{ device.deviceClass }}
|
||||
</span>
|
||||
</span>
|
||||
<span style="display:block;width:220px">
|
||||
{{ truncateText(device.deviceInfo) }}
|
||||
</span>
|
||||
</span>
|
||||
<span style="float: right;">
|
||||
<el-tag type="success">
|
||||
{{ device.connTypeName }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<!-- 动态表单组件区域 -->
|
||||
<div class="form-container">
|
||||
<div v-if="currentFormComponent">
|
||||
<component :is="currentFormComponent" :key="selectedDeviceId" />
|
||||
</div>
|
||||
<div v-else class="form-placeholder">
|
||||
<el-empty description="请选择一个设备类型" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted, watch, computed, shallowRef, defineAsyncComponent } from 'vue'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
const $http = inject('$http')
|
||||
|
||||
|
||||
const selectedDeviceId = ref('')
|
||||
const allDevices = ref([])
|
||||
const deviceFormMap = ref({}) // 存储设备ID到组件的映射
|
||||
const currentFormComponent = shallowRef(null) // 当前加载的表单组件
|
||||
|
||||
|
||||
// 初始化设备-组件映射表
|
||||
const initComponentMap = () => {
|
||||
deviceFormMap.value = {}
|
||||
allDevices.value.forEach(device => {
|
||||
deviceFormMap.value[device.id] = cleanFileName(device.deviceTypeName)
|
||||
})
|
||||
}
|
||||
|
||||
// 截断长文本
|
||||
const truncateText = (text) => {
|
||||
const maxLen = 30
|
||||
let total = 0
|
||||
const result = []
|
||||
|
||||
for (const char of text) {
|
||||
const code = char.charCodeAt(0)
|
||||
total += code > 255 ? 2 : 1
|
||||
if (total > maxLen) break
|
||||
result.push(char)
|
||||
}
|
||||
|
||||
return result.join('') + (total > maxLen ? '...' : '')
|
||||
}
|
||||
|
||||
// 处理设备选择变化
|
||||
const handleDeviceChange = (deviceId) => {
|
||||
// 从映射表中获取组件名称
|
||||
const componentName = deviceFormMap.value[deviceId]
|
||||
if (componentName) {
|
||||
// 动态加载对应组件
|
||||
loadFormComponent(componentName)
|
||||
} else {
|
||||
currentFormComponent.value = null
|
||||
ElMessage.warning('该设备类型暂无可用表单')
|
||||
}
|
||||
}
|
||||
|
||||
// 动态加载表单组件
|
||||
const loadFormComponent = async (componentName) => {
|
||||
try {
|
||||
// 动态导入组件(实际路径需要根据项目结构调整)
|
||||
/*const component = defineAsyncComponent(() =>
|
||||
import(`./${componentName}.vue`)
|
||||
)*/
|
||||
// 等待组件加载
|
||||
/*const loadedComponent = await component()
|
||||
currentFormComponent.value = loadedComponent*/
|
||||
const module = await import(`./${componentName}.vue`)
|
||||
currentFormComponent.value = module.default
|
||||
} catch (error) {
|
||||
console.error('组件加载失败:', error)
|
||||
ElMessage.error(`无法加载表单组件: ${componentName}`)
|
||||
currentFormComponent.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取机型列表
|
||||
const getDevices = async () => {
|
||||
try {
|
||||
const rs = await $http.post('MachineTypes/GetMachineTypes', {
|
||||
IsAll: true,
|
||||
ID: 0,
|
||||
})
|
||||
//console.log(rs.data)
|
||||
if (rs.data.isok) {
|
||||
allDevices.value = rs.data.response
|
||||
.sort((a, b) => a.deviceTypeName.localeCompare(b.deviceTypeName))
|
||||
.map((d, index) => ({ ...d, oid: index })) // 添加唯一ID
|
||||
console.log(allDevices.value)
|
||||
initComponentMap()
|
||||
} else {
|
||||
ElMessage.error(rs.data.message)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('请求失败')
|
||||
}
|
||||
}
|
||||
// 清洗文件名 - 只保留字母、汉字和数字
|
||||
const cleanFileName = (name) => {
|
||||
return name.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDevices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 12px;
|
||||
margin-left: 12px;
|
||||
height: calc(100vh - 160px);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
20
src/pages/scopemanage/index.vue
Normal file
20
src/pages/scopemanage/index.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted } from 'vue';
|
||||
const $http = inject('$http')
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
730
src/pages/tcpiptest/index.vue
Normal file
730
src/pages/tcpiptest/index.vue
Normal file
@@ -0,0 +1,730 @@
|
||||
<template>
|
||||
<el-row :gutter="20" class="page-container">
|
||||
<!-- 左侧IP列表 -->
|
||||
<el-col :span="5">
|
||||
<el-card class="ip-list">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="header-top">
|
||||
<el-row :gutter="10" style="width:100%">
|
||||
<el-col :span="4">IP列表</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button type="primary"
|
||||
text
|
||||
@click="handleManualRefresh"
|
||||
:loading="loading"
|
||||
icon="Refresh">
|
||||
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-input v-model="filterText"
|
||||
placeholder="输入IP或ID筛选"
|
||||
clearable
|
||||
class="filter-input" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="radio-list-container">
|
||||
<el-radio-group v-model="selectedEndpoint" v-loading="loading" class="radio-list">
|
||||
<template v-if="!loading">
|
||||
<el-radio v-for="item in filteredIpList" :key="item.endpoint" :label="item.endpoint" class="radio-item">
|
||||
({{ item.clientid }}) {{ item.endpoint }}
|
||||
</el-radio>
|
||||
<div v-if="filteredIpList.length === 0 && !ipListError" class="empty-tip">
|
||||
当前暂无TCP/IP地址
|
||||
</div>
|
||||
<div v-if="ipListError" class="error-tip">
|
||||
当前网络异常,请刷新重试
|
||||
</div>
|
||||
</template>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧操作区域 -->
|
||||
<el-col :span="19">
|
||||
<el-card class="operation-panel">
|
||||
<!-- 请求表单 -->
|
||||
<div class="send-form">
|
||||
<el-form :model="formData"
|
||||
label-width="88px"
|
||||
:disabled="!selectedEndpoint">
|
||||
<el-form-item label="目标IP" prop="ip">
|
||||
<el-tag type="success" size="large" v-if="selectedEndpoint" closable @close="closeTag">{{ selectedEndpoint }}</el-tag>
|
||||
<el-tag type="success" size="large" v-else disabled>请在左侧选择IP</el-tag>
|
||||
<div style="margin-left:6px"></div>
|
||||
<el-button type="warning" plain
|
||||
:disabled="!selectedEndpoint"
|
||||
:loading="sending"
|
||||
@click="forcedShutdown">
|
||||
断开链接
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="参数类型"
|
||||
prop="params"
|
||||
:rules="dataMode === 'json' ? jsonRules : hexRules">
|
||||
<div class="param-header">
|
||||
<!--<el-button-group v-model="dataMode">
|
||||
<el-button type="info" :plain="dataMode !== 'json'" @click="switchMode('json')">
|
||||
JSON模式
|
||||
</el-button>
|
||||
<el-button type="info" :plain="dataMode !== 'hex'" @click="switchMode('hex')">
|
||||
HEX模式
|
||||
</el-button>
|
||||
</el-button-group>-->
|
||||
<!--<el-tag type="info" size="large" effect="dark">HEX模式</el-tag>-->
|
||||
<span class="mode-tip">{{ modeTips[dataMode] }}</span>
|
||||
</div>
|
||||
<el-input v-model="formData.params"
|
||||
type="textarea"
|
||||
:rows="9"
|
||||
:placeholder="modePlaceholder[dataMode]" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div style="width:100%; text-align:center">
|
||||
<el-button type="success"
|
||||
:disabled="!selectedEndpoint"
|
||||
:loading="sending"
|
||||
@click="handleSubmit">
|
||||
发送请求
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- 响应结果 -->
|
||||
<div class="response-area">
|
||||
<el-divider content-position="left">请求历史记录</el-divider>
|
||||
<el-table :data="responseList"
|
||||
style="width: 100%"
|
||||
border
|
||||
:row-class-name="tableRowClassName"
|
||||
height="415px">
|
||||
<!-- 新增状态列 -->
|
||||
<el-table-column label="方向" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.msgtype == 1" type="primary" effect="dark">下行</el-tag>
|
||||
<el-tag v-else type="warning" effect="dark">上行</el-tag>
|
||||
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.isSuccess" type="success" effect="plain">成功</el-tag>
|
||||
<el-tag v-else type="danger" effect="plain">失败</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="sendTime" label="时间" width="180" />
|
||||
|
||||
|
||||
<!-- HEX值列 -->
|
||||
<el-table-column prop="sendData" label="HEX格式" width="280">
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="top-start"
|
||||
width="600"
|
||||
trigger="click">
|
||||
<template #reference>
|
||||
<div class="cell-content truncate">{{ truncateText(row.sendData, 30) }}</div>
|
||||
</template>
|
||||
<pre class="popover-content">{{ formatJson(row.sendData) }}</pre>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- JSON列 -->
|
||||
<el-table-column prop="sendData" label="ASCII格式">
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="top-start"
|
||||
width="600"
|
||||
trigger="click">
|
||||
<template #reference>
|
||||
<div class="cell-content truncate">{{ hexToAscii(truncateText(row.sendData, 30)) }}</div>
|
||||
</template>
|
||||
<pre class="popover-content">{{ hexToAscii(row.sendData) }}</pre>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
|
||||
<!--<el-table-column prop="responseTime" label="接收时间" width="180" />-->
|
||||
<!-- 返回值列 -->
|
||||
<!--<el-table-column prop="responseData">
|
||||
<template #header>
|
||||
<div class="header-content">
|
||||
<span>接收数据 / </span>
|
||||
<el-tag v-if="dataMode === 'json'" type="info" size="small" effect="dark">JSON</el-tag>
|
||||
<el-tag v-else type="info" size="small" effect="dark">HEX</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="top-start"
|
||||
width="800"
|
||||
trigger="click">
|
||||
<template #reference>
|
||||
<div class="cell-content truncate">{{ row.responseData }}</div>
|
||||
</template>
|
||||
<pre class="popover-content">{{ formatJson(row.responseData) }}</pre>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>-->
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted, computed, watch, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const $http = inject('$http')
|
||||
const ajax = inject('ajax')
|
||||
const config = inject('config')
|
||||
const client = inject('client')
|
||||
const mqttlink = inject('mqttlink')
|
||||
const mqttmsg = inject('mqttmsg')
|
||||
|
||||
// IP列表相关
|
||||
const ipList = ref([])
|
||||
const ipListError = ref(false)
|
||||
const selectedEndpoint = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单相关
|
||||
const formData = ref({
|
||||
params: ''
|
||||
})
|
||||
const dataMode = ref('hex')
|
||||
const sending = ref(false)
|
||||
|
||||
// 响应结果
|
||||
const responseText = ref('')
|
||||
// filterText响应式变量
|
||||
const filterText = ref('')
|
||||
|
||||
// filteredIpList计算属性
|
||||
const filteredIpList = computed(() => {
|
||||
const searchText = filterText.value.toLowerCase()
|
||||
return ipList.value.filter(item =>
|
||||
item.endpoint.toLowerCase().includes(searchText) ||
|
||||
String(item.clientid).includes(searchText)
|
||||
)
|
||||
})
|
||||
|
||||
watch(() => selectedEndpoint.value, (newVal, oldVal) => {
|
||||
if (newVal) {
|
||||
mqttlink(newVal, oldVal)
|
||||
ElMessage.success(`已切换到 ${newVal}`)
|
||||
}
|
||||
responseList.value = []
|
||||
})
|
||||
|
||||
// mqtt消息监听
|
||||
watch(() => mqttmsg.value, (newVal) => {
|
||||
if (newVal) {
|
||||
const rawHex = newVal.toString().replace(/\s/g, '') // 原始HEX去空格
|
||||
const processedData = convertResponseData(rawHex, dataMode.value)
|
||||
|
||||
// 查找最近一条服务器端请求
|
||||
const lastRequest = responseList.value.find(
|
||||
item => item.msgtype === 1 && !item.sendData
|
||||
)
|
||||
|
||||
const record = lastRequest || {
|
||||
msgtype: 2,
|
||||
sendTime: formatDateTime(),
|
||||
isSuccess: true,
|
||||
/* sendData: '-',
|
||||
responseTime: formatDateTime(),*/
|
||||
|
||||
}
|
||||
|
||||
Object.assign(record, {
|
||||
rawData: rawHex, // 保存原始HEX数据
|
||||
sendData: processedData // 存储处理后的数据
|
||||
})
|
||||
|
||||
if (!lastRequest) {
|
||||
responseList.value.unshift(record)
|
||||
}
|
||||
responseList.value = [...responseList.value]
|
||||
}
|
||||
})
|
||||
|
||||
// 在HEX模式输入时自动格式化为大写+空格
|
||||
watch(() => formData.value.params, (newVal) => {
|
||||
if (dataMode.value === 'hex') {
|
||||
// 过滤非十六进制字符
|
||||
let filtered = newVal.replace(/[^0-9a-fA-F]/g, '')
|
||||
// 添加空格分隔
|
||||
filtered = filtered.match(/.{1,2}/g)?.join(' ') || ''
|
||||
// 更新为大写带空格
|
||||
formData.value.params = filtered.toUpperCase()
|
||||
}
|
||||
})
|
||||
|
||||
// 新增十六进制处理工具函数
|
||||
const hexToFormatted = (hexStr) => {
|
||||
// 过滤无效字符并添加空格分隔
|
||||
const filtered = hexStr.replace(/[^0-9a-fA-F]/g, '')
|
||||
return filtered.match(/.{1,2}/g)?.join(' ').toUpperCase() || ''
|
||||
}
|
||||
|
||||
const hexToAscii = (hexStr) => {
|
||||
try {
|
||||
// 移除所有非十六进制字符
|
||||
const cleanHex = hexStr.replace(/[^0-9a-fA-F]/g, '')
|
||||
|
||||
// 处理奇数长度情况
|
||||
if (cleanHex.length % 2 !== 0) {
|
||||
console.warn('十六进制字符串长度异常')
|
||||
return hexStr
|
||||
}
|
||||
|
||||
// 转换为ASCII
|
||||
let str = ''
|
||||
for (let i = 0; i < cleanHex.length; i += 2) {
|
||||
const byte = parseInt(cleanHex.substr(i, 2), 16)
|
||||
str += String.fromCharCode(byte)
|
||||
}
|
||||
|
||||
// 尝试解析JSON
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(str), null, 2)
|
||||
} catch {
|
||||
return str
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('十六进制转换失败:', error)
|
||||
return hexStr
|
||||
}
|
||||
}
|
||||
// 验证规则
|
||||
const jsonRules = [{
|
||||
required: true,
|
||||
message: '无效的JSON格式',
|
||||
validator: (_, value, cb) => {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
cb()
|
||||
} catch (e) {
|
||||
cb(new Error('无效的JSON格式'))
|
||||
}
|
||||
}
|
||||
}]
|
||||
const hexRules = [{
|
||||
required: true,
|
||||
message: '需要有效的十六进制(偶数长度)',
|
||||
validator: (_, value, cb) => {
|
||||
// 允许空格的验证
|
||||
const filtered = value.replace(/\s/g, '')
|
||||
const isValid = /^[0-9a-fA-F]*$/.test(filtered) && filtered.length % 2 === 0
|
||||
isValid ? cb() : cb(new Error('需要有效的十六进制(偶数长度)'))
|
||||
}
|
||||
}]
|
||||
const modeTips = {
|
||||
json: '请输入标准JSON格式',
|
||||
hex: '请输入十六进制字符串'
|
||||
}
|
||||
|
||||
const modePlaceholder = {
|
||||
json: '示例:\n{\n "a": "a",\n "b": "b"\n}',
|
||||
hex: '示例:0E 00 00 00 1C FF'
|
||||
}
|
||||
|
||||
// 修改模式切换方法
|
||||
const switchMode = (mode) => {
|
||||
if (mode === dataMode.value) return
|
||||
dataMode.value = mode
|
||||
formData.value.params = ''
|
||||
|
||||
// 转换所有历史数据
|
||||
responseList.value = responseList.value.map(item => ({
|
||||
...item,
|
||||
// 保留原始数据重新转换
|
||||
sendData: item.rawData
|
||||
? convertResponseData(item.rawData, mode)
|
||||
: item.sendData
|
||||
}))
|
||||
}
|
||||
|
||||
// 定时器引用
|
||||
let refreshTimer = null
|
||||
|
||||
// 修改后的getTcpip函数
|
||||
const getTcpip = async () => {
|
||||
try {
|
||||
//loading.value = true
|
||||
ipListError.value = false
|
||||
const payload = {
|
||||
Cmd: "get_tcp_info",
|
||||
Parameterlist: {}
|
||||
}
|
||||
const response = await ajax(config.Tcpip + 'sockettcp/Get_all_TCPClients', payload)
|
||||
console.log(response)
|
||||
// 处理响应数据
|
||||
let newList = []
|
||||
if (Array.isArray(response)) {
|
||||
newList = response
|
||||
} else if (response && typeof response === 'object') {
|
||||
newList = Object.values(response)
|
||||
} else {
|
||||
newList = JSON.parse(response)
|
||||
}
|
||||
|
||||
// 排序后比较(根据需求可以调整比较方式)
|
||||
const isSame = JSON.stringify([...ipList.value].sort()) === JSON.stringify([...newList].sort())
|
||||
|
||||
if (!isSame) {
|
||||
ipList.value = newList
|
||||
ElMessage.success('IP列表已更新')
|
||||
}
|
||||
if (ipList.value.length === 0) {
|
||||
ElMessage.info('当前没有可用的TCP/IP地址')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取IP列表失败:', error)
|
||||
ipListError.value = true
|
||||
ElMessage.error('获取IP列表失败,请检查网络连接')
|
||||
} finally {
|
||||
//loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 断开链接
|
||||
const forcedShutdown = async () => {
|
||||
const payload = {
|
||||
Cmd: "force_close",
|
||||
Parameterlist: {
|
||||
endpoint: selectedEndpoint.value,
|
||||
data: "00 00"
|
||||
}
|
||||
}
|
||||
|
||||
const response = await ajax(config.Tcpip + 'sockettcp/send_data_to_target', payload)
|
||||
if (response.isok == true) {
|
||||
setTimeout(() => {
|
||||
closeTag()
|
||||
handleManualRefresh()
|
||||
ElMessage.success('成功断开链接')
|
||||
}, 233)
|
||||
} else {
|
||||
ElMessage.error('断开失败:' + response)
|
||||
}
|
||||
}
|
||||
|
||||
// 修改后的提交处理
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedEndpoint.value) {
|
||||
ElMessage.warning('请先选择IP地址')
|
||||
return
|
||||
}
|
||||
|
||||
const requestRecord = {
|
||||
sendTime: formatDateTime(),
|
||||
sendData: formData.value.params,
|
||||
/* responseTime: null,
|
||||
responseData: null,*/
|
||||
isSuccess: false,
|
||||
msgtype: 1,
|
||||
}
|
||||
|
||||
try {
|
||||
sending.value = true
|
||||
// 先添加记录
|
||||
responseList.value.unshift(requestRecord)
|
||||
|
||||
let payloadData = formData.value.params
|
||||
|
||||
// 处理HEX模式
|
||||
if (dataMode.value === 'hex') {
|
||||
/* // 过滤空格并统一为小写
|
||||
const hexStr = payloadData.replace(/\s/g, '').toLowerCase()
|
||||
|
||||
// 验证格式
|
||||
if (!/^[0-9a-f]*$/.test(hexStr) || hexStr.length % 2 !== 0) {
|
||||
throw new Error('无效的十六进制格式')
|
||||
}
|
||||
payloadData = hexStr*/
|
||||
/*// 转换为字节数组
|
||||
const bytes = new Uint8Array(hexStr.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)))
|
||||
|
||||
// 转换为Base64(浏览器环境)
|
||||
payloadData = btoa(String.fromCharCode.apply(null, bytes))*/
|
||||
} else {
|
||||
// JSON模式直接解析验证
|
||||
JSON.parse(payloadData)
|
||||
}
|
||||
|
||||
const payload = {
|
||||
Cmd: "tcp_send_data",
|
||||
Parameterlist: {
|
||||
endpoint: selectedEndpoint.value,
|
||||
data: payloadData
|
||||
}
|
||||
}
|
||||
|
||||
const response = await ajax(config.Tcpip + 'sockettcp/send_data_to_target', payload)
|
||||
//const rs = JSON.parse(response)
|
||||
if (response.isok == true) {
|
||||
// 更新记录
|
||||
Object.assign(requestRecord, {
|
||||
/* responseTime: formatDateTime(),
|
||||
responseData: response,*/
|
||||
isSuccess: true,
|
||||
msgtype: 1
|
||||
})
|
||||
ElMessage.success('请求发送成功')
|
||||
} else {
|
||||
Object.assign(requestRecord, {
|
||||
/* responseTime: formatDateTime(),
|
||||
responseData: response,*/
|
||||
isSuccess: false,
|
||||
msgtype: 1
|
||||
})
|
||||
ElMessage.error('请求发送失败:' + response)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('请求错误:', error)
|
||||
Object.assign(requestRecord, {
|
||||
/* responseTime: formatDateTime(),
|
||||
responseData: error.message,*/
|
||||
isSuccess: false,
|
||||
msgtype: 1
|
||||
})
|
||||
ElMessage.error('请求发送失败: ' + error.message)
|
||||
} finally {
|
||||
sending.value = false
|
||||
responseList.value = [...responseList.value]
|
||||
}
|
||||
}
|
||||
|
||||
// 响应历史记录
|
||||
const responseList = ref([])
|
||||
|
||||
// 新增格式化方法
|
||||
const formatDateTime = (date = new Date()) => {
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).replace(/\//g, '-')
|
||||
}
|
||||
|
||||
// 表格行类名处理
|
||||
const tableRowClassName = ({ row }) => {
|
||||
return row.isSuccess ? 'success-row' : 'error-row'
|
||||
}
|
||||
|
||||
// 文本截断方法
|
||||
const truncateText = (text = '', maxLength = 50) => {
|
||||
if (!text) return ''
|
||||
return text.length > maxLength ? text.substr(0, maxLength) + '...' : text
|
||||
}
|
||||
const closeTag = () => {
|
||||
selectedEndpoint.value = ''
|
||||
responseList.value = [] // 清空数据
|
||||
ElMessage.success('已清除IP选择,请重新选择')
|
||||
}
|
||||
// 新增转换函数
|
||||
const convertResponseData = (rawHex, mode) => {
|
||||
if (mode === 'json') {
|
||||
try {
|
||||
// HEX转ASCII后尝试解析JSON
|
||||
const ascii = hexToAscii(rawHex)
|
||||
return JSON.stringify(JSON.parse(ascii), null, 2)
|
||||
} catch {
|
||||
return hexToAscii(rawHex)
|
||||
}
|
||||
}
|
||||
return hexToFormatted(rawHex)
|
||||
}
|
||||
|
||||
/* const hexToAscii = (hex) => {
|
||||
try {
|
||||
// HEX转ASCII后尝试解析JSON
|
||||
const ascii = hexToAscii(rawHex)
|
||||
return JSON.stringify(JSON.parse(ascii), null, 2)
|
||||
} catch {
|
||||
return hexToAscii(rawHex)
|
||||
}
|
||||
}*/
|
||||
|
||||
const formatJson = (data) => {
|
||||
try {
|
||||
if (typeof data === 'string') {
|
||||
const parsed = JSON.parse(data)
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
}
|
||||
return JSON.stringify(data, null, 2)
|
||||
} catch (error) {
|
||||
return data || ''
|
||||
}
|
||||
}
|
||||
// 修改后的手动刷新处理
|
||||
const handleManualRefresh = () => {
|
||||
getTcpip() // 立即执行刷新
|
||||
startRefreshTimer() // 重置定时器
|
||||
//ElMessage.success('IP列表已更新')
|
||||
}
|
||||
|
||||
// 新增定时器管理函数
|
||||
const startRefreshTimer = () => {
|
||||
|
||||
// 清除已有定时器
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
}
|
||||
// 启动新定时器
|
||||
refreshTimer = setInterval(() => {
|
||||
if (localStorage.getItem('url') == '/tcpiptest') {
|
||||
getTcpip()
|
||||
}
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleManualRefresh()
|
||||
//localStorage.setItem('url', '/tcpiptest')
|
||||
})
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null // 防止内存泄漏
|
||||
}
|
||||
// localStorage.removeItem('url') // 按需决定是否清理
|
||||
})
|
||||
// 组件卸载时清除定时器
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
height: calc(100vh - 95px);
|
||||
padding: 15px;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* .ip-list {
|
||||
height: calc(100vh - 105px);
|
||||
}
|
||||
*/
|
||||
.radio-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.operation-panel {
|
||||
height: calc(100vh - 105px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.send-form {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.response-area {
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
/* 新增header样式 */
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 滚动容器样式 */
|
||||
.radio-list-container {
|
||||
max-height: calc(100vh - 220px); /* 根据实际情况调整 */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 调整左侧卡片高度 */
|
||||
.ip-list {
|
||||
height: calc(100vh - 105px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.param-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mode-tip {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.el-button-group {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
/* 响应结果表格样式 */
|
||||
.response-area {
|
||||
margin-top: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
/* 单元格内容样式 */
|
||||
.cell-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* 单元格内容样式 */
|
||||
.cell-content {
|
||||
line-height: 1.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Popover内容样式 */
|
||||
.popover-content {
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
</style>
|
||||
378
src/pages/user/index.vue
Normal file
378
src/pages/user/index.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<div class="user-center-container">
|
||||
<!-- 头部操作按钮 -->
|
||||
<div class="header-actions">
|
||||
<el-button type="primary"
|
||||
:icon="Edit"
|
||||
@click="enableEdit"
|
||||
v-show="!isEdit">
|
||||
编辑信息
|
||||
</el-button>
|
||||
<el-button type="success"
|
||||
:icon="UploadFilled"
|
||||
@click="submitForm"
|
||||
v-show="isEdit && formModified">
|
||||
保存修改
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息表单 -->
|
||||
<el-form :model="userValue"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
label-width="100px"
|
||||
label-position="right"
|
||||
class="user-form">
|
||||
<el-form-item label="登录名" prop="username">
|
||||
<el-input v-model="userValue.username"
|
||||
disabled="false"
|
||||
placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="真实姓名" prop="realname">
|
||||
<el-input v-model="userValue.realname"
|
||||
:disabled="!isEdit"
|
||||
placeholder="请输入真实姓名" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item label="所属公司" prop="comId">
|
||||
<el-select v-model="userValue.comId"
|
||||
:disabled="!isEdit"
|
||||
placeholder="请选择公司">
|
||||
<el-option v-for="company in companies"
|
||||
:key="company.id"
|
||||
:label="company.nameCn"
|
||||
:value="company.id" />
|
||||
</el-select>
|
||||
</el-form-item>-->
|
||||
|
||||
<!-- <el-form-item label="职位" prop="position">
|
||||
<el-select v-model="userValue.position"
|
||||
placeholder="请选择用户类型"
|
||||
:disabled="!isEdit">
|
||||
<el-option v-for="item in positionList"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item" />
|
||||
</el-select>
|
||||
</el-form-item>-->
|
||||
|
||||
<el-form-item label="手机号" prop="mobile">
|
||||
<el-input v-model="userValue.mobile"
|
||||
:disabled="!isEdit"
|
||||
placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="userValue.email"
|
||||
:disabled="!isEdit"
|
||||
placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="微信号" prop="weixin">
|
||||
<el-input v-model="userValue.weixin"
|
||||
:disabled="!isEdit"
|
||||
placeholder="请输入微信号" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 头像上传组件 -->
|
||||
<el-form-item label="头像" prop="avatar">
|
||||
<el-upload class="avatar-uploader"
|
||||
:disabled="!isEdit"
|
||||
drag
|
||||
accept="image/*"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="handleAvatarChange"
|
||||
ref="avatarUploadRef">
|
||||
<div v-if="userValue.avatar" class="avatar-preview">
|
||||
<img :src="selectedFile ? avatarPreview : config.Ads + userValue.avatar"
|
||||
class="avatar-image" />
|
||||
<div v-show="isEdit" class="avatar-mask">
|
||||
<el-icon :size="23"><Edit /></el-icon>
|
||||
<div>点击更换头像</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
拖拽图片到此 或 <em>点击上传</em>
|
||||
</div>
|
||||
<div class="el-upload__tip">
|
||||
支持JPG/PNG,大小不超过5MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, inject, watch } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Delete, Edit, UploadFilled } from '@element-plus/icons-vue';
|
||||
|
||||
const $http = inject('$http');
|
||||
const ajaxfile = inject('ajaxfile');
|
||||
const config = inject('config');
|
||||
|
||||
// 初始状态定义
|
||||
const isEdit = ref(true);
|
||||
const formModified = ref(false);
|
||||
const formRef = ref(null);
|
||||
const avatarUploadRef = ref(null);
|
||||
const selectedFile = ref(null);
|
||||
const avatarPreview = ref('');
|
||||
const loading = ref(false);
|
||||
const companies = ref([]);
|
||||
const userValue = reactive({
|
||||
id: null,
|
||||
username: "",
|
||||
realname: "",
|
||||
comId: null,
|
||||
company: "",
|
||||
password: "",
|
||||
roleId: null,
|
||||
position: "",
|
||||
weixin: "",
|
||||
email: "",
|
||||
mobile: "",
|
||||
avatar: ""
|
||||
});
|
||||
// 监听表单变化
|
||||
watch(userValue, () => {
|
||||
formModified.value = true;
|
||||
}, { deep: true });
|
||||
|
||||
// 启用编辑
|
||||
const enableEdit = () => {
|
||||
isEdit.value = true;
|
||||
};
|
||||
|
||||
// 头像处理
|
||||
const handleAvatarChange = (file) => {
|
||||
selectedFile.value = file.raw;
|
||||
avatarPreview.value = URL.createObjectURL(file.raw);
|
||||
};
|
||||
// 验证规则
|
||||
const rules = reactive({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '长度在3到20个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
|
||||
],
|
||||
mobile: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
|
||||
]
|
||||
});
|
||||
|
||||
// 获取用户列表
|
||||
const getUser = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const rs = await $http.post('Company/GetComInfo', {
|
||||
IsAll: true,
|
||||
ID: 0,
|
||||
});
|
||||
if (rs.data.isok) {
|
||||
companies.value = rs.data.response;
|
||||
|
||||
const rs1 = await $http.post('Users/GetUserInfo', {
|
||||
IsAll: false,
|
||||
ID: localStorage.getItem("uid"),
|
||||
});
|
||||
if (rs1.data.isok) {
|
||||
// 1. 创建公司映射表(提高查找效率)
|
||||
const companyMap = companies.value.reduce((map, company) => {
|
||||
map[company.id] = company.nameCn;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
// 2. 处理用户数据
|
||||
Object.assign(userValue, rs1.data.response);
|
||||
userValue.company = companyMap[userValue.comId];
|
||||
|
||||
console.log(userValue)
|
||||
} else {
|
||||
ElMessage.error(rs1.data.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
// 表单提交
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
// 表单验证
|
||||
await formRef.value.validate();
|
||||
|
||||
// 头像上传逻辑
|
||||
/* if (selectedFile.value) {
|
||||
let filedata = {
|
||||
File: selectedFile.value,
|
||||
Folder: "face"
|
||||
}
|
||||
const uploadRes = await ajaxfile(filedata);
|
||||
if (uploadRes?.fileName) {
|
||||
userValue.avatar = uploadRes.fileName;
|
||||
}
|
||||
}*/
|
||||
if (selectedFile.value) {
|
||||
const uploadRes = await ajaxfile(filedata);
|
||||
if (uploadRes?.fileName) {
|
||||
userValue.avatar = uploadRes.fileName; // 根据实际接口返回结构调整
|
||||
} else {
|
||||
ElMessage.error('头像上传失败: ' + uploadRes);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
userValue.avatar = userValue.avatar.substring(userValue.avatar.lastIndexOf('/') + 1);
|
||||
}
|
||||
// 更新用户信息
|
||||
const rs = await $http.post('Users/EditUser', userValue);
|
||||
if (rs.data.isok) {
|
||||
ElMessage.success('信息更新成功');
|
||||
isEdit.value = false;
|
||||
formModified.value = false;
|
||||
selectedFile.value = null;
|
||||
avatarUploadRef.value.clearFiles();
|
||||
await getUser(); // 刷新数据
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
// 获取身份
|
||||
const positionList = ref([]);
|
||||
const getShengfen = () => {
|
||||
try {
|
||||
const rs = $http.post('ConfigPY/GetSingleValue', {
|
||||
VarName: "用户类型",
|
||||
}).then(rs => {
|
||||
//console.log(JSON.parse(rs.data.response));
|
||||
positionList.value = JSON.parse(rs.data.response)
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
getUser()
|
||||
getShengfen()
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-center-container {
|
||||
padding: 20px;
|
||||
max-width: 444px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.avatar-preview {
|
||||
position: relative;
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
}
|
||||
.avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 拖拽区域样式 */
|
||||
.avatar-uploader :deep(.el-upload-dragger) {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
padding: 20px;
|
||||
border-radius: 5%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.avatar-uploader:hover :deep(.el-upload-dragger) {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* 头像预览样式 */
|
||||
.avatar-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 5%;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.avatar-mask:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 图标和文字样式 */
|
||||
.el-icon--upload {
|
||||
font-size: 36px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.el-upload__text em {
|
||||
color: var(--el-color-primary);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
</style>
|
||||
426
src/pages/usermanage/index.vue
Normal file
426
src/pages/usermanage/index.vue
Normal file
@@ -0,0 +1,426 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<!-- 操作栏 -->
|
||||
|
||||
<div class="header">
|
||||
<el-button type="primary" @click="handleAdd" :icon="Plus">添加用户</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<el-table :data="allUsers" border style="width: 100%" v-loading="loading" empty-text="暂无数据">
|
||||
<el-table-column label="操作" :width="105">
|
||||
<template #default="{ row }">
|
||||
<el-button type="info" :icon="Edit" circle plain @click="handleEdit(row)" />
|
||||
<el-button type="danger" :icon="Delete" circle plain @click="handleDelete(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="用户名" width="130" sortable show-overflow-tooltip />
|
||||
<el-table-column prop="realname" label="真实姓名" width="90" />
|
||||
<el-table-column prop="companyName" label="所属公司" width="90" />
|
||||
<!-- <el-table-column prop="position" label="用户类型" width="135" />-->
|
||||
<el-table-column prop="mobile" label="手机号" width="128" />
|
||||
</el-table>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑用户' : '添加用户'"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
width="600px">
|
||||
<el-form :model="form"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
label-width="100px"
|
||||
label-position="right">
|
||||
<el-form-item label="登录名" prop="username">
|
||||
<el-input v-model="form.username"
|
||||
:disabled="isEdit"
|
||||
placeholder="登录名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="登录密码" prop="password" v-if="!isEdit">
|
||||
<el-input v-model="form.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="密码" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="修改密码" prop="password" v-if="isEdit">
|
||||
<el-input v-model="form.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="密码(为空则不修改)" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="真实姓名" prop="realname">
|
||||
<el-input v-model="form.realname" placeholder="真实姓名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="公司" prop="companyName">
|
||||
<el-select v-model="form.companyName" placeholder="公司">
|
||||
<el-option v-for="item in companies"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号" prop="mobile">
|
||||
<el-input v-model="form.mobile" placeholder="手机号" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="权限" prop="permission">
|
||||
<div class="permission-container">
|
||||
<el-checkbox v-model="allSelected"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleAllPermissionChange">
|
||||
全选
|
||||
</el-checkbox>
|
||||
|
||||
<el-checkbox-group v-model="selectedPermissions"
|
||||
@change="handlePermissionChange"
|
||||
v-show="!allSelected">
|
||||
<el-checkbox v-for="item in permissionOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item" />
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, inject, watch } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Plus, Delete, Edit, UploadFilled } from '@element-plus/icons-vue';
|
||||
|
||||
const $http = inject('$http');
|
||||
const ajaxfile = inject('ajaxfile');
|
||||
const config = inject('config');
|
||||
|
||||
// 数据列表
|
||||
const allUsers = ref([]);
|
||||
const loading = ref(false);
|
||||
const companies = ref([]);
|
||||
|
||||
const nowUser = localStorage.getItem('username');
|
||||
// 对话框相关
|
||||
const dialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const currentUserId = ref(null);
|
||||
const formRef = ref(null);
|
||||
|
||||
const selectedPermissions = ref([]);
|
||||
const allSelected = ref(false);
|
||||
const isIndeterminate = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const defaultForm = {
|
||||
username: '',
|
||||
password: '',
|
||||
realname: '',
|
||||
mobile: '',
|
||||
permission: ''
|
||||
};
|
||||
|
||||
|
||||
const form = reactive({ ...defaultForm });
|
||||
|
||||
// 验证规则
|
||||
const rules = reactive({
|
||||
username: [
|
||||
{ required: true, message: '用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '长度在3到20个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ message: '密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
|
||||
],
|
||||
mobile: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
|
||||
],
|
||||
comId: [
|
||||
{ required: true, message: '公司不可为空', trigger: 'blur' }
|
||||
],
|
||||
permission: [
|
||||
{ required: true, message: '权限不可为空', trigger: 'blur' }
|
||||
],
|
||||
realname: [
|
||||
{ message: '真实姓名', trigger: 'blur' }
|
||||
],
|
||||
});
|
||||
|
||||
// 获取用户列表
|
||||
const getUsers = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const rs = await $http.post('Users/GetUserInfo', {
|
||||
IsAll: true,
|
||||
ID: 0,
|
||||
});
|
||||
//console.log(rs.data.response);
|
||||
if (rs.data.isok) {
|
||||
allUsers.value = rs.data.response.map(user => ({
|
||||
...user,
|
||||
// 显示时自动转换全选状态
|
||||
permissionDisplay: user.permission === '/全选'
|
||||
? permissionOptions.value
|
||||
: user.permission.split('/').filter(p => p)
|
||||
})).sort((a, b) => a.username.localeCompare(b.username));
|
||||
} else {
|
||||
ElMessage.error(rs.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户列表失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 权限选择变化处理
|
||||
const handlePermissionChange = (value) => {
|
||||
const checkedCount = value.length;
|
||||
|
||||
if (checkedCount === permissionOptions.value.length) {
|
||||
handleAllPermissionChange(true)
|
||||
} else {
|
||||
// 手动选择时强制关闭全选状态
|
||||
allSelected.value = false;
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < permissionOptions.value.length;
|
||||
|
||||
// 拼接为斜杠分割的字符串
|
||||
form.permission = checkedCount > 0
|
||||
? `/${value.join('/')}`
|
||||
: '';
|
||||
}
|
||||
};
|
||||
|
||||
// 全选处理
|
||||
const handleAllPermissionChange = (val) => {
|
||||
allSelected.value = val;
|
||||
isIndeterminate.value = false;
|
||||
selectedPermissions.value = []; // 清空具体选项
|
||||
form.permission = val ? '/全选' : '';
|
||||
};
|
||||
|
||||
// 获取company列表
|
||||
const positionList = ref([]);
|
||||
const getCom = () => {
|
||||
try {
|
||||
const rs = $http.post('ConfigPY/GetSingleValue', {
|
||||
VarName: "公司管理",
|
||||
}).then(rs => {
|
||||
//console.log(JSON.parse(rs.data.response));
|
||||
companies.value = JSON.parse(rs.data.response)
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
const permissionOptions = ref([]);
|
||||
const getPermission = () => {
|
||||
try {
|
||||
const rs = $http.post('ConfigPY/GetSingleValue', {
|
||||
VarName: "权限管理",
|
||||
}).then(rs => {
|
||||
//console.log(JSON.parse(rs.data.response));
|
||||
permissionOptions.value = JSON.parse(rs.data.response)
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交
|
||||
const submitForm = async () => {
|
||||
await formRef.value.validate();
|
||||
try {
|
||||
const data = { ...form };
|
||||
let postApi = 'Users/'
|
||||
if (isEdit.value) {
|
||||
data.id = currentUserId.value;
|
||||
postApi += "EditUser"
|
||||
} else {
|
||||
postApi += "AddUser"
|
||||
}
|
||||
|
||||
const rs = await $http.post(postApi, data);
|
||||
if (rs.data.isok) {
|
||||
ElMessage.success(isEdit.value ? '修改成功' : '添加成功');
|
||||
await getUsers();
|
||||
dialogVisible.value = false;
|
||||
} else {
|
||||
ElMessage.error(rs.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 修改打开对话框
|
||||
const handleAdd = () => {
|
||||
Object.assign(form, defaultForm);
|
||||
isEdit.value = false;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑初始化
|
||||
const handleEdit = (row) => {
|
||||
isIndeterminate.value = false;
|
||||
console.log(row);
|
||||
Object.assign(form, row);
|
||||
isEdit.value = true;
|
||||
currentUserId.value = row.id;
|
||||
|
||||
// 初始化全选状态
|
||||
if (form.permission === '/全选') {
|
||||
allSelected.value = true;
|
||||
selectedPermissions.value = [];
|
||||
} else {
|
||||
allSelected.value = false;
|
||||
selectedPermissions.value = form.permission
|
||||
.split('/')
|
||||
.filter(p => p && p !== '全选'); // 移除斜杠和空值
|
||||
}
|
||||
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定删除该用户吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const rs = await $http.post('Users/DelUser', { id: row.id });
|
||||
if (rs.data.isok) {
|
||||
ElMessage.success('删除成功');
|
||||
await getUsers();
|
||||
} else {
|
||||
ElMessage.error(rs.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败');
|
||||
console.error(error);
|
||||
}
|
||||
}).catch(() => { });
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getUsers()
|
||||
getCom()
|
||||
getPermission()
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-management {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 拖拽区域样式 */
|
||||
.avatar-uploader :deep(.el-upload-dragger) {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
padding: 20px;
|
||||
border-radius: 5%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.avatar-uploader:hover :deep(.el-upload-dragger) {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* 头像预览样式 */
|
||||
.avatar-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 5%;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.avatar-mask:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 图标和文字样式 */
|
||||
.el-icon--upload {
|
||||
font-size: 36px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.el-upload__text em {
|
||||
color: var(--el-color-primary);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
137
src/router/index.js
Normal file
137
src/router/index.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import Home from '../pages/home/index.vue';
|
||||
import Login from '../pages/login/index.vue';
|
||||
import UserManage from '../pages/usermanage/index.vue';
|
||||
import ComManage from '../pages/commanage/index.vue';
|
||||
import ModelManage from '../pages/modelmanage/index.vue';
|
||||
import ScopeManage from '../pages/scopemanage/index.vue';
|
||||
import User from '../pages/user/index.vue';
|
||||
import Log from '../pages/log/index.vue';
|
||||
import DicManage from '../pages/dicmanage/index.vue';
|
||||
import IoTRegister from '../pages/iotregister/index.vue';
|
||||
import TcpIpTest from '../pages/tcpiptest/index.vue';
|
||||
import Recorded from '../pages/recorded/index.vue';
|
||||
import NT530R from '../pages/recorded/NT530R.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
// 动态重定向到登录页或主页
|
||||
redirect: () => {
|
||||
const isAuthenticated = localStorage.getItem('token');
|
||||
return isAuthenticated ? '/home' : '/login';
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: '登录',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: '主页',
|
||||
component: Home,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},
|
||||
{
|
||||
path: '/log',
|
||||
name: '日志中心',
|
||||
component: Log,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},
|
||||
{
|
||||
path: '/usermanage',
|
||||
name: '用户管理',
|
||||
component: UserManage,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},
|
||||
{
|
||||
path: '/commanage',
|
||||
name: '组织管理',
|
||||
component: ComManage,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},
|
||||
{
|
||||
path: '/modelmanage',
|
||||
name: '组织管理',
|
||||
component: ModelManage,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},
|
||||
/* {
|
||||
path: '/scopemanage',
|
||||
name: '权限管理',
|
||||
component: ScopeManage,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},*/
|
||||
{
|
||||
path: '/dicmanage',
|
||||
name: '字典管理',
|
||||
component: DicManage,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
name: '个人中心',
|
||||
component: User,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},
|
||||
{
|
||||
path: '/iotregister',
|
||||
name: '设备注册',
|
||||
component: IoTRegister,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},
|
||||
{
|
||||
path: '/tcpiptest',
|
||||
name: '设备测试',
|
||||
component: TcpIpTest,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},
|
||||
{
|
||||
path: '/recorded',
|
||||
name: '设备记录',
|
||||
component: Recorded,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},
|
||||
{
|
||||
path: '/NT530R',
|
||||
name: 'NT530R',
|
||||
component: NT530R,
|
||||
meta: { requiresAuth: true } // 需要认证的路由
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: '404错误',
|
||||
component: () => import('../pages/404/index.vue') // 404页面
|
||||
},
|
||||
|
||||
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
next() //跳过守护
|
||||
const isAuthenticated = localStorage.getItem('token') // 用localStorage存储token
|
||||
const username = localStorage.getItem('username') // 获取用户名
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
// 如果需要认证且没有token,则跳转到登录页面
|
||||
localStorage.removeItem('lastLoginTimestamp')
|
||||
next('/login')
|
||||
} else if ((to.path == '/usermanage' || to.path == '/scopemanage') && (username != 'Admin' && username != 'MoMoWen')) {
|
||||
// 如果访问的是用户管理页面但用户名不是Admin,则跳转到主页
|
||||
next('/home')
|
||||
} else {
|
||||
// 其他情况正常放行
|
||||
sessionStorage.setItem('currentRoute', to.fullPath)
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router;
|
||||
10
vite.config.js
Normal file
10
vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import plugin from '@vitejs/plugin-vue';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [plugin()],
|
||||
server: {
|
||||
port: 4137,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user