迭代 ONE-OS 多原型:统一标注壳与操作规范,租赁明细/合同/提车应收款增强,新增任务工单,同步导航与合包页面。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-07-12 22:44:21 +08:00
parent 824512a224
commit 131b963e4a
317 changed files with 38569 additions and 12009 deletions

View File

@@ -0,0 +1,263 @@
import React, { useMemo, useState } from 'react';
import { Dropdown } from 'antd';
import type { MenuProps } from 'antd';
import {
ArrowRightLeft,
Car,
CircleDollarSign,
Eye,
FileText,
MoreHorizontal,
OctagonX,
Pencil,
RefreshCw,
Trash2,
Undo2,
Upload,
Users,
type LucideIcon,
} from 'lucide-react';
export type OperationActionItem = {
key: string;
label: string;
onClick: () => void;
danger?: boolean;
disabled?: boolean;
hidden?: boolean;
icon?: LucideIcon;
};
const MORE_ACTION_ICONS: Record<string, LucideIcon> = {
edit: Pencil,
del: Trash2,
addVehicle: Car,
renew: RefreshCw,
authorized: FileText,
extraFee: CircleDollarSign,
toTripartite: Users,
terminate: OctagonX,
withdraw: Undo2,
toFormal: ArrowRightLeft,
stampSupplement: Upload,
uploadStamped: Upload,
};
function renderMenuItemLabel(item: OperationActionItem) {
const Icon = item.icon ?? MORE_ACTION_ICONS[item.key];
return (
<span
className={[
'vm-op-menu-item',
item.danger ? 'vm-op-menu-item--danger' : '',
].filter(Boolean).join(' ')}
>
{Icon ? (
<Icon className="vm-op-menu-item__icon" aria-hidden strokeWidth={1.75} />
) : (
<span className="vm-op-menu-item__icon-spacer" aria-hidden />
)}
<span className="vm-op-menu-item__label">{item.label}</span>
</span>
);
}
export type OperationPrimaryAction = {
onClick: () => void;
label?: string;
hidden?: boolean;
disabled?: boolean;
};
export type OperationActionsProps = {
/** 详情:固定外显主操作 */
view?: OperationPrimaryAction;
/**
* @deprecated 编辑已收入「更多」下拉,请放入 more 数组
*/
edit?: OperationPrimaryAction;
/** 其余操作(含编辑)收入「更多」下拉 */
more?: OperationActionItem[];
/** 更多按钮文案,默认「更多」 */
moreLabel?: string;
className?: string;
annotationId?: string;
};
function ViewIcon() {
return <Eye className="vm-op-action__icon" aria-hidden />;
}
function MoreIcon() {
return <MoreHorizontal className="vm-op-action__icon" aria-hidden />;
}
const PrimaryActionButton = React.forwardRef<
HTMLButtonElement,
{
icon: 'view' | 'more';
label: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
disabled?: boolean;
ariaLabel: string;
ariaHasPopup?: boolean;
className?: string;
}
>(function PrimaryActionButton(
{
icon,
label,
onClick,
disabled,
ariaLabel,
ariaHasPopup,
className,
},
ref,
) {
return (
<button
ref={ref}
type="button"
className={['vm-op-action', className].filter(Boolean).join(' ')}
aria-label={ariaLabel}
aria-haspopup={ariaHasPopup ? 'menu' : undefined}
disabled={disabled}
onClick={onClick}
>
{icon === 'view' ? <ViewIcon /> : <MoreIcon />}
<span className="vm-op-action__label">{label}</span>
</button>
);
});
function mergeMoreItems(
more: OperationActionItem[],
edit?: OperationPrimaryAction,
): OperationActionItem[] {
const visibleMore = more.filter((item) => !item.hidden);
if (!edit || edit.hidden) return visibleMore;
const hasEdit = visibleMore.some((item) => item.key === 'edit' || item.label === '编辑');
if (hasEdit) return visibleMore;
return [
{
key: 'edit',
label: edit.label || '编辑',
onClick: edit.onClick,
disabled: edit.disabled,
},
...visibleMore,
];
}
export function OperationActions({
view,
edit,
more = [],
moreLabel = '更多',
className,
annotationId,
}: OperationActionsProps) {
const [moreOpen, setMoreOpen] = useState(false);
const visibleMore = useMemo(
() => mergeMoreItems(more, edit),
[more, edit],
);
const menuItems: MenuProps['items'] = useMemo(() => {
const items: MenuProps['items'] = [];
visibleMore.forEach((item, index) => {
if (item.danger && index > 0 && !visibleMore[index - 1]?.danger) {
items.push({ type: 'divider' });
}
items.push({
key: item.key,
label: renderMenuItemLabel(item),
danger: item.danger,
disabled: item.disabled,
className: item.danger ? 'vm-op-dropdown-menu-item--danger' : undefined,
});
});
return items;
}, [visibleMore]);
const showView = !!(view && !view.hidden);
const showMore = visibleMore.length > 0;
if (!showView && !showMore) {
return <span className="vm-operation-actions vm-operation-actions--empty">-</span>;
}
const handleMenuClick: MenuProps['onClick'] = ({ key, domEvent }) => {
domEvent.stopPropagation();
const target = visibleMore.find((item) => item.key === key);
if (target && !target.disabled) target.onClick();
};
return (
<div
className={['vm-row-actions', 'vm-operation-actions', className].filter(Boolean).join(' ')}
data-annotation-id={annotationId}
onClick={(event) => event.stopPropagation()}
>
{showView ? (
<PrimaryActionButton
icon="view"
label={view?.label || '详情'}
ariaLabel={view?.label || '详情'}
disabled={view?.disabled}
onClick={(event) => {
event.stopPropagation();
if (!view?.disabled) view!.onClick();
}}
/>
) : null}
{showMore ? (
<Dropdown
menu={{
items: menuItems,
onClick: handleMenuClick,
className: 'vm-op-dropdown-menu',
}}
trigger={['click']}
placement="bottomRight"
getPopupContainer={() => document.body}
overlayClassName="vm-op-dropdown"
open={moreOpen}
onOpenChange={setMoreOpen}
align={{ offset: [0, 4] }}
>
<PrimaryActionButton
icon="more"
label={moreLabel}
ariaLabel={moreLabel + '操作'}
ariaHasPopup
className={moreOpen ? 'vm-op-action--open' : undefined}
/>
</Dropdown>
) : null}
</div>
);
}
/**
* 从扁平操作列表中拆分出 view / more编辑收入 more
*/
export function splitOperationActions(items: OperationActionItem[]): {
view?: OperationPrimaryAction;
more: OperationActionItem[];
} {
const visible = items.filter((item) => !item.hidden);
const viewItem = visible.find((item) => item.label === '详情' || item.label === '查看' || item.key === 'view');
const more = visible.filter((item) => item !== viewItem);
return {
view: viewItem
? {
onClick: viewItem.onClick,
label: viewItem.label === '查看' ? '详情' : viewItem.label,
disabled: viewItem.disabled,
}
: undefined,
more,
};
}