Add platform status switch
This commit is contained in:
@@ -182,10 +182,15 @@ func (s *Server) createPlatform(w http.ResponseWriter, r *http.Request) {
|
|||||||
input.Provider = strings.TrimSpace(input.Provider)
|
input.Provider = strings.TrimSpace(input.Provider)
|
||||||
input.Name = strings.TrimSpace(input.Name)
|
input.Name = strings.TrimSpace(input.Name)
|
||||||
input.InternalName = strings.TrimSpace(input.InternalName)
|
input.InternalName = strings.TrimSpace(input.InternalName)
|
||||||
|
input.Status = strings.TrimSpace(input.Status)
|
||||||
if input.Provider == "" || input.Name == "" {
|
if input.Provider == "" || input.Name == "" {
|
||||||
writeError(w, http.StatusBadRequest, "provider and name are required")
|
writeError(w, http.StatusBadRequest, "provider and name are required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if input.Status != "" && input.Status != "enabled" && input.Status != "disabled" {
|
||||||
|
writeError(w, http.StatusBadRequest, "status must be enabled or disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
if input.AuthType == "" {
|
if input.AuthType == "" {
|
||||||
input.AuthType = "bearer"
|
input.AuthType = "bearer"
|
||||||
}
|
}
|
||||||
@@ -213,10 +218,15 @@ func (s *Server) updatePlatform(w http.ResponseWriter, r *http.Request) {
|
|||||||
input.Provider = strings.TrimSpace(input.Provider)
|
input.Provider = strings.TrimSpace(input.Provider)
|
||||||
input.Name = strings.TrimSpace(input.Name)
|
input.Name = strings.TrimSpace(input.Name)
|
||||||
input.InternalName = strings.TrimSpace(input.InternalName)
|
input.InternalName = strings.TrimSpace(input.InternalName)
|
||||||
|
input.Status = strings.TrimSpace(input.Status)
|
||||||
if input.Provider == "" || input.Name == "" {
|
if input.Provider == "" || input.Name == "" {
|
||||||
writeError(w, http.StatusBadRequest, "provider and name are required")
|
writeError(w, http.StatusBadRequest, "provider and name are required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if input.Status != "" && input.Status != "enabled" && input.Status != "disabled" {
|
||||||
|
writeError(w, http.StatusBadRequest, "status must be enabled or disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
if input.AuthType == "" {
|
if input.AuthType == "" {
|
||||||
input.AuthType = "bearer"
|
input.AuthType = "bearer"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ type CreatePlatformInput struct {
|
|||||||
DefaultDiscountFactor float64 `json:"defaultDiscountFactor"`
|
DefaultDiscountFactor float64 `json:"defaultDiscountFactor"`
|
||||||
PricingRuleSetID string `json:"pricingRuleSetId"`
|
PricingRuleSetID string `json:"pricingRuleSetId"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateAPIKeyInput struct {
|
type CreateAPIKeyInput struct {
|
||||||
@@ -591,11 +592,11 @@ func (s *Store) CreatePlatform(ctx context.Context, input CreatePlatformInput) (
|
|||||||
INSERT INTO integration_platforms (
|
INSERT INTO integration_platforms (
|
||||||
provider, platform_key, name, internal_name, base_url, auth_type, credentials, config,
|
provider, platform_key, name, internal_name, base_url, auth_type, credentials, config,
|
||||||
default_pricing_mode, default_discount_factor, pricing_rule_set_id,
|
default_pricing_mode, default_discount_factor, pricing_rule_set_id,
|
||||||
priority, retry_policy, rate_limit_policy
|
priority, retry_policy, rate_limit_policy, status
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, COALESCE(NULLIF($2, ''), 'platform_' || replace(gen_random_uuid()::text, '-', '')), $3, NULLIF($4, ''), $5, $6, $7, $8,
|
$1, COALESCE(NULLIF($2, ''), 'platform_' || replace(gen_random_uuid()::text, '-', '')), $3, NULLIF($4, ''), $5, $6, $7, $8,
|
||||||
$9, $10, NULLIF($11, '')::uuid, $12, $13, $14
|
$9, $10, NULLIF($11, '')::uuid, $12, $13, $14, COALESCE(NULLIF($15, ''), 'enabled')
|
||||||
)
|
)
|
||||||
RETURNING id::text, provider, platform_key, name, COALESCE(internal_name, ''), COALESCE(base_url, ''), auth_type, status,
|
RETURNING id::text, provider, platform_key, name, COALESCE(internal_name, ''), COALESCE(base_url, ''), auth_type, status,
|
||||||
priority, dynamic_priority, COALESCE(dynamic_priority, priority),
|
priority, dynamic_priority, COALESCE(dynamic_priority, priority),
|
||||||
@@ -606,6 +607,7 @@ RETURNING id::text, provider, platform_key, name, COALESCE(internal_name, ''), C
|
|||||||
input.Provider, input.PlatformKey, input.Name, strings.TrimSpace(input.InternalName), input.BaseURL, input.AuthType, credentials, config,
|
input.Provider, input.PlatformKey, input.Name, strings.TrimSpace(input.InternalName), input.BaseURL, input.AuthType, credentials, config,
|
||||||
input.DefaultPricingMode, input.DefaultDiscountFactor, input.PricingRuleSetID, input.Priority,
|
input.DefaultPricingMode, input.DefaultDiscountFactor, input.PricingRuleSetID, input.Priority,
|
||||||
string(retryPolicy), string(rateLimitPolicy),
|
string(retryPolicy), string(rateLimitPolicy),
|
||||||
|
input.Status,
|
||||||
).Scan(
|
).Scan(
|
||||||
&platform.ID,
|
&platform.ID,
|
||||||
&platform.Provider,
|
&platform.Provider,
|
||||||
@@ -684,6 +686,7 @@ SET provider = $2,
|
|||||||
priority = $13,
|
priority = $13,
|
||||||
retry_policy = $14,
|
retry_policy = $14,
|
||||||
rate_limit_policy = $15,
|
rate_limit_policy = $15,
|
||||||
|
status = COALESCE(NULLIF($16, ''), status),
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $1::uuid
|
WHERE id = $1::uuid
|
||||||
RETURNING id::text, provider, platform_key, name, COALESCE(internal_name, ''), COALESCE(base_url, ''), auth_type, status,
|
RETURNING id::text, provider, platform_key, name, COALESCE(internal_name, ''), COALESCE(base_url, ''), auth_type, status,
|
||||||
@@ -707,6 +710,7 @@ RETURNING id::text, provider, platform_key, name, COALESCE(internal_name, ''), C
|
|||||||
input.Priority,
|
input.Priority,
|
||||||
string(retryPolicy),
|
string(retryPolicy),
|
||||||
string(rateLimitPolicy),
|
string(rateLimitPolicy),
|
||||||
|
input.Status,
|
||||||
).Scan(
|
).Scan(
|
||||||
&platform.ID,
|
&platform.ID,
|
||||||
&platform.Provider,
|
&platform.Provider,
|
||||||
|
|||||||
@@ -560,6 +560,24 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function savePlatformStatus(platform: IntegrationPlatform, status: 'enabled' | 'disabled') {
|
||||||
|
setCoreState('loading');
|
||||||
|
setCoreMessage('');
|
||||||
|
try {
|
||||||
|
const input = platformStatusUpdatePayload(platform, status);
|
||||||
|
const updated = await updatePlatform(token, platform.id, input);
|
||||||
|
const platformForState = withCredentialPreviewFallback(updated, input, platform);
|
||||||
|
setPlatforms((current) => current.map((item) => item.id === platform.id ? platformForState : item));
|
||||||
|
invalidateDataKeys('modelCatalog', 'modelRateLimits', 'playgroundModels');
|
||||||
|
setCoreState('ready');
|
||||||
|
setCoreMessage(status === 'enabled' ? '平台已启用。' : '平台已禁用。');
|
||||||
|
} catch (err) {
|
||||||
|
setCoreState('error');
|
||||||
|
setCoreMessage(err instanceof Error ? err.message : '切换平台状态失败');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function savePlatformDynamicPriority(platformId: string, input: PlatformDynamicPriorityUpdateRequest) {
|
async function savePlatformDynamicPriority(platformId: string, input: PlatformDynamicPriorityUpdateRequest) {
|
||||||
setCoreState('loading');
|
setCoreState('loading');
|
||||||
setCoreMessage('');
|
setCoreMessage('');
|
||||||
@@ -1029,6 +1047,7 @@ export function App() {
|
|||||||
onResetBaseModel={resetBaseModelToDefault}
|
onResetBaseModel={resetBaseModelToDefault}
|
||||||
onSavePlatform={savePlatformWithModels}
|
onSavePlatform={savePlatformWithModels}
|
||||||
onSavePlatformDynamicPriority={savePlatformDynamicPriority}
|
onSavePlatformDynamicPriority={savePlatformDynamicPriority}
|
||||||
|
onTogglePlatformStatus={savePlatformStatus}
|
||||||
onSaveProvider={saveProvider}
|
onSaveProvider={saveProvider}
|
||||||
onSavePricingRuleSet={savePricingRuleSet}
|
onSavePricingRuleSet={savePricingRuleSet}
|
||||||
onSaveRunnerPolicy={saveRunnerPolicy}
|
onSaveRunnerPolicy={saveRunnerPolicy}
|
||||||
@@ -1105,6 +1124,25 @@ function mergeExistingPlatformModelInput(input: PlatformModelBindingInput, curre
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function platformStatusUpdatePayload(platform: IntegrationPlatform, status: 'enabled' | 'disabled'): PlatformCreateInput {
|
||||||
|
return {
|
||||||
|
provider: platform.provider,
|
||||||
|
platformKey: platform.platformKey,
|
||||||
|
name: platform.name,
|
||||||
|
internalName: platform.internalName,
|
||||||
|
baseUrl: platform.baseUrl,
|
||||||
|
authType: platform.authType,
|
||||||
|
config: platform.config ?? {},
|
||||||
|
retryPolicy: platform.retryPolicy ?? {},
|
||||||
|
rateLimitPolicy: platform.rateLimitPolicy ?? {},
|
||||||
|
defaultPricingMode: platform.defaultPricingMode,
|
||||||
|
defaultDiscountFactor: platform.defaultDiscountFactor,
|
||||||
|
pricingRuleSetId: platform.pricingRuleSetId,
|
||||||
|
priority: platform.priority,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function withCredentialPreviewFallback(
|
function withCredentialPreviewFallback(
|
||||||
platform: IntegrationPlatform,
|
platform: IntegrationPlatform,
|
||||||
input: PlatformCreateInput,
|
input: PlatformCreateInput,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export * from './message';
|
|||||||
export * from './popover';
|
export * from './popover';
|
||||||
export * from './select';
|
export * from './select';
|
||||||
export * from './separator';
|
export * from './separator';
|
||||||
|
export * from './switch';
|
||||||
export * from './table';
|
export * from './table';
|
||||||
export * from './tabs';
|
export * from './tabs';
|
||||||
export * from './textarea';
|
export * from './textarea';
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
export interface SwitchProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'> {
|
||||||
|
checked: boolean;
|
||||||
|
onCheckedChange?: (checked: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
||||||
|
({ checked, className, disabled, onCheckedChange, ...props }, ref) => (
|
||||||
|
<button
|
||||||
|
aria-checked={checked}
|
||||||
|
className={cn('shSwitch', className)}
|
||||||
|
data-state={checked ? 'checked' : 'unchecked'}
|
||||||
|
disabled={disabled}
|
||||||
|
ref={ref}
|
||||||
|
role="switch"
|
||||||
|
{...props}
|
||||||
|
type="button"
|
||||||
|
onClick={(event) => {
|
||||||
|
props.onClick?.(event);
|
||||||
|
if (!event.defaultPrevented && !disabled) {
|
||||||
|
onCheckedChange?.(!checked);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="shSwitchThumb" />
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Switch.displayName = 'Switch';
|
||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
GatewayTenantUpsertRequest,
|
GatewayTenantUpsertRequest,
|
||||||
GatewayRunnerPolicyUpsertRequest,
|
GatewayRunnerPolicyUpsertRequest,
|
||||||
GatewayUserUpsertRequest,
|
GatewayUserUpsertRequest,
|
||||||
|
IntegrationPlatform,
|
||||||
PlatformDynamicPriorityUpdateRequest,
|
PlatformDynamicPriorityUpdateRequest,
|
||||||
PricingRuleSetUpsertRequest,
|
PricingRuleSetUpsertRequest,
|
||||||
RuntimePolicySetUpsertRequest,
|
RuntimePolicySetUpsertRequest,
|
||||||
@@ -65,6 +66,7 @@ export function AdminPage(props: {
|
|||||||
onBatchAccessRules: (input: GatewayAccessRuleBatchRequest) => Promise<void>;
|
onBatchAccessRules: (input: GatewayAccessRuleBatchRequest) => Promise<void>;
|
||||||
onSavePlatform: (input: PlatformWithModelsInput) => Promise<void>;
|
onSavePlatform: (input: PlatformWithModelsInput) => Promise<void>;
|
||||||
onSavePlatformDynamicPriority: (platformId: string, input: PlatformDynamicPriorityUpdateRequest) => Promise<void>;
|
onSavePlatformDynamicPriority: (platformId: string, input: PlatformDynamicPriorityUpdateRequest) => Promise<void>;
|
||||||
|
onTogglePlatformStatus: (platform: IntegrationPlatform, status: 'enabled' | 'disabled') => Promise<void>;
|
||||||
onSaveProvider: (input: CatalogProviderUpsertRequest, providerId?: string) => Promise<void>;
|
onSaveProvider: (input: CatalogProviderUpsertRequest, providerId?: string) => Promise<void>;
|
||||||
onSavePricingRuleSet: (input: PricingRuleSetUpsertRequest, ruleSetId?: string) => Promise<void>;
|
onSavePricingRuleSet: (input: PricingRuleSetUpsertRequest, ruleSetId?: string) => Promise<void>;
|
||||||
onSaveRunnerPolicy: (input: GatewayRunnerPolicyUpsertRequest) => Promise<void>;
|
onSaveRunnerPolicy: (input: GatewayRunnerPolicyUpsertRequest) => Promise<void>;
|
||||||
@@ -149,6 +151,7 @@ export function AdminPage(props: {
|
|||||||
state={props.state}
|
state={props.state}
|
||||||
onDeletePlatform={props.onDeletePlatform}
|
onDeletePlatform={props.onDeletePlatform}
|
||||||
onSavePlatform={props.onSavePlatform}
|
onSavePlatform={props.onSavePlatform}
|
||||||
|
onTogglePlatformStatus={props.onTogglePlatformStatus}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{props.section === 'realtimeLoad' && (
|
{props.section === 'realtimeLoad' && (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from 'react';
|
import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from 'react';
|
||||||
import { Boxes, CheckCircle2, Globe2, KeyRound, Pencil, Plus, RotateCcw, Search, ServerCog, ShieldCheck, SlidersHorizontal, Trash2, X } from 'lucide-react';
|
import { Boxes, CheckCircle2, Globe2, KeyRound, Pencil, Plus, RotateCcw, Search, ServerCog, ShieldCheck, SlidersHorizontal, Trash2, X } from 'lucide-react';
|
||||||
import type { BaseModelCatalogItem, CatalogProvider, IntegrationPlatform, PlatformModel, PricingRuleSet } from '@easyai-ai-gateway/contracts';
|
import type { BaseModelCatalogItem, CatalogProvider, IntegrationPlatform, PlatformModel, PricingRuleSet } from '@easyai-ai-gateway/contracts';
|
||||||
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, EmptyState, FormDialog, Input, Label, ScreenMessage, Select, Table, TableCell, TableHead, TableRow } from '../../components/ui';
|
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, EmptyState, FormDialog, Input, Label, ScreenMessage, Select, Switch, Table, TableCell, TableHead, TableRow } from '../../components/ui';
|
||||||
import type { LoadState, PlatformWithModelsInput } from '../../types';
|
import type { LoadState, PlatformWithModelsInput } from '../../types';
|
||||||
import {
|
import {
|
||||||
authTypes,
|
authTypes,
|
||||||
@@ -31,6 +31,7 @@ export function PlatformManagementPanel(props: {
|
|||||||
state: LoadState;
|
state: LoadState;
|
||||||
onDeletePlatform: (platformId: string) => Promise<void>;
|
onDeletePlatform: (platformId: string) => Promise<void>;
|
||||||
onSavePlatform: (input: PlatformWithModelsInput) => Promise<void>;
|
onSavePlatform: (input: PlatformWithModelsInput) => Promise<void>;
|
||||||
|
onTogglePlatformStatus: (platform: IntegrationPlatform, status: 'enabled' | 'disabled') => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const defaultProvider = props.providers[0]?.providerKey ?? props.baseModels[0]?.providerKey ?? '';
|
const defaultProvider = props.providers[0]?.providerKey ?? props.baseModels[0]?.providerKey ?? '';
|
||||||
const [now, setNow] = useState(() => Date.now());
|
const [now, setNow] = useState(() => Date.now());
|
||||||
@@ -42,6 +43,7 @@ export function PlatformManagementPanel(props: {
|
|||||||
const [globalProxyNoticeOpen, setGlobalProxyNoticeOpen] = useState(false);
|
const [globalProxyNoticeOpen, setGlobalProxyNoticeOpen] = useState(false);
|
||||||
const [editingPlatform, setEditingPlatform] = useState<IntegrationPlatform | null>(null);
|
const [editingPlatform, setEditingPlatform] = useState<IntegrationPlatform | null>(null);
|
||||||
const [pendingDeletePlatform, setPendingDeletePlatform] = useState<IntegrationPlatform | null>(null);
|
const [pendingDeletePlatform, setPendingDeletePlatform] = useState<IntegrationPlatform | null>(null);
|
||||||
|
const [togglingPlatformId, setTogglingPlatformId] = useState('');
|
||||||
const providerMap = useMemo(() => new Map(props.providers.map((item) => [item.providerKey, item])), [props.providers]);
|
const providerMap = useMemo(() => new Map(props.providers.map((item) => [item.providerKey, item])), [props.providers]);
|
||||||
const platformMap = useMemo(() => new Map(props.platforms.map((item) => [item.id, item])), [props.platforms]);
|
const platformMap = useMemo(() => new Map(props.platforms.map((item) => [item.id, item])), [props.platforms]);
|
||||||
const [form, setForm] = useState<PlatformWizardForm>(() => createEmptyPlatformForm(defaultProvider, providerDefaults(providerMap.get(defaultProvider))));
|
const [form, setForm] = useState<PlatformWizardForm>(() => createEmptyPlatformForm(defaultProvider, providerDefaults(providerMap.get(defaultProvider))));
|
||||||
@@ -157,6 +159,18 @@ export function PlatformManagementPanel(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function togglePlatformStatus(platform: IntegrationPlatform, status: 'enabled' | 'disabled') {
|
||||||
|
setTogglingPlatformId(platform.id);
|
||||||
|
setValidationMessage('');
|
||||||
|
try {
|
||||||
|
await props.onTogglePlatformStatus(platform, status);
|
||||||
|
} catch (err) {
|
||||||
|
setValidationMessage(err instanceof Error ? err.message : '切换平台状态失败');
|
||||||
|
} finally {
|
||||||
|
setTogglingPlatformId('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="pageStack">
|
<section className="pageStack">
|
||||||
<ScreenMessage message={validationMessage} variant="error" onClose={() => setValidationMessage('')} />
|
<ScreenMessage message={validationMessage} variant="error" onClose={() => setValidationMessage('')} />
|
||||||
@@ -183,9 +197,11 @@ export function PlatformManagementPanel(props: {
|
|||||||
platforms={props.platforms}
|
platforms={props.platforms}
|
||||||
providerMap={providerMap}
|
providerMap={providerMap}
|
||||||
pricingRuleSets={props.pricingRuleSets}
|
pricingRuleSets={props.pricingRuleSets}
|
||||||
|
togglingPlatformId={togglingPlatformId}
|
||||||
onDelete={setPendingDeletePlatform}
|
onDelete={setPendingDeletePlatform}
|
||||||
onCreate={openCreateDialog}
|
onCreate={openCreateDialog}
|
||||||
onEdit={openEditDialog}
|
onEdit={openEditDialog}
|
||||||
|
onToggleStatus={togglePlatformStatus}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PlatformModelTable
|
<PlatformModelTable
|
||||||
@@ -404,9 +420,11 @@ function PlatformTable(props: {
|
|||||||
platforms: IntegrationPlatform[];
|
platforms: IntegrationPlatform[];
|
||||||
providerMap: Map<string, CatalogProvider>;
|
providerMap: Map<string, CatalogProvider>;
|
||||||
pricingRuleSets: PricingRuleSet[];
|
pricingRuleSets: PricingRuleSet[];
|
||||||
|
togglingPlatformId: string;
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
onDelete: (platform: IntegrationPlatform) => void;
|
onDelete: (platform: IntegrationPlatform) => void;
|
||||||
onEdit: (platform: IntegrationPlatform) => void;
|
onEdit: (platform: IntegrationPlatform) => void;
|
||||||
|
onToggleStatus: (platform: IntegrationPlatform, status: 'enabled' | 'disabled') => void;
|
||||||
}) {
|
}) {
|
||||||
if (!props.platforms.length) {
|
if (!props.platforms.length) {
|
||||||
return (
|
return (
|
||||||
@@ -438,6 +456,8 @@ function PlatformTable(props: {
|
|||||||
const rateLimit = platformRateLimitSummary(platform.rateLimitPolicy);
|
const rateLimit = platformRateLimitSummary(platform.rateLimitPolicy);
|
||||||
const runtime = platformRuntimeSummary(platform);
|
const runtime = platformRuntimeSummary(platform);
|
||||||
const platformCooldownMs = cooldownRemainingMs(platform.cooldownUntil, props.now);
|
const platformCooldownMs = cooldownRemainingMs(platform.cooldownUntil, props.now);
|
||||||
|
const isEnabled = platform.status === 'enabled';
|
||||||
|
const isToggling = props.togglingPlatformId === platform.id;
|
||||||
return (
|
return (
|
||||||
<TableRow key={platform.id}>
|
<TableRow key={platform.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -467,12 +487,21 @@ function PlatformTable(props: {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{props.platformModelCount.get(platform.id) ?? 0}</TableCell>
|
<TableCell>{props.platformModelCount.get(platform.id) ?? 0}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="platformTableName">
|
<span className="platformStatusCell">
|
||||||
|
<span className="platformStatusToggle">
|
||||||
|
<Switch
|
||||||
|
aria-label={`${platformDisplayName(platform)} ${isEnabled ? '禁用' : '启用'}平台`}
|
||||||
|
checked={isEnabled}
|
||||||
|
disabled={isToggling}
|
||||||
|
onCheckedChange={(checked) => props.onToggleStatus(platform, checked ? 'enabled' : 'disabled')}
|
||||||
|
/>
|
||||||
|
<span>{isEnabled ? '启用' : '禁用'}</span>
|
||||||
|
</span>
|
||||||
<strong>
|
<strong>
|
||||||
{platformCooldownMs > 0 ? (
|
{platformCooldownMs > 0 ? (
|
||||||
<Badge variant="warning">冷却中</Badge>
|
<Badge variant="warning">冷却中</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant={platform.status === 'enabled' ? 'success' : 'secondary'}>{platform.status}</Badge>
|
<Badge variant={isEnabled ? 'success' : 'destructive'}>{isEnabled ? '已启用' : '已禁用'}</Badge>
|
||||||
)}
|
)}
|
||||||
</strong>
|
</strong>
|
||||||
<small>{platformCooldownMs > 0 ? `剩余 ${formatCooldownRemaining(platformCooldownMs)}` : runtime}</small>
|
<small>{platformCooldownMs > 0 ? `剩余 ${formatCooldownRemaining(platformCooldownMs)}` : runtime}</small>
|
||||||
|
|||||||
@@ -948,6 +948,33 @@
|
|||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.platformStatusCell {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platformStatusCell strong,
|
||||||
|
.platformStatusCell small {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platformStatusCell small {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.platformStatusToggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-strong);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
.tableActions {
|
.tableActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -152,6 +152,51 @@
|
|||||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shSwitch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 36px;
|
||||||
|
height: 20px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #d1d5db;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(16, 24, 40, 0.12);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shSwitch[data-state="checked"] {
|
||||||
|
background: #059669;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(6, 95, 70, 0.2), 0 0 0 3px rgba(16, 185, 129, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shSwitch:focus-visible {
|
||||||
|
outline: 2px solid var(--ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shSwitch:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shSwitchThumb {
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(16, 24, 40, 0.24);
|
||||||
|
transition: transform 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shSwitch[data-state="checked"] .shSwitchThumb {
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
|
||||||
.screenMessageViewport {
|
.screenMessageViewport {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export interface PlatformCreateInput {
|
|||||||
defaultDiscountFactor?: number;
|
defaultDiscountFactor?: number;
|
||||||
pricingRuleSetId?: string;
|
pricingRuleSetId?: string;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlatformModelBindingInput {
|
export interface PlatformModelBindingInput {
|
||||||
|
|||||||
Reference in New Issue
Block a user