You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

797 lines
22 KiB

6 months ago
<template>
<div class="home app_content">
<!-- 中间 -->
<div class="warp_body">
<div class="white-body">
<div class="boutique_box">
<div class="boutique_body">
<div class="box-qr-code" @click="toScanPageView">
</div>
</div>
<div class="scan-note">扫一扫企业二维码,即可查看企业信息</div>
<div class="nocode" @click="showSearchPopup()">手动登记,请输入关键字搜索企业 >></div>
</div>
6 months ago
6 months ago
<div class="record">
如遇网络异常、设备故障无法登录系统的情况,<div @click="toSign">补录 >></div>
6 months ago
</div>
</div>
</div>
6 months ago
6 months ago
<!-- html5-qrcode 扫码弹层(仅 dev 环境) -->
<van-overlay :show="showScanner" @click="closeScannerOverlay" z-index="9999">
<div class="scanner-wrapper" @click.stop>
<div class="scanner-header">
<span class="scanner-title">扫描二维码</span>
<van-icon name="cross" class="scanner-close" @click="closeScanner" />
</div>
<div id="qr-reader" class="qr-reader-box"></div>
<div class="scanner-tip">请将二维码置于框内,自动识别</div>
</div>
</van-overlay>
<!-- 隐藏的文件选择(摄像头不可用时的后备方案:上传二维码图片识别) -->
<input
ref="fileInputRef"
type="file"
accept="image/*"
style="display: none;"
@change="onFileSelected"
/>
<!-- html5-qrcode scanFile 需要的隐藏容器 -->
<div id="qr-reader-file-scan" style="display: none;"></div>
6 months ago
<!-- 手动搜索企业弹窗 -->
<van-popup
v-model:show="showSearchModal"
position="bottom"
:style="{ height: '80%' }"
closeable
close-icon="cross"
:close-on-click-overlay="false"
@close="closeSearchModal"
>
<div class="search-popup">
<!-- 无法扫码原因选择 -->
<div class="reason-section">
<div class="section-label">请选择无法扫码的原因:</div>
<div class="reason-tags">
<div
v-for="(reason, index) in reasonOptions"
:key="index"
class="reason-tag"
:class="{ active: selectedReason === index }"
@click="selectedReason = index"
>
{{ reason }}
</div>
</div>
</div>
<!-- 搜索区域 -->
<div class="search-section">
<div class="section-label">请搜索企业名称:</div>
<div class="search-bar">
<div class="search-input-box">
<van-icon name="search" class="search-icon" />
<input
v-model="searchKeyword"
type="text"
class="search-input"
placeholder="请输入企业名称/统一信用代码"
/>
<van-icon
v-if="searchKeyword"
name="clear"
class="clear-icon"
@click="clearSearch"
/>
</div>
<van-button type="primary" class="search-btn" @click="handleSearch">搜索</van-button>
</div>
</div>
<!-- 企业列表 -->
<div class="enterprise-list">
<div
v-for="(item, index) in enterpriseList"
:key="index"
class="enterprise-item"
@click="selectEnterprise(item)"
>
<div class="item-qr">
<van-icon name="qr" />
</div>
<div class="item-name">{{ item.enterpriseName }}</div>
</div>
<van-empty v-if="enterpriseList.length === 0 && !loading && searchKeyword" description="暂无匹配的企业" />
<van-loading v-if="loading" class="loading-more" />
</div>
</div>
</van-popup>
6 months ago
</div>
6 months ago
</template>
6 months ago
<script setup name="ScanEntQRCode">
import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { showFailToast, showSuccessToast, showToast, showConfirmDialog } from 'vant';
import { baseUrl, appEnv } from '@/config/env.js';
import { getEntInfoByQRCode, searchEnterprise } from '@/api/enforcement/index';
6 months ago
import wx from 'weixin-js-sdk';
const router = useRouter();
// ========== 扫码相关 ==========
const showScanner = ref(false);
const fileInputRef = ref(null);
let html5QrCode = null;
/**
* 点击扫码入口
*/
const toScanPageView = () => {
if (appEnv === 'dev') {
openHtml5QrcodeScanner();
} else {
openWxScan();
}
};
// ---------- dev 环境:html5-qrcode ----------
const openHtml5QrcodeScanner = async () => {
showScanner.value = true;
await nextTick();
const { Html5Qrcode } = await import('html5-qrcode');
html5QrCode = new Html5Qrcode('qr-reader');
try {
await html5QrCode.start(
{ facingMode: 'environment' },
{
fps: 10,
qrbox: { width: 250, height: 250 },
},
onScanSuccess,
onScanFailure
);
} catch (err) {
console.error('摄像头启动失败', err);
6 months ago
html5QrCode = null; // 启动失败,清除实例,避免销毁时调 stop 报错
6 months ago
showScanner.value = false;
// 摄像头不可用时,提供上传二维码图片的后备方案
showConfirmDialog({
title: '摄像头不可用',
message: '无法启动摄像头,是否选择一张二维码图片进行识别?',
confirmButtonText: '选择图片',
cancelButtonText: '取消',
}).then(() => {
fileInputRef.value?.click();
}).catch(() => {
// 用户取消
});
}
};
const onScanSuccess = (decodedText, decodedResult) => {
console.log('扫码结果:', decodedText);
closeScanner();
handleScanResult(decodedText);
};
const onScanFailure = (error) => {
// 扫描中,忽略连续失败的回调
};
const closeScanner = async () => {
if (html5QrCode) {
try {
await html5QrCode.stop();
} catch (e) {
console.warn('停止扫码器异常', e);
}
html5QrCode = null;
}
showScanner.value = false;
};
const closeScannerOverlay = () => {
closeScanner();
};
// ---------- 后备方案:上传二维码图片识别 ----------
const onFileSelected = async (event) => {
const file = event.target.files?.[0];
if (!file) return;
try {
const { Html5Qrcode } = await import('html5-qrcode');
const html5Qr = new Html5Qrcode('qr-reader-file-scan');
const decodedText = await html5Qr.scanFile(file, /* showImage= */ false);
console.log('图片识别结果:', decodedText);
handleScanResult(decodedText);
} catch (err) {
console.error('图片二维码识别失败', err);
showFailToast('无法从图片中识别二维码,请确认图片包含有效的二维码');
} finally {
// 重置 input,允许重复选择同一文件
if (fileInputRef.value) {
fileInputRef.value.value = '';
}
}
};
// ---------- prod 环境:wx.scanQRCode ----------
const openWxScan = () => {
// TODO: 对接实际微信 JSSDK token 接口后启用
// 当前仅预留调用骨架,需配合服务端 szyptGetToken 接口获取签名
const fullUrl = window.location.href;
const paramUrl = fullUrl.split('#')[0];
showToast('正在调起微信扫码...');
// 以下为 wx.config + wx.scanQRCode 的标准流程
// 实际使用时需先调用服务端接口获取 config 参数
/*
szyptGetToken({ url: paramUrl }, (res) => {
const data = res.data;
wx.config({
debug: false,
appId: data.corpid,
timestamp: data.timestamp,
nonceStr: data.nonceStr,
signature: data.signature,
jsApiList: ['scanQRCode'],
});
wx.ready(() => {
wxClickScan();
});
wx.error((err) => {
console.error('wx.config 失败', err);
showFailToast('微信配置失败,请重试');
});
});
*/
// 开发阶段直接尝试调用(在微信浏览器内有效)
wxClickScan();
};
const wxClickScan = () => {
wx.scanQRCode({
needResult: 1,
scanType: ['qrCode', 'barCode'],
success: (res) => {
console.log('wx 扫码返回:', res);
handleScanResult(res.resultStr);
},
error: (res) => {
console.error('wx 扫码错误:', res);
if (res.errMsg && res.errMsg.indexOf('function_not_exist') > 0) {
showFailToast('版本过低请升级');
}
},
cancel: () => {
console.log('用户取消扫码');
},
});
};
// ========== 扫码结果处理 ==========
const handleScanResult = (resultStr) => {
console.log('========== 扫码结果处理 ==========');
console.log('原始内容:', resultStr);
6 months ago
// 不管解码结果是 HTTP 链接还是纯文本,统一交给后端解析
showToast({ message: '正在查询企业信息...', duration: 0 });
getEntInfoByQRCode(resultStr, (res) => {
showToast({ message: '', duration: 1 }); // 关闭 loading
if (res.code === 200) {
const data = res.data;
console.log('企业信息查询成功:', data);
if (!data.qrCodeValid) {
showFailToast(data.qrCodeMessage || '二维码已过期');
}
// 将企业信息存入 sessionStorage,预览页直接使用,无需再调接口
sessionStorage.setItem('enterpriseScanData', JSON.stringify(data));
// 跳转企业详情预览页
router.push({ path: '/enforcement/enterpriseScanPreview' });
} else {
showFailToast(res.msg || '查询企业信息失败');
6 months ago
}
6 months ago
}, (err) => {
showToast({ message: '', duration: 1 });
console.error('getEntInfoByQRCode 调用失败', err);
showFailToast('查询企业信息失败,请重试');
});
6 months ago
};
const isHttpLink = (url) => {
return url.indexOf('http://') === 0 || url.indexOf('https://') === 0;
};
// ========== 手动搜索企业弹窗 ==========
const showSearchModal = ref(false);
const selectedReason = ref(4); // 默认选中"其他"
const searchKeyword = ref('');
const enterpriseList = ref([]);
const loading = ref(false);
const finished = ref(false);
const error = ref(false);
const reasonOptions = [
'企业拒绝出示监管码',
'系统未能有效生成监管码',
'扫码未能显示企业信息',
'补录',
'其他'
];
const showSearchPopup = () => {
showSearchModal.value = true;
searchKeyword.value = '';
enterpriseList.value = [];
finished.value = false;
error.value = false;
nextTick(() => {
loadEnterprises();
});
};
const closeSearchModal = () => {
showSearchModal.value = false;
searchKeyword.value = '';
enterpriseList.value = [];
finished.value = false;
error.value = false;
};
const handleSearch = () => {
if (!searchKeyword.value.trim()) {
showToast('请输入企业名称或统一信用代码');
return;
}
enterpriseList.value = [];
finished.value = false;
error.value = false;
loadEnterprises();
};
const clearSearch = () => {
searchKeyword.value = '';
enterpriseList.value = [];
finished.value = false;
error.value = false;
loadEnterprises();
};
const loadEnterprises = () => {
loading.value = true;
// 测试数据 - 后端接口对接后可删除此段
const mockData = [
{
enterpriseName: '深圳市南山区椿立嘉源麻辣烫店',
enterpriseNumber: '92440300MA5FXXXX1',
enterpriseType: '个体工商户',
legalPerson: '张三',
registeredCapital: '5万元',
establishmentTime: '2020-03-15',
operatingTerm: '长期',
enterpriseAddress: '深圳市南山区XX街道XX路XX号',
businessScope: '餐饮服务',
enterpriseTel: '13800138001',
industry: '餐饮业',
},
{
enterpriseName: '深圳伊腾迪新能源有限公司',
enterpriseNumber: '91440300MA5FXXXX2',
enterpriseType: '有限责任公司',
legalPerson: '李四',
registeredCapital: '500万元',
establishmentTime: '2018-06-20',
operatingTerm: '2018-06-20至2038-06-20',
enterpriseAddress: '深圳市南山区XX科技园XX栋',
businessScope: '新能源技术开发、技术咨询',
enterpriseTel: '13800138002',
industry: '新能源',
},
{
enterpriseName: '深圳市永顺新绣花有限公司',
enterpriseNumber: '91440300MA5FXXXX3',
enterpriseType: '有限责任公司',
legalPerson: '王五',
registeredCapital: '100万元',
establishmentTime: '2015-09-10',
operatingTerm: '2015-09-10至2035-09-10',
enterpriseAddress: '深圳市宝安区XX工业区XX号',
businessScope: '绣花加工、服装辅料销售',
enterpriseTel: '13800138003',
industry: '制造业',
},
{
enterpriseName: '深圳市中棋金业有限公司',
enterpriseNumber: '91440300MA5FXXXX4',
enterpriseType: '有限责任公司',
legalPerson: '赵六',
registeredCapital: '1000万元',
establishmentTime: '2019-01-08',
operatingTerm: '2019-01-08至2039-01-08',
enterpriseAddress: '深圳市福田区XX金融中心XX层',
businessScope: '黄金饰品销售、投资管理',
enterpriseTel: '13800138004',
industry: '金融业',
},
{
enterpriseName: '深圳市百合隆科技有限公司',
enterpriseNumber: '91440300MA5FXXXX5',
enterpriseType: '有限责任公司',
legalPerson: '孙七',
registeredCapital: '200万元',
establishmentTime: '2021-04-22',
operatingTerm: '2021-04-22至2041-04-22',
enterpriseAddress: '深圳市龙岗区XX科技产业园XX栋',
businessScope: '电子产品研发、销售',
enterpriseTel: '13800138005',
industry: '科技服务业',
},
];
// 模拟网络请求延迟
setTimeout(() => {
loading.value = false;
if (searchKeyword.value.trim()) {
const keyword = searchKeyword.value.trim().toLowerCase();
const filtered = mockData.filter(
(item) =>
item.enterpriseName.toLowerCase().includes(keyword) ||
item.enterpriseNumber.toLowerCase().includes(keyword)
);
enterpriseList.value = filtered;
} else {
enterpriseList.value = mockData;
}
finished.value = true;
}, 500);
// 后端接口对接后使用以下代码替换上面的测试数据
// searchEnterprise(searchKeyword.value, (res) => {
// loading.value = false;
// if (res.code === 200) {
// const data = res.data || [];
// enterpriseList.value = data;
// finished.value = true;
// } else {
// showFailToast(res.msg || '搜索企业失败');
// error.value = true;
// }
// }, (err) => {
// loading.value = false;
// error.value = true;
// console.error('搜索企业失败', err);
// showFailToast('搜索企业失败,请重试');
// });
};
const onLoad = () => {
// 滚动加载,当前简单实现,如需分页可扩展
finished.value = true;
};
const selectEnterprise = (item) => {
console.log('选中企业:', item);
sessionStorage.setItem('enterpriseScanData', JSON.stringify(item));
showSearchModal.value = false;
router.push({ path: '/enforcement/enterpriseScanPreview' });
};
6 months ago
// ========== 页面跳转 ==========
const toSign = () => {
router.push({
path: '/enforcement/enforcementRegister',
6 months ago
query: {
reason: '补录',
checkCss: 9,
isNoScan: true,
enterpriseName: '',
enterpriseNumber: '',
6 months ago
},
});
};
// ========== 生命周期 ==========
onBeforeUnmount(() => {
// 页面销毁时确保关闭摄像头
if (html5QrCode) {
html5QrCode.stop().catch(() => {});
html5QrCode = null;
}
});
</script>
<style lang="less" scoped>
.home {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background-color: #ebf3fb;
.warp_body {
position: relative;
background-image: linear-gradient(to bottom, #ebf3fb, transparent);
}
.boutique_box {
position: relative;
top: 25%;
z-index: 100;
width: 100%;
.boutique_body {
padding: 0 0 0.27rem 0;
display: flex;
justify-content: center;
}
.box-qr-code {
width: 52%;
text-align: right;
height: 6rem;
background: url("../../assets/images/scan-btn.jpg") no-repeat center center;
background-size: 100%;
}
}
}
.white-body {
position: relative;
margin: 0 3%;
width: 94%;
height: 99%;
background-color: #fff;
border-radius: 0.27rem;
}
.scan-note {
width: 100%;
color: #d3d0d0;
font-size: 0.35rem;
text-align: center;
line-height: 2;
}
.nocode {
margin: 15% 0 0;
width: 100%;
color: #fb7905;
font-size: 0.42rem;
text-align: center;
line-height: 5;
}
.record {
position: absolute;
bottom: 2rem;
color: #bbbaba;
font-size: 0.35rem;
width: 100%;
text-align: center;
white-space: nowrap;
}
.record div {
6 months ago
color: #287eef;
display: inline-block;
6 months ago
}
/* ========== html5-qrcode 扫码弹层样式 ========== */
.scanner-wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 500px;
background: #fff;
border-radius: 0.27rem;
overflow: hidden;
}
.scanner-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem 0.4rem;
background: #1367fe;
color: #fff;
}
.scanner-title {
font-size: 0.42rem;
font-weight: 600;
}
.scanner-close {
font-size: 0.5rem;
cursor: pointer;
}
.qr-reader-box {
width: 100%;
min-height: 300px;
}
6 months ago
6 months ago
.scanner-tip {
padding: 0.25rem 0;
text-align: center;
font-size: 0.32rem;
color: #999;
}
/* ========== 手动搜索企业弹窗样式 ========== */
.search-popup {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
padding: 0.3rem 0.4rem 0;
box-sizing: border-box;
position: relative;
}
:deep(.van-popup__close-icon--top-right) {
top: 0.3rem;
right: 0.3rem;
color: #000;
font-size: 0.5rem;
z-index: 10;
}
.section-label {
font-size: 0.35rem;
color: #000;
margin-bottom: 0.25rem;
font-weight: 500;
}
.reason-section {
margin-bottom: 0.4rem;
}
.reason-tags {
display: flex;
flex-wrap: wrap;
gap: 0.2rem;
}
.reason-tag {
padding: 0.12rem 0.3rem;
font-size: 0.3rem;
color: #999;
background: #f5f5f5;
border-radius: 0.4rem;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.reason-tag.active {
background: #e6f0fd;
color: #1367fe;
}
.search-section {
margin-bottom: 0.4rem;
}
.search-bar {
display: flex;
align-items: center;
gap: 0;
}
.search-input-box {
flex: 1;
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 0.1rem 0 0 0.1rem;
padding: 0 0.25rem;
height: 0.9rem;
}
.search-icon {
font-size: 0.4rem;
color: #bbb;
margin-right: 0.15rem;
flex-shrink: 0;
}
.search-input {
flex: 1;
border: none;
background: transparent;
font-size: 0.35rem;
color: #333;
outline: none;
}
.search-input::placeholder {
color: #bbb;
}
.clear-icon {
font-size: 0.4rem;
color: #bbb;
cursor: pointer;
margin-left: 0.1rem;
flex-shrink: 0;
}
.search-btn {
height: 0.9rem;
padding: 0 0.4rem;
font-size: 0.35rem;
border-radius: 0 0.1rem 0.1rem 0;
background: #1367fe;
border: none;
flex-shrink: 0;
}
.enterprise-list {
flex: 1;
overflow-y: auto;
padding-top: 0.2rem;
}
.enterprise-item {
display: flex;
// align-items: center;
padding: 0.3rem 0;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
}
.enterprise-item:last-child {
border-bottom: none;
}
.item-qr {
width: 1.2rem;
height: 1.2rem;
background: #ebf3fb;
border-radius: 0.1rem;
display: flex;
align-items: center;
justify-content: center;
margin-right: 0.3rem;
flex-shrink: 0;
}
.item-qr .van-icon {
font-size: 0.6rem;
color: #1367fe;
}
.item-name {
flex: 1;
font-size: 0.35rem;
color: #000;
line-height: 1.4;
margin-top: 5px;
}
.loading-more {
padding: 0.3rem 0;
text-align: center;
}
6 months ago
</style>