diff --git a/tenant-platform/src/api/migrate.ts b/tenant-platform/src/api/migrate.ts new file mode 100644 index 0000000..e17038c --- /dev/null +++ b/tenant-platform/src/api/migrate.ts @@ -0,0 +1,8 @@ +import client from './client' + +export const migrateApi = { + requestCode: () => client.post('/migrate/request-code'), + + generateKey: (code: string) => + client.post<{ data: { migrationKey: string } }>('/migrate/generate-key', { code }), +} diff --git a/tenant-platform/src/views/security/SecurityCenterView.vue b/tenant-platform/src/views/security/SecurityCenterView.vue index c2684ce..bd13d59 100644 --- a/tenant-platform/src/views/security/SecurityCenterView.vue +++ b/tenant-platform/src/views/security/SecurityCenterView.vue @@ -38,6 +38,14 @@ 重置当前账号密码 + + +

+ 将当前账号及应用数据迁移至私有化部署环境。迁移密钥仅在生成时显示一次,请及时保存。 +

+ 生成迁移密钥 +
+ @@ -82,6 +90,46 @@ + + +
+

点击发送验证码,系统将向您的注册邮箱发送一次性验证码,验证通过后生成迁移密钥。

+ 发送验证码 +
+
+

请输入邮箱验证码:

+ +
+ +
+ + + + + + + +
{{ migrateKeyValue }}
+ + 复制密钥 + + +
+ @@ -101,6 +149,7 @@ import { onMounted, ref } from 'vue' import { ElMessage } from 'element-plus' import { accountApi } from '@/api/account' import { appApi, type App } from '@/api/app' +import { migrateApi } from '@/api/migrate' import { useAuthStore } from '@/stores/auth' const auth = useAuthStore() @@ -179,6 +228,63 @@ function closeDialog() { codeSent.value = false } +// ── Migration ──────────────────────────────────────────────────────────────── + +const showMigrateDialog = ref(false) +const showMigrateKey = ref(false) +const migrateCodeSent = ref(false) +const migrateSendingCode = ref(false) +const migrateSubmitting = ref(false) +const migrateCode = ref('') +const migrateKeyValue = ref('') + +function openMigrateDialog() { + migrateCodeSent.value = false + migrateCode.value = '' + showMigrateDialog.value = true +} + +function closeMigrateDialog() { + migrateCode.value = '' + migrateCodeSent.value = false +} + +async function sendMigrateCode() { + migrateSendingCode.value = true + try { + await migrateApi.requestCode() + migrateCodeSent.value = true + ElMessage.success('验证码已发送到邮箱') + } finally { + migrateSendingCode.value = false + } +} + +async function submitMigrateCode() { + if (!migrateCode.value.trim()) { + ElMessage.warning('请输入验证码') + return + } + migrateSubmitting.value = true + try { + const res = await migrateApi.generateKey(migrateCode.value.trim()) + migrateKeyValue.value = res.data.data.migrationKey + showMigrateDialog.value = false + showMigrateKey.value = true + } finally { + migrateSubmitting.value = false + } +} + +async function copyMigrateKey() { + try { + await navigator.clipboard.writeText(migrateKeyValue.value) + ElMessage.success('已复制到剪贴板') + } catch { + ElMessage.warning('复制失败,请手动选择并复制') + } +} + function fmt(value: string) { return value ? new Date(value).toLocaleString('zh-CN') : '-' } @@ -210,4 +316,15 @@ onMounted(loadData) font-family: monospace; word-break: break-all; } +.migrate-key-box { + font-family: monospace; + font-size: 14px; + word-break: break-all; + background: #f5f7fa; + border: 1px solid #dcdfe6; + border-radius: 4px; + padding: 12px 16px; + line-height: 1.6; + user-select: all; +} diff --git a/tenant-platform/src/views/update/VersionManagementView.vue b/tenant-platform/src/views/update/VersionManagementView.vue index d1e1287..7cc79f8 100644 --- a/tenant-platform/src/views/update/VersionManagementView.vue +++ b/tenant-platform/src/views/update/VersionManagementView.vue @@ -494,9 +494,8 @@ - 手动上架 - 审核完成自动上架 - 定时上架 + 立即上架 + 计划上架 @@ -549,56 +548,6 @@ - - -
-
-

