火箭,bi,币商相关
This commit is contained in:
parent
cf062aab5d
commit
945e2e7047
@ -446,6 +446,28 @@
|
||||
"x-permissions": ["agency:status"]
|
||||
}
|
||||
},
|
||||
"/admin/agencies/{agency_id}/delete": {
|
||||
"post": {
|
||||
"operationId": "deleteAgency",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "agency_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-permission": "agency:delete",
|
||||
"x-permissions": ["agency:delete"]
|
||||
}
|
||||
},
|
||||
"/admin/agencies/{agency_id}/join-enabled": {
|
||||
"post": {
|
||||
"operationId": "setAgencyJoinEnabled",
|
||||
|
||||
@ -7,14 +7,16 @@ import { formatDateTime } from "../utils/time.js";
|
||||
export function createDashboardModel(overview, { appCode, countryId, range }) {
|
||||
const source = overview || {};
|
||||
const comparisonCaption = isTodayRange(range) ? "与昨日相比" : "近 7 日";
|
||||
const recharge = readNumber(source, "recharge_usd_minor", "rechargeUsdMinor", "total_recharge_usd_minor", "totalRechargeUsdMinor");
|
||||
const recharge = effectiveRechargeUSDMinor(source);
|
||||
const newUserRecharge = readNumber(source, "new_user_recharge_usd_minor", "newUserRechargeUsdMinor");
|
||||
const activeUsers = readNumber(source, "active_users", "activeUsers");
|
||||
const paidUsers = readNumber(source, "paid_users", "paidUsers");
|
||||
const rechargeUsers = readNumber(source, "recharge_users", "rechargeUsers", "paid_users", "paidUsers");
|
||||
const newUsers = readNumber(source, "new_users", "newUsers", "visitors");
|
||||
const arpu = readNumber(source, "arpu_usd_minor", "arpuUsdMinor");
|
||||
const arppu = readNumber(source, "arppu_usd_minor", "arppuUsdMinor");
|
||||
const giftCoinSpent = readNumber(source, "gift_coin_spent", "giftCoinSpent");
|
||||
const coinSellerTransferCoin = readNumber(source, "coin_seller_transfer_coin", "coinSellerTransferCoin");
|
||||
const luckyGiftPools = normalizeLuckyGiftPools(source);
|
||||
const luckyGiftPoolTotals = summarizeLuckyGiftPools(luckyGiftPools);
|
||||
const luckyGiftTurnover = luckyGiftPools.length ? luckyGiftPoolTotals.turnover : readNumber(source, "room_lucky_gift_turnover", "roomLuckyGiftTurnover", "lucky_gift_turnover", "luckyGiftTurnover");
|
||||
@ -31,6 +33,7 @@ export function createDashboardModel(overview, { appCode, countryId, range }) {
|
||||
appCode,
|
||||
businessKpis: [
|
||||
coinMetric("礼物消费", giftCoinSpent, readDelta(source, "gift_coin_spent_delta_rate", "giftCoinSpentDeltaRate"), comparisonCaption),
|
||||
coinMetric("币商转账金币", coinSellerTransferCoin, readDelta(source, "coin_seller_transfer_coin_delta_rate", "coinSellerTransferCoinDeltaRate"), comparisonCaption),
|
||||
coinMetric("幸运礼物流水", luckyGiftTurnover, readDelta(source, "room_lucky_gift_turnover_delta_rate", "lucky_gift_turnover_delta_rate", "roomLuckyGiftTurnoverDeltaRate", "luckyGiftTurnoverDeltaRate"), comparisonCaption, { detailKey: "luckyGiftPools" }),
|
||||
coinMetric("幸运礼物返奖", luckyGiftPayout, readDelta(source, "lucky_gift_payout_delta_rate", "luckyGiftPayoutDeltaRate"), comparisonCaption, { detailKey: "luckyGiftPools" }),
|
||||
coinMetric("幸运礼物利润", luckyGiftProfit, readDelta(source, "lucky_gift_profit_delta_rate", "luckyGiftProfitDeltaRate"), comparisonCaption, { detailKey: "luckyGiftPools" }),
|
||||
@ -42,7 +45,7 @@ export function createDashboardModel(overview, { appCode, countryId, range }) {
|
||||
{ name: "访客", rate: 1, value: newUsers },
|
||||
{ name: "活跃用户", rate: ratio(activeUsers, newUsers), value: activeUsers },
|
||||
{ name: "付费用户", rate: ratio(paidUsers, activeUsers), value: paidUsers },
|
||||
{ name: "充值用户", rate: ratio(numberValue(source.lucky_gift_payers), activeUsers), value: numberValue(source.lucky_gift_payers) }
|
||||
{ name: "充值用户", rate: ratio(rechargeUsers, activeUsers), value: rechargeUsers }
|
||||
],
|
||||
gameMetrics: [
|
||||
{ caption: captionWithDelta(comparisonCaption, readDelta(source, "game_turnover_delta_rate", "gameTurnoverDeltaRate")), label: "流水", value: formatWholeMoney(source.game_turnover) },
|
||||
@ -145,23 +148,34 @@ function normalizeRevenueSeries(source) {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(source.daily_series) && source.daily_series.length) {
|
||||
return source.daily_series.map((item) => ({
|
||||
coin_seller: numberValue(item.coin_seller ?? item.coinSeller),
|
||||
google: numberValue(item.google),
|
||||
label: localizeSeriesLabel(item.label || item.stat_day || item.day || "-"),
|
||||
lineTotal: numberValue(item.line_total ?? item.lineTotal ?? item.trend_total ?? item.trendTotal ?? item.total ?? item.recharge_usd_minor),
|
||||
mifapay: numberValue(item.mifapay ?? item.mifa_pay),
|
||||
total: numberValue(item.total ?? item.recharge_usd_minor)
|
||||
}));
|
||||
return source.daily_series.map((item) => {
|
||||
const coinSeller = 0;
|
||||
const google = numberValue(item.google ?? item.google_recharge_usd_minor ?? item.googleRechargeUsdMinor);
|
||||
const mifapay = numberValue(item.mifapay ?? item.mifa_pay ?? item.mifapay_recharge_usd_minor ?? item.mifaPayRechargeUsdMinor);
|
||||
const channelTotal = google + mifapay;
|
||||
const total = Math.max(numberValue(item.total ?? item.recharge_usd_minor), channelTotal);
|
||||
return {
|
||||
coin_seller: coinSeller,
|
||||
google,
|
||||
label: localizeSeriesLabel(item.label || item.stat_day || item.day || "-"),
|
||||
lineTotal: Math.max(numberValue(item.line_total ?? item.lineTotal ?? item.trend_total ?? item.trendTotal ?? total), channelTotal),
|
||||
mifapay,
|
||||
total
|
||||
};
|
||||
});
|
||||
}
|
||||
const coinSeller = 0;
|
||||
const google = numberValue(source.google_recharge_usd_minor ?? source.googleRechargeUsdMinor);
|
||||
const mifapay = numberValue(source.mifapay_recharge_usd_minor ?? source.mifaPayRechargeUsdMinor);
|
||||
const total = effectiveRechargeUSDMinor(source);
|
||||
return [
|
||||
{
|
||||
coin_seller: numberValue(source.coin_seller_recharge_usd_minor),
|
||||
google: numberValue(source.google_recharge_usd_minor),
|
||||
coin_seller: coinSeller,
|
||||
google,
|
||||
label: "当前",
|
||||
lineTotal: numberValue(source.recharge_usd_minor),
|
||||
mifapay: numberValue(source.mifapay_recharge_usd_minor),
|
||||
total: numberValue(source.recharge_usd_minor)
|
||||
lineTotal: total,
|
||||
mifapay,
|
||||
total
|
||||
}
|
||||
];
|
||||
}
|
||||
@ -177,7 +191,7 @@ function normalizeCountries(source, countryId) {
|
||||
x: 56,
|
||||
y: 52
|
||||
}] : []);
|
||||
const totalRecharge = rows.reduce((sum, item) => sum + numberValue(item.recharge_usd_minor), 0) || 1;
|
||||
const totalRecharge = rows.reduce((sum, item) => sum + effectiveRechargeUSDMinor(item), 0) || 1;
|
||||
return rows
|
||||
.map((item, index) => ({
|
||||
...normalizeCountryRow(item, index, totalRecharge)
|
||||
@ -189,12 +203,13 @@ function normalizeCountryRow(item, index, totalRecharge) {
|
||||
const code = item.country_code || item.countryCode || item.iso_code || item.isoCode;
|
||||
const country = stripCountryFlagPrefix(item.country || item.country_name || code || `国家 ${index + 1}`);
|
||||
const meta = resolveCountryMeta({ code, name: country });
|
||||
const recharge = numberValue(item.recharge_usd_minor);
|
||||
const recharge = effectiveRechargeUSDMinor(item);
|
||||
return {
|
||||
active_users: numberValue(item.active_users),
|
||||
arpu_usd_minor: numberValue(item.arpu_usd_minor ?? item.arpuUsdMinor),
|
||||
arppu_usd_minor: numberValue(item.arppu_usd_minor),
|
||||
avg_recharge_usd_minor: numberValue(item.avg_recharge_usd_minor ?? item.avgRechargeUsdMinor),
|
||||
coin_seller_transfer_coin: numberValue(item.coin_seller_transfer_coin ?? item.coinSellerTransferCoin),
|
||||
country: meta?.label || country,
|
||||
country_code: code || meta?.code || "",
|
||||
flag: countryFlag(code || meta?.code),
|
||||
@ -204,13 +219,20 @@ function normalizeCountryRow(item, index, totalRecharge) {
|
||||
payer_rate: ratio(item.paid_users, item.active_users),
|
||||
recharge_usd_minor: recharge,
|
||||
recharge_conversion_rate: numberValue(item.recharge_conversion_rate ?? item.rechargeConversionRate),
|
||||
recharge_users: numberValue(item.recharge_users ?? item.rechargeUsers),
|
||||
recharge_users: numberValue(item.recharge_users ?? item.rechargeUsers ?? item.paid_users ?? item.paidUsers),
|
||||
share: recharge / totalRecharge,
|
||||
trend_rate: numberValue(item.trend_rate ?? item.trendRate),
|
||||
visitors: numberValue(item.visitors ?? item.new_users ?? item.newUsers)
|
||||
};
|
||||
}
|
||||
|
||||
function effectiveRechargeUSDMinor(source) {
|
||||
const direct = readNumber(source, "recharge_usd_minor", "rechargeUsdMinor", "total_recharge_usd_minor", "totalRechargeUsdMinor");
|
||||
const channelTotal = readNumber(source, "mifapay_recharge_usd_minor", "mifapayRechargeUsdMinor", "mifa_pay_recharge_usd_minor", "mifaPayRechargeUsdMinor") +
|
||||
readNumber(source, "google_recharge_usd_minor", "googleRechargeUsdMinor");
|
||||
return Math.max(direct, channelTotal);
|
||||
}
|
||||
|
||||
function normalizeGiftRanking(source) {
|
||||
if (Array.isArray(source.gift_ranking) && source.gift_ranking.length) {
|
||||
return source.gift_ranking.map((item) => ({
|
||||
|
||||
@ -22,6 +22,37 @@ test("uses lucky gift pool breakdown to aggregate business metrics", () => {
|
||||
expect(model.businessKpis.filter((item) => item.detailKey === "luckyGiftPools")).toHaveLength(3);
|
||||
});
|
||||
|
||||
test("keeps coin seller transfer out of USD recharge and exposes coin metric", () => {
|
||||
const model = createDashboardModel({
|
||||
coin_seller_recharge_usd_minor: 200,
|
||||
coin_seller_transfer_coin: 160_000,
|
||||
country_breakdown: [
|
||||
{
|
||||
coin_seller_recharge_usd_minor: 200,
|
||||
coin_seller_transfer_coin: 160_000,
|
||||
country: "阿富汗",
|
||||
google_recharge_usd_minor: 150,
|
||||
recharge_usd_minor: 150
|
||||
}
|
||||
],
|
||||
daily_series: [
|
||||
{
|
||||
coin_seller: 200,
|
||||
google: 150,
|
||||
label: "当前",
|
||||
recharge_usd_minor: 150
|
||||
}
|
||||
],
|
||||
google_recharge_usd_minor: 150,
|
||||
recharge_usd_minor: 150
|
||||
}, { appCode: "lalu", countryId: 0, range: { end: "2026-06-04", start: "2026-06-04" } });
|
||||
|
||||
expect(metricValue(model, "总充值")).toBe("$2");
|
||||
expect(metricValue(model, "币商转账金币")).toBe("160,000");
|
||||
expect(model.revenueSeries[0]).toEqual(expect.objectContaining({ coin_seller: 0, google: 150, lineTotal: 150, total: 150 }));
|
||||
expect(model.countryBreakdown[0]).toEqual(expect.objectContaining({ coin_seller_transfer_coin: 160_000, recharge_usd_minor: 150 }));
|
||||
});
|
||||
|
||||
function metricValue(model, label) {
|
||||
return model.businessKpis.find((item) => item.label === label)?.value;
|
||||
return [...model.kpis, ...model.businessKpis].find((item) => item.label === label)?.value;
|
||||
}
|
||||
|
||||
@ -636,18 +636,24 @@
|
||||
box-shadow: 0 0 14px rgba(247, 163, 58, 0.72);
|
||||
}
|
||||
|
||||
.metric-grid,
|
||||
.business-metric-grid {
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.business-metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.business-metric-grid .metric-card {
|
||||
height: var(--business-metric-row-height);
|
||||
grid-column: span 1;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
padding: 10px 16px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.business-metric-grid .metric-content {
|
||||
@ -693,7 +699,7 @@
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
margin-top: 3px;
|
||||
font-size: clamp(18px, 1vw, 20px);
|
||||
font-size: clamp(17px, 0.92vw, 20px);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ export const PERMISSIONS = {
|
||||
agencyView: "agency:view",
|
||||
agencyCreate: "agency:create",
|
||||
agencyStatus: "agency:status",
|
||||
agencyDelete: "agency:delete",
|
||||
bdView: "bd:view",
|
||||
bdCreate: "bd:create",
|
||||
bdUpdate: "bd:update",
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
createCoinSeller,
|
||||
createRegion,
|
||||
creditCoinSellerStock,
|
||||
deleteAgency,
|
||||
disableRegion,
|
||||
listAgencies,
|
||||
listBDs,
|
||||
@ -39,7 +40,6 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
||||
await createBDLeader({
|
||||
commandId: "bd-leader-test",
|
||||
positionAlias: "Senior Admin",
|
||||
regionId: 7,
|
||||
targetUserId: 1002,
|
||||
});
|
||||
await updateBDLeaderPositionAlias(1002, {
|
||||
@ -54,6 +54,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
||||
rechargeAmount: "100.000000",
|
||||
type: "usdt_purchase",
|
||||
});
|
||||
await deleteAgency(7001, { commandId: "agency-delete-test", reason: "cleanup" });
|
||||
|
||||
const [bdUrl, bdInit] = vi.mocked(fetch).mock.calls[0];
|
||||
const [agencyUrl] = vi.mocked(fetch).mock.calls[1];
|
||||
@ -64,6 +65,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
||||
const [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[6];
|
||||
const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[7];
|
||||
const [coinSellerStockUrl, coinSellerStockInit] = vi.mocked(fetch).mock.calls[8];
|
||||
const [agencyDeleteUrl, agencyDeleteInit] = vi.mocked(fetch).mock.calls[9];
|
||||
|
||||
expect(String(bdUrl)).toContain("/api/v1/admin/bds?");
|
||||
expect(String(bdUrl)).toContain("parent_leader_user_id=21");
|
||||
@ -83,6 +85,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
||||
expect(String(bdLeaderCreateUrl)).toContain("/api/v1/admin/bd-leaders");
|
||||
expect(bdLeaderCreateInit?.method).toBe("POST");
|
||||
expect(JSON.parse(String(bdLeaderCreateInit?.body))).toMatchObject({ positionAlias: "Senior Admin" });
|
||||
expect(JSON.parse(String(bdLeaderCreateInit?.body))).not.toHaveProperty("regionId");
|
||||
expect(String(bdLeaderAliasUrl)).toContain("/api/v1/admin/bd-leaders/1002/position-alias");
|
||||
expect(bdLeaderAliasInit?.method).toBe("PATCH");
|
||||
expect(JSON.parse(String(bdLeaderAliasInit?.body))).toMatchObject({ positionAlias: "Regional Admin" });
|
||||
@ -92,6 +95,9 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
||||
expect(coinSellerStatusInit?.method).toBe("PATCH");
|
||||
expect(String(coinSellerStockUrl)).toContain("/api/v1/admin/coin-sellers/1001/stock-credits");
|
||||
expect(coinSellerStockInit?.method).toBe("POST");
|
||||
expect(String(agencyDeleteUrl)).toContain("/api/v1/admin/agencies/7001/delete");
|
||||
expect(agencyDeleteInit?.method).toBe("POST");
|
||||
expect(JSON.parse(String(agencyDeleteInit?.body))).toMatchObject({ commandId: "agency-delete-test" });
|
||||
});
|
||||
|
||||
test("country and region APIs use generated admin paths", async () => {
|
||||
|
||||
@ -309,6 +309,17 @@ export function closeAgency(agencyId: EntityId, payload: HostCommandPayload): Pr
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteAgency(agencyId: EntityId, payload: HostCommandPayload): Promise<AgencyDto> {
|
||||
const endpoint = API_ENDPOINTS.deleteAgency;
|
||||
return apiRequest<AgencyDto, HostCommandPayload>(
|
||||
apiEndpointPath(API_OPERATIONS.deleteAgency, { agency_id: agencyId }),
|
||||
{
|
||||
body: payload,
|
||||
method: endpoint.method,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function setAgencyJoinEnabled(agencyId: EntityId, payload: AgencyJoinEnabledPayload): Promise<AgencyDto> {
|
||||
const endpoint = API_ENDPOINTS.setAgencyJoinEnabled;
|
||||
return apiRequest<AgencyDto, AgencyJoinEnabledPayload>(
|
||||
|
||||
@ -1,33 +1,27 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { parseForm } from "@/shared/forms/validation";
|
||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||
import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { closeAgency, createAgency, listAgencies, setAgencyJoinEnabled } from "@/features/host-org/api";
|
||||
import { closeAgency, createAgency, deleteAgency, listAgencies, setAgencyJoinEnabled } from "@/features/host-org/api";
|
||||
import { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||
import { useAgencyAbilities } from "@/features/host-org/permissions.js";
|
||||
import { agencyCloseSchema, agencyJoinEnabledSchema, createAgencySchema } from "@/features/host-org/schema";
|
||||
import { createAgencySchema } from "@/features/host-org/schema";
|
||||
|
||||
const emptyAgencyForm = () => ({
|
||||
commandId: makeCommandId("agency"),
|
||||
joinEnabled: true,
|
||||
maxHosts: 0,
|
||||
name: "",
|
||||
ownerUserId: "",
|
||||
parentBdUserId: "",
|
||||
reason: "",
|
||||
});
|
||||
const emptyCloseForm = () => ({ agencyId: "", commandId: makeCommandId("agency-close"), reason: "" });
|
||||
const emptyJoinEnabledForm = () => ({
|
||||
agencyId: "",
|
||||
commandId: makeCommandId("agency-join"),
|
||||
joinEnabled: true,
|
||||
reason: "",
|
||||
});
|
||||
const pageSize = 50;
|
||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||
|
||||
export function useHostAgenciesPage() {
|
||||
const abilities = useAgencyAbilities();
|
||||
const confirm = useConfirm();
|
||||
const { showToast } = useToast();
|
||||
const { loadingRegions, regionOptions } = useRegionOptions();
|
||||
const [query, setQuery] = useState("");
|
||||
@ -36,8 +30,8 @@ export function useHostAgenciesPage() {
|
||||
const [parentBdUserId, setParentBdUserId] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [agencyForm, setAgencyForm] = useState(emptyAgencyForm);
|
||||
const [closeForm, setCloseForm] = useState(emptyCloseForm);
|
||||
const [joinEnabledForm, setJoinEnabledForm] = useState(emptyJoinEnabledForm);
|
||||
const [rowPatches, setRowPatches] = useState({});
|
||||
const [removedAgencyIds, setRemovedAgencyIds] = useState(() => new Set());
|
||||
const [loadingAction, setLoadingAction] = useState("");
|
||||
const [activeAction, setActiveAction] = useState("");
|
||||
const filters = useMemo(
|
||||
@ -62,6 +56,26 @@ export function useHostAgenciesPage() {
|
||||
pageSize,
|
||||
queryKey: ["host-org", "agencies", filters, page],
|
||||
});
|
||||
const viewData = useMemo(() => {
|
||||
const rawItems = Array.isArray(data.items) ? data.items : [];
|
||||
const items = rawItems
|
||||
.filter((item) => !removedAgencyIds.has(String(item.agencyId)))
|
||||
.map((item) => ({ ...item, ...(rowPatches[String(item.agencyId)] || {}) }));
|
||||
const visibleRemovedCount = rawItems.length - items.length;
|
||||
|
||||
return {
|
||||
...data,
|
||||
items,
|
||||
total: Math.max(0, Number(data.total || 0) - visibleRemovedCount),
|
||||
};
|
||||
}, [data, removedAgencyIds, rowPatches]);
|
||||
|
||||
const patchAgencyRow = (agencyId, patch) => {
|
||||
setRowPatches((current) => {
|
||||
const key = String(agencyId);
|
||||
return { ...current, [key]: { ...(current[key] || {}), ...patch } };
|
||||
});
|
||||
};
|
||||
|
||||
const changeQuery = (value) => {
|
||||
setQuery(value);
|
||||
@ -103,41 +117,59 @@ export function useHostAgenciesPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const submitClose = async (event) => {
|
||||
event.preventDefault();
|
||||
await runAction("agency-close", "Agency 已关闭", async () => {
|
||||
const payload = parseForm(agencyCloseSchema, closeForm);
|
||||
const { agencyId, ...body } = payload;
|
||||
const data = await closeAgency(agencyId, body);
|
||||
setCloseForm(emptyCloseForm());
|
||||
setActiveAction("");
|
||||
await reload();
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
const closeAgencyFromList = async (agency) => {
|
||||
if (!abilities.canStatus || !agency?.agencyId || agency.status !== "active") {
|
||||
return;
|
||||
}
|
||||
await runAction(`agency-status-${agency.agencyId}`, "Agency 已关闭", async () => {
|
||||
await closeAgency(agency.agencyId, {
|
||||
const data = await closeAgency(agency.agencyId, {
|
||||
commandId: makeCommandId("agency-close"),
|
||||
reason: "close agency from list switch",
|
||||
});
|
||||
await reload();
|
||||
patchAgencyRow(agency.agencyId, data || { status: "closed", joinEnabled: false });
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
const submitJoinEnabled = async (event) => {
|
||||
event.preventDefault();
|
||||
await runAction("agency-join", "入会开关已更新", async () => {
|
||||
const payload = parseForm(agencyJoinEnabledSchema, joinEnabledForm);
|
||||
const { agencyId, ...body } = payload;
|
||||
const data = await setAgencyJoinEnabled(agencyId, body);
|
||||
setJoinEnabledForm(emptyJoinEnabledForm());
|
||||
setActiveAction("");
|
||||
await reload();
|
||||
const toggleAgencyJoinEnabledFromList = async (agency, joinEnabled) => {
|
||||
if (!abilities.canStatus || !agency?.agencyId || agency.status !== "active") {
|
||||
return;
|
||||
}
|
||||
await runAction(`agency-join-${agency.agencyId}`, "入会开关已更新", async () => {
|
||||
const data = await setAgencyJoinEnabled(agency.agencyId, {
|
||||
commandId: makeCommandId("agency-join"),
|
||||
joinEnabled,
|
||||
reason: "set agency join enabled from list switch",
|
||||
});
|
||||
patchAgencyRow(agency.agencyId, data || { joinEnabled });
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
const removeAgencyFromList = async (agency) => {
|
||||
if (!abilities.canDelete || !agency?.agencyId) {
|
||||
return;
|
||||
}
|
||||
const ok = await confirm({
|
||||
confirmText: "删除",
|
||||
message: `确认删除 Agency ${agency.name || agency.agencyId}?只删除 Agency 身份,不删除主播;下属主播会解除关联。`,
|
||||
title: "删除 Agency",
|
||||
tone: "danger",
|
||||
});
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
await runAction(`agency-delete-${agency.agencyId}`, "Agency 已删除", async () => {
|
||||
const data = await deleteAgency(agency.agencyId, {
|
||||
commandId: makeCommandId("agency-delete"),
|
||||
reason: "delete agency from list",
|
||||
});
|
||||
setRemovedAgencyIds((current) => {
|
||||
const next = new Set(current);
|
||||
next.add(String(agency.agencyId));
|
||||
return next;
|
||||
});
|
||||
return data;
|
||||
});
|
||||
};
|
||||
@ -163,10 +195,8 @@ export function useHostAgenciesPage() {
|
||||
changeRegionId,
|
||||
changeStatus,
|
||||
closeAgencyFromList,
|
||||
closeForm,
|
||||
data,
|
||||
data: viewData,
|
||||
error,
|
||||
joinEnabledForm,
|
||||
loading,
|
||||
loadingAction,
|
||||
loadingRegions,
|
||||
@ -179,16 +209,12 @@ export function useHostAgenciesPage() {
|
||||
resetFilters,
|
||||
closeAction: () => setActiveAction(""),
|
||||
openAgencyForm: () => setActiveAction("agency"),
|
||||
openCloseForm: () => setActiveAction("agency-close"),
|
||||
openJoinEnabledForm: () => setActiveAction("agency-join"),
|
||||
removeAgencyFromList,
|
||||
setAgencyForm,
|
||||
setCloseForm,
|
||||
setJoinEnabledForm,
|
||||
setPage,
|
||||
status,
|
||||
submitAgency,
|
||||
submitClose,
|
||||
submitJoinEnabled,
|
||||
toggleAgencyJoinEnabledFromList,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -22,7 +22,6 @@ const emptyBDLeaderForm = () => ({
|
||||
commandId: makeCommandId("bd-leader"),
|
||||
positionAlias: "",
|
||||
reason: "",
|
||||
regionId: "",
|
||||
targetUserId: "",
|
||||
});
|
||||
const emptyBDForm = () => ({ commandId: makeCommandId("bd"), parentLeaderUserId: "", reason: "", targetUserId: "" });
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import Add from "@mui/icons-material/Add";
|
||||
import BlockOutlined from "@mui/icons-material/BlockOutlined";
|
||||
import ToggleOnOutlined from "@mui/icons-material/ToggleOnOutlined";
|
||||
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
@ -11,6 +10,7 @@ import { HostOrgEntity, HostOrgPerson, hostOrgRegionName } from "@/features/host
|
||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
||||
import { useHostAgenciesPage } from "@/features/host-org/hooks/useHostAgenciesPage.js";
|
||||
import { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.js";
|
||||
import styles from "@/features/host-org/host-org.module.css";
|
||||
|
||||
@ -48,40 +48,29 @@ const agencyColumns = [
|
||||
{
|
||||
key: "joinEnabled",
|
||||
label: "入会",
|
||||
render: (item) => (
|
||||
<span className={item.joinEnabled ? styles.positive : styles.negative}>
|
||||
{item.joinEnabled ? "允许" : "禁止"}
|
||||
</span>
|
||||
),
|
||||
width: "minmax(80px, 0.6fr)",
|
||||
render: (item, _index, context) => <AgencyJoinSwitch item={item} page={context?.page} />,
|
||||
width: "minmax(90px, 0.65fr)",
|
||||
},
|
||||
{ key: "maxHosts", label: "最大主播数", width: "minmax(100px, 0.7fr)" },
|
||||
{
|
||||
key: "updatedAtMs",
|
||||
label: "更新时间",
|
||||
render: (item) => formatMillis(item.updatedAtMs),
|
||||
width: "minmax(170px, 1fr)",
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: "操作",
|
||||
render: (item, _index, context) => <AgencyActions item={item} page={context?.page} />,
|
||||
width: "minmax(80px, 0.45fr)",
|
||||
},
|
||||
];
|
||||
|
||||
export function HostAgenciesPage() {
|
||||
const page = useHostAgenciesPage();
|
||||
const createDisabled = !page.abilities.canCreate;
|
||||
const statusDisabled = !page.abilities.canStatus;
|
||||
const items = page.data.items || [];
|
||||
const total = page.data.total || 0;
|
||||
const toolbarActions = [
|
||||
page.abilities.canStatus
|
||||
? { icon: <ToggleOnOutlined fontSize="small" />, label: "入会开关", onClick: page.openJoinEnabledForm }
|
||||
: null,
|
||||
page.abilities.canStatus
|
||||
? {
|
||||
icon: <BlockOutlined fontSize="small" />,
|
||||
label: "关闭 Agency",
|
||||
onClick: page.openCloseForm,
|
||||
tone: "danger",
|
||||
}
|
||||
: null,
|
||||
page.abilities.canCreate
|
||||
? { icon: <Add fontSize="small" />, label: "创建 Agency", onClick: page.openAgencyForm, variant: "primary" }
|
||||
: null,
|
||||
@ -134,7 +123,7 @@ export function HostAgenciesPage() {
|
||||
columns={columns}
|
||||
context={{ page, regionOptions: page.regionOptions }}
|
||||
items={items}
|
||||
minWidth="1250px"
|
||||
minWidth="1180px"
|
||||
pagination={
|
||||
total > 0
|
||||
? {
|
||||
@ -182,13 +171,6 @@ export function HostAgenciesPage() {
|
||||
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 })}
|
||||
/>
|
||||
<AdminSwitch
|
||||
checked={page.agencyForm.joinEnabled}
|
||||
checkedLabel="允许"
|
||||
@ -208,77 +190,6 @@ export function HostAgenciesPage() {
|
||||
onChange={(event) => page.setAgencyForm({ ...page.agencyForm, reason: event.target.value })}
|
||||
/>
|
||||
</HostOrgActionModal>
|
||||
|
||||
<HostOrgActionModal
|
||||
disabled={statusDisabled}
|
||||
loading={page.loadingAction === "agency-join"}
|
||||
open={page.activeAction === "agency-join"}
|
||||
sectionTitle="入会设置"
|
||||
onClose={page.closeAction}
|
||||
onSubmit={page.submitJoinEnabled}
|
||||
title="入会开关"
|
||||
>
|
||||
<TextField
|
||||
disabled={statusDisabled}
|
||||
label="Agency ID"
|
||||
required
|
||||
type="number"
|
||||
value={page.joinEnabledForm.agencyId}
|
||||
onChange={(event) =>
|
||||
page.setJoinEnabledForm({ ...page.joinEnabledForm, agencyId: event.target.value })
|
||||
}
|
||||
/>
|
||||
<AdminSwitch
|
||||
checked={page.joinEnabledForm.joinEnabled}
|
||||
checkedLabel="允许"
|
||||
disabled={statusDisabled}
|
||||
label="入会状态"
|
||||
uncheckedLabel="禁止"
|
||||
onChange={(event) =>
|
||||
page.setJoinEnabledForm({ ...page.joinEnabledForm, joinEnabled: event.target.checked })
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
disabled={statusDisabled}
|
||||
label="原因"
|
||||
multiline
|
||||
minRows={2}
|
||||
value={page.joinEnabledForm.reason}
|
||||
onChange={(event) =>
|
||||
page.setJoinEnabledForm({ ...page.joinEnabledForm, reason: event.target.value })
|
||||
}
|
||||
/>
|
||||
</HostOrgActionModal>
|
||||
|
||||
<HostOrgActionModal
|
||||
disabled={statusDisabled}
|
||||
loading={page.loadingAction === "agency-close"}
|
||||
open={page.activeAction === "agency-close"}
|
||||
sectionTitle="关闭信息"
|
||||
onClose={page.closeAction}
|
||||
onSubmit={page.submitClose}
|
||||
submitLabel="确认关闭"
|
||||
submitVariant="danger"
|
||||
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
|
||||
disabled={statusDisabled}
|
||||
label="原因"
|
||||
multiline
|
||||
minRows={2}
|
||||
required
|
||||
value={page.closeForm.reason}
|
||||
onChange={(event) => page.setCloseForm({ ...page.closeForm, reason: event.target.value })}
|
||||
/>
|
||||
</HostOrgActionModal>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -320,3 +231,41 @@ function AgencyStatusSwitch({ item, page }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AgencyJoinSwitch({ item, page }) {
|
||||
const active = item.status === "active";
|
||||
return (
|
||||
<AdminSwitch
|
||||
checked={Boolean(item.joinEnabled)}
|
||||
disabled={!active || !page?.abilities.canStatus || page.loadingAction === `agency-join-${item.agencyId}`}
|
||||
inputProps={{ "aria-label": item.joinEnabled ? "禁止入会" : "允许入会" }}
|
||||
onChange={(event) => page.toggleAgencyJoinEnabledFromList(item, event.target.checked)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AgencyActions({ item, page }) {
|
||||
return (
|
||||
<AdminRowActions>
|
||||
{page?.abilities.canDelete ? (
|
||||
<AdminActionIconButton
|
||||
disabled={page.loadingAction === `agency-delete-${item.agencyId}`}
|
||||
label="删除 Agency"
|
||||
sx={dangerActionSx}
|
||||
onClick={() => page.removeAgencyFromList(item)}
|
||||
>
|
||||
<DeleteOutlineOutlined fontSize="small" />
|
||||
</AdminActionIconButton>
|
||||
) : null}
|
||||
</AdminRowActions>
|
||||
);
|
||||
}
|
||||
|
||||
const dangerActionSx = {
|
||||
borderColor: "var(--danger-border)",
|
||||
color: "var(--danger)",
|
||||
"&:hover": {
|
||||
borderColor: "var(--danger-border-strong)",
|
||||
backgroundColor: "var(--danger-surface)",
|
||||
},
|
||||
};
|
||||
|
||||
@ -12,7 +12,6 @@ import { HostOrgActionModal } from "@/features/host-org/components/HostOrgAction
|
||||
import { HostOrgStatus } from "@/features/host-org/components/HostOrgStatus.jsx";
|
||||
import { HostOrgTable } from "@/features/host-org/components/HostOrgTable.jsx";
|
||||
import { HostOrgToolbar } from "@/features/host-org/components/HostOrgToolbar.jsx";
|
||||
import { RegionSelect } from "@/shared/ui/RegionSelect.jsx";
|
||||
import { useHostBdsPage } from "@/features/host-org/hooks/useHostBdsPage.js";
|
||||
import styles from "@/features/host-org/host-org.module.css";
|
||||
|
||||
@ -135,15 +134,6 @@ export function HostBdLeadersPage() {
|
||||
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="职位别名"
|
||||
|
||||
@ -28,6 +28,7 @@ export function useAgencyAbilities() {
|
||||
|
||||
return {
|
||||
canCreate: can(PERMISSIONS.agencyCreate),
|
||||
canDelete: can(PERMISSIONS.agencyDelete),
|
||||
canStatus: can(PERMISSIONS.agencyStatus),
|
||||
canView: can(PERMISSIONS.agencyView),
|
||||
};
|
||||
|
||||
@ -70,7 +70,6 @@ const regionBaseSchema = z
|
||||
|
||||
export const createBDLeaderSchema = commandContactSchema.extend({
|
||||
positionAlias: optionalTextSchema(64, "职位别名不能超过 64 个字符"),
|
||||
regionId: regionIdSchema,
|
||||
targetUserId: userIdSchema,
|
||||
});
|
||||
|
||||
@ -116,7 +115,6 @@ export const coinSellerStockCreditSchema = z
|
||||
|
||||
export const createAgencySchema = commandBaseSchema.extend({
|
||||
joinEnabled: z.boolean().default(true),
|
||||
maxHosts: z.coerce.number().int().min(0, "最大主播数不能小于 0").default(0),
|
||||
name: z.string().trim().min(1, "请输入 Agency 名称").max(80, "Agency 名称不能超过 80 个字符"),
|
||||
ownerUserId: userIdSchema,
|
||||
parentBdUserId: optionalUserIdSchema,
|
||||
|
||||
@ -283,6 +283,33 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.resourceCell {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.resourceThumb {
|
||||
display: inline-flex;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-card-strong);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.resourceThumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.statusCell {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import Add from "@mui/icons-material/Add";
|
||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
||||
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
@ -646,10 +647,16 @@ function ResourceLabel({ id, resources }) {
|
||||
return <span className={styles.emptyValue}>未配置</span>;
|
||||
}
|
||||
const resource = resources.find((item) => Number(item.resourceId) === Number(id));
|
||||
const preview = resourceImageURL(resource);
|
||||
return (
|
||||
<div className={styles.stack}>
|
||||
<span className={styles.resourceText}>{resource?.name || resource?.resourceCode || `资源 #${id}`}</span>
|
||||
<span className={styles.meta}>{resource ? resourceMetaLabel(resource, id) : `ID ${id}`}</span>
|
||||
<div className={styles.resourceCell}>
|
||||
<span className={styles.resourceThumb}>
|
||||
{preview ? <img alt="" src={preview} /> : <Inventory2Outlined fontSize="small" />}
|
||||
</span>
|
||||
<div className={styles.stack}>
|
||||
<span className={styles.resourceText}>{resource?.name || resource?.resourceCode || `资源 #${id}`}</span>
|
||||
<span className={styles.meta}>{resource ? resourceMetaLabel(resource, id) : `ID ${id}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -700,6 +707,18 @@ function resourceMetaLabel(resource, id) {
|
||||
return `ID ${id}`;
|
||||
}
|
||||
|
||||
function resourceImageURL(resource) {
|
||||
return imageURL(resource?.previewUrl) || imageURL(resource?.assetUrl) || imageURL(resource?.animationUrl);
|
||||
}
|
||||
|
||||
function imageURL(value) {
|
||||
const url = String(value || "").trim();
|
||||
if (!url) {
|
||||
return "";
|
||||
}
|
||||
return /\.(avif|gif|jpe?g|png|webp)(\?|#|$)/i.test(url) ? url : "";
|
||||
}
|
||||
|
||||
function badgeFormFromMetadata(metadataJson) {
|
||||
if (!metadataJson) {
|
||||
return "";
|
||||
|
||||
@ -10,7 +10,11 @@ import { roomPinCreateSchema } from "@/features/rooms/schema";
|
||||
|
||||
const pageSize = 50;
|
||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||
const emptyForm = () => ({ durationDays: "30", roomId: "", weight: "0" });
|
||||
const emptyForm = () => {
|
||||
const start = new Date();
|
||||
const end = new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
return { expiresAtMs: formatDateTimeInputValue(end), pinType: "region", pinnedAtMs: formatDateTimeInputValue(start), roomId: "", weight: "0" };
|
||||
};
|
||||
|
||||
export function useRoomPinsPage() {
|
||||
const abilities = useRoomAbilities();
|
||||
@ -224,3 +228,18 @@ function roomOptionLabel(room) {
|
||||
}
|
||||
return [room.title, room.roomShortId, room.roomId].filter(Boolean).join(" / ");
|
||||
}
|
||||
|
||||
function formatDateTimeInputValue(date) {
|
||||
const pad = (value) => String(value).padStart(2, "0");
|
||||
return [
|
||||
date.getFullYear(),
|
||||
"-",
|
||||
pad(date.getMonth() + 1),
|
||||
"-",
|
||||
pad(date.getDate()),
|
||||
"T",
|
||||
pad(date.getHours()),
|
||||
":",
|
||||
pad(date.getMinutes()),
|
||||
].join("");
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import CloseOutlined from "@mui/icons-material/CloseOutlined";
|
||||
import MeetingRoomOutlined from "@mui/icons-material/MeetingRoomOutlined";
|
||||
import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { Button } from "@/shared/ui/Button.jsx";
|
||||
@ -24,6 +25,11 @@ const pinStatusOptions = [
|
||||
{ label: "全部", value: "all" },
|
||||
];
|
||||
|
||||
const pinTypeOptions = [
|
||||
{ label: "区域置顶", value: "region" },
|
||||
{ label: "全区置顶", value: "global" },
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: "room",
|
||||
@ -45,10 +51,22 @@ const columns = [
|
||||
},
|
||||
{
|
||||
key: "weight",
|
||||
label: "权重",
|
||||
label: "排序",
|
||||
width: "minmax(100px, 0.5fr)",
|
||||
render: (pin) => Number(pin.weight || 0).toLocaleString("zh-CN"),
|
||||
},
|
||||
{
|
||||
key: "pinType",
|
||||
label: "置顶类型",
|
||||
width: "minmax(130px, 0.7fr)",
|
||||
render: (pin) => pinTypeLabel(pin.pinType),
|
||||
},
|
||||
{
|
||||
key: "pinnedAt",
|
||||
label: "置顶时间",
|
||||
width: "minmax(170px, 0.9fr)",
|
||||
render: (pin) => formatMillis(pin.pinnedAtMs),
|
||||
},
|
||||
{
|
||||
key: "remaining",
|
||||
label: "剩余时间",
|
||||
@ -132,7 +150,7 @@ export function RoomPinsPage() {
|
||||
<DataTable
|
||||
columns={tableColumns}
|
||||
items={items}
|
||||
minWidth="1240px"
|
||||
minWidth="1480px"
|
||||
rowKey={(pin) => pin.id}
|
||||
pagination={
|
||||
total > 0
|
||||
@ -172,6 +190,7 @@ export function RoomPinsPage() {
|
||||
error={Boolean(page.roomOptionsError)}
|
||||
helperText={page.roomOptionsError}
|
||||
label="选择房间"
|
||||
placeholder="输入用户短ID / 房间ID"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
@ -193,18 +212,40 @@ export function RoomPinsPage() {
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="权重"
|
||||
label="置顶类型"
|
||||
required
|
||||
select
|
||||
value={page.form.pinType}
|
||||
onChange={(event) => page.setForm({ ...page.form, pinType: event.target.value })}
|
||||
>
|
||||
{pinTypeOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
label="排序"
|
||||
required
|
||||
type="number"
|
||||
value={page.form.weight}
|
||||
onChange={(event) => page.setForm({ ...page.form, weight: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="置顶时间(天)"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
label="开始时间"
|
||||
required
|
||||
type="number"
|
||||
value={page.form.durationDays}
|
||||
onChange={(event) => page.setForm({ ...page.form, durationDays: event.target.value })}
|
||||
type="datetime-local"
|
||||
value={page.form.pinnedAtMs}
|
||||
onChange={(event) => page.setForm({ ...page.form, pinnedAtMs: event.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
InputLabelProps={{ shrink: true }}
|
||||
label="结束时间"
|
||||
required
|
||||
type="datetime-local"
|
||||
value={page.form.expiresAtMs}
|
||||
onChange={(event) => page.setForm({ ...page.form, expiresAtMs: event.target.value })}
|
||||
/>
|
||||
</AdminFormDialog>
|
||||
</section>
|
||||
@ -287,3 +328,7 @@ function formatRemaining(pin) {
|
||||
}
|
||||
return `${Math.max(hours, 1)}小时`;
|
||||
}
|
||||
|
||||
function pinTypeLabel(pinType) {
|
||||
return pinType === "global" ? "全区置顶" : "区域置顶";
|
||||
}
|
||||
|
||||
@ -2,6 +2,22 @@ import { z } from "zod";
|
||||
|
||||
const optionalText = (max: number, message: string) => z.string().trim().max(max, message).optional().default("");
|
||||
const roomSeatCandidates = [10, 15, 20, 25, 30] as const;
|
||||
const pinTypeSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((value) => value === "region" || value === "global", "请选择置顶类型")
|
||||
.default("region");
|
||||
const dateTimeMsSchema = z.union([z.string(), z.number()]).transform((value, context) => {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
const ms = Date.parse(String(value || "").trim());
|
||||
if (!Number.isFinite(ms)) {
|
||||
context.addIssue({ code: z.ZodIssueCode.custom, message: "请选择置顶时间" });
|
||||
return z.NEVER;
|
||||
}
|
||||
return ms;
|
||||
});
|
||||
|
||||
export const roomUpdateSchema = z.object({
|
||||
coverUrl: optionalText(512, "房间头像不能超过 512 个字符"),
|
||||
@ -41,11 +57,18 @@ export const roomConfigSchema = z
|
||||
path: ["defaultSeatCount"],
|
||||
});
|
||||
|
||||
export const roomPinCreateSchema = z.object({
|
||||
durationDays: z.coerce.number().int().min(1, "请输入置顶天数").max(3650, "置顶时间不能超过 3650 天").default(30),
|
||||
roomId: z.string().trim().min(1, "请选择房间").max(64, "房间 ID 不正确"),
|
||||
weight: z.coerce.number().int().min(0, "权重不能小于 0").default(0),
|
||||
});
|
||||
export const roomPinCreateSchema = z
|
||||
.object({
|
||||
expiresAtMs: dateTimeMsSchema,
|
||||
pinType: pinTypeSchema,
|
||||
pinnedAtMs: dateTimeMsSchema,
|
||||
roomId: z.string().trim().min(1, "请选择房间").max(64, "房间 ID 不正确"),
|
||||
weight: z.coerce.number().int().min(0, "排序不能小于 0").default(0),
|
||||
})
|
||||
.refine((value) => value.expiresAtMs > value.pinnedAtMs, {
|
||||
message: "结束时间必须晚于开始时间",
|
||||
path: ["expiresAtMs"],
|
||||
});
|
||||
|
||||
export type RoomUpdateForm = z.infer<typeof roomUpdateSchema>;
|
||||
export type RoomStatusForm = z.infer<typeof roomStatusSchema>;
|
||||
|
||||
@ -56,6 +56,7 @@ export const API_OPERATIONS = {
|
||||
createUser: "createUser",
|
||||
creditCoinSellerStock: "creditCoinSellerStock",
|
||||
dashboardOverview: "dashboardOverview",
|
||||
deleteAgency: "deleteAgency",
|
||||
deleteAppVersion: "deleteAppVersion",
|
||||
deleteBanner: "deleteBanner",
|
||||
deleteBDLeader: "deleteBDLeader",
|
||||
@ -521,6 +522,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "overview:view",
|
||||
permissions: ["overview:view"]
|
||||
},
|
||||
deleteAgency: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.deleteAgency,
|
||||
path: "/v1/admin/agencies/{agency_id}/delete",
|
||||
permission: "agency:delete",
|
||||
permissions: ["agency:delete"]
|
||||
},
|
||||
deleteAppVersion: {
|
||||
method: "DELETE",
|
||||
operationId: API_OPERATIONS.deleteAppVersion,
|
||||
|
||||
30
src/shared/api/generated/schema.d.ts
vendored
30
src/shared/api/generated/schema.d.ts
vendored
@ -372,6 +372,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/agencies/{agency_id}/delete": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["deleteAgency"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/agencies/{agency_id}/join-enabled": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -3143,6 +3159,20 @@ export interface operations {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
deleteAgency: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
agency_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
setAgencyJoinEnabled: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@ -650,6 +650,7 @@ export interface RoomPinDto {
|
||||
createdByAdminId?: number;
|
||||
expiresAtMs?: number;
|
||||
id: EntityId;
|
||||
pinType?: string;
|
||||
pinnedAtMs?: number;
|
||||
remainingMs?: number;
|
||||
room: RoomPinRoomDto;
|
||||
@ -661,6 +662,9 @@ export interface RoomPinDto {
|
||||
|
||||
export interface RoomPinPayload {
|
||||
durationDays?: number;
|
||||
expiresAtMs?: number;
|
||||
pinType?: string;
|
||||
pinnedAtMs?: number;
|
||||
roomId: string;
|
||||
weight: number;
|
||||
}
|
||||
@ -816,7 +820,6 @@ export interface HostCommandPayload {
|
||||
|
||||
export interface CreateBDLeaderPayload extends HostCommandPayload {
|
||||
positionAlias?: string;
|
||||
regionId: number;
|
||||
targetUserId: number;
|
||||
}
|
||||
|
||||
@ -844,7 +847,6 @@ export interface CoinSellerStatusPayload extends BDStatusPayload {
|
||||
|
||||
export interface CreateAgencyPayload extends HostCommandPayload {
|
||||
joinEnabled: boolean;
|
||||
maxHosts?: number;
|
||||
name: string;
|
||||
ownerUserId: number;
|
||||
parentBdUserId: number;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user