feat: 应用扁平化与 Token 验证 API
All checks were successful
continuous-integration/drone/push Build is passing

- 新增 /api/auth/verify 接口供外部应用验证 token
- 简化应用管理:移除 tools 字段,每个应用独立存在
- 简化应用配置:移除 allowed_tools,专注于租户订阅
- 优化 Token 展示和复制功能
This commit is contained in:
111
2026-01-24 10:05:24 +08:00
parent c4bd7c8251
commit 6a93e05ec3
3 changed files with 264 additions and 331 deletions

View File

@@ -1,8 +1,9 @@
"""认证路由"""
import hmac
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from typing import Optional
from typing import Optional, List
from sqlalchemy.orm import Session
from ..database import get_db
@@ -15,7 +16,10 @@ from ..services.auth import (
TokenData,
UserInfo
)
from ..services.crypto import decrypt_config
from ..models.user import User
from ..models.tenant_app import TenantApp
from ..models.tenant_wechat_app import TenantWechatApp
router = APIRouter(prefix="/auth", tags=["认证"])
security = HTTPBearer()
@@ -221,3 +225,114 @@ async def delete_user(
db.commit()
return {"success": True}
# ============ Token 验证 API供外部应用调用 ============
class VerifyTokenRequest(BaseModel):
"""Token 验证请求"""
token: str
app_code: Optional[str] = None # 可选,用于验证 token 是否属于特定应用
class WechatConfig(BaseModel):
"""企微配置"""
corp_id: Optional[str] = None
agent_id: Optional[str] = None
secret: Optional[str] = None
class VerifyTokenResponse(BaseModel):
"""Token 验证响应"""
valid: bool
tenant_id: Optional[str] = None
app_code: Optional[str] = None
wechat_config: Optional[WechatConfig] = None
error: Optional[str] = None
@router.post("/verify", response_model=VerifyTokenResponse)
async def verify_token(
request: VerifyTokenRequest,
db: Session = Depends(get_db)
):
"""
验证 Token 有效性(供外部应用调用,无需登录)
外部应用收到用户请求后,可调用此接口验证 token
1. 验证 token 是否存在且有效
2. 如传入 app_code验证 token 是否属于该应用
3. 返回租户信息和企微配置
Args:
token: 访问令牌
app_code: 应用代码(可选,用于验证 token 是否属于特定应用)
Returns:
valid: 是否有效
tenant_id: 租户ID
app_code: 应用代码
wechat_config: 企微配置(如有)
"""
if not request.token:
return VerifyTokenResponse(valid=False, error="Token 不能为空")
# 根据 token 查询租户应用配置
query = db.query(TenantApp).filter(
TenantApp.access_token == request.token,
TenantApp.status == 1
)
# 如果指定了 app_code验证 token 是否属于该应用
if request.app_code:
query = query.filter(TenantApp.app_code == request.app_code)
tenant_app = query.first()
if not tenant_app:
return VerifyTokenResponse(valid=False, error="Token 无效或已过期")
# 获取关联的企微配置
wechat_config = None
if tenant_app.wechat_app_id:
wechat_app = db.query(TenantWechatApp).filter(
TenantWechatApp.id == tenant_app.wechat_app_id,
TenantWechatApp.status == 1
).first()
if wechat_app:
# 解密 secret
secret = None
if wechat_app.secret_encrypted:
try:
secret = decrypt_config(wechat_app.secret_encrypted)
except:
pass
wechat_config = WechatConfig(
corp_id=wechat_app.corp_id,
agent_id=wechat_app.agent_id,
secret=secret
)
return VerifyTokenResponse(
valid=True,
tenant_id=tenant_app.tenant_id,
app_code=tenant_app.app_code,
wechat_config=wechat_config
)
@router.get("/verify")
async def verify_token_get(
token: str,
app_code: Optional[str] = None,
db: Session = Depends(get_db)
):
"""
验证 TokenGET 方式,便于简单测试)
"""
return await verify_token(
VerifyTokenRequest(token=token, app_code=app_code),
db
)

View File

@@ -18,11 +18,11 @@ const query = reactive({
// 应用列表(从应用管理获取)
const appList = ref([])
const appToolsMap = ref({}) // app_code -> tools[]
const appRequireJssdk = ref({}) // app_code -> require_jssdk
const appBaseUrl = ref({}) // app_code -> base_url
// 企微应用列表(按租户)
const wechatAppList = ref([]) // 当前表单租户的企微应用列表
const wechatAppList = ref([])
// 对话框
const dialogVisible = ref(false)
@@ -31,10 +31,9 @@ const editingId = ref(null)
const formRef = ref(null)
const form = reactive({
tenant_id: '',
app_code: 'tools',
app_code: '',
app_name: '',
wechat_app_id: null, // 关联的企微应用ID
allowed_tools: []
wechat_app_id: null
})
// 当前选择的应用是否需要 JS-SDK
@@ -42,34 +41,15 @@ const currentAppRequireJssdk = computed(() => {
return appRequireJssdk.value[form.app_code] || false
})
// 根据选择的应用获取工具选项
const toolOptions = computed(() => {
const tools = appToolsMap.value[form.app_code] || []
if (tools.length > 0) {
return tools.map(t => ({ label: t.name, value: t.code }))
}
// 默认工具列表(兼容旧数据)
return [
{ label: '高情商回复', value: 'high-eq' },
{ label: '头脑风暴', value: 'brainstorm' },
{ label: '面诊方案', value: 'consultation' },
{ label: '客户画像', value: 'customer-profile' },
{ label: '医疗合规', value: 'medical-compliance' }
]
})
const rules = {
tenant_id: [{ required: true, message: '请输入租户ID', trigger: 'blur' }],
app_code: [{ required: true, message: '请选择应用', trigger: 'change' }]
}
// 生成链接对话框
const urlDialogVisible = ref(false)
const urlLoading = ref(false)
const currentRow = ref(null)
const selectedTool = ref('')
const generatedUrl = ref('')
const urlInfo = ref({})
// 查看 Token 对话框
const tokenDialogVisible = ref(false)
const currentToken = ref('')
const currentAppUrl = ref('')
async function fetchApps() {
try {
@@ -77,15 +57,9 @@ async function fetchApps() {
const apps = res.data.items || []
appList.value = apps.map(a => ({ app_code: a.app_code, app_name: a.app_name }))
// 获取每个应用的工具列表和 JS-SDK 要求
for (const app of apps) {
appRequireJssdk.value[app.app_code] = app.require_jssdk || false
try {
const toolsRes = await api.get(`/api/apps/${app.app_code}/tools`)
appToolsMap.value[app.app_code] = toolsRes.data || []
} catch (e) {
appToolsMap.value[app.app_code] = []
}
appBaseUrl.value[app.app_code] = app.base_url || ''
}
} catch (e) {
console.error('获取应用列表失败:', e)
@@ -130,13 +104,12 @@ function handlePageChange(page) {
function handleCreate() {
editingId.value = null
dialogTitle.value = '新建应用配置'
dialogTitle.value = '新建应用订阅'
Object.assign(form, {
tenant_id: '',
app_code: 'tools',
app_code: '',
app_name: '',
wechat_app_id: null,
allowed_tools: []
wechat_app_id: null
})
wechatAppList.value = []
dialogVisible.value = true
@@ -144,20 +117,17 @@ function handleCreate() {
async function handleEdit(row) {
editingId.value = row.id
dialogTitle.value = '编辑应用配置'
dialogTitle.value = '编辑应用订阅'
Object.assign(form, {
tenant_id: row.tenant_id,
app_code: row.app_code,
app_name: row.app_name || '',
wechat_app_id: row.wechat_app_id || null,
allowed_tools: row.allowed_tools || []
wechat_app_id: row.wechat_app_id || null
})
// 获取该租户的企微应用列表
await fetchWechatApps(row.tenant_id)
dialogVisible.value = true
}
// 租户ID变化时重新获取企微应用列表
async function handleTenantChange() {
form.wechat_app_id = null
await fetchWechatApps(form.tenant_id)
@@ -174,7 +144,9 @@ async function handleSubmit() {
ElMessage.success('更新成功')
} else {
const res = await api.post('/api/tenant-apps', data)
ElMessage.success(`创建成功Access Token: ${res.data.access_token}`)
ElMessage.success(`创建成功`)
// 显示新生成的 token
showToken(res.data.access_token, form.app_code)
}
dialogVisible.value = false
fetchList()
@@ -184,7 +156,7 @@ async function handleSubmit() {
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除配置吗?`, '提示', {
await ElMessageBox.confirm(`确定删除${row.app_code}」的订阅配置吗?`, '提示', {
type: 'warning'
})
@@ -198,96 +170,43 @@ async function handleDelete(row) {
}
async function handleRegenerateToken(row) {
await ElMessageBox.confirm('重新生成 Access Token 将使旧的链接失效,确定继续?', '提示', {
await ElMessageBox.confirm('重新生成 Token 将使旧 Token 失效,确定继续?', '提示', {
type: 'warning'
})
try {
const res = await api.post(`/api/tenant-apps/${row.id}/regenerate-token`)
ElMessage.success(`新 Access Token: ${res.data.access_token}`)
showToken(res.data.access_token, row.app_code)
fetchList()
} catch (e) {
// 错误已在拦截器处理
}
}
// 生成链接功能
function handleShowUrl(row) {
currentRow.value = row
selectedTool.value = ''
generatedUrl.value = ''
urlInfo.value = {}
urlDialogVisible.value = true
function showToken(token, appCode) {
currentToken.value = token
currentAppUrl.value = appBaseUrl.value[appCode] || ''
tokenDialogVisible.value = true
}
async function handleGenerateUrl() {
if (!currentRow.value) return
urlLoading.value = true
try {
const res = await api.post('/api/apps/generate-url', {
tenant_id: currentRow.value.tenant_id,
app_code: currentRow.value.app_code,
tool_code: selectedTool.value || null
function handleCopyToken() {
navigator.clipboard.writeText(currentToken.value).then(() => {
ElMessage.success('Token 已复制')
})
if (res.data.success) {
generatedUrl.value = res.data.url
urlInfo.value = res.data
} else {
ElMessage.error(res.data.error || '生成失败')
}
} catch (e) {
console.error(e)
} finally {
urlLoading.value = false
}
}
function handleCopyUrl() {
if (!generatedUrl.value) return
navigator.clipboard.writeText(generatedUrl.value).then(() => {
ElMessage.success('链接已复制到剪贴板')
}).catch(() => {
// 降级方案
const input = document.createElement('input')
input.value = generatedUrl.value
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
ElMessage.success('链接已复制到剪贴板')
const url = `${currentAppUrl.value}?token=${currentToken.value}`
navigator.clipboard.writeText(url).then(() => {
ElMessage.success('链接已复制')
})
}
// 获取当前行可选的工具
const currentToolOptions = computed(() => {
if (!currentRow.value) return []
const appTools = appToolsMap.value[currentRow.value.app_code] || []
const allowedTools = currentRow.value.allowed_tools || []
if (appTools.length > 0) {
// 过滤出允许的工具
if (allowedTools.length > 0) {
return appTools.filter(t => allowedTools.includes(t.code)).map(t => ({ label: t.name, value: t.code }))
}
return appTools.map(t => ({ label: t.name, value: t.code }))
}
// 默认工具
const defaultTools = [
{ label: '高情商回复', value: 'high-eq' },
{ label: '头脑风暴', value: 'brainstorm' },
{ label: '面诊方案', value: 'consultation' },
{ label: '客户画像', value: 'customer-profile' },
{ label: '医疗合规', value: 'medical-compliance' }
]
if (allowedTools.length > 0) {
return defaultTools.filter(t => allowedTools.includes(t.value))
}
return defaultTools
})
async function handleViewToken(row) {
// 这里需要后端返回真实 token暂时用 placeholder
// 实际生产中可能需要单独 API 获取
showToken(row.access_token === '******' ? '需要调用API获取' : row.access_token, row.app_code)
}
onMounted(() => {
fetchApps()
@@ -298,13 +217,19 @@ onMounted(() => {
<template>
<div class="page-container">
<div class="page-header">
<div class="title">应用配置</div>
<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外部应用可通过 Token 向平台验证身份
</el-alert>
</div>
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
@@ -314,9 +239,8 @@ onMounted(() => {
style="width: 160px"
@keyup.enter="handleSearch"
/>
<el-select v-model="query.app_code" placeholder="应用" clearable style="width: 120px">
<el-option label="tools" value="tools" />
<el-option label="interview" value="interview" />
<el-select v-model="query.app_code" placeholder="应用" clearable style="width: 150px">
<el-option v-for="app in appList" :key="app.app_code" :label="app.app_name" :value="app.app_code" />
</el-select>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</div>
@@ -325,8 +249,8 @@ onMounted(() => {
<el-table v-loading="loading" :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="tenant_id" label="租户ID" width="120" />
<el-table-column prop="app_code" label="应用" width="100" />
<el-table-column prop="app_name" label="应用名称" width="150" />
<el-table-column prop="app_code" label="应用代码" width="150" />
<el-table-column prop="app_name" label="备注名称" width="150" />
<el-table-column label="企微应用" width="180">
<template #default="{ row }">
<template v-if="row.wechat_app">
@@ -335,25 +259,24 @@ onMounted(() => {
<el-tag v-else type="info" size="small">未关联</el-tag>
</template>
</el-table-column>
<el-table-column label="Access Token" width="120">
<el-table-column label="Token 状态" width="120">
<template #default="{ row }">
<el-tag v-if="row.access_token" type="success" size="small">配置</el-tag>
<el-tag v-else type="danger" size="small">配置</el-tag>
<el-tag v-if="row.access_token" type="success" size="small">生成</el-tag>
<el-tag v-else type="danger" size="small">生成</el-tag>
</template>
</el-table-column>
<el-table-column prop="allowed_tools" label="允许工具" min-width="150">
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag v-for="tool in (row.allowed_tools || []).slice(0, 3)" :key="tool" size="small" style="margin-right: 4px">
{{ tool }}
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
<span v-if="(row.allowed_tools || []).length > 3">...</span>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button type="success" link size="small" @click="handleShowUrl(row)">生成链接</el-button>
<el-button v-if="authStore.isOperator" type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="authStore.isOperator" type="info" link size="small" @click="handleRegenerateToken(row)">重置Token</el-button>
<el-button v-if="authStore.isOperator" type="success" link size="small" @click="handleViewToken(row)">查看Token</el-button>
<el-button v-if="authStore.isOperator" type="warning" link size="small" @click="handleRegenerateToken(row)">重置Token</el-button>
<el-button v-if="authStore.isOperator" type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
@@ -371,7 +294,7 @@ onMounted(() => {
</div>
<!-- 编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="租户ID" prop="tenant_id">
<el-input
@@ -382,18 +305,18 @@ onMounted(() => {
/>
</el-form-item>
<el-form-item label="应用" prop="app_code">
<el-select v-model="form.app_code" :disabled="!!editingId" placeholder="选择应用" style="width: 100%">
<el-option v-for="app in appList" :key="app.app_code" :label="app.app_name" :value="app.app_code" />
<el-option label="tools (默认)" value="tools" />
<el-select v-model="form.app_code" :disabled="!!editingId" placeholder="选择要订阅的应用" style="width: 100%">
<el-option v-for="app in appList" :key="app.app_code" :label="`${app.app_name} (${app.app_code})`" :value="app.app_code" />
</el-select>
</el-form-item>
<el-form-item label="配置名称">
<el-input v-model="form.app_name" placeholder="显示名称(可选)" />
<el-form-item label="备注名称">
<el-input v-model="form.app_name" placeholder="可选,用于区分同应用多配置" />
</el-form-item>
<el-divider v-if="currentAppRequireJssdk" content-position="left">企业微信关联</el-divider>
<template v-if="currentAppRequireJssdk">
<el-divider content-position="left">企业微信关联</el-divider>
<el-form-item v-if="currentAppRequireJssdk" label="关联企微应用">
<el-form-item label="关联企微应用">
<el-select
v-model="form.wechat_app_id"
placeholder="选择企微应用"
@@ -411,14 +334,7 @@ onMounted(() => {
该租户暂无企微应用请先在企微应用中配置
</div>
</el-form-item>
<el-divider content-position="left">权限配置</el-divider>
<el-form-item label="允许的工具">
<el-checkbox-group v-model="form.allowed_tools">
<el-checkbox v-for="opt in toolOptions" :key="opt.value" :label="opt.value">{{ opt.label }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
@@ -426,77 +342,67 @@ onMounted(() => {
</template>
</el-dialog>
<!-- 生成链接对话框 -->
<el-dialog v-model="urlDialogVisible" title="生成访问链接" width="650px">
<div v-if="currentRow" class="url-dialog-content">
<el-descriptions :column="2" border size="small" style="margin-bottom: 20px">
<el-descriptions-item label="租户ID">{{ currentRow.tenant_id }}</el-descriptions-item>
<el-descriptions-item label="应用">{{ currentRow.app_code }}</el-descriptions-item>
<el-descriptions-item label="Access Token">
<el-tag v-if="currentRow.access_token" type="success" size="small">已配置</el-tag>
<el-tag v-else type="danger" size="small">未配置</el-tag>
</el-descriptions-item>
<el-descriptions-item label="允许工具">
{{ (currentRow.allowed_tools || []).length > 0 ? currentRow.allowed_tools.join(', ') : '全部' }}
</el-descriptions-item>
</el-descriptions>
<!-- Token 显示对话框 -->
<el-dialog v-model="tokenDialogVisible" title="访问 Token" width="600px">
<div class="token-dialog-content">
<el-alert type="warning" :closable="false" style="margin-bottom: 16px">
请妥善保管 Token它是应用访问平台的凭证
</el-alert>
<el-form label-width="80px">
<el-form-item label="选择工具">
<el-select v-model="selectedTool" placeholder="选择工具(留空则生成首页链接)" clearable style="width: 100%">
<el-option v-for="opt in currentToolOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="urlLoading" @click="handleGenerateUrl">
生成链接
</el-button>
</el-form-item>
</el-form>
<div class="token-section">
<div class="token-label">Access Token:</div>
<el-input v-model="currentToken" readonly>
<template #append>
<el-button @click="handleCopyToken">复制</el-button>
</template>
</el-input>
</div>
<div v-if="generatedUrl" class="url-result">
<el-divider content-position="left">生成结果</el-divider>
<el-alert
type="success"
:title="urlInfo.note || '静态链接,长期有效'"
:closable="false"
style="margin-bottom: 12px"
/>
<div class="url-box">
<el-input
v-model="generatedUrl"
type="textarea"
:rows="3"
readonly
/>
<el-button type="primary" style="margin-top: 10px" @click="handleCopyUrl">
<div v-if="currentAppUrl" class="token-section">
<div class="token-label">完整访问链接:</div>
<el-input :model-value="`${currentAppUrl}?token=${currentToken}`" readonly type="textarea" :rows="2" />
<el-button type="primary" style="margin-top: 8px" @click="handleCopyUrl">
<el-icon><CopyDocument /></el-icon>
复制链接
</el-button>
</div>
<el-divider />
<div class="token-section">
<div class="token-label">验证 API:</div>
<el-input
model-value="POST /api/auth/verify"
readonly
/>
<div style="color: #909399; font-size: 12px; margin-top: 4px">
外部应用可调用此接口验证 Token 有效性获取租户和企微配置
</div>
</div>
</div>
<template #footer>
<el-button @click="urlDialogVisible = false">关闭</el-button>
<el-button @click="tokenDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.url-dialog-content {
.page-tip {
margin-bottom: 16px;
}
.token-dialog-content {
padding: 0 10px;
}
.url-result {
margin-top: 10px;
.token-section {
margin-bottom: 16px;
}
.url-box {
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
.token-label {
font-weight: 500;
margin-bottom: 8px;
color: #303133;
}
</style>

View File

@@ -24,8 +24,7 @@ const form = reactive({
app_name: '',
base_url: '',
description: '',
require_jssdk: false,
tools: []
require_jssdk: false
})
const rules = {
@@ -33,15 +32,6 @@ const rules = {
app_name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }]
}
// 工具编辑
const toolDialogVisible = ref(false)
const editingToolIndex = ref(-1)
const toolForm = reactive({
code: '',
name: '',
path: ''
})
async function fetchList() {
loading.value = true
try {
@@ -73,8 +63,7 @@ function handleCreate() {
app_name: '',
base_url: '',
description: '',
require_jssdk: false,
tools: []
require_jssdk: false
})
dialogVisible.value = true
}
@@ -87,8 +76,7 @@ function handleEdit(row) {
app_name: row.app_name,
base_url: row.base_url || '',
description: row.description || '',
require_jssdk: row.require_jssdk || false,
tools: row.tools || []
require_jssdk: row.require_jssdk || false
})
dialogVisible.value = true
}
@@ -138,39 +126,6 @@ async function handleToggleStatus(row) {
}
}
// 工具管理
function handleAddTool() {
editingToolIndex.value = -1
Object.assign(toolForm, { code: '', name: '', path: '' })
toolDialogVisible.value = true
}
function handleEditTool(index) {
editingToolIndex.value = index
const tool = form.tools[index]
Object.assign(toolForm, { ...tool })
toolDialogVisible.value = true
}
function handleDeleteTool(index) {
form.tools.splice(index, 1)
}
function handleSaveTool() {
if (!toolForm.code || !toolForm.name) {
ElMessage.warning('请填写工具代码和名称')
return
}
const tool = { ...toolForm }
if (editingToolIndex.value >= 0) {
form.tools[editingToolIndex.value] = tool
} else {
form.tools.push(tool)
}
toolDialogVisible.value = false
}
onMounted(() => {
fetchList()
})
@@ -188,22 +143,17 @@ onMounted(() => {
<div class="page-tip">
<el-alert type="info" :closable="false">
应用管理定义可供租户使用的应用配置应用的基础URL和工具列表
租户配置中选择应用后即可生成带签名的访问链接
应用管理每个应用是一个独立的服务有独立的访问地址
租户订阅应用后平台生成 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="120" />
<el-table-column prop="app_name" label="应用名称" width="150" />
<el-table-column prop="base_url" label="基础URL" min-width="250" show-overflow-tooltip />
<el-table-column label="工具数量" width="100">
<template #default="{ row }">
<el-tag size="small">{{ (row.tools || []).length }} </el-tag>
</template>
</el-table-column>
<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="JS-SDK" width="90">
<template #default="{ row }">
<el-tag :type="row.require_jssdk ? 'warning' : 'info'" size="small">
@@ -241,45 +191,26 @@ onMounted(() => {
</div>
<!-- 编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px">
<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="唯一标识,如: tools" />
<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-input v-model="form.app_name" placeholder="显示名称,如: 头脑风暴" />
</el-form-item>
<el-form-item label="基础URL">
<el-input v-model="form.base_url" placeholder="如: https://tools.test.ai.ireborn.com.cn" />
<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>
<el-form-item label="工具">
<div style="width: 100%">
<el-table :data="form.tools" size="small" border style="margin-bottom: 10px">
<el-table-column prop="code" label="代码" width="120" />
<el-table-column prop="name" label="名称" width="120" />
<el-table-column prop="path" label="路径" />
<el-table-column label="操作" width="120">
<template #default="{ $index }">
<el-button type="primary" link size="small" @click="handleEditTool($index)">编辑</el-button>
<el-button type="danger" link size="small" @click="handleDeleteTool($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-button type="primary" size="small" @click="handleAddTool">
<el-icon><Plus /></el-icon>
添加工具
</el-button>
</div>
<span style="margin-left: 12px; color: #909399; font-size: 12px">开启后租户需关联企微应用</span>
</el-form-item>
</el-form>
<template #footer>
@@ -287,25 +218,6 @@ onMounted(() => {
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
<!-- 工具编辑对话框 -->
<el-dialog v-model="toolDialogVisible" :title="editingToolIndex >= 0 ? '编辑工具' : '添加工具'" width="400px">
<el-form :model="toolForm" label-width="80px">
<el-form-item label="代码">
<el-input v-model="toolForm.code" placeholder="如: brainstorm" />
</el-form-item>
<el-form-item label="名称">
<el-input v-model="toolForm.name" placeholder="如: 头脑风暴" />
</el-form-item>
<el-form-item label="路径">
<el-input v-model="toolForm.path" placeholder="如: /brainstorm" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="toolDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveTool">确定</el-button>
</template>
</el-dialog>
</div>
</template>