初始化

This commit is contained in:
2025-11-20 14:12:37 +08:00
commit cc05808c6e
47 changed files with 15821 additions and 0 deletions

9
.editorconfig Normal file
View 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
View File

@@ -0,0 +1 @@
* text=auto eol=lf

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# 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
*.sln
*.sw?
*.tsbuildinfo
/.vs
/obj
/.vscode
/*.user

12
CHANGELOG.md Normal file
View 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 bls_web_vue -- --eslint `.
- Updating `vite.config.js` with port.
- Create project file (`bls_web_vue.esproj`).
- Create `launch.json` to enable debugging.
- Add project to solution.
- Write this file.

35
README.md Normal file
View File

@@ -0,0 +1,35 @@
# bls_web_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
```

11
bls_web_vue.esproj Normal file
View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.2752196">
<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>

24
eslint.config.js Normal file
View 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
View 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>宝来威BLS平台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
jsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

6081
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "bls_web_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",
"echarts": "^5.6.0",
"element-china-area-data": "^6.1.0",
"element-plus": "^2.10.2",
"jquery": "^3.7.1",
"localforage": "^1.10.0",
"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"
}
}

67
public/config.js Normal file
View File

@@ -0,0 +1,67 @@
const config = {
// http访问后端接口
Api: "http://blv-rd.tech:19088/api/",
Ads: "http://blv-rd.tech:19088/",
ApiList: [
"http://blv-rd.tech:19088/api/",
"http://blv-rd.tech:19055/api/",
"http://www.boonlive-rcu.com:7000/api/",
],
// 本地调试接口
/* Api: "http://localhost:5245/api/",
Ads: "http://localhost:5245/",
ApiList: [
"http://localhost:5245/api/",
"http://blv-rd.tech:19055/api/",
"http://www.boonlive-rcu.com:7000/api/",
],*/
CommandComparison: [
{ "key": "SearchHost", "value": "01" },
{ "key": "Heart", "value": "02" },
{ "key": "InsertCard", "value": "03" },
{ "key": "ServiceRequest", "value": "04" },
{ "key": "AlarmStatus", "value": "05" },
{ "key": "AirConditionStatus", "value": "06" },
{ "key": "EnergySavingMode", "value": "07" },
{ "key": "SyncTime", "value": "08" },
{ "key": "RoomControl", "value": "10" },
{ "key": "UpdateHost", "value": "0A" },
{ "key": "UpdateConfig", "value": "A2" },
{ "key": "UpdateProgressBar", "value": "B6" },
{ "key": "UploadCurrentVersion", "value": "0B" },
{ "key": "RoomStatusChanged", "value": "0C" },
{ "key": "LightScene", "value": "0D" },
{ "key": "RoomStatus", "value": "0E" },
{ "key": "StatusFilter", "value": "0E" },
{ "key": "StatusPass", "value": "0E" },
{ "key": "DeviceControl", "value": "0F" },
{ "key": "ConnectingRoom", "value": "10" },
{ "key": "NetworkSetting", "value": "11" },
{ "key": "Service", "value": "12" },
{ "key": "AirProperty", "value": "13" },
{ "key": "WordsReport", "value": "14" },
{ "key": "TvControl", "value": "21" },
{ "key": "HostAuthorization", "value": "22" },
{ "key": "CurtainControl", "value": "23" },
{ "key": "PowerSupplyControl", "value": "24" },
{ "key": "UnlockControl", "value": "25" },
{ "key": "MusicControl", "value": "26" },
{ "key": "SetMAC", "value": "27" },
{ "key": "SetDeviceSecret", "value": "28" },
{ "key": "GetHostSecret", "value": "29" },
{ "key": "TFTPUpdate", "value": "68" },
{ "key": "FTPUpdate", "value": "69" },
{ "key": "GetRegister", "value": "30" },
{ "key": "SetRegister", "value": "31" },
{ "key": "RCUInfo", "value": "B1" },
{ "key": "ExplainDomainIP", "value": "D6" },
{ "key": "SetRCULog", "value": "D9" },
{ "key": "ReceiveRCULog", "value": "DA" },
{ "key": "HotelTime", "value": "DB" },
{ "key": "TFTPLog", "value": "DA" },
{ "key": "TFTPLog_Setting", "value": "D9" },
]
}
export default config

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

19
public/logo.svg Normal file
View 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
View 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

799
src/App.vue Normal file
View File

@@ -0,0 +1,799 @@
<template>
<div class="main-container">
<!-- 登录后的布局 -->
<template v-if="alreadyLogin">
<!-- 侧边菜单 -->
<div class="header-container" v-show="!fullScreen">
<el-menu ref="parentRef"
mode="horizontal"
:default-active="currentTitle.routes"
@select="menuChange"
:ellipsis-icon="Menu"
: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">
<el-popover v-model:visible="showPopover"
placement="bottom-end"
trigger="click">
<template #reference>
<el-avatar><span style="font-size:16px">{{ userAvatar }}</span></el-avatar>
</template>
<div class="user-menu">
<!-- 刷新按钮 -->
<el-button link @click="handleAction('refresh')">
<el-icon><Refresh /></el-icon>
刷新页面
</el-button>
<el-divider />
<!-- 主题切换按钮 -->
<el-button link @click="handleAction('toggleTheme')">
<span v-if="!isDarkMode"><el-icon><Moon /></el-icon></span>
<span v-else><el-icon><Sunny /></el-icon></span>
切换主题
</el-button>
<el-divider />
<!-- 退出登录按钮 -->
<el-button link @click="handleAction('logout')">
<el-icon><SwitchButton /></el-icon>
退出登录
</el-button>
</div>
</el-popover>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<div class="router-container" :style="{ height: fullScreen ? '100vh' : 'calc(100vh - 64px)' }">
<keep-alive :include="cachedViews">
<router-view v-slot="{ Component }">
<component :is="Component"
:key="route.fullPath"
class="router-view-container" />
</router-view>
</keep-alive>
</div>
<el-backtop target=".router-container"
:visibility-height="200"
:right="isMobile ? 10 : 60"
style="z-index: 9999;"
:bottom="isMobile ? 50 : 80">
<div class="backtop-content">
<el-icon :size="24"><Top /></el-icon>
</div>
</el-backtop>
</div>
</template>
<!-- 未登录时的布局 -->
<template v-else>
<router-view />
</template>
</div>
</template>
<script setup>
import { useDark, useToggle } from '@vueuse/core'
import { h, provide, ref, onMounted, watchEffect, computed, reactive, inject, watch, onBeforeUnmount, nextTick, onUnmounted } from 'vue'
import Vuex from 'vuex';
import { ElMessage, ElTableV2, ElAutoResizer, ElButton } from 'element-plus'
import en from 'element-plus/dist/locale/en.mjs'
import { useRouter, useRoute } from 'vue-router'
import { Menu } from '@element-plus/icons-vue';
import $ from 'jquery';
import qs from 'qs'
const language = ref('zh-cn')
const locale = computed(() => (language.value === 'zh-cn' ? zhCn : en))
const isDark = useDark()
const toggleDark = useToggle(isDark)
const isMobile = ref(false)
const fullScreen = ref(false)
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'
if (localStorage.getItem("TokenT")) {
if (calculateTimeDiff(localStorage.getItem("TokenT")) < 260000) { // 3天内免密登录
localStorage.setItem("TokenT", new Date())
} else {
localStorage.removeItem("TokenT")
router.push('/login')
}
} else {
router.push('/login')
}
if (localStorage.getItem('login') == 'true') {
// 初始化数据
userAvatar.value = getByCharWidth(localStorage.getItem('username'))
userName.value = localStorage.getItem('username')
if (localStorage.getItem("TokenT")) {
}
const permissionString = "/全选"
//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 = []
}
}
}
const userAvatar = ref("")
const userName = ref("")
// 检测屏幕大小变化的函数
const checkIfMobile = () => {
isMobile.value = window.innerWidth < 768;
}
// 计算时间差
const calculateTimeDiff = (targetTimeStr) => {
const targetDate = new Date(targetTimeStr);
// 检查日期是否有效
if (isNaN(targetDate.getTime())) {
throw new Error("无效的时间格式");
}
const now = new Date();
return Math.floor((now - targetDate) / 1000);
}
const alreadyLogin = ref(false)
const router = useRouter()
const route = useRoute()
const $http = inject('$http')
const config = inject('config')
//const username = localStorage.getItem('username')
const searchInputRef = ref(null)
const currentTitle = reactive({
label: '',
routes: localStorage.getItem('url') || '/udplog',
}) // 当前页面
const isCollapse = ref(false)
const showPopover = ref(false)
// 添加计算属性
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 menuValue = reactive([
{
label: "功能",
value: "dashboard",
icon: "Menu",
options: [
/*{ label: "首页", value: "/home", icon: "House" },*/
{ label: "UDP监控", value: "/udplog", icon: "Connection" },
{ label: "UDP过滤", value: "/blacklist", icon: "CircleClose" },
{ label: "线程日志", value: "/tasktimelog", icon: "DataAnalysis" },
{ label: "语音助手日志", value: "/voicelog", icon: "Microphone" },
{ label: "房态日志", value: "/statuslog", icon: "Postcard" },
{ label: "TFTP上传管理", value: "/tftpwhitelist", icon: "Sort" },
/*{ label: "功率记录", value: "/powerlog", icon: "Odometer" },*/
]
}/*,
{
label: "系统操作",
value: "system",
icon: "Tools",
options: [
{ label: "字典管理", value: "/dicmanage", icon: "Notebook" },
]
}*/
]);
// 菜单操作处理
const handleAction = (action) => {
showPopover.value = false;
switch (action) {
case 'refresh':
refreshToHome();
break;
case 'toggleTheme':
toggleTheme();
break;
case 'logout':
localStorage.setItem('login', false);
localStorage.removeItem('TokenT');
router.push('/login');
break;
default:
console.log('跳转错误!');
break;
}
};
const refreshToHome = () => {
//$http.post('ConfigPY/RefreshConfig')
//router.push('/home')
setTimeout(() => {
location.reload(true);
}, 0);
}
// 处理搜索框回车事件
const handleHotelSearchEnter = () => {
if (hotelSearchQuery.value.trim() === '') return;
const filtered = filteredHotels.value;
if (filtered.length === 1) {
switchHotel(filtered[0]);
} else if (filtered.length > 1) {
ElMessage.warning(`${filtered.length} 个匹配项,请手动选择`);
} else {
ElMessage.error('未找到匹配的酒店');
}
};
const parentRef = ref(null)
// 菜单处理
const menuChange = (val) => {
if (currentTitle.routes === val) return
// 使用我们提取函数确保一致性
val = getRouteFromUrl(val)
currentTitle.routes = val
localStorage.setItem('url', val)
router.push(val)
}
const getRouteFromUrl = (url) => {
const match = url.match(/.*\/([^?/#]+)/)
return match ? `/${match[1]}` : '/home'
}
// 控制深色模式
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 cachedViews = ref(new Set())
watch(
() => route.fullPath,
(newPath) => {
const newRoute = getRouteFromUrl(newPath)
currentTitle.routes = newRoute
localStorage.setItem('url', newRoute)
}
)
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 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 formattedTime = ref('');
// 每秒更新时间
const updateTime = () => {
const now = new Date();
const year = String(now.getFullYear()).padStart(4, '0');
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
formattedTime.value = /*`${year}-*/`${month}-${day} ${hours}:${minutes}:${seconds}`;
};
provide('ajaxfile', ajaxfile)
provide('ajax', ajax)
provide('checkLoginStatus', checkLoginStatus)
provide('calculateTimeDiff', calculateTimeDiff)
provide('isMobile', isMobile)
provide('fullScreen', fullScreen)
onMounted(() => {
localStorage.setItem('login', true)
currentTitle.routes = getRouteFromUrl(route.fullPath)
// 初始检查登录状态
checkLoginStatus()
// 主题初始化
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleColorSchemeChange = (e) => {
webToDark(e.matches)
}
// 初始设置
webToDark(darkModeQuery.matches)
// 监听系统主题变化
darkModeQuery.addEventListener('change', handleColorSchemeChange)
checkIfMobile();
// 添加窗口大小变化监听
window.addEventListener('resize', checkIfMobile);
})
onBeforeUnmount(() => {
window.removeEventListener('resize', checkIfMobile);
darkModeQuery.removeEventListener('change', handleColorSchemeChange)
})
onUnmounted(() => {
});
</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;
padding-right: 8px;
}
/* 按钮图标 */
.right-functions .el-button {
font-size: 22px;
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: 2px;
padding-left: 10px;
}
:deep(.el-menu--horizontal > .el-sub-menu) {
margin: 0 4px;
}
}
/* 移动端提示样式 */
.empty-hint {
text-align: center;
padding: 20px;
color: var(--el-text-color-secondary);
}
.empty-hint .el-icon {
font-size: 48px;
margin-bottom: 16px;
color: var(--el-color-info);
}
.empty-hint div {
font-size: 14px;
margin-top: 8px;
}
/* 主内容区样式 */
.main-content {
flex: 1;
position: relative;
}
.router-container {
/* height: calc(100vh - 64px);*/
overflow: auto;
}
/* 用户菜单样式 */
.user-menu {
display: flex;
flex-direction: column;
padding: 0;
}
.user-menu .el-button {
display: flex;
align-items: center;
padding: 8px 0px;
width: 100%;
text-align: left;
color: var(--el-text-color-regular);
}
.user-menu .el-button:hover {
background-color: var(--el-fill-color-light);
}
.user-menu .el-icon {
margin-right: 8px;
font-size: 18px;
}
.user-menu .el-divider {
margin: 4px 0;
}
/* 表格容器样式 */
.hotel-table-container {
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow: hidden;
min-height: 400px; /* 确保最小高度 */
}
/* 单元格样式 */
.code-cell {
font-weight: bold;
color: var(--el-color-primary);
font-family: Consolas, Monaco, monospace;
}
.name-cell {
padding: 0 12px;
word-break: break-word;
}
/* 操作按钮样式 */
.action-button {
font-size: 12px;
padding: 4px 8px;
}
/* 当前酒店样式 */
.current-hotel {
color: var(--el-color-success);
font-weight: bold;
}
/* 添加历史记录下拉框样式 */
.history-dropdown {
position: absolute;
top: 44px;
left: 6px;
right: 6px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
}
.history-item {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
}
.history-item:hover {
background-color: var(--el-fill-color-light);
}
.history-item .el-icon {
margin-right: 8px;
color: var(--el-text-color-secondary);
}
.backtop-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.backtop-hint {
font-size: 11px;
margin-top: 2px;
color: var(--el-color-primary);
white-space: nowrap;
}
/* 删除按钮样式 */
.delete-icon {
position: absolute;
right: 8px;
font-size: 24px;
color: var(--el-text-color-secondary);
opacity: 0.6;
padding: 2px;
border-radius: 4px;
}
.delete-icon:hover {
color: var(--el-color-danger);
background-color: rgba(245, 108, 108, 0.1);
opacity: 1;
}
/* 清空全部按钮样式 */
.clear-all {
display: flex;
align-items: center;
padding: 8px 12px;
font-size: 14px;
color: var(--el-color-danger);
border-top: 1px solid var(--el-border-color);
cursor: pointer;
}
.clear-all .el-icon {
margin-right: 8px;
}
.clear-all:hover {
background-color: var(--el-fill-color-light);
}
/* 确保表格内容可见 */
:deep(.el-table-v2) {
color: var(--el-text-color-regular);
font-size: 14px;
}
:deep(.el-table-v2__header-cell) {
background-color: var(--el-fill-color-light);
font-weight: bold;
}
:deep(.el-table-v2__row-cell) {
z-index: 1;
position: relative;
}
@media (max-width: 768px) {
.backtop-hint {
display: none;
}
}
</style>

BIN
src/assets/SourceCode.ttf Normal file

Binary file not shown.

86
src/assets/base.css Normal file
View 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;
}

