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:
ShukeBta
2026-05-17 00:54:40 +08:00
parent 158fffaba0
commit 9be81222cc
8 changed files with 272 additions and 341 deletions
+2 -2
View File
@@ -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 ?? []),
}
+1 -1
View File
@@ -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
View File
@@ -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),
}
+1 -1
View File
@@ -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),