first commit

This commit is contained in:
Tao-826 2025-07-07 14:14:38 +08:00
commit 421e3346bf
73 changed files with 8981 additions and 0 deletions

6
.env.Staging Normal file
View File

@ -0,0 +1,6 @@
NODE_ENV = Staging
VITE_NAME='Staging'
VITE_BASE_URL='https://106.52.199.114:8000'
VITE_FiLE_SYS='https://106.52.199.114:8000'
VITE_HOME_URL='https://106.52.199.114:8000'
VITE_CLIENT_ID='55DFB4F6-9FF1-4E26-8766-C9F1B1E2A912'

9
.env.development Normal file
View File

@ -0,0 +1,9 @@
NODE_ENV = development
VITE_NAME='开发环境'
VITE_BASE_URL='http://localhost:19903'
VITE_BASE_URL_SYSTEM='http://localhost:19901'
VITE_HOME_URL='http://localhost:3000'
VITE_CLIENT_ID="66DFB4F6-9FF1-4E26-8766-C9F1B1E2A984"
VITE_FiLE_SYS='http://localhost:19901'
VITE_FiLE_IMG='http://localhost:19903'
VITE_FiLE='http://localhost:19903'

5
.env.production Normal file
View File

@ -0,0 +1,5 @@
NODE_ENV = production
VITE_NAME='生产环境'
VITE_BASE_URL=''
VITE_HOME_URL=''
VITE_CLIENT_ID="66DFB4F6-9FF1-4E26-8766-C9F1B1E2A984"

10
.env.test Normal file
View File

@ -0,0 +1,10 @@
NODE_ENV = test
VITE_NAME='测试环境'
VITE_BASE_URL=''
VITE_BASE_URL2='https://192.168.1.100:8000'
VITE_HOME_URL='https://192.168.1.100:8000'
VITE_BASE_URL_SYSTEM='http://192.168.1.100:8001'
VITE_CLIENT_ID="66DFB4F6-9FF1-4E26-8766-C9F1B1E2A984"
VITE_FiLE='https://192.168.1.100:19902'
VITE_FiLE_SYS='https://192.168.1.100:19901'
VITE_FiLE_IMG='https://192.168.1.100:19903'

23
.eslintrc.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:vue/essential"
],
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"vue"
],
"rules": {
"vue/no-v-model-argument": "off",
"vue/multi-word-component-names": "off",
"no-var": "error",
}
};

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# 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?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

