Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
W
WebAgent
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xuchentao
WebAgent
Commits
33d48dd2
Commit
33d48dd2
authored
Jul 20, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add unified action dialogs
parent
9b479a5e
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
75 additions
and
19 deletions
+75
-19
App.tsx
frontend/src/App.tsx
+73
-18
styles.css
frontend/src/styles.css
+2
-1
No files found.
frontend/src/App.tsx
View file @
33d48dd2
...
...
@@ -6,7 +6,7 @@ import {
Plus
,
RefreshCw
,
Rocket
,
RotateCcw
,
Search
,
Send
,
Settings2
,
ShieldCheck
,
Smartphone
,
Sparkles
,
Trash2
,
WandSparkles
,
X
,
}
from
"lucide-react"
;
import
type
{
AgentRunEvent
,
CreateSiteInput
,
CreateTenantInput
,
DomainBinding
,
SessionInfo
,
SiteInfo
,
TenantAdminInfo
}
from
"@webagent/shared"
;
import
type
{
AgentRunEvent
,
CreateSiteInput
,
CreateTenantInput
,
DomainBinding
,
GitHistoryItem
,
SessionInfo
,
SiteInfo
,
TenantAdminInfo
}
from
"@webagent/shared"
;
import
{
api
}
from
"./api"
;
type
ChatMessage
=
{
role
:
"user"
|
"agent"
;
text
:
string
;
meta
?:
string
};
...
...
@@ -127,6 +127,10 @@ function AdminWorkspace({ session, onLogout }: { session: SessionInfo; onLogout:
const
[
selectedTenantId
,
setSelectedTenantId
]
=
useState
(
""
);
const
[
showCreate
,
setShowCreate
]
=
useState
(
false
);
const
[
form
,
setForm
]
=
useState
<
CreateTenantInput
>
({
name
:
""
,
username
:
""
,
password
:
""
});
const
[
passwordTarget
,
setPasswordTarget
]
=
useState
<
{
tenantId
:
string
;
userId
:
string
;
username
:
string
}
>
();
const
[
newPassword
,
setNewPassword
]
=
useState
(
""
);
const
[
revokeTarget
,
setRevokeTarget
]
=
useState
<
{
tenantId
:
string
;
userId
:
string
;
username
:
string
}
>
();
const
[
adminNotice
,
setAdminNotice
]
=
useState
(
""
);
const
refresh
=
()
=>
queryClient
.
invalidateQueries
({
queryKey
:
[
"admin-tenants"
]
});
const
createMutation
=
useMutation
({
mutationFn
:
api
.
createTenant
,
...
...
@@ -135,11 +139,11 @@ function AdminWorkspace({ session, onLogout }: { session: SessionInfo; onLogout:
const
statusMutation
=
useMutation
({
mutationFn
:
({
tenantId
,
enabled
}:
{
tenantId
:
string
;
enabled
:
boolean
})
=>
api
.
setTenantEnabled
(
tenantId
,
enabled
),
onSuccess
:
refresh
});
const
passwordMutation
=
useMutation
({
mutationFn
:
({
tenantId
,
userId
,
password
}:
{
tenantId
:
string
;
userId
:
string
;
password
:
string
})
=>
api
.
resetTenantPassword
(
tenantId
,
userId
,
password
),
onSuccess
:
()
=>
window
.
alert
(
"密码已重置,该账号的全部 session 已注销"
)
,
onSuccess
:
()
=>
{
setPasswordTarget
(
undefined
);
setNewPassword
(
""
);
setAdminNotice
(
"密码已重置,该账号的全部 session 已注销。"
);
}
,
});
const
revokeMutation
=
useMutation
({
mutationFn
:
({
tenantId
,
userId
}:
{
tenantId
:
string
;
userId
:
string
})
=>
api
.
revokeTenantSessions
(
tenantId
,
userId
),
onSuccess
:
()
=>
window
.
alert
(
"该账号的全部 session 已注销"
)
,
onSuccess
:
()
=>
{
setRevokeTarget
(
undefined
);
setAdminNotice
(
"该账号的全部 session 已注销。"
);
}
,
});
const
tenants
=
tenantsQuery
.
data
||
[];
const
selected
=
tenants
.
find
((
tenant
)
=>
tenant
.
tenantId
===
selectedTenantId
)
||
tenants
[
0
];
...
...
@@ -149,11 +153,6 @@ function AdminWorkspace({ session, onLogout }: { session: SessionInfo; onLogout:
enabled
:
Boolean
(
selected
&&
!
showCreate
),
});
const
resetPassword
=
(
tenant
:
TenantAdminInfo
,
userId
:
string
)
=>
{
const
password
=
window
.
prompt
(
"输入新密码(至少 8 个字符)"
);
if
(
password
)
passwordMutation
.
mutate
({
tenantId
:
tenant
.
tenantId
,
userId
,
password
});
};
return
<
main
className=
"admin-page"
>
<
header
className=
"admin-topbar"
><
Logo
/><
div
><
span
className=
"settings-avatar"
>
{
session
.
username
.
slice
(
0
,
1
).
toUpperCase
()
}
</
span
><
span
><
strong
>
{
session
.
username
}
</
strong
><
small
>
系统管理员
</
small
></
span
><
button
type=
"button"
onClick=
{
onLogout
}
><
ArrowLeft
size=
{
14
}
/>
退出
</
button
></
div
></
header
>
<
div
className=
"admin-layout"
>
...
...
@@ -179,7 +178,7 @@ function AdminWorkspace({ session, onLogout }: { session: SessionInfo; onLogout:
<
div
className=
"admin-stats"
><
article
><
strong
>
{
selected
.
accounts
.
length
}
</
strong
><
span
>
租户账号
</
span
></
article
><
article
><
strong
>
{
selected
.
siteCount
}
</
strong
><
span
>
站点数量
</
span
></
article
><
article
><
strong
className=
{
selected
.
enabled
?
"ok"
:
"off"
}
>
{
selected
.
enabled
?
"已启用"
:
"已禁用"
}
</
strong
><
span
>
租户状态
</
span
></
article
></
div
>
<
section
className=
"admin-card"
>
<
div
className=
"admin-section-title"
><
div
><
h2
>
租户账号
</
h2
><
p
>
重置密码会同时注销该账号的所有现有 session。
</
p
></
div
></
div
>
<
div
className=
"admin-account-list"
>
{
selected
.
accounts
.
map
((
account
)
=>
<
article
key=
{
account
.
userId
}
><
span
className=
"settings-avatar"
>
{
account
.
username
.
slice
(
0
,
1
).
toUpperCase
()
}
</
span
><
div
><
strong
>
{
account
.
username
}
</
strong
><
small
>
{
account
.
userId
}
·
{
account
.
enabled
?
"已启用"
:
"已禁用"
}
</
small
></
div
><
button
type=
"button"
disabled=
{
passwordMutation
.
isPending
}
onClick=
{
()
=>
resetPassword
(
selected
,
account
.
userId
)
}
>
重置密码
</
button
><
button
type=
"button"
disabled=
{
revokeMutation
.
isPending
}
onClick=
{
()
=>
revokeMutation
.
mutate
({
tenantId
:
selected
.
tenantId
,
userId
:
account
.
userId
})
}
>
注销全部 session
</
button
></
article
>)
}
</
div
>
<
div
className=
"admin-account-list"
>
{
selected
.
accounts
.
map
((
account
)
=>
<
article
key=
{
account
.
userId
}
><
span
className=
"settings-avatar"
>
{
account
.
username
.
slice
(
0
,
1
).
toUpperCase
()
}
</
span
><
div
><
strong
>
{
account
.
username
}
</
strong
><
small
>
{
account
.
userId
}
·
{
account
.
enabled
?
"已启用"
:
"已禁用"
}
</
small
></
div
><
button
type=
"button"
disabled=
{
passwordMutation
.
isPending
}
onClick=
{
()
=>
{
passwordMutation
.
reset
();
setNewPassword
(
""
);
setPasswordTarget
({
tenantId
:
selected
.
tenantId
,
userId
:
account
.
userId
,
username
:
account
.
username
});
}
}
>
重置密码
</
button
><
button
type=
"button"
disabled=
{
revokeMutation
.
isPending
}
onClick=
{
()
=>
{
revokeMutation
.
reset
();
setRevokeTarget
({
tenantId
:
selected
.
tenantId
,
userId
:
account
.
userId
,
username
:
account
.
username
});
}
}
>
注销全部 session
</
button
></
article
>)
}
</
div
>
</
section
>
<
section
className=
"admin-card admin-sites-card"
>
<
div
className=
"admin-section-title"
><
div
><
h2
>
租户站点
</
h2
><
p
>
管理员仅可查看站点状态并打开公开链接,不能编辑或发布。
</
p
></
div
><
span
>
{
tenantSitesQuery
.
data
?.
length
??
selected
.
siteCount
}
</
span
></
div
>
...
...
@@ -196,6 +195,9 @@ function AdminWorkspace({ session, onLogout }: { session: SessionInfo; onLogout:
</>
:
tenantsQuery
.
isLoading
?
<
Splash
/>
:
<
div
className=
"admin-empty"
><
ShieldCheck
size=
{
30
}
/><
h2
>
暂无租户
</
h2
><
button
type=
"button"
onClick=
{
()
=>
setShowCreate
(
true
)
}
>
创建第一个租户
</
button
></
div
>
}
</
section
>
</
div
>
{
passwordTarget
&&
<
AppDialog
title=
"重置账号密码"
description=
{
`为账号“${passwordTarget.username}”设置新密码。保存后,该账号当前的全部登录会话将立即失效。`
}
confirmLabel=
"重置密码"
busy=
{
passwordMutation
.
isPending
}
confirmDisabled=
{
newPassword
.
length
<
8
}
error=
{
passwordMutation
.
error
?.
message
}
icon=
{
<
ShieldCheck
size=
{
20
}
/>
}
onCancel=
{
()
=>
{
setPasswordTarget
(
undefined
);
setNewPassword
(
""
);
}
}
onConfirm=
{
()
=>
passwordMutation
.
mutate
({
tenantId
:
passwordTarget
.
tenantId
,
userId
:
passwordTarget
.
userId
,
password
:
newPassword
})
}
input=
{
{
label
:
"新密码(至少 8 个字符)"
,
value
:
newPassword
,
type
:
"password"
,
placeholder
:
"请输入新密码"
,
onChange
:
setNewPassword
}
}
/>
}
{
revokeTarget
&&
<
AppDialog
title=
"注销全部登录会话?"
description=
{
`账号“${revokeTarget.username}”需要重新登录后才能继续使用,当前密码不会改变。`
}
confirmLabel=
"确认注销"
busy=
{
revokeMutation
.
isPending
}
error=
{
revokeMutation
.
error
?.
message
}
tone=
"danger"
icon=
{
<
ShieldCheck
size=
{
20
}
/>
}
onCancel=
{
()
=>
setRevokeTarget
(
undefined
)
}
onConfirm=
{
()
=>
revokeMutation
.
mutate
({
tenantId
:
revokeTarget
.
tenantId
,
userId
:
revokeTarget
.
userId
})
}
/>
}
{
adminNotice
&&
<
AppDialog
title=
"操作完成"
description=
{
adminNotice
}
confirmLabel=
"知道了"
tone=
"success"
icon=
{
<
CheckCircle2
size=
{
20
}
/>
}
onConfirm=
{
()
=>
setAdminNotice
(
""
)
}
/>
}
</
main
>;
}
...
...
@@ -410,12 +412,12 @@ function SiteManagementWorkspace({ sites, currentSiteId, onEdit, onCreate, onBac
] as Array<[SiteManagementFilter, string]>).map(([value, label]) => <button type="button" className={filter === value ? "active" : ""} key={value} onClick={() => setFilter(value)}>{label}<span>{counts[value]}</span></button>)}</nav>
<div className="site-management-tools"><label><Search size={16} /><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="搜索网站名称、行业或 ID" /></label><span>{filter === "archived" ? "回收站内容保留 30 天" : "按最近更新时间排序"}</span></div>
<div className="managed-site-list">
{visible.map((site) => filter === "archived" ? <ArchivedSiteCard key={site.siteId} site={site} busy={restoreMutation.isPending || deleteMutation.isPending} onRestore={() => restoreMutation.mutate(site.siteId)} onDelete={() => { setDeleteTarget(site); setDeleteConfirmation(""); }} /> : <ManagedSiteCard key={site.siteId} site={site} selected={site.siteId === currentSiteId} busy={archiveMutation.isPending} onEdit={() => onEdit(site)} onArchive={() => setArchiveTarget(site)} />)}
{visible.map((site) => filter === "archived" ? <ArchivedSiteCard key={site.siteId} site={site} busy={restoreMutation.isPending || deleteMutation.isPending} onRestore={() => restoreMutation.mutate(site.siteId)} onDelete={() => {
deleteMutation.reset();
setDeleteTarget(site); setDeleteConfirmation(""); }} /> : <ManagedSiteCard key={site.siteId} site={site} selected={site.siteId === currentSiteId} busy={archiveMutation.isPending} onEdit={() => onEdit(site)} onArchive={() => setArchiveTarget(site)} />)}
{!visible.length && <div className="managed-sites-empty">{filter === "archived" ? <Archive size={28} /> : <LayoutGrid size={28} />}<h2>{search ? "没有匹配的网站" : filter === "archived" ? "回收站是空的" : "这个分类下暂无网站"}</h2><p>{search ? "换一个关键词试试。" : filter === "archived" ? "移入回收站的网站会显示在这里。" : "可以切换筛选条件或创建一个新网站。"}</p></div>}
</div>
</section>
{archiveTarget && <ConfirmDialog title="移入回收站?" description={`
“
$
{
archiveTarget
.
name
}
”将停止编辑和发布,测试与线上访问会下线,数据保留
30
天。
`} confirmLabel="移入回收站" busy={archiveMutation.isPending} onCancel={() => setArchiveTarget(undefined)} onConfirm={() => archiveMutation.mutate(archiveTarget.siteId)} />}
{deleteTarget && <
div className="confirm-backdrop" role="presentation"><section className="confirm-dialog" role="dialog" aria-modal="true" aria-labelledby="delete-site-title"><span className="confirm-icon danger"><Trash2 size={20} /></span><h2 id="delete-site-title">永久删除网站?</h2><p>网站代码、版本历史、草稿和构建文件都会被删除,此操作无法恢复。</p><label>请输入 <strong>{deleteTarget.name}</strong> 确认<input autoFocus value={deleteConfirmation} onChange={(event) => setDeleteConfirmation(event.target.value)} /></label><div><button type="button" onClick={() => { setDeleteTarget(undefined); setDeleteConfirmation(""); }}>取消</button><button className="danger" type="button" disabled={deleteConfirmation !== deleteTarget.name || deleteMutation.isPending} onClick={() => deleteMutation.mutate(deleteTarget.siteId)}>{deleteMutation.isPending && <LoaderCircle className="spin" size={14} />}永久删除</button></div></section></div
>}
{deleteTarget && <
AppDialog title="永久删除网站?" description={`
网站代码、版本历史、草稿和构建文件都会被删除。请输入“
$
{
deleteTarget
.
name
}
”确认,此操作无法恢复。
`} confirmLabel="永久删除" busy={deleteMutation.isPending} confirmDisabled={deleteConfirmation !== deleteTarget.name} error={deleteMutation.error?.message} tone="danger" icon={<Trash2 size={20} />} input={{ label: "网站名称", value: deleteConfirmation, placeholder: deleteTarget.name, onChange: setDeleteConfirmation }} onCancel={() => { setDeleteTarget(undefined); setDeleteConfirmation(""); }} onConfirm={() => deleteMutation.mutate(deleteTarget.siteId)} /
>}
</main>;
}
...
...
@@ -428,10 +430,40 @@ function ArchivedSiteCard({ site, busy, onRestore, onDelete }: { site: SiteInfo;
return <article className="managed-site-card archived"><span className="managed-site-avatar"><Archive size={18} /></span><div className="managed-site-main"><div><strong>{site.name}</strong><span className="archived">回收站</span></div><small>{site.industry} · 移入于 {formatDate(site.archivedAt || site.updatedAt)}</small><div className="managed-site-states"><span>自动清理时间:{site.deleteAfter ? formatDate(site.deleteAfter) : "30 天后"}</span><span><code>{site.siteId}</code></span></div></div><div className="managed-site-actions"><button className="restore" type="button" disabled={busy} onClick={onRestore}><RotateCcw size={13} />恢复网站</button><button className="delete" type="button" disabled={busy} onClick={onDelete}><Trash2 size={13} />永久删除</button></div></article>;
}
type AppDialogProps = {
title: string;
description: string;
confirmLabel: string;
busy?: boolean;
confirmDisabled?: boolean;
error?: string;
tone?: "default" | "danger" | "success";
icon?: React.ReactNode;
input?: { label: string; value: string; type?: string; placeholder?: string; onChange: (value: string) => void };
onCancel?: () => void;
onConfirm: () => void;
};
function AppDialog({ title, description, confirmLabel, busy = false, confirmDisabled = false, error, tone = "default", icon, input, onCancel, onConfirm }: AppDialogProps) {
return <div className="confirm-backdrop" role="presentation"><section className={`
confirm
-
dialog
$
{
tone
}
`} role="dialog" aria-modal="true" aria-labelledby="app-dialog-title">
<span className={`
confirm
-
icon
$
{
tone
}
`}>{icon || (tone === "success" ? <CheckCircle2 size={20} /> : <Archive size={20} />)}</span>
<h2 id="app-dialog-title">{title}</h2><p>{description}</p>
{input && <label>{input.label}<input autoFocus type={input.type || "text"} value={input.value} placeholder={input.placeholder} onChange={(event) => input.onChange(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter" && !busy && !confirmDisabled) onConfirm(); }} /></label>}
{error && <div className="dialog-error"><X size={13} />{error}</div>}
<div>{onCancel && <button type="button" disabled={busy} onClick={onCancel}>取消</button>}<button className={tone === "danger" ? "danger" : "primary"} type="button" disabled={busy || confirmDisabled} onClick={onConfirm}>{busy && <LoaderCircle className="spin" size={14} />}{confirmLabel}</button></div>
</section></div>;
}
function ConfirmDialog({ title, description, confirmLabel, busy, onCancel, onConfirm }: { title: string; description: string; confirmLabel: string; busy: boolean; onCancel: () => void; onConfirm: () => void }) {
return <
div className="confirm-backdrop" role="presentation"><section className="confirm-dialog" role="dialog" aria-modal="true" aria-labelledby="confirm-title"><span className="confirm-icon"><Archive size={20} /></span><h2 id="confirm-title">{title}</h2><p>{description}</p><div><button type="button" onClick={onCancel}>取消</button><button className="danger" type="button" disabled={busy} onClick={onConfirm}>{busy && <LoaderCircle className="spin" size={14} />}{confirmLabel}</button></div></section></div
>;
return <
AppDialog title={title} description={description} confirmLabel={confirmLabel} busy={busy} tone="danger" onCancel={onCancel} onConfirm={onConfirm} /
>;
}
type WorkspaceDialog =
| { kind: "discard" }
| { kind: "restore"; version: GitHistoryItem }
| { kind: "publish" }
| { kind: "publish-blocked"; title: string; description: string; goToVersions: boolean };
function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage, onLogout }: { siteId: string; sites: SiteInfo[]; agentMode: "model" | "local"; onSelectSite: (id: string) => void; onCreate: () => void; onManage: () => void; onLogout: () => void }) {
const queryClient = useQueryClient();
const siteQuery = useQuery({ queryKey: ["site", siteId], queryFn: () => api.site(siteId) });
...
...
@@ -444,6 +476,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
const [versionMessage, setVersionMessage] = useState("");
const [activeRunId, setActiveRunId] = useState("");
const [agentProgress, setAgentProgress] = useState("");
const [workspaceDialog, setWorkspaceDialog] = useState<WorkspaceDialog>();
const [iframeVersion, setIframeVersion] = useState(0);
const [messages, setMessages] = useState<ChatMessage[]>([
{ role: "agent", text: "网站已准备好。多轮修改会累积为未保存草稿,并自动更新测试预览;你可以在“版本”中决定何时保存。", meta: agentMode === "model" ? "模型 Agent 已连接" : "本地演示 Agent" },
...
...
@@ -461,6 +494,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
useEffect(() => {
setVersionMessage("");
versionMessageKeyRef.current = "";
setWorkspaceDialog(undefined);
}, [siteId]);
useEffect(() => {
if (!site?.draftBaseCommit) {
...
...
@@ -580,6 +614,21 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
const
environmentStatus
=
showingProduction
?
site
.
lastPublishError
?
"上线失败 · 线上未变"
:
!
site
.
publishedCommit
?
"尚未上线"
:
productionMatchesPreview
?
"生产已同步"
:
"生产版本较旧"
:
site
.
lastError
?
"构建失败 · 预览未变"
:
site
.
draftBaseCommit
?
"未保存草稿预览"
:
site
.
status
===
"ready"
?
"测试已同步"
:
"需要检查"
;
const
requestPublish
=
()
=>
{
if
(
site
.
draftBaseCommit
)
{
setWorkspaceDialog
({
kind
:
"publish-blocked"
,
title
:
"暂时不能上线"
,
description
:
"请先进入左侧“版本”,保存或放弃未保存修改后,才能上线。"
,
goToVersions
:
true
});
return
;
}
if
(
site
.
status
!==
"ready"
||
!
site
.
previewCommit
)
{
const
description
=
site
.
lastError
?
"测试环境构建失败。请先修复问题并重新构建成功后,才能上线。"
:
"请先等待测试环境构建成功,确认预览可用后,才能上线。"
;
setWorkspaceDialog
({
kind
:
"publish-blocked"
,
title
:
"暂时不能上线"
,
description
,
goToVersions
:
false
});
return
;
}
publishMutation
.
reset
();
setWorkspaceDialog
({
kind
:
"publish"
});
};
return
<
main
className=
{
`workspace ${tab === "domains" ? "domains-mode" : ""}`
}
>
<
aside
className=
"rail"
>
<
Logo
compact
/>
...
...
@@ -611,7 +660,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
<
div
className=
"draft-card-heading"
><
span
><
Sparkles
size=
{
13
}
/></
span
><
div
><
strong
>
未保存修改
</
strong
><
small
>
更新于
{
site
.
draftUpdatedAt
?
formatTime
(
site
.
draftUpdatedAt
)
:
"刚刚"
}
· 基于
{
site
.
draftBaseCommit
.
slice
(
0
,
7
)
}
</
small
></
div
></
div
>
<
p
className=
"draft-card-summary"
>
{
site
.
draftSummary
||
"多轮 Agent 修改正在此处累积,尚未写入 Git 历史。"
}
</
p
>
<
label
className=
"version-message-field"
><
span
>
版本说明
</
span
><
input
value=
{
versionMessage
}
maxLength=
{
120
}
placeholder=
"例如:首页内容与品牌视觉优化"
onChange=
{
(
event
)
=>
setVersionMessage
(
event
.
target
.
value
)
}
/></
label
>
<
div
className=
"draft-card-actions primary"
><
button
type=
"button"
disabled=
{
busy
||
versionMessage
.
trim
().
length
<
2
}
onClick=
{
()
=>
saveDraftMutation
.
mutate
()
}
>
{
saveDraftMutation
.
isPending
?
<
LoaderCircle
className=
"spin"
size=
{
11
}
/>
:
<
CheckCircle2
size=
{
11
}
/>
}{
saveDraftMutation
.
isPending
?
"保存中"
:
"保存为新版本"
}
</
button
><
button
type=
"button"
disabled=
{
busy
}
onClick=
{
()
=>
{
if
(
window
.
confirm
(
"确定放弃全部未保存修改吗?此操作不能撤销。"
))
discardDraftMutation
.
mutate
(
);
}
}
><
Trash2
size=
{
11
}
/>
放弃修改
</
button
></
div
>
<
div
className=
"draft-card-actions primary"
><
button
type=
"button"
disabled=
{
busy
||
versionMessage
.
trim
().
length
<
2
}
onClick=
{
()
=>
saveDraftMutation
.
mutate
()
}
>
{
saveDraftMutation
.
isPending
?
<
LoaderCircle
className=
"spin"
size=
{
11
}
/>
:
<
CheckCircle2
size=
{
11
}
/>
}{
saveDraftMutation
.
isPending
?
"保存中"
:
"保存为新版本"
}
</
button
><
button
type=
"button"
disabled=
{
busy
}
onClick=
{
()
=>
{
discardDraftMutation
.
reset
();
setWorkspaceDialog
({
kind
:
"discard"
}
);
}
}
><
Trash2
size=
{
11
}
/>
放弃修改
</
button
></
div
>
<
div
className=
"draft-card-actions secondary"
><
button
type=
"button"
onClick=
{
()
=>
setTab
(
"chat"
)
}
><
MessageSquareText
size=
{
11
}
/>
继续编辑
</
button
><
button
type=
"button"
disabled=
{
busy
}
onClick=
{
()
=>
previewDraftMutation
.
mutate
()
}
>
{
previewDraftMutation
.
isPending
?
<
LoaderCircle
className=
"spin"
size=
{
11
}
/>
:
<
Monitor
size=
{
11
}
/>
}{
previewDraftMutation
.
isPending
?
"构建中"
:
"重新构建预览"
}
</
button
></
div
>
</
section
>
}
{
historicalPreview
&&
<
div
className=
"history-edit-lock history-panel-lock"
><
div
><
History
size=
{
14
}
/><
span
>
{
site
.
draftBaseCommit
?
"正在查看旧版本,未保存修改仍保留"
:
"当前只是在预览历史版本;可恢复为新的当前版本"
}
</
span
></
div
></
div
>
}
...
...
@@ -626,7 +675,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
<
span
className=
"commit-info"
><
span
><
strong
>
{
item
.
message
}
</
strong
>
{
isPreview
&&
<
em
className=
"preview-badge"
>
当前查看
</
em
>
}{
isProduction
&&
<
em
className=
"production-badge"
>
线上版本
</
em
>
}{
isCurrent
&&
<
em
className=
"current-badge"
>
最近保存
</
em
>
}
</
span
><
small
><
code
>
{
item
.
shortHash
}
</
code
><
span
>
·
</
span
>
{
formatDate
(
item
.
date
)
}
</
small
></
span
>
<
span
className=
"history-version-actions"
>
{
isPreview
?
<
span
className=
"history-row-state"
>
正在查看
</
span
>
:
<
button
className=
"history-preview-button"
type=
"button"
disabled=
{
busy
}
onClick=
{
()
=>
{
setEnvironment
(
"preview"
);
previewVersionMutation
.
mutate
(
item
.
hash
);
}
}
>
{
selecting
?
<
LoaderCircle
className=
"spin"
size=
{
10
}
/>
:
<
Monitor
size=
{
11
}
/>
}{
selecting
?
"切换中"
:
"预览"
}
</
button
>
}
{
!
isCurrent
&&
<
button
className=
"history-restore-button"
type=
"button"
disabled=
{
busy
||
Boolean
(
site
.
draftBaseCommit
)
}
onClick=
{
()
=>
{
if
(
window
.
confirm
(
`确定恢复版本 ${item.shortHash} 吗?系统会创建一个新的恢复版本。`
))
restoreVersionMutation
.
mutate
(
item
.
hash
);
}
}
><
RotateCcw
size=
{
11
}
/>
恢复
</
button
>
}
{
!
isCurrent
&&
<
button
className=
"history-restore-button"
type=
"button"
disabled=
{
busy
||
Boolean
(
site
.
draftBaseCommit
)
}
onClick=
{
()
=>
{
restoreVersionMutation
.
reset
();
setWorkspaceDialog
({
kind
:
"restore"
,
version
:
item
}
);
}
}
><
RotateCcw
size=
{
11
}
/>
恢复
</
button
>
}
</
span
>
</
div
>
</
article
>;
...
...
@@ -644,7 +693,7 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
{
!
showingProduction
&&
<
button
className=
"rebuild-button"
disabled=
{
busy
}
onClick=
{
()
=>
rebuildMutation
.
mutate
()
}
title=
"重新构建测试环境"
><
RefreshCw
className=
{
rebuildMutation
.
isPending
?
"spin"
:
""
}
size=
{
14
}
/><
span
>
重新构建
</
span
></
button
>
}
<
a
className=
"environment-link preview-link"
href=
{
site
.
previewUrl
}
target=
"_blank"
rel=
"noreferrer"
title=
"打开测试环境"
><
Monitor
size=
{
13
}
/><
span
>
测试链接
</
span
></
a
>
<
a
className=
"environment-link production-link"
href=
{
site
.
productionUrl
}
target=
"_blank"
rel=
"noreferrer"
title=
"打开生产环境"
><
Globe2
size=
{
13
}
/><
span
>
生产链接
</
span
></
a
>
{
!
showingProduction
&&
<
button
className=
"publish-button"
disabled=
{
busy
||
!
canPublish
}
onClick=
{
()
=>
{
if
(
window
.
confirm
(
productionMatchesPreview
?
"确定重新发布当前版本吗?这会重新构建并更新生产产物。"
:
"确定将当前测试预览版本发布到生产环境吗?"
))
publishMutation
.
mutate
();
}
}
title=
{
site
.
status
!==
"ready"
?
"测试环境构建成功后才能发布"
:
productionMatchesPreview
?
"重新构建并发布当前版本"
:
"发布当前测试预览版本到生产环境"
}
>
{
!
showingProduction
&&
<
button
className=
{
`publish-button ${!canPublish ? "unavailable" : ""}`
}
aria
-
disabled=
{
!
canPublish
||
busy
}
disabled=
{
busy
}
onClick=
{
requestPublish
}
title=
{
site
.
draftBaseCommit
?
"请先保存或放弃未保存修改"
:
site
.
status
!==
"ready"
?
"测试环境构建成功后才能发布"
:
productionMatchesPreview
?
"重新构建并发布当前版本"
:
"发布当前测试预览版本到生产环境"
}
>
{
publishMutation
.
isPending
?
<
LoaderCircle
className=
"spin"
size=
{
14
}
/>
:
<
Rocket
size=
{
14
}
/>
}
<
span
>
{
productionMatchesPreview
?
"重新上线"
:
"上线当前版本"
}
</
span
>
</
button
>
}
</
div
>
...
...
@@ -660,6 +709,10 @@ function Workspace({ siteId, sites, agentMode, onSelectSite, onCreate, onManage,
<
span
>
{
showingProduction
?
<
Globe2
size=
{
12
}
/>
:
<
Monitor
size=
{
12
}
/>
}
当前查看 ·
{
showingProduction
?
`生产版本 ${productionVersion?.shortHash || site.publishedCommit?.slice(0, 7) || "尚未上线"}`
:
site
.
draftBaseCommit
?
`未保存草稿 · 基于 ${site.draftBaseCommit.slice(0, 7)}`
:
`${historicalPreview ? "历史版本" : "测试版本"} ${previewVersion?.shortHash || site.currentCommit.slice(0, 7)}`
}
·
{
showingProduction
?
site
.
publishedAt
?
formatDate
(
site
.
publishedAt
)
:
"尚未发布"
:
site
.
draftBaseCommit
?
site
.
draftUpdatedAt
?
formatDate
(
site
.
draftUpdatedAt
)
:
"刚刚"
:
previewVersion
?
formatDate
(
previewVersion
.
date
)
:
formatDate
(
site
.
updatedAt
)
}{
historicalPreview
&&
!
showingProduction
?
" · 只读"
:
""
}
</
span
>
<
span
><
Clock3
size=
{
12
}
/>
{
site
.
publishedAt
?
`线上版本 ${productionVersion?.shortHash || site.publishedCommit?.slice(0, 7)} · ${formatDate(site.publishedAt)}`
:
"生产环境尚未发布"
}
</
span
>
</
footer
>
{
workspaceDialog
?.
kind
===
"discard"
&&
<
AppDialog
title=
"放弃未保存修改?"
description=
"全部工作草稿都会被删除,并恢复到最近保存的版本。此操作不能撤销。"
confirmLabel=
"放弃修改"
busy=
{
discardDraftMutation
.
isPending
}
error=
{
discardDraftMutation
.
error
?.
message
}
tone=
"danger"
icon=
{
<
Trash2
size=
{
20
}
/>
}
onCancel=
{
()
=>
setWorkspaceDialog
(
undefined
)
}
onConfirm=
{
()
=>
discardDraftMutation
.
mutate
(
undefined
,
{
onSuccess
:
()
=>
setWorkspaceDialog
(
undefined
)
})
}
/>
}
{
workspaceDialog
?.
kind
===
"restore"
&&
<
AppDialog
title=
{
`恢复版本 ${workspaceDialog.version.shortHash}?`
}
description=
{
`将恢复“${workspaceDialog.version.message}”,并创建一个新的恢复版本;已有历史不会删除。`
}
confirmLabel=
"确认恢复"
busy=
{
restoreVersionMutation
.
isPending
}
error=
{
restoreVersionMutation
.
error
?.
message
}
icon=
{
<
RotateCcw
size=
{
20
}
/>
}
onCancel=
{
()
=>
setWorkspaceDialog
(
undefined
)
}
onConfirm=
{
()
=>
restoreVersionMutation
.
mutate
(
workspaceDialog
.
version
.
hash
,
{
onSuccess
:
()
=>
setWorkspaceDialog
(
undefined
)
})
}
/>
}
{
workspaceDialog
?.
kind
===
"publish"
&&
<
AppDialog
title=
{
productionMatchesPreview
?
"重新上线当前版本?"
:
"上线当前版本?"
}
description=
{
productionMatchesPreview
?
"系统会重新构建当前保存版本并更新生产环境。"
:
"系统会构建当前保存版本,并更新生产环境和已连接的自定义域名。"
}
confirmLabel=
{
productionMatchesPreview
?
"重新上线"
:
"确认上线"
}
busy=
{
publishMutation
.
isPending
}
error=
{
publishMutation
.
error
?.
message
}
icon=
{
<
Rocket
size=
{
20
}
/>
}
onCancel=
{
()
=>
setWorkspaceDialog
(
undefined
)
}
onConfirm=
{
()
=>
publishMutation
.
mutate
(
undefined
,
{
onSuccess
:
()
=>
setWorkspaceDialog
(
undefined
)
})
}
/>
}
{
workspaceDialog
?.
kind
===
"publish-blocked"
&&
<
AppDialog
title=
{
workspaceDialog
.
title
}
description=
{
workspaceDialog
.
description
}
confirmLabel=
{
workspaceDialog
.
goToVersions
?
"前往版本"
:
"知道了"
}
icon=
{
<
Rocket
size=
{
20
}
/>
}
onCancel=
{
()
=>
setWorkspaceDialog
(
undefined
)
}
onConfirm=
{
()
=>
{
if
(
workspaceDialog
.
goToVersions
)
setTab
(
"history"
);
setWorkspaceDialog
(
undefined
);
}
}
/>
}
</
section
>
}
</
main
>;
}
...
...
@@ -683,6 +736,7 @@ function DomainManagement({ site }: { site: SiteInfo }) {
const
[
copied
,
setCopied
]
=
useState
(
""
);
const
[
selectedDomainId
,
setSelectedDomainId
]
=
useState
(
""
);
const
[
addingNew
,
setAddingNew
]
=
useState
(
false
);
const
[
removeTarget
,
setRemoveTarget
]
=
useState
<
DomainBinding
>
();
const
domainsQuery
=
useQuery
({
queryKey
:
[
"domains"
,
site
.
siteId
],
queryFn
:
()
=>
api
.
domains
(
site
.
siteId
),
refetchInterval
:
15000
});
const
refresh
=
()
=>
queryClient
.
invalidateQueries
({
queryKey
:
[
"domains"
,
site
.
siteId
]
});
const
addMutation
=
useMutation
({
...
...
@@ -691,7 +745,7 @@ function DomainManagement({ site }: { site: SiteInfo }) {
});
const
verifyMutation
=
useMutation
({
mutationFn
:
(
domainId
:
string
)
=>
api
.
verifyDomain
(
site
.
siteId
,
domainId
),
onSuccess
:
refresh
});
const
primaryMutation
=
useMutation
({
mutationFn
:
(
domainId
:
string
)
=>
api
.
setPrimaryDomain
(
site
.
siteId
,
domainId
),
onSuccess
:
refresh
});
const
removeMutation
=
useMutation
({
mutationFn
:
(
domainId
:
string
)
=>
api
.
removeDomain
(
site
.
siteId
,
domainId
),
onSuccess
:
refresh
});
const
removeMutation
=
useMutation
({
mutationFn
:
(
domainId
:
string
)
=>
api
.
removeDomain
(
site
.
siteId
,
domainId
),
onSuccess
:
async
()
=>
{
setRemoveTarget
(
undefined
);
await
refresh
();
}
});
const
copy
=
async
(
key
:
string
,
value
:
string
)
=>
{
await
navigator
.
clipboard
.
writeText
(
value
);
setCopied
(
key
);
...
...
@@ -705,7 +759,7 @@ function DomainManagement({ site }: { site: SiteInfo }) {
}
},
[
domains
,
selectedDomainId
]);
useEffect
(()
=>
{
setHostname
(
""
);
setSelectedDomainId
(
""
);
setAddingNew
(
false
);
setHostname
(
""
);
setSelectedDomainId
(
""
);
setAddingNew
(
false
);
setRemoveTarget
(
undefined
);
},
[
site
.
siteId
]);
const
selectedDomain
=
domains
.
find
((
domain
)
=>
domain
.
domainId
===
selectedDomainId
);
...
...
@@ -762,10 +816,11 @@ function DomainManagement({ site }: { site: SiteInfo }) {
{
wizardStep
===
4
&&
selectedDomain
&&
<
div
className=
"wizard-body wizard-success"
><
span
><
CheckCircle2
size=
{
30
}
/></
span
><
h3
>
域名已经连接成功
</
h3
><
p
>
现在可以使用
<
strong
>
{
selectedDomain
.
hostname
}
</
strong
>
访问官网。以后发布新版本时,这个域名会自动同步。
</
p
><
div
><
a
href=
{
selectedDomain
.
url
}
target=
"_blank"
rel=
"noreferrer"
>
打开网站测试
<
ExternalLink
size=
{
14
}
/></
a
>
{
!
selectedDomain
.
isPrimary
&&
<
button
type=
"button"
disabled=
{
busy
}
onClick=
{
()
=>
primaryMutation
.
mutate
(
selectedDomain
.
domainId
)
}
>
设为主域名
</
button
>
}
<
button
type=
"button"
onClick=
{
()
=>
{
setAddingNew
(
true
);
setHostname
(
""
);
}
}
><
Plus
size=
{
14
}
/>
再连接一个域名
</
button
></
div
><
small
>
测试时请确认页面能打开,并且浏览器地址栏显示安全锁标志。
</
small
></
div
>
}
</
section
>
{
domains
.
length
>
0
&&
<
section
className=
"domain-overview"
><
div
className=
"domain-section-heading"
><
div
><
h2
>
我的域名
</
h2
><
p
>
选择域名可以继续配置、查看状态或测试访问。
</
p
></
div
><
span
>
{
domains
.
length
}
</
span
></
div
><
div
>
{
domains
.
map
((
domain
)
=>
<
DomainOverviewRow
key=
{
domain
.
domainId
}
domain=
{
domain
}
selected=
{
domain
.
domainId
===
selectedDomainId
&&
!
addingNew
}
busy=
{
busy
}
onSelect=
{
()
=>
{
setSelectedDomainId
(
domain
.
domainId
);
setAddingNew
(
false
);
}
}
onPrimary=
{
()
=>
primaryMutation
.
mutate
(
domain
.
domainId
)
}
onRemove=
{
()
=>
{
if
(
window
.
confirm
(
`确定解除 ${domain.hostname} 的绑定吗?`
))
removeMutation
.
mutate
(
domain
.
domainId
);
}
}
/>)
}
</
div
></
section
>
}
{
domains
.
length
>
0
&&
<
section
className=
"domain-overview"
><
div
className=
"domain-section-heading"
><
div
><
h2
>
我的域名
</
h2
><
p
>
选择域名可以继续配置、查看状态或测试访问。
</
p
></
div
><
span
>
{
domains
.
length
}
</
span
></
div
><
div
>
{
domains
.
map
((
domain
)
=>
<
DomainOverviewRow
key=
{
domain
.
domainId
}
domain=
{
domain
}
selected=
{
domain
.
domainId
===
selectedDomainId
&&
!
addingNew
}
busy=
{
busy
}
onSelect=
{
()
=>
{
setSelectedDomainId
(
domain
.
domainId
);
setAddingNew
(
false
);
}
}
onPrimary=
{
()
=>
primaryMutation
.
mutate
(
domain
.
domainId
)
}
onRemove=
{
()
=>
{
removeMutation
.
reset
();
setRemoveTarget
(
domain
);
}
}
/>)
}
</
div
></
section
>
}
<
ProviderGuides
/>
</
div
>
{
removeTarget
&&
<
AppDialog
title=
"解除域名绑定?"
description=
{
`解除 ${removeTarget.hostname} 后,该域名将停止访问当前官网,系统默认生产链接不受影响。`
}
confirmLabel=
"解除绑定"
busy=
{
removeMutation
.
isPending
}
error=
{
removeMutation
.
error
?.
message
}
tone=
"danger"
icon=
{
<
Globe2
size=
{
20
}
/>
}
onCancel=
{
()
=>
setRemoveTarget
(
undefined
)
}
onConfirm=
{
()
=>
removeMutation
.
mutate
(
removeTarget
.
domainId
)
}
/>
}
</
section
>;
}
...
...
frontend/src/styles.css
View file @
33d48dd2
...
...
@@ -75,6 +75,7 @@
.site-management-tools
{
max-width
:
1180px
;
margin
:
17px
auto
13px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
gap
:
18px
}
.site-management-tools
label
{
width
:
min
(
360px
,
100%
);
height
:
38px
;
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
padding
:
0
11px
;
color
:
#9b98a3
;
background
:
#fff
;
border
:
1px
solid
#e4e1e9
;
border-radius
:
9px
}
.site-management-tools
label
:focus-within
{
color
:
var
(
--primary
);
border-color
:
#c7b6fb
;
box-shadow
:
0
0
0
3px
rgba
(
112
,
40
,
255
,
.06
)}
.site-management-tools
input
{
width
:
100%
;
height
:
100%
;
padding
:
0
;
background
:
transparent
;
border
:
0
;
outline
:
0
;
font-size
:
10px
}
.site-management-tools
>
span
{
color
:
#9a97a2
;
font-size
:
9px
}
.managed-site-list
{
max-width
:
1180px
;
margin
:
auto
;
display
:
grid
;
gap
:
10px
}
.managed-site-card
{
min-width
:
0
;
min-height
:
108px
;
padding
:
17px
;
display
:
grid
;
grid-template-columns
:
46px
minmax
(
260px
,
1
fr
)
auto
;
align-items
:
center
;
gap
:
14px
;
background
:
#fff
;
border
:
1px
solid
#e7e4eb
;
border-radius
:
14px
;
box-shadow
:
0
7px
24px
rgba
(
41
,
31
,
69
,
.035
);
transition
:
.15s
ease
}
.managed-site-card
:hover
{
border-color
:
#d9d2e7
;
box-shadow
:
0
10px
30px
rgba
(
41
,
31
,
69
,
.065
)}
.managed-site-card.selected
{
border-color
:
#d2c3ff
;
box-shadow
:
0
0
0
3px
rgba
(
112
,
40
,
255
,
.045
)}
.managed-site-avatar
{
width
:
46px
;
height
:
46px
;
display
:
grid
;
place-items
:
center
;
color
:
#6739cd
;
background
:
linear-gradient
(
145deg
,
#f1edff
,
#e7e1fa
);
border-radius
:
12px
;
font-size
:
15px
;
font-weight
:
850
}
.managed-site-card.archived
.managed-site-avatar
{
color
:
#77727f
;
background
:
#efedf2
}
.managed-site-main
{
min-width
:
0
}
.managed-site-main
>
div
:first-child
{
display
:
flex
;
align-items
:
center
;
gap
:
7px
}
.managed-site-main
strong
{
font-size
:
13px
}
.managed-site-main
em
,
.managed-site-main
>
div
:first-child
>
span
{
padding
:
3px
6px
;
border-radius
:
5px
;
font-size
:
8px
;
font-style
:
normal
;
font-weight
:
750
}
.managed-site-main
em
{
color
:
var
(
--primary
);
background
:
var
(
--soft
)}
.managed-site-main
>
div
:first-child
>
span
.published
{
color
:
#177457
;
background
:
#e6f7f0
}
.managed-site-main
>
div
:first-child
>
span
.unpublished
{
color
:
#77727f
;
background
:
#efedf2
}
.managed-site-main
>
div
:first-child
>
span
.failed
{
color
:
#b23e3e
;
background
:
#fff0f0
}
.managed-site-main
>
div
:first-child
>
span
.archived
{
color
:
#68636e
;
background
:
#efedf2
}
.managed-site-main
>
small
{
display
:
block
;
margin-top
:
6px
;
color
:
#96929d
;
font-size
:
9px
}
.managed-site-states
{
display
:
flex
;
align-items
:
center
;
gap
:
15px
;
margin-top
:
11px
;
color
:
#706d79
;
font-size
:
8px
}
.managed-site-states
>
span
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
}
.managed-site-states
i
{
width
:
6px
;
height
:
6px
;
border-radius
:
50%
;
background
:
#aaa6b0
}
.managed-site-states
i
.ok
{
background
:
#25a777
}
.managed-site-states
i
.building
,
.managed-site-states
i
.creating
,
.managed-site-states
i
.publishing
{
background
:
#d89b20
}
.managed-site-states
i
.failed
{
background
:
#d55454
}
.managed-site-states
span
.draft
{
color
:
#7250bf
}
.managed-site-states
code
{
font-size
:
8px
}
.managed-site-actions
{
display
:
flex
;
align-items
:
center
;
justify-content
:
flex-end
;
gap
:
6px
}
.managed-site-actions
button
,
.managed-site-actions
a
{
height
:
32px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
5px
;
padding
:
0
10px
;
color
:
#625e6b
;
background
:
#fff
;
border
:
1px
solid
#dfdbe5
;
border-radius
:
8px
;
font-size
:
8px
;
font-weight
:
750
;
text-decoration
:
none
;
white-space
:
nowrap
}
.managed-site-actions
button
:hover
,
.managed-site-actions
a
:hover
{
color
:
var
(
--primary
);
background
:
#faf9ff
;
border-color
:
#cfc1fa
}
.managed-site-actions
button
.primary
{
color
:
white
;
background
:
var
(
--gradient
);
border
:
0
}
.managed-site-actions
button
.archive
{
width
:
32px
;
padding
:
0
;
color
:
#8b6570
}
.managed-site-actions
button
.delete
{
color
:
#b13c3c
}
.managed-site-actions
button
.restore
{
color
:
#1a755a
}
.managed-sites-empty
{
min-height
:
310px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-direction
:
column
;
color
:
#aaa6b1
;
background
:
#fff
;
border
:
1px
dashed
#dedbe3
;
border-radius
:
14px
}
.managed-sites-empty
h2
{
margin
:
14px
0
5px
;
color
:
#55515e
;
font-size
:
15px
}
.managed-sites-empty
p
{
margin
:
0
;
font-size
:
10px
}
.confirm-backdrop
{
position
:
fixed
;
z-index
:
100
;
inset
:
0
;
display
:
grid
;
place-items
:
center
;
padding
:
20px
;
background
:
rgba
(
27
,
22
,
39
,
.36
);
backdrop-filter
:
blur
(
4px
)}
.confirm-dialog
{
width
:
min
(
430px
,
100%
);
padding
:
27px
;
background
:
#fff
;
border
:
1px
solid
#e4dfeb
;
border-radius
:
18px
;
box-shadow
:
0
24px
75px
rgba
(
24
,
14
,
50
,
.25
)}
.confirm-icon
{
display
:
grid
;
place-items
:
center
;
width
:
42px
;
height
:
42px
;
color
:
#9b5d19
;
background
:
#fff5df
;
border-radius
:
12px
}
.confirm-icon.danger
{
color
:
#b43d3d
;
background
:
#fff0f0
}
.confirm-dialog
h2
{
margin
:
17px
0
8px
;
font-size
:
19px
}
.confirm-dialog
p
{
margin
:
0
;
color
:
#77737f
;
font-size
:
11px
;
line-height
:
1.75
}
.confirm-dialog
>
label
{
display
:
block
;
margin-top
:
18px
;
color
:
#686471
;
font-size
:
10px
;
line-height
:
1.6
}
.confirm-dialog
>
label
strong
{
color
:
#27242d
}
.confirm-dialog
>
label
input
{
width
:
100%
;
height
:
39px
;
margin-top
:
7px
;
padding
:
0
11px
;
border
:
1px
solid
#ddd9e2
;
border-radius
:
9px
;
outline
:
0
}
.confirm-dialog
>
label
input
:focus
{
border-color
:
#c2aff8
;
box-shadow
:
0
0
0
3px
rgba
(
112
,
40
,
255
,
.07
)}
.confirm-dialog
>
div
:last-child
{
display
:
flex
;
justify-content
:
flex-end
;
gap
:
8px
;
margin-top
:
23px
}
.confirm-dialog
>
div
:last-child
button
{
height
:
36px
;
display
:
flex
;
align-items
:
center
;
gap
:
6px
;
padding
:
0
13px
;
color
:
#625e6a
;
background
:
#fff
;
border
:
1px
solid
#ded9e4
;
border-radius
:
9px
;
font-size
:
9px
;
font-weight
:
750
}
.confirm-dialog
>
div
:last-child
button
.danger
{
color
:
white
;
background
:
#bd4141
;
border-color
:
#bd4141
}
.confirm-backdrop
{
position
:
fixed
;
z-index
:
100
;
inset
:
0
;
display
:
grid
;
place-items
:
center
;
padding
:
20px
;
background
:
rgba
(
27
,
22
,
39
,
.36
);
backdrop-filter
:
blur
(
4px
)}
.confirm-dialog
{
width
:
min
(
430px
,
100%
);
padding
:
27px
;
background
:
#fff
;
border
:
1px
solid
#e4dfeb
;
border-radius
:
18px
;
box-shadow
:
0
24px
75px
rgba
(
24
,
14
,
50
,
.25
)}
.confirm-icon
{
display
:
grid
;
place-items
:
center
;
width
:
42px
;
height
:
42px
;
color
:
#6845c5
;
background
:
#f2edff
;
border-radius
:
12px
}
.confirm-icon.danger
{
color
:
#b43d3d
;
background
:
#fff0f0
}
.confirm-icon.success
{
color
:
#16805f
;
background
:
#e8f8f2
}
.confirm-dialog
h2
{
margin
:
17px
0
8px
;
font-size
:
19px
}
.confirm-dialog
p
{
margin
:
0
;
color
:
#77737f
;
font-size
:
11px
;
line-height
:
1.75
}
.confirm-dialog
>
label
{
display
:
block
;
margin-top
:
18px
;
color
:
#686471
;
font-size
:
10px
;
line-height
:
1.6
}
.confirm-dialog
>
label
strong
{
color
:
#27242d
}
.confirm-dialog
>
label
input
{
width
:
100%
;
height
:
39px
;
margin-top
:
7px
;
padding
:
0
11px
;
border
:
1px
solid
#ddd9e2
;
border-radius
:
9px
;
outline
:
0
}
.confirm-dialog
>
label
input
:focus
{
border-color
:
#c2aff8
;
box-shadow
:
0
0
0
3px
rgba
(
112
,
40
,
255
,
.07
)}
.dialog-error
{
display
:
flex
;
align-items
:
flex-start
;
gap
:
6px
;
margin-top
:
14px
;
padding
:
9px
10px
;
color
:
#a83a3a
;
background
:
#fff1f1
;
border-radius
:
8px
;
font-size
:
9px
;
line-height
:
1.5
}
.dialog-error
svg
{
flex
:
0
0
auto
;
margin-top
:
1px
}
.confirm-dialog
>
div
:last-child
{
display
:
flex
;
justify-content
:
flex-end
;
gap
:
8px
;
margin-top
:
23px
}
.confirm-dialog
>
div
:last-child
button
{
height
:
36px
;
display
:
flex
;
align-items
:
center
;
gap
:
6px
;
padding
:
0
13px
;
color
:
#625e6a
;
background
:
#fff
;
border
:
1px
solid
#ded9e4
;
border-radius
:
9px
;
font-size
:
9px
;
font-weight
:
750
}
.confirm-dialog
>
div
:last-child
button
.primary
{
color
:
#fff
;
background
:
var
(
--gradient
);
border-color
:
transparent
}
.confirm-dialog
>
div
:last-child
button
.danger
{
color
:
white
;
background
:
#bd4141
;
border-color
:
#bd4141
}
.confirm-dialog
>
div
:last-child
button
:disabled
{
opacity
:
.48
;
cursor
:
not-allowed
}
.toolbar-actions
.publish-button.unavailable
,
.toolbar-actions
.publish-button.unavailable
:hover
{
color
:
#8e9099
;
background
:
#efeff4
;
border
:
1px
solid
#e1e1e7
;
box-shadow
:
none
;
cursor
:
pointer
}
@media
(
max-width
:
1000px
){
.managed-site-card
{
grid-template-columns
:
46px
minmax
(
0
,
1
fr
)}
.managed-site-actions
{
grid-column
:
2
;
justify-content
:
flex-start
;
flex-wrap
:
wrap
}
.managed-site-states
{
flex-wrap
:
wrap
}}
@media
(
max-width
:
800px
){
.site-management-workspace
{
grid-template-columns
:
52px
1
fr
}
.site-management-page
{
padding
:
28px
16px
42px
}
.site-management-header
{
align-items
:
flex-start
;
flex-direction
:
column
}
.site-management-filters
{
overflow-x
:
auto
}
.site-management-tools
{
align-items
:
flex-start
;
flex-direction
:
column
}
.site-management-tools
label
{
width
:
100%
}
.managed-site-card
{
grid-template-columns
:
38px
minmax
(
0
,
1
fr
);
padding
:
13px
}
.managed-site-avatar
{
width
:
38px
;
height
:
38px
}
.managed-site-states
{
gap
:
8px
}
.managed-site-actions
a
,
.managed-site-actions
button
{
height
:
30px
}
.site-management-header
>
button
{
height
:
36px
}}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment