Files
ccdi/docs/plans/frontend/2026-05-07-project-special-check-graph-atlas-frontend-implementation-plan.md

16 KiB
Raw Permalink Blame History

项目详情专项排查图谱嵌入前端实施计划

执行说明: 按当前项目 AGENTS.md 规则执行。未得到用户明确授权时默认不开启 subagent如用户明确要求使用 subagent必须按项目规则指定模型与推理强度。Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在项目详情“专项排查”页面内,将现有图谱占位卡片替换为可输入证件号并生成 iframe 链接的资金图谱/关系图谱展示区。

Architecture: 只做前端最短路径改造。新增一个独立 Vue 2 组件负责证件号标准化、md5、tab 状态和 iframe src 拼接;SpecialCheck.vue 只负责引入组件并替换现有占位卡片。外部图谱当前可能打不开,验收只检查 iframe 链接是否按规则生成。

Tech Stack: Vue 2、Element UI、SCSS、@/utils/md5browser-use:browser 实际页面验证。


Scope Check

本计划只覆盖项目详情“专项排查”页内的图谱 iframe 嵌入,不新增后端接口,不保存证件号,不新增项目详情一级菜单,不做身份证格式校验,不增加外部图谱服务兜底或降级逻辑。

File Structure

  • Create: ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue
    • 责任:维护共用证件号输入、资金/关系图谱 tab、证件号标准化、md5、iframe 链接生成和空状态展示。
  • Modify: ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue
    • 责任:移除现有“图谱外链展示”占位卡片,引入并渲染 GraphAtlasSection,并让图谱在资产负债核查空数据状态下仍可展示,清理不再使用的占位样式。
  • Create: docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md
    • 责任:记录本次前端实现内容、影响范围和验证情况。
  • Do not commit: output/browser-use/ 或其它临时测试文件。

Test Data

用于链接断言的固定样例:

  • 输入:110101199003074211
  • md59b1b5eba4a26c9a68ff1ca06f40bee1b
  • 资金图谱期望 vIdidno_node/9b1b5eba4a26c9a68ff1ca06f40bee1b
  • 关系图谱期望 vIdrel_node/9b1b5eba4a26c9a68ff1ca06f40bee1b

标准化样例:

  • 输入:33078219900101123x
  • 标准化后:33078219900101123X
  • md5233c8519f86a57b1f00ec88a32152ce3

Task 1: Establish Current Failure Baseline

Files:

  • Read: docs/design/2026-05-07-project-special-check-graph-atlas-design.md

  • Read: ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue

  • Step 1: Confirm the spec

Run:

sed -n '1,140p' docs/design/2026-05-07-project-special-check-graph-atlas-design.md

Expected: spec confirms 专项排查内嵌 iframe、两个 tab、共用证件号、trim + x 转 X + md5、只校验 iframe src

  • Step 2: Confirm current placeholder

Run:

rg -n "图谱外链展示|graph-placeholder" ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue

Expected: current page still has placeholder card and no证件号 input, no graph iframe.

  • Step 3: Record baseline failure

Acceptance baseline:

  • Current page cannot input证件号 in the graph area.
  • Current page cannot produce idno_node/<md5> or rel_node/<md5> iframe links.

Expected: FAIL relative to the new spec. This is the intended pre-implementation failure.

Task 2: Create GraphAtlasSection Component

Files:

  • Create: ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue

  • Step 1: Create the component with minimal behavior

Create GraphAtlasSection.vue with this implementation:

