| New file |
| | |
| | | [*.{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 |
| New file |
| | |
| | | * text=auto eol=lf |
| New file |
| | |
| | | # 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 |
| | | |
| | | # Editor directories and files |
| | | .vscode/* |
| | | !.vscode/extensions.json |
| | | .idea |
| | | *.suo |
| | | *.ntvs* |
| | | *.njsproj |
| | | *.sln |
| | | *.sw? |
| | | |
| | | *.tsbuildinfo |
| | | |
| | | .eslintcache |
| | | |
| | | # Cypress |
| | | /cypress/videos/ |
| | | /cypress/screenshots/ |
| | | |
| | | # Vitest |
| | | __screenshots__/ |
| New file |
| | |
| | | { |
| | | "$schema": "https://json.schemastore.org/prettierrc", |
| | | "semi": false, |
| | | "singleQuote": true, |
| | | "printWidth": 100 |
| | | } |
| New file |
| | |
| | | { |
| | | "recommendations": [ |
| | | "Vue.volar", |
| | | "dbaeumer.vscode-eslint", |
| | | "EditorConfig.EditorConfig", |
| | | "esbenp.prettier-vscode" |
| | | ] |
| | | } |
| | |
| | | ## app-web-examination-home |
| | | |
| | | 广东省考务系统 |
| | | |
| | | # 技能人才评价考务管理系统 |
| New file |
| | |
| | | import js from '@eslint/js' |
| | | import pluginVue from 'eslint-plugin-vue' |
| | | import prettier from '@vue/eslint-config-prettier' |
| | | |
| | | export default [ |
| | | js.configs.recommended, |
| | | ...pluginVue.configs['flat/essential'], |
| | | prettier, |
| | | |
| | | { |
| | | files: ['**/*.{js,mjs,jsx,vue}'], |
| | | languageOptions: { |
| | | ecmaVersion: 'latest', |
| | | sourceType: 'module', |
| | | globals: { |
| | | console: 'readonly', |
| | | process: 'readonly', |
| | | Buffer: 'readonly', |
| | | __dirname: 'readonly', |
| | | __filename: 'readonly', |
| | | module: 'readonly', |
| | | require: 'readonly', |
| | | exports: 'readonly', |
| | | global: 'readonly', |
| | | window: 'readonly', |
| | | document: 'readonly', |
| | | navigator: 'readonly', |
| | | location: 'readonly', |
| | | history: 'readonly', |
| | | localStorage: 'readonly', |
| | | sessionStorage: 'readonly', |
| | | setTimeout: 'readonly', |
| | | clearTimeout: 'readonly', |
| | | setInterval: 'readonly', |
| | | clearInterval: 'readonly', |
| | | fetch: 'readonly', |
| | | alert: 'readonly', |
| | | confirm: 'readonly', |
| | | prompt: 'readonly', |
| | | }, |
| | | }, |
| | | rules: { |
| | | 'vue/multi-word-component-names': 'off', |
| | | 'no-unused-vars': 'warn', |
| | | 'no-console': 'off', |
| | | 'no-undef': 'off', |
| | | 'prettier/prettier': 'off', |
| | | 'vue/html-self-closing': 'off', |
| | | 'vue/max-attributes-per-line': 'off', |
| | | 'vue/html-indent': 'off', |
| | | 'vue/html-closing-bracket-newline': 'off', |
| | | 'vue/singleline-html-element-content-newline': 'off', |
| | | }, |
| | | }, |
| | | |
| | | { |
| | | ignores: [ |
| | | '**/dist/**', |
| | | '**/dist-ssr/**', |
| | | '**/coverage/**', |
| | | '**/node_modules/**', |
| | | '**/*.min.js', |
| | | ], |
| | | }, |
| | | ] |
| New file |
| | |
| | | <!DOCTYPE html> |
| | | <html lang=""> |
| | | <head> |
| | | <meta charset="UTF-8"> |
| | | <link rel="icon" href="/favicon.ico"> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | | <title>广东省技能人才评价考务管理系统</title> |
| | | </head> |
| | | <body> |
| | | <div id="app"></div> |
| | | <script type="module" src="/src/main.js"></script> |
| | | </body> |
| | | </html> |
| New file |
| | |
| | | { |
| | | "name": "app-web-examination-platform", |
| | | "version": "0.0.0", |
| | | "private": true, |
| | | "type": "module", |
| | | "engines": { |
| | | "node": "^20.19.0 || >=22.12.0" |
| | | }, |
| | | "scripts": { |
| | | "dev": "vite", |
| | | "build": "vite build", |
| | | "preview": "vite preview", |
| | | "lint": "eslint . --fix", |
| | | "format": "prettier --write src/" |
| | | }, |
| | | "dependencies": { |
| | | "@element-plus/icons-vue": "^2.3.2", |
| | | "@iconify/vue": "^5.0.0", |
| | | "axios": "^1.13.2", |
| | | "dayjs": "^1.11.19", |
| | | "element-plus": "^2.11.8", |
| | | "file-saver": "^2.0.5", |
| | | "pinia": "^3.0.3", |
| | | "qs": "^6.14.1", |
| | | "vue": "^3.5.22", |
| | | "vue-router": "^4.6.3", |
| | | "xlsx": "^0.18.5", |
| | | "xlsx-js-style": "^1.2.0" |
| | | }, |
| | | "devDependencies": { |
| | | "@vitejs/plugin-vue": "^6.0.1", |
| | | "@vue/eslint-config-prettier": "^10.2.0", |
| | | "eslint": "^9.37.0", |
| | | "eslint-plugin-vue": "~10.5.0", |
| | | "npm-run-all2": "^8.0.4", |
| | | "prettier": "3.6.2", |
| | | "vite": "^7.1.11", |
| | | "vite-plugin-vue-devtools": "^8.0.3" |
| | | } |
| | | } |
| New file |
| | |
| | | <template> |
| | | <div id="app"> |
| | | <router-view /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | // import { useWindowSize } from '@/utils/hook.js' |
| | | |
| | | // const { width, height } = useWindowSize() |
| | | </script> |
| | | |
| | | <style scoped> |
| | | #app { |
| | | width: 100vw; |
| | | height: 100vh; |
| | | margin: 0; |
| | | padding: 0; |
| | | } |
| | | </style> |
| New file |
| | |
| | | :root { |
| | | --el-color-primary: #007aff; |
| | | --el-text-color-regular: #333 !important; |
| | | --el-text-color-secondary: #666 !important; |
| | | } |
| New file |
| | |
| | | /* ================================ |
| | | 全局样式重置 |
| | | ================================ */ |
| | | * { |
| | | margin: 0; |
| | | padding: 0; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | html { |
| | | font-size: 16px; |
| | | line-height: 1.5; |
| | | -webkit-text-size-adjust: 100%; |
| | | -webkit-font-smoothing: antialiased; |
| | | -moz-osx-font-smoothing: grayscale; |
| | | } |
| | | |
| | | body { |
| | | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', |
| | | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', |
| | | sans-serif; |
| | | font-size: 1rem; |
| | | line-height: 1.5; |
| | | color: #333; |
| | | background-color: #fff; |
| | | width: 100%; |
| | | height: 100%; |
| | | overflow-x: hidden; |
| | | } |
| | | |
| | | .main-content { |
| | | width: 100%; |
| | | max-width: 1280px; |
| | | padding: 0 40px; |
| | | margin: 0 auto; |
| | | } |
| | | |
| | | /* ================================ |
| | | 根容器样式 |
| | | ================================ */ |
| | | #app { |
| | | width: 100vw; |
| | | height: 100vh; |
| | | position: relative; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* ================================ |
| | | 滚动条样式 |
| | | ================================ */ |
| | | ::-webkit-scrollbar { |
| | | width: 6px; |
| | | height: 6px; |
| | | } |
| | | |
| | | ::-webkit-scrollbar-track { |
| | | background: #f1f1f1; |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | ::-webkit-scrollbar-thumb { |
| | | background: #c1c1c1; |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | ::-webkit-scrollbar-thumb:hover { |
| | | background: #a8a8a8; |
| | | } |
| | | |
| | | /* ================================ |
| | | 间距工具类 |
| | | ================================ */ |
| | | .m-0 { margin: 0; } |
| | | .m-1 { margin: 4px; } |
| | | .m-2 { margin: 8px; } |
| | | .m-3 { margin: 12px; } |
| | | .m-4 { margin: 16px; } |
| | | .m-5 { margin: 20px; } |
| | | .m-6 { margin: 24px; } |
| | | .m-7 { margin: 28px; } |
| | | |
| | | .mx-0 { margin-left: 0; margin-right: 0; } |
| | | .mx-1 { margin-left: 4px; margin-right: 4px; } |
| | | .mx-2 { margin-left: 8px; margin-right: 8px; } |
| | | .mx-3 { margin-left: 12px; margin-right: 12px; } |
| | | .mx-4 { margin-left: 16px; margin-right: 16px; } |
| | | .mx-5 { margin-left: 20px; margin-right: 20px; } |
| | | .mx-6 { margin-left: 24px; margin-right: 24px; } |
| | | .mx-7 { margin-left: 28px; margin-right: 28px; } |
| | | |
| | | .my-0 { margin-top: 0; margin-bottom: 0;} |
| | | .my-1 { margin-top: 4px; margin-bottom: 4px;} |
| | | .my-2 { margin-top: 8px; margin-bottom: 8px;} |
| | | .my-3 { margin-top: 12px; margin-bottom: 12px; } |
| | | .my-4 { margin-top: 16px; margin-bottom: 16px; } |
| | | .my-5 { margin-top: 20px; margin-bottom: 20px; } |
| | | .my-6 { margin-top: 24px; margin-bottom: 24px; } |
| | | .my-7 { margin-top: 28px; margin-bottom: 28px; } |
| | | |
| | | .mt-0 { margin-top: 0; } |
| | | .mt-1 { margin-top: 4px; } |
| | | .mt-2 { margin-top: 8px; } |
| | | .mt-3 { margin-top: 12px; } |
| | | .mt-4 { margin-top: 16px; } |
| | | .mt-5 { margin-top: 20px; } |
| | | .mt-6 { margin-top: 24px; } |
| | | .mt-7 { margin-top: 28px; } |
| | | |
| | | .mb-0 { margin-bottom: 0; } |
| | | .mb-1 { margin-bottom: 4px; } |
| | | .mb-2 { margin-bottom: 8px; } |
| | | .mb-3 { margin-bottom: 12px; } |
| | | .mb-4 { margin-bottom: 16px; } |
| | | .mb-5 { margin-bottom: 20px; } |
| | | .mb-6 { margin-bottom: 24px; } |
| | | .mb-7 { margin-bottom: 28px; } |
| | | |
| | | .ml-0 { margin-left: 0; } |
| | | .ml-1 { margin-left: 4px; } |
| | | .ml-2 { margin-left: 8px; } |
| | | .ml-3 { margin-left: 12px; } |
| | | .ml-4 { margin-left: 16px; } |
| | | .ml-5 { margin-left: 20px; } |
| | | .ml-6 { margin-left: 24px; } |
| | | .ml-7 { margin-left: 28px; } |
| | | |
| | | .mr-0 { margin-right: 0; } |
| | | .mr-1 { margin-right: 4px; } |
| | | .mr-2 { margin-right: 8px; } |
| | | .mr-3 { margin-right: 12px; } |
| | | .mr-4 { margin-right: 16px; } |
| | | .mr-5 { margin-right: 20px; } |
| | | .mr-6 { margin-right: 24px; } |
| | | .mr-7 { margin-right: 28px; } |
| | | |
| | | .p-0 { padding: 0; } |
| | | .p-1 { padding: 4px; } |
| | | .p-2 { padding: 8px; } |
| | | .p-3 { padding: 12px; } |
| | | .p-4 { padding: 16px; } |
| | | .p-5 { padding: 20px; } |
| | | .p-6 { padding: 24px; } |
| | | .p-7 { padding: 28px; } |
| | | |
| | | .px-0 { padding-left: 0; padding-right: 0; } |
| | | .px-1 { padding-left: 4px; padding-right: 4px; } |
| | | .px-2 { padding-left: 8px; padding-right: 8px; } |
| | | .px-3 { padding-left: 12px; padding-right: 12px; } |
| | | .px-4 { padding-left: 16px; padding-right: 16px; } |
| | | .px-5 { padding-left: 20px; padding-right: 20px; } |
| | | .px-6 { padding-left: 24px; padding-right: 24px; } |
| | | .px-7 { padding-left: 28px; padding-right: 28px; } |
| | | |
| | | .py-0 { padding-top: 0; padding-bottom: 0;} |
| | | .py-1 { padding-top: 4px; padding-bottom: 4px;} |
| | | .py-2 { padding-top: 8px; padding-bottom: 8px;} |
| | | .py-3 { padding-top: 12px; padding-bottom: 12px;} |
| | | .py-4 { padding-top: 16px; padding-bottom: 16px;} |
| | | .py-5 { padding-top: 20px; padding-bottom: 20px;} |
| | | .py-6 { padding-top: 24px; padding-bottom: 24px;} |
| | | .py-7 { padding-top: 28px; padding-bottom: 28px;} |
| | | |
| | | .pt-0 { padding-top: 0; } |
| | | .pt-1 { padding-top: 4px; } |
| | | .pt-2 { padding-top: 8px; } |
| | | .pt-3 { padding-top: 12px; } |
| | | .pt-4 { padding-top: 16px; } |
| | | .pt-5 { padding-top: 20px; } |
| | | .pt-6 { padding-top: 24px; } |
| | | .pt-7 { padding-top: 28px; } |
| | | |
| | | .pb-0 { padding-bottom: 0; } |
| | | .pb-1 { padding-bottom: 4px; } |
| | | .pb-2 { padding-bottom: 8px; } |
| | | .pb-3 { padding-bottom: 12px; } |
| | | .pb-4 { padding-bottom: 16px; } |
| | | .pb-5 { padding-bottom: 20px; } |
| | | .pb-6 { padding-bottom: 24px; } |
| | | .pb-7 { padding-bottom: 28px; } |
| | | |
| | | .pl-0 { padding-left: 0; } |
| | | .pl-1 { padding-left: 4px; } |
| | | .pl-2 { padding-left: 8px; } |
| | | .pl-3 { padding-left: 12px; } |
| | | .pl-4 { padding-left: 16px; } |
| | | .pl-5 { padding-left: 20px; } |
| | | .pl-6 { padding-left: 24px; } |
| | | .pl-7 { padding-left: 28px; } |
| | | |
| | | .pr-0 { padding-right: 0; } |
| | | .pr-1 { padding-right: 4px; } |
| | | .pr-2 { padding-right: 8px; } |
| | | .pr-3 { padding-right: 12px; } |
| | | .pr-4 { padding-right: 16px; } |
| | | .pr-5 { padding-right: 20px; } |
| | | .pr-6 { padding-right: 24px; } |
| | | .pr-7 { padding-right: 28px; } |
| | | |
| | | /* ================================ |
| | | 文本工具类 |
| | | ================================ */ |
| | | .text-center { text-align: center; } |
| | | .text-left { text-align: left; } |
| | | .text-right { text-align: right; } |
| | | |
| | | .text-xs { font-size: 12px; } |
| | | .text-sm { font-size: 13px; } |
| | | .text-base { font-size: 14px; } |
| | | .text-lg { font-size: 16px; } |
| | | .text-xl { font-size: 18px; } |
| | | .text-2xl { font-size: 20px; } |
| | | .text-3xl { font-size: 22px; } |
| | | |
| | | .font-normal { font-weight: 400; } |
| | | .font-medium { font-weight: 500; } |
| | | .font-semibold { font-weight: 600; } |
| | | .font-bold { font-weight: 700; } |
| | | |
| | | .text-primary { color: #007AFF; } |
| | | .text-success { color: #67c23a; } |
| | | .text-warning { color: #e6a23c; } |
| | | .text-danger { color: #f56c6c; } |
| | | .text-info { color: #909399; } |
| | | .text-white { color: #fff; } |
| | | .text-note { color: #333 } |
| | | .text-black { color: #000; } |
| | | .text-red {color: #FF0000 } |
| | | |
| | | .flex-1 { |
| | | flex: 1; |
| | | } |
| | | |
| | | /* .el-button:hover, |
| | | .el-button:focus{ |
| | | outline: none !important; |
| | | } |
| | | */ |
| | | .cursor-p { |
| | | cursor: pointer;; |
| | | } |
| | | |
| | | .el-tooltip__trigger:focus-visible { |
| | | outline: unset !important; |
| | | } |
| | | |
| | | .el-tabs__header { |
| | | margin: 0px; |
| | | } |
| | | /* el-tabs ui */ |
| | | /* .el-tabs__header.is-top { |
| | | height: 50px; |
| | | } */ |
| | | |
| | | .el-tabs__active-bar { |
| | | /* margin-bottom: -3px !important; */ |
| | | /* margin-top: -3px !important; */ |
| | | } |
| | | |
| | | /* el-card ui */ |
| | | .el-card__header { |
| | | padding: 6px 12px; |
| | | } |
| | | |
| | | .el-card__body { |
| | | padding: 0px; |
| | | padding-bottom: 12px; |
| | | } |
| | | |
| | | .el-upload__tip { |
| | | display: inline; |
| | | margin-left: 8px; |
| | | } |
| | | |
| | | .el-card__body { |
| | | padding-bottom: 0; |
| | | } |
| New file |
| | |
| | | export default { |
| | | |
| | | } |
| New file |
| | |
| | | let baseDomain = `${location.protocol}//${location.host}` |
| | | let qxyRes = 'https://res.qxueyou.com/' |
| | | let serverContext = '/admin-api' |
| | | let htmlContext = '/examination/user' |
| | | |
| | | export default { |
| | | domain: baseDomain, |
| | | qxyRes: qxyRes, |
| | | baseUrl: serverContext, |
| | | htmlRoot: baseDomain + htmlContext, |
| | | serverRoot: baseDomain + serverContext, |
| | | upload: `${serverContext}/base/file/upload`, |
| | | ACCESS_TOKEN_KEY: 'accessToken', |
| | | REFRESH_TOKEN_KEY: 'refreshToken' |
| | | } |
| New file |
| | |
| | | import { createApp } from 'vue' |
| | | import { createPinia } from 'pinia' |
| | | import ElementPlus from 'element-plus' |
| | | import 'element-plus/dist/index.css' |
| | | import './assets/styles/global.css' |
| | | import './assets/styles/elementTheme.css' |
| | | import { ruleGenerator } from '@/utils/ruleGenerator' |
| | | import { getImageUrl, getDictData, getOccupationName, getJobName } from '@/utils/tool' |
| | | import * as ElementPlusIconsVue from '@element-plus/icons-vue' |
| | | import zhCn from 'element-plus/es/locale/lang/zh-cn' |
| | | import dayjs from 'dayjs' |
| | | import 'dayjs/locale/zh-cn' |
| | | import App from './App.vue' |
| | | import router from './router' |
| | | import axios from '@/utils/axios.js' |
| | | import { Icon } from '@iconify/vue' |
| | | import DataTable from '@/views/main/components/DataTable.vue' |
| | | import Filtrate from '@/views/main/components/Filtrate.vue' |
| | | import ReturnBtn from '@/views/main/components/ReturnBtn.vue' |
| | | import DictTag from '@/views/main/components/DictTag.vue' |
| | | import UploadBtn from '@/views/main/components/UploadBtn.vue' |
| | | |
| | | import property from '@/config/property.js' |
| | | import qxueyou from '@/config/qxueyou.js' |
| | | |
| | | const app = createApp(App) |
| | | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { |
| | | app.component(key, component) |
| | | } |
| | | app.config.globalProperties.$rules = ruleGenerator |
| | | app.config.globalProperties.$property = property |
| | | app.config.globalProperties.$qxueyou = qxueyou |
| | | app.config.globalProperties.$axios = axios |
| | | app.config.globalProperties.$dayjs = dayjs |
| | | app.config.globalProperties.$getImageUrl = getImageUrl |
| | | app.config.globalProperties.$getDictData = getDictData |
| | | app.config.globalProperties.$getOccupationName = getOccupationName |
| | | app.config.globalProperties.$getJobName = getJobName |
| | | |
| | | app.component('DataTable', DataTable); |
| | | app.component('Filtrate', Filtrate); |
| | | app.component('DictTag', DictTag); |
| | | app.component('ReturnBtn', ReturnBtn); |
| | | app.component('Icon', Icon) |
| | | app.component('UploadBtn', UploadBtn) |
| | | |
| | | app.use(ElementPlus, { |
| | | locale: zhCn |
| | | }) |
| | | app.use(createPinia()) |
| | | app.use(router) |
| | | |
| | | app.mount('#app') |
| New file |
| | |
| | | const router = [ |
| | | { |
| | | path: '/auth/login', |
| | | name: '登录', |
| | | component: () => import('@/views/login/index.vue'), |
| | | }, |
| | | ] |
| | | export default router |
| New file |
| | |
| | | const router = [ |
| | | { |
| | | path: '/error/:code', |
| | | name: '错误页面', |
| | | component: () => import('@/views/errorPage/index.vue'), |
| | | }, |
| | | ] |
| | | export default router |
| New file |
| | |
| | | import { createRouter, createWebHistory } from 'vue-router' |
| | | import authPage from '@/router/auth/index.js' |
| | | import errorPage from '@/router/error/index.js' |
| | | import mainPage from '@/router/main/index.js' |
| | | |
| | | const router = createRouter({ |
| | | history: createWebHistory(import.meta.env.BASE_URL), |
| | | routes: [...mainPage, ...authPage, ...errorPage], |
| | | }) |
| | | |
| | | router.beforeEach((to, from, next) => { |
| | | if (!to.matched.length) { |
| | | if (to.path === '/') { |
| | | next({ path: '/main/home' }) |
| | | } else { |
| | | next({ path: '/error/404', query: { errorUrl: to.path } }) |
| | | } |
| | | } else { |
| | | next() |
| | | } |
| | | }) |
| | | |
| | | export default router |
| New file |
| | |
| | | const home = [ |
| | | { |
| | | path: 'home', |
| | | name: '首页', |
| | | component: () => import('@/views/main/home/index.vue'), |
| | | }, |
| | | ] |
| | | |
| | | export default home |
| New file |
| | |
| | | import home from '@/router/main/home/index.js' |
| | | import notice from '@/router/main/notice/index.js' |
| | | |
| | | const mainRouter = [ |
| | | ...home, |
| | | ...notice |
| | | ] |
| | | const router = [ |
| | | { |
| | | path: '/main', |
| | | name: '用户端', |
| | | component: () => import('@/views/main/index.vue'), |
| | | children: mainRouter, |
| | | }, |
| | | ] |
| | | export default router |
| New file |
| | |
| | | const notice = [ |
| | | { |
| | | path: 'noticeList', |
| | | name: '公告列表', |
| | | component: () => import('@/views/main/notice/list.vue'), |
| | | }, |
| | | { |
| | | path: 'noticeDetail', |
| | | name: '公告详情', |
| | | component: () => import('@/views/main/notice/detail.vue'), |
| | | }, |
| | | ] |
| | | |
| | | export default notice |
| New file |
| | |
| | | import { ref } from 'vue' |
| | | import { defineStore } from 'pinia' |
| | | export const useLoginStore = defineStore('login', () => { |
| | | const loginDialogVisible = ref(false) |
| | | |
| | | function setLoginDialogVisible(visible) { |
| | | loginDialogVisible.value = visible |
| | | } |
| | | |
| | | return { loginDialogVisible, setLoginDialogVisible } |
| | | }) |
| New file |
| | |
| | | import { ref } from 'vue' |
| | | import { defineStore } from 'pinia' |
| | | export const useOptionItemsStore = defineStore('optionItems', () => { |
| | | const cityItems = ref([]) |
| | | const occupationItems = ref([]) |
| | | |
| | | function setCityItems(data) { |
| | | cityItems.value = data |
| | | } |
| | | function setOccupationItems(data) { |
| | | occupationItems.value = data |
| | | } |
| | | |
| | | return { cityItems, setCityItems, occupationItems, setOccupationItems } |
| | | }) |
| New file |
| | |
| | | import { ref } from 'vue' |
| | | import { defineStore } from 'pinia' |
| | | export const useSessionStore = defineStore('session', () => { |
| | | const userInfo = ref({}) |
| | | |
| | | function setUserInfo(data) { |
| | | userInfo.value = data |
| | | } |
| | | |
| | | return { userInfo, setUserInfo } |
| | | }) |
| New file |
| | |
| | | import axios from 'axios' |
| | | import $qxueyou from '@/config/qxueyou.js' |
| | | import { getUUID } from "@/utils/tool.js"; |
| | | |
| | | let createAxios = axios.create({ |
| | | baseURL: $qxueyou.serverRoot, |
| | | timeout: 30000, |
| | | headers: { |
| | | "Content-Type": `application/json;charset=utf-8` |
| | | } |
| | | }) |
| | | |
| | | createAxios.all = axios.all |
| | | createAxios.spread = axios.spread |
| | | |
| | | // Token 工具函数 |
| | | const ACCESS_TOKEN_KEY = $qxueyou.ACCESS_TOKEN_KEY; |
| | | const REFRESH_TOKEN_KEY = $qxueyou.REFRESH_TOKEN_KEY; |
| | | export const tokenUtils = { |
| | | // 获取 Access Token |
| | | getAccessToken() { |
| | | return localStorage.getItem(ACCESS_TOKEN_KEY); |
| | | }, |
| | | // 获取 Refresh Token |
| | | getRefreshToken() { |
| | | return localStorage.getItem(REFRESH_TOKEN_KEY); |
| | | }, |
| | | // 设置 Token |
| | | setTokens(accessToken, refreshToken) { |
| | | if (accessToken) { |
| | | localStorage.setItem(ACCESS_TOKEN_KEY, accessToken); |
| | | } |
| | | if (refreshToken) { |
| | | localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); |
| | | } |
| | | }, |
| | | // 清除 Token |
| | | clearTokens() { |
| | | localStorage.removeItem(ACCESS_TOKEN_KEY); |
| | | localStorage.removeItem(REFRESH_TOKEN_KEY); |
| | | }, |
| | | // 检查 Token 是否存在 |
| | | hasTokens() { |
| | | return !!(this.getAccessToken() && this.getRefreshToken()); |
| | | }, |
| | | } |
| | | let isRefreshing = false; |
| | | // 刷新 token 期间的请求队列 |
| | | let refreshQueue = []; |
| | | // 刷新 Access Token |
| | | export async function refreshAccessToken() { |
| | | const currentRefreshToken = tokenUtils.getRefreshToken(); |
| | | if (!currentRefreshToken) { |
| | | throw new Error("No refresh token available"); |
| | | } |
| | | try { |
| | | const response = await createAxios.post( |
| | | `/system/auth/refresh-token?refreshToken=${currentRefreshToken}` |
| | | ); |
| | | |
| | | if (response.data && response.data.code === 0) { |
| | | const { accessToken, refreshToken: newRefreshToken } = response.data.data; |
| | | tokenUtils.setTokens(accessToken, newRefreshToken); |
| | | return accessToken; |
| | | } else { |
| | | throw new Error("Token refresh failed"); |
| | | } |
| | | } catch (error) { |
| | | console.error('Token refresh error:', error); |
| | | throw error; |
| | | } |
| | | } |
| | | |
| | | let strTrim = function(data) { |
| | | let newData = {...data} |
| | | Object.keys(newData).forEach((key) => { |
| | | if (typeof newData[key] === 'string') { |
| | | newData[key] = newData[key].trim() |
| | | } |
| | | }) |
| | | return newData |
| | | } |
| | | |
| | | //网络请求监听 |
| | | createAxios.interceptors.request.use( |
| | | function(config) { |
| | | // 刷新 token 的请求不需要添加 Authorization header |
| | | if (!config.url.includes('/auth/refresh-token')) { |
| | | config.headers = { |
| | | ...config.headers, |
| | | Authorization: localStorage.getItem('accessToken') |
| | | } |
| | | } |
| | | config.flag = getUUID().toString().slice(-4) |
| | | if (config.data) { |
| | | console.log(`data-${config.flag} `, config.data) |
| | | // config.data = JSON.stringify(config.data) |
| | | } |
| | | if (config.params) { |
| | | console.log(`params-${config.flag} `, config.params) |
| | | // config.params = strTrim(config.params) |
| | | } |
| | | return config |
| | | }, |
| | | function(error) { |
| | | return Promise.reject(error) |
| | | } |
| | | ) |
| | | |
| | | createAxios.interceptors.response.use( |
| | | async response => { |
| | | const resultCode = response.data.code; |
| | | const originalRequest = response.config; |
| | | if (response.data && [500, 400].includes(resultCode)) { |
| | | console.log(response) |
| | | return response |
| | | } |
| | | if (response.data && resultCode == '401') { |
| | | if (!originalRequest._retry && tokenUtils.getRefreshToken) { |
| | | originalRequest._retry = true; |
| | | |
| | | if (isRefreshing) { |
| | | // 如果正在刷新,将请求加入队列 |
| | | return new Promise((resolve, reject) => { |
| | | refreshQueue.push({ resolve, reject, config: originalRequest }); |
| | | }); |
| | | } |
| | | |
| | | isRefreshing = true; |
| | | |
| | | try { |
| | | const newAccessToken = await refreshAccessToken(); |
| | | |
| | | // 处理队列中的请求 |
| | | refreshQueue.forEach(({ resolve, config }) => { |
| | | config.headers.Authorization = newAccessToken; |
| | | resolve(createAxios(config)); |
| | | }); |
| | | refreshQueue = []; |
| | | |
| | | // 重新发送原始请求 |
| | | originalRequest.headers.Authorization = newAccessToken; |
| | | return createAxios(originalRequest) |
| | | } catch (refreshError) { |
| | | console.log(refreshError) |
| | | // 刷新失败,处理队列中的请求 |
| | | refreshQueue.forEach(({ reject }) => { |
| | | reject(refreshError); |
| | | }); |
| | | refreshQueue = []; |
| | | tokenUtils.clearTokens(); |
| | | // $store.commit("session/loginFlag", false); |
| | | // $store.commit("session/userInfo", {}); |
| | | // 可以在这里触发登录页面跳转 |
| | | } finally { |
| | | isRefreshing = false; |
| | | } |
| | | } else { |
| | | tokenUtils.clearTokens(); |
| | | // $store.commit("session/loginFlag", false); |
| | | // $store.commit("session/userInfo", {}); |
| | | } |
| | | } |
| | | |
| | | console.log(`url-${response.config.flag} `, response.config.url, response.data) |
| | | return response |
| | | }, |
| | | error => { |
| | | // store.commit('network/changeNetworkState', false) |
| | | if (!error.response) return Promise.reject(error) |
| | | |
| | | // store.commit('network/status', error.response.status) |
| | | if (401 === error.response.status) { |
| | | // 刷新 token 的请求返回 401 时,直接返回错误,不要抛出新的错误 |
| | | if (error.config && error.config.url && error.config.url.includes('/auth/refresh-token')) { |
| | | return Promise.reject(error) |
| | | } |
| | | // store.commit('session/clearUser') |
| | | // store.commit('endea/clearShareK') |
| | | // store.commit('network/logout') |
| | | throw new Error('登录失效') |
| | | } |
| | | return Promise.reject(error) |
| | | } |
| | | ) |
| | | |
| | | export default createAxios |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| New file |
| | |
| | | import * as XLSX from 'xlsx-js-style'; // 另一种常见的导入方式 |
| | | export const exportToExcel = (headers, list, fileName) => { |
| | | // 定义自定义表头和数据映射 |
| | | const headerData = headers.filter(item => !item.noExport).map(item => item.label); |
| | | const bodyData = list.map(item => { |
| | | let itemList = [] |
| | | headers.forEach(header => { |
| | | if (header.noExport) return |
| | | itemList.push(item[header.value]) |
| | | }) |
| | | return itemList |
| | | }); |
| | | |
| | | // 将表头和数据体组合成一个二维数组 |
| | | const allData = [headerData, ...bodyData]; |
| | | |
| | | // 使用 aoa_to_sheet 方法,它接受二维数组 |
| | | const ws = XLSX.utils.aoa_to_sheet(allData); |
| | | |
| | | const headerStyle = { |
| | | font: { bold: true, sz: 12 }, |
| | | alignment: { horizontal: 'center', vertical: 'center' }, |
| | | } |
| | | |
| | | // 数据单元格样式:居中 |
| | | const cellStyle = { |
| | | alignment: { horizontal: 'center', vertical: 'center' } |
| | | } |
| | | |
| | | // 3. 计算并设置列宽(核心:最大宽度限制下的自适应) |
| | | const maxWidth = 30 // 设置最大列宽(单位:字符数) |
| | | const colWidths = [] |
| | | |
| | | // 遍历每一列计算最大宽度 |
| | | for (let col = 0; col < allData[0].length; col++) { |
| | | let maxLen = 10 // 默认最小宽度 |
| | | for (let row = 0; row < allData.length; row++) { |
| | | const value = allData[row][col] |
| | | if (value != null) { |
| | | // 计算单元格内容长度(中文按2个字符算) |
| | | let len = 0 |
| | | const str = value.toString() |
| | | for (let i = 0; i < str.length; i++) { |
| | | const charCode = str.charCodeAt(i) |
| | | len += (charCode > 255) ? 2 : 1 // 中文字符算2个宽度 |
| | | } |
| | | if (len > maxLen) maxLen = len |
| | | } |
| | | } |
| | | // 应用最大宽度限制 |
| | | colWidths.push({ wch: Math.min(maxLen + 2, maxWidth) }) |
| | | } |
| | | ws['!cols'] = colWidths |
| | | |
| | | // 4. 应用样式到所有单元格 |
| | | const range = XLSX.utils.decode_range(ws['!ref']) |
| | | for (let R = range.s.r; R <= range.e.r; R++) { |
| | | for (let C = range.s.c; C <= range.e.c; C++) { |
| | | const cellAddress = { c: C, r: R } |
| | | const cellRef = XLSX.utils.encode_cell(cellAddress) |
| | | |
| | | if (!ws[cellRef]) ws[cellRef] = {} // 确保单元格存在 |
| | | if (!ws[cellRef].s) ws[cellRef].s = {} // 确保样式对象存在 |
| | | |
| | | // 第一行(表头)应用headerStyle,其他行应用cellStyle |
| | | ws[cellRef].s = R === 0 ? |
| | | { ...ws[cellRef].s, ...headerStyle } : |
| | | { ...ws[cellRef].s, ...cellStyle } |
| | | } |
| | | } |
| | | |
| | | // ... 后续创建工作簿和导出的步骤同上 |
| | | |
| | | const workbook = XLSX.utils.book_new(); |
| | | XLSX.utils.book_append_sheet(workbook, ws, 'Sheet1'); |
| | | |
| | | // 3. 生成Excel文件并保存 |
| | | XLSX.writeFile(workbook, `${fileName || '导出数据'}.xlsx`); // 使用xlsx库的writeFile方法 |
| | | |
| | | }; |
| New file |
| | |
| | | import { ref, onMounted, onUnmounted } from 'vue' |
| | | |
| | | export function useWindowSize() { |
| | | const width = ref(window.innerWidth) |
| | | const height = ref(window.innerHeight) |
| | | |
| | | const handleResize = () => { |
| | | width.value = window.innerWidth |
| | | height.value = window.innerHeight |
| | | } |
| | | |
| | | onMounted(() => { |
| | | window.addEventListener('resize', handleResize) |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | window.removeEventListener('resize', handleResize) |
| | | }) |
| | | |
| | | return { width, height } |
| | | } |
| New file |
| | |
| | | //表单校验规则 |
| | | |
| | | export const ruleGenerator = { |
| | | // 必填字段 |
| | | required(message = '此字段为必填项') { |
| | | return { required: true, message, trigger: 'blur' } |
| | | }, |
| | | |
| | | // 长度限制 |
| | | length(min, max, fieldName = '') { |
| | | const message = fieldName |
| | | ? `${fieldName}长度应在${min}到${max}个字符之间` |
| | | : `长度应在${min}到${max}个字符之间` |
| | | |
| | | return { |
| | | validator: (rule, value, callback) => { |
| | | if (!value || (value.length >= min && value.length <= max)) { |
| | | callback() |
| | | } else { |
| | | callback(new Error(message)) |
| | | } |
| | | }, |
| | | trigger: 'blur' |
| | | } |
| | | }, |
| | | |
| | | // 手机号验证 |
| | | phone() { |
| | | return { |
| | | validator: (rule, value, callback) => { |
| | | const reg = /^1[3-9]\d{9}$/ |
| | | if (!value || reg.test(value)) { |
| | | callback() |
| | | } else { |
| | | callback(new Error('请输入正确的手机号码')) |
| | | } |
| | | }, |
| | | trigger: ['blur', 'change'] |
| | | } |
| | | }, |
| | | //验证码验证 |
| | | code() { |
| | | return { |
| | | validator: (rule, value, callback) => { |
| | | const reg = /^\d{4}$/ |
| | | if (!value || reg.test(value)) { |
| | | callback() |
| | | } else { |
| | | callback(new Error('请输入正确的验证码')) |
| | | } |
| | | }, |
| | | trigger: ['blur', 'change'] |
| | | } |
| | | }, |
| | | password() { |
| | | return { |
| | | validator: (rule, value, callback) => { |
| | | const reg = /^\d{6}$/ |
| | | if (!value || reg.test(value)) { |
| | | callback() |
| | | } else { |
| | | callback(new Error('密码格式为6个字符')) |
| | | } |
| | | }, |
| | | trigger: ['blur', 'change'] |
| | | } |
| | | }, |
| | | |
| | | // 多选框验证 |
| | | checkbox(message) { |
| | | return { |
| | | validator: (rule, value, callback) => { |
| | | if (!value) { |
| | | callback(new Error(message)); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | trigger: 'change' |
| | | } |
| | | }, |
| | | |
| | | // 邮箱验证 |
| | | email() { |
| | | return { |
| | | validator: (rule, value, callback) => { |
| | | const reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ |
| | | if (!value || reg.test(value)) { |
| | | callback() |
| | | } else { |
| | | callback(new Error('请输入正确的邮箱地址')) |
| | | } |
| | | }, |
| | | trigger: 'blur' |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | import { useOptionItemsStore } from '@/stores/optionItems.js'; |
| | | /** |
| | | * 获取 assets/images 目录下的图片URL |
| | | * @param {string} imageName - 图片文件名(包含扩展名) |
| | | * @returns {string} 图片的完整URL |
| | | */ |
| | | export const getImageUrl = (imageName) => { |
| | | try { |
| | | return new URL('../assets/images/' + imageName, import.meta.url).href; |
| | | } catch (error) { |
| | | console.warn(`Failed to load image: ${imageName}`, error); |
| | | return ""; |
| | | } |
| | | }; |
| | | export const getUUID = () => { |
| | | let s = [] |
| | | let num10 = 0x10 |
| | | let num3 = 0x3 |
| | | let num8 = 0x8 |
| | | let hexDigits = '0123456789abcdef' |
| | | for (let i = 0; i < 36; i++) { |
| | | s[i] = hexDigits.substr(Math.floor(Math.random() * num10), 1) |
| | | } |
| | | s[14] = '4' |
| | | s[19] = hexDigits.substr((s[19] & num3) | num8, 1) |
| | | s[8] = s[13] = s[18] = s[23] = '' |
| | | |
| | | return s.join('') |
| | | } |
| | | export const getDictData = (key) => { |
| | | let list = JSON.parse(localStorage.getItem('dictData')) || [] |
| | | return list.filter(ele => ele.dictType == key) |
| | | } |
| | | |
| | | /** |
| | | * 防抖函数 |
| | | * @param {Function} func - 需要防抖的函数 |
| | | * @param {number} delay - 延迟时间(毫秒),默认 300ms |
| | | * @param {boolean} immediate - 是否立即执行,默认 false |
| | | * @returns {Function} 防抖后的函数 |
| | | */ |
| | | export const debounce = (func, delay = 300, immediate = false) => { |
| | | let timer = null |
| | | let isImmediateExecuted = false |
| | | |
| | | return function (...args) { |
| | | const context = this |
| | | |
| | | if (timer) { |
| | | clearTimeout(timer) |
| | | timer = null |
| | | } |
| | | |
| | | if (immediate && !isImmediateExecuted) { |
| | | func.apply(context, args) |
| | | isImmediateExecuted = true |
| | | } |
| | | |
| | | timer = setTimeout(() => { |
| | | if (!immediate) { |
| | | func.apply(context, args) |
| | | } |
| | | isImmediateExecuted = false |
| | | timer = null |
| | | }, delay) |
| | | } |
| | | } |
| | | |
| | | export const getOccupationName = (code) => { |
| | | const { occupationItems } = useOptionItemsStore() |
| | | const obj = occupationItems.find(ele => ele.code == code) |
| | | |
| | | return obj?.name || '' |
| | | } |
| | | |
| | | export const getJobName = (occupationCode, jobCode) => { |
| | | const { occupationItems } = useOptionItemsStore() |
| | | const occupation = occupationItems.find(ele => ele.code == occupationCode) |
| | | if (!occupation) return '' |
| | | |
| | | const job = occupation.jobs?.find(ele => ele.jobCode == jobCode) |
| | | if (!job) return '' |
| | | |
| | | return job.jobName || '' |
| | | } |
| New file |
| | |
| | | <template> |
| | | <div class="error-page"> |
| | | <div class="error-container"> |
| | | <div class="error-content"> |
| | | <div class="error-code">404</div> |
| | | <div class="error-message">页面未找到</div> |
| | | <div class="error-description">抱歉,您访问的页面不存在或已被移除</div> |
| | | <div class="error-actions"> |
| | | <el-button @click="goBack" size="large"> |
| | | <Icon icon="icon-park-solid:return" width="16" height="16" class="mr-1" /> |
| | | 返回首页 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <div class="error-illustration"> |
| | | <Icon icon="icon-park-solid:file-search" width="200" height="200" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | export default { |
| | | methods: { |
| | | goHome() { |
| | | this.$router.push('/') |
| | | }, |
| | | goBack() { |
| | | if (window.history.length > 1) { |
| | | this.$router.go(-1) |
| | | } else { |
| | | this.$router.push('/') |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .error-page { |
| | | height: 100vh; |
| | | background: #ffffff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| | | } |
| | | |
| | | .error-container { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 60px; |
| | | max-width: 1000px; |
| | | padding: 40px; |
| | | } |
| | | |
| | | .error-content { |
| | | flex: 1; |
| | | text-align: left; |
| | | } |
| | | |
| | | .error-code { |
| | | font-size: 120px; |
| | | font-weight: bold; |
| | | color: #667eea; |
| | | line-height: 1; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .error-message { |
| | | font-size: 32px; |
| | | font-weight: 600; |
| | | color: #333333; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .error-description { |
| | | font-size: 18px; |
| | | color: #666666; |
| | | margin-bottom: 40px; |
| | | line-height: 1.6; |
| | | } |
| | | |
| | | .error-actions { |
| | | display: flex; |
| | | gap: 16px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .error-illustration { |
| | | flex: 1; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | } |
| | | |
| | | .error-illustration .iconify { |
| | | color: #667eea; |
| | | animation: float 3s ease-in-out infinite; |
| | | } |
| | | |
| | | @keyframes float { |
| | | 0%, 100% { |
| | | transform: translateY(0px); |
| | | } |
| | | 50% { |
| | | transform: translateY(-20px); |
| | | } |
| | | } |
| | | |
| | | /* 响应式设计 */ |
| | | @media (max-width: 768px) { |
| | | .error-container { |
| | | flex-direction: column; |
| | | text-align: center; |
| | | gap: 40px; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .error-code { |
| | | font-size: 80px; |
| | | } |
| | | |
| | | .error-message { |
| | | font-size: 24px; |
| | | } |
| | | |
| | | .error-description { |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .error-actions { |
| | | justify-content: center; |
| | | } |
| | | |
| | | .error-illustration .iconify { |
| | | width: 120px !important; |
| | | height: 120px !important; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 480px) { |
| | | .error-code { |
| | | font-size: 60px; |
| | | } |
| | | |
| | | .error-message { |
| | | font-size: 20px; |
| | | } |
| | | |
| | | .error-actions { |
| | | flex-direction: column; |
| | | align-items: center; |
| | | } |
| | | |
| | | .error-actions .el-button { |
| | | width: 200px; |
| | | } |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="login-form-content"> |
| | | <p>账号密码登录</p> |
| | | <el-form ref="accountForm" :model="form" :rules="rules" style="width: 100%;"> |
| | | <el-form-item prop="username"> |
| | | <el-input |
| | | v-model="form.username" |
| | | placeholder="请输入账号" |
| | | style="width: 100%" size="large" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item prop="password"> |
| | | <el-input |
| | | v-model="form.password" |
| | | placeholder="请输入密码" |
| | | style="width: 100%" size="large" |
| | | type="password" |
| | | show-password |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <el-button type="primary" style="width: 100%" size="large" @click="login()">登 录</el-button> |
| | | <el-row align="middle"> |
| | | <el-text class="text-primary cursor-p" @click="resetDialog()">忘记密码</el-text> |
| | | <el-divider style="height: 20px;margin:0 14px" direction="vertical"></el-divider> |
| | | <el-text class="text-primary cursor-p" @click="changeLoginType('mobile')">手机号验证码登录</el-text> |
| | | <el-divider style="height: 20px;margin:0 14px" direction="vertical"></el-divider> |
| | | <el-text @click="changeLoginType('qrCode')" class="text-primary cursor-p">微信扫码登录</el-text> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import { tokenUtils } from '@/utils/axios.js'; |
| | | export default { |
| | | data() { |
| | | return { |
| | | form: { |
| | | username: this.$route.query.target=='tenant' ? '13030003000' : 'admin', |
| | | password: this.$route.query.target=='tenant' ? '123456' : 'admin123', |
| | | }, |
| | | rules: { |
| | | username: [ this.$rules.required('账号不能为空') ], |
| | | password: [ this.$rules.required('密码不能为空') ] |
| | | } |
| | | } |
| | | }, |
| | | methods: { |
| | | changeLoginType(type) { |
| | | this.$emit('changeLoginType', type) |
| | | }, |
| | | resetDialog() { |
| | | |
| | | }, |
| | | async login() { |
| | | const validate = await this.$refs.accountForm.validate() |
| | | if (validate) { |
| | | const data = { |
| | | ...this.form |
| | | } |
| | | this.$axios.post('/system/auth/login', data).then(res => { |
| | | if (res.data.code == 0 && res.data.data) { |
| | | const resData = res.data.data |
| | | tokenUtils.setTokens(resData.accessToken, resData.refreshToken) |
| | | this.$message.success('登录成功') |
| | | this.$router.push('/console') |
| | | } else { |
| | | this.$message.error(res.data.msg || '登录失败') |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style> |
| | | .login-form-content { |
| | | width: 100%; |
| | | height: 350px; |
| | | padding: 20px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: space-around; |
| | | align-items: center; |
| | | } |
| | | .login-form-content p{ |
| | | font-size: 22px; |
| | | font-weight: bold; |
| | | color: black; |
| | | letter-spacing: 10px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <el-dialog |
| | | v-model="loginDialogVisible" |
| | | width="500" |
| | | align-center |
| | | :close-on-click-modal="false" |
| | | :close-on-press-escape="false" |
| | | > |
| | | <div class="px-4"> |
| | | <el-row justify="center"> |
| | | <el-text class="text-2xl font-bold">{{ title }}</el-text> |
| | | </el-row> |
| | | <el-form v-if="loginType == 'mobile'" ref="accountForm" :model="form" :rules="rules" class="pt-6 pb-3"> |
| | | <el-form-item prop="mobile"> |
| | | <el-input v-model="form.mobile" placeholder="请输入手机号" /> |
| | | </el-form-item> |
| | | <el-form-item prop="code" class="mb-2"> |
| | | <el-input v-model="form.code" placeholder="请输入验证码"> |
| | | <template #append> |
| | | <el-row style="width: 70px;justify-content: center;"> |
| | | <el-button |
| | | v-if="countdown == 180" |
| | | style="color: var(--el-color-primary);" |
| | | class="cursor-p" |
| | | :loading="sendCodeLoading" |
| | | @click="sendCode()" |
| | | > |
| | | 获取验证码 |
| | | </el-button> |
| | | <el-text v-else>{{ countdown }}s</el-text> |
| | | </el-row> |
| | | </template> |
| | | </el-input> |
| | | </el-form-item> |
| | | <el-form-item prop="agreement" style="height: 30px;"> |
| | | <el-checkbox v-model="form.agreement" label="同意xxx服务协议" size="large" /> |
| | | </el-form-item> |
| | | |
| | | <el-button class="mt-1" @click="submitLogin()" type="primary" size="large" style="width: 100%;"> |
| | | <el-text class="text-lg text-white">登录</el-text> |
| | | </el-button> |
| | | </el-form> |
| | | |
| | | <el-row v-else-if="loginType == 'qrCode'" justify="center" class="mt-7"> |
| | | <el-image style="width: 200px;" :src="$getImageUrl('/qrCode.png')"></el-image> |
| | | </el-row> |
| | | |
| | | <el-form v-else-if="loginType == 'register'" ref="registerForm" :model="form" :rules="rules" class="pt-6 pb-3"> |
| | | <el-form-item prop="name" class="mb-2"> |
| | | <el-input v-model="form.name" placeholder="请输入姓名" /> |
| | | </el-form-item> |
| | | <el-form-item prop="sex" style="height: 20px;"> |
| | | <el-radio-group v-model="form.sex"> |
| | | <el-radio :value="1">男</el-radio> |
| | | <el-radio :value="0">女</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item prop="idCard"> |
| | | <el-input v-model="form.idCard" placeholder="请输入身份证号" /> |
| | | </el-form-item> |
| | | <el-form-item prop="mobile"> |
| | | <el-input v-model="form.mobile" placeholder="请输入手机号" /> |
| | | </el-form-item> |
| | | <el-form-item prop="code" class="mb-2"> |
| | | <el-input v-model="form.code" placeholder="请输入验证码"> |
| | | <template #append> |
| | | <el-row style="width: 70px;justify-content: center;"> |
| | | <el-button |
| | | v-if="countdown == 180" |
| | | style="color: var(--el-color-primary);" |
| | | class="cursor-p" |
| | | :loading="sendCodeLoading" |
| | | @click="sendCode()" |
| | | > |
| | | 获取验证码 |
| | | </el-button> |
| | | <el-text v-else>{{ countdown }}s</el-text> |
| | | </el-row> |
| | | </template> |
| | | </el-input> |
| | | </el-form-item> |
| | | <el-form-item prop="agreement" style="height: 30px;"> |
| | | <el-checkbox v-model="form.agreement" label="同意xxx服务协议" size="large" /> |
| | | </el-form-item> |
| | | |
| | | <el-button class="mt-1" @click="submitRegister()" type="primary" size="large" style="width: 100%;"> |
| | | <el-text class="text-lg text-white">注册</el-text> |
| | | </el-button> |
| | | </el-form> |
| | | |
| | | <el-row class="mt-7" justify="center"> |
| | | <el-button v-if="loginType!='register'" text type="primary" @click="changeLoginType('register')">注册账号</el-button> |
| | | <el-divider v-if="loginType!='register'" direction="vertical" class="m-0 mt-1 mx-4" style="height: 24px !important" /> |
| | | <el-button v-if="loginType!='qrCode'" text type="primary" @click="changeLoginType('qrCode')">二维码登录</el-button> |
| | | <el-divider v-if="loginType=='register'" direction="vertical" class="m-0 mt-1 mx-4" style="height: 24px !important" /> |
| | | <el-button v-if="loginType!='mobile'" text type="primary" @click="changeLoginType('mobile')">手机号登录</el-button> |
| | | </el-row> |
| | | </div> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script> |
| | | import { useLoginStore } from '@/stores/login.js' |
| | | import { storeToRefs } from 'pinia'; |
| | | export default { |
| | | components: {}, |
| | | setup() { |
| | | const { loginDialogVisible } = storeToRefs(useLoginStore()) |
| | | return { loginDialogVisible } |
| | | }, |
| | | data() { |
| | | return { |
| | | loginType: 'mobile', // mobile、qrCode |
| | | form: { |
| | | name: '', |
| | | sex: 1, |
| | | idCard: '', |
| | | mobile: '13537719675', |
| | | code: '', |
| | | agreement: false |
| | | }, |
| | | rules: { |
| | | name: [ this.$rules.required('请输入姓名') ], |
| | | idCard: [ this.$rules.required('请输入身份证号') ], |
| | | mobile: [ this.$rules.required('请输入手机号'), this.$rules.phone() ], |
| | | code: [ this.$rules.required('请填写验证码'), this.$rules.code() ], |
| | | agreement: [ this.$rules.checkbox('请阅读并勾选同意协议') ] |
| | | }, |
| | | countdown: 180, |
| | | countdownInterval: null, |
| | | sendCodeLoading: false, |
| | | } |
| | | }, |
| | | computed: { |
| | | title() { |
| | | let obj = { |
| | | register: '注 册', |
| | | mobile: '手 机 号登 录', |
| | | qrCode: '微 信 扫 码 登 录' |
| | | } |
| | | return obj[this.loginType] |
| | | } |
| | | }, |
| | | created() { |
| | | |
| | | }, |
| | | unmounted() { |
| | | this.clearCountdownInterval() |
| | | }, |
| | | watch: { |
| | | |
| | | }, |
| | | methods: { |
| | | async sendCode() { |
| | | try { |
| | | await this.$refs.accountForm.validateField('mobile') |
| | | this.startCountdownInterval() |
| | | this.$message.success('已发送验证码,请注意查收') |
| | | } catch (error) { |
| | | this.$message.error('请输入手机号码') |
| | | } |
| | | }, |
| | | startCountdownInterval() { |
| | | this.clearCountdownInterval() |
| | | this.countdown-- |
| | | this.countdownInterval = setInterval(() => { |
| | | if (this.countdown > 0) { |
| | | this.countdown-- |
| | | } else { |
| | | this.countdown = 180 |
| | | this.clearCountdownInterval() |
| | | } |
| | | }, 1000) |
| | | }, |
| | | clearCountdownInterval() { |
| | | clearInterval(this.countdownInterval) |
| | | this.countdownInterval = null |
| | | }, |
| | | async submitLogin() { |
| | | try { |
| | | await this.$refs.accountForm.validate() |
| | | } catch (error) { |
| | | console.log() |
| | | } |
| | | }, |
| | | async submitRegister() { |
| | | try { |
| | | await this.$refs.registerForm.validate() |
| | | } catch (error) { |
| | | console.log() |
| | | } |
| | | }, |
| | | changeLoginType(type) { |
| | | this.loginType = type |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="mt-4 mb-2"> |
| | | <el-table |
| | | :data="showList" |
| | | table-layout="auto" |
| | | stripe |
| | | :empty-text="emptyText" |
| | | style="width: 100%" |
| | | > |
| | | <el-table-column v-if="selectionFlag" type="selection" width="55" /> |
| | | <el-table-column |
| | | v-for="(item,index) in headers" :key="index" |
| | | :prop="item.value" |
| | | :label="item.label" |
| | | :width="item.width" |
| | | :align="item.align || 'center'" |
| | | :sortable="item.sortable" |
| | | > |
| | | <template v-if="$slots[item.value]" #default="scope"> |
| | | <slot :name="item.value" v-bind="scope"></slot> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-row class="mt-4" align="middle" justify="end" v-if="paginator"> |
| | | <el-pagination |
| | | v-model:current-page="page" |
| | | v-model:page-size="size" |
| | | :page-sizes="pageSize" |
| | | background |
| | | :layout="layout" |
| | | :total="totalCount || list.length" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" |
| | | /> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | |
| | | export default { |
| | | data() { |
| | | return { |
| | | pageSize: [12, 24, 36, 48], |
| | | page: 1, |
| | | size: 12, |
| | | showList: [], |
| | | } |
| | | }, |
| | | props: { |
| | | filter: { |
| | | type: Object, |
| | | default: () => { return { pageNo: 1, pageSize: 12 } } |
| | | }, |
| | | headers: { |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | list: { |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | totalCount: { |
| | | type: Number, |
| | | default: 0 |
| | | }, |
| | | layout: { |
| | | type: String, |
| | | default: 'total, sizes, prev, pager, next, jumper' |
| | | }, |
| | | selectionFlag: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | emptyText: { |
| | | type: String, |
| | | default: '暂无数据' |
| | | }, |
| | | paginator: { |
| | | type: Boolean, |
| | | default: true |
| | | } |
| | | }, |
| | | watch: { |
| | | list: { |
| | | handler() { |
| | | this.updateShowList() |
| | | }, |
| | | immediate: true |
| | | }, |
| | | 'filter.page': function(val) { |
| | | this.page = val |
| | | }, |
| | | 'filter.size': function(val) { |
| | | this.size = val |
| | | } |
| | | }, |
| | | methods: { |
| | | handleSizeChange() { |
| | | this.$emit('update:filter', { |
| | | ...this.filter, |
| | | size: this.size |
| | | }) |
| | | this.updateShowList() |
| | | }, |
| | | handleCurrentChange() { |
| | | this.$emit('update:filter', { |
| | | ...this.filter, |
| | | page: this.page, |
| | | }) |
| | | this.updateShowList() |
| | | }, |
| | | updateShowList() { |
| | | if (this.paginator) { |
| | | this.showList = this.list.slice((this.page - 1) * this.size, this.page * this.size) |
| | | } else { |
| | | this.showList = this.list |
| | | } |
| | | |
| | | } |
| | | } |
| | | } |
| | | |
| | | </script> |
| | | |
| | | <style> |
| | | .el-table th.el-table__cell { |
| | | padding: 12px 0; |
| | | background-color: #fafafa; |
| | | } |
| | | </style> |
| | | |
| New file |
| | |
| | | <template> |
| | | <el-tag |
| | | :round="round" |
| | | :style="cssClass" |
| | | > |
| | | <el-text :style="{ color: cssClass.color }"> |
| | | {{ text }} |
| | | </el-text> |
| | | </el-tag> |
| | | </template> |
| | | <script> |
| | | export default { |
| | | data() { |
| | | return { |
| | | list: [] |
| | | } |
| | | }, |
| | | props: { |
| | | valueKey: [String, Number], |
| | | dictType: String, |
| | | round: { |
| | | type: Boolean, |
| | | default: true |
| | | } |
| | | }, |
| | | created() { |
| | | this.initList() |
| | | }, |
| | | computed: { |
| | | dictObj: function() { |
| | | return this.list.find(ele => ele.value == this.valueKey) || {} |
| | | }, |
| | | text: function() { |
| | | return this.dictObj?.label || '' |
| | | }, |
| | | cssClass: function() { |
| | | let str = '' |
| | | try { |
| | | str = JSON.parse(this.dictObj?.cssClass) |
| | | } catch(error) { |
| | | console.log() |
| | | } |
| | | return str |
| | | } |
| | | }, |
| | | methods: { |
| | | initList() { |
| | | this.list = this.$getDictData(this.dictType) |
| | | } |
| | | } |
| | | } |
| | | </script> |
| New file |
| | |
| | | <template> |
| | | <el-row style="width: 100%;flex-wrap: nowrap;"> |
| | | <div style="flex-grow: 0;"> |
| | | <el-tooltip content="刷新列表数据" placement="top-start"> |
| | | <el-button type="text" style="width: 30px;" :loading="loadingFlag" @click.stop="handlerRefresh()" class="mr-2"> |
| | | <Icon v-show="!loadingFlag" icon="material-symbols:refresh-rounded" width="26" height="26" style="color: #4d4d4d" /> |
| | | <template #loading> |
| | | <Icon icon="line-md:loading-loop" width="30" height="30" style="color: black" /> |
| | | </template> |
| | | </el-button> |
| | | </el-tooltip> |
| | | </div> |
| | | <slot name="default"></slot> |
| | | </el-row> |
| | | </template> |
| | | |
| | | <script> |
| | | import { debounce } from '@/utils/tool.js' |
| | | export default { |
| | | data() { |
| | | return { |
| | | loading: false |
| | | } |
| | | }, |
| | | props: { |
| | | loadingFlag: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | refresh: { |
| | | type: Function, |
| | | default: () => {} |
| | | }, |
| | | filter: { |
| | | type: Object, |
| | | default: () => { |
| | | return {} |
| | | } |
| | | } |
| | | }, |
| | | watch: { |
| | | filter: { |
| | | handler: debounce(function() { |
| | | this.handlerRefresh() |
| | | }, 1000), |
| | | deep: true |
| | | } |
| | | }, |
| | | methods: { |
| | | handlerRefresh() { |
| | | this.refresh() |
| | | } |
| | | } |
| | | } |
| | | </script> |
| New file |
| | |
| | | <template> |
| | | <div class="custom-footer"> |
| | | <el-row justify="center"> |
| | | <el-select |
| | | v-for="(item,index) in selectList" |
| | | v-model="item.value" |
| | | :placeholder="item.placeholder" |
| | | :key="`select${index}`" |
| | | style="width: 288px;font-size: 16px;" |
| | | class="mx-2 mt-6" |
| | | > |
| | | <el-option |
| | | v-for="item in item.options" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-row> |
| | | <el-row> |
| | | <el-text class="text-white font-medium text-default"> |
| | | ©2025 深圳市企鹅网络科技有限公司 粤ICP备15026064号 粤公网安备 44030502007327 号 |
| | | </el-text> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | <script> |
| | | export default { |
| | | data() { |
| | | return { |
| | | selectList: [ |
| | | { |
| | | options: [], |
| | | placeholder: "上级政府网站", |
| | | value: "" |
| | | }, |
| | | { |
| | | options: [], |
| | | placeholder: "各省人社部门网站", |
| | | value: "" |
| | | }, |
| | | { |
| | | options: [], |
| | | placeholder: "各地市人社部门网站", |
| | | value: "" |
| | | }, |
| | | { |
| | | options: [], |
| | | placeholder: "业务网站", |
| | | value: "" |
| | | }, |
| | | ] |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .custom-footer { |
| | | width: 100%; |
| | | min-height: 185px !important; |
| | | display: flex; |
| | | padding: 20px; |
| | | flex-direction: column; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | background-color: #007AFF; |
| | | margin-top: 30px; |
| | | /* background-image: url('@/assets/images/test.png'); */ |
| | | } |
| | | :deep(.el-select__wrapper) { |
| | | min-height: 44px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="custom-header"> |
| | | <el-row class="content" justify="space-between"> |
| | | <el-text class="text-white text-2xl font-bold">广东省技能人才评价考务管理平台</el-text> |
| | | <el-dropdown v-if="!userInfo.id" placement="bottom"> |
| | | <el-row align="middle"> |
| | | <Icon icon="fa:user-circle" width="22" height="22" class="mr-2" style="color: #fff" /> |
| | | <el-text class="text-white cursor-p text-lg font-bold">黄婷婷</el-text> |
| | | <Icon icon="flowbite:caret-down-solid" width="22" height="22" class="ml-2" style="color: #fff" /> |
| | | </el-row> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item>个人中心</el-dropdown-item> |
| | | <el-dropdown-item>退出登录</el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> |
| | | <el-button v-else color="#007AFF"> |
| | | <el-text class="text-white cursor-p text-lg font-bold" @click="login">登 录</el-text> |
| | | </el-button> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import { useSessionStore } from '@/stores/session.js' |
| | | import { useLoginStore } from '@/stores/login.js' |
| | | import { storeToRefs } from 'pinia'; |
| | | |
| | | export default { |
| | | setup() { |
| | | const { loginDialogVisible } = storeToRefs(useLoginStore()) |
| | | const { userInfo } = storeToRefs(useSessionStore()) |
| | | return { loginDialogVisible, userInfo } |
| | | }, |
| | | data() { |
| | | return { |
| | | |
| | | } |
| | | }, |
| | | methods: { |
| | | login() { |
| | | this.loginDialogVisible = true |
| | | } |
| | | } |
| | | } |
| | | |
| | | </script> |
| | | <style> |
| | | .custom-header { |
| | | width: 100%; |
| | | align-content: center; |
| | | background-color: var(--el-color-primary); |
| | | height: 60px; |
| | | } |
| | | .content { |
| | | width: 100%; |
| | | max-width: 1280px; |
| | | margin: 0 auto; |
| | | padding: 0 40px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <el-button text class="pl-0 pr-2 my-1" @click="goBack"> |
| | | <el-row align="middle"> |
| | | <Icon icon="mingcute:home-6-line" width="16" height="16" style="color: var(--el-color-primary)" /> |
| | | <el-text class="ml-1" style="color: var(--el-color-primary);">返回首页</el-text> |
| | | </el-row> |
| | | </el-button> |
| | | </template> |
| | | <script> |
| | | export default { |
| | | methods: { |
| | | goBack() { |
| | | this.$router.replace('/main/home') |
| | | } |
| | | } |
| | | } |
| | | </script> |
| New file |
| | |
| | | <template> |
| | | <div></div> |
| | | </template> |
| New file |
| | |
| | | <template> |
| | | <el-upload |
| | | ref="upload" |
| | | class="my-3" action="#" |
| | | v-model:file-list="list" |
| | | :accept="acceptType" |
| | | :list-type="listType" |
| | | :http-request="uploadRequest" |
| | | :multiple="true" |
| | | :limit="limitFileCount" |
| | | :on-preview="handlePreview" |
| | | :on-remove="handleRemove" |
| | | :before-remove="beforeRemove" |
| | | :on-exceed="handleExceed" |
| | | :disabled="disabled" |
| | | :class="{ hideUpload: hideUpload }" |
| | | > |
| | | <template #default> |
| | | <template v-if="listType=='picture-card'"> |
| | | <el-icon v-if="!disabled"><Plus /></el-icon> |
| | | <el-text v-if="disabled">暂未上传</el-text> |
| | | </template> |
| | | <template v-else> |
| | | <el-row> |
| | | <el-button type="primary"> |
| | | <el-row align="middle"> |
| | | <Icon icon="material-symbols:upload-rounded" width="20" height="20" style="color: #fff" /> |
| | | <el-text class="text-white ml-1">{{ againUploadFlag ? '重新上传' : '立即上传' }}</el-text> |
| | | </el-row> |
| | | </el-button> |
| | | <el-text style="color: #666666" class="ml-4"> |
| | | {{ tip }} |
| | | </el-text> |
| | | </el-row> |
| | | </template> |
| | | </template> |
| | | <template #file="{ file, index }"> |
| | | <template v-if="listType=='picture-card'"> |
| | | <el-image |
| | | ref="previewImg" |
| | | :src="file.url" |
| | | :initial-index="initialPreviewImgIndex" |
| | | :preview-src-list="filterPreviewImgList" |
| | | > |
| | | </el-image> |
| | | <span class="el-upload-list__item-actions"> |
| | | <span @click="previewImage(index)"> |
| | | <el-icon><zoom-in /></el-icon> |
| | | </span> |
| | | <!-- <span @click="replaceUpload(index)"> |
| | | <el-icon><upload /></el-icon> |
| | | </span> --> |
| | | <span @click="deleteFileItem(index)"> |
| | | <el-icon><delete /></el-icon> |
| | | </span> |
| | | </span> |
| | | </template> |
| | | <template v-else> |
| | | <div class="file-box"> |
| | | <Icon |
| | | @click="deleteFileItem(index)" |
| | | class="cursor-p mr-3" |
| | | icon="eva:close-fill" |
| | | width="18" |
| | | height="18" |
| | | style="color: #666666" |
| | | /> |
| | | <Icon class="mr-1" icon="uis:paperclip" width="18" height="18" style="color:#007AFF" /> |
| | | <span>{{ file.name }}</span> |
| | | </div> |
| | | </template> |
| | | </template> |
| | | </el-upload> |
| | | <el-dialog v-model="imgPreviewDialogFlag"> |
| | | <img w-full :src="previewUrl" alt="Preview Image" /> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script> |
| | | const pdf = 'application/pdf' |
| | | const xls = 'application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' |
| | | const doc = 'application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document' |
| | | const image = 'image/*' |
| | | const jpg = 'image/jpeg' |
| | | const png = 'image/png' |
| | | import { genFileId } from 'element-plus' |
| | | export default { |
| | | data() { |
| | | return { |
| | | list: [], |
| | | imgPreviewDialogFlag: false, |
| | | previewUrl: '', |
| | | initialPreviewImgIndex: 0 |
| | | } |
| | | }, |
| | | props: { |
| | | listType: { |
| | | type: String, |
| | | default: 'text' |
| | | }, |
| | | accept: { |
| | | type: Array, |
| | | default: () => { |
| | | return ['pdf', 'xls', 'doc', 'image', 'jpg', 'png'] |
| | | } |
| | | }, |
| | | limitFileCount: { |
| | | type: Number, |
| | | default: 1 |
| | | }, |
| | | modelValue: { |
| | | type: Array, |
| | | default: () => { |
| | | return [] |
| | | } |
| | | }, |
| | | disabled: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }, |
| | | watch: { |
| | | modelValue: { |
| | | handler: function() { |
| | | this.list = this.modelValue |
| | | }, |
| | | deep: true |
| | | }, |
| | | list: { |
| | | handler: function(val) { |
| | | this.$emit('update:modelValue', val) |
| | | }, |
| | | deep: true |
| | | } |
| | | }, |
| | | computed: { |
| | | acceptType() { |
| | | let obj = { pdf, xls, doc, image, jpg, png } |
| | | return this.accept.map(ele => obj[ele]).join(',') |
| | | }, |
| | | tip() { |
| | | let obj = { |
| | | pdf: 'PDF', |
| | | xls: 'EXCEL', |
| | | doc: 'WORD', |
| | | image: '图片', |
| | | jpg: 'JPEG/JPG', |
| | | png: 'PNG' |
| | | } |
| | | let tip = this.accept.map(ele => obj[ele]).join('、') |
| | | return `支持${tip}类型文件` |
| | | }, |
| | | againUploadFlag() { |
| | | return this.limitFileCount == 1 && this.list.length == 1 |
| | | }, |
| | | filterPreviewImgList() { |
| | | let list = this.list.map(ele => ele.url ) |
| | | return list |
| | | }, |
| | | hideUpload() { |
| | | return this.list.length >= this.limitFileCount |
| | | } |
| | | }, |
| | | methods: { |
| | | uploadRequest(UploadRequestOptions) { |
| | | const data = { |
| | | file: UploadRequestOptions.file, |
| | | directory: '' |
| | | } |
| | | this.$axios.post('/infra/file/upload', data, { |
| | | headers: { 'Content-Type': "multipart/form-data" } |
| | | }).then(res => { |
| | | let index = this.list.findIndex(ele => ele.uid == data.file.uid) |
| | | if (res.data.code == 0) { |
| | | let index = this.list.findIndex(ele => ele.uid == data.file.uid) |
| | | if (index != -1) { |
| | | this.list[index].uploadStatus = 'success' |
| | | this.list[index].url = res.data.data |
| | | } |
| | | this.$message.success('上传成功') |
| | | } else { |
| | | if (index != -1) { |
| | | this.list[index].uploadStatus = 'fail' |
| | | } |
| | | this.$message.error(res.data.msg || '上传失败') |
| | | } |
| | | }) |
| | | }, |
| | | deleteFileItem(index) { |
| | | this.list.splice(index, 1) |
| | | }, |
| | | previewImage(index) { |
| | | this.initialPreviewImgIndex = index |
| | | this.$refs.previewImg?.showPreview() |
| | | }, |
| | | replaceUpload() { |
| | | |
| | | }, |
| | | handleRemove() {}, |
| | | beforeRemove() {}, |
| | | handlePreview() {}, |
| | | handleExceed(file) { |
| | | if (this.againUploadFlag) { |
| | | this.$refs.upload?.clearFiles() |
| | | let newFile = file[0] |
| | | newFile.uid = genFileId() |
| | | this.$refs.upload?.handleStart(newFile) |
| | | this.$refs.upload?.submit() |
| | | } else { |
| | | this.$message.error(`最多支持上传 ${this.limitFileCount} 个文件`) |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .hideUpload :deep(.el-upload--picture-card) { |
| | | display: none !important; |
| | | } |
| | | .file-box { |
| | | display: flex; |
| | | color: #007AFF; |
| | | align-content: center; |
| | | background-color: #ECF5FF; |
| | | padding: 8px; |
| | | font-size: 15px; |
| | | line-height: 16px; |
| | | } |
| | | .el-upload-list__item-actions { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background-color: rgba(0, 0, 0, 0.5); /* 半透明黑色蒙版 */ |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | opacity: 0; /* 默认透明 */ |
| | | transition: opacity .3s; /* 添加过渡动画效果 */ |
| | | } |
| | | |
| | | /* 鼠标悬停时显示蒙版 */ |
| | | .el-upload-list__item:hover .el-upload-list__item-actions { |
| | | opacity: 1; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <el-row> |
| | | <el-col> |
| | | <div class="area">{{ title }}</div> |
| | | </el-col> |
| | | </el-row> |
| | | </template> |
| | | <script> |
| | | export default { |
| | | data() { |
| | | return { |
| | | |
| | | } |
| | | }, |
| | | props: { |
| | | title: { |
| | | type: String, |
| | | default: '基本信息', |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | <style scoped> |
| | | .area { |
| | | color: '#000'; |
| | | background-color: #f7f7f7; |
| | | padding: 14px; |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | margin-top: 14px; |
| | | margin-bottom: 14px; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <el-dialog |
| | | v-model="dialogFlag" |
| | | title="提示" |
| | | width="500" |
| | | align-center |
| | | > |
| | | <span>是否退出登录?</span> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="dialogFlag = false">取消</el-button> |
| | | <el-button type="primary" @click="confirm()"> |
| | | 确定 |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | <script> |
| | | export default { |
| | | data() { |
| | | return { |
| | | dialogFlag: false |
| | | } |
| | | }, |
| | | props: { |
| | | modelValue: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }, |
| | | watch: { |
| | | modelValue(val) { |
| | | this.dialogFlag = val |
| | | }, |
| | | dialogFlag(val) { |
| | | this.$emit('update:modelValue', val) |
| | | } |
| | | }, |
| | | methods: { |
| | | confirm() { |
| | | this.dialogFlag = false |
| | | this.$router.push('/auth/login') |
| | | } |
| | | } |
| | | } |
| | | </script> |
| New file |
| | |
| | | <template> |
| | | <template v-if="$property[propertyKey]"> |
| | | <el-tag |
| | | :round="round" |
| | | :style="{ borderColor: $property[propertyKey].borderColor[statusKey]}" |
| | | :color="$property[propertyKey].bgColor[statusKey]" |
| | | > |
| | | <el-text :style="{ color: $property[propertyKey].textColor[statusKey] }"> |
| | | {{ $property[propertyKey].text[statusKey] }} |
| | | </el-text> |
| | | </el-tag> |
| | | </template> |
| | | <el-text v-else> |
| | | {{ statusKey }} |
| | | </el-text> |
| | | </template> |
| | | <script> |
| | | export default { |
| | | data() { |
| | | return {} |
| | | }, |
| | | props: { |
| | | statusKey: String, |
| | | propertyKey: String, |
| | | round: { |
| | | type: Boolean, |
| | | default: true |
| | | } |
| | | }, |
| | | } |
| | | </script> |
| New file |
| | |
| | | <template> |
| | | <div> |
| | | <el-image |
| | | :src="$getImageUrl('/home/banner1.png')" |
| | | style="width: 100%;max-height: 430px;" |
| | | > |
| | | </el-image> |
| | | <div class="main-content"> |
| | | <el-row justify="space-between"> |
| | | <el-card |
| | | v-for="(item,index) in operationList" |
| | | :key="`operation${index}`" |
| | | shadow="hover" |
| | | style="max-width: 270px;" |
| | | class="cursor-p my-4" |
| | | > |
| | | <el-image :src="$getImageUrl(`/home/${item.value}.png`)"> |
| | | </el-image> |
| | | </el-card> |
| | | </el-row> |
| | | |
| | | <el-row justify="space-between" class="py-2" style="border-bottom: 2px solid var(--el-color-primary);"> |
| | | <el-text class="text-xl font-bold"> |
| | | <span style="color: var(--el-color-primary);">通知</span> |
| | | <span>公告</span> |
| | | </el-text> |
| | | <el-button text type="primary">查看全部>></el-button> |
| | | </el-row> |
| | | |
| | | <el-card |
| | | v-for="(notice,index) in noticeList" |
| | | :key="`notice${index}`" |
| | | class="mt-2 p-4 py-3" |
| | | shadow="never" |
| | | > |
| | | <el-row justify="space-between" align="middle"> |
| | | <div> |
| | | <el-row><el-text class="text-lg text-black font-medium">{{ notice.title }}</el-text></el-row> |
| | | <el-row class="mt-2"> |
| | | <el-text style="margin-right: 40px;">发布时间:{{ notice.publishTime }}</el-text> |
| | | <el-text>所属地区:{{ notice.area }}</el-text> |
| | | </el-row> |
| | | </div> |
| | | <div> |
| | | <el-button text type="primary">点击查看详情>></el-button> |
| | | </div> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <el-row class="mt-5" v-if="noticeList.length == 0" justify="center"> |
| | | <el-text>暂无公告~</el-text> |
| | | </el-row> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | export default { |
| | | data() { |
| | | return { |
| | | operationList: [ |
| | | { name: "评价计划", value: 'appraisalPlan' }, |
| | | { name: "准考证查询", value: 'examTicket' }, |
| | | { name: "成绩查询", value: 'score' }, |
| | | { name: "证书查询", value: 'certificate' }, |
| | | ], |
| | | noticeList: [] |
| | | } |
| | | }, |
| | | created() { |
| | | this.getNoticeList() |
| | | }, |
| | | methods: { |
| | | getNoticeList() { |
| | | setTimeout(() => { |
| | | this.noticeList = |
| | | [ |
| | | { |
| | | title: "关于公布2024年广东省产教评技能生态链链主培育单位入选名单的通知", |
| | | publishTime: '2024-07-12 14:24:33', |
| | | area: '广东省', |
| | | }, |
| | | { |
| | | title: "关于公布2024年广东省产教评技能生态链链主培育单位入选名单的通知", |
| | | publishTime: '2024-07-12 14:24:33', |
| | | area: '广东省', |
| | | }, |
| | | { |
| | | title: "关于公布2024年广东省产教评技能生态链链主培育单位入选名单的通知", |
| | | publishTime: '2024-07-12 14:24:33', |
| | | area: '广东省', |
| | | }, |
| | | { |
| | | title: "关于公布2024年广东省产教评技能生态链链主培育单位入选名单的通知", |
| | | publishTime: '2024-07-12 14:24:33', |
| | | area: '广东省', |
| | | }, |
| | | { |
| | | title: "关于公布2024年广东省产教评技能生态链链主培育单位入选名单的通知", |
| | | publishTime: '2024-07-12 14:24:33', |
| | | area: '广东省', |
| | | }, |
| | | ] |
| | | }, 400) |
| | | } |
| | | } |
| | | } |
| | | |
| | | </script> |
| New file |
| | |
| | | <template> |
| | | <el-container direction="vertical" style="height: 100vh;"> |
| | | <el-header class="p-0"> |
| | | <MyHeader></MyHeader> |
| | | </el-header> |
| | | <el-main :style="{height: mainHeight}" class="custom-main p-0" > |
| | | <div> |
| | | <router-view></router-view> |
| | | </div> |
| | | <MyFooter></MyFooter> |
| | | </el-main> |
| | | |
| | | <LoginDialog /> |
| | | </el-container> |
| | | </template> |
| | | <script> |
| | | import { useWindowSize } from '@/utils/hook.js' |
| | | import MyHeader from '@/views/main/components/MyHeader.vue' |
| | | import MyFooter from '@/views/main/components/MyFooter.vue' |
| | | import LoginDialog from '@/views/login/index.vue' |
| | | export default { |
| | | components: { |
| | | MyHeader, |
| | | MyFooter, |
| | | LoginDialog |
| | | }, |
| | | setup() { |
| | | const { height } = useWindowSize() |
| | | return { height } |
| | | }, |
| | | computed: { |
| | | mainHeight: function() { |
| | | return `${this.height - 60}px` |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | |
| | | .custom-main { |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: space-between; |
| | | -ms-overflow-style: none; /* IE 和 Edge */ |
| | | scrollbar-width: none; /* Firefox */ |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="main-content"> |
| | | <ReturnBtn></ReturnBtn> |
| | | |
| | | <el-card shadow="never" class="p-6"> |
| | | <!-- 标题 --> |
| | | <el-row justify="center"> |
| | | <el-text class="text-3xl font-bold">{{detail.title}}</el-text> |
| | | </el-row> |
| | | <!-- 发表时间、地区、操作栏 --> |
| | | <el-row class="mt-4" justify="space-between" align="middle"> |
| | | <div> |
| | | <el-row> |
| | | <el-text class="mr-4 text-note">发布时间:{{ detail.publishTime }}</el-text> |
| | | <el-text class="text-note">所属地区:{{ detail.area }}</el-text> |
| | | </el-row> |
| | | </div> |
| | | <div> |
| | | <el-row> |
| | | <el-text>字体:</el-text> |
| | | <el-text |
| | | v-for="(item,index) in setFontList" |
| | | :key="`setFont${index}`" |
| | | @click="fontSize = item.fontSize" |
| | | class="cursor-p mx-1" |
| | | :style="{ color: fontSize==item.fontSize ? `var(--el-color-primary)` : '' }" |
| | | > |
| | | {{ item.label }} |
| | | </el-text> |
| | | </el-row> |
| | | </div> |
| | | </el-row> |
| | | <el-divider class="my-3" border-style="dashed"></el-divider> |
| | | <!-- 内容 --> |
| | | <div v-html="detail.content" :style="{ fontSize: `${fontSize}px` }"></div> |
| | | |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | <script> |
| | | export default { |
| | | data() { |
| | | return { |
| | | setFontList: [ |
| | | { label: '[大]', fontSize: 22 }, |
| | | { label: '[中]', fontSize: 18 }, |
| | | { label: '[小]', fontSize: 14 }, |
| | | ], |
| | | fontSize: 18, |
| | | detail: { |
| | | title: '关于 [院校名称] [XXXX] 年第 [XX] 批次 [工种 / 项目名称] 职业技能等级认定(院校统考)成绩的公示', |
| | | publishTime: '2026-01-28 14:24:33', |
| | | area: '广东省', |
| | | content: ` |
| | | <article> |
| | | <h3>关于人工智能发展的思考</h3> |
| | | <p class="meta">作者:AI研究员 | 发布日期:2024年1月</p> |
| | | |
| | | <p>近年来,<strong>人工智能(AI)</strong>技术取得了突破性进展,特别是在深度学习、自然语言处理和计算机视觉领域。</p> |
| | | |
| | | <blockquote> |
| | | “AI不会取代人类,但会使用AI的人将取代不使用AI的人。” |
| | | </blockquote> |
| | | |
| | | <p>主要技术突破包括:</p> |
| | | <ul> |
| | | <li>大规模预训练模型(如GPT系列)</li> |
| | | <li>多模态理解与生成</li> |
| | | <li>强化学习在实际问题中的应用</li> |
| | | </ul> |
| | | |
| | | <p>未来发展趋势:</p> |
| | | <ol> |
| | | <li>更加高效和轻量化的模型</li> |
| | | <li>AI与具体行业的深度融合</li> |
| | | <li>对AI伦理和安全性的更多关注</li> |
| | | </ol> |
| | | |
| | | <p>了解更多信息,请访问<a href="#" onclick="alert('示例链接')">我们的专题页面</a>。</p> |
| | | </article> |
| | | ` |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </script> |
| New file |
| | |
| | | <template> |
| | | <div class="main-content">列表</div> |
| | | </template> |
| New file |
| | |
| | | import { fileURLToPath, URL } from 'node:url' |
| | | |
| | | import { defineConfig } from 'vite' |
| | | import vue from '@vitejs/plugin-vue' |
| | | import vueDevTools from 'vite-plugin-vue-devtools' |
| | | |
| | | // https://vite.dev/config/ |
| | | export default defineConfig({ |
| | | base: '/examination/user', |
| | | plugins: [vue(), vueDevTools()], |
| | | resolve: { |
| | | alias: { |
| | | '@': fileURLToPath(new URL('./src', import.meta.url)), |
| | | }, |
| | | }, |
| | | server: { |
| | | proxy: { |
| | | '/admin-api': { |
| | | target: 'http://101.43.143.75:48180', // dev |
| | | changeOrigin: true, |
| | | }, |
| | | }, |
| | | }, |
| | | }) |