初始化项目
This commit is contained in:
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
49
.github/copilot-instructions.md
vendored
Normal file
49
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<!-- .github/copilot-instructions.md -->
|
||||||
|
# 快速上手(供 AI 助手)
|
||||||
|
|
||||||
|
此项目是一个基于 Vue 3 + Vite 的 Web 前端(Element Plus UI)。下面的说明帮助 AI 编码/补全助手快速理解项目结构、运行方式、常见约定和注意点,以便生成正确、可运行的改动。
|
||||||
|
|
||||||
|
- 运行 / 构建
|
||||||
|
- 安装依赖: `npm install`
|
||||||
|
- 开发: `npm run dev`(本地 dev server 默认端口在 `vite.config.js` 中配置为 63615)
|
||||||
|
- 生产构建: `npm run build`
|
||||||
|
- 预览产物: `npm run preview`
|
||||||
|
- 修复 lint: `npm run lint`
|
||||||
|
|
||||||
|
- 核心技术栈(来自 `package.json`)
|
||||||
|
- Vue 3, Vite, Vue Router 4, Vuex 4
|
||||||
|
- Element Plus (UI)、axios(HTTP)、vue-i18n
|
||||||
|
|
||||||
|
- 关键文件/位置(示例引用)
|
||||||
|
- `src/main.js`:应用入口。全局注入 `$http`(axios 实例)和 `config`(来自 `public/config.js`),并挂载 ElementPlus 与 i18n。
|
||||||
|
- `src/router/index.js`:路由定义与路由守卫。认证依赖 `localStorage.token`;有 Admin 特权判断(username 为 Admin 或 MoMoWen)。添加页面通常在 `src/pages/*` 中创建并在此处引入。
|
||||||
|
- `src/axios.js`:导出默认 axios 实例与 `createAxiosInstance(urlnum)` 工厂函数。注意:
|
||||||
|
- baseURL 来源:`public/config.js` 的 `ApiList`(index 默认 0)或 `import.meta.env.VITE_API_BASE_URL`。
|
||||||
|
- 请求拦截器会为非 Login/Login 请求附带 `Authorization: Bearer <token>`(从 localStorage 读取)。
|
||||||
|
- 响应拦截器实现了简单的 401 处理与 `refreshToken()` 的自动尝试(依赖 rememberedUsername/rememberedPassword 存储)。
|
||||||
|
- `public/config.js`:运行时 API 列表与资源地址(更新测试或新服务地址请在这里调整或通过 env 覆盖)。
|
||||||
|
|
||||||
|
- 项目约定与注意事项(基于代码可观测行为)
|
||||||
|
- 认证:token 存在于 `localStorage.token`。路由守卫依赖该字段。如果生成与登录/认证相关的修改,请保持兼容性。
|
||||||
|
- 自动刷新 Token:`src/axios.js` 会尝试在响应后刷新 token(会读取 `rememberedUsername` 与 `rememberedPassword`)。不要移除这些本地键,除非同步修改登录逻辑。
|
||||||
|
- Axios 用法:默认 export 是已配置实例(供现有代码使用 `this.$http` 或 `app.config.globalProperties.$http`)。若需要对不同 API host 调用,请使用 `createAxiosInstance(n)`。
|
||||||
|
- 路由/页面:新页面放到 `src/pages/<name>/index.vue` 并在 `src/router/index.js` 中按现有模式引入与注册。
|
||||||
|
- 全局注入:`main.js` 使用 `app.provide('config', config)`,生成的自动补全可参考 `config.ApiList`、`config.Api` 等字段。
|
||||||
|
|
||||||
|
- 编辑/调试建议(对生成代码很重要)
|
||||||
|
- 本地运行 dev 后打开 `http://localhost:63615`(或 Vite 提示的端口)。
|
||||||
|
- 推荐编辑器/插件:VSCode + Volar(项目 README 推荐)。
|
||||||
|
|
||||||
|
- 示例片段(如何安全地添加 API 调用)
|
||||||
|
- 使用默认实例(与现有代码兼容):
|
||||||
|
- 在组件中直接使用 `this.$http.get('Some/Endpoint')`(或 `import axios from '@/axios'` 然后 `axios.get(...)`)。
|
||||||
|
- 使用备用 base URL:
|
||||||
|
- `import { createAxiosInstance } from '@/axios'
|
||||||
|
const api2 = createAxiosInstance(1)
|
||||||
|
api2.get('Some/Endpoint')`
|
||||||
|
|
||||||
|
- 变更约束(AI 应遵守以避免破坏现有行为)
|
||||||
|
- 不要删除或重命名 `localStorage` 中的 key(如: `token`, `username`, `rememberedUsername`, `rememberedPassword`, `lastAutoLoginTime`),除非同时更新登录与刷新流程。
|
||||||
|
- 保持 `Login/Login` 在 axios 拦截器中作为例外路径(或同时更新拦截器逻辑)。
|
||||||
|
|
||||||
|
若你需要我把某部分扩展为示例代码补丁(比如为某个新页面、API 或路由添加实现),告诉我目标文件与预期行为,我会生成并应用补丁;之后请告知额外运行/测试步骤或后端约束。
|
||||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 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
|
||||||
|
/.vscode
|
||||||
|
/*.user
|
||||||
12
CHANGELOG.md
Normal file
12
CHANGELOG.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
This file explains how Visual Studio created the project.
|
||||||
|
|
||||||
|
The following tools were used to generate this project:
|
||||||
|
- create-vite
|
||||||
|
|
||||||
|
The following steps were used to generate this project:
|
||||||
|
- Create vue project with create-vite: `npm init --yes vue@latest panel_selection -- --eslint `.
|
||||||
|
- Updating `vite.config.js` with port.
|
||||||
|
- Create project file (`panel_selection.esproj`).
|
||||||
|
- Create `launch.json` to enable debugging.
|
||||||
|
- Add project to solution.
|
||||||
|
- Write this file.
|
||||||
35
README.md
Normal file
35
README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# panel_selection
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
24
eslint.config.js
Normal file
24
eslint.config.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
import globals from 'globals'
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{js,mjs,jsx,vue}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||||
|
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
js.configs.recommended,
|
||||||
|
...pluginVue.configs['flat/essential'],
|
||||||
|
])
|
||||||
14
index.html
Normal file
14
index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!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">
|
||||||
|
<META NAME="ROBOTS" CONTENT="NOINDEX,NOFOLLOW"><!-- 禁止搜索引擎索引 -->
|
||||||
|
<title>宝来威-天工平台</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
39
obj/Debug/package.g.props
Normal file
39
obj/Debug/package.g.props
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<PackageJsonName Condition="$(PackageJsonName) == ''">panel_selection</PackageJsonName>
|
||||||
|
<PackageJsonVersion Condition="$(PackageJsonVersion) == ''">0.0.0</PackageJsonVersion>
|
||||||
|
<PackageJsonPrivate Condition="$(PackageJsonPrivate) == ''">true</PackageJsonPrivate>
|
||||||
|
<PackageJsonType Condition="$(PackageJsonType) == ''">module</PackageJsonType>
|
||||||
|
<PackageJsonScriptsDev Condition="$(PackageJsonScriptsDev) == ''">vite</PackageJsonScriptsDev>
|
||||||
|
<PackageJsonScriptsBuild Condition="$(PackageJsonScriptsBuild) == ''">vite build</PackageJsonScriptsBuild>
|
||||||
|
<PackageJsonScriptsPreview Condition="$(PackageJsonScriptsPreview) == ''">vite preview</PackageJsonScriptsPreview>
|
||||||
|
<PackageJsonScriptsLint Condition="$(PackageJsonScriptsLint) == ''">eslint . --fix</PackageJsonScriptsLint>
|
||||||
|
<PackageJsonDependenciesElementPlusIconsVue Condition="$(PackageJsonDependenciesElementPlusIconsVue) == ''">^2.3.1</PackageJsonDependenciesElementPlusIconsVue>
|
||||||
|
<PackageJsonDependenciesVueuseCore Condition="$(PackageJsonDependenciesVueuseCore) == ''">^13.1.0</PackageJsonDependenciesVueuseCore>
|
||||||
|
<PackageJsonDependenciesAxios Condition="$(PackageJsonDependenciesAxios) == ''">^1.8.3</PackageJsonDependenciesAxios>
|
||||||
|
<PackageJsonDependenciesDayjs Condition="$(PackageJsonDependenciesDayjs) == ''">^1.11.13</PackageJsonDependenciesDayjs>
|
||||||
|
<PackageJsonDependenciesElementChinaAreaData Condition="$(PackageJsonDependenciesElementChinaAreaData) == ''">^6.1.0</PackageJsonDependenciesElementChinaAreaData>
|
||||||
|
<PackageJsonDependenciesElementPlus Condition="$(PackageJsonDependenciesElementPlus) == ''">^2.9.6</PackageJsonDependenciesElementPlus>
|
||||||
|
<PackageJsonDependenciesFabric Condition="$(PackageJsonDependenciesFabric) == ''">^6.7.1</PackageJsonDependenciesFabric>
|
||||||
|
<PackageJsonDependenciesJquery Condition="$(PackageJsonDependenciesJquery) == ''">^3.7.1</PackageJsonDependenciesJquery>
|
||||||
|
<PackageJsonDependenciesMakerjs Condition="$(PackageJsonDependenciesMakerjs) == ''">^0.18.1</PackageJsonDependenciesMakerjs>
|
||||||
|
<PackageJsonDependenciesQs Condition="$(PackageJsonDependenciesQs) == ''">^6.14.0</PackageJsonDependenciesQs>
|
||||||
|
<PackageJsonDependenciesSsh2SftpClient Condition="$(PackageJsonDependenciesSsh2SftpClient) == ''">^12.0.0</PackageJsonDependenciesSsh2SftpClient>
|
||||||
|
<PackageJsonDependenciesVue Condition="$(PackageJsonDependenciesVue) == ''">^3.5.13</PackageJsonDependenciesVue>
|
||||||
|
<PackageJsonDependenciesVueI18n Condition="$(PackageJsonDependenciesVueI18n) == ''">^11.1.3</PackageJsonDependenciesVueI18n>
|
||||||
|
<PackageJsonDependenciesVueRouter Condition="$(PackageJsonDependenciesVueRouter) == ''">^4.5.0</PackageJsonDependenciesVueRouter>
|
||||||
|
<PackageJsonDependenciesVueSimpleVerify Condition="$(PackageJsonDependenciesVueSimpleVerify) == ''">^1.1.0</PackageJsonDependenciesVueSimpleVerify>
|
||||||
|
<PackageJsonDependenciesVuex Condition="$(PackageJsonDependenciesVuex) == ''">^4.1.0</PackageJsonDependenciesVuex>
|
||||||
|
<PackageJsonDependenciesXlsx Condition="$(PackageJsonDependenciesXlsx) == ''">^0.18.5</PackageJsonDependenciesXlsx>
|
||||||
|
<PackageJsonDevdependenciesEslintJs Condition="$(PackageJsonDevdependenciesEslintJs) == ''">^9.21.0</PackageJsonDevdependenciesEslintJs>
|
||||||
|
<PackageJsonDevdependenciesVitejsPluginVue Condition="$(PackageJsonDevdependenciesVitejsPluginVue) == ''">^5.2.1</PackageJsonDevdependenciesVitejsPluginVue>
|
||||||
|
<PackageJsonDevdependenciesEslint Condition="$(PackageJsonDevdependenciesEslint) == ''">^9.21.0</PackageJsonDevdependenciesEslint>
|
||||||
|
<PackageJsonDevdependenciesEslintPluginVue Condition="$(PackageJsonDevdependenciesEslintPluginVue) == ''">~10.0.0</PackageJsonDevdependenciesEslintPluginVue>
|
||||||
|
<PackageJsonDevdependenciesGlobals Condition="$(PackageJsonDevdependenciesGlobals) == ''">^16.0.0</PackageJsonDevdependenciesGlobals>
|
||||||
|
<PackageJsonDevdependenciesUnpluginAutoImport Condition="$(PackageJsonDevdependenciesUnpluginAutoImport) == ''">^19.1.1</PackageJsonDevdependenciesUnpluginAutoImport>
|
||||||
|
<PackageJsonDevdependenciesUnpluginVueComponents Condition="$(PackageJsonDevdependenciesUnpluginVueComponents) == ''">^28.4.1</PackageJsonDevdependenciesUnpluginVueComponents>
|
||||||
|
<PackageJsonDevdependenciesVite Condition="$(PackageJsonDevdependenciesVite) == ''">^6.2.1</PackageJsonDevdependenciesVite>
|
||||||
|
<PackageJsonDevdependenciesVitePluginVueDevtools Condition="$(PackageJsonDevdependenciesVitePluginVueDevtools) == ''">^7.7.2</PackageJsonDevdependenciesVitePluginVueDevtools>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
1
obj/Debug/panel_selection.esproj.FileListAbsolute.txt
Normal file
1
obj/Debug/panel_selection.esproj.FileListAbsolute.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
E:\Project\PanelSelection\obj\Debug\panel_selection.esproj.CoreCompileInputs.cache
|
||||||
6386
package-lock.json
generated
Normal file
6386
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
package.json
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "panel_selection",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@vueuse/core": "^13.1.0",
|
||||||
|
"axios": "^1.8.3",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"element-china-area-data": "^6.1.0",
|
||||||
|
"element-plus": "^2.9.6",
|
||||||
|
"fabric": "^6.7.1",
|
||||||
|
"jquery": "^3.7.1",
|
||||||
|
"makerjs": "^0.18.1",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
panel_selection.esproj
Normal file
11
panel_selection.esproj
Normal 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>
|
||||||
32
public/config.js
Normal file
32
public/config.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const config = {
|
||||||
|
// http访问后端接口
|
||||||
|
/* Api: "http://project.blv-oa.com:898/api/",
|
||||||
|
Ads: "http://project.blv-oa.com:898/",
|
||||||
|
PicAds: "http://blv-rd.tech:19055/PanelSelectionPic/",
|
||||||
|
ApiList: [
|
||||||
|
"http://project.blv-oa.com:898/api/",
|
||||||
|
"http://blv-rd.tech:19055/api/",
|
||||||
|
"http://www.boonlive-rcu.com:7000/api/",
|
||||||
|
],*/
|
||||||
|
|
||||||
|
// 本地调试接口
|
||||||
|
/* Api: "http://project.blv-oa.com:898/api/",
|
||||||
|
Ads: "http://project.blv-oa.com:898/",
|
||||||
|
PicAds: "http://blv-rd.tech:10010/PanelSelectionPic/",
|
||||||
|
ApiList: [
|
||||||
|
"http://project.blv-oa.com:898/api/",
|
||||||
|
"http://localhost:19368/api/",
|
||||||
|
"http://www.boonlive-rcu.com:7000/api/",
|
||||||
|
], */
|
||||||
|
// 新服务接口
|
||||||
|
Api: "http://project.blv-oa.com:898/api/",
|
||||||
|
Ads: "http://project.blv-oa.com:898/",
|
||||||
|
PicAds: "http://blv-rd.tech:10010/PanelSelectionPic/",
|
||||||
|
ApiList: [
|
||||||
|
"http://project.blv-oa.com:898/api/",
|
||||||
|
"http://blv-rd.tech:10010/api/",
|
||||||
|
"http://www.boonlive-rcu.com:7000/api/",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
21
public/logo.svg
Normal file
21
public/logo.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?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="477px" height="621px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||||
|
viewBox="0 0 80.92 105.37"
|
||||||
|
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}
|
||||||
|
.fil1 {fill:#008C8C;fill-rule:nonzero}
|
||||||
|
]]>
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="图层_x0020_1">
|
||||||
|
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||||
|
<path class="fil0" d="M72.67 62.65c2.16,-4 3.39,-8.58 3.39,-13.45 0,-15.66 -12.69,-28.35 -28.35,-28.35 -4.92,0 -9.54,1.26 -13.57,3.46l0 11.7c3.44,-3.54 8.25,-5.74 13.57,-5.74 10.46,0 18.93,8.47 18.93,18.93 0,10.45 -8.47,18.93 -18.93,18.93 -5.21,0 -9.93,-2.11 -13.35,-5.51 -3.4,-3.35 -4.19,-8.41 -4.36,-13.47l0 -31.15 0 -15.23 0 -0.35 0 -0.01 0 -1.04c3.33,-0.89 6.84,-1.37 10.46,-1.37 22.35,0 40.46,18.12 40.46,40.46 0,22.35 -18.11,40.46 -40.46,40.46 -22.34,0 -40.46,-18.11 -40.46,-40.46 0,-14.88 8.04,-27.89 20.01,-34.92l0 1.18 0 0 0 0.4 0 10.88 0 29.2c0,4.65 0.18,6.98 1.17,10.53 2.24,8.04 7.74,14.13 15.26,17.49 3.45,1.49 7.27,2.33 11.27,2.33 3.02,0 5.93,-0.48 8.66,-1.35 6.6,-2.95 12.23,-7.66 16.3,-13.55z"/>
|
||||||
|
<polygon class="fil1" points="15.53,105.37 15.53,97.52 65.39,97.52 65.39,105.37 "/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
40
public/logobig.svg
Normal file
40
public/logobig.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
BIN
public/pic/BLWTG.png
Normal file
BIN
public/pic/BLWTG.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
595
src/App.vue
Normal file
595
src/App.vue
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main-container">
|
||||||
|
<template v-if="alreadyLogin">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<el-header class="top-header">
|
||||||
|
<!-- Logo区域 -->
|
||||||
|
<div class="logo-container">
|
||||||
|
<!-- Logo显示(根据屏幕尺寸切换) -->
|
||||||
|
<img v-if="!isMobile" src="../public/logobig.svg" @click="handleLeftClick('home')" style="cursor: pointer; height: 30px;" />
|
||||||
|
<img v-else="isMobile" src="../public/logo.svg" @click="handleLeftClick('home')" style="cursor: pointer; height: 30px; margin-right: 10px;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 横向菜单(桌面端显示)暂时v-show隐藏,不能删除 -->
|
||||||
|
<el-menu ref="parentRef"
|
||||||
|
mode="horizontal"
|
||||||
|
v-show="false"
|
||||||
|
class="horizontal-menu"
|
||||||
|
:default-active="currentTitle.routes"
|
||||||
|
@select="menuChange">
|
||||||
|
<template v-for="menu in menuValue" :key="menu.label">
|
||||||
|
<el-sub-menu v-if="menu.options" :index="menu.value">
|
||||||
|
<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>
|
||||||
|
<el-menu-item v-else :index="menu.value">
|
||||||
|
<el-icon>
|
||||||
|
<component :is="menu.icon" />
|
||||||
|
</el-icon>
|
||||||
|
{{ menu.label }}
|
||||||
|
</el-menu-item>
|
||||||
|
</template>
|
||||||
|
</el-menu>
|
||||||
|
|
||||||
|
<!-- 中间区域 -->
|
||||||
|
<div class="header-center">
|
||||||
|
<img src="../public/pic/BLWTG.png" class="center-logo" />
|
||||||
|
<span class="center-text">宝来威-天工平台</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧操作区 -->
|
||||||
|
<div class="header-right">
|
||||||
|
<!-- <el-select v-model="selectedProjectId" placeholder="请选择项目" filterable clearable @change="handleProjectChangeApp"
|
||||||
|
style="width:250px;margin: 0 10px;" :disabled="isProjectLocked">
|
||||||
|
<el-option v-for="project in projectList" :key="project.id" :label="project.projectName" :value="project.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-popover placement="bottom-end" :width="1100" trigger="click">
|
||||||
|
<template #reference>
|
||||||
|
<el-button v-if="selectedProject">项目详情</el-button>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<el-descriptions border column="4" size="small" class="details" v-if="selectedProject">
|
||||||
|
<el-descriptions-item label="项目名称">{{ selectedProject.projectName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="详细地址">{{ selectedProject.addressDetail }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="所在地区">{{ selectedProject.quyu }} · {{ selectedProject.shengfen }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="项目编号">{{ selectedProject.projectCode }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="审批状态">
|
||||||
|
<el-tag size="small" :type="selectedProject.shengpiStatus === '3' ? 'success' : 'warning'">
|
||||||
|
{{ selectedProject.shengpiStatus === '3' ? '已批准' : '待审批' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="报备信息">{{ selectedProject.customization }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="房间信息">{{ selectedProject.roomTypeCount }} 种房型 · {{ selectedProject.roomTotalCount }} 间客房</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后更新">{{ formatDate(selectedProject.updatetime) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-popover>-->
|
||||||
|
|
||||||
|
<!-- 刷新 -->
|
||||||
|
<!-- <el-button link size="large" style="font-size:22px"
|
||||||
|
@click="refreshToHome"
|
||||||
|
icon="Refresh" />-->
|
||||||
|
|
||||||
|
<!-- 用户头像 -->
|
||||||
|
<el-popover v-model:visible="showPopover"
|
||||||
|
placement="bottom-end"
|
||||||
|
trigger="click">
|
||||||
|
<template #reference>
|
||||||
|
<el-avatar class="user-avatar"
|
||||||
|
:src="userAvatar" />
|
||||||
|
</template>
|
||||||
|
<div class="user-menu">
|
||||||
|
<el-button link @click="handleAction('logout')">退出登录</el-button>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<!-- 路由内容区 -->
|
||||||
|
<div class="router-container" :class="{'mobile-view': isMobile}">
|
||||||
|
<keep-alive :include="cachedViews">
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<component :is="Component"
|
||||||
|
:key="route.fullPath"
|
||||||
|
class="router-view-container" />
|
||||||
|
</router-view>
|
||||||
|
</keep-alive>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-footer class="global-footer" :class="{'mobile-footer': isMobile}" v-if="!isMobile">
|
||||||
|
Copyright © 2025 宝来威智能(广东)有限公司. All Rights Reserved.
|
||||||
|
</el-footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
|
import { provide, ref, onMounted, computed, reactive, inject, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
import en from 'element-plus/dist/locale/en.mjs'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { createAxiosInstance } from './axios.js'
|
||||||
|
|
||||||
|
const language = ref('zh-cn')
|
||||||
|
const locale = computed(() => (language.value === 'zh-cn' ? zhCn : en))
|
||||||
|
|
||||||
|
const isDark = useDark()
|
||||||
|
const toggleDark = useToggle(isDark)
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
language.value = language.value === 'zh-cn' ? 'en' : 'zh-cn'
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkLoginStatus = () => {
|
||||||
|
alreadyLogin.value = localStorage.getItem('login') == 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const userAvatar = ref("")
|
||||||
|
const drawerVisible = ref(false) // 控制移动端侧边栏显示
|
||||||
|
|
||||||
|
// 检测屏幕大小变化的函数
|
||||||
|
const isMobile = ref(false)
|
||||||
|
const checkIfMobile = () => {
|
||||||
|
isMobile.value = window.innerWidth < 768;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const projectList = ref([]);
|
||||||
|
const selectedProjectId = ref(null);
|
||||||
|
const selectedProject = ref(null)
|
||||||
|
const cloudHttp = createAxiosInstance(0);
|
||||||
|
const localHttp = createAxiosInstance(1);
|
||||||
|
|
||||||
|
const getProject = async () => {
|
||||||
|
try {
|
||||||
|
const rs = await cloudHttp.post('AasProjectInfoes/Get_ProjectByStatusCode');
|
||||||
|
projectList.value = rs.data.response.sort((a, b) => b.id - a.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取项目列表失败:', error);
|
||||||
|
ElMessage.error('获取项目列表失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleProjectChangeApp = async (projectId) => {
|
||||||
|
if (!projectId) {
|
||||||
|
selectedProject.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedProject.value = projectList.value.find(p => p.id === projectId);
|
||||||
|
if (!selectedProject.value) {
|
||||||
|
ElMessage.warning('未找到选中的项目');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const helloworld = () => {
|
||||||
|
try {
|
||||||
|
if (getUrl() == "/login") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rs = cloudHttp.post('Login/Helloooo').then(rs => {
|
||||||
|
if (rs.data != 'hello') {
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onMounted(async () => {
|
||||||
|
localStorage.setItem('login', true)
|
||||||
|
helloworld()
|
||||||
|
// 初始检查登录状态
|
||||||
|
checkLoginStatus()
|
||||||
|
userAvatar.value = config.httpAds + localStorage.getItem('avatar')
|
||||||
|
|
||||||
|
// 初始检查屏幕尺寸
|
||||||
|
checkIfMobile();
|
||||||
|
// 添加窗口大小变化监听
|
||||||
|
window.addEventListener('resize', checkIfMobile);
|
||||||
|
// 主题初始化
|
||||||
|
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
const handleColorSchemeChange = (e) => {
|
||||||
|
webToDark(e.matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始设置
|
||||||
|
webToDark(darkModeQuery.matches)
|
||||||
|
|
||||||
|
// 监听系统主题变化
|
||||||
|
darkModeQuery.addEventListener('change', handleColorSchemeChange)
|
||||||
|
|
||||||
|
// 清理监听器
|
||||||
|
return () => {
|
||||||
|
darkModeQuery.removeEventListener('change', handleColorSchemeChange)
|
||||||
|
window.removeEventListener('resize', checkIfMobile);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const getUrl = () => {
|
||||||
|
const url = window.location.href;
|
||||||
|
const lastSlashIndex = url.lastIndexOf('/');
|
||||||
|
return url.substring(lastSlashIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyLogin = ref(false)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const config = inject('config')
|
||||||
|
const username = localStorage.getItem('username')
|
||||||
|
|
||||||
|
const currentTitle = reactive({
|
||||||
|
label: '',
|
||||||
|
routes: localStorage.getItem('url'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const showPopover = ref(false)
|
||||||
|
|
||||||
|
const menuValue = reactive([
|
||||||
|
{
|
||||||
|
label: "菜单",
|
||||||
|
value: "dashboard",
|
||||||
|
icon: "Menu",
|
||||||
|
options: [
|
||||||
|
/*{ label: "主页", value: "/home", icon: "House" },*/
|
||||||
|
{ label: "面板选型", value: "/panelselection", icon: "EditPen" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 左侧返回首页按钮点击处理
|
||||||
|
const handleLeftClick = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'home':
|
||||||
|
menuChange("/home")
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('跳转错误!');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单操作处理
|
||||||
|
const handleAction = (action) => {
|
||||||
|
showPopover.value = false
|
||||||
|
switch (action) {
|
||||||
|
case 'profile':
|
||||||
|
router.push('/user')
|
||||||
|
break;
|
||||||
|
case 'logout':
|
||||||
|
localStorage.setItem('login', false)
|
||||||
|
router.push('/login')
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('跳转错误!');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshToHome = () => {
|
||||||
|
localHttp.post('ConfigPY/RefreshConfig')
|
||||||
|
router.push('/home')
|
||||||
|
setTimeout(() => {
|
||||||
|
location.reload(true);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单处理
|
||||||
|
const menuChange = (val) => {
|
||||||
|
if (currentTitle.routes == val) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentTitle.label = val;
|
||||||
|
currentTitle.routes = val;
|
||||||
|
localStorage.setItem('url', val)
|
||||||
|
router.push(val)
|
||||||
|
|
||||||
|
// 移动端选择菜单后关闭侧边栏
|
||||||
|
if (isMobile.value) {
|
||||||
|
drawerVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 控制深色模式
|
||||||
|
const isDarkMode = ref(false)
|
||||||
|
const webToDark = (dark) => {
|
||||||
|
// 获取 <html> 元素的类名
|
||||||
|
let ishtmldark = null;
|
||||||
|
if (document.documentElement.className.indexOf('dark') !== -1) {
|
||||||
|
ishtmldark = false
|
||||||
|
} else {
|
||||||
|
ishtmldark = true
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听路由变化
|
||||||
|
watch(() => route.path, (newPath) => {
|
||||||
|
if (newPath === '/login') return
|
||||||
|
|
||||||
|
// 同步更新当前激活菜单
|
||||||
|
currentTitle.routes = newPath
|
||||||
|
localStorage.setItem('url', newPath)
|
||||||
|
}, { immediate: true })
|
||||||
|
// 监听路由变化
|
||||||
|
/* watch(() => alreadyLogin, async (newPath) => {
|
||||||
|
if (newPath.value) {
|
||||||
|
//await getProject()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
})*/
|
||||||
|
|
||||||
|
const ajaxfile = async (form) => {
|
||||||
|
if (!form.File || !form.File instanceof File) {
|
||||||
|
throw new Error('文件对象无效!');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
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.httpApi + 'FileUpload/UploadFile', settings);
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
provide('checkLoginStatus', checkLoginStatus)
|
||||||
|
provide('projectList', projectList);
|
||||||
|
provide('selectedProjectId', selectedProjectId);
|
||||||
|
provide('selectedProject', selectedProject);
|
||||||
|
provide('cloudHttp', cloudHttp);
|
||||||
|
provide('localHttp', localHttp);
|
||||||
|
provide('formatDate', formatDate);
|
||||||
|
provide('ajaxfile', ajaxfile)
|
||||||
|
provide('isMobile', isMobile)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.main-container {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航栏样式 */
|
||||||
|
.top-header {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 横向菜单样式 */
|
||||||
|
.horizontal-menu {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 中间区域样式 */
|
||||||
|
.header-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-logo {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-text {
|
||||||
|
font-size: 23px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 10px;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu {
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 路由内容区样式 */
|
||||||
|
.router-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.router-container.mobile-view {
|
||||||
|
padding: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部样式 */
|
||||||
|
.global-footer {
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(128, 128, 128, 0.2);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-footer.mobile-footer {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 0 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整菜单项间距 */
|
||||||
|
:deep(.el-menu--horizontal) > .el-menu-item,
|
||||||
|
:deep(.el-menu--horizontal) > .el-sub-menu .el-sub-menu__title {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整菜单图标样式 */
|
||||||
|
:deep(.el-menu--horizontal) .el-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端侧边栏样式 */
|
||||||
|
.mobile-drawer {
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-container {
|
||||||
|
padding: 20px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-logo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.top-header {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-center {
|
||||||
|
display: none; /* 在移动端隐藏中间区域 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
padding-right: 5px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
72
src/assets/base.css
Normal file
72
src/assets/base.css
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::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
1
src/assets/logo.svg
Normal 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
37
src/assets/main.css
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
@import './base.css';
|
||||||
|
|
||||||
|
#app {
|
||||||
|
/*max-width: 1440px;*/
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.green {
|
||||||
|
text-decoration: none;
|
||||||
|
color: hsla(160, 100%, 37%, 1);
|
||||||
|
transition: 0.4s;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
a:hover {
|
||||||
|
background-color: hsla(160, 100%, 37%, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: grid;
|
||||||
|
/*grid-template-columns: 1fr 1fr;*/
|
||||||
|
padding: 0 ;
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/axios.js
Normal file
115
src/axios.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import config from '../public/config.js';
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
// 创建一个可以生成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);
|
||||||
|
}
|
||||||
|
// ============ token处理逻辑 ============
|
||||||
|
// 获取当前请求路径(不含查询参数)
|
||||||
|
const requestPath = config.url.split('?')[0];
|
||||||
|
//console.log(requestPath);
|
||||||
|
// 检查是否需要添加token
|
||||||
|
if (!requestPath.includes('Login/Login')) {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ============ 结束新增 ============
|
||||||
|
return config;
|
||||||
|
}, error => Promise.reject(error));
|
||||||
|
|
||||||
|
// 添加响应拦截器
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
if (response.config.url !== 'Login/Login' && response.status === 200) {
|
||||||
|
refreshToken();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
if (error.response.status === 401) {
|
||||||
|
ElMessage.error('token失效 请重新登录')
|
||||||
|
setTimeout(() => {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
window.location.href = '/login'
|
||||||
|
}, 233);
|
||||||
|
}
|
||||||
|
console.log(error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建一个默认实例(保持现有用法兼容)
|
||||||
|
const defaultInstance = createAxiosInstance(import.meta.env.VITE_API_BASE_URL || '');
|
||||||
|
|
||||||
|
// 导出默认实例(保持现有用法)
|
||||||
|
export default defaultInstance;
|
||||||
|
|
||||||
|
// 同时导出工厂函数(用于创建自定义实例)
|
||||||
|
export { createAxiosInstance };
|
||||||
|
|
||||||
|
|
||||||
|
// Token刷新方法
|
||||||
|
async function refreshToken() {
|
||||||
|
try {
|
||||||
|
// 检查上次自动登录时间(新增)
|
||||||
|
const lastAutoLoginTime = localStorage.getItem('lastAutoLoginTime');
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
|
||||||
|
// 如果15分钟内已自动登录过,则跳过(新增)
|
||||||
|
if (lastAutoLoginTime && currentTime - lastAutoLoginTime < 15 * 60 * 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = localStorage.getItem('rememberedUsername')
|
||||||
|
const password = localStorage.getItem('rememberedPassword')
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
throw new Error('登录过期 请重新登录')
|
||||||
|
}
|
||||||
|
|
||||||
|
const formdata = {
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新自动登录时间(新增)
|
||||||
|
localStorage.setItem('lastAutoLoginTime', currentTime.toString());
|
||||||
|
|
||||||
|
createAxiosInstance(0).post('Login/Login', formdata)
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.isok) {
|
||||||
|
localStorage.setItem('token', response.data.response.accessToken);
|
||||||
|
//console.log('自动登录成功')
|
||||||
|
} else {
|
||||||
|
throw new Error('token更新失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
// 登录失败时清除时间记录(新增)
|
||||||
|
localStorage.removeItem('lastAutoLoginTime');
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/components/HelloWorld.vue
Normal file
44
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
msg: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="greetings">
|
||||||
|
<h1 class="green">{{ msg }}</h1>
|
||||||
|
<h3>
|
||||||
|
You’ve successfully created a project with
|
||||||
|
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h1 {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
position: relative;
|
||||||
|
top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
94
src/components/TheWelcome.vue
Normal file
94
src/components/TheWelcome.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<script setup>
|
||||||
|
import WelcomeItem from './WelcomeItem.vue'
|
||||||
|
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||||
|
import ToolingIcon from './icons/IconTooling.vue'
|
||||||
|
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||||
|
import CommunityIcon from './icons/IconCommunity.vue'
|
||||||
|
import SupportIcon from './icons/IconSupport.vue'
|
||||||
|
|
||||||
|
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<DocumentationIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Documentation</template>
|
||||||
|
|
||||||
|
Vue’s
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||||
|
provides you with all information you need to get started.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<ToolingIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Tooling</template>
|
||||||
|
|
||||||
|
This project is served and bundled with
|
||||||
|
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||||
|
recommended IDE setup is
|
||||||
|
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||||
|
+
|
||||||
|
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
|
||||||
|
you need to test your components and web pages, check out
|
||||||
|
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
||||||
|
and
|
||||||
|
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||||
|
/
|
||||||
|
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
More instructions are available in
|
||||||
|
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||||
|
>.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<EcosystemIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Ecosystem</template>
|
||||||
|
|
||||||
|
Get official tools and libraries for your project:
|
||||||
|
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||||
|
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||||
|
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||||
|
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||||
|
you need more resources, we suggest paying
|
||||||
|
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||||
|
a visit.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<CommunityIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Community</template>
|
||||||
|
|
||||||
|
Got stuck? Ask your question on
|
||||||
|
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||||
|
(our official Discord server), or
|
||||||
|
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||||
|
>StackOverflow</a
|
||||||
|
>. You should also follow the official
|
||||||
|
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||||
|
Bluesky account or the
|
||||||
|
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||||
|
X account for latest news in the Vue world.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<SupportIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Support Vue</template>
|
||||||
|
|
||||||
|
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||||
|
us by
|
||||||
|
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||||
|
</WelcomeItem>
|
||||||
|
</template>
|
||||||
87
src/components/WelcomeItem.vue
Normal file
87
src/components/WelcomeItem.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="item">
|
||||||
|
<i>
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
</i>
|
||||||
|
<div class="details">
|
||||||
|
<h3>
|
||||||
|
<slot name="heading"></slot>
|
||||||
|
</h3>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
place-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
color: var(--color-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.item {
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
top: calc(50% - 25px);
|
||||||
|
left: -26px;
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background: var(--color-background);
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:before {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:after {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:first-of-type:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:last-of-type:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
26
src/components/i18n.js
Normal file
26
src/components/i18n.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||||
|
import en from 'element-plus/es/locale/lang/en';
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
en: {
|
||||||
|
message: {
|
||||||
|
hello: 'hello world'
|
||||||
|
},
|
||||||
|
el: en // Element Plus的英文翻译
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
message: {
|
||||||
|
hello: '你好,世界'
|
||||||
|
},
|
||||||
|
el: zhCn // Element Plus的中文翻译
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
locale: 'zh', // 设置默认语言
|
||||||
|
fallbackLocale: 'en', // 设置备用语言
|
||||||
|
messages, // 设置翻译信息
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
7
src/components/icons/IconCommunity.vue
Normal file
7
src/components/icons/IconCommunity.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
src/components/icons/IconDocumentation.vue
Normal file
7
src/components/icons/IconDocumentation.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
src/components/icons/IconEcosystem.vue
Normal file
7
src/components/icons/IconEcosystem.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
src/components/icons/IconSupport.vue
Normal file
7
src/components/icons/IconSupport.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
19
src/components/icons/IconTooling.vue
Normal file
19
src/components/icons/IconTooling.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
class="iconify iconify--mdi"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
29
src/main.js
Normal file
29
src/main.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import config from '../public/config.js';
|
||||||
|
import axios from './axios' // 导入配置好的实例
|
||||||
|
import i18n from './components/i18n.js' // 导入i18n
|
||||||
|
import './assets/main.css'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
// axios挂载到全局属性
|
||||||
|
app.config.globalProperties.$http = axios
|
||||||
|
|
||||||
|
// 提供依赖注入
|
||||||
|
app.provide('config', config)
|
||||||
|
app.provide('$http', axios)
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus, { locale: zhCn })
|
||||||
|
app.use(i18n)
|
||||||
|
app.mount('#app')
|
||||||
110
src/pages/404/index.vue
Normal file
110
src/pages/404/index.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="interactive-container">
|
||||||
|
<div class="error-card">
|
||||||
|
<div class="liquid"></div>
|
||||||
|
<div class="content">
|
||||||
|
<h1>404</h1>
|
||||||
|
<p>错误(404):当前页面未找到</p>
|
||||||
|
<p>SYSTEM ERROR: PAGE NOT FOUND</p>
|
||||||
|
<el-button type="success" plain round
|
||||||
|
class="magnetic-btn" :icon="HomeFilled"
|
||||||
|
@mouseenter="playHoverSound"
|
||||||
|
@click="goHome">
|
||||||
|
返回主页
|
||||||
|
<span class="hover-effect"></span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { HomeFilled } from '@element-plus/icons-vue'
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
router.push('home');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.interactive-container {
|
||||||
|
height: 80%;
|
||||||
|
width: 90%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
position: relative;
|
||||||
|
width: 400px;
|
||||||
|
height: 500px;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 25px 45px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.liquid {
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
animation: rotate 10s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: absolute;
|
||||||
|
inset: 2px;
|
||||||
|
border-radius: 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 8rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 1rem 0 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magnetic-btn {
|
||||||
|
position: relative;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border: 2px solid rgba(255,255,255,0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magnetic-btn:hover {
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-effect {
|
||||||
|
position: absolute;
|
||||||
|
background: radial-gradient(circle at center, rgba(255,255,255,0.4) 0%, transparent 70%);
|
||||||
|
transform: scale(0);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magnetic-btn:hover .hover-effect {
|
||||||
|
transform: scale(2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
561
src/pages/commanage/index.vue
Normal file
561
src/pages/commanage/index.vue
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
<template>
|
||||||
|
<div class="company-management">
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<div class="header">
|
||||||
|
<el-button type="primary" @click="handleAdd" :icon="Plus">添加企业</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 企业表格 -->
|
||||||
|
<el-table :data="companies" border style="width: 100%" v-loading="loading" empty-text="暂无数据">
|
||||||
|
<el-table-column label="操作" width="102">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="info" :icon="Edit" circle plain @click="handleEdit(row)" />
|
||||||
|
<el-button type="danger" :icon="Delete" circle plain @click="handleDelete(row)" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="code" label="公司代码" width="100" />
|
||||||
|
<el-table-column prop="nameCn" label="中文名称" width="180" />
|
||||||
|
<el-table-column prop="nameEn" label="英文名称" width="180" />
|
||||||
|
<el-table-column prop="industry" label="所属行业" width="120" />
|
||||||
|
<el-table-column prop="identity" label="身份" width="120" />
|
||||||
|
<el-table-column prop="region" label="地区" width="120" />
|
||||||
|
<!--<el-table-column prop="licenseCode" label="营业执照" width="220" />-->
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 添加/编辑对话框 -->
|
||||||
|
<el-dialog v-model="dialogVisible"
|
||||||
|
:title="isEdit ? '编辑企业' : '添加企业'"
|
||||||
|
width="800px">
|
||||||
|
<el-form :model="form"
|
||||||
|
:rules="rules"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="120px"
|
||||||
|
label-position="right">
|
||||||
|
|
||||||
|
<el-form-item label="中文名称" prop="nameCn">
|
||||||
|
<el-input v-model="form.nameCn" placeholder="请输入中文名称" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="英文名称" prop="nameEn">
|
||||||
|
<el-input v-model="form.nameEn" placeholder="请输入英文名称" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="所属行业" prop="industry">
|
||||||
|
<el-select v-model="form.industry" placeholder="请选择行业">
|
||||||
|
<el-option v-for="item in industryOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="item"
|
||||||
|
:value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="公司身份" prop="identity">
|
||||||
|
<el-select v-model="form.identity" placeholder="请选择身份">
|
||||||
|
<el-option v-for="item in identityOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="item"
|
||||||
|
:value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="所属地区" prop="region">
|
||||||
|
<el-select v-model="form.region" placeholder="请选择地区">
|
||||||
|
<el-option v-for="(key,item) in regionOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="item"
|
||||||
|
:value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<!--
|
||||||
|
<el-form-item label="营业执照代码" prop="licenseCode">
|
||||||
|
<el-input v-model="form.licenseCode" placeholder="请输入营业执照代码" />
|
||||||
|
</el-form-item>-->
|
||||||
|
<!--<el-form-item label="营业执照" prop="licenseImage">
|
||||||
|
<el-upload class="license-uploader"
|
||||||
|
drag
|
||||||
|
action="/api/upload"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-change="handleLicenseChange"
|
||||||
|
:auto-upload="false">-->
|
||||||
|
<!-- 预览区域 -->
|
||||||
|
<!--<div v-if="form.licenseImage || form.licensePreview" class="image-preview">
|
||||||
|
<img v-if="isImageFile(form.licenseImageFile)"
|
||||||
|
:src="form.licensePreview || getFullUrl(form.licenseImage)"
|
||||||
|
class="preview-image" />
|
||||||
|
<div v-else class="file-preview">
|
||||||
|
<el-icon size="33"><DocumentChecked /></el-icon>
|
||||||
|
<div>营业执照文件 <span style="color: #409EFF">√已上传</span></div>
|
||||||
|
<div class="file-name">{{ getFileName(form.licenseImage) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="preview-mask">
|
||||||
|
<el-icon><UploadFilled /></el-icon>
|
||||||
|
<div>点击更换文件</div>
|
||||||
|
</div>
|
||||||
|
</div>-->
|
||||||
|
<!-- 未上传状态 -->
|
||||||
|
<!--<template v-else>
|
||||||
|
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
拖拽文件到此 或 <em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
支持JPG/PNG/PDF格式,大小不超过5MB
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>-->
|
||||||
|
|
||||||
|
<el-form-item label="公司Logo" prop="logo">
|
||||||
|
<el-upload class="logo-uploader"
|
||||||
|
drag
|
||||||
|
action="/api/upload"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-change="handleLogoChange"
|
||||||
|
:auto-upload="false">
|
||||||
|
<!-- 预览区域 -->
|
||||||
|
<div v-if="form.logoaddress || form.logoPreview" class="image-preview">
|
||||||
|
<img :src="form.logoPreview || getFullUrl(form.logoaddress)"
|
||||||
|
class="preview-image" />
|
||||||
|
<div class="preview-mask">
|
||||||
|
<el-icon><UploadFilled /></el-icon>
|
||||||
|
<div>点击更换Logo</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未上传状态 -->
|
||||||
|
<template v-else>
|
||||||
|
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
拖拽图片到此 或 <em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
支持JPG/PNG格式,大小不超过5MB
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, inject, onUnmounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Plus, Delete, Edit, UploadFilled, DocumentChecked } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const $http = inject('$http')
|
||||||
|
const ajaxfile = inject('ajaxfile');
|
||||||
|
const config = inject('config');
|
||||||
|
|
||||||
|
// 数据列表
|
||||||
|
const companies = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 对话框相关
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const currentCompanyId = ref(null)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
// 表单初始数据
|
||||||
|
const defaultForm = {
|
||||||
|
code: '',
|
||||||
|
nameCn: '',
|
||||||
|
nameEn: '',
|
||||||
|
industry: '',
|
||||||
|
identity: '',
|
||||||
|
region: '',
|
||||||
|
licenseCode: '',
|
||||||
|
logoaddress: '',
|
||||||
|
licenseImage: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive({ ...defaultForm })
|
||||||
|
|
||||||
|
// 选项数据
|
||||||
|
const industryOptions = ref([]);
|
||||||
|
const getIndustry = () => {
|
||||||
|
try {
|
||||||
|
const rs = $http.post('ConfigPY/GetSingleValue', {
|
||||||
|
VarName: "行业分类",
|
||||||
|
}).then(rs => {
|
||||||
|
//console.log(JSON.parse(rs.data.response));
|
||||||
|
industryOptions.value = JSON.parse(rs.data.response)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const identityOptions = ref([]);
|
||||||
|
const getIdentity = () => {
|
||||||
|
try {
|
||||||
|
const rs = $http.post('ConfigPY/GetSingleValue', {
|
||||||
|
VarName: "公司身份",
|
||||||
|
}).then(rs => {
|
||||||
|
//console.log(JSON.parse(rs.data.response));
|
||||||
|
identityOptions.value = JSON.parse(rs.data.response)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const regionOptions = ref([]);
|
||||||
|
const getRegion = () => {
|
||||||
|
try {
|
||||||
|
const rs = $http.post('ConfigPY/GetSingleValue', {
|
||||||
|
VarName: "区域",
|
||||||
|
}).then(rs => {
|
||||||
|
//console.log(JSON.parse(rs.data.response));
|
||||||
|
regionOptions.value = JSON.parse(rs.data.response)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证规则
|
||||||
|
const rules = reactive({
|
||||||
|
nameCn: [
|
||||||
|
{ required: true, message: '请输入中文名称', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
industry: [
|
||||||
|
{ required: true, message: '请选择所属行业', trigger: 'change' }
|
||||||
|
],
|
||||||
|
identity: [
|
||||||
|
{ required: true, message: '请选择所属身份', trigger: 'change' }
|
||||||
|
],
|
||||||
|
region: [
|
||||||
|
{ required: true, message: '请选择所属地区', trigger: 'change' }
|
||||||
|
]/*,
|
||||||
|
licenseCode: [
|
||||||
|
{ required: true, message: '请输入营业执照代码', trigger: 'blur' }
|
||||||
|
]*/
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取企业列表
|
||||||
|
const getCompanies = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const rs = await $http.post('Company/GetComInfo', {
|
||||||
|
IsAll: true,
|
||||||
|
ID: 0,
|
||||||
|
})
|
||||||
|
if (rs.data.isok) {
|
||||||
|
companies.value = rs.data.response
|
||||||
|
//console.log(companies.value);
|
||||||
|
} else {
|
||||||
|
ElMessage.error(rs.data.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取企业列表失败')
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取完整图片URL
|
||||||
|
const getFullUrl = (filename) => {
|
||||||
|
//console.log(config.httpAds + `${filename}`);
|
||||||
|
if (!filename) return '';
|
||||||
|
// 如果已经是完整URL直接返回
|
||||||
|
if (filename.startsWith('http')) return filename;
|
||||||
|
// 根据实际存储路径拼接
|
||||||
|
return config.httpAds + `${filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeImageUpload = (file) => {
|
||||||
|
const isImage = file.type.startsWith('image/')
|
||||||
|
const isLt5M = file.size / 1024 / 1024 < 5
|
||||||
|
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error('只能上传图片文件!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!isLt5M) {
|
||||||
|
ElMessage.error('文件大小不能超过5MB!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const beforeImageUploadPDF = (file) => {
|
||||||
|
const isImageOrPDF = file.type.startsWith('image/') || file.type === 'application/pdf';
|
||||||
|
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||||
|
|
||||||
|
if (!isImageOrPDF) {
|
||||||
|
ElMessage.error('只能上传图片文件或PDF文件!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isLt5M) {
|
||||||
|
ElMessage.error('文件大小不能超过5MB!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 添加公司
|
||||||
|
const handleAdd = () => {
|
||||||
|
Object.assign(form, defaultForm);
|
||||||
|
isEdit.value = false;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑公司
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
Object.assign(form, row);
|
||||||
|
isEdit.value = true;
|
||||||
|
currentCompanyId.value = row.id;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除公司
|
||||||
|
const handleDelete = (row) => {
|
||||||
|
ElMessageBox.confirm('确定删除该公司吗?', '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
//console.log(row.id);
|
||||||
|
const rs = await $http.post('Company/DelCom', { Id: row.id });
|
||||||
|
if (rs.data.isok) {
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
await getCompanies();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(rs.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}).catch(() => { });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogoChange = (file) => {
|
||||||
|
form.logoFile = file.raw;
|
||||||
|
// 生成本地预览图URL
|
||||||
|
form.logoPreview = URL.createObjectURL(file.raw); // 新增预览字段
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLicenseChange = (file) => {
|
||||||
|
form.licenseImageFile = file.raw;
|
||||||
|
// 生成本地预览图URL
|
||||||
|
form.licensePreview = URL.createObjectURL(file.raw); // 新增预览字段
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
await formRef.value.validate();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 上传Logo
|
||||||
|
if (form.logoFile) {
|
||||||
|
const logoFileData = {
|
||||||
|
File: form.logoFile,
|
||||||
|
Folder: "companylogo" // 根据实际存储目录调整
|
||||||
|
};
|
||||||
|
const logoUploadRes = await ajaxfile(logoFileData);
|
||||||
|
if (logoUploadRes?.fileName) {
|
||||||
|
form.logoaddress = "Uploads/companylogo/" + logoUploadRes.fileName;
|
||||||
|
} else {
|
||||||
|
ElMessage.error('上传Logo失败: ' + logoUploadRes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (form.logoaddress) { // 保留已有Logo逻辑
|
||||||
|
form.logoaddress = form.logoaddress.substring(form.logoaddress.lastIndexOf('/') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* // 上传营业执照
|
||||||
|
if (form.licenseImageFile) {
|
||||||
|
const licenseFileData = {
|
||||||
|
File: form.licenseImageFile,
|
||||||
|
Folder: "businesslic" // 根据实际存储目录调整
|
||||||
|
};
|
||||||
|
const licenseUploadRes = await ajaxfile(licenseFileData);
|
||||||
|
if (licenseUploadRes?.fileName) {
|
||||||
|
form.licenseImage = licenseUploadRes.fileName;
|
||||||
|
} else {
|
||||||
|
ElMessage.error('上传营业执照失败: ' + licenseUploadRes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (form.licenseImage) { // 保留已有营业执照逻辑
|
||||||
|
form.licenseImage = form.licenseImage.substring(form.licenseImage.lastIndexOf('/') + 1);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// 提交表单数据
|
||||||
|
const api = isEdit.value ? 'Company/EditCom' : 'Company/AddCom';
|
||||||
|
const data = isEdit.value ? { ...form, id: currentCompanyId.value } : form;
|
||||||
|
|
||||||
|
const rs = await $http.post(api, data);
|
||||||
|
if (rs.data.isok) {
|
||||||
|
ElMessage.success(isEdit.value ? '修改成功' : '添加成功');
|
||||||
|
dialogVisible.value = false;
|
||||||
|
await getCompanies();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(rs.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('操作失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isImageFile = (url) => {
|
||||||
|
return /\.(jpe?g|png|gif|webp)$/i.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileName = (url) => {
|
||||||
|
return url.split('/').pop()
|
||||||
|
}
|
||||||
|
// 其他操作函数(handleAdd/handleEdit/handleDelete)与公司管理页面逻辑类似
|
||||||
|
// 此处省略,保持与公司管理页面相同逻辑即可
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getCompanies()
|
||||||
|
getIndustry()
|
||||||
|
getIdentity()
|
||||||
|
getRegion()
|
||||||
|
})
|
||||||
|
// 在组件卸载时释放对象URL
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (form.logoPreview) URL.revokeObjectURL(form.logoPreview);
|
||||||
|
if (form.licensePreview) URL.revokeObjectURL(form.licensePreview);
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.company-management {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-uploader,
|
||||||
|
.license-uploader {
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-uploader {
|
||||||
|
width: 300px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license-uploader {
|
||||||
|
width: 300px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-uploader:hover,
|
||||||
|
.license-uploader:hover {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通用上传样式 */
|
||||||
|
.logo-uploader :deep(.el-upload-dragger),
|
||||||
|
.license-uploader :deep(.el-upload-dragger) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 180px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览区域公共样式 */
|
||||||
|
.image-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 遮罩层样式 */
|
||||||
|
.preview-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview:hover .preview-mask {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.el-icon--upload {
|
||||||
|
font-size: 36px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
498
src/pages/dicmanage/index.vue
Normal file
498
src/pages/dicmanage/index.vue
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div>字典名称</div>
|
||||||
|
<el-select v-model="currentVarName"
|
||||||
|
placeholder="请选择字典项"
|
||||||
|
@change="handleDictChange">
|
||||||
|
<el-option v-for="item in dictList"
|
||||||
|
:key="item.varName"
|
||||||
|
:label="item.varName"
|
||||||
|
:value="item.varName" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>字典修改</div>
|
||||||
|
|
||||||
|
<!-- 区域字典特殊表格 -->
|
||||||
|
<el-table v-if="isRegionDict"
|
||||||
|
:data="regionData"
|
||||||
|
stripe
|
||||||
|
style="width: 777px">
|
||||||
|
<el-table-column prop="key" label="大区名称">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.key"
|
||||||
|
placeholder="请输入大区名称" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="value" label="包含省份(点击修改省份/地区)" width="521">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" @click="showProvinceDialog(row)"
|
||||||
|
link style="width: 100%; padding: 0; height: auto; min-height: 32px; white-space: normal; word-break: break-all; line-height: 1.5; display: inline-flex; align-items: center;">
|
||||||
|
<span style="text-align: left;">
|
||||||
|
{{ row.value.join(',') || '点击选择省份' }}
|
||||||
|
</span>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="63">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
circle
|
||||||
|
@click="deleteRegion($index)" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 普通字典表格 -->
|
||||||
|
<el-table v-else
|
||||||
|
:data="currentValues"
|
||||||
|
stripe
|
||||||
|
style="width: 777px">
|
||||||
|
<el-table-column prop="value" label="字典值">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-input v-model="currentValues[$index]" placeholder="请输入" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="63">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
circle
|
||||||
|
@click="handleDelete($index)" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 操作按钮区域 -->
|
||||||
|
<div class="footer">
|
||||||
|
<template v-if="isRegionDict">
|
||||||
|
<el-button type="primary"
|
||||||
|
:icon="Plus"
|
||||||
|
plain
|
||||||
|
round
|
||||||
|
@click="addNewRegion">
|
||||||
|
添加大区
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-button type="primary"
|
||||||
|
:icon="Plus"
|
||||||
|
plain
|
||||||
|
round
|
||||||
|
@click="handleAdd">
|
||||||
|
添加值
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-button type="success"
|
||||||
|
:icon="FolderChecked"
|
||||||
|
@click="handleSave">
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 省份选择对话框 -->
|
||||||
|
<el-dialog v-model="provinceDialogVisible"
|
||||||
|
title="选择省份/地区"
|
||||||
|
width="50%">
|
||||||
|
<el-checkbox-group v-model="selectedProvinces">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col v-for="province in allProvinces"
|
||||||
|
:key="province"
|
||||||
|
:span="8"
|
||||||
|
style="margin-bottom: 15px;">
|
||||||
|
<el-checkbox :label="province"
|
||||||
|
:value="province" />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-checkbox-group>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="provinceDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmProvinceSelection">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, inject, computed } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { Plus, Delete, Edit, UploadFilled, RefreshLeft, Upload, EditPen, FolderChecked } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
const $http = inject('$http')
|
||||||
|
const config = inject('config');
|
||||||
|
|
||||||
|
// 初始字典数据
|
||||||
|
const dictList = reactive([]);
|
||||||
|
// 新增响应式数据
|
||||||
|
const provinceDialogVisible = ref(false);
|
||||||
|
const selectedProvinces = ref([]);
|
||||||
|
const currentEditingRegion = ref(null);
|
||||||
|
const allProvinces = computed(() => {
|
||||||
|
const provinceDict = dictList.find(d => d.varName == '省份地区');
|
||||||
|
return provinceDict?.varValue || [];
|
||||||
|
});
|
||||||
|
// 获取字典
|
||||||
|
const getDic = async () => {
|
||||||
|
try {
|
||||||
|
const response = await $http.post('ConfigPY/GetConfigString');
|
||||||
|
const serverData = response.data.response || [];
|
||||||
|
|
||||||
|
//console.log(response);
|
||||||
|
// 清空原有数据
|
||||||
|
dictList.length = 0;
|
||||||
|
|
||||||
|
// 转换数据格式
|
||||||
|
serverData.forEach(item => {
|
||||||
|
dictList.push({
|
||||||
|
varName: item.varName,
|
||||||
|
varValue: tryParseJson(item.varValue) || []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
//console.log(dictList);
|
||||||
|
// 默认选中第一个(如果存在)
|
||||||
|
if (dictList.length > 0) {
|
||||||
|
currentVarName.value = dictList[0].varName;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取字典失败:', error);
|
||||||
|
ElMessage.error('字典数据加载失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// JSON安全解析方法
|
||||||
|
const tryParseJson = (str) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(str);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentVarName = ref('');
|
||||||
|
const currentDict = computed(() =>
|
||||||
|
dictList.find(item => item.varName === currentVarName.value)
|
||||||
|
);
|
||||||
|
const currentValues = computed({
|
||||||
|
get: () => currentDict.value?.varValue || [],
|
||||||
|
set: (val) => {
|
||||||
|
if (currentDict.value) {
|
||||||
|
currentDict.value.varValue = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理字典项变化
|
||||||
|
const handleDictChange = (val) => {
|
||||||
|
if (!val) return;
|
||||||
|
};
|
||||||
|
// 判断是否是区域字典
|
||||||
|
const isRegionDict = computed(() => currentVarName.value === '区域');
|
||||||
|
|
||||||
|
// 修改区域数据计算属性(移除setter)
|
||||||
|
const regionData = computed(() => {
|
||||||
|
if (!isRegionDict.value) return [];
|
||||||
|
const regionDict = dictList.find(d => d.varName === '区域');
|
||||||
|
return Object.entries(regionDict?.varValue || {}).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value: Array.isArray(value) ? value : [value]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示省份选择对话框
|
||||||
|
const showProvinceDialog = (row) => {
|
||||||
|
currentEditingRegion.value = row;
|
||||||
|
selectedProvinces.value = [...row.value];
|
||||||
|
provinceDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改省份选择确认方法
|
||||||
|
const confirmProvinceSelection = async () => {
|
||||||
|
try {
|
||||||
|
if (!currentEditingRegion.value) return;
|
||||||
|
|
||||||
|
// 获取当前编辑的大区信息
|
||||||
|
const currentKey = currentEditingRegion.value.key;
|
||||||
|
const selectedProvincesList = selectedProvinces.value;
|
||||||
|
|
||||||
|
// 获取区域字典数据
|
||||||
|
const regionDict = dictList.find(d => d.varName === '区域');
|
||||||
|
if (!regionDict) return;
|
||||||
|
|
||||||
|
// 收集所有其他大区的省份映射
|
||||||
|
const provinceMap = new Map();
|
||||||
|
Object.entries(regionDict.varValue).forEach(([region, provinces]) => {
|
||||||
|
if (region !== currentKey) {
|
||||||
|
provinces.forEach(province => {
|
||||||
|
provinceMap.set(province, region);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查重复省份
|
||||||
|
const duplicates = [];
|
||||||
|
selectedProvincesList.forEach(province => {
|
||||||
|
if (provinceMap.has(province)) {
|
||||||
|
duplicates.push({
|
||||||
|
province,
|
||||||
|
region: provinceMap.get(province)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果有重复则提示
|
||||||
|
if (duplicates.length > 0) {
|
||||||
|
const confirmMessage = `
|
||||||
|
以下省份已在其他大区存在:<br>
|
||||||
|
${duplicates.map(d => `${d.province} → ${d.region}`).join('<br>')}<br>
|
||||||
|
是否确认继续保存?
|
||||||
|
`;
|
||||||
|
|
||||||
|
await ElMessageBox.confirm(confirmMessage, '重复省份警告', {
|
||||||
|
confirmButtonText: '强制保存',
|
||||||
|
cancelButtonText: '返回修改',
|
||||||
|
type: 'warning',
|
||||||
|
dangerouslyUseHTMLString: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行保存操作
|
||||||
|
regionDict.varValue = {
|
||||||
|
...regionDict.varValue,
|
||||||
|
[currentKey]: selectedProvincesList
|
||||||
|
};
|
||||||
|
provinceDialogVisible.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
return; // 用户取消则中断流程
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/* const confirmProvinceSelection = () => {
|
||||||
|
if (currentEditingRegion.value) {
|
||||||
|
const regionDict = dictList.find(d => d.varName === '区域');
|
||||||
|
if (regionDict) {
|
||||||
|
regionDict.varValue = {
|
||||||
|
...regionDict.varValue,
|
||||||
|
[currentEditingRegion.value.key]: selectedProvinces.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
provinceDialogVisible.value = false;
|
||||||
|
};*/
|
||||||
|
|
||||||
|
// 修改添加大区方法
|
||||||
|
const addNewRegion = () => {
|
||||||
|
const regionDict = dictList.find(d => d.varName === '区域');
|
||||||
|
if (!regionDict) return;
|
||||||
|
|
||||||
|
// 生成唯一键名
|
||||||
|
let newKey = "新增大区";
|
||||||
|
let counter = 1;
|
||||||
|
while (regionDict.varValue[newKey]) {
|
||||||
|
newKey = `新增大区${counter++}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接修改原始数据
|
||||||
|
regionDict.varValue = {
|
||||||
|
...regionDict.varValue,
|
||||||
|
[newKey]: []
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改删除大区方法
|
||||||
|
const deleteRegion = (index) => {
|
||||||
|
ElMessageBox.confirm('确定要删除该大区吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
const regionDict = dictList.find(d => d.varName === '区域');
|
||||||
|
if (!regionDict) return;
|
||||||
|
|
||||||
|
const keyToDelete = regionData.value[index].key;
|
||||||
|
const newValue = { ...regionDict.varValue };
|
||||||
|
delete newValue[keyToDelete];
|
||||||
|
regionDict.varValue = newValue;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加行
|
||||||
|
const handleAdd = () => {
|
||||||
|
if (!currentDict.value) return;
|
||||||
|
currentDict.value.varValue.push('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除行
|
||||||
|
const handleDelete = (index) => {
|
||||||
|
ElMessageBox.confirm('确定要删除该行吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
currentDict.value.varValue.splice(index, 1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存字典
|
||||||
|
const saveDic = async (name, value) => {
|
||||||
|
try {
|
||||||
|
let filteredValue;
|
||||||
|
|
||||||
|
// 根据数据类型处理
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// 处理普通数组格式
|
||||||
|
filteredValue = value
|
||||||
|
.map(item => String(item).trim())
|
||||||
|
.filter(item => item !== "");
|
||||||
|
|
||||||
|
if (filteredValue.length === 0) {
|
||||||
|
throw new Error("不能保存空字典项");
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'object' && value !== null) {
|
||||||
|
// 处理区域字典的对象格式
|
||||||
|
filteredValue = Object.entries(value).reduce((acc, [key, values]) => {
|
||||||
|
const filtered = values
|
||||||
|
.map(item => String(item).trim())
|
||||||
|
.filter(item => item !== "");
|
||||||
|
|
||||||
|
// 保留key即使值为空数组(如"其他")
|
||||||
|
if (key === '其他' || filtered.length > 0) {
|
||||||
|
acc[key] = filtered;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// 检查是否为空对象
|
||||||
|
if (Object.keys(filteredValue).length === 0) {
|
||||||
|
throw new Error("至少需要保留一个有效区域");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("无效的字典格式");
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueJs = JSON.stringify(filteredValue);
|
||||||
|
const rs = await $http.post('ConfigPY/SaveOrAddConfigString', {
|
||||||
|
VarName: name,
|
||||||
|
VarValue: valueJs
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredValue;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存处理
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!currentDict.value) {
|
||||||
|
ElMessage.warning('请先选择要修改的字典项');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'此操作将永久修改字典数据,是否继续?',
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确认保存',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
center: true,
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
beforeClose: (action, instance, done) => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
instance.confirmButtonLoading = true;
|
||||||
|
saveDic(currentDict.value.varName, currentDict.value.varValue)
|
||||||
|
.then(() => {
|
||||||
|
done();
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
instance.confirmButtonLoading = false;
|
||||||
|
ElMessage.error('保存失败');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// 处理区域字典的特殊结构
|
||||||
|
let saveData = currentDict.value.varValue;
|
||||||
|
|
||||||
|
if (isRegionDict.value) {
|
||||||
|
// 转换回原始对象格式
|
||||||
|
saveData = regionData.value.reduce((acc, cur) => {
|
||||||
|
acc[cur.key] = cur.value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存并获取处理后的数据
|
||||||
|
const filteredData = await saveDic(
|
||||||
|
currentDict.value.varName,
|
||||||
|
saveData
|
||||||
|
);
|
||||||
|
|
||||||
|
// 更新前端数据
|
||||||
|
currentDict.value.varValue = isRegionDict.value
|
||||||
|
? { ...filteredData } // 对象格式
|
||||||
|
: [...filteredData]; // 数组格式
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error === 'cancel') {
|
||||||
|
//console.log('用户取消保存');
|
||||||
|
} else if (error.message === '不能保存空字典项') {
|
||||||
|
ElMessage.warning('字典项至少需要包含一个有效值');
|
||||||
|
} else {
|
||||||
|
ElMessage.error('保存失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getDic()
|
||||||
|
// 初始化选择第一个
|
||||||
|
if (dictList.length > 0) {
|
||||||
|
currentVarName.value = dictList[0].varName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 333px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
width: 777px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between; /* 推荐方案 */
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.el-message-box {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
src/pages/home/index.vue
Normal file
24
src/pages/home/index.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div>Home</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject, onMounted } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
const $http = inject('$http')
|
||||||
|
const router = useRouter()
|
||||||
|
// 注入方法
|
||||||
|
const checkLoginStatus = inject('checkLoginStatus');
|
||||||
|
onMounted(() => {
|
||||||
|
localStorage.setItem('url', '/home')
|
||||||
|
checkLoginStatus();
|
||||||
|
router.push('/panelselection')
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
20
src/pages/log/index.vue
Normal file
20
src/pages/log/index.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject, onMounted } from 'vue';
|
||||||
|
const $http = inject('$http')
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
458
src/pages/login/index.vue
Normal file
458
src/pages/login/index.vue
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
<template>
|
||||||
|
<div style="height:100vh">
|
||||||
|
<div class="container">
|
||||||
|
<div class="logo-container">
|
||||||
|
<img src="../../../public/logobig.svg" style="width: 400px; height: auto; " />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="login-container">
|
||||||
|
<h1 style="margin:100px 0px 100px 0px; text-align:center">面板选型平台</h1>
|
||||||
|
<el-form :model="form"
|
||||||
|
status-icon
|
||||||
|
:disabled="isLocked"
|
||||||
|
@submit.prevent="handleSubmit">
|
||||||
|
<!-- 账号输入 -->
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input v-model="form.username"
|
||||||
|
placeholder="请输入账号"
|
||||||
|
clearable
|
||||||
|
:prefix-icon="User" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 密码输入 -->
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
show-password
|
||||||
|
:prefix-icon="Lock" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 验证码区域 -->
|
||||||
|
<el-form-item prop="code" class="captcha-container">
|
||||||
|
<div class="captcha-input">
|
||||||
|
<el-input v-model="form.code"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
:prefix-icon="Key"
|
||||||
|
maxlength="4" />
|
||||||
|
<img :src="captchaSrc"
|
||||||
|
class="captcha-image"
|
||||||
|
alt="验证码"
|
||||||
|
@click="generateCaptcha" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 记住我 & 操作按钮 -->
|
||||||
|
<el-form-item>
|
||||||
|
<div class="form-actions">
|
||||||
|
<el-checkbox v-model="form.remember">记住我</el-checkbox>
|
||||||
|
<el-button type="primary"
|
||||||
|
native-type="submit"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="submitDisabled">
|
||||||
|
{{ isLocked ? `请等待${lockRemainTime}后重试` : '立即登录' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 错误提示 -->
|
||||||
|
<div v-if="errorAttempts > 0" class="error-tip">
|
||||||
|
已错误尝试 {{ errorAttempts }} 次(5次后将锁定账号)
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { User, Lock, Key } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
|
||||||
|
const checkLoginStatus = inject('checkLoginStatus');
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const $http = inject('$http')
|
||||||
|
|
||||||
|
// 响应式状态
|
||||||
|
const form = ref({
|
||||||
|
username: localStorage.getItem('rememberedUsername') || '',
|
||||||
|
password: localStorage.getItem('rememberedPassword') || '',
|
||||||
|
remember: localStorage.getItem('rememberedUsername') ? true : false,
|
||||||
|
code: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitDisabled = ref(false);
|
||||||
|
const isLocked = ref(false);
|
||||||
|
const errorAttempts = ref(parseInt(localStorage.getItem('loginErrorAttempts')) || 0);
|
||||||
|
const lockUntil = ref(parseInt(localStorage.getItem('loginLockUntil')) || 0);
|
||||||
|
const captchaSrc = ref('');
|
||||||
|
const captchaValue = ref('');
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const lockRemainTime = computed(() => {
|
||||||
|
if (!isLocked) return '00:00'
|
||||||
|
const remain = (lockUntil.value - Date.now()) / 1000
|
||||||
|
return `${Math.floor(remain / 60)}分${Math.floor(remain % 60)}秒`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.setItem('login', false)
|
||||||
|
initAuthState()
|
||||||
|
generateCaptcha()
|
||||||
|
checkLoginStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法实现
|
||||||
|
const initAuthState = () => {
|
||||||
|
if (lockUntil.value && Date.now() < lockUntil.value) {
|
||||||
|
setupUnlockTimer(lockUntil.value - Date.now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 = '#FFFFFF';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// 绘制噪点
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
for (let i = 0; i < 33; i++) {
|
||||||
|
const x = Math.random() * canvas.width;
|
||||||
|
const y = Math.random() * canvas.height;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, 1, 0, Math.PI * 2, false);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制验证码
|
||||||
|
ctx.font = '24px Arial';
|
||||||
|
let xnd = 10;
|
||||||
|
for (let i = 0; i < captcha.length; i++) {
|
||||||
|
const angle = (Math.random() * 30) - 15; // -10度到+10度
|
||||||
|
|
||||||
|
// 保存当前状态
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
// 旋转并绘制文字
|
||||||
|
ctx.translate(xnd, 20);
|
||||||
|
ctx.rotate(angle * Math.PI / 180);
|
||||||
|
ctx.fillText(captcha[i], 0, 0);
|
||||||
|
|
||||||
|
// 恢复状态
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// 更新下一个字符的x位置
|
||||||
|
xnd += 15 + Math.abs(angle) / 2; // 根据角度调整字符间距
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加5-10条干扰条纹
|
||||||
|
ctx.strokeStyle = '#6F4A2F';
|
||||||
|
const bug = Math.floor(Math.random() * 6) + 3
|
||||||
|
for (let i = 0; i < bug; i++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
|
||||||
|
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
// 将canvas转换为图片URL
|
||||||
|
captchaSrc.value = canvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (form.value.username == "WYSERVER") return
|
||||||
|
if (isLocked.value || submitDisabled.value) return
|
||||||
|
if (!validateForm()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await $http.post('Login/Login', {
|
||||||
|
username: form.value.username,
|
||||||
|
password: form.value.password
|
||||||
|
})
|
||||||
|
//console.log(response)
|
||||||
|
handleLoginResponse(response.data)
|
||||||
|
} catch (error) {
|
||||||
|
handleLoginError(error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 新增表单验证方法
|
||||||
|
const validateForm = () => {
|
||||||
|
if (!form.value.username.trim()) {
|
||||||
|
ElMessage.error('请输入账号!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!form.value.password.trim()) {
|
||||||
|
ElMessage.error('请输入密码!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!form.value.code.trim()) {
|
||||||
|
ElMessage.error('请输入验证码!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (form.value.code !== captchaValue.value) {
|
||||||
|
ElMessage.error('验证码错误!')
|
||||||
|
generateCaptcha()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 新增错误处理方法
|
||||||
|
const handleLoginError = (error) => {
|
||||||
|
console.error('登录请求失败:', error)
|
||||||
|
ElMessage.error('网络请求失败,请检查网络连接')
|
||||||
|
errorAttempts.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleLoginResponse = (data) => {
|
||||||
|
if (data.isok) {
|
||||||
|
handleLoginSuccess(data.response)
|
||||||
|
} else {
|
||||||
|
handleLoginFailure(data.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoginSuccess = (userData) => {
|
||||||
|
// 登录成功处理...
|
||||||
|
errorAttempts.value = 0
|
||||||
|
if (localStorage.getItem('ATTEMPTS_STORAGE_KEY')) {
|
||||||
|
localStorage.removeItem('ATTEMPTS_STORAGE_KEY')
|
||||||
|
}
|
||||||
|
if (localStorage.getItem('LOCK_STORAGE_KEY')) {
|
||||||
|
localStorage.removeItem('LOCK_STORAGE_KEY')
|
||||||
|
}
|
||||||
|
localStorage.removeItem('loginErrorAttempts')
|
||||||
|
// 存储token
|
||||||
|
localStorage.setItem('token', userData.accessToken)
|
||||||
|
localStorage.setItem('username', userData.username)
|
||||||
|
localStorage.setItem('login', true)
|
||||||
|
localStorage.setItem('uid', userData.id)
|
||||||
|
localStorage.setItem('roleId', userData.roleId)
|
||||||
|
localStorage.setItem('realname', userData.realname)
|
||||||
|
localStorage.setItem('position', userData.position)
|
||||||
|
localStorage.setItem('comId', userData.comId)
|
||||||
|
localStorage.setItem('avatar', userData.avatar)
|
||||||
|
|
||||||
|
// 记住账号和密码
|
||||||
|
if (form.value.remember) {
|
||||||
|
localStorage.setItem('rememberedUsername', form.value.username)
|
||||||
|
localStorage.setItem('rememberedPassword', form.value.password)
|
||||||
|
} else {
|
||||||
|
if (localStorage.getItem('rememberedUsername')) {
|
||||||
|
localStorage.removeItem('rememberedPassword')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElMessage.success(`登录成功,欢迎:${userData.username}!`)
|
||||||
|
router.push('/home')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoginFailure = (message) => {
|
||||||
|
ElMessage.error(message)
|
||||||
|
errorAttempts.value++
|
||||||
|
localStorage.setItem('loginErrorAttempts', errorAttempts.value)
|
||||||
|
|
||||||
|
if (errorAttempts.value >= 5) {
|
||||||
|
const lockUntil = Date.now() + 3600000
|
||||||
|
lockUntil.value = lockUntil
|
||||||
|
localStorage.setItem('loginLockUntil', lockUntil)
|
||||||
|
setupUnlockTimer(3600000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container,
|
||||||
|
.login-container {
|
||||||
|
flex: 1; /* 各占50% */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center; /* 水平居中 */
|
||||||
|
justify-content: center; /* 垂直居中 */
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 400px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.login-container {
|
||||||
|
background: var(--el-bg-color);*/ /* 保持与主题一致 */
|
||||||
|
/*padding: 30px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
margin: 2rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 基础样式保持桌面端布局 */
|
||||||
|
/*.login-container {
|
||||||
|
width: 400px;*/ /* 固定桌面端宽度 */
|
||||||
|
/*margin: 0 auto;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/* 外层容器样式 */
|
||||||
|
div[style*="display: flex"] {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 通用样式保持 */
|
||||||
|
.captcha-container {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-image {
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-tip {
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: -10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
div[style*="display: flex"] {
|
||||||
|
flex-direction: column; /* 改为垂直布局 */
|
||||||
|
padding: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片容器调整 */
|
||||||
|
div[style*="display: flex"] > div:first-child {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片样式调整 */
|
||||||
|
img[src*="logobig.svg"] {
|
||||||
|
width: 80% !important;
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录容器调整 */
|
||||||
|
/*.login-container {
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 30px auto 0;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: none;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/* 标题调整 */
|
||||||
|
/*.login-container h1 {
|
||||||
|
margin: 50px 0 !important;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/* 验证码输入容器 */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1542
src/pages/panelselection-bak/index-test.vue
Normal file
1542
src/pages/panelselection-bak/index-test.vue
Normal file
File diff suppressed because it is too large
Load Diff
6084
src/pages/panelselection/index.vue
Normal file
6084
src/pages/panelselection/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
20
src/pages/scopemanage/index.vue
Normal file
20
src/pages/scopemanage/index.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject, onMounted } from 'vue';
|
||||||
|
const $http = inject('$http')
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
299
src/pages/switchselection/index.vue
Normal file
299
src/pages/switchselection/index.vue
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<el-card class="control-panel">
|
||||||
|
<div class="controls">
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="画布宽度">
|
||||||
|
<el-input-number v-model="canvasWidth"
|
||||||
|
:min="100"
|
||||||
|
:max="2000"
|
||||||
|
:step="50" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="画布高度">
|
||||||
|
<el-input-number v-model="canvasHeight"
|
||||||
|
:min="100"
|
||||||
|
:max="2000"
|
||||||
|
:step="50" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="间距(px)">
|
||||||
|
<el-input-number v-model="svgSpacing"
|
||||||
|
:min="0"
|
||||||
|
:max="200"
|
||||||
|
:step="5" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button @click="importSVG" type="primary">
|
||||||
|
<el-icon><Upload /></el-icon>
|
||||||
|
导入SVG文件
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button @click="clearAll" type="danger">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
清空画布
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button @click="exportSVG" type="success">
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
导出SVG文件
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<div class="canvas-container">
|
||||||
|
<svg ref="svgCanvas"
|
||||||
|
:width="canvasWidth"
|
||||||
|
:height="canvasHeight"
|
||||||
|
class="svg-canvas"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="svg-container"></g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Upload, Download, Delete } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// 画布尺寸控制(默认800x600)
|
||||||
|
const canvasWidth = ref(800)
|
||||||
|
const canvasHeight = ref(550)
|
||||||
|
const svgSpacing = ref(10) // SVG之间的间距
|
||||||
|
|
||||||
|
// 引用SVG画布元素
|
||||||
|
const svgCanvas = ref(null)
|
||||||
|
|
||||||
|
// 存储导入的SVG数据
|
||||||
|
const importedSVGs = ref([])
|
||||||
|
|
||||||
|
// 更新SVG显示
|
||||||
|
const updateSVGDisplay = () => {
|
||||||
|
if (!svgCanvas.value) return
|
||||||
|
|
||||||
|
const container = svgCanvas.value.querySelector('#svg-container')
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
// 清空现有内容
|
||||||
|
container.innerHTML = ''
|
||||||
|
|
||||||
|
let currentX = 0
|
||||||
|
|
||||||
|
// 按顺序渲染所有SVG
|
||||||
|
importedSVGs.value.forEach(svgData => {
|
||||||
|
if (svgData.content) {
|
||||||
|
const wrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||||
|
wrapper.setAttribute('transform', `translate(${currentX},0)`)
|
||||||
|
wrapper.innerHTML = svgData.content
|
||||||
|
|
||||||
|
container.appendChild(wrapper)
|
||||||
|
|
||||||
|
// 更新下一个SVG的位置
|
||||||
|
currentX += svgData.width + svgSpacing.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 调整画布宽度以适应所有SVG
|
||||||
|
if (importedSVGs.value.length > 0) {
|
||||||
|
const totalWidth = currentX - svgSpacing.value
|
||||||
|
canvasWidth.value = Math.max(totalWidth, canvasWidth.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入SVG逻辑
|
||||||
|
const importSVG = () => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = '.svg'
|
||||||
|
input.multiple = true
|
||||||
|
input.style.display = 'none'
|
||||||
|
|
||||||
|
input.onchange = async (e) => {
|
||||||
|
const files = Array.from(e.target.files)
|
||||||
|
if (!files.length) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 处理所有选中的SVG文件
|
||||||
|
const newSVGs = await Promise.all(files.map(file =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const content = event.target.result
|
||||||
|
|
||||||
|
// 获取SVG尺寸
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.innerHTML = content
|
||||||
|
document.body.appendChild(div)
|
||||||
|
const svgElement = div.querySelector('svg')
|
||||||
|
let originalWidth = 100, originalHeight = 100
|
||||||
|
|
||||||
|
if (svgElement) {
|
||||||
|
originalWidth = svgElement.width.baseVal.value ||
|
||||||
|
svgElement.getBoundingClientRect().width || 100
|
||||||
|
originalHeight = svgElement.height.baseVal.value ||
|
||||||
|
svgElement.getBoundingClientRect().height || 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算新尺寸,固定高度为100px,宽度按比例缩放
|
||||||
|
const ratio = originalWidth / originalHeight || 1;
|
||||||
|
const height = 100;
|
||||||
|
const width = ratio * height;
|
||||||
|
|
||||||
|
document.body.removeChild(div)
|
||||||
|
|
||||||
|
// 创建一个独特的ID避免冲突
|
||||||
|
const uniqueId = 'svg-' + Math.random().toString(36).substr(2, 9)
|
||||||
|
const contentWithUniqueIds = content.replace(/id="([^"]+)"/g, `id="${uniqueId}-$1"`)
|
||||||
|
.replace(/url$#([^)]+)$/g, `url(#${uniqueId}-$1)`)
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
content: contentWithUniqueIds,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
name: file.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
))
|
||||||
|
|
||||||
|
// 添加到已导入列表
|
||||||
|
importedSVGs.value = [...importedSVGs.value, ...newSVGs]
|
||||||
|
updateSVGDisplay()
|
||||||
|
|
||||||
|
ElMessage.success(`成功导入${files.length}个SVG文件`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入SVG失败:', error)
|
||||||
|
ElMessage.error('导入SVG失败: ' + error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(input)
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空所有SVG
|
||||||
|
const clearAll = () => {
|
||||||
|
importedSVGs.value = []
|
||||||
|
updateSVGDisplay()
|
||||||
|
ElMessage.success('画布已清空')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当间距变化时更新显示
|
||||||
|
watch(svgSpacing, updateSVGDisplay)
|
||||||
|
|
||||||
|
// SVG导出功能
|
||||||
|
const exportSVG = () => {
|
||||||
|
try {
|
||||||
|
if (importedSVGs.value.length === 0) {
|
||||||
|
ElMessage.warning('没有可导出的SVG内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算导出尺寸
|
||||||
|
const totalWidth = importedSVGs.value.reduce(
|
||||||
|
(sum, svg, index) => sum + svg.width + (index === 0 ? 0 : svgSpacing.value),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxHeight = Math.max(...importedSVGs.value.map(svg => svg.height))
|
||||||
|
|
||||||
|
// 获取所有SVG的组合内容
|
||||||
|
const svgHeader = `<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="${totalWidth}"
|
||||||
|
height="${maxHeight}"
|
||||||
|
viewBox="0 0 ${totalWidth} ${maxHeight}">`
|
||||||
|
|
||||||
|
let currentX = 0
|
||||||
|
let combinedContent = ''
|
||||||
|
|
||||||
|
importedSVGs.value.forEach(svg => {
|
||||||
|
combinedContent += `<g transform="translate(${currentX},0)">${svg.content}</g>`
|
||||||
|
currentX += svg.width + svgSpacing.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const fullSVG = `${svgHeader}${combinedContent}</svg>`
|
||||||
|
|
||||||
|
// 创建Blob对象
|
||||||
|
const blob = new Blob([fullSVG], { type: 'image/svg+xml;charset=utf-8' })
|
||||||
|
|
||||||
|
// 创建下载链接
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = `combined-svg-${new Date().getTime()}.svg`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
|
||||||
|
ElMessage.success('SVG导出成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出SVG失败:', error)
|
||||||
|
ElMessage.error('导出SVG失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
ElMessage.success('SVG画布已初始化')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-container {
|
||||||
|
flex: 1;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-canvas {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
background-image: linear-gradient(45deg, #f5f5f5 25%, transparent 25%), linear-gradient(-45deg, #f5f5f5 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #f5f5f5 75%), linear-gradient(-45deg, transparent 75%, #f5f5f5 75%);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imported-svg {
|
||||||
|
cursor: move;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
378
src/pages/user/index.vue
Normal file
378
src/pages/user/index.vue
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-center-container">
|
||||||
|
<!-- 头部操作按钮 -->
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button type="primary"
|
||||||
|
:icon="Edit"
|
||||||
|
@click="enableEdit"
|
||||||
|
v-show="!isEdit">
|
||||||
|
编辑信息
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success"
|
||||||
|
:icon="UploadFilled"
|
||||||
|
@click="submitForm"
|
||||||
|
v-show="isEdit && formModified">
|
||||||
|
保存修改
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户信息表单 -->
|
||||||
|
<el-form :model="userValue"
|
||||||
|
:rules="rules"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="100px"
|
||||||
|
label-position="right"
|
||||||
|
class="user-form">
|
||||||
|
<el-form-item label="登录名" prop="username">
|
||||||
|
<el-input v-model="userValue.username"
|
||||||
|
disabled="false"
|
||||||
|
placeholder="请输入用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="真实姓名" prop="realname">
|
||||||
|
<el-input v-model="userValue.realname"
|
||||||
|
:disabled="!isEdit"
|
||||||
|
placeholder="请输入真实姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- <el-form-item label="所属公司" prop="comId">
|
||||||
|
<el-select v-model="userValue.comId"
|
||||||
|
:disabled="!isEdit"
|
||||||
|
placeholder="请选择公司">
|
||||||
|
<el-option v-for="company in companies"
|
||||||
|
:key="company.id"
|
||||||
|
:label="company.nameCn"
|
||||||
|
:value="company.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>-->
|
||||||
|
|
||||||
|
<!-- <el-form-item label="职位" prop="position">
|
||||||
|
<el-select v-model="userValue.position"
|
||||||
|
placeholder="请选择用户类型"
|
||||||
|
:disabled="!isEdit">
|
||||||
|
<el-option v-for="item in positionList"
|
||||||
|
:key="item"
|
||||||
|
:label="item"
|
||||||
|
:value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>-->
|
||||||
|
|
||||||
|
<el-form-item label="手机号" prop="mobile">
|
||||||
|
<el-input v-model="userValue.mobile"
|
||||||
|
:disabled="!isEdit"
|
||||||
|
placeholder="请输入手机号" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="userValue.email"
|
||||||
|
:disabled="!isEdit"
|
||||||
|
placeholder="请输入邮箱" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="微信号" prop="weixin">
|
||||||
|
<el-input v-model="userValue.weixin"
|
||||||
|
:disabled="!isEdit"
|
||||||
|
placeholder="请输入微信号" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 头像上传组件 -->
|
||||||
|
<el-form-item label="头像" prop="avatar">
|
||||||
|
<el-upload class="avatar-uploader"
|
||||||
|
:disabled="!isEdit"
|
||||||
|
drag
|
||||||
|
accept="image/*"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-change="handleAvatarChange"
|
||||||
|
ref="avatarUploadRef">
|
||||||
|
<div v-if="userValue.avatar" class="avatar-preview">
|
||||||
|
<img :src="selectedFile ? avatarPreview : config.httpAds + userValue.avatar"
|
||||||
|
class="avatar-image" />
|
||||||
|
<div v-show="isEdit" class="avatar-mask">
|
||||||
|
<el-icon :size="23"><Edit /></el-icon>
|
||||||
|
<div>点击更换头像</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
拖拽图片到此 或 <em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
支持JPG/PNG,大小不超过5MB
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, inject, watch } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { Plus, Delete, Edit, UploadFilled } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
const $http = inject('$http');
|
||||||
|
const ajaxfile = inject('ajaxfile');
|
||||||
|
const config = inject('config');
|
||||||
|
|
||||||
|
// 初始状态定义
|
||||||
|
const isEdit = ref(true);
|
||||||
|
const formModified = ref(false);
|
||||||
|
const formRef = ref(null);
|
||||||
|
const avatarUploadRef = ref(null);
|
||||||
|
const selectedFile = ref(null);
|
||||||
|
const avatarPreview = ref('');
|
||||||
|
const loading = ref(false);
|
||||||
|
const companies = ref([]);
|
||||||
|
const userValue = reactive({
|
||||||
|
id: null,
|
||||||
|
username: "",
|
||||||
|
realname: "",
|
||||||
|
comId: null,
|
||||||
|
company: "",
|
||||||
|
password: "",
|
||||||
|
roleId: null,
|
||||||
|
position: "",
|
||||||
|
weixin: "",
|
||||||
|
email: "",
|
||||||
|
mobile: "",
|
||||||
|
avatar: ""
|
||||||
|
});
|
||||||
|
// 监听表单变化
|
||||||
|
watch(userValue, () => {
|
||||||
|
formModified.value = true;
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// 启用编辑
|
||||||
|
const enableEdit = () => {
|
||||||
|
isEdit.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 头像处理
|
||||||
|
const handleAvatarChange = (file) => {
|
||||||
|
selectedFile.value = file.raw;
|
||||||
|
avatarPreview.value = URL.createObjectURL(file.raw);
|
||||||
|
};
|
||||||
|
// 验证规则
|
||||||
|
const rules = reactive({
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||||
|
{ min: 3, max: 20, message: '长度在3到20个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
mobile: [
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
const getUser = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const rs = await $http.post('Company/GetComInfo', {
|
||||||
|
IsAll: true,
|
||||||
|
ID: 0,
|
||||||
|
});
|
||||||
|
if (rs.data.isok) {
|
||||||
|
companies.value = rs.data.response;
|
||||||
|
|
||||||
|
const rs1 = await $http.post('Users/GetUserInfo', {
|
||||||
|
IsAll: false,
|
||||||
|
ID: localStorage.getItem("uid"),
|
||||||
|
});
|
||||||
|
if (rs1.data.isok) {
|
||||||
|
// 1. 创建公司映射表(提高查找效率)
|
||||||
|
const companyMap = companies.value.reduce((map, company) => {
|
||||||
|
map[company.id] = company.nameCn;
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// 2. 处理用户数据
|
||||||
|
Object.assign(userValue, rs1.data.response);
|
||||||
|
userValue.company = companyMap[userValue.comId];
|
||||||
|
|
||||||
|
console.log(userValue)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(rs1.data.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取用户列表失败');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 表单提交
|
||||||
|
const submitForm = async () => {
|
||||||
|
try {
|
||||||
|
// 表单验证
|
||||||
|
await formRef.value.validate();
|
||||||
|
|
||||||
|
// 头像上传逻辑
|
||||||
|
/* if (selectedFile.value) {
|
||||||
|
let filedata = {
|
||||||
|
File: selectedFile.value,
|
||||||
|
Folder: "face"
|
||||||
|
}
|
||||||
|
const uploadRes = await ajaxfile(filedata);
|
||||||
|
if (uploadRes?.fileName) {
|
||||||
|
userValue.avatar = uploadRes.fileName;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
if (selectedFile.value) {
|
||||||
|
const uploadRes = await ajaxfile(filedata);
|
||||||
|
if (uploadRes?.fileName) {
|
||||||
|
userValue.avatar = uploadRes.fileName; // 根据实际接口返回结构调整
|
||||||
|
} else {
|
||||||
|
ElMessage.error('头像上传失败: ' + uploadRes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userValue.avatar = userValue.avatar.substring(userValue.avatar.lastIndexOf('/') + 1);
|
||||||
|
}
|
||||||
|
// 更新用户信息
|
||||||
|
const rs = await $http.post('Users/EditUser', userValue);
|
||||||
|
if (rs.data.isok) {
|
||||||
|
ElMessage.success('信息更新成功');
|
||||||
|
isEdit.value = false;
|
||||||
|
formModified.value = false;
|
||||||
|
selectedFile.value = null;
|
||||||
|
avatarUploadRef.value.clearFiles();
|
||||||
|
await getUser(); // 刷新数据
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('保存失败: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 获取身份
|
||||||
|
const positionList = ref([]);
|
||||||
|
const getShengfen = () => {
|
||||||
|
try {
|
||||||
|
const rs = $http.post('ConfigPY/GetSingleValue', {
|
||||||
|
VarName: "用户类型",
|
||||||
|
}).then(rs => {
|
||||||
|
//console.log(JSON.parse(rs.data.response));
|
||||||
|
positionList.value = JSON.parse(rs.data.response)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
getUser()
|
||||||
|
getShengfen()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-center-container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 444px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽区域样式 */
|
||||||
|
.avatar-uploader :deep(.el-upload-dragger) {
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader:hover :deep(.el-upload-dragger) {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头像预览样式 */
|
||||||
|
.avatar-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 5%;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-mask:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标和文字样式 */
|
||||||
|
.el-icon--upload {
|
||||||
|
font-size: 36px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__text em {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
581
src/pages/usermanage/index.vue
Normal file
581
src/pages/usermanage/index.vue
Normal file
@@ -0,0 +1,581 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-management">
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<!-- <div class="header">
|
||||||
|
<el-button type="primary" @click="handleAdd" :icon="Plus">添加用户</el-button>
|
||||||
|
</div>-->
|
||||||
|
<div class="header">
|
||||||
|
<span style="margin-left: 20px;">
|
||||||
|
用户类型:
|
||||||
|
<el-switch v-model="userType"
|
||||||
|
active-value="wy"
|
||||||
|
inactive-value="platform"
|
||||||
|
active-text="威云用户"
|
||||||
|
inactive-text="平台用户"
|
||||||
|
style="--el-switch-on-color: #409eff; --el-switch-off-color: #409eff" />
|
||||||
|
</span>
|
||||||
|
<span> </span>
|
||||||
|
<el-button type="primary" @click="handleAdd" :icon="Plus" v-if="userType === 'platform'">添加用户</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户表格 -->
|
||||||
|
<el-table :data="userType == 'platform' ? allUsers : wyUsers" border style="width: 100%" v-loading="loading" empty-text="暂无数据">
|
||||||
|
<el-table-column label="操作" :width="userType == 'platform' ? 105 : 65">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="info" :icon="Edit" circle plain @click="handleEdit(row)" />
|
||||||
|
<!--<el-button type="danger" :icon="Delete" circle plain @click="handleDelete(row)" />-->
|
||||||
|
<el-button type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
circle plain
|
||||||
|
@click="handleDelete(row)"
|
||||||
|
v-show="row.id != 1 && row.id != 2"
|
||||||
|
v-if="userType == 'platform'" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="username" label="用户名" width="130" sortable show-overflow-tooltip />
|
||||||
|
<el-table-column prop="realname" label="真实姓名" width="90" />
|
||||||
|
<el-table-column prop="company" label="所属公司" width="90" />
|
||||||
|
<el-table-column prop="position" label="用户类型" width="135" />
|
||||||
|
<el-table-column prop="mobile" label="手机号" width="128" />
|
||||||
|
<el-table-column prop="email" label="邮箱" width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="weixin" label="微信号" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="头像" width="65">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image v-if="row.avatar"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
hide-on-click-modal="true"
|
||||||
|
:src="config.httpAds + row.avatar"
|
||||||
|
:preview-src-list="[ config.httpAds + row.avatar]"
|
||||||
|
fit="cover" />
|
||||||
|
<span v-else>无</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 添加/编辑对话框 -->
|
||||||
|
<el-dialog v-model="dialogVisible"
|
||||||
|
:title="isEdit ? '编辑用户' : '添加用户'"
|
||||||
|
width="600px">
|
||||||
|
<el-form :model="form"
|
||||||
|
:rules="rules"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="100px"
|
||||||
|
label-position="right">
|
||||||
|
<el-form-item label="登录名" prop="username">
|
||||||
|
<el-input v-model="form.username"
|
||||||
|
:disabled="isEdit"
|
||||||
|
placeholder="用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="登录密码" prop="password" v-if="!isEdit">
|
||||||
|
<el-input v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="密码" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="修改密码" prop="password" v-if="isEdit && userType !== 'wy'">
|
||||||
|
<el-input v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="密码(为空则不修改)" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="真实姓名" prop="realname">
|
||||||
|
<el-input v-model="form.realname" placeholder="真实姓名" :disabled="userType == 'wy'" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="公司" prop="comId">
|
||||||
|
<el-select v-model="form.comId" placeholder="公司">
|
||||||
|
<el-option v-for="item in companies"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.nameCn"
|
||||||
|
:value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="用户类型" prop="position">
|
||||||
|
<el-select v-model="form.position" placeholder="用户类型">
|
||||||
|
<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="form.mobile" placeholder="手机号" :disabled="userType == 'wy'" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="form.email" placeholder="邮箱" :disabled="userType == 'wy'" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="微信号" prop="weixin">
|
||||||
|
<el-input v-model="form.weixin" placeholder="微信号" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="头像" prop="avatar" v-if="userType !== 'wy'">
|
||||||
|
<el-upload class="avatar-uploader"
|
||||||
|
drag
|
||||||
|
accept="image/*"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-change="handleAvatarChange"
|
||||||
|
ref="avatarUploadRef">
|
||||||
|
<!-- 头像预览区域 -->
|
||||||
|
<div v-if="form.avatar" class="avatar-preview">
|
||||||
|
<img v-if="!selectedFile" :src="config.httpAds + form.avatar" class="avatar-image" />
|
||||||
|
<img v-else :src="form.avatar" class="avatar-image" />
|
||||||
|
<div 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-item label="头像" prop="avatar" v-if="userType === 'wy'">
|
||||||
|
<img :src="form.avatar" class="avatar-image" />
|
||||||
|
</el-form-item>-->
|
||||||
|
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, inject, watch } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { Plus, Delete, Edit, UploadFilled } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
const $http = inject('$http');
|
||||||
|
const ajaxfile = inject('ajaxfile');
|
||||||
|
const config = inject('config');
|
||||||
|
|
||||||
|
// 数据列表
|
||||||
|
const allUsers = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const companies = ref([]);
|
||||||
|
|
||||||
|
const nowUser = localStorage.getItem('username');
|
||||||
|
// 对话框相关
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const currentUserId = ref(null);
|
||||||
|
const formRef = ref(null);
|
||||||
|
|
||||||
|
const userType = ref('platform'); // 新增用户类型状态
|
||||||
|
const wyUsers = ref([]); // 新增威云用户数据
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const defaultForm = {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
realname: '',
|
||||||
|
mobile: '',
|
||||||
|
email: '',
|
||||||
|
weixin: '',
|
||||||
|
avatar: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = reactive({ ...defaultForm });
|
||||||
|
|
||||||
|
// 验证规则
|
||||||
|
const rules = reactive({
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '用户名', trigger: 'blur' },
|
||||||
|
{ min: 3, max: 20, message: '长度在3到20个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ message: '密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
mobile: [
|
||||||
|
{ required: true, pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
comId: [
|
||||||
|
{ required: true, message: '公司不可为空', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
position: [
|
||||||
|
{ required: true, message: '用户类型不可为空', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
realname: userType.value === 'wy' ? [] : [
|
||||||
|
{ required: true, message: '真实姓名', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
/*// 获取用户列表
|
||||||
|
const getUsers = 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: true,
|
||||||
|
ID: 0,
|
||||||
|
});
|
||||||
|
//console.log(rs1.data.response);
|
||||||
|
if (rs1.data.isok) {
|
||||||
|
// 1. 创建公司映射表(提高查找效率)
|
||||||
|
const companyMap = companies.value.reduce((map, company) => {
|
||||||
|
map[company.id] = company.nameCn;
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// 2. 处理用户数据
|
||||||
|
allUsers.value = rs1.data.response.reduce((acc, user) => {
|
||||||
|
if (user.username !== "WYSERVER") {
|
||||||
|
acc.push({
|
||||||
|
...user,
|
||||||
|
company: companyMap[user.comId] || '未知公司' // 处理找不到公司的情况
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
// 3. 排序(在映射之后)
|
||||||
|
allUsers.value.sort((a, b) => a.username.localeCompare(b.username));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ElMessage.error(rs1.data.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取用户列表失败');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};*/
|
||||||
|
|
||||||
|
// 获取身份
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增代码:上传组件引用和文件状态
|
||||||
|
const avatarUploadRef = ref(null);
|
||||||
|
const selectedFile = ref(null);
|
||||||
|
|
||||||
|
// 修改头像处理逻辑
|
||||||
|
const handleAvatarChange = (uploadFile) => {
|
||||||
|
const file = uploadFile.raw;
|
||||||
|
const isImage = file.type.startsWith('image/');
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < 5;
|
||||||
|
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error('只能上传图片文件!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isLt2M) {
|
||||||
|
ElMessage.error('图片大小不能超过5MB!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览本地文件
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
form.avatar = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
selectedFile.value = file;
|
||||||
|
return false; // 阻止自动上传
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改后的提交逻辑
|
||||||
|
const submitForm = async () => {
|
||||||
|
|
||||||
|
await formRef.value.validate();
|
||||||
|
try {
|
||||||
|
// 如果有新选择的头像文件,先上传
|
||||||
|
let filedata = {
|
||||||
|
File: selectedFile.value,
|
||||||
|
Folder: "face",
|
||||||
|
}
|
||||||
|
if (selectedFile.value) {
|
||||||
|
const uploadRes = await ajaxfile(filedata);
|
||||||
|
if (uploadRes?.fileName) {
|
||||||
|
form.avatar = uploadRes.fileName; // 根据实际接口返回结构调整
|
||||||
|
} else {
|
||||||
|
ElMessage.error('头像上传失败: ' + uploadRes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (form.avatar) {
|
||||||
|
form.avatar = form.avatar.substring(form.avatar.lastIndexOf('/') + 1);
|
||||||
|
}
|
||||||
|
var apiPrefix = '';
|
||||||
|
|
||||||
|
if (isEdit.value) {
|
||||||
|
apiPrefix = userType.value === 'wy' ? 'WyUsers/EditMyUser' : 'Users/EditUser';
|
||||||
|
} else {
|
||||||
|
apiPrefix = userType.value === 'wy' ? 'WyUsers/AddMyUser' : 'Users/AddUser';
|
||||||
|
}
|
||||||
|
// 提交用户数据
|
||||||
|
const api = `${apiPrefix}`;
|
||||||
|
// 提交用户数据
|
||||||
|
const data = isEdit.value ? { ...form, id: currentUserId.value } : form;
|
||||||
|
if (isEdit.value) data.id = currentUserId.value;
|
||||||
|
const rs = await $http.post(api, data);
|
||||||
|
if (rs.data.isok) {
|
||||||
|
ElMessage.success(isEdit.value ? '修改成功' : '添加成功');
|
||||||
|
// 重置状态
|
||||||
|
selectedFile.value = null;
|
||||||
|
avatarUploadRef.value?.clearFiles();
|
||||||
|
await getUsers();
|
||||||
|
dialogVisible.value = false;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(rs.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('操作失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改打开对话框逻辑
|
||||||
|
const handleAdd = () => {
|
||||||
|
Object.assign(form, defaultForm);
|
||||||
|
isEdit.value = false;
|
||||||
|
selectedFile.value = null;
|
||||||
|
avatarUploadRef.value?.clearFiles();
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
Object.assign(form, row);
|
||||||
|
isEdit.value = true;
|
||||||
|
currentUserId.value = row.id;
|
||||||
|
selectedFile.value = null;
|
||||||
|
avatarUploadRef.value?.clearFiles();
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
const handleDelete = (row) => {
|
||||||
|
ElMessageBox.confirm('确定删除该用户吗?', '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
const rs = await $http.post('Users/DelUser', { Id: row.id });
|
||||||
|
if (rs.data.isok) {
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
await getUsers();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(rs.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除失败');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}).catch(() => { });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 头像上传处理
|
||||||
|
const handleAvatarSuccess = (res) => {
|
||||||
|
form.avatar = res.data.url; // 根据实际接口返回调整
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeAvatarUpload = (file) => {
|
||||||
|
const isImage = /^image\/(jpeg|png|gif)$/.test(file.type);
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||||
|
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error('只能上传图片文件!');
|
||||||
|
}
|
||||||
|
if (!isLt2M) {
|
||||||
|
ElMessage.error('图片大小不能超过2MB!');
|
||||||
|
}
|
||||||
|
return isImage && isLt2M;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
watch(userType, (newVal) => {
|
||||||
|
getUsers();
|
||||||
|
});
|
||||||
|
// 修改后的获取用户列表方法
|
||||||
|
const getUsers = 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 apiPrefix = userType.value === 'wy' ? 'WyUsers' : 'Users';
|
||||||
|
const rs1 = await $http.post(`${apiPrefix}/GetUserInfo`, {
|
||||||
|
IsAll: true,
|
||||||
|
ID: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rs1.data.isok) {
|
||||||
|
const processData = (data) => {
|
||||||
|
const companyMap = companies.value.reduce((map, company) => {
|
||||||
|
map[company.id] = company.nameCn;
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return data.reduce((acc, user) => {
|
||||||
|
if (user.username !== "WYSERVER") {
|
||||||
|
acc.push({
|
||||||
|
...user,
|
||||||
|
company: companyMap[user.comId] || '未知公司'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userType.value === 'wy') {
|
||||||
|
wyUsers.value = processData(rs1.data.response);
|
||||||
|
} else {
|
||||||
|
allUsers.value = processData(rs1.data.response);
|
||||||
|
allUsers.value.sort((a, b) => a.username.localeCompare(b.username));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取用户列表失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getUsers();
|
||||||
|
getShengfen()
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-management {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽区域样式 */
|
||||||
|
.avatar-uploader :deep(.el-upload-dragger) {
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader:hover :deep(.el-upload-dragger) {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头像预览样式 */
|
||||||
|
.avatar-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 5%;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-mask:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标和文字样式 */
|
||||||
|
.el-icon--upload {
|
||||||
|
font-size: 36px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__text em {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
115
src/router/index.js
Normal file
115
src/router/index.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
import Home from '../pages/home/index.vue';
|
||||||
|
import Login from '../pages/login/index.vue';
|
||||||
|
import UserManage from '../pages/usermanage/index.vue';
|
||||||
|
import ComManage from '../pages/commanage/index.vue';
|
||||||
|
import ScopeManage from '../pages/scopemanage/index.vue';
|
||||||
|
import User from '../pages/user/index.vue';
|
||||||
|
import SwitchSelection from '../pages/switchselection/index.vue';
|
||||||
|
import PanelSelection from '../pages/panelselection/index.vue';
|
||||||
|
import Log from '../pages/log/index.vue';
|
||||||
|
import DicManage from '../pages/dicmanage/index.vue';
|
||||||
|
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
// 动态重定向到登录页或主页
|
||||||
|
redirect: () => {
|
||||||
|
const isAuthenticated = localStorage.getItem('token');
|
||||||
|
return isAuthenticated ? '/home' : '/login';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: '登录',
|
||||||
|
component: Login
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/home',
|
||||||
|
name: '主页',
|
||||||
|
component: Home,
|
||||||
|
meta: { requiresAuth: true } // 需要认证的路由
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/log',
|
||||||
|
name: '日志中心',
|
||||||
|
component: Log,
|
||||||
|
meta: { requiresAuth: true } // 需要认证的路由
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/usermanage',
|
||||||
|
name: '用户管理',
|
||||||
|
component: UserManage,
|
||||||
|
meta: { requiresAuth: true } // 需要认证的路由
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/commanage',
|
||||||
|
name: '组织管理',
|
||||||
|
component: ComManage,
|
||||||
|
meta: { requiresAuth: true } // 需要认证的路由
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/scopemanage',
|
||||||
|
name: '权限管理',
|
||||||
|
component: ScopeManage,
|
||||||
|
meta: { requiresAuth: true } // 需要认证的路由
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/dicmanage',
|
||||||
|
name: '字典管理',
|
||||||
|
component: DicManage,
|
||||||
|
meta: { requiresAuth: true } // 需要认证的路由
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/user',
|
||||||
|
name: '个人中心',
|
||||||
|
component: User,
|
||||||
|
meta: { requiresAuth: true } // 需要认证的路由
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/switchselection',
|
||||||
|
name: '开关选型',
|
||||||
|
component: SwitchSelection,
|
||||||
|
meta: { requiresAuth: true } // 需要认证的路由
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/panelselection',
|
||||||
|
name: '开关选型',
|
||||||
|
component: PanelSelection,
|
||||||
|
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) => {
|
||||||
|
const isAuthenticated = localStorage.getItem('token') // 用localStorage存储token
|
||||||
|
const username = localStorage.getItem('username') // 获取用户名
|
||||||
|
|
||||||
|
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;
|
||||||
10
vite.config.js
Normal file
10
vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import plugin from '@vitejs/plugin-vue';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [plugin()],
|
||||||
|
server: {
|
||||||
|
port: 63615,
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user