96
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,96 @@
// Jenkinsfile for Project A
pipeline {
agent any
parameters {
// 项目特定的默认值
string(name: 'GIT_REPO_URL', defaultValue: 'https://your-git-server.com/project-a.git', description: 'Git仓库URL')
string(name: 'GIT_BRANCH', defaultValue: 'main', description: '要拉取的Git分支')
credentials(name: 'GIT_CREDENTIALS_ID', defaultValue: 'your-git-credentials-id', description: 'Git凭证ID', required: false)
string(name: 'BUILD_COMMAND', defaultValue: 'mvn clean package -DskipTests', description: '项目A打包命令') // project-a 的打包命令
string(name: 'DOCKER_REGISTRY_URL', defaultValue: 'your-docker-registry.com', description: 'Docker镜像仓库URL')
string(name: 'DOCKER_IMAGE_NAME', defaultValue: 'project-a-app', description: 'Docker镜像名称') // project-a 的镜像名
string(name: 'IMAGE_BASE_TAG', defaultValue: '1.0', description: '基础镜像标签')
credentials(name: 'DOCKER_CREDENTIALS_ID', defaultValue: 'your-docker-registry-credentials-id', description: 'Docker镜像仓库凭证ID', required: true)
booleanParam(name: 'PUSH_LATEST_TAG', defaultValue: true, description: '是否同时推送 latest 标签?')
}
environment {
FULL_IMAGE_NAME = "${params.DOCKER_REGISTRY_URL}/${params.DOCKER_IMAGE_NAME}"
IMAGE_TAG = "" // 将在 Checkout 后动态设置
}
// tools { ... } // 如果需要
stages {
stage('1. Checkout Code') {
steps {
echo "拉取代码从 ${params.GIT_REPO_URL}, 分支: ${params.GIT_BRANCH}"
cleanWs()
checkout([
$class: 'GitSCM',
branches: [[name: params.GIT_BRANCH]],
userRemoteConfigs: [[
url: params.GIT_REPO_URL,
credentialsId: params.GIT_CREDENTIALS_ID
]],
extensions: [
[$class: 'CloneOption', shallow: true, noTags: true, depth: 1, timeout: 20],
[$class: 'PruneStaleBranch']
]
])
script {
def shortCommit = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
env.IMAGE_TAG = "${params.IMAGE_BASE_TAG}-${BUILD_NUMBER}-${shortCommit}"
echo "生成的镜像TAG: ${env.IMAGE_TAG}"
}
}
}
stage('2. Build and Package Application') {
steps {
echo "开始打包应用: ${params.BUILD_COMMAND}"
sh "${params.BUILD_COMMAND}"
echo "应用打包完成."
}
}
stage('3. Build Docker Image') {
steps {
echo "开始构建Docker镜像: ${env.FULL_IMAGE_NAME}:${env.IMAGE_TAG}"
script {
docker.build("${env.FULL_IMAGE_NAME}:${env.IMAGE_TAG}", "-f Dockerfile .") // 假设 Dockerfile 在根目录
}
echo "Docker镜像构建完成: ${env.FULL_IMAGE_NAME}:${env.IMAGE_TAG}"
}
}
stage('4. Push Docker Image') {
steps {
echo "开始推送Docker镜像到 ${params.DOCKER_REGISTRY_URL}"
script {
docker.withRegistry(params.DOCKER_REGISTRY_URL, params.DOCKER_CREDENTIALS_ID) {
docker.image("${env.FULL_IMAGE_NAME}:${env.IMAGE_TAG}").push()
echo "镜像 ${env.FULL_IMAGE_NAME}:${env.IMAGE_TAG} 推送成功."
if (params.PUSH_LATEST_TAG) {
docker.image("${env.FULL_IMAGE_NAME}:${env.IMAGE_TAG}").push('latest')
echo "镜像 ${env.FULL_IMAGE_NAME}:latest 推送成功."
}
}
}
}
}
}
post {
always {
echo 'Pipeline 结束.'
cleanWs()
}
// success { ... }
// failure { ... }
}
}

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# vue-project
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) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
yarn install
```
### Compile and Hot-Reload for Development
```sh
yarn dev
```
### Compile and Minify for Production
```sh
yarn build
```

12
eslint.config.mjs Normal file
View File

@ -0,0 +1,12 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import pluginVue from "eslint-plugin-vue";
/** @type {import('eslint').Linter.Config[]} */
export default [
{files: ["**/*.{js,mjs,cjs,vue}"]},
{languageOptions: { globals: {...globals.browser, ...globals.node} }},
pluginJs.configs.recommended,
...pluginVue.configs["flat/essential"],
];

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>集成化平台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "vue-project",
"version": "0.0.0",
"scripts": {
"dev": "vite --mode development",
"build": "vite build --mode production",
"test": "vite build --mode test",
"build:env": "vite build --mode development",
"preview": "vite preview --mode production"
},
"dependencies": {
"axios": "^1.4.0",
"crypto-js": "^4.2.0",
"echarts": "^5.4.2",
"element-plus": "^2.9.1",
"jwt-decode": "^3.1.2",
"oidc-client": "^1.11.5",
"pinia": "^2.0.14",
"pinia-plugin-persist": "^1.0.0",
"print-js": "^1.6.0",
"qrcode.vue": "^3.6.0",
"vue": "^3.2.37",
"vue-router": "^4.1.2",
"xlsx": "^0.18.5",
"xlsx-style": "^0.8.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.3",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/cli-plugin-eslint": "^5.0.8",
"autoprefixer": "^10.4.20",
"eslint": "^7.32.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-vue": "^9.32.0",
"postcss-pxtorem": "^6.1.0",
"sass": "^1.32.13",
"vite": "^2.9.14",
"vite-plugin-svg-icons": "^2.0.1"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

46
src/App.vue Normal file
View File

@ -0,0 +1,46 @@
<template>
<router-view v-if="isRoutesReady"></router-view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { usePermissionStore } from '@/stores/permission'
import { useRouter } from 'vue-router'
import { GetDictionaryGetPage } from '@/api/system/index'
const isRoutesReady = ref(false)
const permissionStore = usePermissionStore()
const router = useRouter()
const getDictionaryGetPage = () => {
const params = {
LambdaExp: 'IsDeleted==false&&dicName.Contains("")',
SelectSorts: '[{"FieldName":"CreateTime","IsAsc":false}]',
PageNum: 1,
PageSize: 10000
}
GetDictionaryGetPage(params).then(res => {
if (res.StatusCode == 200) {
sessionStorage.setItem('dictionarys', JSON.stringify(res.Data))
}
})
}
onMounted(async () => {
getDictionaryGetPage()
try {
if (!permissionStore.isRoutesGenerated) {
sessionStorage.removeItem('tabs')
await permissionStore.generateRoutes()
}
//
if (router.currentRoute.value.matched.length === 0 &&
router.currentRoute.value.path !== '/404') {
router.replace('/home')
}
isRoutesReady.value = true
} catch (error) {
console.error('Failed to generate routes:', error)
router.replace('/404')
}
})
</script>

108
src/api/common/index.js Normal file
View File

@ -0,0 +1,108 @@
import service from '@/utils/request'
export function UploadAttachment(data, config) {
return service({
url: '/api/lmg/Common/UploadAttachment',
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data',
'Custom-Header': 'CustomValue',
},
...config
})
}
export function UploadMultipleAttachments(data, config) {
return service({
url: '/api/lmg/Common/UploadMultipleAttachments',
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data',
'Custom-Header': 'CustomValue',
},
...config
})
}
export function GetAttachmentById(params) {
return service({
url: '/api/lmg/Common/GetAttachmentById',
method: 'get',
params
})
}
export function DeleteAttachment(params) {
return service({
url: '/api/lmg/Common/DeleteAttachment',
method: 'delete',
params
})
}
export function GetStringObjectId(params) {
return service({
url: '/api/lmg/Common/GetStringObjectId',
method: 'get',
params
})
}
export function MigrationFile(params) {
return service({
url: '/api/lmg/Common/MigrationFile',
method: 'put',
params
})
}
export function GetProjectInfoList(params) {
return service({
url: '/api/lmg/Common/GetProjectInfoList',
method: 'get',
params
})
}
export function GetSectionList(params) {
return service({
url: '/api/lmg/Common/GetSectionList',
method: 'get',
params
})
}
export function GetLaborTeamPageList(params) {
return service({
url: '/api/lmg/Common/GetLaborTeamPageList',
method: 'get',
params
})
}
export function GetWorkerGroupPageList(params) {
return service({
url: '/api/lmg/Common/GetWorkerGroupPageList',
method: 'get',
params
})
}
export function GetAttacBusinessId(params) {
return service({
url: '/api/lmg/Common/GetAttacBusinessId',
method: 'get',
params
})
}
export function GetLoginResourceTree(params) {
return service({
url: '/api/lmg/Common/GetLoginResourceTree',
method: 'get',
params
})
}

217
src/api/system/index.js Normal file
View File

@ -0,0 +1,217 @@
import service from "@/utils/requestSystem";
export function GetLoginResourceTree(params) {
return service({
url: '/api/sys/Resources/GetLoginResourceTree',
method: 'get',
params
})
}
export function GetOwnerUnitPage(params) {
return service({
url: '/api/sys/OwnerUnit/GetPage',
method: 'get',
params
})
}
export function PostOwnerUnit(data) {
return service({
url: '/api/sys/OwnerUnit/Post',
method: 'post',
data
})
}
export function PutOwnerUnit(data) {
return service({
url: '/api/sys/OwnerUnit/Put',
method: 'put',
data
})
}
export function DeleteOwnerUnit(data) {
return service({
url: `/api/sys/OwnerUnit/Delete/${data}`,
method: 'delete',
})
}
export function GetSectionPage(params) {
return service({
url: '/api/sys/Section/GetPage',
method: 'get',
params
})
}
export function PostSection(data) {
return service({
url: '/api/sys/Section/Post',
method: 'post',
data
})
}
export function PutSection(data) {
return service({
url: '/api/sys/Section/Put',
method: 'put',
data
})
}
export function DeleteSection(data) {
return service({
url: `/api/sys/Section/Delete/${data}`,
method: 'delete',
})
}
export function UploadAttachment(data, config) {
return service({
url: '/api/sys/AttachmentConfig/UploadAttachment',
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data',
'Custom-Header': 'CustomValue',
},
...config
})
}
export function UpdateFile(params) {
return service({
url: '/api/sys/OtherAttachments/UpdateFile',
method: 'put',
params
})
}
export function GetAttacBusinessId(params) {
return service({
url: '/api/sys/OtherAttachments/GetAttacBusinessId',
method: 'get',
params
})
}
export function DeleteAttachment(data) {
return service({
url: `/api/sys/AttachmentConfig/DeleteAttachment/`,
method: 'delete',
data,
})
}
// export function DeleteAttachment(params) {
// return service({
// url: '/api/sys/OtherAttachments/DeleteAttachment',
// method: 'delete',
// params
// })
// }
export function GetProjectInfoPage(params) {
return service({
url: '/api/sys/ProjectInfo/GetPage',
method: 'get',
params
})
}
export function PostProjectInfo(data) {
return service({
url: '/api/sys/ProjectInfo/Post',
method: 'post',
data
})
}
export function PutProjectInfo(data) {
return service({
url: '/api/sys/ProjectInfo/Put',
method: 'put',
data
})
}
export function DeleteProjectInfo(data) {
return service({
url: `/api/sys/ProjectInfo/Delete/${data}`,
method: 'delete',
})
}
export function GetLaborTeamPage(params) {
return service({
url: '/api/sys/LaborTeam/GetPage',
method: 'get',
params
})
}
export function PostLaborTeam(data) {
return service({
url: '/api/sys/LaborTeam/Post',
method: 'post',
data
})
}
export function PutLaborTeam(data) {
return service({
url: '/api/sys/LaborTeam/Put',
method: 'put',
data
})
}
export function DeleteLaborTeam(data) {
return service({
url: `/api/sys/LaborTeam/Delete/${data}`,
method: 'delete',
})
}
export function GetWorkerGroupPage(params) {
return service({
url: '/api/sys/WorkerGroup/GetPage',
method: 'get',
params
})
}
export function GetDictionaryGetPage(params) {
return service({
url: '/api/sys/Dictionary/GetPage',
method: 'get',
params
})
}
export function GetDictionaryidDicItems(params) {
return service({
url: '/api/sys/Dictionary/GetDictionaryidDicItems',
method: 'get',
params
})
}
export function ResetPassword(data) {
return service({
url: `/api/sys/User/ResetPassword/ResetPassword`,
method: 'post',
data
})
}

74
src/assets/base.css Normal file
View File

@ -0,0 +1,74 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
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;
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1736752681898" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4266" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M823.00322 24.512277l-591.737042 0c-11.307533 0-20.466124 9.15859-20.466124 20.466124l0 934.043199c0 11.2973 9.15859 20.466124 20.466124 20.466124l591.737042 0c11.307533 0 20.466124-9.168824 20.466124-20.466124L843.469344 44.978401C843.469344 33.670867 834.310753 24.512277 823.00322 24.512277zM802.537096 773.96127l-480.135268 0c-11.307533 0-20.466124 9.168824-20.466124 20.466124 0 11.307533 9.15859 20.466124 20.466124 20.466124l480.135268 0 0 143.661957-550.804794 0L251.732301 65.444525l550.804794 0L802.537096 773.96127z" p-id="4267"></path><path d="M527.134699 886.514719m-48.461735 0a47.358 47.358 0 1 0 96.92347 0 47.358 47.358 0 1 0-96.92347 0Z" p-id="4268"></path></svg>

After

Width:  |  Height:  |  Size: 1012 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746427106785" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1696" width="13" height="13" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M387.017 261.155h242.129c74.925 0 132.079-162.482 132.079-162.482 0-48.66-4.872-85.474-88.07-88.068-83.172-2.62-103.098 58.158-162.872 58.158-61.266 0-91.91-47.78-167.301-58.158-75.365-10.427-88.044 39.408-88.044 88.068 0 0.001 57.129 162.482 132.079 162.482z m255.346 50.227H387.017c-282.762 0-352.204 555.51-352.204 555.51 0 72.967 59.138 146.253 132.08 146.253h695.57c72.942 0 132.078-73.286 132.078-146.253 0 0-69.465-555.51-352.178-555.51z m28.982 426.93c17.036 0 30.842 13.83 30.842 30.867 0 17.035-13.805 30.865-30.842 30.865H546.143v80.971c0 17.035-13.805 30.867-30.817 30.867-17.036 0-30.84-13.832-30.84-30.867v-80.97h-126.01c-17.035 0-30.84-13.83-30.84-30.866 0-17.037 13.805-30.867 30.84-30.867h126.01V699.15h-126.01c-17.035 0-30.84-13.806-30.84-30.842s13.805-30.89 30.84-30.89h85.181l-87.212-154.99c-8.518-14.785-3.452-33.633 11.284-42.15 14.759-8.519 33.607-3.476 42.125 11.259l105.008 185.88h5.31l105.008-185.88c8.517-14.735 27.365-19.779 42.101-11.258 14.76 8.516 19.828 27.364 11.309 42.148l-87.213 154.99h79.968c17.036 0 30.842 13.854 30.842 30.891 0 17.035-13.805 30.842-30.842 30.842H546.143v39.162h125.202z" fill="#bfcbd9" p-id="1697"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><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: 308 B

35
src/assets/main.css Normal file
View File

@ -0,0 +1,35 @@
/* @import "./base.css"; */
/* #app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
}
@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 2rem;
}
} */

View File

@ -0,0 +1,31 @@
@use 'variables' as v;
// 混入
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
@mixin text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin multi-ellipsis($lines) {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
}
// 响应式混入
@mixin respond-to($breakpoint) {
@if map-has-key(v.$breakpoints, $breakpoint) {
@media (min-width: map-get(v.$breakpoints, $breakpoint)) {
@content;
}
}
}

View File

@ -0,0 +1,153 @@
// 颜色系统
$colors: (
'primary': #409EFF,
'success': #67C23A,
'warning': #E6A23C,
'danger': #F56C6C,
'info': #909399,
'blue': #3385ff,
'white': #FFFFFF,
'black': #000000,
'theme1': #0A397D
);
// 中性色
$gray: (
'100': #f7f7f7,
'200': #f2f2f2,
'300': #e6e6e6,
'400': #d9d9d9,
'500': #bfbfbf,
'600': #8c8c8c,
'700': #595959,
'800': #434343,
'900': #262626
);
// 字体家族
$font-family: (
'base': -apple-system,
'BlinkMacSystemFont': 'Segoe UI',
'Roboto': 'Helvetica Neue',
'Arial': sans-serif
);
// 字体大小
$font-size: (
'xs': 12px,
'sm': 14px,
'base': 16px,
'lg': 18px,
'xl': 20px,
'2xl': 24px,
'3xl': 30px,
'4xl': 36px
);
// 字重
$font-weight: (
'light': 300,
'regular': 400,
'medium': 500,
'semibold': 600,
'bold': 700
);
// 行高
$line-height: (
'tight': 1.25,
'normal': 1.5,
'relaxed': 1.75,
'loose': 2
);
// 间距
$spacing: (
'none': 0,
'xs': 4px,
'sm': 8px,
'md': 16px,
'lg': 24px,
'xl': 32px,
'2xl': 48px
);
// 圆角
$border-radius: (
'none': 0,
'sm': 2px,
'base': 4px,
'md': 6px,
'lg': 8px,
'xl': 12px,
'full': 9999px
);
// 阴影
$box-shadow: (
'sm': '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
'base': '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
'md': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
'lg': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
'xl': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)'
);
// 断点
$breakpoints: (
'sm': 640px,
'md': 768px,
'lg': 1024px,
'xl': 1280px,
'2xl': 1536px
);
// z-index管理
$z-index: (
'dropdown': 1000,
'sticky': 1020,
'fixed': 1030,
'modal': 1040,
'popover': 1050,
'tooltip': 1060
);
// 主题
$themes: (
// 默认主题
'default': (
'bg1': #0A397D,
'color1': #FFFFFF,
),
// 主题1
'theme1': (
'bg1': #FF5733,
'color1': #FFFFFF,
),
// 主题2
'theme2': (
'bg1': #33FF57,
'color1': #000000,
)
);
// 导出变量到 CSS 变量
:root {
@each $name, $color in $colors {
--color-#{$name}: #{$color};
}
@each $key, $value in $spacing {
--spacing-#{$key}: #{$value};
}
@each $key, $value in $font-size {
--font-size-#{$key}: #{$value};
}
@each $theme-name, $theme-colors in $themes {
@each $name, $color in $theme-colors {
--color-#{$theme-name}-#{$name}: #{$color};
}
}
}

View File

@ -0,0 +1,15 @@
@use '../variables' as v;
@use '../mixins' as m;
.grid {
display: grid;
gap: map-get(v.$spacing, 'md');
&-cols-2 {
grid-template-columns: repeat(2, 1fr);
}
&-cols-3 {
grid-template-columns: repeat(3, 1fr);
}
}

162
src/assets/scss/main.scss Normal file
View File

@ -0,0 +1,162 @@
// @use 'variables' as v;
// @use 'mixins' as m;
// @use 'modules/button';
// @use 'modules/form';
// @use 'pages/dashboard';
// @use 'pages/login';
// 重置样式
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
// 全局样式
body {
// font-family: map-get(v.$font-family, 'base');
// font-size: map-get(v.$font-size, 'base');
// line-height: map-get(v.$line-height, 'normal');
// color: map-get(v.$colors, 'black');
}
// 通用类
.text-ellipsis {
@include m.text-ellipsis;
}
.flex-center {
@include m.flex-center;
}
.el-upload-list__item.el-icon--close-tip {
display: none !important;
}
.el-upload-list__item.is-success:focus:not(:hover) .el-icon--close-tip {
display: none !important;
}
// 响应式容器
.container {
width: 100%;
margin: 0 auto;
// padding: 0 map-get(v.$spacing, 'md');
// @include m.respond-to('sm') {
// max-width: map-get(v.$breakpoints, 'sm');
// }
// @include m.respond-to('md') {
// max-width: map-get(v.$breakpoints, 'md');
// }
// @include m.respond-to('lg') {
// max-width: map-get(v.$breakpoints, 'lg');
// }
// @include m.respond-to('xl') {
// max-width: map-get(v.$breakpoints, 'xl');
// }
}
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #abaeb4;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
::-webkit-scrollbar-corner {
background: #f1f1f1;
}
.overall-container {
height: 80vh !important;
width: 100%;
padding: 20px;
background-color: #fff;
overflow: auto !important;
.search-container {
.search-container-from {
display: flex;
flex-wrap: wrap;
}
}
.table-container {
width: 100%;
}
.layout-pagination {
margin-top: 20px;
display: flex;
flex-wrap: wrap;
}
.layout-w150 {
width: 150px;
}
.layout-w180 {
width: 180px;
}
.layout-w200 {
width: 200px;
}
.layout-w250{
width: 250px;
}
.layout-pr10 {
padding-right: 10px;
}
.layout-mr10 {
margin-right: 10px;
}
.layout_flex {
display: flex;
justify-content: center;
}
}
.layout_form {
padding: 0 20px;
overflow: auto;
.el-input {
width: 100% !important;
}
}
.layout_footer {
text-align: center;
}
.layout_form {
padding: 0 20px;
}
.layout_header_menu .el-menu--collapse .el-menu .el-submenu, .el-menu--popup {
min-width: 100px !important;
}
.el-popover.el-popper.layout_attendance_popover {
text-align: center !important;
min-width: 85px !important;
}

View File

@ -0,0 +1,19 @@
@use '../variables' as v;
@use '../mixins' as m;
// 按钮样式
.btn {
padding: map-get(v.$spacing, 'sm') map-get(v.$spacing, 'md');
border-radius: map-get(v.$border-radius, 'base');
font-size: map-get(v.$font-size, 'base');
&-primary {
background-color: map-get(v.$colors, 'primary');
color: map-get(v.$colors, 'white');
}
&-success {
background-color: map-get(v.$colors, 'success');
color: map-get(v.$colors, 'white');
}
}

View File

@ -0,0 +1,13 @@
@use '../variables' as v;
@use '../mixins' as m;
.dashboard {
&-header {
@include m.flex-center;
padding: map-get(v.$spacing, 'lg');
}
&-content {
margin: map-get(v.$spacing, 'xl');
}
}

View File

@ -0,0 +1,108 @@
$layout-title-height: 30px;
$layout-title-fontsize: 16px;
$layout-padding-horizontal: 2%;
$layout-top-height: 70%;
$layout-bottom-height: 30%;
$sidebar-width: 33%;
$section-padding: 10px;
$white-color: #FFFFFF;
.layout_wapper {
width: 100%;
height: 100%;
font-size: 12px;
.layout_content {
@extend .layout_wapper;
display: flex;
flex-direction: column;
color: $white-color;
.layout_m10 {
padding: $section-padding $section-padding $section-padding 0;
}
.layout_title {
background: url(@/assets/images/largeScreen/preview.png) center/100% 100%;
height: $layout-title-height;
width: 90%;
line-height: $layout-title-height;
span {
padding-left: $section-padding * 3;
font-size: $layout-title-fontsize;
}
}
.layout_top {
width: 100%;
height: $layout-top-height;
display: flex;
padding: 0 $layout-padding-horizontal;
.layout_top_left {
width: 33%;
.layout_top_left_one {
width: 100%;
height: 55%;
}
.layout_top_left_two {
width: 100%;
height: 45%;
}
}
.layout_top_center {
width: $sidebar-width;
height: 100%;
margin: 0 $layout-padding-horizontal;
}
.layout_top_right {
width: 33%;
.layout_top_right_one {
width: 100%;
height: 55%;
}
.layout_top_right_two {
width: 100%;
height: 45%;
}
}
}
.layout_bottom {
padding-left: $layout-padding-horizontal;
width: 100%;
height: $layout-bottom-height;
}
}
}
.layout_btn_back {
position: absolute;
top: 50%;
right: $layout-padding-horizontal;
z-index: 9;
}
::-webkit-scrollbar {
width: 5px;
height: 6px;
background: #f3f3f3;
}
// ::-webkit-scrollbar-thumb {
// border-radius: 6px;
// background: #e3e3e3;
// }
// ::-webkit-scrollbar-track {
// border-radius: 6px;
// background-color: transparent !important;
// }

View File

@ -0,0 +1,173 @@
$layout-title-height: 30px;
$layout-title-fontsize: 16px;
$sidebar-width: 33%;
$section-padding: 10px;
$content-padding: 1%;
$bottom-padding: 1.5%;
$white-color: #FFFFFF;
.layout_wapper {
width: 100%;
height: 100%;
font-size: 12px;
}
.layout_content {
@extend .layout_wapper;
display: flex;
color: $white-color;
}
.layout_m10 {
padding: $section-padding 0 0 0;
}
.layout_title {
background: url(@/assets/images/largeScreen/preview.png) center/100% 100%;
height: $layout-title-height;
width: 90%;
line-height: $layout-title-height;
span {
padding-left: $section-padding * 3;
font-size: $layout-title-fontsize;
}
}
%sidebar-layout {
width: $sidebar-width;
height: 100%;
padding-bottom: $bottom-padding;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.layout_left {
@extend %sidebar-layout;
padding-left: $content-padding;
.left_top { height: 37%; }
.left_center { height: 32%; }
.left_bottom { height: 31%; }
}
.layout_center {
@extend %sidebar-layout;
.center_top { height: 37%; }
.center_center { height: 32%; }
.center_bottom { height: 31%; }
}
.layout_right {
@extend %sidebar-layout;
.right_top { height: 64%; }
.right_bottom { height: 35%; }
}
// .layout_wapper {
// width: 100%;
// height: 100%;
// font-size: 12px;
// .layout_content {
// display: flex;
// color: #FFFFFF;
// width: 100%;
// height: 100%;
// .layout_m10 {
// padding: 10px 0 0 0;
// }
// .layout_title {
// background-image: url(@/assets/images/largeScreen/preview.png);
// background-size: 100% 100%;
// height: 30px;
// width: 90%;
// line-height: 30px;
// span {
// padding-left: 30px;
// font-size: 16px;
// }
// }
// .layout_left {
// width: 33%;
// height: 100%;
// padding-left: 1%;
// padding-bottom: 1.5%;
// display: flex;
// flex-direction: column;
// justify-content: space-between;
// .left_top {
// width: 100%;
// height: 37%;
// }
// .left_center {
// width: 100%;
// height: 32%;
// }
// .left_bottom {
// width: 100%;
// height: 31%;
// }
// }
// .layout_center {
// width: 33%;
// height: 100%;
// padding-bottom: 1.5%;
// display: flex;
// flex-direction: column;
// justify-content: space-between;
// .center_top {
// width: 100%;
// height: 37%;
// }
// .center_center {
// width: 100%;
// height: 32%;
// }
// .center_bottom {
// width: 100%;
// height: 31%;
// }
// }
// .layout_right {
// width: 33%;
// height: 100%;
// padding-bottom: 1.5%;
// display: flex;
// flex-direction: column;
// justify-content: space-between;
// .right_top {
// width: 100%;
// height: 64%;
// }
// .right_bottom {
// width: 100%;
// height: 35%;
// }
// }
// }
// }

View File

@ -0,0 +1,14 @@
@use '../variables' as v;
$theme-colors: (
'background': #ffffff,
'text': #333333,
'border': #e5e5e5
);
// 导出主题变量
:root[data-theme='light'] {
@each $name, $color in $theme-colors {
--theme-#{$name}: #{$color};
}
}

17
src/common/message.js Normal file
View File

@ -0,0 +1,17 @@
import { ElMessage } from 'element-plus';
export const showMessage = (type, message, options = {}) => {
const windowHeight = window.innerHeight;
const messageHeight = 60;
const offset = (windowHeight - messageHeight) / 2;
ElMessage({
message,
type,
plain: true,
offset: offset,
duration: 2000,
// showClose: true,
...options
});
};

View File

@ -0,0 +1,44 @@
import { ElNotification } from 'element-plus';
const DURATION = 1500;
const NotificationService = {
success(message, title = '成功', options = {}) {
return ElNotification({
title,
message,
duration:DURATION,
type: 'success',
...options,
});
},
warning(message, title = '警告', options = {}) {
return ElNotification({
title,
message,
duration:DURATION,
type: 'warning',
...options,
});
},
info(message, title = '信息', options = {}) {
return ElNotification({
title,
message,
duration:DURATION,
type: 'info',
...options,
});
},
error(message, title = '错误', options = {}) {
return ElNotification({
title,
message,
duration:DURATION,
type: 'error',
...options,
});
},
};
export default NotificationService;

View File

@ -0,0 +1,217 @@
import { ref } from 'vue'
import { GetProjectInfoList, GetSectionList, GetLaborTeamPageList, GetWorkerGroupPageList } from '@/api/common/index'
const commonParams = {
Skip: 0,
Take: 1000,
RequireTotalCount: true,
Sort: JSON.stringify([{ Selector: "CreateTime", Desc: true }]),
Filter: JSON.stringify(["IsDeleted", "=", false])
}
const CACHE_KEYS = {
PROJECTS: 'cached_projects',
SECTIONS: 'cached_sections',
LABOR_TEAMS: 'cached_labor_teams',
WORKER_GROUPS: 'cached_worker_groups',
LAST_CACHE_TIME: 'last_cache_time'
}
const CACHE_DURATION = 5 * 60 * 1000
export const useCascadeSelect = (fetchOptions) => {
const projectState = ref('')
const projectListAll = ref([])
const projectList = ref([])
const sectionListAll = ref([])
const sectionState = ref('')
const sectionList = ref([])
const laborTeamListAll = ref([])
const laborTeamState = ref('')
const laborTeamList = ref([])
const teamsGroupsState = ref('')
const teamsGroupsList = ref([])
const teamsGroupsListAll = ref([])
const isCacheValid = (cacheKey) => {
const lastCacheTime = sessionStorage.getItem(CACHE_KEYS.LAST_CACHE_TIME)
if (!lastCacheTime) return false
const cachedData = sessionStorage.getItem(cacheKey)
return cachedData &&
(Date.now() - parseInt(lastCacheTime)) < CACHE_DURATION
}
const getFromCache = (cacheKey) => {
const cachedData = sessionStorage.getItem(cacheKey)
return cachedData ? JSON.parse(cachedData) : null
}
const saveToCache = (cacheKey, data) => {
sessionStorage.setItem(cacheKey, JSON.stringify(data))
sessionStorage.setItem(CACHE_KEYS.LAST_CACHE_TIME, Date.now().toString())
}
const fetchData = async (apiFunc, refValue, cacheKey) => {
try {
if (isCacheValid(cacheKey)) {
const cachedData = getFromCache(cacheKey)
if (cachedData) {
refValue.value = cachedData
return
}
}
const res = await apiFunc(commonParams)
if (res.StatusCode === 200) {
const FIELD_FILTERS = {
[CACHE_KEYS.PROJECTS]: item => ({
Id: item.Id,
AC001: item.AC001,
AC002: item.AC002
}),
[CACHE_KEYS.SECTIONS]: item => ({
Id: item.Id,
AID: item.AID,
BC01: item.BC01,
BC02: item.BC02
}),
[CACHE_KEYS.LABOR_TEAMS]: item => ({
Id: item.Id,
BID: item.BID,
DC01: item.DC01
}),
[CACHE_KEYS.WORKER_GROUPS]: item => ({
Id: item.Id,
BID: item.BID,
DID: item.DID,
EC01: item.EC01
})
};
const filteredData = res.Data.map(FIELD_FILTERS[cacheKey] || (item => ({ Id: item.Id })));
refValue.value = filteredData;
saveToCache(cacheKey, filteredData);
} else {
refValue.value = []
}
} catch {
refValue.value = []
}
}
const initSelect = () => {
if (projectListAll.value?.length > 0) {
projectList.value = projectListAll.value
projectState.value = projectListAll.value[0]?.Id || ''
sectionList.value = sectionListAll.value?.filter(item => item.AID === projectState.value) || []
sectionState.value = sectionList.value[0]?.Id || ''
laborTeamList.value = laborTeamListAll.value?.filter(item => item.BID === sectionState.value) || []
laborTeamState.value = laborTeamList.value[0]?.Id || ''
teamsGroupsList.value = teamsGroupsListAll.value?.filter(item => item.DID === laborTeamState.value) || []
teamsGroupsState.value = teamsGroupsList.value[0]?.Id || ''
return
}
if (sectionListAll.value?.length > 0) {
sectionList.value = sectionListAll.value
sectionState.value = sectionListAll.value[0]?.Id || ''
laborTeamList.value = laborTeamListAll.value?.filter(item => item.BID === sectionState.value) || []
laborTeamState.value = laborTeamList.value[0]?.Id || ''
teamsGroupsList.value = teamsGroupsListAll.value?.filter(item => item.DID === laborTeamState.value) || []
teamsGroupsState.value = teamsGroupsList.value[0]?.Id || ''
return
}
if (laborTeamListAll.value?.length > 0) {
laborTeamList.value = laborTeamListAll.value
laborTeamState.value = laborTeamList.value[0]?.Id || ''
teamsGroupsList.value = teamsGroupsListAll.value?.filter(item => item.DID === laborTeamState.value) || []
teamsGroupsState.value = teamsGroupsList.value[0]?.Id || ''
return
}
if (teamsGroupsListAll.value?.length > 0) {
teamsGroupsList.value = teamsGroupsListAll.value
teamsGroupsState.value = teamsGroupsList.value[0]?.Id || ''
}
}
const projectChange = () => {
sectionList.value = sectionListAll.value?.filter(item => item.AID === projectState.value) || []
sectionState.value = sectionList.value[0]?.Id || ''
laborTeamList.value = laborTeamListAll.value?.filter(item => item.BID === sectionState.value) || []
laborTeamState.value = laborTeamList.value[0]?.Id || ''
teamsGroupsList.value = teamsGroupsListAll.value?.filter(item => item.DID === laborTeamState.value) || []
teamsGroupsState.value = teamsGroupsList.value[0]?.Id || ''
fetchOptions.onChange?.()
}
const sectionChange = () => {
laborTeamList.value = laborTeamListAll.value?.filter(item => item.BID === sectionState.value) || []
laborTeamState.value = laborTeamList.value[0]?.Id || ''
teamsGroupsList.value = teamsGroupsListAll.value?.filter(item => item.DID === laborTeamState.value) || []
teamsGroupsState.value = teamsGroupsList.value[0]?.Id || ''
fetchOptions.onChange?.()
}
const laborTeamChange = () => {
teamsGroupsList.value = teamsGroupsListAll.value?.filter(item => item.DID === laborTeamState.value) || []
teamsGroupsState.value = teamsGroupsList.value[0]?.Id || ''
fetchOptions.onChange?.()
}
const teamsGroupsChange = () => {
fetchOptions.onChange?.()
}
const initData = async () => {
await Promise.all([
fetchData(GetProjectInfoList, projectListAll, CACHE_KEYS.PROJECTS),
fetchData(GetSectionList, sectionListAll, CACHE_KEYS.SECTIONS),
fetchData(GetLaborTeamPageList, laborTeamListAll, CACHE_KEYS.LABOR_TEAMS),
fetchData(GetWorkerGroupPageList, teamsGroupsListAll, CACHE_KEYS.WORKER_GROUPS)
])
initSelect()
}
const clearCache = () => {
Object.values(CACHE_KEYS).forEach(key => {
sessionStorage.removeItem(key)
})
}
return {
projectState,
projectList,
projectListAll,
sectionState,
sectionList,
sectionListAll,
laborTeamState,
laborTeamList,
laborTeamListAll,
teamsGroupsState,
teamsGroupsList,
teamsGroupsListAll,
projectChange,
sectionChange,
laborTeamChange,
teamsGroupsChange,
initData,
clearCache
}
}

View File

@ -0,0 +1,42 @@
<template>
<svg :class="svgClass" :style="{ color: color }" aria-hidden="true">
<use :xlink:href="iconName" />
</svg>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
name: {
type: String,
required: true
},
color: {
type: String,
default: ''
},
customClass: {
type: String,
default: ''
}
})
const iconName = computed(() => `#icon-${props.name}`)
const svgClass = computed(() => {
if (props.customClass) {
return `svg-icon ${props.customClass}`
}
return 'el-icon'
})
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,54 @@
<style lang="scss" scoped>
.layout_container {
height: 600px !important;
padding: 0 20px;
}
</style>
<template>
<el-dialog v-model="drawerVisible" align-center :title="title" width="800" @close="handleClose"
:close-on-click-modal="showDrawerClose" :close-on-press-escape="showDrawerClose" :show-close="true">
<div class="layout_container">
<iframe :src="iframeSrc" frameborder="0" width="100%" height="100%" style="width: 100%; height: 100%;"></iframe>
</div>
<template #footer>
<div class="layout_footer">
<el-button type="info" @click="handleClose">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue'
const props = defineProps({
store: Object
});
const emit = defineEmits(['close'])
const showDrawerClose = ref(false)
const drawerVisible = ref(false)
const title = ref('附件')
const iframeSrc = ref('')
watch(() => props.store, (newValue) => {
if (newValue && newValue.openDditFlag !== undefined) {
drawerVisible.value = newValue.openDditFlag
}
if (newValue && newValue.src !== undefined) {
iframeSrc.value = newValue.src
}
}, { immediate: true, deep: true })
const handleClose = () => {
iframeSrc.value = ''
emit('close')
}
onMounted(() => {
})
</script>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

