前端框架整体修改

This commit is contained in:
扈兆增 2026-03-27 14:50:35 +08:00
parent 30c469d7aa
commit f62efcec81
75 changed files with 1040 additions and 5126 deletions

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="NewFrameWork2023-WEB" /> <meta name="description" content="NewFrameWork2023-WEB" />
<meta name="keywords" content="NewFrameWork2023-WEB" /> <meta name="keywords" content="NewFrameWork2023-WEB" />

View File

@ -10,11 +10,13 @@
"prettier": "prettier --write ." "prettier": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@element-plus/icons-vue": "^2.0.10", "@element-plus/icons-vue": "^2.0.10",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.2",
"@vueuse/core": "^9.1.1", "@vueuse/core": "^9.1.1",
"@wangeditor/editor": "^5.0.0", "@wangeditor/editor": "^5.0.0",
"@wangeditor/editor-for-vue": "^5.1.10", "@wangeditor/editor-for-vue": "^5.1.10",
"ant-design-vue": "^4.2.6",
"axios": "^1.2.0", "axios": "^1.2.0",
"better-scroll": "^2.4.2", "better-scroll": "^2.4.2",
"default-passive-events": "^2.0.0", "default-passive-events": "^2.0.0",
@ -63,6 +65,5 @@
}, },
"repository": "https://gitee.com/youlaiorg/vue3-element-admin.git", "repository": "https://gitee.com/youlaiorg/vue3-element-admin.git",
"author": "有来开源组织", "author": "有来开源组织",
"license": "MIT", "license": "MIT"
"__npminstall_done": false
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

BIN
frontend/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ElConfigProvider } from 'element-plus'; import { ElConfigProvider } from 'element-plus';
import { useAppStore } from '@/store/modules/app'; import { useAppStore,usetTheme } from '@/store/modules/app';
const appStore = useAppStore(); const appStore = useAppStore();
</script> </script>
<template> <template>
<el-config-provider :locale="appStore.locale" :size="appStore.size"> <el-config-provider :locale="appStore.locale" :size="appStore.size">
<router-view /> <a-config-provider :theme="usetTheme">
<router-view />
</a-config-provider>
</el-config-provider> </el-config-provider>
</template> </template>

View File

@ -1,28 +0,0 @@
import request from '@/utils/request';
export function getMessageList(params:any) {
return request({
url: '/system/message/getMessageList',
method: 'get',
params
});
}
export function setMessageStatus(data:any) {
return request({
url: '/system/message/setMessageStatus?id=' + data,
method: 'post'
});
}
export function setAllMessageStatus() {
return request({
url: '/system/message/setAllMessageStatus',
method: 'post'
});
}
export function deleteMessageById(data:any) {
return request({
url: '/system/message/deleteMessageById?id=' + data,
method: 'post',
data
});
}

View File

@ -1,51 +0,0 @@
import request from '@/utils/request';
//获取表格内容
export function getTaskList(params: any) {
return request({
url: '/system/quartzjob/getQuartzJobList',
method: 'get',
params: params
});
}
//新增表格内容
export function addTaskList(params: any) {
return request({
url: '/system/quartzjob/addQuartzJob',
method: 'post',
data: params
});
}
//删除定时任务
export function delTaskList(params: any) {
return request({
url: '/system/quartzjob/deleteQuartzJob',
method: 'post',
params: params
});
}
//修改定时任务
export function updataTaskList(params: any) {
return request({
url: '/system/quartzjob/updateQuartzJob',
method: 'post',
data: params
});
}
//定时任务是否有效
export function setTaskList(params: any) {
return request({
url: '/system/quartzjob/setQuartzStatus',
method: 'post',
params: params
});
}
//拖拽
export function changeItemOrder(params: any) {
return request({
url: '/system/quartzjob/changeDictOrder',
method: 'post',
params: params
});
}

View File

@ -0,0 +1 @@
<svg width="106" height="51" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-32.8%" y="-233.3%" width="165.6%" height="566.7%" filterUnits="objectBoundingBox" id="a"><feGaussianBlur stdDeviation="7" in="SourceGraphic"/></filter><filter x="-23.7%" y="-289.3%" width="147.4%" height="678.6%" filterUnits="objectBoundingBox" id="b"><feGaussianBlur stdDeviation="3" in="SourceGraphic"/></filter><filter x="-4.7%" y="-300%" width="109.4%" height="700%" filterUnits="objectBoundingBox" id="d"><feGaussianBlur stdDeviation="1" in="SourceGraphic"/></filter><linearGradient x1="100%" y1="50%" x2="0%" y2="50%" id="c"><stop stop-color="#37FDF7" stop-opacity="0" offset="0%"/><stop stop-color="#37FDF7" offset="51.347%"/><stop stop-color="#37FDF7" stop-opacity="0" offset="100%"/></linearGradient></defs><g transform="translate(21 21)" fill="none" fill-rule="evenodd"><ellipse fill="#36FCF6" filter="url(#a)" cx="32" cy="4.5" rx="32" ry="4.5"/><ellipse fill="#11FFF2" filter="url(#b)" cx="32" cy="2" rx="19" ry="1.556"/><path fill="url(#c)" filter="url(#d)" d="M0 1h64v1H0z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,105 +0,0 @@
<template>
<el-breadcrumb
separator-class="el-icon-arrow-right"
class="h-[50px] flex items-center"
>
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path">
<span
v-if="
item.redirect === 'noredirect' || index === breadcrumbs.length - 1
"
class="text-[#97a8be]"
>{{ generateTitle(item.meta.title) }}</span
>
<a v-else @click.prevent="handleLink(item)">
{{ generateTitle(item.meta.title) }}
</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup lang="ts">
import { onBeforeMount, ref, watch } from 'vue';
import { useRoute, RouteLocationMatched } from 'vue-router';
import { compile } from 'path-to-regexp';
import router from '@/router';
import { generateTitle } from '@/utils/i18n';
const currentRoute = useRoute();
const pathCompile = (path: string) => {
const { params } = currentRoute;
const toPath = compile(path);
return toPath(params);
};
const breadcrumbs = ref([] as Array<RouteLocationMatched>);
function getBreadcrumb() {
let matched = currentRoute.matched.filter(
item => item.meta && item.meta.title
);
const first = matched[0];
if (!isDashboard(first)) {
matched = [
{ path: '/dashboard', meta: { title: 'dashboard' } } as any
].concat(matched);
}
breadcrumbs.value = matched.filter(item => {
return item.meta && item.meta.title && item.meta.breadcrumb !== false;
});
}
function isDashboard(route: RouteLocationMatched) {
const name = route && route.name;
if (!name) {
return false;
}
return (
name.toString().trim().toLocaleLowerCase() ===
'Dashboard'.toLocaleLowerCase()
);
}
function handleLink(item: any) {
const { redirect, path } = item;
if (redirect) {
router.push(redirect).catch(err => {
console.warn(err);
});
return;
}
router.push(pathCompile(path)).catch(err => {
console.warn(err);
});
}
watch(
() => currentRoute.path,
path => {
if (path.startsWith('/redirect/')) {
return;
}
getBreadcrumb();
}
);
onBeforeMount(() => {
getBreadcrumb();
});
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

View File

