pangu-user-platform/docs/05-模块技术方案/应用管理-前端技术方案.md

1229 lines
37 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.

# 盘古用户平台 - 应用管理模块前端技术方案
---
| 文档信息 | 内容 |
|---------|------|
| **文档版本** | V1.0 |
| **模块名称** | 应用管理模块 - 前端 |
| **编写团队 | pangu |
| **创建日期** | 2026-01-31 |
| **审核状态** | 待评审 |
---
## 1. 技术栈
| 技术 | 版本 | 用途 |
|------|------|------|
| Vue | 3.x | 前端框架 |
| Element Plus | 2.x | UI组件库 |
| Axios | 1.x | HTTP客户端 |
| Pinia | 2.x | 状态管理 |
| Vite | 5.x | 构建工具 |
---
## 2. 目录结构
```
frontend/src/
├── api/
│ └── application.js # 应用管理API接口封装
├── views/
│ └── application/
│ ├── index.vue # 应用列表页(主页面)
│ └── components/
│ ├── AppDialog.vue # 新增/编辑弹窗组件
│ └── SecretDialog.vue # 密钥展示弹窗组件
├── mock/
│ └── application.js # Mock数据开发阶段使用
└── router/
└── index.js # 路由配置(添加应用管理路由)
```
---
## 3. 页面组件设计
### 3.1 应用列表页 (index.vue)
#### 3.1.1 页面功能
- 应用列表展示(分页)
- 按应用名称、编码、状态筛选
- 新增应用入口
- 编辑应用入口
- 重置密钥操作
- 删除应用操作
#### 3.1.2 页面布局
```
┌─────────────────────────────────────────────────────────────────────┐
│ 搜索区域el-card
│ ┌────────────────┐ ┌────────────────┐ ┌──────────┐ ┌────┐ ┌────┐ │
│ │ 应用名称 │ │ 应用编码 │ │ 状态 ▼ │ │搜索│ │重置│ │
│ └────────────────┘ └────────────────┘ └──────────┘ └────┘ └────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 操作区域 │
│ [+ 新增] │
├─────────────────────────────────────────────────────────────────────┤
│ 表格区域el-table
│ ┌───────┬──────────┬─────────────────────┬────────┬────────┬──────┐│
│ │应用名称│ 应用编码 │ 授权接口 │ 状态 │ 创建时间│ 操作 ││
│ ├───────┼──────────┼─────────────────────┼────────┼────────┼──────┤│
│ │AI智慧 │ YY000001 │ [学校][年级][班级] │ ● 正常 │01-01 │ 编辑 ││
│ │平台 │ │ │ │10:00 │ 重置 ││
│ │ │ │ │ │ │ 删除 ││
│ ├───────┼──────────┼─────────────────────┼────────┼────────┼──────┤│
│ │在线课 │ YY000002 │ [学生][会员]+2 │ ● 停用 │01-02 │ 编辑 ││
│ │堂系统 │ │ │ │14:30 │ 重置 ││
│ │ │ │ │ │ │ 删除 ││
│ └───────┴──────────┴─────────────────────┴────────┴────────┴──────┘│
├─────────────────────────────────────────────────────────────────────┤
│ 分页区域 │
│ 共20条 [<] 1 2 3 ... [>] │
└─────────────────────────────────────────────────────────────────────┘
```
#### 3.1.3 组件状态
```javascript
// 查询参数
const queryParams = ref({
pageNum: 1,
pageSize: 10,
appName: '', // 应用名称
appCode: '', // 应用编码
status: '' // 状态:空=全部0=正常1=停用
})
// 列表数据
const loading = ref(false) // 加载状态
const tableData = ref([]) // 表格数据
const total = ref(0) // 总记录数
// 子组件引用
const appDialogRef = ref() // 新增/编辑弹窗
const secretDialogRef = ref() // 密钥弹窗
```
#### 3.1.4 核心方法
| 方法名 | 功能说明 | 调用时机 |
|-------|---------|---------|
| `getList` | 获取应用列表 | 页面加载、搜索、分页 |
| `handleQuery` | 搜索查询 | 点击搜索按钮 |
| `resetQuery` | 重置查询 | 点击重置按钮 |
| `handleAdd` | 新增应用 | 点击新增按钮 |
| `handleEdit` | 编辑应用 | 点击编辑按钮 |
| `handleResetSecret` | 重置密钥 | 点击重置密钥按钮 |
| `handleDelete` | 删除应用 | 点击删除按钮 |
#### 3.1.5 完整代码
```vue
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-wrapper">
<el-form :model="queryParams" :inline="true">
<el-form-item label="应用名称">
<el-input
v-model="queryParams.appName"
placeholder="请输入应用名称"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="应用编码">
<el-input
v-model="queryParams.appCode"
placeholder="请输入应用编码"
clearable
style="width: 150px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width: 100px">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
<el-button :icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 表格区域 -->
<el-card shadow="never" style="margin-top: 12px">
<el-row :gutter="10" style="margin-bottom: 12px">
<el-col :span="1.5">
<el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
</el-row>
<el-table
v-loading="loading"
:data="tableData"
border
stripe
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
>
<el-table-column prop="appName" label="应用名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="appCode" label="应用编码" width="120" />
<el-table-column prop="apis" label="授权接口" min-width="250">
<template #default="{ row }">
<template v-if="row.apis?.length">
<el-tag
v-for="api in row.apis.slice(0, 3)"
:key="api.apiCode"
size="small"
style="margin-right: 4px; margin-bottom: 2px"
>
{{ api.apiName }}
</el-tag>
<el-tag v-if="row.apis.length > 3" size="small" type="info">
+{{ row.apis.length - 3 }}
</el-tag>
</template>
<span v-else style="color: #909399">未授权</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
{{ row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column prop="createBy" label="创建人" width="100" />
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
<el-button type="warning" link :icon="Key" @click="handleResetSecret(row)">重置密钥</el-button>
<el-popconfirm
title="确定要删除该应用吗?"
confirm-button-text="确定"
cancel-button-text="取消"
@confirm="handleDelete(row)"
>
<template #reference>
<el-button type="danger" link :icon="Delete">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 16px; justify-content: flex-end"
@size-change="getList"
@current-change="getList"
/>
</el-card>
<!-- 弹窗组件 -->
<AppDialog ref="appDialogRef" @success="getList" />
<SecretDialog ref="secretDialogRef" />
</div>
</template>
<script setup>
/**
* 应用管理列表页
* @author pangu
*/
import { listApplication, deleteApplication, resetAppSecret } from '@/api/application'
import { Delete, Edit, Key, Plus, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref } from 'vue'
import AppDialog from './components/AppDialog.vue'
import SecretDialog from './components/SecretDialog.vue'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const queryParams = ref({
pageNum: 1,
pageSize: 10,
appName: '',
appCode: '',
status: ''
})
const appDialogRef = ref()
const secretDialogRef = ref()
/**
* 获取应用列表
*/
const getList = async () => {
loading.value = true
try {
const res = await listApplication(queryParams.value)
if (res.code === 200) {
tableData.value = res.rows
total.value = res.total
}
} finally {
loading.value = false
}
}
/**
* 搜索查询
*/
const handleQuery = () => {
queryParams.value.pageNum = 1
getList()
}
/**
* 重置查询条件
*/
const resetQuery = () => {
queryParams.value = {
pageNum: 1,
pageSize: 10,
appName: '',
appCode: '',
status: ''
}
getList()
}
/**
* 新增应用
*/
const handleAdd = () => {
appDialogRef.value?.open()
}
/**
* 编辑应用
*/
const handleEdit = (row) => {
appDialogRef.value?.open(row)
}
/**
* 重置密钥
*/
const handleResetSecret = (row) => {
ElMessageBox.confirm(
`确定要重置应用"${row.appName}"的密钥吗?重置后旧密钥将立即失效。`,
'重置密钥',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
const res = await resetAppSecret(row.appId)
if (res.code === 200) {
ElMessage.success('密钥重置成功')
// 打开密钥展示弹窗
secretDialogRef.value?.open({
appName: row.appName,
appCode: row.appCode,
appSecret: res.data.appSecret
})
}
}).catch(() => {})
}
/**
* 删除应用
*/
const handleDelete = async (row) => {
const res = await deleteApplication(row.appId)
if (res.code === 200) {
ElMessage.success('删除成功')
getList()
}
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.app-container {
padding: 16px;
}
.search-wrapper {
margin-bottom: 0;
}
</style>
```
---
### 3.2 新增/编辑弹窗 (AppDialog.vue)
#### 3.2.1 组件功能
- 新增应用表单
- 编辑应用表单
- 接口授权勾选
- 表单验证
#### 3.2.2 表单布局
```
┌─────────────────────────────────────────────────────────────────┐
│ 新增应用 / 编辑应用 [X] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 应用名称* [________________________] 必填最大100字符
│ │
│ 应用编码 [保存后自动生成___________] (只读,系统生成) │
│ │
│ 应用描述 ┌────────────────────────┐ │
│ │ │ 选填最大500字符
│ │ │ │
│ └────────────────────────┘ │
│ │
│ 联系人 [________________________] (选填) │
│ │
│ 联系电话 [________________________] (选填,手机号格式) │
│ │
│ 状态 ◉ 正常 ○ 停用 │
│ │
│ 接口授权 ┌────────────────────────────────────────────┐ │
│ │ [✓] 查询学生信息 [✓] 查询学校信息 │ │
│ │ [✓] 查询年级信息 [ ] 查询班级信息 │ │
│ │ [ ] 查询会员信息 [ ] 查询区域树 │ │
│ └────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ [取消] [确定] │
└─────────────────────────────────────────────────────────────────┘
```
#### 3.2.3 组件状态
```javascript
// 弹窗状态
const visible = ref(false)
const submitLoading = ref(false)
const isEdit = ref(false)
// 表单数据
const form = reactive({
appId: null,
appName: '',
appCode: '',
appDesc: '',
contactPerson: '',
contactPhone: '',
status: '0',
apiCodes: []
})
// 接口授权选项
const apiOptions = ref([])
```
#### 3.2.4 表单验证规则
```javascript
const rules = {
appName: [
{ required: true, message: '请输入应用名称', trigger: 'blur' },
{ max: 100, message: '应用名称不能超过100个字符', trigger: 'blur' }
],
contactPhone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
appDesc: [
{ max: 500, message: '应用描述不能超过500个字符', trigger: 'blur' }
]
}
```
#### 3.2.5 完整代码
```vue
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="650px"
:close-on-click-modal="false"
destroy-on-close
@closed="handleClosed"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
>
<el-form-item label="应用名称" prop="appName">
<el-input
v-model="form.appName"
placeholder="请输入应用名称"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="应用编码">
<el-input
v-model="form.appCode"
placeholder="保存后自动生成"
disabled
/>
</el-form-item>
<el-form-item label="应用描述" prop="appDesc">
<el-input
v-model="form.appDesc"
type="textarea"
placeholder="请输入应用描述"
:rows="3"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="联系人" prop="contactPerson">
<el-input
v-model="form.contactPerson"
placeholder="请输入联系人"
maxlength="50"
/>
</el-form-item>
<el-form-item label="联系电话" prop="contactPhone">
<el-input
v-model="form.contactPhone"
placeholder="请输入联系电话"
maxlength="11"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio value="0">正常</el-radio>
<el-radio value="1">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="接口授权" prop="apiCodes">
<el-checkbox-group v-model="form.apiCodes">
<el-checkbox
v-for="item in apiOptions"
:key="item.apiCode"
:value="item.apiCode"
style="width: 180px; margin-right: 0;"
>
{{ item.apiName }}
</el-checkbox>
</el-checkbox-group>
<div class="api-tips">
<el-text type="info" size="small">
勾选后该应用可调用对应的开放API接口
</el-text>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup>
/**
* 应用管理 - 新增/编辑弹窗
* @author pangu
*/
import { addApplication, getApiList, getApplication, updateApplication } from '@/api/application'
import { ElMessage } from 'element-plus'
import { computed, nextTick, reactive, ref } from 'vue'
const emit = defineEmits(['success'])
const visible = ref(false)
const submitLoading = ref(false)
const formRef = ref(null)
const isEdit = ref(false)
const apiOptions = ref([])
const form = reactive({
appId: null,
appName: '',
appCode: '',
appDesc: '',
contactPerson: '',
contactPhone: '',
status: '0',
apiCodes: []
})
const rules = {
appName: [
{ required: true, message: '请输入应用名称', trigger: 'blur' },
{ max: 100, message: '应用名称不能超过100个字符', trigger: 'blur' }
],
contactPhone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
appDesc: [
{ max: 500, message: '应用描述不能超过500个字符', trigger: 'blur' }
]
}
const dialogTitle = computed(() => isEdit.value ? '编辑应用' : '新增应用')
/**
* 加载接口授权选项
*/
const loadApiOptions = async () => {
try {
const res = await getApiList()
if (res.code === 200) {
apiOptions.value = res.data
}
} catch (error) {
console.error('加载接口授权选项失败:', error)
}
}
/**
* 加载应用详情
*/
const loadAppDetail = async (appId) => {
try {
const res = await getApplication(appId)
if (res.code === 200) {
const data = res.data
Object.assign(form, {
appId: data.appId,
appName: data.appName,
appCode: data.appCode,
appDesc: data.appDesc || '',
contactPerson: data.contactPerson || '',
contactPhone: data.contactPhone || '',
status: data.status || '0',
apiCodes: data.apis?.map(a => a.apiCode) || []
})
}
} catch (error) {
console.error('加载应用详情失败:', error)
}
}
/**
* 打开弹窗
* @param row 编辑时传入行数据,新增时不传
*/
const open = async (row = null) => {
visible.value = true
// 加载接口授权选项
await loadApiOptions()
if (row?.appId) {
isEdit.value = true
await loadAppDetail(row.appId)
} else {
isEdit.value = false
}
}
/**
* 提交表单
*/
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitLoading.value = true
const submitData = {
appId: form.appId,
appName: form.appName,
appDesc: form.appDesc,
contactPerson: form.contactPerson,
contactPhone: form.contactPhone,
status: form.status,
apiCodes: form.apiCodes
}
const api = isEdit.value ? updateApplication : addApplication
const res = await api(submitData)
if (res.code === 200) {
ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
visible.value = false
emit('success')
// 新增时可以显示密钥
// 根据业务需求决定是否在新增后显示密钥
} else {
ElMessage.error(res.msg || '操作失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('表单提交失败:', error)
}
} finally {
submitLoading.value = false
}
}
/**
* 关闭弹窗后重置表单
*/
const handleClosed = () => {
formRef.value?.resetFields()
Object.assign(form, {
appId: null,
appName: '',
appCode: '',
appDesc: '',
contactPerson: '',
contactPhone: '',
status: '0',
apiCodes: []
})
isEdit.value = false
}
defineExpose({ open })
</script>
<style scoped>
.api-tips {
margin-top: 8px;
}
</style>
```
---
### 3.3 密钥展示弹窗 (SecretDialog.vue)
#### 3.3.1 组件功能
- 展示应用密钥
- 提供复制功能
- 安全提示
#### 3.3.2 弹窗布局
```
┌─────────────────────────────────────────────────────────────────┐
│ 应用密钥 [X] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ⚠️ 请妥善保管密钥,密钥重置后旧密钥将立即失效 │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 应用名称 AI智慧平台 │
│ │
│ 应用编码 YY000001 │
│ │
│ 应用密钥 ┌────────────────────────────────┬──────┐ │
│ │ a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 │ 复制 │ │
│ └────────────────────────────────┴──────┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ [关闭] │
└─────────────────────────────────────────────────────────────────┘
```
#### 3.3.3 完整代码
```vue
<template>
<el-dialog
v-model="visible"
title="应用密钥"
width="550px"
:close-on-click-modal="false"
>
<el-alert
title="请妥善保管密钥,密钥重置后旧密钥将立即失效"
type="warning"
:closable="false"
show-icon
style="margin-bottom: 20px;"
/>
<el-descriptions :column="1" border>
<el-descriptions-item label="应用名称" label-class-name="desc-label">
{{ appInfo.appName }}
</el-descriptions-item>
<el-descriptions-item label="应用编码" label-class-name="desc-label">
{{ appInfo.appCode }}
</el-descriptions-item>
<el-descriptions-item label="应用密钥" label-class-name="desc-label">
<div class="secret-wrapper">
<el-input
v-model="appInfo.appSecret"
readonly
class="secret-input"
/>
<el-button type="primary" @click="handleCopy">
<el-icon style="margin-right: 4px;"><DocumentCopy /></el-icon>
复制
</el-button>
</div>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button type="primary" @click="visible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
/**
* 应用管理 - 密钥展示弹窗
* @author pangu
*/
import { DocumentCopy } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
const visible = ref(false)
const appInfo = reactive({
appName: '',
appCode: '',
appSecret: ''
})
/**
* 打开弹窗
* @param data 包含 appName, appCode, appSecret
*/
const open = (data) => {
visible.value = true
Object.assign(appInfo, {
appName: data.appName || '',
appCode: data.appCode || '',
appSecret: data.appSecret || ''
})
}
/**
* 复制密钥到剪贴板
*/
const handleCopy = async () => {
try {
// 优先使用现代API
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(appInfo.appSecret)
ElMessage.success('复制成功')
return
}
// 降级方案:使用 execCommand
const textarea = document.createElement('textarea')
textarea.value = appInfo.appSecret
textarea.style.position = 'fixed'
textarea.style.left = '-9999px'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
ElMessage.success('复制成功')
} catch (error) {
console.error('复制失败:', error)
ElMessage.error('复制失败,请手动复制')
}
}
defineExpose({ open })
</script>
<style scoped>
.secret-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.secret-input {
flex: 1;
}
:deep(.desc-label) {
width: 100px;
}
</style>
```
---
## 4. API接口封装
### 4.1 接口定义 (api/application.js)
```javascript
/**
* 应用管理API接口
* @author pangu
*/
import request from '@/utils/request'
/**
* 获取应用列表(分页)
* @param {Object} params 查询参数
* @param {string} params.appName 应用名称
* @param {string} params.appCode 应用编码
* @param {string} params.status 状态0正常1停用
* @param {number} params.pageNum 页码
* @param {number} params.pageSize 每页条数
*/
export function listApplication(params) {
return request({
url: '/api/application/list',
method: 'get',
params
})
}
/**
* 获取应用详情
* @param {number} appId 应用ID
*/
export function getApplication(appId) {
return request({
url: `/api/application/${appId}`,
method: 'get'
})
}
/**
* 新增应用
* @param {Object} data 应用数据
*/
export function addApplication(data) {
return request({
url: '/api/application',
method: 'post',
data
})
}
/**
* 修改应用
* @param {Object} data 应用数据
*/
export function updateApplication(data) {
return request({
url: '/api/application',
method: 'put',
data
})
}
/**
* 删除应用
* @param {number} appId 应用ID
*/
export function deleteApplication(appId) {
return request({
url: `/api/application/${appId}`,
method: 'delete'
})
}
/**
* 重置应用密钥
* @param {number} appId 应用ID
*/
export function resetAppSecret(appId) {
return request({
url: `/api/application/resetSecret/${appId}`,
method: 'put'
})
}
/**
* 获取API接口列表用于授权选择
*/
export function getApiList() {
return request({
url: '/api/application/apiList',
method: 'get'
})
}
```
---
## 5. Mock数据 (mock/application.js)
> 开发阶段使用,后端接口完成后移除
```javascript
/**
* 应用管理Mock数据
* @author pangu
*/
import Mock from 'mockjs'
// API接口字典
const apiDict = [
{ apiCode: 'STUDENT_LIST', apiName: '查询学生信息', apiPath: '/open/student/list' },
{ apiCode: 'SCHOOL_LIST', apiName: '查询学校信息', apiPath: '/open/school/list' },
{ apiCode: 'GRADE_LIST', apiName: '查询年级信息', apiPath: '/open/grade/list' },
{ apiCode: 'CLASS_LIST', apiName: '查询班级信息', apiPath: '/open/class/list' },
{ apiCode: 'MEMBER_LIST', apiName: '查询会员信息', apiPath: '/open/member/list' },
{ apiCode: 'REGION_TREE', apiName: '查询区域树', apiPath: '/open/region/tree' }
]
// 生成应用编码
let appSeq = 1
const generateAppCode = () => `YY${String(appSeq++).padStart(6, '0')}`
// 生成32位密钥
const generateSecret = () => Mock.Random.string('abcdefghijklmnopqrstuvwxyz0123456789', 32)
// 模拟应用数据
let applicationList = [
{
appId: 1,
appCode: 'YY000001',
appName: 'AI智慧平台',
appSecret: generateSecret(),
appDesc: 'AI智慧教育平台',
contactPerson: '张经理',
contactPhone: '13812345678',
status: '0',
apis: [
{ apiCode: 'SCHOOL_LIST', apiName: '查询学校信息', apiPath: '/open/school/list' },
{ apiCode: 'GRADE_LIST', apiName: '查询年级信息', apiPath: '/open/grade/list' },
{ apiCode: 'CLASS_LIST', apiName: '查询班级信息', apiPath: '/open/class/list' }
],
createTime: '2026-01-01 10:00:00',
createBy: 'admin'
}
]
// 获取应用列表
Mock.mock(/\/api\/application\/list/, 'get', (options) => {
const url = new URL('http://localhost' + options.url)
const appName = url.searchParams.get('appName') || ''
const appCode = url.searchParams.get('appCode') || ''
const status = url.searchParams.get('status') || ''
const pageNum = parseInt(url.searchParams.get('pageNum')) || 1
const pageSize = parseInt(url.searchParams.get('pageSize')) || 10
let filtered = applicationList.filter(item => {
let match = true
if (appName) match = match && item.appName.includes(appName)
if (appCode) match = match && item.appCode.includes(appCode)
if (status) match = match && item.status === status
return match
})
const total = filtered.length
const start = (pageNum - 1) * pageSize
const rows = filtered.slice(start, start + pageSize)
return { code: 200, msg: '查询成功', total, rows }
})
// 获取应用详情
Mock.mock(/\/api\/application\/\d+$/, 'get', (options) => {
const appId = parseInt(options.url.match(/\/api\/application\/(\d+)/)[1])
const app = applicationList.find(item => item.appId === appId)
if (app) {
return { code: 200, msg: '查询成功', data: app }
}
return { code: 500, msg: '应用不存在' }
})
// 新增应用
Mock.mock('/api/application', 'post', (options) => {
const data = JSON.parse(options.body)
const newApp = {
appId: applicationList.length + 1,
appCode: generateAppCode(),
appSecret: generateSecret(),
appName: data.appName,
appDesc: data.appDesc,
contactPerson: data.contactPerson,
contactPhone: data.contactPhone,
status: data.status || '0',
apis: apiDict.filter(api => data.apiCodes?.includes(api.apiCode)),
createTime: Mock.Random.now('yyyy-MM-dd HH:mm:ss'),
createBy: 'admin'
}
applicationList.unshift(newApp)
return {
code: 200,
msg: '新增成功',
data: { appCode: newApp.appCode, appSecret: newApp.appSecret }
}
})
// 修改应用
Mock.mock('/api/application', 'put', (options) => {
const data = JSON.parse(options.body)
const index = applicationList.findIndex(item => item.appId === data.appId)
if (index !== -1) {
applicationList[index] = {
...applicationList[index],
appName: data.appName,
appDesc: data.appDesc,
contactPerson: data.contactPerson,
contactPhone: data.contactPhone,
status: data.status,
apis: apiDict.filter(api => data.apiCodes?.includes(api.apiCode))
}
return { code: 200, msg: '修改成功' }
}
return { code: 500, msg: '应用不存在' }
})
// 删除应用
Mock.mock(/\/api\/application\/\d+$/, 'delete', (options) => {
const appId = parseInt(options.url.match(/\/api\/application\/(\d+)/)[1])
const index = applicationList.findIndex(item => item.appId === appId)
if (index !== -1) {
applicationList.splice(index, 1)
return { code: 200, msg: '删除成功' }
}
return { code: 500, msg: '应用不存在' }
})
// 重置密钥
Mock.mock(/\/api\/application\/resetSecret\/\d+/, 'put', (options) => {
const appId = parseInt(options.url.match(/\/api\/application\/resetSecret\/(\d+)/)[1])
const app = applicationList.find(item => item.appId === appId)
if (app) {
const newSecret = generateSecret()
app.appSecret = newSecret
return { code: 200, msg: '重置成功', data: { appSecret: newSecret } }
}
return { code: 500, msg: '应用不存在' }
})
// 获取API接口列表
Mock.mock('/api/application/apiList', 'get', () => {
return { code: 200, msg: '查询成功', data: apiDict }
})
```
---
## 6. 路由配置
`router/index.js` 中添加路由:
```javascript
{
path: '/application',
component: Layout,
children: [
{
path: '',
name: 'Application',
component: () => import('@/views/application/index.vue'),
meta: { title: '应用管理', icon: 'app', roles: ['admin'] }
}
]
}
```
---
## 7. 开发规范
### 7.1 命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 组件文件名 | 大驼峰 | `AppDialog.vue` |
| 组件名 | 大驼峰 | `AppDialog` |
| 变量名 | 小驼峰 | `tableData` |
| 常量名 | 大写下划线 | `API_BASE_URL` |
| 方法名 | 小驼峰事件用handle前缀 | `handleQuery` |
| CSS类名 | 短横线分隔 | `app-container` |
### 7.2 代码规范
- 使用 `<script setup>` 语法
- 使用 Composition API
- 每个组件文件必须有作者注释
- 复杂逻辑添加注释说明
- API请求统一封装不直接使用axios
### 7.3 交互规范
| 操作 | 交互方式 |
|------|---------|
| 列表加载 | 表格显示 loading 状态 |
| 表单提交 | 按钮显示 loading 状态 |
| 删除操作 | 使用 el-popconfirm 二次确认 |
| 危险操作 | 使用 ElMessageBox.confirm 确认 |
| 成功提示 | ElMessage.success |
| 错误提示 | ElMessage.error |
---
## 8. 开发检查清单
### 8.1 开发前检查
- [ ] 阅读需求文档和原型
- [ ] 确认接口文档
- [ ] 创建目录结构
### 8.2 开发中检查
- [ ] 列表页面开发完成
- [ ] 新增/编辑弹窗开发完成
- [ ] 密钥展示弹窗开发完成
- [ ] API接口封装完成
- [ ] Mock数据开发完成
- [ ] 表单验证完成
- [ ] 交互效果完成
### 8.3 开发后检查
- [ ] 代码格式化
- [ ] 删除console.log
- [ ] 删除无用代码
- [ ] 添加必要注释
- [ ] 自测功能正常
---
*文档结束*