1
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Real API client for connecting to the FastAPI backend.
|
||||
* Handles auth tokens, request/response encryption, snake_case→camelCase conversion.
|
||||
*/
|
||||
|
||||
import { encrypt, decrypt } from './crypto';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
||||
const USE_ENCRYPTION = !!import.meta.env.VITE_ENCRYPTION_KEY;
|
||||
|
||||
interface RequestOptions {
|
||||
method?: string;
|
||||
body?: unknown;
|
||||
auth?: boolean;
|
||||
encryptBody?: boolean;
|
||||
}
|
||||
|
||||
/** Convert snake_case string to camelCase */
|
||||
function toCamel(s: string): string {
|
||||
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/** Recursively convert all snake_case keys in an object/array to camelCase */
|
||||
function keysToCamel(obj: unknown): unknown {
|
||||
if (Array.isArray(obj)) return obj.map(keysToCamel);
|
||||
if (obj !== null && typeof obj === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [toCamel(k), keysToCamel(v)])
|
||||
);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function getToken(): string | null {
|
||||
return localStorage.getItem('auth_token');
|
||||
}
|
||||
|
||||
export function setToken(token: string): void {
|
||||
localStorage.setItem('auth_token', token);
|
||||
}
|
||||
|
||||
export function clearToken(): void {
|
||||
localStorage.removeItem('auth_token');
|
||||
}
|
||||
|
||||
/** Try to decrypt response data, return null if it fails */
|
||||
async function tryDecrypt(data: string): Promise<string | null> {
|
||||
try {
|
||||
return await decrypt(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { method = 'GET', body, auth = true, encryptBody = USE_ENCRYPTION } = options;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (auth) {
|
||||
const token = getToken();
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
let bodyStr: string | undefined;
|
||||
if (body !== undefined) {
|
||||
const json = JSON.stringify(body);
|
||||
if (encryptBody) {
|
||||
headers['X-Encrypted'] = 'true';
|
||||
bodyStr = JSON.stringify({ data: await encrypt(json) });
|
||||
} else {
|
||||
bodyStr = json;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(`${BASE_URL}/api${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: bodyStr,
|
||||
});
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
|
||||
const text = await res.text();
|
||||
if (!text) return undefined as T;
|
||||
|
||||
// Parse the response body
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch {
|
||||
throw new Error(`响应解析失败 (${res.status})`);
|
||||
}
|
||||
|
||||
// If the response is encrypted, decrypt it
|
||||
const hasEncryptedData = parsed && typeof parsed.data === 'string';
|
||||
if (hasEncryptedData && encryptBody) {
|
||||
const decrypted = await tryDecrypt(parsed.data);
|
||||
if (decrypted !== null) {
|
||||
parsed = JSON.parse(decrypted);
|
||||
}
|
||||
// If decryption fails, fall through — parsed still has {data: "..."}
|
||||
// which means the response was NOT actually encrypted (just has a "data" field)
|
||||
}
|
||||
|
||||
// Handle error responses
|
||||
if (!res.ok) {
|
||||
const msg = parsed?.detail || `请求失败 (${res.status})`;
|
||||
if (res.status === 401) {
|
||||
clearToken();
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
return keysToCamel(parsed) as T;
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
export const api = {
|
||||
get: <T>(path: string, auth = true) => apiRequest<T>(path, { auth }),
|
||||
post: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'POST', body, auth }),
|
||||
put: <T>(path: string, body?: unknown, auth = true) => apiRequest<T>(path, { method: 'PUT', body, auth }),
|
||||
delete: <T>(path: string, auth = true) => apiRequest<T>(path, { method: 'DELETE', auth }),
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* AES-256-GCM encryption/decryption for API request/response.
|
||||
* Uses Web Crypto API with a shared symmetric key.
|
||||
*/
|
||||
|
||||
const ALGO = 'AES-GCM';
|
||||
const IV_LENGTH = 12;
|
||||
const TAG_LENGTH = 128;
|
||||
|
||||
let cryptoKey: CryptoKey | null = null;
|
||||
|
||||
async function getCryptoKey(): Promise<CryptoKey> {
|
||||
if (cryptoKey) return cryptoKey;
|
||||
const keyB64 = import.meta.env.VITE_ENCRYPTION_KEY || '';
|
||||
if (!keyB64) throw new Error('VITE_ENCRYPTION_KEY not configured');
|
||||
let keyBytes = Uint8Array.from(atob(keyB64), c => c.charCodeAt(0));
|
||||
// AES-256 requires exactly 32 bytes — pad or truncate to match backend
|
||||
if (keyBytes.length !== 32) {
|
||||
const padded = new Uint8Array(32);
|
||||
padded.set(keyBytes.slice(0, 32));
|
||||
keyBytes = padded;
|
||||
}
|
||||
cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: ALGO }, false, ['encrypt', 'decrypt']);
|
||||
return cryptoKey;
|
||||
}
|
||||
|
||||
export async function encrypt(plaintext: string): Promise<string> {
|
||||
const key = await getCryptoKey();
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||
const encoded = new TextEncoder().encode(plaintext);
|
||||
const cipherBuf = await crypto.subtle.encrypt({ name: ALGO, iv, tagLength: TAG_LENGTH }, key, encoded);
|
||||
const cipherBytes = new Uint8Array(cipherBuf);
|
||||
// Prepend IV to ciphertext
|
||||
const combined = new Uint8Array(iv.length + cipherBytes.length);
|
||||
combined.set(iv);
|
||||
combined.set(cipherBytes, iv.length);
|
||||
return btoa(String.fromCharCode(...combined));
|
||||
}
|
||||
|
||||
export async function decrypt(cipherB64: string): Promise<string> {
|
||||
const key = await getCryptoKey();
|
||||
const combined = Uint8Array.from(atob(cipherB64), c => c.charCodeAt(0));
|
||||
const iv = combined.slice(0, IV_LENGTH);
|
||||
const cipherBytes = combined.slice(IV_LENGTH);
|
||||
const plainBuf = await crypto.subtle.decrypt({ name: ALGO, iv, tagLength: TAG_LENGTH }, key, cipherBytes);
|
||||
return new TextDecoder().decode(plainBuf);
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* API abstraction layer.
|
||||
* Switches between mock data and real backend based on VITE_USE_MOCK env var.
|
||||
*/
|
||||
|
||||
import { api, setToken, clearToken } from './client';
|
||||
import * as mock from './mock';
|
||||
import type {
|
||||
User, CreditRecord, Project, GenerationRecord, OptimizeParams, GenerateParams, OptimizeResult,
|
||||
Industry, IndustryConfig, AdminUser, AdminStats, ModelConfig, SystemConfig, AdminNotification,
|
||||
} from '../types';
|
||||
|
||||
const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true';
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────
|
||||
|
||||
export async function login(username: string, password: string, captchaToken?: string, rememberMe?: boolean): Promise<User> {
|
||||
if (USE_MOCK) return mock.mockLogin({ username, password });
|
||||
const res = await api.post<{ accessToken: string; user: User }>('/auth/login', { username, password, captcha_token: captchaToken, remember_me: rememberMe || false }, false);
|
||||
setToken(res.accessToken);
|
||||
return res.user;
|
||||
}
|
||||
|
||||
export async function register(phone: string, code: string, password: string): Promise<User> {
|
||||
const res = await api.post<{ accessToken: string; user: User }>('/auth/register', { phone, code, password }, false);
|
||||
setToken(res.accessToken);
|
||||
return res.user;
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
if (USE_MOCK) return mock.mockLogout();
|
||||
await api.post('/auth/logout');
|
||||
clearToken();
|
||||
}
|
||||
|
||||
export async function getUser(): Promise<User | null> {
|
||||
if (USE_MOCK) return mock.mockGetUser();
|
||||
try {
|
||||
return await api.get<User>('/auth/me');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function changePassword(oldPwd: string, newPwd: string): Promise<void> {
|
||||
if (USE_MOCK) return;
|
||||
await api.post('/auth/change-password', { old_password: oldPwd, new_password: newPwd });
|
||||
}
|
||||
|
||||
// ── Projects ──────────────────────────────────────────────
|
||||
|
||||
export async function getProjects(): Promise<Project[]> {
|
||||
if (USE_MOCK) return mock.mockGetProjects();
|
||||
return api.get<Project[]>('/projects');
|
||||
}
|
||||
|
||||
export async function createProject(name: string, industry: Industry): Promise<Project> {
|
||||
if (USE_MOCK) return mock.mockCreateProject(name, industry);
|
||||
return api.post<Project>('/projects', { name, industry });
|
||||
}
|
||||
|
||||
export async function deleteProject(id: string): Promise<void> {
|
||||
if (USE_MOCK) return mock.mockDeleteProject(id);
|
||||
await api.delete(`/projects/${id}`);
|
||||
}
|
||||
|
||||
// ── Generation ────────────────────────────────────────────
|
||||
|
||||
export async function getRecords(projectId?: string): Promise<GenerationRecord[]> {
|
||||
if (USE_MOCK) return mock.mockGetGenerationRecords(projectId);
|
||||
const q = projectId ? `?project_id=${projectId}` : '';
|
||||
return api.get<GenerationRecord[]>(`/generation-records${q}`);
|
||||
}
|
||||
|
||||
export async function optimizePrompt(
|
||||
projectId: string, params: OptimizeParams
|
||||
): Promise<OptimizeResult> {
|
||||
if (USE_MOCK) return mock.mockOptimizePrompt(projectId, params as any);
|
||||
return api.post('/generation-records/optimize', {
|
||||
project_id: projectId,
|
||||
prompt: params.prompt,
|
||||
duration: params.duration,
|
||||
references: params.references || null,
|
||||
idempotency_key: params.idempotencyKey || null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadImage(file: File): Promise<{ url: string; filename: string }> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const res = await fetch(`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}/api/generation-records/upload-image`, {
|
||||
method: 'POST',
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
body: form,
|
||||
});
|
||||
if (!res.ok) throw new Error('图片上传失败');
|
||||
const data = await res.json();
|
||||
return { url: data.url, filename: data.filename };
|
||||
}
|
||||
|
||||
export async function uploadVideo(file: File): Promise<{ url: string; filename: string }> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const res = await fetch(`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}/api/generation-records/upload-video`, {
|
||||
method: 'POST',
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
body: form,
|
||||
});
|
||||
if (!res.ok) throw new Error('视频上传失败');
|
||||
const data = await res.json();
|
||||
return { url: data.url, filename: data.filename };
|
||||
}
|
||||
|
||||
export async function deleteUpload(url: string): Promise<void> {
|
||||
await api.post(`/generation-records/delete-file?url=${encodeURIComponent(url)}`);
|
||||
}
|
||||
|
||||
export async function updateRecordPrompt(recordId: string, optimizedPrompt: string): Promise<void> {
|
||||
await api.put(`/generation-records/${recordId}/prompt`, { optimized_prompt: optimizedPrompt });
|
||||
}
|
||||
|
||||
export async function generateVideo(recordId: string, params: GenerateParams): Promise<GenerationRecord> {
|
||||
if (USE_MOCK) return mock.mockGenerateVideo(recordId);
|
||||
return api.post<GenerationRecord>(`/generation-records/${recordId}/generate`, {
|
||||
aspect_ratio: params.aspectRatio,
|
||||
resolution: params.resolution,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Credits ───────────────────────────────────────────────
|
||||
|
||||
export async function getCredits(): Promise<{ credits: number; records: CreditRecord[] }> {
|
||||
if (USE_MOCK) return mock.mockGetCredits();
|
||||
return api.get('/credits');
|
||||
}
|
||||
|
||||
// ── Captcha ───────────────────────────────────────────────
|
||||
|
||||
export async function getSliderCaptcha(): Promise<{ captcha_id: string; bg_image: string; slider_image: string }> {
|
||||
if (USE_MOCK) return { captcha_id: 'mock', bg_image: '', slider_image: '' };
|
||||
return api.get('/captcha/slider', false);
|
||||
}
|
||||
|
||||
export async function verifyCaptcha(captchaId: string, x: number): Promise<string> {
|
||||
if (USE_MOCK) return 'mock-token';
|
||||
const res = await api.post<{ token: string }>('/captcha/verify', { captcha_id: captchaId, x_offset: x }, false);
|
||||
return res.token;
|
||||
}
|
||||
|
||||
// ── Site Info ─────────────────────────────────────────────
|
||||
|
||||
export async function getSiteInfo(): Promise<{ siteName: string; siteLogo: string; userAgreementUrl: string; privacyPolicyUrl: string }> {
|
||||
if (USE_MOCK) return { siteName: 'VideoGen.AI', siteLogo: '', userAgreementUrl: '', privacyPolicyUrl: '' };
|
||||
return api.get('/auth/site-info', false);
|
||||
}
|
||||
|
||||
// ── Video Engines ─────────────────────────────────────────
|
||||
|
||||
export async function getVideoEngines(): Promise<{ items: { id: string; name: string; provider: string; supportedRatios: string[]; supportedResolutions: string[]; supportedDurations: number[] }[] }> {
|
||||
if (USE_MOCK) return { items: [{ id: 'mock', name: 'Seedance', provider: 'seedance', supportedRatios: ['16:9', '9:16', '1:1', '4:3', '3:4', '21:9'], supportedResolutions: ['480p', '720p', '1080p'], supportedDurations: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] }] };
|
||||
return api.get('/video-engines', false);
|
||||
}
|
||||
|
||||
// ── SMS ───────────────────────────────────────────────────
|
||||
|
||||
export async function sendSms(phone: string, captchaToken?: string): Promise<void> {
|
||||
if (USE_MOCK) return;
|
||||
await api.post('/sms/send', { phone, captcha_token: captchaToken }, false);
|
||||
}
|
||||
|
||||
export async function verifySms(phone: string, code: string): Promise<{ token: string }> {
|
||||
if (USE_MOCK) return { token: 'mock-sms-token' };
|
||||
return api.post('/sms/verify', { phone, code }, false);
|
||||
}
|
||||
|
||||
// ── Notifications ─────────────────────────────────────────
|
||||
|
||||
export async function getNotifications(): Promise<AdminNotification[]> {
|
||||
if (USE_MOCK) return mock.mockGetAdminNotifications();
|
||||
return api.get('/notifications');
|
||||
}
|
||||
|
||||
export async function getUnreadCount(): Promise<number> {
|
||||
if (USE_MOCK) return mock.mockGetAdminNotifications().then(n => n.filter(x => !x.isRead).length);
|
||||
const res = await api.get<{ count: number }>('/notifications/unread-count');
|
||||
return res.count;
|
||||
}
|
||||
|
||||
export async function markNotificationRead(id: string): Promise<void> {
|
||||
if (USE_MOCK) return;
|
||||
await api.put(`/notifications/${id}/read`);
|
||||
}
|
||||
|
||||
// ── Admin ─────────────────────────────────────────────────
|
||||
|
||||
export async function getAdminStats(): Promise<AdminStats> {
|
||||
if (USE_MOCK) return mock.mockGetAdminStats();
|
||||
return api.get('/admin/stats');
|
||||
}
|
||||
|
||||
export async function getAdminUsers(search?: string): Promise<AdminUser[]> {
|
||||
if (USE_MOCK) return mock.mockGetAdminUsers(search);
|
||||
const q = search ? `?search=${encodeURIComponent(search)}` : '';
|
||||
return api.get(`/admin/users${q}`);
|
||||
}
|
||||
|
||||
export async function adjustCredits(userId: string, amount: number, description: string): Promise<void> {
|
||||
if (USE_MOCK) return mock.mockAdjustCredits(userId, amount, description);
|
||||
await api.post(`/admin/users/${userId}/credits`, { amount, description });
|
||||
}
|
||||
|
||||
export async function toggleUserStatus(userId: string, isActive: boolean): Promise<void> {
|
||||
if (USE_MOCK) return mock.mockToggleUserStatus(userId, isActive);
|
||||
await api.put(`/admin/users/${userId}/status`, { is_active: isActive });
|
||||
}
|
||||
|
||||
export async function getModelConfigs(): Promise<ModelConfig[]> {
|
||||
if (USE_MOCK) return mock.mockGetModelConfigs();
|
||||
return api.get('/admin/model-configs');
|
||||
}
|
||||
|
||||
export async function saveModelConfig(config: Partial<ModelConfig> & { id?: string }): Promise<ModelConfig> {
|
||||
if (USE_MOCK) return mock.mockSaveModelConfig(config as any);
|
||||
if (config.id) return api.put(`/admin/model-configs/${config.id}`, config);
|
||||
return api.post('/admin/model-configs', config);
|
||||
}
|
||||
|
||||
export async function deleteModelConfig(id: string): Promise<void> {
|
||||
if (USE_MOCK) return mock.mockDeleteModelConfig(id);
|
||||
await api.delete(`/admin/model-configs/${id}`);
|
||||
}
|
||||
|
||||
export async function getSystemConfigs(): Promise<SystemConfig[]> {
|
||||
if (USE_MOCK) return mock.mockGetSystemConfigs();
|
||||
return api.get('/admin/system-configs');
|
||||
}
|
||||
|
||||
export async function updateSystemConfig(id: string, value: string): Promise<void> {
|
||||
if (USE_MOCK) return mock.mockUpdateSystemConfig(id, value);
|
||||
await api.put(`/admin/system-configs/${id}`, { value });
|
||||
}
|
||||
|
||||
// ── Industries ─────────────────────────────────────────────
|
||||
|
||||
export async function getIndustries(): Promise<IndustryConfig[]> {
|
||||
const data = await api.get<any[]>('/industries');
|
||||
return data.map((item: any) => {
|
||||
const skills = Array.isArray(item.skills) ? item.skills : [];
|
||||
const optionGroups = skills
|
||||
.filter((s: any) => s && s.type === 'option_group' && s.name && Array.isArray(s.options))
|
||||
.map((s: any) => ({ name: s.name, options: s.options }));
|
||||
return { ...item, optionGroups };
|
||||
});
|
||||
}
|
||||
|
||||
// ── Menu Config ────────────────────────────────────────────
|
||||
|
||||
export async function getMenuConfigs(): Promise<any[]> {
|
||||
return api.get('/menu-configs');
|
||||
}
|
||||
|
||||
// ── Recharge Packages ──────────────────────────────────────
|
||||
|
||||
export async function getRechargePackages(): Promise<any[]> {
|
||||
return api.get('/recharge-packages');
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
import type {
|
||||
User,
|
||||
CreditRecord,
|
||||
Project,
|
||||
GenerationRecord,
|
||||
OptimizeParams,
|
||||
OptimizeResult,
|
||||
LoginParams,
|
||||
Industry,
|
||||
AdminUser,
|
||||
AdminStats,
|
||||
ModelConfig,
|
||||
SystemConfig,
|
||||
AdminNotification,
|
||||
} from '../types';
|
||||
|
||||
// Simulated delay
|
||||
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// ── Mock Data ──────────────────────────────────────────────
|
||||
|
||||
let currentUser: User | null = null;
|
||||
|
||||
const MOCK_USER: User = {
|
||||
id: 'u-001',
|
||||
username: 'videomaker',
|
||||
email: 'demo@videogen.ai',
|
||||
credits: 2680,
|
||||
};
|
||||
|
||||
const MOCK_CREDIT_RECORDS: CreditRecord[] = [
|
||||
{ id: 'c-1', type: 'recharge', amount: 3000, description: '会员充值赠送', createdAt: '2026-04-28 10:00:00' },
|
||||
{ id: 'c-2', type: 'consume', amount: -120, description: '提示词优化 - 电商广告视频', createdAt: '2026-04-29 14:22:00' },
|
||||
{ id: 'c-3', type: 'consume', amount: -80, description: '提示词优化 - 教育课程视频', createdAt: '2026-04-30 09:15:00' },
|
||||
{ id: 'c-4', type: 'consume', amount: -120, description: '提示词优化 - 产品宣传视频', createdAt: '2026-05-01 16:40:00' },
|
||||
{ id: 'c-5', type: 'recharge', amount: 500, description: '活动赠送积分', createdAt: '2026-05-02 11:00:00' },
|
||||
{ id: 'c-6', type: 'consume', amount: -100, description: '提示词优化 - 品牌故事视频', createdAt: '2026-05-03 08:30:00' },
|
||||
];
|
||||
|
||||
let MOCK_PROJECTS: Project[] = [
|
||||
{ id: 'p-1', name: '618电商大促宣传片', industry: 'ecommerce', createdAt: '2026-04-28', updatedAt: '2026-05-01' },
|
||||
{ id: 'p-2', name: '在线课程推广视频', industry: 'education', createdAt: '2026-04-30', updatedAt: '2026-05-02' },
|
||||
{ id: 'p-3', name: '新游戏预告片', industry: 'gaming', createdAt: '2026-05-01', updatedAt: '2026-05-03' },
|
||||
];
|
||||
|
||||
let MOCK_RECORDS: GenerationRecord[] = [
|
||||
{
|
||||
id: 'r-1',
|
||||
projectId: 'p-1',
|
||||
projectName: '618电商大促宣传片',
|
||||
originalPrompt: '一个年轻女性在时尚直播间推荐夏季新款连衣裙,背景明亮温馨',
|
||||
optimizedPrompt: '镜头缓慢推近一位25岁左右的亚洲女性主播,她身穿亮色夏季连衣裙,站在精心布置的粉色系直播间内。身后是柔和的环形补光灯和商品展示架。她面带微笑,手势优雅地展示裙摆细节。暖色调LED背景墙上显示"618大促"字样。4K画质,浅景深,时尚美妆类短视频风格。',
|
||||
duration: 15,
|
||||
aspectRatio: '9:16',
|
||||
resolution: '1080p',
|
||||
status: 'completed',
|
||||
videoUrl: 'https://example.com/video1.mp4',
|
||||
textCreditsCost: 10,
|
||||
textTokensUsed: 850,
|
||||
creditsCost: 120,
|
||||
videoTokensUsed: 0,
|
||||
createdAt: '2026-04-29 14:22:00',
|
||||
generatedAt: '2026-04-29 14:25:00',
|
||||
},
|
||||
{
|
||||
id: 'r-2',
|
||||
projectId: 'p-2',
|
||||
projectName: '在线课程推广视频',
|
||||
originalPrompt: '学生在明亮的教室里用平板电脑学习编程课程',
|
||||
optimizedPrompt: '俯拍视角,一位大学生坐在现代化开放式学习空间的木质书桌前,手持iPad Pro,屏幕上显示Python代码编辑器界面。桌上摆放着咖啡杯、笔记本和绿色小盆栽。自然光从落地窗洒入,营造温暖的学习氛围。背景虚化处可见其他学生在安静学习。画面节奏舒缓,配以轻柔的钢琴背景音乐。',
|
||||
duration: 20,
|
||||
aspectRatio: '16:9',
|
||||
resolution: '1080p',
|
||||
status: 'completed',
|
||||
videoUrl: 'https://example.com/video2.mp4',
|
||||
textCreditsCost: 8,
|
||||
textTokensUsed: 720,
|
||||
creditsCost: 80,
|
||||
videoTokensUsed: 0,
|
||||
createdAt: '2026-04-30 09:15:00',
|
||||
generatedAt: '2026-04-30 09:18:00',
|
||||
},
|
||||
{
|
||||
id: 'r-3',
|
||||
projectId: 'p-1',
|
||||
projectName: '618电商大促宣传片',
|
||||
originalPrompt: '多个快递包裹从仓库货架上飞出,物流车快速配送',
|
||||
optimizedPrompt: '高速摄影风格,镜头从大型智能仓储中心内部开始,自动化机械臂精准抓取印有品牌Logo的快递包裹。包裹沿传送带高速移动,在分拣中心精准落入对应区域。画面切换至无人机和无人配送车在城市街道上进行最后一公里配送。最终画面定格在消费者微笑签收包裹的瞬间。整体采用蓝色科技感色调,快节奏剪辑。',
|
||||
duration: 10,
|
||||
aspectRatio: '16:9',
|
||||
resolution: '4K',
|
||||
status: 'prompt_optimized',
|
||||
textCreditsCost: 12,
|
||||
textTokensUsed: 960,
|
||||
creditsCost: 120,
|
||||
videoTokensUsed: 0,
|
||||
createdAt: '2026-05-01 16:40:00',
|
||||
},
|
||||
{
|
||||
id: 'r-4',
|
||||
projectId: 'p-3',
|
||||
projectName: '新游戏预告片',
|
||||
originalPrompt: '一个奇幻世界里的魔法城堡,龙在天空飞过',
|
||||
optimizedPrompt: '史诗级航拍镜头,一座哥特式魔法城堡矗立在云雾缭绕的山巅,城堡尖塔闪烁着神秘的紫色光芒。天空中一头银色巨龙展开双翼翱翔而过,鳞片在夕阳下折射出炫目光芒。镜头环绕城堡360度旋转,展示城堡周围悬浮的魔法水晶和瀑布。大气磅礴的管弦乐配乐,电影级CG画质,暗色调魔幻风格。',
|
||||
duration: 30,
|
||||
aspectRatio: '16:9',
|
||||
resolution: '4K',
|
||||
status: 'prompt_optimized',
|
||||
textCreditsCost: 10,
|
||||
textTokensUsed: 880,
|
||||
creditsCost: 100,
|
||||
videoTokensUsed: 0,
|
||||
createdAt: '2026-05-03 08:30:00',
|
||||
},
|
||||
];
|
||||
|
||||
// ── Mock API Functions ─────────────────────────────────────
|
||||
|
||||
export async function mockLogin(params: LoginParams): Promise<User> {
|
||||
await delay(800);
|
||||
if (!params.username || !params.password) {
|
||||
throw new Error('请输入用户名和密码');
|
||||
}
|
||||
currentUser = { ...MOCK_USER, username: params.username };
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
export async function mockLogout(): Promise<void> {
|
||||
await delay(300);
|
||||
currentUser = null;
|
||||
}
|
||||
|
||||
export async function mockGetUser(): Promise<User | null> {
|
||||
await delay(200);
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
export async function mockGetCredits(): Promise<{ credits: number; records: CreditRecord[] }> {
|
||||
await delay(400);
|
||||
return {
|
||||
credits: currentUser?.credits ?? 0,
|
||||
records: MOCK_CREDIT_RECORDS,
|
||||
};
|
||||
}
|
||||
|
||||
export async function mockGetProjects(): Promise<Project[]> {
|
||||
await delay(300);
|
||||
return [...MOCK_PROJECTS];
|
||||
}
|
||||
|
||||
export async function mockCreateProject(name: string, industry: Industry): Promise<Project> {
|
||||
await delay(500);
|
||||
const project: Project = {
|
||||
id: `p-${Date.now()}`,
|
||||
name,
|
||||
industry,
|
||||
createdAt: new Date().toISOString().slice(0, 10),
|
||||
updatedAt: new Date().toISOString().slice(0, 10),
|
||||
};
|
||||
MOCK_PROJECTS.unshift(project);
|
||||
return project;
|
||||
}
|
||||
|
||||
export async function mockDeleteProject(id: string): Promise<void> {
|
||||
await delay(300);
|
||||
MOCK_PROJECTS = MOCK_PROJECTS.filter((p) => p.id !== id);
|
||||
}
|
||||
|
||||
export async function mockOptimizePrompt(
|
||||
projectId: string,
|
||||
params: OptimizeParams
|
||||
): Promise<OptimizeResult> {
|
||||
await delay(1500);
|
||||
|
||||
const project = MOCK_PROJECTS.find((p) => p.id === projectId);
|
||||
const textCredits = Math.max(1, Math.ceil(params.prompt.length * 0.3));
|
||||
const textTokens = Math.round(params.prompt.length * 1.2);
|
||||
|
||||
if (currentUser) {
|
||||
currentUser.credits -= textCredits;
|
||||
}
|
||||
|
||||
const optimizedPromptMap: Record<string, string> = {
|
||||
'直播': `镜头缓慢推近一位25岁左右的亚洲女性主播,她身穿亮色系服装,站在精心布置的现代直播间内。身后是柔和的环形补光灯和多层商品展示架。她面带自信微笑,手势优雅地展示产品细节。暖色调LED背景墙上动态显示品牌元素。4K画质,浅景深,时尚美妆类短视频风格。`,
|
||||
'产品': `微距镜头缓缓推进,展示一件精致的科技产品放置在纯黑大理石台面上。柔和的三点布光突显产品的金属质感和流线型设计。镜头沿产品表面缓慢滑行,每一个细节——按钮、接口、材质纹理——都被清晰捕捉。背景为虚化的深蓝色渐变,偶尔有微弱的光斑闪烁。极简主义广告风格。`,
|
||||
'课程': `俯拍视角,一位学生坐在现代化学习空间的木质书桌前,手持平板设备,屏幕上显示丰富的学习内容。桌上摆放着咖啡杯、笔记本和绿色小盆栽。自然光从落地窗洒入,营造温暖的学习氛围。画面节奏舒缓,知识改变命运的叙事风格。`,
|
||||
'美食': `高速微距摄影,新鲜食材从空中落下慢动作特写:翠绿的蔬菜叶片、鲜红的番茄切片、金黄的芝士丝在暖色灯光下飞舞。镜头切换至厨师双手在光滑的不锈钢操作台上精心摆盘。最终成品——一道精致的创意料理在柔光中呈现,蒸汽袅袅升起。暖色调,美食纪录片风格。`,
|
||||
'品牌': `电影级运镜,镜头从城市天际线的黎明金光中缓缓下降,穿过玻璃幕墙的反光,进入一间充满设计感的创意办公室。设计师团队围坐在大屏幕前热烈讨论。镜头继续穿越屏幕,进入品牌视觉世界——色彩、字体、图像在三维空间中流动重组,最终凝聚成一个令人印象深刻的标志。品牌叙事风格。`,
|
||||
'游戏': `史诗级航拍镜头,一座宏伟的建筑矗立在云雾缭绕的山巅,闪烁着神秘的光芒。天空中壮观的景象展开,光芒在夕阳下折射出炫目光芒。镜头环绕建筑360度旋转,展示周围悬浮的元素。大气磅礴的配乐,电影级CG画质。`,
|
||||
};
|
||||
|
||||
let optimizedPrompt = `针对"${params.prompt}"的专业视频描述:`;
|
||||
const keys = Object.keys(optimizedPromptMap);
|
||||
const matched = keys.find((k) => params.prompt.includes(k));
|
||||
if (matched) {
|
||||
optimizedPrompt = optimizedPromptMap[matched];
|
||||
} else {
|
||||
optimizedPrompt = `精心构图的画面中,${params.prompt}。采用电影级镜头语言,自然光线与人工光源完美结合,营造出沉浸式视觉体验。画面色彩饱满而真实,细节丰富。运镜流畅自然,节奏张弛有度,完美适配${params.duration}秒时长。`;
|
||||
}
|
||||
|
||||
const record: GenerationRecord = {
|
||||
id: `r-${Date.now()}`,
|
||||
projectId,
|
||||
projectName: project?.name ?? '未知项目',
|
||||
originalPrompt: params.prompt,
|
||||
optimizedPrompt,
|
||||
duration: params.duration,
|
||||
status: 'prompt_optimized',
|
||||
textCreditsCost: textCredits,
|
||||
textTokensUsed: textTokens,
|
||||
creditsCost: 0,
|
||||
videoTokensUsed: 0,
|
||||
createdAt: new Date().toLocaleString('zh-CN'),
|
||||
};
|
||||
|
||||
MOCK_RECORDS.unshift(record);
|
||||
|
||||
return { optimizedPrompt, textCreditsCost: textCredits, textTokensUsed: textTokens, record };
|
||||
}
|
||||
|
||||
export async function mockGenerateVideo(recordId: string): Promise<GenerationRecord> {
|
||||
await delay(2000);
|
||||
const record = MOCK_RECORDS.find((r) => r.id === recordId);
|
||||
if (!record) throw new Error('记录不存在');
|
||||
|
||||
record.status = 'completed';
|
||||
record.videoUrl = `https://example.com/video-${recordId}.mp4`;
|
||||
record.generatedAt = new Date().toLocaleString('zh-CN');
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
export async function mockGetGenerationRecords(projectId?: string): Promise<GenerationRecord[]> {
|
||||
await delay(300);
|
||||
if (projectId) {
|
||||
return MOCK_RECORDS.filter((r) => r.projectId === projectId);
|
||||
}
|
||||
return [...MOCK_RECORDS];
|
||||
}
|
||||
|
||||
// ── Admin Mock Data ───────────────────────────────────
|
||||
|
||||
const MOCK_ADMIN_USERS: AdminUser[] = [
|
||||
{ id: 'u-001', username: 'videomaker', email: 'demo@videogen.ai', credits: 2680, isActive: true, isAdmin: false, userType: 'normal', createdAt: '2026-04-15', lastLoginAt: '2026-05-06 14:30' },
|
||||
{ id: 'u-002', username: 'designer', email: 'designer@example.com', credits: 520, isActive: true, isAdmin: false, userType: 'normal', createdAt: '2026-04-20', lastLoginAt: '2026-05-05 09:12' },
|
||||
{ id: 'u-003', username: 'marketer', email: 'mkt@company.com', phone: '13800138000', credits: 0, isActive: true, isAdmin: false, userType: 'normal', createdAt: '2026-04-25', lastLoginAt: '2026-05-04 16:45' },
|
||||
{ id: 'u-004', username: 'editor', email: 'editor@studio.com', credits: 1500, isActive: false, isAdmin: false, userType: 'normal', createdAt: '2026-04-10', lastLoginAt: '2026-04-28 11:20' },
|
||||
{ id: 'u-005', username: 'admin', email: 'admin@videogen.ai', credits: 10000, isActive: true, isAdmin: true, userType: 'admin', createdAt: '2026-04-01', lastLoginAt: '2026-05-07 08:00' },
|
||||
];
|
||||
|
||||
let MOCK_MODEL_CONFIGS: ModelConfig[] = [
|
||||
{ id: 'm-1', name: 'GPT-4o', provider: 'openai_compatible', apiBase: 'https://api.openai.com/v1', apiKey: 'sk-****', modelName: 'gpt-4o', weight: 3, maxTokens: 4096, temperature: 0.7, isActive: true, priority: 1 },
|
||||
{ id: 'm-2', name: 'DeepSeek-V3', provider: 'openai_compatible', apiBase: 'https://api.deepseek.com/v1', apiKey: 'sk-****', modelName: 'deepseek-chat', weight: 2, maxTokens: 4096, temperature: 0.7, isActive: true, priority: 2 },
|
||||
{ id: 'm-3', name: 'Mock模式', provider: 'mock', apiBase: '', apiKey: '', modelName: 'mock', weight: 1, maxTokens: 2048, temperature: 0.5, isActive: false, priority: 0 },
|
||||
];
|
||||
|
||||
let MOCK_SYSTEM_CONFIGS: SystemConfig[] = [
|
||||
{ id: 'sc-1', key: 'site_name', value: 'VideoGen.AI', description: '网站名称' },
|
||||
{ id: 'sc-2', key: 'site_logo', value: '', description: '网站Logo URL' },
|
||||
{ id: 'sc-3', key: 'seo_title', value: 'VideoGen.AI - AI视频生成平台', description: 'SEO标题' },
|
||||
{ id: 'sc-4', key: 'seo_description', value: '专业的AI视频生成服务,一键将文字转化为精美视频', description: 'SEO描述' },
|
||||
{ id: 'sc-5', key: 'seo_keywords', value: 'AI视频,视频生成,人工智能,AIGC,短视频', description: 'SEO关键词' },
|
||||
];
|
||||
|
||||
const MOCK_ADMIN_NOTIFICATIONS: AdminNotification[] = [
|
||||
{ id: 'n-1', title: '系统上线通知', content: 'VideoGen.AI 平台正式上线,欢迎体验AI视频生成功能!', type: 'system', isRead: false, createdAt: '2026-05-01 09:00:00' },
|
||||
{ id: 'n-2', title: '积分充值优惠', content: '限时活动:充值进阶包额外赠送200积分,活动截止5月15日。', type: 'credit', isRead: false, createdAt: '2026-05-03 10:00:00' },
|
||||
{ id: 'n-3', title: '功能更新', content: '新增9:16竖屏比例支持,适配抖音、快手等短视频平台。', type: 'system', isRead: true, createdAt: '2026-05-05 14:00:00' },
|
||||
];
|
||||
|
||||
// ── Admin Mock API Functions ──────────────────────────
|
||||
|
||||
export async function mockGetAdminStats(): Promise<AdminStats> {
|
||||
await delay(500);
|
||||
return {
|
||||
totalUsers: 5,
|
||||
totalProjects: 12,
|
||||
totalGenerations: 48,
|
||||
totalRevenue: 8560,
|
||||
creditsConsumedToday: 320,
|
||||
};
|
||||
}
|
||||
|
||||
export async function mockGetAdminUsers(search?: string): Promise<AdminUser[]> {
|
||||
await delay(400);
|
||||
if (search) {
|
||||
return MOCK_ADMIN_USERS.filter(u =>
|
||||
u.username.includes(search) || u.email.includes(search)
|
||||
);
|
||||
}
|
||||
return [...MOCK_ADMIN_USERS];
|
||||
}
|
||||
|
||||
export async function mockAdjustCredits(userId: string, amount: number, _description: string): Promise<void> {
|
||||
await delay(500);
|
||||
const user = MOCK_ADMIN_USERS.find(u => u.id === userId);
|
||||
if (user) user.credits += amount;
|
||||
}
|
||||
|
||||
export async function mockToggleUserStatus(userId: string, isActive: boolean): Promise<void> {
|
||||
await delay(300);
|
||||
const user = MOCK_ADMIN_USERS.find(u => u.id === userId);
|
||||
if (user) user.isActive = isActive;
|
||||
}
|
||||
|
||||
export async function mockGetModelConfigs(): Promise<ModelConfig[]> {
|
||||
await delay(300);
|
||||
return [...MOCK_MODEL_CONFIGS];
|
||||
}
|
||||
|
||||
export async function mockSaveModelConfig(config: Omit<ModelConfig, 'id'> & { id?: string }): Promise<ModelConfig> {
|
||||
await delay(500);
|
||||
if (config.id) {
|
||||
const idx = MOCK_MODEL_CONFIGS.findIndex(m => m.id === config.id);
|
||||
if (idx >= 0) {
|
||||
MOCK_MODEL_CONFIGS[idx] = { ...config, id: config.id } as ModelConfig;
|
||||
return MOCK_MODEL_CONFIGS[idx];
|
||||
}
|
||||
}
|
||||
const newConfig: ModelConfig = { ...config, id: `m-${Date.now()}` } as ModelConfig;
|
||||
MOCK_MODEL_CONFIGS.push(newConfig);
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
export async function mockDeleteModelConfig(id: string): Promise<void> {
|
||||
await delay(300);
|
||||
MOCK_MODEL_CONFIGS = MOCK_MODEL_CONFIGS.filter(m => m.id !== id);
|
||||
}
|
||||
|
||||
export async function mockGetSystemConfigs(): Promise<SystemConfig[]> {
|
||||
await delay(300);
|
||||
return [...MOCK_SYSTEM_CONFIGS];
|
||||
}
|
||||
|
||||
export async function mockUpdateSystemConfig(id: string, value: string): Promise<void> {
|
||||
await delay(300);
|
||||
const config = MOCK_SYSTEM_CONFIGS.find(c => c.id === id);
|
||||
if (config) config.value = value;
|
||||
}
|
||||
|
||||
export async function mockGetAdminNotifications(): Promise<AdminNotification[]> {
|
||||
await delay(300);
|
||||
return [...MOCK_ADMIN_NOTIFICATIONS];
|
||||
}
|
||||
Reference in New Issue
Block a user