master
LiuJiaNan 2025-10-22 11:23:09 +08:00
commit 8ecee5ddef
43 changed files with 3033 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/YTHFile

2
.env.development Normal file
View File

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

2
.env.production Normal file
View File

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

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
/src/test/
/target/
.idea
/node_modules
/dist
*.local
package-lock.json
yarn.lock
pnpm-lock.yaml
env.d.ts
auto-imports.d.ts
components.d.ts

47
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,47 @@
{
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"json5",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
]
}

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).

56
eslint.config.js Normal file
View File

@ -0,0 +1,56 @@
import antfu from "@antfu/eslint-config";
export default antfu({
formatters: {
html: false,
css: true,
},
test: false,
typescript: false,
vue: true,
stylistic: {
semi: true,
quotes: "double",
},
overrides: {
vue: {
"vue/no-template-shadow": "error",
"vue/attribute-hyphenation": "error",
"vue/html-end-tags": "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",
},
javascript: {
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-alert": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-restricted-syntax": [
"error",
{
selector: "VariableDeclarator[id.name='pd']",
message: "不允许使用 pd请改用有语义化的变量名",
},
{
selector: "ObjectExpression > Property[key.name='pd']",
message: "不允许使用 pd请改用有语义化的变量名",
},
],
},
},
rules: {
"antfu/top-level-function": "off",
"node/prefer-global/process": "off",
"linebreak-style": ["off", "windows"],
},
});

19
index.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="stylesheet" href="https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/skins/default/aliplayer-min.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
<title>blank-vue-template</title>
</head>
<body>
<div id="app"></div>
<noscript>
<strong>很抱歉如果没有启用JavaScript网站无法正常工作请启用JavaScript使其正常工作。</strong>
</noscript>
<script type="module" src="/src/main.js"></script>
<script charset="utf-8" type="text/javascript" src="https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/aliplayer-min.js"></script>
<script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&type=webgl&ak=OElqFYoKiAH8KFtph8ftLKF5NlNrbCUr"></script>
</body>
</html>

10
jsconfig.json Normal file
View File

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

65
package.json Normal file
View File

@ -0,0 +1,65 @@
{
"name": "blank-vue-template",
"type": "module",
"version": "0.0.0",
"private": true,
"web-types": "./web-types.json",
"scripts": {
"dev": "vite --mode development",
"build": "vite build --mode production",
"preview": "vite preview",
"lint": "eslint --ext .js,.vue --fix src"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@icon-park/vue-next": "^1.4.2",
"@vueuse/core": "^13.9.0",
"@vueuse/integrations": "^13.9.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"animate.css": "^4.1.1",
"autofit.js": "^3.2.3",
"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": "^3.0.3",
"pinia-plugin-persistedstate": "^4.3.0",
"qrcode": "^1.5.4",
"qs": "^6.13.1",
"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",
"zy-vue-library": "^1.1.5"
},
"devDependencies": {
"@antfu/eslint-config": "^5.4.1",
"@types/node": "^18.19.68",
"@vant/auto-import-resolver": "^1.2.1",
"@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.36.0",
"eslint-plugin-format": "^1.0.2",
"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"
}
}

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

25
src/App.vue Normal file
View File

@ -0,0 +1,25 @@
<script setup>
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
import { useRequestLoading } from "zy-vue-library";
const loading = useRequestLoading();
</script>
<template>
<suspense>
<template #default>
<el-config-provider :locale="zhCn">
<router-view
v-loading.fullscreen.lock="loading"
element-loading-text="加载中..."
element-loading-background="rgba(0, 0, 0, 0.5)"
/>
</el-config-provider>
</template>
<template #fallback>
<div>加载中...</div>
</template>
</suspense>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1 @@
// 将常用的值储存成常量,防止重复使用写错

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

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

View File

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

View File

@ -0,0 +1,14 @@
import { getDataDictionariesList } from "@/request/data_dictionary.js";
// 无法确定parentId的数据字典
export const appFnGetDataDictionary = async (params) => {
const { dictionariesList } = await getDataDictionariesList(params);
return dictionariesList;
};
// 导航栏
export const appFnGetMenuNavList = async () => {
const { dictionariesList } = await getDataDictionariesList({
parentId: "7b2cf146798280ecf8f4dfbf8c4f59d8",
});
return dictionariesList;
};

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

