-
+ {/* 移动端顶栏(当左侧Banner隐藏时生效) */}
+
-
-
-
-
-
-
-
+
+ {/* 右上角多语言切换入口 */}
+
+
+
+ {/* 主体两栏 */}
+
+ {/* 左侧品牌与核心能力展板 */}
+
+
+ {/* 右侧居中宽适登录卡片 */}
+
+
);
}
+
Login.displayName = 'LoginPage';
export default Login;
diff --git a/admin/src/pages/login/locale/index.ts b/admin/src/pages/login/locale/index.ts
index d3de87b..5701b0d 100644
--- a/admin/src/pages/login/locale/index.ts
+++ b/admin/src/pages/login/locale/index.ts
@@ -1,6 +1,7 @@
const i18n = {
'en-US': {
'login.form.title': 'Sign in to OmniCard',
+ 'login.form.subTitle': 'Global Digital Card & Mobile Mini-site SaaS',
'login.form.userName.errMsg': 'Username cannot be empty',
'login.form.password.errMsg': 'Password cannot be empty',
'login.form.login.errMsg': 'Login error, please refresh and try again',
@@ -14,9 +15,21 @@ const i18n = {
'login.form.forgetPassword': 'Forgot password',
'login.form.login': 'Sign in',
'login.form.register': 'Create account',
+ 'login.banner.badge': 'GLOBAL ENTERPRISE SAAS',
+ 'login.banner.mainTitle': 'The Intelligent Standard for Global Business Identity.',
+ 'login.banner.mainSubTitle':
+ 'Unifying NFC hardware, mobile mini-sites, and real-time visitor radar for global commerce.',
+ 'login.banner.card1.title': 'Global Reach',
+ 'login.banner.card1.metric': '120+',
+ 'login.banner.card1.desc': 'Countries with edge network distribution',
+ 'login.banner.card2.title': 'Visitor Radar',
+ 'login.banner.card2.metric': 'Real-time',
+ 'login.banner.card2.desc': 'Intent scoring & lead journey insights',
+ 'login.banner.card3.title': 'Enterprise Shield',
+ 'login.banner.card3.metric': 'GDPR & SOC2',
+ 'login.banner.card3.desc': 'Bank-grade encryption & privacy safeguards',
'login.banner.slogan1': 'Digital cards for global business',
- 'login.banner.subSlogan1':
- 'NFC tap or scan to open. No app required.',
+ 'login.banner.subSlogan1': 'NFC tap or scan to open. No app required.',
'login.banner.slogan2': 'Visitor radar and lightweight CRM',
'login.banner.subSlogan2':
'See who visits, how long they stay, and who is ready to talk',
@@ -25,6 +38,7 @@ const i18n = {
},
'zh-CN': {
'login.form.title': '登录 OmniCard',
+ 'login.form.subTitle': '全球智能数字名片与出海访客雷达中枢',
'login.form.userName.errMsg': '用户名不能为空',
'login.form.password.errMsg': '密码不能为空',
'login.form.login.errMsg': '登录出错,请刷新重试',
@@ -38,6 +52,19 @@ const i18n = {
'login.form.forgetPassword': '忘记密码',
'login.form.login': '登录',
'login.form.register': '注册账号',
+ 'login.banner.badge': '全球出海企业级数字身份',
+ 'login.banner.mainTitle': '下一代全球商务智能数字名片',
+ 'login.banner.mainSubTitle':
+ '集 NFC 实体卡管理、Bento Grid 微官网与实时访客雷达于一体的出海智能平台。',
+ 'login.banner.card1.title': '全球覆盖',
+ 'login.banner.card1.metric': '120+',
+ 'login.banner.card1.desc': '国家与地区,边缘节点毫秒分发',
+ 'login.banner.card2.title': '访客雷达',
+ 'login.banner.card2.metric': '实时捕获',
+ 'login.banner.card2.desc': '停留轨迹追踪与客商意向度量化',
+ 'login.banner.card3.title': '企业级安全',
+ 'login.banner.card3.metric': 'GDPR & SOC2',
+ 'login.banner.card3.desc': '银行级数据传输加密与隐私合规',
'login.banner.slogan1': '全球商务智能数字名片',
'login.banner.subSlogan1': 'NFC 触碰或扫码即开,无需下载',
'login.banner.slogan2': '访客雷达与轻量 CRM',
diff --git a/admin/src/pages/login/style/index.module.less b/admin/src/pages/login/style/index.module.less
index db228f7..8d99df9 100644
--- a/admin/src/pages/login/style/index.module.less
+++ b/admin/src/pages/login/style/index.module.less
@@ -1,158 +1,516 @@
+// OmniCard 登录页面样式规范 (瑞士国际主义 + 现代 SaaS + 编辑感)
+// 严格遵循 AGENTS.md Token 与 /ui-ux-pro-max 规范
+
.container {
display: flex;
height: 100vh;
+ width: 100vw;
+ overflow: hidden;
+ background-color: var(--omni-color-surface, #f8fafc);
+ position: relative;
+ font-family: var(--omni-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
+}
- .banner {
- width: 550px;
- background: linear-gradient(163.85deg, #1d2129 0%, #00308f 100%);
+// 移动端顶栏 (默认隐藏,小屏展示)
+.mobile-header {
+ display: none;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 56px;
+ padding: 0 20px;
+ align-items: center;
+ z-index: 15;
+ background: #ffffff;
+ border-bottom: 1px solid var(--omni-color-border, #e3e7ed);
+
+ .mobile-brand {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .logo-badge {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+
+ .logo-svg {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .brand-name {
+ font-size: 16px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ color: var(--omni-color-text, #101828);
+ }
+
+ .brand-tag {
+ font-size: 10px;
+ font-weight: 600;
+ line-height: 1;
+ padding: 2px 6px;
+ border-radius: 4px;
+ background: rgba(11, 92, 255, 0.1);
+ border: 1px solid rgba(11, 92, 255, 0.3);
+ color: var(--omni-color-primary, #0b5cff);
+ }
+ }
+}
+
+// 右上角多语言切换器
+.lang-container {
+ position: absolute;
+ top: 24px;
+ right: 32px;
+ z-index: 20;
+
+ .lang-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ height: 34px;
+ padding: 0 12px;
+ border-radius: 8px;
+ background: #ffffff;
+ border: 1px solid var(--omni-color-border, #e3e7ed);
+ color: var(--omni-color-text, #101828);
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
+ transition: all 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
+
+ &:hover {
+ background: #f1f5f9;
+ border-color: #cbd5e1;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--omni-color-primary, #0b5cff);
+ outline-offset: 2px;
+ }
+
+ .lang-icon {
+ font-size: 14px;
+ color: var(--omni-color-text-2, #667085);
+ }
+ }
+}
+
+// 主体双栏
+.main {
+ display: flex;
+ width: 100%;
+ height: 100%;
+}
+
+// 左侧品牌展板
+.banner {
+ width: 520px;
+ min-width: 500px;
+ height: 100%;
+ background: #0b0d10;
+ border-right: 1px solid #1e293b;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ position: relative;
+ overflow-y: auto;
+ padding: 48px;
+
+ // 瑞士精密微网格线 (微弱低调,无大面积霓虹渐变)
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background-image:
+ linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
+ linear-gradient(to bottom, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
+ background-size: 36px 36px;
+ pointer-events: none;
}
- .content {
- flex: 1;
+ &-inner {
position: relative;
- padding-bottom: 40px;
+ z-index: 2;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ &-board {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ }
+
+ // 品牌标识
+ .brand-identity {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+
+ .logo-badge {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+
+ .logo-svg {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .brand-name {
+ font-size: 20px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ color: #f2f4f7;
+ }
+
+ .brand-tag {
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1;
+ padding: 3px 6px;
+ border-radius: 6px;
+ background: rgba(11, 92, 255, 0.2);
+ border: 1px solid rgba(11, 92, 255, 0.4);
+ color: #4d8dff;
+ letter-spacing: 0.05em;
+ }
+ }
+
+ &-header {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ &-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ width: fit-content;
+ padding: 4px 10px;
+ background: rgba(255, 255, 255, 0.06);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 9999px;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ color: #94a3b8;
+ text-transform: uppercase;
+
+ &-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background-color: var(--omni-color-success, #12b76a);
+ box-shadow: 0 0 6px rgba(18, 183, 106, 0.6);
+ }
+ }
+
+ &-headline {
+ margin: 0;
+ font-size: 26px;
+ font-weight: 600;
+ line-height: 1.3;
+ letter-spacing: -0.02em;
+ color: #f8fafc;
+ }
+
+ &-description {
+ margin: 0;
+ font-size: 13px;
+ line-height: 1.6;
+ color: #94a3b8;
+ }
+
+ &-cards {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ &-metric-card {
+ background: #12151a;
+ border: 1px solid #262b33;
+ border-radius: 12px;
+ padding: 14px 18px;
+ transition: transform 150ms cubic-bezier(0.2, 0.8, 0.2, 1), border-color 150ms ease;
+
+ &:hover {
+ border-color: #334155;
+ transform: translateY(-1px);
+ }
+ }
+
+ &-metric-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 4px;
+ }
+
+ &-metric-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #4d8dff;
+ }
+
+ &-metric-number {
+ font-size: 15px;
+ font-weight: 700;
+ color: #f8fafc;
+ letter-spacing: -0.01em;
+ }
+
+ &-metric-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: #e2e8f0;
+ margin-bottom: 2px;
+ }
+
+ &-metric-desc {
+ font-size: 12px;
+ line-height: 1.4;
+ color: #64748b;
+ }
+
+ &-footer-note {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 11px;
+ color: #64748b;
+ margin-top: 4px;
+
+ .banner-quote-line {
+ width: 20px;
+ height: 1px;
+ background: #334155;
+ }
+ }
+}
+
+// 右侧内容区 (浅灰底板,呼吸感居中)
+.content {
+ flex: 1;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ position: relative;
+ padding: 32px 24px 20px;
+ background-color: var(--omni-color-surface, #f8fafc);
+ overflow-y: auto;
+
+ &-inner {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: auto 0;
}
.footer {
width: 100%;
- position: absolute;
- bottom: 0;
- right: 0;
+ text-align: center;
+ padding-top: 16px;
}
}
-.logo {
- position: fixed;
- top: 24px;
- left: 22px;
- display: inline-flex;
- align-items: center;
- z-index: 1;
+// 登录主卡片 (420px 宽适大气,瑞士国际主义白底卡片)
+.login-card {
+ width: 420px;
+ max-width: 100%;
+ padding: 40px 36px;
+ background: #ffffff;
+ border-radius: 16px;
+ border: 1px solid var(--omni-color-border, #e3e7ed);
+ box-shadow: 0 1px 3px rgba(16, 24, 40, 0.04), 0 20px 32px -12px rgba(16, 24, 40, 0.04);
- &-text {
- margin-left: 4px;
- margin-right: 4px;
- font-size: 20px;
- color: var(--color-fill-1);
- }
-}
-
-.banner {
- display: flex;
- justify-content: center;
- align-items: center;
-
- &-inner {
- height: 100%;
- flex: 1;
- }
-}
-
-.content {
- display: flex;
- justify-content: center;
- align-items: center;
-}
-
-.carousel {
- height: 100%;
-
- &-item {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- height: 100%;
- }
-
- &-title {
- font-weight: 500;
- font-size: 20px;
- line-height: 28px;
- color: var(--color-fill-1);
- }
-
- &-sub-title {
- margin-top: 8px;
- font-size: 14px;
- line-height: 22px;
- color: var(--color-text-3);
- }
-
- &-image {
- margin-top: 30px;
- width: 320px;
- }
-}
-
-.login-form {
- &-wrapper {
- width: 320px;
+ &-header {
+ margin-bottom: 24px;
}
&-title {
+ margin: 0;
font-size: 24px;
- font-weight: 500;
- color: var(--color-text-1);
- line-height: 32px;
+ font-weight: 600;
+ color: var(--omni-color-text, #101828);
+ line-height: 1.3;
+ letter-spacing: -0.02em;
}
&-sub-title {
- font-size: 16px;
- line-height: 24px;
- color: var(--color-text-3);
+ margin: 8px 0 0;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--omni-color-text-2, #667085);
+ }
+}
+
+// 错误提示条
+.login-error-alert {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 12px;
+ margin-bottom: 16px;
+ border-radius: 8px;
+ background-color: #fef3f2;
+ border: 1px solid #fee4e2;
+ color: var(--omni-color-error, #f04438);
+ font-size: 13px;
+ line-height: 1.4;
+
+ .login-error-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background-color: var(--omni-color-error, #f04438);
+ flex-shrink: 0;
+ }
+}
+
+.login-error-placeholder {
+ height: 8px;
+}
+
+// 表单项定制
+.login-form {
+ &-item {
+ margin-bottom: 20px;
+
+ :global(.arco-input-inner-wrapper) {
+ height: 44px;
+ border-radius: 10px;
+ border-color: var(--omni-color-border, #e3e7ed);
+ background-color: #ffffff;
+ padding-left: 14px;
+ padding-right: 14px;
+ transition: all 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
+
+ &:hover {
+ border-color: #cbd5e1;
+ }
+
+ &:focus-within {
+ border-color: var(--omni-color-primary, #0b5cff);
+ box-shadow: 0 0 0 2px var(--omni-color-primary-soft, #eaf1ff);
+ }
+ }
+
+ :global(.arco-input) {
+ font-size: 14px;
+ color: var(--omni-color-text, #101828);
+ }
}
- &-error-msg {
- height: 32px;
- line-height: 32px;
- color: rgb(var(--red-6));
+ .input-prefix-icon {
+ font-size: 16px;
+ color: var(--omni-color-text-2, #667085);
+ margin-right: 8px;
}
&-password-actions {
display: flex;
justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+ font-size: 13px;
+
+ :global(.arco-checkbox) {
+ font-size: 13px;
+ color: var(--omni-color-text-2, #667085);
+ }
+
+ .forgot-password-link {
+ font-size: 13px;
+ color: var(--omni-color-text-2, #667085);
+ cursor: pointer;
+ transition: color 150ms ease;
+
+ &:hover {
+ color: var(--omni-color-primary, #0b5cff);
+ }
+ }
+ }
+
+ .action-space {
+ width: 100%;
+ }
+
+ .login-primary-btn {
+ height: 44px;
+ border-radius: 12px;
+ font-size: 15px;
+ font-weight: 500;
+ background-color: var(--omni-color-primary, #0b5cff);
+ border-color: var(--omni-color-primary, #0b5cff);
+ box-shadow: 0 1px 2px rgba(11, 92, 255, 0.16);
+ transition: all 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
+
+ &:hover {
+ background-color: #004ed6;
+ border-color: #004ed6;
+ }
}
&-register-btn {
- color: var(--color-text-3) !important;
- }
-
- &-captcha {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- }
-
- &-captcha-item {
- flex: 1;
- margin-bottom: 20px;
- }
-
- &-captcha-refresh {
- width: 120px;
height: 44px;
- padding: 0;
- border: 1px solid var(--color-border-2);
border-radius: 12px;
- background: var(--color-fill-1);
- cursor: pointer;
- overflow: hidden;
- }
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--omni-color-text-2, #667085) !important;
+ transition: all 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
- &-captcha-image {
- display: block;
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
-
- &-captcha-placeholder {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- font-size: 12px;
- color: var(--color-text-3);
+ &:hover {
+ background-color: #f1f5f9 !important;
+ color: var(--omni-color-text, #101828) !important;
+ }
+ }
+}
+
+// 响应式设计
+@media (max-width: 1024px) {
+ .banner {
+ display: none;
+ }
+
+ .mobile-header {
+ display: flex;
+ }
+
+ .content {
+ padding-top: 80px;
+ }
+}
+
+@media (max-width: 480px) {
+ .login-card {
+ width: 100%;
+ padding: 24px 20px;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ }
+
+ .lang-container {
+ top: 12px;
+ right: 16px;
}
}
diff --git a/admin/tests/login-page-ui.test.ts b/admin/tests/login-page-ui.test.ts
new file mode 100644
index 0000000..bcb904a
--- /dev/null
+++ b/admin/tests/login-page-ui.test.ts
@@ -0,0 +1,71 @@
+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(rel: string): string {
+ return fs.readFileSync(path.join(adminRoot, rel), 'utf8');
+}
+
+test('1. 登录表单样式必须大气宽适并符合设计规范', () => {
+ const style = read('src/pages/login/style/index.module.less');
+
+ // 必须扩展表单宽度至 400px 以上,杜绝 320px 局促感
+ assert.ok(
+ /width:\s*(400|420|440|460|480)px/.test(style),
+ '登录表单宽度必须在 400px 以上,显得更宽适大气'
+ );
+
+ // 必须包含输入框或按钮高度 44px 标准尺寸
+ assert.ok(
+ style.includes('44px'),
+ '交互元素必须包含 44px 触控尺寸'
+ );
+
+ // 严禁纯黑 #000
+ assert.ok(!style.includes('#000;'), '严禁使用纯黑 #000');
+});
+
+test('2. 彻底去除模板化 AI 味与外部廉价插画', () => {
+ const banner = read('src/pages/login/banner.tsx');
+
+ // 彻底移除 byteimg 外部模板插画
+ assert.ok(
+ !banner.includes('p1-arco.byteimg.com'),
+ '严禁使用 Arco 外部模板图纸插画'
+ );
+
+ // 必须包含 OmniCard 全球商务品牌核心能力(全球覆盖、访客雷达、安全合规)
+ assert.ok(
+ banner.includes('Global') || banner.includes('global') || banner.includes('Radar') || banner.includes('radar'),
+ '左侧品牌看板必须包含全球商业与访客雷达相关能力陈列'
+ );
+
+ // 严禁 emoji 作为图标
+ const emojiRegex = /[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
+ assert.ok(!emojiRegex.test(banner), 'Banner 中严禁使用 emoji');
+});
+
+test('3. 登录主页面必须支持多语言切换与品牌标识', () => {
+ const index = read('src/pages/login/index.tsx');
+
+ // 页面必须包含多语言选择入口
+ assert.ok(
+ index.includes('Select') || index.includes('Dropdown') || index.includes('Radio') || index.includes('lang'),
+ '登录主页面必须支持语言切换'
+ );
+
+ // 严禁 any
+ assert.ok(!/\bany\b/.test(index), 'index.tsx 严禁使用 any');
+});
+
+test('4. 登录表单核心逻辑依然与 Cool-Admin 契约兼容且无 any', () => {
+ const form = read('src/pages/login/form.tsx');
+
+ assert.ok(form.includes('/api/admin/base/open/login'), '接口路径保持不变');
+ assert.ok(form.includes('size="large"') || form.includes("size={'large'}"), '输入框采用大尺寸以显大气');
+ assert.ok(!/\bany\b/.test(form), 'form.tsx 严禁使用 any');
+});