fix(admin): restore Vite admin source files

This commit is contained in:
kate522 committed 2026-08-17 20:02:36 +08:00
1 parent 8fe3a89c36
commit ac5c0b2dd7
114 files changed
+17272

No files matched your search

+508
View File
@@ -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">&times;</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>