mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
fixed:issues#69
This commit is contained in:
parent
91e2d5962a
commit
7c5279e445
6 changed files with 254 additions and 9 deletions
|
|
@ -122,6 +122,11 @@ const EN = Object.freeze({
|
|||
'选择机器人工作区目录': 'Select bot workspace folder',
|
||||
'当前目录': 'Current folder',
|
||||
'主目录': 'Home',
|
||||
'直接输入路径': 'Enter a path directly',
|
||||
'支持 Windows 盘符、UNC 与 POSIX 绝对路径。': 'Supports Windows drives, UNC paths, and POSIX absolute paths.',
|
||||
'输入 Host 上的完整绝对路径': 'Enter a full absolute path on the Host',
|
||||
'前往': 'Go',
|
||||
'读取中…': 'Loading…',
|
||||
'正在准备目录选择器…': 'Preparing folder picker…',
|
||||
'正在读取目录…': 'Loading folders…',
|
||||
'这个目录中没有子文件夹。': 'This folder has no subfolders.',
|
||||
|
|
|
|||
|
|
@ -184,8 +184,21 @@ const CSS = String.raw`
|
|||
.dim-directoryCrumbs button { max-width: 210px; overflow: hidden; padding: 3px 5px; border: 0; border-radius: 6px; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-size: 12px; line-height: 18px; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
|
||||
.dim-directoryCrumbs button:hover:not(:disabled) { color: var(--dim-blue); background: var(--dim-blue-soft); }
|
||||
.dim-directoryCrumbs button[aria-current="page"] { color: var(--dsw-alias-label-primary, #1f2329); font-weight: 650; }
|
||||
.dim-directoryCrumbs button:focus-visible, .dim-directoryList button:focus-visible, .dim-directoryPickerActions button:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 65%, white); outline-offset: 1px; }
|
||||
.dim-directoryCrumbs button:focus-visible, .dim-directoryPathInput:focus-visible, .dim-directoryPathControl button:focus-visible, .dim-directoryList button:focus-visible, .dim-directoryPickerActions button:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 65%, white); outline-offset: 1px; }
|
||||
.dim-directoryCrumbSeparator { flex: none; font-size: 12px; }
|
||||
.dim-directoryPathForm { display: grid; gap: 7px; margin-top: 14px; }
|
||||
.dim-directoryPathMeta { min-width: 0; display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
||||
.dim-directoryPathMeta label { flex: none; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; font-weight: 650; }
|
||||
.dim-directoryPathMeta span { min-width: 0; overflow: hidden; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dim-directoryPathControl { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; gap: 8px; }
|
||||
.dim-directoryPathInput { min-width: 0; height: 38px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: 12px ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; line-height: 38px; }
|
||||
.dim-directoryPathInput::placeholder { color: var(--dsw-alias-label-tertiary, #8f959e); }
|
||||
.dim-directoryPathInput:hover:not(:disabled) { border-color: var(--dsw-alias-border-l3, #bbbfc4); }
|
||||
.dim-directoryPathInput:focus { border-color: var(--dim-blue); }
|
||||
.dim-directoryPathInput[aria-invalid="true"] { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 62%, var(--dsw-alias-border-l2, #dfe1e5)); }
|
||||
.dim-directoryPathControl button { min-height: 38px; padding: 0 14px; border: 1px solid color-mix(in srgb, var(--dim-blue) 30%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 8px; color: var(--dim-blue); background: var(--dim-blue-soft); font: inherit; font-size: 13px; font-weight: 650; cursor: pointer; }
|
||||
.dim-directoryPathControl button:hover:not(:disabled) { border-color: color-mix(in srgb, var(--dim-blue) 48%, var(--dsw-alias-border-l2, #dfe1e5)); background: color-mix(in srgb, var(--dim-blue) 13%, transparent); }
|
||||
.dim-directoryPathInput:disabled, .dim-directoryPathControl button:disabled { cursor: not-allowed; opacity: .55; }
|
||||
.dim-directoryPickerBody { min-height: 0; overflow-y: auto; padding: 14px 16px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2, #dfe1e5) transparent; }
|
||||
.dim-directoryList { display: grid; gap: 3px; margin: 0; padding: 0; list-style: none; }
|
||||
.dim-directoryList button { width: 100%; min-height: 46px; display: grid; grid-template-columns: 24px minmax(0, 1fr) 18px; align-items: center; gap: 10px; padding: 7px 11px; border: 0; border-radius: 9px; color: var(--dsw-alias-label-primary, #1f2329); background: transparent; font: inherit; text-align: left; cursor: pointer; }
|
||||
|
|
@ -279,6 +292,7 @@ const CSS = String.raw`
|
|||
.dim-directoryPicker { height: calc(100vh - 20px); min-height: 0; border-radius: 14px; }
|
||||
.dim-directoryPickerHeader { padding: 18px 17px 14px; }
|
||||
.dim-directoryPickerHeader h3 { font-size: 18px; }
|
||||
.dim-directoryPathMeta span { display: none; }
|
||||
.dim-directoryPickerBody { padding: 10px; }
|
||||
.dim-directoryPickerFooter { grid-template-columns: minmax(0, 1fr) max-content; gap: 10px; padding: 13px 14px; }
|
||||
.dim-directoryPickerNotice { grid-column: 1 / -1; grid-row: 1; text-align: left; }
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export function WorkspaceDirectoryPicker({
|
|||
const [listing, setListing] = React.useState(null);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [pathDraft, setPathDraft] = React.useState(startPath ?? '');
|
||||
const [showHidden, setShowHidden] = React.useState(false);
|
||||
const [retryKey, setRetryKey] = React.useState(0);
|
||||
const requestRef = React.useRef(0);
|
||||
|
|
@ -56,6 +57,8 @@ export function WorkspaceDirectoryPicker({
|
|||
const bodyRef = React.useRef(null);
|
||||
const titleId = React.useId();
|
||||
const noticeId = React.useId();
|
||||
const pathInputId = React.useId();
|
||||
const errorId = React.useId();
|
||||
const initialPathRef = React.useRef(startPath);
|
||||
const onPickedRef = React.useRef(onPicked);
|
||||
const onCancelRef = React.useRef(onCancel);
|
||||
|
|
@ -78,6 +81,7 @@ export function WorkspaceDirectoryPicker({
|
|||
if (request !== requestRef.current || controller.signal.aborted) return { aborted: true };
|
||||
if (bodyRef.current) bodyRef.current.scrollTop = 0;
|
||||
setListing(next);
|
||||
if (typeof next?.path === 'string') setPathDraft(next.path);
|
||||
setError(null);
|
||||
return { value: next };
|
||||
} catch (cause) {
|
||||
|
|
@ -94,6 +98,7 @@ export function WorkspaceDirectoryPicker({
|
|||
let active = true;
|
||||
setListing(null);
|
||||
setError(null);
|
||||
setPathDraft(initialPathRef.current ?? '');
|
||||
setShowHidden(false);
|
||||
dialogRef.current?.focus?.();
|
||||
const handleKeyDown = (event) => {
|
||||
|
|
@ -145,6 +150,7 @@ export function WorkspaceDirectoryPicker({
|
|||
const entries = (listing?.entries ?? []).filter((entry) => showHidden || !entry.hidden);
|
||||
const crumbs = listing ? displayCrumbs(listing) : [];
|
||||
const presentedError = saveError ?? error;
|
||||
const pathReady = listing !== null && pathDraft === listing.path;
|
||||
|
||||
const content = h('div', {
|
||||
className: 'dim-directoryPickerBackdrop',
|
||||
|
|
@ -177,7 +183,40 @@ export function WorkspaceDirectoryPicker({
|
|||
? h('span', null, '主目录')
|
||||
: (crumb.name || crumb.path)))),
|
||||
)
|
||||
: h('p', null, '正在准备目录选择器…')),
|
||||
: h('p', null, '正在准备目录选择器…'),
|
||||
h('form', {
|
||||
className: 'dim-directoryPathForm',
|
||||
onSubmit: (event) => {
|
||||
event.preventDefault();
|
||||
if (!busy && !loading && pathDraft.trim()) void loadDirectory(pathDraft);
|
||||
},
|
||||
},
|
||||
h('div', { className: 'dim-directoryPathMeta' },
|
||||
h('label', { htmlFor: pathInputId }, '直接输入路径'),
|
||||
h('span', null, '支持 Windows 盘符、UNC 与 POSIX 绝对路径。')),
|
||||
h('div', { className: 'dim-directoryPathControl' },
|
||||
h('input', {
|
||||
id: pathInputId,
|
||||
className: 'dim-directoryPathInput',
|
||||
value: pathDraft,
|
||||
placeholder: '输入 Host 上的完整绝对路径',
|
||||
'aria-label': '工作区绝对路径',
|
||||
'aria-describedby': presentedError ? errorId : undefined,
|
||||
'aria-invalid': presentedError ? 'true' : undefined,
|
||||
autoCapitalize: 'none',
|
||||
autoCorrect: 'off',
|
||||
spellCheck: false,
|
||||
maxLength: 4_096,
|
||||
disabled: busy || loading,
|
||||
onChange: (event) => {
|
||||
setPathDraft(event.target.value);
|
||||
setError(null);
|
||||
},
|
||||
}),
|
||||
h('button', {
|
||||
type: 'submit',
|
||||
disabled: busy || loading || !pathDraft.trim(),
|
||||
}, loading ? '读取中…' : '前往')))),
|
||||
h('div', { ref: bodyRef, className: 'dim-directoryPickerBody', 'aria-busy': loading },
|
||||
loading && !listing
|
||||
? h('div', { className: 'dim-directoryPickerState' },
|
||||
|
|
@ -201,7 +240,7 @@ export function WorkspaceDirectoryPicker({
|
|||
listing?.truncated
|
||||
? h('p', { className: 'dim-directoryPickerTruncated' }, '此目录的子文件夹过多,仅显示前一部分。')
|
||||
: null,
|
||||
presentedError ? h('div', { className: 'dim-directoryPickerError', role: 'alert' },
|
||||
presentedError ? h('div', { id: errorId, className: 'dim-directoryPickerError', role: 'alert' },
|
||||
h('span', null, presentedError),
|
||||
!listing && !busy ? h('button', {
|
||||
type: 'button', onClick: () => setRetryKey((value) => value + 1),
|
||||
|
|
@ -222,7 +261,7 @@ export function WorkspaceDirectoryPicker({
|
|||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dim-directoryPickerPrimary',
|
||||
disabled: busy || loading || !listing,
|
||||
disabled: busy || loading || !pathReady,
|
||||
onClick: () => listing && void onPicked(listing.path),
|
||||
}, busy ? '切换中…' : '选择此目录')))));
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue