fastbee/yack-web/src/views/platform/components/InterfaceDrawer.vue
2026-06-06 16:35:49 +08:00

522 lines
24 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.

<template>
<el-drawer
:title="`接口管理 - ${platformName}`"
:visible.sync="visible"
direction="rtl"
size="78%"
:before-close="handleClose"
append-to-body
>
<div class="interface-drawer-content">
<!-- 接口列表 -->
<div v-show="!editing" class="interface-list">
<el-row :gutter="10" style="margin-bottom:12px">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增接口</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="list" size="small" border>
<el-table-column label="排序" prop="sortOrder" width="60" align="center" />
<el-table-column label="接口名称" prop="name" min-width="140" />
<el-table-column label="标识" prop="code" width="160" show-overflow-tooltip />
<el-table-column label="方法" prop="method" width="80" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="methodTagType(scope.row.method)">{{ scope.row.method }}</el-tag>
</template>
</el-table-column>
<el-table-column label="路径" prop="path" min-width="220" show-overflow-tooltip />
<el-table-column label="Content-Type" prop="contentType" width="180" show-overflow-tooltip />
<el-table-column label="入参数量" width="90" align="center">
<template slot-scope="scope">{{ schemaCount(scope.row.requestSchema) }}</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 编辑表单 -->
<div v-show="editing" class="interface-edit">
<div class="edit-header">
<el-button size="mini" icon="el-icon-back" @click="cancelEdit">返回列表</el-button>
<span class="edit-title">{{ editTitle }}</span>
</div>
<el-form ref="ifaceForm" :model="form" :rules="rules" label-width="100px" style="margin-top:16px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="接口名称" prop="name">
<el-input v-model="form.name" placeholder="如:推送倾角数据" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="接口标识" prop="code">
<el-input v-model="form.code" placeholder="如:dcp_push_inclino" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="请求方法" prop="method">
<el-select v-model="form.method" style="width:100%">
<el-option v-for="m in methods" :key="m" :label="m" :value="m" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="路径" prop="path">
<el-input v-model="form.path" placeholder="/api/app/inclino-data/many,支持 {变量名} 路径参数" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="Content-Type">
<el-select v-model="form.contentType" style="width:100%" clearable>
<el-option label="application/json" value="application/json" />
<el-option label="application/x-www-form-urlencoded" value="application/x-www-form-urlencoded" />
<el-option label="multipart/form-data" value="multipart/form-data" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="排序">
<el-input-number v-model="form.sortOrder" :min="0" :max="999" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="备注">
<el-input v-model="form.remark" placeholder="接口用途说明" />
</el-form-item>
</el-col>
</el-row>
<!-- 入参 Schema 可视化表格 -->
<el-divider content-position="left">入参定义(request_schema)</el-divider>
<div class="schema-tip">
定义接口需要的入参字段。参数来源使用业务化名称:
固定值、认证参数、平台参数、上一步结果、当前设备、自动UUID、子设备传感器列表、基准点成对传感器列表、设备物模型、上报时间。
</div>
<el-table :data="requestSchemaRows" size="mini" border style="width:100%;margin-top:8px">
<el-table-column label="字段名" width="130">
<template slot-scope="scope">
<el-input v-model="scope.row.name" size="mini" placeholder="如 deviceId" />
</template>
</el-table-column>
<el-table-column label="类型" width="110">
<template slot-scope="scope">
<el-select v-model="scope.row.type" size="mini" style="width:100%">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="boolean" value="boolean" />
<el-option label="object" value="object" />
<el-option label="array" value="array" />
</el-select>
</template>
</el-table-column>
<el-table-column label="参数来源" width="150">
<template slot-scope="scope">
<el-select v-model="scope.row.source" size="mini" style="width:100%" @change="onSourceChange(scope.row)">
<el-option label="固定值" value="fixed" />
<el-option label="认证参数" value="auth" />
<el-option label="平台参数" value="extra" />
<el-option label="上一步结果" value="context" />
<el-option label="当前设备" value="device" />
<el-option label="自动UUID" value="uuid" />
<el-option label="子设备传感器列表" value="subDeviceSensorIds" />
<el-option label="基准点成对传感器列表" value="referenceSubDeviceSensorIdPairs" />
<el-option label="设备物模型" value="thingsModel" />
<el-option label="上报时间" value="ts" />
</el-select>
</template>
</el-table-column>
<el-table-column label="取值配置" min-width="180">
<template slot-scope="scope">
<!-- fixed: 直接填值 -->
<el-input v-if="scope.row.source === 'fixed'" v-model="scope.row.value" size="mini" placeholder="固定值" />
<!-- auth: 选择认证配置中的字段 -->
<el-select v-else-if="scope.row.source === 'auth' && authFieldOptions.length > 0" v-model="scope.row.field" size="mini" filterable allow-create default-first-option style="width:100%">
<el-option v-for="item in authFieldOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-input v-else-if="scope.row.source === 'auth'" v-model="scope.row.field" size="mini" placeholder="认证参数名,如 userName" />
<!-- extra: 选择平台参数中的字段 -->
<el-select v-else-if="scope.row.source === 'extra' && extraFieldOptions.length > 0" v-model="scope.row.field" size="mini" filterable allow-create default-first-option style="width:100%">
<el-option v-for="item in extraFieldOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-input v-else-if="scope.row.source === 'extra'" v-model="scope.row.field" size="mini" placeholder="平台参数名,如 projectId" />
<!-- context: 上一步结果的key -->
<el-input v-else-if="scope.row.source === 'context'" v-model="scope.row.contextKey" size="mini" placeholder="流程页可选择,如 step1.id" />
<!-- device: 设备字段 -->
<el-select v-else-if="scope.row.source === 'device'" v-model="scope.row.field" size="mini" style="width:100%">
<el-option label="平台设备ID" value="platform_device_id" />
<el-option label="平台测点ID" value="platform_point_id" />
<el-option label="系统设备编号" value="serial_number" />
</el-select>
<span v-else-if="scope.row.source === 'uuid'" class="schema-hint">每次请求自动生成UUID</span>
<span v-else-if="scope.row.source === 'subDeviceSensorIds'" class="schema-hint">自动取设备绑定页子设备映射中的 sensorId 列表</span>
<span v-else-if="scope.row.source === 'referenceSubDeviceSensorIdPairs'" class="schema-hint">按基准点与每个监测点成对拆分请求</span>
<span v-else-if="scope.row.source === 'thingsModel'" class="schema-hint">设备绑定页面选择物模型字段</span>
<span v-else-if="scope.row.source === 'ts'" class="schema-hint">系统自动取设备上报时间</span>
<span v-else class="schema-hint">请先选择来源</span>
</template>
</el-table-column>
<el-table-column label="必填" width="55" align="center">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.required" />
</template>
</el-table-column>
<el-table-column label="说明" width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.desc" size="mini" placeholder="字段说明" />
</template>
</el-table-column>
<el-table-column label="数组项定义" width="100" align="center">
<template slot-scope="scope">
<el-button v-if="scope.row.type === 'array'" size="mini" type="text" icon="el-icon-s-operation" @click="openItemSchemaDialog(scope.row)">数组项</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="requestSchemaRows.splice(scope.$index, 1)" />
</template>
</el-table-column>
</el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="addRequestField">添加入参字段</el-button>
<!-- 数组项 itemSchema 编辑弹窗 -->
<el-dialog :visible.sync="itemSchemaDialogVisible" title="定义数组项字段" width="650px" append-to-body :close-on-click-modal="false">
<div class="schema-tip" style="margin-bottom:8px">
定义数组中每个元素的字段。可选来源:设备物模型、子设备映射、上报时间、固定值。
</div>
<el-table :data="itemSchemaRows" size="mini" border style="width:100%">
<el-table-column label="字段名" width="130">
<template slot-scope="scope">
<el-input v-model="scope.row.name" size="mini" placeholder="如 height" />
</template>
</el-table-column>
<el-table-column label="类型" width="100">
<template slot-scope="scope">
<el-select v-model="scope.row.type" size="mini" style="width:100%">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="boolean" value="boolean" />
</el-select>
</template>
</el-table-column>
<el-table-column label="参数来源" width="150">
<template slot-scope="scope">
<el-select v-model="scope.row.source" size="mini" style="width:100%">
<el-option label="设备物模型" value="thingsModel" />
<el-option label="子设备映射" value="subDeviceId" />
<el-option label="上报时间" value="ts" />
<el-option label="固定值" value="fixed" />
</el-select>
</template>
</el-table-column>
<el-table-column label="固定值" width="120">
<template slot-scope="scope">
<el-input v-if="scope.row.source === 'fixed'" v-model="scope.row.value" size="mini" placeholder="固定值" />
</template>
</el-table-column>
<el-table-column label="说明" width="120">
<template slot-scope="scope">
<el-input v-model="scope.row.desc" size="mini" placeholder="说明" />
</template>
</el-table-column>
<el-table-column label="操作" width="50" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="itemSchemaRows.splice(scope.$index, 1)" />
</template>
</el-table-column>
</el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="addItemSchemaField">添加字段</el-button>
<div slot="footer">
<el-button @click="itemSchemaDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="confirmItemSchema">确定</el-button>
</div>
</el-dialog>
<!-- 出参 Schema 可视化表格 -->
<el-divider content-position="left">出参定义(response_schema)</el-divider>
<div class="schema-tip">
定义接口响应中需要提取的字段。<b>path</b> 为 JSON 路径(如 access_token、[0].id)
</div>
<el-table :data="responseSchemaRows" size="mini" border style="width:100%;margin-top:8px">
<el-table-column label="字段名" width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.name" size="mini" placeholder="如 access_token" />
</template>
</el-table-column>
<el-table-column label="类型" width="110">
<template slot-scope="scope">
<el-select v-model="scope.row.type" size="mini" style="width:100%">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="boolean" value="boolean" />
<el-option label="object" value="object" />
<el-option label="array" value="array" />
</el-select>
</template>
</el-table-column>
<el-table-column label="JSON路径(path)" min-width="180">
<template slot-scope="scope">
<el-input v-model="scope.row.path" size="mini" placeholder="如 access_token 或 [0].id" />
</template>
</el-table-column>
<el-table-column label="说明" min-width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.desc" size="mini" placeholder="字段说明" />
</template>
</el-table-column>
<el-table-column label="操作" width="50" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="responseSchemaRows.splice(scope.$index, 1)" />
</template>
</el-table-column>
</el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="addResponseField">添加出参字段</el-button>
</el-form>
<div class="edit-footer">
<el-button @click="cancelEdit">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading">保存</el-button>
</div>
</div>
</div>
</el-drawer>
</template>
<script>
import { listPlatformInterface, addPlatformInterface, updatePlatformInterface, delPlatformInterface } from '@/api/platform/interface'
import { getPlatformInstance } from '@/api/platform/instance'
export default {
name: 'PlatformInterfaceDrawer',
props: {
platformId: { type: Number, default: null },
platformName: { type: String, default: '' }
},
data() {
return {
visible: false,
loading: false,
submitLoading: false,
list: [],
editing: false,
editTitle: '新增接口',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
form: {},
authFieldOptions: [],
extraFieldOptions: [],
requestSchemaRows: [],
responseSchemaRows: [],
itemSchemaDialogVisible: false,
itemSchemaEditingRow: null,
itemSchemaRows: [],
rules: {
name: [{ required: true, message: '请输入接口名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入接口标识', trigger: 'blur' }],
method: [{ required: true, message: '请选择请求方法', trigger: 'change' }],
path: [{ required: true, message: '请输入接口路径', trigger: 'blur' }]
}
}
},
methods: {
open() {
this.visible = true
this.editing = false
this.loadPlatformOptions()
this.getList()
},
handleClose(done) {
this.editing = false
done()
},
getList() {
if (!this.platformId) return
this.loading = true
listPlatformInterface({ platformId: this.platformId, pageNum: 1, pageSize: 200 }).then(res => {
this.list = res.rows || []
this.loading = false
})
},
schemaCount(schema) {
if (!schema) return 0
try { return JSON.parse(schema).length } catch (e) { return 0 }
},
methodTagType(method) {
const map = { GET: '', POST: 'success', PUT: 'warning', DELETE: 'danger', PATCH: 'info' }
return map[method] || ''
},
handleAdd() {
this.form = {
platformId: this.platformId,
method: 'POST',
path: '',
contentType: 'application/json',
sortOrder: this.list.length + 1,
remark: ''
}
this.requestSchemaRows = []
this.responseSchemaRows = []
this.editTitle = '新增接口'
this.editing = true
},
handleEdit(row) {
this.form = { ...row }
// 解析 requestSchema JSON → 表格行
this.requestSchemaRows = this.parseSchema(row.requestSchema)
this.responseSchemaRows = this.parseSchema(row.responseSchema)
this.editTitle = '编辑接口 - ' + row.name
this.editing = true
},
parseSchema(json) {
if (!json) return []
try {
const arr = JSON.parse(json)
return arr.map(f => ({ ...f, itemSchema: f.itemSchema || [], required: !!f.required }))
} catch (e) { return [] }
},
handleDelete(row) {
this.$confirm(`确认删除接口【${row.name}】?`).then(() => {
delPlatformInterface(row.id).then(() => {
this.$modal.msgSuccess('删除成功')
this.getList()
})
})
},
cancelEdit() {
this.editing = false
},
onSourceChange(row) {
// 切换 source 时清空无关字段
row.value = undefined
row.field = undefined
row.contextKey = undefined
},
loadPlatformOptions() {
if (!this.platformId) {
this.authFieldOptions = []
this.extraFieldOptions = []
return
}
getPlatformInstance(this.platformId).then(res => {
const data = res.data || {}
this.authFieldOptions = this.extractJsonKeys(data.authConfig)
this.extraFieldOptions = this.extractJsonKeys(data.extraConfig)
}).catch(() => {
this.authFieldOptions = []
this.extraFieldOptions = []
})
},
extractJsonKeys(value) {
if (!value) return []
try {
return Object.keys(JSON.parse(value))
} catch (e) {
return []
}
},
addRequestField() {
this.requestSchemaRows.push({ name: '', type: 'string', source: 'fixed', value: '', field: '', contextKey: '', required: false, desc: '', itemSchema: [] })
},
addResponseField() {
this.responseSchemaRows.push({ name: '', type: 'string', path: '', desc: '' })
},
openItemSchemaDialog(row) {
this.itemSchemaEditingRow = row
this.itemSchemaRows = (row.itemSchema || []).map(s => ({ ...s }))
if (this.itemSchemaRows.length === 0) {
// 预填一个默认字段
this.itemSchemaRows.push({ name: '', type: 'string', source: 'thingsModel', value: '', desc: '' })
}
this.itemSchemaDialogVisible = true
},
addItemSchemaField() {
this.itemSchemaRows.push({ name: '', type: 'string', source: 'thingsModel', value: '', desc: '' })
},
confirmItemSchema() {
if (this.itemSchemaEditingRow) {
this.itemSchemaEditingRow.itemSchema = this.itemSchemaRows.filter(s => s.name).map(s => {
const item = { name: s.name, type: s.type || 'string', source: s.source || 'thingsModel', desc: s.desc }
if (s.source === 'fixed') item.value = s.value
return Object.assign({}, item)
})
}
this.itemSchemaDialogVisible = false
},
submitForm() {
this.$refs.ifaceForm.validate(valid => {
if (!valid) return
// 将表格行序列化为 JSON 存入 form
this.form.requestSchema = JSON.stringify(this.requestSchemaRows.filter(r => r.name).map(r => {
const field = { name: r.name, type: r.type, source: r.source, required: !!r.required, desc: r.desc }
if (r.type === 'array' && r.itemSchema && r.itemSchema.length > 0) {
field.itemSchema = r.itemSchema.filter(s => s.name).map(s => {
const item = { name: s.name, type: s.type || 'string', source: s.source || 'thingsModel', desc: s.desc }
if (s.source === 'fixed') item.value = s.value
return item
})
}
if (r.source === 'fixed') field.value = r.value
if (r.source === 'auth' || r.source === 'extra' || r.source === 'device') field.field = r.field
if (r.source === 'context') field.contextKey = r.contextKey
return field
}))
this.form.responseSchema = JSON.stringify(this.responseSchemaRows.filter(r => r.name).map(r => ({
name: r.name, type: r.type, path: r.path, desc: r.desc
})))
this.submitLoading = true
const api = this.form.id ? updatePlatformInterface : addPlatformInterface
api(this.form).then(() => {
this.$modal.msgSuccess(this.form.id ? '修改成功' : '新增成功')
this.editing = false
this.getList()
}).finally(() => { this.submitLoading = false })
})
}
}
}
</script>
<style scoped>
.interface-drawer-content {
padding: 16px 24px;
height: 100%;
overflow-y: auto;
}
.edit-header {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #ebeef5;
}
.edit-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
.edit-footer {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #ebeef5;
text-align: right;
}
.schema-tip {
font-size: 12px;
color: #909399;
line-height: 1.6;
margin-top: 4px;
}
.schema-hint {
font-size: 12px;
color: #c0c4cc;
}
</style>