fix:联调登录接口
This commit is contained in:
16
components.d.ts
vendored
16
components.d.ts
vendored
@@ -12,12 +12,19 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
CardItem: typeof import('./src/components/cardItem/index.vue')['default']
|
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']
|
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']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
ElDatePick: typeof import('element-plus/es')['ElDatePick']
|
ElDatePick: typeof import('element-plus/es')['ElDatePick']
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
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']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
@@ -32,11 +39,20 @@ declare module 'vue' {
|
|||||||
ElMain: typeof import('element-plus/es')['ElMain']
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
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']
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
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']
|
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']
|
PageForm: typeof import('./src/components/pageForm/index.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
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_PROJECT_PREFIX = '/';
|
||||||
export const VITE_PUBLIC_PATH = './';
|
export const VITE_PUBLIC_PATH = '/';
|
||||||
export const VITE_APP_BASE_API = 'http://api.test.com';
|
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';
|
import request from '@/request';
|
||||||
|
|
||||||
// 设置请求的参数信息
|
|
||||||
export const getUserList = (params?: any) => {
|
|
||||||
return request.get('/api/user/list', params);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取路由菜单数据
|
// 获取路由菜单数据
|
||||||
export const getRouteMenus = () => {
|
export const getRouteMenus = () => {
|
||||||
return request.get('/api/menus');
|
return request.get('/auth/v1/backend/menu');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 登录接口
|
// 登录接口
|
||||||
export const login = (data: { username: string; password: string }) => {
|
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;
|
justify-content: space-between;
|
||||||
:deep(.mj-panel-title){
|
:deep(.mj-panel-title){
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-breadcrumbs-content{
|
.stage-breadcrumbs-content{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
&::before{
|
&::before{
|
||||||
content:'';
|
content:'';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "clue",
|
name: "clue",
|
||||||
path: "clue",
|
path: "clue",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackBackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "线索管理",
|
title: "线索管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -58,7 +58,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "customer",
|
name: "customer",
|
||||||
path: "customer",
|
path: "customer",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "客户管理",
|
title: "客户管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -69,7 +69,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "studio",
|
name: "studio",
|
||||||
path: "studio",
|
path: "studio",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "游戏与工作室",
|
title: "游戏与工作室",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -80,7 +80,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "businessmanage",
|
name: "businessmanage",
|
||||||
path: "businessmanage",
|
path: "businessmanage",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "商机管理",
|
title: "商机管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -103,7 +103,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "income",
|
name: "income",
|
||||||
path: "income",
|
path: "income",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "收入合同",
|
title: "收入合同",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -126,7 +126,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "requirement",
|
name: "requirement",
|
||||||
path: "requirement",
|
path: "requirement",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "需求管理",
|
title: "需求管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -137,7 +137,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "projectmanage",
|
name: "projectmanage",
|
||||||
path: "projectmanage",
|
path: "projectmanage",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "项目管理",
|
title: "项目管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -148,7 +148,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "task",
|
name: "task",
|
||||||
path: "task",
|
path: "task",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "任务管理",
|
title: "任务管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -159,7 +159,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "work",
|
name: "work",
|
||||||
path: "work",
|
path: "work",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "工时管理",
|
title: "工时管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -193,7 +193,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "resume",
|
name: "resume",
|
||||||
path: "resume",
|
path: "resume",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "流程管理",
|
title: "流程管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -204,7 +204,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "push",
|
name: "push",
|
||||||
path: "push",
|
path: "push",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "推送管理",
|
title: "推送管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -216,7 +216,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "job",
|
name: "job",
|
||||||
path: "job",
|
path: "job",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "主场岗位",
|
title: "主场岗位",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -240,7 +240,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
path: "flow",
|
path: "flow",
|
||||||
name: "flow",
|
name: "flow",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "流程管理",
|
title: "流程管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -250,7 +250,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "organization",
|
name: "organization",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
path: "organization",
|
path: "organization",
|
||||||
meta: {
|
meta: {
|
||||||
title: "组织管理",
|
title: "组织管理",
|
||||||
@@ -262,7 +262,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "personnel",
|
name: "personnel",
|
||||||
path: "personnel",
|
path: "personnel",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "人员管理",
|
title: "人员管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -273,7 +273,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "permission",
|
name: "permission",
|
||||||
path: "permission",
|
path: "permission",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/organization/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "权限管理",
|
title: "权限管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
@@ -284,7 +284,7 @@ export const mockMenuData: MockMenuRoute[] = [
|
|||||||
{
|
{
|
||||||
name: "dict",
|
name: "dict",
|
||||||
path: "dict",
|
path: "dict",
|
||||||
component: "@/pages/StageManage/organization/index.vue",
|
component: "@/pages/BackstageManage/dict/index.vue",
|
||||||
meta: {
|
meta: {
|
||||||
title: "字典管理",
|
title: "字典管理",
|
||||||
icon: "",
|
icon: "",
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
<el-aside :width="width">
|
<el-aside :width="width">
|
||||||
<div class="mj-aside-content">
|
<div class="mj-aside-content">
|
||||||
<!-- 顶部company公司标记 -->
|
<!-- 顶部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-menu">
|
||||||
<div class="mj-aside-title" v-show="!isCollapse">
|
<div class="mj-aside-title" v-show="!isCollapse">
|
||||||
{{ topTitle }}
|
{{ topTitle }}
|
||||||
@@ -25,12 +27,15 @@
|
|||||||
</el-aside>
|
</el-aside>
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header class="mj-header-content">
|
<el-header class="mj-header-content">
|
||||||
|
<!-- 左侧的菜单展示 -->
|
||||||
<mjMenus
|
<mjMenus
|
||||||
:menuList="topLevelMenuList"
|
:menuList="topLevelMenuList"
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
:active-menu="selectedTopMenu"
|
:active-menu="selectedTopMenu"
|
||||||
@menu-select="handleTopMenuSelect"
|
@menu-select="handleTopMenuSelect"
|
||||||
/>
|
/>
|
||||||
|
<!-- 右侧用户的内容 -->
|
||||||
|
<rightMenuGroup />
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main>
|
<el-main>
|
||||||
<!-- <card-item :list="[1,2,3,4,5,6]"/> -->
|
<!-- <card-item :list="[1,2,3,4,5,6]"/> -->
|
||||||
@@ -44,6 +49,8 @@
|
|||||||
import mjMenus from "@/components/standardMenu/index.vue";
|
import mjMenus from "@/components/standardMenu/index.vue";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
import { DArrowLeft, DArrowRight } from "@element-plus/icons-vue";
|
import { DArrowLeft, DArrowRight } from "@element-plus/icons-vue";
|
||||||
|
import rightMenuGroup from './rightMenuGroup.vue';
|
||||||
|
import companyLogo from '@/assets/images/logo.png';
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "Layout",
|
name: "Layout",
|
||||||
});
|
});
|
||||||
@@ -212,10 +219,18 @@ onUnmounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-right: 1px solid var(--mj-border-color);
|
border-right: 1px solid var(--mj-border-color);
|
||||||
|
|
||||||
.mj-aside-company {
|
.mj-aside-logo {
|
||||||
height: var(--mj-menu-header-height);
|
height: var(--mj-menu-header-height);
|
||||||
|
line-height: var(--mj-menu-header-height);
|
||||||
border-bottom: 1px solid var(--mj-border-color);
|
border-bottom: 1px solid var(--mj-border-color);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
.mj-company-logo{
|
||||||
|
width: 39px;
|
||||||
|
height: 32px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mj-aside-menu {
|
.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 {
|
.mj-header-content {
|
||||||
--el-header-padding: 0;
|
--el-header-padding: 0;
|
||||||
border-bottom: 1px solid var(--mj-border-color);
|
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>
|
<template #label>
|
||||||
<div class="password-label">
|
<div class="password-label">
|
||||||
<span>密码</span>
|
<span>密码</span>
|
||||||
<el-link type="primary" underline="never" class="forgot-pass"
|
|
||||||
>忘记密码?</el-link
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-input
|
<el-input
|
||||||
@@ -72,7 +69,7 @@
|
|||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</div>
|
</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-icon class="el-icon--right"><Right /></el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -90,42 +87,32 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||||
import { Message, Lock, Right } from "@element-plus/icons-vue";
|
import { Message, Lock, Right } from "@element-plus/icons-vue";
|
||||||
import { login } from "@/api";
|
import { login } from "@/api";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
import useTokenRefresh from "@/hooks/useTokenRefresh";
|
import TokenManager from '@/utils/storage';
|
||||||
|
|
||||||
defineOptions({ name: "Login" });
|
defineOptions({ name: "Login" });
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { setTokens } = useTokenRefresh(import.meta.env.VITE_APP_BASE_API || "");
|
const tokenManager = TokenManager.getInstance();
|
||||||
|
|
||||||
const loginFormRef = ref<FormInstance>();
|
const loginFormRef = ref<FormInstance>();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const loginForm = reactive({
|
const loginForm = reactive({
|
||||||
username: "admin",
|
username: "user",
|
||||||
password: "123456",
|
password: "password"
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginRules: FormRules = {
|
const loginRules: FormRules = {
|
||||||
username: [
|
username: [
|
||||||
{ required: true, message: "请输入用户名", trigger: "blur" },
|
{ required: true, message: "请输入用户名", trigger: "blur" }
|
||||||
{
|
|
||||||
min: 3,
|
|
||||||
max: 20,
|
|
||||||
message: "用户名长度在 3 到 20 个字符",
|
|
||||||
trigger: "blur",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, message: "请输入密码", trigger: "blur" },
|
{ required: true, message: "请输入密码", trigger: "blur" }
|
||||||
{ min: 6, max: 20, message: "密码长度在 6 到 20 个字符", trigger: "blur" },
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,21 +127,24 @@ const handleLogin = async () => {
|
|||||||
const response = await login({
|
const response = await login({
|
||||||
username: loginForm.username,
|
username: loginForm.username,
|
||||||
password: loginForm.password,
|
password: loginForm.password,
|
||||||
|
grant_type: 'password'
|
||||||
});
|
});
|
||||||
|
if (response) {
|
||||||
if (response.code === 0 && response.data) {
|
const { access_token, refresh_token, expires_in,username,userId,avatar } = response;
|
||||||
const { accessToken, refreshToken, expiresIn, userInfo } =
|
const userInfo = {
|
||||||
response.data;
|
username,
|
||||||
|
userId,
|
||||||
|
avatar
|
||||||
|
}
|
||||||
// 保存 token
|
// 保存 token
|
||||||
setTokens({
|
tokenManager.setToken('accessToken',access_token);
|
||||||
accessToken,
|
tokenManager.setToken('refreshToken',refresh_token);
|
||||||
refreshToken,
|
tokenManager.setToken('expiresAt',expires_in);
|
||||||
expiresAt: Date.now() + (expiresIn || 7200) * 1000,
|
tokenManager.setToken('userInfo',JSON.stringify(userInfo));
|
||||||
});
|
|
||||||
|
|
||||||
// 保存用户信息
|
// 保存用户信息
|
||||||
userStore.setToken(accessToken);
|
userStore.setToken(refresh_token);
|
||||||
|
// TODO:获取用户信息
|
||||||
userStore.setUserInfo(userInfo);
|
userStore.setUserInfo(userInfo);
|
||||||
|
|
||||||
ElMessage.success("登录成功");
|
ElMessage.success("登录成功");
|
||||||
@@ -162,8 +152,6 @@ const handleLogin = async () => {
|
|||||||
// 跳转到首页或之前访问的页面
|
// 跳转到首页或之前访问的页面
|
||||||
const redirect = (route.query.redirect as string) || "/";
|
const redirect = (route.query.redirect as string) || "/";
|
||||||
router.push(redirect);
|
router.push(redirect);
|
||||||
} else {
|
|
||||||
ElMessage.error(response.msg || "登录失败");
|
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Login error:", error);
|
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菜单 -->
|
<!-- 顶部的tabs菜单 -->
|
||||||
<div class="organization-tabs">
|
<div class="organization-tabs">
|
||||||
<stageBreadcrumbs title="组织管理">
|
<stageBreadcrumbs title="组织管理">
|
||||||
<template #content> 内容占位 </template>
|
<template #content>
|
||||||
|
<OverflowTabs v-model="activeTab" :items="tabList" :height="60"/>
|
||||||
|
</template>
|
||||||
<template #action>
|
<template #action>
|
||||||
<el-button type="primary" :icon="Plus" plain>新增集团</el-button>
|
<el-button type="primary" :icon="Plus" plain>新增集团</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -39,12 +41,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mj-organization-card organization-info">
|
<div class="mj-organization-card organization-info">
|
||||||
<div class="mj-panel_header">
|
<el-tabs v-model="activeName" class="organization-info-tabs">
|
||||||
<el-tabs v-model="activeName">
|
<el-tab-pane label="基础信息" name="baseInfo">
|
||||||
<el-tab-pane label="基础信息" name="first"></el-tab-pane>
|
<OrganizationDetail />
|
||||||
<el-tab-pane label="动态日志" name="second"></el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
<el-tab-pane label="动态日志" name="auditLogs">
|
||||||
</div>
|
<AuditLogs />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,13 +56,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Plus, Search } from "@element-plus/icons-vue";
|
import { Plus, Search } from "@element-plus/icons-vue";
|
||||||
import stageBreadcrumbs from "@/components/stageBreadcrumbs/index.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" });
|
defineOptions({ name: "Organization" });
|
||||||
|
|
||||||
const addValue = ref("");
|
const addValue = ref("");
|
||||||
const search = 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 {
|
interface Tree {
|
||||||
label: string;
|
label: string;
|
||||||
children?: Tree[];
|
children?: Tree[];
|
||||||
@@ -134,6 +151,12 @@ const defaultProps = {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use "sass:math";
|
@use "sass:math";
|
||||||
.mj-organization {
|
.mj-organization {
|
||||||
|
.organization-tabs{
|
||||||
|
:deep(.stage-breadcrumbs){
|
||||||
|
// border-bottom: none;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
}
|
||||||
.mj-organization-card {
|
.mj-organization-card {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid #e2e8f099;
|
border: 1px solid #e2e8f099;
|
||||||
@@ -141,13 +164,8 @@ const defaultProps = {
|
|||||||
margin-top: $mj-padding-standard;
|
margin-top: $mj-padding-standard;
|
||||||
box-shadow: 0 0 6px #e9e8e8;
|
box-shadow: 0 0 6px #e9e8e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.organization-tabs {
|
|
||||||
}
|
|
||||||
|
|
||||||
.organization-content {
|
.organization-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 400px;
|
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
.org-tree {
|
.org-tree {
|
||||||
flex: 0 0 280px;
|
flex: 0 0 280px;
|
||||||
@@ -173,6 +191,11 @@ const defaultProps = {
|
|||||||
}
|
}
|
||||||
.organization-info {
|
.organization-info {
|
||||||
flex: 1;
|
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 axios from "axios";
|
||||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
import { ElNotification } from 'element-plus';
|
import { ElNotification } from "element-plus";
|
||||||
import { getMockData, shouldUseMock } from '@/mock' //mock数据信息
|
import { VITE_APP_BASE_API } from "../../config.js";
|
||||||
|
import TokenManager from "@/utils/storage";
|
||||||
|
import { getMockData, shouldUseMock } from "@/mock"; //mock数据信息
|
||||||
const baseUrl = import.meta.env.VITE_APP_BASE_API;
|
const tokenManager = TokenManager.getInstance();
|
||||||
|
const baseUrl = "/api"; //TODO: 本地调试需要修改为/api
|
||||||
|
|
||||||
// 1. 锁和队列定义在类外部,确保全局唯一
|
// 1. 锁和队列定义在类外部,确保全局唯一
|
||||||
let isRefreshing = false;
|
let isRefreshing = false;
|
||||||
let requestsQueue: Array<(token: string) => void> = [];
|
let requestsQueue: Array<(token: string) => void> = [];
|
||||||
|
|
||||||
|
// 登录接口 传递参数不一样
|
||||||
|
const AUTH_OAUTH2_TOKEN_URL = "/auth/oauth2/token";
|
||||||
|
const CLIENT_CREDENTIALS = "Basic " + window.btoa("mversion:secret");
|
||||||
|
|
||||||
class HttpRequest {
|
class HttpRequest {
|
||||||
private instance: AxiosInstance;
|
private instance: AxiosInstance;
|
||||||
|
|
||||||
@@ -24,16 +29,24 @@ class HttpRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Token 管理方法 ---
|
// --- Token 管理方法 ---
|
||||||
private getAccessToken() { return localStorage.getItem("accessToken"); }
|
private getAccessToken() {
|
||||||
private getRefreshToken() { return localStorage.getItem("refreshToken"); }
|
return tokenManager.getToken("accessToken");
|
||||||
|
}
|
||||||
|
private getRefreshToken() {
|
||||||
|
return tokenManager.getToken("refreshToken");
|
||||||
|
}
|
||||||
private clearTokens() {
|
private clearTokens() {
|
||||||
localStorage.removeItem("accessToken");
|
tokenManager.clearStorage();
|
||||||
localStorage.removeItem("refreshToken");
|
|
||||||
// 这里可以触发跳转登录逻辑,例如:router.push('/login')
|
// 这里可以触发跳转登录逻辑,例如:router.push('/login')
|
||||||
}
|
}
|
||||||
private setTokens(data: any) {
|
private setTokens(data: any) {
|
||||||
localStorage.setItem("accessToken", data.accessToken);
|
tokenManager.setToken("accessToken", data.accessToken);
|
||||||
localStorage.setItem("refreshToken", data.refreshToken);
|
tokenManager.setToken("refreshToken", data.refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为认证接口
|
||||||
|
private isAuthEndpoint(url: string): boolean {
|
||||||
|
return url === AUTH_OAUTH2_TOKEN_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupInterceptors() {
|
private setupInterceptors() {
|
||||||
@@ -41,7 +54,11 @@ class HttpRequest {
|
|||||||
this.instance.interceptors.request.use(
|
this.instance.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const token = this.getAccessToken();
|
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}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
@@ -54,12 +71,16 @@ class HttpRequest {
|
|||||||
async (response: AxiosResponse) => {
|
async (response: AxiosResponse) => {
|
||||||
const { data: res, config: originalRequest } = response;
|
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 (res.code === 401) {
|
||||||
|
|
||||||
// 如果已经在刷新中了,将请求挂起并加入队列
|
// 如果已经在刷新中了,将请求挂起并加入队列
|
||||||
if (isRefreshing) {
|
if (isRefreshing) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -73,30 +94,32 @@ class HttpRequest {
|
|||||||
// 开始刷新流程
|
// 开始刷新流程
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
const rToken = this.getRefreshToken();
|
const rToken = this.getRefreshToken();
|
||||||
|
|
||||||
if (!rToken) {
|
if (!rToken) {
|
||||||
this.clearTokens();
|
this.clearTokens();
|
||||||
return Promise.reject(res);
|
return Promise.reject(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用一个干净的 axios 实例去发刷新请求,避免拦截器死循环
|
// TODO:使用一个干净的 axios 实例去发刷新请求,避免拦截器死循环
|
||||||
const refreshRes = await axios.post(`${this.instance.defaults.baseURL}/auth/refresh`, {
|
const refreshRes = await axios.post(
|
||||||
refreshToken: rToken
|
`${this.instance.defaults.baseURL}/auth/refresh`,
|
||||||
});
|
{
|
||||||
|
refreshToken: rToken,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const newToken = refreshRes.data.data.accessToken;
|
const newToken = refreshRes.data.data.accessToken;
|
||||||
this.setTokens(refreshRes.data.data);
|
this.setTokens(refreshRes.data.data);
|
||||||
|
|
||||||
// 刷新成功:释放队列
|
// 刷新成功:释放队列
|
||||||
requestsQueue.forEach(callback => callback(newToken));
|
requestsQueue.forEach((callback) => callback(newToken));
|
||||||
requestsQueue = [];
|
requestsQueue = [];
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
|
|
||||||
// 重试本次请求
|
// 重试本次请求
|
||||||
originalRequest.headers!.Authorization = `Bearer ${newToken}`;
|
originalRequest.headers!.Authorization = `Bearer ${newToken}`;
|
||||||
return this.instance(originalRequest);
|
return this.instance(originalRequest);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 刷新失败:清理并彻底报错
|
// 刷新失败:清理并彻底报错
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
@@ -107,7 +130,7 @@ class HttpRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 其它业务错误
|
// 其它业务错误
|
||||||
ElNotification.error({ title: '提示', message: res.msg || '服务异常' });
|
ElNotification.error({ title: "提示", message: res.msg || "服务异常" });
|
||||||
return Promise.reject(res);
|
return Promise.reject(res);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -121,63 +144,75 @@ class HttpRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public request<T = any>(config: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
public request<T = any>(config: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||||
// 检查是否应该使用 Mock 数据
|
// TODO:检查是否应该使用 Mock 数据
|
||||||
const requestUrl = config.url || '';
|
// const requestUrl = config.url || '';
|
||||||
|
// if (shouldUseMock(requestUrl)) {
|
||||||
// 优先使用请求 URL(通常是相对路径,如 /api/menus)
|
// const mockData = getMockData(requestUrl, config.params, config.data);
|
||||||
// 这样可以直接匹配 mockApiMap 中的 key
|
// if (mockData) {
|
||||||
if (shouldUseMock(requestUrl)) {
|
// console.log(`[Mock] 使用 Mock 数据: ${requestUrl}`, mockData);
|
||||||
const mockData = getMockData(requestUrl, config.params, config.data);
|
// // 模拟网络延迟
|
||||||
if (mockData) {
|
// return new Promise((resolve) => {
|
||||||
console.log(`[Mock] 使用 Mock 数据: ${requestUrl}`, mockData);
|
// setTimeout(() => {
|
||||||
// 模拟网络延迟
|
// resolve(mockData as ApiResponse<T>);
|
||||||
return new Promise((resolve) => {
|
// }, 100);
|
||||||
setTimeout(() => {
|
// });
|
||||||
resolve(mockData as ApiResponse<T>);
|
// }
|
||||||
}, 100);
|
// }
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.instance(config) as unknown as Promise<ApiResponse<T>>;
|
return this.instance(config) as unknown as Promise<ApiResponse<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET 请求
|
// GET 请求
|
||||||
public get<T = any>(url: string, params?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
public get<T = any>(
|
||||||
return this.request<T>({
|
url: string,
|
||||||
url,
|
params?: any,
|
||||||
method: 'get',
|
config?: AxiosRequestConfig
|
||||||
params,
|
): Promise<ApiResponse<T>> {
|
||||||
...config
|
return this.request<T>({
|
||||||
|
url,
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
...config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST 请求
|
// POST 请求
|
||||||
public post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
public post<T = any>(
|
||||||
return this.request<T>({
|
url: string,
|
||||||
url,
|
data?: any,
|
||||||
method: 'post',
|
config?: AxiosRequestConfig
|
||||||
data,
|
): Promise<ApiResponse<T>> {
|
||||||
...config
|
return this.request<T>({
|
||||||
|
url,
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
...config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT 请求
|
// PUT 请求
|
||||||
public put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
public put<T = any>(
|
||||||
return this.request<T>({
|
url: string,
|
||||||
url,
|
data?: any,
|
||||||
method: 'put',
|
config?: AxiosRequestConfig
|
||||||
data,
|
): Promise<ApiResponse<T>> {
|
||||||
...config
|
return this.request<T>({
|
||||||
|
url,
|
||||||
|
method: "put",
|
||||||
|
data,
|
||||||
|
...config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE 请求
|
// DELETE 请求
|
||||||
public delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
public delete<T = any>(
|
||||||
return this.request<T>({
|
url: string,
|
||||||
url,
|
config?: AxiosRequestConfig
|
||||||
method: 'delete',
|
): Promise<ApiResponse<T>> {
|
||||||
...config
|
return this.request<T>({
|
||||||
|
url,
|
||||||
|
method: "delete",
|
||||||
|
...config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,13 +221,13 @@ const httpRequest = new HttpRequest(baseUrl);
|
|||||||
|
|
||||||
// 导出方法
|
// 导出方法
|
||||||
export const request = {
|
export const request = {
|
||||||
get: <T = any>(url: string, params?: any, config?: AxiosRequestConfig) =>
|
get: <T = any>(url: string, params?: any, config?: AxiosRequestConfig) =>
|
||||||
httpRequest.get<T>(url, params, config),
|
httpRequest.get<T>(url, params, config),
|
||||||
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
|
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
|
||||||
httpRequest.post<T>(url, data, config),
|
httpRequest.post<T>(url, data, config),
|
||||||
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
|
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
|
||||||
httpRequest.put<T>(url, data, config),
|
httpRequest.put<T>(url, data, config),
|
||||||
delete: <T = any>(url: string, config?: AxiosRequestConfig) =>
|
delete: <T = any>(url: string, config?: AxiosRequestConfig) =>
|
||||||
httpRequest.delete<T>(url, config),
|
httpRequest.delete<T>(url, config),
|
||||||
};
|
};
|
||||||
export default httpRequest;
|
export default httpRequest;
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { createWebHistory, createRouter, type RouteRecordRaw } from 'vue-router'
|
import { createWebHistory, createRouter, type RouteRecordRaw } from 'vue-router'
|
||||||
import { useUserStore } from '@/store'
|
import { useUserStore } from '@/store'
|
||||||
import { getRouteMenus } from '@/api'
|
import { getRouteMenus } from '@/api'
|
||||||
import useTokenRefresh from '@/hooks/useTokenRefresh'
|
import TokenManager from '@/utils/storage';
|
||||||
|
|
||||||
import Login from '@/pages/Login/index.vue';
|
import Login from '@/pages/Login/index.vue';
|
||||||
import HomeView from '@/pages/Layout/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[] = [
|
const constantRoutes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
@@ -27,7 +26,7 @@ const asyncRoutes: RouteRecordRaw[] = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
name: 'Layout',
|
name: 'Layout',
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
redirect: '/home',
|
// redirect: '/home',
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
},
|
},
|
||||||
@@ -57,7 +56,7 @@ const loadComponent = (componentPath: string) => {
|
|||||||
fullPath = componentPath
|
fullPath = componentPath
|
||||||
} else if (componentPath.includes('/')) {
|
} else if (componentPath.includes('/')) {
|
||||||
// 补全路径,确保以 @/pages 开头
|
// 补全路径,确保以 @/pages 开头
|
||||||
fullPath = componentPath.startsWith('pages/') ? `@/${componentPath}` : `@/pages/${componentPath}`
|
fullPath = componentPath.startsWith('pages/') ? `@/${componentPath}` : `@/pages/${componentPath}/index.vue`
|
||||||
} else {
|
} else {
|
||||||
// 补全 index.vue
|
// 补全 index.vue
|
||||||
fullPath = `@/pages/${componentPath}/index.vue`
|
fullPath = `@/pages/${componentPath}/index.vue`
|
||||||
@@ -76,34 +75,35 @@ const loadComponent = (componentPath: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 将后端返回的路由数据转换为 Vue Router 路由
|
// 将后端返回的路由数据转换为 Vue Router 路由
|
||||||
const transformRoutes = (routes: any[]): RouteRecordRaw[] => {
|
const transformRoutes = (routes: any[], parentCode: string = ''): RouteRecordRaw[] => {
|
||||||
return routes.map((route) => {
|
return routes.flatMap((route) => {
|
||||||
const component = route.component ? loadComponent(route.component) : undefined
|
const fullCode = parentCode ? `${parentCode}/${route.code}` : route.code;
|
||||||
// 构建基础路由对象
|
|
||||||
const routeRecord: any = {
|
// 如果当前路由有子路由,说明它是一个路由前缀,不需要组件
|
||||||
path: route.path,
|
|
||||||
name: route.name || route.path,
|
|
||||||
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,
|
|
||||||
...route.meta,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有组件,添加组件属性
|
|
||||||
if (component) {
|
|
||||||
routeRecord.component = component
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理子路由
|
|
||||||
if (route.children && route.children.length > 0) {
|
if (route.children && route.children.length > 0) {
|
||||||
routeRecord.children = transformRoutes(route.children)
|
// 将子路由的路径加上当前路由的前缀,然后递归处理
|
||||||
}
|
return transformRoutes(route.children, fullCode);
|
||||||
|
} else {
|
||||||
|
// 叶子节点才需要组件和路由配置
|
||||||
|
const component = fullCode ? loadComponent(fullCode) : undefined;
|
||||||
|
|
||||||
|
const routeRecord: any = {
|
||||||
|
path: route.code,
|
||||||
|
name: route.code,
|
||||||
|
meta: {
|
||||||
|
title: route.name,
|
||||||
|
icon: route.icon,
|
||||||
|
...route.meta,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return routeRecord as RouteRecordRaw
|
if (component) {
|
||||||
})
|
routeRecord.component = component;
|
||||||
|
}
|
||||||
|
|
||||||
|
return routeRecord as RouteRecordRaw;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加动态路由
|
// 添加动态路由
|
||||||
@@ -116,15 +116,50 @@ const addDynamicRoutes = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 从后端获取路由菜单数据
|
// TODO:从后端获取路由菜单数据 (这边需要区分 后台的菜单 和用户的菜单)
|
||||||
const response = await getRouteMenus()
|
let response:any;
|
||||||
if (response.code === 0 && response.data) {
|
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 中
|
// 将动态路由添加到 Layout 的 children 中
|
||||||
const layoutRoute = router.getRoutes().find(route => route.name === 'Layout')
|
const layoutRoute = router.getRoutes().find(route => route.name === 'Layout')
|
||||||
|
|
||||||
console.log('Layout route:', layoutRoute,dynamicRoutes)
|
|
||||||
if (layoutRoute) {
|
if (layoutRoute) {
|
||||||
dynamicRoutes.forEach(route => {
|
dynamicRoutes.forEach(route => {
|
||||||
router.addRoute('Layout', route)
|
router.addRoute('Layout', route)
|
||||||
@@ -135,15 +170,14 @@ const addDynamicRoutes = async () => {
|
|||||||
router.addRoute(route)
|
router.addRoute(route)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
console.log('Layout route:', router.getRoutes())
|
||||||
// 保存路由数据到 store
|
// 保存路由数据到 store
|
||||||
userStore.setRoutes(response.data)
|
userStore.setRoutes(response)
|
||||||
|
|
||||||
// 标记路由已加载
|
// 标记路由已加载
|
||||||
userStore.isRoutesLoaded = true
|
userStore.isRoutesLoaded = true
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load routes:', error)
|
|
||||||
// 如果获取路由失败,清除用户数据并跳转到登录页
|
// 如果获取路由失败,清除用户数据并跳转到登录页
|
||||||
userStore.clearUserData()
|
userStore.clearUserData()
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
@@ -153,10 +187,10 @@ const addDynamicRoutes = async () => {
|
|||||||
// 路由导航守卫
|
// 路由导航守卫
|
||||||
router.beforeEach(async (to, _from, next) => {
|
router.beforeEach(async (to, _from, next) => {
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const { getAccessToken } = useTokenRefresh(baseUrl)
|
const accessToken = tokenManager.getToken('accessToken');
|
||||||
|
|
||||||
// 获取 token
|
// 获取 token
|
||||||
const token = getAccessToken() || userStore.token
|
const token = accessToken || userStore.token
|
||||||
|
|
||||||
// 如果已登录,更新 store 中的 token
|
// 如果已登录,更新 store 中的 token
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineStore } from "pinia";
|
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 {
|
interface UserInfo {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -25,12 +25,14 @@ interface RouteMenu {
|
|||||||
|
|
||||||
const useUserStore = defineStore("user", {
|
const useUserStore = defineStore("user", {
|
||||||
state: () => {
|
state: () => {
|
||||||
const { getAccessToken } = useTokenRefresh(baseUrl);
|
const accessToken = tokenManager.getToken('accessToken');
|
||||||
|
const userInfo = tokenManager.getToken('userInfo');
|
||||||
return {
|
return {
|
||||||
token: getAccessToken() || "",
|
token: accessToken || "",
|
||||||
userInfo: {} as UserInfo,
|
userInfo: userInfo ? JSON.parse(userInfo) : {},
|
||||||
routes: [] as RouteMenu[],
|
routes: [] as RouteMenu[],
|
||||||
isRoutesLoaded: false, // 标记路由是否已加载
|
isRoutesLoaded: false, // 标记路由是否已加载
|
||||||
|
isBackendUser:true, //标记是否是后台用户
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@@ -54,8 +56,7 @@ const useUserStore = defineStore("user", {
|
|||||||
this.userInfo = {};
|
this.userInfo = {};
|
||||||
this.routes = [];
|
this.routes = [];
|
||||||
this.isRoutesLoaded = false;
|
this.isRoutesLoaded = false;
|
||||||
const { clearTokens } = useTokenRefresh(baseUrl);
|
tokenManager.clearStorage();
|
||||||
clearTokens();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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{
|
.standard-ui-dialog{
|
||||||
--el-dialog-padding-primary:0;
|
&.el-dialog{
|
||||||
--el-dialog-inset-padding-primary:16px;
|
--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{
|
.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);
|
padding: var(--el-dialog-inset-padding-primary);
|
||||||
|
border-radius: var(--el-dialog-border-radius) var(--el-dialog-border-radius) 0 0;
|
||||||
}
|
}
|
||||||
.el-dialog__headerbtn{
|
.el-dialog__headerbtn{
|
||||||
height: 60px;
|
height: 60px;
|
||||||
@@ -37,12 +45,8 @@
|
|||||||
}
|
}
|
||||||
.el-dialog__footer{
|
.el-dialog__footer{
|
||||||
padding: var(--el-dialog-inset-padding-primary);
|
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,30 +1,35 @@
|
|||||||
|
.mj-panel-title {
|
||||||
|
|
||||||
.mj-panel-title{
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color:#1D293D;
|
color: #1D293D;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
&::before{
|
|
||||||
content:"";
|
&::before {
|
||||||
|
content: "";
|
||||||
width: 4px;
|
width: 4px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
background-color: #155DFC;
|
background-color: #155DFC;
|
||||||
display: inline-block;
|
border-radius: 2px;
|
||||||
vertical-align: middle;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mj-panel_header{
|
.mj-panel_header {
|
||||||
height: 54px;
|
height: 54px;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-bottom: 1px solid #F1F5F9;
|
border-bottom: 1px solid #F1F5F9;
|
||||||
.el-tabs{
|
|
||||||
--el-tabs-header-height:54px;
|
.el-tabs {
|
||||||
--el-border-color-light:transparent;
|
--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": {
|
"/api": {
|
||||||
target: VITE_APP_BASE_API,
|
target: VITE_APP_BASE_API,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
rewrite: (path) => path.replace(/^\/api/, "")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user