diff --git a/admin/src/components/Footer/style/index.module.less b/admin/src/components/Footer/style/index.module.less index 892f71c..f0c8ed2 100644 --- a/admin/src/components/Footer/style/index.module.less +++ b/admin/src/components/Footer/style/index.module.less @@ -2,7 +2,9 @@ display: flex; align-items: center; justify-content: center; - height: 40px; + height: 48px; + margin-top: 32px; + border-top: 1px solid var(--omni-color-border, var(--color-border)); text-align: center; - color: var(--color-text-2); + color: var(--omni-color-text-2, var(--color-text-2)); } diff --git a/admin/src/components/NavBar/index.tsx b/admin/src/components/NavBar/index.tsx index c638ede..0979b8f 100644 --- a/admin/src/components/NavBar/index.tsx +++ b/admin/src/components/NavBar/index.tsx @@ -61,9 +61,21 @@ function Navbar({ show }: { show: boolean }) { ); } - const isSuperAdmin = true; // 当前登录账号为系统最高权限账号 - const roleTitle = lang === 'zh-CN' ? '超级管理员 [最高权限]' : 'Platform Super Admin'; - const roleScope = lang === 'zh-CN' ? '全平台运营总控' : 'Global Platform'; + const isSuperAdmin = Boolean(userInfo?.isSuper || userInfo?.role === 'super_admin'); + const roleTitle = isSuperAdmin + ? lang === 'zh-CN' + ? '超级管理员' + : 'Super Admin' + : lang === 'zh-CN' + ? '租户管理员' + : 'Workspace Admin'; + const roleScope = isSuperAdmin + ? lang === 'zh-CN' + ? '全平台运营总控' + : 'Global platform' + : lang === 'zh-CN' + ? '当前企业工作区' + : 'Current workspace'; const droplist = (
  • - - - - - - + + + + + + - - - - - - - - - - + - + +
    + + + + + + + + + + + + + +
    + +
    + + ); } diff --git a/admin/src/pages/crm/leads/locale/index.ts b/admin/src/pages/crm/leads/locale/index.ts index 9f616d7..c696201 100644 --- a/admin/src/pages/crm/leads/locale/index.ts +++ b/admin/src/pages/crm/leads/locale/index.ts @@ -1,6 +1,7 @@ const i18n = { 'en-US': { 'menu.crm.leads': 'Leads', + 'crmLeads.page.subtitle': 'Track inbound interest and open the next conversation.', 'crmLeads.filter.keyWord': 'Keyword', 'crmLeads.filter.keyWord.placeholder': 'Search name, company, or contact', 'crmLeads.filter.status': 'Status', @@ -31,6 +32,7 @@ const i18n = { }, 'zh-CN': { 'menu.crm.leads': '潜客线索', + 'crmLeads.page.subtitle': '查看进线意向,并跟进下一次沟通。', 'crmLeads.filter.keyWord': '关键词', 'crmLeads.filter.keyWord.placeholder': '搜索姓名、公司或联系方式', 'crmLeads.filter.status': '状态', @@ -61,6 +63,7 @@ const i18n = { }, 'ar-SA': { 'menu.crm.leads': 'العملاء المحتملون', + 'crmLeads.page.subtitle': 'تابع الاهتمام الوارد وافتح المحادثة التالية.', 'crmLeads.filter.keyWord': 'كلمة البحث', 'crmLeads.filter.keyWord.placeholder': 'ابحث بالاسم أو الشركة أو وسيلة التواصل', 'crmLeads.filter.status': 'الحالة', diff --git a/admin/src/pages/crm/radar-log/index.tsx b/admin/src/pages/crm/radar-log/index.tsx index bcc5d2f..3fa5af1 100644 --- a/admin/src/pages/crm/radar-log/index.tsx +++ b/admin/src/pages/crm/radar-log/index.tsx @@ -4,11 +4,17 @@ import { Form, Input, Select, - Card, Tag, Message, Button, + Space, } from '@arco-design/web-react'; +import { + ContentCard, + FilterCard, + PageHeader, + PageShell, +} from '@/components/PageShell'; import type { PaginationProps, TableColumnProps } from '@arco-design/web-react'; import axios from 'axios'; import useLocale from '@/utils/useLocale'; @@ -320,43 +326,55 @@ function RadarLogPage() { })); return ( - -
    - - - - - - - - - - - -
    + - + +
    + + + + + + + + + + + + + +
    + +
    + + ); } diff --git a/admin/src/pages/crm/radar-log/locale/index.ts b/admin/src/pages/crm/radar-log/locale/index.ts index b318081..6663abd 100644 --- a/admin/src/pages/crm/radar-log/locale/index.ts +++ b/admin/src/pages/crm/radar-log/locale/index.ts @@ -1,6 +1,7 @@ const i18n = { 'en-US': { - 'menu.crm.radarLog': 'Visitor Radar', + 'menu.crm.radarLog': 'Visitor Logs', + 'radarLog.page.subtitle': 'See who stayed, where they came from, and what they viewed.', 'radarLog.filter.cardId': 'Card ID', 'radarLog.filter.cardId.placeholder': 'Filter by card', 'radarLog.filter.touchType': 'Touch type', @@ -25,6 +26,7 @@ const i18n = { }, 'zh-CN': { 'menu.crm.radarLog': '访客足迹', + 'radarLog.page.subtitle': '查看访客来源、停留时长与浏览内容。', 'radarLog.filter.cardId': '名片 ID', 'radarLog.filter.cardId.placeholder': '按名片筛选', 'radarLog.filter.touchType': '触达方式', @@ -49,6 +51,7 @@ const i18n = { }, 'ar-SA': { 'menu.crm.radarLog': 'رادار الزوار', + 'radarLog.page.subtitle': 'اعرف من بقي، ومن أين جاء، وما الذي شاهده.', 'radarLog.filter.cardId': 'معرّف البطاقة', 'radarLog.filter.cardId.placeholder': 'تصفية حسب البطاقة', 'radarLog.filter.touchType': 'طريقة الوصول', diff --git a/admin/src/pages/dashboard/financial/index.tsx b/admin/src/pages/dashboard/financial/index.tsx index 8270b62..a45bf94 100644 --- a/admin/src/pages/dashboard/financial/index.tsx +++ b/admin/src/pages/dashboard/financial/index.tsx @@ -189,9 +189,9 @@ export default function DashboardFinancialPage() { ); return ( -
    +
    - + {/* 1. 顶部控制栏与全局视角切换 */} {view === 'coo' ? ( <> - + - + - + - + - + - + ) : ( <> - + - + - + - + - + - + {/* COO 模块 A: SaaS 套餐梯队分布 */} - + {/* COO 模块 B: 全球出海目标市场营收分布 */} - + {/* CFO 模块 A: 国际多币种汇总结算结构 */} - + {/* CFO 模块 B: 应收账款账龄与坏账风险分析 */} - + +
    {/* 顶部标题区与唯一主 CTA */}
    state.userInfo); const [loading, setLoading] = useState(true); const [exporting, setExporting] = useState(false); @@ -149,8 +151,8 @@ function Workplace(): React.ReactElement { }); const tokenStyle = useMemo( - () => getCssVariables('light') as React.CSSProperties, - [] + () => getCssVariables(theme === 'dark' ? 'dark' : 'light') as React.CSSProperties, + [theme] ); const welcome = useMemo(() => { diff --git a/admin/src/pages/dashboard/workplace/style/index.module.less b/admin/src/pages/dashboard/workplace/style/index.module.less index cc697af..109bd35 100644 --- a/admin/src/pages/dashboard/workplace/style/index.module.less +++ b/admin/src/pages/dashboard/workplace/style/index.module.less @@ -1,10 +1,8 @@ .page { min-height: 100%; width: 100%; - max-width: 1680px; + max-width: 1200px; margin: 0 auto; - padding: var(--omni-space-24); - background: var(--omni-color-surface); font-family: var(--omni-font-family); box-sizing: border-box; } @@ -15,7 +13,7 @@ justify-content: space-between; gap: var(--omni-space-16); flex-wrap: wrap; - margin-bottom: var(--omni-space-20); + margin-bottom: var(--omni-space-24); } .headerInfo { @@ -42,7 +40,7 @@ display: inline-flex; align-items: center; gap: var(--omni-space-8); - padding: 4px 10px; + padding: 4px 8px; border-radius: var(--omni-radius-button); background: var(--omni-color-primary-soft); border: 1px solid rgba(11, 92, 255, 0.15); @@ -293,7 +291,7 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: var(--omni-space-6); + margin-bottom: var(--omni-space-8); font-size: 12px; } @@ -311,14 +309,14 @@ .navGrid { display: grid; grid-template-columns: repeat(2, 1fr); - gap: var(--omni-space-10, 10px); + gap: var(--omni-space-12); } .navCard { display: flex; align-items: flex-start; - gap: var(--omni-space-10, 10px); - padding: 10px; + gap: var(--omni-space-12); + padding: 12px; border: 1px solid var(--omni-color-border); border-radius: 12px; background: var(--omni-color-surface); @@ -329,9 +327,7 @@ .navCard:hover { background: var(--omni-color-bg); - border-color: rgba(11, 92, 255, 0.35); - box-shadow: 0 2px 6px rgba(11, 92, 255, 0.08); - transform: translateY(-1px); + border-color: var(--omni-color-primary); } .navCard:focus-visible { @@ -364,7 +360,7 @@ .navTitle { display: block; color: var(--omni-color-text); - font-size: 13px; + font-size: 14px; font-weight: 600; line-height: 1.3; white-space: nowrap; @@ -374,9 +370,9 @@ .navSub { display: block; - margin-top: 2px; + margin-top: 4px; color: var(--omni-color-text-2); - font-size: 11px; + font-size: 12px; line-height: 1.3; white-space: nowrap; overflow: hidden; @@ -387,7 +383,7 @@ .toolboxList { display: flex; flex-direction: column; - gap: var(--omni-space-10, 10px); + gap: var(--omni-space-12); } .shortcut { @@ -406,8 +402,7 @@ .shortcut:hover { background: var(--omni-color-bg); - border-color: rgba(11, 92, 255, 0.3); - transform: translateY(-1px); + border-color: var(--omni-color-primary); } .shortcut:disabled { @@ -461,8 +456,7 @@ /* 待办预警卡片 */ .alertPanel { - border-color: rgba(247, 144, 9, 0.3) !important; - background: #FFFDF9 !important; + border-color: var(--omni-color-border) !important; } .alertContent { diff --git a/admin/src/pages/nfc/batch/index.tsx b/admin/src/pages/nfc/batch/index.tsx index d4595f6..449153e 100644 --- a/admin/src/pages/nfc/batch/index.tsx +++ b/admin/src/pages/nfc/batch/index.tsx @@ -5,11 +5,17 @@ import { Input, InputNumber, Select, - Card, Tag, Message, Button, + Space, } from '@arco-design/web-react'; +import { + ContentCard, + FilterCard, + PageHeader, + PageShell, +} from '@/components/PageShell'; import type { TableColumnProps } from '@arco-design/web-react'; import axios from 'axios'; import copy from 'copy-to-clipboard'; @@ -206,74 +212,84 @@ function NfcBatchPage() { })); return ( - -
    - - - - -
    + - + + + + + + +
    + + ); } diff --git a/admin/src/pages/nfc/batch/locale/index.ts b/admin/src/pages/nfc/batch/locale/index.ts index 0186642..b4efe63 100644 --- a/admin/src/pages/nfc/batch/locale/index.ts +++ b/admin/src/pages/nfc/batch/locale/index.ts @@ -1,6 +1,7 @@ const i18n = { 'en-US': { 'menu.nfc.batch': 'Batch Cards', + 'nfcBatch.page.subtitle': 'Stock a batch, then copy the generated short links.', 'nfcBatch.form.batchNo': 'Batch number', 'nfcBatch.form.batchNo.placeholder': 'Enter a batch number', 'nfcBatch.form.material': 'Material', @@ -25,6 +26,7 @@ const i18n = { }, 'zh-CN': { 'menu.nfc.batch': '批量制卡', + 'nfcBatch.page.subtitle': '入库一批卡片,并复制生成的短链。', 'nfcBatch.form.batchNo': '批次号', 'nfcBatch.form.batchNo.placeholder': '请输入批次号', 'nfcBatch.form.material': '材质', @@ -49,6 +51,7 @@ const i18n = { }, 'ar-SA': { 'menu.nfc.batch': 'بطاقات الدفعة', + 'nfcBatch.page.subtitle': 'أدخل دفعة ثم انسخ الروابط المختصرة الناتجة.', 'nfcBatch.form.batchNo': 'رقم الدفعة', 'nfcBatch.form.batchNo.placeholder': 'أدخل رقم الدفعة', 'nfcBatch.form.material': 'الخامة', diff --git a/admin/src/pages/nfc/devices/index.tsx b/admin/src/pages/nfc/devices/index.tsx index 816e35e..72ee254 100644 --- a/admin/src/pages/nfc/devices/index.tsx +++ b/admin/src/pages/nfc/devices/index.tsx @@ -4,13 +4,19 @@ import { Form, Input, Select, - Card, Tag, Badge, Message, Button, Popconfirm, + Space, } from '@arco-design/web-react'; +import { + ContentCard, + FilterCard, + PageHeader, + PageShell, +} from '@/components/PageShell'; import type { PaginationProps, TableColumnProps } from '@arco-design/web-react'; import axios from 'axios'; import useLocale from '@/utils/useLocale'; @@ -396,44 +402,56 @@ function NfcDevicesPage() { })); return ( - - - - - - - - - - - - - -
    + - + +
    + + + + + + + + + + + + + +
    + +
    + + ); } diff --git a/admin/src/pages/nfc/devices/locale/index.ts b/admin/src/pages/nfc/devices/locale/index.ts index 34f66aa..6f49350 100644 --- a/admin/src/pages/nfc/devices/locale/index.ts +++ b/admin/src/pages/nfc/devices/locale/index.ts @@ -1,6 +1,7 @@ const i18n = { 'en-US': { 'menu.nfc.devices': 'Device Pool', + 'nfcDevices.page.subtitle': 'Inspect, freeze, or reset NFC cards in the current workspace.', 'nfcDevices.filter.keyWord': 'Keyword', 'nfcDevices.filter.keyWord.placeholder': 'Search UID or batch', 'nfcDevices.filter.status': 'Status', @@ -41,6 +42,7 @@ const i18n = { }, 'zh-CN': { 'menu.nfc.devices': '设备池', + 'nfcDevices.page.subtitle': '查看、冻结或重置当前工作区的 NFC 卡片。', 'nfcDevices.filter.keyWord': '关键词', 'nfcDevices.filter.keyWord.placeholder': '搜索 UID 或批次号', 'nfcDevices.filter.status': '状态', @@ -81,6 +83,7 @@ const i18n = { }, 'ar-SA': { 'menu.nfc.devices': 'مجموعة الأجهزة', + 'nfcDevices.page.subtitle': 'افحص بطاقات NFC أو جمّدها أو أعد ضبطها في مساحة العمل الحالية.', 'nfcDevices.filter.keyWord': 'كلمة البحث', 'nfcDevices.filter.keyWord.placeholder': 'ابحث بمعرف الشريحة أو رقم الدفعة', 'nfcDevices.filter.status': 'الحالة', diff --git a/admin/src/pages/nfc/rules/index.tsx b/admin/src/pages/nfc/rules/index.tsx index 9a40148..ffd75d6 100644 --- a/admin/src/pages/nfc/rules/index.tsx +++ b/admin/src/pages/nfc/rules/index.tsx @@ -525,13 +525,7 @@ const NfcRulesPage: React.FC = () => { }; return ( -
    +
    {/* 顶部筛选与单一主 CTA 区域 */} ; case 'refunded': - return ; + return ; default: return ; } @@ -237,7 +237,7 @@ export default function RenewalsPage() { .reduce((sum, d) => sum + Number(d.amount || 0), 0); return ( -
    +
    {/* 顶部统计卡片 */}
    @@ -257,7 +257,7 @@ export default function RenewalsPage() { value={totalCollected} prefix="$" precision={2} - valueStyle={{ color: '#00B42A' }} + style={{ color: '#00B42A' }} /> @@ -273,7 +273,7 @@ export default function RenewalsPage() { : 80 } suffix="%" - valueStyle={{ color: '#0B5CFF' }} + style={{ color: '#0B5CFF' }} /> @@ -283,7 +283,7 @@ export default function RenewalsPage() { title="超管人工代续订单" value={data.filter(d => d.channel === 'manual_admin').length} suffix="笔" - valueStyle={{ color: '#722ED1' }} + style={{ color: '#722ED1' }} /> @@ -374,7 +374,7 @@ export default function RenewalsPage() { visible={manualModalVisible} onOk={handleConfirmManualRenew} onCancel={() => setManualModalVisible(false)} - width={500} + style={{ width: 500 }} > diff --git a/admin/src/pages/platform/subscribers/index.tsx b/admin/src/pages/platform/subscribers/index.tsx index 9b1a57c..511f1b3 100644 --- a/admin/src/pages/platform/subscribers/index.tsx +++ b/admin/src/pages/platform/subscribers/index.tsx @@ -189,7 +189,7 @@ export default function SubscribersPage() { case 'past_due': return ; case 'canceled': - return ; + return ; default: return ; } @@ -296,7 +296,7 @@ export default function SubscribersPage() { ]; return ( -
    +
    {/* 顶部总控概览卡片 */}
    @@ -314,7 +314,7 @@ export default function SubscribersPage() { @@ -324,7 +324,7 @@ export default function SubscribersPage() { @@ -336,7 +336,7 @@ export default function SubscribersPage() { value={overview?.totalRenewalRevenue || 8540} prefix="$" precision={2} - valueStyle={{ color: '#00B42A' }} + style={{ color: '#00B42A' }} /> diff --git a/admin/src/pages/platform/subscriptions/index.tsx b/admin/src/pages/platform/subscriptions/index.tsx index 7eb0f75..1f69337 100644 --- a/admin/src/pages/platform/subscriptions/index.tsx +++ b/admin/src/pages/platform/subscriptions/index.tsx @@ -98,7 +98,7 @@ export default function SubscriptionsPage() { }; return ( -
    +
    {/* 顶部公告 */} @@ -124,7 +124,7 @@ export default function SubscriptionsPage() { @@ -134,7 +134,7 @@ export default function SubscriptionsPage() { title="年付优惠策略折扣" value={16.7} suffix="%" - valueStyle={{ color: '#FF7D00' }} + style={{ color: '#FF7D00' }} /> @@ -266,7 +266,7 @@ export default function SubscriptionsPage() { visible={editModalVisible} onOk={handleSavePlan} onCancel={() => setEditModalVisible(false)} - width={520} + style={{ width: 520 }} > diff --git a/admin/src/pages/settings/billing/index.tsx b/admin/src/pages/settings/billing/index.tsx index 174f18b..de6b9ba 100644 --- a/admin/src/pages/settings/billing/index.tsx +++ b/admin/src/pages/settings/billing/index.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react'; import { Alert, Button, - Card, Grid, Message, Progress, @@ -10,6 +9,7 @@ import { Tag, Typography, } from '@arco-design/web-react'; +import { ContentCard, PageHeader, PageShell } from '@/components/PageShell'; import axios from 'axios'; import useLocale from '@/utils/useLocale'; import { lightColors } from '@/style/tokens'; @@ -162,7 +162,9 @@ function SettingsBillingPage() { const planLabel = t[`settingsBilling.plan.${quota.planType}`]; return ( - + + + - + {t['settingsBilling.quota.seats']} - + {t['settingsBilling.quota.cards']} - + + ); } diff --git a/admin/src/pages/settings/tenant/index.tsx b/admin/src/pages/settings/tenant/index.tsx index 5440a54..5503cec 100644 --- a/admin/src/pages/settings/tenant/index.tsx +++ b/admin/src/pages/settings/tenant/index.tsx @@ -2,13 +2,13 @@ import React, { useEffect, useState } from 'react'; import { Form, Input, - Card, Message, Button, Space, Tag, Alert, } from '@arco-design/web-react'; +import { ContentCard, PageHeader, PageShell } from '@/components/PageShell'; import axios from 'axios'; import useLocale from '@/utils/useLocale'; import { lightColors } from '@/style/tokens'; @@ -218,7 +218,9 @@ function SettingsTenantPage() { const sslLabelKey = `settingsTenant.ssl.${sslStatus}` as const; return ( - + + + @@ -255,7 +257,8 @@ function SettingsTenantPage() { - + + ); } diff --git a/admin/src/pages/team/handover/index.tsx b/admin/src/pages/team/handover/index.tsx index b8deea2..7ccdd9c 100644 --- a/admin/src/pages/team/handover/index.tsx +++ b/admin/src/pages/team/handover/index.tsx @@ -2,13 +2,13 @@ import React, { useState } from 'react'; import { Form, Input, - Card, Alert, Message, Button, Popconfirm, Space, } from '@arco-design/web-react'; +import { ContentCard, PageHeader, PageShell } from '@/components/PageShell'; import axios from 'axios'; import useLocale from '@/utils/useLocale'; import { lightColors } from '@/style/tokens'; @@ -118,7 +118,9 @@ function TeamHandoverPage() { }; return ( - + + + - + + ); } diff --git a/admin/src/pages/team/members/index.tsx b/admin/src/pages/team/members/index.tsx index 3297c83..b499b2b 100644 --- a/admin/src/pages/team/members/index.tsx +++ b/admin/src/pages/team/members/index.tsx @@ -429,7 +429,7 @@ function TeamMembersPage() { {/* 左侧部门树 */} -
    + {/* 右侧成员列表 */} - + {/* 顶栏操作区:单屏单一主 CTA */}
    setKeyword(val)} onSearch={() => fetchMembers(1, pagination.pageSize)} diff --git a/admin/src/pages/team/members/locale/index.ts b/admin/src/pages/team/members/locale/index.ts index e616c36..364c293 100644 --- a/admin/src/pages/team/members/locale/index.ts +++ b/admin/src/pages/team/members/locale/index.ts @@ -1,6 +1,6 @@ const i18n = { 'en-US': { - 'menu.team.members': 'Members & Structure', + 'menu.team.members': 'Members', 'teamMembers.deptTree.title': 'Organization Structure', 'teamMembers.deptTree.all': 'All Departments', 'teamMembers.deptTree.add': 'Add Dept', diff --git a/admin/src/settings.json b/admin/src/settings.json index c84e59c..da31cc8 100644 --- a/admin/src/settings.json +++ b/admin/src/settings.json @@ -4,5 +4,5 @@ "menu": true, "footer": true, "themeColor": "#165DFF", - "menuWidth": 220 + "menuWidth": 230 } diff --git a/admin/src/style/global.less b/admin/src/style/global.less index 36c2f60..c7c0b03 100644 --- a/admin/src/style/global.less +++ b/admin/src/style/global.less @@ -1,5 +1,25 @@ @import 'nprogress/nprogress.css'; +:root { + --omni-color-primary: #0b5cff; + --omni-color-primary-soft: #eaf1ff; + --omni-color-ai: #7c5cff; + --omni-color-bg: #ffffff; + --omni-color-surface: #f7f8fa; + --omni-color-border: #e3e7ed; + --omni-color-text: #101828; + --omni-color-text-2: #667085; + --omni-color-success: #12b76a; + --omni-color-warning: #f79009; + --omni-color-error: #f04438; + --omni-radius-button: 12px; + --omni-radius-card: 16px; + --omni-radius-modal: 20px; + --omni-shadow: 0 1px 2px rgba(16, 24, 40, 0.04); + --omni-font-family: Inter, 'Noto Sans SC', 'Noto Sans Arabic', 'Noto Sans JP', + 'Noto Sans KR', sans-serif; +} + html, body { width: 100%; @@ -7,35 +27,19 @@ body { margin: 0; padding: 0; font-size: 14px; - background-color: var(--color-bg-1); + line-height: 1.6; + font-family: var(--omni-font-family); + background-color: var(--omni-color-surface, var(--color-bg-1)); } .chart-wrapper { .bizcharts-tooltip { - background: linear-gradient( - 304.17deg, - rgb(253 254 255 / 60%) -6.04%, - rgb(244 247 252 / 60%) 85.2% - ) !important; - border-radius: 6px; - backdrop-filter: blur(10px); + background: var(--omni-color-bg) !important; + border: 1px solid var(--omni-color-border) !important; + border-radius: 12px; + box-shadow: var(--omni-shadow) !important; padding: 8px !important; width: 180px !important; opacity: 1 !important; } } - -body[arco-theme='dark'] { - .chart-wrapper { - .bizcharts-tooltip { - background: linear-gradient( - 304.17deg, - rgba(90, 92, 95, 0.6) -6.04%, - rgba(87, 87, 87, 0.6) 85.2% - ) !important; - backdrop-filter: blur(10px); - border-radius: 6px; - box-shadow: none !important; - } - } -} diff --git a/admin/src/style/layout.module.less b/admin/src/style/layout.module.less index d467372..4dcc3b7 100644 --- a/admin/src/style/layout.module.less +++ b/admin/src/style/layout.module.less @@ -1,5 +1,4 @@ @nav-size-height: 60px; -@layout-max-width: 1100px; .layout { width: 100%; @@ -9,7 +8,7 @@ .layout-navbar { position: fixed; width: 100%; - min-width: @layout-max-width; + min-width: 0; top: 0; left: 0; height: @nav-size-height; @@ -60,16 +59,15 @@ } .collapse-btn { - height: 24px; - width: 24px; + height: 32px; + width: 32px; background-color: var(--color-fill-1); color: var(--color-text-3); - border-radius: 2px; + border-radius: 8px; cursor: pointer; display: flex; justify-content: center; align-items: center; - // 位置 position: absolute; bottom: 12px; right: 12px; @@ -114,15 +112,17 @@ } .layout-content { - background-color: var(--color-fill-2); - min-width: @layout-max-width; + background-color: var(--omni-color-surface, var(--color-fill-2)); + min-width: 0; min-height: 100vh; transition: padding-left 0.2s; box-sizing: border-box; } .layout-content-wrapper { - padding: 16px 20px 0; + padding: 24px 24px 32px; + width: 100%; + box-sizing: border-box; } .layout-breadcrumb { diff --git a/admin/src/utils/changeTheme.ts b/admin/src/utils/changeTheme.ts index 2046fab..6eb5d28 100644 --- a/admin/src/utils/changeTheme.ts +++ b/admin/src/utils/changeTheme.ts @@ -1,9 +1,21 @@ -function changeTheme(theme) { +import { getCssVariables } from '@/style/tokens'; + +function applyOmniTokens(theme: 'light' | 'dark'): void { + const vars = getCssVariables(theme); + const root = document.documentElement; + Object.entries(vars).forEach(([key, value]) => { + root.style.setProperty(key, value); + }); +} + +function changeTheme(theme: string): void { if (theme === 'dark') { document.body.setAttribute('arco-theme', 'dark'); - } else { - document.body.removeAttribute('arco-theme'); + applyOmniTokens('dark'); + return; } + document.body.removeAttribute('arco-theme'); + applyOmniTokens('light'); } export default changeTheme; diff --git a/admin/tests/admin-layout-page-shell.test.ts b/admin/tests/admin-layout-page-shell.test.ts new file mode 100644 index 0000000..d5798d7 --- /dev/null +++ b/admin/tests/admin-layout-page-shell.test.ts @@ -0,0 +1,63 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'path'; +import { fileURLToPath } from 'node:url'; + +const adminRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +function read(relPath: string): string { + return fs.readFileSync(path.join(adminRoot, relPath), 'utf-8'); +} + +test('1. PageShell 必须提供统一页头 / 筛选卡 / 内容卡', () => { + const shell = read('src/components/PageShell/index.tsx'); + const styles = read('src/components/PageShell/style/index.module.less'); + + assert.ok(shell.includes('export function PageShell'), '必须导出 PageShell'); + assert.ok(shell.includes('export function PageHeader'), '必须导出 PageHeader'); + assert.ok(shell.includes('export function FilterCard'), '必须导出 FilterCard'); + assert.ok(shell.includes('export function ContentCard'), '必须导出 ContentCard'); + assert.ok(shell.includes("from '@arco-design/web-react'"), '必须复用 Arco Card'); + assert.ok(!/\bany\b/.test(shell), '禁止使用 any'); + + assert.ok(styles.includes('1200px'), '内容区最大宽度必须是 1200px'); + assert.ok(styles.includes('720px'), '表单窄栏必须是 720px'); + assert.ok(styles.includes('16px'), '卡片圆角必须是 16px'); +}); + +test('2. 全局骨架禁止 1100 最小宽,内边距必须走 24/24/32', () => { + const layout = read('src/style/layout.module.less'); + assert.ok(!layout.includes('min-width: @layout-max-width'), 'Navbar/Content 不得锁 1100 最小宽'); + assert.ok(layout.includes('padding: 24px 24px 32px'), '内容区内边距必须是 24 24 32'); + assert.ok(layout.includes('32px'), '折叠按钮热区不得小于 32'); +}); + +test('3. 主题切换必须把 Omni token 注入 :root', () => { + const theme = read('src/utils/changeTheme.ts'); + assert.ok(theme.includes('getCssVariables'), 'changeTheme 必须注入 getCssVariables'); + assert.ok(theme.includes('document.documentElement'), '必须写到 documentElement'); +}); + +test('4. 列表页必须拆成页头 / 筛选 / 表格三层,不再套一张全宽 Card', () => { + const pages = [ + 'src/pages/card/list/index.tsx', + 'src/pages/crm/leads/index.tsx', + 'src/pages/crm/radar-log/index.tsx', + 'src/pages/nfc/devices/index.tsx', + ]; + for (const rel of pages) { + const content = read(rel); + assert.ok(content.includes('PageHeader'), `${rel} 必须使用 PageHeader`); + assert.ok(content.includes('FilterCard'), `${rel} 必须使用 FilterCard`); + assert.ok(content.includes('ContentCard'), `${rel} 必须使用 ContentCard`); + assert.ok(!content.includes(' { + const tenant = read('src/pages/settings/tenant/index.tsx'); + const handover = read('src/pages/team/handover/index.tsx'); + assert.ok(tenant.includes('narrow') || tenant.includes('FormNarrow'), '域名页必须使用窄栏'); + assert.ok(handover.includes('narrow') || handover.includes('FormNarrow'), '交接页必须使用窄栏'); +}); diff --git a/admin/tests/ai-knowledge.test.ts b/admin/tests/ai-knowledge.test.ts index 38abdd9..67856ce 100644 --- a/admin/tests/ai-knowledge.test.ts +++ b/admin/tests/ai-knowledge.test.ts @@ -99,6 +99,6 @@ test('3. ai/knowledge locale 必须包含 menu.ai.knowledge 与中英文案', () assert.ok(content.includes('aiKnowledge.filter.keyWord'), '必须包含 keyWord 筛选文案'); assert.ok(!/\bany\b/.test(content), 'locale 禁止使用 any'); - assert.equal(defaultI18n['en-US']['menu.ai.knowledge'], 'Knowledge Base'); + assert.equal(defaultI18n['en-US']['menu.ai.knowledge'], 'Knowledge'); assert.equal(defaultI18n['zh-CN']['menu.ai.knowledge'], '知识库'); }); diff --git a/admin/tests/card-cases.test.ts b/admin/tests/card-cases.test.ts index c56f6a7..45ca193 100644 --- a/admin/tests/card-cases.test.ts +++ b/admin/tests/card-cases.test.ts @@ -33,7 +33,7 @@ test('1. routes.ts 已注册 card/cases,对应 pages/card/cases', () => { }); test('2. locale 必须包含 menu.card.cases 中英文案', () => { - assert.equal(defaultI18n['en-US']['menu.card.cases'], 'Case Studies'); + assert.equal(defaultI18n['en-US']['menu.card.cases'], 'Portfolio'); assert.equal(defaultI18n['zh-CN']['menu.card.cases'], '出海案例作品集'); }); diff --git a/admin/tests/card-list.test.ts b/admin/tests/card-list.test.ts index 921fe12..986bb6f 100644 --- a/admin/tests/card-list.test.ts +++ b/admin/tests/card-list.test.ts @@ -23,8 +23,8 @@ test('1. routes.ts 必须包含 key card/list,供 layout glob 加载', () => { test('2. locale 必须包含 menu.card / menu.card.list 中英文案', () => { const content = fs.readFileSync(localePath, 'utf-8'); - assert.ok(content.includes("'menu.card': 'Digital Cards'"), '英文 menu.card'); - assert.ok(content.includes("'menu.card.list': 'Card Library'"), '英文 menu.card.list'); + assert.ok(content.includes("'menu.card': 'Cards'"), '英文 menu.card'); + assert.ok(content.includes("'menu.card.list': 'All Cards'"), '英文 menu.card.list'); assert.ok(content.includes("'menu.card': '数字名片'"), '中文 menu.card'); assert.ok(content.includes("'menu.card.list': '名片资产库'"), '中文 menu.card.list'); }); diff --git a/admin/tests/crm-radar-log.test.ts b/admin/tests/crm-radar-log.test.ts index 902d53b..0c2ea83 100644 --- a/admin/tests/crm-radar-log.test.ts +++ b/admin/tests/crm-radar-log.test.ts @@ -38,7 +38,7 @@ test('1. routes.ts 必须包含 key crm/radar-log,供 layout glob 加载', () }); test('2. locale 必须包含 menu.crm.radarLog 中英文案', () => { - assert.equal(defaultI18n['en-US']['menu.crm.radarLog'], 'Visitor Radar'); + assert.equal(defaultI18n['en-US']['menu.crm.radarLog'], 'Visitor Logs'); assert.equal(defaultI18n['zh-CN']['menu.crm.radarLog'], '访客足迹'); }); diff --git a/admin/tests/menu-locales.test.ts b/admin/tests/menu-locales.test.ts new file mode 100644 index 0000000..3ab8a1c --- /dev/null +++ b/admin/tests/menu-locales.test.ts @@ -0,0 +1,72 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import i18n from '../src/locale/index'; +import { routes, IRoute } from '../src/routes'; + +// 递归收集 routes.ts 中所有的菜单 key (name) +function extractRouteNames(list: IRoute[]): string[] { + const names: string[] = []; + for (const item of list) { + if (item.name) { + names.push(item.name); + } + if (item.children && item.children.length > 0) { + names.push(...extractRouteNames(item.children)); + } + } + return names; +} + +test('1. 所有路由中的菜单项必须在 en-US 和 zh-CN 字典中有完整定义', () => { + const routeNames = extractRouteNames(routes); + const enMessages = i18n['en-US']; + const zhMessages = i18n['zh-CN']; + + for (const name of routeNames) { + assert.ok( + typeof enMessages[name] === 'string' && enMessages[name].length > 0, + `en-US 缺少菜单项翻译: ${name}` + ); + assert.ok( + typeof zhMessages[name] === 'string' && zhMessages[name].length > 0, + `zh-CN 缺少菜单项翻译: ${name}` + ); + } +}); + +test('2. 英文菜单文案必须简练原生,长度不可超过 15 个字符以杜绝截断', () => { + const routeNames = extractRouteNames(routes); + const enMessages = i18n['en-US']; + + for (const name of routeNames) { + const text = enMessages[name]; + assert.ok( + text.length <= 15, + `菜单项 [${name}] 英文文案 "${text}" 过长 (${text.length} 字符),超过了 15 字符上限,容易在 220px 侧边栏下被截断!` + ); + } +}); + +test('3. 关键菜单英文必须采用原生简洁的地道 SaaS 表达', () => { + const en = i18n['en-US']; + // 平台总控必须为 Platform,杜绝 Platform Operations + assert.equal(en['menu.platform'], 'Platform'); + // 财务统计必须为 Finance,杜绝 Financial & Operations + assert.equal(en['menu.dashboard.financial'], 'Finance'); + // 全球雷达必须为 Radar Map,杜绝 Global Radar Heatmap + assert.equal(en['menu.dashboard.radarMap'], 'Radar Map'); + // 术语库必须为 Glossary,杜绝 AI Translation & Glossary + assert.equal(en['menu.ai.translation'], 'Glossary'); + // 组织成员必须为 Members,杜绝 Members & Structure + assert.equal(en['menu.team.members'], 'Members'); + // 订阅套餐必须为 Plans,杜绝 Subscription Plans + assert.equal(en['menu.platform.subscriptions'], 'Plans'); + // 文章动态必须为 Articles,杜绝 Articles & Posts + assert.equal(en['menu.card.articles'], 'Articles'); + // 案例作品集必须为 Portfolio,杜绝 Case Studies + assert.equal(en['menu.card.cases'], 'Portfolio'); + // 名片列表必须为 All Cards,杜绝 Card Library + assert.equal(en['menu.card.list'], 'All Cards'); + // 设备池必须为 Devices,杜绝 Device Pool + assert.equal(en['menu.nfc.devices'], 'Devices'); +}); diff --git a/backend/test/mpuniapp/owner-lead-detail.test.ts b/backend/test/mpuniapp/owner-lead-detail.test.ts index 382a906..e2cc5b8 100644 --- a/backend/test/mpuniapp/owner-lead-detail.test.ts +++ b/backend/test/mpuniapp/owner-lead-detail.test.ts @@ -97,6 +97,7 @@ type UseLeadDetailModule = { }, api?: SaveNoteApi ) => Promise; + resolveLeadDetailText: (key: string) => string; }; const EMOJI_RE = /[\u2600-\u27BF\uFE0F]|[\uD83C-\uDBFF][\uDC00-\uDFFF]/; @@ -262,22 +263,48 @@ describe('OmniCard AI 名片主潜客详情业务逻辑 (useLeadDetail.uts)', () expect(result.status).toBe('hot'); }); }); + + describe('4. resolveLeadDetailText 只增文案', () => { + it('4.1 话术与上传说明有文案,保存语义仍可用', () => { + const { resolveLeadDetailText } = loadUseLeadDetail(); + + expect(resolveLeadDetailText('aiScript').length).toBeGreaterThan(0); + expect(resolveLeadDetailText('comingSoon')).toBe('即将提供'); + expect(resolveLeadDetailText('uploadHint').length).toBeGreaterThan(0); + expect(resolveLeadDetailText('save').length).toBeGreaterThan(0); + }); + }); }); describe('OmniCard AI 名片主潜客详情页契约 (leads-detail.uvue)', () => { - it('页面存在,足迹用 scroll-view,备忘用 textarea,单一主 CTA', () => { + it('页面存在,足迹用 list-view,备忘用 textarea,单一主 CTA', () => { expect(fs.existsSync(DETAIL_PAGE)).toBe(true); expect(fs.existsSync(DETAIL_UTS)).toBe(true); const pageSource = fs.readFileSync(DETAIL_PAGE, 'utf8'); const logicSource = fs.readFileSync(DETAIL_UTS, 'utf8'); const combined = `${pageSource}\n${logicSource}`; + const template = pageSource.split(']*v-for=/.test(pageSource)).toBe(false); expect(pageSource.includes(' { expect(pageSource.includes(' 5') || logicSource.includes('length > 5')).toBe( - true + false ); + expect((pageSource.match(/