Files
rui-docs/superpowers/plans/2026-06-07-sysapp-management-plan.md
T

962 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
- [x] **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: 无错误输出 ✅
- [x] **Step 3: Commit** (commit `67d6686`)
```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`
- [x] **Step 1: 追加导出语句** (commit `0b4b02f`)
在文件末尾追加:
```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`
- [x] **Step 1: 在 zh-CN.ts 添加中文** (commit `98741a0`)
定位到 `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`
- [x] **Step 1: 在 M 常量加键** (commit `e961bc5`)
定位到 `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` 执行