1
src/assets/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

37
src/assets/main.css Normal file
View 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 ;
}
}

41
src/axios.js Normal file
View File

@@ -0,0 +1,41 @@
import axios from 'axios';
import config from '../public/config.js';
import { ElMessage } from 'element-plus'
// 创建一个可以生成axios实例的工厂函数
const createAxiosInstance = (urlnum) => {
const instance = axios.create({
baseURL: config.ApiList[urlnum || 0], // 使用传入的baseURL参数
timeout: 150000,
});
// 添加请求拦截器
instance.interceptors.request.use(config => {
if (config.headers['Content-Type'] == undefined) {
config.headers['Content-Type'] = 'application/json';
}
if (config.data && typeof config.data === 'object') {
config.data = JSON.stringify(config.data);
}
return config;
}, error => Promise.reject(error));
// 添加响应拦截器
instance.interceptors.response.use(
response => response,
error => {
console.log(error);
return Promise.reject(error);
}
);
return instance;
};
// 创建一个默认实例(保持现有用法兼容)
const defaultInstance = createAxiosInstance(import.meta.env.VITE_API_BASE_URL || '');
// 导出默认实例(保持现有用法)
export default defaultInstance;
// 同时导出工厂函数(用于创建自定义实例)
export { createAxiosInstance };

View File

@@ -0,0 +1,44 @@
<script setup>
defineProps({
msg: {
type: String,
required: true,
},
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve 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>

View 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>
Vues
<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>

View 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
View 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;

View 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>

View 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>

View 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>

View 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>

View 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
View 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')

116
src/pages/404/index.vue Normal file
View File

@@ -0,0 +1,116 @@
<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 { onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { HomeFilled } from '@element-plus/icons-vue'
const router = useRouter();
const goHome = () => {
router.push('home');
};
onMounted(async () => {
localStorage.setItem('url', '/404');
});
</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>

View File

@@ -0,0 +1,474 @@
<template>
<!--<el-button type="info" plain @click="allbl">测试</el-button>-->
<div class="container">
<div class="pagination-container">
<el-input v-model="searchKeyword"
placeholder="搜索酒店名称或编号"
clearable
size="small"
style="width: 200px;"
@input="handleSearch">
<template #prefix>
<el-icon><search /></el-icon>
</template>
</el-input>
<el-checkbox v-model="showOnlyBlack" size="small">
只显示过滤名单
</el-checkbox>
<!-- 过滤名单数 -->
<el-text>过滤名单: {{ blackListNum }} | : {{ allHotelData.length }} </el-text>
<!-- 翻页控件 -->
<el-pagination size="small"
layout=" prev, pager, next, sizes"
:page-size="pageSize"
:page-sizes="[100, 200, 300, 500]"
:total="filteredData.length"
pager-count="5"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
v-model:current-page="currentPage">
</el-pagination>
</div>
<div style="overflow-x: auto; max-width:98vw;">
<el-table :data="paginatedData"
border
highlight-current-row
:max-height="tableMaxHeight"
style="width: 100%;">
<el-table-column label="操作" width="90">
<template #default="{ row }">
<span v-if="row.isBlack">
<el-button type="warning" size="small" plain @click="openCancelDialog(row.code)">
取消过滤
</el-button>
</span>
<span v-else>
<el-button type="danger" size="small" plain @click="openIntoDialog(row.code)">
过滤
</el-button>
</span>
</template>
</el-table-column>
<el-table-column prop="code" label="编号" width="70" sortable>
<template #default="{ row }">
<span v-if="row.isBlack"><el-text type="danger">{{ row.code }}</el-text></span>
<span v-if="!row.isBlack"><el-text type="success">{{ row.code }}</el-text></span>
</template>
</el-table-column>
<el-table-column prop="name" label="酒店名称" width="280">
<template #default="{ row }">
<span v-if="row.isBlack"><el-text type="danger">{{ row.name }}</el-text></span>
<span v-if="!row.isBlack"><el-text type="success">{{ row.name }}</el-text></span>
</template>
</el-table-column>
<!--<el-table-column label="详情" width="90">
</el-table-column>-->
</el-table>
</div>
</div>
</template>
<script setup>
import { ref, inject, computed, onMounted, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh, Search } from '@element-plus/icons-vue'
import config from '../../../public/config.js'
import qs from 'qs'
import { useRouter, useRoute } from 'vue-router'
import { createAxiosInstance } from '../../axios.js'
// 引入Vue工具
const router = useRouter()
const $http = inject('$http')
const checkLoginStatus = inject('checkLoginStatus');
const customHttp = createAxiosInstance(1);
// 响应式变量
const globalLoading = ref(false);
const loadingText = ref('加载中,请稍候...');
const allUpdLogsList = ref([]);
const tableData = ref([]);
const commandTypes = ref([]);
const allHotels = ref([]);
const hotels = ref([]);
const rooms = ref([]);
const allRooms = ref([]);
const hotelCurrent = ref(null);
const roomCurrent = ref(null);
const roomDisable = ref(true);
const isTheRoomGet = ref(false);
const searchKeyword = ref(''); // 搜索关键词
const blackListNum = ref(null);
// 分页相关变量
const currentPage = ref(1);
const pageSize = ref(100); // 默认每页100条
const showOnlyBlack = ref(true); // 默认只显示过滤名单酒店
const allHotelData = ref([]);
const allBlackList = ref([]);
// 添加对话框相关变量
const cancelDialogVisible = ref(false);
const intoDialogVisible = ref(false);
const currentHotelCode = ref('');
// 合并酒店和过滤名单数据
const mergeHotelBlacklistData = () => {
if (!Array.isArray(allHotels.value) || !allBlackList.value) return [];
// 转换为 Map 方便查找
const blacklistMap = new Map(Object.entries(allBlackList.value));
return allHotels.value.map(hotel => {
const codeStr = hotel.code.toString();
const isBlack = blacklistMap.has(codeStr);
const blackList = isBlack ? blacklistMap.get(codeStr) : [];
return {
...hotel,
isBlack,
blackList
};
});
};
// 筛选后的数据
const filteredData = computed(() => {
let result = allHotelData.value;
// 应用搜索过滤
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase();
result = result.filter(hotel =>
hotel.name.toLowerCase().includes(keyword) ||
hotel.code.toString().includes(keyword)
);
}
// 应用过滤名单筛选
if (showOnlyBlack.value) {
result = result.filter(hotel => hotel.isBlack);
}
return result;
});
// 打开取消过滤对话框
const openCancelDialog = (hotelCode) => {
currentHotelCode.value = hotelCode;
ElMessageBox.confirm(
'确定取消该酒店的过滤吗?',
'取消过滤确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
cancelHotelBlack(hotelCode);
}).catch(() => {
// 用户取消操作
});
};
// 打开过滤对话框
const openIntoDialog = (hotelCode) => {
currentHotelCode.value = hotelCode;
ElMessageBox.confirm(
'确定过滤该酒店吗?',
'过滤确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
intoHotelBlack(hotelCode);
}).catch(() => {
// 用户取消操作
});
};
// 当前页的数据
const paginatedData = computed(() => {
const startIndex = (currentPage.value - 1) * pageSize.value;
const endIndex = startIndex + pageSize.value;
return filteredData.value.slice(startIndex, endIndex);
});
// 处理分页大小变化
const handleSizeChange = (newSize) => {
pageSize.value = newSize;
currentPage.value = 1; // 重置到第一页
};
// 处理当前页变化
const handleCurrentChange = (newPage) => {
currentPage.value = newPage;
};
const handleSearch = () => {
currentPage.value = 1; // 搜索时重置到第一页
};
// 取消整个酒店过滤
const cancelHotelBlack = async (hotelCode) => {
try {
const data = { HotelCode: hotelCode };
const rs = await customHttp.post('BlockIP/BlockLWRemove', JSON.stringify(data));
if (rs.data.isok) {
ElMessage.success('取消过滤成功');
await getBlockList(); // 刷新过滤名单数据
} else {
ElMessage.error('操作失败: ' + (rs.data.message || '未知错误'));
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message);
}
};
// 添加整个酒店过滤
const intoHotelBlack = async (hotelCode) => {
try {
const data = { HotelCode: hotelCode, HostNumberList: [] };
const rs = await customHttp.post('BlockIP/BlockLWSet', JSON.stringify(data));
if (rs.data.isok) {
ElMessage.success('过滤成功');
await getBlockList(); // 刷新过滤名单数据
} else {
ElMessage.error('操作失败: ' + (rs.data.message || '未知错误'));
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message);
}
};
// 批量添加过滤清单
const allbl = async () => {
const list = []
for (let i = 0; i < list.length; i++) {
console.log(list[i]);
const data = { HotelCode: list[i].toString(), HostNumberList: [] };
const rs = await customHttp.post('BlockIP/BlockLWSet', JSON.stringify(data));
}
}
// 加入过滤名单 (保持不变)
const joinBlack = async () => {
try {
if (!hotelCurrent.value) {
ElMessage.warning('请先选择酒店');
return;
}
const getdate = {
HotelCode: hotelCurrent.value.code,
HostNumberList: roomCurrent.value ?
roomCurrent.value.map(item => String(item.hostnumber)) :
[]
};
const rs = await customHttp.post('BlockIP/BlockLWSet', JSON.stringify(getdate));
if (rs.data.isok) {
ElMessage.success('操作成功');
hotelCurrent.value = null;
roomCurrent.value = null;
await getBlockList(); // 刷新过滤名单数据
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message);
}
};
// 获取所有酒店
const getAllHotels = async () => {
try {
const rs = await customHttp.post('LowerMachineLog/GetHotelList');
if (rs.data.isok) {
allHotels.value = rs.data.response || [];
// 更新所有酒店数据(含过滤名单状态)
allHotelData.value = mergeHotelBlacklistData();
}
} catch (error) {
ElMessage.error('获取酒店列表失败: ' + error.message);
}
};
// 获取过滤名单数据的方法
const getBlockList = async () => {
try {
const rs = await customHttp.post('BlockIP/GetBlockLWSetData');
if (rs.data.isok) {
const response = JSON.parse(rs.data.response.response);
allBlackList.value = response.Result || {};
// 更新所有酒店数据(含过滤名单状态)
//console.log(allBlackList.value);
allHotelData.value = mergeHotelBlacklistData();
blackListNum.value = Object.keys(allBlackList.value).length
}
} catch (error) {
ElMessage.error('获取过滤名单失败: ' + error.message);
}
};
// 酒店搜索
const remoteHotel = (query) => {
if (query) {
hotels.value = allHotels.value.filter(
item => item.code.includes(query) ||
item.name.includes(query)
);
} else {
hotels.value = [...allHotels.value];
}
};
// 搜索room
const remoteRoom = (query) => {
//console.log(query);
if (query != "" && (roomCurrent.value == null || roomCurrent.value == "")) {
rooms.value = [];
const nc = allRooms.value.filter(item => item.roomno.includes(query));
//console.log(nc);
if (nc.length > 0) {
rooms.value = nc;
} else {
rooms.value = JSON.parse(JSON.stringify(allRooms.value));
}
} else {
rooms.value = JSON.parse(JSON.stringify(allRooms.value));
}
}
const hotelChange = (hotel) => {
if (hotel == null || hotel == "") {
roomDisable.value = true;
hotels.value = JSON.parse(localStorage.getItem('hotelCurrent'));
} else {
saveObjectToLocalStorage('hotelCurrent', hotel);
isTheRoomGet.value = false;
getRooms(hotel);
roomDisable.value = false;
}
roomCurrent.value = null;
}
const getRooms = async (hotel) => {
try {
if (!isTheRoomGet.value) {
const getdate = {
hotel_code: hotel.code,
createDate: hotel.createdate,
};
//console.log(hotel);
const header = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
};
const rs = await $http.post('LowerMachineLog/GetHostList', qs.stringify(getdate), header);
if (rs.data.isok) {
rooms.value = JSON.parse(JSON.stringify(rs.data.response));
allRooms.value = JSON.parse(JSON.stringify(rs.data.response));
console.log(rooms.value);
}
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message);
}
};
// 计算表格最大高度
const tableMaxHeight = computed(() => {
return `calc(100vh - 125px)`
})
// 保存对象到localStorage
const saveObjectToLocalStorage = (key, object) => {
// 从localStorage获取旧的数组
const oldArrayString = localStorage.getItem(key);
let oldArray;
// 检查是否已经有旧的数组存在
if (oldArrayString) {
// 如果存在,则解析为对象数组
oldArray = JSON.parse(oldArrayString);
} else {
// 如果不存在,初始化为空数组
oldArray = [];
}
// 添加新对象到数组
oldArray.push(object);
const uniqueArray = removeDuplicatesById(oldArray);
// 将更新后的数组转换回字符串并保存到localStorage
localStorage.setItem(key, JSON.stringify(uniqueArray));
}
// 根据id去重
const removeDuplicatesById = (array) => {
//console.log(array);
const uniqueArray = array.reduce((acc, current) => {
// 检查累加器数组中是否已经存在具有相同id的对象
const exists = acc.some(item => item.id === current.id);
// 如果不存在,则添加到累加器数组中
if (!exists) {
acc.push(current);
}
return acc;
}, []);
return uniqueArray.sort((a, b) => a.id - b.id);;
}
// 监听酒店变化
/* watch(hotelCurrent, (newVal) => {
if (newVal) {
getRooms(newVal)
} else {
rooms.value = [];
}
});*/
const refreshData = async () => {
await getAllHotels();
await getBlockList();
currentPage.value = 1; // 重置到第一页
};
// 初始化
onMounted(async () => {
localStorage.setItem('url', '/blacklist');
await getAllHotels();
await getBlockList();
});
</script>
<style scoped>
.container {
padding: 10px;
overflow-x: auto;
}
.pagination-container {
display: flex;
justify-content: flex-start;
padding: 0;
gap: 10px;
flex-wrap: wrap; /* 允许换行 */
row-gap: 0px; /* 行间距 */
padding-bottom: 3px; /* 底部留空间 */
}
.el-table {
font-size: 14px;
}
:deep(.el-table__cell) {
padding: 1px !important;
}
:deep(.el-table .cell) {
padding: 1px 3px !important;
}
@media (max-width: 768px) {
.el-table {
font-size: 12px;
}
/* 使分页控件占据整行 */
.pagination-container > .el-pagination {
width: 100%;
justify-content: center;
}
}
</style>

