文件
OmniCard/mpuniapp/pages/owner/nfc/security.uvue
wikiboss eec259a3a7 feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换
- 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片
- 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转
- 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出
- 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志
- 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票
- 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏
- 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
2026-09-20 23:47:47 +08:00

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>