refine Semi UI account access readiness
This commit is contained in:
@@ -33,6 +33,14 @@ type Draft = {
|
||||
};
|
||||
|
||||
type EditorSection = 'identity' | 'menus' | 'vehicles';
|
||||
type CustomerScopeFilter = 'all' | Draft['status'] | 'attention';
|
||||
type CustomerAccessState = {
|
||||
key: 'ready' | 'missing-menus' | 'missing-vehicles' | 'disabled';
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
color: 'green' | 'amber' | 'grey';
|
||||
attention: boolean;
|
||||
};
|
||||
|
||||
const shanghaiDateTimeFormatter = new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false
|
||||
@@ -74,6 +82,20 @@ function formatGrantTime(value?: string) {
|
||||
return value.replace('T', ' ').slice(0, 16);
|
||||
}
|
||||
|
||||
function customerAccessState(status: Draft['status'], menuCount: number, vehicleCount: number): CustomerAccessState {
|
||||
if (status === 'disabled') return { key: 'disabled', label: '账号已停用', shortLabel: '停用', color: 'grey', attention: false };
|
||||
if (menuCount === 0) return { key: 'missing-menus', label: '待开放菜单', shortLabel: '待菜单', color: 'amber', attention: true };
|
||||
if (vehicleCount === 0) return { key: 'missing-vehicles', label: '待分配车辆', shortLabel: '待车辆', color: 'amber', attention: true };
|
||||
return { key: 'ready', label: '权限就绪', shortLabel: '就绪', color: 'green', attention: false };
|
||||
}
|
||||
|
||||
function customerScopeLabel(scope: CustomerScopeFilter) {
|
||||
if (scope === 'enabled') return '启用账号';
|
||||
if (scope === 'disabled') return '停用账号';
|
||||
if (scope === 'attention') return '待完善权限';
|
||||
return '全部状态';
|
||||
}
|
||||
|
||||
export default function UsersPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const mobileLayout = useMobileLayout();
|
||||
@@ -81,12 +103,14 @@ export default function UsersPage() {
|
||||
const customers = useMemo(() => (users.data ?? []).filter((user) => user.userType === 'customer'), [users.data]);
|
||||
const [customerKeyword, setCustomerKeyword] = useState('');
|
||||
const deferredCustomerKeyword = useDeferredValue(customerKeyword.trim());
|
||||
const [customerStatus, setCustomerStatus] = useState<'all' | Draft['status']>('all');
|
||||
const [customerStatus, setCustomerStatus] = useState<CustomerScopeFilter>('all');
|
||||
const [filtersCollapsed, setFiltersCollapsed] = useState(true);
|
||||
const visibleCustomers = useMemo(() => {
|
||||
const keyword = deferredCustomerKeyword.toLocaleLowerCase('zh-CN');
|
||||
return customers.filter((user) => {
|
||||
if (customerStatus !== 'all' && user.status !== customerStatus) return false;
|
||||
const accessState = customerAccessState(user.status, user.menuKeys.length, user.vehicles.length);
|
||||
if (customerStatus === 'attention' && !accessState.attention) return false;
|
||||
if (customerStatus !== 'all' && customerStatus !== 'attention' && user.status !== customerStatus) return false;
|
||||
if (!keyword) return true;
|
||||
return [user.displayName, user.username, user.customerRef, user.tenantRef]
|
||||
.some((value) => value?.toLocaleLowerCase('zh-CN').includes(keyword));
|
||||
@@ -107,7 +131,9 @@ export default function UsersPage() {
|
||||
const [vehicleLabels, setVehicleLabels] = useState<Record<string, string>>({});
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [editingGrantVIN, setEditingGrantVIN] = useState('');
|
||||
const enabledCustomers = useMemo(() => visibleCustomers.filter((user) => user.status === 'enabled').length, [visibleCustomers]);
|
||||
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 attentionCustomers = useMemo(() => visibleCustomers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).attention).length, [visibleCustomers]);
|
||||
const grantedVehicles = useMemo(() => visibleCustomers.reduce((total, user) => total + user.vehicles.length, 0), [visibleCustomers]);
|
||||
const editingGrant = useMemo(() => draft.vehicleGrants.find((grant) => grant.vin === editingGrantVIN), [draft.vehicleGrants, editingGrantVIN]);
|
||||
const editingGrantPlate = editingGrant ? vehicleLabels[editingGrant.vin] : '';
|
||||
@@ -115,6 +141,7 @@ export default function UsersPage() {
|
||||
const editorVisible = creating || Boolean(selected);
|
||||
const vehicleComposerVisible = !mobileLayout || creating || draft.vehicleGrants.length === 0 || vehicleComposerOpen;
|
||||
const hasUnsavedChanges = useMemo(() => draftSignature(draft) !== draftSignature(baselineDraft), [baselineDraft, draft]);
|
||||
const draftAccessState = customerAccessState(draft.status, draft.menuKeys.length, draft.vehicleGrants.length);
|
||||
useSideSheetA11y(editorVisible, '.v2-user-editor-sidesheet', 'v2-user-editor-sheet', '客户账号详情', '关闭账号详情');
|
||||
useSideSheetA11y(Boolean(editingGrant), '.v2-user-grant-sidesheet', 'v2-user-grant-window', '车辆授权有效期', '关闭车辆授权有效期');
|
||||
|
||||
@@ -311,15 +338,15 @@ export default function UsersPage() {
|
||||
ariaLabel="账号管理操作"
|
||||
title="客户访问治理"
|
||||
description="菜单与车辆按最小权限开放,变更最多 30 秒生效"
|
||||
status={`${customers.length} 个客户账号`}
|
||||
status={`${customers.length} 个客户账号${attentionCustomerCount ? ` · ${attentionCustomerCount} 待完善` : ' · 权限就绪'}`}
|
||||
meta={<Typography.Text type="tertiary">本地账号 · 可扩展外部身份源</Typography.Text>}
|
||||
actions={<Button className="v2-workspace-mobile-tool-button is-primary" theme="solid" icon={<IconPlus />} aria-label="新建客户账号" onClick={startCreate}>新建客户账号</Button>}
|
||||
/>
|
||||
<WorkspaceFilterPanel
|
||||
className="v2-user-filter-panel"
|
||||
title="账号范围"
|
||||
description="按客户名称、登录账号、客户标识与启停状态查找"
|
||||
mobileSummary={`${customerStatus === 'all' ? '全部状态' : customerStatus === 'enabled' ? '启用账号' : '停用账号'}${customerKeyword.trim() ? ` · ${customerKeyword.trim()}` : ''}`}
|
||||
description="按客户名称、登录账号与权限可用状态查找"
|
||||
mobileSummary={`${customerScopeLabel(customerStatus)}${customerKeyword.trim() ? ` · ${customerKeyword.trim()}` : ''}`}
|
||||
expanded={!filtersCollapsed}
|
||||
status={users.isPending ? '正在读取账号' : `${visibleCustomers.length} / ${customers.length} 个账号`}
|
||||
statusColor={customerKeyword.trim() || customerStatus !== 'all' ? 'blue' : 'grey'}
|
||||
@@ -332,7 +359,7 @@ export default function UsersPage() {
|
||||
<Input className="v2-user-list-search" aria-label="搜索客户账号" prefix={<IconSearch />} showClear value={customerKeyword} onChange={setCustomerKeyword} placeholder="搜索名称、账号或客户标识" />
|
||||
</label>
|
||||
<label>
|
||||
<span id="v2-user-status-filter-label">账号状态</span>
|
||||
<span id="v2-user-status-filter-label">访问状态</span>
|
||||
<Select
|
||||
aria-labelledby="v2-user-status-filter-label"
|
||||
value={customerStatus}
|
||||
@@ -340,7 +367,8 @@ export default function UsersPage() {
|
||||
optionList={[
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'enabled', label: '启用账号' },
|
||||
{ value: 'disabled', label: '停用账号' }
|
||||
{ value: 'disabled', label: '停用账号' },
|
||||
{ value: 'attention', label: '待完善权限' }
|
||||
]}
|
||||
/>
|
||||
</label>
|
||||
@@ -369,7 +397,8 @@ export default function UsersPage() {
|
||||
description="当前范围内的账号、菜单与车辆授权摘要"
|
||||
meta={<span className="v2-user-directory-summary">
|
||||
<Tag color={visibleCustomers.length === customers.length ? 'grey' : 'blue'} type="light" size="small">{visibleCustomers.length} / {customers.length} 个账号</Tag>
|
||||
<Typography.Text type="tertiary">启用 {enabledCustomers}</Typography.Text>
|
||||
<Typography.Text className="is-ready" type="tertiary">权限就绪 {readyCustomers}</Typography.Text>
|
||||
{attentionCustomers ? <Tag className="is-attention" color="amber" type="light" size="small">待完善 {attentionCustomers}</Tag> : null}
|
||||
<Typography.Text type="tertiary">车辆授权 {grantedVehicles}</Typography.Text>
|
||||
</span>}
|
||||
/>
|
||||
@@ -380,31 +409,34 @@ export default function UsersPage() {
|
||||
<span>菜单权限</span>
|
||||
<span>车辆权限</span>
|
||||
<span>最近登录</span>
|
||||
<span>状态</span>
|
||||
<span>访问状态</span>
|
||||
</div> : null}
|
||||
<div className="v2-customer-list-scroll" role="region" aria-label="客户账号目录,可上下滚动" tabIndex={0}>
|
||||
<List
|
||||
className="v2-customer-list"
|
||||
dataSource={visibleCustomers}
|
||||
emptyContent={<Empty className="v2-user-filter-empty" title="没有匹配账号" description="请更换名称或账号关键词。" />}
|
||||
renderItem={(user) => <List.Item key={user.id} className={`v2-user-list-row${selectedID === user.id && !creating ? ' is-active' : ''}`}>
|
||||
emptyContent={<Empty className="v2-user-filter-empty" title="没有匹配账号" description="请调整关键词或访问状态筛选。" />}
|
||||
renderItem={(user) => {
|
||||
const accessState = customerAccessState(user.status, user.menuKeys.length, user.vehicles.length);
|
||||
return <List.Item key={user.id} className={`v2-user-list-row is-access-${accessState.key}${accessState.attention ? ' has-attention' : ''}${selectedID === user.id && !creating ? ' is-active' : ''}`}>
|
||||
<Button
|
||||
className="v2-user-list-item"
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
aria-label={`选择客户 ${user.displayName},账号 ${user.username},${user.vehicles.length} 辆授权车`}
|
||||
aria-label={`选择客户 ${user.displayName},账号 ${user.username},${user.vehicles.length} 辆授权车,${accessState.label}`}
|
||||
aria-pressed={selectedID === user.id && !creating}
|
||||
aria-expanded={selectedID === user.id && !creating}
|
||||
onClick={() => selectCustomer(user)}
|
||||
>
|
||||
<Avatar className="v2-user-avatar" color={user.status === 'enabled' ? 'light-blue' : 'grey'} shape="square" size="small">{user.displayName.slice(0, 1)}</Avatar>
|
||||
<span className="v2-user-list-identity"><b>{user.displayName}</b><small>@{user.username}{user.customerRef ? ` · ${user.customerRef}` : ''}</small></span>
|
||||
<span className="v2-user-list-facts is-menu"><small>菜单权限</small><b>{user.menuKeys.length} 项</b></span>
|
||||
<span className="v2-user-list-facts is-menu"><small>菜单权限</small><b>{user.menuKeys.length}<i> / {customerMenus.length}</i></b></span>
|
||||
<span className="v2-user-list-facts is-vehicle"><small>车辆权限</small><b>{user.vehicles.length} 辆</b></span>
|
||||
<span className="v2-user-list-facts is-login"><small>最近登录</small><b>{formatTime(user.lastLoginAt)}</b></span>
|
||||
<span className="v2-user-list-trailing"><Tag className={`v2-user-status-tag is-${user.status}`} color={user.status === 'enabled' ? 'green' : 'grey'} type="light" size="small">{user.status === 'enabled' ? '启用' : '停用'}</Tag><IconChevronRight /></span>
|
||||
<span className="v2-user-list-trailing"><Tag className={`v2-user-status-tag is-${accessState.key}`} color={accessState.color} type="light" size="small"><span className="v2-user-status-tag-desktop">{accessState.label}</span><span className="v2-user-status-tag-mobile">{accessState.shortLabel}</span></Tag><IconChevronRight /></span>
|
||||
</Button>
|
||||
</List.Item>}
|
||||
</List.Item>;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>}
|
||||
@@ -417,7 +449,13 @@ export default function UsersPage() {
|
||||
width={mobileLayout ? '100%' : 'min(840px, 100vw)'}
|
||||
aria-label="客户账号详情"
|
||||
title={<div className="v2-user-editor-sheet-title">
|
||||
<span><strong>{creating ? '创建客户账号' : selected?.displayName}</strong><small>{creating ? '设置登录身份和最小必要权限' : <><span>@{selected?.username} · {draft.menuKeys.length} 个菜单 · {draft.vehicleGrants.length} 辆车</span><span className="v2-user-editor-last-login"> · 最近登录 {formatTime(selected?.lastLoginAt)}</span></>}</small></span>
|
||||
<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>}
|
||||
onCancel={requestCloseEditor}
|
||||
|
||||
Reference in New Issue
Block a user