View 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>

24
src/pages/home/index.vue Normal file
View File

@@ -0,0 +1,24 @@
<template>
Home
</template>
<script setup>
import { ref, inject, onMounted, watch, computed } from 'vue';
import { ElMessage, ElIcon, ElButton } from 'element-plus'
import { House, OfficeBuilding, Close, View, Checked, Connection } from '@element-plus/icons-vue'
import qs from 'qs'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const $http = inject('$http')
// 注入方法
const checkLoginStatus = inject('checkLoginStatus');
onMounted(() => {
router.push('/udplog')
checkLoginStatus()
})
</script>
<style scoped>
</style>

596
src/pages/login/index.vue Normal file
View File

@@ -0,0 +1,596 @@
<template>
<div style="height:100vh">
<div class="container">
<div class="logo-container">
<img src="../../../public/logobig.svg" class="log-img" />
<!--<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><span style="font-size:40px">BLS</span> 宝来威日志平台</h1>
<el-form :model="form"
status-icon
:disabled="isLocked"
class="login-form"
@submit.prevent="handleSubmit">
<!-- 账号输入 -->
<el-form-item prop="username">
<el-input v-model="form.username"
placeholder="请输入账号"
clearable
@focus="scrollToFormBottom"
:prefix-icon="User" />
</el-form-item>
<!-- 密码输入 -->
<el-form-item prop="password">
<el-input v-model="form.password"
type="password"
placeholder="请输入密码"
show-password
@focus="scrollToFormBottom"
:prefix-icon="Lock" />
</el-form-item>
<!-- 验证码区域 -->
<el-form-item prop="code" class="captcha-container">
<div class="captcha-input">
<el-input v-model="form.code"
@focus="scrollToFormBottom"
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 } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { User, Lock, Key } from '@element-plus/icons-vue'
import { useNow } from '@vueuse/core';
const checkLoginStatus = inject('checkLoginStatus');
const calculateTimeDiff = inject('calculateTimeDiff');
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)}`
})
// 方法实现
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 (isLocked.value || submitDisabled.value) return
if (!validateForm()) return
try {
loading.value = true
/*const response = await $http.post('LeiDa/Login', {
username: form.value.username,
password: form.value.password
})*/
const validUsers = {
"momo": "momo123",
"admin": "123456",
"blveng":"blw@123",
};
const usn = form.value.username.toLowerCase();
if (validUsers[usn]) { // 检查用户是否存在
if (form.value.password !== validUsers[usn]) {
ElMessage.error('密码错误!');
return;
}
// 密码正确,继续执行其他
} else {
ElMessage.error('账号不存在!');
return;
}
// 登录成功处理...临时...
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('username', form.value.username)
localStorage.setItem('login', true)
localStorage.setItem("TokenT", new Date())
// 记住账号和密码
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(`登录成功,欢迎:${form.value.username}`)
router.push('/home')
return
//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(JSON.parse(data.response))
} else {
handleLoginFailure(data.message)
}
}
// 滚动到表单底部(用于移动端输入框聚焦)
const scrollToFormBottom = () => {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
}
const handleLoginSuccess = (userData) => {
if (!userData.isok) {
handleLoginFailure(userData.message)
return
}
// 登录成功处理...
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('username', form.value.username)
localStorage.setItem('login', true)
localStorage.setItem('AccessibleHotels', JSON.stringify(userData.response))
localStorage.setItem("TokenT", new Date())
// 记住账号和密码
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(`登录成功,欢迎:${form.value.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)
}
}
// 验证时间
const TokenTCheck = () => {
generateCaptcha()
console.log(localStorage.getItem("TokenT"))
if (localStorage.getItem("TokenT")) {
if (calculateTimeDiff(localStorage.getItem("TokenT")) < 260000) {
form.value.code = captchaValue.value
if (validateForm()) {
handleSubmit()
}
} else {
localStorage.removeItem("TokenT")
}
}
}
// 初始化
onMounted(() => {
localStorage.setItem('url', '/login');
localStorage.removeItem('AccessibleHotels')
localStorage.setItem('login', false)
TokenTCheck()
initAuthState()
checkLoginStatus()
})
</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-title {
margin: 2rem 0;
text-align: center;
}
/* 外层容器样式 */
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;
}
.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);
}
/* 其他保持原有样式 */
h1 {
margin: 100px 0px 100px 0px;
text-align: center
}
.sub-text {
font-size: 12px;
letter-spacing: 0.5px;
text-align: left;
}
.log-img {
width: 400px;
height: auto;
}
/* 移动端适配 */
@media (max-width: 768px) {
.log-img {
width: 220px;
}
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 {
flex: 9;
}
.logo-container,
.login-container {
padding: 0rem;
justify-content: start;
}
.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;
}
h1 {
margin: 10px 0px 50px 0px;
}
.login-form {
padding: 0;
}
}
</style>

View File

