feat(mobile-upgrade): add IT0 App version management support

Add IT0 App as the third application in the mobile-upgrade frontend,
connecting to IT0's new version-service backend.

Changes:
- api-client.ts: added 'it0' to AppType union, APP_CONFIGS entry
  pointing to NEXT_PUBLIC_IT0_API_URL (default: it0api.szaiai.com),
  exported it0ApiClient instance
- version-repository-impl.ts: added it0VersionRepository instance,
  updated constructor client selection for it0, updated
  getVersionRepository() factory with switch/case
- page.tsx: added IT0 App to APP_LABELS, added emerald green toggle
  button in app selector
- upload-modal.tsx: fixed existing bug where parsePackage() was
  hardcoded to use mobile repository regardless of selected app;
  now uses getVersionRepository(appType) dynamically

Backend: IT0 version-service at it0api.szaiai.com/api/v1/versions
Env var: NEXT_PUBLIC_IT0_API_URL

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-03 07:49:37 -08:00
parent 2b2e1efc7a
commit 33ae08c90f
4 changed files with 42 additions and 12 deletions

View File

@ -2,15 +2,16 @@
* *
* *
* : * :
* App App * App App IT0 App
* *
* *
* : * :
* - App (mobile): admin-service * - App (mobile): admin-service
* - App (mining): mining-admin-service * - App (mining): mining-admin-service
* - IT0 App (it0): IT0 version-service
* *
* : * :
* 1. - * 1. -
* 2. - Android/iOS * 2. - Android/iOS
* 3. - APK/IPA * 3. - APK/IPA
* 4. - / * 4. - /
@ -33,6 +34,7 @@ import toast from 'react-hot-toast'
const APP_LABELS: Record<AppType, { name: string; description: string }> = { const APP_LABELS: Record<AppType, { name: string; description: string }> = {
mobile: { name: '榴莲 App', description: '管理榴莲 App 的版本更新' }, mobile: { name: '榴莲 App', description: '管理榴莲 App 的版本更新' },
mining: { name: '股行 App', description: '管理股行 App 的版本更新' }, mining: { name: '股行 App', description: '管理股行 App 的版本更新' },
it0: { name: 'IT0 App', description: '管理 IT0 App 的版本更新' },
} }
export default function HomePage() { export default function HomePage() {
@ -120,6 +122,16 @@ export default function HomePage() {
> >
App App
</button> </button>
<button
onClick={() => handleAppTypeChange('it0')}
className={`px-5 py-2.5 rounded-lg text-sm font-medium transition-all shadow-sm ${
appType === 'it0'
? 'bg-emerald-600 text-white shadow-emerald-200'
: 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-200'
}`}
>
IT0 App
</button>
</div> </div>
</div> </div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">

View File

@ -3,14 +3,16 @@
* *
* : * :
* HTTP客户端 * HTTP客户端
* : * :
* - mobile ( App): admin-service * - mobile ( App): admin-service
* - mining ( App): mining-admin-service * - mining ( App): mining-admin-service
* - it0 (IT0 App): IT0 version-service
* *
* : * :
* : * :
* - NEXT_PUBLIC_API_URL: 榴莲App后端地址 * - NEXT_PUBLIC_API_URL: 榴莲App后端地址
* - NEXT_PUBLIC_MINING_API_URL: 股行App后端地址 * - NEXT_PUBLIC_MINING_API_URL: 股行App后端地址
* - NEXT_PUBLIC_IT0_API_URL: IT0 App后端地址
* *
* 使: * 使:
* ```typescript * ```typescript
@ -38,8 +40,9 @@ export class ApiError extends Error {
* *
* - mobile: 榴莲 App (使 admin-service ) * - mobile: 榴莲 App (使 admin-service )
* - mining: 股行 App (使 mining-admin-service ) * - mining: 股行 App (使 mining-admin-service )
* - it0: IT0 App (使 IT0 version-service )
*/ */
export type AppType = 'mobile' | 'mining' export type AppType = 'mobile' | 'mining' | 'it0'
export interface ApiConfig { export interface ApiConfig {
baseURL: string baseURL: string
@ -52,7 +55,8 @@ export interface ApiConfig {
* : * :
* - mobile 使 /api/v1/versions (admin-service) * - mobile 使 /api/v1/versions (admin-service)
* - mining 使 /api/v2/upgrade-versions (mining-admin-service ) * - mining 使 /api/v2/upgrade-versions (mining-admin-service )
* - API前缀不同是因为两个后端是独立的服务 * - it0 使 /api/v1/versions (IT0 version-service)
* - API前缀不同是因为各后端是独立的服务
*/ */
const APP_CONFIGS: Record<AppType, ApiConfig> = { const APP_CONFIGS: Record<AppType, ApiConfig> = {
// 榴莲 App - 连接原有的 admin-service 后端 // 榴莲 App - 连接原有的 admin-service 后端
@ -65,6 +69,11 @@ const APP_CONFIGS: Record<AppType, ApiConfig> = {
baseURL: process.env.NEXT_PUBLIC_MINING_API_URL || 'http://localhost:3023', baseURL: process.env.NEXT_PUBLIC_MINING_API_URL || 'http://localhost:3023',
apiPrefix: '/api/v2/upgrade-versions', apiPrefix: '/api/v2/upgrade-versions',
}, },
// IT0 App - 连接 IT0 version-service 后端
it0: {
baseURL: process.env.NEXT_PUBLIC_IT0_API_URL || 'https://it0api.szaiai.com',
apiPrefix: '/api/v1/versions',
},
} }
export function createApiClient(appType: AppType = 'mobile'): AxiosInstance { export function createApiClient(appType: AppType = 'mobile'): AxiosInstance {
@ -111,3 +120,4 @@ export function getApiPrefix(appType: AppType): string {
export const apiClient = createApiClient('mobile') export const apiClient = createApiClient('mobile')
export const miningApiClient = createApiClient('mining') export const miningApiClient = createApiClient('mining')
export const it0ApiClient = createApiClient('it0')

View File

@ -9,7 +9,7 @@ import {
ParsedPackageInfo, ParsedPackageInfo,
Platform, Platform,
} from '@/domain' } from '@/domain'
import { apiClient, miningApiClient, AppType, getApiPrefix } from '../http/api-client' import { apiClient, miningApiClient, it0ApiClient, AppType, getApiPrefix } from '../http/api-client'
export class VersionRepositoryImpl implements IVersionRepository { export class VersionRepositoryImpl implements IVersionRepository {
private client: AxiosInstance private client: AxiosInstance
@ -20,7 +20,9 @@ export class VersionRepositoryImpl implements IVersionRepository {
this.client = client this.client = client
this.apiPrefix = '/api/v1/versions' this.apiPrefix = '/api/v1/versions'
} else { } else {
this.client = appType === 'mining' ? miningApiClient : apiClient this.client = appType === 'mining' ? miningApiClient
: appType === 'it0' ? it0ApiClient
: apiClient
this.apiPrefix = getApiPrefix(appType) this.apiPrefix = getApiPrefix(appType)
} }
} }
@ -118,11 +120,16 @@ export class VersionRepositoryImpl implements IVersionRepository {
} }
} }
// Pre-created repository instances for both apps // Pre-created repository instances for all apps
export const versionRepository = new VersionRepositoryImpl('mobile') export const versionRepository = new VersionRepositoryImpl('mobile')
export const miningVersionRepository = new VersionRepositoryImpl('mining') export const miningVersionRepository = new VersionRepositoryImpl('mining')
export const it0VersionRepository = new VersionRepositoryImpl('it0')
// Factory function to get repository by app type // Factory function to get repository by app type
export function getVersionRepository(appType: AppType): VersionRepositoryImpl { export function getVersionRepository(appType: AppType): VersionRepositoryImpl {
return appType === 'mining' ? miningVersionRepository : versionRepository switch (appType) {
case 'mining': return miningVersionRepository
case 'it0': return it0VersionRepository
default: return versionRepository
}
} }

View File

@ -2,8 +2,8 @@
import { useState, useRef } from 'react' import { useState, useRef } from 'react'
import { Platform } from '@/domain' import { Platform } from '@/domain'
import { useVersionActions } from '@/application' import { useVersionActions, useAppType } from '@/application'
import { versionRepository } from '@/infrastructure/repositories/version-repository-impl' import { getVersionRepository } from '@/infrastructure/repositories/version-repository-impl'
interface UploadModalProps { interface UploadModalProps {
onClose: () => void onClose: () => void
@ -12,6 +12,7 @@ interface UploadModalProps {
export function UploadModal({ onClose, onSuccess }: UploadModalProps) { export function UploadModal({ onClose, onSuccess }: UploadModalProps) {
const { uploadVersion } = useVersionActions() const { uploadVersion } = useVersionActions()
const { appType } = useAppType()
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@ -48,7 +49,7 @@ export function UploadModal({ onClose, onSuccess }: UploadModalProps) {
// Auto-parse package info // Auto-parse package info
setIsParsing(true) setIsParsing(true)
try { try {
const parsed = await versionRepository.parsePackage(selectedFile, detectedPlatform) const parsed = await getVersionRepository(appType).parsePackage(selectedFile, detectedPlatform)
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
platform: detectedPlatform, platform: detectedPlatform,