@ -0,0 +1,24 @@
<script setup>
import { AppLayout } from "zy-vue-library";
import { useMenuStore } from "@/pinia/menu.js";
import { useNavStore } from "@/pinia/nav.js";
import { useUserStore } from "@/pinia/user.js";
defineOptions({
name: "Layout",
});
const menuStore = useMenuStore();
const userStore = useUserStore();
const navStore = useNavStore();
</script>
<template>
<app-layout
:menu-store="menuStore"
:user-store="userStore"
:nav-store="navStore"
:user-name="userStore.getUserInfo.username"
/>
</template>
<style scoped lang="scss"></style>

61
src/main.js Normal file
View File

@ -0,0 +1,61 @@
import { install } from "@icon-park/vue-next/es/all";
import ElDialog from "element-plus/es/components/dialog/index";
import ElSelect from "element-plus/es/components/select/index";
import VueViewer from "v-viewer";
import { createApp } from "vue";
import print from "vue3-print-nb";
import {
configureAesSecret,
configureAxios,
configureDynamicRouter,
permissionDirective,
} from "zy-vue-library";
import { useMenuStore } from "@/pinia/menu.js";
import { useNavStore } from "@/pinia/nav.js";
import { useRouterStore } from "@/pinia/router.js";
import { useUserStore } from "@/pinia/user.js";
import App from "./App";
import pinia from "./pinia";
import router from "./router";
import "dayjs/locale/zh-cn";
import "zy-vue-library/css/index.scss";
import "normalize.css";
import "animate.css";
import "viewerjs/dist/viewer.css";
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 "element-plus/es/components/notification/style/css";
ElDialog.props.closeOnClickModal.default = false;
ElDialog.props.closeOnPressEscape.default = false;
ElSelect.props.filterable = { type: Boolean, default: true };
ElSelect.props.clearable = { type: Boolean, default: true };
const app = createApp(App);
install(app, "icon");
app
.use(pinia)
.use(router)
.use(VueViewer, {
defaultOptions: {
zIndex: 9999,
},
})
.use(print)
.use(permissionDirective)
.mount("#app");
configureAesSecret({ encryptKey: "daac3ae52eff4cec" });
configureAxios({ router, store: useUserStore() });
configureDynamicRouter({
router,
stores: {
routerStore: useRouterStore(),
menuStore: useMenuStore(),
userStore: useUserStore(),
navStore: useNavStore(),
},
modules: import.meta.glob([
"./views/**/*.vue",
"!./views/**/components/*.vue",
]),
});

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,
},
});

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

@ -0,0 +1,18 @@
import { defineStore } from "pinia";
export const useNavStore = defineStore("navStore", {
state: () => ({
navList: [],
}),
getters: {
getNavList: state => state.navList,
},
actions: {
setNavList(navList) {
this.navList = navList;
},
},
persist: {
storage: window.sessionStorage,
},
});

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: "",
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,
},
});

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

@ -0,0 +1,4 @@
import { postRequest } from "zy-vue-library";
export const Login = params => postRequest("/sys/login", params); // 登录
export const getUserInfo = params => postRequest("/sys-user/info", params); // 获取用户信息

View File

@ -0,0 +1,11 @@
import { getRequest, postRequest } from "zy-vue-library";
// 获取部门树形
export const getDepartmentTree = params =>
getRequest("/sysdepartment/listTree", params);
// 获取部门树形
export const getDepartmentTreeByCorpId = params =>
getRequest("/corp-department/list-tree", params);
// 获取数据字典
export const getDataDictionariesList = params =>
postRequest("/sys/dictionaries/list", params);

View File

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

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

@ -0,0 +1,59 @@
import { createRouter, createWebHashHistory } from "vue-router";
import layout from "@/layout/index.vue";
import children from "../components/children/index.vue";
const routes = [
{
path: "/login",
name: "/login",
meta: { title: "登录", isLogin: false },
component: () => import("@/views/login/index"),
},
{
path: "/",
name: "app",
redirect: "/index",
meta: { title: "首页", isLogin: true },
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", isLogin: false },
component: () => import("@/views/404"),
},
{
path: "/mobile",
meta: { isLogin: false },
component: children,
children: [],
},
];
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;

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

