fix:联调登录接口
This commit is contained in:
16
components.d.ts
vendored
16
components.d.ts
vendored
@@ -12,12 +12,19 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CardItem: typeof import('./src/components/cardItem/index.vue')['default']
|
||||
CommonFilter: typeof import('./src/components/commonFilter/index.vue')['default']
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDatePick: typeof import('element-plus/es')['ElDatePick']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
@@ -32,11 +39,20 @@ declare module 'vue' {
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
OverflowTabs: typeof import('./src/components/overflowTabs/index.vue')['default']
|
||||
PageForm: typeof import('./src/components/pageForm/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
||||
10
config.js
10
config.js
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* 项目配置文件
|
||||
* VITE_PROJECT_PREFIX 项目前缀,默认是'/'
|
||||
* VITE_PUBLIC_PATH 当前项目的basePath默认是'/'
|
||||
* VITE_APP_BASE_API 请求接口地址域名
|
||||
* */
|
||||
export const VITE_PROJECT_PREFIX = '/';
|
||||
export const VITE_PUBLIC_PATH = './';
|
||||
export const VITE_APP_BASE_API = 'http://api.test.com';
|
||||
export const VITE_PUBLIC_PATH = '/';
|
||||
export const VITE_APP_BASE_API = 'https://mversion-dev.zzmjart.com/api' //'http://192.168.42.106';
|
||||
@@ -1,18 +1,17 @@
|
||||
import request from '@/request';
|
||||
|
||||
// 设置请求的参数信息
|
||||
export const getUserList = (params?: any) => {
|
||||
return request.get('/api/user/list', params);
|
||||
};
|
||||
|
||||
// 获取路由菜单数据
|
||||
export const getRouteMenus = () => {
|
||||
return request.get('/api/menus');
|
||||
return request.get('/auth/v1/backend/menu');
|
||||
};
|
||||
|
||||
// 登录接口
|
||||
export const login = (data: { username: string; password: string }) => {
|
||||
return request.post('/api/auth/login', data);
|
||||
return request.post('/auth/oauth2/token', data,{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 获取用户信息
|
||||
|
||||
BIN
src/assets/images/logo.png
Normal file
BIN
src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
70
src/components/commonFilter/index.vue
Normal file
70
src/components/commonFilter/index.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="mj-filter-group">
|
||||
<div class="mj-icon-container">
|
||||
<el-popover
|
||||
ref="filterPopover"
|
||||
trigger="click"
|
||||
popper-class="filter-popper"
|
||||
placement="bottom-end"
|
||||
:teleported="true"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="mj-icon-item" title="筛选">
|
||||
<el-icon><Filter /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="filter-container">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</el-popover>
|
||||
<div class="mj-icon-item" title="下载" @click="$emit('download')">
|
||||
<el-icon><Download /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Filter, Download } from "@element-plus/icons-vue";
|
||||
const filterPopover = ref(null);
|
||||
|
||||
// 定义事件:重置、应用、下载
|
||||
defineEmits(["download"]);
|
||||
|
||||
|
||||
defineExpose({
|
||||
close() {
|
||||
filterPopover.value?.hide()
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mj-icon-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.mj-icon-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 18px;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
220
src/components/overflowTabs/index.vue
Normal file
220
src/components/overflowTabs/index.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div class="tabs-outer-container" ref="containerRef" :style="{ height: height + 'px' }">
|
||||
<div class="tabs-wrapper">
|
||||
<div
|
||||
v-for="(item, index) in visibleItems"
|
||||
:key="item.id"
|
||||
class="tab-item"
|
||||
:class="{ active: modelValue === item.id }"
|
||||
@click="$emit('update:modelValue', item.id)"
|
||||
>
|
||||
<span class="tab-text">{{ item.label }}</span>
|
||||
</div>
|
||||
|
||||
<el-dropdown
|
||||
v-if="hiddenItems.length > 0"
|
||||
trigger="click"
|
||||
@command="handleCommand"
|
||||
class="more-dropdown"
|
||||
>
|
||||
<div class="tab-item more-trigger">
|
||||
<span>更多</span>
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item in hiddenItems"
|
||||
:key="item.id"
|
||||
:command="item.id"
|
||||
|
||||
>
|
||||
<span :class="{ 'is-active-item-overflow-tabs': modelValue === item.id }">{{ item.label }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<div class="active-bar" :style="activeBarStyle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed, nextTick, watch } from "vue";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: [String, Number],
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 32,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const containerRef = ref(null);
|
||||
const splitIndex = ref(props.items.length);
|
||||
let timer = null;
|
||||
|
||||
const activeBarStyle = ref({
|
||||
width: "0px",
|
||||
left: "0px",
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
const visibleItems = computed(() => props.items.slice(0, splitIndex.value));
|
||||
const hiddenItems = computed(() => props.items.slice(splitIndex.value));
|
||||
const isHiddenActive = computed(() => {
|
||||
return hiddenItems.value.some((item) => item.id === props.modelValue);
|
||||
});
|
||||
|
||||
const updateActiveBar = async () => {
|
||||
await nextTick();
|
||||
if (!containerRef.value) return;
|
||||
|
||||
// 1. 检查激活项是否在可见区域
|
||||
const activeIndex = visibleItems.value.findIndex((item) => item.id === props.modelValue);
|
||||
|
||||
if (activeIndex >= 0) {
|
||||
// 激活项在可见区域:计算位置并显示下划线
|
||||
const tabItems = containerRef.value.querySelectorAll(".tab-item:not(.more-trigger)");
|
||||
const activeElement = tabItems[activeIndex];
|
||||
|
||||
if (activeElement) {
|
||||
const rect = activeElement.getBoundingClientRect();
|
||||
const containerRect = containerRef.value.getBoundingClientRect();
|
||||
|
||||
activeBarStyle.value = {
|
||||
width: `${rect.width * 0.6}px`,
|
||||
left: `${rect.left - containerRect.left + rect.width * 0.2}px`,
|
||||
opacity: 1,
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果在隐藏区域或未找到,将下划线宽度设为 0
|
||||
activeBarStyle.value = {
|
||||
...activeBarStyle.value,
|
||||
width: "0px",
|
||||
opacity: 0,
|
||||
};
|
||||
};
|
||||
|
||||
const calculateLayout = () => {
|
||||
if (!containerRef.value) return;
|
||||
const containerWidth = Math.floor(containerRef.value.getBoundingClientRect().width);
|
||||
const itemsNodes = containerRef.value.querySelectorAll(".tab-item:not(.more-trigger)");
|
||||
const moreBtnWidth = 90;
|
||||
|
||||
let currentWidth = 0;
|
||||
let newSplitIndex = props.items.length;
|
||||
|
||||
for (let i = 0; i < itemsNodes.length; i++) {
|
||||
currentWidth += Math.ceil(itemsNodes[i].getBoundingClientRect().width) + 20;
|
||||
if (currentWidth + moreBtnWidth > containerWidth) {
|
||||
newSplitIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (splitIndex.value !== newSplitIndex) {
|
||||
splitIndex.value = newSplitIndex;
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedCalc = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
calculateLayout();
|
||||
updateActiveBar();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
let resizeObserver = null;
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
calculateLayout();
|
||||
updateActiveBar();
|
||||
resizeObserver = new ResizeObserver(() => debouncedCalc());
|
||||
if (containerRef.value) resizeObserver.observe(containerRef.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeObserver) resizeObserver.disconnect();
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
|
||||
const handleCommand = (id) => emit("update:modelValue", id);
|
||||
|
||||
watch(() => props.modelValue, () => updateActiveBar());
|
||||
watch(() => props.items, async () => {
|
||||
splitIndex.value = props.items.length;
|
||||
await nextTick();
|
||||
calculateLayout();
|
||||
updateActiveBar();
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tabs-outer-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.tabs-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&.active {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.more-trigger {
|
||||
margin-left: auto;
|
||||
border-left: 1px solid #f0f2f5;
|
||||
|
||||
// 选中更多中的数据时,仅文字和图标变蓝
|
||||
&.is-more-active {
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.active-bar {
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
background-color: #409eff;
|
||||
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.2s;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -26,10 +26,13 @@ const { title } = defineProps<{
|
||||
justify-content: space-between;
|
||||
:deep(.mj-panel-title){
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stage-breadcrumbs-content{
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&::before{
|
||||
content:'';
|
||||
display: inline-block;
|
||||
|
||||
@@ -46,7 +46,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "clue",
|
||||
path: "clue",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackBackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "线索管理",
|
||||
icon: "",
|
||||
@@ -58,7 +58,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "customer",
|
||||
path: "customer",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "客户管理",
|
||||
icon: "",
|
||||
@@ -69,7 +69,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "studio",
|
||||
path: "studio",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "游戏与工作室",
|
||||
icon: "",
|
||||
@@ -80,7 +80,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "businessmanage",
|
||||
path: "businessmanage",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "商机管理",
|
||||
icon: "",
|
||||
@@ -103,7 +103,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "income",
|
||||
path: "income",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "收入合同",
|
||||
icon: "",
|
||||
@@ -126,7 +126,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "requirement",
|
||||
path: "requirement",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "需求管理",
|
||||
icon: "",
|
||||
@@ -137,7 +137,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "projectmanage",
|
||||
path: "projectmanage",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "项目管理",
|
||||
icon: "",
|
||||
@@ -148,7 +148,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "task",
|
||||
path: "task",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "任务管理",
|
||||
icon: "",
|
||||
@@ -159,7 +159,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "work",
|
||||
path: "work",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "工时管理",
|
||||
icon: "",
|
||||
@@ -193,7 +193,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "resume",
|
||||
path: "resume",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "流程管理",
|
||||
icon: "",
|
||||
@@ -204,7 +204,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "push",
|
||||
path: "push",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "推送管理",
|
||||
icon: "",
|
||||
@@ -216,7 +216,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "job",
|
||||
path: "job",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "主场岗位",
|
||||
icon: "",
|
||||
@@ -240,7 +240,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
path: "flow",
|
||||
name: "flow",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "流程管理",
|
||||
icon: "",
|
||||
@@ -250,7 +250,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
},
|
||||
{
|
||||
name: "organization",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
path: "organization",
|
||||
meta: {
|
||||
title: "组织管理",
|
||||
@@ -262,7 +262,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "personnel",
|
||||
path: "personnel",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "人员管理",
|
||||
icon: "",
|
||||
@@ -273,7 +273,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "permission",
|
||||
path: "permission",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/organization/index.vue",
|
||||
meta: {
|
||||
title: "权限管理",
|
||||
icon: "",
|
||||
@@ -284,7 +284,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
||||
{
|
||||
name: "dict",
|
||||
path: "dict",
|
||||
component: "@/pages/StageManage/organization/index.vue",
|
||||
component: "@/pages/BackstageManage/dict/index.vue",
|
||||
meta: {
|
||||
title: "字典管理",
|
||||
icon: "",
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<el-aside :width="width">
|
||||
<div class="mj-aside-content">
|
||||
<!-- 顶部company公司标记 -->
|
||||
<div class="mj-aside-company"></div>
|
||||
<div class="mj-aside-logo">
|
||||
<img :src="companyLogo" class="mj-company-logo" />
|
||||
</div>
|
||||
<div class="mj-aside-menu">
|
||||
<div class="mj-aside-title" v-show="!isCollapse">
|
||||
{{ topTitle }}
|
||||
@@ -25,12 +27,15 @@
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header class="mj-header-content">
|
||||
<!-- 左侧的菜单展示 -->
|
||||
<mjMenus
|
||||
:menuList="topLevelMenuList"
|
||||
mode="horizontal"
|
||||
:active-menu="selectedTopMenu"
|
||||
@menu-select="handleTopMenuSelect"
|
||||
/>
|
||||
<!-- 右侧用户的内容 -->
|
||||
<rightMenuGroup />
|
||||
</el-header>
|
||||
<el-main>
|
||||
<!-- <card-item :list="[1,2,3,4,5,6]"/> -->
|
||||
@@ -44,6 +49,8 @@
|
||||
import mjMenus from "@/components/standardMenu/index.vue";
|
||||
import { useUserStore } from "@/store";
|
||||
import { DArrowLeft, DArrowRight } from "@element-plus/icons-vue";
|
||||
import rightMenuGroup from './rightMenuGroup.vue';
|
||||
import companyLogo from '@/assets/images/logo.png';
|
||||
defineOptions({
|
||||
name: "Layout",
|
||||
});
|
||||
@@ -212,10 +219,18 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--mj-border-color);
|
||||
|
||||
.mj-aside-company {
|
||||
.mj-aside-logo {
|
||||
height: var(--mj-menu-header-height);
|
||||
line-height: var(--mj-menu-header-height);
|
||||
border-bottom: 1px solid var(--mj-border-color);
|
||||
flex-shrink: 0;
|
||||
.mj-company-logo{
|
||||
width: 39px;
|
||||
height: 32px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.mj-aside-menu {
|
||||
@@ -245,6 +260,14 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.mj-header-content{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.mj-standard-menu{
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mj-header-content {
|
||||
--el-header-padding: 0;
|
||||
border-bottom: 1px solid var(--mj-border-color);
|
||||
|
||||
197
src/pages/Layout/rightMenuGroup.vue
Normal file
197
src/pages/Layout/rightMenuGroup.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div class="mj-right-menu-group">
|
||||
<div class="user-header-container">
|
||||
<div class="action-group">
|
||||
<div class="action-item">
|
||||
<el-icon :size="16"><Monitor /></el-icon>
|
||||
</div>
|
||||
<div class="action-item">
|
||||
<el-badge is-dot class="notice-badge">
|
||||
<el-icon :size="16"><Bell /></el-icon>
|
||||
</el-badge>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown placement="bottom" trigger="click" @command="handleCommand">
|
||||
<div class="user-info">
|
||||
<div class="text-meta">
|
||||
<span class="userinfo-username">{{ userInfo.username }}</span>
|
||||
<span class="userinfo-role">SUPER ADMIN</span>
|
||||
</div>
|
||||
<el-avatar :size="30" :src="userInfo.avatar" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">
|
||||
<div class="logout-section-content">
|
||||
<svg
|
||||
class="logout-section-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="logout-section-text">退出登录</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Monitor, Bell } from "@element-plus/icons-vue";
|
||||
import TokenManager from "@/utils/storage";
|
||||
import { useUserStore } from "@/store";
|
||||
defineOptions({ name: "RightMenuGroup" });
|
||||
const userStore = useUserStore();
|
||||
const tokenManager = TokenManager.getInstance();
|
||||
const router = useRouter();
|
||||
const handleCommand = (command: string) => {
|
||||
if (command === "logout") {
|
||||
// 退出登录
|
||||
tokenManager.clearStorage();
|
||||
router.replace({ name: "Login" });
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前的用户的数据信息
|
||||
const userInfo = computed(() => {
|
||||
return userStore.userInfo;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.mj-right-menu-group {
|
||||
$text-main: #303133;
|
||||
$text-secondary: #909399;
|
||||
$border-color: #e4e7ed;
|
||||
$bg-capsule: #f5f7fa;
|
||||
.user-header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
gap: 24px;
|
||||
font-family: "PingFang SC", "Helvetica Neue", Arial, sans-serif;
|
||||
|
||||
// 左侧胶囊形状容器
|
||||
.action-group {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: $bg-capsule;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 30px;
|
||||
padding: 6px 16px;
|
||||
box-sizing: border-box;
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: $text-secondary;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:not(:last-child) {
|
||||
&::after {
|
||||
content: "";
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background-color: #d3dce9;
|
||||
margin: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.notice-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// 调整 ElementPlus badge 小红点位置
|
||||
:deep(.el-badge__content.is-fixed.is-dot) {
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
background-color: #ff5722;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用户信息区域
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 2px 4px 2px 16px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 30px;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.3s ease, background-color 0.3s ease-in-out;
|
||||
.el-avatar {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
border-color: $border-color;
|
||||
.el-avatar {
|
||||
transform: scale(1.06);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.text-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end; // 文字向右对齐
|
||||
line-height: 1.2;
|
||||
|
||||
.userinfo-username {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
color: $text-main;
|
||||
}
|
||||
|
||||
.userinfo-role {
|
||||
font-size: 12px;
|
||||
color: #9b9ea3;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-avatar {
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout-section-content {
|
||||
width: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
.logout-section-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #8c8c8c;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.logout-section-text {
|
||||
font-size: 14px;
|
||||
color: #ff4d4f;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -52,9 +52,6 @@
|
||||
<template #label>
|
||||
<div class="password-label">
|
||||
<span>密码</span>
|
||||
<el-link type="primary" underline="never" class="forgot-pass"
|
||||
>忘记密码?</el-link
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<el-input
|
||||
@@ -72,7 +69,7 @@
|
||||
</el-checkbox>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" class="login-btn" @click="handleLogin">
|
||||
<el-button type="primary" class="login-btn" @click="handleLogin" :loading="loading">
|
||||
登录系统 <el-icon class="el-icon--right"><Right /></el-icon>
|
||||
</el-button>
|
||||
</el-form>
|
||||
@@ -90,42 +87,32 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { Message, Lock, Right } from "@element-plus/icons-vue";
|
||||
import { login } from "@/api";
|
||||
import { useUserStore } from "@/store";
|
||||
import useTokenRefresh from "@/hooks/useTokenRefresh";
|
||||
import TokenManager from '@/utils/storage';
|
||||
|
||||
defineOptions({ name: "Login" });
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const { setTokens } = useTokenRefresh(import.meta.env.VITE_APP_BASE_API || "");
|
||||
|
||||
const tokenManager = TokenManager.getInstance();
|
||||
const loginFormRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
|
||||
const loginForm = reactive({
|
||||
username: "admin",
|
||||
password: "123456",
|
||||
username: "user",
|
||||
password: "password"
|
||||
});
|
||||
|
||||
const loginRules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: "请输入用户名", trigger: "blur" },
|
||||
{
|
||||
min: 3,
|
||||
max: 20,
|
||||
message: "用户名长度在 3 到 20 个字符",
|
||||
trigger: "blur",
|
||||
},
|
||||
{ required: true, message: "请输入用户名", trigger: "blur" }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: "请输入密码", trigger: "blur" },
|
||||
{ min: 6, max: 20, message: "密码长度在 6 到 20 个字符", trigger: "blur" },
|
||||
{ required: true, message: "请输入密码", trigger: "blur" }
|
||||
],
|
||||
};
|
||||
|
||||
@@ -140,21 +127,24 @@ const handleLogin = async () => {
|
||||
const response = await login({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password,
|
||||
grant_type: 'password'
|
||||
});
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
const { accessToken, refreshToken, expiresIn, userInfo } =
|
||||
response.data;
|
||||
|
||||
if (response) {
|
||||
const { access_token, refresh_token, expires_in,username,userId,avatar } = response;
|
||||
const userInfo = {
|
||||
username,
|
||||
userId,
|
||||
avatar
|
||||
}
|
||||
// 保存 token
|
||||
setTokens({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresAt: Date.now() + (expiresIn || 7200) * 1000,
|
||||
});
|
||||
tokenManager.setToken('accessToken',access_token);
|
||||
tokenManager.setToken('refreshToken',refresh_token);
|
||||
tokenManager.setToken('expiresAt',expires_in);
|
||||
tokenManager.setToken('userInfo',JSON.stringify(userInfo));
|
||||
|
||||
// 保存用户信息
|
||||
userStore.setToken(accessToken);
|
||||
userStore.setToken(refresh_token);
|
||||
// TODO:获取用户信息
|
||||
userStore.setUserInfo(userInfo);
|
||||
|
||||
ElMessage.success("登录成功");
|
||||
@@ -162,8 +152,6 @@ const handleLogin = async () => {
|
||||
// 跳转到首页或之前访问的页面
|
||||
const redirect = (route.query.redirect as string) || "/";
|
||||
router.push(redirect);
|
||||
} else {
|
||||
ElMessage.error(response.msg || "登录失败");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Login error:", error);
|
||||
|
||||
121
src/pages/stage/dict/dictManage.vue
Normal file
121
src/pages/stage/dict/dictManage.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="dict-manage">
|
||||
<el-dialog
|
||||
class="standard-ui-dialog"
|
||||
:title
|
||||
:width="558"
|
||||
:model-value="dialogVisible"
|
||||
@close="onCancel"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-form
|
||||
ref="ruleFormRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="字典名称:" prop="name">
|
||||
<el-input placeholder="请输入字典名称" v-model="form.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="type">
|
||||
<template #label>
|
||||
<div class="el-form-item-owerner">
|
||||
<span>字典类型</span>
|
||||
<el-tooltip content="字典类型" placement="top-start">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
<span>:</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-input placeholder="请输入字典类型" v-model="form.type"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="管理人员:">
|
||||
<el-input
|
||||
placeholder="请选择管理人员"
|
||||
v-model="form.manager"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态:" prop="resource">
|
||||
<el-radio-group v-model="form.resource">
|
||||
<el-radio :value="1">显示</el-radio>
|
||||
<el-radio :value="0">隐藏</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注:">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:row="4"
|
||||
placeholder="请输入备注"
|
||||
v-model="form.remark"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="onCancel">取消</el-button>
|
||||
<el-button @click="onConfirm(ruleFormRef)" type="primary">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from "vue";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import { QuestionFilled } from "@element-plus/icons-vue";
|
||||
defineOptions({ name: "DictManage" });
|
||||
|
||||
const { dialogVisible = false,row={},title="字典管理" } = defineProps<{
|
||||
dialogVisible: boolean;
|
||||
row: any;
|
||||
title?:string
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:dialogVisible", value: boolean): void;
|
||||
(e: "close"): void;
|
||||
(e: "confirm"): void;
|
||||
}>();
|
||||
const ruleFormRef = ref<FormInstance>();
|
||||
|
||||
const form = reactive({
|
||||
name: "",
|
||||
type: "",
|
||||
manager: "",
|
||||
status: "",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: "请输入字典名称", trigger: "blur" }],
|
||||
type: [{ required: true, message: "请输入字典类型", trigger: "blur" }],
|
||||
resource: [{ required: true, message: "请选择状态", trigger: "change" }],
|
||||
});
|
||||
|
||||
// 确定
|
||||
const onConfirm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
emit("update:dialogVisible", false);
|
||||
emit("confirm",form);
|
||||
} else {
|
||||
console.log("error submit!", fields);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 取消
|
||||
const onCancel = () => {
|
||||
ruleFormRef.value && ruleFormRef.value.resetFields()
|
||||
emit("update:dialogVisible", false);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.el-form-item-owerner{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
</style>
|
||||
63
src/pages/stage/dict/index.vue
Normal file
63
src/pages/stage/dict/index.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="mj-dict">
|
||||
<stageBreadcrumbs title="组织管理">
|
||||
<template #content>
|
||||
<el-button :icon="Plus" type="primary" @click="addDict">新增字典</el-button>
|
||||
</template>
|
||||
<template #action>
|
||||
<div class="mj-dict-actions">
|
||||
<CommonFilter ref="filterPopover"></CommonFilter>
|
||||
<div class="search-dict-input">
|
||||
<el-input
|
||||
placeholder="搜索字典..."
|
||||
class="auto-expand-input"
|
||||
></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</stageBreadcrumbs>
|
||||
|
||||
<!-- Table内容 -->
|
||||
|
||||
|
||||
<!-- 新增-编辑字典弹窗 -->
|
||||
<dict-manage v-model:dialogVisible="visible" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import dictManage from "./DictManage.vue";
|
||||
defineOptions({ name: "Dictionary" });
|
||||
const visible = ref(false);
|
||||
|
||||
|
||||
const addDict = () => {
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.mj-dict {
|
||||
.stage-breadcrumbs {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
.mj-dict-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.search-dict-input {
|
||||
width: 160px;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
&:focus-within {
|
||||
width: 224px;
|
||||
}
|
||||
.auto-expand-input {
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
src/pages/stage/flow/index.vue
Normal file
13
src/pages/stage/flow/index.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {reactive,ref,onMounted} from "vue"
|
||||
|
||||
defineOptions({})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
169
src/pages/stage/origanization/AuditLogs.vue
Normal file
169
src/pages/stage/origanization/AuditLogs.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="audit-log-container">
|
||||
<div class="section-header">
|
||||
<el-icon class="history-icon"><Timer /></el-icon>
|
||||
<span class="title">变更日志</span>
|
||||
</div>
|
||||
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(item, index) in logData"
|
||||
:key="index"
|
||||
:hide-timestamp="true"
|
||||
>
|
||||
<template #dot>
|
||||
<div class="custom-node-dot"></div>
|
||||
</template>
|
||||
|
||||
<div class="log-card-wrapper">
|
||||
<div class="log-header">
|
||||
<div class="log-action">{{ item.action }}</div>
|
||||
<div class="log-time">{{ item.time }}</div>
|
||||
</div>
|
||||
<div class="log-content">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
<div class="log-operator">
|
||||
操作人:<span class="name">{{ item.operator }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Timer } from "@element-plus/icons-vue";
|
||||
|
||||
const logData = [
|
||||
{
|
||||
action: "新增部门",
|
||||
time: "2025-12-29 16:06:11",
|
||||
content: "在集团1下新增东京分公司",
|
||||
operator: "名匠",
|
||||
},
|
||||
{
|
||||
action: "修改配置",
|
||||
time: "2025-12-29 14:20:05",
|
||||
content: "更新了企业 CorpID",
|
||||
operator: "张三",
|
||||
},
|
||||
{
|
||||
action: "变更负责人",
|
||||
time: "2025-12-29 10:15:30",
|
||||
content: "广州分公司行政负责人变更为张荣平",
|
||||
operator: "李四",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 变量定义
|
||||
$bg-default: #fcfdfe;
|
||||
$bg-hover: #ffffff;
|
||||
$primary-color: #409eff;
|
||||
$border-color: #ebeef5;
|
||||
|
||||
.audit-log-container {
|
||||
margin: 0 2px;
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.history-icon {
|
||||
font-size: 18px;
|
||||
margin-right: 8px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
// 时间轴整体样式
|
||||
:deep(.el-timeline) {
|
||||
padding-left: 2px;
|
||||
|
||||
.el-timeline-item__tail {
|
||||
border-left: 2px solid #e4e7ed;
|
||||
left: 5px; // 配合圆点居中
|
||||
}
|
||||
|
||||
.el-timeline-item__wrapper {
|
||||
padding-left: 32px;
|
||||
top: -4px; // 让内容顶部与圆点对齐
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义圆环节点
|
||||
.custom-node-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #fff;
|
||||
border: 2px solid #409eff; // 变细边框
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
left: -1px;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// 右侧内容块样式
|
||||
.log-card-wrapper {
|
||||
background-color: $bg-default;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
transition: all 0.3s ease; // 平滑过渡
|
||||
cursor: default;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: $bg-hover;
|
||||
border-color: transparent; // 悬浮时隐藏边框更显高级
|
||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.08); // 柔和的阴影
|
||||
transform: translateY(-2px); // 微小的上移效果
|
||||
}
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.log-action {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.log-content {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.log-operator {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
|
||||
.name {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
224
src/pages/stage/origanization/OrganizationDetail.vue
Normal file
224
src/pages/stage/origanization/OrganizationDetail.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="organization-detail-container">
|
||||
<el-card class="header-card" shadow="never">
|
||||
<div class="header-content">
|
||||
<div class="left-section">
|
||||
<div class="icon-wrapper">
|
||||
<el-icon :size="32" color="#409eff"><OfficeBuilding /></el-icon>
|
||||
</div>
|
||||
<div class="info-text">
|
||||
<div class="title-row">
|
||||
<span class="main-title">集团1</span>
|
||||
<el-tag size="small" effect="plain" class="title-tag"
|
||||
>集团</el-tag
|
||||
>
|
||||
</div>
|
||||
<div class="sub-id">ID: 3</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-actions">
|
||||
<el-button plain :icon="EditPen">编辑配置</el-button>
|
||||
<el-button type="danger" plain :icon="CircleClose">禁用</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div class="content-grid">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-card class="info-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="mj-panel-title">基本信息</div>
|
||||
</template>
|
||||
|
||||
<div class="info-list">
|
||||
<div
|
||||
class="info-item"
|
||||
v-for="(base, baseIndex) in baseList"
|
||||
:key="baseIndex"
|
||||
>
|
||||
<div class="info-label">{{ base.name }}</div>
|
||||
<div class="info-value">
|
||||
<el-button link type="primary" v-if="base.slotName === 'link'">{{base.value}} ></el-button>
|
||||
<span v-else>{{ base.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card class="info-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="mj-panel-title">系统属性</div>
|
||||
</template>
|
||||
<!-- 系统属性列表 -->
|
||||
<div class="info-list">
|
||||
<div
|
||||
class="info-item"
|
||||
v-for="(item, index) in systemList"
|
||||
:key="index"
|
||||
>
|
||||
<div class="info-label">{{ item.name }}</div>
|
||||
<div class="info-value">
|
||||
<!-- 普通展示内容 -->
|
||||
<span v-if="!item.slotName">{{ item.value }}</span>
|
||||
<!-- slot 方式处理系统属性中的头像 -->
|
||||
<div class="_user-info" v-if="item.slotName === 'avatar'">
|
||||
<el-avatar :size="20" class="mini-avatar">名</el-avatar>
|
||||
<span>{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { OfficeBuilding, EditPen, CircleClose } from "@element-plus/icons-vue";
|
||||
|
||||
const systemList = [
|
||||
{
|
||||
name: "生效时间",
|
||||
value: "2024-01-01",
|
||||
},
|
||||
{
|
||||
name: "过期时间",
|
||||
value: "永久生效",
|
||||
},
|
||||
{
|
||||
name: "最后更新",
|
||||
value: "2025-12-29 16:06",
|
||||
},
|
||||
{
|
||||
name: "更新人",
|
||||
value: "名匠",
|
||||
slotName: "avatar",
|
||||
},
|
||||
];
|
||||
|
||||
const baseList = [
|
||||
{
|
||||
name: "部门/公司名称",
|
||||
value: "广州分公司",
|
||||
},
|
||||
{
|
||||
name: "上级单位",
|
||||
value: "集团1",
|
||||
},
|
||||
{
|
||||
name: "同步企微",
|
||||
value: "已开启",
|
||||
slotName: "link",
|
||||
},
|
||||
{
|
||||
name: "部门负责人",
|
||||
value: "赵康, 李思奇, 董峥",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.organization-detail-container {
|
||||
margin: 0 2px;
|
||||
.header-card {
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.left-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon-wrapper {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
box-shadow: 0 0 4px #e2e8f0;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.main-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-id {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
margin-bottom: 2px;
|
||||
.info-card {
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
|
||||
// 移除 card 默认 padding 以便底线撑满
|
||||
:deep(.el-card__body) {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #f8fafc; // 原型要求的色值
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none; // 最后一行去掉底线
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: #86909c; // 较淡的标签文字
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: #1d2129; // 较深的数值文字
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// 处理系统属性中的头像
|
||||
._user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.mini-avatar {
|
||||
background-color: #3c7eff;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,9 @@
|
||||
<!-- 顶部的tabs菜单 -->
|
||||
<div class="organization-tabs">
|
||||
<stageBreadcrumbs title="组织管理">
|
||||
<template #content> 内容占位 </template>
|
||||
<template #content>
|
||||
<OverflowTabs v-model="activeTab" :items="tabList" :height="60"/>
|
||||
</template>
|
||||
<template #action>
|
||||
<el-button type="primary" :icon="Plus" plain>新增集团</el-button>
|
||||
</template>
|
||||
@@ -39,26 +41,41 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mj-organization-card organization-info">
|
||||
<div class="mj-panel_header">
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane label="基础信息" name="first"></el-tab-pane>
|
||||
<el-tab-pane label="动态日志" name="second"></el-tab-pane>
|
||||
<el-tabs v-model="activeName" class="organization-info-tabs">
|
||||
<el-tab-pane label="基础信息" name="baseInfo">
|
||||
<OrganizationDetail />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="动态日志" name="auditLogs">
|
||||
<AuditLogs />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Plus, Search } from "@element-plus/icons-vue";
|
||||
import stageBreadcrumbs from "@/components/stageBreadcrumbs/index.vue";
|
||||
import OverflowTabs from "@/components/overflowTabs/index.vue";
|
||||
import AuditLogs from "./AuditLogs.vue";
|
||||
import OrganizationDetail from "./OrganizationDetail.vue";
|
||||
import { OfficeBuilding, EditPen, CircleClose } from "@element-plus/icons-vue";
|
||||
defineOptions({ name: "Organization" });
|
||||
|
||||
const addValue = ref("");
|
||||
const search = ref("");
|
||||
const activeName = ref("baseInfo");
|
||||
|
||||
const activeName = ref("first");
|
||||
|
||||
// 集团Tabs切换
|
||||
const activeTab = ref(1);
|
||||
const tabList = ref([
|
||||
{ id: 1, label: '集团1' },
|
||||
{ id: 2, label: '集团2' },
|
||||
{ id: 3, label: '集团3' },
|
||||
{ id: 4, label: '集团4' },
|
||||
{ id: 5, label: '集团5' },
|
||||
{ id: 6, label: '集团6' },
|
||||
]);
|
||||
interface Tree {
|
||||
label: string;
|
||||
children?: Tree[];
|
||||
@@ -134,6 +151,12 @@ const defaultProps = {
|
||||
<style lang="scss" scoped>
|
||||
@use "sass:math";
|
||||
.mj-organization {
|
||||
.organization-tabs{
|
||||
:deep(.stage-breadcrumbs){
|
||||
// border-bottom: none;
|
||||
padding:0;
|
||||
}
|
||||
}
|
||||
.mj-organization-card {
|
||||
border-radius: 16px;
|
||||
border: 1px solid #e2e8f099;
|
||||
@@ -141,13 +164,8 @@ const defaultProps = {
|
||||
margin-top: $mj-padding-standard;
|
||||
box-shadow: 0 0 6px #e9e8e8;
|
||||
}
|
||||
|
||||
.organization-tabs {
|
||||
}
|
||||
|
||||
.organization-content {
|
||||
display: flex;
|
||||
height: 400px;
|
||||
gap: 16px;
|
||||
.org-tree {
|
||||
flex: 0 0 280px;
|
||||
@@ -173,6 +191,11 @@ const defaultProps = {
|
||||
}
|
||||
.organization-info {
|
||||
flex: 1;
|
||||
padding: 0 22px 22px 22px;
|
||||
.organization-info-tabs {
|
||||
--el-tabs-header-height: 54px;
|
||||
--el-border-color-light: #f1f5f9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/pages/stage/permission/index.vue
Normal file
13
src/pages/stage/permission/index.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {reactive,ref,onMounted} from "vue"
|
||||
|
||||
defineOptions({})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
13
src/pages/stage/personnel/index.vue
Normal file
13
src/pages/stage/personnel/index.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {reactive,ref,onMounted} from "vue"
|
||||
|
||||
defineOptions({})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,15 +1,20 @@
|
||||
import axios from "axios";
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import { getMockData, shouldUseMock } from '@/mock' //mock数据信息
|
||||
|
||||
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API;
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { ElNotification } from "element-plus";
|
||||
import { VITE_APP_BASE_API } from "../../config.js";
|
||||
import TokenManager from "@/utils/storage";
|
||||
import { getMockData, shouldUseMock } from "@/mock"; //mock数据信息
|
||||
const tokenManager = TokenManager.getInstance();
|
||||
const baseUrl = "/api"; //TODO: 本地调试需要修改为/api
|
||||
|
||||
// 1. 锁和队列定义在类外部,确保全局唯一
|
||||
let isRefreshing = false;
|
||||
let requestsQueue: Array<(token: string) => void> = [];
|
||||
|
||||
// 登录接口 传递参数不一样
|
||||
const AUTH_OAUTH2_TOKEN_URL = "/auth/oauth2/token";
|
||||
const CLIENT_CREDENTIALS = "Basic " + window.btoa("mversion:secret");
|
||||
|
||||
class HttpRequest {
|
||||
private instance: AxiosInstance;
|
||||
|
||||
@@ -24,16 +29,24 @@ class HttpRequest {
|
||||
}
|
||||
|
||||
// --- Token 管理方法 ---
|
||||
private getAccessToken() { return localStorage.getItem("accessToken"); }
|
||||
private getRefreshToken() { return localStorage.getItem("refreshToken"); }
|
||||
private getAccessToken() {
|
||||
return tokenManager.getToken("accessToken");
|
||||
}
|
||||
private getRefreshToken() {
|
||||
return tokenManager.getToken("refreshToken");
|
||||
}
|
||||
private clearTokens() {
|
||||
localStorage.removeItem("accessToken");
|
||||
localStorage.removeItem("refreshToken");
|
||||
tokenManager.clearStorage();
|
||||
// 这里可以触发跳转登录逻辑,例如:router.push('/login')
|
||||
}
|
||||
private setTokens(data: any) {
|
||||
localStorage.setItem("accessToken", data.accessToken);
|
||||
localStorage.setItem("refreshToken", data.refreshToken);
|
||||
tokenManager.setToken("accessToken", data.accessToken);
|
||||
tokenManager.setToken("refreshToken", data.refreshToken);
|
||||
}
|
||||
|
||||
// 判断是否为认证接口
|
||||
private isAuthEndpoint(url: string): boolean {
|
||||
return url === AUTH_OAUTH2_TOKEN_URL;
|
||||
}
|
||||
|
||||
private setupInterceptors() {
|
||||
@@ -41,7 +54,11 @@ class HttpRequest {
|
||||
this.instance.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = this.getAccessToken();
|
||||
if (token && config.headers) {
|
||||
|
||||
// 如果login接口传递clientId参数
|
||||
if (this.isAuthEndpoint(config.url || "")) {
|
||||
config.headers.Authorization = CLIENT_CREDENTIALS;
|
||||
} else if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
@@ -54,12 +71,16 @@ class HttpRequest {
|
||||
async (response: AxiosResponse) => {
|
||||
const { data: res, config: originalRequest } = response;
|
||||
|
||||
console.log("响应拦截器",res,originalRequest)
|
||||
// TODO:如果是登录接口就不要走全局的拦截 而是直接返回当前的数据信息
|
||||
if (this.isAuthEndpoint(originalRequest.url || "")) {
|
||||
return res;
|
||||
}
|
||||
// 业务成功直接返回数据
|
||||
if (res.code === 0) return res;
|
||||
if (res.code === 200) return res.data;
|
||||
|
||||
// 重点:401 未授权处理
|
||||
// 401 未授权处理
|
||||
if (res.code === 401) {
|
||||
|
||||
// 如果已经在刷新中了,将请求挂起并加入队列
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve) => {
|
||||
@@ -80,23 +101,25 @@ class HttpRequest {
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用一个干净的 axios 实例去发刷新请求,避免拦截器死循环
|
||||
const refreshRes = await axios.post(`${this.instance.defaults.baseURL}/auth/refresh`, {
|
||||
refreshToken: rToken
|
||||
});
|
||||
// TODO:使用一个干净的 axios 实例去发刷新请求,避免拦截器死循环
|
||||
const refreshRes = await axios.post(
|
||||
`${this.instance.defaults.baseURL}/auth/refresh`,
|
||||
{
|
||||
refreshToken: rToken,
|
||||
}
|
||||
);
|
||||
|
||||
const newToken = refreshRes.data.data.accessToken;
|
||||
this.setTokens(refreshRes.data.data);
|
||||
|
||||
// 刷新成功:释放队列
|
||||
requestsQueue.forEach(callback => callback(newToken));
|
||||
requestsQueue.forEach((callback) => callback(newToken));
|
||||
requestsQueue = [];
|
||||
isRefreshing = false;
|
||||
|
||||
// 重试本次请求
|
||||
originalRequest.headers!.Authorization = `Bearer ${newToken}`;
|
||||
return this.instance(originalRequest);
|
||||
|
||||
} catch (error) {
|
||||
// 刷新失败:清理并彻底报错
|
||||
isRefreshing = false;
|
||||
@@ -107,7 +130,7 @@ class HttpRequest {
|
||||
}
|
||||
|
||||
// 其它业务错误
|
||||
ElNotification.error({ title: '提示', message: res.msg || '服务异常' });
|
||||
ElNotification.error({ title: "提示", message: res.msg || "服务异常" });
|
||||
return Promise.reject(res);
|
||||
},
|
||||
(error) => {
|
||||
@@ -121,63 +144,75 @@ class HttpRequest {
|
||||
}
|
||||
|
||||
public request<T = any>(config: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
// 检查是否应该使用 Mock 数据
|
||||
const requestUrl = config.url || '';
|
||||
|
||||
// 优先使用请求 URL(通常是相对路径,如 /api/menus)
|
||||
// 这样可以直接匹配 mockApiMap 中的 key
|
||||
if (shouldUseMock(requestUrl)) {
|
||||
const mockData = getMockData(requestUrl, config.params, config.data);
|
||||
if (mockData) {
|
||||
console.log(`[Mock] 使用 Mock 数据: ${requestUrl}`, mockData);
|
||||
// 模拟网络延迟
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(mockData as ApiResponse<T>);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO:检查是否应该使用 Mock 数据
|
||||
// const requestUrl = config.url || '';
|
||||
// if (shouldUseMock(requestUrl)) {
|
||||
// const mockData = getMockData(requestUrl, config.params, config.data);
|
||||
// if (mockData) {
|
||||
// console.log(`[Mock] 使用 Mock 数据: ${requestUrl}`, mockData);
|
||||
// // 模拟网络延迟
|
||||
// return new Promise((resolve) => {
|
||||
// setTimeout(() => {
|
||||
// resolve(mockData as ApiResponse<T>);
|
||||
// }, 100);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
return this.instance(config) as unknown as Promise<ApiResponse<T>>;
|
||||
}
|
||||
|
||||
// GET 请求
|
||||
public get<T = any>(url: string, params?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
public get<T = any>(
|
||||
url: string,
|
||||
params?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'get',
|
||||
method: "get",
|
||||
params,
|
||||
...config
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
// POST 请求
|
||||
public post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
public post<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'post',
|
||||
method: "post",
|
||||
data,
|
||||
...config
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
// PUT 请求
|
||||
public put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
public put<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'put',
|
||||
method: "put",
|
||||
data,
|
||||
...config
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE 请求
|
||||
public delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
public delete<T = any>(
|
||||
url: string,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'delete',
|
||||
...config
|
||||
method: "delete",
|
||||
...config,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { createWebHistory, createRouter, type RouteRecordRaw } from 'vue-router'
|
||||
import { useUserStore } from '@/store'
|
||||
import { getRouteMenus } from '@/api'
|
||||
import useTokenRefresh from '@/hooks/useTokenRefresh'
|
||||
import TokenManager from '@/utils/storage';
|
||||
|
||||
import Login from '@/pages/Login/index.vue';
|
||||
import HomeView from '@/pages/Layout/index.vue';
|
||||
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API || '';
|
||||
|
||||
const tokenManager = TokenManager.getInstance();
|
||||
// 基础路由(不需要权限验证)
|
||||
const constantRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
@@ -27,7 +26,7 @@ const asyncRoutes: RouteRecordRaw[] = [
|
||||
path: '/',
|
||||
name: 'Layout',
|
||||
component: HomeView,
|
||||
redirect: '/home',
|
||||
// redirect: '/home',
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
@@ -57,7 +56,7 @@ const loadComponent = (componentPath: string) => {
|
||||
fullPath = componentPath
|
||||
} else if (componentPath.includes('/')) {
|
||||
// 补全路径,确保以 @/pages 开头
|
||||
fullPath = componentPath.startsWith('pages/') ? `@/${componentPath}` : `@/pages/${componentPath}`
|
||||
fullPath = componentPath.startsWith('pages/') ? `@/${componentPath}` : `@/pages/${componentPath}/index.vue`
|
||||
} else {
|
||||
// 补全 index.vue
|
||||
fullPath = `@/pages/${componentPath}/index.vue`
|
||||
@@ -76,34 +75,35 @@ const loadComponent = (componentPath: string) => {
|
||||
}
|
||||
|
||||
// 将后端返回的路由数据转换为 Vue Router 路由
|
||||
const transformRoutes = (routes: any[]): RouteRecordRaw[] => {
|
||||
return routes.map((route) => {
|
||||
const component = route.component ? loadComponent(route.component) : undefined
|
||||
// 构建基础路由对象
|
||||
const transformRoutes = (routes: any[], parentCode: string = ''): RouteRecordRaw[] => {
|
||||
return routes.flatMap((route) => {
|
||||
const fullCode = parentCode ? `${parentCode}/${route.code}` : route.code;
|
||||
|
||||
// 如果当前路由有子路由,说明它是一个路由前缀,不需要组件
|
||||
if (route.children && route.children.length > 0) {
|
||||
// 将子路由的路径加上当前路由的前缀,然后递归处理
|
||||
return transformRoutes(route.children, fullCode);
|
||||
} else {
|
||||
// 叶子节点才需要组件和路由配置
|
||||
const component = fullCode ? loadComponent(fullCode) : undefined;
|
||||
|
||||
const routeRecord: any = {
|
||||
path: route.path,
|
||||
name: route.name || route.path,
|
||||
path: route.code,
|
||||
name: route.code,
|
||||
meta: {
|
||||
title: route.meta?.title || route.title || route.name,
|
||||
icon: route.meta?.icon || route.icon,
|
||||
requiresAuth: route.meta?.requiresAuth !== false, // 默认需要权限
|
||||
roles: route.meta?.roles || route.roles,
|
||||
title: route.name,
|
||||
icon: route.icon,
|
||||
...route.meta,
|
||||
},
|
||||
}
|
||||
|
||||
// 如果有组件,添加组件属性
|
||||
if (component) {
|
||||
routeRecord.component = component
|
||||
routeRecord.component = component;
|
||||
}
|
||||
|
||||
// 处理子路由
|
||||
if (route.children && route.children.length > 0) {
|
||||
routeRecord.children = transformRoutes(route.children)
|
||||
return routeRecord as RouteRecordRaw;
|
||||
}
|
||||
|
||||
return routeRecord as RouteRecordRaw
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// 添加动态路由
|
||||
@@ -116,15 +116,50 @@ const addDynamicRoutes = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 从后端获取路由菜单数据
|
||||
const response = await getRouteMenus()
|
||||
if (response.code === 0 && response.data) {
|
||||
// TODO:从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
|
||||
let response:any;
|
||||
if (userStore.isBackendUser) {
|
||||
const backendResponse = await getRouteMenus();
|
||||
response = [{
|
||||
code: 'stage',
|
||||
name: '后台管理',
|
||||
icon: '',
|
||||
children: backendResponse,
|
||||
}];
|
||||
}else{
|
||||
// response = await getUserMenus();
|
||||
response = [];
|
||||
}
|
||||
if (response) {
|
||||
const processRoutes = (routes: any[], prefix: string = ''): RouteRecordRaw[] => {
|
||||
return routes.flatMap(route => {
|
||||
const currentPath = prefix ? `${prefix}/${route.code}` : route.code;
|
||||
|
||||
if (route.children && route.children.length > 0) {
|
||||
// 如果有子路由,递归处理并添加当前路径作为前缀
|
||||
return processRoutes(route.children, currentPath);
|
||||
} else {
|
||||
// 叶子节点,创建路由记录
|
||||
const component = loadComponent(currentPath);
|
||||
return {
|
||||
path: route.code,
|
||||
name: route.code,
|
||||
component: component || HomeView, // 使用Layout的组件
|
||||
meta: {
|
||||
title: route.name,
|
||||
icon: route.icon,
|
||||
...route.meta,
|
||||
}
|
||||
} as RouteRecordRaw;
|
||||
}
|
||||
});
|
||||
};
|
||||
// 转换路由数据
|
||||
const dynamicRoutes = transformRoutes(Array.isArray(response.data) ? response.data : [response.data])
|
||||
// const dynamicRoutes = transformRoutes(Array.isArray(response) ? response : [response])
|
||||
const dynamicRoutes = processRoutes(Array.isArray(response) ? response : [response])
|
||||
// 将动态路由添加到 Layout 的 children 中
|
||||
const layoutRoute = router.getRoutes().find(route => route.name === 'Layout')
|
||||
|
||||
console.log('Layout route:', layoutRoute,dynamicRoutes)
|
||||
if (layoutRoute) {
|
||||
dynamicRoutes.forEach(route => {
|
||||
router.addRoute('Layout', route)
|
||||
@@ -135,15 +170,14 @@ const addDynamicRoutes = async () => {
|
||||
router.addRoute(route)
|
||||
})
|
||||
}
|
||||
|
||||
console.log('Layout route:', router.getRoutes())
|
||||
// 保存路由数据到 store
|
||||
userStore.setRoutes(response.data)
|
||||
userStore.setRoutes(response)
|
||||
|
||||
// 标记路由已加载
|
||||
userStore.isRoutesLoaded = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load routes:', error)
|
||||
// 如果获取路由失败,清除用户数据并跳转到登录页
|
||||
userStore.clearUserData()
|
||||
router.push('/login')
|
||||
@@ -153,10 +187,10 @@ const addDynamicRoutes = async () => {
|
||||
// 路由导航守卫
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const userStore = useUserStore()
|
||||
const { getAccessToken } = useTokenRefresh(baseUrl)
|
||||
const accessToken = tokenManager.getToken('accessToken');
|
||||
|
||||
// 获取 token
|
||||
const token = getAccessToken() || userStore.token
|
||||
const token = accessToken || userStore.token
|
||||
|
||||
// 如果已登录,更新 store 中的 token
|
||||
if (token) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from "pinia";
|
||||
import useTokenRefresh from "@/hooks/useTokenRefresh";
|
||||
import TokenManager from '@/utils/storage';
|
||||
const tokenManager = TokenManager.getInstance();
|
||||
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API || "";
|
||||
|
||||
interface UserInfo {
|
||||
name?: string;
|
||||
@@ -25,12 +25,14 @@ interface RouteMenu {
|
||||
|
||||
const useUserStore = defineStore("user", {
|
||||
state: () => {
|
||||
const { getAccessToken } = useTokenRefresh(baseUrl);
|
||||
const accessToken = tokenManager.getToken('accessToken');
|
||||
const userInfo = tokenManager.getToken('userInfo');
|
||||
return {
|
||||
token: getAccessToken() || "",
|
||||
userInfo: {} as UserInfo,
|
||||
token: accessToken || "",
|
||||
userInfo: userInfo ? JSON.parse(userInfo) : {},
|
||||
routes: [] as RouteMenu[],
|
||||
isRoutesLoaded: false, // 标记路由是否已加载
|
||||
isBackendUser:true, //标记是否是后台用户
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
@@ -54,8 +56,7 @@ const useUserStore = defineStore("user", {
|
||||
this.userInfo = {};
|
||||
this.routes = [];
|
||||
this.isRoutesLoaded = false;
|
||||
const { clearTokens } = useTokenRefresh(baseUrl);
|
||||
clearTokens();
|
||||
tokenManager.clearStorage();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,4 +19,19 @@ html,body{
|
||||
}
|
||||
|
||||
|
||||
.filter-popper.el-popover.el-popper{
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// 全局重新element相关样式
|
||||
.mj-input-form{
|
||||
.el-input{
|
||||
--el-border-radius-base:10px;
|
||||
--el-border-color:#E2E8F0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -23,11 +23,19 @@
|
||||
|
||||
// 标注弹窗样式
|
||||
.standard-ui-dialog{
|
||||
--el-dialog-padding-primary:0;
|
||||
&.el-dialog{
|
||||
--el-dialog-inset-padding-primary:16px;
|
||||
--el-dialog-padding-primary:0;
|
||||
--el-dialog-border-radius:16px;
|
||||
--el-dialog-bg-header-footer:#FBFCFD;
|
||||
--el-dialog-border-header-footer-color:#E5E7EB;
|
||||
padding: var(--el-dialog-padding-primary);
|
||||
}
|
||||
.el-dialog__header{
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
border-bottom: 1px solid var(--el-dialog-border-header-footer-color);
|
||||
background-color:var(--el-dialog-bg-header-footer);
|
||||
padding: var(--el-dialog-inset-padding-primary);
|
||||
border-radius: var(--el-dialog-border-radius) var(--el-dialog-border-radius) 0 0;
|
||||
}
|
||||
.el-dialog__headerbtn{
|
||||
height: 60px;
|
||||
@@ -37,12 +45,8 @@
|
||||
}
|
||||
.el-dialog__footer{
|
||||
padding: var(--el-dialog-inset-padding-primary);
|
||||
background-color: var(--el-dialog-bg-header-footer);
|
||||
border-top: 1px solid var(--el-dialog-border-header-footer-color);
|
||||
border-radius: 0 0 var(--el-dialog-border-radius) var(--el-dialog-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 全局重新element相关样式
|
||||
.el-input{
|
||||
--el-border-radius-base:10px;
|
||||
--el-border-color:#E2E8F0;
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
|
||||
|
||||
.mj-panel-title {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #1D293D;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #155DFC;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
border-radius: 3px;
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +21,15 @@
|
||||
padding: 0 24px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #F1F5F9;
|
||||
|
||||
.el-tabs {
|
||||
--el-tabs-header-height: 54px;
|
||||
--el-border-color-light: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义组件中overflow-tabs高亮样式
|
||||
.is-active-item-overflow-tabs {
|
||||
color: #409eff;
|
||||
font-weight: bold;
|
||||
}
|
||||
33
src/utils/storage.ts
Normal file
33
src/utils/storage.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
class TokenManager {
|
||||
private static instance: TokenManager | null = null;
|
||||
private storage: Storage;
|
||||
|
||||
private constructor(storageType: 'localStorage' | 'sessionStorage' = 'localStorage') {
|
||||
this.storage = storageType === 'localStorage' ? localStorage : sessionStorage;
|
||||
}
|
||||
|
||||
public static getInstance(storageType: 'localStorage' | 'sessionStorage' = 'localStorage'): TokenManager {
|
||||
if (!TokenManager.instance) {
|
||||
TokenManager.instance = new TokenManager(storageType);
|
||||
}
|
||||
return TokenManager.instance;
|
||||
}
|
||||
|
||||
setToken(key: string, token: string): void {
|
||||
this.storage.setItem(key, token);
|
||||
}
|
||||
|
||||
getToken(key: string): string | null {
|
||||
return this.storage.getItem(key);
|
||||
}
|
||||
|
||||
removeToken(key: string): void {
|
||||
this.storage.removeItem(key);
|
||||
}
|
||||
clearStorage(): void {
|
||||
this.storage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default TokenManager;
|
||||
@@ -44,7 +44,7 @@ export default defineConfig(({ mode }) => {
|
||||
"/api": {
|
||||
target: VITE_APP_BASE_API,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
rewrite: (path) => path.replace(/^\/api/, "")
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user