@ -1,59 +0,0 @@
<template>
<a
href="https://github.com/hxrui"
target="_blank"
class="github-corner"
aria-label="View source on Github"
>
<svg
width="80"
height="80"
viewBox="0 0 250 250"
style="fill: #40c9c6; color: #fff"
aria-hidden="true"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
<path
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor"
style="transform-origin: 130px 106px"
class="octo-arm"
/>
<path
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
class="octo-body"
/>
</svg>
</a>
</template>
<style scoped>
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0);
}
20%,
60% {
transform: rotate(-25deg);
}
40%,
80% {
transform: rotate(10deg);
}
}
@media (max-width: 500px) {
.github-corner:hover .octo-arm {
animation: none;
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
}
</style>

View File

@ -1,41 +0,0 @@
<template>
<div
@click="toggleClick"
class="px-[15px] hover:bg-gray-50 cursor-pointer h-[50px] leading-[50px] text-center fixed bottom-2 "
style="z-index:1005;display: flex;display: -webkit-flex; justify-content: center; align-items: center; -webkit-justify-content: center; -webkit-align-items: center;"
:style="appStore.sidebar.opened?'left:80px': 'left:2px'"
>
<img v-if="isActive" src="@/assets/MenuIcon/dh_sq.png" alt="">
<img v-else src="@/assets/MenuIcon/dh_sq1.png" alt="">
</div>
</template>
<script setup lang="ts">
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
defineProps({
isActive: {
required: true,
type: Boolean,
default: false
}
});
const emit = defineEmits(['toggleClick']);
function toggleClick() {
emit('toggleClick');
}
</script>
<style scoped>
.hamburger {
width: 20px;
height: 20px;
fill: #409eff;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

View File

@ -3,14 +3,11 @@ import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { addClass, removeClass } from '@/utils/index'; import { addClass, removeClass } from '@/utils/index';
// import { useSettingsStore } from '@/store/modules/settings';
// //
import { Close, Setting } from '@element-plus/icons-vue'; import { Close, Setting } from '@element-plus/icons-vue';
import { ElColorPicker } from 'element-plus'; import { ElColorPicker } from 'element-plus';
// const settingsStore = useSettingsStore();
const show = ref(false); const show = ref(false);
defineProps({ defineProps({
@ -32,21 +29,6 @@ watch(show, value => {
}); });
function addEventClick() { function addEventClick() {
window.addEventListener('click', closeSidebar, { passive: true });
}
function closeSidebar(evt: any) {
//
let parent = evt.target.closest('.theme-picker-dropdown');
if (parent) {
return;
}
parent = evt.target.closest('.right-panel');
if (!parent) {
show.value = false;
window.removeEventListener('click', closeSidebar);
}
} }
const rightPanel = ref(ElColorPicker); const rightPanel = ref(ElColorPicker);
@ -92,7 +74,6 @@ onBeforeUnmount(() => {
.showRightPanel { .showRightPanel {
overflow: hidden; overflow: hidden;
position: relative; position: relative;
width: calc(100% - 15px);
} }
</style> </style>

View File

@ -16,8 +16,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// import { useSettingsStore } from '@/store/modules/settings';
// const settingsStore = useSettingsStore();
</script> </script>
<style> <style>

View File

@ -6,11 +6,11 @@ export default {
}, },
// 登录页面国际化 // 登录页面国际化
login: { login: {
title: '公司开发平台框架', title: '水电水利建设项目全过程环境管理信息平台',
username: '用户名', username: '用户名',
rulesUsername: '请输入用户名', rulesUsername: '用户账号/身份证号/手机号 不能为空',
password: '密码', password: '密码',
rulesPassword: '请输入密码', rulesPassword: '密码 不能为空',
rulesPasswordPlace: '密码不能少于6位', rulesPasswordPlace: '密码不能少于6位',
login: '登 录', login: '登 录',
code: '请输入验证码', code: '请输入验证码',

View File

@ -22,28 +22,13 @@ const routeKey = computed(() => router.path + Math.random());
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@use "@/styles/variables.module.scss" as *;
.app-main { .app-main {
min-height: calc(100vh - 114px); min-height: calc(100vh - $layout-header-height - $locationbar-height);
width: 100%; width: 100%;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background-color: #f0f2f5; background-color: #ffffff;
padding: 0 16px 16px;
box-sizing: border-box; box-sizing: border-box;
} }
.fixed-header + .app-main {
padding-top: 50px;
}
.hasTagsView {
.app-main {
/* 84 = navbar + tags-view = 50 + 34 */
min-height: calc(100vh - 114px);
}
.fixed-header + .app-main {
padding-top: 84px;
}
}
</style> </style>

View File

@ -1,46 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'; import { ref, onMounted, onBeforeUnmount } from "vue";
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from "vue-router";
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from "element-plus";
import { getToken } from '@/utils/auth'; import { getToken } from "@/utils/auth";
import { UserOutlined, LogoutOutlined } from "@ant-design/icons-vue";
import Screenfull from '@/components/Screenfull/index.vue'; //
import SizeSelect from '@/components/SizeSelect/index.vue'; import { useI18n } from "vue-i18n";
import News from './news.vue'; const { t } = useI18n();
// import LangSelect from '@/components/LangSelect/index.vue'; // import LangSelect from '@/components/LangSelect/index.vue';
import MixNav from './Sidebar/MixNav.vue'; import Sidebar from "./Sidebar/index.vue";
// import { CaretBottom } from '@element-plus/icons-vue';
import Logo from './Sidebar/Logo.vue';
import { useAppStore } from '@/store/modules/app'; import { useTagsViewStore } from "@/store/modules/tagsView";
import { useTagsViewStore } from '@/store/modules/tagsView'; import { useUserStore } from "@/store/modules/user";
import { useUserStore } from '@/store/modules/user'; import Cookies from "js-cookie";
import { useSettingsStore } from '@/store/modules/settings'; import { storeToRefs } from "pinia";
import Cookies from 'js-cookie';
import { storeToRefs } from 'pinia';
const url = import.meta.env.VITE_APP_BASE_API; const url = import.meta.env.VITE_APP_BASE_API;
const username = Cookies.get('username'); const username = Cookies.get("username");
const settingsStore = useSettingsStore();
const { sidebarLogo } = storeToRefs(settingsStore);
const isCollapse = computed(() => !appStore.sidebar.opened);
const appStore = useAppStore();
const tagsViewStore = useTagsViewStore(); const tagsViewStore = useTagsViewStore();
const userStore = useUserStore(); const userStore = useUserStore();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const querystr = ref('');
const dialogVisible = ref(false)
const device = computed(() => appStore.device);
const news = ref()
function logout() { function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', { ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
confirmButtonText: '确定', confirmButtonText: "确定",
cancelButtonText: '取消', cancelButtonText: "取消",
type: 'warning' type: "warning",
}).then(() => { }).then(() => {
userStore userStore
.logout() .logout()
@ -52,114 +39,128 @@ function logout() {
}); });
}); });
} }
function querystrChange() { const badgeval = ref(0);
} const isbadge = ref(true);
const badgeval = ref(0) var source = new EventSource(url + `/sse/connect/` + getToken());
const isbadge = ref(true)
var source = new EventSource(url+ `/sse/connect/` + getToken(),);
onMounted(() => { onMounted(() => {
if ("EventSource" in window) { if ("EventSource" in window) {
source.onmessage = function(e) { source.onmessage = function (e) {
if(e.data>0) { if (e.data > 0) {
badgeval.value = e.data badgeval.value = e.data;
isbadge.value = false isbadge.value = false;
news.value.init()
} else { } else {
isbadge.value = true isbadge.value = true;
} }
}; };
source.onopen = function(e) { source.onopen = function (e) {};
}; source.onerror = function (e: any) {
source.onerror = function(e:any) {
if (e.readyState == EventSource.CLOSED) { if (e.readyState == EventSource.CLOSED) {
} else { } else {
} }
}; };
} else { } else {
}; }
}) });
onBeforeUnmount(()=>{ onBeforeUnmount(() => {
source.close(); source.close();
}) });
</script> </script>
<template> <template>
<div class="navbar"> <div class="navbar">
<logo v-if="sidebarLogo" :collapse="isCollapse" /> <a-layout-header class="header">
<!-- <mix-nav v-if="device !== 'mobile' && settingsStore.layout === 'mix'" /> --> <transition class="bg-white-800">
<div v-if="settingsStore.layout === 'left'" class="flex justify-start"> <a
<div class="flex justify-center items-center"> href="/"
<!--全屏 --> class="h-[50px] min-w-[350px] flex items-center justify-center text-white"
<div v-if="device === 'desktop'" style="position: relative;"> >
<el-input class="keywords" v-model="querystr" placeholder="请输入搜索关键字" clearable <h1 class="text-blank font-bold fontSize-16">{{ t("login.title") }}</h1></a
@change="querystrChange"></el-input> >
<img src="@/assets/MenuIcon/top_ss.png" alt="" style="position: absolute;right: 30px;top: 10px;"> </transition>
</div> <Sidebar />
<div class="flex items-center cursor-pointer" style="margin-right:15px;margin-top: 6px;">
<el-badge :value="badgeval" :hidden="isbadge" :max="10">
<el-icon style="font-size: 22px;" @click="dialogVisible = true,news.init()"><Bell /></el-icon>
</el-badge>
</div>
<!-- 布局大小 -->
<el-tooltip content="字号大小" effect="dark" placement="bottom">
<size-select />
</el-tooltip>
<!-- 全屏 -->
<el-tooltip content="全屏缩放" effect="dark" placement="bottom">
<screenfull id="screenfull" />
</el-tooltip>
<!--语言选择-->
<!-- <lang-select /> -->
</div>
<!-- 头像 -->
<div style="heigth:100%;display: flex;display: -webkit-flex;align-items: center; -webkit-align-items: center; ">
<div class="heighta"></div>
<img v-if="userStore.avatar!== ''" :src="url + '/avatar/' + userStore.avatar + '?imageView2/1/w/80/h/80'"
class="w-[30px] h-[30px] rounded-lg rounded-full" style="border-radius: 50%;" />
<img v-else src="@/assets/MenuIcon/top_tx.png" alt="" class="w-[30px] h-[30px] rounded-lg rounded-full" style="border-radius: 50%;">
</div>
<el-tooltip content="个人中心" effect="dark" placement="bottom"> <a-dropdown :trigger="['click']" placement="bottomRight">
<div class="cursor-pointer flex justify-center items-center pl-[10px]" <a-space class="username">
:style="{ fontSize: appStore.size === 'default' ? '14px' : '16px' }" @click="$router.push('/personalCenter')">{{ <div>
username }}</div> <span class="icon">
</el-tooltip> <UserOutlined />
</span>
<div class="flex justify-center items-center cursor-pointer pr-[12px]" <span class="text">{{ username }}</span>
:style="{ fontSize: appStore.size === 'default' ? '14px' : '16px' }" @click="logout"><el-divider </div>
direction="vertical" style="padding-right:5px" />{{ $t('navbar.logout') }}</div> </a-space>
<News ref="news" v-model:dialog-visible="dialogVisible" /> <template #overlay>
</div> <a-menu>
<a-menu-item key="changePassword">
<UserOutlined />
修改密码
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" @click="logout">
<LogoutOutlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-layout-header>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@use "@/styles/variables.module.scss" as *;
.navbar { .navbar {
width: 100%; width: 100%;
position: relative; position: relative;
height: 60px; height: 110px;
display: -webkit-flex; display: -webkit-flex;
display: flex;
align-items: center;
-webkit-align-items: center;
justify-content: space-between;
-webkit-justify-content: space-between;
box-shadow: 0px 0px 10px rgb(219 225 236);
z-index: 98; z-index: 98;
}
.keywords { .header {
width: 240px; width: 100%;
height: 34px; display: flex;
margin-right: 20px; align-items: center;
background-color: #005293;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
height: $layout-header-height;
position: sticky;
top: 0;
z-index: 100;
padding-left: 0;
padding-right: 10px;
}
.username {
height: $layout-header-height;
line-height: $layout-header-height;
display: flex;
cursor: pointer;
:deep(.el-input__wrapper) { div {
padding-left: 15px; display: flex;
border-radius: 40px; align-items: center;
}
.icon {
width: 32px;
height: 32px;
line-height: 32px;
text-align: center;
color: $main-menu-color;
border-radius: 50%;
display: inline-block;
font-size: 18px;
background-color: rgba(255, 255, 255, 0.2);
}
.text {
color: $main-menu-color;
padding-left: 10px;
margin-top: -5px;
}
} }
} }
.heighta{
border-left:1px solid #dcdfe6 ; .heighta {
border-left: 1px solid #dcdfe6;
height: 1em; height: 1em;
margin-right: 15px; margin-right: 15px;
} }

View File

@ -1,198 +0,0 @@
<script setup lang="ts">
import { Sunny, Moon } from '@element-plus/icons-vue';
import { useSettingsStore } from '@/store/modules/settings';
import { useDark, useToggle } from '@vueuse/core';
import { ElDivider, ElSwitch, ElTooltip } from 'element-plus';
import { onMounted } from 'vue';
const settingsStore = useSettingsStore();
const isDark = useDark();
function toggleTheme() {
const isDark = useDark();
useToggle(isDark);
}
onMounted(() => {
window.document.body.setAttribute('layout', settingsStore.layout);
});
function changeLayout(layout: string) {
settingsStore.changeSetting({ key: 'layout', value: layout });
window.document.body.setAttribute('layout', settingsStore.layout);
}
</script>
<template>
<div class="settings-container">
<h3 class="text-base font-bold">项目配置</h3>
<el-divider />
<div class="drawer-item">
<span>开启 Tags-View</span>
<el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>固定 Header</span>
<el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>侧边栏 Logo</span>
<el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
</div>
<el-divider>主题</el-divider>
<div class="flex justify-center" @click.stop>
<el-switch
v-model="isDark"
inline-prompt
@change="toggleTheme"
:active-icon="Sunny"
:inactive-icon="Moon"
/>
</div>
<el-divider>导航栏布局</el-divider>
<ul class="layout">
<el-tooltip content="左侧模式" placement="bottom">
<li
:class="
'layout-item layout-left ' +
(settingsStore.layout == 'left' ? 'is-active' : '')
"
@click="changeLayout('left')"
>
<div />
<div />
</li>
</el-tooltip>
<el-tooltip content="顶部模式" placement="bottom">
<li
:class="
'layout-item layout-top ' +
(settingsStore.layout == 'top' ? 'is-active' : '')
"
@click="changeLayout('top')"
>
<div />
<div />
</li>
</el-tooltip>
<el-tooltip content="混合模式" placement="bottom">
<li
:class="
'layout-item layout-mix ' +
(settingsStore.layout == 'mix' ? 'is-active' : '')
"
@click="changeLayout('mix')"
>
<div />
<div />
</li>
</el-tooltip>
</ul>
</div>
</template>
<style lang="scss" scoped>
.settings-container {
padding: 16px;
font-size: 14px;
.drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
line-height: 22px;
}
.drawer-item {
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
padding: 12px 0;
}
.drawer-switch {
float: right;
}
.layout {
display: flex;
display: -webkit-flex;
flex-wrap: wrap;
justify-content: space-around;
width: 100%;
height: 50px;
padding: 0;
&-item {
width: 18%;
height: 45px;
background: #f0f2f5;
position: relative;
overflow: hidden;
cursor: pointer;
border-radius: 4px;
}
&-item.is-active {
border: 2px solid var(--el-color-primary);
}
&-left {
div {
&:nth-child(1) {
width: 30%;
height: 100%;
background: #1b2a47;
}
&:nth-child(2) {
width: 70%;
height: 30%;
top: 0;
right: 0;
background: #fff;
box-shadow: 0 0 1px #888;
position: absolute;
}
}
}
&-top {
div {
&:nth-child(1) {
width: 100%;
height: 30%;
background: #1b2a47;
box-shadow: 0 0 1px #888;
}
}
}
&-mix {
div {
&:nth-child(1) {
width: 100%;
height: 30%;
background: #1b2a47;
box-shadow: 0 0 1px #888;
}
&:nth-child(2) {
width: 30%;
height: 70%;
bottom: 0;
left: 0;
background: #1b2a47;
box-shadow: 0 0 1px #888;
position: absolute;
}
}
}
}
}
</style>

View File

@ -1,37 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { isExternal } from '@/utils/validate';
import { useRouter } from 'vue-router';
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const sidebar = computed(() => appStore.sidebar);
const device = computed(() => appStore.device);
const props = defineProps({
to: {
type: String,
required: true
}
});
const router = useRouter();
function push() {
if (device.value === 'mobile' && sidebar.value.opened == true) {
appStore.closeSideBar(false);
}
router.push(props.to).catch(err => {
console.error(err);
});
}
</script>
<template>
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
<slot />
</a>
<div v-else @click="push">
<slot />
</div>
</template>

View File

@ -1,33 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useAppStore } from '@/store/modules/app';
//
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const appStore = useAppStore();
defineProps({
collapse: {
type: Boolean,
required: true
}
});
const logo = ref<string>(
new URL(`../../../assets/logo.png`, import.meta.url).href
);
</script>
<template>
<transition class="ml-5 bg-white-800">
<router-link
key="collapse"
class="h-[50px] flex items-center justify-center"
to="/dashboard"
>
<img :src="logo" class="w-5 h-5" />
<h1 class="ml-3 text-blank font-bold" :style="{fontSize: appStore.size === 'default' ? '20px' : '22px'}">{{t('login.title')}}</h1>
</router-link>
</transition>
</template>

View File

