fix(ui):修复多个页面vue语法告警
This commit is contained in:
1 parent
29e07facb4
commit
bdca11903d
20 files changed
+57
-241
No files matched your search
@@ -672,7 +672,7 @@ function formatHeaders(headers: Record<string, string>): string {
|
||||
|
||||
<!-- Update Key Modal -->
|
||||
<AModal v-model:visible="showUpdateKeyModal" title="更新 API Key" :width="450" :on-before-ok="handleUpdateKey" :ok-loading="updateKeyLoading">
|
||||
<AForm layout="vertical">
|
||||
<AForm :model="{ key: newApiKey }" layout="vertical">
|
||||
<AFormItem label="新 API Key" required>
|
||||
<AInput v-model="newApiKey" type="textarea" :auto-size="{ minRows: 3, maxRows: 5 }" placeholder="输入新的 API Key" />
|
||||
</AFormItem>
|
||||
@@ -681,7 +681,7 @@ function formatHeaders(headers: Record<string, string>): string {
|
||||
|
||||
<!-- Clone Channel Modal -->
|
||||
<AModal v-model:visible="showCloneModal" title="克隆渠道" :width="500" :on-before-ok="handleClone" :ok-loading="cloneLoading">
|
||||
<AForm layout="vertical">
|
||||
<AForm :model="cloneForm" layout="vertical">
|
||||
<AFormItem label="新渠道名称">
|
||||
<AInput v-model="cloneForm.name" :placeholder="(detail?.name || '') + ' (副本)'" />
|
||||
</AFormItem>
|
||||
|
||||
@@ -327,7 +327,7 @@ const { exporting, exportFile } = useExport({
|
||||
<AModal v-model:visible="showOAuthDialog" title="官方渠道 OAuth 授权" :width="560" :footer="false" :mask-closable="false">
|
||||
<!-- Step 1: Choose platform & mode -->
|
||||
<div v-if="!oauthStep2">
|
||||
<AForm layout="vertical">
|
||||
<AForm :model="oauthForm" layout="vertical">
|
||||
<AFormItem label="选择平台" required>
|
||||
<ARadioGroup v-model="oauthForm.platform">
|
||||
<Radio value="claude">Claude</Radio>
|
||||
@@ -358,7 +358,7 @@ const { exporting, exportFile } = useExport({
|
||||
<!-- Step 2: Enter code -->
|
||||
<div v-else>
|
||||
<AAlert type="info" class="mb-4">请在弹出窗口中完成授权,将回调 URL 中的 code 参数粘贴到下方。</AAlert>
|
||||
<AForm layout="vertical">
|
||||
<AForm :model="oauthForm" layout="vertical">
|
||||
<AFormItem label="Authorization Code" required>
|
||||
<AInput v-model="oauthForm.code" placeholder="粘贴 code 参数值" />
|
||||
</AFormItem>
|
||||
|
||||
@@ -388,8 +388,8 @@ onMounted(() => {
|
||||
<PageHeader title="支付设置" description="管理支付渠道和全局支付配置" />
|
||||
|
||||
<ATabs v-model:active-key="activeTab" type="rounded" class="mb-4">
|
||||
<ATab key="channels" title="支付渠道" />
|
||||
<ATab key="settings" title="全局设置" />
|
||||
<ATabPane key="channels" title="支付渠道" />
|
||||
<ATabPane key="settings" title="全局设置" />
|
||||
</ATabs>
|
||||
|
||||
<!-- 支付渠道管理 -->
|
||||
|
||||
@@ -181,7 +181,7 @@ onUnmounted(() => {
|
||||
</a-card>
|
||||
|
||||
<a-modal v-model:visible="showResolveModal" title="解决告警" :on-before-ok="handleResolve" :ok-loading="resolveLoading">
|
||||
<a-form layout="vertical">
|
||||
<a-form :model="{ notes: resolveNotes }" layout="vertical">
|
||||
<a-form-item label="处理备注">
|
||||
<a-textarea v-model="resolveNotes" placeholder="请输入处理说明..." :auto-size="{ minRows: 3, maxRows: 6 }" />
|
||||
</a-form-item>
|
||||
|
||||
@@ -11,7 +11,7 @@ const total = ref(0)
|
||||
const pagination = reactive({ current: 1, pageSize: 20 })
|
||||
|
||||
const filterSource = ref('')
|
||||
const filterErrorCode = ref('')
|
||||
const filterErrorCode = ref<number | undefined>(undefined)
|
||||
const filterResolved = ref('')
|
||||
const filterDateRange = ref<string[]>([])
|
||||
const filterKeyword = ref('')
|
||||
@@ -118,7 +118,7 @@ async function fetchData() {
|
||||
page_size: pagination.pageSize,
|
||||
}
|
||||
if (filterSource.value) params.source = filterSource.value
|
||||
if (filterErrorCode.value) params.error_code = parseInt(filterErrorCode.value)
|
||||
if (filterErrorCode.value) params.error_code = filterErrorCode.value
|
||||
if (filterResolved.value) params.resolved = filterResolved.value
|
||||
if (filterKeyword.value) params.keyword = filterKeyword.value
|
||||
if (filterDateRange.value?.length === 2) {
|
||||
@@ -194,7 +194,7 @@ function handleFilter() {
|
||||
|
||||
function resetFilter() {
|
||||
filterSource.value = ''
|
||||
filterErrorCode.value = ''
|
||||
filterErrorCode.value = undefined
|
||||
filterResolved.value = ''
|
||||
filterDateRange.value = []
|
||||
filterKeyword.value = ''
|
||||
|
||||
@@ -176,7 +176,7 @@ onMounted(fetchTemplates)
|
||||
|
||||
<!-- Test Modal -->
|
||||
<AModal v-model:visible="showTestModal" title="模板测试" :width="650" :footer="false">
|
||||
<AForm layout="vertical">
|
||||
<AForm :model="{ code: testingCode, variables: testVariables }" layout="vertical">
|
||||
<AFormItem label="模板编码">
|
||||
<AInput :model-value="testingCode" disabled />
|
||||
</AFormItem>
|
||||
|
||||
@@ -358,7 +358,7 @@ onMounted(fetchTickets)
|
||||
|
||||
<!-- Reply Modal -->
|
||||
<AModal v-model:visible="showReplyModal" title="回复工单" :width="500" :mask-closable="false" :on-before-ok="handleReply" :ok-loading="replyLoading">
|
||||
<AForm layout="vertical">
|
||||
<AForm :model="replyForm" layout="vertical">
|
||||
<AFormItem label="回复内容" required>
|
||||
<ATextarea v-model="replyForm.content" placeholder="请输入回复内容" :auto-size="{ minRows: 4, maxRows: 8 }" />
|
||||
</AFormItem>
|
||||
|
||||
@@ -81,7 +81,7 @@ onMounted(() => {
|
||||
<div class="settings-content">
|
||||
<ACard :bordered="false">
|
||||
<ASpin :loading="loading">
|
||||
<component :is="categoryMap[activeTab]" v-if="categoryMap[activeTab]" v-model="formValues" />
|
||||
<component :is="categoryMap[activeTab]" v-if="categoryMap[activeTab]" />
|
||||
|
||||
<div class="settings-footer">
|
||||
<AButton type="primary" :loading="saving" @click="save">保存设置</AButton>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const values = defineModel<Record<string, any>>({ required: true })
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const values = defineModel<Record<string, any>>({ required: true })
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const values = defineModel<Record<string, any>>({ required: true })
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
const values = defineModel<Record<string, any>>({ required: true })
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
|
||||
function num(key: string) {
|
||||
return Number(values.value[key]) || 0
|
||||
return Number(values[key]) || 0
|
||||
}
|
||||
function set(key: string, v: number | undefined): void {
|
||||
values.value[key] = String(v ?? 0)
|
||||
values[key] = String(v ?? 0)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const values = defineModel<Record<string, any>>({ required: true })
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const values = defineModel<Record<string, any>>({ required: true })
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const values = defineModel<Record<string, any>>({ required: true })
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
const values = defineModel<Record<string, any>>({ required: true })
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
|
||||
function syncRate(direction: 'cny_to_usd' | 'usd_to_cny') {
|
||||
if (direction === 'cny_to_usd') {
|
||||
const usdToCny = parseFloat(values.value['payment_exchange_rate_usd_to_cny'])
|
||||
const usdToCny = parseFloat(values['payment_exchange_rate_usd_to_cny'])
|
||||
if (usdToCny > 0) {
|
||||
values.value['payment_exchange_rate_cny_to_usd'] = String(Math.round((1 / usdToCny) * 10000) / 10000)
|
||||
values['payment_exchange_rate_cny_to_usd'] = String(Math.round((1 / usdToCny) * 10000) / 10000)
|
||||
}
|
||||
} else {
|
||||
const cnyToUsd = parseFloat(values.value['payment_exchange_rate_cny_to_usd'])
|
||||
const cnyToUsd = parseFloat(values['payment_exchange_rate_cny_to_usd'])
|
||||
if (cnyToUsd > 0) {
|
||||
values.value['payment_exchange_rate_usd_to_cny'] = String(Math.round((1 / cnyToUsd) * 10000) / 10000)
|
||||
values['payment_exchange_rate_usd_to_cny'] = String(Math.round((1 / cnyToUsd) * 10000) / 10000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
const values = defineModel<Record<string, any>>({ required: true })
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
|
||||
function num(key: string) {
|
||||
return Number(values.value[key]) || 0
|
||||
return Number(values[key]) || 0
|
||||
}
|
||||
function set(key: string, v: number | undefined): void {
|
||||
values.value[key] = String(v ?? 0)
|
||||
values[key] = String(v ?? 0)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,105 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const values = defineModel<Record<string, any>>({ required: true })
|
||||
|
||||
// 2FA state
|
||||
const totpEnabled = ref(false)
|
||||
const setupLoading = ref(false)
|
||||
const enableLoading = ref(false)
|
||||
const showSetupModal = ref(false)
|
||||
const showBackupCodesModal = ref(false)
|
||||
const showDisableModal = ref(false)
|
||||
const showRegenerateModal = ref(false)
|
||||
const setupStep = ref(1)
|
||||
const setupSecret = ref('')
|
||||
const qrCodeUrl = ref('')
|
||||
const backupCodes = ref<string[]>([])
|
||||
|
||||
const enableForm = reactive({ code: '', password: '' })
|
||||
const disableForm = reactive({ code: '' })
|
||||
const regenerateCode = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
check2FAStatus()
|
||||
})
|
||||
|
||||
async function check2FAStatus() {
|
||||
try {
|
||||
const profileRes = await request.get('/api/admin/users/profile')
|
||||
if (profileRes.data?.code === 0) {
|
||||
totpEnabled.value = profileRes.data.data?.totp_enabled || false
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSetup() {
|
||||
setupLoading.value = true
|
||||
try {
|
||||
const res = await request.post('/api/admin/security/2fa/setup')
|
||||
setupSecret.value = res.data.data.secret
|
||||
qrCodeUrl.value = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(res.data.data.uri)}`
|
||||
setupStep.value = 1
|
||||
showSetupModal.value = true
|
||||
} catch {
|
||||
// error toast already shown by interceptor
|
||||
} finally {
|
||||
setupLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEnable() {
|
||||
enableLoading.value = true
|
||||
try {
|
||||
const res = await request.post('/api/admin/security/2fa/enable', enableForm)
|
||||
totpEnabled.value = true
|
||||
showSetupModal.value = false
|
||||
backupCodes.value = res.data.data.backup_codes || []
|
||||
showBackupCodesModal.value = true
|
||||
Message.success('双因素认证已启用')
|
||||
} catch {
|
||||
// error toast already shown by interceptor
|
||||
} finally {
|
||||
enableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDisable() {
|
||||
if (!disableForm.code) {
|
||||
Message.warning('请输入验证码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await request.post('/api/admin/security/2fa/disable', disableForm)
|
||||
totpEnabled.value = false
|
||||
showDisableModal.value = false
|
||||
disableForm.code = ''
|
||||
Message.success('双因素认证已禁用')
|
||||
} catch {
|
||||
// error toast already shown by interceptor
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegenerate() {
|
||||
if (!regenerateCode.value) {
|
||||
Message.warning('请输入验证码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await request.post('/api/admin/security/2fa/backup-codes', { code: regenerateCode.value })
|
||||
regenerateCode.value = ''
|
||||
showRegenerateModal.value = false
|
||||
backupCodes.value = res.data.data.backup_codes || []
|
||||
showBackupCodesModal.value = true
|
||||
Message.success('恢复码已重新生成')
|
||||
} catch {
|
||||
// error toast already shown by interceptor
|
||||
}
|
||||
}
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -210,77 +111,6 @@ async function handleRegenerate() {
|
||||
</AFormItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 双因素认证 -->
|
||||
<div class="section">
|
||||
<div class="section-title">双因素认证(2FA)</div>
|
||||
<template v-if="!totpEnabled">
|
||||
<div class="section-desc">启用双因素认证后,登录时需要输入动态验证码,大幅提升账号安全性。</div>
|
||||
<AButton type="primary" @click="handleSetup" :loading="setupLoading">开启 2FA</AButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<AAlert type="success" class="mb-4">已启用双因素认证</AAlert>
|
||||
<ASpace>
|
||||
<AButton @click="showRegenerateModal = true">重新生成恢复码</AButton>
|
||||
<AButton status="danger" @click="showDisableModal = true">禁用 2FA</AButton>
|
||||
</ASpace>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 2FA Setup Modal -->
|
||||
<AModal v-model:visible="showSetupModal" title="设置双因素认证" :maskClosable="false" :footer="false" width="480px">
|
||||
<div v-if="setupStep === 1">
|
||||
<AAlert type="info" class="mb-4">请使用身份验证器 App(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码。</AAlert>
|
||||
<div class="qr-container">
|
||||
<img :src="qrCodeUrl" alt="QR Code" v-if="qrCodeUrl" class="qr-image" />
|
||||
</div>
|
||||
<div class="secret-key">
|
||||
<span class="label">手动输入密钥:</span>
|
||||
<ATypographyText copyable code>{{ setupSecret }}</ATypographyText>
|
||||
</div>
|
||||
<AButton type="primary" long @click="setupStep = 2" class="mt-4">我已扫描,下一步</AButton>
|
||||
</div>
|
||||
<div v-if="setupStep === 2">
|
||||
<AForm :model="enableForm" @submit-success="handleEnable" layout="vertical">
|
||||
<AFormItem label="输入验证码" field="code" :rules="[{ required: true, message: '请输入6位验证码' }, { length: 6, message: '验证码为6位' }]">
|
||||
<AInput v-model="enableForm.code" placeholder="6位数字验证码" maxlength="6" />
|
||||
</AFormItem>
|
||||
<AFormItem label="当前密码" field="password" :rules="[{ required: true, message: '请输入密码' }]">
|
||||
<AInputPassword v-model="enableForm.password" placeholder="请输入当前密码以确认" />
|
||||
</AFormItem>
|
||||
<AButton type="primary" html-type="submit" long :loading="enableLoading">确认启用</AButton>
|
||||
</AForm>
|
||||
</div>
|
||||
</AModal>
|
||||
|
||||
<!-- Backup Codes Modal -->
|
||||
<AModal v-model:visible="showBackupCodesModal" title="备用恢复码" :maskClosable="false" width="400px">
|
||||
<AAlert type="warning" class="mb-4">请妥善保存以下恢复码。每个恢复码只能使用一次。关闭后无法再次查看。</AAlert>
|
||||
<div class="backup-codes-grid">
|
||||
<ATag v-for="code in backupCodes" :key="code" size="large" class="backup-code">{{ code }}</ATag>
|
||||
</div>
|
||||
<template #footer>
|
||||
<AButton type="primary" @click="showBackupCodesModal = false">我已保存</AButton>
|
||||
</template>
|
||||
</AModal>
|
||||
|
||||
<!-- Disable Modal -->
|
||||
<AModal v-model:visible="showDisableModal" title="禁用双因素认证" @ok="handleDisable" :maskClosable="false">
|
||||
<AForm :model="disableForm" layout="vertical">
|
||||
<AFormItem label="输入验证码或恢复码">
|
||||
<AInput v-model="disableForm.code" placeholder="6位验证码或8位恢复码" />
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
</AModal>
|
||||
|
||||
<!-- Regenerate Backup Codes Modal -->
|
||||
<AModal v-model:visible="showRegenerateModal" title="重新生成恢复码" @ok="handleRegenerate" :maskClosable="false">
|
||||
<AForm layout="vertical">
|
||||
<AFormItem label="输入验证码">
|
||||
<AInput v-model="regenerateCode" placeholder="6位数字验证码" maxlength="6" />
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
</AModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -291,37 +121,4 @@ async function handleRegenerate() {
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.qr-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
.qr-image {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
.secret-key {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.secret-key .label {
|
||||
color: var(--color-text-2);
|
||||
margin-right: 8px;
|
||||
}
|
||||
.backup-codes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.backup-code {
|
||||
font-family: monospace;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
const values = defineModel<Record<string, any>>({ required: true })
|
||||
import { useFormValues } from './useSettings'
|
||||
const values = useFormValues()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive, provide, inject, type InjectionKey } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const settingsFormKey: InjectionKey<Record<string, any>> = Symbol('settingsFormValues')
|
||||
|
||||
export function useSettings(category: () => string) {
|
||||
const formValues = ref<Record<string, any>>({})
|
||||
const formValues = reactive<Record<string, any>>({})
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
provide(settingsFormKey, formValues)
|
||||
|
||||
async function refresh() {
|
||||
if (!category()) return
|
||||
loading.value = true
|
||||
@@ -22,9 +26,10 @@ export function useSettings(category: () => string) {
|
||||
}
|
||||
vals[item.key] = val
|
||||
}
|
||||
formValues.value = vals
|
||||
Object.keys(formValues).forEach(k => delete formValues[k])
|
||||
Object.assign(formValues, vals)
|
||||
} catch {
|
||||
formValues.value = {}
|
||||
Object.keys(formValues).forEach(k => delete formValues[k])
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -33,7 +38,7 @@ export function useSettings(category: () => string) {
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
await request.put(`/admin/settings/${category()}`, { settings: formValues.value })
|
||||
await request.put(`/admin/settings/${category()}`, { settings: formValues })
|
||||
Message.success('保存成功')
|
||||
await refresh()
|
||||
} catch {
|
||||
@@ -45,3 +50,7 @@ export function useSettings(category: () => string) {
|
||||
|
||||
return { formValues, loading, saving, refresh, save }
|
||||
}
|
||||
|
||||
export function useFormValues() {
|
||||
return inject(settingsFormKey)!
|
||||
}
|
||||
Reference in new issue
Block a user