747 lines
17 KiB
Vue
747 lines
17 KiB
Vue
<template>
|
|
<view class="scan-page">
|
|
<!-- 扫码区域容器 -->
|
|
<view class="scan-window">
|
|
<!-- html5-qrcode 扫描器容器 -->
|
|
<view id="qr-reader" class="qr-reader"></view>
|
|
|
|
<!-- 扫描装饰 -->
|
|
<view class="scan-mask">
|
|
<view class="scan-frame">
|
|
<view class="scan-line" v-if="scanning"></view>
|
|
<view class="corner top-left"></view>
|
|
<view class="corner top-right"></view>
|
|
<view class="corner bottom-left"></view>
|
|
<view class="corner bottom-right"></view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="scan-tip">{{ tipText }}</view>
|
|
</view>
|
|
|
|
<!-- 底部操作栏 -->
|
|
<view class="bottom-actions">
|
|
<view class="action-item" @click.stop="chooseImage">
|
|
<!-- <view class="action-icon">📷</view> -->
|
|
<uv-icon name="photo" size="24" color="#fff"></uv-icon>
|
|
<text>相册</text>
|
|
</view>
|
|
|
|
<view class="action-item" @click.stop="toggleInput">
|
|
<!-- <view class="action-icon">✏️</view> -->
|
|
<uv-icon name="edit-pen" size="24" color="#fff"></uv-icon>
|
|
<text>手动输入</text>
|
|
</view>
|
|
|
|
|
|
<view class="action-item" @click.stop="goBack">
|
|
<!-- <view class="action-icon">←</view> -->
|
|
<uv-icon name="arrow-left" size="24" color="#fff"></uv-icon>
|
|
<text>返回</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 手动输入弹窗 -->
|
|
<uv-popup ref="inputPopup" mode="center" round="16" :closeOnClickOverlay="true">
|
|
<view class="input-dialog">
|
|
<view class="dialog-title">手动输入设备号</view>
|
|
<input
|
|
v-model="manualDeviceNo"
|
|
placeholder="请输入设备上的编号"
|
|
class="device-input"
|
|
type="text"
|
|
/>
|
|
<view class="dialog-btns">
|
|
<button class="cancel-btn" @click="closeInput">取消</button>
|
|
<button class="confirm-btn" @click="confirmManualInput">确定</button>
|
|
</view>
|
|
</view>
|
|
</uv-popup>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted } from 'vue';
|
|
import { getQueryString } from '../../util/index.js';
|
|
import { Html5Qrcode, Html5QrcodeSupportedFormats } from 'html5-qrcode';
|
|
|
|
const inputPopup = ref(null);
|
|
const manualDeviceNo = ref('');
|
|
const tipText = ref('正在初始化...');
|
|
const scanning = ref(false);
|
|
const hasFlash = ref(false);
|
|
const flashOn = ref(false);
|
|
|
|
let html5QrCode = null;
|
|
let isProcessing = false; // 防止重复处理
|
|
|
|
// 统一扫描配置:增大识别区域,提升低清晰度二维码识别成功率
|
|
const getScanConfig = () => ({
|
|
fps: 12,
|
|
qrbox: (viewfinderWidth, viewfinderHeight) => {
|
|
const minEdge = Math.min(viewfinderWidth, viewfinderHeight);
|
|
const size = Math.max(220, Math.floor(minEdge * 0.72));
|
|
return { width: size, height: size };
|
|
},
|
|
disableFlip: false,
|
|
formatsToSupport: [Html5QrcodeSupportedFormats.QR_CODE],
|
|
experimentalFeatures: {
|
|
useBarCodeDetectorIfSupported: true
|
|
}
|
|
});
|
|
|
|
// 初始化扫码
|
|
const initScan = async () => {
|
|
try {
|
|
tipText.value = '正在初始化...';
|
|
console.log('=== 开始初始化扫码 ===');
|
|
|
|
// 检查浏览器支持
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
throw new Error('您的浏览器不支持摄像头访问');
|
|
}
|
|
|
|
// 等待 DOM 渲染
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
// 检查容器元素
|
|
const readerElement = document.getElementById('qr-reader');
|
|
if (!readerElement) {
|
|
throw new Error('扫码容器元素未找到');
|
|
}
|
|
console.log('✓ 扫码容器元素已找到');
|
|
|
|
// 创建 Html5Qrcode 实例
|
|
html5QrCode = new Html5Qrcode('qr-reader', {
|
|
verbose: false // 关闭详细日志
|
|
});
|
|
console.log('✓ Html5Qrcode 实例创建成功');
|
|
|
|
// 启动扫描
|
|
await startScanning();
|
|
|
|
} catch (err) {
|
|
console.error('❌ 初始化失败:', err);
|
|
handleInitError(err);
|
|
}
|
|
};
|
|
|
|
// 开始扫描
|
|
const startScanning = async () => {
|
|
try {
|
|
if (!html5QrCode) {
|
|
throw new Error('Html5Qrcode 实例不存在');
|
|
}
|
|
|
|
tipText.value = '正在启动摄像头...';
|
|
console.log('=== 开始启动扫描 ===');
|
|
console.log('html5QrCode 实例:', html5QrCode);
|
|
console.log('html5QrCode.start 方法:', typeof html5QrCode.start);
|
|
|
|
// 配置扫描参数
|
|
const config = getScanConfig();
|
|
|
|
console.log('扫描配置:', config);
|
|
console.log('准备调用 html5QrCode.start()...');
|
|
|
|
// 启动扫描 - 使用 { facingMode: "environment" } 优先后置摄像头
|
|
const startResult = await html5QrCode.start(
|
|
{ facingMode: "environment" },
|
|
config,
|
|
onScanSuccess,
|
|
onScanFailure
|
|
);
|
|
|
|
console.log('start() 调用结果:', startResult);
|
|
|
|
scanning.value = true;
|
|
tipText.value = '将二维码放入框内扫描';
|
|
console.log('✅ 扫描已成功启动');
|
|
|
|
// 延迟隐藏默认UI
|
|
setTimeout(() => {
|
|
hideDefaultUI();
|
|
}, 200);
|
|
|
|
} catch (err) {
|
|
console.error('❌ 启动扫描失败:', err);
|
|
console.error('错误详情:', {
|
|
name: err.name,
|
|
message: err.message,
|
|
stack: err.stack
|
|
});
|
|
|
|
// 尝试使用前置摄像头
|
|
console.log('尝试使用前置摄像头...');
|
|
try {
|
|
const config = getScanConfig();
|
|
|
|
await html5QrCode.start(
|
|
{ facingMode: "user" },
|
|
config,
|
|
onScanSuccess,
|
|
onScanFailure
|
|
);
|
|
|
|
scanning.value = true;
|
|
tipText.value = '将二维码放入框内扫描';
|
|
console.log('✅ 使用前置摄像头启动成功');
|
|
|
|
setTimeout(() => {
|
|
hideDefaultUI();
|
|
}, 200);
|
|
|
|
} catch (err2) {
|
|
console.error('❌ 前置摄像头也失败:', err2);
|
|
|
|
// 最后尝试:不指定摄像头
|
|
console.log('最后尝试:使用默认摄像头...');
|
|
try {
|
|
const config = getScanConfig();
|
|
|
|
// 获取摄像头列表
|
|
const cameras = await Html5Qrcode.getCameras();
|
|
console.log('可用摄像头:', cameras);
|
|
|
|
if (cameras && cameras.length > 0) {
|
|
const cameraId = cameras[0].id;
|
|
console.log('使用摄像头ID:', cameraId);
|
|
|
|
await html5QrCode.start(
|
|
cameraId,
|
|
config,
|
|
onScanSuccess,
|
|
onScanFailure
|
|
);
|
|
|
|
scanning.value = true;
|
|
tipText.value = '将二维码放入框内扫描';
|
|
console.log('✅ 使用默认摄像头启动成功');
|
|
|
|
setTimeout(() => {
|
|
hideDefaultUI();
|
|
}, 200);
|
|
} else {
|
|
throw new Error('未找到可用的摄像头');
|
|
}
|
|
} catch (err3) {
|
|
console.error('❌ 所有方式都失败:', err3);
|
|
throw err3;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// 隐藏默认UI
|
|
const hideDefaultUI = () => {
|
|
try {
|
|
const shaded = document.getElementById('qr-shaded-region');
|
|
if (shaded) {
|
|
shaded.style.border = 'none';
|
|
console.log('✓ 已隐藏默认边框');
|
|
}
|
|
} catch (err) {
|
|
console.warn('隐藏默认UI失败:', err);
|
|
}
|
|
};
|
|
|
|
// 扫描成功回调
|
|
const onScanSuccess = (decodedText, decodedResult) => {
|
|
// 防止重复处理
|
|
if (isProcessing) {
|
|
console.log('正在处理中,忽略重复扫码');
|
|
return;
|
|
}
|
|
|
|
if (!scanning.value) {
|
|
console.log('扫描已停止,忽略结果');
|
|
return;
|
|
}
|
|
|
|
isProcessing = true;
|
|
|
|
console.log('========== 扫码成功 ==========');
|
|
console.log('解码文本:', decodedText);
|
|
console.log('解码结果:', decodedResult);
|
|
console.log('==============================');
|
|
|
|
// 验证结果
|
|
if (!decodedText || decodedText.trim() === '') {
|
|
console.error('扫码结果为空');
|
|
isProcessing = false;
|
|
return;
|
|
}
|
|
|
|
const finalResult = decodedText.trim();
|
|
|
|
// 停止扫描
|
|
stopScan().then(() => {
|
|
// 震动反馈
|
|
if (navigator.vibrate) {
|
|
navigator.vibrate(200);
|
|
}
|
|
|
|
// 通过全局事件通知首页
|
|
uni.$emit('h5ScanSuccess', {
|
|
result: finalResult,
|
|
scanType: 'QR_CODE',
|
|
path: finalResult
|
|
});
|
|
|
|
// 不要立即返回,等待首页处理完成
|
|
// 首页会在处理完成后自动关闭扫码页
|
|
console.log('扫码结果已发送,等待首页处理...');
|
|
});
|
|
};
|
|
|
|
// 扫描失败回调
|
|
const onScanFailure = (error) => {
|
|
// 正常情况,不需要处理
|
|
};
|
|
|
|
// 停止扫描
|
|
const stopScan = async () => {
|
|
if (!html5QrCode) {
|
|
return;
|
|
}
|
|
|
|
console.log('=== 停止扫描 ===');
|
|
scanning.value = false;
|
|
flashOn.value = false;
|
|
|
|
try {
|
|
const isScanning = html5QrCode.isScanning;
|
|
if (isScanning) {
|
|
await html5QrCode.stop();
|
|
console.log('✓ Html5Qrcode 已停止');
|
|
}
|
|
} catch (err) {
|
|
console.warn('停止扫描失败:', err);
|
|
}
|
|
};
|
|
|
|
// 处理初始化错误
|
|
const handleInitError = (err) => {
|
|
console.error('处理初始化错误:', err);
|
|
|
|
let errMsg = '初始化失败';
|
|
let errDetail = '';
|
|
|
|
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
|
|
errMsg = '摄像头权限被拒绝';
|
|
errDetail = '请在浏览器设置中允许访问摄像头';
|
|
}
|
|
else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
|
|
errMsg = '未找到可用的摄像头';
|
|
errDetail = '请确保设备有摄像头';
|
|
}
|
|
else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
|
|
errMsg = '摄像头被占用';
|
|
errDetail = '请关闭其他使用摄像头的应用';
|
|
}
|
|
else if (err.name === 'NotSupportedError') {
|
|
errMsg = '浏览器不支持';
|
|
errDetail = '请使用现代浏览器访问';
|
|
}
|
|
// else if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
|
// errMsg = '需要 HTTPS 环境';
|
|
// errDetail = '摄像头功能需要在安全环境下使用';
|
|
// }
|
|
else {
|
|
errMsg = err.message || '摄像头启动失败';
|
|
errDetail = '请尝试刷新页面或使用其他方式';
|
|
}
|
|
|
|
tipText.value = errMsg;
|
|
|
|
// 显示错误提示,提供备选方案
|
|
uni.showModal({
|
|
title: errMsg,
|
|
content: errDetail + '\n\n您可以:\n1. 从相册选择二维码图片\n2. 手动输入设备号',
|
|
showCancel: true,
|
|
cancelText: '返回',
|
|
confirmText: '手动输入',
|
|
success: (res) => {
|
|
if (res.confirm) {
|
|
toggleInput();
|
|
} else if (res.cancel) {
|
|
goBack();
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
// 从相册选择图片识别
|
|
const chooseImage = async () => {
|
|
// #ifdef H5
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = 'image/*';
|
|
input.onchange = async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
uni.showLoading({ title: '正在识别...' });
|
|
|
|
try {
|
|
// 先停止摄像头扫描
|
|
const wasScanning = scanning.value;
|
|
if (wasScanning) {
|
|
await stopScan();
|
|
console.log('已停止摄像头扫描,准备识别图片');
|
|
// 等待停止完成
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
}
|
|
|
|
if (!html5QrCode) {
|
|
html5QrCode = new Html5Qrcode('qr-reader', { verbose: false });
|
|
}
|
|
|
|
// 使用 html5-qrcode 扫描文件
|
|
const result = await html5QrCode.scanFile(file, true);
|
|
console.log(result);
|
|
uni.hideLoading();
|
|
|
|
if (result) {
|
|
console.log('图片识别成功:', result);
|
|
|
|
// 震动反馈
|
|
if (navigator.vibrate) {
|
|
navigator.vibrate(200);
|
|
}
|
|
|
|
// 通过全局事件通知首页
|
|
uni.$emit('h5ScanSuccess', {
|
|
result: result,
|
|
scanType: 'QR_CODE',
|
|
path: result
|
|
});
|
|
|
|
// 不要立即返回,等待首页处理完成
|
|
console.log('图片识别结果已发送,等待首页处理...');
|
|
} else {
|
|
uni.showToast({ title: '未识别到二维码', icon: 'none' });
|
|
// 识别失败,重新启动摄像头扫描
|
|
if (wasScanning) {
|
|
setTimeout(async () => {
|
|
await startScanning();
|
|
}, 500);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('图片识别失败:', err);
|
|
uni.hideLoading();
|
|
uni.showToast({ title: '识别失败', icon: 'none' });
|
|
// 识别失败,重新启动摄像头扫描
|
|
setTimeout(async () => {
|
|
try {
|
|
await startScanning();
|
|
} catch (e) {
|
|
console.error('重新启动扫描失败:', e);
|
|
}
|
|
}, 500);
|
|
}
|
|
};
|
|
input.click();
|
|
// #endif
|
|
|
|
// #ifndef H5
|
|
uni.chooseImage({
|
|
count: 1,
|
|
sourceType: ['album'],
|
|
success: (res) => {
|
|
uni.showToast({ title: '该功能仅在H5环境可用', icon: 'none' });
|
|
}
|
|
});
|
|
// #endif
|
|
};
|
|
|
|
// 打开手动输入弹窗
|
|
const toggleInput = () => {
|
|
if (inputPopup.value) {
|
|
inputPopup.value.open();
|
|
}
|
|
};
|
|
|
|
// 关闭手动输入弹窗
|
|
const closeInput = () => {
|
|
if (inputPopup.value) {
|
|
inputPopup.value.close();
|
|
}
|
|
};
|
|
|
|
// 确认手动输入
|
|
const confirmManualInput = () => {
|
|
const deviceNo = manualDeviceNo.value.trim();
|
|
|
|
if (!deviceNo) {
|
|
uni.showToast({ title: '请输入设备号', icon: 'none' });
|
|
return;
|
|
}
|
|
|
|
closeInput();
|
|
stopScan();
|
|
|
|
// 处理可能包含 URL 的情况
|
|
let finalDeviceNo = deviceNo;
|
|
if (deviceNo.includes('deviceNo=')) {
|
|
finalDeviceNo = getQueryString(deviceNo, 'deviceNo') || deviceNo;
|
|
}
|
|
|
|
// 通过全局事件通知首页
|
|
uni.$emit('h5ScanSuccess', {
|
|
result: finalDeviceNo,
|
|
scanType: 'MANUAL'
|
|
});
|
|
|
|
// 不要立即返回,等待首页处理完成
|
|
console.log('手动输入结果已发送,等待首页处理...');
|
|
};
|
|
|
|
// 返回
|
|
const goBack = () => {
|
|
stopScan();
|
|
uni.navigateBack();
|
|
};
|
|
|
|
onMounted(() => {
|
|
console.log('扫码页面已挂载');
|
|
|
|
// 延迟初始化,确保 DOM 已渲染
|
|
setTimeout(() => {
|
|
console.log('开始初始化扫码');
|
|
initScan();
|
|
}, 500);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
console.log('扫码页面卸载,清理资源');
|
|
isProcessing = false;
|
|
|
|
if (html5QrCode) {
|
|
stopScan().then(() => {
|
|
html5QrCode.clear().catch(err => {
|
|
console.warn('清理 Html5Qrcode 实例失败:', err);
|
|
});
|
|
html5QrCode = null;
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.scan-page {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background: #000;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.scan-window {
|
|
width: 100%;
|
|
height: 100%;
|
|
position: relative;
|
|
|
|
.qr-reader {
|
|
width: 100%;
|
|
height: 100%;
|
|
|
|
// 覆盖 html5-qrcode 默认样式
|
|
:deep(#qr-shaded-region) {
|
|
border: none !important; // 隐藏默认边框
|
|
background: transparent !important; // 透明背景
|
|
}
|
|
|
|
:deep(video) {
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
object-fit: cover !important;
|
|
display: block !important;
|
|
}
|
|
|
|
:deep(canvas) {
|
|
display: none !important;
|
|
}
|
|
|
|
// 隐藏 html5-qrcode 的所有内置 UI 元素
|
|
:deep(#qr-shaded-region > div) {
|
|
display: none !important;
|
|
}
|
|
}
|
|
}
|
|
|
|
.scan-mask {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
pointer-events: none;
|
|
z-index: 10;
|
|
background: rgba(0, 0, 0, 0.5); // 添加半透明遮罩
|
|
|
|
.scan-frame {
|
|
width: 500rpx;
|
|
height: 500rpx;
|
|
position: relative;
|
|
background: transparent; // 扫描区域透明
|
|
box-shadow: 0 0 0 2000px rgba(0, 0, 0, 0.5); // 使用 box-shadow 创建外部遮罩
|
|
|
|
.scan-line {
|
|
width: 100%;
|
|
height: 4rpx;
|
|
background: linear-gradient(to right, transparent, #3EAB64, transparent);
|
|
position: absolute;
|
|
top: 0;
|
|
animation: scanMove 3s linear infinite;
|
|
box-shadow: 0 0 10rpx #3EAB64;
|
|
}
|
|
|
|
.corner {
|
|
position: absolute;
|
|
width: 40rpx;
|
|
height: 40rpx;
|
|
border: 6rpx solid #3EAB64;
|
|
box-shadow: 0 0 10rpx rgba(62, 171, 100, 0.5);
|
|
}
|
|
|
|
.top-left { top: -2rpx; left: -2rpx; border-right: none; border-bottom: none; }
|
|
.top-right { top: -2rpx; right: -2rpx; border-left: none; border-bottom: none; }
|
|
.bottom-left { bottom: -2rpx; left: -2rpx; border-right: none; border-top: none; }
|
|
.bottom-right { bottom: -2rpx; right: -2rpx; border-left: none; border-top: none; }
|
|
}
|
|
}
|
|
|
|
@keyframes scanMove {
|
|
0% { top: 0; opacity: 0; }
|
|
10% { opacity: 1; }
|
|
90% { opacity: 1; }
|
|
100% { top: 100%; opacity: 0; }
|
|
}
|
|
|
|
.scan-tip {
|
|
position: absolute;
|
|
top: 15%;
|
|
left: 0;
|
|
right: 0;
|
|
text-align: center;
|
|
color: #fff;
|
|
font-size: 28rpx;
|
|
padding: 0 40rpx;
|
|
line-height: 1.6;
|
|
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
|
|
z-index: 11;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.bottom-actions {
|
|
position: absolute;
|
|
bottom: 80rpx;
|
|
left: 0;
|
|
right: 0;
|
|
display: flex;
|
|
justify-content: space-around;
|
|
padding: 0 60rpx;
|
|
z-index: 20;
|
|
|
|
.action-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8rpx;
|
|
margin-bottom: 20rpx;
|
|
padding: 12rpx 16rpx;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border-radius: 16rpx;
|
|
backdrop-filter: blur(10rpx);
|
|
transition: all 0.3s ease;
|
|
min-width: 100rpx;
|
|
border: 1rpx solid rgba(255, 255, 255, 0.1);
|
|
|
|
.action-icon {
|
|
font-size: 40rpx;
|
|
line-height: 1;
|
|
}
|
|
|
|
text {
|
|
color: #fff;
|
|
font-size: 22rpx;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
&:active {
|
|
transform: scale(0.95);
|
|
background: rgba(62, 171, 100, 0.3);
|
|
}
|
|
}
|
|
}
|
|
|
|
.input-dialog {
|
|
width: 600rpx;
|
|
background: #fff;
|
|
padding: 40rpx;
|
|
border-radius: 24rpx;
|
|
|
|
.dialog-title {
|
|
font-size: 32rpx;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
margin-bottom: 40rpx;
|
|
color: #333;
|
|
}
|
|
|
|
.device-input {
|
|
height: 88rpx;
|
|
background: #F8F9FA;
|
|
border-radius: 12rpx;
|
|
padding: 0 24rpx;
|
|
font-size: 28rpx;
|
|
border: 1rpx solid #eee;
|
|
margin-bottom: 40rpx;
|
|
box-sizing: border-box;
|
|
|
|
&:focus {
|
|
border-color: #3EAB64;
|
|
background: #fff;
|
|
}
|
|
}
|
|
|
|
.dialog-btns {
|
|
display: flex;
|
|
gap: 20rpx;
|
|
|
|
button {
|
|
flex: 1;
|
|
height: 80rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 28rpx;
|
|
border-radius: 40rpx;
|
|
border: none;
|
|
transition: all 0.3s ease;
|
|
|
|
&:active {
|
|
transform: scale(0.95);
|
|
}
|
|
}
|
|
|
|
.cancel-btn {
|
|
background: #f5f5f5;
|
|
color: #666;
|
|
}
|
|
|
|
.confirm-btn {
|
|
background: #3EAB64;
|
|
color: #fff;
|
|
}
|
|
}
|
|
}
|
|
</style>
|