fix(admin): 优化管理后台英文菜单多语言文案杜绝截断并修复各业务页面与API错误

这个提交包含在:
wikiboss
2026-09-21 13:47:14 +08:00
父节点 d250a151e2
当前提交 84385f4f8e
共有 48 个文件被更改,包括 890 次插入 和 409 次删除

查看文件

@@ -2,7 +2,9 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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; text-align: center;
color: var(--color-text-2); color: var(--omni-color-text-2, var(--color-text-2));
} }

查看文件

@@ -61,9 +61,21 @@ function Navbar({ show }: { show: boolean }) {
); );
} }
const isSuperAdmin = true; // 当前登录账号为系统最高权限账号 const isSuperAdmin = Boolean(userInfo?.isSuper || userInfo?.role === 'super_admin');
const roleTitle = lang === 'zh-CN' ? '超级管理员 [最高权限]' : 'Platform Super Admin'; const roleTitle = isSuperAdmin
const roleScope = lang === 'zh-CN' ? '全平台运营总控' : 'Global Platform'; ? 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 = ( const droplist = (
<Menu <Menu
@@ -107,7 +119,12 @@ function Navbar({ show }: { show: boolean }) {
</li> </li>
<li> <li>
<Select <Select
triggerElement={<IconButton icon={<IconLanguage />} />} triggerElement={
<IconButton
icon={<IconLanguage />}
aria-label={t['navbar.language'] || 'Language'}
/>
}
options={[ options={[
{ label: '中文', value: 'zh-CN' }, { label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' }, { label: 'English', value: 'en-US' },
@@ -136,6 +153,11 @@ function Navbar({ show }: { show: boolean }) {
> >
<IconButton <IconButton
icon={theme !== 'dark' ? <IconMoonFill /> : <IconSunFill />} icon={theme !== 'dark' ? <IconMoonFill /> : <IconSunFill />}
aria-label={
theme === 'light'
? t['settings.navbar.theme.toDark']
: t['settings.navbar.theme.toLight']
}
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')} onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
/> />
</Tooltip> </Tooltip>
@@ -143,19 +165,21 @@ function Navbar({ show }: { show: boolean }) {
<Settings /> <Settings />
{userInfo && ( {userInfo && (
<li style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <li style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Tag {isSuperAdmin ? (
color="arcoblue" <Tag
style={{ color="arcoblue"
borderRadius: 12, style={{
fontWeight: 500, borderRadius: 12,
fontSize: 12, fontWeight: 500,
padding: '2px 8px', fontSize: 12,
cursor: 'default', padding: '2px 8px',
}} cursor: 'default',
> }}
<IconSafe style={{ marginRight: 4 }} /> >
{roleTitle} <IconSafe style={{ marginRight: 4 }} />
</Tag> {roleTitle}
</Tag>
) : null}
<Dropdown droplist={droplist} position="br" disabled={userLoading}> <Dropdown droplist={droplist} position="br" disabled={userLoading}>
<Avatar size={32} style={{ cursor: 'pointer' }}> <Avatar size={32} style={{ cursor: 'pointer' }}>
{userLoading ? ( {userLoading ? (

查看文件

@@ -15,23 +15,23 @@
.logo { .logo {
display: flex; display: flex;
align-items: center; align-items: center;
width: 200px; gap: 12px;
padding-left: 20px; width: 220px;
padding-inline-start: 16px;
box-sizing: border-box; box-sizing: border-box;
} }
.logo-name { .logo-name {
color: var(--color-text-1); color: var(--omni-color-text, var(--color-text-1));
font-weight: 500; font-weight: 500;
font-size: 20px; font-size: 20px;
margin-left: 10px; font-family: var(--omni-font-family);
font-family: 'PingFang SC';
} }
.right { .right {
display: flex; display: flex;
list-style: none; list-style: none;
padding-right: 20px; padding-inline-end: 16px;
li { li {
padding: 0 8px; padding: 0 8px;
@@ -50,8 +50,10 @@
} }
.round { .round {
width: 240px;
:global(.arco-input-inner-wrapper) { :global(.arco-input-inner-wrapper) {
border-radius: 16px; border-radius: 12px;
} }
svg { svg {

查看文件

@@ -0,0 +1,93 @@
import React from 'react';
import { Card, Typography } from '@arco-design/web-react';
import cs from 'classnames';
import styles from './style/index.module.less';
interface PageShellProps {
children: React.ReactNode;
narrow?: boolean;
className?: string;
}
interface PageHeaderProps {
title: React.ReactNode;
subtitle?: React.ReactNode;
extra?: React.ReactNode;
}
interface SurfaceCardProps {
children: React.ReactNode;
loading?: boolean;
className?: string;
}
export function PageShell({
children,
narrow = false,
className,
}: PageShellProps): React.ReactElement {
return (
<div className={cs(styles.shell, narrow ? styles.shellNarrow : '', className)}>
{children}
</div>
);
}
export function PageHeader({
title,
subtitle,
extra,
}: PageHeaderProps): React.ReactElement {
return (
<div className={styles.header}>
<div className={styles.headerInfo}>
<Typography.Title heading={5} className={styles.title}>
{title}
</Typography.Title>
{subtitle ? (
<Typography.Text className={styles.subtitle}>{subtitle}</Typography.Text>
) : null}
</div>
{extra ? <div className={styles.extra}>{extra}</div> : null}
</div>
);
}
export function FilterCard({
children,
loading,
className,
}: SurfaceCardProps): React.ReactElement {
return (
<Card
bordered
loading={loading}
className={cs(styles.card, styles.filterForm, className)}
>
{children}
</Card>
);
}
export function ContentCard({
children,
loading,
className,
}: SurfaceCardProps): React.ReactElement {
return (
<Card bordered loading={loading} className={cs(styles.card, className)}>
{children}
</Card>
);
}
export function FormNarrow({
children,
loading,
}: SurfaceCardProps): React.ReactElement {
return (
<ContentCard loading={loading} className={styles.shellNarrow}>
{children}
</ContentCard>
);
}

查看文件

@@ -0,0 +1,68 @@
.shell {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
max-width: 1200px;
box-sizing: border-box;
}
.shellNarrow {
max-width: 720px;
}
.header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.headerInfo {
flex: 1;
min-width: 240px;
}
.title {
margin: 0;
color: var(--omni-color-text, var(--color-text-1));
font-size: 24px;
font-weight: 600;
line-height: 1.25;
}
.subtitle {
display: block;
margin-top: 8px;
color: var(--omni-color-text-2, var(--color-text-3));
font-size: 14px;
line-height: 1.6;
}
.extra {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.card {
border: 1px solid var(--omni-color-border, var(--color-border));
border-radius: 16px;
background: var(--omni-color-bg, var(--color-bg-2));
box-shadow: var(--omni-shadow, 0 1px 2px rgba(16, 24, 40, 0.04));
}
.filterForm {
:global(.arco-form-inline) {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
:global(.arco-form-inline .arco-form-item) {
margin-right: 0;
margin-bottom: 0;
}
}

查看文件

@@ -4,44 +4,44 @@ const i18n = {
'en-US': { 'en-US': {
...omniLocales['en-US'].messages, ...omniLocales['en-US'].messages,
'menu.dashboard': 'Dashboard', 'menu.dashboard': 'Dashboard',
'menu.card': 'Digital Cards', 'menu.card': 'Cards',
'menu.card.list': 'Card Library', 'menu.card.list': 'All Cards',
'menu.card.builder': 'Bento Builder', 'menu.card.builder': 'Bento Builder',
'menu.card.cases': 'Case Studies', 'menu.card.cases': 'Portfolio',
'menu.card.articles': 'Articles & Posts', 'menu.card.articles': 'Articles',
'menu.crm': 'Radar CRM', 'menu.crm': 'Radar CRM',
'menu.crm.leads': 'Leads', 'menu.crm.leads': 'Leads',
'menu.crm.pipeline': 'Pipeline', 'menu.crm.pipeline': 'Pipeline',
'menu.crm.radarLog': 'Visitor Radar', 'menu.crm.radarLog': 'Visitor Logs',
'menu.crm.leadProfile': 'Lead Profile', 'menu.crm.leadProfile': 'Lead Profile',
'menu.nfc': 'NFC Fleet', 'menu.nfc': 'NFC Cards',
'menu.nfc.devices': 'Device Pool', 'menu.nfc.devices': 'Devices',
'menu.nfc.batch': 'Batch Cards', 'menu.nfc.batch': 'Batch Cards',
'menu.nfc.rules': 'Routing Rules', 'menu.nfc.rules': 'Routing Rules',
'menu.ai': 'Omni AI', 'menu.ai': 'Omni AI',
'menu.ai.deepseek': 'Model & BYOK', 'menu.ai.deepseek': 'Model & BYOK',
'menu.ai.knowledge': 'Knowledge Base', 'menu.ai.knowledge': 'Knowledge',
'menu.ai.translation': 'AI Translation & Glossary', 'menu.ai.translation': 'Glossary',
'menu.ai.concierge': 'AI Concierge', 'menu.ai.concierge': 'AI Concierge',
'menu.team': 'Team', 'menu.team': 'Team',
'menu.team.members': 'Members & Structure', 'menu.team.members': 'Members',
'menu.team.brandKit': 'Brand Kit', 'menu.team.brandKit': 'Brand Kit',
'menu.team.handover': 'Handover', 'menu.team.handover': 'Handover',
'menu.settingsBiz': 'Workspace', 'menu.settingsBiz': 'Workspace',
'menu.settings.tenant': 'Domain & SSL', 'menu.settings.tenant': 'Domain & SSL',
'menu.settings.billing': 'Billing', 'menu.settings.billing': 'Billing',
'menu.settings.auditLogs': 'Audit Logs', 'menu.settings.auditLogs': 'Audit Logs',
'menu.platform': 'Platform Operations', 'menu.platform': 'Platform',
'menu.platform.subscribers': 'Subscribers', 'menu.platform.subscribers': 'Subscribers',
'menu.platform.renewals': 'Renewals', 'menu.platform.renewals': 'Renewals',
'menu.platform.subscriptions': 'Subscription Plans', 'menu.platform.subscriptions': 'Plans',
'menu.exception': 'Exception', 'menu.exception': 'Exception',
'menu.exception.403': '403', 'menu.exception.403': '403',
'menu.exception.404': '404', 'menu.exception.404': '404',
'menu.exception.500': '500', 'menu.exception.500': '500',
'menu.dashboard.workplace': 'Workplace', 'menu.dashboard.workplace': 'Overview',
'menu.dashboard.radarMap': 'Global Radar Heatmap', 'menu.dashboard.radarMap': 'Radar Map',
'menu.dashboard.financial': 'Financial & Operations', 'menu.dashboard.financial': 'Finance',
'navbar.logout': 'Logout', 'navbar.logout': 'Logout',
'settings.title': 'Settings', 'settings.title': 'Settings',
'settings.themeColor': 'Theme Color', 'settings.themeColor': 'Theme Color',

查看文件

@@ -332,13 +332,7 @@ export default function AiConciergePage(): JSX.Element {
}; };
return ( return (
<div <div>
style={{
padding: spacing[24],
backgroundColor: lightColors.bg,
minHeight: '100%',
}}
>
<Card <Card
title={t['aiConcierge.card.title']} title={t['aiConcierge.card.title']}
bordered={false} bordered={false}

查看文件

@@ -139,9 +139,9 @@ export default function DeepSeekConsolePage(): React.ReactElement {
}; };
return ( return (
<div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}> <div>
<Spin loading={loading} style={{ width: '100%' }}> <Spin loading={loading} style={{ width: '100%' }}>
<Space direction="vertical" size={20} style={{ width: '100%' }}> <Space direction="vertical" size={16} style={{ width: '100%' }}>
{/* 页面标题区 */} {/* 页面标题区 */}
<div> <div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
@@ -165,7 +165,7 @@ export default function DeepSeekConsolePage(): React.ReactElement {
title={t['ai.deepseek.stats.totalTokens']} title={t['ai.deepseek.stats.totalTokens']}
value={usage.totalTokens} value={usage.totalTokens}
groupSeparator groupSeparator
valueStyle={{ color: lightColors.primary, fontWeight: 700 }} style={{ color: lightColors.primary, fontWeight: 700 }}
/> />
</Col> </Col>
<Col span={6}> <Col span={6}>
@@ -203,7 +203,7 @@ export default function DeepSeekConsolePage(): React.ReactElement {
</div> </div>
<Progress <Progress
percent={usage.percent} percent={usage.percent}
status={usage.percent >= 90 ? 'danger' : usage.percent >= 70 ? 'warning' : 'normal'} status={usage.percent >= 90 ? 'error' : usage.percent >= 70 ? 'warning' : 'normal'}
/> />
</div> </div>
) : ( ) : (
@@ -217,7 +217,7 @@ export default function DeepSeekConsolePage(): React.ReactElement {
{/* 模型配置与 BYOK 表单 */} {/* 模型配置与 BYOK 表单 */}
<Card title="模型连接参数 (BYOK)" bordered={true} style={{ borderRadius: '16px' }}> <Card title="模型连接参数 (BYOK)" bordered={true} style={{ borderRadius: '16px' }}>
<Form form={form} layout="vertical" onFinish={handleSave}> <Form form={form} layout="vertical" onSubmit={handleSave}>
<Form.Item <Form.Item
label={t['ai.deepseek.form.apiKey']} label={t['ai.deepseek.form.apiKey']}
field="apiKey" field="apiKey"

查看文件

@@ -1,6 +1,6 @@
const i18n = { const i18n = {
'en-US': { 'en-US': {
'menu.ai.knowledge': 'Knowledge Base', 'menu.ai.knowledge': 'Knowledge',
'aiKnowledge.filter.cardId': 'Card ID', 'aiKnowledge.filter.cardId': 'Card ID',
'aiKnowledge.filter.cardId.placeholder': 'Filter by card', 'aiKnowledge.filter.cardId.placeholder': 'Filter by card',
'aiKnowledge.filter.locale': 'Language', 'aiKnowledge.filter.locale': 'Language',

查看文件

@@ -1,6 +1,6 @@
const i18n = { const i18n = {
'en-US': { 'en-US': {
'menu.ai.translation': 'AI Translation & Glossary', 'menu.ai.translation': 'Glossary',
'aiTranslation.playground.title': 'Live Localization Playground', 'aiTranslation.playground.title': 'Live Localization Playground',
'aiTranslation.playground.text': 'Source Text', 'aiTranslation.playground.text': 'Source Text',
'aiTranslation.playground.text.placeholder': 'Enter text to polish and align with glossary terms...', 'aiTranslation.playground.text.placeholder': 'Enter text to polish and align with glossary terms...',

查看文件

@@ -381,7 +381,7 @@ export default function CardArticlesPage() {
]; ];
return ( return (
<div style={{ padding: '24px', backgroundColor: lightColors.surface, minHeight: '100%' }}> <div>
{/* 头部导航与操作栏 */} {/* 头部导航与操作栏 */}
<Card <Card
bordered={false} bordered={false}

查看文件

@@ -1,6 +1,6 @@
const i18n = { const i18n = {
'en-US': { 'en-US': {
'menu.card.articles': 'Articles & Posts', 'menu.card.articles': 'Articles',
'cardArticles.title': 'Articles & Updates Hub', 'cardArticles.title': 'Articles & Updates Hub',
'cardArticles.description': 'Publish blog articles and dynamic updates to your digital card portal with automated OpenGraph SEO extraction.', 'cardArticles.description': 'Publish blog articles and dynamic updates to your digital card portal with automated OpenGraph SEO extraction.',
'cardArticles.action.add': 'Create Article', 'cardArticles.action.add': 'Create Article',

查看文件

@@ -394,7 +394,7 @@ export default function CardCasesPage(): React.ReactElement {
}; };
return ( return (
<div style={{ padding: '20px 24px', backgroundColor: lightColors.bg }}> <div>
{/* 头部标题与单一主 CTA */} {/* 头部标题与单一主 CTA */}
<div <div
style={{ style={{

查看文件

@@ -4,12 +4,19 @@ import {
Form, Form,
Input, Input,
Select, Select,
Card,
Badge, Badge,
Message, Message,
Button, Button,
Popconfirm, Popconfirm,
Space,
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import { IconPlus } from '@arco-design/web-react/icon';
import {
ContentCard,
FilterCard,
PageHeader,
PageShell,
} from '@/components/PageShell';
import type { PaginationProps, TableColumnProps } from '@arco-design/web-react'; import type { PaginationProps, TableColumnProps } from '@arco-design/web-react';
import axios from 'axios'; import axios from 'axios';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
@@ -317,7 +324,7 @@ function CardListPage() {
? t['cardList.unpublish.confirm'] ? t['cardList.unpublish.confirm']
: t['cardList.publish.confirm']; : t['cardList.publish.confirm'];
return ( return (
<> <Space size={8}>
<Button <Button
type="text" type="text"
size="small" size="small"
@@ -351,7 +358,7 @@ function CardListPage() {
</Button> </Button>
</Popconfirm> </Popconfirm>
) : null} ) : null}
</> </Space>
); );
}, },
}, },
@@ -374,43 +381,64 @@ function CardListPage() {
})); }));
return ( return (
<Card title={t['menu.card.list']}> <PageShell>
<Form form={form} layout="inline" onSubmit={handleSearch}> <PageHeader
<Form.Item field="name" label={t['cardList.filter.name']}> title={t['menu.card.list']}
<Input subtitle={t['cardList.page.subtitle']}
allowClear extra={
placeholder={t['cardList.filter.name.placeholder']} <Button
/> type="primary"
</Form.Item> icon={<IconPlus />}
<Form.Item field="company" label={t['cardList.filter.company']}> onClick={() => history.push('/card/builder')}
<Input >
allowClear {t['cardList.page.create']}
placeholder={t['cardList.filter.company.placeholder']}
/>
</Form.Item>
<Form.Item field="status" label={t['cardList.filter.status']}>
<Select
allowClear
placeholder={t['cardList.filter.status.placeholder']}
options={statusOptions}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
{t['cardList.form.search']}
</Button> </Button>
<Button onClick={handleReset}>{t['cardList.form.reset']}</Button> }
</Form.Item>
</Form>
<Table
rowKey="id"
loading={loading}
columns={columns}
data={data}
pagination={pagination}
onChange={handleTableChange}
/> />
</Card> <FilterCard>
<Form form={form} layout="inline" onSubmit={handleSearch}>
<Form.Item field="name" label={t['cardList.filter.name']}>
<Input
allowClear
placeholder={t['cardList.filter.name.placeholder']}
/>
</Form.Item>
<Form.Item field="company" label={t['cardList.filter.company']}>
<Input
allowClear
placeholder={t['cardList.filter.company.placeholder']}
/>
</Form.Item>
<Form.Item field="status" label={t['cardList.filter.status']}>
<Select
allowClear
placeholder={t['cardList.filter.status.placeholder']}
options={statusOptions}
/>
</Form.Item>
<Form.Item>
<Space size={8}>
<Button type="outline" htmlType="submit">
{t['cardList.form.search']}
</Button>
<Button type="text" onClick={handleReset}>
{t['cardList.form.reset']}
</Button>
</Space>
</Form.Item>
</Form>
</FilterCard>
<ContentCard>
<Table
rowKey="id"
loading={loading}
columns={columns}
data={data}
pagination={pagination}
onChange={handleTableChange}
/>
</ContentCard>
</PageShell>
); );
} }

查看文件

@@ -1,6 +1,8 @@
const i18n = { const i18n = {
'en-US': { 'en-US': {
'menu.card.list': 'Card Library', 'menu.card.list': 'All Cards',
'cardList.page.subtitle': 'Review, publish, and share digital cards from one place.',
'cardList.page.create': 'New card',
'cardList.filter.name': 'Name', 'cardList.filter.name': 'Name',
'cardList.filter.name.placeholder': 'Search by name', 'cardList.filter.name.placeholder': 'Search by name',
'cardList.filter.company': 'Company', 'cardList.filter.company': 'Company',
@@ -38,6 +40,8 @@ const i18n = {
}, },
'zh-CN': { 'zh-CN': {
'menu.card.list': '名片资产库', 'menu.card.list': '名片资产库',
'cardList.page.subtitle': '在同一处查看、发布并分享数字名片。',
'cardList.page.create': '新建名片',
'cardList.filter.name': '姓名', 'cardList.filter.name': '姓名',
'cardList.filter.name.placeholder': '按姓名筛选', 'cardList.filter.name.placeholder': '按姓名筛选',
'cardList.filter.company': '公司', 'cardList.filter.company': '公司',
@@ -75,6 +79,8 @@ const i18n = {
}, },
'ar-SA': { 'ar-SA': {
'menu.card.list': 'مكتبة البطاقات', 'menu.card.list': 'مكتبة البطاقات',
'cardList.page.subtitle': 'راجع البطاقات وانشرها وشاركها من مكان واحد.',
'cardList.page.create': 'بطاقة جديدة',
'cardList.filter.name': 'الاسم', 'cardList.filter.name': 'الاسم',
'cardList.filter.name.placeholder': 'البحث بالاسم', 'cardList.filter.name.placeholder': 'البحث بالاسم',
'cardList.filter.company': 'الشركة', 'cardList.filter.company': 'الشركة',

查看文件

@@ -4,11 +4,17 @@ import {
Form, Form,
Input, Input,
Select, Select,
Card,
Tag, Tag,
Message, Message,
Button, Button,
Space,
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import {
ContentCard,
FilterCard,
PageHeader,
PageShell,
} from '@/components/PageShell';
import type { PaginationProps, TableColumnProps } from '@arco-design/web-react'; import type { PaginationProps, TableColumnProps } from '@arco-design/web-react';
import axios from 'axios'; import axios from 'axios';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@@ -308,44 +314,56 @@ function CrmLeadsPage() {
})); }));
return ( return (
<Card title={t['menu.crm.leads']}> <PageShell>
<Form form={form} layout="inline" onSubmit={handleSearch}> <PageHeader
<Form.Item field="keyWord" label={t['crmLeads.filter.keyWord']}> title={t['menu.crm.leads']}
<Input subtitle={t['crmLeads.page.subtitle']}
allowClear
placeholder={t['crmLeads.filter.keyWord.placeholder']}
/>
</Form.Item>
<Form.Item field="status" label={t['crmLeads.filter.status']}>
<Select
allowClear
placeholder={t['crmLeads.filter.status.placeholder']}
options={statusOptions}
/>
</Form.Item>
<Form.Item field="source" label={t['crmLeads.filter.source']}>
<Select
allowClear
placeholder={t['crmLeads.filter.source.placeholder']}
options={sourceOptions}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
{t['crmLeads.form.search']}
</Button>
<Button onClick={handleReset}>{t['crmLeads.form.reset']}</Button>
</Form.Item>
</Form>
<Table
rowKey="id"
loading={loading}
columns={columns}
data={data}
pagination={pagination}
onChange={handleTableChange}
/> />
</Card> <FilterCard>
<Form form={form} layout="inline" onSubmit={handleSearch}>
<Form.Item field="keyWord" label={t['crmLeads.filter.keyWord']}>
<Input
allowClear
placeholder={t['crmLeads.filter.keyWord.placeholder']}
/>
</Form.Item>
<Form.Item field="status" label={t['crmLeads.filter.status']}>
<Select
allowClear
placeholder={t['crmLeads.filter.status.placeholder']}
options={statusOptions}
/>
</Form.Item>
<Form.Item field="source" label={t['crmLeads.filter.source']}>
<Select
allowClear
placeholder={t['crmLeads.filter.source.placeholder']}
options={sourceOptions}
/>
</Form.Item>
<Form.Item>
<Space size={8}>
<Button type="primary" htmlType="submit">
{t['crmLeads.form.search']}
</Button>
<Button type="text" onClick={handleReset}>
{t['crmLeads.form.reset']}
</Button>
</Space>
</Form.Item>
</Form>
</FilterCard>
<ContentCard>
<Table
rowKey="id"
loading={loading}
columns={columns}
data={data}
pagination={pagination}
onChange={handleTableChange}
/>
</ContentCard>
</PageShell>
); );
} }

查看文件

@@ -1,6 +1,7 @@
const i18n = { const i18n = {
'en-US': { 'en-US': {
'menu.crm.leads': 'Leads', 'menu.crm.leads': 'Leads',
'crmLeads.page.subtitle': 'Track inbound interest and open the next conversation.',
'crmLeads.filter.keyWord': 'Keyword', 'crmLeads.filter.keyWord': 'Keyword',
'crmLeads.filter.keyWord.placeholder': 'Search name, company, or contact', 'crmLeads.filter.keyWord.placeholder': 'Search name, company, or contact',
'crmLeads.filter.status': 'Status', 'crmLeads.filter.status': 'Status',
@@ -31,6 +32,7 @@ const i18n = {
}, },
'zh-CN': { 'zh-CN': {
'menu.crm.leads': '潜客线索', 'menu.crm.leads': '潜客线索',
'crmLeads.page.subtitle': '查看进线意向,并跟进下一次沟通。',
'crmLeads.filter.keyWord': '关键词', 'crmLeads.filter.keyWord': '关键词',
'crmLeads.filter.keyWord.placeholder': '搜索姓名、公司或联系方式', 'crmLeads.filter.keyWord.placeholder': '搜索姓名、公司或联系方式',
'crmLeads.filter.status': '状态', 'crmLeads.filter.status': '状态',
@@ -61,6 +63,7 @@ const i18n = {
}, },
'ar-SA': { 'ar-SA': {
'menu.crm.leads': 'العملاء المحتملون', 'menu.crm.leads': 'العملاء المحتملون',
'crmLeads.page.subtitle': 'تابع الاهتمام الوارد وافتح المحادثة التالية.',
'crmLeads.filter.keyWord': 'كلمة البحث', 'crmLeads.filter.keyWord': 'كلمة البحث',
'crmLeads.filter.keyWord.placeholder': 'ابحث بالاسم أو الشركة أو وسيلة التواصل', 'crmLeads.filter.keyWord.placeholder': 'ابحث بالاسم أو الشركة أو وسيلة التواصل',
'crmLeads.filter.status': 'الحالة', 'crmLeads.filter.status': 'الحالة',

查看文件

@@ -4,11 +4,17 @@ import {
Form, Form,
Input, Input,
Select, Select,
Card,
Tag, Tag,
Message, Message,
Button, Button,
Space,
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import {
ContentCard,
FilterCard,
PageHeader,
PageShell,
} from '@/components/PageShell';
import type { PaginationProps, TableColumnProps } from '@arco-design/web-react'; import type { PaginationProps, TableColumnProps } from '@arco-design/web-react';
import axios from 'axios'; import axios from 'axios';
import useLocale from '@/utils/useLocale'; import useLocale from '@/utils/useLocale';
@@ -320,43 +326,55 @@ function RadarLogPage() {
})); }));
return ( return (
<Card title={t['menu.crm.radarLog']}> <PageShell>
<Form form={form} layout="inline" onSubmit={handleSearch}> <PageHeader
<Form.Item field="cardId" label={t['radarLog.filter.cardId']}> title={t['menu.crm.radarLog']}
<Input subtitle={t['radarLog.page.subtitle']}
allowClear
placeholder={t['radarLog.filter.cardId.placeholder']}
/>
</Form.Item>
<Form.Item field="touchType" label={t['radarLog.filter.touchType']}>
<Select
allowClear
placeholder={t['radarLog.filter.touchType.placeholder']}
options={touchTypeOptions}
/>
</Form.Item>
<Form.Item field="country" label={t['radarLog.filter.country']}>
<Input
allowClear
placeholder={t['radarLog.filter.country.placeholder']}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
{t['radarLog.form.search']}
</Button>
<Button onClick={handleReset}>{t['radarLog.form.reset']}</Button>
</Form.Item>
</Form>
<Table
rowKey="id"
loading={loading}
columns={columns}
data={data}
pagination={pagination}
onChange={handleTableChange}
/> />
</Card> <FilterCard>
<Form form={form} layout="inline" onSubmit={handleSearch}>
<Form.Item field="cardId" label={t['radarLog.filter.cardId']}>
<Input
allowClear
placeholder={t['radarLog.filter.cardId.placeholder']}
/>
</Form.Item>
<Form.Item field="touchType" label={t['radarLog.filter.touchType']}>
<Select
allowClear
placeholder={t['radarLog.filter.touchType.placeholder']}
options={touchTypeOptions}
/>
</Form.Item>
<Form.Item field="country" label={t['radarLog.filter.country']}>
<Input
allowClear
placeholder={t['radarLog.filter.country.placeholder']}
/>
</Form.Item>
<Form.Item>
<Space size={8}>
<Button type="primary" htmlType="submit">
{t['radarLog.form.search']}
</Button>
<Button type="text" onClick={handleReset}>
{t['radarLog.form.reset']}
</Button>
</Space>
</Form.Item>
</Form>
</FilterCard>
<ContentCard>
<Table
rowKey="id"
loading={loading}
columns={columns}
data={data}
pagination={pagination}
onChange={handleTableChange}
/>
</ContentCard>
</PageShell>
); );
} }

查看文件

@@ -1,6 +1,7 @@
const i18n = { const i18n = {
'en-US': { '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': 'Card ID',
'radarLog.filter.cardId.placeholder': 'Filter by card', 'radarLog.filter.cardId.placeholder': 'Filter by card',
'radarLog.filter.touchType': 'Touch type', 'radarLog.filter.touchType': 'Touch type',
@@ -25,6 +26,7 @@ const i18n = {
}, },
'zh-CN': { 'zh-CN': {
'menu.crm.radarLog': '访客足迹', 'menu.crm.radarLog': '访客足迹',
'radarLog.page.subtitle': '查看访客来源、停留时长与浏览内容。',
'radarLog.filter.cardId': '名片 ID', 'radarLog.filter.cardId': '名片 ID',
'radarLog.filter.cardId.placeholder': '按名片筛选', 'radarLog.filter.cardId.placeholder': '按名片筛选',
'radarLog.filter.touchType': '触达方式', 'radarLog.filter.touchType': '触达方式',
@@ -49,6 +51,7 @@ const i18n = {
}, },
'ar-SA': { 'ar-SA': {
'menu.crm.radarLog': 'رادار الزوار', 'menu.crm.radarLog': 'رادار الزوار',
'radarLog.page.subtitle': 'اعرف من بقي، ومن أين جاء، وما الذي شاهده.',
'radarLog.filter.cardId': 'معرّف البطاقة', 'radarLog.filter.cardId': 'معرّف البطاقة',
'radarLog.filter.cardId.placeholder': 'تصفية حسب البطاقة', 'radarLog.filter.cardId.placeholder': 'تصفية حسب البطاقة',
'radarLog.filter.touchType': 'طريقة الوصول', 'radarLog.filter.touchType': 'طريقة الوصول',

查看文件

@@ -189,9 +189,9 @@ export default function DashboardFinancialPage() {
); );
return ( return (
<div style={{ padding: '24px', backgroundColor: lightColors.surface }}> <div>
<Spin loading={loading} style={{ width: '100%' }}> <Spin loading={loading} style={{ width: '100%' }}>
<Space direction="vertical" size={20} style={{ width: '100%' }}> <Space direction="vertical" size={16} style={{ width: '100%' }}>
{/* 1. 顶部控制栏与全局视角切换 */} {/* 1. 顶部控制栏与全局视角切换 */}
<Card <Card
bordered={false} bordered={false}
@@ -279,7 +279,7 @@ export default function DashboardFinancialPage() {
<Grid.Row gutter={16}> <Grid.Row gutter={16}>
{view === 'coo' ? ( {view === 'coo' ? (
<> <>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -295,7 +295,7 @@ export default function DashboardFinancialPage() {
/> />
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -311,7 +311,7 @@ export default function DashboardFinancialPage() {
/> />
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -325,7 +325,7 @@ export default function DashboardFinancialPage() {
/> />
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -339,7 +339,7 @@ export default function DashboardFinancialPage() {
/> />
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -354,7 +354,7 @@ export default function DashboardFinancialPage() {
/> />
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -372,7 +372,7 @@ export default function DashboardFinancialPage() {
</> </>
) : ( ) : (
<> <>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -388,7 +388,7 @@ export default function DashboardFinancialPage() {
/> />
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -404,7 +404,7 @@ export default function DashboardFinancialPage() {
/> />
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -420,7 +420,7 @@ export default function DashboardFinancialPage() {
/> />
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -436,7 +436,7 @@ export default function DashboardFinancialPage() {
/> />
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -452,7 +452,7 @@ export default function DashboardFinancialPage() {
/> />
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col xs={24} sm={12} md={8} xl={4}>
<Card <Card
bordered={false} bordered={false}
style={{ style={{
@@ -476,7 +476,7 @@ export default function DashboardFinancialPage() {
{view === 'coo' ? ( {view === 'coo' ? (
<Grid.Row gutter={16}> <Grid.Row gutter={16}>
{/* COO 模块 A: SaaS 套餐梯队分布 */} {/* COO 模块 A: SaaS 套餐梯队分布 */}
<Grid.Col span={12}> <Grid.Col xs={24} lg={12}>
<Card <Card
title={t['dashboardFinancial.section.planDistribution']} title={t['dashboardFinancial.section.planDistribution']}
bordered={false} bordered={false}
@@ -532,7 +532,7 @@ export default function DashboardFinancialPage() {
</Grid.Col> </Grid.Col>
{/* COO 模块 B: 全球出海目标市场营收分布 */} {/* COO 模块 B: 全球出海目标市场营收分布 */}
<Grid.Col span={12}> <Grid.Col xs={24} lg={12}>
<Card <Card
title={t['dashboardFinancial.section.regionalRevenue']} title={t['dashboardFinancial.section.regionalRevenue']}
bordered={false} bordered={false}
@@ -576,7 +576,7 @@ export default function DashboardFinancialPage() {
) : ( ) : (
<Grid.Row gutter={16}> <Grid.Row gutter={16}>
{/* CFO 模块 A: 国际多币种汇总结算结构 */} {/* CFO 模块 A: 国际多币种汇总结算结构 */}
<Grid.Col span={12}> <Grid.Col xs={24} lg={12}>
<Card <Card
title={t['dashboardFinancial.section.currencyBreakdown']} title={t['dashboardFinancial.section.currencyBreakdown']}
bordered={false} bordered={false}
@@ -621,7 +621,7 @@ export default function DashboardFinancialPage() {
</Grid.Col> </Grid.Col>
{/* CFO 模块 B: 应收账款账龄与坏账风险分析 */} {/* CFO 模块 B: 应收账款账龄与坏账风险分析 */}
<Grid.Col span={12}> <Grid.Col xs={24} lg={12}>
<Card <Card
title={t['dashboardFinancial.section.agingAnalysis']} title={t['dashboardFinancial.section.agingAnalysis']}
bordered={false} bordered={false}

查看文件

@@ -232,13 +232,7 @@ export default function RadarMapDashboard(): React.ReactElement {
const top10Countries = (analyticsData.countryDistribution || []).slice(0, 10); const top10Countries = (analyticsData.countryDistribution || []).slice(0, 10);
return ( return (
<div <div>
style={{
padding: spacing[24],
background: lightColors.surface,
minHeight: '100%',
}}
>
{/* 顶部标题区与唯一主 CTA */} {/* 顶部标题区与唯一主 CTA */}
<div <div
style={{ style={{

查看文件

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import axios from 'axios'; import axios from 'axios';
@@ -34,6 +34,7 @@ import {
IconUserGroup, IconUserGroup,
} from '@arco-design/web-react/icon'; } from '@arco-design/web-react/icon';
import useLocale from '@/utils/useLocale'; import useLocale from '@/utils/useLocale';
import { GlobalContext } from '@/context';
import { getCssVariables, lightColors } from '@/style/tokens'; import { getCssVariables, lightColors } from '@/style/tokens';
import type { GlobalState } from '@/store'; import type { GlobalState } from '@/store';
import type { import type {
@@ -139,6 +140,7 @@ function resolveErrorMessage(error: unknown, fallback: string): string {
function Workplace(): React.ReactElement { function Workplace(): React.ReactElement {
const t = useLocale(locale); const t = useLocale(locale);
const history = useHistory(); const history = useHistory();
const { theme } = useContext(GlobalContext);
const userInfo = useSelector((state: GlobalState) => state.userInfo); const userInfo = useSelector((state: GlobalState) => state.userInfo);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [exporting, setExporting] = useState(false); const [exporting, setExporting] = useState(false);
@@ -149,8 +151,8 @@ function Workplace(): React.ReactElement {
}); });
const tokenStyle = useMemo( const tokenStyle = useMemo(
() => getCssVariables('light') as React.CSSProperties, () => getCssVariables(theme === 'dark' ? 'dark' : 'light') as React.CSSProperties,
[] [theme]
); );
const welcome = useMemo(() => { const welcome = useMemo(() => {

查看文件

@@ -1,10 +1,8 @@
.page { .page {
min-height: 100%; min-height: 100%;
width: 100%; width: 100%;
max-width: 1680px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: var(--omni-space-24);
background: var(--omni-color-surface);
font-family: var(--omni-font-family); font-family: var(--omni-font-family);
box-sizing: border-box; box-sizing: border-box;
} }
@@ -15,7 +13,7 @@
justify-content: space-between; justify-content: space-between;
gap: var(--omni-space-16); gap: var(--omni-space-16);
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: var(--omni-space-20); margin-bottom: var(--omni-space-24);
} }
.headerInfo { .headerInfo {
@@ -42,7 +40,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: var(--omni-space-8); gap: var(--omni-space-8);
padding: 4px 10px; padding: 4px 8px;
border-radius: var(--omni-radius-button); border-radius: var(--omni-radius-button);
background: var(--omni-color-primary-soft); background: var(--omni-color-primary-soft);
border: 1px solid rgba(11, 92, 255, 0.15); border: 1px solid rgba(11, 92, 255, 0.15);
@@ -293,7 +291,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: var(--omni-space-6); margin-bottom: var(--omni-space-8);
font-size: 12px; font-size: 12px;
} }
@@ -311,14 +309,14 @@
.navGrid { .navGrid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: var(--omni-space-10, 10px); gap: var(--omni-space-12);
} }
.navCard { .navCard {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: var(--omni-space-10, 10px); gap: var(--omni-space-12);
padding: 10px; padding: 12px;
border: 1px solid var(--omni-color-border); border: 1px solid var(--omni-color-border);
border-radius: 12px; border-radius: 12px;
background: var(--omni-color-surface); background: var(--omni-color-surface);
@@ -329,9 +327,7 @@
.navCard:hover { .navCard:hover {
background: var(--omni-color-bg); background: var(--omni-color-bg);
border-color: rgba(11, 92, 255, 0.35); border-color: var(--omni-color-primary);
box-shadow: 0 2px 6px rgba(11, 92, 255, 0.08);
transform: translateY(-1px);
} }
.navCard:focus-visible { .navCard:focus-visible {
@@ -364,7 +360,7 @@
.navTitle { .navTitle {
display: block; display: block;
color: var(--omni-color-text); color: var(--omni-color-text);
font-size: 13px; font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: 1.3; line-height: 1.3;
white-space: nowrap; white-space: nowrap;
@@ -374,9 +370,9 @@
.navSub { .navSub {
display: block; display: block;
margin-top: 2px; margin-top: 4px;
color: var(--omni-color-text-2); color: var(--omni-color-text-2);
font-size: 11px; font-size: 12px;
line-height: 1.3; line-height: 1.3;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -387,7 +383,7 @@
.toolboxList { .toolboxList {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--omni-space-10, 10px); gap: var(--omni-space-12);
} }
.shortcut { .shortcut {
@@ -406,8 +402,7 @@
.shortcut:hover { .shortcut:hover {
background: var(--omni-color-bg); background: var(--omni-color-bg);
border-color: rgba(11, 92, 255, 0.3); border-color: var(--omni-color-primary);
transform: translateY(-1px);
} }
.shortcut:disabled { .shortcut:disabled {
@@ -461,8 +456,7 @@
/* 待办预警卡片 */ /* 待办预警卡片 */
.alertPanel { .alertPanel {
border-color: rgba(247, 144, 9, 0.3) !important; border-color: var(--omni-color-border) !important;
background: #FFFDF9 !important;
} }
.alertContent { .alertContent {

查看文件

@@ -5,11 +5,17 @@ import {
Input, Input,
InputNumber, InputNumber,
Select, Select,
Card,
Tag, Tag,
Message, Message,
Button, Button,
Space,
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import {
ContentCard,
FilterCard,
PageHeader,
PageShell,
} from '@/components/PageShell';
import type { TableColumnProps } from '@arco-design/web-react'; import type { TableColumnProps } from '@arco-design/web-react';
import axios from 'axios'; import axios from 'axios';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
@@ -206,74 +212,84 @@ function NfcBatchPage() {
})); }));
return ( return (
<Card title={t['menu.nfc.batch']}> <PageShell>
<Form <PageHeader
form={form} title={t['menu.nfc.batch']}
layout="inline" subtitle={t['nfcBatch.page.subtitle']}
initialValues={{ material: 'pvc', count: DEFAULT_COUNT }}
onSubmit={handleInbound}
>
<Form.Item
field="batchNo"
label={t['nfcBatch.form.batchNo']}
rules={[
{ required: true, message: t['nfcBatch.validation.batchNo'] },
]}
>
<Input
allowClear
placeholder={t['nfcBatch.form.batchNo.placeholder']}
/>
</Form.Item>
<Form.Item
field="material"
label={t['nfcBatch.form.material']}
rules={[
{ required: true, message: t['nfcBatch.validation.material'] },
]}
>
<Select
placeholder={t['nfcBatch.form.material.placeholder']}
options={materialOptions}
/>
</Form.Item>
<Form.Item
field="count"
label={t['nfcBatch.form.count']}
rules={[
{ required: true, message: t['nfcBatch.validation.count'] },
{
type: 'number',
min: MIN_COUNT,
max: MAX_COUNT,
message: t['nfcBatch.validation.count'],
},
]}
>
<InputNumber min={MIN_COUNT} max={MAX_COUNT} precision={0} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>
{t['nfcBatch.form.submit']}
</Button>
<Button onClick={handleCopyAll} disabled={loading || !rows.length}>
{t['nfcBatch.form.copyAll']}
</Button>
</Form.Item>
</Form>
<Table
rowKey="uid"
loading={loading}
columns={columns}
data={rows}
noDataElement={t['nfcBatch.empty']}
pagination={{
showTotal: true,
sizeCanChange: true,
pageSize: 10,
}}
/> />
</Card> <FilterCard>
<Form
form={form}
layout="inline"
initialValues={{ material: 'pvc', count: DEFAULT_COUNT }}
onSubmit={handleInbound}
>
<Form.Item
field="batchNo"
label={t['nfcBatch.form.batchNo']}
rules={[
{ required: true, message: t['nfcBatch.validation.batchNo'] },
]}
>
<Input
allowClear
placeholder={t['nfcBatch.form.batchNo.placeholder']}
/>
</Form.Item>
<Form.Item
field="material"
label={t['nfcBatch.form.material']}
rules={[
{ required: true, message: t['nfcBatch.validation.material'] },
]}
>
<Select
placeholder={t['nfcBatch.form.material.placeholder']}
options={materialOptions}
/>
</Form.Item>
<Form.Item
field="count"
label={t['nfcBatch.form.count']}
rules={[
{ required: true, message: t['nfcBatch.validation.count'] },
{
type: 'number',
min: MIN_COUNT,
max: MAX_COUNT,
message: t['nfcBatch.validation.count'],
},
]}
>
<InputNumber min={MIN_COUNT} max={MAX_COUNT} precision={0} />
</Form.Item>
<Form.Item>
<Space size={8}>
<Button type="primary" htmlType="submit" loading={loading}>
{t['nfcBatch.form.submit']}
</Button>
<Button onClick={handleCopyAll} disabled={loading || !rows.length}>
{t['nfcBatch.form.copyAll']}
</Button>
</Space>
</Form.Item>
</Form>
</FilterCard>
<ContentCard>
<Table
rowKey="uid"
loading={loading}
columns={columns}
data={rows}
noDataElement={t['nfcBatch.empty']}
pagination={{
showTotal: true,
sizeCanChange: true,
pageSize: 10,
}}
/>
</ContentCard>
</PageShell>
); );
} }

查看文件

@@ -1,6 +1,7 @@
const i18n = { const i18n = {
'en-US': { 'en-US': {
'menu.nfc.batch': 'Batch Cards', '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': 'Batch number',
'nfcBatch.form.batchNo.placeholder': 'Enter a batch number', 'nfcBatch.form.batchNo.placeholder': 'Enter a batch number',
'nfcBatch.form.material': 'Material', 'nfcBatch.form.material': 'Material',
@@ -25,6 +26,7 @@ const i18n = {
}, },
'zh-CN': { 'zh-CN': {
'menu.nfc.batch': '批量制卡', 'menu.nfc.batch': '批量制卡',
'nfcBatch.page.subtitle': '入库一批卡片,并复制生成的短链。',
'nfcBatch.form.batchNo': '批次号', 'nfcBatch.form.batchNo': '批次号',
'nfcBatch.form.batchNo.placeholder': '请输入批次号', 'nfcBatch.form.batchNo.placeholder': '请输入批次号',
'nfcBatch.form.material': '材质', 'nfcBatch.form.material': '材质',
@@ -49,6 +51,7 @@ const i18n = {
}, },
'ar-SA': { 'ar-SA': {
'menu.nfc.batch': 'بطاقات الدفعة', 'menu.nfc.batch': 'بطاقات الدفعة',
'nfcBatch.page.subtitle': 'أدخل دفعة ثم انسخ الروابط المختصرة الناتجة.',
'nfcBatch.form.batchNo': 'رقم الدفعة', 'nfcBatch.form.batchNo': 'رقم الدفعة',
'nfcBatch.form.batchNo.placeholder': 'أدخل رقم الدفعة', 'nfcBatch.form.batchNo.placeholder': 'أدخل رقم الدفعة',
'nfcBatch.form.material': 'الخامة', 'nfcBatch.form.material': 'الخامة',

查看文件

@@ -4,13 +4,19 @@ import {
Form, Form,
Input, Input,
Select, Select,
Card,
Tag, Tag,
Badge, Badge,
Message, Message,
Button, Button,
Popconfirm, Popconfirm,
Space,
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import {
ContentCard,
FilterCard,
PageHeader,
PageShell,
} from '@/components/PageShell';
import type { PaginationProps, TableColumnProps } from '@arco-design/web-react'; import type { PaginationProps, TableColumnProps } from '@arco-design/web-react';
import axios from 'axios'; import axios from 'axios';
import useLocale from '@/utils/useLocale'; import useLocale from '@/utils/useLocale';
@@ -396,44 +402,56 @@ function NfcDevicesPage() {
})); }));
return ( return (
<Card title={t['menu.nfc.devices']}> <PageShell>
<Form form={form} layout="inline" onSubmit={handleSearch}> <PageHeader
<Form.Item field="keyWord" label={t['nfcDevices.filter.keyWord']}> title={t['menu.nfc.devices']}
<Input subtitle={t['nfcDevices.page.subtitle']}
allowClear
placeholder={t['nfcDevices.filter.keyWord.placeholder']}
/>
</Form.Item>
<Form.Item field="status" label={t['nfcDevices.filter.status']}>
<Select
allowClear
placeholder={t['nfcDevices.filter.status.placeholder']}
options={statusOptions}
/>
</Form.Item>
<Form.Item field="material" label={t['nfcDevices.filter.material']}>
<Select
allowClear
placeholder={t['nfcDevices.filter.material.placeholder']}
options={materialOptions}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
{t['nfcDevices.form.search']}
</Button>
<Button onClick={handleReset}>{t['nfcDevices.form.reset']}</Button>
</Form.Item>
</Form>
<Table
rowKey="id"
loading={loading}
columns={columns}
data={data}
pagination={pagination}
onChange={handleTableChange}
/> />
</Card> <FilterCard>
<Form form={form} layout="inline" onSubmit={handleSearch}>
<Form.Item field="keyWord" label={t['nfcDevices.filter.keyWord']}>
<Input
allowClear
placeholder={t['nfcDevices.filter.keyWord.placeholder']}
/>
</Form.Item>
<Form.Item field="status" label={t['nfcDevices.filter.status']}>
<Select
allowClear
placeholder={t['nfcDevices.filter.status.placeholder']}
options={statusOptions}
/>
</Form.Item>
<Form.Item field="material" label={t['nfcDevices.filter.material']}>
<Select
allowClear
placeholder={t['nfcDevices.filter.material.placeholder']}
options={materialOptions}
/>
</Form.Item>
<Form.Item>
<Space size={8}>
<Button type="primary" htmlType="submit">
{t['nfcDevices.form.search']}
</Button>
<Button type="text" onClick={handleReset}>
{t['nfcDevices.form.reset']}
</Button>
</Space>
</Form.Item>
</Form>
</FilterCard>
<ContentCard>
<Table
rowKey="id"
loading={loading}
columns={columns}
data={data}
pagination={pagination}
onChange={handleTableChange}
/>
</ContentCard>
</PageShell>
); );
} }

查看文件

@@ -1,6 +1,7 @@
const i18n = { const i18n = {
'en-US': { 'en-US': {
'menu.nfc.devices': 'Device Pool', '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': 'Keyword',
'nfcDevices.filter.keyWord.placeholder': 'Search UID or batch', 'nfcDevices.filter.keyWord.placeholder': 'Search UID or batch',
'nfcDevices.filter.status': 'Status', 'nfcDevices.filter.status': 'Status',
@@ -41,6 +42,7 @@ const i18n = {
}, },
'zh-CN': { 'zh-CN': {
'menu.nfc.devices': '设备池', 'menu.nfc.devices': '设备池',
'nfcDevices.page.subtitle': '查看、冻结或重置当前工作区的 NFC 卡片。',
'nfcDevices.filter.keyWord': '关键词', 'nfcDevices.filter.keyWord': '关键词',
'nfcDevices.filter.keyWord.placeholder': '搜索 UID 或批次号', 'nfcDevices.filter.keyWord.placeholder': '搜索 UID 或批次号',
'nfcDevices.filter.status': '状态', 'nfcDevices.filter.status': '状态',
@@ -81,6 +83,7 @@ const i18n = {
}, },
'ar-SA': { 'ar-SA': {
'menu.nfc.devices': 'مجموعة الأجهزة', 'menu.nfc.devices': 'مجموعة الأجهزة',
'nfcDevices.page.subtitle': 'افحص بطاقات NFC أو جمّدها أو أعد ضبطها في مساحة العمل الحالية.',
'nfcDevices.filter.keyWord': 'كلمة البحث', 'nfcDevices.filter.keyWord': 'كلمة البحث',
'nfcDevices.filter.keyWord.placeholder': 'ابحث بمعرف الشريحة أو رقم الدفعة', 'nfcDevices.filter.keyWord.placeholder': 'ابحث بمعرف الشريحة أو رقم الدفعة',
'nfcDevices.filter.status': 'الحالة', 'nfcDevices.filter.status': 'الحالة',

查看文件

@@ -525,13 +525,7 @@ const NfcRulesPage: React.FC = () => {
}; };
return ( return (
<div <div>
style={{
padding: spacing[24],
backgroundColor: lightColors.surface,
minHeight: '100%',
}}
>
{/* 顶部筛选与单一主 CTA 区域 */} {/* 顶部筛选与单一主 CTA 区域 */}
<Card <Card
style={{ style={{

查看文件

@@ -151,7 +151,7 @@ export default function RenewalsPage() {
case 'pending': case 'pending':
return <Badge status="warning" text="等待入账" />; return <Badge status="warning" text="等待入账" />;
case 'refunded': case 'refunded':
return <Badge status="danger" text="已退款注销" />; return <Badge status="error" text="已退款注销" />;
default: default:
return <Badge status="default" text={status} />; return <Badge status="default" text={status} />;
} }
@@ -237,7 +237,7 @@ export default function RenewalsPage() {
.reduce((sum, d) => sum + Number(d.amount || 0), 0); .reduce((sum, d) => sum + Number(d.amount || 0), 0);
return ( return (
<div style={{ padding: 20 }}> <div>
{/* 顶部统计卡片 */} {/* 顶部统计卡片 */}
<Row gutter={16} style={{ marginBottom: 16 }}> <Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}> <Col span={6}>
@@ -257,7 +257,7 @@ export default function RenewalsPage() {
value={totalCollected} value={totalCollected}
prefix="$" prefix="$"
precision={2} precision={2}
valueStyle={{ color: '#00B42A' }} style={{ color: '#00B42A' }}
/> />
</Card> </Card>
</Col> </Col>
@@ -273,7 +273,7 @@ export default function RenewalsPage() {
: 80 : 80
} }
suffix="%" suffix="%"
valueStyle={{ color: '#0B5CFF' }} style={{ color: '#0B5CFF' }}
/> />
</Card> </Card>
</Col> </Col>
@@ -283,7 +283,7 @@ export default function RenewalsPage() {
title="超管人工代续订单" title="超管人工代续订单"
value={data.filter(d => d.channel === 'manual_admin').length} value={data.filter(d => d.channel === 'manual_admin').length}
suffix="笔" suffix="笔"
valueStyle={{ color: '#722ED1' }} style={{ color: '#722ED1' }}
/> />
</Card> </Card>
</Col> </Col>
@@ -374,7 +374,7 @@ export default function RenewalsPage() {
visible={manualModalVisible} visible={manualModalVisible}
onOk={handleConfirmManualRenew} onOk={handleConfirmManualRenew}
onCancel={() => setManualModalVisible(false)} onCancel={() => setManualModalVisible(false)}
width={500} style={{ width: 500 }}
> >
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Form.Item label="选择会员企业 (租户)" field="tenantId" required> <Form.Item label="选择会员企业 (租户)" field="tenantId" required>

查看文件

@@ -189,7 +189,7 @@ export default function SubscribersPage() {
case 'past_due': case 'past_due':
return <Badge status="warning" text="已到期待续费" />; return <Badge status="warning" text="已到期待续费" />;
case 'canceled': case 'canceled':
return <Badge status="danger" text="已冻结停用" />; return <Badge status="error" text="已冻结停用" />;
default: default:
return <Badge status="default" text={status} />; return <Badge status="default" text={status} />;
} }
@@ -296,7 +296,7 @@ export default function SubscribersPage() {
]; ];
return ( return (
<div style={{ padding: 20 }}> <div>
{/* 顶部总控概览卡片 */} {/* 顶部总控概览卡片 */}
<Row gutter={16} style={{ marginBottom: 16 }}> <Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}> <Col span={6}>
@@ -314,7 +314,7 @@ export default function SubscribersPage() {
<Statistic <Statistic
title="付费出海企业 (Pro/Ent)" title="付费出海企业 (Pro/Ent)"
value={(overview?.proCount || 0) + (overview?.enterpriseCount || 0)} value={(overview?.proCount || 0) + (overview?.enterpriseCount || 0)}
valueStyle={{ color: '#0B5CFF' }} style={{ color: '#0B5CFF' }}
suffix="家" suffix="家"
/> />
</Card> </Card>
@@ -324,7 +324,7 @@ export default function SubscribersPage() {
<Statistic <Statistic
title="30天内到期预警会员" title="30天内到期预警会员"
value={overview?.expiringSoonCount || 1} value={overview?.expiringSoonCount || 1}
valueStyle={{ color: '#FF7D00' }} style={{ color: '#FF7D00' }}
suffix="家" suffix="家"
/> />
</Card> </Card>
@@ -336,7 +336,7 @@ export default function SubscribersPage() {
value={overview?.totalRenewalRevenue || 8540} value={overview?.totalRenewalRevenue || 8540}
prefix="$" prefix="$"
precision={2} precision={2}
valueStyle={{ color: '#00B42A' }} style={{ color: '#00B42A' }}
/> />
</Card> </Card>
</Col> </Col>

查看文件

@@ -98,7 +98,7 @@ export default function SubscriptionsPage() {
}; };
return ( return (
<div style={{ padding: 20 }}> <div>
{/* 顶部公告 */} {/* 顶部公告 */}
<Alert <Alert
type="info" type="info"
@@ -115,7 +115,7 @@ export default function SubscriptionsPage() {
title="当前活跃套餐梯队" title="当前活跃套餐梯队"
value={plans.length} value={plans.length}
suffix="档" suffix="档"
valueStyle={{ color: '#0B5CFF' }} style={{ color: '#0B5CFF' }}
/> />
</Card> </Card>
</Col> </Col>
@@ -124,7 +124,7 @@ export default function SubscriptionsPage() {
<Statistic <Statistic
title="推荐出海主推方案" title="推荐出海主推方案"
value="Pro 专业商务版" value="Pro 专业商务版"
valueStyle={{ fontSize: 18, color: '#00B42A' }} style={{ fontSize: 18, color: '#00B42A' }}
/> />
</Card> </Card>
</Col> </Col>
@@ -134,7 +134,7 @@ export default function SubscriptionsPage() {
title="年付优惠策略折扣" title="年付优惠策略折扣"
value={16.7} value={16.7}
suffix="%" suffix="%"
valueStyle={{ color: '#FF7D00' }} style={{ color: '#FF7D00' }}
/> />
</Card> </Card>
</Col> </Col>
@@ -266,7 +266,7 @@ export default function SubscriptionsPage() {
visible={editModalVisible} visible={editModalVisible}
onOk={handleSavePlan} onOk={handleSavePlan}
onCancel={() => setEditModalVisible(false)} onCancel={() => setEditModalVisible(false)}
width={520} style={{ width: 520 }}
> >
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Form.Item label="套餐展示全称" field="name" required> <Form.Item label="套餐展示全称" field="name" required>

查看文件

@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { import {
Alert, Alert,
Button, Button,
Card,
Grid, Grid,
Message, Message,
Progress, Progress,
@@ -10,6 +9,7 @@ import {
Tag, Tag,
Typography, Typography,
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import { ContentCard, PageHeader, PageShell } from '@/components/PageShell';
import axios from 'axios'; import axios from 'axios';
import useLocale from '@/utils/useLocale'; import useLocale from '@/utils/useLocale';
import { lightColors } from '@/style/tokens'; import { lightColors } from '@/style/tokens';
@@ -162,7 +162,9 @@ function SettingsBillingPage() {
const planLabel = t[`settingsBilling.plan.${quota.planType}`]; const planLabel = t[`settingsBilling.plan.${quota.planType}`];
return ( return (
<Card title={t['menu.settings.billing']} loading={loading}> <PageShell>
<PageHeader title={t['menu.settings.billing']} />
<ContentCard loading={loading}>
<Space direction="vertical" size={16} style={{ width: '100%' }}> <Space direction="vertical" size={16} style={{ width: '100%' }}>
<Alert type="info" content={t['settingsBilling.alert.later']} /> <Alert type="info" content={t['settingsBilling.alert.later']} />
<Typography.Title <Typography.Title
@@ -173,7 +175,7 @@ function SettingsBillingPage() {
</Typography.Title> </Typography.Title>
<Grid.Row gutter={24}> <Grid.Row gutter={24}>
<Grid.Col xs={24} md={12}> <Grid.Col xs={24} md={12}>
<Typography.Text style={{ color: lightColors.text2 }}> <Typography.Text style={{ color: lightColors.text2, display: 'block', marginBottom: 8 }}>
{t['settingsBilling.quota.seats']} {t['settingsBilling.quota.seats']}
</Typography.Text> </Typography.Text>
<Progress <Progress
@@ -183,7 +185,7 @@ function SettingsBillingPage() {
/> />
</Grid.Col> </Grid.Col>
<Grid.Col xs={24} md={12}> <Grid.Col xs={24} md={12}>
<Typography.Text style={{ color: lightColors.text2 }}> <Typography.Text style={{ color: lightColors.text2, display: 'block', marginBottom: 8 }}>
{t['settingsBilling.quota.cards']} {t['settingsBilling.quota.cards']}
</Typography.Text> </Typography.Text>
<Progress <Progress
@@ -215,7 +217,8 @@ function SettingsBillingPage() {
{t['settingsBilling.action.refresh']} {t['settingsBilling.action.refresh']}
</Button> </Button>
</Space> </Space>
</Card> </ContentCard>
</PageShell>
); );
} }

查看文件

@@ -2,13 +2,13 @@ import React, { useEffect, useState } from 'react';
import { import {
Form, Form,
Input, Input,
Card,
Message, Message,
Button, Button,
Space, Space,
Tag, Tag,
Alert, Alert,
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import { ContentCard, PageHeader, PageShell } from '@/components/PageShell';
import axios from 'axios'; import axios from 'axios';
import useLocale from '@/utils/useLocale'; import useLocale from '@/utils/useLocale';
import { lightColors } from '@/style/tokens'; import { lightColors } from '@/style/tokens';
@@ -218,7 +218,9 @@ function SettingsTenantPage() {
const sslLabelKey = `settingsTenant.ssl.${sslStatus}` as const; const sslLabelKey = `settingsTenant.ssl.${sslStatus}` as const;
return ( return (
<Card title={t['menu.settings.tenant']} loading={loading}> <PageShell narrow>
<PageHeader title={t['menu.settings.tenant']} />
<ContentCard loading={loading}>
<Space direction="vertical" size={16} style={{ width: '100%' }}> <Space direction="vertical" size={16} style={{ width: '100%' }}>
<Alert type="info" content={t['settingsTenant.ssl.alert']} /> <Alert type="info" content={t['settingsTenant.ssl.alert']} />
<Space> <Space>
@@ -255,7 +257,8 @@ function SettingsTenantPage() {
</Form.Item> </Form.Item>
</Form> </Form>
</Space> </Space>
</Card> </ContentCard>
</PageShell>
); );
} }

查看文件

@@ -2,13 +2,13 @@ import React, { useState } from 'react';
import { import {
Form, Form,
Input, Input,
Card,
Alert, Alert,
Message, Message,
Button, Button,
Popconfirm, Popconfirm,
Space, Space,
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import { ContentCard, PageHeader, PageShell } from '@/components/PageShell';
import axios from 'axios'; import axios from 'axios';
import useLocale from '@/utils/useLocale'; import useLocale from '@/utils/useLocale';
import { lightColors } from '@/style/tokens'; import { lightColors } from '@/style/tokens';
@@ -118,7 +118,9 @@ function TeamHandoverPage() {
}; };
return ( return (
<Card title={t['menu.team.handover']}> <PageShell narrow>
<PageHeader title={t['menu.team.handover']} />
<ContentCard>
<Space direction="vertical" size={16} style={{ width: '100%' }}> <Space direction="vertical" size={16} style={{ width: '100%' }}>
<Alert <Alert
type="info" type="info"
@@ -193,7 +195,8 @@ function TeamHandoverPage() {
</Form.Item> </Form.Item>
</Form> </Form>
</Space> </Space>
</Card> </ContentCard>
</PageShell>
); );
} }

查看文件

@@ -429,7 +429,7 @@ function TeamMembersPage() {
<Card title={t['menu.team.members']}> <Card title={t['menu.team.members']}>
<Row gutter={16}> <Row gutter={16}>
{/* 左侧部门树 */} {/* 左侧部门树 */}
<Col span={6}> <Col xs={24} lg={6}>
<Card <Card
title={t['teamMembers.deptTree.title']} title={t['teamMembers.deptTree.title']}
extra={ extra={
@@ -495,7 +495,7 @@ function TeamMembersPage() {
</Col> </Col>
{/* 右侧成员列表 */} {/* 右侧成员列表 */}
<Col span={18}> <Col xs={24} lg={18}>
<Space direction="vertical" size={16} style={{ width: '100%' }}> <Space direction="vertical" size={16} style={{ width: '100%' }}>
{/* 顶栏操作区:单屏单一主 CTA */} {/* 顶栏操作区:单屏单一主 CTA */}
<div <div
@@ -509,7 +509,7 @@ function TeamMembersPage() {
<Input.Search <Input.Search
allowClear allowClear
placeholder={t['teamMembers.memberTable.searchPlaceholder']} placeholder={t['teamMembers.memberTable.searchPlaceholder']}
style={{ width: 320 }} style={{ width: '100%', maxWidth: 320 }}
value={keyword} value={keyword}
onChange={(val) => setKeyword(val)} onChange={(val) => setKeyword(val)}
onSearch={() => fetchMembers(1, pagination.pageSize)} onSearch={() => fetchMembers(1, pagination.pageSize)}

查看文件

@@ -1,6 +1,6 @@
const i18n = { const i18n = {
'en-US': { 'en-US': {
'menu.team.members': 'Members & Structure', 'menu.team.members': 'Members',
'teamMembers.deptTree.title': 'Organization Structure', 'teamMembers.deptTree.title': 'Organization Structure',
'teamMembers.deptTree.all': 'All Departments', 'teamMembers.deptTree.all': 'All Departments',
'teamMembers.deptTree.add': 'Add Dept', 'teamMembers.deptTree.add': 'Add Dept',

查看文件

@@ -4,5 +4,5 @@
"menu": true, "menu": true,
"footer": true, "footer": true,
"themeColor": "#165DFF", "themeColor": "#165DFF",
"menuWidth": 220 "menuWidth": 230
} }

查看文件

@@ -1,5 +1,25 @@
@import 'nprogress/nprogress.css'; @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, html,
body { body {
width: 100%; width: 100%;
@@ -7,35 +27,19 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-size: 14px; 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 { .chart-wrapper {
.bizcharts-tooltip { .bizcharts-tooltip {
background: linear-gradient( background: var(--omni-color-bg) !important;
304.17deg, border: 1px solid var(--omni-color-border) !important;
rgb(253 254 255 / 60%) -6.04%, border-radius: 12px;
rgb(244 247 252 / 60%) 85.2% box-shadow: var(--omni-shadow) !important;
) !important;
border-radius: 6px;
backdrop-filter: blur(10px);
padding: 8px !important; padding: 8px !important;
width: 180px !important; width: 180px !important;
opacity: 1 !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;
}
}
}

查看文件

@@ -1,5 +1,4 @@
@nav-size-height: 60px; @nav-size-height: 60px;
@layout-max-width: 1100px;
.layout { .layout {
width: 100%; width: 100%;
@@ -9,7 +8,7 @@
.layout-navbar { .layout-navbar {
position: fixed; position: fixed;
width: 100%; width: 100%;
min-width: @layout-max-width; min-width: 0;
top: 0; top: 0;
left: 0; left: 0;
height: @nav-size-height; height: @nav-size-height;
@@ -60,16 +59,15 @@
} }
.collapse-btn { .collapse-btn {
height: 24px; height: 32px;
width: 24px; width: 32px;
background-color: var(--color-fill-1); background-color: var(--color-fill-1);
color: var(--color-text-3); color: var(--color-text-3);
border-radius: 2px; border-radius: 8px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
// 位置
position: absolute; position: absolute;
bottom: 12px; bottom: 12px;
right: 12px; right: 12px;
@@ -114,15 +112,17 @@
} }
.layout-content { .layout-content {
background-color: var(--color-fill-2); background-color: var(--omni-color-surface, var(--color-fill-2));
min-width: @layout-max-width; min-width: 0;
min-height: 100vh; min-height: 100vh;
transition: padding-left 0.2s; transition: padding-left 0.2s;
box-sizing: border-box; box-sizing: border-box;
} }
.layout-content-wrapper { .layout-content-wrapper {
padding: 16px 20px 0; padding: 24px 24px 32px;
width: 100%;
box-sizing: border-box;
} }
.layout-breadcrumb { .layout-breadcrumb {

查看文件

@@ -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') { if (theme === 'dark') {
document.body.setAttribute('arco-theme', 'dark'); document.body.setAttribute('arco-theme', 'dark');
} else { applyOmniTokens('dark');
document.body.removeAttribute('arco-theme'); return;
} }
document.body.removeAttribute('arco-theme');
applyOmniTokens('light');
} }
export default changeTheme; export default changeTheme;

查看文件

@@ -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('<Card title='), `${rel} 不得再用 Card.title 当页头`);
}
});
test('5. 表单设置页必须锁 720 窄栏', () => {
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'), '交接页必须使用窄栏');
});

查看文件

@@ -99,6 +99,6 @@ test('3. ai/knowledge locale 必须包含 menu.ai.knowledge 与中英文案', ()
assert.ok(content.includes('aiKnowledge.filter.keyWord'), '必须包含 keyWord 筛选文案'); assert.ok(content.includes('aiKnowledge.filter.keyWord'), '必须包含 keyWord 筛选文案');
assert.ok(!/\bany\b/.test(content), 'locale 禁止使用 any'); 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'], '知识库'); assert.equal(defaultI18n['zh-CN']['menu.ai.knowledge'], '知识库');
}); });

查看文件

@@ -33,7 +33,7 @@ test('1. routes.ts 已注册 card/cases,对应 pages/card/cases', () => {
}); });
test('2. locale 必须包含 menu.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'], '出海案例作品集'); assert.equal(defaultI18n['zh-CN']['menu.card.cases'], '出海案例作品集');
}); });

查看文件

@@ -23,8 +23,8 @@ test('1. routes.ts 必须包含 key card/list,供 layout glob 加载', () => {
test('2. locale 必须包含 menu.card / menu.card.list 中英文案', () => { test('2. locale 必须包含 menu.card / menu.card.list 中英文案', () => {
const content = fs.readFileSync(localePath, 'utf-8'); const content = fs.readFileSync(localePath, 'utf-8');
assert.ok(content.includes("'menu.card': 'Digital Cards'"), '英文 menu.card'); assert.ok(content.includes("'menu.card': 'Cards'"), '英文 menu.card');
assert.ok(content.includes("'menu.card.list': 'Card Library'"), '英文 menu.card.list'); 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': '数字名片'"), '中文 menu.card');
assert.ok(content.includes("'menu.card.list': '名片资产库'"), '中文 menu.card.list'); assert.ok(content.includes("'menu.card.list': '名片资产库'"), '中文 menu.card.list');
}); });

查看文件

@@ -38,7 +38,7 @@ test('1. routes.ts 必须包含 key crm/radar-log,供 layout glob 加载', ()
}); });
test('2. locale 必须包含 menu.crm.radarLog 中英文案', () => { 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'], '访客足迹'); assert.equal(defaultI18n['zh-CN']['menu.crm.radarLog'], '访客足迹');
}); });

查看文件

@@ -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');
});

查看文件

@@ -97,6 +97,7 @@ type UseLeadDetailModule = {
}, },
api?: SaveNoteApi api?: SaveNoteApi
) => Promise<LeadEntity>; ) => Promise<LeadEntity>;
resolveLeadDetailText: (key: string) => string;
}; };
const EMOJI_RE = /[\u2600-\u27BF\uFE0F]|[\uD83C-\uDBFF][\uDC00-\uDFFF]/; const EMOJI_RE = /[\u2600-\u27BF\uFE0F]|[\uD83C-\uDBFF][\uDC00-\uDFFF]/;
@@ -262,22 +263,48 @@ describe('OmniCard AI 名片主潜客详情业务逻辑 (useLeadDetail.uts)', ()
expect(result.status).toBe('hot'); 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)', () => { 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_PAGE)).toBe(true);
expect(fs.existsSync(DETAIL_UTS)).toBe(true); expect(fs.existsSync(DETAIL_UTS)).toBe(true);
const pageSource = fs.readFileSync(DETAIL_PAGE, 'utf8'); const pageSource = fs.readFileSync(DETAIL_PAGE, 'utf8');
const logicSource = fs.readFileSync(DETAIL_UTS, 'utf8'); const logicSource = fs.readFileSync(DETAIL_UTS, 'utf8');
const combined = `${pageSource}\n${logicSource}`; const combined = `${pageSource}\n${logicSource}`;
const template = pageSource.split('<script')[0];
expect(pageSource.includes('script setup lang="uts"')).toBe(true); expect(pageSource.includes('script setup lang="uts"')).toBe(true);
expect(pageSource.includes('getOmniText')).toBe(true); expect(pageSource.includes('getOmniText')).toBe(true);
expect(pageSource.includes('<scroll-view')).toBe(true); expect(pageSource.includes('<list-view')).toBe(true);
expect(pageSource.includes('<list-item')).toBe(true);
expect(/<view[^>]*v-for=/.test(pageSource)).toBe(false);
expect(pageSource.includes('<textarea')).toBe(true); expect(pageSource.includes('<textarea')).toBe(true);
expect(pageSource.includes('min-height: 88px')).toBe(true);
expect(pageSource.includes('primary-cta-btn')).toBe(true); expect(pageSource.includes('primary-cta-btn')).toBe(true);
expect(template.includes('WhatsApp')).toBe(false);
expect(pageSource.includes('calc(88px + env(safe-area-inset-bottom, 0px))')).toBe(
true
);
expect(pageSource.includes('lead-status-hot')).toBe(true);
expect(pageSource.includes('#F04438')).toBe(true);
expect(pageSource.includes('aiScript')).toBe(true);
expect(pageSource.includes('comingSoon')).toBe(true);
expect(pageSource.includes('uploadHint')).toBe(true);
expect(pageSource.includes('image-uploader')).toBe(true);
expect(logicSource.includes('async function submitLeadNote')).toBe(true);
expect( expect(
combined.includes('fetchOwnerLeadInfo') || combined.includes('loadDetail') combined.includes('fetchOwnerLeadInfo') || combined.includes('loadDetail')
).toBe(true); ).toBe(true);

查看文件

@@ -407,8 +407,19 @@ describe('OmniCard AI 展会团队战报页契约 (overview.uvue)', () => {
expect(pageSource.includes('<list-view')).toBe(true); expect(pageSource.includes('<list-view')).toBe(true);
expect(pageSource.includes('<list-item')).toBe(true); expect(pageSource.includes('<list-item')).toBe(true);
expect(pageSource.includes('length > 5') || logicSource.includes('length > 5')).toBe( expect(pageSource.includes('length > 5') || logicSource.includes('length > 5')).toBe(
true false
); );
expect((pageSource.match(/<list-view/g) || []).length).toBe(2);
expect(pageSource.includes('gap: 12px')).toBe(true);
expect(pageSource.includes('48%')).toBe(false);
expect(pageSource.includes('statusLabel')).toBe(true);
expect(pageSource.includes('lead-status-hot')).toBe(true);
expect(pageSource.includes(' · ')).toBe(true);
expect(pageSource.includes('isRtl')).toBe(true);
expect(pageSource.includes('overview-rtl')).toBe(true);
expect(pageSource.includes('--oc-error')).toBe(true);
expect(pageSource.includes('metric-country-long')).toBe(true);
expect(pageSource.includes('chart') || pageSource.includes('bar-chart')).toBe(false);
expect(combined.includes('summarize')).toBe(true); expect(combined.includes('summarize')).toBe(true);
expect(combined.includes('loadOverview')).toBe(true); expect(combined.includes('loadOverview')).toBe(true);
expect(combined.includes('fetchOwnerLeadPage')).toBe(true); expect(combined.includes('fetchOwnerLeadPage')).toBe(true);