版本 {{ preflightResult.versionName }} 的前置检查结果:

- - - - - - - - - - - - - - - - - - - 跳过厂商状态检查后继续提交(管理员) - -
- - -
@@ -987,7 +936,6 @@ import { type GrayMemberGroup, type GrayMode, type GraySelectionSource, - type PreflightSubmitResultDto, type PublishMode, type RnBundle, type RnBundleInspectResult, @@ -1555,15 +1503,9 @@ const showSubmitStore = ref(false) const submittingToStores = ref(false) const submitStoreVersion = ref(null) const selectedStores = ref([]) -const submitStoreMode = ref('MANUAL') +const submitStoreMode = ref('AUTO_REVIEW') const submitStoreScheduledAt = ref('') -// Preflight dialog state -const showPreflight = ref(false) -const preflightLoading = ref(false) -const preflightResult = ref(null) -const preflightSkipCheck = ref(false) - const showStoreReviewDetail = ref(false) const storeReviewDetailVersion = ref(null) type StoreReviewItem = { @@ -1701,32 +1643,15 @@ async function handleUpdatePublishSchedule() { } } -async function openSubmitStoreDialog(row: AppVersion) { +function openSubmitStoreDialog(row: AppVersion) { submitStoreVersion.value = row - preflightResult.value = null - preflightSkipCheck.value = false - showPreflight.value = true - preflightLoading.value = true - try { - const res = await updateAdminApi.preflightStoreSubmission(row.id) - preflightResult.value = res.data.data - // Auto-open submit dialog if all allowed - const allAllowed = preflightResult.value?.stores.every(s => s.canSubmit) ?? false - if (allAllowed) { - showPreflight.value = false - showSubmitStore.value = true - } - } catch (e: any) { - ElMessage.error(e?.response?.data?.message || '前置检查失败') - } finally { - preflightLoading.value = false - } - // Fallback: pre-select stores selectedStores.value = enabledStores.value .map(s => s.type) .filter(t => !isStoreActiveReview(row, t)) - submitStoreMode.value = row.storeSubmitMode ?? 'MANUAL' + const savedMode = row.storeSubmitMode + submitStoreMode.value = (savedMode === 'SCHEDULED' ? 'SCHEDULED' : 'AUTO_REVIEW') as PublishMode submitStoreScheduledAt.value = row.storeSubmitScheduledAt ?? '' + showSubmitStore.value = true } function getSubmitDialogStoreState(version: AppVersion | null, storeType: string): string { @@ -2301,30 +2226,6 @@ function hasActiveStoreReview(row: AppVersion): boolean { return items.some(i => i.state === 'SUBMITTING' || i.state === 'UNDER_REVIEW') } -function preflightTagType(state: string): string { - return { - ONLINE: 'success', - UNDER_REVIEW: 'warning', - UNDER_REVIEW_XIAOMI: 'warning', - REJECTED: 'danger', - NOT_FOUND: 'info', - UNKNOWN: 'info', - QUERY_FAILED: 'danger', - }[state] ?? '' -} - -function preflightStateLabel(state: string): string { - return { - ONLINE: '已上线', - UNDER_REVIEW: '审核中', - UNDER_REVIEW_XIAOMI: '审核中(版本号可能不准确)', - REJECTED: '已拒绝', - NOT_FOUND: '未找到', - UNKNOWN: '未知', - QUERY_FAILED: '查询失败', - }[state] ?? state -} - async function promptUnpublishRn(id: string) { const reason = await promptUnpublishReason('下架确认') if (!reason) return