feat(captcha): add provider configuration tabs

This commit is contained in:
2026-09-02 16:55:57 +08:00
parent ff4ec1eb43
commit 0985d0cb0a
4 changed files with 57 additions and 41 deletions
@@ -53,7 +53,7 @@
:collapsible="!!group.collapsible"
:default-collapsed="resolveGroupCollapsed(group)"
>
<template v-if="group.fields">
<template v-if="group.fields && !hasGroupSlot(group)">
<template v-for="field in group.fields" :key="field.key">
<template v-if="isFieldVisible(field)">
<slot
@@ -127,7 +127,7 @@
* - Dependency-based field visibility
* - Loading and error states
*/
import { ref, computed } from 'vue'
import { ref, computed, useSlots } from 'vue'
import FaIcon from './FaIcon.vue'
import AksFieldGroup from './AksFieldGroup.vue'
import AksField from './AksField.vue'
@@ -162,6 +162,7 @@ const props = defineProps({
})
const emit = defineEmits(['update:formData', 'save', 'reload'])
const slots = useSlots()
const searchQuery = ref('')
@@ -202,6 +203,10 @@ function matchDependsOn(dependsOn) {
return true
}
function hasGroupSlot(group) {
return !!slots[`group-${group.key}`]
}
/** Check if a field should be visible based on its dependsOn rule */
function isFieldVisible(field) {
return matchDependsOn(field.dependsOn)
+43 -25
View File
@@ -17,30 +17,29 @@
>
<template
v-if="isPermissionsLogin"
#field-tencent="{ value, setValue }"
#group-providers
>
<TencentConfig :model-value="value || {}" @update:model-value="setValue" @clear-secret="markCaptchaSecretForClear('tencent', $event)" @restore-secret="unmarkCaptchaSecretForClear('tencent', $event)" />
</template>
<template
v-if="isPermissionsLogin"
#field-aliyun="{ value, setValue }"
>
<AliyunConfig :model-value="value || {}" @update:model-value="setValue" @clear-secret="markCaptchaSecretForClear('aliyun', $event)" @restore-secret="unmarkCaptchaSecretForClear('aliyun', $event)" />
</template>
<template
v-if="isPermissionsLogin"
#field-cloudflare="{ value, setValue }"
>
<CloudflareConfig :model-value="value || {}" @update:model-value="setValue" @clear-secret="markCaptchaSecretForClear('cloudflare', $event)" @restore-secret="unmarkCaptchaSecretForClear('cloudflare', $event)" />
</template>
<template
v-if="isPermissionsLogin"
#field-geetest="{ value, setValue }"
>
<GeetestConfig :model-value="value || {}" @update:model-value="setValue" @clear-secret="markCaptchaSecretForClear('geetest', $event)" @restore-secret="unmarkCaptchaSecretForClear('geetest', $event)" />
<n-tabs
v-model:value="captchaProviderTab"
type="segment"
animated
class="aksrt-tabs aksrt-captcha-provider-tabs"
>
<n-tab-pane
v-for="provider in captchaProviders"
:key="provider.key"
:name="provider.key"
:tab="provider.label"
>
<component
:is="provider.component"
:model-value="getCaptchaProviderConfig(provider.key)"
@update:model-value="setCaptchaProviderConfig(provider.key, $event)"
@clear-secret="markCaptchaSecretForClear(provider.key, $event)"
@restore-secret="unmarkCaptchaSecretForClear(provider.key, $event)"
/>
</n-tab-pane>
</n-tabs>
</template>
<template
@@ -132,6 +131,13 @@ const testLoading = ref(false)
const testResult = ref(null)
const serverSchema = ref(null)
const captchaSecretsToClear = ref(new Set())
const captchaProviders = [
{ key: 'tencent', label: '腾讯云 Captcha', component: TencentConfig },
{ key: 'aliyun', label: '阿里云 CAPTCHA2.0', component: AliyunConfig },
{ key: 'cloudflare', label: 'Cloudflare Turnstile', component: CloudflareConfig },
{ key: 'geetest', label: '极验 Geetest', component: GeetestConfig },
]
const captchaProviderTab = ref('tencent')
/** Resolve domain/section from props or route meta (canonicalDomain + section, with legacy fallback) */
const resolvedDomain = computed(() => normalizeSchemaDomain(props.domain || route.meta?.canonicalDomain || route.meta?.schemaDomain || resolveDomainFromPath(route.path)))
@@ -233,10 +239,11 @@ async function handleSave() {
}
}
function handleReload() {
async function handleReload() {
closeTestModal()
captchaSecretsToClear.value = new Set()
reload()
await reload()
syncCaptchaProviderTab()
message.info('已放弃更改')
}
@@ -255,6 +262,15 @@ function unmarkCaptchaSecretForClear(provider, field) {
captchaSecretsToClear.value = next
}
function setCaptchaProviderConfig(provider, value) {
formData.value = { ...formData.value, [provider]: value || {} }
}
function syncCaptchaProviderTab() {
const provider = formData.value?.active_provider
captchaProviderTab.value = captchaProviders.some(({ key }) => key === provider) ? provider : 'tencent'
}
function getCaptchaProviderConfig(provider) {
if (!formData.value || !provider) return {}
const cfg = formData.value[provider]
@@ -452,6 +468,8 @@ function closeTestModal() {
}
}
watch(() => formData.value?.active_provider, syncCaptchaProviderTab)
watch([resolvedDomain, resolvedSection], ([nextDomain, nextSection], [prevDomain, prevSection]) => {
if (nextDomain !== prevDomain || nextSection !== prevSection) {
closeTestModal()
@@ -68,20 +68,13 @@ export const permissionsSchemas = {
key: 'providers',
title: '服务商配置',
icon: 'fa fas fa-server',
description: '按当前选择的服务商填写对应密钥。',
description: '切换标签页填写各服务商配置;实际启用的服务商由“当前服务商”决定。',
dependsOn: { key: 'enabled', value: true },
fields: [
{
key: 'provider_hint',
label: '配置说明',
type: 'notice',
alertType: 'info',
alertContent: '下方会渲染当前验证码服务商的专属配置面板。保存仍通过统一 schema contract 进行。',
dependsOn: { key: 'enabled', value: true },
},
{ key: 'tencent', label: '腾讯云配置', type: 'custom', default: { secret_id: '', secret_key: '', app_id: '', app_secret_key: '' }, secretFields: ['secret_key', 'app_secret_key'], dependsOn: { key: 'active_provider', value: 'tencent' } },
{ key: 'aliyun', label: '阿里云配置', type: 'custom', default: { region: 'cn', prefix: '', scene_id: '', access_key_id: '', access_key_secret: '' }, secretFields: ['access_key_secret'], dependsOn: { key: 'active_provider', value: 'aliyun' } },
{ key: 'cloudflare', label: 'Cloudflare 配置', type: 'custom', default: { site_key: '', secret_key: '', theme: 'auto', size: 'normal' }, secretFields: ['secret_key'], dependsOn: { key: 'active_provider', value: 'cloudflare' } },
{ key: 'geetest', label: '极验配置', type: 'custom', default: { captcha_id: '', captcha_key: '', product: 'bind' }, secretFields: ['captcha_key'], dependsOn: { key: 'active_provider', value: 'geetest' } },
{ key: 'tencent', label: '腾讯云配置', type: 'custom', default: { secret_id: '', secret_key: '', app_id: '', app_secret_key: '' }, secretFields: ['secret_key', 'app_secret_key'] },
{ key: 'aliyun', label: '阿里云配置', type: 'custom', default: { region: 'cn', prefix: '', scene_id: '', access_key_id: '', access_key_secret: '' }, secretFields: ['access_key_secret'] },
{ key: 'cloudflare', label: 'Cloudflare 配置', type: 'custom', default: { site_key: '', secret_key: '', theme: 'auto', size: 'normal' }, secretFields: ['secret_key'] },
{ key: 'geetest', label: '极验配置', type: 'custom', default: { captcha_id: '', captcha_key: '', product: 'bind' }, secretFields: ['captcha_key'] },
],
},
],
+1 -1
View File
@@ -54,7 +54,7 @@ class SchemaPermissions {
['label' => '前台投稿', 'value' => 'front_submit'],
]],
]],
['key' => 'providers', 'title' => '服务商配置', 'fields' => $provider_fields],
['key' => 'providers', 'title' => '服务商配置', 'dependsOn' => ['key' => 'enabled', 'value' => true], 'fields' => $provider_fields],
],
],
'attachment' => [