@ -0,0 +1,574 @@
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
</script>
<template>
<div class="containerer">
<div class="container container-star">
<div v-for="item in 30" :key="item" class="star-1" />
<div v-for="item in 30" :key="item" class="star-2" />
</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 class="wing wing-right">
<div class="wing-right-top" />
</div>
</div>
</div>
<div class="bird bird-anim">
<div class="bird-container">
<div class="wing wing-left">
<div class="wing-left-top" />
</div>
<div class="wing wing-right">
<div class="wing-right-top" />
</div>
</div>
</div>
<div class="bird bird-anim">
<div class="bird-container">
<div class="wing wing-left">
<div class="wing-left-top" />
</div>
<div class="wing wing-right">
<div class="wing-right-top" />
</div>
</div>
</div>
<div class="bird bird-anim">
<div class="bird-container">
<div class="wing wing-left">
<div class="wing-left-top" />
</div>
<div class="wing wing-right">
<div class="wing-right-top" />
</div>
</div>
</div>
<div class="bird bird-anim">
<div class="bird-container">
<div class="wing wing-left">
<div class="wing-left-top" />
</div>
<div class="wing wing-right">
<div class="wing-right-top" />
</div>
</div>
</div>
<div class="bird bird-anim">
<div class="bird-container">
<div class="wing wing-left">
<div class="wing-left-top" />
</div>
<div class="wing wing-right">
<div class="wing-right-top" />
</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 class="eyes">
<div class="eye-left" />
<div class="eye-right" />
</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>
<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;
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 @@
<script setup></script>
<template>
<div>1</div>
</template>
<style lang="scss" scoped></style>

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

