fix(admin): restore Vite admin source files
This commit is contained in:
1 parent
8fe3a89c36
commit
ac5c0b2dd7
114 files changed
+17272
No files matched your search
@@ -0,0 +1,508 @@
|
||||
<template>
|
||||
<div class="aksrt-schema-page">
|
||||
<AksFormRenderer
|
||||
:title="pageMeta.title"
|
||||
:description="pageMeta.description"
|
||||
:icon="pageMeta.icon"
|
||||
:schema="pageMeta.groups"
|
||||
:form-data="formData"
|
||||
:dirty="dirty"
|
||||
:loading="loading"
|
||||
:saving="saving"
|
||||
:load-error="loadError"
|
||||
:show-search="showSearch"
|
||||
@update:form-data="formData = $event"
|
||||
@save="handleSave"
|
||||
@reload="handleReload"
|
||||
>
|
||||
<template
|
||||
v-if="isPermissionsLogin"
|
||||
#field-tencent="{ value, setValue }"
|
||||
>
|
||||
<TencentConfig :model-value="value || {}" @update:model-value="setValue" />
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="isPermissionsLogin"
|
||||
#field-aliyun="{ value, setValue }"
|
||||
>
|
||||
<AliyunConfig :model-value="value || {}" @update:model-value="setValue" />
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="isPermissionsLogin"
|
||||
#field-cloudflare="{ value, setValue }"
|
||||
>
|
||||
<CloudflareConfig :model-value="value || {}" @update:model-value="setValue" />
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="isPermissionsLogin"
|
||||
#field-geetest="{ value, setValue }"
|
||||
>
|
||||
<GeetestConfig :model-value="value || {}" @update:model-value="setValue" />
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="isPermissionsModeration"
|
||||
#field-geoip_block_countries="{ value, setValue }"
|
||||
>
|
||||
<n-dynamic-tags :value="Array.isArray(value) ? value : []" @update:value="setValue" />
|
||||
<p class="aksrt-schema-page__field-hint">使用 ISO 国家代码,如 CN / US / RU。</p>
|
||||
</template>
|
||||
|
||||
<!-- Slot for custom group content (override per page if needed) -->
|
||||
<template v-for="(_, name) in $slots" #[name]="slotData">
|
||||
<slot :name="name" v-bind="slotData" />
|
||||
</template>
|
||||
</AksFormRenderer>
|
||||
|
||||
<div
|
||||
v-if="isPermissionsLogin && !loading"
|
||||
class="aksrt-schema-page__tool-actions"
|
||||
>
|
||||
<n-button
|
||||
:disabled="!formData?.enabled || !formData?.active_provider"
|
||||
@click="handleCaptchaTest"
|
||||
>
|
||||
测试验证
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div v-if="showTestModal" class="captcha-test-overlay" @click.self="closeTestModal">
|
||||
<div class="captcha-test-panel">
|
||||
<div class="captcha-test-header">
|
||||
<span>测试验证码</span>
|
||||
<button class="captcha-test-close" @click="closeTestModal">×</button>
|
||||
</div>
|
||||
<div class="captcha-test-body">
|
||||
<div v-if="testLoading" style="text-align:center;padding:40px;">
|
||||
<n-spin />
|
||||
<p style="margin-top:10px;color:#999;">加载中...</p>
|
||||
</div>
|
||||
<div v-if="testResult && !testLoading" :style="{color: testResult.success ? '#18a058' : '#d03050', textAlign:'center', padding:'20px'}">
|
||||
<p style="font-size:16px;margin:0;">{{ testResult.message }}</p>
|
||||
</div>
|
||||
<div v-if="!testLoading && !testResult" id="captcha-test-box" style="min-height:150px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* SchemaPage — generic Schema-driven settings page.
|
||||
*
|
||||
* Reads the schema from the Vue-side schema registry, fetches/saves settings
|
||||
* via the API client, and renders using AksFormRenderer.
|
||||
*
|
||||
* Usage (in router or parent):
|
||||
* <SchemaPage domain="global" section="logo" />
|
||||
*
|
||||
* Or auto-detect from route meta:
|
||||
* The router's meta.schemaKey provides domain/section lookup.
|
||||
*/
|
||||
import { computed, watch, ref, nextTick, onBeforeUnmount } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import AksFormRenderer from '@/components/form/AksFormRenderer.vue'
|
||||
import { useSectionForm } from '@/composables/useSectionForm'
|
||||
import { getSchema, normalizeSchemaDomain } from '@/schema/index.js'
|
||||
import api from '@/api/index.js'
|
||||
import TencentConfig from '@/components/captcha/TencentConfig.vue'
|
||||
import AliyunConfig from '@/components/captcha/AliyunConfig.vue'
|
||||
import CloudflareConfig from '@/components/captcha/CloudflareConfig.vue'
|
||||
import GeetestConfig from '@/components/captcha/GeetestConfig.vue'
|
||||
|
||||
const props = defineProps({
|
||||
/** Schema domain key (e.g. 'global') */
|
||||
domain: { type: String, default: '' },
|
||||
/** Schema section key (e.g. 'logo') */
|
||||
section: { type: String, default: '' },
|
||||
/** Whether to show inline search */
|
||||
showSearch: { type: Boolean, default: true },
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
|
||||
const showTestModal = ref(false)
|
||||
const testLoading = ref(false)
|
||||
const testResult = ref(null)
|
||||
|
||||
/** 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)))
|
||||
const resolvedSection = computed(() => props.section || route.meta?.section || route.meta?.schemaSection || resolveSectionFromPath(route.path))
|
||||
const isPermissionsLogin = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'login')
|
||||
const isPermissionsModeration = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'moderation')
|
||||
|
||||
function resolveDomainFromPath(path) {
|
||||
const parts = path.split('/').filter(Boolean)
|
||||
return parts[0] || 'global'
|
||||
}
|
||||
|
||||
/** Extract section from route path: /global/logo → logo */
|
||||
function resolveSectionFromPath(path) {
|
||||
const parts = path.split('/').filter(Boolean)
|
||||
return parts[parts.length - 1] || 'common'
|
||||
}
|
||||
|
||||
/** Load schema from the Vue-side registry */
|
||||
const pageMeta = computed(() => {
|
||||
const schema = getSchema(resolvedDomain.value, resolvedSection.value)
|
||||
if (schema && schema.groups && schema.groups.length > 0) {
|
||||
return {
|
||||
title: schema.title || '',
|
||||
description: schema.description || '',
|
||||
icon: schema.icon || '',
|
||||
groups: schema.groups,
|
||||
}
|
||||
}
|
||||
// Fallback: read from route meta
|
||||
return {
|
||||
title: route.meta?.title || '',
|
||||
description: route.meta?.desc || '',
|
||||
icon: '',
|
||||
groups: [],
|
||||
}
|
||||
})
|
||||
|
||||
/** Default values from schema */
|
||||
const defaults = computed(() => {
|
||||
const schema = getSchema(resolvedDomain.value, resolvedSection.value)
|
||||
if (!schema) return {}
|
||||
const d = {}
|
||||
for (const group of schema.groups || []) {
|
||||
for (const field of group.fields || []) {
|
||||
if (field.default !== undefined) {
|
||||
d[field.key] = field.default
|
||||
}
|
||||
}
|
||||
}
|
||||
return d
|
||||
})
|
||||
|
||||
/** Use the section form composable */
|
||||
const {
|
||||
formData, loading, saving, loadError, dirty,
|
||||
save: doSave, reload,
|
||||
} = useSectionForm({
|
||||
fetch: () => api.getSchemaPage(resolvedDomain.value, resolvedSection.value),
|
||||
submit: (data) => api.saveSchemaPage(resolvedDomain.value, resolvedSection.value, data),
|
||||
defaults: defaults.value,
|
||||
onSaved: () => {
|
||||
message.success('设置已保存')
|
||||
},
|
||||
})
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await doSave()
|
||||
} catch (err) {
|
||||
message.error('保存失败: ' + (err?.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
function handleReload() {
|
||||
closeTestModal()
|
||||
reload()
|
||||
message.info('已放弃更改')
|
||||
}
|
||||
|
||||
function getCaptchaProviderConfig(provider) {
|
||||
if (!formData.value || !provider) return {}
|
||||
const cfg = formData.value[provider]
|
||||
return cfg && typeof cfg === 'object' ? cfg : {}
|
||||
}
|
||||
|
||||
function handleCaptchaTest() {
|
||||
const provider = formData.value?.active_provider
|
||||
if (!provider) return
|
||||
|
||||
const cfg = getCaptchaProviderConfig(provider)
|
||||
const required = {
|
||||
tencent: ['secret_id', 'secret_key', 'app_id', 'app_secret_key'],
|
||||
aliyun: ['region', 'prefix', 'scene_id', 'access_key_id', 'access_key_secret'],
|
||||
cloudflare: ['site_key', 'secret_key'],
|
||||
geetest: ['captcha_id', 'captcha_key'],
|
||||
}
|
||||
const missing = (required[provider] || []).filter((key) => !cfg[key])
|
||||
if (missing.length) {
|
||||
message.warning(`请先填写必填字段: ${missing.join(', ')}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (provider === 'tencent') {
|
||||
loadTencentTest(cfg)
|
||||
return
|
||||
}
|
||||
|
||||
showTestModal.value = true
|
||||
testResult.value = null
|
||||
testLoading.value = true
|
||||
|
||||
nextTick(() => {
|
||||
createTestIframe(provider, cfg)
|
||||
})
|
||||
}
|
||||
|
||||
function loadTencentTest(cfg) {
|
||||
const sdkSrc = 'https://turing.captcha.qcloud.com/TJCaptcha.js'
|
||||
|
||||
if (typeof window.TencentCaptcha !== 'undefined') {
|
||||
try {
|
||||
new window.TencentCaptcha(cfg.app_id, function(res) {
|
||||
if (res.ret === 0) message.success('腾讯云验证码测试成功!配置正确。')
|
||||
else message.warning('验证已取消。')
|
||||
}, { userLanguage: 'zh-cn' }).show()
|
||||
} catch (error) {
|
||||
message.error('初始化失败: ' + error.message)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const existingScript = document.querySelector('script[src="' + sdkSrc + '"]')
|
||||
if (existingScript) {
|
||||
existingScript.addEventListener('load', function() { loadTencentTest(cfg) }, { once: true })
|
||||
return
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = sdkSrc
|
||||
script.onload = function () {
|
||||
try {
|
||||
new window.TencentCaptcha(cfg.app_id, function(res) {
|
||||
if (res.ret === 0) message.success('腾讯云验证码测试成功!')
|
||||
else message.warning('验证已取消。')
|
||||
}, { userLanguage: 'zh-cn' }).show()
|
||||
} catch (error) {
|
||||
message.error('初始化失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
script.onerror = function () { message.error('加载腾讯验证码SDK失败') }
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
function createTestIframe(provider, cfg) {
|
||||
setTimeout(() => {
|
||||
testLoading.value = false
|
||||
loadProviderSDK(provider, cfg)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function loadProviderSDK(provider, cfg) {
|
||||
const oldScript = document.getElementById('captcha-test-sdk')
|
||||
if (oldScript) oldScript.remove()
|
||||
|
||||
if (window.__gtTestCaptcha) {
|
||||
try { window.__gtTestCaptcha.destroy() } catch (error) {}
|
||||
window.__gtTestCaptcha = null
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.id = 'captcha-test-sdk'
|
||||
|
||||
switch (provider) {
|
||||
case 'tencent':
|
||||
script.src = 'https://turing.captcha.qcloud.com/TJCaptcha.js'
|
||||
script.onload = function () {
|
||||
try {
|
||||
new window.TencentCaptcha(cfg.app_id, function(res) {
|
||||
testResult.value = { success: res.ret === 0, message: res.ret === 0 ? '验证成功!' : '验证已取消。' }
|
||||
}, { userLanguage: 'zh-cn' }).show()
|
||||
} catch (error) {
|
||||
testResult.value = { success: false, message: '初始化失败: ' + error.message }
|
||||
}
|
||||
}
|
||||
script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
break
|
||||
|
||||
case 'cloudflare':
|
||||
window.onTurnstileTestLoad = function() {
|
||||
try {
|
||||
window.turnstile.render('#captcha-test-box', {
|
||||
sitekey: cfg.site_key,
|
||||
theme: 'auto',
|
||||
size: cfg.size || 'normal',
|
||||
callback: function() {
|
||||
testResult.value = { success: true, message: '验证成功!' }
|
||||
message.success('Cloudflare Turnstile 测试成功!')
|
||||
setTimeout(closeTestModal, 1500)
|
||||
},
|
||||
'error-callback': function() {
|
||||
testResult.value = { success: false, message: '验证失败' }
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
testResult.value = { success: false, message: '初始化失败: ' + error.message }
|
||||
}
|
||||
}
|
||||
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileTestLoad'
|
||||
script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
break
|
||||
|
||||
case 'geetest':
|
||||
script.src = 'https://static.geetest.com/v4/gt4.js'
|
||||
script.onload = function () {
|
||||
try {
|
||||
window.initGeetest4({ captchaId: cfg.captcha_id, product: cfg.product || 'float' }, function(captchaObj) {
|
||||
window.__gtTestCaptcha = captchaObj
|
||||
const onSuccess = function() {
|
||||
testResult.value = { success: true, message: '验证成功!' }
|
||||
message.success('极验 GeeTest 测试成功!')
|
||||
setTimeout(closeTestModal, 1500)
|
||||
}
|
||||
const onError = function(error) {
|
||||
testResult.value = { success: false, message: '出错: ' + (error.msg || '?') }
|
||||
}
|
||||
const onClose = function() {
|
||||
if (!testResult.value) testResult.value = { success: false, message: '已关闭' }
|
||||
}
|
||||
if ((cfg.product || 'float') === 'bind') {
|
||||
captchaObj.onReady(() => captchaObj.showCaptcha()).onSuccess(onSuccess).onError(onError).onClose(onClose)
|
||||
} else {
|
||||
captchaObj.appendTo('#captcha-test-box').onReady(() => {}).onSuccess(onSuccess).onError(onError).onClose(onClose)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
testResult.value = { success: false, message: '初始化失败: ' + error.message }
|
||||
}
|
||||
}
|
||||
script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
break
|
||||
|
||||
case 'aliyun':
|
||||
window.AliyunCaptchaConfig = { region: cfg.region || 'cn', prefix: cfg.prefix }
|
||||
script.src = 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js'
|
||||
script.onload = function () {
|
||||
try {
|
||||
window.initAliyunCaptcha({
|
||||
SceneId: cfg.scene_id,
|
||||
mode: 'embed',
|
||||
element: '#captcha-test-box',
|
||||
success: function() {
|
||||
testResult.value = { success: true, message: '验证成功!' }
|
||||
message.success('阿里云测试成功!')
|
||||
setTimeout(closeTestModal, 1500)
|
||||
},
|
||||
fail: function(result) {
|
||||
testResult.value = { success: false, message: String(result) }
|
||||
},
|
||||
getInstance: function() {},
|
||||
slideStyle: { width: 360, height: 40 },
|
||||
language: 'cn',
|
||||
})
|
||||
} catch (error) {
|
||||
testResult.value = { success: false, message: '初始化失败: ' + error.message }
|
||||
}
|
||||
}
|
||||
script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
break
|
||||
|
||||
default:
|
||||
testResult.value = { success: false, message: '未知服务商' }
|
||||
return
|
||||
}
|
||||
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
function closeTestModal() {
|
||||
showTestModal.value = false
|
||||
testResult.value = null
|
||||
testLoading.value = false
|
||||
if (window.__gtTestCaptcha) {
|
||||
try { window.__gtTestCaptcha.destroy() } catch (error) {}
|
||||
window.__gtTestCaptcha = null
|
||||
}
|
||||
const script = document.getElementById('captcha-test-sdk')
|
||||
if (script) script.remove()
|
||||
if (window.onTurnstileTestLoad) {
|
||||
try { delete window.onTurnstileTestLoad } catch (error) {}
|
||||
}
|
||||
}
|
||||
|
||||
watch([resolvedDomain, resolvedSection], ([nextDomain, nextSection], [prevDomain, prevSection]) => {
|
||||
if (nextDomain !== prevDomain || nextSection !== prevSection) {
|
||||
closeTestModal()
|
||||
reload()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
closeTestModal()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-schema-page {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.aksrt-schema-page__tool-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.aksrt-schema-page__field-hint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
color: #8a909c;
|
||||
}
|
||||
|
||||
.captcha-test-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.captcha-test-panel {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
width: 420px;
|
||||
max-width: 90%;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.captcha-test-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 18px 22px;
|
||||
border-bottom: 1px solid #e8e8ec;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.captcha-test-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.captcha-test-close:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.captcha-test-body {
|
||||
padding: 24px;
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user