组件 · COMPONENT
确认窗 Dialog
纸面卡片、宽 440、圆角 16,遮罩只降亮不模糊——背景仍能认出正在操作什么。朱键只用于破坏性确认。
还没有操作
import { useState } from 'react'
import { ConfirmProvider, useConfirm } from './extracted/ConfirmDialog.jsx'
import { Btn } from './components/v4/Button.jsx'
function Actions() {
const confirm = useConfirm()
const [result, setResult] = useState('还没有操作')
const remove = async () => {
const ok = await confirm({
title: '确认删除这条笔记?',
description: '此操作不可撤销。',
tone: 'destructive',
confirmText: '删除',
})
setResult(ok ? '已删除' : '已取消')
}
const publish = async () => {
const ok = await confirm({ title: '发布这节课?', description: '发布后学员立即可见。', confirmText: '发布' })
setResult(ok ? '已发布' : '已取消')
}
return (
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
<Btn variant="accent" onClick={remove}>删除笔记</Btn>
<Btn onClick={publish}>发布课节</Btn>
<span style={{ fontSize: 12, color: '#8B8B85' }}>{result}</span>
</div>
)
}
export default function DialogDemo() {
return (
<ConfirmProvider>
<Actions />
</ConfirmProvider>
)
}<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap">
<button type="button" class="zzm-btn zzm-btn--accent">删除笔记</button>
<button type="button" class="zzm-btn">发布课节</button>
<span style="font-size: 12px; color: #8b8b85">还没有操作</span>
</div>/* controls.css */
.zzm-v4 .zzm-btn {
-webkit-appearance: none;
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 36px;
padding: 0 16px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
letter-spacing: .2px;
white-space: nowrap;
cursor: pointer;
user-select: none;
border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */
border: 1px solid rgba(0, 0, 0, .14);
color: #3F3F3B;
background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
box-shadow:
0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */
0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */
0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */
inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */
inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */
transition: box-shadow .16s ease, transform .16s ease, background .16s ease,
border-color .16s ease, filter .16s ease, opacity .16s ease;
}
.zzm-v4 .zzm-btn:hover {
transform: translateY(-1px);
border-color: rgba(0, 0, 0, .18);
box-shadow:
0 0 0 4px rgba(255, 255, 255, .8),
0 2px 4px rgba(0, 0, 0, .10),
0 4px 12px rgba(0, 0, 0, .09),
inset 0 1px 0 rgba(255, 255, 255, .95),
inset 0 -1px 0 rgba(0, 0, 0, .05);
}
.zzm-v4 .zzm-btn:active {
transform: translateY(0);
background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */
box-shadow:
0 0 0 3px rgba(255, 255, 255, .45),
0 1px 0 rgba(255, 255, 255, .65),
inset 0 2px 4px rgba(0, 0, 0, .13),
inset 0 1px 2px rgba(0, 0, 0, .09);
}
.zzm-v4 .zzm-btn:focus-visible {
outline: none;
box-shadow:
0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */
0 1px 2px rgba(0, 0, 0, .09),
0 2px 6px rgba(0, 0, 0, .06),
inset 0 1px 0 rgba(255, 255, 255, .92);
}
.zzm-v4 .zzm-btn:disabled {
cursor: not-allowed;
transform: none;
}
.zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) {
color: #A2A29C;
background: #F2F2EF;
border-color: rgba(0, 0, 0, .08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04);
}
.zzm-v4 .zzm-btn--accent {
color: #FFFFFF;
border-color: rgba(122, 42, 29, .9);
background: linear-gradient(180deg, #C25743 0%, #A6402F 56%, #99392A 100%);
box-shadow:
0 0 0 3px rgba(166, 64, 47, .10), /* 朱色反光光晕 */
0 1px 2px rgba(0, 0, 0, .14),
0 3px 10px rgba(166, 64, 47, .28),
inset 0 1px 0 rgba(255, 255, 255, .32),
inset 0 -1px 0 rgba(0, 0, 0, .20);
}
.zzm-v4 .zzm-btn--accent:hover {
filter: brightness(1.06);
border-color: rgba(122, 42, 29, .9);
box-shadow:
0 0 0 4px rgba(166, 64, 47, .16),
0 2px 4px rgba(0, 0, 0, .16),
0 5px 14px rgba(166, 64, 47, .34),
inset 0 1px 0 rgba(255, 255, 255, .36),
inset 0 -1px 0 rgba(0, 0, 0, .20);
}
.zzm-v4 .zzm-btn--accent:active {
filter: none;
background: linear-gradient(180deg, #8E3425 0%, #A03D2C 100%);
box-shadow:
0 0 0 3px rgba(166, 64, 47, .10),
inset 0 2px 5px rgba(0, 0, 0, .34),
inset 0 1px 2px rgba(0, 0, 0, .24);
}确认删除这条笔记?
此操作不可撤销。确认窗保留操作上下文,不让浏览器原生灰框打断视觉语言。
import { ConfirmPanel } from './extracted/ConfirmDialog.jsx'
export function Destructive() {
return (
<ConfirmPanel
inline
title="确认删除这条笔记?"
description="此操作不可撤销。确认窗保留操作上下文,不让浏览器原生灰框打断视觉语言。"
tone="destructive"
confirmText="删除"
/>
)
}<div
role="alertdialog"
aria-labelledby="zzm-dialog-title"
aria-describedby="zzm-dialog-desc"
class="zzm-dialog-panel zzm-dialog-panel--inline"
>
<div class="zzm-dialog-header">
<h2 id="zzm-dialog-title" class="zzm-dialog-title">确认删除这条笔记?</h2>
<p id="zzm-dialog-desc" class="zzm-dialog-description">
此操作不可撤销。确认窗保留操作上下文,不让浏览器原生灰框打断视觉语言。
</p>
</div>
<div class="zzm-dialog-actions">
<button type="button" class="zzm-btn zzm-btn--sm">取消</button>
<button type="button" class="zzm-btn zzm-btn--sm zzm-btn--accent">删除</button>
</div>
</div>/* controls.css */
.zzm-v4 .zzm-btn {
-webkit-appearance: none;
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 36px;
padding: 0 16px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
letter-spacing: .2px;
white-space: nowrap;
cursor: pointer;
user-select: none;
border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */
border: 1px solid rgba(0, 0, 0, .14);
color: #3F3F3B;
background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
box-shadow:
0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */
0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */
0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */
inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */
inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */
transition: box-shadow .16s ease, transform .16s ease, background .16s ease,
border-color .16s ease, filter .16s ease, opacity .16s ease;
}
.zzm-v4 .zzm-btn:hover {
transform: translateY(-1px);
border-color: rgba(0, 0, 0, .18);
box-shadow:
0 0 0 4px rgba(255, 255, 255, .8),
0 2px 4px rgba(0, 0, 0, .10),
0 4px 12px rgba(0, 0, 0, .09),
inset 0 1px 0 rgba(255, 255, 255, .95),
inset 0 -1px 0 rgba(0, 0, 0, .05);
}
.zzm-v4 .zzm-btn:active {
transform: translateY(0);
background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */
box-shadow:
0 0 0 3px rgba(255, 255, 255, .45),
0 1px 0 rgba(255, 255, 255, .65),
inset 0 2px 4px rgba(0, 0, 0, .13),
inset 0 1px 2px rgba(0, 0, 0, .09);
}
.zzm-v4 .zzm-btn:focus-visible {
outline: none;
box-shadow:
0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */
0 1px 2px rgba(0, 0, 0, .09),
0 2px 6px rgba(0, 0, 0, .06),
inset 0 1px 0 rgba(255, 255, 255, .92);
}
.zzm-v4 .zzm-btn:disabled {
cursor: not-allowed;
transform: none;
}
.zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) {
color: #A2A29C;
background: #F2F2EF;
border-color: rgba(0, 0, 0, .08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04);
}
.zzm-v4 .zzm-btn--accent {
color: #FFFFFF;
border-color: rgba(122, 42, 29, .9);
background: linear-gradient(180deg, #C25743 0%, #A6402F 56%, #99392A 100%);
box-shadow:
0 0 0 3px rgba(166, 64, 47, .10), /* 朱色反光光晕 */
0 1px 2px rgba(0, 0, 0, .14),
0 3px 10px rgba(166, 64, 47, .28),
inset 0 1px 0 rgba(255, 255, 255, .32),
inset 0 -1px 0 rgba(0, 0, 0, .20);
}
.zzm-v4 .zzm-btn--accent:hover {
filter: brightness(1.06);
border-color: rgba(122, 42, 29, .9);
box-shadow:
0 0 0 4px rgba(166, 64, 47, .16),
0 2px 4px rgba(0, 0, 0, .16),
0 5px 14px rgba(166, 64, 47, .34),
inset 0 1px 0 rgba(255, 255, 255, .36),
inset 0 -1px 0 rgba(0, 0, 0, .20);
}
.zzm-v4 .zzm-btn--accent:active {
filter: none;
background: linear-gradient(180deg, #8E3425 0%, #A03D2C 100%);
box-shadow:
0 0 0 3px rgba(166, 64, 47, .10),
inset 0 2px 5px rgba(0, 0, 0, .34),
inset 0 1px 2px rgba(0, 0, 0, .24);
}
.zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; }
.zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; }
.zzm-v4 .zzm-dialog-panel {
width: min(calc(100% - 32px), 440px);
max-width: 440px;
gap: 16px;
padding: 24px;
border: 1px solid rgba(0, 0, 0, .14);
border-radius: 16px;
background: #FFFFFF;
box-shadow:
0 1px 2px rgba(0, 0, 0, .10),
0 18px 48px rgba(0, 0, 0, .22),
inset 0 1px 0 rgba(255, 255, 255, .92);
}
.zzm-v4 .zzm-dialog-title {
color: #1D1D1B;
font-size: 17px;
font-weight: 700;
line-height: 24px;
}
.zzm-v4 .zzm-dialog-description {
color: #6E6E68;
font-size: 13px;
line-height: 20px;
}
.zzm-v4 .zzm-dialog-actions {
margin-top: 4px;
gap: 8px;
}
/* extracted.css */
.zzm-v4 .zzm-dialog-panel {
position: fixed; top: 50%; left: 50%; z-index: 51; display: grid;
transform: translate(-50%, -50%); animation: zzmDialogIn .2s cubic-bezier(.2, .7, .2, 1) both;
}
.zzm-v4 .zzm-dialog-panel--inline { position: relative; top: auto; left: auto; z-index: auto; transform: none; animation: none; }
.zzm-v4 .zzm-dialog-header { display: grid; gap: 8px; }
.zzm-v4 .zzm-dialog-title,
.zzm-v4 .zzm-dialog-description { margin: 0; }
.zzm-v4 .zzm-dialog-actions { display: flex; justify-content: flex-end; }
/* keyframes */
@keyframes zzmDialogIn {
from { opacity: 0; transform: translate(-50%, -50%) scale(.96) }
to { opacity: 1; transform: translate(-50%, -50%) }
}发布这节课?
发布后学员立即可见。
import { ConfirmPanel } from './extracted/ConfirmDialog.jsx'
export function Normal() {
return <ConfirmPanel inline title="发布这节课?" description="发布后学员立即可见。" confirmText="发布" />
}<div
role="alertdialog"
aria-labelledby="zzm-dialog-title"
aria-describedby="zzm-dialog-desc"
class="zzm-dialog-panel zzm-dialog-panel--inline"
>
<div class="zzm-dialog-header">
<h2 id="zzm-dialog-title" class="zzm-dialog-title">发布这节课?</h2>
<p id="zzm-dialog-desc" class="zzm-dialog-description">发布后学员立即可见。</p>
</div>
<div class="zzm-dialog-actions">
<button type="button" class="zzm-btn zzm-btn--sm">取消</button>
<button type="button" class="zzm-btn zzm-btn--sm zzm-btn--primary">发布</button>
</div>
</div>/* controls.css */
.zzm-v4 .zzm-btn {
-webkit-appearance: none;
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 36px;
padding: 0 16px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
letter-spacing: .2px;
white-space: nowrap;
cursor: pointer;
user-select: none;
border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */
border: 1px solid rgba(0, 0, 0, .14);
color: #3F3F3B;
background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
box-shadow:
0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */
0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */
0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */
inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */
inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */
transition: box-shadow .16s ease, transform .16s ease, background .16s ease,
border-color .16s ease, filter .16s ease, opacity .16s ease;
}
.zzm-v4 .zzm-btn:hover {
transform: translateY(-1px);
border-color: rgba(0, 0, 0, .18);
box-shadow:
0 0 0 4px rgba(255, 255, 255, .8),
0 2px 4px rgba(0, 0, 0, .10),
0 4px 12px rgba(0, 0, 0, .09),
inset 0 1px 0 rgba(255, 255, 255, .95),
inset 0 -1px 0 rgba(0, 0, 0, .05);
}
.zzm-v4 .zzm-btn:active {
transform: translateY(0);
background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */
box-shadow:
0 0 0 3px rgba(255, 255, 255, .45),
0 1px 0 rgba(255, 255, 255, .65),
inset 0 2px 4px rgba(0, 0, 0, .13),
inset 0 1px 2px rgba(0, 0, 0, .09);
}
.zzm-v4 .zzm-btn:focus-visible {
outline: none;
box-shadow:
0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */
0 1px 2px rgba(0, 0, 0, .09),
0 2px 6px rgba(0, 0, 0, .06),
inset 0 1px 0 rgba(255, 255, 255, .92);
}
.zzm-v4 .zzm-btn:disabled {
cursor: not-allowed;
transform: none;
}
.zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) {
color: #A2A29C;
background: #F2F2EF;
border-color: rgba(0, 0, 0, .08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04);
}
.zzm-v4 .zzm-btn--primary {
color: #FFFFFF;
border-color: rgba(0, 0, 0, .85);
background: linear-gradient(180deg, #3A3A36 0%, #1D1D1B 58%, #141412 100%);
box-shadow:
0 0 0 3px rgba(22, 22, 22, .06),
0 1px 2px rgba(0, 0, 0, .18),
0 3px 10px rgba(22, 22, 22, .24),
inset 0 1px 0 rgba(255, 255, 255, .20),
inset 0 -1px 0 rgba(0, 0, 0, .38);
}
.zzm-v4 .zzm-btn--primary:hover {
filter: brightness(1.12);
border-color: rgba(0, 0, 0, .85);
box-shadow:
0 0 0 4px rgba(22, 22, 22, .09),
0 2px 4px rgba(0, 0, 0, .20),
0 5px 14px rgba(22, 22, 22, .28),
inset 0 1px 0 rgba(255, 255, 255, .24),
inset 0 -1px 0 rgba(0, 0, 0, .38);
}
.zzm-v4 .zzm-btn--primary:active {
filter: none;
background: linear-gradient(180deg, #101010 0%, #22221F 100%);
box-shadow:
0 0 0 3px rgba(22, 22, 22, .06),
inset 0 2px 5px rgba(0, 0, 0, .55),
inset 0 1px 2px rgba(0, 0, 0, .4);
}
.zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; }
.zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; }
.zzm-v4 .zzm-dialog-panel {
width: min(calc(100% - 32px), 440px);
max-width: 440px;
gap: 16px;
padding: 24px;
border: 1px solid rgba(0, 0, 0, .14);
border-radius: 16px;
background: #FFFFFF;
box-shadow:
0 1px 2px rgba(0, 0, 0, .10),
0 18px 48px rgba(0, 0, 0, .22),
inset 0 1px 0 rgba(255, 255, 255, .92);
}
.zzm-v4 .zzm-dialog-title {
color: #1D1D1B;
font-size: 17px;
font-weight: 700;
line-height: 24px;
}
.zzm-v4 .zzm-dialog-description {
color: #6E6E68;
font-size: 13px;
line-height: 20px;
}
.zzm-v4 .zzm-dialog-actions {
margin-top: 4px;
gap: 8px;
}
/* extracted.css */
.zzm-v4 .zzm-dialog-panel {
position: fixed; top: 50%; left: 50%; z-index: 51; display: grid;
transform: translate(-50%, -50%); animation: zzmDialogIn .2s cubic-bezier(.2, .7, .2, 1) both;
}
.zzm-v4 .zzm-dialog-panel--inline { position: relative; top: auto; left: auto; z-index: auto; transform: none; animation: none; }
.zzm-v4 .zzm-dialog-header { display: grid; gap: 8px; }
.zzm-v4 .zzm-dialog-title,
.zzm-v4 .zzm-dialog-description { margin: 0; }
.zzm-v4 .zzm-dialog-actions { display: flex; justify-content: flex-end; }
/* keyframes */
@keyframes zzmDialogIn {
from { opacity: 0; transform: translate(-50%, -50%) scale(.96) }
to { opacity: 1; transform: translate(-50%, -50%) }
}useConfirm()
与 edu 的 useDialog().confirm 同一个形状:在根部挂一次 ConfirmProvider,任何地方 await confirm({...}) 拿到 true / false。
| 属性 | 取值 | 默认 | 说明 |
|---|---|---|---|
| title | string | — | 标题(17px / 700) |
| description | string | — | 说明(13px / 20,灰) |
| tone | 'default' · 'primary' · 'destructive' | 'default' | 确认键:墨键,破坏性用朱键 |
| confirmText | string | '确认' | 确认键文字,写动作本身(「删除」而不是「确定」) |
| cancelText | string | '取消' | 取消键文字 |
从 edu 到这里改了什么
edu 的确认窗建在 shadcn dialog.jsx 与 Tailwind 工具类上。这里去掉了这两层依赖:皮肤仍是 controls.css 的 .zzm-dialog-*,定位与入场由 extracted.css 用纯 CSS 补齐,所以不装 Tailwind 也能 1:1 成立。
铁律
✓ 要朱键只给「删除、作废、注销」这类破坏性确认
✗ 不要用
window.confirm() 原生灰框✓ 要遮罩
rgba(22,22,22,.46),只降亮✗ 不要给遮罩加模糊