Simplify provider configuration UI
This commit is contained in:
@@ -1,75 +1,254 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { CheckCircle2, PlugZap } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
provider_code: "供应商编码",
|
||||
provider_name: "供应商名称",
|
||||
api_format: "接口协议",
|
||||
base_url: "接口地址",
|
||||
api_key: "接口密钥",
|
||||
api_secret: "接口密钥补充",
|
||||
webhook_secret: "回调密钥",
|
||||
timeout_seconds: "超时时间(秒)",
|
||||
max_retries: "最大重试次数",
|
||||
status: "状态",
|
||||
remark: "备注",
|
||||
type ProviderAccountRow = {
|
||||
id: number;
|
||||
providerCode: string;
|
||||
providerName: string;
|
||||
apiFormat: string;
|
||||
baseUrl: string;
|
||||
timeoutSeconds: number;
|
||||
maxRetries: number;
|
||||
status: number;
|
||||
remark: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
const protocolOptions = [
|
||||
{
|
||||
value: "openai_official_video",
|
||||
label: "OpenAI 兼容视频",
|
||||
note: "/v1/videos",
|
||||
},
|
||||
{
|
||||
value: "seedance_video_generation",
|
||||
label: "Seedance 旧版",
|
||||
note: "/v1/video/generations",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ProviderAccountsPage() {
|
||||
const [form, setForm] = useState({
|
||||
provider_code: "openai-backup",
|
||||
provider_name: "OpenAI 备用账号",
|
||||
api_format: "openai_official_video",
|
||||
base_url: "mock://openai",
|
||||
api_key: "mock",
|
||||
providerCode: "newapi-seedance",
|
||||
providerName: "NewAPI Seedance",
|
||||
apiFormat: "openai_official_video",
|
||||
baseUrl: "https://your-newapi-domain.com",
|
||||
apiKey: "",
|
||||
status: 1,
|
||||
remark: "NewAPI 上游",
|
||||
});
|
||||
|
||||
const [feedback, setFeedback] = useState("");
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["provider-accounts"],
|
||||
queryFn: () => api.get<ProviderAccountRow[]>("/api/v1/admin/provider-accounts"),
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
api.post("/api/v1/admin/provider-accounts", {
|
||||
provider_code: form.providerCode.trim(),
|
||||
provider_name: form.providerName.trim(),
|
||||
api_format: form.apiFormat,
|
||||
base_url: form.baseUrl.trim().replace(/\/+$/, ""),
|
||||
api_key: form.apiKey.trim(),
|
||||
api_secret: "",
|
||||
webhook_secret: "",
|
||||
timeout_seconds: 120,
|
||||
max_retries: 3,
|
||||
status: 1,
|
||||
remark: "备用线路",
|
||||
});
|
||||
const query = useQuery({
|
||||
queryKey: ["provider-accounts"],
|
||||
queryFn: () => api.get("/api/v1/admin/provider-accounts"),
|
||||
});
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => api.post("/api/v1/admin/provider-accounts", form),
|
||||
onSuccess: () => query.refetch(),
|
||||
status: form.status,
|
||||
remark: form.remark.trim(),
|
||||
}),
|
||||
async onSuccess() {
|
||||
setFeedback("渠道已创建。");
|
||||
await query.refetch();
|
||||
},
|
||||
onError() {
|
||||
setFeedback("创建失败,请检查渠道编码是否重复或密钥是否为空。");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="two-col-grid">
|
||||
<section className="panel">
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<span className="header-kicker">渠道配置</span>
|
||||
<h3>新增供应商账号</h3>
|
||||
<div className="form-stack">
|
||||
{Object.entries(form).map(([key, value]) => (
|
||||
<label className="field-label" key={key}>
|
||||
{fieldLabels[key] ?? key}
|
||||
<p className="muted">按 NewAPI 的渠道思路配置:协议、地址、密钥。</p>
|
||||
</div>
|
||||
<PlugZap size={22} />
|
||||
</div>
|
||||
|
||||
<div className="form-stack admin-simple-form">
|
||||
<div className="admin-form-grid">
|
||||
<label className="field-label">
|
||||
渠道名称
|
||||
<input
|
||||
value={String(value)}
|
||||
value={form.providerName}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
[key]:
|
||||
typeof value === "number" ? Number(event.target.value) : event.target.value,
|
||||
providerName: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="field-label">
|
||||
渠道编码
|
||||
<input
|
||||
value={form.providerCode}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
providerCode: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="field-label">
|
||||
接口协议
|
||||
<select
|
||||
value={form.apiFormat}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
apiFormat: event.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{protocolOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label} - {option.note}
|
||||
</option>
|
||||
))}
|
||||
<button className="primary-button" onClick={() => createMutation.mutate()}>
|
||||
创建供应商账号
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="field-label">
|
||||
接口地址
|
||||
<input
|
||||
value={form.baseUrl}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
baseUrl: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="field-label">
|
||||
密钥
|
||||
<input
|
||||
placeholder="sk-..."
|
||||
type="password"
|
||||
value={form.apiKey}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
apiKey: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="admin-form-grid">
|
||||
<label className="field-label">
|
||||
状态
|
||||
<select
|
||||
value={form.status}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
status: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value={1}>启用</option>
|
||||
<option value={0}>停用</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="field-label">
|
||||
备注
|
||||
<input
|
||||
value={form.remark}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
remark: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{feedback ? <div className="inline-feedback">{feedback}</div> : null}
|
||||
|
||||
<button
|
||||
className="primary-button"
|
||||
disabled={
|
||||
createMutation.isPending ||
|
||||
!form.providerCode.trim() ||
|
||||
!form.providerName.trim() ||
|
||||
!form.baseUrl.trim() ||
|
||||
!form.apiKey.trim()
|
||||
}
|
||||
onClick={() => createMutation.mutate()}
|
||||
type="button"
|
||||
>
|
||||
创建渠道
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<h3>当前账号</h3>
|
||||
<pre className="code-block">{JSON.stringify(query.data ?? [], null, 2)}</pre>
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<span className="header-kicker">渠道列表</span>
|
||||
<h3>当前供应商账号</h3>
|
||||
</div>
|
||||
<span className="mini-note">{query.data?.length ?? 0} 个渠道</span>
|
||||
</div>
|
||||
|
||||
<div className="admin-card-list">
|
||||
{query.data?.length ? (
|
||||
query.data.map((item) => (
|
||||
<article className="admin-data-card" key={item.id}>
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<strong>{item.providerName}</strong>
|
||||
<div className="muted">{item.providerCode}</div>
|
||||
</div>
|
||||
<span className={item.status === 1 ? "status-badge tone-success" : "status-badge tone-ghost"}>
|
||||
{item.status === 1 ? "启用" : "停用"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="admin-meta-grid">
|
||||
<span>{protocolOptions.find((option) => option.value === item.apiFormat)?.label ?? item.apiFormat}</span>
|
||||
<span>{item.baseUrl}</span>
|
||||
<span>超时 {item.timeoutSeconds}s</span>
|
||||
</div>
|
||||
{item.remark ? <p className="muted">{item.remark}</p> : null}
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-state">还没有供应商账号。</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="inline-feedback admin-helper-note">
|
||||
<CheckCircle2 size={16} />
|
||||
NewAPI 上游通常选择 OpenAI 兼容视频,接口地址填写域名根地址即可。
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,49 +1,300 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Boxes, Plus } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
type ProviderAccountRow = {
|
||||
id: number;
|
||||
providerName: string;
|
||||
providerCode: string;
|
||||
status: number;
|
||||
};
|
||||
|
||||
type ProviderModelRow = {
|
||||
id: number;
|
||||
providerAccountId: number;
|
||||
providerName: string;
|
||||
modelCode: string;
|
||||
modelName: string;
|
||||
defaultRatio: string;
|
||||
defaultResolution: string;
|
||||
minDuration: number;
|
||||
maxDuration: number;
|
||||
status: number;
|
||||
};
|
||||
|
||||
function parseModelCodes(value: string) {
|
||||
return value
|
||||
.split(/[\n,,]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export default function ProviderModelsPage() {
|
||||
const form = {
|
||||
provider_account_id: 1,
|
||||
model_code: "sora-2-pro",
|
||||
model_name: "Sora 2 Pro",
|
||||
request_content_type: "multipart/form-data",
|
||||
const [form, setForm] = useState({
|
||||
providerAccountId: 0,
|
||||
modelCodes: "seedance, seedance-fast",
|
||||
defaultRatio: "16:9",
|
||||
defaultResolution: "1280x720",
|
||||
minDuration: 4,
|
||||
maxDuration: 12,
|
||||
status: 1,
|
||||
});
|
||||
const [feedback, setFeedback] = useState("");
|
||||
|
||||
const accountsQuery = useQuery({
|
||||
queryKey: ["provider-accounts"],
|
||||
queryFn: () => api.get<ProviderAccountRow[]>("/api/v1/admin/provider-accounts"),
|
||||
});
|
||||
|
||||
const modelsQuery = useQuery({
|
||||
queryKey: ["provider-models"],
|
||||
queryFn: () => api.get<ProviderModelRow[]>("/api/v1/admin/provider-models"),
|
||||
});
|
||||
|
||||
const enabledAccounts = useMemo(
|
||||
() => (accountsQuery.data ?? []).filter((item) => item.status === 1),
|
||||
[accountsQuery.data],
|
||||
);
|
||||
|
||||
const activeProviderAccountId =
|
||||
form.providerAccountId || enabledAccounts[0]?.id || accountsQuery.data?.[0]?.id || 0;
|
||||
|
||||
const modelCodes = parseModelCodes(form.modelCodes);
|
||||
const uniqueModelCodes = Array.from(new Set(modelCodes));
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return Promise.all(
|
||||
uniqueModelCodes.map((modelCode) =>
|
||||
api.post("/api/v1/admin/provider-models", {
|
||||
provider_account_id: activeProviderAccountId,
|
||||
model_code: modelCode,
|
||||
model_name: modelCode,
|
||||
request_content_type: "application/json",
|
||||
supports_text_to_video: true,
|
||||
supports_image_to_video: true,
|
||||
supports_video_reference: false,
|
||||
supports_video_reference: true,
|
||||
supports_audio_reference: false,
|
||||
supports_generate_audio: true,
|
||||
supports_remix: false,
|
||||
supports_webhook: true,
|
||||
min_duration: 4,
|
||||
max_duration: 12,
|
||||
default_ratio: "16:9",
|
||||
default_resolution: "1280x720",
|
||||
status: 1,
|
||||
};
|
||||
const query = useQuery({
|
||||
queryKey: ["provider-models"],
|
||||
queryFn: () => api.get("/api/v1/admin/provider-models"),
|
||||
});
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => api.post("/api/v1/admin/provider-models", form),
|
||||
onSuccess: () => query.refetch(),
|
||||
supports_webhook: false,
|
||||
min_duration: form.minDuration,
|
||||
max_duration: form.maxDuration,
|
||||
default_ratio: form.defaultRatio,
|
||||
default_resolution: form.defaultResolution,
|
||||
status: form.status,
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
async onSuccess() {
|
||||
setFeedback(`已创建 ${uniqueModelCodes.length} 个模型。`);
|
||||
await modelsQuery.refetch();
|
||||
},
|
||||
onError() {
|
||||
setFeedback("创建失败,请检查模型名是否重复或渠道是否可用。");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="two-col-grid">
|
||||
<section className="panel">
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<span className="header-kicker">模型配置</span>
|
||||
<h3>新增供应商模型</h3>
|
||||
<pre className="code-block">{JSON.stringify(form, null, 2)}</pre>
|
||||
<button className="primary-button" style={{ marginTop: 16 }} onClick={() => createMutation.mutate()}>
|
||||
创建供应商模型
|
||||
<p className="muted">像 NewAPI 一样,把模型名粘进来即可。</p>
|
||||
</div>
|
||||
<Boxes size={22} />
|
||||
</div>
|
||||
|
||||
<div className="form-stack admin-simple-form">
|
||||
<label className="field-label">
|
||||
所属渠道
|
||||
<select
|
||||
value={activeProviderAccountId}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
providerAccountId: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
>
|
||||
{accountsQuery.data?.length ? null : (
|
||||
<option value={0} disabled>
|
||||
请先创建供应商账号
|
||||
</option>
|
||||
)}
|
||||
{(accountsQuery.data ?? []).map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.providerName}({account.providerCode})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="field-label">
|
||||
模型名称
|
||||
<textarea
|
||||
className="prompt-editor admin-model-textarea"
|
||||
value={form.modelCodes}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
modelCodes: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="field-label">
|
||||
状态
|
||||
<select
|
||||
value={form.status}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
status: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value={1}>启用</option>
|
||||
<option value={0}>停用</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<details className="admin-advanced-block">
|
||||
<summary>默认视频参数</summary>
|
||||
<div className="admin-form-grid">
|
||||
<label className="field-label">
|
||||
默认比例
|
||||
<select
|
||||
value={form.defaultRatio}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
defaultRatio: event.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="16:9">16:9</option>
|
||||
<option value="9:16">9:16</option>
|
||||
<option value="1:1">1:1</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="field-label">
|
||||
默认分辨率
|
||||
<select
|
||||
value={form.defaultResolution}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
defaultResolution: event.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="1280x720">1280x720</option>
|
||||
<option value="1920x1080">1920x1080</option>
|
||||
<option value="720x1280">720x1280</option>
|
||||
<option value="1080x1920">1080x1920</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-grid">
|
||||
<label className="field-label">
|
||||
最小时长
|
||||
<input
|
||||
type="number"
|
||||
value={form.minDuration}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
minDuration: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="field-label">
|
||||
最大时长
|
||||
<input
|
||||
type="number"
|
||||
value={form.maxDuration}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
...previous,
|
||||
maxDuration: Number(event.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="admin-model-preview">
|
||||
{modelCodes.length ? (
|
||||
uniqueModelCodes.map((modelCode) => <span key={modelCode}>{modelCode}</span>)
|
||||
) : (
|
||||
<span>等待输入模型名</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{feedback ? <div className="inline-feedback">{feedback}</div> : null}
|
||||
|
||||
<button
|
||||
className="primary-button"
|
||||
disabled={
|
||||
createMutation.isPending ||
|
||||
!activeProviderAccountId ||
|
||||
!modelCodes.length
|
||||
}
|
||||
onClick={() => createMutation.mutate()}
|
||||
type="button"
|
||||
>
|
||||
<Plus size={16} />
|
||||
批量创建模型
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<h3>当前模型</h3>
|
||||
<pre className="code-block">{JSON.stringify(query.data ?? [], null, 2)}</pre>
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<span className="header-kicker">模型列表</span>
|
||||
<h3>当前供应商模型</h3>
|
||||
</div>
|
||||
<span className="mini-note">{modelsQuery.data?.length ?? 0} 个模型</span>
|
||||
</div>
|
||||
|
||||
<div className="admin-card-list">
|
||||
{modelsQuery.data?.length ? (
|
||||
modelsQuery.data.map((item) => (
|
||||
<article className="admin-data-card" key={item.id}>
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<strong>{item.modelName || item.modelCode}</strong>
|
||||
<div className="muted">{item.modelCode}</div>
|
||||
</div>
|
||||
<span className={item.status === 1 ? "status-badge tone-success" : "status-badge tone-ghost"}>
|
||||
{item.status === 1 ? "启用" : "停用"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="admin-meta-grid">
|
||||
<span>{item.providerName}</span>
|
||||
<span>{item.defaultResolution}</span>
|
||||
<span>{item.defaultRatio}</span>
|
||||
<span>{item.minDuration}-{item.maxDuration}s</span>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-state">还没有供应商模型。</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1841,7 +1841,7 @@ p {
|
||||
.admin-app-shell .inline-feedback.is-error {
|
||||
border-color: rgba(255, 111, 54, 0.34);
|
||||
background: rgba(255, 111, 54, 0.09);
|
||||
color: #ffd3c0;
|
||||
color: #9b3411;
|
||||
}
|
||||
|
||||
.admin-app-shell {
|
||||
@@ -1991,6 +1991,115 @@ p {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.admin-simple-form {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.admin-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.admin-card-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.admin-data-card {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: var(--radius-card);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.admin-data-card strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.admin-data-card p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-meta-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-meta-grid span {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
padding: 7px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--admin-border);
|
||||
background: #f7f3ea;
|
||||
color: var(--admin-text-soft);
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-helper-note {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
color: var(--admin-text-soft);
|
||||
}
|
||||
|
||||
.admin-model-textarea {
|
||||
min-height: 112px;
|
||||
}
|
||||
|
||||
.admin-model-preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-model-preview span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
padding: 7px 11px;
|
||||
border-radius: 999px;
|
||||
background: #f0f7f5;
|
||||
border: 1px solid #c7ddd9;
|
||||
color: var(--admin-accent);
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.admin-advanced-block {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: var(--radius-card);
|
||||
background: #fbf8f1;
|
||||
}
|
||||
|
||||
.admin-advanced-block summary {
|
||||
cursor: pointer;
|
||||
color: var(--admin-text-soft);
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.admin-advanced-block[open] summary {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.admin-advanced-block .admin-form-grid + .admin-form-grid {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.admin-app-shell .panel,
|
||||
.admin-app-shell .stat-card,
|
||||
.admin-app-shell .pulse-card,
|
||||
@@ -2180,7 +2289,8 @@ p {
|
||||
.marketing-stat-row,
|
||||
.upload-row,
|
||||
.admin-auth-grid,
|
||||
.marketing-ops-grid {
|
||||
.marketing-ops-grid,
|
||||
.admin-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user