This commit is contained in:
2026-08-03 17:44:42 +08:00
parent 64d051153d
commit 0260b9bb41
14 changed files with 216 additions and 78 deletions
+1
View File
@@ -45,6 +45,7 @@ declare module 'vue' {
ElTimeline: typeof import('element-plus/es')['ElTimeline'] ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElUpload: typeof import('element-plus/es')['ElUpload'] ElUpload: typeof import('element-plus/es')['ElUpload']
FirstLegOrderEditorShell: typeof import('./components/first-leg/FirstLegOrderEditorShell.vue')['default']
LoadErrorState: typeof import('./components/common/LoadErrorState.vue')['default'] LoadErrorState: typeof import('./components/common/LoadErrorState.vue')['default']
QueryToolbar: typeof import('./components/common/QueryToolbar.vue')['default'] QueryToolbar: typeof import('./components/common/QueryToolbar.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
+12 -2
View File
@@ -35,7 +35,12 @@ const groupId = computed(() => `client-nav-group-${props.node.key}`)
:aria-controls="groupId" :aria-controls="groupId"
@click="emit('toggle', node.key)" @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> <span>{{ node.title }}</span>
<ChevronDown class="client-sidebar-group-chevron" :size="16" aria-hidden="true" /> <ChevronDown class="client-sidebar-group-chevron" :size="16" aria-hidden="true" />
</button> </button>
@@ -66,7 +71,12 @@ const groupId = computed(() => `client-nav-group-${props.node.key}`)
:aria-current="node.path === activePath ? 'page' : undefined" :aria-current="node.path === activePath ? 'page' : undefined"
@click="emit('navigate', node.path)" @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> <span>{{ node.title }}</span>
</RouterLink> </RouterLink>
</li> </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>
+7 -5
View File
@@ -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.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.doesNotMatch(layoutSource, /<aside[^>]*aria-label=/)
assert.match(layoutSource, /<nav 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, /<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, /<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) 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/) 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 \{[^}]*inset: 0 0 auto 250px;[^}]*max-width: calc\(100vw - 250px\);[^}]*padding: 0 24px;/)
assert.match(shellStyles, /\.top-header \{[^}]*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: rgba\(255, 255, 255, \.98\)/) 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 \{[^}]*background: var\(--topbar\)/) 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(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, /@media \(max-width: 720px\) \{[\s\S]*\.top-header \{ inset: 0;[^}]*padding: 0 14px; \}/)
assert.match(globalStyles, /\.client-header-menu-button \{ display: grid; \}/) 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, /<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, /<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, /<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, /@navigate="emit\('navigate', \$event\)"/)
assert.match(sidebarMenuNodeSource, /@toggle="emit\('toggle', \$event\)"/) assert.match(sidebarMenuNodeSource, /@toggle="emit\('toggle', \$event\)"/)
}) })
+2 -2
View File
@@ -134,8 +134,8 @@ async function handleLogout() {
> >
<div class="client-sidebar-brand"> <div class="client-sidebar-brand">
<RouterLink class="client-sidebar-brand-link" to="/" aria-label="返回业务总览" @click="closeMobileMenu"> <RouterLink class="client-sidebar-brand-link" to="/" aria-label="返回业务总览" @click="closeMobileMenu">
<span class="client-sidebar-brand-mark"><img :src="baiyueLogoDark" alt="" /></span> <img class="client-sidebar-brand-logo" :src="baiyueLogoDark" alt="柏悦物流" />
<span class="client-sidebar-brand-copy"><strong>柏悦物流</strong><small>客户工作台</small></span> <span class="client-sidebar-brand-copy"><small>客户工作台</small></span>
</RouterLink> </RouterLink>
<button <button
ref="sidebarCloseButtonRef" ref="sidebarCloseButtonRef"
+6 -6
View File
@@ -2,12 +2,12 @@ import type { Component } from 'vue'
import { import {
Building2, Building2,
CircleUserRound, CircleUserRound,
ClipboardList, FileText,
Gauge,
KeyRound, KeyRound,
LayoutGrid,
Settings, Settings,
Ship,
ShieldCheck, ShieldCheck,
Truck,
Users, Users,
} from '@lucide/vue' } from '@lucide/vue'
@@ -34,13 +34,13 @@ export interface ClientNavigationGroup extends ClientNavigationBaseNode {
export type ClientNavigationNode = ClientNavigationLeaf | ClientNavigationGroup export type ClientNavigationNode = ClientNavigationLeaf | ClientNavigationGroup
export const clientNavigation: ClientNavigationNode[] = [ 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', key: 'first-leg',
title: '头程物流', title: '头程物流',
icon: Truck, icon: Ship,
children: [ 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' }, // { key: 'first-leg-import', title: '一键导入', icon: Package, path: '/first-leg/import', permission: 'merchant:first-leg:import' },
// 原型入口:接入快速下单接口与权限资源后再放开。 // 原型入口:接入快速下单接口与权限资源后再放开。
+1
View File
@@ -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/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: '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', 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/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: '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: '管理海外采购订单' } }, { path: 'cross-border/procurement', name: 'cross-border-procurement', component: () => import('@/views/cross-border/ProcurementView.vue'), meta: { title: '海外采购', description: '管理海外采购订单' } },
+1
View File
@@ -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; } .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; } .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-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-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-main { width: 100%; max-width: 100%; margin-left: 0; padding: 88px 14px 30px; }
.client-header-menu-button { display: grid; } .client-header-menu-button { display: grid; }
+10 -9
View File
@@ -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', () => { 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-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, /\.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*\{[^}]*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/) 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', () => { 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.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-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-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\(102, 126, 234, \.12\)[^}]*box-shadow:\s*none[^}]*color:\s*var\(--primary\)/) 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*\{[^}]*background:\s*transparent[^}]*color:\s*#52525b/) 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\(102, 126, 234, \.12\)/) 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*4px 0/) 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*42px[^}]*margin:\s*0 12px[^}]*padding:\s*0 20px 0 54px[^}]*border-radius:\s*10px/) 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-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-link\s*\{[^}]*text-decoration:\s*none/)
assert.match(compact, /\.client-sidebar (?:\.client-sidebar-group-trigger|\.client-sidebar-link) svg[^\{]*\{[^}]*flex:\s*0 0 18px/) 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', () => { 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,[^\{]*\.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-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*#fff1f0/) 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', () => { test('style source contract: turns the sidebar into a layered drawer at 720px and below', () => {
+24 -25
View File
@@ -1,22 +1,20 @@
.client-shell { min-height: 100vh; overflow-x: hidden; } .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; 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); } .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-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 { 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 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: 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 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-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 { display: flex; flex-direction: column; }
.client-user-copy strong { color: #fff; font-size: 12px; } .client-user-copy strong { color: #25324a; font-size: 12px; }
.client-user-copy small { color: rgba(255, 255, 255, .68); font-size: 10px; } .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 20px; background: rgba(255, 255, 255, .98); box-shadow: 4px 0 32px rgba(0, 0, 0, .08); color: var(--navy); } .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 68px; min-height: 68px; align-items: center; justify-content: space-between; gap: 10px; margin: 0 -14px 14px; padding: 0 18px; background: var(--topbar); } .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-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-logo { display: block; width: 142px; height: 54px; object-fit: contain; object-position: left center; filter: brightness(0) invert(1); }
.client-sidebar-brand-mark img { display: block; width: 28px; height: 28px; object-fit: contain; }
.client-sidebar-brand-copy { display: flex; min-width: 0; flex-direction: column; gap: 2px; } .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, .52); font-size: 10px; white-space: nowrap; }
.client-sidebar-brand-copy small { color: rgba(255, 255, 255, .76); font-size: 10px; letter-spacing: .12em; }
.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 { 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 { width: 10px; background: transparent; }
.client-sidebar nav::-webkit-scrollbar-track { background: transparent; } .client-sidebar nav::-webkit-scrollbar-track { background: transparent; }
@@ -27,20 +25,21 @@
.client-nav-menu, .client-nav-menu,
.client-nav-submenu { min-width: 0; margin: 0; padding: 0; list-style: none; } .client-nav-submenu { min-width: 0; margin: 0; padding: 0; list-style: none; }
.client-nav-menu { flex: 1; background: transparent; } .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 { 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-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-group-trigger { border: 0; background: transparent; cursor: pointer; text-align: left; }
.client-sidebar-link { text-decoration: none; } .client-sidebar-link { text-decoration: none; }
.client-sidebar-group-trigger:hover, .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-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(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-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: 42px; margin: 0 12px; padding: 0 20px 0 54px; border-radius: 10px; color: #71717a; } .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-nav-submenu .client-sidebar-link:hover { background: rgba(102, 126, 234, .08); color: var(--primary); transform: translateX(4px); } .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[aria-current="page"] { background: rgba(102, 126, 234, .12); box-shadow: none; color: var(--primary); font-weight: 700; } .client-nav-submenu .client-sidebar-link:hover { background: rgba(255, 255, 255, .07); color: #fff; }
.client-nav-node.is-active-parent > .client-sidebar-group-trigger { background: transparent; color: #52525b; font-weight: 400; } .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-group-trigger svg,
.client-sidebar .client-sidebar-link svg { flex: 0 0 18px; } .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; } .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-sidebar-link:focus-visible,
.client-header-menu-button:focus-visible, .client-header-menu-button:focus-visible,
.client-sidebar-brand 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-brand button,
.client-sidebar-scrim { display: none; } .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 { 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: #ffe3e1; color: #b52f35; } .client-sidebar-logout:hover { background: rgba(239, 68, 68, .22); color: #fff; }
.client-sidebar-logout svg { flex: 0 0 18px; } .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; } .client-main { width: calc(100% - 250px); max-width: 100%; min-height: 100vh; margin-left: 250px; padding: 88px 20px 40px; }
+29 -4
View File
@@ -4,11 +4,22 @@ import test from 'node:test'
const source = readFileSync(new URL('./FirstLegOrdersView.vue', import.meta.url), 'utf8') const source = readFileSync(new URL('./FirstLegOrdersView.vue', import.meta.url), 'utf8')
const routerSource = readFileSync(new URL('../router/index.ts', 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', () => { test('routes the confirmed first-leg path to the implemented order view', () => {
assert.match(routerSource, /path: 'orders\/first-leg'.*FirstLegOrdersView\.vue/) 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', () => { test('exposes real list, draft, submit, and detail workflow entries without cancellation', () => {
assert.match(source, /function openCreate\(\)/) assert.match(source, /function openCreate\(\)/)
assert.match(source, /async function saveDraft\(\)/) 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 QueryToolbar, \{ type QueryField, type QueryModel \}/)
assert.match(source, /import DataTable, \{ type DataTableColumn \}/) assert.match(source, /import DataTable, \{ type DataTableColumn \}/)
assert.match(source, /const filters = ref<QueryModel>/) 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, /async function searchOrders\(model: QueryModel\)/)
assert.match(source, /await store\.load\(\)/) assert.match(source, /await store\.load\(\)/)
assert.match(source, /<DataTable v-model:page="tablePage" v-model:page-size="tablePageSize" :data="pagedOrders" :columns="orderColumns"/) 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', () => { test('keeps the first viewport compact and moves create into the table toolbar', () => {
assert.match(source, /class="order-table-toolbar"/) assert.match(source, /class="order-table-toolbar"/)
assert.match(source, /<strong>订单列表<\/strong><span>共 \{\{ filteredOrders\.length \}\} 条<\/span>/) 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.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', () => { 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, /label="平台收货仓"[^>]*required/)
assert.match(source, /v-model="form\.receivingWarehouseId"/) assert.match(source, /v-model="form\.receivingWarehouseId"/)
assert.match(source, /selectedWarehouse\.businessHours/) 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\?\.name/)
assert.match(source, /detailOrder\.receivingWarehouseSnapshot\.code/) assert.match(source, /detailOrder\.receivingWarehouseSnapshot\.code/)
assert.match(source, /loadSelectedWarehouse\(detailedOrder\.receivingWarehouseId\)/) 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', () => { 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="shipperPartySnapshot\.contactName" required/)
assert.match(source, /prop="consigneePartySnapshot\.address" required/) assert.match(source, /prop="consigneePartySnapshot\.address" required/)
assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/) assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/)
+62 -23
View File
@@ -1,11 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { onBeforeRouteLeave, useRouter } from 'vue-router' import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox, type FormInstance, type FormItemRule, type FormRules } from 'element-plus' 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 { 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 QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue' import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import FirstLegOrderEditorShell from '@/components/first-leg/FirstLegOrderEditorShell.vue'
import { useFirstLegOrdersStore } from '@/stores/firstLegOrders' import { useFirstLegOrdersStore } from '@/stores/firstLegOrders'
import { useTenantAdministrationStore } from '@/stores/tenantAdministration' import { useTenantAdministrationStore } from '@/stores/tenantAdministration'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
@@ -25,7 +26,9 @@ import {
const store = useFirstLegOrdersStore() const store = useFirstLegOrdersStore()
const tenantStore = useTenantAdministrationStore() const tenantStore = useTenantAdministrationStore()
const auth = useAuthStore() const auth = useAuthStore()
const route = useRoute()
const router = useRouter() const router = useRouter()
const isCreatePage = computed(() => route.name === 'first-leg-order-create')
const companyApproved = computed(() => tenantStore.company.auditStatus === 'ACTIVE') const companyApproved = computed(() => tenantStore.company.auditStatus === 'ACTIVE')
const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create')) const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create'))
const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit')) const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit'))
@@ -151,7 +154,7 @@ function emptyDraft(): FirstLegOrderDraft {
const form = reactive<FirstLegOrderDraft>(emptyDraft()) const form = reactive<FirstLegOrderDraft>(emptyDraft())
const serializedDraft = computed(() => JSON.stringify(form)) 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 filteredOrders = computed(() => store.orders.filter((order) => {
const keywordValue = appliedFilters.value.keyword const keywordValue = appliedFilters.value.keyword
@@ -234,6 +237,13 @@ onMounted(() => {
void loadPageData() void loadPageData()
}) })
watch(isCreatePage, (isCreate, wasCreate) => {
if (!isCreate || wasCreate) return
editingId.value = undefined
replaceForm(emptyDraft())
resetEditorValidation()
}, { immediate: true })
onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnload)) onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnload))
onBeforeRouteLeave(async () => { onBeforeRouteLeave(async () => {
@@ -362,15 +372,15 @@ async function loadSelectedWarehouse(id: string | number | boolean | undefined)
} }
} }
function openCreate() { async function openCreate() {
if (availableWarehouses.value.length === 0) { if (availableWarehouses.value.length === 0) {
ElMessage.warning('当前没有可用的平台收货仓,请联系平台维护仓库后再创建订单') ElMessage.warning('当前没有可用的平台收货仓,请联系平台维护仓库后再创建订单')
return return
} }
editingId.value = undefined editingId.value = undefined
replaceForm(emptyDraft()) replaceForm(emptyDraft())
editorOpen.value = true
resetEditorValidation() resetEditorValidation()
await router.push({ name: 'first-leg-order-create' })
} }
async function openEdit(order: FirstLegOrder) { async function openEdit(order: FirstLegOrder) {
@@ -439,6 +449,10 @@ async function requestCloseEditor(done: () => void) {
function closeEditor() { function closeEditor() {
void requestCloseEditor(() => { void requestCloseEditor(() => {
editorOpen.value = false 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 editingId.value = order.id
markEditorClean() markEditorClean()
editorOpen.value = false editorOpen.value = false
if (isCreatePage.value) await router.push({ name: 'first-leg-orders' })
ElMessage.success('订单草稿已保存') ElMessage.success('订单草稿已保存')
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '草稿保存失败') 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) const order = await store.submit(cloneDraft(form), tenantStore.company.companyName, editingId.value, tenantStore.company.auditStatus)
markEditorClean() markEditorClean()
editorOpen.value = false editorOpen.value = false
if (isCreatePage.value) await router.push({ name: 'first-leg-orders' })
detailId.value = order.id detailId.value = order.id
detailOpen.value = true detailOpen.value = true
ElMessage.success('订单已提交,等待平台审核') ElMessage.success('订单已提交,等待平台审核')
@@ -544,17 +560,18 @@ async function searchOrders(model: QueryModel) {
</script> </script>
<template> <template>
<section class="first-leg-page"> <section class="first-leg-page" :class="{ 'is-create-route': isCreatePage }">
<QueryToolbar v-model="filters" :fields="queryFields" ariaLabel="头程订单查询" @search="searchOrders" /> <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="client-card order-table-card">
<div class="order-table-toolbar"> <div class="order-table-toolbar">
<div><strong>订单列表</strong><span>共 {{ filteredOrders.length }} 条</span></div> <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> <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>
<div v-if="!supportingDataErrors.some((message) => message.startsWith('企业资料'))" class="order-eligibility" :class="companyApproved ? 'is-approved' : 'is-pending'" role="status"> <div v-if="!companyApproved && !supportingDataErrors.some((message) => message.startsWith('企业资料'))" class="order-eligibility is-pending" role="status">
<component :is="companyApproved ? CheckCircle2 : AlertTriangle" :size="20" /> <AlertTriangle :size="20" />
<div><strong>{{ companyApproved ? '企业资料已审核,可正常提交订单' : canCreateOrder ? '企业资料待审核,当前仅可保存订单草稿' : '企业资料待审核,当前订单为只读' }}</strong><span>{{ companyApproved ? '提交后订单将进入平台审核。' : canCreateOrder ? '平台审核通过后即可提交;您仍可创建、编辑和保存草稿。' : '您当前拥有只读权限,可以继续查看订单进展。' }}</span></div> <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> <el-button v-if="!companyApproved && auth.hasPermission('company:profile:view')" link type="primary" @click="router.push('/company/profile')">查看企业资料</el-button>
</div> </div>
<div v-if="supportingDataErrors.length" class="supporting-data-error" role="alert"> <div v-if="supportingDataErrors.length" class="supporting-data-error" role="alert">
@@ -607,9 +624,17 @@ async function searchOrders(model: QueryModel) {
</div> </div>
</template> </template>
</div> </div>
</template>
<el-drawer v-model="editorOpen" class="first-leg-editor" :title="editingId ? '编辑订单草稿' : '创建头程订单'" size="1080px" destroy-on-close :before-close="requestCloseEditor"> <div v-if="isCreatePage && supportingDataErrors.length" class="supporting-data-error" role="alert">
<el-form ref="orderFormRef" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll-to-error> <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"> <section class="order-form-section prototype-section">
<div class="order-section-heading"><div><h3>下单信息</h3><p>选择送货方式、收货仓和运输渠道。</p></div></div> <div class="order-section-heading"><div><h3>下单信息</h3><p>选择送货方式、收货仓和运输渠道。</p></div></div>
<div class="order-form-grid"> <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-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> <el-form-item label="订单备注"><el-input v-model="form.customerRemark" maxlength="1000" placeholder="选填,例如:入仓前请电话联系" /></el-form-item>
</div> </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>
<section class="order-form-section prototype-section destination-section"> <section class="order-form-section prototype-section destination-section">
@@ -687,7 +712,7 @@ async function searchOrders(model: QueryModel) {
</section> </section>
</el-form> </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> <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"> <el-drawer v-model="detailOpen" class="first-leg-detail" title="订单详情" size="680px">
<div v-loading="detailLoading" class="order-detail-content"> <div v-loading="detailLoading" class="order-detail-content">
@@ -724,14 +749,17 @@ async function searchOrders(model: QueryModel) {
<style scoped> <style scoped>
.first-leg-page { display: flex; flex-direction: column; gap: 14px; } .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:not(.is-create-route)) > .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 > 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 { 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 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 div { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 3px; }
.order-eligibility strong { color: var(--navy); font-size: 13px; } .order-eligibility strong { color: var(--navy); font-size: 13px; }
.order-eligibility span { color: var(--muted); font-size: 11px; line-height: 1.55; } .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; } .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 { 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; } .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 { 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-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-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 :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 + .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; } .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 { 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__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); } .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 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; } .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 { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.order-section-heading h3 { margin-bottom: 5px; } .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 { align-items: flex-end; padding-top: 10px; border-top: 1px solid var(--border); }
.mobile-order-card footer > div { display: flex; } .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) { @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 { align-items: stretch; flex-direction: column; gap: 10px; }
.order-table-toolbar > div { justify-content: space-between; } .order-table-toolbar > div { justify-content: space-between; }
.order-table-toolbar .el-button { width: 100%; min-height: 40px; } .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; } .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 { 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; } .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%; } .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; } .form-span-full { grid-column: auto; }
.editor-actions :deep(.el-button) { min-height: 44px; } .editor-actions :deep(.el-button) { min-height: 44px; }
.desktop-order-table { display: none; } .desktop-order-table { display: none; }
+1 -1
View File
@@ -163,7 +163,7 @@ async function resetPassword(member: TenantMember) {
<el-button :loading="loading" @click="loadMembers">重新加载</el-button> <el-button :loading="loading" @click="loadMembers">重新加载</el-button>
</div> </div>
<template v-else> <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 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> <div v-if="auth.hasPermission('company:member:write')" class="table-actions"><el-button type="primary" @click="openCreate"><Plus :size="17" />新增成员</el-button></div>
+1 -1
View File
@@ -144,7 +144,7 @@ function assignPermissions(role: TenantRole) {
<el-button :loading="loading" @click="loadRoles">重新加载</el-button> <el-button :loading="loading" @click="loadRoles">重新加载</el-button>
</div> </div>
<template v-else> <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 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> <div v-if="auth.hasPermission('company:role:write')" class="table-actions"><el-button type="primary" @click="openCreate"><Plus :size="17" />新增角色</el-button></div>