fix frontend review followups and pin deploy image tags

This commit is contained in:
2026-04-23 21:46:04 +08:00
parent fa0aacc559
commit ec58da94cd
14 changed files with 425 additions and 70 deletions

View File

@@ -6,9 +6,9 @@
# Stage 3: Final minimal image
# =============================================================================
ARG NODE_IMAGE=node:24-alpine
ARG GOLANG_IMAGE=golang:1.26.2-alpine
ARG ALPINE_IMAGE=alpine:3.20
ARG NODE_IMAGE=node:24.14.0-alpine
ARG GOLANG_IMAGE=golang:1.24.2-alpine
ARG ALPINE_IMAGE=alpine:3.21.3
ARG GOPROXY=https://goproxy.cn,direct
ARG GOSUMDB=sum.golang.google.cn

View File

@@ -901,6 +901,16 @@
@confirm="handleMixedChannelConfirm"
@cancel="handleMixedChannelCancel"
/>
<ConfirmDialog
:show="showErrorCodeWarningDialog"
:title="t('common.confirm')"
:message="errorCodeWarningMessage"
:confirm-text="t('common.confirm')"
:cancel-text="t('common.cancel')"
:danger="true"
@confirm="confirmErrorCodeWarning"
@cancel="clearErrorCodeWarningDialog"
/>
</template>
<script setup lang="ts">
@@ -1018,6 +1028,9 @@ const submitting = ref(false)
const showMixedChannelWarning = ref(false)
const mixedChannelWarningMessage = ref('')
const pendingUpdatesForConfirm = ref<Record<string, unknown> | null>(null)
const showErrorCodeWarningDialog = ref(false)
const pendingErrorCodeWarningAction = ref<(() => void) | null>(null)
const pendingErrorCodeWarningCode = ref<number | null>(null)
const baseUrl = ref('')
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
const allowedModels = ref<string[]>([])
@@ -1093,19 +1106,45 @@ const addPresetMapping = (from: string, to: string) => {
modelMappings.value.push({ from, to })
}
const errorCodeWarningMessage = computed(() => {
if (pendingErrorCodeWarningCode.value === 429) {
return t('admin.accounts.customErrorCodes429Warning')
}
if (pendingErrorCodeWarningCode.value === 529) {
return t('admin.accounts.customErrorCodes529Warning')
}
return ''
})
const clearErrorCodeWarningDialog = () => {
showErrorCodeWarningDialog.value = false
pendingErrorCodeWarningAction.value = null
pendingErrorCodeWarningCode.value = null
}
const requestErrorCodeWarning = (code: number, action: () => void) => {
pendingErrorCodeWarningCode.value = code
pendingErrorCodeWarningAction.value = action
showErrorCodeWarningDialog.value = true
}
const confirmErrorCodeWarning = () => {
const action = pendingErrorCodeWarningAction.value
clearErrorCodeWarningDialog()
action?.()
}
// Error code helpers
const toggleErrorCode = (code: number) => {
const index = selectedErrorCodes.value.indexOf(code)
if (index === -1) {
// Adding code - check for 429/529 warning
if (code === 429) {
if (!confirm(t('admin.accounts.customErrorCodes429Warning'))) {
return
}
requestErrorCodeWarning(429, () => selectedErrorCodes.value.push(code))
return
} else if (code === 529) {
if (!confirm(t('admin.accounts.customErrorCodes529Warning'))) {
return
}
requestErrorCodeWarning(529, () => selectedErrorCodes.value.push(code))
return
}
selectedErrorCodes.value.push(code)
} else {
@@ -1125,13 +1164,17 @@ const addCustomErrorCode = () => {
}
// Check for 429/529 warning
if (code === 429) {
if (!confirm(t('admin.accounts.customErrorCodes429Warning'))) {
return
}
requestErrorCodeWarning(429, () => {
selectedErrorCodes.value.push(code)
customErrorCodeInput.value = null
})
return
} else if (code === 529) {
if (!confirm(t('admin.accounts.customErrorCodes529Warning'))) {
return
}
requestErrorCodeWarning(529, () => {
selectedErrorCodes.value.push(code)
customErrorCodeInput.value = null
})
return
}
selectedErrorCodes.value.push(code)
customErrorCodeInput.value = null
@@ -1297,6 +1340,7 @@ const handleClose = () => {
mixedChannelWarningMessage.value = ''
pendingUpdatesForConfirm.value = null
mixedChannelConfirmed.value = false
clearErrorCodeWarningDialog()
emit('close')
}
@@ -1465,6 +1509,7 @@ watch(
mixedChannelWarningMessage.value = ''
pendingUpdatesForConfirm.value = null
mixedChannelConfirmed.value = false
clearErrorCodeWarningDialog()
}
}
)