@ -1,159 +0,0 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import {
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElMenu,
ElMessageBox,
ElTooltip
} from 'element-plus';
import Screenfull from '@/components/Screenfull/index.vue';
import SizeSelect from '@/components/SizeSelect/index.vue';
import LangSelect from '@/components/LangSelect/index.vue';
import { CaretBottom } from '@element-plus/icons-vue';
import SidebarItem from './SidebarItem.vue';
import variables from '@/styles/variables.module.scss';
import { useTagsViewStore } from '@/store/modules/tagsView';
import { useUserStore } from '@/store/modules/user';
import { usePermissionStore } from '@/store/modules/permission';
const tagsViewStore = useTagsViewStore();
const userStore = useUserStore();
const permissionStore = usePermissionStore();
const route = useRoute();
const router = useRouter();
const routes = [] as any[];
onMounted(() => {
permissionStore.routes.forEach(item => {
const { ...newItem } = item;
routes.push(newItem);
});
});
const activeMenu = computed<string>(() => {
const { meta, path } = route;
if (meta?.activeMenu) {
return meta.activeMenu as string;
}
return path;
});
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userStore
.logout()
.then(() => {
tagsViewStore.delAllViews();
})
.then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
});
}
</script>
<template>
<div class="horizontal-header">
<el-menu
class="horizontal-header-menu"
:default-active="activeMenu"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:active-text-color="variables.menuActiveText"
mode="horizontal"
>
<sidebar-item
v-for="route in routes"
:item="route"
:key="route.path"
:base-path="route.path"
/>
</el-menu>
<div class="horizontal-header-right">
<!--全屏 -->
<screenfull id="screenfull" />
<!-- 布局大小 -->
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select />
</el-tooltip>
123
<!--语言选择-->
<lang-select />
<el-dropdown trigger="click">
<div class="flex justify-center items-center pr-[20px]">
<img
:src="userStore.avatar + '?imageView2/1/w/80/h/80'"
class="w-[40px] h-[40px] rounded-lg"
/>
<CaretBottom class="w-3 h-3" />
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/">
<el-dropdown-item>{{ $t('navbar.dashboard') }}</el-dropdown-item>
</router-link>
<a target="_blank" href="https://github.com/hxrui">
<el-dropdown-item>Github</el-dropdown-item>
</a>
<a target="_blank" href="https://gitee.com/haoxr">
<el-dropdown-item>{{ $t('navbar.gitee') }}</el-dropdown-item>
</a>
<a target="_blank" href="https://www.cnblogs.com/haoxianrui/">
<el-dropdown-item>{{ $t('navbar.document') }}</el-dropdown-item>
</a>
<el-dropdown-item divided @click="logout">
{{ $t('navbar.logout') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<style lang="scss" scoped>
@use '@/styles/variables.module' as variables;
.horizontal-header {
display: flex;
display: -webkit-flex;
width: 100%;
align-items: center;
-webkit-align-items: center;
justify-content: space-around;
-webkit-justify-content: space-around;
background: #001529;
&-menu {
height: 100%;
width: 100%;
border: none;
background-color: transparent;
}
&-right {
display: flex;
display: -webkit-flex;
min-width: 340px;
align-items: center;
-webkit-align-items: center;
justify-content: flex-end;
-webkit-justify-content: flex-end;
color: #fff;
}
}
</style>

View File

@ -1,125 +0,0 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { isExternal } from "@/utils/validate";
import AppLink from "./Link.vue";
import { generateTitle } from "@/utils/i18n";
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const url = import.meta.env.VITE_APP_BASE_API;
defineProps({
item: {
type: Object,
required: true,
},
isNest: {
type: Boolean,
required: false,
},
// basePath: {
// type: String,
// required: true,
// },
});
const onlyOneChild = ref();
function hasOneShowingChild(children = [] as any, parent: any) {
if (!children) {
children = [];
}
const showingChildren = children.filter((item: any) => {
if (item.meta && item.meta.hidden) {
return false;
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item;
return true;
}
});
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true;
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
return true;
}
return false;
}
function resolvePath(routePath: any) {
// if (isExternal(props.basePath)) {
// return props.basePath;
// }
if (routePath.islink == 0) {
if (isExternal(routePath.opturl)) {
return '';
}
}
return routePath.opturl;
}
onMounted(() => {
generateTitle
})
</script>
<template>
<div v-if="!item.meta || !item.meta.hidden || item.isdisplay == '1'">
<template v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
(!item.meta || !item.meta.alwaysShow)
">
<app-link v-if="onlyOneChild.meta || item.isdisplay == '1'" :to="resolvePath(onlyOneChild)">
<el-menu-item :index="resolvePath(onlyOneChild)" :class="{ 'submenu-title-noDropdown': !isNest }"
style="height: 44px;">
<img v-if="onlyOneChild.icon && onlyOneChild.name == '首页'" src="@/assets/MenuIcon/dh_sy.png"
style="margin-right:10px;width: 16px">
<img v-if="onlyOneChild.icon && onlyOneChild.name !== '首页'" :src="url + '/menu/' + item.icon"
style="margin-right:10px;width: 16px">
<template #title>
<span :style="{ fontSize: appStore.size === 'default' ? '14px' : '16px' }">{{ generateTitle(onlyOneChild.name)
}}</span>
</template>
</el-menu-item>
</app-link>
</template>
<el-sub-menu v-else :index="resolvePath(item)" telested>
<template #title>
<div style="height: 44px; display: flex;display: -webkit-flex; align-items: center;-webkit-align-items: center;justify-content: space-around;">
<img v-if="item.icon" :icon-class="item.icon" :src="url + '/menu/' + item.icon" alt=""
style="margin-right:10px;width: 16px; " class="management">
<span :style="{ fontSize: appStore.size === 'default' ? '14px' : '16px' }" style="display: block;" >{{
generateTitle(item.name) }}</span>
</div>
<!-- <svg-icon v-if="item.icon" :icon-class="item.icon" :style="{fontSize: appStore.size === 'default' ? '14px' : '16px'}" /> -->
</template>
<sidebar-item v-for="child in item.children" :key="child.opturl" :item="child" :is-nest="true"
:base-path="resolvePath(child)" />
</el-sub-menu>
</div>
</template>
<style scoped>
:deep(.el-sub-menu__title) {
height: 44px !important;
}
:deep(.el-icon) {
font-size: 16px;
}
:deep(.el-tooltip__trigger span) {
display: none !important;
}
:deep(.el-tooltip__trigger .management) {
margin-left:20px !important ;
}
:deep(.el-menu-tooltip__trigger){
width: 105% !important;
}
</style>

View File

@ -1,47 +1,173 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { ref, onBeforeMount, onMounted } from "vue";
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from "vue-router";
import SidebarItem from './SidebarItem.vue'; import { usePermissionStore } from "@/store/modules/permission";
import variables from '@/styles/variables.module.scss';
import { useSettingsStore } from '@/store/modules/settings';
import { usePermissionStore } from '@/store/modules/permission';
import { useAppStore } from '@/store/modules/app';
import { storeToRefs } from 'pinia';
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore();
const menus: any = ref([]); //
const subMenus: any = ref([]); //
const activeKey = ref(""); //
const subActiveKey = ref(""); //
const appStore = useAppStore();
const { sidebarLogo } = storeToRefs(settingsStore);
const route = useRoute(); const route = useRoute();
const router = useRouter();
const isCollapse = computed(() => !appStore.sidebar.opened); //
const handleTabChange = (key: string) => {
const activeMenu = computed<string>(() => { let data = menus.value.find((item: any) => item.id === key);
const { meta, path } = route; subMenus.value = data.children;
if (meta?.activeMenu) { activeKey.value = key;
return meta.activeMenu as string; if (data.children.length > 0) {
subActiveKey.value = data.children[0].id;
let subData = subMenus.value.find((item: any) => item.id === subActiveKey.value);
router.push(subData.path || subData.opturl);
} else {
subActiveKey.value = "";
} }
return path; };
}); //
const handleSubTabChange = (key: string) => {
subActiveKey.value = key;
let data = subMenus.value.find((item: any) => item.id === key);
router.push(data.path || data.opturl);
};
onBeforeMount(() => {
console.log(route)
console.log(route)
console.log(permissionStore.routes)
permissionStore.routes.map((item: any) => {
if (item.meta?.hidden) {
return "";
} else {
if (item?.parentid == null) {
menus.value.push(item?.children?.[0]);
} else {
menus.value.push(item);
}
if (item.children) {
item.children.map((child: any) => {
if (child.path === route.path) {
activeKey.value = item.id;
subMenus.value = item.children;
subActiveKey.value = child.id;
}
});
}
}
});
});
onMounted(() => {});
</script> </script>
<template> <template>
<div :class="{ 'has-logo': sidebarLogo }"> <a-tabs
<el-scrollbar> class="sidebar-container"
<el-menu :default-active="activeMenu" :collapse="isCollapse" :background-color="variables.menuBg" v-model:activeKey="activeKey"
:text-color="variables.menuText" :active-text-color="variables.menuActiveText" :unique-opened="false" @change="handleTabChange"
:collapse-transition="false" mode="vertical" > size="small"
<sidebar-item v-for="route in permissionStore.routes" :item="route" :key="route.opturl" >
:is-collapse="isCollapse" /> <a-tab-pane
</el-menu> v-for="route in menus"
</el-scrollbar> :key="route.id"
:tab="route.meta.title"
></a-tab-pane>
</a-tabs>
<div class="sub-menus">
<a-tabs v-model:activeKey="subActiveKey" @change="handleSubTabChange" size="small">
<a-tab-pane
v-for="route in subMenus"
:key="route.id"
:tab="route.meta.title"
></a-tab-pane>
</a-tabs>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@use '@/styles/variables.module' as variables; @use "@/styles/variables.module.scss" as *;
</style>
.sidebar-container {
flex: 1;
overflow-y: hidden !important;
margin: 0 30px;
&::-webkit-scrollbar {
display: none;
}
:deep(.ant-tabs-nav) {
height: $layout-header-height;
margin-bottom: 0;
background-color: transparent;
&::-webkit-scrollbar {
display: none;
}
.ant-tabs-nav-operations {
color: $main-menu-color;
}
}
:deep(.ant-tabs-tab) {
padding: 0 20px;
margin: 0;
border: none;
color: $main-menu-color;
}
:deep(.ant-tabs-tab-active) {
color: $main-menu-color-active !important;
background: $main-menu-bg-color-active;
.ant-tabs-tab-btn {
color: $main-menu-color-active !important;
}
}
:deep(.ant-tabs-tab:hover) {
color: $main-menu-color-active;
}
:deep(.ant-tabs-ink-bar) {
visibility: visible;
border: none;
background: url("@/assets/icons/menuActiveBg.svg") no-repeat center 30px;
height: 51px;
}
}
.sub-menus {
position: absolute;
left: 0;
top: $layout-header-height;
height: $locationbar-height;
width: 100%;
background-color: #f8f8f8;
z-index: 100;
padding-left: 12px;
:deep(.ant-tabs-nav) {
margin-bottom: 0;
.ant-tabs-tab {
padding: 12px 0;
}
.ant-tabs-ink-bar {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAYAAACXU8ZrAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAxMS8wMy8yMrqNQAoAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAOklEQVQImWNgQAchkx3QhZhQeEFTExj+MexnCJoyH1mYEUUBwz8kScYFDOtyEhGKMBSgKmTErQChEAA6FRM7O0rIOgAAAABJRU5ErkJggg==")
no-repeat center bottom;
border-bottom: 2px solid #005293;
height: 7px;
}
.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: #2f6b98 !important;
}
}
}
}
</style>

View File

