Commit 1a80b5cf by qinjianhui

feat: 顶部导航栏添加页面标签栏

parent c5456bd8
<template> <template>
<div class="nav-menu"> <div>
<div class="header-logo"> <div class="nav-menu">
<img src="../assets/images/factory-logo.png" alt="logo" /> <div class="header-logo">
</div> <img src="../assets/images/factory-logo.png" alt="logo" />
<!-- 导航栏 --> </div>
<el-menu <!-- 导航栏 -->
class="el-menu-demo" <el-menu
mode="horizontal" class="el-menu-demo"
background-color="#001529" mode="horizontal"
text-color="#fff" background-color="#001529"
:default-active="defaultActive" text-color="#fff"
router :default-active="defaultActive"
> router
<template v-for="item in menuList"> >
<el-menu-item <template v-for="item in menuList">
v-if="!item.children"
:key="item.id"
:index="item.index"
>{{ item.label }}</el-menu-item
>
<el-sub-menu v-else :key="item.index" :index="item.index">
<template #title>
<span class="label">{{ item.label }}</span>
</template>
<el-menu-item <el-menu-item
v-for="sub in item.children" v-if="!item.children"
:key="sub.id" :key="item.id"
:index="sub.index" :index="item.index"
>{{ sub.label }}</el-menu-item >{{ item.label }}</el-menu-item
> >
</el-sub-menu> <el-sub-menu v-else :key="item.index" :index="item.index">
</template> <template #title>
</el-menu> <span class="label">{{ item.label }}</span>
<div v-if="userInfo" class="user-info"> </template>
<span class="user-avatar"> <el-menu-item
<el-icon><User /></el-icon> v-for="sub in item.children"
</span> :key="sub.id"
<el-dropdown style="color: #fff; font-size: 14px"> :index="sub.index"
<span class="el-dropdown-link"> >{{ sub.label }}</el-menu-item
<el-icon style="vertical-align: middle"><User /></el-icon> >
{{ userInfo.account }} </el-sub-menu>
<el-icon style="vertical-align: middle" class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="resetPassword">修改密码</el-dropdown-item>
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template> </template>
</el-dropdown> </el-menu>
<div v-if="userInfo" class="user-info">
<span class="user-avatar">
<el-icon><User /></el-icon>
</span>
<el-dropdown style="color: #fff; font-size: 14px">
<span class="el-dropdown-link">
<el-icon style="vertical-align: middle"><User /></el-icon>
{{ userInfo.account }}
<el-icon style="vertical-align: middle" class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="resetPassword"
>修改密码</el-dropdown-item
>
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div ref="tabsContainer" class="nav-tabs">
<el-icon
v-if="showScrollArrows"
class="nav-tabs-arrow left"
@click="scrollTabs('left')"
>
<ArrowLeft />
</el-icon>
<div
ref="tabsWrapper"
class="tabs-scroll-container"
:style="{ width: showScrollArrows ? 'calc(100% - 80px)' : '100%' }"
>
<div
class="tabs-wrapper"
:style="{ transform: `translateX(${scrollOffset}px)` }"
>
<div
v-for="item in tabs"
:key="item.name"
class="tabs-node"
:style="{
backgroundColor: item.name === activeTab ? '#409EFF' : '',
color: item.name === activeTab ? '#fff' : '',
}"
@click="activeTab = item.name"
>
<div class="nav-tabs-node_label">
<span>{{ item.title }}</span>
<el-icon
v-if="tabs.length > 1"
class="el-icon-close"
@click.stop="removeTab(item.name)"
>
<CircleClose />
</el-icon>
</div>
</div>
</div>
</div>
<el-icon
v-if="showScrollArrows"
class="nav-tabs-arrow right"
@click="scrollTabs('right')"
>
<ArrowRight />
</el-icon>
</div> </div>
</div> </div>
<el-dialog <el-dialog
...@@ -100,28 +154,51 @@ ...@@ -100,28 +154,51 @@
</el-dialog> </el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { User, ArrowDown } from '@element-plus/icons-vue' import {
import { ref, reactive } from 'vue' User,
import { useRoute } from 'vue-router' ArrowDown,
CircleClose,
ArrowLeft,
ArrowRight,
} from '@element-plus/icons-vue'
import {
ref,
reactive,
watch,
computed,
nextTick,
onMounted,
onUnmounted,
} from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Menu from '@/router/menu' import Menu from '@/router/menu'
import userUserStore from '@/store/user' import userUserStore from '@/store/user'
import type { FormRules } from 'element-plus' import type { FormRules } from 'element-plus'
import { useValue } from '@/utils/hooks/useValue' import { useValue } from '@/utils/hooks/useValue'
import { changePasswordApi } from '@/api/auth' import { changePasswordApi } from '@/api/auth'
interface PasswordForm { interface PasswordForm {
oldPwd: string oldPwd: string
newPwd: string newPwd: string
confirmPwd?: string confirmPwd?: string
} }
const route = useRoute() const route = useRoute()
const defaultActive = ref(route.path) // 使用 computed 替代直接赋值
const defaultActive = computed({
get: () => route.path,
set: (val) => {
router.push(val)
},
})
const menuList = reactive(Menu) const menuList = reactive(Menu)
const userStore = userUserStore() const userStore = userUserStore()
const userInfo = userStore.user const userInfo = userStore.user
const dialogVisible = ref(false) const dialogVisible = ref(false)
// 密码form // 密码form
const [passwordForm, resetPasswordForm] = useValue<PasswordForm>({} as PasswordForm) const [passwordForm, resetPasswordForm] = useValue<PasswordForm>(
{} as PasswordForm,
)
const passwordFormRef = ref() const passwordFormRef = ref()
const rules = reactive<FormRules<PasswordForm>>({ const rules = reactive<FormRules<PasswordForm>>({
oldPwd: [ oldPwd: [
...@@ -173,21 +250,231 @@ const submitForm = async () => { ...@@ -173,21 +250,231 @@ const submitForm = async () => {
return return
} }
try { try {
const res = await changePasswordApi(passwordForm.value) const res = await changePasswordApi(passwordForm.value)
ElMessage({ ElMessage({
message: res.message, message: res.message,
type: 'success', type: 'success',
offset: window.innerHeight / 2, offset: window.innerHeight / 2,
}) })
dialogVisible.value = false dialogVisible.value = false
userStore.logout() userStore.logout()
window.location.reload() window.location.reload()
} catch (e) { } catch (e) {
// showError(e) // showError(e)
} }
} }
const tabs = ref<Array<{ name: string; title: string; path: string }>>([])
const activeTab = ref('')
const router = useRouter()
// 添加标签页
const addTab = () => {
const { path, meta } = route
const existingTab = tabs.value.find((tab) => tab.path === path)
if (!existingTab) {
const newTab = {
name: path,
title: (meta.title as string) || '未命名页面',
path: path,
}
tabs.value.push(newTab)
}
activeTab.value = path
}
// 移除标签页
const removeTab = (targetName: string) => {
const tabIndex = tabs.value.findIndex((tab) => tab.name === targetName)
if (tabIndex !== -1) {
tabs.value.splice(tabIndex, 1)
// 如果关闭的是当前激活的标签,则切换到最后一个标签
if (activeTab.value === targetName) {
const lastTab = tabs.value[tabs.value.length - 1]
if (lastTab) {
nextTick(() => {
activeTab.value = lastTab.name
router.push(lastTab.path)
})
}
}
}
}
const tabsContainer = ref<HTMLDivElement | null>(null)
const tabsWrapper = ref<HTMLDivElement | null>(null)
const scrollOffset = ref(0)
const scrollStep = 200 // 每次滚动的像素数
const showScrollArrows = ref(false)
// 检查是否需要显示滚动箭头
const checkScrollArrows = () => {
nextTick(() => {
if (!tabsContainer.value || !tabsWrapper.value) return
const containerWidth = tabsContainer.value.clientWidth
const wrapperWidth = tabsWrapper.value.scrollWidth
// 如果包装器宽度大于容器宽度,显示箭头
showScrollArrows.value = wrapperWidth > containerWidth
// 调整滚动位置,确保不会超出边界
if (showScrollArrows.value) {
const maxScroll = -(wrapperWidth - containerWidth)
// 如果当前滚动位置已经超出最大可滚动范围,重新调整
if (scrollOffset.value < maxScroll) {
scrollOffset.value = maxScroll
}
// 确保不会滚动到容器左侧之外
if (scrollOffset.value > 0) {
scrollOffset.value = 0
}
} else {
// 如果不需要滚动,重置滚动位置
scrollOffset.value = 0
}
})
}
// 添加窗口大小监听器
const resizeObserver = new ResizeObserver(checkScrollArrows)
const scrollTabs = (direction: 'left' | 'right') => {
if (!tabsContainer.value || !tabsWrapper.value) return
const containerWidth = tabsContainer.value.clientWidth
const wrapperWidth = tabsWrapper.value.scrollWidth
if (direction === 'left') {
// 向左滚动(显示前面的标签)
scrollOffset.value = Math.min(0, scrollOffset.value + scrollStep)
} else {
// 向右滚动(显示后面的标签)
const maxScroll = -(wrapperWidth - containerWidth)
scrollOffset.value = Math.max(maxScroll, scrollOffset.value - scrollStep)
}
}
// 确保活动标签始终在可视范围内
const ensureActiveTabVisible = () => {
nextTick(() => {
if (!tabsContainer.value || !tabsWrapper.value) return
const activeTab = tabsWrapper.value.querySelector(
'.tabs-node[style*="background-color: rgb(64, 158, 255)"]',
) as HTMLElement
if (!activeTab) return
const containerRect = tabsContainer.value.getBoundingClientRect()
const activeTabRect = activeTab.getBoundingClientRect()
const wrapperRect = tabsWrapper.value.getBoundingClientRect()
// 计算活动标签相对于包装器的位置
const relativeLeft = activeTabRect.left - wrapperRect.left
const relativeRight = activeTabRect.right - wrapperRect.left
// 如果标签部分或完全在容器外
if (relativeLeft < 0 || relativeRight > containerRect.width) {
// 调整滚动位置,使活动标签居中
const centerOffset = containerRect.width / 2 - activeTabRect.width / 2
scrollOffset.value = -relativeLeft + centerOffset
// 重新检查滚动边界
checkScrollArrows()
}
})
}
// 监听路由变化,自动添加标签
watch(
() => route.path,
() => {
addTab()
},
{ immediate: true },
)
// 点击标签时切换路由
watch(activeTab, (newTab) => {
if (newTab) {
router.push(newTab)
}
// 监听活动标签变化时调整滚动
ensureActiveTabVisible()
checkScrollArrows()
})
watch(tabs, checkScrollArrows)
// 初始化时确保活动标签可见
onMounted(() => {
// 初始检查
checkScrollArrows()
// 监听容器大小变化
if (tabsContainer.value) {
resizeObserver.observe(tabsContainer.value)
}
})
onUnmounted(() => {
// 清理监听器
resizeObserver.disconnect()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.nav-tabs {
background-color: #f5f5f5;
padding: 10px 20px;
font-size: 14px;
display: flex;
align-items: center;
}
.tabs-scroll-container {
overflow: hidden;
flex-grow: 1;
}
.tabs-wrapper {
display: flex;
align-items: center;
gap: 10px;
transition: transform 0.3s ease;
}
.nav-tabs-arrow {
cursor: pointer;
color: #409eff;
font-size: 20px;
&.left {
margin-right: 10px;
}
&.right {
margin-left: 10px;
}
&:hover {
color: #66b1ff;
}
}
.tabs-node {
cursor: pointer;
padding: 4px 6px;
border-radius: 4px;
transition: all 0.3s;
box-shadow: var(--el-box-shadow-light);
background-color: #fff;
&:hover {
background-color: #c6e2ff;
}
}
.nav-tabs-node_label {
display: flex;
align-items: center;
gap: 2px;
white-space: nowrap;
}
.nav-menu { .nav-menu {
height: 60px; height: 60px;
background-color: #001529; background-color: #001529;
......
...@@ -30,38 +30,65 @@ const router = createRouter({ ...@@ -30,38 +30,65 @@ const router = createRouter({
children: [ children: [
{ {
path: '/dashboard', path: '/dashboard',
meta: {
title: '概览',
},
component: Dashboard, component: Dashboard,
}, },
{ {
path: '/order/list', path: '/order/list',
meta: {
title: '定制订单',
},
component: OrderList, component: OrderList,
}, },
{ {
path: '/pod-order/list', path: '/pod-order/list',
meta: {
title: 'POD订单',
},
component: PodOrderList, component: PodOrderList,
}, },
{ {
path: '/pod-delivery-note/list', path: '/pod-delivery-note/list',
meta: {
title: 'POD发货单',
},
component: PodDeliveryNoteList, component: PodDeliveryNoteList,
}, },
{ {
path: '/production/complete', path: '/production/complete',
meta: {
title: '生产完成',
},
component: ProductionComplete, component: ProductionComplete,
}, },
{ {
path: '/system/user', path: '/system/user',
meta: {
title: '用户管理',
},
component: UserPage, component: UserPage,
}, },
{ {
path: '/system/delivery-note', path: '/system/delivery-note',
meta: {
title: '定制发货单',
},
component: DeliveryNotePage, component: DeliveryNotePage,
}, },
{ {
path: '/account/statement-note', path: '/account/statement-note',
meta: {
title: '对账单',
},
component: AccountStatementNote, component: AccountStatementNote,
}, },
{ {
path: '/typesetting-management/list', path: '/typesetting-management/list',
meta: {
title: '打版管理',
},
component: TypeseetingManagement, component: TypeseetingManagement,
}, },
], ],
......
...@@ -12,37 +12,53 @@ const menu: MenuItem[] = [ ...@@ -12,37 +12,53 @@ const menu: MenuItem[] = [
label: '概览', label: '概览',
}, },
{ {
index: '/order/list', index: '1',
id: 2, id: 2,
label: '订单', label: '订单',
children: [
{
index: '/order/list',
id: 1,
label: '定制订单',
},
{
index: '/pod-order/list',
id: 7,
label: 'POD订单',
},
],
}, },
{
index: '/pod-order/list',
id: 7,
label: 'POD订单',
},
{ {
index: '/account/statement-note', index: '/account/statement-note',
id: 3, id: 3,
label: '对账单', label: '对账单',
}, },
{ {
index: '/system/delivery-note', index: '2',
id: 4, id: 4,
label: '发货单', label: '发货单',
children: [
{
index: '/system/delivery-note',
id: 1,
label: '定制发货单',
},
{
index: '/pod-delivery-note/list',
id: 2,
label: 'POD发货单',
},
],
}, },
{
index: '/pod-delivery-note/list',
id: 8,
label: 'POD发货单',
},
{ {
index: '/typesetting-management/list', index: '/typesetting-management/list',
id: 5, id: 5,
label: '打版管理', label: '打版管理',
}, },
{ {
index: '', index: '3',
id: 6, id: 6,
label: '系统设置', label: '系统设置',
children: [ children: [
......
...@@ -20,7 +20,7 @@ import NavMenu from '@/components/NavMenu.vue' ...@@ -20,7 +20,7 @@ import NavMenu from '@/components/NavMenu.vue'
.container { .container {
flex: 1; flex: 1;
padding: 10px; padding: 0 10px 10px;
background-color: #f6f6f6; background-color: #f6f6f6;
overflow: hidden; overflow: hidden;
} }
......
...@@ -93,9 +93,6 @@ ...@@ -93,9 +93,6 @@
<ElFormItem> <ElFormItem>
<ElButton type="primary" @click="search">查询</ElButton> <ElButton type="primary" @click="search">查询</ElButton>
</ElFormItem> </ElFormItem>
<ElFormItem>
<ElButton @click="resetSearchForm">重置</ElButton>
</ElFormItem>
</ElForm> </ElForm>
</div> </div>
<div class="header-filter-tab"> <div class="header-filter-tab">
...@@ -617,7 +614,7 @@ const changeTab = (item: Tab) => { ...@@ -617,7 +614,7 @@ const changeTab = (item: Tab) => {
searchForm.value.timeType = null searchForm.value.timeType = null
search() search()
} }
const [searchForm, resetSearchForm] = useValue<SearchForm>({ const [searchForm] = useValue<SearchForm>({
timeType: null, timeType: null,
shopNumber: '', shopNumber: '',
...@@ -978,6 +975,12 @@ onMounted(() => { ...@@ -978,6 +975,12 @@ onMounted(() => {
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.header-filter-form {
:deep(.el-form-item) {
margin-right: 14px;
margin-bottom: 10px;
}
}
.tabs { .tabs {
display: flex; display: flex;
align-items: center; align-items: center;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment