All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m30s
运单列表/工作台/返佣四态展示,注册归因与客户表单改绑代理,侧栏与占位邮箱优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
532 lines
12 KiB
Vue
532 lines
12 KiB
Vue
<template>
|
|
<aside
|
|
class="agent-side-rail"
|
|
:class="{
|
|
'is-collapsed': collapsed,
|
|
'is-compact': compactLayout,
|
|
'is-animating': animating,
|
|
}"
|
|
:style="{
|
|
width: `${railWidth}px`,
|
|
'--asr-shell-width': `${AGENT_SIDE_RAIL_EXPANDED_WIDTH}px`,
|
|
}"
|
|
aria-label="渠道代理导航"
|
|
@transitionend="onRailTransitionEnd"
|
|
>
|
|
<div class="agent-side-rail__shell">
|
|
<a
|
|
class="agent-side-rail__brand"
|
|
href="/"
|
|
:title="t('brand.fullName')"
|
|
@click.prevent="goOfficialSite"
|
|
>
|
|
<img :src="navLogo" :alt="t('brand.name')" class="agent-side-rail__brand-logo" />
|
|
<span class="agent-side-rail__brand-name agent-side-rail__fade">{{ t('brand.name') }}</span>
|
|
</a>
|
|
|
|
<div class="agent-side-rail__scroll">
|
|
<section class="agent-side-rail__group is-first">
|
|
<a-tooltip
|
|
v-for="item in navItems"
|
|
:key="item.path"
|
|
:content="item.label"
|
|
position="right"
|
|
:disabled="!compactLayout"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="agent-side-rail__item"
|
|
:class="{ active: isActive(item.path) }"
|
|
:aria-current="isActive(item.path) ? 'page' : undefined"
|
|
@click="onNavigate(item.path)"
|
|
>
|
|
<span class="agent-side-rail__icon">
|
|
<component :is="item.icon" />
|
|
</span>
|
|
<span class="agent-side-rail__text agent-side-rail__fade">{{ item.label }}</span>
|
|
</button>
|
|
</a-tooltip>
|
|
</section>
|
|
</div>
|
|
|
|
<div class="agent-side-rail__dock">
|
|
<div class="agent-side-rail__account">
|
|
<div class="agent-side-rail__user">
|
|
<RightTopUser
|
|
variant="sider"
|
|
compact
|
|
dropdown-position="tl"
|
|
:account-menus="[]"
|
|
show-language-menu
|
|
:show-recharge-cta="false"
|
|
/>
|
|
</div>
|
|
<div class="agent-side-rail__account-meta agent-side-rail__fade">
|
|
<div class="agent-side-rail__account-name" :title="agentDisplayName">
|
|
{{ agentDisplayName }}
|
|
</div>
|
|
<div
|
|
v-if="agentDisplayCode"
|
|
class="agent-side-rail__account-code"
|
|
:title="agentDisplayCode"
|
|
>
|
|
{{ agentDisplayCode }}
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="agent-side-rail__toggle"
|
|
:aria-label="collapsed ? t('nav.sideRail.expand') : t('nav.sideRail.collapse')"
|
|
:title="collapsed ? t('nav.sideRail.expand') : t('nav.sideRail.collapse')"
|
|
@click="toggleCollapsed"
|
|
>
|
|
<icon-double-right v-if="collapsed" class="agent-side-rail__toggle-icon" />
|
|
<icon-double-left v-else class="agent-side-rail__toggle-icon" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, onMounted, onUnmounted, ref, type Component } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useI18n } from 'vue-i18n'
|
|
import {
|
|
IconDashboard,
|
|
IconDoubleLeft,
|
|
IconDoubleRight,
|
|
IconGift,
|
|
IconList,
|
|
IconPlusCircle,
|
|
IconShareInternal,
|
|
IconThunderbolt,
|
|
IconUserGroup,
|
|
} from '@arco-design/web-vue/es/icon'
|
|
import RightTopUser from '@/components/top-nav/RightTopUser.vue'
|
|
import { AgentPortalApi, type AgentDto } from '@/api/agent'
|
|
import {
|
|
CUSTOMER_SIDE_RAIL_COLLAPSED_WIDTH,
|
|
CUSTOMER_SIDE_RAIL_EXPANDED_WIDTH,
|
|
} from '@/utils/customer-portal-nav'
|
|
import navLogo from '@/assets/logo2.png'
|
|
|
|
const AGENT_SIDE_RAIL_COLLAPSED_WIDTH = CUSTOMER_SIDE_RAIL_COLLAPSED_WIDTH
|
|
const AGENT_SIDE_RAIL_EXPANDED_WIDTH = CUSTOMER_SIDE_RAIL_EXPANDED_WIDTH
|
|
const AGENT_SIDE_RAIL_STORAGE_KEY = 'oms.agentSideRail.collapsed'
|
|
const RAIL_ANIM_MS = 300
|
|
|
|
type AgentNavItem = {
|
|
path: string
|
|
label: string
|
|
icon: Component
|
|
}
|
|
|
|
const { t } = useI18n()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const collapsed = ref(readCollapsed())
|
|
const compactLayout = ref(collapsed.value)
|
|
const animating = ref(false)
|
|
let compactTimer = 0
|
|
|
|
const profile = ref<AgentDto | null>(null)
|
|
|
|
const railWidth = computed(() =>
|
|
collapsed.value ? AGENT_SIDE_RAIL_COLLAPSED_WIDTH : AGENT_SIDE_RAIL_EXPANDED_WIDTH,
|
|
)
|
|
|
|
const navItems = computed<AgentNavItem[]>(() => [
|
|
{
|
|
path: '/agent/workplace',
|
|
label: '工作台',
|
|
icon: IconDashboard,
|
|
},
|
|
{
|
|
path: '/agent/invite',
|
|
label: '邀请获客',
|
|
icon: IconShareInternal,
|
|
},
|
|
{
|
|
path: '/agent/priceCheck',
|
|
label: '运费查价',
|
|
icon: IconThunderbolt,
|
|
},
|
|
{
|
|
path: '/agent/placeOrder',
|
|
label: '运单下单',
|
|
icon: IconPlusCircle,
|
|
},
|
|
{
|
|
path: '/agent/customers',
|
|
label: '名下客户',
|
|
icon: IconUserGroup,
|
|
},
|
|
{
|
|
path: '/agent/orders',
|
|
label: '运单列表',
|
|
icon: IconList,
|
|
},
|
|
{
|
|
path: '/agent/commission',
|
|
label: '返佣明细',
|
|
icon: IconGift,
|
|
},
|
|
])
|
|
|
|
const agentDisplayName = computed(
|
|
() => profile.value?.agentName?.trim() || profile.value?.agentCode || '代理',
|
|
)
|
|
|
|
const agentDisplayCode = computed(() => {
|
|
const code = profile.value?.agentCode?.trim() || ''
|
|
const name = profile.value?.agentName?.trim() || ''
|
|
// 名称与编号相同时只展示一行,避免「一代 / 一代」重复
|
|
if (!code || code === name) return ''
|
|
return code
|
|
})
|
|
|
|
function readCollapsed(): boolean {
|
|
try {
|
|
return localStorage.getItem(AGENT_SIDE_RAIL_STORAGE_KEY) === '1'
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function writeCollapsed(value: boolean) {
|
|
try {
|
|
localStorage.setItem(AGENT_SIDE_RAIL_STORAGE_KEY, value ? '1' : '0')
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
function isActive(path: string) {
|
|
const current = route.path
|
|
return current === path || current.startsWith(`${path}/`)
|
|
}
|
|
|
|
function onNavigate(path: string) {
|
|
if (route.path === path) return
|
|
void router.push(path)
|
|
}
|
|
|
|
function goOfficialSite() {
|
|
window.location.href = '/'
|
|
}
|
|
|
|
function toggleCollapsed() {
|
|
animating.value = true
|
|
collapsed.value = !collapsed.value
|
|
writeCollapsed(collapsed.value)
|
|
if (compactTimer) window.clearTimeout(compactTimer)
|
|
if (collapsed.value) {
|
|
compactTimer = window.setTimeout(() => {
|
|
compactLayout.value = true
|
|
animating.value = false
|
|
}, RAIL_ANIM_MS)
|
|
} else {
|
|
compactLayout.value = false
|
|
compactTimer = window.setTimeout(() => {
|
|
animating.value = false
|
|
}, RAIL_ANIM_MS)
|
|
}
|
|
}
|
|
|
|
function onRailTransitionEnd(e: TransitionEvent) {
|
|
if (e.propertyName !== 'width') return
|
|
if (!collapsed.value) compactLayout.value = false
|
|
}
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
profile.value = await AgentPortalApi.profile()
|
|
} catch {
|
|
profile.value = null
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (compactTimer) window.clearTimeout(compactTimer)
|
|
})
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.agent-side-rail {
|
|
--asr-pad-x: 14px;
|
|
--asr-item-h: 44px;
|
|
--asr-radius: 10px;
|
|
--asr-panel-radius: 4px;
|
|
--asr-panel-inset: 16px;
|
|
--asr-shell-width: 200px;
|
|
--asr-ease: cubic-bezier(0.32, 0.72, 0, 1);
|
|
--asr-dur: 0.3s;
|
|
|
|
flex-shrink: 0;
|
|
align-self: stretch;
|
|
box-sizing: border-box;
|
|
height: calc(100% - var(--asr-panel-inset) * 2);
|
|
margin: var(--asr-panel-inset) 0 var(--asr-panel-inset) var(--asr-panel-inset);
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
background: var(--app-card-bg, #fff);
|
|
border-radius: var(--asr-panel-radius);
|
|
transition: width var(--asr-dur) var(--asr-ease);
|
|
will-change: width;
|
|
z-index: 10;
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
--asr-dur: 0.01ms;
|
|
}
|
|
|
|
&__shell {
|
|
width: var(--asr-shell-width);
|
|
height: 100%;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
&__fade {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
transition:
|
|
opacity 0.18s ease,
|
|
transform 0.18s var(--asr-ease);
|
|
}
|
|
|
|
&.is-collapsed &__fade {
|
|
opacity: 0;
|
|
transform: translateX(-6px);
|
|
pointer-events: none;
|
|
transition-duration: 0.12s;
|
|
}
|
|
|
|
&:not(.is-collapsed) &__fade {
|
|
transition-delay: 0.1s;
|
|
}
|
|
|
|
&__brand {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
flex-shrink: 0;
|
|
min-height: 56px;
|
|
padding: 10px 16px;
|
|
color: inherit;
|
|
text-decoration: none;
|
|
border-bottom: 1px solid var(--color-border-2, rgba(0, 0, 0, 0.06));
|
|
}
|
|
|
|
&__brand-logo {
|
|
display: block;
|
|
width: 32px;
|
|
height: 32px;
|
|
flex-shrink: 0;
|
|
object-fit: contain;
|
|
}
|
|
|
|
&__brand-name {
|
|
min-width: 0;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
line-height: 1.2;
|
|
color: var(--color-text-1);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
&__scroll {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
padding: 14px var(--asr-pad-x) 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
&__group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
&__item {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
width: 100%;
|
|
min-height: var(--asr-item-h);
|
|
padding: 0 12px 0 14px;
|
|
border: none;
|
|
border-radius: var(--asr-radius);
|
|
background: transparent;
|
|
color: var(--color-text-2);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
transition:
|
|
background-color 0.14s ease,
|
|
color 0.14s ease;
|
|
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 50%;
|
|
width: 3px;
|
|
height: 0;
|
|
border-radius: 0 3px 3px 0;
|
|
background: var(--app-primary-color, rgb(var(--primary-6)));
|
|
transform: translateY(-50%);
|
|
opacity: 0;
|
|
transition:
|
|
height 0.14s ease,
|
|
opacity 0.14s ease;
|
|
}
|
|
|
|
&:hover {
|
|
color: var(--color-text-1);
|
|
background: rgba(var(--primary-6), 0.06);
|
|
}
|
|
|
|
&.active {
|
|
color: var(--app-primary-color, rgb(var(--primary-6)));
|
|
background: rgba(var(--primary-6), 0.1);
|
|
font-weight: 600;
|
|
|
|
&::before {
|
|
height: 18px;
|
|
opacity: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
&__icon {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 20px;
|
|
height: 20px;
|
|
flex-shrink: 0;
|
|
font-size: 18px;
|
|
}
|
|
|
|
&__text {
|
|
min-width: 0;
|
|
font-size: 14px;
|
|
line-height: 1.3;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
&__dock {
|
|
flex-shrink: 0;
|
|
padding: 10px var(--asr-pad-x) 12px;
|
|
border-top: 1px solid var(--color-border-2, rgba(0, 0, 0, 0.06));
|
|
}
|
|
|
|
&__account {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
padding: 4px 4px 4px 6px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
&__user {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
&__account-meta {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
&__account-name {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
line-height: 1.25;
|
|
color: var(--color-text-1);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
&__account-code {
|
|
font-size: 11px;
|
|
line-height: 1.2;
|
|
color: var(--color-text-3);
|
|
letter-spacing: 0.02em;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
&__toggle {
|
|
flex-shrink: 0;
|
|
width: 28px;
|
|
height: 28px;
|
|
margin-left: auto;
|
|
border: none;
|
|
border-radius: 6px;
|
|
background: transparent;
|
|
color: var(--color-text-3);
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
&:hover {
|
|
color: var(--app-primary-color, rgb(var(--primary-6)));
|
|
background: rgba(var(--primary-6), 0.08);
|
|
}
|
|
}
|
|
|
|
&__toggle-icon {
|
|
font-size: 14px;
|
|
}
|
|
|
|
&.is-compact &__item {
|
|
justify-content: center;
|
|
padding: 0;
|
|
gap: 0;
|
|
}
|
|
|
|
&.is-compact &__dock {
|
|
padding: 8px 0 10px;
|
|
}
|
|
|
|
&.is-compact &__account {
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
width: 100%;
|
|
padding: 0;
|
|
}
|
|
|
|
&.is-compact &__account-meta {
|
|
display: none;
|
|
}
|
|
|
|
&.is-compact &__toggle {
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
</style>
|