优化布局
This commit is contained in:
parent
3698b05159
commit
0fd67199b6
File diff suppressed because it is too large
Load Diff
@ -40,6 +40,7 @@ export function AdminLayout() {
|
|||||||
<Header
|
<Header
|
||||||
activeLabel={activeNav?.label || "总览"}
|
activeLabel={activeNav?.label || "总览"}
|
||||||
isSidebarCollapsed={sidebarCollapsed}
|
isSidebarCollapsed={sidebarCollapsed}
|
||||||
|
menuItems={menus}
|
||||||
onRefresh={refresh}
|
onRefresh={refresh}
|
||||||
onToggleSidebar={() => setSidebarCollapsed((current) => !current)}
|
onToggleSidebar={() => setSidebarCollapsed((current) => !current)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
|||||||
const SearchDialog = lazy(() => import("@/features/search/components/SearchDialog.jsx").then((module) => ({ default: module.SearchDialog })));
|
const SearchDialog = lazy(() => import("@/features/search/components/SearchDialog.jsx").then((module) => ({ default: module.SearchDialog })));
|
||||||
const emptyPasswordForm = { oldPassword: "", newPassword: "", confirmPassword: "" };
|
const emptyPasswordForm = { oldPassword: "", newPassword: "", confirmPassword: "" };
|
||||||
|
|
||||||
export function Header({ activeLabel, isSidebarCollapsed, onRefresh, onToggleSidebar }) {
|
export function Header({ activeLabel, isSidebarCollapsed, menuItems = [], onRefresh, onToggleSidebar }) {
|
||||||
const [searchOpen, setSearchOpen] = useState(false);
|
const [searchOpen, setSearchOpen] = useState(false);
|
||||||
const [searchMounted, setSearchMounted] = useState(false);
|
const [searchMounted, setSearchMounted] = useState(false);
|
||||||
const [userMenuAnchor, setUserMenuAnchor] = useState(null);
|
const [userMenuAnchor, setUserMenuAnchor] = useState(null);
|
||||||
@ -186,7 +186,7 @@ export function Header({ activeLabel, isSidebarCollapsed, onRefresh, onToggleSid
|
|||||||
</TextField>
|
</TextField>
|
||||||
<button className="search-trigger" type="button" onClick={openSearch}>
|
<button className="search-trigger" type="button" onClick={openSearch}>
|
||||||
<Search fontSize="small" />
|
<Search fontSize="small" />
|
||||||
<span>搜索用户、角色...</span>
|
<span>搜索菜单、用户、角色...</span>
|
||||||
</button>
|
</button>
|
||||||
<IconButton label="刷新" onClick={refresh}>
|
<IconButton label="刷新" onClick={refresh}>
|
||||||
<Refresh fontSize="small" />
|
<Refresh fontSize="small" />
|
||||||
@ -223,7 +223,7 @@ export function Header({ activeLabel, isSidebarCollapsed, onRefresh, onToggleSid
|
|||||||
|
|
||||||
{searchMounted ? (
|
{searchMounted ? (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<SearchDialog open={searchOpen} onClose={() => setSearchOpen(false)} onSelect={handleSearchSelect} />
|
<SearchDialog menuItems={menuItems} open={searchOpen} onClose={() => setSearchOpen(false)} onSelect={handleSearchSelect} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
) : null}
|
) : null}
|
||||||
<AdminFormDialog
|
<AdminFormDialog
|
||||||
|
|||||||
@ -94,6 +94,11 @@ function NavGroup({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasChildren && children.length === 1 && children[0].path && !isChildActive) {
|
||||||
|
onNavigate(children[0].path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
onToggleExpanded();
|
onToggleExpanded();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -23,6 +23,10 @@ import WalletOutlined from "@mui/icons-material/WalletOutlined";
|
|||||||
import { getRouteByMenuCode } from "@/app/router/routeConfig";
|
import { getRouteByMenuCode } from "@/app/router/routeConfig";
|
||||||
|
|
||||||
const deprecatedMenuCodes = new Set(["services"]);
|
const deprecatedMenuCodes = new Set(["services"]);
|
||||||
|
const menuLabelOverrides = {
|
||||||
|
system: "后台设置",
|
||||||
|
"system-users": "后台用户"
|
||||||
|
};
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
apartment: ApartmentOutlined,
|
apartment: ApartmentOutlined,
|
||||||
@ -55,7 +59,7 @@ export const fallbackNavigation = [
|
|||||||
code: "system",
|
code: "system",
|
||||||
icon: SettingsApplicationsOutlined,
|
icon: SettingsApplicationsOutlined,
|
||||||
id: "system",
|
id: "system",
|
||||||
label: "系统管理",
|
label: "后台设置",
|
||||||
children: [
|
children: [
|
||||||
routeNavItem("system-users", { icon: ManageAccountsOutlined }),
|
routeNavItem("system-users", { icon: ManageAccountsOutlined }),
|
||||||
routeNavItem("system-roles", { icon: ShieldOutlined }),
|
routeNavItem("system-roles", { icon: ShieldOutlined }),
|
||||||
@ -162,7 +166,7 @@ export function mapBackendMenus(menus = []) {
|
|||||||
code: item.code,
|
code: item.code,
|
||||||
icon: iconMap[item.icon] || MenuOpenOutlined,
|
icon: iconMap[item.icon] || MenuOpenOutlined,
|
||||||
id: item.code || String(item.id),
|
id: item.code || String(item.id),
|
||||||
label: item.label,
|
label: menuLabelOverrides[item.code] || route?.label || item.label,
|
||||||
path: route?.path || item.path,
|
path: route?.path || item.path,
|
||||||
permissionCode: route?.permission || item.permissionCode
|
permissionCode: route?.permission || item.permissionCode
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export const PERMISSIONS = {
|
|||||||
coinSellerView: "coin-seller:view",
|
coinSellerView: "coin-seller:view",
|
||||||
coinSellerCreate: "coin-seller:create",
|
coinSellerCreate: "coin-seller:create",
|
||||||
coinSellerUpdate: "coin-seller:update",
|
coinSellerUpdate: "coin-seller:update",
|
||||||
|
coinSellerStockCredit: "coin-seller:stock-credit",
|
||||||
paymentBillView: "payment-bill:view",
|
paymentBillView: "payment-bill:view",
|
||||||
roleView: "role:view",
|
roleView: "role:view",
|
||||||
roleCreate: "role:create",
|
roleCreate: "role:create",
|
||||||
@ -73,7 +74,7 @@ export const PERMISSIONS = {
|
|||||||
uploadCreate: "upload:create",
|
uploadCreate: "upload:create",
|
||||||
jobView: "job:view",
|
jobView: "job:view",
|
||||||
jobCancel: "job:cancel",
|
jobCancel: "job:cancel",
|
||||||
exportCreate: "export:create"
|
exportCreate: "export:create",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type PermissionCode = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
|
export type PermissionCode = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
|
||||||
@ -113,7 +114,7 @@ export const MENU_CODES = {
|
|||||||
hostOrgCoinSellers: "host-org-coin-sellers",
|
hostOrgCoinSellers: "host-org-coin-sellers",
|
||||||
payment: "payment",
|
payment: "payment",
|
||||||
paymentBillList: "payment-bill-list",
|
paymentBillList: "payment-bill-list",
|
||||||
jobs: "jobs"
|
jobs: "jobs",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type MenuCode = (typeof MENU_CODES)[keyof typeof MENU_CODES];
|
export type MenuCode = (typeof MENU_CODES)[keyof typeof MENU_CODES];
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const emptyForm = () => ({
|
|||||||
platform: "android",
|
platform: "android",
|
||||||
regionId: "",
|
regionId: "",
|
||||||
sortOrder: "0",
|
sortOrder: "0",
|
||||||
status: "active"
|
status: "active",
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useBannerConfigPage() {
|
export function useBannerConfigPage() {
|
||||||
@ -45,15 +45,20 @@ export function useBannerConfigPage() {
|
|||||||
keyword,
|
keyword,
|
||||||
platform,
|
platform,
|
||||||
regionId,
|
regionId,
|
||||||
status
|
status,
|
||||||
}),
|
}),
|
||||||
[countryCode, keyword, platform, regionId, status]
|
[countryCode, keyword, platform, regionId, status],
|
||||||
);
|
);
|
||||||
const queryFn = useCallback(() => listBanners(filters), [filters]);
|
const queryFn = useCallback(() => listBanners(filters), [filters]);
|
||||||
const { data = emptyData, error, loading, reload } = useAdminQuery(queryFn, {
|
const {
|
||||||
|
data = emptyData,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
reload,
|
||||||
|
} = useAdminQuery(queryFn, {
|
||||||
errorMessage: "加载 BANNER 配置失败",
|
errorMessage: "加载 BANNER 配置失败",
|
||||||
initialData: emptyData,
|
initialData: emptyData,
|
||||||
queryKey: ["app-config", "banners", filters]
|
queryKey: ["app-config", "banners", filters],
|
||||||
});
|
});
|
||||||
|
|
||||||
const setForm = (patch) => {
|
const setForm = (patch) => {
|
||||||
@ -78,6 +83,14 @@ export function useBannerConfigPage() {
|
|||||||
setFormState(emptyForm());
|
setFormState(emptyForm());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setKeyword("");
|
||||||
|
setStatus("");
|
||||||
|
setPlatform("");
|
||||||
|
setRegionId("");
|
||||||
|
setCountryCode("");
|
||||||
|
};
|
||||||
|
|
||||||
const submitBanner = async (event) => {
|
const submitBanner = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const payload = parseForm(appBannerSchema, normalizeForm(form));
|
const payload = parseForm(appBannerSchema, normalizeForm(form));
|
||||||
@ -105,7 +118,7 @@ export function useBannerConfigPage() {
|
|||||||
confirmText: "删除",
|
confirmText: "删除",
|
||||||
message: item.description || item.param || `BANNER ${item.id}`,
|
message: item.description || item.param || `BANNER ${item.id}`,
|
||||||
title: "删除BANNER",
|
title: "删除BANNER",
|
||||||
tone: "danger"
|
tone: "danger",
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
@ -144,6 +157,7 @@ export function useBannerConfigPage() {
|
|||||||
regionOptions,
|
regionOptions,
|
||||||
reload,
|
reload,
|
||||||
removeBanner,
|
removeBanner,
|
||||||
|
resetFilters,
|
||||||
setCountryCode,
|
setCountryCode,
|
||||||
setForm,
|
setForm,
|
||||||
setKeyword,
|
setKeyword,
|
||||||
@ -151,7 +165,7 @@ export function useBannerConfigPage() {
|
|||||||
setRegionId,
|
setRegionId,
|
||||||
setStatus,
|
setStatus,
|
||||||
status,
|
status,
|
||||||
submitBanner
|
submitBanner,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +174,7 @@ function normalizeForm(form) {
|
|||||||
...form,
|
...form,
|
||||||
countryCode: form.countryCode || "",
|
countryCode: form.countryCode || "",
|
||||||
regionId: form.regionId || 0,
|
regionId: form.regionId || 0,
|
||||||
sortOrder: form.sortOrder || 0
|
sortOrder: form.sortOrder || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +188,6 @@ function formFromBanner(item) {
|
|||||||
platform: item.platform || "android",
|
platform: item.platform || "android",
|
||||||
regionId: item.regionId ? String(item.regionId) : "",
|
regionId: item.regionId ? String(item.regionId) : "",
|
||||||
sortOrder: String(item.sortOrder || 0),
|
sortOrder: String(item.sortOrder || 0),
|
||||||
status: item.status || "active"
|
status: item.status || "active",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,31 +11,30 @@ import { Button } from "@/shared/ui/Button.jsx";
|
|||||||
import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx";
|
import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx";
|
||||||
import {
|
import {
|
||||||
AdminActionIconButton,
|
AdminActionIconButton,
|
||||||
AdminFilterSelect,
|
AdminFilterResetButton,
|
||||||
AdminListBody,
|
AdminListBody,
|
||||||
AdminListPage,
|
AdminListPage,
|
||||||
AdminListToolbar,
|
AdminListToolbar,
|
||||||
AdminRowActions,
|
AdminRowActions,
|
||||||
AdminSearchBox,
|
|
||||||
adminListClasses
|
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
import { createRegionColumnFilter, RegionFilterControl } from "@/shared/ui/RegionFilterControl.jsx";
|
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
||||||
import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
|
import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
["", "全部状态"],
|
["", "全部状态"],
|
||||||
["active", "启用"],
|
["active", "启用"],
|
||||||
["disabled", "关闭"]
|
["disabled", "关闭"],
|
||||||
];
|
];
|
||||||
|
|
||||||
const platformOptions = [
|
const platformOptions = [
|
||||||
["", "全部平台"],
|
["", "全部平台"],
|
||||||
["android", "安卓"],
|
["android", "安卓"],
|
||||||
["ios", "iOS"]
|
["ios", "iOS"],
|
||||||
];
|
];
|
||||||
|
|
||||||
export function BannerConfigPage() {
|
export function BannerConfigPage() {
|
||||||
@ -48,32 +47,20 @@ export function BannerConfigPage() {
|
|||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
actions={
|
actions={
|
||||||
page.abilities.canUpdate ? (
|
page.abilities.canUpdate ? (
|
||||||
<Button startIcon={<AddOutlined fontSize="small" />} variant="primary" onClick={page.openCreate}>
|
<Button
|
||||||
|
startIcon={<AddOutlined fontSize="small" />}
|
||||||
|
variant="primary"
|
||||||
|
onClick={page.openCreate}
|
||||||
|
>
|
||||||
新增BANNER
|
新增BANNER
|
||||||
</Button>
|
</Button>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
filters={
|
filters={
|
||||||
<>
|
<AdminFilterResetButton
|
||||||
<AdminSearchBox value={page.keyword} onChange={page.setKeyword} placeholder="搜索参数、国家、描述..." />
|
disabled={!page.keyword && !page.platform && !page.status && !page.regionId && !page.countryCode}
|
||||||
<AdminFilterSelect label="平台" options={platformOptions} value={page.platform} onChange={page.setPlatform} />
|
onClick={page.resetFilters}
|
||||||
<AdminFilterSelect label="状态" options={statusOptions} value={page.status} onChange={page.setStatus} />
|
|
||||||
<RegionFilterControl
|
|
||||||
emptyLabel="全部区域"
|
|
||||||
loading={page.loadingRegions}
|
|
||||||
options={page.regionOptions}
|
|
||||||
value={page.regionId}
|
|
||||||
onChange={page.setRegionId}
|
|
||||||
/>
|
/>
|
||||||
<CountrySelect
|
|
||||||
className={adminListClasses.statusSelect}
|
|
||||||
disabled={page.loadingCountries}
|
|
||||||
emptyLabel="全部国家"
|
|
||||||
options={page.countryOptions}
|
|
||||||
value={page.countryCode}
|
|
||||||
onChange={page.setCountryCode}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
@ -89,7 +76,9 @@ export function BannerConfigPage() {
|
|||||||
loading={page.loadingAction === "create" || page.loadingAction === "edit"}
|
loading={page.loadingAction === "create" || page.loadingAction === "edit"}
|
||||||
open={Boolean(page.activeAction)}
|
open={Boolean(page.activeAction)}
|
||||||
size="narrow"
|
size="narrow"
|
||||||
submitDisabled={!page.abilities.canUpdate || page.loadingAction === "create" || page.loadingAction === "edit"}
|
submitDisabled={
|
||||||
|
!page.abilities.canUpdate || page.loadingAction === "create" || page.loadingAction === "edit"
|
||||||
|
}
|
||||||
title={page.editingItem ? "编辑BANNER" : "新增BANNER"}
|
title={page.editingItem ? "编辑BANNER" : "新增BANNER"}
|
||||||
onClose={page.closeDialog}
|
onClose={page.closeDialog}
|
||||||
onSubmit={page.submitBanner}
|
onSubmit={page.submitBanner}
|
||||||
@ -100,7 +89,14 @@ export function BannerConfigPage() {
|
|||||||
value={page.form.coverUrl}
|
value={page.form.coverUrl}
|
||||||
onChange={(coverUrl) => page.setForm({ coverUrl })}
|
onChange={(coverUrl) => page.setForm({ coverUrl })}
|
||||||
/>
|
/>
|
||||||
<TextField disabled={!page.abilities.canUpdate} label="类型" required select value={page.form.bannerType} onChange={(event) => page.setForm({ bannerType: event.target.value })}>
|
<TextField
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="类型"
|
||||||
|
required
|
||||||
|
select
|
||||||
|
value={page.form.bannerType}
|
||||||
|
onChange={(event) => page.setForm({ bannerType: event.target.value })}
|
||||||
|
>
|
||||||
<MenuItem value="h5">H5</MenuItem>
|
<MenuItem value="h5">H5</MenuItem>
|
||||||
<MenuItem value="app">APP</MenuItem>
|
<MenuItem value="app">APP</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
@ -111,11 +107,25 @@ export function BannerConfigPage() {
|
|||||||
value={page.form.param}
|
value={page.form.param}
|
||||||
onChange={(event) => page.setForm({ param: event.target.value })}
|
onChange={(event) => page.setForm({ param: event.target.value })}
|
||||||
/>
|
/>
|
||||||
<TextField disabled={!page.abilities.canUpdate} label="状态" required select value={page.form.status} onChange={(event) => page.setForm({ status: event.target.value })}>
|
<TextField
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="状态"
|
||||||
|
required
|
||||||
|
select
|
||||||
|
value={page.form.status}
|
||||||
|
onChange={(event) => page.setForm({ status: event.target.value })}
|
||||||
|
>
|
||||||
<MenuItem value="active">启用</MenuItem>
|
<MenuItem value="active">启用</MenuItem>
|
||||||
<MenuItem value="disabled">关闭</MenuItem>
|
<MenuItem value="disabled">关闭</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField disabled={!page.abilities.canUpdate} label="平台" required select value={page.form.platform} onChange={(event) => page.setForm({ platform: event.target.value })}>
|
<TextField
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="平台"
|
||||||
|
required
|
||||||
|
select
|
||||||
|
value={page.form.platform}
|
||||||
|
onChange={(event) => page.setForm({ platform: event.target.value })}
|
||||||
|
>
|
||||||
<MenuItem value="android">安卓</MenuItem>
|
<MenuItem value="android">安卓</MenuItem>
|
||||||
<MenuItem value="ios">iOS</MenuItem>
|
<MenuItem value="ios">iOS</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
@ -162,49 +172,86 @@ function bannerColumns(page) {
|
|||||||
key: "cover",
|
key: "cover",
|
||||||
label: "封面图",
|
label: "封面图",
|
||||||
render: (item) => <BannerCover src={item.coverUrl} />,
|
render: (item) => <BannerCover src={item.coverUrl} />,
|
||||||
width: "minmax(112px, 0.55fr)"
|
width: "minmax(112px, 0.55fr)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "target",
|
key: "target",
|
||||||
label: "类型 / 参数",
|
label: "类型 / 参数",
|
||||||
render: (item) => <Stack primary={typeLabel(item.bannerType)} secondary={item.param || "-"} />,
|
render: (item) => <Stack primary={typeLabel(item.bannerType)} secondary={item.param || "-"} />,
|
||||||
width: "minmax(260px, 1.3fr)"
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索参数、国家、描述",
|
||||||
|
value: page.keyword,
|
||||||
|
onChange: page.setKeyword,
|
||||||
|
}),
|
||||||
|
width: "minmax(260px, 1.3fr)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "state",
|
key: "platform",
|
||||||
label: "平台 / 状态",
|
label: "平台",
|
||||||
render: (item) => <Stack primary={platformLabel(item.platform)} secondary={<StatusBadge status={item.status} />} />,
|
render: (item) => platformLabel(item.platform),
|
||||||
width: "minmax(140px, 0.75fr)"
|
filter: createOptionsColumnFilter({
|
||||||
|
options: platformOptions,
|
||||||
|
placeholder: "搜索平台",
|
||||||
|
value: page.platform,
|
||||||
|
onChange: page.setPlatform,
|
||||||
|
}),
|
||||||
|
width: "minmax(110px, 0.6fr)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "scope",
|
key: "status",
|
||||||
label: "区域 / 国家",
|
label: "状态",
|
||||||
|
render: (item) => <StatusBadge status={item.status} />,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: statusOptions,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.setStatus,
|
||||||
|
}),
|
||||||
|
width: "minmax(100px, 0.55fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "region",
|
||||||
|
label: "区域",
|
||||||
|
render: (item) => regionLabel(page.regionOptions, item.regionId),
|
||||||
filter: createRegionColumnFilter({
|
filter: createRegionColumnFilter({
|
||||||
loading: page.loadingRegions,
|
loading: page.loadingRegions,
|
||||||
options: page.regionOptions,
|
options: page.regionOptions,
|
||||||
value: page.regionId,
|
value: page.regionId,
|
||||||
onChange: page.setRegionId
|
onChange: page.setRegionId,
|
||||||
}),
|
}),
|
||||||
render: (item) => <Stack primary={regionLabel(page.regionOptions, item.regionId)} secondary={countryLabel(page.countryOptions, item.countryCode)} />,
|
width: "minmax(150px, 0.8fr)",
|
||||||
width: "minmax(180px, 0.95fr)"
|
},
|
||||||
|
{
|
||||||
|
key: "country",
|
||||||
|
label: "国家",
|
||||||
|
render: (item) => countryLabel(page.countryOptions, item.countryCode),
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
emptyLabel: "全部国家",
|
||||||
|
loading: page.loadingCountries,
|
||||||
|
options: page.countryOptions,
|
||||||
|
placeholder: "搜索国家",
|
||||||
|
value: page.countryCode,
|
||||||
|
onChange: page.setCountryCode,
|
||||||
|
}),
|
||||||
|
width: "minmax(150px, 0.8fr)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "sortOrder",
|
key: "sortOrder",
|
||||||
label: "排序",
|
label: "排序",
|
||||||
width: "minmax(80px, 0.4fr)"
|
width: "minmax(80px, 0.4fr)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "description",
|
key: "description",
|
||||||
label: "描述",
|
label: "描述",
|
||||||
render: (item) => item.description || "-",
|
render: (item) => item.description || "-",
|
||||||
width: "minmax(180px, 0.9fr)"
|
width: "minmax(180px, 0.9fr)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "updatedAtMs",
|
key: "updatedAtMs",
|
||||||
label: "更新时间",
|
label: "更新时间",
|
||||||
render: (item) => formatMillis(item.updatedAtMs),
|
render: (item) => formatMillis(item.updatedAtMs),
|
||||||
width: "minmax(160px, 0.8fr)"
|
width: "minmax(160px, 0.8fr)",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!page.abilities.canUpdate) {
|
if (!page.abilities.canUpdate) {
|
||||||
@ -217,8 +264,8 @@ function bannerColumns(page) {
|
|||||||
key: "actions",
|
key: "actions",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
render: (item) => <BannerActions item={item} page={page} />,
|
render: (item) => <BannerActions item={item} page={page} />,
|
||||||
width: "minmax(96px, 0.45fr)"
|
width: "minmax(96px, 0.45fr)",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,10 +284,18 @@ function BannerActions({ item, page }) {
|
|||||||
const deleting = page.loadingAction === `delete:${item.id}`;
|
const deleting = page.loadingAction === `delete:${item.id}`;
|
||||||
return (
|
return (
|
||||||
<AdminRowActions>
|
<AdminRowActions>
|
||||||
<AdminActionIconButton disabled={Boolean(page.loadingAction)} label="编辑" onClick={() => page.openEdit(item)}>
|
<AdminActionIconButton
|
||||||
|
disabled={Boolean(page.loadingAction)}
|
||||||
|
label="编辑"
|
||||||
|
onClick={() => page.openEdit(item)}
|
||||||
|
>
|
||||||
<EditOutlined fontSize="small" />
|
<EditOutlined fontSize="small" />
|
||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
<AdminActionIconButton disabled={deleting || Boolean(page.loadingAction)} label="删除" onClick={() => page.removeBanner(item)}>
|
<AdminActionIconButton
|
||||||
|
disabled={deleting || Boolean(page.loadingAction)}
|
||||||
|
label="删除"
|
||||||
|
onClick={() => page.removeBanner(item)}
|
||||||
|
>
|
||||||
<DeleteOutlineOutlined fontSize="small" />
|
<DeleteOutlineOutlined fontSize="small" />
|
||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
</AdminRowActions>
|
</AdminRowActions>
|
||||||
@ -249,7 +304,15 @@ function BannerActions({ item, page }) {
|
|||||||
|
|
||||||
function CountrySelect({ className, disabled, emptyLabel, label = "国家", onChange, options, value }) {
|
function CountrySelect({ className, disabled, emptyLabel, label = "国家", onChange, options, value }) {
|
||||||
return (
|
return (
|
||||||
<TextField className={className} disabled={disabled} label={label} select size="small" value={value || ""} onChange={(event) => onChange(event.target.value)}>
|
<TextField
|
||||||
|
className={className}
|
||||||
|
disabled={disabled}
|
||||||
|
label={label}
|
||||||
|
select
|
||||||
|
size="small"
|
||||||
|
value={value || ""}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
>
|
||||||
<MenuItem value="">{emptyLabel}</MenuItem>
|
<MenuItem value="">{emptyLabel}</MenuItem>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<MenuItem key={option.value} value={option.value}>
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
|||||||
@ -3,13 +3,7 @@ import { parseForm } from "@/shared/forms/validation";
|
|||||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||||
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
import {
|
import { banAppUser, listAppUsers, setAppUserPassword, unbanAppUser, updateAppUser } from "@/features/app-users/api";
|
||||||
banAppUser,
|
|
||||||
listAppUsers,
|
|
||||||
setAppUserPassword,
|
|
||||||
unbanAppUser,
|
|
||||||
updateAppUser
|
|
||||||
} from "@/features/app-users/api";
|
|
||||||
import { useAppUserAbilities } from "@/features/app-users/permissions.js";
|
import { useAppUserAbilities } from "@/features/app-users/permissions.js";
|
||||||
import { appUserCountrySchema, appUserPasswordSchema, appUserUpdateSchema } from "@/features/app-users/schema";
|
import { appUserCountrySchema, appUserPasswordSchema, appUserUpdateSchema } from "@/features/app-users/schema";
|
||||||
import { useCountryOptions } from "@/features/host-org/hooks/useCountryOptions.js";
|
import { useCountryOptions } from "@/features/host-org/hooks/useCountryOptions.js";
|
||||||
@ -37,17 +31,22 @@ export function useAppUsersPage() {
|
|||||||
const filters = useMemo(
|
const filters = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
keyword: query,
|
keyword: query,
|
||||||
status
|
status,
|
||||||
}),
|
}),
|
||||||
[query, status]
|
[query, status],
|
||||||
);
|
);
|
||||||
const { data = emptyData, error, loading, reload } = usePaginatedQuery({
|
const {
|
||||||
|
data = emptyData,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
reload,
|
||||||
|
} = usePaginatedQuery({
|
||||||
errorMessage: "加载用户列表失败",
|
errorMessage: "加载用户列表失败",
|
||||||
fetcher: listAppUsers,
|
fetcher: listAppUsers,
|
||||||
filters,
|
filters,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
queryKey: ["app-users", filters, page]
|
queryKey: ["app-users", filters, page],
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeQuery = (value) => {
|
const changeQuery = (value) => {
|
||||||
@ -60,12 +59,18 @@ export function useAppUsersPage() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setStatus("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
const openEdit = (user) => {
|
const openEdit = (user) => {
|
||||||
setActiveUser(user);
|
setActiveUser(user);
|
||||||
setEditForm({
|
setEditForm({
|
||||||
avatar: user.avatar || "",
|
avatar: user.avatar || "",
|
||||||
gender: user.gender || "",
|
gender: user.gender || "",
|
||||||
username: user.username || ""
|
username: user.username || "",
|
||||||
});
|
});
|
||||||
setActiveAction("edit");
|
setActiveAction("edit");
|
||||||
};
|
};
|
||||||
@ -131,7 +136,7 @@ export function useAppUsersPage() {
|
|||||||
confirmText: banned ? "解封" : "封禁",
|
confirmText: banned ? "解封" : "封禁",
|
||||||
message: `${user.username || user.displayUserId || user.userId} 的状态会立即变更。`,
|
message: `${user.username || user.displayUserId || user.userId} 的状态会立即变更。`,
|
||||||
title: banned ? "解封用户" : "封禁用户",
|
title: banned ? "解封用户" : "封禁用户",
|
||||||
tone: banned ? "primary" : "danger"
|
tone: banned ? "primary" : "danger",
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
@ -180,6 +185,7 @@ export function useAppUsersPage() {
|
|||||||
passwordForm,
|
passwordForm,
|
||||||
query,
|
query,
|
||||||
reload,
|
reload,
|
||||||
|
resetFilters,
|
||||||
setCountryForm,
|
setCountryForm,
|
||||||
setEditForm,
|
setEditForm,
|
||||||
setPage,
|
setPage,
|
||||||
@ -188,7 +194,7 @@ export function useAppUsersPage() {
|
|||||||
submitCountry,
|
submitCountry,
|
||||||
submitEdit,
|
submitEdit,
|
||||||
submitPassword,
|
submitPassword,
|
||||||
toggleBan
|
toggleBan,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,17 +10,13 @@ import Tooltip from "@mui/material/Tooltip";
|
|||||||
import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx";
|
import { AdminFormDialog } from "@/shared/ui/AdminFormDialog.jsx";
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
|
import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
import { SearchBox } from "@/shared/ui/SearchBox.jsx";
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import { StatusSelect } from "@/shared/ui/StatusSelect.jsx";
|
|
||||||
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import {
|
import { appUserStatusFilters, appUserStatusLabels, genderOptions } from "@/features/app-users/constants.js";
|
||||||
appUserStatusFilters,
|
|
||||||
appUserStatusLabels,
|
|
||||||
genderOptions
|
|
||||||
} from "@/features/app-users/constants.js";
|
|
||||||
import { useAppUsersPage } from "@/features/app-users/hooks/useAppUsersPage.js";
|
import { useAppUsersPage } from "@/features/app-users/hooks/useAppUsersPage.js";
|
||||||
import styles from "@/features/app-users/app-users.module.css";
|
import styles from "@/features/app-users/app-users.module.css";
|
||||||
|
|
||||||
@ -29,7 +25,7 @@ const columns = [
|
|||||||
key: "identity",
|
key: "identity",
|
||||||
label: "用户",
|
label: "用户",
|
||||||
width: "minmax(240px, 1.5fr)",
|
width: "minmax(240px, 1.5fr)",
|
||||||
render: (user) => <UserIdentity user={user} />
|
render: (user) => <UserIdentity user={user} />,
|
||||||
},
|
},
|
||||||
{ key: "coin", label: "金币", width: "minmax(90px, 0.6fr)", render: (user) => formatNumber(user.coin) },
|
{ key: "coin", label: "金币", width: "minmax(90px, 0.6fr)", render: (user) => formatNumber(user.coin) },
|
||||||
{ key: "diamond", label: "钻石", width: "minmax(90px, 0.6fr)", render: (user) => formatNumber(user.diamond) },
|
{ key: "diamond", label: "钻石", width: "minmax(90px, 0.6fr)", render: (user) => formatNumber(user.diamond) },
|
||||||
@ -37,7 +33,7 @@ const columns = [
|
|||||||
key: "status",
|
key: "status",
|
||||||
label: "状态",
|
label: "状态",
|
||||||
width: "minmax(92px, 0.6fr)",
|
width: "minmax(92px, 0.6fr)",
|
||||||
render: (user) => <UserStatus status={user.status} />
|
render: (user) => <UserStatus status={user.status} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "time",
|
key: "time",
|
||||||
@ -48,8 +44,8 @@ const columns = [
|
|||||||
<span>{formatMillis(user.createdAtMs)}</span>
|
<span>{formatMillis(user.createdAtMs)}</span>
|
||||||
<span className={styles.meta}>{formatMillis(user.lastActiveAtMs)}</span>
|
<span className={styles.meta}>{formatMillis(user.lastActiveAtMs)}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function AppUserListPage() {
|
export function AppUserListPage() {
|
||||||
@ -57,20 +53,39 @@ export function AppUserListPage() {
|
|||||||
const items = page.data.items || [];
|
const items = page.data.items || [];
|
||||||
const total = page.data.total || 0;
|
const total = page.data.total || 0;
|
||||||
const tableColumns = [
|
const tableColumns = [
|
||||||
columns[0],
|
{
|
||||||
|
...columns[0],
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索名称、短 ID、用户 ID",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "location",
|
key: "location",
|
||||||
label: "国家 / 区域",
|
label: "国家 / 区域",
|
||||||
width: "minmax(170px, 1fr)",
|
width: "minmax(170px, 1fr)",
|
||||||
render: (user) => <UserLocation page={page} user={user} />
|
render: (user) => <UserLocation page={page} user={user} />,
|
||||||
},
|
},
|
||||||
...columns.slice(1),
|
...columns.slice(1).map((column) =>
|
||||||
|
column.key === "status"
|
||||||
|
? {
|
||||||
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: appUserStatusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.changeStatus,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: column,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
key: "actions",
|
key: "actions",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: "minmax(148px, 0.8fr)",
|
width: "minmax(148px, 0.8fr)",
|
||||||
render: (user) => <UserActions page={page} user={user} />
|
render: (user) => <UserActions page={page} user={user} />,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -78,20 +93,26 @@ export function AppUserListPage() {
|
|||||||
<div className={styles.contentPanel}>
|
<div className={styles.contentPanel}>
|
||||||
<div className={styles.toolbar}>
|
<div className={styles.toolbar}>
|
||||||
<section className={styles.filters}>
|
<section className={styles.filters}>
|
||||||
<SearchBox className={styles.search} value={page.query} onChange={page.changeQuery} placeholder="搜索名称、短 ID、用户 ID..." />
|
<AdminFilterResetButton disabled={!page.query && !page.status} onClick={page.resetFilters} />
|
||||||
<StatusSelect
|
|
||||||
className={styles.statusSelect}
|
|
||||||
options={appUserStatusFilters}
|
|
||||||
value={page.status}
|
|
||||||
onChange={page.changeStatus}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<div className={styles.listBlock}>
|
<div className={styles.listBlock}>
|
||||||
<DataTable columns={tableColumns} items={items} minWidth="1220px" rowKey={(user) => user.userId} />
|
<DataTable
|
||||||
{total > 0 ? <PaginationBar page={page.page} pageSize={page.data.pageSize || 10} total={total} onPageChange={page.setPage} /> : null}
|
columns={tableColumns}
|
||||||
|
items={items}
|
||||||
|
minWidth="1220px"
|
||||||
|
rowKey={(user) => user.userId}
|
||||||
|
/>
|
||||||
|
{total > 0 ? (
|
||||||
|
<PaginationBar
|
||||||
|
page={page.page}
|
||||||
|
pageSize={page.data.pageSize || 10}
|
||||||
|
total={total}
|
||||||
|
onPageChange={page.setPage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</DataState>
|
</DataState>
|
||||||
</div>
|
</div>
|
||||||
@ -104,11 +125,29 @@ export function AppUserListPage() {
|
|||||||
onClose={page.closeAction}
|
onClose={page.closeAction}
|
||||||
onSubmit={page.submitEdit}
|
onSubmit={page.submitEdit}
|
||||||
>
|
>
|
||||||
<TextField disabled={!page.abilities.canUpdate} label="用户名称" value={page.editForm.username} onChange={(event) => page.setEditForm({ ...page.editForm, username: event.target.value })} />
|
<TextField
|
||||||
<UploadField disabled={!page.abilities.canUpdate || !page.abilities.canUpload} label="头像" value={page.editForm.avatar} onChange={(avatar) => page.setEditForm({ ...page.editForm, avatar })} />
|
disabled={!page.abilities.canUpdate}
|
||||||
<TextField disabled={!page.abilities.canUpdate} label="性别" select value={page.editForm.gender} onChange={(event) => page.setEditForm({ ...page.editForm, gender: event.target.value })}>
|
label="用户名称"
|
||||||
|
value={page.editForm.username}
|
||||||
|
onChange={(event) => page.setEditForm({ ...page.editForm, username: event.target.value })}
|
||||||
|
/>
|
||||||
|
<UploadField
|
||||||
|
disabled={!page.abilities.canUpdate || !page.abilities.canUpload}
|
||||||
|
label="头像"
|
||||||
|
value={page.editForm.avatar}
|
||||||
|
onChange={(avatar) => page.setEditForm({ ...page.editForm, avatar })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="性别"
|
||||||
|
select
|
||||||
|
value={page.editForm.gender}
|
||||||
|
onChange={(event) => page.setEditForm({ ...page.editForm, gender: event.target.value })}
|
||||||
|
>
|
||||||
{genderOptions.map(([value, label]) => (
|
{genderOptions.map(([value, label]) => (
|
||||||
<MenuItem key={value || "empty"} value={value}>{label}</MenuItem>
|
<MenuItem key={value || "empty"} value={value}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
</ActionModal>
|
</ActionModal>
|
||||||
@ -142,7 +181,13 @@ export function AppUserListPage() {
|
|||||||
onClose={page.closeAction}
|
onClose={page.closeAction}
|
||||||
onSubmit={page.submitPassword}
|
onSubmit={page.submitPassword}
|
||||||
>
|
>
|
||||||
<TextField disabled={!page.abilities.canPassword} label="新密码" type="password" value={page.passwordForm.password} onChange={(event) => page.setPasswordForm({ ...page.passwordForm, password: event.target.value })} />
|
<TextField
|
||||||
|
disabled={!page.abilities.canPassword}
|
||||||
|
label="新密码"
|
||||||
|
type="password"
|
||||||
|
value={page.passwordForm.password}
|
||||||
|
onChange={(event) => page.setPasswordForm({ ...page.passwordForm, password: event.target.value })}
|
||||||
|
/>
|
||||||
</ActionModal>
|
</ActionModal>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -201,7 +246,12 @@ function UserActions({ page, user }) {
|
|||||||
{page.abilities.canStatus ? (
|
{page.abilities.canStatus ? (
|
||||||
<Tooltip arrow title={banned ? "解封" : "封禁"}>
|
<Tooltip arrow title={banned ? "解封" : "封禁"}>
|
||||||
<span>
|
<span>
|
||||||
<IconButton disabled={page.loadingAction === `status-${user.userId}`} label={banned ? "解封" : "封禁"} tone={banned ? "success" : "danger"} onClick={() => page.toggleBan(user)}>
|
<IconButton
|
||||||
|
disabled={page.loadingAction === `status-${user.userId}`}
|
||||||
|
label={banned ? "解封" : "封禁"}
|
||||||
|
tone={banned ? "success" : "danger"}
|
||||||
|
onClick={() => page.toggleBan(user)}
|
||||||
|
>
|
||||||
{banned ? <LockOpenOutlined fontSize="small" /> : <BlockOutlined fontSize="small" />}
|
{banned ? <LockOpenOutlined fontSize="small" /> : <BlockOutlined fontSize="small" />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
createCountry,
|
createCountry,
|
||||||
createCoinSeller,
|
createCoinSeller,
|
||||||
createRegion,
|
createRegion,
|
||||||
|
creditCoinSellerStock,
|
||||||
disableRegion,
|
disableRegion,
|
||||||
listAgencies,
|
listAgencies,
|
||||||
listBDs,
|
listBDs,
|
||||||
@ -13,7 +14,7 @@ import {
|
|||||||
listRegions,
|
listRegions,
|
||||||
replaceRegionCountries,
|
replaceRegionCountries,
|
||||||
setCoinSellerStatus,
|
setCoinSellerStatus,
|
||||||
updateCountry
|
updateCountry,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -24,7 +25,9 @@ afterEach(() => {
|
|||||||
test("host org list APIs use generated admin paths and filters", async () => {
|
test("host org list APIs use generated admin paths and filters", async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
"fetch",
|
"fetch",
|
||||||
vi.fn(async () => new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 10, total: 0 } })))
|
vi.fn(
|
||||||
|
async () => new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 10, total: 0 } })),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await listBDs({ page: 1, page_size: 10, parent_leader_user_id: 21, region_id: 7, status: "active" });
|
await listBDs({ page: 1, page_size: 10, parent_leader_user_id: 21, region_id: 7, status: "active" });
|
||||||
@ -33,6 +36,12 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
|||||||
await listCoinSellers({ page: 1, page_size: 10, region_id: 7, status: "active" });
|
await listCoinSellers({ page: 1, page_size: 10, region_id: 7, status: "active" });
|
||||||
await createCoinSeller({ commandId: "coin-seller-test", reason: "contact", targetUserId: 1001 });
|
await createCoinSeller({ commandId: "coin-seller-test", reason: "contact", targetUserId: 1001 });
|
||||||
await setCoinSellerStatus(1001, { commandId: "coin-seller-status-test", reason: "disable", status: "disabled" });
|
await setCoinSellerStatus(1001, { commandId: "coin-seller-status-test", reason: "disable", status: "disabled" });
|
||||||
|
await creditCoinSellerStock(1001, {
|
||||||
|
coinAmount: 8000000,
|
||||||
|
commandId: "coin-seller-stock-test",
|
||||||
|
rechargeAmount: "100.000000",
|
||||||
|
type: "usdt_purchase",
|
||||||
|
});
|
||||||
|
|
||||||
const [bdUrl, bdInit] = vi.mocked(fetch).mock.calls[0];
|
const [bdUrl, bdInit] = vi.mocked(fetch).mock.calls[0];
|
||||||
const [agencyUrl] = vi.mocked(fetch).mock.calls[1];
|
const [agencyUrl] = vi.mocked(fetch).mock.calls[1];
|
||||||
@ -40,6 +49,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
|||||||
const [coinSellerUrl, coinSellerInit] = vi.mocked(fetch).mock.calls[3];
|
const [coinSellerUrl, coinSellerInit] = vi.mocked(fetch).mock.calls[3];
|
||||||
const [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[4];
|
const [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[4];
|
||||||
const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[5];
|
const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[5];
|
||||||
|
const [coinSellerStockUrl, coinSellerStockInit] = vi.mocked(fetch).mock.calls[6];
|
||||||
|
|
||||||
expect(String(bdUrl)).toContain("/api/v1/admin/bds?");
|
expect(String(bdUrl)).toContain("/api/v1/admin/bds?");
|
||||||
expect(String(bdUrl)).toContain("parent_leader_user_id=21");
|
expect(String(bdUrl)).toContain("parent_leader_user_id=21");
|
||||||
@ -58,16 +68,23 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
|||||||
expect(coinSellerCreateInit?.method).toBe("POST");
|
expect(coinSellerCreateInit?.method).toBe("POST");
|
||||||
expect(String(coinSellerStatusUrl)).toContain("/api/v1/admin/coin-sellers/1001/status");
|
expect(String(coinSellerStatusUrl)).toContain("/api/v1/admin/coin-sellers/1001/status");
|
||||||
expect(coinSellerStatusInit?.method).toBe("PATCH");
|
expect(coinSellerStatusInit?.method).toBe("PATCH");
|
||||||
|
expect(String(coinSellerStockUrl)).toContain("/api/v1/admin/coin-sellers/1001/stock-credits");
|
||||||
|
expect(coinSellerStockInit?.method).toBe("POST");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("country and region APIs use generated admin paths", async () => {
|
test("country and region APIs use generated admin paths", async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
"fetch",
|
"fetch",
|
||||||
vi.fn(async () => new Response(JSON.stringify({ code: 0, data: { items: [], total: 0 } })))
|
vi.fn(async () => new Response(JSON.stringify({ code: 0, data: { items: [], total: 0 } }))),
|
||||||
);
|
);
|
||||||
|
|
||||||
await listCountries({ enabled: true });
|
await listCountries({ enabled: true });
|
||||||
await createCountry({ countryCode: "US", countryDisplayName: "United States", countryName: "United States", enabled: true });
|
await createCountry({
|
||||||
|
countryCode: "US",
|
||||||
|
countryDisplayName: "United States",
|
||||||
|
countryName: "United States",
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
await updateCountry(12, { countryDisplayName: "United States", countryName: "United States" });
|
await updateCountry(12, { countryDisplayName: "United States", countryName: "United States" });
|
||||||
await listRegions({ status: "active" });
|
await listRegions({ status: "active" });
|
||||||
await createRegion({ countries: ["US"], name: "North America", regionCode: "NA" });
|
await createRegion({ countries: ["US"], name: "North America", regionCode: "NA" });
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import type {
|
|||||||
BDProfileDto,
|
BDProfileDto,
|
||||||
BDStatusPayload,
|
BDStatusPayload,
|
||||||
CoinSellerDto,
|
CoinSellerDto,
|
||||||
|
CoinSellerStockCreditDto,
|
||||||
|
CoinSellerStockCreditPayload,
|
||||||
CoinSellerStatusPayload,
|
CoinSellerStatusPayload,
|
||||||
CountryDto,
|
CountryDto,
|
||||||
CountryPayload,
|
CountryPayload,
|
||||||
@ -25,21 +27,21 @@ import type {
|
|||||||
RegionCountriesPayload,
|
RegionCountriesPayload,
|
||||||
RegionDto,
|
RegionDto,
|
||||||
RegionPayload,
|
RegionPayload,
|
||||||
RegionUpdatePayload
|
RegionUpdatePayload,
|
||||||
} from "@/shared/api/types";
|
} from "@/shared/api/types";
|
||||||
|
|
||||||
export function listCountries(query: PageQuery = {}): Promise<ApiList<CountryDto>> {
|
export function listCountries(query: PageQuery = {}): Promise<ApiList<CountryDto>> {
|
||||||
const endpoint = API_ENDPOINTS.listCountries;
|
const endpoint = API_ENDPOINTS.listCountries;
|
||||||
return apiRequest<ApiList<CountryDto>>(apiEndpointPath(API_OPERATIONS.listCountries), {
|
return apiRequest<ApiList<CountryDto>>(apiEndpointPath(API_OPERATIONS.listCountries), {
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
query
|
query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCountry(countryId: EntityId): Promise<CountryDto> {
|
export function getCountry(countryId: EntityId): Promise<CountryDto> {
|
||||||
const endpoint = API_ENDPOINTS.getCountry;
|
const endpoint = API_ENDPOINTS.getCountry;
|
||||||
return apiRequest<CountryDto>(apiEndpointPath(API_OPERATIONS.getCountry, { country_id: countryId }), {
|
return apiRequest<CountryDto>(apiEndpointPath(API_OPERATIONS.getCountry, { country_id: countryId }), {
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,43 +49,46 @@ export function createCountry(payload: CountryPayload): Promise<CountryDto> {
|
|||||||
const endpoint = API_ENDPOINTS.createCountry;
|
const endpoint = API_ENDPOINTS.createCountry;
|
||||||
return apiRequest<CountryDto, CountryPayload>(apiEndpointPath(API_OPERATIONS.createCountry), {
|
return apiRequest<CountryDto, CountryPayload>(apiEndpointPath(API_OPERATIONS.createCountry), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateCountry(countryId: EntityId, payload: CountryUpdatePayload): Promise<CountryDto> {
|
export function updateCountry(countryId: EntityId, payload: CountryUpdatePayload): Promise<CountryDto> {
|
||||||
const endpoint = API_ENDPOINTS.updateCountry;
|
const endpoint = API_ENDPOINTS.updateCountry;
|
||||||
return apiRequest<CountryDto, CountryUpdatePayload>(apiEndpointPath(API_OPERATIONS.updateCountry, { country_id: countryId }), {
|
return apiRequest<CountryDto, CountryUpdatePayload>(
|
||||||
|
apiEndpointPath(API_OPERATIONS.updateCountry, { country_id: countryId }),
|
||||||
|
{
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enableCountry(countryId: EntityId): Promise<CountryDto> {
|
export function enableCountry(countryId: EntityId): Promise<CountryDto> {
|
||||||
const endpoint = API_ENDPOINTS.enableCountry;
|
const endpoint = API_ENDPOINTS.enableCountry;
|
||||||
return apiRequest<CountryDto>(apiEndpointPath(API_OPERATIONS.enableCountry, { country_id: countryId }), {
|
return apiRequest<CountryDto>(apiEndpointPath(API_OPERATIONS.enableCountry, { country_id: countryId }), {
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function disableCountry(countryId: EntityId): Promise<CountryDto> {
|
export function disableCountry(countryId: EntityId): Promise<CountryDto> {
|
||||||
const endpoint = API_ENDPOINTS.disableCountry;
|
const endpoint = API_ENDPOINTS.disableCountry;
|
||||||
return apiRequest<CountryDto>(apiEndpointPath(API_OPERATIONS.disableCountry, { country_id: countryId }), {
|
return apiRequest<CountryDto>(apiEndpointPath(API_OPERATIONS.disableCountry, { country_id: countryId }), {
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteCountry(countryId: EntityId): Promise<CountryDto> {
|
export function deleteCountry(countryId: EntityId): Promise<CountryDto> {
|
||||||
const endpoint = API_ENDPOINTS.deleteCountry;
|
const endpoint = API_ENDPOINTS.deleteCountry;
|
||||||
return apiRequest<CountryDto>(apiEndpointPath(API_OPERATIONS.deleteCountry, { country_id: countryId }), {
|
return apiRequest<CountryDto>(apiEndpointPath(API_OPERATIONS.deleteCountry, { country_id: countryId }), {
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRegion(regionId: EntityId): Promise<RegionDto> {
|
export function getRegion(regionId: EntityId): Promise<RegionDto> {
|
||||||
const endpoint = API_ENDPOINTS.getRegion;
|
const endpoint = API_ENDPOINTS.getRegion;
|
||||||
return apiRequest<RegionDto>(apiEndpointPath(API_OPERATIONS.getRegion, { region_id: regionId }), {
|
return apiRequest<RegionDto>(apiEndpointPath(API_OPERATIONS.getRegion, { region_id: regionId }), {
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,16 +96,19 @@ export function createRegion(payload: RegionPayload): Promise<RegionDto> {
|
|||||||
const endpoint = API_ENDPOINTS.createRegion;
|
const endpoint = API_ENDPOINTS.createRegion;
|
||||||
return apiRequest<RegionDto, RegionPayload>(apiEndpointPath(API_OPERATIONS.createRegion), {
|
return apiRequest<RegionDto, RegionPayload>(apiEndpointPath(API_OPERATIONS.createRegion), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateRegion(regionId: EntityId, payload: RegionUpdatePayload): Promise<RegionDto> {
|
export function updateRegion(regionId: EntityId, payload: RegionUpdatePayload): Promise<RegionDto> {
|
||||||
const endpoint = API_ENDPOINTS.updateRegion;
|
const endpoint = API_ENDPOINTS.updateRegion;
|
||||||
return apiRequest<RegionDto, RegionUpdatePayload>(apiEndpointPath(API_OPERATIONS.updateRegion, { region_id: regionId }), {
|
return apiRequest<RegionDto, RegionUpdatePayload>(
|
||||||
|
apiEndpointPath(API_OPERATIONS.updateRegion, { region_id: regionId }),
|
||||||
|
{
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceRegionCountries(regionId: EntityId, payload: RegionCountriesPayload): Promise<RegionDto> {
|
export function replaceRegionCountries(regionId: EntityId, payload: RegionCountriesPayload): Promise<RegionDto> {
|
||||||
@ -109,22 +117,22 @@ export function replaceRegionCountries(regionId: EntityId, payload: RegionCountr
|
|||||||
apiEndpointPath(API_OPERATIONS.replaceRegionCountries, { region_id: regionId }),
|
apiEndpointPath(API_OPERATIONS.replaceRegionCountries, { region_id: regionId }),
|
||||||
{
|
{
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enableRegion(regionId: EntityId): Promise<RegionDto> {
|
export function enableRegion(regionId: EntityId): Promise<RegionDto> {
|
||||||
const endpoint = API_ENDPOINTS.enableRegion;
|
const endpoint = API_ENDPOINTS.enableRegion;
|
||||||
return apiRequest<RegionDto>(apiEndpointPath(API_OPERATIONS.enableRegion, { region_id: regionId }), {
|
return apiRequest<RegionDto>(apiEndpointPath(API_OPERATIONS.enableRegion, { region_id: regionId }), {
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function disableRegion(regionId: EntityId): Promise<RegionDto> {
|
export function disableRegion(regionId: EntityId): Promise<RegionDto> {
|
||||||
const endpoint = API_ENDPOINTS.disableRegion;
|
const endpoint = API_ENDPOINTS.disableRegion;
|
||||||
return apiRequest<RegionDto>(apiEndpointPath(API_OPERATIONS.disableRegion, { region_id: regionId }), {
|
return apiRequest<RegionDto>(apiEndpointPath(API_OPERATIONS.disableRegion, { region_id: regionId }), {
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +140,7 @@ export function listBDLeaders(query: PageQuery = {}): Promise<ApiPage<BDProfileD
|
|||||||
const endpoint = API_ENDPOINTS.listBDLeaders;
|
const endpoint = API_ENDPOINTS.listBDLeaders;
|
||||||
return apiRequest<ApiPage<BDProfileDto>>(apiEndpointPath(API_OPERATIONS.listBDLeaders), {
|
return apiRequest<ApiPage<BDProfileDto>>(apiEndpointPath(API_OPERATIONS.listBDLeaders), {
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
query
|
query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +148,7 @@ export function listBDs(query: PageQuery = {}): Promise<ApiPage<BDProfileDto>> {
|
|||||||
const endpoint = API_ENDPOINTS.listBDs;
|
const endpoint = API_ENDPOINTS.listBDs;
|
||||||
return apiRequest<ApiPage<BDProfileDto>>(apiEndpointPath(API_OPERATIONS.listBDs), {
|
return apiRequest<ApiPage<BDProfileDto>>(apiEndpointPath(API_OPERATIONS.listBDs), {
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
query
|
query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +156,7 @@ export function listAgencies(query: PageQuery = {}): Promise<ApiPage<AgencyDto>>
|
|||||||
const endpoint = API_ENDPOINTS.listAgencies;
|
const endpoint = API_ENDPOINTS.listAgencies;
|
||||||
return apiRequest<ApiPage<AgencyDto>>(apiEndpointPath(API_OPERATIONS.listAgencies), {
|
return apiRequest<ApiPage<AgencyDto>>(apiEndpointPath(API_OPERATIONS.listAgencies), {
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
query
|
query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +164,7 @@ export function listHosts(query: PageQuery = {}): Promise<ApiPage<HostProfileDto
|
|||||||
const endpoint = API_ENDPOINTS.listHosts;
|
const endpoint = API_ENDPOINTS.listHosts;
|
||||||
return apiRequest<ApiPage<HostProfileDto>>(apiEndpointPath(API_OPERATIONS.listHosts), {
|
return apiRequest<ApiPage<HostProfileDto>>(apiEndpointPath(API_OPERATIONS.listHosts), {
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
query
|
query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +172,7 @@ export function listCoinSellers(query: PageQuery = {}): Promise<ApiPage<CoinSell
|
|||||||
const endpoint = API_ENDPOINTS.listCoinSellers;
|
const endpoint = API_ENDPOINTS.listCoinSellers;
|
||||||
return apiRequest<ApiPage<CoinSellerDto>>(apiEndpointPath(API_OPERATIONS.listCoinSellers), {
|
return apiRequest<ApiPage<CoinSellerDto>>(apiEndpointPath(API_OPERATIONS.listCoinSellers), {
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
query
|
query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,7 +180,7 @@ export function createBDLeader(payload: CreateBDLeaderPayload): Promise<BDProfil
|
|||||||
const endpoint = API_ENDPOINTS.createBDLeader;
|
const endpoint = API_ENDPOINTS.createBDLeader;
|
||||||
return apiRequest<BDProfileDto, CreateBDLeaderPayload>(apiEndpointPath(API_OPERATIONS.createBDLeader), {
|
return apiRequest<BDProfileDto, CreateBDLeaderPayload>(apiEndpointPath(API_OPERATIONS.createBDLeader), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,23 +188,32 @@ export function createBD(payload: CreateBDPayload): Promise<BDProfileDto> {
|
|||||||
const endpoint = API_ENDPOINTS.createBD;
|
const endpoint = API_ENDPOINTS.createBD;
|
||||||
return apiRequest<BDProfileDto, CreateBDPayload>(apiEndpointPath(API_OPERATIONS.createBD), {
|
return apiRequest<BDProfileDto, CreateBDPayload>(apiEndpointPath(API_OPERATIONS.createBD), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteBDLeader(userId: EntityId): Promise<BDProfileDto> {
|
||||||
|
return apiRequest<BDProfileDto>(`/v1/admin/bd-leaders/${encodeURIComponent(String(userId))}`, {
|
||||||
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setBDLeaderStatus(userId: EntityId, payload: BDStatusPayload): Promise<BDProfileDto> {
|
export function setBDLeaderStatus(userId: EntityId, payload: BDStatusPayload): Promise<BDProfileDto> {
|
||||||
const endpoint = API_ENDPOINTS.setBDLeaderStatus;
|
const endpoint = API_ENDPOINTS.setBDLeaderStatus;
|
||||||
return apiRequest<BDProfileDto, BDStatusPayload>(apiEndpointPath(API_OPERATIONS.setBDLeaderStatus, { user_id: userId }), {
|
return apiRequest<BDProfileDto, BDStatusPayload>(
|
||||||
|
apiEndpointPath(API_OPERATIONS.setBDLeaderStatus, { user_id: userId }),
|
||||||
|
{
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setBDStatus(userId: EntityId, payload: BDStatusPayload): Promise<BDProfileDto> {
|
export function setBDStatus(userId: EntityId, payload: BDStatusPayload): Promise<BDProfileDto> {
|
||||||
const endpoint = API_ENDPOINTS.setBDStatus;
|
const endpoint = API_ENDPOINTS.setBDStatus;
|
||||||
return apiRequest<BDProfileDto, BDStatusPayload>(apiEndpointPath(API_OPERATIONS.setBDStatus, { user_id: userId }), {
|
return apiRequest<BDProfileDto, BDStatusPayload>(apiEndpointPath(API_OPERATIONS.setBDStatus, { user_id: userId }), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,7 +221,7 @@ export function createCoinSeller(payload: CreateCoinSellerPayload): Promise<Coin
|
|||||||
const endpoint = API_ENDPOINTS.createCoinSeller;
|
const endpoint = API_ENDPOINTS.createCoinSeller;
|
||||||
return apiRequest<CoinSellerDto, CreateCoinSellerPayload>(apiEndpointPath(API_OPERATIONS.createCoinSeller), {
|
return apiRequest<CoinSellerDto, CreateCoinSellerPayload>(apiEndpointPath(API_OPERATIONS.createCoinSeller), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,8 +231,22 @@ export function setCoinSellerStatus(userId: EntityId, payload: CoinSellerStatusP
|
|||||||
apiEndpointPath(API_OPERATIONS.setCoinSellerStatus, { user_id: userId }),
|
apiEndpointPath(API_OPERATIONS.setCoinSellerStatus, { user_id: userId }),
|
||||||
{
|
{
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function creditCoinSellerStock(
|
||||||
|
userId: EntityId,
|
||||||
|
payload: CoinSellerStockCreditPayload,
|
||||||
|
): Promise<CoinSellerStockCreditDto> {
|
||||||
|
const endpoint = API_ENDPOINTS.creditCoinSellerStock;
|
||||||
|
return apiRequest<CoinSellerStockCreditDto, CoinSellerStockCreditPayload>(
|
||||||
|
apiEndpointPath(API_OPERATIONS.creditCoinSellerStock, { user_id: userId }),
|
||||||
|
{
|
||||||
|
body: payload,
|
||||||
|
method: endpoint.method,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,16 +254,19 @@ export function createAgency(payload: CreateAgencyPayload): Promise<CreateAgency
|
|||||||
const endpoint = API_ENDPOINTS.createAgency;
|
const endpoint = API_ENDPOINTS.createAgency;
|
||||||
return apiRequest<CreateAgencyResultDto, CreateAgencyPayload>(apiEndpointPath(API_OPERATIONS.createAgency), {
|
return apiRequest<CreateAgencyResultDto, CreateAgencyPayload>(apiEndpointPath(API_OPERATIONS.createAgency), {
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeAgency(agencyId: EntityId, payload: HostCommandPayload): Promise<AgencyDto> {
|
export function closeAgency(agencyId: EntityId, payload: HostCommandPayload): Promise<AgencyDto> {
|
||||||
const endpoint = API_ENDPOINTS.closeAgency;
|
const endpoint = API_ENDPOINTS.closeAgency;
|
||||||
return apiRequest<AgencyDto, HostCommandPayload>(apiEndpointPath(API_OPERATIONS.closeAgency, { agency_id: agencyId }), {
|
return apiRequest<AgencyDto, HostCommandPayload>(
|
||||||
|
apiEndpointPath(API_OPERATIONS.closeAgency, { agency_id: agencyId }),
|
||||||
|
{
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setAgencyJoinEnabled(agencyId: EntityId, payload: AgencyJoinEnabledPayload): Promise<AgencyDto> {
|
export function setAgencyJoinEnabled(agencyId: EntityId, payload: AgencyJoinEnabledPayload): Promise<AgencyDto> {
|
||||||
@ -241,7 +275,7 @@ export function setAgencyJoinEnabled(agencyId: EntityId, payload: AgencyJoinEnab
|
|||||||
apiEndpointPath(API_OPERATIONS.setAgencyJoinEnabled, { agency_id: agencyId }),
|
apiEndpointPath(API_OPERATIONS.setAgencyJoinEnabled, { agency_id: agencyId }),
|
||||||
{
|
{
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method
|
method: endpoint.method,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export function HostOrgActionModal({
|
|||||||
onClose,
|
onClose,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
open,
|
open,
|
||||||
|
size = "compact",
|
||||||
submitLabel = "提交",
|
submitLabel = "提交",
|
||||||
submitVariant = "primary",
|
submitVariant = "primary",
|
||||||
title
|
title
|
||||||
@ -15,7 +16,7 @@ export function HostOrgActionModal({
|
|||||||
<AdminFormDialog
|
<AdminFormDialog
|
||||||
loading={loading}
|
loading={loading}
|
||||||
open={open}
|
open={open}
|
||||||
size="compact"
|
size={size}
|
||||||
submitDisabled={disabled || loading}
|
submitDisabled={disabled || loading}
|
||||||
submitLabel={submitLabel}
|
submitLabel={submitLabel}
|
||||||
submitVariant={submitVariant}
|
submitVariant={submitVariant}
|
||||||
|
|||||||
@ -1,50 +1,20 @@
|
|||||||
import TextField from "@mui/material/TextField";
|
import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { RegionFilterControl } from "@/shared/ui/RegionFilterControl.jsx";
|
|
||||||
import { SearchBox } from "@/shared/ui/SearchBox.jsx";
|
|
||||||
import { StatusSelect } from "@/shared/ui/StatusSelect.jsx";
|
|
||||||
import styles from "@/features/host-org/host-org.module.css";
|
import styles from "@/features/host-org/host-org.module.css";
|
||||||
|
|
||||||
export function HostOrgFilters({
|
export function HostOrgFilters({
|
||||||
extraFilters = [],
|
extraFilters = [],
|
||||||
loadingRegions = false,
|
onReset,
|
||||||
onQueryChange,
|
|
||||||
onRegionIdChange,
|
|
||||||
onStatusChange,
|
|
||||||
query,
|
query,
|
||||||
regionId,
|
regionId,
|
||||||
regionOptions = [],
|
|
||||||
searchPlaceholder,
|
|
||||||
status,
|
status,
|
||||||
statusOptions
|
|
||||||
}) {
|
}) {
|
||||||
|
const hasActiveFilters = [query, regionId, status, ...extraFilters.map((filter) => filter.value)].some(
|
||||||
|
(value) => String(value || "").trim() !== "",
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.filters}>
|
<div className={styles.filters}>
|
||||||
<SearchBox className={styles.search} value={query} onChange={onQueryChange} placeholder={searchPlaceholder} />
|
{onReset ? <AdminFilterResetButton disabled={!hasActiveFilters} onClick={onReset} /> : null}
|
||||||
<StatusSelect
|
|
||||||
className={styles.statusSelect}
|
|
||||||
options={statusOptions}
|
|
||||||
value={status}
|
|
||||||
onChange={onStatusChange}
|
|
||||||
/>
|
|
||||||
<div className={styles.filterFields}>
|
|
||||||
<RegionFilterControl
|
|
||||||
emptyLabel="全部区域"
|
|
||||||
loading={loadingRegions}
|
|
||||||
options={regionOptions}
|
|
||||||
value={regionId}
|
|
||||||
onChange={onRegionIdChange}
|
|
||||||
/>
|
|
||||||
{extraFilters.map((filter) => (
|
|
||||||
<TextField
|
|
||||||
key={filter.name}
|
|
||||||
label={filter.label}
|
|
||||||
size="small"
|
|
||||||
type="number"
|
|
||||||
value={filter.value}
|
|
||||||
onChange={(event) => filter.onChange(event.target.value)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/features/host-org/components/HostOrgIdentity.jsx
Normal file
47
src/features/host-org/components/HostOrgIdentity.jsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import styles from "@/features/host-org/host-org.module.css";
|
||||||
|
|
||||||
|
export function HostOrgPerson({ avatar, displayUserId, fallbackId, username }) {
|
||||||
|
const name = username || "-";
|
||||||
|
const meta = displayUserId || fallbackId || "-";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.sellerIdentity}>
|
||||||
|
{avatar ? (
|
||||||
|
<img alt="" className={styles.sellerAvatar} src={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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HostOrgEntity({ id, name, prefix = "" }) {
|
||||||
|
if (!id && !name) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className={styles.sellerText}>
|
||||||
|
<span className={styles.sellerName}>{name || "-"}</span>
|
||||||
|
<span className={styles.sellerMeta}>
|
||||||
|
{prefix}
|
||||||
|
{id || "-"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hostOrgRegionName(item, regionOptions = []) {
|
||||||
|
if (item.regionName) {
|
||||||
|
return item.regionName;
|
||||||
|
}
|
||||||
|
const option = regionOptions.find((region) => String(region.value) === String(item.regionId));
|
||||||
|
return option?.name || item.regionId || "-";
|
||||||
|
}
|
||||||
@ -2,19 +2,10 @@ import { useMemo, useState } from "react";
|
|||||||
import { parseForm } from "@/shared/forms/validation";
|
import { parseForm } from "@/shared/forms/validation";
|
||||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
import {
|
import { closeAgency, createAgency, listAgencies, setAgencyJoinEnabled } from "@/features/host-org/api";
|
||||||
closeAgency,
|
|
||||||
createAgency,
|
|
||||||
listAgencies,
|
|
||||||
setAgencyJoinEnabled
|
|
||||||
} from "@/features/host-org/api";
|
|
||||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||||
import { useAgencyAbilities } from "@/features/host-org/permissions.js";
|
import { useAgencyAbilities } from "@/features/host-org/permissions.js";
|
||||||
import {
|
import { agencyCloseSchema, agencyJoinEnabledSchema, createAgencySchema } from "@/features/host-org/schema";
|
||||||
agencyCloseSchema,
|
|
||||||
agencyJoinEnabledSchema,
|
|
||||||
createAgencySchema
|
|
||||||
} from "@/features/host-org/schema";
|
|
||||||
|
|
||||||
const emptyAgencyForm = () => ({
|
const emptyAgencyForm = () => ({
|
||||||
commandId: makeCommandId("agency"),
|
commandId: makeCommandId("agency"),
|
||||||
@ -23,14 +14,14 @@ const emptyAgencyForm = () => ({
|
|||||||
name: "",
|
name: "",
|
||||||
ownerUserId: "",
|
ownerUserId: "",
|
||||||
parentBdUserId: "",
|
parentBdUserId: "",
|
||||||
reason: ""
|
reason: "",
|
||||||
});
|
});
|
||||||
const emptyCloseForm = () => ({ agencyId: "", commandId: makeCommandId("agency-close"), reason: "" });
|
const emptyCloseForm = () => ({ agencyId: "", commandId: makeCommandId("agency-close"), reason: "" });
|
||||||
const emptyJoinEnabledForm = () => ({
|
const emptyJoinEnabledForm = () => ({
|
||||||
agencyId: "",
|
agencyId: "",
|
||||||
commandId: makeCommandId("agency-join"),
|
commandId: makeCommandId("agency-join"),
|
||||||
joinEnabled: true,
|
joinEnabled: true,
|
||||||
reason: ""
|
reason: "",
|
||||||
});
|
});
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||||
@ -54,17 +45,22 @@ export function useHostAgenciesPage() {
|
|||||||
keyword: query,
|
keyword: query,
|
||||||
parent_bd_user_id: parentBdUserId,
|
parent_bd_user_id: parentBdUserId,
|
||||||
region_id: regionId,
|
region_id: regionId,
|
||||||
status
|
status,
|
||||||
}),
|
}),
|
||||||
[parentBdUserId, query, regionId, status]
|
[parentBdUserId, query, regionId, status],
|
||||||
);
|
);
|
||||||
const { data = emptyData, error, loading, reload } = usePaginatedQuery({
|
const {
|
||||||
|
data = emptyData,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
reload,
|
||||||
|
} = usePaginatedQuery({
|
||||||
errorMessage: "加载 Agency 列表失败",
|
errorMessage: "加载 Agency 列表失败",
|
||||||
fetcher: listAgencies,
|
fetcher: listAgencies,
|
||||||
filters,
|
filters,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
queryKey: ["host-org", "agencies", filters, page]
|
queryKey: ["host-org", "agencies", filters, page],
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeQuery = (value) => {
|
const changeQuery = (value) => {
|
||||||
@ -87,6 +83,14 @@ export function useHostAgenciesPage() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setStatus("");
|
||||||
|
setRegionId("");
|
||||||
|
setParentBdUserId("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
const submitAgency = async (event) => {
|
const submitAgency = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await runAction("agency", "Agency 已创建", async () => {
|
await runAction("agency", "Agency 已创建", async () => {
|
||||||
@ -158,6 +162,7 @@ export function useHostAgenciesPage() {
|
|||||||
regionId,
|
regionId,
|
||||||
regionOptions,
|
regionOptions,
|
||||||
reload,
|
reload,
|
||||||
|
resetFilters,
|
||||||
closeAction: () => setActiveAction(""),
|
closeAction: () => setActiveAction(""),
|
||||||
openAgencyForm: () => setActiveAction("agency"),
|
openAgencyForm: () => setActiveAction("agency"),
|
||||||
openCloseForm: () => setActiveAction("agency-close"),
|
openCloseForm: () => setActiveAction("agency-close"),
|
||||||
@ -169,7 +174,7 @@ export function useHostAgenciesPage() {
|
|||||||
status,
|
status,
|
||||||
submitAgency,
|
submitAgency,
|
||||||
submitClose,
|
submitClose,
|
||||||
submitJoinEnabled
|
submitJoinEnabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +1,38 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { parseForm } from "@/shared/forms/validation";
|
import { parseForm } from "@/shared/forms/validation";
|
||||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||||
|
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
import {
|
import {
|
||||||
createBD,
|
createBD,
|
||||||
createBDLeader,
|
createBDLeader,
|
||||||
|
deleteBDLeader,
|
||||||
listBDLeaders,
|
listBDLeaders,
|
||||||
listBDs,
|
listBDs,
|
||||||
setBDLeaderStatus,
|
setBDLeaderStatus,
|
||||||
setBDStatus
|
setBDStatus,
|
||||||
} from "@/features/host-org/api";
|
} from "@/features/host-org/api";
|
||||||
|
import { listAppUsers } from "@/features/app-users/api";
|
||||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||||
import { useBDAbilities } from "@/features/host-org/permissions.js";
|
import { useBDAbilities } from "@/features/host-org/permissions.js";
|
||||||
import { bdStatusSchema, createBDSchema, createBDLeaderSchema } from "@/features/host-org/schema";
|
import { bdStatusSchema, createBDSchema, createBDLeaderSchema } from "@/features/host-org/schema";
|
||||||
|
|
||||||
const emptyBDLeaderForm = () => ({ commandId: makeCommandId("bd-leader"), reason: "", regionId: "", targetUserId: "" });
|
const emptyBDLeaderForm = () => ({ commandId: makeCommandId("bd-leader"), reason: "", regionId: "", targetUserId: "" });
|
||||||
const emptyBDForm = () => ({ commandId: makeCommandId("bd"), parentLeaderUserId: "", reason: "", targetUserId: "" });
|
const emptyBDForm = () => ({ commandId: makeCommandId("bd"), parentLeaderUserId: "", reason: "", targetUserId: "" });
|
||||||
const emptyBDStatusForm = (targetType = "bd") => ({ commandId: makeCommandId("bd-status"), reason: "", status: "active", targetType, targetUserId: "" });
|
const emptyBDStatusForm = (targetType = "bd") => ({
|
||||||
|
commandId: makeCommandId("bd-status"),
|
||||||
|
reason: "",
|
||||||
|
status: "active",
|
||||||
|
targetType,
|
||||||
|
targetUserId: "",
|
||||||
|
});
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||||
|
const emptyLookup = () => ({ error: "", loading: false, user: null });
|
||||||
|
|
||||||
export function useHostBdsPage({ profileType = "bd" } = {}) {
|
export function useHostBdsPage({ profileType = "bd" } = {}) {
|
||||||
const abilities = useBDAbilities();
|
const abilities = useBDAbilities();
|
||||||
|
const confirm = useConfirm();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@ -32,6 +43,11 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
|||||||
const [bdLeaderForm, setBDLeaderForm] = useState(emptyBDLeaderForm);
|
const [bdLeaderForm, setBDLeaderForm] = useState(emptyBDLeaderForm);
|
||||||
const [bdForm, setBDForm] = useState(emptyBDForm);
|
const [bdForm, setBDForm] = useState(emptyBDForm);
|
||||||
const [bdStatusForm, setBDStatusForm] = useState(() => emptyBDStatusForm(profileType));
|
const [bdStatusForm, setBDStatusForm] = useState(() => emptyBDStatusForm(profileType));
|
||||||
|
const [bdUserLookup, setBDUserLookup] = useState(emptyLookup);
|
||||||
|
const [leaderBDs, setLeaderBDs] = useState(emptyData);
|
||||||
|
const [leaderBDsError, setLeaderBDsError] = useState("");
|
||||||
|
const [leaderBDsLoading, setLeaderBDsLoading] = useState(false);
|
||||||
|
const [selectedLeader, setSelectedLeader] = useState(null);
|
||||||
const [loadingAction, setLoadingAction] = useState("");
|
const [loadingAction, setLoadingAction] = useState("");
|
||||||
const [activeAction, setActiveAction] = useState("");
|
const [activeAction, setActiveAction] = useState("");
|
||||||
const filters = useMemo(
|
const filters = useMemo(
|
||||||
@ -39,20 +55,80 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
|||||||
keyword: query,
|
keyword: query,
|
||||||
parent_leader_user_id: profileType === "bd" ? parentLeaderUserId : "",
|
parent_leader_user_id: profileType === "bd" ? parentLeaderUserId : "",
|
||||||
region_id: regionId,
|
region_id: regionId,
|
||||||
status
|
status,
|
||||||
}),
|
}),
|
||||||
[parentLeaderUserId, profileType, query, regionId, status]
|
[parentLeaderUserId, profileType, query, regionId, status],
|
||||||
);
|
);
|
||||||
const fetcher = profileType === "bd" ? listBDs : listBDLeaders;
|
const fetcher = profileType === "bd" ? listBDs : listBDLeaders;
|
||||||
const { data = emptyData, error, loading, reload } = usePaginatedQuery({
|
const {
|
||||||
|
data = emptyData,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
reload,
|
||||||
|
} = usePaginatedQuery({
|
||||||
errorMessage: profileType === "leader" ? "加载 BD Leader 列表失败" : "加载 BD 列表失败",
|
errorMessage: profileType === "leader" ? "加载 BD Leader 列表失败" : "加载 BD 列表失败",
|
||||||
fetcher,
|
fetcher,
|
||||||
filters,
|
filters,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
queryKey: ["host-org", "bds", profileType, filters, page]
|
queryKey: ["host-org", "bds", profileType, filters, page],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const loadLeaderBDs = useCallback(async (leader) => {
|
||||||
|
if (!leader?.userId) {
|
||||||
|
setLeaderBDs(emptyData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLeaderBDsError("");
|
||||||
|
setLeaderBDsLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await listBDs({
|
||||||
|
page: 1,
|
||||||
|
page_size: 100,
|
||||||
|
parent_leader_user_id: leader.userId,
|
||||||
|
});
|
||||||
|
setLeaderBDs(data || emptyData);
|
||||||
|
} catch (err) {
|
||||||
|
setLeaderBDsError(err.message || "加载下级 BD 失败");
|
||||||
|
} finally {
|
||||||
|
setLeaderBDsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeAction !== "leader-add-bd") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyword = String(bdForm.targetUserId || "").trim();
|
||||||
|
if (!keyword) {
|
||||||
|
setBDUserLookup(emptyLookup());
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const timer = window.setTimeout(async () => {
|
||||||
|
setBDUserLookup({ error: "", loading: true, user: null });
|
||||||
|
try {
|
||||||
|
const data = await listAppUsers({ keyword, page: 1, page_size: 5 });
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBDUserLookup({ error: "", loading: false, user: pickUserMatch(data?.items || [], keyword) });
|
||||||
|
} catch (err) {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBDUserLookup({ error: err.message || "搜索用户失败", loading: false, user: null });
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [activeAction, bdForm.targetUserId]);
|
||||||
|
|
||||||
const changeQuery = (value) => {
|
const changeQuery = (value) => {
|
||||||
setQuery(value);
|
setQuery(value);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
@ -73,6 +149,14 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setStatus("");
|
||||||
|
setRegionId("");
|
||||||
|
setParentLeaderUserId("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
const submitBDLeader = async (event) => {
|
const submitBDLeader = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await runAction("bd-leader", "BD Leader 已创建", async () => {
|
await runAction("bd-leader", "BD Leader 已创建", async () => {
|
||||||
@ -97,12 +181,26 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitLeaderBD = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
await runAction("leader-add-bd", "下级 BD 已添加", async () => {
|
||||||
|
const payload = parseForm(createBDSchema, bdForm);
|
||||||
|
const data = await createBD(payload);
|
||||||
|
setBDForm(emptyBDForm());
|
||||||
|
setBDUserLookup(emptyLookup());
|
||||||
|
setActiveAction("");
|
||||||
|
await reload();
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const submitBDStatus = async (event) => {
|
const submitBDStatus = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await runAction("bd-status", profileType === "leader" ? "BD Leader 状态已更新" : "BD 状态已更新", async () => {
|
await runAction("bd-status", profileType === "leader" ? "BD Leader 状态已更新" : "BD 状态已更新", async () => {
|
||||||
const payload = parseForm(bdStatusSchema, { ...bdStatusForm, targetType: profileType });
|
const payload = parseForm(bdStatusSchema, { ...bdStatusForm, targetType: profileType });
|
||||||
const { targetType, targetUserId, ...body } = payload;
|
const { targetType, targetUserId, ...body } = payload;
|
||||||
const data = targetType === "leader"
|
const data =
|
||||||
|
targetType === "leader"
|
||||||
? await setBDLeaderStatus(targetUserId, body)
|
? await setBDLeaderStatus(targetUserId, body)
|
||||||
: await setBDStatus(targetUserId, body);
|
: await setBDStatus(targetUserId, body);
|
||||||
setBDStatusForm(emptyBDStatusForm(profileType));
|
setBDStatusForm(emptyBDStatusForm(profileType));
|
||||||
@ -112,6 +210,57 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleBDLeader = async (leader, enabled) => {
|
||||||
|
const status = enabled ? "active" : "disabled";
|
||||||
|
await runAction(
|
||||||
|
`bd-leader-status-${leader.userId}`,
|
||||||
|
enabled ? "BD Leader 已启用" : "BD Leader 已停用",
|
||||||
|
async () => {
|
||||||
|
const data = await setBDLeaderStatus(leader.userId, {
|
||||||
|
commandId: makeCommandId("bd-leader-status"),
|
||||||
|
reason: "",
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
await reload();
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBDLeader = async (leader) => {
|
||||||
|
const ok = await confirm({
|
||||||
|
confirmText: "删除",
|
||||||
|
message: `确认删除 BD Leader ${leader.displayUserId || leader.userId}?删除后该身份会从列表移除。`,
|
||||||
|
title: "删除 BD Leader",
|
||||||
|
tone: "danger",
|
||||||
|
});
|
||||||
|
if (!ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await runAction(`bd-leader-delete-${leader.userId}`, "BD Leader 已删除", async () => {
|
||||||
|
const data = await deleteBDLeader(leader.userId);
|
||||||
|
await reload();
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openLeaderBDs = (leader) => {
|
||||||
|
setSelectedLeader(leader);
|
||||||
|
setActiveAction("leader-bds");
|
||||||
|
void loadLeaderBDs(leader);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddLeaderBD = (leader) => {
|
||||||
|
setSelectedLeader(leader);
|
||||||
|
setBDUserLookup(emptyLookup());
|
||||||
|
setBDForm({
|
||||||
|
...emptyBDForm(),
|
||||||
|
parentLeaderUserId: leader.displayUserId || String(leader.userId || ""),
|
||||||
|
});
|
||||||
|
setActiveAction("leader-add-bd");
|
||||||
|
};
|
||||||
|
|
||||||
const runAction = async (action, successMessage, submitter) => {
|
const runAction = async (action, successMessage, submitter) => {
|
||||||
setLoadingAction(action);
|
setLoadingAction(action);
|
||||||
try {
|
try {
|
||||||
@ -124,12 +273,19 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeAction = () => {
|
||||||
|
setActiveAction("");
|
||||||
|
setBDUserLookup(emptyLookup());
|
||||||
|
setLeaderBDsError("");
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
abilities,
|
abilities,
|
||||||
activeAction,
|
activeAction,
|
||||||
bdForm,
|
bdForm,
|
||||||
bdLeaderForm,
|
bdLeaderForm,
|
||||||
bdStatusForm,
|
bdStatusForm,
|
||||||
|
bdUserLookup,
|
||||||
changeParentLeaderUserId,
|
changeParentLeaderUserId,
|
||||||
changeQuery,
|
changeQuery,
|
||||||
changeRegionId,
|
changeRegionId,
|
||||||
@ -139,6 +295,9 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
|||||||
loading,
|
loading,
|
||||||
loadingAction,
|
loadingAction,
|
||||||
loadingRegions,
|
loadingRegions,
|
||||||
|
leaderBDs,
|
||||||
|
leaderBDsError,
|
||||||
|
leaderBDsLoading,
|
||||||
page,
|
page,
|
||||||
parentLeaderUserId,
|
parentLeaderUserId,
|
||||||
profileType,
|
profileType,
|
||||||
@ -146,10 +305,16 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
|||||||
regionId,
|
regionId,
|
||||||
regionOptions,
|
regionOptions,
|
||||||
reload,
|
reload,
|
||||||
closeAction: () => setActiveAction(""),
|
removeBDLeader,
|
||||||
|
resetFilters,
|
||||||
|
closeAction,
|
||||||
|
loadLeaderBDs: () => loadLeaderBDs(selectedLeader),
|
||||||
|
openAddLeaderBD,
|
||||||
openBDForm: () => setActiveAction("bd"),
|
openBDForm: () => setActiveAction("bd"),
|
||||||
openBDLeaderForm: () => setActiveAction("bd-leader"),
|
openBDLeaderForm: () => setActiveAction("bd-leader"),
|
||||||
openBDStatusForm: () => setActiveAction("bd-status"),
|
openBDStatusForm: () => setActiveAction("bd-status"),
|
||||||
|
openLeaderBDs,
|
||||||
|
selectedLeader,
|
||||||
setBDForm,
|
setBDForm,
|
||||||
setBDLeaderForm,
|
setBDLeaderForm,
|
||||||
setBDStatusForm,
|
setBDStatusForm,
|
||||||
@ -157,10 +322,23 @@ export function useHostBdsPage({ profileType = "bd" } = {}) {
|
|||||||
status,
|
status,
|
||||||
submitBD,
|
submitBD,
|
||||||
submitBDLeader,
|
submitBDLeader,
|
||||||
submitBDStatus
|
submitLeaderBD,
|
||||||
|
submitBDStatus,
|
||||||
|
toggleBDLeader,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCommandId(prefix) {
|
function makeCommandId(prefix) {
|
||||||
return `${prefix}-${Date.now()}`;
|
return `${prefix}-${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pickUserMatch(items, keyword) {
|
||||||
|
const normalized = String(keyword || "").trim();
|
||||||
|
return (
|
||||||
|
items.find(
|
||||||
|
(item) => String(item.displayUserId || "") === normalized || String(item.userId || "") === normalized,
|
||||||
|
) ||
|
||||||
|
items[0] ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -2,20 +2,35 @@ import { useMemo, useState } from "react";
|
|||||||
import { parseForm } from "@/shared/forms/validation";
|
import { parseForm } from "@/shared/forms/validation";
|
||||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||||
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
|
||||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
import { createCoinSeller, listCoinSellers, setCoinSellerStatus } from "@/features/host-org/api";
|
import { createCoinSeller, creditCoinSellerStock, listCoinSellers, setCoinSellerStatus } from "@/features/host-org/api";
|
||||||
import { useCoinSellerAbilities } from "@/features/host-org/permissions.js";
|
import { useCoinSellerAbilities } from "@/features/host-org/permissions.js";
|
||||||
import { coinSellerStatusSchema, createCoinSellerSchema } from "@/features/host-org/schema";
|
import {
|
||||||
|
coinSellerStatusSchema,
|
||||||
|
coinSellerStockCreditSchema,
|
||||||
|
createCoinSellerSchema,
|
||||||
|
} from "@/features/host-org/schema";
|
||||||
|
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||||
const emptyCreateForm = () => ({ commandId: makeCommandId("coin-seller"), reason: "", targetUserId: "" });
|
const emptyCreateForm = () => ({ commandId: makeCommandId("coin-seller"), contact: "", reason: "", targetUserId: "" });
|
||||||
const emptyEditForm = () => ({ commandId: makeCommandId("coin-seller-status"), reason: "", status: "active", targetUserId: "" });
|
const emptyEditForm = () => ({
|
||||||
|
commandId: makeCommandId("coin-seller-status"),
|
||||||
|
contact: "",
|
||||||
|
reason: "",
|
||||||
|
status: "active",
|
||||||
|
targetUserId: "",
|
||||||
|
});
|
||||||
|
const emptyStockForm = () => ({
|
||||||
|
coinAmount: "",
|
||||||
|
commandId: makeCommandId("coin-seller-stock"),
|
||||||
|
reason: "",
|
||||||
|
rechargeAmount: "",
|
||||||
|
type: "usdt_purchase",
|
||||||
|
});
|
||||||
|
|
||||||
export function useHostCoinSellersPage() {
|
export function useHostCoinSellersPage() {
|
||||||
const abilities = useCoinSellerAbilities();
|
const abilities = useCoinSellerAbilities();
|
||||||
const confirm = useConfirm();
|
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@ -25,23 +40,29 @@ export function useHostCoinSellersPage() {
|
|||||||
const [activeAction, setActiveAction] = useState("");
|
const [activeAction, setActiveAction] = useState("");
|
||||||
const [createForm, setCreateForm] = useState(emptyCreateForm);
|
const [createForm, setCreateForm] = useState(emptyCreateForm);
|
||||||
const [editForm, setEditForm] = useState(emptyEditForm);
|
const [editForm, setEditForm] = useState(emptyEditForm);
|
||||||
|
const [stockForm, setStockForm] = useState(emptyStockForm);
|
||||||
const [selectedSeller, setSelectedSeller] = useState(null);
|
const [selectedSeller, setSelectedSeller] = useState(null);
|
||||||
const [loadingAction, setLoadingAction] = useState("");
|
const [loadingAction, setLoadingAction] = useState("");
|
||||||
const filters = useMemo(
|
const filters = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
keyword: query,
|
keyword: query,
|
||||||
region_id: regionId,
|
region_id: regionId,
|
||||||
status
|
status,
|
||||||
}),
|
}),
|
||||||
[query, regionId, status]
|
[query, regionId, status],
|
||||||
);
|
);
|
||||||
const { data = emptyData, error, loading, reload } = usePaginatedQuery({
|
const {
|
||||||
|
data = emptyData,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
reload,
|
||||||
|
} = usePaginatedQuery({
|
||||||
errorMessage: "加载币商列表失败",
|
errorMessage: "加载币商列表失败",
|
||||||
fetcher: listCoinSellers,
|
fetcher: listCoinSellers,
|
||||||
filters,
|
filters,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
queryKey: ["host-org", "coin-sellers", filters, page]
|
queryKey: ["host-org", "coin-sellers", filters, page],
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeQuery = (value) => {
|
const changeQuery = (value) => {
|
||||||
@ -59,6 +80,13 @@ export function useHostCoinSellersPage() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setStatus("");
|
||||||
|
setRegionId("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
const openCreateSeller = () => {
|
const openCreateSeller = () => {
|
||||||
setCreateForm(emptyCreateForm());
|
setCreateForm(emptyCreateForm());
|
||||||
setSelectedSeller(null);
|
setSelectedSeller(null);
|
||||||
@ -72,13 +100,23 @@ export function useHostCoinSellersPage() {
|
|||||||
setSelectedSeller(seller);
|
setSelectedSeller(seller);
|
||||||
setEditForm({
|
setEditForm({
|
||||||
commandId: makeCommandId("coin-seller-status"),
|
commandId: makeCommandId("coin-seller-status"),
|
||||||
reason: "",
|
contact: seller.contact || "",
|
||||||
|
reason: "update coin seller contact",
|
||||||
status: seller.status || "active",
|
status: seller.status || "active",
|
||||||
targetUserId: seller.userId
|
targetUserId: seller.userId,
|
||||||
});
|
});
|
||||||
setActiveAction("edit");
|
setActiveAction("edit");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openStockCredit = (seller) => {
|
||||||
|
if (!seller?.userId || seller.status !== "active" || !abilities.canStockCredit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedSeller(seller);
|
||||||
|
setStockForm(emptyStockForm());
|
||||||
|
setActiveAction("stock");
|
||||||
|
};
|
||||||
|
|
||||||
const closeAction = () => {
|
const closeAction = () => {
|
||||||
setActiveAction("");
|
setActiveAction("");
|
||||||
setSelectedSeller(null);
|
setSelectedSeller(null);
|
||||||
@ -97,13 +135,31 @@ export function useHostCoinSellersPage() {
|
|||||||
|
|
||||||
const submitEditSeller = async (event) => {
|
const submitEditSeller = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await runAction(`coin-seller-edit-${editForm.targetUserId || selectedSeller?.userId || ""}`, "币商已更新", async () => {
|
await runAction(
|
||||||
|
`coin-seller-edit-${editForm.targetUserId || selectedSeller?.userId || ""}`,
|
||||||
|
"币商已更新",
|
||||||
|
async () => {
|
||||||
const payload = parseForm(coinSellerStatusSchema, editForm);
|
const payload = parseForm(coinSellerStatusSchema, editForm);
|
||||||
const { targetUserId, ...body } = payload;
|
const { targetUserId, ...body } = payload;
|
||||||
await setCoinSellerStatus(targetUserId, body);
|
await setCoinSellerStatus(targetUserId, body);
|
||||||
setEditForm(emptyEditForm());
|
setEditForm(emptyEditForm());
|
||||||
closeAction();
|
closeAction();
|
||||||
await reload();
|
await reload();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitStockCredit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!selectedSeller?.userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await runAction(`coin-seller-stock-${selectedSeller.userId}`, "币商库存已入账", async () => {
|
||||||
|
const payload = parseForm(coinSellerStockCreditSchema, stockForm);
|
||||||
|
await creditCoinSellerStock(selectedSeller.userId, payload);
|
||||||
|
setStockForm(emptyStockForm());
|
||||||
|
closeAction();
|
||||||
|
await reload();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,30 +175,7 @@ export function useHostCoinSellersPage() {
|
|||||||
await setCoinSellerStatus(seller.userId, {
|
await setCoinSellerStatus(seller.userId, {
|
||||||
commandId: makeCommandId("coin-seller-status"),
|
commandId: makeCommandId("coin-seller-status"),
|
||||||
reason: "",
|
reason: "",
|
||||||
status: nextStatus
|
status: nextStatus,
|
||||||
});
|
|
||||||
await reload();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteSeller = async (seller) => {
|
|
||||||
if (!abilities.canUpdate || !seller?.userId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ok = await confirm({
|
|
||||||
confirmText: "删除",
|
|
||||||
message: seller.username || seller.displayUserId || String(seller.userId),
|
|
||||||
title: "删除币商",
|
|
||||||
tone: "danger"
|
|
||||||
});
|
|
||||||
if (!ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await runAction(`coin-seller-delete-${seller.userId}`, "币商已删除", async () => {
|
|
||||||
await setCoinSellerStatus(seller.userId, {
|
|
||||||
commandId: makeCommandId("coin-seller-delete"),
|
|
||||||
reason: "delete coin seller",
|
|
||||||
status: "disabled"
|
|
||||||
});
|
});
|
||||||
await reload();
|
await reload();
|
||||||
});
|
});
|
||||||
@ -169,7 +202,6 @@ export function useHostCoinSellersPage() {
|
|||||||
closeAction,
|
closeAction,
|
||||||
createForm,
|
createForm,
|
||||||
data,
|
data,
|
||||||
deleteSeller,
|
|
||||||
editForm,
|
editForm,
|
||||||
error,
|
error,
|
||||||
loading,
|
loading,
|
||||||
@ -177,19 +209,24 @@ export function useHostCoinSellersPage() {
|
|||||||
loadingRegions,
|
loadingRegions,
|
||||||
openCreateSeller,
|
openCreateSeller,
|
||||||
openEditSeller,
|
openEditSeller,
|
||||||
|
openStockCredit,
|
||||||
page,
|
page,
|
||||||
query,
|
query,
|
||||||
regionId,
|
regionId,
|
||||||
regionOptions,
|
regionOptions,
|
||||||
reload,
|
reload,
|
||||||
|
resetFilters,
|
||||||
selectedSeller,
|
selectedSeller,
|
||||||
setCreateForm,
|
setCreateForm,
|
||||||
setEditForm,
|
setEditForm,
|
||||||
setPage,
|
setPage,
|
||||||
|
setStockForm,
|
||||||
status,
|
status,
|
||||||
|
stockForm,
|
||||||
submitCreateSeller,
|
submitCreateSeller,
|
||||||
submitEditSeller,
|
submitEditSeller,
|
||||||
toggleSeller
|
submitStockCredit,
|
||||||
|
toggleSeller,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,13 +2,7 @@ import { useCallback, useMemo, useState } from "react";
|
|||||||
import { parseForm } from "@/shared/forms/validation";
|
import { parseForm } from "@/shared/forms/validation";
|
||||||
import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
|
import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js";
|
||||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
import {
|
import { createCountry, disableCountry, enableCountry, listCountries, updateCountry } from "@/features/host-org/api";
|
||||||
createCountry,
|
|
||||||
disableCountry,
|
|
||||||
enableCountry,
|
|
||||||
listCountries,
|
|
||||||
updateCountry
|
|
||||||
} from "@/features/host-org/api";
|
|
||||||
import { useCountryAbilities } from "@/features/host-org/permissions.js";
|
import { useCountryAbilities } from "@/features/host-org/permissions.js";
|
||||||
import { countryCreateSchema, countryUpdateSchema } from "@/features/host-org/schema";
|
import { countryCreateSchema, countryUpdateSchema } from "@/features/host-org/schema";
|
||||||
|
|
||||||
@ -21,7 +15,7 @@ const emptyCountryForm = () => ({
|
|||||||
isoAlpha3: "",
|
isoAlpha3: "",
|
||||||
isoNumeric: "",
|
isoNumeric: "",
|
||||||
phoneCountryCode: "",
|
phoneCountryCode: "",
|
||||||
sortOrder: 0
|
sortOrder: 0,
|
||||||
});
|
});
|
||||||
const emptyData = { items: [], total: 0 };
|
const emptyData = { items: [], total: 0 };
|
||||||
|
|
||||||
@ -36,16 +30,21 @@ export function useHostCountriesPage() {
|
|||||||
const [activeAction, setActiveAction] = useState("");
|
const [activeAction, setActiveAction] = useState("");
|
||||||
const filters = useMemo(
|
const filters = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
enabled: enabledStatus === "enabled" ? true : enabledStatus === "disabled" ? false : undefined
|
enabled: enabledStatus === "enabled" ? true : enabledStatus === "disabled" ? false : undefined,
|
||||||
}),
|
}),
|
||||||
[enabledStatus]
|
[enabledStatus],
|
||||||
);
|
);
|
||||||
const queryFn = useCallback(() => listCountries(filters), [filters]);
|
const queryFn = useCallback(() => listCountries(filters), [filters]);
|
||||||
const { data = emptyData, error, loading, reload } = useAdminQuery(queryFn, {
|
const {
|
||||||
|
data = emptyData,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
reload,
|
||||||
|
} = useAdminQuery(queryFn, {
|
||||||
errorMessage: "加载国家列表失败",
|
errorMessage: "加载国家列表失败",
|
||||||
initialData: emptyData,
|
initialData: emptyData,
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
queryKey: ["host-org", "countries", filters]
|
queryKey: ["host-org", "countries", filters],
|
||||||
});
|
});
|
||||||
const items = useMemo(() => filterCountries(data?.items || [], query), [data?.items, query]);
|
const items = useMemo(() => filterCountries(data?.items || [], query), [data?.items, query]);
|
||||||
|
|
||||||
@ -57,6 +56,11 @@ export function useHostCountriesPage() {
|
|||||||
setEnabledStatus(value);
|
setEnabledStatus(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setEnabledStatus("");
|
||||||
|
};
|
||||||
|
|
||||||
const openCreateCountry = () => {
|
const openCreateCountry = () => {
|
||||||
setEditingCountry(null);
|
setEditingCountry(null);
|
||||||
setCountryForm(emptyCountryForm());
|
setCountryForm(emptyCountryForm());
|
||||||
@ -74,7 +78,7 @@ export function useHostCountriesPage() {
|
|||||||
isoAlpha3: country.isoAlpha3 || "",
|
isoAlpha3: country.isoAlpha3 || "",
|
||||||
isoNumeric: country.isoNumeric || "",
|
isoNumeric: country.isoNumeric || "",
|
||||||
phoneCountryCode: country.phoneCountryCode || "",
|
phoneCountryCode: country.phoneCountryCode || "",
|
||||||
sortOrder: country.sortOrder || 0
|
sortOrder: country.sortOrder || 0,
|
||||||
});
|
});
|
||||||
setActiveAction("edit");
|
setActiveAction("edit");
|
||||||
};
|
};
|
||||||
@ -84,9 +88,7 @@ export function useHostCountriesPage() {
|
|||||||
const isEdit = activeAction === "edit";
|
const isEdit = activeAction === "edit";
|
||||||
await runAction(isEdit ? "country-edit" : "country-create", isEdit ? "国家已更新" : "国家已创建", async () => {
|
await runAction(isEdit ? "country-edit" : "country-create", isEdit ? "国家已更新" : "国家已创建", async () => {
|
||||||
const payload = parseForm(isEdit ? countryUpdateSchema : countryCreateSchema, countryForm);
|
const payload = parseForm(isEdit ? countryUpdateSchema : countryCreateSchema, countryForm);
|
||||||
const data = isEdit
|
const data = isEdit ? await updateCountry(editingCountry.countryId, payload) : await createCountry(payload);
|
||||||
? await updateCountry(editingCountry.countryId, payload)
|
|
||||||
: await createCountry(payload);
|
|
||||||
setCountryForm(emptyCountryForm());
|
setCountryForm(emptyCountryForm());
|
||||||
setEditingCountry(null);
|
setEditingCountry(null);
|
||||||
setActiveAction("");
|
setActiveAction("");
|
||||||
@ -135,9 +137,10 @@ export function useHostCountriesPage() {
|
|||||||
openEditCountry,
|
openEditCountry,
|
||||||
query,
|
query,
|
||||||
reload,
|
reload,
|
||||||
|
resetFilters,
|
||||||
setCountryForm,
|
setCountryForm,
|
||||||
submitCountry,
|
submitCountry,
|
||||||
toggleCountry
|
toggleCountry,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +156,7 @@ function filterCountries(items, query) {
|
|||||||
item.countryName,
|
item.countryName,
|
||||||
item.isoAlpha3,
|
item.isoAlpha3,
|
||||||
item.isoNumeric,
|
item.isoNumeric,
|
||||||
item.phoneCountryCode
|
item.phoneCountryCode,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.some((value) => String(value).toLowerCase().includes(keyword));
|
.some((value) => String(value).toLowerCase().includes(keyword));
|
||||||
|
|||||||
@ -20,17 +20,22 @@ export function useHostHostsPage() {
|
|||||||
agency_id: agencyId,
|
agency_id: agencyId,
|
||||||
keyword: query,
|
keyword: query,
|
||||||
region_id: regionId,
|
region_id: regionId,
|
||||||
status
|
status,
|
||||||
}),
|
}),
|
||||||
[agencyId, query, regionId, status]
|
[agencyId, query, regionId, status],
|
||||||
);
|
);
|
||||||
const { data = emptyData, error, loading, reload } = usePaginatedQuery({
|
const {
|
||||||
|
data = emptyData,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
reload,
|
||||||
|
} = usePaginatedQuery({
|
||||||
errorMessage: "加载主播列表失败",
|
errorMessage: "加载主播列表失败",
|
||||||
fetcher: listHosts,
|
fetcher: listHosts,
|
||||||
filters,
|
filters,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
queryKey: ["host-org", "hosts", filters, page]
|
queryKey: ["host-org", "hosts", filters, page],
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeQuery = (value) => {
|
const changeQuery = (value) => {
|
||||||
@ -53,6 +58,14 @@ export function useHostHostsPage() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setStatus("");
|
||||||
|
setRegionId("");
|
||||||
|
setAgencyId("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
abilities,
|
abilities,
|
||||||
agencyId,
|
agencyId,
|
||||||
@ -69,7 +82,8 @@ export function useHostHostsPage() {
|
|||||||
regionId,
|
regionId,
|
||||||
regionOptions,
|
regionOptions,
|
||||||
reload,
|
reload,
|
||||||
|
resetFilters,
|
||||||
setPage,
|
setPage,
|
||||||
status
|
status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
listCountries,
|
listCountries,
|
||||||
listRegions,
|
listRegions,
|
||||||
replaceRegionCountries,
|
replaceRegionCountries,
|
||||||
updateRegion
|
updateRegion,
|
||||||
} from "@/features/host-org/api";
|
} from "@/features/host-org/api";
|
||||||
import { useRegionAbilities } from "@/features/host-org/permissions.js";
|
import { useRegionAbilities } from "@/features/host-org/permissions.js";
|
||||||
import { regionCreateSchema, regionUpdateSchema } from "@/features/host-org/schema";
|
import { regionCreateSchema, regionUpdateSchema } from "@/features/host-org/schema";
|
||||||
@ -18,7 +18,7 @@ const emptyRegionForm = () => ({
|
|||||||
countries: [],
|
countries: [],
|
||||||
name: "",
|
name: "",
|
||||||
regionCode: "",
|
regionCode: "",
|
||||||
sortOrder: 0
|
sortOrder: 0,
|
||||||
});
|
});
|
||||||
const emptyData = { items: [], total: 0 };
|
const emptyData = { items: [], total: 0 };
|
||||||
const globalRegionCode = "GLOBAL";
|
const globalRegionCode = "GLOBAL";
|
||||||
@ -33,25 +33,34 @@ export function useHostRegionsPage() {
|
|||||||
const [loadingAction, setLoadingAction] = useState("");
|
const [loadingAction, setLoadingAction] = useState("");
|
||||||
const [activeAction, setActiveAction] = useState("");
|
const [activeAction, setActiveAction] = useState("");
|
||||||
const queryFn = useCallback(() => listRegions(), []);
|
const queryFn = useCallback(() => listRegions(), []);
|
||||||
const { data = emptyData, error, loading, reload } = useAdminQuery(queryFn, {
|
const {
|
||||||
|
data = emptyData,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
reload,
|
||||||
|
} = useAdminQuery(queryFn, {
|
||||||
errorMessage: "加载区域列表失败",
|
errorMessage: "加载区域列表失败",
|
||||||
initialData: emptyData,
|
initialData: emptyData,
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
queryKey: ["host-org", "regions"]
|
queryKey: ["host-org", "regions"],
|
||||||
});
|
});
|
||||||
const countryQueryFn = useCallback(() => listCountries(), []);
|
const countryQueryFn = useCallback(() => listCountries(), []);
|
||||||
const { data: countriesData = emptyData, loading: loadingCountries } = useAdminQuery(countryQueryFn, {
|
const { data: countriesData = emptyData, loading: loadingCountries } = useAdminQuery(countryQueryFn, {
|
||||||
errorMessage: "加载国家列表失败",
|
errorMessage: "加载国家列表失败",
|
||||||
initialData: emptyData,
|
initialData: emptyData,
|
||||||
queryKey: ["host-org", "countries", "region-options"],
|
queryKey: ["host-org", "countries", "region-options"],
|
||||||
staleTime: 5 * 60 * 1000
|
staleTime: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
const items = useMemo(() => filterRegions(data?.items || [], query, status), [data?.items, query, status]);
|
const items = useMemo(() => filterRegions(data?.items || [], query, status), [data?.items, query, status]);
|
||||||
const assignedCountries = useMemo(() => {
|
const assignedCountries = useMemo(() => {
|
||||||
const currentRegionId = editingRegion?.regionId;
|
const currentRegionId = editingRegion?.regionId;
|
||||||
const assigned = new Set();
|
const assigned = new Set();
|
||||||
(data?.items || []).forEach((region) => {
|
(data?.items || []).forEach((region) => {
|
||||||
if (region.status !== "active" || region.regionId === currentRegionId || isGlobalRegionCode(region.regionCode)) {
|
if (
|
||||||
|
region.status !== "active" ||
|
||||||
|
region.regionId === currentRegionId ||
|
||||||
|
isGlobalRegionCode(region.regionCode)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(region.countries || []).forEach((country) => assigned.add(country));
|
(region.countries || []).forEach((country) => assigned.add(country));
|
||||||
@ -66,7 +75,9 @@ export function useHostRegionsPage() {
|
|||||||
const countryLabels = useMemo(() => {
|
const countryLabels = useMemo(() => {
|
||||||
return (countriesData?.items || []).reduce((labels, country) => {
|
return (countriesData?.items || []).reduce((labels, country) => {
|
||||||
if (country.countryCode) {
|
if (country.countryCode) {
|
||||||
labels[country.countryCode] = [country.countryCode, country.countryDisplayName || country.countryName].filter(Boolean).join(" · ");
|
labels[country.countryCode] = [country.countryCode, country.countryDisplayName || country.countryName]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" · ");
|
||||||
}
|
}
|
||||||
return labels;
|
return labels;
|
||||||
}, {});
|
}, {});
|
||||||
@ -86,6 +97,11 @@ export function useHostRegionsPage() {
|
|||||||
setStatus(value);
|
setStatus(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setStatus("");
|
||||||
|
};
|
||||||
|
|
||||||
const openCreateRegion = () => {
|
const openCreateRegion = () => {
|
||||||
setEditingRegion(null);
|
setEditingRegion(null);
|
||||||
setRegionFormState(emptyRegionForm());
|
setRegionFormState(emptyRegionForm());
|
||||||
@ -98,7 +114,7 @@ export function useHostRegionsPage() {
|
|||||||
countries: region.countries || [],
|
countries: region.countries || [],
|
||||||
name: region.name || "",
|
name: region.name || "",
|
||||||
regionCode: region.regionCode || "",
|
regionCode: region.regionCode || "",
|
||||||
sortOrder: region.sortOrder || 0
|
sortOrder: region.sortOrder || 0,
|
||||||
});
|
});
|
||||||
setActiveAction("edit");
|
setActiveAction("edit");
|
||||||
};
|
};
|
||||||
@ -118,7 +134,11 @@ export function useHostRegionsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleRegion = async (region, nextActive = region.status !== "active") => {
|
const toggleRegion = async (region, nextActive = region.status !== "active") => {
|
||||||
if (isGlobalRegionCode(region.regionCode) || !abilities.canStatus || (region.status === "active") === nextActive) {
|
if (
|
||||||
|
isGlobalRegionCode(region.regionCode) ||
|
||||||
|
!abilities.canStatus ||
|
||||||
|
(region.status === "active") === nextActive
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await runAction(`region-status-${region.regionId}`, nextActive ? "区域已启用" : "区域已停用", async () => {
|
await runAction(`region-status-${region.regionId}`, nextActive ? "区域已启用" : "区域已停用", async () => {
|
||||||
@ -160,10 +180,11 @@ export function useHostRegionsPage() {
|
|||||||
query,
|
query,
|
||||||
regionForm,
|
regionForm,
|
||||||
reload,
|
reload,
|
||||||
|
resetFilters,
|
||||||
setRegionForm,
|
setRegionForm,
|
||||||
status,
|
status,
|
||||||
submitRegion,
|
submitRegion,
|
||||||
toggleRegion
|
toggleRegion,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,5 +219,9 @@ function filterRegions(items, query, status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isGlobalRegionCode(regionCode) {
|
function isGlobalRegionCode(regionCode) {
|
||||||
return String(regionCode || "").trim().toUpperCase() === globalRegionCode;
|
return (
|
||||||
|
String(regionCode || "")
|
||||||
|
.trim()
|
||||||
|
.toUpperCase() === globalRegionCode
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -165,6 +165,62 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contactButton {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: left;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactButton:hover {
|
||||||
|
color: var(--primary-strong);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactPlaceholder {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.childBdList {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.childBdItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
|
background: var(--bg-input);
|
||||||
|
}
|
||||||
|
|
||||||
|
.childBdStatus {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lookupPanel {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
min-height: 44px;
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
background: var(--bg-input);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--admin-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
.positive {
|
.positive {
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,30 +7,64 @@ import TextField from "@mui/material/TextField";
|
|||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
|
||||||
import { agencyStatusFilters } from "@/features/host-org/constants.js";
|
import { agencyStatusFilters } from "@/features/host-org/constants.js";
|
||||||
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
||||||
|
import { HostOrgEntity, HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx";
|
||||||
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
|
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
|
||||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||||
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
||||||
import { useHostAgenciesPage } from "@/features/host-org/hooks/useHostAgenciesPage.js";
|
import { useHostAgenciesPage } from "@/features/host-org/hooks/useHostAgenciesPage.js";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import styles from "@/features/host-org/host-org.module.css";
|
import styles from "@/features/host-org/host-org.module.css";
|
||||||
|
|
||||||
const agencyColumns = [
|
const agencyColumns = [
|
||||||
{ key: "agencyId", label: "Agency ID", width: "minmax(110px, 0.8fr)" },
|
{
|
||||||
{ key: "name", label: "名称", width: "minmax(160px, 1.2fr)" },
|
key: "agency",
|
||||||
{ key: "ownerUserId", label: "Owner 用户 ID" },
|
label: "Agency",
|
||||||
{ key: "parentBdUserId", label: "上级 BD 用户 ID" },
|
render: (item) => <HostOrgEntity id={item.agencyId} name={item.name} prefix="Agency " />,
|
||||||
{ key: "regionId", label: "区域 ID", width: "minmax(90px, 0.7fr)" },
|
width: "minmax(190px, 1fr)",
|
||||||
{ key: "status", label: "状态", render: (item) => <HostOrgStatus value={item.status} />, width: "minmax(90px, 0.7fr)" },
|
},
|
||||||
|
{
|
||||||
|
key: "ownerUserId",
|
||||||
|
label: "Owner",
|
||||||
|
render: (item) => <AgencyOwner item={item} />,
|
||||||
|
width: "minmax(220px, 1.2fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "parentBdUserId",
|
||||||
|
label: "上级 BD",
|
||||||
|
render: (item) => <ParentBD item={item} />,
|
||||||
|
width: "minmax(220px, 1.2fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "regionId",
|
||||||
|
label: "区域",
|
||||||
|
render: (item, _index, context) => hostOrgRegionName(item, context?.regionOptions),
|
||||||
|
width: "minmax(130px, 0.75fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
label: "状态",
|
||||||
|
render: (item) => <HostOrgStatus value={item.status} />,
|
||||||
|
width: "minmax(90px, 0.7fr)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "joinEnabled",
|
key: "joinEnabled",
|
||||||
label: "入会",
|
label: "入会",
|
||||||
render: (item) => <span className={item.joinEnabled ? styles.positive : styles.negative}>{item.joinEnabled ? "允许" : "禁止"}</span>,
|
render: (item) => (
|
||||||
width: "minmax(80px, 0.6fr)"
|
<span className={item.joinEnabled ? styles.positive : styles.negative}>
|
||||||
|
{item.joinEnabled ? "允许" : "禁止"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
width: "minmax(80px, 0.6fr)",
|
||||||
|
},
|
||||||
|
{ key: "maxHosts", label: "最大主播数", width: "minmax(100px, 0.7fr)" },
|
||||||
|
{
|
||||||
|
key: "updatedAtMs",
|
||||||
|
label: "更新时间",
|
||||||
|
render: (item) => formatMillis(item.updatedAtMs),
|
||||||
|
width: "minmax(170px, 1fr)",
|
||||||
},
|
},
|
||||||
{ key: "maxHosts", label: "最大主播数", width: "minmax(90px, 0.7fr)" },
|
|
||||||
{ key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function HostAgenciesPage() {
|
export function HostAgenciesPage() {
|
||||||
@ -44,12 +78,52 @@ export function HostAgenciesPage() {
|
|||||||
? { icon: <ToggleOnOutlined fontSize="small" />, label: "入会开关", onClick: page.openJoinEnabledForm }
|
? { icon: <ToggleOnOutlined fontSize="small" />, label: "入会开关", onClick: page.openJoinEnabledForm }
|
||||||
: null,
|
: null,
|
||||||
page.abilities.canStatus
|
page.abilities.canStatus
|
||||||
? { icon: <BlockOutlined fontSize="small" />, label: "关闭 Agency", onClick: page.openCloseForm, tone: "danger" }
|
? {
|
||||||
|
icon: <BlockOutlined fontSize="small" />,
|
||||||
|
label: "关闭 Agency",
|
||||||
|
onClick: page.openCloseForm,
|
||||||
|
tone: "danger",
|
||||||
|
}
|
||||||
: null,
|
: null,
|
||||||
page.abilities.canCreate
|
page.abilities.canCreate
|
||||||
? { icon: <Add fontSize="small" />, label: "创建 Agency", onClick: page.openAgencyForm, variant: "primary" }
|
? { icon: <Add fontSize="small" />, label: "创建 Agency", onClick: page.openAgencyForm, variant: "primary" }
|
||||||
: null
|
: null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
const columns = agencyColumns.map((column) => {
|
||||||
|
if (column.key === "agency") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索 Agency、Owner、用户",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "parentBdUserId") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索上级 BD 用户 ID",
|
||||||
|
value: page.parentBdUserId,
|
||||||
|
onChange: page.changeParentBdUserId,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "status") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: agencyStatusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.changeStatus,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.root}>
|
<section className={styles.root}>
|
||||||
<div className={styles.contentPanel}>
|
<div className={styles.contentPanel}>
|
||||||
@ -61,8 +135,8 @@ export function HostAgenciesPage() {
|
|||||||
label: "上级 BD 用户 ID",
|
label: "上级 BD 用户 ID",
|
||||||
name: "parentBdUserId",
|
name: "parentBdUserId",
|
||||||
onChange: page.changeParentBdUserId,
|
onChange: page.changeParentBdUserId,
|
||||||
value: page.parentBdUserId
|
value: page.parentBdUserId,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
loadingRegions: page.loadingRegions,
|
loadingRegions: page.loadingRegions,
|
||||||
query: page.query,
|
query: page.query,
|
||||||
@ -73,24 +147,28 @@ export function HostAgenciesPage() {
|
|||||||
statusOptions: agencyStatusFilters,
|
statusOptions: agencyStatusFilters,
|
||||||
onQueryChange: page.changeQuery,
|
onQueryChange: page.changeQuery,
|
||||||
onRegionIdChange: page.changeRegionId,
|
onRegionIdChange: page.changeRegionId,
|
||||||
onStatusChange: page.changeStatus
|
onReset: page.resetFilters,
|
||||||
|
onStatusChange: page.changeStatus,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<div className={styles.listBlock}>
|
<div className={styles.listBlock}>
|
||||||
<HostOrgTable
|
<HostOrgTable
|
||||||
columns={agencyColumns}
|
columns={columns}
|
||||||
|
context={{ regionOptions: page.regionOptions }}
|
||||||
items={items}
|
items={items}
|
||||||
regionFilter={createRegionColumnFilter({
|
minWidth="1250px"
|
||||||
loading: page.loadingRegions,
|
|
||||||
options: page.regionOptions,
|
|
||||||
value: page.regionId,
|
|
||||||
onChange: page.changeRegionId
|
|
||||||
})}
|
|
||||||
rowKey={(item) => item.agencyId}
|
rowKey={(item) => item.agencyId}
|
||||||
/>
|
/>
|
||||||
{total > 0 ? <PaginationBar page={page.page} pageSize={page.data.pageSize || 10} total={total} onPageChange={page.setPage} /> : null}
|
{total > 0 ? (
|
||||||
|
<PaginationBar
|
||||||
|
page={page.page}
|
||||||
|
pageSize={page.data.pageSize || 10}
|
||||||
|
total={total}
|
||||||
|
onPageChange={page.setPage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</DataState>
|
</DataState>
|
||||||
</div>
|
</div>
|
||||||
@ -103,15 +181,56 @@ export function HostAgenciesPage() {
|
|||||||
onSubmit={page.submitAgency}
|
onSubmit={page.submitAgency}
|
||||||
title="创建 Agency"
|
title="创建 Agency"
|
||||||
>
|
>
|
||||||
<TextField disabled={createDisabled} label="Agency 名称" required value={page.agencyForm.name} onChange={(event) => page.setAgencyForm({ ...page.agencyForm, name: event.target.value })} />
|
<TextField
|
||||||
<TextField disabled={createDisabled} label="Owner 短 ID" required type="number" value={page.agencyForm.ownerUserId} onChange={(event) => page.setAgencyForm({ ...page.agencyForm, ownerUserId: event.target.value })} />
|
disabled={createDisabled}
|
||||||
<TextField disabled={createDisabled} label="上级 BD 短 ID" required type="number" value={page.agencyForm.parentBdUserId} onChange={(event) => page.setAgencyForm({ ...page.agencyForm, parentBdUserId: event.target.value })} />
|
label="Agency 名称"
|
||||||
<TextField disabled={createDisabled} label="最大主播数" type="number" value={page.agencyForm.maxHosts} onChange={(event) => page.setAgencyForm({ ...page.agencyForm, maxHosts: event.target.value })} />
|
required
|
||||||
|
value={page.agencyForm.name}
|
||||||
|
onChange={(event) => page.setAgencyForm({ ...page.agencyForm, name: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={createDisabled}
|
||||||
|
label="Owner 短 ID"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={page.agencyForm.ownerUserId}
|
||||||
|
onChange={(event) => page.setAgencyForm({ ...page.agencyForm, ownerUserId: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={createDisabled}
|
||||||
|
label="上级 BD 短 ID"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={page.agencyForm.parentBdUserId}
|
||||||
|
onChange={(event) => page.setAgencyForm({ ...page.agencyForm, parentBdUserId: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={createDisabled}
|
||||||
|
label="最大主播数"
|
||||||
|
type="number"
|
||||||
|
value={page.agencyForm.maxHosts}
|
||||||
|
onChange={(event) => page.setAgencyForm({ ...page.agencyForm, maxHosts: event.target.value })}
|
||||||
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={<Switch checked={page.agencyForm.joinEnabled} disabled={createDisabled} onChange={(event) => page.setAgencyForm({ ...page.agencyForm, joinEnabled: event.target.checked })} />}
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={page.agencyForm.joinEnabled}
|
||||||
|
disabled={createDisabled}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setAgencyForm({ ...page.agencyForm, joinEnabled: event.target.checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={page.agencyForm.joinEnabled ? "允许入会" : "禁止入会"}
|
label={page.agencyForm.joinEnabled ? "允许入会" : "禁止入会"}
|
||||||
/>
|
/>
|
||||||
<TextField disabled={createDisabled} label="原因" multiline minRows={2} value={page.agencyForm.reason} onChange={(event) => page.setAgencyForm({ ...page.agencyForm, reason: event.target.value })} />
|
<TextField
|
||||||
|
disabled={createDisabled}
|
||||||
|
label="原因"
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
value={page.agencyForm.reason}
|
||||||
|
onChange={(event) => page.setAgencyForm({ ...page.agencyForm, reason: event.target.value })}
|
||||||
|
/>
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
|
|
||||||
<HostOrgActionModal
|
<HostOrgActionModal
|
||||||
@ -122,12 +241,38 @@ export function HostAgenciesPage() {
|
|||||||
onSubmit={page.submitJoinEnabled}
|
onSubmit={page.submitJoinEnabled}
|
||||||
title="入会开关"
|
title="入会开关"
|
||||||
>
|
>
|
||||||
<TextField disabled={statusDisabled} label="Agency ID" required type="number" value={page.joinEnabledForm.agencyId} onChange={(event) => page.setJoinEnabledForm({ ...page.joinEnabledForm, agencyId: event.target.value })} />
|
<TextField
|
||||||
|
disabled={statusDisabled}
|
||||||
|
label="Agency ID"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={page.joinEnabledForm.agencyId}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setJoinEnabledForm({ ...page.joinEnabledForm, agencyId: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={<Switch checked={page.joinEnabledForm.joinEnabled} disabled={statusDisabled} onChange={(event) => page.setJoinEnabledForm({ ...page.joinEnabledForm, joinEnabled: event.target.checked })} />}
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={page.joinEnabledForm.joinEnabled}
|
||||||
|
disabled={statusDisabled}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setJoinEnabledForm({ ...page.joinEnabledForm, joinEnabled: event.target.checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={page.joinEnabledForm.joinEnabled ? "允许入会" : "禁止入会"}
|
label={page.joinEnabledForm.joinEnabled ? "允许入会" : "禁止入会"}
|
||||||
/>
|
/>
|
||||||
<TextField disabled={statusDisabled} label="原因" multiline minRows={2} value={page.joinEnabledForm.reason} onChange={(event) => page.setJoinEnabledForm({ ...page.joinEnabledForm, reason: event.target.value })} />
|
<TextField
|
||||||
|
disabled={statusDisabled}
|
||||||
|
label="原因"
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
value={page.joinEnabledForm.reason}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setJoinEnabledForm({ ...page.joinEnabledForm, reason: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
|
|
||||||
<HostOrgActionModal
|
<HostOrgActionModal
|
||||||
@ -140,10 +285,46 @@ export function HostAgenciesPage() {
|
|||||||
submitVariant="danger"
|
submitVariant="danger"
|
||||||
title="关闭 Agency"
|
title="关闭 Agency"
|
||||||
>
|
>
|
||||||
<TextField disabled={statusDisabled} label="Agency ID" required type="number" value={page.closeForm.agencyId} onChange={(event) => page.setCloseForm({ ...page.closeForm, agencyId: event.target.value })} />
|
<TextField
|
||||||
<TextField disabled={statusDisabled} label="原因" multiline minRows={2} required value={page.closeForm.reason} onChange={(event) => page.setCloseForm({ ...page.closeForm, reason: event.target.value })} />
|
disabled={statusDisabled}
|
||||||
|
label="Agency ID"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={page.closeForm.agencyId}
|
||||||
|
onChange={(event) => page.setCloseForm({ ...page.closeForm, agencyId: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={statusDisabled}
|
||||||
|
label="原因"
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
required
|
||||||
|
value={page.closeForm.reason}
|
||||||
|
onChange={(event) => page.setCloseForm({ ...page.closeForm, reason: event.target.value })}
|
||||||
|
/>
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AgencyOwner({ item }) {
|
||||||
|
return (
|
||||||
|
<HostOrgPerson
|
||||||
|
avatar={item.ownerAvatar}
|
||||||
|
displayUserId={item.ownerDisplayUserId}
|
||||||
|
fallbackId={item.ownerUserId}
|
||||||
|
username={item.ownerUsername || "Owner"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ParentBD({ item }) {
|
||||||
|
return (
|
||||||
|
<HostOrgPerson
|
||||||
|
avatar={item.parentBdAvatar}
|
||||||
|
displayUserId={item.parentBdDisplayUserId}
|
||||||
|
fallbackId={item.parentBdUserId}
|
||||||
|
username={item.parentBdUsername || "BD"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import Add from "@mui/icons-material/Add";
|
import Add from "@mui/icons-material/Add";
|
||||||
import SyncAltOutlined from "@mui/icons-material/SyncAltOutlined";
|
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import PersonAddAlt1Outlined from "@mui/icons-material/PersonAddAlt1Outlined";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
|
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
|
||||||
import { bdStatusFilters } from "@/features/host-org/constants.js";
|
import { bdStatusFilters } from "@/features/host-org/constants.js";
|
||||||
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
||||||
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
|
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
|
||||||
@ -15,30 +17,72 @@ import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
|
|||||||
import { useHostBdsPage } from "@/features/host-org/hooks/useHostBdsPage.js";
|
import { useHostBdsPage } from "@/features/host-org/hooks/useHostBdsPage.js";
|
||||||
import styles from "@/features/host-org/host-org.module.css";
|
import styles from "@/features/host-org/host-org.module.css";
|
||||||
|
|
||||||
const bdLeaderColumns = [
|
const bdLeaderBaseColumns = [
|
||||||
{ key: "userId", label: "用户 ID", width: "minmax(110px, 0.8fr)" },
|
{
|
||||||
{ key: "role", label: "角色", width: "minmax(110px, 0.8fr)" },
|
key: "user",
|
||||||
{ key: "regionId", label: "区域 ID", width: "minmax(90px, 0.7fr)" },
|
label: "用户",
|
||||||
{ key: "status", label: "状态", render: (item) => <HostOrgStatus value={item.status} />, width: "minmax(90px, 0.7fr)" },
|
render: (item) => <PersonIdentity item={item} />,
|
||||||
{ key: "createdByUserId", label: "创建人用户 ID" },
|
width: "minmax(220px, 1.25fr)",
|
||||||
{ key: "createdAtMs", label: "创建时间", render: (item) => formatMillis(item.createdAtMs), width: "minmax(170px, 1fr)" },
|
},
|
||||||
{ key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" }
|
{
|
||||||
|
key: "regionId",
|
||||||
|
label: "区域",
|
||||||
|
render: (item, _index, context) => regionName(item, context?.regionOptions),
|
||||||
|
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() {
|
export function HostBdLeadersPage() {
|
||||||
const page = useHostBdsPage({ profileType: "leader" });
|
const page = useHostBdsPage({ profileType: "leader" });
|
||||||
const createDisabled = !page.abilities.canCreate;
|
const createDisabled = !page.abilities.canCreate;
|
||||||
const updateDisabled = !page.abilities.canUpdate;
|
|
||||||
const items = page.data.items || [];
|
const items = page.data.items || [];
|
||||||
const total = page.data.total || 0;
|
const total = page.data.total || 0;
|
||||||
const toolbarActions = [
|
const bdLeaderColumns = [
|
||||||
page.abilities.canUpdate
|
...bdLeaderBaseColumns,
|
||||||
? { icon: <SyncAltOutlined fontSize="small" />, label: "BD Leader 状态", onClick: page.openBDStatusForm }
|
page.abilities.canCreate || page.abilities.canUpdate
|
||||||
|
? {
|
||||||
|
key: "actions",
|
||||||
|
label: "操作",
|
||||||
|
render: (item) => <BDLeaderActions item={item} page={page} />,
|
||||||
|
width: "minmax(118px, 0.7fr)",
|
||||||
|
}
|
||||||
: null,
|
: null,
|
||||||
page.abilities.canCreate
|
|
||||||
? { icon: <Add fontSize="small" />, label: "创建 BD Leader", onClick: page.openBDLeaderForm, variant: "primary" }
|
|
||||||
: null
|
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
const toolbarActions = page.abilities.canCreate
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
icon: <Add fontSize="small" />,
|
||||||
|
label: "创建 BD Leader",
|
||||||
|
onClick: page.openBDLeaderForm,
|
||||||
|
variant: "primary",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.root}>
|
<section className={styles.root}>
|
||||||
<div className={styles.contentPanel}>
|
<div className={styles.contentPanel}>
|
||||||
@ -54,7 +98,8 @@ export function HostBdLeadersPage() {
|
|||||||
statusOptions: bdStatusFilters,
|
statusOptions: bdStatusFilters,
|
||||||
onQueryChange: page.changeQuery,
|
onQueryChange: page.changeQuery,
|
||||||
onRegionIdChange: page.changeRegionId,
|
onRegionIdChange: page.changeRegionId,
|
||||||
onStatusChange: page.changeStatus
|
onReset: page.resetFilters,
|
||||||
|
onStatusChange: page.changeStatus,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -62,16 +107,19 @@ export function HostBdLeadersPage() {
|
|||||||
<div className={styles.listBlock}>
|
<div className={styles.listBlock}>
|
||||||
<HostOrgTable
|
<HostOrgTable
|
||||||
columns={bdLeaderColumns}
|
columns={bdLeaderColumns}
|
||||||
|
context={{ page, regionOptions: page.regionOptions }}
|
||||||
items={items}
|
items={items}
|
||||||
regionFilter={createRegionColumnFilter({
|
minWidth="1120px"
|
||||||
loading: page.loadingRegions,
|
|
||||||
options: page.regionOptions,
|
|
||||||
value: page.regionId,
|
|
||||||
onChange: page.changeRegionId
|
|
||||||
})}
|
|
||||||
rowKey={(item) => `bd-leader-${item.userId}`}
|
rowKey={(item) => `bd-leader-${item.userId}`}
|
||||||
/>
|
/>
|
||||||
{total > 0 ? <PaginationBar page={page.page} pageSize={page.data.pageSize || 10} total={total} onPageChange={page.setPage} /> : null}
|
{total > 0 ? (
|
||||||
|
<PaginationBar
|
||||||
|
page={page.page}
|
||||||
|
pageSize={page.data.pageSize || 10}
|
||||||
|
total={total}
|
||||||
|
onPageChange={page.setPage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</DataState>
|
</DataState>
|
||||||
</div>
|
</div>
|
||||||
@ -84,27 +132,208 @@ export function HostBdLeadersPage() {
|
|||||||
onSubmit={page.submitBDLeader}
|
onSubmit={page.submitBDLeader}
|
||||||
title="创建 BD Leader"
|
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
|
||||||
<RegionSelect disabled={createDisabled} emptyLabel="选择区域" loading={page.loadingRegions} options={page.regionOptions} required value={page.bdLeaderForm.regionId} onChange={(value) => page.setBDLeaderForm({ ...page.bdLeaderForm, regionId: value })} />
|
disabled={createDisabled}
|
||||||
<TextField disabled={createDisabled} label="联系方式" value={page.bdLeaderForm.reason} onChange={(event) => page.setBDLeaderForm({ ...page.bdLeaderForm, reason: event.target.value })} />
|
label="用户短 ID"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={page.bdLeaderForm.targetUserId}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setBDLeaderForm({ ...page.bdLeaderForm, targetUserId: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<RegionSelect
|
||||||
|
disabled={createDisabled}
|
||||||
|
emptyLabel="选择区域"
|
||||||
|
loading={page.loadingRegions}
|
||||||
|
options={page.regionOptions}
|
||||||
|
required
|
||||||
|
value={page.bdLeaderForm.regionId}
|
||||||
|
onChange={(value) => page.setBDLeaderForm({ ...page.bdLeaderForm, regionId: value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={createDisabled}
|
||||||
|
label="联系方式"
|
||||||
|
value={page.bdLeaderForm.reason}
|
||||||
|
onChange={(event) => page.setBDLeaderForm({ ...page.bdLeaderForm, reason: event.target.value })}
|
||||||
|
/>
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
|
|
||||||
<HostOrgActionModal
|
<HostOrgActionModal
|
||||||
disabled={updateDisabled}
|
disabled={createDisabled}
|
||||||
loading={page.loadingAction === "bd-status"}
|
loading={page.loadingAction === "leader-add-bd"}
|
||||||
open={page.activeAction === "bd-status"}
|
open={page.activeAction === "leader-add-bd"}
|
||||||
onClose={page.closeAction}
|
onClose={page.closeAction}
|
||||||
onSubmit={page.submitBDStatus}
|
onSubmit={page.submitLeaderBD}
|
||||||
title="BD Leader 状态"
|
title="增加下级 BD"
|
||||||
>
|
>
|
||||||
<TextField disabled={updateDisabled} label="用户 ID" required type="number" value={page.bdStatusForm.targetUserId} onChange={(event) => page.setBDStatusForm({ ...page.bdStatusForm, targetUserId: event.target.value })} />
|
<TextField
|
||||||
<TextField disabled={updateDisabled} label="状态" required select value={page.bdStatusForm.status} onChange={(event) => page.setBDStatusForm({ ...page.bdStatusForm, status: event.target.value })}>
|
disabled
|
||||||
<MenuItem value="active">启用</MenuItem>
|
label="上级 Leader"
|
||||||
<MenuItem value="disabled">停用</MenuItem>
|
value={page.selectedLeader?.displayUserId || page.selectedLeader?.userId || ""}
|
||||||
</TextField>
|
/>
|
||||||
<TextField disabled={updateDisabled} label="原因" multiline minRows={2} value={page.bdStatusForm.reason} onChange={(event) => page.setBDStatusForm({ ...page.bdStatusForm, reason: event.target.value })} />
|
<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>
|
</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>
|
</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 (
|
||||||
|
<Switch
|
||||||
|
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 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)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -5,24 +5,60 @@ import TextField from "@mui/material/TextField";
|
|||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
|
||||||
import { bdStatusFilters } from "@/features/host-org/constants.js";
|
import { bdStatusFilters } from "@/features/host-org/constants.js";
|
||||||
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
||||||
|
import { HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx";
|
||||||
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
|
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
|
||||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||||
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
||||||
import { useHostBdsPage } from "@/features/host-org/hooks/useHostBdsPage.js";
|
import { useHostBdsPage } from "@/features/host-org/hooks/useHostBdsPage.js";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import styles from "@/features/host-org/host-org.module.css";
|
import styles from "@/features/host-org/host-org.module.css";
|
||||||
|
|
||||||
const bdColumns = [
|
const bdColumns = [
|
||||||
{ key: "userId", label: "用户 ID", width: "minmax(110px, 0.8fr)" },
|
{
|
||||||
{ key: "role", label: "角色", width: "minmax(110px, 0.8fr)" },
|
key: "user",
|
||||||
{ key: "parentLeaderUserId", label: "上级 Leader 用户 ID", render: (item) => item.parentLeaderUserId || "-" },
|
label: "用户",
|
||||||
{ key: "regionId", label: "区域 ID", width: "minmax(90px, 0.7fr)" },
|
render: (item) => <BDUser item={item} />,
|
||||||
{ key: "status", label: "状态", render: (item) => <HostOrgStatus value={item.status} />, width: "minmax(90px, 0.7fr)" },
|
width: "minmax(220px, 1.25fr)",
|
||||||
{ key: "createdByUserId", label: "创建人用户 ID" },
|
},
|
||||||
{ key: "createdAtMs", label: "创建时间", render: (item) => formatMillis(item.createdAtMs), width: "minmax(170px, 1fr)" },
|
{ key: "role", label: "角色", width: "minmax(100px, 0.65fr)" },
|
||||||
{ key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" }
|
{
|
||||||
|
key: "parentLeaderUserId",
|
||||||
|
label: "上级 Leader",
|
||||||
|
render: (item) => <ParentLeader item={item} />,
|
||||||
|
width: "minmax(220px, 1.25fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "regionId",
|
||||||
|
label: "区域",
|
||||||
|
render: (item, _index, context) => hostOrgRegionName(item, context?.regionOptions),
|
||||||
|
width: "minmax(130px, 0.75fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
label: "状态",
|
||||||
|
render: (item) => <HostOrgStatus value={item.status} />,
|
||||||
|
width: "minmax(90px, 0.7fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "createdByUserId",
|
||||||
|
label: "创建人",
|
||||||
|
render: (item) => <Creator item={item} />,
|
||||||
|
width: "minmax(180px, 1fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "createdAtMs",
|
||||||
|
label: "创建时间",
|
||||||
|
render: (item) => formatMillis(item.createdAtMs),
|
||||||
|
width: "minmax(170px, 1fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "updatedAtMs",
|
||||||
|
label: "更新时间",
|
||||||
|
render: (item) => formatMillis(item.updatedAtMs),
|
||||||
|
width: "minmax(170px, 1fr)",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function HostBdsPage() {
|
export function HostBdsPage() {
|
||||||
@ -37,8 +73,43 @@ export function HostBdsPage() {
|
|||||||
: null,
|
: null,
|
||||||
page.abilities.canCreate
|
page.abilities.canCreate
|
||||||
? { icon: <Add fontSize="small" />, label: "创建 BD", onClick: page.openBDForm, variant: "primary" }
|
? { icon: <Add fontSize="small" />, label: "创建 BD", onClick: page.openBDForm, variant: "primary" }
|
||||||
: null
|
: null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
const columns = bdColumns.map((column) => {
|
||||||
|
if (column.key === "user") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索用户 ID、展示 ID、用户名",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "parentLeaderUserId") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索 Leader 用户 ID",
|
||||||
|
value: page.parentLeaderUserId,
|
||||||
|
onChange: page.changeParentLeaderUserId,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "status") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: bdStatusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.changeStatus,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.root}>
|
<section className={styles.root}>
|
||||||
<div className={styles.contentPanel}>
|
<div className={styles.contentPanel}>
|
||||||
@ -50,8 +121,8 @@ export function HostBdsPage() {
|
|||||||
label: "上级 Leader 用户 ID",
|
label: "上级 Leader 用户 ID",
|
||||||
name: "parentLeaderUserId",
|
name: "parentLeaderUserId",
|
||||||
onChange: page.changeParentLeaderUserId,
|
onChange: page.changeParentLeaderUserId,
|
||||||
value: page.parentLeaderUserId
|
value: page.parentLeaderUserId,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
loadingRegions: page.loadingRegions,
|
loadingRegions: page.loadingRegions,
|
||||||
query: page.query,
|
query: page.query,
|
||||||
@ -62,24 +133,28 @@ export function HostBdsPage() {
|
|||||||
statusOptions: bdStatusFilters,
|
statusOptions: bdStatusFilters,
|
||||||
onQueryChange: page.changeQuery,
|
onQueryChange: page.changeQuery,
|
||||||
onRegionIdChange: page.changeRegionId,
|
onRegionIdChange: page.changeRegionId,
|
||||||
onStatusChange: page.changeStatus
|
onReset: page.resetFilters,
|
||||||
|
onStatusChange: page.changeStatus,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<div className={styles.listBlock}>
|
<div className={styles.listBlock}>
|
||||||
<HostOrgTable
|
<HostOrgTable
|
||||||
columns={bdColumns}
|
columns={columns}
|
||||||
|
context={{ regionOptions: page.regionOptions }}
|
||||||
items={items}
|
items={items}
|
||||||
regionFilter={createRegionColumnFilter({
|
minWidth="1260px"
|
||||||
loading: page.loadingRegions,
|
|
||||||
options: page.regionOptions,
|
|
||||||
value: page.regionId,
|
|
||||||
onChange: page.changeRegionId
|
|
||||||
})}
|
|
||||||
rowKey={(item) => `bd-${item.userId}`}
|
rowKey={(item) => `bd-${item.userId}`}
|
||||||
/>
|
/>
|
||||||
{total > 0 ? <PaginationBar page={page.page} pageSize={page.data.pageSize || 10} total={total} onPageChange={page.setPage} /> : null}
|
{total > 0 ? (
|
||||||
|
<PaginationBar
|
||||||
|
page={page.page}
|
||||||
|
pageSize={page.data.pageSize || 10}
|
||||||
|
total={total}
|
||||||
|
onPageChange={page.setPage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</DataState>
|
</DataState>
|
||||||
</div>
|
</div>
|
||||||
@ -92,9 +167,30 @@ export function HostBdsPage() {
|
|||||||
onSubmit={page.submitBD}
|
onSubmit={page.submitBD}
|
||||||
title="创建 BD"
|
title="创建 BD"
|
||||||
>
|
>
|
||||||
<TextField disabled={createDisabled} label="用户短 ID" required type="number" value={page.bdForm.targetUserId} onChange={(event) => page.setBDForm({ ...page.bdForm, targetUserId: event.target.value })} />
|
<TextField
|
||||||
<TextField disabled={createDisabled} label="上级 Leader 短 ID" required type="number" value={page.bdForm.parentLeaderUserId} onChange={(event) => page.setBDForm({ ...page.bdForm, parentLeaderUserId: event.target.value })} />
|
disabled={createDisabled}
|
||||||
<TextField disabled={createDisabled} label="原因" multiline minRows={2} value={page.bdForm.reason} onChange={(event) => page.setBDForm({ ...page.bdForm, reason: event.target.value })} />
|
label="用户短 ID"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={page.bdForm.targetUserId}
|
||||||
|
onChange={(event) => page.setBDForm({ ...page.bdForm, targetUserId: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={createDisabled}
|
||||||
|
label="上级 Leader 短 ID"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={page.bdForm.parentLeaderUserId}
|
||||||
|
onChange={(event) => page.setBDForm({ ...page.bdForm, parentLeaderUserId: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={createDisabled}
|
||||||
|
label="原因"
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
value={page.bdForm.reason}
|
||||||
|
onChange={(event) => page.setBDForm({ ...page.bdForm, reason: event.target.value })}
|
||||||
|
/>
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
|
|
||||||
<HostOrgActionModal
|
<HostOrgActionModal
|
||||||
@ -105,14 +201,75 @@ export function HostBdsPage() {
|
|||||||
onSubmit={page.submitBDStatus}
|
onSubmit={page.submitBDStatus}
|
||||||
title="BD 状态"
|
title="BD 状态"
|
||||||
>
|
>
|
||||||
<TextField disabled={updateDisabled} label="用户 ID" required type="number" value={page.bdStatusForm.targetUserId} onChange={(event) => page.setBDStatusForm({ ...page.bdStatusForm, targetUserId: event.target.value })} />
|
<TextField
|
||||||
<TextField disabled={updateDisabled} label="状态" required select value={page.bdStatusForm.status} onChange={(event) => page.setBDStatusForm({ ...page.bdStatusForm, status: event.target.value })}>
|
disabled={updateDisabled}
|
||||||
|
label="用户 ID"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={page.bdStatusForm.targetUserId}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setBDStatusForm({ ...page.bdStatusForm, targetUserId: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={updateDisabled}
|
||||||
|
label="状态"
|
||||||
|
required
|
||||||
|
select
|
||||||
|
value={page.bdStatusForm.status}
|
||||||
|
onChange={(event) => page.setBDStatusForm({ ...page.bdStatusForm, status: event.target.value })}
|
||||||
|
>
|
||||||
<MenuItem value="active">启用</MenuItem>
|
<MenuItem value="active">启用</MenuItem>
|
||||||
<MenuItem value="disabled">停用</MenuItem>
|
<MenuItem value="disabled">停用</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField disabled={updateDisabled} label="原因" multiline minRows={2} value={page.bdStatusForm.reason} onChange={(event) => page.setBDStatusForm({ ...page.bdStatusForm, reason: event.target.value })} />
|
<TextField
|
||||||
|
disabled={updateDisabled}
|
||||||
|
label="原因"
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
value={page.bdStatusForm.reason}
|
||||||
|
onChange={(event) => page.setBDStatusForm({ ...page.bdStatusForm, reason: event.target.value })}
|
||||||
|
/>
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BDUser({ item }) {
|
||||||
|
return (
|
||||||
|
<HostOrgPerson
|
||||||
|
avatar={item.avatar}
|
||||||
|
displayUserId={item.displayUserId}
|
||||||
|
fallbackId={item.userId}
|
||||||
|
username={item.username}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ParentLeader({ item }) {
|
||||||
|
if (!item.parentLeaderUserId) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<HostOrgPerson
|
||||||
|
avatar={item.parentLeaderAvatar}
|
||||||
|
displayUserId={item.parentLeaderDisplayUserId}
|
||||||
|
fallbackId={item.parentLeaderUserId}
|
||||||
|
username={item.parentLeaderUsername || "Leader"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Creator({ item }) {
|
||||||
|
if (!item.createdByUserId && !item.createdByDisplayUserId && !item.createdByUsername) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<HostOrgPerson
|
||||||
|
avatar={item.createdByAvatar}
|
||||||
|
displayUserId={item.createdByDisplayUserId}
|
||||||
|
fallbackId={item.createdByUserId}
|
||||||
|
username={item.createdByUsername || "创建人"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,19 +1,25 @@
|
|||||||
import Add from "@mui/icons-material/Add";
|
import Add from "@mui/icons-material/Add";
|
||||||
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined";
|
||||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Switch from "@mui/material/Switch";
|
import Switch from "@mui/material/Switch";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
|
import {
|
||||||
|
AdminFormAmountField,
|
||||||
|
AdminFormFieldGrid,
|
||||||
|
AdminFormReadOnlyField,
|
||||||
|
AdminFormSection,
|
||||||
|
} from "@/shared/ui/AdminFormDialog.jsx";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
|
||||||
import { coinSellerStatusFilters } from "@/features/host-org/constants.js";
|
import { coinSellerStatusFilters } from "@/features/host-org/constants.js";
|
||||||
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
||||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||||
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
||||||
import { useHostCoinSellersPage } from "@/features/host-org/hooks/useHostCoinSellersPage.js";
|
import { useHostCoinSellersPage } from "@/features/host-org/hooks/useHostCoinSellersPage.js";
|
||||||
|
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import styles from "@/features/host-org/host-org.module.css";
|
import styles from "@/features/host-org/host-org.module.css";
|
||||||
|
|
||||||
const baseColumns = [
|
const baseColumns = [
|
||||||
@ -21,13 +27,44 @@ const baseColumns = [
|
|||||||
key: "seller",
|
key: "seller",
|
||||||
label: "币商",
|
label: "币商",
|
||||||
render: (item) => <SellerIdentity item={item} />,
|
render: (item) => <SellerIdentity item={item} />,
|
||||||
width: "minmax(220px, 1.3fr)"
|
width: "minmax(220px, 1.3fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "regionId",
|
||||||
|
label: "区域",
|
||||||
|
render: (item, _index, context) => regionName(item, context?.regionOptions),
|
||||||
|
width: "minmax(140px, 0.8fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "contact",
|
||||||
|
label: "联系方式",
|
||||||
|
render: (item, _index, context) => <SellerContact item={item} page={context?.page} />,
|
||||||
|
width: "minmax(170px, 1fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "merchantAssetType",
|
||||||
|
label: "资产类型",
|
||||||
|
render: (item) => item.merchantAssetType || "-",
|
||||||
|
width: "minmax(150px, 0.9fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "merchantBalance",
|
||||||
|
label: "币商余额",
|
||||||
|
render: (item) => formatNumber(item.merchantBalance),
|
||||||
|
width: "minmax(120px, 0.8fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "createdByUserId",
|
||||||
|
label: "创建人 ID",
|
||||||
|
render: (item) => item.createdByUserId || "-",
|
||||||
|
width: "minmax(110px, 0.7fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "updatedAtMs",
|
||||||
|
label: "更新时间",
|
||||||
|
render: (item) => formatMillis(item.updatedAtMs),
|
||||||
|
width: "minmax(170px, 1fr)",
|
||||||
},
|
},
|
||||||
{ key: "regionId", label: "区域", render: (item, _index, context) => regionName(item, context?.regionOptions), width: "minmax(140px, 0.8fr)" },
|
|
||||||
{ key: "merchantAssetType", label: "资产类型", render: (item) => item.merchantAssetType || "-", width: "minmax(150px, 0.9fr)" },
|
|
||||||
{ key: "merchantBalance", label: "币商余额", render: (item) => formatNumber(item.merchantBalance), width: "minmax(120px, 0.8fr)" },
|
|
||||||
{ key: "createdByUserId", label: "创建人 ID", render: (item) => item.createdByUserId || "-", width: "minmax(110px, 0.7fr)" },
|
|
||||||
{ key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function HostCoinSellersPage() {
|
export function HostCoinSellersPage() {
|
||||||
@ -42,18 +79,49 @@ export function HostCoinSellersPage() {
|
|||||||
key: "status",
|
key: "status",
|
||||||
label: "状态",
|
label: "状态",
|
||||||
render: (item) => <SellerStatusSwitch item={item} page={page} />,
|
render: (item) => <SellerStatusSwitch item={item} page={page} />,
|
||||||
width: "minmax(90px, 0.6fr)"
|
width: "minmax(90px, 0.6fr)",
|
||||||
},
|
},
|
||||||
...baseColumns.slice(2),
|
...baseColumns.slice(2),
|
||||||
page.abilities.canUpdate
|
page.abilities.canStockCredit
|
||||||
? {
|
? {
|
||||||
key: "actions",
|
key: "actions",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
render: (item) => <SellerActions item={item} page={page} />,
|
render: (item) => <SellerActions item={item} page={page} />,
|
||||||
width: "minmax(120px, 0.8fr)"
|
width: "minmax(90px, 0.5fr)",
|
||||||
}
|
}
|
||||||
: null
|
: null,
|
||||||
].filter(Boolean);
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((column) => {
|
||||||
|
if (column.key === "seller") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索用户 ID、展示 ID、用户名",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "status") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: coinSellerStatusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.changeStatus,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
const regionFilter = createRegionColumnFilter({
|
||||||
|
loading: page.loadingRegions,
|
||||||
|
options: page.regionOptions,
|
||||||
|
value: page.regionId,
|
||||||
|
onChange: page.changeRegionId,
|
||||||
|
});
|
||||||
const toolbarActions = page.abilities.canCreate
|
const toolbarActions = page.abilities.canCreate
|
||||||
? [{ icon: <Add fontSize="small" />, label: "添加币商", onClick: page.openCreateSeller, variant: "primary" }]
|
? [{ icon: <Add fontSize="small" />, label: "添加币商", onClick: page.openCreateSeller, variant: "primary" }]
|
||||||
: [];
|
: [];
|
||||||
@ -73,7 +141,8 @@ export function HostCoinSellersPage() {
|
|||||||
statusOptions: coinSellerStatusFilters,
|
statusOptions: coinSellerStatusFilters,
|
||||||
onQueryChange: page.changeQuery,
|
onQueryChange: page.changeQuery,
|
||||||
onRegionIdChange: page.changeRegionId,
|
onRegionIdChange: page.changeRegionId,
|
||||||
onStatusChange: page.changeStatus
|
onReset: page.resetFilters,
|
||||||
|
onStatusChange: page.changeStatus,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -81,18 +150,20 @@ export function HostCoinSellersPage() {
|
|||||||
<div className={styles.listBlock}>
|
<div className={styles.listBlock}>
|
||||||
<HostOrgTable
|
<HostOrgTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
context={{ regionOptions: page.regionOptions }}
|
context={{ page, regionOptions: page.regionOptions }}
|
||||||
items={items}
|
items={items}
|
||||||
minWidth="1040px"
|
minWidth="1040px"
|
||||||
regionFilter={createRegionColumnFilter({
|
regionFilter={regionFilter}
|
||||||
loading: page.loadingRegions,
|
|
||||||
options: page.regionOptions,
|
|
||||||
value: page.regionId,
|
|
||||||
onChange: page.changeRegionId
|
|
||||||
})}
|
|
||||||
rowKey={(item) => item.userId}
|
rowKey={(item) => item.userId}
|
||||||
/>
|
/>
|
||||||
{total > 0 ? <PaginationBar page={page.page} pageSize={page.data.pageSize || 10} total={total} onPageChange={page.setPage} /> : null}
|
{total > 0 ? (
|
||||||
|
<PaginationBar
|
||||||
|
page={page.page}
|
||||||
|
pageSize={page.data.pageSize || 10}
|
||||||
|
total={total}
|
||||||
|
onPageChange={page.setPage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</DataState>
|
</DataState>
|
||||||
</div>
|
</div>
|
||||||
@ -105,8 +176,20 @@ export function HostCoinSellersPage() {
|
|||||||
onSubmit={page.submitCreateSeller}
|
onSubmit={page.submitCreateSeller}
|
||||||
title="添加币商"
|
title="添加币商"
|
||||||
>
|
>
|
||||||
<TextField disabled={createDisabled} label="用户短 ID" required type="number" value={page.createForm.targetUserId} onChange={(event) => page.setCreateForm({ ...page.createForm, targetUserId: event.target.value })} />
|
<TextField
|
||||||
<TextField disabled={createDisabled} label="联系方式" value={page.createForm.reason} onChange={(event) => page.setCreateForm({ ...page.createForm, reason: event.target.value })} />
|
disabled={createDisabled}
|
||||||
|
label="用户短 ID"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
value={page.createForm.targetUserId}
|
||||||
|
onChange={(event) => page.setCreateForm({ ...page.createForm, targetUserId: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={createDisabled}
|
||||||
|
label="联系方式"
|
||||||
|
value={page.createForm.contact}
|
||||||
|
onChange={(event) => page.setCreateForm({ ...page.createForm, contact: event.target.value })}
|
||||||
|
/>
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
|
|
||||||
<HostOrgActionModal
|
<HostOrgActionModal
|
||||||
@ -115,14 +198,90 @@ export function HostCoinSellersPage() {
|
|||||||
open={page.activeAction === "edit"}
|
open={page.activeAction === "edit"}
|
||||||
onClose={page.closeAction}
|
onClose={page.closeAction}
|
||||||
onSubmit={page.submitEditSeller}
|
onSubmit={page.submitEditSeller}
|
||||||
title="编辑币商"
|
title="编辑联系方式"
|
||||||
>
|
>
|
||||||
<TextField disabled label="用户 ID" required type="number" value={page.editForm.targetUserId} />
|
<TextField disabled label="用户 ID" required type="number" value={page.editForm.targetUserId} />
|
||||||
<TextField disabled={updateDisabled} label="状态" required select value={page.editForm.status} onChange={(event) => page.setEditForm({ ...page.editForm, status: event.target.value })}>
|
<TextField
|
||||||
<MenuItem value="active">启用</MenuItem>
|
disabled={updateDisabled}
|
||||||
<MenuItem value="disabled">停用</MenuItem>
|
label="联系方式"
|
||||||
|
value={page.editForm.contact}
|
||||||
|
onChange={(event) => page.setEditForm({ ...page.editForm, contact: event.target.value })}
|
||||||
|
/>
|
||||||
|
</HostOrgActionModal>
|
||||||
|
|
||||||
|
<HostOrgActionModal
|
||||||
|
disabled={!page.abilities.canStockCredit || page.selectedSeller?.status !== "active"}
|
||||||
|
loading={String(page.loadingAction).startsWith("coin-seller-stock")}
|
||||||
|
open={page.activeAction === "stock"}
|
||||||
|
size="standard"
|
||||||
|
submitLabel="确认入账"
|
||||||
|
onClose={page.closeAction}
|
||||||
|
onSubmit={page.submitStockCredit}
|
||||||
|
title="币商进货"
|
||||||
|
>
|
||||||
|
<AdminFormSection title="基础信息">
|
||||||
|
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||||||
|
<AdminFormReadOnlyField label="币商用户 ID" value={page.selectedSeller?.userId || ""} />
|
||||||
|
<AdminFormReadOnlyField
|
||||||
|
label="当前库存余额"
|
||||||
|
value={formatNumber(page.selectedSeller?.merchantBalance)}
|
||||||
|
/>
|
||||||
|
</AdminFormFieldGrid>
|
||||||
|
</AdminFormSection>
|
||||||
|
<AdminFormSection title="充值信息">
|
||||||
|
<TextField
|
||||||
|
disabled={!page.abilities.canStockCredit}
|
||||||
|
label="类型"
|
||||||
|
required
|
||||||
|
select
|
||||||
|
value={page.stockForm.type}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setStockForm({
|
||||||
|
...page.stockForm,
|
||||||
|
type: event.target.value,
|
||||||
|
rechargeAmount: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value="usdt_purchase">USDT进货</MenuItem>
|
||||||
|
<MenuItem value="coin_compensation">金币补偿</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField disabled={updateDisabled} label="联系方式" value={page.editForm.reason} onChange={(event) => page.setEditForm({ ...page.editForm, reason: event.target.value })} />
|
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||||||
|
<AdminFormAmountField
|
||||||
|
disabled={!page.abilities.canStockCredit}
|
||||||
|
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
|
||||||
|
label="充值金币"
|
||||||
|
required
|
||||||
|
unit="金币"
|
||||||
|
value={page.stockForm.coinAmount}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setStockForm({ ...page.stockForm, coinAmount: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{page.stockForm.type === "usdt_purchase" ? (
|
||||||
|
<AdminFormAmountField
|
||||||
|
disabled={!page.abilities.canStockCredit}
|
||||||
|
label="充值金额"
|
||||||
|
required
|
||||||
|
unit="USDT"
|
||||||
|
value={page.stockForm.rechargeAmount}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setStockForm({ ...page.stockForm, rechargeAmount: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</AdminFormFieldGrid>
|
||||||
|
</AdminFormSection>
|
||||||
|
<AdminFormSection title="备注信息">
|
||||||
|
<TextField
|
||||||
|
disabled={!page.abilities.canStockCredit}
|
||||||
|
label="原因"
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
value={page.stockForm.reason}
|
||||||
|
onChange={(event) => page.setStockForm({ ...page.stockForm, reason: event.target.value })}
|
||||||
|
/>
|
||||||
|
</AdminFormSection>
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@ -134,7 +293,11 @@ function SellerIdentity({ item }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.sellerIdentity}>
|
<div className={styles.sellerIdentity}>
|
||||||
{item.avatar ? <img alt="" className={styles.sellerAvatar} src={item.avatar} /> : <span className={styles.sellerAvatar}>{name.slice(0, 1).toUpperCase()}</span>}
|
{item.avatar ? (
|
||||||
|
<img alt="" className={styles.sellerAvatar} src={item.avatar} />
|
||||||
|
) : (
|
||||||
|
<span className={styles.sellerAvatar}>{name.slice(0, 1).toUpperCase()}</span>
|
||||||
|
)}
|
||||||
<span className={styles.sellerText}>
|
<span className={styles.sellerText}>
|
||||||
<span className={styles.sellerName}>{name}</span>
|
<span className={styles.sellerName}>{name}</span>
|
||||||
<span className={styles.sellerMeta}>{meta}</span>
|
<span className={styles.sellerMeta}>{meta}</span>
|
||||||
@ -154,17 +317,32 @@ function SellerStatusSwitch({ item, page }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SellerActions({ item, page }) {
|
function SellerContact({ item, page }) {
|
||||||
const deleting = page.loadingAction === `coin-seller-delete-${item.userId}`;
|
const contact = String(item.contact || "").trim();
|
||||||
|
if (!page?.abilities.canUpdate) {
|
||||||
|
return <span className={contact ? styles.inlineValue : styles.contactPlaceholder}>{contact || "未填写"}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={styles.contactButton} type="button" onClick={() => page.openEditSeller(item)}>
|
||||||
|
{contact || "填写联系方式"}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SellerActions({ item, page }) {
|
||||||
return (
|
return (
|
||||||
<AdminRowActions>
|
<AdminRowActions>
|
||||||
<AdminActionIconButton label="编辑币商" onClick={() => page.openEditSeller(item)}>
|
{page.abilities.canStockCredit ? (
|
||||||
<EditOutlined fontSize="small" />
|
<AdminActionIconButton
|
||||||
</AdminActionIconButton>
|
disabled={item.status !== "active" || page.loadingAction === `coin-seller-stock-${item.userId}`}
|
||||||
<AdminActionIconButton disabled={deleting || item.status === "disabled"} label="删除币商" tone="danger" onClick={() => page.deleteSeller(item)}>
|
label="加金币"
|
||||||
<DeleteOutlineOutlined fontSize="small" />
|
sx={coinCreditActionSx}
|
||||||
|
onClick={() => page.openStockCredit(item)}
|
||||||
|
>
|
||||||
|
<MonetizationOnOutlined fontSize="small" />
|
||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
|
) : null}
|
||||||
</AdminRowActions>
|
</AdminRowActions>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -180,3 +358,17 @@ function regionName(item, regionOptions = []) {
|
|||||||
function formatNumber(value) {
|
function formatNumber(value) {
|
||||||
return Number(value || 0).toLocaleString("zh-CN");
|
return Number(value || 0).toLocaleString("zh-CN");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const coinCreditActionSx = {
|
||||||
|
borderColor: "rgba(245, 158, 11, 0.45)",
|
||||||
|
backgroundColor: "rgba(251, 191, 36, 0.16)",
|
||||||
|
color: "#b45309",
|
||||||
|
"&:hover": {
|
||||||
|
borderColor: "rgba(245, 158, 11, 0.72)",
|
||||||
|
backgroundColor: "rgba(251, 191, 36, 0.24)",
|
||||||
|
color: "#92400e",
|
||||||
|
},
|
||||||
|
"& .MuiSvgIcon-root": {
|
||||||
|
color: "#f59e0b",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -7,28 +7,38 @@ import { formatMillis } from "@/shared/utils/time.js";
|
|||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import {
|
import {
|
||||||
AdminActionIconButton,
|
AdminActionIconButton,
|
||||||
AdminFilterSelect,
|
AdminFilterResetButton,
|
||||||
AdminListBody,
|
AdminListBody,
|
||||||
AdminListPage,
|
AdminListPage,
|
||||||
AdminListToolbar,
|
AdminListToolbar,
|
||||||
AdminRowActions,
|
AdminRowActions,
|
||||||
AdminSearchBox
|
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { countryEnabledFilters } from "@/features/host-org/constants.js";
|
import { countryEnabledFilters } from "@/features/host-org/constants.js";
|
||||||
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
||||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||||
import { useHostCountriesPage } from "@/features/host-org/hooks/useHostCountriesPage.js";
|
import { useHostCountriesPage } from "@/features/host-org/hooks/useHostCountriesPage.js";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import styles from "@/features/host-org/host-org.module.css";
|
import styles from "@/features/host-org/host-org.module.css";
|
||||||
|
|
||||||
const baseCountryColumns = [
|
const baseCountryColumns = [
|
||||||
{ key: "countryId", label: "国家 ID", width: "minmax(90px, 0.6fr)" },
|
{ key: "countryId", label: "国家 ID", width: "minmax(90px, 0.6fr)" },
|
||||||
{ key: "flag", label: "国旗", render: (item) => <span className={styles.flagCell}>{item.flag || "-"}</span>, width: "minmax(70px, 0.4fr)" },
|
{
|
||||||
|
key: "flag",
|
||||||
|
label: "国旗",
|
||||||
|
render: (item) => <span className={styles.flagCell}>{item.flag || "-"}</span>,
|
||||||
|
width: "minmax(70px, 0.4fr)",
|
||||||
|
},
|
||||||
{ key: "countryCode", label: "国家码", width: "minmax(90px, 0.7fr)" },
|
{ key: "countryCode", label: "国家码", width: "minmax(90px, 0.7fr)" },
|
||||||
{ key: "countryDisplayName", label: "展示名称", width: "minmax(150px, 1fr)" },
|
{ key: "countryDisplayName", label: "展示名称", width: "minmax(150px, 1fr)" },
|
||||||
{ key: "countryName", label: "国家名称", width: "minmax(150px, 1fr)" },
|
{ key: "countryName", label: "国家名称", width: "minmax(150px, 1fr)" },
|
||||||
{ key: "phoneCountryCode", label: "电话区号", width: "minmax(90px, 0.7fr)" },
|
{ key: "phoneCountryCode", label: "电话区号", width: "minmax(90px, 0.7fr)" },
|
||||||
{ key: "sortOrder", label: "排序", width: "minmax(80px, 0.5fr)" },
|
{ key: "sortOrder", label: "排序", width: "minmax(80px, 0.5fr)" },
|
||||||
{ key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" }
|
{
|
||||||
|
key: "updatedAtMs",
|
||||||
|
label: "更新时间",
|
||||||
|
render: (item) => formatMillis(item.updatedAtMs),
|
||||||
|
width: "minmax(170px, 1fr)",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function HostCountriesPage() {
|
export function HostCountriesPage() {
|
||||||
@ -43,10 +53,27 @@ export function HostCountriesPage() {
|
|||||||
key: "enabled",
|
key: "enabled",
|
||||||
label: "状态",
|
label: "状态",
|
||||||
render: (item) => <CountryStatusSwitch item={item} page={page} />,
|
render: (item) => <CountryStatusSwitch item={item} page={page} />,
|
||||||
width: "minmax(90px, 0.6fr)"
|
filter: createOptionsColumnFilter({
|
||||||
|
options: countryEnabledFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.enabledStatus,
|
||||||
|
onChange: page.changeEnabledStatus,
|
||||||
|
}),
|
||||||
|
width: "minmax(90px, 0.6fr)",
|
||||||
},
|
},
|
||||||
...baseCountryColumns.slice(6)
|
...baseCountryColumns.slice(6),
|
||||||
];
|
].map((column) =>
|
||||||
|
column.key === "countryCode"
|
||||||
|
? {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索国家码、国家名称",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: column,
|
||||||
|
);
|
||||||
const columns = page.abilities.canUpdate
|
const columns = page.abilities.canUpdate
|
||||||
? [
|
? [
|
||||||
...countryColumns,
|
...countryColumns,
|
||||||
@ -54,37 +81,28 @@ export function HostCountriesPage() {
|
|||||||
key: "actions",
|
key: "actions",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
render: (item) => <CountryActions item={item} page={page} />,
|
render: (item) => <CountryActions item={item} page={page} />,
|
||||||
width: "minmax(104px, 0.7fr)"
|
width: "minmax(104px, 0.7fr)",
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
: countryColumns;
|
: countryColumns;
|
||||||
return (
|
return (
|
||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
filters={(
|
filters={
|
||||||
<>
|
<AdminFilterResetButton disabled={!page.query && !page.enabledStatus} onClick={page.resetFilters} />
|
||||||
<AdminSearchBox value={page.query} onChange={page.changeQuery} placeholder="搜索国家码、国家名称..." />
|
}
|
||||||
<AdminFilterSelect
|
actions={
|
||||||
options={countryEnabledFilters}
|
page.abilities.canCreate ? (
|
||||||
value={page.enabledStatus}
|
|
||||||
onChange={page.changeEnabledStatus}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
actions={page.abilities.canCreate ? (
|
|
||||||
<AdminActionIconButton label="创建国家" primary onClick={page.openCreateCountry}>
|
<AdminActionIconButton label="创建国家" primary onClick={page.openCreateCountry}>
|
||||||
<Add fontSize="small" />
|
<Add fontSize="small" />
|
||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
) : null}
|
) : null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<AdminListBody>
|
<AdminListBody>
|
||||||
<HostOrgTable
|
<HostOrgTable columns={columns} items={items} rowKey={(item) => item.countryId} />
|
||||||
columns={columns}
|
|
||||||
items={items}
|
|
||||||
rowKey={(item) => item.countryId}
|
|
||||||
/>
|
|
||||||
<div className="pagination-bar">
|
<div className="pagination-bar">
|
||||||
<span>{total} 条</span>
|
<span>{total} 条</span>
|
||||||
</div>
|
</div>
|
||||||
@ -99,22 +117,77 @@ export function HostCountriesPage() {
|
|||||||
onSubmit={page.submitCountry}
|
onSubmit={page.submitCountry}
|
||||||
title={page.activeAction === "edit" ? "编辑国家" : "创建国家"}
|
title={page.activeAction === "edit" ? "编辑国家" : "创建国家"}
|
||||||
>
|
>
|
||||||
<TextField disabled={page.activeAction === "edit" || createDisabled} label="国家码" required value={page.countryForm.countryCode} onChange={(event) => page.setCountryForm({ ...page.countryForm, countryCode: event.target.value })} />
|
<TextField
|
||||||
<TextField disabled={page.activeAction === "edit" ? updateDisabled : createDisabled} label="展示名称" required value={page.countryForm.countryDisplayName} onChange={(event) => page.setCountryForm({ ...page.countryForm, countryDisplayName: event.target.value })} />
|
disabled={page.activeAction === "edit" || createDisabled}
|
||||||
<TextField disabled={page.activeAction === "edit" ? updateDisabled : createDisabled} label="国家名称" required value={page.countryForm.countryName} onChange={(event) => page.setCountryForm({ ...page.countryForm, countryName: event.target.value })} />
|
label="国家码"
|
||||||
<TextField disabled={page.activeAction === "edit" ? updateDisabled : createDisabled} label="ISO Alpha-3" value={page.countryForm.isoAlpha3} onChange={(event) => page.setCountryForm({ ...page.countryForm, isoAlpha3: event.target.value })} />
|
required
|
||||||
<TextField disabled={page.activeAction === "edit" ? updateDisabled : createDisabled} label="ISO Numeric" value={page.countryForm.isoNumeric} onChange={(event) => page.setCountryForm({ ...page.countryForm, isoNumeric: event.target.value })} />
|
value={page.countryForm.countryCode}
|
||||||
<TextField disabled={page.activeAction === "edit" ? updateDisabled : createDisabled} label="电话区号" value={page.countryForm.phoneCountryCode} onChange={(event) => page.setCountryForm({ ...page.countryForm, phoneCountryCode: event.target.value })} />
|
onChange={(event) => page.setCountryForm({ ...page.countryForm, countryCode: event.target.value })}
|
||||||
<TextField disabled={page.activeAction === "edit" ? updateDisabled : createDisabled} label="国旗" value={page.countryForm.flag} onChange={(event) => page.setCountryForm({ ...page.countryForm, flag: event.target.value })} />
|
/>
|
||||||
<TextField disabled={page.activeAction === "edit" ? updateDisabled : createDisabled} label="排序" type="number" value={page.countryForm.sortOrder} onChange={(event) => page.setCountryForm({ ...page.countryForm, sortOrder: event.target.value })} />
|
<TextField
|
||||||
|
disabled={page.activeAction === "edit" ? updateDisabled : createDisabled}
|
||||||
|
label="展示名称"
|
||||||
|
required
|
||||||
|
value={page.countryForm.countryDisplayName}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setCountryForm({ ...page.countryForm, countryDisplayName: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={page.activeAction === "edit" ? updateDisabled : createDisabled}
|
||||||
|
label="国家名称"
|
||||||
|
required
|
||||||
|
value={page.countryForm.countryName}
|
||||||
|
onChange={(event) => page.setCountryForm({ ...page.countryForm, countryName: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={page.activeAction === "edit" ? updateDisabled : createDisabled}
|
||||||
|
label="ISO Alpha-3"
|
||||||
|
value={page.countryForm.isoAlpha3}
|
||||||
|
onChange={(event) => page.setCountryForm({ ...page.countryForm, isoAlpha3: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={page.activeAction === "edit" ? updateDisabled : createDisabled}
|
||||||
|
label="ISO Numeric"
|
||||||
|
value={page.countryForm.isoNumeric}
|
||||||
|
onChange={(event) => page.setCountryForm({ ...page.countryForm, isoNumeric: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={page.activeAction === "edit" ? updateDisabled : createDisabled}
|
||||||
|
label="电话区号"
|
||||||
|
value={page.countryForm.phoneCountryCode}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setCountryForm({ ...page.countryForm, phoneCountryCode: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={page.activeAction === "edit" ? updateDisabled : createDisabled}
|
||||||
|
label="国旗"
|
||||||
|
value={page.countryForm.flag}
|
||||||
|
onChange={(event) => page.setCountryForm({ ...page.countryForm, flag: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={page.activeAction === "edit" ? updateDisabled : createDisabled}
|
||||||
|
label="排序"
|
||||||
|
type="number"
|
||||||
|
value={page.countryForm.sortOrder}
|
||||||
|
onChange={(event) => page.setCountryForm({ ...page.countryForm, sortOrder: event.target.value })}
|
||||||
|
/>
|
||||||
{page.activeAction === "create" ? (
|
{page.activeAction === "create" ? (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={<Switch checked={page.countryForm.enabled} disabled={createDisabled} onChange={(event) => page.setCountryForm({ ...page.countryForm, enabled: event.target.checked })} />}
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={page.countryForm.enabled}
|
||||||
|
disabled={createDisabled}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setCountryForm({ ...page.countryForm, enabled: event.target.checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={page.countryForm.enabled ? "启用" : "停用"}
|
label={page.countryForm.enabled ? "启用" : "停用"}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
|
|
||||||
</AdminListPage>
|
</AdminListPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,111 @@
|
|||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
|
||||||
import { hostSourceLabels, hostStatusFilters } from "@/features/host-org/constants.js";
|
import { hostSourceLabels, hostStatusFilters } from "@/features/host-org/constants.js";
|
||||||
|
import { HostOrgEntity, HostOrgPerson, hostOrgRegionName } from "@/features/host-org/components/HostOrgIdentity.jsx";
|
||||||
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
|
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
|
||||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||||
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
||||||
import { useHostHostsPage } from "@/features/host-org/hooks/useHostHostsPage.js";
|
import { useHostHostsPage } from "@/features/host-org/hooks/useHostHostsPage.js";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import styles from "@/features/host-org/host-org.module.css";
|
import styles from "@/features/host-org/host-org.module.css";
|
||||||
|
|
||||||
const hostColumns = [
|
const hostColumns = [
|
||||||
{ key: "userId", label: "用户 ID", width: "minmax(110px, 0.8fr)" },
|
{
|
||||||
{ key: "status", label: "状态", render: (item) => <HostOrgStatus value={item.status} />, width: "minmax(90px, 0.7fr)" },
|
key: "user",
|
||||||
{ key: "regionId", label: "区域 ID", width: "minmax(90px, 0.7fr)" },
|
label: "用户",
|
||||||
{ key: "currentAgencyId", label: "当前 Agency ID", render: (item) => item.currentAgencyId || "-" },
|
render: (item) => <HostUser item={item} />,
|
||||||
{ key: "currentMembershipId", label: "当前 Membership ID", render: (item) => item.currentMembershipId || "-" },
|
width: "minmax(220px, 1.25fr)",
|
||||||
{ key: "source", label: "来源", render: (item) => hostSourceLabels[item.source] || item.source || "-", width: "minmax(150px, 1fr)" },
|
},
|
||||||
{ key: "firstBecameHostAtMs", label: "首次成为主播", render: (item) => formatMillis(item.firstBecameHostAtMs), width: "minmax(170px, 1fr)" },
|
{
|
||||||
{ key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" }
|
key: "status",
|
||||||
|
label: "状态",
|
||||||
|
render: (item) => <HostOrgStatus value={item.status} />,
|
||||||
|
width: "minmax(90px, 0.7fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "regionId",
|
||||||
|
label: "区域",
|
||||||
|
render: (item, _index, context) => hostOrgRegionName(item, context?.regionOptions),
|
||||||
|
width: "minmax(130px, 0.75fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "currentAgencyId",
|
||||||
|
label: "当前 Agency",
|
||||||
|
render: (item) => (
|
||||||
|
<HostOrgEntity id={item.currentAgencyId || ""} name={item.currentAgencyName || ""} prefix="Agency " />
|
||||||
|
),
|
||||||
|
width: "minmax(190px, 1fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "currentMembershipId",
|
||||||
|
label: "当前 Membership",
|
||||||
|
render: (item) =>
|
||||||
|
item.currentMembershipId ? (
|
||||||
|
<HostOrgEntity id={item.currentMembershipId} name="Membership" prefix="" />
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
),
|
||||||
|
width: "minmax(170px, 0.9fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "source",
|
||||||
|
label: "来源",
|
||||||
|
render: (item) => hostSourceLabels[item.source] || item.source || "-",
|
||||||
|
width: "minmax(150px, 1fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "firstBecameHostAtMs",
|
||||||
|
label: "首次成为主播",
|
||||||
|
render: (item) => formatMillis(item.firstBecameHostAtMs),
|
||||||
|
width: "minmax(170px, 1fr)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "updatedAtMs",
|
||||||
|
label: "更新时间",
|
||||||
|
render: (item) => formatMillis(item.updatedAtMs),
|
||||||
|
width: "minmax(170px, 1fr)",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function HostHostsPage() {
|
export function HostHostsPage() {
|
||||||
const page = useHostHostsPage();
|
const page = useHostHostsPage();
|
||||||
const items = page.data.items || [];
|
const items = page.data.items || [];
|
||||||
const total = page.data.total || 0;
|
const total = page.data.total || 0;
|
||||||
|
const columns = hostColumns.map((column) => {
|
||||||
|
if (column.key === "user") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索用户 ID、展示 ID、用户名、Agency",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "currentAgencyId") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索 Agency ID",
|
||||||
|
value: page.agencyId,
|
||||||
|
onChange: page.changeAgencyId,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "status") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: hostStatusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.changeStatus,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.root}>
|
<section className={styles.root}>
|
||||||
@ -35,8 +117,8 @@ export function HostHostsPage() {
|
|||||||
label: "Agency ID",
|
label: "Agency ID",
|
||||||
name: "agencyId",
|
name: "agencyId",
|
||||||
onChange: page.changeAgencyId,
|
onChange: page.changeAgencyId,
|
||||||
value: page.agencyId
|
value: page.agencyId,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
loadingRegions: page.loadingRegions,
|
loadingRegions: page.loadingRegions,
|
||||||
query: page.query,
|
query: page.query,
|
||||||
@ -47,27 +129,42 @@ export function HostHostsPage() {
|
|||||||
statusOptions: hostStatusFilters,
|
statusOptions: hostStatusFilters,
|
||||||
onQueryChange: page.changeQuery,
|
onQueryChange: page.changeQuery,
|
||||||
onRegionIdChange: page.changeRegionId,
|
onRegionIdChange: page.changeRegionId,
|
||||||
onStatusChange: page.changeStatus
|
onReset: page.resetFilters,
|
||||||
|
onStatusChange: page.changeStatus,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<div className={styles.listBlock}>
|
<div className={styles.listBlock}>
|
||||||
<HostOrgTable
|
<HostOrgTable
|
||||||
columns={hostColumns}
|
columns={columns}
|
||||||
|
context={{ regionOptions: page.regionOptions }}
|
||||||
items={items}
|
items={items}
|
||||||
regionFilter={createRegionColumnFilter({
|
minWidth="1240px"
|
||||||
loading: page.loadingRegions,
|
|
||||||
options: page.regionOptions,
|
|
||||||
value: page.regionId,
|
|
||||||
onChange: page.changeRegionId
|
|
||||||
})}
|
|
||||||
rowKey={(item) => item.userId}
|
rowKey={(item) => item.userId}
|
||||||
/>
|
/>
|
||||||
{total > 0 ? <PaginationBar page={page.page} pageSize={page.data.pageSize || 10} total={total} onPageChange={page.setPage} /> : null}
|
{total > 0 ? (
|
||||||
|
<PaginationBar
|
||||||
|
page={page.page}
|
||||||
|
pageSize={page.data.pageSize || 10}
|
||||||
|
total={total}
|
||||||
|
onPageChange={page.setPage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</DataState>
|
</DataState>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function HostUser({ item }) {
|
||||||
|
return (
|
||||||
|
<HostOrgPerson
|
||||||
|
avatar={item.avatar}
|
||||||
|
displayUserId={item.displayUserId}
|
||||||
|
fallbackId={item.userId}
|
||||||
|
username={item.username}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -5,23 +5,33 @@ import TextField from "@mui/material/TextField";
|
|||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
|
import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
||||||
import { MultiValueAutocomplete } from "@/shared/ui/MultiValueAutocomplete.jsx";
|
import { MultiValueAutocomplete } from "@/shared/ui/MultiValueAutocomplete.jsx";
|
||||||
import { SearchBox } from "@/shared/ui/SearchBox.jsx";
|
|
||||||
import { StatusSelect } from "@/shared/ui/StatusSelect.jsx";
|
|
||||||
import { regionStatusFilters } from "@/features/host-org/constants.js";
|
import { regionStatusFilters } from "@/features/host-org/constants.js";
|
||||||
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
import { HostOrgActionModal } from "@/features/host-org/components/HostOrgActionModal.jsx";
|
||||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||||
import { useHostRegionsPage } from "@/features/host-org/hooks/useHostRegionsPage.js";
|
import { useHostRegionsPage } from "@/features/host-org/hooks/useHostRegionsPage.js";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import styles from "@/features/host-org/host-org.module.css";
|
import styles from "@/features/host-org/host-org.module.css";
|
||||||
|
|
||||||
const baseRegionColumns = [
|
const baseRegionColumns = [
|
||||||
{ key: "regionId", label: "区域 ID", width: "minmax(90px, 0.6fr)" },
|
{ key: "regionId", label: "区域 ID", width: "minmax(90px, 0.6fr)" },
|
||||||
{ key: "regionCode", label: "区域编码", width: "minmax(110px, 0.8fr)" },
|
{ key: "regionCode", label: "区域编码", width: "minmax(110px, 0.8fr)" },
|
||||||
{ key: "name", label: "区域名称", width: "minmax(150px, 1fr)" },
|
{ key: "name", label: "区域名称", width: "minmax(150px, 1fr)" },
|
||||||
{ key: "countries", label: "国家码", render: (item) => <CountryCodes countries={item.countries} />, width: "minmax(220px, 1.5fr)" },
|
{
|
||||||
|
key: "countries",
|
||||||
|
label: "国家码",
|
||||||
|
render: (item) => <CountryCodes countries={item.countries} />,
|
||||||
|
width: "minmax(220px, 1.5fr)",
|
||||||
|
},
|
||||||
{ key: "sortOrder", label: "排序", width: "minmax(80px, 0.5fr)" },
|
{ key: "sortOrder", label: "排序", width: "minmax(80px, 0.5fr)" },
|
||||||
{ key: "updatedAtMs", label: "更新时间", render: (item) => formatMillis(item.updatedAtMs), width: "minmax(170px, 1fr)" }
|
{
|
||||||
|
key: "updatedAtMs",
|
||||||
|
label: "更新时间",
|
||||||
|
render: (item) => formatMillis(item.updatedAtMs),
|
||||||
|
width: "minmax(170px, 1fr)",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function HostRegionsPage() {
|
export function HostRegionsPage() {
|
||||||
@ -36,10 +46,27 @@ export function HostRegionsPage() {
|
|||||||
key: "status",
|
key: "status",
|
||||||
label: "状态",
|
label: "状态",
|
||||||
render: (item) => <RegionStatusSwitch item={item} page={page} />,
|
render: (item) => <RegionStatusSwitch item={item} page={page} />,
|
||||||
width: "minmax(90px, 0.6fr)"
|
filter: createOptionsColumnFilter({
|
||||||
|
options: regionStatusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.changeStatus,
|
||||||
|
}),
|
||||||
|
width: "minmax(90px, 0.6fr)",
|
||||||
},
|
},
|
||||||
...baseRegionColumns.slice(3)
|
...baseRegionColumns.slice(3),
|
||||||
];
|
].map((column) =>
|
||||||
|
column.key === "regionCode"
|
||||||
|
? {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索区域、国家码",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: column,
|
||||||
|
);
|
||||||
const columns = page.abilities.canUpdate
|
const columns = page.abilities.canUpdate
|
||||||
? [
|
? [
|
||||||
...regionColumns,
|
...regionColumns,
|
||||||
@ -47,8 +74,8 @@ export function HostRegionsPage() {
|
|||||||
key: "actions",
|
key: "actions",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
render: (item) => <RegionActions item={item} page={page} />,
|
render: (item) => <RegionActions item={item} page={page} />,
|
||||||
width: "minmax(80px, 0.5fr)"
|
width: "minmax(80px, 0.5fr)",
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
: regionColumns;
|
: regionColumns;
|
||||||
return (
|
return (
|
||||||
@ -57,12 +84,9 @@ export function HostRegionsPage() {
|
|||||||
<div className={styles.toolbar}>
|
<div className={styles.toolbar}>
|
||||||
<div className={styles.toolbarLeft}>
|
<div className={styles.toolbarLeft}>
|
||||||
<section className={styles.filters}>
|
<section className={styles.filters}>
|
||||||
<SearchBox className={styles.search} value={page.query} onChange={page.changeQuery} placeholder="搜索区域、国家码..." />
|
<AdminFilterResetButton
|
||||||
<StatusSelect
|
disabled={!page.query && !page.status}
|
||||||
className={styles.statusSelect}
|
onClick={page.resetFilters}
|
||||||
options={regionStatusFilters}
|
|
||||||
value={page.status}
|
|
||||||
onChange={page.changeStatus}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -82,11 +106,7 @@ export function HostRegionsPage() {
|
|||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<div className={styles.listBlock}>
|
<div className={styles.listBlock}>
|
||||||
<HostOrgTable
|
<HostOrgTable columns={columns} items={items} rowKey={(item) => item.regionId} />
|
||||||
columns={columns}
|
|
||||||
items={items}
|
|
||||||
rowKey={(item) => item.regionId}
|
|
||||||
/>
|
|
||||||
<div className="pagination-bar">
|
<div className="pagination-bar">
|
||||||
<span>{total} 条</span>
|
<span>{total} 条</span>
|
||||||
</div>
|
</div>
|
||||||
@ -102,11 +122,31 @@ export function HostRegionsPage() {
|
|||||||
onSubmit={page.submitRegion}
|
onSubmit={page.submitRegion}
|
||||||
title={page.activeAction === "edit" ? "编辑区域" : "创建区域"}
|
title={page.activeAction === "edit" ? "编辑区域" : "创建区域"}
|
||||||
>
|
>
|
||||||
<TextField disabled={page.activeAction === "edit" ? updateDisabled : createDisabled} label="区域编码" required value={page.regionForm.regionCode} onChange={(event) => page.setRegionForm({ ...page.regionForm, regionCode: event.target.value })} />
|
<TextField
|
||||||
<TextField disabled={page.activeAction === "edit" ? updateDisabled : createDisabled} label="区域名称" required value={page.regionForm.name} onChange={(event) => page.setRegionForm({ ...page.regionForm, name: event.target.value })} />
|
disabled={page.activeAction === "edit" ? updateDisabled : createDisabled}
|
||||||
<TextField disabled={page.activeAction === "edit" ? updateDisabled : createDisabled} label="排序" type="number" value={page.regionForm.sortOrder} onChange={(event) => page.setRegionForm({ ...page.regionForm, sortOrder: event.target.value })} />
|
label="区域编码"
|
||||||
|
required
|
||||||
|
value={page.regionForm.regionCode}
|
||||||
|
onChange={(event) => page.setRegionForm({ ...page.regionForm, regionCode: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={page.activeAction === "edit" ? updateDisabled : createDisabled}
|
||||||
|
label="区域名称"
|
||||||
|
required
|
||||||
|
value={page.regionForm.name}
|
||||||
|
onChange={(event) => page.setRegionForm({ ...page.regionForm, name: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={page.activeAction === "edit" ? updateDisabled : createDisabled}
|
||||||
|
label="排序"
|
||||||
|
type="number"
|
||||||
|
value={page.regionForm.sortOrder}
|
||||||
|
onChange={(event) => page.setRegionForm({ ...page.regionForm, sortOrder: event.target.value })}
|
||||||
|
/>
|
||||||
<CountryCodeSelect
|
<CountryCodeSelect
|
||||||
disabled={page.isGlobalRegionForm || (page.activeAction === "edit" ? updateDisabled : createDisabled)}
|
disabled={
|
||||||
|
page.isGlobalRegionForm || (page.activeAction === "edit" ? updateDisabled : createDisabled)
|
||||||
|
}
|
||||||
label={page.isGlobalRegionForm ? "国家码" : "国家码 *"}
|
label={page.isGlobalRegionForm ? "国家码" : "国家码 *"}
|
||||||
labels={page.countryLabels}
|
labels={page.countryLabels}
|
||||||
loading={page.loadingCountries}
|
loading={page.loadingCountries}
|
||||||
@ -115,7 +155,6 @@ export function HostRegionsPage() {
|
|||||||
onChange={(countries) => page.setRegionForm({ ...page.regionForm, countries })}
|
onChange={(countries) => page.setRegionForm({ ...page.regionForm, countries })}
|
||||||
/>
|
/>
|
||||||
</HostOrgActionModal>
|
</HostOrgActionModal>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -191,6 +230,6 @@ const primaryActionSx = {
|
|||||||
"&:hover": {
|
"&:hover": {
|
||||||
borderColor: "var(--primary-strong)",
|
borderColor: "var(--primary-strong)",
|
||||||
backgroundColor: "var(--primary-strong)",
|
backgroundColor: "var(--primary-strong)",
|
||||||
color: "var(--active-contrast)"
|
color: "var(--active-contrast)",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export function useCountryAbilities() {
|
|||||||
canCreate: can(PERMISSIONS.countryCreate),
|
canCreate: can(PERMISSIONS.countryCreate),
|
||||||
canStatus: can(PERMISSIONS.countryStatus),
|
canStatus: can(PERMISSIONS.countryStatus),
|
||||||
canUpdate: can(PERMISSIONS.countryUpdate),
|
canUpdate: can(PERMISSIONS.countryUpdate),
|
||||||
canView: can(PERMISSIONS.countryView)
|
canView: can(PERMISSIONS.countryView),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ export function useRegionAbilities() {
|
|||||||
canCreate: can(PERMISSIONS.regionCreate),
|
canCreate: can(PERMISSIONS.regionCreate),
|
||||||
canStatus: can(PERMISSIONS.regionStatus),
|
canStatus: can(PERMISSIONS.regionStatus),
|
||||||
canUpdate: can(PERMISSIONS.regionUpdate),
|
canUpdate: can(PERMISSIONS.regionUpdate),
|
||||||
canView: can(PERMISSIONS.regionView)
|
canView: can(PERMISSIONS.regionView),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ export function useAgencyAbilities() {
|
|||||||
return {
|
return {
|
||||||
canCreate: can(PERMISSIONS.agencyCreate),
|
canCreate: can(PERMISSIONS.agencyCreate),
|
||||||
canStatus: can(PERMISSIONS.agencyStatus),
|
canStatus: can(PERMISSIONS.agencyStatus),
|
||||||
canView: can(PERMISSIONS.agencyView)
|
canView: can(PERMISSIONS.agencyView),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ export function useHostAbilities() {
|
|||||||
const { can } = useAuth();
|
const { can } = useAuth();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canView: can(PERMISSIONS.hostView)
|
canView: can(PERMISSIONS.hostView),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ export function useBDAbilities() {
|
|||||||
return {
|
return {
|
||||||
canCreate: can(PERMISSIONS.bdCreate),
|
canCreate: can(PERMISSIONS.bdCreate),
|
||||||
canUpdate: can(PERMISSIONS.bdUpdate),
|
canUpdate: can(PERMISSIONS.bdUpdate),
|
||||||
canView: can(PERMISSIONS.bdView)
|
canView: can(PERMISSIONS.bdView),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +56,8 @@ export function useCoinSellerAbilities() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
canCreate: can(PERMISSIONS.coinSellerCreate),
|
canCreate: can(PERMISSIONS.coinSellerCreate),
|
||||||
|
canStockCredit: can(PERMISSIONS.coinSellerStockCredit),
|
||||||
canUpdate: can(PERMISSIONS.coinSellerUpdate),
|
canUpdate: can(PERMISSIONS.coinSellerUpdate),
|
||||||
canView: can(PERMISSIONS.coinSellerView)
|
canView: can(PERMISSIONS.coinSellerView),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,13 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const commandBaseSchema = z.object({
|
const commandBaseSchema = z.object({
|
||||||
commandId: z.string().trim().min(1, "请输入 Command ID").max(120, "Command ID 不能超过 120 个字符"),
|
commandId: z.string().trim().min(1, "请输入 Command ID").max(120, "Command ID 不能超过 120 个字符"),
|
||||||
reason: z.string().trim().max(200, "原因不能超过 200 个字符").optional().default("")
|
reason: z.string().trim().max(200, "原因不能超过 200 个字符").optional().default(""),
|
||||||
});
|
});
|
||||||
|
|
||||||
const commandContactSchema = z.object({
|
const commandContactSchema = z.object({
|
||||||
commandId: z.string().trim().min(1, "请输入 Command ID").max(120, "Command ID 不能超过 120 个字符"),
|
commandId: z.string().trim().min(1, "请输入 Command ID").max(120, "Command ID 不能超过 120 个字符"),
|
||||||
reason: z.string().trim().max(200, "联系方式不能超过 200 个字符").optional().default("")
|
contact: z.string().trim().max(128, "联系方式不能超过 128 个字符").optional().default(""),
|
||||||
|
reason: z.string().trim().max(200, "原因不能超过 200 个字符").optional().default(""),
|
||||||
});
|
});
|
||||||
|
|
||||||
const userIdSchema = z.coerce.number().int().positive("请输入有效用户短 ID");
|
const userIdSchema = z.coerce.number().int().positive("请输入有效用户短 ID");
|
||||||
@ -16,9 +17,7 @@ const sortOrderSchema = z.coerce.number().int().min(0, "排序不能小于 0").d
|
|||||||
const optionalTextSchema = (max: number, message: string) => z.string().trim().max(max, message).optional().default("");
|
const optionalTextSchema = (max: number, message: string) => z.string().trim().max(max, message).optional().default("");
|
||||||
const codeSchema = (label: string) => z.string().trim().min(1, `请输入${label}`).max(40, `${label}不能超过 40 个字符`);
|
const codeSchema = (label: string) => z.string().trim().min(1, `请输入${label}`).max(40, `${label}不能超过 40 个字符`);
|
||||||
const countryCodesSchema = z.array(z.string()).transform((values, ctx) => {
|
const countryCodesSchema = z.array(z.string()).transform((values, ctx) => {
|
||||||
const countries = values
|
const countries = values.map((item) => item.trim().toUpperCase()).filter(Boolean);
|
||||||
.map((item) => item.trim().toUpperCase())
|
|
||||||
.filter(Boolean);
|
|
||||||
if (new Set(countries).size !== countries.length) {
|
if (new Set(countries).size !== countries.length) {
|
||||||
ctx.addIssue({ code: "custom", message: "国家码不能重复" });
|
ctx.addIssue({ code: "custom", message: "国家码不能重复" });
|
||||||
}
|
}
|
||||||
@ -31,19 +30,21 @@ const retiredRegionCodes = new Set([
|
|||||||
"MICRONESIA",
|
"MICRONESIA",
|
||||||
"POLYNESIA",
|
"POLYNESIA",
|
||||||
"SOUTHERN AFRICA",
|
"SOUTHERN AFRICA",
|
||||||
"UNSPECIFIED"
|
"UNSPECIFIED",
|
||||||
]);
|
]);
|
||||||
const requiredCountryCodesSchema = countryCodesSchema.superRefine((countries, ctx) => {
|
const requiredCountryCodesSchema = countryCodesSchema.superRefine((countries, ctx) => {
|
||||||
if (!countries.length) {
|
if (!countries.length) {
|
||||||
ctx.addIssue({ code: "custom", message: "请选择国家码" });
|
ctx.addIssue({ code: "custom", message: "请选择国家码" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const regionBaseSchema = z.object({
|
const regionBaseSchema = z
|
||||||
|
.object({
|
||||||
countries: countryCodesSchema,
|
countries: countryCodesSchema,
|
||||||
name: z.string().trim().min(1, "请输入区域名称").max(80, "区域名称不能超过 80 个字符"),
|
name: z.string().trim().min(1, "请输入区域名称").max(80, "区域名称不能超过 80 个字符"),
|
||||||
regionCode: codeSchema("区域编码").transform((value) => value.toUpperCase()),
|
regionCode: codeSchema("区域编码").transform((value) => value.toUpperCase()),
|
||||||
sortOrder: sortOrderSchema
|
sortOrder: sortOrderSchema,
|
||||||
}).superRefine((value, ctx) => {
|
})
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
if (retiredRegionCodes.has(value.regionCode)) {
|
if (retiredRegionCodes.has(value.regionCode)) {
|
||||||
ctx.addIssue({ code: "custom", message: "该区域已移除", path: ["regionCode"] });
|
ctx.addIssue({ code: "custom", message: "该区域已移除", path: ["regionCode"] });
|
||||||
}
|
}
|
||||||
@ -57,27 +58,47 @@ const regionBaseSchema = z.object({
|
|||||||
|
|
||||||
export const createBDLeaderSchema = commandContactSchema.extend({
|
export const createBDLeaderSchema = commandContactSchema.extend({
|
||||||
regionId: regionIdSchema,
|
regionId: regionIdSchema,
|
||||||
targetUserId: userIdSchema
|
targetUserId: userIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createBDSchema = commandBaseSchema.extend({
|
export const createBDSchema = commandBaseSchema.extend({
|
||||||
parentLeaderUserId: userIdSchema,
|
parentLeaderUserId: userIdSchema,
|
||||||
targetUserId: userIdSchema
|
targetUserId: userIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const bdStatusSchema = commandBaseSchema.extend({
|
export const bdStatusSchema = commandBaseSchema.extend({
|
||||||
status: z.string().trim().min(1, "请输入状态").max(40, "状态不能超过 40 个字符"),
|
status: z.string().trim().min(1, "请输入状态").max(40, "状态不能超过 40 个字符"),
|
||||||
targetType: z.enum(["bd", "leader"]),
|
targetType: z.enum(["bd", "leader"]),
|
||||||
targetUserId: userIdSchema
|
targetUserId: userIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createCoinSellerSchema = commandContactSchema.extend({
|
export const createCoinSellerSchema = commandContactSchema.extend({
|
||||||
targetUserId: userIdSchema
|
targetUserId: userIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const coinSellerStatusSchema = commandContactSchema.extend({
|
export const coinSellerStatusSchema = commandContactSchema.extend({
|
||||||
status: z.enum(["active", "disabled"]),
|
status: z.enum(["active", "disabled"]),
|
||||||
targetUserId: userIdSchema
|
targetUserId: userIdSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const coinSellerStockCreditSchema = z
|
||||||
|
.object({
|
||||||
|
coinAmount: z.coerce.number().int().positive("请输入充值金币"),
|
||||||
|
commandId: z.string().trim().min(1, "请输入 Command ID").max(120, "Command ID 不能超过 120 个字符"),
|
||||||
|
reason: z.string().trim().max(512, "原因不能超过 512 个字符").optional().default(""),
|
||||||
|
rechargeAmount: z.string().trim().max(32, "充值金额不能超过 32 个字符").optional().default(""),
|
||||||
|
type: z.enum(["usdt_purchase", "coin_compensation"]),
|
||||||
|
})
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
if (value.type === "usdt_purchase") {
|
||||||
|
if (!/^(0|[1-9]\d*)(\.\d{1,6})?$/.test(value.rechargeAmount) || Number(value.rechargeAmount) <= 0) {
|
||||||
|
ctx.addIssue({ code: "custom", message: "请输入最多 6 位小数的 USDT 金额", path: ["rechargeAmount"] });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value.rechargeAmount) {
|
||||||
|
ctx.addIssue({ code: "custom", message: "金币补偿不能提交充值金额", path: ["rechargeAmount"] });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createAgencySchema = commandBaseSchema.extend({
|
export const createAgencySchema = commandBaseSchema.extend({
|
||||||
@ -85,20 +106,24 @@ export const createAgencySchema = commandBaseSchema.extend({
|
|||||||
maxHosts: z.coerce.number().int().min(0, "最大主播数不能小于 0").default(0),
|
maxHosts: z.coerce.number().int().min(0, "最大主播数不能小于 0").default(0),
|
||||||
name: z.string().trim().min(1, "请输入 Agency 名称").max(80, "Agency 名称不能超过 80 个字符"),
|
name: z.string().trim().min(1, "请输入 Agency 名称").max(80, "Agency 名称不能超过 80 个字符"),
|
||||||
ownerUserId: userIdSchema,
|
ownerUserId: userIdSchema,
|
||||||
parentBdUserId: userIdSchema
|
parentBdUserId: userIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const agencyCloseSchema = commandBaseSchema.extend({
|
export const agencyCloseSchema = commandBaseSchema.extend({
|
||||||
agencyId: z.coerce.number().int().positive("请输入有效 Agency ID")
|
agencyId: z.coerce.number().int().positive("请输入有效 Agency ID"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const agencyJoinEnabledSchema = commandBaseSchema.extend({
|
export const agencyJoinEnabledSchema = commandBaseSchema.extend({
|
||||||
agencyId: z.coerce.number().int().positive("请输入有效 Agency ID"),
|
agencyId: z.coerce.number().int().positive("请输入有效 Agency ID"),
|
||||||
joinEnabled: z.boolean().default(true)
|
joinEnabled: z.boolean().default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const countryCreateSchema = z.object({
|
export const countryCreateSchema = z.object({
|
||||||
countryCode: z.string().trim().regex(/^[A-Za-z]{2,3}$/, "国家码必须是 2-3 位字母").transform((value) => value.toUpperCase()),
|
countryCode: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.regex(/^[A-Za-z]{2,3}$/, "国家码必须是 2-3 位字母")
|
||||||
|
.transform((value) => value.toUpperCase()),
|
||||||
countryDisplayName: z.string().trim().min(1, "请输入展示名称").max(80, "展示名称不能超过 80 个字符"),
|
countryDisplayName: z.string().trim().min(1, "请输入展示名称").max(80, "展示名称不能超过 80 个字符"),
|
||||||
countryName: z.string().trim().min(1, "请输入国家名称").max(80, "国家名称不能超过 80 个字符"),
|
countryName: z.string().trim().min(1, "请输入国家名称").max(80, "国家名称不能超过 80 个字符"),
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
@ -106,12 +131,12 @@ export const countryCreateSchema = z.object({
|
|||||||
isoAlpha3: optionalTextSchema(8, "ISO Alpha-3 不能超过 8 个字符").transform((value) => value.toUpperCase()),
|
isoAlpha3: optionalTextSchema(8, "ISO Alpha-3 不能超过 8 个字符").transform((value) => value.toUpperCase()),
|
||||||
isoNumeric: optionalTextSchema(8, "ISO Numeric 不能超过 8 个字符"),
|
isoNumeric: optionalTextSchema(8, "ISO Numeric 不能超过 8 个字符"),
|
||||||
phoneCountryCode: optionalTextSchema(16, "电话区号不能超过 16 个字符"),
|
phoneCountryCode: optionalTextSchema(16, "电话区号不能超过 16 个字符"),
|
||||||
sortOrder: sortOrderSchema
|
sortOrder: sortOrderSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const countryUpdateSchema = countryCreateSchema.omit({
|
export const countryUpdateSchema = countryCreateSchema.omit({
|
||||||
countryCode: true,
|
countryCode: true,
|
||||||
enabled: true
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const regionCreateSchema = regionBaseSchema;
|
export const regionCreateSchema = regionBaseSchema;
|
||||||
@ -119,7 +144,7 @@ export const regionCreateSchema = regionBaseSchema;
|
|||||||
export const regionUpdateSchema = regionBaseSchema;
|
export const regionUpdateSchema = regionBaseSchema;
|
||||||
|
|
||||||
export const regionCountriesSchema = z.object({
|
export const regionCountriesSchema = z.object({
|
||||||
countries: requiredCountryCodesSchema
|
countries: requiredCountryCodesSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateBDLeaderForm = z.infer<typeof createBDLeaderSchema>;
|
export type CreateBDLeaderForm = z.infer<typeof createBDLeaderSchema>;
|
||||||
@ -127,6 +152,7 @@ export type CreateBDForm = z.infer<typeof createBDSchema>;
|
|||||||
export type BDStatusForm = z.infer<typeof bdStatusSchema>;
|
export type BDStatusForm = z.infer<typeof bdStatusSchema>;
|
||||||
export type CreateCoinSellerForm = z.infer<typeof createCoinSellerSchema>;
|
export type CreateCoinSellerForm = z.infer<typeof createCoinSellerSchema>;
|
||||||
export type CoinSellerStatusForm = z.infer<typeof coinSellerStatusSchema>;
|
export type CoinSellerStatusForm = z.infer<typeof coinSellerStatusSchema>;
|
||||||
|
export type CoinSellerStockCreditForm = z.infer<typeof coinSellerStockCreditSchema>;
|
||||||
export type CreateAgencyForm = z.infer<typeof createAgencySchema>;
|
export type CreateAgencyForm = z.infer<typeof createAgencySchema>;
|
||||||
export type AgencyCloseForm = z.infer<typeof agencyCloseSchema>;
|
export type AgencyCloseForm = z.infer<typeof agencyCloseSchema>;
|
||||||
export type AgencyJoinEnabledForm = z.infer<typeof agencyJoinEnabledSchema>;
|
export type AgencyJoinEnabledForm = z.infer<typeof agencyJoinEnabledSchema>;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { SearchBox } from "@/shared/ui/SearchBox.jsx";
|
import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
|
|
||||||
export function LogFilters({ onQueryChange, query }) {
|
export function LogFilters({ onReset, query }) {
|
||||||
return (
|
return (
|
||||||
<section className="filters-panel">
|
<section className="filters-panel">
|
||||||
<SearchBox value={query} onChange={onQueryChange} placeholder="搜索用户、IP、动作、资源..." />
|
<AdminFilterResetButton disabled={!query} onClick={onReset} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,20 @@
|
|||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
|
import { createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
|
|
||||||
export function LogTable({ data, isLogin }) {
|
export function LogTable({ data, isLogin, onQueryChange, query }) {
|
||||||
const items = data.items || [];
|
const items = data.items || [];
|
||||||
const columns = isLogin
|
const columns = isLogin
|
||||||
? [
|
? [
|
||||||
{ key: "username", label: "用户", width: "minmax(140px, 1fr)" },
|
{
|
||||||
|
key: "username",
|
||||||
|
label: "用户",
|
||||||
|
width: "minmax(140px, 1fr)",
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索用户、IP、动作、资源",
|
||||||
|
value: query,
|
||||||
|
onChange: onQueryChange
|
||||||
|
})
|
||||||
|
},
|
||||||
{ key: "ip", label: "IP", width: "minmax(140px, 1fr)", render: (item) => <span className="muted">{item.ip || "-"}</span> },
|
{ key: "ip", label: "IP", width: "minmax(140px, 1fr)", render: (item) => <span className="muted">{item.ip || "-"}</span> },
|
||||||
{ key: "status", label: "状态", width: "110px" },
|
{ key: "status", label: "状态", width: "110px" },
|
||||||
{
|
{
|
||||||
@ -21,7 +31,16 @@ export function LogTable({ data, isLogin }) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{ key: "username", label: "用户", width: "minmax(140px, 1fr)" },
|
{
|
||||||
|
key: "username",
|
||||||
|
label: "用户",
|
||||||
|
width: "minmax(140px, 1fr)",
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索用户、IP、动作、资源",
|
||||||
|
value: query,
|
||||||
|
onChange: onQueryChange
|
||||||
|
})
|
||||||
|
},
|
||||||
{ key: "action", label: "动作", width: "minmax(120px, 1fr)" },
|
{ key: "action", label: "动作", width: "minmax(120px, 1fr)" },
|
||||||
{
|
{
|
||||||
key: "resource",
|
key: "resource",
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export function useLogsPage(type) {
|
|||||||
errorMessage: "加载日志失败",
|
errorMessage: "加载日志失败",
|
||||||
initialData: emptyData,
|
initialData: emptyData,
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
queryKey: ["logs", type, requestQuery]
|
queryKey: ["logs", type, requestQuery],
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeQuery = (value) => {
|
const changeQuery = (value) => {
|
||||||
@ -32,6 +32,11 @@ export function useLogsPage(type) {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
const downloadLogs = () => {
|
const downloadLogs = () => {
|
||||||
const exporter = isLogin ? exportLoginLogs : exportOperationLogs;
|
const exporter = isLogin ? exportLoginLogs : exportOperationLogs;
|
||||||
const filename = isLogin ? "hyapp-login-logs.csv" : "hyapp-operation-logs.csv";
|
const filename = isLogin ? "hyapp-login-logs.csv" : "hyapp-operation-logs.csv";
|
||||||
@ -49,6 +54,7 @@ export function useLogsPage(type) {
|
|||||||
page,
|
page,
|
||||||
query,
|
query,
|
||||||
reload,
|
reload,
|
||||||
setPage
|
resetFilters,
|
||||||
|
setPage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,11 +11,16 @@ export function LogsPage({ type }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LogToolbar canExport={page.abilities.canExport} isLogin={page.isLogin} onExport={page.downloadLogs} />
|
<LogToolbar canExport={page.abilities.canExport} isLogin={page.isLogin} onExport={page.downloadLogs} />
|
||||||
<LogFilters query={page.query} onQueryChange={page.changeQuery} />
|
<LogFilters query={page.query} onReset={page.resetFilters} />
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<LogTable data={page.data} isLogin={page.isLogin} />
|
<LogTable data={page.data} isLogin={page.isLogin} query={page.query} onQueryChange={page.changeQuery} />
|
||||||
<PaginationBar page={page.page} pageSize={page.data.pageSize || 10} total={page.data.total || 0} onPageChange={page.setPage} />
|
<PaginationBar
|
||||||
|
page={page.page}
|
||||||
|
pageSize={page.data.pageSize || 10}
|
||||||
|
total={page.data.total || 0}
|
||||||
|
onPageChange={page.setPage}
|
||||||
|
/>
|
||||||
</DataState>
|
</DataState>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,8 +10,9 @@ import {
|
|||||||
adminListClasses
|
adminListClasses
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
|
import { createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
|
|
||||||
export function MenuTable({ abilities, flatMenus, onEdit, onReorder, onRemove, onToggleVisible }) {
|
export function MenuTable({ abilities, flatMenus, onEdit, onQueryChange, onReorder, onRemove, onToggleVisible, query }) {
|
||||||
const [draggingMenu, setDraggingMenu] = useState(null);
|
const [draggingMenu, setDraggingMenu] = useState(null);
|
||||||
const [dragOverId, setDragOverId] = useState(null);
|
const [dragOverId, setDragOverId] = useState(null);
|
||||||
const columns = [
|
const columns = [
|
||||||
@ -25,6 +26,11 @@ export function MenuTable({ abilities, flatMenus, onEdit, onReorder, onRemove, o
|
|||||||
key: "label",
|
key: "label",
|
||||||
label: "菜单",
|
label: "菜单",
|
||||||
width: "minmax(180px, 1.2fr)",
|
width: "minmax(180px, 1.2fr)",
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索菜单、权限码",
|
||||||
|
value: query,
|
||||||
|
onChange: onQueryChange
|
||||||
|
}),
|
||||||
render: (menu) => <span style={{ paddingLeft: menu.depth * 18 }}>{menu.label}</span>
|
render: (menu) => <span style={{ paddingLeft: menu.depth * 18 }}>{menu.label}</span>
|
||||||
},
|
},
|
||||||
{ key: "path", label: "路由", width: "minmax(180px, 1.2fr)", render: (menu) => <span className="muted">{menu.path || "-"}</span> },
|
{ key: "path", label: "路由", width: "minmax(180px, 1.2fr)", render: (menu) => <span className="muted">{menu.path || "-"}</span> },
|
||||||
|
|||||||
@ -3,14 +3,24 @@ import EditOutlined from "@mui/icons-material/EditOutlined";
|
|||||||
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
import { kindLabel } from "@/features/menus/constants.js";
|
import { kindLabel } from "@/features/menus/constants.js";
|
||||||
|
import { createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
|
|
||||||
export function PermissionTable({ abilities, onEdit, onRemove, permissions }) {
|
export function PermissionTable({ abilities, onEdit, onQueryChange, onRemove, permissions, query }) {
|
||||||
if (!abilities.canLoadPermissions) {
|
if (!abilities.canLoadPermissions) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: "name", label: "权限", width: "minmax(160px, 1fr)" },
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "权限",
|
||||||
|
width: "minmax(160px, 1fr)",
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索权限、权限码",
|
||||||
|
value: query,
|
||||||
|
onChange: onQueryChange
|
||||||
|
})
|
||||||
|
},
|
||||||
{ key: "code", label: "编码", width: "minmax(180px, 1.2fr)", render: (permission) => <span className="muted">{permission.code}</span> },
|
{ key: "code", label: "编码", width: "minmax(180px, 1.2fr)", render: (permission) => <span className="muted">{permission.code}</span> },
|
||||||
{ key: "kind", label: "类型", width: "110px", render: (permission) => kindLabel(permission.kind) },
|
{ key: "kind", label: "类型", width: "110px", render: (permission) => kindLabel(permission.kind) },
|
||||||
{
|
{
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
syncPermissions,
|
syncPermissions,
|
||||||
updateMenu,
|
updateMenu,
|
||||||
updateMenuVisible,
|
updateMenuVisible,
|
||||||
updatePermission
|
updatePermission,
|
||||||
} from "@/features/menus/api";
|
} from "@/features/menus/api";
|
||||||
import { useMenuAbilities } from "@/features/menus/permissions.js";
|
import { useMenuAbilities } from "@/features/menus/permissions.js";
|
||||||
import { menuFormSchema, permissionFormSchema } from "@/features/menus/schema";
|
import { menuFormSchema, permissionFormSchema } from "@/features/menus/schema";
|
||||||
@ -27,7 +27,7 @@ const emptyMenu = {
|
|||||||
path: "",
|
path: "",
|
||||||
permissionCode: "",
|
permissionCode: "",
|
||||||
sort: 0,
|
sort: 0,
|
||||||
visible: true
|
visible: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyPermission = { code: "", description: "", kind: "button", name: "" };
|
const emptyPermission = { code: "", description: "", kind: "button", name: "" };
|
||||||
@ -55,7 +55,7 @@ export function useMenusPage() {
|
|||||||
errorMessage: "加载菜单权限失败",
|
errorMessage: "加载菜单权限失败",
|
||||||
initialData: emptyData,
|
initialData: emptyData,
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
queryKey: ["menus", abilities.canLoadPermissions]
|
queryKey: ["menus", abilities.canLoadPermissions],
|
||||||
});
|
});
|
||||||
|
|
||||||
const flatMenus = useMemo(() => flattenMenus(data.menus || []), [data.menus]);
|
const flatMenus = useMemo(() => flattenMenus(data.menus || []), [data.menus]);
|
||||||
@ -65,7 +65,11 @@ export function useMenusPage() {
|
|||||||
return flatMenus;
|
return flatMenus;
|
||||||
}
|
}
|
||||||
return flatMenus.filter((menu) => {
|
return flatMenus.filter((menu) => {
|
||||||
return [menu.label, menu.code, menu.path, menu.permissionCode].some((value) => String(value || "").toLowerCase().includes(keyword));
|
return [menu.label, menu.code, menu.path, menu.permissionCode].some((value) =>
|
||||||
|
String(value || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(keyword),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}, [flatMenus, query]);
|
}, [flatMenus, query]);
|
||||||
const filteredPermissions = useMemo(() => {
|
const filteredPermissions = useMemo(() => {
|
||||||
@ -75,7 +79,11 @@ export function useMenusPage() {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
return items.filter((permission) => {
|
return items.filter((permission) => {
|
||||||
return [permission.name, permission.code, permission.kind, permission.description].some((value) => String(value || "").toLowerCase().includes(keyword));
|
return [permission.name, permission.code, permission.kind, permission.description].some((value) =>
|
||||||
|
String(value || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(keyword),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}, [data.permissions, query]);
|
}, [data.permissions, query]);
|
||||||
|
|
||||||
@ -83,6 +91,10 @@ export function useMenusPage() {
|
|||||||
setQuery(value);
|
setQuery(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
};
|
||||||
|
|
||||||
const openCreateMenu = () => {
|
const openCreateMenu = () => {
|
||||||
setEditingMenu(null);
|
setEditingMenu(null);
|
||||||
setMenuForm(emptyMenu);
|
setMenuForm(emptyMenu);
|
||||||
@ -99,7 +111,7 @@ export function useMenusPage() {
|
|||||||
path: menu.path || "",
|
path: menu.path || "",
|
||||||
permissionCode: menu.permissionCode || "",
|
permissionCode: menu.permissionCode || "",
|
||||||
sort: menu.sort || 0,
|
sort: menu.sort || 0,
|
||||||
visible: Boolean(menu.visible)
|
visible: Boolean(menu.visible),
|
||||||
});
|
});
|
||||||
setMenuDrawerOpen(true);
|
setMenuDrawerOpen(true);
|
||||||
};
|
};
|
||||||
@ -116,7 +128,7 @@ export function useMenusPage() {
|
|||||||
code: permission.code,
|
code: permission.code,
|
||||||
description: permission.description || "",
|
description: permission.description || "",
|
||||||
kind: permission.kind,
|
kind: permission.kind,
|
||||||
name: permission.name
|
name: permission.name,
|
||||||
});
|
});
|
||||||
setPermissionDrawerOpen(true);
|
setPermissionDrawerOpen(true);
|
||||||
};
|
};
|
||||||
@ -158,7 +170,12 @@ export function useMenusPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeMenu = async (menu) => {
|
const removeMenu = async (menu) => {
|
||||||
const ok = await confirm({ confirmText: "删除", message: `${menu.label} 删除前需要先删除子菜单。`, title: "删除菜单", tone: "danger" });
|
const ok = await confirm({
|
||||||
|
confirmText: "删除",
|
||||||
|
message: `${menu.label} 删除前需要先删除子菜单。`,
|
||||||
|
title: "删除菜单",
|
||||||
|
tone: "danger",
|
||||||
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -172,7 +189,12 @@ export function useMenusPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removePermission = async (permission) => {
|
const removePermission = async (permission) => {
|
||||||
const ok = await confirm({ confirmText: "删除", message: `${permission.name} 删除前不能被角色或菜单引用。`, title: "删除权限", tone: "danger" });
|
const ok = await confirm({
|
||||||
|
confirmText: "删除",
|
||||||
|
message: `${permission.name} 删除前不能被角色或菜单引用。`,
|
||||||
|
title: "删除权限",
|
||||||
|
tone: "danger",
|
||||||
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -256,6 +278,7 @@ export function useMenusPage() {
|
|||||||
permissions: filteredPermissions,
|
permissions: filteredPermissions,
|
||||||
query,
|
query,
|
||||||
reload,
|
reload,
|
||||||
|
resetFilters,
|
||||||
removeMenu,
|
removeMenu,
|
||||||
removePermission,
|
removePermission,
|
||||||
reorderMenu,
|
reorderMenu,
|
||||||
@ -264,13 +287,10 @@ export function useMenusPage() {
|
|||||||
submitMenu,
|
submitMenu,
|
||||||
submitPermission,
|
submitPermission,
|
||||||
syncDefaultPermissions,
|
syncDefaultPermissions,
|
||||||
toggleVisible
|
toggleVisible,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenMenus(menus, depth = 0) {
|
function flattenMenus(menus, depth = 0) {
|
||||||
return menus.flatMap((menu) => [
|
return menus.flatMap((menu) => [{ ...menu, depth }, ...flattenMenus(menu.children || [], depth + 1)]);
|
||||||
{ ...menu, depth },
|
|
||||||
...flattenMenus(menu.children || [], depth + 1)
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import SyncOutlined from "@mui/icons-material/SyncOutlined";
|
|||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import {
|
import {
|
||||||
AdminActionIconButton,
|
AdminActionIconButton,
|
||||||
|
AdminFilterResetButton,
|
||||||
AdminListBody,
|
AdminListBody,
|
||||||
AdminListPage,
|
AdminListPage,
|
||||||
AdminListToolbar,
|
AdminListToolbar,
|
||||||
AdminSearchBox
|
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { MenuFormDrawer } from "@/features/menus/components/MenuFormDrawer.jsx";
|
import { MenuFormDrawer } from "@/features/menus/components/MenuFormDrawer.jsx";
|
||||||
import { MenuTable } from "@/features/menus/components/MenuTable.jsx";
|
import { MenuTable } from "@/features/menus/components/MenuTable.jsx";
|
||||||
@ -21,8 +21,10 @@ export function MenuManagementPage() {
|
|||||||
<>
|
<>
|
||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
filters={<AdminSearchBox value={page.query} onChange={page.changeQuery} placeholder="搜索菜单、权限码..." />}
|
filters={
|
||||||
actions={(
|
<AdminFilterResetButton disabled={!page.query} onClick={page.resetFilters} />
|
||||||
|
}
|
||||||
|
actions={
|
||||||
<>
|
<>
|
||||||
{page.abilities.canSyncPermission ? (
|
{page.abilities.canSyncPermission ? (
|
||||||
<AdminActionIconButton label="同步权限" onClick={page.syncDefaultPermissions}>
|
<AdminActionIconButton label="同步权限" onClick={page.syncDefaultPermissions}>
|
||||||
@ -40,7 +42,7 @@ export function MenuManagementPage() {
|
|||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
@ -49,14 +51,18 @@ export function MenuManagementPage() {
|
|||||||
<MenuTable
|
<MenuTable
|
||||||
abilities={page.abilities}
|
abilities={page.abilities}
|
||||||
flatMenus={page.flatMenus}
|
flatMenus={page.flatMenus}
|
||||||
|
query={page.query}
|
||||||
onEdit={page.openEditMenu}
|
onEdit={page.openEditMenu}
|
||||||
|
onQueryChange={page.changeQuery}
|
||||||
onReorder={page.reorderMenu}
|
onReorder={page.reorderMenu}
|
||||||
onRemove={page.removeMenu}
|
onRemove={page.removeMenu}
|
||||||
onToggleVisible={page.toggleVisible}
|
onToggleVisible={page.toggleVisible}
|
||||||
/>
|
/>
|
||||||
<PermissionTable
|
<PermissionTable
|
||||||
abilities={page.abilities}
|
abilities={page.abilities}
|
||||||
|
query={page.query}
|
||||||
onEdit={page.openEditPermission}
|
onEdit={page.openEditPermission}
|
||||||
|
onQueryChange={page.changeQuery}
|
||||||
onRemove={page.removePermission}
|
onRemove={page.removePermission}
|
||||||
permissions={page.permissions}
|
permissions={page.permissions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,28 +1,26 @@
|
|||||||
import TextField from "@mui/material/TextField";
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { listRechargeBills } from "@/features/payment/api";
|
import { listRechargeBills } from "@/features/payment/api";
|
||||||
import {
|
import {
|
||||||
AdminFilterSelect,
|
AdminFilterResetButton,
|
||||||
AdminListBody,
|
AdminListBody,
|
||||||
AdminListPage,
|
AdminListPage,
|
||||||
AdminListToolbar,
|
AdminListToolbar,
|
||||||
AdminSearchBox,
|
|
||||||
adminListClasses
|
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
|
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
["", "全部状态"],
|
["", "全部状态"],
|
||||||
["succeeded", "成功"]
|
["succeeded", "成功"],
|
||||||
];
|
];
|
||||||
const rechargeTypeOptions = [
|
const rechargeTypeOptions = [
|
||||||
["", "全部途径"],
|
["", "全部途径"],
|
||||||
["coin_seller_transfer", "币商充值"]
|
["coin_seller_transfer", "币商充值"],
|
||||||
];
|
];
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
@ -30,25 +28,30 @@ const columns = [
|
|||||||
key: "bill",
|
key: "bill",
|
||||||
label: "账单",
|
label: "账单",
|
||||||
width: "minmax(240px, 1.4fr)",
|
width: "minmax(240px, 1.4fr)",
|
||||||
render: (bill) => (
|
render: (bill) => <Stack primary={bill.transactionId} secondary={bill.commandId || bill.externalRef || "-"} />,
|
||||||
<Stack primary={bill.transactionId} secondary={bill.commandId || bill.externalRef || "-"} />
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "users",
|
key: "userId",
|
||||||
label: "用户 / 来源",
|
label: "用户 ID",
|
||||||
width: "minmax(180px, 0.9fr)",
|
width: "minmax(120px, 0.65fr)",
|
||||||
render: (bill) => (
|
render: (bill) => bill.userId || "-",
|
||||||
<Stack primary={`用户 ${bill.userId || "-"}`} secondary={bill.sellerUserId ? `币商 ${bill.sellerUserId}` : "-"} />
|
},
|
||||||
)
|
{
|
||||||
|
key: "sellerUserId",
|
||||||
|
label: "币商 ID",
|
||||||
|
width: "minmax(120px, 0.65fr)",
|
||||||
|
render: (bill) => bill.sellerUserId || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "amount",
|
key: "amount",
|
||||||
label: "金币 / 金额",
|
label: "金币 / 金额",
|
||||||
width: "minmax(150px, 0.75fr)",
|
width: "minmax(150px, 0.75fr)",
|
||||||
render: (bill) => (
|
render: (bill) => (
|
||||||
<Stack primary={formatNumber(bill.coinAmount)} secondary={formatMoney(bill.usdMinorAmount, bill.currencyCode)} />
|
<Stack
|
||||||
)
|
primary={formatNumber(bill.coinAmount)}
|
||||||
|
secondary={formatMoney(bill.usdMinorAmount, bill.currencyCode)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "policy",
|
key: "policy",
|
||||||
@ -59,28 +62,37 @@ const columns = [
|
|||||||
primary={bill.policyVersion || "-"}
|
primary={bill.policyVersion || "-"}
|
||||||
secondary={`${formatNumber(bill.exchangeCoinAmount)} / ${formatMoney(bill.exchangeUsdMinorAmount, bill.currencyCode)}`}
|
secondary={`${formatNumber(bill.exchangeCoinAmount)} / ${formatMoney(bill.exchangeUsdMinorAmount, bill.currencyCode)}`}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "region",
|
key: "region",
|
||||||
label: "区域",
|
label: "区域",
|
||||||
width: "minmax(110px, 0.55fr)",
|
width: "minmax(110px, 0.55fr)",
|
||||||
render: (bill) => <Stack primary={bill.targetRegionId ? `区域 ${bill.targetRegionId}` : "-"} secondary={bill.sellerRegionId ? `来源 ${bill.sellerRegionId}` : "-"} />
|
render: (bill) => (
|
||||||
|
<Stack
|
||||||
|
primary={bill.targetRegionId ? `区域 ${bill.targetRegionId}` : "-"}
|
||||||
|
secondary={bill.sellerRegionId ? `来源 ${bill.sellerRegionId}` : "-"}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "state",
|
key: "rechargeType",
|
||||||
label: "途径 / 状态",
|
label: "充值途径",
|
||||||
width: "minmax(150px, 0.75fr)",
|
width: "minmax(130px, 0.7fr)",
|
||||||
render: (bill) => (
|
render: (bill) => rechargeTypeLabel(bill.rechargeType),
|
||||||
<Stack primary={rechargeTypeLabel(bill.rechargeType)} secondary={<StatusBadge status={bill.status} />} />
|
},
|
||||||
)
|
{
|
||||||
|
key: "status",
|
||||||
|
label: "状态",
|
||||||
|
width: "minmax(100px, 0.55fr)",
|
||||||
|
render: (bill) => <StatusBadge status={bill.status} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "time",
|
key: "time",
|
||||||
label: "创建时间",
|
label: "创建时间",
|
||||||
width: "minmax(160px, 0.8fr)",
|
width: "minmax(160px, 0.8fr)",
|
||||||
render: (bill) => formatMillis(bill.createdAtMs)
|
render: (bill) => formatMillis(bill.createdAtMs),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function PaymentBillListPage() {
|
export function PaymentBillListPage() {
|
||||||
@ -97,9 +109,9 @@ export function PaymentBillListPage() {
|
|||||||
recharge_type: rechargeType,
|
recharge_type: rechargeType,
|
||||||
seller_user_id: sellerUserId,
|
seller_user_id: sellerUserId,
|
||||||
status,
|
status,
|
||||||
user_id: userId
|
user_id: userId,
|
||||||
}),
|
}),
|
||||||
[keyword, rechargeType, sellerUserId, status, userId]
|
[keyword, rechargeType, sellerUserId, status, userId],
|
||||||
);
|
);
|
||||||
const query = usePaginatedQuery({
|
const query = usePaginatedQuery({
|
||||||
errorMessage: "获取账单列表失败",
|
errorMessage: "获取账单列表失败",
|
||||||
@ -107,7 +119,7 @@ export function PaymentBillListPage() {
|
|||||||
filters,
|
filters,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
queryKey: ["payment-bills", filters, page]
|
queryKey: ["payment-bills", filters, page],
|
||||||
});
|
});
|
||||||
const data = query.data || { items: [], total: 0, page, pageSize };
|
const data = query.data || { items: [], total: 0, page, pageSize };
|
||||||
const items = data.items || [];
|
const items = data.items || [];
|
||||||
@ -118,35 +130,96 @@ export function PaymentBillListPage() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setKeyword("");
|
||||||
|
setStatus("");
|
||||||
|
setRechargeType("");
|
||||||
|
setUserId("");
|
||||||
|
setSellerUserId("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
const tableColumns = columns.map((column) => {
|
||||||
|
if (column.key === "bill") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索账单号、命令号、外部单号",
|
||||||
|
value: keyword,
|
||||||
|
onChange: resetPage(setKeyword),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "userId") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索用户 ID",
|
||||||
|
value: userId,
|
||||||
|
onChange: (value) => resetPage(setUserId)(digitsOnly(value)),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "sellerUserId") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索币商 ID",
|
||||||
|
value: sellerUserId,
|
||||||
|
onChange: (value) => resetPage(setSellerUserId)(digitsOnly(value)),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "rechargeType") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: rechargeTypeOptions,
|
||||||
|
placeholder: "搜索充值途径",
|
||||||
|
value: rechargeType,
|
||||||
|
onChange: resetPage(setRechargeType),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "status") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: statusOptions,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: status,
|
||||||
|
onChange: resetPage(setStatus),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
filters={
|
filters={
|
||||||
<>
|
<AdminFilterResetButton
|
||||||
<AdminSearchBox value={keyword} onChange={resetPage(setKeyword)} placeholder="搜索账单号、命令号、外部单号..." />
|
disabled={!keyword && !status && !rechargeType && !userId && !sellerUserId}
|
||||||
<TextField
|
onClick={resetFilters}
|
||||||
className={adminListClasses.statusSelect}
|
|
||||||
label="用户 ID"
|
|
||||||
size="small"
|
|
||||||
value={userId}
|
|
||||||
onChange={(event) => resetPage(setUserId)(digitsOnly(event.target.value))}
|
|
||||||
/>
|
/>
|
||||||
<TextField
|
|
||||||
className={adminListClasses.statusSelect}
|
|
||||||
label="币商 ID"
|
|
||||||
size="small"
|
|
||||||
value={sellerUserId}
|
|
||||||
onChange={(event) => resetPage(setSellerUserId)(digitsOnly(event.target.value))}
|
|
||||||
/>
|
|
||||||
<AdminFilterSelect label="充值途径" options={rechargeTypeOptions} value={rechargeType} onChange={resetPage(setRechargeType)} />
|
|
||||||
<AdminFilterSelect label="状态" options={statusOptions} value={status} onChange={resetPage(setStatus)} />
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DataState error={query.error} loading={query.loading} onRetry={query.reload}>
|
<DataState error={query.error} loading={query.loading} onRetry={query.reload}>
|
||||||
<AdminListBody>
|
<AdminListBody>
|
||||||
<DataTable columns={columns} items={items} minWidth="1180px" rowKey={(bill) => bill.transactionId} />
|
<DataTable
|
||||||
{total > 0 ? <PaginationBar page={page} pageSize={data.pageSize || pageSize} total={total} onPageChange={setPage} /> : null}
|
columns={tableColumns}
|
||||||
|
items={items}
|
||||||
|
minWidth="1260px"
|
||||||
|
rowKey={(bill) => bill.transactionId}
|
||||||
|
/>
|
||||||
|
{total > 0 ? (
|
||||||
|
<PaginationBar
|
||||||
|
page={page}
|
||||||
|
pageSize={data.pageSize || pageSize}
|
||||||
|
total={total}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</AdminListBody>
|
</AdminListBody>
|
||||||
</DataState>
|
</DataState>
|
||||||
</AdminListPage>
|
</AdminListPage>
|
||||||
@ -199,7 +272,7 @@ function formatMoney(value, currency = "USD") {
|
|||||||
}
|
}
|
||||||
return `${currency || "USD"} ${(Number(value) / 100).toLocaleString("zh-CN", {
|
return `${currency || "USD"} ${(Number(value) / 100).toLocaleString("zh-CN", {
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
minimumFractionDigits: 2
|
minimumFractionDigits: 2,
|
||||||
})}`;
|
})}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -221,6 +221,13 @@ export function useResourceListPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setStatus("");
|
||||||
|
setResourceType("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...sharedPageState({ page, query, result, setPage, setQuery }),
|
...sharedPageState({ page, query, result, setPage, setQuery }),
|
||||||
abilities,
|
abilities,
|
||||||
@ -234,6 +241,7 @@ export function useResourceListPage() {
|
|||||||
openEditResource,
|
openEditResource,
|
||||||
resourceType,
|
resourceType,
|
||||||
selectedResource,
|
selectedResource,
|
||||||
|
resetFilters,
|
||||||
setForm,
|
setForm,
|
||||||
status,
|
status,
|
||||||
submitResource,
|
submitResource,
|
||||||
@ -385,6 +393,12 @@ export function useResourceGroupListPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setStatus("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...sharedPageState({ page, query, result, setPage, setQuery }),
|
...sharedPageState({ page, query, result, setPage, setQuery }),
|
||||||
abilities,
|
abilities,
|
||||||
@ -401,6 +415,7 @@ export function useResourceGroupListPage() {
|
|||||||
resourceOptions,
|
resourceOptions,
|
||||||
resourceOptionsLoading,
|
resourceOptionsLoading,
|
||||||
selectedGroup,
|
selectedGroup,
|
||||||
|
resetFilters,
|
||||||
setForm,
|
setForm,
|
||||||
status,
|
status,
|
||||||
submitGroup,
|
submitGroup,
|
||||||
@ -534,6 +549,13 @@ export function useGiftListPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setStatus("");
|
||||||
|
setRegionId("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...sharedPageState({ page, query, result, setPage, setQuery }),
|
...sharedPageState({ page, query, result, setPage, setQuery }),
|
||||||
abilities,
|
abilities,
|
||||||
@ -551,6 +573,7 @@ export function useGiftListPage() {
|
|||||||
resourceOptions,
|
resourceOptions,
|
||||||
resourceOptionsLoading,
|
resourceOptionsLoading,
|
||||||
selectedGift,
|
selectedGift,
|
||||||
|
resetFilters,
|
||||||
setForm,
|
setForm,
|
||||||
status,
|
status,
|
||||||
submitGift,
|
submitGift,
|
||||||
@ -650,6 +673,12 @@ export function useResourceGrantListPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setStatus("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...sharedPageState({ page, query, result, setPage, setQuery }),
|
...sharedPageState({ page, query, result, setPage, setQuery }),
|
||||||
abilities,
|
abilities,
|
||||||
@ -662,6 +691,7 @@ export function useResourceGrantListPage() {
|
|||||||
openCreateGrant,
|
openCreateGrant,
|
||||||
optionsLoading,
|
optionsLoading,
|
||||||
resourceOptions,
|
resourceOptions,
|
||||||
|
resetFilters,
|
||||||
setForm,
|
setForm,
|
||||||
status,
|
status,
|
||||||
submitGrant,
|
submitGrant,
|
||||||
|
|||||||
@ -17,17 +17,17 @@ import { Button } from "@/shared/ui/Button.jsx";
|
|||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
import { MultiValueAutocomplete } from "@/shared/ui/MultiValueAutocomplete.jsx";
|
import { MultiValueAutocomplete } from "@/shared/ui/MultiValueAutocomplete.jsx";
|
||||||
import { createRegionColumnFilter, RegionFilterControl } from "@/shared/ui/RegionFilterControl.jsx";
|
|
||||||
import {
|
import {
|
||||||
AdminActionIconButton,
|
AdminActionIconButton,
|
||||||
AdminFilterSelect,
|
AdminFilterResetButton,
|
||||||
AdminListBody,
|
AdminListBody,
|
||||||
AdminListPage,
|
AdminListPage,
|
||||||
AdminRowActions,
|
AdminRowActions,
|
||||||
AdminListToolbar,
|
AdminListToolbar,
|
||||||
AdminSearchBox
|
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
|
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import { resourceStatusFilters } from "@/features/resources/constants.js";
|
import { resourceStatusFilters } from "@/features/resources/constants.js";
|
||||||
import { useGiftListPage } from "@/features/resources/hooks/useResourcePages.js";
|
import { useGiftListPage } from "@/features/resources/hooks/useResourcePages.js";
|
||||||
@ -43,16 +43,16 @@ const giftTypeOptions = [
|
|||||||
["flag", "国旗礼物"],
|
["flag", "国旗礼物"],
|
||||||
["activity", "活动礼物"],
|
["activity", "活动礼物"],
|
||||||
["magic", "魔法礼物"],
|
["magic", "魔法礼物"],
|
||||||
["custom", "定制礼物"]
|
["custom", "定制礼物"],
|
||||||
];
|
];
|
||||||
const chargeAssetOptions = [
|
const chargeAssetOptions = [
|
||||||
["COIN", "金币"],
|
["COIN", "金币"],
|
||||||
["DIAMOND", "钻石"]
|
["DIAMOND", "钻石"],
|
||||||
];
|
];
|
||||||
const effectOptions = [
|
const effectOptions = [
|
||||||
["animation", "动画"],
|
["animation", "动画"],
|
||||||
["music", "音乐"],
|
["music", "音乐"],
|
||||||
["global_broadcast", "全局广播"]
|
["global_broadcast", "全局广播"],
|
||||||
];
|
];
|
||||||
const effectLabels = Object.fromEntries(effectOptions);
|
const effectLabels = Object.fromEntries(effectOptions);
|
||||||
const weekLabels = ["一", "二", "三", "四", "五", "六", "日"];
|
const weekLabels = ["一", "二", "三", "四", "五", "六", "日"];
|
||||||
@ -62,13 +62,13 @@ const baseColumns = [
|
|||||||
key: "gift",
|
key: "gift",
|
||||||
label: "礼物",
|
label: "礼物",
|
||||||
width: "minmax(260px, 1.4fr)",
|
width: "minmax(260px, 1.4fr)",
|
||||||
render: (gift) => <GiftIdentity gift={gift} />
|
render: (gift) => <GiftIdentity gift={gift} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "resource",
|
key: "resource",
|
||||||
label: "资源",
|
label: "资源",
|
||||||
width: "minmax(220px, 1.1fr)",
|
width: "minmax(220px, 1.1fr)",
|
||||||
render: (gift) => <ResourceRef gift={gift} />
|
render: (gift) => <ResourceRef gift={gift} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "price",
|
key: "price",
|
||||||
@ -77,9 +77,11 @@ const baseColumns = [
|
|||||||
render: (gift) => (
|
render: (gift) => (
|
||||||
<div className={styles.stack}>
|
<div className={styles.stack}>
|
||||||
<span>{giftTypeLabel(gift.giftTypeCode)}</span>
|
<span>{giftTypeLabel(gift.giftTypeCode)}</span>
|
||||||
<span className={styles.meta}>{chargeAssetLabel(gift.chargeAssetType)} · {formatNumber(gift.coinPrice)}</span>
|
<span className={styles.meta}>
|
||||||
|
{chargeAssetLabel(gift.chargeAssetType)} · {formatNumber(gift.coinPrice)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "income",
|
key: "income",
|
||||||
@ -90,12 +92,12 @@ const baseColumns = [
|
|||||||
<span>{formatNumber(gift.giftPointAmount)}</span>
|
<span>{formatNumber(gift.giftPointAmount)}</span>
|
||||||
<span className={styles.meta}>{formatNumber(gift.heatValue)}</span>
|
<span className={styles.meta}>{formatNumber(gift.heatValue)}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "regions",
|
key: "regions",
|
||||||
label: "区域",
|
label: "区域",
|
||||||
width: "minmax(160px, 0.8fr)"
|
width: "minmax(160px, 0.8fr)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "effects",
|
key: "effects",
|
||||||
@ -106,24 +108,24 @@ const baseColumns = [
|
|||||||
<span>{effectiveRangeLabel(gift)}</span>
|
<span>{effectiveRangeLabel(gift)}</span>
|
||||||
<span className={styles.meta}>{effectTypesLabel(gift.effectTypes)}</span>
|
<span className={styles.meta}>{effectTypesLabel(gift.effectTypes)}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: "status",
|
||||||
label: "状态",
|
label: "状态",
|
||||||
width: "minmax(92px, 0.55fr)"
|
width: "minmax(92px, 0.55fr)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "time",
|
key: "time",
|
||||||
label: "更新时间",
|
label: "更新时间",
|
||||||
width: "minmax(170px, 0.9fr)",
|
width: "minmax(170px, 0.9fr)",
|
||||||
render: (gift) => formatMillis(gift.updatedAtMs || gift.createdAtMs)
|
render: (gift) => formatMillis(gift.updatedAtMs || gift.createdAtMs),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "actions",
|
key: "actions",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: "minmax(76px, 0.4fr)"
|
width: "minmax(76px, 0.4fr)",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function GiftListPage() {
|
export function GiftListPage() {
|
||||||
@ -141,21 +143,37 @@ export function GiftListPage() {
|
|||||||
loading: page.loadingRegions,
|
loading: page.loadingRegions,
|
||||||
options: giftRegionOptions,
|
options: giftRegionOptions,
|
||||||
value: page.regionId,
|
value: page.regionId,
|
||||||
onChange: page.changeRegionId
|
onChange: page.changeRegionId,
|
||||||
}),
|
}),
|
||||||
render: (gift) => <RegionTags regionIds={gift.regionIds} regionLabelById={regionLabelById} />
|
render: (gift) => <RegionTags regionIds={gift.regionIds} regionLabelById={regionLabelById} />,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (column.key === "status") {
|
if (column.key === "status") {
|
||||||
return {
|
return {
|
||||||
...column,
|
...column,
|
||||||
render: (gift) => <GiftStatusSwitch gift={gift} page={page} />
|
filter: createOptionsColumnFilter({
|
||||||
|
options: resourceStatusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.changeStatus,
|
||||||
|
}),
|
||||||
|
render: (gift) => <GiftStatusSwitch gift={gift} page={page} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "gift") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索礼物名称、礼物 ID",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (column.key === "actions") {
|
if (column.key === "actions") {
|
||||||
return {
|
return {
|
||||||
...column,
|
...column,
|
||||||
render: (gift) => <GiftRowActions gift={gift} page={page} />
|
render: (gift) => <GiftRowActions gift={gift} page={page} />,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return column;
|
return column;
|
||||||
@ -165,17 +183,10 @@ export function GiftListPage() {
|
|||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
filters={
|
filters={
|
||||||
<>
|
<AdminFilterResetButton
|
||||||
<AdminSearchBox value={page.query} onChange={page.changeQuery} placeholder="搜索礼物名称、礼物 ID..." />
|
disabled={!page.query && !page.status && !page.regionId}
|
||||||
<AdminFilterSelect options={resourceStatusFilters} value={page.status} onChange={page.changeStatus} />
|
onClick={page.resetFilters}
|
||||||
<RegionFilterControl
|
|
||||||
emptyLabel="全部区域"
|
|
||||||
loading={page.loadingRegions}
|
|
||||||
options={giftRegionOptions}
|
|
||||||
value={page.regionId}
|
|
||||||
onChange={page.changeRegionId}
|
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
page.abilities.canCreateGift ? (
|
page.abilities.canCreateGift ? (
|
||||||
@ -275,7 +286,9 @@ function GiftFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open
|
|||||||
onChange={(event) => page.setForm({ ...form, giftTypeCode: event.target.value })}
|
onChange={(event) => page.setForm({ ...form, giftTypeCode: event.target.value })}
|
||||||
>
|
>
|
||||||
{giftTypeOptions.map(([value, label]) => (
|
{giftTypeOptions.map(([value, label]) => (
|
||||||
<MenuItem key={value} value={value}>{label}</MenuItem>
|
<MenuItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
@ -301,7 +314,9 @@ function GiftFormDialog({ disabled, form, loading, mode, onClose, onSubmit, open
|
|||||||
onChange={(event) => page.setForm({ ...form, chargeAssetType: event.target.value })}
|
onChange={(event) => page.setForm({ ...form, chargeAssetType: event.target.value })}
|
||||||
>
|
>
|
||||||
{chargeAssetOptions.map(([value, label]) => (
|
{chargeAssetOptions.map(([value, label]) => (
|
||||||
<MenuItem key={value} value={value}>{label}</MenuItem>
|
<MenuItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
@ -396,13 +411,10 @@ function DateTimeField({ disabled, label, onChange, value }) {
|
|||||||
setViewDate((current) => new Date(current.getFullYear(), current.getMonth() + offset, 1));
|
setViewDate((current) => new Date(current.getFullYear(), current.getMonth() + offset, 1));
|
||||||
};
|
};
|
||||||
const changeDay = (day) => {
|
const changeDay = (day) => {
|
||||||
setDraftDate((current) => new Date(
|
setDraftDate(
|
||||||
day.getFullYear(),
|
(current) =>
|
||||||
day.getMonth(),
|
new Date(day.getFullYear(), day.getMonth(), day.getDate(), current.getHours(), current.getMinutes()),
|
||||||
day.getDate(),
|
);
|
||||||
current.getHours(),
|
|
||||||
current.getMinutes()
|
|
||||||
));
|
|
||||||
setViewDate(new Date(day.getFullYear(), day.getMonth(), 1));
|
setViewDate(new Date(day.getFullYear(), day.getMonth(), 1));
|
||||||
};
|
};
|
||||||
const changeTime = (unit, nextValue) => {
|
const changeTime = (unit, nextValue) => {
|
||||||
@ -428,7 +440,7 @@ function DateTimeField({ disabled, label, onChange, value }) {
|
|||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<CalendarMonthOutlined fontSize="small" />
|
<CalendarMonthOutlined fontSize="small" />
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
)
|
),
|
||||||
}}
|
}}
|
||||||
label={label}
|
label={label}
|
||||||
placeholder="长期有效"
|
placeholder="长期有效"
|
||||||
@ -454,7 +466,9 @@ function DateTimeField({ disabled, label, onChange, value }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.calendarGrid}>
|
<div className={styles.calendarGrid}>
|
||||||
{weekLabels.map((weekday) => (
|
{weekLabels.map((weekday) => (
|
||||||
<span className={styles.weekday} key={weekday}>{weekday}</span>
|
<span className={styles.weekday} key={weekday}>
|
||||||
|
{weekday}
|
||||||
|
</span>
|
||||||
))}
|
))}
|
||||||
{days.map((day) => {
|
{days.map((day) => {
|
||||||
const isOutside = day.getMonth() !== viewDate.getMonth();
|
const isOutside = day.getMonth() !== viewDate.getMonth();
|
||||||
@ -464,8 +478,10 @@ function DateTimeField({ disabled, label, onChange, value }) {
|
|||||||
className={[
|
className={[
|
||||||
styles.dayButton,
|
styles.dayButton,
|
||||||
isOutside ? styles.dayOutside : "",
|
isOutside ? styles.dayOutside : "",
|
||||||
isSelected ? styles.daySelected : ""
|
isSelected ? styles.daySelected : "",
|
||||||
].filter(Boolean).join(" ")}
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
key={day.toISOString()}
|
key={day.toISOString()}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => changeDay(day)}
|
onClick={() => changeDay(day)}
|
||||||
@ -484,7 +500,9 @@ function DateTimeField({ disabled, label, onChange, value }) {
|
|||||||
onChange={(event) => changeTime("hour", event.target.value)}
|
onChange={(event) => changeTime("hour", event.target.value)}
|
||||||
>
|
>
|
||||||
{Array.from({ length: 24 }, (_, hour) => (
|
{Array.from({ length: 24 }, (_, hour) => (
|
||||||
<MenuItem key={hour} value={String(hour)}>{twoDigits(hour)}</MenuItem>
|
<MenuItem key={hour} value={String(hour)}>
|
||||||
|
{twoDigits(hour)}
|
||||||
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
@ -495,7 +513,9 @@ function DateTimeField({ disabled, label, onChange, value }) {
|
|||||||
onChange={(event) => changeTime("minute", event.target.value)}
|
onChange={(event) => changeTime("minute", event.target.value)}
|
||||||
>
|
>
|
||||||
{Array.from({ length: 60 }, (_, minute) => (
|
{Array.from({ length: 60 }, (_, minute) => (
|
||||||
<MenuItem key={minute} value={String(minute)}>{twoDigits(minute)}</MenuItem>
|
<MenuItem key={minute} value={String(minute)}>
|
||||||
|
{twoDigits(minute)}
|
||||||
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
</div>
|
</div>
|
||||||
@ -527,7 +547,7 @@ function DateTimeField({ disabled, label, onChange, value }) {
|
|||||||
function GiftRegionSelect({ disabled, labels, onChange, options, value }) {
|
function GiftRegionSelect({ disabled, labels, onChange, options, value }) {
|
||||||
const selected = value || [];
|
const selected = value || [];
|
||||||
const mergedOptions = [...new Set([...options.map((option) => option.value), ...selected])].sort(
|
const mergedOptions = [...new Set([...options.map((option) => option.value), ...selected])].sort(
|
||||||
(left, right) => Number(left) - Number(right)
|
(left, right) => Number(left) - Number(right),
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<MultiValueAutocomplete
|
<MultiValueAutocomplete
|
||||||
@ -564,7 +584,9 @@ function RegionTags({ regionIds = [], regionLabelById }) {
|
|||||||
return (
|
return (
|
||||||
<div className="admin-tag-list">
|
<div className="admin-tag-list">
|
||||||
{items.map((regionId) => (
|
{items.map((regionId) => (
|
||||||
<span className="admin-tag" key={regionId}>{regionLabelById[String(regionId)] || regionId}</span>
|
<span className="admin-tag" key={regionId}>
|
||||||
|
{regionLabelById[String(regionId)] || regionId}
|
||||||
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -619,8 +641,9 @@ function effectTypeLabel(value) {
|
|||||||
return effectOptions.find(([optionValue]) => optionValue === value)?.[1] || value;
|
return effectOptions.find(([optionValue]) => optionValue === value)?.[1] || value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function effectTypesLabel(values = []) {
|
function effectTypesLabel(values) {
|
||||||
return values.length ? values.map(effectTypeLabel).join("、") : "无特效";
|
const list = values || [];
|
||||||
|
return list.length ? list.map(effectTypeLabel).join("、") : "无特效";
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLocalDateTime(value) {
|
function parseLocalDateTime(value) {
|
||||||
@ -663,9 +686,11 @@ function calendarDays(viewDate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sameDay(left, right) {
|
function sameDay(left, right) {
|
||||||
return left.getFullYear() === right.getFullYear()
|
return (
|
||||||
&& left.getMonth() === right.getMonth()
|
left.getFullYear() === right.getFullYear() &&
|
||||||
&& left.getDate() === right.getDate();
|
left.getMonth() === right.getMonth() &&
|
||||||
|
left.getDate() === right.getDate()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function monthTitle(date) {
|
function monthTitle(date) {
|
||||||
@ -691,7 +716,7 @@ function shortDateTime(value) {
|
|||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit"
|
minute: "2-digit",
|
||||||
}).format(new Date(Number(value)));
|
}).format(new Date(Number(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import { DataState } from "@/shared/ui/DataState.jsx";
|
|||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
import {
|
import {
|
||||||
AdminActionIconButton,
|
AdminActionIconButton,
|
||||||
AdminFilterSelect,
|
AdminFilterResetButton,
|
||||||
AdminListBody,
|
AdminListBody,
|
||||||
AdminListPage,
|
AdminListPage,
|
||||||
AdminListToolbar,
|
AdminListToolbar,
|
||||||
AdminSearchBox,
|
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import {
|
import {
|
||||||
resourceGrantStatusFilters,
|
resourceGrantStatusFilters,
|
||||||
@ -86,19 +86,36 @@ export function ResourceGrantListPage() {
|
|||||||
const page = useResourceGrantListPage();
|
const page = useResourceGrantListPage();
|
||||||
const items = page.data.items || [];
|
const items = page.data.items || [];
|
||||||
const total = page.data.total || 0;
|
const total = page.data.total || 0;
|
||||||
|
const columns = grantColumns.map((column) => {
|
||||||
|
if (column.key === "target") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索用户 ID",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (column.key === "status") {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: resourceGrantStatusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.changeStatus,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
filters={
|
filters={
|
||||||
<>
|
<AdminFilterResetButton disabled={!page.query && !page.status} onClick={page.resetFilters} />
|
||||||
<AdminSearchBox value={page.query} onChange={page.changeQuery} placeholder="搜索用户 ID..." />
|
|
||||||
<AdminFilterSelect
|
|
||||||
options={resourceGrantStatusFilters}
|
|
||||||
value={page.status}
|
|
||||||
onChange={page.changeStatus}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
page.abilities.canCreateGrant ? (
|
page.abilities.canCreateGrant ? (
|
||||||
@ -110,7 +127,12 @@ export function ResourceGrantListPage() {
|
|||||||
/>
|
/>
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<AdminListBody>
|
<AdminListBody>
|
||||||
<DataTable columns={grantColumns} items={items} minWidth="1190px" rowKey={(grant) => grant.grantId} />
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
items={items}
|
||||||
|
minWidth="1190px"
|
||||||
|
rowKey={(grant) => grant.grantId}
|
||||||
|
/>
|
||||||
{total > 0 ? (
|
{total > 0 ? (
|
||||||
<PaginationBar
|
<PaginationBar
|
||||||
page={page.page}
|
page={page.page}
|
||||||
|
|||||||
@ -22,20 +22,16 @@ import { DataState } from "@/shared/ui/DataState.jsx";
|
|||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
import {
|
import {
|
||||||
AdminActionIconButton,
|
AdminActionIconButton,
|
||||||
AdminFilterSelect,
|
AdminFilterResetButton,
|
||||||
AdminListBody,
|
AdminListBody,
|
||||||
AdminListPage,
|
AdminListPage,
|
||||||
AdminListToolbar,
|
AdminListToolbar,
|
||||||
AdminRowActions,
|
AdminRowActions,
|
||||||
AdminSearchBox,
|
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import {
|
import { resourceGroupAssetLabels, resourceStatusFilters, resourceTypeLabels } from "@/features/resources/constants.js";
|
||||||
resourceGroupAssetLabels,
|
|
||||||
resourceStatusFilters,
|
|
||||||
resourceTypeLabels,
|
|
||||||
} from "@/features/resources/constants.js";
|
|
||||||
import { useResourceGroupListPage } from "@/features/resources/hooks/useResourcePages.js";
|
import { useResourceGroupListPage } from "@/features/resources/hooks/useResourcePages.js";
|
||||||
import styles from "@/features/resources/resources.module.css";
|
import styles from "@/features/resources/resources.module.css";
|
||||||
|
|
||||||
@ -83,9 +79,24 @@ export function ResourceGroupListPage() {
|
|||||||
const items = page.data.items || [];
|
const items = page.data.items || [];
|
||||||
const total = page.data.total || 0;
|
const total = page.data.total || 0;
|
||||||
const columns = baseColumns.map((column) =>
|
const columns = baseColumns.map((column) =>
|
||||||
column.key === "status"
|
column.key === "group"
|
||||||
? {
|
? {
|
||||||
...column,
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索资源组名称、资源组编码",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: column.key === "status"
|
||||||
|
? {
|
||||||
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: resourceStatusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.changeStatus,
|
||||||
|
}),
|
||||||
render: (group) => <ResourceGroupStatusSwitch group={group} page={page} />,
|
render: (group) => <ResourceGroupStatusSwitch group={group} page={page} />,
|
||||||
}
|
}
|
||||||
: column.key === "actions"
|
: column.key === "actions"
|
||||||
@ -100,18 +111,7 @@ export function ResourceGroupListPage() {
|
|||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
filters={
|
filters={
|
||||||
<>
|
<AdminFilterResetButton disabled={!page.query && !page.status} onClick={page.resetFilters} />
|
||||||
<AdminSearchBox
|
|
||||||
value={page.query}
|
|
||||||
onChange={page.changeQuery}
|
|
||||||
placeholder="搜索资源组名称、资源组编码..."
|
|
||||||
/>
|
|
||||||
<AdminFilterSelect
|
|
||||||
options={resourceStatusFilters}
|
|
||||||
value={page.status}
|
|
||||||
onChange={page.changeStatus}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
page.abilities.canCreateGroup ? (
|
page.abilities.canCreateGroup ? (
|
||||||
@ -135,7 +135,9 @@ export function ResourceGroupListPage() {
|
|||||||
</AdminListBody>
|
</AdminListBody>
|
||||||
</DataState>
|
</DataState>
|
||||||
<ResourceGroupFormDialog
|
<ResourceGroupFormDialog
|
||||||
disabled={page.activeAction === "edit" ? !page.abilities.canUpdateGroup : !page.abilities.canCreateGroup}
|
disabled={
|
||||||
|
page.activeAction === "edit" ? !page.abilities.canUpdateGroup : !page.abilities.canCreateGroup
|
||||||
|
}
|
||||||
form={page.form}
|
form={page.form}
|
||||||
loading={
|
loading={
|
||||||
page.activeAction === "edit"
|
page.activeAction === "edit"
|
||||||
@ -230,13 +232,25 @@ function ResourceGroupFormDialog({ disabled, form, loading, mode, onClose, onSub
|
|||||||
title="组内资源"
|
title="组内资源"
|
||||||
actions={
|
actions={
|
||||||
<AdminFormInlineActions>
|
<AdminFormInlineActions>
|
||||||
<Button disabled={loading} onClick={page.addResourceItem} startIcon={<Inventory2Outlined fontSize="small" />}>
|
<Button
|
||||||
|
disabled={loading}
|
||||||
|
onClick={page.addResourceItem}
|
||||||
|
startIcon={<Inventory2Outlined fontSize="small" />}
|
||||||
|
>
|
||||||
资源
|
资源
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={loading} onClick={() => page.addWalletAssetItem("COIN")} startIcon={<MonetizationOnOutlined fontSize="small" />}>
|
<Button
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => page.addWalletAssetItem("COIN")}
|
||||||
|
startIcon={<MonetizationOnOutlined fontSize="small" />}
|
||||||
|
>
|
||||||
金币
|
金币
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={loading} onClick={() => page.addWalletAssetItem("DIAMOND")} startIcon={<DiamondOutlined fontSize="small" />}>
|
<Button
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => page.addWalletAssetItem("DIAMOND")}
|
||||||
|
startIcon={<DiamondOutlined fontSize="small" />}
|
||||||
|
>
|
||||||
钻石
|
钻石
|
||||||
</Button>
|
</Button>
|
||||||
</AdminFormInlineActions>
|
</AdminFormInlineActions>
|
||||||
|
|||||||
@ -11,14 +11,14 @@ import { DataTable } from "@/shared/ui/DataTable.jsx";
|
|||||||
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||||||
import {
|
import {
|
||||||
AdminActionIconButton,
|
AdminActionIconButton,
|
||||||
AdminFilterSelect,
|
AdminFilterResetButton,
|
||||||
AdminListBody,
|
AdminListBody,
|
||||||
AdminListPage,
|
AdminListPage,
|
||||||
AdminRowActions,
|
AdminRowActions,
|
||||||
AdminListToolbar,
|
AdminListToolbar,
|
||||||
AdminSearchBox,
|
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
import {
|
import {
|
||||||
grantStrategyLabels,
|
grantStrategyLabels,
|
||||||
@ -80,9 +80,34 @@ export function ResourceListPage() {
|
|||||||
const total = page.data.total || 0;
|
const total = page.data.total || 0;
|
||||||
const createDisabled = !page.abilities.canCreate || !page.abilities.canUpload;
|
const createDisabled = !page.abilities.canCreate || !page.abilities.canUpload;
|
||||||
const columns = baseColumns.map((column) =>
|
const columns = baseColumns.map((column) =>
|
||||||
column.key === "status"
|
column.key === "resource"
|
||||||
? {
|
? {
|
||||||
...column,
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索资源名称、资源编码",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: column.key === "type"
|
||||||
|
? {
|
||||||
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: resourceTypeFilters,
|
||||||
|
placeholder: "搜索类型",
|
||||||
|
value: page.resourceType,
|
||||||
|
onChange: page.changeResourceType,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: column.key === "status"
|
||||||
|
? {
|
||||||
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: resourceStatusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.changeStatus,
|
||||||
|
}),
|
||||||
render: (resource) => <ResourceStatusSwitch page={page} resource={resource} />,
|
render: (resource) => <ResourceStatusSwitch page={page} resource={resource} />,
|
||||||
}
|
}
|
||||||
: column.key === "actions"
|
: column.key === "actions"
|
||||||
@ -97,24 +122,10 @@ export function ResourceListPage() {
|
|||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
filters={
|
filters={
|
||||||
<>
|
<AdminFilterResetButton
|
||||||
<AdminSearchBox
|
disabled={!page.query && !page.status && !page.resourceType}
|
||||||
value={page.query}
|
onClick={page.resetFilters}
|
||||||
onChange={page.changeQuery}
|
|
||||||
placeholder="搜索资源名称、资源编码..."
|
|
||||||
/>
|
/>
|
||||||
<AdminFilterSelect
|
|
||||||
options={resourceStatusFilters}
|
|
||||||
value={page.status}
|
|
||||||
onChange={page.changeStatus}
|
|
||||||
/>
|
|
||||||
<AdminFilterSelect
|
|
||||||
label="类型"
|
|
||||||
options={resourceTypeFilters}
|
|
||||||
value={page.resourceType}
|
|
||||||
onChange={page.changeResourceType}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
page.abilities.canCreate ? (
|
page.abilities.canCreate ? (
|
||||||
|
|||||||
@ -2,10 +2,20 @@ import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
|||||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||||
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
|
import { createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
|
|
||||||
export function RoleTable({ abilities, onEdit, onRemove, roles }) {
|
export function RoleTable({ abilities, onEdit, onQueryChange, onRemove, query, roles }) {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: "name", label: "角色", width: "minmax(160px, 1fr)" },
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "角色",
|
||||||
|
width: "minmax(160px, 1fr)",
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索角色、编码",
|
||||||
|
value: query,
|
||||||
|
onChange: onQueryChange
|
||||||
|
})
|
||||||
|
},
|
||||||
{ key: "code", label: "编码", width: "minmax(180px, 1.2fr)", render: (role) => <span className="muted">{role.code}</span> },
|
{ key: "code", label: "编码", width: "minmax(180px, 1.2fr)", render: (role) => <span className="muted">{role.code}</span> },
|
||||||
{ key: "permissionCount", label: "权限数量", width: "110px", render: (role) => role.permissions?.length || 0 },
|
{ key: "permissionCount", label: "权限数量", width: "110px", render: (role) => role.permissions?.length || 0 },
|
||||||
{
|
{
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
listPermissions,
|
listPermissions,
|
||||||
listRoles,
|
listRoles,
|
||||||
replaceRolePermissions,
|
replaceRolePermissions,
|
||||||
updateRole
|
updateRole,
|
||||||
} from "@/features/roles/api";
|
} from "@/features/roles/api";
|
||||||
import { useRoleAbilities } from "@/features/roles/permissions.js";
|
import { useRoleAbilities } from "@/features/roles/permissions.js";
|
||||||
import { roleFormSchema } from "@/features/roles/schema";
|
import { roleFormSchema } from "@/features/roles/schema";
|
||||||
@ -31,7 +31,7 @@ const permissionGroupDefinitions = [
|
|||||||
{ key: "logs", prefixes: ["log"], title: "日志审计" },
|
{ key: "logs", prefixes: ["log"], title: "日志审计" },
|
||||||
{ key: "notifications", prefixes: ["notification"], title: "通知中心" },
|
{ key: "notifications", prefixes: ["notification"], title: "通知中心" },
|
||||||
{ key: "jobs", prefixes: ["job", "export"], title: "任务中心" },
|
{ key: "jobs", prefixes: ["job", "export"], title: "任务中心" },
|
||||||
{ key: "common", prefixes: ["upload"], title: "通用能力" }
|
{ key: "common", prefixes: ["upload"], title: "通用能力" },
|
||||||
];
|
];
|
||||||
const fallbackPermissionGroup = { key: "other", title: "其他权限" };
|
const fallbackPermissionGroup = { key: "other", title: "其他权限" };
|
||||||
const permissionKindOrder = { menu: 1, button: 2, api: 3 };
|
const permissionKindOrder = { menu: 1, button: 2, api: 3 };
|
||||||
@ -55,7 +55,7 @@ export function useRolesPage() {
|
|||||||
errorMessage: "加载角色失败",
|
errorMessage: "加载角色失败",
|
||||||
initialData: emptyData,
|
initialData: emptyData,
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
queryKey: ["roles", abilities.canLoadPermissions]
|
queryKey: ["roles", abilities.canLoadPermissions],
|
||||||
});
|
});
|
||||||
|
|
||||||
const permissionGroups = useMemo(() => {
|
const permissionGroups = useMemo(() => {
|
||||||
@ -69,7 +69,11 @@ export function useRolesPage() {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
return items.filter((role) => {
|
return items.filter((role) => {
|
||||||
return [role.name, role.code, role.description].some((value) => String(value || "").toLowerCase().includes(keyword));
|
return [role.name, role.code, role.description].some((value) =>
|
||||||
|
String(value || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(keyword),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}, [data.roles, query]);
|
}, [data.roles, query]);
|
||||||
|
|
||||||
@ -77,6 +81,10 @@ export function useRolesPage() {
|
|||||||
setQuery(value);
|
setQuery(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
};
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditingRole(null);
|
setEditingRole(null);
|
||||||
setForm(emptyRole);
|
setForm(emptyRole);
|
||||||
@ -89,7 +97,7 @@ export function useRolesPage() {
|
|||||||
code: role.code,
|
code: role.code,
|
||||||
description: role.description || "",
|
description: role.description || "",
|
||||||
name: role.name,
|
name: role.name,
|
||||||
permissionIds: role.permissions?.map((permission) => permission.id) || []
|
permissionIds: role.permissions?.map((permission) => permission.id) || [],
|
||||||
});
|
});
|
||||||
setDrawerOpen(true);
|
setDrawerOpen(true);
|
||||||
};
|
};
|
||||||
@ -107,7 +115,7 @@ export function useRolesPage() {
|
|||||||
await updateRole(editingRole.id, {
|
await updateRole(editingRole.id, {
|
||||||
code: payload.code,
|
code: payload.code,
|
||||||
description: payload.description,
|
description: payload.description,
|
||||||
name: payload.name
|
name: payload.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (abilities.canGrant) {
|
if (abilities.canGrant) {
|
||||||
@ -119,7 +127,7 @@ export function useRolesPage() {
|
|||||||
code: payload.code,
|
code: payload.code,
|
||||||
description: payload.description,
|
description: payload.description,
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
permissionIds: abilities.canGrant ? payload.permissionIds : []
|
permissionIds: abilities.canGrant ? payload.permissionIds : [],
|
||||||
});
|
});
|
||||||
showToast("角色已创建", "success");
|
showToast("角色已创建", "success");
|
||||||
}
|
}
|
||||||
@ -135,7 +143,7 @@ export function useRolesPage() {
|
|||||||
confirmText: "删除",
|
confirmText: "删除",
|
||||||
message: `${role.name} 删除前需要先解绑用户。`,
|
message: `${role.name} 删除前需要先解绑用户。`,
|
||||||
title: "删除角色",
|
title: "删除角色",
|
||||||
tone: "danger"
|
tone: "danger",
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
@ -157,7 +165,7 @@ export function useRolesPage() {
|
|||||||
...current,
|
...current,
|
||||||
permissionIds: checked
|
permissionIds: checked
|
||||||
? Array.from(new Set([...current.permissionIds, permissionId]))
|
? Array.from(new Set([...current.permissionIds, permissionId]))
|
||||||
: current.permissionIds.filter((id) => id !== permissionId)
|
: current.permissionIds.filter((id) => id !== permissionId),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -172,7 +180,7 @@ export function useRolesPage() {
|
|||||||
...current,
|
...current,
|
||||||
permissionIds: checked
|
permissionIds: checked
|
||||||
? Array.from(new Set([...current.permissionIds, ...permissionIds]))
|
? Array.from(new Set([...current.permissionIds, ...permissionIds]))
|
||||||
: current.permissionIds.filter((id) => !targetIds.has(id))
|
: current.permissionIds.filter((id) => !targetIds.has(id)),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -193,18 +201,23 @@ export function useRolesPage() {
|
|||||||
query,
|
query,
|
||||||
reload,
|
reload,
|
||||||
remove,
|
remove,
|
||||||
|
resetFilters,
|
||||||
roles,
|
roles,
|
||||||
setForm,
|
setForm,
|
||||||
submit,
|
submit,
|
||||||
togglePermission,
|
togglePermission,
|
||||||
togglePermissionGroup
|
togglePermissionGroup,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupPermissionsByMenu(permissions) {
|
function groupPermissionsByMenu(permissions) {
|
||||||
const groups = new Map(permissionGroupDefinitions.map((definition) => [definition.key, { ...definition, items: [] }]));
|
const groups = new Map(
|
||||||
|
permissionGroupDefinitions.map((definition) => [definition.key, { ...definition, items: [] }]),
|
||||||
|
);
|
||||||
const prefixToGroup = new Map(
|
const prefixToGroup = new Map(
|
||||||
permissionGroupDefinitions.flatMap((definition) => definition.prefixes.map((prefix) => [prefix, definition.key]))
|
permissionGroupDefinitions.flatMap((definition) =>
|
||||||
|
definition.prefixes.map((prefix) => [prefix, definition.key]),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const fallbackGroup = { ...fallbackPermissionGroup, items: [] };
|
const fallbackGroup = { ...fallbackPermissionGroup, items: [] };
|
||||||
|
|
||||||
@ -219,7 +232,7 @@ function groupPermissionsByMenu(permissions) {
|
|||||||
.filter((group) => group.items.length > 0)
|
.filter((group) => group.items.length > 0)
|
||||||
.map((group) => ({
|
.map((group) => ({
|
||||||
...group,
|
...group,
|
||||||
items: [...group.items].sort(comparePermissions)
|
items: [...group.items].sort(comparePermissions),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import Add from "@mui/icons-material/Add";
|
|||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import {
|
import {
|
||||||
AdminActionIconButton,
|
AdminActionIconButton,
|
||||||
|
AdminFilterResetButton,
|
||||||
AdminListBody,
|
AdminListBody,
|
||||||
AdminListPage,
|
AdminListPage,
|
||||||
AdminListToolbar,
|
AdminListToolbar,
|
||||||
AdminSearchBox
|
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { RoleFormDrawer } from "@/features/roles/components/RoleFormDrawer.jsx";
|
import { RoleFormDrawer } from "@/features/roles/components/RoleFormDrawer.jsx";
|
||||||
import { RoleTable } from "@/features/roles/components/RoleTable.jsx";
|
import { RoleTable } from "@/features/roles/components/RoleTable.jsx";
|
||||||
@ -18,17 +18,28 @@ export function RoleManagementPage() {
|
|||||||
<>
|
<>
|
||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
filters={<AdminSearchBox value={page.query} onChange={page.changeQuery} placeholder="搜索角色、编码..." />}
|
filters={
|
||||||
actions={page.abilities.canCreate ? (
|
<AdminFilterResetButton disabled={!page.query} onClick={page.resetFilters} />
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
page.abilities.canCreate ? (
|
||||||
<AdminActionIconButton label="新增角色" primary onClick={page.openCreate}>
|
<AdminActionIconButton label="新增角色" primary onClick={page.openCreate}>
|
||||||
<Add fontSize="small" />
|
<Add fontSize="small" />
|
||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
) : null}
|
) : null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<AdminListBody>
|
<AdminListBody>
|
||||||
<RoleTable abilities={page.abilities} onEdit={page.openEdit} onRemove={page.remove} roles={page.roles} />
|
<RoleTable
|
||||||
|
abilities={page.abilities}
|
||||||
|
query={page.query}
|
||||||
|
onEdit={page.openEdit}
|
||||||
|
onQueryChange={page.changeQuery}
|
||||||
|
onRemove={page.remove}
|
||||||
|
roles={page.roles}
|
||||||
|
/>
|
||||||
<div className="pagination-bar">
|
<div className="pagination-bar">
|
||||||
<span>{page.roles.length} 条</span>
|
<span>{page.roles.length} 条</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
120
src/features/rooms/components/RoomDetailDrawer.jsx
Normal file
120
src/features/rooms/components/RoomDetailDrawer.jsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import MeetingRoomOutlined from "@mui/icons-material/MeetingRoomOutlined";
|
||||||
|
import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
|
||||||
|
import { SideDrawer } from "@/shared/ui/SideDrawer.jsx";
|
||||||
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
|
import { roomStatusLabels } from "@/features/rooms/constants.js";
|
||||||
|
import styles from "@/features/rooms/rooms.module.css";
|
||||||
|
|
||||||
|
export function RoomDetailDrawer({ onClose, open, room }) {
|
||||||
|
const owner = room?.owner || {};
|
||||||
|
const normalizedStatus = room?.status === "active" ? "active" : "closed";
|
||||||
|
const ownerDisplayId = owner.displayUserId || room?.ownerUserId || "-";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SideDrawer open={open} title="房间详情" width="wide" onClose={onClose}>
|
||||||
|
{room ? (
|
||||||
|
<>
|
||||||
|
<header className={styles.detailHero}>
|
||||||
|
<span className={styles.detailCover}>
|
||||||
|
{room.coverUrl ? <img src={room.coverUrl} alt="" /> : <MeetingRoomOutlined fontSize="medium" />}
|
||||||
|
</span>
|
||||||
|
<div className={styles.detailHeroText}>
|
||||||
|
<div className={styles.detailTitleRow}>
|
||||||
|
<h3>{room.title || "-"}</h3>
|
||||||
|
<StatusBadge status={normalizedStatus} />
|
||||||
|
</div>
|
||||||
|
<span className={styles.detailMeta}>房间 ID {room.roomId}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<DetailSection
|
||||||
|
items={[
|
||||||
|
["房间名称", room.title],
|
||||||
|
["房间 ID", room.roomId],
|
||||||
|
["房间状态", roomStatusLabels[normalizedStatus]],
|
||||||
|
["房间模式", room.mode],
|
||||||
|
["区域", room.regionName || formatRegion(room.visibleRegionId)],
|
||||||
|
["可见区域 ID", room.visibleRegionId],
|
||||||
|
["创建时间", formatMillis(room.createdAtMs)],
|
||||||
|
["更新时间", formatMillis(room.updatedAtMs)]
|
||||||
|
]}
|
||||||
|
title="基础信息"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section className={styles.detailSection}>
|
||||||
|
<h3 className={styles.detailSectionTitle}>房主信息</h3>
|
||||||
|
<div className={styles.detailOwner}>
|
||||||
|
<span className={styles.detailOwnerAvatar}>
|
||||||
|
{owner.avatar ? <img src={owner.avatar} alt="" /> : <PersonOutlineOutlined fontSize="small" />}
|
||||||
|
</span>
|
||||||
|
<div className={styles.detailOwnerText}>
|
||||||
|
<strong>{owner.username || "-"}</strong>
|
||||||
|
<span>{ownerDisplayId}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailGrid}>
|
||||||
|
<DetailItem label="房主用户 ID" value={room.ownerUserId || owner.userId} />
|
||||||
|
<DetailItem label="主持用户 ID" value={room.hostUserId} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<DetailSection
|
||||||
|
items={[
|
||||||
|
["在线人数", `${formatNumber(room.onlineCount)} 人`],
|
||||||
|
["座位数", `${formatNumber(room.seatCount)} 个`],
|
||||||
|
["已占座位", `${formatNumber(room.occupiedSeatCount)} 个`],
|
||||||
|
["热度", formatNumber(room.heat)],
|
||||||
|
["排序分", formatNumber(room.sortScore)]
|
||||||
|
]}
|
||||||
|
title="房间数据"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</SideDrawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailSection({ items, title }) {
|
||||||
|
return (
|
||||||
|
<section className={styles.detailSection}>
|
||||||
|
<h3 className={styles.detailSectionTitle}>{title}</h3>
|
||||||
|
<div className={styles.detailGrid}>
|
||||||
|
{items.map(([label, value]) => (
|
||||||
|
<DetailItem key={label} label={label} value={value} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailItem({ label, value }) {
|
||||||
|
return (
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<span className={styles.detailLabel}>{label}</span>
|
||||||
|
<strong className={styles.detailValue}>{displayValue(value)}</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge({ status }) {
|
||||||
|
const tone = status === "active" ? "running" : "stopped";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`status-badge status-badge--${tone}`}>
|
||||||
|
<span className="status-point" />
|
||||||
|
{roomStatusLabels[status]}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayValue(value) {
|
||||||
|
return value === 0 || value ? value : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(value) {
|
||||||
|
return Number(value || 0).toLocaleString("zh-CN");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRegion(value) {
|
||||||
|
return value ? `区域 ${value}` : "-";
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ const emptyForm = () => ({
|
|||||||
coverUrl: "",
|
coverUrl: "",
|
||||||
description: "",
|
description: "",
|
||||||
title: "",
|
title: "",
|
||||||
visibleRegionId: 0
|
visibleRegionId: 0,
|
||||||
});
|
});
|
||||||
const emptyStatusForm = () => ({ status: "active" });
|
const emptyStatusForm = () => ({ status: "active" });
|
||||||
|
|
||||||
@ -36,17 +36,22 @@ export function useRoomsPage() {
|
|||||||
() => ({
|
() => ({
|
||||||
keyword: query,
|
keyword: query,
|
||||||
region_id: regionId,
|
region_id: regionId,
|
||||||
status
|
status,
|
||||||
}),
|
}),
|
||||||
[query, regionId, status]
|
[query, regionId, status],
|
||||||
);
|
);
|
||||||
const { data = emptyData, error, loading, reload } = usePaginatedQuery({
|
const {
|
||||||
|
data = emptyData,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
reload,
|
||||||
|
} = usePaginatedQuery({
|
||||||
errorMessage: "加载房间列表失败",
|
errorMessage: "加载房间列表失败",
|
||||||
fetcher: listRooms,
|
fetcher: listRooms,
|
||||||
filters,
|
filters,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
queryKey: ["rooms", filters, page]
|
queryKey: ["rooms", filters, page],
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeQuery = (value) => {
|
const changeQuery = (value) => {
|
||||||
@ -64,17 +69,29 @@ export function useRoomsPage() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setStatus("");
|
||||||
|
setRegionId("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
const openEdit = (room) => {
|
const openEdit = (room) => {
|
||||||
setActiveRoom(room);
|
setActiveRoom(room);
|
||||||
setForm({
|
setForm({
|
||||||
coverUrl: room.coverUrl || "",
|
coverUrl: room.coverUrl || "",
|
||||||
description: "",
|
description: "",
|
||||||
title: room.title || "",
|
title: room.title || "",
|
||||||
visibleRegionId: room.visibleRegionId || 0
|
visibleRegionId: room.visibleRegionId || 0,
|
||||||
});
|
});
|
||||||
setActiveAction("edit");
|
setActiveAction("edit");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openDetail = (room) => {
|
||||||
|
setActiveRoom(room);
|
||||||
|
setActiveAction("detail");
|
||||||
|
};
|
||||||
|
|
||||||
const openStatus = (room) => {
|
const openStatus = (room) => {
|
||||||
setActiveRoom(room);
|
setActiveRoom(room);
|
||||||
setStatusForm({ status: room.status === "closed" ? "closed" : "active" });
|
setStatusForm({ status: room.status === "closed" ? "closed" : "active" });
|
||||||
@ -120,7 +137,7 @@ export function useRoomsPage() {
|
|||||||
confirmText: "删除",
|
confirmText: "删除",
|
||||||
message: `${room.title || room.roomId} 会从房间列表和房间状态中删除。`,
|
message: `${room.title || room.roomId} 会从房间列表和房间状态中删除。`,
|
||||||
title: "删除房间",
|
title: "删除房间",
|
||||||
tone: "danger"
|
tone: "danger",
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
@ -157,6 +174,7 @@ export function useRoomsPage() {
|
|||||||
loading,
|
loading,
|
||||||
loadingAction,
|
loadingAction,
|
||||||
loadingRegions,
|
loadingRegions,
|
||||||
|
openDetail,
|
||||||
openEdit,
|
openEdit,
|
||||||
openStatus,
|
openStatus,
|
||||||
page,
|
page,
|
||||||
@ -165,12 +183,13 @@ export function useRoomsPage() {
|
|||||||
regionOptions,
|
regionOptions,
|
||||||
reload,
|
reload,
|
||||||
removeRoom,
|
removeRoom,
|
||||||
|
resetFilters,
|
||||||
setForm,
|
setForm,
|
||||||
setPage,
|
setPage,
|
||||||
setStatusForm,
|
setStatusForm,
|
||||||
status,
|
status,
|
||||||
statusForm,
|
statusForm,
|
||||||
submitStatus,
|
submitStatus,
|
||||||
submitEdit
|
submitEdit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,12 +10,13 @@ import { DataState } from "@/shared/ui/DataState.jsx";
|
|||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
import { createRegionColumnFilter, RegionFilterControl } from "@/shared/ui/RegionFilterControl.jsx";
|
import { AdminFilterResetButton } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
|
import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
|
||||||
import { SearchBox } from "@/shared/ui/SearchBox.jsx";
|
import { createRegionColumnFilter } from "@/shared/ui/RegionFilterControl.jsx";
|
||||||
import { StatusSelect } from "@/shared/ui/StatusSelect.jsx";
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
|
import { RoomDetailDrawer } from "@/features/rooms/components/RoomDetailDrawer.jsx";
|
||||||
import { roomStatusFilters, roomStatusLabels } from "@/features/rooms/constants.js";
|
import { roomStatusFilters, roomStatusLabels } from "@/features/rooms/constants.js";
|
||||||
import { useRoomsPage } from "@/features/rooms/hooks/useRoomsPage.js";
|
import { useRoomsPage } from "@/features/rooms/hooks/useRoomsPage.js";
|
||||||
import styles from "@/features/rooms/rooms.module.css";
|
import styles from "@/features/rooms/rooms.module.css";
|
||||||
@ -25,38 +26,38 @@ const columns = [
|
|||||||
key: "identity",
|
key: "identity",
|
||||||
label: "房间",
|
label: "房间",
|
||||||
width: "minmax(240px, 1.5fr)",
|
width: "minmax(240px, 1.5fr)",
|
||||||
render: (room) => <RoomIdentity room={room} />
|
render: (room) => <RoomIdentity room={room} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "owner",
|
key: "owner",
|
||||||
label: "房主",
|
label: "房主",
|
||||||
width: "minmax(210px, 1.2fr)",
|
width: "minmax(210px, 1.2fr)",
|
||||||
render: (room) => <OwnerIdentity room={room} />
|
render: (room) => <OwnerIdentity room={room} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "region",
|
key: "region",
|
||||||
label: "区域",
|
label: "区域",
|
||||||
width: "minmax(140px, 0.8fr)",
|
width: "minmax(140px, 0.8fr)",
|
||||||
render: (room) => room.regionName || (room.visibleRegionId ? `区域 ${room.visibleRegionId}` : "-")
|
render: (room) => room.regionName || (room.visibleRegionId ? `区域 ${room.visibleRegionId}` : "-"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "onlineCount",
|
key: "onlineCount",
|
||||||
label: "房间人数",
|
label: "房间人数",
|
||||||
width: "minmax(110px, 0.7fr)",
|
width: "minmax(110px, 0.7fr)",
|
||||||
render: (room) => `${formatNumber(room.onlineCount)} 人`
|
render: (room) => `${formatNumber(room.onlineCount)} 人`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: "status",
|
||||||
label: "状态",
|
label: "状态",
|
||||||
width: "minmax(100px, 0.7fr)",
|
width: "minmax(100px, 0.7fr)",
|
||||||
render: (room) => <RoomStatus status={room.status} />
|
render: (room) => <RoomStatus status={room.status} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "time",
|
key: "time",
|
||||||
label: "创建时间",
|
label: "创建时间",
|
||||||
width: "minmax(170px, 1fr)",
|
width: "minmax(170px, 1fr)",
|
||||||
render: (room) => formatMillis(room.createdAtMs)
|
render: (room) => formatMillis(room.createdAtMs),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function RoomListPage() {
|
export function RoomListPage() {
|
||||||
@ -72,24 +73,44 @@ export function RoomListPage() {
|
|||||||
loading: page.loadingRegions,
|
loading: page.loadingRegions,
|
||||||
options: page.regionOptions,
|
options: page.regionOptions,
|
||||||
value: page.regionId,
|
value: page.regionId,
|
||||||
onChange: page.changeRegionId
|
onChange: page.changeRegionId,
|
||||||
})
|
}),
|
||||||
|
}
|
||||||
|
: column.key === "identity"
|
||||||
|
? {
|
||||||
|
...column,
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索房间、房主",
|
||||||
|
value: page.query,
|
||||||
|
onChange: page.changeQuery,
|
||||||
|
}),
|
||||||
|
render: (room) => <RoomIdentity room={room} onClick={() => page.openDetail(room)} />,
|
||||||
}
|
}
|
||||||
: column.key === "status"
|
: column.key === "status"
|
||||||
? {
|
? {
|
||||||
...column,
|
...column,
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: roomStatusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: page.status,
|
||||||
|
onChange: page.changeStatus,
|
||||||
|
}),
|
||||||
render: (room) => (
|
render: (room) => (
|
||||||
<RoomStatus disabled={!page.abilities.canUpdate} status={room.status} onClick={() => page.openStatus(room)} />
|
<RoomStatus
|
||||||
)
|
disabled={!page.abilities.canUpdate}
|
||||||
|
status={room.status}
|
||||||
|
onClick={() => page.openStatus(room)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}
|
}
|
||||||
: column
|
: column,
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
key: "actions",
|
key: "actions",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: "minmax(104px, 0.6fr)",
|
width: "minmax(104px, 0.6fr)",
|
||||||
render: (room) => <RoomActions page={page} room={room} />
|
render: (room) => <RoomActions page={page} room={room} />,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -97,32 +118,35 @@ export function RoomListPage() {
|
|||||||
<div className={styles.contentPanel}>
|
<div className={styles.contentPanel}>
|
||||||
<div className={styles.toolbar}>
|
<div className={styles.toolbar}>
|
||||||
<section className={styles.filters}>
|
<section className={styles.filters}>
|
||||||
<SearchBox className={styles.search} value={page.query} onChange={page.changeQuery} placeholder="搜索房间、房主..." />
|
<AdminFilterResetButton
|
||||||
<StatusSelect
|
disabled={!page.query && !page.status && !page.regionId}
|
||||||
className={styles.statusSelect}
|
onClick={page.resetFilters}
|
||||||
options={roomStatusFilters}
|
|
||||||
value={page.status}
|
|
||||||
onChange={page.changeStatus}
|
|
||||||
/>
|
|
||||||
<RegionFilterControl
|
|
||||||
className={styles.regionInput}
|
|
||||||
emptyLabel="全部区域"
|
|
||||||
loading={page.loadingRegions}
|
|
||||||
options={page.regionOptions}
|
|
||||||
value={page.regionId}
|
|
||||||
onChange={page.changeRegionId}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
<div className={styles.listBlock}>
|
<div className={styles.listBlock}>
|
||||||
<DataTable columns={tableColumns} items={items} minWidth="1180px" rowKey={(room) => room.roomId} />
|
<DataTable
|
||||||
{total > 0 ? <PaginationBar page={page.page} pageSize={page.data.pageSize || 10} total={total} onPageChange={page.setPage} /> : null}
|
columns={tableColumns}
|
||||||
|
items={items}
|
||||||
|
minWidth="1180px"
|
||||||
|
rowKey={(room) => room.roomId}
|
||||||
|
/>
|
||||||
|
{total > 0 ? (
|
||||||
|
<PaginationBar
|
||||||
|
page={page.page}
|
||||||
|
pageSize={page.data.pageSize || 10}
|
||||||
|
total={total}
|
||||||
|
onPageChange={page.setPage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</DataState>
|
</DataState>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<RoomDetailDrawer open={page.activeAction === "detail"} room={page.activeRoom} onClose={page.closeAction} />
|
||||||
|
|
||||||
<ActionModal
|
<ActionModal
|
||||||
disabled={!page.abilities.canUpdate}
|
disabled={!page.abilities.canUpdate}
|
||||||
loading={page.loadingAction === "edit"}
|
loading={page.loadingAction === "edit"}
|
||||||
@ -131,10 +155,36 @@ export function RoomListPage() {
|
|||||||
onClose={page.closeAction}
|
onClose={page.closeAction}
|
||||||
onSubmit={page.submitEdit}
|
onSubmit={page.submitEdit}
|
||||||
>
|
>
|
||||||
<TextField disabled={!page.abilities.canUpdate} label="房间名称" required value={page.form.title} onChange={(event) => page.setForm({ ...page.form, title: event.target.value })} />
|
<TextField
|
||||||
<UploadField disabled={!page.abilities.canUpdate || !page.abilities.canUpload} label="房间头像" value={page.form.coverUrl} onChange={(coverUrl) => page.setForm({ ...page.form, coverUrl })} />
|
disabled={!page.abilities.canUpdate}
|
||||||
<RegionSelect disabled={!page.abilities.canUpdate} emptyLabel="选择区域" loading={page.loadingRegions} options={page.regionOptions} required value={page.form.visibleRegionId} onChange={(visibleRegionId) => page.setForm({ ...page.form, visibleRegionId })} />
|
label="房间名称"
|
||||||
<TextField disabled={!page.abilities.canUpdate} label="房间简介" multiline minRows={2} value={page.form.description} onChange={(event) => page.setForm({ ...page.form, description: event.target.value })} />
|
required
|
||||||
|
value={page.form.title}
|
||||||
|
onChange={(event) => page.setForm({ ...page.form, title: event.target.value })}
|
||||||
|
/>
|
||||||
|
<UploadField
|
||||||
|
disabled={!page.abilities.canUpdate || !page.abilities.canUpload}
|
||||||
|
label="房间头像"
|
||||||
|
value={page.form.coverUrl}
|
||||||
|
onChange={(coverUrl) => page.setForm({ ...page.form, coverUrl })}
|
||||||
|
/>
|
||||||
|
<RegionSelect
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
emptyLabel="选择区域"
|
||||||
|
loading={page.loadingRegions}
|
||||||
|
options={page.regionOptions}
|
||||||
|
required
|
||||||
|
value={page.form.visibleRegionId}
|
||||||
|
onChange={(visibleRegionId) => page.setForm({ ...page.form, visibleRegionId })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="房间简介"
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
value={page.form.description}
|
||||||
|
onChange={(event) => page.setForm({ ...page.form, description: event.target.value })}
|
||||||
|
/>
|
||||||
</ActionModal>
|
</ActionModal>
|
||||||
|
|
||||||
<ActionModal
|
<ActionModal
|
||||||
@ -145,7 +195,14 @@ export function RoomListPage() {
|
|||||||
onClose={page.closeAction}
|
onClose={page.closeAction}
|
||||||
onSubmit={page.submitStatus}
|
onSubmit={page.submitStatus}
|
||||||
>
|
>
|
||||||
<TextField disabled={!page.abilities.canUpdate} label="状态" required select value={page.statusForm.status} onChange={(event) => page.setStatusForm({ status: event.target.value })}>
|
<TextField
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="状态"
|
||||||
|
required
|
||||||
|
select
|
||||||
|
value={page.statusForm.status}
|
||||||
|
onChange={(event) => page.setStatusForm({ status: event.target.value })}
|
||||||
|
>
|
||||||
<MenuItem value="active">正常</MenuItem>
|
<MenuItem value="active">正常</MenuItem>
|
||||||
<MenuItem value="closed">关闭</MenuItem>
|
<MenuItem value="closed">关闭</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
@ -154,8 +211,8 @@ export function RoomListPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RoomIdentity({ room }) {
|
function RoomIdentity({ onClick, room }) {
|
||||||
return (
|
const content = (
|
||||||
<div className={styles.identity}>
|
<div className={styles.identity}>
|
||||||
<span className={styles.cover}>
|
<span className={styles.cover}>
|
||||||
{room.coverUrl ? <img src={room.coverUrl} alt="" /> : <MeetingRoomOutlined fontSize="small" />}
|
{room.coverUrl ? <img src={room.coverUrl} alt="" /> : <MeetingRoomOutlined fontSize="small" />}
|
||||||
@ -166,6 +223,21 @@ function RoomIdentity({ room }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!onClick) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-label={`查看房间详情:${room.title || room.roomId}`}
|
||||||
|
className={styles.identityButton}
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OwnerIdentity({ room }) {
|
function OwnerIdentity({ room }) {
|
||||||
@ -201,7 +273,12 @@ function RoomActions({ page, room }) {
|
|||||||
{page.abilities.canDelete ? (
|
{page.abilities.canDelete ? (
|
||||||
<Tooltip arrow title="删除">
|
<Tooltip arrow title="删除">
|
||||||
<span>
|
<span>
|
||||||
<IconButton disabled={page.loadingAction === `delete-${room.roomId}`} label="删除" tone="danger" onClick={() => page.removeRoom(room)}>
|
<IconButton
|
||||||
|
disabled={page.loadingAction === `delete-${room.roomId}`}
|
||||||
|
label="删除"
|
||||||
|
tone="danger"
|
||||||
|
onClick={() => page.removeRoom(room)}
|
||||||
|
>
|
||||||
<DeleteOutlineOutlined fontSize="small" />
|
<DeleteOutlineOutlined fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -78,6 +78,34 @@
|
|||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.identityButton {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 2px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background var(--motion-base) var(--ease-standard),
|
||||||
|
box-shadow var(--motion-base) var(--ease-standard),
|
||||||
|
transform var(--motion-base) var(--ease-emphasized);
|
||||||
|
}
|
||||||
|
|
||||||
|
.identityButton:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.identityButton:hover .name {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.identityButton:focus-visible {
|
||||||
|
outline: 2px solid var(--primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.cover {
|
.cover {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
@ -170,6 +198,156 @@
|
|||||||
border-color: var(--primary-border);
|
border-color: var(--primary-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detailHero {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding-bottom: var(--space-4);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailCover {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
|
background: var(--bg-card-strong);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailCover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailHeroText {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailTitleRow {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailTitleRow h3 {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailMeta {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: var(--admin-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailSection {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-4);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
|
background: var(--bg-card-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailSectionTitle {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--admin-font-size);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailItem {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailLabel {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailValue {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--admin-font-size);
|
||||||
|
font-weight: 650;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailOwner {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailOwnerAvatar {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailOwnerAvatar img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailOwnerText {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailOwnerText strong,
|
||||||
|
.detailOwnerText span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailOwnerText strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--admin-font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailOwnerText span {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.toolbar,
|
.toolbar,
|
||||||
.filters {
|
.filters {
|
||||||
@ -183,4 +361,8 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detailGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,14 +2,18 @@ import Dialog from "@mui/material/Dialog";
|
|||||||
import List from "@mui/material/List";
|
import List from "@mui/material/List";
|
||||||
import ListItemButton from "@mui/material/ListItemButton";
|
import ListItemButton from "@mui/material/ListItemButton";
|
||||||
import ListItemText from "@mui/material/ListItemText";
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { searchGlobal } from "@/features/search/api";
|
import { searchGlobal } from "@/features/search/api";
|
||||||
import { SearchBox } from "@/shared/ui/SearchBox.jsx";
|
import { SearchBox } from "@/shared/ui/SearchBox.jsx";
|
||||||
|
|
||||||
export function SearchDialog({ onClose, onSelect, open }) {
|
const maxMenuResults = 8;
|
||||||
|
|
||||||
|
export function SearchDialog({ menuItems = [], onClose, onSelect, open }) {
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
const [results, setResults] = useState([]);
|
const [results, setResults] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const menuResults = useMemo(() => searchMenus(menuItems, keyword), [keyword, menuItems]);
|
||||||
|
const displayResults = useMemo(() => [...menuResults, ...results], [menuResults, results]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@ -53,11 +57,11 @@ export function SearchDialog({ onClose, onSelect, open }) {
|
|||||||
slotProps={{ paper: { className: "search-dialog" } }}
|
slotProps={{ paper: { className: "search-dialog" } }}
|
||||||
>
|
>
|
||||||
<div className="search-dialog__head">
|
<div className="search-dialog__head">
|
||||||
<SearchBox autoFocus className="search-dialog__input" value={keyword} onChange={setKeyword} placeholder="搜索用户、角色..." />
|
<SearchBox autoFocus className="search-dialog__input" value={keyword} onChange={setKeyword} placeholder="搜索菜单、用户、角色..." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<List className="search-dialog__list" disablePadding>
|
<List className="search-dialog__list" disablePadding>
|
||||||
{results.map((item) => (
|
{displayResults.map((item) => (
|
||||||
<ListItemButton className="search-dialog__item" key={`${item.type}-${item.id}`} onClick={() => selectResult(item)}>
|
<ListItemButton className="search-dialog__item" key={`${item.type}-${item.id}`} onClick={() => selectResult(item)}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={item.title}
|
primary={item.title}
|
||||||
@ -69,8 +73,61 @@ export function SearchDialog({ onClose, onSelect, open }) {
|
|||||||
/>
|
/>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
))}
|
))}
|
||||||
{!results.length ? <div className="search-dialog__empty">{loading ? "搜索中..." : "暂无匹配结果"}</div> : null}
|
{!displayResults.length ? <div className="search-dialog__empty">{loading ? "搜索中..." : "暂无匹配结果"}</div> : null}
|
||||||
</List>
|
</List>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function searchMenus(items = [], keyword = "") {
|
||||||
|
const normalizedKeyword = normalizeSearchText(keyword);
|
||||||
|
if (!normalizedKeyword) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return flattenMenuResults(items)
|
||||||
|
.filter((item) => item.searchText.includes(normalizedKeyword))
|
||||||
|
.slice(0, maxMenuResults)
|
||||||
|
.map(({ searchText: _searchText, ...item }) => item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenMenuResults(items = [], ancestors = []) {
|
||||||
|
return items.flatMap((item) => {
|
||||||
|
const label = item.label || item.title || item.code || "";
|
||||||
|
const breadcrumb = [...ancestors, label].filter(Boolean);
|
||||||
|
const target = item.path || findFirstMenuPath(item.children);
|
||||||
|
const current = target
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: item.code || item.id || breadcrumb.join("/"),
|
||||||
|
searchText: normalizeSearchText([label, item.code, item.path].filter(Boolean).join(" ")),
|
||||||
|
subtitle: breadcrumb.join(" / "),
|
||||||
|
target,
|
||||||
|
title: label,
|
||||||
|
type: "菜单"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return [...current, ...flattenMenuResults(item.children || [], breadcrumb)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFirstMenuPath(items = []) {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.path) {
|
||||||
|
return item.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childPath = findFirstMenuPath(item.children || []);
|
||||||
|
if (childPath) {
|
||||||
|
return childPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSearchText(value) {
|
||||||
|
return String(value || "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|||||||
42
src/features/search/components/SearchDialog.test.jsx
Normal file
42
src/features/search/components/SearchDialog.test.jsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { searchMenus } from "./SearchDialog.jsx";
|
||||||
|
|
||||||
|
describe("searchMenus", () => {
|
||||||
|
const menus = [
|
||||||
|
{
|
||||||
|
code: "overview",
|
||||||
|
label: "系统概览",
|
||||||
|
path: "/overview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "app-users",
|
||||||
|
label: "用户管理",
|
||||||
|
children: [{ code: "app-users-list", label: "用户列表", path: "/app/users" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "system",
|
||||||
|
label: "后台设置",
|
||||||
|
children: [
|
||||||
|
{ code: "system-users", label: "后台用户", path: "/system/users" },
|
||||||
|
{ code: "system-roles", label: "角色配置", path: "/system/roles" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
test("matches visible menu labels and returns navigable targets", () => {
|
||||||
|
expect(searchMenus(menus, "后台")).toEqual([
|
||||||
|
expect.objectContaining({ target: "/system/users", title: "后台设置", type: "菜单" }),
|
||||||
|
expect.objectContaining({ target: "/system/users", title: "后台用户", type: "菜单" })
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("matches menu code and keeps breadcrumb for context", () => {
|
||||||
|
expect(searchMenus(menus, "app-users-list")).toEqual([
|
||||||
|
expect.objectContaining({ subtitle: "用户管理 / 用户列表", target: "/app/users", title: "用户列表" })
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns nothing for blank keyword", () => {
|
||||||
|
expect(searchMenus(menus, " ")).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import { Button } from "@/shared/ui/Button.jsx";
|
|
||||||
import { FilterChip } from "@/shared/ui/FilterChip.jsx";
|
|
||||||
import { SearchBox } from "@/shared/ui/SearchBox.jsx";
|
|
||||||
|
|
||||||
const statusFilters = [
|
|
||||||
["all", "全部"],
|
|
||||||
["active", "启用"],
|
|
||||||
["locked", "锁定"],
|
|
||||||
["disabled", "停用"]
|
|
||||||
];
|
|
||||||
|
|
||||||
export function UserFilters({
|
|
||||||
canBatchStatus,
|
|
||||||
onBatchStatus,
|
|
||||||
onQueryChange,
|
|
||||||
onRoleFilterChange,
|
|
||||||
onStatusFilterChange,
|
|
||||||
query,
|
|
||||||
roleFilter,
|
|
||||||
roleFilters,
|
|
||||||
statusFilter
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<section className="filters-panel user-filters-panel">
|
|
||||||
<SearchBox value={query} onChange={onQueryChange} placeholder="搜索用户、账号、角色、团队..." />
|
|
||||||
<div className="filter-row" aria-label="角色筛选">
|
|
||||||
{roleFilters.map(([value, label]) => (
|
|
||||||
<FilterChip active={roleFilter === value} key={value} label={label} onClick={() => onRoleFilterChange(value)} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="filter-row" aria-label="状态筛选">
|
|
||||||
{statusFilters.map(([value, label]) => (
|
|
||||||
<FilterChip active={statusFilter === value} key={value} label={label} onClick={() => onStatusFilterChange(value)} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{canBatchStatus ? (
|
|
||||||
<div className="batch-actions">
|
|
||||||
<Button onClick={() => onBatchStatus("active")}>批量启用</Button>
|
|
||||||
<Button variant="danger" onClick={() => onBatchStatus("disabled")}>批量停用</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -8,6 +8,7 @@ import VerifiedUserOutlined from "@mui/icons-material/VerifiedUserOutlined";
|
|||||||
import Checkbox from "@mui/material/Checkbox";
|
import Checkbox from "@mui/material/Checkbox";
|
||||||
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
|
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
import { UserStatusBadge } from "./UserStatusBadge.jsx";
|
import { UserStatusBadge } from "./UserStatusBadge.jsx";
|
||||||
|
|
||||||
export function UserTable({
|
export function UserTable({
|
||||||
@ -16,11 +17,19 @@ export function UserTable({
|
|||||||
canStatus,
|
canStatus,
|
||||||
onEdit,
|
onEdit,
|
||||||
onOpenProfile = () => {},
|
onOpenProfile = () => {},
|
||||||
|
onQueryChange,
|
||||||
onResetPassword,
|
onResetPassword,
|
||||||
|
onRoleFilterChange,
|
||||||
|
onStatusFilterChange,
|
||||||
onToggleDisabled,
|
onToggleDisabled,
|
||||||
onToggleLocked,
|
onToggleLocked,
|
||||||
|
query,
|
||||||
|
roleFilter,
|
||||||
|
roleFilters = [],
|
||||||
selectedIds,
|
selectedIds,
|
||||||
setSelectedIds,
|
setSelectedIds,
|
||||||
|
statusFilter,
|
||||||
|
statusFilters = [],
|
||||||
users
|
users
|
||||||
}) {
|
}) {
|
||||||
const allChecked = users.length > 0 && users.every((user) => selectedIds.includes(user.id));
|
const allChecked = users.length > 0 && users.every((user) => selectedIds.includes(user.id));
|
||||||
@ -51,28 +60,55 @@ export function UserTable({
|
|||||||
key: "user",
|
key: "user",
|
||||||
label: "用户",
|
label: "用户",
|
||||||
width: "minmax(180px, 1.4fr)",
|
width: "minmax(180px, 1.4fr)",
|
||||||
|
filter: createTextColumnFilter({
|
||||||
|
placeholder: "搜索用户、账号、角色、团队",
|
||||||
|
value: query,
|
||||||
|
onChange: onQueryChange
|
||||||
|
}),
|
||||||
render: (user) => (
|
render: (user) => (
|
||||||
<div className="user-main">
|
|
||||||
<button
|
<button
|
||||||
aria-label={`查看 ${user.name || user.account || user.id} 信息`}
|
aria-label={`查看 ${user.name || user.account || user.id} 信息`}
|
||||||
className="user-avatar-button"
|
className="user-main-button"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onOpenProfile(user)}
|
onClick={() => onOpenProfile(user)}
|
||||||
>
|
>
|
||||||
|
<span className="user-main">
|
||||||
<span className="user-avatar">
|
<span className="user-avatar">
|
||||||
<Person fontSize="small" />
|
<Person fontSize="small" />
|
||||||
</span>
|
</span>
|
||||||
|
<span className="user-main__text">
|
||||||
|
<span className="service-name">{user.name}</span>
|
||||||
|
<span className="service-desc">{user.account}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div>
|
|
||||||
<div className="service-name">{user.name}</div>
|
|
||||||
<div className="service-desc">{user.account}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ key: "role", label: "角色", width: "minmax(120px, 1fr)", render: (user) => user.role || "-" },
|
{
|
||||||
|
key: "role",
|
||||||
|
label: "角色",
|
||||||
|
width: "minmax(120px, 1fr)",
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: roleFilters,
|
||||||
|
placeholder: "搜索角色",
|
||||||
|
value: roleFilter,
|
||||||
|
onChange: onRoleFilterChange
|
||||||
|
}),
|
||||||
|
render: (user) => user.role || "-"
|
||||||
|
},
|
||||||
{ key: "team", label: "团队", width: "minmax(120px, 1fr)", render: (user) => user.team || "-" },
|
{ key: "team", label: "团队", width: "minmax(120px, 1fr)", render: (user) => user.team || "-" },
|
||||||
{ key: "status", label: "状态", width: "minmax(96px, 0.72fr)", render: (user) => <UserStatusBadge status={user.status} /> },
|
{
|
||||||
|
key: "status",
|
||||||
|
label: "状态",
|
||||||
|
width: "minmax(96px, 0.72fr)",
|
||||||
|
filter: createOptionsColumnFilter({
|
||||||
|
options: statusFilters,
|
||||||
|
placeholder: "搜索状态",
|
||||||
|
value: statusFilter,
|
||||||
|
onChange: onStatusFilterChange
|
||||||
|
}),
|
||||||
|
render: (user) => <UserStatusBadge status={user.status} />
|
||||||
|
},
|
||||||
{ key: "mfa", label: "MFA", width: "minmax(80px, 0.72fr)" },
|
{ key: "mfa", label: "MFA", width: "minmax(80px, 0.72fr)" },
|
||||||
{ key: "lastLogin", label: "最后登录", width: "minmax(120px, 0.9fr)", className: "muted" },
|
{ key: "lastLogin", label: "最后登录", width: "minmax(120px, 0.9fr)", className: "muted" },
|
||||||
{
|
{
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const baseUser = {
|
|||||||
team: "平台"
|
team: "平台"
|
||||||
};
|
};
|
||||||
|
|
||||||
test("opens user profile from avatar", async () => {
|
test("opens user profile from user identity", async () => {
|
||||||
const onOpenProfile = vi.fn();
|
const onOpenProfile = vi.fn();
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
listUsers,
|
listUsers,
|
||||||
resetUserPassword,
|
resetUserPassword,
|
||||||
updateUser,
|
updateUser,
|
||||||
updateUserStatus
|
updateUserStatus,
|
||||||
} from "@/features/users/api";
|
} from "@/features/users/api";
|
||||||
import { useUserAbilities } from "@/features/users/permissions.js";
|
import { useUserAbilities } from "@/features/users/permissions.js";
|
||||||
import { userCreateFormSchema, userUpdateFormSchema } from "@/features/users/schema";
|
import { userCreateFormSchema, userUpdateFormSchema } from "@/features/users/schema";
|
||||||
@ -30,7 +30,7 @@ const emptyForm = {
|
|||||||
roleIds: [],
|
roleIds: [],
|
||||||
status: "active",
|
status: "active",
|
||||||
team: "",
|
team: "",
|
||||||
username: ""
|
username: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useUsersPage() {
|
export function useUsersPage() {
|
||||||
@ -50,13 +50,13 @@ export function useUsersPage() {
|
|||||||
|
|
||||||
const requestQuery = useMemo(
|
const requestQuery = useMemo(
|
||||||
() => toPageQuery({ keyword: query, page, pageSize, role: roleFilter, status: statusFilter }),
|
() => toPageQuery({ keyword: query, page, pageSize, role: roleFilter, status: statusFilter }),
|
||||||
[page, query, roleFilter, statusFilter]
|
[page, query, roleFilter, statusFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const queryFn = useCallback(async () => {
|
const queryFn = useCallback(async () => {
|
||||||
const [userData, roleData] = await Promise.all([
|
const [userData, roleData] = await Promise.all([
|
||||||
listUsers(requestQuery),
|
listUsers(requestQuery),
|
||||||
abilities.canViewRoles ? listRoles() : []
|
abilities.canViewRoles ? listRoles() : [],
|
||||||
]);
|
]);
|
||||||
setSelectedIds([]);
|
setSelectedIds([]);
|
||||||
return { roles: roleData || [], users: userData || emptyUsers };
|
return { roles: roleData || [], users: userData || emptyUsers };
|
||||||
@ -66,7 +66,7 @@ export function useUsersPage() {
|
|||||||
errorMessage: "加载用户失败",
|
errorMessage: "加载用户失败",
|
||||||
initialData: emptyData,
|
initialData: emptyData,
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
queryKey: ["users", requestQuery, abilities.canViewRoles]
|
queryKey: ["users", requestQuery, abilities.canViewRoles],
|
||||||
});
|
});
|
||||||
|
|
||||||
const users = data?.users || emptyUsers;
|
const users = data?.users || emptyUsers;
|
||||||
@ -97,6 +97,13 @@ export function useUsersPage() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setQuery("");
|
||||||
|
setRoleFilter("");
|
||||||
|
setStatusFilter("");
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditingUser(null);
|
setEditingUser(null);
|
||||||
setForm(emptyForm);
|
setForm(emptyForm);
|
||||||
@ -112,7 +119,7 @@ export function useUsersPage() {
|
|||||||
roleIds: user.roleIds || [],
|
roleIds: user.roleIds || [],
|
||||||
status: user.status,
|
status: user.status,
|
||||||
team: user.team || "",
|
team: user.team || "",
|
||||||
username: user.account
|
username: user.account,
|
||||||
});
|
});
|
||||||
setFormOpen(true);
|
setFormOpen(true);
|
||||||
};
|
};
|
||||||
@ -154,7 +161,7 @@ export function useUsersPage() {
|
|||||||
confirmText: user.status === "locked" ? "解锁" : "锁定",
|
confirmText: user.status === "locked" ? "解锁" : "锁定",
|
||||||
message: `${user.name} 的登录状态会立即变更。`,
|
message: `${user.name} 的登录状态会立即变更。`,
|
||||||
title: user.status === "locked" ? "解锁用户" : "锁定用户",
|
title: user.status === "locked" ? "解锁用户" : "锁定用户",
|
||||||
tone: user.status === "locked" ? "primary" : "danger"
|
tone: user.status === "locked" ? "primary" : "danger",
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
@ -170,7 +177,7 @@ export function useUsersPage() {
|
|||||||
confirmText: user.status === "disabled" ? "启用" : "停用",
|
confirmText: user.status === "disabled" ? "启用" : "停用",
|
||||||
message: `${user.name} 的账号状态会立即变更。`,
|
message: `${user.name} 的账号状态会立即变更。`,
|
||||||
title: user.status === "disabled" ? "启用用户" : "停用用户",
|
title: user.status === "disabled" ? "启用用户" : "停用用户",
|
||||||
tone: user.status === "disabled" ? "primary" : "danger"
|
tone: user.status === "disabled" ? "primary" : "danger",
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
@ -185,7 +192,7 @@ export function useUsersPage() {
|
|||||||
confirmText: "重置",
|
confirmText: "重置",
|
||||||
message: `${user.name} 的密码会重置为系统生成的初始密码。`,
|
message: `${user.name} 的密码会重置为系统生成的初始密码。`,
|
||||||
title: "重置密码",
|
title: "重置密码",
|
||||||
tone: "danger"
|
tone: "danger",
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
@ -203,7 +210,7 @@ export function useUsersPage() {
|
|||||||
confirmText: "确认",
|
confirmText: "确认",
|
||||||
message: `将更新 ${selectedIds.length} 个用户状态。`,
|
message: `将更新 ${selectedIds.length} 个用户状态。`,
|
||||||
title: "批量更新状态",
|
title: "批量更新状态",
|
||||||
tone: status === "active" ? "primary" : "danger"
|
tone: status === "active" ? "primary" : "danger",
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return;
|
return;
|
||||||
@ -216,14 +223,14 @@ export function useUsersPage() {
|
|||||||
const downloadUsers = () => {
|
const downloadUsers = () => {
|
||||||
return downloadCsv(
|
return downloadCsv(
|
||||||
() => exportUsers({ keyword: query, role: roleFilter, status: statusFilter }),
|
() => exportUsers({ keyword: query, role: roleFilter, status: statusFilter }),
|
||||||
"hyapp-users.csv"
|
"hyapp-users.csv",
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setRoleChecked = (roleId, checked) => {
|
const setRoleChecked = (roleId, checked) => {
|
||||||
setForm((current) => ({
|
setForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
roleIds: checked ? [...current.roleIds, roleId] : current.roleIds.filter((id) => id !== roleId)
|
roleIds: checked ? [...current.roleIds, roleId] : current.roleIds.filter((id) => id !== roleId),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -250,6 +257,7 @@ export function useUsersPage() {
|
|||||||
query,
|
query,
|
||||||
reload,
|
reload,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
|
resetFilters,
|
||||||
roleFilter,
|
roleFilter,
|
||||||
roleFilters,
|
roleFilters,
|
||||||
roles,
|
roles,
|
||||||
@ -263,6 +271,6 @@ export function useUsersPage() {
|
|||||||
submitUser,
|
submitUser,
|
||||||
toggleDisabled,
|
toggleDisabled,
|
||||||
toggleLocked,
|
toggleLocked,
|
||||||
users
|
users,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,11 +7,10 @@ import { DataState } from "@/shared/ui/DataState.jsx";
|
|||||||
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
import { PaginationBar } from "@/shared/ui/PaginationBar.jsx";
|
||||||
import {
|
import {
|
||||||
AdminActionIconButton,
|
AdminActionIconButton,
|
||||||
AdminFilterSelect,
|
AdminFilterResetButton,
|
||||||
AdminListBody,
|
AdminListBody,
|
||||||
AdminListPage,
|
AdminListPage,
|
||||||
AdminListToolbar,
|
AdminListToolbar,
|
||||||
AdminSearchBox
|
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { UserFormDrawer } from "@/features/users/components/UserFormDrawer.jsx";
|
import { UserFormDrawer } from "@/features/users/components/UserFormDrawer.jsx";
|
||||||
import { UserProfileDrawer } from "@/features/users/components/UserProfileDrawer.jsx";
|
import { UserProfileDrawer } from "@/features/users/components/UserProfileDrawer.jsx";
|
||||||
@ -26,14 +25,13 @@ export function UserManagementPage() {
|
|||||||
<>
|
<>
|
||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
filters={(
|
filters={
|
||||||
<>
|
<AdminFilterResetButton
|
||||||
<AdminSearchBox value={page.query} onChange={page.changeQuery} placeholder="搜索用户、账号、角色、团队..." />
|
disabled={!page.query && !page.roleFilter && !page.statusFilter}
|
||||||
<AdminFilterSelect label="角色" options={page.roleFilters} value={page.roleFilter} onChange={page.changeRoleFilter} />
|
onClick={page.resetFilters}
|
||||||
<AdminFilterSelect options={userStatusFilters} value={page.statusFilter} onChange={page.changeStatusFilter} />
|
/>
|
||||||
</>
|
}
|
||||||
)}
|
actions={
|
||||||
actions={(
|
|
||||||
<>
|
<>
|
||||||
{page.abilities.canViewRoles ? (
|
{page.abilities.canViewRoles ? (
|
||||||
<AdminActionIconButton label="角色配置" onClick={() => page.navigate("/system/roles")}>
|
<AdminActionIconButton label="角色配置" onClick={() => page.navigate("/system/roles")}>
|
||||||
@ -45,7 +43,11 @@ export function UserManagementPage() {
|
|||||||
<AdminActionIconButton label="批量启用" onClick={() => page.batchStatus("active")}>
|
<AdminActionIconButton label="批量启用" onClick={() => page.batchStatus("active")}>
|
||||||
<VerifiedUserOutlined fontSize="small" />
|
<VerifiedUserOutlined fontSize="small" />
|
||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
<AdminActionIconButton label="批量停用" tone="danger" onClick={() => page.batchStatus("disabled")}>
|
<AdminActionIconButton
|
||||||
|
label="批量停用"
|
||||||
|
tone="danger"
|
||||||
|
onClick={() => page.batchStatus("disabled")}
|
||||||
|
>
|
||||||
<PersonRemoveOutlined fontSize="small" />
|
<PersonRemoveOutlined fontSize="small" />
|
||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
</>
|
</>
|
||||||
@ -61,7 +63,7 @@ export function UserManagementPage() {
|
|||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
@ -75,11 +77,24 @@ export function UserManagementPage() {
|
|||||||
onResetPassword={page.resetPassword}
|
onResetPassword={page.resetPassword}
|
||||||
onToggleDisabled={page.toggleDisabled}
|
onToggleDisabled={page.toggleDisabled}
|
||||||
onToggleLocked={page.toggleLocked}
|
onToggleLocked={page.toggleLocked}
|
||||||
|
query={page.query}
|
||||||
|
roleFilter={page.roleFilter}
|
||||||
|
roleFilters={page.roleFilters}
|
||||||
selectedIds={page.selectedIds}
|
selectedIds={page.selectedIds}
|
||||||
setSelectedIds={page.setSelectedIds}
|
setSelectedIds={page.setSelectedIds}
|
||||||
|
statusFilter={page.statusFilter}
|
||||||
|
statusFilters={userStatusFilters}
|
||||||
|
onQueryChange={page.changeQuery}
|
||||||
|
onRoleFilterChange={page.changeRoleFilter}
|
||||||
|
onStatusFilterChange={page.changeStatusFilter}
|
||||||
users={page.users.items || []}
|
users={page.users.items || []}
|
||||||
/>
|
/>
|
||||||
<PaginationBar page={page.page} pageSize={page.users.pageSize || 10} total={page.users.total || 0} onPageChange={page.setPage} />
|
<PaginationBar
|
||||||
|
page={page.page}
|
||||||
|
pageSize={page.users.pageSize || 10}
|
||||||
|
total={page.users.total || 0}
|
||||||
|
onPageChange={page.setPage}
|
||||||
|
/>
|
||||||
</AdminListBody>
|
</AdminListBody>
|
||||||
</DataState>
|
</DataState>
|
||||||
</AdminListPage>
|
</AdminListPage>
|
||||||
@ -94,11 +109,7 @@ export function UserManagementPage() {
|
|||||||
roles={page.roles}
|
roles={page.roles}
|
||||||
setForm={page.setForm}
|
setForm={page.setForm}
|
||||||
/>
|
/>
|
||||||
<UserProfileDrawer
|
<UserProfileDrawer onClose={page.closeProfile} open={Boolean(page.profileUser)} user={page.profileUser} />
|
||||||
onClose={page.closeProfile}
|
|
||||||
open={Boolean(page.profileUser)}
|
|
||||||
user={page.profileUser}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { MENU_CODES, PERMISSIONS } from "@/app/permissions";
|
|||||||
|
|
||||||
export const usersRoutes = [
|
export const usersRoutes = [
|
||||||
{
|
{
|
||||||
label: "用户管理",
|
label: "后台用户",
|
||||||
loader: () => import("./pages/UserManagementPage.jsx").then((module) => module.UserManagementPage),
|
loader: () => import("./pages/UserManagementPage.jsx").then((module) => module.UserManagementPage),
|
||||||
menuCode: MENU_CODES.systemUsers,
|
menuCode: MENU_CODES.systemUsers,
|
||||||
pageKey: "users",
|
pageKey: "users",
|
||||||
|
|||||||
@ -19,6 +19,59 @@
|
|||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-main-button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 2px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background var(--motion-base) var(--ease-standard),
|
||||||
|
box-shadow var(--motion-base) var(--ease-standard),
|
||||||
|
transform var(--motion-base) var(--ease-emphasized);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-main-button:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-main-button:hover .service-name {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-main-button:focus-visible {
|
||||||
|
outline: 2px solid var(--primary-border-strong);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-main__text {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-main__text .service-name,
|
||||||
|
.user-main__text .service-desc {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-main__text .service-name {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-main__text .service-desc {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@ -33,24 +86,6 @@
|
|||||||
transform var(--motion-base) var(--ease-emphasized);
|
transform var(--motion-base) var(--ease-emphasized);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avatar-button {
|
|
||||||
display: grid;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--radius-control);
|
|
||||||
background: transparent;
|
|
||||||
color: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar-button:focus-visible {
|
|
||||||
outline: 2px solid var(--primary-border-strong);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-row:hover .user-avatar {
|
.admin-row:hover .user-avatar {
|
||||||
background: var(--primary-surface-strong);
|
background: var(--primary-surface-strong);
|
||||||
transform: scale(1.08);
|
transform: scale(1.08);
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export const API_OPERATIONS = {
|
|||||||
createRole: "createRole",
|
createRole: "createRole",
|
||||||
createUser: "createUser",
|
createUser: "createUser",
|
||||||
createUserExportJob: "createUserExportJob",
|
createUserExportJob: "createUserExportJob",
|
||||||
|
creditCoinSellerStock: "creditCoinSellerStock",
|
||||||
dashboardOverview: "dashboardOverview",
|
dashboardOverview: "dashboardOverview",
|
||||||
deleteBanner: "deleteBanner",
|
deleteBanner: "deleteBanner",
|
||||||
deleteCountry: "deleteCountry",
|
deleteCountry: "deleteCountry",
|
||||||
@ -126,7 +127,7 @@ export const API_OPERATIONS = {
|
|||||||
updateUser: "updateUser",
|
updateUser: "updateUser",
|
||||||
updateUserStatus: "updateUserStatus",
|
updateUserStatus: "updateUserStatus",
|
||||||
uploadFile: "uploadFile",
|
uploadFile: "uploadFile",
|
||||||
uploadImage: "uploadImage"
|
uploadImage: "uploadImage",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ApiOperationId = (typeof API_OPERATIONS)[keyof typeof API_OPERATIONS];
|
export type ApiOperationId = (typeof API_OPERATIONS)[keyof typeof API_OPERATIONS];
|
||||||
@ -137,783 +138,790 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
operationId: API_OPERATIONS.appBanUser,
|
operationId: API_OPERATIONS.appBanUser,
|
||||||
path: "/v1/app/users/{id}/ban",
|
path: "/v1/app/users/{id}/ban",
|
||||||
permission: "app-user:status",
|
permission: "app-user:status",
|
||||||
permissions: ["app-user:status"]
|
permissions: ["app-user:status"],
|
||||||
},
|
},
|
||||||
appGetUser: {
|
appGetUser: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.appGetUser,
|
operationId: API_OPERATIONS.appGetUser,
|
||||||
path: "/v1/app/users/{id}",
|
path: "/v1/app/users/{id}",
|
||||||
permission: "app-user:view",
|
permission: "app-user:view",
|
||||||
permissions: ["app-user:view"]
|
permissions: ["app-user:view"],
|
||||||
},
|
},
|
||||||
appListUsers: {
|
appListUsers: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.appListUsers,
|
operationId: API_OPERATIONS.appListUsers,
|
||||||
path: "/v1/app/users",
|
path: "/v1/app/users",
|
||||||
permission: "app-user:view",
|
permission: "app-user:view",
|
||||||
permissions: ["app-user:view"]
|
permissions: ["app-user:view"],
|
||||||
},
|
},
|
||||||
appSetPassword: {
|
appSetPassword: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.appSetPassword,
|
operationId: API_OPERATIONS.appSetPassword,
|
||||||
path: "/v1/app/users/{id}/password",
|
path: "/v1/app/users/{id}/password",
|
||||||
permission: "app-user:password",
|
permission: "app-user:password",
|
||||||
permissions: ["app-user:password"]
|
permissions: ["app-user:password"],
|
||||||
},
|
},
|
||||||
appUnbanUser: {
|
appUnbanUser: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.appUnbanUser,
|
operationId: API_OPERATIONS.appUnbanUser,
|
||||||
path: "/v1/app/users/{id}/unban",
|
path: "/v1/app/users/{id}/unban",
|
||||||
permission: "app-user:status",
|
permission: "app-user:status",
|
||||||
permissions: ["app-user:status"]
|
permissions: ["app-user:status"],
|
||||||
},
|
},
|
||||||
appUpdateUser: {
|
appUpdateUser: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.appUpdateUser,
|
operationId: API_OPERATIONS.appUpdateUser,
|
||||||
path: "/v1/app/users/{id}",
|
path: "/v1/app/users/{id}",
|
||||||
permission: "app-user:update",
|
permission: "app-user:update",
|
||||||
permissions: ["app-user:update"]
|
permissions: ["app-user:update"],
|
||||||
},
|
},
|
||||||
batchUpdateUserStatus: {
|
batchUpdateUserStatus: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.batchUpdateUserStatus,
|
operationId: API_OPERATIONS.batchUpdateUserStatus,
|
||||||
path: "/v1/users/batch/status",
|
path: "/v1/users/batch/status",
|
||||||
permission: "user:status",
|
permission: "user:status",
|
||||||
permissions: ["user:status"]
|
permissions: ["user:status"],
|
||||||
},
|
},
|
||||||
cancelJob: {
|
cancelJob: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.cancelJob,
|
operationId: API_OPERATIONS.cancelJob,
|
||||||
path: "/v1/jobs/{id}/cancel",
|
path: "/v1/jobs/{id}/cancel",
|
||||||
permission: "job:cancel",
|
permission: "job:cancel",
|
||||||
permissions: ["job:cancel"]
|
permissions: ["job:cancel"],
|
||||||
},
|
},
|
||||||
changePassword: {
|
changePassword: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.changePassword,
|
operationId: API_OPERATIONS.changePassword,
|
||||||
path: "/v1/auth/change-password"
|
path: "/v1/auth/change-password",
|
||||||
},
|
},
|
||||||
closeAgency: {
|
closeAgency: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.closeAgency,
|
operationId: API_OPERATIONS.closeAgency,
|
||||||
path: "/v1/admin/agencies/{agency_id}/close",
|
path: "/v1/admin/agencies/{agency_id}/close",
|
||||||
permission: "agency:status",
|
permission: "agency:status",
|
||||||
permissions: ["agency:status"]
|
permissions: ["agency:status"],
|
||||||
},
|
},
|
||||||
createAgency: {
|
createAgency: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createAgency,
|
operationId: API_OPERATIONS.createAgency,
|
||||||
path: "/v1/admin/agencies",
|
path: "/v1/admin/agencies",
|
||||||
permission: "agency:create",
|
permission: "agency:create",
|
||||||
permissions: ["agency:create"]
|
permissions: ["agency:create"],
|
||||||
},
|
},
|
||||||
createBanner: {
|
createBanner: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createBanner,
|
operationId: API_OPERATIONS.createBanner,
|
||||||
path: "/v1/admin/app-config/banners",
|
path: "/v1/admin/app-config/banners",
|
||||||
permission: "app-config:update",
|
permission: "app-config:update",
|
||||||
permissions: ["app-config:update"]
|
permissions: ["app-config:update"],
|
||||||
},
|
},
|
||||||
createBD: {
|
createBD: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createBD,
|
operationId: API_OPERATIONS.createBD,
|
||||||
path: "/v1/admin/bds",
|
path: "/v1/admin/bds",
|
||||||
permission: "bd:create",
|
permission: "bd:create",
|
||||||
permissions: ["bd:create"]
|
permissions: ["bd:create"],
|
||||||
},
|
},
|
||||||
createBDLeader: {
|
createBDLeader: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createBDLeader,
|
operationId: API_OPERATIONS.createBDLeader,
|
||||||
path: "/v1/admin/bd-leaders",
|
path: "/v1/admin/bd-leaders",
|
||||||
permission: "bd:create",
|
permission: "bd:create",
|
||||||
permissions: ["bd:create"]
|
permissions: ["bd:create"],
|
||||||
},
|
},
|
||||||
createCoinSeller: {
|
createCoinSeller: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createCoinSeller,
|
operationId: API_OPERATIONS.createCoinSeller,
|
||||||
path: "/v1/admin/coin-sellers",
|
path: "/v1/admin/coin-sellers",
|
||||||
permission: "coin-seller:create",
|
permission: "coin-seller:create",
|
||||||
permissions: ["coin-seller:create"]
|
permissions: ["coin-seller:create"],
|
||||||
},
|
},
|
||||||
createCountry: {
|
createCountry: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createCountry,
|
operationId: API_OPERATIONS.createCountry,
|
||||||
path: "/v1/admin/countries",
|
path: "/v1/admin/countries",
|
||||||
permission: "country:create",
|
permission: "country:create",
|
||||||
permissions: ["country:create"]
|
permissions: ["country:create"],
|
||||||
},
|
},
|
||||||
createGift: {
|
createGift: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createGift,
|
operationId: API_OPERATIONS.createGift,
|
||||||
path: "/v1/admin/gifts",
|
path: "/v1/admin/gifts",
|
||||||
permission: "gift:create",
|
permission: "gift:create",
|
||||||
permissions: ["gift:create"]
|
permissions: ["gift:create"],
|
||||||
},
|
},
|
||||||
createMenu: {
|
createMenu: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createMenu,
|
operationId: API_OPERATIONS.createMenu,
|
||||||
path: "/v1/system/menus",
|
path: "/v1/system/menus",
|
||||||
permission: "menu:create",
|
permission: "menu:create",
|
||||||
permissions: ["menu:create"]
|
permissions: ["menu:create"],
|
||||||
},
|
},
|
||||||
createPermission: {
|
createPermission: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createPermission,
|
operationId: API_OPERATIONS.createPermission,
|
||||||
path: "/v1/permissions",
|
path: "/v1/permissions",
|
||||||
permission: "permission:create",
|
permission: "permission:create",
|
||||||
permissions: ["permission:create"]
|
permissions: ["permission:create"],
|
||||||
},
|
},
|
||||||
createRegion: {
|
createRegion: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createRegion,
|
operationId: API_OPERATIONS.createRegion,
|
||||||
path: "/v1/admin/regions",
|
path: "/v1/admin/regions",
|
||||||
permission: "region:create",
|
permission: "region:create",
|
||||||
permissions: ["region:create"]
|
permissions: ["region:create"],
|
||||||
},
|
},
|
||||||
createResource: {
|
createResource: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createResource,
|
operationId: API_OPERATIONS.createResource,
|
||||||
path: "/v1/admin/resources",
|
path: "/v1/admin/resources",
|
||||||
permission: "resource:create",
|
permission: "resource:create",
|
||||||
permissions: ["resource:create"]
|
permissions: ["resource:create"],
|
||||||
},
|
},
|
||||||
createResourceGroup: {
|
createResourceGroup: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createResourceGroup,
|
operationId: API_OPERATIONS.createResourceGroup,
|
||||||
path: "/v1/admin/resource-groups",
|
path: "/v1/admin/resource-groups",
|
||||||
permission: "resource-group:create",
|
permission: "resource-group:create",
|
||||||
permissions: ["resource-group:create"]
|
permissions: ["resource-group:create"],
|
||||||
},
|
},
|
||||||
createRole: {
|
createRole: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createRole,
|
operationId: API_OPERATIONS.createRole,
|
||||||
path: "/v1/roles",
|
path: "/v1/roles",
|
||||||
permission: "role:create",
|
permission: "role:create",
|
||||||
permissions: ["role:create","role:manage"]
|
permissions: ["role:create", "role:manage"],
|
||||||
},
|
},
|
||||||
createUser: {
|
createUser: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createUser,
|
operationId: API_OPERATIONS.createUser,
|
||||||
path: "/v1/users",
|
path: "/v1/users",
|
||||||
permission: "user:create",
|
permission: "user:create",
|
||||||
permissions: ["user:create"]
|
permissions: ["user:create"],
|
||||||
},
|
},
|
||||||
createUserExportJob: {
|
createUserExportJob: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createUserExportJob,
|
operationId: API_OPERATIONS.createUserExportJob,
|
||||||
path: "/v1/exports/users",
|
path: "/v1/exports/users",
|
||||||
permission: "export:create",
|
permission: "export:create",
|
||||||
permissions: ["export:create"]
|
permissions: ["export:create"],
|
||||||
|
},
|
||||||
|
creditCoinSellerStock: {
|
||||||
|
method: "POST",
|
||||||
|
operationId: API_OPERATIONS.creditCoinSellerStock,
|
||||||
|
path: "/v1/admin/coin-sellers/{user_id}/stock-credits",
|
||||||
|
permission: "coin-seller:stock-credit",
|
||||||
|
permissions: ["coin-seller:stock-credit"],
|
||||||
},
|
},
|
||||||
dashboardOverview: {
|
dashboardOverview: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.dashboardOverview,
|
operationId: API_OPERATIONS.dashboardOverview,
|
||||||
path: "/v1/dashboard/overview",
|
path: "/v1/dashboard/overview",
|
||||||
permission: "overview:view",
|
permission: "overview:view",
|
||||||
permissions: ["overview:view"]
|
permissions: ["overview:view"],
|
||||||
},
|
},
|
||||||
deleteBanner: {
|
deleteBanner: {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
operationId: API_OPERATIONS.deleteBanner,
|
operationId: API_OPERATIONS.deleteBanner,
|
||||||
path: "/v1/admin/app-config/banners/{banner_id}",
|
path: "/v1/admin/app-config/banners/{banner_id}",
|
||||||
permission: "app-config:update",
|
permission: "app-config:update",
|
||||||
permissions: ["app-config:update"]
|
permissions: ["app-config:update"],
|
||||||
},
|
},
|
||||||
deleteCountry: {
|
deleteCountry: {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
operationId: API_OPERATIONS.deleteCountry,
|
operationId: API_OPERATIONS.deleteCountry,
|
||||||
path: "/v1/admin/countries/{country_id}",
|
path: "/v1/admin/countries/{country_id}",
|
||||||
permission: "country:status",
|
permission: "country:status",
|
||||||
permissions: ["country:status"]
|
permissions: ["country:status"],
|
||||||
},
|
},
|
||||||
deleteMenu: {
|
deleteMenu: {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
operationId: API_OPERATIONS.deleteMenu,
|
operationId: API_OPERATIONS.deleteMenu,
|
||||||
path: "/v1/system/menus/{id}",
|
path: "/v1/system/menus/{id}",
|
||||||
permission: "menu:delete",
|
permission: "menu:delete",
|
||||||
permissions: ["menu:delete"]
|
permissions: ["menu:delete"],
|
||||||
},
|
},
|
||||||
deleteNotification: {
|
deleteNotification: {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
operationId: API_OPERATIONS.deleteNotification,
|
operationId: API_OPERATIONS.deleteNotification,
|
||||||
path: "/v1/notifications/{id}",
|
path: "/v1/notifications/{id}",
|
||||||
permission: "notification:delete",
|
permission: "notification:delete",
|
||||||
permissions: ["notification:delete"]
|
permissions: ["notification:delete"],
|
||||||
},
|
},
|
||||||
deletePermission: {
|
deletePermission: {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
operationId: API_OPERATIONS.deletePermission,
|
operationId: API_OPERATIONS.deletePermission,
|
||||||
path: "/v1/permissions/{id}",
|
path: "/v1/permissions/{id}",
|
||||||
permission: "permission:delete",
|
permission: "permission:delete",
|
||||||
permissions: ["permission:delete"]
|
permissions: ["permission:delete"],
|
||||||
},
|
},
|
||||||
deleteRole: {
|
deleteRole: {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
operationId: API_OPERATIONS.deleteRole,
|
operationId: API_OPERATIONS.deleteRole,
|
||||||
path: "/v1/roles/{id}",
|
path: "/v1/roles/{id}",
|
||||||
permission: "role:delete",
|
permission: "role:delete",
|
||||||
permissions: ["role:delete","role:manage"]
|
permissions: ["role:delete", "role:manage"],
|
||||||
},
|
},
|
||||||
deleteRoom: {
|
deleteRoom: {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
operationId: API_OPERATIONS.deleteRoom,
|
operationId: API_OPERATIONS.deleteRoom,
|
||||||
path: "/v1/admin/rooms/{room_id}",
|
path: "/v1/admin/rooms/{room_id}",
|
||||||
permission: "room:delete",
|
permission: "room:delete",
|
||||||
permissions: ["room:delete"]
|
permissions: ["room:delete"],
|
||||||
},
|
},
|
||||||
disableCountry: {
|
disableCountry: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.disableCountry,
|
operationId: API_OPERATIONS.disableCountry,
|
||||||
path: "/v1/admin/countries/{country_id}/disable",
|
path: "/v1/admin/countries/{country_id}/disable",
|
||||||
permission: "country:status",
|
permission: "country:status",
|
||||||
permissions: ["country:status"]
|
permissions: ["country:status"],
|
||||||
},
|
},
|
||||||
disableGift: {
|
disableGift: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.disableGift,
|
operationId: API_OPERATIONS.disableGift,
|
||||||
path: "/v1/admin/gifts/{gift_id}/disable",
|
path: "/v1/admin/gifts/{gift_id}/disable",
|
||||||
permission: "gift:status",
|
permission: "gift:status",
|
||||||
permissions: ["gift:status"]
|
permissions: ["gift:status"],
|
||||||
},
|
},
|
||||||
disableRegion: {
|
disableRegion: {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
operationId: API_OPERATIONS.disableRegion,
|
operationId: API_OPERATIONS.disableRegion,
|
||||||
path: "/v1/admin/regions/{region_id}",
|
path: "/v1/admin/regions/{region_id}",
|
||||||
permission: "region:status",
|
permission: "region:status",
|
||||||
permissions: ["region:status"]
|
permissions: ["region:status"],
|
||||||
},
|
},
|
||||||
disableResource: {
|
disableResource: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.disableResource,
|
operationId: API_OPERATIONS.disableResource,
|
||||||
path: "/v1/admin/resources/{resource_id}/disable",
|
path: "/v1/admin/resources/{resource_id}/disable",
|
||||||
permission: "resource:update",
|
permission: "resource:update",
|
||||||
permissions: ["resource:update"]
|
permissions: ["resource:update"],
|
||||||
},
|
},
|
||||||
disableResourceGroup: {
|
disableResourceGroup: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.disableResourceGroup,
|
operationId: API_OPERATIONS.disableResourceGroup,
|
||||||
path: "/v1/admin/resource-groups/{group_id}/disable",
|
path: "/v1/admin/resource-groups/{group_id}/disable",
|
||||||
permission: "resource-group:update",
|
permission: "resource-group:update",
|
||||||
permissions: ["resource-group:update"]
|
permissions: ["resource-group:update"],
|
||||||
},
|
},
|
||||||
downloadJobArtifact: {
|
downloadJobArtifact: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.downloadJobArtifact,
|
operationId: API_OPERATIONS.downloadJobArtifact,
|
||||||
path: "/v1/jobs/{id}/artifact",
|
path: "/v1/jobs/{id}/artifact",
|
||||||
permission: "job:view",
|
permission: "job:view",
|
||||||
permissions: ["job:view"]
|
permissions: ["job:view"],
|
||||||
},
|
},
|
||||||
enableCountry: {
|
enableCountry: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.enableCountry,
|
operationId: API_OPERATIONS.enableCountry,
|
||||||
path: "/v1/admin/countries/{country_id}/enable",
|
path: "/v1/admin/countries/{country_id}/enable",
|
||||||
permission: "country:status",
|
permission: "country:status",
|
||||||
permissions: ["country:status"]
|
permissions: ["country:status"],
|
||||||
},
|
},
|
||||||
enableGift: {
|
enableGift: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.enableGift,
|
operationId: API_OPERATIONS.enableGift,
|
||||||
path: "/v1/admin/gifts/{gift_id}/enable",
|
path: "/v1/admin/gifts/{gift_id}/enable",
|
||||||
permission: "gift:status",
|
permission: "gift:status",
|
||||||
permissions: ["gift:status"]
|
permissions: ["gift:status"],
|
||||||
},
|
},
|
||||||
enableRegion: {
|
enableRegion: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.enableRegion,
|
operationId: API_OPERATIONS.enableRegion,
|
||||||
path: "/v1/admin/regions/{region_id}/enable",
|
path: "/v1/admin/regions/{region_id}/enable",
|
||||||
permission: "region:status",
|
permission: "region:status",
|
||||||
permissions: ["region:status"]
|
permissions: ["region:status"],
|
||||||
},
|
},
|
||||||
enableResource: {
|
enableResource: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.enableResource,
|
operationId: API_OPERATIONS.enableResource,
|
||||||
path: "/v1/admin/resources/{resource_id}/enable",
|
path: "/v1/admin/resources/{resource_id}/enable",
|
||||||
permission: "resource:update",
|
permission: "resource:update",
|
||||||
permissions: ["resource:update"]
|
permissions: ["resource:update"],
|
||||||
},
|
},
|
||||||
enableResourceGroup: {
|
enableResourceGroup: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.enableResourceGroup,
|
operationId: API_OPERATIONS.enableResourceGroup,
|
||||||
path: "/v1/admin/resource-groups/{group_id}/enable",
|
path: "/v1/admin/resource-groups/{group_id}/enable",
|
||||||
permission: "resource-group:update",
|
permission: "resource-group:update",
|
||||||
permissions: ["resource-group:update"]
|
permissions: ["resource-group:update"],
|
||||||
},
|
},
|
||||||
exportLoginLogs: {
|
exportLoginLogs: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.exportLoginLogs,
|
operationId: API_OPERATIONS.exportLoginLogs,
|
||||||
path: "/v1/logs/login/export",
|
path: "/v1/logs/login/export",
|
||||||
permission: "log:export",
|
permission: "log:export",
|
||||||
permissions: ["log:export"]
|
permissions: ["log:export"],
|
||||||
},
|
},
|
||||||
exportOperationLogs: {
|
exportOperationLogs: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.exportOperationLogs,
|
operationId: API_OPERATIONS.exportOperationLogs,
|
||||||
path: "/v1/logs/operations/export",
|
path: "/v1/logs/operations/export",
|
||||||
permission: "log:export",
|
permission: "log:export",
|
||||||
permissions: ["log:export"]
|
permissions: ["log:export"],
|
||||||
},
|
},
|
||||||
exportUsers: {
|
exportUsers: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.exportUsers,
|
operationId: API_OPERATIONS.exportUsers,
|
||||||
path: "/v1/users/export",
|
path: "/v1/users/export",
|
||||||
permission: "user:export",
|
permission: "user:export",
|
||||||
permissions: ["user:export"]
|
permissions: ["user:export"],
|
||||||
},
|
},
|
||||||
getCountry: {
|
getCountry: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.getCountry,
|
operationId: API_OPERATIONS.getCountry,
|
||||||
path: "/v1/admin/countries/{country_id}",
|
path: "/v1/admin/countries/{country_id}",
|
||||||
permission: "country:view",
|
permission: "country:view",
|
||||||
permissions: ["country:view"]
|
permissions: ["country:view"],
|
||||||
},
|
},
|
||||||
getRegion: {
|
getRegion: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.getRegion,
|
operationId: API_OPERATIONS.getRegion,
|
||||||
path: "/v1/admin/regions/{region_id}",
|
path: "/v1/admin/regions/{region_id}",
|
||||||
permission: "region:view",
|
permission: "region:view",
|
||||||
permissions: ["region:view"]
|
permissions: ["region:view"],
|
||||||
},
|
},
|
||||||
getResource: {
|
getResource: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.getResource,
|
operationId: API_OPERATIONS.getResource,
|
||||||
path: "/v1/admin/resources/{resource_id}",
|
path: "/v1/admin/resources/{resource_id}",
|
||||||
permission: "resource:view",
|
permission: "resource:view",
|
||||||
permissions: ["resource:view"]
|
permissions: ["resource:view"],
|
||||||
},
|
},
|
||||||
getResourceGroup: {
|
getResourceGroup: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.getResourceGroup,
|
operationId: API_OPERATIONS.getResourceGroup,
|
||||||
path: "/v1/admin/resource-groups/{group_id}",
|
path: "/v1/admin/resource-groups/{group_id}",
|
||||||
permission: "resource-group:view",
|
permission: "resource-group:view",
|
||||||
permissions: ["resource-group:view"]
|
permissions: ["resource-group:view"],
|
||||||
},
|
},
|
||||||
getRoleDataScopes: {
|
getRoleDataScopes: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.getRoleDataScopes,
|
operationId: API_OPERATIONS.getRoleDataScopes,
|
||||||
path: "/v1/roles/{id}/data-scopes",
|
path: "/v1/roles/{id}/data-scopes",
|
||||||
permission: "role:data-scope",
|
permission: "role:data-scope",
|
||||||
permissions: ["role:data-scope"]
|
permissions: ["role:data-scope"],
|
||||||
},
|
},
|
||||||
getUser: {
|
getUser: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.getUser,
|
operationId: API_OPERATIONS.getUser,
|
||||||
path: "/v1/users/{id}",
|
path: "/v1/users/{id}",
|
||||||
permission: "user:view",
|
permission: "user:view",
|
||||||
permissions: ["user:view"]
|
permissions: ["user:view"],
|
||||||
},
|
},
|
||||||
grantResource: {
|
grantResource: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.grantResource,
|
operationId: API_OPERATIONS.grantResource,
|
||||||
path: "/v1/admin/resource-grants/resource",
|
path: "/v1/admin/resource-grants/resource",
|
||||||
permission: "resource-grant:create",
|
permission: "resource-grant:create",
|
||||||
permissions: ["resource-grant:create"]
|
permissions: ["resource-grant:create"],
|
||||||
},
|
},
|
||||||
grantResourceGroup: {
|
grantResourceGroup: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.grantResourceGroup,
|
operationId: API_OPERATIONS.grantResourceGroup,
|
||||||
path: "/v1/admin/resource-grants/group",
|
path: "/v1/admin/resource-grants/group",
|
||||||
permission: "resource-grant:create",
|
permission: "resource-grant:create",
|
||||||
permissions: ["resource-grant:create"]
|
permissions: ["resource-grant:create"],
|
||||||
},
|
},
|
||||||
listAgencies: {
|
listAgencies: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listAgencies,
|
operationId: API_OPERATIONS.listAgencies,
|
||||||
path: "/v1/admin/agencies",
|
path: "/v1/admin/agencies",
|
||||||
permission: "agency:view",
|
permission: "agency:view",
|
||||||
permissions: ["agency:view"]
|
permissions: ["agency:view"],
|
||||||
},
|
},
|
||||||
listApps: {
|
listApps: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listApps,
|
operationId: API_OPERATIONS.listApps,
|
||||||
path: "/v1/admin/apps"
|
path: "/v1/admin/apps",
|
||||||
},
|
},
|
||||||
listBanners: {
|
listBanners: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listBanners,
|
operationId: API_OPERATIONS.listBanners,
|
||||||
path: "/v1/admin/app-config/banners",
|
path: "/v1/admin/app-config/banners",
|
||||||
permission: "app-config:view",
|
permission: "app-config:view",
|
||||||
permissions: ["app-config:view"]
|
permissions: ["app-config:view"],
|
||||||
},
|
},
|
||||||
listBDLeaders: {
|
listBDLeaders: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listBDLeaders,
|
operationId: API_OPERATIONS.listBDLeaders,
|
||||||
path: "/v1/admin/bd-leaders",
|
path: "/v1/admin/bd-leaders",
|
||||||
permission: "bd:view",
|
permission: "bd:view",
|
||||||
permissions: ["bd:view"]
|
permissions: ["bd:view"],
|
||||||
},
|
},
|
||||||
listBDs: {
|
listBDs: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listBDs,
|
operationId: API_OPERATIONS.listBDs,
|
||||||
path: "/v1/admin/bds",
|
path: "/v1/admin/bds",
|
||||||
permission: "bd:view",
|
permission: "bd:view",
|
||||||
permissions: ["bd:view"]
|
permissions: ["bd:view"],
|
||||||
},
|
},
|
||||||
listCoinSellers: {
|
listCoinSellers: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listCoinSellers,
|
operationId: API_OPERATIONS.listCoinSellers,
|
||||||
path: "/v1/admin/coin-sellers",
|
path: "/v1/admin/coin-sellers",
|
||||||
permission: "coin-seller:view",
|
permission: "coin-seller:view",
|
||||||
permissions: ["coin-seller:view"]
|
permissions: ["coin-seller:view"],
|
||||||
},
|
},
|
||||||
listCountries: {
|
listCountries: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listCountries,
|
operationId: API_OPERATIONS.listCountries,
|
||||||
path: "/v1/admin/countries",
|
path: "/v1/admin/countries",
|
||||||
permission: "country:view",
|
permission: "country:view",
|
||||||
permissions: ["country:view"]
|
permissions: ["country:view"],
|
||||||
},
|
},
|
||||||
listGifts: {
|
listGifts: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listGifts,
|
operationId: API_OPERATIONS.listGifts,
|
||||||
path: "/v1/admin/gifts",
|
path: "/v1/admin/gifts",
|
||||||
permission: "gift:view",
|
permission: "gift:view",
|
||||||
permissions: ["gift:view"]
|
permissions: ["gift:view"],
|
||||||
},
|
},
|
||||||
listH5Links: {
|
listH5Links: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listH5Links,
|
operationId: API_OPERATIONS.listH5Links,
|
||||||
path: "/v1/admin/app-config/h5-links",
|
path: "/v1/admin/app-config/h5-links",
|
||||||
permission: "app-config:view",
|
permission: "app-config:view",
|
||||||
permissions: ["app-config:view"]
|
permissions: ["app-config:view"],
|
||||||
},
|
},
|
||||||
listHosts: {
|
listHosts: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listHosts,
|
operationId: API_OPERATIONS.listHosts,
|
||||||
path: "/v1/admin/hosts",
|
path: "/v1/admin/hosts",
|
||||||
permission: "host:view",
|
permission: "host:view",
|
||||||
permissions: ["host:view"]
|
permissions: ["host:view"],
|
||||||
},
|
},
|
||||||
listJobs: {
|
listJobs: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listJobs,
|
operationId: API_OPERATIONS.listJobs,
|
||||||
path: "/v1/jobs",
|
path: "/v1/jobs",
|
||||||
permission: "job:view",
|
permission: "job:view",
|
||||||
permissions: ["job:view"]
|
permissions: ["job:view"],
|
||||||
},
|
},
|
||||||
listLoginLogs: {
|
listLoginLogs: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listLoginLogs,
|
operationId: API_OPERATIONS.listLoginLogs,
|
||||||
path: "/v1/logs/login",
|
path: "/v1/logs/login",
|
||||||
permission: "log:view",
|
permission: "log:view",
|
||||||
permissions: ["log:view"]
|
permissions: ["log:view"],
|
||||||
},
|
},
|
||||||
listNotifications: {
|
listNotifications: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listNotifications,
|
operationId: API_OPERATIONS.listNotifications,
|
||||||
path: "/v1/notifications",
|
path: "/v1/notifications",
|
||||||
permission: "notification:view",
|
permission: "notification:view",
|
||||||
permissions: ["notification:view"]
|
permissions: ["notification:view"],
|
||||||
},
|
},
|
||||||
listOperationLogs: {
|
listOperationLogs: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listOperationLogs,
|
operationId: API_OPERATIONS.listOperationLogs,
|
||||||
path: "/v1/logs/operations",
|
path: "/v1/logs/operations",
|
||||||
permission: "log:view",
|
permission: "log:view",
|
||||||
permissions: ["log:view"]
|
permissions: ["log:view"],
|
||||||
},
|
},
|
||||||
listPermissions: {
|
listPermissions: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listPermissions,
|
operationId: API_OPERATIONS.listPermissions,
|
||||||
path: "/v1/permissions",
|
path: "/v1/permissions",
|
||||||
permission: "permission:view",
|
permission: "permission:view",
|
||||||
permissions: ["permission:view","role:permission","role:manage"]
|
permissions: ["permission:view", "role:permission", "role:manage"],
|
||||||
},
|
},
|
||||||
listRechargeBills: {
|
listRechargeBills: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listRechargeBills,
|
operationId: API_OPERATIONS.listRechargeBills,
|
||||||
path: "/v1/admin/payment/recharge-bills",
|
path: "/v1/admin/payment/recharge-bills",
|
||||||
permission: "payment-bill:view",
|
permission: "payment-bill:view",
|
||||||
permissions: ["payment-bill:view"]
|
permissions: ["payment-bill:view"],
|
||||||
},
|
},
|
||||||
listRegions: {
|
listRegions: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listRegions,
|
operationId: API_OPERATIONS.listRegions,
|
||||||
path: "/v1/admin/regions",
|
path: "/v1/admin/regions",
|
||||||
permission: "region:view",
|
permission: "region:view",
|
||||||
permissions: ["region:view"]
|
permissions: ["region:view"],
|
||||||
},
|
},
|
||||||
listResourceGrants: {
|
listResourceGrants: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listResourceGrants,
|
operationId: API_OPERATIONS.listResourceGrants,
|
||||||
path: "/v1/admin/resource-grants",
|
path: "/v1/admin/resource-grants",
|
||||||
permission: "resource-grant:view",
|
permission: "resource-grant:view",
|
||||||
permissions: ["resource-grant:view"]
|
permissions: ["resource-grant:view"],
|
||||||
},
|
},
|
||||||
listResourceGroups: {
|
listResourceGroups: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listResourceGroups,
|
operationId: API_OPERATIONS.listResourceGroups,
|
||||||
path: "/v1/admin/resource-groups",
|
path: "/v1/admin/resource-groups",
|
||||||
permission: "resource-group:view",
|
permission: "resource-group:view",
|
||||||
permissions: ["resource-group:view"]
|
permissions: ["resource-group:view"],
|
||||||
},
|
},
|
||||||
listResources: {
|
listResources: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listResources,
|
operationId: API_OPERATIONS.listResources,
|
||||||
path: "/v1/admin/resources",
|
path: "/v1/admin/resources",
|
||||||
permission: "resource:view",
|
permission: "resource:view",
|
||||||
permissions: ["resource:view"]
|
permissions: ["resource:view"],
|
||||||
},
|
},
|
||||||
listRoles: {
|
listRoles: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listRoles,
|
operationId: API_OPERATIONS.listRoles,
|
||||||
path: "/v1/roles",
|
path: "/v1/roles",
|
||||||
permission: "role:view",
|
permission: "role:view",
|
||||||
permissions: ["role:view"]
|
permissions: ["role:view"],
|
||||||
},
|
},
|
||||||
listRooms: {
|
listRooms: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listRooms,
|
operationId: API_OPERATIONS.listRooms,
|
||||||
path: "/v1/admin/rooms",
|
path: "/v1/admin/rooms",
|
||||||
permission: "room:view",
|
permission: "room:view",
|
||||||
permissions: ["room:view"]
|
permissions: ["room:view"],
|
||||||
},
|
},
|
||||||
listSystemMenus: {
|
listSystemMenus: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listSystemMenus,
|
operationId: API_OPERATIONS.listSystemMenus,
|
||||||
path: "/v1/system/menus",
|
path: "/v1/system/menus",
|
||||||
permission: "menu:view",
|
permission: "menu:view",
|
||||||
permissions: ["menu:view"]
|
permissions: ["menu:view"],
|
||||||
},
|
},
|
||||||
listUsers: {
|
listUsers: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listUsers,
|
operationId: API_OPERATIONS.listUsers,
|
||||||
path: "/v1/users",
|
path: "/v1/users",
|
||||||
permission: "user:view",
|
permission: "user:view",
|
||||||
permissions: ["user:view"]
|
permissions: ["user:view"],
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.login,
|
operationId: API_OPERATIONS.login,
|
||||||
path: "/v1/auth/login"
|
path: "/v1/auth/login",
|
||||||
},
|
},
|
||||||
logout: {
|
logout: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.logout,
|
operationId: API_OPERATIONS.logout,
|
||||||
path: "/v1/auth/logout"
|
path: "/v1/auth/logout",
|
||||||
},
|
},
|
||||||
markAllNotificationsRead: {
|
markAllNotificationsRead: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.markAllNotificationsRead,
|
operationId: API_OPERATIONS.markAllNotificationsRead,
|
||||||
path: "/v1/notifications/read-all",
|
path: "/v1/notifications/read-all",
|
||||||
permission: "notification:read-all",
|
permission: "notification:read-all",
|
||||||
permissions: ["notification:read-all"]
|
permissions: ["notification:read-all"],
|
||||||
},
|
},
|
||||||
markNotificationRead: {
|
markNotificationRead: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.markNotificationRead,
|
operationId: API_OPERATIONS.markNotificationRead,
|
||||||
path: "/v1/notifications/{id}/read",
|
path: "/v1/notifications/{id}/read",
|
||||||
permission: "notification:read",
|
permission: "notification:read",
|
||||||
permissions: ["notification:read"]
|
permissions: ["notification:read"],
|
||||||
},
|
},
|
||||||
me: {
|
me: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.me,
|
operationId: API_OPERATIONS.me,
|
||||||
path: "/v1/auth/me"
|
path: "/v1/auth/me",
|
||||||
},
|
},
|
||||||
navigationMenus: {
|
navigationMenus: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.navigationMenus,
|
operationId: API_OPERATIONS.navigationMenus,
|
||||||
path: "/v1/navigation/menus"
|
path: "/v1/navigation/menus",
|
||||||
},
|
},
|
||||||
refresh: {
|
refresh: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.refresh,
|
operationId: API_OPERATIONS.refresh,
|
||||||
path: "/v1/auth/refresh"
|
path: "/v1/auth/refresh",
|
||||||
},
|
},
|
||||||
replaceRegionCountries: {
|
replaceRegionCountries: {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
operationId: API_OPERATIONS.replaceRegionCountries,
|
operationId: API_OPERATIONS.replaceRegionCountries,
|
||||||
path: "/v1/admin/regions/{region_id}/countries",
|
path: "/v1/admin/regions/{region_id}/countries",
|
||||||
permission: "region:update",
|
permission: "region:update",
|
||||||
permissions: ["region:update"]
|
permissions: ["region:update"],
|
||||||
},
|
},
|
||||||
replaceRoleDataScopes: {
|
replaceRoleDataScopes: {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
operationId: API_OPERATIONS.replaceRoleDataScopes,
|
operationId: API_OPERATIONS.replaceRoleDataScopes,
|
||||||
path: "/v1/roles/{id}/data-scopes",
|
path: "/v1/roles/{id}/data-scopes",
|
||||||
permission: "role:data-scope",
|
permission: "role:data-scope",
|
||||||
permissions: ["role:data-scope"]
|
permissions: ["role:data-scope"],
|
||||||
},
|
},
|
||||||
replaceRolePermissions: {
|
replaceRolePermissions: {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
operationId: API_OPERATIONS.replaceRolePermissions,
|
operationId: API_OPERATIONS.replaceRolePermissions,
|
||||||
path: "/v1/roles/{id}/permissions",
|
path: "/v1/roles/{id}/permissions",
|
||||||
permission: "role:permission",
|
permission: "role:permission",
|
||||||
permissions: ["role:permission","role:manage"]
|
permissions: ["role:permission", "role:manage"],
|
||||||
},
|
},
|
||||||
resetUserPassword: {
|
resetUserPassword: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.resetUserPassword,
|
operationId: API_OPERATIONS.resetUserPassword,
|
||||||
path: "/v1/users/{id}/reset-password",
|
path: "/v1/users/{id}/reset-password",
|
||||||
permission: "user:reset-password",
|
permission: "user:reset-password",
|
||||||
permissions: ["user:reset-password"]
|
permissions: ["user:reset-password"],
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.search,
|
operationId: API_OPERATIONS.search,
|
||||||
path: "/v1/search"
|
path: "/v1/search",
|
||||||
},
|
},
|
||||||
setAgencyJoinEnabled: {
|
setAgencyJoinEnabled: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.setAgencyJoinEnabled,
|
operationId: API_OPERATIONS.setAgencyJoinEnabled,
|
||||||
path: "/v1/admin/agencies/{agency_id}/join-enabled",
|
path: "/v1/admin/agencies/{agency_id}/join-enabled",
|
||||||
permission: "agency:status",
|
permission: "agency:status",
|
||||||
permissions: ["agency:status"]
|
permissions: ["agency:status"],
|
||||||
},
|
},
|
||||||
setBDLeaderStatus: {
|
setBDLeaderStatus: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.setBDLeaderStatus,
|
operationId: API_OPERATIONS.setBDLeaderStatus,
|
||||||
path: "/v1/admin/bd-leaders/{user_id}/status",
|
path: "/v1/admin/bd-leaders/{user_id}/status",
|
||||||
permission: "bd:update",
|
permission: "bd:update",
|
||||||
permissions: ["bd:update"]
|
permissions: ["bd:update"],
|
||||||
},
|
},
|
||||||
setBDStatus: {
|
setBDStatus: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.setBDStatus,
|
operationId: API_OPERATIONS.setBDStatus,
|
||||||
path: "/v1/admin/bds/{user_id}/status",
|
path: "/v1/admin/bds/{user_id}/status",
|
||||||
permission: "bd:update",
|
permission: "bd:update",
|
||||||
permissions: ["bd:update"]
|
permissions: ["bd:update"],
|
||||||
},
|
},
|
||||||
setCoinSellerStatus: {
|
setCoinSellerStatus: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.setCoinSellerStatus,
|
operationId: API_OPERATIONS.setCoinSellerStatus,
|
||||||
path: "/v1/admin/coin-sellers/{user_id}/status",
|
path: "/v1/admin/coin-sellers/{user_id}/status",
|
||||||
permission: "coin-seller:update",
|
permission: "coin-seller:update",
|
||||||
permissions: ["coin-seller:update"]
|
permissions: ["coin-seller:update"],
|
||||||
},
|
},
|
||||||
sortMenus: {
|
sortMenus: {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
operationId: API_OPERATIONS.sortMenus,
|
operationId: API_OPERATIONS.sortMenus,
|
||||||
path: "/v1/system/menus/sort",
|
path: "/v1/system/menus/sort",
|
||||||
permission: "menu:sort",
|
permission: "menu:sort",
|
||||||
permissions: ["menu:sort"]
|
permissions: ["menu:sort"],
|
||||||
},
|
},
|
||||||
syncPermissions: {
|
syncPermissions: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.syncPermissions,
|
operationId: API_OPERATIONS.syncPermissions,
|
||||||
path: "/v1/permissions/sync",
|
path: "/v1/permissions/sync",
|
||||||
permission: "permission:sync",
|
permission: "permission:sync",
|
||||||
permissions: ["permission:sync"]
|
permissions: ["permission:sync"],
|
||||||
},
|
},
|
||||||
updateBanner: {
|
updateBanner: {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
operationId: API_OPERATIONS.updateBanner,
|
operationId: API_OPERATIONS.updateBanner,
|
||||||
path: "/v1/admin/app-config/banners/{banner_id}",
|
path: "/v1/admin/app-config/banners/{banner_id}",
|
||||||
permission: "app-config:update",
|
permission: "app-config:update",
|
||||||
permissions: ["app-config:update"]
|
permissions: ["app-config:update"],
|
||||||
},
|
},
|
||||||
updateCountry: {
|
updateCountry: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.updateCountry,
|
operationId: API_OPERATIONS.updateCountry,
|
||||||
path: "/v1/admin/countries/{country_id}",
|
path: "/v1/admin/countries/{country_id}",
|
||||||
permission: "country:update",
|
permission: "country:update",
|
||||||
permissions: ["country:update"]
|
permissions: ["country:update"],
|
||||||
},
|
},
|
||||||
updateGift: {
|
updateGift: {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
operationId: API_OPERATIONS.updateGift,
|
operationId: API_OPERATIONS.updateGift,
|
||||||
path: "/v1/admin/gifts/{gift_id}",
|
path: "/v1/admin/gifts/{gift_id}",
|
||||||
permission: "gift:update",
|
permission: "gift:update",
|
||||||
permissions: ["gift:update"]
|
permissions: ["gift:update"],
|
||||||
},
|
},
|
||||||
updateH5Links: {
|
updateH5Links: {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
operationId: API_OPERATIONS.updateH5Links,
|
operationId: API_OPERATIONS.updateH5Links,
|
||||||
path: "/v1/admin/app-config/h5-links",
|
path: "/v1/admin/app-config/h5-links",
|
||||||
permission: "app-config:update",
|
permission: "app-config:update",
|
||||||
permissions: ["app-config:update"]
|
permissions: ["app-config:update"],
|
||||||
},
|
},
|
||||||
updateMenu: {
|
updateMenu: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.updateMenu,
|
operationId: API_OPERATIONS.updateMenu,
|
||||||
path: "/v1/system/menus/{id}",
|
path: "/v1/system/menus/{id}",
|
||||||
permission: "menu:update",
|
permission: "menu:update",
|
||||||
permissions: ["menu:update"]
|
permissions: ["menu:update"],
|
||||||
},
|
},
|
||||||
updateMenuVisible: {
|
updateMenuVisible: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.updateMenuVisible,
|
operationId: API_OPERATIONS.updateMenuVisible,
|
||||||
path: "/v1/system/menus/{id}/visible",
|
path: "/v1/system/menus/{id}/visible",
|
||||||
permission: "menu:visible",
|
permission: "menu:visible",
|
||||||
permissions: ["menu:visible"]
|
permissions: ["menu:visible"],
|
||||||
},
|
},
|
||||||
updatePermission: {
|
updatePermission: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.updatePermission,
|
operationId: API_OPERATIONS.updatePermission,
|
||||||
path: "/v1/permissions/{id}",
|
path: "/v1/permissions/{id}",
|
||||||
permission: "permission:update",
|
permission: "permission:update",
|
||||||
permissions: ["permission:update"]
|
permissions: ["permission:update"],
|
||||||
},
|
},
|
||||||
updateRegion: {
|
updateRegion: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.updateRegion,
|
operationId: API_OPERATIONS.updateRegion,
|
||||||
path: "/v1/admin/regions/{region_id}",
|
path: "/v1/admin/regions/{region_id}",
|
||||||
permission: "region:update",
|
permission: "region:update",
|
||||||
permissions: ["region:update"]
|
permissions: ["region:update"],
|
||||||
},
|
},
|
||||||
updateResource: {
|
updateResource: {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
operationId: API_OPERATIONS.updateResource,
|
operationId: API_OPERATIONS.updateResource,
|
||||||
path: "/v1/admin/resources/{resource_id}",
|
path: "/v1/admin/resources/{resource_id}",
|
||||||
permission: "resource:update",
|
permission: "resource:update",
|
||||||
permissions: ["resource:update"]
|
permissions: ["resource:update"],
|
||||||
},
|
},
|
||||||
updateResourceGroup: {
|
updateResourceGroup: {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
operationId: API_OPERATIONS.updateResourceGroup,
|
operationId: API_OPERATIONS.updateResourceGroup,
|
||||||
path: "/v1/admin/resource-groups/{group_id}",
|
path: "/v1/admin/resource-groups/{group_id}",
|
||||||
permission: "resource-group:update",
|
permission: "resource-group:update",
|
||||||
permissions: ["resource-group:update"]
|
permissions: ["resource-group:update"],
|
||||||
},
|
},
|
||||||
updateResourceGroupItems: {
|
updateResourceGroupItems: {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
operationId: API_OPERATIONS.updateResourceGroupItems,
|
operationId: API_OPERATIONS.updateResourceGroupItems,
|
||||||
path: "/v1/admin/resource-groups/{group_id}/items",
|
path: "/v1/admin/resource-groups/{group_id}/items",
|
||||||
permission: "resource-group:update",
|
permission: "resource-group:update",
|
||||||
permissions: ["resource-group:update"]
|
permissions: ["resource-group:update"],
|
||||||
},
|
},
|
||||||
updateRole: {
|
updateRole: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.updateRole,
|
operationId: API_OPERATIONS.updateRole,
|
||||||
path: "/v1/roles/{id}",
|
path: "/v1/roles/{id}",
|
||||||
permission: "role:update",
|
permission: "role:update",
|
||||||
permissions: ["role:update","role:manage"]
|
permissions: ["role:update", "role:manage"],
|
||||||
},
|
},
|
||||||
updateRoom: {
|
updateRoom: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.updateRoom,
|
operationId: API_OPERATIONS.updateRoom,
|
||||||
path: "/v1/admin/rooms/{room_id}",
|
path: "/v1/admin/rooms/{room_id}",
|
||||||
permission: "room:update",
|
permission: "room:update",
|
||||||
permissions: ["room:update"]
|
permissions: ["room:update"],
|
||||||
},
|
},
|
||||||
updateUser: {
|
updateUser: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.updateUser,
|
operationId: API_OPERATIONS.updateUser,
|
||||||
path: "/v1/users/{id}",
|
path: "/v1/users/{id}",
|
||||||
permission: "user:update",
|
permission: "user:update",
|
||||||
permissions: ["user:update"]
|
permissions: ["user:update"],
|
||||||
},
|
},
|
||||||
updateUserStatus: {
|
updateUserStatus: {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
operationId: API_OPERATIONS.updateUserStatus,
|
operationId: API_OPERATIONS.updateUserStatus,
|
||||||
path: "/v1/users/{id}/status",
|
path: "/v1/users/{id}/status",
|
||||||
permission: "user:status",
|
permission: "user:status",
|
||||||
permissions: ["user:status"]
|
permissions: ["user:status"],
|
||||||
},
|
},
|
||||||
uploadFile: {
|
uploadFile: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.uploadFile,
|
operationId: API_OPERATIONS.uploadFile,
|
||||||
path: "/v1/admin/files/upload",
|
path: "/v1/admin/files/upload",
|
||||||
permission: "upload:create",
|
permission: "upload:create",
|
||||||
permissions: ["upload:create"]
|
permissions: ["upload:create"],
|
||||||
},
|
},
|
||||||
uploadImage: {
|
uploadImage: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.uploadImage,
|
operationId: API_OPERATIONS.uploadImage,
|
||||||
path: "/v1/admin/files/image/upload",
|
path: "/v1/admin/files/image/upload",
|
||||||
permission: "upload:create",
|
permission: "upload:create",
|
||||||
permissions: ["upload:create"]
|
permissions: ["upload:create"],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const API_OPERATION_IDS = Object.values(API_OPERATIONS);
|
export const API_OPERATION_IDS = Object.values(API_OPERATIONS);
|
||||||
@ -922,8 +930,8 @@ export const API_PERMISSION_CODES = Array.from(
|
|||||||
new Set(
|
new Set(
|
||||||
Object.values(API_ENDPOINTS)
|
Object.values(API_ENDPOINTS)
|
||||||
.flatMap((endpoint) => endpoint.permissions || (endpoint.permission ? [endpoint.permission] : []))
|
.flatMap((endpoint) => endpoint.permissions || (endpoint.permission ? [endpoint.permission] : []))
|
||||||
.filter((permission): permission is PermissionCode => Boolean(permission))
|
.filter((permission): permission is PermissionCode => Boolean(permission)),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ApiPathParams = Record<string, number | string>;
|
export type ApiPathParams = Record<string, number | string>;
|
||||||
|
|||||||
30
src/shared/api/generated/schema.d.ts
vendored
30
src/shared/api/generated/schema.d.ts
vendored
@ -212,6 +212,22 @@ export interface paths {
|
|||||||
patch: operations["setCoinSellerStatus"];
|
patch: operations["setCoinSellerStatus"];
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/admin/coin-sellers/{user_id}/stock-credits": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["creditCoinSellerStock"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/admin/countries": {
|
"/admin/countries": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -2103,6 +2119,20 @@ export interface operations {
|
|||||||
200: components["responses"]["EmptyResponse"];
|
200: components["responses"]["EmptyResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
creditCoinSellerStock: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
user_id: number;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
listCountries: {
|
listCountries: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@ -217,14 +217,25 @@ export interface RechargeBillDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BDProfileDto {
|
export interface BDProfileDto {
|
||||||
|
avatar?: string;
|
||||||
createdAtMs?: number;
|
createdAtMs?: number;
|
||||||
|
createdByAvatar?: string;
|
||||||
|
createdByDisplayUserId?: string;
|
||||||
|
createdByUsername?: string;
|
||||||
createdByUserId?: number;
|
createdByUserId?: number;
|
||||||
|
displayUserId?: string;
|
||||||
parentLeaderUserId?: number;
|
parentLeaderUserId?: number;
|
||||||
|
parentLeaderDisplayUserId?: string;
|
||||||
|
parentLeaderUsername?: string;
|
||||||
|
parentLeaderAvatar?: string;
|
||||||
regionId?: number;
|
regionId?: number;
|
||||||
|
regionName?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
subBdCount?: number;
|
||||||
updatedAtMs?: number;
|
updatedAtMs?: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgencyDto {
|
export interface AgencyDto {
|
||||||
@ -234,27 +245,40 @@ export interface AgencyDto {
|
|||||||
joinEnabled?: boolean;
|
joinEnabled?: boolean;
|
||||||
maxHosts?: number;
|
maxHosts?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
ownerAvatar?: string;
|
||||||
|
ownerDisplayUserId?: string;
|
||||||
|
ownerUsername?: string;
|
||||||
ownerUserId?: number;
|
ownerUserId?: number;
|
||||||
|
parentBdAvatar?: string;
|
||||||
|
parentBdDisplayUserId?: string;
|
||||||
|
parentBdUsername?: string;
|
||||||
parentBdUserId?: number;
|
parentBdUserId?: number;
|
||||||
regionId?: number;
|
regionId?: number;
|
||||||
|
regionName?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
updatedAtMs?: number;
|
updatedAtMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HostProfileDto {
|
export interface HostProfileDto {
|
||||||
|
avatar?: string;
|
||||||
createdAtMs?: number;
|
createdAtMs?: number;
|
||||||
currentAgencyId?: number;
|
currentAgencyId?: number;
|
||||||
|
currentAgencyName?: string;
|
||||||
currentMembershipId?: number;
|
currentMembershipId?: number;
|
||||||
|
displayUserId?: string;
|
||||||
firstBecameHostAtMs?: number;
|
firstBecameHostAtMs?: number;
|
||||||
regionId?: number;
|
regionId?: number;
|
||||||
|
regionName?: string;
|
||||||
source?: string;
|
source?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
updatedAtMs?: number;
|
updatedAtMs?: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CoinSellerDto {
|
export interface CoinSellerDto {
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
contact?: string;
|
||||||
createdAtMs?: number;
|
createdAtMs?: number;
|
||||||
createdByUserId?: number;
|
createdByUserId?: number;
|
||||||
displayUserId?: string;
|
displayUserId?: string;
|
||||||
@ -268,6 +292,26 @@ export interface CoinSellerDto {
|
|||||||
username?: string;
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CoinSellerStockCreditPayload {
|
||||||
|
coinAmount: number;
|
||||||
|
commandId: string;
|
||||||
|
reason?: string;
|
||||||
|
rechargeAmount?: string;
|
||||||
|
type: "usdt_purchase" | "coin_compensation";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CoinSellerStockCreditDto {
|
||||||
|
balanceAfter: number;
|
||||||
|
coinAmount: number;
|
||||||
|
countsAsSellerRecharge: boolean;
|
||||||
|
createdAtMs: number;
|
||||||
|
paidAmountMicro: number;
|
||||||
|
paidCurrencyCode: string;
|
||||||
|
sellerUserId: string;
|
||||||
|
stockType: string;
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppUserDto {
|
export interface AppUserDto {
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
coin?: number;
|
coin?: number;
|
||||||
@ -440,10 +484,13 @@ export interface BDStatusPayload extends HostCommandPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateCoinSellerPayload extends HostCommandPayload {
|
export interface CreateCoinSellerPayload extends HostCommandPayload {
|
||||||
|
contact?: string;
|
||||||
targetUserId: number;
|
targetUserId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CoinSellerStatusPayload = BDStatusPayload;
|
export interface CoinSellerStatusPayload extends BDStatusPayload {
|
||||||
|
contact?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateAgencyPayload extends HostCommandPayload {
|
export interface CreateAgencyPayload extends HostCommandPayload {
|
||||||
joinEnabled: boolean;
|
joinEnabled: boolean;
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import Dialog from "@mui/material/Dialog";
|
|||||||
import DialogActions from "@mui/material/DialogActions";
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
import DialogContent from "@mui/material/DialogContent";
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
import DialogTitle from "@mui/material/DialogTitle";
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import InputAdornment from "@mui/material/InputAdornment";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
import { Button } from "@/shared/ui/Button.jsx";
|
import { Button } from "@/shared/ui/Button.jsx";
|
||||||
import styles from "@/shared/ui/AdminFormDialog.module.css";
|
import styles from "@/shared/ui/AdminFormDialog.module.css";
|
||||||
|
|
||||||
@ -66,6 +68,43 @@ export function AdminFormFieldGrid({ children, columns }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AdminFormReadOnlyField({ label, value }) {
|
||||||
|
const displayValue = value === undefined || value === null || value === "" ? "-" : value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.readOnlyField}>
|
||||||
|
<span className={styles.readOnlyLabel}>{label}</span>
|
||||||
|
<span className={styles.readOnlyValue}>{displayValue}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdminFormAmountField({ className = "", inputProps, InputProps, slotProps, unit, ...props }) {
|
||||||
|
const inputSlotProps = { ...(slotProps?.input || {}), ...(InputProps || {}) };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
{...props}
|
||||||
|
className={[styles.amountField, className].filter(Boolean).join(" ")}
|
||||||
|
slotProps={{
|
||||||
|
...slotProps,
|
||||||
|
htmlInput: { inputMode: "decimal", ...slotProps?.htmlInput, ...inputProps },
|
||||||
|
input: {
|
||||||
|
...inputSlotProps,
|
||||||
|
endAdornment: unit ? (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<span className={styles.inputUnit}>{unit}</span>
|
||||||
|
</InputAdornment>
|
||||||
|
) : (
|
||||||
|
inputSlotProps.endAdornment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type={props.type || "text"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function AdminFormInlineActions({ children }) {
|
export function AdminFormInlineActions({ children }) {
|
||||||
return <div className={styles.inlineActions}>{children}</div>;
|
return <div className={styles.inlineActions}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
width: min(var(--admin-form-dialog-width), calc(100vw - 32px)) !important;
|
width: min(var(--admin-form-dialog-width), calc(100vw - 32px)) !important;
|
||||||
max-width: min(var(--admin-form-dialog-width), calc(100vw - 32px)) !important;
|
max-width: min(var(--admin-form-dialog-width), calc(100vw - 32px)) !important;
|
||||||
border-radius: var(--radius-card);
|
border-radius: var(--radius-card);
|
||||||
|
box-shadow: var(--shadow-dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact {
|
.compact {
|
||||||
@ -77,9 +78,14 @@
|
|||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section + .section {
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
border-top: 1px solid var(--border-soft);
|
||||||
|
}
|
||||||
|
|
||||||
.sectionHeader {
|
.sectionHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: var(--control-height);
|
min-height: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -87,9 +93,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
color: var(--text-primary);
|
color: var(--text-secondary);
|
||||||
font-size: var(--admin-font-size);
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section > :global(.MuiTextField-root),
|
||||||
|
.section > :global(.MuiFormControl-root),
|
||||||
|
.section > :global(.MuiAutocomplete-root) {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionActions,
|
.sectionActions,
|
||||||
@ -109,10 +122,62 @@
|
|||||||
|
|
||||||
.fieldGrid > :global(.MuiTextField-root),
|
.fieldGrid > :global(.MuiTextField-root),
|
||||||
.fieldGrid > :global(.MuiFormControl-root),
|
.fieldGrid > :global(.MuiFormControl-root),
|
||||||
.fieldGrid > :global(.MuiAutocomplete-root) {
|
.fieldGrid > :global(.MuiAutocomplete-root),
|
||||||
|
.fieldGrid > .readOnlyField {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.readOnlyField {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 56px;
|
||||||
|
align-content: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: 8px var(--space-3);
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
background: var(--bg-card-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.readOnlyLabel {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readOnlyValue {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--admin-font-size);
|
||||||
|
font-weight: 760;
|
||||||
|
line-height: var(--admin-line-height);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amountField :global(.MuiOutlinedInput-root) {
|
||||||
|
background: var(--bg-input-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amountField :global(.MuiOutlinedInput-root.Mui-focused) {
|
||||||
|
box-shadow: 0 0 0 3px var(--focus-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amountField :global(.MuiInputBase-input) {
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputUnit {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
@ -135,8 +200,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
|
border-top: 1px solid var(--border-soft);
|
||||||
|
background: var(--bg-card);
|
||||||
padding: var(--space-3) var(--admin-form-content-x) var(--space-5);
|
padding: var(--space-3) var(--admin-form-content-x) var(--space-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
import RestartAltOutlined from "@mui/icons-material/RestartAltOutlined";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import { Button } from "@/shared/ui/Button.jsx";
|
||||||
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
||||||
import { SearchBox } from "@/shared/ui/SearchBox.jsx";
|
import { SearchBox } from "@/shared/ui/SearchBox.jsx";
|
||||||
import { StatusSelect } from "@/shared/ui/StatusSelect.jsx";
|
import { StatusSelect } from "@/shared/ui/StatusSelect.jsx";
|
||||||
@ -36,7 +38,22 @@ export function AdminSearchBox(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AdminFilterSelect(props) {
|
export function AdminFilterSelect(props) {
|
||||||
return <StatusSelect {...props} className={[styles.statusSelect, props.className || ""].filter(Boolean).join(" ")} />;
|
return (
|
||||||
|
<StatusSelect {...props} className={[styles.statusSelect, props.className || ""].filter(Boolean).join(" ")} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdminFilterResetButton({ disabled = false, label = "重置", onClick }) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={styles.resetButton}
|
||||||
|
disabled={disabled}
|
||||||
|
startIcon={<RestartAltOutlined fontSize="small" />}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdminActionIconButton({ children, label, primary = false, tooltip = label, ...props }) {
|
export function AdminActionIconButton({ children, label, primary = false, tooltip = label, ...props }) {
|
||||||
@ -65,6 +82,6 @@ const primaryActionSx = {
|
|||||||
"&:hover": {
|
"&:hover": {
|
||||||
borderColor: "var(--primary-strong)",
|
borderColor: "var(--primary-strong)",
|
||||||
backgroundColor: "var(--primary-strong)",
|
backgroundColor: "var(--primary-strong)",
|
||||||
color: "var(--active-contrast)"
|
color: "var(--active-contrast)",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -77,6 +77,10 @@
|
|||||||
flex: 0 0 150px;
|
flex: 0 0 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resetButton {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.listBlock {
|
.listBlock {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import SearchOutlined from "@mui/icons-material/SearchOutlined";
|
import SearchOutlined from "@mui/icons-material/SearchOutlined";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import MenuList from "@mui/material/MenuList";
|
||||||
import Popover from "@mui/material/Popover";
|
import Popover from "@mui/material/Popover";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { Button } from "@/shared/ui/Button.jsx";
|
||||||
|
|
||||||
const DEFAULT_COLUMN_WIDTH = "minmax(120px, 1fr)";
|
const DEFAULT_COLUMN_WIDTH = "minmax(120px, 1fr)";
|
||||||
const DEFAULT_RESIZE_MIN_WIDTH = 72;
|
const DEFAULT_RESIZE_MIN_WIDTH = 72;
|
||||||
@ -43,6 +45,7 @@ export function DataTable({
|
|||||||
const gridTemplate = preparedColumns.map((column) => column.width).join(" ");
|
const gridTemplate = preparedColumns.map((column) => column.width).join(" ");
|
||||||
const activeFilterColumn = preparedColumns.find((column) => column.key === filterMenu.columnKey && column.filter) || null;
|
const activeFilterColumn = preparedColumns.find((column) => column.key === filterMenu.columnKey && column.filter) || null;
|
||||||
const activeFilter = activeFilterColumn?.filter || null;
|
const activeFilter = activeFilterColumn?.filter || null;
|
||||||
|
const activeFilterIsText = activeFilter?.type === "text";
|
||||||
const filterOptions = useMemo(() => filterVisibleOptions(activeFilter, filterQuery), [activeFilter, filterQuery]);
|
const filterOptions = useMemo(() => filterVisibleOptions(activeFilter, filterQuery), [activeFilter, filterQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -112,7 +115,7 @@ export function DataTable({
|
|||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setFilterMenu({ anchorEl: event.currentTarget, columnKey: column.key });
|
setFilterMenu({ anchorEl: event.currentTarget, columnKey: column.key });
|
||||||
setFilterQuery("");
|
setFilterQuery(column.filter.type === "text" ? String(column.filter.value ?? "") : "");
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeColumnFilter = () => {
|
const closeColumnFilter = () => {
|
||||||
@ -125,6 +128,17 @@ export function DataTable({
|
|||||||
closeColumnFilter();
|
closeColumnFilter();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitTextFilter = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
activeFilter?.onChange?.(filterQuery.trim());
|
||||||
|
closeColumnFilter();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearTextFilter = () => {
|
||||||
|
activeFilter?.onChange?.("");
|
||||||
|
closeColumnFilter();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={["table-frame", className].filter(Boolean).join(" ")}>
|
<section className={["table-frame", className].filter(Boolean).join(" ")}>
|
||||||
{title || total !== undefined ? (
|
{title || total !== undefined ? (
|
||||||
@ -189,6 +203,27 @@ export function DataTable({
|
|||||||
onClose={closeColumnFilter}
|
onClose={closeColumnFilter}
|
||||||
>
|
>
|
||||||
{activeFilterColumn ? (
|
{activeFilterColumn ? (
|
||||||
|
activeFilterIsText ? (
|
||||||
|
<form className="admin-table-filter" onSubmit={submitTextFilter}>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
className="admin-table-filter__search"
|
||||||
|
disabled={activeFilter.loading}
|
||||||
|
placeholder={activeFilter.placeholder || `搜索${activeFilterColumn.label || ""}`}
|
||||||
|
size="small"
|
||||||
|
value={filterQuery}
|
||||||
|
onChange={(event) => setFilterQuery(event.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="admin-table-filter__actions">
|
||||||
|
<Button disabled={activeFilter.loading || !String(activeFilter.value ?? "").trim()} onClick={clearTextFilter}>
|
||||||
|
清空
|
||||||
|
</Button>
|
||||||
|
<Button disabled={activeFilter.loading} type="submit" variant="primary">
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
<div className="admin-table-filter">
|
<div className="admin-table-filter">
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
@ -199,7 +234,7 @@ export function DataTable({
|
|||||||
value={filterQuery}
|
value={filterQuery}
|
||||||
onChange={(event) => setFilterQuery(event.target.value)}
|
onChange={(event) => setFilterQuery(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="admin-table-filter__options">
|
<MenuList className="admin-table-filter__options">
|
||||||
{filterOptions.length ? (
|
{filterOptions.length ? (
|
||||||
filterOptions.map((option) => (
|
filterOptions.map((option) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -213,8 +248,9 @@ export function DataTable({
|
|||||||
) : (
|
) : (
|
||||||
<div className="admin-table-filter__empty">{activeFilter.loading ? "加载中..." : "无匹配选项"}</div>
|
<div className="admin-table-filter__empty">{activeFilter.loading ? "加载中..." : "无匹配选项"}</div>
|
||||||
)}
|
)}
|
||||||
|
</MenuList>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</Popover>
|
</Popover>
|
||||||
</section>
|
</section>
|
||||||
@ -232,6 +268,7 @@ function HeaderLabel({ column, onOpenFilter }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const active = isFilterActive(column.filter);
|
const active = isFilterActive(column.filter);
|
||||||
|
const filterValueLabel = active ? filterDisplayValue(column.filter) : "";
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={["admin-cell__head-trigger", active ? "admin-cell__head-trigger--active" : ""].filter(Boolean).join(" ")}
|
className={["admin-cell__head-trigger", active ? "admin-cell__head-trigger--active" : ""].filter(Boolean).join(" ")}
|
||||||
@ -239,7 +276,10 @@ function HeaderLabel({ column, onOpenFilter }) {
|
|||||||
onClick={(event) => onOpenFilter(event, column)}
|
onClick={(event) => onOpenFilter(event, column)}
|
||||||
>
|
>
|
||||||
<SearchOutlined className="admin-cell__head-icon" fontSize="inherit" />
|
<SearchOutlined className="admin-cell__head-icon" fontSize="inherit" />
|
||||||
<span className="admin-cell__head-label">{content}</span>
|
<span className="admin-cell__head-label">
|
||||||
|
{content}
|
||||||
|
{filterValueLabel ? <span className="admin-cell__head-filter">({filterValueLabel})</span> : null}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -320,6 +360,19 @@ function isFilterActive(filter) {
|
|||||||
return filter && filter.value !== undefined && filter.value !== null && String(filter.value) !== "";
|
return filter && filter.value !== undefined && filter.value !== null && String(filter.value) !== "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterDisplayValue(filter) {
|
||||||
|
const value = String(filter?.value ?? "");
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (filter?.type === "text") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedOption = normalizeFilterOptions(filter).find((option) => option.value === value);
|
||||||
|
return matchedOption?.label || value;
|
||||||
|
}
|
||||||
|
|
||||||
function filterVisibleOptions(filter, query) {
|
function filterVisibleOptions(filter, query) {
|
||||||
if (!filter) {
|
if (!filter) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import RestartAltOutlined from "@mui/icons-material/RestartAltOutlined";
|
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
|
||||||
import { IconButton } from "@/shared/ui/IconButton.jsx";
|
|
||||||
import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
|
import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
|
||||||
|
import { createOptionsColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||||
|
|
||||||
export function RegionFilterControl({
|
export function RegionFilterControl({
|
||||||
className = "",
|
className = "",
|
||||||
@ -10,14 +8,10 @@ export function RegionFilterControl({
|
|||||||
loading = false,
|
loading = false,
|
||||||
onChange,
|
onChange,
|
||||||
options = [],
|
options = [],
|
||||||
resetLabel = "重置区域",
|
|
||||||
selectClassName = "",
|
selectClassName = "",
|
||||||
value,
|
value,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
const selectedValue = value === undefined || value === null ? "" : String(value);
|
|
||||||
const canReset = selectedValue !== "" && !disabled && !loading;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={["region-filter-control", className].filter(Boolean).join(" ")}>
|
<div className={["region-filter-control", className].filter(Boolean).join(" ")}>
|
||||||
<RegionSelect
|
<RegionSelect
|
||||||
@ -30,18 +24,6 @@ export function RegionFilterControl({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
<Tooltip arrow title={resetLabel}>
|
|
||||||
<span>
|
|
||||||
<IconButton
|
|
||||||
className="region-filter-control__reset"
|
|
||||||
disabled={!canReset}
|
|
||||||
label={resetLabel}
|
|
||||||
onClick={() => onChange?.("")}
|
|
||||||
>
|
|
||||||
<RestartAltOutlined fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -52,14 +34,14 @@ export function createRegionColumnFilter({
|
|||||||
onChange,
|
onChange,
|
||||||
options = [],
|
options = [],
|
||||||
placeholder = "搜索区域",
|
placeholder = "搜索区域",
|
||||||
value
|
value,
|
||||||
}) {
|
}) {
|
||||||
return {
|
return createOptionsColumnFilter({
|
||||||
emptyLabel,
|
emptyLabel,
|
||||||
loading,
|
loading,
|
||||||
onChange,
|
onChange,
|
||||||
options,
|
options,
|
||||||
placeholder,
|
placeholder,
|
||||||
value
|
value,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/shared/ui/tableFilters.js
Normal file
20
src/shared/ui/tableFilters.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export function createTextColumnFilter({ loading = false, onChange, placeholder, value }) {
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
type: "text",
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOptionsColumnFilter({ emptyLabel, loading = false, onChange, options = [], placeholder, value }) {
|
||||||
|
return {
|
||||||
|
emptyLabel,
|
||||||
|
loading,
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
|
placeholder,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -130,11 +130,6 @@
|
|||||||
flex: 0 0 180px;
|
flex: 0 0 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.region-filter-control__reset.icon-button {
|
|
||||||
flex: 0 0 var(--control-height);
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.MuiInputBase-root.MuiOutlinedInput-root:not(.MuiInputBase-multiline) {
|
.MuiInputBase-root.MuiOutlinedInput-root:not(.MuiInputBase-multiline) {
|
||||||
height: var(--control-height);
|
height: var(--control-height);
|
||||||
min-height: var(--control-height);
|
min-height: var(--control-height);
|
||||||
@ -416,7 +411,6 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.kpi-card {
|
.kpi-card {
|
||||||
@ -677,9 +671,7 @@
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: var(--space-6);
|
padding: var(--space-6);
|
||||||
background:
|
background: var(--login-gradient), var(--bg-page);
|
||||||
var(--login-gradient),
|
|
||||||
var(--bg-page);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-panel {
|
.login-panel {
|
||||||
@ -918,7 +910,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-table {
|
.admin-table {
|
||||||
--admin-table-cell-x: var(--space-2);
|
--admin-table-cell-x: var(--space-4);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: max(100%, var(--admin-table-min-width, 900px));
|
width: max(100%, var(--admin-table-min-width, 900px));
|
||||||
@ -986,6 +978,11 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-cell__head-filter {
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-cell__head-trigger {
|
.admin-cell__head-trigger {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@ -1105,6 +1102,12 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-table-filter__actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
.admin-table-filter__options .MuiMenuItem-root {
|
.admin-table-filter__options .MuiMenuItem-root {
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
border-radius: var(--radius-control);
|
border-radius: var(--radius-control);
|
||||||
|
|||||||
11
src/theme.js
11
src/theme.js
@ -130,6 +130,13 @@ export const theme = createTheme({
|
|||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
"&[type='number']": {
|
||||||
|
MozAppearance: "textfield"
|
||||||
|
},
|
||||||
|
"&[type='number']::-webkit-outer-spin-button, &[type='number']::-webkit-inner-spin-button": {
|
||||||
|
WebkitAppearance: "none",
|
||||||
|
margin: 0
|
||||||
|
},
|
||||||
"&::placeholder": {
|
"&::placeholder": {
|
||||||
color: token("text-tertiary"),
|
color: token("text-tertiary"),
|
||||||
opacity: 1
|
opacity: 1
|
||||||
@ -237,6 +244,10 @@ export const theme = createTheme({
|
|||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
fontSize: 14
|
fontSize: 14
|
||||||
|
},
|
||||||
|
asterisk: {
|
||||||
|
color: token("danger"),
|
||||||
|
marginLeft: 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user