View File

@ -0,0 +1,81 @@
<template>
<el-container class="layout-container">
<el-header>
<nav-header />
</el-header>
<el-container>
<el-aside :class="{ 'collapsed': isCollapse, 'expanded': !isCollapse }">
<div class="scrollable_menu">
<nav-menu :is-collapse="isCollapse" @toggleCollapse="toggleCollapse" />
</div>
</el-aside>
<el-main>
<Tabs />
<Home v-show="showDefaultContent" />
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import NavHeader from '@/components/layouts/navHeader.vue'
import NavMenu from '@/components/layouts/navMenu.vue'
import Tabs from '@/components/layouts/tabs.vue'
import Home from '@/views/home/index.vue'
const route = useRoute()
const showDefaultContent = ref(true)
const isCollapse = ref(false)
//
watch(() => route.path, (newPath) => {
showDefaultContent.value = newPath === '/home'
}, { immediate: true })
const toggleCollapse = () => {
isCollapse.value = !isCollapse.value
}
</script>
<style lang="scss" scoped>
.layout-container {
height: 100vh;
:deep(.el-header) {
background-color: #fff;
border-bottom: 1px solid #dcdfe6;
padding: 0;
}
:deep(.el-aside) {
// background-color: #304156;
background: #fff;
color: #fff;
transition: width 0.3s;
height: calc(100vh - 60px);
position: relative;
}
:deep(.el-main) {
background-color: #f0f2f5;
padding: 20px;
transition: margin-left 0.3s;
}
.collapsed {
width: 75px;
}
.expanded {
width: 200px;
}
}
.scrollable_menu {
height: 100%;
overflow-y: auto;
background: var(--color-bg1)
}
</style>

