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

> **执行说明:** 按当前项目 `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/md5`、`browser-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`
- md5：`9b1b5eba4a26c9a68ff1ca06f40bee1b`
- 资金图谱期望 `vId`：`idno_node/9b1b5eba4a26c9a68ff1ca06f40bee1b`
- 关系图谱期望 `vId`：`rel_node/9b1b5eba4a26c9a68ff1ca06f40bee1b`

标准化样例：

- 输入：` 33078219900101123x `
- 标准化后：`33078219900101123X`
- md5：`233c8519f86a57b1f00ec88a32152ce3`

## 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:

```bash
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:

```bash
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:

```vue
<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:

```bash
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:

```js
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:

```vue
<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`:

```scss
.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:

```bash
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:

```bash
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:

```text
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:

```text
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:

```text
"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:

```text
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:

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

## 修改内容

- 新增 `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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

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

Expected: Chinese commit message, no unrelated files included.
