feat: 自制下拉列表——文件夹分组标题(📁+小字)与1px细分隔线,弃用原生select/optgroup

This commit is contained in:
Aisland
2026-08-18 12:37:40 +08:00
parent a84cb12c5f
commit e1b082796b
4 changed files with 101 additions and 54 deletions
+61 -28
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { Terminal } from 'xterm'
import 'xterm/css/xterm.css'
import MarkdownIt from 'markdown-it'
@@ -1245,6 +1245,51 @@ function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
)
}
/** 自制下拉列表(原生 select 无法美化):文件夹分组标题 + 1px 细分隔线 + 选项列表 */
function SelectPop(props: {
value: string | null
groups: { title: string; items: { id: string; label: string; isCurrent?: boolean }[] }[]
placeholder?: string
onChange: (id: string) => void
}) {
const [open, setOpen] = useState(false)
const flat = props.groups.flatMap((g) => g.items)
const selected = flat.find((i) => i.id === props.value)
return (
<div className="dsh-wt_select">
<button type="button" className="dsh-wt_selectBtn" onClick={() => setOpen((v) => !v)}>
<span className={'dsh-wt_selectVal' + (selected ? '' : ' dsh-wt_selectPh')}>{selected?.label ?? props.placeholder ?? ''}</span>
<span className="dsh-wt_selectCaret" aria-hidden>▾</span>
</button>
{open && (
<div className="dsh-wt_selectList">
{props.groups.map((g, gi) => (
<Fragment key={g.title || 'g' + gi}>
{g.title && (
<>
<div className="dsh-wt_selectDivider" />
<div className="dsh-wt_selectGroup">📁 {g.title}</div>
</>
)}
{g.items.map((it) => (
<button
key={it.id}
type="button"
className={'dsh-wt_selectItem' + (it.id === props.value ? ' dsh-wt_selectItemOn' : '')}
onClick={() => { props.onChange(it.id); setOpen(false) }}
>
<span className="dsh-wt_selectItemTitle">{it.label}</span>
{it.isCurrent && <span className="dsh-wt_selectCurrent">{T('custom.sessionCurrent')}</span>}
</button>
))}
</Fragment>
))}
</div>
)}
</div>
)
}
/** 自定义窗口:居中对话框。两种模式:新建专属会话 / 发送到已有会话(默认当前会话)。 */
function CustomPane() {
const custom = splitEnv?.custom
@@ -1320,37 +1365,25 @@ function CustomPane() {
{mode === 'existing' && (
<div className="dsh-wt_customRow">
<span className="dsh-wt_customLabel">{T('custom.session')}</span>
<select
className="dsh-wt_customSelect"
value={sessionId ?? ''}
onChange={(e) => setSessionId(e.target.value)}
>
{sessionGroups.map((g) => g.title
? (
<optgroup key={g.title} label={g.title}>
{g.sessions.map((s) => (
<option key={s.id} value={s.id}>{s.title}{s.isCurrent ? ' ' + T('custom.sessionCurrent') : ''}</option>
))}
</optgroup>
)
: g.sessions.map((s) => (
<option key={s.id} value={s.id}>{s.title}{s.isCurrent ? ' ' + T('custom.sessionCurrent') : ''}</option>
)),
)}
</select>
<SelectPop
value={sessionId}
groups={sessionGroups.map((g) => ({
title: g.title,
items: g.sessions.map((s) => ({ id: s.id, label: s.title, isCurrent: s.isCurrent })),
}))}
placeholder={T('custom.session')}
onChange={setSessionId}
/>
</div>
)}
<div className="dsh-wt_customRow">
<span className="dsh-wt_customLabel">{T('custom.project')}</span>
<select
className="dsh-wt_customSelect"
value={projectId ?? ''}
onChange={(e) => setProjectId(e.target.value)}
>
{projects.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
<SelectPop
value={projectId}
groups={[{ title: '', items: projects.map((p) => ({ id: p.id, label: p.name })) }]}
placeholder={T('custom.project')}
onChange={setProjectId}
/>
</div>
<button
type="button"
+15
View File
@@ -205,6 +205,21 @@ export const css = [
'.dsh-wt_customLabel{flex:none;font-size:11.5px;color:var(--dsw-alias-label-secondary,#9aa4b2)}',
'.dsh-wt_customSelect{flex:1;min-width:0;padding:5px 8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;outline:none}',
'.dsh-wt_customSelect option{background-color:var(--dsw-alias-bg-base,#0b0e14);color:var(--dsw-alias-label-primary,#e6e8eb)}',
// 自制下拉(文件夹分组 + 细分隔线)
'.dsh-wt_select{position:relative;flex:1;min-width:0}',
'.dsh-wt_selectBtn{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;padding:5px 8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;cursor:pointer}',
'.dsh-wt_selectBtn:hover{border-color:var(--dsw-alias-border-l2,#3a4150)}',
'.dsh-wt_selectVal{flex:1;min-width:0;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
'.dsh-wt_selectPh{color:var(--dsw-alias-label-tertiary,#6e7683)}',
'.dsh-wt_selectCaret{flex:none;color:var(--dsw-alias-label-tertiary,#6e7683);font-size:10px;line-height:1}',
'.dsh-wt_selectList{margin-top:4px;max-height:180px;overflow:auto;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:8px;background:var(--dsw-alias-bg-base,#0b0e14);padding:4px;box-shadow:var(--dsw-shadow-lv2,0 8px 24px rgba(0,0,0,.4))}',
'.dsh-wt_selectItem{display:flex;align-items:center;gap:6px;width:100%;padding:5px 8px;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:17px;cursor:pointer;text-align:left}',
'.dsh-wt_selectItem:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
'.dsh-wt_selectItemOn{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06));color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
'.dsh-wt_selectItemTitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
'.dsh-wt_selectCurrent{flex:none;font-size:10px;color:var(--dsw-alias-label-tertiary,#6e7683)}',
'.dsh-wt_selectDivider{height:1px;background:var(--dsw-alias-border-l1,#262b36);margin:4px 2px}',
'.dsh-wt_selectGroup{display:flex;align-items:center;gap:5px;padding:2px 8px 1px;font-size:10px;letter-spacing:.04em;color:var(--dsw-alias-label-tertiary,#6e7683);user-select:none}',
'.dsh-wt_customSend{align-self:flex-end;padding:6px 14px;border:none;border-radius:6px;background:var(--dsw-alias-state-accent-primary,#4f8ef7);color:#fff;font:inherit;font-size:12px;cursor:pointer}',
'.dsh-wt_customSend:disabled{opacity:.55;cursor:default}',
'.dsh-wt_customFail{font-size:11px;color:var(--dsw-alias-state-danger,#e5484d);margin:0}',