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:
oliver 2026-09-05 17:20:53 +08:00
parent a4508a7caa
commit e8e58924ca
6 changed files with 196 additions and 47 deletions

View file

@ -571,7 +571,7 @@ var React24 = __toESM(require("react"), 1);
// package.json // package.json
var package_default = { var package_default = {
name: "dsh-im-ops", name: "dsh-im-ops",
version: "4.9.1-ops.7", version: "4.9.1-ops.8",
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 \u2014 access/session policies owned here.", description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 \u2014 access/session policies owned here.",
keywords: [ keywords: [
"deepseek-harness", "deepseek-harness",
@ -852,6 +852,14 @@ var EN = Object.freeze({
"\u673A\u5668\u4EBA\u8BBE\u7F6E\u9875\u7B7E": "Bot settings tabs", "\u673A\u5668\u4EBA\u8BBE\u7F6E\u9875\u7B7E": "Bot settings tabs",
"\u6295\u9012\u8BBE\u7F6E": "Delivery settings", "\u6295\u9012\u8BBE\u7F6E": "Delivery settings",
"\u8BBF\u95EE\u8BBE\u7F6E": "Access settings", "\u8BBF\u95EE\u8BBE\u7F6E": "Access settings",
"\u7FA4\u804A \xB7 ": "Group \xB7 ",
"\u3002\u8BB0\u5F97\u70B9\u4FDD\u5B58\u3002": ". Remember to save.",
"\u5DF2\u52A0\u5165\u7FA4\u6388\u6743\uFF1A": "Added group access for: ",
"\u52A0\u5165\u7FA4\uFF1A": "Add to group: ",
"\u5DF2\u5728\u7FA4\uFF1A": "Already in group: ",
"\u586B\u5199\u4FBF\u4E8E\u8BC6\u522B\u7684\u7FA4\u540D\u79F0": "Enter a readable group name",
"\u7FA4\u540D\u79F0": "Group name",
"\u672A\u77E5\u7FA4": "Unknown group",
"\u65E0\u6CD5\u8BFB\u53D6\u8BBF\u95EE\u6388\u6743\uFF0C\u8BF7\u8FD4\u56DE\u5237\u65B0\u540E\u91CD\u8BD5\u3002": "Could not load the access grant. Go back, refresh, and try again.", "\u65E0\u6CD5\u8BFB\u53D6\u8BBF\u95EE\u6388\u6743\uFF0C\u8BF7\u8FD4\u56DE\u5237\u65B0\u540E\u91CD\u8BD5\u3002": "Could not load the access grant. Go back, refresh, and try again.",
"\u6B63\u5728\u8BFB\u53D6\u5206\u7EA7\u8BBF\u95EE\u8BBE\u7F6E\u2026": "Loading graded access settings\u2026", "\u6B63\u5728\u8BFB\u53D6\u5206\u7EA7\u8BBF\u95EE\u8BBE\u7F6E\u2026": "Loading graded access settings\u2026",
"\u52A0\u672C\u7FA4": "Add to group", "\u52A0\u672C\u7FA4": "Add to group",
@ -12580,6 +12588,17 @@ function contactLabel(contact) {
const phone = contact.phone || "\u5F85\u8865\u7535\u8BDD"; const phone = contact.phone || "\u5F85\u8865\u7535\u8BDD";
return `${name2} \xB7 ${phone}`; return `${name2} \xB7 ${phone}`;
} }
function groupDisplayName(groupJid, groups) {
const title = groups?.[groupJid]?.title;
if (typeof title === "string" && title.trim()) return title.trim();
const raw = String(groupJid ?? "");
const id5 = raw.replace(/@g\.us$/i, "");
if (id5.length > 14) return `${id5.slice(0, 10)}\u2026@g.us`;
return raw || "\u672A\u77E5\u7FA4";
}
function digitsOnly(value) {
return String(value ?? "").replace(/[^\d]/g, "");
}
function PhoneTypeahead({ function PhoneTypeahead({
value, value,
onChange, onChange,
@ -12589,15 +12608,25 @@ function PhoneTypeahead({
requirePhone = true requirePhone = true
}) { }) {
const [query, setQuery] = React21.useState(value || ""); const [query, setQuery] = React21.useState(value || "");
const [open, setOpen] = React21.useState(false);
const blurTimer = React21.useRef(null);
React21.useEffect(() => { React21.useEffect(() => {
setQuery(value || ""); setQuery(value || "");
}, [value]); }, [value]);
React21.useEffect(() => () => {
if (blurTimer.current) window.clearTimeout(blurTimer.current);
}, []);
const q = query.trim().toLowerCase(); const q = query.trim().toLowerCase();
const queryDigits = digitsOnly(query);
const suggestions = (contacts ?? []).filter((contact) => { const suggestions = (contacts ?? []).filter((contact) => {
if (requirePhone && !contact.phone) return false; if (requirePhone && !contact.phone) return false;
if (contact.phone && contact.phone === queryDigits && contact.phone === digitsOnly(value)) {
return false;
}
if (!q) return Boolean(contact.phone); if (!q) return Boolean(contact.phone);
return (contact.phone ?? "").includes(q.replace(/[^\d]/g, "")) || (contact.pushName ?? "").toLowerCase().includes(q); return (contact.phone ?? "").includes(queryDigits) || (contact.pushName ?? "").toLowerCase().includes(q);
}).slice(0, 8); }).slice(0, 8);
const showSuggestions = open && !disabled && suggestions.length > 0;
return h2( return h2(
"div", "div",
{ className: "dim-accessTypeahead" }, { className: "dim-accessTypeahead" },
@ -12609,29 +12638,40 @@ function PhoneTypeahead({
autoCapitalize: "none", autoCapitalize: "none",
autoCorrect: "off", autoCorrect: "off",
spellCheck: false, spellCheck: false,
autoComplete: "off",
onFocus: () => {
if (blurTimer.current) window.clearTimeout(blurTimer.current);
setOpen(true);
},
onBlur: () => {
blurTimer.current = window.setTimeout(() => setOpen(false), 120);
},
onChange: (event) => { onChange: (event) => {
setQuery(event.target.value); setQuery(event.target.value);
onChange(event.target.value); onChange(event.target.value);
setOpen(true);
} }
}), }),
suggestions.length === 0 ? null : h2( showSuggestions ? h2(
"ul", "ul",
{ className: "dim-accessSuggestList" }, { className: "dim-accessSuggestList", role: "listbox" },
suggestions.map((contact) => h2( suggestions.map((contact) => h2(
"li", "li",
{ key: `${contact.phone}-${contact.lids?.[0] ?? ""}` }, { key: `${contact.phone}-${contact.lids?.[0] ?? ""}` },
h2("button", { h2("button", {
type: "button", type: "button",
className: "dim-deliveryButton", className: "dim-accessSuggestItem",
disabled: disabled || requirePhone && !contact.phone, disabled: disabled || requirePhone && !contact.phone,
onMouseDown: (event) => event.preventDefault(),
onClick: () => { onClick: () => {
if (!contact.phone) return; if (!contact.phone) return;
setQuery(contact.phone); setQuery(contact.phone);
onChange(contact.phone); onChange(contact.phone);
setOpen(false);
} }
}, contactLabel(contact)) }, contactLabel(contact))
)) ))
) ) : null
); );
} }
function MemberRows({ function MemberRows({
@ -12639,12 +12679,16 @@ function MemberRows({
members, members,
contacts, contacts,
disabled, disabled,
onChange onChange,
nested = false
}) { }) {
return h2( return h2(
"fieldset", nested ? "div" : "fieldset",
{ className: "dim-accessScene", disabled }, {
h2("legend", null, title), className: nested ? "dim-accessSubblock" : "dim-accessScene",
...nested ? {} : { disabled }
},
nested ? h2("h4", { className: "dim-accessSubblockTitle" }, title) : h2("legend", null, title),
h2("ul", { className: "dim-accessUserList" }, members.map((member, index) => h2( h2("ul", { className: "dim-accessUserList" }, members.map((member, index) => h2(
"li", "li",
{ key: `member-${index}`, className: "dim-accessUserRow" }, { key: `member-${index}`, className: "dim-accessUserRow" },
@ -12695,13 +12739,17 @@ function AdminPhones({
contacts, contacts,
disabled, disabled,
lockedPhone, lockedPhone,
onChange onChange,
nested = false
}) { }) {
return h2( return h2(
"fieldset", nested ? "div" : "fieldset",
{ className: "dim-accessScene", disabled }, {
h2("legend", null, title), className: nested ? "dim-accessSubblock" : "dim-accessScene",
help ? h2("p", { className: "dim-accessUsersEmpty" }, help) : null, ...nested ? {} : { disabled }
},
nested ? h2("h4", { className: "dim-accessSubblockTitle" }, title) : h2("legend", null, title),
help ? h2("p", { className: "dim-accessHint" }, help) : null,
h2("ul", { className: "dim-accessUserList" }, phones.map((phone, index) => { h2("ul", { className: "dim-accessUserList" }, phones.map((phone, index) => {
const locked = lockedPhone && phone === lockedPhone; const locked = lockedPhone && phone === lockedPhone;
return h2( return h2(
@ -12914,7 +12962,7 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
h2( h2(
"div", "div",
{ className: "dim-accessField" }, { className: "dim-accessField" },
h2("span", null, entry.kind === "group" ? ["\u7FA4\u804A", " ", entry.groupJid].join("") : "\u79C1\u804A"), h2("span", null, ...entry.kind === "group" ? ["\u7FA4\u804A \xB7 ", groupDisplayName(entry.groupJid, draft.groups)] : ["\u79C1\u804A"]),
h2("strong", null, entry.pushName || entry.phone || entry.lid || entry.id), h2("strong", null, entry.pushName || entry.phone || entry.lid || entry.id),
entry.unresolved ? h2("span", null, "\uFF08\u7535\u8BDD\u672A\u89E3\u6790\uFF0C\u8BF7\u5148\u5728\u8054\u7CFB\u4EBA\u4E2D\u786E\u8BA4\uFF09") : null, entry.unresolved ? h2("span", null, "\uFF08\u7535\u8BDD\u672A\u89E3\u6790\uFF0C\u8BF7\u5148\u5728\u8054\u7CFB\u4EBA\u4E2D\u786E\u8BA4\uFF09") : null,
entry.requestText ? h2("p", null, entry.requestText.slice(0, 120)) : null entry.requestText ? h2("p", null, entry.requestText.slice(0, 120)) : null
@ -12948,15 +12996,21 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
const group = draft.groups[groupJid] ?? { title: "", admins: [], members: [] }; const group = draft.groups[groupJid] ?? { title: "", admins: [], members: [] };
return h2( return h2(
"div", "div",
{ key: groupJid, className: "dim-accessScene", "data-scene": "group-grant" }, { key: groupJid, className: "dim-accessGroupCard" },
h2("h3", null, group.title || groupJid), h2(
"div",
{ className: "dim-accessGroupHeading" },
h2("h3", null, groupDisplayName(groupJid, draft.groups)),
h2("code", { className: "dim-accessGroupJid" }, groupJid)
),
h2( h2(
"label", "label",
{ className: "dim-accessField" }, { className: "dim-accessField" },
h2("span", null, "\u7FA4\u5907\u6CE8\u540D"), h2("span", null, "\u7FA4\u540D\u79F0"),
h2("input", { h2("input", {
value: group.title ?? "", value: group.title ?? "",
maxLength: 128, maxLength: 128,
placeholder: "\u586B\u5199\u4FBF\u4E8E\u8BC6\u522B\u7684\u7FA4\u540D\u79F0",
onChange: (event) => setDraft((current) => ({ onChange: (event) => setDraft((current) => ({
...current, ...current,
groups: { groups: {
@ -12973,6 +13027,7 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
contacts, contacts,
disabled: saving, disabled: saving,
lockedPhone: null, lockedPhone: null,
nested: true,
onChange: (admins) => setDraft((current) => ({ onChange: (admins) => setDraft((current) => ({
...current, ...current,
groups: { groups: {
@ -12986,6 +13041,7 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
members: group.members ?? [], members: group.members ?? [],
contacts, contacts,
disabled: saving, disabled: saving,
nested: true,
onChange: (members) => setDraft((current) => ({ onChange: (members) => setDraft((current) => ({
...current, ...current,
groups: { groups: {
@ -13078,6 +13134,7 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
}, alreadyDirect ? "\u5DF2\u5728\u79C1\u804A" : "\u52A0\u79C1\u804A") : null, }, alreadyDirect ? "\u5DF2\u5728\u79C1\u804A" : "\u52A0\u79C1\u804A") : null,
...groupTargets.map((groupJid) => { ...groupTargets.map((groupJid) => {
const group = draft.groups[groupJid] ?? { admins: [], members: [] }; const group = draft.groups[groupJid] ?? { admins: [], members: [] };
const name2 = groupDisplayName(groupJid, draft.groups);
const already = group.admins.includes(phone) || group.members.some((m) => m.phone === phone); const already = group.admins.includes(phone) || group.members.some((m) => m.phone === phone);
return h2("button", { return h2("button", {
key: `add-${groupJid}`, key: `add-${groupJid}`,
@ -13104,9 +13161,12 @@ function AccessGrantSettingsPage({ channel: channel4, account, rpcCall, onSaved
} }
}; };
}); });
setFeedback({ tone: "success", message: "\u5DF2\u52A0\u5165\u672C\u7FA4\u6388\u6743\uFF0C\u8BB0\u5F97\u70B9\u4FDD\u5B58\u3002" }); setFeedback({
tone: "success",
message: ["\u5DF2\u52A0\u5165\u7FA4\u6388\u6743\uFF1A", name2, "\u3002\u8BB0\u5F97\u70B9\u4FDD\u5B58\u3002"].join("")
});
} }
}, already ? "\u5DF2\u5728\u672C\u7FA4" : "\u52A0\u672C\u7FA4"); }, [already ? "\u5DF2\u5728\u7FA4\uFF1A" : "\u52A0\u5165\u7FA4\uFF1A", name2].join(""));
}) })
); );
})) }))
@ -14520,12 +14580,22 @@ var CSS11 = String.raw`
.dim-accessFeedback[data-tone="success"] { color: var(--dsw-alias-state-success-primary, #20a162); } .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-accessFeedback[data-tone="error"] { color: var(--dsw-alias-state-error-primary, #d54941); }
.dim-accessActions { display: flex; justify-content: flex-end; } .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 { 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-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 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 .dim-botCard .dim-cardFooter { margin-top: 0; }
.dim-panel .ddt-headingCopy { display: none; } .dim-panel .ddt-headingCopy { display: none; }
.dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); } .dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
{ {
"name": "dsh-im-ops", "name": "dsh-im-ops",
"version": "4.9.1-ops.7", "version": "4.9.1-ops.8",
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 — access/session policies owned here.", "description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 — access/session policies owned here.",
"keywords": [ "keywords": [
"deepseek-harness", "deepseek-harness",

View file

@ -71,6 +71,19 @@ function contactLabel(contact) {
return `${name} · ${phone}`; 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({ function PhoneTypeahead({
value, value,
onChange, onChange,
@ -80,16 +93,28 @@ function PhoneTypeahead({
requirePhone = true, requirePhone = true,
}) { }) {
const [query, setQuery] = React.useState(value || ''); const [query, setQuery] = React.useState(value || '');
const [open, setOpen] = React.useState(false);
const blurTimer = React.useRef(null);
React.useEffect(() => { setQuery(value || ''); }, [value]); React.useEffect(() => { setQuery(value || ''); }, [value]);
React.useEffect(() => () => {
if (blurTimer.current) window.clearTimeout(blurTimer.current);
}, []);
const q = query.trim().toLowerCase(); const q = query.trim().toLowerCase();
const queryDigits = digitsOnly(query);
const suggestions = (contacts ?? []) const suggestions = (contacts ?? [])
.filter((contact) => { .filter((contact) => {
if (requirePhone && !contact.phone) return false; 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); if (!q) return Boolean(contact.phone);
return (contact.phone ?? '').includes(q.replace(/[^\d]/g, '')) return (contact.phone ?? '').includes(queryDigits)
|| (contact.pushName ?? '').toLowerCase().includes(q); || (contact.pushName ?? '').toLowerCase().includes(q);
}) })
.slice(0, 8); .slice(0, 8);
const showSuggestions = open && !disabled && suggestions.length > 0;
return h('div', { className: 'dim-accessTypeahead' }, return h('div', { className: 'dim-accessTypeahead' },
h('input', { h('input', {
@ -100,23 +125,34 @@ function PhoneTypeahead({
autoCapitalize: 'none', autoCapitalize: 'none',
autoCorrect: 'off', autoCorrect: 'off',
spellCheck: false, spellCheck: false,
autoComplete: 'off',
onFocus: () => {
if (blurTimer.current) window.clearTimeout(blurTimer.current);
setOpen(true);
},
onBlur: () => {
blurTimer.current = window.setTimeout(() => setOpen(false), 120);
},
onChange: (event) => { onChange: (event) => {
setQuery(event.target.value); setQuery(event.target.value);
onChange(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] ?? ''}` }, suggestions.map((contact) => h('li', { key: `${contact.phone}-${contact.lids?.[0] ?? ''}` },
h('button', { h('button', {
type: 'button', type: 'button',
className: 'dim-deliveryButton', className: 'dim-accessSuggestItem',
disabled: disabled || (requirePhone && !contact.phone), disabled: disabled || (requirePhone && !contact.phone),
onMouseDown: (event) => event.preventDefault(),
onClick: () => { onClick: () => {
if (!contact.phone) return; if (!contact.phone) return;
setQuery(contact.phone); setQuery(contact.phone);
onChange(contact.phone); onChange(contact.phone);
setOpen(false);
}, },
}, contactLabel(contact)))))); }, contactLabel(contact))))) : null);
} }
function MemberRows({ function MemberRows({
@ -125,9 +161,13 @@ function MemberRows({
contacts, contacts,
disabled, disabled,
onChange, onChange,
nested = false,
}) { }) {
return h('fieldset', { className: 'dim-accessScene', disabled }, return h(nested ? 'div' : 'fieldset', {
h('legend', null, title), 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('ul', { className: 'dim-accessUserList' }, members.map((member, index) =>
h('li', { key: `member-${index}`, className: 'dim-accessUserRow' }, h('li', { key: `member-${index}`, className: 'dim-accessUserRow' },
h('label', { className: 'dim-accessField dim-accessUserId' }, h('label', { className: 'dim-accessField dim-accessUserId' },
@ -174,10 +214,14 @@ function AdminPhones({
disabled, disabled,
lockedPhone, lockedPhone,
onChange, onChange,
nested = false,
}) { }) {
return h('fieldset', { className: 'dim-accessScene', disabled }, return h(nested ? 'div' : 'fieldset', {
h('legend', null, title), className: nested ? 'dim-accessSubblock' : 'dim-accessScene',
help ? h('p', { className: 'dim-accessUsersEmpty' }, help) : null, ...(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) => { h('ul', { className: 'dim-accessUserList' }, phones.map((phone, index) => {
const locked = lockedPhone && phone === lockedPhone; const locked = lockedPhone && phone === lockedPhone;
return h('li', { key: `admin-${index}`, className: 'dim-accessUserRow' }, 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) .filter((entry) => entry.status === 'pending' || !entry.status)
.map((entry) => h('li', { key: entry.id, className: 'dim-accessUserRow' }, .map((entry) => h('li', { key: entry.id, className: 'dim-accessUserRow' },
h('div', { className: 'dim-accessField' }, h('div', { className: 'dim-accessField' },
h('span', null, entry.kind === 'group' h('span', null, ...(entry.kind === 'group'
? ['群聊', ' ', entry.groupJid].join('') ? ['群聊 · ', groupDisplayName(entry.groupJid, draft.groups)]
: '私聊'), : ['私聊'])),
h('strong', null, entry.pushName || entry.phone || entry.lid || entry.id), h('strong', null, entry.pushName || entry.phone || entry.lid || entry.id),
entry.unresolved entry.unresolved
? h('span', null, '(电话未解析,请先在联系人中确认)') ? h('span', null, '(电话未解析,请先在联系人中确认)')
@ -421,13 +465,16 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
'尚无已知群。可在下方粘贴群 JID(…@g.us),或等群内有人 @ 机器人后自动出现。') '尚无已知群。可在下方粘贴群 JID(…@g.us),或等群内有人 @ 机器人后自动出现。')
: knownGroupJids.map((groupJid) => { : knownGroupJids.map((groupJid) => {
const group = draft.groups[groupJid] ?? { title: '', admins: [], members: [] }; const group = draft.groups[groupJid] ?? { title: '', admins: [], members: [] };
return h('div', { key: groupJid, className: 'dim-accessScene', 'data-scene': 'group-grant' }, return h('div', { key: groupJid, className: 'dim-accessGroupCard' },
h('h3', null, group.title || groupJid), h('div', { className: 'dim-accessGroupHeading' },
h('h3', null, groupDisplayName(groupJid, draft.groups)),
h('code', { className: 'dim-accessGroupJid' }, groupJid)),
h('label', { className: 'dim-accessField' }, h('label', { className: 'dim-accessField' },
h('span', null, '群备注名'), h('span', null, '群名称'),
h('input', { h('input', {
value: group.title ?? '', value: group.title ?? '',
maxLength: 128, maxLength: 128,
placeholder: '填写便于识别的群名称',
onChange: (event) => setDraft((current) => ({ onChange: (event) => setDraft((current) => ({
...current, ...current,
groups: { groups: {
@ -443,6 +490,7 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
contacts, contacts,
disabled: saving, disabled: saving,
lockedPhone: null, lockedPhone: null,
nested: true,
onChange: (admins) => setDraft((current) => ({ onChange: (admins) => setDraft((current) => ({
...current, ...current,
groups: { groups: {
@ -456,6 +504,7 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
members: group.members ?? [], members: group.members ?? [],
contacts, contacts,
disabled: saving, disabled: saving,
nested: true,
onChange: (members) => setDraft((current) => ({ onChange: (members) => setDraft((current) => ({
...current, ...current,
groups: { groups: {
@ -532,6 +581,7 @@ export function AccessGrantSettingsPage({ channel, account, rpcCall, onSaved })
}, alreadyDirect ? '已在私聊' : '加私聊') : null, }, alreadyDirect ? '已在私聊' : '加私聊') : null,
...groupTargets.map((groupJid) => { ...groupTargets.map((groupJid) => {
const group = draft.groups[groupJid] ?? { admins: [], members: [] }; const group = draft.groups[groupJid] ?? { admins: [], members: [] };
const name = groupDisplayName(groupJid, draft.groups);
const already = group.admins.includes(phone) const already = group.admins.includes(phone)
|| group.members.some((m) => m.phone === phone); || group.members.some((m) => m.phone === phone);
return h('button', { 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 }, h('fieldset', { className: 'dim-accessScene', disabled: saving },

View file

@ -11,6 +11,22 @@ const EN = Object.freeze({
'机器人设置页签': 'Bot settings tabs', '机器人设置页签': 'Bot settings tabs',
'投递设置': 'Delivery settings', '投递设置': 'Delivery settings',
'访问设置': 'Access 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.", "无法读取访问授权,请返回刷新后重试。": "Could not load the access grant. Go back, refresh, and try again.",
"正在读取分级访问设置…": "Loading graded access settings…", "正在读取分级访问设置…": "Loading graded access settings…",

View file

@ -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="success"] { color: var(--dsw-alias-state-success-primary, #20a162); }
.dim-accessFeedback[data-tone="error"] { color: var(--dsw-alias-state-error-primary, #d54941); } .dim-accessFeedback[data-tone="error"] { color: var(--dsw-alias-state-error-primary, #d54941); }
.dim-accessActions { display: flex; justify-content: flex-end; } .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 { 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-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 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 .dim-botCard .dim-cardFooter { margin-top: 0; }
.dim-panel .ddt-headingCopy { display: none; } .dim-panel .ddt-headingCopy { display: none; }
.dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); } .dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }