refactor: converge account and access workspaces on semi ui

This commit is contained in:
lingniu
2026-07-19 15:24:03 +08:00
parent fb105a67c8
commit 81532d91b7
7 changed files with 247 additions and 15 deletions

View File

@@ -15,6 +15,7 @@ import { WorkspaceConfirmDialog } from '../shared/WorkspaceDialog';
import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { WorkspaceSideSheet, type WorkspaceSideSheetBadgeColor } from '../shared/WorkspaceSideSheet';
import { WorkspaceDateTimeField } from '../shared/WorkspaceDateTimeField';
import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
import { PanelEmpty, PanelLoading } from '../shared/AsyncState';
@@ -149,6 +150,7 @@ export default function UsersPage() {
const [vehicleLabels, setVehicleLabels] = useState<Record<string, string>>({});
const [feedback, setFeedback] = useState('');
const [editingGrantVIN, setEditingGrantVIN] = useState('');
const [grantEndEnabled, setGrantEndEnabled] = useState(false);
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 readyCustomers = useMemo(() => visibleCustomers.filter((user) => customerAccessState(user.status, user.menuKeys.length, user.vehicles.length).key === 'ready').length, [visibleCustomers]);
@@ -186,7 +188,11 @@ export default function UsersPage() {
];
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)));
const grantWindowInvalid = Boolean(editingGrant && (
!editingGrant.validFrom
|| (grantEndEnabled && !editingGrant.validTo)
|| (editingGrant.validTo && editingGrant.validTo <= editingGrant.validFrom)
));
const editorVisible = creating || Boolean(selected);
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
const vehicleComposerVisible = !mobileLayout || creating || draft.vehicleGrants.length === 0 || vehicleComposerOpen;
@@ -272,6 +278,7 @@ export default function UsersPage() {
setVehicleLabels({});
setFeedback('');
setEditingGrantVIN('');
setGrantEndEnabled(false);
setConfirmation(null);
};
const closeEditor = () => {
@@ -288,6 +295,7 @@ export default function UsersPage() {
setVehicleLabels({});
setFeedback('');
setEditingGrantVIN('');
setGrantEndEnabled(false);
setConfirmation(null);
};
const requestCloseEditor = () => {
@@ -311,6 +319,7 @@ export default function UsersPage() {
setVehicleComposerOpen(user.vehicles.length === 0);
setFeedback('');
setEditingGrantVIN('');
setGrantEndEnabled(false);
};
const toggleVIN = (vin: string) => {
const isAssigned = assigned.has(vin);
@@ -327,7 +336,10 @@ export default function UsersPage() {
setFeedback(isAssigned
? `${vehicleLabels[vin] || vin} 已从授权草稿移除,保存后生效`
: `${vehicleLabels[vin] || vin} 已加入授权草稿,保存后生效`);
if (editingGrantVIN === vin) setEditingGrantVIN('');
if (editingGrantVIN === vin) {
setEditingGrantVIN('');
setGrantEndEnabled(false);
}
};
const clearVehicleGrants = () => {
const removedCount = draft.vehicleGrants.length;
@@ -336,12 +348,22 @@ export default function UsersPage() {
setAssignedPage(1);
setVehicleComposerOpen(true);
setEditingGrantVIN('');
setGrantEndEnabled(false);
setFeedback(`已从草稿移除 ${removedCount} 辆车,保存后生效`);
setConfirmation(null);
};
const updateGrant = (vin: string, patch: Partial<CustomerVehicleGrantInput>) => setDraft((value) => ({
...value, vehicleGrants: value.vehicleGrants.map((grant) => grant.vin === vin ? { ...grant, ...patch } : grant)
}));
const openGrantEditor = (vin: string) => {
const grant = draft.vehicleGrants.find((item) => item.vin === vin);
setGrantEndEnabled(Boolean(grant?.validTo));
setEditingGrantVIN(vin);
};
const closeGrantEditor = () => {
setEditingGrantVIN('');
setGrantEndEnabled(false);
};
const addBulkVINs = () => {
const next = bulkVINs.split(/[\s,;]+/).map((value) => value.trim().toUpperCase()).filter(Boolean);
setDraft((value) => {
@@ -364,7 +386,7 @@ export default function UsersPage() {
const invalidGrant = draft.vehicleGrants.find((grant) => !grant.validFrom || (grant.validTo && grant.validTo <= grant.validFrom));
if (invalidGrant) {
setActiveSection('vehicles');
setEditingGrantVIN(invalidGrant.vin);
openGrantEditor(invalidGrant.vin);
setFeedback('请检查车辆授权有效期:停用时间必须晚于启用时间');
return;
}
@@ -684,7 +706,7 @@ export default function UsersPage() {
/>
</div>
{visibleAssignedGrants.length ? <div className="v2-assigned-vins">{visibleAssignedGrants.map((grant) => { const plate = vehicleLabels[grant.vin]; return <Card key={grant.vin} className={`v2-assigned-vehicle-card${mobileLayout ? ' is-mobile-compact' : ''}`} bodyStyle={{ padding: 0 }}>
<header><span><b>{plate || '未登记车牌'}</b><small>{grant.vin}</small></span><span className="v2-assigned-vehicle-actions"><Button theme="light" type="tertiary" size="small" aria-haspopup="dialog" aria-controls="v2-user-grant-window" aria-label={`调整 ${plate || grant.vin} 有效期`} onClick={() => setEditingGrantVIN(grant.vin)}></Button><Button theme="borderless" type="tertiary" size="small" aria-label={`移除 ${plate || grant.vin}`} icon={<IconDelete />} onClick={() => toggleVIN(grant.vin)} /></span></header>
<header><span><b>{plate || '未登记车牌'}</b><small>{grant.vin}</small></span><span className="v2-assigned-vehicle-actions"><Button theme="light" type="tertiary" size="small" aria-haspopup="dialog" aria-controls="v2-user-grant-window" aria-label={`调整 ${plate || grant.vin} 有效期`} onClick={() => openGrantEditor(grant.vin)}></Button><Button theme="borderless" type="tertiary" size="small" aria-label={`移除 ${plate || grant.vin}`} icon={<IconDelete />} onClick={() => toggleVIN(grant.vin)} /></span></header>
<div className="v2-assigned-vehicle-interval" aria-label={`授权有效期:启用 ${formatGrantTime(grant.validFrom)};停用 ${grant.validTo ? formatGrantTime(grant.validTo) : '持续有效'}`}><span><small></small><strong>{formatGrantTime(grant.validFrom)}</strong></span><i aria-hidden="true"></i><span><small></small><strong>{grant.validTo ? formatGrantTime(grant.validTo) : '持续有效'}</strong></span></div>
</Card>; })}</div> : <Empty className="v2-user-assigned-filter-empty" title="没有匹配的授权车辆" description="请更换车牌或 VIN 关键词。" />}
<div className="v2-assigned-pagination">
@@ -728,19 +750,30 @@ export default function UsersPage() {
{ label: '启用', value: formatGrantTime(editingGrant.validFrom), detail: '授权开始' },
{ label: '停用', value: editingGrant.validTo ? formatGrantTime(editingGrant.validTo) : '持续有效', detail: editingGrant.validTo ? '授权结束' : '未设置结束时间', tone: editingGrant.validTo ? 'warning' : 'success' }
] : []}
onCancel={() => setEditingGrantVIN('')}
onCancel={closeGrantEditor}
footerNote="完成后仍需保存账号权限才会生效"
primaryAction={{ label: '完成', onClick: () => setEditingGrantVIN(''), disabled: grantWindowInvalid }}
primaryAction={{ label: '完成', onClick: closeGrantEditor, disabled: grantWindowInvalid }}
>
{editingGrant ? <div className="v2-user-grant-form">
<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>
<label><span></span><WorkspaceDateTimeField ariaLabel={`${editingGrantPlate || editingGrant.vin} 启用时间`} value={editingGrant.validFrom} onChange={(value) => updateGrant(editingGrant.vin, { validFrom: value })} /></label>
<div className="v2-user-grant-policy">
<span><strong></strong><small>{grantEndEnabled ? '关闭后必须指定停用时间' : '保持开启则不自动收回权限'}</small></span>
<Switch
aria-label={`${editingGrantPlate || editingGrant.vin} 持续有效`}
checked={!grantEndEnabled}
onChange={(checked) => {
setGrantEndEnabled(!checked);
if (checked) updateGrant(editingGrant.vin, { validTo: '' });
}}
/>
</div>
{grantEndEnabled ? <label><span></span><WorkspaceDateTimeField ariaLabel={`${editingGrantPlate || editingGrant.vin} 停用时间`} value={editingGrant.validTo} onChange={(value) => updateGrant(editingGrant.vin, { validTo: value })} placeholder="选择停用日期和时间" showClear /></label> : null}
</div>
</div>
{grantWindowInvalid ? <p role="alert"></p> : <p></p>}
{grantWindowInvalid ? <p role="alert"></p> : <p>{grantEndEnabled ? '到达停用时间后,客户将不再看到该车数据。' : '当前授权持续有效,不会自动收回。'}</p>}
</div> : null}
</WorkspaceSideSheet>
<WorkspaceConfirmDialog