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:
@@ -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 1(Service)、Task 3(i18n)、Task 4(Router)必须先完成。
|
||||
|
||||
- [ ] **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 1(Service)必须先完成。
|
||||
|
||||
- [ ] **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="第三方平台应用ID(UNIQUE)" />
|
||||
</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 数组。格式:[{name, content}]。待后端文件上传接口就绪后升级为文件上传。
|
||||
</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 切换正常,失败时回滚
|
||||
- [ ] 单删:删除确认 → 行消失
|
||||
- [ ] 批删:选中多行 → 批量删除 → 全部消失
|
||||
- [ ] 弹窗:宽度 760px,4 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` 执行
|
||||
Reference in New Issue
Block a user