按设计稿改版登录后的全部页面,登录页与 Logo 资源未改动。 - 全局外壳:浅色侧边栏与分组菜单、左下角用户卡片、Ctrl/⌘K 搜索面板、 测试模式提示条、深色模式配色、窄屏适配 - 共用组件:页头 PageHeader、托盘流程条 FlowStepper、扫码反馈 ScanFeedback、 缩短单号 DocNo、托盘档案链接 PalletCodeLink、图标 SfIcon、上级资料选择 MasterRefSelect,以及编号段解析等工具 - 工作台重做;新增托盘档案页;托盘追溯增加单托盘入口,报表托盘号可跳转档案 - 基础资料维护:各页加指标条、名称代替 ID、详情面板;三步式批量导入预览; 分配用户对话框标注增减;7 个新增/编辑对话框统一布局 - 业务管理:发货出库、仓库入库、工厂装填、客户现场使用、退回仓库改为 列表 + 详情面板 + 全屏作业区;写入流程、幂等键与防串单逻辑保持不变 - 修复入库页扫码反馈条因同名状态变量遮蔽组件而不显示的问题,并新增契约检查 - 新增入库粘贴解析、退回、装填的行为测试与现场使用页新流程测试, 同步更新业务与工作台契约脚本 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
383 lines
10 KiB
Vue
383 lines
10 KiB
Vue
<template>
|
||
<div class="navbar" :class="'nav' + settingsStore.navType">
|
||
<hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
||
<breadcrumb v-if="settingsStore.navType == 1" id="breadcrumb-container" class="breadcrumb-container" />
|
||
<top-nav v-if="settingsStore.navType == 2" id="topmenu-container" class="topmenu-container" />
|
||
<template v-if="settingsStore.navType == 3">
|
||
<logo v-show="settingsStore.sidebarLogo" :collapse="false"></logo>
|
||
<top-bar id="topbar-container" class="topbar-container" />
|
||
</template>
|
||
|
||
<div class="right-menu">
|
||
<button type="button" class="right-menu-item search-trigger" aria-label="搜索或扫码(Ctrl K)" @click="openSearch">
|
||
<sf-icon name="search" :size="15" />
|
||
<span class="search-trigger__text">搜索或扫码</span>
|
||
<kbd class="search-trigger__kbd">Ctrl K</kbd>
|
||
</button>
|
||
<test-batch-mode-switch class="right-menu-item test-batch-mode" aria-label="测试数据模式" />
|
||
<template v-if="appStore.device !== 'mobile'">
|
||
<el-tooltip content="全屏" effect="dark" placement="bottom">
|
||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||
</el-tooltip>
|
||
|
||
<el-tooltip :content="settingsStore.isDark ? '切换浅色模式' : '切换深色模式'" effect="dark" placement="bottom">
|
||
<div class="right-menu-item hover-effect theme-switch-wrapper" @click="toggleTheme">
|
||
<svg-icon v-if="settingsStore.isDark" icon-class="sunny" />
|
||
<svg-icon v-if="!settingsStore.isDark" icon-class="moon" />
|
||
</div>
|
||
</el-tooltip>
|
||
|
||
<el-tooltip content="布局大小" effect="dark" placement="bottom">
|
||
<size-select id="size-select" class="right-menu-item hover-effect" />
|
||
</el-tooltip>
|
||
</template>
|
||
|
||
<el-dropdown v-if="!sidebarShowsUser" @command="handleCommand" class="avatar-container right-menu-item hover-effect" trigger="hover">
|
||
<div class="avatar-wrapper">
|
||
<img :src="userStore.avatar" class="user-avatar" alt="" />
|
||
<span class="user-nickname"> {{ userStore.nickName }} </span>
|
||
</div>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<router-link to="/user/profile">
|
||
<el-dropdown-item>个人中心</el-dropdown-item>
|
||
</router-link>
|
||
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
|
||
<span>布局设置</span>
|
||
</el-dropdown-item>
|
||
<el-dropdown-item divided command="logout">
|
||
<span>退出登录</span>
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ElMessageBox } from 'element-plus'
|
||
import Breadcrumb from '@/components/Breadcrumb'
|
||
import TopNav from '@/components/TopNav'
|
||
import TopBar from './TopBar'
|
||
import Logo from './Sidebar/Logo'
|
||
import Hamburger from '@/components/Hamburger'
|
||
import Screenfull from '@/components/Screenfull'
|
||
import SizeSelect from '@/components/SizeSelect'
|
||
import SfIcon from '@/components/SfIcon'
|
||
import TestBatchModeSwitch from '@/components/TestBatchModeSwitch'
|
||
import useAppStore from '@/store/modules/app'
|
||
import useUserStore from '@/store/modules/user'
|
||
import useSettingsStore from '@/store/modules/settings'
|
||
|
||
const appStore = useAppStore()
|
||
const userStore = useUserStore()
|
||
const settingsStore = useSettingsStore()
|
||
// 左侧导航底部已展示当前用户时,顶部不再重复显示头像
|
||
const sidebarShowsUser = computed(() => appStore.device !== 'mobile' && !appStore.sidebar.hide && settingsStore.navType == 1)
|
||
|
||
function toggleSideBar() {
|
||
appStore.toggleSideBar()
|
||
}
|
||
|
||
function handleCommand(command) {
|
||
switch (command) {
|
||
case "setLayout":
|
||
setLayout()
|
||
break
|
||
case "logout":
|
||
logout()
|
||
break
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
|
||
function logout() {
|
||
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
userStore.logOut().then(() => {
|
||
location.href = '/index'
|
||
})
|
||
}).catch(() => { })
|
||
}
|
||
|
||
const emits = defineEmits(['setLayout', 'openSearch'])
|
||
function setLayout() {
|
||
emits('setLayout')
|
||
}
|
||
|
||
function openSearch() {
|
||
emits('openSearch')
|
||
}
|
||
|
||
async function toggleTheme(event) {
|
||
const x = event?.clientX || window.innerWidth / 2
|
||
const y = event?.clientY || window.innerHeight / 2
|
||
const wasDark = settingsStore.isDark
|
||
|
||
const isReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||
const isSupported = document.startViewTransition && !isReducedMotion
|
||
|
||
if (!isSupported) {
|
||
settingsStore.toggleTheme()
|
||
return
|
||
}
|
||
|
||
try {
|
||
const transition = document.startViewTransition(async () => {
|
||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||
settingsStore.toggleTheme()
|
||
await nextTick()
|
||
})
|
||
await transition.ready
|
||
|
||
const endRadius = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y))
|
||
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`]
|
||
document.documentElement.animate(
|
||
{
|
||
clipPath: !wasDark ? [...clipPath].reverse() : clipPath
|
||
}, {
|
||
duration: 650,
|
||
easing: "cubic-bezier(0.4, 0, 0.2, 1)",
|
||
fill: "forwards",
|
||
pseudoElement: !wasDark ? "::view-transition-old(root)" : "::view-transition-new(root)"
|
||
}
|
||
)
|
||
await transition.finished
|
||
} catch (error) {
|
||
console.warn("View transition failed, falling back to immediate toggle:", error)
|
||
settingsStore.toggleTheme()
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang='scss' scoped>
|
||
.navbar.nav3 {
|
||
.hamburger-container {
|
||
display: none !important;
|
||
}
|
||
}
|
||
|
||
.navbar {
|
||
height: 56px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
background: var(--navbar-bg);
|
||
border-bottom: 1px solid var(--sf-border-soft, #eef0f2);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 0 12px 0 8px;
|
||
box-sizing: border-box;
|
||
|
||
.hamburger-container {
|
||
height: 34px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
color: var(--sf-text-3, #5b616e);
|
||
transition: background 0.15s;
|
||
-webkit-tap-highlight-color: transparent;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
margin-right: 4px;
|
||
|
||
&:hover {
|
||
background: var(--sf-hover, rgba(15, 17, 21, 0.045));
|
||
}
|
||
|
||
:deep(div) {
|
||
padding: 0 8px !important;
|
||
}
|
||
|
||
:deep(.hamburger) {
|
||
width: 18px;
|
||
height: 18px;
|
||
}
|
||
}
|
||
|
||
.breadcrumb-container {
|
||
flex-shrink: 0;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.topmenu-container {
|
||
position: absolute;
|
||
left: 50px;
|
||
}
|
||
|
||
.topbar-container {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
overflow: hidden;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.right-menu {
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
margin-left: auto;
|
||
min-width: 0;
|
||
flex-shrink: 1;
|
||
overflow: hidden;
|
||
|
||
&:focus {
|
||
outline: none;
|
||
}
|
||
|
||
.right-menu-item {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 34px;
|
||
height: 34px;
|
||
padding: 0 8px;
|
||
border-radius: 8px;
|
||
font-size: 17px;
|
||
color: var(--sf-text-3, #5b616e);
|
||
vertical-align: middle;
|
||
|
||
&.hover-effect {
|
||
cursor: pointer;
|
||
transition: background 0.15s, color 0.15s;
|
||
|
||
&:hover {
|
||
background: var(--sf-hover, rgba(15, 17, 21, 0.045));
|
||
color: var(--sf-text-1, #0f1115);
|
||
}
|
||
}
|
||
|
||
&.theme-switch-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
}
|
||
|
||
.search-trigger {
|
||
gap: 8px;
|
||
width: 248px;
|
||
height: 34px;
|
||
margin-right: 8px;
|
||
padding: 0 8px 0 10px;
|
||
border: 1px solid var(--sf-border-strong, #dde0e4);
|
||
background: var(--sf-surface, #fff);
|
||
color: var(--sf-text-4, #6b717d);
|
||
font: inherit;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
justify-content: flex-start;
|
||
transition: border-color 0.15s;
|
||
|
||
&:hover {
|
||
border-color: #c3c8cf;
|
||
}
|
||
|
||
.search-trigger__text {
|
||
flex: 1;
|
||
text-align: left;
|
||
}
|
||
|
||
.search-trigger__kbd {
|
||
padding: 1px 6px;
|
||
border: 1px solid var(--sf-border, #e7e9ec);
|
||
border-radius: 5px;
|
||
font-family: var(--sf-font-mono);
|
||
font-size: 11px;
|
||
color: var(--sf-text-4, #6b717d);
|
||
}
|
||
}
|
||
|
||
.avatar-container {
|
||
margin-left: 4px;
|
||
padding: 0;
|
||
|
||
.avatar-wrapper {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 2px 8px 2px 2px;
|
||
border-radius: 18px;
|
||
transition: background 0.15s ease;
|
||
|
||
&:hover {
|
||
background: var(--sf-hover, rgba(15, 17, 21, 0.045));
|
||
}
|
||
|
||
.user-avatar {
|
||
cursor: pointer;
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
border: 1px solid var(--sf-border, #e7e9ec);
|
||
}
|
||
|
||
.user-nickname {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--sf-text-1, #0f1115);
|
||
}
|
||
}
|
||
}
|
||
|
||
.test-batch-mode {
|
||
display: flex;
|
||
align-items: center;
|
||
min-width: 0;
|
||
margin-right: 6px;
|
||
font-size: 14px;
|
||
color: inherit;
|
||
}
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 1200px) {
|
||
.navbar .right-menu .search-trigger {
|
||
width: auto;
|
||
|
||
.search-trigger__text,
|
||
.search-trigger__kbd {
|
||
display: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 767px) {
|
||
.navbar {
|
||
.breadcrumb-container {
|
||
display: none;
|
||
}
|
||
|
||
.right-menu {
|
||
overflow: visible;
|
||
|
||
.test-batch-mode {
|
||
padding: 0;
|
||
}
|
||
|
||
.search-trigger {
|
||
margin-right: 2px;
|
||
border: 0;
|
||
}
|
||
|
||
.avatar-container .avatar-wrapper .user-nickname {
|
||
display: none;
|
||
}
|
||
|
||
.avatar-container .avatar-wrapper .user-avatar {
|
||
margin-right: 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|