Files
wecom_it_smart_desk/frontend-admin/src/views/Integrations.vue
T

700 lines
23 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.
<!--
=============================================================================
企微IT智能服务台 外部系统集成配置页
=============================================================================
说明显示6个外部系统的连接状态和配置入口
- Dify AI / RAGFlow可配置API URL + Key url_key 模式
- 火绒安全可配置AccessKey ID + Secret + Base URL access_key 模式
- 联软LV7000可配置API账号 + 密码 + Base URL account_password 模式
- 数据平台 / 北森 eHR显示状态 + 占位
-->
<template>
<div class="integrations-page">
<!-- 页面标题 -->
<div class="page-title">外部系统集成</div>
<div class="page-desc">管理后台管配置和参数Dify 网页管Workflow 逻辑两者边界明确</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-state">
<el-skeleton :rows="6" animated />
</div>
<!-- 集成系统卡片网格 -->
<div v-else class="integration-grid">
<IntegrationCard
v-for="item in integrations"
:key="item.id"
:integration="item"
:icon="getIntegrationIcon(item.id)"
:icon-bg="getIntegrationIconBg(item.id)"
:icon-color="getIntegrationIconColor(item.id)"
@configure="handleConfigure"
@test="handleTest"
@view="handleView"
/>
</div>
<!-- ================================================================ -->
<!-- 配置对话框 url_key 模式Dify / RAGFlow -->
<!-- ================================================================ -->
<el-dialog
v-if="configuringIntegration?.config_type === 'url_key'"
v-model="configDialogVisible"
:title="'配置集成 — ' + configuringIntegration?.name"
width="480px"
destroy-on-close
>
<el-form label-position="top">
<el-form-item label="API URL">
<el-input
v-model="urlKeyForm.apiUrl"
placeholder="https://api.dify.ai/v1"
/>
</el-form-item>
<el-form-item label="API Key">
<el-input
v-model="urlKeyForm.apiKey"
type="password"
placeholder="输入 API Key"
show-password
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="configDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleUrlKeySave">保存配置</el-button>
</template>
</el-dialog>
<!-- ================================================================ -->
<!-- 配置对话框 access_key 模式火绒安全 -->
<!-- ================================================================ -->
<el-dialog
v-if="configuringIntegration?.config_type === 'access_key'"
v-model="configDialogVisible"
:title="'配置集成 — ' + configuringIntegration?.name"
width="520px"
destroy-on-close
>
<el-form label-position="top">
<el-form-item label="Base URL(内网地址)">
<el-input
v-model="accessKeyForm.baseUrl"
placeholder="http://huorong.oa.servyou-it.com:8080"
/>
<div class="form-hint">火绒终端安全管理系统的内网API地址</div>
</el-form-item>
<el-form-item label="AccessKey ID">
<el-input
v-model="accessKeyForm.accessKeyId"
placeholder="输入 AccessKey ID"
/>
<div class="form-hint">在火绒管理后台 系统设置 API管理 中创建</div>
</el-form-item>
<el-form-item label="AccessKey Secret">
<el-input
v-model="accessKeyForm.accessKeySecret"
type="password"
placeholder="输入 AccessKey Secret"
show-password
/>
<div class="form-hint">Secret 仅在创建时显示一次请妥善保管</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="configDialogVisible = false">取消</el-button>
<el-button
:loading="testingConnection"
@click="handleTestHuorongConnection"
>
{{ testingConnection ? '测试中...' : '测试连接' }}
</el-button>
<el-button type="primary" @click="handleAccessKeySave">保存配置</el-button>
</template>
</el-dialog>
<!-- ================================================================ -->
<!-- 配置对话框 account_password 模式联软LV7000 -->
<!-- ================================================================ -->
<el-dialog
v-if="configuringIntegration?.config_type === 'account_password'"
v-model="configDialogVisible"
:title="'配置集成 — ' + configuringIntegration?.name"
width="520px"
destroy-on-close
>
<el-form label-position="top">
<el-form-item label="Base URL(内网地址)">
<el-input
v-model="accountPasswordForm.baseUrl"
placeholder="http://192.168.x.x:30098"
/>
<div class="form-hint">联软LV7000终端安全管理系统的内网API地址默认端口30098</div>
</el-form-item>
<el-form-item label="API 账号">
<el-input
v-model="accountPasswordForm.apiAccount"
placeholder="输入API账号"
/>
<div class="form-hint">在联软管理后台 系统设置 API管理 中创建</div>
</el-form-item>
<el-form-item label="API 密码">
<el-input
v-model="accountPasswordForm.apiPassword"
type="password"
placeholder="输入API密码"
show-password
/>
<div class="form-hint">与API账号配套的密码仅在创建时显示</div>
</el-form-item>
<el-form-item label="验证密钥(可选)">
<el-input
v-model="accountPasswordForm.validateKey"
placeholder="输入验证密钥(如需要)"
/>
<div class="form-hint">部分联软版本需要额外的验证密钥无则留空</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="configDialogVisible = false">取消</el-button>
<el-button
:loading="testingConnection"
@click="handleTestLianruanConnection"
>
{{ testingConnection ? '测试中...' : '测试连接' }}
</el-button>
<el-button type="primary" @click="handleAccountPasswordSave">保存配置</el-button>
</template>
</el-dialog>
<!-- ================================================================ -->
<!-- 联软连接测试结果对话框 -->
<!-- ================================================================ -->
<el-dialog
v-model="testResultVisible"
title="连接测试结果"
width="420px"
>
<div v-if="testResult" class="test-result">
<el-result
:icon="testResult.success ? 'success' : 'error'"
:title="testResult.success ? '连接成功' : '连接失败'"
:sub-title="testResult.message"
/>
<div v-if="testResult.success && testResult.total_terminals" class="test-detail">
检测到 <strong>{{ testResult.total_terminals }}</strong> 个终端
</div>
<!-- 失败时显示调试提示 -->
<div v-if="!testResult.success && testResult.debug_hint" class="test-debug-hint">
<el-icon><WarningFilled /></el-icon>
<span>{{ testResult.debug_hint }}</span>
</div>
<!-- 显示凭据摘要 -->
<div v-if="testResult.debug" class="test-debug-info">
<div>Base URL: {{ testResult.debug.base_url }}</div>
<div>AccessKey ID: {{ testResult.debug.access_key_id }}</div>
<div>Secret 长度: {{ testResult.debug.key_length }} 字符</div>
</div>
</div>
<template #footer>
<el-button type="primary" @click="testResultVisible = false">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
// ==========================================================================
// 依赖导入
// ==========================================================================
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { WarningFilled } from '@element-plus/icons-vue'
import IntegrationCard from '@/components/IntegrationCard.vue'
import { getIntegrations, updateIntegration, testHuorongConnection, testLianruanConnection, testRagflowConnection } from '@/api/admin'
import type { Integration } from '@/types'
// ==========================================================================
// 状态
// ==========================================================================
const loading = ref<boolean>(false)
const integrations = ref<Integration[]>([])
// ==========================================================================
// 配置对话框 — 通用状态
// ==========================================================================
const configDialogVisible = ref<boolean>(false)
const configuringIntegration = ref<Integration | null>(null)
// url_key 模式表单(Dify / RAGFlow
const urlKeyForm = reactive({
apiUrl: '',
apiKey: '',
})
// access_key 模式表单(火绒安全)
const accessKeyForm = reactive({
baseUrl: '',
accessKeyId: '',
accessKeySecret: '',
})
// account_password 模式表单(联软LV7000
const accountPasswordForm = reactive({
baseUrl: '',
apiAccount: '',
apiPassword: '',
validateKey: '',
})
// 火绒连接测试
const testingConnection = ref<boolean>(false)
const testResultVisible = ref<boolean>(false)
const testResult = ref<{ success: boolean; message: string; total_terminals?: number; debug_hint?: string; debug?: Record<string, unknown> } | null>(null)
// ==========================================================================
// 初始化
// ==========================================================================
onMounted(async () => {
loading.value = true
try {
const response = await getIntegrations()
integrations.value = response.data.data.items
} catch {
// 使用默认 demo 数据
integrations.value = getDefaultIntegrations()
} finally {
loading.value = false
}
})
// ==========================================================================
// 默认集成数据(API 不可用时的 fallback
// ==========================================================================
function getDefaultIntegrations(): Integration[] {
return [
{
id: 'dify',
name: 'Dify AI',
status: 'connected',
configurable: true,
config_type: 'url_key',
config: { api_url: 'https://api.dify.ai/v1', api_key_set: true },
},
{
id: 'ragflow',
name: 'RAGFlow',
status: 'partial',
configurable: true,
config_type: 'url_key',
config: { api_url: '', api_key_set: false },
},
{
id: 'data_platform',
name: '数据平台',
status: 'disconnected',
configurable: false,
config: null,
},
{
id: 'beisen',
name: '北森 eHR',
status: 'disconnected',
configurable: false,
config: null,
},
{
id: 'huorong',
name: '火绒安全',
status: 'disconnected',
configurable: true,
config_type: 'access_key',
config: { api_url: '', api_key_set: false, access_key_id_set: false, access_key_secret_set: false, base_url: null },
},
{
id: 'lianruan',
name: '联软LV7000',
status: 'disconnected',
configurable: true,
config_type: 'account_password',
config: { api_url: '', api_account_set: false, api_password_set: false, base_url: null },
},
]
}
// ==========================================================================
// 集成系统图标映射
// ==========================================================================
const iconMap: Record<string, { icon: string; bg: string; color: string }> = {
dify: { icon: 'Cpu', bg: 'var(--accent-light)', color: 'var(--accent)' },
ragflow: { icon: 'Coin', bg: 'var(--success-bg)', color: 'var(--success)' },
data_platform: { icon: 'TrendCharts', bg: 'var(--warning-bg)', color: 'var(--warning)' },
beisen: { icon: 'Avatar', bg: 'rgba(139,92,246,0.12)', color: '#8b5cf6' },
huorong: { icon: 'WarningFilled', bg: 'var(--danger-bg)', color: 'var(--danger)' },
lianruan: { icon: 'Monitor', bg: 'rgba(236,72,153,0.12)', color: '#ec4899' },
}
function getIntegrationIcon(id: string): string {
return iconMap[id]?.icon || 'Setting'
}
function getIntegrationIconBg(id: string): string {
return iconMap[id]?.bg || 'var(--accent-light)'
}
function getIntegrationIconColor(id: string): string {
return iconMap[id]?.color || 'var(--accent)'
}
// ==========================================================================
// 操作处理
// ==========================================================================
/** 打开配置对话框(根据 config_type 显示不同表单) */
function handleConfigure(integration: Integration): void {
configuringIntegration.value = integration
const config = integration.config as Record<string, unknown> | null
if (integration.config_type === 'access_key') {
// 火绒模式:填充 access_key 表单
accessKeyForm.baseUrl = (config?.base_url as string) || (config?.api_url as string) || ''
accessKeyForm.accessKeyId = ''
accessKeyForm.accessKeySecret = ''
} else if (integration.config_type === 'account_password') {
// 联软模式:填充 account_password 表单
accountPasswordForm.baseUrl = (config?.base_url as string) || (config?.api_url as string) || ''
accountPasswordForm.apiAccount = ''
accountPasswordForm.apiPassword = ''
accountPasswordForm.validateKey = ''
} else {
// 默认 url_key 模式(Dify / RAGFlow
urlKeyForm.apiUrl = (config?.api_url as string) || ''
urlKeyForm.apiKey = ''
}
configDialogVisible.value = true
}
/** 保存 url_key 模式配置(Dify / RAGFlow */
async function handleUrlKeySave(): Promise<void> {
if (!configuringIntegration.value) return
try {
await updateIntegration(configuringIntegration.value.id, {
api_url: urlKeyForm.apiUrl,
api_key: urlKeyForm.apiKey || undefined,
})
ElMessage.success('配置已保存')
// 更新本地数据
const idx = integrations.value.findIndex((i) => i.id === configuringIntegration.value!.id)
if (idx >= 0) {
integrations.value[idx] = {
...integrations.value[idx],
status: urlKeyForm.apiUrl ? 'connected' : 'partial',
config: {
api_url: urlKeyForm.apiUrl,
api_key_set: !!urlKeyForm.apiKey,
},
}
}
configDialogVisible.value = false
} catch {
ElMessage.error('保存配置失败')
}
}
/** 保存 access_key 模式配置(火绒安全) */
async function handleAccessKeySave(): Promise<void> {
if (!configuringIntegration.value) return
// 校验必填项
if (!accessKeyForm.baseUrl) {
ElMessage.warning('请填写 Base URL')
return
}
if (!accessKeyForm.accessKeyId) {
ElMessage.warning('请填写 AccessKey ID')
return
}
if (!accessKeyForm.accessKeySecret) {
ElMessage.warning('请填写 AccessKey Secret')
return
}
try {
await updateIntegration(configuringIntegration.value.id, {
access_key_id: accessKeyForm.accessKeyId,
access_key_secret: accessKeyForm.accessKeySecret,
base_url: accessKeyForm.baseUrl,
})
ElMessage.success('配置已保存')
// 更新本地数据(标记为已配置,但不一定连接成功)
const idx = integrations.value.findIndex((i) => i.id === configuringIntegration.value!.id)
if (idx >= 0) {
integrations.value[idx] = {
...integrations.value[idx],
status: 'partial', // 需要测试连接才能确认 connected
config: {
api_url: accessKeyForm.baseUrl,
api_key_set: true,
access_key_id_set: true,
access_key_secret_set: true,
base_url: accessKeyForm.baseUrl,
},
}
}
configDialogVisible.value = false
// 提示用户测试连接
ElMessage.info('请在集成管理页点击"测试连接"验证凭据是否正确')
} catch {
ElMessage.error('保存配置失败')
}
}
/** 保存 account_password 模式配置(联软LV7000 */
async function handleAccountPasswordSave(): Promise<void> {
if (!configuringIntegration.value) return
// 校验必填项
if (!accountPasswordForm.baseUrl) {
ElMessage.warning('请填写 Base URL')
return
}
if (!accountPasswordForm.apiAccount) {
ElMessage.warning('请填写 API 账号')
return
}
if (!accountPasswordForm.apiPassword) {
ElMessage.warning('请填写 API 密码')
return
}
try {
await updateIntegration(configuringIntegration.value.id, {
api_account: accountPasswordForm.apiAccount,
api_password: accountPasswordForm.apiPassword,
base_url: accountPasswordForm.baseUrl,
validate_key: accountPasswordForm.validateKey || undefined,
})
ElMessage.success('配置已保存')
// 更新本地数据
const idx = integrations.value.findIndex((i) => i.id === configuringIntegration.value!.id)
if (idx >= 0) {
integrations.value[idx] = {
...integrations.value[idx],
status: 'connected',
config: {
api_url: accountPasswordForm.baseUrl,
api_account_set: true,
api_password_set: true,
base_url: accountPasswordForm.baseUrl,
},
}
}
configDialogVisible.value = false
} catch {
ElMessage.error('保存配置失败')
}
}
/** 测试联软API连接 */
async function handleTestLianruanConnection(): Promise<void> {
// 先保存配置再测试,确保使用最新配置
if (!accountPasswordForm.baseUrl || !accountPasswordForm.apiAccount || !accountPasswordForm.apiPassword) {
ElMessage.warning('请先填写完整配置')
return
}
testingConnection.value = true
try {
// 先保存配置
await updateIntegration(configuringIntegration.value!.id, {
api_account: accountPasswordForm.apiAccount,
api_password: accountPasswordForm.apiPassword,
base_url: accountPasswordForm.baseUrl,
validate_key: accountPasswordForm.validateKey || undefined,
})
// 再测试连接
const response = await testLianruanConnection()
testResult.value = response.data.data
testResultVisible.value = true
// 如果测试成功,更新本地状态
if (testResult.value?.success) {
const idx = integrations.value.findIndex((i) => i.id === configuringIntegration.value!.id)
if (idx >= 0) {
integrations.value[idx] = {
...integrations.value[idx],
status: 'connected',
config: {
api_url: accountPasswordForm.baseUrl,
api_account_set: true,
api_password_set: true,
base_url: accountPasswordForm.baseUrl,
},
}
}
}
} catch {
testResult.value = { success: false, message: '请求失败,请检查网络连接' }
testResultVisible.value = true
} finally {
testingConnection.value = false
}
}
/** 测试火绒API连接 */
async function handleTestHuorongConnection(): Promise<void> {
// 先保存配置再测试,确保使用最新配置
if (!accessKeyForm.baseUrl || !accessKeyForm.accessKeyId || !accessKeyForm.accessKeySecret) {
ElMessage.warning('请先填写完整配置')
return
}
testingConnection.value = true
try {
// 先保存配置
await updateIntegration(configuringIntegration.value!.id, {
access_key_id: accessKeyForm.accessKeyId,
access_key_secret: accessKeyForm.accessKeySecret,
base_url: accessKeyForm.baseUrl,
})
// 再测试连接
const response = await testHuorongConnection()
testResult.value = response.data.data
testResultVisible.value = true
// 如果测试成功,更新本地状态
if (testResult.value?.success) {
const idx = integrations.value.findIndex((i) => i.id === configuringIntegration.value!.id)
if (idx >= 0) {
integrations.value[idx] = {
...integrations.value[idx],
status: 'connected',
config: {
api_url: accessKeyForm.baseUrl,
api_key_set: true,
access_key_id_set: true,
access_key_secret_set: true,
base_url: accessKeyForm.baseUrl,
},
}
}
}
} catch {
testResult.value = { success: false, message: '请求失败,请检查网络连接' }
testResultVisible.value = true
} finally {
testingConnection.value = false
}
}
/** 测试连接(通用,非火绒/联软) */
async function handleTest(integration: Integration): Promise<void> {
if (integration.id === 'huorong' || integration.id === 'lianruan') {
// 火绒/联软的测试在配置对话框中完成
return
}
if (integration.id === 'ragflow') {
// RAGFlow 测试连接
testingConnection.value = true
try {
const response = await testRagflowConnection()
testResult.value = response.data.data
testResultVisible.value = true
// 如果测试成功,更新本地状态
if (testResult.value?.success) {
const idx = integrations.value.findIndex((i) => i.id === integration.id)
if (idx >= 0) {
integrations.value[idx] = {
...integrations.value[idx],
status: 'connected',
}
}
}
} catch {
testResult.value = { success: false, message: '请求失败,请检查网络连接' }
testResultVisible.value = true
} finally {
testingConnection.value = false
}
return
}
ElMessage.info(`正在测试 ${integration.name} 连接...`)
}
/** 查看详情 */
function handleView(integration: Integration): void {
ElMessage.info(`${integration.name} — 暂无更多配置信息`)
}
</script>
<style scoped>
/* 页面 */
.integrations-page {
/* 样式使用全局 .integration-grid */
}
/* 加载状态 */
.loading-state {
padding: 24px 0;
}
/* 表单提示 */
.form-hint {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
line-height: 1.4;
}
/* 测试结果 */
.test-result {
text-align: center;
}
.test-detail {
margin-top: 8px;
font-size: 14px;
color: var(--text-secondary);
}
/* 调试提示 */
.test-debug-hint {
display: flex;
align-items: flex-start;
gap: 6px;
margin-top: 12px;
padding: 10px 12px;
background: rgba(245, 158, 11, 0.08);
border: 1px solid rgba(245, 158, 11, 0.2);
border-radius: 6px;
font-size: 12px;
color: #d97706;
text-align: left;
line-height: 1.5;
}
.test-debug-info {
margin-top: 8px;
padding: 8px 12px;
background: var(--bg-tertiary);
border-radius: 6px;
font-size: 11px;
color: var(--text-muted);
text-align: left;
font-family: monospace;
line-height: 1.6;
}
</style>