优化
This commit is contained in:
Vendored
-1
@@ -12,4 +12,3 @@ declare module 'vue-router' {
|
||||
permission?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const layoutSource = readFileSync(new URL('./AppLayout.vue', import.meta.url), 'utf8')
|
||||
const shellStyles = readFileSync(new URL('../styles/shell.css', import.meta.url), 'utf8')
|
||||
const globalStyles = readFileSync(new URL('../styles/index.css', import.meta.url), 'utf8')
|
||||
const routerSource = readFileSync(new URL('../router/index.ts', import.meta.url), 'utf8')
|
||||
const sidebarMenuSource = readFileSync(new URL('../components/ClientSidebarMenu.vue', import.meta.url), 'utf8')
|
||||
const sidebarMenuNodeSource = readFileSync(new URL('../components/ClientSidebarMenuNode.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('source contract: renders the grouped sidebar menu and mobile drawer controls', () => {
|
||||
assert.match(layoutSource, /import \{ computed, nextTick, onBeforeUnmount, onMounted, ref, watch \} from 'vue'/)
|
||||
assert.match(layoutSource, /import \{[\s\S]*?\bX,[\s\S]*?\} from '@lucide\/vue'/)
|
||||
assert.match(layoutSource, /import \{[\s\S]*?\bX\s*\} from '@lucide\/vue'/)
|
||||
assert.match(layoutSource, /import ClientSidebarMenu from '@\/components\/ClientSidebarMenu\.vue'/)
|
||||
assert.match(layoutSource, /:class="\{ 'mobile-menu-open': mobileMenuOpen \}"/)
|
||||
assert.match(layoutSource, /<button\s+v-if="mobileMenuOpen"[^>]*class="client-sidebar-scrim"[^>]*aria-label="关闭导航"[^>]*@click="closeMobileMenu"/s)
|
||||
@@ -42,6 +44,24 @@ test('source contract: removes the header and main content from the accessibilit
|
||||
assert.match(layoutSource, /<main\s+class="client-main"\s+:inert="mobileMenuOpen"\s+:aria-hidden="mobileMenuOpen \? 'true' : undefined"\s*>/)
|
||||
})
|
||||
|
||||
test('source contract: keeps route context and user actions in the compact header without tenant identity', () => {
|
||||
assert.match(layoutSource, /const pageTitle = computed\(\(\) => route\.meta\.title \|\| '业务总览'\)/)
|
||||
assert.match(layoutSource, /const pageDescription = computed\(\(\) => route\.meta\.description \|\| ''\)/)
|
||||
assert.match(layoutSource, /<div class="client-header-heading">\s*<h1>\{\{ pageTitle \}\}<\/h1>\s*<p v-if="pageDescription">\{\{ pageDescription \}\}<\/p>/s)
|
||||
assert.match(layoutSource, /<div class="client-user">[\s\S]*\{\{ auth\.user\?\.name \}\}[\s\S]*aria-label="退出登录"/)
|
||||
assert.doesNotMatch(layoutSource, /Building2|tenant-name|tenantName/)
|
||||
assert.doesNotMatch(routerSource, /showPageHeading|showTenantName/)
|
||||
})
|
||||
|
||||
test('source contract: uses compact desktop padding and a bounded mobile header', () => {
|
||||
assert.match(shellStyles, /\.top-header \{[^}]*padding: 0 20px;/)
|
||||
assert.match(shellStyles, /\.client-main \{[^}]*padding: 88px 20px 40px;/)
|
||||
assert.match(globalStyles, /@media \(max-width: 720px\) \{[\s\S]*\.top-header \{ padding: 0 14px; \}/)
|
||||
assert.match(globalStyles, /\.client-brand \{ display: none; \}/)
|
||||
assert.match(globalStyles, /\.client-header-menu-button \{ display: grid; \}/)
|
||||
assert.match(globalStyles, /\.client-header-heading p, \.client-user-copy \{ display: none; \}/)
|
||||
})
|
||||
|
||||
test('source contract: closes the mobile menu on route changes and Escape', () => {
|
||||
assert.match(layoutSource, /watch\(\(\) => route\.fullPath, closeMobileMenu\)/)
|
||||
assert.match(layoutSource, /event\.key === 'Escape'/)
|
||||
|
||||
+17
-25
@@ -2,12 +2,7 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Building2,
|
||||
LogOut,
|
||||
Menu,
|
||||
X,
|
||||
} from '@lucide/vue'
|
||||
import { LogOut, Menu, X } from '@lucide/vue'
|
||||
import baiyueLogoDark from '@/assets/brand/baiyue-logo-dark.png'
|
||||
import ClientSidebarMenu from '@/components/ClientSidebarMenu.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -100,15 +95,25 @@ async function handleLogout() {
|
||||
|
||||
<template>
|
||||
<div class="client-shell" :class="{ 'mobile-menu-open': mobileMenuOpen }">
|
||||
<header
|
||||
class="top-header"
|
||||
:inert="mobileMenuOpen"
|
||||
:aria-hidden="mobileMenuOpen ? 'true' : undefined"
|
||||
>
|
||||
<header class="top-header" :inert="mobileMenuOpen" :aria-hidden="mobileMenuOpen ? 'true' : undefined">
|
||||
<RouterLink class="client-brand" to="/">
|
||||
<img class="client-brand-logo" :src="baiyueLogoDark" alt="柏悦物流" />
|
||||
</RouterLink>
|
||||
<div class="tenant-name"><Building2 :size="17" /> {{ auth.user?.tenantName }}</div>
|
||||
<button
|
||||
ref="menuButtonRef"
|
||||
type="button"
|
||||
class="client-header-menu-button"
|
||||
aria-label="打开导航"
|
||||
aria-controls="client-sidebar"
|
||||
:aria-expanded="mobileMenuOpen"
|
||||
@click="openMobileMenu"
|
||||
>
|
||||
<Menu :size="20" />
|
||||
</button>
|
||||
<div class="client-header-heading">
|
||||
<h1>{{ pageTitle }}</h1>
|
||||
<p v-if="pageDescription">{{ pageDescription }}</p>
|
||||
</div>
|
||||
<div class="client-user">
|
||||
<span class="client-avatar">{{ auth.user?.name.slice(0, 1) }}</span>
|
||||
<span class="client-user-copy"><strong>{{ auth.user?.name }}</strong><small>{{ auth.user?.roles.join('、') }}</small></span>
|
||||
@@ -156,19 +161,6 @@ async function handleLogout() {
|
||||
:inert="mobileMenuOpen"
|
||||
:aria-hidden="mobileMenuOpen ? 'true' : undefined"
|
||||
>
|
||||
<div class="client-page-heading">
|
||||
<button
|
||||
ref="menuButtonRef"
|
||||
type="button"
|
||||
aria-label="打开导航"
|
||||
aria-controls="client-sidebar"
|
||||
:aria-expanded="mobileMenuOpen"
|
||||
@click="openMobileMenu"
|
||||
>
|
||||
<Menu :size="20" />
|
||||
</button>
|
||||
<div><h1>{{ pageTitle }}</h1><p>{{ pageDescription }}</p></div>
|
||||
</div>
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -102,11 +102,11 @@
|
||||
.client-sidebar-header strong { font-size: 13px; }
|
||||
.client-sidebar-header button { display: grid; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 8px; background: transparent; color: var(--navy); place-items: center; }
|
||||
.client-main { width: 100%; max-width: 100%; margin-left: 0; padding: 88px 14px 30px; }
|
||||
.client-brand { width: auto; }
|
||||
.client-brand-logo { width: 132px; height: 50px; }
|
||||
.tenant-name, .client-user-copy { display: none; }
|
||||
.client-brand { display: none; }
|
||||
.client-header-menu-button { display: grid; }
|
||||
.client-header-heading p, .client-user-copy { display: none; }
|
||||
.client-header-heading h1 { font-size: 16px; }
|
||||
.client-user { margin-left: auto; }
|
||||
.client-page-heading > button { display: grid; width: 44px; height: 44px; place-items: center; }
|
||||
.dashboard-load-error { grid-template-columns: 1fr; justify-items: start; }
|
||||
.dashboard-load-error .el-button { width: 100%; min-height: 44px; }
|
||||
.client-stats { grid-template-columns: 1fr; }
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
.client-shell { min-height: 100vh; overflow-x: hidden; }
|
||||
.top-header { position: fixed; inset: 0 0 auto 0; z-index: 20; display: flex; width: 100%; max-width: 100vw; height: 68px; align-items: center; padding: 0 28px; border-bottom: 1px solid var(--border); background: #fff; }
|
||||
.top-header { position: fixed; inset: 0 0 auto 0; z-index: 20; display: flex; width: 100%; max-width: 100vw; height: 68px; align-items: center; gap: 16px; padding: 0 20px; border-bottom: 1px solid var(--border); background: #fff; }
|
||||
.client-brand { display: flex; width: 232px; min-width: 0; align-items: center; color: var(--navy); text-decoration: none; }
|
||||
.client-brand-logo { display: block; width: 150px; height: 54px; object-fit: contain; object-position: left center; }
|
||||
.tenant-name { display: flex; flex: 1; align-items: center; gap: 8px; color: #475467; font-size: 13px; }
|
||||
.client-header-menu-button { display: none; width: 44px; height: 44px; flex: 0 0 44px; padding: 0; border: 0; border-radius: 8px; background: transparent; color: var(--navy); place-items: center; }
|
||||
.client-header-heading { min-width: 0; flex: 1; }
|
||||
.client-header-heading h1 { overflow: hidden; margin: 0; color: var(--navy); font-size: 18px; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.client-header-heading p { overflow: hidden; margin: 2px 0 0; color: var(--muted); 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: #e7f3f1; color: var(--primary); font-weight: 750; }
|
||||
.client-user-copy { display: flex; flex-direction: column; }
|
||||
@@ -31,16 +34,11 @@
|
||||
.client-sidebar-link span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.client-sidebar-group-trigger:focus-visible,
|
||||
.client-sidebar-link:focus-visible,
|
||||
.client-page-heading > button:focus-visible,
|
||||
.client-header-menu-button:focus-visible,
|
||||
.client-sidebar-header button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
||||
.client-sidebar-header,
|
||||
.client-sidebar-scrim { display: none; }
|
||||
.sidebar-help { display: flex; flex: 0 0 auto; flex-direction: column; gap: 4px; margin-top: 16px; padding: 14px; border-left: 3px solid var(--primary); background: #f2f7f6; }
|
||||
.sidebar-help strong { font-size: 12px; }
|
||||
.sidebar-help span { color: var(--muted); font-size: 11px; }
|
||||
.client-main { width: calc(100% - 232px); max-width: 100%; min-height: 100vh; margin-left: 232px; padding: 92px 28px 40px; }
|
||||
.client-page-heading { display: flex; min-width: 0; align-items: center; gap: 12px; margin-bottom: 22px; }
|
||||
.client-page-heading > div { min-width: 0; }
|
||||
.client-page-heading button { display: none; width: 44px; height: 44px; flex: 0 0 44px; padding: 0; border: 0; border-radius: 8px; background: none; color: var(--navy); }
|
||||
.client-page-heading h1 { margin: 0; font-size: 22px; }
|
||||
.client-page-heading p { margin: 5px 0 0; color: var(--muted); font-size: 12px; }
|
||||
.client-main { width: calc(100% - 232px); max-width: 100%; min-height: 100vh; margin-left: 232px; padding: 88px 20px 40px; }
|
||||
|
||||
@@ -17,6 +17,7 @@ test('registers the account profile route and navigation entry', () => {
|
||||
test('implements profile view, edit, save and cancel states', () => {
|
||||
assert.match(viewSource, /基本信息/)
|
||||
assert.doesNotMatch(viewSource, /account-identity/)
|
||||
assert.doesNotMatch(viewSource, /<h2>基本信息<\/h2>|<h2>修改密码<\/h2>/)
|
||||
assert.match(viewSource, /function startEditing\(\)/)
|
||||
assert.match(viewSource, /function cancelEditing\(\)/)
|
||||
assert.match(viewSource, /async function saveProfile\(\)/)
|
||||
|
||||
@@ -4,10 +4,12 @@ import { useRouter } from 'vue-router'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
CheckCircle2,
|
||||
KeyRound,
|
||||
Pencil,
|
||||
Save,
|
||||
ShieldCheck,
|
||||
UserRound,
|
||||
X,
|
||||
} from '@lucide/vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -172,10 +174,9 @@ async function changePassword() {
|
||||
<el-tab-pane name="profile">
|
||||
<template #label><span class="account-tab-label"><UserRound :size="16" />基本信息</span></template>
|
||||
|
||||
<header class="account-section-heading">
|
||||
<div><h2>基本信息</h2><p>维护当前登录人的联系方式;账号和所属企业由系统身份关系确定。</p></div>
|
||||
<el-button v-if="!editing" type="primary" @click="startEditing"><Pencil :size="16" />编辑信息</el-button>
|
||||
</header>
|
||||
<div v-if="!editing" class="account-actions">
|
||||
<el-button type="primary" @click="startEditing"><Pencil :size="16" />编辑信息</el-button>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
v-if="editing"
|
||||
@@ -217,9 +218,6 @@ async function changePassword() {
|
||||
|
||||
<div class="password-layout">
|
||||
<div>
|
||||
<header class="account-section-heading password-heading">
|
||||
<div><h2>修改密码</h2><p>输入当前密码并设置 8–64 位、同时包含字母和数字的新密码。</p></div>
|
||||
</header>
|
||||
<el-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
@@ -270,10 +268,7 @@ async function changePassword() {
|
||||
.account-tabs :deep(.el-tabs__header) { margin-bottom: 0; }
|
||||
.account-tabs :deep(.el-tabs__item) { min-height: 52px; }
|
||||
.account-tab-label { display: inline-flex; align-items: center; gap: 7px; }
|
||||
.account-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; padding: 24px 0 20px; }
|
||||
.account-section-heading h2 { margin: 0 0 6px; color: var(--navy); font-size: 18px; }
|
||||
.account-section-heading p { max-width: 620px; margin: 0; color: var(--muted); font-size: 12px; line-height: 1.7; }
|
||||
.account-actions { display: flex; gap: 8px; }
|
||||
.account-actions { display: flex; justify-content: flex-end; gap: 8px; margin: 20px 0; }
|
||||
.account-info-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--border); border-left: 1px solid var(--border); }
|
||||
.account-info-grid div { min-width: 0; min-height: 94px; padding: 18px 20px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||||
.account-info-grid dt { color: var(--muted); font-size: 11px; }
|
||||
@@ -283,7 +278,6 @@ async function changePassword() {
|
||||
.account-form-actions { display: flex; grid-column: 1 / -1; justify-content: flex-end; gap: 8px; padding-top: 2px; }
|
||||
.field-help { margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.5; }
|
||||
.password-layout { display: grid; grid-template-columns: minmax(320px, 520px) minmax(240px, 1fr); align-items: start; gap: clamp(36px, 7vw, 90px); }
|
||||
.password-heading { padding-bottom: 18px; }
|
||||
.password-form { max-width: 440px; }
|
||||
.password-form :deep(.el-form-item) { margin-bottom: 20px; }
|
||||
.password-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 2px; }
|
||||
@@ -304,15 +298,14 @@ async function changePassword() {
|
||||
.security-note code { color: var(--navy); font-weight: 700; }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.account-context { align-items: flex-start; flex-direction: column; gap: 7px; }
|
||||
.password-layout { grid-template-columns: 1fr; gap: 14px; }
|
||||
.security-note { margin-top: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.account-workspace { min-height: 0; padding: 0 16px 22px; }
|
||||
.account-section-heading { align-items: stretch; flex-direction: column; gap: 14px; padding-top: 20px; }
|
||||
.account-section-heading > .el-button { width: 100%; }
|
||||
.account-actions { width: 100%; margin: 16px 0; }
|
||||
.account-actions .el-button { width: 100%; min-height: 44px; }
|
||||
.account-form-actions { width: 100%; }
|
||||
.account-form-actions .el-button { min-height: 44px; flex: 1; }
|
||||
.account-info-grid, .account-form-grid { grid-template-columns: 1fr; }
|
||||
|
||||
@@ -13,11 +13,11 @@ const store = useTenantAdministrationStore()
|
||||
const auth = useAuthStore()
|
||||
const companyApproved = computed(() => store.company.auditStatus === 'ACTIVE')
|
||||
const auditStatusCopy = computed(() => ({
|
||||
PROFILE_REQUIRED: { label: '待完善资料', title: '请上传营业执照', detail: '营业执照提交审核前不能提交头程订单。' },
|
||||
PENDING: { label: '审核中', title: '企业资料正在等待平台审核', detail: '审核期间仍可维护资料和保存订单草稿;审核通过前不能提交订单。' },
|
||||
ACTIVE: { label: '已认证', title: '企业资料已审核,下单资格已开通', detail: '您可以创建、保存并提交头程订单。' },
|
||||
REJECTED: { label: '审核驳回', title: '企业资料审核未通过', detail: '请根据平台反馈修正资料;当前后端暂不支持同一企业重新上传。' },
|
||||
DISABLED: { label: '企业已停用', title: '企业账号已被平台停用', detail: '当前不能提交订单,请联系平台客服核实。' },
|
||||
PROFILE_REQUIRED: { title: '请上传营业执照', detail: '营业执照提交审核前不能提交头程订单。' },
|
||||
PENDING: { title: '企业资料正在等待平台审核', detail: '审核期间仍可维护资料和保存订单草稿;审核通过前不能提交订单。' },
|
||||
ACTIVE: { title: '企业资料已审核,下单资格已开通', detail: '您可以创建、保存并提交头程订单。' },
|
||||
REJECTED: { title: '企业资料审核未通过', detail: '请根据平台反馈修正资料;当前后端暂不支持同一企业重新上传。' },
|
||||
DISABLED: { title: '企业账号已被平台停用', detail: '当前不能提交订单,请联系平台客服核实。' },
|
||||
})[store.company.auditStatus])
|
||||
const activeTab = ref('basic')
|
||||
const editing = ref(false)
|
||||
@@ -214,15 +214,6 @@ function qualificationStatus(status: string) {
|
||||
<el-button :loading="loading" @click="loadCompanyContext">重新加载</el-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="profile-summary-card client-card">
|
||||
<div class="company-mark"><Building2 :size="28" /></div>
|
||||
<div class="company-summary">
|
||||
<div><h2>{{ store.company.companyName }}</h2><el-tag :type="companyApproved ? 'success' : store.company.auditStatus === 'REJECTED' || store.company.auditStatus === 'DISABLED' ? 'danger' : 'warning'" effect="light">{{ auditStatusCopy.label }}</el-tag></div>
|
||||
<p>{{ store.company.creditCode || '统一社会信用代码暂未填写' }}</p>
|
||||
</div>
|
||||
<el-button v-if="auth.hasPermission('company:profile:edit')" type="primary" @click="startEdit"><Pencil :size="16" />编辑企业资料</el-button>
|
||||
</div>
|
||||
|
||||
<div class="company-audit-notice" :class="companyApproved ? 'is-approved' : 'is-pending'" role="status">
|
||||
<component :is="companyApproved ? CheckCircle2 : Clock3" :size="21" />
|
||||
<div><strong>{{ auditStatusCopy.title }}</strong><span>{{ auditStatusCopy.detail }}</span></div>
|
||||
@@ -232,6 +223,7 @@ function qualificationStatus(status: string) {
|
||||
<el-tab-pane name="basic">
|
||||
<template #label><span class="tab-label"><Building2 :size="16" />企业基本信息</span></template>
|
||||
<div class="client-card info-panel">
|
||||
<header class="info-panel-header"><h3>企业基本信息</h3><el-button v-if="auth.hasPermission('company:profile:edit')" type="primary" @click="startEdit"><Pencil :size="16" />编辑企业资料</el-button></header>
|
||||
<dl class="profile-grid">
|
||||
<div><dt>公司名称</dt><dd>{{ store.company.companyName || '-' }}</dd></div>
|
||||
<div><dt>统一社会信用代码</dt><dd>{{ store.company.creditCode || '-' }}</dd></div>
|
||||
@@ -243,6 +235,7 @@ function qualificationStatus(status: string) {
|
||||
<el-tab-pane name="contact">
|
||||
<template #label><span class="tab-label"><UserRound :size="16" />联系信息</span></template>
|
||||
<div class="client-card info-panel">
|
||||
<header class="info-panel-header"><h3>联系信息</h3></header>
|
||||
<dl class="profile-grid">
|
||||
<div><dt>联系人</dt><dd>{{ store.company.contactName || '-' }}</dd></div>
|
||||
<div><dt>联系电话</dt><dd>{{ store.company.contactPhone || '-' }}</dd></div>
|
||||
@@ -346,16 +339,13 @@ function qualificationStatus(status: string) {
|
||||
.company-audit-notice.is-approved { border-color: #bcd9d2; background: #eff8f5; color: #20745f; }
|
||||
.qualification-preview-image { display: block; max-width: 100%; max-height: 360px; margin: 0 auto 14px; object-fit: contain; }
|
||||
.company-audit-notice.is-pending { border-color: #e5d2a5; background: #fff9eb; color: #9a6a12; }
|
||||
.profile-summary-card { display: flex; align-items: center; gap: 16px; }
|
||||
.company-mark { display: grid; width: 58px; height: 58px; flex: 0 0 58px; place-items: center; background: #e7f3f1; color: var(--primary); }
|
||||
.company-summary { flex: 1; min-width: 0; }
|
||||
.company-summary > div { display: flex; align-items: center; gap: 10px; }
|
||||
.company-summary h2 { overflow: hidden; margin: 0; color: var(--navy); font-size: 20px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.company-summary p, .qualification-panel header p { margin: 6px 0 0; color: var(--muted); font-size: 12px; }
|
||||
.qualification-panel header p { margin: 6px 0 0; color: var(--muted); font-size: 12px; }
|
||||
.profile-tabs :deep(.el-tabs__header) { margin-bottom: 14px; padding: 0 16px; border: 1px solid var(--border); background: #fff; }
|
||||
.profile-tabs :deep(.el-tabs__nav-wrap::after) { display: none; }
|
||||
.tab-label { display: inline-flex; align-items: center; gap: 7px; }
|
||||
.info-panel { padding: 0; }
|
||||
.info-panel-header { min-height: 66px; padding: 14px 22px; border-bottom: 1px solid var(--border); }
|
||||
.info-panel-header h3 { margin: 0; font-size: 15px; }
|
||||
.profile-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; }
|
||||
.profile-grid > div { min-height: 86px; padding: 20px 22px; border-bottom: 1px solid var(--border); }
|
||||
.profile-grid > div:nth-child(odd) { border-right: 1px solid var(--border); }
|
||||
@@ -385,8 +375,6 @@ function qualificationStatus(status: string) {
|
||||
.document-preview dd { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--navy); font-size: 12px; font-weight: 650; }
|
||||
.document-preview > span { max-width: 440px; padding: 0 16px; color: var(--muted); font-size: 11px; line-height: 1.6; text-align: center; }
|
||||
@media (max-width: 720px) {
|
||||
.profile-summary-card { align-items: flex-start; flex-wrap: wrap; }
|
||||
.profile-summary-card > .el-button { width: 100%; }
|
||||
.profile-grid, .form-grid-two { grid-template-columns: 1fr; }
|
||||
.profile-grid > div:nth-child(odd) { border-right: 0; }
|
||||
.qualification-panel > header { align-items: flex-start; gap: 12px; }
|
||||
|
||||
@@ -46,7 +46,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" :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"/)
|
||||
@@ -54,6 +54,14 @@ test('uses the shared query toolbar and data table without metric cards', () =>
|
||||
assert.doesNotMatch(source, /order-metrics|订单概览/)
|
||||
})
|
||||
|
||||
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.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;/)
|
||||
})
|
||||
|
||||
test('does not expose arbitrary status mutation controls to customers', () => {
|
||||
assert.doesNotMatch(source, /setStatusForPreview/)
|
||||
assert.doesNotMatch(source, /状态更新|更新状态/)
|
||||
|
||||
@@ -524,23 +524,23 @@ async function searchOrders(model: QueryModel) {
|
||||
|
||||
<template>
|
||||
<section class="first-leg-page">
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
<QueryToolbar v-model="filters" :fields="queryFields" ariaLabel="头程订单查询" @search="searchOrders">
|
||||
<template #actions>
|
||||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :disabled="availableWarehouses.length === 0" @click="openCreate"><FilePlus2 :size="17" />创建订单</el-button>
|
||||
</template>
|
||||
</QueryToolbar>
|
||||
<QueryToolbar v-model="filters" :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>
|
||||
<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">
|
||||
<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>
|
||||
<div v-if="store.loadError" class="order-load-state" role="alert">
|
||||
<AlertTriangle :size="24" />
|
||||
<div><strong>订单数据加载失败</strong><span>{{ store.loadError }}</span></div>
|
||||
@@ -725,6 +725,8 @@ 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; }
|
||||
.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; }
|
||||
@@ -738,6 +740,11 @@ async function searchOrders(model: QueryModel) {
|
||||
.supporting-data-error span { overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.55; }
|
||||
.order-row-actions, .editor-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.order-table-card { padding: 0; overflow: hidden; }
|
||||
.order-table-toolbar { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 16px; border-bottom: 1px solid var(--border); }
|
||||
.order-table-toolbar > div { display: flex; min-width: 0; align-items: baseline; gap: 8px; }
|
||||
.order-table-toolbar strong { color: var(--navy); font-size: 14px; }
|
||||
.order-table-toolbar span { color: var(--muted); font-size: 11px; }
|
||||
.order-table-card > .order-eligibility, .order-table-card > .supporting-data-error { border-width: 0 0 1px; }
|
||||
.order-load-state { display: grid; min-height: 220px; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14px; padding: 28px; color: #9a5a09; }
|
||||
.order-load-state > div { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
|
||||
.order-load-state strong { color: var(--navy); font-size: 14px; }
|
||||
@@ -828,6 +835,10 @@ async function searchOrders(model: QueryModel) {
|
||||
.mobile-order-card footer > div { display: flex; }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
:global(.client-main:has(> .first-leg-page) > .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; }
|
||||
.order-eligibility { flex-wrap: wrap; }
|
||||
.order-eligibility .el-button { width: 100%; min-height: 44px; }
|
||||
.order-form-grid, .cargo-form-grid, .order-detail-grid, .party-editor-grid, .party-snapshot-fields { grid-template-columns: 1fr; }
|
||||
|
||||
@@ -163,11 +163,10 @@ 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">
|
||||
<template #actions><el-button v-if="auth.hasPermission('company:member:write')" type="primary" @click="openCreate"><Plus :size="17" />新增成员</el-button></template>
|
||||
</QueryToolbar>
|
||||
<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>
|
||||
<DataTable v-model:page="memberPage" v-model:page-size="memberPageSize" class="desktop-member-table" :data="pagedMembers" :columns="memberTableColumns" :loading="loading" :total="filteredMembers.length" row-key="id">
|
||||
<template #actions="{ row }"><el-button v-if="auth.hasPermission('company:member:write')" link type="primary" @click="openEdit(row as TenantMember)"><Pencil :size="15" />编辑</el-button><el-button v-if="auth.hasPermission('company:member:reset-password')" link type="primary" @click="resetPassword(row as TenantMember)"><KeyRound :size="15" />重置密码</el-button><el-button v-if="auth.hasPermission('company:member:write') && !(row as TenantMember).isAdministrator" link :type="(row as TenantMember).status === 'ACTIVE' ? 'danger' : 'success'" @click="toggleStatus(row as TenantMember)">{{ (row as TenantMember).status === 'ACTIVE' ? '停用' : '启用' }}</el-button><span v-if="!auth.hasPermission('company:member:write') && !auth.hasPermission('company:member:reset-password')" class="readonly-action">仅查看</span></template>
|
||||
<template #member="{ row }"><div class="member-identity"><span>{{ (row as TenantMember).name.slice(0, 1) }}</span><div><strong>{{ (row as TenantMember).name }}</strong><small>{{ (row as TenantMember).account }}</small></div></div></template>
|
||||
@@ -209,6 +208,7 @@ async function resetPassword(member: TenantMember) {
|
||||
.collection-load-error > div { display: flex; max-width: 520px; flex-direction: column; gap: 6px; }
|
||||
.collection-load-error span { color: var(--muted); font-size: 12px; line-height: 1.6; }
|
||||
.member-table-card { padding: 0; overflow: hidden; }
|
||||
.table-actions { display: flex; justify-content: flex-end; padding: 12px 16px; border-bottom: 1px solid var(--border); }
|
||||
.member-identity { display: flex; align-items: center; gap: 10px; }
|
||||
.member-identity > span { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 50%; background: #e7f3f1; color: var(--primary); font-weight: 750; }
|
||||
.member-identity > div, .member-contact { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
||||
|
||||
@@ -94,8 +94,7 @@ async function savePermissions() {
|
||||
<el-button :loading="loading" @click="loadRoles">重新加载</el-button>
|
||||
</div>
|
||||
<template v-else-if="store.roles.length">
|
||||
<aside class="client-card role-selector">
|
||||
<header><div><span class="section-label">ROLES</span><h3>选择角色</h3></div></header>
|
||||
<aside class="client-card role-selector" aria-label="选择角色">
|
||||
<button v-for="role in store.roles" :key="role.id" type="button" :class="{ active: role.id === selectedRoleId }" @click="selectedRoleId = role.id">
|
||||
<span class="role-select-icon"><LockKeyhole v-if="role.builtin" :size="17" /><ShieldCheck v-else :size="17" /></span>
|
||||
<span><strong>{{ role.name }}</strong><small>{{ role.code }}</small></span>
|
||||
@@ -104,10 +103,7 @@ async function savePermissions() {
|
||||
</aside>
|
||||
|
||||
<div class="permission-content">
|
||||
<div class="permission-heading client-card">
|
||||
<div><span class="section-label">PERMISSIONS</span><h2>{{ selectedRole?.name }}</h2><p>{{ selectedRole?.description }}</p></div>
|
||||
<div class="permission-actions"><el-button :disabled="!editable || !hasChanges || saving" @click="resetPermissions"><RotateCcw :size="16" />撤销修改</el-button><el-button type="primary" :loading="saving" :disabled="!editable || !hasChanges || saving" @click="savePermissions"><Save :size="16" />保存权限</el-button></div>
|
||||
</div>
|
||||
<div class="permission-actions" aria-label="权限操作"><el-button :disabled="!editable || !hasChanges || saving" @click="resetPermissions"><RotateCcw :size="16" />撤销修改</el-button><el-button type="primary" :loading="saving" :disabled="!editable || !hasChanges || saving" @click="savePermissions"><Save :size="16" />保存权限</el-button></div>
|
||||
|
||||
<div v-if="selectedRole?.builtin" class="permission-notice"><LockKeyhole :size="18" /><span>当前角色为系统内置角色,权限由平台统一维护,不允许修改。</span></div>
|
||||
<div v-else-if="selectedRole?.status === 'INACTIVE'" class="permission-notice warning"><span>该角色已停用,请先启用角色后再调整权限。</span></div>
|
||||
@@ -141,8 +137,6 @@ async function savePermissions() {
|
||||
.permission-load-error span { color: var(--muted); font-size: 12px; line-height: 1.6; }
|
||||
.permission-empty { display: grid; place-items: center; color: var(--muted); font-size: 12px; text-align: center; }
|
||||
.role-selector { position: sticky; top: 84px; padding: 12px; }
|
||||
.role-selector header { padding: 8px 8px 12px; }
|
||||
.role-selector h3 { margin-bottom: 0; }
|
||||
.role-selector > button { display: flex; width: 100%; min-height: 58px; align-items: center; gap: 9px; margin: 3px 0; padding: 9px; border: 0; border-radius: 8px; background: transparent; color: var(--navy); cursor: pointer; text-align: left; }
|
||||
.role-selector > button:hover, .role-selector > button.active { background: #edf6f4; }
|
||||
.role-selector > button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
||||
@@ -152,10 +146,7 @@ async function savePermissions() {
|
||||
.role-selector strong { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.role-selector small { color: var(--muted); font-size: 9px; }
|
||||
.permission-content { display: flex; min-width: 0; flex-direction: column; gap: 12px; }
|
||||
.permission-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
||||
.permission-heading h2 { margin: 5px 0; font-size: 19px; }
|
||||
.permission-heading p { margin: 0; color: var(--muted); font-size: 11px; }
|
||||
.permission-actions { display: flex; gap: 8px; }
|
||||
.permission-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.permission-notice { display: flex; align-items: center; gap: 9px; padding: 12px 15px; border: 1px solid #cfe1dc; background: #edf6f4; color: #45615d; font-size: 11px; }
|
||||
.permission-notice.warning { border-color: #f1d4a8; background: #fff7ed; color: #9a5a09; }
|
||||
.permission-groups { display: grid; gap: 12px; }
|
||||
@@ -172,7 +163,7 @@ async function savePermissions() {
|
||||
.permission-item strong { color: var(--navy); font-size: 12px; }
|
||||
.permission-item small { color: var(--muted); font-size: 10px; line-height: 1.5; }
|
||||
.permission-item code { color: #86939a; font-size: 9px; }
|
||||
@media (max-width: 900px) { .permissions-page { grid-template-columns: 1fr; } .role-selector { position: static; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } .role-selector header { grid-column: 1 / -1; } }
|
||||
@media (max-width: 720px) { .permission-heading { align-items: flex-start; flex-direction: column; } .permission-actions { width: 100%; } .permission-actions .el-button { flex: 1; } .permission-group :deep(.el-checkbox-group) { grid-template-columns: 1fr; } .permission-item:nth-child(odd) { border-right: 0; } }
|
||||
@media (max-width: 480px) { .role-selector { grid-template-columns: 1fr; } .role-selector header { grid-column: auto; } }
|
||||
@media (max-width: 900px) { .permissions-page { grid-template-columns: 1fr; } .role-selector { position: static; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 720px) { .permission-actions { width: 100%; } .permission-actions .el-button { flex: 1; } .permission-group :deep(.el-checkbox-group) { grid-template-columns: 1fr; } .permission-item:nth-child(odd) { border-right: 0; } }
|
||||
@media (max-width: 480px) { .role-selector { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
|
||||
@@ -144,11 +144,10 @@ 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">
|
||||
<template #actions><el-button v-if="auth.hasPermission('company:role:write')" type="primary" @click="openCreate"><Plus :size="17" />新增角色</el-button></template>
|
||||
</QueryToolbar>
|
||||
<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>
|
||||
<DataTable v-model:page="rolePage" v-model:page-size="rolePageSize" :data="pagedRoles" :columns="roleTableColumns" :loading="loading" :total="filteredRoles.length" row-key="id">
|
||||
<template #actions="{ row }"><el-button v-if="auth.hasPermission('company:permission:list')" link type="primary" :disabled="actionRoleId === (row as TenantRole).id" @click="assignPermissions(row as TenantRole)">{{ auth.hasPermission('company:permission:assign') && !(row as TenantRole).builtin ? '分配权限' : '查看权限' }}</el-button><el-button v-if="auth.hasPermission('company:role:write') && !(row as TenantRole).builtin" link type="primary" :disabled="actionRoleId === (row as TenantRole).id" @click="openEdit(row as TenantRole)"><Pencil :size="15" />编辑</el-button><el-button v-if="auth.hasPermission('company:role:write') && !(row as TenantRole).builtin" link :type="(row as TenantRole).status === 'ACTIVE' ? 'warning' : 'success'" :loading="actionRoleId === (row as TenantRole).id" @click="toggleStatus(row as TenantRole)">{{ (row as TenantRole).status === 'ACTIVE' ? '停用' : '启用' }}</el-button><el-button v-if="auth.hasPermission('company:role:write') && !(row as TenantRole).builtin" link type="danger" :disabled="actionRoleId === (row as TenantRole).id" @click="removeRole(row as TenantRole)"><Trash2 :size="15" />删除</el-button></template>
|
||||
<template #role="{ row }"><div class="role-identity"><span class="role-icon"><LockKeyhole v-if="(row as TenantRole).builtin" :size="20" /><ShieldCheck v-else :size="20" /></span><div><strong>{{ (row as TenantRole).name }}</strong><code>{{ (row as TenantRole).code }}</code></div></div></template>
|
||||
@@ -178,6 +177,7 @@ function assignPermissions(role: TenantRole) {
|
||||
.collection-load-error > div { display: flex; max-width: 520px; flex-direction: column; gap: 6px; }
|
||||
.collection-load-error span { color: var(--muted); font-size: 12px; line-height: 1.6; }
|
||||
.role-table-card { padding: 0; overflow: hidden; }
|
||||
.table-actions { display: flex; justify-content: flex-end; padding: 12px 16px; border-bottom: 1px solid var(--border); }
|
||||
.role-identity { display: flex; min-width: 0; align-items: center; gap: 10px; }
|
||||
.role-icon { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; background: #e7f3f1; color: var(--primary); }
|
||||
.role-identity > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
||||
|
||||
@@ -32,6 +32,8 @@ test('implements company details, contact information and qualification workflow
|
||||
assert.match(profileSource, /slotName: 'actions'/)
|
||||
assert.match(profileSource, /<DataTable[\s\S]*v-model:page="qualificationPage"[\s\S]*v-model:page-size="qualificationPageSize"[\s\S]*:columns="qualificationColumns"/)
|
||||
assert.doesNotMatch(profileSource, /<el-table class="desktop-qualification-table"/)
|
||||
assert.doesNotMatch(profileSource, /class="profile-summary-card"/)
|
||||
assert.match(profileSource, /class="info-panel-header"[\s\S]*auth\.hasPermission\('company:profile:edit'\)/)
|
||||
assert.match(profileSource, /ref="profileFormRef" :model="draft" :rules="profileFormRules"/)
|
||||
assert.match(profileSource, /prop="companyName" required><el-input v-model="draft\.companyName" placeholder="请输入公司名称"/)
|
||||
assert.match(profileSource, /prop="fileName" required :error="qualificationFileError"/)
|
||||
@@ -66,6 +68,8 @@ test('implements member list, role assignment, password reset and status control
|
||||
assert.match(membersSource, /<QueryToolbar v-model="memberFilters"/)
|
||||
assert.match(membersSource, /<DataTable v-model:page="memberPage" v-model:page-size="memberPageSize"/)
|
||||
assert.match(membersSource, /fixed: 'left'/)
|
||||
assert.match(membersSource, /class="table-actions"/)
|
||||
assert.doesNotMatch(membersSource, /template #actions><el-button v-if="auth\.hasPermission\('company:member:write'\)/)
|
||||
assert.doesNotMatch(membersSource, /member-metrics/)
|
||||
})
|
||||
|
||||
@@ -91,6 +95,8 @@ test('protects built-in roles and exposes role permission assignment', () => {
|
||||
assert.match(rolesSource, /<QueryToolbar v-model="roleFilters"/)
|
||||
assert.match(rolesSource, /<DataTable v-model:page="rolePage" v-model:page-size="rolePageSize"/)
|
||||
assert.match(rolesSource, /fixed: 'left'/)
|
||||
assert.match(rolesSource, /class="table-actions"/)
|
||||
assert.doesNotMatch(rolesSource, /template #actions><el-button v-if="auth\.hasPermission\('company:role:write'\)/)
|
||||
assert.doesNotMatch(rolesSource, /role-guidance/)
|
||||
})
|
||||
|
||||
@@ -116,6 +122,9 @@ test('implements grouped menu and action permission editing', () => {
|
||||
assert.match(permissionsSource, /const saving = ref\(false\)/)
|
||||
assert.match(permissionsSource, /:loading="saving"/)
|
||||
assert.match(permissionsSource, /button:focus-visible/)
|
||||
assert.match(permissionsSource, /class="permission-actions" aria-label="权限操作"/)
|
||||
assert.doesNotMatch(permissionsSource, /permission-heading/)
|
||||
assert.doesNotMatch(permissionsSource, /section-label/)
|
||||
assert.doesNotMatch(permissionsSource, /当前只读展示权限资源/)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user