View File

@@ -2792,6 +2792,16 @@
@confirm="handleMixedChannelConfirm"
@cancel="handleMixedChannelCancel"
/>
<ConfirmDialog
:show="showErrorCodeWarningDialog"
:title="t('common.confirm')"
:message="errorCodeWarningMessage"
:confirm-text="t('common.confirm')"
:cancel-text="t('common.cancel')"
:danger="true"
@confirm="confirmErrorCodeWarning"
@cancel="clearErrorCodeWarningDialog"
/>
</template>
<script setup lang="ts">
@@ -3020,6 +3030,9 @@ const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: str
)
const mixedChannelWarningRawMessage = ref('')
const mixedChannelWarningAction = ref<(() => Promise<void>) | null>(null)
const showErrorCodeWarningDialog = ref(false)
const pendingErrorCodeWarningAction = ref<(() => void) | null>(null)
const pendingErrorCodeWarningCode = ref<number | null>(null)
const antigravityMixedChannelConfirmed = ref(false)
const showAdvancedOAuth = ref(false)
const showGeminiHelpDialog = ref(false)
@@ -3406,19 +3419,45 @@ const addAntigravityPresetMapping = (from: string, to: string) => {
antigravityModelMappings.value.push({ from, to })
}
const errorCodeWarningMessage = computed(() => {
if (pendingErrorCodeWarningCode.value === 429) {
return t('admin.accounts.customErrorCodes429Warning')
}
if (pendingErrorCodeWarningCode.value === 529) {
return t('admin.accounts.customErrorCodes529Warning')
}
return ''
})
const clearErrorCodeWarningDialog = () => {
showErrorCodeWarningDialog.value = false
pendingErrorCodeWarningAction.value = null
pendingErrorCodeWarningCode.value = null
}
const requestErrorCodeWarning = (code: number, action: () => void) => {
pendingErrorCodeWarningCode.value = code
pendingErrorCodeWarningAction.value = action
showErrorCodeWarningDialog.value = true
}
const confirmErrorCodeWarning = () => {
const action = pendingErrorCodeWarningAction.value
clearErrorCodeWarningDialog()
action?.()
}
// Error code toggle helper
const toggleErrorCode = (code: number) => {
const index = selectedErrorCodes.value.indexOf(code)
if (index === -1) {
// Adding code - check for 429/529 warning
if (code === 429) {
if (!confirm(t('admin.accounts.customErrorCodes429Warning'))) {
return
}
requestErrorCodeWarning(429, () => selectedErrorCodes.value.push(code))
return
} else if (code === 529) {
if (!confirm(t('admin.accounts.customErrorCodes529Warning'))) {
return
}
requestErrorCodeWarning(529, () => selectedErrorCodes.value.push(code))
return
}
selectedErrorCodes.value.push(code)
} else {
@@ -3439,13 +3478,17 @@ const addCustomErrorCode = () => {
}
// Check for 429/529 warning
if (code === 429) {
if (!confirm(t('admin.accounts.customErrorCodes429Warning'))) {
return
}
requestErrorCodeWarning(429, () => {
selectedErrorCodes.value.push(code)
customErrorCodeInput.value = null
})
return
} else if (code === 529) {
if (!confirm(t('admin.accounts.customErrorCodes529Warning'))) {
return
}
requestErrorCodeWarning(529, () => {
selectedErrorCodes.value.push(code)
customErrorCodeInput.value = null
})
return
}
selectedErrorCodes.value.push(code)
customErrorCodeInput.value = null
@@ -3561,6 +3604,7 @@ const clearMixedChannelDialog = () => {
mixedChannelWarningDetails.value = null
mixedChannelWarningRawMessage.value = ''
mixedChannelWarningAction.value = null
clearErrorCodeWarningDialog()
}
const openMixedChannelDialog = (opts: {

View File

@@ -1743,6 +1743,16 @@
@confirm="handleMixedChannelConfirm"
@cancel="handleMixedChannelCancel"
/>
<ConfirmDialog
:show="showErrorCodeWarningDialog"
:title="t('common.confirm')"
:message="errorCodeWarningMessage"
:confirm-text="t('common.confirm')"
:cancel-text="t('common.cancel')"
:danger="true"
@confirm="confirmErrorCodeWarning"
@cancel="clearErrorCodeWarningDialog"
/>
</template>
<script setup lang="ts">
@@ -1864,6 +1874,9 @@ const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: str
)
const mixedChannelWarningRawMessage = ref('')
const mixedChannelWarningAction = ref<(() => Promise<void>) | null>(null)
const showErrorCodeWarningDialog = ref(false)
const pendingErrorCodeWarningAction = ref<(() => void) | null>(null)
const pendingErrorCodeWarningCode = ref<number | null>(null)
const antigravityMixedChannelConfirmed = ref(false)
// Quota control state (Anthropic OAuth/SetupToken only)
@@ -2353,19 +2366,45 @@ const addAntigravityPresetMapping = (from: string, to: string) => {
antigravityModelMappings.value.push({ from, to })
}
const errorCodeWarningMessage = computed(() => {
if (pendingErrorCodeWarningCode.value === 429) {
return t('admin.accounts.customErrorCodes429Warning')
}
if (pendingErrorCodeWarningCode.value === 529) {
return t('admin.accounts.customErrorCodes529Warning')
}
return ''
})
const clearErrorCodeWarningDialog = () => {
showErrorCodeWarningDialog.value = false
pendingErrorCodeWarningAction.value = null
pendingErrorCodeWarningCode.value = null
}
const requestErrorCodeWarning = (code: number, action: () => void) => {
pendingErrorCodeWarningCode.value = code
pendingErrorCodeWarningAction.value = action
showErrorCodeWarningDialog.value = true
}
const confirmErrorCodeWarning = () => {
const action = pendingErrorCodeWarningAction.value
clearErrorCodeWarningDialog()
action?.()
}
// Error code toggle helper
const toggleErrorCode = (code: number) => {
const index = selectedErrorCodes.value.indexOf(code)
if (index === -1) {
// Adding code - check for 429/529 warning
if (code === 429) {
if (!confirm(t('admin.accounts.customErrorCodes429Warning'))) {
return
}
requestErrorCodeWarning(429, () => selectedErrorCodes.value.push(code))
return
} else if (code === 529) {
if (!confirm(t('admin.accounts.customErrorCodes529Warning'))) {
return
}
requestErrorCodeWarning(529, () => selectedErrorCodes.value.push(code))
return
}
selectedErrorCodes.value.push(code)
} else {
@@ -2386,13 +2425,17 @@ const addCustomErrorCode = () => {
}
// Check for 429/529 warning
if (code === 429) {
if (!confirm(t('admin.accounts.customErrorCodes429Warning'))) {
return
}
requestErrorCodeWarning(429, () => {
selectedErrorCodes.value.push(code)
customErrorCodeInput.value = null
})
return
} else if (code === 529) {
if (!confirm(t('admin.accounts.customErrorCodes529Warning'))) {
return
}
requestErrorCodeWarning(529, () => {
selectedErrorCodes.value.push(code)
customErrorCodeInput.value = null
})
return
}
selectedErrorCodes.value.push(code)
customErrorCodeInput.value = null
@@ -2624,6 +2667,7 @@ const clearMixedChannelDialog = () => {
mixedChannelWarningDetails.value = null
mixedChannelWarningRawMessage.value = ''
mixedChannelWarningAction.value = null
clearErrorCodeWarningDialog()
}
const openMixedChannelDialog = (opts: {

View File

@@ -67,7 +67,7 @@
<button
class="sora-gallery-card-action"
title="删除"
@click.stop="handleDelete(item.id)"
@click.stop="requestDelete(item.id)"
>
🗑
</button>
@@ -119,6 +119,16 @@
@save="handleSaveFromPreview"
@download="handleDownloadUrl"
/>
<ConfirmDialog
:show="showDeleteConfirmDialog"
:title="t('common.delete')"
:message="t('sora.confirmDelete')"
:confirm-text="t('common.delete')"
:cancel-text="t('common.cancel')"
danger
@confirm="confirmDelete"
@cancel="closeDeleteConfirmDialog"
/>
</div>
</template>
@@ -127,6 +137,7 @@ import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import soraAPI, { type SoraGeneration } from '@/api/sora'
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import SoraMediaPreview from './SoraMediaPreview.vue'
const emit = defineEmits<{
@@ -142,6 +153,8 @@ const hasMore = ref(true)
const activeFilter = ref('all')
const previewVisible = ref(false)
const previewItem = ref<SoraGeneration | null>(null)
const showDeleteConfirmDialog = ref(false)
const pendingDeleteId = ref<number | null>(null)
const filters = computed(() => [
{ value: 'all', label: t('sora.filterAll') },
@@ -217,8 +230,17 @@ function openPreview(item: SoraGeneration) {
previewVisible.value = true
}
function closeDeleteConfirmDialog() {
showDeleteConfirmDialog.value = false
pendingDeleteId.value = null
}
function requestDelete(id: number) {
pendingDeleteId.value = id
showDeleteConfirmDialog.value = true
}
async function handleDelete(id: number) {
if (!confirm(t('sora.confirmDelete'))) return
try {
await soraAPI.deleteGeneration(id)
items.value = items.value.filter(i => i.id !== id)
@@ -227,6 +249,13 @@ async function handleDelete(id: number) {
}
}
async function confirmDelete() {
const id = pendingDeleteId.value
closeDeleteConfirmDialog()
if (id == null) return
await handleDelete(id)
}
function handleDownload(item: SoraGeneration) {
if (item.media_url) {
window.open(item.media_url, '_blank')

View File

@@ -382,11 +382,11 @@ export default {
passwordLabel: 'Password',
passwordPlaceholder: 'Enter your password',
createPasswordPlaceholder: 'Create a strong password',
passwordHint: 'At least 6 characters',
passwordHint: 'At least 8 characters',
emailRequired: 'Email is required',
invalidEmail: 'Please enter a valid email address',
passwordRequired: 'Password is required',
passwordMinLength: 'Password must be at least 6 characters',
passwordMinLength: 'Password must be at least 8 characters',
loginFailed: 'Login failed. Please check your credentials and try again.',
registrationFailed: 'Registration failed. Please try again.',
emailSuffixNotAllowed: 'This email domain is not allowed for registration.',

View File

@@ -382,11 +382,11 @@ export default {
passwordLabel: '密码',
passwordPlaceholder: '请输入密码',
createPasswordPlaceholder: '创建一个安全的密码',
passwordHint: '至少 6 个字符',
passwordHint: '至少 8 个字符',
emailRequired: '请输入邮箱',
invalidEmail: '请输入有效的邮箱地址',
passwordRequired: '请输入密码',
passwordMinLength: '密码至少需要 6 个字符',
passwordMinLength: '密码至少需要 8 个字符',
loginFailed: '登录失败,请检查您的凭据后重试。',
registrationFailed: '注册失败,请重试。',
emailSuffixNotAllowed: '该邮箱域名不在允许注册范围内。',

View File

@@ -300,6 +300,16 @@
<span>{{ t('admin.accounts.dataExportIncludeProxies') }}</span>
</label>
</ConfirmDialog>
<ConfirmDialog
:show="showBulkActionConfirmDialog"
:title="bulkActionConfirmTitle"
:message="t('common.confirm')"
:confirm-text="bulkActionConfirmText"
:cancel-text="t('common.cancel')"
:danger="pendingBulkAction === 'delete'"
@confirm="confirmBulkAction"
@cancel="closeBulkActionConfirmDialog"
/>
<ErrorPassthroughRulesModal :show="showErrorPassthrough" @close="showErrorPassthrough = false" />
<TLSFingerprintProfilesModal :show="showTLSFingerprintProfiles" @close="showTLSFingerprintProfiles = false" />
</AppLayout>
@@ -376,6 +386,7 @@ const includeProxyOnExport = ref(true)
const showBulkEdit = ref(false)
const showTempUnsched = ref(false)
const showDeleteDialog = ref(false)
const showBulkActionConfirmDialog = ref(false)
const showReAuth = ref(false)
const showTest = ref(false)
const showStats = ref(false)
@@ -387,6 +398,7 @@ const deletingAcc = ref<Account | null>(null)
const reAuthAcc = ref<Account | null>(null)
const testingAcc = ref<Account | null>(null)
const statsAcc = ref<Account | null>(null)
const pendingBulkAction = ref<'delete' | 'reset-status' | 'refresh-token' | null>(null)
const showSchedulePanel = ref(false)
const scheduleAcc = ref<Account | null>(null)
const scheduleModelOptions = ref<SelectOption[]>([])
@@ -1023,9 +1035,60 @@ const toggleSelectAllVisible = (event: Event) => {
const target = event.target as HTMLInputElement
toggleVisible(target.checked)
}
const handleBulkDelete = async () => { if(!confirm(t('common.confirm'))) return; try { await Promise.all(selIds.value.map(id => adminAPI.accounts.delete(id))); clearSelection(); reload() } catch (error) { console.error('Failed to bulk delete accounts:', error) } }
const handleBulkResetStatus = async () => {
if (!confirm(t('common.confirm'))) return
const bulkActionConfirmTitle = computed(() => {
switch (pendingBulkAction.value) {
case 'delete':
return t('common.delete')
case 'reset-status':
return t('admin.accounts.bulkActions.resetStatus')
case 'refresh-token':
return t('admin.accounts.bulkActions.refreshToken')
default:
return t('common.confirm')
}
})
const bulkActionConfirmText = computed(() =>
pendingBulkAction.value === 'delete' ? t('common.delete') : t('common.confirm')
)
const closeBulkActionConfirmDialog = () => {
showBulkActionConfirmDialog.value = false
pendingBulkAction.value = null
}
const openBulkActionConfirmDialog = (action: 'delete' | 'reset-status' | 'refresh-token') => {
pendingBulkAction.value = action
showBulkActionConfirmDialog.value = true
}
const confirmBulkAction = async () => {
const action = pendingBulkAction.value
closeBulkActionConfirmDialog()
if (action === 'delete') {
await performBulkDelete()
return
}
if (action === 'reset-status') {
await performBulkResetStatus()
return
}
if (action === 'refresh-token') {
await performBulkRefreshToken()
}
}
const handleBulkDelete = () => {
openBulkActionConfirmDialog('delete')
}
const performBulkDelete = async () => {
try {
await Promise.all(selIds.value.map(id => adminAPI.accounts.delete(id)))
clearSelection()
reload()
} catch (error) {
console.error('Failed to bulk delete accounts:', error)
}
}
const handleBulkResetStatus = () => {
openBulkActionConfirmDialog('reset-status')
}
const performBulkResetStatus = async () => {
try {
const result = await adminAPI.accounts.batchClearError(selIds.value)
if (result.failed > 0) {
@@ -1040,8 +1103,10 @@ const handleBulkResetStatus = async () => {
appStore.showError(String(error))
}
}
const handleBulkRefreshToken = async () => {
if (!confirm(t('common.confirm'))) return
const handleBulkRefreshToken = () => {
openBulkActionConfirmDialog('refresh-token')
}
const performBulkRefreshToken = async () => {
try {
const result = await adminAPI.accounts.batchRefresh(selIds.value)
if (result.failed > 0) {

View File

@@ -168,14 +168,14 @@
type="button"
class="btn btn-secondary btn-xs"
:disabled="restoringId === record.id"
@click="restoreBackup(record.id)"
@click="requestRestoreBackup(record.id)"
>
{{ restoringId === record.id ? t('common.loading') : t('admin.backup.actions.restore') }}
</button>
<button
type="button"
class="btn btn-danger btn-xs"
@click="removeBackup(record.id)"
@click="requestRemoveBackup(record.id)"
>
{{ t('common.delete') }}
</button>
@@ -193,6 +193,17 @@
</div>
</div>
<ConfirmDialog
:show="showBackupConfirmDialog"
:title="pendingBackupAction === 'restore' ? t('admin.backup.actions.restore') : t('common.delete')"
:message="pendingBackupAction === 'restore' ? t('admin.backup.actions.restoreConfirm') : t('admin.backup.actions.deleteConfirm')"
:confirm-text="pendingBackupAction === 'restore' ? t('admin.backup.actions.restore') : t('common.delete')"
:cancel-text="t('common.cancel')"
:danger="pendingBackupAction === 'delete'"
@confirm="confirmBackupAction"
@cancel="closeBackupConfirmDialog"
/>
<!-- Cloudflare R2 Setup Guide Modal -->
<teleport to="body">
<transition name="modal">
@@ -284,6 +295,7 @@ import { useI18n } from 'vue-i18n'
import { adminAPI } from '@/api'
import { useAppStore } from '@/stores'
import type { BackupS3Config, BackupScheduleConfig, BackupRecord } from '@/api/admin/backup'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
const { t } = useI18n()
const appStore = useAppStore()
@@ -317,6 +329,9 @@ const loadingBackups = ref(false)
const creatingBackup = ref(false)
const restoringId = ref('')
const manualExpireDays = ref(14)
const showBackupConfirmDialog = ref(false)
const pendingBackupAction = ref<'restore' | 'delete' | null>(null)
const pendingBackupId = ref('')
// Polling
const pollingTimer = ref<ReturnType<typeof setInterval> | null>(null)
@@ -546,8 +561,37 @@ async function downloadBackup(id: string) {
}
}
function openBackupConfirmDialog(action: 'restore' | 'delete', id: string) {
pendingBackupAction.value = action
pendingBackupId.value = id
showBackupConfirmDialog.value = true
}
function closeBackupConfirmDialog() {
showBackupConfirmDialog.value = false
pendingBackupAction.value = null
pendingBackupId.value = ''
}
async function confirmBackupAction() {
const action = pendingBackupAction.value
const id = pendingBackupId.value
closeBackupConfirmDialog()
if (!id) return
if (action === 'restore') {
await restoreBackup(id)
return
}
if (action === 'delete') {
await removeBackup(id)
}
}
function requestRestoreBackup(id: string) {
openBackupConfirmDialog('restore', id)
}
async function restoreBackup(id: string) {
if (!window.confirm(t('admin.backup.actions.restoreConfirm'))) return
const password = window.prompt(t('admin.backup.actions.restorePasswordPrompt'))
if (!password) return
restoringId.value = id
@@ -565,8 +609,11 @@ async function restoreBackup(id: string) {
}
}
function requestRemoveBackup(id: string) {
openBackupConfirmDialog('delete', id)
}
async function removeBackup(id: string) {
if (!window.confirm(t('admin.backup.actions.deleteConfirm'))) return
try {
await adminAPI.backup.deleteBackup(id)
appStore.showSuccess(t('admin.backup.actions.deleted'))

View File

@@ -2511,6 +2511,16 @@
@close="showProviderDialog = false"
@save="handleSaveProvider"
/>
<ConfirmDialog
:show="showAdminApiKeyConfirmDialog"
:title="pendingAdminApiKeyAction === 'regenerate' ? t('admin.settings.adminApiKey.regenerate') : t('admin.settings.adminApiKey.delete')"
:message="pendingAdminApiKeyAction === 'regenerate' ? t('admin.settings.adminApiKey.regenerateConfirm') : t('admin.settings.adminApiKey.deleteConfirm')"
:confirm-text="pendingAdminApiKeyAction === 'regenerate' ? t('admin.settings.adminApiKey.regenerate') : t('common.delete')"
:cancel-text="t('common.cancel')"
:danger="pendingAdminApiKeyAction === 'delete'"
@confirm="confirmAdminApiKeyAction"
@cancel="closeAdminApiKeyConfirmDialog"
/>
<ConfirmDialog :show="showDeleteProviderDialog" :title="t('admin.settings.payment.deleteProvider')" :message="t('admin.settings.payment.deleteProviderConfirm')" :confirm-text="t('common.delete')" danger @confirm="handleDeleteProvider" @cancel="showDeleteProviderDialog = false" />
</div>
</AppLayout>
@@ -2583,6 +2593,8 @@ const adminApiKeyExists = ref(false)
const adminApiKeyMasked = ref('')
const adminApiKeyOperating = ref(false)
const newAdminApiKey = ref('')
const showAdminApiKeyConfirmDialog = ref(false)
const pendingAdminApiKeyAction = ref<'regenerate' | 'delete' | null>(null)
const subscriptionGroups = ref<AdminGroup[]>([])
// Overload Cooldown (529) 状态
@@ -3269,13 +3281,33 @@ async function createAdminApiKey() {
}
}
async function regenerateAdminApiKey() {
if (!confirm(t('admin.settings.adminApiKey.regenerateConfirm'))) return
await createAdminApiKey()
function openAdminApiKeyConfirmDialog(action: 'regenerate' | 'delete') {
pendingAdminApiKeyAction.value = action
showAdminApiKeyConfirmDialog.value = true
}
async function deleteAdminApiKey() {
if (!confirm(t('admin.settings.adminApiKey.deleteConfirm'))) return
function closeAdminApiKeyConfirmDialog() {
showAdminApiKeyConfirmDialog.value = false
pendingAdminApiKeyAction.value = null
}
async function confirmAdminApiKeyAction() {
const action = pendingAdminApiKeyAction.value
closeAdminApiKeyConfirmDialog()
if (action === 'regenerate') {
await createAdminApiKey()
return
}
if (action === 'delete') {
await performDeleteAdminApiKey()
}
}
async function regenerateAdminApiKey() {
openAdminApiKeyConfirmDialog('regenerate')
}
async function performDeleteAdminApiKey() {
adminApiKeyOperating.value = true
try {
await adminAPI.settings.deleteAdminApiKey()
@@ -3290,6 +3322,10 @@ async function deleteAdminApiKey() {
}
}
function deleteAdminApiKey() {
openAdminApiKeyConfirmDialog('delete')
}
function copyNewKey() {
navigator.clipboard
.writeText(newAdminApiKey.value)

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { opsAPI, type OpsRuntimeLogConfig, type OpsSystemLog, type OpsSystemLogSinkHealth } from '@/api/admin/ops'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import Pagination from '@/components/common/Pagination.vue'
import Select from '@/components/common/Select.vue'
import { useAppStore } from '@/stores'
@@ -32,6 +33,8 @@ const health = ref<OpsSystemLogSinkHealth>({
const runtimeLoading = ref(false)
const runtimeSaving = ref(false)
const showConfirmDialog = ref(false)
const pendingConfirmAction = ref<'reset-runtime' | 'cleanup' | null>(null)
const runtimeConfig = reactive<OpsRuntimeLogConfig>({
level: 'info',
enable_sampling: false,
@@ -249,10 +252,31 @@ const saveRuntimeConfig = async () => {
}
}
const resetRuntimeConfig = async () => {
const ok = window.confirm('确认回滚为启动配置env/yaml并立即生效')
if (!ok) return
const closeConfirmDialog = () => {
showConfirmDialog.value = false
pendingConfirmAction.value = null
}
const openConfirmDialog = (action: 'reset-runtime' | 'cleanup') => {
pendingConfirmAction.value = action
showConfirmDialog.value = true
}
const confirmDialogTitle = computed(() =>
pendingConfirmAction.value === 'cleanup' ? '清理系统日志' : '回滚运行时配置'
)
const confirmDialogMessage = computed(() =>
pendingConfirmAction.value === 'cleanup'
? '确认按当前筛选条件清理系统日志?该操作不可撤销。'
: '确认回滚为启动配置env/yaml并立即生效'
)
const resetRuntimeConfig = () => {
openConfirmDialog('reset-runtime')
}
const performResetRuntimeConfig = async () => {
runtimeSaving.value = true
try {
const saved = await opsAPI.resetRuntimeLogConfig()
@@ -273,9 +297,11 @@ const resetRuntimeConfig = async () => {
}
}
const cleanupCurrentFilter = async () => {
const ok = window.confirm('确认按当前筛选条件清理系统日志?该操作不可撤销。')
if (!ok) return
const cleanupCurrentFilter = () => {
openConfirmDialog('cleanup')
}
const performCleanupCurrentFilter = async () => {
try {
const payload = {
start_time: toRFC3339(filters.start_time),
@@ -300,6 +326,18 @@ const cleanupCurrentFilter = async () => {
}
}
const confirmDialogAction = async () => {
const action = pendingConfirmAction.value
closeConfirmDialog()
if (action === 'reset-runtime') {
await performResetRuntimeConfig()
return
}
if (action === 'cleanup') {
await performCleanupCurrentFilter()
}
}
const resetFilters = () => {
filters.time_range = '1h'
filters.start_time = ''
@@ -517,4 +555,14 @@ onMounted(async () => {
/>
</div>
</section>
<ConfirmDialog
:show="showConfirmDialog"
:title="confirmDialogTitle"
:message="confirmDialogMessage"
:confirm-text="pendingConfirmAction === 'cleanup' ? '清理' : '回滚'"
cancel-text="取消"
:danger="pendingConfirmAction === 'cleanup'"
@confirm="confirmDialogAction"
@cancel="closeConfirmDialog"
/>
</template>

View File

@@ -317,9 +317,6 @@ function validateForm(): boolean {
if (!formData.password) {
errors.password = t('auth.passwordRequired')
isValid = false
} else if (formData.password.length < 6) {
errors.password = t('auth.passwordMinLength')
isValid = false
}
// Turnstile validation

View File

@@ -629,7 +629,7 @@ function validateForm(): boolean {
if (!formData.password) {
errors.password = t('auth.passwordRequired')
isValid = false
} else if (formData.password.length < 6) {
} else if (formData.password.length < 8) {
errors.password = t('auth.passwordMinLength')
isValid = false
}

View File

@@ -283,7 +283,7 @@ function validateForm(): boolean {
if (!formData.password) {
errors.password = t('auth.passwordRequired')
isValid = false
} else if (formData.password.length < 6) {
} else if (formData.password.length < 8) {
errors.password = t('auth.passwordMinLength')
isValid = false
}