<template>
  <section class="graph-atlas-card">
    <div class="graph-atlas-header">
      <div>
        <div class="graph-atlas-title">图谱展示</div>
        <div class="graph-atlas-subtitle">输入证件号后查看资金图谱和关系图谱</div>
      </div>
    </div>

    <div class="graph-atlas-query">
      <el-input
        v-model="certNoInput"
        class="graph-atlas-input"
        clearable
        placeholder="请输入证件号"
        size="small"
        @keyup.enter.native="handleQuery"
      />
      <el-button
        type="primary"
        size="small"
        icon="el-icon-search"
        @click="handleQuery"
      >
        查询图谱
      </el-button>
    </div>

    <el-tabs v-model="activeGraphTab" class="graph-atlas-tabs">
      <el-tab-pane
        v-for="item in graphTabs"
        :key="item.name"
        :label="item.label"
        :name="item.name"
      />
    </el-tabs>

    <div v-if="!certNoHash" class="graph-atlas-empty">
      <el-empty description="请输入证件号后查询图谱" />
    </div>

    <div v-else class="graph-atlas-frame-wrapper">
      <iframe
        :key="`${activeGraphTab}-${certNoHash}`"
        class="graph-atlas-frame"
        :src="currentGraphUrl"
        title="专项排查图谱"
      />
    </div>
  </section>
</template>

<script>
import md5 from "@/utils/md5";

const GRAPH_ATLAS_CONFIG = {
  fund: {
    label: "资金图谱",
    urlTemplate: 'http://64.202.65.112:8082/atlas/refactor/#/home/graph/downloadService?id=ccdi_lanxi_trans&mode=K_EXPAND&type=NORMAL&atlasToken=F4BBA291A285858BAF4526C6EC312388&params={"vId":"idno_node/{hash}"}',
  },
  relationship: {
    label: "关系图谱",
    urlTemplate: 'http://64.202.65.112:8082/atlas/refactor/#/home/graph/downloadService?id=lanxitest&mode=K_EXPAND&type=NORMAL&atlasToken=2C914E5E1FBFBC4AD15163E0AB03B800&params={"vId":"rel_node/{hash}"}',
  },
};

export default {
  name: "GraphAtlasSection",
  data() {
    return {
      certNoInput: "",
      certNoHash: "",
      activeGraphTab: "fund",
      graphTabs: [
        { name: "fund", label: GRAPH_ATLAS_CONFIG.fund.label },
        { name: "relationship", label: GRAPH_ATLAS_CONFIG.relationship.label },
      ],
    };
  },
  computed: {
    currentGraphUrl() {
      if (!this.certNoHash) {
        return "";
      }
      const config = GRAPH_ATLAS_CONFIG[this.activeGraphTab];
      return config.urlTemplate.replace("{hash}", this.certNoHash);
    },
  },
  methods: {
    handleQuery() {
      const certNo = this.normalizeCertNo(this.certNoInput);
      if (!certNo) {
        this.certNoHash = "";
        this.$message.warning("请输入证件号");
        return;
      }
      this.certNoHash = md5(certNo);
    },
    normalizeCertNo(value) {
      const certNo = value === null || value === undefined ? "" : String(value).trim();
      if (certNo.endsWith("x")) {
        return `${certNo.slice(0, -1)}X`;
      }
      return certNo;
    },
  },
};
</script>

<style lang="scss" scoped>
.graph-atlas-card {
  margin-top: 16px;
  padding: 20px;
  background: #fff;
  border: 1px solid var(--ccdi-border);
  border-radius: 14px;
  box-shadow: var(--ccdi-shadow);
}

.graph-atlas-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.graph-atlas-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--ccdi-text-primary);
}

.graph-atlas-subtitle {
  margin-top: 4px;
  font-size: 12px;
  color: var(--ccdi-text-muted);
}

.graph-atlas-query {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 16px;
}

.graph-atlas-input {
  width: 320px;
}

.graph-atlas-tabs {
  margin-top: 16px;
}

.graph-atlas-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 360px;
  border: 1px dashed #d9e3ee;
  background: #f8fbfe;
}

.graph-atlas-frame-wrapper {
  overflow: hidden;
  border: 1px solid #d9e3ee;
  background: #fff;
}

.graph-atlas-frame {
  display: block;
  width: 100%;
  height: calc(100vh - 280px);
  min-height: 560px;
  max-height: 760px;
  border: 0;
}

