feat: unify workspace dialogs
This commit is contained in:
@@ -15,7 +15,7 @@ import {
|
|||||||
IconUser,
|
IconUser,
|
||||||
IconExit
|
IconExit
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { Avatar, Banner, Button, Card, Dropdown, Layout, Modal, Nav, Tag, Typography } from '@douyinfe/semi-ui';
|
import { Avatar, Banner, Button, Card, Dropdown, Layout, Nav, Tag, Typography } from '@douyinfe/semi-ui';
|
||||||
import { type CSSProperties, FormEvent, type MouseEvent, useEffect, useLayoutEffect, useState } from 'react';
|
import { type CSSProperties, FormEvent, type MouseEvent, useEffect, useLayoutEffect, useState } from 'react';
|
||||||
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
@@ -24,6 +24,7 @@ import { hasMenu } from '../auth/session';
|
|||||||
import { useMobileLayout } from '../hooks/useMobileLayout';
|
import { useMobileLayout } from '../hooks/useMobileLayout';
|
||||||
import { preloadRoute, scheduleIdleRoutePreloads, shouldPreloadRouteOnIntent } from '../routing/routeModules';
|
import { preloadRoute, scheduleIdleRoutePreloads, shouldPreloadRouteOnIntent } from '../routing/routeModules';
|
||||||
import { PasswordInput } from '../shared/PasswordInput';
|
import { PasswordInput } from '../shared/PasswordInput';
|
||||||
|
import { WorkspaceDialog } from '../shared/WorkspaceDialog';
|
||||||
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
|
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
@@ -237,17 +238,30 @@ function PasswordDialog({ onClose, onChanged }: { onClose: () => void; onChanged
|
|||||||
setError(reason instanceof Error ? reason.message : '密码修改失败');
|
setError(reason instanceof Error ? reason.message : '密码修改失败');
|
||||||
} finally { setPending(false); }
|
} finally { setPending(false); }
|
||||||
};
|
};
|
||||||
return <Modal
|
return <WorkspaceDialog
|
||||||
className="v2-password-modal"
|
className="v2-password-modal"
|
||||||
visible
|
visible
|
||||||
title={<div className="v2-password-title"><span aria-hidden="true"><IconLock /></span><div><strong>修改登录密码</strong><Text aria-hidden="true" type="tertiary" size="small">更新账号安全凭据</Text></div></div>}
|
ariaLabel="修改登录密码"
|
||||||
aria-label="修改登录密码"
|
closeLabel="关闭修改登录密码"
|
||||||
|
title="修改登录密码"
|
||||||
|
description="更新账号安全凭据"
|
||||||
|
icon={<IconLock />}
|
||||||
|
badge="安全设置"
|
||||||
|
badgeColor="blue"
|
||||||
|
width={500}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
closeOnEsc={!pending}
|
closeOnEsc={!pending}
|
||||||
maskClosable={!pending}
|
maskClosable={!pending}
|
||||||
footer={null}
|
closeDisabled={pending}
|
||||||
|
footer={<>
|
||||||
|
<span className="v2-workspace-dialog-footer-note"><IconLock aria-hidden="true" />修改成功后需要重新登录</span>
|
||||||
|
<span className="v2-workspace-dialog-footer-actions">
|
||||||
|
<Button type="tertiary" onClick={onClose} disabled={pending}>取消</Button>
|
||||||
|
<Button form="v2-password-change-form" htmlType="submit" theme="solid" type="primary" loading={pending} disabled={pending || !currentPassword || !passwordValid || !passwordsMatch}>{pending ? '正在修改…' : '确认修改'}</Button>
|
||||||
|
</span>
|
||||||
|
</>}
|
||||||
>
|
>
|
||||||
<form className="v2-password-dialog" onSubmit={submit}>
|
<form id="v2-password-change-form" className="v2-password-dialog" onSubmit={submit}>
|
||||||
<Banner type="warning" bordered closeIcon={null} title="修改后需要重新登录" description="为保护账号安全,当前设备和其他设备上的旧会话都会失效。" />
|
<Banner type="warning" bordered closeIcon={null} title="修改后需要重新登录" description="为保护账号安全,当前设备和其他设备上的旧会话都会失效。" />
|
||||||
<label><span>当前密码</span><PasswordInput aria-label="当前密码" autoFocus required autoComplete="current-password" visibilityLabel="当前密码" value={currentPassword} onChange={setCurrentPassword} size="large" /></label>
|
<label><span>当前密码</span><PasswordInput aria-label="当前密码" autoFocus required autoComplete="current-password" visibilityLabel="当前密码" value={currentPassword} onChange={setCurrentPassword} size="large" /></label>
|
||||||
<label><span>新密码</span><PasswordInput aria-label="新密码" required minLength={10} autoComplete="new-password" visibilityLabel="新密码" value={newPassword} onChange={setNewPassword} placeholder="至少 10 位,包含三类字符" size="large" /></label>
|
<label><span>新密码</span><PasswordInput aria-label="新密码" required minLength={10} autoComplete="new-password" visibilityLabel="新密码" value={newPassword} onChange={setNewPassword} placeholder="至少 10 位,包含三类字符" size="large" /></label>
|
||||||
@@ -258,9 +272,8 @@ function PasswordDialog({ onClose, onChanged }: { onClose: () => void; onChanged
|
|||||||
<label><span>确认新密码</span><PasswordInput aria-label="确认新密码" required minLength={10} autoComplete="new-password" visibilityLabel="确认新密码" value={confirmPassword} onChange={setConfirmPassword} size="large" /></label>
|
<label><span>确认新密码</span><PasswordInput aria-label="确认新密码" required minLength={10} autoComplete="new-password" visibilityLabel="确认新密码" value={confirmPassword} onChange={setConfirmPassword} size="large" /></label>
|
||||||
{confirmationStarted ? <Text className={`v2-password-match${passwordsMatch ? ' is-valid' : ' is-error'}`} role="status">{passwordsMatch ? '两次输入一致' : '两次输入的新密码不一致'}</Text> : null}
|
{confirmationStarted ? <Text className={`v2-password-match${passwordsMatch ? ' is-valid' : ' is-error'}`} role="status">{passwordsMatch ? '两次输入一致' : '两次输入的新密码不一致'}</Text> : null}
|
||||||
{error ? <Banner className="v2-password-error" type="danger" bordered title="无法修改密码" description={error} /> : null}
|
{error ? <Banner className="v2-password-error" type="danger" bordered title="无法修改密码" description={error} /> : null}
|
||||||
<footer><Button type="tertiary" onClick={onClose} disabled={pending}>取消</Button><Button htmlType="submit" theme="solid" type="primary" loading={pending} disabled={pending || !currentPassword || !passwordValid || !passwordsMatch}>{pending ? '正在修改…' : '确认修改'}</Button></footer>
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>;
|
</WorkspaceDialog>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mobilePrimaryNavigation = [navigation[0], navigation[1], navigation[2], navigation[4]];
|
const mobilePrimaryNavigation = [navigation[0], navigation[1], navigation[2], navigation[4]];
|
||||||
|
|||||||
@@ -203,14 +203,16 @@ test('makes draft state explicit, protects bulk removal and preserves edits acro
|
|||||||
|
|
||||||
fireEvent.click(screen.getByRole('tab', { name: /车辆权限/ }));
|
fireEvent.click(screen.getByRole('tab', { name: /车辆权限/ }));
|
||||||
fireEvent.click(screen.getByRole('button', { name: '清空全部 1 辆车辆权限' }));
|
fireEvent.click(screen.getByRole('button', { name: '清空全部 1 辆车辆权限' }));
|
||||||
expect(await screen.findByText('清空 1 辆车辆权限?')).toBeInTheDocument();
|
expect(await screen.findByRole('dialog', { name: '确认清空车辆权限' })).toHaveTextContent('影响对象1 辆当前账号全部车辆权限');
|
||||||
|
expect(screen.getByRole('dialog', { name: '确认清空车辆权限' })).toHaveTextContent('未保存不会影响服务器');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '确认清空' }));
|
fireEvent.click(screen.getByRole('button', { name: '确认清空' }));
|
||||||
await waitFor(() => expect(screen.queryByRole('button', { name: '移除 粤A11111' })).not.toBeInTheDocument());
|
await waitFor(() => expect(screen.queryByRole('button', { name: '移除 粤A11111' })).not.toBeInTheDocument());
|
||||||
expect(screen.getByText('已从草稿移除 1 辆车,保存后生效')).toBeInTheDocument();
|
expect(screen.getByText('已从草稿移除 1 辆车,保存后生效')).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '关闭账号详情' }));
|
fireEvent.click(screen.getByRole('button', { name: '关闭账号详情' }));
|
||||||
expect(await screen.findByText('放弃未保存的更改?')).toBeInTheDocument();
|
expect(await screen.findByRole('dialog', { name: '确认放弃未保存的更改' })).toHaveTextContent('放弃未保存的更改?');
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'cancel' }));
|
expect(screen.getByRole('dialog', { name: '确认放弃未保存的更改' })).toHaveTextContent('服务器数据不受影响');
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '继续编辑' }));
|
||||||
expect(document.querySelector('.v2-user-editor-form')).toBeInTheDocument();
|
expect(document.querySelector('.v2-user-editor-form')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: '保存权限' })).toBeEnabled();
|
expect(screen.getByRole('button', { name: '保存权限' })).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { IconChevronRight, IconDelete, IconPlus, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
|
import { IconChevronRight, IconDelete, IconPlus, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
|
||||||
import { Avatar, Button, Card, Checkbox, Collapse, Empty, Input, List, Modal, Popconfirm, Select, Spin, Switch, Tabs, Tag, Typography } from '@douyinfe/semi-ui';
|
import { Avatar, Button, Card, Checkbox, Collapse, Empty, Input, List, Select, Spin, Switch, Tabs, Tag, Typography } from '@douyinfe/semi-ui';
|
||||||
import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react';
|
import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import type { AdminUser, CustomerUserInput, CustomerVehicleGrantInput } from '../../api/types';
|
import type { AdminUser, CustomerUserInput, CustomerVehicleGrantInput } from '../../api/types';
|
||||||
@@ -11,6 +11,7 @@ import { TablePagination } from '../shared/TablePagination';
|
|||||||
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
|
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
|
||||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||||
|
import { WorkspaceConfirmDialog } from '../shared/WorkspaceDialog';
|
||||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||||
import { WorkspaceSideSheet, type WorkspaceSideSheetBadgeColor } from '../shared/WorkspaceSideSheet';
|
import { WorkspaceSideSheet, type WorkspaceSideSheetBadgeColor } from '../shared/WorkspaceSideSheet';
|
||||||
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
|
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
|
||||||
@@ -147,6 +148,7 @@ export default function UsersPage() {
|
|||||||
const [vehicleLabels, setVehicleLabels] = useState<Record<string, string>>({});
|
const [vehicleLabels, setVehicleLabels] = useState<Record<string, string>>({});
|
||||||
const [feedback, setFeedback] = useState('');
|
const [feedback, setFeedback] = useState('');
|
||||||
const [editingGrantVIN, setEditingGrantVIN] = useState('');
|
const [editingGrantVIN, setEditingGrantVIN] = useState('');
|
||||||
|
const [confirmation, setConfirmation] = useState<'clear-vehicles' | 'discard-editor' | null>(null);
|
||||||
const attentionCustomerCount = useMemo(() => customers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).attention).length, [customers]);
|
const attentionCustomerCount = useMemo(() => customers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).attention).length, [customers]);
|
||||||
const readyCustomers = useMemo(() => visibleCustomers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).key === 'ready').length, [visibleCustomers]);
|
const readyCustomers = useMemo(() => visibleCustomers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).key === 'ready').length, [visibleCustomers]);
|
||||||
const attentionCustomers = useMemo(() => visibleCustomers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).attention).length, [visibleCustomers]);
|
const attentionCustomers = useMemo(() => visibleCustomers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).attention).length, [visibleCustomers]);
|
||||||
@@ -239,6 +241,7 @@ export default function UsersPage() {
|
|||||||
setVehicleLabels({});
|
setVehicleLabels({});
|
||||||
setFeedback('');
|
setFeedback('');
|
||||||
setEditingGrantVIN('');
|
setEditingGrantVIN('');
|
||||||
|
setConfirmation(null);
|
||||||
};
|
};
|
||||||
const closeEditor = () => {
|
const closeEditor = () => {
|
||||||
const nextDraft = draftFromUser();
|
const nextDraft = draftFromUser();
|
||||||
@@ -254,20 +257,14 @@ export default function UsersPage() {
|
|||||||
setVehicleLabels({});
|
setVehicleLabels({});
|
||||||
setFeedback('');
|
setFeedback('');
|
||||||
setEditingGrantVIN('');
|
setEditingGrantVIN('');
|
||||||
|
setConfirmation(null);
|
||||||
};
|
};
|
||||||
const requestCloseEditor = () => {
|
const requestCloseEditor = () => {
|
||||||
if (!hasUnsavedChanges) {
|
if (!hasUnsavedChanges) {
|
||||||
closeEditor();
|
closeEditor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Modal.confirm({
|
setConfirmation('discard-editor');
|
||||||
title: '放弃未保存的更改?',
|
|
||||||
content: '登录身份、菜单或车辆权限的修改尚未保存,关闭后将恢复到上次保存状态。',
|
|
||||||
okText: '放弃更改',
|
|
||||||
cancelText: '继续编辑',
|
|
||||||
okType: 'danger',
|
|
||||||
onOk: closeEditor
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
const selectCustomer = (user: AdminUser) => {
|
const selectCustomer = (user: AdminUser) => {
|
||||||
const nextDraft = draftFromUser(user);
|
const nextDraft = draftFromUser(user);
|
||||||
@@ -309,6 +306,7 @@ export default function UsersPage() {
|
|||||||
setVehicleComposerOpen(true);
|
setVehicleComposerOpen(true);
|
||||||
setEditingGrantVIN('');
|
setEditingGrantVIN('');
|
||||||
setFeedback(`已从草稿移除 ${removedCount} 辆车,保存后生效`);
|
setFeedback(`已从草稿移除 ${removedCount} 辆车,保存后生效`);
|
||||||
|
setConfirmation(null);
|
||||||
};
|
};
|
||||||
const updateGrant = (vin: string, patch: Partial<CustomerVehicleGrantInput>) => setDraft((value) => ({
|
const updateGrant = (vin: string, patch: Partial<CustomerVehicleGrantInput>) => setDraft((value) => ({
|
||||||
...value, vehicleGrants: value.vehicleGrants.map((grant) => grant.vin === vin ? { ...grant, ...patch } : grant)
|
...value, vehicleGrants: value.vehicleGrants.map((grant) => grant.vin === vin ? { ...grant, ...patch } : grant)
|
||||||
@@ -541,6 +539,7 @@ export default function UsersPage() {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
onCancel={requestCloseEditor}
|
onCancel={requestCloseEditor}
|
||||||
|
closeOnEsc={!confirmation}
|
||||||
footerNote={<span className="v2-user-save-state" role="status">
|
footerNote={<span className="v2-user-save-state" role="status">
|
||||||
<Tag color={save.isError ? 'red' : hasUnsavedChanges ? 'amber' : 'green'} type="light" size="small">{save.isError ? '保存失败' : hasUnsavedChanges ? '待保存' : '已同步'}</Tag>
|
<Tag color={save.isError ? 'red' : hasUnsavedChanges ? 'amber' : 'green'} type="light" size="small">{save.isError ? '保存失败' : hasUnsavedChanges ? '待保存' : '已同步'}</Tag>
|
||||||
<Typography.Text className={`v2-user-feedback${save.isError ? ' is-error' : ''}`} type={save.isError ? 'danger' : 'tertiary'}>{feedback || (hasUnsavedChanges ? '更改仅保存在当前草稿,保存后最多 30 秒生效' : '账号与权限已和服务器同步')}</Typography.Text>
|
<Typography.Text className={`v2-user-feedback${save.isError ? ' is-error' : ''}`} type={save.isError ? 'danger' : 'tertiary'}>{feedback || (hasUnsavedChanges ? '更改仅保存在当前草稿,保存后最多 30 秒生效' : '账号与权限已和服务器同步')}</Typography.Text>
|
||||||
@@ -621,17 +620,7 @@ export default function UsersPage() {
|
|||||||
aria-label={vehicleComposerOpen ? '收起添加车辆' : '添加授权车辆'}
|
aria-label={vehicleComposerOpen ? '收起添加车辆' : '添加授权车辆'}
|
||||||
onClick={() => setVehicleComposerOpen((value) => !value)}
|
onClick={() => setVehicleComposerOpen((value) => !value)}
|
||||||
>{vehicleComposerOpen ? '收起' : '添加'}</Button> : null}
|
>{vehicleComposerOpen ? '收起' : '添加'}</Button> : null}
|
||||||
{draft.vehicleGrants.length ? <Popconfirm
|
{draft.vehicleGrants.length ? <Button theme="borderless" type="tertiary" size="small" aria-haspopup="dialog" aria-controls="v2-user-clear-vehicles" aria-label={`清空全部 ${draft.vehicleGrants.length} 辆车辆权限`} onClick={() => setConfirmation('clear-vehicles')}>清空</Button> : null}
|
||||||
title={`清空 ${draft.vehicleGrants.length} 辆车辆权限?`}
|
|
||||||
content="车辆会先从当前草稿中移除,点击底部保存后正式生效。"
|
|
||||||
okText="确认清空"
|
|
||||||
cancelText="取消"
|
|
||||||
okType="danger"
|
|
||||||
position="bottomRight"
|
|
||||||
onConfirm={clearVehicleGrants}
|
|
||||||
>
|
|
||||||
<Button theme="borderless" type="tertiary" size="small" aria-label={`清空全部 ${draft.vehicleGrants.length} 辆车辆权限`}>清空</Button>
|
|
||||||
</Popconfirm> : null}
|
|
||||||
</div> : null}
|
</div> : null}
|
||||||
>
|
>
|
||||||
{vehicleComposerVisible ? <div id="v2-user-vehicle-composer" className="v2-user-vehicle-composer">
|
{vehicleComposerVisible ? <div id="v2-user-vehicle-composer" className="v2-user-vehicle-composer">
|
||||||
@@ -723,5 +712,39 @@ export default function UsersPage() {
|
|||||||
{grantWindowInvalid ? <p role="alert">请设置启用时间,并确保停用时间晚于启用时间。</p> : <p>不设置停用时间时,车辆授权将持续有效。</p>}
|
{grantWindowInvalid ? <p role="alert">请设置启用时间,并确保停用时间晚于启用时间。</p> : <p>不设置停用时间时,车辆授权将持续有效。</p>}
|
||||||
</div> : null}
|
</div> : null}
|
||||||
</WorkspaceSideSheet>
|
</WorkspaceSideSheet>
|
||||||
|
<WorkspaceConfirmDialog
|
||||||
|
visible={confirmation === 'clear-vehicles'}
|
||||||
|
ariaLabel="确认清空车辆权限"
|
||||||
|
className="v2-user-confirm-dialog"
|
||||||
|
title={`清空 ${draft.vehicleGrants.length} 辆车辆权限?`}
|
||||||
|
description="车辆会先从当前草稿中移除,只有点击账号详情底部的保存权限后才会正式生效。"
|
||||||
|
summaryItems={[
|
||||||
|
{ label: '影响对象', value: `${draft.vehicleGrants.length} 辆`, detail: '当前账号全部车辆权限', tone: 'danger' },
|
||||||
|
{ label: '当前动作', value: '修改草稿', detail: '确认后仍可继续编辑', tone: 'warning' },
|
||||||
|
{ label: '正式生效', value: '保存权限后', detail: '未保存不会影响服务器', tone: 'success' }
|
||||||
|
]}
|
||||||
|
cancelLabel="返回核对"
|
||||||
|
confirmLabel="确认清空"
|
||||||
|
note="这是草稿操作,仍需再次保存权限。"
|
||||||
|
onCancel={() => setConfirmation(null)}
|
||||||
|
onConfirm={clearVehicleGrants}
|
||||||
|
/>
|
||||||
|
<WorkspaceConfirmDialog
|
||||||
|
visible={confirmation === 'discard-editor'}
|
||||||
|
ariaLabel="确认放弃未保存的更改"
|
||||||
|
className="v2-user-confirm-dialog"
|
||||||
|
title="放弃未保存的更改?"
|
||||||
|
description="登录身份、菜单或车辆权限的修改尚未保存,关闭后将恢复到上次保存状态。"
|
||||||
|
summaryItems={[
|
||||||
|
{ label: '草稿状态', value: '尚未保存', detail: `${draft.menuKeys.length} 菜单 · ${draft.vehicleGrants.length} 辆`, tone: 'warning' },
|
||||||
|
{ label: '关闭结果', value: creating ? '清空新建草稿' : '恢复已保存状态', detail: creating ? '账号不会被创建' : `账号 @${selected?.username || draft.username}`, tone: 'danger' },
|
||||||
|
{ label: '服务器数据', value: '不受影响', detail: '不会发送保存请求', tone: 'success' }
|
||||||
|
]}
|
||||||
|
cancelLabel="继续编辑"
|
||||||
|
confirmLabel="放弃更改"
|
||||||
|
note="放弃后无法恢复当前草稿。"
|
||||||
|
onCancel={() => setConfirmation(null)}
|
||||||
|
onConfirm={closeEditor}
|
||||||
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const mobileFilterSheetSource = readFileSync(resolve(process.cwd(), 'src/v2/shar
|
|||||||
const passwordInputSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/PasswordInput.tsx'), 'utf8');
|
const passwordInputSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/PasswordInput.tsx'), 'utf8');
|
||||||
const workspaceFilterPanelSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceFilterPanel.tsx'), 'utf8');
|
const workspaceFilterPanelSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceFilterPanel.tsx'), 'utf8');
|
||||||
const workspaceMetricRailSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceMetricRail.tsx'), 'utf8');
|
const workspaceMetricRailSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceMetricRail.tsx'), 'utf8');
|
||||||
|
const workspaceDialogSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceDialog.tsx'), 'utf8');
|
||||||
const workspaceSideSheetSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceSideSheet.tsx'), 'utf8');
|
const workspaceSideSheetSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceSideSheet.tsx'), 'utf8');
|
||||||
const workspaceDetailSideSheetSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceDetailSideSheet.tsx'), 'utf8');
|
const workspaceDetailSideSheetSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceDetailSideSheet.tsx'), 'utf8');
|
||||||
const segmentedTabsSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/SegmentedTabs.tsx'), 'utf8');
|
const segmentedTabsSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/SegmentedTabs.tsx'), 'utf8');
|
||||||
@@ -275,6 +276,10 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.UsersPage).toContain('<Card className="v2-user-list"');
|
expect(corePageSources.UsersPage).toContain('<Card className="v2-user-list"');
|
||||||
expect(corePageSources.UsersPage).toContain('className="v2-user-editor-sidesheet"');
|
expect(corePageSources.UsersPage).toContain('className="v2-user-editor-sidesheet"');
|
||||||
expect(corePageSources.UsersPage).toContain("from '../shared/WorkspaceSideSheet'");
|
expect(corePageSources.UsersPage).toContain("from '../shared/WorkspaceSideSheet'");
|
||||||
|
expect(corePageSources.UsersPage).toContain("from '../shared/WorkspaceDialog'");
|
||||||
|
expect(corePageSources.UsersPage).toContain('<WorkspaceConfirmDialog');
|
||||||
|
expect(corePageSources.UsersPage).not.toContain('Modal.confirm');
|
||||||
|
expect(corePageSources.UsersPage).not.toContain('<Popconfirm');
|
||||||
expect(corePageSources.UsersPage).toContain('<WorkspaceSideSheet\n className="v2-user-editor-sidesheet"');
|
expect(corePageSources.UsersPage).toContain('<WorkspaceSideSheet\n className="v2-user-editor-sidesheet"');
|
||||||
expect(corePageSources.UsersPage).toContain('<WorkspaceSideSheet\n className="v2-user-grant-sidesheet"');
|
expect(corePageSources.UsersPage).toContain('<WorkspaceSideSheet\n className="v2-user-grant-sidesheet"');
|
||||||
expect(corePageSources.UsersPage).toContain('variant="editor"');
|
expect(corePageSources.UsersPage).toContain('variant="editor"');
|
||||||
@@ -440,6 +445,11 @@ describe('V2 production entry', () => {
|
|||||||
expect(workspaceStyles).toContain('.v2-workspace-filter-sidesheet .v2-workspace-config-title-icon {');
|
expect(workspaceStyles).toContain('.v2-workspace-filter-sidesheet .v2-workspace-config-title-icon {');
|
||||||
expect(workspaceStyles).toContain('.v2-workspace-config-summary {');
|
expect(workspaceStyles).toContain('.v2-workspace-config-summary {');
|
||||||
expect(workspaceStyles).toContain('.v2-workspace-config-footer {');
|
expect(workspaceStyles).toContain('.v2-workspace-config-footer {');
|
||||||
|
expect(workspaceDialogSource).toContain("from '@douyinfe/semi-ui'");
|
||||||
|
expect(workspaceDialogSource).toContain('<Modal');
|
||||||
|
expect(workspaceDialogSource).toContain('export function WorkspaceConfirmDialog');
|
||||||
|
expect(appShellSource).toContain('<WorkspaceDialog');
|
||||||
|
expect(appShellSource).not.toContain('<Modal');
|
||||||
expect(vehicleSearchSource).toContain('<MobileFilterSheet');
|
expect(vehicleSearchSource).toContain('<MobileFilterSheet');
|
||||||
expect(corePageSources.StatisticsPage).not.toContain('v2-mileage-source-popover');
|
expect(corePageSources.StatisticsPage).not.toContain('v2-mileage-source-popover');
|
||||||
expect(corePageSources.StatisticsPage).toContain('<Table className="v2-mileage-table"');
|
expect(corePageSources.StatisticsPage).toContain('<Table className="v2-mileage-table"');
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { afterEach, expect, test, vi } from 'vitest';
|
||||||
|
import { WorkspaceConfirmDialog, WorkspaceDialog } from './WorkspaceDialog';
|
||||||
|
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
test('renders one shared Semi dialog shell with a localized close action', () => {
|
||||||
|
const onCancel = vi.fn();
|
||||||
|
render(<WorkspaceDialog
|
||||||
|
visible
|
||||||
|
ariaLabel="修改安全设置"
|
||||||
|
closeLabel="关闭安全设置"
|
||||||
|
title="安全设置"
|
||||||
|
description="更新账号凭据"
|
||||||
|
icon={<span>锁</span>}
|
||||||
|
badge="账号安全"
|
||||||
|
onCancel={onCancel}
|
||||||
|
footer={<button>保存</button>}
|
||||||
|
>
|
||||||
|
<p>设置内容</p>
|
||||||
|
</WorkspaceDialog>);
|
||||||
|
|
||||||
|
const dialog = screen.getByRole('dialog', { name: '修改安全设置' });
|
||||||
|
expect(dialog.closest('.v2-workspace-dialog') ?? document.querySelector('.v2-workspace-dialog')).toBeInTheDocument();
|
||||||
|
expect(dialog).toHaveTextContent('安全设置更新账号凭据账号安全设置内容保存');
|
||||||
|
fireEvent.click(screen.getByLabelText('关闭安全设置'));
|
||||||
|
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps confirmation impact, cancel and destructive action in one accessible dialog', () => {
|
||||||
|
const onCancel = vi.fn();
|
||||||
|
const onConfirm = vi.fn();
|
||||||
|
render(<WorkspaceConfirmDialog
|
||||||
|
visible
|
||||||
|
ariaLabel="确认批量操作"
|
||||||
|
title="清空车辆权限?"
|
||||||
|
description="确认后先修改当前草稿。"
|
||||||
|
summaryItems={[
|
||||||
|
{ label: '影响对象', value: '43 辆', detail: '全部车辆权限', tone: 'danger' },
|
||||||
|
{ label: '正式生效', value: '保存后', detail: '当前仅修改草稿', tone: 'success' }
|
||||||
|
]}
|
||||||
|
cancelLabel="返回核对"
|
||||||
|
confirmLabel="确认清空"
|
||||||
|
onCancel={onCancel}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>);
|
||||||
|
|
||||||
|
const dialog = screen.getByRole('dialog', { name: '确认批量操作' });
|
||||||
|
expect(dialog).toHaveTextContent('影响对象43 辆全部车辆权限正式生效保存后当前仅修改草稿');
|
||||||
|
expect(screen.getByRole('list', { name: '操作影响摘要' })).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '返回核对' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '确认清空' }));
|
||||||
|
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
134
vehicle-data-platform/apps/web/src/v2/shared/WorkspaceDialog.tsx
Normal file
134
vehicle-data-platform/apps/web/src/v2/shared/WorkspaceDialog.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { IconAlertTriangle, IconClose } from '@douyinfe/semi-icons';
|
||||||
|
import { Button, Modal, Tag } from '@douyinfe/semi-ui';
|
||||||
|
import { cloneElement, type ReactElement, type ReactNode } from 'react';
|
||||||
|
|
||||||
|
export type WorkspaceDialogBadgeColor = 'blue' | 'green' | 'grey' | 'orange' | 'red';
|
||||||
|
|
||||||
|
export type WorkspaceDialogSummaryItem = {
|
||||||
|
label: string;
|
||||||
|
value: ReactNode;
|
||||||
|
detail: string;
|
||||||
|
tone?: 'neutral' | 'primary' | 'success' | 'warning' | 'danger';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceDialogProps = {
|
||||||
|
visible: boolean;
|
||||||
|
ariaLabel: string;
|
||||||
|
closeLabel: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: ReactNode;
|
||||||
|
badge?: ReactNode;
|
||||||
|
badgeColor?: WorkspaceDialogBadgeColor;
|
||||||
|
width?: string | number;
|
||||||
|
className?: string;
|
||||||
|
closeOnEsc?: boolean;
|
||||||
|
maskClosable?: boolean;
|
||||||
|
closeDisabled?: boolean;
|
||||||
|
footer?: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
onCancel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WorkspaceDialog({
|
||||||
|
visible,
|
||||||
|
ariaLabel,
|
||||||
|
closeLabel,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
badge,
|
||||||
|
badgeColor = 'blue',
|
||||||
|
width = 480,
|
||||||
|
className = '',
|
||||||
|
closeOnEsc = true,
|
||||||
|
maskClosable = true,
|
||||||
|
closeDisabled = false,
|
||||||
|
footer,
|
||||||
|
children,
|
||||||
|
onCancel
|
||||||
|
}: WorkspaceDialogProps) {
|
||||||
|
return <Modal
|
||||||
|
className={`v2-workspace-dialog${className ? ` ${className}` : ''}`}
|
||||||
|
visible={visible}
|
||||||
|
centered
|
||||||
|
width={width}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
title={<div className="v2-workspace-dialog-title">
|
||||||
|
<span className="v2-workspace-dialog-title-icon" aria-hidden="true">{icon}</span>
|
||||||
|
<span><strong>{title}</strong><small>{description}</small></span>
|
||||||
|
{badge ? <Tag color={badgeColor} type="light" size="small">{badge}</Tag> : null}
|
||||||
|
<Button className="v2-workspace-dialog-close" theme="borderless" type="tertiary" size="small" icon={<IconClose />} aria-label={closeLabel} disabled={closeDisabled} onClick={onCancel} />
|
||||||
|
</div>}
|
||||||
|
closable={false}
|
||||||
|
closeOnEsc={closeOnEsc}
|
||||||
|
maskClosable={maskClosable}
|
||||||
|
onCancel={onCancel}
|
||||||
|
modalRender={(content) => cloneElement(content as ReactElement, { 'aria-label': ariaLabel, 'aria-labelledby': undefined })}
|
||||||
|
footer={footer ? <div className="v2-workspace-dialog-footer">{footer}</div> : null}
|
||||||
|
>
|
||||||
|
<div className="v2-workspace-dialog-content">{children}</div>
|
||||||
|
</Modal>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkspaceConfirmDialogProps = {
|
||||||
|
visible: boolean;
|
||||||
|
ariaLabel: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
summaryItems: WorkspaceDialogSummaryItem[];
|
||||||
|
confirmLabel: string;
|
||||||
|
cancelLabel: string;
|
||||||
|
note?: string;
|
||||||
|
pending?: boolean;
|
||||||
|
className?: string;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WorkspaceConfirmDialog({
|
||||||
|
visible,
|
||||||
|
ariaLabel,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
summaryItems,
|
||||||
|
confirmLabel,
|
||||||
|
cancelLabel,
|
||||||
|
note = '确认前请再次核对影响范围。',
|
||||||
|
pending = false,
|
||||||
|
className = '',
|
||||||
|
onConfirm,
|
||||||
|
onCancel
|
||||||
|
}: WorkspaceConfirmDialogProps) {
|
||||||
|
return <WorkspaceDialog
|
||||||
|
visible={visible}
|
||||||
|
ariaLabel={ariaLabel}
|
||||||
|
closeLabel={`关闭${ariaLabel}`}
|
||||||
|
className={`v2-workspace-confirm-dialog${className ? ` ${className}` : ''}`}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
icon={<IconAlertTriangle />}
|
||||||
|
badge="需要确认"
|
||||||
|
badgeColor="orange"
|
||||||
|
closeOnEsc={!pending}
|
||||||
|
maskClosable={!pending}
|
||||||
|
closeDisabled={pending}
|
||||||
|
onCancel={onCancel}
|
||||||
|
footer={<>
|
||||||
|
<span className="v2-workspace-dialog-footer-note"><IconAlertTriangle aria-hidden="true" />{note}</span>
|
||||||
|
<span className="v2-workspace-dialog-footer-actions">
|
||||||
|
<Button type="tertiary" disabled={pending} onClick={onCancel}>{cancelLabel}</Button>
|
||||||
|
<Button theme="solid" type="danger" loading={pending} disabled={pending} onClick={onConfirm}>{confirmLabel}</Button>
|
||||||
|
</span>
|
||||||
|
</>}
|
||||||
|
>
|
||||||
|
<p className="v2-workspace-confirm-copy">{description}</p>
|
||||||
|
<div className="v2-workspace-dialog-summary" role="list" aria-label="操作影响摘要">
|
||||||
|
{summaryItems.map((item) => <span className={`is-${item.tone || 'neutral'}`} role="listitem" key={item.label}>
|
||||||
|
<small>{item.label}</small>
|
||||||
|
<strong>{item.value}</strong>
|
||||||
|
<em>{item.detail}</em>
|
||||||
|
</span>)}
|
||||||
|
</div>
|
||||||
|
</WorkspaceDialog>;
|
||||||
|
}
|
||||||
@@ -22266,3 +22266,354 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shared Semi UI dialog language.
|
||||||
|
* High-impact forms and confirmations now use one title, content, impact
|
||||||
|
* summary and footer hierarchy on desktop and mobile.
|
||||||
|
*/
|
||||||
|
.v2-workspace-dialog .semi-modal {
|
||||||
|
width: min(480px, calc(100vw - 32px)) !important;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog .semi-modal-content {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #d9e3ef;
|
||||||
|
border-radius: 17px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 28px 88px rgba(19, 37, 61, .22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog .semi-modal-header {
|
||||||
|
min-height: 78px;
|
||||||
|
border-bottom: 1px solid #e5ebf2;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 0 0, rgba(18, 104, 243, .07), transparent 44%),
|
||||||
|
linear-gradient(180deg, #fff 0%, #fbfcfe 100%);
|
||||||
|
padding: 15px 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog .semi-modal-title {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-title {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
grid-template-columns: 40px minmax(0, 1fr) auto 34px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-title-icon {
|
||||||
|
display: grid;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid #d7e5f8;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #edf5ff;
|
||||||
|
color: #1268f3;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-confirm-dialog .v2-workspace-dialog-title-icon {
|
||||||
|
border-color: #f4ddbd;
|
||||||
|
background: #fff7e8;
|
||||||
|
color: #dc7b12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-title > span:nth-child(2) {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-title strong {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #263b53;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 740;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-title small {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #7d8da1;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 520;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-title > .semi-tag {
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding-inline: 9px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 720;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-close.semi-button {
|
||||||
|
width: 34px;
|
||||||
|
min-width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #72839a;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-close.semi-button:hover {
|
||||||
|
background: #edf3fb;
|
||||||
|
color: #23415f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog .semi-modal-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-content {
|
||||||
|
padding: 16px 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog .semi-modal-footer {
|
||||||
|
margin: 0;
|
||||||
|
border-top: 1px solid #e5ebf2;
|
||||||
|
background: rgba(251, 252, 254, .96);
|
||||||
|
padding: 11px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-footer-note {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #75869b;
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-footer-note > svg {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: #d78624;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-footer-actions {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-footer-actions > .semi-button {
|
||||||
|
min-height: 38px;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 9px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-confirm-copy {
|
||||||
|
margin: 0 0 13px;
|
||||||
|
color: #53677f;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dfe7f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary > span {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 86px;
|
||||||
|
align-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary > span + span {
|
||||||
|
border-left: 1px solid #dfe7f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary small {
|
||||||
|
color: #8795a7;
|
||||||
|
font-size: 8px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary strong {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #2d425a;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 740;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary em {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #7b8b9f;
|
||||||
|
font-size: 8px;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 1.35;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary > .is-primary strong {
|
||||||
|
color: #1268f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary > .is-success strong {
|
||||||
|
color: #16805b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary > .is-warning strong {
|
||||||
|
color: #b86c18;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary > .is-danger strong {
|
||||||
|
color: #c33c32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-password-modal.v2-workspace-dialog .semi-modal {
|
||||||
|
width: min(500px, calc(100vw - 32px)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-password-modal .v2-workspace-dialog-content {
|
||||||
|
padding: 16px 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-password-modal .v2-password-dialog > label {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-password-modal .v2-password-dialog > label .semi-input-wrapper {
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.v2-workspace-dialog .semi-modal {
|
||||||
|
width: calc(100vw - 16px) !important;
|
||||||
|
max-width: calc(100vw - 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog .semi-modal-content {
|
||||||
|
max-height: calc(100dvh - 16px);
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog .semi-modal-header {
|
||||||
|
min-height: 70px;
|
||||||
|
padding: 11px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-title {
|
||||||
|
grid-template-columns: 36px minmax(0, 1fr) auto 34px;
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-title-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 11px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-title strong {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-title small {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-title > .semi-tag {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-content {
|
||||||
|
max-height: calc(100dvh - 158px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 13px 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-confirm-copy {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary > span {
|
||||||
|
min-height: 76px;
|
||||||
|
padding: 9px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary strong {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-summary em {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog .semi-modal-footer {
|
||||||
|
padding: 9px 10px calc(9px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-footer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-footer-note {
|
||||||
|
min-height: 18px;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-footer-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-dialog-footer-actions > .semi-button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-password-modal .v2-workspace-dialog-content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-password-modal .v2-password-dialog > label .semi-input-wrapper {
|
||||||
|
height: 46px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user