@ -1,130 +0,0 @@
<script setup lang="ts">
import {
ref,
computed,
onMounted,
onBeforeUnmount,
getCurrentInstance
} from 'vue';
import { useTagsViewStore, TagView } from '@/store/modules/tagsView';
const tagAndTagSpacing = ref(4);
const { proxy } = getCurrentInstance() as any;
const emits = defineEmits(['scroll']);
const emitScroll = () => {
emits('scroll');
};
const tagsViewStore = useTagsViewStore();
const scrollWrapper = computed(
() => proxy?.$refs.scrollContainer.$refs.wrapRef
);
onMounted(() => {
scrollWrapper.value.addEventListener('scroll', emitScroll, true);
});
onBeforeUnmount(() => {
scrollWrapper.value.removeEventListener('scroll', emitScroll);
});
function handleScroll(e: WheelEvent) {
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
scrollWrapper.value.scrollLeft =
scrollWrapper.value.scrollLeft + eventDelta / 4;
}
function moveToTarget(currentTag: TagView) {
const $container = proxy.$refs.scrollContainer.$el;
const $containerWidth = $container.offsetWidth;
const $scrollWrapper = scrollWrapper.value;
let firstTag = null;
let lastTag = null;
// find first tag and last tag
if (tagsViewStore.visitedViews.length > 0) {
firstTag = tagsViewStore.visitedViews[0];
lastTag = tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1];
}
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0;
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
} else {
const tagListDom = document.getElementsByClassName('tags-item');
const currentIndex = tagsViewStore.visitedViews.findIndex(
item => item === currentTag
);
let prevTag = null;
let nextTag = null;
for (const k in tagListDom) {
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
if (
(tagListDom[k] as any).dataset.path ===
tagsViewStore.visitedViews[currentIndex - 1].path
) {
prevTag = tagListDom[k];
}
if (
(tagListDom[k] as any).dataset.path ===
tagsViewStore.visitedViews[currentIndex + 1].path
) {
nextTag = tagListDom[k];
}
}
}
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft =
(nextTag as any).offsetLeft +
(nextTag as any).offsetWidth +
tagAndTagSpacing.value;
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft =
(prevTag as any).offsetLeft - tagAndTagSpacing.value;
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
}
}
}
defineExpose({
moveToTarget
});
</script>
<template>
<el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll"
>
<slot />
</el-scrollbar>
</template>
<style lang="scss" scoped>
.scroll-container {
.el-scrollbar__bar {
bottom: 0px;
}
.el-scrollbar__wrap {
height: 49px;
}
}
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
}
</style>

View File

