Release/test 合并上生产 #1
Vendored
+1
@@ -45,6 +45,7 @@ declare module 'vue' {
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
FirstLegOrderEditorShell: typeof import('./components/first-leg/FirstLegOrderEditorShell.vue')['default']
|
||||
LoadErrorState: typeof import('./components/common/LoadErrorState.vue')['default']
|
||||
QueryToolbar: typeof import('./components/common/QueryToolbar.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
||||
@@ -35,7 +35,12 @@ const groupId = computed(() => `client-nav-group-${props.node.key}`)
|
||||
:aria-controls="groupId"
|
||||
@click="emit('toggle', node.key)"
|
||||
>
|
||||
<component v-if="!level" :is="node.icon" :size="20" />
|
||||
<component
|
||||
:is="node.icon"
|
||||
class="client-sidebar-node-icon"
|
||||
:size="level ? 16 : 20"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ node.title }}</span>
|
||||
<ChevronDown class="client-sidebar-group-chevron" :size="16" aria-hidden="true" />
|
||||
</button>
|
||||
@@ -66,7 +71,12 @@ const groupId = computed(() => `client-nav-group-${props.node.key}`)
|
||||
:aria-current="node.path === activePath ? 'page' : undefined"
|
||||
@click="emit('navigate', node.path)"
|
||||
>
|
||||
<component v-if="!level" :is="node.icon" :size="20" />
|
||||
<component
|
||||
:is="node.icon"
|
||||
class="client-sidebar-node-icon"
|
||||
:size="level ? 16 : 20"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ node.title }}</span>
|
||||
</RouterLink>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeft } from '@lucide/vue'
|
||||
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
page: boolean
|
||||
title: string
|
||||
beforeClose: (done: () => void) => void | Promise<void>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
back: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="page" class="first-leg-editor-page">
|
||||
<div class="editor-page-nav">
|
||||
<el-button link type="primary" @click="emit('back')"><ArrowLeft :size="17" />返回订单列表</el-button>
|
||||
<span>可先保存草稿,确认无误后再提交平台审核</span>
|
||||
</div>
|
||||
<div class="editor-page-body"><slot /></div>
|
||||
<footer class="editor-page-footer"><slot name="footer" /></footer>
|
||||
</section>
|
||||
|
||||
<el-drawer
|
||||
v-else
|
||||
class="first-leg-editor"
|
||||
:model-value="modelValue"
|
||||
:title="title"
|
||||
size="1080px"
|
||||
destroy-on-close
|
||||
:before-close="beforeClose"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
>
|
||||
<slot />
|
||||
<template #footer><slot name="footer" /></template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.first-leg-editor-page { min-width: 0; border: 1px solid var(--border); background: #fff; }
|
||||
.editor-page-nav { display: flex; min-height: 52px; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 20px; border-bottom: 1px solid var(--border); }
|
||||
.editor-page-nav span { color: var(--muted); font-size: 12px; }
|
||||
.editor-page-body { padding: 22px 24px 30px; }
|
||||
.editor-page-footer { position: sticky; z-index: 3; bottom: 0; display: flex; justify-content: flex-end; padding: 12px 24px; border-top: 1px solid var(--border); background: rgba(255, 255, 255, 0.96); }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.editor-page-nav { align-items: flex-start; flex-direction: column; gap: 2px; padding: 10px 16px; }
|
||||
.editor-page-nav span { padding-left: 1px; line-height: 1.5; }
|
||||
.editor-page-body { padding: 18px 16px 24px; }
|
||||
.editor-page-footer { padding: 10px 16px; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.editor-page-footer { position: static; }
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,7 @@ test('source contract: renders the grouped sidebar menu and mobile drawer contro
|
||||
assert.match(layoutSource, /<aside\s+ref="sidebarRef"\s+id="client-sidebar"\s+class="client-sidebar"\s+:inert="!desktopViewport && !mobileMenuOpen"\s+:aria-hidden="!desktopViewport && !mobileMenuOpen \? 'true' : undefined"\s*>/)
|
||||
assert.doesNotMatch(layoutSource, /<aside[^>]*aria-label=/)
|
||||
assert.match(layoutSource, /<nav aria-label="客户工作台导航">/)
|
||||
assert.match(layoutSource, /<div class="client-sidebar-brand">[\s\S]*?<RouterLink class="client-sidebar-brand-link" to="\/" aria-label="返回业务总览" @click="closeMobileMenu">[\s\S]*?柏悦物流[\s\S]*?客户工作台/s)
|
||||
assert.match(layoutSource, /<div class="client-sidebar-brand">[\s\S]*?<RouterLink class="client-sidebar-brand-link" to="\/" aria-label="返回业务总览" @click="closeMobileMenu">[\s\S]*?alt="柏悦物流"[\s\S]*?客户工作台/s)
|
||||
assert.match(layoutSource, /<button[^>]*ref="sidebarCloseButtonRef"[^>]*aria-label="关闭导航"[^>]*@click="closeMobileMenu"[^>]*>\s*<X :size="20" \/>/s)
|
||||
assert.match(layoutSource, /<ClientSidebarMenu\s+:active-path="route\.path"\s+@navigate="closeMobileMenu"\s*\/>/s)
|
||||
assert.match(layoutSource, /<button type="button" class="client-sidebar-logout" @click="handleLogout">[\s\S]*?退出登录/s)
|
||||
@@ -55,11 +55,11 @@ test('source contract: keeps route context and user identity in the compact head
|
||||
assert.doesNotMatch(routerSource, /showPageHeading|showTenantName/)
|
||||
})
|
||||
|
||||
test('source contract: aligns the admin-colored top bar beside the light sidebar and bounds the mobile header', () => {
|
||||
test('source contract: aligns the light top bar beside the prototype-inspired dark sidebar and bounds the mobile header', () => {
|
||||
assert.match(shellStyles, /\.top-header \{[^}]*inset: 0 0 auto 250px;[^}]*max-width: calc\(100vw - 250px\);[^}]*padding: 0 24px;/)
|
||||
assert.match(shellStyles, /\.top-header \{[^}]*background: var\(--topbar\)/)
|
||||
assert.match(shellStyles, /\.client-sidebar \{[^}]*inset: 0 auto 0 0;[^}]*width: 250px;[^}]*background: rgba\(255, 255, 255, \.98\)/)
|
||||
assert.match(shellStyles, /\.client-sidebar-brand \{[^}]*background: var\(--topbar\)/)
|
||||
assert.match(shellStyles, /\.top-header \{[^}]*background: rgba\(255, 255, 255, \.96\)/)
|
||||
assert.match(shellStyles, /\.client-sidebar \{[^}]*inset: 0 auto 0 0;[^}]*width: 250px;[^}]*background: linear-gradient\(180deg, #0b1f3a 0%, #0c2a52 100%\)/)
|
||||
assert.match(shellStyles, /\.client-sidebar-brand \{[^}]*border-bottom: 1px solid rgba\(255, 255, 255, \.09\)/)
|
||||
assert.match(shellStyles, /\.client-main \{[^}]*padding: 88px 20px 40px;/)
|
||||
assert.match(globalStyles, /@media \(max-width: 720px\) \{[\s\S]*\.top-header \{ inset: 0;[^}]*padding: 0 14px; \}/)
|
||||
assert.match(globalStyles, /\.client-header-menu-button \{ display: grid; \}/)
|
||||
@@ -114,6 +114,8 @@ test('source contract: uses a native recursive disclosure list without Element m
|
||||
assert.match(sidebarMenuNodeSource, /<button[\s\S]*?:aria-expanded="openedKeys\.has\(node\.key\)"[\s\S]*?:aria-controls="groupId"[\s\S]*?@click="emit\('toggle', node\.key\)"/)
|
||||
assert.match(sidebarMenuNodeSource, /<ul\s+v-show="openedKeys\.has\(node\.key\)"\s+:id="groupId"\s+class="client-nav-submenu"\s*>/)
|
||||
assert.match(sidebarMenuNodeSource, /<RouterLink[\s\S]*?:aria-current="node\.path === activePath \? 'page' : undefined"[\s\S]*?@click="emit\('navigate', node\.path\)"/)
|
||||
assert.match(sidebarMenuNodeSource, /<component\s+:is="node\.icon"\s+class="client-sidebar-node-icon"\s+:size="level \? 16 : 20"\s+aria-hidden="true"/)
|
||||
assert.doesNotMatch(sidebarMenuNodeSource, /<component\s+v-if="!level"/)
|
||||
assert.match(sidebarMenuNodeSource, /@navigate="emit\('navigate', \$event\)"/)
|
||||
assert.match(sidebarMenuNodeSource, /@toggle="emit\('toggle', \$event\)"/)
|
||||
})
|
||||
|
||||
@@ -134,8 +134,8 @@ async function handleLogout() {
|
||||
>
|
||||
<div class="client-sidebar-brand">
|
||||
<RouterLink class="client-sidebar-brand-link" to="/" aria-label="返回业务总览" @click="closeMobileMenu">
|
||||
<span class="client-sidebar-brand-mark"><img :src="baiyueLogoDark" alt="" /></span>
|
||||
<span class="client-sidebar-brand-copy"><strong>柏悦物流</strong><small>客户工作台</small></span>
|
||||
<img class="client-sidebar-brand-logo" :src="baiyueLogoDark" alt="柏悦物流" />
|
||||
<span class="client-sidebar-brand-copy"><small>客户工作台</small></span>
|
||||
</RouterLink>
|
||||
<button
|
||||
ref="sidebarCloseButtonRef"
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { Component } from 'vue'
|
||||
import {
|
||||
Building2,
|
||||
CircleUserRound,
|
||||
ClipboardList,
|
||||
FileText,
|
||||
Gauge,
|
||||
KeyRound,
|
||||
LayoutGrid,
|
||||
Settings,
|
||||
Ship,
|
||||
ShieldCheck,
|
||||
Truck,
|
||||
Users,
|
||||
} from '@lucide/vue'
|
||||
|
||||
@@ -34,13 +34,13 @@ export interface ClientNavigationGroup extends ClientNavigationBaseNode {
|
||||
export type ClientNavigationNode = ClientNavigationLeaf | ClientNavigationGroup
|
||||
|
||||
export const clientNavigation: ClientNavigationNode[] = [
|
||||
{ key: 'dashboard', title: '业务总览', icon: LayoutGrid, path: '/', permission: 'dashboard:view' },
|
||||
{ key: 'dashboard', title: '业务总览', icon: Gauge, path: '/', permission: 'dashboard:view' },
|
||||
{
|
||||
key: 'first-leg',
|
||||
title: '头程物流',
|
||||
icon: Truck,
|
||||
icon: Ship,
|
||||
children: [
|
||||
{ key: 'first-leg-orders', title: '头程下单', icon: ClipboardList, path: '/orders/first-leg', permission: 'order:first-leg:list' },
|
||||
{ key: 'first-leg-orders', title: '头程下单', icon: FileText, path: '/orders/first-leg', permission: 'order:first-leg:list' },
|
||||
// 原型入口:接入批量导入接口与权限资源后再放开。
|
||||
// { key: 'first-leg-import', title: '一键导入', icon: Package, path: '/first-leg/import', permission: 'merchant:first-leg:import' },
|
||||
// 原型入口:接入快速下单接口与权限资源后再放开。
|
||||
|
||||
@@ -20,6 +20,7 @@ const router = createRouter({
|
||||
{ path: 'organization/roles', name: 'roles', component: () => import('@/views/RolesView.vue'), meta: { title: '角色管理', description: '建立符合企业分工的成员角色', permission: 'company:role:list' } },
|
||||
{ path: 'organization/permissions', name: 'permissions', component: () => import('@/views/PermissionsView.vue'), meta: { title: '菜单权限', description: '为本企业角色分配菜单及操作权限', permission: 'company:permission:list' } },
|
||||
{ path: 'orders/first-leg', name: 'first-leg-orders', component: () => import('@/views/FirstLegOrdersView.vue'), meta: { title: '头程订单', description: '创建、查询并跟进本企业头程订单', permission: 'order:first-leg:list' } },
|
||||
{ path: 'orders/first-leg/create', name: 'first-leg-order-create', component: () => import('@/views/FirstLegOrdersView.vue'), meta: { title: '创建头程订单', description: '填写发货、目的地和货物信息', permission: 'order:first-leg:create' } },
|
||||
{ path: 'first-leg/import', name: 'first-leg-import', component: () => import('@/views/first-leg/BatchImportView.vue'), meta: { title: '一键导入', description: '批量导入头程订单' } },
|
||||
{ path: 'first-leg/quick-order', name: 'first-leg-quick-order', component: () => import('@/views/first-leg/QuickOrderView.vue'), meta: { title: '快速下单', description: '快速创建头程订单' } },
|
||||
{ path: 'cross-border/procurement', name: 'cross-border-procurement', component: () => import('@/views/cross-border/ProcurementView.vue'), meta: { title: '海外采购', description: '管理海外采购订单' } },
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
.client-sidebar { display: flex; width: min(286px, 86vw); padding: 0 14px 18px; visibility: hidden; pointer-events: none; transform: translateX(-100%); transition: transform .2s ease, visibility 0s linear .2s; z-index: 21; }
|
||||
.mobile-menu-open .client-sidebar { visibility: visible; pointer-events: auto; transform: translateX(0); transition-delay: 0s; }
|
||||
.client-sidebar-scrim { position: fixed; inset: 0; z-index: 20; display: block; padding: 0; border: 0; background: rgba(7, 18, 34, .56); }
|
||||
.client-header-menu-button { background: #eef3f8; color: #24324a; }
|
||||
.client-sidebar-brand button { display: grid; width: 44px; height: 44px; flex: 0 0 44px; padding: 0; border: 0; border-radius: 8px; background: transparent; color: #fff; place-items: center; }
|
||||
.client-main { width: 100%; max-width: 100%; margin-left: 0; padding: 88px 14px 30px; }
|
||||
.client-header-menu-button { display: grid; }
|
||||
|
||||
@@ -10,7 +10,7 @@ const mobileStyles = compact.slice(compact.indexOf('@media (max-width: 720px)'))
|
||||
|
||||
test('style source contract: constrains the desktop sidebar and gives the menu its own scroll region', () => {
|
||||
assert.match(compact, /\.client-shell\s*\{[^}]*overflow-x:\s*hidden/)
|
||||
assert.match(compact, /\.client-sidebar\s*\{[^}]*inset:\s*0 auto 0 0[^}]*width:\s*250px[^}]*overflow:\s*hidden[^}]*background:\s*rgba\(255, 255, 255, \.98\)/)
|
||||
assert.match(compact, /\.client-sidebar\s*\{[^}]*inset:\s*0 auto 0 0[^}]*width:\s*250px[^}]*overflow:\s*hidden[^}]*background:\s*linear-gradient\(180deg, #0b1f3a 0%, #0c2a52 100%\)/)
|
||||
assert.match(compact, /\.top-header\s*\{[^}]*inset:\s*0 0 auto 250px[^}]*max-width:\s*calc\(100vw - 250px\)/)
|
||||
assert.match(compact, /\.client-sidebar nav\s*\{[^}]*flex:\s*1[^}]*min-height:\s*0[^}]*overflow-y:\s*auto/)
|
||||
assert.match(compact, /\.client-sidebar nav\s*\{[^}]*scrollbar-color:\s*rgba\(102, 126, 234, \.3\) transparent[^}]*scrollbar-width:\s*thin/)
|
||||
@@ -26,12 +26,13 @@ test('style source contract: constrains the desktop sidebar and gives the menu i
|
||||
test('style source contract: presents native grouped rows matching the prototype active and divider states', () => {
|
||||
assert.doesNotMatch(compact, /\.client-sidebar \.el-menu|\.el-menu-item|\.el-sub-menu/)
|
||||
assert.match(compact, /\.client-nav-menu,[^\{]*\.client-nav-submenu\s*\{[^}]*margin:\s*0[^}]*padding:\s*0[^}]*list-style:\s*none/)
|
||||
assert.match(compact, /\.client-sidebar-group-trigger,[^\{]*\.client-sidebar-link\s*\{[^}]*height:\s*44px[^}]*border-radius:\s*0 12px 12px 0[^}]*color:\s*#595959/)
|
||||
assert.match(compact, /\.client-nav-submenu \.client-sidebar-link\[aria-current="page"\]\s*\{[^}]*background:\s*rgba\(102, 126, 234, \.12\)[^}]*box-shadow:\s*none[^}]*color:\s*var\(--primary\)/)
|
||||
assert.match(compact, /\.client-nav-node\.is-active-parent > \.client-sidebar-group-trigger\s*\{[^}]*background:\s*transparent[^}]*color:\s*#52525b/)
|
||||
assert.match(compact, /\.client-nav-node\.has-divider-before\s*\{[^}]*border-top:\s*1px solid rgba\(102, 126, 234, \.12\)/)
|
||||
assert.match(compact, /\.client-nav-submenu\s*\{[^}]*padding:\s*4px 0/)
|
||||
assert.match(compact, /\.client-nav-submenu \.client-sidebar-link\s*\{[^}]*height:\s*42px[^}]*margin:\s*0 12px[^}]*padding:\s*0 20px 0 54px[^}]*border-radius:\s*10px/)
|
||||
assert.match(compact, /\.client-sidebar-group-trigger,[^\{]*\.client-sidebar-link\s*\{[^}]*height:\s*44px[^}]*border-radius:\s*10px[^}]*color:\s*rgba\(255, 255, 255, \.72\)/)
|
||||
assert.match(compact, /\.client-nav-submenu \.client-sidebar-link\[aria-current="page"\]\s*\{[^}]*background:\s*rgba\(30, 136, 229, \.25\)[^}]*color:\s*#fff/)
|
||||
assert.match(compact, /\.client-nav-node\.is-active-parent > \.client-sidebar-group-trigger\s*\{[^}]*color:\s*#fff/)
|
||||
assert.match(compact, /\.client-nav-node\.has-divider-before\s*\{[^}]*border-top:\s*1px solid rgba\(255, 255, 255, \.09\)/)
|
||||
assert.match(compact, /\.client-nav-submenu\s*\{[^}]*padding:\s*2px 0 2px 12px/)
|
||||
assert.match(compact, /\.client-nav-submenu \.client-sidebar-link\s*\{[^}]*height:\s*40px[^}]*margin:\s*1px 0[^}]*padding:\s*0 12px[^}]*border-radius:\s*9px/)
|
||||
assert.match(compact, /\.client-sidebar \.client-nav-submenu \.client-sidebar-node-icon\s*\{[^}]*width:\s*16px[^}]*height:\s*16px[^}]*flex:\s*0 0 16px/)
|
||||
assert.match(compact, /\.client-sidebar-group-trigger\s*\{[^}]*border:\s*0[^}]*background:\s*transparent/)
|
||||
assert.match(compact, /\.client-sidebar-link\s*\{[^}]*text-decoration:\s*none/)
|
||||
assert.match(compact, /\.client-sidebar (?:\.client-sidebar-group-trigger|\.client-sidebar-link) svg[^\{]*\{[^}]*flex:\s*0 0 18px/)
|
||||
@@ -40,8 +41,8 @@ test('style source contract: presents native grouped rows matching the prototype
|
||||
|
||||
test('style source contract: makes navigation and drawer controls touch sized with visible keyboard focus', () => {
|
||||
assert.match(compact, /\.client-sidebar-group-trigger,[^\{]*\.client-sidebar-link\s*\{[^}]*height:\s*44px/)
|
||||
assert.match(compact, /\.client-sidebar-group-trigger:focus-visible,\s*\.client-sidebar-link:focus-visible,[\s\S]*?\.client-sidebar-logout:focus-visible\s*\{[^}]*outline:\s*2px solid var\(--primary\)[^}]*outline-offset:\s*2px/)
|
||||
assert.match(compact, /\.client-sidebar-logout\s*\{[^}]*height:\s*44px[^}]*background:\s*#fff1f0/)
|
||||
assert.match(compact, /\.client-sidebar-group-trigger:focus-visible,\s*\.client-sidebar-link:focus-visible,[\s\S]*?\.client-sidebar-logout:focus-visible\s*\{[^}]*outline:\s*2px solid #6db9ff[^}]*outline-offset:\s*2px/)
|
||||
assert.match(compact, /\.client-sidebar-logout\s*\{[^}]*height:\s*44px[^}]*background:\s*rgba\(239, 68, 68, \.12\)/)
|
||||
})
|
||||
|
||||
test('style source contract: turns the sidebar into a layered drawer at 720px and below', () => {
|
||||
|
||||
+24
-25
@@ -1,22 +1,20 @@
|
||||
.client-shell { min-height: 100vh; overflow-x: hidden; }
|
||||
.top-header { position: fixed; inset: 0 0 auto 250px; z-index: 20; display: flex; width: auto; max-width: calc(100vw - 250px); height: 68px; align-items: center; gap: 16px; padding: 0 24px; background: var(--topbar); box-shadow: 0 8px 32px rgba(0, 0, 0, .15), 0 2px 8px rgba(0, 0, 0, .1); backdrop-filter: blur(20px); }
|
||||
.client-shell { min-height: 100vh; overflow-x: hidden; background: #f5f7fa; }
|
||||
.top-header { position: fixed; inset: 0 0 auto 250px; z-index: 20; display: flex; width: auto; max-width: calc(100vw - 250px); height: 68px; align-items: center; gap: 16px; padding: 0 24px; border-bottom: 1px solid #e9eef5; background: rgba(255, 255, 255, .96); box-shadow: 0 4px 18px rgba(15, 31, 58, .045); backdrop-filter: blur(12px); }
|
||||
.client-header-menu-button { display: none; width: 44px; height: 44px; flex: 0 0 44px; padding: 0; border: 0; border-radius: 8px; background: rgba(255, 255, 255, .1); color: #fff; place-items: center; }
|
||||
.client-header-heading { min-width: 0; flex: 1; }
|
||||
.client-header-heading h1 { overflow: hidden; margin: 0; color: #fff; font-size: 18px; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.client-header-heading p { overflow: hidden; margin: 2px 0 0; color: rgba(255, 255, 255, .72); font-size: 11px; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.client-header-heading h1 { overflow: hidden; margin: 0; color: #17233a; font-size: 17px; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.client-header-heading p { overflow: hidden; margin: 3px 0 0; color: #7c8798; font-size: 11px; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.client-user { display: flex; align-items: center; gap: 10px; }
|
||||
.client-avatar { display: grid; width: 34px; height: 34px; place-items: center; border-radius: 50%; background: #fff; color: var(--primary); font-weight: 750; box-shadow: 0 4px 12px rgba(0, 0, 0, .15); }
|
||||
.client-avatar { display: grid; width: 34px; height: 34px; place-items: center; border-radius: 50%; background: #e9f2ff; color: #1565c0; font-weight: 750; }
|
||||
.client-user-copy { display: flex; flex-direction: column; }
|
||||
.client-user-copy strong { color: #fff; font-size: 12px; }
|
||||
.client-user-copy small { color: rgba(255, 255, 255, .68); font-size: 10px; }
|
||||
.client-sidebar { position: fixed; inset: 0 auto 0 0; z-index: 21; display: flex; width: 250px; flex-direction: column; overflow: hidden; padding: 0 14px 20px; background: rgba(255, 255, 255, .98); box-shadow: 4px 0 32px rgba(0, 0, 0, .08); color: var(--navy); }
|
||||
.client-sidebar-brand { display: flex; flex: 0 0 68px; min-height: 68px; align-items: center; justify-content: space-between; gap: 10px; margin: 0 -14px 14px; padding: 0 18px; background: var(--topbar); }
|
||||
.client-user-copy strong { color: #25324a; font-size: 12px; }
|
||||
.client-user-copy small { color: #8893a3; font-size: 10px; }
|
||||
.client-sidebar { position: fixed; inset: 0 auto 0 0; z-index: 21; display: flex; width: 250px; flex-direction: column; overflow: hidden; padding: 0 14px 16px; background: linear-gradient(180deg, #0b1f3a 0%, #0c2a52 100%); box-shadow: 4px 0 24px rgba(8, 25, 48, .14); color: #fff; }
|
||||
.client-sidebar-brand { display: flex; flex: 0 0 82px; min-height: 82px; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; padding: 10px 4px 12px; border-bottom: 1px solid rgba(255, 255, 255, .09); }
|
||||
.client-sidebar-brand-link { display: flex; min-width: 0; align-items: center; gap: 10px; color: #fff; text-decoration: none; }
|
||||
.client-sidebar-brand-mark { display: grid; width: 36px; height: 36px; flex: 0 0 36px; place-items: center; border: 1px solid rgba(102, 126, 234, .14); border-radius: 9px; background: #fff; box-shadow: 0 3px 10px rgba(30, 41, 59, .08); }
|
||||
.client-sidebar-brand-mark img { display: block; width: 28px; height: 28px; object-fit: contain; }
|
||||
.client-sidebar-brand-logo { display: block; width: 142px; height: 54px; object-fit: contain; object-position: left center; filter: brightness(0) invert(1); }
|
||||
.client-sidebar-brand-copy { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
|
||||
.client-sidebar-brand-copy strong { font-size: 15px; letter-spacing: .02em; line-height: 1.2; }
|
||||
.client-sidebar-brand-copy small { color: rgba(255, 255, 255, .76); font-size: 10px; letter-spacing: .12em; }
|
||||
.client-sidebar-brand-copy small { color: rgba(255, 255, 255, .52); font-size: 10px; white-space: nowrap; }
|
||||
.client-sidebar nav { display: flex; flex: 1; min-width: 0; min-height: 0; flex-direction: column; overflow-x: hidden; overflow-y: auto; scrollbar-color: rgba(102, 126, 234, .3) transparent; scrollbar-width: thin; }
|
||||
.client-sidebar nav::-webkit-scrollbar { width: 10px; background: transparent; }
|
||||
.client-sidebar nav::-webkit-scrollbar-track { background: transparent; }
|
||||
@@ -27,20 +25,21 @@
|
||||
.client-nav-menu,
|
||||
.client-nav-submenu { min-width: 0; margin: 0; padding: 0; list-style: none; }
|
||||
.client-nav-menu { flex: 1; background: transparent; }
|
||||
.client-nav-submenu { padding: 4px 0; background: linear-gradient(180deg, rgba(102, 126, 234, .03), transparent); }
|
||||
.client-nav-submenu { margin: 2px 0 6px; padding: 2px 0 2px 12px; border-left: 1px solid rgba(255, 255, 255, .1); }
|
||||
.client-nav-node { min-width: 0; }
|
||||
.client-nav-node.has-divider-before { margin-top: 14px; padding-top: 14px; border-top: 1px solid rgba(102, 126, 234, .12); }
|
||||
.client-nav-node.has-divider-before { margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, .09); }
|
||||
.client-sidebar-group-trigger,
|
||||
.client-sidebar-link { display: flex; width: 100%; height: 44px; min-width: 0; align-items: center; gap: 11px; margin: 2px 0; padding: 0 10px; border-radius: 0 12px 12px 0; color: #595959; transition: color .2s ease, background .2s ease, transform .2s ease; }
|
||||
.client-sidebar-link { display: flex; width: 100%; height: 44px; min-width: 0; align-items: center; gap: 11px; margin: 2px 0; padding: 0 12px; border-radius: 10px; color: rgba(255, 255, 255, .72); font-size: 13px; font-weight: 500; transition: color .2s ease, background .2s ease, box-shadow .2s ease; }
|
||||
.client-sidebar-group-trigger { border: 0; background: transparent; cursor: pointer; text-align: left; }
|
||||
.client-sidebar-link { text-decoration: none; }
|
||||
.client-sidebar-group-trigger:hover,
|
||||
.client-sidebar-link:hover { background: linear-gradient(135deg, rgba(102, 126, 234, .1), rgba(240, 147, 251, .08)); color: var(--primary); transform: translateX(3px); }
|
||||
.client-nav-menu > .client-nav-node > .client-sidebar-link[aria-current="page"] { background: linear-gradient(135deg, rgba(102, 126, 234, .18), rgba(240, 147, 251, .12)); box-shadow: inset 3px 0 var(--primary), 0 2px 8px rgba(102, 126, 234, .1); color: var(--primary); font-weight: 700; }
|
||||
.client-nav-submenu .client-sidebar-link { height: 42px; margin: 0 12px; padding: 0 20px 0 54px; border-radius: 10px; color: #71717a; }
|
||||
.client-nav-submenu .client-sidebar-link:hover { background: rgba(102, 126, 234, .08); color: var(--primary); transform: translateX(4px); }
|
||||
.client-nav-submenu .client-sidebar-link[aria-current="page"] { background: rgba(102, 126, 234, .12); box-shadow: none; color: var(--primary); font-weight: 700; }
|
||||
.client-nav-node.is-active-parent > .client-sidebar-group-trigger { background: transparent; color: #52525b; font-weight: 400; }
|
||||
.client-sidebar-link:hover { background: rgba(255, 255, 255, .08); color: #fff; }
|
||||
.client-nav-menu > .client-nav-node > .client-sidebar-link[aria-current="page"] { background: linear-gradient(90deg, #1565c0, #1e88e5); box-shadow: 0 6px 16px rgba(3, 15, 35, .28); color: #fff; font-weight: 650; }
|
||||
.client-nav-submenu .client-sidebar-link { height: 40px; margin: 1px 0; padding: 0 12px; border-radius: 9px; color: rgba(255, 255, 255, .58); font-size: 12px; }
|
||||
.client-sidebar .client-nav-submenu .client-sidebar-node-icon { width: 16px; height: 16px; flex: 0 0 16px; opacity: .86; }
|
||||
.client-nav-submenu .client-sidebar-link:hover { background: rgba(255, 255, 255, .07); color: #fff; }
|
||||
.client-nav-submenu .client-sidebar-link[aria-current="page"] { background: rgba(30, 136, 229, .25); box-shadow: inset 0 0 0 1px rgba(109, 185, 255, .14); color: #fff; font-weight: 650; }
|
||||
.client-nav-node.is-active-parent > .client-sidebar-group-trigger { color: #fff; font-weight: 600; }
|
||||
.client-sidebar .client-sidebar-group-trigger svg,
|
||||
.client-sidebar .client-sidebar-link svg { flex: 0 0 18px; }
|
||||
.client-sidebar .client-sidebar-group-chevron { width: 16px; height: 16px; flex: 0 0 16px; margin-left: auto; transform: rotate(-90deg); transition: transform .2s ease; }
|
||||
@@ -51,10 +50,10 @@
|
||||
.client-sidebar-link:focus-visible,
|
||||
.client-header-menu-button:focus-visible,
|
||||
.client-sidebar-brand button:focus-visible,
|
||||
.client-sidebar-logout:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
||||
.client-sidebar-logout:focus-visible { outline: 2px solid #6db9ff; outline-offset: 2px; }
|
||||
.client-sidebar-brand button,
|
||||
.client-sidebar-scrim { display: none; }
|
||||
.client-sidebar-logout { display: flex; width: 100%; height: 44px; align-items: center; justify-content: flex-start; gap: 11px; margin: 2px 0 0; padding: 0 10px; border: 0; border-radius: 10px; background: #fff1f0; color: #d94b50; cursor: pointer; font-weight: 600; text-align: left; }
|
||||
.client-sidebar-logout:hover { background: #ffe3e1; color: #b52f35; }
|
||||
.client-sidebar-logout { display: flex; width: 100%; height: 44px; align-items: center; justify-content: flex-start; gap: 11px; margin: 10px 0 0; padding: 0 12px; border: 0; border-radius: 10px; background: rgba(239, 68, 68, .12); color: #fca5a5; cursor: pointer; font-size: 13px; font-weight: 600; text-align: left; }
|
||||
.client-sidebar-logout:hover { background: rgba(239, 68, 68, .22); color: #fff; }
|
||||
.client-sidebar-logout svg { flex: 0 0 18px; }
|
||||
.client-main { width: calc(100% - 250px); max-width: 100%; min-height: 100vh; margin-left: 250px; padding: 88px 20px 40px; }
|
||||
|
||||
@@ -4,11 +4,22 @@ import test from 'node:test'
|
||||
|
||||
const source = readFileSync(new URL('./FirstLegOrdersView.vue', import.meta.url), 'utf8')
|
||||
const routerSource = readFileSync(new URL('../router/index.ts', import.meta.url), 'utf8')
|
||||
const editorShellSource = readFileSync(new URL('../components/first-leg/FirstLegOrderEditorShell.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('routes the confirmed first-leg path to the implemented order view', () => {
|
||||
assert.match(routerSource, /path: 'orders\/first-leg'.*FirstLegOrdersView\.vue/)
|
||||
})
|
||||
|
||||
test('opens order creation as a dedicated protected page while keeping draft editing in a drawer', () => {
|
||||
assert.match(routerSource, /path: 'orders\/first-leg\/create'.*name: 'first-leg-order-create'.*permission: 'order:first-leg:create'/)
|
||||
assert.match(source, /router\.push\(\{ name: 'first-leg-order-create' \}\)/)
|
||||
assert.match(source, /const isCreatePage = computed\(\(\) => route\.name === 'first-leg-order-create'\)/)
|
||||
assert.match(source, /<FirstLegOrderEditorShell[^>]*:page="isCreatePage"/)
|
||||
assert.match(editorShellSource, /<section v-if="page" class="first-leg-editor-page">/)
|
||||
assert.match(editorShellSource, /<el-drawer[\s\S]*v-else/)
|
||||
assert.match(editorShellSource, /返回订单列表/)
|
||||
})
|
||||
|
||||
test('exposes real list, draft, submit, and detail workflow entries without cancellation', () => {
|
||||
assert.match(source, /function openCreate\(\)/)
|
||||
assert.match(source, /async function saveDraft\(\)/)
|
||||
@@ -46,7 +57,7 @@ test('uses the shared query toolbar and data table without metric cards', () =>
|
||||
assert.match(source, /import QueryToolbar, \{ type QueryField, type QueryModel \}/)
|
||||
assert.match(source, /import DataTable, \{ type DataTableColumn \}/)
|
||||
assert.match(source, /const filters = ref<QueryModel>/)
|
||||
assert.match(source, /<QueryToolbar v-model="filters" :fields="queryFields" ariaLabel="头程订单查询" @search="searchOrders" \/>/)
|
||||
assert.match(source, /<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="头程订单查询" @search="searchOrders" \/>/)
|
||||
assert.match(source, /async function searchOrders\(model: QueryModel\)/)
|
||||
assert.match(source, /await store\.load\(\)/)
|
||||
assert.match(source, /<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="orderColumns"/)
|
||||
@@ -57,9 +68,18 @@ test('uses the shared query toolbar and data table without metric cards', () =>
|
||||
test('keeps the first viewport compact and moves create into the table toolbar', () => {
|
||||
assert.match(source, /class="order-table-toolbar"/)
|
||||
assert.match(source, /<strong>订单列表<\/strong><span>共 \{\{ filteredOrders\.length \}\} 条<\/span>/)
|
||||
assert.match(source, /class="order-table-toolbar"[\s\S]*order:first-leg:create[\s\S]*创建订单[\s\S]*class="order-eligibility"/)
|
||||
assert.match(source, /class="order-table-toolbar"[\s\S]*order:first-leg:create[\s\S]*创建订单[\s\S]*class="order-eligibility is-pending"/)
|
||||
assert.doesNotMatch(source, /<QueryToolbar[\s\S]*?<template #actions>/)
|
||||
assert.match(source, /client-main:has\(> \.first-leg-page\) > \.client-page-heading > div\) \{ position: absolute; width: 1px; height: 1px;/)
|
||||
assert.match(source, /client-main:has\(> \.first-leg-page:not\(\.is-create-route\)\) > \.client-page-heading > div\) \{ position: absolute; width: 1px; height: 1px;/)
|
||||
})
|
||||
|
||||
test('keeps desktop query actions on the first row and only shows the qualification notice before approval', () => {
|
||||
assert.match(source, /\.first-leg-query-toolbar \{ flex-wrap: nowrap; \}/)
|
||||
assert.match(source, /query-toolbar__filters\) \{ display: grid; min-width: 0; flex: 1 1 auto;/)
|
||||
assert.match(source, /query-toolbar__actions\) \{ align-self: flex-start; flex-wrap: nowrap; margin-left: auto;/)
|
||||
assert.match(source, /v-if="!companyApproved && !supportingDataErrors\.some/)
|
||||
assert.doesNotMatch(source, /企业资料已审核,可正常提交订单/)
|
||||
assert.doesNotMatch(source, /order-eligibility\.is-approved/)
|
||||
})
|
||||
|
||||
test('does not expose arbitrary status mutation controls to customers', () => {
|
||||
@@ -73,6 +93,11 @@ test('requires an enabled platform receiving warehouse in the order form', () =>
|
||||
assert.match(source, /label="平台收货仓"[^>]*required/)
|
||||
assert.match(source, /v-model="form\.receivingWarehouseId"/)
|
||||
assert.match(source, /selectedWarehouse\.businessHours/)
|
||||
assert.match(source, /class="selected-warehouse" aria-label="所选收货仓详情"/)
|
||||
assert.doesNotMatch(source, /class="selected-warehouse"[^>]*>[\s\S]*?<strong>\{\{ selectedWarehouse\.name \}\}<\/strong>/)
|
||||
assert.match(source, /first-leg-editor \.el-drawer__header\) \{[^}]*margin-bottom: 0;[^}]*border-bottom: 1px solid var\(--border\)/)
|
||||
assert.match(source, /first-leg-editor \.el-drawer__body\) \{[^}]*min-height: 0;[^}]*padding: 18px 22px 32px;[^}]*overscroll-behavior: contain/)
|
||||
assert.match(source, /first-leg-editor \.el-drawer__footer\) \{[^}]*border-top: 1px solid var\(--border\);[^}]*background: #fff/)
|
||||
assert.match(source, /detailOrder\.receivingWarehouseSnapshot\?\.name/)
|
||||
assert.match(source, /detailOrder\.receivingWarehouseSnapshot\.code/)
|
||||
assert.match(source, /loadSelectedWarehouse\(detailedOrder\.receivingWarehouseId\)/)
|
||||
@@ -97,7 +122,7 @@ test('distills order creation into the confirmed prototype sections without fake
|
||||
})
|
||||
|
||||
test('shows field-level validation and concise guidance for required order inputs', () => {
|
||||
assert.match(source, /ref="orderFormRef" :model="form" :rules="orderFormRules"/)
|
||||
assert.match(source, /ref="orderFormRef"[^>]*:model="form" :rules="orderFormRules"/)
|
||||
assert.match(source, /prop="shipperPartySnapshot\.contactName" required/)
|
||||
assert.match(source, /prop="consigneePartySnapshot\.address" required/)
|
||||
assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { onBeforeRouteLeave, useRouter } from 'vue-router'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormItemRule, type FormRules } from 'element-plus'
|
||||
import { AlertTriangle, Box, CheckCircle2, Eye, FilePlus2, MapPin, PackageCheck, Pencil, Plus, Send, Trash2 } from '@lucide/vue'
|
||||
import { AlertTriangle, Box, Eye, FilePlus2, MapPin, PackageCheck, Pencil, Plus, Send, Trash2 } from '@lucide/vue'
|
||||
import { FIRST_LEG_ORDER_STATUSES, FIRST_LEG_TRANSPORT_MODES, type FirstLegOrderStatus } from '@chongchong/shared'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import FirstLegOrderEditorShell from '@/components/first-leg/FirstLegOrderEditorShell.vue'
|
||||
import { useFirstLegOrdersStore } from '@/stores/firstLegOrders'
|
||||
import { useTenantAdministrationStore } from '@/stores/tenantAdministration'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -25,7 +26,9 @@ import {
|
||||
const store = useFirstLegOrdersStore()
|
||||
const tenantStore = useTenantAdministrationStore()
|
||||
const auth = useAuthStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const isCreatePage = computed(() => route.name === 'first-leg-order-create')
|
||||
const companyApproved = computed(() => tenantStore.company.auditStatus === 'ACTIVE')
|
||||
const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create'))
|
||||
const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit'))
|
||||
@@ -151,7 +154,7 @@ function emptyDraft(): FirstLegOrderDraft {
|
||||
|
||||
const form = reactive<FirstLegOrderDraft>(emptyDraft())
|
||||
const serializedDraft = computed(() => JSON.stringify(form))
|
||||
const editorDirty = computed(() => editorOpen.value && serializedDraft.value !== editorBaseline.value)
|
||||
const editorDirty = computed(() => (editorOpen.value || isCreatePage.value) && serializedDraft.value !== editorBaseline.value)
|
||||
|
||||
const filteredOrders = computed(() => store.orders.filter((order) => {
|
||||
const keywordValue = appliedFilters.value.keyword
|
||||
@@ -234,6 +237,13 @@ onMounted(() => {
|
||||
void loadPageData()
|
||||
})
|
||||
|
||||
watch(isCreatePage, (isCreate, wasCreate) => {
|
||||
if (!isCreate || wasCreate) return
|
||||
editingId.value = undefined
|
||||
replaceForm(emptyDraft())
|
||||
resetEditorValidation()
|
||||
}, { immediate: true })
|
||||
|
||||
onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnload))
|
||||
|
||||
onBeforeRouteLeave(async () => {
|
||||
@@ -362,15 +372,15 @@ async function loadSelectedWarehouse(id: string | number | boolean | undefined)
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
async function openCreate() {
|
||||
if (availableWarehouses.value.length === 0) {
|
||||
ElMessage.warning('当前没有可用的平台收货仓,请联系平台维护仓库后再创建订单')
|
||||
return
|
||||
}
|
||||
editingId.value = undefined
|
||||
replaceForm(emptyDraft())
|
||||
editorOpen.value = true
|
||||
resetEditorValidation()
|
||||
await router.push({ name: 'first-leg-order-create' })
|
||||
}
|
||||
|
||||
async function openEdit(order: FirstLegOrder) {
|
||||
@@ -439,6 +449,10 @@ async function requestCloseEditor(done: () => void) {
|
||||
function closeEditor() {
|
||||
void requestCloseEditor(() => {
|
||||
editorOpen.value = false
|
||||
if (isCreatePage.value) {
|
||||
markEditorClean()
|
||||
void router.push({ name: 'first-leg-orders' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -485,6 +499,7 @@ async function saveDraft() {
|
||||
editingId.value = order.id
|
||||
markEditorClean()
|
||||
editorOpen.value = false
|
||||
if (isCreatePage.value) await router.push({ name: 'first-leg-orders' })
|
||||
ElMessage.success('订单草稿已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '草稿保存失败')
|
||||
@@ -518,6 +533,7 @@ async function submitOrder() {
|
||||
const order = await store.submit(cloneDraft(form), tenantStore.company.companyName, editingId.value, tenantStore.company.auditStatus)
|
||||
markEditorClean()
|
||||
editorOpen.value = false
|
||||
if (isCreatePage.value) await router.push({ name: 'first-leg-orders' })
|
||||
detailId.value = order.id
|
||||
detailOpen.value = true
|
||||
ElMessage.success('订单已提交,等待平台审核')
|
||||
@@ -544,17 +560,18 @@ async function searchOrders(model: QueryModel) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="first-leg-page">
|
||||
<QueryToolbar v-model="filters" :fields="queryFields" ariaLabel="头程订单查询" @search="searchOrders" />
|
||||
<section class="first-leg-page" :class="{ 'is-create-route': isCreatePage }">
|
||||
<template v-if="!isCreatePage">
|
||||
<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="头程订单查询" @search="searchOrders" />
|
||||
|
||||
<div class="client-card order-table-card">
|
||||
<div class="order-table-toolbar">
|
||||
<div><strong>订单列表</strong><span>共 {{ filteredOrders.length }} 条</span></div>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :disabled="availableWarehouses.length === 0" @click="openCreate"><FilePlus2 :size="17" />创建订单</el-button>
|
||||
</div>
|
||||
<div v-if="!supportingDataErrors.some((message) => message.startsWith('企业资料'))" class="order-eligibility" :class="companyApproved ? 'is-approved' : 'is-pending'" role="status">
|
||||
<component :is="companyApproved ? CheckCircle2 : AlertTriangle" :size="20" />
|
||||
<div><strong>{{ companyApproved ? '企业资料已审核,可正常提交订单' : canCreateOrder ? '企业资料待审核,当前仅可保存订单草稿' : '企业资料待审核,当前订单为只读' }}</strong><span>{{ companyApproved ? '提交后订单将进入平台审核。' : canCreateOrder ? '平台审核通过后即可提交;您仍可创建、编辑和保存草稿。' : '您当前拥有只读权限,可以继续查看订单进展。' }}</span></div>
|
||||
<div v-if="!companyApproved && !supportingDataErrors.some((message) => message.startsWith('企业资料'))" class="order-eligibility is-pending" role="status">
|
||||
<AlertTriangle :size="20" />
|
||||
<div><strong>{{ canCreateOrder ? '企业资料待审核,当前仅可保存订单草稿' : '企业资料待审核,当前订单为只读' }}</strong><span>{{ canCreateOrder ? '平台审核通过后即可提交;您仍可创建、编辑和保存草稿。' : '您当前拥有只读权限,可以继续查看订单进展。' }}</span></div>
|
||||
<el-button v-if="!companyApproved && auth.hasPermission('company:profile:view')" link type="primary" @click="router.push('/company/profile')">查看企业资料</el-button>
|
||||
</div>
|
||||
<div v-if="supportingDataErrors.length" class="supporting-data-error" role="alert">
|
||||
@@ -607,9 +624,17 @@ async function searchOrders(model: QueryModel) {
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-drawer v-model="editorOpen" class="first-leg-editor" :title="editingId ? '编辑订单草稿' : '创建头程订单'" size="1080px" destroy-on-close :before-close="requestCloseEditor">
|
||||
<el-form ref="orderFormRef" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll-to-error>
|
||||
<div v-if="isCreatePage && supportingDataErrors.length" class="supporting-data-error" role="alert">
|
||||
<AlertTriangle :size="20" />
|
||||
<div><strong>下单配套数据加载失败</strong><span v-for="message in supportingDataErrors" :key="message">{{ message }}</span></div>
|
||||
<el-button :loading="supportingDataLoading || store.loading" @click="loadPageData">重新加载</el-button>
|
||||
</div>
|
||||
<el-alert v-if="isCreatePage && !supportingDataLoading && availableWarehouses.length === 0" title="当前没有可用的平台收货仓" description="请联系平台维护并启用收货仓后再创建订单。" type="warning" :closable="false" show-icon />
|
||||
|
||||
<FirstLegOrderEditorShell v-model="editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : '创建头程订单'" :before-close="requestCloseEditor" @back="closeEditor">
|
||||
<el-form ref="orderFormRef" v-loading="isCreatePage && (supportingDataLoading || store.loading)" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll-to-error>
|
||||
<section class="order-form-section prototype-section">
|
||||
<div class="order-section-heading"><div><h3>下单信息</h3><p>选择送货方式、收货仓和运输渠道。</p></div></div>
|
||||
<div class="order-form-grid">
|
||||
@@ -622,7 +647,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<el-form-item label="期望出货日期"><el-date-picker v-model="form.expectedDepartureDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择期望出货日期" /></el-form-item>
|
||||
<el-form-item label="订单备注"><el-input v-model="form.customerRemark" maxlength="1000" placeholder="选填,例如:入仓前请电话联系" /></el-form-item>
|
||||
</div>
|
||||
<div v-if="selectedWarehouse" class="selected-warehouse"><MapPin :size="17" /><div><strong>{{ selectedWarehouse.name }}</strong><span>{{ selectedWarehouse.address ? `${selectedWarehouse.province}${selectedWarehouse.city} · ${selectedWarehouse.address}` : '仓库地址详情暂不可用' }}</span><small>{{ selectedWarehouse.businessHours ? `收货时间:${selectedWarehouse.businessHours}` : '收货时间暂不可用' }}</small></div></div>
|
||||
<div v-if="selectedWarehouse" class="selected-warehouse" aria-label="所选收货仓详情"><MapPin :size="17" /><div><span>{{ selectedWarehouse.address ? `${selectedWarehouse.province}${selectedWarehouse.city} · ${selectedWarehouse.address}` : '仓库地址详情暂不可用' }}</span><small>{{ selectedWarehouse.businessHours ? `收货时间:${selectedWarehouse.businessHours}` : '收货时间暂不可用' }}</small></div></div>
|
||||
</section>
|
||||
|
||||
<section class="order-form-section prototype-section destination-section">
|
||||
@@ -687,7 +712,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</section>
|
||||
</el-form>
|
||||
<template #footer><div class="editor-actions"><el-button :disabled="saving" @click="closeEditor">返回</el-button><el-button v-if="editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" type="primary" :loading="saving" @click="submitOrder"><Send :size="16" />{{ companyApproved ? '提交订单' : '提交前需完成资料审核' }}</el-button></div></template>
|
||||
</el-drawer>
|
||||
</FirstLegOrderEditorShell>
|
||||
|
||||
<el-drawer v-model="detailOpen" class="first-leg-detail" title="订单详情" size="680px">
|
||||
<div v-loading="detailLoading" class="order-detail-content">
|
||||
@@ -724,14 +749,17 @@ async function searchOrders(model: QueryModel) {
|
||||
|
||||
<style scoped>
|
||||
.first-leg-page { display: flex; flex-direction: column; gap: 14px; }
|
||||
:global(.client-main:has(> .first-leg-page) > .client-page-heading) { margin-bottom: 0; }
|
||||
:global(.client-main:has(> .first-leg-page) > .client-page-heading > div) { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; border: 0; clip: rect(0 0 0 0); white-space: nowrap; }
|
||||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading) { margin-bottom: 0; }
|
||||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading > div) { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; border: 0; clip: rect(0 0 0 0); white-space: nowrap; }
|
||||
.first-leg-query-toolbar { flex-wrap: nowrap; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { display: grid; min-width: 0; flex: 1 1 auto; grid-template-columns: minmax(160px, 1.4fr) minmax(100px, .72fr) minmax(100px, .72fr) minmax(190px, 1.15fr); }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters .el-input), .first-leg-query-toolbar :deep(.query-toolbar__filters .el-select), .first-leg-query-toolbar :deep(.query-toolbar__filters .el-date-editor) { width: 100%; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__actions) { align-self: flex-start; flex-wrap: nowrap; margin-left: auto; }
|
||||
.order-eligibility { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border: 1px solid; }
|
||||
.order-eligibility svg { flex: 0 0 20px; margin-top: 1px; }
|
||||
.order-eligibility div { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 3px; }
|
||||
.order-eligibility strong { color: var(--navy); font-size: 13px; }
|
||||
.order-eligibility span { color: var(--muted); font-size: 11px; line-height: 1.55; }
|
||||
.order-eligibility.is-approved { border-color: #bcd9d2; background: #eff8f5; color: #20745f; }
|
||||
.order-eligibility.is-pending { border-color: #e5d2a5; background: #fff9eb; color: #9a6a12; }
|
||||
.supporting-data-error { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 14px 16px; border: 1px solid #e5d2a5; background: #fff9eb; color: #9a6a12; }
|
||||
.supporting-data-error > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
||||
@@ -755,7 +783,10 @@ async function searchOrders(model: QueryModel) {
|
||||
.order-number-link { display: block; padding: 0; border: 0; background: none; color: var(--primary); cursor: pointer; font-weight: 700; text-align: left; }
|
||||
.order-number-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
|
||||
.order-reference { display: block; margin-top: 4px; color: var(--muted); font-size: 11px; font-weight: 400; }
|
||||
.order-form { padding: 0 2px 28px; }
|
||||
:global(.first-leg-editor .el-drawer__header) { flex: 0 0 auto; min-height: 58px; margin-bottom: 0; padding: 17px 22px; border-bottom: 1px solid var(--border); }
|
||||
:global(.first-leg-editor .el-drawer__body) { min-height: 0; padding: 18px 22px 32px; overscroll-behavior: contain; }
|
||||
:global(.first-leg-editor .el-drawer__footer) { flex: 0 0 auto; padding: 12px 22px; border-top: 1px solid var(--border); background: #fff; }
|
||||
.order-form { width: min(100%, 1000px); margin: 0 auto; padding: 0 0 12px; }
|
||||
.order-form :deep(.el-form-item__error) { position: static; padding-top: 6px; overflow-wrap: anywhere; line-height: 1.45; }
|
||||
.order-form-section + .order-form-section { margin-top: 24px; padding-top: 22px; border-top: 1px solid var(--border); }
|
||||
.order-form-section h3 { margin: 0 0 16px; color: var(--navy); font-size: 16px; }
|
||||
@@ -770,9 +801,9 @@ async function searchOrders(model: QueryModel) {
|
||||
.warehouse-options { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.warehouse-options :deep(.el-radio-button__inner) { min-height: 40px; border: 1px solid var(--border); border-radius: 6px !important; box-shadow: none !important; }
|
||||
.warehouse-options :deep(.el-radio-button.is-active .el-radio-button__inner) { border-color: var(--primary); }
|
||||
.selected-warehouse { display: flex; gap: 10px; margin-top: -2px; padding: 12px 14px; border: 1px solid #cfe1dd; background: #f3f8f7; color: var(--primary); }
|
||||
.selected-warehouse { display: flex; align-items: flex-start; gap: 10px; margin-top: 0; padding: 11px 13px; border: 1px solid #d9def8; background: #f6f7fe; color: var(--primary); }
|
||||
.selected-warehouse svg { flex: 0 0 17px; margin-top: 2px; }
|
||||
.selected-warehouse div { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
||||
.selected-warehouse div { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
|
||||
.selected-warehouse span, .selected-warehouse small { color: var(--muted); font-size: 11px; line-height: 1.5; }
|
||||
.order-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
|
||||
.order-section-heading h3 { margin-bottom: 5px; }
|
||||
@@ -838,8 +869,13 @@ async function searchOrders(model: QueryModel) {
|
||||
.mobile-order-card footer { align-items: flex-end; padding-top: 10px; border-top: 1px solid var(--border); }
|
||||
.mobile-order-card footer > div { display: flex; }
|
||||
}
|
||||
@media (min-width: 721px) and (max-width: 1050px) {
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
:global(.client-main:has(> .first-leg-page) > .client-page-heading) { margin-bottom: 10px; }
|
||||
.first-leg-query-toolbar { flex-wrap: wrap; }
|
||||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { display: flex; }
|
||||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading) { margin-bottom: 10px; }
|
||||
.order-table-toolbar { align-items: stretch; flex-direction: column; gap: 10px; }
|
||||
.order-table-toolbar > div { justify-content: space-between; }
|
||||
.order-table-toolbar .el-button { width: 100%; min-height: 40px; }
|
||||
@@ -848,8 +884,11 @@ async function searchOrders(model: QueryModel) {
|
||||
.order-form-grid, .order-detail-grid, .party-select-grid, .party-contact-grid, .destination-fields, .cargo-primary-grid, .cargo-secondary-grid { grid-template-columns: 1fr; }
|
||||
.segmented-options { width: 100%; align-items: stretch; flex-direction: column; }
|
||||
.segmented-options :deep(.el-radio-button__inner) { border: 1px solid var(--border); border-radius: 6px !important; box-shadow: none !important; }
|
||||
.warehouse-options { display: grid; grid-template-columns: 1fr; }
|
||||
.warehouse-options { display: grid; width: 100%; grid-template-columns: 1fr; }
|
||||
.warehouse-options :deep(.el-radio-button), .warehouse-options :deep(.el-radio-button__inner) { width: 100%; }
|
||||
:global(.first-leg-editor .el-drawer__header) { min-height: 56px; padding: 15px 20px; }
|
||||
:global(.first-leg-editor .el-drawer__body) { padding: 16px 20px 28px; }
|
||||
:global(.first-leg-editor .el-drawer__footer) { padding: 10px 20px; }
|
||||
.form-span-full { grid-column: auto; }
|
||||
.editor-actions :deep(.el-button) { min-height: 44px; }
|
||||
.desktop-order-table { display: none; }
|
||||
|
||||
@@ -163,7 +163,7 @@ async function resetPassword(member: TenantMember) {
|
||||
<el-button :loading="loading" @click="loadMembers">重新加载</el-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<QueryToolbar v-model="memberFilters" ariaLabel="企业成员筛选" :fields="memberQueryFields" search-text="查询成员" @search="searchMembers" />
|
||||
<QueryToolbar v-model="memberFilters" ariaLabel="企业成员筛选" :fields="memberQueryFields" search-text="查询" @search="searchMembers" />
|
||||
|
||||
<div class="client-card member-table-card">
|
||||
<div v-if="auth.hasPermission('company:member:write')" class="table-actions"><el-button type="primary" @click="openCreate"><Plus :size="17" />新增成员</el-button></div>
|
||||
|
||||
@@ -144,7 +144,7 @@ function assignPermissions(role: TenantRole) {
|
||||
<el-button :loading="loading" @click="loadRoles">重新加载</el-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<QueryToolbar v-model="roleFilters" ariaLabel="企业角色筛选" :fields="roleQueryFields" search-text="查询角色" @search="searchRoles" />
|
||||
<QueryToolbar v-model="roleFilters" ariaLabel="企业角色筛选" :fields="roleQueryFields" search-text="查询" @search="searchRoles" />
|
||||
|
||||
<div class="client-card role-table-card">
|
||||
<div v-if="auth.hasPermission('company:role:write')" class="table-actions"><el-button type="primary" @click="openCreate"><Plus :size="17" />新增角色</el-button></div>
|
||||
|
||||
Reference in New Issue
Block a user