fix(admin): 优化管理后台英文菜单多语言文案杜绝截断并修复各业务页面与API错误
这个提交包含在:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 = (
|
||||
<Menu
|
||||
@@ -107,7 +119,12 @@ function Navbar({ show }: { show: boolean }) {
|
||||
</li>
|
||||
<li>
|
||||
<Select
|
||||
triggerElement={<IconButton icon={<IconLanguage />} />}
|
||||
triggerElement={
|
||||
<IconButton
|
||||
icon={<IconLanguage />}
|
||||
aria-label={t['navbar.language'] || 'Language'}
|
||||
/>
|
||||
}
|
||||
options={[
|
||||
{ label: '中文', value: 'zh-CN' },
|
||||
{ label: 'English', value: 'en-US' },
|
||||
@@ -136,6 +153,11 @@ function Navbar({ show }: { show: boolean }) {
|
||||
>
|
||||
<IconButton
|
||||
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')}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -143,19 +165,21 @@ function Navbar({ show }: { show: boolean }) {
|
||||
<Settings />
|
||||
{userInfo && (
|
||||
<li style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Tag
|
||||
color="arcoblue"
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
fontWeight: 500,
|
||||
fontSize: 12,
|
||||
padding: '2px 8px',
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
<IconSafe style={{ marginRight: 4 }} />
|
||||
{roleTitle}
|
||||
</Tag>
|
||||
{isSuperAdmin ? (
|
||||
<Tag
|
||||
color="arcoblue"
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
fontWeight: 500,
|
||||
fontSize: 12,
|
||||
padding: '2px 8px',
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
<IconSafe style={{ marginRight: 4 }} />
|
||||
{roleTitle}
|
||||
</Tag>
|
||||
) : null}
|
||||
<Dropdown droplist={droplist} position="br" disabled={userLoading}>
|
||||
<Avatar size={32} style={{ cursor: 'pointer' }}>
|
||||
{userLoading ? (
|
||||
|
||||
@@ -15,23 +15,23 @@
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 200px;
|
||||
padding-left: 20px;
|
||||
gap: 12px;
|
||||
width: 220px;
|
||||
padding-inline-start: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo-name {
|
||||
color: var(--color-text-1);
|
||||
color: var(--omni-color-text, var(--color-text-1));
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
margin-left: 10px;
|
||||
font-family: 'PingFang SC';
|
||||
font-family: var(--omni-font-family);
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding-right: 20px;
|
||||
padding-inline-end: 16px;
|
||||
|
||||
li {
|
||||
padding: 0 8px;
|
||||
@@ -50,8 +50,10 @@
|
||||
}
|
||||
|
||||
.round {
|
||||
width: 240px;
|
||||
|
||||
:global(.arco-input-inner-wrapper) {
|
||||
border-radius: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
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': {
|
||||
...omniLocales['en-US'].messages,
|
||||
'menu.dashboard': 'Dashboard',
|
||||
'menu.card': 'Digital Cards',
|
||||
'menu.card.list': 'Card Library',
|
||||
'menu.card': 'Cards',
|
||||
'menu.card.list': 'All Cards',
|
||||
'menu.card.builder': 'Bento Builder',
|
||||
'menu.card.cases': 'Case Studies',
|
||||
'menu.card.articles': 'Articles & Posts',
|
||||
'menu.card.cases': 'Portfolio',
|
||||
'menu.card.articles': 'Articles',
|
||||
'menu.crm': 'Radar CRM',
|
||||
'menu.crm.leads': 'Leads',
|
||||
'menu.crm.pipeline': 'Pipeline',
|
||||
'menu.crm.radarLog': 'Visitor Radar',
|
||||
'menu.crm.radarLog': 'Visitor Logs',
|
||||
'menu.crm.leadProfile': 'Lead Profile',
|
||||
'menu.nfc': 'NFC Fleet',
|
||||
'menu.nfc.devices': 'Device Pool',
|
||||
'menu.nfc': 'NFC Cards',
|
||||
'menu.nfc.devices': 'Devices',
|
||||
'menu.nfc.batch': 'Batch Cards',
|
||||
'menu.nfc.rules': 'Routing Rules',
|
||||
'menu.ai': 'Omni AI',
|
||||
'menu.ai.deepseek': 'Model & BYOK',
|
||||
'menu.ai.knowledge': 'Knowledge Base',
|
||||
'menu.ai.translation': 'AI Translation & Glossary',
|
||||
'menu.ai.knowledge': 'Knowledge',
|
||||
'menu.ai.translation': 'Glossary',
|
||||
'menu.ai.concierge': 'AI Concierge',
|
||||
'menu.team': 'Team',
|
||||
'menu.team.members': 'Members & Structure',
|
||||
'menu.team.members': 'Members',
|
||||
'menu.team.brandKit': 'Brand Kit',
|
||||
'menu.team.handover': 'Handover',
|
||||
'menu.settingsBiz': 'Workspace',
|
||||
'menu.settings.tenant': 'Domain & SSL',
|
||||
'menu.settings.billing': 'Billing',
|
||||
'menu.settings.auditLogs': 'Audit Logs',
|
||||
'menu.platform': 'Platform Operations',
|
||||
'menu.platform': 'Platform',
|
||||
'menu.platform.subscribers': 'Subscribers',
|
||||
'menu.platform.renewals': 'Renewals',
|
||||
'menu.platform.subscriptions': 'Subscription Plans',
|
||||
'menu.platform.subscriptions': 'Plans',
|
||||
'menu.exception': 'Exception',
|
||||
'menu.exception.403': '403',
|
||||
'menu.exception.404': '404',
|
||||
'menu.exception.500': '500',
|
||||
'menu.dashboard.workplace': 'Workplace',
|
||||
'menu.dashboard.radarMap': 'Global Radar Heatmap',
|
||||
'menu.dashboard.financial': 'Financial & Operations',
|
||||
'menu.dashboard.workplace': 'Overview',
|
||||
'menu.dashboard.radarMap': 'Radar Map',
|
||||
'menu.dashboard.financial': 'Finance',
|
||||
'navbar.logout': 'Logout',
|
||||
'settings.title': 'Settings',
|
||||
'settings.themeColor': 'Theme Color',
|
||||
|
||||
@@ -332,13 +332,7 @@ export default function AiConciergePage(): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: spacing[24],
|
||||
backgroundColor: lightColors.bg,
|
||||
minHeight: '100%',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Card
|
||||
title={t['aiConcierge.card.title']}
|
||||
bordered={false}
|
||||
|
||||
@@ -139,9 +139,9 @@ export default function DeepSeekConsolePage(): React.ReactElement {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<div>
|
||||
<Spin loading={loading} style={{ width: '100%' }}>
|
||||
<Space direction="vertical" size={20} style={{ width: '100%' }}>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
{/* 页面标题区 */}
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
@@ -165,7 +165,7 @@ export default function DeepSeekConsolePage(): React.ReactElement {
|
||||
title={t['ai.deepseek.stats.totalTokens']}
|
||||
value={usage.totalTokens}
|
||||
groupSeparator
|
||||
valueStyle={{ color: lightColors.primary, fontWeight: 700 }}
|
||||
style={{ color: lightColors.primary, fontWeight: 700 }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
@@ -203,7 +203,7 @@ export default function DeepSeekConsolePage(): React.ReactElement {
|
||||
</div>
|
||||
<Progress
|
||||
percent={usage.percent}
|
||||
status={usage.percent >= 90 ? 'danger' : usage.percent >= 70 ? 'warning' : 'normal'}
|
||||
status={usage.percent >= 90 ? 'error' : usage.percent >= 70 ? 'warning' : 'normal'}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -217,7 +217,7 @@ export default function DeepSeekConsolePage(): React.ReactElement {
|
||||
|
||||
{/* 模型配置与 BYOK 表单 */}
|
||||
<Card title="模型连接参数 (BYOK)" bordered={true} style={{ borderRadius: '16px' }}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSave}>
|
||||
<Form form={form} layout="vertical" onSubmit={handleSave}>
|
||||
<Form.Item
|
||||
label={t['ai.deepseek.form.apiKey']}
|
||||
field="apiKey"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const i18n = {
|
||||
'en-US': {
|
||||
'menu.ai.knowledge': 'Knowledge Base',
|
||||
'menu.ai.knowledge': 'Knowledge',
|
||||
'aiKnowledge.filter.cardId': 'Card ID',
|
||||
'aiKnowledge.filter.cardId.placeholder': 'Filter by card',
|
||||
'aiKnowledge.filter.locale': 'Language',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const i18n = {
|
||||
'en-US': {
|
||||
'menu.ai.translation': 'AI Translation & Glossary',
|
||||
'menu.ai.translation': 'Glossary',
|
||||
'aiTranslation.playground.title': 'Live Localization Playground',
|
||||
'aiTranslation.playground.text': 'Source Text',
|
||||
'aiTranslation.playground.text.placeholder': 'Enter text to polish and align with glossary terms...',
|
||||
|
||||
@@ -381,7 +381,7 @@ export default function CardArticlesPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', backgroundColor: lightColors.surface, minHeight: '100%' }}>
|
||||
<div>
|
||||
{/* 头部导航与操作栏 */}
|
||||
<Card
|
||||
bordered={false}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const i18n = {
|
||||
'en-US': {
|
||||
'menu.card.articles': 'Articles & Posts',
|
||||
'menu.card.articles': 'Articles',
|
||||
'cardArticles.title': 'Articles & Updates Hub',
|
||||
'cardArticles.description': 'Publish blog articles and dynamic updates to your digital card portal with automated OpenGraph SEO extraction.',
|
||||
'cardArticles.action.add': 'Create Article',
|
||||
|
||||
@@ -394,7 +394,7 @@ export default function CardCasesPage(): React.ReactElement {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px 24px', backgroundColor: lightColors.bg }}>
|
||||
<div>
|
||||
{/* 头部标题与单一主 CTA */}
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -4,12 +4,19 @@ import {
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Card,
|
||||
Badge,
|
||||
Message,
|
||||
Button,
|
||||
Popconfirm,
|
||||
Space,
|
||||
} 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 axios from 'axios';
|
||||
import copy from 'copy-to-clipboard';
|
||||
@@ -317,7 +324,7 @@ function CardListPage() {
|
||||
? t['cardList.unpublish.confirm']
|
||||
: t['cardList.publish.confirm'];
|
||||
return (
|
||||
<>
|
||||
<Space size={8}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
@@ -351,7 +358,7 @@ function CardListPage() {
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : null}
|
||||
</>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -374,43 +381,64 @@ function CardListPage() {
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card title={t['menu.card.list']}>
|
||||
<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>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t['cardList.form.search']}
|
||||
<PageShell>
|
||||
<PageHeader
|
||||
title={t['menu.card.list']}
|
||||
subtitle={t['cardList.page.subtitle']}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => history.push('/card/builder')}
|
||||
>
|
||||
{t['cardList.page.create']}
|
||||
</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 = {
|
||||
'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.placeholder': 'Search by name',
|
||||
'cardList.filter.company': 'Company',
|
||||
@@ -38,6 +40,8 @@ const i18n = {
|
||||
},
|
||||
'zh-CN': {
|
||||
'menu.card.list': '名片资产库',
|
||||
'cardList.page.subtitle': '在同一处查看、发布并分享数字名片。',
|
||||
'cardList.page.create': '新建名片',
|
||||
'cardList.filter.name': '姓名',
|
||||
'cardList.filter.name.placeholder': '按姓名筛选',
|
||||
'cardList.filter.company': '公司',
|
||||
@@ -75,6 +79,8 @@ const i18n = {
|
||||
},
|
||||
'ar-SA': {
|
||||
'menu.card.list': 'مكتبة البطاقات',
|
||||
'cardList.page.subtitle': 'راجع البطاقات وانشرها وشاركها من مكان واحد.',
|
||||
'cardList.page.create': 'بطاقة جديدة',
|
||||
'cardList.filter.name': 'الاسم',
|
||||
'cardList.filter.name.placeholder': 'البحث بالاسم',
|
||||
'cardList.filter.company': 'الشركة',
|
||||
|
||||
@@ -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 { useHistory } from 'react-router-dom';
|
||||
@@ -308,44 +314,56 @@ function CrmLeadsPage() {
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card title={t['menu.crm.leads']}>
|
||||
<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>
|
||||
<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}
|
||||
<PageShell>
|
||||
<PageHeader
|
||||
title={t['menu.crm.leads']}
|
||||
subtitle={t['crmLeads.page.subtitle']}
|
||||
/>
|
||||
</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 = {
|
||||
'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': 'الحالة',
|
||||
|
||||
@@ -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 (
|
||||
<Card title={t['menu.crm.radarLog']}>
|
||||
<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>
|
||||
<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}
|
||||
<PageShell>
|
||||
<PageHeader
|
||||
title={t['menu.crm.radarLog']}
|
||||
subtitle={t['radarLog.page.subtitle']}
|
||||
/>
|
||||
</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 = {
|
||||
'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': 'طريقة الوصول',
|
||||
|
||||
@@ -189,9 +189,9 @@ export default function DashboardFinancialPage() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', backgroundColor: lightColors.surface }}>
|
||||
<div>
|
||||
<Spin loading={loading} style={{ width: '100%' }}>
|
||||
<Space direction="vertical" size={20} style={{ width: '100%' }}>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
{/* 1. 顶部控制栏与全局视角切换 */}
|
||||
<Card
|
||||
bordered={false}
|
||||
@@ -279,7 +279,7 @@ export default function DashboardFinancialPage() {
|
||||
<Grid.Row gutter={16}>
|
||||
{view === 'coo' ? (
|
||||
<>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -295,7 +295,7 @@ export default function DashboardFinancialPage() {
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -311,7 +311,7 @@ export default function DashboardFinancialPage() {
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -325,7 +325,7 @@ export default function DashboardFinancialPage() {
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -339,7 +339,7 @@ export default function DashboardFinancialPage() {
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -354,7 +354,7 @@ export default function DashboardFinancialPage() {
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -372,7 +372,7 @@ export default function DashboardFinancialPage() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -388,7 +388,7 @@ export default function DashboardFinancialPage() {
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -404,7 +404,7 @@ export default function DashboardFinancialPage() {
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -420,7 +420,7 @@ export default function DashboardFinancialPage() {
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -436,7 +436,7 @@ export default function DashboardFinancialPage() {
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -452,7 +452,7 @@ export default function DashboardFinancialPage() {
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<Grid.Col xs={24} sm={12} md={8} xl={4}>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
@@ -476,7 +476,7 @@ export default function DashboardFinancialPage() {
|
||||
{view === 'coo' ? (
|
||||
<Grid.Row gutter={16}>
|
||||
{/* COO 模块 A: SaaS 套餐梯队分布 */}
|
||||
<Grid.Col span={12}>
|
||||
<Grid.Col xs={24} lg={12}>
|
||||
<Card
|
||||
title={t['dashboardFinancial.section.planDistribution']}
|
||||
bordered={false}
|
||||
@@ -532,7 +532,7 @@ export default function DashboardFinancialPage() {
|
||||
</Grid.Col>
|
||||
|
||||
{/* COO 模块 B: 全球出海目标市场营收分布 */}
|
||||
<Grid.Col span={12}>
|
||||
<Grid.Col xs={24} lg={12}>
|
||||
<Card
|
||||
title={t['dashboardFinancial.section.regionalRevenue']}
|
||||
bordered={false}
|
||||
@@ -576,7 +576,7 @@ export default function DashboardFinancialPage() {
|
||||
) : (
|
||||
<Grid.Row gutter={16}>
|
||||
{/* CFO 模块 A: 国际多币种汇总结算结构 */}
|
||||
<Grid.Col span={12}>
|
||||
<Grid.Col xs={24} lg={12}>
|
||||
<Card
|
||||
title={t['dashboardFinancial.section.currencyBreakdown']}
|
||||
bordered={false}
|
||||
@@ -621,7 +621,7 @@ export default function DashboardFinancialPage() {
|
||||
</Grid.Col>
|
||||
|
||||
{/* CFO 模块 B: 应收账款账龄与坏账风险分析 */}
|
||||
<Grid.Col span={12}>
|
||||
<Grid.Col xs={24} lg={12}>
|
||||
<Card
|
||||
title={t['dashboardFinancial.section.agingAnalysis']}
|
||||
bordered={false}
|
||||
|
||||
@@ -232,13 +232,7 @@ export default function RadarMapDashboard(): React.ReactElement {
|
||||
const top10Countries = (analyticsData.countryDistribution || []).slice(0, 10);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: spacing[24],
|
||||
background: lightColors.surface,
|
||||
minHeight: '100%',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{/* 顶部标题区与唯一主 CTA */}
|
||||
<div
|
||||
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 { useSelector } from 'react-redux';
|
||||
import axios from 'axios';
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
IconUserGroup,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import { GlobalContext } from '@/context';
|
||||
import { getCssVariables, lightColors } from '@/style/tokens';
|
||||
import type { GlobalState } from '@/store';
|
||||
import type {
|
||||
@@ -139,6 +140,7 @@ function resolveErrorMessage(error: unknown, fallback: string): string {
|
||||
function Workplace(): React.ReactElement {
|
||||
const t = useLocale(locale);
|
||||
const history = useHistory();
|
||||
const { theme } = useContext(GlobalContext);
|
||||
const userInfo = useSelector((state: GlobalState) => 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(() => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<Card title={t['menu.nfc.batch']}>
|
||||
<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>
|
||||
<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,
|
||||
}}
|
||||
<PageShell>
|
||||
<PageHeader
|
||||
title={t['menu.nfc.batch']}
|
||||
subtitle={t['nfcBatch.page.subtitle']}
|
||||
/>
|
||||
</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 = {
|
||||
'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': 'الخامة',
|
||||
|
||||
@@ -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 (
|
||||
<Card title={t['menu.nfc.devices']}>
|
||||
<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>
|
||||
<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}
|
||||
<PageShell>
|
||||
<PageHeader
|
||||
title={t['menu.nfc.devices']}
|
||||
subtitle={t['nfcDevices.page.subtitle']}
|
||||
/>
|
||||
</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 = {
|
||||
'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': 'الحالة',
|
||||
|
||||
@@ -525,13 +525,7 @@ const NfcRulesPage: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: spacing[24],
|
||||
backgroundColor: lightColors.surface,
|
||||
minHeight: '100%',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{/* 顶部筛选与单一主 CTA 区域 */}
|
||||
<Card
|
||||
style={{
|
||||
|
||||
@@ -151,7 +151,7 @@ export default function RenewalsPage() {
|
||||
case 'pending':
|
||||
return <Badge status="warning" text="等待入账" />;
|
||||
case 'refunded':
|
||||
return <Badge status="danger" text="已退款注销" />;
|
||||
return <Badge status="error" text="已退款注销" />;
|
||||
default:
|
||||
return <Badge status="default" text={status} />;
|
||||
}
|
||||
@@ -237,7 +237,7 @@ export default function RenewalsPage() {
|
||||
.reduce((sum, d) => sum + Number(d.amount || 0), 0);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<div>
|
||||
{/* 顶部统计卡片 */}
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}>
|
||||
@@ -257,7 +257,7 @@ export default function RenewalsPage() {
|
||||
value={totalCollected}
|
||||
prefix="$"
|
||||
precision={2}
|
||||
valueStyle={{ color: '#00B42A' }}
|
||||
style={{ color: '#00B42A' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -273,7 +273,7 @@ export default function RenewalsPage() {
|
||||
: 80
|
||||
}
|
||||
suffix="%"
|
||||
valueStyle={{ color: '#0B5CFF' }}
|
||||
style={{ color: '#0B5CFF' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -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' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -374,7 +374,7 @@ export default function RenewalsPage() {
|
||||
visible={manualModalVisible}
|
||||
onOk={handleConfirmManualRenew}
|
||||
onCancel={() => setManualModalVisible(false)}
|
||||
width={500}
|
||||
style={{ width: 500 }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="选择会员企业 (租户)" field="tenantId" required>
|
||||
|
||||
@@ -189,7 +189,7 @@ export default function SubscribersPage() {
|
||||
case 'past_due':
|
||||
return <Badge status="warning" text="已到期待续费" />;
|
||||
case 'canceled':
|
||||
return <Badge status="danger" text="已冻结停用" />;
|
||||
return <Badge status="error" text="已冻结停用" />;
|
||||
default:
|
||||
return <Badge status="default" text={status} />;
|
||||
}
|
||||
@@ -296,7 +296,7 @@ export default function SubscribersPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<div>
|
||||
{/* 顶部总控概览卡片 */}
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}>
|
||||
@@ -314,7 +314,7 @@ export default function SubscribersPage() {
|
||||
<Statistic
|
||||
title="付费出海企业 (Pro/Ent)"
|
||||
value={(overview?.proCount || 0) + (overview?.enterpriseCount || 0)}
|
||||
valueStyle={{ color: '#0B5CFF' }}
|
||||
style={{ color: '#0B5CFF' }}
|
||||
suffix="家"
|
||||
/>
|
||||
</Card>
|
||||
@@ -324,7 +324,7 @@ export default function SubscribersPage() {
|
||||
<Statistic
|
||||
title="30天内到期预警会员"
|
||||
value={overview?.expiringSoonCount || 1}
|
||||
valueStyle={{ color: '#FF7D00' }}
|
||||
style={{ color: '#FF7D00' }}
|
||||
suffix="家"
|
||||
/>
|
||||
</Card>
|
||||
@@ -336,7 +336,7 @@ export default function SubscribersPage() {
|
||||
value={overview?.totalRenewalRevenue || 8540}
|
||||
prefix="$"
|
||||
precision={2}
|
||||
valueStyle={{ color: '#00B42A' }}
|
||||
style={{ color: '#00B42A' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function SubscriptionsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<div>
|
||||
{/* 顶部公告 */}
|
||||
<Alert
|
||||
type="info"
|
||||
@@ -115,7 +115,7 @@ export default function SubscriptionsPage() {
|
||||
title="当前活跃套餐梯队"
|
||||
value={plans.length}
|
||||
suffix="档"
|
||||
valueStyle={{ color: '#0B5CFF' }}
|
||||
style={{ color: '#0B5CFF' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -124,7 +124,7 @@ export default function SubscriptionsPage() {
|
||||
<Statistic
|
||||
title="推荐出海主推方案"
|
||||
value="Pro 专业商务版"
|
||||
valueStyle={{ fontSize: 18, color: '#00B42A' }}
|
||||
style={{ fontSize: 18, color: '#00B42A' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -134,7 +134,7 @@ export default function SubscriptionsPage() {
|
||||
title="年付优惠策略折扣"
|
||||
value={16.7}
|
||||
suffix="%"
|
||||
valueStyle={{ color: '#FF7D00' }}
|
||||
style={{ color: '#FF7D00' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -266,7 +266,7 @@ export default function SubscriptionsPage() {
|
||||
visible={editModalVisible}
|
||||
onOk={handleSavePlan}
|
||||
onCancel={() => setEditModalVisible(false)}
|
||||
width={520}
|
||||
style={{ width: 520 }}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="套餐展示全称" field="name" required>
|
||||
|
||||
@@ -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 (
|
||||
<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%' }}>
|
||||
<Alert type="info" content={t['settingsBilling.alert.later']} />
|
||||
<Typography.Title
|
||||
@@ -173,7 +175,7 @@ function SettingsBillingPage() {
|
||||
</Typography.Title>
|
||||
<Grid.Row gutter={24}>
|
||||
<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']}
|
||||
</Typography.Text>
|
||||
<Progress
|
||||
@@ -183,7 +185,7 @@ function SettingsBillingPage() {
|
||||
/>
|
||||
</Grid.Col>
|
||||
<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']}
|
||||
</Typography.Text>
|
||||
<Progress
|
||||
@@ -215,7 +217,8 @@ function SettingsBillingPage() {
|
||||
{t['settingsBilling.action.refresh']}
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</ContentCard>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<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%' }}>
|
||||
<Alert type="info" content={t['settingsTenant.ssl.alert']} />
|
||||
<Space>
|
||||
@@ -255,7 +257,8 @@ function SettingsTenantPage() {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
</Card>
|
||||
</ContentCard>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Card title={t['menu.team.handover']}>
|
||||
<PageShell narrow>
|
||||
<PageHeader title={t['menu.team.handover']} />
|
||||
<ContentCard>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
@@ -193,7 +195,8 @@ function TeamHandoverPage() {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
</Card>
|
||||
</ContentCard>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -429,7 +429,7 @@ function TeamMembersPage() {
|
||||
<Card title={t['menu.team.members']}>
|
||||
<Row gutter={16}>
|
||||
{/* 左侧部门树 */}
|
||||
<Col span={6}>
|
||||
<Col xs={24} lg={6}>
|
||||
<Card
|
||||
title={t['teamMembers.deptTree.title']}
|
||||
extra={
|
||||
@@ -495,7 +495,7 @@ function TeamMembersPage() {
|
||||
</Col>
|
||||
|
||||
{/* 右侧成员列表 */}
|
||||
<Col span={18}>
|
||||
<Col xs={24} lg={18}>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
{/* 顶栏操作区:单屏单一主 CTA */}
|
||||
<div
|
||||
@@ -509,7 +509,7 @@ function TeamMembersPage() {
|
||||
<Input.Search
|
||||
allowClear
|
||||
placeholder={t['teamMembers.memberTable.searchPlaceholder']}
|
||||
style={{ width: 320 }}
|
||||
style={{ width: '100%', maxWidth: 320 }}
|
||||
value={keyword}
|
||||
onChange={(val) => setKeyword(val)}
|
||||
onSearch={() => fetchMembers(1, pagination.pageSize)}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"menu": true,
|
||||
"footer": true,
|
||||
"themeColor": "#165DFF",
|
||||
"menuWidth": 220
|
||||
"menuWidth": 230
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(!/\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'], '知识库');
|
||||
});
|
||||
|
||||
@@ -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'], '出海案例作品集');
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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'], '访客足迹');
|
||||
});
|
||||
|
||||
|
||||
72
admin/tests/menu-locales.test.ts
普通文件
72
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');
|
||||
});
|
||||
@@ -97,6 +97,7 @@ type UseLeadDetailModule = {
|
||||
},
|
||||
api?: SaveNoteApi
|
||||
) => Promise<LeadEntity>;
|
||||
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('<script')[0];
|
||||
|
||||
expect(pageSource.includes('script setup lang="uts"')).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('min-height: 88px')).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(
|
||||
combined.includes('fetchOwnerLeadInfo') || combined.includes('loadDetail')
|
||||
).toBe(true);
|
||||
|
||||
@@ -407,8 +407,19 @@ describe('OmniCard AI 展会团队战报页契约 (overview.uvue)', () => {
|
||||
expect(pageSource.includes('<list-view')).toBe(true);
|
||||
expect(pageSource.includes('<list-item')).toBe(true);
|
||||
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('loadOverview')).toBe(true);
|
||||
expect(combined.includes('fetchOwnerLeadPage')).toBe(true);
|
||||
|
||||
在新工单中引用
屏蔽一个用户