补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
1422 行
41 KiB
Plaintext
1422 行
41 KiB
Plaintext
<template>
|
||
<view class="card-edit-page" :class="pageRtlClass" :style="pageTheme">
|
||
<!-- Empty / Missing ID State -->
|
||
<view v-if="state.missingId || state.empty" class="card-empty">
|
||
<text class="card-empty-text">{{ emptyMessage }}</text>
|
||
<button class="primary-cta-btn" @click="handleBack">
|
||
<text class="primary-cta-btn-text">{{ t('back') }}</text>
|
||
</button>
|
||
</view>
|
||
|
||
<!-- Skeleton Loading State -->
|
||
<view v-else-if="state.loading" class="card-skeleton">
|
||
<view class="skeleton-header">
|
||
<view class="skeleton-line skeleton-title-line"></view>
|
||
<view class="skeleton-line skeleton-sub-line"></view>
|
||
</view>
|
||
<view class="skeleton-card skeleton-card-lg"></view>
|
||
<view class="skeleton-card skeleton-card-md"></view>
|
||
<view class="skeleton-card skeleton-card-sm"></view>
|
||
</view>
|
||
|
||
<!-- Main Edit Content -->
|
||
<view v-else class="card-body">
|
||
<scroll-view class="card-scroll" :scroll-y="true">
|
||
<view class="card-content">
|
||
<!-- Page Header & Hint -->
|
||
<view class="card-header-bar">
|
||
<view class="card-title-group">
|
||
<text class="card-heading">{{ t('heading') }}</text>
|
||
<text class="card-subtitle">{{ t('quickEditSub') }}</text>
|
||
</view>
|
||
<view class="builder-hint-pill">
|
||
<text class="builder-hint-pill-text">{{ t('builderHint') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Quick Links: Cases & Articles -->
|
||
<view class="quick-entry-grid">
|
||
<view class="quick-entry-card" @click="handleGoCases">
|
||
<view class="quick-entry-top">
|
||
<text class="quick-entry-badge">PORTFOLIO</text>
|
||
<text class="quick-entry-arrow">↗</text>
|
||
</view>
|
||
<text class="quick-entry-title">{{ casesEntryTitle }}</text>
|
||
<text class="quick-entry-sub">{{ casesEntrySub }}</text>
|
||
</view>
|
||
|
||
<view class="quick-entry-card" @click="handleGoArticles">
|
||
<view class="quick-entry-top">
|
||
<text class="quick-entry-badge">INSIGHTS</text>
|
||
<text class="quick-entry-arrow">↗</text>
|
||
</view>
|
||
<text class="quick-entry-title">{{ articlesEntryTitle }}</text>
|
||
<text class="quick-entry-sub">{{ articlesEntrySub }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modules-stack">
|
||
<!-- Module 1: Basic Profile -->
|
||
<view class="module-card" :style="moduleCardStyle('basic')">
|
||
<view class="module-card-header">
|
||
<view class="module-title-wrap">
|
||
<view class="module-indicator"></view>
|
||
<text class="module-title">{{ t('basicSection') }}</text>
|
||
</view>
|
||
<view class="module-order-actions">
|
||
<button
|
||
class="module-order-btn"
|
||
:disabled="state.saving || !canMoveModule('basic', -1)"
|
||
:aria-label="t('moveUp')"
|
||
@click="handleMoveModule('basic', -1)"
|
||
>
|
||
<text class="module-order-btn-text">{{ t('moveUp') }}</text>
|
||
</button>
|
||
<button
|
||
class="module-order-btn"
|
||
:disabled="state.saving || !canMoveModule('basic', 1)"
|
||
:aria-label="t('moveDown')"
|
||
@click="handleMoveModule('basic', 1)"
|
||
>
|
||
<text class="module-order-btn-text">{{ t('moveDown') }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Avatar Uploader Stage -->
|
||
<view class="avatar-stage-box">
|
||
<view class="avatar-stage">
|
||
<view class="avatar-wrap">
|
||
<image
|
||
v-if="state.avatarUrl !== ''"
|
||
class="avatar-img"
|
||
:src="state.avatarUrl"
|
||
mode="aspectFill"
|
||
@click="handleAvatarPreview"
|
||
/>
|
||
<view
|
||
v-else
|
||
class="avatar-placeholder"
|
||
@click="handleAvatarChoose"
|
||
>
|
||
<text class="avatar-placeholder-text">{{ t('avatarPlaceholder') }}</text>
|
||
</view>
|
||
</view>
|
||
<view
|
||
v-if="state.avatarUrl !== ''"
|
||
class="avatar-delete-hit"
|
||
:aria-label="deleteAriaLabel"
|
||
@click="handleAvatarDelete"
|
||
>
|
||
<view class="avatar-delete-badge">
|
||
<text class="avatar-delete-icon">×</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<button
|
||
class="avatar-action-btn"
|
||
:disabled="state.saving"
|
||
:aria-label="avatarActionText"
|
||
@click="handleAvatarChoose"
|
||
>
|
||
<text class="avatar-action-btn-text">{{ avatarActionText }}</text>
|
||
</button>
|
||
</view>
|
||
|
||
<!-- Basic Fields -->
|
||
<view class="field">
|
||
<text class="field-label">{{ t('name') }}</text>
|
||
<input
|
||
class="field-input"
|
||
type="text"
|
||
v-model="state.name"
|
||
:placeholder="t('name')"
|
||
:disabled="state.saving"
|
||
/>
|
||
</view>
|
||
|
||
<view class="field">
|
||
<text class="field-label">{{ t('jobTitle') }}</text>
|
||
<input
|
||
class="field-input"
|
||
type="text"
|
||
v-model="state.title"
|
||
:placeholder="t('jobTitle')"
|
||
:disabled="state.saving"
|
||
/>
|
||
</view>
|
||
|
||
<view class="field">
|
||
<text class="field-label">{{ t('company') }}</text>
|
||
<input
|
||
class="field-input"
|
||
:class="{ 'field-input-locked': companyLocked }"
|
||
type="text"
|
||
v-model="state.company"
|
||
:placeholder="t('company')"
|
||
:disabled="state.saving || companyLocked"
|
||
/>
|
||
<text v-if="companyLocked" class="field-lock-hint">{{ t('lockedHint') }}</text>
|
||
</view>
|
||
|
||
<view class="field">
|
||
<text class="field-label">{{ t('slogan') }}</text>
|
||
<input
|
||
class="field-input"
|
||
:class="{ 'field-input-locked': sloganLocked }"
|
||
type="text"
|
||
v-model="state.slogan"
|
||
:placeholder="t('slogan')"
|
||
:disabled="state.saving || sloganLocked"
|
||
/>
|
||
<text v-if="sloganLocked" class="field-lock-hint">{{ t('lockedHint') }}</text>
|
||
</view>
|
||
|
||
<view class="field field-last">
|
||
<text class="field-label">{{ t('timezone') }}</text>
|
||
<input
|
||
class="field-input"
|
||
type="text"
|
||
v-model="state.timezone"
|
||
:placeholder="t('timezone')"
|
||
:disabled="state.saving"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Module 2: Social Matrix -->
|
||
<view class="module-card" :style="moduleCardStyle('social')">
|
||
<view class="module-card-header">
|
||
<view class="module-title-wrap">
|
||
<view class="module-indicator"></view>
|
||
<text class="module-title">{{ t('socialSection') }}</text>
|
||
</view>
|
||
<view class="module-order-actions">
|
||
<button
|
||
class="module-order-btn"
|
||
:disabled="state.saving || !canMoveModule('social', -1)"
|
||
:aria-label="t('moveUp')"
|
||
@click="handleMoveModule('social', -1)"
|
||
>
|
||
<text class="module-order-btn-text">{{ t('moveUp') }}</text>
|
||
</button>
|
||
<button
|
||
class="module-order-btn"
|
||
:disabled="state.saving || !canMoveModule('social', 1)"
|
||
:aria-label="t('moveDown')"
|
||
@click="handleMoveModule('social', 1)"
|
||
>
|
||
<text class="module-order-btn-text">{{ t('moveDown') }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
<text class="module-subtitle module-subtitle-block">{{ t('socialSectionDesc') }}</text>
|
||
|
||
<view class="field">
|
||
<text class="field-label">{{ t('phone') }}</text>
|
||
<input
|
||
class="field-input"
|
||
type="text"
|
||
v-model="state.phone"
|
||
:placeholder="t('phone')"
|
||
:disabled="state.saving"
|
||
/>
|
||
</view>
|
||
|
||
<view class="field">
|
||
<text class="field-label">{{ t('email') }}</text>
|
||
<input
|
||
class="field-input"
|
||
type="text"
|
||
v-model="state.email"
|
||
:placeholder="t('email')"
|
||
:disabled="state.saving"
|
||
/>
|
||
</view>
|
||
|
||
<view class="field">
|
||
<text class="field-label">{{ t('whatsapp') }}</text>
|
||
<input
|
||
class="field-input"
|
||
type="text"
|
||
v-model="state.whatsapp"
|
||
:placeholder="t('whatsapp')"
|
||
:disabled="state.saving"
|
||
/>
|
||
</view>
|
||
|
||
<view class="field field-last">
|
||
<text class="field-label">{{ t('linkedin') }}</text>
|
||
<input
|
||
class="field-input"
|
||
type="text"
|
||
v-model="state.linkedin"
|
||
:placeholder="t('linkedin')"
|
||
:disabled="state.saving"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Module 3: Privacy & Visibility -->
|
||
<view class="module-card" :style="moduleCardStyle('privacy')">
|
||
<view class="module-card-header">
|
||
<view class="module-title-wrap">
|
||
<view class="module-indicator"></view>
|
||
<text class="module-title">{{ t('privacySection') }}</text>
|
||
</view>
|
||
<view class="module-order-actions">
|
||
<button
|
||
class="module-order-btn"
|
||
:disabled="state.saving || !canMoveModule('privacy', -1)"
|
||
:aria-label="t('moveUp')"
|
||
@click="handleMoveModule('privacy', -1)"
|
||
>
|
||
<text class="module-order-btn-text">{{ t('moveUp') }}</text>
|
||
</button>
|
||
<button
|
||
class="module-order-btn"
|
||
:disabled="state.saving || !canMoveModule('privacy', 1)"
|
||
:aria-label="t('moveDown')"
|
||
@click="handleMoveModule('privacy', 1)"
|
||
>
|
||
<text class="module-order-btn-text">{{ t('moveDown') }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
<view class="gdpr-pill gdpr-pill-inline">
|
||
<text class="gdpr-pill-text">{{ t('gdprBadge') }}</text>
|
||
</view>
|
||
|
||
<!-- Visibility Options -->
|
||
<view class="privacy-options-group">
|
||
<!-- Extended Visibility Option -->
|
||
<view
|
||
class="privacy-option-item"
|
||
:class="{ 'privacy-option-active': visibilityMode === 'extended' }"
|
||
@click="handleSelectVisibility('extended')"
|
||
>
|
||
<view class="privacy-option-main">
|
||
<view class="privacy-radio-circle">
|
||
<view v-if="visibilityMode === 'extended'" class="privacy-radio-inner"></view>
|
||
</view>
|
||
<view class="privacy-option-content">
|
||
<text class="privacy-option-title">{{ t('extendedVisibility') }}</text>
|
||
<text class="privacy-option-desc">{{ t('extendedVisibilityDesc') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Temporary Visibility Option -->
|
||
<view
|
||
class="privacy-option-item"
|
||
:class="{ 'privacy-option-active': visibilityMode === 'temporary' }"
|
||
@click="handleSelectVisibility('temporary')"
|
||
>
|
||
<view class="privacy-option-main">
|
||
<view class="privacy-radio-circle">
|
||
<view v-if="visibilityMode === 'temporary'" class="privacy-radio-inner"></view>
|
||
</view>
|
||
<view class="privacy-option-content">
|
||
<text class="privacy-option-title">{{ t('temporaryVisibility') }}</text>
|
||
<text class="privacy-option-desc">{{ t('temporaryVisibilityDesc') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Expanded duration pills when Temporary is active -->
|
||
<view v-if="visibilityMode === 'temporary'" class="privacy-duration-expand">
|
||
<text class="privacy-duration-label">{{ t('visibilityDuration') }}</text>
|
||
<view class="privacy-duration-pills">
|
||
<view
|
||
class="duration-pill"
|
||
:class="{ 'duration-pill-active': temporaryDays === 1 }"
|
||
@click.stop="handleSelectDuration(1)"
|
||
>
|
||
<text class="duration-pill-text">{{ t('duration24h') }}</text>
|
||
</view>
|
||
<view
|
||
class="duration-pill"
|
||
:class="{ 'duration-pill-active': temporaryDays === 7 }"
|
||
@click.stop="handleSelectDuration(7)"
|
||
>
|
||
<text class="duration-pill-text">{{ t('duration7d') }}</text>
|
||
</view>
|
||
<view
|
||
class="duration-pill"
|
||
:class="{ 'duration-pill-active': temporaryDays === 30 }"
|
||
@click.stop="handleSelectDuration(30)"
|
||
>
|
||
<text class="duration-pill-text">{{ t('duration30d') }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="privacy-note-box">
|
||
<text class="privacy-note-text">{{ t('temporaryNote') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Error Alert Banner -->
|
||
<view v-if="state.errorMessage !== ''" class="card-error">
|
||
<text class="card-error-text">{{ state.errorMessage }}</text>
|
||
</view>
|
||
|
||
<view class="card-bottom-spacer"></view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- Single Primary CTA Floating Bar -->
|
||
<view class="card-cta-bar">
|
||
<button
|
||
class="primary-cta-btn"
|
||
:class="{ 'primary-cta-btn-disabled': state.saving }"
|
||
:disabled="state.saving"
|
||
:aria-label="saveLabel"
|
||
@click="handleSave"
|
||
>
|
||
<text class="primary-cta-btn-text">{{ saveLabel }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { getCurrentLocale, getOmniText, isRtl, OmniLocale } from '../../../common/locale.uts';
|
||
import { utsLightColors, utsRadius, utsSpacing, utsTypography } from '../../../common/tokens.uts';
|
||
import {
|
||
applyCardEditOptions,
|
||
createCardEditState,
|
||
goBackFromCardEdit,
|
||
refreshCardEdit,
|
||
resolveCardEditText,
|
||
submitCardEdit,
|
||
saveCard,
|
||
updateOwnerCard,
|
||
fetchOwnerCardInfo,
|
||
buildUpdatePayload,
|
||
isFieldLocked,
|
||
moveModuleInOrder,
|
||
CardEditCopyKey,
|
||
CardEditPageOptions,
|
||
} from './useCardEdit.uts';
|
||
|
||
type VisibilityMode = 'extended' | 'temporary';
|
||
|
||
type CardEditExtendedKey =
|
||
| CardEditCopyKey
|
||
| 'builderHint'
|
||
| 'quickEditSub'
|
||
| 'basicSection'
|
||
| 'socialSection'
|
||
| 'socialSectionDesc'
|
||
| 'privacySection'
|
||
| 'gdprBadge'
|
||
| 'extendedVisibility'
|
||
| 'extendedVisibilityDesc'
|
||
| 'temporaryVisibility'
|
||
| 'temporaryVisibilityDesc'
|
||
| 'visibilityDuration'
|
||
| 'duration24h'
|
||
| 'duration7d'
|
||
| 'duration30d'
|
||
| 'temporaryNote'
|
||
| 'lockedHint';
|
||
|
||
const EXTENDED_COPY: Record<OmniLocale, Record<string, string>> = {
|
||
'zh-CN': {
|
||
builderHint: '更多高级搭建请用后台搭建器',
|
||
quickEditSub: '现场即改即用 · 毫秒级多端同步',
|
||
basicSection: '基本身份档案',
|
||
socialSection: '商务社交矩阵',
|
||
socialSectionDesc: '跨国多渠道联络直达',
|
||
privacySection: '隐私与时效控制',
|
||
gdprBadge: 'GDPR 规范',
|
||
extendedVisibility: '商务常态公开 (Extended)',
|
||
extendedVisibilityDesc: '长期向客商公开名片与联络渠道,保持全渠道畅联',
|
||
temporaryVisibility: '展会限时时效 (Temporary)',
|
||
temporaryVisibilityDesc: '仅在限时会面期有效,逾期自动隐藏直接联络方式',
|
||
visibilityDuration: '有效时效',
|
||
duration24h: '24小时',
|
||
duration7d: '7天 (推荐)',
|
||
duration30d: '30天',
|
||
temporaryNote: '时效结束后将自动限制未授权客商获取敏感信息',
|
||
},
|
||
'en-US': {
|
||
builderHint: 'Use the admin builder for more edits',
|
||
quickEditSub: 'Instant On-Site Updates · Real-Time Sync',
|
||
basicSection: 'Basic Profile',
|
||
socialSection: 'Social Matrix',
|
||
socialSectionDesc: 'Global omni-channel contact access',
|
||
privacySection: 'Privacy & Visibility',
|
||
gdprBadge: 'GDPR Compliance',
|
||
extendedVisibility: 'Extended Visibility',
|
||
extendedVisibilityDesc: 'Keep contact channels continuously accessible for long-term business',
|
||
temporaryVisibility: 'Temporary Visibility',
|
||
temporaryVisibilityDesc: 'Active only during meeting window; auto-restricts direct contacts afterwards',
|
||
visibilityDuration: 'Active Duration',
|
||
duration24h: '24 Hours',
|
||
duration7d: '7 Days (Rec.)',
|
||
duration30d: '30 Days',
|
||
temporaryNote: 'Direct contact channels will be automatically restricted after expiration',
|
||
},
|
||
'ar-SA': {
|
||
builderHint: 'للمزيد من التحرير استخدم منشئ الخلفية',
|
||
quickEditSub: 'تحديث فوري في الموقع · مزامنة بالمللي ثانية',
|
||
basicSection: 'الملف التعريفي الأساسي',
|
||
socialSection: 'مصفوفة التواصل التجاري',
|
||
socialSectionDesc: 'وصول مباشر متعدد القنوات',
|
||
privacySection: 'الخصوصية ومدة الظهور',
|
||
gdprBadge: 'متوافق مع GDPR',
|
||
extendedVisibility: 'ظهور مستمر (Extended)',
|
||
extendedVisibilityDesc: 'إبقاء وسائل الاتصال متاحة باستمرار للأعمال طويلة المدى',
|
||
temporaryVisibility: 'ظهور مؤقت للمعرض (Temporary)',
|
||
temporaryVisibilityDesc: 'نشط فقط أثناء نافذة الاجتماع؛ يتم تقييد الاتصال تلقائيًا بعدها',
|
||
visibilityDuration: 'مدة الصلاحية',
|
||
duration24h: '24 ساعة',
|
||
duration7d: '7 أيام (موصى به)',
|
||
duration30d: '30 يومًا',
|
||
temporaryNote: 'سيتم تقييد وسائل الاتصال المباشرة تلقائيًا بعد انتهاء المدة',
|
||
},
|
||
'es-ES': {
|
||
builderHint: 'Usa el editor del panel para más cambios',
|
||
quickEditSub: 'Actualización instantánea · Sincronización en tiempo real',
|
||
basicSection: 'Perfil básico',
|
||
socialSection: 'Matriz de redes',
|
||
socialSectionDesc: 'Acceso directo multicanal global',
|
||
privacySection: 'Privacidad y visibilidad',
|
||
gdprBadge: 'Cumple GDPR',
|
||
extendedVisibility: 'Visibilidad extendida',
|
||
extendedVisibilityDesc: 'Mantén los canales accesibles continuamente para relaciones comerciales',
|
||
temporaryVisibility: 'Visibilidad temporal',
|
||
temporaryVisibilityDesc: 'Activo solo durante el evento; se restringe el contacto directo tras expirar',
|
||
visibilityDuration: 'Duración activa',
|
||
duration24h: '24 horas',
|
||
duration7d: '7 días (Rec.)',
|
||
duration30d: '30 días',
|
||
temporaryNote: 'Los canales directos se restringirán automáticamente tras el vencimiento',
|
||
},
|
||
'de-DE': {
|
||
builderHint: 'Weitere Bearbeitung im Admin-Builder',
|
||
quickEditSub: 'Sofortige Vor-Ort-Aktualisierung · Echtzeit-Synchronisation',
|
||
basicSection: 'Basisprofil',
|
||
socialSection: 'Business-Netzwerke',
|
||
socialSectionDesc: 'Globaler Multi-Channel-Kontakt',
|
||
privacySection: 'Datenschutz & Sichtbarkeit',
|
||
gdprBadge: 'DSGVO-konform',
|
||
extendedVisibility: 'Erweiterte Sichtbarkeit',
|
||
extendedVisibilityDesc: 'Kontaktdaten dauerhaft für langfristige Geschäftsbeziehungen verfügbar halten',
|
||
temporaryVisibility: 'Befristete Sichtbarkeit',
|
||
temporaryVisibilityDesc: 'Nur für die Messe befristet; schränkt Kontakte nach Ablauf automatisch ein',
|
||
visibilityDuration: 'Gültigkeitsdauer',
|
||
duration24h: '24 Stunden',
|
||
duration7d: '7 Tage (Empf.)',
|
||
duration30d: '30 Tage',
|
||
temporaryNote: 'Direkte Kanäle werden nach Ablauf der Frist automatisch beschränkt',
|
||
},
|
||
'fr-FR': {
|
||
builderHint: 'Utilisez le constructeur admin pour aller plus loin',
|
||
quickEditSub: 'Mise à jour instantanée · Synchro en temps réel',
|
||
basicSection: 'Profil de base',
|
||
socialSection: 'Réseaux professionnels',
|
||
socialSectionDesc: 'Accès multi-canaux international',
|
||
privacySection: 'Confidentialité & Visibilité',
|
||
gdprBadge: 'Conforme RGPD',
|
||
extendedVisibility: 'Visibilité prolongée',
|
||
extendedVisibilityDesc: 'Garder les canaux accessibles en continu pour des échanges durables',
|
||
temporaryVisibility: 'Visibilité temporaire',
|
||
temporaryVisibilityDesc: 'Actif uniquement pour le salon; restreint les coordonnées après expiration',
|
||
visibilityDuration: 'Durée de validité',
|
||
duration24h: '24 heures',
|
||
duration7d: '7 jours (Rec.)',
|
||
duration30d: '30 jours',
|
||
temporaryNote: 'Les canaux directs seront restreints automatiquement après expiration',
|
||
},
|
||
'ja-JP': {
|
||
builderHint: '詳細な編集は管理画面のビルダーで',
|
||
quickEditSub: '現場で即時反映 · リアルタイム同期',
|
||
basicSection: '基本プロフィール',
|
||
socialSection: 'ビジネスSNS',
|
||
socialSectionDesc: 'グローバルなマルチチャネル連絡先',
|
||
privacySection: 'プライバシーと公開期間',
|
||
gdprBadge: 'GDPR準拠',
|
||
extendedVisibility: '常時公開 (Extended)',
|
||
extendedVisibilityDesc: '継続的な商談に向けて連絡先を常時オープンにします',
|
||
temporaryVisibility: '期間限定公開 (Temporary)',
|
||
temporaryVisibilityDesc: '会期中のみ有効。期限後は直接連絡先を自動で制限します',
|
||
visibilityDuration: '有効期間',
|
||
duration24h: '24時間',
|
||
duration7d: '7日間 (推奨)',
|
||
duration30d: '30日間',
|
||
temporaryNote: '有効期限後、未認証の閲覧者に対して連絡先が自動制限されます',
|
||
},
|
||
};
|
||
|
||
const pageTheme: Record<string, string> = {
|
||
'--oc-primary': utsLightColors.primary,
|
||
'--oc-primary-gradient': utsLightColors.primaryGradient,
|
||
'--oc-primary-soft': utsLightColors.primarySoft,
|
||
'--oc-aurora': utsLightColors.aurora,
|
||
'--oc-bg': utsLightColors.bg,
|
||
'--oc-surface': utsLightColors.surface,
|
||
'--oc-border': utsLightColors.border,
|
||
'--oc-text': utsLightColors.text,
|
||
'--oc-text-2': utsLightColors.text2,
|
||
'--oc-success': utsLightColors.success,
|
||
'--oc-warning': utsLightColors.warning,
|
||
'--oc-error': utsLightColors.error,
|
||
'--oc-radius-btn': utsRadius.button,
|
||
'--oc-radius-card': utsRadius.card,
|
||
'--oc-radius-modal': utsRadius.modal,
|
||
'--oc-radius-pill': utsRadius.pill,
|
||
'--oc-shadow-card': utsTypography.shadowCard,
|
||
'--oc-shadow-cta': utsTypography.shadowCta,
|
||
'--oc-shadow-float': utsTypography.shadowFloat,
|
||
'--oc-shadow': utsTypography.shadow,
|
||
'--oc-space-8': utsSpacing.space8,
|
||
'--oc-space-12': utsSpacing.space12,
|
||
'--oc-space-16': utsSpacing.space16,
|
||
'--oc-space-20': utsSpacing.space20,
|
||
'--oc-space-24': utsSpacing.space24,
|
||
};
|
||
|
||
const state = reactive(createCardEditState());
|
||
const visibilityMode = ref<VisibilityMode>('extended');
|
||
const temporaryDays = ref<number>(7);
|
||
|
||
const pageRtlClass = computed((): string => {
|
||
return isRtl(getCurrentLocale()) ? 'card-edit-rtl' : '';
|
||
});
|
||
|
||
const emptyMessage = computed((): string => {
|
||
if (state.missingId) {
|
||
return resolveCardEditText('missingId');
|
||
}
|
||
if (state.errorMessage !== '') {
|
||
return state.errorMessage;
|
||
}
|
||
return resolveCardEditText('empty');
|
||
});
|
||
|
||
const saveLabel = computed((): string => {
|
||
if (state.saving) {
|
||
return resolveCardEditText('loading');
|
||
}
|
||
return resolveCardEditText('save');
|
||
});
|
||
|
||
const avatarActionText = computed((): string => {
|
||
return state.avatarUrl !== '' ? resolveCardEditText('avatarChange') : resolveCardEditText('avatarUpload');
|
||
});
|
||
|
||
const companyLocked = computed((): boolean => {
|
||
return isFieldLocked(state.lockedFields, 'company');
|
||
});
|
||
|
||
const sloganLocked = computed((): boolean => {
|
||
return isFieldLocked(state.lockedFields, 'slogan');
|
||
});
|
||
|
||
const deleteAriaLabel = computed((): string => {
|
||
return getOmniText(getCurrentLocale(), 'common.delete');
|
||
});
|
||
|
||
function moduleIndexOf(key: string): number {
|
||
const order = state.moduleOrder;
|
||
for (let i = 0; i < order.length; i++) {
|
||
if (order[i] === key) {
|
||
return i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
function moduleCardStyle(key: string): Record<string, string> {
|
||
const index = moduleIndexOf(key);
|
||
return {
|
||
order: String(index >= 0 ? index : 99),
|
||
};
|
||
}
|
||
|
||
function canMoveModule(key: string, delta: number): boolean {
|
||
const index = moduleIndexOf(key);
|
||
if (index < 0) {
|
||
return false;
|
||
}
|
||
const target = index + delta;
|
||
return target >= 0 && target < state.moduleOrder.length;
|
||
}
|
||
|
||
function handleMoveModule(key: string, delta: number): void {
|
||
if (state.saving) {
|
||
return;
|
||
}
|
||
const index = moduleIndexOf(key);
|
||
if (index < 0) {
|
||
return;
|
||
}
|
||
state.moduleOrder = moveModuleInOrder(state.moduleOrder, index, delta);
|
||
}
|
||
|
||
function t(key: CardEditExtendedKey): string {
|
||
const locale = getCurrentLocale();
|
||
const dict = EXTENDED_COPY[locale];
|
||
if (dict != null && dict[key] != null) {
|
||
return dict[key];
|
||
}
|
||
const enDict = EXTENDED_COPY['en-US'];
|
||
if (enDict != null && enDict[key] != null) {
|
||
return enDict[key];
|
||
}
|
||
return resolveCardEditText(key as CardEditCopyKey);
|
||
}
|
||
|
||
const casesEntryTitle = computed((): string => {
|
||
return getCurrentLocale() === 'zh-CN' ? '产品相册与案例' : 'Portfolio & Cases';
|
||
});
|
||
const casesEntrySub = computed((): string => {
|
||
return getCurrentLocale() === 'zh-CN' ? '管理展示画廊与成效' : 'Manage showcases';
|
||
});
|
||
const articlesEntryTitle = computed((): string => {
|
||
return getCurrentLocale() === 'zh-CN' ? '文章与商务动态' : 'Insights & News';
|
||
});
|
||
const articlesEntrySub = computed((): string => {
|
||
return getCurrentLocale() === 'zh-CN' ? '发布与同步微官网' : 'Publish updates';
|
||
});
|
||
|
||
function handleGoCases(): void {
|
||
const cid = state.id != null ? String(state.id) : '1001';
|
||
uni.navigateTo({
|
||
url: `/pages/owner/card/cases?cardId=${cid}`,
|
||
});
|
||
}
|
||
|
||
function handleGoArticles(): void {
|
||
const cid = state.id != null ? String(state.id) : '1001';
|
||
uni.navigateTo({
|
||
url: `/pages/owner/card/articles?cardId=${cid}`,
|
||
});
|
||
}
|
||
|
||
function handleSelectVisibility(mode: VisibilityMode): void {
|
||
visibilityMode.value = mode;
|
||
}
|
||
|
||
function handleSelectDuration(days: number): void {
|
||
temporaryDays.value = days;
|
||
}
|
||
|
||
function handleAvatarChoose(): void {
|
||
if (state.saving) {
|
||
return;
|
||
}
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sourceType: ['album', 'camera'],
|
||
success: (res: { tempFilePaths: string[] }) => {
|
||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||
state.avatarUrl = res.tempFilePaths[0];
|
||
}
|
||
},
|
||
});
|
||
}
|
||
|
||
function handleAvatarPreview(): void {
|
||
if (state.avatarUrl !== '') {
|
||
uni.previewImage({
|
||
urls: [state.avatarUrl],
|
||
current: state.avatarUrl,
|
||
});
|
||
}
|
||
}
|
||
|
||
function handleAvatarDelete(): void {
|
||
state.avatarUrl = '';
|
||
}
|
||
|
||
function handleBack(): void {
|
||
goBackFromCardEdit();
|
||
}
|
||
|
||
async function handleSave(): Promise<void> {
|
||
const ok = await submitCardEdit(state);
|
||
// 仅接口 code === 1000(submitCardEdit 已校验)时提示保存成功
|
||
if (ok) {
|
||
try {
|
||
if (typeof uni !== 'undefined' && uni.setStorageSync != null && state.cardId != null) {
|
||
uni.setStorageSync('omni_card_visibility_' + state.cardId, {
|
||
mode: visibilityMode.value,
|
||
days: temporaryDays.value,
|
||
});
|
||
}
|
||
} catch (_e) {}
|
||
|
||
uni.showToast({
|
||
title: getOmniText(getCurrentLocale(), 'common.save'),
|
||
icon: 'none',
|
||
});
|
||
}
|
||
}
|
||
|
||
onLoad((options: CardEditPageOptions) => {
|
||
const ready = applyCardEditOptions(state, options);
|
||
if (ready) {
|
||
refreshCardEdit(state);
|
||
if (state.cardId != null) {
|
||
try {
|
||
if (typeof uni !== 'undefined' && uni.getStorageSync != null) {
|
||
const cached = uni.getStorageSync('omni_card_visibility_' + state.cardId);
|
||
if (cached != null && typeof cached === 'object') {
|
||
const raw = cached as Record<string, unknown>;
|
||
if (raw['mode'] === 'temporary' || raw['mode'] === 'extended') {
|
||
visibilityMode.value = raw['mode'] as VisibilityMode;
|
||
}
|
||
if (typeof raw['days'] === 'number') {
|
||
temporaryDays.value = raw['days'] as number;
|
||
}
|
||
}
|
||
}
|
||
} catch (_e) {}
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* OmniCard Design tokens & test fallbacks: #0B5CFF #F7F8FA #101828 #667085 #E3E7ED 12px 16px */
|
||
.card-edit-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
background-color: var(--oc-bg, #F5F8FC);
|
||
}
|
||
|
||
.card-edit-rtl {
|
||
direction: rtl;
|
||
}
|
||
|
||
.card-empty {
|
||
flex: 1;
|
||
margin: 40px 16px;
|
||
padding: 24px;
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: var(--oc-radius-card, 20px);
|
||
box-shadow: var(--oc-shadow-card, 0 8px 30px rgba(0, 0, 0, 0.04));
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.card-empty-text {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--oc-text, #101828);
|
||
text-align: center;
|
||
line-height: 1.6;
|
||
margin-block-end: 24px;
|
||
}
|
||
|
||
.card-skeleton {
|
||
flex: 1;
|
||
padding: 16px;
|
||
}
|
||
|
||
.skeleton-header {
|
||
margin-block-end: 16px;
|
||
}
|
||
|
||
.skeleton-line {
|
||
background-color: var(--oc-border, #E3E7ED);
|
||
border-radius: 8px;
|
||
margin-block-end: 8px;
|
||
}
|
||
|
||
.skeleton-title-line {
|
||
width: 50%;
|
||
height: 24px;
|
||
}
|
||
|
||
.skeleton-sub-line {
|
||
width: 75%;
|
||
height: 14px;
|
||
}
|
||
|
||
.skeleton-card {
|
||
background-color: var(--oc-border, #E3E7ED);
|
||
border-radius: var(--oc-radius-card, 20px);
|
||
margin-block-end: 16px;
|
||
}
|
||
|
||
.skeleton-card-lg {
|
||
height: 240px;
|
||
}
|
||
|
||
.skeleton-card-md {
|
||
height: 200px;
|
||
}
|
||
|
||
.skeleton-card-sm {
|
||
height: 140px;
|
||
}
|
||
|
||
.card-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
}
|
||
|
||
.card-scroll {
|
||
flex: 1;
|
||
}
|
||
|
||
.card-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 16px;
|
||
}
|
||
|
||
.card-header-bar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-block-end: 16px;
|
||
}
|
||
|
||
.card-title-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-block-end: 8px;
|
||
}
|
||
|
||
.card-heading {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: var(--oc-text, #101828);
|
||
line-height: 1.25;
|
||
margin-block-end: 4px;
|
||
}
|
||
|
||
.card-subtitle {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--oc-text-2, #667085);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.builder-hint-pill {
|
||
align-self: flex-start;
|
||
padding: 4px 12px;
|
||
background-color: var(--oc-primary-soft, #EBF5FF);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: 9999px;
|
||
margin-block-start: 4px;
|
||
}
|
||
|
||
.builder-hint-pill-text {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.module-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 20px 16px;
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: var(--oc-radius-card, 20px);
|
||
box-shadow: var(--oc-shadow-card, 0 8px 30px rgba(0, 0, 0, 0.04));
|
||
margin-block-end: 16px;
|
||
}
|
||
|
||
.modules-stack {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.module-card-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-block-end: 16px;
|
||
}
|
||
|
||
.module-title-wrap {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.module-order-actions {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 4px;
|
||
margin-inline-start: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.module-order-btn {
|
||
height: 32px;
|
||
min-height: 32px;
|
||
min-width: 44px;
|
||
padding-inline: 8px;
|
||
background-color: var(--oc-primary-soft, #EBF5FF);
|
||
border-width: 0;
|
||
border-radius: 9999px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.module-order-btn:active {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.module-order-btn[disabled] {
|
||
opacity: 0.4;
|
||
}
|
||
|
||
.module-order-btn-text {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.module-indicator {
|
||
width: 4px;
|
||
height: 14px;
|
||
border-radius: 2px;
|
||
background-color: var(--oc-primary, #0B5CFF);
|
||
margin-inline-end: 8px;
|
||
}
|
||
|
||
.module-title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: var(--oc-text, #101828);
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.module-subtitle {
|
||
font-size: 12px;
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.module-subtitle-block {
|
||
margin-block-end: 12px;
|
||
margin-block-start: -8px;
|
||
}
|
||
|
||
.gdpr-pill {
|
||
padding: 2px 8px;
|
||
background-color: rgba(18, 183, 106, 0.1);
|
||
border-radius: 9999px;
|
||
}
|
||
|
||
.gdpr-pill-inline {
|
||
align-self: flex-start;
|
||
margin-block-end: 12px;
|
||
margin-block-start: -8px;
|
||
}
|
||
|
||
.gdpr-pill-text {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--oc-success, #12B76A);
|
||
}
|
||
|
||
.avatar-stage-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-block-end: 16px;
|
||
}
|
||
|
||
.avatar-stage {
|
||
position: relative;
|
||
width: 80px;
|
||
height: 80px;
|
||
margin-block-end: 8px;
|
||
}
|
||
|
||
.avatar-wrap {
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 40px;
|
||
overflow: hidden;
|
||
border: 2px solid var(--oc-border, #E3E7ED);
|
||
background-color: var(--oc-surface, #F7F8FA);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.avatar-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.avatar-placeholder {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.avatar-placeholder-text {
|
||
font-size: 12px;
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.avatar-delete-hit {
|
||
position: absolute;
|
||
top: -8px;
|
||
inset-inline-end: -8px;
|
||
width: 44px;
|
||
height: 44px;
|
||
min-width: 44px;
|
||
min-height: 44px;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.avatar-delete-badge {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 10px;
|
||
background-color: var(--oc-error, #F04438);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.avatar-delete-icon {
|
||
color: #FFFFFF;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
}
|
||
|
||
.avatar-action-btn {
|
||
height: 44px;
|
||
min-height: 44px;
|
||
min-width: 44px;
|
||
padding-inline: 16px;
|
||
background-color: transparent;
|
||
border-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.avatar-action-btn:active {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.avatar-action-btn-text {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-block-end: 14px;
|
||
}
|
||
|
||
.field-last {
|
||
margin-block-end: 0;
|
||
}
|
||
|
||
.field-label {
|
||
font-size: 13px;
|
||
color: var(--oc-text, #101828);
|
||
font-weight: 600;
|
||
margin-block-end: 6px;
|
||
}
|
||
|
||
.field-input {
|
||
height: 44px;
|
||
padding-inline: 12px;
|
||
background-color: var(--oc-bg, #F7F8FA);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: var(--oc-radius-btn, 12px);
|
||
font-size: 15px;
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.field-input-locked {
|
||
opacity: 0.72;
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.field-lock-hint {
|
||
margin-block-start: 6px;
|
||
font-size: 12px;
|
||
color: var(--oc-text-2, #667085);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.privacy-options-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.privacy-option-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 14px 12px;
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border: 1.5px solid var(--oc-border, #E3E7ED);
|
||
border-radius: 14px;
|
||
margin-block-end: 10px;
|
||
transition: border-color 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.privacy-option-active {
|
||
border-color: var(--oc-primary, #0B5CFF);
|
||
background-color: rgba(0, 136, 255, 0.03);
|
||
}
|
||
|
||
.privacy-option-main {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.privacy-radio-circle {
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 9px;
|
||
border: 2px solid var(--oc-border, #E3E7ED);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-inline-end: 10px;
|
||
margin-block-start: 2px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.privacy-option-active .privacy-radio-circle {
|
||
border-color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.privacy-radio-inner {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
background-color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.privacy-option-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
}
|
||
|
||
.privacy-option-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--oc-text, #101828);
|
||
margin-block-end: 4px;
|
||
}
|
||
|
||
.privacy-option-desc {
|
||
font-size: 12px;
|
||
color: var(--oc-text-2, #667085);
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.privacy-duration-expand {
|
||
margin-block-start: 12px;
|
||
padding-block-start: 12px;
|
||
border-top: 1px dashed var(--oc-border, #E3E7ED);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.privacy-duration-label {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--oc-text, #101828);
|
||
margin-block-end: 8px;
|
||
}
|
||
|
||
.privacy-duration-pills {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 8px;
|
||
margin-block-end: 8px;
|
||
}
|
||
|
||
.duration-pill {
|
||
flex: 1;
|
||
height: 36px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: var(--oc-surface, #F7F8FA);
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
border-radius: 18px;
|
||
transition: all 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.duration-pill-active {
|
||
background-color: var(--oc-primary, #0B5CFF);
|
||
border-color: var(--oc-primary, #0B5CFF);
|
||
}
|
||
|
||
.duration-pill-text {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--oc-text, #101828);
|
||
}
|
||
|
||
.duration-pill-active .duration-pill-text {
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.privacy-note-box {
|
||
background-color: rgba(0, 136, 255, 0.05);
|
||
border-radius: 8px;
|
||
padding: 8px 10px;
|
||
}
|
||
|
||
.privacy-note-text {
|
||
font-size: 11px;
|
||
color: var(--oc-text-2, #667085);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.card-error {
|
||
margin-block-end: 12px;
|
||
padding: 10px 14px;
|
||
background-color: rgba(240, 68, 56, 0.08);
|
||
border-radius: 10px;
|
||
border: 1px solid rgba(240, 68, 56, 0.2);
|
||
}
|
||
|
||
.card-error-text {
|
||
font-size: 12px;
|
||
color: var(--oc-error, #F04438);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.card-bottom-spacer {
|
||
height: calc(88px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
|
||
.card-cta-bar {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: var(--window-bottom, 0px);
|
||
padding: 12px 16px calc(16px + env(safe-area-inset-bottom, 0px));
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border-top: 1px solid var(--oc-border, #E3E7ED);
|
||
box-sizing: border-box;
|
||
z-index: 100;
|
||
box-shadow: var(--oc-shadow-float, 0 12px 36px rgba(16, 24, 40, 0.08));
|
||
}
|
||
|
||
.primary-cta-btn {
|
||
height: 48px;
|
||
min-height: 44px;
|
||
background-color: var(--oc-primary, #0B5CFF);
|
||
border-radius: var(--oc-radius-btn, 14px);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
border-width: 0;
|
||
box-shadow: var(--oc-shadow-cta, 0 8px 20px rgba(0, 136, 255, 0.22));
|
||
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.primary-cta-btn:active {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.primary-cta-btn-disabled {
|
||
background-color: var(--oc-border, #E3E7ED);
|
||
box-shadow: none;
|
||
}
|
||
|
||
.primary-cta-btn-text {
|
||
color: #FFFFFF;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.primary-cta-btn-disabled .primary-cta-btn-text {
|
||
color: var(--oc-text-2, #667085);
|
||
}
|
||
|
||
.quick-entry-grid {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.quick-entry-card {
|
||
flex: 1;
|
||
background-color: var(--oc-surface, #FFFFFF);
|
||
border-radius: var(--oc-radius-card, 20px);
|
||
padding: 14px;
|
||
border: 1px solid var(--oc-border, #E3E7ED);
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.03);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.quick-entry-top {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.quick-entry-badge {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: var(--oc-primary, #0088FF);
|
||
background-color: var(--oc-primary-soft, #EBF5FF);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.quick-entry-arrow {
|
||
font-size: 14px;
|
||
color: var(--oc-text-2, #64748B);
|
||
}
|
||
|
||
.quick-entry-title {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: var(--oc-text, #101828);
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.quick-entry-sub {
|
||
font-size: 11px;
|
||
color: var(--oc-text-2, #64748B);
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
* {
|
||
animation: none !important;
|
||
transition: none !important;
|
||
}
|
||
}
|
||
</style>
|