View File

@ -0,0 +1,83 @@
<style lang="scss" scoped>
.layout_menu {
border: none;
.title {
font-size: 24px;
color: rgba(255, 255, 255, 0.88);
font-style: normal;
font-weight: bold;
}
.user {
margin: 5px;
width: 30px;
height: 30px;
}
}
.el-menu--horizontal>.el-menu-item:nth-child(1) {
margin-right: auto;
}
.el-menu--horizontal > .el-menu-item {
border-bottom: none;
text-decoration: none;
}
</style>
<template>
<el-menu :default-active="activeIndex" class="layout_menu" mode="horizontal" :ellipsis="false" @select="handleSelect"
background-color="#0A397D" text-color="#fff" active-text-color="#fff">
<el-menu-item index=" 0">
<span class="title">{{ systemName }}</span>
</el-menu-item>
<el-menu-item index="1" @click="handleLogout">系统切换</el-menu-item>
<el-sub-menu index="2">
<template #title>{{ userName }}</template>
<el-menu-item index="2-1" @click="reset">密码重置</el-menu-item>
<el-menu-item index="2-2" @click="onExit">退出系统</el-menu-item>
</el-sub-menu>
<ResetDetail v-if="resetDetailFlag" :resetDetailFlag = resetDetailFlag @close="handleClose" />
</el-menu>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import ResetDetail from "./resetDetail.vue";
const userStore = useUserStore()
const systemName = JSON.parse(sessionStorage.getItem('system')).systemName
const userName = JSON.parse(sessionStorage.getItem('userInfo')).idp
const resetDetailFlag = ref(false)
const reset = () => {
resetDetailFlag.value = true
}
const handleClose = () => {
resetDetailFlag.value = false
}
const handleLogout = () => {
userStore.logout()
sessionStorage.clear()
window.open(import.meta.env.VITE_HOME_URL, '_self')
}
const activeIndex = ref('1')
const currentTheme = ref('default')
const toggleTheme = (type = 'default') => {
currentTheme.value = type
document.documentElement.style.setProperty('--color-bg1', `var(--color-${currentTheme.value}-bg1)`)
document.documentElement.style.setProperty('--color-color1', `var(--color-${currentTheme.value}-color1)`)
}
const handleSelect = () => { }
const onExit = () => {
userStore.logout()
sessionStorage.clear()
window.open(import.meta.env.VITE_HOME_URL + '?access=exit', '_self');
}
onMounted(() => {
toggleTheme()
})
</script>

