mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 01:53:21 +08:00
Polish access-grant UI: no sticky typeahead overlay, flatter group cards (4.9.1-ops.8).
Suggestions only open while focused and hide exact matches; nested group admins/members drop extra frames; pending and actions show group names. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a4508a7caa
commit
e8e58924ca
6 changed files with 196 additions and 47 deletions
|
|
@ -71,6 +71,19 @@ function contactLabel(contact) {
|
|||
return `${name} · ${phone}`;
|
||||
}
|
||||
|
||||
function groupDisplayName(groupJid, groups) {
|
||||
const title = groups?.[groupJid]?.title;
|
||||
if (typeof title === 'string' && title.trim()) return title.trim();
|
||||
const raw = String(groupJid ?? '');
|
||||
const id = raw.replace(/@g\.us$/i, '');
|
||||
if (id.length > 14) return `${id.slice(0, 10)}…@g.us`;
|
||||
return raw || '未知群';
|
||||
}
|
||||
|
||||
function digitsOnly(value) {
|
||||
return String(value ?? '').replace(/[^\d]/g, '');
|
||||
}
|
||||
|
||||
function PhoneTypeahead({
|
||||
value,
|
||||
onChange,
|
||||
|
|
@ -80,16 +93,28 @@ function PhoneTypeahead({
|
|||
requirePhone = true,
|
||||
}) {
|
||||
const [query, setQuery] = React.useState(value || '');
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const blurTimer = React.useRef(null);
|
||||
React.useEffect(() => { setQuery(value || ''); }, [value]);
|
||||
React.useEffect(() => () => {
|
||||
if (blurTimer.current) window.clearTimeout(blurTimer.current);
|
||||
}, []);
|
||||
|
||||
const q = query.trim().toLowerCase();
|
||||
const queryDigits = digitsOnly(query);
|
||||
const suggestions = (contacts ?? [])
|
||||
.filter((contact) => {
|
||||
if (requirePhone && !contact.phone) return false;
|
||||
// Exact current value: hide the redundant chip that was covering the next field.
|
||||
if (contact.phone && contact.phone === queryDigits && contact.phone === digitsOnly(value)) {
|
||||
return false;
|
||||
}
|
||||
if (!q) return Boolean(contact.phone);
|
||||
return (contact.phone ?? '').includes(q.replace(/[^\d]/g, ''))
|
||||
return (contact.phone ?? '').includes(queryDigits)
|
||||
|| (contact.pushName ?? '').toLowerCase().includes(q);
|
||||
})
|
||||
.slice(0, 8);
|
||||
const showSuggestions = open && !disabled && suggestions.length > 0;
|
||||
|
||||
return h('div', { className: 'dim-accessTypeahead' },
|
||||
h('input', {
|
||||
|
|
@ -100,23 +125,34 @@ function PhoneTypeahead({
|
|||
autoCapitalize: 'none',
|
||||
autoCorrect: 'off',
|
||||
spellCheck: false,
|
||||
autoComplete: 'off',
|
||||
onFocus: () => {
|
||||
if (blurTimer.current) window.clearTimeout(blurTimer.current);
|
||||
setOpen(true);
|
||||
},
|
||||
onBlur: () => {
|
||||
blurTimer.current = window.setTimeout(() => setOpen(false), 120);
|
||||
},
|
||||
onChange: (event) => {
|
||||
setQuery(event.target.value);
|
||||
onChange(event.target.value);
|
||||
setOpen(true);
|
||||
},
|
||||
}),
|
||||
suggestions.length === 0 ? null : h('ul', { className: 'dim-accessSuggestList' },
|
||||
showSuggestions ? h('ul', { className: 'dim-accessSuggestList', role: 'listbox' },
|
||||
suggestions.map((contact) => h('li', { key: `${contact.phone}-${contact.lids?.[0] ?? ''}` },
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dim-deliveryButton',
|
||||
className: 'dim-accessSuggestItem',
|
||||
disabled: disabled || (requirePhone && !contact.phone),
|
||||
onMouseDown: (event) => event.preventDefault(),
|
||||
onClick: () => {
|
||||
if (!contact.phone) return;
|
||||
setQuery(contact.phone);
|
||||
onChange(contact.phone);
|
||||
setOpen(false);
|
||||
},
|
||||
}, contactLabel(contact))))));
|
||||
}, contactLabel(contact))))) : null);
|
||||
}
|
||||
|
||||
function MemberRows({
|
||||
|
|
@ -125,9 +161,13 @@ function MemberRows({
|
|||
contacts,
|
||||
disabled,
|
||||
onChange,
|
||||
nested = false,
|
||||
}) {
|
||||
return h('fieldset', { className: 'dim-accessScene', disabled },
|
||||
h('legend', null, title),
|
||||
return h(nested ? 'div' : 'fieldset', {
|
||||
className: nested ? 'dim-accessSubblock' : 'dim-accessScene',
|
||||
...(nested ? {} : { disabled }),
|
||||
},
|
||||
nested ? h('h4', { className: 'dim-accessSubblockTitle' }, title) : h('legend', null, title),
|
||||
h('ul', { className: 'dim-accessUserList' }, members.map((member, index) =>
|
||||
h('li', { key: `member-${index}`, className: 'dim-accessUserRow' },
|
||||
h('label', { className: 'dim-accessField dim-accessUserId' },
|
||||
|
|
@ -174,10 +214,14 @@ function AdminPhones({
|
|||
disabled,
|
||||
lockedPhone,
|
||||
onChange,
|
||||
nested = false,
|
||||
}) {
|
||||
return h('fieldset', { className: 'dim-accessScene', disabled },
|
||||
h('legend', null, title),
|
||||
help ? h('p', { className: 'dim-accessUsersEmpty' }, help) : null,
|
||||
return h(nested ? 'div' : 'fieldset', {
|
||||
className: nested ? 'dim-accessSubblock' : 'dim-accessScene',
|
||||
...(nested ? {} : { disabled }),
|
||||
},
|
||||
nested ? h('h4', { className: 'dim-accessSubblockTitle' }, title) : h('legend', null, title),
|
||||
help ? h('p', { className: 'dim-accessHint' }, help) : null,
|
||||
h('ul', { className: 'dim-accessUserList' }, phones.map((phone, index) => {
|
||||
const locked = lockedPhone && phone === lockedPhone;
|
||||
return h('li', { key: `admin-${index}`, className: 'dim-accessUserRow' },
|
||||
|
|
@ -390,9 +434,9 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
|||
.filter((entry) => entry.status === 'pending' || !entry.status)
|
||||
.map((entry) => h('li', { key: entry.id, className: 'dim-accessUserRow' },
|
||||
h('div', { className: 'dim-accessField' },
|
||||
h('span', null, entry.kind === 'group'
|
||||
? ['群聊', ' ', entry.groupJid].join('')
|
||||
: '私聊'),
|
||||
h('span', null, ...(entry.kind === 'group'
|
||||
? ['群聊 · ', groupDisplayName(entry.groupJid, draft.groups)]
|
||||
: ['私聊'])),
|
||||
h('strong', null, entry.pushName || entry.phone || entry.lid || entry.id),
|
||||
entry.unresolved
|
||||
? h('span', null, '(电话未解析,请先在联系人中确认)')
|
||||
|
|
@ -421,13 +465,16 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
|||
'尚无已知群。可在下方粘贴群 JID(…@g.us),或等群内有人 @ 机器人后自动出现。')
|
||||
: knownGroupJids.map((groupJid) => {
|
||||
const group = draft.groups[groupJid] ?? { title: '', admins: [], members: [] };
|
||||
return h('div', { key: groupJid, className: 'dim-accessScene', 'data-scene': 'group-grant' },
|
||||
h('h3', null, group.title || groupJid),
|
||||
return h('div', { key: groupJid, className: 'dim-accessGroupCard' },
|
||||
h('div', { className: 'dim-accessGroupHeading' },
|
||||
h('h3', null, groupDisplayName(groupJid, draft.groups)),
|
||||
h('code', { className: 'dim-accessGroupJid' }, groupJid)),
|
||||
h('label', { className: 'dim-accessField' },
|
||||
h('span', null, '群备注名'),
|
||||
h('span', null, '群名称'),
|
||||
h('input', {
|
||||
value: group.title ?? '',
|
||||
maxLength: 128,
|
||||
placeholder: '填写便于识别的群名称',
|
||||
onChange: (event) => setDraft((current) => ({
|
||||
...current,
|
||||
groups: {
|
||||
|
|
@ -443,6 +490,7 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
|||
contacts,
|
||||
disabled: saving,
|
||||
lockedPhone: null,
|
||||
nested: true,
|
||||
onChange: (admins) => setDraft((current) => ({
|
||||
...current,
|
||||
groups: {
|
||||
|
|
@ -456,6 +504,7 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
|||
members: group.members ?? [],
|
||||
contacts,
|
||||
disabled: saving,
|
||||
nested: true,
|
||||
onChange: (members) => setDraft((current) => ({
|
||||
...current,
|
||||
groups: {
|
||||
|
|
@ -532,6 +581,7 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
|||
}, alreadyDirect ? '已在私聊' : '加私聊') : null,
|
||||
...groupTargets.map((groupJid) => {
|
||||
const group = draft.groups[groupJid] ?? { admins: [], members: [] };
|
||||
const name = groupDisplayName(groupJid, draft.groups);
|
||||
const already = group.admins.includes(phone)
|
||||
|| group.members.some((m) => m.phone === phone);
|
||||
return h('button', {
|
||||
|
|
@ -560,9 +610,12 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
|
|||
},
|
||||
};
|
||||
});
|
||||
setFeedback({ tone: 'success', message: '已加入本群授权,记得点保存。' });
|
||||
setFeedback({
|
||||
tone: 'success',
|
||||
message: ['已加入群授权:', name, '。记得点保存。'].join(''),
|
||||
});
|
||||
},
|
||||
}, already ? '已在本群' : '加本群');
|
||||
}, [already ? '已在群:' : '加入群:', name].join(''));
|
||||
}));
|
||||
}))),
|
||||
h('fieldset', { className: 'dim-accessScene', disabled: saving },
|
||||
|
|
|
|||
|
|
@ -11,6 +11,22 @@ const EN = Object.freeze({
|
|||
'机器人设置页签': 'Bot settings tabs',
|
||||
'投递设置': 'Delivery settings',
|
||||
'访问设置': 'Access settings',
|
||||
"群聊 · ": "Group · ",
|
||||
|
||||
"。记得点保存。": ". Remember to save.",
|
||||
|
||||
"已加入群授权:": "Added group access for: ",
|
||||
|
||||
"加入群:": "Add to group: ",
|
||||
|
||||
"已在群:": "Already in group: ",
|
||||
|
||||
"填写便于识别的群名称": "Enter a readable group name",
|
||||
|
||||
"群名称": "Group name",
|
||||
|
||||
"未知群": "Unknown group",
|
||||
|
||||
"无法读取访问授权,请返回刷新后重试。": "Could not load the access grant. Go back, refresh, and try again.",
|
||||
|
||||
"正在读取分级访问设置…": "Loading graded access settings…",
|
||||
|
|
|
|||
|
|
@ -468,12 +468,22 @@ const CSS = String.raw`
|
|||
.dim-accessFeedback[data-tone="success"] { color: var(--dsw-alias-state-success-primary, #20a162); }
|
||||
.dim-accessFeedback[data-tone="error"] { color: var(--dsw-alias-state-error-primary, #d54941); }
|
||||
.dim-accessActions { display: flex; justify-content: flex-end; }
|
||||
.dim-accessTypeahead { position: relative; min-width: 0; display: grid; gap: 6px; }
|
||||
.dim-accessTypeahead { position: relative; z-index: 1; min-width: 0; display: grid; gap: 0; }
|
||||
.dim-accessTypeahead:focus-within { z-index: 6; }
|
||||
.dim-accessTypeahead > input { width: 100%; min-width: 0; height: 36px; padding: 0 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; }
|
||||
.dim-accessTypeahead > input:focus { outline: 2px solid color-mix(in srgb, var(--dsw-alias-state-business-primary, #3370ff) 28%, transparent); border-color: var(--dsw-alias-state-business-primary, #3370ff); }
|
||||
.dim-accessSuggestList { position: absolute; z-index: 4; top: calc(100% + 4px); left: 0; right: 0; margin: 0; padding: 6px; list-style: none; display: grid; gap: 4px; max-height: 220px; overflow: auto; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 8px 24px color-mix(in srgb, #1f2329 12%, transparent); }
|
||||
.dim-accessSuggestList { position: absolute; z-index: 7; top: calc(100% + 4px); left: 0; right: 0; margin: 0; padding: 4px; list-style: none; display: grid; gap: 2px; max-height: 180px; overflow: auto; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 10px 28px color-mix(in srgb, #1f2329 16%, transparent); }
|
||||
.dim-accessSuggestList li { min-width: 0; }
|
||||
.dim-accessSuggestList .dim-deliveryButton { width: 100%; justify-content: flex-start; text-align: left; white-space: normal; }
|
||||
.dim-accessSuggestItem { width: 100%; min-height: 32px; padding: 6px 9px; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-primary, #1f2329); font: inherit; font-size: 12px; text-align: left; cursor: pointer; }
|
||||
.dim-accessSuggestItem:hover, .dim-accessSuggestItem:focus-visible { background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
|
||||
.dim-accessSubblock { min-width: 0; display: grid; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--dsw-alias-border-l1, #eef0f3); }
|
||||
.dim-accessSubblockTitle { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 13px; line-height: 20px; font-weight: 620; }
|
||||
.dim-accessHint { margin: 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 17px; }
|
||||
.dim-accessGroupCard { min-width: 0; display: grid; gap: 12px; margin-top: 12px; padding: 14px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 10px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
|
||||
.dim-accessGroupHeading { min-width: 0; display: grid; gap: 2px; }
|
||||
.dim-accessGroupHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 21px; font-weight: 650; }
|
||||
.dim-accessGroupJid { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 16px; overflow-wrap: anywhere; }
|
||||
.dim-accessUserRow { position: relative; z-index: 0; overflow: visible; }
|
||||
.dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
|
||||
.dim-panel .ddt-headingCopy { display: none; }
|
||||
.dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue