From 5a1b72387d101c0206831772903d2f3579bd2b47 Mon Sep 17 00:00:00 2001 From: sagit <36596628+Sagit-chu@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:50:17 +0800 Subject: [PATCH] fix(frontend): polish batch actions and sharing page guidance (#157) * fix(frontend): polish batch actions and sharing page guidance * fix(frontend): add top spacing above sharing section banners * fix(frontend): allow manual and calendar expiry date input * fix(frontend): switch batch action buttons to warning tone * fix(frontend): switch batch action buttons to default tone --- vite-frontend/src/pages/forward.tsx | 2 +- vite-frontend/src/pages/group.tsx | 3 +- vite-frontend/src/pages/node.tsx | 2 +- vite-frontend/src/pages/panel-sharing.tsx | 89 ++++++- vite-frontend/src/pages/tunnel.tsx | 2 +- vite-frontend/src/pages/user.tsx | 29 ++- .../src/shadcn-bridge/heroui/date-picker.tsx | 239 +++++++++++++++--- 7 files changed, 302 insertions(+), 64 deletions(-) diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 5e28eb7..cfa0684 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -1584,7 +1584,7 @@ export default function ForwardPage() { + + ) : (
{shares.map((share) => ( @@ -507,11 +542,30 @@ export default function PanelSharingPage() { - + + + Consumer + + 远程节点 +
+ } + > - -
+ +
+
+

+ Consumer 接入 +

+

+ 导入远程节点后,可在这里查看端口占用和同步状态。 +

+
{remoteUsageLoading ? ( -
加载中...
+
加载中...
) : remoteUsageNodes.length === 0 ? ( -
-

暂无远程节点占用记录。

-

+

+

+ 暂无远程节点占用记录 +

+

导入远程节点并创建隧道后,这里会显示远端端口占用情况。

+
+ +
) : (
diff --git a/vite-frontend/src/pages/tunnel.tsx b/vite-frontend/src/pages/tunnel.tsx index 27f1a50..cd53f25 100644 --- a/vite-frontend/src/pages/tunnel.tsx +++ b/vite-frontend/src/pages/tunnel.tsx @@ -678,7 +678,7 @@ export default function TunnelPage() {
+
+ + {batchTunnelSelections.size > 0 && ( + + 已选 {batchTunnelSelections.size} 个隧道 + + )} +
@@ -1434,7 +1438,6 @@ export default function UserPage() { 12 || day < 1 || day > 31) { + return false; + } + const candidate = new Date(year, month - 1, day); + + return ( + candidate.getFullYear() === year && + candidate.getMonth() === month - 1 && + candidate.getDate() === day + ); +} + +function parseDateText(value: string) { + const trimmed = value.trim(); + + if (!trimmed) { + return null; + } + + const matched = trimmed.match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})$/); + + if (!matched) { + return null; + } + + const year = Number(matched[1]); + const month = Number(matched[2]); + const day = Number(matched[3]); + + if (!isValidCalendarDate(year, month, day)) { + return null; + } + + return { day, month, year }; +} + export interface DatePickerProps extends FieldMetaProps { className?: string; isDisabled?: boolean; @@ -39,9 +84,105 @@ export function DatePicker({ isRequired, label, onChange, + showMonthAndYearPickers, value, }: DatePickerProps) { const id = React.useId(); + const nativePickerRef = React.useRef(null); + const formattedValue = React.useMemo(() => formatDateValue(value), [value]); + const [textValue, setTextValue] = React.useState(formattedValue); + + React.useEffect(() => { + setTextValue(formattedValue); + }, [formattedValue]); + + const shouldUseTextInput = Boolean(showMonthAndYearPickers); + + const notifyNativeDateChange = (rawValue: string) => { + if (!onChange) { + return; + } + + if (!rawValue) { + onChange(null); + + return; + } + + const [yearText, monthText, dayText] = rawValue.split("-"); + const year = Number(yearText); + const month = Number(monthText); + const day = Number(dayText); + + if (!isValidCalendarDate(year, month, day)) { + onChange(null); + + return; + } + + onChange({ day, month, year }); + }; + + const notifyTextDateChange = (rawValue: string) => { + if (!onChange) { + return; + } + + if (!rawValue.trim()) { + onChange(null); + + return; + } + + const parsed = parseDateText(rawValue); + + if (parsed) { + onChange(parsed); + } + }; + + const commitTextInput = () => { + const parsed = parseDateText(textValue); + + if (parsed) { + setTextValue(formatDateValue(parsed)); + onChange?.(parsed); + + return; + } + + if (!textValue.trim()) { + onChange?.(null); + + return; + } + + setTextValue(formattedValue); + }; + + const openNativePicker = () => { + if (isDisabled) { + return; + } + const picker = nativePickerRef.current; + + if (!picker) { + return; + } + + const pickerWithShow = picker as HTMLInputElement & { + showPicker?: () => void; + }; + + if (typeof pickerWithShow.showPicker === "function") { + pickerWithShow.showPicker(); + + return; + } + + picker.focus(); + picker.click(); + }; return ( - { - if (!onChange) { - return; - } + {shouldUseTextInput ? ( +
+ { + const nextValue = event.target.value; - if (!event.target.value) { - onChange(null); + setTextValue(nextValue); + notifyTextDateChange(nextValue); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + commitTextInput(); + } + }} + /> + + { + const nextValue = event.target.value; - return; - } - - const [yearText, monthText, dayText] = event.target.value.split("-"); - const year = Number(yearText); - const month = Number(monthText); - const day = Number(dayText); - - if ( - !Number.isFinite(year) || - !Number.isFinite(month) || - !Number.isFinite(day) - ) { - onChange(null); - - return; - } - - onChange({ day, month, year }); - }} - /> + setTextValue(nextValue); + notifyNativeDateChange(nextValue); + }} + /> +
+ ) : ( + { + notifyNativeDateChange(event.target.value); + }} + /> + )}
); }