View File

@ -0,0 +1,92 @@
<template>
<el-menu v-if="permissionStore.isRoutesGenerated"
:default-active="route.path"
class="nav-menu"
background-color="#04285C"
text-color="#bfcbd9"
active-text-color="#409EFF"
@select="handleSelect"
:collapse="isCollapse"
unique-opened>
<recursive-menu-item
v-for="routeItem in menuRoutes"
:key="routeItem.path"
:route-item="routeItem"
:base-path="routeItem.path"
/>
</el-menu>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { usePermissionStore } from '@/stores/permission'
import RecursiveMenuItem from './recursiveMenuItem.vue'
const route = useRoute()
const router = useRouter()
const permissionStore = usePermissionStore()
const { isCollapse } = defineProps({
isCollapse: {
type: Boolean,
default: false
}
})
//
const menuRoutes = computed(() => {
return permissionStore.routes.filter(route => {
return !route.meta?.hidden && (route.children?.length > 0 || route.meta?.title)
})
})
//
const handleSelect = (index) => {
try {
router.push(index)
.then(() => {
// console.log('Navigation successful to:', index)
})
.catch(() => {
// console.error('Navigation failed:', error)
router.push('/home')
})
} catch (error) {
console.error('Navigation error:', error)
}
}
</script>
<style lang="scss" scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
.nav-menu {
height: 100%;
border-right: none;
background: #0A397D;
:deep(.el-menu-item) {
&.is-active {
background-color: #04285C;
}
}
.el-menu-item,
.el-sub-menu__title {
&:hover {
background-color: #103261;
}
}
}
.layout_menu {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -0,0 +1,52 @@
<template>
<el-menu-item v-if="!hasChildren" :index="fullPath">
<el-icon v-if="routeItem.meta?.icon && !isSvgIcon">
<component :is="routeItem.meta.icon" />
</el-icon>
<svg-icon v-else-if="routeItem.meta?.icon && isSvgIcon" :name="routeItem.meta.icon" />
<span>{{ routeItem.meta?.title }}</span>
</el-menu-item>
<el-sub-menu v-else :index="fullPath">
<template #title>
<el-icon v-if="routeItem.meta?.icon && !isSvgIcon">
<component :is="routeItem.meta.icon" />
</el-icon>
<svg-icon v-else-if="routeItem.meta?.icon && isSvgIcon" :name="routeItem.meta.icon" />
<span>{{ routeItem.meta?.title }}</span>
</template>
<recursive-menu-item v-for="child in routeItem.children" :key="child.path" :route-item="child"
:base-path="fullPath" />
</el-sub-menu>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
routeItem: {
type: Object,
required: true
},
basePath: {
type: String,
required: true
}
})
const hasChildren = computed(() => {
return props.routeItem.children && props.routeItem.children.length > 0
})
const isSvgIcon = computed(() => {
return props.routeItem.meta?.icon?.startsWith('svg-')
})
const fullPath = computed(() => {
// basePath basePath
if (props.basePath.endsWith(props.routeItem.path)) {
return props.basePath
}
//
return `${props.basePath}/${props.routeItem.path}`.replace(/\/+/g, '/')
})
</script>

View File

@ -0,0 +1,134 @@
<template>
<el-dialog v-model="drawerVisible" title="重置密码" width="650" align-center @close="handleClose()"
:close-on-click-modal="showDrawerClose" :close-on-press-escape="showDrawerClose">
<el-form ref="ruleFormRef" label-width="150" :model="form" :rules="rules" status-icon class="layout_form">
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="原密码" prop="password">
<el-input v-model="form.password" name="password" clearable
placeholder="请输入原密码" maxlength="16" show-word-limit show-password />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="form.newPassword" name="newPassword" clearable show-password
placeholder="请输入新密码" maxlength="16" show-word-limit />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="再次输入新密码" prop="qrNewPassword">
<el-input v-model="form.qrNewPassword" name="qrNewPassword" clearable show-password
placeholder="请输入再次输入新密码" maxlength="16" show-word-limit />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="layout_footer">
<el-button type="info" @click="handleClose">取消</el-button>
<el-button type="primary" @click="submitForm">提交</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import { showMessage } from '@/common/message'
import regexPatterns from '@/utils/regexp.js';
import { ResetPassword } from '@/api/system'
const props = defineProps({
resetDetailFlag: Boolean
});
const showDrawerClose = ref(false)
const emit = defineEmits(['close'])
const drawerVisible = ref(false)
const form = ref({
password: '',
newPassword: '',
qrNewPassword: '',
id: '',
})
const ruleFormRef = ref()
watch(() => props.resetDetailFlag, (newValue) => {
if (newValue && newValue !== undefined) {
drawerVisible.value = newValue
}
}, { immediate: true, deep: true })
const checkidPasswordDifficult = (rule, value, callback) => {
if (!regexPatterns.passwordDifficult.test(value)) {
callback(new Error('请输入大写字母、小写字母、数字、特殊字符组成的8-16位密码'))
} else {
callback()
}
}
const checkidPasswordsDifficult = (rule, value, callback) => {
if (value !== form.value.qrNewPassword) {
callback(new Error('两次输入的密码不一致'))
}
if (!regexPatterns.passwordDifficult.test(value)) {
callback(new Error('请输入大写字母、小写字母、数字、特殊字符组成的8-16位密码'))
} else {
callback()
}
}
const rules = {
password: [
{ required: true, validator: checkidPasswordDifficult, trigger: 'change' },
],
newPassword: [
{ required: true, validator: checkidPasswordDifficult, trigger: 'change' },
],
qrNewPassword: [
{ required: true, validator: checkidPasswordsDifficult, trigger: 'change' },
],
}
const resetPassword = () => {
ResetPassword(form.value).then(res => {
if (res.StatusCode === 200) {
showMessage("success", "重置密码成功");
handleClose("refresh");
} else {
showMessage("error", res.Message);
}
}).catch(error => {
showMessage("error", error.Message);
})
}
const submitForm = async () => {
const userInfo = JSON.parse(sessionStorage.getItem('userInfo'));
if (userInfo) {
form.value.id = userInfo.sub
ruleFormRef.value.validate((valid) => {
if (valid) {
resetPassword()
}
})
} else {
showMessage('error', '用户信息获取失败,请重新登录')
}
}
const handleClose = () => {
drawerVisible.value = false
form.value = {
password: '',
newPassword: '',
qrNewPassword: '',
id: '',
}
emit('close')
}
</script>

View File

@ -0,0 +1,60 @@
<template>
<div class="tabs-container">
<el-tabs
v-model="tabsStore.activeTab"
type="card"
@tab-click="handleTabClick"
@tab-remove="handleTabRemove"
>
<el-tab-pane
v-for="item in tabsStore.tabs"
:key="item.path"
:label="item.title"
:name="item.path"
:closable="!item.fixed"
>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { useTabsStore } from '@/stores/tabs'
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const tabsStore = useTabsStore()
//
watch(() => route, (newRoute) => {
if (newRoute.meta?.title && !newRoute.meta?.noTab) {
const fromMenu = newRoute.query.fromMenu || false
tabsStore.addTab({
...newRoute,
meta: {
...newRoute.meta,
fromMenu
}
})
}
}, { immediate: true, deep: true })
//
const handleTabClick = (tab) => {
tabsStore.switchTab(tab.props.name)
}
//
const handleTabRemove = (targetPath) => {
tabsStore.removeTab(targetPath)
}
</script>
<style lang="scss" scoped>
.tabs-container {
background: #fff;
padding: 6px 6px 0;
// border-radius: 0px;
}
</style>

34
src/main.js Normal file
View File

@ -0,0 +1,34 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './utils/rem'
import './assets/scss/main.scss';
import ElementPlus from 'element-plus'//引入elmentplus的组件
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import 'element-plus/dist/index.css'//引入elmentplus的组件的样式
import * as ElementPlusIconsVue from '@element-plus/icons-vue' //图标
import 'virtual:svg-icons-register'
import SvgIcon from '@/components/SvgIcon/index.vue' // 导入 SVG 组件
//需要npm install pinia-plugin-persist npm install pinia-plugin-persist@1.0.0 -- 支持本地数据持久化保存
import piniaPluginPersist from 'pinia-plugin-persist'
import NotificationService from './common/notification.js';
import '@/stores/login'
const app = createApp(App)
//让ElementPlus 图标文件生效
for (const[key,component] of Object.entries(ElementPlusIconsVue)){
app.component(key,component)
}
app.component('svg-icon', SvgIcon)
app.use(createPinia().use(piniaPluginPersist));
app.provide('notification', NotificationService);
app.use(router)
app.use(ElementPlus,{locale: zhCn});//全局引入ElementPlus
app.mount('#app')

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

