百顺游戏
This commit is contained in:
parent
d06718cc49
commit
f59c374eae
@ -185,7 +185,7 @@ function normalizeCatalog(game: Partial<GameCatalogDto>): GameCatalogDto {
|
||||
category: stringValue(game.category),
|
||||
iconUrl: stringValue(game.iconUrl),
|
||||
coverUrl: stringValue(game.coverUrl),
|
||||
launchMode: stringValue(game.launchMode || "h5_popup"),
|
||||
launchMode: normalizeLaunchMode(game.launchMode),
|
||||
orientation: stringValue(game.orientation || "portrait"),
|
||||
minCoin: numberValue(game.minCoin),
|
||||
status: stringValue(game.status || "active"),
|
||||
@ -204,3 +204,8 @@ function numberValue(value: unknown) {
|
||||
const number = Number(value || 0);
|
||||
return Number.isFinite(number) ? number : 0;
|
||||
}
|
||||
|
||||
function normalizeLaunchMode(value: unknown) {
|
||||
const mode = stringValue(value);
|
||||
return mode === "half_screen" || mode === "three_quarter_screen" || mode === "full_screen" ? mode : "full_screen";
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from "@/features/games/api";
|
||||
import { useGameAbilities } from "@/features/games/permissions.js";
|
||||
import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
|
||||
import { normalizeJsonObjectString } from "@/shared/ui/JsonEditorField.jsx";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
|
||||
const defaultGameForm = {
|
||||
@ -19,7 +20,7 @@ const defaultGameForm = {
|
||||
category: "casual",
|
||||
iconUrl: "",
|
||||
coverUrl: "",
|
||||
launchMode: "h5_popup",
|
||||
launchMode: "full_screen",
|
||||
orientation: "portrait",
|
||||
minCoin: 0,
|
||||
status: "active",
|
||||
@ -201,7 +202,13 @@ export function useGamesPage() {
|
||||
|
||||
const submitPlatform = async (event) => {
|
||||
event.preventDefault();
|
||||
const payload = platformPayload(platformForm);
|
||||
let payload;
|
||||
try {
|
||||
payload = platformPayload(platformForm);
|
||||
} catch (err) {
|
||||
showToast(err.message || "适配器配置 JSON 不正确", "error");
|
||||
return;
|
||||
}
|
||||
setLoadingAction(activeAction);
|
||||
try {
|
||||
await upsertGamePlatform(payload, editingPlatformCode);
|
||||
@ -365,7 +372,7 @@ function gameToForm(game) {
|
||||
category: game.category || "casual",
|
||||
iconUrl: game.iconUrl || "",
|
||||
coverUrl: game.coverUrl || "",
|
||||
launchMode: game.launchMode || "h5_popup",
|
||||
launchMode: normalizeLaunchMode(game.launchMode),
|
||||
orientation: game.orientation || "portrait",
|
||||
minCoin: Number(game.minCoin || 0),
|
||||
status: game.status || "active",
|
||||
@ -399,7 +406,7 @@ function gamePayload(form) {
|
||||
category: form.category.trim(),
|
||||
iconUrl: form.iconUrl.trim(),
|
||||
coverUrl: form.coverUrl.trim(),
|
||||
launchMode: form.launchMode.trim() || "h5_popup",
|
||||
launchMode: normalizeLaunchMode(form.launchMode),
|
||||
orientation: form.orientation.trim() || "portrait",
|
||||
minCoin: Number(form.minCoin || 0),
|
||||
status: form.status.trim() || "active",
|
||||
@ -411,6 +418,14 @@ function gamePayload(form) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLaunchMode(value) {
|
||||
const mode = String(value || "").trim();
|
||||
if (mode === "half_screen" || mode === "three_quarter_screen" || mode === "full_screen") {
|
||||
return mode;
|
||||
}
|
||||
return "full_screen";
|
||||
}
|
||||
|
||||
function platformPayload(form) {
|
||||
const isLeaderCC = form.adapterType.trim() === "leadercc_v1";
|
||||
return {
|
||||
@ -428,8 +443,7 @@ function platformPayload(form) {
|
||||
.split(/[\n,]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean),
|
||||
// JSON 不在前端强解析,避免前端版本限制后端新增厂商字段。
|
||||
adapterConfigJson: form.adapterConfigJson.trim() || "{}",
|
||||
adapterConfigJson: normalizeJsonObjectString(form.adapterConfigJson),
|
||||
sortOrder: Number(form.sortOrder || 0),
|
||||
};
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import { AdminFormDialog, AdminFormFieldGrid, AdminFormSection } from "@/shared/
|
||||
import { Button } from "@/shared/ui/Button.jsx";
|
||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||
import { JsonEditorField } from "@/shared/ui/JsonEditorField.jsx";
|
||||
import {
|
||||
AdminActionIconButton,
|
||||
AdminListBody,
|
||||
@ -35,7 +36,11 @@ const orientationOptions = [
|
||||
["portrait", "Portrait"],
|
||||
["landscape", "Landscape"],
|
||||
];
|
||||
const launchModeOptions = [["h5_popup", "H5 弹窗"]];
|
||||
const launchModeOptions = [
|
||||
["full_screen", "全屏"],
|
||||
["half_screen", "半屏"],
|
||||
["three_quarter_screen", "3/4屏"],
|
||||
];
|
||||
// 这里的值必须和 game-service 的 adapter_type 白名单一致;新增厂商先后端登记,再放到后台可选项。
|
||||
const adapterTypeOptions = [
|
||||
["demo", "Demo"],
|
||||
@ -69,7 +74,7 @@ const baseColumns = [
|
||||
width: "minmax(150px, 0.75fr)",
|
||||
render: (game) => (
|
||||
<div className={styles.stack}>
|
||||
<span>{game.launchMode}</span>
|
||||
<span>{launchModeLabel(game.launchMode)}</span>
|
||||
<span className={styles.meta}>{game.orientation}</span>
|
||||
</div>
|
||||
),
|
||||
@ -223,14 +228,15 @@ function GameIdentity({ game }) {
|
||||
}
|
||||
|
||||
function GameStatusSwitch({ page, game }) {
|
||||
const disabled =
|
||||
!page.abilities.canStatus || game.status === "disabled" || page.loadingAction === `status-${game.gameId}`;
|
||||
const disabled = !page.abilities.canStatus || page.loadingAction === `status-${game.gameId}`;
|
||||
return (
|
||||
<AdminSwitch
|
||||
checked={game.status === "active"}
|
||||
checkedLabel="启用"
|
||||
disabled={disabled}
|
||||
label={`${game.gameName || game.gameId} 状态`}
|
||||
size="small"
|
||||
uncheckedLabel={game.status === "disabled" ? "下架" : "维护"}
|
||||
onChange={(event) => page.changeGameStatus(game, event.target.checked)}
|
||||
/>
|
||||
);
|
||||
@ -252,6 +258,7 @@ function GameRowActions({ page, game }) {
|
||||
|
||||
function GameFormDialog({ form, loading, mode, open, page, onClose, onSubmit }) {
|
||||
const disabled = mode === "edit" ? !page.abilities.canUpdate : !page.abilities.canCreate;
|
||||
const gameURL = resolveGameURL(page, form);
|
||||
return (
|
||||
<AdminFormDialog
|
||||
disabled={disabled}
|
||||
@ -330,6 +337,14 @@ function GameFormDialog({ form, loading, mode, open, page, onClose, onSubmit })
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
label="游戏 URL"
|
||||
className={styles.fullField}
|
||||
minRows={2}
|
||||
multiline
|
||||
slotProps={{ input: { readOnly: true } }}
|
||||
value={gameURL || "未配置"}
|
||||
/>
|
||||
<TextField
|
||||
label="最低金币"
|
||||
type="number"
|
||||
@ -472,13 +487,12 @@ function PlatformFormDialog({ form, loading, mode, open, page, onClose, onSubmit
|
||||
/>
|
||||
) : null}
|
||||
{/* 厂商差异字段放 JSON:如 uid_mode、default_lang、token_ttl_seconds、game_level_uid。 */}
|
||||
<TextField
|
||||
<JsonEditorField
|
||||
className={styles.fullField}
|
||||
label="适配器配置 JSON"
|
||||
minRows={5}
|
||||
multiline
|
||||
value={form.adapterConfigJson}
|
||||
onChange={(event) => page.setPlatformForm({ ...form, adapterConfigJson: event.target.value })}
|
||||
onChange={(adapterConfigJson) => page.setPlatformForm({ ...form, adapterConfigJson })}
|
||||
/>
|
||||
</AdminFormFieldGrid>
|
||||
</AdminFormSection>
|
||||
@ -615,3 +629,32 @@ function SwitchOnlyField({ checked, checkedLabel, disabled, label, onChange, unc
|
||||
function formatNumber(value) {
|
||||
return Number(value || 0).toLocaleString();
|
||||
}
|
||||
|
||||
function launchModeLabel(value) {
|
||||
return launchModeOptions.find(([mode]) => mode === value)?.[1] || "全屏";
|
||||
}
|
||||
|
||||
function resolveGameURL(page, form) {
|
||||
const platform = (page.platformData.items || []).find((item) => item.platformCode === form.platformCode);
|
||||
if (!platform) {
|
||||
return "";
|
||||
}
|
||||
const config = parseAdapterConfig(platform.adapterConfigJson);
|
||||
const gameURLs = config?.game_urls && typeof config.game_urls === "object" ? config.game_urls : {};
|
||||
const candidates = [form.providerGameId, form.gameId, String(form.gameId || "").toLowerCase()].filter(Boolean);
|
||||
for (const key of candidates) {
|
||||
const value = gameURLs[key];
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return platform.apiBaseUrl || "";
|
||||
}
|
||||
|
||||
function parseAdapterConfig(raw) {
|
||||
try {
|
||||
return JSON.parse(raw || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,87 +1,87 @@
|
||||
import Switch from "@mui/material/Switch";
|
||||
|
||||
const baseSwitchSx = {
|
||||
"--switch-width": "40px",
|
||||
"--switch-height": "24px",
|
||||
"--switch-thumb-size": "18px",
|
||||
"--switch-padding": "3px",
|
||||
"--switch-translate-x": "16px",
|
||||
margin: 0
|
||||
"--switch-width": "40px",
|
||||
"--switch-height": "24px",
|
||||
"--switch-thumb-size": "18px",
|
||||
"--switch-padding": "3px",
|
||||
"--switch-translate-x": "16px",
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
function cssContent(value) {
|
||||
return `"${String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
||||
return `"${String(value ?? "")
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
|
||||
function labeledSwitchSx(checkedLabel, uncheckedLabel) {
|
||||
const labelLength = Math.max(Array.from(String(checkedLabel || "")).length, Array.from(String(uncheckedLabel || "")).length);
|
||||
const width = Math.max(56, Math.min(84, 34 + labelLength * 11));
|
||||
const labelLength = Math.max(
|
||||
Array.from(String(checkedLabel || "")).length,
|
||||
Array.from(String(uncheckedLabel || "")).length,
|
||||
);
|
||||
const width = Math.max(56, Math.min(84, 34 + labelLength * 11));
|
||||
|
||||
return {
|
||||
"--switch-width": `${width}px`,
|
||||
"--switch-height": "26px",
|
||||
"--switch-thumb-size": "20px",
|
||||
"--switch-padding": "3px",
|
||||
"--switch-translate-x": `${width - 26}px`,
|
||||
"& .MuiSwitch-track": {
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
"&::before, &::after": {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontSize: 10.5,
|
||||
fontWeight: 760,
|
||||
lineHeight: 1,
|
||||
pointerEvents: "none",
|
||||
transition: "opacity 180ms cubic-bezier(0.2, 0, 0, 1)"
|
||||
},
|
||||
"&::before": {
|
||||
right: 26,
|
||||
left: 7,
|
||||
justifyContent: "flex-start",
|
||||
color: "var(--active-contrast)",
|
||||
content: cssContent(checkedLabel),
|
||||
opacity: 0
|
||||
},
|
||||
"&::after": {
|
||||
right: 7,
|
||||
left: 26,
|
||||
justifyContent: "flex-end",
|
||||
color: "var(--text-tertiary)",
|
||||
content: cssContent(uncheckedLabel),
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
"& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track::before": {
|
||||
opacity: 1
|
||||
},
|
||||
"& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track::after": {
|
||||
opacity: 0
|
||||
}
|
||||
};
|
||||
return {
|
||||
"--switch-width": `${width}px`,
|
||||
"--switch-height": "26px",
|
||||
"--switch-thumb-size": "20px",
|
||||
"--switch-padding": "3px",
|
||||
"--switch-translate-x": `${width - 26}px`,
|
||||
"& .MuiSwitch-track": {
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
"&::before, &::after": {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontSize: 10,
|
||||
fontWeight: 720,
|
||||
lineHeight: 1,
|
||||
whiteSpace: "nowrap",
|
||||
pointerEvents: "none",
|
||||
transition: "opacity 180ms cubic-bezier(0.2, 0, 0, 1)",
|
||||
},
|
||||
"&::before": {
|
||||
right: 24,
|
||||
left: 5,
|
||||
justifyContent: "flex-start",
|
||||
color: "var(--active-contrast)",
|
||||
content: cssContent(checkedLabel),
|
||||
opacity: 0,
|
||||
},
|
||||
"&::after": {
|
||||
right: 5,
|
||||
left: 24,
|
||||
justifyContent: "flex-end",
|
||||
color: "var(--text-tertiary)",
|
||||
content: cssContent(uncheckedLabel),
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
"& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track::before": {
|
||||
opacity: 1,
|
||||
},
|
||||
"& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track::after": {
|
||||
opacity: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function AdminSwitch({ checkedLabel, inputProps, label, sx, uncheckedLabel, ...props }) {
|
||||
const hasInlineLabel = Boolean(checkedLabel || uncheckedLabel);
|
||||
const switchLabelSx = hasInlineLabel ? labeledSwitchSx(checkedLabel, uncheckedLabel) : null;
|
||||
const mergedSx = Array.isArray(sx)
|
||||
? [baseSwitchSx, switchLabelSx, ...sx].filter(Boolean)
|
||||
: [baseSwitchSx, switchLabelSx, sx].filter(Boolean);
|
||||
const mergedInputProps = label
|
||||
? {
|
||||
"aria-label": label,
|
||||
...inputProps
|
||||
}
|
||||
: inputProps;
|
||||
const hasInlineLabel = Boolean(checkedLabel || uncheckedLabel);
|
||||
const switchLabelSx = hasInlineLabel ? labeledSwitchSx(checkedLabel, uncheckedLabel) : null;
|
||||
const mergedSx = Array.isArray(sx)
|
||||
? [baseSwitchSx, switchLabelSx, ...sx].filter(Boolean)
|
||||
: [baseSwitchSx, switchLabelSx, sx].filter(Boolean);
|
||||
const mergedInputProps = label
|
||||
? {
|
||||
"aria-label": label,
|
||||
...inputProps,
|
||||
}
|
||||
: inputProps;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
{...props}
|
||||
inputProps={mergedInputProps}
|
||||
sx={mergedSx}
|
||||
/>
|
||||
);
|
||||
return <Switch {...props} inputProps={mergedInputProps} sx={mergedSx} />;
|
||||
}
|
||||
|
||||
114
src/shared/ui/JsonEditorField.jsx
Normal file
114
src/shared/ui/JsonEditorField.jsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { Button } from "@/shared/ui/Button.jsx";
|
||||
import styles from "@/shared/ui/JsonEditorField.module.css";
|
||||
|
||||
export function JsonEditorField({
|
||||
className = "",
|
||||
disabled = false,
|
||||
label,
|
||||
minRows = 6,
|
||||
onChange,
|
||||
required = false,
|
||||
value,
|
||||
}) {
|
||||
const [validated, setValidated] = useState(false);
|
||||
const validation = useMemo(() => validateJsonObject(value), [value]);
|
||||
const showError = validated && !validation.valid;
|
||||
const helperText = showError
|
||||
? validation.message
|
||||
: validated
|
||||
? "JSON 格式正确"
|
||||
: "支持格式化和校验,内容必须是 JSON 对象";
|
||||
const lineCount = Math.max(1, String(value || "").split("\n").length);
|
||||
|
||||
const formatJson = () => {
|
||||
const result = parseJsonObject(value);
|
||||
setValidated(true);
|
||||
if (!result.valid) {
|
||||
return;
|
||||
}
|
||||
onChange(JSON.stringify(result.value, null, 2));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={[styles.root, className].filter(Boolean).join(" ")}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>
|
||||
<span>{label}</span>
|
||||
{required ? <span className={styles.required}>*</span> : null}
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<Button disabled={disabled} onClick={() => setValidated(true)}>
|
||||
校验 JSON
|
||||
</Button>
|
||||
<Button disabled={disabled} onClick={formatJson}>
|
||||
格式化 JSON
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={[styles.editor, showError ? styles.editorInvalid : ""].filter(Boolean).join(" ")}>
|
||||
<pre className={styles.gutter} aria-hidden="true">
|
||||
{Array.from({ length: lineCount }, (_, index) => index + 1).join("\n")}
|
||||
</pre>
|
||||
<TextField
|
||||
className={styles.editorField}
|
||||
disabled={disabled}
|
||||
minRows={minRows}
|
||||
multiline
|
||||
slotProps={{
|
||||
input: { disableUnderline: true },
|
||||
htmlInput: {
|
||||
"aria-label": label,
|
||||
autoCapitalize: "off",
|
||||
autoCorrect: "off",
|
||||
spellCheck: false,
|
||||
},
|
||||
}}
|
||||
value={value}
|
||||
variant="standard"
|
||||
onBlur={() => setValidated(true)}
|
||||
onChange={(event) => {
|
||||
setValidated(false);
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={[styles.helper, showError ? styles.helperInvalid : validated ? styles.helperValid : ""].join(
|
||||
" ",
|
||||
)}
|
||||
>
|
||||
{helperText}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeJsonObjectString(raw) {
|
||||
const result = parseJsonObject(raw);
|
||||
if (!result.valid) {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
return JSON.stringify(result.value);
|
||||
}
|
||||
|
||||
function validateJsonObject(raw) {
|
||||
return parseJsonObject(raw);
|
||||
}
|
||||
|
||||
function parseJsonObject(raw) {
|
||||
const value = String(raw || "").trim();
|
||||
if (!value) {
|
||||
return { valid: true, value: {} };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return { valid: false, message: '配置 JSON 必须是对象,例如 {"app_id": 123}' };
|
||||
}
|
||||
return { valid: true, value: parsed };
|
||||
} catch (err) {
|
||||
return { valid: false, message: `JSON 格式错误:${err.message}` };
|
||||
}
|
||||
}
|
||||
105
src/shared/ui/JsonEditorField.module.css
Normal file
105
src/shared/ui/JsonEditorField.module.css
Normal file
@ -0,0 +1,105 @@
|
||||
.root {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(90deg, var(--bg-card-strong) 0 44px, transparent 44px), var(--bg-card);
|
||||
transition:
|
||||
border-color var(--motion-fast) var(--ease-standard),
|
||||
box-shadow var(--motion-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.editor:focus-within {
|
||||
border-color: var(--primary-border-strong);
|
||||
box-shadow: 0 0 0 3px var(--primary-surface);
|
||||
}
|
||||
|
||||
.editorInvalid {
|
||||
border-color: var(--danger-border-strong);
|
||||
box-shadow: 0 0 0 3px var(--danger-surface);
|
||||
}
|
||||
|
||||
.gutter {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
padding: 14px 10px 14px 0;
|
||||
border-right: 1px solid var(--border-soft);
|
||||
color: var(--text-tertiary);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.editorField {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.editorField :global(.MuiInputBase-root) {
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.editorField :global(textarea) {
|
||||
box-sizing: border-box;
|
||||
padding: 14px 14px 14px 12px;
|
||||
color: var(--text-primary);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.editorField :global(textarea::placeholder) {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.helper {
|
||||
min-height: 18px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.helperInvalid {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.helperValid {
|
||||
color: var(--success);
|
||||
}
|
||||
12
src/shared/ui/JsonEditorField.test.jsx
Normal file
12
src/shared/ui/JsonEditorField.test.jsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { normalizeJsonObjectString } from "@/shared/ui/JsonEditorField.jsx";
|
||||
|
||||
test("normalizes valid json object strings", () => {
|
||||
expect(normalizeJsonObjectString(`{"app_id":307715}`)).toBe(`{"app_id":307715}`);
|
||||
expect(normalizeJsonObjectString("")).toBe("{}");
|
||||
});
|
||||
|
||||
test("rejects invalid json and non-object json", () => {
|
||||
expect(() => normalizeJsonObjectString("{")).toThrow("JSON 格式错误");
|
||||
expect(() => normalizeJsonObjectString("[]")).toThrow("配置 JSON 必须是对象");
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user