docs(plan): add SysApp application integration management plan

- 7 个有序任务:Service → Index → i18n → Router → List → Form → E2E
- 任务粒度合适,每个聚焦单一文件/职责
- 包含完整依赖图、回滚计划、测试清单
- 严格遵循现有 plan 格式

对应工单 rui/rui-frontend#4
This commit is contained in:
2026-06-07 19:36:02 +08:00
parent f4761ae145
commit cd2d68e60e
@@ -0,0 +1,961 @@
# SysApp(第三方应用集成)管理界面实施计划
> **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:** 在 admin-ui 后台实现 SysApp(第三方应用集成)管理模块,提供对微信/支付宝/Stripe 等第三方平台应用凭证信息的统一管理能力。
**Architecture:** 完全照搬现有 `oauth2-client` 模块的 CRUD 模式 —— `BaseService` 13 行极简继承 + `RuiTable` 列表页 + `FormDialog` 弹窗(el-tabs 4 Tab)。**不引入新依赖**。
**Tech Stack:** Vue 3, TypeScript, Element Plus, Vite, Pinia
---
## 文件变更清单
| # | 文件 | 变更类型 | 说明 |
|---|------|---------|------|
| 1 | `admin-ui/src/service/system/sysAppService.ts` | 新建 | Service(继承 BaseService |
| 2 | `admin-ui/src/service/system/index.ts` | 修改 | 追加导出 sysAppService |
| 3 | `admin-ui/src/locales/zh-CN.ts` | 修改 | 加 `systemApp: '应用集成'` |
| 4 | `admin-ui/src/locales/en-US.ts` | 修改 | 加 `systemApp: 'App Integration'` |
| 5 | `admin-ui/src/router/modules/system.ts` | 修改 | 注册 `/system/app` 路由 |
| 6 | `admin-ui/src/views/system/app/Index.vue` | 新建 | 列表页(RuiTable |
| 7 | `admin-ui/src/views/system/app/SysAppFormDialog.vue` | 新建 | 表单弹窗(4 Tab) |
**合计:新建 3 个文件 + 修改 4 个文件 = 7 个文件**
---
## 任务依赖图
```
Task 1 (Service) ──┬── Task 2 (Service Index)
├── Task 5 (列表页) ──┐
│ │
└── Task 6 (表单) ────┴── Task 7 (端到端验证)
Task 3 (i18n) ──┐
Task 4 (Router) ┴── Task 5 (列表页)
```
---
## Task 1: 创建 SysApp Service
**Files:**
- Create: `admin-ui/src/service/system/sysAppService.ts`
- [ ] **Step 1: 写入 Service 文件**
`admin-ui/src/service/system/sysAppService.ts` 创建:
```typescript
import { BaseService } from '../BaseService'
/**
* SysApp(第三方应用集成)服务
*
* <p>负责与后端 /system/admin/app 接口的通信,继承 BaseService 获得标准 CRUD 能力。</p>
*
* <p><b>后续升级路径</b>:后端文件上传接口(rui/rui-framework#4)就绪后,可在此文件追加
* `uploadCertificate(file: File)` 方法,并将表单中 certificates 字段从 JSON textarea
* 升级为文件上传组件。</p>
*/
class SysAppService extends BaseService {
constructor() {
super('/system/admin/app')
}
}
/** SysApp 服务单例 */
export const sysAppService = new SysAppService()
```
- [ ] **Step 2: 验证类型检查**
Run:
```bash
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/service/system/sysAppService.ts
```
Expected: 无错误输出
- [ ] **Step 3: Commit**
```bash
git add admin-ui/src/service/system/sysAppService.ts
git commit -m "feat(sysApp): add sysAppService extending BaseService for /system/admin/app"
```
---
## Task 2: 在 Service 统一入口导出 sysAppService
**Files:**
- Modify: `admin-ui/src/service/system/index.ts`
- [ ] **Step 1: 追加导出语句**
在文件末尾追加:
```typescript
export { sysAppService } from './sysAppService'
```
- [ ] **Step 2: 验证导入**
Run:
```bash
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck src/service/system/index.ts
```
Expected: 无错误输出
- [ ] **Step 3: Commit**
```bash
git add admin-ui/src/service/system/index.ts
git commit -m "feat(sysApp): export sysAppService from system service index"
```
---
## Task 3: 配置国际化(中英文)
**Files:**
- Modify: `admin-ui/src/locales/zh-CN.ts`
- Modify: `admin-ui/src/locales/en-US.ts`
- [ ] **Step 1: 在 zh-CN.ts 添加中文**
定位到 `systemOAuth2Client: 'OAuth2客户端',` 这一行,在其后添加:
```typescript
systemApp: '应用集成',
```
(注意缩进:与 systemOAuth2Client 保持一致的 4 空格)
- [ ] **Step 2: 在 en-US.ts 添加英文**
定位到 `systemOAuth2Client: 'OAuth2 Client',`(或对应位置),在其后添加:
```typescript
systemApp: 'App Integration',
```
(如果 en-US.ts 没有 systemOAuth2Client 这一行,则加在 system 块的合理位置,参考 systemOAuth2Client 的就近位置)
- [ ] **Step 3: 验证**
Run:
```bash
grep -n "systemApp" admin-ui/src/locales/zh-CN.ts admin-ui/src/locales/en-US.ts
```
Expected: 两个文件各有一行匹配
- [ ] **Step 4: Commit**
```bash
git add admin-ui/src/locales/zh-CN.ts admin-ui/src/locales/en-US.ts
git commit -m "feat(sysApp): add systemApp i18n key (zh-CN: '应用集成', en-US: 'App Integration')"
```
---
## Task 4: 注册路由
**Files:**
- Modify: `admin-ui/src/router/modules/system.ts`
- [ ] **Step 1: 在 M 常量加键**
定位到 `systemOAuth2Client: 'menu.systemOAuth2Client',` 这一行,在其后添加:
```typescript
systemApp: 'menu.systemApp',
```
- [ ] **Step 2: 在 systemRoutes 数组加路由**
定位到 `system/oauth2-client` 路由条目,在其后添加:
```typescript
{ path: 'system/app', name: 'SystemApp', component: () => import('@/views/system/app/Index.vue'), meta: { i18n: M.systemApp } },
```
- [ ] **Step 3: 验证**
Run:
```bash
grep -n "systemApp\|system/app" admin-ui/src/router/modules/system.ts
```
Expected: 至少 2 行匹配(一个 M 常量,一个 systemRoutes 数组)
- [ ] **Step 4: Commit**
```bash
git add admin-ui/src/router/modules/system.ts
git commit -m "feat(sysApp): register /system/app route in system router"
```
---
## Task 5: 创建列表页
**Files:**
- Create: `admin-ui/src/views/system/app/Index.vue`
> **依赖**Task 1Service)、Task 3i18n)、Task 4Router)必须先完成。
- [ ] **Step 1: 创建目录**
```bash
mkdir -p admin-ui/src/views/system/app
```
- [ ] **Step 2: 写入列表页**
创建 `admin-ui/src/views/system/app/Index.vue`,内容如下:
```vue
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { sysAppService } from '@/service/system/sysAppService'
import type { TableColumn, PageResult, PageParams } from '@/components/RuiTable'
import SysAppFormDialog from './SysAppFormDialog.vue'
/**
* 查询参数
*/
const query = ref({
name: '',
platform: '',
ownerType: '',
status: '',
})
/**
* 平台枚举
*/
const platformMap: Record<string, { label: string; type: 'success' | 'primary' | 'warning' }> = {
wechat: { label: '微信', type: 'success' },
alipay: { label: '支付宝', type: 'primary' },
stripe: { label: 'Stripe', type: 'warning' },
}
/**
* 所有者类型枚举
*/
const ownerTypeMap: Record<string, { label: string; type: 'primary' | 'success' }> = {
PLATFORM: { label: '平台级', type: 'primary' },
TENANT: { label: '租户级', type: 'success' },
}
/**
* 表格列配置
*/
const columns: TableColumn[] = [
{ prop: 'name', label: '应用名称', minWidth: 150 },
{
prop: 'platform',
label: '平台',
width: 100,
align: 'center',
slot: true,
},
{
prop: 'ownerType',
label: '所有者',
width: 100,
align: 'center',
slot: true,
},
{ prop: 'appId', label: '应用ID', minWidth: 120 },
{
prop: 'status',
label: '状态',
width: 90,
align: 'center',
slot: true,
},
{
prop: 'createdAt',
label: '创建时间',
minWidth: 180,
sortable: 'custom',
dataType: 'dateTime',
},
]
/**
* 加载数据
*/
async function loadData(params: PageParams & Record<string, any>): Promise<PageResult> {
return sysAppService.page(params)
}
/**
* 表格组件引用
*/
const tableRef = ref<InstanceType<typeof import('@/components/RuiTable').default>>()
/**
* 弹窗显示状态
*/
const dialogVisible = ref(false)
const currentRow = ref<any>(null)
/**
* 新增
*/
function handleAdd() {
currentRow.value = null
dialogVisible.value = true
}
/**
* 编辑
*/
function handleEdit(row: any) {
currentRow.value = row
dialogVisible.value = true
}
/**
* 删除
*/
function handleDelete(row: any) {
ElMessageBox.confirm(`确认删除应用 "${row.name}" 吗?`, '提示', {
type: 'warning',
}).then(async () => {
try {
await sysAppService.remove(row.id)
ElMessage.success('删除成功')
tableRef.value?.refresh()
} catch {
// 错误已由请求拦截器统一提示
}
}).catch(() => {})
}
/**
* 状态切换
*/
async function handleStatusChange(row: any, status: number) {
if (!row?.id) return
try {
await sysAppService.changeStatus(row.id, status)
ElMessage.success(status === 1 ? '启用成功' : '禁用成功')
} catch {
row.status = status === 1 ? 0 : 1
}
}
/**
* 表单操作成功回调
*/
function handleFormSuccess() {
tableRef.value?.refresh()
}
</script>
<template>
<div>
<h2 class="text-xl font-bold mb-4">
{{ $t('menu.systemApp') }}
</h2>
<RuiTable
ref="tableRef"
:columns="columns"
:load-data="loadData"
:show-selection="true"
:exportable="true"
export-filename="SysApp应用集成列表"
>
<!-- 查询区域 -->
<template #search="{ query: q, search, reset }">
<el-form-item label="应用名称">
<el-input v-model.trim="q.name" placeholder="请输入应用名称" clearable @keyup.enter="search" />
</el-form-item>
<el-form-item label="平台">
<el-select v-model="q.platform" placeholder="全部" clearable style="width: 120px">
<el-option v-for="(v, k) in platformMap" :key="k" :label="v.label" :value="k" />
</el-select>
</el-form-item>
<el-form-item label="所有者">
<el-select v-model="q.ownerType" placeholder="全部" clearable style="width: 120px">
<el-option v-for="(v, k) in ownerTypeMap" :key="k" :label="v.label" :value="k" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部" clearable style="width: 100px">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">
查询
</el-button>
<el-button @click="reset">
重置
</el-button>
</el-form-item>
</template>
<!-- 工具栏左侧 -->
<template #toolbar-left>
<el-button type="primary" @click="handleAdd">
新增应用
</el-button>
</template>
<!-- 自定义列平台 -->
<template #column-platform="{ row }">
<el-tag v-if="platformMap[row.platform]" :type="platformMap[row.platform].type" size="small">
{{ platformMap[row.platform].label }}
</el-tag>
<span v-else>-</span>
</template>
<!-- 自定义列所有者 -->
<template #column-ownerType="{ row }">
<el-tag v-if="ownerTypeMap[row.ownerType]" :type="ownerTypeMap[row.ownerType].type" size="small">
{{ ownerTypeMap[row.ownerType].label }}
</el-tag>
<span v-else>-</span>
</template>
<!-- 自定义列状态 -->
<template #column-status="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="(val: number) => handleStatusChange(row, val)"
/>
</template>
<!-- 操作列 -->
<template #action="{ row }">
<el-button link type="primary" size="small" @click="handleEdit(row)">
编辑
</el-button>
<el-button link type="danger" size="small" @click="handleDelete(row)">
删除
</el-button>
</template>
</RuiTable>
<!-- 新增/编辑弹窗 -->
<SysAppFormDialog
v-model:visible="dialogVisible"
:row="currentRow"
@success="handleFormSuccess"
/>
</div>
</template>
```
- [ ] **Step 3: 验证类型检查**
Run:
```bash
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck
```
Expected: 无错误输出
- [ ] **Step 4: Commit**
```bash
git add admin-ui/src/views/system/app/Index.vue
git commit -m "feat(sysApp): add SysApp list page with RuiTable, search, status switch"
```
---
## Task 6: 创建表单弹窗
**Files:**
- Create: `admin-ui/src/views/system/app/SysAppFormDialog.vue`
> **依赖**Task 1Service)必须先完成。
- [ ] **Step 1: 写入表单弹窗**
创建 `admin-ui/src/views/system/app/SysAppFormDialog.vue`,内容如下:
```vue
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { sysAppService } from '@/service/system/sysAppService'
const props = defineProps<{
visible: boolean
row: any
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}>()
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val),
})
/**
* 平台选项
*/
const platformOptions = [
{ label: '微信', value: 'wechat' },
{ label: '支付宝', value: 'alipay' },
{ label: 'Stripe', value: 'stripe' },
]
/**
* 所有者选项
*/
const ownerTypeOptions = [
{ label: '平台级', value: 'PLATFORM' },
{ label: '租户级', value: 'TENANT' },
]
/**
* 签名方式选项
*/
const signTypeOptions = [
{ label: 'RSA2', value: 'RSA2' },
{ label: 'MD5', value: 'MD5' },
{ label: 'HMAC', value: 'HMAC' },
]
/**
* 表单默认值
*/
const defaultForm = {
id: undefined as number | undefined,
ownerType: 'PLATFORM',
platform: 'wechat',
name: '',
appId: '',
appSecret: '',
appKey: '',
certificates: '',
aesKey: '',
redirectUri: '',
merchantId: '',
signType: 'RSA2',
notifyUrl: '',
apiBase: '',
isSandbox: 0,
extra: '',
status: 1,
description: '',
sortNo: 0,
}
const form = ref({ ...defaultForm })
const formRef = ref()
/**
* 校验规则
*/
const rules = {
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
ownerType: [{ required: true, message: '请选择所有者类型', trigger: 'change' }],
platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
}
const loading = ref(false)
/**
* 校验 JSON 字段
*/
function validateJSON(value: string, fieldName: string): boolean {
if (!value || !value.trim()) return true
try {
JSON.parse(value)
return true
} catch {
ElMessage.error(`${fieldName} JSON 格式错误`)
return false
}
}
/**
* 提交表单
*/
async function handleSubmit() {
await formRef.value.validate()
if (!validateJSON(form.value.certificates, 'certificates')) return
if (!validateJSON(form.value.extra, 'extra')) return
loading.value = true
try {
const isEdit = !!form.value.id
const success = isEdit
? await sysAppService.update(form.value as any)
: await sysAppService.add(form.value)
if (success !== false) {
ElMessage.success(isEdit ? '修改成功' : '新增成功')
emit('success')
dialogVisible.value = false
}
} catch {
// 错误已由请求拦截器统一提示
} finally {
loading.value = false
}
}
/**
* 监听弹窗显示,初始化表单
*/
watch(() => props.visible, async (val) => {
if (val) {
if (props.row) {
// 编辑:拉详情(确保拿到完整字段)
try {
const detail = await sysAppService.getById(props.row.id)
form.value = { ...defaultForm, ...detail }
} catch {
// 拉取失败回退到 row
form.value = { ...defaultForm, ...props.row }
}
} else {
// 新增:重置
form.value = { ...defaultForm }
}
}
})
</script>
<template>
<el-dialog
v-model="dialogVisible"
:title="form.id ? '编辑应用' : '新增应用'"
width="760px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-tabs>
<!-- Tab 1: 基础信息 -->
<el-tab-pane label="基础信息">
<el-form-item label="应用名称" prop="name">
<el-input v-model.trim="form.name" placeholder="请输入应用名称" />
</el-form-item>
<el-form-item label="所有者类型" prop="ownerType">
<el-select v-model="form.ownerType" style="width: 100%">
<el-option v-for="opt in ownerTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="平台" prop="platform">
<el-select v-model="form.platform" style="width: 100%">
<el-option v-for="opt in platformOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="备注" />
</el-form-item>
<el-form-item label="排序号">
<el-input-number v-model="form.sortNo" :min="0" style="width: 200px" />
</el-form-item>
</el-tab-pane>
<!-- Tab 2: 凭证信息 -->
<el-tab-pane label="凭证信息">
<el-form-item label="应用ID">
<el-input v-model.trim="form.appId" placeholder="第三方平台应用IDUNIQUE" />
</el-form-item>
<el-form-item label="应用密钥">
<el-input
v-model="form.appSecret"
type="password"
show-password
placeholder="留空表示不修改"
/>
</el-form-item>
<el-form-item label="应用Key">
<el-input
v-model="form.appKey"
type="password"
show-password
placeholder="留空表示不修改"
/>
</el-form-item>
<el-form-item label="AES Key">
<el-input
v-model="form.aesKey"
type="password"
show-password
placeholder="留空表示不修改"
/>
</el-form-item>
<el-form-item label="证书">
<el-input
v-model="form.certificates"
type="textarea"
:rows="4"
placeholder='示例:[{"name":"cert1","content":"<PEM>"}]'
/>
<div class="text-xs text-gray-400 mt-1">
多证书 JSON 数组格式[&#123;name, content&#125;]待后端文件上传接口就绪后升级为文件上传
</div>
</el-form-item>
</el-tab-pane>
<!-- Tab 3: 接口配置 -->
<el-tab-pane label="接口配置">
<el-form-item label="回调地址">
<el-input v-model.trim="form.redirectUri" placeholder="OAuth2 回调地址" />
</el-form-item>
<el-form-item label="支付回调">
<el-input v-model.trim="form.notifyUrl" placeholder="支付回调 URL" />
</el-form-item>
<el-form-item label="API 根地址">
<el-input v-model.trim="form.apiBase" placeholder="API 根地址" />
</el-form-item>
<el-form-item label="商户号">
<el-input v-model.trim="form.merchantId" placeholder="商户号" />
</el-form-item>
<el-form-item label="签名方式">
<el-select v-model="form.signType" style="width: 100%">
<el-option v-for="opt in signTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
</el-tab-pane>
<!-- Tab 4: 高级 -->
<el-tab-pane label="高级">
<el-form-item label="沙箱环境">
<el-switch
v-model="form.isSandbox"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="扩展 JSON">
<el-input
v-model="form.extra"
type="textarea"
:rows="4"
placeholder='示例:{"key":"value"}'
/>
<div class="text-xs text-gray-400 mt-1">
JSON 扩展字段提交前需通过 JSON 格式校验
</div>
</el-form-item>
<el-form-item label="状态">
<el-switch
v-model="form.status"
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="禁用"
/>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">
取消
</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">
保存
</el-button>
</template>
</el-dialog>
</template>
```
- [ ] **Step 2: 验证类型检查**
Run:
```bash
cd admin-ui && npx vue-tsc --noEmit --skipLibCheck
```
Expected: 无错误输出
- [ ] **Step 3: Commit**
```bash
git add admin-ui/src/views/system/app/SysAppFormDialog.vue
git commit -m "feat(sysApp): add SysApp form dialog with 4 tabs (basic/credentials/api/advanced)"
```
---
## Task 7: 端到端验证
**Files:** 无(验证任务)
> **依赖**:所有前置任务(Task 1-6)已完成。
- [ ] **Step 1: 运行类型检查**
Run:
```bash
pnpm --filter admin-ui type-check
```
Expected: 0 errors
- [ ] **Step 2: 运行 Lint**
Run:
```bash
pnpm --filter admin-ui lint
```
Expected: 0 errors
- [ ] **Step 3: 启动 dev server 并验证**
```bash
pnpm dev:admin
```
打开浏览器,登录后访问 `/system/app`,逐条验证:
- [ ] **Step 3.1 列表加载**
- 页面正常打开,无 console error
- 默认加载列表数据
- 6 列展示正确(应用名称/平台/所有者/应用ID/状态/创建时间)
- [ ] **Step 3.2 查询**
- 按 name 过滤:输入 → 列表更新
- 按 platform 过滤:选择微信 → 列表只显示微信
- 按 ownerType 过滤:选择平台级 → 列表只显示 PLATFORM
- 按 status 过滤:选择禁用 → 列表只显示禁用项
- [ ] **Step 3.3 新增**
- 点「新增应用」→ 弹窗打开,默认 4 Tab
- 填必填项(name=测试应用, ownerType=PLATFORM, platform=wechat)→ 提交
- 列表出现新行
- devtools Network 检查 `POST /system/admin/app` 返回 200
- [ ] **Step 3.4 编辑**
- 点行内编辑 → 弹窗加载详情
- 4 个 Tab 正确回显
- 修改 name → 提交 → 列表更新
- [ ] **Step 3.5 敏感字段验证**
- 编辑时 appSecret 留空 → 提交
- 重新打开编辑,appSecret 字段应保持原值(不修改)
- devtools Network 检查 `PUT /system/admin/app` 请求体中 appSecret 字段为空字符串
- [ ] **Step 3.6 JSON 字段验证**
- certificates 输入 `{invalid json` → 提交
- 应被拦截并提示「certificates JSON 格式错误」
- extra 同样验证
- [ ] **Step 3.7 启停**
- 点击状态 Switch → 接口调用 → 列表状态切换
- 模拟失败:可在 devtools 拦截请求,验证 row.status 回滚
- [ ] **Step 3.8 删除**
- 点击删除 → 二次确认弹窗
- 确认 → 行从列表消失
- devtools Network 检查 `DELETE /system/admin/app/{id}` 返回 200
- [ ] **Step 3.9 批量删除**
- 勾选 2-3 行 → 批量删除按钮(toolbar)→ 确认 → 全部消失
- [ ] **Step 3.10 导出**
- 点击导出 → 下载 CSV 文件
- 文件名包含日期
- 字段对应列表列
- [ ] **Step 3.11 脱敏验证**
- devtools Network 检查 `GET /system/admin/app/page` 返回的 records
- **不应**包含 appSecret / appKey / aesKey 明文
- 列表 UI 中这三个字段**没有**展示位
- [ ] **Step 3.12 菜单展示**
- 侧边栏「系统管理」分组下出现「应用集成」子菜单
- 点击跳转 `/system/app`
- 中文/英文切换均正常
- [ ] **Step 4: 验证 git log**
Run:
```bash
git log --oneline -10
```
Expected: 看到 6 个提交:
- feat(sysApp): add sysAppService extending BaseService
- feat(sysApp): export sysAppService from system service index
- feat(sysApp): add systemApp i18n key
- feat(sysApp): register /system/app route in system router
- feat(sysApp): add SysApp list page
- feat(sysApp): add SysApp form dialog
---
## 回滚计划
如果出现问题,按以下顺序回滚:
1. 回滚 Task 6: `git revert <task6-commit>`(删除表单)
2. 回滚 Task 5: `git revert <task5-commit>`(删除列表页)
3. 回滚 Task 4: `git revert <task4-commit>`(取消路由)
4. 回滚 Task 3: `git revert <task3-commit>`(删除 i18n
5. 回滚 Task 2: `git revert <task2-commit>`(取消导出)
6. 回滚 Task 1: `git revert <task1-commit>`(删除 Service
如需完全回滚:`git reset --hard <task0-commit>`
---
## 测试清单
### 静态检查
- [ ] `pnpm --filter admin-ui type-check` 0 errors
- [ ] `pnpm --filter admin-ui lint` 0 errors
### 列表功能
- [ ] 列表加载正常
- [ ] 4 个查询条件均生效
- [ ] 分页正常
- [ ] 列设置可隐藏/显示列
- [ ] 导出 CSV 成功
### 表单功能
- [ ] 新增:填写必填项 → 提交 → 列表出现新行
- [ ] 编辑:弹窗加载详情 → 修改 → 提交 → 列表更新
- [ ] 必填校验:name/ownerType/platform 未填时拦截
- [ ] JSON 校验:certificates/extra 非法格式拦截
- [ ] 敏感字段:appSecret/appKey/aesKey 留空不修改
### 交互
- [ ] 启停:状态 Switch 切换正常,失败时回滚
- [ ] 单删:删除确认 → 行消失
- [ ] 批删:选中多行 → 批量删除 → 全部消失
- [ ] 弹窗:宽度 760px4 Tab 可切换
### 菜单与导航
- [ ] 侧边栏「系统管理 → 应用集成」菜单显示
- [ ] 路由跳转正常
- [ ] 中英文 i18n 切换正常
### 脱敏
- [ ] 列表中无任何明文密钥字段
- [ ] 列表接口返回的 records 不含 appSecret/appKey/aesKey 明文
---
## 关联信息
- **Spec 文档**: `docs/superpowers/specs/2026-06-07-sysapp-management-design.md`
- **工单**: rui/rui-frontend#4
- **后端 Issue**: rui/rui-framework#4(文件上传接口依赖,本期不阻塞)
- **参考实现**: `admin-ui/src/views/system/oauth2-client/Index.vue` + `OAuth2ClientFormDialog.vue`
---
**计划状态**: 待评审
**下一步**: 用户评审通过后,使用 `superpowers-subagent-driven-development``superpowers-executing-plans` 执行