06aa99e3eb
使用单次哨兵查询统一计算扫描范围、匹配总数和截断状态 增加完整候选集服务端排序与越界页码钳制 增加固定分页栏、扫描上限提示和后端排序交互 补充大表响应边界文档与前后端回归测试
871 lines
23 KiB
TypeScript
871 lines
23 KiB
TypeScript
import type {
|
|
AdminUpsertCreationEntryEventBannersRequest,
|
|
AdminUpsertCreationEntryTypeConfigRequest,
|
|
AdminCreationEntryConfigResponse,
|
|
AdminDashboardQuery,
|
|
AdminDashboardResponse,
|
|
AdminDebugHttpRequest,
|
|
AdminDebugHttpResponse,
|
|
AdminDisableProfileRedeemCodeRequest,
|
|
AdminDisableProfileTaskConfigRequest,
|
|
AdminDatabaseTableListResponse,
|
|
AdminDatabaseTableRowsQuery,
|
|
AdminDatabaseTableRowsResponse,
|
|
AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
|
|
AdminCreateEditorShowcaseCampaignImageUploadTicketResponse,
|
|
AdminEditorAssetListQuery,
|
|
AdminEditorAssetListResponse,
|
|
AdminDirectUploadTicketPayload,
|
|
AdminEditorShowcaseAssetResponse,
|
|
AdminEditorShowcaseCampaignResponse,
|
|
AdminEditorShowcaseDisplayRequest,
|
|
AdminEditorShowcaseListQuery,
|
|
AdminEditorShowcaseListResponse,
|
|
AdminEditorShowcaseReviewRequest,
|
|
AdminFeatureGateConfigResponse,
|
|
AdminLoginResponse,
|
|
AdminMeResponse,
|
|
AdminOverviewResponse,
|
|
AdminTrackingEventListQuery,
|
|
AdminTrackingEventKeyListResponse,
|
|
AdminTrackingEventListResponse,
|
|
AdminUpdateWorkVisibilityRequest,
|
|
AdminUpdateWorkVisibilityResponse,
|
|
AdminUploadedEditorShowcaseCampaignImage,
|
|
AdminUpsertEditorShowcaseCampaignRequest,
|
|
AdminUpsertFeatureGateConfigRequest,
|
|
AdminUpsertProfileInviteCodeRequest,
|
|
AdminUpsertProfileRechargeProductRequest,
|
|
AdminUpsertProfileRedeemCodeRequest,
|
|
AdminUpsertProfileTaskConfigRequest,
|
|
AdminUpsertProfileWalletConfigRequest,
|
|
AdminUpsertPublicWorkInteractionConfigRequest,
|
|
AdminWorkVisibilityListResponse,
|
|
ApiErrorEnvelope,
|
|
ApiMeta,
|
|
ApiSuccessEnvelope,
|
|
EditorGenerationPricingConfigPayload,
|
|
ProfileInviteCodeAdminListResponse,
|
|
ProfileInviteCodeAdminResponse,
|
|
ProfileRechargeProductConfigAdminListResponse,
|
|
ProfileRechargeProductConfigAdminResponse,
|
|
ProfileRedeemCodeAdminListResponse,
|
|
ProfileRedeemCodeAdminResponse,
|
|
ProfileTaskConfigAdminListResponse,
|
|
ProfileTaskConfigAdminResponse,
|
|
ProfileWalletConfigAdminResponse,
|
|
} from './adminApiTypes';
|
|
|
|
const API_RESPONSE_ENVELOPE_HEADER = 'x-genarrative-response-envelope';
|
|
const ADMIN_API_BASE_URL = normalizeBaseUrl(
|
|
import.meta.env.VITE_ADMIN_API_BASE_URL ?? '',
|
|
);
|
|
|
|
interface AdminRequestOptions {
|
|
method?: string;
|
|
token?: string;
|
|
body?: unknown;
|
|
headers?: Record<string, string>;
|
|
signal?: AbortSignal;
|
|
}
|
|
|
|
interface AdminAssetReadUrlQuery {
|
|
objectKey?: string | null;
|
|
legacyPublicPath?: string | null;
|
|
expireSeconds?: number | null;
|
|
}
|
|
|
|
export interface AdminAssetReadUrlResponse {
|
|
read?: {
|
|
objectKey?: string;
|
|
signedUrl?: string;
|
|
expiresAt?: string;
|
|
};
|
|
signedUrl?: string;
|
|
objectKey?: string;
|
|
expiresAt?: string;
|
|
}
|
|
|
|
export class AdminApiError extends Error {
|
|
status: number;
|
|
code: string;
|
|
details: Record<string, unknown> | null;
|
|
meta: ApiMeta | null;
|
|
responseText: string;
|
|
|
|
constructor(params: {
|
|
message: string;
|
|
status: number;
|
|
code?: string;
|
|
details?: Record<string, unknown> | null;
|
|
meta?: ApiMeta | null;
|
|
responseText?: string;
|
|
}) {
|
|
super(params.message);
|
|
this.name = 'AdminApiError';
|
|
this.status = params.status;
|
|
this.code = params.code ?? 'ADMIN_API_ERROR';
|
|
this.details = params.details ?? null;
|
|
this.meta = params.meta ?? null;
|
|
this.responseText = params.responseText ?? '';
|
|
}
|
|
}
|
|
|
|
export function isAdminApiError(error: unknown): error is AdminApiError {
|
|
return error instanceof AdminApiError;
|
|
}
|
|
|
|
export function formatAdminApiError(error: unknown) {
|
|
if (isAdminApiError(error)) {
|
|
return error.message;
|
|
}
|
|
|
|
if (error instanceof Error && error.message.trim()) {
|
|
return error.message;
|
|
}
|
|
|
|
return '请求失败';
|
|
}
|
|
|
|
export async function request<T>(
|
|
path: string,
|
|
options: AdminRequestOptions = {},
|
|
): Promise<T> {
|
|
const method = options.method ?? 'GET';
|
|
const headers: Record<string, string> = {
|
|
Accept: 'application/json',
|
|
[API_RESPONSE_ENVELOPE_HEADER]: 'v1',
|
|
...(options.headers ?? {}),
|
|
};
|
|
|
|
const token = options.token?.trim();
|
|
if (token) {
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
const init: RequestInit = {
|
|
method,
|
|
headers,
|
|
signal: options.signal,
|
|
};
|
|
|
|
if (typeof options.body !== 'undefined') {
|
|
headers['Content-Type'] = 'application/json';
|
|
init.body = JSON.stringify(options.body);
|
|
}
|
|
|
|
const response = await fetch(buildRequestUrl(path), init);
|
|
const responseText = await response.text();
|
|
const payload = parseJsonResponse(responseText);
|
|
|
|
if (!response.ok) {
|
|
throw buildAdminApiError(response, payload, responseText);
|
|
}
|
|
|
|
return unwrapSuccessPayload<T>(payload);
|
|
}
|
|
|
|
export function loginAdmin(username: string, password: string) {
|
|
return request<AdminLoginResponse>('/admin/api/login', {
|
|
method: 'POST',
|
|
body: { username, password },
|
|
});
|
|
}
|
|
|
|
export function getAdminMe(token: string) {
|
|
return request<AdminMeResponse>('/admin/api/me', { token });
|
|
}
|
|
|
|
export function getAdminOverview(token: string) {
|
|
return request<AdminOverviewResponse>('/admin/api/overview', { token });
|
|
}
|
|
|
|
export function getAdminDashboard(
|
|
token: string,
|
|
query: AdminDashboardQuery = {},
|
|
) {
|
|
return request<AdminDashboardResponse>(
|
|
`/admin/api/dashboard${buildDashboardQuery(query)}`,
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function getAdminDatabaseTables(token: string) {
|
|
return request<AdminDatabaseTableListResponse>('/admin/api/database/tables', {
|
|
token,
|
|
});
|
|
}
|
|
|
|
export function getAdminDatabaseTableRows(
|
|
token: string,
|
|
tableName: string,
|
|
query: AdminDatabaseTableRowsQuery = {},
|
|
) {
|
|
return request<AdminDatabaseTableRowsResponse>(
|
|
`/admin/api/database/tables/${encodeURIComponent(tableName)}/rows${buildDatabaseTableRowsQuery(query)}`,
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function debugAdminHttp(token: string, payload: AdminDebugHttpRequest) {
|
|
return request<AdminDebugHttpResponse>('/admin/api/debug/http', {
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
});
|
|
}
|
|
|
|
export function listAdminTrackingEvents(
|
|
token: string,
|
|
query: AdminTrackingEventListQuery = {},
|
|
) {
|
|
return request<AdminTrackingEventListResponse>(
|
|
`/admin/api/tracking/events${buildQueryString(query)}`,
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function listAdminTrackingEventKeys(token: string) {
|
|
return request<AdminTrackingEventKeyListResponse>(
|
|
'/admin/api/tracking/event-keys',
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function getAdminFeatureGateConfig(token: string) {
|
|
return request<AdminFeatureGateConfigResponse>('/admin/api/feature-gates', {
|
|
token,
|
|
});
|
|
}
|
|
|
|
export function upsertAdminFeatureGateConfig(
|
|
token: string,
|
|
payload: AdminUpsertFeatureGateConfigRequest,
|
|
) {
|
|
return request<AdminFeatureGateConfigResponse>('/admin/api/feature-gates', {
|
|
method: 'PUT',
|
|
token,
|
|
body: payload,
|
|
});
|
|
}
|
|
|
|
export function getAdminCreationEntryConfig(token: string) {
|
|
return request<AdminCreationEntryConfigResponse>(
|
|
'/admin/api/creation-entry/config',
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function upsertAdminCreationEntryConfig(
|
|
token: string,
|
|
payload: AdminUpsertCreationEntryTypeConfigRequest,
|
|
) {
|
|
return request<AdminCreationEntryConfigResponse>(
|
|
'/admin/api/creation-entry/config',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
/** 保存创作入口公告表单序列化后的后端传输字段。 */
|
|
export function upsertAdminCreationEntryBanners(
|
|
token: string,
|
|
payload: AdminUpsertCreationEntryEventBannersRequest,
|
|
) {
|
|
return request<AdminCreationEntryConfigResponse>(
|
|
'/admin/api/creation-entry/config/banners',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
/** 保存公开作品详情页点赞 / 改造能力配置。 */
|
|
export function upsertAdminPublicWorkInteractions(
|
|
token: string,
|
|
payload: AdminUpsertPublicWorkInteractionConfigRequest,
|
|
) {
|
|
return request<AdminCreationEntryConfigResponse>(
|
|
'/admin/api/creation-entry/config/interactions',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function getAdminEditorGenerationPricing(token: string) {
|
|
return request<EditorGenerationPricingConfigPayload>(
|
|
'/admin/api/editor-generation-pricing',
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function upsertAdminEditorGenerationPricing(
|
|
token: string,
|
|
payload: EditorGenerationPricingConfigPayload,
|
|
) {
|
|
return request<EditorGenerationPricingConfigPayload>(
|
|
'/admin/api/editor-generation-pricing',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function listAdminWorkVisibility(token: string) {
|
|
return request<AdminWorkVisibilityListResponse>(
|
|
'/admin/api/works/visibility',
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function updateAdminWorkVisibility(
|
|
token: string,
|
|
payload: AdminUpdateWorkVisibilityRequest,
|
|
) {
|
|
return request<AdminUpdateWorkVisibilityResponse>(
|
|
'/admin/api/works/visibility',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function getAdminAssetReadUrl(
|
|
token: string,
|
|
query: AdminAssetReadUrlQuery,
|
|
) {
|
|
return request<AdminAssetReadUrlResponse>(
|
|
`/admin/api/assets/read-url${buildAssetReadUrlQuery(query)}`,
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function listAdminEditorAssets(
|
|
token: string,
|
|
query: AdminEditorAssetListQuery = {},
|
|
) {
|
|
return request<AdminEditorAssetListResponse>(
|
|
`/admin/api/editor-assets${buildEditorAssetListQuery(query)}`,
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function listAdminEditorShowcaseAssets(
|
|
token: string,
|
|
query: AdminEditorShowcaseListQuery = {},
|
|
) {
|
|
return request<AdminEditorShowcaseListResponse>(
|
|
`/admin/api/editor-showcase/assets${buildEditorShowcaseListQuery(query)}`,
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function reviewAdminEditorShowcaseAsset(
|
|
token: string,
|
|
payload: AdminEditorShowcaseReviewRequest,
|
|
) {
|
|
return request<AdminEditorShowcaseAssetResponse>(
|
|
'/admin/api/editor-showcase/assets/review',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function updateAdminEditorShowcaseDisplay(
|
|
token: string,
|
|
payload: AdminEditorShowcaseDisplayRequest,
|
|
) {
|
|
return request<AdminEditorShowcaseAssetResponse>(
|
|
'/admin/api/editor-showcase/assets/display',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function getAdminEditorShowcaseCampaign(token: string) {
|
|
return request<AdminEditorShowcaseCampaignResponse>(
|
|
'/admin/api/editor-showcase/campaign',
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function upsertAdminEditorShowcaseCampaign(
|
|
token: string,
|
|
payload: AdminUpsertEditorShowcaseCampaignRequest,
|
|
) {
|
|
return request<AdminEditorShowcaseCampaignResponse>(
|
|
'/admin/api/editor-showcase/campaign',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function uploadAdminEditorShowcaseCampaignImage(
|
|
token: string,
|
|
file: File,
|
|
): Promise<AdminUploadedEditorShowcaseCampaignImage> {
|
|
const contentType = resolveAdminImageContentType(file);
|
|
const dimensions = await readAdminImageFileDimensions(file);
|
|
const response = await request<AdminCreateEditorShowcaseCampaignImageUploadTicketResponse>(
|
|
'/admin/api/editor-showcase/campaign/image-upload-ticket',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: {
|
|
fileName: file.name.trim() || 'showcase-campaign.png',
|
|
contentType,
|
|
contentLength: file.size,
|
|
} satisfies AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
|
|
},
|
|
);
|
|
await postAdminDirectUploadFile(response.upload, file);
|
|
const objectKey = response.upload.objectKey.trim().replace(/^\/+/u, '');
|
|
return {
|
|
imageSrc: objectKey ? `/${objectKey}` : response.upload.legacyPublicPath,
|
|
imageObjectKey: objectKey,
|
|
imageWidth: dimensions.imageWidth,
|
|
imageHeight: dimensions.imageHeight,
|
|
legacyPublicPath: response.upload.legacyPublicPath,
|
|
};
|
|
}
|
|
|
|
export function listProfileRedeemCodes(token: string) {
|
|
return request<ProfileRedeemCodeAdminListResponse>(
|
|
'/admin/api/profile/redeem-codes',
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function upsertProfileRedeemCode(
|
|
token: string,
|
|
payload: AdminUpsertProfileRedeemCodeRequest,
|
|
) {
|
|
return request<ProfileRedeemCodeAdminResponse>(
|
|
'/admin/api/profile/redeem-codes',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function listProfileInviteCodes(token: string) {
|
|
return request<ProfileInviteCodeAdminListResponse>(
|
|
'/admin/api/profile/invite-codes',
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function upsertProfileInviteCode(
|
|
token: string,
|
|
payload: AdminUpsertProfileInviteCodeRequest,
|
|
) {
|
|
return request<ProfileInviteCodeAdminResponse>(
|
|
'/admin/api/profile/invite-codes',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function disableProfileRedeemCode(
|
|
token: string,
|
|
payload: AdminDisableProfileRedeemCodeRequest,
|
|
) {
|
|
return request<ProfileRedeemCodeAdminResponse>(
|
|
'/admin/api/profile/redeem-codes/disable',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function listProfileTaskConfigs(token: string) {
|
|
return request<ProfileTaskConfigAdminListResponse>(
|
|
'/admin/api/profile/tasks',
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function upsertProfileTaskConfig(
|
|
token: string,
|
|
payload: AdminUpsertProfileTaskConfigRequest,
|
|
) {
|
|
return request<ProfileTaskConfigAdminResponse>('/admin/api/profile/tasks', {
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
});
|
|
}
|
|
|
|
export function disableProfileTaskConfig(
|
|
token: string,
|
|
payload: AdminDisableProfileTaskConfigRequest,
|
|
) {
|
|
return request<ProfileTaskConfigAdminResponse>(
|
|
'/admin/api/profile/tasks/disable',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function getProfileWalletConfig(token: string) {
|
|
return request<ProfileWalletConfigAdminResponse>(
|
|
'/admin/api/profile/wallet-config',
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function upsertProfileWalletConfig(
|
|
token: string,
|
|
payload: AdminUpsertProfileWalletConfigRequest,
|
|
) {
|
|
return request<ProfileWalletConfigAdminResponse>(
|
|
'/admin/api/profile/wallet-config',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function listProfileRechargeProducts(token: string) {
|
|
return request<ProfileRechargeProductConfigAdminListResponse>(
|
|
'/admin/api/profile/recharge-products',
|
|
{ token },
|
|
);
|
|
}
|
|
|
|
export function upsertProfileRechargeProduct(
|
|
token: string,
|
|
payload: AdminUpsertProfileRechargeProductRequest,
|
|
) {
|
|
return request<ProfileRechargeProductConfigAdminResponse>(
|
|
'/admin/api/profile/recharge-products',
|
|
{
|
|
method: 'POST',
|
|
token,
|
|
body: payload,
|
|
},
|
|
);
|
|
}
|
|
|
|
function normalizeBaseUrl(value: string) {
|
|
return value.trim().replace(/\/+$/, '');
|
|
}
|
|
|
|
function buildRequestUrl(path: string) {
|
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
return `${ADMIN_API_BASE_URL}${normalizedPath}`;
|
|
}
|
|
|
|
function buildAssetReadUrlQuery(query: AdminAssetReadUrlQuery) {
|
|
const params = new URLSearchParams();
|
|
const objectKey = query.objectKey?.trim().replace(/^\/+/u, '') ?? '';
|
|
const legacyPublicPath = query.legacyPublicPath?.trim() ?? '';
|
|
if (objectKey) {
|
|
params.set('objectKey', objectKey);
|
|
} else if (legacyPublicPath) {
|
|
params.set(
|
|
'legacyPublicPath',
|
|
`/${legacyPublicPath.replace(/^\/+/u, '')}`,
|
|
);
|
|
}
|
|
if (
|
|
typeof query.expireSeconds === 'number' &&
|
|
Number.isFinite(query.expireSeconds) &&
|
|
query.expireSeconds > 0
|
|
) {
|
|
params.set('expireSeconds', String(Math.floor(query.expireSeconds)));
|
|
}
|
|
const queryString = params.toString();
|
|
return queryString ? `?${queryString}` : '';
|
|
}
|
|
|
|
function resolveAdminImageContentType(file: File) {
|
|
const declaredType = file.type.trim();
|
|
if (declaredType.startsWith('image/')) {
|
|
return declaredType;
|
|
}
|
|
const extension = file.name.trim().toLowerCase().match(/\.([a-z0-9]+)$/u)?.[1];
|
|
if (extension === 'jpg' || extension === 'jpeg') {
|
|
return 'image/jpeg';
|
|
}
|
|
if (extension === 'png') {
|
|
return 'image/png';
|
|
}
|
|
if (extension === 'webp') {
|
|
return 'image/webp';
|
|
}
|
|
if (extension === 'gif') {
|
|
return 'image/gif';
|
|
}
|
|
return declaredType || 'application/octet-stream';
|
|
}
|
|
|
|
function normalizeAdminImageDimensions(width: number, height: number) {
|
|
const imageWidth = Math.round(width);
|
|
const imageHeight = Math.round(height);
|
|
if (
|
|
!Number.isFinite(imageWidth) ||
|
|
!Number.isFinite(imageHeight) ||
|
|
imageWidth <= 0 ||
|
|
imageHeight <= 0
|
|
) {
|
|
return null;
|
|
}
|
|
return { imageWidth, imageHeight };
|
|
}
|
|
|
|
async function readAdminImageFileDimensions(file: File) {
|
|
if (typeof createImageBitmap === 'function') {
|
|
try {
|
|
const bitmap = await createImageBitmap(file);
|
|
const dimensions = normalizeAdminImageDimensions(
|
|
bitmap.width,
|
|
bitmap.height,
|
|
);
|
|
bitmap.close();
|
|
if (dimensions) {
|
|
return dimensions;
|
|
}
|
|
} catch {
|
|
// Fall back to HTMLImageElement decoding below.
|
|
}
|
|
}
|
|
|
|
if (
|
|
typeof Image === 'undefined' ||
|
|
typeof URL === 'undefined' ||
|
|
typeof URL.createObjectURL !== 'function'
|
|
) {
|
|
throw new Error('读取活动卡图片尺寸失败,请重新选择图片');
|
|
}
|
|
|
|
return new Promise<{ imageWidth: number; imageHeight: number }>(
|
|
(resolve, reject) => {
|
|
const objectUrl = URL.createObjectURL(file);
|
|
const image = new Image();
|
|
const cleanup = () => {
|
|
if (typeof URL.revokeObjectURL === 'function') {
|
|
URL.revokeObjectURL(objectUrl);
|
|
}
|
|
};
|
|
image.onload = () => {
|
|
cleanup();
|
|
const dimensions = normalizeAdminImageDimensions(
|
|
image.naturalWidth || image.width,
|
|
image.naturalHeight || image.height,
|
|
);
|
|
if (dimensions) {
|
|
resolve(dimensions);
|
|
return;
|
|
}
|
|
reject(new Error('读取活动卡图片尺寸失败,请重新选择图片'));
|
|
};
|
|
image.onerror = () => {
|
|
cleanup();
|
|
reject(new Error('读取活动卡图片尺寸失败,请重新选择图片'));
|
|
};
|
|
image.src = objectUrl;
|
|
},
|
|
);
|
|
}
|
|
|
|
function buildAdminDirectUploadFormData(
|
|
upload: AdminDirectUploadTicketPayload,
|
|
file: File,
|
|
) {
|
|
const formData = new FormData();
|
|
Object.entries(upload.formFields).forEach(([key, value]) => {
|
|
if (value !== null && value !== undefined) {
|
|
formData.append(key, value);
|
|
}
|
|
});
|
|
formData.append('file', file, file.name);
|
|
return formData;
|
|
}
|
|
|
|
async function postAdminDirectUploadFile(
|
|
upload: AdminDirectUploadTicketPayload,
|
|
file: File,
|
|
) {
|
|
const response = await fetch(upload.host, {
|
|
method: 'POST',
|
|
body: buildAdminDirectUploadFormData(upload, file),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`上传活动卡图片失败:HTTP ${response.status}`);
|
|
}
|
|
}
|
|
|
|
function buildQueryString(query: AdminTrackingEventListQuery) {
|
|
const params = new URLSearchParams();
|
|
appendQueryParam(params, 'eventKey', query.eventKey);
|
|
appendQueryParam(params, 'userId', query.userId);
|
|
appendQueryParam(params, 'scopeKind', query.scopeKind);
|
|
appendQueryParam(params, 'scopeId', query.scopeId);
|
|
appendQueryParam(params, 'startDate', query.startDate);
|
|
appendQueryParam(params, 'endDate', query.endDate);
|
|
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
|
params.set('limit', String(query.limit));
|
|
}
|
|
if (query.exportAll) {
|
|
params.set('exportAll', 'true');
|
|
}
|
|
const queryString = params.toString();
|
|
return queryString ? `?${queryString}` : '';
|
|
}
|
|
|
|
function buildDashboardQuery(query: AdminDashboardQuery) {
|
|
const params = new URLSearchParams();
|
|
appendQueryParam(params, 'granularity', query.granularity);
|
|
appendQueryParam(params, 'anchor', query.anchor);
|
|
appendQueryParam(params, 'startDate', query.startDate);
|
|
appendQueryParam(params, 'endDate', query.endDate);
|
|
const queryString = params.toString();
|
|
return queryString ? `?${queryString}` : '';
|
|
}
|
|
|
|
function buildDatabaseTableRowsQuery(query: AdminDatabaseTableRowsQuery) {
|
|
const params = new URLSearchParams();
|
|
appendQueryParam(params, 'search', query.search);
|
|
appendQueryParam(params, 'filters', query.filters);
|
|
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
|
params.set('limit', String(query.limit));
|
|
}
|
|
if (typeof query.page === 'number' && Number.isFinite(query.page)) {
|
|
params.set('page', String(query.page));
|
|
}
|
|
appendQueryParam(params, 'sortColumn', query.sortColumn);
|
|
appendQueryParam(params, 'sortDirection', query.sortDirection);
|
|
const queryString = params.toString();
|
|
return queryString ? `?${queryString}` : '';
|
|
}
|
|
|
|
function buildEditorAssetListQuery(query: AdminEditorAssetListQuery) {
|
|
const params = new URLSearchParams();
|
|
appendQueryParam(params, 'cursor', query.cursor);
|
|
appendQueryParam(params, 'ownerUserId', query.ownerUserId);
|
|
appendQueryParam(params, 'keyword', query.keyword);
|
|
appendQueryParam(params, 'createdAfter', query.createdAfter);
|
|
appendQueryParam(params, 'createdBefore', query.createdBefore);
|
|
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
|
params.set('limit', String(query.limit));
|
|
}
|
|
const queryString = params.toString();
|
|
return queryString ? `?${queryString}` : '';
|
|
}
|
|
|
|
function buildEditorShowcaseListQuery(query: AdminEditorShowcaseListQuery) {
|
|
const params = new URLSearchParams();
|
|
appendQueryParam(params, 'cursor', query.cursor);
|
|
appendQueryParam(params, 'ownerUserId', query.ownerUserId);
|
|
appendQueryParam(params, 'reviewStatus', query.reviewStatus);
|
|
appendQueryParam(params, 'submittedAfter', query.submittedAfter);
|
|
appendQueryParam(params, 'submittedBefore', query.submittedBefore);
|
|
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
|
params.set('limit', String(query.limit));
|
|
}
|
|
const queryString = params.toString();
|
|
return queryString ? `?${queryString}` : '';
|
|
}
|
|
|
|
function appendQueryParam(
|
|
params: URLSearchParams,
|
|
key: string,
|
|
value: string | null | undefined,
|
|
) {
|
|
const trimmed = value?.trim();
|
|
if (trimmed) {
|
|
params.set(key, trimmed);
|
|
}
|
|
}
|
|
|
|
function parseJsonResponse(responseText: string): unknown {
|
|
if (!responseText.trim()) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(responseText) as unknown;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function unwrapSuccessPayload<T>(payload: unknown): T {
|
|
if (isRecord(payload) && 'data' in payload) {
|
|
return (payload as ApiSuccessEnvelope<T>).data as T;
|
|
}
|
|
|
|
return payload as T;
|
|
}
|
|
|
|
function buildAdminApiError(
|
|
response: Response,
|
|
payload: unknown,
|
|
responseText: string,
|
|
) {
|
|
const envelope = isRecord(payload) ? (payload as ApiErrorEnvelope) : null;
|
|
const errorPayload = envelope?.error;
|
|
const details = isRecord(errorPayload?.details) ? errorPayload.details : null;
|
|
const detailsMessage =
|
|
typeof details?.message === 'string' ? details.message.trim() : '';
|
|
const payloadMessage =
|
|
typeof errorPayload?.message === 'string'
|
|
? errorPayload.message.trim()
|
|
: '';
|
|
const topLevelMessage =
|
|
typeof envelope?.message === 'string' ? envelope.message.trim() : '';
|
|
const message =
|
|
detailsMessage ||
|
|
payloadMessage ||
|
|
topLevelMessage ||
|
|
response.statusText ||
|
|
`HTTP ${response.status}`;
|
|
|
|
return new AdminApiError({
|
|
message,
|
|
status: response.status,
|
|
code: errorPayload?.code,
|
|
details,
|
|
meta: envelope?.meta ?? null,
|
|
responseText,
|
|
});
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
}
|