mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
Fix system data import export flows
This commit is contained in:
@@ -49,7 +49,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const ADMIN_SYSTEM_IMPORT_MAX_SIZE_BYTES: usize = 10 * 1024 * 1024;
|
const ADMIN_SYSTEM_IMPORT_MAX_SIZE_BYTES: usize = 500 * 1024 * 1024;
|
||||||
|
|
||||||
fn invalid_request(detail: impl Into<String>) -> (http::StatusCode, Value) {
|
fn invalid_request(detail: impl Into<String>) -> (http::StatusCode, Value) {
|
||||||
(
|
(
|
||||||
@@ -173,6 +173,9 @@ fn normalize_import_endpoint_format(value: &str) -> Result<String, String> {
|
|||||||
let normalized = match value.trim().to_ascii_lowercase().as_str() {
|
let normalized = match value.trim().to_ascii_lowercase().as_str() {
|
||||||
"openai:cli" => "openai:responses",
|
"openai:cli" => "openai:responses",
|
||||||
"openai:compact" => "openai:responses:compact",
|
"openai:compact" => "openai:responses:compact",
|
||||||
|
"openai_image" | "images" | "image" | "/v1/images/generations" | "/v1/images/edits" => {
|
||||||
|
"openai:image"
|
||||||
|
}
|
||||||
"claude:chat" | "claude:cli" => "claude:messages",
|
"claude:chat" | "claude:cli" => "claude:messages",
|
||||||
"gemini:chat" | "gemini:cli" => "gemini:generate_content",
|
"gemini:chat" | "gemini:cli" => "gemini:generate_content",
|
||||||
_ => value.trim(),
|
_ => value.trim(),
|
||||||
@@ -956,7 +959,7 @@ impl<'a> AdminAppState<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if request_body.len() > ADMIN_SYSTEM_DATA_IMPORT_MAX_SIZE_BYTES {
|
if request_body.len() > ADMIN_SYSTEM_DATA_IMPORT_MAX_SIZE_BYTES {
|
||||||
return Ok(Err(invalid_request("请求体大小不能超过 20MB")));
|
return Ok(Err(invalid_request("请求体大小不能超过 500MB")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let root = match serde_json::from_slice::<Value>(request_body) {
|
let root = match serde_json::from_slice::<Value>(request_body) {
|
||||||
@@ -1053,7 +1056,7 @@ impl<'a> AdminAppState<'a> {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
if request_body.len() > ADMIN_SYSTEM_IMPORT_MAX_SIZE_BYTES {
|
if request_body.len() > ADMIN_SYSTEM_IMPORT_MAX_SIZE_BYTES {
|
||||||
return Ok(Err(invalid_request("请求体大小不能超过 10MB")));
|
return Ok(Err(invalid_request("请求体大小不能超过 500MB")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed = routed!(parse_admin_system_config_import_request(request_body));
|
let parsed = routed!(parse_admin_system_config_import_request(request_body));
|
||||||
@@ -1993,7 +1996,7 @@ impl<'a> AdminAppState<'a> {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
if request_body.len() > ADMIN_SYSTEM_IMPORT_MAX_SIZE_BYTES {
|
if request_body.len() > ADMIN_SYSTEM_IMPORT_MAX_SIZE_BYTES {
|
||||||
return Ok(Err(invalid_request("请求体大小不能超过 10MB")));
|
return Ok(Err(invalid_request("请求体大小不能超过 500MB")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let root = match serde_json::from_slice::<Value>(request_body) {
|
let root = match serde_json::from_slice::<Value>(request_body) {
|
||||||
@@ -3062,6 +3065,10 @@ mod tests {
|
|||||||
for (raw, expected) in [
|
for (raw, expected) in [
|
||||||
("openai:cli", "openai:responses"),
|
("openai:cli", "openai:responses"),
|
||||||
("openai:compact", "openai:responses:compact"),
|
("openai:compact", "openai:responses:compact"),
|
||||||
|
("openai_image", "openai:image"),
|
||||||
|
("images", "openai:image"),
|
||||||
|
("/v1/images/generations", "openai:image"),
|
||||||
|
("/v1/images/edits", "openai:image"),
|
||||||
("claude:chat", "claude:messages"),
|
("claude:chat", "claude:messages"),
|
||||||
("claude:cli", "claude:messages"),
|
("claude:cli", "claude:messages"),
|
||||||
("gemini:chat", "gemini:generate_content"),
|
("gemini:chat", "gemini:generate_content"),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ mod proxy_nodes;
|
|||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
const ADMIN_SYSTEM_DATA_EXPORT_VERSION: &str = "1.0";
|
const ADMIN_SYSTEM_DATA_EXPORT_VERSION: &str = "1.0";
|
||||||
const ADMIN_SYSTEM_DATA_IMPORT_MAX_SIZE_BYTES: usize = 20 * 1024 * 1024;
|
const ADMIN_SYSTEM_DATA_IMPORT_MAX_SIZE_BYTES: usize = 500 * 1024 * 1024;
|
||||||
|
|
||||||
impl<'a> AdminAppState<'a> {
|
impl<'a> AdminAppState<'a> {
|
||||||
pub(crate) async fn upsert_system_config_json_value(
|
pub(crate) async fn upsert_system_config_json_value(
|
||||||
|
|||||||
@@ -478,6 +478,73 @@ async fn gateway_imports_admin_system_config_locally_and_persists_data() {
|
|||||||
let _ = upstream_url;
|
let _ = upstream_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn gateway_imports_admin_system_config_openai_image_aliases() {
|
||||||
|
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
));
|
||||||
|
let global_model_repository = Arc::new(InMemoryGlobalModelReadRepository::seed(Vec::<
|
||||||
|
StoredPublicGlobalModel,
|
||||||
|
>::new()));
|
||||||
|
let data_state = build_admin_system_data_state_with_repositories(
|
||||||
|
Arc::clone(&provider_catalog_repository),
|
||||||
|
Arc::clone(&global_model_repository),
|
||||||
|
);
|
||||||
|
let gateway = build_router_with_state(
|
||||||
|
AppState::new()
|
||||||
|
.expect("gateway should build")
|
||||||
|
.with_data_state_for_tests(data_state),
|
||||||
|
);
|
||||||
|
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||||
|
|
||||||
|
let mut payload = sample_system_import_payload();
|
||||||
|
payload["providers"][0]["endpoints"][0]["api_format"] = json!("openai_image");
|
||||||
|
payload["providers"][0]["api_keys"][0]["api_formats"] = json!(["images"]);
|
||||||
|
payload["providers"][0]["api_keys"][0]["supported_endpoints"] = json!(["openai:image"]);
|
||||||
|
payload["providers"][0]["models"][0]["supports_image_generation"] = json!(true);
|
||||||
|
|
||||||
|
let response = reqwest::Client::new()
|
||||||
|
.post(format!("{gateway_url}/api/admin/system/config/import"))
|
||||||
|
.header(GATEWAY_HEADER, "rust-phase3b")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||||
|
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||||
|
.json(&payload)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("request should succeed");
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
let body: Value = response.json().await.expect("json body should parse");
|
||||||
|
assert_eq!(status, StatusCode::OK, "payload={body}");
|
||||||
|
assert_eq!(body["stats"]["endpoints"]["created"], json!(1));
|
||||||
|
assert_eq!(body["stats"]["keys"]["created"], json!(1));
|
||||||
|
|
||||||
|
let providers = provider_catalog_repository
|
||||||
|
.list_providers(false)
|
||||||
|
.await
|
||||||
|
.expect("providers should load");
|
||||||
|
let provider_ids = providers
|
||||||
|
.iter()
|
||||||
|
.map(|provider| provider.id.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let endpoints = provider_catalog_repository
|
||||||
|
.list_endpoints_by_provider_ids(&provider_ids)
|
||||||
|
.await
|
||||||
|
.expect("endpoints should load");
|
||||||
|
assert_eq!(endpoints[0].api_format, "openai:image");
|
||||||
|
|
||||||
|
let keys = provider_catalog_repository
|
||||||
|
.list_keys_by_provider_ids(&provider_ids)
|
||||||
|
.await
|
||||||
|
.expect("keys should load");
|
||||||
|
assert_eq!(keys[0].api_formats, Some(json!(["openai:image"])));
|
||||||
|
|
||||||
|
gateway_handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gateway_returns_503_for_admin_system_config_import_when_local_data_is_unavailable() {
|
async fn gateway_returns_503_for_admin_system_config_import_when_local_data_is_unavailable() {
|
||||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||||
|
|||||||
@@ -625,10 +625,18 @@ fn assert_usage_and_pricing(
|
|||||||
expected.cache_read_tokens
|
expected.cache_read_tokens
|
||||||
);
|
);
|
||||||
assert_eq!(stored_usage.total_tokens, expected.total_tokens());
|
assert_eq!(stored_usage.total_tokens, expected.total_tokens());
|
||||||
assert_eq!(
|
if expected_ttfb_ms.is_some() {
|
||||||
stored_usage.response_time_ms,
|
assert!(
|
||||||
Some(expected_response_time_ms)
|
stored_usage.response_time_ms >= Some(expected_response_time_ms),
|
||||||
);
|
"stream response_time_ms should be at least reported telemetry: expected >= {expected_response_time_ms:?}, got {:?}",
|
||||||
|
stored_usage.response_time_ms
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert_eq!(
|
||||||
|
stored_usage.response_time_ms,
|
||||||
|
Some(expected_response_time_ms)
|
||||||
|
);
|
||||||
|
}
|
||||||
assert_eq!(stored_usage.first_byte_time_ms, expected_ttfb_ms);
|
assert_eq!(stored_usage.first_byte_time_ms, expected_ttfb_ms);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stored_usage.settlement_input_price_per_1m(),
|
stored_usage.settlement_input_price_per_1m(),
|
||||||
|
|||||||
@@ -275,6 +275,10 @@ mod tests {
|
|||||||
"gemini:generate_content"
|
"gemini:generate_content"
|
||||||
));
|
));
|
||||||
assert!(!api_format_alias_matches("openai:cli", "openai:responses"));
|
assert!(!api_format_alias_matches("openai:cli", "openai:responses"));
|
||||||
|
assert_eq!(
|
||||||
|
normalize_api_format_alias("openai:compact"),
|
||||||
|
"openai:compact"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import apiClient from './client'
|
import apiClient from './client'
|
||||||
import type { ModelTestCapabilities } from './endpoints/types'
|
import type { ModelTestCapabilities } from './endpoints/types'
|
||||||
import axios from 'axios'
|
import axios, { type AxiosRequestConfig } from 'axios'
|
||||||
import { cachedRequest, buildCacheKey } from '@/utils/cache'
|
import { cachedRequest, buildCacheKey } from '@/utils/cache'
|
||||||
import type { BillingSummary } from './auth'
|
import type { BillingSummary } from './auth'
|
||||||
import type { ApiKeyInstallSession, InstallSessionTargetSystem, InstallTargetCli } from './me'
|
import type { ApiKeyInstallSession, InstallSessionTargetSystem, InstallTargetCli } from './me'
|
||||||
|
|
||||||
const SYSTEM_DATA_IMPORT_TIMEOUT_MS = 10 * 60 * 1000
|
const SYSTEM_DATA_IMPORT_TIMEOUT_MS = 10 * 60 * 1000
|
||||||
|
|
||||||
|
export interface SystemDataImportOptions {
|
||||||
|
onUploadProgress?: AxiosRequestConfig['onUploadProgress']
|
||||||
|
}
|
||||||
|
|
||||||
function extractConflictPayload(error: unknown): ManualUsageCleanupConflict | null {
|
function extractConflictPayload(error: unknown): ManualUsageCleanupConflict | null {
|
||||||
if (!axios.isAxiosError(error) || error.response?.status !== 409) {
|
if (!axios.isAxiosError(error) || error.response?.status !== 409) {
|
||||||
return null
|
return null
|
||||||
@@ -859,11 +863,11 @@ export const adminApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 导入配置
|
// 导入配置
|
||||||
async importConfig(data: ConfigImportRequest): Promise<ConfigImportResponse> {
|
async importConfig(data: ConfigImportRequest, options: SystemDataImportOptions = {}): Promise<ConfigImportResponse> {
|
||||||
const response = await apiClient.post<ConfigImportResponse>(
|
const response = await apiClient.post<ConfigImportResponse>(
|
||||||
'/api/admin/system/config/import',
|
'/api/admin/system/config/import',
|
||||||
data,
|
data,
|
||||||
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
|
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS, ...options }
|
||||||
)
|
)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
@@ -875,27 +879,27 @@ export const adminApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 导入用户数据
|
// 导入用户数据
|
||||||
async importUsers(data: UsersImportRequest): Promise<UsersImportResponse> {
|
async importUsers(data: UsersImportRequest, options: SystemDataImportOptions = {}): Promise<UsersImportResponse> {
|
||||||
const response = await apiClient.post<UsersImportResponse>(
|
const response = await apiClient.post<UsersImportResponse>(
|
||||||
'/api/admin/system/users/import',
|
'/api/admin/system/users/import',
|
||||||
data,
|
data,
|
||||||
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
|
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS, ...options }
|
||||||
)
|
)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导出聚合数据(配置数据 + 用户数据)
|
// 导出完整备份(配置数据 + 用户数据)
|
||||||
async exportAggregateData(): Promise<AggregateExportData> {
|
async exportAggregateData(): Promise<AggregateExportData> {
|
||||||
const response = await apiClient.get<AggregateExportData>('/api/admin/system/data/export')
|
const response = await apiClient.get<AggregateExportData>('/api/admin/system/data/export')
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导入聚合数据(配置数据 + 用户数据)
|
// 导入完整备份(配置数据 + 用户数据)
|
||||||
async importAggregateData(data: AggregateImportRequest): Promise<AggregateImportResponse> {
|
async importAggregateData(data: AggregateImportRequest, options: SystemDataImportOptions = {}): Promise<AggregateImportResponse> {
|
||||||
const response = await apiClient.post<AggregateImportResponse>(
|
const response = await apiClient.post<AggregateImportResponse>(
|
||||||
'/api/admin/system/data/import',
|
'/api/admin/system/data/import',
|
||||||
data,
|
data,
|
||||||
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
|
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS, ...options }
|
||||||
)
|
)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -198,6 +198,7 @@
|
|||||||
:merge-mode="mergeMode"
|
:merge-mode="mergeMode"
|
||||||
:merge-mode-select-open="mergeModeSelectOpen"
|
:merge-mode-select-open="mergeModeSelectOpen"
|
||||||
:import-loading="importLoading"
|
:import-loading="importLoading"
|
||||||
|
:import-progress="importProgress"
|
||||||
@confirm="confirmImport"
|
@confirm="confirmImport"
|
||||||
@update:import-dialog-open="importDialogOpen = $event"
|
@update:import-dialog-open="importDialogOpen = $event"
|
||||||
@update:import-result-dialog-open="importResultDialogOpen = $event"
|
@update:import-result-dialog-open="importResultDialogOpen = $event"
|
||||||
@@ -214,6 +215,7 @@
|
|||||||
:users-merge-mode="usersMergeMode"
|
:users-merge-mode="usersMergeMode"
|
||||||
:users-merge-mode-select-open="usersMergeModeSelectOpen"
|
:users-merge-mode-select-open="usersMergeModeSelectOpen"
|
||||||
:import-users-loading="importUsersLoading"
|
:import-users-loading="importUsersLoading"
|
||||||
|
:import-users-progress="importUsersProgress"
|
||||||
@confirm="confirmImportUsers"
|
@confirm="confirmImportUsers"
|
||||||
@update:import-users-dialog-open="importUsersDialogOpen = $event"
|
@update:import-users-dialog-open="importUsersDialogOpen = $event"
|
||||||
@update:import-users-result-dialog-open="importUsersResultDialogOpen = $event"
|
@update:import-users-result-dialog-open="importUsersResultDialogOpen = $event"
|
||||||
@@ -221,7 +223,7 @@
|
|||||||
@update:users-merge-mode-select-open="usersMergeModeSelectOpen = $event"
|
@update:users-merge-mode-select-open="usersMergeModeSelectOpen = $event"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 聚合数据导入对话框 -->
|
<!-- 完整备份导入对话框 -->
|
||||||
<AggregateImportDialog
|
<AggregateImportDialog
|
||||||
:aggregate-import-dialog-open="aggregateImportDialogOpen"
|
:aggregate-import-dialog-open="aggregateImportDialogOpen"
|
||||||
:aggregate-import-result-dialog-open="aggregateImportResultDialogOpen"
|
:aggregate-import-result-dialog-open="aggregateImportResultDialogOpen"
|
||||||
@@ -230,6 +232,7 @@
|
|||||||
:aggregate-merge-mode="aggregateMergeMode"
|
:aggregate-merge-mode="aggregateMergeMode"
|
||||||
:aggregate-merge-mode-select-open="aggregateMergeModeSelectOpen"
|
:aggregate-merge-mode-select-open="aggregateMergeModeSelectOpen"
|
||||||
:import-aggregate-loading="importAggregateLoading"
|
:import-aggregate-loading="importAggregateLoading"
|
||||||
|
:import-aggregate-progress="importAggregateProgress"
|
||||||
@confirm="confirmImportAggregate"
|
@confirm="confirmImportAggregate"
|
||||||
@update:aggregate-import-dialog-open="aggregateImportDialogOpen = $event"
|
@update:aggregate-import-dialog-open="aggregateImportDialogOpen = $event"
|
||||||
@update:aggregate-import-result-dialog-open="aggregateImportResultDialogOpen = $event"
|
@update:aggregate-import-result-dialog-open="aggregateImportResultDialogOpen = $event"
|
||||||
@@ -364,6 +367,7 @@ const {
|
|||||||
importResult,
|
importResult,
|
||||||
mergeMode,
|
mergeMode,
|
||||||
mergeModeSelectOpen,
|
mergeModeSelectOpen,
|
||||||
|
importProgress,
|
||||||
handleExportConfig,
|
handleExportConfig,
|
||||||
handleConfigFileSelect,
|
handleConfigFileSelect,
|
||||||
confirmImport,
|
confirmImport,
|
||||||
@@ -375,6 +379,7 @@ const {
|
|||||||
importUsersResult,
|
importUsersResult,
|
||||||
usersMergeMode,
|
usersMergeMode,
|
||||||
usersMergeModeSelectOpen,
|
usersMergeModeSelectOpen,
|
||||||
|
importUsersProgress,
|
||||||
handleExportUsers,
|
handleExportUsers,
|
||||||
handleUsersFileSelect,
|
handleUsersFileSelect,
|
||||||
confirmImportUsers,
|
confirmImportUsers,
|
||||||
@@ -386,6 +391,7 @@ const {
|
|||||||
aggregateImportResult,
|
aggregateImportResult,
|
||||||
aggregateMergeMode,
|
aggregateMergeMode,
|
||||||
aggregateMergeModeSelectOpen,
|
aggregateMergeModeSelectOpen,
|
||||||
|
importAggregateProgress,
|
||||||
handleExportAggregate,
|
handleExportAggregate,
|
||||||
handleAggregateFileSelect,
|
handleAggregateFileSelect,
|
||||||
confirmImportAggregate,
|
confirmImportAggregate,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 聚合数据导入对话框 -->
|
<!-- 完整备份导入对话框 -->
|
||||||
<Dialog
|
<Dialog
|
||||||
:open="aggregateImportDialogOpen"
|
:open="aggregateImportDialogOpen"
|
||||||
title="导入聚合数据"
|
title="导入完整备份"
|
||||||
description="选择冲突处理模式并确认导入"
|
description="选择冲突处理模式并确认导入"
|
||||||
@update:open="$emit('update:aggregateImportDialogOpen', $event)"
|
@update:open="$emit('update:aggregateImportDialogOpen', $event)"
|
||||||
>
|
>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
class="text-sm"
|
class="text-sm"
|
||||||
>
|
>
|
||||||
<p class="font-medium mb-2">
|
<p class="font-medium mb-2">
|
||||||
聚合数据预览
|
完整备份预览
|
||||||
</p>
|
</p>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-muted-foreground">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-muted-foreground">
|
||||||
<div>
|
<div>
|
||||||
@@ -84,8 +84,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
注意:聚合数据会先导入配置数据,再导入用户数据;用户 API Keys 需要目标系统使用相同的 ENCRYPTION_KEY。
|
注意:完整备份会先导入配置数据,再导入用户数据;文件包含用户、用户组、API Keys 与钱包快照,用户 API Keys 需要目标系统使用相同的 ENCRYPTION_KEY。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="importAggregateProgress"
|
||||||
|
class="space-y-2 rounded-md border border-border p-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
||||||
|
<span>{{ importAggregateProgress.message }}</span>
|
||||||
|
<span>{{ importAggregateProgress.percent }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1.5 overflow-hidden rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
class="h-full bg-primary transition-all"
|
||||||
|
:style="{ width: `${importAggregateProgress.percent}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -104,10 +120,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- 聚合数据导入结果对话框 -->
|
<!-- 完整备份导入结果对话框 -->
|
||||||
<Dialog
|
<Dialog
|
||||||
:open="aggregateImportResultDialogOpen"
|
:open="aggregateImportResultDialogOpen"
|
||||||
title="聚合数据导入完成"
|
title="完整备份导入完成"
|
||||||
@update:open="$emit('update:aggregateImportResultDialogOpen', $event)"
|
@update:open="$emit('update:aggregateImportResultDialogOpen', $event)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -174,6 +190,7 @@ import SelectContent from '@/components/ui/select-content.vue'
|
|||||||
import SelectItem from '@/components/ui/select-item.vue'
|
import SelectItem from '@/components/ui/select-item.vue'
|
||||||
import { Dialog } from '@/components/ui'
|
import { Dialog } from '@/components/ui'
|
||||||
import type { AggregateExportData, AggregateImportResponse } from '@/api/admin'
|
import type { AggregateExportData, AggregateImportResponse } from '@/api/admin'
|
||||||
|
import type { ImportProgressState } from './composables/useConfigExportImport'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
aggregateImportDialogOpen: boolean
|
aggregateImportDialogOpen: boolean
|
||||||
@@ -183,6 +200,7 @@ const props = defineProps<{
|
|||||||
aggregateMergeMode: 'skip' | 'overwrite' | 'error'
|
aggregateMergeMode: 'skip' | 'overwrite' | 'error'
|
||||||
aggregateMergeModeSelectOpen: boolean
|
aggregateMergeModeSelectOpen: boolean
|
||||||
importAggregateLoading: boolean
|
importAggregateLoading: boolean
|
||||||
|
importAggregateProgress: ImportProgressState | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
|
|||||||
@@ -74,6 +74,22 @@
|
|||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
注意:相同的 API Keys 会自动跳过,不会创建重复记录。
|
注意:相同的 API Keys 会自动跳过,不会创建重复记录。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="importProgress"
|
||||||
|
class="space-y-2 rounded-md border border-border p-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
||||||
|
<span>{{ importProgress.message }}</span>
|
||||||
|
<span>{{ importProgress.percent }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1.5 overflow-hidden rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
class="h-full bg-primary transition-all"
|
||||||
|
:style="{ width: `${importProgress.percent}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -220,6 +236,7 @@ import SelectContent from '@/components/ui/select-content.vue'
|
|||||||
import SelectItem from '@/components/ui/select-item.vue'
|
import SelectItem from '@/components/ui/select-item.vue'
|
||||||
import { Dialog } from '@/components/ui'
|
import { Dialog } from '@/components/ui'
|
||||||
import type { ConfigExportData, ConfigImportResponse } from '@/api/admin'
|
import type { ConfigExportData, ConfigImportResponse } from '@/api/admin'
|
||||||
|
import type { ImportProgressState } from './composables/useConfigExportImport'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
importDialogOpen: boolean
|
importDialogOpen: boolean
|
||||||
@@ -229,6 +246,7 @@ defineProps<{
|
|||||||
mergeMode: 'skip' | 'overwrite' | 'error'
|
mergeMode: 'skip' | 'overwrite' | 'error'
|
||||||
mergeModeSelectOpen: boolean
|
mergeModeSelectOpen: boolean
|
||||||
importLoading: boolean
|
importLoading: boolean
|
||||||
|
importProgress: ImportProgressState | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
|
|||||||
@@ -209,10 +209,10 @@ const dataItems = computed<DataItem[]>(() => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'aggregate',
|
key: 'aggregate',
|
||||||
title: '聚合数据',
|
title: '完整备份',
|
||||||
description: '配置数据和用户数据的一体化备份文件',
|
description: '配置与用户数据的一体化备份,包含用户、用户组、API Keys 与钱包快照',
|
||||||
exportLabel: '导出聚合',
|
exportLabel: '导出备份',
|
||||||
importLabel: '导入聚合',
|
importLabel: '导入备份',
|
||||||
icon: markRaw(Layers3),
|
icon: markRaw(Layers3),
|
||||||
exportLoading: props.aggregateExportLoading,
|
exportLoading: props.aggregateExportLoading,
|
||||||
importLoading: props.aggregateImportLoading,
|
importLoading: props.aggregateImportLoading,
|
||||||
|
|||||||
@@ -67,6 +67,22 @@
|
|||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
注意:用户 API Keys 需要目标系统使用相同的 ENCRYPTION_KEY 环境变量才能正常工作。
|
注意:用户 API Keys 需要目标系统使用相同的 ENCRYPTION_KEY 环境变量才能正常工作。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="importUsersProgress"
|
||||||
|
class="space-y-2 rounded-md border border-border p-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
||||||
|
<span>{{ importUsersProgress.message }}</span>
|
||||||
|
<span>{{ importUsersProgress.percent }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1.5 overflow-hidden rounded-full bg-muted">
|
||||||
|
<div
|
||||||
|
class="h-full bg-primary transition-all"
|
||||||
|
:style="{ width: `${importUsersProgress.percent}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -175,6 +191,7 @@ import SelectContent from '@/components/ui/select-content.vue'
|
|||||||
import SelectItem from '@/components/ui/select-item.vue'
|
import SelectItem from '@/components/ui/select-item.vue'
|
||||||
import { Dialog } from '@/components/ui'
|
import { Dialog } from '@/components/ui'
|
||||||
import type { UsersExportData, UsersImportResponse } from '@/api/admin'
|
import type { UsersExportData, UsersImportResponse } from '@/api/admin'
|
||||||
|
import type { ImportProgressState } from './composables/useConfigExportImport'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
importUsersDialogOpen: boolean
|
importUsersDialogOpen: boolean
|
||||||
@@ -184,6 +201,7 @@ defineProps<{
|
|||||||
usersMergeMode: 'skip' | 'overwrite' | 'error'
|
usersMergeMode: 'skip' | 'overwrite' | 'error'
|
||||||
usersMergeModeSelectOpen: boolean
|
usersMergeModeSelectOpen: boolean
|
||||||
importUsersLoading: boolean
|
importUsersLoading: boolean
|
||||||
|
importUsersProgress: ImportProgressState | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ref } from 'vue'
|
import { nextTick, ref } from 'vue'
|
||||||
|
import type { AxiosProgressEvent } from 'axios'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import {
|
import {
|
||||||
adminApi,
|
adminApi,
|
||||||
@@ -22,6 +23,11 @@ const MAX_AGGREGATE_FILE_SIZE = MAX_AGGREGATE_FILE_SIZE_MB * BYTES_PER_MB
|
|||||||
|
|
||||||
type JsonObject = Record<string, unknown>
|
type JsonObject = Record<string, unknown>
|
||||||
|
|
||||||
|
export interface ImportProgressState {
|
||||||
|
percent: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
function asJsonObject(value: unknown): JsonObject | null {
|
function asJsonObject(value: unknown): JsonObject | null {
|
||||||
return value && typeof value === 'object' && !Array.isArray(value)
|
return value && typeof value === 'object' && !Array.isArray(value)
|
||||||
? value as JsonObject
|
? value as JsonObject
|
||||||
@@ -56,6 +62,47 @@ function fileSizeLimitMessage(limitMb: number): string {
|
|||||||
return `文件大小不能超过 ${limitMb}MB`
|
return `文件大小不能超过 ${limitMb}MB`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
if (bytes >= BYTES_PER_MB) {
|
||||||
|
return `${(bytes / BYTES_PER_MB).toFixed(1)}MB`
|
||||||
|
}
|
||||||
|
if (bytes >= 1024) {
|
||||||
|
return `${Math.round(bytes / 1024)}KB`
|
||||||
|
}
|
||||||
|
return `${bytes}B`
|
||||||
|
}
|
||||||
|
|
||||||
|
function setImportProgress(
|
||||||
|
target: { value: ImportProgressState | null },
|
||||||
|
percent: number,
|
||||||
|
message: string,
|
||||||
|
) {
|
||||||
|
target.value = {
|
||||||
|
percent: Math.max(0, Math.min(100, Math.round(percent))),
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUploadProgressHandler(
|
||||||
|
target: { value: ImportProgressState | null },
|
||||||
|
label: string,
|
||||||
|
) {
|
||||||
|
return (event: AxiosProgressEvent) => {
|
||||||
|
if (!event.total) {
|
||||||
|
setImportProgress(target, 15, `${label}上传中:${formatBytes(event.loaded)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadPercent = Math.min(85, 10 + (event.loaded / event.total) * 75)
|
||||||
|
const loaded = formatBytes(event.loaded)
|
||||||
|
const total = formatBytes(event.total)
|
||||||
|
const message = event.loaded >= event.total
|
||||||
|
? `${label}已上传,服务端正在校验并写入数据`
|
||||||
|
: `${label}上传中:${loaded} / ${total}`
|
||||||
|
setImportProgress(target, uploadPercent, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function downloadJson(data: unknown, filename: string) {
|
function downloadJson(data: unknown, filename: string) {
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
@@ -81,6 +128,7 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
const importResult = ref<ConfigImportResponse | null>(null)
|
const importResult = ref<ConfigImportResponse | null>(null)
|
||||||
const mergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
|
const mergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
|
||||||
const mergeModeSelectOpen = ref(false)
|
const mergeModeSelectOpen = ref(false)
|
||||||
|
const importProgress = ref<ImportProgressState | null>(null)
|
||||||
|
|
||||||
// 用户数据导出/导入相关
|
// 用户数据导出/导入相关
|
||||||
const exportUsersLoading = ref(false)
|
const exportUsersLoading = ref(false)
|
||||||
@@ -92,8 +140,9 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
const importUsersResult = ref<UsersImportResponse | null>(null)
|
const importUsersResult = ref<UsersImportResponse | null>(null)
|
||||||
const usersMergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
|
const usersMergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
|
||||||
const usersMergeModeSelectOpen = ref(false)
|
const usersMergeModeSelectOpen = ref(false)
|
||||||
|
const importUsersProgress = ref<ImportProgressState | null>(null)
|
||||||
|
|
||||||
// 聚合数据导出/导入相关
|
// 完整备份导出/导入相关
|
||||||
const exportAggregateLoading = ref(false)
|
const exportAggregateLoading = ref(false)
|
||||||
const importAggregateLoading = ref(false)
|
const importAggregateLoading = ref(false)
|
||||||
const aggregateImportDialogOpen = ref(false)
|
const aggregateImportDialogOpen = ref(false)
|
||||||
@@ -102,6 +151,7 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
const aggregateImportResult = ref<AggregateImportResponse | null>(null)
|
const aggregateImportResult = ref<AggregateImportResponse | null>(null)
|
||||||
const aggregateMergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
|
const aggregateMergeMode = ref<'skip' | 'overwrite' | 'error'>('skip')
|
||||||
const aggregateMergeModeSelectOpen = ref(false)
|
const aggregateMergeModeSelectOpen = ref(false)
|
||||||
|
const importAggregateProgress = ref<ImportProgressState | null>(null)
|
||||||
|
|
||||||
// 导出配置
|
// 导出配置
|
||||||
async function handleExportConfig() {
|
async function handleExportConfig() {
|
||||||
@@ -182,11 +232,16 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
if (!importPreview.value) return
|
if (!importPreview.value) return
|
||||||
|
|
||||||
importLoading.value = true
|
importLoading.value = true
|
||||||
|
setImportProgress(importProgress, 5, '准备提交配置数据')
|
||||||
|
await nextTick()
|
||||||
try {
|
try {
|
||||||
const result = await adminApi.importConfig({
|
const result = await adminApi.importConfig({
|
||||||
...importPreview.value,
|
...importPreview.value,
|
||||||
merge_mode: mergeMode.value,
|
merge_mode: mergeMode.value,
|
||||||
|
}, {
|
||||||
|
onUploadProgress: buildUploadProgressHandler(importProgress, '配置数据'),
|
||||||
})
|
})
|
||||||
|
setImportProgress(importProgress, 100, '配置数据导入完成')
|
||||||
importResult.value = result
|
importResult.value = result
|
||||||
importDialogOpen.value = false
|
importDialogOpen.value = false
|
||||||
mergeModeSelectOpen.value = false
|
mergeModeSelectOpen.value = false
|
||||||
@@ -197,6 +252,7 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
log.error('导入配置失败:', err)
|
log.error('导入配置失败:', err)
|
||||||
} finally {
|
} finally {
|
||||||
importLoading.value = false
|
importLoading.value = false
|
||||||
|
importProgress.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,11 +345,16 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
if (!importUsersPreview.value) return
|
if (!importUsersPreview.value) return
|
||||||
|
|
||||||
importUsersLoading.value = true
|
importUsersLoading.value = true
|
||||||
|
setImportProgress(importUsersProgress, 5, '准备提交用户数据')
|
||||||
|
await nextTick()
|
||||||
try {
|
try {
|
||||||
const result = await adminApi.importUsers({
|
const result = await adminApi.importUsers({
|
||||||
...importUsersPreview.value,
|
...importUsersPreview.value,
|
||||||
merge_mode: usersMergeMode.value,
|
merge_mode: usersMergeMode.value,
|
||||||
|
}, {
|
||||||
|
onUploadProgress: buildUploadProgressHandler(importUsersProgress, '用户数据'),
|
||||||
})
|
})
|
||||||
|
setImportProgress(importUsersProgress, 100, '用户数据导入完成')
|
||||||
importUsersResult.value = result
|
importUsersResult.value = result
|
||||||
importUsersDialogOpen.value = false
|
importUsersDialogOpen.value = false
|
||||||
usersMergeModeSelectOpen.value = false
|
usersMergeModeSelectOpen.value = false
|
||||||
@@ -304,10 +365,11 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
log.error('导入用户数据失败:', err)
|
log.error('导入用户数据失败:', err)
|
||||||
} finally {
|
} finally {
|
||||||
importUsersLoading.value = false
|
importUsersLoading.value = false
|
||||||
|
importUsersProgress.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出聚合数据
|
// 导出完整备份
|
||||||
async function handleExportAggregate() {
|
async function handleExportAggregate() {
|
||||||
exportAggregateLoading.value = true
|
exportAggregateLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -316,16 +378,16 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
data,
|
data,
|
||||||
`${systemConfig.value.site_name.toLowerCase()}-data-${new Date().toISOString().slice(0, 10)}.json`,
|
`${systemConfig.value.site_name.toLowerCase()}-data-${new Date().toISOString().slice(0, 10)}.json`,
|
||||||
)
|
)
|
||||||
success('聚合数据已导出')
|
success('完整备份已导出')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error('导出聚合数据失败')
|
error('导出完整备份失败')
|
||||||
log.error('导出聚合数据失败:', err)
|
log.error('导出完整备份失败:', err)
|
||||||
} finally {
|
} finally {
|
||||||
exportAggregateLoading.value = false
|
exportAggregateLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理聚合数据文件选择
|
// 处理完整备份文件选择
|
||||||
function handleAggregateFileSelect(event: Event) {
|
function handleAggregateFileSelect(event: Event) {
|
||||||
const input = event.target as HTMLInputElement
|
const input = event.target as HTMLInputElement
|
||||||
const file = input.files?.[0]
|
const file = input.files?.[0]
|
||||||
@@ -343,7 +405,7 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
const content = e.target?.result as string
|
const content = e.target?.result as string
|
||||||
const root = asJsonObject(JSON.parse(content))
|
const root = asJsonObject(JSON.parse(content))
|
||||||
if (!root) {
|
if (!root) {
|
||||||
error('无效的聚合数据文件:JSON 顶层必须是对象')
|
error('无效的完整备份文件:JSON 顶层必须是对象')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,24 +415,24 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
} else if (looksLikeUsersExport(root)) {
|
} else if (looksLikeUsersExport(root)) {
|
||||||
error('这是用户数据导出文件,请使用“导入用户数据”')
|
error('这是用户数据导出文件,请使用“导入用户数据”')
|
||||||
} else {
|
} else {
|
||||||
error('无效的聚合数据文件:未找到配置数据和用户数据')
|
error('无效的完整备份文件:未找到配置数据和用户数据')
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!root.version) {
|
if (!root.version) {
|
||||||
error('无效的聚合数据文件:缺少版本信息')
|
error('无效的完整备份文件:缺少版本信息')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const configData = asJsonObject(root.config_data)
|
const configData = asJsonObject(root.config_data)
|
||||||
const userData = asJsonObject(root.user_data)
|
const userData = asJsonObject(root.user_data)
|
||||||
if (!configData || !looksLikeConfigExport(configData)) {
|
if (!configData || !looksLikeConfigExport(configData)) {
|
||||||
error('无效的聚合数据文件:config_data 格式不正确')
|
error('无效的完整备份文件:config_data 格式不正确')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!userData || !looksLikeUsersExport(userData)) {
|
if (!userData || !looksLikeUsersExport(userData)) {
|
||||||
error('无效的聚合数据文件:user_data 格式不正确')
|
error('无效的完整备份文件:user_data 格式不正确')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,8 +441,8 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
aggregateMergeMode.value = 'skip'
|
aggregateMergeMode.value = 'skip'
|
||||||
aggregateImportDialogOpen.value = true
|
aggregateImportDialogOpen.value = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error('解析聚合数据文件失败,请确保是有效的 JSON 文件')
|
error('解析完整备份文件失败,请确保是有效的 JSON 文件')
|
||||||
log.error('解析聚合数据文件失败:', err)
|
log.error('解析完整备份文件失败:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.readAsText(file)
|
reader.readAsText(file)
|
||||||
@@ -388,26 +450,32 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
input.value = ''
|
input.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认导入聚合数据
|
// 确认导入完整备份
|
||||||
async function confirmImportAggregate() {
|
async function confirmImportAggregate() {
|
||||||
if (!aggregateImportPreview.value) return
|
if (!aggregateImportPreview.value) return
|
||||||
|
|
||||||
importAggregateLoading.value = true
|
importAggregateLoading.value = true
|
||||||
|
setImportProgress(importAggregateProgress, 5, '准备提交完整备份')
|
||||||
|
await nextTick()
|
||||||
try {
|
try {
|
||||||
const result = await adminApi.importAggregateData({
|
const result = await adminApi.importAggregateData({
|
||||||
...aggregateImportPreview.value,
|
...aggregateImportPreview.value,
|
||||||
merge_mode: aggregateMergeMode.value,
|
merge_mode: aggregateMergeMode.value,
|
||||||
|
}, {
|
||||||
|
onUploadProgress: buildUploadProgressHandler(importAggregateProgress, '完整备份'),
|
||||||
})
|
})
|
||||||
|
setImportProgress(importAggregateProgress, 100, '完整备份导入完成')
|
||||||
aggregateImportResult.value = result
|
aggregateImportResult.value = result
|
||||||
aggregateImportDialogOpen.value = false
|
aggregateImportDialogOpen.value = false
|
||||||
aggregateMergeModeSelectOpen.value = false
|
aggregateMergeModeSelectOpen.value = false
|
||||||
aggregateImportResultDialogOpen.value = true
|
aggregateImportResultDialogOpen.value = true
|
||||||
success('聚合数据导入成功')
|
success('完整备份导入成功')
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
error(parseApiError(err, '导入聚合数据失败'))
|
error(parseApiError(err, '导入完整备份失败'))
|
||||||
log.error('导入聚合数据失败:', err)
|
log.error('导入完整备份失败:', err)
|
||||||
} finally {
|
} finally {
|
||||||
importAggregateLoading.value = false
|
importAggregateLoading.value = false
|
||||||
|
importAggregateProgress.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +490,7 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
importResult,
|
importResult,
|
||||||
mergeMode,
|
mergeMode,
|
||||||
mergeModeSelectOpen,
|
mergeModeSelectOpen,
|
||||||
|
importProgress,
|
||||||
handleExportConfig,
|
handleExportConfig,
|
||||||
triggerConfigFileSelect,
|
triggerConfigFileSelect,
|
||||||
handleConfigFileSelect,
|
handleConfigFileSelect,
|
||||||
@@ -436,11 +505,12 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
importUsersResult,
|
importUsersResult,
|
||||||
usersMergeMode,
|
usersMergeMode,
|
||||||
usersMergeModeSelectOpen,
|
usersMergeModeSelectOpen,
|
||||||
|
importUsersProgress,
|
||||||
handleExportUsers,
|
handleExportUsers,
|
||||||
triggerUsersFileSelect,
|
triggerUsersFileSelect,
|
||||||
handleUsersFileSelect,
|
handleUsersFileSelect,
|
||||||
confirmImportUsers,
|
confirmImportUsers,
|
||||||
// 聚合数据导出/导入
|
// 完整备份导出/导入
|
||||||
exportAggregateLoading,
|
exportAggregateLoading,
|
||||||
importAggregateLoading,
|
importAggregateLoading,
|
||||||
aggregateImportDialogOpen,
|
aggregateImportDialogOpen,
|
||||||
@@ -449,6 +519,7 @@ export function useConfigExportImport(systemConfig: { value: SystemConfig }) {
|
|||||||
aggregateImportResult,
|
aggregateImportResult,
|
||||||
aggregateMergeMode,
|
aggregateMergeMode,
|
||||||
aggregateMergeModeSelectOpen,
|
aggregateMergeModeSelectOpen,
|
||||||
|
importAggregateProgress,
|
||||||
handleExportAggregate,
|
handleExportAggregate,
|
||||||
handleAggregateFileSelect,
|
handleAggregateFileSelect,
|
||||||
confirmImportAggregate,
|
confirmImportAggregate,
|
||||||
|
|||||||
Reference in New Issue
Block a user