1229 lines
37 KiB
Markdown
1229 lines
37 KiB
Markdown
# 盘古用户平台 - 应用管理模块前端技术方案
|
||
|
||
---
|
||
|
||
| 文档信息 | 内容 |
|
||
|---------|------|
|
||
| **文档版本** | 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
|
||
- [ ] 删除无用代码
|
||
- [ ] 添加必要注释
|
||
- [ ] 自测功能正常
|
||
|
||
---
|
||
|
||
*文档结束*
|