Add admin settings UI and provider deletes
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { CheckCircle2, PlugZap } from "lucide-react";
|
||||
import { CheckCircle2, PlugZap, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { api } from "@/lib/api";
|
||||
import { api, ApiError } from "@/lib/api";
|
||||
|
||||
type ProviderAccountRow = {
|
||||
id: number;
|
||||
@@ -74,6 +74,18 @@ export default function ProviderAccountsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (accountId: number) =>
|
||||
api.del(`/api/v1/admin/provider-accounts/${accountId}`),
|
||||
async onSuccess() {
|
||||
setFeedback("渠道已删除。");
|
||||
await query.refetch();
|
||||
},
|
||||
onError(error) {
|
||||
setFeedback(error instanceof ApiError ? error.message : "删除失败,请稍后重试。");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="two-col-grid">
|
||||
<section className="panel">
|
||||
@@ -228,9 +240,25 @@ export default function ProviderAccountsPage() {
|
||||
<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 className="row">
|
||||
<span className={item.status === 1 ? "status-badge tone-success" : "status-badge tone-ghost"}>
|
||||
{item.status === 1 ? "启用" : "停用"}
|
||||
</span>
|
||||
<button
|
||||
className="danger-button compact-button admin-icon-action"
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={() => {
|
||||
if (window.confirm(`确定删除渠道「${item.providerName}」吗?该渠道下未使用的供应商模型也会一起删除。`)) {
|
||||
deleteMutation.mutate(item.id);
|
||||
}
|
||||
}}
|
||||
title="删除渠道"
|
||||
type="button"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-meta-grid">
|
||||
<span>{protocolOptions.find((option) => option.value === item.apiFormat)?.label ?? item.apiFormat}</span>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Boxes, Plus } from "lucide-react";
|
||||
import { Boxes, Plus, Trash2 } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { api } from "@/lib/api";
|
||||
import { api, ApiError } from "@/lib/api";
|
||||
|
||||
type ProviderAccountRow = {
|
||||
id: number;
|
||||
@@ -100,6 +100,17 @@ export default function ProviderModelsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (modelId: number) => api.del(`/api/v1/admin/provider-models/${modelId}`),
|
||||
async onSuccess() {
|
||||
setFeedback("模型已删除。");
|
||||
await modelsQuery.refetch();
|
||||
},
|
||||
onError(error) {
|
||||
setFeedback(error instanceof ApiError ? error.message : "删除失败,请稍后重试。");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="two-col-grid">
|
||||
<section className="panel">
|
||||
@@ -279,9 +290,25 @@ export default function ProviderModelsPage() {
|
||||
<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 className="row">
|
||||
<span className={item.status === 1 ? "status-badge tone-success" : "status-badge tone-ghost"}>
|
||||
{item.status === 1 ? "启用" : "停用"}
|
||||
</span>
|
||||
<button
|
||||
className="danger-button compact-button admin-icon-action"
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={() => {
|
||||
if (window.confirm(`确定删除模型「${item.modelName || item.modelCode}」吗?相关模型绑定会一起删除。`)) {
|
||||
deleteMutation.mutate(item.id);
|
||||
}
|
||||
}}
|
||||
title="删除模型"
|
||||
type="button"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-meta-grid">
|
||||
<span>{item.providerName}</span>
|
||||
|
||||
@@ -1,73 +1,466 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { Save, Settings2, SlidersHorizontal } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { api } from "@/lib/api";
|
||||
import { api, ApiError } from "@/lib/api";
|
||||
|
||||
type ConfigRow = {
|
||||
configKey: string;
|
||||
configValue: string;
|
||||
valueType: string;
|
||||
groupName: string;
|
||||
description: string;
|
||||
isPublic: boolean;
|
||||
};
|
||||
|
||||
const fieldLabels: Record<string, string> = {
|
||||
config_key: "配置键",
|
||||
config_value: "配置值",
|
||||
value_type: "值类型",
|
||||
group_name: "配置分组",
|
||||
description: "配置说明",
|
||||
is_public: "是否公开",
|
||||
type EditableConfig = {
|
||||
key: string;
|
||||
label: string;
|
||||
helper: string;
|
||||
group: string;
|
||||
valueType: "string" | "number" | "boolean";
|
||||
control: "text" | "textarea" | "number" | "switch";
|
||||
defaultValue: string;
|
||||
isPublic: boolean;
|
||||
};
|
||||
|
||||
const groupMeta = [
|
||||
{ key: "site", title: "站点设置", subtitle: "前台展示名称和公告文案。" },
|
||||
{ key: "reward", title: "奖励设置", subtitle: "注册奖励、邀请奖励的开关和积分。" },
|
||||
{ key: "invite", title: "邀请设置", subtitle: "控制邀请码体系是否开放。" },
|
||||
{ key: "task", title: "任务设置", subtitle: "视频任务调度和轮询默认值。" },
|
||||
];
|
||||
|
||||
const editableConfigs: EditableConfig[] = [
|
||||
{
|
||||
key: "site.title",
|
||||
label: "站点名称",
|
||||
helper: "展示在前台导航、标题和登录页。",
|
||||
group: "site",
|
||||
valueType: "string",
|
||||
control: "text",
|
||||
defaultValue: "AIVideo",
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
key: "site.notice",
|
||||
label: "站点公告",
|
||||
helper: "给用户展示的运营提示,可以留空。",
|
||||
group: "site",
|
||||
valueType: "string",
|
||||
control: "textarea",
|
||||
defaultValue: "",
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
key: "reward.signup.enabled",
|
||||
label: "注册奖励",
|
||||
helper: "开启后,新用户注册自动发放积分。",
|
||||
group: "reward",
|
||||
valueType: "boolean",
|
||||
control: "switch",
|
||||
defaultValue: "1",
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
key: "reward.signup.points",
|
||||
label: "注册奖励积分",
|
||||
helper: "新用户注册时发放的积分数量。",
|
||||
group: "reward",
|
||||
valueType: "number",
|
||||
control: "number",
|
||||
defaultValue: "300",
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
key: "reward.invite.enabled",
|
||||
label: "邀请奖励",
|
||||
helper: "开启后,邀请关系成立时发放奖励。",
|
||||
group: "reward",
|
||||
valueType: "boolean",
|
||||
control: "switch",
|
||||
defaultValue: "1",
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
key: "reward.invite.points",
|
||||
label: "邀请奖励积分",
|
||||
helper: "邀请成功后给邀请人的积分数量。",
|
||||
group: "reward",
|
||||
valueType: "number",
|
||||
control: "number",
|
||||
defaultValue: "500",
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
key: "invite.code.enabled",
|
||||
label: "邀请码系统",
|
||||
helper: "关闭后不再展示邀请入口和邀请码能力。",
|
||||
group: "invite",
|
||||
valueType: "boolean",
|
||||
control: "switch",
|
||||
defaultValue: "1",
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
key: "task.default_poll_interval_seconds",
|
||||
label: "任务轮询间隔",
|
||||
helper: "后台轮询供应商任务状态的默认秒数。",
|
||||
group: "task",
|
||||
valueType: "number",
|
||||
control: "number",
|
||||
defaultValue: "5",
|
||||
isPublic: false,
|
||||
},
|
||||
];
|
||||
|
||||
const editableKeys = new Set(editableConfigs.map((item) => item.key));
|
||||
|
||||
function isEnabled(value: string) {
|
||||
return value === "1" || value === "true";
|
||||
}
|
||||
|
||||
export default function SystemConfigPage() {
|
||||
const [form, setForm] = useState({
|
||||
config_key: "site.notice",
|
||||
config_value: "当前为本地联调环境。",
|
||||
value_type: "string",
|
||||
group_name: "site",
|
||||
description: "公告",
|
||||
is_public: true,
|
||||
const [draftValues, setDraftValues] = useState<Record<string, string>>({});
|
||||
const [customForm, setCustomForm] = useState({
|
||||
configKey: "",
|
||||
configValue: "",
|
||||
valueType: "string",
|
||||
groupName: "custom",
|
||||
description: "",
|
||||
isPublic: false,
|
||||
});
|
||||
const [feedback, setFeedback] = useState("");
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["system-config"],
|
||||
queryFn: () => api.get<ConfigRow[]>("/api/v1/admin/system-config"),
|
||||
});
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => api.put("/api/v1/admin/system-config", form),
|
||||
onSuccess: () => query.refetch(),
|
||||
|
||||
const configByKey = useMemo(
|
||||
() => new Map((query.data ?? []).map((item) => [item.configKey, item])),
|
||||
[query.data],
|
||||
);
|
||||
|
||||
const groupedConfigs = useMemo(
|
||||
() =>
|
||||
groupMeta.map((group) => ({
|
||||
...group,
|
||||
configs: editableConfigs.filter((config) => config.group === group.key),
|
||||
})),
|
||||
[],
|
||||
);
|
||||
|
||||
const otherConfigs = useMemo(
|
||||
() => (query.data ?? []).filter((item) => !editableKeys.has(item.configKey)),
|
||||
[query.data],
|
||||
);
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
Promise.all(
|
||||
editableConfigs.map((config) =>
|
||||
api.put("/api/v1/admin/system-config", {
|
||||
config_key: config.key,
|
||||
config_value:
|
||||
draftValues[config.key] ??
|
||||
configByKey.get(config.key)?.configValue ??
|
||||
config.defaultValue,
|
||||
value_type: config.valueType,
|
||||
group_name: config.group,
|
||||
description: config.label,
|
||||
is_public: config.isPublic,
|
||||
}),
|
||||
),
|
||||
),
|
||||
async onSuccess() {
|
||||
setFeedback("系统设置已保存。");
|
||||
setDraftValues({});
|
||||
await query.refetch();
|
||||
},
|
||||
onError(error) {
|
||||
setFeedback(error instanceof ApiError ? error.message : "保存失败,请稍后重试。");
|
||||
},
|
||||
});
|
||||
|
||||
const customMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
api.put("/api/v1/admin/system-config", {
|
||||
config_key: customForm.configKey.trim(),
|
||||
config_value: customForm.configValue,
|
||||
value_type: customForm.valueType,
|
||||
group_name: customForm.groupName.trim() || "custom",
|
||||
description: customForm.description.trim(),
|
||||
is_public: customForm.isPublic,
|
||||
}),
|
||||
async onSuccess() {
|
||||
setFeedback("高级配置已保存。");
|
||||
setCustomForm((previous) => ({
|
||||
...previous,
|
||||
configKey: "",
|
||||
configValue: "",
|
||||
description: "",
|
||||
}));
|
||||
await query.refetch();
|
||||
},
|
||||
onError(error) {
|
||||
setFeedback(error instanceof ApiError ? error.message : "保存失败,请稍后重试。");
|
||||
},
|
||||
});
|
||||
|
||||
function getConfigValue(config: EditableConfig) {
|
||||
return (
|
||||
draftValues[config.key] ??
|
||||
configByKey.get(config.key)?.configValue ??
|
||||
config.defaultValue
|
||||
);
|
||||
}
|
||||
|
||||
function renderControl(config: EditableConfig) {
|
||||
const value = getConfigValue(config);
|
||||
if (config.control === "switch") {
|
||||
const enabled = isEnabled(value);
|
||||
return (
|
||||
<button
|
||||
className={enabled ? "admin-switch is-on" : "admin-switch"}
|
||||
onClick={() =>
|
||||
setDraftValues((previous) => ({
|
||||
...previous,
|
||||
[config.key]: enabled ? "0" : "1",
|
||||
}))
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<span>{enabled ? "已开启" : "已关闭"}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (config.control === "textarea") {
|
||||
return (
|
||||
<textarea
|
||||
className="admin-setting-textarea"
|
||||
value={value}
|
||||
onChange={(event) =>
|
||||
setDraftValues((previous) => ({
|
||||
...previous,
|
||||
[config.key]: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
min={config.control === "number" ? 0 : undefined}
|
||||
type={config.control === "number" ? "number" : "text"}
|
||||
value={value}
|
||||
onChange={(event) =>
|
||||
setDraftValues((previous) => ({
|
||||
...previous,
|
||||
[config.key]: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="two-col-grid">
|
||||
<div className="admin-config-page">
|
||||
<section className="panel">
|
||||
<h3>更新系统配置</h3>
|
||||
<div className="form-stack">
|
||||
{Object.entries(form).map(([key, value]) => (
|
||||
<label className="field-label" key={key}>
|
||||
{fieldLabels[key] ?? key}
|
||||
<input
|
||||
value={String(value)}
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<span className="header-kicker">可视化配置</span>
|
||||
<h3>系统设置</h3>
|
||||
<p className="muted">常用运营项直接在这里开关和填写,不需要改配置文件。</p>
|
||||
</div>
|
||||
<Settings2 size={22} />
|
||||
</div>
|
||||
|
||||
<div className="admin-settings-grid">
|
||||
{groupedConfigs.map((group) => (
|
||||
<article className="admin-setting-group" key={group.key}>
|
||||
<div>
|
||||
<strong>{group.title}</strong>
|
||||
<p>{group.subtitle}</p>
|
||||
</div>
|
||||
<div className="admin-setting-list">
|
||||
{group.configs.map((config) => (
|
||||
<label className="admin-setting-card" key={config.key}>
|
||||
<span>
|
||||
<strong>{config.label}</strong>
|
||||
<small>{config.helper}</small>
|
||||
</span>
|
||||
<div className="admin-setting-control">{renderControl(config)}</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{feedback ? <div className="inline-feedback">{feedback}</div> : null}
|
||||
|
||||
<button
|
||||
className="primary-button admin-save-button"
|
||||
disabled={saveMutation.isPending}
|
||||
onClick={() => saveMutation.mutate()}
|
||||
type="button"
|
||||
>
|
||||
<Save size={16} />
|
||||
保存全部设置
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section className="two-col-grid">
|
||||
<div className="panel">
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<span className="header-kicker">高级配置</span>
|
||||
<h3>新增 / 更新配置</h3>
|
||||
<p className="muted">用于少量非核心配置,也通过表单维护。</p>
|
||||
</div>
|
||||
<SlidersHorizontal size={22} />
|
||||
</div>
|
||||
|
||||
<div className="form-stack admin-simple-form">
|
||||
<div className="admin-form-grid">
|
||||
<label className="field-label">
|
||||
配置键
|
||||
<input
|
||||
placeholder="例如 site.footer"
|
||||
value={customForm.configKey}
|
||||
onChange={(event) =>
|
||||
setCustomForm((previous) => ({
|
||||
...previous,
|
||||
configKey: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="field-label">
|
||||
分组
|
||||
<input
|
||||
value={customForm.groupName}
|
||||
onChange={(event) =>
|
||||
setCustomForm((previous) => ({
|
||||
...previous,
|
||||
groupName: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="field-label">
|
||||
配置值
|
||||
<textarea
|
||||
className="admin-setting-textarea"
|
||||
value={customForm.configValue}
|
||||
onChange={(event) =>
|
||||
setForm((previous) => ({
|
||||
setCustomForm((previous) => ({
|
||||
...previous,
|
||||
[key]:
|
||||
typeof value === "boolean"
|
||||
? event.target.value === "true"
|
||||
: event.target.value,
|
||||
configValue: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
<button className="primary-button" onClick={() => mutation.mutate()}>
|
||||
保存配置
|
||||
</button>
|
||||
|
||||
<div className="admin-form-grid">
|
||||
<label className="field-label">
|
||||
值类型
|
||||
<select
|
||||
value={customForm.valueType}
|
||||
onChange={(event) =>
|
||||
setCustomForm((previous) => ({
|
||||
...previous,
|
||||
valueType: event.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="string">文本</option>
|
||||
<option value="number">数字</option>
|
||||
<option value="boolean">开关</option>
|
||||
<option value="json">JSON</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="field-label">
|
||||
配置说明
|
||||
<input
|
||||
value={customForm.description}
|
||||
onChange={(event) =>
|
||||
setCustomForm((previous) => ({
|
||||
...previous,
|
||||
description: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="toggle-card admin-public-toggle">
|
||||
<input
|
||||
checked={customForm.isPublic}
|
||||
onChange={(event) =>
|
||||
setCustomForm((previous) => ({
|
||||
...previous,
|
||||
isPublic: event.target.checked,
|
||||
}))
|
||||
}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
前台可读取
|
||||
<small>仅站点标题、公告这类公开信息建议开启。</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
className="primary-button"
|
||||
disabled={customMutation.isPending || !customForm.configKey.trim()}
|
||||
onClick={() => customMutation.mutate()}
|
||||
type="button"
|
||||
>
|
||||
保存高级配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel">
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<span className="header-kicker">配置清单</span>
|
||||
<h3>其他配置</h3>
|
||||
</div>
|
||||
<span className="mini-note">{otherConfigs.length} 项</span>
|
||||
</div>
|
||||
|
||||
<div className="admin-card-list">
|
||||
{otherConfigs.length ? (
|
||||
otherConfigs.map((item) => (
|
||||
<article className="admin-data-card" key={item.configKey}>
|
||||
<div className="toolbar">
|
||||
<div>
|
||||
<strong>{item.description || item.configKey}</strong>
|
||||
<div className="muted">{item.configKey}</div>
|
||||
</div>
|
||||
<span className="status-badge tone-ghost">{item.groupName}</span>
|
||||
</div>
|
||||
<div className="admin-meta-grid">
|
||||
<span>{item.valueType}</span>
|
||||
<span>{item.isPublic ? "公开" : "后台"}</span>
|
||||
</div>
|
||||
<p className="admin-config-value">{item.configValue}</p>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-state">暂无其他配置。</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="panel">
|
||||
<h3>配置清单</h3>
|
||||
<pre className="code-block">{JSON.stringify(query.data ?? [], null, 2)}</pre>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2053,6 +2053,11 @@ p {
|
||||
color: var(--admin-text-soft);
|
||||
}
|
||||
|
||||
.admin-icon-action {
|
||||
min-height: 34px;
|
||||
padding: 8px 11px;
|
||||
}
|
||||
|
||||
.admin-model-textarea {
|
||||
min-height: 112px;
|
||||
}
|
||||
@@ -2100,6 +2105,127 @@ p {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.admin-config-page {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.admin-settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.admin-setting-group {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: var(--radius-card);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.admin-setting-group > div:first-child strong {
|
||||
display: block;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.admin-setting-group > div:first-child p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--admin-text-soft);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.admin-setting-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-setting-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(160px, 0.42fr);
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
padding: 13px;
|
||||
border-radius: var(--radius-card);
|
||||
border: 1px solid var(--admin-border);
|
||||
background: #fbf8f1;
|
||||
}
|
||||
|
||||
.admin-setting-card strong,
|
||||
.admin-setting-card small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.admin-setting-card strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.admin-setting-card small {
|
||||
margin-top: 5px;
|
||||
color: var(--admin-text-soft);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.admin-setting-control input,
|
||||
.admin-setting-control textarea,
|
||||
.admin-setting-textarea {
|
||||
width: 100%;
|
||||
padding: 12px 13px;
|
||||
border: 1px solid var(--admin-border-strong);
|
||||
border-radius: var(--radius-control);
|
||||
background: #ffffff;
|
||||
color: var(--admin-text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.admin-setting-textarea {
|
||||
min-height: 104px;
|
||||
resize: vertical;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.admin-switch {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--admin-border-strong);
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: var(--admin-text-soft);
|
||||
cursor: pointer;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.admin-switch.is-on {
|
||||
border-color: rgba(0, 122, 114, 0.34);
|
||||
background: #e8f6f3;
|
||||
color: var(--admin-accent);
|
||||
}
|
||||
|
||||
.admin-save-button {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.admin-public-toggle {
|
||||
margin-top: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.admin-config-value {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-card);
|
||||
background: #fbf8f1;
|
||||
color: var(--admin-text-soft);
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-app-shell .panel,
|
||||
.admin-app-shell .stat-card,
|
||||
.admin-app-shell .pulse-card,
|
||||
@@ -2290,7 +2416,9 @@ p {
|
||||
.upload-row,
|
||||
.admin-auth-grid,
|
||||
.marketing-ops-grid,
|
||||
.admin-form-grid {
|
||||
.admin-form-grid,
|
||||
.admin-settings-grid,
|
||||
.admin-setting-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user