基础模块升级

main
liujun 2025-06-10 09:31:15 +08:00
commit ff11cae03f
89 changed files with 13351 additions and 0 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

2
.env Normal file
View File

@ -0,0 +1,2 @@
VITE_PROXY=/api
VITE_FILE_URL=https://file.zcloudchina.com/KjkFile

2
.env.development Normal file
View File

@ -0,0 +1,2 @@
VITE_BASE=/
VITE_BASE_URL=http://192.168.0.14:8059/new_template/

1
.env.production Normal file
View File

@ -0,0 +1 @@
VITE_BASE=/

5
.eslintignore Normal file
View File

@ -0,0 +1,5 @@
public
dist
package.json
!.prettierrc.cjs
env.d.ts

61
.eslintrc.cjs Normal file
View File

@ -0,0 +1,61 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"plugin:vue/vue3-recommended",
"standard",
"@vue/prettier",
"eslint:recommended",
],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["vue"],
rules: {
"no-console": "warn",
"vue/multi-word-component-names": "off",
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
camelcase: "off",
eqeqeq: "error",
"vue/no-template-shadow": "error",
"vue/attribute-hyphenation": "error",
"vue/html-end-tags": "error",
"vue/eqeqeq": "error",
"vue/component-name-in-template-casing": ["error", "kebab-case"],
"vue/enforce-style-attribute": ["error", { allow: ["scoped", "module"] }],
"vue/v-on-event-hyphenation": [
"error",
"always",
{
autofix: true,
},
],
"vue/require-explicit-emits": "error",
"no-unused-vars": [
"error",
{ vars: "all", args: "after-used", ignoreRestSiblings: false },
],
"linebreak-style": ["off", "windows"],
"no-restricted-properties": [
"error",
{ object: "Object", property: "assign" },
],
"no-restricted-syntax": [
"error",
{
selector: "VariableDeclarator[id.name='pd']",
message: "不允许使用 pd请改用有语义化的变量名",
},
{
selector: "ObjectExpression > Property[key.name='pd']",
message: "不允许使用 pd请改用有语义化的变量名",
},
],
},
globals: {},
};

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/src/test/
/target/
/.idea

4
.prettierrc.cjs Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
extends: ["@vue/prettier", "plugin:prettier/recommended"],
endOfLine: "crlf",
};

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](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).

8
env.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
// Auto generate by env-parse
readonly VITE_PROXY: string
readonly VITE_FILE_URL: string
readonly VITE_BASE: string
readonly VITE_BASE_URL: string
}

16
index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
<title>template</title>
</head>
<body>
<div id="app"></div>
<noscript>
<strong>很抱歉如果没有启用JavaScript网站无法正常工作请启用JavaScript使其正常工作。</strong>
</noscript>
<script type="module" src="/src/main.js"></script>
</body>
</html>

10
jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
}
}

6833
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

69
package.json Normal file
View File

@ -0,0 +1,69 @@
{
"name": "template",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --mode development",
"build": "vite build --mode production",
"preview": "vite preview",
"lint": "eslint --ext .js,.vue --fix src .prettierrc.cjs"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@icon-park/vue-next": "^1.4.2",
"@vueuse/core": "^12.0.0",
"@vueuse/integrations": "^12.0.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"animate.css": "^4.1.1",
"axios": "^1.7.9",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"element-plus": "^2.9.1",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"normalize.css": "^8.0.1",
"pinia": "^2.3.0",
"pinia-plugin-persistedstate": "^3.2.3",
"plyr": "^3.7.8",
"qrcode": "^1.5.4",
"throttle-debounce": "^5.0.2",
"v-viewer": "^3.0.21",
"viewerjs": "^1.11.7",
"vue": "^3.5.13",
"vue-countup-v3": "^1.4.2",
"vue-draggable-plus": "^0.3.5",
"vue-esign": "^1.1.4",
"vue-router": "^4.5.0",
"vue3-pdfjs": "^0.1.6",
"vue3-print-nb": "^0.1.4",
"vue3-puzzle-vcode": "^1.1.7",
"vue3-seamless-scroll": "^2.0.1"
},
"devDependencies": {
"@types/node": "^18.19.68",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-prettier": "^7.1.0",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.10.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.6.0",
"eslint-plugin-vue": "^9.32.0",
"prettier": "^2.8.8",
"sass": "^1.83.0",
"unplugin-auto-import": "^0.12.2",
"unplugin-element-plus": "^0.8.0",
"unplugin-vue-components": "^0.22.12",
"vite": "^6.0.3",
"vite-plugin-env-parse": "^1.0.15",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-remove-console": "^2.2.0",
"vue-eslint-parser": "^9.4.3"
}
}

28
postcss.config.cjs Normal file
View File

@ -0,0 +1,28 @@
module.exports = {
plugins: {
autoprefixer: {
overrideBrowserslist: [
"Android 4.1",
"iOS 7.1",
"Chrome > 31",
"ff > 31",
"ie >= 8",
"> 1%",
],
grid: true,
},
// '@our-patches/postcss-px-to-viewport': {
// unitToConvert: 'px',
// viewportWidth: 1920,
// unitPrecision: 3,
// viewportUnit: 'vw',
// selectorBlackList: ['.ignore', '.hairlines'],
// minPixelValue: 1,
// mediaQuery: false,
// exclude: [/^node_modules$/],
// include: [/BI/],
// landscapeUnit: 'vw',
// landscapeWidth: 750,
// }
},
};

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

18
src/App.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<suspense>
<template #default>
<el-config-provider :locale="zhCn">
<router-view />
</el-config-provider>
</template>
<template #fallback>
<div>加载中...</div>
</template>
</suspense>
</template>
<script setup>
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
</script>
<style lang="scss" scoped></style>

99
src/addRouters.js Normal file
View File

@ -0,0 +1,99 @@
import router from "./router";
import { useRouterStore } from "./pinia/router";
import { useMenuStore } from "./pinia/menu";
import { useUserStore } from "@/pinia/user";
import { cloneDeep } from "lodash-es";
import pinia from "./pinia";
import children from "@/components/children/index";
import { MODEL } from "@/assets/js/constant";
import asyncRouter from "@/assets/js/asyncRouter";
// import { getAsyncRouter } from "@/request/api";
// import conversionRouterMeta from "@/assets/js/conversion_router_meta.js";
const modules = import.meta.glob([
"./views/**/*.vue",
"!./views/**/components/*.vue",
]); // 获取到views下所有的vue文件
let storageRouter = null; // 用来获取后台拿到的路由
router.beforeEach(async (to, from, next) => {
const routerStore = useRouterStore(pinia);
const menuStore = useMenuStore(pinia);
const userStore = useUserStore(pinia);
// 需要登陆
if (to.meta.isLogin !== false) {
if (!userStore.getUserInfo.userId) {
next("/login");
return;
}
if (!storageRouter) {
// 变量里没有储存路由
// pinia里没有储存路由去后台获取路由
if (routerStore.getRouters.length === 0) {
// const { menuList, permissions } = await getAsyncRouter();
// userStore.setPermissions(permissions);
storageRouter = asyncRouter; // 死路由
// storageRouter = conversionRouterMeta(menuList); // 后台请求得到的路由数据
routerStore.setRouters(storageRouter); // 存储路由
routerGo(to, next); // 执行路由跳转方法
} else {
// pinia里储存了路由
storageRouter = routerStore.getRouters; // 拿到路由
routerGo(to, next); // 执行路由跳转方法
}
} else {
next();
}
} else {
// 不需要登陆,清空储存路由
storageRouter = null;
routerStore.$reset();
menuStore.$reset();
userStore.$reset();
next();
}
});
function routerGo(to, next) {
const menuStore = useMenuStore(pinia);
storageRouter = filterAsyncRouter(cloneDeep(storageRouter)); // 过滤路由
for (let i = 0; i < storageRouter.length; i++) {
router.addRoute("app", storageRouter[i]); // 动态添加路由
}
router.addRoute({ path: "/:pathMatch(.*)*", redirect: "/404" }); // 将404路由添加到最后
for (let i = 0; i < router.options.routes.length; i++) {
if (router.options.routes[i].path === "/") {
menuStore.setMenus(
router.options.routes[i].children.concat(storageRouter)
); // 将路由数据存到一个新的pinia里做菜单渲染
if (!menuStore.getModel) {
menuStore.setModel(MODEL["1"]);
}
break;
}
}
next({ ...to, replace: true }); // 等待addRoute执行完毕跳转路由
}
function filterAsyncRouter(asyncRouterMap) {
// 遍历后台传来的路由字符串,转换为组件对象
return asyncRouterMap.filter((route) => {
route.name = ""; // 后台将mete.title存成了name这里清空name重复会找不到路由
if (route.component) {
if (route.component === "children") {
route.component = children;
} else {
if (route.component.charAt(0) === "/") {
route.component = modules[`./views${route.component}.vue`];
} else {
route.component = modules[`./views/${route.component}.vue`];
}
}
}
// 如果存在children递归
if (route.list && route.list.length) {
route.children = filterAsyncRouter(route.list);
delete route.list;
}
return true;
});
}

261
src/assets/css/common.scss Normal file
View File

@ -0,0 +1,261 @@
// 5
// 使1使flexmin-width: 0;
@for $i from 1 through 5 {
.line-#{$i} {
@if $i == 1 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} @else {
display: -webkit-box !important;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
-webkit-line-clamp: $i;
-webkit-box-orient: vertical !important;
}
}
}
// 1-50marginpadding
@for $i from 1 through 50 {
.m-#{$i} {
margin: #{$i}px;
}
.mt-#{$i} {
margin-top: #{$i}px;
}
.mr-#{$i} {
margin-right: #{$i}px;
}
.mb-#{$i} {
margin-bottom: #{$i}px;
}
.ml-#{$i} {
margin-left: #{$i}px;
}
.mtb-#{$i} {
margin-top: #{$i}px;
margin-bottom: #{$i}px;
}
.mlr-#{$i} {
margin-left: #{$i}px;
margin-right: #{$i}px;
}
.p-#{$i} {
padding: #{$i}px;
}
.pt-#{$i} {
padding-top: #{$i}px;
}
.pr-#{$i} {
padding-right: #{$i}px;
}
.pb-#{$i} {
padding-bottom: #{$i}px;
}
.pl-#{$i} {
padding-left: #{$i}px;
}
.ptb-#{$i} {
padding-top: #{$i}px;
padding-bottom: #{$i}px;
}
.plr-#{$i} {
padding-left: #{$i}px;
padding-right: #{$i}px;
}
.m--#{$i} {
margin: -#{$i}px;
}
.mt--#{$i} {
margin-top: -#{$i}px;
}
.mr--#{$i} {
margin-right: -#{$i}px;
}
.mb--#{$i} {
margin-bottom: -#{$i}px;
}
.ml--#{$i} {
margin-left: -#{$i}px;
}
.mtb--#{$i} {
margin-top: -#{$i}px;
margin-bottom: -#{$i}px;
}
.mlr--#{$i} {
margin-left: -#{$i}px;
margin-right: -#{$i}px;
}
.p--#{$i} {
padding: -#{$i}px;
}
.pt--#{$i} {
padding-top: -#{$i}px;
}
.pr--#{$i} {
padding-right: -#{$i}px;
}
.pb--#{$i} {
padding-bottom: -#{$i}px;
}
.pl--#{$i} {
padding-left: -#{$i}px;
}
.ptb--#{$i} {
padding-top: -#{$i}px;
padding-bottom: -#{$i}px;
}
.plr--#{$i} {
padding-left: -#{$i}px;
padding-right: -#{$i}px;
}
}
* {
box-sizing: border-box;
font-size: 14px;
&:not(dd,dl,dt) {
margin: 0;
padding: 0;
}
}
h1, h2, h3, h4, h5, h6 {
font-size: revert;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-transition-delay: 99999s;
-webkit-transition: color 99999s ease-out, background-color 99999s ease-out;
}
#app {
background-color: #f6f8f9;
min-height: 100vh;
}
a {
text-decoration: none;
color: var(--el-color-primary);
}
.end {
.el-form-item__content {
justify-content: end;
}
}
.p0.el-descriptions__content {
padding: 0 !important;
}
.p0 .el-descriptions tr {
.el-descriptions__label {
border-left: none !important;
}
.el-descriptions__content {
border-right: none !important;
}
&:first-child .el-descriptions__cell {
border-top: none !important;
}
&:last-child .el-descriptions__cell {
border-bottom: none !important;
}
}
.text-blue {
color: #0000ff;
}
.text-yellow {
color: #bebe05;
}
.text-orange {
color: #de9004;
}
.text-red {
color: #ff0000;
}
.text-green {
color: #0bb20c;
}
.tc {
text-align: center;
}
.tr {
text-align: right;
}
.tl {
text-align: left;
}
.dn {
display: none;
}
.print_use {
display: none;
}
.viewer-zoom-in, .viewer-zoom-out, .viewer-one-to-one, .viewer-reset, .viewer-prev, .viewer-play, .viewer-next, .viewer-rotate-left, .viewer-rotate-right, .viewer-flip-horizontal, .viewer-flip-vertical, .viewer-fullscreen, .viewer-fullscreen-exit, .viewer-close {
&::before {
font-size: unset !important;
}
}
.vue-pdf__wrapper-annotation-layer {
height: 0 !important;
}
.w-e-bar{
border: 1px solid var(--el-border-color);
border-bottom: none;
}
.w-e-text-container{
border: 1px solid var(--el-border-color);
}
.w-e-bar-divider {
display: none !important;
}
//
@page {
size: auto;
margin: 3mm;
}
@media print {
.print_use {
border-collapse: collapse;
width: 100%;
display: table;
td, th {
border: 1px solid #eaeaea;
padding: 8px;
line-height: 1.6;
text-align: center;
}
}
.print_no_use {
display: none;
}
}

View File

@ -0,0 +1,93 @@
.el-select, .el-cascader, .el-date-editor.el-input, .el-date-editor.el-input__wrapper, .el-input__wrapper, .el-input-number, .el-select-v2 {
width: 100% !important;
}
.el-pagination .el-select {
width: 128px !important;
}
.el-pagination--small .el-select {
width: 100px !important;
}
.el-table .el-table__cell {
text-align: left;
}
.el-descriptions__label {
width: 200px;
}
.el-descriptions__content {
width: auto;
}
.el-divider__text {
font-size: 16px !important;
font-weight: 700 !important;
}
.el-form-item__label {
font-weight: 700;
}
.el-dialog {
--el-dialog-margin-top: 50px !important;
padding: 0 !important;
.el-dialog__header {
border-bottom: 1px solid #f1f1f1;
padding-left: 20px;
height: 48px;
line-height: 48px;
}
.el-dialog__body {
padding: 16px 20px;
}
.el-dialog__footer {
border-top: 1px solid #f1f1f1;
padding: 16px 20px;
}
}
.el-table {
* {
font-size: 12px;
}
th.el-table__cell {
--el-table-header-bg-color: rgb(245, 247, 250);
font-weight: bold;
color: rgb(0, 0, 0);
}
.el-table__cell {
padding: 8px 0 !important;
text-align: center !important;
}
}
.el-page-header {
border-bottom: 1px solid #eaeaea;
padding: 0 20px 20px 20px;
margin: 0 -20px 20px -20px;
.el-page-header__content {
font-size: 17px;
}
}
.el-form-item__label {
font-weight: normal;
font-size: 13px;
}
.el-button > span {
font-size: 12px;
}
.el-input__inner {
font-size: 13px !important;
}

View File

