fix(admin): 优化管理后台英文菜单多语言文案杜绝截断并修复各业务页面与API错误
这个提交包含在:
@@ -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'], '访客足迹');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
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
|
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);
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户