@ -1,356 +0,0 @@
<script setup lang="ts">
import {
getCurrentInstance,
nextTick,
ref,
watch,
onMounted,
ComponentInternalInstance
} from 'vue';
import path from 'path-browserify';
import { useRoute, useRouter } from 'vue-router';
import { useAppStore } from '@/store/modules/app';
import ScrollPane from './ScrollPane.vue';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { generateTitle } from '@/utils/i18n';
import { usePermissionStore } from '@/store/modules/permission';
import { useTagsViewStore, TagView } from '@/store/modules/tagsView';
const appStore = useAppStore();
const permissionStore = usePermissionStore();
const tagsViewStore = useTagsViewStore();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const router = useRouter();
const route = useRoute();
const visible = ref(false);
const selectedTag = ref({});
const scrollPaneRef = ref();
const left = ref(0);
const top = ref(0);
const affixTags = ref<TagView[]>([]);
watch(
route,
() => {
addTags();
moveToCurrentTag();
},
{
//
immediate: true
}
);
watch(visible, value => {
if (value) {
document.body.addEventListener('click', closeMenu);
} else {
document.body.removeEventListener('click', closeMenu);
}
});
function filterAffixTags(routes: any[], basePath = '/') {
let tags: TagView[] = [];
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path);
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
});
}
if (route.children) {
const childTags = filterAffixTags(route.children, route.path);
if (childTags.length >= 1) {
tags = tags.concat(childTags);
}
}
});
return tags;
}
function initTags() {
const tags = filterAffixTags(permissionStore.routes);
affixTags.value = tags;
for (const tag of tags) {
// Must have tag name
if ((tag as TagView).name) {
tagsViewStore.addVisitedView(tag);
}
}
}
function addTags() {
if (route.name) {
tagsViewStore.addView(route);
}
}
function moveToCurrentTag() {
nextTick(() => {
for (const r of tagsViewStore.visitedViews) {
if (r.path === route.path) {
scrollPaneRef.value.moveToTarget(r);
// when query is different then update
if (r.fullPath !== route.fullPath) {
tagsViewStore.updateVisitedView(route);
}
}
}
});
}
function isActive(tag: TagView) {
return tag.path === route.path;
}
function isAffix(tag: TagView) {
return tag.meta && tag.meta.affix;
}
function isFirstView() {
try {
return (
(selectedTag.value as TagView).fullPath ===
tagsViewStore.visitedViews[1].fullPath ||
(selectedTag.value as TagView).fullPath === '/index'
);
} catch (err) {
return false;
}
}
function isLastView() {
try {
return (
(selectedTag.value as TagView).fullPath ===
tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
);
} catch (err) {
return false;
}
}
function refreshSelectedTag(view: TagView) {
tagsViewStore.delCachedView(view);
const { fullPath } = view;
nextTick(() => {
router.replace({ path: '/redirect' + fullPath }).catch(err => {
console.warn(err);
});
});
}
function toLastView(visitedViews: TagView[], view?: any) {
const latestView = visitedViews.slice(-1)[0];
if (latestView && latestView.fullPath) {
router.push(latestView.fullPath);
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
router.replace({ path: '/redirect' + view.fullPath });
} else {
router.push('/');
}
}
}
function closeSelectedTag(view: TagView) {
tagsViewStore.delView(view).then((res: any) => {
if (isActive(view)) {
toLastView(res.visitedViews, view);
}
});
}
function closeLeftTags() {
tagsViewStore.delLeftViews(selectedTag.value).then((res: any) => {
if (
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) {
toLastView(res.visitedViews);
}
});
}
function closeRightTags() {
tagsViewStore.delRightViews(selectedTag.value).then((res: any) => {
if (
!res.visitedViews.find((item: any) => item.fullPath === route.fullPath)
) {
toLastView(res.visitedViews);
}
});
}
function closeOtherTags() {
router.push(selectedTag.value);
tagsViewStore.delOtherViews(selectedTag.value).then(() => {
moveToCurrentTag();
});
}
function closeAllTags(view: TagView) {
tagsViewStore.delAllViews().then((res: any) => {
toLastView(res.visitedViews, view);
});
}
function openMenu(tag: TagView, e: any) {
const menuMinWidth = 105;
const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
const offsetWidth = proxy?.$el.offsetWidth; // container width
const maxLeft = offsetWidth - menuMinWidth; // left boundary
const l = e.clientX - offsetLeft + 15; // 15: margin right
if (l > maxLeft) {
left.value = maxLeft;
} else {
left.value = l;
}
top.value = e.layerY + 10;
visible.value = true;
selectedTag.value = tag;
}
function closeMenu() {
visible.value = false;
}
function handleScroll() {
closeMenu();
}
onMounted(() => {
initTags();
});
</script>
<template>
<div class="w-full h-[54px] py-[10px]" style="background:#f0f2f5">
<scroll-pane ref="scrollPaneRef" class="tags-container" @scroll="handleScroll">
<router-link v-for="tag in tagsViewStore.visitedViews" :key="tag.path" :data-path="tag.path"
:class="isActive(tag) ? 'active' : ''" :to="{ path: tag.path, query: tag.query }" class="tags-item"
:style="{ fontSize: appStore.size === 'default' ? '14px' : '16px' }"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" @contextmenu.prevent="openMenu(tag, $event)">
{{ generateTitle(tag.name) }}
<span v-if="!isAffix(tag)" class="tags-item-remove" @click.prevent.stop="closeSelectedTag(tag)" >
<svg-icon icon-class="close" />
</span>
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="tags-item-menu">
<li @click="refreshSelectedTag(selectedTag)">
<svg-icon icon-class="refresh" />
刷新
</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<svg-icon icon-class="close" />
关闭
</li>
<!-- <li @click="closeOtherTags">
<svg-icon icon-class="close_other" />
关闭其它
</li> -->
<li v-if="!isFirstView()" @click="closeLeftTags">
<svg-icon icon-class="close_left" />
关闭左侧
</li>
<li v-if="!isLastView()" @click="closeRightTags">
<svg-icon icon-class="close_right" />
关闭右侧
</li>
<li @click="closeAllTags(selectedTag)">
<svg-icon icon-class="close_all" />
关闭所有
</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
.tags-container {
.tags-item {
display: inline-block;
cursor: pointer;
border: 1px solid #d8dce5;
background: #ffffff;
padding: 4px 10px;
margin: 0 0 0 5px;
border-radius: 2px;
box-shadow: 0px 0px 10px rgb(219 225 236);
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&:hover {
color: var(--el-color-primary);
}
&.active {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
&-remove {
width: 12px;
height: 12px;
line-height: 10px;
// text-align: center;
display: inline-block;
border-radius: 50%;
&:hover {
color: #fff;
background-color: #ee7b7b;
}
}
}
:deep(.el-scrollbar__view) {
height: 100%;
display: flex;
display: -webkit-flex;
align-items: center;
-webkit-align-items: center;
}
}
.tags-item-menu {
background: #fff;
z-index: 99;
position: absolute;
border-radius: 4px;
font-size: 12px;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li {
padding: 8px 15px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
</style>

View File

@ -1,4 +1,2 @@
export { default as Navbar } from './Navbar.vue'; export { default as Navbar } from './Navbar.vue';
export { default as AppMain } from './AppMain.vue'; export { default as AppMain } from './AppMain.vue';
export { default as Settings } from './Settings/index.vue';
export { default as TagsView } from './TagsView/index.vue';

View File

@ -1,257 +0,0 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus';
import { getMessageList, setMessageStatus, setAllMessageStatus, deleteMessageById } from '@/api/message/index'
import Page from '@/components/Pagination/page.vue'
const emit = defineEmits(['update:dialogVisible'])
const props = defineProps({
dialogVisible: {
type: Boolean
},
});
const active = ref('0')
const loading = ref(false)
const tableData = ref([])
const tableDataSel = ref([])
const isLookOver = ref(false)
const info = ref({
title: '',
createtime: '',
content: ''
})
const queryParams = ref({
current: 1,
size: 10,
title: '',
type: '',
status: active.value,
startDate: '',
endDate: ''
});
const createtime = ref('')
const total = ref()
const handleClose = () => {
emit(
'update:dialogVisible',
false
);
}
function handleSelectionChange(val: any) {
tableDataSel.value = val;
}
function reset() {
queryParams.value.title = ''
queryParams.value.type = ''
queryParams.value.startDate = ''
queryParams.value.endDate = ''
createtime.value = ''
init()
}
function init() {
loading.value = true
queryParams.value.status = active.value
if(createtime.value) {
queryParams.value.startDate = createtime.value[0]
queryParams.value.endDate = createtime.value[1]
}
if(Number(active.value) == 0) {
queryParams.value.title = ''
queryParams.value.type = ''
createtime.value = ''
}
getMessageList(queryParams.value).then((result) => {
loading.value = false
tableData.value = result.data.records
total.value = result.data.total
queryParams.value.size = result.data.size;
queryParams.value.current = result.data.current
}).catch(() => {
loading.value = false
});
}
function open(row: any) {
const id = [row.id]
setMessageStatus(id.join(',')).then(() => {
init()
})
isLookOver.value = true
info.value = JSON.parse(JSON.stringify(row))
}
function del(row:any) {
ElMessageBox.confirm(
'确定删除此消息吗',
'删除提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then( async () => {
const id = [] as any[]
if(row.id == null) {
tableDataSel.value.forEach((element:any) => {
id.push(element.id)
});
} else {
id.push(row.id)
}
deleteMessageById(id.join(',')).then(() => {
ElMessage({
message: "删除成功",
type: "success",
});
init()
})
})
}
function tabClick() {
ElMessageBox.confirm(
'确定将全部消息标记为已阅吗?',
'提示信息',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then( async () => {
setAllMessageStatus().then(() => {
ElMessage({
message: "已读成功",
type: "success",
});
init()
})
})
}
defineExpose({
init
});
onMounted(() => {
})
</script>
<template>
<el-dialog class="dialog" draggable v-model="props.dialogVisible" width="1100" top="3vh" append-to-body
:before-close="handleClose" :show-close="!isLookOver">
<el-tabs v-show="!isLookOver" v-model="active" @tab-change="init" type="border-card">
<el-tab-pane label="最新消息">
</el-tab-pane>
<el-tab-pane label="历史消息">
<div class="flex justify-between mb-4">
<div class="flex items-center ">
<el-input v-model="queryParams.title" placeholder="请输入消息标题" clearable style="width: 200px;margin-right:10px" @keyup.enter="init" />
<el-select v-model="queryParams.type" clearable default-first-option placeholder="消息类型" style="width: 200px;margin-right:10px" @change="init">
<el-option label="定时任务" value="1" />
<el-option label="工作流触发" value="2" />
<el-option label="人工任务" value="3" />
</el-select>
<el-date-picker
v-model="createtime"
type="daterange"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD"
@change="init"
/>
<el-button type="primary" style="margin-left: 10px;" @click="init">搜索</el-button>
<el-button @click="reset">重置</el-button>
</div>
<el-button :disabled="tableDataSel.length==0" type="primary" @click="del">
<span>删除</span>
</el-button>
</div>
</el-tab-pane>
<el-table v-loading="loading" :data="tableData" style="width: 100%; margin-bottom: 20px;overflow:hidden;" height="400px"
row-key="id" border @selection-change="handleSelectionChange" default-expand-all
:header-cell-style="{ background: 'rgb(250 250 250)', height: '50px' }">
<el-table-column v-if="active == '1'" type="selection" align="center" width="50" />
<el-table-column type="index" label="序号" align="center" width="70" />
<el-table-column prop="title" label="消息标题">
<template #default="scope">
<span class="title" @click="open(scope.row)">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column prop="type" label="消息类型" width="100">
<template #default="scope">
{{ scope.row.type == '1' ? '定时任务' : (scope.row.type == '2' ? '工作流触发' : '人工触发') }}
</template>
</el-table-column>
<el-table-column prop="senderName" label="发送者" width="120"></el-table-column>
<el-table-column prop="createtime" label="通知时间" width="180"></el-table-column>
<el-table-column label="状态" width="180">
<template #default="scope">
<span :style="{color:scope.row.status == '2'?'rgb(55, 196, 122)':scope.row.status == '9'?'rgb(255, 153, 0)':''}">{{ scope.row.status != '1'?'●':''}} {{ scope.row.status == '1' ? '初始创建' : (scope.row.status == '2' ? '消息已阅' : '消息过期') }}</span>
</template>
</el-table-column>
<el-table-column v-if="active == '1'" fixed="right" align="center" label="操作" width="80">
<template #default="scope">
<img src="@/assets/MenuIcon/lbcz_sc.png" alt="" @click="del(scope.row)"
title="删除" style="cursor: pointer;margin-left:10px">
</template>
</el-table-column>
</el-table>
<div class="flex justify-between items-center mb-4">
<Page :total="total" v-model:size="queryParams.size" v-model:current="queryParams.current" @pagination="init()" ></Page>
<el-button v-if="active == '0'" type="primary" @click="tabClick">
<span>全部已读</span>
</el-button>
</div>
</el-tabs>
<div v-show="isLookOver" class="p-6">
<div class="flex justify-between">
<span class="font-black" style="color: #000;font-size: 20px;">{{ info.title }}</span>
<el-button type="primary" plain @click="isLookOver = false">
返回
</el-button>
</div>
<div style="height:50px;line-height: 50px;border-bottom: 1px solid #e4e4e4;">
<span style="color: rgb(148, 148, 148);font-size:14px;">{{ info.createtime }}</span>
</div>
<div style="color:rgb(80, 80, 80);font-size: 14px;padding-top:15px;overflow:auto">
{{ info.content }}
</div>
</div>
</el-dialog>
</template>
<style lang="scss">
.title{
color:rgb(64, 158, 255);
cursor: pointer;
&:hover{
text-decoration: underline;
}
}
.dialog {
height: 500px;
img {
display: inline-block;
}
.el-tabs__content,
.el-tabs--border-card {
border: none !important;
}
.el-tabs__item {
height: 50px !important;
line-height: 50px !important;
}
.el-dialog__headerbtn {
z-index: 100;
top: 0px;
}
.el-dialog__header {
padding: 0 !important;
}
.el-dialog__body {
padding: 0 !important;
}
}
</style>

View File

@ -1,134 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, watchEffect } from 'vue'; import { computed } from "vue";
import { useWindowSize } from '@vueuse/core'; import { AppMain, Navbar } from "./components/index";
import { AppMain, Navbar, Settings, TagsView } from './components/index';
import Sidebar from './components/Sidebar/index.vue';
import RightPanel from '@/components/RightPanel/index.vue';
import Hamburger from '@/components/Hamburger/index.vue';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from "@/store/modules/app";
import { useSettingsStore } from '@/store/modules/settings';
const { width } = useWindowSize();
/**
* 响应式布局容器固定宽度
*
* 大屏>=1200px
* 中屏>=992px
* 小屏>=768px
*/
const WIDTH = 992;
const appStore = useAppStore(); const appStore = useAppStore();
const settingsStore = useSettingsStore();
const fixedHeader = computed(() => settingsStore.fixedHeader);
const showTagsView = computed(() => settingsStore.tagsView);
const showSettings = computed(() => settingsStore.showSettings);
const classObj = computed(() => ({ const classObj = computed(() => ({
hideSidebar: !appStore.sidebar.opened,
openSidebar: appStore.sidebar.opened,
withoutAnimation: appStore.sidebar.withoutAnimation, withoutAnimation: appStore.sidebar.withoutAnimation,
mobile: appStore.device === 'mobile'
})); }));
watchEffect(() => {
if (width.value < WIDTH) {
appStore.toggleDevice('mobile');
appStore.closeSideBar(true);
} else {
appStore.toggleDevice('desktop');
if (width.value >= 1200) {
//
appStore.openSideBar(true);
} else {
appStore.closeSideBar(true);
}
}
});
function handleOutsideClick() {
appStore.closeSideBar(false);
}
function toggleSideBar() {
appStore.toggleSidebar(true);
}
</script> </script>
<template> <template>
<div :class="classObj" class="app-wrapper"> <div :class="classObj" class="app-wrapper">
<!-- 手机设备 && 侧边栏 显示遮罩层 --> <navbar />
<div <div class="main-container">
class="drawer-bg"
v-if="classObj.mobile && classObj.openSidebar"
@click="handleOutsideClick"
></div>
<Sidebar class="sidebar-container" />
<navbar :class="{ 'fixed-header': fixedHeader }" />
<div v-show="fixedHeader" style="height:60px"></div>
<hamburger
:is-active="appStore.sidebar.opened"
@toggleClick="toggleSideBar"
/>
<div :class="{ hasTagsView: showTagsView }" class="main-container">
<tags-view v-if="showTagsView" />
<!--主页面--> <!--主页面-->
<app-main /> <app-main />
<!-- 设置面板 -->
<RightPanel v-if="showSettings">
<settings />
</RightPanel>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@use '@/styles/mixin.scss' as mixin; @use '@/styles/mixin.scss' as mixin;
@use '@/styles/variables.module.scss' as variables;
.app-wrapper { .app-wrapper {
// @include clearfix; // @include clearfix;
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
} }
.drawer-bg { </style>
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 55;
}
.fixed-header {
position: fixed;
top: 0;
left: 0;
z-index: 98;
width: 100%;
transition: width 0.28s;
background: #ffffff;
}
.hideSidebar .fixed-header {
// width: calc(100% - 54px);
width:100%;
}
.mobile .fixed-header {
width: 100%;
}
</style>

View File

@ -8,6 +8,8 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import Pagination from '@/components/Pagination/index.vue'; import Pagination from '@/components/Pagination/index.vue';
import '@/permission'; import '@/permission';
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css' // Ant Design 全局样式重置
// 引入svg注册脚本 // 引入svg注册脚本
import 'virtual:svg-icons-register'; import 'virtual:svg-icons-register';
@ -39,6 +41,7 @@ app
.component('Pagination', Pagination) .component('Pagination', Pagination)
.use(router) .use(router)
.use(ElementPlus) .use(ElementPlus)
.use(Antd)
.use(WujieVue) .use(WujieVue)
.use(i18n) .use(i18n)
.mount('#app'); .mount('#app');

View File

@ -5,20 +5,35 @@ import { usePermissionStoreHook } from '@/store/modules/permission';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
NProgress.configure({ showSpinner: false }); // 进度条 NProgress.configure({ showSpinner: false });
const permissionStore = usePermissionStoreHook(); const permissionStore = usePermissionStoreHook();
// 白名单路由 // 白名单路由
const whiteList = ['/login']; const whiteList = ['/login'];
if (import.meta.env.DEV) { // 查找第一个可用路由
whiteList.push('/process/antd-demo'); function findFirstAvailableRoute(routes: RouteRecordRaw[]): string | undefined {
for (const route of routes) {
if (route.meta?.hidden) continue;
if (route.children?.length > 0) {
const child = route.children[0];
// 优先使用 opturl 或 path
const targetPath = child.opturl || child.path;
return targetPath?.startsWith('/') ? targetPath : `/${targetPath}`;
}
const targetPath = route.opturl || route.path;
return targetPath;
}
return '/404';
} }
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
NProgress.start(); NProgress.start();
const userStore = useUserStoreHook(); const userStore = useUserStoreHook();
if (userStore.Token) { if (userStore.Token) {
// 登录成功,跳转到首页 // 登录成功,跳转到首页
if (to.path === '/login') { if (to.path === '/login') {
@ -26,9 +41,19 @@ router.beforeEach(async (to, from, next) => {
NProgress.done(); NProgress.done();
} else { } else {
const hasGetUserInfo = userStore.roles.length > 0; const hasGetUserInfo = userStore.roles.length > 0;
// const hasGetUserInfo = true;
if (hasGetUserInfo) { if (hasGetUserInfo) {
// 已获取用户信息,检查路由匹配
if (to.matched.length === 0) { if (to.matched.length === 0) {
// 路由未匹配,可能是访问根路径
if (to.path === '/') {
const firstRoute = findFirstAvailableRoute(permissionStore.routes);
if (firstRoute) {
next(firstRoute);
NProgress.done();
return;
}
}
from.name ? next({ name: from.name as any }) : next('/401'); from.name ? next({ name: from.name as any }) : next('/401');
} else { } else {
next(); next();
@ -36,15 +61,25 @@ router.beforeEach(async (to, from, next) => {
} else { } else {
try { try {
const { roles } = await userStore.getInfo(); const { roles } = await userStore.getInfo();
const accessRoutes: RouteRecordRaw[] = const accessRoutes: RouteRecordRaw[] = await permissionStore.generateRoutes(roles);
await permissionStore.generateRoutes(roles);
accessRoutes.forEach((route: any) => { accessRoutes.forEach((route: any) => {
router.addRoute(route); router.addRoute(route);
}); });
// 关键:如果是根路径,加载完路由后跳转到第一个可用路由
if (to.path === '/') {
const firstRoute = findFirstAvailableRoute(accessRoutes);
if (firstRoute) {
next(firstRoute);
NProgress.done();
return;
}
}
next({ ...to, replace: true }); next({ ...to, replace: true });
} catch (error) { } catch (error) {
// 移除 token 并跳转登录页 console.log(error);
await userStore.resetToken(); await userStore.resetToken();
next(`/login?redirect=${to.path}`); next(`/login?redirect=${to.path}`);
NProgress.done(); NProgress.done();
@ -64,4 +99,4 @@ router.beforeEach(async (to, from, next) => {
router.afterEach(() => { router.afterEach(() => {
NProgress.done(); NProgress.done();
}); });

View File

@ -26,48 +26,11 @@ export const constantRoutes: RouteRecordRaw[] = [
component: () => import('@/views/error-page/404.vue'), component: () => import('@/views/error-page/404.vue'),
meta: { hidden: true } meta: { hidden: true }
}, },
{
{ path: '/401',
path: '/', component: () => import('@/views/error-page/401.vue'),
component: Layout, meta: { hidden: true }
redirect: '/dashboard', },
children: [
{
path: 'dashboard',
opturl: '/dashboard',
component: () => import('@/views/dashboard/index.vue'),
name: '首页',
icon: 'homepage',
meta: { title: 'dashboard', icon: 'homepage', affix: true }
},
{
path: 'personalCenter',
component: () => import('@/views/system/user/personalCenter.vue'),
name: '个人中心',
meta: { title: 'personalCenter',hidden: true, icon: 'personalCenter' }
},
{
path: '401',
component: () => import('@/views/error-page/401.vue'),
meta: { hidden: true }
},
]
},
{
path: '/process',
opturl: '/process',
component: Layout,
redirect: '/process/antd-demo',
children: [
{
path: 'antd-demo',
opturl: '/process/antd-demo',
component: () => import('@/views/process/antd-demo.vue'),
name: 'AntD Demo',
meta: { title: 'AntD Demo' }
}
]
}
]; ];
// 创建路由 // 创建路由

View File

@ -10,7 +10,7 @@ interface DefaultSettings {
} }
const defaultSettings: DefaultSettings = { const defaultSettings: DefaultSettings = {
title: '公司开发平台框架', title: '水电水利建设项目全过程环境管理信息平台',
showSettings: false, showSettings: false,
tagsView: true, tagsView: true,
fixedHeader: true, fixedHeader: true,

View File

@ -1,5 +1,4 @@
import { import {
getSidebarStatus,
setSidebarStatus, setSidebarStatus,
getSize, getSize,
setSize, setSize,
@ -24,7 +23,12 @@ export enum SizeType {
large, large,
small small
} }
export const usetTheme = {
token: {
colorPrimary: '#1890ff',
borderRadius: 2,
},
};
// setup // setup
export const useAppStore = defineStore('app', () => { export const useAppStore = defineStore('app', () => {
// state // state
@ -32,7 +36,6 @@ export const useAppStore = defineStore('app', () => {
const size = ref(getSize() || 'default'); const size = ref(getSize() || 'default');
const language = ref(getLanguage()); const language = ref(getLanguage());
const sidebar = reactive({ const sidebar = reactive({
opened: getSidebarStatus() !== 'closed',
withoutAnimation: false withoutAnimation: false
}); });
@ -44,32 +47,6 @@ export const useAppStore = defineStore('app', () => {
} }
}); });
// actions
function toggleSidebar(withoutAnimation: boolean) {
sidebar.opened = !sidebar.opened;
sidebar.withoutAnimation = withoutAnimation;
if (sidebar.opened) {
setSidebarStatus('opened');
} else {
setSidebarStatus('closed');
}
}
function closeSideBar(withoutAnimation: boolean) {
sidebar.opened = false;
sidebar.withoutAnimation = withoutAnimation;
setSidebarStatus('closed');
}
function openSideBar(withoutAnimation: boolean) {
sidebar.opened = true;
sidebar.withoutAnimation = withoutAnimation;
setSidebarStatus('opened');
}
function toggleDevice(val: string) {
device.value = val;
}
function changeSize(val: string) { function changeSize(val: string) {
size.value = val; size.value = val;
@ -87,11 +64,7 @@ export const useAppStore = defineStore('app', () => {
language, language,
locale, locale,
size, size,
toggleDevice,
changeSize, changeSize,
changeLanguage, changeLanguage,
toggleSidebar,
closeSideBar,
openSideBar
}; };
}); });

View File

@ -8,24 +8,18 @@ import { ref } from 'vue';
const modules = import.meta.glob('../../views/**/**.vue'); const modules = import.meta.glob('../../views/**/**.vue');
export const Layout = () => import('@/layout/index.vue'); export const Layout = () => import('@/layout/index.vue');
// const hasPermission = (roles: string[], route: RouteRecordRaw) => {
// if (route.meta && route.meta.roles) {
// if (roles.includes('ROOT')) {
// return true;
// }
// return roles.some(role => {
// if (route.meta?.roles !== undefined) {
// return (route.meta.roles as string[]).includes(role);
// }
// });
// }
// return false;
// };
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => { const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
const res: RouteRecordRaw[] = []; const res: RouteRecordRaw[] = [];
routes.forEach(route => { routes.forEach(route => {
const tmp = { ...route } as any; const tmp = { ...route } as any;
// ✅ 保存原始名称到 meta用于菜单显示
tmp.meta = {
...tmp.meta,
title: tmp.name || tmp.menuName, // 原始名称用于显示
};
// ✅ name 使用路径生成唯一值
tmp.name = tmp.path || tmp.opturl;
// if (hasPermission(roles, tmp)) { // if (hasPermission(roles, tmp)) {
tmp.path = tmp.opturl; tmp.path = tmp.opturl;
if (tmp.type == '0') { if (tmp.type == '0') {

View File

@ -1,64 +0,0 @@
import { defineStore } from 'pinia';
import defaultSettings from '../../settings';
import { ref } from 'vue';
import { useStorage } from '@vueuse/core';
/**
*
*/
export enum ThemeType {
light,
dark
}
/**
*
*/
export enum LayoutType {
left,
top,
mix
}
export const useSettingsStore = defineStore('setting', () => {
// state
const showSettings = ref<boolean>(defaultSettings.showSettings);
const tagsView = useStorage<boolean>('tagsView', defaultSettings.tagsView);
const fixedHeader = ref<boolean>(defaultSettings.fixedHeader);
const sidebarLogo = ref<boolean>(defaultSettings.sidebarLogo);
const layout = useStorage<string>('layout', defaultSettings.layout);
// actions
function changeSetting(param: { key: string; value: any }) {
const { key, value } = param;
switch (key) {
case 'showSettings':
showSettings.value = value;
break;
case 'fixedHeader':
fixedHeader.value = value;
break;
case 'tagsView':
tagsView.value = value;
break;
case 'sidevarLogo':
sidebarLogo.value = value;
break;
case 'layout':
layout.value = value;
break;
default:
break;
}
}
return {
showSettings,
tagsView,
fixedHeader,
sidebarLogo,
layout,
changeSetting
};
});

View File

@ -4,56 +4,4 @@
--el-color-primary-dark: #0d84ff; --el-color-primary-dark: #0d84ff;
// --el-font-size-base: 16px !important; // --el-font-size-base: 16px !important;
} }
.el-button--large, .el-input--large, .el-table--large, .el-form--large, .el-select__tags-text{
font-size: 16px !important;
.el-form-item__label{
font-size: 16px !important;
}
--el-font-size-base: 16px !important;
}
// 覆盖 element-plus 的样式
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
}
.el-upload {
input[type='file'] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
// dropdown
.el-dropdown-menu {
a {
display: block;
}
}
// to fix el-date-picker css style
.el-range-separator {
box-sizing: content-box;
}
// 选中行背景色值
.el-table__body tr.current-row td {
background-color: #e1f3d8b5 !important;
}
// card 的header统一高度
.el-card__header {
height: 60px !important;
}
// 表格表头和表体未对齐
.el-table__header col[name='gutter'] {
display: table-cell !important;
}

View File

@ -1,26 +1,34 @@
@use 'src/styles/variables.module' as variables;
@use 'src/styles/element-plus' as element-plus; @use 'src/styles/element-plus' as element-plus;
@use './sidebar' as sidebar; @use './sidebar' as sidebar;
@use './tailwind' as tailwind; @use './tailwind' as tailwind;
html,body,#app{ html,
body,
#app {
height: 100%; height: 100%;
font-family: auto;
} }
/* Webkit 浏览器 (Chrome, Safari, Edge) */
body::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
// main-container global css // main-container global css
.app-container { .app-container {
padding: 20px; padding: 20px;
} }
.search{ .search {
padding: 18px 0 0 10px; padding: 18px 0 0 10px;
margin-bottom: 10px; margin-bottom: 10px;
border-radius: 5px; border-radius: 5px;
border: 1px solid #ddd; border: 1px solid #ddd;
box-shadow: 6px 2px 6px #CCC; box-shadow: 6px 2px 6px #ccc;
} }
svg{ svg {
display: inline-block; display: inline-block;
} }
.el-dialog { .el-dialog {
@ -28,12 +36,12 @@ svg{
flex-direction: column !important; flex-direction: column !important;
margin: auto !important; margin: auto !important;
position: absolute !important; position: absolute !important;
top: 0 ; top: 0;
left: 0 ; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
height: max-content; height: max-content;
} }
.el-dialog__body { .el-dialog__body {
padding: 20px !important; padding: 20px !important;
} }

View File

@ -1,5 +1,3 @@
@use 'src/styles/variables.module' as variables;
svg { svg {
vertical-align: text-bottom !important; vertical-align: text-bottom !important;
} }
@ -7,249 +5,13 @@ svg {
.main-container { .main-container {
// min-height: 100%; // min-height: 100%;
transition: margin-left 0.28s; transition: margin-left 0.28s;
margin-left: variables.$sideBarWidth;
position: relative; position: relative;
} }
.sidebar-container {
transition: width 0.28s;
width: variables.$sideBarWidth !important;
background-color: variables.$menuBg;
height: calc(100vh - 60px);
padding-top: 15px;
position: absolute;
top: 60px;
bottom: 0;
left: 0;
z-index: 98;
overflow: hidden;
// reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out,
0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
}
.el-scrollbar__bar.is-vertical {
right: 0px;
}
.el-scrollbar {
height: 100%;
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 50px);
}
}
.is-horizontal {
display: none;
}
.svg-icon {
margin-right: 16px;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
// menu hover
.submenu-title-noDropdown,
.el-sub-menu__title {
&:hover {
color: variables.$menuHover !important;
}
}
// .is-active > .el-sub-menu__title {
// color: variables.$subMenuActiveText !important;
// }
& .nest-menu .el-sub-menu > .el-sub-menu__title,
& .el-sub-menu .el-menu-item {
min-width: variables.$sideBarWidth !important;
background-color: variables.$subMenuBg !important;
&:hover {
color: variables.$subMenuHover !important;
}
}
.el-menu-item.is-active {
border-right: 3px solid variables.$subMenuHover;
background: #e8f3ff !important;
}
}
.hideSidebar {
.sidebar-container {
width: 54px !important;
.svg-icon {
margin-right: 0px;
}
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon {
margin-left: 19px;
}
}
}
.el-sub-menu {
overflow: hidden;
& > .el-sub-menu__title {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon {
margin-left: 19px;
}
.el-sub-menu__icon-arrow {
display: none;
}
}
}
.el-menu--collapse {
.el-sub-menu {
& > .el-sub-menu__title {
& > span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.el-menu--collapse .el-menu .el-sub-menu {
min-width: variables.$sideBarWidth !important;
}
// mobile responsive
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform 0.28s;
width: variables.$sideBarWidth !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(- variables.$sideBarWidth, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
} }
// when menu collapsed body[layout='mix'] {
.el-menu--vertical { .svg-icon {
& > .el-menu {
.svg-icon {
margin-right: 16px;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
}
.nest-menu .el-sub-menu > .el-sub-menu__title,
.el-menu-item {
&:hover {
background-color: #ffffff;
// you can use variables.$subMenuHover
color: variables.$menuHover !important;
}
}
// the scroll bar appears when the subMenu is too long
> .el-menu--popup {
max-height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
}
body[layout="mix"] {
.horizontal-header{
.el-menu-item{
height: 50px!important;
line-height: 50px!important;
}
.el-sub-menu__title {
background-color: #001529!important;
height: 50px!important;
}
}
.horizontal-header-right>div {
color: #FFF;
}
.svg-icon{
margin-right: 16px; margin-right: 16px;
} }
}
}

View File

@ -1,11 +0,0 @@
:export {
menuText: #409eff;
menuActiveText: #409eff;
subMenuActiveText: #409eff;
menuBg: #ffffff;
menuHover: #409eff;
subMenuBg: #ffffff;
subMenuHover: #409eff;
sideBarWidth: 210px;
}
/*# sourceMappingURL=variables.module.css.map */

View File

@ -1,9 +0,0 @@
{
"version": 3,
"mappings": "AAeA,AAAA,OAAO,CAAC;EACN,QAAQ,EAfC,OAAO;EAgBhB,cAAc,EAfC,OAAO;EAgBtB,iBAAiB,EAfC,OAAO;EAgBzB,MAAM,EAdC,OAAO;EAed,SAAS,EAdC,OAAO;EAejB,SAAS,EAbC,OAAO;EAcjB,YAAY,EAbC,OAAO;EAcpB,YAAY,EAZC,KAAK;CAanB",
"sources": [
"variables.module.scss"
],
"names": [],
"file": "variables.module.css"
}

View File

@ -1,25 +1,17 @@
// sidebar $main-menu-color: #fff;
$menuText: #505050; $main-menu-color-active: #00e6fc;
$menuActiveText: #409eff; $main-menu-bg-color-active: #003e6e;
$subMenuActiveText: #409eff;
$menuBg: #ffffff; $layout-header-height: 60px;
$menuHover: #409eff; $locationbar-height: 50px;
$subMenuBg: #ffffff;
$subMenuHover: #409eff;
$sideBarWidth: 210px;
// the :export directive is the magic sauce for webpack // the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export { :export {
menuText: $menuText; main-menu-color: $main-menu-color;
menuActiveText: $menuActiveText; main-menu-color-active: $main-menu-color-active;
subMenuActiveText: $subMenuActiveText; main-menu-bg-color-active: $main-menu-bg-color-active;
menuBg: $menuBg;
menuHover: $menuHover; layout-header-height: $layout-header-height;
subMenuBg: $subMenuBg; locationbar-height: $locationbar-height;
subMenuHover: $subMenuHover;
sideBarWidth: $sideBarWidth;
} }

View File

@ -21,12 +21,6 @@ export const localStorage = {
} }
}; };
// 侧边栏状态(显示/隐藏)
const SidebarStatusKey = 'sidebarStatus';
export function getSidebarStatus() {
return localStorage.get(SidebarStatusKey);
}
export function setSidebarStatus(sidebarStatus: string) { export function setSidebarStatus(sidebarStatus: string) {
localStorage.set(SidebarStatusKey, sidebarStatus); localStorage.set(SidebarStatusKey, sidebarStatus);
} }

View File

@ -1,5 +1,5 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus'; import { message, Modal } from 'ant-design-vue';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { useUserStoreHook } from '@/store/modules/user'; import { useUserStoreHook } from '@/store/modules/user';
@ -33,18 +33,14 @@ service.interceptors.request.use(
service.interceptors.response.use( service.interceptors.response.use(
(response: AxiosResponse) => { (response: AxiosResponse) => {
const { status, msg } = response; const { status, msg } = response;
console.log(msg)
console.log(response);
if (status === 200) { if (status === 200) {
if (response.data.code == 401) { if (response.data.code == 401) {
ElMessage({ message.error(response.data.msg||'请求失败');
message: '用户名或密码错误,请重试!',
type: 'error'
});
return; return;
}else if(response.data.code == 1){ }else if(response.data.code == 1){
ElMessage({ message.error(response.data.msg);
message: response.data.msg,
type: 'error'
});
return; return;
} }
return response.data; return response.data;
@ -53,10 +49,7 @@ service.interceptors.response.use(
if (response.data instanceof ArrayBuffer) { if (response.data instanceof ArrayBuffer) {
return response; return response;
} }
ElMessage({ message.error( msg || '系统出错');
message: msg || '系统出错',
type: 'error'
});
return Promise.reject(new Error(msg || 'Error')); return Promise.reject(new Error(msg || 'Error'));
} }
}, },
@ -65,18 +58,18 @@ service.interceptors.response.use(
const { status, msg } = error.response.data; const { status, msg } = error.response.data;
// token 过期,重新登录 // token 过期,重新登录
if (status === '403') { if (status === '403') {
ElMessageBox.confirm('当前页面已失效,请重新登录', '提示', { Modal.confirm({
confirmButtonText: 'OK', title: "提示",
type: 'warning' content: "当前页面已失效,请重新登录",
}).then(() => { okText: "确定",
localStorage.clear(); cancelButtonProps: { style: { display: "none" } },
window.location.href = '/'; onOk: () => {
localStorage.clear();
window.location.href = '/';
},
}); });
} else { } else {
ElMessage({ message.error(msg || '当前页面已失效');
message: msg || '当前页面已失效',
type: 'error'
});
} }
} }
return Promise.reject(error.message); return Promise.reject(error.message);

View File

@ -1,12 +0,0 @@
/**
* Created by PanJiaChen on 16/11/18.
*/
/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path: string) {
const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path);
return isExternal;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +0,0 @@
<script setup lang="ts">
import Editor from '@/components/WangEditor/index.vue';
import { ElForm } from 'element-plus';
import { reactive, ref, toRefs } from 'vue';
const dataFormRef = ref(ElForm);
const state = reactive({
formData: {
content: '初始内容'
}
});
const { formData } = toRefs(state);
</script>
<template>
<div class="app-container">
<el-form ref="dataFormRef" :model="formData">
<editor v-model="formData.content" style="height: 600px" />
</el-form>
</div>
</template>

View File

@ -1,33 +0,0 @@
<script setup lang="ts">
import SingleUpload from '@/components/Upload/SingleUpload.vue';
import MultiUpload from '@/components/Upload/MultiUpload.vue';
import { ElForm } from 'element-plus';
import { reactive, ref, toRefs } from 'vue';
const dataFormRef = ref(ElForm);
const state = reactive({
formData: {
picUrl:
'https://oss.youlai.tech/default/2022/11/20/18e206dae97b40329661537d1e433639.jpg',
picUrls: [
'https://oss.youlai.tech/default/2022/11/20/8af5567816094545b53e76b38ae9c974.webp',
'https://oss.youlai.tech/default/2022/11/20/13dbfd7feaf848c2acec2b21675eb9d3.webp'
]
}
});
const { formData } = toRefs(state);
</script>
<template>
<div class="app-container">
<el-form ref="dataFormRef" :model="formData">
<el-form-item label="单图上传">
<single-upload v-model="formData.picUrl"></single-upload>
</el-form-item>
<el-form-item label="多图上传">
<multi-upload v-model="formData.picUrls"></multi-upload>
</el-form-item>
</el-form>
</div>
</template>

View File

@ -1,174 +0,0 @@
<!-- 线 + 柱混合图 -->
<template>
<div :id="id" :class="className" :style="{ height, width }" />
</template>
<script setup lang="ts">
import {
nextTick,
onActivated,
onBeforeUnmount,
onDeactivated,
onMounted,
} from 'vue';
import { init, EChartsOption } from 'echarts';
import * as echarts from 'echarts';
import resize from '@/utils/resize';
const props = defineProps({
id: {
type: String,
default: 'barChart',
},
className: {
type: String,
default: '',
},
width: {
type: String,
default: '200px',
required: true,
},
height: {
type: String,
default: '200px',
required: true,
},
});
const { mounted, chart, beforeDestroy, activated, deactivated } = resize();
function initChart() {
const barChart = init(document.getElementById(props.id) as HTMLDivElement);
barChart.setOption({
title: {
show: true,
text: '业绩总览',
x: 'center',
padding: 15,
textStyle: {
fontSize: 18,
fontStyle: 'normal',
fontWeight: 'bold',
color: '#337ecc',
},
},
grid: {
left: '2%',
right: '2%',
bottom: '10%',
containLabel: true,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999',
},
},
},
legend: {
x: 'center',
y: 'bottom',
data: ['收入', '毛利润', '收入增长率', '利润增长率'],
},
xAxis: [
{
type: 'category',
data: [ '浙江', '北京', '上海', '广东','深圳'],
axisPointer: {
type: 'shadow',
},
},
],
yAxis: [
{
type: 'value',
min: 0,
max: 10000,
interval: 2000,
axisLabel: {
formatter: '{value} ',
},
},
{
type: 'value',
min: 0,
max: 100,
interval: 20,
axisLabel: {
formatter: '{value}%',
},
},
],
series: [
{
name: '收入',
type: 'bar',
data: [7000, 7100, 7200, 7300,7400],
barWidth: 20,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' },
]),
},
},
{
name: '毛利润',
type: 'bar',
data: [ 8000,8200, 8400, 8600, 8800],
barWidth: 20,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#25d73c' },
{ offset: 0.5, color: '#1bc23d' },
{ offset: 1, color: '#179e61' },
]),
},
},
{
name: '收入增长率',
type: 'line',
yAxisIndex: 1,
data: [ 60,65, 70, 75, 80],
itemStyle: {
color: '#67C23A',
},
},
{
name: '利润增长率',
type: 'line',
yAxisIndex: 1,
data: [ 70,75, 80, 85, 90],
itemStyle: {
color: '#409EFF',
},
},
],
} as EChartsOption);
chart.value = barChart;
}
onBeforeUnmount(() => {
beforeDestroy();
});
onActivated(() => {
activated();
});
onDeactivated(() => {
deactivated();
});
onMounted(() => {
mounted();
nextTick(() => {
initChart();
});
});
</script>

View File

@ -1,131 +0,0 @@
<!-- 漏斗图 -->
<template>
<div :id="id" :class="className" :style="{ height, width }" />
</template>
<script setup lang="ts">
import {
nextTick,
onActivated,
onBeforeUnmount,
onDeactivated,
onMounted
} from 'vue';
import { init, EChartsOption } from 'echarts';
import resize from '@/utils/resize';
const props = defineProps({
id: {
type: String,
default: 'funnelChart'
},
className: {
type: String,
default: ''
},
width: {
type: String,
default: '200px',
required: true
},
height: {
type: String,
default: '200px',
required: true
}
});
const { mounted, chart, beforeDestroy, activated, deactivated } = resize();
function initChart() {
const funnelChart = init(document.getElementById(props.id) as HTMLDivElement);
funnelChart.setOption({
title: {
show: true,
text: '订单线索转化漏斗图',
x: 'center',
padding: 15,
textStyle: {
fontSize: 18,
fontStyle: 'normal',
fontWeight: 'bold',
color: '#337ecc'
}
},
grid: {
left: '2%',
right: '2%',
bottom: '10%',
containLabel: true
},
legend: {
x: 'center',
y: 'bottom',
data: ['Show', 'Click', 'Visit', 'Inquiry', 'Order']
},
series: [
{
name: 'Funnel',
type: 'funnel',
left: '20%',
top: 60,
bottom: 60,
width: '60%',
sort: 'descending',
gap: 2,
label: {
show: true,
position: 'inside'
},
labelLine: {
length: 10,
lineStyle: {
width: 1,
type: 'solid'
}
},
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
emphasis: {
label: {
fontSize: 20
}
},
data: [
{ value: 60, name: 'Visit' },
{ value: 40, name: 'Inquiry' },
{ value: 20, name: 'Order' },
{ value: 80, name: 'Click' },
{ value: 100, name: 'Show' }
]
}
]
} as EChartsOption);
chart.value = funnelChart;
}
onBeforeUnmount(() => {
beforeDestroy();
});
onActivated(() => {
activated();
});
onDeactivated(() => {
deactivated();
});
onMounted(() => {
mounted();
nextTick(() => {
initChart();
});
});
</script>
<style lang="scss" scoped></style>

View File

@ -1,113 +0,0 @@
<!-- 饼图 -->
<template>
<div :id="id" :class="className" :style="{ height, width }" />
</template>
<script setup lang="ts">
import {
nextTick,
onActivated,
onBeforeUnmount,
onDeactivated,
onMounted
} from 'vue';
import { init, EChartsOption } from 'echarts';
import resize from '@/utils/resize';
const props = defineProps({
id: {
type: String,
default: 'pieChart'
},
className: {
type: String,
default: ''
},
width: {
type: String,
default: '200px',
required: true
},
height: {
type: String,
default: '200px',
required: true
}
});
const { mounted, chart, beforeDestroy, activated, deactivated } = resize();
function initChart() {
const pieChart = init(document.getElementById(props.id) as HTMLDivElement);
pieChart.setOption({
title: {
show: true,
text: '产品分类总览',
x: 'center',
padding: 15,
textStyle: {
fontSize: 18,
fontStyle: 'normal',
fontWeight: 'bold',
color: '#337ecc'
}
},
grid: {
left: '2%',
right: '2%',
bottom: '10%',
containLabel: true
},
legend: {
top: 'bottom'
},
series: [
{
name: 'Nightingale Chart',
type: 'pie',
radius: [50, 130],
center: ['50%', '50%'],
roseType: 'area',
itemStyle: {
borderRadius: 1,
color: function (params: any) {
//
const colorList = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C'];
return colorList[params.dataIndex];
}
},
data: [
{ value: 26, name: '家用电器' },
{ value: 27, name: '户外运动' },
{ value: 24, name: '汽车用品' },
{ value: 23, name: '手机数码' }
]
}
]
} as EChartsOption);
chart.value = pieChart;
}
onBeforeUnmount(() => {
beforeDestroy();
});
onActivated(() => {
activated();
});
onDeactivated(() => {
deactivated();
});
onMounted(() => {
mounted();
nextTick(() => {
initChart();
});
});
</script>
<style lang="scss" scoped></style>

View File

@ -1,132 +0,0 @@
<!-- 雷达图 -->
<template>
<div :id="id" :class="className" :style="{ height, width }" />
</template>
<script setup lang="ts">
import {
nextTick,
onActivated,
onBeforeUnmount,
onDeactivated,
onMounted
} from 'vue';
import { init, EChartsOption } from 'echarts';
import resize from '@/utils/resize';
const props = defineProps({
id: {
type: String,
default: 'radarChart'
},
className: {
type: String,
default: ''
},
width: {
type: String,
default: '200px',
required: true
},
height: {
type: String,
default: '200px',
required: true
}
});
const { mounted, chart, beforeDestroy, activated, deactivated } = resize();
function initChart() {
const radarChart = init(document.getElementById(props.id) as HTMLDivElement);
radarChart.setOption({
title: {
show: true,
text: '订单状态统计',
x: 'center',
padding: 15,
textStyle: {
fontSize: 18,
fontStyle: 'normal',
fontWeight: 'bold',
color: '#337ecc'
}
},
grid: {
left: '2%',
right: '2%',
bottom: '10%',
containLabel: true
},
legend: {
x: 'center',
y: 'bottom',
data: ['预定数量', '下单数量', '发货数量']
},
radar: {
// shape: 'circle',
radius: '60%',
indicator: [
{ name: '家用电器' },
{ name: '服装箱包' },
{ name: '运动户外' },
{ name: '手机数码' },
{ name: '汽车用品' },
{ name: '家具厨具' }
]
},
series: [
{
name: 'Budget vs spending',
type: 'radar',
itemStyle: {
borderRadius: 6,
color: function (params: any) {
//
const colorList = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C'];
return colorList[params.dataIndex];
}
},
data: [
{
value: [400, 400, 400, 400, 400, 400],
name: '预定数量'
},
{
value: [300, 300, 300, 300, 300, 300],
name: '下单数量'
},
{
value: [200, 200, 200, 200, 200, 200],
name: '发货数量'
}
]
}
]
} as EChartsOption);
chart.value = radarChart;
}
onBeforeUnmount(() => {
beforeDestroy();
});
onActivated(() => {
activated();
});
onDeactivated(() => {
deactivated();
});
onMounted(() => {
mounted();
nextTick(() => {
initChart();
});
});
</script>
<style lang="scss" scoped></style>

View File

@ -1,119 +0,0 @@
<template>
<div class="component-container">
<el-card class="project-card">
<template #header>
<span class="fw-b">有来项目简介</span>
</template>
<div class="project-card__main">
<!-- 项目简介 -->
<el-link target="_blank" type="primary" href="https://gitee.com/haoxr">
youlai-mall
</el-link>
是基于Spring Boot 2.7Spring Cloud 2021 & Alibaba
2021Vue3Element-Plusuni-app等主流技术栈构建的一整套全栈开源商城项目
涉及
<el-link
target="_blank"
type="primary"
href="https://gitee.com/youlaitech/youlai-mall"
>后端微服务</el-link
>
<el-link
target="_blank"
type="success"
href="https://gitee.com/youlaitech/youlai-mall-admin"
>前端管理</el-link
>
<el-link
target="_blank"
type="warning"
href="https://gitee.com/youlaitech/youlai-mall-weapp"
>微信小程序
</el-link>
<el-link
target="_blank"
type="danger"
href="https://gitee.com/youlaitech/youlai-mall-weapp"
>APP应用</el-link
>
等多端的开发
<el-divider />
<!-- 源码地址 -->
<el-row :gutter="10">
<el-col :span="6">
<el-badge value="免费开源" class="fw-b"> 项目地址 </el-badge>
</el-col>
<el-col :span="6">
<el-link
target="_blank"
type="warning"
href="https://www.youlai.tech/"
>官方文档</el-link
>
</el-col>
<el-col :span="6">
<el-link
target="_blank"
type="primary"
href="https://github.com/youlaitech"
>Github</el-link
>
</el-col>
<el-col :span="6">
<el-link
target="_blank"
type="success"
href="https://gitee.com/youlaiorg"
>码云</el-link
>
</el-col>
</el-row>
<el-divider />
<!-- 技术栈 -->
<el-row :gutter="10">
<el-col :span="6" class="fw-b"> 后端技术栈 </el-col>
<el-col :span="18">
Spring BootSpring Cloud & AlibabaSpring Security
OAuth2JWTElastic Stack K8s...
</el-col>
</el-row>
<el-divider />
<el-row :gutter="10">
<el-col :span="6" class="fw-b"> 前端技术栈 </el-col>
<el-col :span="18">
Vue3TypeScriptElement-Plusuni-appvue3-element-admin ...
</el-col>
</el-row>
</div>
</el-card>
</div>
</template>
<script lang="ts">
export default {
name: 'index'
};
</script>
<style lang="scss" scoped>
.component-container {
.project-card {
font-size: 14px;
&__main {
line-height: 28px;
height: 320px;
overflow-y: auto;
overflow-x: hidden;
}
}
.fw-b {
font-weight: bold;
}
}
</style>

View File

@ -1,240 +0,0 @@
<!-- 团队介绍 -->
<template>
<div class="component-container">
<el-card class="team-card">
<template #header>
<span class="fw-b">有来开源组织 & 技术团队</span>
</template>
<el-tabs v-model="teamActiveName">
<el-tab-pane label="开发者" name="1">
<div class="developer" ref="dev_wrapper">
<ul class="developer__container">
<li
class="developer__item"
v-for="(item, index) in developers"
:key="index"
>
<div class="developer__inner">
<el-image
class="developer__img"
:src="item.imgUrl"
:preview-src-list="[item.imgUrl]"
></el-image>
<div class="developer__info">
<span class="developer__nickname">{{ item.nickname }}</span>
<div class="developer__position">
<el-tag
v-for="(position, i) in item.positions"
:type="(colors[i % colors.length] as any)"
:class="i !== 0 ? 'f-ml' : ''"
size="small"
:key="i"
>{{ position }}</el-tag
>
</div>
<div class="developer__homepage">
<a :href="item.homepage" target="_blank">个人主页</a>
</div>
</div>
</div>
</li>
</ul>
<el-image class="developer__indicator" :src="indicatorImgUrl" />
</div>
</el-tab-pane>
<el-tab-pane label="交流群" name="2">
<div class="group">
<el-image
class="group-img"
src="https://www.youlai.tech/files/blog/youlaiqun.png"
:preview-src-list="[
'https://www.youlai.tech/files/blog/youlaiqun.png'
]"
/>
<div class="group-tip">
群二维码过期可添加开发者微信由其拉入群备注有来即可
</div>
</div>
</el-tab-pane>
<el-tab-pane label="加入我们" name="3">
<div class="join">
<p>1. 人品良好善于思考执行力强</p>
<p>2. 熟悉项目且至少给项目提交()一个PR</p>
<p>3. Git代码库活跃个人主页或博客完善者优先</p>
<p>4. 过分优秀者我们会主动联系您...</p>
<div class="join__desc">申请加入方式: 添加开发者微信申请即可</div>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, reactive, ref, toRefs, watchEffect } from 'vue';
import BScroll from 'better-scroll';
const state = reactive({
teamActiveName: '1',
developers: [
{
imgUrl: 'https://s2.loli.net/2022/04/06/yRx8uzj4emA5QVr.jpg',
nickname: '郝先瑞',
positions: ['后端', '前端', '文档'],
homepage: 'https://www.cnblogs.com/haoxianrui/'
},
{
imgUrl: 'https://s2.loli.net/2022/04/06/cQihGv9uPsTjXk1.jpg',
nickname: '张川',
positions: ['后端', '前端'],
homepage: 'https://blog.csdn.net/qq_41595149'
},
{
imgUrl: 'https://s2.loli.net/2022/04/07/2IiOYBHnRGKgCSd.jpg',
nickname: '张加林',
positions: ['DevOps'],
homepage: 'https://gitee.com/ximy'
}
],
colors: ['', 'success', 'warning', 'danger'],
indicatorImgUrl: new URL(
`../../../../assets/index/indicator.png`,
import.meta.url
).href
});
const { teamActiveName, developers, colors, indicatorImgUrl } = toRefs(state);
let bScroll = reactive({});
const dev_wrapper = ref<HTMLElement | any>(null);
onMounted(() => {
bScroll = new BScroll(dev_wrapper.value, {
mouseWheel: true, //
disableMouse: false, //
scrollX: true //X
});
});
watchEffect(() => {
nextTick(() => {
bScroll && (bScroll as any).refresh();
});
});
</script>
<style lang="scss" scoped>
.component-container {
.team-card {
font-size: 14px;
.el-tabs__content {
.el-tab-pane {
height: 265px;
}
}
.developer {
width: 100%;
overflow: hidden;
&__container {
display: inline-flex;
overflow: hidden;
justify-content: flex-start;
padding: 10px;
.developer__item {
&:not(:first-child) {
margin-left: 20px;
}
align-items: center;
list-style: none;
width: 180px;
min-width: 180px;
.developer__inner {
border: 1px solid #cccccc;
border-radius: 5px;
box-shadow: 6px 6px 6px #aaa;
padding: 8px;
text-align: center;
.developer__img {
height: 100px;
width: 100px;
}
.developer__info {
padding: 6px;
font-size: 14px;
.developer__position {
margin-top: 10px;
}
.developer__homepage {
margin-top: 16px;
a {
display: inline-block;
padding: 4px 10px;
color: #409eff;
border: 1px solid #409eff;
border-radius: 5px;
background: #ecf5ff;
&:hover {
background: #409eff;
color: #ffffff;
}
}
}
}
}
}
}
&__indicator {
position: absolute;
right: 0;
bottom: 0;
width: 120px;
height: 100px;
}
}
.join {
overflow: hidden;
p {
font-weight: bold;
}
&__desc {
margin-top: 20px;
color: #409eff;
font-weight: bold;
}
}
.group {
&-img {
height: 200px;
width: 200px;
}
}
}
.fw-b {
font-weight: bold;
}
.f-ml {
margin-left: 5px;
}
}
</style>

View File

@ -1,19 +0,0 @@
<template>
<div class="dashboard-container">
<github-corner class="github-corner" />
</div>
</template>
<script lang="ts">
export default { name: 'Dashboard' };
</script>
<script setup lang="ts">
//
import GithubCorner from '@/components/GithubCorner/index.vue';
</script>
<style lang="scss" scoped>
</style>

View File

@ -40,7 +40,7 @@ function back() {
<ul class="list-unstyled"> <ul class="list-unstyled">
<li>或者你可以去:</li> <li>或者你可以去:</li>
<li class="link-type"> <li class="link-type">
<router-link to="/dashboard"> 回首页 </router-link> <router-link to="/"> 回首页 </router-link>
</li> </li>
<li class="link-type"> <li class="link-type">
<a href="https://www.taobao.com/">随便看看</a> <a href="https://www.taobao.com/">随便看看</a>

View File

@ -0,0 +1,5 @@
<template>
<div>
<h1>环保设施情况</h1>
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div>
<h1>监测工作开展情况</h1>
</div>
</template>

View File

@ -0,0 +1,6 @@
<template>
<div>
<h1>水电开放平台</h1>
</div>
</template>

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
<template>
<div style="padding: 30px">
<el-alert :closable="false" title="菜单一级">
<router-view />
</el-alert>
</div>
</template>

View File

@ -1,7 +0,0 @@
<template>
<div style="padding: 30px">
<el-alert :closable="false" title="菜单二级" type="success">
<router-view />
</el-alert>
</div>
</template>

View File

@ -1,5 +0,0 @@
<template>
<div style="padding: 30px">
<el-alert :closable="false" title="菜单三级-1" type="error" />
</div>
</template>

View File

@ -1,5 +0,0 @@
<template>
<div style="padding: 30px">
<el-alert :closable="false" title="菜单三级-2" type="warning" />
</div>
</template>

View File

@ -1,10 +0,0 @@
<template>
<div style="height: 100%">
<WujieVue name="process" :url="entryUrl" :sync="true" />
</div>
</template>
<script setup lang="ts">
const entryUrl = import.meta.env.VITE_PROCESS_ENTRY || '/process/';
</script>

View File

@ -0,0 +1,5 @@
<template>
<div>
<h2>生态流量达标情况</h2>
</div>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div>
<h1>水质监测</h1>
</div>
</template>

View File

@ -1,5 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true,
"target": "esnext", "target": "esnext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "esnext", "module": "esnext",
@ -24,6 +26,6 @@
"./types" "./types"
] /* './node_modules/@types'. */ ] /* './node_modules/@types'. */
}, },
"include": ["src/**/*.ts", "src/**/*.vue", "types/**/*.d.ts"], "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["node_modules", "dist", "**/*.js"] "exclude": ["node_modules", "dist", "**/*.js"]
} }

View File

@ -27,7 +27,7 @@ export default ({ mode }: ConfigEnv): UserConfig => {
// 线上API地址 // 线上API地址
//target: 'http://192.168.1.20:8090/', //target: 'http://192.168.1.20:8090/',
// 本地API地址 // 本地API地址
target: 'http://localhost:8093', target: 'http://10.84.1.66:8093',
changeOrigin: true, changeOrigin: true,
rewrite: path => rewrite: path =>
path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '') path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
@ -41,7 +41,7 @@ export default ({ mode }: ConfigEnv): UserConfig => {
resolve: { resolve: {
// Vite路径别名配置 // Vite路径别名配置
alias: { alias: {
'@': path.resolve('./src') '@': path.resolve(__dirname, './src')
} }
}, },
css: { css: {