version temp2
This commit is contained in:
@@ -1,10 +1,32 @@
|
||||
<script setup>
|
||||
import ShareReportButton from "./ShareReportButton.vue";
|
||||
import TitleBanner from "./TitleBanner.vue";
|
||||
import VerificationCard from "./VerificationCard.vue";
|
||||
import StyledTabs from "./StyledTabs.vue";
|
||||
import { splitDWBG8B4DForTabs } from '@/ui/CDWBG8B4D/utils/simpleSplitter.js';
|
||||
import { splitDWBG6A2CForTabs } from '@/ui/DWBG6A2C/utils/simpleSplitter.js';
|
||||
import { splitCJRZQ5E9FForTabs } from '@/ui/CJRZQ5E9F/utils/simpleSplitter.js';
|
||||
import { splitCQYGL3F8EForTabs } from '@/ui/CQYGL3F8E/utils/simpleSplitter.js';
|
||||
|
||||
// 动态导入产品背景图片的函数
|
||||
const loadProductBackground = async (productType) => {
|
||||
try {
|
||||
switch (productType) {
|
||||
case 'companyinfo':
|
||||
return (await import("@/assets/images/report/xwqy_report_bg.png")).default;
|
||||
case 'preloanbackgroundcheck':
|
||||
return (await import("@/assets/images/report/dqfx_report_bg.png")).default;
|
||||
case 'personalData':
|
||||
return (await import("@/assets/images/report/grdsj_report_bg.png")).default;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load background image for ${productType}:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
isShare: {
|
||||
type: Boolean,
|
||||
@@ -48,6 +70,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isExample: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 使用toRefs将props转换为组件内的ref
|
||||
@@ -59,11 +85,80 @@ const {
|
||||
reportDateTime,
|
||||
isEmpty,
|
||||
isDone,
|
||||
isExample,
|
||||
} = toRefs(props);
|
||||
|
||||
const active = ref(null);
|
||||
const backgroundContainerRef = ref(null); // 背景容器的引用
|
||||
|
||||
const reportScore = ref(0); // 默认分数
|
||||
const productBackground = ref('');
|
||||
const backgroundHeight = ref(0);
|
||||
const imageAspectRatio = ref(0); // 缓存图片宽高比
|
||||
const MAX_BACKGROUND_HEIGHT = 211; // 最大背景高度,防止图片过高变形
|
||||
|
||||
// 计算背景高度
|
||||
const calculateBackgroundHeight = () => {
|
||||
if (imageAspectRatio.value > 0) {
|
||||
// 获取容器的实际宽度,而不是整个窗口宽度
|
||||
const containerWidth = backgroundContainerRef.value
|
||||
? backgroundContainerRef.value.offsetWidth
|
||||
: window.innerWidth;
|
||||
const calculatedHeight = containerWidth * imageAspectRatio.value;
|
||||
// 限制最大高度,防止图片过高
|
||||
backgroundHeight.value = Math.min(calculatedHeight, MAX_BACKGROUND_HEIGHT);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载背景图片并计算高度
|
||||
const loadBackgroundImage = async () => {
|
||||
const background = await loadProductBackground(feature.value);
|
||||
productBackground.value = background || '';
|
||||
|
||||
// 加载图片后计算高度
|
||||
if (background) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
// 缓存图片宽高比
|
||||
imageAspectRatio.value = img.height / img.width;
|
||||
// 图片加载完成后,等待下一帧再计算高度,确保容器已渲染
|
||||
nextTick(() => {
|
||||
calculateBackgroundHeight();
|
||||
});
|
||||
};
|
||||
img.src = background;
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖定时器
|
||||
let resizeTimer = null;
|
||||
|
||||
// 在组件挂载时加载背景图
|
||||
onMounted(async () => {
|
||||
console.log("isExample", isExample.value);
|
||||
await loadBackgroundImage();
|
||||
|
||||
// 监听窗口大小变化,重新计算高度
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 处理窗口大小变化(带防抖)
|
||||
const handleResize = () => {
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer);
|
||||
}
|
||||
resizeTimer = setTimeout(() => {
|
||||
calculateBackgroundHeight();
|
||||
}, 100); // 100ms 防抖延迟
|
||||
};
|
||||
|
||||
// 组件卸载时移除监听器
|
||||
onUnmounted(() => {
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer);
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 处理数据拆分(支持DWBG8B4D、DWBG6A2C、CJRZQ5E9F和CQYGL3F8E)
|
||||
const processedReportData = computed(() => {
|
||||
@@ -75,31 +170,50 @@ const processedReportData = computed(() => {
|
||||
// 拆分DWBG6A2C数据
|
||||
data = splitDWBG6A2CForTabs(data);
|
||||
|
||||
// 拆分CJRZQ5E9F数据
|
||||
data = splitCJRZQ5E9FForTabs(data);
|
||||
// // 拆分CJRZQ5E9F数据
|
||||
// data = splitCJRZQ5E9FForTabs(data);
|
||||
|
||||
// 拆分CQYGL3F8E数据
|
||||
data = splitCQYGL3F8EForTabs(data);
|
||||
|
||||
// 过滤掉在featureMap中没有对应的项
|
||||
return data.filter(item => featureMap[item.data.apiID]);
|
||||
});
|
||||
|
||||
watch(reportData, () => {
|
||||
reportScore.value = calculateScore(reportData.value);
|
||||
// 获取产品背景图片
|
||||
const getProductBackground = computed(() => productBackground.value);
|
||||
|
||||
// 背景图片容器样式
|
||||
const backgroundContainerStyle = computed(() => {
|
||||
if (backgroundHeight.value > 0) {
|
||||
return {
|
||||
height: `${backgroundHeight.value}px`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
height: '180px', // 默认高度
|
||||
};
|
||||
});
|
||||
|
||||
// 背景图片样式
|
||||
const backgroundImageStyle = computed(() => {
|
||||
if (getProductBackground.value) {
|
||||
return {
|
||||
backgroundImage: `url(${getProductBackground.value})`,
|
||||
backgroundSize: '100% auto', // 宽度100%,高度自动保持比例
|
||||
backgroundPosition: 'center top',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
overflow: 'hidden', // 超出部分裁剪
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const featureMap = {
|
||||
IVYZ5733: {
|
||||
name: "婚姻状态",
|
||||
component: defineAsyncComponent(() => import("@/ui/CIVYZ5733.vue")),
|
||||
remark: '查询结果为"未婚或尚未登记结婚"时,表示婚姻登记处暂无相关的登记记录。婚姻状态信息由婚姻登记处逐级上报,可能存在数据遗漏或更新滞后。当前可查询的婚姻状态包括:未婚或尚未登记结婚、已婚、离异。如您对查询结果有疑问,请联系客服反馈。',
|
||||
},
|
||||
// IVYZ81NC
|
||||
IVYZ81NC: {
|
||||
name: "婚姻状况",
|
||||
component: defineAsyncComponent(() => import("@/ui/CIVYZ81NC.vue")),
|
||||
remark: '查询结果为"未婚或尚未登记结婚"时,表示婚姻登记处暂无相关的登记记录。婚姻状态信息由婚姻登记处逐级上报,可能存在数据遗漏或更新滞后。当前可查询的婚姻状态包括:未婚或尚未登记结婚、已婚、离异。如您对查询结果有疑问,请联系客服反馈。',
|
||||
},
|
||||
JRZQ0A03: {
|
||||
name: "借贷申请记录",
|
||||
component: defineAsyncComponent(() =>
|
||||
@@ -402,374 +516,199 @@ const maskValue = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
// 计算综合评分的函数
|
||||
const calculateScore = (reportData) => {
|
||||
// 从0分开始(0分表示无风险)
|
||||
let score = 0;
|
||||
// 最高分为90分(90分表示最高风险)
|
||||
const maxScore = 90;
|
||||
// ==================== 新评分系统 ====================
|
||||
// Feature 风险等级配置(权重越高表示风险越大,最终分数越高越安全)
|
||||
const featureRiskLevels = {
|
||||
// 🔴 高风险类 - 权重 10
|
||||
'FLXG0V4B': 20, // 司法涉诉
|
||||
'FLXG3D56': 10, // 违约失信
|
||||
'JRZQ4AA8': 10, // 还款压力
|
||||
|
||||
// 定义各接口的相对风险权重比例
|
||||
const relativeWeights = {
|
||||
// 关键风险指标(高优先级)
|
||||
FLXG0V3B: 250, // 不良记录
|
||||
FLXG3D56: 100, // 违约异常
|
||||
FLXG0V4B: 400, // 司法涉诉
|
||||
G35SC01: 20, // 司法涉诉(次要)
|
||||
Q23SC01: 50, // 企业涉诉
|
||||
FIN019: 100, // 银行卡黑名单
|
||||
// 🟠 中高风险类 - 权重 7
|
||||
'JRZQ0A03': 7, // 借贷申请记录
|
||||
'JRZQ8203': 7, // 借贷行为记录
|
||||
'JRZQ4B6C': 7, // 信贷表现
|
||||
'BehaviorRiskScan': 7, // 风险行为扫描
|
||||
|
||||
// 高风险指标(中优先级)
|
||||
JRZQ0A03: 40, // 借贷申请记录
|
||||
JRZQ8203: 40, // 借贷行为记录
|
||||
FLXG54F5: 70, // 手机号码风险
|
||||
// 🟡 中风险类 - 权重 5
|
||||
'QYGL3F8E': 5, // 人企关系加强版
|
||||
'QCXG7A2B': 5, // 名下车辆
|
||||
'JRZQ09J8': 5, // 收入评估
|
||||
|
||||
// 中风险指标(低优先级)
|
||||
YYSYF7DB: 50, // 手机二次卡
|
||||
YYSY4B37: 50, // 手机在网时长
|
||||
QYGLB4C0: 50, // 人企关系
|
||||
JRZQ4B6C: 60, // 信贷表现
|
||||
JRZQ09J8: 40, // 收入评估
|
||||
// 🔵 低风险类 - 权重 3
|
||||
'IVYZ5733': 3, // 婚姻状态
|
||||
'IVYZ9A2B': 3, // 学历信息
|
||||
|
||||
// 验证指标(最低优先级)
|
||||
YYSY6F2E: 25, // 手机三要素
|
||||
IVYZ0B03: 25, // 手机号二要素
|
||||
KZEYS: 25, // 身份证二要素
|
||||
JRZQDCBE: 25, // 银行卡四要素核验
|
||||
// 📊 复合报告类 - 按子模块动态计算
|
||||
'DWBG8B4D': 0, // 谛听多维报告(由子模块计算)
|
||||
'DWBG6A2C': 0, // 司南报告(由子模块计算)
|
||||
'JRZQ5E9F': 0, // 贷款风险评估(由子模块计算)
|
||||
|
||||
// 谛听多维报告子模块
|
||||
'DWBG8B4D_Overview': 10,
|
||||
'DWBG8B4D_ElementVerification': 4,
|
||||
'DWBG8B4D_Identity': 4,
|
||||
'DWBG8B4D_RiskWarning': 10,
|
||||
'DWBG8B4D_OverdueRisk': 9,
|
||||
'DWBG8B4D_LoanEvaluation': 7,
|
||||
'DWBG8B4D_LeasingRisk': 6,
|
||||
'DWBG8B4D_RiskSupervision': 8,
|
||||
'DWBG8B4D_RiskWarningTab': 9,
|
||||
|
||||
// 司南报告子模块
|
||||
'DWBG6A2C_StandLiveInfo': 4,
|
||||
'DWBG6A2C_RiskPoint': 9,
|
||||
'DWBG6A2C_SecurityInfo': 15,
|
||||
'DWBG6A2C_AntiFraudInfo': 15,
|
||||
'DWBG6A2C_RiskList': 12,
|
||||
'DWBG6A2C_ApplicationStatistics': 7,
|
||||
'DWBG6A2C_LendingStatistics': 6,
|
||||
'DWBG6A2C_PerformanceStatistics': 7,
|
||||
'DWBG6A2C_OverdueRecord': 9,
|
||||
'DWBG6A2C_CreditDetail': 5,
|
||||
'DWBG6A2C_RentalBehavior': 5,
|
||||
'DWBG6A2C_RiskSupervision': 8,
|
||||
|
||||
// 贷款风险评估子模块
|
||||
'CJRZQ5E9F_RiskOverview': 8,
|
||||
'CJRZQ5E9F_CreditScores': 7,
|
||||
'CJRZQ5E9F_LoanBehaviorAnalysis': 7,
|
||||
'CJRZQ5E9F_InstitutionAnalysis': 5,
|
||||
'CJRZQ5E9F_TimeTrendAnalysis': 6,
|
||||
'CJRZQ5E9F_RiskIndicators': 8,
|
||||
'CJRZQ5E9F_RiskAdvice': 2,
|
||||
|
||||
// 人企关系加强版子模块
|
||||
'CQYGL3F8E_Investment': 4,
|
||||
'CQYGL3F8E_SeniorExecutive': 4,
|
||||
'CQYGL3F8E_Lawsuit': 8,
|
||||
'CQYGL3F8E_InvestHistory': 3,
|
||||
'CQYGL3F8E_FinancingHistory': 3,
|
||||
'CQYGL3F8E_Punishment': 7,
|
||||
'CQYGL3F8E_Abnormal': 6,
|
||||
'CQYGL3F8E_TaxRisk': 7,
|
||||
};
|
||||
|
||||
// 存储每个组件的 ref 引用
|
||||
const componentRefs = ref({});
|
||||
|
||||
// 存储每个组件的风险评分(由组件主动通知)
|
||||
const componentRiskScores = ref({});
|
||||
|
||||
// 提供方法让子组件通知自己的风险评分(0-100分,分数越高越安全)
|
||||
const notifyRiskStatus = (apiID, index, riskScore) => {
|
||||
const key = `${apiID}_${index}`;
|
||||
componentRiskScores.value[key] = riskScore;
|
||||
};
|
||||
|
||||
// 暴露给子组件
|
||||
defineExpose({
|
||||
notifyRiskStatus
|
||||
});
|
||||
|
||||
// 计算综合评分的函数(分数越高越安全)
|
||||
const calculateScore = () => {
|
||||
// 收集实际存在的 features 及其风险权重
|
||||
const presentFeatures = [];
|
||||
|
||||
processedReportData.value.forEach((item, index) => {
|
||||
const apiID = item.data?.apiID;
|
||||
if (!apiID) return;
|
||||
|
||||
// 获取风险权重(如果不在配置中,默认为 3)
|
||||
const weight = featureRiskLevels[apiID] ?? 3;
|
||||
|
||||
// 跳过权重为 0 的复合报告主模块(它们由子模块计算)
|
||||
if (weight === 0) return;
|
||||
|
||||
presentFeatures.push({
|
||||
apiID,
|
||||
index,
|
||||
weight
|
||||
});
|
||||
});
|
||||
|
||||
if (presentFeatures.length === 0) return 100; // 无有效特征时返回满分(最安全)
|
||||
|
||||
// 累计总风险分数
|
||||
let totalRiskScore = 0;
|
||||
const riskDetails = []; // 用于调试
|
||||
|
||||
presentFeatures.forEach(({ apiID, index, weight }) => {
|
||||
// 从组件风险评分中获取评分(0-100分,分数越高越安全)
|
||||
const key = `${apiID}_${index}`;
|
||||
const componentScore = componentRiskScores.value[key] ?? 100; // 默认100分(最安全)
|
||||
|
||||
// 将组件评分转换为风险分数(0-100 -> 100-0)
|
||||
const componentRisk = 100 - componentScore;
|
||||
|
||||
// 计算该模块的风险贡献(固定分值,不按占比)
|
||||
// 使用权重系数放大高风险模块的影响
|
||||
// 高风险模块(权重10)如果风险分数是0,扣20分(权重10 × 系数2)
|
||||
// 中风险模块(权重7)如果风险分数是0,扣14分(权重7 × 系数2)
|
||||
// 低风险模块(权重3)如果风险分数是0,扣6分(权重3 × 系数2)
|
||||
const weightMultiplier = 1.5; // 权重系数,可以调整这个值来控制影响程度
|
||||
const riskContribution = (componentRisk / 100) * weight * weightMultiplier;
|
||||
|
||||
riskDetails.push({
|
||||
apiID,
|
||||
index,
|
||||
weight,
|
||||
componentScore,
|
||||
componentRisk,
|
||||
riskContribution,
|
||||
hasStatus: key in componentRiskScores.value
|
||||
});
|
||||
|
||||
// 累加风险分数
|
||||
totalRiskScore += riskContribution;
|
||||
});
|
||||
|
||||
// 将总风险分数限制在 0-90 范围内(确保最低分为10分)
|
||||
const finalRiskScore = Math.max(0, Math.min(90, Math.round(totalRiskScore)));
|
||||
|
||||
// 转换为安全分数:分数越高越安全(100 - 风险分数)
|
||||
// 最终分数范围:10-100分
|
||||
const safetyScore = 100 - finalRiskScore;
|
||||
|
||||
return safetyScore;
|
||||
};
|
||||
|
||||
// 监听 reportData 和 componentRiskScores 变化并计算评分
|
||||
watch([reportData, componentRiskScores], () => {
|
||||
reportScore.value = calculateScore();
|
||||
|
||||
// 将评分系统数据整理到一个对象中
|
||||
const scoreData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
finalScore: reportScore.value,
|
||||
reportModules: processedReportData.value.map((item, index) => ({
|
||||
apiID: item.data.apiID,
|
||||
name: featureMap[item.data.apiID]?.name || '未知',
|
||||
index: index,
|
||||
riskScore: componentRiskScores.value[`${item.data.apiID}_${index}`] ?? '未上报',
|
||||
weight: featureRiskLevels[item.data.apiID] ?? 0
|
||||
})),
|
||||
componentScores: componentRiskScores.value,
|
||||
riskLevels: featureRiskLevels
|
||||
};
|
||||
|
||||
// 找出当前报告中包含的接口
|
||||
const availableAPIs = reportData
|
||||
.map((item) => item.data.apiID)
|
||||
.filter((id) => relativeWeights[id]);
|
||||
|
||||
// 如果没有可评分的接口,返回默认分数
|
||||
if (availableAPIs.length === 0) return 30; // 默认30分(中等风险)
|
||||
|
||||
// 计算当前报告中所有接口的相对权重总和
|
||||
let totalWeight = 0;
|
||||
availableAPIs.forEach((apiID) => {
|
||||
totalWeight += relativeWeights[apiID];
|
||||
});
|
||||
|
||||
// 计算每个权重点对应的分数
|
||||
const pointValue = maxScore / totalWeight;
|
||||
|
||||
// 基于当前报告中的接口计算实际权重
|
||||
const actualWeights = {};
|
||||
availableAPIs.forEach((apiID) => {
|
||||
// 将相对权重转换为实际分数权重
|
||||
actualWeights[apiID] = relativeWeights[apiID] * pointValue;
|
||||
});
|
||||
|
||||
// 遍历报告数据进行评分 - 风险越高分数越高
|
||||
reportData.forEach((item) => {
|
||||
const apiID = item.data.apiID;
|
||||
const data = item.data.data;
|
||||
|
||||
// 如果没有定义权重,跳过
|
||||
if (!actualWeights[apiID]) return;
|
||||
|
||||
// 根据不同的API ID计算分数(有风险时加分)
|
||||
switch (apiID) {
|
||||
case "G09SC02": // 婚姻状态
|
||||
// 不计入风险
|
||||
break;
|
||||
|
||||
case "JRZQ0A03": // 借贷申请记录
|
||||
if (data) {
|
||||
// 检查是否有申请记录(有则表示风险)
|
||||
let hasRisk = false;
|
||||
for (const key in data) {
|
||||
if (
|
||||
data[key] !== 0 &&
|
||||
data[key] !== "0" &&
|
||||
key.indexOf("allnum") > -1 &&
|
||||
!isNaN(parseInt(data[key])) &&
|
||||
parseInt(data[key]) > 0
|
||||
) {
|
||||
hasRisk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasRisk) {
|
||||
score += actualWeights[apiID];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "JRZQ8203": // 借贷行为记录
|
||||
if (data) {
|
||||
// 检查是否有借贷记录(有则表示风险)
|
||||
let hasRisk = false;
|
||||
for (const key in data) {
|
||||
if (
|
||||
data[key] !== 0 &&
|
||||
data[key] !== "0" &&
|
||||
(key.indexOf("lendamt") > -1 ||
|
||||
key.indexOf("num") > -1) &&
|
||||
!isNaN(parseInt(data[key])) &&
|
||||
parseInt(data[key]) > 0
|
||||
) {
|
||||
hasRisk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasRisk) {
|
||||
score += actualWeights[apiID];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "FLXG3D56": // 违约异常
|
||||
if (data) {
|
||||
// 检查除特定字段外的其他字段是否有异常值(有异常则表示风险)
|
||||
const excludeFields = [
|
||||
"swift_number",
|
||||
"code",
|
||||
"flag_specialList_c",
|
||||
];
|
||||
let hasRisk = false;
|
||||
|
||||
for (const key in data) {
|
||||
if (
|
||||
!excludeFields.includes(key) &&
|
||||
data[key] !== 0 &&
|
||||
data[key] !== "0"
|
||||
) {
|
||||
hasRisk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasRisk) {
|
||||
score += actualWeights[apiID];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "FLXG0V3B": // 不良记录
|
||||
if (data && data.risk_level) {
|
||||
// 根据风险等级加分
|
||||
switch (data.risk_level) {
|
||||
case "A": // 无风险
|
||||
// 不加分
|
||||
break;
|
||||
case "F": // 低风险
|
||||
score += actualWeights[apiID] * 0.3;
|
||||
break;
|
||||
case "C": // 中风险
|
||||
case "D": // 中风险
|
||||
score += actualWeights[apiID] * 0.7;
|
||||
break;
|
||||
case "B": // 高风险
|
||||
case "E": // 高风险
|
||||
score += actualWeights[apiID];
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "G35SC01": // 司法涉诉
|
||||
case "FLXG0V4B": // 司法涉诉
|
||||
case "Q23SC01": // 企业涉诉
|
||||
if (data) {
|
||||
let hasRisk = false;
|
||||
|
||||
// 检查各种涉诉信息 - 处理嵌套数据结构
|
||||
// entout是一个单元素数组,数组中第一个元素是JSON对象,对象中有entout属性
|
||||
if (
|
||||
data.entout &&
|
||||
Array.isArray(data.entout) &&
|
||||
data.entout.length > 0 &&
|
||||
data.entout[0] &&
|
||||
data.entout[0].entout &&
|
||||
((Array.isArray(data.entout[0].entout) &&
|
||||
data.entout[0].entout.length > 0) ||
|
||||
(typeof data.entout[0].entout === "object" &&
|
||||
Object.keys(data.entout[0].entout).length > 0))
|
||||
) {
|
||||
hasRisk = true;
|
||||
}
|
||||
|
||||
// 处理sxbzxr(失信被执行人)嵌套结构
|
||||
if (
|
||||
data.sxbzxr &&
|
||||
Array.isArray(data.sxbzxr) &&
|
||||
data.sxbzxr.length > 0 &&
|
||||
data.sxbzxr[0] &&
|
||||
data.sxbzxr[0].sxbzxr &&
|
||||
((Array.isArray(data.sxbzxr[0].sxbzxr) &&
|
||||
data.sxbzxr[0].sxbzxr.length > 0) ||
|
||||
(typeof data.sxbzxr[0].sxbzxr === "object" &&
|
||||
Object.keys(data.sxbzxr[0].sxbzxr).length > 0))
|
||||
) {
|
||||
hasRisk = true;
|
||||
}
|
||||
|
||||
// 处理xgbzxr(限制高消费被执行人)嵌套结构
|
||||
if (
|
||||
data.xgbzxr &&
|
||||
Array.isArray(data.xgbzxr) &&
|
||||
data.xgbzxr.length > 0 &&
|
||||
data.xgbzxr[0] &&
|
||||
data.xgbzxr[0].xgbzxr &&
|
||||
((Array.isArray(data.xgbzxr[0].xgbzxr) &&
|
||||
data.xgbzxr[0].xgbzxr.length > 0) ||
|
||||
(typeof data.xgbzxr[0].xgbzxr === "object" &&
|
||||
Object.keys(data.xgbzxr[0].xgbzxr).length > 0))
|
||||
) {
|
||||
hasRisk = true;
|
||||
}
|
||||
|
||||
if (hasRisk) {
|
||||
score += actualWeights[apiID];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "FLXG54F5": // 手机号码风险
|
||||
if (data && data.filterType) {
|
||||
// 根据filterType判断风险等级
|
||||
switch (data.filterType) {
|
||||
case "0": // 安全
|
||||
// 不加分
|
||||
break;
|
||||
case "3": // 低危
|
||||
score += actualWeights[apiID] * 0.3;
|
||||
break;
|
||||
case "2": // 中危
|
||||
score += actualWeights[apiID] * 0.7;
|
||||
break;
|
||||
case "1": // 高危
|
||||
score += actualWeights[apiID];
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "YYSYF7DB": // 手机二次卡
|
||||
if (data && data.is_second_card === true) {
|
||||
score += actualWeights[apiID];
|
||||
}
|
||||
break;
|
||||
|
||||
case "YYSY4B37": // 手机在网时长
|
||||
if (data && data.online_months < 6) {
|
||||
score += actualWeights[apiID];
|
||||
}
|
||||
break;
|
||||
|
||||
case "YYSY6F2E": // 手机三要素
|
||||
case "IVYZ0B03": // 手机号二要素
|
||||
case "KZEYS": // 身份证二要素
|
||||
case "JRZQDCBE": // 银行卡四要素核验
|
||||
if (data && data.is_consistent === false) {
|
||||
score += actualWeights[apiID];
|
||||
}
|
||||
break;
|
||||
|
||||
case "FIN019": // 银行卡黑名单
|
||||
if (data && data.is_blacklisted === true) {
|
||||
score += actualWeights[apiID];
|
||||
}
|
||||
break;
|
||||
|
||||
case "JRZQ4B6C": // 信贷表现
|
||||
if (data) {
|
||||
let riskScore = 0;
|
||||
|
||||
// 根据结果编码评分
|
||||
switch (data.result_code) {
|
||||
case "1": // A(Overdue) - 逾期,高风险
|
||||
riskScore += 0.8;
|
||||
break;
|
||||
case "3": // B(Delay) - 延迟,中风险
|
||||
riskScore += 0.5;
|
||||
break;
|
||||
case "2": // B(Normal) - 正常,低风险
|
||||
riskScore += 0.1;
|
||||
break;
|
||||
case "4": // U - 未知,中等风险
|
||||
riskScore += 0.3;
|
||||
break;
|
||||
}
|
||||
|
||||
// 当前逾期机构数
|
||||
const currentlyOverdue = parseInt(data.currently_overdue) || 0;
|
||||
if (currentlyOverdue > 0) {
|
||||
riskScore += Math.min(currentlyOverdue * 0.1, 0.3);
|
||||
}
|
||||
|
||||
// 异常还款机构数
|
||||
const accExc = parseInt(data.acc_exc) || 0;
|
||||
if (accExc > 0) {
|
||||
riskScore += Math.min(accExc * 0.05, 0.2);
|
||||
}
|
||||
|
||||
// 应用风险评分
|
||||
score += actualWeights[apiID] * Math.min(riskScore, 1.0);
|
||||
}
|
||||
break;
|
||||
|
||||
case "JRZQ09J8": // 收入评估
|
||||
if (data && data.level) {
|
||||
let riskScore = 0;
|
||||
|
||||
// 根据收入等级评分(收入越低风险越高)
|
||||
switch (data.level) {
|
||||
case "-": // 无记录,高风险
|
||||
riskScore = 0.9;
|
||||
break;
|
||||
case "A": // A级,中高风险
|
||||
riskScore = 0.7;
|
||||
break;
|
||||
case "B": // B级,中等风险
|
||||
riskScore = 0.5;
|
||||
break;
|
||||
case "C": // C级,中低风险
|
||||
riskScore = 0.3;
|
||||
break;
|
||||
case "D": // D级,低风险
|
||||
riskScore = 0.2;
|
||||
break;
|
||||
case "E": // E级,很低风险
|
||||
riskScore = 0.1;
|
||||
break;
|
||||
case "F": // F级,极低风险
|
||||
case "G": // G级,极低风险
|
||||
case "H": // H级,无风险
|
||||
case "I": // I级,无风险
|
||||
case "J": // J级,零风险
|
||||
riskScore = 0.05;
|
||||
break;
|
||||
}
|
||||
|
||||
// 应用风险评分
|
||||
score += actualWeights[apiID] * riskScore;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// 未知接口类型不影响评分
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 确保分数在0-90范围内并四舍五入
|
||||
return Math.max(0, Math.min(maxScore, Math.round(score)));
|
||||
};
|
||||
}, { immediate: true, deep: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-full from-blue-100 to-white bg-gradient-to-b">
|
||||
|
||||
<div class="min-h-full bg-[#FCF3F3]">
|
||||
<template v-if="isDone">
|
||||
<van-tabs v-model:active="active" scrollspy sticky :offset-top="46">
|
||||
<!-- 背景图容器 -->
|
||||
<div ref="backgroundContainerRef" class="w-full" :style="[backgroundContainerStyle, backgroundImageStyle]">
|
||||
</div>
|
||||
|
||||
<!-- Tabs 区域 -->
|
||||
<StyledTabs v-model:active="active" scrollspy sticky :offset-top="46">
|
||||
<div class="flex flex-col gap-y-4 p-4">
|
||||
<LEmpty v-if="isEmpty" />
|
||||
<van-tab title="分析指数">
|
||||
<div id="analysis" class="title mb-4">分析指数</div>
|
||||
<TitleBanner id="analysis" class="mb-4">分析指数</TitleBanner>
|
||||
<div class="card mb-4">
|
||||
<div class="my-4">
|
||||
<GaugeChart :score="reportScore" />
|
||||
@@ -777,132 +716,51 @@ const calculateScore = (reportData) => {
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="基本信息">
|
||||
<div id="basic" class="title mb-4">基本信息</div>
|
||||
<div class="card">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<LTitle title="报告信息" type="blue-green"></LTitle>
|
||||
<div class="flex flex-col gap-2 my-2">
|
||||
<div class="flex justify-between border-b pb-2 pl-2">
|
||||
<span class="text-gray-700 font-bold">报告时间:</span>
|
||||
<span class="text-gray-600">{{
|
||||
reportDateTime ||
|
||||
"2025-01-01 12:00:00"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b pb-2 pl-2" v-if="!isEmpty">
|
||||
<span class="text-gray-700 font-bold">报告项目:</span>
|
||||
<span class="text-gray-600">
|
||||
{{ reportName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="Object.keys(reportParams).length != 0">
|
||||
<LTitle title="报告对象" type="blue-green"></LTitle>
|
||||
<div class="flex flex-col gap-2 my-2">
|
||||
<div class="flex justify-between border-b pb-2 pl-2" v-if="reportParams?.name">
|
||||
<span class="text-gray-700 font-bold">姓名</span>
|
||||
<span class="text-gray-600">{{
|
||||
maskValue(
|
||||
"name",
|
||||
reportParams?.name
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b pb-2 pl-2"
|
||||
v-if="reportParams?.id_card">
|
||||
<span class="text-gray-700 font-bold">身份证号</span>
|
||||
<span class="text-gray-600">
|
||||
{{
|
||||
maskValue(
|
||||
"id_card",
|
||||
reportParams?.id_card
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b pb-2 pl-2"
|
||||
v-if="reportParams?.mobile">
|
||||
<span class="text-gray-700 font-bold">手机号</span>
|
||||
<span class="text-gray-600">{{
|
||||
maskValue(
|
||||
"mobile",
|
||||
reportParams?.mobile
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center bg-blue-100 rounded-xl px-4 py-2 flex-1">
|
||||
<div
|
||||
class="bg-green-500 w-12 h-12 text-white text-xl flex items-center justify-center rounded-full mr-4">
|
||||
✔
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-lg">
|
||||
身份证检查结果
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
身份证信息核验通过
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center bg-blue-100 rounded-xl px-4 py-2 flex-1">
|
||||
<div
|
||||
class="bg-green-500 w-12 h-12 text-white text-xl flex items-center justify-center rounded-full mr-4">
|
||||
✔
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-lg">
|
||||
手机号检测结果
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
被查询人姓名与运营商提供的一致
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
被查询人身份证与运营商提供的一致
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<ShareReportButton v-if="!isShare" :order-id="orderId" :order-no="orderNo"
|
||||
:is-example="!orderId" />
|
||||
</div>
|
||||
<TitleBanner id="basic" class="mb-4">基本信息</TitleBanner>
|
||||
<VerificationCard :report-params="reportParams" :report-date-time="reportDateTime"
|
||||
:report-name="reportName" :is-empty="isEmpty" :is-share="isShare" :order-id="orderId"
|
||||
:order-no="orderNo" />
|
||||
<LRemark content="如查询的姓名/身份证与运营商提供的不一致,可能会存在报告内容不匹配的情况" />
|
||||
</van-tab>
|
||||
<van-tab v-for="(item, index) in processedReportData" :key="index"
|
||||
<van-tab v-for="(item, index) in processedReportData" :key="`${item.data.apiID}_${index}`"
|
||||
:title="featureMap[item.data.apiID]?.name">
|
||||
<div :id="item.data.apiID" class="title mb-4">
|
||||
<TitleBanner :id="item.data.apiID" class="mb-4">
|
||||
{{ featureMap[item.data.apiID]?.name }}
|
||||
</div>
|
||||
<component :is="featureMap[item.data.apiID]?.component" :data="item.data.data"
|
||||
:params="reportParams">
|
||||
</TitleBanner>
|
||||
<component :is="featureMap[item.data.apiID]?.component" :ref="el => {
|
||||
if (el) {
|
||||
const refKey = `${item.data.apiID}_${index}`;
|
||||
componentRefs[refKey] = el;
|
||||
}
|
||||
}" :data="item.data.data" :params="reportParams" :api-id="item.data.apiID" :index="index"
|
||||
:notify-risk-status="notifyRiskStatus">
|
||||
</component>
|
||||
<LRemark v-if="featureMap[item.data.apiID]?.remark"
|
||||
:content="featureMap[item.data.apiID]?.remark" />
|
||||
</van-tab>
|
||||
<ShareReportButton v-if="!isShare" class="mb-4" :order-id="orderId" :order-no="orderNo"
|
||||
:is-example="!orderId" />
|
||||
|
||||
<ShareReportButton v-if="!isShare" class="h-12 text-3xl mt-8" :order-id="orderId"
|
||||
:order-no="orderNo" :is-example="isExample" />
|
||||
<span class="mb-4 text-center text-sm text-gray-500">分享当前{{ isExample ? '示例' : '报告' }}链接</span>
|
||||
<div class="card">
|
||||
<div>
|
||||
<div class="text-bold text-blue-500 mb-2">
|
||||
<div class="text-bold text-center text-[#333333] mb-2">
|
||||
免责声明
|
||||
</div>
|
||||
<p>
|
||||
<p class="text-[#999999]">
|
||||
|
||||
1、本份报告是在取得您个人授权后,我们才向合法存有您以上个人信息的机构去调取相关内容,我们不会以任何形式对您的报告进行存储,除您和您授权的人外不会提供给任何人和机构进行查看。
|
||||
</p>
|
||||
<p>
|
||||
2、本报告自生成之日起,有效期 <strong class="text-red-500">30
|
||||
天</strong>,过期自动删除。如果您对本份报告存有异议,可能是合作机构数据有延迟或未能获取到您的相关数据,出于合作平台数据隐私的保护,本平台将不做任何解释。
|
||||
<p class="text-[#999999]">
|
||||
2、本报告自生成之日起,有效期 30
|
||||
天,过期自动删除。如果您对本份报告存有异议,可能是合作机构数据有延迟或未能获取到您的相关数据,出于合作平台数据隐私的保护,本平台将不做任何解释。
|
||||
</p>
|
||||
<p>
|
||||
<p class="text-[#999999]">
|
||||
3、若以上数据有错误,请联系平台客服。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-tabs>
|
||||
</StyledTabs>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
@@ -924,11 +782,6 @@ const calculateScore = (reportData) => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
@apply mx-auto mt-2 w-64 border rounded-3xl py-2 text-center text-white font-bold;
|
||||
background: linear-gradient(135deg, var(--van-theme-primary), var(--van-theme-primary-dark));
|
||||
}
|
||||
|
||||
.a {
|
||||
color: #e03131;
|
||||
}
|
||||
@@ -946,5 +799,6 @@ const calculateScore = (reportData) => {
|
||||
|
||||
:deep(.card) {
|
||||
@apply p-3;
|
||||
box-shadow: 0px 0px 24px 0px #3F3F3F0F;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -95,10 +95,20 @@ async function handleBind() {
|
||||
localStorage.setItem('refreshAfter', data.value.data.refreshAfter)
|
||||
localStorage.setItem('accessExpire', data.value.data.accessExpire)
|
||||
closeDialog();
|
||||
agentStore.fetchAgentStatus();
|
||||
userStore.fetchUserInfo();
|
||||
await Promise.all([
|
||||
agentStore.fetchAgentStatus(),
|
||||
userStore.fetchUserInfo()
|
||||
]);
|
||||
|
||||
// 发出绑定成功的事件
|
||||
emit('bind-success')
|
||||
emit('bind-success');
|
||||
|
||||
// 延迟执行路由检查,确保状态已更新
|
||||
setTimeout(() => {
|
||||
// 重新触发路由检查
|
||||
const currentRoute = router.currentRoute.value;
|
||||
router.replace(currentRoute.path);
|
||||
}, 100);
|
||||
} else {
|
||||
showToast(data.value.msg);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<div ref="chartRef" :style="{ width: '100%', height: '200px' }"></div>
|
||||
<div class="risk-description">
|
||||
{{ riskDescription }}
|
||||
</div>
|
||||
<div ref="chartRef" :style="{ width: '100%', height: '200px' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -18,10 +18,10 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
// 根据分数计算风险等级和颜色
|
||||
// 根据分数计算风险等级和颜色(分数越高越安全)
|
||||
const riskLevel = computed(() => {
|
||||
const score = props.score;
|
||||
if (score >= 0 && score <= 25) {
|
||||
if (score >= 75 && score <= 100) {
|
||||
return {
|
||||
level: "无任何风险",
|
||||
color: "#52c41a",
|
||||
@@ -30,7 +30,7 @@ const riskLevel = computed(() => {
|
||||
{ offset: 1, color: "#7fdb42" }
|
||||
]
|
||||
};
|
||||
} else if (score > 25 && score <= 50) {
|
||||
} else if (score >= 50 && score < 75) {
|
||||
return {
|
||||
level: "风险指数较低",
|
||||
color: "#faad14",
|
||||
@@ -39,7 +39,7 @@ const riskLevel = computed(() => {
|
||||
{ offset: 1, color: "#ffc53d" }
|
||||
]
|
||||
};
|
||||
} else if (score > 50 && score <= 75) {
|
||||
} else if (score >= 25 && score < 50) {
|
||||
return {
|
||||
level: "风险指数较高",
|
||||
color: "#fa8c16",
|
||||
@@ -60,14 +60,14 @@ const riskLevel = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// 评分解释文本
|
||||
// 评分解释文本(分数越高越安全)
|
||||
const riskDescription = computed(() => {
|
||||
const score = props.score;
|
||||
if (score >= 0 && score <= 25) {
|
||||
if (score >= 75 && score <= 100) {
|
||||
return "根据综合分析,当前报告未检测到明显风险因素,各项指标表现正常,总体状况良好。";
|
||||
} else if (score > 25 && score <= 50) {
|
||||
} else if (score >= 50 && score < 75) {
|
||||
return "根据综合分析,当前报告存在少量风险信号,建议关注相关指标变化,保持警惕。";
|
||||
} else if (score > 50 && score <= 75) {
|
||||
} else if (score >= 25 && score < 50) {
|
||||
return "根据综合分析,当前报告风险指数较高,多项指标显示异常,建议进一步核实相关情况。";
|
||||
} else {
|
||||
return "根据综合分析,当前报告显示高度风险状态,多项重要指标严重异常,请立即采取相应措施。";
|
||||
@@ -104,8 +104,8 @@ const updateChart = () => {
|
||||
center: ["50%", "80%"],
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, risk.gradient),
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.2)',
|
||||
shadowBlur: 6,
|
||||
shadowColor: risk.color,
|
||||
},
|
||||
progress: {
|
||||
show: true,
|
||||
@@ -121,39 +121,40 @@ const updateChart = () => {
|
||||
[1, new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{
|
||||
offset: 0,
|
||||
color: "rgba(0, 0, 0, 0.1)"
|
||||
color: risk.color + "30" // 使用风险颜色,透明度20%
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: "rgba(0, 0, 0, 0.05)"
|
||||
color: risk.color + "25" // 使用风险颜色,透明度10%
|
||||
}
|
||||
])]
|
||||
]
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
show: true,
|
||||
distance: -30,
|
||||
length: 6,
|
||||
splitNumber: 10, // 每1分一个小刻度
|
||||
lineStyle: {
|
||||
color: risk.color,
|
||||
width: 1,
|
||||
opacity: 0.5
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
distance: -26,
|
||||
length: 5,
|
||||
distance: -36,
|
||||
length: 12,
|
||||
splitNumber: 9, // 9个大刻度,100分分成9个区间
|
||||
lineStyle: {
|
||||
color: "#999",
|
||||
width: 2
|
||||
color: risk.color,
|
||||
width: 2,
|
||||
opacity: 0.5
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
distance: -8,
|
||||
fontSize: 12,
|
||||
color: "#999",
|
||||
formatter: function (value) {
|
||||
if (value % 20 === 0) {
|
||||
return value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
show: false,
|
||||
},
|
||||
anchor: {
|
||||
show: false
|
||||
@@ -243,11 +244,9 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.risk-description {
|
||||
margin-top: 10px;
|
||||
padding: 8px 12px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
padding: 0 12px;
|
||||
color: #666666;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
|
||||
275
src/components/ImageSaveGuide.vue
Normal file
275
src/components/ImageSaveGuide.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<div v-if="show" class="image-save-guide-overlay">
|
||||
<div class="guide-content" @click.stop>
|
||||
<!-- 关闭按钮 -->
|
||||
<button class="close-button" @click="close">
|
||||
<span class="close-icon">×</span>
|
||||
</button>
|
||||
|
||||
<!-- 图片区域 -->
|
||||
<div v-if="imageUrl" class="image-container">
|
||||
<img :src="imageUrl" class="guide-image" />
|
||||
</div>
|
||||
|
||||
<!-- 文字内容区域 -->
|
||||
<div class="text-container">
|
||||
<div class="guide-title">{{ title }}</div>
|
||||
<div class="guide-instruction">长按图片保存</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
imageUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '保存图片到相册'
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const close = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
// 自动关闭功能已禁用
|
||||
// watch(() => props.show, (newVal) => {
|
||||
// if (newVal && props.autoCloseDelay > 0) {
|
||||
// setTimeout(() => {
|
||||
// close();
|
||||
// }, props.autoCloseDelay);
|
||||
// }
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-save-guide-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16px;
|
||||
padding: 24px 20px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
color: #333;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 20px;
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 图片容器 */
|
||||
.image-container {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.guide-image {
|
||||
max-width: 100%;
|
||||
max-height: 60vh;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 文字容器 */
|
||||
.text-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.guide-instruction {
|
||||
font-size: 16px;
|
||||
color: #4a4a4a;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 超小屏幕 (320px - 375px) */
|
||||
@media (max-width: 375px) {
|
||||
.image-save-guide-overlay {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.guide-content {
|
||||
padding: 20px 16px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.guide-instruction {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.guide-image {
|
||||
max-height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏幕 (376px - 414px) */
|
||||
@media (min-width: 376px) and (max-width: 414px) {
|
||||
.guide-content {
|
||||
padding: 22px 18px;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等屏幕 (415px - 480px) */
|
||||
@media (min-width: 415px) and (max-width: 480px) {
|
||||
.guide-content {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏幕 (481px+) */
|
||||
@media (min-width: 481px) {
|
||||
.guide-content {
|
||||
padding: 28px 24px;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.guide-instruction {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 横屏适配 */
|
||||
@media (orientation: landscape) and (max-height: 500px) {
|
||||
.guide-content {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.guide-image {
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
font-size: 18px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.guide-instruction {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.guide-content {
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
color: #e5e5e5;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.guide-instruction {
|
||||
color: #d1d5db;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,66 +1,86 @@
|
||||
<template>
|
||||
<van-popup v-model:show="show" round position="bottom">
|
||||
<div class="max-h-[calc(100vh-100px)] m-4">
|
||||
<div class="p-4">
|
||||
<van-swipe
|
||||
class="poster-swiper rounded-xl shadow"
|
||||
indicator-color="white"
|
||||
@change="onSwipeChange"
|
||||
>
|
||||
<van-swipe-item
|
||||
v-for="(_, index) in posterImages"
|
||||
:key="index"
|
||||
>
|
||||
<canvas
|
||||
:ref="(el) => (posterCanvasRefs[index] = el)"
|
||||
class="poster-canvas rounded-xl h-[800px] m-auto"
|
||||
></canvas>
|
||||
<van-popup v-model:show="show" round position="bottom" :style="{ maxHeight: '95vh' }">
|
||||
<div class="qrcode-popup-container">
|
||||
<div class="qrcode-content">
|
||||
<van-swipe class="poster-swiper rounded-lg sm:rounded-xl shadow" indicator-color="white"
|
||||
@change="onSwipeChange">
|
||||
<van-swipe-item v-for="(_, index) in posterImages" :key="index">
|
||||
<canvas :ref="(el) => (posterCanvasRefs[index] = el)"
|
||||
class="poster-canvas rounded-lg sm:rounded-xl m-auto"></canvas>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</div>
|
||||
<div
|
||||
v-if="mode === 'promote'"
|
||||
class="swipe-tip text-center text-gray-700 text-sm mb-2"
|
||||
>
|
||||
<div v-if="mode === 'promote'"
|
||||
class="swipe-tip text-center text-gray-700 text-xs sm:text-sm mb-1 sm:mb-2 px-2">
|
||||
<span class="swipe-icon">←</span> 左右滑动切换海报
|
||||
<span class="swipe-icon">→</span>
|
||||
</div>
|
||||
<van-divider>分享到好友</van-divider>
|
||||
<van-divider class="my-2 sm:my-3">分享到好友</van-divider>
|
||||
|
||||
<div class="flex items-center justify-around">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center"
|
||||
@click="savePoster"
|
||||
>
|
||||
<img
|
||||
src="@/assets/images/icon_share_img.svg"
|
||||
class="w-10 h-10 rounded-full"
|
||||
/>
|
||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
||||
保存图片
|
||||
<div class="flex items-center justify-around pb-3 sm:pb-4 px-4">
|
||||
<!-- 微信环境:显示分享、保存和复制按钮 -->
|
||||
<template v-if="isWeChat">
|
||||
<!-- <div class="flex flex-col items-center justify-center cursor-pointer" @click="shareToFriend">
|
||||
<img src="@/assets/images/icon_share_friends.svg"
|
||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
||||
分享给好友
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center"
|
||||
@click="copyUrl"
|
||||
>
|
||||
<img
|
||||
src="@/assets/images/icon_share_url.svg"
|
||||
class="w-10 h-10 rounded-full"
|
||||
/>
|
||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
||||
复制链接
|
||||
<div class="flex flex-col items-center justify-center cursor-pointer" @click="shareToTimeline">
|
||||
<img src="@/assets/images/icon_share_wechat.svg"
|
||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
||||
分享到朋友圈
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePosterForWeChat">
|
||||
<img src="@/assets/images/icon_share_img.svg"
|
||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
||||
保存图片
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
|
||||
<img src="@/assets/images/icon_share_url.svg"
|
||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
||||
复制链接
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 非微信环境:显示保存和复制按钮 -->
|
||||
<template v-else>
|
||||
<div class="flex flex-col items-center justify-center cursor-pointer" @click="savePoster">
|
||||
<img src="@/assets/images/icon_share_img.svg"
|
||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
||||
保存图片
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center cursor-pointer" @click="copyUrl">
|
||||
<img src="@/assets/images/icon_share_url.svg"
|
||||
class="share-icon w-9 h-9 sm:w-10 sm:h-10 rounded-full" />
|
||||
<div class="text-center mt-1 text-gray-600 text-xs">
|
||||
复制链接
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 图片保存指引遮罩层 -->
|
||||
<ImageSaveGuide :show="showImageGuide" :image-url="currentImageUrl" :title="imageGuideTitle"
|
||||
@close="closeImageGuide" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, computed, onMounted, toRefs } from "vue";
|
||||
import QRCode from "qrcode";
|
||||
import { showToast } from "vant";
|
||||
import { useWeixinShare } from "@/composables/useWeixinShare";
|
||||
import ImageSaveGuide from "./ImageSaveGuide.vue";
|
||||
|
||||
const props = defineProps({
|
||||
linkIdentifier: {
|
||||
@@ -77,6 +97,19 @@ const posterCanvasRefs = ref([]); // 用于绘制海报的canvas数组
|
||||
const currentIndex = ref(0); // 当前显示的海报索引
|
||||
const postersGenerated = ref([]); // 标记海报是否已经生成过,将在onMounted中初始化
|
||||
const show = defineModel("show");
|
||||
|
||||
// 微信环境检测
|
||||
const isWeChat = computed(() => {
|
||||
return /MicroMessenger/i.test(navigator.userAgent);
|
||||
});
|
||||
|
||||
// 微信分享功能
|
||||
const { configWeixinShare } = useWeixinShare();
|
||||
|
||||
// 图片保存指引遮罩层相关
|
||||
const showImageGuide = ref(false);
|
||||
const currentImageUrl = ref('');
|
||||
const imageGuideTitle = ref('');
|
||||
const url = computed(() => {
|
||||
const baseUrl = window.location.origin; // 获取当前站点的域名
|
||||
return mode.value === "promote"
|
||||
@@ -91,13 +124,13 @@ const posterImages = ref([]);
|
||||
const qrCodePositions = ref({
|
||||
// promote模式的配置 (tg_qrcode)
|
||||
promote: [
|
||||
{ x: 180, y: 1440, size: 300 }, // tg_qrcode_1.png
|
||||
{ x: 525, y: 1955, size: 500 }, // tg_qrcode_4.jpg
|
||||
{ x: 255, y: 940, size: 250 }, // tg_qrcode_8.jpg
|
||||
{ x: 138, y: 954, size: 220 }, // tg_qrcode_1.png
|
||||
{ x: 138, y: 954, size: 220 }, // tg_qrcode_2.jpg
|
||||
{ x: 138, y: 954, size: 220 }, // tg_qrcode_3.jpg
|
||||
],
|
||||
// invitation模式的配置 (yq_qrcode)
|
||||
invitation: [
|
||||
{ x: 360, y: -1370, size: 360 }, // yq_qrcode_1.png
|
||||
{ x: 138, y: 954, size: 220 }, // yq_qrcode_1.png
|
||||
],
|
||||
});
|
||||
|
||||
@@ -115,7 +148,7 @@ const loadPosterImages = async () => {
|
||||
const basePrefix = mode.value === "promote" ? "tg_qrcode_" : "yq_qrcode_";
|
||||
|
||||
// 根据模式确定要加载的图片编号
|
||||
const imageNumbers = mode.value === "promote" ? [1, 4, 8] : [1];
|
||||
const imageNumbers = mode.value === "promote" ? [1, 2, 3] : [1];
|
||||
|
||||
// 加载图片
|
||||
for (const i of imageNumbers) {
|
||||
@@ -236,22 +269,216 @@ watch(show, (newVal) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 分享到微信
|
||||
const toPromote = () => {
|
||||
// 这里可以实现微信分享的功能,比如调用微信JS-SDK等
|
||||
console.log("分享到微信好友");
|
||||
// 分享给好友
|
||||
const shareToFriend = () => {
|
||||
if (!isWeChat.value) {
|
||||
showToast({ message: "请在微信中打开" });
|
||||
return;
|
||||
}
|
||||
|
||||
const shareUrl = generalUrl();
|
||||
const shareConfig = {
|
||||
title: mode.value === "promote"
|
||||
? "哈密大数据查询 - 推广链接"
|
||||
: "哈密大数据查询 - 邀请链接",
|
||||
desc: mode.value === "promote"
|
||||
? "扫码查看哈密大数据查询推广信息"
|
||||
: "扫码申请哈密大数据查询代理权限",
|
||||
link: shareUrl,
|
||||
imgUrl: "https://hm.tianyuandb.com/logo.jpg"
|
||||
};
|
||||
|
||||
configWeixinShare(shareConfig);
|
||||
|
||||
// 显示分享指引
|
||||
showShareGuide("好友");
|
||||
};
|
||||
|
||||
// 保存海报图片
|
||||
// 分享到朋友圈
|
||||
const shareToTimeline = () => {
|
||||
if (!isWeChat.value) {
|
||||
showToast({ message: "请在微信中打开" });
|
||||
return;
|
||||
}
|
||||
|
||||
const shareUrl = generalUrl();
|
||||
const shareConfig = {
|
||||
title: mode.value === "promote"
|
||||
? "哈密大数据查询 - 推广链接"
|
||||
: "哈密大数据查询 - 邀请链接",
|
||||
desc: mode.value === "promote"
|
||||
? "扫码查看哈密大数据查询推广信息"
|
||||
: "扫码申请哈密大数据查询代理权限",
|
||||
link: shareUrl,
|
||||
imgUrl: "https://hm.tianyuandb.com/logo.jpg"
|
||||
};
|
||||
|
||||
configWeixinShare(shareConfig);
|
||||
|
||||
// 显示分享指引
|
||||
showShareGuide("朋友圈");
|
||||
};
|
||||
|
||||
// 显示分享指引
|
||||
const showShareGuide = (target) => {
|
||||
// 设置遮罩层内容
|
||||
currentImageUrl.value = ''; // 分享指引不需要图片
|
||||
imageGuideTitle.value = `分享到${target}`;
|
||||
|
||||
// 显示遮罩层
|
||||
showImageGuide.value = true;
|
||||
};
|
||||
|
||||
// 微信环境保存图片
|
||||
const savePosterForWeChat = () => {
|
||||
const canvas = posterCanvasRefs.value[currentIndex.value];
|
||||
const dataURL = canvas.toDataURL("image/png");
|
||||
|
||||
// 设置遮罩层内容
|
||||
currentImageUrl.value = dataURL;
|
||||
imageGuideTitle.value = '保存图片到相册';
|
||||
|
||||
// 显示遮罩层
|
||||
showImageGuide.value = true;
|
||||
};
|
||||
|
||||
// 关闭图片保存指引
|
||||
const closeImageGuide = () => {
|
||||
showImageGuide.value = false;
|
||||
};
|
||||
|
||||
// 保存海报图片 - 多种保存方式(非微信环境)
|
||||
const savePoster = () => {
|
||||
const canvas = posterCanvasRefs.value[currentIndex.value];
|
||||
const dataURL = canvas.toDataURL("image/png"); // 获取 canvas 内容为图片
|
||||
const dataURL = canvas.toDataURL("image/png");
|
||||
|
||||
// 检测环境
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
|
||||
if (isMobile) {
|
||||
// 手机浏览器环境
|
||||
saveForMobile(dataURL);
|
||||
} else {
|
||||
// PC浏览器环境
|
||||
saveForPC(dataURL);
|
||||
}
|
||||
};
|
||||
|
||||
// PC浏览器保存方式
|
||||
const saveForPC = (dataURL) => {
|
||||
const a = document.createElement("a");
|
||||
a.href = dataURL;
|
||||
a.download = "天远数据查询.png";
|
||||
a.download = "哈密大数据查询海报.png";
|
||||
a.click();
|
||||
};
|
||||
|
||||
// 手机浏览器保存方式
|
||||
const saveForMobile = async (dataURL) => {
|
||||
// 方法1: 尝试使用 File System Access API (Chrome 86+)
|
||||
const fileSystemSuccess = await saveWithFileSystemAPI(dataURL);
|
||||
if (fileSystemSuccess) return;
|
||||
|
||||
// 方法2: 尝试使用 Blob 和 URL.createObjectURL
|
||||
try {
|
||||
const blob = dataURLToBlob(dataURL);
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "哈密大数据查询海报.png";
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
// 清理 URL 对象
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
|
||||
showToast({ message: "图片已保存到相册" });
|
||||
} catch (error) {
|
||||
console.error("Blob保存失败:", error);
|
||||
// 方法3: 尝试使用 share API (支持分享到其他应用)
|
||||
const shareSuccess = await tryShareAPI(dataURL);
|
||||
if (!shareSuccess) {
|
||||
// 方法4: 降级到长按保存提示
|
||||
showLongPressTip(dataURL);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 显示长按保存提示(非微信环境使用)
|
||||
const showLongPressTip = (dataURL) => {
|
||||
// 设置遮罩层内容
|
||||
currentImageUrl.value = dataURL;
|
||||
imageGuideTitle.value = '保存图片到相册';
|
||||
|
||||
// 显示遮罩层
|
||||
showImageGuide.value = true;
|
||||
};
|
||||
|
||||
// 将 dataURL 转换为 Blob
|
||||
const dataURLToBlob = (dataURL) => {
|
||||
const arr = dataURL.split(',');
|
||||
const mime = arr[0].match(/:(.*?);/)[1];
|
||||
const bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
const u8arr = new Uint8Array(n);
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
return new Blob([u8arr], { type: mime });
|
||||
};
|
||||
|
||||
// 备用保存方法 - 使用 File System Access API (现代浏览器)
|
||||
const saveWithFileSystemAPI = async (dataURL) => {
|
||||
if ('showSaveFilePicker' in window) {
|
||||
try {
|
||||
const blob = dataURLToBlob(dataURL);
|
||||
const fileHandle = await window.showSaveFilePicker({
|
||||
suggestedName: '哈密大数据查询海报.png',
|
||||
types: [{
|
||||
description: 'PNG images',
|
||||
accept: { 'image/png': ['.png'] }
|
||||
}]
|
||||
});
|
||||
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(blob);
|
||||
await writable.close();
|
||||
|
||||
showToast({ message: "图片已保存" });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("File System API 保存失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 尝试使用 Share API
|
||||
const tryShareAPI = async (dataURL) => {
|
||||
if (navigator.share && navigator.canShare) {
|
||||
try {
|
||||
const blob = dataURLToBlob(dataURL);
|
||||
const file = new File([blob], '哈密大数据查询海报.png', { type: 'image/png' });
|
||||
|
||||
if (navigator.canShare({ files: [file] })) {
|
||||
await navigator.share({
|
||||
title: '哈密大数据查询海报',
|
||||
text: '分享海报图片',
|
||||
files: [file]
|
||||
});
|
||||
showToast({ message: "图片已分享" });
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Share API 失败:", error);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const generalUrl = () => {
|
||||
return url.value + encodeURIComponent(linkIdentifier.value);
|
||||
};
|
||||
@@ -291,11 +518,64 @@ const copyToClipboard = (text) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qrcode-popup-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 95vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qrcode-content {
|
||||
flex-shrink: 0;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
/* 小屏设备优化 */
|
||||
@media (max-width: 375px) {
|
||||
.qrcode-content {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等及以上屏幕 */
|
||||
@media (min-width: 640px) {
|
||||
.qrcode-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.poster-swiper {
|
||||
height: 500px;
|
||||
height: calc(95vh - 180px);
|
||||
min-height: 300px;
|
||||
max-height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 小屏设备:更小的海报高度 */
|
||||
@media (max-width: 375px) {
|
||||
.poster-swiper {
|
||||
height: calc(95vh - 160px);
|
||||
min-height: 280px;
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中等屏幕 */
|
||||
@media (min-width: 640px) and (max-width: 767px) {
|
||||
.poster-swiper {
|
||||
height: calc(95vh - 190px);
|
||||
max-height: 520px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏幕 */
|
||||
@media (min-width: 768px) {
|
||||
.poster-swiper {
|
||||
height: calc(95vh - 200px);
|
||||
max-height: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.poster-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -304,30 +584,61 @@ const copyToClipboard = (text) => {
|
||||
|
||||
.swipe-tip {
|
||||
animation: fadeInOut 2s infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.swipe-icon {
|
||||
display: inline-block;
|
||||
animation: slideLeftRight 1.5s infinite;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.swipe-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.share-icon {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.share-icon:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
@keyframes fadeInOut {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideLeftRight {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 优化 van-divider 在小屏幕上的间距 */
|
||||
:deep(.van-divider) {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:deep(.van-divider) {
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -155,172 +155,118 @@ function toPrivacyPolicy() {
|
||||
|
||||
<template>
|
||||
<div v-if="dialogStore.showRealNameAuth" class="real-name-auth-dialog-box">
|
||||
<van-popup
|
||||
v-model:show="dialogStore.showRealNameAuth"
|
||||
round
|
||||
position="bottom"
|
||||
@close="closeDialog"
|
||||
>
|
||||
<div class="real-name-auth-dialog" style="background: linear-gradient(135deg, var(--van-theme-primary-light), rgba(255,255,255,0.9));">
|
||||
<van-popup v-model:show="dialogStore.showRealNameAuth" round position="bottom" @close="closeDialog"
|
||||
:style="{ maxHeight: '90vh' }">
|
||||
<div class="real-name-auth-dialog"
|
||||
style="background: linear-gradient(135deg, var(--van-theme-primary-light), rgba(255,255,255,0.9));">
|
||||
<div class="title-bar">
|
||||
<div class="font-bold" style="color: var(--van-text-color);">实名认证</div>
|
||||
<van-icon
|
||||
name="cross"
|
||||
class="close-icon"
|
||||
style="color: var(--van-text-color-2);"
|
||||
@click="closeDialog"
|
||||
/>
|
||||
<div class="text-base sm:text-lg font-bold" style="color: var(--van-text-color);">实名认证</div>
|
||||
<van-icon name="cross" class="close-icon" style="color: var(--van-text-color-2);"
|
||||
@click="closeDialog" />
|
||||
</div>
|
||||
<div class="px-8 py-2">
|
||||
<div class="auth-notice p-4 rounded-lg mb-6" style="background-color: var(--van-theme-primary-light); border: 1px solid rgba(162, 37, 37, 0.2);">
|
||||
<div class="text-sm space-y-2" style="color: var(--van-text-color);">
|
||||
<p class="font-medium" style="color: var(--van-theme-primary);">
|
||||
实名认证说明:
|
||||
</p>
|
||||
<p>1. 实名认证是提现的必要条件</p>
|
||||
<p>2. 提现金额将转入您实名认证的银行卡账户</p>
|
||||
<p>
|
||||
3.
|
||||
请确保填写的信息真实有效,否则将影响提现功能的使用
|
||||
</p>
|
||||
<p>4. 认证信息提交后将无法修改,请仔细核对</p>
|
||||
<div class="dialog-content">
|
||||
<div class="px-4 sm:px-6 md:px-8 py-3 sm:py-4">
|
||||
<div class="auth-notice p-3 sm:p-4 rounded-lg mb-4 sm:mb-6"
|
||||
style="background-color: var(--van-theme-primary-light); border: 1px solid rgba(162, 37, 37, 0.2);">
|
||||
<div class="text-xs sm:text-sm space-y-1.5 sm:space-y-2"
|
||||
style="color: var(--van-text-color);">
|
||||
<p class="font-medium" style="color: var(--van-theme-primary);">
|
||||
实名认证说明:
|
||||
</p>
|
||||
<p>1. 实名认证是提现的必要条件</p>
|
||||
<p>2. 提现金额将转入您实名认证的银行卡账户</p>
|
||||
<p>3. 请确保填写的信息真实有效,否则将影响提现功能的使用</p>
|
||||
<p>4. 认证信息提交后将无法修改,请仔细核对</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<!-- 姓名输入 -->
|
||||
<div
|
||||
:class="[
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
<!-- 姓名输入 -->
|
||||
<div :class="[
|
||||
'input-container',
|
||||
nameFocused ? 'focused' : '',
|
||||
]"
|
||||
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);"
|
||||
>
|
||||
<input
|
||||
v-model="realName"
|
||||
class="input-field"
|
||||
type="text"
|
||||
placeholder="请输入真实姓名"
|
||||
style="color: var(--van-text-color);"
|
||||
@focus="nameFocused = true"
|
||||
@blur="nameFocused = false"
|
||||
/>
|
||||
</div>
|
||||
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);">
|
||||
<input v-model="realName" class="input-field" type="text" placeholder="请输入真实姓名"
|
||||
style="color: var(--van-text-color);" @focus="nameFocused = true"
|
||||
@blur="nameFocused = false" />
|
||||
</div>
|
||||
|
||||
<!-- 身份证号输入 -->
|
||||
<div
|
||||
:class="[
|
||||
<!-- 身份证号输入 -->
|
||||
<div :class="[
|
||||
'input-container',
|
||||
idCardFocused ? 'focused' : '',
|
||||
]"
|
||||
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);"
|
||||
>
|
||||
<input
|
||||
v-model="idCard"
|
||||
class="input-field"
|
||||
type="text"
|
||||
placeholder="请输入身份证号"
|
||||
maxlength="18"
|
||||
style="color: var(--van-text-color);"
|
||||
@focus="idCardFocused = true"
|
||||
@blur="idCardFocused = false"
|
||||
/>
|
||||
</div>
|
||||
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);">
|
||||
<input v-model="idCard" class="input-field" type="text" placeholder="请输入身份证号"
|
||||
maxlength="18" style="color: var(--van-text-color);" @focus="idCardFocused = true"
|
||||
@blur="idCardFocused = false" />
|
||||
</div>
|
||||
|
||||
<!-- 手机号输入 -->
|
||||
<div
|
||||
:class="[
|
||||
<!-- 手机号输入 -->
|
||||
<div :class="[
|
||||
'input-container',
|
||||
phoneFocused ? 'focused' : '',
|
||||
]"
|
||||
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);"
|
||||
>
|
||||
<input
|
||||
v-model="phoneNumber"
|
||||
class="input-field"
|
||||
type="tel"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
style="color: var(--van-text-color);"
|
||||
@focus="phoneFocused = true"
|
||||
@blur="phoneFocused = false"
|
||||
/>
|
||||
</div>
|
||||
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);">
|
||||
<input v-model="phoneNumber" class="input-field" type="tel" placeholder="请输入手机号"
|
||||
maxlength="11" style="color: var(--van-text-color);" @focus="phoneFocused = true"
|
||||
@blur="phoneFocused = false" />
|
||||
</div>
|
||||
|
||||
<!-- 验证码输入 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
:class="[
|
||||
'input-container',
|
||||
<!-- 验证码输入 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div :class="[
|
||||
'input-container flex-1',
|
||||
codeFocused ? 'focused' : '',
|
||||
]"
|
||||
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);"
|
||||
>
|
||||
<input
|
||||
v-model="verificationCode"
|
||||
class="input-field"
|
||||
placeholder="请输入验证码"
|
||||
maxlength="6"
|
||||
style="color: var(--van-text-color);"
|
||||
@focus="codeFocused = true"
|
||||
@blur="codeFocused = false"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="ml-2 px-4 py-2 text-sm font-bold flex-shrink-0 rounded-lg transition duration-300"
|
||||
:class="
|
||||
isCountingDown || !isPhoneNumberValid
|
||||
style="background-color: var(--van-theme-primary-light); border: 2px solid rgba(162, 37, 37, 0);">
|
||||
<input v-model="verificationCode" class="input-field" placeholder="请输入验证码"
|
||||
maxlength="6" style="color: var(--van-text-color);" @focus="codeFocused = true"
|
||||
@blur="codeFocused = false" />
|
||||
</div>
|
||||
<button
|
||||
class="verify-code-btn px-3 sm:px-4 py-2.5 sm:py-3 text-xs sm:text-sm font-bold flex-shrink-0 rounded-lg transition duration-300 whitespace-nowrap"
|
||||
:class="isCountingDown || !isPhoneNumberValid
|
||||
? 'cursor-not-allowed bg-gray-300 text-gray-500'
|
||||
: 'text-white hover:opacity-90'
|
||||
"
|
||||
:style="isCountingDown || !isPhoneNumberValid
|
||||
? ''
|
||||
: 'background-color: var(--van-theme-primary);'"
|
||||
@click="sendVerificationCode"
|
||||
>
|
||||
{{
|
||||
isCountingDown
|
||||
? `${countdown}s重新获取`
|
||||
: "获取验证码"
|
||||
}}
|
||||
</button>
|
||||
" :style="isCountingDown || !isPhoneNumberValid
|
||||
? ''
|
||||
: 'background-color: var(--van-theme-primary);'"
|
||||
@click="sendVerificationCode">
|
||||
{{
|
||||
isCountingDown
|
||||
? `${countdown}s`
|
||||
: "获取验证码"
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 协议同意框 -->
|
||||
<div class="flex items-start gap-2">
|
||||
<input type="checkbox" v-model="isAgreed"
|
||||
class="mt-0.5 sm:mt-1 flex-shrink-0 accent-[#A22525]" />
|
||||
<span class="text-xs sm:text-sm leading-relaxed"
|
||||
style="color: var(--van-text-color-2);">
|
||||
我已阅读并同意
|
||||
<a class="cursor-pointer hover:underline" style="color: var(--van-theme-primary);"
|
||||
@click="toUserAgreement">《用户协议》</a>
|
||||
和
|
||||
<a class="cursor-pointer hover:underline" style="color: var(--van-theme-primary);"
|
||||
@click="toPrivacyPolicy">《隐私政策》</a>
|
||||
,并确认以上信息真实有效,将用于提现等资金操作
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 协议同意框 -->
|
||||
<div class="flex items-start space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="isAgreed"
|
||||
class="mt-1"
|
||||
/>
|
||||
<span class="text-xs leading-tight" style="color: var(--van-text-color-2);">
|
||||
我已阅读并同意
|
||||
<a
|
||||
class="cursor-pointer hover:underline"
|
||||
style="color: var(--van-theme-primary);"
|
||||
@click="toUserAgreement"
|
||||
>《用户协议》</a
|
||||
>
|
||||
和
|
||||
<a
|
||||
class="cursor-pointer hover:underline"
|
||||
style="color: var(--van-theme-primary);"
|
||||
@click="toPrivacyPolicy"
|
||||
>《隐私政策》</a
|
||||
>
|
||||
,并确认以上信息真实有效,将用于提现等资金操作
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
class="mb-6 sm:mb-8 mt-6 sm:mt-8 w-full py-2.5 sm:py-3 text-base sm:text-lg font-bold text-white rounded-full transition duration-300"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': !canSubmit }"
|
||||
:style="canSubmit ? 'background-color: var(--van-theme-primary);' : 'background-color: var(--van-text-color-3);'"
|
||||
@click="handleSubmit">
|
||||
确认认证
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="mb-12 mt-10 w-full py-3 text-lg font-bold text-white rounded-full transition duration-300"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': !canSubmit }"
|
||||
:style="canSubmit ? 'background-color: var(--van-theme-primary);' : 'background-color: var(--van-text-color-3);'"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
确认认证
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
@@ -328,22 +274,47 @@ function toPrivacyPolicy() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.real-name-auth-dialog {
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--van-border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.close-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-container {
|
||||
border-radius: 1rem;
|
||||
transition: duration-200;
|
||||
border-radius: 0.75rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.input-container {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.input-container.focused {
|
||||
@@ -352,11 +323,54 @@ function toPrivacyPolicy() {
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.input-field {
|
||||
padding: 0.875rem 1rem;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.input-field {
|
||||
padding: 1rem;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.verify-code-btn {
|
||||
min-width: 85px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.verify-code-btn {
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 优化小屏幕上的间距 */
|
||||
@media (max-width: 375px) {
|
||||
.input-field {
|
||||
padding: 0.625rem;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.verify-code-btn {
|
||||
min-width: 75px;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保弹窗在键盘弹出时可以滚动 */
|
||||
.real-name-auth-dialog-box {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
17
src/components/SectionTitle.vue
Normal file
17
src/components/SectionTitle.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-1.5 h-5 bg-[#A22525] rounded-xl"></div>
|
||||
<div class="text-lg text-gray-800">{{ title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -15,6 +15,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = ref(false);
|
||||
@@ -29,7 +33,7 @@ const copyToClipboard = async (text) => {
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
if (isLoading.value) return;
|
||||
if (isLoading.value || props.disabled) return;
|
||||
|
||||
// 如果是示例模式,直接分享当前URL
|
||||
if (props.isExample) {
|
||||
@@ -115,26 +119,15 @@ const handleShare = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-4 flex flex-col items-center gap-2">
|
||||
<van-button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="!bg-blue-500 !border-blue-500"
|
||||
:loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
@click="handleShare"
|
||||
>
|
||||
<template #icon>
|
||||
<van-icon name="share-o" />
|
||||
</template>
|
||||
<div class="bg-[#A22525] border border-[#A22525] rounded-[40px] px-3 py-1 flex items-center justify-center cursor-pointer hover:bg-[#8A1F1F] transition-colors duration-200"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': isLoading || disabled }" @click="handleShare">
|
||||
<img src="@/assets/images/report/fx.png" alt="分享" class="w-4 h-4 mr-1" />
|
||||
<span class="text-white text-sm font-medium">
|
||||
{{ isLoading ? "生成中..." : (isExample ? "分享示例" : "分享报告") }}
|
||||
</van-button>
|
||||
<div class="text-xs text-gray-500">{{ isExample ? "分享当前示例链接" : "分享链接将在7天后过期" }}</div>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-button) {
|
||||
min-width: 120px;
|
||||
}
|
||||
/* 样式已通过 Tailwind CSS 类实现 */
|
||||
</style>
|
||||
|
||||
44
src/components/StyledTabs.vue
Normal file
44
src/components/StyledTabs.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<van-tabs v-bind="$attrs" type="card" class="styled-tabs">
|
||||
<slot></slot>
|
||||
</van-tabs>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 透传所有属性和事件到 van-tabs
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* van-tabs 卡片样式定制 - 仅用于此组件 */
|
||||
.styled-tabs:deep(.van-tabs__line) {
|
||||
background-color: var(--van-theme-primary) !important;
|
||||
}
|
||||
|
||||
.styled-tabs:deep(.van-tabs__nav) {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.styled-tabs:deep(.van-tabs__nav--card) {
|
||||
border: unset !important;
|
||||
}
|
||||
|
||||
.styled-tabs:deep(.van-tab--card) {
|
||||
color: #666666 !important;
|
||||
border-right: unset !important;
|
||||
background-color: #eeeeee !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.styled-tabs:deep(.van-tab--active) {
|
||||
color: white !important;
|
||||
background-color: var(--van-theme-primary) !important;
|
||||
}
|
||||
|
||||
.styled-tabs:deep(.van-tabs__wrap) {
|
||||
background-color: #ffffff !important;
|
||||
padding: 9px 0;
|
||||
}
|
||||
</style>
|
||||
19
src/components/TitleBanner.vue
Normal file
19
src/components/TitleBanner.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="title-banner">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 不需要额外的 props 或逻辑,只是一个简单的样式组件
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title-banner {
|
||||
@apply mx-auto mt-2 w-64 py-2 text-center text-white;
|
||||
background-image: url('@/assets/images/report/title.png');
|
||||
background-size: auto 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
</style>
|
||||
180
src/components/VerificationCard.vue
Normal file
180
src/components/VerificationCard.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="card" style="padding-left: 0; padding-right: 0; padding-bottom: 24px;">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<!-- 报告信息 -->
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<LTitle title="报告信息"></LTitle>
|
||||
<!-- 分享按钮 -->
|
||||
<ShareReportButton v-if="!isShare" :order-id="orderId" :order-no="orderNo" :is-example="!orderId"
|
||||
class="mr-4" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 my-2 mx-4">
|
||||
<div class="flex pb-2 pl-2">
|
||||
<span class="text-[#666666] w-[6em]">报告时间:</span>
|
||||
<span class="text-gray-600">{{
|
||||
reportDateTime ||
|
||||
"2025-01-01 12:00:00"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex pb-2 pl-2" v-if="!isEmpty">
|
||||
<span class="text-[#666666] w-[6em]">报告项目:</span>
|
||||
<span class="text-gray-600 font-bold">
|
||||
{{ reportName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 报告对象 -->
|
||||
<template v-if="Object.keys(reportParams).length != 0">
|
||||
<LTitle title="报告对象"></LTitle>
|
||||
<div class="flex flex-col gap-2 my-2 mx-4">
|
||||
<!-- 姓名 -->
|
||||
<div class="flex pb-2 pl-2" v-if="reportParams?.name">
|
||||
<span class="text-[#666666] w-[6em]">姓名</span>
|
||||
<span class="text-gray-600">{{
|
||||
maskValue(
|
||||
"name",
|
||||
reportParams?.name
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 身份证号 -->
|
||||
<div class="flex pb-2 pl-2" v-if="reportParams?.id_card">
|
||||
<span class="text-[#666666] w-[6em]">身份证号</span>
|
||||
<span class="text-gray-600">{{
|
||||
maskValue(
|
||||
"id_card",
|
||||
reportParams?.id_card
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<div class="flex pb-2 pl-2" v-if="reportParams?.mobile">
|
||||
<span class="text-[#666666] w-[6em]">手机号</span>
|
||||
<span class="text-gray-600">{{
|
||||
maskValue(
|
||||
"mobile",
|
||||
reportParams?.mobile
|
||||
)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 验证卡片 -->
|
||||
<div class="flex flex-col gap-4 mt-4">
|
||||
<!-- 身份证检查结果 -->
|
||||
<div class="flex items-center px-4 py-3 flex-1 border border-[#EEEEEE] rounded-lg bg-[#F9F9F9]">
|
||||
<div class="w-11 h-11 flex items-center justify-center mr-4">
|
||||
<img src="@/assets/images/report/sfz.png" alt="身份证" class="w-10 h-10 object-contain" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-bold text-gray-800 text-lg">
|
||||
身份证检查结果
|
||||
</div>
|
||||
<div class="text-sm text-[#999999]">
|
||||
身份证信息核验通过
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-11 h-11 flex items-center justify-center ml-4">
|
||||
<img src="@/assets/images/report/zq.png" alt="资金安全" class="w-10 h-10 object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手机号检测结果 -->
|
||||
<div class="flex items-center px-4 py-3 flex-1 border border-[#EEEEEE] rounded-lg bg-[#F9F9F9]">
|
||||
<div class="w-11 h-11 flex items-center justify-center mr-4">
|
||||
<img src="@/assets/images/report/sjh.png" alt="手机号" class="w-10 h-10 object-contain" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-bold text-gray-800 text-lg">
|
||||
手机号检测结果
|
||||
</div>
|
||||
<div class="text-sm text-[#999999]">
|
||||
被查询人姓名与运营商提供的一致
|
||||
</div>
|
||||
<div class="text-sm text-[#999999]">
|
||||
被查询人身份证与运营商提供的一致
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-11 h-11 flex items-center justify-center ml-4">
|
||||
<img src="@/assets/images/report/zq.png" alt="资金安全" class="w-10 h-10 object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LTitle from './LTitle.vue'
|
||||
import ShareReportButton from './ShareReportButton.vue'
|
||||
|
||||
const props = defineProps({
|
||||
reportParams: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
reportDateTime: {
|
||||
type: [String, null],
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
reportName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isEmpty: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
isShare: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
orderId: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
orderNo: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// 脱敏函数
|
||||
const maskValue = (type, value) => {
|
||||
if (!value) return value;
|
||||
if (type === "name") {
|
||||
// 姓名脱敏(保留首位)
|
||||
if (value.length === 1) {
|
||||
return "*"; // 只保留一个字,返回 "*"
|
||||
} else if (value.length === 2) {
|
||||
return value[0] + "*"; // 两个字,保留姓氏,第二个字用 "*" 替代
|
||||
} else {
|
||||
return (
|
||||
value[0] +
|
||||
"*".repeat(value.length - 2) +
|
||||
value[value.length - 1]
|
||||
); // 两个字以上,保留第一个和最后一个字,其余的用 "*" 替代
|
||||
}
|
||||
} else if (type === "id_card") {
|
||||
// 身份证号脱敏(保留前6位和最后4位)
|
||||
return value.replace(/^(.{6})(?:\d+)(.{4})$/, "$1****$2");
|
||||
} else if (type === "mobile") {
|
||||
if (value.length === 11) {
|
||||
return value.substring(0, 3) + "****" + value.substring(7);
|
||||
}
|
||||
return value; // 如果手机号不合法或长度不为 11 位,直接返回原手机号
|
||||
}
|
||||
return value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 组件样式已通过 Tailwind CSS 类实现 */
|
||||
</style>
|
||||
Reference in New Issue
Block a user