diff --git a/docs/superpowers/plans/2026-09-14-reimburse-apply-attachment-display-name.md b/docs/superpowers/plans/2026-09-14-reimburse-apply-attachment-display-name.md
new file mode 100644
index 000000000..9e813a2a7
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-14-reimburse-apply-attachment-display-name.md
@@ -0,0 +1,416 @@
+# 报销申请附件展示名(整单备注)Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 在 Web / 小程序报销申请的申请附件区,有整单备注时用「备注_序号.扩展名」作展示名;空备注回退原始文件名;不改库表与下载名。
+
+**Architecture:** 纯前端展示层。共享命名纯函数;Web 的 `BxAttachmentUpload` / `BxAttachmentReadonly` 增加可选 `remark` prop,内部按列表下标计算展示名;下载仍走原始 `fileName`。小程序申请/详情当前缩略图网格**不显示文件名**,需补一行 caption 才看得见效果。
+
+**Tech Stack:** Vue 3 + TypeScript(web-admin);uni-app(miniprogram);`node:test` 单测(与现有 `web-admin/src/utils/*.test.ts` 一致)
+
+**Spec:** `docs/superpowers/specs/2026-09-14-reimburse-apply-attachment-display-name-design.md`
+
+---
+
+## File map
+
+| File | Responsibility |
+|------|----------------|
+| `web-admin/src/utils/bx/attachment-display-name.ts` | 命名纯函数(sanitize / 截断 / 序号) |
+| `web-admin/src/utils/bx/attachment-display-name.test.ts` | 单测 |
+| `web-admin/src/components/bx/base/BxAttachmentUpload.vue` | 可选 `remark`;UI 用展示名;下载用原始名 |
+| `web-admin/src/components/bx/base/BxAttachmentReadonly.vue` | 同上 |
+| `web-admin/src/views/bx/reimburse/apply/index.vue` | 上传组件与详情只读传入 `addForm.remark` / `viewData.remark` |
+| `web-admin/src/views/bx/reimburse/approve/index.vue` | 详情申请附件只读传入 `viewData.remark`(有则套用) |
+| `miniprogram/utils/bxAttachmentDisplayName.js` | 与 Web 对称的纯函数 |
+| `miniprogram/pages/bx/reimburse/apply.vue` | 附件网格补展示名 caption |
+| `miniprogram/pages/bx/reimburse/detail.vue` | 同上 |
+
+---
+
+### Task 1: Web 命名纯函数 + 单测(TDD)
+
+**Files:**
+- Create: `web-admin/src/utils/bx/attachment-display-name.ts`
+- Create: `web-admin/src/utils/bx/attachment-display-name.test.ts`
+
+- [ ] **Step 1: 写失败测试**
+
+```ts
+import assert from 'node:assert/strict'
+import { describe, it } from 'node:test'
+import {
+ sanitizeRemarkBaseName,
+ buildApplyAttachmentDisplayName,
+ mapApplyAttachmentDisplayNames
+} from './attachment-display-name'
+
+describe('sanitizeRemarkBaseName', () => {
+ it('trims and replaces illegal chars and spaces', () => {
+ assert.equal(sanitizeRemarkBaseName(' 差旅/说明 A '), '差旅_说明_A')
+ })
+
+ it('truncates to 40 chars', () => {
+ const long = '啊'.repeat(50)
+ assert.equal(sanitizeRemarkBaseName(long).length, 40)
+ })
+
+ it('returns empty for blank', () => {
+ assert.equal(sanitizeRemarkBaseName(' '), '')
+ })
+})
+
+describe('buildApplyAttachmentDisplayName', () => {
+ it('falls back to original when remark empty', () => {
+ assert.equal(
+ buildApplyAttachmentDisplayName({ remark: '', originalFileName: 'a.pdf', index1: 1 }),
+ 'a.pdf'
+ )
+ })
+
+ it('uses remark_index + ext', () => {
+ assert.equal(
+ buildApplyAttachmentDisplayName({
+ remark: '差旅说明',
+ originalFileName: 'photo.JPG',
+ index1: 2
+ }),
+ '差旅说明_2.JPG'
+ )
+ })
+
+ it('handles missing extension', () => {
+ assert.equal(
+ buildApplyAttachmentDisplayName({
+ remark: '说明',
+ originalFileName: 'readme',
+ index1: 1
+ }),
+ '说明_1'
+ )
+ })
+})
+
+describe('mapApplyAttachmentDisplayNames', () => {
+ it('maps list with 1-based indices', () => {
+ const out = mapApplyAttachmentDisplayNames('备注', [
+ { fileName: 'a.pdf' },
+ { fileName: 'b.jpg' }
+ ])
+ assert.deepEqual(out, ['备注_1.pdf', '备注_2.jpg'])
+ })
+})
+```
+
+- [ ] **Step 2: 跑测确认失败**
+
+```bash
+cd web-admin && node --import tsx --test src/utils/bx/attachment-display-name.test.ts
+```
+
+Expected: FAIL(模块不存在)。若项目惯用其它跑法,对照 `src/utils/pl-slip-product-brand.test.ts` 的执行方式(例如 `pnpm exec tsx --test ...`)。
+
+- [ ] **Step 3: 实现**
+
+`web-admin/src/utils/bx/attachment-display-name.ts`:
+
+```ts
+const ILLEGAL = /[/\\:*?"<>|\u0000-\u001f]/g
+
+export function sanitizeRemarkBaseName(remark: string): string {
+ const cleaned = String(remark || '')
+ .trim()
+ .replace(ILLEGAL, '_')
+ .replace(/\s+/g, '_')
+ .replace(/_+/g, '_')
+ .replace(/^_|_$/g, '')
+ if (!cleaned) return ''
+ return cleaned.length > 40 ? cleaned.slice(0, 40) : cleaned
+}
+
+function splitExt(fileName: string): { base: string; ext: string } {
+ const name = String(fileName || '')
+ const i = name.lastIndexOf('.')
+ if (i <= 0 || i === name.length - 1) return { base: name, ext: '' }
+ return { base: name.slice(0, i), ext: name.slice(i) }
+}
+
+export function buildApplyAttachmentDisplayName(opts: {
+ remark: string
+ originalFileName: string
+ index1: number
+}): string {
+ const original = String(opts.originalFileName || '').trim() || '申请附件'
+ const base = sanitizeRemarkBaseName(opts.remark)
+ if (!base) return original
+ const { ext } = splitExt(original)
+ const n = Math.max(1, Math.floor(Number(opts.index1) || 1))
+ return `${base}_${n}${ext}`
+}
+
+export function mapApplyAttachmentDisplayNames(
+ remark: string,
+ files: Array<{ fileName?: string | null }>
+): string[] {
+ return (files || []).map((f, i) =>
+ buildApplyAttachmentDisplayName({
+ remark,
+ originalFileName: f.fileName || '',
+ index1: i + 1
+ })
+ )
+}
+```
+
+- [ ] **Step 4: 跑测确认通过**
+
+同 Step 2 命令。Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add web-admin/src/utils/bx/attachment-display-name.ts web-admin/src/utils/bx/attachment-display-name.test.ts
+git commit -m "$(cat <<'EOF'
+feat(web-admin): add apply attachment display name helper
+
+EOF
+)"
+```
+
+---
+
+### Task 2: Web 附件组件支持 `remark` 展示名
+
+**Files:**
+- Modify: `web-admin/src/components/bx/base/BxAttachmentUpload.vue`
+- Modify: `web-admin/src/components/bx/base/BxAttachmentReadonly.vue`
+
+- [ ] **Step 1: Upload 组件**
+
+1. Import `buildApplyAttachmentDisplayName`
+2. Props 增加可选 `remark?: string`(默认 `''`)
+3. 增加方法:
+
+```ts
+function displayNameOf(file: BxAttachmentItem, index: number) {
+ return buildApplyAttachmentDisplayName({
+ remark: props.remark || '',
+ originalFileName: file.fileName || '',
+ index1: index + 1
+ })
+}
+```
+
+4. 模板中(约 L44、L68):
+ - `:alt` / 名称 span / `:title` 改用 `displayNameOf(file, index)`
+ - **下载** `handleDownload` / `downloadBxAttachment(file)` 仍传原始 `file`(含原始 `fileName`)
+ - 预览弹窗标题可用展示名(调用 `previewBxAttachment` 后把 `previewFileName` 设为展示名亦可;勿改下载)
+
+`v-for="(file, index) in files"` 确保有 `index`。
+
+- [ ] **Step 2: Readonly 组件**
+
+同样增加 `remark?: string`,`displayNameOf`,列表文案用展示名;`handleDownload` 仍用原始 `file`。
+
+注意:`displayFiles` 是 watch 出来的副本,v-for 用 index 即可,勿把展示名写回 `file.fileName`。
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add web-admin/src/components/bx/base/BxAttachmentUpload.vue web-admin/src/components/bx/base/BxAttachmentReadonly.vue
+git commit -m "$(cat <<'EOF'
+feat(web-admin): show remark-based names on bx attachment cards
+
+EOF
+)"
+```
+
+---
+
+### Task 3: Web 报销申请 / 审批页传入 `remark`
+
+**Files:**
+- Modify: `web-admin/src/views/bx/reimburse/apply/index.vue`
+- Modify: `web-admin/src/views/bx/reimburse/approve/index.vue`
+
+- [ ] **Step 1: 申请页**
+
+`BxAttachmentUpload` 增加:
+
+```vue
+:remark="addForm.remark"
+```
+
+详情「申请附件」:
+
+```vue
+
+```
+
+**不要**给关联发票 / 发放 / 冲抵的 `BxAttachmentReadonly` 传备注。
+
+- [ ] **Step 2: 审批页**
+
+申请附件只读同样传入 `:remark="String(viewData.remark || '')"`(`viewData` 为当前打开的报销单;若变量名不同,用该页实际的详情对象上的 `remark`)。
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add web-admin/src/views/bx/reimburse/apply/index.vue web-admin/src/views/bx/reimburse/approve/index.vue
+git commit -m "$(cat <<'EOF'
+feat(web-admin): pass remark into reimburse apply attachment UI
+
+EOF
+)"
+```
+
+---
+
+### Task 4: 小程序对称工具函数
+
+**Files:**
+- Create: `miniprogram/utils/bxAttachmentDisplayName.js`
+
+- [ ] **Step 1: 实现**(与 Web 逻辑一致,便于两端对齐)
+
+```js
+const ILLEGAL = /[/\\:*?"<>|\u0000-\u001f]/g
+
+export function sanitizeRemarkBaseName(remark) {
+ const cleaned = String(remark || '')
+ .trim()
+ .replace(ILLEGAL, '_')
+ .replace(/\s+/g, '_')
+ .replace(/_+/g, '_')
+ .replace(/^_|_$/g, '')
+ if (!cleaned) return ''
+ return cleaned.length > 40 ? cleaned.slice(0, 40) : cleaned
+}
+
+function splitExt(fileName) {
+ const name = String(fileName || '')
+ const i = name.lastIndexOf('.')
+ if (i <= 0 || i === name.length - 1) return { base: name, ext: '' }
+ return { base: name.slice(0, i), ext: name.slice(i) }
+}
+
+export function buildApplyAttachmentDisplayName({ remark, originalFileName, index1 }) {
+ const original = String(originalFileName || '').trim() || '申请附件'
+ const base = sanitizeRemarkBaseName(remark)
+ if (!base) return original
+ const { ext } = splitExt(original)
+ const n = Math.max(1, Math.floor(Number(index1) || 1))
+ return `${base}_${n}${ext}`
+}
+```
+
+(小程序可不强制单测文件;逻辑与 Web 测试用例对齐即可。)
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add miniprogram/utils/bxAttachmentDisplayName.js
+git commit -m "$(cat <<'EOF'
+feat(miniprogram): add apply attachment display name helper
+
+EOF
+)"
+```
+
+---
+
+### Task 5: 小程序申请页 / 详情页展示名
+
+**Files:**
+- Modify: `miniprogram/pages/bx/reimburse/apply.vue`
+- Modify: `miniprogram/pages/bx/reimburse/detail.vue`
+
+**说明:** 当前附件区只有缩略图网格、**没有文件名文案**;必须补 caption,否则功能不可见。
+
+- [ ] **Step 1: 申请页**
+
+Import:
+
+```js
+import { buildApplyAttachmentDisplayName } from '@/utils/bxAttachmentDisplayName'
+```
+
+Helper:
+
+```js
+function attachmentDisplayName(file, index) {
+ return buildApplyAttachmentDisplayName({
+ remark: form.remark || '',
+ originalFileName: file.fileName || '',
+ index1: index + 1
+ })
+}
+```
+
+模板在 `att-grid` 内每个 `att-img` 下增加一行名称(样式用现有 muted 小字即可,例如 `att-img__name`):
+
+```vue
+
+
+ ×
+ {{ attachmentDisplayName(file, idx) }}
+
+```
+
+注意:上传成功后 `files` 项可能未带 `fileName`(当前 `uploadOne` 未写入)。在 `uploadOne` 成功回写时补上:
+
+```js
+fileName: data.fileName || data.file_name || ''
+```
+
+否则有备注时扩展名可能丢失,空备注时只能显示「申请附件」。
+
+加少量 SCSS:名称单行省略、字号约 11–12px、居中或左对齐与网格协调。
+
+- [ ] **Step 2: 详情页**
+
+同样在申请附件网格下显示 `buildApplyAttachmentDisplayName({ remark: detail.value?.remark || '', originalFileName: file.fileName || '', index1: idx + 1 })`。
+
+加载附件列表时确保映射了 `fileName`(若 `listByBiz` 已有则沿用)。
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add miniprogram/pages/bx/reimburse/apply.vue miniprogram/pages/bx/reimburse/detail.vue
+git commit -m "$(cat <<'EOF'
+feat(miniprogram): show remark-based names on reimburse apply attachments
+
+EOF
+)"
+```
+
+---
+
+### Task 6: 图谱(若改动需更新)与验收核对
+
+- [ ] **Step 1:** `./scripts/update-graph.sh frontend` 或 `all`(仅 web-admin / miniprogram 有代码变更时)
+- [ ] **Step 2:** 对照 spec 验收清单代码级勾选:
+ 1. 有备注 → `备注_n.ext`
+ 2. 清空备注 → 原始名
+ 3. 下载仍原始名(组件未改 `downloadBxAttachment` 入参中的 fileName)
+ 4. 发票区未传 remark
+ 5. Web / 小程序规则一致
+- [ ] **Step 3:** 有图谱变更则 commit;无则跳过空提交
+
+---
+
+## Spec coverage
+
+| Spec | Task |
+|------|------|
+| 纯函数命名规则 | Task 1、4 |
+| Web 上传/只读展示 | Task 2 |
+| 申请页 + 详情传 remark;审批页一并 | Task 3 |
+| 小程序申请 + 详情 | Task 5 |
+| 不改库 / 不改下载 / 不动发票 | Task 2–5 约束 |
+| 空备注回退原始名 | Task 1 测试 |
+
+## 执行说明
+
+- 分支:按用户要求在当前分支继续(勿另开 worktree,除非另有指示)
+- 勿提交 `backend/app.pid`