mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
fix: 修复三个空白页面 + 许可证页面重构
- 修复发现页空白:后端 discover.go nil→[] Match,前端 discover.ts 加 ?? []
- 修复下载客户端页空白:后端 download_clients.go nil→[]model.DownloadClient,前端加 ?? []
- 修复通知渠道页空白:后端 notify_channels.go nil→[]struct{},前端加 ?? []
- 许可证页面重构:移除生成密钥功能,对接 LicenseServer 绑定/状态模式
- 所有变更通过 Go build + npm run build 零错误验证
This commit is contained in:
@@ -15,7 +15,7 @@ export interface DiscoverItem extends Partial<Media> {
|
||||
|
||||
export const discoverAPI = {
|
||||
trending: () =>
|
||||
api.get<{ items: DiscoverItem[] }>('/discover/trending').then((r) => r.data.items),
|
||||
api.get<{ items: DiscoverItem[] }>('/discover/trending').then((r) => r.data.items ?? []),
|
||||
popular: () =>
|
||||
api.get<{ items: DiscoverItem[] }>('/discover/popular').then((r) => r.data.items),
|
||||
api.get<{ items: DiscoverItem[] }>('/discover/popular').then((r) => r.data.items ?? []),
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface DownloadClientInput {
|
||||
|
||||
export const downloadClientsAPI = {
|
||||
list: () =>
|
||||
api.get<DownloadClient[]>('/admin/download/clients').then((r) => r.data),
|
||||
api.get<DownloadClient[]>('/admin/download/clients').then((r) => r.data ?? []),
|
||||
|
||||
create: (input: DownloadClientInput) =>
|
||||
api.post<DownloadClient>('/admin/download/clients', input).then((r) => r.data),
|
||||
|
||||
+37
-49
@@ -1,71 +1,59 @@
|
||||
import { api } from './client'
|
||||
|
||||
// LicenseKey + LicenseActivation mirror the Go model structs.
|
||||
export interface LicenseKey {
|
||||
id: string
|
||||
key: string
|
||||
customer?: string
|
||||
plan: string
|
||||
max_activations: number
|
||||
issued_at: string
|
||||
expires_at?: string | null
|
||||
revoked: boolean
|
||||
notes?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
// ── License Server types (generic, ready for MediaStationLicenseServer integration) ──
|
||||
|
||||
/** Response after binding/activating a license. */
|
||||
export interface LicenseActivation {
|
||||
id: string
|
||||
key_id: string
|
||||
/** The license key string (e.g. MS-XXXX-XXXX-XXXX) */
|
||||
key?: string
|
||||
device_id: string
|
||||
device_name?: string
|
||||
plan?: string
|
||||
max_activations?: number
|
||||
/** ISO8601 — null means perpetual */
|
||||
expires_at?: string | null
|
||||
valid: boolean
|
||||
ip?: string
|
||||
unbound_at?: string | null
|
||||
heartbeat_at?: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface GenerateKeyInput {
|
||||
customer?: string
|
||||
plan?: string
|
||||
max_activations?: number
|
||||
expires_at?: string // RFC3339; "" or omit for perpetual
|
||||
notes?: string
|
||||
/** Status of the currently bound license on this device. */
|
||||
export interface LicenseStatus {
|
||||
/** Whether a license is currently active */
|
||||
active: boolean
|
||||
activation?: LicenseActivation
|
||||
/** Error or status message */
|
||||
message?: string
|
||||
}
|
||||
|
||||
/** Generate a short device fingerprint from browser/OS info. */
|
||||
function deviceFingerprint(): string {
|
||||
const nav = window.navigator
|
||||
const parts = [nav.hardwareConcurrency, nav.language, screen.width, screen.height]
|
||||
return btoa(parts.join('|')).slice(0, 32).replace(/[+/=]/g, '')
|
||||
}
|
||||
|
||||
// ── API methods ──
|
||||
|
||||
export const licenseAPI = {
|
||||
generate: (input: GenerateKeyInput) =>
|
||||
api.post<LicenseKey>('/admin/license/generate', input).then((r) => r.data),
|
||||
|
||||
list: () => api.get<LicenseKey[]>('/admin/license/list').then((r) => r.data),
|
||||
|
||||
listActivations: (keyID: string) =>
|
||||
api
|
||||
.get<LicenseActivation[]>(`/admin/license/${keyID}/activations`)
|
||||
.then((r) => r.data),
|
||||
|
||||
revoke: (keyID: string) =>
|
||||
api.post(`/admin/license/${keyID}/revoke`).then((r) => r.data),
|
||||
|
||||
unbind: (activationID: string) =>
|
||||
api.post(`/admin/license/activation/${activationID}/unbind`).then((r) => r.data),
|
||||
|
||||
// Self-service
|
||||
activate: (key: string, deviceID: string, deviceName?: string) =>
|
||||
/** Bind / activate a license key for this device. */
|
||||
bind: (key: string) =>
|
||||
api
|
||||
.post<LicenseActivation>('/license/activate', {
|
||||
key,
|
||||
device_id: deviceID,
|
||||
device_name: deviceName,
|
||||
key: key.trim(),
|
||||
device_id: deviceFingerprint(),
|
||||
device_name: navigator.platform || 'Web Client',
|
||||
})
|
||||
.then((r) => r.data),
|
||||
|
||||
status: (keyID: string) =>
|
||||
api
|
||||
.get<{ key: LicenseKey; active_activations: number; valid: boolean }>(
|
||||
'/license/status',
|
||||
{ params: { key_id: keyID } },
|
||||
)
|
||||
.then((r) => r.data),
|
||||
/** Get the status of the currently active license. */
|
||||
status: () =>
|
||||
api.get<LicenseStatus>('/license/status').then((r) => r.data),
|
||||
|
||||
/** Refresh the heartbeat for the active license. */
|
||||
heartbeat: () =>
|
||||
api.post('/license/heartbeat').then((r) => r.data),
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface NotifyChannelInput {
|
||||
// notifyChannelsAPI wraps the admin /admin/notify/channels surface.
|
||||
export const notifyChannelsAPI = {
|
||||
list: () =>
|
||||
api.get<NotifyChannel[]>('/admin/notify/channels').then((r) => r.data),
|
||||
api.get<NotifyChannel[]>('/admin/notify/channels').then((r) => r.data ?? []),
|
||||
|
||||
create: (input: NotifyChannelInput) =>
|
||||
api.post<NotifyChannel>('/admin/notify/channels', input).then((r) => r.data),
|
||||
|
||||
Reference in New Issue
Block a user