@media (max-width: 768px) {
  .graph-atlas-query {
    align-items: stretch;
    flex-direction: column;
  }

  .graph-atlas-input {
    width: 100%;
  }

  .graph-atlas-frame {
    min-height: 480px;
  }
}
</style>
  • Step 2: Check component syntax

Run:

cd ruoyi-ui
nvm use
npm run build:prod

Expected: this may still fail before wiring if the file is not imported, but it must not report syntax errors in GraphAtlasSection.vue once wired in Task 3.

Task 3: Replace SpecialCheck Placeholder

Files:

  • Modify: ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue

  • Step 1: Import and register the new component

Modify the script section:

import ExtendedQuerySection from "./ExtendedQuerySection";
import FamilyAssetLiabilitySection from "./FamilyAssetLiabilitySection";
import GraphAtlasSection from "./GraphAtlasSection";

export default {
  name: "SpecialCheck",
  components: {
    ExtendedQuerySection,
    FamilyAssetLiabilitySection,
    GraphAtlasSection,
  },
  • Step 2: Restructure the content area and replace the placeholder block

The graph must render after the专项排查 data request finishes, even when the family asset/liability list is empty. Replace the current loading/empty/loaded template split with this structure:

<div v-if="pageState === 'loading'" class="special-check-state">
  <div class="state-card">
    <el-skeleton animated :rows="6" />
  </div>
</div>

<div v-else class="special-check-page">
  <div v-if="pageState === 'empty'" class="special-check-state">
    <div class="state-card">
      <el-empty description="暂无员工家庭资产负债核查数据" />
    </div>
  </div>

  <family-asset-liability-section
    v-else
    :rows="currentData.rows"
    :loading="false"
    :project-id="projectId"
    :title="sectionTitle"
    :subtitle="sectionSubtitle"
    @evidence-confirm="$emit('evidence-confirm', $event)"
  />

  <graph-atlas-section />
</div>

Keep the existing <div v-if="projectId" class="special-check-extended-wrapper"> below this block unchanged, so the final order is: asset/liability loaded table or empty state -> graph atlas -> extended query.

  • Step 3: Remove obsolete placeholder styles

Delete these obsolete scoped style blocks from SpecialCheck.vue:

.graph-placeholder-card { ... }
.graph-placeholder-header { ... }
.graph-placeholder-title { ... }
.graph-placeholder-subtitle { ... }
.graph-placeholder-body { ... }
.graph-placeholder-text { ... }

Keep .special-check-extended-wrapper unchanged so the扩展查询 spacing remains stable.

  • Step 4: Build the frontend

Run:

cd ruoyi-ui
nvm use
npm run build:prod

Expected: build completes successfully with no Vue template or module resolution errors.

Task 4: Browser Verification

Files:

  • Verify: ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue

  • Verify: ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue

  • Do not commit: output/browser-use/

  • Step 1: Start frontend for real page testing

Run:

cd ruoyi-ui
nvm use
npm run dev

Expected: dev server starts successfully. Record the local URL. If the default port is occupied, use the URL printed by Vue CLI.

  • Step 2: Use browser-use on a real project detail page

Use browser-use:browser, not a prototype page.

Navigate to a real project detail route, for example:

http://localhost:<port>/ccdiProject/detail/<projectId>?tab=special

Expected:

  • “图谱展示” appears in the专项排查 page after the data request finishes.

  • If the project has资产负债核查 rows, the graph is below员工家庭资产负债专项核查 and above采购/招聘/调动扩展查询.

  • If the project has no资产负债核查 rows, the graph still appears below the existing empty state and above采购/招聘/调动扩展查询.

  • Step 3: Verify empty input behavior

Actions:

  1. Confirm iframe is absent before any query.
  2. Click “查询图谱” with empty input.
  3. Press Enter in the empty input.

Expected:

  • Page shows “请输入证件号”.

  • iframe is still absent.

  • Step 4: Verify funds graph URL by button

Actions:

  1. Input 110101199003074211.
  2. Click “查询图谱”.
  3. Inspect the iframe raw attribute with document.querySelector(".graph-atlas-frame").getAttribute("src"). If the tool only exposes normalized iframe.src, run decodeURIComponent(src) before checking the JSON params fragment.

Expected raw iframe src, or decoded normalized src, contains:

id=ccdi_lanxi_trans
atlasToken=F4BBA291A285858BAF4526C6EC312388
"vId":"idno_node/9b1b5eba4a26c9a68ff1ca06f40bee1b"

External graph content may fail to load; this is acceptable. The src is the pass condition.

  • Step 5: Verify funds graph URL by Enter

Actions:

  1. Replace input with 33078219900101123x.
  2. Press Enter.
  3. Inspect the iframe raw attribute with document.querySelector(".graph-atlas-frame").getAttribute("src"). If the tool only exposes normalized iframe.src, run decodeURIComponent(src) before checking the JSON params fragment.

Expected raw iframe src, or decoded normalized src, contains:

"vId":"idno_node/233c8519f86a57b1f00ec88a32152ce3"

This proves trim + x 转 X + md5.

  • Step 6: Verify relationship graph URL

Actions:

  1. Switch to “关系图谱”.
  2. Inspect the iframe raw attribute with document.querySelector(".graph-atlas-frame").getAttribute("src"). If the tool only exposes normalized iframe.src, run decodeURIComponent(src) before checking the JSON params fragment.

Expected raw iframe src, or decoded normalized src, contains:

id=lanxitest
atlasToken=2C914E5E1FBFBC4AD15163E0AB03B800
"vId":"rel_node/233c8519f86a57b1f00ec88a32152ce3"

External graph content may fail to load; this is acceptable. The src is the pass condition.

  • Step 7: Stop frontend dev process

Stop the process started in Step 1.

Expected: no test frontend process remains running.

Task 5: Implementation Record

Files:

  • Create: docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md

  • Step 1: Write the implementation report

Create the report with this structure:

# 项目详情专项排查图谱嵌入前端实施记录

## 修改内容

- 新增 `GraphAtlasSection.vue`,实现证件号输入、资金图谱/关系图谱页签、md5 后 iframe 链接拼接。
- 调整 `SpecialCheck.vue`,将原图谱占位卡片替换为图谱展示组件。

## 影响范围

- 仅影响项目详情页“专项排查”中的图谱展示区域。
- 不涉及后端接口、数据库、权限、菜单和项目详情顶部一级导航。

## 验证情况

- `cd ruoyi-ui && nvm use && npm run build:prod`:记录结果。
- 真实项目详情页专项排查浏览器验证:记录项目地址、输入样例、资金图谱 iframe `src`、关系图谱 iframe `src`- 说明当前外部图谱内容打不开属于预期,验收只检查 iframe 链接正确。

## 遗留事项

- 无。
  • Step 2: Check report path

Run:

ls docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md

Expected: file exists under docs/reports/implementation/.

Task 6: Final Git Check

Files:

  • Stage when ready:

    • ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue
    • ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue
    • docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md
  • Step 1: Inspect worktree

Run:

git status --short

Expected: existing unrelated dirty files may still be present. Do not stage unrelated files such as existing Docker, application config, SQL, PDF exporter, or other report changes unless the user explicitly requests them.

  • Step 2: Inspect relevant diff

Run:

git diff -- ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md

Expected: diff only contains the graph atlas feature and implementation report.

  • Step 3: Stage only this task

Run:

git add -- ruoyi-ui/src/views/ccdiProject/components/detail/GraphAtlasSection.vue ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue docs/reports/implementation/2026-05-07-project-special-check-graph-atlas-frontend-record.md
git diff --cached --name-status

Expected: staged files are exactly the three paths above.

  • Step 4: Commit if requested

Run only when the user asks to commit:

git commit -m "接入专项排查图谱展示"

Expected: Chinese commit message, no unrelated files included.