@ -0,0 +1,36 @@
//router-view
.view-leave-active {
opacity: 1;
transform: scaleY(1);
transition: all .5s;
transform-origin: center top;
}
.view-enter-active .view-leave-active {
transform-origin: center bottom;
}
.view-enter-from, .view-leave-active {
opacity: 0;
transform: scaleY(0);
}
//
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all .5s;
}
.breadcrumb-leave-active {
position: absolute;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,25 @@
import CryptoJS from "crypto-js";
const key = CryptoJS.enc.Utf8.parse("daac3ae52eff4cec"); // 16位
const encrypt = (word) => {
let encrypted = "";
if (typeof word === "string") {
const src = CryptoJS.enc.Utf8.parse(word);
encrypted = CryptoJS.AES.encrypt(src, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
} else if (typeof word === "object") {
// 对象格式的转成json字符串
const data = JSON.stringify(word);
const src = CryptoJS.enc.Utf8.parse(data);
encrypted = CryptoJS.AES.encrypt(src, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
}
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
};
export { encrypt };

View File

@ -0,0 +1,85 @@
import { MODEL } from "@/assets/js/constant";
export default [
{
path: "/system_management",
redirect: "/system_management/menu",
component: "children",
meta: { title: "系统管理", model: MODEL["2"], icon: "system" },
list: [
{
path: "/system_management/role",
component: "system_management/role/index",
meta: {
title: "角色管理",
isSubMenu: false,
isBack: false,
icon: "category-management",
},
},
{
path: "/system_management/menu",
component: "system_management/menu/index",
meta: {
title: "菜单管理",
isSubMenu: false,
isBack: false,
icon: "application-menu",
},
},
{
path: "/system_management/data_dictionary",
component: "system_management/data_dictionary/index",
meta: {
title: "数据字典",
isSubMenu: false,
isBack: false,
icon: "ad-product",
},
},
{
path: "/schedule/job",
component: "schedule/job/index",
meta: {
title: "定时任务",
isSubMenu: false,
isBack: false,
icon: "alarm-clock",
},
list: [],
},
],
},
{
path: "/user_management",
redirect: "/user_management/user",
component: "children",
meta: {
title: "用户管理",
model: MODEL["2"],
icon: "user",
},
list: [
{
path: "/user_management/department",
component: "user_management/department/index",
meta: {
title: "部门管理",
isSubMenu: false,
isBack: false,
icon: "newspaper-folding",
},
},
{
path: "/user_management/user",
component: "user_management/user/index",
meta: {
title: "账号管理",
isSubMenu: false,
isBack: false,
icon: "people",
},
},
],
},
];

18
src/assets/js/button.js Normal file
View File

@ -0,0 +1,18 @@
import { ElMessage } from "element-plus";
import { useUserStore } from "@/pinia/user.js";
import pinia from "@/pinia/index.js";
export default {
install: (app) => {
app.directive("button", {
mounted(el, { value }) {
const userStore = useUserStore(pinia);
if (value) {
if (!userStore.getPermissions.includes(value)) {
el.parentNode.removeChild(el);
}
} else ElMessage.error("参数无效,请联系管理员");
},
});
},
};

17
src/assets/js/constant.js Normal file
View File

@ -0,0 +1,17 @@
// 将常用的值储存成常量,防止重复使用写错
// 头部导航条切换的model
export const MODEL = {
1: "other",
2: "system",
};
// 头部导航条
export const MENU = [
{ title: "基础数据", model: MODEL["1"], icon: "database-position" },
{ title: "系统管理", model: MODEL["2"], icon: "setting" },
];
export const styleText =
'<style type="text/css" media="print">\n' +
" @page { size: landscape; }\n" +
"</style>";

View File

@ -0,0 +1,9 @@
export default function conversionRouterMeta(menuList) {
for (let i = 0; i < menuList.length; i++) {
menuList[i].meta = JSON.parse(menuList[i].meta);
if (menuList[i].list.length > 0) {
conversionRouterMeta(menuList[i].list);
}
}
return menuList;
}

View File

@ -0,0 +1,23 @@
import {
getDataDictionaries,
getDepartmentTree,
} from "@/request/data_dictionary";
import { ref } from "vue";
// 部门
export const appFnGetDepartmentTree = async (parentId) => {
const resData = await getDepartmentTree(parentId);
return ref(resData.deptTree);
};
// 学历
export const appFnGetDegree = async () => {
const resData = await getDataDictionaries({
parentId: "ddce2eac1cf27e4b114231051ec9123b",
});
return ref(resData.dictionariesList);
};
// 无法确定parentId的数据字典
export const appFnGetDataDictionary = async (parentId) => {
const resData = await getDataDictionaries({ parentId });
return ref(resData.dictionariesList);
};

2
src/assets/js/mitt.js Normal file
View File

@ -0,0 +1,2 @@
import mitt from "mitt";
export default mitt();

View File

@ -0,0 +1,14 @@
import dayjs from "dayjs";
import { setRefreshToken } from "@/request/api.js";
import { useUserStore } from "@/pinia/user.js";
import pinia from "@/pinia/index.js";
export default async function () {
const userStore = useUserStore(pinia);
if (userStore.getTokenTime) {
if (dayjs().diff(dayjs(userStore.getTokenTime), "minute") >= 5) {
await userStore.setTokenTime(dayjs().format("YYYY-MM-DD HH:mm:ss"));
await setRefreshToken();
}
}
}

6
src/assets/js/regular.js Normal file
View File

@ -0,0 +1,6 @@
export const PHONE =
/^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-7|9])|(?:5[0-3|5-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[1|8|9]))\d{8}$/;
export const UNIFIED_SOCIAL_CREDIT_CODE =
/^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/;
export const ID_NUMBER =
/^[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}[\dXx]$/;

View File

@ -0,0 +1,36 @@
import { ElMessage } from "element-plus";
import dayjs from "dayjs";
import axios from "axios";
export default function useDownloadBlob(
url,
option = { name: "", type: "", params: {} }
) {
return new Promise((resolve, reject) => {
axios
.get(url, { responseType: "blob", params: { ...option.params } })
.then((resData) => {
if (resData.data.type === "application/json") {
throw new Error("导出失败");
}
const blob = new Blob([resData.data], {
type: option.type || "application/vnd.ms-excel",
});
const downloadElement = document.createElement("a");
const href = window.URL.createObjectURL(blob);
downloadElement.style.display = "none";
downloadElement.href = href;
downloadElement.download =
option.name || dayjs().format("YYYY-MM-DD HH:mm:ss");
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
resolve(resData);
})
.catch((err) => {
ElMessage.error("导出失败");
reject(err);
});
});
}

View File

@ -0,0 +1,24 @@
import { ElMessageBox } from "element-plus";
import { getFileName, getFileSuffix } from "@/assets/js/utils.js";
export default async function useDownloadFile(url, name) {
if (!url) throw new Error("没有下载地址");
await ElMessageBox.confirm("确定要下载此文件吗?", { type: "warning" });
const FILE_URL = import.meta.env.VITE_FILE_URL;
if (name) {
if (!getFileSuffix(url)) name = name + getFileSuffix(url);
} else name = getFileName(url);
fetch(url.indexOf(FILE_URL) === -1 ? FILE_URL + url : url)
.then((res) => res.blob())
.then((blob) => {
const a = document.createElement("a");
document.body.appendChild(a);
a.style.display = "none";
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = `${name}`;
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
});
}

View File

@ -0,0 +1,28 @@
import { ElMessage } from "element-plus";
import { useTemplateRef } from "vue";
export default function useFormValidate() {
const formRef = useTemplateRef("formRef");
const validate = (message = "请补全必填项!") => {
return new Promise((resolve, reject) => {
formRef.value.validate((valid) => {
if (valid) {
resolve(valid);
} else {
reject(valid);
ElMessage.warning(message);
setTimeout(() => {
const element = document.querySelectorAll(
".el-form-item__error"
)[0];
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
}, 100);
}
});
});
};
return { validate, formRef };
}

View File

@ -0,0 +1,125 @@
import { nextTick, ref, useTemplateRef } from "vue";
import { getDataType } from "@/assets/js/utils.js";
import {
getQueryCriteria,
setQueryCriteria,
} from "@/assets/js/useQueryCriteria.js";
/**
* @param api {Function} 接口函数
* @param options {Object?: {callback, params, defaultSearchForm, immediate, usePagination, key}} 配置项
* @param options.callback {Function?} 回调函数返回值第一个参数表格数据第二个参数后台返回的所有数据
* @param options.params {Object?} 其它接口参数
* @param options.defaultSearchForm {Object?} searchForm默认值
* @param options.immediate {Boolean?} 是否立即执行接口函数默认是
* @param options.usePagination {Boolean?} 是否使用分页默认是
* @param options.clearSelection {Boolean?} 调用resetPagination是是否清空表格选择数据默认是
* @param options.key {String?} 返回的存放数组的key默认list
* @param options.isStorageQueryCriteria {Boolean?} 是否保存查询条件默认是
* @param options.tabsActiveName {String?} 存在tabs组件时当前tabs的activeName用于缓存查询条件
* @return {Object} 返回对象包含以下属性list 表格数据pagination 分页数据searchForm 搜索表单数据tableRef 表格实例getData 获取数据函数resetPagination 重置分页函数
*/
export default function useListData(api, options = {}) {
if (getDataType(api) !== "Function") throw new Error("api必须是一个函数");
if (getDataType(options) !== "Object")
throw new Error("options必须是一个对象");
if (options.immediate && getDataType(options.immediate) !== "Boolean")
throw new Error("options.immediate必须是一个布尔值");
if (options.usePagination && getDataType(options.usePagination) !== "Boolean")
throw new Error("options.usePagination必须是一个布尔值");
if (options.key && getDataType(options.key) !== "String")
throw new Error("options.key必须是一个字符串");
if (
options.callback &&
getDataType(options.callback) !== "Function" &&
getDataType(options.callback) !== "AsyncFunction"
)
throw new Error("options.callback必须是一个函数");
if (
options.defaultSearchForm &&
getDataType(options.defaultSearchForm) !== "Object"
)
throw new Error("options.defaultSearchForm必须是一个对象");
if (
options.clearSelection &&
getDataType(options.clearSelection) !== "Boolean"
)
throw new Error("options.clearSelection必须是一个布尔值");
if (options.params && getDataType(options.params) !== "Object")
throw new Error("options.otherParams必须是一个对象");
if (
options.isStorageQueryCriteria &&
getDataType(options.isStorageQueryCriteria) !== "Boolean"
)
throw new Error("options.isStorageQueryCriteria必须是一个布尔值");
if (
options.tabsActiveName &&
getDataType(options.tabsActiveName) !== "String"
)
throw new Error("options.tabsActiveName必须是一个字符串");
const immediate = options.immediate ?? true;
const usePagination = options.usePagination ?? true;
const key = options.key ?? "list";
const defaultSearchForm = options.defaultSearchForm ?? {};
const clearSelection = options.clearSelection ?? true;
const isStorageQueryCriteria = options.isStorageQueryCriteria ?? true;
const list = ref([]);
const queryCriteria = getQueryCriteria();
const pagination = ref(
queryCriteria.pagination || {
currentPage: 1,
pageSize: 10,
total: 0,
}
);
const searchForm = ref(queryCriteria.searchForm || defaultSearchForm);
const tableRef = useTemplateRef("tableRef");
const getData = async (params = {}) => {
const resData = await api({
...(usePagination
? {
curPage: pagination.value.currentPage,
limit: pagination.value.pageSize,
}
: {}),
...searchForm.value,
...(options.params || {}),
...(getDataType(params) === "Object" ? params : {}),
});
list.value = usePagination ? resData.page[key] : resData[key];
if (usePagination) pagination.value.total = resData.page.totalCount;
options.callback && options.callback(list.value, resData);
!usePagination &&
clearSelection &&
tableRef.value &&
tableRef.value.clearSelection();
if (isStorageQueryCriteria) {
setQueryCriteria({
searchForm: searchForm.value,
pagination: pagination.value,
tabsActiveName: options.tabsActiveName,
});
}
};
immediate && getData().then();
const resetPagination = async (params) => {
list.value = [];
pagination.value = {
currentPage: 1,
pageSize: 10,
total: 0,
};
await nextTick();
await getData(params);
clearSelection && tableRef.value && tableRef.value.clearSelection();
};
return {
list,
pagination,
searchForm,
tableRef,
getData: async (params) => await getData(params),
resetPagination: async (params) => await resetPagination(params),
};
}

View File

@ -0,0 +1,54 @@
import { useMiscellaneousStore } from "@/pinia/miscellaneous.js";
export const getQueryCriteria = () => {
const miscellaneousStore = useMiscellaneousStore();
const key = window.location.href;
let queryCriteria = miscellaneousStore.getQueryCriteria[key] || {};
if (queryCriteria.tabsActiveName) {
queryCriteria =
miscellaneousStore.getQueryCriteria[
key + "/" + queryCriteria.tabsActiveName
] || {};
}
const pagination = queryCriteria.pagination;
const searchForm = queryCriteria.searchForm;
const tabsActiveName = queryCriteria.tabsActiveName;
return {
pagination,
searchForm,
tabsActiveName,
};
};
export const setQueryCriteria = (data) => {
const miscellaneousStore = useMiscellaneousStore();
let key = window.location.href;
if (data.tabsActiveName) {
miscellaneousStore.setQueryCriteria({
...miscellaneousStore.getQueryCriteria,
[key]: {
...miscellaneousStore.getQueryCriteria[key],
tabsActiveName: data.tabsActiveName,
},
});
key = key + "/" + data.tabsActiveName;
}
miscellaneousStore.setQueryCriteria({
...miscellaneousStore.getQueryCriteria,
[key]: {
...miscellaneousStore.getQueryCriteria[key],
...data,
},
});
};
export const resetQueryCriteria = () => {
const miscellaneousStore = useMiscellaneousStore();
miscellaneousStore.resetQueryCriteria();
};
export const getTabsActiveName = () => {
const key = window.location.href;
const miscellaneousStore = useMiscellaneousStore();
const queryCriteria = miscellaneousStore.getQueryCriteria[key] || {};
return queryCriteria.tabsActiveName;
};

View File

@ -0,0 +1,13 @@
import { ref } from "vue";
const useSearchCollapse = () => {
const collapse = ref(true);
const changeSearchCollapse = () => {
collapse.value = !collapse.value;
};
return {
collapse,
changeSearchCollapse,
};
};
export default useSearchCollapse;

386
src/assets/js/utils.js Normal file
View File

@ -0,0 +1,386 @@
import { ElMessage } from "element-plus";
/**
* @description 计算序号
* @param {Object} pagination 分页数据对象
* @param {number | string} pagination.currentPage 当前页
* @param {number | string} pagination.pageSize 每页条数
* @param {number} index 当页数据的索引值
* @return {number} 序号
**/
export function serialNumber(pagination, index) {
return (pagination.currentPage - 1) * pagination.pageSize + (index + 1);
}
/**
* @description 字符串数组转数组
* @param {string} value 转换的字符串数组
* @return {Array} 转换后的数组
**/
export function toArrayString(value) {
// eslint-disable-next-line no-eval
return value ? eval(value).map(String) : [];
}
/**
* @description 判断文件后缀名是否符合
* @param {string} name 文件名字
* @param {string} suffix 文件后缀
* @return {boolean} 是否符合
**/
export function interceptTheSuffix(name, suffix) {
return (
name.substring(name.lastIndexOf("."), name.length).toLowerCase() ===
suffix.toLowerCase()
);
}
/**
* @description 图片转base64
* @param {string} imgUrl 图片地址
* @return {Promise} Promise实例then包含base64编码
**/
export function image2Base64(imgUrl) {
return new Promise((resolve) => {
const img = new Image();
img.src = imgUrl;
img.crossOrigin = "Anonymous";
img.onload = function () {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
const ext = img.src.substring(img.src.lastIndexOf(".") + 1).toLowerCase();
resolve(canvas.toDataURL("image/" + ext));
};
});
}
/**
* @description 判断图片是否可访问成功
* @param {string} imgUrl 图片地址
* @return {Promise} Promise实例
**/
export function checkImgExists(imgUrl) {
return new Promise((resolve, reject) => {
const ImgObj = new Image();
ImgObj.src = imgUrl;
ImgObj.onload = function (res) {
resolve(res);
};
ImgObj.onerror = function (err) {
reject(err);
};
});
}
/**
* @description 获取数据类型
* @param {any} data 数据
* @return {string} 数据类型
**/
export function getDataType(data) {
return Object.prototype.toString.call(data).slice(8, -1);
}
/**
* @description 数组去重
* @param {Array<number,string>} arr 去重的数组
* @return {Array} 去重后的数组
**/
export function ArrayDeduplication(arr) {
return [...new Set(arr)];
}
/**
* @description 数组对象去重
* @param {Array} arr 去重的数组
* @param {string} name 去重的key
* @return {Array} 去重后的数组
**/
export function arrayObjectDeduplication(arr, name) {
const obj = {};
arr = arr.reduce(function (previousValue, currentValue) {
if (!obj[currentValue[name]]) {
obj[currentValue[name]] = true;
previousValue.push(currentValue);
}
return previousValue;
}, []);
return arr;
}
/**
* @description 查找字符串中指定的值第几次出现的位置
* @param {Array} str 查找的字符串数组
* @param {string} char 查找的值
* @param {number} num 第几次出现
* @return {number} 出现的位置
**/
export function findCharIndex(str, char, num) {
let index = str.indexOf(char);
if (index === -1) return -1;
for (let i = 0; i < num - 1; i++) {
index = str.indexOf(char, index + 1);
if (index === -1) return -1;
}
return index;
}
/**
* @description 生成指定两个值之间的随机数
* @param {number} min 最小值
* @param {number} max 最大值
* @return {number} 随机数
**/
export function randoms(min, max) {
return Math.random() * (max - min + 1) + min;
}
/**
* @description 千位分隔符
* @param {number | string} num 转换的值
* @return {string} 转换后的值
**/
export function numFormat(num) {
if (num) {
const numArr = num.toString().split(".");
const arr = numArr[0].split("").reverse();
let res = [];
for (let i = 0; i < arr.length; i++) {
if (i % 3 === 0 && i !== 0) {
res.push(",");
}
res.push(arr[i]);
}
res.reverse();
if (numArr[1]) {
res = res.join("").concat("." + numArr[1]);
} else {
res = res.join("");
}
return res;
}
}
/**
* @description 验证是否为空
* @param {any} value 验证的值
* @return {boolean} 是否为空
**/
export function isEmpty(value) {
return (
value === undefined ||
value === null ||
(typeof value === "object" && Object.keys(value).length === 0) ||
(typeof value === "string" && value.trim().length === 0)
);
}
/**
* @description 获取url参数
* @param {string} name 获取的key
* @return {string} 获取的值
**/
export function getUrlParam(name) {
const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
const r = window.location.search.substr(1).match(reg);
if (r != null) return decodeURI(r[2]);
return "";
}
/**
* @description 数据分页
* @param {Array} list 分页的数组
* @param {number | string} currentPage 当前页
* @param {number | string} pageSize 每页条数
* @return {Array} 分页后的数组
**/
export function paging(list, currentPage, pageSize) {
return list.filter((item, index) => {
return (
index < +currentPage * +pageSize &&
index >= (+currentPage - 1) * +pageSize
);
});
}
/**
* @description 获取文件后缀
* @param {string} name 文件名
* @return {string} 文件后缀
**/
export function getFileSuffix(name) {
return name.substring(name.lastIndexOf(".") + 1);
}
/**
* @description 获取文件名称
* @param {string} name 文件地址
* @return {string} 文件名称
**/
export function getFileName(name) {
if (!name) return "";
return name.substring(name.lastIndexOf("/") + 1);
}
/**
* @description 读取txt文档
* @param {string} filePah 文档路径
* @return {resolve,string} 读取后的内容
**/
export function readTxtDocument(filePah) {
return new Promise((resolve) => {
const FILE_URL = import.meta.env.VITE_FILE_URL;
const file_url = FILE_URL + filePah;
const xhr = new XMLHttpRequest();
xhr.open("get", file_url, true);
xhr.responseType = "blob";
xhr.onload = function (event) {
const reader = new FileReader();
reader.readAsText(event.target.response, "GB2312");
reader.onload = function () {
resolve(reader.result);
};
};
xhr.send();
});
}
/**
* @description 将秒转换成时分秒
* @param {string,number} second 需要转换的秒数
* @return {string} 转换后的时间
**/
export function secondConversion(second) {
if (!second) return 0;
const h = parseInt(second / 60 / 60, 10);
const m = parseInt((second / 60) % 60, 10);
const s = parseInt(second % 60, 10);
if (h) {
return h + "小时" + m + "分钟" + s + "秒";
} else {
if (m) {
return m + "分钟" + s + "秒";
} else {
return s + "秒";
}
}
}
/**
* @description 附件添加前缀
* @param {Array} list 附件数组
* @return {Array} 添加完的数组
**/
export function addingPrefixToFile(list) {
const FILE_URL = import.meta.env.VITE_FILE_URL;
for (let i = 0; i < list.length; i++) {
list[i].url = FILE_URL + list[i].filepath;
list[i].name = getFileName(list[i].filepath);
}
return list;
}
/**
* @description 验证重复选择
* @param {Array} list 验证的数组
* @param {number} index 选择的索引
* @param {string} key 验证的字段
* @param {string} id 验证的值
**/
export async function verifyDuplicateSelection(list, index, key, id) {
return new Promise((resolve, reject) => {
if (list.some((item) => item[key] === id)) {
ElMessage.warning("不能重复选择");
// eslint-disable-next-line prefer-promise-reject-errors
reject();
} else {
list[index][key] = id;
resolve();
}
});
}
/**
* @description 翻译状态
* @param {number | string} status 状态
* @param {Array} list 翻译的数组
* @return {string} 翻译后的状态
**/
export function translationStatus(status, list) {
for (let i = 0; i < list.length; i++) {
if (status === list[i].ID) {
return list[i].NAME;
}
}
}
/**
* @description 计算文件大小
* @param {number | string} size 文件kb
* @return {string} 计算后的文件大小
**/
export function calculateFileSize(size) {
return size > 1024
? (size / 1024 + "").substring(0, (size / 1024 + "").lastIndexOf(".") + 3) +
"MB"
: size + "KB";
}
/**
* @description 根据身份证号获取出生日期和性别
* @param {String} idCard 身份证号
* @return {Object} 出生日期和性别 date sex
**/
export function idCardGetDateAndGender(idCard) {
const reg =
/^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
let sex = "";
let date = "";
if (reg.test(idCard)) {
const org_birthday = idCard.substring(6, 14);
const org_gender = idCard.substring(16, 17);
const birthday =
org_birthday.substring(0, 4) +
"-" +
org_birthday.substring(4, 6) +
"-" +
org_birthday.substring(6, 8);
const birthdays = new Date(birthday.replace(/-/g, "/"));
const Month = birthdays.getMonth() + 1;
let MonthDate;
const DayDate = birthdays.getDate();
let Day;
if (Month < 10) MonthDate = "0" + Month;
else MonthDate = Month;
if (DayDate < 10) Day = "0" + DayDate;
else Day = DayDate;
sex = org_gender % 2 === 1 ? "1" : "0";
date = birthdays.getFullYear() + "-" + MonthDate + "-" + Day;
}
return { sex, date };
}
/**
* @description 获取select的label
* @param {Array} list 获取的数组
* @param {number | string} value 获取的值
* @param {string?} idKey 获取的id
* @param {string?} labelKey 获取的label
* @return {string} 获取的label
**/
export function getSelectLabel(
list,
value,
idKey = "bianma",
labelKey = "name"
) {
const result = list.find((item) => item[idKey] === value);
return result ? result[labelKey] : "";
}
export function getFileUrl() {
return import.meta.env.VITE_FILE_URL;
}

View File

@ -0,0 +1,9 @@
<template>
<router-view />
</template>
<script setup>
defineOptions({
name: "Children",
});
</script>

View File

@ -0,0 +1,59 @@
<template>
<el-tree-select
v-model="modelValue"
:data="tree"
node-key="departmentId"
:props="{
children: 'list',
label: 'name',
}"
:render-after-expand="false"
accordion
:check-strictly="checkStrictly"
:clearable="clearable"
:show-checkbox="showCheckbox"
:multiple="multiple"
:collapse-tags="collapseTags"
@change="change"
/>
</template>
<script setup>
import { appFnGetDepartmentTree } from "@/assets/js/data_dictionary";
import { nextTick } from "vue";
defineOptions({
name: "AppDepartment",
});
defineProps({
checkStrictly: {
type: Boolean,
default: true,
},
showCheckbox: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: false,
},
collapseTags: {
type: Boolean,
default: false,
},
clearable: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(["change"]);
const modelValue = defineModel({ type: [String, Array], required: true });
const change = async () => {
await nextTick();
emits("change", modelValue.value);
};
const tree = await appFnGetDepartmentTree(0);
</script>
<style scoped></style>

View File

@ -0,0 +1,48 @@
<template>
<div style="flex: 1">
<toolbar :editor="editorRef" :default-config="toolbarConfig" />
<editor
v-model="modelValue"
:style="{ height, 'overflow-y': 'hidden' }"
@on-created="fnEditorCreated"
/>
</div>
</template>
<script setup>
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import "@wangeditor/editor/dist/css/style.css";
import { shallowRef, onBeforeUnmount } from "vue";
defineOptions({
name: "AppEditor",
});
defineProps({
height: {
type: String,
default: "300px",
},
});
const modelValue = defineModel({ type: String, required: true });
const editorRef = shallowRef();
const toolbarConfig = {
excludeKeys: [
"color",
"bgColor",
"group-image",
"group-video",
"insertLink",
"codeBlock",
"emotion",
"todo",
],
};
const fnEditorCreated = (editor) => {
editorRef.value = editor;
};
onBeforeUnmount(() => {
editorRef.value && editorRef.value.destroy();
});
</script>
<style scoped></style>

View File

@ -0,0 +1,71 @@
<template>
<el-dialog v-model="visible" :title="title" :before-close="fnClose">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="附件" prop="file">
<app-upload v-model:file-list="form.file" accept=".xls,.xlsx" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="fnExportTemplates"></el-button>
<el-button type="primary" @click="fnSubmit"></el-button>
<el-button @click="fnClose"></el-button>
</template>
</el-dialog>
</template>
<script setup>
import AppUpload from "@/components/upload/index.vue";
import { ElMessageBox } from "element-plus";
import { ref, watchEffect } from "vue";
import { debounce } from "throttle-debounce";
import useFormValidate from "@/assets/js/useFormValidate.js";
defineOptions({
name: "AppImportFile",
});
const props = defineProps({
title: {
type: String,
default: "导入",
},
templateUrl: {
type: String,
required: true,
},
});
const emits = defineEmits(["submit"]);
const visible = defineModel("visible", { type: Boolean, required: true });
const rules = {
file: [{ required: true, message: "附件不能为空", trigger: "change" }],
};
const { formRef, validate } = useFormValidate();
const form = ref({
file: [],
});
const fnExportTemplates = async () => {
await ElMessageBox.confirm("确定要下载excel模板吗", { type: "warning" });
window.open("https://file.zcloudchina.com/KjkFile" + props.templateUrl);
};
watchEffect(() => {
if (!props.visible) {
formRef.value && formRef.value.resetFields();
}
});
const fnClose = () => {
formRef.value.resetFields();
visible.value = false;
};
const fnSubmit = debounce(
1000,
async () => {
await validate("请上传附件");
const formData = new FormData();
formData.append("paperFiles", form.value.file[0].raw);
formData.append("FFILEName", form.value.file[0].name);
emits("submit", formData);
},
{ atBegin: true }
);
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,37 @@
<template>
<el-pagination
size="small"
:current-page="pagination.currentPage || 1"
:page-size="pagination.pageSize || 10"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total || 0"
@update:current-page="handleCurrentChange"
@update:page-size="handleSizeChange"
/>
</template>
<script setup>
defineOptions({
name: "AppPagination",
});
const emits = defineEmits(["get-data"]);
const pagination = defineModel("pagination", { type: Object, required: true });
const handleCurrentChange = (val) => {
pagination.value = {
currentPage: val,
pageSize: pagination.value.pageSize,
total: pagination.value.total,
};
emits("get-data");
};
const handleSizeChange = (val) => {
pagination.value = {
currentPage: 1,
pageSize: val,
total: pagination.value.total,
};
emits("get-data");
};
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,89 @@
<template>
<el-dialog
title="文档"
:model-value="visible && model === 'dialog'"
:append-to-body="appendToBody"
@update:model-value="visible = false"
>
<div v-if="visible" style="height: 690px; overflow-y: auto">
<vue-pdf
v-for="page in numOfPages"
:key="page"
:src="fnSrc(props.src)"
:page="page"
/>
</div>
<template #footer>
<el-button type="primary" @click="fnDownload"> </el-button>
</template>
</el-dialog>
<div
v-if="model === 'normal'"
:key="src"
style="height: 690px; overflow-y: auto"
>
<vue-pdf
v-for="page in numOfPages"
:key="page"
:src="fnSrc(props.src)"
:page="page"
/>
</div>
</template>
<script setup>
import { watchEffect, ref } from "vue";
import { VuePdf, createLoadingTask } from "vue3-pdfjs/esm";
import { ElMessage, ElMessageBox } from "element-plus";
const VITE_FILE_URL = import.meta.env.VITE_FILE_URL;
defineOptions({
name: "AppPdf",
});
const props = defineProps({
src: {
type: String,
required: true,
},
model: {
type: String,
validator: (value) => {
const typeList = ["dialog", "normal"];
if (typeList.includes(value)) {
return true;
} else {
throw new Error(`model必须是${typeList.join("、")}之一`);
}
},
default: "dialog",
},
appendToBody: {
type: Boolean,
default: false,
},
});
const fnDownload = async () => {
await ElMessageBox.confirm("确定要下载吗?", { type: "warning" });
window.open(VITE_FILE_URL + props.src);
};
const visible = defineModel("visible", { type: Boolean, default: false });
const numOfPages = ref(0);
const fnSrc = (src) => {
if (!src) return;
if (src.indexOf("http") !== -1 || src.indexOf("https") !== -1) return src;
else return VITE_FILE_URL + src;
};
watchEffect(() => {
if (visible.value || props.src) {
const loadingTask = createLoadingTask(fnSrc(props.src));
loadingTask.promise
.then((pdf) => {
numOfPages.value = pdf.numPages;
})
.catch(() => {
visible.value = false;
ElMessage.error("文件加载失败");
});
}
});
</script>

View File

@ -0,0 +1,27 @@
<template>
<div class="tc mt-20 mb-20">
<img :src="src" alt="" width="200" height="200" />
</div>
</template>
<script setup>
import { useQRCode } from "@vueuse/integrations/useQRCode";
defineOptions({
name: "AppQrCode",
});
const props = defineProps({
src: {
type: String,
required: true,
},
});
const src = useQRCode(() => props.src, {
width: 200,
height: 200,
margin: 1,
correctLevel: "H",
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,48 @@
<template>
<el-form
:model="modelValue"
:label-width="labelWidth"
@submit.prevent="emits('submit')"
>
<el-row>
<slot :collapse="collapse"></slot>
<el-col :span="6">
<el-form-item label-width="10px">
<el-button type="primary" native-type="submit">搜索</el-button>
<el-button native-type="reset" @click="emits('submit')">
重置
</el-button>
<app-search-collapse-button
v-if="showCollapseButton"
:change-search-collapse="changeSearchCollapse"
:collapse="collapse"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup>
import AppSearchCollapseButton from "@/components/search_collapse_button/index.vue";
import useSearchCollapse from "@/assets/js/useSearchCollapse.js";
const { collapse, changeSearchCollapse } = useSearchCollapse();
defineOptions({
name: "AppSearch",
});
defineProps({
labelWidth: {
type: String,
default: "100px",
},
showCollapseButton: {
type: Boolean,
default: false,
},
});
const modelValue = defineModel({ type: Object, required: true });
const emits = defineEmits(["submit"]);
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,42 @@
<template>
<el-button
v-if="collapse"
:icon="ArrowDown"
link
text
type="primary"
@click="changeSearchCollapse"
>
展开
</el-button>
<el-button
v-if="!collapse"
:icon="ArrowUp"
link
text
type="primary"
@click="changeSearchCollapse"
>
收起
</el-button>
</template>
<script setup>
import { ArrowDown, ArrowUp } from "@element-plus/icons-vue";
defineOptions({
name: "AppSearchCollapseButton",
});
defineProps({
collapse: {
type: Boolean,
required: true,
},
changeSearchCollapse: {
type: Function,
required: true,
},
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,177 @@
<template>
<el-table
ref="tableRef"
size="small"
:data="data"
:border="border"
:stripe="stripe"
:height="height"
:max-height="maxHeight"
:highlight-current-row="highlightCurrentRow"
:row-key="getRowKey"
:row-class-name="rowClassName"
:row-style="rowStyle"
:show-header="showHeader"
:show-summary="showSummary"
:summary-method="summaryMethod"
:span-method="spanMethod"
:default-expand-all="defaultExpandAll"
:tree-props="treeProps"
:header-cell-style="headerCellStyle"
:cell-style="cellStyle"
@row-click="rowClick"
@row-dblclick="rowDblclick"
>
<el-table-column
v-if="showSelection"
type="selection"
reserve-selection
width="60"
/>
<el-table-column v-if="showIndex" label="序号" width="60">
<template #default="{ $index }">
{{ serialNumber(pagination, $index) }}
</template>
</el-table-column>
<slot></slot>
</el-table>
<div v-if="showPagination || slots.button" class="table_footer">
<div>
<slot name="button"></slot>
</div>
<app-pagination
v-if="showPagination"
v-model:pagination="pagination"
@get-data="emits('get-data')"
/>
</div>
</template>
<script setup>
import { useSlots, useTemplateRef } from "vue";
import AppPagination from "@/components/pagination/index.vue";
import { serialNumber } from "@/assets/js/utils.js";
const slots = useSlots();
defineOptions({
name: "AppTable",
});
const props = defineProps({
data: {
type: Array,
default: () => [],
},
showPagination: {
type: Boolean,
default: true,
},
showIndex: {
type: Boolean,
default: true,
},
showSelection: {
type: Boolean,
default: false,
},
stripe: {
type: Boolean,
default: true,
},
border: {
type: Boolean,
default: true,
},
showHeader: {
type: Boolean,
default: true,
},
highlightCurrentRow: {
type: Boolean,
default: false,
},
showSummary: {
type: Boolean,
default: false,
},
defaultExpandAll: {
type: Boolean,
default: false,
},
rowKey: {
type: [String, Function],
},
maxHeight: {
type: [String, Number],
},
height: {
type: [String, Number],
},
rowClassName: {
type: Function,
},
rowStyle: {
type: Function,
},
summaryMethod: {
type: Function,
},
spanMethod: {
type: Function,
},
treeProps: {
type: Object,
default: () => ({ hasChildren: "hasChildren", children: "children" }),
},
headerCellStyle: {
type: Object,
default: () => ({}),
},
cellStyle: {
type: Object,
default: () => ({}),
},
});
const pagination = defineModel("pagination", {
type: Object,
default: () => ({
currentPage: 1,
pageSize: 10,
total: 0,
}),
});
const emits = defineEmits(["get-data", "row-click", "row-dblclick"]);
const tableRef = useTemplateRef("tableRef");
const getRowKey = (row) => {
if (!props.rowKey) return;
if (typeof props.rowKey === "string") return row[props.rowKey];
else return props.rowKey(row);
};
const rowClick = (row, column, event) => {
emits("row-click", row, column, event);
};
const rowDblclick = (row, column, event) => {
emits("row-dblclick", row, column, event);
};
const getSelectionRows = () => {
return tableRef.value.getSelectionRows();
};
const clearSelection = () => {
return tableRef.value.clearSelection();
};
const toggleRowSelection = (value, selected = true) => {
tableRef.value.toggleRowSelection(value, selected);
};
defineExpose({
getSelectionRows,
clearSelection,
toggleRowSelection,
});
</script>
<style lang="scss" scoped>
.table_footer {
margin-top: 20px;
display: flex;
justify-content: space-between;
}
</style>

View File

@ -0,0 +1,162 @@
<template>
<el-upload
ref="uploadRef"
style="width: 100%"
:file-list="fileList"
:action="action"
multiple
:limit="limit"
:list-type="listType"
:auto-upload="autoUpload"
:disabled="disabled"
:accept="accept"
:on-remove="onRemove"
:before-remove="beforeRemove"
:on-change="onChange"
:on-exceed="onExceed"
:on-preview="onPreview"
:http-request="httpRequest"
:show-file-list="showFileList"
:class="{ hide: fileList.length === limit }"
>
<el-icon v-if="listType === 'picture-card'" size="32"><plus /></el-icon>
<el-button v-else type="primary">点击选择文件上传</el-button>
<template #tip>
<div class="mt-10 text-red"><slot name="tip"></slot></div>
</template>
</el-upload>
<el-dialog v-model="visible" title="查看图片">
<img
:src="imageUrl"
alt="Preview Image"
style="width: 100%; object-fit: scale-down"
/>
</el-dialog>
</template>
<script setup>
import { ref, useTemplateRef } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { setDeleteImg } from "@/request/api";
import { Plus } from "@element-plus/icons-vue";
defineOptions({
name: "AppUpload",
});
const props = defineProps({
autoUpload: {
type: Boolean,
default: false,
},
action: {
type: String,
default: "",
},
limit: {
type: Number,
default: 1,
},
listType: {
type: String,
default: "text",
},
accept: {
type: String,
default: "",
},
ratio: {
type: String,
default: "",
},
disabled: {
type: Boolean,
default: false,
},
deleteToServer: {
type: Boolean,
default: false,
},
showFileList: {
type: Boolean,
default: true,
},
httpRequest: {
type: Function,
},
size: {
type: Number,
default: 0,
},
});
const visible = ref(false);
const imageUrl = ref("");
const fileList = defineModel("fileList", { type: Array, required: true });
const emits = defineEmits(["preview"]);
const uploadRef = useTemplateRef("uploadRef");
const onExceed = () => {
ElMessage.warning(`最多上传${props.limit}个文件`);
};
const beforeRemove = async (uploadFile) => {
if (props.deleteToServer && uploadFile.imgfilesId) {
await ElMessageBox.confirm("确定要删除吗?", {
type: "warning",
});
await setDeleteImg({
imgfilesId: uploadFile.imgfilesId,
filepath: uploadFile.filepath,
});
}
};
const onRemove = (uploadFile, uploadFiles) => {
fileList.value = uploadFiles;
};
const onChange = (uploadFile, uploadFiles) => {
const accept = props.accept && props.accept.split(",");
const ratio = props.ratio && props.ratio.split("*");
const suffix = uploadFile.raw.name.substring(
uploadFile.raw.name.lastIndexOf("."),
uploadFile.raw.name.length
);
const size = props.size * 1024 * 1024;
if (ratio) {
const img = new Image();
img.src = uploadFile.url;
img.onload = () => {
if (img.width !== +ratio[0] && img.height !== +ratio[1]) {
ElMessage.warning(`只能上传${props.ratio}分辨率的图片`);
uploadRef.value.handleRemove(uploadFile.raw);
}
};
}
if (size) {
if (uploadFile.size > size) {
ElMessage.warning(`文件大小不能超过${props.size}M`);
uploadRef.value.handleRemove(uploadFile.raw);
}
}
if (accept) {
if (accept.includes(suffix)) {
fileList.value = uploadFiles;
} else {
ElMessage.warning(`只能上传${props.accept}格式的文件`);
uploadRef.value.handleRemove(uploadFile.raw);
}
} else {
fileList.value = uploadFiles;
}
};
const onPreview = (uploadFile) => {
if (props.listType === "picture-card") {
visible.value = true;
imageUrl.value = uploadFile.url;
} else {
emits("preview", uploadFile);
}
};
</script>
<style scoped lang="scss">
.hide :deep(.el-upload--picture-card) {
display: none;
}
</style>

View File

@ -0,0 +1,209 @@
<template>
<div class="mi-captcha">
<div class="mi-captcha-content">
<!-- 没有进行验证-->
<div
v-if="!verificationPass"
class="mi-captcha-radar"
@click="verificationShow = true"
>
<div class="mi-captcha-radar-ready">
<div class="mi-captcha-radar-ring" />
<div class="mi-captcha-radar-dot" />
</div>
<div class="mi-captcha-radar-tip">点击按钮进行验证</div>
</div>
<!-- 验证通过-->
<div
v-if="verificationPass"
class="mi-captcha-radar mi-captcha-radar-pass"
>
<div class="mi-captcha-radar-success mi-captcha-radar-success-icon">
<span role="img" aria-label="verified">
<svg
focusable="false"
class=""
data-icon="verified"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
viewBox="64 64 896 896"
>
<path
d="M447.8 588.8l-7.3-32.5c-.2-1-.6-1.9-1.1-2.7a7.94 7.94 0 00-11.1-2.2L405 567V411c0-4.4-3.6-8-8-8h-81c-4.4 0-8 3.6-8 8v36c0 4.4 3.6 8 8 8h37v192.4a8 8 0 0012.7 6.5l79-56.8c2.6-1.9 3.8-5.1 3.1-8.3zm-56.7-216.6l.2.2c3.2 3 8.3 2.8 11.3-.5l24.1-26.2a8.1 8.1 0 00-.3-11.2l-53.7-52.1a8 8 0 00-11.2.1l-24.7 24.7c-3.1 3.1-3.1 8.2.1 11.3l54.2 53.7z"
/>
<path
d="M866.9 169.9L527.1 54.1C523 52.7 517.5 52 512 52s-11 .7-15.1 2.1L157.1 169.9c-8.3 2.8-15.1 12.4-15.1 21.2v482.4c0 8.8 5.7 20.4 12.6 25.9L499.3 968c3.5 2.7 8 4.1 12.6 4.1s9.2-1.4 12.6-4.1l344.7-268.6c6.9-5.4 12.6-17 12.6-25.9V191.1c.2-8.8-6.6-18.3-14.9-21.2zM810 654.3L512 886.5 214 654.3V226.7l298-101.6 298 101.6v427.6z"
/>
<path
d="M452 297v36c0 4.4 3.6 8 8 8h108v274h-38V405c0-4.4-3.6-8-8-8h-35c-4.4 0-8 3.6-8 8v210h-31c-4.4 0-8 3.6-8 8v37c0 4.4 3.6 8 8 8h244c4.4 0 8-3.6 8-8v-37c0-4.4-3.6-8-8-8h-72V493h58c4.4 0 8-3.6 8-8v-35c0-4.4-3.6-8-8-8h-58V341h63c4.4 0 8-3.6 8-8v-36c0-4.4-3.6-8-8-8H460c-4.4 0-8 3.6-8 8z"
/>
</svg>
</span>
</div>
<div class="mi-captcha-radar-tip">通过验证</div>
</div>
</div>
</div>
<verification
:show="verificationShow"
@success="verificationSuccess"
@close="verificationClose"
/>
</template>
<script setup>
import Verification from "vue3-puzzle-vcode";
import { ref } from "vue";
defineOptions({
name: "AppVerification",
});
const verificationPass = defineModel("verificationPass", {
type: Boolean,
default: false,
});
const verificationShow = ref(false);
const verificationClose = () => {
verificationShow.value = false;
};
const verificationSuccess = () => {
verificationPass.value = true;
verificationClose();
};
</script>
<style scoped lang="scss">
.mi-captcha {
width: 100%;
flex: 1;
height: 2.625rem;
font-family: "Pingfang SC", "Microsoft YaHei", "Monospaced Number",
"Chinese Quote", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"PingFang SC", "Hiragino Sans GB", "Helvetica Neue", Helvetica, Arial,
sans-serif;
}
.mi-captcha-content {
width: 100%;
height: 100%;
position: relative;
}
.mi-captcha-radar {
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
height: 100%;
width: 100%;
background-color: #1d1e23;
background-image: linear-gradient(315deg, #a8b4d3 0%, #adc0ed 74%);
border: 1px solid #96a4c8;
cursor: pointer;
min-width: 10rem;
position: relative;
border-radius: 4px;
}
.mi-captcha-radar-ready,
.mi-captcha-radar-success {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
flex-wrap: nowrap;
position: relative;
transition: all 0.4s ease;
width: 1.875rem;
height: 1.875rem;
margin: 0.375rem;
}
.mi-captcha-radar-ring,
.mi-captcha-radar-dot {
position: absolute;
border-radius: 50%;
transform: scale(0.4);
width: 100%;
height: 100%;
box-shadow: inset 0 0 0 1px #2c67ec;
background-image: linear-gradient(0, rgba(0, 0, 0, 0) 50%, #fff 50%),
linear-gradient(0, #fff 50%, rgba(0, 0, 0, 0) 50%);
}
.mi-captcha-radar-dot {
background: #2c67ec;
}
.mi-captcha-radar-ring {
animation: mi-anim-wait 1s infinite;
transform: scale(1);
}
.mi-captcha-radar-success {
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
cursor: default;
}
.mi-captcha-radar-success-icon {
color: #f6ca9d;
animation-name: mi-captcha-success;
animation-timing-function: ease;
animation-iteration-count: 1;
animation-delay: 0.5s;
animation-duration: 0.4s;
font-size: 1.25rem;
}
.mi-captcha-radar-tip {
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
height: 2.625rem;
padding-left: 0.125rem;
font-size: 0.875rem;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.mi-captcha-radar-pass .mi-captcha-radar-tip {
color: #f6ca9d;
}
@keyframes mi-captcha-success {
25% {
transform: rotate(25deg);
}
100% {
transform: rotate(-360deg);
}
}
@keyframes mi-anim-wait {
60% {
transform: scale(0.75);
}
}
:deep {
.vue-auth-box_ {
background: #2e63d8 !important;
border: 1px solid #2752b3 !important;
}
.mi-captcha-radar-pass .mi-captcha-radar-tip {
color: #ffffff !important;
}
.mi-captcha-radar-success-icon {
color: #ffffff !important;
}
}
</style>

View File

@ -0,0 +1,55 @@
<template>
<el-dialog v-model="visible" title="视频" width="840px" @close="fnClose">
<video
v-if="visible"
:id="className"
playsinline
controls
:data-poster="poster"
style="width: 100%"
>
<source :src="fnGetSrc()" type="video/mp4" />
</video>
</el-dialog>
</template>
<script setup>
import Plyr from "plyr";
import "plyr/dist/plyr.css";
import { nextTick, ref, watchEffect } from "vue";
import { uniqueId } from "lodash-es";
import { addLocalFilePrefix } from "@/assets/js/utils.js";
let player = null;
defineOptions({
name: "AppVideo",
});
const props = defineProps({
src: {
type: String,
required: true,
},
poster: {
type: String,
},
});
const visible = defineModel("visible", { type: Boolean, required: true });
const className = ref(uniqueId("_"));
const fnCreateVideo = async () => {
await nextTick();
const video = document.querySelector(`#${className.value}`);
player = new Plyr(video);
};
watchEffect(() => {
if (props.visible || props.src) fnCreateVideo();
});
const fnClose = () => {
player && player.destroy();
player = null;
visible.value = false;
};
const fnGetSrc = () => {
return addLocalFilePrefix(props.src);
};
</script>

View File

@ -0,0 +1,64 @@
<template>
<div
style="display: flex; justify-content: space-between; align-items: center"
>
<div class="breadcrumb">
<el-breadcrumb class="app-breadcrumb" separator=">">
<transition-group name="breadcrumb">
<el-breadcrumb-item
v-for="(item, index) in breadcrumbList"
:key="item.path"
>
<router-link v-if="index === 0" :to="item.path">
{{ item.meta.title }}
</router-link>
<span v-else-if="index !== breadcrumbList.length - 1">
{{ item.meta.title }}
</span>
<span v-else class="no-redirect">{{ item.meta.title }}</span>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</div>
</div>
</template>
<script setup>
import { ref, watch } from "vue";
import { useRoute } from "vue-router";
defineOptions({
name: "LayoutBreadcrumb",
});
const route = useRoute();
const breadcrumbList = ref([]);
const fnGetBreadcrumb = () => {
const matched = route.matched.filter((item) => item.meta?.title);
if (matched[0].path === "/") matched[0].path = "/index";
breadcrumbList.value = matched.filter(
(item) => item.meta?.title && item.meta?.breadcrumb !== false
);
};
fnGetBreadcrumb();
watch(
() => route,
() => fnGetBreadcrumb(),
{ deep: true }
);
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
font-size: 14px;
line-height: var(--el-header-height);
.no-redirect {
color: #97a8be;
cursor: text;
}
.el-breadcrumb__inner a {
font-weight: normal;
}
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<el-dialog v-model="visible" title="修改密码" width="600px" @close="fnClose">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="旧密码" prop="password">
<el-input v-model="form.password" type="password" />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="form.newPassword" type="password" />
</el-form-item>
<el-form-item label="确认密码" prop="newPasswordConfirm">
<el-input v-model="form.newPasswordConfirm" type="password" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="fnSubmit"></el-button>
<el-button @click="fnClose"></el-button>
</template>
</el-dialog>
</template>
<script setup>
import { useVModel } from "@vueuse/core";
import { ref } from "vue";
import { debounce } from "throttle-debounce";
import useFormValidate from "@/assets/js/useFormValidate.js";
import { setChangePassword } from "@/request/api.js";
const props = defineProps({
visible: {
type: Boolean,
required: true,
},
});
const emits = defineEmits(["update:visible", "submit"]);
const visible = useVModel(props, "visible", emits);
const form = ref({
password: "",
newPassword: "",
newPasswordConfirm: "",
});
const { formRef, validate } = useFormValidate();
const validatePass = (_rule, value, callback) => {
if (value === "") {
callback(new Error("请再次输入新密码"));
} else if (value !== form.value.newPassword) {
callback(new Error("两次输入密码不一致!"));
} else {
callback();
}
};
const rules = {
password: [{ required: true, message: "请输入旧密码", trigger: "blur" }],
newPassword: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 6, max: 18, message: "密码长度为6-18位", trigger: "blur" },
],
newPasswordConfirm: [
{ required: true, validator: validatePass, trigger: "blur" },
],
};
const fnClose = () => {
formRef.value.resetFields();
visible.value = false;
};
const fnSubmit = debounce(
1000,
async () => {
await validate();
await setChangePassword({ ...form.value });
fnClose();
emits("submit");
},
{ atBegin: true }
);
</script>
<style scoped lang="scss"></style>

172
src/layout/header/index.vue Normal file
View File

@ -0,0 +1,172 @@
<template>
<div class="header">
<div class="logo">
<!-- <img-->
<!-- src="/src/assets/images/public/logo1.png"-->
<!-- alt=""-->
<!-- width="134"-->
<!-- height="28"-->
<!-- />-->
</div>
<div class="breadcrumb">
<layout-breadcrumb v-if="route.meta.isBreadcrumb !== false" />
</div>
<div class="right">
<div class="menu">
<ul>
<li
v-for="(item, index) in MENU"
:key="index"
:class="{ active: item.model === menuStore.getModel }"
@click="switchMenu(item.model)"
>
<component
:is="'icon-' + item.icon"
theme="filled"
fill="#a5b2c2"
size="18"
:stroke-width="3"
style="height: 18px"
/>
<div class="title">{{ item.title }}</div>
</li>
</ul>
</div>
<div class="user">
<el-dropdown
trigger="click"
placement="bottom-end"
@command="dropdownCommand"
>
<div class="user_info">
<el-avatar shape="circle" :size="30" fit="fill" :src="tx" />
<span>{{ userStore.getUserInfo.username }}</span>
<icon-down
theme="filled"
size="16"
fill="#a2c2d3"
:stroke-width="3"
/>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="modifyPassword">
修改密码
</el-dropdown-item>
<el-dropdown-item command="signOut">退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<change-password
v-model:visible="passwordDialogVisible"
@submit="fnSignOut"
/>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useMenuStore } from "@/pinia/menu.js";
import { useUserStore } from "@/pinia/user.js";
import { MENU } from "@/assets/js/constant.js";
import { logout } from "@/request/api.js";
import LayoutBreadcrumb from "@/layout/breadcrumb/index.vue";
import tx from "@/assets/images/public/tx.png";
import ChangePassword from "./components/change_password.vue";
defineOptions({
name: "LayoutHeader",
});
const route = useRoute();
const router = useRouter();
const menuStore = useMenuStore();
const userStore = useUserStore();
const passwordDialogVisible = ref(false);
const dropdownCommand = async (command) => {
if (command === "signOut") {
await fnSignOut();
}
if (command === "modifyPassword") {
passwordDialogVisible.value = true;
}
};
const switchMenu = (model) => {
menuStore.setModel(model);
};
const fnSignOut = async () => {
await logout();
userStore.$reset();
await router.replace("/login");
};
</script>
<style lang="scss" scoped>
.header {
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
padding-right: 20px;
background-color: #fff;
.logo {
width: 210px;
height: 50px;
background-color: #2b2f3a;
color: #ffffff;
text-align: center;
line-height: 60px;
margin-right: 20px;
}
.right {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
.menu {
margin-right: 30px;
ul {
display: flex;
li {
height: 50px;
padding: 0 10px;
margin: 0 10px;
cursor: pointer;
display: flex;
align-items: center;
&.active {
color: #79bbff;
border-bottom: 2px solid #79bbff;
}
.title {
padding-left: 10px;
}
}
}
}
.user {
margin-top: 5px;
cursor: pointer;
.el-avatar {
margin-right: 5px;
}
.user_info {
display: flex;
align-items: center;
}
}
}
}
</style>

113
src/layout/index.vue Normal file
View File

@ -0,0 +1,113 @@
<template>
<el-container>
<el-header>
<layout-header />
</el-header>
<el-container>
<el-aside>
<el-scrollbar style="height: calc(100vh - 50px)">
<el-menu
router
unique-opened
:default-active="$route.meta.activeMenu || $route.path"
background-color="rgb(48, 65, 86)"
text-color="#edf7ff"
active-text-color="#409eff"
>
<layout-menu :menus="routes" />
</el-menu>
</el-scrollbar>
</el-aside>
<el-main>
<el-scrollbar style="height: calc(100vh - 50px)">
<router-view v-slot="{ Component }">
<transition name="view" mode="out-in">
<el-card :key="route.path">
<el-page-header
v-if="route.meta.isBack !== false"
title="返回"
@back="router.back()"
>
<template #content>
{{
route.matched.filter((item) => item.meta?.title).at(-1)
.meta.title
}}
</template>
</el-page-header>
<component :is="Component"></component>
</el-card>
</transition>
</router-view>
</el-scrollbar>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import LayoutHeader from "@/layout/header/index.vue";
import LayoutMenu from "@/layout/menu/index.vue";
import { computed } from "vue";
import { useMenuStore } from "@/pinia/menu.js";
import { useRoute, useRouter } from "vue-router";
defineOptions({
name: "Layout",
});
const router = useRouter();
const route = useRoute();
const menuStore = useMenuStore();
const routes = computed(() => menuStore.getMenus);
</script>
<style scoped lang="scss">
.el-header {
--el-header-padding: 0;
--el-header-height: 50px;
}
.el-aside {
width: 210px;
background-color: rgb(48, 65, 86);
}
.el-menu {
width: 210px;
min-height: calc(100vh - 50px);
border-right: none;
background-color: rgb(48, 65, 86);
:deep {
span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
:deep {
.el-sub-menu__title *,
.el-menu-item * {
vertical-align: middle !important;
}
.el-card {
margin: 15px;
border: none !important;
min-height: calc(100vh - 115px);
}
.is-always-shadow {
box-shadow: none !important;
}
}
.el-main {
--el-main-padding: 0;
}
.el-card {
margin: 20px;
border: none !important;
}
</style>

80
src/layout/menu/index.vue Normal file
View File

@ -0,0 +1,80 @@
<template>
<template v-for="menu in props.menus" :key="menu.path">
<!-- 没有二级导航-->
<el-menu-item
v-if="fnIsShowMenuItem(menu)"
:index="menu.path"
@click="fnNavigate"
>
<component
:is="'icon-' + menu.meta?.icon"
v-if="menu.meta?.icon"
theme="filled"
fill="#a5b2c2"
size="18"
:stroke-width="3"
style="margin-right: 15px"
/>
<span>{{ menu.meta?.title }}</span>
</el-menu-item>
<!-- 有二级导航-->
<el-sub-menu v-else-if="fnIsShowSubmenu(menu)" :index="menu.path">
<template #title>
<component
:is="'icon-' + menu.meta?.icon"
v-if="menu.meta?.icon"
theme="filled"
fill="#bfcbd9"
size="18"
:stroke-width="3"
style="margin-right: 10px"
/>
<span>{{ menu.meta?.title }}</span>
</template>
<!-- 递归调用当前组件生成导航-->
<layout-menu :menus="menu.children" />
</el-sub-menu>
</template>
</template>
<script setup>
import { useRouter } from "vue-router";
import { resetQueryCriteria } from "@/assets/js/useQueryCriteria.js";
const router = useRouter();
defineOptions({
name: "LayoutMenu",
});
const props = defineProps({
menus: {
type: Array,
default: () => [],
},
});
const fnIsShowMenuItem = (menu) => {
if (menu.meta?.isMenu === false) {
return false;
}
if (menu.meta?.isSubMenu === false) {
return true;
} else {
return !menu.children || menu.children.length === 0;
}
};
const fnIsShowSubmenu = (menu) => {
if (menu.meta?.isMenu === false) {
return false;
}
if (menu.meta?.isSubMenu === false) {
return false;
} else {
return menu.children && menu.children.length > 0;
}
};
const fnNavigate = (event) => {
resetQueryCriteria();
router.push(event.index);
};
</script>
<style scoped lang="scss"></style>

36
src/main.js Normal file
View File

@ -0,0 +1,36 @@
import { createApp } from "vue";
import "@/assets/css/common.scss";
import "@/assets/css/transition.scss";
import "@/assets/css/element.scss";
import "dayjs/locale/zh-cn";
import App from "./App";
import pinia from "./pinia";
import router from "./router";
import "normalize.css";
import "animate.css";
import "viewerjs/dist/viewer.css";
import VueViewer from "v-viewer";
import print from "vue3-print-nb";
import button from "@/assets/js/button";
import "./addRouters";
import "element-plus/es/components/loading/style/css";
import "element-plus/es/components/message/style/css";
import "element-plus/es/components/message-box/style/css";
import { install } from "@icon-park/vue-next/es/all";
import ElDialog from "element-plus/es/components/dialog/index";
ElDialog.props.closeOnClickModal.default = false;
ElDialog.props.closeOnPressEscape.default = false;
const app = createApp(App);
install(app, "icon");
app
.use(pinia)
.use(router)
.use(VueViewer, {
defaultOptions: {
zIndex: 9999,
},
})
.use(print)
.use(button)
.mount("#app");

7
src/pinia/index.js Normal file
View File

@ -0,0 +1,7 @@
import { createPinia } from "pinia";
import piniaPersistedstate from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(piniaPersistedstate);
export default pinia;

30
src/pinia/menu.js Normal file
View File

@ -0,0 +1,30 @@
import { defineStore } from "pinia";
export const useMenuStore = defineStore("menuStore", {
state: () => ({
menusAll: [],
menus: [],
model: "",
}),
getters: {
getMenus: (state) => state.menus,
getModel: (state) => state.model,
},
actions: {
setMenus(menus) {
this.menusAll = menus;
},
setModel(model) {
this.model = model;
this.menus = [];
for (let i = 0; i < this.menusAll.length; i++) {
if (this.menusAll[i].meta.model === model) {
this.menus.push(this.menusAll[i]);
}
}
},
},
persist: {
storage: window.sessionStorage,
},
});

View File

@ -0,0 +1,24 @@
import { defineStore } from "pinia";
export const useMiscellaneousStore = defineStore("miscellaneousStore", {
state: () => ({
queryCriteria: {},
}),
getters: {
getQueryCriteria() {
return this.queryCriteria;
},
},
actions: {
setQueryCriteria(data) {
this.queryCriteria = data;
},
resetQueryCriteria() {
this.queryCriteria[window.location.href] = {};
},
},
persist: {
storage: window.sessionStorage,
paths: [],
},
});

18
src/pinia/router.js Normal file
View File

@ -0,0 +1,18 @@
import { defineStore } from "pinia";
export const useRouterStore = defineStore("routerStore", {
state: () => ({
routers: [],
}),
getters: {
getRouters: (state) => state.routers,
},
actions: {
setRouters(routers) {
this.routers = routers;
},
},
persist: {
storage: window.sessionStorage,
},
});

33
src/pinia/user.js Normal file
View File

@ -0,0 +1,33 @@
import { defineStore } from "pinia";
export const useUserStore = defineStore("userStore", {
state: () => ({
userInfo: {},
token: "",
tokenTime: "1",
permissions: [],
}),
getters: {
getUserInfo: (state) => state.userInfo,
getToken: (state) => state.token,
getTokenTime: (state) => state.tokenTime,
getPermissions: (state) => state.permissions,
},
actions: {
setUserInfo(userInfo) {
this.userInfo = userInfo;
},
async setToken(token) {
this.token = token;
},
async setTokenTime(tokenTime) {
this.tokenTime = tokenTime;
},
setPermissions(permissions) {
this.permissions = permissions;
},
},
persist: {
storage: window.sessionStorage,
},
});

10
src/request/api.js Normal file
View File

@ -0,0 +1,10 @@
import { post } from "./axios";
export const Login = (params) => post("/sys/login", params); // 登录
export const logout = (params) => post("/sys/logout", params); // 退出登录
export const getAsyncRouter = (params) => post("/sys/menu/nav", params); // 获取动态路由
export const getUserInfo = (params) => post("/sys/user/info", params); // 获取用户信息
export const setChangePassword = (params) => post("/sys/user/password", params); // 修改密码
export const setRefreshToken = (params) =>
post("/sys/refreshToken", { loading: false, ...params }); // 刷新token
export const setDeleteImg = (params) => post("/busImgfiles/delete", params); // 删除附件

134
src/request/axios.js Normal file
View File

@ -0,0 +1,134 @@
import axios from "axios";
import { ElLoading, ElMessage } from "element-plus";
import router from "../router";
import pinia from "../pinia";
import { useUserStore } from "@/pinia/user.js";
import refreshToken from "@/assets/js/refreshToken.js";
let loading = null;
function startLoading() {
loading = ElLoading.service({
lock: true,
text: "加载中...",
background: "rgba(0, 0, 0, 0.5)",
});
}
function endLoading() {
loading && loading.close();
}
// axios.defaults.baseURL = import.meta.env[
// import.meta.env.DEV ? "VITE_PROXY" : "VITE_BASE_URL"
// ];
axios.defaults.baseURL = import.meta.env.VITE_PROXY;
axios.defaults.timeout = 1000 * 60 * 10;
// axios.defaults.withCredentials = true;
axios.interceptors.request.use(
async (config) => {
const userStore = useUserStore(pinia);
config.headers.Token = userStore.getToken;
if (config.method === "get" || config.method === "GET") {
if (config.params.loading !== false) startLoading();
}
if (config.method === "post" || config.method === "POST") {
if (config.data.loading !== false) startLoading();
}
return config;
},
(error) => Promise.reject(error)
);
axios.interceptors.response.use(
(config) => {
endLoading();
if (config.data.code === 401) {
ElMessage.error("登录失效,请重新登录");
router.push("/login").then();
} else {
refreshToken();
}
return config;
},
(error) => {
if (error && error.response) {
error.message = `连接错误${error.response.status}`;
import.meta.env.DEV &&
ElMessage.error(`连接错误${error.response.status}`);
} else {
error.message = "连接到服务器失败";
ElMessage.error("连接到服务器失败");
}
return Promise.reject(error.message);
}
);
export function post(url, params = {}) {
const userStore = useUserStore(pinia);
return new Promise((resolve, reject) => {
axios
.post(url, {
corpinfoId: userStore.getUserInfo.corpinfoId,
userId: userStore.getUserInfo.userId,
...params,
})
.then((res) => {
if (res.data.result === "success") {
resolve(res.data);
} else {
ElMessage.error(res.data.msg || "系统开小差了");
reject(res.data);
}
})
.catch((err) => {
reject(err);
});
});
}
export function get(url, params = {}) {
const userStore = useUserStore(pinia);
return new Promise((resolve, reject) => {
axios
.get(url, {
params: {
corpinfoId: userStore.getUserInfo.corpinfoId,
userId: userStore.getUserInfo.userId,
...params,
},
})
.then((res) => {
if (res.data.result === "success") {
resolve(res.data);
} else {
ElMessage.error(res.data.msg || "系统开小差了");
reject(res.data);
}
})
.catch((err) => {
reject(err);
});
});
}
export function upload(url, params = {}) {
return new Promise((resolve, reject) => {
axios
.post(url, params, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((res) => {
if (res.data.result === "success") {
resolve(res.data);
} else {
ElMessage.error(res.data.msg || "系统开小差了");
reject(res.data);
}
})
.catch((err) => {
reject(err);
});
});
}

View File

@ -0,0 +1,14 @@
import { post } from "./axios";
// 获取部门
export const getDepartmentTree = (parentId = "0") =>
post("/oa/department/getTree", {
loading: false,
parentId,
});
// 获取数据字典
export const getDataDictionaries = (params) =>
post("/dictionaries/getLevels", {
loading: false,
...params,
});

View File

@ -0,0 +1,16 @@
import { post } from "./axios";
export const getScheduleJobList = (params) =>
post("/sys/schedule/list", params); // 定时任务列表
export const getScheduleJobInfo = (params) =>
post("/sys/schedule/info", params); // 定时任务查看
export const setScheduleJobAdd = (params) => post("/sys/schedule/save", params); // 定时任务新增
export const setScheduleJobUpdate = (params) =>
post("/sys/schedule/update", params); // 定时任务修改
export const setScheduleJobDelete = (params) =>
post("/sys/schedule/delete", params); // 定时任务删除
export const setScheduleJobPause = (params) =>
post("/sys/schedule/pause", params); // 定时任务暂停
export const setScheduleJobResume = (params) =>
post("/sys/schedule/resume", params); // 定时任务恢复
export const setScheduleJobRun = (params) => post("/sys/schedule/run", params); // 定时任务执行

View File

@ -0,0 +1,26 @@
import { post } from "@/request/axios";
export const getRoleList = (params) => post("/sys/role/listPage", params); // 角色管理列表
export const getRoleListAll = (params) => post("/sys/role/listAll", params); // 角色管理列表所有
export const setRoleDelete = (params) => post("/sys/role/delete", params); // 角色管理删除
export const setRoleAdd = (params) => post("/sys/role/save", params); // 角色管理添加
export const setRoleEdit = (params) => post("/sys/role/update", params); // 角色管理修改
export const getRoleView = (params) => post("/sys/role/info", params); // 角色管理查看
export const getDataDictionaryList = (params) =>
post("/sys/dictionaries/listPage", params); // 数据字典列表
export const setDataDictionaryDelete = (params) =>
post("/sys/dictionaries/delete", params); // 数据字典删除
export const setDataDictionaryAdd = (params) =>
post("/sys/dictionaries/save", params); // 数据字典添加
export const setDataDictionaryEdit = (params) =>
post("/sys/dictionaries/update", params); // 数据字典修改
export const getDataDictionaryInfo = (params) =>
post("/sys/dictionaries/info", params); // 数据字典查看
export const getDataDictionaryRepeat = (params) =>
post("/sys/dictionaries/findByBianma", params); // 数据字典验证编码是否重复
export const getRouteList = (params) => post("/sys/menu/list", params); // 菜单管理列表
export const getRouteView = (params) => post("/sys/menu/info", params); // 菜单管理查看
export const setRouteAdd = (params) => post("/sys/menu/save", params); // 菜单管理添加
export const setRouteEdit = (params) => post("/sys/menu/update", params); // 菜单管理修改
export const setRouteDelete = (params) => post("/sys/menu/delete", params); // 菜单管理删除
export const setRouteIcon = (params) => post("/sys/menu/icon", params); // 菜单管理修改图标

View File

@ -0,0 +1,28 @@
import { post, upload } from "@/request/axios";
export const getDepartmentList = (params) =>
post("/oa/department/listPage", params); // 部门管理列表
export const setDepartmentDelete = (params) =>
post("/oa/department/delete", params); // 部门管理删除
export const setDepartmentAdd = (params) => post("/oa/department/save", params); // 部门管理添加
export const setDepartmentEdit = (params) =>
post("/oa/department/update", params); // 部门管理修改
export const getDepartmentView = (params) =>
post("/oa/department/info", params); // 部门管理查看
export const getJobList = (params) => post("/oa/post/listPage", params); // 岗位管理列表
export const getJobListAll = (params) => post("/oa/post/listAll", params); // 岗位管理列表所有
export const setJobDelete = (params) => post("/oa/post/delete", params); // 岗位管理删除
export const getJobView = (params) => post("/oa/post/info", params); // 岗位管理查看
export const setJobAdd = (params) => post("/oa/post/save", params); // 岗位管理添加
export const setJobEdit = (params) => post("/oa/post/update", params); // 岗位管理修改
export const getUserList = (params) => post("/sys/user/list", params); // 用户管理列表
export const getUserListAll = (params) => post("sys/user/listAll", params); // 用户管理列表所有
export const setUserDelete = (params) => post("/sys/user/delete", params); // 用户管理删除
export const setUserResetPassword = (params) =>
post("/sys/user/resetPassword", params); // 用户管理重置密码
export const getUserView = (params) => post("/sys/user/getInfo", params); // 用户管理查看
export const getUserUserNameRepeat = (params) =>
post("/sys/user/hasUser", params); // 用户管理用户名去重
export const setUserAdd = (params) => post("/sys/user/save", params); // 用户管理添加
export const setUserEdit = (params) => post("/sys/user/update", params); // 用户管理修改
export const setUserExercisesImport = (params) =>
upload("/sys/user/readExcel", params); // 人员导入

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

@ -0,0 +1,69 @@
import { createRouter, createWebHashHistory } from "vue-router";
import layout from "@/layout/index.vue";
// import children from "../components/children/index.vue";
/**
* path 从跟开始每一级都要写
* name 和path一样
* redirect 重定向到哪一个路由子级路由第一个的path没有子级路由不需要填写
* component 路由对应的组件位置必填views下的文件views和.vue不需要填写
* meta参数说明
* title 显示在菜单和面包屑中名称
* model 归类到头部导航哪一级中最外层路由需要填写
* activeMenu 当前路由选中状态是哪个导航isSubMenu:false时需要填写设置isSubMenu:false路由的path
* isMenu false 不显示当前菜单
* isLogin false 不需要登录可以访问
* breadcrumb false 当前页不显示在面包屑中
* isBreadcrumb false 当前页不显示面包屑
* isSubMenu false 当前菜单不显示子菜单
* isBack false 当前菜页不显示返回按钮
**/
const routes = [
{
path: "/login",
name: "/login",
meta: { title: "登录", isLogin: false },
component: () => import("@/views/login/index"),
},
{
path: "/",
name: "app",
redirect: "/index",
meta: { title: "首页" },
component: layout,
children: [
{
path: "/index",
name: "/index",
meta: {
title: "首页",
breadcrumb: false,
isMenu: false,
isSubMenu: false,
isBack: false,
},
component: () => import("@/views/index/index"),
},
],
},
{
path: "/404",
name: "/404",
meta: { title: "404", isBreadcrumb: false, isMenu: false },
component: () => import("@/views/404"),
},
];
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { left: 0, top: 0 };
}
},
});
export default router;

565
src/views/404.vue Normal file
View File

@ -0,0 +1,565 @@
<template>
<div class="containerer">
<div class="container container-star">
<div v-for="item in 30" :key="item" class="star-1"></div>
<div v-for="item in 30" :key="item" class="star-2"></div>
</div>
<div class="container container-bird">
<div class="bird bird-anim">
<div class="bird-container">
<div class="wing wing-left">
<div class="wing-left-top"></div>
</div>
<div class="wing wing-right">
<div class="wing-right-top"></div>
</div>
</div>
</div>
<div class="bird bird-anim">
<div class="bird-container">
<div class="wing wing-left">
<div class="wing-left-top"></div>
</div>
<div class="wing wing-right">
<div class="wing-right-top"></div>
</div>
</div>
</div>
<div class="bird bird-anim">
<div class="bird-container">
<div class="wing wing-left">
<div class="wing-left-top"></div>
</div>
<div class="wing wing-right">
<div class="wing-right-top"></div>
</div>
</div>
</div>
<div class="bird bird-anim">
<div class="bird-container">
<div class="wing wing-left">
<div class="wing-left-top"></div>
</div>
<div class="wing wing-right">
<div class="wing-right-top"></div>
</div>
</div>
</div>
<div class="bird bird-anim">
<div class="bird-container">
<div class="wing wing-left">
<div class="wing-left-top"></div>
</div>
<div class="wing wing-right">
<div class="wing-right-top"></div>
</div>
</div>
</div>
<div class="bird bird-anim">
<div class="bird-container">
<div class="wing wing-left">
<div class="wing-left-top"></div>
</div>
<div class="wing wing-right">
<div class="wing-right-top"></div>
</div>
</div>
</div>
<div class="container-title">
<div class="title">
<div class="number">4</div>
<div class="moon">
<div class="face">
<div class="mouth"></div>
<div class="eyes">
<div class="eye-left"></div>
<div class="eye-right"></div>
</div>
</div>
</div>
<div class="number">4</div>
</div>
<div class="subtitle">抱歉您访问的页面不存在</div>
<button class="backbtn" @click="router.push({ name: '/index' })">
返回首页
</button>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
</script>
<style lang="scss" scoped>
@import url("https://fonts.googleapis.com/css?family=Lato|Russo+One");
*,
*:after,
*:before {
box-sizing: border-box;
}
body {
padding: 0;
margin: 0;
}
.container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100vh;
overflow: hidden;
}
//stars
.container-star {
background-image: linear-gradient(
to bottom,
#04112b 0%,
#474c88 70%,
#a871d6 100%
);
&:after {
background: radial-gradient(
ellipse at center,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0) 40%,
rgba(15, 10, 38, 0.2) 100%
);
content: "";
width: 100%;
height: 100%;
position: absolute;
top: 0;
}
}
.star-1 {
position: absolute;
border-radius: 50%;
background-color: #ffffff;
animation: twinkle 5s infinite ease-in-out;
&:after {
height: 100%;
width: 100%;
transform: rotate(90deg);
content: "";
position: absolute;
background-color: #fff;
border-radius: 50%;
}
&:before {
background: radial-gradient(
ellipse at center,
rgba(255, 255, 255, 0.5) 0%,
rgba(255, 255, 255, 0) 60%,
rgba(255, 255, 255, 0) 100%
);
position: absolute;
border-radius: 50%;
content: "";
top: -20%;
left: -50%;
}
}
@for $i from 1 through (30) {
$top: random(100) + vh;
$left: random(100) + vw;
$size: random(6) + 3px;
.star-1:nth-of-type(#{$i}) {
top: $top;
left: $left;
width: $size;
height: calc($size / 3);
animation-delay: random(5) + s;
&:before {
width: $size * 2;
height: $size * 2;
top: -250%;
}
}
}
.star-2 {
position: absolute;
border-radius: 50%;
background-color: #ffffff;
animation: twinkle 5s infinite ease-in-out;
}
@for $i from 31 through (60) {
$top: random(100) + vh;
$left: random(100) + vw;
$size: random(3) + 1px;
.star-2:nth-of-type(#{$i}) {
top: $top;
left: $left;
width: $size;
height: $size;
animation-delay: random(5) + s;
&:before {
width: $size * 2;
height: $size * 2;
top: -250%;
}
}
}
//text and moon
.container-title {
width: 600px;
height: 450px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
position: absolute;
color: white;
line-height: 1;
font-weight: 700;
text-align: center;
justify-content: center;
align-items: center;
flex-direction: column;
display: flex;
}
.title > * {
display: inline-block;
font-size: 200px;
}
.number {
text-shadow: 20px 20px 20px rgba(0, 0, 0, 0.2);
padding: 0 0.2em;
}
.subtitle {
font-size: 24px;
margin-top: 3em;
text-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);
font-weight: normal;
}
button {
font-size: 14px;
margin-top: 2em;
padding: 0.5em 1em;
letter-spacing: 1px;
color: white;
background-color: transparent;
border: 0;
cursor: pointer;
z-index: 999;
border: 1px solid white;
border-radius: 5px;
text-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);
transition: opacity 0.2s ease;
&:hover {
opacity: 0.7;
}
&:focus {
outline: 0;
}
}
.moon {
position: relative;
border-radius: 50%;
width: 160px;
height: 160px;
z-index: 2;
background-color: #fff;
box-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #fff, 0 0 40px #fff,
0 0 70px #fff, 0 0 80px #fff, 0 0 100px #ff1177;
animation: rotate 5s ease-in-out infinite;
.face {
top: 60%;
left: 47%;
position: absolute;
.mouth {
border-top-left-radius: 50%;
border-bottom-right-radius: 50%;
border-top-right-radius: 50%;
background-color: #5c3191;
width: 25px;
height: 25px;
position: absolute;
animation: snore 5s ease-in-out infinite;
transform: rotate(45deg);
box-shadow: inset -4px -4px 4px rgba(0, 0, 0, 0.3);
}
.eyes {
position: absolute;
top: -30px;
left: -30px;
.eye-left,
.eye-right {
border: 4px solid #5c3191;
width: 30px;
height: 15px;
border-bottom-left-radius: 100px;
border-bottom-right-radius: 100px;
border-top: 0;
position: absolute;
&:before,
&:after {
content: "";
position: absolute;
border-radius: 50%;
width: 4px;
height: 4px;
background-color: #5c3191;
top: -2px;
left: -4px;
}
&:after {
left: auto;
right: -4px;
}
}
.eye-right {
left: 50px;
}
}
}
}
//birds
.container-bird {
perspective: 2000px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.bird {
position: absolute;
z-index: 1000;
left: 50%;
top: 50%;
height: 40px;
width: 50px;
transform: translate3d(-100vw, 0, 0) rotateY(90deg);
transform-style: preserve-3d;
}
.bird-container {
left: 0;
top: 0;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transform: translate3d(50px, 30px, -300px);
}
.wing {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border-radius: 3px;
transform-style: preserve-3d;
transform-origin: center bottom;
z-index: 300;
}
.wing-left {
background: linear-gradient(to bottom, #a58dc4 0%, #7979a8 100%);
transform: translate3d(0, 0, 0) rotateX(-30deg);
animation: wingLeft 1.3s cubic-bezier(0.45, 0, 0.5, 0.95) infinite;
}
.wing-right {
background: linear-gradient(to bottom, #d9d3e2 0%, #b8a5d1 100%);
transform: translate3d(0, 0, 0) rotateX(-30deg);
animation: wingRight 1.3s cubic-bezier(0.45, 0, 0.5, 0.95) infinite;
}
.wing-right-top,
.wing-left-top {
border-right: 25px solid transparent;
border-left: 25px solid transparent;
top: -20px;
width: 100%;
position: absolute;
transform-origin: 100% 100%;
}
.wing-right-top {
border-bottom: 20px solid #b8a5d1;
transform: translate3d(0, 0, 0) rotateX(60deg);
animation: wingRightTop 1.3s cubic-bezier(0.45, 0, 0.5, 0.95) infinite;
}
.wing-left-top {
border-bottom: 20px solid #7979a8;
transform: translate3d(0, 0, 0) rotateX(-60deg);
animation: wingLeftTop 1.3s cubic-bezier(0.45, 0, 0.5, 0.95) infinite;
}
.bird-anim:nth-child(1) {
animation: bird1 30s linear infinite forwards;
}
.bird-anim:nth-child(2) {
animation: bird2 30s linear infinite forwards;
animation-delay: 3s;
z-index: -1;
}
.bird-anim:nth-child(3) {
animation: bird3 30s linear infinite forwards;
animation-delay: 5s;
}
.bird-anim:nth-child(4) {
animation: bird4 30s linear infinite forwards;
animation-delay: 7s;
}
.bird-anim:nth-child(5) {
animation: bird5 30s linear infinite forwards;
animation-delay: 14s;
}
.bird-anim:nth-child(6) {
animation: bird6 30s linear infinite forwards;
animation-delay: 10s;
z-index: -1;
}
//keyframes
@keyframes rotate {
0%,
100% {
transform: rotate(-8deg);
}
50% {
transform: rotate(0deg);
}
}
@keyframes snore {
0%,
100% {
transform: scale(1) rotate(30deg);
}
50% {
transform: scale(0.5) rotate(30deg);
border-bottom-left-radius: 50%;
}
}
@keyframes twinkle {
0%,
100% {
opacity: 0.7;
}
50% {
opacity: 0.3;
}
}
@keyframes wingLeft {
0%,
100% {
transform: translate3d(0, 0, 0) rotateX(-50deg);
}
50% {
transform: translate3d(0, -20px, 0) rotateX(-130deg);
background: linear-gradient(to bottom, #d9d3e2 0%, #b8a5d1 100%);
}
}
@keyframes wingLeftTop {
0%,
100% {
transform: translate3d(0, 0, 0) rotateX(-10deg);
}
50% {
transform: translate3d(0px, 0px, 0) rotateX(-40deg);
border-bottom: 20px solid #b8a5d1;
}
}
@keyframes wingRight {
0%,
100% {
transform: translate3d(0, 0, 0) rotateX(50deg);
}
50% {
transform: translate3d(0, -20px, 0) rotateX(130deg);
background: linear-gradient(to bottom, #a58dc4 0%, #7979a8 100%);
}
}
@keyframes wingRightTop {
0%,
100% {
transform: translate3d(0, 0, 0) rotateX(10deg);
}
50% {
transform: translate3d(0px, 0px, 0px) rotateX(40deg);
border-bottom: 20px solid #7979a8;
}
}
@keyframes bird1 {
0% {
transform: translate3d(-120vw, -20px, -1000px) rotateY(-40deg) rotateX(0deg);
}
100% {
transform: translate3d(100vw, -40vh, 1000px) rotateY(-40deg) rotateX(0deg);
}
}
@keyframes bird2 {
0%,
15% {
transform: translate3d(100vw, -300px, -1000px) rotateY(10deg) rotateX(0deg);
}
100% {
transform: translate3d(-100vw, -20px, -1000px) rotateY(10deg) rotateX(0deg);
}
}
@keyframes bird3 {
0% {
transform: translate3d(100vw, -50vh, 100px) rotateY(-5deg) rotateX(-20deg);
}
100% {
transform: translate3d(-100vw, -10vh, 100px) rotateY(-5deg) rotateX(-20deg);
}
}
@keyframes bird4 {
0% {
transform: translate3d(100vw, 30vh, 200px) rotateY(-5deg) rotateX(10deg);
}
100% {
transform: translate3d(-100vw, -30vh, 200px) rotateY(-5deg) rotateX(10deg);
}
}
@keyframes bird5 {
0%,
5% {
transform: translate3d(100vw, 30vh, 400px) rotateY(-15deg) rotateX(-10deg);
}
100% {
transform: translate3d(-100vw, 10vh, 400px) rotateY(-15deg) rotateX(-10deg);
}
}
@keyframes bird6 {
0%,
10% {
transform: translate3d(-100vw, 20vh, -500px) rotateY(15deg) rotateX(10deg);
}
100% {
transform: translate3d(100vw, 40vh, -800px) rotateY(5deg) rotateX(10deg);
}
}
@media screen and (max-width: 580px) {
.container-404 {
width: 100%;
}
.number {
font-size: 100px;
}
.subtitle {
font-size: 20px;
padding: 0 1em;
}
.moon {
width: 100px;
height: 100px;
}
.face {
transform: scale(0.7);
}
}
</style>

View File

@ -0,0 +1,7 @@
<template>
<div>1</div>
</template>
<script setup></script>
<style lang="scss" scoped></style>

186
src/views/login/index.vue Normal file
View File

@ -0,0 +1,186 @@
<template>
<div class="login">
<div class="main">
<div class="logo">
<!-- <img-->
<!-- src="/src/assets/images/public/logo.png"-->
<!-- alt=""-->
<!-- width="200"-->
<!-- height="33"-->
<!-- />-->
</div>
<div class="content">
<div class="introduce_content"></div>
<div class="form">
<div class="title">账号登录</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
@submit.prevent="fnLogin"
>
<el-form-item prop="username">
<el-input
v-model="form.username"
placeholder="请输入用户名"
tabindex="1"
>
<template #prepend>
<icon-people theme="filled" size="16" fill="#909399" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="请输入密码"
tabindex="2"
>
<template #prepend>
<icon-lock
theme="filled"
size="16"
fill="#909399"
:stroke-width="3"
/>
</template>
</el-input>
</el-form-item>
<el-form-item>
<verification v-model:verification-pass="verificationPass" />
</el-form-item>
<el-form-item class="button">
<el-button native-type="submit">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import Verification from "@/components/verification/index";
import { useUserStore } from "@/pinia/user";
import { getUserInfo, Login } from "@/request/api";
import { debounce } from "throttle-debounce";
import useFormValidate from "@/assets/js/useFormValidate.js";
import dayjs from "dayjs";
import { encrypt } from "@/assets/js/aes_secret.js";
const router = useRouter();
const { formRef, validate } = useFormValidate();
const verificationPass = ref(false);
const userStore = useUserStore();
const form = ref({
username: "",
password: "",
});
const rules = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
};
const fnLogin = debounce(
1000,
() => {
if (import.meta.env.DEV) {
fnSubmitLogin();
return;
}
fnSubmitLogin();
},
{ atBegin: true }
);
const fnSubmitLogin = async () => {
await validate("请输入用户名密码");
const { token } = await Login({
username: encrypt(form.value.username),
password: encrypt(form.value.password),
});
await userStore.setToken(token);
await userStore.setTokenTime(dayjs().format("YYYY-MM-DD HH:mm:ss"));
const { user } = await getUserInfo();
userStore.setUserInfo({
...userStore.getUserInfo,
...user,
});
await router.replace("/index");
};
</script>
<style scoped lang="scss">
.login {
width: 100%;
max-width: 1920px;
margin: 0 auto;
height: 100vh;
position: relative;
background: url("/src/assets/images/public/bg.png") no-repeat top center;
background-size: 100% 100%;
.main {
width: 1200px;
padding-top: 70px;
margin: 0 auto;
.content {
margin-top: 170px;
display: flex;
justify-content: space-between;
.introduce_content {
width: 600px;
}
.form {
width: 400px;
height: 380px;
box-sizing: border-box;
border-radius: 5px;
background: #ffffff;
padding-bottom: 50px;
margin-right: 20px;
box-shadow: 0 0 20px rgb(109 109 109 / 40%);
margin-top: 20px;
.title {
text-align: center;
color: #222;
font-size: 24px;
border-bottom: 1px solid #eee;
line-height: 60px;
}
.el-form-item {
width: 320px;
margin: 20px auto;
.el-input {
height: 40px;
}
}
.button {
.el-button {
background: #0a7dfe;
height: 45px;
width: 100%;
color: #ffffff;
margin-top: 10px;
}
}
}
}
}
}
:deep {
.el-input-group__prepend {
padding-top: 5px !important;
}
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<el-dialog
v-model="visible"
:title="type === 'edit' ? '修改' : '新增'"
:before-close="fnClose"
>
<el-form ref="formRef" :rules="rules" :model="form" label-width="110px">
<el-form-item label="bean名称" prop="beanName">
<el-input v-model="form.beanName" placeholder="请输入bean名称" />
</el-form-item>
<el-form-item label="参数" prop="params">
<el-input v-model="form.params" placeholder="参数" />
</el-form-item>
<el-form-item label="cron表达式" prop="cronExpression">
<el-input
v-model="form.cronExpression"
placeholder="如: 0 0 12 * * ?"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="fnClose"></el-button>
<el-button type="primary" @click="fnSubmit"></el-button>
</template>
</el-dialog>
</template>
<script setup>
import { debounce } from "throttle-debounce";
import useFormValidate from "@/assets/js/useFormValidate.js";
import { ElMessage } from "element-plus";
import {
setScheduleJobAdd,
setScheduleJobUpdate,
} from "@/request/schedule_job.js";
const props = defineProps({
type: {
type: String,
required: true,
},
});
const emits = defineEmits(["getData"]);
const visible = defineModel("visible", { type: Boolean, required: true });
const form = defineModel("form", { type: Object, required: true });
const { formRef, validate } = useFormValidate();
const rules = {
beanName: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
cronExpression: [
{ required: true, message: "cron表达式不能为空", trigger: "blur" },
],
};
const fnClose = () => {
formRef.value.resetFields();
visible.value = false;
};
const fnSubmit = debounce(
1000,
async () => {
await validate();
if (props.type === "add") {
await setScheduleJobAdd({
...form.value,
});
}
if (props.type === "edit")
await setScheduleJobUpdate({
...form.value,
});
ElMessage.success("操作成功");
fnClose();
emits("getData");
},
{ atBegin: true }
);
</script>

View File

@ -0,0 +1,129 @@
<template>
<div>
<app-table v-model:pagination="pagination" :data="list" @get-data="getData">
<el-table-column prop="beanName" label="名称" />
<el-table-column prop="params" label="参数" />
<el-table-column prop="cronExpression" label="cron表达式" />
<el-table-column prop="remark" label="备注" />
<el-table-column label="状态">
<template #default="{ row }">
<el-tag v-if="row.status === 0" size="small"></el-tag>
<el-tag v-else size="small" type="danger">暂停</el-tag>
</template>
</el-table-column>
<el-table-column width="200" label="操作">
<template #default="{ row }">
<el-button
type="primary"
text
link
@click="fnAddOrEdit(row.jobId, 'edit')"
>
修改
</el-button>
<el-button type="primary" text link @click="fnDelete(row.jobId)">
删除
</el-button>
<el-button
v-if="row.status === 0"
type="primary"
text
link
@click="fnPause(row.jobId)"
>
暂停
</el-button>
<el-button
v-if="row.status === 1"
type="primary"
text
link
@click="fnResume(row.jobId)"
>
恢复
</el-button>
<el-button type="primary" text link @click="fnRun(row.jobId)">
立即执行
</el-button>
</template>
</el-table-column>
<template #button>
<el-button type="primary" @click="fnAddOrEdit('', 'add')">
新增
</el-button>
</template>
</app-table>
<add
v-model:form="addOrEditDialog.form"
v-model:visible="addOrEditDialog.visible"
:type="addOrEditDialog.type"
@get-data="resetPagination"
/>
</div>
</template>
<script setup>
import { nextTick, ref } from "vue";
import {
getScheduleJobList,
getScheduleJobInfo,
setScheduleJobDelete,
setScheduleJobRun,
setScheduleJobPause,
setScheduleJobResume,
} from "@/request/schedule_job.js";
import { ElMessage, ElMessageBox } from "element-plus";
import AppTable from "@/components/table/index.vue";
import Add from "./components/add.vue";
import useListData from "@/assets/js/useListData.js";
const { list, pagination, getData, resetPagination } =
useListData(getScheduleJobList);
const addOrEditDialog = ref({
visible: false,
type: "",
form: {
jobId: "",
beanName: "",
params: "",
cronExpression: "",
status: "",
remark: "",
},
});
const fnAddOrEdit = async (jobId, type) => {
addOrEditDialog.value.visible = true;
addOrEditDialog.value.type = type;
await nextTick();
if (type === "edit") {
const resData = await getScheduleJobInfo({ jobId });
addOrEditDialog.value.form = resData.schedule;
}
};
const fnDelete = async (jobId) => {
await ElMessageBox.confirm(`确定要删除吗?`, { type: "warning" });
await setScheduleJobDelete({ jobId });
ElMessage.success("删除成功");
await resetPagination();
};
const fnPause = async (jobId) => {
await ElMessageBox.confirm(`确定要暂停吗?`, { type: "warning" });
await setScheduleJobPause({ jobId });
ElMessage.success("暂停成功");
await resetPagination();
};
const fnResume = async (jobId) => {
await ElMessageBox.confirm(`确定要恢复吗?`, { type: "warning" });
await setScheduleJobResume({ jobId });
ElMessage.success("恢复成功");
await resetPagination();
};
const fnRun = async (jobId) => {
await ElMessageBox.confirm(`确定要立即执行吗?`, { type: "warning" });
await setScheduleJobRun({ jobId });
ElMessage.success("立即执行成功");
await resetPagination();
};
</script>

View File

@ -0,0 +1,120 @@
<template>
<el-dialog
v-model="visible"
:title="type === 'edit' ? '修改' : '新增'"
:before-close="fnClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="上级菜单">
<el-tag>{{ parentName }}</el-tag>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="编码" prop="bianma">
<el-input
v-model="form.bianma"
:disabled="type === 'edit'"
placeholder="请输入编码"
/>
</el-form-item>
<el-form-item label="排序" prop="orderBy">
<el-input v-model.number="form.orderBy" placeholder="请输入排序" />
</el-form-item>
<el-form-item label="备注" prop="descr">
<el-input
v-model="form.descr"
:autosize="{ minRows: 1 }"
type="textarea"
placeholder="请输入备注"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="fnClose"> </el-button>
<el-button type="primary" @click="fnSubmit"> </el-button>
</template>
</el-dialog>
</template>
<script setup>
import {
getDataDictionaryRepeat,
setDataDictionaryAdd,
setDataDictionaryEdit,
} from "@/request/system_management.js";
import { debounce } from "throttle-debounce";
import useFormValidate from "@/assets/js/useFormValidate.js";
import { ElMessage } from "element-plus";
const props = defineProps({
type: {
type: String,
required: true,
},
parentName: {
type: String,
required: true,
},
parentId: {
type: String,
required: true,
},
});
const emits = defineEmits(["getData"]);
const visible = defineModel("visible", { type: Boolean, required: true });
const form = defineModel("form", { type: Object, required: true });
const { formRef, validate } = useFormValidate();
const rules = {
name: [
{ required: true, message: "字典名称不能为空", trigger: "change" },
{ min: 2, max: 30, message: "长度在2到30个字符", trigger: "blur" },
],
bianma: [
{ required: true, message: "字典编码名称不能为空", trigger: "change" },
{ min: 2, max: 30, message: "长度在2到30个字符", trigger: "blur" },
],
orderBy: [
{ required: true, message: "排序不能为空", trigger: ["change", "blur"] },
{
type: "number",
message: "排序必须为数字",
trigger: ["change", "blur"],
},
],
};
const fnClose = () => {
formRef.value.resetFields();
visible.value = false;
};
const fnSubmit = debounce(
1000,
async () => {
await validate();
if (props.type === "add") {
const { dictionaries } = await getDataDictionaryRepeat({
bianma: form.value.bianma,
});
if (dictionaries) {
ElMessage.error("添加失败,编码重复");
return;
}
await setDataDictionaryAdd({
...form.value,
parentId: props.parentId,
dictionariesId: undefined,
});
}
if (props.type === "edit")
await setDataDictionaryEdit({
...form.value,
});
ElMessage.success("操作成功");
fnClose();
emits("getData");
},
{ atBegin: true }
);
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,136 @@
<template>
<div>
<app-table
v-model:pagination="pagination"
:data="list"
@get-data="fnGetData"
>
<el-table-column label="名称">
<template #default="{ row }">
<el-button
type="primary"
text
link
@click="
router.push({
path: '/system_management/data_dictionary',
query: {
parentName: row.name,
parentId: row.dictionariesId,
},
})
"
>
{{ row.name }} <el-icon><arrow-right /></el-icon>
</el-button>
</template>
</el-table-column>
<el-table-column prop="bianma" label="编码" />
<el-table-column prop="dictionariesId" label="ID" width="300" />
<el-table-column prop="orderBy" label="排序" width="50" />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button
type="primary"
text
link
@click="fnAddOrEdit(row.dictionariesId, 'edit')"
>
编辑
</el-button>
<el-button
type="primary"
text
link
@click="fnDelete(row.dictionariesId)"
>
删除
</el-button>
</template>
</el-table-column>
<template #button>
<el-button type="primary" @click="fnAddOrEdit('', 'add')">
新增
</el-button>
<el-button
v-if="parentId !== '0'"
:icon="ArrowLeft"
@click="router.back()"
>
返回
</el-button>
</template>
</app-table>
<add
v-model:form="addOrEditDialog.form"
v-model:visible="addOrEditDialog.visible"
:parent-name="parentName"
:parent-id="parentId"
:type="addOrEditDialog.type"
@get-data="resetPagination"
/>
</div>
</template>
<script setup>
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import { nextTick, ref } from "vue";
import { useRouter, onBeforeRouteUpdate, useRoute } from "vue-router";
import AppTable from "@/components/table/index.vue";
import {
setDataDictionaryDelete,
getDataDictionaryList,
getDataDictionaryInfo,
} from "@/request/system_management.js";
import { ElMessage, ElMessageBox } from "element-plus";
import useListData from "@/assets/js/useListData.js";
import Add from "./components/add.vue";
const router = useRouter();
const route = useRoute();
const parentIdDefault = "0";
const parentNameDefault = "(无)此项为顶级菜单";
const parentId = ref(route.query.parentId || parentIdDefault);
const parentName = ref(route.query.parentName || parentNameDefault);
const { list, pagination, resetPagination, getData } = useListData(
getDataDictionaryList,
{
params: { parentId: parentId.value },
}
);
const addOrEditDialog = ref({
visible: false,
type: "",
form: {
name: "",
bianma: "",
orderBy: "",
descr: "",
},
});
const fnGetData = () => {
getData({ parentId: parentId.value });
};
onBeforeRouteUpdate((to) => {
parentId.value = to.query.parentId || parentIdDefault;
parentName.value = to.query.parentName || parentNameDefault;
fnGetData();
});
const fnDelete = async (dictionariesId) => {
await ElMessageBox.confirm(`确定要删除吗?`, { type: "warning" });
await setDataDictionaryDelete({ dictionariesId });
ElMessage.success("删除成功");
await resetPagination();
};
const fnAddOrEdit = async (dictionariesId, type) => {
addOrEditDialog.value.visible = true;
addOrEditDialog.value.type = type;
await nextTick();
if (type === "edit") {
const resData = await getDataDictionaryInfo({ dictionariesId });
addOrEditDialog.value.form = resData.dictionaries;
}
};
</script>
<style scoped></style>

View File

@ -0,0 +1,76 @@
<template>
<el-dialog
v-model="visible"
:title="title"
width="60%"
:before-close="fnClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="24">
<el-form-item label="当前路由" prop="parentMenuId">
{{ form.currentName || "首页" }}
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="按钮名称" prop="name">
<el-input v-model="form.name" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="标识" prop="perms">
<el-input v-model="form.perms" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button type="primary" @click="fnSubmit"></el-button>
<el-button @click="fnClose"></el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref } from "vue";
import { debounce } from "throttle-debounce";
import { setRouteAdd, setRouteEdit } from "@/request/system_management.js";
import { ElMessage } from "element-plus";
const props = defineProps({
title: {
type: String,
required: true,
},
});
const emits = defineEmits(["getData"]);
const visible = defineModel("visible", { type: Boolean, required: true });
const form = defineModel("form", { type: Object, required: true });
const formRef = ref(null);
const rules = {
name: [{ required: true, message: "请输入按钮名称", trigger: "blur" }],
perms: [{ required: true, message: "请输入标识", trigger: "blur" }],
};
const fnClose = () => {
formRef.value.resetFields();
visible.value = false;
};
const fnSubmit = debounce(
1000,
async () => {
const params = {
...form.value,
type: 2,
};
props.title === "编辑"
? await setRouteEdit({ ...params })
: await setRouteAdd(params);
ElMessage.success("操作成功");
fnClose();
emits("getData");
},
{ atBegin: true }
);
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,173 @@
<template>
<el-dialog
v-model="visible"
title="设置图标"
width="40%"
:before-close="fnClose"
>
<div>
<a href="https://iconpark.oceanengine.com/official" target="_blank">
点击此处
</a>
查看所有图标
</div>
<div class="mt-20">
<el-form label-width="80px" @submit.prevent="fnInit">
<el-row>
<el-col :span="12">
<el-form-item label="图标名称">
<el-input v-model="keywords" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label-width="10px">
<el-button type="primary" native-type="submit">搜索</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<div class="icon_container">
<template v-for="(item, index) in svgList" :key="index">
<el-tooltip :content="item.title">
<div
class="item"
:class="{ active: iconIndex === index }"
@click="iconIndex = index"
>
<component
:is="'icon-' + item.name"
theme="filled"
fill="#a5b2c2"
size="38"
:stroke-width="3"
/>
</div>
</el-tooltip>
</template>
</div>
<div class="mt-20 flex-end">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
size="small"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@update:current-page="fnSearch"
@update:page-size="fnSearch"
/>
</div>
<template #footer>
<el-button type="primary" @click="fnSubmit"></el-button>
<el-button @click="fnClose"></el-button>
</template>
</el-dialog>
</template>
<script setup>
import { debounce } from "throttle-debounce";
import { setRouteIcon } from "@/request/system_management.js";
import { ElMessage } from "element-plus";
import { paging } from "@/assets/js/utils.js";
import { ref } from "vue";
import icons from "@icon-park/vue-next/icons.json";
const props = defineProps({
menuId: {
type: Number,
required: true,
},
meta: {
type: Object,
required: true,
},
});
const visible = defineModel("visible", { type: Boolean, required: true });
const keywords = ref("");
const svgList = ref([]);
const pagination = ref({
currentPage: 1,
pageSize: 50,
total: icons.length,
});
const iconIndex = ref("");
const fnSetPagination = () => {
pagination.value = {
currentPage: 1,
pageSize: 50,
total: icons.length,
};
};
const fnSearch = () => {
const filterIcons = icons.filter(
(item) =>
item.name.indexOf(keywords.value) !== -1 ||
item.title.indexOf(keywords.value) !== -1
);
svgList.value = paging(
filterIcons,
pagination.value.currentPage,
pagination.value.pageSize
);
pagination.value.total = filterIcons.length;
};
const fnInit = () => {
fnSetPagination();
fnSearch();
};
fnInit();
const fnSubmit = debounce(
1000,
async () => {
await setRouteIcon({
menuId: props.menuId,
meta: JSON.stringify({
...props.meta,
icon: iconIndex.value !== "" ? svgList.value[iconIndex.value].name : "",
}),
});
ElMessage.success("设置成功");
fnClose();
},
{ atBegin: true }
);
const fnClose = () => {
iconIndex.value = "";
keywords.value = "";
fnInit();
visible.value = false;
};
</script>
<style lang="scss" scoped>
.icon_container {
display: flex;
flex-wrap: wrap;
.item {
flex-basis: calc(5% - 10px);
border: 1px solid var(--el-border-color);
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
margin-right: 13px;
padding: 10px;
cursor: pointer;
&:nth-child(10n) {
margin-right: 0;
}
&:hover,
&.active {
border: 1px solid #79bbff;
}
}
}
.flex-end {
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<el-dialog
v-model="visible"
:title="title"
width="60%"
:before-close="fnClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form-item label="上级路由" prop="parentMenuId">
{{ form.parentName }}
</el-form-item>
<el-form-item label="路由名称" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="路由地址(从/开始)" prop="path">
<el-input v-model="form.path" />
</el-form-item>
<el-form-item
label="路由重定向地址(重定向到哪一个路由,子级路由第一个的路由地址,(没有子级路由不需要填写))"
prop="redirect"
>
<el-input v-model="form.redirect" />
</el-form-item>
<el-form-item
label="文件位置(路由对应的组件位置必填views下的文件views和.vue不需要填写如果是children只需要填写children)"
prop="component"
>
<el-input v-model="form.component" />
</el-form-item>
<el-form-item
label="model(归类到头部导航哪一级中(一级路由需要填写))"
prop="model"
>
<el-select v-model="form.model" clearable>
<el-option
v-for="item in MENU"
:key="item.model"
:label="item.title"
:value="item.model"
/>
</el-select>
</el-form-item>
<el-form-item label="权限标识" prop="perms">
<el-input v-model="form.perms" />
</el-form-item>
<el-form-item label="序号" prop="orderNum">
<el-input v-model.number="form.orderNum" />
</el-form-item>
<el-form-item
label="选中的菜单(当前路由选中状态是哪个导航(只有当父级路由或祖先级路由’当前菜单是否显示子菜单为否‘时需要填写,设置‘当前菜单是否显示子菜单为否’的路由地址))"
prop="activeMenu"
>
<el-input v-model="form.activeMenu" />
</el-form-item>
<el-form-item label="是否显示当前菜单" prop="isMenu">
<el-select v-model="form.isMenu">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="是否需要登录才可以访问" prop="isLogin">
<el-select v-model="form.isLogin">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="当前页是否显示在面包屑中" prop="breadcrumb">
<el-select v-model="form.breadcrumb">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="当前页是否显示面包屑" prop="isBreadcrumb">
<el-select v-model="form.isBreadcrumb">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
</el-select>
</el-form-item>
<el-form-item
label="当前菜单是否显示子菜单(菜单有增删改查等等子路由的情况下需要勾选为否)"
prop="isSubMenu"
>
<el-select v-model="form.isSubMenu">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="当前页是否显示全局返回按钮" prop="isBack">
<el-select v-model="form.isBack">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="fnSubmit"></el-button>
<el-button @click="fnClose"></el-button>
</template>
</el-dialog>
</template>
<script setup>
import { MENU } from "@/assets/js/constant.js";
import { debounce } from "throttle-debounce";
import useFormValidate from "@/assets/js/useFormValidate.js";
import { setRouteAdd, setRouteEdit } from "@/request/system_management.js";
import { ElMessage } from "element-plus";
const props = defineProps({
title: {
type: String,
required: true,
},
});
const emits = defineEmits(["getData"]);
const visible = defineModel("visible", { type: Boolean, required: true });
const form = defineModel("form", { type: Object, required: true });
const { formRef, validate } = useFormValidate();
const rules = {
component: [{ required: true, message: "请输入文件位置", trigger: "blur" }],
orderNum: [
{ required: true, message: "请输入序号", trigger: "blur" },
{ type: "number", message: "序号必须是数字", trigger: "blur" },
],
};
const fnClose = () => {
formRef.value.resetFields();
visible.value = false;
};
const fnSubmit = debounce(
1000,
async () => {
await validate();
const meta = {
title: form.value.title,
model: form.value.model,
activeMenu: form.value.activeMenu,
isMenu: form.value.isMenu,
isLogin: form.value.isLogin,
breadcrumb: form.value.breadcrumb,
isBreadcrumb: form.value.isBreadcrumb,
isSubMenu: form.value.isSubMenu,
isBack: form.value.isBack,
icon: form.value.icon || "",
};
const params = {
path: form.value.path,
name: form.value.title,
parentId: form.value.parentId || 0,
orderNum: form.value.orderNum,
component: form.value.component,
perms: form.value.perms,
redirect: form.value.redirect,
meta: JSON.stringify(meta),
type: 1,
menuId: form.value.menuId,
};
props.title === "编辑"
? await setRouteEdit({ ...params })
: await setRouteAdd(params);
ElMessage.success("操作成功");
fnClose();
emits("getData");
},
{ atBegin: true }
);
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,281 @@
<template>
<div>
<app-table
:data="menusAll"
:show-pagination="false"
row-key="menuId"
:show-index="false"
:tree-props="{ hasChildren: 'hasChildren', children: 'list' }"
>
<el-table-column label="名称" show-overflow-tooltip width="300">
<template #default="{ row }">
{{ row?.meta?.title || row.name }}
</template>
</el-table-column>
<el-table-column label="路由地址" prop="path" show-overflow-tooltip />
<el-table-column label="文件位置" show-overflow-tooltip>
<template #default="{ row }">
{{
row.type === 1
? row.component === "children"
? "/src/components/children/index.vue"
: "/src/views/" + row.component + ".vue"
: ""
}}
</template>
</el-table-column>
<el-table-column label="排序" prop="orderNum" width="50" />
<el-table-column
label="权限标识"
prop="perms"
width="100"
show-overflow-tooltip
/>
<el-table-column label="类型" width="65">
<template #default="{ row }">
<el-tag v-if="row.type === 1"></el-tag>
<el-tag v-if="row.type === 2" type="danger"></el-tag>
</template>
</el-table-column>
<el-table-column label="是否显示在菜单" width="120">
<template #default="{ row }">
{{
row.type === 1
? row.meta?.isMenu !== false && !row.meta?.activeMenu && row.path
? "是"
: "否"
: ""
}}
</template>
</el-table-column>
<el-table-column label="操作" width="330">
<template #default="{ row }">
<el-button
v-if="
row.type === 1 &&
row.meta?.isMenu !== false &&
!row.meta?.activeMenu &&
row.path
"
text
link
type="primary"
@click="fnAddIcon(row)"
>
图标
</el-button>
<el-button
v-if="row.type === 1"
text
link
type="primary"
@click="fnAddRouter(row, '编辑')"
>
编辑路由
</el-button>
<el-button
v-if="row.type === 2"
text
link
type="primary"
@click="fnAddButton(row, '编辑')"
>
编辑按钮
</el-button>
<el-button text link type="primary" @click="fnDelRouter(row)">
删除{{ row.type === 1 ? "路由" : "按钮" }}
</el-button>
<el-button
v-if="row.type === 1 && row.component === 'children'"
text
link
type="primary"
@click="fnAddRouter(row, '新增下级')"
>
新增下级路由
</el-button>
<el-button
v-if="row.type === 1 && row.component !== 'children'"
text
link
type="primary"
@click="fnAddButton(row, '新增')"
>
新增按钮
</el-button>
</template>
</el-table-column>
<template #button>
<el-button type="primary" @click="fnAddRouter({}, '新增一级菜单')">
新增一级路由
</el-button>
</template>
</app-table>
<add-menu
v-model:visible="menuDialog.visible"
v-model:form="menuDialog.form"
:title="menuDialog.title"
@get-data="getData"
/>
<add-button
v-model:visible="buttonDialog.visible"
v-model:form="buttonDialog.form"
:title="buttonDialog.title"
@get-data="getData"
/>
<icon
v-model:visible="iconDialog.visible"
:menu-id="iconDialog.menuId"
:meta="iconDialog.meta"
/>
</div>
</template>
<script setup>
import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
getRouteList,
setRouteDelete,
getRouteView,
} from "@/request/system_management.js";
import { debounce } from "throttle-debounce";
import AppTable from "@/components/table/index.vue";
import Icon from "./components/icon.vue";
import AddMenu from "./components/menu.vue";
import AddButton from "./components/button.vue";
import conversionRouterMeta from "@/assets/js/conversion_router_meta.js";
import useListData from "@/assets/js/useListData.js";
const menusAll = ref([]);
const { getData } = useListData(getRouteList, {
params: { parentId: 0 },
usePagination: false,
key: "menuList",
callback: (list) => {
menusAll.value = conversionRouterMeta(list);
},
});
const menuDialog = ref({
visible: false,
title: "",
form: {
parentId: "",
parentName: "",
title: "",
path: "",
redirect: "",
component: "",
model: "",
perms: "",
orderNum: "",
activeMenu: "",
menuId: "",
icon: "",
isMenu: true,
isLogin: true,
breadcrumb: true,
isBreadcrumb: true,
isSubMenu: true,
isBack: true,
},
});
const iconDialog = ref({
visible: false,
menuId: 0,
meta: {},
});
const buttonDialog = ref({
visible: false,
title: "",
form: {
currentName: "",
parentId: "",
name: "",
perms: "",
},
});
const fnAddRouter = async (row, type) => {
menuDialog.value.visible = true;
menuDialog.value.title = type;
if (type === "编辑") {
const resData = await getRouteView({ menuId: row.menuId });
resData.menu.meta = JSON.parse(resData.menu.meta);
menuDialog.value.form = {
menuId: row.menuId,
parentId: row.parentId,
orderNum: row.orderNum,
perms: row.perms,
path: row.path,
redirect: row.redirect,
component: row.component,
parentName: row.parentName || "(无)",
title: row.meta.title,
model: row.meta.model,
activeMenu: row.meta.activeMenu,
isMenu: row.meta.isMenu,
isLogin: row.meta.isLogin,
breadcrumb: row.meta.breadcrumb,
isBreadcrumb: row.meta.isBreadcrumb,
isSubMenu: row.meta.isSubMenu,
isBack: row.meta.isBack,
icon: row.meta.icon,
};
} else if (type === "新增下级") {
menuDialog.value.form.menuId = undefined;
menuDialog.value.form.parentId = row.menuId;
menuDialog.value.form.parentName = row.meta.title;
if (row.meta.isSubMenu === false)
menuDialog.value.form.activeMenu = row.path;
else menuDialog.value.form.activeMenu = row.meta.activeMenu || "";
menuDialog.value.form.orderNum = row.list?.length + 1 || 1;
} else if (type === "新增一级菜单") {
menuDialog.value.form.menuId = undefined;
menuDialog.value.form.parentId = 0;
menuDialog.value.form.parentName = "(无)";
menuDialog.value.form.orderNum = menusAll.value.length + 1;
}
};
const fnDelRouter = debounce(
1000,
async (row) => {
await ElMessageBox.confirm(
`确认删除名称为【${row?.meta?.title || row.name}】的这条数据吗?`,
"提示",
{ type: "warning" }
);
await setRouteDelete({
menuId: row.menuId,
});
ElMessage.success("删除成功");
await getData();
},
{ atBegin: true }
);
const fnAddIcon = (row) => {
iconDialog.value.visible = true;
iconDialog.value.menuId = row.menuId;
iconDialog.value.meta = row.meta;
};
const fnAddButton = async (row, type) => {
buttonDialog.value.visible = true;
buttonDialog.value.title = type;
if (type === "新增") {
buttonDialog.value.form.currentName = row.name;
buttonDialog.value.form.parentId = row.menuId;
buttonDialog.value.form.menuId = undefined;
} else if (type === "编辑") {
const resData = await getRouteView({ menuId: row.menuId });
buttonDialog.value.form = resData.menu;
buttonDialog.value.form.currentName = resData.menu.parentName;
}
};
</script>
<style scoped lang="scss">
:deep {
.el-table .el-table__cell {
text-align: left !important;
}
}
</style>

View File

@ -0,0 +1,148 @@
<template>
<el-dialog
v-model="visible"
:title="type === 'edit' ? '修改' : '新增'"
:before-close="fnClose"
>
<el-form ref="formRef" :rules="rules" :model="form" label-width="110px">
<el-form-item label="名称" prop="roleName">
<el-input v-model="form.roleName" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
:autosize="{ minRows: 1 }"
placeholder="请输入备注"
/>
</el-form-item>
<el-form-item label="菜单" prop="menuIdList">
<el-tree
ref="treeRef"
node-key="menuId"
:data="menusAll"
:props="{
label: 'name',
children: 'list',
}"
show-checkbox
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="fnClose"></el-button>
<el-button type="primary" @click="fnSubmit"></el-button>
</template>
</el-dialog>
</template>
<script setup>
import { debounce } from "throttle-debounce";
import useFormValidate from "@/assets/js/useFormValidate.js";
import { ElMessage } from "element-plus";
import { nextTick, ref, watchEffect } from "vue";
import {
getRouteList,
setRoleAdd,
setRoleEdit,
} from "@/request/system_management.js";
const props = defineProps({
type: {
type: String,
required: true,
},
});
const emits = defineEmits(["getData"]);
const visible = defineModel("visible", { type: Boolean, required: true });
const form = defineModel("form", { type: Object, required: true });
const { formRef, validate } = useFormValidate();
const treeRef = ref(null);
const menusAll = ref([]);
const rules = {
roleName: [
{ required: true, message: "名称不能为空", trigger: "blur" },
{ min: 2, max: 30, message: "长度在2到30个字符", trigger: "blur" },
],
};
const fnGetData = async () => {
const resData = await getRouteList({ parentId: 0 });
menusAll.value = fnConversion(resData.menuList);
};
const fnConversion = (menuList) => {
for (let i = 0; i < menuList.length; i++) {
menuList[i].meta = JSON.parse(menuList[i].meta);
if (menuList[i].type === 1) {
menuList[i].name = (menuList[i].name || "首页") + "(菜单)";
}
if (menuList[i].type === 2) {
menuList[i].name = menuList[i].name + "(按钮)";
}
if (menuList[i].list.length > 0) {
fnConversion(menuList[i].list);
}
}
return menuList;
};
const fnClose = () => {
formRef.value.resetFields();
visible.value = false;
treeRef.value.setCheckedKeys([]);
};
watchEffect(async () => {
if (visible.value) {
await fnGetData();
await nextTick();
let checkedKeys = [];
form.value.menuIdList.forEach((item) => {
checkedKeys = fnGetChecked(item, menusAll.value, checkedKeys);
});
treeRef.value.setCheckedKeys(checkedKeys);
}
});
const fnGetChecked = (menuId, menusAll, checkedKeys) => {
for (let i = 0; i < menusAll.length; i++) {
if (menuId === menusAll[i].menuId) {
if (menusAll[i].list.length === 0) {
checkedKeys.push(menusAll[i].menuId);
break;
}
} else {
if (menusAll[i].list.length > 0) {
fnGetChecked(menuId, menusAll[i].list, checkedKeys);
}
}
}
return checkedKeys;
};
const fnSubmit = debounce(
1000,
async () => {
await validate();
const checkedKeys = treeRef.value.getCheckedKeys() || [];
const halfCheckedKeys = treeRef.value.getHalfCheckedKeys() || [];
if (props.type === "add") {
await setRoleAdd({
...form.value,
menuIdList: [...checkedKeys, ...halfCheckedKeys],
roleId: undefined,
});
}
if (props.type === "edit")
await setRoleEdit({
...form.value,
menuIdList: [...checkedKeys, ...halfCheckedKeys],
});
ElMessage.success("操作成功");
fnClose();
emits("getData");
},
{ atBegin: true }
);
</script>
<style lang="scss" scoped>
:deep(.el-tree) {
width: 100%;
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<div>
<app-table
ref="tableRef"
v-model:pagination="pagination"
:data="list"
row-key="roleId"
show-selection
@get-data="getData"
>
<el-table-column prop="roleName" label="名称" />
<el-table-column prop="remark" label="备注" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button
type="primary"
text
link
@click="fnAddOrEdit(row.roleId, 'edit')"
>
编辑
</el-button>
<el-button type="primary" text link @click="fnDelete([row.roleId])">
删除
</el-button>
</template>
</el-table-column>
<template #button>
<el-button type="primary" @click="fnAddOrEdit('', 'add')">
新增
</el-button>
<el-button type="danger" @click="fnDeleteMultiple"></el-button>
</template>
</app-table>
<add
v-model:visible="addOrEditDialog.visible"
v-model:form="addOrEditDialog.form"
:type="addOrEditDialog.type"
@get-data="resetPagination"
/>
</div>
</template>
<script setup>
import { nextTick, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { debounce } from "throttle-debounce";
import AppTable from "@/components/table/index.vue";
import Add from "./components/add.vue";
import {
getRoleList,
getRoleView,
setRoleDelete,
} from "@/request/system_management.js";
import useListData from "@/assets/js/useListData.js";
const { list, pagination, getData, resetPagination, tableRef } =
useListData(getRoleList);
const addOrEditDialog = ref({
visible: false,
type: "",
form: {
roleName: "",
remark: "",
menuIdList: [],
},
});
const fnDelete = debounce(
1000,
async (roleIds) => {
await ElMessageBox.confirm(`确定要删除吗?`, {
type: "warning",
});
await setRoleDelete({ roleIds });
ElMessage.success("删除成功");
resetPagination();
},
{ atBegin: true }
);
const fnDeleteMultiple = debounce(
1000,
async () => {
const selectionData = tableRef.value.getSelectionRows();
if (selectionData.length === 0) {
ElMessage.warning("请选择要删除的数据");
return;
}
await ElMessageBox.confirm(`确定要删除吗?`, {
type: "warning",
});
const roleIds = selectionData.map((item) => item.roleId);
await setRoleDelete({ roleIds });
ElMessage.success("删除成功");
await resetPagination();
},
{ atBegin: true }
);
const fnAddOrEdit = async (roleId, type) => {
addOrEditDialog.value.visible = true;
addOrEditDialog.value.type = type;
await nextTick();
if (type === "edit") {
const resData = await getRoleView({ roleId });
addOrEditDialog.value.form = resData.role;
}
};
</script>
<style scoped></style>

View File

@ -0,0 +1,105 @@
<template>
<el-dialog
v-model="visible"
:title="type === 'edit' ? '修改' : '新增'"
:before-close="fnClose"
>
<el-form ref="formRef" :rules="rules" :model="form" label-width="130px">
<el-form-item label="上级部门">
<el-tag>{{ parentName }}</el-tag>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="部门级别" prop="level">
<el-select v-model="form.level" placeholder="请选择部门级别">
<el-option label="公司级" value="1" />
<el-option label="部门级" value="2" />
<el-option label="小组级" value="3" />
</el-select>
</el-form-item>
<el-form-item label="部门负责人" prop="headman">
<el-input v-model="form.headman" placeholder="请输入部门负责人" />
</el-form-item>
<el-form-item label="部门负责人手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入部门负责人手机号" />
</el-form-item>
<el-form-item label="排序" prop="orderBy">
<el-input v-model.number="form.orderBy" placeholder="请输入排序" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="fnClose"></el-button>
<el-button type="primary" @click="fnSubmit"></el-button>
</template>
</el-dialog>
</template>
<script setup>
import { debounce } from "throttle-debounce";
import useFormValidate from "@/assets/js/useFormValidate.js";
import { ElMessage } from "element-plus";
import {
setDepartmentAdd,
setDepartmentEdit,
} from "@/request/user_management.js";
import { PHONE } from "@/assets/js/regular.js";
const props = defineProps({
type: {
type: String,
required: true,
},
parentName: {
type: String,
required: true,
},
parentId: {
type: String,
required: true,
},
});
const emits = defineEmits(["getData"]);
const visible = defineModel("visible", { type: Boolean, required: true });
const form = defineModel("form", { type: Object, required: true });
const { formRef, validate } = useFormValidate();
const rules = {
name: [
{ required: true, message: "名称不能为空", trigger: "blur" },
{ min: 2, max: 30, message: "长度在2到30个字符", trigger: "blur" },
],
level: [{ required: true, message: "部门级别不能为空", trigger: "change" }],
phone: [{ pattern: PHONE, message: "手机号格式不正确", trigger: "blur" }],
orderBy: [
{ required: true, message: "排序不能为空", trigger: ["change", "blur"] },
{ type: "number", message: "排序必须为数字", trigger: ["change", "blur"] },
],
};
const fnClose = () => {
formRef.value.resetFields();
visible.value = false;
};
const fnSubmit = debounce(
1000,
async () => {
await validate();
if (props.type === "add") {
await setDepartmentAdd({
...form.value,
parentId: props.parentId,
departmentId: undefined,
});
}
if (props.type === "edit")
await setDepartmentEdit({
...form.value,
});
ElMessage.success("操作成功");
fnClose();
emits("getData");
},
{ atBegin: true }
);
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,170 @@
<template>
<div>
<app-search
v-model="searchForm"
label-width="80px"
@submit="resetPagination"
>
<el-col :span="6">
<el-form-item label="名称" prop="name">
<el-input v-model="searchForm.name" placeholder="请输入名称" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="部门级别" prop="level">
<el-select v-model="searchForm.level" placeholder="请选择部门级别">
<el-option label="公司级" value="1" />
<el-option label="部门级" value="2" />
<el-option label="小组级" value="3" />
</el-select>
</el-form-item>
</el-col>
</app-search>
<app-table
v-model:pagination="pagination"
:data="list"
@get-data="fnGetData"
>
<el-table-column label="名称">
<template #default="{ row }">
<el-button
type="primary"
text
link
@click="
router.push({
path: '/user_management/department',
query: {
parentName: row.name,
parentId: row.departmentId,
},
})
"
>
{{ row.name }} <el-icon><arrow-right /></el-icon>
</el-button>
</template>
</el-table-column>
<el-table-column label="部门级别">
<template #default="{ row }">
<span v-if="row.level === '1'"></span>
<span v-if="row.level === '2'"></span>
<span v-if="row.level === '3'"></span>
</template>
</el-table-column>
<el-table-column prop="headman" label="主管领导" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button
type="primary"
text
link
@click="fnAddOrEdit(row.departmentId, 'edit')"
>
编辑
</el-button>
<el-button
type="primary"
text
link
@click="fnDelete(row.departmentId)"
>
删除
</el-button>
</template>
</el-table-column>
<template #button>
<el-button type="primary" @click="fnAddOrEdit('', 'add')">
新增
</el-button>
<el-button
v-if="parentId !== '0'"
:icon="ArrowLeft"
@click="router.back()"
>
返回
</el-button>
</template>
</app-table>
<add
v-model:form="addOrEditDialog.form"
v-model:visible="addOrEditDialog.visible"
:parent-name="parentName"
:parent-id="parentId"
:type="addOrEditDialog.type"
@get-data="resetPagination"
/>
</div>
</template>
<script setup>
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import { nextTick, ref } from "vue";
import {
getDepartmentList,
setDepartmentDelete,
getDepartmentView,
} from "@/request/user_management.js";
import { ElMessage, ElMessageBox } from "element-plus";
import { debounce } from "throttle-debounce";
import AppTable from "@/components/table/index.vue";
import AppSearch from "@/components/search/index.vue";
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
import Add from "./components/add.vue";
import useListData from "@/assets/js/useListData.js";
const router = useRouter();
const route = useRoute();
const parentIdDefault = "0";
const parentNameDefault = "(无)此项为顶级菜单";
const parentId = ref(route.query.parentId || parentIdDefault);
const parentName = ref(route.query.parentName || parentNameDefault);
const { list, pagination, searchForm, getData, resetPagination } = useListData(
getDepartmentList,
{
params: { parentId: parentId.value },
}
);
const addOrEditDialog = ref({
visible: false,
type: "",
form: {
name: "",
level: "",
headman: "",
phone: "",
orderBy: "",
},
});
const fnGetData = () => {
getData({ parentId: parentId.value });
};
onBeforeRouteUpdate((to) => {
parentId.value = to.query.parentId || parentIdDefault;
parentName.value = to.query.parentName || parentNameDefault;
fnGetData();
});
const fnDelete = debounce(
1000,
async (departmentId) => {
await ElMessageBox.confirm(`确定要删除吗?`, {
type: "warning",
});
await setDepartmentDelete({ departmentId });
ElMessage.success("删除成功");
await resetPagination();
},
{ atBegin: true }
);
const fnAddOrEdit = async (departmentId, type) => {
addOrEditDialog.value.visible = true;
addOrEditDialog.value.type = type;
await nextTick();
if (type === "edit") {
const resData = await getDepartmentView({ departmentId });
addOrEditDialog.value.form = resData.department;
}
};
</script>
<style scoped></style>

View File

@ -0,0 +1,152 @@
<template>
<el-dialog
v-model="visible"
:title="type === 'edit' ? '修改' : '新增'"
:before-close="fnClose"
>
<el-form ref="formRef" :rules="rules" :model="form" label-width="110px">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="form.mobile" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="身份证号" prop="userIdCard">
<el-input v-model="form.userIdCard" placeholder="请输入身份证号" />
</el-form-item>
<el-form-item label="学历" prop="degree">
<el-select v-model="form.degree">
<el-option
v-for="item in degreeList"
:key="item.dictionariesId"
:label="item.name"
:value="item.dictionariesId"
/>
</el-select>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="部门" prop="departmentId">
<app-department v-model="form.departmentId" />
</el-form-item>
<el-form-item label="人员类型" prop="type">
<el-select v-model="form.type" multiple>
<el-option
v-for="item in typeList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="角色" prop="roleIdList">
<el-select v-model="form.roleIdList" multiple>
<el-option
v-for="item in roleList"
:key="item.roleId"
:label="item.roleName"
:value="item.roleId"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="fnClose"></el-button>
<el-button type="primary" @click="fnSubmit"></el-button>
</template>
</el-dialog>
</template>
<script setup>
import { debounce } from "throttle-debounce";
import useFormValidate from "@/assets/js/useFormValidate.js";
import { ElMessage } from "element-plus";
import AppDepartment from "@/components/department/index.vue";
import {
getUserUserNameRepeat,
setUserAdd,
setUserEdit,
} from "@/request/user_management.js";
import { getRoleListAll } from "@/request/system_management.js";
import { appFnGetDegree } from "@/assets/js/data_dictionary.js";
import { ID_NUMBER, PHONE } from "@/assets/js/regular.js";
const props = defineProps({
type: {
type: String,
required: true,
},
});
const emits = defineEmits(["getData"]);
const visible = defineModel("visible", { type: Boolean, required: true });
const form = defineModel("form", { type: Object, required: true });
const { formRef, validate } = useFormValidate();
const validateUserName = async (_rule, value, callback) => {
if (props.type === "add") {
const { user } = await getUserUserNameRepeat({ username: value });
if (user) callback(new Error("用户名重复"));
else callback();
} else callback();
};
const rules = {
name: [
{ required: true, message: "姓名不能为空", trigger: "blur" },
{ min: 2, max: 30, message: "长度在2到30个字符", trigger: "blur" },
],
username: [
{ required: true, message: "用户名不能为空", trigger: "blur" },
{ validator: validateUserName, trigger: "blur" },
],
userIdCard: [
{ pattern: ID_NUMBER, message: "请输入正确的身份证号", trigger: "blur" },
],
mobile: [
{ required: true, message: "手机号不能为空", trigger: "blur" },
{ pattern: PHONE, message: "手机号格式不正确", trigger: "blur" },
],
email: [{ type: "email", message: "邮箱格式不正确", trigger: "blur" }],
departmentId: [
{ required: true, message: "部门不能为空", trigger: "change" },
],
type: [{ required: true, message: "人员类型不能为空", trigger: "change" }],
roleIdList: [{ required: true, message: "角色不能为空", trigger: "change" }],
};
const { roleList } = await getRoleListAll();
const degreeList = await appFnGetDegree();
const typeList = [
{ id: "1", name: "销售" },
{ id: "2", name: "服务" },
{ id: "3", name: "专家" },
];
const fnClose = () => {
formRef.value.resetFields();
visible.value = false;
};
const fnSubmit = debounce(
1000,
async () => {
await validate();
form.value.type = form.value.type.join(",");
if (props.type === "add") {
await setUserAdd({
...form.value,
userId: undefined,
});
}
if (props.type === "edit")
await setUserEdit({
...form.value,
});
ElMessage.success("操作成功");
fnClose();
emits("getData");
},
{ atBegin: true }
);
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,51 @@
<template>
<el-dialog v-model="visible" title="查看" :before-close="fnClose">
<el-descriptions border :column="2">
<el-descriptions-item label="姓名">
{{ info.name }}
</el-descriptions-item>
<el-descriptions-item label="用户名">
{{ info.username }}
</el-descriptions-item>
<el-descriptions-item label="手机号">
{{ info.mobile }}
</el-descriptions-item>
<el-descriptions-item label="性别">
{{ info.sex }}
</el-descriptions-item>
<el-descriptions-item label="年龄">
{{ info.age }}
</el-descriptions-item>
<el-descriptions-item label="学历">
{{ info.degreeName }}
</el-descriptions-item>
<el-descriptions-item label="邮箱">
{{ info.email }}
</el-descriptions-item>
<el-descriptions-item label="部门">
{{ info.departmentName }}
</el-descriptions-item>
<el-descriptions-item label="岗位">
{{ info.postName }}
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="fnClose"></el-button>
</template>
</el-dialog>
</template>
<script setup>
defineProps({
info: {
type: Object,
required: true,
},
});
const visible = defineModel("visible", { type: Boolean, required: true });
const fnClose = () => {
visible.value = false;
};
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,183 @@
<template>
<div>
<app-search
v-model="searchForm"
label-width="60px"
@submit="resetPagination"
>
<el-col :span="6">
<el-form-item label="姓名" prop="name">
<el-input v-model="searchForm.name" placeholder="请输入姓名" />
</el-form-item>
</el-col>
</app-search>
<app-table
ref="tableRef"
v-model:pagination="pagination"
:data="list"
row-key="userId"
show-selection
@get-data="getData"
>
<el-table-column prop="name" label="姓名" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="userIdCard" label="身份证号" />
<el-table-column prop="mobile" label="手机号" />
<el-table-column prop="departmentName" label="部门" />
<el-table-column label="类型" width="190">
<template #default="{ row }">
{{ fnGetUserType(row) }}
</template>
</el-table-column>
<el-table-column label="操作" width="190">
<template #default="{ row }">
<el-button type="primary" text link @click="fnView(row.userId)">
查看
</el-button>
<el-button
type="primary"
text
link
@click="fnAddOrEdit(row.userId, 'edit')"
>
编辑
</el-button>
<el-button
type="primary"
text
link
@click="fnResetPassword(row.userId)"
>
重置密码
</el-button>
<el-button type="primary" text link @click="fnDelete(row.userId)">
删除
</el-button>
</template>
</el-table-column>
<template #button>
<el-button type="primary" @click="fnAddOrEdit('', 'add')">
新增
</el-button>
<el-button type="danger" @click="fnDeleteMultiple"></el-button>
</template>
</app-table>
<add
v-model:visible="addOrEditDialog.visible"
v-model:form="addOrEditDialog.form"
:type="addOrEditDialog.type"
@get-data="resetPagination"
/>
<view-info v-model:visible="viewDialog.visible" :info="viewDialog.info" />
</div>
</template>
<script setup>
import { nextTick, ref } from "vue";
import {
setUserDelete,
getUserView,
getUserList,
setUserResetPassword,
} from "@/request/user_management.js";
import { ElMessage, ElMessageBox } from "element-plus";
import { debounce } from "throttle-debounce";
import AppTable from "@/components/table/index.vue";
import AppSearch from "@/components/search/index.vue";
import Add from "./components/add.vue";
import ViewInfo from "./components/view.vue";
import { setRoleDelete } from "@/request/system_management.js";
import useListData from "@/assets/js/useListData.js";
const { list, pagination, searchForm, getData, resetPagination, tableRef } =
useListData(getUserList);
const addOrEditDialog = ref({
visible: false,
type: "",
form: {
name: "",
username: "",
mobile: "",
degree: "",
type: "",
email: "",
departmentId: "",
},
});
const viewDialog = ref({
visible: false,
info: {},
});
const fnGetUserType = (row) => {
const type = row.type.split(",");
const typeList = [
{ id: "1", name: "销售" },
{ id: "2", name: "服务" },
{ id: "3", name: "专家" },
];
return typeList
.filter((item) => type.includes(item.id.toString()))
.map((item) => item.name)
.join(",");
};
const fnResetPassword = debounce(
1000,
async (userId) => {
await ElMessageBox.confirm(`是否重置密码为Aqsc@2024`, {
type: "warning",
});
await setUserResetPassword({ userId });
ElMessage.success("重置密码成功");
await resetPagination();
},
{ atBegin: true }
);
const fnDelete = debounce(
1000,
async (userId) => {
await ElMessageBox.confirm(`确定要删除吗?`, {
type: "warning",
});
await setUserDelete({ userIds: [userId] });
ElMessage.success("删除成功");
await resetPagination();
},
{ atBegin: true }
);
const fnDeleteMultiple = debounce(
1000,
async () => {
const selectionData = tableRef.value.getSelectionRows();
if (selectionData.length === 0) {
ElMessage.warning("请选择要删除的数据");
return;
}
await ElMessageBox.confirm(`确定要删除吗?`, {
type: "warning",
});
const userIds = selectionData.map((item) => item.userId);
await setRoleDelete({ userIds });
ElMessage.success("删除成功");
await resetPagination();
},
{ atBegin: true }
);
const fnAddOrEdit = async (userId, type) => {
addOrEditDialog.value.visible = true;
addOrEditDialog.value.type = type;
await nextTick();
if (type === "edit") {
const resData = await getUserView({ userId });
resData.user.roleIdList = resData.user.rolesId.split(",");
resData.user.type = resData.user.type.split(",");
addOrEditDialog.value.form = resData.user;
}
};
const fnView = async (userId) => {
const resData = await getUserView({ userId });
viewDialog.value.info = resData.user;
viewDialog.value.visible = true;
};
</script>
<style scoped></style>

90
vite.config.js Normal file
View File

@ -0,0 +1,90 @@
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import eslintPlugin from "vite-plugin-eslint";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import removeConsole from "vite-plugin-remove-console";
// import EnhanceLog from "vite-plugin-enhance-log";
import { envParse } from "vite-plugin-env-parse";
export default ({ mode }) => {
return defineConfig({
base: loadEnv(mode, process.cwd()).VITE_BASE,
plugins: [
vue(),
envParse(),
eslintPlugin(),
removeConsole({
includes: [
"assert",
"clear",
"count",
"countReset",
"createTask",
"debug",
"dir",
"dirxml",
"error",
"group",
"groupCollapsed",
"groupEnd",
"info",
"log",
"profile",
"profileEnd",
"table",
"time",
"timeEnd",
"timeLog",
"timeStamp",
"trace",
"warn",
],
}),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
// EnhanceLog({
// splitBy: "🐶",
// preTip: "🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀",
// }),
],
server: {
host: true, // 本机的局域网IP不然其他人无法通过IP访问到0.0.0.0或true会自动获取本机的IP
port: 8099, // 端口号
open: true, // 是否自动打开浏览器
proxy: {
[loadEnv(mode, process.cwd()).VITE_PROXY]: {
target: loadEnv(mode, process.cwd()).VITE_BASE_URL,
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
resolve: {
alias: {
"@": "/src", // 别名,@代表src目录
},
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"], // 引入文件时哪些后缀名可以不写
},
build: {
rollupOptions: {
// 打包多个入口文件
// input: {
// admin: path.resolve(__dirname, "index.html"),
// 其它入口文件路径需要为src/views/*/index.html
// },
output: {
chunkFileNames: "static/js/[name]-[hash].js",
entryFileNames: "static/js/[name]-[hash].js",
assetFileNames: "static/[ext]/name-[hash].[ext]",
},
},
},
});
};