@@ -0,0 +1,300 @@
< template >
< div class = "aksrt-page-container" >
< PageHeader
title = "身份认证"
desc = "统一管理认证申请规则与云服务商凭证;审核、驳回和取消认证仍在 WordPress 用户菜单处理。"
/ >
< n-spin :show = "loading" style = "width: 100%; min-height: 240px" >
< n-grid :cols = "3" :x-gap = "12" :y-gap = "12" responsive = "screen" item -responsive >
< n-grid-item span = "3 m:1" >
< n-card class = "aksrt-status-card" size = "small" >
< n-statistic label = "身份认证功能" >
< template # default > { { verifyForm . user _verify _enabled ? '已启用' : '已停用' } } < / template >
< / n-statistic >
< n-tag : type = "verifyForm.user_verify_enabled ? 'success' : 'default'" round size = "small" >
{ { verifyForm . user _verify _enabled ? '运行中' : '未运行' } }
< / n-tag >
< / n-card >
< / n-grid-item >
< n-grid-item span = "3 m:1" v-for = "key in providerKeys" :key="key" >
< n -card class = "aksrt-status-card" size = "small" >
< n-statistic : label = "providerLabel(key) + '凭证'" >
< template # default > { { providerState ( key ) . configured ? '已配置' : '未配置' } } < / template >
< / n-statistic >
< n-tag : type = "providerState(key).configured ? 'success' : 'warning'" round size = "small" >
{ { identityForm . provider === key ? '当前服务商' : '备用服务商' } }
< / n-tag >
< / n-card >
< / n-grid-item >
< / n-grid >
< n-card class = "aksrt-section-card" title = "通用认证设置" >
< n-form :model = "verifyForm" label -placement = " left " label -width = " 190 " >
< n-form-item label = "启用身份认证" > < n-switch v -model :value = "verifyForm.user_verify_enabled" / > < / n-form-item >
< n-form-item label = "公开页显示认证徽标" > < n-switch v -model :value = "verifyForm.user_verify_public_display" / > < / n-form-item >
< n-form-item label = "允许用户前台申请" > < n-switch v -model :value = "verifyForm.user_verify_allow_apply" / > < / n-form-item >
< n-form-item label = "个人认证徽标文案" >
< n-input v -model :value = "verifyForm.user_verify_personal_badge_text" placeholder = "个人认证" / >
< / n-form-item >
< n-form-item label = "企业认证徽标文案" >
< n-input v -model :value = "verifyForm.user_verify_enterprise_badge_text" placeholder = "企业认证" / >
< / n-form-item >
< n-form-item label = "停用申请说明" >
< n-input v -model :value = "verifyForm.user_verify_disable_desc" type = "textarea" :rows = "2" / >
< / n-form-item >
< n-form-item label = "申请介绍" >
< n-input v -model :value = "verifyForm.user_verify_desc" type = "textarea" :rows = "3" / >
< / n-form-item >
< / n-form >
< / n-card >
< n-card class = "aksrt-section-card" title = "云服务商" >
< n-form :model = "identityForm" label -placement = " left " label -width = " 190 " >
< n-form-item label = "当前服务商" >
< n-radio-group v -model :value = "identityForm.provider" >
< n-space >
< n-radio-button value = "aliyun" > 阿里云 < / n-radio-button >
< n-radio-button value = "tencent" > 腾讯云 < / n-radio-button >
< / n-space >
< / n-radio-group >
< / n-form-item >
< n-form-item label = "回调地址" >
< n-input-group >
< n-input :value = "callbackUrl" readonly / >
< n-button @click ="copyCallback" > 复制 < / n -button >
< / n-input-group >
< template # feedback > 将此地址填入服务商控制台需要的回调地址配置 。 < / template >
< / n-form-item >
< / n-form >
< / n-card >
< n-tabs v -model :value = "activeTab" type = "segment" animated class = "aksrt-provider-tabs" >
< n-tab-pane v-for = "key in providerKeys" :key="key" :name="key" :tab="providerLabel(key)" >
< n -card class = "aksrt-section-card" >
< template # header >
< n-space align = "center" >
< span > { { providerLabel ( key ) } } 凭证 < / span >
< n-tag : type = "providerState(key).configured ? 'success' : 'warning'" size = "small" round >
{ { providerState ( key ) . configured ? '已配置' : '未配置' } }
< / n-tag >
< / n-space >
< / template >
< n-alert type = "info" :show-icon = "false" style = "margin-bottom: 18px" >
已保存凭证不会回显 。 留空表示保留原值 ; 输入新值并保存会替换对应凭证 。
< / n-alert >
< n-form label -placement = " left " label -width = " 190 " >
< n-form-item v-for = "field in credentialFields[key]" :key="field.key" :label="field.label" >
< n -input
v -model :value = "credentials[key][field.key]"
type = "password"
show -password -on = " click "
: placeholder = "credentialPlaceholder(key, field.key)"
autocomplete = "new-password"
/ >
< / n-form-item >
< n-form-item label = "显式清除凭证" >
< n-checkbox v -model :checked = "clearCredentials[key]" >
保存时删除 { { providerLabel ( key ) } } 全部凭证
< / n-checkbox >
< template # feedback > 清除后该服务商立即变为未配置 ; 此操作不会因输入框留空而触发 。 < / template >
< / n-form-item >
< n-form-item label = "配置测试" >
< n-button : loading = "testing === key" :disabled = "saving" @click ="testProvider(key)" >
测试 {{ providerLabel ( key ) }} 配置
< / n -button >
< / n-form-item >
< / n-form >
< / n-card >
< / n-tab-pane >
< / n-tabs >
< div class = "aksrt-form-actions" >
< n-button :disabled = "!dirty" @click ="load" > 放弃更改 < / n -button >
< n-button type = "primary" :loading = "saving" @click ="save" > 保存全部配置 < / n -button >
< / div >
< / n-spin >
< / div >
< / template >
< script setup >
import { computed , onMounted , reactive , ref , watch } from 'vue'
import { useDialog , useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api'
const message = useMessage ( )
const dialog = useDialog ( )
const providerKeys = [ 'aliyun' , 'tencent' ]
const credentialFields = {
aliyun : [
{ key : 'access_key_id' , label : 'AccessKey ID' } ,
{ key : 'access_key_secret' , label : 'AccessKey Secret' } ,
] ,
tencent : [
{ key : 'secret_id' , label : 'SecretId' } ,
{ key : 'secret_key' , label : 'SecretKey' } ,
] ,
}
const verifyDefaults = {
user _verify _enabled : true ,
user _verify _public _display : true ,
user _verify _allow _apply : true ,
user _verify _personal _badge _text : '个人认证' ,
user _verify _enterprise _badge _text : '企业认证' ,
user _verify _disable _desc : '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员' ,
user _verify _desc : '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。' ,
}
const loading = ref ( true )
const saving = ref ( false )
const testing = ref ( '' )
const activeTab = ref ( 'aliyun' )
const callbackUrl = ref ( '' )
const initialSnapshot = ref ( '' )
const states = reactive ( { aliyun : { } , tencent : { } } )
const verifyForm = reactive ( { ... verifyDefaults } )
const identityForm = reactive ( { provider : 'aliyun' } )
const credentials = reactive ( { aliyun : { } , tencent : { } } )
const clearCredentials = reactive ( { aliyun : false , tencent : false } )
const snapshot = computed ( ( ) => JSON . stringify ( { verifyForm , identityForm , credentials , clearCredentials } ) )
const dirty = computed ( ( ) => initialSnapshot . value !== '' && snapshot . value !== initialSnapshot . value )
watch ( ( ) => identityForm . provider , value => { activeTab . value = value } )
function providerLabel ( key ) {
return key === 'aliyun' ? '阿里云' : '腾讯云'
}
function providerState ( key ) {
return states [ key ] || { }
}
function credentialPlaceholder ( provider , field ) {
return providerState ( provider ) . configured _fields ? . [ field ] ? '已配置,留空则保留' : '尚未配置'
}
function extractData ( response ) {
return response ? . data ? ? response ? ? { }
}
async function load ( ) {
loading . value = true
try {
const [ verifyResponse , identityResponse ] = await Promise . all ( [
api . getSchemaPage ( 'users' , 'verify' ) ,
api . getIdentitySettings ( ) ,
] )
Object . assign ( verifyForm , verifyDefaults , extractData ( verifyResponse ) )
const identity = extractData ( identityResponse )
identityForm . provider = providerKeys . includes ( identity . provider ) ? identity . provider : 'aliyun'
callbackUrl . value = identity . callback _url || ''
providerKeys . forEach ( key => {
states [ key ] = identity . providers ? . [ key ] || { }
credentials [ key ] = { }
clearCredentials [ key ] = false
} )
activeTab . value = identityForm . provider
initialSnapshot . value = snapshot . value
} catch ( error ) {
message . error ( error . message || '加载身份认证配置失败' )
} finally {
loading . value = false
}
}
function buildProviderPayload ( key ) {
const replacement = { }
credentialFields [ key ] . forEach ( ( { key : field } ) => {
const value = String ( credentials [ key ] ? . [ field ] || '' ) . trim ( )
if ( value ) replacement [ field ] = value
} )
return {
credentials : replacement ,
clear _credentials : clearCredentials [ key ] ,
}
}
async function persist ( ) {
const [ verifyResponse , identityResponse ] = await Promise . all ( [
api . saveSchemaPage ( 'users' , 'verify' , { ... verifyForm } ) ,
api . saveIdentitySettings ( {
provider : identityForm . provider ,
providers : {
aliyun : buildProviderPayload ( 'aliyun' ) ,
tencent : buildProviderPayload ( 'tencent' ) ,
} ,
} ) ,
] )
const identity = extractData ( identityResponse )
Object . assign ( verifyForm , extractData ( verifyResponse ) )
providerKeys . forEach ( key => {
states [ key ] = identity . providers ? . [ key ] || states [ key ]
credentials [ key ] = { }
clearCredentials [ key ] = false
} )
callbackUrl . value = identity . callback _url || callbackUrl . value
initialSnapshot . value = snapshot . value
}
async function save ( ) {
const hasClear = providerKeys . some ( key => clearCredentials [ key ] )
const run = async ( ) => {
saving . value = true
try {
await persist ( )
message . success ( '身份认证配置已保存' )
} catch ( error ) {
message . error ( error . message || '保存失败' )
} finally {
saving . value = false
}
}
if ( ! hasClear ) return run ( )
dialog . warning ( {
title : '确认清除凭证' ,
content : '已勾选的服务商凭证将被永久删除,是否继续保存?' ,
positiveText : '清除并保存' ,
negativeText : '取消' ,
onPositiveClick : run ,
} )
}
async function testProvider ( provider ) {
testing . value = provider
try {
const response = await api . testIdentitySettings ( { provider } )
message . success ( response ? . message || ` ${ providerLabel ( provider ) } 配置测试通过 ` )
} catch ( error ) {
message . error ( error . message || ` ${ providerLabel ( provider ) } 配置测试失败 ` )
} finally {
testing . value = ''
}
}
async function copyCallback ( ) {
if ( ! callbackUrl . value ) return message . warning ( '回调地址暂不可用' )
try {
await navigator . clipboard . writeText ( callbackUrl . value )
message . success ( '回调地址已复制' )
} catch {
message . error ( '复制失败,请手动复制' )
}
}
onMounted ( load )
< / script >
< style scoped >
. aksrt - status - card : deep ( . n - card _ _content ) {
display : flex ;
align - items : flex - end ;
justify - content : space - between ;
gap : 12 px ;
}
. aksrt - section - card { margin - top : 16 px ; }
. aksrt - provider - tabs { margin - top : 16 px ; }
. aksrt - form - actions {
display : flex ;
justify - content : flex - end ;
gap : 12 px ;
margin - top : 20 px ;
}
< / style >