2025-12-16 12:33:02 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<BaseReport v-if="queryState === 'success'" :order-id="orderId" :order-no="orderNo" :feature="feature"
|
|
|
|
|
|
:reportData="reportData" :reportParams="reportParams" :reportName="reportName" :reportDateTime="reportDateTime"
|
|
|
|
|
|
:isEmpty="isEmpty" :isDone="isDone" :isExample="false" />
|
|
|
|
|
|
<div v-else-if="queryState === 'pending'" class="loading-container">
|
|
|
|
|
|
<div class="loading-spinner"></div>
|
|
|
|
|
|
<p>报告生成中,请稍候...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="p-4" v-else-if="queryState === 'failed'">
|
|
|
|
|
|
<LEmpty />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import LEmpty from "@/components/LEmpty.vue";
|
|
|
|
|
|
import { aesDecrypt } from "@/utils/crypto";
|
|
|
|
|
|
|
|
|
|
|
|
const AES_KEY = import.meta.env.VITE_INQUIRE_AES_KEY;
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
|
const feature = ref("");
|
|
|
|
|
|
const reportData = ref([]);
|
|
|
|
|
|
const reportParams = ref({});
|
|
|
|
|
|
const reportName = ref("");
|
|
|
|
|
|
const reportDateTime = ref(null);
|
|
|
|
|
|
const isEmpty = ref(false);
|
|
|
|
|
|
const isDone = ref(false);
|
|
|
|
|
|
const orderId = ref(null);
|
|
|
|
|
|
const orderNo = ref("");
|
|
|
|
|
|
const queryState = ref("");
|
|
|
|
|
|
const pollingInterval = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeMount(() => {
|
|
|
|
|
|
const query = new URLSearchParams(window.location.search);
|
|
|
|
|
|
orderNo.value = query.get("out_trade_no");
|
|
|
|
|
|
orderId.value = query.get("order_id");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!orderNo.value && !orderId.value) {
|
|
|
|
|
|
orderId.value = route.query.orderId;
|
|
|
|
|
|
orderNo.value = route.query.orderNo;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!orderId.value && !orderNo.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
getReport();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
if (pollingInterval.value) {
|
|
|
|
|
|
clearInterval(pollingInterval.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const getReport = async () => {
|
|
|
|
|
|
let queryUrl = "";
|
|
|
|
|
|
if (orderNo.value) {
|
|
|
|
|
|
queryUrl = `/query/orderNo/${orderNo.value}`;
|
|
|
|
|
|
} else if (orderId.value) {
|
|
|
|
|
|
queryUrl = `/query/orderId/${orderId.value}`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const { data, error } = await useApiFetch(queryUrl).get().json();
|
|
|
|
|
|
|
|
|
|
|
|
if (!AES_KEY) {
|
|
|
|
|
|
console.error("缺少解密密钥");
|
|
|
|
|
|
isEmpty.value = true;
|
|
|
|
|
|
isDone.value = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.value && !error.value) {
|
|
|
|
|
|
if (data.value.code === 200) {
|
|
|
|
|
|
let decryptedData = data.value.data;
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof decryptedData === "string") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const decryptedStr = aesDecrypt(decryptedData, AES_KEY);
|
|
|
|
|
|
decryptedData = JSON.parse(decryptedStr);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("报告数据解密失败", err);
|
|
|
|
|
|
isEmpty.value = true;
|
|
|
|
|
|
isDone.value = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!decryptedData) {
|
|
|
|
|
|
isEmpty.value = true;
|
|
|
|
|
|
isDone.value = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
queryState.value = decryptedData.query_state;
|
|
|
|
|
|
if (queryState.value === "success") {
|
|
|
|
|
|
feature.value = decryptedData.product || "";
|
|
|
|
|
|
|
|
|
|
|
|
const sortedQueryData = Array.isArray(decryptedData.query_data)
|
|
|
|
|
|
? [...decryptedData.query_data].sort((a, b) => {
|
|
|
|
|
|
return a.feature.sort - b.feature.sort;
|
|
|
|
|
|
})
|
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
|
|
reportData.value = sortedQueryData;
|
|
|
|
|
|
reportParams.value = decryptedData.query_params || {};
|
|
|
|
|
|
reportName.value = decryptedData.product_name || "";
|
|
|
|
|
|
reportDateTime.value = decryptedData.create_time || null;
|
|
|
|
|
|
isDone.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果成功,清除轮询
|
|
|
|
|
|
if (pollingInterval.value) {
|
|
|
|
|
|
clearInterval(pollingInterval.value);
|
|
|
|
|
|
pollingInterval.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (queryState.value === "pending") {
|
|
|
|
|
|
// 如果是pending状态且没有轮询,启动轮询
|
|
|
|
|
|
if (!pollingInterval.value) {
|
|
|
|
|
|
pollingInterval.value = setInterval(() => {
|
|
|
|
|
|
getReport();
|
|
|
|
|
|
}, 2000); // 每2秒轮询一次
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (queryState.value === "failed") {
|
|
|
|
|
|
isEmpty.value = true;
|
|
|
|
|
|
isDone.value = true;
|
|
|
|
|
|
// 如果失败,清除轮询
|
|
|
|
|
|
if (pollingInterval.value) {
|
|
|
|
|
|
clearInterval(pollingInterval.value);
|
|
|
|
|
|
pollingInterval.value = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (data.value.code === 200003) {
|
|
|
|
|
|
isEmpty.value = true;
|
|
|
|
|
|
isDone.value = true;
|
2026-02-02 15:02:01 +08:00
|
|
|
|
} else if (data.value.code === 200002 || data.value.code === 100005) {
|
|
|
|
|
|
// 200002:报告生成中;100005:兼容旧后端“报告未就绪”时的 DB_ERROR,按生成中轮询
|
|
|
|
|
|
queryState.value = "pending";
|
|
|
|
|
|
if (!pollingInterval.value) {
|
|
|
|
|
|
pollingInterval.value = setInterval(() => {
|
|
|
|
|
|
getReport();
|
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (data.value?.code === 200002) {
|
|
|
|
|
|
// 接口返回 200002 时可能走不到上面分支,单独处理
|
|
|
|
|
|
queryState.value = "pending";
|
|
|
|
|
|
if (!pollingInterval.value) {
|
|
|
|
|
|
pollingInterval.value = setInterval(() => {
|
|
|
|
|
|
getReport();
|
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (data.value?.code === 100005) {
|
|
|
|
|
|
// 兼容:报告未就绪时后端返回 100005,按“生成中”轮询
|
|
|
|
|
|
queryState.value = "pending";
|
|
|
|
|
|
if (!pollingInterval.value) {
|
|
|
|
|
|
pollingInterval.value = setInterval(() => {
|
|
|
|
|
|
getReport();
|
|
|
|
|
|
}, 2000);
|
2025-12-16 12:33:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.loading-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 100vh;
|
|
|
|
|
|
|
|
|
|
|
|
.loading-spinner {
|
|
|
|
|
|
width: 50px;
|
|
|
|
|
|
height: 50px;
|
|
|
|
|
|
border: 4px solid #f3f3f3;
|
|
|
|
|
|
border-top: 4px solid #3498db;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
|
0% {
|
|
|
|
|
|
transform: rotate(0deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
100% {
|
|
|
|
|
|
transform: rotate(360deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|