@@ -0,0 +1,379 @@
<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(() => {
localStorage.setItem('url', '/logsetup');
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>

View File

@@ -0,0 +1,129 @@
<template>
<el-select v-model="roomHotelCode"
value-key="code"
filterable
clearable
remote
style="width: 150px; margin-right:7px"
size="small"
@focus="hotelChange"
@change="handleHotelForRooms"
:remote-method="remoteHotel"
placeholder="选择酒店">
<template #prefix>
<el-text size="small">{{ selectedHotelDisplay }}</el-text>
</template>
<el-option v-for="hotel in hotels"
:key="hotel.code"
:label="hotel.name"
:value="hotel.code">
<span style="float: left">{{ hotel.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">&nbsp;&nbsp;Code:{{ hotel.code }}</span>
</el-option>
</el-select>
</template>
<script setup>
import { ref, inject, computed, watch, onMounted, onUnmounted, nextTick, shallowRef } from 'vue';
import { ElMessage, ElButton, ElTag, ElCheckbox, ElMessageBox, ElLoading } from 'element-plus'
import { Setting, Refresh, ArrowDownBold, ArrowUpBold } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import config from '../../../public/config.js'
import { useRouter, useRoute } from 'vue-router'
import $ from 'jquery'
import qs from 'qs'
import { createAxiosInstance } from '../../axios.js'
const $http = inject('$http')
// 注入方法
const checkLoginStatus = inject('checkLoginStatus');
const isMobile = inject('isMobile');
const fullScreen = inject('fullScreen');
const customHttp = createAxiosInstance(1);
const rcuHttp = createAxiosInstance(2);
const roomHotelCode = ref(null);
const currentRoomHotelCode = ref('');
const allHotels = ref([]); // 所有酒店信息
const allRooms = ref([]); // 当前房间信息
const hotels = ref([]); // 所有酒店信息
const hotelRoomCounts = ref({});
const getPowerLog = async () => {
const QueryDate = {
HotelCode: '1003',
HostNumber: '',
Mac: '',
Start_Time: pickerDate.value[0],
End_Time: pickerDate.value[1],
PageSize: 1,
PageIndex: 100000,
};
//console.log(QueryDate);
const rs = await $http.post('Power/GetPowerAnalysis', JSON.stringify(QueryDate));
console.log(rs.data);
}
const getAllHotels = async () => {
try {
const rs = await customHttp.post('LowerMachineLog/GetHotelList');
if (rs.data.isok) {
allHotels.value = rs.data.response;
hotels.value = rs.data.response;
// 新增:获取所有酒店的房间数
try {
const roomCountRes = await customHttp.get('BlockIP/GetRoomCount');
roomCountRes.data.response.forEach(item => {
// 将房间数存储在hotelRoomCounts中key为酒店code
hotelRoomCounts.value[item.hotelID] = item.totalCount;
});
} catch (roomError) {
console.error('获取酒店房间数失败:', roomError);
}
// 设置酒店的默认房间数为"-1"(表示未知)
allHotels.value.forEach(hotel => {
hotel.count = hotelRoomCounts.value[hotel.id] || "-1";
});
}
} catch (error) {
// ...错误处理
}
};
// 搜索hotel
const remoteHotel = (query) => {
if (query != "" && (hotelCurrent.value == null || hotelCurrent.value == "")) {
hotels.value = [];
const nc = allHotels.value.filter(item => item.code.includes(query) || item.name.includes(query));
if (nc.length > 0) {
hotels.value = nc;
} else {
hotels.value = JSON.parse(JSON.stringify(allHotels.value));
}
} else {
hotels.value = JSON.parse(JSON.stringify(allHotels.value));
}
}
onMounted(() => {
localStorage.setItem('url', '/home')
checkLoginStatus()
// 同时获取所有房间和监控列表
getAllHotels();
})
</script>
<style scoped>
.container {
padding: 5px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,563 @@
<template>
<el-loading :loading="globalLoading" :text="loadingText" fullscreen />
<div class="container">
<!-- 日期筛选 -->
<div style="display: flex; align-items: center; margin-bottom: 3px;">
<div style="display: flex; margin-left:6px">
<el-button type="text" v-on:click="dateMobile(1)">1小时</el-button>
<el-button type="text" v-on:click="dateMobile(3)">3小时</el-button>
<el-button type="text" v-on:click="exportMainExcel">导出</el-button>
<div style="width: 30px"></div>
<el-button v-on:click="getTableData">刷新列表</el-button>
</div>
</div>
<div style="display: flex; margin-bottom:5px">
<span class="datePicerTxt">开始时间</span>
<el-date-picker v-model="pickerDate[0]"
type="datetime"
size="small"
style="width:150px;"
placeholder="请选择起始时间"
:disabled-date="disabledDate" />
<span class="datePicerTxt">结束时间</span>
<el-date-picker v-model="pickerDate[1]"
type="datetime"
size="small"
style="width:150px;"
placeholder="请选择结束时间"
:disabled-date="disabledDate" />
</div>
<!-- 分页控件 -->
<div style="display: flex; align-items: center; margin-bottom: 5px;">
<el-pagination v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[50, 100, 200, 400]"
pager-count="5"
:total="totalItems"
size="small"
layout="sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</div>
<!-- 总表 -->
<div style="overflow-x: auto; max-width:98vw;">
<el-table :data="paginatedTableData"
border
highlight-current-row
max-height="calc(100vh - 200px)"
style="width: 100%;"
v-loading="tableLoading">
<!-- 固定列序号 -->
<el-table-column label="序号" width="50" align="center">
<template #default="{ $index }">
{{ $index + 1 + (currentPage - 1) * pageSize }}
</template>
</el-table-column>
<!-- 固定列酒店 -->
<el-table-column label="酒店" width="50" align="center">
<template #default="{ row }">
{{ row.hotelCode }}
</template>
</el-table-column>
<!-- 固定列命令 -->
<el-table-column label="命令" width="50" align="center">
<template #default="{ row }">
{{ row.command }}
</template>
</el-table-column>
<!-- 新增列预处理时间Step2-Step1 -->
<el-table-column label="CallBack" width="75" align="right">
<template #default="{ row }">
<div v-if="row.steps[1] && row.steps[2]">
{{ formatMicros(row.steps[2].triggerMicros - row.steps[1].triggerMicros) }}
</div>
</template>
</el-table-column>
<!-- 新增固定列总耗时 -->
<el-table-column label="总耗时" width="75" align="right">
<template #default="{ row }">
{{ formatMicros(row.totalTimeDiff) }}
</template>
</el-table-column>
<!-- 动态列步骤时间 -->
<el-table-column v-for="(step, index) in visibleSteps"
:key="step"
:label="'s' + step"
align="right"
:width="step === 1 ? 160 : 80">
<template #default="{ row }">
<div v-if="row.steps[step]" class="step-cell">
<el-popover placement="top" trigger="hover" :show-after="333" width="270px">
<template #default>
<div style="max-width: 300px;">
<div>
<i>StepDes:</i> {{ row.steps[step].description }}
</div>
<div v-if="step !== 1">
<i>TimeDef:</i> {{ formatMicros(row.steps[step].timeDiff) }}
</div>
<div>
<i>TriTime:</i>&nbsp;&nbsp; {{ row.steps[step].triggerTimeString }}
</div>
<div v-if="step === 1">
<div>
<div><i>hostNumber:</i>&nbsp; {{ row.hostNumber }}</div>
<div><i>MessageId:</i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{{ row.everyMessageId }}</div>
<div><i>Monitor_0E_01:</i> {{ row.monitor_0E_01 }}</div>
<div><i>Content:</i> <div class="fontff">{{ row.content }}</div></div>
</div>
</div>
</div>
</template>
<template #reference>
<span v-if="step === 1">{{ row.steps[step].triggerTimeString.slice(5) }}</span>
<span v-else-if="step === 0">
{{ addCommas(parseFloat(row.steps[step].description).toFixed(0)) }}%
</span>
<span v-else-if="step === 0.1">
{{ extractThreadInfo(row.steps[step].description) }}
</span>
<span v-else>
{{ formatMicros(row.steps[step].timeDiff) }}
</span>
</template>
</el-popover>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script setup>
import { ref, inject, watch, onMounted, computed } from 'vue';
import { ElMessage, ElButton, ElPopover, ElLoading, ElPagination } from 'element-plus'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
// 注册时区插件
dayjs.extend(utc)
dayjs.extend(timezone)
/** 引入变量 */
const $http = inject('$http')
const isMobile = inject('isMobile');
const tableLoading = ref(false)
const globalLoading = ref(false)
const loadingText = ref('加载中...')
// 默认时间范围
const pickerDate = ref([
dayjs().subtract(1, 'hour').format('YYYY-MM-DD HH:mm:ss'),
dayjs().format('YYYY-MM-DD HH:mm:ss')
]);
// 分页相关变量
const currentPage = ref(1)
const pageSize = ref(100)
const totalItems = ref(0)
// 原始表格数据
const tableData = ref([])
// 处理后的表格数据
const processedTableData = ref([])
// 动态步骤列表
const dynamicSteps = ref([])
// 计算属性只显示前100个步骤
const visibleSteps = computed(() => {
return dynamicSteps.value
})
// 计算属性:分页后的数据
const paginatedTableData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
//console.log(processedTableData.value)
return processedTableData.value.slice(start, end)
})
// 日期选择方法
const dateMobile = (hours) => {
const end = dayjs().format('YYYY-MM-DD HH:mm:ss');
const start = dayjs().subtract(hours, 'hour').format('YYYY-MM-DD HH:mm:ss');
pickerDate.value = [start, end];
getTableData();
};
// 禁用日期函数
const disabledDate = (time) => {
return time.getTime() > Date.now();
};
// 格式化本地时间
const formatLocalTime = (utcTime) => {
return dayjs.utc(utcTime).tz(dayjs.tz.guess()).format('YYYY-MM-DD HH:mm:ss.SSS')
}
// 分页大小变化处理
const handleSizeChange = (newSize) => {
pageSize.value = newSize
currentPage.value = 1
}
// 当前页变化处理
const handleCurrentChange = (newPage) => {
currentPage.value = newPage
}
// 导出Excel
const exportMainExcel = () => {
if (processedTableData.value.length === 0) {
ElMessage.warning('没有数据可导出')
return
}
try {
globalLoading.value = true
loadingText.value = '正在导出数据...'
// 创建工作簿
const wb = XLSX.utils.book_new()
// 准备表头
const headers = ['序号', '命令', '总耗时', ...dynamicSteps.value.map(step => `s${step}`)]
// 准备数据
const data = processedTableData.value.map((row, index) => {
const preprocessTime = (row.steps[1] && row.steps[3])
? row.steps[3].triggerMicros - row.steps[1].triggerMicros
: null;
const rowData = [index + 1, row.command, row.totalTimeDiff, formatMicros(preprocessTime),]
dynamicSteps.value.forEach(step => {
if (row.steps[step]) {
rowData.push(step === 1 ?
formatLocalTime(row.steps[step].triggerTimeString) :
`${row.steps[step].timeDiff} ms`)
} else {
rowData.push('')
}
})
return rowData
})
// 添加表头
data.unshift(headers)
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(data)
// 将工作表添加到工作簿
XLSX.utils.book_append_sheet(wb, ws, '时间分析数据')
// 导出文件
XLSX.writeFile(wb, `时间分析_${dayjs().format('YYYYMMDD_HHmmss')}.xlsx`)
ElMessage.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败: ' + error.message)
} finally {
globalLoading.value = false
}
}
// 数据处理函数
const processTableData = (data) => {
if (!data || data.length === 0) return [];
console.time('数据处理耗时');
const allSteps = new Set();
const processedData = [];
// 收集所有步骤ID
data.forEach(item => {
item?.forEach(step => step?.step !== undefined && allSteps.add(step.step));
});
// 排序步骤
dynamicSteps.value = Array.from(allSteps).sort((a, b) => a - b);
// 处理每条数据
data.forEach(item => {
// 安全处理 - 如果 item 无效则跳过
if (!Array.isArray(item) || item.length === 0) return;
// 创建步骤的映射以step为键
const stepsMap = {};
// 首先将所有步骤转换为微秒时间戳并存储
const stepsWithMicros = item
.filter(step => step && step.triggerTimeString) // 过滤无效步骤
.map(step => ({
...step,
triggerMicros: timeStringToMicros(step.triggerTimeString) || 0
}));
// 按时间戳排序
stepsWithMicros.sort((a, b) => a.triggerMicros - b.triggerMicros);
// 提取命令和内容(包含错误处理)
let command = '';
let content = '';
// 尝试找到 step=1 的对象
const step1Obj = stepsWithMicros.find(step => step.step === 1);
// 如果存在 step=1 则使用它,否则使用数组中的第一个对象
const sourceObj = step1Obj || stepsWithMicros[0];
// 从源对象获取内容数据
if (sourceObj?.content) {
try {
const base64Data = sourceObj.content;
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
content = Array.from(bytes)
.map(b => b.toString(16).toUpperCase().padStart(2, '0'))
.join('');
if (content.length > 19) {
command = content.substring(16, 18);
}
} catch (e) {
console.error('命令解析错误:', e);
}
}
// 计算时间差
let minTime = Infinity;
let maxTime = -Infinity;
stepsWithMicros.forEach(step => {
if (step?.triggerMicros !== undefined) {
minTime = Math.min(minTime, step.triggerMicros);
maxTime = Math.max(maxTime, step.triggerMicros);
// 确保我们为步骤创建一个有效的对象
stepsMap[step.step] = {
triggerTimeString: step.triggerTimeString,
description: step.stepDescription,
triggerMicros: step.triggerMicros
};
}
});
// 第二次遍历计算差值(添加安全性检查)
const orderedSteps = Object.values(stepsMap)
.filter(step => step !== undefined && step !== null) // 过滤无效步骤
.sort((a, b) => a.triggerMicros - b.triggerMicros);
let prevMicros = null;
orderedSteps.forEach(step => {
if (step && typeof step === 'object') {
step.timeDiff = prevMicros !== null
? step.triggerMicros - prevMicros
: 0;
prevMicros = step.triggerMicros;
}
});
// 更新回stepsMap确保存在有效的步骤数据
if (Object.keys(stepsMap).length > 0) {
processedData.push({
command,
content: content?.match(/.{1,2}/g)?.join(' ') || '',
steps: { ...stepsMap }, // 创建副本避免引用问题
hotelCode: sourceObj?.hotelCode || '',
hostNumber: sourceObj?.hostNumber || '',
everyMessageId: sourceObj?.everyMessageId || '',
monitor_0E_01: sourceObj?.monitor_0E_01 || '',
totalTimeDiff: minTime !== Infinity && maxTime !== -Infinity ? maxTime - minTime : 0
});
}
});
console.timeEnd('数据处理耗时');
return processedData;
};
const getTableData = async () => {
try {
tableLoading.value = true
globalLoading.value = true
const getdate = {
"Start_Time": dayjs(pickerDate.value[0]).format('YYYY-MM-DD HH:mm:ss'),
"End_Time": dayjs(pickerDate.value[1]).format('YYYY-MM-DD HH:mm:ss'),
"PageSize": 1000000,
"PageIndex": 1
};
const rs = await $http.post('UDPPackage/GetUDPPackageTimeAnalysis', JSON.stringify(getdate));
console.log(rs.data)
if (rs.data.isok) {
tableData.value = rs.data.response || []
processedTableData.value = processTableData(tableData.value)
totalItems.value = processedTableData.value.length
}
} catch (error) {
console.error('获取数据失败:', error);
ElMessage.error('获取数据失败: ' + error.message)
} finally {
tableLoading.value = false
globalLoading.value = false
}
};
const formatMicros = (micros) => {
if (isNaN(micros)) return '';
// 转换为毫秒并保留3位小数
return addCommas(micros)
/*micros > 10000000000
? addCommas((micros / 1000).toFixed(0))
: addCommas(micros);*/
};
const addCommas = (number) => {
if (isNaN(number)) return '';
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
// 将时间字符串YYYY-MM-DD HH:mm:ss.SSSSSS转换为微秒时间戳
// 修复后的时间字符串转微秒函数
const timeStringToMicros = (timeStr) => {
if (!timeStr) return 0;
// 使用正则提取所有时间组件
const match = timeStr.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\.?(\d*)$/);
if (!match) return 0;
const [, year, month, day, hours, minutes, seconds, milliseconds] = match;
// 创建完整的日期对象UTC时间
const date = new Date(Date.UTC(
parseInt(year),
parseInt(month) - 1,
parseInt(day),
parseInt(hours),
parseInt(minutes),
parseInt(seconds)
));
// 获取基本毫秒时间戳
const baseMillis = date.getTime();
// 处理微秒部分6位数字
let microsPart = 0;
if (milliseconds) {
// 确保微秒部分为6位不足补0超过截断
const padded = milliseconds.padEnd(6, '0').slice(0, 6);
microsPart = parseInt(padded, 10);
}
// 总微秒 = 毫秒部分*1000 + 微秒部分
return baseMillis * 1000 + microsPart;
};
const extractThreadInfo = (str) => {
// 使用正则表达式匹配所需的三组数字
const threadRegex = /工作线程(\d+):.*?使用中=(\d+).*?使用中=(\d+)$/;
// 尝试匹配字符串
const match = str.match(threadRegex);
// 如果匹配成功
if (match) {
const threadId = match[1]; // 工作线程ID
const workerInUse = match[2]; // 工作线程使用中数量
const ioInUse = match[3]; // IO线程使用中数量
// 格式化输出结果
return `${threadId} | ${workerInUse} | ${ioInUse}`;
}
// 如果没有匹配到模式,返回错误信息
return "无法从字符串中提取所需信息";
}
// 初始化加载数据
onMounted(async () => {
localStorage.setItem('url', '/tasktimelog');
await getTableData();
});
</script>
<style scoped>
.container {
padding: 8px;
overflow-x: auto;
}
/* 表格单元格样式 */
:deep(.el-table__cell) {
padding: 0 !important;
}
:deep(.el-table .cell) {
padding: 0;
line-height: 1.1;
}
/* 进度条样式 */
:deep(.el-progress-bar .el-progress-bar__outer) {
border-radius: inherit;
background: none;
}
:deep(.el-progress-bar .el-progress-bar__inner) {
border-radius: inherit;
}
/* 加载层样式 */
.fullscreen-loading,
:deep(.custom-fullscreen-loading) {
position: relative;
z-index: 99999;
}
/* 自定义字体 */
@font-face {
font-family: 'sourcecode';
src: url('../../assets/sourcecode.ttf') format('truetype');
}
.fontff {
font-family: 'sourcecode', sans-serif;
}
.datePicerTxt {
display: flex;
align-items: center;
margin: 0 5px;
font-size: 14px;
}
.step-cell {
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 自定义字体 */
@font-face {
font-family: 'sourcecode';
src: url('../../assets/sourcecode.ttf') format('truetype');
}
.fontff {
font-family: 'sourcecode', sans-serif;
}
</style>

View File

@@ -0,0 +1,774 @@
<template>
<!--<el-button type="info" plain @click="allbl">测试</el-button>-->
<div class="container">
<div class="pagination-container">
<el-input v-model="searchKeyword"
placeholder="搜索酒店名称或编号"
clearable
size="small"
style="width: 200px;"
@input="handleSearch">
<template #prefix>
<el-icon><search /></el-icon>
</template>
</el-input>
<el-checkbox v-model="showOnlyBlack" size="small">
只显示上传列表
</el-checkbox>
<!-- 白名单名单数 -->
<el-text>白名单名单: {{ blackListNum }} | : {{ allHotelData.length }} </el-text>
<!-- 翻页控件 -->
<el-pagination size="small"
layout=" prev, pager, next, sizes"
:page-size="pageSize"
:page-sizes="[100, 200, 300, 500]"
:total="filteredData.length"
pager-count="5"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
v-model:current-page="currentPage">
</el-pagination>
</div>
<div style="overflow-x: auto; max-width:98vw;">
<el-table :data="paginatedData"
border
size="small"
highlight-current-row
:max-height="tableMaxHeight"
style="width: 100%;">
<el-table-column label="整体操作" width="100">
<template #default="{ row }">
<span v-if="row.isBlack">
<el-button type="warning" size="small" plain @click="openCancelDialog(row.code)">
取消上传
</el-button>
</span>
<span v-else>
<el-button type="danger" size="small" plain @click="openHotelTFTPSetting(row.code)">
全酒店上传
</el-button>
</span>
</template>
</el-table-column>
<el-table-column prop="code" label="编号" width="50">
<template #default="{ row }">
<span v-if="row.isBlack"><el-text type="danger">{{ row.code }}</el-text></span>
<span v-if="!row.isBlack"><el-text type="success">{{ row.code }}</el-text></span>
</template>
</el-table-column>
<el-table-column prop="name" label="酒店名称" :width="isMoblie ? 210 : 160">
<template #default="{ row }">
<span v-if="row.isBlack"><el-text type="danger">{{ row.name }}</el-text></span>
<span v-if="!row.isBlack"><el-text type="success">{{ row.name }}</el-text></span>
</template>
</el-table-column>
<el-table-column label="TFTP房间数" width="95">
<template #default="{ row }">
<!--<span><el-text type="danger">3599</el-text></span>-->
<span v-if="row.isBlack"><el-button type="danger" link @click="openRoomsDialog(row)">{{ row.blackList.length }}</el-button></span>
<span v-if="!row.isBlack"><el-button type="success" link @click="openRoomsDialog(row)">0</el-button></span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 房间表 -->
<el-dialog v-model="roomsDialogVisible"
:fullscreen="isMobile"
top="0"
style="margin:0 auto;height:100vh"
:close-on-click-modal="false"
width="680">
<!-- 自定义标题栏 -->
<template #header>
<div style="display: flex; align-items: center;">
<span>
{{ currentHotelCode }} - 房间操作
</span>
</div>
</template>
<el-table :data="allRooms"
border
size="small"
class="fontff"
max-height="calc(100vh - 58px)">
<el-table-column label="操作" width="105">
<template #default="{ row }">
<span v-if="isRoomInBlackList(row)">
<el-button type="warning" size="small" plain @click="cancelRoomBlack(row)">
取消上传
</el-button>
</span>
<span v-else>
<el-button type="danger" size="small" plain @click="openRoomTFTPSetting(row)">
本房间上传
</el-button>
</span>
</template>
</el-table-column>
<el-table-column prop="roomno" label="房号" min-width="60" align="center">
<template #default="{ row }">
<el-text :type="row.status ? 'success' : 'info'" size="small">{{ row.roomno }}</el-text>
</template>
</el-table-column>
<el-table-column prop="LauncherVersion" label="Laun" min-width="50" align="center">
<template #default="{ row }">
<el-text :type="row.status ? 'success' : 'info'" size="small">{{ row.LauncherVersion?.slice(13) || '无' }}</el-text>
</template>
</el-table-column>
<el-table-column prop="ConfigurationVersion" label="配置" min-width="58" align="center">
<template #default="{ row }">
<el-text :type="row.status ? 'success' : 'info'" size="small">{{ row.ConfigurationVersion || '无' }}</el-text>
</template>
</el-table-column>
<el-table-column prop="MachineType" label="机型" min-width="45" align="center">
<template #default="{ row }">
<el-text :type="row.status ? 'success' : 'info'" size="small">
{{ row.MachineType?.replace(/\u0000/g,'')?.replace(/BLV-/g,'') || "无"}}
</el-text>
</template>
</el-table-column>
<el-table-column prop="Version" label="固件" min-width="98" align="center">
<template #default="{ row }">
<el-text :type="row.status ? 'success' : 'info'" size="small">{{ row.Version.slice(6) }}</el-text>
</template>
</el-table-column>
<el-table-column prop="Version" label="MAC" min-width="142" align="center">
<template #default="{ row }">
<el-text :type="row.status ? 'success' : 'info'" size="small">{{ row.mac }}</el-text>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- TFTP设置弹窗 -->
<el-dialog v-model="tftpSettingDialogVisible"
:fullscreen="isMobile"
title="TFTP上传设置"
width="450"
:close-on-click-modal="false">
<el-form :model="tftpSettingsForm" label-width="60px" size="small">
<el-form-item label="端口" prop="port">
<el-input-number v-model="tftpSettingsForm.port"
:min="0"
:max="65535"
controls-position="right"
style="width: 120px" />
</el-form-item>
<el-form-item label="域名" prop="domain">
<el-input v-model="tftpSettingsForm.domain" style="width:280px" placeholder="请输入域名" />
</el-form-item>
<el-form-item label="时间(h)" prop="time">
<el-input-number v-model="tftpSettingsForm.time"
:min="1"
:max="360"
controls-position="right"
style="width: 120px" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="tftpSettingDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmTFTPSettings">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, inject, computed, onMounted, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh, Search } from '@element-plus/icons-vue'
import config from '../../../public/config.js'
import qs from 'qs'
import { useRouter, useRoute } from 'vue-router'
import { createAxiosInstance } from '../../axios.js'
// 引入Vue工具
const router = useRouter()
const $http = inject('$http')
const isMobile = inject('isMobile');
const checkLoginStatus = inject('checkLoginStatus');
const customHttp = createAxiosInstance(1);
const rcuHttp = createAxiosInstance(2);
// 常量
const tftpSetDefault = {
enabled: false,
port: 69,
domain: 'blv-tftp-log.blv-oa.com',
time: 12,
}
// 响应式变量
const globalLoading = ref(false);
const loadingText = ref('加载中,请稍候...');
const allUpdLogsList = ref([]);
const tableData = ref([]);
const commandTypes = ref([]);
const allHotels = ref([]);
const hotels = ref([]);
const rooms = ref([]);
const allRooms = ref([]);
const hotelCurrent = ref(null);
const roomCurrent = ref(null);
const roomDisable = ref(true);
const isTheRoomGet = ref(false);
const searchKeyword = ref(''); // 搜索关键词
const blackListNum = ref(null);
// 分页相关变量
const currentPage = ref(1);
const pageSize = ref(100); // 默认每页100条
const showOnlyBlack = ref(true); // 默认只显示白名单名单酒店
const allHotelData = ref([]);
const allBlackList = ref([]);
// 添加对话框相关变量
const cancelDialogVisible = ref(false);
const intoDialogVisible = ref(false);
const currentHotelCode = ref('');
const roomsDialogVisible = ref(false);
const currentHotelBlackList = ref(false);
const tftpSettingDialogVisible = ref(false);
const tftpSettingsForm = ref({
port: tftpSetDefault.port,
domain: tftpSetDefault.domain,
time: tftpSetDefault.time
});
const currentAction = ref({
type: '', // 'room' 或 'hotel'
data: null // 房间对象或酒店代码
});
// TFTP设置
const tftpSettings = ref({
enabled: false,
port: 69,
domain: 'blv-tftp-log.blv-oa.com',
time: 12,
});
// 合并酒店和白名单名单数据
const mergeHotelBlacklistData = () => {
if (!Array.isArray(allHotels.value) || !Array.isArray(allBlackList.value)) return [];
// 将 allBlackList.value 转换为以 hotelId 为键的 Map
const blacklistMap = new Map();
allBlackList.value.forEach(hotel => {
blacklistMap.set(hotel.hotelid, hotel.tftpRoomList);
});
return allHotels.value.map(hotel => {
const isBlack = blacklistMap.has(hotel.id.toString());
const blackList = isBlack ? blacklistMap.get(hotel.id.toString()) : [];
return {
...hotel,
isBlack,
blackList
};
});
};
// 筛选后的数据
const filteredData = computed(() => {
let result = allHotelData.value;
// 应用搜索白名单
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase();
result = result.filter(hotel =>
hotel.name.toLowerCase().includes(keyword) ||
hotel.code.toString().includes(keyword)
);
}
// 应用白名单名单筛选
if (showOnlyBlack.value) {
result = result.filter(hotel => hotel.isBlack);
}
return result;
});
// 打开取消白名单对话框
const openCancelDialog = (hotelCode) => {
currentHotelCode.value = hotelCode;
ElMessageBox.confirm(
'确定取消该酒店的白名单吗?',
'取消白名单确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
cancelHotelBlack(hotelCode);
}).catch(() => {
// 用户取消操作
});
};
// 打开白名单对话框
const openIntoDialog = (hotelCode) => {
currentHotelCode.value = hotelCode;
ElMessageBox.confirm(
'确定白名单该酒店吗?',
'白名单确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
intoHotelBlack(hotelCode);
}).catch(() => {
// 用户取消操作
});
};
// 当前页的数据
const paginatedData = computed(() => {
const startIndex = (currentPage.value - 1) * pageSize.value;
const endIndex = startIndex + pageSize.value;
return filteredData.value.slice(startIndex, endIndex);
});
// 处理分页大小变化
const handleSizeChange = (newSize) => {
pageSize.value = newSize;
currentPage.value = 1; // 重置到第一页
};
// 处理当前页变化
const handleCurrentChange = (newPage) => {
currentPage.value = newPage;
};
const handleSearch = () => {
currentPage.value = 1; // 搜索时重置到第一页
};
// 修改callTFTPSetExecute函数以接受参数
const callTFTPSetExecute = async (hostDataList, isEnable, port, domain, time) => {
try {
const params = {
host_data: hostDataList,
isenable: isEnable,
domain: domain,
port: port,
lasttime: time
};
console.log(params);
const rs = await rcuHttp.post('values/TFTPSet_Execute', JSON.stringify(params));
if (rs.data.isok) {
return true;
} else {
throw new Error(rs.data.message || '操作失败');
}
} catch (error) {
throw error;
}
};
// 取消整个酒店白名单
const cancelHotelBlack = async (hotelCode) => {
try {
// 获取酒店的房间列表
const rooms = await fetchRoomsByHotelCode(hotelCode);
// 构建host_data列表
const hostDataList = rooms.map(room => ({
hostnumber: room.hostnumber,
mac: room.mac
}));
const success = await callTFTPSetExecute(
hostDataList,
false,
tftpSetDefault.port,
tftpSetDefault.domain,
tftpSetDefault.time
);
if (success) {
ElMessage.success('取消白名单成功');
await getBlockList();
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message);
}
};
// 添加整个酒店白名单
const intoHotelBlack = async (hotelCode, port, domain, time) => {
try {
// 获取酒店的房间列表
const rooms = await fetchRoomsByHotelCode(hotelCode);
// 构建host_data列表
const hostDataList = rooms.map(room => ({
hostnumber: room.hostnumber,
mac: room.mac
}));
const success = await callTFTPSetExecute(hostDataList, true, port, domain, time);
if (success) {
ElMessage.success('白名单成功');
await getBlockList();
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message);
}
};
// 获取酒店房间列表的辅助函数
const fetchRoomsByHotelCode = async (hotelCode) => {
try {
const hotel = allHotels.value.find(h => h.code === hotelCode);
if (!hotel) {
throw new Error('未找到酒店信息');
}
const getdate = {
hotel_code: hotelCode,
createdate: hotel.createdate,
};
const header = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
};
const rs = await customHttp.post('LowerMachineLog/GetHostList', qs.stringify(getdate), header);
if (rs.data.isok) {
return rs.data.response;
} else {
throw new Error('获取房间数据失败');
}
} catch (error) {
throw error;
}
};
// 取消房间白名单
const cancelRoomBlack = async (room) => {
try {
const hostDataList = [{
hostnumber: room.hostnumber,
mac: room.mac
}];
const success = await callTFTPSetExecute(
hostDataList,
false,
tftpSetDefault.port,
tftpSetDefault.domain,
tftpSetDefault.time
);
if (success) {
ElMessage.success('取消房间白名单成功');
await getBlockList();
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message);
}
};
// 添加房间到白名单
const intoRoomBlack = async (room, port, domain, time) => {
try {
const hostDataList = [{
hostnumber: room.hostnumber,
mac: room.mac
}];
const success = await callTFTPSetExecute(hostDataList, true, port, domain, time);
if (success) {
ElMessage.success('添加房间到白名单成功');
await getBlockList();
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message);
}
};
// 打开TFTP设置弹窗房间级别
const openRoomTFTPSetting = (room) => {
currentAction.value = {
type: 'room',
data: room
};
tftpSettingsForm.value = { ...tftpSetDefault }; // 重置为默认值
tftpSettingDialogVisible.value = true;
};
// 打开TFTP设置弹窗酒店级别
const openHotelTFTPSetting = (hotelCode) => {
currentAction.value = {
type: 'hotel',
data: hotelCode
};
tftpSettingsForm.value = { ...tftpSetDefault }; // 重置为默认值
tftpSettingDialogVisible.value = true;
};
// 确认TFTP设置并执行上传
const confirmTFTPSettings = async () => {
tftpSettingDialogVisible.value = false;
const { port, domain, time } = tftpSettingsForm.value;
if (currentAction.value.type === 'room') {
await intoRoomBlack(currentAction.value.data, port, domain, time);
} else if (currentAction.value.type === 'hotel') {
await intoHotelBlack(currentAction.value.data, port, domain, time);
}
};
// 获取所有酒店
const getAllHotels = async () => {
try {
const rs = await customHttp.post('LowerMachineLog/GetHotelList');
if (rs.data.isok) {
allHotels.value = rs.data.response || [];
// 更新所有酒店数据(含白名单名单状态)
allHotelData.value = mergeHotelBlacklistData();
console.log(allHotelData.value);
}
} catch (error) {
ElMessage.error('获取酒店列表失败: ' + error.message);
}
};
// 获取白名单名单数据的方法
const getBlockList = async () => {
try {
const rs = await rcuHttp.post('values/GetTFTPInfo');
if (rs.data.isok) {
// 确保 allBlackList.value 是数组
allBlackList.value = rs.data.response || [];
// 更新所有酒店数据(含白名单状态)
allHotelData.value = mergeHotelBlacklistData();
blackListNum.value = allBlackList.value.length;
console.log(allBlackList.value);
}
} catch (error) {
ElMessage.error('获取白名单失败: ' + error.message);
}
};
// 打开房间弹窗
const openRoomsDialog = async (row) => {
try {
// 准备数据
const getdate = {
hotel_code: row.code,
createdate: row.createdate,
};
// 调用获取房间方法
const header = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
};
const rs = await customHttp.post('LowerMachineLog/GetHostList', qs.stringify(getdate), header);
if (rs.data.isok) {
allRooms.value = JSON.parse(JSON.stringify(rs.data.response));
currentHotelCode.value = row.code;
// 确保 blackList 是数组
currentHotelBlackList.value = Array.isArray(row.blackList) ? row.blackList : [];
roomsDialogVisible.value = true;
} else {
ElMessage.error('获取房间数据失败');
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message);
}
};
// 酒店搜索
const remoteHotel = (query) => {
if (query) {
hotels.value = allHotels.value.filter(
item => item.code.includes(query) ||
item.name.includes(query)
);
} else {
hotels.value = [...allHotels.value];
}
};
// 搜索room
const remoteRoom = (query) => {
//console.log(query);
if (query != "" && (roomCurrent.value == null || roomCurrent.value == "")) {
rooms.value = [];
const nc = allRooms.value.filter(item => item.roomno.includes(query));
//console.log(nc);
if (nc.length > 0) {
rooms.value = nc;
} else {
rooms.value = JSON.parse(JSON.stringify(allRooms.value));
}
} else {
rooms.value = JSON.parse(JSON.stringify(allRooms.value));
}
}
const hotelChange = (hotel) => {
if (hotel == null || hotel == "") {
roomDisable.value = true;
hotels.value = JSON.parse(localStorage.getItem('hotelCurrent'));
} else {
saveObjectToLocalStorage('hotelCurrent', hotel);
isTheRoomGet.value = false;
getRooms(hotel);
roomDisable.value = false;
}
roomCurrent.value = null;
}
const getRooms = async (hotel) => {
try {
if (!isTheRoomGet.value) {
const getdate = {
hotel_code: hotel.code,
createDate: hotel.createdate,
};
//console.log(hotel);
const header = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
};
const rs = await $http.post('LowerMachineLog/GetHostList', qs.stringify(getdate), header);
if (rs.data.isok) {
rooms.value = JSON.parse(JSON.stringify(rs.data.response));
allRooms.value = JSON.parse(JSON.stringify(rs.data.response));
console.log(rooms.value);
}
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message);
}
};
// 计算表格最大高度
const tableMaxHeight = computed(() => {
return `calc(100vh - 125px)`
})
// 判断房间是否在白名单中
const isRoomInBlackList = (room) => {
// 确保 currentHotelBlackList.value 是一个数组
const blackList = Array.isArray(currentHotelBlackList.value) ? currentHotelBlackList.value : [];
return blackList.some(item => item.hostid === room.id.toString());
};
// 保存对象到localStorage
const saveObjectToLocalStorage = (key, object) => {
// 从localStorage获取旧的数组
const oldArrayString = localStorage.getItem(key);
let oldArray;
// 检查是否已经有旧的数组存在
if (oldArrayString) {
// 如果存在,则解析为对象数组
oldArray = JSON.parse(oldArrayString);
} else {
// 如果不存在,初始化为空数组
oldArray = [];
}
// 添加新对象到数组
oldArray.push(object);
const uniqueArray = removeDuplicatesById(oldArray);
// 将更新后的数组转换回字符串并保存到localStorage
localStorage.setItem(key, JSON.stringify(uniqueArray));
}
// 根据id去重
const removeDuplicatesById = (array) => {
//console.log(array);
const uniqueArray = array.reduce((acc, current) => {
// 检查累加器数组中是否已经存在具有相同id的对象
const exists = acc.some(item => item.id === current.id);
// 如果不存在,则添加到累加器数组中
if (!exists) {
acc.push(current);
}
return acc;
}, []);
return uniqueArray.sort((a, b) => a.id - b.id);;
}
// 监听酒店变化
/* watch(hotelCurrent, (newVal) => {
if (newVal) {
getRooms(newVal)
} else {
rooms.value = [];
}
});*/
const refreshData = async () => {
await getAllHotels();
await getBlockList();
currentPage.value = 1; // 重置到第一页
};
// 初始化
onMounted(async () => {
localStorage.setItem('url', '/tftpwhitelist');
await getAllHotels();
await getBlockList();
});
</script>
<style scoped>
.container {
padding: 10px;
overflow-x: auto;
}
.pagination-container {
display: flex;
justify-content: flex-start;
padding: 0;
gap: 10px;
flex-wrap: wrap; /* 允许换行 */
row-gap: 0px; /* 行间距 */
padding-bottom: 3px; /* 底部留空间 */
}
.el-table {
font-size: 14px;
}
/* 表格单元格样式 */
:deep(.el-table__cell) {
padding: 0 !important;
}
:deep(.el-table .cell) {
padding: 0;
line-height: 1.1;
}
/* 对话框样式优化 */
:deep(.el-dialog__header) {
padding-bottom: 3px !important;
}
:deep(.el-table--small .el-table__cell .cell) {
padding: 1px !important;
}
/* 表单项间距 */
:deep(.el-form-item) {
margin-bottom: 3px !important;
}
@media (max-width: 768px) {
.el-table {
font-size: 12px;
}
/* 使分页控件占据整行 */
.pagination-container > .el-pagination {
width: 100%;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
</template>
<script setup>
import { ref, inject, onMounted, watch, computed } from 'vue';
import { ElMessage, ElIcon, ElButton } from 'element-plus'
import { House, OfficeBuilding, Close, View, Checked, Connection } from '@element-plus/icons-vue'
import qs from 'qs'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const $http = inject('$http')
// 注入方法
const checkLoginStatus = inject('checkLoginStatus');
onMounted(() => {
router.push('/udplog')
checkLoginStatus()
})
</script>
<style scoped>
</style>

1846
src/pages/udplog/index.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,893 @@
<template>
<!-- 顶部导航栏以及弹窗 -->
<div class="headbox">
<!-- 按钮行 -->
<div class="seachbox">
<!-- 按钮容器 -->
<div v-if="!isMobile" class="button-container">
<div>
<el-button @click="handleLoadData" size="small" type="success" plain :disabled="buttonDisabled">加载数据</el-button>
</div>
</div>
<div v-else style="display:inline-block;margin-left:6px">
<div style="display:inline-block;">
<el-button v-if="isMobile" @click="toggleSearchControls" size="small" type="info" plain>
{{ showSearchControls ? '收起搜索' : '展开搜索' }}
</el-button>
</div>
<div style="display: inline-block;">
<el-button @click="handleLoadData" size="small" type="success" plain :disabled="buttonDisabled">加载数据</el-button>
</div>
</div>
<!-- 所有搜索控件 -->
<div v-if="!isMobile || showSearchControls" class="search-controls-container">
<el-select v-model="roomHotelCode"
value-key="code"
multiple
filterable
clearable
remote
collapse-tags
collapse-tags-tooltip
size="small"
style="width: 280px; margin-right:1px"
@change="handleHotelForRooms"
:remote-method="remoteHotel"
placeholder="全部酒店">
<el-option v-for="hotel in hotels"
:key="hotel.code"
:label="hotel.name"
:value="hotel.code">
<span class="option-left">{{ hotel.name }}</span>
<span class="option-right">&nbsp;&nbsp;Code:{{ hotel.code }}</span>
</el-option>
</el-select>
<el-select v-model="roomCurrentArray"
v-if="roomHotelCode.length == 1"
value-key="id"
multiple
filterable
clearable
remote
size="small"
style="width: 280px; margin-right:1px"
collapse-tags
collapse-tags-tooltip
:disabled="roomDisable"
:remote-method="remoteRoom"
placeholder="全部房间">
<el-option v-for="room in rooms"
:key="room.id"
:label="room.roomno"
:value="room">
<span class="option-left">{{ room.roomno }}</span>
<span class="option-right">&nbsp;&nbsp;&nbsp;&nbsp;Mac:{{ room.mac }}</span>
</el-option>
</el-select>
<span class="date-range-container">
<span class="date-item">
<span class="demonstration">开始时间</span>
<el-date-picker v-model="pickerDate[0]"
type="datetime"
size="small"
style="width: 200px;"
placeholder="开始时间"
:default-value="yesterday">
</el-date-picker>
</span>
<span class="date-item">
<span class="demonstration">结束时间</span>
<el-date-picker v-model="pickerDate[1]"
type="datetime"
size="small"
style="width: 200px;"
placeholder="结束时间"
:default-value="new Date()">
</el-date-picker>
</span>
<el-switch v-model="onlyCorrect"
size="small"
:active-text="activeTxt"
:inactive-text="inactiveTxt">
</el-switch>
</span>
</div>
</div>
</div>
<!-- 主体内容 -->
<div class="log-container">
<el-collapse v-model="activeNames">
<!-- 外层循环 requestId 分组 -->
<el-collapse-item v-for="(logss, requestId) in filteredLogs" :key="requestId" :name="requestId">
<template #title>
<div class="title-container">
<div class="collapse-title">
<span>
<el-text type="info" size="small" line-clamp="1">{{ logss[0].timeDisparity+ 'ms' }}</el-text>
<el-text type="info" size="small" line-clamp="1">&nbsp;|&nbsp;</el-text>
<el-text :type="logss[0].lastStep != 5 ? 'danger' : 'success'" line-clamp="1">{{ logss[0].hotelName }}</el-text>
<el-text type="info" size="small" line-clamp="1">&nbsp;|&nbsp;</el-text>
<el-text :type="logss[0].lastStep != 5 ? 'danger' : 'success'" line-clamp="1">{{ logss[0].roomNumber }}</el-text>
<el-text type="info" size="small" line-clamp="1">&nbsp;|&nbsp;</el-text>
<el-text type="info" line-clamp="1">{{ logss[0].platform }}</el-text>
<el-text type="info" size="small" line-clamp="1">&nbsp;|&nbsp;</el-text>
<el-text type="info" size="small" line-clamp="1">{{ logss[0].triggerTime }}</el-text>
<span v-for="(log, index) in logss.slice(0, 9)" :key="log.id">
<el-text type="info" size="small" line-clamp="1">&nbsp;|&nbsp;</el-text>
<el-text type="info" size="small" line-clamp="1">
{{'S' + (log.step + 1)}}
</el-text>
</span>
<span v-if="logss.length > 9">
<el-text type="info" size="small" line-clamp="1">&nbsp;|&nbsp;</el-text>
<el-text type="info" size="small" line-clamp="1"> | (省略{{ logss.length - 9 }})</el-text>
</span>
</span>
</div>
</div>
</template>
<div class="collapse-content">
<!-- 内层循环 step 渲染每个步骤 -->
<div class="log-item">
<el-text type="info" size="small">S0 - </el-text>
<span v-for="log in logss.slice(0,4)" :key="log.id">
<el-text type="info" size="small" class="log-description" v-if="log.step == 0">
{{ log.remoteIP || '' }}
</el-text>
<span v-if="log.step == 3">
<el-text type="info" size="small" class="log-description">
| {{ log.controlClass || '无类型' }}
<span v-if="log.controlClass == '场景控制'"> | {{ log.sceneName || '' }}</span>
</el-text>
<span v-if="log.controlClass == '设备控制'">
<el-text type="info" size="small" class="log-description" v-for="(item, index) in log.whichOneDevice">
| {{ item.deviceName || '' }} -
{{ item.deviceAddress || '' }}
</el-text>
</span>
</span>
</span>
</div>
<div v-for="log in logss" :key="log.id" class="log-item">
<el-text type="info" size="small">S{{ log.step + 1 }} - {{ log.triggerTime }}</el-text>
<el-text type="info" size="small" class="log-description">
{{ log.commandDescription || 'null' }}
</el-text>
</div>
</div>
</el-collapse-item>
</el-collapse>
<!-- 添加底部加载提示 -->
<div v-if="hasMore" class="loading-more">
<el-text type="info"><el-icon class="is-loading"><Loading /></el-icon>正在加载...</el-text>
</div>
<div v-else class="no-more">
<el-text type="info">没有更多数据了</el-text>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted, onUnmounted, inject, nextTick } from 'vue';
import { ElSwitch, ElDatePicker, ElCollapse, ElCollapseItem, ElMessage, ElLoading, ElText } from 'element-plus';
import dayjs from 'dayjs';
import config from '../../../public/config.js'
import { useRouter, useRoute } from 'vue-router'
import $ from 'jquery'
import qs from 'qs'
import * as XLSX from 'xlsx';
import { createAxiosInstance } from '../../axios.js'
/** 引入变量 */
const $http = inject('$http')
const checkLoginStatus = inject('checkLoginStatus');
const isMobile = inject('isMobile');
const fullScreen = inject('fullScreen');
const customHttp = createAxiosInstance(1);
const rcuHttp = createAxiosInstance(2);
// 响应式数据
const isReady = ref(false);
const tableHeight = ref('0');
const pickerDate = ref([]);
const logs = ref({});
const activeNames = ref([]);
const logData = ref({});
const errLogs = ref({});
const onlyCorrect = ref(false);
const onlyCorrectErr = ref(0);
const onlyCorrectAll = ref(0);
const roomHotelCode = ref([]);
const currentRoomHotelCode = ref('');
const allHotels = ref([]); // 所有酒店信息
const allRooms = ref([]); // 当前房间信息
const rooms = ref([]); // 当前房间信息
const hotels = ref([]); // 所有酒店信息
const hotelRoomCounts = ref({});
const roomDisable = ref(true);
const roomCurrent = ref([]);
const roomCurrentArray = ref([]);
const allLogCount = ref(0);
const allErrLogCount = ref(0);
// 用于懒加载
const hotelMap = new Map();
const showSearchControls = ref(true);
const currentStartTime = ref(null); // 当前加载的时间片开始时间
const currentEndTime = ref(null); // 当前加载的时间片结束时间
const hasMore = ref(true); // 是否还有更多数据
const isLoading = ref(false); // 添加加载状态变量
// 定时器变量
const refreshTimer = ref(null);
const lastRefreshTime = ref(null);
// 添加时间限制变量
const lastCallTime = ref(0);
const callInterval = 888; // 限制抖动
// 添加按钮禁用状态和定时器引用
const buttonDisabled = ref(false);
const buttonTimer = ref(null);
// 计算属性
const activeTxt = computed(() => {
if (allErrLogCount.value == 0) {
return '异常'
}
return `异常 ${Object.keys(errLogs.value).length } / ${allErrLogCount.value || ''}`;
});
const inactiveTxt = computed(() => {
if (allLogCount.value == 0) {
return '全部'
}
return `${Object.keys(logs.value).length} / ${allLogCount.value || ''} 全部`;
});
const timeSlice = computed(() => {
if (roomHotelCode.value && Object.keys(roomHotelCode.value).length > 0) {
return 60;
}
return 2;
});
// 计算属性 filteredLogs
const filteredLogs = computed(() => {
let rs = {};
if (onlyCorrect.value) {
rs = Object.keys(logs.value).reduce((acc, key) => {
if (logs.value[key][0].lastStep != 5) {
acc[key] = logs.value[key];
}
return acc;
}, {});
} else {
rs = logs.value;
}
if (isAtTop() && rs) {
// 将对象键转换为数组取前1000个键
const keys = Object.keys(rs).slice(0, 1000);
// 创建新对象只包含这些键
return keys.reduce((acc, key) => {
acc[key] = rs[key];
return acc;
}, {});
} else {
return rs;
}
});
// 监听器
watch(onlyCorrect, (newVal) => {
nextTick(() => {
checkNeedMoreData();
});
});
// 监听 isMobile 变化
watch(isMobile, (newVal) => {
// 当切换到桌面端时,强制显示所有控件
if (!newVal) {
showSearchControls.value = true;
}
});
// 方法
const logHome = () => {
window.location.href = "/Home/Index";
};
// 切换搜索控件显示状态
const toggleSearchControls = () => {
showSearchControls.value = !showSearchControls.value;
};
// 检查是否在页面顶部
const isAtTop = () => {
const container = document.querySelector('.log-container');
return container && container.scrollTop === 0 ;
};
// 处理加载数据的点击事件
const handleLoadData = () => {
if (buttonDisabled.value) return;
// 设置按钮禁用状态
buttonDisabled.value = true;
// 设置解除禁用
if (buttonTimer.value) {
clearTimeout(buttonTimer.value);
}
buttonTimer.value = setTimeout(() => {
buttonDisabled.value = false;
}, 9999); // 10秒后解除禁用
// 调用原有的加载数据方法
showSelectedLogs();
};
const showSelectedLogs = async (isInitialLoad = true, isRefresh = false) => {
const currentTime = Date.now();
if (currentTime - lastCallTime.value < callInterval) {
if (!roomHotelCode.value) {
console.log('操作过于频繁,请稍后再试');
return;
}
}
lastCallTime.value = currentTime;
if (isLoading.value) return;
isLoading.value = true;
// 如果是刷新操作设置时间范围为最近2分钟
if (isRefresh) {
pickerDate.value = [
dayjs().subtract(1, 'day').toDate(),
dayjs().toDate()
]
const now = new Date();
const startTime = lastRefreshTime.value || new Date(now.getTime() - timeSlice.value * 60000);
lastRefreshTime.value = now;
currentStartTime.value = startTime;
currentEndTime.value = now;
} else if (isInitialLoad) {
// 初始加载设置时间范围
currentEndTime.value = pickerDate.value[1];
currentStartTime.value = new Date(currentEndTime.value.getTime() - timeSlice.value * 60000);
logs.value = {};
logData.value = {};
errLogs.value = {};
}
if (!hasMore.value && !isRefresh) {
isLoading.value = false;
return;
}
if (currentStartTime.value < pickerDate.value[0]) {
currentStartTime.value = pickerDate.value[0];
}
//LoadingPic();
let QueryDate = {
PageSize: 1,
PageIndex: 1,
CommandType: [],
IsQuery_Really: false,
Start_Time: currentStartTime.value,
End_Time: currentEndTime.value,
Start_Time_Really: pickerDate.value[0],
End_Time_Really: pickerDate.value[1],
Data: []
};
try {
// 处理多酒店选择的情况
if (roomHotelCode.value.length > 0) {
if (roomHotelCode.value.length > 1 || roomCurrentArray.value.length === 0) {
roomHotelCode.value.forEach(hotelCode => {
QueryDate.Data.push({
HotelCode: hotelCode,
Key_HostNumber: '',
RoomNumber: '',
MAC: ''
});
});
} else {
roomCurrentArray.value.forEach(room => {
QueryDate.Data.push({
HotelCode: roomHotelCode.value[0],
Key_HostNumber: room.hostnumber,
RoomNumber: room.roomno,
MAC: room.mac
});
});
}
}
//console.log(QueryDate);
const rs = await $http.post('UDPPackage/Get_IOTLog', QueryDate);
//console.log(rs.data);
if (rs.data.isok) {
let mergedData = {};
onlyCorrectAll.value = rs.data.response.totalCount;
mergedData = rs.data.response.devMonitorLogResults.reduce((acc, current) => {
const hotelIdStr = current.hotelId ? current.hotelId.toString() : '';
if (!current.hotelName && hotelIdStr) {
current.hotelName = hotelMap.get(hotelIdStr) || '未知酒店';
}
const key = current.requestId;
if (!acc[key]) acc[key] = [];
acc[key].push(current);
// 排序和计算时间差
acc[key].sort((a, b) => a.step - b.step);
if (acc[key].length === 1) {
acc[key][0].timeDisparity = 0;
acc[key][0].lastStep = current.step;
} else {
const first = acc[key][0];
const last = acc[key][acc[key].length - 1];
first.timeDisparity = new Date(last.triggerTime) - new Date(first.triggerTime);
first.lastStep = last.step + 1;
}
return acc;
}, {});
// 排序(最新的在前)
const sortedKeys = Object.keys(mergedData).sort((a, b) => {
return new Date(mergedData[b][0].triggerTime) - new Date(mergedData[a][0].triggerTime);
});
const sortedData = {};
sortedKeys.forEach(key => {
sortedData[key] = mergedData[key];
});
// 合并数据:如果是刷新操作,新数据放在前面
if (isRefresh) {
// 只添加新的日志条目基于requestId去重
const newData = {};
for (const key in sortedData) {
if (!logs.value[key]) {
newData[key] = sortedData[key];
}
}
// 合并新数据到现有数据的前面
logs.value = { ...newData, ...logs.value };
} else {
// 加载历史数据,合并到后面
Object.assign(logs.value, sortedData);
//console.log(logs.value);
// 更新时间片:向前移动
currentEndTime.value = new Date(currentStartTime.value.getTime());
currentStartTime.value = new Date(currentEndTime.value.getTime() - timeSlice.value * 60000);
}
// 检查是否还有更多数据
hasMore.value = currentStartTime.value >= pickerDate.value[0];
// 计算异常日志
let err = {};
for (const key in logs.value) {
if (logs.value[key][0].lastStep != 5) {
err[key] = logs.value[key];
}
}
errLogs.value = err;
} else {
ElMessage.error(rs.data.message + '');
}
} catch (error) {
console.error('请求失败:', error);
ElMessage.error(`操作失败: ${error.message || '未知错误'}`);
} finally {
isLoading.value = false;
if (loadingInstance && typeof loadingInstance.close === 'function') {
loadingInstance.close();
}
if (!onlyCorrect.value && isInitialLoad) {
QueryDate.IsQuery_Really = true
await getLogCount(QueryDate)
}
nextTick(() => {
checkNeedMoreData();
});
}
};
// 获取总数
const getLogCount = async (QueryDate) => {
const rscount = await $http.post('UDPPackage/Get_IOTLogCount', QueryDate);
allLogCount.value = rscount.data.response.requestIdCount
allErrLogCount.value = rscount.data.response.requestNotContainerCount
};
const formatDate = (date, isStart) => {
const y = date.getFullYear();
const M = (date.getMonth() + 1).toString().padStart(2, '0');
const d = date.getDate().toString().padStart(2, '0');
return isStart ? `${y}-${M}-${d} 00:00:00` : `${y}-${M}-${d} 23:59:59`;
};
let loadingInstance = null;
const LoadingPic = () => {
loadingInstance = ElLoading.service({
lock: true,
text: '加载中...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.1)'
});
};
const getAllHotels = async () => {
try {
const rs = await customHttp.post('LowerMachineLog/GetHotelList');
if (rs.data.isok) {
allHotels.value = rs.data.response;
hotels.value = rs.data.response;
rs.data.response.forEach(hotel => {
hotelMap.set(hotel.id.toString(), hotel.name);
});
//console.log(allHotels.value);
// 获取所有酒店的房间数
try {
const roomCountRes = await customHttp.get('BlockIP/GetRoomCount');
roomCountRes.data.response.forEach(item => {
// 将房间数存储在hotelRoomCounts中key为酒店code
hotelRoomCounts.value[item.hotelID] = item.totalCount;
});
} catch (roomError) {
console.error('获取酒店房间数失败:', roomError);
}
// 设置酒店的默认房间数为"-1"(表示未知)
allHotels.value.forEach(hotel => {
hotel.count = hotelRoomCounts.value[hotel.id] || "-1";
});
}
} catch (error) {
// ...错误处理
}
};
// 获取rooms
const handleHotelForRooms = async () => {
// 处理多酒店选择时的房间选择框状态
if (roomHotelCode.value.length !== 1) {
roomDisable.value = true;
rooms.value = [];
allRooms.value = [];
return;
}
try {
// 查找酒店信息
const hotel = allHotels.value.find(h => h.code === roomHotelCode.value[0]);
if (!hotel) {
ElMessage.error('未找到酒店信息');
return;
}
// 设置当前房间表显示的酒店代码
currentRoomHotelCode.value = roomHotelCode.value[0];
// 准备数据
const getdate = {
hotel_code: hotel.code,
createDate: hotel.createdate,
};
// 调用获取房间方法
const header = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
};
const rs = await customHttp.post('LowerMachineLog/GetHostList', qs.stringify(getdate), header);
//console.log(rs.data)
if (rs.data.isok) {
allRooms.value = JSON.parse(JSON.stringify(rs.data.response));
rooms.value = JSON.parse(JSON.stringify(rs.data.response));
roomDisable.value = false;
} else {
ElMessage.error('获取房间数据失败');
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message);
}
};
// 搜索hotel
const remoteHotel = (query) => {
if (query) {
hotels.value = allHotels.value.filter(item =>
item.code.includes(query) || item.name.includes(query)
);
} else {
hotels.value = [...allHotels.value];
}
}
const remoteRoom = (query) => {
if (query) {
rooms.value = allRooms.value.filter(item =>
item.roomno.includes(query)
);
} else {
rooms.value = [...allRooms.value];
}
}
// 修改后的定时刷新函数
const startAutoRefresh = () => {
if (refreshTimer.value) {
clearInterval(refreshTimer.value);
}
refreshTimer.value = setInterval(async () => {
if (isAtTop() && roomHotelCode.value.length == 0) {
await showSelectedLogs(false, true); // 使用 isRefresh=true 调用
}
}, 60000);
};
// 生命周期钩子
onMounted(async () => {
localStorage.setItem('url', '/voicelog');
try {
await getAllHotels();
await showSelectedLogs(true, true); // 初始加载
startAutoRefresh(); // 启动定时刷新
} catch (error) {
console.error('初始化失败:', error);
ElMessage.error('初始化失败: ' + error.message);
}
// 添加滚动事件监听器
const logContainer = document.querySelector('.log-container');
if (logContainer) {
logContainer.addEventListener('scroll', handleScroll);
}
});
onUnmounted(() => {
// 移除滚动事件监听器
const logContainer = document.querySelector('.log-container');
if (logContainer) {
logContainer.removeEventListener('scroll', handleScroll);
}
// 清除定时器
if (refreshTimer.value) {
clearInterval(refreshTimer.value);
refreshTimer.value = null;
}
});
// 滚动处理
const handleScroll = (e) => {
const container = e.target;
// 计算距离底部的距离(像素)
const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
// 当距离底部小于等于600像素时触发加载
if (distanceToBottom <= 1000 && !isLoading.value && hasMore.value) {
showSelectedLogs(false);
}
};
const checkNeedMoreData = () => {
const logContainer = document.querySelector('.log-container');
if (!logContainer || isLoading.value || !hasMore.value) return false;
// 检查容器是否可滚动(内容是否超出容器高度)
const isScrollable = logContainer.scrollHeight > logContainer.clientHeight;
// 如果容器不可滚动(内容不足),并且还有更多数据,则加载更多
if (!isScrollable && hasMore.value) {
showSelectedLogs(false)
}
return false;
};
</script>
<style scoped>
@font-face {
font-family: 'sourcecode';
src: url('../../assets/sourcecode.ttf') format('truetype');
}
.fontff {
font-family: 'sourcecode', sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.headbox {
padding: 3px;
}
.seachbox > div {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1px;
}
.demonstration {
display: inline-block;
min-width: 60px;
text-align: right;
font-size: 13px;
margin: 0 1px;
}
.log-container {
height: calc(100vh - 110px);
overflow-y: auto;
}
.title-container {
width: 100%;
display: flex;
align-items: center;
}
.collapse-title {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
line-height: 16px; /* 行高 */
padding: 2px 0; /* 上下内边距 */
/* 动态高度每行15px + 上下内边距 */
min-height: calc(22px + 16px * (var(--line-count) - 16px));
}
.collapse-content {
padding: 0 20px;
}
.log-item {
margin: 5px 0;
border-bottom: 1px solid #90939922;
padding-bottom: 2px;
word-break: break-word;
}
.log-description {
color: #A0A0A0;
margin-top: 1px;
font-size: 12px;
}
.option-left {
float: left;
}
.option-right {
float: right;
color: #8492a6;
font-size: 13px;
}
.date-range-container {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 3px;
}
.date-item {
display: inline-flex;
align-items: center;
margin-right: 1px;
}
:deep(.el-collapse-item__header) {
height: auto !important;
min-height: 35px; /* 基础高度 */
padding: 5px 0; /* 增加内边距 */
align-items: flex-start; /* 顶部对齐 */
}
:deep(.el-collapse-item__arrow) {
margin-top: 5px; /* 垂直居中调整 */
}
.seachbox {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
}
.button-container {
display: flex;
align-items: center;
gap: 5px;
margin-right: 10px;
}
.search-controls-container {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
flex: 1;
}
.loading-more, .no-more {
text-align: center;
padding: 10px;
font-size: 14px;
}
.loading-indicator {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.8);
padding: 20px;
border-radius: 5px;
z-index: 1000;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.seachbox > * {
margin-bottom: 2px;
}
.el-select {
width: 96% !important;
margin-right: 0 !important;
}
.el-date-picker {
width: 100% !important;
}
.el-button {
width: 96%;
margin-top: 0px;
}
.collapse-title {
/* 移动端行高稍小 */
line-height: 15px;
}
.date-item {
display: flex;
width: 100%;
margin-bottom: 5px;
}
.date-item .el-date-picker {
flex: 1;
}
.seachbox > div {
display: flex;
flex-direction: column;
gap: 5px;
}
:deep(.el-collapse-item__header) {
min-height: 30px; /* 移动端基础高度稍小 */
padding: 2px 0; /* 移动端内边距减小 */
}
:deep(.el-collapse-item__arrow) {
margin-top: 3px; /* 移动端垂直居中调整 */
}
}
</style>