@ -0,0 +1,118 @@
import { createRouter, createWebHistory } from 'vue-router'
import { usePermissionStore } from '@/stores/permission'
export const constantRoutes = [
{
path: '/',
redirect: '/home',
},
{
path: '/home',
name: 'home',
component: () => import('@/components/layouts/defaultLayout.vue'),
meta: {
requiresAuth: true,
title: '首页',
icon: 'House'
},
},
// {
// path: '/personnePostPhysicalExamination',
// name: 'personnePostPhysicalExamination',
// meta: {
// requiresAuth: true,
// title: '人员岗前体检',
// icon: 'User'
// },
// component: () => import('@/components/layouts/defaultLayout.vue')
// },
// {
// path: '/personnelHealthMonitoring',
// name: 'personnelHealthMonitoring',
// meta: {
// requiresAuth: true,
// title: '人员岗前体检',
// icon: 'User'
// },
// component: () => import('@/components/layouts/defaultLayout.vue')
// },
{
path: '/404',
name: '404',
meta: {
title: '404',
hidden: true,
noTab: true
},
component: () => import('@/views/error/404.vue')
},
{
path: '/:pathMatch(.*)*',
redirect: '/404',
hidden: true
}
];
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes
})
export function resetRouter() {
const newRouter = createRouter({
history: createWebHistory(),
routes: constantRoutes
})
router.matcher = newRouter.matcher
}
// 路由守卫
router.beforeEach(async (to, from, next) => {
const token = sessionStorage.getItem('token')
if (!token) {
window.open(import.meta.env.VITE_HOME_URL, '_self');
return false
}
// 获取权限store
const permissionStore = usePermissionStore()
//permissionStore.generateRoutes()
// 如果路由还没有生成,先生成路由
if (!permissionStore.isRoutesGenerated) {
try {
await permissionStore.generateRoutes()
// 重新进入当前路由
if (to.path === '/404') {
next('/home')
} else {
next({ ...to, replace: true })
}
return
} catch (error) {
console.error('Failed to generate routes:', error)
next('/404')
return
}
}
// 检查路由是否存在
if (to.matched.length === 0 && to.path !== '/404') {
next('/404')
return
}
if (to.matched.some(record => record.meta.requiresAuth)) {
if (token) {
next()
} else {
window.open(import.meta.env.VITE_HOME_URL, '_self');
}
} else {
next()
}
})
export default router

5
src/stores/index.js Normal file
View File

@ -0,0 +1,5 @@
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia

25
src/stores/login.js Normal file
View File

@ -0,0 +1,25 @@
import { createPinia } from 'pinia'
import { usePermissionStore } from '@/stores/permission'
const pinia = createPinia()
const permissionStore = usePermissionStore(pinia)
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.get('token')) {
sessionStorage.setItem('token', urlParams.get('token'))
sessionStorage.setItem('system', urlParams.get('system'))
sessionStorage.setItem('userInfo', urlParams.get('userInfo'))
const token = sessionStorage.getItem('token')
if (!token) {
window.open(import.meta.env.VITE_HOME_URL, '_self');
} else {
permissionStore.generateRoutes()
}
} else {
if (sessionStorage.getItem('token')) {
permissionStore.generateRoutes()
} else {
window.open(import.meta.env.VITE_HOME_URL, '_self');
}
}

138
src/stores/permission.js Normal file
View File

@ -0,0 +1,138 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { GetLoginResourceTree } from '@/api/common/index'
import router, { constantRoutes } from '@/router'
const modules = import.meta.glob('../views/**/index.vue')
const layoutModule = import.meta.glob('../components/layouts/defaultLayout.vue')
export const usePermissionStore = defineStore('permission', () => {
const routes = ref([...constantRoutes])
const dynamicRoutes = ref([])
const isRoutesGenerated = ref(false)
// 处理组件路径
const loadComponent = (path) => {
try {
const formattedPath = path.replace(/^\/+/, '').replace(/\/+/g, '/')
const componentPath = `../views/${formattedPath}/index.vue`
if (modules[componentPath]) {
return modules[componentPath]
}
return modules['../views/error/404.vue']
} catch (error) {
console.error('Failed to load component:', error)
return modules['../views/error/404.vue']
}
}
const getLayoutComponent = () => {
const layoutPath = '../components/layouts/defaultLayout.vue'
return layoutModule[layoutPath]
}
// 生成动态路由
const generateRoutes = async () => {
const token = sessionStorage.getItem('token')
if(!token) return
try {
// 如果路由已经生成过,直接返回
if (isRoutesGenerated.value) {
return dynamicRoutes.value
}
const system = JSON.parse(sessionStorage.getItem('system'))
if (!system.systemId) {
console.error('未找到系统信息')
isRoutesGenerated.value = true
return []
}
const params = {
serviceID: system.systemId
}
const response = await GetLoginResourceTree(params)
isRoutesGenerated.value = true
if (response.StatusCode === 200 && response.Data) {
sessionStorage.setItem('routers', JSON.stringify(response.Data))
const routerData = response.Data
const newRoutes = []
// 处理路由数据
routerData.forEach(item => {
if (item.ResType === '菜单') {
const route = {
path: '/' + item.Path,
name: item.Path,
meta: {
title: item.Name,
icon: item.IconPath,
requiresAuth: true,
},
component: getLayoutComponent(),
children: item.Children ? item.Children.filter(child => child.ResType === '菜单').map(child => {
const childPath = `${item.Path}/${child.Path}`
return {
path: child.Path,
name: childPath,
meta: {
title: child.Name,
requiresAuth: true,
},
component: loadComponent(childPath),
children: child.Children ? child.Children.filter(grandChild => grandChild.ResType === '菜单').map(grandChild => {
const grandChildPath = `${childPath}/${grandChild.Path}`
return {
path: grandChild.Path,
name: grandChildPath,
meta: {
title: grandChild.Name,
requiresAuth: true,
},
component: loadComponent(grandChildPath)
}
}) : []
}
}) : []
}
newRoutes.push(route)
}
})
// 清除现有的动态路由
dynamicRoutes.value.forEach(route => {
if (router.hasRoute(route.name)) {
router.removeRoute(route.name)
}
})
// 添加新的动态路由
newRoutes.forEach(route => {
if (!router.hasRoute(route.name)) {
router.addRoute(route)
}
})
// 更新路由状态
dynamicRoutes.value = newRoutes
routes.value = [...constantRoutes, ...newRoutes]
return newRoutes
}
return []
} catch (error) {
console.error('获取路由数据失败:', error)
window.open(import.meta.env.VITE_HOME_URL, '_self')
return []
}
}
return {
routes,
dynamicRoutes,
isRoutesGenerated,
generateRoutes
}
})

81
src/stores/tabs.js Normal file
View File

@ -0,0 +1,81 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import router from '@/router'
export const useTabsStore = defineStore('tabs', () => {
const defaultTabs = [{
title: '首页',
path: '/home',
name: 'home',
fixed: true
}]
const tabs = ref(JSON.parse(sessionStorage.getItem('tabs')) || defaultTabs)
const activeTab = ref(sessionStorage.getItem('activeTab') || '/home')
// 添加标签页
const addTab = (route) => {
if (route.meta?.noTab || route.meta?.hidden) {
return
}
const isExist = tabs.value.some(tab => tab.path === route.path)
if (!isExist) {
tabs.value.push({
title: route.meta.title,
path: route.path,
name: route.name,
fixed: false
})
}
activeTab.value = route.path
saveToStorage()
}
// 移除标签页
const removeTab = (targetPath) => {
const targetIndex = tabs.value.findIndex(tab => tab.path === targetPath)
if (targetIndex === -1 || tabs.value[targetIndex].fixed) return
if (activeTab.value === targetPath) {
const nextTab = tabs.value[targetIndex + 1] || tabs.value[targetIndex - 1]
if (nextTab) {
activeTab.value = nextTab.path
router.push(nextTab.path)
}
}
tabs.value.splice(targetIndex, 1)
saveToStorage()
}
// 切换标签页
const switchTab = (path) => {
activeTab.value = path
router.push(path)
saveToStorage()
}
// 保存到sessionStorage
const saveToStorage = () => {
sessionStorage.setItem('tabs', JSON.stringify(tabs.value))
sessionStorage.setItem('activeTab', activeTab.value)
}
// 清空标签页
const clearTabs = () => {
tabs.value = [...defaultTabs]
activeTab.value = '/home'
sessionStorage.removeItem('tabs')
sessionStorage.removeItem('activeTab')
}
return {
tabs,
activeTab,
addTab,
removeTab,
switchTab,
clearTabs
}
})

34
src/stores/user.js Normal file
View File

@ -0,0 +1,34 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useTabsStore } from '@/stores/tabs'
export const useUserStore = defineStore('user', () => {
const isLoggedIn = ref(sessionStorage.getItem('isLoggedIn') === 'true')
const userInfo = ref(JSON.parse(sessionStorage.getItem('userInfo')) || null)
// 登录
const login = (info) => {
const tabsStore = useTabsStore()
tabsStore.clearTabs()
isLoggedIn.value = true
userInfo.value = info
sessionStorage.setItem('isLoggedIn', 'true')
sessionStorage.setItem('userInfo', JSON.stringify(info))
}
// 退出登录
const logout = () => {
isLoggedIn.value = false
userInfo.value = null
sessionStorage.clear()
const tabsStore = useTabsStore()
tabsStore.clearTabs()
}
return {
isLoggedIn,
userInfo,
login,
logout
}
})

316
src/style.css Normal file
View File

