2026-06-06 00:24:58 +08:00

422 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Add from "@mui/icons-material/Add";
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
import PersonAddAlt1Outlined from "@mui/icons-material/PersonAddAlt1Outlined";
import { useEffect, useRef, useState } from "react";
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
import TextField from "@mui/material/TextField";
import { formatMillis } from "@/shared/utils/time.js";
import { DataState } from "@/shared/ui/DataState.jsx";
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
import { useHostBdsPage } from "@/features/host-org/hooks/useHostBdsPage.js";
import styles from "@/features/host-org/host-org.module.css";
const bdLeaderBaseColumns = [
{
key: "user",
label: "用户",
render: (item) => <PersonIdentity item={item} />,
width: "minmax(220px, 1.25fr)",
},
{
key: "regionId",
label: "区域",
render: (item, _index, context) => regionName(item, context?.regionOptions),
width: "minmax(130px, 0.75fr)",
},
{
key: "positionAlias",
label: "职位别名",
render: (item, _index, context) => <PositionAliasCell item={item} page={context?.page} />,
width: "minmax(130px, 0.75fr)",
},
{
key: "status",
label: "状态",
render: (item, _index, context) => <BDLeaderStatusSwitch item={item} page={context?.page} />,
width: "minmax(90px, 0.7fr)",
},
{
key: "createdByUserId",
label: "创建人",
render: (item) => <CreatorIdentity item={item} />,
width: "minmax(180px, 1fr)",
},
{
key: "subBdCount",
label: "下级 BD 数量",
render: (item, _index, context) => <SubBDCount item={item} page={context?.page} />,
width: "minmax(130px, 0.75fr)",
},
{
key: "createdAtMs",
label: "创建时间",
render: (item) => formatMillis(item.createdAtMs),
width: "minmax(170px, 1fr)",
},
];
export function HostBdLeadersPage() {
const page = useHostBdsPage({ profileType: "leader" });
const createDisabled = !page.abilities.canCreate;
const items = page.data.items || [];
const total = page.data.total || 0;
const bdLeaderColumns = [
...bdLeaderBaseColumns,
page.abilities.canCreate || page.abilities.canUpdate
? {
key: "actions",
label: "操作",
render: (item) => <BDLeaderActions item={item} page={page} />,
width: "minmax(118px, 0.7fr)",
}
: null,
].filter(Boolean);
const toolbarActions = page.abilities.canCreate
? [
{
icon: <Add fontSize="small" />,
label: "创建 BD Leader",
onClick: page.openBDLeaderForm,
variant: "primary",
},
]
: [];
return (
<section className={styles.root}>
<div className={styles.contentPanel}>
<HostOrgToolbar actions={toolbarActions} />
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
<div className={styles.listBlock}>
<HostOrgTable
columns={bdLeaderColumns}
context={{ page, regionOptions: page.regionOptions }}
items={items}
minWidth="1120px"
pagination={
total > 0
? {
page: page.page,
pageSize: page.data.pageSize || 50,
total,
onPageChange: page.setPage,
}
: undefined
}
rowKey={(item) => `bd-leader-${item.userId}`}
/>
</div>
</DataState>
</div>
<HostOrgActionModal
disabled={createDisabled}
loading={page.loadingAction === "bd-leader"}
open={page.activeAction === "bd-leader"}
sectionTitle="Leader 信息"
onClose={page.closeAction}
onSubmit={page.submitBDLeader}
title="创建 BD Leader"
>
<TextField
disabled={createDisabled}
label="用户短 ID"
required
type="number"
value={page.bdLeaderForm.targetUserId}
onChange={(event) =>
page.setBDLeaderForm({ ...page.bdLeaderForm, targetUserId: event.target.value })
}
/>
<TextField
disabled={createDisabled}
label="职位别名"
value={page.bdLeaderForm.positionAlias}
onChange={(event) =>
page.setBDLeaderForm({ ...page.bdLeaderForm, positionAlias: event.target.value })
}
/>
<TextField
disabled={createDisabled}
label="联系方式"
value={page.bdLeaderForm.reason}
onChange={(event) => page.setBDLeaderForm({ ...page.bdLeaderForm, reason: event.target.value })}
/>
</HostOrgActionModal>
<HostOrgActionModal
disabled={createDisabled}
loading={page.loadingAction === "leader-add-bd"}
open={page.activeAction === "leader-add-bd"}
sectionTitle="下级 BD 信息"
onClose={page.closeAction}
onSubmit={page.submitLeaderBD}
title="增加下级 BD"
>
<TextField
disabled
label="上级 Leader"
value={page.selectedLeader?.displayUserId || page.selectedLeader?.userId || ""}
/>
<TextField
disabled={createDisabled}
label="下级用户短 ID"
required
type="number"
value={page.bdForm.targetUserId}
onChange={(event) => page.setBDForm({ ...page.bdForm, targetUserId: event.target.value })}
/>
<UserLookupPreview keyword={page.bdForm.targetUserId} lookup={page.bdUserLookup} />
<TextField
disabled={createDisabled}
label="原因"
multiline
minRows={2}
value={page.bdForm.reason}
onChange={(event) => page.setBDForm({ ...page.bdForm, reason: event.target.value })}
/>
</HostOrgActionModal>
<SideDrawer
open={page.activeAction === "leader-bds"}
title={`下级 BD ${page.selectedLeader?.displayUserId || page.selectedLeader?.userId || ""}`}
width="wide"
onClose={page.closeAction}
>
<DataState error={page.leaderBDsError} loading={page.leaderBDsLoading} onRetry={page.loadLeaderBDs}>
<div className={styles.childBdList}>
{(page.leaderBDs.items || []).length ? (
page.leaderBDs.items.map((item) => (
<div className={styles.childBdItem} key={item.userId}>
<PersonIdentity item={item} />
<span className={styles.childBdStatus}>
<HostOrgStatus value={item.status} />
</span>
</div>
))
) : (
<div className="empty-state">暂无下级 BD</div>
)}
</div>
</DataState>
</SideDrawer>
</section>
);
}
function PersonIdentity({ item }) {
const name = item.username || "-";
const meta = item.displayUserId || item.userId || "-";
return (
<div className={styles.sellerIdentity}>
{item.avatar ? (
<img alt="" className={styles.sellerAvatar} src={item.avatar} />
) : (
<span className={styles.sellerAvatar}>
{String(name || meta)
.slice(0, 1)
.toUpperCase()}
</span>
)}
<span className={styles.sellerText}>
<span className={styles.sellerName}>{name}</span>
<span className={styles.sellerMeta}>{meta}</span>
</span>
</div>
);
}
function CreatorIdentity({ item }) {
if (!item.createdByUserId && !item.createdByDisplayUserId && !item.createdByUsername) {
return "-";
}
return (
<PersonIdentity
item={{
avatar: item.createdByAvatar,
displayUserId: item.createdByDisplayUserId,
userId: item.createdByUserId,
username: item.createdByUsername || (item.createdByUserId ? `用户 ${item.createdByUserId}` : "-"),
}}
/>
);
}
function BDLeaderStatusSwitch({ item, page }) {
return (
<AdminSwitch
checked={item.status === "active"}
disabled={!page?.abilities.canUpdate || page.loadingAction === `bd-leader-status-${item.userId}`}
inputProps={{ "aria-label": item.status === "active" ? "停用 BD Leader" : "启用 BD Leader" }}
onChange={(event) => page.toggleBDLeader(item, event.target.checked)}
/>
);
}
function PositionAliasCell({ item, page }) {
const inputRef = useRef(null);
const cancelSaveRef = useRef(false);
const originalValue = String(item.positionAlias || "").trim();
const [editing, setEditing] = useState(false);
const [saving, setSaving] = useState(false);
const [value, setValue] = useState(originalValue);
const canEdit = Boolean(page?.abilities.canUpdate);
useEffect(() => {
if (!editing) {
setValue(originalValue);
}
}, [editing, originalValue]);
useEffect(() => {
if (editing) {
inputRef.current?.focus();
inputRef.current?.select();
}
}, [editing]);
const save = async () => {
if (cancelSaveRef.current) {
cancelSaveRef.current = false;
setValue(originalValue);
setEditing(false);
return;
}
const nextValue = value.trim();
if (nextValue === originalValue) {
setValue(originalValue);
setEditing(false);
return;
}
setSaving(true);
try {
await page.saveBDLeaderPositionAlias(item, nextValue);
setEditing(false);
} catch {
setValue(originalValue);
setEditing(false);
} finally {
setSaving(false);
}
};
if (editing) {
return (
<input
ref={inputRef}
className={styles.inlineAliasInput}
disabled={saving}
maxLength={64}
value={value}
onBlur={save}
onChange={(event) => setValue(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.currentTarget.blur();
}
if (event.key === "Escape") {
cancelSaveRef.current = true;
setValue(originalValue);
setEditing(false);
}
}}
/>
);
}
if (!canEdit) {
return <span className={styles.inlineAliasText}>{originalValue || "-"}</span>;
}
return (
<button
className={styles.inlineAliasButton}
title="点击编辑职位别名"
type="button"
onClick={() => {
cancelSaveRef.current = false;
setEditing(true);
}}
>
{originalValue || "-"}
</button>
);
}
function SubBDCount({ item, page }) {
return (
<button className={styles.contactButton} type="button" onClick={() => page.openLeaderBDs(item)}>
{Number(item.subBdCount || 0).toLocaleString("zh-CN")}
</button>
);
}
function BDLeaderActions({ item, page }) {
return (
<AdminRowActions>
{page.abilities.canCreate ? (
<AdminActionIconButton label="增加下级 BD" onClick={() => page.openAddLeaderBD(item)}>
<PersonAddAlt1Outlined fontSize="small" />
</AdminActionIconButton>
) : null}
{page.abilities.canUpdate ? (
<AdminActionIconButton
disabled={page.loadingAction === `bd-leader-delete-${item.userId}`}
label="删除 BD Leader"
sx={dangerActionSx}
onClick={() => page.removeBDLeader(item)}
>
<DeleteOutlineOutlined fontSize="small" />
</AdminActionIconButton>
) : null}
</AdminRowActions>
);
}
function UserLookupPreview({ keyword, lookup }) {
const hasKeyword = String(keyword || "").trim();
if (!hasKeyword) {
return <div className={styles.lookupPanel}>输入用户短 ID 后自动搜索用户</div>;
}
if (lookup.loading) {
return <div className={styles.lookupPanel}>搜索中...</div>;
}
if (lookup.error) {
return <div className={`${styles.lookupPanel} ${styles.negative}`}>{lookup.error}</div>;
}
if (!lookup.user) {
return <div className={styles.lookupPanel}>未找到匹配用户提交时后端会再次校验</div>;
}
return (
<div className={styles.lookupPanel}>
<PersonIdentity item={lookup.user} />
<span className={styles.sellerMeta}>{lookup.user.regionName || "未分配区域"}</span>
</div>
);
}
function regionName(item, regionOptions = []) {
if (item.regionName) {
return item.regionName;
}
const option = regionOptions.find((region) => String(region.value) === String(item.regionId));
return option?.name || item.regionId || "-";
}
const dangerActionSx = {
borderColor: "rgba(239, 68, 68, 0.3)",
backgroundColor: "rgba(239, 68, 68, 0.08)",
color: "var(--danger)",
"&:hover": {
borderColor: "rgba(239, 68, 68, 0.5)",
backgroundColor: "rgba(239, 68, 68, 0.14)",
color: "var(--danger)",
},
};