137
src/router/index.js Normal file
View 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 UdpLog from '../pages/udplog/index.vue';
import PowerLog from '../pages/powerlog/index.vue';
import LogSetup from '../pages/logsetup/index.vue';
import DicManage from '../pages/dicmanage/index.vue';
import VoiceLog from '../pages/voicelog/index.vue';
import StatusLog from '../pages/statuslog/index.vue';
import BlackList from '../pages/blacklist/index.vue';
import TftpWhiteList from '../pages/tftpwhitelist/index.vue';
import TaskTimeLog from '../pages/tasktimelog/index.vue';
const routes = [
{
path: '/',
// 动态重定向到登录页或主页
redirect: () => {
const isAuthenticated = localStorage.getItem('TokenT');
return isAuthenticated ? '/home' : '/login';
}
},
{
path: '/login',
name: '登录',
component: Login
},
{
path: '/home',
name: '主页',
component: Home,
meta: { requiresAuth: true } // 需要认证的路由
},
/* {
path: '/dicmanage',
name: '字典管理',
component: DicManage,
meta: { requiresAuth: true } // 需要认证的路由
},*/
{
path: '/udplog',
name: 'UDP监控',
component: UdpLog,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/tasktimelog',
name: '线程耗时记录',
component: TaskTimeLog,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/blacklist',
name: '黑名单管理',
component: BlackList,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/tftpwhitelist',
name: 'TFTP管理',
component: TftpWhiteList,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/voicelog',
name: '语音助手日志',
component: VoiceLog,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/statuslog',
name: '房态日志',
component: StatusLog,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/powerlog',
name: '功率记录',
component: PowerLog,
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('TokenT') // 检查是否登陆过
//const isAuthenticated = localStorage.getItem('AccessibleHotels') // 检查是否登陆过
//const username = localStorage.getItem('username') // 获取用户名
if (localStorage.getItem("TokenT")) {
if (calculateTimeDiff(localStorage.getItem("TokenT")) > 260000) { // 3天内免密登录
next('/login')
}
} else if (to.fullPath != "/login") {
next('/login')
}
if (to.meta.requiresAuth && !isAuthenticated) {
// 如果需要认证且没有token则跳转到登录页面
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;
// 计算时间差
const calculateTimeDiff = (targetTimeStr) => {
const targetDate = new Date(targetTimeStr);
// 检查日期是否有效
if (isNaN(targetDate.getTime())) {
throw new Error("无效的时间格式");
}
const now = new Date();
return Math.floor((now - targetDate) / 1000);
}

118
src/utils/index.js Normal file
View File

@@ -0,0 +1,118 @@
export class MinuteIndex {
constructor() {
// 外层Map: 分钟级时间 -> 中层Map
this.index = new Map();
// 对象追踪: 存储对象在索引中的位置
this.objectTracker = new WeakMap();
}
// 时间标准化 (处理各种时间格式)
normalizeMinuteKey(timeStr) {
// 移除秒和时区信息
let normalized = timeStr.replace(' ', 'T')
// 替换空格为 T
//normalized = normalized.replace(' ', 'T');
// 确保格式为 YYYY-MM-DDTHH:mm
return normalized.substring(0, 16);
}
// 添加或更新对象
updateItem(item) {
if (!item || !item.removeTime || !item.commandType) return;
// 直接使用 Date 对象解析
const dateObj = formatLocalTime(item.removeTime).replace(" ", "T").slice(0, -3);
//console.log(dateObj);
const newTimeKey = this.normalizeMinuteKey(dateObj);
const commandType = item.commandType;
// 检查是否已有位置记录
const currentPosition = this.objectTracker.get(item) || {};
// 位置未变化时跳过更新
if (currentPosition.timeKey === newTimeKey &&
currentPosition.commandType === commandType) {
return;
}
// 从旧位置移除
if (currentPosition.timeKey) {
const oldTimeMap = this.index.get(currentPosition.timeKey);
if (oldTimeMap && oldTimeMap.has(currentPosition.commandType)) {
oldTimeMap.delete(currentPosition.commandType);
// 如果该时间点没有更多条目,移除该时间点
if (oldTimeMap.size === 0) {
this.index.delete(currentPosition.timeKey);
}
}
}
// 添加或更新到新位置
if (!this.index.has(newTimeKey)) {
this.index.set(newTimeKey, new Map());
}
const typeMap = this.index.get(newTimeKey);
typeMap.set(commandType, item);
// 更新位置追踪
this.objectTracker.set(item, {
timeKey: newTimeKey,
commandType: commandType
});
}
// 批量添加或更新
bulkUpdate(items) {
if (!items || !Array.isArray(items)) return;
for (const item of items) {
this.updateItem(item);
}
}
// 从索引中移除对象
removeItem(item) {
const position = this.objectTracker.get(item);
if (!position) return;
const timeMap = this.index.get(position.timeKey);
if (timeMap) {
timeMap.delete(position.commandType);
// 清理空的时间节点
if (timeMap.size === 0) {
this.index.delete(position.timeKey);
}
}
this.objectTracker.delete(item);
}
// 查询方法 (核心)
getItem(timeStr, commandType) {
//console.log('getItem', timeStr, commandType);
const timeKey = this.normalizeMinuteKey(timeStr);
//console.log(this.index);
const typeMap = this.index.get(timeKey);
//console.log(timeKey, typeMap);
if (!typeMap) return null;
return typeMap.get(commandType) || null;
}
}
function formatLocalTime(utcString) {
const date = new Date(utcString);
// 获取各个时间部分
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}`;
}

10
vite.config.js Normal file
View 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: 57540,
}
})