feat: unify account permission workbenches
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { IconChevronRight, IconDelete, IconPlus, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
|
||||
import { Avatar, Button, Card, Checkbox, Collapse, Empty, Input, List, Modal, Popconfirm, Select, SideSheet, Spin, Switch, Tabs, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { Avatar, Button, Card, Checkbox, Collapse, Empty, Input, List, Modal, Popconfirm, Select, Spin, Switch, Tabs, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react';
|
||||
import { api } from '../../api/client';
|
||||
import type { AdminUser, CustomerUserInput, CustomerVehicleGrantInput } from '../../api/types';
|
||||
@@ -13,6 +13,7 @@ import { VehicleCandidateList } from '../shared/VehicleCandidateList';
|
||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||
import { WorkspaceSideSheet, type WorkspaceSideSheetBadgeColor } from '../shared/WorkspaceSideSheet';
|
||||
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
|
||||
import { PanelEmpty, PanelLoading } from '../shared/AsyncState';
|
||||
|
||||
@@ -105,6 +106,11 @@ function customerScopeLabel(scope: CustomerScopeFilter) {
|
||||
return '全部状态';
|
||||
}
|
||||
|
||||
function accessBadgeColor(color: CustomerAccessState['color']): WorkspaceSideSheetBadgeColor {
|
||||
if (color === 'amber') return 'orange';
|
||||
return color;
|
||||
}
|
||||
|
||||
export default function UsersPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const mobileLayout = useMobileLayout();
|
||||
@@ -155,8 +161,6 @@ export default function UsersPage() {
|
||||
const hasUnsavedChanges = useMemo(() => draftSignature(draft) !== draftSignature(baselineDraft), [baselineDraft, draft]);
|
||||
const draftAccessState = customerAccessState(draft.status, draft.menuKeys.length, draft.vehicleGrants.length);
|
||||
useSideSheetA11y(mobileFiltersOpen, '.v2-user-mobile-filter-sidesheet', 'v2-user-mobile-filters', '客户账号筛选', '关闭客户账号筛选');
|
||||
useSideSheetA11y(editorVisible, '.v2-user-editor-sidesheet', 'v2-user-editor-sheet', '客户账号详情', '关闭账号详情');
|
||||
useSideSheetA11y(Boolean(editingGrant), '.v2-user-grant-sidesheet', 'v2-user-grant-window', '车辆授权有效期', '关闭车辆授权有效期');
|
||||
|
||||
useEffect(() => {
|
||||
if (!creating && selected) {
|
||||
@@ -514,29 +518,33 @@ export default function UsersPage() {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<SideSheet
|
||||
<WorkspaceSideSheet
|
||||
className="v2-user-editor-sidesheet"
|
||||
variant="editor"
|
||||
visible={editorVisible}
|
||||
width={mobileLayout ? '100%' : 'min(840px, 100vw)'}
|
||||
aria-label="客户账号详情"
|
||||
title={<div className="v2-user-editor-sheet-title">
|
||||
<span><strong>{creating ? '创建客户账号' : selected?.displayName}</strong>{creating ? <small>设置登录身份和最小必要权限</small> : <span className="v2-user-editor-context" aria-label="账号权限概览">
|
||||
<span className="is-account">@{selected?.username}</span>
|
||||
<span>菜单 <b>{draft.menuKeys.length}</b></span>
|
||||
<span>车辆 <b>{draft.vehicleGrants.length}</b></span>
|
||||
<span className="v2-user-editor-last-login">最近登录 <b>{formatTime(selected?.lastLoginAt)}</b></span>
|
||||
<Tag className="v2-user-editor-access-tag" color={draftAccessState.color} type="light" size="small">{draftAccessState.label}</Tag>
|
||||
</span>}</span>
|
||||
<label className="v2-user-status"><Switch aria-label={draft.status === 'enabled' ? '账号启用' : '账号停用'} checked={draft.status === 'enabled'} onChange={(checked) => setDraft((value) => ({ ...value, status: checked ? 'enabled' : 'disabled' }))} /><span>{draft.status === 'enabled' ? '账号启用' : '账号停用'}</span></label>
|
||||
</div>}
|
||||
ariaLabel="客户账号详情"
|
||||
closeLabel="关闭账号详情"
|
||||
title={creating ? '创建客户账号' : selected?.displayName || '客户账号'}
|
||||
description={creating ? '设置登录身份和最小必要权限' : `@${selected?.username || '—'} · 最近登录 ${formatTime(selected?.lastLoginAt)}`}
|
||||
badge={draftAccessState.label}
|
||||
badgeColor={accessBadgeColor(draftAccessState.color)}
|
||||
summaryItems={[
|
||||
{ label: '登录账号', value: draft.username ? `@${draft.username}` : '待设置', detail: creating ? '创建后不可修改' : '登录身份', tone: 'primary' },
|
||||
{ label: '权限范围', value: `${draft.menuKeys.length} 菜单 · ${draft.vehicleGrants.length} 辆`, detail: '最小必要权限', tone: draftAccessState.attention ? 'warning' : 'success' },
|
||||
{
|
||||
label: '账号状态',
|
||||
value: <span className="v2-user-editor-status-summary"><Switch aria-label={draft.status === 'enabled' ? '账号启用' : '账号停用'} checked={draft.status === 'enabled'} onChange={(checked) => setDraft((value) => ({ ...value, status: checked ? 'enabled' : 'disabled' }))} /><span>{draft.status === 'enabled' ? '启用' : '停用'}</span></span>,
|
||||
detail: '保存后最多 30 秒生效',
|
||||
tone: draft.status === 'enabled' ? 'success' : 'neutral'
|
||||
}
|
||||
]}
|
||||
onCancel={requestCloseEditor}
|
||||
footer={<div className="v2-user-editor-sheet-footer">
|
||||
<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>
|
||||
<Typography.Text className={`v2-user-feedback${save.isError ? ' is-error' : ''}`} type={save.isError ? 'danger' : 'tertiary'}>{feedback || (hasUnsavedChanges ? '更改仅保存在当前草稿,保存后最多 30 秒生效' : '账号与权限已和服务器同步')}</Typography.Text>
|
||||
</span>
|
||||
<Button theme="solid" htmlType="submit" form="v2-user-editor-form" disabled={save.isPending || !hasUnsavedChanges}>{save.isPending ? '正在保存…' : creating ? '创建账号' : '保存权限'}</Button>
|
||||
</div>}
|
||||
</span>}
|
||||
primaryAction={{ label: creating ? '创建账号' : '保存权限', onClick: persistDraft, disabled: save.isPending || !hasUnsavedChanges, loading: save.isPending }}
|
||||
>
|
||||
{editorVisible ? <form id="v2-user-editor-form" className="v2-user-editor-form" onSubmit={submit}>
|
||||
<Tabs className="v2-user-editor-tabs" activeKey={activeSection} onChange={(key) => setActiveSection(String(key) as EditorSection)}>
|
||||
@@ -680,25 +688,39 @@ export default function UsersPage() {
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</form> : null}
|
||||
</SideSheet>
|
||||
<SideSheet
|
||||
</WorkspaceSideSheet>
|
||||
<WorkspaceSideSheet
|
||||
className="v2-user-grant-sidesheet"
|
||||
variant="config"
|
||||
visible={Boolean(editingGrant)}
|
||||
placement={mobileLayout ? 'bottom' : 'right'}
|
||||
width={420}
|
||||
aria-label="车辆授权有效期"
|
||||
title={<div className="v2-user-grant-sheet-title"><strong>调整车辆有效期</strong><span>{editingGrant ? `${editingGrantPlate || '未登记车牌'} · ${editingGrant.vin}` : '车辆授权时间窗口'}</span></div>}
|
||||
height={mobileLayout ? 'min(82dvh, 680px)' : undefined}
|
||||
ariaLabel="车辆授权有效期"
|
||||
closeLabel="关闭车辆授权有效期"
|
||||
title="车辆授权有效期"
|
||||
description={editingGrant ? `${editingGrantPlate || '未登记车牌'} · ${editingGrant.vin}` : '设置车辆可访问时间窗口'}
|
||||
badge={editingGrant?.validTo ? '定时停用' : '持续有效'}
|
||||
badgeColor={editingGrant?.validTo ? 'orange' : 'green'}
|
||||
summaryItems={editingGrant ? [
|
||||
{ label: '车牌', value: editingGrantPlate || '未登记车牌', tone: 'primary' },
|
||||
{ label: '启用', value: formatGrantTime(editingGrant.validFrom), detail: '授权开始' },
|
||||
{ label: '停用', value: editingGrant.validTo ? formatGrantTime(editingGrant.validTo) : '持续有效', detail: editingGrant.validTo ? '授权结束' : '未设置结束时间', tone: editingGrant.validTo ? 'warning' : 'success' }
|
||||
] : []}
|
||||
onCancel={() => setEditingGrantVIN('')}
|
||||
footer={<div className="v2-user-grant-sheet-footer"><Typography.Text type="tertiary">完成后仍需保存账号权限才会生效</Typography.Text><Button theme="solid" disabled={grantWindowInvalid} onClick={() => setEditingGrantVIN('')}>完成</Button></div>}
|
||||
footerNote="完成后仍需保存账号权限才会生效"
|
||||
primaryAction={{ label: '完成', onClick: () => setEditingGrantVIN(''), disabled: grantWindowInvalid }}
|
||||
>
|
||||
{editingGrant ? <div className="v2-user-grant-form">
|
||||
<Card className="v2-user-grant-summary" bodyStyle={{ padding: 0 }}>
|
||||
<span><small>车牌</small><strong>{editingGrantPlate || '未登记车牌'}</strong></span>
|
||||
<span><small>VIN</small><strong>{editingGrant.vin}</strong></span>
|
||||
</Card>
|
||||
<label><span>启用时间</span><Input aria-label={`${editingGrantPlate || editingGrant.vin} 启用时间`} type="datetime-local" required value={editingGrant.validFrom} onChange={(value) => updateGrant(editingGrant.vin, { validFrom: value })} /></label>
|
||||
<label><span>停用时间(可选)</span><Input aria-label={`${editingGrantPlate || editingGrant.vin} 停用时间`} type="datetime-local" min={editingGrant.validFrom} value={editingGrant.validTo} onChange={(value) => updateGrant(editingGrant.vin, { validTo: value })} /></label>
|
||||
<div className="v2-user-grant-field-group" role="group" aria-labelledby="v2-user-grant-window-heading">
|
||||
<header><span><strong id="v2-user-grant-window-heading">访问时间窗口</strong><small>只在授权区间内开放该车数据</small></span><Tag color="blue" type="light" size="small">上海时间</Tag></header>
|
||||
<div>
|
||||
<label><span>启用时间</span><Input aria-label={`${editingGrantPlate || editingGrant.vin} 启用时间`} type="datetime-local" required value={editingGrant.validFrom} onChange={(value) => updateGrant(editingGrant.vin, { validFrom: value })} /></label>
|
||||
<label><span>停用时间(可选)</span><Input aria-label={`${editingGrantPlate || editingGrant.vin} 停用时间`} type="datetime-local" min={editingGrant.validFrom} value={editingGrant.validTo} onChange={(value) => updateGrant(editingGrant.vin, { validTo: value })} /></label>
|
||||
</div>
|
||||
</div>
|
||||
{grantWindowInvalid ? <p role="alert">请设置启用时间,并确保停用时间晚于启用时间。</p> : <p>不设置停用时间时,车辆授权将持续有效。</p>}
|
||||
</div> : null}
|
||||
</SideSheet>
|
||||
</WorkspaceSideSheet>
|
||||
</div>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user