重构家庭资产负债详情展示

This commit is contained in:
wkc
2026-03-25 19:28:54 +08:00
parent 17a6c389d1
commit 60f935da27
20 changed files with 691 additions and 565 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,21 +0,0 @@
# 结果总览详情弹窗展示优化前端实施记录
## 本轮实施内容
- 头带重排:移除弹窗内部大白卡壳层,改为头带 + 风险摘要条 + 主体工作区结构,并将“当前命中模型”并入头带上下文区域。
- 侧栏改造:将左侧区域改为人物档案面板,重组为人物身份、模型摘要、辅助提示 3 个层级,保留空值统一显示 `-`
- 主区统一:为异常明细与占位页签统一 `analysis-panel` 工作台视觉基线,补充分组摘要句,收口表格、对象卡片和占位容器的边框、圆角与背景风格。
- 边界保持:本轮未改动结果总览入口、路由、详情接口、数据来源边界与默认页签行为。
## 涉及文件
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisSidebar.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisPlaceholderTab.vue`
- `ruoyi-ui/tests/unit/project-analysis-dialog-layout.test.js`
- `ruoyi-ui/tests/unit/project-analysis-dialog-default-tab.test.js`
- `ruoyi-ui/tests/unit/project-analysis-dialog-source-highlight.test.js`
- `ruoyi-ui/tests/unit/project-analysis-dialog-sidebar.test.js`
- `ruoyi-ui/tests/unit/project-analysis-dialog-empty-field.test.js`
- `ruoyi-ui/tests/unit/project-analysis-dialog-abnormal-tab.test.js`

View File

@@ -0,0 +1,70 @@
# 2026-03-25 专项核查员工家庭资产负债展开区改版前端实施记录
## 本次修改文件
### 生产代码
- `ruoyi-ui/src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue`
### 测试
- `ruoyi-ui/tests/unit/special-check-detail-layout.test.js`
- `ruoyi-ui/tests/unit/special-check-detail-summary-groups.test.js`
- `ruoyi-ui/tests/unit/special-check-visual-alignment.test.js`
- `ruoyi-ui/tests/unit/special-check-detail-date-display.test.js`
## 实施范围
- 本次只改专项核查“员工家庭资产负债专项核查”的展开详情组件
- 列表层 `FamilyAssetLiabilitySection.vue`、展开入口、详情缓存、按需请求与接口路径均未改动
- 未新增后端字段、未新增路由、未新增弹窗或抽屉容器
## 实现内容
### 1. 展开区结构改为 5 段纵向汇总卡片
- 将旧的“三列卡片 + 资产/负债表格”改为单列纵向结构
- 固定展示顺序为:
- `总收入`
- `总负债`
- `总资产`
- `关键指标`
- `详查结果`
- 每张卡片标题右侧直接展示汇总值或结果名称
### 2. 资产与负债明细改为来源项聚合
- 资产来源项基于 `assetDetail.items` 按现有类型字段聚合展示
- 负债来源项基于 `debtDetail.items` 按现有类型字段聚合展示
- 每条来源项统一展示:
- 类型名
- 聚合金额
- 占总额比例
- 彻底移除 `el-table` 明细表结构,避免继续展示逐条流水式详情
### 2.1 细项名称展示口径调整
- 根据补充要求,总资产卡片内的细项名称优先展示 `assetName`
- 总负债卡片内的细项名称优先展示 `debtName`
- 仅当名称为空时,才回退使用现有类型字段作为兜底文案
### 3. 关键指标与风险结论改为前端就地计算
- 继续复用现有 `incomeDetail / assetDetail / debtDetail / summary` 数据
- 前端新增以下计算能力:
- `净资产 = 总资产 - 总负债`
- `资产负债率 = 总负债 / 总资产`
- `资产/收入比 = 总资产 / 总收入`
- `负债/收入比 = 总负债 / 总收入`
- 分母为 `0` 时统一展示 `-`
- 详查结果按 `summary.riskLevelCode` 映射文案与样式:
- `NORMAL -> 结构基本合理`
- `RISK -> 负债与收入压力偏高`
- `HIGH -> 资产负债结构明显异常`
- `MISSING_INFO -> 当前信息不完整`
## 边界说明
- 本次未改列表列顺序、风险标签、查看详情入口与项目切换逻辑
- 本次未改接口契约,汇总值优先复用 `summary`,不足时回退详情明细中的现有总额字段
- 日期格式化工具函数仍保留,延续既有金额/日期工具风格

View File

@@ -1,37 +0,0 @@
# 结果总览详情弹窗展示优化前端验证记录
## 验证信息
- 执行日期2026-03-25
- 执行范围:结果总览详情弹窗展示优化前端专项回归
- 验证结论:通过
## 执行命令
```bash
cd ruoyi-ui
node tests/unit/project-analysis-dialog-layout.test.js
node tests/unit/project-analysis-dialog-default-tab.test.js
node tests/unit/project-analysis-dialog-source-highlight.test.js
node tests/unit/project-analysis-dialog-sidebar.test.js
node tests/unit/project-analysis-dialog-empty-field.test.js
node tests/unit/project-analysis-dialog-abnormal-tab.test.js
node tests/unit/preliminary-check-project-analysis-entry.test.js
node tests/unit/preliminary-check-project-analysis-source-context.test.js
```
## 结果记录
- `project-analysis-dialog-layout.test.js`PASS
- `project-analysis-dialog-default-tab.test.js`PASS
- `project-analysis-dialog-source-highlight.test.js`PASS
- `project-analysis-dialog-sidebar.test.js`PASS
- `project-analysis-dialog-empty-field.test.js`PASS
- `project-analysis-dialog-abnormal-tab.test.js`PASS
- `preliminary-check-project-analysis-entry.test.js`PASS
- `preliminary-check-project-analysis-source-context.test.js`PASS
## 人工核验说明
- 本轮未额外启动前端开发服务,未执行浏览器人工核验。
- 结构性验证以单测断言为准,已覆盖首屏层级、侧栏档案面板、主区统一面板样式、来源上下文与入口行为保持不变等关键边界。

View File

@@ -0,0 +1,49 @@
# 2026-03-25 专项核查员工家庭资产负债展开区改版前端验证记录
## 执行命令
```bash
cd ruoyi-ui
node tests/unit/special-check-detail-layout.test.js
node tests/unit/special-check-detail-summary-groups.test.js
node tests/unit/special-check-visual-alignment.test.js
node tests/unit/special-check-detail-expand.test.js
node tests/unit/special-check-detail-date-display.test.js
```
```bash
cd ruoyi-ui
npm run build:prod
```
## 执行结果
- 执行时间2026-03-25 16:13:16 +0800
- 5 个专项核查详情相关静态验证脚本全部通过
- `npm run build:prod` 构建通过
## 输出摘要
```text
special-check-detail-layout test passed
special-check-detail-summary-groups test passed
special-check-detail-date-display test passed
special-check-visual-alignment.test.js PASS
special-check-detail-expand.test.js PASS
DONE Build complete. The dist directory is ready to be deployed.
```
## 重点验证项
- 已验证展开区顺序固定为 `总收入 -> 总负债 -> 总资产 -> 关键指标 -> 详查结果`
- 已验证资产、负债去表格化,改为来源项聚合展示
- 已验证总资产细项优先展示 `assetName`,总负债细项优先展示 `debtName`
- 已验证关键指标包含资产负债率、资产/收入比、负债/收入比的前端计算逻辑
- 已验证列表层展开入口、详情缓存与按需请求逻辑未被破坏
## 告警说明
- 生产构建仍存在既有资源体积告警:
- `asset size limit`
- `entrypoint size limit`
- 上述告警为仓库现存构建告警,本次详情区改版未引入新的构建失败

View File

@@ -1,12 +1,10 @@
<template> <template>
<div class="family-asset-liability-detail" v-loading="loading"> <div class="family-asset-liability-detail" v-loading="loading">
<div class="detail-grid"> <div class="detail-stack">
<section class="detail-block"> <section class="detail-block">
<div class="block-header"> <div class="block-header section-header">
<div> <div class="block-title">总收入</div>
<div class="block-title">收入明细</div> <div class="section-summary-value">{{ formatAmount(totalIncome) }}</div>
<div class="block-subtitle">展示本人配偶与家庭收入汇总</div>
</div>
</div> </div>
<div class="detail-metric-list"> <div class="detail-metric-list">
<div class="detail-metric-item"> <div class="detail-metric-item">
@@ -17,72 +15,79 @@
<span class="metric-label">配偶收入</span> <span class="metric-label">配偶收入</span>
<span class="metric-value">{{ formatAmount(incomeDetail.spouseIncome) }}</span> <span class="metric-value">{{ formatAmount(incomeDetail.spouseIncome) }}</span>
</div> </div>
<div class="detail-metric-item"> </div>
<span class="metric-label">家庭总收入</span> </section>
<span class="metric-value">{{ formatAmount(incomeDetail.totalIncome) }}</span>
<section class="detail-block">
<div class="block-header section-header">
<div class="block-title">总负债</div>
<div class="section-summary-value">{{ formatAmount(totalDebt) }}</div>
</div>
<div v-if="debtGroups.length" class="summary-group-list">
<div v-for="group in debtGroups" :key="group.key" class="summary-group-item">
<div class="summary-group-main">
<span class="summary-group-name">{{ group.label }}</span>
<span class="summary-group-amount">{{ formatAmount(group.amount) }}</span>
</div>
<div class="summary-group-meta">
<span class="summary-group-share-label">占比</span>
<span class="summary-group-share">{{ formatPercent(group.amount, totalDebt) }}</span>
</div>
</div>
</div>
<div v-else class="empty-hint">暂无负债来源项</div>
</section>
<section class="detail-block">
<div class="block-header section-header">
<div class="block-title">总资产</div>
<div class="section-summary-value">{{ formatAmount(totalAsset) }}</div>
</div>
<div v-if="assetGroups.length" class="summary-group-list">
<div v-for="group in assetGroups" :key="group.key" class="summary-group-item">
<div class="summary-group-main">
<span class="summary-group-name">{{ group.label }}</span>
<span class="summary-group-amount">{{ formatAmount(group.amount) }}</span>
</div>
<div class="summary-group-meta">
<span class="summary-group-share-label">占比</span>
<span class="summary-group-share">{{ formatPercent(group.amount, totalAsset) }}</span>
</div>
</div>
</div>
<div v-else class="empty-hint">暂无资产来源项</div>
</section>
<section class="detail-block">
<div class="block-header section-header">
<div class="block-title">关键指标</div>
<div class="section-summary-value">3 </div>
</div>
<div class="metric-grid">
<div class="metric-card">
<span class="metric-card-label">资产负债率</span>
<span class="metric-card-value">{{ assetDebtRatio }}</span>
</div>
<div class="metric-card">
<span class="metric-card-label">资产/收入比</span>
<span class="metric-card-value">{{ assetIncomeRatio }}</span>
</div>
<div class="metric-card">
<span class="metric-card-label">负债/收入比</span>
<span class="metric-card-value">{{ debtIncomeRatio }}</span>
</div> </div>
</div> </div>
</section> </section>
<section class="detail-block"> <section class="detail-block">
<div class="block-header"> <div class="block-header section-header">
<div> <div class="block-title">详查结果</div>
<div class="block-title">负债明细</div> <div class="section-summary-value">{{ riskResult.name }}</div>
<div class="block-subtitle">展示本人及配偶负债小计与明细列表</div>
</div>
</div> </div>
<div v-if="!debtDetail.missingSelfDebtInfo" class="detail-summary"> <div class="result-card" :class="[`result-card--${riskResult.type}`]">
<span>本人负债小计{{ formatAmount(debtDetail.selfTotalDebt) }}</span> <div class="result-card-label">结论</div>
<span>配偶负债小计{{ formatAmount(debtDetail.spouseTotalDebt) }}</span> <div class="result-card-value">{{ riskResult.text }}</div>
</div> </div>
<el-table v-if="debtItems.length" :data="debtItems" size="mini" class="detail-table">
<el-table-column prop="debtName" label="负债名称" min-width="140" />
<el-table-column prop="debtMainType" label="负债大类" min-width="100" />
<el-table-column prop="debtSubType" label="负债小类" min-width="120" />
<el-table-column prop="creditorType" label="债权人类型" min-width="120" />
<el-table-column prop="ownerName" label="归属人" min-width="100" />
<el-table-column label="本金余额" min-width="120">
<template slot-scope="scope">
<span>{{ formatAmount(scope.row.principalBalance) }}</span>
</template>
</el-table-column>
<el-table-column label="查询日期" min-width="160">
<template slot-scope="scope">
<span>{{ formatDetailDateTime(scope.row.queryDate) }}</span>
</template>
</el-table-column>
</el-table>
<el-empty v-else :image-size="64" description="暂无负债明细" />
</section>
<section class="detail-block">
<div class="block-header">
<div>
<div class="block-title">资产明细</div>
<div class="block-subtitle">展示本人及配偶资产小计与明细列表</div>
</div>
</div>
<div v-if="!assetDetail.missingSelfAssetInfo" class="detail-summary">
<span>本人资产小计{{ formatAmount(assetDetail.selfTotalAsset) }}</span>
<span>配偶资产小计{{ formatAmount(assetDetail.spouseTotalAsset) }}</span>
</div>
<el-table v-if="assetItems.length" :data="assetItems" size="mini" class="detail-table">
<el-table-column prop="assetName" label="资产名称" min-width="140" />
<el-table-column prop="assetMainType" label="资产大类" min-width="100" />
<el-table-column prop="assetSubType" label="资产小类" min-width="120" />
<el-table-column prop="holderName" label="持有人" min-width="100" />
<el-table-column label="当前估值" min-width="120">
<template slot-scope="scope">
<span>{{ formatAmount(scope.row.currentValue) }}</span>
</template>
</el-table-column>
<el-table-column label="估值日期" min-width="160">
<template slot-scope="scope">
<span>{{ formatDetailDateTime(scope.row.valuationDate) }}</span>
</template>
</el-table-column>
</el-table>
<el-empty v-else :image-size="64" description="暂无资产明细" />
</section> </section>
</div> </div>
</div> </div>
@@ -113,6 +118,17 @@ export default {
totalIncome: 0, totalIncome: 0,
}; };
}, },
summary() {
return this.detail && this.detail.summary
? this.detail.summary
: {
totalIncome: 0,
totalAsset: 0,
totalDebt: 0,
riskLevelCode: "MISSING_INFO",
riskLevelName: "缺少信息",
};
},
assetDetail() { assetDetail() {
return this.detail && this.detail.assetDetail return this.detail && this.detail.assetDetail
? this.detail.assetDetail ? this.detail.assetDetail
@@ -135,20 +151,135 @@ export default {
items: [], items: [],
}; };
}, },
totalIncome() {
return this.resolveAmount(this.summary.totalIncome, this.incomeDetail.totalIncome);
},
totalAsset() {
return this.resolveAmount(this.summary.totalAsset, this.assetDetail.totalAsset);
},
totalDebt() {
return this.resolveAmount(this.summary.totalDebt, this.debtDetail.totalDebt);
},
assetItems() { assetItems() {
return Array.isArray(this.assetDetail.items) ? this.assetDetail.items : []; return Array.isArray(this.assetDetail.items) ? this.assetDetail.items : [];
}, },
debtItems() { debtItems() {
return Array.isArray(this.debtDetail.items) ? this.debtDetail.items : []; return Array.isArray(this.debtDetail.items) ? this.debtDetail.items : [];
}, },
assetGroups() {
return this.buildAmountGroups(this.assetItems, "currentValue", this.resolveAssetGroupLabel, this.totalAsset);
},
debtGroups() {
return this.buildAmountGroups(this.debtItems, "principalBalance", this.resolveDebtGroupLabel, this.totalDebt);
},
// 净资产 = 总资产 - 总负债
netAsset() {
return this.totalAsset - this.totalDebt;
},
// 资产负债率 = totalDebt / totalAsset
assetDebtRatio() {
return this.formatMetricValue(this.totalDebt / this.totalAsset, {
denominator: this.totalAsset,
suffix: "%",
isPercent: true,
});
},
// 资产/收入比 = totalAsset / totalIncome
assetIncomeRatio() {
return this.formatMetricValue(this.totalAsset / this.totalIncome, {
denominator: this.totalIncome,
suffix: "倍",
});
},
// 负债/收入比 = totalDebt / totalIncome
debtIncomeRatio() {
return this.formatMetricValue(this.totalDebt / this.totalIncome, {
denominator: this.totalIncome,
suffix: "倍",
});
},
riskResult() {
const riskResultMap = {
NORMAL: {
name: this.summary.riskLevelName || "正常",
text: "结构基本合理",
type: "success",
},
RISK: {
name: this.summary.riskLevelName || "存在风险",
text: "负债与收入压力偏高",
type: "warning",
},
HIGH: {
name: this.summary.riskLevelName || "高风险",
text: "资产负债结构明显异常",
type: "danger",
},
MISSING_INFO: {
name: this.summary.riskLevelName || "缺少信息",
text: "当前信息不完整",
type: "info",
},
};
return riskResultMap[this.summary.riskLevelCode] || riskResultMap.MISSING_INFO;
},
}, },
methods: { methods: {
resolveAmount(...values) {
const target = values.find((value) => value !== undefined && value !== null && value !== "");
return Number(target || 0);
},
formatAmount(value) { formatAmount(value) {
const amount = Number(value || 0); const amount = Number(value || 0);
return `${amount.toLocaleString("zh-CN", { return `¥${amount.toLocaleString("zh-CN", {
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2, maximumFractionDigits: 2,
})}`; })}`;
},
buildAmountGroups(items, amountField, resolveLabel, totalAmount) {
const groupMap = items.reduce((result, item, index) => {
const label = resolveLabel(item);
const key = `${label}-${index}`;
if (!result[label]) {
result[label] = {
key,
label,
amount: 0,
};
}
result[label].amount += Number(item[amountField] || 0);
return result;
}, {});
return Object.values(groupMap)
.map((group) => ({
...group,
share: this.formatPercent(group.amount, totalAmount),
}))
.sort((left, right) => right.amount - left.amount);
},
resolveAssetGroupLabel(item) {
return item.assetName || [item.assetMainType, item.assetSubType].filter(Boolean).join(" / ") || "未分类资产";
},
resolveDebtGroupLabel(item) {
return item.debtName || [item.debtMainType, item.debtSubType].filter(Boolean).join(" / ") || "未分类负债";
},
formatPercent(numerator, denominator) {
if (!denominator) {
return "-";
}
return `${((Number(numerator || 0) / Number(denominator)) * 100).toFixed(2)}%`;
},
formatMetricValue(value, options = {}) {
const { denominator, suffix, isPercent } = options;
// 分母为 0 时显示 -
if (!denominator) {
return "-";
}
if (isPercent) {
return `${(Number(value || 0) * 100).toFixed(2)}${suffix}`;
}
return `${Number(value || 0).toFixed(2)}${suffix}`;
}, },
formatDetailDateTime(value) { formatDetailDateTime(value) {
if (!value) { if (!value) {
@@ -170,23 +301,24 @@ export default {
padding: 8px 0 4px; padding: 8px 0 4px;
} }
.detail-grid { .detail-stack {
display: grid; display: grid;
gap: 12px; gap: 12px;
grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.detail-block { .detail-block {
padding: 16px; padding: 16px;
background: #f8fafc; background: #f8fafc;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 12px;
min-width: 0; min-width: 0;
} }
.block-header { .section-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start;
gap: 16px;
margin-bottom: 12px; margin-bottom: 12px;
} }
@@ -196,10 +328,11 @@ export default {
color: #1f2937; color: #1f2937;
} }
.block-subtitle { .section-summary-value {
margin-top: 4px; flex-shrink: 0;
font-size: 12px; font-size: 18px;
color: #94a3b8; font-weight: 700;
color: #111827;
} }
.detail-metric-list { .detail-metric-list {
@@ -220,26 +353,118 @@ export default {
color: #111827; color: #111827;
} }
.detail-summary { .summary-group-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.summary-group-item {
padding: 12px 14px;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 10px;
}
.summary-group-main,
.summary-group-meta {
display: flex; display: flex;
flex-wrap: wrap; justify-content: space-between;
gap: 16px; align-items: center;
margin-bottom: 12px; gap: 12px;
}
.summary-group-meta {
margin-top: 8px;
}
.summary-group-name,
.summary-group-share-label {
font-size: 13px; font-size: 13px;
color: #475569; color: #475569;
} }
.detail-table { .summary-group-amount,
border-radius: 12px; .summary-group-share {
overflow: hidden; font-size: 13px;
font-weight: 600;
color: #111827;
} }
:deep(.detail-table .el-table__body-wrapper) { .metric-grid {
overflow-x: auto; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.metric-card {
padding: 12px 14px;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 10px;
}
.metric-card-label {
display: block;
font-size: 13px;
color: #64748b;
}
.metric-card-value {
display: block;
margin-top: 8px;
font-size: 18px;
font-weight: 700;
color: #111827;
}
.result-card {
padding: 14px 16px;
border-radius: 10px;
border: 1px solid #dbeafe;
background: #eff6ff;
}
.result-card--success {
border-color: #bbf7d0;
background: #f0fdf4;
}
.result-card--warning {
border-color: #fde68a;
background: #fffbeb;
}
.result-card--danger {
border-color: #fecaca;
background: #fef2f2;
}
.result-card--info {
border-color: #cbd5e1;
background: #f8fafc;
}
.result-card-label {
font-size: 12px;
color: #64748b;
}
.result-card-value {
margin-top: 6px;
font-size: 14px;
font-weight: 600;
color: #111827;
}
.empty-hint {
font-size: 13px;
color: #94a3b8;
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.detail-grid { .summary-group-list,
.metric-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }

View File

@@ -4,11 +4,10 @@
<section <section
v-for="(group, groupIndex) in detailGroups" v-for="(group, groupIndex) in detailGroups"
:key="`${group.groupCode || group.groupType || groupIndex}-group`" :key="`${group.groupCode || group.groupType || groupIndex}-group`"
class="abnormal-card analysis-panel" class="abnormal-card"
> >
<div class="abnormal-card__header"> <div class="abnormal-card__header">
<div class="abnormal-card__title">{{ group.groupName || "异常明细" }}</div> <div class="abnormal-card__title">{{ group.groupName || "异常明细" }}</div>
<div class="abnormal-card__summary">{{ getGroupSummary(group) }}</div>
</div> </div>
<el-table <el-table
@@ -152,16 +151,6 @@ export default {
resolveGroupKey(group, index = 0) { resolveGroupKey(group, index = 0) {
return group.groupCode || group.groupName || `BANK_STATEMENT_${index}`; return group.groupCode || group.groupName || `BANK_STATEMENT_${index}`;
}, },
getGroupSummary(group) {
const recordTotal = Array.isArray(group && group.records) ? group.records.length : 0;
if (group && group.groupType === "BANK_STATEMENT") {
return `${recordTotal} 条交易记录,按时间顺序查看重点流水。`;
}
if (group && group.groupType === "OBJECT") {
return `${recordTotal} 个异常对象,结合快照与摘要连续阅读。`;
}
return "按统一工作台区块承载当前异常分组内容。";
},
getStatementPage(group) { getStatementPage(group) {
const groupKey = this.resolveGroupKey(group); const groupKey = this.resolveGroupKey(group);
return this.statementPageMap[groupKey] || 1; return this.statementPageMap[groupKey] || 1;
@@ -190,14 +179,10 @@ export default {
gap: 16px; gap: 16px;
} }
.analysis-panel {
border: 1px solid #e2e8f0;
border-radius: 20px;
background: #ffffff;
}
.abnormal-card { .abnormal-card {
padding: 20px; padding: 20px;
border: 1px solid #e2e8f0;
background: #fff;
} }
.abnormal-card__header { .abnormal-card__header {
@@ -210,10 +195,8 @@ export default {
color: #0f172a; color: #0f172a;
} }
.abnormal-card__summary { .abnormal-card__subtitle {
margin-top: 8px; font-size: 12px;
font-size: 13px;
line-height: 1.7;
color: #64748b; color: #64748b;
} }
@@ -259,8 +242,7 @@ export default {
.object-card { .object-card {
padding: 16px; padding: 16px;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 16px; background: #f8fafc;
background: #ffffff;
} }
.object-card__title { .object-card__title {

View File

@@ -8,76 +8,51 @@
custom-class="project-analysis-dialog" custom-class="project-analysis-dialog"
@close="handleDialogClosed" @close="handleDialogClosed"
> >
<div class="project-analysis-header"> <div class="project-analysis-shell">
<div class="project-analysis-header__main"> <div class="project-analysis-layout">
<div class="project-analysis-header__title-row"> <project-analysis-sidebar
<div class="project-analysis-header__title">{{ dialogData.sidebar.basicInfo.name || "-" }}</div> class="project-analysis-layout__sidebar"
<span class="project-analysis-header__risk">{{ dialogData.sidebar.basicInfo.riskLevel || "-" }}</span> :sidebar-data="dialogData.sidebar"
</div> />
<div class="project-analysis-header__meta"> <div v-loading="detailLoading" class="project-analysis-layout__main">
<span>工号 {{ dialogData.sidebar.basicInfo.staffCode || "-" }}</span> <div class="project-analysis-layout__main-scroll">
<span>部门 {{ dialogData.sidebar.basicInfo.department || "-" }}</span> <el-alert
<span>所属项目 {{ dialogData.sidebar.basicInfo.projectName || "-" }}</span> v-if="detailError"
</div> :closable="false"
</div> class="project-analysis-layout__alert"
<div type="error"
v-if='dialogData.sourceSummary.showCurrentModel && source === "riskModelPeople"' show-icon
class="project-analysis-header__context" :title="detailError"
> >
<span class="project-analysis-header__context-label">当前命中模型</span> <template slot="default">
<span class="project-analysis-header__context-value">{{ dialogData.sourceSummary.currentModelValue }}</span> <el-button type="text" size="mini" @click="handleRetryDetail">重试</el-button>
</div> </template>
</div> </el-alert>
<div class="project-analysis-overview"> <div
<div class="project-analysis-overview__item"> v-if='dialogData.sourceSummary.showCurrentModel && source === "riskModelPeople"'
<span class="project-analysis-overview__label">风险等级</span> class="source-summary"
<strong class="project-analysis-overview__value">{{ dialogData.sidebar.basicInfo.riskLevel || "-" }}</strong> >
</div> <span class="source-summary__label">当前命中模型</span>
<div class="project-analysis-overview__item"> <span class="source-summary__value">{{ dialogData.sourceSummary.currentModelValue }}</span>
<span class="project-analysis-overview__label">命中模型数</span> </div>
<strong class="project-analysis-overview__value">{{ dialogData.sidebar.modelSummary.modelCount || "-" }}</strong> <el-tabs v-model="activeTab" stretch>
</div> <el-tab-pane label="异常明细" name="abnormalDetail">
<div class="project-analysis-overview__item"> <project-analysis-abnormal-tab :detail-data="dialogData.abnormalDetail" />
<span class="project-analysis-overview__label">核心异常标签</span> </el-tab-pane>
<strong class="project-analysis-overview__value">{{ dialogData.sidebar.modelSummary.riskTags.length || 0 }}</strong> <el-tab-pane label="资产分析" name="assetAnalysis">
</div> <project-analysis-placeholder-tab :tab-data="getTabData('assetAnalysis')" />
</div> </el-tab-pane>
<div class="project-analysis-layout"> <el-tab-pane label="征信摘要" name="creditSummary">
<project-analysis-sidebar <project-analysis-placeholder-tab :tab-data="getTabData('creditSummary')" />
class="project-analysis-layout__sidebar" </el-tab-pane>
:sidebar-data="dialogData.sidebar" <el-tab-pane label="关系图谱" name="relationshipGraph">
/> <project-analysis-placeholder-tab :tab-data="getTabData('relationshipGraph')" />
<div v-loading="detailLoading" class="project-analysis-layout__main"> </el-tab-pane>
<div class="project-analysis-layout__main-scroll"> <el-tab-pane label="资金流向" name="fundFlow">
<el-alert <project-analysis-placeholder-tab :tab-data="getTabData('fundFlow')" />
v-if="detailError" </el-tab-pane>
:closable="false" </el-tabs>
class="project-analysis-layout__alert" </div>
type="error"
show-icon
:title="detailError"
>
<template slot="default">
<el-button type="text" size="mini" @click="handleRetryDetail">重试</el-button>
</template>
</el-alert>
<el-tabs v-model="activeTab" stretch>
<el-tab-pane label="异常明细" name="abnormalDetail">
<project-analysis-abnormal-tab class="analysis-panel" :detail-data="dialogData.abnormalDetail" />
</el-tab-pane>
<el-tab-pane label="资产分析" name="assetAnalysis">
<project-analysis-placeholder-tab class="analysis-panel" :tab-data="getTabData('assetAnalysis')" />
</el-tab-pane>
<el-tab-pane label="征信摘要" name="creditSummary">
<project-analysis-placeholder-tab class="analysis-panel" :tab-data="getTabData('creditSummary')" />
</el-tab-pane>
<el-tab-pane label="关系图谱" name="relationshipGraph">
<project-analysis-placeholder-tab class="analysis-panel" :tab-data="getTabData('relationshipGraph')" />
</el-tab-pane>
<el-tab-pane label="资金流向" name="fundFlow">
<project-analysis-placeholder-tab class="analysis-panel" :tab-data="getTabData('fundFlow')" />
</el-tab-pane>
</el-tabs>
</div> </div>
</div> </div>
</div> </div>
@@ -225,121 +200,30 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.project-analysis-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
padding: 24px;
border: 1px solid #e2e8f0;
border-radius: 20px;
background: linear-gradient(135deg, #fffdf7 0%, #ffffff 60%, #f8fafc 100%);
}
.project-analysis-header__main {
min-width: 0;
flex: 1;
}
.project-analysis-header__title-row {
display: flex;
align-items: center;
gap: 12px;
}
.project-analysis-header__title {
font-size: 24px;
font-weight: 600;
line-height: 1.2;
color: #0f172a;
}
.project-analysis-header__risk {
display: inline-flex;
align-items: center;
padding: 6px 12px;
border-radius: 999px;
background: #fee2e2;
font-size: 13px;
font-weight: 600;
color: #b91c1c;
}
.project-analysis-header__meta {
display: flex;
flex-wrap: wrap;
gap: 12px 20px;
margin-top: 14px;
font-size: 13px;
color: #475569;
}
.project-analysis-header__context {
display: flex;
min-width: 220px;
flex-direction: column;
gap: 8px;
padding: 16px 18px;
border-radius: 16px;
background: #fff7ed;
}
.project-analysis-header__context-label {
font-size: 12px;
color: #9a3412;
}
.project-analysis-header__context-value {
font-size: 14px;
font-weight: 600;
line-height: 1.6;
color: #7c2d12;
}
.project-analysis-overview {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
margin-top: 16px;
}
.project-analysis-overview__item {
padding: 18px 20px;
border: 1px solid #e2e8f0;
border-radius: 18px;
background: #ffffff;
}
.project-analysis-overview__label {
display: block;
font-size: 12px;
color: #64748b;
}
.project-analysis-overview__value {
display: block;
margin-top: 8px;
font-size: 18px;
color: #0f172a;
}
.project-analysis-layout { .project-analysis-layout {
display: flex; display: flex;
gap: 20px; gap: 20px;
margin-top: 16px;
min-height: 640px; min-height: 640px;
max-height: calc(90vh - 120px); max-height: calc(90vh - 120px);
} }
.project-analysis-shell {
border: 1px solid #e2e8f0;
border-radius: 20px;
background: #fff;
overflow: hidden;
}
.project-analysis-layout__sidebar { .project-analysis-layout__sidebar {
flex: 0 0 340px; flex: 0 0 340px;
padding: 0; padding: 20px 0 20px 20px;
} }
.project-analysis-layout__main { .project-analysis-layout__main {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
padding: 0; padding: 20px 20px 20px 0;
border-left: 1px solid #e2e8f0;
} }
.project-analysis-layout__main-scroll { .project-analysis-layout__main-scroll {
@@ -350,4 +234,25 @@ export default {
.project-analysis-layout__alert { .project-analysis-layout__alert {
margin-bottom: 16px; margin-bottom: 16px;
} }
.source-summary {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding: 14px 16px;
border: 1px solid #fde68a;
background: #fffbeb;
}
.source-summary__label {
font-size: 12px;
color: #92400e;
}
.source-summary__value {
font-size: 13px;
font-weight: 600;
color: #b45309;
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<section class="project-analysis-placeholder-tab analysis-panel analysis-panel--placeholder"> <section class="project-analysis-placeholder-tab">
<div class="placeholder-title">{{ tabData.label }}</div> <div class="placeholder-title">{{ tabData.label }}</div>
<div class="placeholder-text">{{ tabData.description }}</div> <div class="placeholder-text">{{ tabData.description }}</div>
</section> </section>
@@ -21,19 +21,11 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.analysis-panel {
border: 1px solid #e2e8f0;
border-radius: 20px;
background: #ffffff;
}
.project-analysis-placeholder-tab { .project-analysis-placeholder-tab {
min-height: 560px; min-height: 560px;
padding: 24px; padding: 24px;
} border: 1px solid #e2e8f0;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
.analysis-panel--placeholder {
background: #ffffff;
} }
.placeholder-title { .placeholder-title {

View File

@@ -1,57 +1,51 @@
<template> <template>
<aside class="project-analysis-sidebar"> <aside class="project-analysis-sidebar">
<section class="sidebar-profile"> <section class="sidebar-card">
<div class="sidebar-section__title">人物档案</div> <div class="sidebar-section">
<div class="sidebar-profile__name-row"> <div class="sidebar-card__title">人员基础信息</div>
<div> <div class="sidebar-field">
<div class="sidebar-profile__name">{{ sidebarData.basicInfo.name || "-" }}</div> <span class="sidebar-field__label">姓名</span>
<div class="sidebar-profile__meta"> <span class="sidebar-field__value">{{ sidebarData.basicInfo.name || "-" }}</span>
<span>工号 {{ sidebarData.basicInfo.staffCode || "-" }}</span> </div>
<span>部门 {{ sidebarData.basicInfo.department || "-" }}</span> <div class="sidebar-field">
<span class="sidebar-field__label">工号</span>
<span class="sidebar-field__value">{{ sidebarData.basicInfo.staffCode || "-" }}</span>
</div>
<div class="sidebar-field">
<span class="sidebar-field__label">部门</span>
<span class="sidebar-field__value">{{ sidebarData.basicInfo.department || "-" }}</span>
</div>
<div class="sidebar-field">
<span class="sidebar-field__label">风险等级</span>
<span class="sidebar-field__value">{{ sidebarData.basicInfo.riskLevel || "-" }}</span>
</div>
<div class="sidebar-field">
<span class="sidebar-field__label">所属项目</span>
<span class="sidebar-field__value">{{ sidebarData.basicInfo.projectName || "-" }}</span>
</div>
</div>
<div class="sidebar-section sidebar-section--split">
<div class="sidebar-card__title">命中模型摘要</div>
<div class="sidebar-field">
<span class="sidebar-field__label">命中模型数</span>
<span class="sidebar-field__value">{{ sidebarData.modelSummary.modelCount || "-" }}</span>
</div>
<div class="sidebar-field sidebar-field--column">
<span class="sidebar-field__label">核心异常标签</span>
<div v-if="sidebarData.modelSummary.riskTags.length" class="tag-list">
<el-tag
v-for="(tag, index) in sidebarData.modelSummary.riskTags"
:key="`${formatRiskTag(tag)}-${index}`"
size="mini"
effect="plain"
>
{{ formatRiskTag(tag) }}
</el-tag>
</div> </div>
</div> <span v-else class="sidebar-field__value">暂无异常标签</span>
<span class="sidebar-risk-badge">{{ sidebarData.basicInfo.riskLevel || "-" }}</span>
</div>
<div class="sidebar-profile__grid">
<div class="sidebar-profile__item">
<span class="sidebar-profile__label">所属项目</span>
<span class="sidebar-profile__value">{{ sidebarData.basicInfo.projectName || "-" }}</span>
</div>
<div class="sidebar-profile__item">
<span class="sidebar-profile__label">风险等级</span>
<span class="sidebar-profile__value">{{ sidebarData.basicInfo.riskLevel || "-" }}</span>
</div> </div>
</div> </div>
</section> </section>
<section class="sidebar-summary">
<div class="sidebar-section__title">模型摘要</div>
<div class="sidebar-summary__metric">
<span class="sidebar-summary__label">命中模型数</span>
<strong class="sidebar-summary__value">{{ sidebarData.modelSummary.modelCount || "-" }}</strong>
</div>
<div class="sidebar-summary__block">
<span class="sidebar-summary__label">核心异常标签</span>
<div v-if="sidebarData.modelSummary.riskTags.length" class="tag-list">
<el-tag
v-for="(tag, index) in sidebarData.modelSummary.riskTags"
:key="`${formatRiskTag(tag)}-${index}`"
size="mini"
effect="plain"
>
{{ formatRiskTag(tag) }}
</el-tag>
</div>
<span v-else class="sidebar-summary__empty">暂无异常标签</span>
</div>
</section>
<section class="sidebar-hint">
<div class="sidebar-section__title">辅助提示</div>
<p class="sidebar-hint__text">
侧栏仅保留人物身份与模型摘要详细异常请在主区页签中继续查看
</p>
</section>
</aside> </aside>
</template> </template>
@@ -86,109 +80,69 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.project-analysis-sidebar { .project-analysis-sidebar {
width: 100%; width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
} }
.sidebar-profile, .sidebar-card {
.sidebar-summary, padding: 0;
.sidebar-hint { border: none;
padding: 20px; background: transparent;
border-radius: 20px; box-shadow: none;
background: #ffffff;
} }
.sidebar-section__title { .sidebar-section {
font-size: 13px; padding: 18px 16px;
font-weight: 600;
letter-spacing: 0.08em;
color: #92400e;
} }
.sidebar-profile__name-row { .sidebar-section--split {
display: flex; border-top: 1px solid #e2e8f0;
align-items: flex-start;
justify-content: flex-start;
gap: 12px;
margin-top: 14px;
} }
.sidebar-profile__name { .sidebar-card__title {
font-size: 24px; margin-bottom: 14px;
font-size: 15px;
font-weight: 600; font-weight: 600;
color: #0f172a; color: #0f172a;
} }
.sidebar-profile__meta { .sidebar-field + .sidebar-field {
margin-top: 12px;
}
.sidebar-field {
display: flex; display: flex;
flex-direction: column; align-items: flex-start;
gap: 6px; justify-content: space-between;
margin-top: 8px;
font-size: 12px;
color: #64748b;
}
.sidebar-risk-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 12px;
border-radius: 999px;
background: #fee2e2;
font-size: 12px;
font-weight: 600;
color: #b91c1c;
}
.sidebar-profile__grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 12px; gap: 12px;
margin-top: 18px;
} }
.sidebar-profile__item, .sidebar-field--column {
.sidebar-summary__metric,
.sidebar-summary__block {
padding: 14px 16px;
border-radius: 16px;
background: #f8fafc;
}
.sidebar-profile__label,
.sidebar-summary__label {
display: block; display: block;
}
.sidebar-field--column .sidebar-field__label {
display: block;
margin-bottom: 8px;
}
.sidebar-field__label {
flex: 0 0 84px;
font-size: 12px; font-size: 12px;
color: #64748b; color: #64748b;
} }
.sidebar-profile__value, .sidebar-field__value {
.sidebar-summary__value, flex: 1;
.sidebar-summary__empty { min-width: 0;
display: block; font-size: 13px;
margin-top: 8px;
font-size: 14px;
line-height: 1.6; line-height: 1.6;
text-align: right;
color: #0f172a; color: #0f172a;
word-break: break-all; word-break: break-all;
} }
.sidebar-summary {
display: flex;
flex-direction: column;
gap: 12px;
}
.sidebar-hint__text {
margin: 14px 0 0;
font-size: 13px;
line-height: 1.7;
color: #64748b;
}
.tag-list { .tag-list {
display: flex; display: flex;
width: 100%;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-start; justify-content: flex-start;
gap: 8px; gap: 8px;

View File

@@ -16,27 +16,18 @@ const abnormalTab = fs.readFileSync(
), ),
"utf8" "utf8"
); );
const placeholderTab = fs.readFileSync(
path.resolve(
__dirname,
"../../src/views/ccdiProject/components/detail/ProjectAnalysisPlaceholderTab.vue"
),
"utf8"
);
[ [
"<project-analysis-abnormal-tab", "<project-analysis-abnormal-tab",
':detail-data="dialogData.abnormalDetail"', ':detail-data="dialogData.abnormalDetail"',
"project-analysis-overview", "source-summary",
].forEach((token) => assert(dialog.includes(token), token)); ].forEach((token) => assert(dialog.includes(token), token));
[ [
"analysis-panel",
"detailData.groups", "detailData.groups",
'group.groupType === "BANK_STATEMENT"', 'group.groupType === "BANK_STATEMENT"',
'group.groupType === "OBJECT"', 'group.groupType === "OBJECT"',
"group.groupName", "group.groupName",
"abnormal-card__summary",
"statementPageSize: 5", "statementPageSize: 5",
"statementPageMap", "statementPageMap",
"slice(startIndex, startIndex + this.statementPageSize)", "slice(startIndex, startIndex + this.statementPageSize)",
@@ -57,13 +48,3 @@ const placeholderTab = fs.readFileSync(
"extraFields", "extraFields",
"grid-template-columns: minmax(0, 1fr)", "grid-template-columns: minmax(0, 1fr)",
].forEach((token) => assert(abnormalTab.includes(token), token)); ].forEach((token) => assert(abnormalTab.includes(token), token));
[
"analysis-panel",
"analysis-panel--placeholder",
].forEach((token) => assert(placeholderTab.includes(token), token));
assert(
!placeholderTab.includes("background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)"),
"占位页签不应继续单独使用旧渐变底"
);

View File

@@ -12,8 +12,6 @@ const dialog = fs.readFileSync(
[ [
'activeTab: "abnormalDetail"', 'activeTab: "abnormalDetail"',
"project-analysis-header",
"project-analysis-overview",
"if (value) {", "if (value) {",
"resetDialogState()", "resetDialogState()",
'this.activeTab = "abnormalDetail"', 'this.activeTab = "abnormalDetail"',
@@ -21,5 +19,3 @@ const dialog = fs.readFileSync(
"this.fetchDetailData()", "this.fetchDetailData()",
"this.resetDialogState()", "this.resetDialogState()",
].forEach((token) => assert(dialog.includes(token), token)); ].forEach((token) => assert(dialog.includes(token), token));
assert(!dialog.includes("project-analysis-shell"), "关闭重开后默认页签保持,但内部套娃外壳应已移除");

View File

@@ -25,14 +25,9 @@ const dialog = fs.readFileSync(
); );
[ [
'sidebarData.basicInfo.name || "-"',
'sidebarData.basicInfo.staffCode || "-"', 'sidebarData.basicInfo.staffCode || "-"',
'sidebarData.basicInfo.department || "-"', 'sidebarData.basicInfo.department || "-"',
'sidebarData.basicInfo.projectName || "-"',
'sidebarData.basicInfo.riskLevel || "-"',
"暂无异常标签", "暂无异常标签",
"sidebar-profile",
"sidebar-risk-badge",
].forEach((token) => assert(sidebar.includes(token), token)); ].forEach((token) => assert(sidebar.includes(token), token));
[ [

View File

@@ -16,24 +16,12 @@ const mockSource = fs.readFileSync(
), ),
"utf8" "utf8"
); );
const placeholderTab = fs.readFileSync(
path.resolve(
__dirname,
"../../src/views/ccdiProject/components/detail/ProjectAnalysisPlaceholderTab.vue"
),
"utf8"
);
[ [
'title="项目分析"', 'title="项目分析"',
'width="80%"', 'width="80%"',
'top="5vh"', 'top="5vh"',
"project-analysis-header", "project-analysis-shell",
"project-analysis-overview",
"analysis-panel",
"project-analysis-header__title",
"project-analysis-header__risk",
"project-analysis-header__meta",
"<project-analysis-sidebar", "<project-analysis-sidebar",
'<el-tabs v-model="activeTab"', '<el-tabs v-model="activeTab"',
'name="abnormalDetail"', 'name="abnormalDetail"',
@@ -49,19 +37,10 @@ const placeholderTab = fs.readFileSync(
"project-analysis-layout__main-scroll", "project-analysis-layout__main-scroll",
"overflow-y: auto", "overflow-y: auto",
"max-height: calc(90vh - 120px)", "max-height: calc(90vh - 120px)",
"工号", "border: 1px solid #e2e8f0",
"部门", "border-radius: 20px",
"所属项目",
].forEach((token) => assert(dialog.includes(token), token)); ].forEach((token) => assert(dialog.includes(token), token));
assert(!dialog.includes("project-analysis-shell"), "不应继续保留内部大白卡外壳");
assert(!dialog.includes('class="source-summary"'), "当前命中模型不应停留在主区顶部");
assert(placeholderTab.includes("analysis-panel--placeholder"), "占位页签应与主区面板统一样式基线");
assert(
!placeholderTab.includes("background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)"),
"占位页签不应继续保留旧渐变底"
);
[ [
"projectAnalysisTabs", "projectAnalysisTabs",
'key: "abnormalDetail"', 'key: "abnormalDetail"',

View File

@@ -25,13 +25,13 @@ const entry = fs.readFileSync(
); );
[ [
"sidebar-profile", "人员基础信息",
"sidebar-summary", "命中模型摘要",
"sidebar-hint", "姓名",
"sidebar-risk-badge", "工号",
"人物档案", "部门",
"模型摘要", "风险等级",
"辅助提示", "所属项目",
"命中模型数", "命中模型数",
"核心异常标签", "核心异常标签",
"暂无异常标签", "暂无异常标签",
@@ -39,16 +39,15 @@ const entry = fs.readFileSync(
"tag.ruleName", "tag.ruleName",
"justify-content: flex-start", "justify-content: flex-start",
"width: 100%", "width: 100%",
"sidebar-profile__meta", ".sidebar-field--column .sidebar-field__label",
"sidebar-section__title", "margin-bottom: 8px",
].forEach((token) => assert(sidebar.includes(token), token)); ].forEach((token) => assert(sidebar.includes(token), token));
assert(!sidebar.includes("当前命中模型"), "命中模型摘要应移除当前命中模型字段"); assert(!sidebar.includes("当前命中模型"), "命中模型摘要应移除当前命中模型字段");
assert(!sidebar.includes("排查记录摘要"), "侧栏应移除排查记录摘要"); assert(!sidebar.includes("排查记录摘要"), "侧栏应移除排查记录摘要");
assert(!sidebar.includes("人员基础信息"), "侧栏主视觉不应继续停留在字段平铺式表单"); assert(!sidebar.includes("sidebar-summary"), "侧栏不应再渲染排查记录摘要文案");
assert(!sidebar.includes("position: sticky"), "左侧整卡不应保持固定"); assert(!sidebar.includes("position: sticky"), "左侧整卡不应保持固定");
assert(!sidebar.includes("border: 1px solid #e2e8f0"), "左右区域合并后左侧不应保留独立卡片边框"); assert(!sidebar.includes("border: 1px solid #e2e8f0"), "左右区域合并后左侧不应保留独立卡片边框");
assert(!sidebar.includes("justify-content: space-between"), "不应继续以表单式左右对齐作为主体布局");
assert(!sidebar.includes("关系人画像"), "侧栏不应扩展到额外区块"); assert(!sidebar.includes("关系人画像"), "侧栏不应扩展到额外区块");
assert(!sidebar.includes("资产分布"), "侧栏不应扩展到额外区块"); assert(!sidebar.includes("资产分布"), "侧栏不应扩展到额外区块");

View File

@@ -21,13 +21,10 @@ const mockSource = fs.readFileSync(
'source === "riskModelPeople"', 'source === "riskModelPeople"',
"当前命中模型", "当前命中模型",
"dialogData.sourceSummary", "dialogData.sourceSummary",
"project-analysis-header__context", "source-summary__label",
"project-analysis-header__context-label", "source-summary__value",
"project-analysis-header__context-value",
].forEach((token) => assert(dialog.includes(token), token)); ].forEach((token) => assert(dialog.includes(token), token));
assert(!dialog.includes('class="source-summary"'), "当前命中模型应并入头带上下文区域");
[ [
"sourceSummary", "sourceSummary",
"showCurrentModel: source === \"riskModelPeople\"", "showCurrentModel: source === \"riskModelPeople\"",

View File

@@ -11,8 +11,6 @@ const source = fs.readFileSync(componentPath, "utf8");
[ [
"formatDetailDateTime(value)", "formatDetailDateTime(value)",
'return parseTime(value, hasTime ? "{y}-{m}-{d} {h}:{i}:{s}" : "{y}-{m}-{d}") || "-";', 'return parseTime(value, hasTime ? "{y}-{m}-{d} {h}:{i}:{s}" : "{y}-{m}-{d}") || "-";',
"{{ formatDetailDateTime(scope.row.valuationDate) }}",
"{{ formatDetailDateTime(scope.row.queryDate) }}",
].forEach((token) => { ].forEach((token) => {
assert(source.includes(token), `专项核查详情日期展示缺少关键实现: ${token}`); assert(source.includes(token), `专项核查详情日期展示缺少关键实现: ${token}`);
}); });
@@ -21,5 +19,6 @@ assert(
source.includes("const hasTime = !formatted.endsWith(\" 00:00:00\");"), source.includes("const hasTime = !formatted.endsWith(\" 00:00:00\");"),
"专项核查详情应隐藏无意义的零点时间" "专项核查详情应隐藏无意义的零点时间"
); );
assert(!source.includes("el-table"), "专项核查详情去表格化后不应继续依赖表格日期列");
console.log("special-check-detail-date-display test passed"); console.log("special-check-detail-date-display test passed");

View File

@@ -8,35 +8,47 @@ const source = fs.readFileSync(
); );
[ [
"收入明细", "收入",
"负债明细", "负债",
"资产明细", "资产",
"关键指标",
"详查结果",
"本人收入", "本人收入",
"配偶收入", "配偶收入",
"本人资产小计", "净资产",
"配偶资产小计", "资产负债率",
"本人负债小计", "资产/收入比",
"配偶负债小计", "负债/收入比",
"el-table", "section-summary-value",
"el-empty", "summary-group-list",
"summary-group-item",
"summary-group-share",
].forEach((token) => assert(source.includes(token), token)); ].forEach((token) => assert(source.includes(token), token));
assert(source.includes("missingSelfAssetInfo"), "资产卡片应支持缺少信息判断"); assert(!source.includes("el-table"), "资产与负债来源项不应继续使用表格");
assert(source.includes("missingSelfDebtInfo"), "负债卡片应支持缺少信息判断"); assert(!source.includes("detail-grid"), "详情区不应继续保留三列网格结构");
assert(source.includes('v-if="!assetDetail.missingSelfAssetInfo"'), "资产小计应可隐藏"); assert(!source.includes("detail-table"), "详情区不应继续保留旧表格样式");
assert(source.includes('v-if="!debtDetail.missingSelfDebtInfo"'), "负债小计应可隐藏");
const incomeIndex = source.indexOf("收入明细"); const incomeIndex = source.indexOf("收入");
const debtIndex = source.indexOf("负债明细"); const debtIndex = source.indexOf("负债");
const assetIndex = source.indexOf("资产明细"); const assetIndex = source.indexOf("资产");
const metricIndex = source.indexOf("关键指标");
const resultIndex = source.indexOf("详查结果");
assert(incomeIndex > -1, "缺少收入明细卡片"); assert(incomeIndex > -1, "缺少收入卡片");
assert(debtIndex > -1, "缺少负债明细卡片"); assert(debtIndex > -1, "缺少负债卡片");
assert(assetIndex > -1, "缺少资产明细卡片"); assert(assetIndex > -1, "缺少资产卡片");
assert(incomeIndex < debtIndex && debtIndex < assetIndex, "详情卡片顺序应为收入、负债、资产"); assert(metricIndex > -1, "缺少关键指标卡片");
assert(resultIndex > -1, "缺少详查结果卡片");
assert(
incomeIndex < debtIndex && debtIndex < assetIndex && assetIndex < metricIndex && metricIndex < resultIndex,
"详情卡片顺序应为总收入、总负债、总资产、关键指标、详查结果"
);
assert( assert(
source.includes("grid-template-columns: repeat(3, minmax(0, 1fr));"), source.includes("detail-stack"),
"三个详情卡片应横向均分" "详情区应改为纵向卡片容器"
); );
assert(source.includes("@media (max-width: 1200px)"), "中小屏应保留响应式回落"); assert(source.includes("@media (max-width: 1200px)"), "中小屏应保留响应式回落");
console.log("special-check-detail-layout test passed");

View File

@@ -0,0 +1,45 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const source = fs.readFileSync(
path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/FamilyAssetLiabilityDetail.vue"),
"utf8"
);
[
"assetGroups",
"debtGroups",
"buildAmountGroups",
"resolveAssetGroupLabel",
"resolveDebtGroupLabel",
"item.assetName",
"item.debtName",
"summary.totalAsset",
"summary.totalDebt",
"summary.riskLevelCode",
"summary.riskLevelName",
"NORMAL",
"RISK",
"HIGH",
"MISSING_INFO",
"结构基本合理",
"负债与收入压力偏高",
"资产负债结构明显异常",
"当前信息不完整",
"summary-group-name",
"summary-group-amount",
"summary-group-share",
].forEach((token) => {
assert(source.includes(token), `缺少新版详情聚合或风险结果实现: ${token}`);
});
assert(source.includes("totalDebt / totalAsset"), "缺少资产负债率计算");
assert(source.includes("totalAsset / totalIncome"), "缺少资产收入比计算");
assert(source.includes("totalDebt / totalIncome"), "缺少负债收入比计算");
assert(source.includes("分母为 0"), "比率计算应处理分母为 0 的场景");
assert(source.includes("占比"), "来源项应展示占比语义");
assert(source.includes("return item.assetName ||"), "总资产细项应优先展示 assetName");
assert(source.includes("return item.debtName ||"), "总负债细项应优先展示 debtName");
console.log("special-check-detail-summary-groups test passed");

View File

@@ -23,3 +23,7 @@ assert(sectionSource.includes("block-subtitle"), "列表区块缺少副标题样
assert(sectionSource.includes(":deep(.family-table th)"), "表格头样式应与结果总览统一"); assert(sectionSource.includes(":deep(.family-table th)"), "表格头样式应与结果总览统一");
assert(sectionSource.includes("el-tag"), "风险标签应保留标签形态"); assert(sectionSource.includes("el-tag"), "风险标签应保留标签形态");
assert(detailSource.includes("block-title"), "详情区块标题应与结果总览标题层级统一"); assert(detailSource.includes("block-title"), "详情区块标题应与结果总览标题层级统一");
assert(detailSource.includes("section-summary-value"), "详情区标题应展示汇总值");
assert(detailSource.includes("detail-stack"), "详情区应改为纵向汇总卡片");
assert(detailSource.includes("summary-group-list"), "资产与负债应改为来源项摘要列表");
assert(detailSource.includes("metric-grid"), "关键指标区应保留紧凑指标排布");