422 lines
15 KiB
JavaScript
422 lines
15 KiB
JavaScript
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)",
|
||
},
|
||
};
|