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

37 KiB
Raw Blame History

盘古用户平台 - 应用管理模块前端技术方案


文档信息 内容
文档版本 V1.0
模块名称 应用管理模块 - 前端
编写团队 湖北新华业务中台研发团队
创建日期 2026-01-31
审核状态 待评审

1. 技术栈

技术 版本 用途
Vue 3.x 前端框架
Element Plus 2.x UI组件库
Axios 1.x HTTP客户端
Pinia 2.x 状态管理
Vite 5.x 构建工具

2. 目录结构

pangu-ui/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 组件状态

// 查询参数
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 完整代码

<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 湖北新华业务中台研发团队
 */
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 组件状态

// 弹窗状态
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 表单验证规则

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 完整代码

<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 湖北新华业务中台研发团队
 */
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 完整代码

<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 湖北新华业务中台研发团队
 */
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)

/**
 * 应用管理API接口
 * @author 湖北新华业务中台研发团队
 */
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)

开发阶段使用,后端接口完成后移除

/**
 * 应用管理Mock数据
 * @author 湖北新华业务中台研发团队
 */
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 中添加路由:

{
  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
  • 删除无用代码
  • 添加必要注释
  • 自测功能正常

文档结束