Files
qnc-webview-v3/src/ui/IVYZ0S0D.vue
2026-02-28 17:29:22 +08:00

524 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { computed, ref } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const periodTab = ref('threeYears'); // 近三年 | 近五年
// 支持 data 或 data.result接口返回的 result 对象)
const result = computed(() => props.data?.result ?? props.data ?? {});
const getStatusText = (value) => {
if (value === 1) return '未命中';
if (value === 2) return '命中';
return '—';
};
// 获取通知函期间描述文本(支持数字或字符串如 "2"
const getNoticeLetterPeriodText = (period) => {
const p = Number(period);
const periodMap = { 0: '没有被发送通知函', 1: '近2年内', 2: '2-4年', 3: '5年以上' };
return periodMap[p] ?? '—';
};
// 检查是否至少有一个数据类别有内容
const hasAnyData = computed(() => {
const r = result.value;
return Object.keys(r).length > 0;
});
// 汇总数据 - 按分类分组 { key, title, rows }
const summaryGroups = computed(() => {
const groups = [];
const basic = result.value.basic_info;
if (basic?.risk_flag !== undefined) {
groups.push({ key: 'basic', title: '基础风险', rows: [{ label: '该人员是否有风险', value: basic.risk_flag }] });
}
const dishonesty = result.value.dishonesty?.dishonesty;
const highConsumption = result.value.high_consumption?.high_consumption;
if (dishonesty !== undefined || highConsumption !== undefined) {
const rows = [];
if (dishonesty !== undefined) rows.push({ label: '失信人员风险', value: dishonesty });
if (highConsumption !== undefined) rows.push({ label: '限制高消费人员风险', value: highConsumption });
groups.push({ key: 'credit', title: '失信限高', rows });
}
const labor = result.value.labor_disputes;
if (labor) {
const items = [['劳动争议', labor.labor_disputes], ['劳动合同纠纷', labor.labor_contract], ['劳动关系纠纷', labor.labor_relation], ['追索劳动报酬纠纷', labor.wage_claim], ['经济补偿金纠纷', labor.compensation], ['集体合同纠纷', labor.collective_contract], ['劳务派遣合同纠纷', labor.dispatch_contract], ['非全日制用工纠纷', labor.part_time], ['竞业限制纠纷', labor.non_compete]];
const rows = items.filter((item) => item[1] !== undefined).map((item) => ({ label: item[0], value: item[1] }));
if (rows.length) groups.push({ key: 'labor', title: '劳动争议', rows });
}
const social = result.value.social_insurance;
if (social) {
const items = [['社会保险纠纷', social.social_insurance], ['养老保险待遇纠纷', social.pension], ['工伤保险待遇纠纷', social.injury_insurance], ['医疗保险待遇纠纷', social.medical_insurance], ['生育保险待遇纠纷', social.maternity_insurance], ['商业保险待遇纠纷', social.commercial_insurance]];
const rows = items.filter((item) => item[1] !== undefined).map((item) => ({ label: item[0], value: item[1] }));
if (rows.length) groups.push({ key: 'social', title: '社会保险', rows });
}
if (result.value.welfare_disputes?.welfare !== undefined) {
groups.push({ key: 'welfare', title: '福利待遇', rows: [{ label: '福利待遇纠纷', value: result.value.welfare_disputes.welfare }] });
}
const personnel = result.value.personnel_disputes;
if (personnel) {
const items = [['人事争议类纠纷', personnel.personnel_dispute], ['辞职争议纠纷', personnel.resignation_dispute], ['辞退争议纠纷', personnel.dismissal_dispute], ['聘用合同争议纠纷', personnel.employment_contract]];
const rows = items.filter((item) => item[1] !== undefined).map((item) => ({ label: item[0], value: item[1] }));
if (rows.length) groups.push({ key: 'personnel', title: '人事争议', rows });
}
const arb = result.value.arbitration;
if (arb && (arb.arbitration_confirmation !== undefined || arb.arbitration_revocation !== undefined)) {
const rows = [];
if (arb.arbitration_confirmation !== undefined) rows.push({ label: '申请仲裁确认', value: arb.arbitration_confirmation });
if (arb.arbitration_revocation !== undefined) rows.push({ label: '撤销仲裁裁决', value: arb.arbitration_revocation });
groups.push({ key: 'arbitration', title: '仲裁流程', rows });
}
const notice = result.value.notice_letter;
if (notice?.notice_letter !== undefined) {
const rows = [{ label: '通知函触达', value: notice.notice_letter }];
if (notice.notice_letter_period !== undefined && notice.notice_letter === 2) rows.push({ label: '通知函发送时间', value: null, period: notice.notice_letter_period });
groups.push({ key: 'notice', title: '通知函触达', rows });
}
return groups;
});
const summaryRows = computed(() => summaryGroups.value.flatMap((g) => g.rows));
// 真正的风险项(文档:失信限高、劳动争议、社会保险、福利待遇、人事争议、仲裁流程、通知函触达)
// 排除 basic_info.risk_flag汇总结论和 notice_letter_period非风险项
const riskItemRows = computed(() =>
summaryGroups.value
.filter((g) => g.key !== 'basic')
.flatMap((g) => g.rows)
.filter((r) => r.value === 1 || r.value === 2)
);
// 近三年/近五年 - 按分类分组
const periodGroups = computed(() => {
const suffix = periodTab.value === 'threeYears' ? '_3y' : '_5y';
const groups = [];
const labor = result.value.labor_disputes;
if (labor) {
const keys = ['labor_disputes', 'labor_relation', 'wage_claim', 'compensation', 'collective_contract', 'dispatch_contract', 'part_time', 'non_compete'];
const labels = ['劳动争议', '劳动关系', '追索劳动报酬', '经济补偿金', '集体合同', '劳务派遣', '非全日制用工', '竞业限制'];
const rows = keys.map((k, i) => ({ label: labels[i], value: labor[k + suffix] })).filter((r) => r.value === 2).map((r) => ({ label: r.label, value: 2 }));
if (rows.length) groups.push({ key: 'labor', title: '劳动争议', rows });
}
const social = result.value.social_insurance;
if (social) {
const keys = ['pension', 'injury_insurance', 'medical_insurance', 'maternity_insurance', 'commercial_insurance'];
const labels = ['养老保险', '工伤保险', '医疗保险', '生育保险', '商业保险'];
const rows = keys.map((k, i) => ({ label: labels[i], value: social[k + suffix] })).filter((r) => r.value === 2).map((r) => ({ label: r.label, value: 2 }));
if (rows.length) groups.push({ key: 'social', title: '社会保险', rows });
}
const personnel = result.value.personnel_disputes;
if (personnel) {
const keys = ['resignation_dispute', 'dismissal_dispute', 'employment_contract'];
const labels = ['辞职争议', '辞退争议', '聘用合同'];
const rows = keys.map((k, i) => ({ label: labels[i], value: personnel[k + suffix] })).filter((r) => r.value === 2).map((r) => ({ label: r.label, value: 2 }));
if (rows.length) groups.push({ key: 'personnel', title: '人事争议', rows });
}
const arb = result.value.arbitration;
if (arb) {
const rows = [];
if (arb[`arbitration_confirmation${suffix}`] === 2) rows.push({ label: '申请仲裁确认', value: 2 });
if (arb[`arbitration_revocation${suffix}`] === 2) rows.push({ label: '撤销仲裁裁决', value: 2 });
if (rows.length) groups.push({ key: 'arbitration', title: '仲裁流程', rows });
}
return groups;
});
const periodRows = computed(() => periodGroups.value.flatMap((g) => g.rows));
// 用于 riskScore需要近三年+近五年全部数据
const recentThreeYearsRows = computed(() => {
const r = result.value;
const rows = [];
const labor = r.labor_disputes;
if (labor) {
[['labor_disputes_3y'], ['labor_relation_3y'], ['wage_claim_3y'], ['compensation_3y'], ['collective_contract_3y'], ['dispatch_contract_3y'], ['part_time_3y'], ['non_compete_3y']].forEach(([k]) => { if (labor[k] === 2) rows.push({ value: 2 }); });
}
const social = r.social_insurance;
if (social) {
['pension_3y', 'injury_insurance_3y', 'medical_insurance_3y', 'maternity_insurance_3y', 'commercial_insurance_3y'].forEach((k) => { if (social[k] === 2) rows.push({ value: 2 }); });
}
const personnel = r.personnel_disputes;
if (personnel) {
['resignation_dispute_3y', 'dismissal_dispute_3y', 'employment_contract_3y'].forEach((k) => { if (personnel[k] === 2) rows.push({ value: 2 }); });
}
const arb = r.arbitration;
if (arb) {
if (arb.arbitration_confirmation_3y === 2) rows.push({ value: 2 });
if (arb.arbitration_revocation_3y === 2) rows.push({ value: 2 });
}
return rows;
});
const recentFiveYearsRows = computed(() => {
const r = result.value;
const rows = [];
const labor = r.labor_disputes;
if (labor) {
[['labor_disputes_5y'], ['labor_relation_5y'], ['wage_claim_5y'], ['compensation_5y'], ['collective_contract_5y'], ['dispatch_contract_5y'], ['part_time_5y'], ['non_compete_5y']].forEach(([k]) => { if (labor[k] === 2) rows.push({ value: 2 }); });
}
const social = r.social_insurance;
if (social) {
['pension_5y', 'injury_insurance_5y', 'medical_insurance_5y', 'maternity_insurance_5y', 'commercial_insurance_5y'].forEach((k) => { if (social[k] === 2) rows.push({ value: 2 }); });
}
const personnel = r.personnel_disputes;
if (personnel) {
['resignation_dispute_5y', 'dismissal_dispute_5y', 'employment_contract_5y'].forEach((k) => { if (personnel[k] === 2) rows.push({ value: 2 }); });
}
const arb = r.arbitration;
if (arb) {
if (arb.arbitration_confirmation_5y === 2) rows.push({ value: 2 });
if (arb.arbitration_revocation_5y === 2) rows.push({ value: 2 });
}
return rows;
});
// 头部风险总结:风险类型、建议、命中统计(仅真正的风险项)
const riskSummary = computed(() => {
const basic = result.value.basic_info;
const hasRisk = basic?.risk_flag === 2;
// 真正的风险项:总项数、命中数(文档:失信限高、劳动争议、社会保险、福利待遇、人事争议、仲裁流程、通知函触达)
const totalItems = riskItemRows.value.length;
const hitItems = riskItemRows.value.filter((r) => r.value === 2).length;
// 命中的风险分类(汇总中 value=2 的 group
const riskCategories = summaryGroups.value
.filter((g) => g.key !== 'basic' && g.rows.some((r) => r.value === 2))
.map((g) => g.title);
// 精简建议(取主要类型合并为一句)
const suggestionMap = {
'失信限高': '征信修复与限高事项',
'劳动争议': '劳动纠纷与薪酬离职',
'社会保险': '社保缴纳与补缴',
'福利待遇': '福利待遇合规',
'人事争议': '辞职辞退与聘用合同',
'仲裁流程': '仲裁案件进展',
'通知函触达': '仲裁调解涉诉通知',
};
const suggestionParts = riskCategories.map((c) => suggestionMap[c]).filter(Boolean);
const suggestion = suggestionParts.length ? `建议关注${suggestionParts.slice(0, 3).join('、')}` : '';
return {
hasRisk,
label: hasRisk ? '有风险' : '无风险',
riskCategories,
suggestion,
totalItems,
hitItems,
};
});
// 风险评分 0-100越高越安全供 BaseReport 分析指数)
// 基于真正的风险项riskItemRows与展示逻辑一致通过 useRiskNotifier 递交 BaseReport
const riskScore = computed(() => {
const basic = result.value.basic_info;
if (!basic || basic.risk_flag === 1) return 100;
const hitItems = riskItemRows.value.filter((r) => r.value === 2).length;
const score = 100 - hitItems * 2; // 每命中 1 项扣 2 分
return Math.max(0, Math.min(100, score));
});
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
// 借鉴司法涉诉概览:风险图标与背景样式
const getRiskIcon = () => {
if (riskSummary.value.hasRisk) return new URL('@/assets/images/report/gfx.png', import.meta.url).href;
return new URL('@/assets/images/report/zq.png', import.meta.url).href;
};
</script>
<template>
<div class="report-wrap">
<!-- 头部风险总结底色固定白色命中项用内嵌 card 展现 -->
<div v-if="hasAnyData" class="risk-summary card">
<div class="flex items-center mb-4">
<div class="w-12 h-12 mr-3 flex-shrink-0">
<img :src="getRiskIcon()" alt="风险" class="w-12 h-12 object-contain" />
</div>
<div class="text-gray-700 text-[15px] leading-relaxed">
<template v-if="riskSummary.hasRisk">
<span v-if="riskSummary.riskCategories.length">涉及{{ riskSummary.riskCategories.join('')
}}</span>
<span v-if="riskSummary.suggestion">{{ riskSummary.suggestion }}</span>
</template>
<template v-else>未检测到相关风险</template>
</div>
</div>
<!-- 命中项仅显示总项数 / 命中数真正的风险项失信限高劳动争议社会保险福利待遇人事争议仲裁流程通知函触达 -->
<div v-if="riskSummary.totalItems > 0" class="inner-card p-4 rounded-xl text-center"
:class="riskSummary.hitItems > 0 ? 'inner-card-risk' : 'inner-card-safe'">
<div class="text-2xl font-bold mb-1"
:class="riskSummary.hitItems > 0 ? 'text-[#EB3C3C]' : 'text-[#10b981]'">
{{ riskSummary.hitItems }}/{{ riskSummary.totalItems }}
</div>
<div class="text-sm font-medium text-gray-800">命中项</div>
</div>
</div>
<!-- Card 1: 汇总 -->
<section class="card">
<header class="card-header">
<span class="card-title">风险概览</span>
<span class="card-subtitle">综合评估结果</span>
</header>
<div v-if="hasAnyData" class="group-list">
<div v-for="(group, gi) in summaryGroups" :key="group.key" class="group-box">
<div class="group-header">
<span class="group-title">{{ group.title }}</span>
</div>
<div class="group-body">
<div v-for="(row, ri) in group.rows" :key="ri" class="data-row">
<span class="data-label">{{ row.label }}</span>
<span v-if="row.period !== undefined" class="data-value data-value-text">{{
getNoticeLetterPeriodText(row.period) }}</span>
<span v-else
:class="['data-value', 'data-badge', row.value === 2 ? 'badge-risk' : 'badge-safe']">
<span class="badge-dot" :class="row.value === 2 ? 'dot-risk' : 'dot-safe'" />
{{ getStatusText(row.value) }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<span class="empty-icon"></span>
<span class="empty-text">暂无相关风险数据</span>
</div>
</section>
<!-- Card 2: 近三年 / 近五年 -->
<section class="card">
<header class="card-header">
<span class="card-title">时间维度</span>
<span class="card-subtitle">按周期查看命中情况</span>
</header>
<div class="period-tabs">
<button type="button" :class="['period-tab', periodTab === 'threeYears' && 'active']"
@click="periodTab = 'threeYears'">近三年</button>
<button type="button" :class="['period-tab', periodTab === 'fiveYears' && 'active']"
@click="periodTab = 'fiveYears'">近五年</button>
</div>
<div v-if="periodGroups.length" class="group-list">
<div v-for="(group, gi) in periodGroups" :key="group.key" class="group-box">
<div class="group-header">
<span class="group-title">{{ group.title }}</span>
</div>
<div class="group-body">
<div v-for="(row, ri) in group.rows" :key="ri" class="data-row">
<span class="data-label">{{ row.label }}</span>
<span class="data-value data-badge badge-risk">
<span class="badge-dot dot-risk" />
{{ getStatusText(row.value) }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<span class="empty-icon"></span>
<span class="empty-text">暂无{{ periodTab === 'threeYears' ? '近三年' : '近五年' }}命中数据</span>
</div>
</section>
</div>
</template>
<style lang="scss" scoped>
.report-wrap {
display: flex;
flex-direction: column;
gap: 16px;
}
.card {
background: #fff;
border-radius: 12px;
padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.inner-card {
border: 1px solid #e2e8f0;
}
.inner-card-risk {
background: rgba(235, 60, 60, 0.1);
border-color: rgba(235, 60, 60, 0.3);
}
.inner-card-safe {
background: rgba(16, 185, 129, 0.1);
border-color: rgba(16, 185, 129, 0.3);
}
.card-header {
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px solid #f1f5f9;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
letter-spacing: 0.02em;
}
.card-subtitle {
font-size: 13px;
color: #94a3b8;
}
.period-tabs {
display: flex;
gap: 8px;
margin-bottom: 14px;
padding: 4px;
background: #f8fafc;
border-radius: 10px;
}
.period-tab {
flex: 1;
padding: 10px 16px;
font-size: 15px;
color: #64748b;
background: transparent;
border: none;
border-radius: 8px;
cursor: pointer;
}
.period-tab.active {
color: #1e293b;
font-weight: 500;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.group-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.group-box {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
}
.group-header {
padding: 12px 16px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.group-title {
font-size: 16px;
font-weight: 600;
color: #475569;
letter-spacing: 0.03em;
}
.group-body {
display: flex;
flex-direction: column;
padding: 0 16px;
}
.data-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
font-size: 16px;
}
.data-row:not(:last-child) {
border-bottom: 1px solid #f1f5f9;
}
.data-label {
color: #475569;
flex: 1;
margin-right: 12px;
font-size: 16px;
line-height: 1.5;
}
.data-value {
flex-shrink: 0;
}
.data-value-text {
color: #64748b;
font-size: 15px;
}
.data-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: 500;
}
.badge-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.badge-risk {
background: #fff1f2;
color: #be123c;
}
.dot-risk {
background: #e11d48;
}
.badge-safe {
background: #f0fdf4;
color: #047857;
}
.dot-safe {
background: #10b981;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 28px 16px;
}
.empty-icon {
font-size: 20px;
color: #cbd5e1;
font-weight: 300;
}
.empty-text {
font-size: 15px;
color: #94a3b8;
}
</style>