chore: initial baseline with P0-safety .gitignore

This commit is contained in:
Simon
2026-06-14 16:49:18 +08:00
commit 63262292d7
510 changed files with 146008 additions and 0 deletions
+699
View File
@@ -0,0 +1,699 @@
<!--
=============================================================================
企微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>