@ -0,0 +1,316 @@
* {
padding: 0px;
margin: 0px;
}
ul,
li {
list-style-type: none;
padding:0;
margin:0;
}
a {
color: #333;
text-decoration: none;
}
body {
font-size: 14px;
background-color: #11145C;
color: #fff;
}
body,html{
width:100%;
height:100%;
}
#module {
height:100%;
}
#app {
height:100%;
}
/*登录*/
.login {
background: url(./assets/img/bg.jpg) no-repeat;
width: 100%;
height: 100vh;
background-size: cover;
perspective: 800px;
}
.login-content {
display: flex;
align-items:center;
justify-content: center;
position: relative;
left: 0;
top: 50%;
transform: translate(0,-50%);
background: rgba(255,255,255,.3);
padding: 40px;
}
.login-box {
width: 250px;
height: 200px;
margin: 0 10px;
box-shadow: 0 3px 20px 2px rgba(0, 0, 0, .3);
display:flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
}
.login-box i{
font-size: 40px;
}
.login-form {
width: 45vh;
padding: 20px;
box-shadow: 0 3px 20px 2px rgba(0, 0, 0, .3);
/* position: relative;
left: 50%;
top: 50%;
transform: translate(-50%,-50%); */
text-align: center;
}
.login-form .el-form-item__content {
display: flex;
flex-wrap: wrap;
align-items: center;
flex: 1;
line-height: 32px;
position: relative;
font-size: var(--font-size);
min-width: 0;
justify-content: center;
}
.login-form .el-form-item__content .el-button {
margin-left: -70px;
}
.login-form .login-title,
.login-form .el-form-item__label {
color: #fff;
}
.login .login-title {
font-size: 25px;
margin: 20px 0;
}
.login .login-button {
margin-left: -70px;
width: 100px;
}
.exit {
float: right;
margin:10px;
padding: 2px 10px;
background: #233444;
border: none;
color: #fff;
}
/* 首页 */
#module .el-container {
height:100%;
}
#module .el-container .el-menu-item ,
#module .el-container .el-sub-menu .el-sub-menu__title{
color:#f9f9f9;
}
#module .el-container .el-menu-item:hover ,
#module .el-container .el-sub-menu .el-sub-menu__title:hover{
color:#fff;
background:#30377d;
}
#module .el-menu-item.is-active,
#module .el-sub-menu.is-active {
color: #fff;
background:#30377d;
}
#module .el-container .el-sub-menu.is-active.is-opened,
#module .el-container .el-sub-menu.is-active.is-opened .el-sub-menu__title {
color: #f9f9f9;
background:#20266d;
}
#module .el-container .el-header {
/* border-bottom: 1px solid #f1f1f1; */
background: #20266d;
}
#module .el-container .el-header .exit{
margin-top:10px;
}
#module .aside {
background: #20266d;
}
#module .aside .el-menu {
border-right: solid 1px #20266d;
background: #20266d;
}
#module .el-submenu__title {
color:#f9f9f9;
}
/* 面包屑 */
#module .breadcrumb {
padding:20px;
display: inline-block;
}
#module .breadcrumb ul li {
display: inline-block;
}
#module .breadcrumb ul li span{
padding: 0 5px;
}
#module .el-carousel__item img {
color: #fff;
font-size: 14px;
opacity: 0.75;
line-height: 200px;
margin: 0;
max-width:100%;
}
#module .breadcrumb .el-breadcrumb__item .el-breadcrumb__inner ,
#module .breadcrumb .el-breadcrumb__item .el-breadcrumb__inner a {
color: #fff;
}
#module .el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}
#module .el-carousel__item:nth-child(2n+1) {
background-color: #d3dce6;
}
/* 首页主体部分 */
#module .index-content {
display: flex;
width:100%;
}
#module .index-content .content-left {
flex:30%;
}
#module .index-content .content-right {
flex:70%;
}
#module .index-content .content {
margin: 10px;
background: #fff;
}
#module .el-main .el-card {
border:none;
background: #20266d;
}
#module .el-main .el-card .el-card__body {
color: #fff;
}
@media screen and (max-width: 1024px) {
.index-title li {
width: 18%;
}
}
.index-title li:nth-child(1) {
background: #fe8688;
}
.index-title li:nth-child(2) {
background: #feba35;
}
.index-title li:nth-child(3) {
background: #1bc6bd;
}
.index-title li:nth-child(4) {
background: #ba99ff;
}
.banner {
position: relative;
float:left;
}
.banner .banner-circle {
position: absolute;
bottom: 5px;
left: 0;
right: 0;
color: #fff;
}
.banner .banner-circle li{
display:inline-block;
background: rgba(0,0,0,.3);
border-radius: 50%;
padding:5px;
margin:2px;
}
.banner .banner-circle ul {
text-align: center;
}
.banner .banner-circle .selected {
background: rgba(0,0,0,.8);
}
.banner img {
max-width: 100%;
margin: 0;
padding: 0;
}
.table{
width: 100%;
border-collapse: collapse;
margin: 20px 10px;
text-align: center;
}
.table tbody {
background:#fff;
}
.table td,
.table th{
border: 1px solid #1890ff;
padding: 10px;
}
.table thead tr {
background:#1f76b3;
color:#fff;
}
.module-box {
display:flex; /*弹性布局*/
justify-content: space-between;
}
.list-box{
margin: 0 10px;
padding: 20px;
box-shadow: 0 3px 20px 2px rgb(0 0 0 / 30%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
}
.list-box i {
font-size: 40px;
}
.box:nth-child(1) .list-box {
background-image:linear-gradient(#2277ce,#0356ab);
}
.box:nth-child(2) .list-box {
background-image:linear-gradient(#ba5bd8,#9f3fbd);
}
.box:nth-child(3) .list-box{
background-image:linear-gradient(#ea8052,#cf673a);
}
.box:nth-child(4) .list-box {
background-image:linear-gradient(#84f395,#71c24a);
}
.box:nth-child(5) .list-box {
background-image:linear-gradient(#A44A38,#902569);
}
.marginBottom {
margin-bottom: 10px;
}
.pagination{
margin: 10px 0;
}

33
src/utils/dictionary.js Normal file
View File

@ -0,0 +1,33 @@
import { GetDictionaryidDicItems } from '@/api/system/index'
export const getDictionaryItems = async (dicName, refArray, type) => {
const dictionarys = JSON.parse(sessionStorage.getItem('dictionarys'))
if (dictionarys) {
const dictionary = dictionarys.find(item => item.DicName === dicName)
if (dictionary) {
const params = {
DictionaryId: dictionary.Id,
LambdaExp: `IsDeleted==false`,
SelectSorts: '[{ "FieldName": "CreateTime", "IsAsc": false }]',
}
try {
const res = await GetDictionaryidDicItems(params)
if (res.StatusCode === 200) {
if (type === 'number') {
res.Data.forEach(item => {
item.Value = Number(item.Value)
})
}
refArray.value = res.Data
} else {
refArray.value = []
}
} catch (error) {
refArray.value = []
}
} else {
refArray.value = []
}
} else {
refArray.value = []
}
}

45
src/utils/disabledDate.js Normal file
View File

@ -0,0 +1,45 @@
import { computed } from 'vue'
const now = computed(() => new Date())
export function disabledDateAge(time) {
const year = now.value.getFullYear() - 100
const minDate = new Date(Date.UTC(year, 0))
const maxDate = new Date(now.value.getTime())
const isBeforeMin = time.getTime() < minDate.getTime()
const isAfterMax = time.getTime() > maxDate.getTime()
return isBeforeMin || isAfterMax
}
export function disabledDate2020Start(time) {
return 2020 - time.getFullYear() > 0 || time.getTime() > Date.now()
}
export function disabledDate(time, month = 0) {
const minDate = new Date(Date.UTC(1901, 0));
const currentDate = new Date(now.value.getTime());
const year = currentDate.getFullYear();
const monthValue = currentDate.getMonth();
const newMonth = monthValue - month;
const adjustedYear = year + Math.floor(newMonth / 12);
const adjustedMonth = ((newMonth % 12) + 12) % 12;
const maxDate = new Date(Date.UTC(adjustedYear, adjustedMonth + 1, 0));
return time.getTime() < minDate.getTime() || time.getTime() > maxDate.getTime();
}
export function disabledDayRange(time, beforeDays = 0, afterDays = 0) {
const currentTime = now.value.getTime();
const minDate = beforeDays > 0
? new Date(currentTime - beforeDays * 86400000)
: new Date(Date.UTC(1901, 0, 1));
const maxDate = afterDays > 0
? new Date(currentTime + afterDays * 86400000)
: new Date(currentTime);
minDate.setHours(0, 0, 0, 0);
maxDate.setHours(23, 59, 59, 999);
const checkDate = new Date(time);
checkDate.setHours(0, 0, 0, 0);
return checkDate.getTime() < minDate.getTime() ||
checkDate.getTime() > maxDate.getTime();
}

View File

@ -0,0 +1,10 @@
import axios from 'axios';
export default async function fetchPublicIp() {
try {
const response = await axios.get('https://ipinfo.io/json');
return response.data;
} catch (error) {
console.error('error', error);
return null;
}
}

22
src/utils/formatDate.js Normal file
View File

@ -0,0 +1,22 @@
export default function formatDate(date, format) {
const d = new Date(date);
const year = d.getFullYear();
const month = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
const hours = d.getHours().toString().padStart(2, '0');
const minutes = d.getMinutes().toString().padStart(2, '0');
const seconds = d.getSeconds().toString().padStart(2, '0');
switch (format) {
case 'YYYY-MM-DD':
return `${year}-${month}-${day}`;
case 'YYYY':
return `${year}`;
case 'YYYY-MM':
return `${year}-${month}`;
case 'YYYY-MM-DD HH:MM:SS':
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
default:
return date;
}
}

41
src/utils/permission.js Normal file
View File

@ -0,0 +1,41 @@
// src/utils/permission.js
export function extractButtonPermissions(routers) {
const buttonPermissions = {};
function traverse(nodes) {
nodes.forEach(node => {
if (node.ResType === "按钮") {
const parentPath = findParentPath(routers, node.ParentId);
if (parentPath) {
if (!buttonPermissions[parentPath]) {
buttonPermissions[parentPath] = [];
}
buttonPermissions[parentPath].push(node.AliasName);
}
}
if (node.Children && node.Children.length > 0) {
traverse(node.Children);
}
});
}
traverse(routers);
return buttonPermissions;
}
function findParentPath(nodes, parentId) {
for (const node of nodes) {
if (node.Id === parentId) {
return node.Path;
}
if (node.Children && node.Children.length > 0) {
const path = findParentPath(node.Children, parentId);
if (path) return path;
}
}
return null;
}
export function hasPermission(buttonPermissions, pagePath, buttonAlias) {
return buttonPermissions[pagePath]?.includes(buttonAlias) || false;
}

16
src/utils/regexp.js Normal file
View File

@ -0,0 +1,16 @@
// 正则表达式
const regexPatterns = {
phone: /^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[1589]))\d{8}$/,
passwordDifficult: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,16}$/,
passwordMedium: /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[^\w\s\d])([^\s]){8,16}$/,
passwordSimple: /^(?=.*[a-zA-Z])(?=.*\d)[A-Za-z\d]{8,16}$/,
name: /^([\u4E00-\u9FA5]{2,10})$/,
idCard: /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dX]$/,
number: /^\d{1,10}$/,
numberDate: /^([1-9]|[12][0-9]|3[01])$/,
numberDecimal: /^-?\d{1,10}(\.\d{1,2})?$/,
bankCard: /^\d{16,19}$/
};
export default regexPatterns;

10
src/utils/rem.js Normal file
View File

@ -0,0 +1,10 @@
const baseSize = 16
function setRem () {
const scale = document.documentElement.clientWidth / 1920
let fontSize = (baseSize * Math.min(scale, 2))>12 ? (baseSize * Math.min(scale, 2)): 12
document.documentElement.style.fontSize = fontSize + 'px'
}
setRem()
window.onresize = function () {
setRem()
}

69
src/utils/request.js Normal file
View File

@ -0,0 +1,69 @@
import axios from "axios";
import { showMessage } from '@/common/message'
import { ElMessageBox } from 'element-plus';
const baseURL = import.meta.env.MODE === 'development' ? '' : import.meta.env.VITE_BASE_URL;
const service = axios.create({
baseURL: baseURL,
withCredentials: true, // 必须携带 Cookie
headers: {
'Content-Type': 'application/json' // 避免触发复杂预检
}
});
service.interceptors.request.use(
async (config) => {
const tokenStr = sessionStorage.getItem('token');
const token = tokenStr ? tokenStr : '';
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
//响应拦截器--从服务器响应之后--得到的结果,优先处理
service.interceptors.response.use(
response => {
const handle401Error = () => {
ElMessageBox.alert('登录已过期,请重新登录,点击确认将退出系统。', '提示', {
confirmButtonText: '确认',
showClose: false,
callback: () => {
window.open(import.meta.env.VITE_HOME_URL + '?access=exit', '_self');
},
});
return Promise.reject(new Error(`请求失败,状态码: 401`));
};
if (response.status !== 200) {
return Promise.reject(new Error(`请求失败,状态码: ${response.status}`));
}
if (response.data.StatusCode === 401 ||
(response.data.Message && response.data.Message.includes('401'))) {
return handle401Error();
}
return response.data;
},
async error => {
if (error.response?.status === 401) {
ElMessageBox.alert('登录已过期,请重新登录,点击确认将退出系统。', '提示', {
confirmButtonText: '确认',
callback: () => {
window.open(import.meta.env.VITE_HOME_URL + '?access=exit', '_self');
},
});
}
showMessage('error', error.message || "请求错误");
return Promise.reject(error);
}
);
export default service;

View File

@ -0,0 +1,59 @@
import axios from "axios";
import { showMessage } from '@/common/message'
import { ElMessageBox } from 'element-plus';
const baseURL = import.meta.env.MODE === 'development' ? '' : import.meta.env.VITE_BASE_URL;
const service = axios.create({
baseURL: baseURL,
withCredentials: true, // 必须携带 Cookie
headers: {
'Content-Type': 'application/json' // 避免触发复杂预检
}
});
service.interceptors.request.use(
async (config) => {
const tokenStr = sessionStorage.getItem('token');
const token = tokenStr ? tokenStr : '';
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
service.interceptors.response.use(response => {
if (response.status === 200) {
if (response.data.StatusCode === 401) {
ElMessageBox.alert('登录已过期,请重新登录,点击确认将退出系统。', '提示', {
confirmButtonText: '确认',
callback: () => {
window.open(import.meta.env.VITE_HOME_URL + '?access=exit', '_self')
},
})
return Promise.reject(new Error(`请求失败,状态码: ${response.data.StatusCode}`));
}
return response.data;
} else {
return Promise.reject(new Error(`请求失败,状态码: ${response.status}`));
}
}, async (error) => {
if (error.response && error.response.status === 401) {
ElMessageBox.alert('登录已过期,请重新登录,点击确认将退出系统。', '提示', {
confirmButtonText: '确认',
callback: () => {
window.open(import.meta.env.VITE_HOME_URL + '?access=exit', '_self')
},
})
}
showMessage('error', error.message || "请求错误");
return Promise.reject(error);
});
export default service;

40
src/utils/svgIcon.js Normal file
View File

@ -0,0 +1,40 @@
import { readFileSync, readdirSync } from 'fs'
import { resolve } from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
/**
* 获取所有 SVG 图标
* @param dir 图标目录
*/
export function getSvgIcons(dir) {
try {
const icons = []
const files = readdirSync(dir)
files.forEach((file) => {
if (file.endsWith('.svg')) {
const content = readFileSync(resolve(dir, file), 'utf-8')
icons.push({
name: file.replace('.svg', ''),
content
})
}
})
return icons
} catch (error) {
console.error('Error reading SVG files:', error)
return []
}
}
/**
* Vite SVG 插件配置
* @param {string} iconDirs 图标目录
*/
export function configSvgIconsPlugin(iconDirs) {
return createSvgIconsPlugin({
iconDirs: [iconDirs],
symbolId: 'icon-[dir]-[name]',
inject: 'body-last',
customDomId: '__svg__icons__dom__'
})
}

10
src/utils/utils.js Normal file
View File

@ -0,0 +1,10 @@
export const scrollToFirstError = () => {
const isError = document.getElementsByClassName('is-error');
if (isError[0]) {
isError[0].scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
};

27
src/views/error/404.vue Normal file
View File

@ -0,0 +1,27 @@
<template>
<div class="not-found">
<img class="layout_img" src="@/assets/images/error/404_1.png" alt="404" />
<el-button type="primary" @click="router.push('/home')">返回首页</el-button>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<style scoped lang="scss">
.not-found {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.layout_img {
width: 800px;
height: 600px;
}
}
</style>

181
src/views/home/index.vue Normal file
View File

@ -0,0 +1,181 @@
<style lang="scss" scoped>
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px white inset !important;
-webkit-text-fill-color: #000 !important;
transition: background-color 5000s ease-in-out 0s !important;
}
.card_title {
font-weight: 500;
font-size: 16px;
color: rgba(0, 0, 0, 0.88);
}
.card_header_count {
margin: 5px;
padding: 5px 6px;
background: rgba(0, 0, 0, 0.04);
border-radius: 6px;
font-weight: 600;
font-size: 12px;
color: rgba(0, 0, 0, 0.88);
}
.todos_card {
.card_content {
padding: 0 10px;
height: 65vh;
width: 100%;
overflow-y: auto;
.card_content_item {
margin-bottom: 10px;
padding: 10px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.15);
display: flex;
justify-content: space-between;
align-items: center;
.card_content_item_left {
.card_content_item_left_top {
display: flex;
.title {
font-weight: 600;
font-size: 14px;
color: rgba(0, 10, 26, 0.89);
}
.text {
padding-left: 10px;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
}
}
.card_content_item_left_bottom {
margin-top: 10px;
font-weight: 400;
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
}
}
}
}
.notice_card {
.card_content {
padding: 0 10px;
height: 65vh;
width: 100%;
overflow-y: auto;
.card_content_item {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.card_content_item_left {
display: flex;
align-items: flex-start;
.card_content_item_tag {
width: 40px;
margin-right: 10px;
padding: 2px 8px;
background: #f5f5f5;
font-weight: 400;
font-size: 12px;
color: #000000;
border-radius: 4px;
}
.card_content_item_text {
width: 280px;
display: inline-block;
font-weight: 400;
font-size: 14px;
color: #000;
}
}
.card_content_item_right {
width: 90px;
font-weight: 400;
font-size: 14px;
color: #141414;
}
}
}
}
</style>
<template>
<div class="overall-container">
<!-- <el-row :gutter="20">
<el-col :span="16">
<el-card class="todos_card" :style="{ height: cardHeight }">
<template #header>
<div class="card-header">
<span class="card_title">代办事项</span>
<span class="card_header_count">{{ toDos.length }}</span>
</div>
</template>
<div class="card_content">
<div class="card_content_item" v-for="(item, index) in toDos" :key="index">
<div class="card_content_item_left">
<div class="card_content_item_left_top">
<div class="title">{{ item.title }}</div>
<div class="text">{{ item.text }}</div>
</div>
<div class="card_content_item_left_bottom">{{ item.date }}</div>
</div>
<div class="card_content_item_right">
<el-button type="primary" plain>去处理</el-button>
</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="notice_card" :style="{ height: cardHeight }">
<template #header>
<div class="card-header">
<span class="card_title">通知</span>
</div>
</template>
<div class="card_content">
<div class="card_content_item" v-for="(item, index) in notice" :key="index">
<div class="card_content_item_left">
<span class="card_content_item_tag">{{ item.tag }}</span>
<span class="card_content_item_text">{{ item.text }}</span>
</div>
<div class="card_content_item_right">{{ item.date }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row> -->
</div>
</template>
<script setup>
// import { ref } from 'vue'
// const cardHeight = ref('75vh')
// const toDos = ref([])
// const notice = ref([
// {
// tag: '',
// text: '0.1',
// date: '2025-05-20',
// },
// ])
</script>

105
vite.config.js Normal file
View File

@ -0,0 +1,105 @@
import { fileURLToPath, URL } from 'url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import postCssPxToRem from 'postcss-pxtorem'
import { resolve } from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [
vue(),
vueJsx(),
createSvgIconsPlugin({
iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
symbolId: 'icon-[dir]-[name]',
}),
],
productionSourceMap: false,
configureWebpack: {
devtool: false
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
port: 3002,
host: '0.0.0.0',
https: false,
proxy: {
// 系统管理服务19901
'/api/sys': {
target: env.VITE_BASE_URL_SYSTEM,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/sys/, '/api'),
secure: false,
onProxyReq: (proxyReq, req, res) => {
console.log('Proxying request:');
console.log(` Method: ${req.method}`);
console.log(` Path: ${proxyReq.path}`);
console.log(' Request Headers:', req.headers);
}
},
'/auth': {
target: env.VITE_BASE_URL_SYSTEM,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/auth/, ''),
secure: false,
onProxyReq: (proxyReq, req, res) => {
console.log('Proxying request:');
console.log(` Method: ${req.method}`);
console.log(` Path: ${proxyReq.path}`);
console.log(' Request Headers:', req.headers);
}
},
// 业务系统服务19903
'/api/lmg': {
target: env.VITE_BASE_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/lmg/, '/api'),
secure: false,
onProxyReq: (proxyReq, req, res) => {
console.log('Proxying request:');
console.log(` Method: ${req.method}`);
console.log(` Path: ${proxyReq.path}`);
console.log(' Request Headers:', req.headers);
}
}
}
},
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler',
additionalData: `
@use "@/assets/scss/variables" as v;
@use "@/assets/scss/mixins" as m;
`
}
},
postcss: {
plugins: [
postCssPxToRem({
rootValue: 16,
propList: ['*'],
selectorBlackList: ['no-rem'],
replace: true,
mediaQuery: false,
minPixelValue: 0
}),
]
}
},
define: {
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: JSON.stringify(false),
},
esbuild: {
drop: ['console', 'debugger']
}
};
});

5194
yarn.lock Normal file

File diff suppressed because it is too large Load Diff