feat: update admin navigation and customer management
This commit is contained in:
@@ -26,6 +26,6 @@ const defaultOpeneds = computed(() => getGroupKeys(props.items))
|
||||
:default-openeds="defaultOpeneds"
|
||||
router
|
||||
>
|
||||
<AdminSidebarMenuNode v-for="item in items" :key="item.key" :node="item" />
|
||||
<AdminSidebarMenuNode v-for="item in items" :key="item.key" :node="item" :level="0" />
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
@@ -5,21 +5,36 @@ defineOptions({ name: 'AdminSidebarMenuNode' })
|
||||
|
||||
defineProps<{
|
||||
node: AdminNavigationNode
|
||||
level?: number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-sub-menu v-if="node.children?.length" :index="node.key">
|
||||
<template #title>
|
||||
<component :is="node.icon" :size="19" :stroke-width="1.8" />
|
||||
<component
|
||||
v-if="!level"
|
||||
:is="node.icon"
|
||||
class="menu-icon"
|
||||
:style="{ color: node.iconColor }"
|
||||
:size="20"
|
||||
:stroke-width="1.9"
|
||||
/>
|
||||
<span>{{ node.title }}</span>
|
||||
</template>
|
||||
|
||||
<AdminSidebarMenuNode v-for="child in node.children" :key="child.key" :node="child" />
|
||||
<AdminSidebarMenuNode v-for="child in node.children" :key="child.key" :node="child" :level="(level ?? 0) + 1" />
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item v-else :index="node.path || node.key">
|
||||
<component :is="node.icon" :size="19" :stroke-width="1.8" />
|
||||
<component
|
||||
v-if="!level"
|
||||
:is="node.icon"
|
||||
class="menu-icon"
|
||||
:style="{ color: node.iconColor }"
|
||||
:size="20"
|
||||
:stroke-width="1.9"
|
||||
/>
|
||||
<template #title>{{ node.title }}</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const config = readFileSync(resolve(process.cwd(), 'nginx/default.conf.template'), 'utf8')
|
||||
|
||||
describe('admin nginx deployment config', () => {
|
||||
it('prevents the application shell from retaining stale chunk references', () => {
|
||||
expect(config).toContain('location = /index.html')
|
||||
expect(config).toContain('Cache-Control "no-cache, no-store, must-revalidate"')
|
||||
})
|
||||
|
||||
it('serves hashed assets immutably and never falls back missing chunks to HTML', () => {
|
||||
expect(config).toContain('location /assets/')
|
||||
expect(config).toContain('try_files $uri =404;')
|
||||
expect(config).toContain('Cache-Control "public, max-age=31536000, immutable"')
|
||||
})
|
||||
|
||||
it('keeps Vue history routes on the application shell without caching them', () => {
|
||||
expect(config).toContain('try_files $uri $uri/ /index.html;')
|
||||
expect(config).toContain('Cache-Control "no-cache"')
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ChevronLeft, ChevronRight, LogOut, Menu } from '@lucide/vue'
|
||||
import { LogOut, Menu } from '@lucide/vue'
|
||||
import { getApiErrorDetails } from '@/api/http'
|
||||
import baiyueLogoWhite from '@/assets/brand/baiyue-logo-white.png'
|
||||
import baiyueMarkWhite from '@/assets/brand/baiyue-mark-white.png'
|
||||
@@ -51,6 +51,14 @@ async function handleLogout() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleMenuToggle() {
|
||||
if (window.matchMedia('(max-width: 720px)').matches) {
|
||||
mobileMenuOpen.value = !mobileMenuOpen.value
|
||||
return
|
||||
}
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
watch(() => route.fullPath, () => { mobileMenuOpen.value = false })
|
||||
</script>
|
||||
|
||||
@@ -65,16 +73,17 @@ watch(() => route.fullPath, () => { mobileMenuOpen.value = false })
|
||||
<nav class="side-nav" aria-label="主导航">
|
||||
<AdminSidebarMenu :items="access.filteredNavigation" :active-path="route.path" :collapsed="menuCollapsed" />
|
||||
</nav>
|
||||
<button class="sidebar-toggle" type="button" @click="collapsed = !collapsed">
|
||||
<ChevronRight v-if="collapsed" :size="18" />
|
||||
<ChevronLeft v-else :size="18" />
|
||||
<span v-if="!collapsed">收起导航</span>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<div class="app-main">
|
||||
<header class="app-header" :class="{ 'app-header--without-page-heading': !showPageHeading }">
|
||||
<button class="mobile-menu-button" type="button" aria-label="打开菜单" :aria-expanded="mobileMenuOpen" @click="mobileMenuOpen = true"><Menu :size="20" /></button>
|
||||
<button
|
||||
class="shell-menu-button"
|
||||
type="button"
|
||||
:aria-label="mobileMenuOpen ? '关闭菜单' : collapsed ? '展开导航' : '收起导航'"
|
||||
:aria-expanded="mobileMenuOpen || !collapsed"
|
||||
@click="handleMenuToggle"
|
||||
><Menu :size="22" /></button>
|
||||
<div v-if="showPageHeading" class="page-heading"><h1>{{ pageTitle }}</h1><p v-if="pageDescription">{{ pageDescription }}</p></div>
|
||||
<div class="header-user">
|
||||
<div class="user-avatar">{{ auth.user?.name.slice(0, 1) }}</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface AdminNavigationNode {
|
||||
key: string
|
||||
title: string
|
||||
icon: Component
|
||||
iconColor?: string
|
||||
path?: string
|
||||
permission?: string
|
||||
/** Set to true to remove a prototype module from the sidebar without deleting its route. */
|
||||
@@ -37,6 +38,7 @@ export const adminNavigation: AdminNavigationNode[] = [
|
||||
key: 'customers',
|
||||
title: '客户管理',
|
||||
icon: Building2,
|
||||
iconColor: '#52c41a',
|
||||
children: [
|
||||
{ key: 'customers-list', title: '客户列表', icon: Building2, path: '/customers', permission: 'platform:customer:list' },
|
||||
{ key: 'enterprise-reviews', title: '企业审核', icon: FileCheck2, path: '/customers/reviews', permission: 'platform:customer:review' },
|
||||
@@ -47,6 +49,7 @@ export const adminNavigation: AdminNavigationNode[] = [
|
||||
key: 'domestic-warehouse',
|
||||
title: '国内仓',
|
||||
icon: WarehouseIcon,
|
||||
iconColor: '#52c41a',
|
||||
children: [
|
||||
{ key: 'warehouses', title: '仓库管理', icon: WarehouseIcon, path: '/warehouses', permission: 'platform:warehouse:list' },
|
||||
// { key: 'inbound-list', title: '入库单列表', icon: ClipboardList, path: '/prototype/domestic-warehouse/inbound-list' },
|
||||
@@ -61,6 +64,7 @@ export const adminNavigation: AdminNavigationNode[] = [
|
||||
key: 'first-leg-orders',
|
||||
title: '头程订单',
|
||||
icon: ClipboardList,
|
||||
iconColor: '#1890ff',
|
||||
path: '/orders/first-leg',
|
||||
permission: 'platform:order:list',
|
||||
},
|
||||
@@ -187,6 +191,7 @@ export const adminNavigation: AdminNavigationNode[] = [
|
||||
key: 'general',
|
||||
title: '通用',
|
||||
icon: Database,
|
||||
iconColor: '#722ed1',
|
||||
children: [
|
||||
// { key: 'general-product', title: '产品信息', icon: Database, path: '/prototype/general/product-information' },
|
||||
// { key: 'general-fee', title: '费用项目', icon: Database, path: '/prototype/general/fee-items' },
|
||||
@@ -222,6 +227,7 @@ export const adminNavigation: AdminNavigationNode[] = [
|
||||
key: 'system',
|
||||
title: '系统管理',
|
||||
icon: Settings,
|
||||
iconColor: '#8c8c8c',
|
||||
children: [
|
||||
{ key: 'internal-users', title: '用户管理', icon: Users, path: '/system/users', permission: 'platform:user:list' },
|
||||
{ key: 'roles', title: '角色权限', icon: ShieldCheck, path: '/system/roles', permission: 'platform:role:list' },
|
||||
|
||||
Vendored
+4
-2
@@ -3,8 +3,10 @@
|
||||
color: #172033;
|
||||
background: #f4f6f8;
|
||||
font-synthesis: none;
|
||||
--cc-primary: #155eef;
|
||||
--cc-navy: #172a46;
|
||||
--cc-primary: #667eea;
|
||||
--cc-navy: #667eea;
|
||||
--cc-topbar: rgba(102, 126, 234, .95);
|
||||
--cc-menu-active: #667eea;
|
||||
--cc-border: #e3e8ef;
|
||||
--cc-muted: #667085;
|
||||
}
|
||||
|
||||
@@ -218,7 +218,6 @@
|
||||
.sidebar-toggle { display: none; }
|
||||
.sidebar-scrim { position: fixed; inset: 0 0 0 min(280px, 86vw); z-index: 19; display: block; border: 0; background: rgba(15, 23, 42, .46); }
|
||||
.app-main, .is-collapsed .app-main { margin-left: 0; }
|
||||
.mobile-menu-button { display: inline-grid; place-items: center; }
|
||||
.app-header > .el-dropdown { width: 44px; max-width: 44px; flex: 0 0 44px; }
|
||||
.role-preview-trigger { width: 44px; height: 44px; justify-content: center; padding: 0; }
|
||||
.role-preview-copy, .role-preview-trigger > svg:last-child { display: none; }
|
||||
@@ -271,7 +270,7 @@
|
||||
@media (max-width: 360px) {
|
||||
.app-header { gap: 6px; padding: 8px; }
|
||||
.page-heading h1 { max-width: 100%; font-size: 17px; }
|
||||
.mobile-menu-button, .role-preview-trigger, .header-user .el-button { min-width: 44px; min-height: 44px; }
|
||||
.shell-menu-button, .role-preview-trigger, .header-user .el-button { min-width: 44px; min-height: 44px; }
|
||||
.page-container { padding: 14px 8px 24px; }
|
||||
.forbidden-page { padding-right: 14px; padding-left: 14px; }
|
||||
}
|
||||
|
||||
+25
-20
@@ -1,9 +1,9 @@
|
||||
.app-shell { min-width: 0; min-height: 100vh; }
|
||||
.app-sidebar { position: fixed; inset: 0 auto 0 0; z-index: 20; display: flex; width: 240px; flex-direction: column; background: var(--cc-navy); color: #fff; }
|
||||
.app-sidebar { position: fixed; inset: 0 auto 0 0; z-index: 20; display: flex; width: 240px; flex-direction: column; background: rgba(255,255,255,.96); color: #52525b; box-shadow: 4px 0 32px rgba(0,0,0,.08); }
|
||||
.is-collapsed .app-sidebar { width: 76px; }
|
||||
.brand-block { display: flex; height: 72px; align-items: center; padding: 0 18px; border-bottom: 1px solid rgba(255,255,255,.1); }
|
||||
.brand-block { display: flex; height: 64px; flex: 0 0 64px; align-items: center; padding: 0 18px; background: var(--cc-topbar); }
|
||||
.brand-logo { display: block; object-fit: contain; object-position: left center; }
|
||||
.brand-logo-full { width: 184px; height: 58px; }
|
||||
.brand-logo-full { width: 184px; height: 50px; }
|
||||
.brand-logo-mark { display: none; width: 40px; height: 40px; }
|
||||
.is-collapsed .brand-logo-full { display: none; }
|
||||
.is-collapsed .brand-logo-mark { display: block; }
|
||||
@@ -11,15 +11,15 @@
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 14px 10px;
|
||||
scrollbar-color: rgba(203, 214, 230, .5) transparent;
|
||||
padding: 12px 8px;
|
||||
scrollbar-color: rgba(102, 126, 234, .28) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.side-nav::-webkit-scrollbar { width: 10px; }
|
||||
.side-nav::-webkit-scrollbar-track,
|
||||
.side-nav::-webkit-scrollbar-corner { background: transparent; }
|
||||
.side-nav::-webkit-scrollbar-thumb { border: 3px solid transparent; border-radius: 999px; background: rgba(203, 214, 230, .5); background-clip: padding-box; }
|
||||
.side-nav::-webkit-scrollbar-thumb:hover { background-color: rgba(225, 233, 244, .78); }
|
||||
.side-nav::-webkit-scrollbar-thumb { border: 3px solid transparent; border-radius: 999px; background: rgba(102, 126, 234, .28); background-clip: padding-box; }
|
||||
.side-nav::-webkit-scrollbar-thumb:hover { background-color: rgba(102, 126, 234, .46); }
|
||||
.side-nav::-webkit-scrollbar-button,
|
||||
.side-nav::-webkit-scrollbar-button:single-button,
|
||||
.side-nav::-webkit-scrollbar-button:vertical:decrement,
|
||||
@@ -29,33 +29,38 @@
|
||||
border-right: 0 !important;
|
||||
background: transparent !important;
|
||||
--el-menu-bg-color: transparent;
|
||||
--el-menu-text-color: #bdc7d7;
|
||||
--el-menu-active-color: var(--cc-navy);
|
||||
--el-menu-hover-bg-color: rgba(255,255,255,.08);
|
||||
--el-menu-text-color: #52525b;
|
||||
--el-menu-active-color: var(--cc-menu-active);
|
||||
--el-menu-hover-bg-color: rgba(102,126,234,.08);
|
||||
}
|
||||
.side-nav-menu.el-menu--collapse { width: 56px; }
|
||||
.side-nav-menu .el-menu-item,
|
||||
.side-nav-menu .el-sub-menu__title { height: 44px; margin: 3px 0; border-radius: 8px; line-height: 44px; }
|
||||
.side-nav-menu .el-sub-menu__title { height: 46px; margin: 2px 0; border-radius: 0 12px 12px 0; line-height: 46px; transition: color .2s ease, background .2s ease, transform .2s ease; }
|
||||
.side-nav-menu .el-menu-item:hover,
|
||||
.side-nav-menu .el-sub-menu__title:hover { color: #fff; }
|
||||
.side-nav-menu .el-menu-item.is-active { background: #fff; color: var(--cc-navy); font-weight: 650; }
|
||||
.side-nav-menu .el-sub-menu .el-menu-item { min-width: 0; padding-left: 48px !important; }
|
||||
.side-nav-menu .el-sub-menu__title:hover { color: var(--cc-menu-active); background: linear-gradient(135deg, rgba(102,126,234,.1), rgba(240,147,251,.08)); transform: translateX(3px); }
|
||||
.side-nav-menu .el-menu-item.is-active { background: linear-gradient(135deg, rgba(102,126,234,.18), rgba(240,147,251,.12)); color: var(--cc-menu-active); font-weight: 700; box-shadow: inset 3px 0 var(--cc-menu-active); }
|
||||
.side-nav-menu .el-sub-menu .el-menu { padding: 4px 0; background: linear-gradient(180deg, rgba(102,126,234,.03), transparent) !important; }
|
||||
.side-nav-menu .el-sub-menu .el-menu-item { min-width: 0; height: 42px; margin: 0 12px; padding-left: 54px !important; border-radius: 10px; color: #71717a; line-height: 42px; }
|
||||
.side-nav-menu .el-sub-menu .el-menu-item:hover { background: rgba(102,126,234,.08); color: var(--cc-menu-active); transform: translateX(4px); }
|
||||
.side-nav-menu .el-sub-menu .el-menu-item.is-active { background: rgba(102,126,234,.12); box-shadow: none; color: var(--cc-menu-active); font-weight: 700; }
|
||||
.side-nav-menu .el-menu-item svg,
|
||||
.side-nav-menu .el-sub-menu__title svg { flex: 0 0 auto; margin-right: 12px; }
|
||||
.sidebar-toggle { display: flex; height: 52px; align-items: center; justify-content: center; gap: 8px; border: 0; border-top: 1px solid rgba(255,255,255,.1); background: transparent; color: #aebbd0; cursor: pointer; }
|
||||
.app-main { min-width: 0; min-height: 100vh; margin-left: 240px; }
|
||||
.is-collapsed .app-main { margin-left: 76px; }
|
||||
.app-header { position: sticky; top: 0; z-index: 15; display: flex; min-width: 0; min-height: 72px; align-items: center; gap: 16px; padding: 10px 20px; border-bottom: 1px solid var(--cc-border); background: rgba(255,255,255,.96); }
|
||||
.app-header { position: sticky; top: 0; z-index: 15; display: flex; min-width: 0; min-height: 64px; align-items: center; gap: 16px; padding: 8px 20px; background: var(--cc-topbar); color: #fff; box-shadow: 0 8px 32px rgba(0,0,0,.15), 0 2px 8px rgba(0,0,0,.1); backdrop-filter: blur(20px); }
|
||||
.shell-menu-button { display: grid; width: 44px; height: 44px; flex: 0 0 44px; place-items: center; padding: 0; border: 0; border-radius: 8px; background: transparent; color: #fff; cursor: pointer; transition: background .2s ease, transform .2s ease; }
|
||||
.shell-menu-button:hover { background: rgba(255,255,255,.15); transform: scale(1.05); }
|
||||
.shell-menu-button:focus-visible { outline-color: rgba(255,255,255,.8); }
|
||||
.page-heading { min-width: 0; flex: 1; }
|
||||
.page-heading h1 { margin: 0; overflow: hidden; font-size: 20px; letter-spacing: 0; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.page-heading p { margin: 4px 0 0; overflow: hidden; color: var(--cc-muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.page-heading p { margin: 4px 0 0; overflow: hidden; color: rgba(255,255,255,.78); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.header-user { display: flex; min-width: 0; flex: 0 0 auto; align-items: center; gap: 10px; }
|
||||
.user-avatar { display: grid; width: 36px; height: 36px; place-items: center; border-radius: 50%; background: #eaf1ff; color: var(--cc-primary); font-weight: 700; }
|
||||
.user-avatar { display: grid; width: 36px; height: 36px; place-items: center; border-radius: 50%; background: #fff; color: var(--cc-primary); font-weight: 700; box-shadow: 0 4px 12px rgba(0,0,0,.15); }
|
||||
.user-copy { display: flex; flex-direction: column; }
|
||||
.user-copy strong { font-size: 13px; }
|
||||
.user-copy span { color: var(--cc-muted); font-size: 11px; }
|
||||
.mobile-menu-button { display: none; width: 44px; height: 44px; padding: 0; border: 0; background: none; cursor: pointer; }
|
||||
.header-user .el-button { width: 44px; height: 44px; }
|
||||
.header-user .el-button { width: 44px; height: 44px; color: #fff; }
|
||||
.header-user .el-button:hover { background: rgba(255,255,255,.15); color: #fff; }
|
||||
.page-container { padding: 20px 20px 32px; }
|
||||
|
||||
/* Static permission preview */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { Building2, CheckCircle2, Download, Eye, FileUp, Pencil, Plus, Power } from '@lucide/vue'
|
||||
import { Building2, CheckCircle2, Download, Eye, FileCheck2, FileUp, Pencil, Plus, Power } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
||||
import {
|
||||
@@ -366,7 +366,45 @@ onMounted(loadCustomers)
|
||||
<template #footer><div class="drawer-footer customer-editor-footer"><span>{{ editingId ? '保存资料和上传资质均不会送审,提交审核需要再次确认' : '请先保存资料生成客户草稿,再上传资质' }}</span><div><el-button :disabled="editorLoading || saving || submitting || uploadingQualifications" @click="editorOpen = false">关闭</el-button><el-button :loading="saving" :disabled="editorLoading || submitting || uploadingQualifications" @click="saveCustomer">保存资料</el-button><el-button v-if="canUploadQualifications" :loading="uploadingQualifications" :disabled="editorLoading || !editingId || saving || submitting || !hasSelectedQualifications" @click="uploadQualifications">上传已选资质</el-button><el-button v-if="canSubmitReview" type="primary" :loading="submitting" :disabled="editorLoading || saving || uploadingQualifications" @click="submitReview">提交审核</el-button></div></div></template>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer v-model="detailOpen" title="客户详情" size="620px" class="business-drawer"><div v-loading="detailLoading" class="customer-detail"><template v-if="detail"><div class="detail-title"><span><Building2 :size="22" /></span><div><h3>{{ detail.tenantName }}</h3><p>{{ detail.tenantCode }}</p></div></div><el-descriptions :column="1" border><el-descriptions-item label="统一社会信用代码">{{ displayText(detail.unifiedSocialCreditCode) }}</el-descriptions-item><el-descriptions-item label="联系人">{{ displayText(detail.contactName) }} · {{ displayText(detail.contactPhone) }}</el-descriptions-item><el-descriptions-item label="联系邮箱">{{ displayText(detail.contactEmail) }}</el-descriptions-item><el-descriptions-item label="客户套餐">{{ displayText(detail.servicePackage) }}</el-descriptions-item><el-descriptions-item label="销售人员">{{ displayText(detail.salesOwnerName) }}</el-descriptions-item><el-descriptions-item label="账期">{{ detail.paymentTermDays ?? 0 }} 天</el-descriptions-item><el-descriptions-item label="注册地址">{{ displayText(detail.registeredAddress) }}</el-descriptions-item><el-descriptions-item label="入驻时间">{{ formatDate(detail.createdAt) }}</el-descriptions-item><el-descriptions-item label="内部备注">{{ displayText(detail.internalRemark) }}</el-descriptions-item></el-descriptions><div class="detail-qualification-list"><h4>资质文件</h4><div v-if="detailQualifications.length"><div v-for="item in detailQualifications" :key="String(item.id)"><span>{{ qualifications.find((draft) => draft.type === item.qualificationType)?.label || item.qualificationType }}</span><el-tag :type="item.status === 'APPROVED' ? 'success' : item.status === 'REJECTED' ? 'danger' : 'warning'" size="small">{{ item.status === 'APPROVED' ? '已通过' : item.status === 'REJECTED' ? '已驳回' : '待审核' }}</el-tag></div></div><p v-else>暂未上传资质文件</p></div></template></div></el-drawer>
|
||||
<el-drawer v-model="detailOpen" title="客户详情" size="620px" class="business-drawer">
|
||||
<div v-loading="detailLoading" class="customer-detail">
|
||||
<template v-if="detail">
|
||||
<div class="detail-title">
|
||||
<span><Building2 :size="22" /></span>
|
||||
<div><h3>{{ detail.tenantName }}</h3><p>{{ detail.tenantCode }}</p></div>
|
||||
<el-tag :type="statusMeta(detail.status).type" effect="light">{{ statusMeta(detail.status).label }}</el-tag>
|
||||
</div>
|
||||
|
||||
<section class="customer-detail-section" aria-labelledby="customer-profile-heading">
|
||||
<h4 id="customer-profile-heading">企业资料</h4>
|
||||
<dl class="customer-detail-grid">
|
||||
<div class="customer-detail-item is-wide"><dt>统一社会信用代码</dt><dd>{{ displayText(detail.unifiedSocialCreditCode) }}</dd></div>
|
||||
<div class="customer-detail-item"><dt>联系人</dt><dd>{{ displayText(detail.contactName) }}</dd></div>
|
||||
<div class="customer-detail-item"><dt>联系电话</dt><dd>{{ displayText(detail.contactPhone) }}</dd></div>
|
||||
<div class="customer-detail-item is-wide"><dt>联系邮箱</dt><dd>{{ displayText(detail.contactEmail) }}</dd></div>
|
||||
<div class="customer-detail-item"><dt>客户套餐</dt><dd>{{ displayText(detail.servicePackage) }}</dd></div>
|
||||
<div class="customer-detail-item"><dt>账期</dt><dd>{{ detail.paymentTermDays ?? 0 }} 天</dd></div>
|
||||
<div class="customer-detail-item"><dt>销售人员</dt><dd>{{ displayText(detail.salesOwnerName) }}</dd></div>
|
||||
<div class="customer-detail-item"><dt>入驻时间</dt><dd>{{ formatDate(detail.createdAt) }}</dd></div>
|
||||
<div class="customer-detail-item is-wide"><dt>注册地址</dt><dd>{{ displayText(detail.registeredAddress) }}</dd></div>
|
||||
<div class="customer-detail-item is-wide"><dt>内部备注</dt><dd>{{ displayText(detail.internalRemark) }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="customer-detail-section detail-qualification-list" aria-labelledby="customer-qualification-heading">
|
||||
<div class="detail-section-heading"><h4 id="customer-qualification-heading">资质文件</h4><span>共 {{ detailQualifications.length }} 份</span></div>
|
||||
<div v-if="detailQualifications.length" class="detail-qualification-items">
|
||||
<article v-for="item in detailQualifications" :key="String(item.id)" class="detail-qualification-item">
|
||||
<span class="detail-qualification-icon"><FileCheck2 :size="18" /></span>
|
||||
<div><strong>{{ qualifications.find((draft) => draft.type === item.qualificationType)?.label || item.qualificationType }}</strong><small>{{ item.validTo ? `有效期至 ${item.validTo}` : '长期有效或未设置有效期' }}</small></div>
|
||||
<el-tag :type="item.status === 'APPROVED' ? 'success' : item.status === 'REJECTED' ? 'danger' : 'warning'" size="small" effect="light">{{ item.status === 'APPROVED' ? '已通过' : item.status === 'REJECTED' ? '已驳回' : '待审核' }}</el-tag>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="detail-empty-qualification"><FileCheck2 :size="20" /><span>暂未上传资质文件</span></div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -381,11 +419,35 @@ onMounted(loadCustomers)
|
||||
.qualification-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; } .qualification-heading h3 { margin-bottom:4px; } .qualification-heading p,.qualification-hint { margin:0; color:var(--cc-muted); font-size:12px; line-height:1.6; } .qualification-hint { margin-top:12px; } .qualification-hint span,.qualification-label > span { color:var(--el-color-danger); }
|
||||
.qualification-table { overflow:hidden; margin-top:14px; border:1px solid var(--cc-border); border-radius:8px; } .qualification-row { display:grid; grid-template-columns:180px minmax(260px,1fr) 180px; min-width:0; border-top:1px solid var(--cc-border); } .qualification-row:first-child { border-top:0; } .qualification-row > * { display:flex; min-width:0; min-height:68px; align-items:center; padding:12px 14px; } .qualification-header { min-height:44px; background:#f7f8fc; color:var(--cc-muted); font-size:12px; font-weight:600; } .qualification-header > * { min-height:44px; }
|
||||
.qualification-label { gap:5px; font-weight:600; } .qualification-label svg { margin-left:auto; color:var(--el-color-success); }
|
||||
.qualification-file { flex-wrap:wrap; gap:8px; } .file-picker { display:inline-flex; min-height:36px; align-items:center; gap:7px; padding:0 12px; border:1px solid var(--cc-border); border-radius:6px; background:#fff; color:var(--cc-text); cursor:pointer; } .file-picker:focus-within { outline:2px solid color-mix(in srgb,var(--cc-primary) 35%,transparent); outline-offset:2px; } .file-picker.is-disabled { cursor:not-allowed; opacity:.6; } .file-picker input { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; } .qualification-file-name { min-width:0; overflow:hidden; color:var(--cc-muted); font-size:12px; text-overflow:ellipsis; white-space:nowrap; } .qualification-error { width:100%; color:var(--el-color-danger); line-height:1.4; }
|
||||
.qualification-file { flex-wrap:wrap; gap:8px; } .file-picker { position:relative; display:inline-flex; min-height:36px; align-items:center; gap:7px; overflow:hidden; padding:0 12px; border:1px solid var(--cc-border); border-radius:6px; background:#fff; color:var(--cc-text); cursor:pointer; } .file-picker:focus-within { outline:2px solid color-mix(in srgb,var(--cc-primary) 35%,transparent); outline-offset:2px; } .file-picker.is-disabled { cursor:not-allowed; opacity:.6; } .file-picker input { position:absolute; inset:0; width:100%; height:100%; opacity:0; cursor:inherit; } .qualification-file-name { min-width:0; overflow:hidden; color:var(--cc-muted); font-size:12px; text-overflow:ellipsis; white-space:nowrap; } .qualification-error { width:100%; color:var(--el-color-danger); line-height:1.4; }
|
||||
.qualification-load-error { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-top:12px; padding:10px 12px; border:1px solid #fecdca; border-radius:6px; background:#fffafa; color:#b42318; font-size:12px; }
|
||||
.customer-editor :deep(.el-drawer__body) { min-height:0; overflow-y:auto; overscroll-behavior:contain; }
|
||||
.customer-editor :deep(.el-drawer__footer) { flex:0 0 auto; }
|
||||
.customer-editor-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; } .customer-editor-footer > span { color:var(--cc-muted); font-size:12px; text-align:left; } .customer-editor-footer > div { display:flex; flex:0 0 auto; gap:8px; } .customer-editor-footer :deep(.el-button + .el-button) { margin-left:0; }
|
||||
.customer-detail { min-height:200px; } .detail-title { display:flex; align-items:center; gap:12px; margin-bottom:20px; } .detail-title h3 { margin:0; font-size:20px; } .detail-title p { margin:4px 0 0; color:var(--cc-muted); font-size:12px; }
|
||||
.detail-qualification-list { margin-top:20px; } .detail-qualification-list h4 { margin:0 0 10px; } .detail-qualification-list > div > div { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:10px 0; border-bottom:1px solid var(--cc-border); } .detail-qualification-list p { color:var(--cc-muted); }
|
||||
@media (max-width:720px) { .customers-error,.customers-table-header,.customer-editor-footer { align-items:stretch; flex-direction:column; } .customers-table-actions { display:grid; grid-template-columns:1fr 1fr; } .customers-table-actions :deep(.el-button) { min-height:40px; margin:0; } .qualification-table { overflow-x:auto; } .qualification-row { min-width:640px; } .customer-editor-footer > div { display:grid; grid-template-columns:repeat(auto-fit,minmax(110px,1fr)); width:100%; } .customer-editor-footer :deep(.el-button) { min-height:40px; margin:0; } }
|
||||
.customer-detail { min-height:200px; padding:20px 24px 24px; }
|
||||
.detail-title { display:grid; grid-template-columns:44px minmax(0,1fr) auto; align-items:center; gap:12px; margin-bottom:24px; padding:4px 0 20px; border-bottom:1px solid var(--cc-border); }
|
||||
.detail-title > span { width:44px; height:44px; border-radius:10px; }
|
||||
.detail-title h3 { margin:0; overflow:hidden; color:var(--cc-text); font-size:20px; line-height:1.35; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.detail-title p { margin:4px 0 0; color:var(--cc-muted); font-size:12px; letter-spacing:.02em; }
|
||||
.customer-detail-section + .customer-detail-section { margin-top:24px; }
|
||||
.customer-detail-section h4 { margin:0 0 12px; color:var(--cc-text); font-size:14px; }
|
||||
.customer-detail-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); overflow:hidden; margin:0; border:1px solid var(--cc-border); border-radius:10px; background:#fff; }
|
||||
.customer-detail-item { min-width:0; padding:13px 16px; border-bottom:1px solid var(--cc-border); }
|
||||
.customer-detail-item:nth-child(odd):not(.is-wide) { border-right:1px solid var(--cc-border); }
|
||||
.customer-detail-item.is-wide { grid-column:1 / -1; }
|
||||
.customer-detail-item:last-child { border-bottom:0; }
|
||||
.customer-detail-item dt { margin-bottom:5px; color:var(--cc-muted); font-size:12px; line-height:1.4; }
|
||||
.customer-detail-item dd { margin:0; color:var(--cc-text); font-size:14px; font-weight:600; line-height:1.55; overflow-wrap:anywhere; }
|
||||
.detail-section-heading { display:flex; align-items:baseline; justify-content:space-between; gap:12px; }
|
||||
.detail-section-heading h4 { margin-bottom:12px; }
|
||||
.detail-section-heading span { color:var(--cc-muted); font-size:12px; }
|
||||
.detail-qualification-items { display:flex; flex-direction:column; gap:8px; }
|
||||
.detail-qualification-item { display:grid; grid-template-columns:36px minmax(0,1fr) auto; align-items:center; gap:12px; padding:12px 14px; border:1px solid var(--cc-border); border-radius:8px; background:#fff; }
|
||||
.detail-qualification-icon { display:grid; width:36px; height:36px; place-items:center; border-radius:8px; background:#eef4ff; color:var(--cc-primary); }
|
||||
.detail-qualification-item strong,.detail-qualification-item small { display:block; }
|
||||
.detail-qualification-item strong { color:var(--cc-text); font-size:13px; }
|
||||
.detail-qualification-item small { margin-top:4px; color:var(--cc-muted); font-size:11px; line-height:1.4; }
|
||||
.detail-empty-qualification { display:flex; min-height:88px; align-items:center; justify-content:center; gap:8px; border:1px dashed var(--cc-border); border-radius:8px; background:#fafbfc; color:var(--cc-muted); font-size:12px; }
|
||||
@media (max-width:720px) { .customers-error,.customers-table-header,.customer-editor-footer { align-items:stretch; flex-direction:column; } .customers-table-actions { display:grid; grid-template-columns:1fr 1fr; } .customers-table-actions :deep(.el-button) { min-height:40px; margin:0; } .qualification-table { overflow-x:auto; } .qualification-row { min-width:640px; } .customer-editor-footer > div { display:grid; grid-template-columns:repeat(auto-fit,minmax(110px,1fr)); width:100%; } .customer-editor-footer :deep(.el-button) { min-height:40px; margin:0; } .customer-detail { padding:16px; } .customer-detail-grid { grid-template-columns:1fr; } .customer-detail-item,.customer-detail-item:nth-child(odd):not(.is-wide) { grid-column:1; border-right:0; } .customer-detail-item:last-child { border-bottom:0; } }
|
||||
@media (max-width:360px) { .customers-table-actions,.capital-input { grid-template-columns:1fr; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user