polish Semi UI admin and operations workspaces
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { IconChevronRight, IconClose, IconDelete, IconPlus, IconSearch } from '@douyinfe/semi-icons';
|
||||
import { Avatar, Button, Card, Checkbox, Collapse, Empty, Input, List, SideSheet, Spin, Switch, Tabs, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconChevronRight, IconClose, IconDelete, IconPlus, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
|
||||
import { Avatar, Button, Card, Checkbox, Collapse, Empty, Input, List, Select, SideSheet, 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';
|
||||
@@ -9,6 +9,7 @@ import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
|
||||
import { PageHeader } from '../shared/PageHeader';
|
||||
import { TablePagination } from '../shared/TablePagination';
|
||||
import { VehicleCandidateList } from '../shared/VehicleCandidateList';
|
||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
|
||||
import { PanelEmpty, PanelLoading } from '../shared/AsyncState';
|
||||
@@ -71,12 +72,18 @@ export default function UsersPage() {
|
||||
const users = useQuery({ queryKey: ['admin-users'], queryFn: ({ signal }) => api.adminUsers(signal), staleTime: 10_000 });
|
||||
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 [filtersCollapsed, setFiltersCollapsed] = useState(true);
|
||||
const visibleCustomers = useMemo(() => {
|
||||
const keyword = customerKeyword.trim().toLocaleLowerCase('zh-CN');
|
||||
if (!keyword) return customers;
|
||||
return customers.filter((user) => [user.displayName, user.username, user.customerRef, user.tenantRef]
|
||||
.some((value) => value?.toLocaleLowerCase('zh-CN').includes(keyword)));
|
||||
}, [customerKeyword, customers]);
|
||||
const keyword = deferredCustomerKeyword.toLocaleLowerCase('zh-CN');
|
||||
return customers.filter((user) => {
|
||||
if (customerStatus !== 'all' && 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));
|
||||
});
|
||||
}, [customerStatus, customers, deferredCustomerKeyword]);
|
||||
const [selectedID, setSelectedID] = useState<number | null>(null);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [activeSection, setActiveSection] = useState<EditorSection>('identity');
|
||||
@@ -90,8 +97,8 @@ export default function UsersPage() {
|
||||
const [vehicleLabels, setVehicleLabels] = useState<Record<string, string>>({});
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [editingGrantVIN, setEditingGrantVIN] = useState('');
|
||||
const enabledCustomers = useMemo(() => customers.filter((user) => user.status === 'enabled').length, [customers]);
|
||||
const grantedVehicles = useMemo(() => customers.reduce((total, user) => total + user.vehicles.length, 0), [customers]);
|
||||
const enabledCustomers = useMemo(() => visibleCustomers.filter((user) => user.status === 'enabled').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] : '';
|
||||
const grantWindowInvalid = Boolean(editingGrant && (!editingGrant.validFrom || (editingGrant.validTo && editingGrant.validTo <= editingGrant.validFrom)));
|
||||
@@ -245,13 +252,58 @@ export default function UsersPage() {
|
||||
meta={<Typography.Text type="tertiary">最小权限原则</Typography.Text>}
|
||||
actions={<Button 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()}` : ''}`}
|
||||
expanded={!filtersCollapsed}
|
||||
status={users.isPending ? '正在读取账号' : `${visibleCustomers.length} / ${customers.length} 个账号`}
|
||||
statusColor={customerKeyword.trim() || customerStatus !== 'all' ? 'blue' : 'grey'}
|
||||
collapsedLabel="修改"
|
||||
onToggle={() => setFiltersCollapsed((value) => !value)}
|
||||
>
|
||||
<div className={`v2-user-filter-form${filtersCollapsed ? ' is-mobile-collapsed' : ''}`}>
|
||||
<label className="v2-user-filter-keyword">
|
||||
<span>客户账号</span>
|
||||
<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>
|
||||
<Select
|
||||
aria-labelledby="v2-user-status-filter-label"
|
||||
value={customerStatus}
|
||||
onChange={(value) => setCustomerStatus(String(value) as typeof customerStatus)}
|
||||
optionList={[
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'enabled', label: '启用账号' },
|
||||
{ value: 'disabled', label: '停用账号' }
|
||||
]}
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
className="v2-user-filter-reset"
|
||||
theme="light"
|
||||
type="tertiary"
|
||||
icon={<IconRefresh />}
|
||||
aria-label="清空账号筛选"
|
||||
disabled={!customerKeyword.trim() && customerStatus === 'all'}
|
||||
onClick={() => {
|
||||
setCustomerKeyword('');
|
||||
setCustomerStatus('all');
|
||||
}}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</div>
|
||||
</WorkspaceFilterPanel>
|
||||
<div className={`v2-user-admin-grid${creating || selected ? ' is-editor-open' : ''}`}>
|
||||
<Card className="v2-user-list" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
className="v2-user-directory-header"
|
||||
title="客户权限目录"
|
||||
description="按客户查看账号状态、菜单和车辆授权范围"
|
||||
meta={<Tag color="blue" type="light" size="small">{customers.length} 个账号</Tag>}
|
||||
description="当前范围内的账号、菜单与车辆授权摘要"
|
||||
meta={<Tag color={visibleCustomers.length === customers.length ? 'grey' : 'blue'} type="light" size="small">{visibleCustomers.length} / {customers.length} 个账号</Tag>}
|
||||
/>
|
||||
<div className="v2-user-directory-body">
|
||||
<div className="v2-user-directory-metrics" aria-label="客户账号概览">
|
||||
@@ -260,7 +312,6 @@ export default function UsersPage() {
|
||||
<span><small>车辆授权</small><strong>{grantedVehicles}</strong></span>
|
||||
</div>
|
||||
{users.isPending ? <PanelLoading className="v2-user-list-loading" title="正在加载客户账号" description="账号目录和授权摘要就绪后会自动显示。" /> : customers.length === 0 ? <PanelEmpty className="v2-user-list-empty" title="还没有客户账号" description="创建客户账号后,可在这里配置菜单和车辆范围。" action={<Button theme="solid" onClick={startCreate}>创建第一个客户账号</Button>} /> : <>
|
||||
<Input className="v2-user-list-search" aria-label="搜索客户账号" prefix={<IconSearch />} showClear value={customerKeyword} onChange={setCustomerKeyword} placeholder="搜索名称、账号或客户标识" />
|
||||
<List
|
||||
className="v2-customer-list"
|
||||
dataSource={visibleCustomers}
|
||||
|
||||
Reference in New Issue
Block a user