forked from EduCraft/curriculum-project-hub
feat(filelib-web): 授权面板表格化:搜索、添加弹窗、权限下拉与成员跳转
- GrantsPanel 重写为表格:顶部左侧授权成员搜索框(名称/id/类型过滤), 右侧「添加授权」弹窗(类型 + 主体搜索选择 + 权限);行内权限下拉 直接改级(复用 PUT upsert),操作列删除;成员单元格跳转用户管理 (?q= 过滤)或 Group 管理(?select= 选中)。 - grantService: GrantDto 增加 principalName,list/put/force 三处统一 批量回填(用户 displayName / 组 name),前端不再只显示裸 id。 - 用户管理页加过滤框并从 ?q= 初始化;GroupAdmin 支持 ?select= 直达。 - 测试:resetDb 补 MemberGroup 三表清理(全局表不被 org/user 级联清到, 此前跨用例污染导致级联软删用例断言失败);cph_hub_test 补 migrate。 - 顺带合并 types.ts 里重复的 Grant 声明(interface 合并残留)。
This commit is contained in:
@@ -7,10 +7,12 @@
|
||||
* 这里管的是 org 成员与其角色。
|
||||
*/
|
||||
import { onMount } from "svelte";
|
||||
import { page } from "$app/state";
|
||||
import { api } from "$lib/api.js";
|
||||
import { loadConfig } from "$lib/config.js";
|
||||
import { toastOk, toastErr } from "$lib/stores.js";
|
||||
import type { OrgMember, OrgRole } from "$lib/types.js";
|
||||
import Icon from "$lib/Icon.svelte";
|
||||
|
||||
const ROLE_LABEL: Record<OrgRole, string> = {
|
||||
OWNER: "所有者",
|
||||
@@ -23,6 +25,9 @@
|
||||
let members = $state<OrgMember[] | null>(null);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// 列表过滤;授权面板跳转会带 ?q=<userId>,以此为初始过滤词。
|
||||
let filterText = $state(page.url.searchParams.get("q") ?? "");
|
||||
|
||||
let newOpenId = $state("");
|
||||
let newName = $state("");
|
||||
let newRole = $state<OrgRole>("MEMBER");
|
||||
@@ -30,6 +35,19 @@
|
||||
|
||||
const base = $derived(orgSlug === null ? null : `/api/org/${encodeURIComponent(orgSlug)}`);
|
||||
|
||||
/** 按显示名 / userId / openId 过滤(纯前端;成员全量在手)。 */
|
||||
const shown = $derived.by((): OrgMember[] | null => {
|
||||
if (members === null) return null;
|
||||
const q = filterText.trim().toLowerCase();
|
||||
if (q === "") return members;
|
||||
return members.filter(
|
||||
(m) =>
|
||||
m.displayName.toLowerCase().includes(q) ||
|
||||
m.userId.toLowerCase().includes(q) ||
|
||||
m.feishuOpenId.toLowerCase().includes(q),
|
||||
);
|
||||
});
|
||||
|
||||
async function load(): Promise<void> {
|
||||
if (base === null) return;
|
||||
try {
|
||||
@@ -121,14 +139,28 @@
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="section-title mb-2.5">成员列表</div>
|
||||
<div class="mb-2.5 flex items-center justify-between gap-2">
|
||||
<div class="section-title">成员列表</div>
|
||||
<div class="relative w-[260px] shrink-0">
|
||||
<span class="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-ink-3">
|
||||
<Icon name="search" size={13} />
|
||||
</span>
|
||||
<input
|
||||
class="input w-full !py-[5px] !pl-8 !text-[12.5px]"
|
||||
placeholder="过滤:名称 / userId / openId"
|
||||
bind:value={filterText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="py-3 text-[12.5px] text-danger">{error}</div>
|
||||
{:else if members === null}
|
||||
{:else if shown === null}
|
||||
<div class="quiet py-[18px] text-center">加载中…</div>
|
||||
{:else if members.length === 0}
|
||||
<div class="quiet py-[18px] text-center">暂无成员</div>
|
||||
{:else if shown.length === 0}
|
||||
<div class="quiet py-[18px] text-center">
|
||||
{filterText.trim() === "" ? "暂无成员" : `无匹配「${filterText.trim()}」的成员`}
|
||||
</div>
|
||||
{:else}
|
||||
<table class="list">
|
||||
<thead>
|
||||
@@ -140,7 +172,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each members as m (m.userId)}
|
||||
{#each shown as m (m.userId)}
|
||||
<tr>
|
||||
<td class="text-ink">{m.displayName || m.userId}</td>
|
||||
<td class="file-meta">{m.userId}</td>
|
||||
|
||||
Reference in New Issue
Block a user