Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions ui/src/views/application-detail/access/AccessSettingDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ const isCertification = ref(false)
let savedConfig: ApplicationAccessConfig = {}

const isWechat = computed(() => props.platform.key === APPLICATION_ACCESS_PLATFORM.WECHAT)
const modeField = computed(() => props.platform.modeField)
// 表单字段随连接方式切换:长连接(ws)需要 bot_id/secret,URL 回调(callback)需要 token/encoding_aes_key。
const activeFields = computed(() => {
const mode = form[modeField.value?.key ?? ''] ?? modeField.value?.default
return (modeField.value && mode ? modeField.value.fields?.[mode] : undefined) ?? props.platform.fields
})
// 智能机器人默认长连接(ws),无需回调地址;切到 URL 回调时才展示回调地址。
const showCallback = computed(() => !modeField.value || form[modeField.value.key] === 'callback')
// 消息回调沿用 Admin API 下的 /chat 路径,不使用独立 Chat 入口的 API base。
const callbackUrl = computed(() => `${window.location.origin}${ADMIN_API_BASE_PATH}/chat/${props.platform.key}/${props.applicationId}`)
const rules = computed<FormRules>(() =>
Object.fromEntries(
props.platform.fields
activeFields.value
.filter((field) => field.required !== false)
.map((field) => [field.key, [{ required: true, whitespace: true, message: `请输入 ${field.label}`, trigger: 'blur' }]]),
),
Expand All @@ -36,7 +44,8 @@ function loadPlatformConfig() {
return AccessApi.getPlatformConfig(props.applicationId, props.platform.key)
.then((config) => {
savedConfig = config ?? {}
props.platform.fields.forEach((field) => {
if (modeField.value) form[modeField.value.key] = String(savedConfig[modeField.value.key] ?? modeField.value.default)
activeFields.value.forEach((field) => {
form[field.key] = String(savedConfig[field.key] ?? '')
})
isCertification.value = savedConfig.is_certification ?? false
Expand Down Expand Up @@ -73,7 +82,29 @@ onMounted(loadPlatformConfig)
<MkDrawer v-model="visible" :title="`${platform.name}配置`" @closed="emit('closed')">
<div v-loading="loading" class="min-h-40">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" require-asterisk-position="right" @submit.prevent>
<template v-for="field in platform.fields" :key="field.key">
<el-form-item v-if="modeField" label="连接方式" required>
<div class="w-full space-y-4">
<div
v-for="option in modeField.options"
:key="option.value"
class="flex min-h-[74px] w-full cursor-pointer items-center justify-between rounded-xl border px-4 py-3"
:class="form[modeField.key] === option.value ? 'border-primary!' : 'border-N200'"
@click="form[modeField.key] = option.value"
>
<div class="flex min-w-0 flex-col">
<span class="text-N900">{{ option.label }}</span>
<span class="mt-0.5 text-N500">{{ option.tip }}</span>
</div>
<span
class="ml-4 flex size-4 shrink-0 items-center justify-center rounded-full border"
:class="form[modeField.key] === option.value ? 'border-primary' : 'border-N300'"
>
<span v-if="form[modeField.key] === option.value" class="size-2 rounded-full bg-primary" />
</span>
</div>
</div>
</el-form-item>
<template v-for="field in activeFields" :key="field.key">
<el-form-item :label="field.label" :prop="field.key">
<el-input
v-model="form[field.key]"
Expand All @@ -89,7 +120,7 @@ onMounted(loadPlatformConfig)
<el-switch v-model="isCertification" />
</el-form-item>

<el-form-item label="回调地址">
<el-form-item v-if="showCallback" label="回调地址">
<el-input :model-value="callbackUrl" readonly>
<template #suffix>
<!-- 复制发布渠道回调地址 -->
Expand Down
27 changes: 26 additions & 1 deletion ui/src/views/application-detail/access/platforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@ interface AccessField {
required?: boolean
}

interface AccessModeOption {
value: string
label: string
tip: string
}

export interface AccessPlatformDefinition {
key: ApplicationAccessPlatform
name: string
logo: string
description: string
fields: AccessField[]
modeField?: { key: string; default: string; options: AccessModeOption[]; fields?: Record<string, AccessField[]> }
consoleUrl?: string
consoleName?: string
callbackTip?: string
Expand All @@ -36,9 +43,27 @@ export const accessPlatforms: AccessPlatformDefinition[] = [
{ key: 'token', label: 'Token' },
{ key: 'encoding_aes_key', label: 'EncodingAESKey' },
],
modeField: {
key: 'mode',
default: 'ws',
options: [
{ value: 'ws', label: '使用长连接', tip: '无需域名或 IP 即可接收消息并返回结果' },
{ value: 'callback', label: '使用 URL 回调', tip: '通过配置回调地址接收消息并返回结果' },
],
fields: {
ws: [
{ key: 'bot_id', label: 'Bot ID' },
{ key: 'secret', label: 'Secret', secret: true },
],
callback: [
{ key: 'token', label: 'Token' },
{ key: 'encoding_aes_key', label: 'EncodingAESKey' },
],
},
},
consoleUrl: 'https://work.weixin.qq.com/wework_admin/frame#/manageTools',
consoleName: '企业微信后台',
callbackTip: '-管理工具-智能机器人-创建机器人-API模式创建的 "URL" 中',
callbackTip: '-智能专区-智能机器人-创建机器人-API模式创建的 "URL" 中',
},
{
key: APPLICATION_ACCESS_PLATFORM.WECOM,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { QrLoginPlatformPayload } from '@/api/types'
import type { FormInstance, FormRules } from 'element-plus'
import { LOGIN_METHOD_LABELS, SCAN_FIELD_LABELS } from '@/constants'
import { LOGIN_METHOD } from '@/api/enums'
import { ADMIN_API_BASE_PATH } from '@/api/constants'
import { copyText } from '@/utils/clipboard'
import { MsgSuccess, MsgError } from '@/utils/message'
import perm from '@/permission'

Expand All @@ -14,9 +16,13 @@ const loading = ref(false)
const formRef = useTemplateRef<FormInstance>('formRef')
const form = reactive<QrLoginPlatformPayload>({ key: LOGIN_METHOD.WECOM, isActive: false, config: {} })

// 扫码登录回调地址沿用当前 API base 路径自动生成,只读展示。
const callbackUrl = computed(() => `${window.location.origin}${ADMIN_API_BASE_PATH}/${form.key}`)
const configKeys = computed(() => Object.keys(form.config).filter((key) => key !== 'callback_url'))

const rules = computed<FormRules>(() =>
Object.fromEntries(
Object.keys(form.config).map((key) => [
configKeys.value.map((key) => [
`config.${key}`,
[{ whitespace: true, required: true, message: `请输入${SCAN_FIELD_LABELS[key] ?? key}`, trigger: 'blur' }],
]),
Expand Down Expand Up @@ -53,6 +59,7 @@ function handleValidatePlatform() {

function open(platform: QrLoginPlatformPayload) {
Object.assign(form, platform, { config: { ...platform.config } })
form.config.callback_url = callbackUrl.value
visible.value = true
}

Expand All @@ -66,7 +73,7 @@ defineExpose({ open })
<template>
<MkDrawer v-model="visible" :title="`${LOGIN_METHOD_LABELS[form.key]}设置`" @closed="resetData">
<el-form ref="formRef" v-loading="loading" :model="form" :rules="rules" label-position="top" require-asterisk-position="right">
<el-form-item v-for="(_, key) in form.config" :key="key" :label="SCAN_FIELD_LABELS[key] ?? key" :prop="`config.${key}`">
<el-form-item v-for="key in configKeys" :key="key" :label="SCAN_FIELD_LABELS[key] ?? key" :prop="`config.${key}`">
<el-input
v-model="form.config[key]"
:autocomplete="key === 'app_secret' ? 'new-password' : 'off'"
Expand All @@ -75,6 +82,16 @@ defineExpose({ open })
placeholder="请输入"
/>
</el-form-item>
<el-form-item label="回调地址">
<el-input :model-value="callbackUrl" readonly>
<template #suffix>
<!-- 复制扫码登录回调地址 -->
<el-button text class="-mr-1" :disabled="!callbackUrl" @click="copyText(callbackUrl)">
<MkIcon name="icon_copy_outlined" />
</el-button>
</template>
</el-input>
</el-form-item>
</el-form>

<template #footer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { onMounted, ref, useTemplateRef } from 'vue'
import ChatUserAuthScanApi from '@/api/admin/system/chat-management/chat-user-auth-scan.ts'
import type { QrCodeProvider, QrLoginPlatform, QrLoginPlatformPayload } from '@/api/types'
import { LOGIN_METHOD } from '@/api/enums'
import { LOGIN_METHOD_LABELS, SCAN_FIELD_LABELS } from '@/constants'
import { MsgSuccess, MsgError } from '@/utils/message'
import { LOGIN_METHOD_LABELS } from '@/constants'
import { MsgSuccess } from '@/utils/message'
import dingtalkLogo from '@/assets/logo/logo_dingtalk.svg'
import enterpriseWechatLogo from '@/assets/logo/logo_enterprise-wechat.svg'
import larkLogo from '@/assets/logo/logo_lark.svg'
Expand All @@ -21,8 +21,6 @@ const platformDefinitions: { configKeys: string[]; key: QrCodeProvider; logo: st
{ key: LOGIN_METHOD.LARK, logo: larkLogo, configKeys: ['app_key', 'app_secret'] },
]

const apiBaseUrl = `${window.location.origin}${window.MaxKB?.prefix ?? ''}/api`

const loading = ref(false)
const qrLoginPlatforms = ref<QrLoginPlatformView[]>([])

Expand All @@ -33,10 +31,7 @@ function createQrLoginPlatforms(settings: QrLoginPlatform[]): QrLoginPlatformVie
return {
key,
logo,
config: Object.fromEntries([
...configKeys.map((configKey) => [configKey, setting?.config[configKey] ?? '']),
['callback_url', setting?.config.callback_url || `${apiBaseUrl}/${key}`],
]),
config: Object.fromEntries(configKeys.map((configKey) => [configKey, setting?.config[configKey] ?? ''])),
isActive: setting?.is_active ?? false,
isValid: setting?.is_valid ?? false,
}
Expand Down Expand Up @@ -76,51 +71,32 @@ function handleEditPlatform(platform: QrLoginPlatformView) {
editScanDrawerRef.value?.open(platform)
}

/* 校验 */
function handleValidatePlatform(platform: QrLoginPlatformView) {
loading.value = true
return ChatUserAuthScanApi.putValidateQrLoginPlatform(platform)
.then((res) => {
res ? MsgSuccess('校验成功') : MsgError('校验失败')
})
.finally(() => {
loading.value = false
})
}

onMounted(loadQrPlatforms)
</script>

<template>
<div class="space-y-4">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<template v-for="platform in qrLoginPlatforms" :key="platform.key">
<el-card shadow="hover">
<div class="flex-between mb-4">
<!-- 点击平台卡片配置扫码登录 -->
<el-card shadow="hover" @click="handleEditPlatform(platform)">
<div class="flex-between">
<div class="flex-align-center gap-2">
<img :src="platform.logo" :alt="LOGIN_METHOD_LABELS[platform.key]" class="h-6 w-6" />
<h4>{{ LOGIN_METHOD_LABELS[platform.key] }}</h4>
<el-tag v-if="platform.isValid" size="small" type="success">有效</el-tag>
<!-- <el-tag v-if="!platform.isValid" size="small" type="danger">无效</el-tag> -->
</div>

<div v-if="platform.isValid" class="flex-align-center gap-2">
<span>{{ platform.isActive ? '已启用' : '已关闭' }}</span>
<el-switch v-model="platform.isActive" @change="handlePlatformStatusChange(platform)" />
<div class="flex-align-center gap-4">
<!-- 配置扫码登录平台 -->
<el-button plain @click.stop="handleEditPlatform(platform)">{{ platform.isValid ? '配置' : '未配置' }}</el-button>
<template v-if="platform.isValid">
<el-divider direction="vertical" />
<!-- 启停扫码登录,阻止点击打开配置 -->
<span class="inline-flex" @click.stop>
<el-switch v-model="platform.isActive" @change="handlePlatformStatusChange(platform)" />
</span>
</template>
</div>
<el-button v-else type="primary" @click="handleEditPlatform(platform)">接入</el-button>
</div>
<div class="border-t" v-if="platform.isValid">
<div class="py-4 grid grid-cols-2 gap-x-6 gap-y-4">
<div v-for="(value, key) in platform.config" :key="key">
<p class="text-N600">{{ SCAN_FIELD_LABELS[key] ?? key }}</p>
<p>
{{ key === 'app_secret' ? '••••••••••••' : value }}
</p>
</div>
</div>

<el-button type="primary" @click="handleEditPlatform(platform)">编辑</el-button>
<el-button plain @click="handleValidatePlatform(platform)">校验</el-button>
</div>
</el-card>
</template>
Expand Down
21 changes: 19 additions & 2 deletions ui/src/views/system/settings/authentication/EditSCANDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { QrLoginPlatformPayload } from '@/api/types'
import type { FormInstance, FormRules } from 'element-plus'
import { LOGIN_METHOD } from '@/api/enums'
import { LOGIN_METHOD_LABELS, SCAN_FIELD_LABELS } from '@/constants'
import { ADMIN_API_BASE_PATH } from '@/api/constants'
import { copyText } from '@/utils/clipboard'
import { MsgSuccess, MsgError } from '@/utils/message'
import perm from '@/permission'

Expand All @@ -14,9 +16,13 @@ const loading = ref(false)
const formRef = useTemplateRef<FormInstance>('formRef')
const form = reactive<QrLoginPlatformPayload>({ key: LOGIN_METHOD.WECOM, isActive: false, config: {} })

// 扫码登录回调地址沿用当前 API base 路径自动生成,与发布渠道回调一致只读展示。
const callbackUrl = computed(() => `${window.location.origin}${ADMIN_API_BASE_PATH}/${form.key}`)
const configKeys = computed(() => Object.keys(form.config).filter((key) => key !== 'callback_url'))

const rules = computed<FormRules>(() =>
Object.fromEntries(
Object.keys(form.config).map((key) => [
configKeys.value.map((key) => [
`config.${key}`,
[{ whitespace: true, required: true, message: `请输入${SCAN_FIELD_LABELS[key] ?? key}`, trigger: 'blur' }],
]),
Expand Down Expand Up @@ -53,6 +59,7 @@ function handleValidatePlatform() {

function open(platform: QrLoginPlatformPayload) {
Object.assign(form, platform, { config: { ...platform.config } })
form.config.callback_url = callbackUrl.value
visible.value = true
}

Expand All @@ -66,7 +73,7 @@ defineExpose({ open })
<template>
<MkDrawer v-model="visible" :title="`${LOGIN_METHOD_LABELS[form.key]}设置`" @closed="resetData">
<el-form ref="formRef" v-loading="loading" :model="form" :rules="rules" label-position="top" require-asterisk-position="right">
<el-form-item v-for="(_, key) in form.config" :key="key" :label="SCAN_FIELD_LABELS[key] ?? key" :prop="`config.${key}`">
<el-form-item v-for="key in configKeys" :key="key" :label="SCAN_FIELD_LABELS[key] ?? key" :prop="`config.${key}`">
<el-input
v-model="form.config[key]"
:autocomplete="key === 'app_secret' ? 'new-password' : 'off'"
Expand All @@ -75,6 +82,16 @@ defineExpose({ open })
placeholder="请输入"
/>
</el-form-item>
<el-form-item label="回调地址">
<el-input :model-value="callbackUrl" readonly>
<template #suffix>
<!-- 复制扫码登录回调地址 -->
<el-button text class="-mr-1" :disabled="!callbackUrl" @click="copyText(callbackUrl)">
<MkIcon name="icon_copy_outlined" />
</el-button>
</template>
</el-input>
</el-form-item>
</el-form>

<template #footer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ const platformDefinitions: { configKeys: string[]; key: QrCodeProvider; logo: st
{ key: LOGIN_METHOD.LARK, logo: larkLogo, configKeys: ['app_key', 'app_secret'] },
]

const apiBaseUrl = `${window.location.origin}${window.MaxKB?.prefix ?? ''}/api`

const loading = ref(false)
const qrLoginPlatforms = ref<QrLoginPlatformView[]>([])

Expand All @@ -33,10 +31,7 @@ function createQrLoginPlatforms(settings: QrLoginPlatform[]): QrLoginPlatformVie
return {
key,
logo,
config: Object.fromEntries([
...configKeys.map((configKey) => [configKey, setting?.config[configKey] ?? '']),
['callback_url', setting?.config.callback_url || `${apiBaseUrl}/${key}`],
]),
config: Object.fromEntries(configKeys.map((configKey) => [configKey, setting?.config[configKey] ?? ''])),
isActive: setting?.is_active ?? false,
isValid: setting?.is_valid ?? false,
}
Expand Down
Loading