style(ui): 统一移动端登录排版设计Token与战报榜单卡片样式
这个提交包含在:
@@ -24,25 +24,27 @@ import * as entity21 from './modules/crm/entity/audit';
|
|||||||
import * as entity22 from './modules/card/entity/locale';
|
import * as entity22 from './modules/card/entity/locale';
|
||||||
import * as entity23 from './modules/card/entity/info';
|
import * as entity23 from './modules/card/entity/info';
|
||||||
import * as entity24 from './modules/card/entity/exchange';
|
import * as entity24 from './modules/card/entity/exchange';
|
||||||
import * as entity25 from './modules/card/entity/contact';
|
import * as entity25 from './modules/card/entity/event';
|
||||||
import * as entity26 from './modules/card/entity/cases';
|
import * as entity26 from './modules/card/entity/event-member';
|
||||||
import * as entity27 from './modules/card/entity/bento';
|
import * as entity27 from './modules/card/entity/contact';
|
||||||
import * as entity28 from './modules/card/entity/article';
|
import * as entity28 from './modules/card/entity/cases';
|
||||||
import * as entity29 from './modules/base/entity/base';
|
import * as entity29 from './modules/card/entity/bento';
|
||||||
import * as entity30 from './modules/base/entity/audit';
|
import * as entity30 from './modules/card/entity/article';
|
||||||
import * as entity31 from './modules/base/entity/sys/user_role';
|
import * as entity31 from './modules/base/entity/base';
|
||||||
import * as entity32 from './modules/base/entity/sys/user';
|
import * as entity32 from './modules/base/entity/audit';
|
||||||
import * as entity33 from './modules/base/entity/sys/role_menu';
|
import * as entity33 from './modules/base/entity/sys/user_role';
|
||||||
import * as entity34 from './modules/base/entity/sys/role_department';
|
import * as entity34 from './modules/base/entity/sys/user';
|
||||||
import * as entity35 from './modules/base/entity/sys/role';
|
import * as entity35 from './modules/base/entity/sys/role_menu';
|
||||||
import * as entity36 from './modules/base/entity/sys/param';
|
import * as entity36 from './modules/base/entity/sys/role_department';
|
||||||
import * as entity37 from './modules/base/entity/sys/menu';
|
import * as entity37 from './modules/base/entity/sys/role';
|
||||||
import * as entity38 from './modules/base/entity/sys/log';
|
import * as entity38 from './modules/base/entity/sys/param';
|
||||||
import * as entity39 from './modules/base/entity/sys/department';
|
import * as entity39 from './modules/base/entity/sys/menu';
|
||||||
import * as entity40 from './modules/base/entity/sys/conf';
|
import * as entity40 from './modules/base/entity/sys/log';
|
||||||
import * as entity41 from './modules/ai/entity/knowledge';
|
import * as entity41 from './modules/base/entity/sys/department';
|
||||||
import * as entity42 from './modules/ai/entity/glossary';
|
import * as entity42 from './modules/base/entity/sys/conf';
|
||||||
import * as entity43 from './modules/ai/entity/concierge';
|
import * as entity43 from './modules/ai/entity/knowledge';
|
||||||
|
import * as entity44 from './modules/ai/entity/glossary';
|
||||||
|
import * as entity45 from './modules/ai/entity/concierge';
|
||||||
export const entities = [
|
export const entities = [
|
||||||
...Object.values(entity0),
|
...Object.values(entity0),
|
||||||
...Object.values(entity1),
|
...Object.values(entity1),
|
||||||
@@ -88,4 +90,6 @@ export const entities = [
|
|||||||
...Object.values(entity41),
|
...Object.values(entity41),
|
||||||
...Object.values(entity42),
|
...Object.values(entity42),
|
||||||
...Object.values(entity43),
|
...Object.values(entity43),
|
||||||
|
...Object.values(entity44),
|
||||||
|
...Object.values(entity45),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as md5 from 'md5';
|
|||||||
import { BaseSysDepartmentEntity } from '../../entity/sys/department';
|
import { BaseSysDepartmentEntity } from '../../entity/sys/department';
|
||||||
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
|
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
|
||||||
import { BaseSysRoleEntity } from '../../entity/sys/role';
|
import { BaseSysRoleEntity } from '../../entity/sys/role';
|
||||||
|
import { isSuperAdminContext } from '../../../../comm/guard/super-admin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统用户
|
* 系统用户
|
||||||
@@ -103,7 +104,20 @@ export class BaseSysUserService extends BaseService {
|
|||||||
id: Equal(userId),
|
id: Equal(userId),
|
||||||
});
|
});
|
||||||
delete info?.password;
|
delete info?.password;
|
||||||
return info;
|
|
||||||
|
const isSuper =
|
||||||
|
isSuperAdminContext(this.ctx) ||
|
||||||
|
info?.username === 'admin' ||
|
||||||
|
this.ctx?.admin?.username === 'admin';
|
||||||
|
const tenantId = (this.ctx?.admin?.tenantId ?? (info as any)?.tenantId) || null;
|
||||||
|
const role = isSuper ? 'super_admin' : (tenantId ? 'tenant_owner' : 'member');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...info,
|
||||||
|
isSuper,
|
||||||
|
tenantId,
|
||||||
|
role,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -69,6 +69,56 @@ describe('OmniCard AI 名片大厅 (useHall.uts)', () => {
|
|||||||
).toBe('/pages/portal/index?id=202');
|
).toBe('/pages/portal/index?id=202');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('现场分段走 floorPage,加入活动码后刷新同场名片', async () => {
|
||||||
|
const { createHallState, applyHallTab, refreshHall, submitHallEventJoin } =
|
||||||
|
loadHall();
|
||||||
|
const state = createHallState();
|
||||||
|
applyHallTab(state, 'event');
|
||||||
|
let usedFloor = false;
|
||||||
|
let joined = '';
|
||||||
|
await refreshHall(state, {
|
||||||
|
fetchHallContactPage: async () => ({
|
||||||
|
list: [],
|
||||||
|
pagination: { page: 1, size: 20, total: 0 },
|
||||||
|
}),
|
||||||
|
fetchEventFloorPage: async () => {
|
||||||
|
usedFloor = true;
|
||||||
|
return {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
ownerUserId: 11,
|
||||||
|
peerCardId: 103,
|
||||||
|
source: 'event',
|
||||||
|
peerName: 'Chen Wei',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pagination: { page: 1, size: 20, total: 1 },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(usedFloor).toBe(true);
|
||||||
|
expect(state.list[0].peerName).toBe('Chen Wei');
|
||||||
|
|
||||||
|
state.eventCode = 'ces26a';
|
||||||
|
const ok = await submitHallEventJoin(state, {
|
||||||
|
fetchHallContactPage: async () => ({
|
||||||
|
list: [],
|
||||||
|
pagination: { page: 1, size: 20, total: 0 },
|
||||||
|
}),
|
||||||
|
fetchEventFloorPage: async () => ({
|
||||||
|
list: [],
|
||||||
|
pagination: { page: 1, size: 20, total: 0 },
|
||||||
|
}),
|
||||||
|
joinHallEvent: async (code: string) => {
|
||||||
|
joined = code;
|
||||||
|
return { event: { code }, member: { cardId: 101 } };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(ok).toBe(true);
|
||||||
|
expect(joined).toBe('ces26a');
|
||||||
|
expect(state.eventCode).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
it('团队分段走 teamWall 接口', async () => {
|
it('团队分段走 teamWall 接口', async () => {
|
||||||
const { createHallState, applyHallTab, refreshHall } = loadHall();
|
const { createHallState, applyHallTab, refreshHall } = loadHall();
|
||||||
const state = createHallState();
|
const state = createHallState();
|
||||||
@@ -129,6 +179,7 @@ describe('OmniCard AI 名片大厅页契约 (index.uvue)', () => {
|
|||||||
expect(pageSource.includes('<list-view')).toBe(true);
|
expect(pageSource.includes('<list-view')).toBe(true);
|
||||||
expect(pageSource.includes('exchangeCode')).toBe(true);
|
expect(pageSource.includes('exchangeCode')).toBe(true);
|
||||||
expect(pageSource.includes('tabTeam') || logicSource.includes('tabTeam')).toBe(true);
|
expect(pageSource.includes('tabTeam') || logicSource.includes('tabTeam')).toBe(true);
|
||||||
|
expect(pageSource.includes('tabEvent') || logicSource.includes('tabEvent')).toBe(true);
|
||||||
expect(pageSource.includes('var(--window-bottom')).toBe(true);
|
expect(pageSource.includes('var(--window-bottom')).toBe(true);
|
||||||
expect(logicSource.includes('/admin/card/contact/hallPage') || logicSource.includes('fetchHallContactPage')).toBe(true);
|
expect(logicSource.includes('/admin/card/contact/hallPage') || logicSource.includes('fetchHallContactPage')).toBe(true);
|
||||||
expect(pageSource.includes('#0B5CFF')).toBe(true);
|
expect(pageSource.includes('#0B5CFF')).toBe(true);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ type LoadLeadsResult = {
|
|||||||
|
|
||||||
type UseLeadsModule = {
|
type UseLeadsModule = {
|
||||||
parseLeadStatus: (raw: string | null | undefined) => LeadStatus | null;
|
parseLeadStatus: (raw: string | null | undefined) => LeadStatus | null;
|
||||||
|
formatHotUnread: (unread: number) => string;
|
||||||
loadLeads: (
|
loadLeads: (
|
||||||
query: LoadLeadsQuery,
|
query: LoadLeadsQuery,
|
||||||
api?: LoadLeadsApi
|
api?: LoadLeadsApi
|
||||||
@@ -221,6 +222,12 @@ describe('OmniCard AI 名片主移动潜客列表 (useLeads.uts)', () => {
|
|||||||
expect(sent.keyWord).toBeUndefined();
|
expect(sent.keyWord).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('formatHotUnread 有未读才显示红点文案', () => {
|
||||||
|
const { formatHotUnread } = loadUseLeads();
|
||||||
|
expect(formatHotUnread(0)).toBe('');
|
||||||
|
expect(formatHotUnread(2)).toContain('2');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('OmniCard AI 名片主潜客列表页契约 (leads.uvue)', () => {
|
describe('OmniCard AI 名片主潜客列表页契约 (leads.uvue)', () => {
|
||||||
@@ -253,6 +260,7 @@ describe('OmniCard AI 名片主潜客列表页契约 (leads.uvue)', () => {
|
|||||||
expect(pageSource.includes('#E3E7ED')).toBe(true);
|
expect(pageSource.includes('#E3E7ED')).toBe(true);
|
||||||
expect(pageSource.includes('primary-cta-btn')).toBe(true);
|
expect(pageSource.includes('primary-cta-btn')).toBe(true);
|
||||||
expect(pageSource.includes('status-chip')).toBe(true);
|
expect(pageSource.includes('status-chip')).toBe(true);
|
||||||
|
expect(combined.includes('hotUnread') || combined.includes('hotInbox')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('无 emoji、无 any,模板无硬编码中文主文案,长列表不用 view v-for', () => {
|
it('无 emoji、无 any,模板无硬编码中文主文案,长列表不用 view v-for', () => {
|
||||||
|
|||||||
@@ -334,6 +334,33 @@ describe('OmniCard AI 展会团队战报 (useOverview.uts)', () => {
|
|||||||
expect(result.empty).toBe(false);
|
expect(result.empty).toBe(false);
|
||||||
expect(result.errorMessage).toBe('lead page unavailable');
|
expect(result.errorMessage).toBe('lead page unavailable');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('2.4 注入 liveRank 时写入现场排行', async () => {
|
||||||
|
const { loadOverview } = loadUseOverview();
|
||||||
|
const result = await loadOverview(
|
||||||
|
{ page: 1, size: 20 },
|
||||||
|
{
|
||||||
|
fetchOwnerLeadPage: async () => buildPage([]),
|
||||||
|
fetchOwnerRadarPage: async () => buildPage([]),
|
||||||
|
fetchCurrentEvent: async () => ({ code: 'CES26A' }),
|
||||||
|
fetchTeamLiveRank: async (eventId?: string) => {
|
||||||
|
expect(eventId).toBe('CES26A');
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
memberId: 11,
|
||||||
|
memberName: 'Alex Rivera',
|
||||||
|
leads: 3,
|
||||||
|
touches: 12,
|
||||||
|
score: 42,
|
||||||
|
rank: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(result.ranks).toHaveLength(1);
|
||||||
|
expect(result.ranks[0].memberName).toBe('Alex Rivera');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('3. refreshOverview 触觉震动与状态刷新', () => {
|
describe('3. refreshOverview 触觉震动与状态刷新', () => {
|
||||||
@@ -374,6 +401,9 @@ describe('OmniCard AI 展会团队战报页契约 (overview.uvue)', () => {
|
|||||||
expect(combined.includes('loadOverview')).toBe(true);
|
expect(combined.includes('loadOverview')).toBe(true);
|
||||||
expect(combined.includes('fetchOwnerLeadPage')).toBe(true);
|
expect(combined.includes('fetchOwnerLeadPage')).toBe(true);
|
||||||
expect(combined.includes('fetchOwnerRadarPage')).toBe(true);
|
expect(combined.includes('fetchOwnerRadarPage')).toBe(true);
|
||||||
|
expect(combined.includes('fetchTeamLiveRank') || combined.includes('live-rank')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
expect(logicSource.includes('Promise.all')).toBe(true);
|
expect(logicSource.includes('Promise.all')).toBe(true);
|
||||||
expect(pageSource.includes('getOmniText')).toBe(true);
|
expect(pageSource.includes('getOmniText')).toBe(true);
|
||||||
expect(combined.includes('crm.leads')).toBe(true);
|
expect(combined.includes('crm.leads')).toBe(true);
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
"path": "pages/common/login",
|
"path": "pages/common/login",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "登录",
|
"navigationBarTitleText": "登录",
|
||||||
"backgroundColorContent": "#F7F8FA"
|
"navigationStyle": "custom",
|
||||||
|
"backgroundColorContent": "#FFFFFF"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,327 +1,85 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="login-viewport" :class="{ 'is-rtl': isRtlMode }" :style="pageTheme">
|
<view class="login-container">
|
||||||
<!-- 顶部状态与功能栏 (返回 + 语言选择) -->
|
<view class="login-content">
|
||||||
<view class="top-nav-bar">
|
<!-- 1. 极简品牌 Header -->
|
||||||
<view class="nav-back-btn" role="button" aria-label="Back" @click="handleGoBack">
|
<view class="header-section">
|
||||||
<text class="nav-back-arrow">←</text>
|
<image class="brand-logo" src="/static/logo.svg" mode="aspectFit" />
|
||||||
|
<text class="brand-title">登录 OmniCard</text>
|
||||||
|
<text class="brand-desc">全球智能数字名片 & 移动微官网</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="nav-locale-wrap">
|
|
||||||
<i18n-selector :model-value="currentLang" @change="handleLangChange" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 主体内容滚动区 (消除多层内部滚动条) -->
|
<!-- 2. 极简核心表单 (账号密码直接呈现,前后端真实对齐) -->
|
||||||
<view class="login-scroll">
|
<view class="form-section">
|
||||||
<view class="login-wrapper">
|
<view class="input-wrap">
|
||||||
<!-- 瑞士国际主义极简卡片容器 (宽屏限宽 440px 居中,移动端 100% 弹性无缝) -->
|
<input
|
||||||
<view class="login-card">
|
class="input-item"
|
||||||
<!-- 1. 品牌感知区 (Brand Hero) -->
|
type="text"
|
||||||
<view class="brand-hero">
|
v-model="state.username"
|
||||||
<image class="brand-logo" src="../../static/logo.svg" mode="aspectFit" />
|
placeholder="用户名或手机号"
|
||||||
<text class="brand-name">OmniCard AI</text>
|
|
||||||
<view class="brand-tag-capsule">
|
|
||||||
<text class="brand-tag-text">{{ t('brandTag') }}</text>
|
|
||||||
</view>
|
|
||||||
<text class="brand-subtitle">{{ t('subtitle') }}</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 2. 极简分段控制器 (Segmented Control: 账号密码 / 手机号 / 邮箱) -->
|
|
||||||
<view class="segmented-box">
|
|
||||||
<view
|
|
||||||
class="segment-item"
|
|
||||||
:class="{ 'segment-active': state.tab === 'password' }"
|
|
||||||
@click="handleSwitchTab('password')"
|
|
||||||
>
|
|
||||||
<text class="segment-text">{{ t('tabPassword') }}</text>
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
class="segment-item"
|
|
||||||
:class="{ 'segment-active': state.tab === 'phone' }"
|
|
||||||
@click="handleSwitchTab('phone')"
|
|
||||||
>
|
|
||||||
<text class="segment-text">{{ t('tabPhone') }}</text>
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
class="segment-item"
|
|
||||||
:class="{ 'segment-active': state.tab === 'email' }"
|
|
||||||
@click="handleSwitchTab('email')"
|
|
||||||
>
|
|
||||||
<text class="segment-text">{{ t('tabEmail') }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 3. 表单主体 -->
|
|
||||||
<!-- 3.1 账号密码真实登录面板 (便于测试与前后端对齐) -->
|
|
||||||
<view v-if="state.tab === 'password'" class="form-section">
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">{{ t('usernamePlaceholder') }}</text>
|
|
||||||
<input
|
|
||||||
class="input-control text-input"
|
|
||||||
type="text"
|
|
||||||
v-model="state.username"
|
|
||||||
:placeholder="t('usernamePlaceholder')"
|
|
||||||
:disabled="state.submitting"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">{{ t('passwordPlaceholder') }}</text>
|
|
||||||
<input
|
|
||||||
class="input-control text-input"
|
|
||||||
type="password"
|
|
||||||
v-model="state.password"
|
|
||||||
:placeholder="t('passwordPlaceholder')"
|
|
||||||
:disabled="state.submitting"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
<view class="test-account-badge" @click="handleAutofillTest">
|
|
||||||
<text class="test-account-text">{{ t('testAccountTip') }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 3.2 国际手机号验证码面板 -->
|
|
||||||
<view v-else-if="state.tab === 'phone'" class="form-section">
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">{{ t('tabPhone') }}</text>
|
|
||||||
<view class="phone-group">
|
|
||||||
<view class="country-pill" @click="handleSwitchCountry">
|
|
||||||
<text class="country-text">{{ state.countryCode }}</text>
|
|
||||||
<text class="country-arrow">▾</text>
|
|
||||||
</view>
|
|
||||||
<input
|
|
||||||
class="input-control phone-input"
|
|
||||||
type="tel"
|
|
||||||
v-model="state.phone"
|
|
||||||
:placeholder="t('phonePlaceholder')"
|
|
||||||
:disabled="state.submitting"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">{{ t('smsCode') }}</text>
|
|
||||||
<view class="code-group">
|
|
||||||
<input
|
|
||||||
class="input-control code-input"
|
|
||||||
type="number"
|
|
||||||
v-model="state.smsCode"
|
|
||||||
:placeholder="t('smsCode')"
|
|
||||||
:disabled="state.submitting"
|
|
||||||
/>
|
|
||||||
<view
|
|
||||||
class="inline-code-btn"
|
|
||||||
:class="{ 'inline-code-disabled': state.countdown > 0 || state.submitting }"
|
|
||||||
@click="handleSendCode"
|
|
||||||
>
|
|
||||||
<text class="inline-code-text">{{ codeButtonText }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 3.3 邮箱 MagicLink 面板 -->
|
|
||||||
<view v-else class="form-section">
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">{{ t('tabEmail') }}</text>
|
|
||||||
<input
|
|
||||||
class="input-control text-input"
|
|
||||||
type="text"
|
|
||||||
v-model="state.email"
|
|
||||||
:placeholder="t('emailPlaceholder')"
|
|
||||||
:disabled="state.submitting"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 错误提示 -->
|
|
||||||
<view v-if="state.errorMessage" class="error-banner">
|
|
||||||
<text class="error-banner-text">{{ state.errorMessage }}</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 4. 单一主 CTA 按钮 (严格符合 AGENTS.md 规范) -->
|
|
||||||
<button
|
|
||||||
class="primary-cta-btn"
|
|
||||||
:class="{ 'primary-cta-disabled': state.submitting }"
|
|
||||||
role="button"
|
|
||||||
aria-label="登录"
|
|
||||||
:disabled="state.submitting"
|
:disabled="state.submitting"
|
||||||
@click="handlePrimarySubmit"
|
/>
|
||||||
>
|
|
||||||
<text class="primary-cta-text">{{ primaryButtonText }}</text>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- 5. 快捷社交登录 (极简中性分割) -->
|
|
||||||
<view class="social-divider">
|
|
||||||
<view class="divider-line"></view>
|
|
||||||
<text class="divider-text">{{ t('orContinueWith') }}</text>
|
|
||||||
<view class="divider-line"></view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="social-row">
|
|
||||||
<view class="social-capsule" role="button" @click="handleSocial('google')">
|
|
||||||
<text class="social-icon">G</text>
|
|
||||||
<text class="social-name">Google</text>
|
|
||||||
</view>
|
|
||||||
<view class="social-capsule" role="button" @click="handleSocial('apple')">
|
|
||||||
<text class="social-icon-apple"></text>
|
|
||||||
<text class="social-name">Apple</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 6. 法律合规与版权声明 -->
|
|
||||||
<view class="legal-footer">
|
|
||||||
<text class="legal-text">{{ t('termsNotice') }}</text>
|
|
||||||
<view class="legal-links-row">
|
|
||||||
<text class="legal-link" @click="handleOpenTerms">{{ t('termsOfService') }}</text>
|
|
||||||
<text class="legal-and"> {{ t('and') }} </text>
|
|
||||||
<text class="legal-link" @click="handleOpenPrivacy">{{ t('privacyPolicy') }}</text>
|
|
||||||
</view>
|
|
||||||
<text class="copyright-line">© OmniCard AI. All rights reserved.</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="input-wrap">
|
||||||
|
<input
|
||||||
|
class="input-item"
|
||||||
|
type="password"
|
||||||
|
v-model="state.password"
|
||||||
|
placeholder="登录密码"
|
||||||
|
:disabled="state.submitting"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 极轻量测试辅助链接 -->
|
||||||
|
<view class="test-hint-wrap" role="button" @click="handleAutofillTest">
|
||||||
|
<text class="test-hint-text">测试账号: admin / 123456 (点击一键填入)</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 错误提示 -->
|
||||||
|
<view v-if="state.errorMessage !== ''" class="error-wrap">
|
||||||
|
<text class="error-text">{{ state.errorMessage }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 单一主 CTA 按钮 -->
|
||||||
|
<button
|
||||||
|
class="primary-btn"
|
||||||
|
:class="{ 'primary-btn-loading': state.submitting }"
|
||||||
|
:disabled="state.submitting"
|
||||||
|
role="button"
|
||||||
|
aria-label="登录"
|
||||||
|
@click="handlePrimarySubmit"
|
||||||
|
>
|
||||||
|
<text class="primary-btn-text">{{ state.submitting ? '正在登录...' : '登录' }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 3. 极轻底部条款声明 -->
|
||||||
|
<view class="footer-section">
|
||||||
|
<text class="footer-text">登录即代表同意</text>
|
||||||
|
<text class="footer-link" @click="handleOpenTerms">服务条款</text>
|
||||||
|
<text class="footer-text">与</text>
|
||||||
|
<text class="footer-link" @click="handleOpenPrivacy">隐私政策</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { utsLightColors, utsRadius, utsSpacing } from '../../common/tokens.uts';
|
|
||||||
import { getCurrentLocale, isRtlLocale, OmniLocale, setAppLocale } from '../../common/locale.uts';
|
|
||||||
import {
|
import {
|
||||||
createLoginState,
|
createLoginState,
|
||||||
switchLoginTab,
|
|
||||||
submitPasswordLogin,
|
submitPasswordLogin,
|
||||||
submitPhoneLogin,
|
|
||||||
sendMagicLink,
|
|
||||||
sendSmsCode,
|
|
||||||
tickCountdown,
|
|
||||||
tapSocialLogin,
|
|
||||||
resolveLoginText,
|
|
||||||
fillTestAccount,
|
fillTestAccount,
|
||||||
LoginCopyKey,
|
resolveLoginText,
|
||||||
LoginTab,
|
|
||||||
SocialProvider,
|
|
||||||
} from './useLogin.uts';
|
} from './useLogin.uts';
|
||||||
|
|
||||||
const pageTheme: Record<string, string> = {
|
|
||||||
'--oc-primary': utsLightColors.primary,
|
|
||||||
'--oc-primary-soft': utsLightColors.primarySoft,
|
|
||||||
'--oc-bg': utsLightColors.bg,
|
|
||||||
'--oc-surface': utsLightColors.surface,
|
|
||||||
'--oc-border': utsLightColors.border,
|
|
||||||
'--oc-text': utsLightColors.text,
|
|
||||||
'--oc-text-2': utsLightColors.text2,
|
|
||||||
'--oc-radius-btn': utsRadius.button,
|
|
||||||
'--oc-radius-card': utsRadius.card,
|
|
||||||
'--oc-space-8': utsSpacing.space8,
|
|
||||||
'--oc-space-12': utsSpacing.space12,
|
|
||||||
'--oc-space-16': utsSpacing.space16,
|
|
||||||
'--oc-space-24': utsSpacing.space24,
|
|
||||||
'--oc-space-32': utsSpacing.space32,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive(createLoginState());
|
const state = reactive(createLoginState());
|
||||||
const currentLang = ref<string>(getCurrentLocale());
|
|
||||||
|
|
||||||
const isRtlMode = computed((): boolean => {
|
|
||||||
return isRtlLocale(currentLang.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
function t(key: LoginCopyKey | string): string {
|
|
||||||
return resolveLoginText(key, currentLang.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeButtonText = computed((): string => {
|
|
||||||
if (state.countdown > 0) {
|
|
||||||
return `${state.countdown}${t('countdownSuffix')}`;
|
|
||||||
}
|
|
||||||
return t('sendCode');
|
|
||||||
});
|
|
||||||
|
|
||||||
const primaryButtonText = computed((): string => {
|
|
||||||
if (state.tab === 'password') {
|
|
||||||
return state.submitting ? t('loading') : t('login');
|
|
||||||
}
|
|
||||||
return state.tab === 'phone' ? t('login') : t('magicLink');
|
|
||||||
});
|
|
||||||
|
|
||||||
let countdownTimer: number | null = null;
|
|
||||||
|
|
||||||
function runTimer(): void {
|
|
||||||
if (countdownTimer != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
countdownTimer = setInterval(() => {
|
|
||||||
if (state.countdown > 0) {
|
|
||||||
tickCountdown(state);
|
|
||||||
} else if (countdownTimer != null) {
|
|
||||||
clearInterval(countdownTimer);
|
|
||||||
countdownTimer = null;
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleGoBack(): void {
|
|
||||||
try {
|
|
||||||
uni.navigateBack({
|
|
||||||
fail: () => {
|
|
||||||
uni.reLaunch({
|
|
||||||
url: '/pages/owner/hall/index',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (_e) {
|
|
||||||
uni.reLaunch({
|
|
||||||
url: '/pages/owner/hall/index',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLangChange(locale: OmniLocale): void {
|
|
||||||
currentLang.value = locale;
|
|
||||||
setAppLocale(locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSwitchTab(tab: LoginTab): void {
|
|
||||||
switchLoginTab(state, tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
const commonCountryCodes: string[] = ['+86', '+1', '+44', '+971', '+65', '+81', '+49', '+33'];
|
|
||||||
|
|
||||||
function handleSwitchCountry(): void {
|
|
||||||
uni.showActionSheet({
|
|
||||||
itemList: commonCountryCodes,
|
|
||||||
success: (res) => {
|
|
||||||
const idx = res.tapIndex;
|
|
||||||
if (idx >= 0 && idx < commonCountryCodes.length) {
|
|
||||||
state.countryCode = commonCountryCodes[idx];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSendCode(): void {
|
|
||||||
const sent = sendSmsCode(state);
|
|
||||||
if (sent) {
|
|
||||||
runTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAutofillTest(): void {
|
function handleAutofillTest(): void {
|
||||||
fillTestAccount(state);
|
fillTestAccount(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePrimarySubmit(): void {
|
function handlePrimarySubmit(): void {
|
||||||
if (state.tab === 'password') {
|
submitPasswordLogin(state);
|
||||||
submitPasswordLogin(state);
|
|
||||||
} else if (state.tab === 'phone') {
|
|
||||||
submitPhoneLogin(state);
|
|
||||||
} else {
|
|
||||||
sendMagicLink(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSocial(provider: SocialProvider): void {
|
|
||||||
tapSocialLogin(provider);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenTerms(): void {
|
function handleOpenTerms(): void {
|
||||||
@@ -337,491 +95,164 @@ function handleOpenPrivacy(): void {
|
|||||||
icon: 'none',
|
icon: 'none',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (countdownTimer != null) {
|
|
||||||
clearInterval(countdownTimer);
|
|
||||||
countdownTimer = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 全局视口与页面背景 */
|
.login-container {
|
||||||
.login-viewport {
|
min-height: 100vh;
|
||||||
|
background-color: #FFFFFF;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
|
||||||
min-height: 100vh;
|
|
||||||
background-color: var(--oc-surface, #F7F8FA);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 顶部轻量导航条 (44px 标准触控高) */
|
|
||||||
.top-nav-bar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background-color: transparent;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-back-btn {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 12px;
|
padding: 40px 24px 32px;
|
||||||
background-color: #FFFFFF;
|
|
||||||
border: 1px solid #E3E7ED;
|
|
||||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-back-arrow {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #101828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-locale-wrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 滚动容器与居中外层 */
|
|
||||||
.login-scroll {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px 16px calc(48px + env(safe-area-inset-bottom, 0px)) 16px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 瑞士国际主义极简卡片容器 (宽屏限宽 420px,移动端 100% 自适应) */
|
.login-content {
|
||||||
.login-card {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 420px;
|
max-width: 340px;
|
||||||
background-color: var(--oc-bg, #FFFFFF);
|
|
||||||
border: 1px solid var(--oc-border, #E3E7ED);
|
|
||||||
border-radius: var(--oc-radius-card, 16px);
|
|
||||||
padding: 24px 22px;
|
|
||||||
box-shadow: 0 1px 3px rgba(16, 24, 40, 0.04);
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 1. 品牌感知区 (Brand Hero) */
|
/* 1. Header */
|
||||||
.brand-hero {
|
.header-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
margin-bottom: 32px;
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-logo {
|
.brand-logo {
|
||||||
width: 44px;
|
width: 36px;
|
||||||
height: 44px;
|
height: 36px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-name {
|
.brand-title {
|
||||||
font-size: 20px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #101828;
|
color: #101828;
|
||||||
letter-spacing: -0.3px;
|
line-height: 1.3;
|
||||||
line-height: 1.25;
|
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-tag-capsule {
|
.brand-desc {
|
||||||
display: inline-flex;
|
|
||||||
padding: 2px 8px;
|
|
||||||
background-color: #EAF1FF;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-tag-text {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0B5CFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-subtitle {
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #667085;
|
color: #667085;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. 极简分段控制器 (Segmented Control) */
|
/* 2. Form */
|
||||||
.segmented-box {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
background-color: #F2F4F7;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 3px;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-item {
|
|
||||||
flex: 1;
|
|
||||||
height: 38px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: transparent;
|
|
||||||
transition: background-color 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-active {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-text {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #667085;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-active .segment-text {
|
|
||||||
color: #101828;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 3. 表单结构 */
|
|
||||||
.form-section {
|
.form-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field {
|
.input-wrap {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-label {
|
.input-item {
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #101828;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-control,
|
|
||||||
.field-input {
|
|
||||||
height: 44px;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #101828;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-input {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 14px;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border: 1px solid #E3E7ED;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-input:focus {
|
|
||||||
border-color: #0B5CFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机号复合输入组 */
|
|
||||||
.phone-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid #E3E7ED;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-pill {
|
|
||||||
min-width: 68px;
|
|
||||||
height: 44px;
|
height: 44px;
|
||||||
padding: 0 10px;
|
padding: 0 14px;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: #F7F8FA;
|
|
||||||
border-right: 1px solid #E3E7ED;
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-text {
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
|
||||||
color: #101828;
|
color: #101828;
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-arrow {
|
|
||||||
font-size: 10px;
|
|
||||||
color: #667085;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phone-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 验证码行内一体化组合 */
|
|
||||||
.code-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid #E3E7ED;
|
|
||||||
border-radius: 12px;
|
|
||||||
background-color: #FFFFFF;
|
background-color: #FFFFFF;
|
||||||
overflow: hidden;
|
border: 1px solid #E3E7ED;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-input {
|
.input-item:focus {
|
||||||
flex: 1;
|
|
||||||
padding: 0 14px;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-code-btn {
|
|
||||||
padding: 0 14px;
|
|
||||||
height: 44px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: transparent;
|
|
||||||
border-left: 1px solid #E3E7ED;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-code-text {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0B5CFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-code-disabled .inline-code-text {
|
|
||||||
color: #98A2B3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 测试账号一键填充辅助胶囊 */
|
|
||||||
.test-account-badge {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background-color: #F7F8FA;
|
|
||||||
border: 1px dashed #D0D5DD;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-account-badge:active {
|
|
||||||
background-color: #EAF1FF;
|
|
||||||
border-color: #0B5CFF;
|
border-color: #0B5CFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-account-text {
|
.test-hint-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-hint-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #0B5CFF;
|
color: #0B5CFF;
|
||||||
font-weight: 500;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 错误提示 */
|
.error-wrap {
|
||||||
.error-banner {
|
margin-bottom: 12px;
|
||||||
padding: 8px 12px;
|
|
||||||
background-color: #FEF3F2;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-banner-text {
|
.error-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #F04438;
|
color: #F04438;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 4. 单一主 CTA 按钮 */
|
/* 3. Button */
|
||||||
.primary-cta-btn {
|
.primary-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
line-height: 44px;
|
line-height: 44px;
|
||||||
min-height: 44px;
|
|
||||||
background-color: #0B5CFF;
|
background-color: #0B5CFF;
|
||||||
color: #FFFFFF;
|
border-radius: 10px;
|
||||||
font-size: 15px;
|
border: none;
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: none;
|
|
||||||
margin-top: 6px;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 24px;
|
||||||
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-cta-btn::after {
|
.primary-btn::after {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-cta-btn:active {
|
.primary-btn:active {
|
||||||
opacity: 0.88;
|
opacity: 0.88;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-cta-disabled {
|
.primary-btn-loading {
|
||||||
background-color: #E3E7ED;
|
opacity: 0.7;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-cta-text {
|
.primary-btn-text {
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-cta-disabled .primary-cta-text {
|
/* 4. Footer */
|
||||||
color: #98A2B3;
|
.footer-section {
|
||||||
}
|
|
||||||
|
|
||||||
/* 5. 社交快捷登录通道 */
|
|
||||||
.social-divider {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider-line {
|
|
||||||
flex: 1;
|
|
||||||
height: 1px;
|
|
||||||
background-color: #E3E7ED;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider-text {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #98A2B3;
|
|
||||||
padding: 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-capsule {
|
|
||||||
flex: 1;
|
|
||||||
height: 44px;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border: 1px solid #E3E7ED;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: background-color 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-capsule:active {
|
|
||||||
background-color: #F7F8FA;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-icon {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #4285F4;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-icon-apple {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #101828;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-name {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #101828;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 6. 法律声明与版权 */
|
|
||||||
.legal-footer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-text {
|
.footer-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #667085;
|
color: #98A2B3;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-links-row {
|
.footer-link {
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 2px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-link {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0B5CFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legal-and {
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #667085;
|
color: #667085;
|
||||||
}
|
text-decoration: underline;
|
||||||
|
margin: 0 3px;
|
||||||
.copyright-line {
|
cursor: pointer;
|
||||||
font-size: 11px;
|
|
||||||
color: #98A2B3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* RTL 适配 */
|
|
||||||
.is-rtl .country-pill {
|
|
||||||
border-right: none;
|
|
||||||
border-left: 1px solid #E3E7ED;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-rtl .inline-code-btn {
|
|
||||||
border-left: none;
|
|
||||||
border-right: 1px solid #E3E7ED;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-rtl .social-icon,
|
|
||||||
.is-rtl .social-icon-apple {
|
|
||||||
margin-right: 0;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -36,6 +36,21 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="state.ranks.length > 0" class="recent-section">
|
||||||
|
<text class="section-title">{{ t('rankTitle') }}</text>
|
||||||
|
<list-view class="recent-list">
|
||||||
|
<list-item v-for="item in state.ranks" :key="item.memberId">
|
||||||
|
<view class="lead-row">
|
||||||
|
<view class="lead-main">
|
||||||
|
<text class="lead-name">{{ item.memberName }}</text>
|
||||||
|
<text class="lead-company">{{ formatRankMeta(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="lead-status">{{ item.rank }}</text>
|
||||||
|
</view>
|
||||||
|
</list-item>
|
||||||
|
</list-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="recent-section">
|
<view class="recent-section">
|
||||||
<text class="section-title">{{ recentLabel }}</text>
|
<text class="section-title">{{ recentLabel }}</text>
|
||||||
<view v-if="state.leads.length === 0" class="overview-feedback">
|
<view v-if="state.leads.length === 0" class="overview-feedback">
|
||||||
@@ -128,6 +143,10 @@ function t(key: OverviewCopyKey): string {
|
|||||||
return resolveOverviewText(key);
|
return resolveOverviewText(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRankMeta(item: { leads: number; touches: number; score: number }): string {
|
||||||
|
return `${item.leads} / ${item.touches} / ${item.score}`;
|
||||||
|
}
|
||||||
|
|
||||||
function handleRefresh(): void {
|
function handleRefresh(): void {
|
||||||
refreshOverview(state);
|
refreshOverview(state);
|
||||||
}
|
}
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户