- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
439 行
9.7 KiB
Plaintext
439 行
9.7 KiB
Plaintext
<template>
|
|
<view class="security-page" :style="pageTheme">
|
|
<scroll-view class="security-scroll" :scroll-y="true">
|
|
<view class="security-content">
|
|
<!-- 页面标题与简介 (瑞士国际主义排版) -->
|
|
<view class="header-section">
|
|
<text class="page-title">{{ t('title') }}</text>
|
|
<text class="page-desc">{{ t('desc') }}</text>
|
|
</view>
|
|
|
|
<!-- 设备空状态 -->
|
|
<view v-if="state.devices.length === 0" class="empty-card">
|
|
<text class="empty-text">{{ t('deviceEmpty') }}</text>
|
|
</view>
|
|
|
|
<!-- 设备资产卡片列表 -->
|
|
<view v-else class="devices-group">
|
|
<view
|
|
v-for="item in state.devices"
|
|
:key="item.id"
|
|
class="device-card"
|
|
:class="{ 'device-card-active': state.selectedId === item.id }"
|
|
@click="handleSelect(item.id)"
|
|
>
|
|
<view class="card-top-row">
|
|
<view class="card-identity">
|
|
<text class="card-batch-tag">{{ item.batchNo || 'NFC-CORE' }}</text>
|
|
<text class="card-uid-text">{{ item.uid }}</text>
|
|
</view>
|
|
<!-- 状态徽标 -->
|
|
<view
|
|
class="status-badge"
|
|
:class="item.status === 'active' ? 'badge-active' : 'badge-frozen'"
|
|
>
|
|
<text
|
|
class="badge-text"
|
|
:class="item.status === 'active' ? 'badge-text-active' : 'badge-text-frozen'"
|
|
>
|
|
{{ item.status === 'active' ? t('statusActive') : t('statusFrozen') }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 防伪验签栏 -->
|
|
<view class="card-security-row">
|
|
<text class="security-label">{{ t('uidLabel') }}</text>
|
|
<view class="security-chip-indicator">
|
|
<view
|
|
class="indicator-dot"
|
|
:class="isUidValid(item.uid) ? 'dot-valid' : 'dot-invalid'"
|
|
></view>
|
|
<text class="indicator-text">
|
|
{{ isUidValid(item.uid) ? t('antiCounterfeitValid') : t('antiCounterfeitInvalid') }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 安全警示机制说明卡片 -->
|
|
<view class="warning-box">
|
|
<view class="warning-header">
|
|
<text class="warning-title">{{ t('warningTitle') }}</text>
|
|
</view>
|
|
<text class="warning-desc">{{ t('warningDesc') }}</text>
|
|
</view>
|
|
|
|
<!-- 底部占位以防被固定按钮遮挡 -->
|
|
<view class="bottom-spacer"></view>
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<!-- 底部固定单一主 CTA 区域 (一屏只允许 1 个主 CTA,触控区 >= 44x44) -->
|
|
<view class="security-cta-bar">
|
|
<button
|
|
class="primary-cta-btn"
|
|
:class="ctaBtnClass"
|
|
:disabled="state.submitting || state.selectedId === null"
|
|
@click="handleToggleSecurity"
|
|
>
|
|
<text class="primary-cta-btn-text">{{ ctaBtnText }}</text>
|
|
</button>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import { utsLightColors, utsRadius, utsSpacing } from '../../../common/tokens.uts';
|
|
import {
|
|
createSecurityState,
|
|
selectDevice,
|
|
freezeDevice,
|
|
unfreezeDevice,
|
|
validateNfcUid,
|
|
resolveSecurityText,
|
|
NfcSecurityDevice,
|
|
SecurityState,
|
|
SecurityCopyKey,
|
|
} from './useSecurity.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-success': utsLightColors.success,
|
|
'--oc-warning': utsLightColors.warning,
|
|
'--oc-error': utsLightColors.error,
|
|
'--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,
|
|
};
|
|
|
|
// 预设默认资产硬件卡,真实环境下可由 API 载入
|
|
const initialDevices: NfcSecurityDevice[] = [
|
|
{
|
|
id: 1,
|
|
uid: '04A32B1C8E5F90',
|
|
cardId: 101,
|
|
status: 'active',
|
|
batchNo: 'OMNI-PRO-2026',
|
|
material: 'metal',
|
|
},
|
|
];
|
|
|
|
const state: SecurityState = reactive(createSecurityState(initialDevices));
|
|
|
|
function t(key: SecurityCopyKey): string {
|
|
return resolveSecurityText(key);
|
|
}
|
|
|
|
function isUidValid(uid: string): boolean {
|
|
return validateNfcUid(uid).valid;
|
|
}
|
|
|
|
const currentDevice = computed((): NfcSecurityDevice | null => {
|
|
if (state.selectedId === null) {
|
|
return null;
|
|
}
|
|
const found = state.devices.find((d: NfcSecurityDevice): boolean => d.id === state.selectedId);
|
|
return found || null;
|
|
});
|
|
|
|
const ctaBtnText = computed((): string => {
|
|
if (state.submitting) {
|
|
return '处理中...';
|
|
}
|
|
if (!currentDevice.value) {
|
|
return t('freezeBtn');
|
|
}
|
|
return currentDevice.value.status === 'active' ? t('freezeBtn') : t('unfreezeBtn');
|
|
});
|
|
|
|
const ctaBtnClass = computed((): Record<string, boolean> => {
|
|
const isFrozen = currentDevice.value?.status === 'frozen';
|
|
return {
|
|
'primary-cta-btn-disabled': state.submitting || state.selectedId === null,
|
|
'cta-btn-danger': currentDevice.value?.status === 'active',
|
|
'cta-btn-success': isFrozen,
|
|
};
|
|
});
|
|
|
|
function handleSelect(id: number): void {
|
|
selectDevice(state, id);
|
|
}
|
|
|
|
async function handleToggleSecurity(): Promise<void> {
|
|
if (!currentDevice.value || state.submitting) {
|
|
return;
|
|
}
|
|
|
|
const device = currentDevice.value;
|
|
try {
|
|
if (device.status === 'active') {
|
|
await freezeDevice(state, device.id);
|
|
uni.showToast({
|
|
title: t('freezeSuccess'),
|
|
icon: 'none',
|
|
});
|
|
} else {
|
|
await unfreezeDevice(state, device.id);
|
|
uni.showToast({
|
|
title: t('unfreezeSuccess'),
|
|
icon: 'none',
|
|
});
|
|
}
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : '操作异常';
|
|
uni.showToast({
|
|
title: msg,
|
|
icon: 'none',
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.security-page {
|
|
flex: 1;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: var(--oc-bg);
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.security-scroll {
|
|
flex: 1;
|
|
width: 100%;
|
|
}
|
|
|
|
.security-content {
|
|
padding: 24px 20px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.header-section {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
color: var(--oc-text);
|
|
line-height: 1.25;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.page-desc {
|
|
font-size: 14px;
|
|
color: var(--oc-text-2);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.empty-card {
|
|
padding: 32px 16px;
|
|
background-color: var(--oc-surface);
|
|
border-radius: var(--oc-radius-card);
|
|
border-width: 1px;
|
|
border-color: var(--oc-border);
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.empty-text {
|
|
font-size: 14px;
|
|
color: var(--oc-text-2);
|
|
}
|
|
|
|
.devices-group {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.device-card {
|
|
background-color: var(--oc-surface);
|
|
border-radius: var(--oc-radius-card);
|
|
border-width: 1px;
|
|
border-color: var(--oc-border);
|
|
padding: 16px;
|
|
margin-bottom: 12px;
|
|
transition-duration: 150ms;
|
|
}
|
|
|
|
.device-card-active {
|
|
border-color: var(--oc-primary);
|
|
background-color: #FFFFFF;
|
|
}
|
|
|
|
.card-top-row {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.card-identity {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.card-batch-tag {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--oc-text-2);
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.card-uid-text {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--oc-text);
|
|
font-family: monospace;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 4px 10px;
|
|
border-radius: 9999px;
|
|
border-width: 1px;
|
|
}
|
|
|
|
.badge-active {
|
|
background-color: #ECFDF3;
|
|
border-color: #ABEFC6;
|
|
}
|
|
|
|
.badge-frozen {
|
|
background-color: #FEF3F2;
|
|
border-color: #FECDCA;
|
|
}
|
|
|
|
.badge-text {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.badge-text-active {
|
|
color: var(--oc-success);
|
|
}
|
|
|
|
.badge-text-frozen {
|
|
color: var(--oc-error);
|
|
}
|
|
|
|
.card-security-row {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
border-top-width: 1px;
|
|
border-top-color: var(--oc-border);
|
|
padding-top: 10px;
|
|
}
|
|
|
|
.security-label {
|
|
font-size: 12px;
|
|
color: var(--oc-text-2);
|
|
}
|
|
|
|
.security-chip-indicator {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
|
|
.indicator-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
margin-right: 6px;
|
|
}
|
|
|
|
.dot-valid {
|
|
background-color: var(--oc-success);
|
|
}
|
|
|
|
.dot-invalid {
|
|
background-color: var(--oc-error);
|
|
}
|
|
|
|
.indicator-text {
|
|
font-size: 12px;
|
|
color: var(--oc-text-2);
|
|
}
|
|
|
|
.warning-box {
|
|
background-color: #F8F9FB;
|
|
border-radius: var(--oc-radius-card);
|
|
border-width: 1px;
|
|
border-color: var(--oc-border);
|
|
padding: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.warning-header {
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.warning-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--oc-text);
|
|
}
|
|
|
|
.warning-desc {
|
|
font-size: 12px;
|
|
color: var(--oc-text-2);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.bottom-spacer {
|
|
height: 80px;
|
|
}
|
|
|
|
/* 底部单一主 CTA 悬浮栏:触控区 >= 44x44 */
|
|
.security-cta-bar {
|
|
position: fixed;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: #FFFFFF;
|
|
border-top-width: 1px;
|
|
border-top-color: var(--oc-border);
|
|
padding: 12px 20px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.primary-cta-btn {
|
|
height: 48px;
|
|
min-height: 44px;
|
|
border-radius: var(--oc-radius-btn);
|
|
background-color: var(--oc-primary);
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-width: 0;
|
|
}
|
|
|
|
.cta-btn-danger {
|
|
background-color: #D92D20;
|
|
}
|
|
|
|
.cta-btn-success {
|
|
background-color: var(--oc-primary);
|
|
}
|
|
|
|
.primary-cta-btn-disabled {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.primary-cta-btn-text {
|
|
color: #FFFFFF;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
}
|
|
</style>
|