@ -0,0 +1,231 @@
<script setup>
import dayjs from "dayjs";
import { ElMessage } from "element-plus";
import { debounce } from "throttle-debounce";
import { ref } from "vue";
import { useRouter } from "vue-router";
import { aesEncrypt, AppVerification, useForm } from "zy-vue-library";
import { useUserStore } from "@/pinia/user";
import { getUserInfo, Login } from "@/request/api";
const userStore = useUserStore();
const router = useRouter();
const { formRef, validate } = useForm();
const verificationPass = ref(false);
const form = ref({
username: "",
password: "",
code: "",
});
const rules = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
code: [{ required: true, message: "请输入验证码", trigger: "blur" }],
};
const fnLogin = debounce(
1000,
() => {
if (import.meta.env.DEV) {
fnSubmitLogin();
return;
}
if (verificationPass.value) {
fnSubmitLogin();
}
else {
ElMessage.warning("请进行登录验证");
}
},
{ atBegin: true },
);
async function fnSubmitLogin() {
await validate("请输入用户名密码");
const { token } = await Login({
username: aesEncrypt(form.value.username),
password: aesEncrypt(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>
<template>
<div class="login login-container">
<div class="logo">
<img
src="https://picsum.photos/seed/picsum/500/84"
alt=""
width="500"
height="84"
>
</div>
<div class="form">
<div class="title">
template
</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>
<app-verification v-model:verification-pass="verificationPass" @update:verification-pass="fnLogin" />
</el-form-item>
<el-form-item class="button">
<el-button native-type="submit">
登录
</el-button>
</el-form-item>
</el-form>
</div>
<div class="footer">
ICP备案号 冀ICP备15003849号 技术支持河北秦安安全科技股份有限公司
</div>
</div>
</template>
<style scoped lang="scss">
.login-container {
height: 100vh;
width: 100%;
background-color: #2d3a4b;
overflow: hidden;
background-image: url(https://picsum.photos/seed/picsum/1920/1080);
background-repeat: no-repeat;
background-size: 100% 100%;
}
.login {
width: 100%;
max-width: 1920px;
margin: 0 auto;
height: 100vh;
position: relative;
background-color: #fff;
.logo {
margin-top: 20px;
margin-left: 20px;
width: 100%;
pointer-events: none;
}
.form {
border-radius: 5px;
box-shadow: 0 0 20px rgb(109 109 109 / 40%);
position: absolute;
right: 230px;
top: 50%;
transform: translateY(-50%);
width: 400px;
height: 440px;
padding: 40px 50px;
background-color: #fff;
z-index: 1;
&:after {
content: "";
position: absolute;
top: 40px;
left: -30px;
width: calc(100% + 60px);
height: calc(100% - 80px);
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.3);
z-index: -1;
}
&:before {
content: "";
position: absolute;
top: 20px;
left: -15px;
width: calc(100% + 30px);
height: calc(100% - 40px);
border-radius: 10px;
background-color: rgba(255, 255, 255, 0.5);
z-index: -2;
}
.title {
font-size: 20px;
color: #000;
line-height: 60px;
}
.el-form-item {
.el-input {
height: 40px;
}
}
.button {
.el-button {
background: #0a7dfe;
height: 45px;
width: 100%;
color: #ffffff;
margin-top: 10px;
}
}
}
.footer {
width: 100%;
height: 80px;
font-size: 14px;
color: rgba(0, 0, 0, 0.5);
text-align: center;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
z-index: 1;
line-height: 80px;
}
}
:deep {
.el-input-group__prepend {
padding-top: 5px !important;
}
.el-carousel__indicators {
display: none;
}
}
</style>

View File

@ -0,0 +1,144 @@
<script setup>
import { ElMessage } from "element-plus";
import { debounce } from "throttle-debounce";
import { useForm } from "zy-vue-library";
import {
getDataDictionaryRepeat,
setDataDictionaryAdd,
setDataDictionaryEdit,
} from "@/request/system_management.js";
const props = defineProps({
type: { type: String, required: true },
parent: {
type: Object,
required: false,
default: () => ({
dictionariesId: "0",
name: "(无)此项为顶级菜单",
bianma: "",
parentIds: "",
parentBianmas: "",
parentNames: "",
rootId: "",
}),
},
});
const emits = defineEmits(["getData"]);
const visible = defineModel("visible", { type: Boolean, required: true });
const form = defineModel("form", { type: Object, required: true });
const { formRef, validate, reset } = useForm();
const rules = {
name: [
{ required: true, message: "字典名称不能为空", trigger: "change" },
{ min: 2, max: 100, message: "长度在2到100个字符", trigger: "blur" },
],
bianma: [
{ required: true, message: "字典编码名称不能为空", trigger: "change" },
{ min: 2, max: 100, message: "长度在2到100个字符", trigger: "blur" },
],
orderBy: [
{ required: true, message: "排序不能为空", trigger: ["change", "blur"] },
{
type: "number",
message: "排序必须为数字",
trigger: ["change", "blur"],
},
],
};
const fnClose = () => {
reset();
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.parent.dictionariesId,
parentIds:
props.parent.dictionariesId === "0"
? ""
: (props.parent.parentIds ? `${props.parent.parentIds},` : "")
+ props.parent.dictionariesId,
parentBianmas:
props.parent.dictionariesId === "0"
? ""
: (props.parent.parentBianmas
? `${props.parent.parentBianmas},`
: "") + props.parent.bianma,
parentNames:
props.parent.dictionariesId === "0"
? ""
: (props.parent.parentNames ? `${props.parent.parentNames},` : "")
+ props.parent.name,
rootId: props.parent.dictionariesId === "0" ? "" : props.parent.rootId,
dictionariesId: undefined,
});
}
if (props.type === "edit") {
await setDataDictionaryEdit({
...form.value,
});
}
ElMessage.success("操作成功");
fnClose();
emits("getData");
},
{ atBegin: true },
);
</script>
<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>{{ parent.name }}</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>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,148 @@
<script setup>
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { nextTick, ref } from "vue";
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
import { AppTable, useListData } from "zy-vue-library";
import {
getDataDictionaryInfo,
getDataDictionaryList,
setDataDictionaryDelete,
} from "@/request/system_management.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: "",
parentIds: "",
parentBianmas: "",
parentNames: "",
rootId: "",
},
parent: {
dictionariesId: parentIdDefault,
name: parentNameDefault,
bianma: "",
orderBy: "",
descr: "",
parentIds: "",
parentBianmas: "",
parentNames: "",
rootId: "",
},
});
onBeforeRouteUpdate((to) => {
parentId.value = to.query.parentId || parentIdDefault;
parentName.value = to.query.parentName || parentNameDefault;
resetPagination();
});
const fnDelete = async (dictionariesId) => {
await ElMessageBox.confirm(`确定要删除吗?`, { type: "warning" });
await setDataDictionaryDelete({ dictionariesId });
ElMessage.success("删除成功");
resetPagination();
};
const fnAddOrEdit = async (dictionariesId, type) => {
addOrEditDialog.value.visible = true;
addOrEditDialog.value.type = type;
if (type === "add" && parentId.value !== "0") {
const resData = await getDataDictionaryInfo({
dictionariesId: parentId.value,
});
addOrEditDialog.value.parent = resData.dictionaries;
}
await nextTick();
if (type === "edit") {
const resData = await getDataDictionaryInfo({ dictionariesId });
addOrEditDialog.value.form = resData.dictionaries;
}
};
</script>
<template>
<div>
<app-table v-model:pagination="pagination" :data="list" @get-data="getData">
<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="danger"
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="addOrEditDialog.parent"
:type="addOrEditDialog.type"
@get-data="resetPagination"
/>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,78 @@
<script setup>
import { ElMessage } from "element-plus";
import { debounce } from "throttle-debounce";
import { useForm } from "zy-vue-library";
import { setRouteAdd, setRouteEdit } from "@/request/system_management.js";
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, reset } = useForm();
const rules = {
name: [{ required: true, message: "请输入按钮名称", trigger: "blur" }],
perms: [{ required: true, message: "请输入标识", trigger: "blur" }],
};
const fnClose = () => {
reset();
visible.value = false;
};
const fnSubmit = debounce(
1000,
async () => {
await validate();
const params = {
...form.value,
type: 2,
};
props.title === "编辑"
? await setRouteEdit({ ...params })
: await setRouteAdd(params);
ElMessage.success("操作成功");
fnClose();
emits("getData");
},
{ atBegin: true },
);
</script>
<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>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,185 @@
<script setup>
import icons from "@icon-park/vue-next/icons.json";
import { ElMessage } from "element-plus";
import { debounce } from "throttle-debounce";
import { ref } from "vue";
import { paging } from "zy-vue-library";
import { setRouteIcon } from "@/request/system_management.js";
const props = defineProps({
menuId: { type: Number, required: true },
meta: { type: Object, required: true },
});
const emits = defineEmits(["getData"]);
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.includes(keywords.value)
|| item.title.includes(keywords.value),
);
svgList.value = paging(
filterIcons,
pagination.value.currentPage,
pagination.value.pageSize,
);
pagination.value.total = filterIcons.length;
};
const fnInit = () => {
fnSetPagination();
fnSearch();
};
fnInit();
const fnClose = () => {
iconIndex.value = "";
keywords.value = "";
fnInit();
visible.value = false;
};
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();
emits("getData");
},
{ atBegin: true },
);
</script>
<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" style="display: flex; align-items: center">
当前图标
<component
:is="`icon-${meta.icon}`"
theme="filled"
fill="#a5b2c2"
size="38"
:stroke-width="3"
/>
</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>
<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,218 @@
<script setup>
import { ElMessage } from "element-plus";
import { debounce } from "throttle-debounce";
import { useForm } from "zy-vue-library";
import {
getDataRouteKeyDuplication,
setRouteAdd,
setRouteEdit,
} from "@/request/system_management.js";
const props = defineProps({
title: { type: String, required: true },
navList: { type: Array, 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, reset } = useForm();
const rules = {
component: [{ required: true, message: "请输入文件位置", trigger: "blur" }],
orderNum: [
{ required: true, message: "请输入序号", trigger: "blur" },
{ type: "number", message: "序号必须是数字", trigger: "blur" },
],
};
const fnClose = () => {
reset();
visible.value = false;
};
const fnSubmit = debounce(
1000,
async () => {
await validate();
if (form.value.routeKey) {
const { menu } = await getDataRouteKeyDuplication({
routeKey: form.value.routeKey,
menuId: form.value.menuId,
});
if (menu) {
ElMessage.error("路由名称(唯一标识)重复,请修改!");
return;
}
}
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 || "",
props: !!form.value.props,
};
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: form.value.props,
routeKey: form.value.routeKey,
};
props.title === "编辑"
? await setRouteEdit({ ...params })
: await setRouteAdd(params);
ElMessage.success("操作成功");
fnClose();
emits("getData");
},
{ atBegin: true },
);
</script>
<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="路由name唯一值有name可以使用params参数不要使用中文对应vue-router中的name"
:rules="[
{
required: form.props === 1,
message: '路由名称不能为空',
trigger: 'blur',
},
]"
prop="routeKey"
>
<el-input v-model="form.routeKey" />
</el-form-item>
<el-form-item
label="路由地址(从/开始对应vue-router中的path"
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 navList"
:key="item.dictionariesId"
:label="item.name"
:value="item.bianma"
/>
</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-item
label="是否将route.params解构为组件的props需要将页面当成弹窗里的组件使用时需要选择是对应vue-router中的props"
prop="props"
>
<el-select v-model="form.props">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</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>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,84 @@
<script setup>
import { ElMessage } from "element-plus";
import { cloneDeep } from "lodash-es";
import { debounce } from "throttle-debounce";
import { VueDraggable } from "vue-draggable-plus";
import { setRouteSort } from "@/request/system_management.js";
const emits = defineEmits(["getData"]);
const visible = defineModel("visible", { type: Boolean, required: true });
const modelValue = defineModel({ type: Array, required: true });
const fnClose = () => {
visible.value = false;
};
const fnMetaToJson = (menuList) => {
for (let i = 0; i < menuList.length; i++) {
menuList[i].meta = JSON.stringify(menuList[i].meta);
if (menuList[i].list.length > 0) {
fnMetaToJson(menuList[i].list);
}
}
return menuList;
};
const fnSubmit = debounce(
1000,
async () => {
const menuList = fnMetaToJson(cloneDeep(modelValue.value));
for (let i = 0; i < menuList.length; i++) {
menuList[i].orderNum = i + 1;
}
await setRouteSort({ menuList });
ElMessage.success("保存成功");
fnClose();
emits("getData");
},
{ atBegin: true },
);
</script>
<template>
<el-dialog v-model="visible" title="排序" width="60%" :before-close="fnClose">
<div class="items">
<vue-draggable v-model="modelValue" filter=".disabled">
<div
v-for="item in modelValue"
:key="item.menuId"
class="item" :class="[{ disabled: !item.path }]"
>
<div v-if="item.path">
{{ item?.meta?.title || item.name }}
</div>
<div v-else style="color: red">
空页面不需要排序
</div>
</div>
</vue-draggable>
</div>
<template #footer>
<el-button type="primary" @click="fnSubmit">
保存
</el-button>
<el-button @click="fnClose">
关闭
</el-button>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
.items {
.item {
margin-top: 10px;
padding: 10px;
border: 1px dashed #eee;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
cursor: move;
&.disabled {
cursor: not-allowed;
}
}
}
</style>

