Files
000-platform/frontend/src/views/apps/index.vue
Admin 830361073b
All checks were successful
continuous-integration/drone/push Build is passing
fix: 恢复应用管理的配置项定义功能
定时任务提交(104487f)误删了应用 config_schema 相关代码,现恢复:
- backend/app/models/app.py: 恢复 config_schema 数据库字段
- backend/app/routers/apps.py: 恢复 ConfigSchemaItem、API 路由、格式化函数
- frontend/src/views/apps/index.vue: 恢复配置项编辑 UI
2026-01-29 17:45:43 +08:00

412 lines
13 KiB
Vue
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.
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Plus } from '@element-plus/icons-vue'
import api from '@/api'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const query = reactive({
page: 1,
size: 20
})
// 配置项类型选项
const configTypes = [
{ value: 'text', label: '文本输入' },
{ value: 'radio', label: '单选' },
{ value: 'select', label: '下拉选择' },
{ value: 'switch', label: '开关' }
]
// 对话框
const dialogVisible = ref(false)
const dialogTitle = ref('')
const editingId = ref(null)
const formRef = ref(null)
const form = reactive({
app_code: '',
app_name: '',
base_url: '',
description: '',
require_jssdk: false,
config_schema: [] // 配置项定义
})
const rules = {
app_code: [{ required: true, message: '请输入应用代码', trigger: 'blur' }],
app_name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }]
}
async function fetchList() {
loading.value = true
try {
const res = await api.get('/api/apps', { params: query })
tableData.value = res.data.items || []
total.value = res.data.total || 0
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
function handleSearch() {
query.page = 1
fetchList()
}
function handlePageChange(page) {
query.page = page
fetchList()
}
function handleCreate() {
editingId.value = null
dialogTitle.value = '新建应用'
Object.assign(form, {
app_code: '',
app_name: '',
base_url: '',
description: '',
require_jssdk: false,
config_schema: []
})
dialogVisible.value = true
}
function handleEdit(row) {
editingId.value = row.id
dialogTitle.value = '编辑应用'
Object.assign(form, {
app_code: row.app_code,
app_name: row.app_name,
base_url: row.base_url || '',
description: row.description || '',
require_jssdk: row.require_jssdk || false,
config_schema: row.config_schema ? row.config_schema.map(c => ({
...c,
options: c.options || [],
option_labels: c.option_labels || {}
})) : []
})
dialogVisible.value = true
}
// 配置项管理
function addConfigItem() {
form.config_schema.push({
key: '',
label: '',
type: 'text',
options: [],
option_labels: {},
default: '',
placeholder: '',
required: false
})
}
function removeConfigItem(index) {
form.config_schema.splice(index, 1)
}
// 选项管理radio/select 类型)
function addOption(config) {
const optionKey = `option_${config.options.length + 1}`
config.options.push(optionKey)
config.option_labels[optionKey] = ''
}
function removeOption(config, index) {
const optionKey = config.options[index]
config.options.splice(index, 1)
delete config.option_labels[optionKey]
}
function updateOptionKey(config, index, newKey) {
const oldKey = config.options[index]
const oldLabel = config.option_labels[oldKey]
delete config.option_labels[oldKey]
config.options[index] = newKey
config.option_labels[newKey] = oldLabel || ''
}
async function handleSubmit() {
await formRef.value.validate()
const data = { ...form }
try {
if (editingId.value) {
await api.put(`/api/apps/${editingId.value}`, data)
ElMessage.success('更新成功')
} else {
await api.post('/api/apps', data)
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchList()
} catch (e) {
// 错误已在拦截器处理
}
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除应用 "${row.app_name}" 吗?`, '提示', {
type: 'warning'
})
try {
await api.delete(`/api/apps/${row.id}`)
ElMessage.success('删除成功')
fetchList()
} catch (e) {
// 错误已在拦截器处理
}
}
async function handleToggleStatus(row) {
const newStatus = row.status === 1 ? 0 : 1
try {
await api.put(`/api/apps/${row.id}`, { status: newStatus })
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
fetchList()
} catch (e) {
// 错误已在拦截器处理
}
}
onMounted(() => {
fetchList()
})
</script>
<template>
<div class="page-container">
<div class="page-header">
<div class="title">应用管理</div>
<el-button v-if="authStore.isOperator" type="primary" @click="handleCreate">
<el-icon><Plus /></el-icon>
新建应用
</el-button>
</div>
<div class="page-tip">
<el-alert type="info" :closable="false">
应用管理每个应用是一个独立的服务有独立的访问地址
租户订阅应用后平台生成 Token 供应用鉴权使用
</el-alert>
</div>
<!-- 表格 -->
<el-table v-loading="loading" :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="app_code" label="应用代码" width="150" />
<el-table-column prop="app_name" label="应用名称" width="180" />
<el-table-column prop="base_url" label="访问地址" min-width="300" show-overflow-tooltip />
<el-table-column label="配置项" width="90">
<template #default="{ row }">
<el-tag v-if="row.config_schema && row.config_schema.length > 0" type="primary" size="small">
{{ row.config_schema.length }}
</el-tag>
<span v-else style="color: #909399; font-size: 12px">-</span>
</template>
</el-table-column>
<el-table-column label="JS-SDK" width="90">
<template #default="{ row }">
<el-tag :type="row.require_jssdk ? 'warning' : 'info'" size="small">
{{ row.require_jssdk ? '需要' : '不需要' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button v-if="authStore.isOperator" type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="authStore.isOperator" :type="row.status === 1 ? 'warning' : 'success'" link size="small" @click="handleToggleStatus(row)">
{{ row.status === 1 ? '禁用' : '启用' }}
</el-button>
<el-button v-if="authStore.isOperator" type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div style="margin-top: 20px; display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="query.page"
:page-size="query.size"
:total="total"
layout="total, prev, pager, next"
@current-change="handlePageChange"
/>
</div>
<!-- 编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="应用代码" prop="app_code">
<el-input v-model="form.app_code" :disabled="!!editingId" placeholder="唯一标识,如: brainstorm" />
</el-form-item>
<el-form-item label="应用名称" prop="app_name">
<el-input v-model="form.app_name" placeholder="显示名称,如: 头脑风暴" />
</el-form-item>
<el-form-item label="访问地址">
<el-input v-model="form.base_url" placeholder="如: https://brainstorm.example.com" />
<div style="color: #909399; font-size: 12px; margin-top: 4px">
应用的访问地址用于生成链接和跳转
</div>
</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="企微JS-SDK">
<el-switch v-model="form.require_jssdk" />
<span style="margin-left: 12px; color: #909399; font-size: 12px">开启后租户需关联企微应用</span>
</el-form-item>
<!-- 配置项定义 -->
<el-divider content-position="left">配置项定义</el-divider>
<div class="config-schema-section">
<div class="config-schema-tip">
定义租户订阅时可配置的参数如行业类型提示词等
</div>
<div v-for="(config, index) in form.config_schema" :key="index" class="config-schema-item">
<div class="config-header">
<span class="config-index">#{{ index + 1 }}</span>
<el-button type="danger" :icon="Delete" circle size="small" @click="removeConfigItem(index)" />
</div>
<div class="config-row">
<el-input v-model="config.key" placeholder="配置键(如:industry)" style="width: 140px" />
<el-input v-model="config.label" placeholder="显示标签(如:行业类型)" style="width: 160px" />
<el-select v-model="config.type" placeholder="类型" style="width: 120px">
<el-option v-for="t in configTypes" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
<el-checkbox v-model="config.required">必填</el-checkbox>
</div>
<!-- text 类型显示 placeholder -->
<div v-if="config.type === 'text'" class="config-row" style="margin-top: 8px">
<el-input v-model="config.placeholder" placeholder="输入提示文字" style="width: 300px" />
<el-input v-model="config.default" placeholder="默认值" style="width: 200px" />
</div>
<!-- switch 类型显示默认值 -->
<div v-if="config.type === 'switch'" class="config-row" style="margin-top: 8px">
<span style="color: #606266; margin-right: 8px">默认值</span>
<el-switch v-model="config.default" active-value="true" inactive-value="false" />
</div>
<!-- radio/select 类型显示选项编辑 -->
<div v-if="config.type === 'radio' || config.type === 'select'" class="config-options">
<div class="options-label">选项列表</div>
<div v-for="(opt, optIndex) in config.options" :key="optIndex" class="option-row">
<el-input
:model-value="opt"
@update:model-value="v => updateOptionKey(config, optIndex, v)"
placeholder="选项值(:medical)"
style="width: 140px"
/>
<el-input
v-model="config.option_labels[opt]"
placeholder="显示名(:医美)"
style="width: 140px"
/>
<el-radio v-model="config.default" :value="opt">默认</el-radio>
<el-button type="danger" :icon="Delete" circle size="small" @click="removeOption(config, optIndex)" />
</div>
<el-button type="primary" plain size="small" @click="addOption(config)">
<el-icon><Plus /></el-icon> 添加选项
</el-button>
</div>
</div>
<el-button type="primary" plain @click="addConfigItem" style="margin-top: 12px">
<el-icon><Plus /></el-icon> 添加配置项
</el-button>
</div>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.page-tip {
margin-bottom: 16px;
}
/* 配置项定义样式 */
.config-schema-section {
padding: 0 10px;
}
.config-schema-tip {
color: #909399;
font-size: 13px;
margin-bottom: 12px;
}
.config-schema-item {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.config-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.config-index {
font-weight: 600;
color: #409eff;
}
.config-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.config-options {
margin-top: 10px;
padding: 10px;
background: #fff;
border-radius: 6px;
}
.options-label {
font-size: 13px;
color: #606266;
margin-bottom: 8px;
}
.option-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
</style>