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.
 
 
 
 
 
 

876 lines
25 KiB

<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>
<div class="record">
如遇网络异常设备故障无法登录系统的情况<div @click="toSign">补录 >></div>
</div>
</div>
</div>
<!-- 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>
<!-- 手动搜索企业弹窗 -->
<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 filteredReasonOptions" :key="reason.value" class="reason-tag"
:class="{ active: selectedReason === reason.originalIndex }" @click="selectedReason = reason.originalIndex">
{{ reason.text }}
</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">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="" @load="onLoad"
:offset="50">
<div v-for="(item, index) in enterpriseList" :key="index" class="enterprise-item"
@click="selectEnterprise(item)">
<div class="item-qr">
<img v-if="item.qrCodeUrl" :src="item.qrCodeUrl" class="qr-image" />
<van-icon v-else name="qr" />
</div>
<div class="item-name">{{ item.enterpriseName }}</div>
</div>
<!-- 搜索不到企业时显示非深圳市注册企业提示 -->
<div v-if="enterpriseList.length === 0 && finished && !loading && searchKeyword"
class="no-result-section">
<div class="no-result-card">
<div class="no-result-icon">
</div>
<div class="no-result-title">非深圳市注册企业</div>
<van-button class="no-result-btn"
@click="handleOutOfCityEnterprise">市外注册企业信息录入 ></van-button>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</div>
</van-popup>
</div>
</template>
<script setup name="ScanEntQRCode">
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { showFailToast, showSuccessToast, showToast, showConfirmDialog } from 'vant';
import { baseUrl, appEnv } from '@/config/env.js';
import { getEntInfoByQRCode, queryEnterpriseListAndQrInfo, szyptGetToken } from '@/api/enforcement/index';
import { loadDict } from '@/util/dictUtil';
import wx from 'weixin-js-sdk';
const router = useRouter();
const route = useRoute();
// 获取执法类型参数
const enforceType = route.query.type;
// ========== 扫码相关 ==========
const showScanner = ref(false);
const fileInputRef = ref(null);
let html5QrCode = null;
/**
* 点击扫码入口
*/
const toScanPageView = () => {
console.log("appEnv: ", appEnv);
if (appEnv === 'dev') {
console.log("dev----------------------<")
openHtml5QrcodeScanner();
} else {
console.log("pro----------------------<")
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);
html5QrCode = null; // 启动失败,清除实例,避免销毁时调 stop 报错
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 = () => {
let fullUrl = window.location.href;
let code = fullUrl.substring(fullUrl.indexOf('code=') + 5, fullUrl.indexOf('&state='));
let paramUrl = window.location.href.split("#")[0];
let param = {code: code, url: paramUrl};
szyptGetToken(param, success => {
let resMsg = success.data;
console.log(resMsg);
wx.config({
// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
debug: false,
// 必填,公众号的唯一标识
appId: resMsg.corpid,
// 必填,生成签名的时间戳
timestamp: resMsg.timestamp,
// 必填,生成签名的随机串
nonceStr: resMsg.nonceStr,
// 必填,签名,见附录1
signature: resMsg.signature,
// 必填,需要使用的JS接口列表,所有JS接口列表见附录2
jsApiList: ["scanQRCode"],
});
wx.ready(() => {
//微信扫一扫请求操作
ClickScan()
});
wx.error((res) => {
console.log("获取失败", res);
});
}, error => {
});
};
const ClickScan = () => {
wx.scanQRCode({
needResult: 1, // 默认为0,扫描结果由微信处理,1则直接返回扫描结果,
scanType: ['qrCode', 'barCode'], // 可以指定扫二维码还是一维码,默认二者都有
success: (res) => {
// 页面跳转
console.log("=========== 扫码返回 ===========", res);
handleScanResult(res.resultStr);
},
error: function(res) {
console.log("错误回调",res)
if (res.errMsg.indexOf('function_not_exist') > 0) {
showFailToast('版本过低请升级')
}
},
cancel: (res) => {
console.log('err', res);
},
});
};
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);
// 不管解码结果是 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',
query: { type: enforceType, enterpriseNumber: data.enterpriseNumber }
});
} else {
showFailToast(res.msg || '查询企业信息失败');
}
}, (err) => {
showToast({ message: '', duration: 1 });
console.error('getEntInfoByQRCode 调用失败', err);
showFailToast('查询企业信息失败,请重试');
});
};
const isHttpLink = (url) => {
return url.indexOf('http://') === 0 || url.indexOf('https://') === 0;
};
// ========== 手动搜索企业弹窗 ==========
const showSearchModal = ref(false);
const selectedReason = ref(-1); // 默认未选中,字典加载后设置
const searchKeyword = ref('');
const enterpriseList = ref([]);
const loading = ref(false);
const finished = ref(false);
const error = ref(false);
const refreshing = ref(false);
// 分页参数
const pageNum = ref(1);
const pageSize = ref(20);
const reasonOptions = ref([]);
const filteredReasonOptions = computed(() => {
return reasonOptions.value
.map((item, index) => ({ ...item, originalIndex: index }))
.filter(item => item.text !== '补录' && item.value !== '补录');
});
const showSearchPopup = () => {
showSearchModal.value = true;
searchKeyword.value = '';
enterpriseList.value = [];
finished.value = false;
error.value = false;
pageNum.value = 1;
loading.value = true;
if (reasonOptions.value.length === 0) {
loadDict('no-scan-reason', (columns) => {
reasonOptions.value = columns;
const otherItem = filteredReasonOptions.value.find(item => item.text === '其他' || item.value === '其他');
selectedReason.value = otherItem ? otherItem.originalIndex : (filteredReasonOptions.value[0]?.originalIndex ?? 0);
fetchEnterprises();
});
} else {
if (selectedReason.value === -1) {
const otherItem = filteredReasonOptions.value.find(item => item.text === '其他' || item.value === '其他');
selectedReason.value = otherItem ? otherItem.originalIndex : (filteredReasonOptions.value[0]?.originalIndex ?? 0);
}
fetchEnterprises();
}
};
const closeSearchModal = () => {
showSearchModal.value = false;
searchKeyword.value = '';
enterpriseList.value = [];
finished.value = false;
error.value = false;
pageNum.value = 1;
refreshing.value = false;
};
const handleSearch = () => {
if (!searchKeyword.value.trim()) {
showToast('请输入企业名称或统一信用代码');
return;
}
enterpriseList.value = [];
finished.value = false;
error.value = false;
pageNum.value = 1;
loading.value = true;
fetchEnterprises();
};
const clearSearch = () => {
searchKeyword.value = '';
enterpriseList.value = [];
finished.value = false;
error.value = false;
pageNum.value = 1;
loading.value = true;
fetchEnterprises();
};
const fetchEnterprises = () => {
loading.value = true;
error.value = false;
const params = {
pageSize: pageSize.value,
pageNum: pageNum.value,
orderByColumn: 'createTime',
isAsc: 'desc',
searchValue: searchKeyword.value.trim() || undefined
};
queryEnterpriseListAndQrInfo(params, (res) => {
loading.value = false;
if (res.code === 200) {
const rows = res.rows || [];
const total = res.total || 0;
if (refreshing.value) {
enterpriseList.value = rows;
refreshing.value = false;
} else {
enterpriseList.value = [...enterpriseList.value, ...rows];
}
if (total <= pageSize.value * pageNum.value) {
finished.value = true;
} else {
pageNum.value++;
}
} else {
showFailToast(res.msg || '搜索企业失败');
error.value = true;
finished.value = true;
}
}, (err) => {
loading.value = false;
error.value = true;
finished.value = true;
refreshing.value = false;
console.error('搜索企业失败', err);
showFailToast('搜索企业失败,请重试');
});
};
const onLoad = () => {
if (refreshing.value) return;
fetchEnterprises();
};
const onRefresh = () => {
pageNum.value = 1;
enterpriseList.value = [];
finished.value = false;
loading.value = true;
fetchEnterprises();
};
const selectEnterprise = (item) => {
console.log('选中企业:', item);
// 保存无法扫码原因到 sessionStorage
const selectedReasonText = reasonOptions.value[selectedReason.value]?.text || '';
sessionStorage.setItem('scanReason', selectedReasonText);
showSearchModal.value = false;
router.push({
path: '/enforcement/enterpriseScanPreview',
query: {
enterpriseNumber: item.enterpriseNumber,
type: enforceType,
isManualSearch: 'true'
}
});
};
// 市外注册企业信息录入
const handleOutOfCityEnterprise = () => {
const selectedReasonText = reasonOptions.value[selectedReason.value]?.text || '';
sessionStorage.setItem('scanReason', selectedReasonText);
showSearchModal.value = false;
router.push({
path: '/enforcement/outOfCityEnterprise',
query: { isManualSearch: 'true' }
});
};
// ========== 页面跳转 ==========
const toSign = () => {
// 保存原因为"补录"
sessionStorage.setItem('scanReason', '补录');
router.push({
path: '/enforcement/enforcementRegister',
query: {
reason: '补录',
checkCss: 9,
isNoScan: true,
enterpriseName: '',
enterpriseNumber: '',
type: enforceType
},
});
};
// ========== 生命周期 ==========
onMounted(() => {
loadDict('no-scan-reason', (columns) => {
reasonOptions.value = columns;
const otherItem = filteredReasonOptions.value.find(item => item.text === '其他' || item.value === '其他');
if (otherItem) {
selectedReason.value = otherItem.originalIndex;
} else if (filteredReasonOptions.value.length > 0) {
selectedReason.value = filteredReasonOptions.value[0].originalIndex;
}
});
});
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: #fbfdff;
.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 {
color: #287eef;
display: inline-block;
}
/* ========== 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;
}
.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: #fbfdff;
border-radius: 0.1rem;
display: flex;
align-items: center;
justify-content: center;
margin-right: 0.3rem;
flex-shrink: 0;
overflow: hidden;
}
.item-qr .van-icon {
font-size: 0.6rem;
color: #1367fe;
}
.item-qr .qr-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.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;
}
/* 搜索无结果提示卡片 */
.no-result-section {
position: relative;
margin: 0px 10px;
}
.no-result-card {
display: flex;
flex-direction: column;
background: linear-gradient(135deg, #ebf3fb 0%, #f5f9ff 100%);
border-radius: 20px;
overflow: hidden;
margin-top: 80px;
z-index: 2;
padding:20px 30px;
}
.no-result-icon {
background: url('@/assets/images/com_sign.png') no-repeat center center;
background-size: 100% 100%;
width: 124px;
height: 124px;
position: absolute;
right: 0px;
top: -30px;
z-index: 4;
opacity: 0.5;
}
.no-result-title {
font-size: 16px;
color: #000;
font-weight: 580;
margin-bottom: 0.35rem;
z-index: 1;
}
.no-result-btn {
background: #fff;
color: #1367fe;
border-radius: 0.3rem;
font-size: 14px;
padding:5px 10px;
z-index: 1;
width: 182px;
height: 30px;
margin: 16px 0px;
:deep(.van-button__text) {
padding: 0;
}
}
</style>