This commit is contained in:
hezhidong
2025-05-16 08:55:13 +08:00
parent e62c3fce70
commit 839703dc1a
66 changed files with 5029 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="//at.alicdn.com/t/c/font_3654542_filhj6brvds.css" />
<title>后台管理系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<!-- 百度地图引入 -->
<!-- <script type="text/javascript"
src="https://api.map.baidu.com/api?v=1.0&type=webgl&ak=Ol0d1CqPErteakupIcX4actXo6GiQI7g"></script> -->
</body>
</html>
+1562
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
{
"name": "vue-manage-system",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "vite build",
"dev": "vite --host",
"preview": "vite preview"
},
"dependencies": {
"@braks/vue-flow": "^0.4.38",
"@element-plus/icons-vue": "^1.1.4",
"@kangc/v-md-editor": "^2.3.15",
"@wangeditor/editor": "^5.1.14",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^0.27.2",
"dingtalk-jsapi": "^2.15.6",
"driver.js": "^0.9.8",
"echarts": "^5.3.3",
"element-plus": "^2.1.11",
"js-base64": "^3.7.2",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"screenfull": "^6.0.1",
"video.js": "^7.20.3",
"vue": "^3.2.25",
"vue-router": "^4.0.13",
"vue-video-player": "^6.0.0",
"vue3-print-nb": "^0.1.4",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.1",
"sass": "^1.51.0",
"vite": "^2.9.7",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-mock": "^2.9.6",
"vue-cli-plugin-style-resources-loader": "~0.1.5"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+25
View File
@@ -0,0 +1,25 @@
<template>
<router-view v-if="isRouterAlive"></router-view>
</template>
<script setup>
import { onMounted, ref, nextTick, provide } from "vue";
import { useStore } from "vuex";
// 局部组件刷新
const isRouterAlive = ref(true);
const globalStore = useStore();
const reload = () => {
isRouterAlive.value = false;
nextTick(() => {
isRouterAlive.value = true;
console.log("数据刷新成功");
});
};
provide("reload", reload);
onMounted(() => {
globalStore.dispatch("user/changeThem", "#4060c7");
});
</script>
<style scoped lang="scss"></style>
+67
View File
@@ -0,0 +1,67 @@
import service from "../request.js";
// 获取组别列表
export function getGroupList() {
return service({
url: "/group/list",
method: "get",
});
}
// 添加组别
export function addGroup(data) {
return service({
url: "/group",
method: "post",
data,
});
}
// 修改组别名称
export function updateGroup(data) {
return service({
url: "/group",
method: "put",
data,
});
}
// 删除组别
export function deleteGroup(groupId) {
return service({
url: `/group/${groupId}`,
method: "delete",
});
}
// 获取未分组人员列表
export function getNonGroupUsers() {
return service({
url: "/user/non/group",
method: "get",
});
}
// 添加/修改组内人员角色
export function updateGroupUserRole(groupId, userid, role) {
return service({
url: `/group/${groupId}/user/${userid}/role/${role}`,
method: "post",
});
}
// 获取组内人员列表
export function getGroupUsers(groupId) {
return service({
url: `/user/${groupId}`,
method: "get",
});
}
// 删除组内人员
export function deleteGroupUser(groupId, userid) {
return service({
url: `/group/user/${groupId}/${userid}`,
method: "delete",
});
}
+109
View File
@@ -0,0 +1,109 @@
import axios from "axios";
import { ElMessage, ElMessageBox } from "element-plus";
import router from "../router/router";
const errorCode = {
401: "登录状态已过期,即将返回首页重新登录。",
403: "当前操作没有权限",
404: "访问资源不存在",
default: "系统未知错误,请反馈给管理员",
};
// 肖星越服务
const url = "https://paystubs.vip.cpolar.top";
// 袁永军测试服务
// const url = "https://crm-test.scdswj.cn/prod-api";
// 正式环境
// const url = "https://paystubs.vip.cpolar.top";
const service = axios.create({
baseURL: url,
timeout: 10000,
});
// Request interceptors
service.interceptors.request.use(
(config) => {
if (sessionStorage.getItem("token")) {
config.headers["Authorization"] =
"Bearer " + sessionStorage.getItem("token");
}
if (sessionStorage.getItem("deptId")) {
config.headers["selected-dept-id"] = sessionStorage.getItem("deptId");
}
let params = config.params;
let newData = {};
for (let key in params) {
if (params[key] !== null && params[key] !== "") {
newData[key] = params[key];
}
}
config.params = newData;
return config;
},
(error) => {
Promise.reject(error);
}
);
// Response interceptors
service.interceptors.response.use(
(res) => {
// 对响应数据做点什么
const response = res.data;
const code = res.data.code || 200;
const message = errorCode[code] || res.data.msg || errorCode["default"];
if (code === 401) {
if (res.code === 401 || res.code === 4001) {
// 清除浏览器全部临时缓存
// sessionStorage.clear();
// localStorage.clear();
router.push("/login");
// store.commit('setMenuData', {});
// resetRouter(); // 重置路由
ElMessageBox.alert("你已被登出,请重新登录", "提示", {})
.then(() => {})
.catch(() => {});
}
} else if (code === 500) {
// Message({ message: msg, type: 'error' })
ElMessage.error(message);
return Promise.reject(new Error(msg));
} else if (code === 601) {
// Message({ message: msg, type: 'warning' })
ElMessage.error(message);
return Promise.reject("error");
} else if (code != 200) {
ElMessage.error(message);
return Promise.reject("error");
} else {
return response;
}
},
(error) => {
console.log("走了错误的", error);
// 对响应错误做点什么
if (error.message.indexOf("timeout") != -1) {
ElMessage.error("网络超时");
} else if (error.message == "Network Error") {
ElMessage.error("网络连接错误");
} else if (error.request.status == "401") {
console.log("登录过期捕获到");
sessionStorage.clear();
router.push("/login");
ElMessage.error("登录信息过期");
} else {
ElMessage.error(
error.response.data.errmsg ||
error.response.data.error ||
error.response.data.errMessage
);
}
return Promise.reject(error);
}
);
export default service;
+41
View File
@@ -0,0 +1,41 @@
export const showMessage = (status) => {
let message = "";
switch (status) {
case 400:
message = "请求错误(400)";
break;
case 401:
message = "未授权,请重新登录(401)";
break;
case 403:
message = "拒绝访问(403)";
break;
case 404:
message = "请求出错(404)";
break;
case 408:
message = "请求超时(408)";
break;
case 500:
message = "服务器错误(500)";
break;
case 501:
message = "服务未实现(501)";
break;
case 502:
message = "网络错误(502)";
break;
case 503:
message = "服务不可用(503)";
break;
case 504:
message = "网络超时(504)";
break;
case 505:
message = "HTTP版本不受支持(505)";
break;
default:
message = `连接出错(${status})!`;
}
return `${message},请检查网络或联系管理员!`;
};
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

+80
View File
@@ -0,0 +1,80 @@
<template>
<div class="videoPlay">
<video
ref="m3u8_video"
class="video-js vjs-default-skin vjs-big-play-centered"
controls
>
<source :src="videoSrc" />
</video>
</div>
</template>
<script setup>
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import videojs from 'video.js'
import 'video.js/dist/video-js.css'
const props = defineProps({
videoSrc: String,
autoPlay: Boolean,
})
const m3u8_video = ref()
let player
const initPlay = async () => {
videojs.addLanguage('zh-CN', zh)
await nextTick()
const options = {
muted: true,
controls: true,
autoplay: true,
loop: true,
language: 'zh-CN',
techOrder: ['html5'],
}
player = videojs(m3u8_video.value, options, () => {
videojs.log('播放器已经准备好了!')
if (props.autoPlay && props.videoSrc) {
player.play()
}
player.on('ended', () => {
videojs.log('播放结束了!')
})
player.on('error', () => {
videojs.log('播放器解析出错!')
})
})
}
onMounted(() => {
initPlay()
})
//直接改变路径测试
watch(
() => props.videoSrc,
() => {
player.pause()
player.src(props.videoSrc)
player.load()
if (props.videoSrc) {
player.play()
}
}
)
onBeforeUnmount(() => {
player?.dispose()
})
</script>
<style lang="scss" scoped>
.videoPlay {
width: 100%;
height: 100%;
.video-js {
height: 100%;
width: 100%;
object-fit: fill;
}
}
:deep(.vjs-tech) {
object-fit: fill;
}
</style>
+55
View File
@@ -0,0 +1,55 @@
<template>
<el-dialog v-model="dialogVisible" title="添加数据">
<el-form v-model="uploadFrom">
<el-form-item label="文件上传:">
<el-upload
style="width: 80%"
drag
multiple
:limit="excelLimit"
:on-success="uploadSuccess"
accept="application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>或点击上传</em></div>
<template #tip>
<div class="el-upload__tip">请上传 .xls , .xlsx 标准格式文件</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="数据覆盖:">
<el-switch v-model="is_cover" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="dialogVisible = false">确认</el-button>
</template>
</el-dialog>
</template>
<script setup name="filesUpload">
import { ref, defineEmits } from 'vue'
const excelLimit = ref(1)
const dialogVisible = ref(false)
const parameter = ref({})
const is_cover = ref(false)
const emit = defineEmits(['acceptParams'])
// 接收父组件参数
const acceptParams = (params) => {
parameter.value = params
emit('gatewayData', parameter.value)
dialogVisible.value = true
}
const uploadExcel = (param) => {
console.log(param)
}
const uploadSuccess = (files) => {
console.log('上传成功' + JSON.stringify(files))
}
// 接受父组件得参数
defineExpose({
acceptParams,
})
</script>
+24
View File
@@ -0,0 +1,24 @@
import draggable from "./modules/draggable";
import copy from "./modules/copy";
import debounce from "./modules/debounce";
import longPress from "./modules/longPress";
import throttle from "./modules/throttle";
const directivesList = {
draggable,
copy,
debounce,
longPress,
throttle
};
const directives = {
install: function (app) {
Object.keys(directivesList).forEach((key) => {
// 注册所有自定义指令
app.directive(key, directivesList[key]);
});
},
};
export default directives;
+58
View File
@@ -0,0 +1,58 @@
/*
需求:实现一个复制指定。复制指定内容至剪贴板
1.动态创建 textarea 标签,并设置 readOnly 属性及移出可视区域
2.将要复制的值赋给 textarea 标签的 value 属性,并插入到 body
3.选中值 textarea 并复制
4.将 body 中插入的 textarea 移除
5.在第一次调用时绑定事件,在解绑时移除事件
*/
import { ElMessage } from "element-plus";
export const copy = {
mounted: function (el, { value }) {
console.log(arguments);
el.$value = value;
el.onclick = () => {
if (!el.$value) {
// 值为空的时候,给出提示。可根据项目UI仔细设计
ElMessage({
type: "error",
message: "无复制内容",
});
return;
}
// 动态创建 textarea 标签
const textarea = document.createElement("input");
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = "readonly";
textarea.style.position = "absolute";
textarea.style.left = "-9999px";
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = el.$value;
// 将 textarea 插入到 body 中
document.body.appendChild(textarea);
// 选中值并复制
textarea.select();
const result = document.execCommand("Copy");
if (result) {
ElMessage({
type: "success",
message: "复制成功",
});
}
document.body.removeChild(textarea);
};
// 绑定点击事件,就是所谓的一键 copy 啦
el.addEventListener("click", el.handler);
},
// 当传进来的值更新的时候触发
beforeUpdate(el, { value }) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unmounted(el) {
el.removeEventListener("click", el.handler);
},
};
export default copy;
+28
View File
@@ -0,0 +1,28 @@
/**
* v-debounce
* 按钮防抖指令,可自行扩展至input
* 接收参数:function类型
*/
const debounce = {
mounted(el, binding) {
if (typeof binding.value != "function") {
console.log("callback must be a function");
}
let timer = null;
el.__handleClick__ = function () {
// if (timer) clearInterval(timer);
//
timer && clearTimeout(timer)
timer = setTimeout(() => {
binding.value();
}, 3000);
};
el.addEventListener("click", el.__handleClick__);
},
beforeUnmount(el) {
el.removeEventListener("click", el.__handleClick__);
},
};
export default debounce;
+47
View File
@@ -0,0 +1,47 @@
/*
需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。
思路:
1、设置需要拖拽的元素为absolute,其父元素为relative。
2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
3、鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
4、鼠标松开(onmouseup)时完成一次拖拽
使用:在 Dom 上加上 v-draggable 即可
<div class="dialog-model" v-draggable></div>
*/
const draggable = {
mounted: function (el) {
console.log(el)
el.style.cursor = "move";
el.style.position = "absolute";
el.onmousedown = function (e) {
let disX = e.pageX - el.offsetLeft;
let disY = e.pageY - el.offsetTop;
document.onmousemove = function (e) {
let x = e.pageX - disX;
let y = e.pageY - disY;
console.log(el.parentNode.offsetWidth)
let maxX = el.parentNode.offsetWidth - el.offsetWidth;
let maxY = el.parentNode.offsetHeight - el.offsetHeight;
if (x < 0) {
x = 0;
} else if (x > maxX) {
x = maxX;
}
if (y < 0) {
y = 0;
} else if (y > maxY) {
y = maxY;
}
el.style.left = x + "px";
el.style.top = y + "px";
};
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null;
};
};
}
};
export default draggable;
+44
View File
@@ -0,0 +1,44 @@
const longPress = {
mounted(el, binding) {
if (typeof binding.value != "function") {
console.log("callback must be a function");
}
let pressTimer = null;
let start = (e) => {
if (e.type === "click" && e.button !== 0) {
return;
}
if (pressTimer === null) {
pressTimer = setTimeout(() => {
// 执行函数
handler();
}, 2000);
}
};
// 取消计时器
let cancel = () => {
// 检查计时器是否有值
if (pressTimer !== null) {
clearTimeout(pressTimer);
pressTimer = null;
}
};
// 运行函数
const handler = (e) => {
// 执行传递给指令的方法
binding.value(e);
console.log(e);
};
// 添加事件监听器
el.addEventListener("mousedown", start);
el.addEventListener("touchstart", start);
el.addEventListener("click", start);
// 取消计时器
el.addEventListener("click", cancel);
el.addEventListener("mouseout", cancel);
el.addEventListener("touchend", cancel);
el.addEventListener("touchcancel", cancel);
},
};
export default longPress;
+30
View File
@@ -0,0 +1,30 @@
/**
* 一段时间内控制请求的频率
* 上一次时间触发的时间和这次触发的时间相减 传入节流的时间
*
*
* 应用场景:scroll加载的时候 和input框输入的情况
*/
const throttle = {
mounted(el, binding) {
if (typeof binding.value != "function") {
console.log("callback must be a function");
}
let lastTime = 0
let newtimer = +new Date(); //拿到当前的时间
el.__handleClick__ = function () {
if (newtimer - lastTime > 3000) {
binding.value()
lastTime = newtimer
}
};
el.addEventListener("click", el.__handleClick__);
},
beforeUnmount(el) {
el.removeEventListener("click", el.__handleClick__);
},
}
export default throttle
+79
View File
@@ -0,0 +1,79 @@
<template>
<div class="tags">
<el-tag
ref="tagRef"
v-for="tag in tags"
:key="tag.title"
class="mR10 tabs"
:closable="tag.close"
:effect="tag.checked ? 'dark' : 'plain'"
@click="tagChange(tag)"
@close="delectTag(tag, index)"
>
<el-icon class="tabs-icon">
<component :is="tag.icon"></component>
</el-icon>
{{ tag.title }}
</el-tag>
</div>
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import store from '../../store/index.js'
import { useStore } from 'vuex'
const tabsMenuValue = ref('')
const nameScroll = ref(false)
const tagRef = ref()
const route = useRoute()
const router = useRouter()
const globalStore = useStore() // 该方法用于返回store 实例
// 监听路由的变化(防止浏览器后退/前进不变化 tabsMenuValue
watch(
() => route.path,
() => {
let params = {
title: route.meta.name,
url: route.path,
icon: 'Menu',
close: true,
checked: true,
}
globalStore.dispatch('tabs/addTabs', params)
},
{
immediate: true,
}
)
const tags = store.getters.tabsMenuList
const tagChange = (item) => {
item.checked = true
router.push(item.url)
}
const delectTag = (item, index) => {
globalStore.dispatch('tabs/delectTag', item)
}
onMounted(() => {
nextTick(() => {})
})
</script>
<style lang="scss" scoped>
.tags {
width: 95%;
overflow: hidden;
height: 100%;
display: flex;
flex-flow: row;
overflow: scroll;
white-space: nowrap;
&::-webkit-scrollbar {
display: none;
}
.tabs {
height: 30px;
}
}
</style>
View File
@@ -0,0 +1,64 @@
<template>
<el-icon class="icon-style" id="collapseIcon" @click="changeShrinkage">
<component :is="!isCollapse ? 'Fold' : 'Expand'"></component>
</el-icon>
<el-breadcrumb separator="/" id="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="index">
<span class="no-redirect" v-if="index === breadcrumbList.length - 1" :style="{ color: themeConfig.footColor }">{{ item.meta.name }}</span>
<span class="redirect" v-else @click="handleRedirect(item.path)" :style="{ color: themeConfig.footColor }">{{ item.meta.name }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup>
import { watch, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import store from "../../../store";
import { useStore } from "vuex";
const route = useRoute();
const router = useRouter();
const breadcrumbList = ref([]);
const isCollapse = ref(false);
const globalStore = useStore();
const themeConfig = store.getters.themeConfig;
const initBreadcrumbList = () => {
breadcrumbList.value = route.matched;
console.log(route.matched);
};
const handleRedirect = (path) => {
router.push(path);
};
const changeShrinkage = () => {
isCollapse.value = !store.getters.isCollapse;
globalStore.dispatch("user/changeIsCollapse", isCollapse.value);
};
watch(
route,
() => {
initBreadcrumbList();
},
{ deep: true, immediate: true }
);
</script>
<style lang="scss" scoped>
.no-redirect {
// color: #97a8be;
cursor: text;
}
.redirect {
font-weight: 600;
cursor: pointer;
&:hover {
color: #97a8be;
}
}
.icon-style {
font-size: 20px;
cursor: pointer;
margin-right: 20px;
}
</style>
@@ -0,0 +1,25 @@
export const steps = () => [{
element: "#guide",
popover: {
title: "菜单栏",
description: "Body of the popover",
position: "left",
},
},
{
element: "#breadcrumb",
popover: {
title: "标签页",
description: "",
position: "bottom",
},
},
{
element: "#screenFul",
popover: {
title: "全屏",
description: "Body of the popover",
position: "bottom",
},
},
];
@@ -0,0 +1,33 @@
<template>
<el-icon @click="handleDriver"><Position /></el-icon>
</template>
<script setup>
import Driver from 'driver.js'
import 'driver.js/dist/driver.min.css'
import { onMounted, ref } from 'vue'
import { steps } from './index.js'
let driver = ref('')
onMounted(() => {
driver.value = new Driver({
className: 'scoped-class', //包裹driver.js弹窗的类名 className to wrap driver.js popover
animate: true, // 高亮元素改变时是否显示动画 Animate while changing highlighted element
opacity: 0.75, //背景透明度(0 表示只有弹窗并且没有遮罩) Background opacity (0 means only popovers and without overlay)
padding: 10, // 元素与边缘的距离 Distance of element from around the edges
allowClose: true, // 是否允许点击遮罩时关闭 Whether clicking on overlay should close or not
overlayClickNext: false, //是否允许点击遮罩时移到到下一步 Should it move to next step on overlay click
doneBtnText: '完成', // 最终按钮上的文本 Text on the final button
closeBtnText: '跳过', // 当前步骤关闭按钮上的文本 Text on the close button for this step
nextBtnText: '下一步', //当前步骤下一步按钮上的文本 Next button text for this step
prevBtnText: '上一步',
})
})
const handleDriver = () => {
driver.value.defineSteps(steps)
driver.value.start()
}
</script>
<style lang="scss" scoped></style>
+66
View File
@@ -0,0 +1,66 @@
<template>
<el-icon class="icon-style" @click="dialogVisible = true"><Search /></el-icon>
<el-dialog v-model="dialogVisible" :show-close="false">
<template #header>
<el-input
v-model="searchValue"
placeholder="搜索"
class="input-with-select"
>
<template #prepend>
<el-button :icon="Search" />
</template>
</el-input>
</template>
<div class="centent col-center">暂无搜索结果🦁🦁🦁🦁</div>
<template #footer>
<div class="footer-tip flx-row">
<span></span> 确认 <span class="ml10"></span>切换
<span class="ml10" style="font-size: 10px">ESC</span>关闭
</div>
</template>
</el-dialog>
</template>
<script setup>
import { Search, Top, Bottom } from '@element-plus/icons-vue'
import { nextTick, onMounted, ref } from 'vue'
const dialogVisible = ref(false)
const searchValue = ref('')
onMounted(() => {
nextTick(() => {
document.addEventListener('keyup', function (e) {
if (e.keyCode == 27) {
dialogVisible.value = false
}
})
})
})
</script>
<style lang="scss" scoped>
.input-with-select {
height: 40px;
}
.footer-tip {
height: 20px;
padding: 0;
font-size: 12px;
span {
display: flex;
width: 20px;
height: 18px;
padding-bottom: 2px;
margin-right: 0.4em;
background-color: linear-gradient(-225deg, #d5dbe4, #f8f8f8);
border-radius: 2px;
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
0 1px 2px 1px #1e235a66;
align-items: center;
justify-content: center;
}
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<el-dropdown placement="bottom">
<div class="flx-center">
<div class="avatar">
<img src="../../../assets/images/avart.jpg" alt="avatar" />
</div>
<span class="username" :style="{ color: themeConfig.footColor }">BIG CUTE</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<!-- <el-dropdown-item>个人中心</el-dropdown-item> -->
<el-dropdown-item @click="gotoLogin">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup>
import { ArrowDown } from "@element-plus/icons-vue";
import { nextTick, onMounted, ref } from "vue";
import store from "../../../store/index.js";
import { useRouter } from "vue-router";
const router = useRouter();
const themeConfig = store.getters.themeConfig;
const handleClick = () => {
// eslint-disable-next-line no-alert
alert("button click");
};
const gotoLogin = () => {
sessionStorage.removeItem("token");
router.push("/login");
};
</script>
<style lang="scss" scoped>
.el-dropdown {
cursor: pointer;
.el-tooltip__trigger {
display: flex;
flex-flow: row;
align-items: center;
}
}
.username {
font-size: 15px;
margin: 0 0 0 10px;
}
.avatar {
width: 35px;
height: 35px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
margin-left: 20px;
img {
width: 100%;
height: 100%;
}
}
</style>
@@ -0,0 +1,33 @@
<template>
<el-icon id="fullscreen" class="icon-style" @click="handleFullScreen">
<component :is="icon ? 'Rank' : 'FullScreen'"></component>
</el-icon>
</template>
<script setup>
import screenfull from 'screenfull'
import { ref, onMounted, onBeforeMount } from 'vue'
import { ElMessage } from 'element-plus'
let icon = ref(screenfull.isFullscreen)
const handleFullScreen = () => {
if (screenfull.isEnabled) {
screenfull.toggle()
}
}
const changeIcon = () => {
icon.value = screenfull.isFullscreen
}
onMounted(() => {
screenfull.on('change', changeIcon)
})
onBeforeMount(() => {
screenfull.off('change')
})
</script>
<style scoped></style>
+78
View File
@@ -0,0 +1,78 @@
<template>
<el-popover :width="400" placement="bottom" trigger="click">
<!-- <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick"> -->
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="通知" name="first">
<div class="notice-lists">
<div class="item one-cut-txt" v-for="item in notList" :key="item.id">
<span class="type">通知</span> {{ item.text }}
</div>
</div>
</el-tab-pane>
<el-tab-pane label="公告" name="second">
<div class="notice-lists">
<div class="item one-cut-txt" v-for="item in notList" :key="item.id">
<span class="type">公告</span> {{ item.text }}
</div>
</div>
</el-tab-pane>
<el-tab-pane label="系统消息" name="third">
<div class="notice-lists">
<div class="item one-cut-txt" v-for="item in notList" :key="item.id">
<span class="type" style="color: rgb(47, 96, 194); background-color: #e8f3ff"
>系统消息</span
>
{{ item.text }}
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #reference>
<el-icon class="icon-style" @click="visible = !visible"><bell /></el-icon>
</template>
</el-popover>
</template>
<script setup name="messages">
import { Bell } from "@element-plus/icons-vue";
import { nextTick, onMounted, ref } from "vue";
const visible = ref(false);
const activeName = ref("first");
const notList = ref("first");
onMounted(() => {});
</script>
<style lang="scss" scoped>
.icon-style {
font-size: 20px;
cursor: pointer;
margin: 0 11px;
}
.notice-lists {
height: 260px;
overflow: scroll;
.item {
font-size: 14px;
padding: 10px;
color: #666;
.type {
font-size: 10px;
padding: 5px 10px;
font-weight: 500;
line-height: 0px;
color: #fa4a1e;
text-align: left;
border-radius: 6px;
background: #fa4a1e14;
&.error-type {
color: #4060c7;
background: #e8f3ff;
}
}
}
}
</style>
+192
View File
@@ -0,0 +1,192 @@
<template>
<el-icon @click="openDrawer" class="icon-style"><Setting /></el-icon>
<el-drawer v-model="drawerVisible" title="布局设置" size="300px">
<el-divider>主题颜色</el-divider>
<div class="flx-row">
<div
class="theme-item"
v-for="item in colorList"
:key="item"
:style="{ backgroundColor: item }"
@click="changePrimary(item)"
>
<el-icon v-if="item == themeConfig.primary" class="icon"
><Select
/></el-icon>
</div>
</div>
<el-divider>导航栏颜色</el-divider>
<div class="flx-row">
<div
class="theme-item"
v-for="item in tabColorList"
:key="item"
:style="{ backgroundColor: item }"
@click="changeTabCole(item)"
>
<el-icon v-if="item == themeConfig.tabColor" class="icon"
><Select
/></el-icon>
</div>
</div>
<!-- <el-switch v-model="value2" class="mt-2" style="margin-left: 24px" inline-prompt :active-icon="Check" :inactive-icon="Close" /> -->
<div class="flx-row">
<div class="flx-tit">侧边栏模式</div>
<el-switch
v-model="value2"
@change="changeGreyOrWeak($event)"
class="mt-2"
style="margin-left: 24px"
inline-prompt
:active-icon="Sunny"
:inactive-icon="Moon"
/>
</div>
<div class="flx-row">
<div class="flx-tit">标签栏是否显示</div>
<el-switch
v-model="istags"
@change="changeTags($event)"
class="mt-2"
style="margin-left: 24px"
/>
</div>
<!-- <div class="theme-item">
<el-color-picker
v-model="themeConfig.primary"
:predefine="colorList"
@change="changePrimary"
>
</el-color-picker>
</div> -->
<!-- <div class="theme-item">
<span>暗黑模式</span>
<SwitchDark></SwitchDark>
</div>
<div class="theme-item">
<span>灰色模式</span>
<el-switch
v-model="themeConfig.isGrey"
@change="changeGreyOrWeak($event, 'grey')"
/>
</div>
<div class="theme-item">
<span>色弱模式</span>
<el-switch
v-model="themeConfig.isWeak"
@change="changeGreyOrWeak($event, 'weak')"
/>
</div>
<br />
<el-divider class="divider" content-position="center">
<el-icon><Setting /></el-icon>
界面设置
</el-divider>
<div class="theme-item">
<span>折叠菜单</span>
<el-switch v-model="isCollapse" />
</div>
<div class="theme-item">
<span>面包屑导航</span>
<el-switch v-model="themeConfig.breadcrumb" />
</div>
<div class="theme-item">
<span>标签栏</span>
<el-switch v-model="themeConfig.tabs" />
</div>
<div class="theme-item">
<span>页脚</span>
<el-switch v-model="themeConfig.footer" />
</div> -->
</el-drawer>
</template>
<script setup>
import { ref, computed, useAttrs } from 'vue'
import { Sunny, Moon } from '@element-plus/icons-vue'
import store from '../../../store/index.js'
import { mix } from '../../../utils/color.js'
import { useStore } from 'vuex'
const value2 = ref(true)
const istags = computed(() => {
return store.getters.themeConfig.istags
})
// 预定义主题颜色
const colorList = [
'#4060c7',
'#409EFF',
'#009688',
'#27ae60',
'#e74c3c',
'#fd726d',
'#f39c12',
'#9b59b6',
]
// 预导航栏颜色
const tabColorList = [
'#FFFFFF',
'#333333',
'#009688',
'#27ae60',
'#e74c3c',
'#fd726d',
'#f39c12',
'#9b59b6',
]
// 主题初始化
const globalStore = useStore()
const themeConfig = computed(() => {
return store.getters.themeConfig
})
const changePrimary = (val) => {
globalStore.dispatch('user/changeThem', val)
}
const changeTabCole = (val) => {
globalStore.dispatch('user/changeTabColor', val)
}
const drawerVisible = ref(false)
const openDrawer = () => {
drawerVisible.value = true
}
const changeGreyOrWeak = (e) => {
globalStore.dispatch('user/changeMenuColor', e)
}
const changeTags = (e) => {
globalStore.dispatch('user/changeTags', e)
}
</script>
<style scoped lang="scss">
.icon-style {
font-size: 20px;
cursor: pointer;
margin: 0 11px;
}
.flx-row {
flex-wrap: wrap;
}
.flx-tit {
color: #303133;
flex: 1;
font-size: 14px;
}
.theme-item {
margin: 16px 5px;
width: 20px;
height: 20px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 2px;
position: relative;
.icon {
color: #fff;
position: absolute;
top: 3px;
right: 3px;
font-size: 14px;
}
}
</style>
+89
View File
@@ -0,0 +1,89 @@
<template>
<div
class="header"
:style="{ backgroundColor: themeConfig.tabColor, color: themeConfig.footColor }"
>
<div class="header-lf flx-center">
<Breadcrumb></Breadcrumb>
</div>
<div class="header-ri flx-center">
<!-- <Search></Search> -->
<el-icon class="icon-style" @click="refresh"><Refresh /></el-icon>
<!-- <message></message> -->
<!-- <Driver></Driver> -->
<fullScreen></fullScreen>
<avatar></avatar>
<setting></setting>
</div>
</div>
</template>
<script setup name="header">
import { inject, ref } from "vue";
import store from "../../store/index.js";
// import { Setting, Search } from '@element-plus/icons-vue'
import avatar from "./components/avatar.vue";
import setting from "./components/setting.vue";
import Search from "./components/Search.vue";
import fullScreen from "./components/fullScreen.vue";
import message from "./components/message.vue";
import Breadcrumb from "./components/Breadcrumb.vue";
// import Driver from "./components/Driver/index.vue";
// 局部数据刷新
const refresh = inject("reload");
const themeConfig = store.getters.themeConfig;
</script>
<style scoped lang="scss">
.header {
border-bottom: 1px solid #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
height: 48px;
line-height: 48px;
.header-lf {
.collapse-icon {
font-size: 22px;
margin-right: 20px;
cursor: pointer;
}
}
.header-ri {
// margin-left:10px;
.message-box {
height: 100%;
position: relative;
.dot {
position: absolute;
top: -5px;
right: 10px;
background-color: #d54f50;
border-radius: 50%;
width: 8px;
height: 8px;
}
.bell-icon {
font-size: 22px;
margin-right: 20px;
cursor: pointer;
}
}
.icon-style {
font-size: 20px;
cursor: pointer;
margin: 0 0 0 11px;
}
}
}
</style>
+65
View File
@@ -0,0 +1,65 @@
.el-container {
width: 100%;
height: 100vh;
overflow: hidden;
.el-aside {
width: auto;
overflow: inherit;
}
.el-header,
.el-footer {
height: auto;
padding: 0;
}
.main-tabs {
width: 100%;
margin-bottom: 10px;
background-color: #fff;
border: 0;
box-shadow: none;
overflow: hidden;
height: 30px;
}
.el-main {
background: #f0f2f5;
padding: 10px 13px;
box-sizing: border-box;
// 防止切换出现横向滚动条
overflow-x: hidden;
.main-box {
width: 100%;
height: 100%;
// background-color: #f5f5f5;
// box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
// border-radius: 4px;
// padding: 20px;
box-sizing: border-box;
overflow: auto;
overflow-x: hidden !important;
&::-webkit-scrollbar {
background-color: white;
}
}
}
}
.shake {
animation: shake 0.5s linear;
}
@keyframes shake {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
+61
View File
@@ -0,0 +1,61 @@
<template>
<el-container class="container">
<el-aside :style="{ backgroundColor: themeConfig.backgroundColor }"
><Menu></Menu
></el-aside>
<el-container>
<el-header>
<Header></Header>
</el-header>
<el-main>
<div class="main-tabs flx-row" v-if="themeConfig.istags">
<tabs></tabs>
</div>
<div class="main-box">
<router-view :class="{ shake: disabled }"></router-view>
</div>
</el-main>
</el-container>
</el-container>
</template>
<script setup name="layout">
import { useRouter, onBeforeRouteUpdate } from 'vue-router'
import { ref, watch } from 'vue'
import store from '../store/index.js'
import Header from './header/index.vue'
import Menu from './menu/index.vue'
import tabs from './Tabs/index.vue'
const themeConfig = store.getters.themeConfig
console.log(themeConfig.backgroundColor)
let router = useRouter()
const disabled = ref(false)
// onMounted(() => {
// disabled.value = true
// setTimeout(() => {
// disabled.value = false
// }, 1500)
// })
watch(
() => router.currentRoute.value.path,
(newValue, oldValue) => {
console.log('watch', newValue)
console.log('watch', oldValue)
if (newValue == oldValue) {
} else {
disabled.value = true
setTimeout(() => {
disabled.value = false
}, 1000)
}
},
{ immediate: true }
)
</script>
<style scoped lang="scss">
@import './index.scss';
</style>
@@ -0,0 +1,48 @@
<template>
<template v-for="subItem in menuList" :key="subItem.path">
<el-sub-menu
v-if="subItem.children && subItem.children.length > 0"
:index="subItem.path"
>
<template #title>
<el-icon>
<component :is="subItem.meta.icon"></component>
</el-icon>
<span>{{ subItem.meta.title }}</span>
</template>
<Menu-items :menuList="subItem.children"></Menu-items>
<!--<template v-for="child in subItem.children" :key="child.path">
<template v-if="child.children && child.children.length > 0">
<Menu-items :menuList="child.children"></Menu-items>
</template>
<el-menu-item v-else :index="child.path">
<el-icon>
<component :is="child.meta.icon"></component>
</el-icon>
<template #title>
<span>{{ child.meta.title }}</span>
</template>
</el-menu-item>
</template> -->
</el-sub-menu>
<el-menu-item v-else :index="subItem.path">
<el-icon>
<component :is="subItem.meta.icon"></component>
</el-icon>
<template #title>
<span>{{ subItem.meta.title }}</span>
</template>
</el-menu-item>
</template>
</template>
<script setup>
defineProps(["menuList"]);
</script>
<style scoped>
.icons {
width: 24px;
height: 18px;
}
</style>
+32
View File
@@ -0,0 +1,32 @@
<template>
<template v-for="subItem in menuList" :key="subItem.path">
<el-menu-item v-if="!subItem.children" :index="subItem.url" :route="subItem.url">
<el-icon>
<component :is="subItem.icon"></component>
</el-icon>
<span>{{ subItem.title }}</span>
</el-menu-item>
<!--有多级菜单-->
<el-sub-menu v-if="subItem.children" :key="subItem.url" :index="subItem.url">
<template #title>
<el-icon>
<component :is="subItem.icon"></component>
</el-icon>
<span> {{ subItem.title }}</span>
</template>
<!--递归组件把遍历的值传回子组件完成递归调用-->
<menu-items :menuList="subItem.children"></menu-items>
</el-sub-menu>
</template>
</template>
<script setup>
defineProps(["menuList"]);
</script>
<style scoped>
.icons {
width: 24px;
height: 18px;
}
</style>
+118
View File
@@ -0,0 +1,118 @@
.menu {
transition: all 0.3s ease;
background: #20222a;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
.logo {
transition: width 0.3s ease;
height: 55px;
box-sizing: border-box;
border-bottom: 2px solid #1d1e26;
box-shadow: 2px 0 6px rgb(0 21 41 / 35%);
span {
font-size: 22px;
font-weight: bold;
color: #dadada;
white-space: nowrap;
}
img {
width: 30px;
object-fit: contain;
margin-right: 8px;
}
}
.el-scrollbar {
height: calc(100% - 55px);
.el-menu {
// flex: 1;
overflow: auto;
overflow-x: hidden;
border: none !important;
width: 100% !important;
&::-webkit-scrollbar {
background-color: #20222a;
}
&::-webkit-scrollbar-thumb {
background-color: #41444b;
}
.el-menu-item {
width: 200px;
&.is-active {
background-color: #060708 !important;
}
&.is-active::before {
content: "";
top: 0;
left: 0;
bottom: 0;
width: 4px;
background: $bg-color;
position: absolute;
}
}
.href {
display: inline-block;
text-decoration: none;
color: #bdbdc0;
width: 100%;
height: 100%;
}
}
}
}
// .el-menu--popup {
// .el-menu-item {
// background-color: #20222a;
// width: 200px;
// i {
// margin-right: 5px;
// }
// i,
// span {
// color: hsla(0, 0%, 100%, 0.7);
// }
// //
// &:hover {
// i,
// span {
// color: #fff !important;
// }
// }
// &.is-active {
// background-color: #060708 !important;
// &:before {
// content: "";
// top: 0;
// left: 0;
// bottom: 0;
// width: 4px;
// background: $bg-color;
// position: absolute;
// }
// i,
// span {
// color: #fff !important;
// }
// }
// }
// }
+77
View File
@@ -0,0 +1,77 @@
<template>
<div
id="guide"
class="menu"
:style="{
width: store.getters.isCollapse == true ? '' : '200px',
height: '100%',
color: themeConfig.textColor,
}"
>
<div class="logo" v-if="!store.getters.isCollapse">
<img src="../../assets/logo.png" alt="" style="margin-right: 5px" /> BIG CUTE
</div>
<div class="logo" v-else>
<img src="../../assets/logo.png" alt="" />
</div>
<el-scrollbar style="height: 100%">
<el-menu
:default-active="activeMenu"
:router="true"
:collapse="store.getters.isCollapse"
:collapse-transition="false"
:unique-opened="true"
:background-color="themeConfig.backgroundColor"
:text-color="themeConfig.textColor"
:active-text-color="themeConfig.primary"
>
<menuItems :menuList="menuList"></menuItems>
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import store from "../../store/index.js";
import menuItems from "./components/menuItems.vue";
// 引入本地路由表渲染导航栏
import menuTable from "./menuTable.js";
import { useRouter, useRoute } from "vue-router";
const route = useRoute();
const activeMenu = computed(() => {
return route.path;
});
const menuList = ref([]);
const handleOpen = (key, keyPath) => {
console.log(key, keyPath);
};
const handleClose = (key, keyPath) => {
console.log(key, keyPath);
};
const themeConfig = store.getters.themeConfig;
// 菜单表存在后端的操作方式
onMounted(() => {
// 获取本地菜单表渲染
menuList.value = menuTable;
});
</script>
<style scoped lang="scss">
.menu {
// width: 200px;
.logo {
height: 48px;
line-height: 48px;
padding: 0 20px;
list-style: none;
cursor: pointer;
position: relative;
img {
width: 25px;
vertical-align: middle;
}
}
}
</style>
+14
View File
@@ -0,0 +1,14 @@
// 菜单列表改了路由要记得改菜单相互匹配
const menuTable = [
{
title: "首页",
url: "/home",
icon: "HomeFilled",
},
{
title: "组别管理",
url: "/groupManagement",
icon: "Notebook",
},
];
export default menuTable;
+31
View File
@@ -0,0 +1,31 @@
import { createApp } from "vue";
import router from "./router/index";
import store from "../src/store/index";
import "./styles/common.scss";
import ElementPlus from "element-plus";
import zhCn from "element-plus/es/locale/lang/zh-cn";
// 引入注册自定义指令
import directives from "./instruction/index.js";
import "element-plus/dist/index.css";
import App from "./App.vue";
import * as ELIcons from "@element-plus/icons-vue";
import VMdEditor from "@kangc/v-md-editor";
import vuepressTheme from "@kangc/v-md-editor/lib/theme/vuepress.js";
import "@kangc/v-md-editor/lib/style/base-editor.css";
import "@kangc/v-md-editor/lib/theme/style/vuepress.css";
VMdEditor.use(vuepressTheme);
const app = createApp(App);
// 小图标引用
for (const iconName in ELIcons) {
// 注册组件
app.component(iconName, ELIcons[iconName]);
}
app.use(router).use(store).use(directives).use(VMdEditor).use(ElementPlus, {
locale: zhCn,
});
app.mount("#app");
+30
View File
@@ -0,0 +1,30 @@
import router from "./router.js";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
NProgress.configure({
ease: "ease",
speed: 500,
});
const writeNames = ["/login"];
// router.beforeEach((to, from, next) => {
// NProgress.start();
// if (sessionStorage.getItem("token")) {
// if (to.path === "/login") {
// next("/");
// }
// return next();
// } else {
// if (writeNames.includes(to.path)) {
// next();
// } else {
// next("/login");
// }
// }
// });
router.afterEach((transition) => {
NProgress.done();
});
export default router;
+52
View File
@@ -0,0 +1,52 @@
// 模板自带的功能路由表
import login from "../views/login/index.vue";
import Layout from "../layout/index.vue";
const routerList = [
{
path: "/login",
name: login,
component: login,
},
{
path: "/",
component: Layout,
name: "container",
redirect: "home",
meta: {
requiresAuth: true, //有一些页面是否登录才能进去
name: "首页",
},
children: [
{
path: "/home",
name: "home",
component: () => import("../views/home/index.vue"),
meta: {
requiresAuth: true, //有一些页面是否登录才能进去
name: "首页",
},
},
],
},
{
path: "/group",
name: "employe",
component: Layout,
meta: {
requiresAuth: true, //有一些页面是否登录才能进去
name: "组别管理",
},
children: [
{
path: "/groupManagement",
name: "groupManagement",
component: () => import("../views/groupManagement/index.vue"),
meta: {
requiresAuth: true,
name: "组别管理",
},
},
],
},
];
export default routerList;
+15
View File
@@ -0,0 +1,15 @@
import { createRouter, createWebHashHistory } from "vue-router";
import home from "../views/home/index.vue";
import login from "../views/login/index.vue";
import Layout from "../layout/index.vue";
// 原本模板功能路由表
import routerList from "./originalRoutingTable.js";
const routes = routerList;
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;
+9
View File
@@ -0,0 +1,9 @@
const getters = {
token: (state) => state.user.token,
isCollapse: (state) => state.user.isCollapse,
UserInfo: (state) => state.user.UserInfo,
themeConfig: (state) => state.user.themeConfig,
tabsMenuList: (state) => state.tabs.tabsMenuList
};
export default getters;
+23
View File
@@ -0,0 +1,23 @@
import {
createStore
} from "vuex";
import user from "./modules/users";
import tabs from './modules/tabs'
import getters from "./getters";
import createPersistedState from "vuex-persistedstate";
export default createStore({
getters,
modules: {
user,
tabs
},
plugins: [
// 默认储存在localstorage
createPersistedState({
// 本地储存名
key: "user",
// 指定模块
paths: ["user"],
}),
],
});
+69
View File
@@ -0,0 +1,69 @@
import {
ElStep
} from "element-plus";
import router from "../../router/router.js";
export default {
namespaced: true,
state: {
tabsMenuList: [{
title: "首页",
url: 'home',
icon: "Menu",
close: false,
checked: true
}]
},
mutations: {
addTabsMenu(state, obj) {
let titles = state.tabsMenuList.map(item => item.title).join(',')
if (titles.includes(obj.title)) {
for (let index = 0; index < state.tabsMenuList.length; index++) {
const element = state.tabsMenuList[index];
state.tabsMenuList[index].checked = false
if (element.title == obj.title) {
state.tabsMenuList[index].checked = true
}
}
} else {
state.tabsMenuList.forEach(item => {
item.checked = false
});
state.tabsMenuList.push(obj)
}
},
delectTagMenu(state, item) {
if (state.tabsMenuList.length == 0) {
return
}
for (let i = 0; i < state.tabsMenuList.length; i++) {
let ele = state.tabsMenuList[i]
console.log(ele)
if (ele == item) {
state.tabsMenuList[i - 1].checked = true
state.tabsMenuList.splice(i, 1)
return router.push(state.tabsMenuList[i - 1].url)
}
}
console.log(state.tabsMenuList)
}
},
actions: {
addTabs({
commit
}, str) {
console.log(str);
commit("addTabsMenu", str);
},
delectTag({
commit
}, val) {
commit('delectTagMenu', val)
}
},
};
+112
View File
@@ -0,0 +1,112 @@
import router from "../../router/router.js";
import { mix } from "../../utils/color.js";
import { ElMessage } from "element-plus";
export default {
namespaced: true,
state: {
UserInfo: {},
token: sessionStorage.getItem("token") || "",
isCollapse: true,
themeConfig: {
primary: "#4060c7",
tabColor: "#FFFFFF",
footColor: "#606266",
backgroundColor: "#FFFFFF",
textColor: "#00000099",
istags: true,
},
},
mutations: {
setToken(state, token) {
state.token = token;
},
setUserInfo(state, userinfo) {
state.UserInfo = userinfo;
},
SetIsCollapse(state, isCollapse) {
state.isCollapse = isCollapse;
},
setThemeConfig(state, primary) {
state.themeConfig.primary = primary;
},
setThemeConfigTbaColor(state, primary) {
state.themeConfig.tabColor = primary;
if (primary == "#FFFFFF") {
state.themeConfig.footColor = "#606266";
} else {
state.themeConfig.footColor = "#ffffff";
}
},
setThemeConfigMenuColor(state, primary) {
if (primary) {
state.themeConfig.backgroundColor = "#FFFFFF";
state.themeConfig.textColor = "#00000099";
} else {
state.themeConfig.backgroundColor = "#1d2129";
state.themeConfig.textColor = "#bdbdc0";
}
},
setThemeConfigchangeTags(state, primary) {
if (primary) {
state.themeConfig.istags = true;
} else {
state.themeConfig.istags = false;
}
},
},
actions: {
// 登录
// login({ commit }, userInfo) {
// return new Promise((resolve, reject) => {
// LoginInfo(userInfo)
// .then((res) => {
// console.log(res.data.data);
// sessionStorage.setItem("token", res.data.data.token);
// sessionStorage.setItem("UserInfo", JSON.stringify(res.data.data));
// commit("setToken", res.data.data.token);
// commit("setUserInfo", res.data.data);
// router.replace("/");
// ElMessage({
// message: "登录成功",
// type: "success",
// });
// resolve();
// })
// .catch((err) => {
// reject(err);
// });
// });
// },
changeIsCollapse({ commit }, str) {
console.log(str);
commit("SetIsCollapse", str);
},
changeThem({ commit }, str) {
commit("setThemeConfig", str);
const pre = "--el-color-primary";
// 白色混合色
const mixWhite = "#ffffff";
// 黑色混合色
const mixBlack = "#000000";
const el = document.documentElement;
el.style.setProperty(pre, str);
// 这里是覆盖原有颜色的核心代码
for (let i = 1; i < 10; i += 1) {
el.style.setProperty(`${pre}-light-${i}`, mix(str, mixWhite, i * 0.1));
}
el.style.setProperty("--el-color-primary-dark", mix(str, mixBlack, 0.1));
},
changeTabColor({ commit }, val) {
commit("setThemeConfigTbaColor", val);
},
changeMenuColor({ commit }, val) {
commit("setThemeConfigMenuColor", val);
},
changeTags({ commit }, val) {
commit("setThemeConfigchangeTags", val);
},
},
};
+216
View File
@@ -0,0 +1,216 @@
/*
Reset style sheet
*/
html,
body {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
html,
body,
#app {
height: 100%;
width: 100%;
}
/* flex */
.flx-center {
display: flex;
justify-content: center;
align-items: center;
}
.flx-justify-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.flx-align-center {
display: flex;
align-items: center;
}
.flx-row {
display: flex;
flex-flow: row;
align-items: center;
}
.flx-row-right {
display: flex;
flex-flow: row-reverse;
align-items: center;
}
.col-center {
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
align-items: center;
}
.ml10 {
margin-left: 10px;
}
.p10 {
padding: 10px;
}
.p20 {
padding: 20px;
}
.m10 {
margin: 10px;
}
.m20 {
margin: 20px;
}
.mt10 {
margin-top: 10px;
}
.mt20 {
margin-top: 20px;
}
.w100 {
width: 100%;
}
.ml20 {
margin-left: 20px;
}
.mR10 {
margin-right: 10px;
}
.one-cut-txt {
/*强制文字在一行文本框内*/
white-space: nowrap;
/*溢出部分文字隐藏*/
overflow: hidden;
/*溢出部分省略号处理*/
text-overflow: ellipsis;
}
/* 清除浮动 */
.clearfix::after {
content: "";
display: block;
height: 0px;
clear: both;
overflow: hidden;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.2s;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
transition: all 0.2s;
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
transition: all 0.2s;
}
/* Breadcrumb */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all 0.2s ease;
}
.breadcrumb-enter-from,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(10px);
}
.breadcrumb-leave-active {
position: absolute;
z-index: -1;
}
/* scroll bar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: white;
}
::-webkit-scrollbar-thumb {
box-shadow: inset 0 0 0px white;
-webkit-box-shadow: inset 0 0 0px white;
background-color: #dddee0;
border-radius: 20px;
}
.content-box {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
font-size: 23px;
font-weight: bold;
color: rgb(88, 88, 88);
line-height: 100px;
min-height: 400px;
}
+16
View File
@@ -0,0 +1,16 @@
export const mix = (c1, c2, ratio) => {
ratio = Math.max(Math.min(Number(ratio), 1), 0)
let r1 = parseInt(c1.substring(1, 3), 16)
let g1 = parseInt(c1.substring(3, 5), 16)
let b1 = parseInt(c1.substring(5, 7), 16)
let r2 = parseInt(c2.substring(1, 3), 16)
let g2 = parseInt(c2.substring(3, 5), 16)
let b2 = parseInt(c2.substring(5, 7), 16)
let r = Math.round(r1 * (1 - ratio) + r2 * ratio) + ''
let g = Math.round(g1 * (1 - ratio) + g2 * ratio) + ''
let b = Math.round(b1 * (1 - ratio) + b2 * ratio) + ''
r = ('0' + (r || 0).toString(16)).slice(-2)
g = ('0' + (g || 0).toString(16)).slice(-2)
b = ('0' + (b || 0).toString(16)).slice(-2)
return '#' + r + g + b
}
+44
View File
@@ -0,0 +1,44 @@
export function getDateTime(type) {
var date = new Date();
var hengGang = "-";
var maoHao = ":";
var year = date.getFullYear();
var month = date.getMonth() + 1;
var curDate = date.getDate();
var curHours = date.getHours();
var curMinutes = date.getMinutes();
var curSeconds = date.getSeconds();
if (month >= 1 && month <= 9) {
month = "0" + month;
}
if (curDate >= 0 && curDate <= 9) {
curDate = "0" + curDate;
}
if (curHours >= 0 && curHours <= 9) {
curHours = "0" + curHours;
}
if (curMinutes >= 0 && curMinutes <= 9) {
curMinutes = "0" + curMinutes;
}
if (curSeconds >= 0 && curSeconds <= 9) {
curSeconds = "0" + curSeconds;
}
var currentdate = "";
if (type == "year") {
currentdate = year;
return currentdate;
} else if (type == "month") {
currentdate = year + hengGang + month;
return currentdate;
} else {
currentdate = year + hengGang + month + hengGang + curDate + " "
return currentdate;
}
}
// var year = getDateTime('year');
// console.log(year); // 2021
// var month = getDateTime('month');
// console.log(month); // 12
// var date = getDateTime('');
// console.log(date); // 2021-12-03 09:00:00
+377
View File
@@ -0,0 +1,377 @@
<template>
<div class="group-management-container">
<!-- 左侧组别管理 -->
<div class="left-panel">
<el-card>
<template #header>
<div class="card-header">
<span>组别管理</span>
<el-button type="primary" @click="handleAdd">添加组别</el-button>
</div>
</template>
<el-table :data="groupList" style="width: 100%" @row-click="handleGroupSelect">
<el-table-column prop="groupId" label="组别ID" width="180" />
<el-table-column prop="groupName" label="组别名称" />
<el-table-column prop="createTime" label="创建时间">
<template #default="scope">
{{ scope.row.createTime || "暂无" }}
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button type="primary" link @click.stop="handleEdit(scope.row)"
>编辑</el-button
>
<el-button type="danger" link @click.stop="handleDelete(scope.row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 右侧成员管理 -->
<div class="right-panel">
<el-card v-if="currentGroup">
<template #header>
<div class="card-header">
<span>{{ currentGroup.groupName }} - 成员管理</span>
</div>
</template>
<div class="user-management">
<div class="user-container">
<!-- 组内人员列表 -->
<div class="user-section">
<h3>组内成员</h3>
<el-table :data="groupUsers" style="width: 100%" height="500">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" />
<el-table-column label="角色" width="120">
<template #default="scope">
<el-select
v-model="scope.row.role"
placeholder="选择角色"
@change="(value) => handleRoleChange(scope.row, value)"
>
<el-option label="普通员工" :value="0" />
<el-option label="部门主管" :value="1" />
<el-option label="综合部" :value="2" />
<el-option label="财务部" :value="3" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button type="danger" link @click="handleRemoveUser(scope.row)"
>移除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
<!-- 未分组人员列表 -->
<div class="user-section">
<h3>未分组人员</h3>
<el-table :data="nonGroupUsers" style="width: 100%" height="500">
<el-table-column label="头像" width="80">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" />
<el-table-column label="操作" width="200">
<template #default="scope">
<el-select
v-model="scope.row.selectedRole"
placeholder="选择角色"
style="width: 120px"
>
<el-option label="普通员工" :value="0" />
<el-option label="部门主管" :value="1" />
<el-option label="综合部" :value="2" />
<el-option label="财务部" :value="3" />
</el-select>
<el-button type="primary" link @click="handleAddUser(scope.row)"
>添加</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</el-card>
<el-empty v-else description="请选择左侧组别进行管理" />
</div>
<!-- 添加/编辑组别对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '添加组别' : '编辑组别'"
width="500px"
>
<el-form :model="groupForm" label-width="80px">
<el-form-item label="组别名称" required>
<el-input v-model="groupForm.groupName" placeholder="请输入组别名称" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
getGroupList,
addGroup,
updateGroup,
deleteGroup,
getNonGroupUsers,
updateGroupUserRole,
getGroupUsers,
deleteGroupUser,
} from "../../api/modules/index";
// 组别列表数据
const groupList = ref([]);
const currentGroup = ref(null);
// 对话框相关
const dialogVisible = ref(false);
const dialogType = ref("add"); // add 或 edit
const groupForm = ref({
groupId: null,
groupName: "",
});
// 成员管理相关
const nonGroupUsers = ref([]);
const groupUsers = ref([]);
// 获取组别列表
const fetchGroupList = async () => {
try {
const res = await getGroupList();
if (res.errcode === 0) {
groupList.value = res.result;
}
} catch (error) {
ElMessage.error("获取组别列表失败");
}
};
// 选择组别
const handleGroupSelect = async (row) => {
currentGroup.value = row;
await Promise.all([fetchNonGroupUsers(), fetchGroupUsers(row.groupId)]);
};
// 获取未分组人员列表
const fetchNonGroupUsers = async () => {
try {
const res = await getNonGroupUsers();
if (res.errcode === 0) {
nonGroupUsers.value = res.result.map((user) => ({
...user,
selectedRole: 0,
}));
}
} catch (error) {
ElMessage.error("获取未分组人员列表失败");
}
};
// 获取组内人员列表
const fetchGroupUsers = async (groupId) => {
try {
const res = await getGroupUsers(groupId);
if (res.errcode === 0) {
groupUsers.value = res.result;
}
} catch (error) {
ElMessage.error("获取组内人员列表失败");
}
};
// 添加成员
const handleAddUser = async (user) => {
try {
const res = await updateGroupUserRole(
currentGroup.value.groupId,
user.userid,
user.selectedRole
);
if (res.errcode === 0) {
ElMessage.success("添加成功");
await Promise.all([
fetchNonGroupUsers(),
fetchGroupUsers(currentGroup.value.groupId),
]);
}
} catch (error) {
ElMessage.error("添加失败");
}
};
// 修改成员角色
const handleRoleChange = async (user, role) => {
try {
const res = await updateGroupUserRole(currentGroup.value.groupId, user.userid, role);
if (res.errcode === 0) {
ElMessage.success("修改成功");
}
} catch (error) {
ElMessage.error("修改失败");
}
};
// 移除成员
const handleRemoveUser = async (user) => {
try {
const res = await deleteGroupUser(currentGroup.value.groupId, user.userid);
if (res.errcode === 0) {
ElMessage.success("移除成功");
await Promise.all([
fetchNonGroupUsers(),
fetchGroupUsers(currentGroup.value.groupId),
]);
}
} catch (error) {
ElMessage.error("移除失败");
}
};
// 添加组别
const handleAdd = () => {
dialogType.value = "add";
groupForm.value = {
groupId: null,
groupName: "",
};
dialogVisible.value = true;
};
// 编辑组别
const handleEdit = (row) => {
dialogType.value = "edit";
groupForm.value = {
groupId: row.groupId,
groupName: row.groupName,
};
dialogVisible.value = true;
};
// 删除组别
const handleDelete = (row) => {
ElMessageBox.confirm("确认删除该组别吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
try {
const res = await deleteGroup(row.groupId);
if (res.errcode === 0) {
ElMessage.success("删除成功");
if (currentGroup.value?.groupId === row.groupId) {
currentGroup.value = null;
}
fetchGroupList();
}
} catch (error) {
ElMessage.error("删除失败");
}
});
};
// 提交表单
const handleSubmit = async () => {
if (!groupForm.value.groupName) {
ElMessage.warning("请输入组别名称");
return;
}
try {
if (dialogType.value === "add") {
const res = await addGroup({ groupName: groupForm.value.groupName });
if (res.errcode === 0) {
ElMessage.success("添加成功");
dialogVisible.value = false;
fetchGroupList();
}
} else {
const res = await updateGroup({
groupId: groupForm.value.groupId,
groupName: groupForm.value.groupName,
});
if (res.errcode === 0) {
ElMessage.success("修改成功");
dialogVisible.value = false;
fetchGroupList();
}
}
} catch (error) {
ElMessage.error(dialogType.value === "add" ? "添加失败" : "修改失败");
}
};
// 页面加载时获取组别列表
onMounted(() => {
fetchGroupList();
});
</script>
<style lang="scss" scoped>
.group-management-container {
display: flex;
gap: 20px;
height: calc(100vh - 120px);
padding: 20px;
.left-panel {
width: 40%;
min-width: 500px;
}
.right-panel {
flex: 1;
min-width: 600px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
.user-management {
.user-container {
display: flex;
gap: 20px;
}
.user-section {
flex: 1;
min-width: 0; // 防止flex子项溢出
h3 {
margin-bottom: 10px;
font-size: 16px;
font-weight: 500;
}
}
}
</style>
+390
View File
@@ -0,0 +1,390 @@
.data-lists {
width: 100%;
.el-col {
height: 100%;
}
.data-item-one {
background: #2969ff;
margin-right: 10px;
padding: 10px;
height: 156px;
border-radius: 6px;
color: #fff;
.item-left {}
.item-right {
padding-left: 20px;
padding-right: 10px;
flex: 1;
.item-right-top {
margin-bottom: 10px;
.tit {
padding-bottom: 5px;
}
.num {
font-size: 24px;
font-weight: 700;
text-align: left;
}
}
.line {
width: 100%;
height: 2px;
background-color: #fff;
opacity: 0.1;
}
.item-right-bottom {
margin-top: 10px;
.item {
flex: 1;
.tit {
font-size: 14px;
padding-bottom: 5px;
}
.num {
font-size: 18px;
font-weight: bold;
}
}
}
}
}
.data-item-two {
height: 176px;
margin-right: 10px;
display: flex;
flex-wrap: wrap;
.item {
height: 50%;
width: 50%;
.item-des {
margin-right: 8px;
margin-bottom: 8px;
background-color: #fff;
padding: 20px;
border-radius: 6px;
&:nth-child(2n) {
margin-right: 0px;
}
.right {
flex: 1;
padding-left: 22px;
.num {
font-size: 18px;
font-weight: 700;
letter-spacing: 0px;
span {
font-size: 12px;
color: rgba(30, 189, 94, 1);
}
}
.txt {
padding-top: 8px;
font-size: 12px;
font-weight: 400;
color: rgba(0, 0, 0, 0.3);
}
}
}
}
}
.data-item-three {
padding: 10px;
height: 156px;
margin-bottom: 10px;
background-color: #fff;
border-radius: 6px;
.tit {
font-size: 14px;
padding-bottom: 5px;
}
.content {
display: flex;
flex-wrap: wrap;
.item {
width: 50%;
height: 50%;
.item-data {
margin: 5px;
opacity: 1;
padding: 10px;
border-radius: 6px;
background: #fafafcff;
position: relative;
.icon {
position: absolute;
right: 16px;
top: 40%;
color: rgba(25, 30, 36, 0.5);
font-size: 14px;
}
.tit {
font-size: 12px;
font-weight: 400;
color: rgba(25, 30, 36, 0.5);
padding-bottom: 5px;
}
.num {
color: #0052d9;
font-size: 14px;
font-weight: 700;
span {
color: #666;
font-weight: 400;
color: rgba(25, 30, 36, 0.5);
}
}
}
}
}
}
}
.dataLayer {
.datalayer-echarts {
flex: 1;
height: 260px;
margin-right: 10px;
margin-bottom: 10px;
background-color: #fff;
border-radius: 6px;
}
.table-data {
border-radius: 6px;
margin-right: 10px;
background-color: #fff;
padding-bottom: 10px;
.tit {
padding: 10px 10px 0 10px;
font-size: 14px;
font-weight: bold;
letter-spacing: 1px;
}
.table-box {
margin: 10px;
.el-table {
width: 100%;
height: 360px;
.type {
font-size: 10px;
padding: 5px 10px;
font-weight: 500;
line-height: 0px;
color: #fa4a1e;
text-align: left;
border-radius: 6px;
background: #fa4a1e14;
&.error-type {
color: #4060c7;
background: #e8f3ff;
}
}
.state {
display: inline-block;
width: 8px;
height: 8px;
background-color: #4060c7;
border-radius: 50%;
margin-right: 5px;
&.error-state {
background-color: #fa4a1e;
}
}
}
}
}
.data-item-three {
padding: 10px;
// height: 156px;
margin-bottom: 10px;
background-color: #fff;
border-radius: 6px;
.tit {
font-size: 14px;
padding-bottom: 5px;
}
.content {
display: flex;
flex-wrap: wrap;
.item {
width: 50%;
height: 50%;
.item-data {
margin: 5px;
opacity: 1;
padding: 10px;
border-radius: 6px;
background: #fafafcff;
position: relative;
align-items: center;
.icon {
position: absolute;
right: 16px;
top: 40%;
color: rgba(25, 30, 36, 0.5);
font-size: 14px;
}
.tit {
font-size: 12px;
font-weight: 400;
color: rgba(25, 30, 36, 0.5);
padding-bottom: 5px;
display: flex;
align-items: center;
.el-icon {
font-size: 20px;
color: rgba(25, 30, 36, 0.5);
margin-right: 5px;
}
}
}
}
}
}
.notice {
width: 100%;
height: 152px;
background-color: #fff;
border-radius: 6px;
overflow: hidden;
margin-bottom: 10px;
.notice-news {
width: 100%;
height: 100%;
position: relative;
img {
width: 100%;
object-fit: cover;
}
.txt {
position: absolute;
bottom: 0;
right: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.3);
font-size: 14px;
color: #fff;
padding: 10px;
}
}
}
.notice-lists {
height: 260px;
overflow: scroll;
.item {
font-size: 14px;
padding: 10px;
color: #666;
.type {
font-size: 10px;
padding: 5px 10px;
font-weight: 500;
line-height: 0px;
color: #fa4a1e;
text-align: left;
border-radius: 6px;
background: #fa4a1e14;
&.error-type {
color: #4060c7;
background: #e8f3ff;
}
}
}
}
}
.border-right-1px {
border-right: 1px solid #eae3e3;
}
.icon-edit {
font-size: 20px;
color: #2f60c2;
margin: 0 10px;
}
.icon-dele {
font-size: 20px;
color: #ff5722;
}
.shake {
animation: shake 0.5s linear;
}
.shakeRight {
animation: shakeRight 0.5s linear;
}
@keyframes shake {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
@keyframes shakeRight {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
+9
View File
@@ -0,0 +1,9 @@
<template>
<el-card>
<div>欢迎使用💐🌸💮🏵🌹🌺🌻</div>
</el-card>
</template>
<script setup></script>
<style lang="scss" scoped></style>
+99
View File
@@ -0,0 +1,99 @@
.login-container {
width: 100vw;
height: 100vh;
// background-color: #6c88d5;
background-image: url('../../assets/bg.png');
background-size: 100% 100%;
display: flex;
align-items: center;
justify-content: center;
.login-box {
width: 96%;
height: 94%;
background-color: hsla(0, 0%, 100%, 0.85);
border-radius: 10px;
padding: 0 8% 0 50px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-around;
.login-left {
width: 800px;
color: #6E736FFF;
.title {
font-size: 48px;
font-weight: 400;
letter-spacing: 0px;
color: rgba(47, 57, 78, 1);
text-align: left;
}
img {
width: 735px;
height: 465px;
}
}
.login-form {
width: 400px;
height: 260px;
padding: 10px 40px 35px 40px;
border-radius: 10px;
-webkit-box-shadow: 2px 3px 7px rgb(0 0 0 / 20%);
box-shadow: 2px 3px 7px rgb(0 0 0 / 20%);
.login-logo {
display: flex;
align-items: center;
justify-content: center;
// margin-bottom: 45px;
.login-icon {
width: 60px;
// height: 52px;
}
.logo-text {
font-weight: bold;
font-size: 32px;
padding-left: 25px;
white-space: nowrap;
color: #2d51e6;
}
}
.el-form-item {
margin-bottom: 20px;
}
.flx-row {
span {
font-size: 14px;
color: #2d51e6;
}
.img {
flex: 1;
text-align: center;
}
}
.login-btn {
margin-top: 20px;
padding-bottom: 20px;
display: flex;
justify-content: space-between;
width: 100%;
white-space: nowrap;
.el-button {
width: 185px;
}
}
}
}
}
+82
View File
@@ -0,0 +1,82 @@
<template>
<div class="login-container flx-center">
<div class="login-box">
<div class="login-left">
<div class="title">人资薪资核算系统<span style="font-size: 14px">v1.7</span></div>
<div class="sub">用于自动管理核算人员工资</div>
<img src="../../assets/login.png" alt="" />
</div>
<div class="login-form">
<div class="login-logo">
<img class="login-icon" src="../../assets/logo.png" alt="" />
<p class="logo-text">人资薪资核算系统</p>
</div>
</div>
</div>
</div>
</template>
<script setup name="login">
import { useRouter } from "vue-router";
import { ref } from "vue";
// import { login, obtainUserInformationApi } from "../../api/modules/index";
import { ElMessage } from "element-plus";
import dd from "dingtalk-jsapi";
const router = useRouter();
// 登录
const loginIn = async (code) => {
let res = await login(code);
sessionStorage.setItem("token", res.result.token);
ElMessage({
message: "授权成功",
type: "success",
});
// 拿用户信息
obInformation();
router.push("/");
};
// 登录判断
const token = sessionStorage.getItem("token");
if (token) {
// 有then代表已经登录
} else {
// 钉钉环境去登录,
if (dd.env.platform === "notInDingTalk") {
// ElMessage({
// message: "检测到不是钉钉环境,请进入钉钉环境打开",
// type: "error",
// });
router.push("/");
console.log("跳转");
} else {
dd.ready(() => {
dd.runtime.permission.requestAuthCode({
corpId: "ding03f8e88dde9b426835c2f4657eb6378f", //企业id
onSuccess: function (info) {
loginIn(info.code);
},
});
});
}
// 测试修改环境
// loginIn("4545");
}
// 获取个人信息
const obInformation = async () => {
let res = await obtainUserInformationApi();
const userInfo = res.result;
ElMessage({
message: "获取个人信息成功",
type: "success",
});
sessionStorage.setItem("userInfo", JSON.stringify(userInfo));
router.push("/");
};
</script>
<style scoped lang="scss">
@import "./index.scss";
</style>
+61
View File
@@ -0,0 +1,61 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { viteMockServe } from "vite-plugin-mock";
import viteCompression from "vite-plugin-compression";
export default defineConfig({
viteMockServe: {
supportTs: false,
logger: false,
mockPath: "./mock/index.js",
},
plugins: [
vue(),
viteCompression({
//gzip压缩
verbose: true,
disable: false,
threshold: 10240,
algorithm: "gzip",
ext: ".gz",
}),
],
build: {
minify: "terser", // 明确指定使用 Terser 作为压缩工具
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
// chunkSizeWarningLimit: 1500,大文件报警阈值设置,不建议使用
rollupOptions: {
output: {
//静态资源分类打包
chunkFileNames: "static/js/[name]-[hash].js",
entryFileNames: "static/js/[name]-[hash].js",
assetFileNames: "static/[ext]/[name]-[hash].[ext]",
manualChunks(id) {
//静态资源分拆打包
if (id.includes("node_modules")) {
return id
.toString()
.split("node_modules/")[1]
.split("/")[0]
.toString();
}
},
},
},
},
css: {
/* CSS 预处理器 导入全局样式*/
preprocessorOptions: {
scss: {
// additionalData: '@import "src/styles/common.scss";',
},
},
},
});