View File

@ -0,0 +1,353 @@
<script setup>
import { ElMessage, ElMessageBox } from "element-plus";
import { debounce } from "throttle-debounce";
import { ref } from "vue";
import {
AppTable,
conversionRouterMeta,
getLabelName,
useDataDictionary,
useListData,
} from "zy-vue-library";
import { appFnGetMenuNavList } from "@/data_dictionary/index.js";
import {
getRouteList,
getRouteView,
setRouteDelete,
} from "@/request/system_management.js";
import AddButton from "./components/button.vue";
import Icon from "./components/icon.vue";
import AddMenu from "./components/menu.vue";
import Sort from "./components/sort.vue";
const menusAll = ref([]);
const { getData } = useListData(getRouteList, {
params: { parentId: 0 },
usePagination: false,
key: "menuList",
callback: (list) => {
menusAll.value = conversionRouterMeta(list);
},
});
const [navList] = useDataDictionary(appFnGetMenuNavList);
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,
props: 0,
routeKey: "",
isShowTip: false,
},
});
const iconDialog = ref({
visible: false,
menuId: 0,
meta: {},
});
const buttonDialog = ref({
visible: false,
title: "",
form: {
currentName: "",
parentId: "",
name: "",
perms: "",
},
});
const sortDialog = ref({
visible: false,
list: [],
});
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,
isShowTip: row.meta.isShowTip,
props: row.props,
routeKey: row.routeKey,
};
}
else if (type === "新增下级") {
menuDialog.value.form.menuId = undefined;
menuDialog.value.form.parentId = row.menuId;
menuDialog.value.form.parentName = row.meta.title;
menuDialog.value.form.path = row.path;
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;
menuDialog.value.form.icon = "";
}
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;
menuDialog.value.form.icon = "";
}
};
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;
}
};
const fnSort = (list) => {
sortDialog.value.visible = true;
if (!list) {
sortDialog.value.list = menusAll.value;
}
else {
sortDialog.value.list = list;
}
};
</script>
<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.charAt(0) === "/"
? row.component.substring(1)
: row.component
}.vue`
: ""
}}
</template>
</el-table-column>
<el-table-column
label="路由name"
prop="routeKey"
show-overflow-tooltip
width="90"
/>
<el-table-column label="排序" prop="orderNum" width="50" />
<el-table-column label="所属导航栏" width="90">
<template #default="{ row }">
{{ getLabelName(row?.meta?.model, navList, "bianma") }}
</template>
</el-table-column>
<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="320">
<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="danger" @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>
<el-button
v-if="row.list.length > 0"
text
link
type="primary"
@click="fnSort(row.list)"
>
排序
</el-button>
</template>
</el-table-column>
<template #button>
<el-button type="primary" @click="fnAddRouter({}, '新增一级菜单')">
新增一级路由
</el-button>
<el-button type="primary" @click="fnSort('')">
排序
</el-button>
</template>
</app-table>
<add-menu
v-if="menuDialog.visible"
v-model:visible="menuDialog.visible"
v-model:form="menuDialog.form"
:title="menuDialog.title"
:nav-list="navList"
@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"
@get-data="getData"
/>
<sort
v-model:visible="sortDialog.visible"
v-model="sortDialog.list"
@get-data="getData"
/>
</div>
</template>
<style scoped lang="scss">
:deep {
.el-table .el-table__cell {
text-align: left !important;
}
}
</style>

View File

@ -0,0 +1,151 @@
<script setup>
import { ElMessage } from "element-plus";
import { debounce } from "throttle-debounce";
import { nextTick, ref, watchEffect } from "vue";
import { useForm } from "zy-vue-library";
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, reset } = useForm();
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 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 fnGetData = async () => {
const resData = await getRouteList({ parentId: 0 });
menusAll.value = fnConversion(resData.menuList);
};
const fnClose = () => {
reset();
visible.value = false;
treeRef.value?.setCheckedKeys([]);
};
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;
};
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 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>
<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 v-if="form.parentId !== '0'" 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>
<style lang="scss" scoped>
:deep(.el-tree) {
width: 100%;
}
</style>

View File

@ -0,0 +1,125 @@
<script setup>
import { ElMessage, ElMessageBox } from "element-plus";
import { debounce } from "throttle-debounce";
import { nextTick, ref } from "vue";
import { AppSearch, AppTable, useListData } from "zy-vue-library";
import {
getRoleList,
getRoleView,
setRoleDelete,
} from "@/request/system_management.js";
import Add from "./components/add.vue";
const { list, pagination, searchForm, getData, resetPagination, tableRef }
= useListData(getRoleList);
const options = [{ key: "roleName", label: "名称" }];
const addOrEditDialog = ref({
visible: false,
type: "",
form: {
roleName: "",
remark: "",
corpRoleType: "",
menuIdList: [],
},
});
const fnDelete = debounce(
1000,
async (roleIds) => {
await ElMessageBox.confirm(`确定要删除吗?`, {
type: "warning",
});
await setRoleDelete({ roleIds });
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 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>
<template>
<div>
<app-search
v-model="searchForm"
:options
label-width="50px"
@submit="resetPagination"
/>
<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="danger"
text
link
:disabled="!row.corpinfoId"
@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"
:corp-menu-type="addOrEditDialog.form.corpRoleType"
@get-data="resetPagination"
/>
</div>
</template>
<style scoped></style>

97
vite.config.js Normal file
View File

@ -0,0 +1,97 @@
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import Components from "unplugin-vue-components/vite";
import { defineConfig, loadEnv } from "vite";
// import EnhanceLog from "vite-plugin-enhance-log";
import { envParse } from "vite-plugin-env-parse";
import eslintPlugin from "vite-plugin-eslint";
import removeConsole from "vite-plugin-remove-console";
export default ({ mode }) => {
return defineConfig({
base: loadEnv(mode, process.cwd()).VITE_BASE,
plugins: [
vue(),
envParse(),
eslintPlugin({
cache: false,
include: ["./src/**/*.js", "./src/**/*.vue", "./src/**/*.ts"],
exclude: ["./node_modules/**", "./zy-vue-library/**"],
}),
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: "🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀",
// }),
],
optimizeDeps: {
include: ["qrcode", "@vue-office/pdf", "crypto-js"],
},
server: {
host: true, // 本机的局域网IP不然其他人无法通过IP访问到0.0.0.0或true会自动获取本机的IP
port: 18471, // 端口号
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]",
},
},
},
});
};

22
web-types.json Normal file
View File

@ -0,0 +1,22 @@
{
"$schema": "https://json.schemastore.org/web-types",
"framework": "vue",
"name": "name written in package.json",
"version": "version written in package.json",
"contributions": {
"html": {
"types-syntax": "typescript",
"attributes": [
{
"name": "v-permission"
},
{
"name": "v-viewer"
},
{
"name": "v-print"
}
]
}
}
}