Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
F
factory_front
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
qinjianhui
factory_front
Commits
1a80b5cf
Commit
1a80b5cf
authored
Feb 13, 2025
by
qinjianhui
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 顶部导航栏添加页面标签栏
parent
c5456bd8
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
348 additions
and
15 deletions
+348
-15
src/components/NavMenu.vue
+293
-6
src/router/index.ts
+27
-0
src/router/menu.ts
+20
-4
src/views/Home.vue
+1
-1
src/views/order/pod/index.vue
+7
-4
No files found.
src/components/NavMenu.vue
View file @
1a80b5cf
<
template
>
<div>
<div
class=
"nav-menu"
>
<div
class=
"header-logo"
>
<img
src=
"../assets/images/factory-logo.png"
alt=
"logo"
/>
...
...
@@ -46,13 +47,66 @@
</span>
<
template
#
dropdown
>
<el-dropdown-menu>
<el-dropdown-item
@
click=
"resetPassword"
>
修改密码
</el-dropdown-item>
<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>
<el-dialog
v-model=
"dialogVisible"
title=
"修改密码"
...
...
@@ -100,28 +154,51 @@
</el-dialog>
</template>
<
script
setup
lang=
"ts"
>
import
{
User
,
ArrowDown
}
from
'@element-plus/icons-vue'
import
{
ref
,
reactive
}
from
'vue'
import
{
useRoute
}
from
'vue-router'
import
{
User
,
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
userUserStore
from
'@/store/user'
import
type
{
FormRules
}
from
'element-plus'
import
{
useValue
}
from
'@/utils/hooks/useValue'
import
{
changePasswordApi
}
from
'@/api/auth'
interface
PasswordForm
{
oldPwd
:
string
newPwd
:
string
confirmPwd
?:
string
}
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
userStore
=
userUserStore
()
const
userInfo
=
userStore
.
user
const
dialogVisible
=
ref
(
false
)
// 密码form
const
[
passwordForm
,
resetPasswordForm
]
=
useValue
<
PasswordForm
>
({}
as
PasswordForm
)
const
[
passwordForm
,
resetPasswordForm
]
=
useValue
<
PasswordForm
>
(
{}
as
PasswordForm
,
)
const
passwordFormRef
=
ref
()
const
rules
=
reactive
<
FormRules
<
PasswordForm
>>
({
oldPwd
:
[
...
...
@@ -186,8 +263,218 @@ const submitForm = async () => {
// 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
>
<
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
{
height
:
60px
;
background-color
:
#001529
;
...
...
src/router/index.ts
View file @
1a80b5cf
...
...
@@ -30,38 +30,65 @@ const router = createRouter({
children
:
[
{
path
:
'/dashboard'
,
meta
:
{
title
:
'概览'
,
},
component
:
Dashboard
,
},
{
path
:
'/order/list'
,
meta
:
{
title
:
'定制订单'
,
},
component
:
OrderList
,
},
{
path
:
'/pod-order/list'
,
meta
:
{
title
:
'POD订单'
,
},
component
:
PodOrderList
,
},
{
path
:
'/pod-delivery-note/list'
,
meta
:
{
title
:
'POD发货单'
,
},
component
:
PodDeliveryNoteList
,
},
{
path
:
'/production/complete'
,
meta
:
{
title
:
'生产完成'
,
},
component
:
ProductionComplete
,
},
{
path
:
'/system/user'
,
meta
:
{
title
:
'用户管理'
,
},
component
:
UserPage
,
},
{
path
:
'/system/delivery-note'
,
meta
:
{
title
:
'定制发货单'
,
},
component
:
DeliveryNotePage
,
},
{
path
:
'/account/statement-note'
,
meta
:
{
title
:
'对账单'
,
},
component
:
AccountStatementNote
,
},
{
path
:
'/typesetting-management/list'
,
meta
:
{
title
:
'打版管理'
,
},
component
:
TypeseetingManagement
,
},
],
...
...
src/router/menu.ts
View file @
1a80b5cf
...
...
@@ -12,37 +12,53 @@ const menu: MenuItem[] = [
label
:
'概览'
,
},
{
index
:
'
/order/list
'
,
index
:
'
1
'
,
id
:
2
,
label
:
'订单'
,
children
:
[
{
index
:
'/order/list'
,
id
:
1
,
label
:
'定制订单'
,
},
{
index
:
'/pod-order/list'
,
id
:
7
,
label
:
'POD订单'
,
},
],
},
{
index
:
'/account/statement-note'
,
id
:
3
,
label
:
'对账单'
,
},
{
index
:
'
/system/delivery-note
'
,
index
:
'
2
'
,
id
:
4
,
label
:
'发货单'
,
children
:
[
{
index
:
'/system/delivery-note'
,
id
:
1
,
label
:
'定制发货单'
,
},
{
index
:
'/pod-delivery-note/list'
,
id
:
8
,
id
:
2
,
label
:
'POD发货单'
,
},
],
},
{
index
:
'/typesetting-management/list'
,
id
:
5
,
label
:
'打版管理'
,
},
{
index
:
''
,
index
:
'
3
'
,
id
:
6
,
label
:
'系统设置'
,
children
:
[
...
...
src/views/Home.vue
View file @
1a80b5cf
...
...
@@ -20,7 +20,7 @@ import NavMenu from '@/components/NavMenu.vue'
.container
{
flex
:
1
;
padding
:
10px
;
padding
:
0
10px
10px
;
background-color
:
#f6f6f6
;
overflow
:
hidden
;
}
...
...
src/views/order/pod/index.vue
View file @
1a80b5cf
...
...
@@ -93,9 +93,6 @@
<ElFormItem>
<ElButton
type=
"primary"
@
click=
"search"
>
查询
</ElButton>
</ElFormItem>
<ElFormItem>
<ElButton
@
click=
"resetSearchForm"
>
重置
</ElButton>
</ElFormItem>
</ElForm>
</div>
<div
class=
"header-filter-tab"
>
...
...
@@ -617,7 +614,7 @@ const changeTab = (item: Tab) => {
searchForm
.
value
.
timeType
=
null
search
()
}
const
[
searchForm
,
resetSearchForm
]
=
useValue
<
SearchForm
>
({
const
[
searchForm
]
=
useValue
<
SearchForm
>
({
timeType
:
null
,
shopNumber
:
''
,
...
...
@@ -978,6 +975,12 @@ onMounted(() => {
}
)
<
/script
>
<
style
lang
=
"scss"
scoped
>
.
header
-
filter
-
form
{
:
deep
(.
el
-
form
-
item
)
{
margin
-
right
:
14
px
;
margin
-
bottom
:
10
px
;
}
}
.
tabs
{
display
:
flex
;
align
-
items
:
center
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment