20260712212917

This commit is contained in:
oneao committed 2026-07-12 21:29:17 +08:00
1 parent 4677f6c519
commit 00472688ab
361 files changed
-153949

No files matched your search

-109
View File
@@ -1,109 +0,0 @@
<script setup>
import { computed, defineComponent, h, watch } from "vue";
import { RouterView } from "vue-router";
import { useTabStore } from "@/store/tab";
const tabStore = useTabStore();
const includeKeys = computed(() => tabStore.cachedKeys);
const wrapperMap = new Map();
function formatComponentInstance(component, fullPath) {
if (!component) return null;
let wrapper = wrapperMap.get(fullPath);
if (!wrapper) {
wrapper = defineComponent({
name: fullPath,
inheritAttrs: false,
setup(_, { attrs }) {
return () => h("div", { class: "g3-content-page" }, [h(component, attrs)]);
},
});
wrapperMap.set(fullPath, wrapper);
}
return wrapper;
}
watch(
() => tabStore.tabs,
(tabs) => {
const alive = new Set(tabs.map((t) => t.fullPath));
for (const key of wrapperMap.keys()) {
if (!alive.has(key)) {
wrapperMap.delete(key);
}
}
},
{ deep: true },
);
</script>
<template>
<div class="g3-content">
<RouterView v-slot="{ Component, route: childRoute }">
<Transition name="slide" mode="out-in">
<template v-if="childRoute.meta.keepAlive">
<keep-alive :include="includeKeys">
<component
:is="formatComponentInstance(Component, childRoute.fullPath)"
:key="childRoute.fullPath"
/>
</keep-alive>
</template>
<template v-else>
<div :key="childRoute.fullPath" class="g3-content-page">
<component :is="Component" />
</div>
</template>
</Transition>
</RouterView>
</div>
</template>
<style scoped lang="scss">
.g3-content {
flex: 1;
overflow: hidden;
box-sizing: border-box;
position: relative;
margin-top: 4px;
background-color: #fff;
}
.g3-content-page {
width: 100%;
height: 100%;
overflow: hidden;
box-sizing: border-box;
position: relative;
background-color: #fff;
}
.slide-enter-active,
.slide-leave-active {
transition:
opacity 0.24s ease,
transform 0.24s ease;
}
.slide-enter-from {
opacity: 0;
transform: translateX(18px);
}
.slide-leave-to {
opacity: 0;
transform: translateX(-18px);
}
.slide-enter-to,
.slide-leave-from {
opacity: 1;
transform: translateX(0);
}
</style>
-134
View File
@@ -1,134 +0,0 @@
<script lang="js" setup>
import { onBeforeUnmount, ref } from "vue";
import Menu from "./menu.vue";
import Topbar from "./topbar.vue";
import Tab from "./tab.vue";
import Content from "./content.vue";
const EXPANDED_MENU_WIDTH = 220;
const COLLAPSED_MENU_WIDTH = 64;
const COLLAPSE_ANIMATION_TIME = 300;
const isCollapse = ref(false);
const leftSize = ref(EXPANDED_MENU_WIDTH);
const isCollapsing = ref(false);
let collapseTimer = null;
function handleCollapse() {
if (isCollapsing.value) return;
isCollapsing.value = true;
isCollapse.value = !isCollapse.value;
leftSize.value = isCollapse.value ? COLLAPSED_MENU_WIDTH : EXPANDED_MENU_WIDTH;
window.clearTimeout(collapseTimer);
collapseTimer = window.setTimeout(() => {
isCollapsing.value = false;
}, COLLAPSE_ANIMATION_TIME);
}
function handleResizeEnd(index, sizes) {
window.clearTimeout(collapseTimer);
isCollapsing.value = false;
leftSize.value = sizes[0];
isCollapse.value = sizes[0] <= 80;
}
onBeforeUnmount(() => {
window.clearTimeout(collapseTimer);
});
</script>
<template>
<div class="g3-layout" :class="{ 'is-collapsing': isCollapsing }">
<el-splitter @resize-end="handleResizeEnd">
<el-splitter-panel
class="g3-layout-panel-left"
:min="COLLAPSED_MENU_WIDTH"
:size="leftSize"
:max="400"
>
<div class="g3-layout-left">
<Menu :collapse="isCollapse" :animating="isCollapsing" @collapse="handleCollapse" />
</div>
</el-splitter-panel>
<el-splitter-panel class="g3-layout-panel-right">
<div class="g3-layout-right">
<Topbar />
<Tab />
<Content />
</div>
</el-splitter-panel>
</el-splitter>
</div>
</template>
<style lang="scss" scoped>
.g3-layout {
display: flex;
width: 100%;
height: 100%;
background-color: #f0f2f5;
overflow: hidden;
:deep(.el-splitter-bar__dragger-horizontal::after),
:deep(.el-splitter-bar__dragger-horizontal::before) {
width: 1px;
}
:deep(.el-splitter-bar__dragger::after),
:deep(.el-splitter-bar__dragger::before) {
// background-color: var(--g3-layout-divider-color);
background-color: #fff;
}
}
.g3-layout-left {
flex-shrink: 0;
height: 100%;
overflow: hidden;
// border-right: 1px solid #bfbfc6;
}
/* 为 el-splitter-panel 添加过渡动画 */
:deep(.el-splitter-panel) {
transition: none !important;
}
:deep(.g3-layout-panel-left),
:deep(.g3-layout-panel-right) {
overflow: hidden;
}
:deep(.g3-layout-panel-left) {
z-index: 1;
overflow: visible;
border-right: 1px solid var(--g3-layout-divider-color);
}
/* 为 el-splitter-bar 添加过渡动画 */
:deep(.el-splitter-bar) {
transition: none !important;
}
.g3-layout.is-collapsing {
:deep(.el-splitter-panel) {
transition: flex-basis 0.3s ease !important;
}
:deep(.el-splitter-bar) {
transition:
left 0.3s ease,
right 0.3s ease !important;
}
}
.g3-layout-right {
flex: 1;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
margin: 0px 10px;
}
</style>
@@ -1,82 +0,0 @@
<template>
<el-sub-menu v-if="hasChildren" :index="menuIndex" :class="levelClass">
<template #title>
<el-icon v-if="item.icon">
<G3Icon :icon="item.icon" />
</el-icon>
<span class="menu-label">{{ item.label }}</span>
</template>
<MenuItem
v-for="(child, idx) in item.children"
:key="child.path || idx"
:item="child"
:parent-index="menuIndex + '-' + idx"
/>
</el-sub-menu>
<el-menu-item
v-else
:index="item.path"
:route="{
path: item.path,
}"
>
<el-icon v-if="item.icon">
<G3Icon :icon="item.icon" />
</el-icon>
<span class="menu-label">{{ item.label }}</span>
</el-menu-item>
</template>
<script setup lang="js">
import { computed } from "vue";
import G3Icon from "@/components/g3-icon/index.vue";
const props = defineProps({
item: { type: Object, required: true },
parentIndex: { type: String, default: "0" },
});
const hasChildren = props.item.children && props.item.children.length > 0;
// 父菜单没有 path,用索引生成唯一 index,子菜单 path 就用真实路由
const menuIndex = props.item.path || props.parentIndex;
// 一级、二级 class
const levelClass = computed(() => {
if (!hasChildren) return "";
const level = props.parentIndex.split("-").length;
// 一级菜单
if (level === 1) {
// 判断子节点是否还有 children
const childHasChildren = props.item.children.some(
(child) => child.children && child.children.length > 0,
);
// 子节点都是叶子节点,才加 g3level1
return childHasChildren ? "" : "g3level1";
}
// 二级菜单,只要有子菜单就加 g3level2
if (level === 2) {
return "g3level2";
}
return "";
});
</script>
<style scoped>
.menu-label {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
:deep(.el-icon svg) {
width: 16px;
height: 16px;
}
</style>
-422
View File
@@ -1,422 +0,0 @@
<script setup>
import { ref, computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import axios from "@/utils/axios";
import G3Icon from "@/components/g3-icon/index.vue";
import MenuItem from "./menu-item.vue";
const route = useRoute();
const router = useRouter();
const props = defineProps({
collapse: {
type: Boolean,
default: false,
},
animating: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(["collapse"]);
const title = ref("G3后台管理系统");
// 方便以后根据不同的权限显示不同的菜单
const selectedMenuKey = computed(() => route.fullPath);
const menuList = ref([
// 首页
{
label: "首页",
path: "/home",
icon: "svghome",
},
// 业务管理
{
label: "业务管理",
icon: "ri:folder-chart-line",
children: [
{ label: "业务管理", path: "/business" },
{ label: "排车列表", path: "/business/listcontainer" },
],
},
// 财务管理
{
label: "财务管理",
icon: "ri:wallet-line",
children: [
{
label: "应收管理",
children: [
{ label: "应收费用列表", path: "/cw/ys/list" },
{ label: "应收账单", path: "/cw/ys/listbill" },
{ label: "应收发票", path: "/cw/ys/listinvoice" },
{ label: "收款管理", path: "/cw/ys/listskd" },
],
},
{
label: "应付管理",
children: [
{ label: "应付费用列表", path: "/cw/yf/list" },
{ label: "应付账单", path: "/cw/yf/listbill" },
{ label: "应付发票", path: "/cw/yf/listinvoice" },
{ label: "付款管理", path: "/cw/yf/listfkd" },
],
},
{
label: "日常费用",
children: [
{ label: "日常费用", path: "/cw/assistance/daily-expenses" },
{ label: "收付账户", path: "/cw/assistance/payment-account" },
],
},
],
},
// 统计分析
{
label: "统计分析",
icon: "ri:bar-chart-box-line",
children: [
{
label: "收款统计分析",
children: [
{ label: "应收未收明细", path: "/tj/ystj/tj_nots" },
{ label: "应收未收按单位汇总", path: "/tj/ystj/tj_notsbydw" },
{ label: "未收款按业务员汇总", path: "/tj/ystj/tj_notsbysales" },
{ label: "收款明细统计", path: "/tj/ystj/tj_cwskxz" },
],
},
{
label: "付款统计分析",
children: [
{ label: "应付未付明细", path: "/tj/yftj/tj_alone_notf" },
{ label: "应付未付按单位汇总", path: "/tj/yftj/tj_notfbydw" },
{ label: "付款明细统计", path: "/tj/yftj/tj_cwfkxz" },
],
},
{
label: "利润统计分析",
children: [
{ label: "利润分析单票明细", path: "/tj/lrtj/tj_lr_module_id" },
{ label: "利润分析按业务员", path: "/tj/lrtj/tj_lrbysales" },
{ label: "利润分析按客户", path: "/tj/lrtj/tj_lrbycustomer" },
],
},
],
},
// 审批中心
{
label: "管理中心",
icon: "ri:government-line",
children: [
{ label: "审批中心", path: "/shcenter" },
{ label: "合同管理", path: "/partners/khht" },
{ label: "运费模版", path: "/pricelist/list" },
{
label: "企业公告",
path: "/system/oa-management/companyAnnouncement",
},
{ label: "审批设置", path: "/system/approvalSettings" },
{ label: "提醒设置", path: "/system/oa-management/userReminder" },
{ label: "结算汇率", path: "/partners/hl" },
],
},
// 基础数据
{
label: "基础数据",
icon: "ri:database-line",
children: [
{ label: "合作伙伴", path: "/partners" },
{ label: "联系人", path: "/partners/contact" },
{ label: "其他数据", path: "/base/other" },
],
},
// 系统管理
{
label: "系统管理",
icon: "ri:settings-line",
children: [
{ label: "权限管理", path: "/system/permission" },
{ label: "字段配置", path: "/system/field" },
{ label: "查询设置", path: "/system/statisticalSettings" },
{ label: "映射配置", path: "/system/mappingConfig" },
{ label: "模块设置", path: "/system/moduleSettings" },
{ label: "自动编码", path: "/system/autoId" },
],
},
// 系统帮助
{
label: "系统帮助",
icon: "ri:question-line",
children: [
{ label: "帮助文档", path: "/help" },
{ label: "操作视频", path: "/videos" },
],
},
// 功能测试
{
label: "功能测试",
icon: "ri:bug-line",
children: [{ label: "测试页面", path: "/test/test" }],
},
]);
function handleSelect(val) {
router.push(val);
}
function handleCollapse() {
emits("collapse");
}
onMounted(async () => {
let searchsql = `select * from s_keyvalue where b_key='bodycaption'`;
const response = await axios.get("/selectlistbysql", {
params: { sql: searchsql },
});
if (response.data?.success) {
const records = response.data.data.records || [];
if (records.length > 0) {
title.value = records[0].b_value;
}
}
});
</script>
<template>
<div class="app-menu" :class="{ collapse: props.collapse, 'is-animating': props.animating }">
<div
class="menu-logo"
@click="
() => {
router.push('/home');
}
"
>
<img src="@/assets/g3logo.png" alt="Logo" class="logo" />
<div v-show="!props.collapse" class="title">{{ title }}</div>
</div>
<div class="menu-container">
<el-menu
:collapse="props.collapse"
:collapse-transition="false"
class="menu-inner"
:default-active="selectedMenuKey"
@select="handleSelect"
>
<MenuItem
v-for="(item, idx) in menuList"
:key="item.path || idx"
:item="item"
:parent-index="idx.toString()"
/>
</el-menu>
</div>
<div class="menu-footer">
<div @click="handleCollapse()">
<G3Icon :icon="props.collapse ? 'ri:menu-fold-4-line' : 'ri:menu-fold-3-line'" :size="20" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.app-menu {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
width: 100%;
background-color: #f8f8f8;
overflow: hidden;
transition: background-color 0.3s ease;
&.collapse {
.menu-logo .logo {
width: 46px;
height: 16px;
}
.menu-footer {
justify-content: center;
padding-right: 0px;
}
}
&.is-animating {
pointer-events: none;
}
}
.menu-logo {
display: flex;
align-items: center;
justify-content: center;
height: 42px;
padding: 19px 0px;
cursor: pointer;
width: 100%;
overflow: hidden;
transition: padding 0.3s ease;
.logo {
width: 80px;
height: 24px;
transition: all 0.3s ease;
}
.title {
margin-left: 10px;
font-size: 16px;
font-weight: 700;
color: #70cfb1;
white-space: nowrap;
transition: all 0.3s ease;
}
}
.menu-container {
flex: 1;
position: relative;
width: 100%;
overflow: hidden;
background-color: #f8f8f8;
.menu-inner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow-y: auto;
overflow-x: hidden;
min-width: 64px;
box-sizing: border-box;
width: 100% !important;
border-right: none !important;
scrollbar-width: thin;
scrollbar-color: #cfd3dc transparent;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: #cfd3dc;
border-radius: 999px;
&:hover {
background-color: #a8abb2;
}
}
}
:deep(.el-sub-menu__title) {
height: 28px !important;
line-height: 28px !important;
box-sizing: border-box;
color: #525252;
font-size: 13px;
font-family:
"InterVariable", "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
:deep(.el-menu-item) {
height: 28px !important;
line-height: 28px !important;
box-sizing: border-box;
color: #525252;
font-size: 13px;
font-family:
"InterVariable", "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
&.is-active {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-weight: 500;
}
}
:deep(.el-menu-item:hover) {
background-color: #f3f3f3;
border-radius: 8px;
}
:deep(.el-sub-menu) {
&.is-active {
> .el-sub-menu__title {
color: var(--el-color-primary);
font-weight: 500;
}
}
}
}
:deep(.el-sub-menu),
:deep(.el-menu) {
background-color: #f8f8f8 !important;
}
:deep(.el-sub-menu.is-active .el-sub-menu__title) {
border-bottom-color: #525252;
}
:deep(.el-sub-menu__title:hover) {
background-color: #f3f3f3;
}
:deep(.menu-container .el-menu-item.is-active) {
color: #525252;
font-weight: 500;
background: rgba(255, 255, 255, 1);
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
:deep(.menu-container .el-sub-menu.is-active > .el-sub-menu__title) {
color: #525252;
font-weight: 500;
}
:deep(.g3level1 .el-menu) {
margin-left: 30px;
}
:deep(.g3level1 .el-menu .el-menu-item) {
padding: 0px 20px 0px 20px !important;
}
:deep(.g3level2 .el-sub-menu__title) {
margin-left: 30px;
padding: 0px 20px 0px 20px !important;
}
:deep(.g3level2 .el-menu) {
margin-left: 42px;
}
:deep(.g3level2 .el-menu .el-menu-item) {
padding: 0px 20px 0px 20px !important;
}
.menu-footer {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
cursor: pointer;
box-sizing: border-box;
padding: 8px 0px 8px 0px;
padding-right: 12px;
overflow: hidden;
transition: padding-right 0.3s ease;
div {
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
&:hover {
background-color: rgba(45, 140, 240, 0.1);
color: rgba(45, 140, 240, 0.8);
}
}
}
</style>
@@ -1,370 +0,0 @@
<script setup>
import { ref, computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import axios from "@/utils/axios";
import G3Icon from "@/components/g3-icon/index.vue";
import MenuItem from "./menu-item.vue";
const route = useRoute();
const router = useRouter();
const props = defineProps({
collapse: {
type: Boolean,
default: false,
},
animating: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(["collapse"]);
const title = ref("G3后台管理系统");
// 方便以后根据不同的权限显示不同的菜单
const selectedMenuKey = computed(() => route.fullPath);
const menuList = ref([
// 首页
{
label: "首页",
path: "/home",
icon: "mdi:home",
},
// 业务管理
{
label: "业务管理",
icon: "ri:folder-chart-line",
children: [
{ label: "业务管理", path: "/business" },
{ label: "排车列表", path: "/business/listcontainer" },
],
},
// 财务管理
{
label: "财务管理",
icon: "ri:wallet-line",
children: [
{
label: "应收管理",
children: [
{ label: "应收费用列表", path: "/cw/ys/list" },
{ label: "应收账单", path: "/cw/ys/listbill" },
{ label: "应收发票", path: "/cw/ys/listinvoice" },
{ label: "收款管理", path: "/cw/ys/listskd" },
],
},
{
label: "应付管理",
children: [
{ label: "应付费用列表", path: "/cw/yf/list" },
{ label: "应付账单", path: "/cw/yf/listbill" },
{ label: "应付发票", path: "/cw/yf/listinvoice" },
{ label: "付款管理", path: "/cw/yf/listfkd" },
],
},
{
label: "日常费用",
children: [
{ label: "日常费用", path: "/cw/assistance/daily-expenses" },
{ label: "收付账户", path: "/cw/assistance/payment-account" },
],
},
],
},
// 统计分析
{
label: "统计分析",
icon: "ri:bar-chart-box-line",
children: [
{
label: "收款统计分析",
children: [
{ label: "应收未收明细", path: "/tj/ystj/tj_nots" },
{ label: "应收未收按单位汇总", path: "/tj/ystj/tj_notsbydw" },
{ label: "未收款按业务员汇总", path: "/tj/ystj/tj_notsbysales" },
{ label: "收款明细统计", path: "/tj/ystj/tj_cwskxz" },
],
},
{
label: "付款统计分析",
children: [
{ label: "应付未付明细", path: "/tj/yftj/tj_alone_notf" },
{ label: "应付未付按单位汇总", path: "/tj/yftj/tj_notfbydw" },
{ label: "付款明细统计", path: "/tj/yftj/tj_cwfkxz" },
],
},
{
label: "利润统计分析",
children: [
{ label: "利润分析单票明细", path: "/tj/lrtj/tj_lr_module_id" },
{ label: "利润分析按业务员", path: "/tj/lrtj/tj_lrbysales" },
{ label: "利润分析按客户", path: "/tj/lrtj/tj_lrbycustomer" },
],
},
],
},
// 审批中心
{
label: "管理中心",
icon: "ri:government-line",
children: [
{ label: "审批中心", path: "/shcenter" },
{ label: "合同管理", path: "/partners/khht" },
{ label: "运费模版", path: "/pricelist/list" },
{
label: "企业公告",
path: "/system/oa-management/companyAnnouncement",
},
{ label: "审批设置", path: "/system/approvalSettings" },
{ label: "提醒设置", path: "/system/oa-management/userReminder" },
{ label: "结算汇率", path: "/partners/hl" },
],
},
// 基础数据
{
label: "基础数据",
icon: "ri:database-line",
children: [
{ label: "合作伙伴", path: "/partners" },
{ label: "联系人", path: "/partners/contact" },
{ label: "其他数据", path: "/base/other" },
],
},
// 系统管理
{
label: "系统管理",
icon: "ri:settings-line",
children: [
{ label: "权限管理", path: "/system/permission" },
{ label: "字段配置", path: "/system/field" },
{ label: "查询设置", path: "/system/statisticalSettings" },
{ label: "映射配置", path: "/system/mappingConfig" },
{ label: "模块设置", path: "/system/moduleSettings" },
{ label: "自动编码", path: "/system/autoId" },
],
},
// 系统帮助
{
label: "系统帮助",
icon: "ri:question-line",
children: [
{ label: "帮助文档", path: "/help" },
{ label: "操作视频", path: "/videos" },
],
},
// 功能测试
{
label: "功能测试",
icon: "ri:bug-line",
children: [{ label: "测试页面", path: "/test/test" }],
},
]);
function handleSelect(val) {
router.push(val);
}
function handleCollapse() {
emits("collapse");
}
onMounted(async () => {
let searchsql = `select * from s_keyvalue where b_key='bodycaption'`;
const response = await axios.get("/selectlistbysql", {
params: { sql: searchsql },
});
if (response.data?.success) {
const records = response.data.data.records || [];
if (records.length > 0) {
title.value = records[0].b_value;
}
}
});
</script>
<template>
<div class="app-menu" :class="{ collapse: props.collapse, 'is-animating': props.animating }">
<div
class="menu-logo"
@click="
() => {
router.push('/home');
}
"
>
<img src="@/assets/g3logo.png" alt="Logo" class="logo" />
<div v-show="!props.collapse" class="title">{{ title }}</div>
</div>
<div class="menu-container">
<el-menu
:collapse="props.collapse"
:collapse-transition="false"
class="menu-inner"
:default-active="selectedMenuKey"
@select="handleSelect"
>
<MenuItem
v-for="(item, idx) in menuList"
:key="item.path || idx"
:item="item"
:parent-index="idx.toString()"
/>
</el-menu>
</div>
<div class="menu-footer">
<div @click="handleCollapse()">
<G3Icon :icon="props.collapse ? 'ri:menu-fold-4-line' : 'ri:menu-fold-3-line'" :size="20" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.app-menu {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
width: 100%;
background-color: white;
overflow: hidden;
transition: background-color 0.3s ease;
&.collapse {
.menu-logo .logo {
width: 46px;
height: 16px;
}
.menu-footer {
justify-content: center;
padding-right: 0px;
}
}
&.is-animating {
pointer-events: none;
}
}
.menu-logo {
display: flex;
align-items: center;
justify-content: center;
height: 42px;
padding: 19px 0px;
cursor: pointer;
width: 100%;
overflow: hidden;
transition: padding 0.3s ease;
.logo {
width: 80px;
height: 24px;
transition: all 0.3s ease;
}
.title {
margin-left: 10px;
font-size: 16px;
font-weight: 700;
color: #70cfb1;
white-space: nowrap;
transition: all 0.3s ease;
}
}
.menu-container {
flex: 1;
position: relative;
width: 100%;
overflow: hidden;
.menu-inner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow-y: auto;
overflow-x: hidden;
min-width: 64px;
box-sizing: border-box;
width: 100% !important;
border-right: none !important;
scrollbar-width: thin;
scrollbar-color: #cfd3dc transparent;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: #cfd3dc;
border-radius: 999px;
&:hover {
background-color: #a8abb2;
}
}
}
:deep(.el-sub-menu__title) {
height: 42px !important;
line-height: 42px !important;
box-sizing: border-box;
}
:deep(.el-menu-item) {
height: 42px !important;
line-height: 42px !important;
box-sizing: border-box;
&.is-active {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-weight: 500;
}
}
:deep(.el-sub-menu) {
&.is-active {
> .el-sub-menu__title {
color: var(--el-color-primary);
font-weight: 500;
}
}
}
}
.menu-footer {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
cursor: pointer;
box-sizing: border-box;
padding: 8px 0px 8px 0px;
padding-right: 12px;
overflow: hidden;
transition: padding-right 0.3s ease;
div {
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
&:hover {
background-color: rgba(45, 140, 240, 0.1);
color: rgba(45, 140, 240, 0.8);
}
}
}
</style>
-402
View File
@@ -1,402 +0,0 @@
<script lang="js" setup>
import G3Icon from "@/components/g3-icon/index.vue";
import { useRoute } from "vue-router";
import { useRouter } from "vue-router";
import { nextTick, reactive, ref, watch } from "vue";
import { useTabStore } from "@/store/tab";
const { refs, setRefs } = useRefs();
const showScroll = ref(false);
const route = useRoute();
const router = useRouter();
const tabStore = useTabStore();
watch(
() => tabStore.activeTab,
(newVal) => {
nextTick(() => {
updateScroll();
const tabIndex = tabStore.findTabIndex(newVal);
moveScroll(tabIndex);
});
},
);
function useRefs() {
const refs = reactive({});
function setRefs(name) {
return (el) => {
refs[name] = el;
return () => refs[name];
};
}
return { refs, setRefs };
}
function updateScroll() {
const wrap = refs.scroller;
if (!wrap) return;
showScroll.value = wrap.scrollWidth > wrap.clientWidth;
}
function moveScroll(index) {
nextTick(() => {
const el = refs[`item-${index}`];
const wrap = refs.scroller;
if (!el || !wrap) return;
let attempts = 0;
const maxAttempts = 5;
const scrollToActiveTab = () => {
const scrollLeft =
el.getBoundingClientRect().left -
wrap.getBoundingClientRect().left +
wrap.scrollLeft -
wrap.clientWidth / 2 +
el.clientWidth / 2;
wrap.scrollLeft = scrollLeft;
if (Math.abs(wrap.scrollLeft - scrollLeft) > 1 && attempts < maxAttempts) {
attempts++;
requestAnimationFrame(scrollToActiveTab);
}
};
scrollToActiveTab();
});
}
// 切换标签
function scrollTabs(direction) {
const wrap = refs.scroller;
if (!wrap) return;
const distance = wrap.clientWidth * 0.8;
wrap.scrollLeft += direction === "left" ? -distance : distance;
}
function switchTab(tab, index) {
router.push(tab.fullPath);
moveScroll(index);
}
// 关闭标签
async function closeTab(tab) {
if (tab.fullPath === "/home") return; // ❗首页不能关
const activePath = await tabStore.closeTab(tab.fullPath);
if (activePath && route.fullPath !== activePath) {
router.push(activePath);
} else if (!activePath) {
router.push("/home");
}
}
// 关闭左侧
function closeLeft(data) {
const currentIndex = tabStore.tabs.findIndex((item) => item.fullPath === data.fullPath);
if (currentIndex === -1) return;
tabStore.tabs
.slice(0, currentIndex)
.filter((item) => item.fullPath !== "/home") // ❗过滤首页
.forEach((item) => {
const index = tabStore.tabs.findIndex((t) => t.fullPath === item.fullPath);
if (index !== -1) tabStore.tabs.splice(index, 1);
if (item.fullPath === route.fullPath) {
router.push(data.fullPath);
}
});
}
// 关闭右侧
function closeRight(data) {
const currentIndex = tabStore.tabs.findIndex((item) => item.fullPath === data.fullPath);
if (currentIndex === -1) return;
tabStore.tabs
.slice(currentIndex + 1)
.filter((item) => item.fullPath !== "/home") // ❗过滤首页
.forEach((item) => {
const index = tabStore.tabs.findIndex((t) => t.fullPath === item.fullPath);
if (index !== -1) tabStore.tabs.splice(index, 1);
if (item.fullPath === route.fullPath) {
router.push(data.fullPath);
}
});
}
// 关闭其他
function closeOther(tab) {
tabStore.tabs = tabStore.tabs.filter(
(t) => t.fullPath === tab.fullPath || t.fullPath === "/home", // ❗保留首页
);
router.push(tab.fullPath);
}
// 关闭全部
function closeAll() {
tabStore.tabs = tabStore.tabs.filter(
(t) => t.fullPath === "/home", // ❗只保留首页
);
router.push("/home");
}
</script>
<template>
<div class="g3-tabs" :class="{ 'has-scroll': showScroll }">
<div v-if="showScroll" class="g3-action-icon g3-scroll-more" @click="scrollTabs('left')">
<G3Icon icon="lucide:chevrons-left" />
</div>
<div class="g3-tab-container">
<div :ref="setRefs('scroller')" class="app-tab-scroller">
<el-dropdown
v-for="(item, index) in tabStore.tabs"
:key="item.fullPath"
trigger="contextmenu"
:persistent="false"
style="
height: 24px;
border: 1px solid #f2f3f5;
margin-right: 6px;
background-color: #f2f3f5;
"
>
<template #dropdown>
<el-dropdown-menu v-if="item.fullPath !== '/home'">
<el-dropdown-item @click="closeTab(item)"> 关闭 </el-dropdown-item>
<el-dropdown-item @click="closeOther(item)"> 关闭其他 </el-dropdown-item>
<el-dropdown-item @click="closeLeft(item)"> 关闭左侧 </el-dropdown-item>
<el-dropdown-item @click="closeRight(item)"> 关闭右侧 </el-dropdown-item>
<el-dropdown-item @click="closeAll"> 关闭所有 </el-dropdown-item>
</el-dropdown-menu>
</template>
<div
:ref="setRefs(`item-${index}`)"
class="app-tab-item"
:class="{ active: item.fullPath === tabStore.activeTab }"
@click="switchTab(item)"
>
<span class="label">{{ item.title }}</span>
<G3Icon
v-if="item.fullPath !== '/home'"
icon="lucide:x"
size="12"
class="icon"
@click.stop="closeTab(item)"
/>
</div>
</el-dropdown>
</div>
</div>
<div v-if="showScroll" class="g3-action-icon g3-scroll-more" @click="scrollTabs('right')">
<G3Icon icon="lucide:chevrons-right" />
</div>
</div>
</template>
<style lang="scss" scoped>
.g3-tabs {
display: flex;
align-items: center;
height: 32px;
user-select: none;
background-color: #fff;
border-radius: 6px;
margin-top: 3px;
}
.g3-action-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 38px;
transition: all 0.2s ease-in-out;
outline: none;
cursor: pointer;
flex-shrink: 0;
color: #18181b;
background-color: #fff;
&:first-child {
border-radius: 0 0 0 6px;
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 100%;
background-color: var(--g3-layout-divider-color);
pointer-events: none;
}
}
&:last-child {
border-radius: 0 0 6px 0;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 100%;
background-color: var(--g3-layout-divider-color);
pointer-events: none;
}
}
&:hover {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-8);
}
}
.g3-tab-container {
height: 100%;
flex: 1;
position: relative;
overflow: hidden;
:deep(.el-dropdown) {
position: relative;
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 100%;
background-color: var(--g3-layout-divider-color);
pointer-events: none;
}
}
}
.g3-tabs.has-scroll {
.g3-tab-container {
:deep(.el-dropdown:last-child) {
&::after {
display: none;
}
}
}
}
.app-tab-scroller {
height: 100%;
width: 100%;
white-space: nowrap;
position: absolute;
left: 0;
top: 0;
scrollbar-width: none;
overflow: hidden;
scroll-behavior: smooth;
display: flex;
align-items: center;
padding-left: 12px;
}
.app-tab-item {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0px 6px;
cursor: pointer;
color: #4e5969;
background-color: #f2f3f5;
min-width: 40px;
.label {
font-size: 12px;
line-height: 1;
font-weight: 500;
letter-spacing: 0.05em;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
PingFang SC,
Hiragino Sans GB,
noto sans,
Microsoft YaHei,
Helvetica Neue,
Helvetica,
Arial,
sans-serif;
}
.icon {
margin-left: 5px;
}
&:hover {
background-color: var(--el-color-primary-light-8);
}
&.active {
background-color: var(--el-color-primary-light-9);
&::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 2px;
background-color: var(--el-color-primary);
pointer-events: none;
}
.label {
color: var(--el-color-primary);
font-weight: 500;
}
.icon {
color: var(--el-color-primary);
}
&:hover {
background-color: var(--el-color-primary-light-9);
}
}
}
.g3-tabs:not(.has-scroll) :deep(.el-dropdown:first-child) {
.app-tab-item {
overflow: hidden;
border-radius: 0 0 0 6px;
}
}
.g3-tab-container .el-dropdown::after {
display: none;
}
.app-tab-item.active::after {
display: none;
}
.app-tab-item.active {
background-color: unset;
}
</style>
-287
View File
@@ -1,287 +0,0 @@
<script setup>
import { ArrowDown } from "@element-plus/icons-vue";
import { useRouter } from "vue-router";
import { onMounted, ref } from "vue";
import axios from "@/utils/axios";
import updatePassword from "@/views/user-center/updatepassword.vue";
import { useTabStore, useUserStore } from "@/store";
const router = useRouter();
const userStore = useUserStore();
const tabStore = useTabStore();
const title = ref("G3服务平台");
let yjt_svg =
'<svg data-v-c361ffca="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="18px" height="18px" viewBox="0 0 48 48" class="iconify iconify--icon-park-outline"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m19 12l12 12l-12 12"></path></svg>';
let grzx_svg =
'<svg data-v-c361ffca="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="18px" height="18px" viewBox="0 0 24 24" class="iconify iconify--tdesign"><g fill="none"><path d="M16.5 7.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2z"></path><path stroke="currentColor" stroke-linecap="square" stroke-width="2" d="M16.5 7.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0ZM20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2z"></path></g></svg>';
let xgmm_svg =
'<svg data-v-c361ffca="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="18px" height="18px" viewBox="0 0 24 24" class="iconify iconify--mdi"><path fill="currentColor" d="M12 17a2 2 0 0 1-2-2c0-1.11.89-2 2-2a2 2 0 0 1 2 2a2 2 0 0 1-2 2m6 3V10H6v10zm0-12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10c0-1.11.89-2 2-2h1V6a5 5 0 0 1 5-5a5 5 0 0 1 5 5v2zm-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3"></path></svg>';
let logout_svg =
'<svg data-v-c361ffca="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="18px" height="18px" viewBox="0 0 24 24" class="iconify iconify--mdi"><path fill="currentColor" d="M14.08 15.59L16.67 13H7v-2h9.67l-2.59-2.59L15.5 7l5 5l-5 5zM19 3a2 2 0 0 1 2 2v4.67l-2-2V5H5v14h14v-2.67l2-2V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5c0-1.11.89-2 2-2z"></path></svg>';
// 跳转到个人中心
function goToProfile() {
router.push("/user-center");
}
const passwordDialogVisible = ref(false);
function handleUpdatePassword() {
passwordDialogVisible.value = true;
}
// 退出登录
function logout() {
tabStore.reset();
userStore.reset();
router.push("/login");
}
onMounted(async () => {
let searchsql = `select * from s_keyvalue where b_key='maincaption'`;
const response = await axios.get("/selectlistbysql", { params: { sql: searchsql } });
if (response.data?.success) {
const records = response.data.data.records || [];
if (records.length > 0) {
title.value = records[0].b_value;
}
}
});
</script>
<template>
<div class="g3-topbar">
<div />
<div class="g3-topbar-right">
<div class="company-name">
{{ title }}
</div>
<div class="topbar-divider" />
<el-dropdown placement="bottom-end">
<div class="user-card">
<img
class="avatar"
src="https://api.dicebear.com/9.x/initials/svg?seed=User&backgroundColor=e5e7eb&fontColor=6b7280"
/>
<div class="name">{{ userStore.userInfo.userid || "未登录" }}</div>
<el-icon :size="14">
<arrow-down />
</el-icon>
</div>
<template #dropdown>
<div class="user-dropdown">
<div class="user-info">
<img
class="avatar"
src="https://api.dicebear.com/9.x/initials/svg?seed=User&backgroundColor=e5e7eb&fontColor=6b7280"
/>
<div>
<div class="name">{{ userStore.userInfo.userid || "未登录" }}</div>
<div class="orgid">
<div>机构码:</div>
<div>{{ userStore.userInfo.orgid || "无机构码" }}</div>
</div>
</div>
</div>
<el-dropdown-menu>
<el-dropdown-item divided @click="goToProfile">
<div class="g3flex g3color">
<div>
<div class="g3grzx" v-html="grzx_svg"></div>
<div class="g3lable">个人中心</div>
</div>
<div class="g3yjtsvg" v-html="yjt_svg"></div>
</div>
</el-dropdown-item>
<el-dropdown-item @click="handleUpdatePassword">
<div class="g3flex g3color">
<div>
<div class="g3grzx" v-html="xgmm_svg"></div>
<div class="g3lable">修改密码</div>
</div>
<div class="g3yjtsvg" v-html="yjt_svg"></div>
</div>
</el-dropdown-item>
</el-dropdown-menu>
<div class="g3hr"></div>
<div class="g3logout">
<div @click="logout">
<div class="g3logoutsvg" v-html="logout_svg"></div>
<div class="g3lable">退出登录</div>
</div>
</div>
</div>
</template>
</el-dropdown>
</div>
</div>
<updatePassword v-model:visible="passwordDialogVisible" />
</template>
<style lang="css" scoped>
.g3-topbar {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 100%;
height: 48px;
flex-shrink: 0;
/* border-bottom: 1px solid var(--g3-layout-divider-color); */
padding: 0 10px;
border-radius: 0 0 6px 6px;
background-color: #fff;
}
.g3-topbar-right {
display: flex;
align-items: center;
justify-content: center;
padding: 0px 4px;
}
.company-name {
font-size: 14px;
font-weight: bold;
color: #333;
}
.topbar-divider {
width: 1px;
height: 16px;
background-color: var(--g3-layout-divider-color);
margin: 0px 12px;
}
.user-info {
display: flex;
align-items: center;
}
.user-info > div {
min-width: 157px;
}
.user-card {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
outline: none;
box-shadow: none;
.name {
margin: 0 6px;
}
.avatar {
width: 32px;
height: 32px;
background-color: rgba(204, 204, 204, 1);
border-radius: 50%;
}
}
.user-dropdown {
padding: 8px 14px;
.avatar {
width: 34px;
height: 34px;
background-color: rgba(204, 204, 204, 1);
margin-right: 12px;
}
.name {
display: flex;
font-size: 16px;
font-weight: 500;
color: rgb(31, 34, 37);
margin-bottom: 3px;
}
.orgid {
display: flex;
align-items: center;
font-size: 12px;
color: rgb(118, 124, 130);
}
}
:deep(.el-dropdown-menu__item) {
padding: 12px 20px;
}
.g3flex {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.g3flex > div {
display: flex;
align-items: center;
}
.g3grzx {
margin-right: 4px;
display: flex;
align-items: center;
}
:deep(.el-dropdown-menu__item:not(.is-disabled):focus),
:deep(.el-dropdown-menu__item:not(.is-disabled):hover) {
background-color: rgba(243, 243, 245, 1);
color: unset;
border-radius: 8px;
}
.g3color {
color: rgb(51, 54, 57);
}
.g3lable {
font-size: 14px;
font-family:
v-sans,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol";
}
.g3logout {
display: flex;
justify-content: center;
margin-top: 14px;
margin-bottom: 8px;
}
.g3logout > div {
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
padding: 0px 14px;
font-size: 14px;
height: 34px;
border-radius: 8px;
color: rgb(118, 124, 130);
background-color: rgba(46, 51, 56, 0.05);
cursor: pointer;
}
.g3logoutsvg {
display: flex;
align-items: center;
margin-right: 6px;
color: rgb(118, 124, 130);
}
.g3hr {
background-color: #ebeef5;
width: 100%;
height: 1px;
}
</style>