火箭,bi,币商相关
This commit is contained in:
parent
cf062aab5d
commit
945e2e7047
@ -446,6 +446,28 @@
|
|||||||
"x-permissions": ["agency:status"]
|
"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": {
|
"/admin/agencies/{agency_id}/join-enabled": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "setAgencyJoinEnabled",
|
"operationId": "setAgencyJoinEnabled",
|
||||||
|
|||||||
@ -7,14 +7,16 @@ import { formatDateTime } from "../utils/time.js";
|
|||||||
export function createDashboardModel(overview, { appCode, countryId, range }) {
|
export function createDashboardModel(overview, { appCode, countryId, range }) {
|
||||||
const source = overview || {};
|
const source = overview || {};
|
||||||
const comparisonCaption = isTodayRange(range) ? "与昨日相比" : "近 7 日";
|
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 newUserRecharge = readNumber(source, "new_user_recharge_usd_minor", "newUserRechargeUsdMinor");
|
||||||
const activeUsers = readNumber(source, "active_users", "activeUsers");
|
const activeUsers = readNumber(source, "active_users", "activeUsers");
|
||||||
const paidUsers = readNumber(source, "paid_users", "paidUsers");
|
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 newUsers = readNumber(source, "new_users", "newUsers", "visitors");
|
||||||
const arpu = readNumber(source, "arpu_usd_minor", "arpuUsdMinor");
|
const arpu = readNumber(source, "arpu_usd_minor", "arpuUsdMinor");
|
||||||
const arppu = readNumber(source, "arppu_usd_minor", "arppuUsdMinor");
|
const arppu = readNumber(source, "arppu_usd_minor", "arppuUsdMinor");
|
||||||
const giftCoinSpent = readNumber(source, "gift_coin_spent", "giftCoinSpent");
|
const giftCoinSpent = readNumber(source, "gift_coin_spent", "giftCoinSpent");
|
||||||
|
const coinSellerTransferCoin = readNumber(source, "coin_seller_transfer_coin", "coinSellerTransferCoin");
|
||||||
const luckyGiftPools = normalizeLuckyGiftPools(source);
|
const luckyGiftPools = normalizeLuckyGiftPools(source);
|
||||||
const luckyGiftPoolTotals = summarizeLuckyGiftPools(luckyGiftPools);
|
const luckyGiftPoolTotals = summarizeLuckyGiftPools(luckyGiftPools);
|
||||||
const luckyGiftTurnover = luckyGiftPools.length ? luckyGiftPoolTotals.turnover : readNumber(source, "room_lucky_gift_turnover", "roomLuckyGiftTurnover", "lucky_gift_turnover", "luckyGiftTurnover");
|
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,
|
appCode,
|
||||||
businessKpis: [
|
businessKpis: [
|
||||||
coinMetric("礼物消费", giftCoinSpent, readDelta(source, "gift_coin_spent_delta_rate", "giftCoinSpentDeltaRate"), comparisonCaption),
|
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("幸运礼物流水", 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("幸运礼物返奖", 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" }),
|
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: 1, value: newUsers },
|
||||||
{ name: "活跃用户", rate: ratio(activeUsers, newUsers), value: activeUsers },
|
{ name: "活跃用户", rate: ratio(activeUsers, newUsers), value: activeUsers },
|
||||||
{ name: "付费用户", rate: ratio(paidUsers, activeUsers), value: paidUsers },
|
{ 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: [
|
gameMetrics: [
|
||||||
{ caption: captionWithDelta(comparisonCaption, readDelta(source, "game_turnover_delta_rate", "gameTurnoverDeltaRate")), label: "流水", value: formatWholeMoney(source.game_turnover) },
|
{ caption: captionWithDelta(comparisonCaption, readDelta(source, "game_turnover_delta_rate", "gameTurnoverDeltaRate")), label: "流水", value: formatWholeMoney(source.game_turnover) },
|
||||||
@ -145,23 +148,34 @@ function normalizeRevenueSeries(source) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (Array.isArray(source.daily_series) && source.daily_series.length) {
|
if (Array.isArray(source.daily_series) && source.daily_series.length) {
|
||||||
return source.daily_series.map((item) => ({
|
return source.daily_series.map((item) => {
|
||||||
coin_seller: numberValue(item.coin_seller ?? item.coinSeller),
|
const coinSeller = 0;
|
||||||
google: numberValue(item.google),
|
const google = numberValue(item.google ?? item.google_recharge_usd_minor ?? item.googleRechargeUsdMinor);
|
||||||
label: localizeSeriesLabel(item.label || item.stat_day || item.day || "-"),
|
const mifapay = numberValue(item.mifapay ?? item.mifa_pay ?? item.mifapay_recharge_usd_minor ?? item.mifaPayRechargeUsdMinor);
|
||||||
lineTotal: numberValue(item.line_total ?? item.lineTotal ?? item.trend_total ?? item.trendTotal ?? item.total ?? item.recharge_usd_minor),
|
const channelTotal = google + mifapay;
|
||||||
mifapay: numberValue(item.mifapay ?? item.mifa_pay),
|
const total = Math.max(numberValue(item.total ?? item.recharge_usd_minor), channelTotal);
|
||||||
total: numberValue(item.total ?? item.recharge_usd_minor)
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
coin_seller: numberValue(source.coin_seller_recharge_usd_minor),
|
coin_seller: coinSeller,
|
||||||
google: numberValue(source.google_recharge_usd_minor),
|
google,
|
||||||
label: "当前",
|
label: "当前",
|
||||||
lineTotal: numberValue(source.recharge_usd_minor),
|
lineTotal: total,
|
||||||
mifapay: numberValue(source.mifapay_recharge_usd_minor),
|
mifapay,
|
||||||
total: numberValue(source.recharge_usd_minor)
|
total
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -177,7 +191,7 @@ function normalizeCountries(source, countryId) {
|
|||||||
x: 56,
|
x: 56,
|
||||||
y: 52
|
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
|
return rows
|
||||||
.map((item, index) => ({
|
.map((item, index) => ({
|
||||||
...normalizeCountryRow(item, index, totalRecharge)
|
...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 code = item.country_code || item.countryCode || item.iso_code || item.isoCode;
|
||||||
const country = stripCountryFlagPrefix(item.country || item.country_name || code || `国家 ${index + 1}`);
|
const country = stripCountryFlagPrefix(item.country || item.country_name || code || `国家 ${index + 1}`);
|
||||||
const meta = resolveCountryMeta({ code, name: country });
|
const meta = resolveCountryMeta({ code, name: country });
|
||||||
const recharge = numberValue(item.recharge_usd_minor);
|
const recharge = effectiveRechargeUSDMinor(item);
|
||||||
return {
|
return {
|
||||||
active_users: numberValue(item.active_users),
|
active_users: numberValue(item.active_users),
|
||||||
arpu_usd_minor: numberValue(item.arpu_usd_minor ?? item.arpuUsdMinor),
|
arpu_usd_minor: numberValue(item.arpu_usd_minor ?? item.arpuUsdMinor),
|
||||||
arppu_usd_minor: numberValue(item.arppu_usd_minor),
|
arppu_usd_minor: numberValue(item.arppu_usd_minor),
|
||||||
avg_recharge_usd_minor: numberValue(item.avg_recharge_usd_minor ?? item.avgRechargeUsdMinor),
|
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: meta?.label || country,
|
||||||
country_code: code || meta?.code || "",
|
country_code: code || meta?.code || "",
|
||||||
flag: countryFlag(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),
|
payer_rate: ratio(item.paid_users, item.active_users),
|
||||||
recharge_usd_minor: recharge,
|
recharge_usd_minor: recharge,
|
||||||
recharge_conversion_rate: numberValue(item.recharge_conversion_rate ?? item.rechargeConversionRate),
|
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,
|
share: recharge / totalRecharge,
|
||||||
trend_rate: numberValue(item.trend_rate ?? item.trendRate),
|
trend_rate: numberValue(item.trend_rate ?? item.trendRate),
|
||||||
visitors: numberValue(item.visitors ?? item.new_users ?? item.newUsers)
|
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) {
|
function normalizeGiftRanking(source) {
|
||||||
if (Array.isArray(source.gift_ranking) && source.gift_ranking.length) {
|
if (Array.isArray(source.gift_ranking) && source.gift_ranking.length) {
|
||||||
return source.gift_ranking.map((item) => ({
|
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);
|
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) {
|
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);
|
box-shadow: 0 0 14px rgba(247, 163, 58, 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-grid,
|
.metric-grid {
|
||||||
.business-metric-grid {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.business-metric-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.business-metric-grid .metric-card {
|
.business-metric-grid .metric-card {
|
||||||
height: var(--business-metric-row-height);
|
height: var(--business-metric-row-height);
|
||||||
|
grid-column: span 1;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
padding: 10px 16px;
|
padding: 10px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.business-metric-grid .metric-content {
|
.business-metric-grid .metric-content {
|
||||||
@ -693,7 +699,7 @@
|
|||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
font-size: clamp(18px, 1vw, 20px);
|
font-size: clamp(17px, 0.92vw, 20px);
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export const PERMISSIONS = {
|
|||||||
agencyView: "agency:view",
|
agencyView: "agency:view",
|
||||||
agencyCreate: "agency:create",
|
agencyCreate: "agency:create",
|
||||||
agencyStatus: "agency:status",
|
agencyStatus: "agency:status",
|
||||||
|
agencyDelete: "agency:delete",
|
||||||
bdView: "bd:view",
|
bdView: "bd:view",
|
||||||
bdCreate: "bd:create",
|
bdCreate: "bd:create",
|
||||||
bdUpdate: "bd:update",
|
bdUpdate: "bd:update",
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
createCoinSeller,
|
createCoinSeller,
|
||||||
createRegion,
|
createRegion,
|
||||||
creditCoinSellerStock,
|
creditCoinSellerStock,
|
||||||
|
deleteAgency,
|
||||||
disableRegion,
|
disableRegion,
|
||||||
listAgencies,
|
listAgencies,
|
||||||
listBDs,
|
listBDs,
|
||||||
@ -39,7 +40,6 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
|||||||
await createBDLeader({
|
await createBDLeader({
|
||||||
commandId: "bd-leader-test",
|
commandId: "bd-leader-test",
|
||||||
positionAlias: "Senior Admin",
|
positionAlias: "Senior Admin",
|
||||||
regionId: 7,
|
|
||||||
targetUserId: 1002,
|
targetUserId: 1002,
|
||||||
});
|
});
|
||||||
await updateBDLeaderPositionAlias(1002, {
|
await updateBDLeaderPositionAlias(1002, {
|
||||||
@ -54,6 +54,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
|
|||||||
rechargeAmount: "100.000000",
|
rechargeAmount: "100.000000",
|
||||||
type: "usdt_purchase",
|
type: "usdt_purchase",
|
||||||
});
|
});
|
||||||
|
await deleteAgency(7001, { commandId: "agency-delete-test", reason: "cleanup" });
|
||||||
|
|
||||||
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];
|
||||||
@ -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 [coinSellerCreateUrl, coinSellerCreateInit] = vi.mocked(fetch).mock.calls[6];
|
||||||
const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[7];
|
const [coinSellerStatusUrl, coinSellerStatusInit] = vi.mocked(fetch).mock.calls[7];
|
||||||
const [coinSellerStockUrl, coinSellerStockInit] = vi.mocked(fetch).mock.calls[8];
|
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("/api/v1/admin/bds?");
|
||||||
expect(String(bdUrl)).toContain("parent_leader_user_id=21");
|
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(String(bdLeaderCreateUrl)).toContain("/api/v1/admin/bd-leaders");
|
||||||
expect(bdLeaderCreateInit?.method).toBe("POST");
|
expect(bdLeaderCreateInit?.method).toBe("POST");
|
||||||
expect(JSON.parse(String(bdLeaderCreateInit?.body))).toMatchObject({ positionAlias: "Senior Admin" });
|
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(String(bdLeaderAliasUrl)).toContain("/api/v1/admin/bd-leaders/1002/position-alias");
|
||||||
expect(bdLeaderAliasInit?.method).toBe("PATCH");
|
expect(bdLeaderAliasInit?.method).toBe("PATCH");
|
||||||
expect(JSON.parse(String(bdLeaderAliasInit?.body))).toMatchObject({ positionAlias: "Regional Admin" });
|
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(coinSellerStatusInit?.method).toBe("PATCH");
|
||||||
expect(String(coinSellerStockUrl)).toContain("/api/v1/admin/coin-sellers/1001/stock-credits");
|
expect(String(coinSellerStockUrl)).toContain("/api/v1/admin/coin-sellers/1001/stock-credits");
|
||||||
expect(coinSellerStockInit?.method).toBe("POST");
|
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 () => {
|
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> {
|
export function setAgencyJoinEnabled(agencyId: EntityId, payload: AgencyJoinEnabledPayload): Promise<AgencyDto> {
|
||||||
const endpoint = API_ENDPOINTS.setAgencyJoinEnabled;
|
const endpoint = API_ENDPOINTS.setAgencyJoinEnabled;
|
||||||
return apiRequest<AgencyDto, AgencyJoinEnabledPayload>(
|
return apiRequest<AgencyDto, AgencyJoinEnabledPayload>(
|
||||||
|
|||||||
@ -1,33 +1,27 @@
|
|||||||
import { useMemo, useState } from "react";
|
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 { useConfirm } from "@/shared/ui/ConfirmProvider.jsx";
|
||||||
import { useToast } from "@/shared/ui/ToastProvider.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 { useRegionOptions } from "@/shared/hooks/useRegionOptions.js";
|
||||||
import { useAgencyAbilities } from "@/features/host-org/permissions.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 = () => ({
|
const emptyAgencyForm = () => ({
|
||||||
commandId: makeCommandId("agency"),
|
commandId: makeCommandId("agency"),
|
||||||
joinEnabled: true,
|
joinEnabled: true,
|
||||||
maxHosts: 0,
|
|
||||||
name: "",
|
name: "",
|
||||||
ownerUserId: "",
|
ownerUserId: "",
|
||||||
parentBdUserId: "",
|
parentBdUserId: "",
|
||||||
reason: "",
|
reason: "",
|
||||||
});
|
});
|
||||||
const emptyCloseForm = () => ({ agencyId: "", commandId: makeCommandId("agency-close"), reason: "" });
|
|
||||||
const emptyJoinEnabledForm = () => ({
|
|
||||||
agencyId: "",
|
|
||||||
commandId: makeCommandId("agency-join"),
|
|
||||||
joinEnabled: true,
|
|
||||||
reason: "",
|
|
||||||
});
|
|
||||||
const pageSize = 50;
|
const pageSize = 50;
|
||||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
||||||
|
|
||||||
export function useHostAgenciesPage() {
|
export function useHostAgenciesPage() {
|
||||||
const abilities = useAgencyAbilities();
|
const abilities = useAgencyAbilities();
|
||||||
|
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("");
|
||||||
@ -36,8 +30,8 @@ export function useHostAgenciesPage() {
|
|||||||
const [parentBdUserId, setParentBdUserId] = useState("");
|
const [parentBdUserId, setParentBdUserId] = useState("");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [agencyForm, setAgencyForm] = useState(emptyAgencyForm);
|
const [agencyForm, setAgencyForm] = useState(emptyAgencyForm);
|
||||||
const [closeForm, setCloseForm] = useState(emptyCloseForm);
|
const [rowPatches, setRowPatches] = useState({});
|
||||||
const [joinEnabledForm, setJoinEnabledForm] = useState(emptyJoinEnabledForm);
|
const [removedAgencyIds, setRemovedAgencyIds] = useState(() => new Set());
|
||||||
const [loadingAction, setLoadingAction] = useState("");
|
const [loadingAction, setLoadingAction] = useState("");
|
||||||
const [activeAction, setActiveAction] = useState("");
|
const [activeAction, setActiveAction] = useState("");
|
||||||
const filters = useMemo(
|
const filters = useMemo(
|
||||||
@ -62,6 +56,26 @@ export function useHostAgenciesPage() {
|
|||||||
pageSize,
|
pageSize,
|
||||||
queryKey: ["host-org", "agencies", filters, page],
|
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) => {
|
const changeQuery = (value) => {
|
||||||
setQuery(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) => {
|
const closeAgencyFromList = async (agency) => {
|
||||||
if (!abilities.canStatus || !agency?.agencyId || agency.status !== "active") {
|
if (!abilities.canStatus || !agency?.agencyId || agency.status !== "active") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await runAction(`agency-status-${agency.agencyId}`, "Agency 已关闭", async () => {
|
await runAction(`agency-status-${agency.agencyId}`, "Agency 已关闭", async () => {
|
||||||
await closeAgency(agency.agencyId, {
|
const data = await closeAgency(agency.agencyId, {
|
||||||
commandId: makeCommandId("agency-close"),
|
commandId: makeCommandId("agency-close"),
|
||||||
reason: "close agency from list switch",
|
reason: "close agency from list switch",
|
||||||
});
|
});
|
||||||
await reload();
|
patchAgencyRow(agency.agencyId, data || { status: "closed", joinEnabled: false });
|
||||||
|
return data;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitJoinEnabled = async (event) => {
|
const toggleAgencyJoinEnabledFromList = async (agency, joinEnabled) => {
|
||||||
event.preventDefault();
|
if (!abilities.canStatus || !agency?.agencyId || agency.status !== "active") {
|
||||||
await runAction("agency-join", "入会开关已更新", async () => {
|
return;
|
||||||
const payload = parseForm(agencyJoinEnabledSchema, joinEnabledForm);
|
}
|
||||||
const { agencyId, ...body } = payload;
|
await runAction(`agency-join-${agency.agencyId}`, "入会开关已更新", async () => {
|
||||||
const data = await setAgencyJoinEnabled(agencyId, body);
|
const data = await setAgencyJoinEnabled(agency.agencyId, {
|
||||||
setJoinEnabledForm(emptyJoinEnabledForm());
|
commandId: makeCommandId("agency-join"),
|
||||||
setActiveAction("");
|
joinEnabled,
|
||||||
await reload();
|
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;
|
return data;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -163,10 +195,8 @@ export function useHostAgenciesPage() {
|
|||||||
changeRegionId,
|
changeRegionId,
|
||||||
changeStatus,
|
changeStatus,
|
||||||
closeAgencyFromList,
|
closeAgencyFromList,
|
||||||
closeForm,
|
data: viewData,
|
||||||
data,
|
|
||||||
error,
|
error,
|
||||||
joinEnabledForm,
|
|
||||||
loading,
|
loading,
|
||||||
loadingAction,
|
loadingAction,
|
||||||
loadingRegions,
|
loadingRegions,
|
||||||
@ -179,16 +209,12 @@ export function useHostAgenciesPage() {
|
|||||||
resetFilters,
|
resetFilters,
|
||||||
closeAction: () => setActiveAction(""),
|
closeAction: () => setActiveAction(""),
|
||||||
openAgencyForm: () => setActiveAction("agency"),
|
openAgencyForm: () => setActiveAction("agency"),
|
||||||
openCloseForm: () => setActiveAction("agency-close"),
|
removeAgencyFromList,
|
||||||
openJoinEnabledForm: () => setActiveAction("agency-join"),
|
|
||||||
setAgencyForm,
|
setAgencyForm,
|
||||||
setCloseForm,
|
|
||||||
setJoinEnabledForm,
|
|
||||||
setPage,
|
setPage,
|
||||||
status,
|
status,
|
||||||
submitAgency,
|
submitAgency,
|
||||||
submitClose,
|
toggleAgencyJoinEnabledFromList,
|
||||||
submitJoinEnabled,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,6 @@ const emptyBDLeaderForm = () => ({
|
|||||||
commandId: makeCommandId("bd-leader"),
|
commandId: makeCommandId("bd-leader"),
|
||||||
positionAlias: "",
|
positionAlias: "",
|
||||||
reason: "",
|
reason: "",
|
||||||
regionId: "",
|
|
||||||
targetUserId: "",
|
targetUserId: "",
|
||||||
});
|
});
|
||||||
const emptyBDForm = () => ({ commandId: makeCommandId("bd"), parentLeaderUserId: "", reason: "", targetUserId: "" });
|
const emptyBDForm = () => ({ commandId: makeCommandId("bd"), parentLeaderUserId: "", reason: "", targetUserId: "" });
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import Add from "@mui/icons-material/Add";
|
import Add from "@mui/icons-material/Add";
|
||||||
import BlockOutlined from "@mui/icons-material/BlockOutlined";
|
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||||
import ToggleOnOutlined from "@mui/icons-material/ToggleOnOutlined";
|
|
||||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||||
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";
|
||||||
@ -11,6 +10,7 @@ import { HostOrgEntity, HostOrgPerson, hostOrgRegionName } from "@/features/host
|
|||||||
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 { AdminActionIconButton, AdminRowActions } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { createOptionsColumnFilter, createTextColumnFilter } from "@/shared/ui/tableFilters.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";
|
||||||
|
|
||||||
@ -48,40 +48,29 @@ const agencyColumns = [
|
|||||||
{
|
{
|
||||||
key: "joinEnabled",
|
key: "joinEnabled",
|
||||||
label: "入会",
|
label: "入会",
|
||||||
render: (item) => (
|
render: (item, _index, context) => <AgencyJoinSwitch item={item} page={context?.page} />,
|
||||||
<span className={item.joinEnabled ? styles.positive : styles.negative}>
|
width: "minmax(90px, 0.65fr)",
|
||||||
{item.joinEnabled ? "允许" : "禁止"}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
width: "minmax(80px, 0.6fr)",
|
|
||||||
},
|
},
|
||||||
{ key: "maxHosts", label: "最大主播数", width: "minmax(100px, 0.7fr)" },
|
|
||||||
{
|
{
|
||||||
key: "updatedAtMs",
|
key: "updatedAtMs",
|
||||||
label: "更新时间",
|
label: "更新时间",
|
||||||
render: (item) => formatMillis(item.updatedAtMs),
|
render: (item) => formatMillis(item.updatedAtMs),
|
||||||
width: "minmax(170px, 1fr)",
|
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() {
|
export function HostAgenciesPage() {
|
||||||
const page = useHostAgenciesPage();
|
const page = useHostAgenciesPage();
|
||||||
const createDisabled = !page.abilities.canCreate;
|
const createDisabled = !page.abilities.canCreate;
|
||||||
const statusDisabled = !page.abilities.canStatus;
|
|
||||||
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 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
|
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,
|
||||||
@ -134,7 +123,7 @@ export function HostAgenciesPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
context={{ page, regionOptions: page.regionOptions }}
|
context={{ page, regionOptions: page.regionOptions }}
|
||||||
items={items}
|
items={items}
|
||||||
minWidth="1250px"
|
minWidth="1180px"
|
||||||
pagination={
|
pagination={
|
||||||
total > 0
|
total > 0
|
||||||
? {
|
? {
|
||||||
@ -182,13 +171,6 @@ export function HostAgenciesPage() {
|
|||||||
value={page.agencyForm.parentBdUserId}
|
value={page.agencyForm.parentBdUserId}
|
||||||
onChange={(event) => page.setAgencyForm({ ...page.agencyForm, parentBdUserId: event.target.value })}
|
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
|
<AdminSwitch
|
||||||
checked={page.agencyForm.joinEnabled}
|
checked={page.agencyForm.joinEnabled}
|
||||||
checkedLabel="允许"
|
checkedLabel="允许"
|
||||||
@ -208,77 +190,6 @@ export function HostAgenciesPage() {
|
|||||||
onChange={(event) => page.setAgencyForm({ ...page.agencyForm, reason: event.target.value })}
|
onChange={(event) => page.setAgencyForm({ ...page.agencyForm, reason: event.target.value })}
|
||||||
/>
|
/>
|
||||||
</HostOrgActionModal>
|
</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>
|
</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 { 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 { 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";
|
||||||
|
|
||||||
@ -135,15 +134,6 @@ export function HostBdLeadersPage() {
|
|||||||
page.setBDLeaderForm({ ...page.bdLeaderForm, targetUserId: event.target.value })
|
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
|
<TextField
|
||||||
disabled={createDisabled}
|
disabled={createDisabled}
|
||||||
label="职位别名"
|
label="职位别名"
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export function useAgencyAbilities() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
canCreate: can(PERMISSIONS.agencyCreate),
|
canCreate: can(PERMISSIONS.agencyCreate),
|
||||||
|
canDelete: can(PERMISSIONS.agencyDelete),
|
||||||
canStatus: can(PERMISSIONS.agencyStatus),
|
canStatus: can(PERMISSIONS.agencyStatus),
|
||||||
canView: can(PERMISSIONS.agencyView),
|
canView: can(PERMISSIONS.agencyView),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -70,7 +70,6 @@ const regionBaseSchema = z
|
|||||||
|
|
||||||
export const createBDLeaderSchema = commandContactSchema.extend({
|
export const createBDLeaderSchema = commandContactSchema.extend({
|
||||||
positionAlias: optionalTextSchema(64, "职位别名不能超过 64 个字符"),
|
positionAlias: optionalTextSchema(64, "职位别名不能超过 64 个字符"),
|
||||||
regionId: regionIdSchema,
|
|
||||||
targetUserId: userIdSchema,
|
targetUserId: userIdSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -116,7 +115,6 @@ export const coinSellerStockCreditSchema = z
|
|||||||
|
|
||||||
export const createAgencySchema = commandBaseSchema.extend({
|
export const createAgencySchema = commandBaseSchema.extend({
|
||||||
joinEnabled: z.boolean().default(true),
|
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 个字符"),
|
name: z.string().trim().min(1, "请输入 Agency 名称").max(80, "Agency 名称不能超过 80 个字符"),
|
||||||
ownerUserId: userIdSchema,
|
ownerUserId: userIdSchema,
|
||||||
parentBdUserId: optionalUserIdSchema,
|
parentBdUserId: optionalUserIdSchema,
|
||||||
|
|||||||
@ -283,6 +283,33 @@
|
|||||||
white-space: nowrap;
|
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 {
|
.statusCell {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import Add from "@mui/icons-material/Add";
|
import Add from "@mui/icons-material/Add";
|
||||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||||
|
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
||||||
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
||||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
@ -646,10 +647,16 @@ function ResourceLabel({ id, resources }) {
|
|||||||
return <span className={styles.emptyValue}>未配置</span>;
|
return <span className={styles.emptyValue}>未配置</span>;
|
||||||
}
|
}
|
||||||
const resource = resources.find((item) => Number(item.resourceId) === Number(id));
|
const resource = resources.find((item) => Number(item.resourceId) === Number(id));
|
||||||
|
const preview = resourceImageURL(resource);
|
||||||
return (
|
return (
|
||||||
<div className={styles.stack}>
|
<div className={styles.resourceCell}>
|
||||||
<span className={styles.resourceText}>{resource?.name || resource?.resourceCode || `资源 #${id}`}</span>
|
<span className={styles.resourceThumb}>
|
||||||
<span className={styles.meta}>{resource ? resourceMetaLabel(resource, id) : `ID ${id}`}</span>
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -700,6 +707,18 @@ function resourceMetaLabel(resource, id) {
|
|||||||
return `ID ${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) {
|
function badgeFormFromMetadata(metadataJson) {
|
||||||
if (!metadataJson) {
|
if (!metadataJson) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@ -10,7 +10,11 @@ import { roomPinCreateSchema } from "@/features/rooms/schema";
|
|||||||
|
|
||||||
const pageSize = 50;
|
const pageSize = 50;
|
||||||
const emptyData = { items: [], page: 1, pageSize, total: 0 };
|
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() {
|
export function useRoomPinsPage() {
|
||||||
const abilities = useRoomAbilities();
|
const abilities = useRoomAbilities();
|
||||||
@ -224,3 +228,18 @@ function roomOptionLabel(room) {
|
|||||||
}
|
}
|
||||||
return [room.title, room.roomShortId, room.roomId].filter(Boolean).join(" / ");
|
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 MeetingRoomOutlined from "@mui/icons-material/MeetingRoomOutlined";
|
||||||
import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
|
import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined";
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import { Button } from "@/shared/ui/Button.jsx";
|
import { Button } from "@/shared/ui/Button.jsx";
|
||||||
@ -24,6 +25,11 @@ const pinStatusOptions = [
|
|||||||
{ label: "全部", value: "all" },
|
{ label: "全部", value: "all" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const pinTypeOptions = [
|
||||||
|
{ label: "区域置顶", value: "region" },
|
||||||
|
{ label: "全区置顶", value: "global" },
|
||||||
|
];
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
key: "room",
|
key: "room",
|
||||||
@ -45,10 +51,22 @@ const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "weight",
|
key: "weight",
|
||||||
label: "权重",
|
label: "排序",
|
||||||
width: "minmax(100px, 0.5fr)",
|
width: "minmax(100px, 0.5fr)",
|
||||||
render: (pin) => Number(pin.weight || 0).toLocaleString("zh-CN"),
|
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",
|
key: "remaining",
|
||||||
label: "剩余时间",
|
label: "剩余时间",
|
||||||
@ -132,7 +150,7 @@ export function RoomPinsPage() {
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
items={items}
|
items={items}
|
||||||
minWidth="1240px"
|
minWidth="1480px"
|
||||||
rowKey={(pin) => pin.id}
|
rowKey={(pin) => pin.id}
|
||||||
pagination={
|
pagination={
|
||||||
total > 0
|
total > 0
|
||||||
@ -172,6 +190,7 @@ export function RoomPinsPage() {
|
|||||||
error={Boolean(page.roomOptionsError)}
|
error={Boolean(page.roomOptionsError)}
|
||||||
helperText={page.roomOptionsError}
|
helperText={page.roomOptionsError}
|
||||||
label="选择房间"
|
label="选择房间"
|
||||||
|
placeholder="输入用户短ID / 房间ID"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -193,18 +212,40 @@ export function RoomPinsPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<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
|
required
|
||||||
type="number"
|
type="number"
|
||||||
value={page.form.weight}
|
value={page.form.weight}
|
||||||
onChange={(event) => page.setForm({ ...page.form, weight: event.target.value })}
|
onChange={(event) => page.setForm({ ...page.form, weight: event.target.value })}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="置顶时间(天)"
|
InputLabelProps={{ shrink: true }}
|
||||||
|
label="开始时间"
|
||||||
required
|
required
|
||||||
type="number"
|
type="datetime-local"
|
||||||
value={page.form.durationDays}
|
value={page.form.pinnedAtMs}
|
||||||
onChange={(event) => page.setForm({ ...page.form, durationDays: event.target.value })}
|
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>
|
</AdminFormDialog>
|
||||||
</section>
|
</section>
|
||||||
@ -287,3 +328,7 @@ function formatRemaining(pin) {
|
|||||||
}
|
}
|
||||||
return `${Math.max(hours, 1)}小时`;
|
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 optionalText = (max: number, message: string) => z.string().trim().max(max, message).optional().default("");
|
||||||
const roomSeatCandidates = [10, 15, 20, 25, 30] as const;
|
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({
|
export const roomUpdateSchema = z.object({
|
||||||
coverUrl: optionalText(512, "房间头像不能超过 512 个字符"),
|
coverUrl: optionalText(512, "房间头像不能超过 512 个字符"),
|
||||||
@ -41,11 +57,18 @@ export const roomConfigSchema = z
|
|||||||
path: ["defaultSeatCount"],
|
path: ["defaultSeatCount"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const roomPinCreateSchema = z.object({
|
export const roomPinCreateSchema = z
|
||||||
durationDays: z.coerce.number().int().min(1, "请输入置顶天数").max(3650, "置顶时间不能超过 3650 天").default(30),
|
.object({
|
||||||
roomId: z.string().trim().min(1, "请选择房间").max(64, "房间 ID 不正确"),
|
expiresAtMs: dateTimeMsSchema,
|
||||||
weight: z.coerce.number().int().min(0, "权重不能小于 0").default(0),
|
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 RoomUpdateForm = z.infer<typeof roomUpdateSchema>;
|
||||||
export type RoomStatusForm = z.infer<typeof roomStatusSchema>;
|
export type RoomStatusForm = z.infer<typeof roomStatusSchema>;
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export const API_OPERATIONS = {
|
|||||||
createUser: "createUser",
|
createUser: "createUser",
|
||||||
creditCoinSellerStock: "creditCoinSellerStock",
|
creditCoinSellerStock: "creditCoinSellerStock",
|
||||||
dashboardOverview: "dashboardOverview",
|
dashboardOverview: "dashboardOverview",
|
||||||
|
deleteAgency: "deleteAgency",
|
||||||
deleteAppVersion: "deleteAppVersion",
|
deleteAppVersion: "deleteAppVersion",
|
||||||
deleteBanner: "deleteBanner",
|
deleteBanner: "deleteBanner",
|
||||||
deleteBDLeader: "deleteBDLeader",
|
deleteBDLeader: "deleteBDLeader",
|
||||||
@ -521,6 +522,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "overview:view",
|
permission: "overview:view",
|
||||||
permissions: ["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: {
|
deleteAppVersion: {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
operationId: API_OPERATIONS.deleteAppVersion,
|
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;
|
patch?: never;
|
||||||
trace?: 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": {
|
"/admin/agencies/{agency_id}/join-enabled": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -3143,6 +3159,20 @@ export interface operations {
|
|||||||
200: components["responses"]["EmptyResponse"];
|
200: components["responses"]["EmptyResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
deleteAgency: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
agency_id: number;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
setAgencyJoinEnabled: {
|
setAgencyJoinEnabled: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@ -650,6 +650,7 @@ export interface RoomPinDto {
|
|||||||
createdByAdminId?: number;
|
createdByAdminId?: number;
|
||||||
expiresAtMs?: number;
|
expiresAtMs?: number;
|
||||||
id: EntityId;
|
id: EntityId;
|
||||||
|
pinType?: string;
|
||||||
pinnedAtMs?: number;
|
pinnedAtMs?: number;
|
||||||
remainingMs?: number;
|
remainingMs?: number;
|
||||||
room: RoomPinRoomDto;
|
room: RoomPinRoomDto;
|
||||||
@ -661,6 +662,9 @@ export interface RoomPinDto {
|
|||||||
|
|
||||||
export interface RoomPinPayload {
|
export interface RoomPinPayload {
|
||||||
durationDays?: number;
|
durationDays?: number;
|
||||||
|
expiresAtMs?: number;
|
||||||
|
pinType?: string;
|
||||||
|
pinnedAtMs?: number;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
weight: number;
|
weight: number;
|
||||||
}
|
}
|
||||||
@ -816,7 +820,6 @@ export interface HostCommandPayload {
|
|||||||
|
|
||||||
export interface CreateBDLeaderPayload extends HostCommandPayload {
|
export interface CreateBDLeaderPayload extends HostCommandPayload {
|
||||||
positionAlias?: string;
|
positionAlias?: string;
|
||||||
regionId: number;
|
|
||||||
targetUserId: number;
|
targetUserId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -844,7 +847,6 @@ export interface CoinSellerStatusPayload extends BDStatusPayload {
|
|||||||
|
|
||||||
export interface CreateAgencyPayload extends HostCommandPayload {
|
export interface CreateAgencyPayload extends HostCommandPayload {
|
||||||
joinEnabled: boolean;
|
joinEnabled: boolean;
|
||||||
maxHosts?: number;
|
|
||||||
name: string;
|
name: string;
|
||||||
ownerUserId: number;
|
ownerUserId: number;
|
||||||
parentBdUserId: number;
|
parentBdUserId: number;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user