Compare commits

..

1 Commits
main ... test

Author SHA1 Message Date
zhx
d81c9548a2 fix admin reward group bigint ids 2026-05-26 20:50:42 +08:00
24 changed files with 148 additions and 1568 deletions

View File

@ -108,51 +108,3 @@ export async function syncLingxianGames(data: Record<string, any>) {
data,
);
}
export async function getHotgameProviderConfig(params: Record<string, any>) {
return requestClient.get<Record<string, any>>('/go/operate/hotgame-game/config', {
params,
});
}
export async function saveHotgameProviderConfig(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/go/operate/hotgame-game/config',
data,
);
}
export async function activateHotgameProviderProfile(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/go/operate/hotgame-game/config/activate',
data,
);
}
export async function pageHotgameCatalog(params: Record<string, any>) {
return requestClient.get<Record<string, any>>(
'/go/operate/hotgame-game/catalog/page',
{ params },
);
}
export async function fetchHotgameCatalog(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/go/operate/hotgame-game/catalog/fetch',
data,
);
}
export async function importHotgameCatalog(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/go/operate/hotgame-game/import-catalog',
data,
);
}
export async function syncHotgameGames(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/go/operate/hotgame-game/sync',
data,
);
}

View File

@ -14,7 +14,6 @@ export interface CountryDashboardMetric {
d30RetentionRate: number | string;
d30RetentionUser: number;
dailyActiveUser: number;
dailyRechargeUser: number;
dealerRecharge: number | string;
googleRecharge: number | string;
gamePayoutRate: number | string;
@ -33,7 +32,6 @@ export interface CountryDashboardMetric {
luckyGiftUser: number;
luckyGiftUserRate: number | string;
mifapayRecharge: number | string;
newDealerUserRecharge: number | string;
newUserRecharge: number | string;
officialRecharge: number | string;
periodKey: string;
@ -41,11 +39,9 @@ export interface CountryDashboardMetric {
salaryExchange: number | string;
salaryTransfer: number | string;
totalRecharge: number | string;
userRecharge: number | string;
}
export interface CountryDashboardResult {
arpuSummary?: CountryDashboardArpuSummary;
computedAt: string;
endDate?: string;
periodType: string;
@ -55,12 +51,6 @@ export interface CountryDashboardResult {
total: CountryDashboardMetric;
}
export interface CountryDashboardArpuSummary {
day?: CountryDashboardResult | null;
threeDay?: CountryDashboardResult | null;
week?: CountryDashboardResult | null;
}
export interface CountryDashboardRechargeDetail {
amount: number | string;
coinQuantity: number | string;

View File

@ -212,17 +212,6 @@ export async function getClsGoldRunningWater(params: Record<string, any>) {
});
}
export async function exportClsGoldRunningWater(
params: Record<string, any>,
filename = 'GmGoldRunningWater',
) {
return downloadLegacyExcel(
'/user-wallet/gold/running-water/cls/export',
params,
filename,
);
}
export async function getPurchaseTable(params: Record<string, any>) {
return requestClient.get<LegacyPageResult<Record<string, any>>>(
'/order/purchase/history/page',
@ -241,17 +230,6 @@ export async function listInAppPurchase(params: Record<string, any>) {
);
}
export async function exportInAppPurchaseDetails(
params: Record<string, any>,
filename = 'InAppPurchaseOrders',
) {
return downloadLegacyExcel(
'/order/in-app-purchase/details/export',
params,
filename,
);
}
export async function getInAppPurchase(id: number | string) {
return requestClient.get<Record<string, any>>(
'/order/in-app-purchase/details',

View File

@ -78,13 +78,6 @@ export async function updatePayOpenCountyShelf(
});
}
export async function syncPayOpenCountryCurrencyRates(data: Record<string, any>) {
return requestClient.post<Record<string, any>>(
'/sys-pay-open-country/sync-currency-rates',
data,
);
}
export async function pagePayChannel(params: Record<string, any>) {
return requestClient.get<LegacyPageResult<Record<string, any>>>(
'/sys-pay-channel/page',

View File

@ -43,23 +43,12 @@ export interface LegacyUser {
}>;
}
export interface LegacyBackUserOption {
id: number | string;
loginName?: string;
nickname?: string;
status?: number | string;
}
export async function pageUsers(params: Record<string, any>) {
return requestClient.get<LegacyPageResult<LegacyUser>>('/users', {
params,
});
}
export async function listBackUserOptions() {
return requestClient.get<LegacyBackUserOption[]>('/users/list/combo');
}
export async function switchUserStatus(
id: number | string,
status: number | string,

View File

@ -216,14 +216,11 @@ export async function pageFreightRunningWater(params: Record<string, any>) {
);
}
export async function exportFreightWaters(
params: Record<string, any>,
filename = 'ExportFreightWaters',
) {
export async function exportFreightWaters(params: Record<string, any>) {
return downloadLegacyExcel(
'/freight/running-water/export',
params,
filename,
'ExportFreightWaters',
);
}

View File

@ -29,12 +29,6 @@ const routes: RouteRecordRaw[] = [
component: () => import('#/views/operate/lingxian-config.vue'),
meta: { title: '灵仙配置' },
},
{
name: 'OperateHotgameConfig',
path: 'hotgame-config',
component: () => import('#/views/operate/hotgame-config.vue'),
meta: { title: '热游配置' },
},
],
},
];

View File

@ -21,12 +21,9 @@ import {
} from 'antdv-next';
import dayjs from 'dayjs';
import { pageGmGoldOperation, submitGmGoldOperation } from '#/api/legacy/gm';
import {
exportClsGoldRunningWater,
getClsGoldRunningWater,
} from '#/api/legacy/operate';
import { listBackUserOptions } from '#/api/legacy/system';
import { submitGmGoldOperation } from '#/api/legacy/gm';
import { getClsGoldRunningWater } from '#/api/legacy/operate';
import { listMembers } from '#/api/legacy/team';
import { getUserBaseInfoBySysOriginAccount } from '#/api/legacy/user';
import AccountInput from '#/components/account-input.vue';
import UserProfileLink from '#/views/operate/components/user-profile-link.vue';
@ -59,8 +56,6 @@ const DEFAULT_PAGE_SIZE = 100;
const open = ref(false);
const loading = ref(false);
const loadMoreLoading = ref(false);
const operatorsLoading = ref(false);
const exporting = ref(false);
const submitting = ref(false);
const list = ref<Array<Record<string, any>>>([]);
const notMore = ref(false);
@ -69,7 +64,7 @@ const rangeDate = ref<[string, string] | null>(null);
const operators = ref<Array<Record<string, any>>>([]);
const query = reactive<Record<string, any>>({
backOperationUsers: [],
backOperationUser: undefined,
context: '',
endTime: '',
lastId: '',
@ -108,14 +103,10 @@ const showUsdQuantity = computed(
);
const operatorOptions = computed(() =>
operators.value.map((item) => {
const name = item.nickname || item.loginName || item.name || item.id;
const account = item.loginName && item.loginName !== name ? ` / ${item.loginName}` : '';
return {
label: `${name}${account}`,
value: item.id,
};
}),
operators.value.map((item) => ({
label: `${item.nickname || item.loginName || item.id} / ${item.loginName || item.id}`,
value: item.id,
})),
);
const currentPageList = computed(() => {
@ -268,81 +259,22 @@ function filterOperatorOption(input: string, option: any) {
}
async function loadOperators() {
operatorsLoading.value = true;
try {
let backUsers: Array<Record<string, any>> = [];
try {
const result = await listBackUserOptions();
backUsers = Array.isArray(result) ? result : [];
} catch {
backUsers = [];
}
operators.value =
backUsers.length > 0
? normalizeOperators(backUsers)
: await loadRecentGmOperators().catch(() => []);
} finally {
operatorsLoading.value = false;
}
}
function normalizeOperators(items: Array<Record<string, any>>) {
const operatorMap = new Map<string, Record<string, any>>();
items.forEach((item) => {
const id = item.id || item.applicantId;
if (!id) {
return;
}
operatorMap.set(String(id), {
id,
loginName: item.loginName,
name: item.name,
nickname: item.nickname || item.applicantName,
status: item.status,
});
});
return [...operatorMap.values()];
}
async function loadRecentGmOperators() {
const result = await pageGmGoldOperation({ cursor: 1, limit: 500 });
return normalizeOperators(result?.records || []);
}
function validateQueryTimeRange() {
if (!query.startTime || !query.endTime) {
message.warning('请选择时间范围');
return false;
}
const startYear = new Date(Number(query.startTime)).getFullYear();
const endYear = new Date(Number(query.endTime)).getFullYear();
if (startYear !== endYear) {
message.warning('不允许跨年查询');
return false;
}
return true;
}
function buildRunningWaterParams() {
const backOperationUsers = Array.isArray(query.backOperationUsers)
? query.backOperationUsers.filter(Boolean).join(',')
: '';
return {
backOperationUsers: backOperationUsers || undefined,
endTime: query.endTime,
queryBackOperationUser: true,
startTime: query.startTime,
type: query.type,
userId: query.userId || undefined,
};
operators.value = (await listMembers()) || [];
}
async function loadData(reset = false) {
if (loading.value || loadMoreLoading.value) {
return;
}
if (!validateQueryTimeRange()) {
if (!query.startTime || !query.endTime) {
message.warning('请选择时间范围');
return;
}
const startYear = new Date(Number(query.startTime)).getFullYear();
const endYear = new Date(Number(query.endTime)).getFullYear();
if (startYear !== endYear) {
message.warning('不允许跨年查询');
return;
}
@ -362,10 +294,15 @@ async function loadData(reset = false) {
try {
const result = await getClsGoldRunningWater({
...buildRunningWaterParams(),
backOperationUser: query.backOperationUser || undefined,
context: query.context || undefined,
endTime: query.endTime,
lastId: query.lastId || undefined,
limit: query.limit,
queryBackOperationUser: true,
startTime: query.startTime,
type: query.type,
userId: query.userId || undefined,
});
const waters = result?.waters || [];
const nextList = reset ? waters : [...list.value, ...waters];
@ -415,21 +352,6 @@ async function handleSearch() {
await loadData(true);
}
async function handleExport() {
if (exporting.value || !validateQueryTimeRange()) {
return;
}
exporting.value = true;
try {
await exportClsGoldRunningWater(
buildRunningWaterParams(),
`GmGoldRunningWater_${dayjs(Number(query.startTime)).format('YYYYMMDD')}_${dayjs(Number(query.endTime)).format('YYYYMMDD')}`,
);
} finally {
exporting.value = false;
}
}
async function handleSubmit() {
if (!validateForm()) {
return;
@ -498,19 +420,15 @@ void loadData(true);
style="width: 240px"
/>
<Select
v-model:value="query.backOperationUsers"
v-model:value="query.backOperationUser"
allow-clear
mode="multiple"
show-search
max-tag-count="responsive"
placeholder="操作人"
style="width: 220px"
:filter-option="filterOperatorOption"
:loading="operatorsLoading"
:options="operatorOptions"
/>
<Button :loading="loading" type="primary" @click="handleSearch">查询</Button>
<Button :loading="exporting" @click="handleExport">导出Excel</Button>
</div>
<Table

View File

@ -1,9 +1,6 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import {
OSS_FILE_BUCKETS,
getAccessImgUrl,
@ -116,8 +113,6 @@ const selectCountryCodes = ref<string[]>([]);
const countriesLoading = ref(false);
const regionsLoading = ref(false);
const form = reactive<Record<string, any>>(createForm());
const startTimePicker = ref<Dayjs | null>(null);
const expiredTimePicker = ref<Dayjs | null>(null);
const regionSelectOptions = computed(() =>
regionOptions.value.map((item) => ({
@ -161,27 +156,6 @@ function normalizeMultiValue(value: any) {
.filter(Boolean);
}
function normalizePickerValue(value: any) {
if (value === '' || value === null || value === undefined) {
return null;
}
if (dayjs.isDayjs(value)) {
return value.isValid() ? value : null;
}
const raw = String(value).trim();
const numericValue = Number(raw);
const parsed = raw !== '' && Number.isFinite(numericValue) ? dayjs(numericValue) : dayjs(value);
return parsed.isValid() ? parsed : null;
}
function pickerToTimestamp(value: Dayjs | null) {
return value?.isValid() ? String(value.valueOf()) : '';
}
function getDatePickerPopupContainer(triggerNode: HTMLElement) {
return triggerNode.parentElement || document.body;
}
watch(
() => ({ open: props.open, row: props.row, sysOrigin: props.sysOrigin }),
({ open, row, sysOrigin }) => {
@ -200,8 +174,6 @@ watch(
roomSearchValue.value = String(form.params);
}
}
startTimePicker.value = normalizePickerValue(form.startTime);
expiredTimePicker.value = normalizePickerValue(form.expiredTime);
void Promise.all([
loadCountries(),
sysOrigin ? loadRegions(sysOrigin) : Promise.resolve(),
@ -232,14 +204,6 @@ watch(
},
);
watch(startTimePicker, (value) => {
form.startTime = pickerToTimestamp(value);
});
watch(expiredTimePicker, (value) => {
form.expiredTime = pickerToTimestamp(value);
});
async function loadCountries() {
countriesLoading.value = true;
try {
@ -323,18 +287,10 @@ async function submitForm() {
message.warning('请选择系统');
return;
}
if (!form.startTime) {
message.warning('请选择开始时间');
return;
}
if (!form.expiredTime) {
message.warning('请选择过期时间');
return;
}
if (Number(form.expiredTime) <= Number(form.startTime)) {
message.warning('过期时间必须晚于开始时间');
return;
}
if (form.content === 'ENTER_ROOM' && !form.params) {
message.warning('请选择房间');
return;
@ -530,22 +486,20 @@ async function submitForm() {
<div class="field">
<div class="label">开始时间</div>
<DatePicker
v-model:value="startTimePicker"
format="YYYY-MM-DD HH:mm:ss"
:get-popup-container="getDatePickerPopupContainer"
v-model:value="form.startTime"
show-time
style="width: 100%"
value-format="x"
/>
</div>
<div class="field">
<div class="label">过期时间</div>
<DatePicker
v-model:value="expiredTimePicker"
format="YYYY-MM-DD HH:mm:ss"
:get-popup-container="getDatePickerPopupContainer"
v-model:value="form.expiredTime"
show-time
style="width: 100%"
value-format="x"
/>
</div>

View File

@ -1,6 +1,11 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import {
addOrUpdatePayCommodity,
listCountrytSupportAmountChannels,
} from '#/api/legacy/pay';
import {
Collapse,
CollapsePanel,
@ -8,16 +13,11 @@ import {
FormItem,
Image,
Input,
message,
Modal,
Select,
message,
} from 'antdv-next';
import {
addOrUpdatePayCommodity,
listCountrytSupportAmountChannels,
} from '#/api/legacy/pay';
import { WEB_PRODUCT_TYPE_OPTIONS } from '../pay-shared';
const props = withDefaults(
@ -60,7 +60,6 @@ const form = reactive<Record<string, any>>({
content: '',
id: '',
payCountryId: '',
payCountryIds: [],
regionId: '',
shelf: true,
type: 'GOLD',
@ -69,27 +68,7 @@ const form = reactive<Record<string, any>>({
const isUpdate = computed(() => Boolean(form.id));
const modalTitle = computed(() => (isUpdate.value ? '修改商品' : '创建商品'));
const selectedApp = computed(() => props.row?.selectApp || {});
const supportCountries = computed(() => props.row?.supportCountries || []);
const countryOptions = computed(() =>
supportCountries.value.map((item: Record<string, any>) => ({
label:
item.country?.aliasName ||
item.country?.enName ||
item.country?.countryName ||
'-',
value: item.id,
})),
);
const selectedCountry = computed(() => {
const currentPayCountryId = form.payCountryId || form.payCountryIds?.[0];
const current = supportCountries.value.find(
(item: Record<string, any>) => String(item.id) === String(currentPayCountryId),
);
if (!current) {
return props.row?.country || {};
}
return toCountryMeta(current);
});
const selectedCountry = computed(() => props.row?.country || {});
const regionName = computed(() => props.row?.regionName || '');
watch(
@ -105,14 +84,6 @@ watch(
form.content = row.content ?? '';
form.id = row.id ?? '';
form.payCountryId = row.payCountryId ?? '';
form.payCountryIds = Array.isArray(row.payCountryIds)
? [...row.payCountryIds]
: (row.payCountryId
? [row.payCountryId]
: []);
if (props.mode === 'country' && !form.payCountryId && form.payCountryIds.length > 0) {
form.payCountryId = form.payCountryIds[0];
}
form.regionId = row.regionId ?? '';
form.shelf = row.shelf ?? true;
form.type = row.type ?? 'GOLD';
@ -121,26 +92,12 @@ watch(
{ immediate: true },
);
function toCountryMeta(country: Record<string, any>) {
return {
countryName:
country.country?.aliasName ||
country.country?.enName ||
country.country?.countryName ||
'-',
currency: country.currency || '',
exchangeRate: country.usdExchangeRate || '',
icon: country.country?.nationalFlag || '',
};
}
function isPositiveNumberPointTwo(value: string) {
return /^\d+(\.\d{1,2})?$/.test(value);
}
async function loadSupportAmountChannels() {
const currentPayCountryId = form.payCountryId || form.payCountryIds?.[0];
if (props.mode !== 'country' || !currentPayCountryId || !isPositiveNumberPointTwo(String(form.amountUsd || ''))) {
if (props.mode !== 'country' || !form.payCountryId || !isPositiveNumberPointTwo(String(form.amountUsd || ''))) {
supportAmounts.value = [];
computedAmount.value = '';
amountUsdText.value = '';
@ -148,9 +105,8 @@ async function loadSupportAmountChannels() {
}
associateLoading.value = true;
try {
// .
const result = await listCountrytSupportAmountChannels(
currentPayCountryId,
form.payCountryId,
form.amountUsd,
);
supportAmounts.value = result?.channels || [];
@ -168,20 +124,11 @@ function handleAmountChange() {
void loadSupportAmountChannels();
}
function handleCountryIdsChange(value: Array<number | string>) {
form.payCountryId = value?.[0] || '';
void loadSupportAmountChannels();
}
function validateForm() {
if (props.mode === 'country' && isUpdate.value && !form.payCountryId) {
if (props.mode === 'country' && !form.payCountryId) {
message.warning('缺少支付国家');
return false;
}
if (props.mode === 'country' && !isUpdate.value && form.payCountryIds.length === 0) {
message.warning('请选择售卖国家');
return false;
}
if (props.mode === 'region' && !form.regionId) {
message.warning('缺少区域');
return false;
@ -215,16 +162,13 @@ async function handleSubmit() {
}
loading.value = true;
try {
const createCountryIds =
props.mode === 'country' && !isUpdate.value ? form.payCountryIds : undefined;
await addOrUpdatePayCommodity({
amountUsd: form.amountUsd,
applicationId: form.applicationId,
awardContent: form.awardContent,
content: form.content,
id: form.id,
payCountryId: form.payCountryId || form.payCountryIds?.[0],
payCountryIds: createCountryIds,
payCountryId: form.payCountryId,
regionId: form.regionId,
shelf: form.shelf,
type: form.type,
@ -255,16 +199,7 @@ async function handleSubmit() {
<Form layout="vertical">
<FormItem v-if="mode === 'country'" label="售卖国家">
<Select
v-if="!isUpdate"
v-model:value="form.payCountryIds"
mode="multiple"
:options="countryOptions"
option-label-prop="label"
placeholder="请选择售卖国家"
@change="handleCountryIdsChange"
/>
<div v-else class="inline-meta">
<div class="inline-meta">
<Image
v-if="selectedCountry.icon"
:preview="false"
@ -317,7 +252,7 @@ async function handleSubmit() {
<div v-else-if="supportAmounts.length === 0" class="support-empty">
暂无可展示渠道
</div>
<Collapse v-else v-model:active-key="activeKeys">
<Collapse v-else v-model:activeKey="activeKeys">
<CollapsePanel
v-for="item in supportAmounts"
:key="String(item.channel?.channelCode || '')"

View File

@ -1,6 +1,14 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { regionConfigTable } from '#/api/legacy/system';
import {
listPayOpenCountry,
pagePayCommodity,
switchShelfCommodity,
} from '#/api/legacy/pay';
import { formatDate } from '#/views/system/shared';
import {
Button,
Card,
@ -11,15 +19,8 @@ import {
Table,
} from 'antdv-next';
import {
listPayOpenCountry,
pagePayCommodity,
switchShelfCommodity,
} from '#/api/legacy/pay';
import { regionConfigTable } from '#/api/legacy/system';
import { formatDate } from '#/views/system/shared';
import { WEB_PRODUCT_TYPE_OPTIONS } from '../pay-shared';
import PayCommodityEditModal from './pay-commodity-edit-modal.vue';
const props = withDefaults(
@ -184,7 +185,7 @@ function handlePageChange(page: number, pageSize: number) {
void loadData();
}
function handleApplicationChange(value: number | string) {
function handleApplicationChange(value: string | number) {
selectedApp.value =
props.appInfo?.appList?.find((item: Record<string, any>) => item.id === value) ||
{};
@ -192,7 +193,7 @@ function handleApplicationChange(value: number | string) {
void loadSupportData().then(() => loadData(true));
}
function handleDimensionChange(value: number | string) {
function handleDimensionChange(value: string | number) {
selectedDimension.value =
supportList.value.find((item) => item.id === value) || {};
if (props.mode === 'country') {
@ -217,12 +218,10 @@ function openCreate() {
amountUsd: '',
applicationId: query.applicationId,
payCountryId: query.payCountryId,
payCountryIds: props.mode === 'country' ? [query.payCountryId].filter(Boolean) : [],
regionId: query.regionId,
regionName: selectedDimension.value.regionName || '',
selectApp: selectedApp.value,
shelf: true,
supportCountries: props.mode === 'country' ? supportList.value : [],
type: query.type,
...(props.mode === 'country'
? {
@ -244,11 +243,8 @@ function openCreate() {
function openEdit(record: Record<string, any>) {
activeRow.value = {
...record,
payCountryIds:
props.mode === 'country' ? [record.payCountryId || query.payCountryId].filter(Boolean) : [],
regionName: record.regionName || selectedDimension.value.regionName || '',
selectApp: selectedApp.value,
supportCountries: props.mode === 'country' ? supportList.value : [],
...(props.mode === 'country'
? {
country: {
@ -270,23 +266,20 @@ function openEdit(record: Record<string, any>) {
<template>
<Card :title="title">
<div class="toolbar">
<Select
option-label-prop="label"
<Select option-label-prop="label"
v-model:value="query.type"
:options="productTypeOptions"
style="width: 180px"
@change="handleSearch"
/>
<Select
option-label-prop="label"
<Select option-label-prop="label"
v-model:value="query.applicationId"
:options="applicationOptions"
placeholder="应用"
style="width: 200px"
@change="handleApplicationChange"
/>
<Select
option-label-prop="label"
<Select option-label-prop="label"
v-model:value="selectedDimensionId"
:loading="supportLoading"
:options="supportOptions"
@ -294,8 +287,7 @@ function openEdit(record: Record<string, any>) {
style="width: 200px"
@change="handleDimensionChange"
/>
<Select
option-label-prop="label"
<Select option-label-prop="label"
v-model:value="query.shelf"
:options="shelfOptions"
style="width: 140px"
@ -349,7 +341,7 @@ function openEdit(record: Record<string, any>) {
:total="total"
show-size-changer
@change="handlePageChange"
@show-size-change="handlePageChange"
@showSizeChange="handlePageChange"
/>
</div>

View File

@ -1,468 +0,0 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import {
activateHotgameProviderProfile,
fetchHotgameCatalog,
getHotgameProviderConfig,
importHotgameCatalog,
pageHotgameCatalog,
saveHotgameProviderConfig,
} from '#/api/legacy/baishun-game';
import { getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
Collapse,
CollapsePanel,
Form,
FormItem,
Input,
Pagination,
Select,
Table,
Tag,
message,
} from 'antdv-next';
defineOptions({ name: 'OperateHotgameConfig' });
function createProviderForm(sysOrigin = '') {
return {
appKey: '',
callbackBaseUrl: '',
profile: 'PROD',
sysOrigin,
testToken: '',
testUid: '',
};
}
const accessStore = useAccessStore();
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
const profileOptions = [
{ label: '正式环境', value: 'PROD' },
{ label: '测试环境', value: 'TEST' },
];
const importShowcaseOptions = [
{ label: '默认上架', value: true as any },
{ label: '默认下架', value: false as any },
];
const contextQuery = reactive({ profile: 'PROD', sysOrigin: '' });
const providerForm = reactive(createProviderForm());
const catalogQuery = reactive<Record<string, any>>({
cursor: 1,
keyword: '',
limit: 20,
});
const providerLoading = ref(false);
const providerSaving = ref(false);
const catalogLoading = ref(false);
const catalogFetching = ref(false);
const catalogImporting = ref(false);
const profileActivating = ref(false);
const totalCatalog = ref(0);
const catalogList = ref<Array<Record<string, any>>>([]);
const selectedCatalogKeys = ref<Array<string>>([]);
const activeProfile = ref('PROD');
const importDefaultShowcase = ref(true as any);
const isCurrentProfileActive = computed(
() => activeProfile.value === contextQuery.profile,
);
const activeProfileLabel = computed(
() =>
profileOptions.find((item) => item.value === activeProfile.value)?.label ||
activeProfile.value,
);
const catalogColumns = [
{ dataIndex: 'name', key: 'name', title: '名称', width: 180 },
{ dataIndex: 'gameId', key: 'gameId', title: '内部ID', width: 150 },
{ dataIndex: 'vendorGameId', key: 'vendorGameId', title: '热游ID', width: 120 },
{ dataIndex: 'status', key: 'status', title: '目录状态', width: 110 },
{ dataIndex: 'packageVersion', key: 'packageVersion', title: '版本', width: 90 },
{ dataIndex: 'added', key: 'added', title: '统一游戏列表', width: 140 },
{ dataIndex: 'downloadUrl', key: 'downloadUrl', title: '启动地址', width: 520 },
{ dataIndex: 'updateTime', key: 'updateTime', title: '更新时间', width: 180 },
];
const catalogRowSelection = computed(() => ({
selectedRowKeys: selectedCatalogKeys.value,
onChange: (keys: Array<number | string>) => {
selectedCatalogKeys.value = keys.map((item) => String(item));
},
getCheckboxProps: (record: Record<string, any>) => ({
disabled: Boolean(record.added),
}),
}));
watch(
sysOriginOptions,
(options) => {
if (!contextQuery.sysOrigin) {
contextQuery.sysOrigin = String(options[0]?.value || '');
}
},
{ immediate: true },
);
watch(
() => [contextQuery.sysOrigin, contextQuery.profile],
([sysOrigin, profile]) => {
if (!sysOrigin || !profile) {
return;
}
Object.assign(providerForm, createProviderForm(sysOrigin), {
profile,
sysOrigin,
});
selectedCatalogKeys.value = [];
void Promise.all([loadProviderConfig(), loadCatalog(true)]);
},
{ immediate: true },
);
function applyProviderForm(record: Record<string, any>) {
activeProfile.value = String(record.activeProfile || activeProfile.value || 'PROD');
Object.assign(providerForm, createProviderForm(contextQuery.sysOrigin), {
...record,
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
}
async function loadProviderConfig() {
if (!contextQuery.sysOrigin) {
return;
}
providerLoading.value = true;
try {
const result = await getHotgameProviderConfig({
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
applyProviderForm(result || {});
} finally {
providerLoading.value = false;
}
}
async function persistProviderConfig(showSuccess = true) {
if (!contextQuery.sysOrigin || !providerForm.appKey) {
message.warning('请补全热游签名 Key');
return false;
}
providerSaving.value = true;
try {
const result = await saveHotgameProviderConfig({
...providerForm,
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
applyProviderForm(result || providerForm);
if (showSuccess) {
message.success('热游配置已保存');
}
return true;
} catch {
return false;
} finally {
providerSaving.value = false;
}
}
async function loadCatalog(reset = false) {
if (!contextQuery.sysOrigin) {
return;
}
if (reset) {
catalogQuery.cursor = 1;
}
catalogLoading.value = true;
try {
const result = await pageHotgameCatalog({
...catalogQuery,
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
catalogList.value = Array.isArray(result.records) ? result.records : [];
totalCatalog.value = Number(result.total || 0);
} finally {
catalogLoading.value = false;
}
}
function handleCatalogPageChange(page: number, pageSize: number) {
catalogQuery.cursor = page;
catalogQuery.limit = pageSize;
void loadCatalog();
}
async function handleActivateProfile() {
if (!(await persistProviderConfig(false))) {
return;
}
profileActivating.value = true;
try {
const result = await activateHotgameProviderProfile({
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
applyProviderForm(result || providerForm);
message.success(`已切换到${activeProfileLabel.value}`);
await loadCatalog(true);
} finally {
profileActivating.value = false;
}
}
async function handleFetchCatalog() {
if (!(await persistProviderConfig(false))) {
return;
}
catalogFetching.value = true;
try {
const result = await fetchHotgameCatalog({
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
});
message.success(
`同步完成:新增 ${Number(result.inserted || 0)},更新 ${Number(result.updated || 0)},跳过 ${Number(result.skipped || 0)}`,
);
await loadCatalog(true);
} finally {
catalogFetching.value = false;
}
}
async function handleImportSelected() {
if (selectedCatalogKeys.value.length === 0) {
message.warning('请选择要添加到统一游戏列表的热游游戏');
return;
}
catalogImporting.value = true;
try {
const result = await importHotgameCatalog({
profile: contextQuery.profile,
sysOrigin: contextQuery.sysOrigin,
vendorGameIds: selectedCatalogKeys.value,
defaultShowcase: importDefaultShowcase.value,
});
selectedCatalogKeys.value = [];
message.success(
`添加完成:新增 ${Number(result.imported || 0)},已存在 ${Number(result.existing || 0)}`,
);
await loadCatalog(true);
} finally {
catalogImporting.value = false;
}
}
</script>
<template>
<Page title="热游配置">
<Card :loading="providerLoading" title="热游接入配置">
<Form layout="vertical">
<div class="form-grid provider-grid">
<FormItem label="系统">
<SysOriginSelect
v-model:value="contextQuery.sysOrigin"
:options="sysOriginOptions"
/>
</FormItem>
<FormItem label="配置环境" extra="app 侧只读取当前启用环境。">
<Select
v-model:value="contextQuery.profile"
:options="profileOptions"
option-label-prop="label"
/>
</FormItem>
<FormItem label="回调域名">
<Input
v-model:value="providerForm.callbackBaseUrl"
placeholder="https://api.global-interaction.com/"
/>
</FormItem>
<FormItem label="Key">
<Input
v-model:value="providerForm.appKey"
placeholder="请输入热游签名 Key"
type="password"
/>
</FormItem>
</div>
<Collapse class="test-user-collapse" ghost>
<CollapsePanel key="test-user" header="测试用户">
<div class="form-grid">
<FormItem label="UID">
<Input v-model:value="providerForm.testUid" placeholder="可选" />
</FormItem>
<FormItem label="Token">
<Input
v-model:value="providerForm.testToken"
placeholder="可选"
type="password"
/>
</FormItem>
</div>
</CollapsePanel>
</Collapse>
<div class="toolbar">
<Button
type="primary"
:loading="providerSaving"
@click="persistProviderConfig(true)"
>
保存配置
</Button>
<Button
:disabled="isCurrentProfileActive"
:loading="profileActivating"
@click="handleActivateProfile"
>
启用此环境
</Button>
<Button :loading="catalogFetching" @click="handleFetchCatalog">
同步热游目录
</Button>
<Tag :color="isCurrentProfileActive ? 'success' : 'warning'">
当前启用{{ activeProfileLabel }}
</Tag>
</div>
</Form>
</Card>
<Card class="content-card" title="热游目录">
<div class="toolbar">
<Input
v-model:value="catalogQuery.keyword"
placeholder="搜索目录名称 / 内部ID / 热游ID"
style="width: 280px"
@pressEnter="loadCatalog(true)"
/>
<Button :loading="catalogLoading" type="primary" @click="loadCatalog(true)">
搜索
</Button>
<Button :loading="catalogFetching" @click="handleFetchCatalog">
重新同步目录
</Button>
<Select
v-model:value="importDefaultShowcase"
:options="importShowcaseOptions"
style="width: 120px"
/>
<Button
type="primary"
:loading="catalogImporting"
@click="handleImportSelected"
>
添加已选游戏
</Button>
</div>
<Table
:columns="catalogColumns"
:data-source="catalogList"
:loading="catalogLoading"
:pagination="false"
row-key="vendorGameId"
:row-selection="catalogRowSelection"
:scroll="{ x: 1500 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<Tag :color="record.status === 'ENABLED' ? 'success' : 'default'">
{{ record.status || '-' }}
</Tag>
</template>
<template v-else-if="column.key === 'added'">
<Tag :color="record.added ? 'success' : 'default'">
{{ record.added ? '已进入统一游戏列表' : '未添加' }}
</Tag>
</template>
<template v-else-if="column.key === 'downloadUrl'">
<a
v-if="record.downloadUrl"
:href="record.downloadUrl"
class="link-cell"
rel="noreferrer"
target="_blank"
>
{{ record.downloadUrl }}
</a>
<span v-else>-</span>
</template>
</template>
</Table>
<div v-if="totalCatalog > 0" class="pager">
<Pagination
:current="catalogQuery.cursor"
:page-size="catalogQuery.limit"
:total="totalCatalog"
show-size-changer
@change="handleCatalogPageChange"
@showSizeChange="handleCatalogPageChange"
/>
</div>
</Card>
</Page>
</template>
<style scoped>
.content-card {
margin-top: 16px;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 16px;
}
.provider-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.test-user-collapse {
margin-bottom: 12px;
}
.link-cell {
display: inline-block;
max-width: 500px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 960px) {
.form-grid,
.provider-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -6,8 +6,6 @@ import {
watch,
} from 'vue';
import dayjs from 'dayjs';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
@ -23,7 +21,6 @@ import { getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
DatePicker,
Modal,
Select,
Space,
@ -62,7 +59,6 @@ const salarySortKey = ref<'' | SalarySortKey>('');
const salarySortOrder = ref<SalarySortOrder>('');
const query = reactive({
billBelong: dayjs().format('YYYYMM'),
countryCode: '',
cursor: 1,
limit: 20,
@ -79,13 +75,13 @@ const columns = [
{ dataIndex: 'currentTarget', key: 'currentTarget', title: '本次积分', width: 130 },
{ dataIndex: 'policyLevel', key: 'policyLevel', title: '本次积分档位', width: 130 },
{ dataIndex: 'expectedMemberSalary', key: 'expectedMemberSalary', title: '主播应发', width: 120 },
{ dataIndex: 'expectedAgentSalary', key: 'expectedAgentSalary', title: '上级代理应发', width: 130 },
{ dataIndex: 'expectedAgentSalary', key: 'expectedAgentSalary', title: '代理应发', width: 120 },
{ dataIndex: 'expectedSalary', key: 'expectedSalary', title: '应发合计', width: 120 },
{ dataIndex: 'payableMemberSalary', key: 'payableMemberSalary', title: '主播差额', width: 120 },
{ dataIndex: 'payableAgentSalary', key: 'payableAgentSalary', title: '上级代理差额', width: 130 },
{ dataIndex: 'payableSalary', key: 'payableSalary', title: '合计待发', width: 140 },
{ dataIndex: 'payableAgentSalary', key: 'payableAgentSalary', title: '代理差额', width: 120 },
{ dataIndex: 'payableSalary', key: 'payableSalary', title: '合计差额', width: 140 },
{ dataIndex: 'memberIssuedSalary', key: 'memberIssuedSalary', title: '主播实收', width: 140 },
{ dataIndex: 'agentIssuedSalary', key: 'agentIssuedSalary', title: '上级代理实收', width: 140 },
{ dataIndex: 'agentIssuedSalary', key: 'agentIssuedSalary', title: '代理实收', width: 140 },
{ dataIndex: 'totalIssuedSalary', key: 'totalIssuedSalary', title: '总发工资', width: 140 },
{ dataIndex: 'currentSalaryBalance', key: 'currentSalaryBalance', title: '当前工资', width: 140 },
{ dataIndex: 'action', fixed: 'right', key: 'action', title: '操作', width: 110 },
@ -172,7 +168,7 @@ watch(
);
watch(
[() => query.sysOrigin, () => query.countryCode, () => query.userId, () => query.billBelong],
[() => query.sysOrigin, () => query.countryCode, () => query.userId],
() => {
selectedUserIds.value = [];
},
@ -230,8 +226,19 @@ function formatMoney(value?: number | string) {
return `$${amount.toFixed(2)}`;
}
function shouldShowAgentSalary(record: Record<string, any>) {
if (record.agentMember !== undefined && record.agentMember !== null) {
return record.agentMember === true;
}
return (
!!record.userId &&
!!record.teamOwnId &&
String(record.userId) === String(record.teamOwnId)
);
}
function formatAgentMoney(record: Record<string, any>, key: string) {
return formatMoney(record[key]);
return shouldShowAgentSalary(record) ? formatMoney(record[key]) : '-';
}
function formatInteger(value?: number | string) {
@ -249,13 +256,6 @@ function countryLabel(code?: string) {
return `${country.phonePrefix || ''} ${country.countryName || country.aliasName || code}`;
}
function billBelongLabel() {
if (!query.billBelong || query.billBelong.length !== 6) {
return '当前账期';
}
return `${query.billBelong.slice(0, 4)}-${query.billBelong.slice(4, 6)}`;
}
function getRowKey(record: Record<string, any>) {
return String(record.id || record.userProfile?.account || record.userId || '');
}
@ -327,7 +327,6 @@ function toggleSalarySort(key?: number | string) {
async function submitManualSalary(userIds?: string[]) {
const hasSelectedUsers = !!userIds?.length;
resultRows.value = await confirmManualSalary({
billBelong: Number(query.billBelong),
countryCode: query.countryCode,
sysOrigin: query.sysOrigin,
...(hasSelectedUsers ? { userIds } : {}),
@ -355,8 +354,8 @@ function confirmPay() {
? '本次只发放已勾选的用户。'
: '未勾选用户,本次将发放当前筛选国家的所有可发工资用户。',
title: hasSelectedUsers
? `确认发放已选择的 ${userIds.length} ${billBelongLabel()} 工资?`
: `确认发放 ${countryLabel(query.countryCode)} ${billBelongLabel()} 工资?`,
? `确认发放已选择的 ${userIds.length}当前账期工资?`
: `确认发放 ${countryLabel(query.countryCode)} 当前账期工资?`,
async onOk() {
paying.value = true;
try {
@ -384,7 +383,7 @@ function settleUser(record: Record<string, any>) {
content: payableSalary < 0
? '该用户应发小于实收本次发放0并置为已结算不会对用户扣款。'
: '本次只结算当前这一行用户的工资。',
title: `确认结算用户 ${record.userProfile?.account || userId} ${billBelongLabel()} 工资?`,
title: `确认结算用户 ${record.userProfile?.account || userId}当前账期工资?`,
async onOk() {
settlingUserId.value = userId;
try {
@ -429,16 +428,6 @@ void loadCountries();
@change="loadData(true)"
@search="countryKeyword = $event"
/>
<DatePicker
v-model:value="query.billBelong"
:allow-clear="false"
format="YYYYMM"
picker="month"
placeholder="账期月份"
style="width: 140px"
value-format="YYYYMM"
@change="loadData(true)"
/>
<AccountInput
v-model:value="query.userId"
:sys-origin="query.sysOrigin"
@ -557,7 +546,7 @@ void loadCountries();
<template v-else-if="column.key === 'agentIssuedSalary'">
<span>{{ formatAgentMoney(record, 'agentIssuedSalary') }}</span>
<span
v-if="Number(record.agentOverIssuedSalary || 0) > 0"
v-if="shouldShowAgentSalary(record) && Number(record.agentOverIssuedSalary || 0) > 0"
class="over-issued-salary"
>
({{ formatMoney(record.agentOverIssuedSalary) }})

View File

@ -1,30 +1,33 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import {
computed,
reactive,
ref,
watch } from 'vue';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import {
exportInAppPurchaseDetails,
getOrderAmount,
listInAppPurchase,
} from '#/api/legacy/operate';
} from '#/api/legacy/operate';
import { regionConfigTable } from '#/api/legacy/system';
import AccountInput from '#/components/account-input.vue';
import SysOriginTag from '#/components/sys-origin-tag.vue';
import { formatDate, getAllowedSysOrigins } from '#/views/system/shared';
import { formatDate,
getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
DatePicker,
Input,
message,
Select,
Space,
Table,
Tag,
Tag
} from 'antdv-next';
import {
@ -47,7 +50,6 @@ const sysOriginOptions = computed(() => {
const loading = ref(false);
const loadMoreLoading = ref(false);
const exporting = ref(false);
const list = ref<Array<Record<string, any>>>([]);
const regions = ref<Array<Record<string, any>>>([]);
const totalInfo = ref<Record<string, any>>({});
@ -135,15 +137,10 @@ async function loadData(reset = false) {
}
}
function buildQueryParams(includePaging = true) {
function buildQueryParams() {
query.sysOrigins = getEffectiveSysOrigin();
const params = { ...query };
if (!includePaging) {
delete params.lastId;
delete params.limit;
}
return Object.fromEntries(
Object.entries(params).filter(([, value]) => {
Object.entries({ ...query }).filter(([, value]) => {
if (Array.isArray(value)) {
return value.length > 0;
}
@ -156,16 +153,6 @@ function handleSearch() {
void loadData(true);
}
async function handleExport() {
exporting.value = true;
try {
await exportInAppPurchaseDetails(buildQueryParams(false));
message.success('导出成功');
} finally {
exporting.value = false;
}
}
function openDetails(record: Record<string, any>) {
activeOrderId.value = record.details?.id || '';
detailsOpen.value = true;
@ -246,9 +233,6 @@ void loadData(true);
<Button :loading="loading" type="primary" @click="handleSearch">
搜索
</Button>
<Button :loading="exporting" @click="handleExport">
导出
</Button>
</div>
<div class="summary">

View File

@ -8,7 +8,6 @@ import { Page } from '@vben/common-ui';
import {
pagePayOpenCounty,
syncPayOpenCountryCurrencyRates,
updatePayOpenCountryCurrent,
updatePayOpenCountryExchangeRate,
updatePayOpenCountrySort,
@ -42,9 +41,6 @@ const activeRow = ref<Record<string, any>>({});
const editOpen = ref(false);
const editLoading = ref(false);
const editValue = ref('');
const syncOpen = ref(false);
const syncLoading = ref(false);
const syncMarkupPercent = ref('');
const editConfig = reactive({
field: '' as 'currency' | 'rate' | 'sort' | '',
title: '',
@ -176,34 +172,6 @@ function openAssociated(row: Record<string, any>) {
associatedOpen.value = true;
}
function openSyncRates() {
syncMarkupPercent.value = '';
syncOpen.value = true;
}
function validateSyncMarkupPercent() {
return /^(100(\.0{1,4})?|([1-9]\d?|0)(\.\d{1,4})?)$/.test(syncMarkupPercent.value.trim());
}
async function submitSyncRates() {
if (!validateSyncMarkupPercent()) {
message.warning('请输入0~100之间的上浮比例最多四位小数');
return;
}
syncLoading.value = true;
try {
const result = await syncPayOpenCountryCurrencyRates({
markupPercent: syncMarkupPercent.value.trim(),
});
const skippedText = result?.skippedCount ? `,跳过${result.skippedCount}` : '';
message.success(`同步成功,更新${result?.updatedCount || 0}个国家${skippedText}`);
syncOpen.value = false;
await loadData();
} finally {
syncLoading.value = false;
}
}
void loadData(true);
</script>
@ -220,7 +188,6 @@ void loadData(true);
/>
<Button type="primary" @click="countrySelectOpen = true">添加</Button>
<Button type="primary" @click="handleSearch">刷新</Button>
<Button :loading="syncLoading" @click="openSyncRates">同步全球汇率</Button>
</div>
<Table
@ -301,19 +268,6 @@ void loadData(true);
>
<Input v-model:value="editValue" />
</Modal>
<Modal
:confirm-loading="syncLoading"
:open="syncOpen"
title="同步全球汇率"
@cancel="syncOpen = false"
@ok="submitSyncRates"
>
<Input
v-model:value="syncMarkupPercent"
placeholder="请输入上浮比例5表示上浮5%"
/>
</Modal>
</Page>
</template>

View File

@ -1,5 +1,9 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import {
computed,
reactive,
ref,
watch } from 'vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
@ -7,10 +11,10 @@ import { useAccessStore } from '@vben/stores';
import {
exportFreightWaters,
pageFreightRunningWater,
} from '#/api/legacy/user';
import { listBackUserOptions } from '#/api/legacy/system';
} from '#/api/legacy/user';
import AccountInput from '#/components/account-input.vue';
import { formatDate, getAllowedSysOrigins } from '#/views/system/shared';
import { formatDate,
getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
@ -19,7 +23,7 @@ import {
Pagination,
Select,
Table,
message,
message
} from 'antdv-next';
import UserProfileLink from './components/user-profile-link.vue';
@ -46,14 +50,11 @@ const loading = ref(false);
const total = ref(0);
const list = ref<Array<Record<string, any>>>([]);
const exporting = ref(false);
const operatorsLoading = ref(false);
const operators = ref<Array<Record<string, any>>>([]);
const rangeDate = ref<[string, string] | null>(null);
const query = reactive<Record<string, any>>({
acceptUserId: '',
cursor: 1,
createUsers: [],
endTime: '',
limit: 20,
origin: undefined,
@ -75,17 +76,6 @@ const columns = [
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
];
const operatorOptions = computed(() =>
operators.value.map((item) => {
const name = item.nickname || item.loginName || item.name || item.id;
const account = item.loginName && item.loginName !== name ? ` / ${item.loginName}` : '';
return {
label: `${name}${account}`,
value: item.id,
};
}),
);
watch(
sysOriginOptions,
(options) => {
@ -113,63 +103,6 @@ watch(
{ immediate: true },
);
function normalizeOperators(items: Array<Record<string, any>>) {
const operatorMap = new Map<string, Record<string, any>>();
items.forEach((item) => {
const id = item.id || item.applicantId;
if (!id) {
return;
}
operatorMap.set(String(id), {
id,
loginName: item.loginName,
name: item.name,
nickname: item.nickname || item.applicantName,
status: item.status,
});
});
return [...operatorMap.values()];
}
function filterOperatorOption(input: string, option: any) {
return String(option?.label || '')
.toLowerCase()
.includes(input.toLowerCase());
}
async function loadOperators() {
operatorsLoading.value = true;
try {
const result = await listBackUserOptions();
operators.value = normalizeOperators(Array.isArray(result) ? result : []);
} catch {
operators.value = [];
} finally {
operatorsLoading.value = false;
}
}
function buildFreightRunningWaterParams(withPage = true) {
const createUsers = Array.isArray(query.createUsers)
? query.createUsers.filter(Boolean).join(',')
: '';
const params: Record<string, any> = {
acceptUserId: query.acceptUserId || undefined,
createUsers: createUsers || undefined,
endTime: query.endTime || undefined,
origin: query.origin || undefined,
queryBackOperationUser: query.queryBackOperationUser,
startTime: query.startTime || undefined,
sysOrigin: query.sysOrigin,
userId: query.userId || undefined,
};
if (withPage) {
params.cursor = query.cursor;
params.limit = query.limit;
}
return params;
}
async function loadData(reset = false) {
if (!query.sysOrigin) {
return;
@ -179,7 +112,7 @@ async function loadData(reset = false) {
}
loading.value = true;
try {
const result = await pageFreightRunningWater(buildFreightRunningWaterParams());
const result = await pageFreightRunningWater({ ...query });
list.value = result.records || [];
total.value = result.total || 0;
} finally {
@ -204,26 +137,19 @@ async function handleExport() {
}
exporting.value = true;
try {
await exportFreightWaters(
buildFreightRunningWaterParams(false),
`ExportFreightWaters_${query.startTime || 'all'}_${query.endTime || 'all'}`,
);
await exportFreightWaters({ ...query });
message.success('导出成功');
} finally {
exporting.value = false;
}
}
void loadOperators();
void loadData(true);
</script>
<template>
<component
:is="queryBackOperation ? 'div' : Page"
:title="queryBackOperation ? undefined : '货运代理流水'"
>
<component :is="queryBackOperation ? 'div' : Card">
<Page v-if="!queryBackOperation" title="货运代理流水">
<Card>
<div class="toolbar">
<SysOriginSelect
v-model:value="query.sysOrigin"
@ -249,21 +175,9 @@ void loadData(true);
allow-clear
placeholder="来源"
style="width: 140px"
:options="FREIGHT_BALANCE_ORIGIN_OPTIONS.map((item) => ({ label: `${item.name}`, value: item.value as any }))"
/>
<Select
v-model:value="query.createUsers"
allow-clear
mode="multiple"
show-search
max-tag-count="responsive"
option-label-prop="label"
placeholder="操作人"
style="width: 220px"
:filter-option="filterOperatorOption"
:loading="operatorsLoading"
:options="operatorOptions"
/>
<DatePicker.RangePicker
v-model:value="rangeDate"
show-time
@ -320,8 +234,8 @@ void loadData(true);
@showSizeChange="handlePageChange"
/>
</div>
</component>
</component>
</Card>
</Page>
</template>
<style scoped>

View File

@ -42,7 +42,6 @@ function createRowKey(seed?: number | string) {
function createLevel(level = 1, rechargeAmount = 10) {
return {
enabled: true,
googleProductId: '',
id: null,
level,
rechargeAmount,
@ -86,7 +85,6 @@ const form = reactive<Record<string, any>>({
enabled: false,
id: 0,
levelConfigs: defaultLevels(),
minAppVersion: '',
sysOrigin: '',
updateTime: '',
});
@ -95,7 +93,6 @@ const levelColumns = [
{ dataIndex: 'enabled', key: 'enabled', title: '启用', width: 90 },
{ dataIndex: 'level', key: 'level', title: '档位', width: 110 },
{ dataIndex: 'rechargeAmount', key: 'rechargeAmount', title: '首冲金额', width: 180 },
{ dataIndex: 'googleProductId', key: 'googleProductId', title: '谷歌商品ID', width: 240 },
{ dataIndex: 'rewardGroup', key: 'rewardGroup', title: '奖励资源组', width: 420 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 100 },
];
@ -166,7 +163,6 @@ function normalizeConfig(result: null | Record<string, any> | undefined) {
form.configured = Boolean(value.configured);
form.enabled = Boolean(value.enabled);
form.id = value.id || 0;
form.minAppVersion = String(value.minAppVersion || '');
form.sysOrigin = String(value.sysOrigin || selectedSysOrigin.value || '');
form.updateTime = String(value.updateTime || '');
const levels = Array.isArray(value.levelConfigs) && value.levelConfigs.length > 0
@ -179,7 +175,6 @@ function normalizeConfig(result: null | Record<string, any> | undefined) {
);
return {
enabled: item.enabled !== false,
googleProductId: String(item.googleProductId || ''),
id: item.id ?? null,
level: Number(item.level || index + 1),
rechargeAmount: Number.isFinite(rechargeAmount) ? rechargeAmount : 0,
@ -282,7 +277,6 @@ function validateBeforeSave() {
}
const seenLevels = new Set<string>();
const seenAmounts = new Set<string>();
const seenGoogleProductIds = new Set<string>();
for (const item of levels) {
const level = Number(item.level || 0);
const amountCents = toAmountCents(item.rechargeAmount);
@ -306,14 +300,6 @@ function validateBeforeSave() {
message.warning('首冲金额不能重复');
return false;
}
const googleProductId = String(item.googleProductId || '').trim();
if (googleProductId) {
if (seenGoogleProductIds.has(googleProductId)) {
message.warning('谷歌商品ID不能重复');
return false;
}
seenGoogleProductIds.add(googleProductId);
}
seenLevels.add(String(level));
seenAmounts.add(String(amountCents));
}
@ -321,10 +307,6 @@ function validateBeforeSave() {
message.warning('启用配置时至少要启用一个首冲奖励档位');
return false;
}
if (form.enabled && !String(form.minAppVersion || '').trim()) {
message.warning('启用配置时需要填写最低App版本');
return false;
}
return true;
}
@ -339,7 +321,6 @@ async function submitForm() {
id: form.id || 0,
levelConfigs: (form.levelConfigs || []).map((item: Record<string, any>) => ({
enabled: item.enabled !== false,
googleProductId: String(item.googleProductId || '').trim(),
id: item.id || 0,
level: Number(item.level || 0),
rechargeAmount: formatAmount(item.rechargeAmount),
@ -347,7 +328,6 @@ async function submitForm() {
rewardGroupId: normalizeID(item.rewardGroupId) || null,
rewardGroupName: String(item.rewardGroupName || ''),
})),
minAppVersion: String(form.minAppVersion || '').trim(),
sysOrigin: selectedSysOrigin.value,
});
message.success('保存成功');
@ -441,13 +421,6 @@ watch(activeTab, (value) => {
<Space align="center" wrap>
<span class="field-label">启用</span>
<Switch v-model:checked="form.enabled" />
<span class="field-label">最低App版本</span>
<Input
v-model:value="form.minAppVersion"
allow-clear
placeholder="例如 1.5.0"
style="width: 160px"
/>
<span class="sub-text">更新时间{{ form.updateTime || '-' }}</span>
</Space>
</div>
@ -458,7 +431,7 @@ watch(activeTab, (value) => {
:loading="loading"
:pagination="false"
row-key="rowKey"
:scroll="{ x: 1140 }"
:scroll="{ x: 900 }"
>
<template #bodyCell="{ column, index, record }">
<template v-if="column.key === 'enabled'">
@ -480,14 +453,6 @@ watch(activeTab, (value) => {
style="width: 140px"
/>
</template>
<template v-else-if="column.key === 'googleProductId'">
<Input
v-model:value="record.googleProductId"
allow-clear
placeholder="Google商品ID"
style="width: 200px"
/>
</template>
<template v-else-if="column.key === 'rewardGroup'">
<div class="reward-group-cell">
<Space wrap>

View File

@ -196,9 +196,7 @@ const recordSummary = reactive<Record<string, any>>({
endTime: '',
participantCount: 0,
startTime: '',
totalDecorationRewardGold: 0,
totalDrawTimes: 0,
totalGiftRewardGold: 0,
totalPaidGold: 0,
totalRewardGold: 0,
});
@ -420,9 +418,7 @@ function normalizeRecordPage(result: null | Record<string, any> | undefined) {
endTime: String(value.endTime || ''),
participantCount: Number(value.participantCount || 0),
startTime: String(value.startTime || ''),
totalDecorationRewardGold: Number(value.totalDecorationRewardGold || 0),
totalDrawTimes: Number(value.totalDrawTimes || 0),
totalGiftRewardGold: Number(value.totalGiftRewardGold || 0),
totalPaidGold: Number(value.totalPaidGold || 0),
totalRewardGold: Number(value.totalRewardGold || 0),
});
@ -1177,12 +1173,8 @@ watch(activePageTab, (value) => {
<strong>{{ numberText(recordSummary.totalPaidGold) }}</strong>
</div>
<div class="record-summary-item">
<span>礼物价值</span>
<strong>{{ numberText(recordSummary.totalGiftRewardGold) }}</strong>
</div>
<div class="record-summary-item">
<span>装扮价值</span>
<strong>{{ numberText(recordSummary.totalDecorationRewardGold) }}</strong>
<span>礼物发放金币总价值</span>
<strong>{{ numberText(recordSummary.totalRewardGold) }}</strong>
</div>
<div class="record-summary-item">
<span>用户总抽奖次数</span>
@ -1326,7 +1318,7 @@ watch(activePageTab, (value) => {
.record-summary {
display: grid;
gap: 10px;
grid-template-columns: repeat(5, minmax(160px, 1fr));
grid-template-columns: repeat(4, minmax(160px, 1fr));
margin-bottom: 16px;
}

View File

@ -164,7 +164,6 @@ const metricColumns = new Set([
'd7RetentionRate',
'd30RetentionRate',
'dailyActiveUser',
'dailyRechargeUser',
'dealerRecharge',
'gamePayout',
'gamePayoutRate',
@ -182,18 +181,15 @@ const metricColumns = new Set([
'luckyGiftUser',
'luckyGiftUserRate',
'mifapayRecharge',
'newDealerUserRecharge',
'newUserRecharge',
'salaryExchange',
'salaryTransfer',
'totalRecharge',
'userRecharge',
]);
const integerMetricColumns = new Set([
'countryNewUser',
'dailyActiveUser',
'dailyRechargeUser',
'gameUser',
'luckyGiftUser',
]);
@ -212,11 +208,8 @@ const percentMetricColumns = new Set([
const summaryMetricKeys = [
'countryNewUser',
'dailyActiveUser',
'dailyRechargeUser',
'newUserRecharge',
'dealerRecharge',
'newDealerUserRecharge',
'userRecharge',
'mifapayRecharge',
'googleRecharge',
'salaryExchange',
@ -242,11 +235,8 @@ const summaryMetricKeys = [
const summaryMetricDefinitions = [
{ key: 'countryNewUser', label: '新增' },
{ key: 'dailyActiveUser', label: '当日活跃' },
{ key: 'dailyRechargeUser', label: '新增当日充值用户数' },
{ key: 'newUserRecharge', label: '新增用户充值' },
{ key: 'dealerRecharge', label: '币商平台充值' },
{ key: 'newDealerUserRecharge', label: '新增币商用户充值' },
{ key: 'userRecharge', label: '用户充值' },
{ key: 'newUserRecharge', label: '新增充值' },
{ key: 'dealerRecharge', label: '币商充值' },
{ key: 'mifapayRecharge', label: 'MifaPay 充值' },
{ key: 'googleRecharge', label: 'Google 充值' },
{ key: 'salaryExchange', label: '工资兑换' },
@ -294,11 +284,8 @@ const overviewColumns: any[] = [
{ align: 'right', dataIndex: 'd1RetentionRate', key: 'd1RetentionRate', title: '次日留存率', width: 140 },
{ align: 'right', dataIndex: 'd7RetentionRate', key: 'd7RetentionRate', title: '七日留存率', width: 140 },
{ align: 'right', dataIndex: 'd30RetentionRate', key: 'd30RetentionRate', title: '30 日留存率', width: 150 },
{ align: 'right', dataIndex: 'dailyRechargeUser', key: 'dailyRechargeUser', title: '新增当日充值用户数', width: 180 },
{ align: 'right', dataIndex: 'newUserRecharge', key: 'newUserRecharge', title: '新增用户充值', width: 140 },
{ align: 'right', dataIndex: 'dealerRecharge', key: 'dealerRecharge', title: '币商平台充值', width: 140 },
{ align: 'right', dataIndex: 'newDealerUserRecharge', key: 'newDealerUserRecharge', title: '新增币商用户充值', width: 170 },
{ align: 'right', dataIndex: 'userRecharge', key: 'userRecharge', title: '用户充值', width: 130 },
{ align: 'right', dataIndex: 'newUserRecharge', key: 'newUserRecharge', title: '新增充值', width: 130 },
{ align: 'right', dataIndex: 'dealerRecharge', key: 'dealerRecharge', title: '币商充值', width: 130 },
{ align: 'right', dataIndex: 'mifapayRecharge', key: 'mifapayRecharge', title: 'MifaPay 充值', width: 150 },
{ align: 'right', dataIndex: 'googleRecharge', key: 'googleRecharge', title: 'Google 充值', width: 140 },
{ align: 'right', dataIndex: 'salaryExchange', key: 'salaryExchange', title: '工资兑换', width: 130 },
@ -328,9 +315,7 @@ const rechargeColumns: any[] = [
{ dataIndex: 'countryName', fixed: 'left' as const, key: 'countryName', title: '国家', width: 150 },
{ dataIndex: 'countryCode', key: 'countryCode', title: 'Code', width: 86 },
{ dataIndex: 'periodName', key: 'periodName', title: '周期', width: 110 },
{ align: 'right', dataIndex: 'dealerRecharge', key: 'dealerRecharge', title: '币商平台充值', width: 150 },
{ align: 'right', dataIndex: 'newDealerUserRecharge', key: 'newDealerUserRecharge', title: '新增币商用户充值', width: 170 },
{ align: 'right', dataIndex: 'userRecharge', key: 'userRecharge', title: '用户充值', width: 140 },
{ align: 'right', dataIndex: 'dealerRecharge', key: 'dealerRecharge', title: '币商充值', width: 140 },
{ align: 'right', dataIndex: 'mifapayRecharge', key: 'mifapayRecharge', title: 'MifaPay 充值', width: 160 },
{ align: 'right', dataIndex: 'googleRecharge', key: 'googleRecharge', title: 'Google 充值', width: 150 },
{ align: 'right', dataIndex: 'totalRecharge', key: 'totalRecharge', title: '总充值', width: 140 },
@ -400,7 +385,7 @@ const rechargeDetailScrollX = rechargeDetailColumns.reduce(
0,
);
const rechargeSourceNameMap: Record<string, string> = {
DEALER: '币商平台充值',
DEALER: '币商充值',
MIFAPAY: 'MifaPay 充值',
OFFICIAL: '官方充值',
};
@ -461,7 +446,6 @@ const appDataCards = computed(() => {
const arpuCards = computed(() => {
return [
{ label: '日 ARPU', value: formatArpu(arpuDashboards.value.day) },
{ label: '充值用户日 ARPU', value: formatRechargeUserDayArpu(arpuDashboards.value.day) },
{ label: '3 日 ARPU', value: formatArpu(arpuDashboards.value.threeDay) },
{ label: '周 ARPU', value: formatArpu(arpuDashboards.value.week) },
{ label: '筛选 ARPU', value: formatAmount(calcArpu(visibleSummary.value)) },
@ -471,10 +455,6 @@ const totalRechargeCard = computed(() => ({
label: '总充值',
value: formatAmount(visibleSummary.value.totalRecharge),
}));
const userRechargeCard = computed(() => ({
label: '用户充值',
value: formatAmount(visibleSummary.value.userRecharge),
}));
function hasPermission(code: string) {
const codes = accessCodes.value;
@ -522,18 +502,36 @@ function buildParams() {
return params;
}
function buildRecentDayParams(dayCount: number) {
const end = dayjs().tz(DEFAULT_STAT_TIMEZONE);
const start = end.subtract(dayCount - 1, 'day');
return {
periodType: 'DAY',
startDate: start.format('YYYY-MM-DD'),
endDate: end.format('YYYY-MM-DD'),
statTimezone: DEFAULT_STAT_TIMEZONE,
sysOrigin: query.sysOrigin || undefined,
};
}
async function loadDashboard() {
if (!canQuery.value) {
return;
}
loading.value = true;
try {
const mainDashboard = await countryDashboard(buildParams());
const [mainDashboard, dayArpuDashboard, threeDayArpuDashboard, weekArpuDashboard] =
await Promise.all([
countryDashboard(buildParams()),
countryDashboard(buildRecentDayParams(1)),
countryDashboard(buildRecentDayParams(3)),
countryDashboard(buildRecentDayParams(7)),
]);
dashboard.value = mainDashboard;
arpuDashboards.value = {
day: mainDashboard.arpuSummary?.day || null,
threeDay: mainDashboard.arpuSummary?.threeDay || null,
week: mainDashboard.arpuSummary?.week || null,
day: dayArpuDashboard,
threeDay: threeDayArpuDashboard,
week: weekArpuDashboard,
};
if (activeTableTab.value === 'game') {
await loadGameMetrics();
@ -820,24 +818,11 @@ function calcArpu(summary: Record<string, any>) {
return toAmount(summary.totalRecharge) / activeUser;
}
function calcRechargeUserDayArpu(summary: Record<string, any>) {
const dailyRechargeUser = toAmount(summary.dailyRechargeUser);
if (dailyRechargeUser === 0) {
return 0;
}
return toAmount(summary.userRecharge) / dailyRechargeUser;
}
function formatArpu(result?: CountryDashboardResult | null) {
const summary = buildVisibleSummary(filterRecordsBySelectedCountries(result?.records || []));
return formatAmount(calcArpu(summary));
}
function formatRechargeUserDayArpu(result?: CountryDashboardResult | null) {
const summary = buildVisibleSummary(filterRecordsBySelectedCountries(result?.records || []));
return formatAmount(calcRechargeUserDayArpu(summary));
}
function emptySummary(): SummaryMetric {
return {
countryNewUser: 0,
@ -851,7 +836,6 @@ function emptySummary(): SummaryMetric {
d30RetentionRate: 0,
d30RetentionUser: 0,
dailyActiveUser: 0,
dailyRechargeUser: 0,
dealerRecharge: 0,
googleRecharge: 0,
gamePayout: 0,
@ -869,12 +853,10 @@ function emptySummary(): SummaryMetric {
luckyGiftUser: 0,
luckyGiftUserRate: 0,
mifapayRecharge: 0,
newDealerUserRecharge: 0,
newUserRecharge: 0,
salaryExchange: 0,
salaryTransfer: 0,
totalRecharge: 0,
userRecharge: 0,
};
}
@ -1093,11 +1075,8 @@ async function renderVisualCharts() {
}
renderUserRechargeChart(baseChartOption('新增用户与充值', list, [
amountSeries('新增用户', 'countryNewUser', list),
amountSeries('当日充值用户数', 'dailyRechargeUser', list),
amountSeries('新增用户充值', 'newUserRecharge', list),
amountSeries('币商平台充值', 'dealerRecharge', list),
amountSeries('新增币商用户充值', 'newDealerUserRecharge', list),
amountSeries('用户充值', 'userRecharge', list),
amountSeries('新增充值', 'newUserRecharge', list),
amountSeries('币商充值', 'dealerRecharge', list),
amountSeries('MifaPay 充值', 'mifapayRecharge', list),
amountSeries('Google 充值', 'googleRecharge', list),
amountSeries('工资兑换', 'salaryExchange', list),
@ -1263,10 +1242,6 @@ onBeforeUnmount(() => {
<div class="metric-card__label">{{ totalRechargeCard.label }}</div>
<div class="metric-card__value">{{ totalRechargeCard.value }}</div>
</div>
<div class="metric-card">
<div class="metric-card__label">{{ userRechargeCard.label }}</div>
<div class="metric-card__value">{{ userRechargeCard.value }}</div>
</div>
</div>
</section>
@ -1608,7 +1583,7 @@ onBeforeUnmount(() => {
}
.metric-card-grid--recharge {
grid-template-columns: repeat(2, minmax(180px, 260px));
grid-template-columns: minmax(180px, 260px);
}
.metric-card {

View File

@ -13,7 +13,6 @@ import {
FormItem,
Input,
Modal,
Switch,
message,
} from 'antdv-next';
@ -30,19 +29,7 @@ const emit = defineEmits<{
const saving = ref(false);
type FeatureSwitchKey =
| 'canAddBdLeader'
| 'canBanUser'
| 'canChangeCountry'
| 'canGiftProps'
| 'canUnbanUser';
const form = reactive({
canAddBdLeader: true,
canBanUser: true,
canChangeCountry: true,
canGiftProps: true,
canUnbanUser: true,
contact: '',
id: '',
regionId: '',
@ -53,46 +40,6 @@ const form = reactive({
const isUpdate = computed(() => Boolean(props.row?.id));
const title = computed(() => (isUpdate.value ? '修改经理' : '添加经理'));
function normalizeFeatureEnabled(value: unknown) {
if (value === undefined || value === null || value === '') {
return true;
}
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
return value !== 0;
}
const normalized = String(value).trim().toLowerCase();
return !['0', 'false', 'n', 'no', 'off', '关', '关闭'].includes(normalized);
}
function camelToSnake(key: string) {
return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
}
function featurePayloadKey(key: FeatureSwitchKey) {
const bodyKey = key.replace(/^can/, '');
return bodyKey ? `${bodyKey[0]?.toLowerCase()}${bodyKey.slice(1)}` : key;
}
function rowFeatureValue(row: null | Record<string, any>, key: FeatureSwitchKey) {
const snakeKey = camelToSnake(key);
const value = Object.prototype.hasOwnProperty.call(row || {}, key)
? row?.[key]
: undefined;
if (value !== undefined) {
return value;
}
if (Object.prototype.hasOwnProperty.call(row || {}, snakeKey)) {
return row?.[snakeKey];
}
const payloadKey = featurePayloadKey(key);
return Object.prototype.hasOwnProperty.call(row?.features || {}, payloadKey)
? row?.features?.[payloadKey]
: row?.features?.[key];
}
watch(
() => props.open,
(open) => {
@ -104,23 +51,10 @@ watch(
form.sysOrigin = props.row?.sysOrigin || props.sysOrigin || 'LIKEI';
form.contact = props.row?.contact || '';
form.regionId = String(props.row?.regionId || '');
form.canAddBdLeader = normalizeFeatureEnabled(rowFeatureValue(props.row, 'canAddBdLeader'));
form.canGiftProps = normalizeFeatureEnabled(rowFeatureValue(props.row, 'canGiftProps'));
form.canBanUser = normalizeFeatureEnabled(rowFeatureValue(props.row, 'canBanUser'));
form.canUnbanUser = normalizeFeatureEnabled(rowFeatureValue(props.row, 'canUnbanUser'));
form.canChangeCountry = normalizeFeatureEnabled(rowFeatureValue(props.row, 'canChangeCountry'));
},
{ immediate: true },
);
function setFeature(key: FeatureSwitchKey, checked: unknown) {
form[key] = normalizeFeatureEnabled(checked);
}
function toggleFeature(key: FeatureSwitchKey) {
form[key] = !form[key];
}
async function handleSubmit() {
if (!isUpdate.value && !String(form.userId || '').trim()) {
message.warning('请输入经理用户ID');
@ -133,11 +67,6 @@ async function handleSubmit() {
saving.value = true;
try {
const payload = {
canAddBdLeader: form.canAddBdLeader,
canBanUser: form.canBanUser,
canChangeCountry: form.canChangeCountry,
canGiftProps: form.canGiftProps,
canUnbanUser: form.canUnbanUser,
contact: form.contact.trim(),
id: form.id || undefined,
regionId: form.regionId,
@ -185,123 +114,6 @@ async function handleSubmit() {
<FormItem label="联系方式">
<Input v-model:value="form.contact" placeholder="联系方式" />
</FormItem>
<FormItem label="功能开关">
<div class="feature-switches">
<div
class="feature-switch-row"
role="switch"
tabindex="0"
:aria-checked="form.canAddBdLeader"
@click="toggleFeature('canAddBdLeader')"
@keydown.enter.prevent="toggleFeature('canAddBdLeader')"
@keydown.space.prevent="toggleFeature('canAddBdLeader')"
>
<span>加BD Leader</span>
<span class="feature-switch-control" @click.stop>
<Switch
:checked="form.canAddBdLeader"
@change="(checked) => setFeature('canAddBdLeader', checked)"
/>
</span>
</div>
<div
class="feature-switch-row"
role="switch"
tabindex="0"
:aria-checked="form.canGiftProps"
@click="toggleFeature('canGiftProps')"
@keydown.enter.prevent="toggleFeature('canGiftProps')"
@keydown.space.prevent="toggleFeature('canGiftProps')"
>
<span>赠送道具</span>
<span class="feature-switch-control" @click.stop>
<Switch
:checked="form.canGiftProps"
@change="(checked) => setFeature('canGiftProps', checked)"
/>
</span>
</div>
<div
class="feature-switch-row"
role="switch"
tabindex="0"
:aria-checked="form.canBanUser"
@click="toggleFeature('canBanUser')"
@keydown.enter.prevent="toggleFeature('canBanUser')"
@keydown.space.prevent="toggleFeature('canBanUser')"
>
<span>封禁用户</span>
<span class="feature-switch-control" @click.stop>
<Switch
:checked="form.canBanUser"
@change="(checked) => setFeature('canBanUser', checked)"
/>
</span>
</div>
<div
class="feature-switch-row"
role="switch"
tabindex="0"
:aria-checked="form.canUnbanUser"
@click="toggleFeature('canUnbanUser')"
@keydown.enter.prevent="toggleFeature('canUnbanUser')"
@keydown.space.prevent="toggleFeature('canUnbanUser')"
>
<span>解封用户</span>
<span class="feature-switch-control" @click.stop>
<Switch
:checked="form.canUnbanUser"
@change="(checked) => setFeature('canUnbanUser', checked)"
/>
</span>
</div>
<div
class="feature-switch-row"
role="switch"
tabindex="0"
:aria-checked="form.canChangeCountry"
@click="toggleFeature('canChangeCountry')"
@keydown.enter.prevent="toggleFeature('canChangeCountry')"
@keydown.space.prevent="toggleFeature('canChangeCountry')"
>
<span>改国家</span>
<span class="feature-switch-control" @click.stop>
<Switch
:checked="form.canChangeCountry"
@change="(checked) => setFeature('canChangeCountry', checked)"
/>
</span>
</div>
</div>
</FormItem>
</Form>
</Modal>
</template>
<style scoped>
.feature-switches {
display: grid;
gap: 10px;
}
.feature-switch-row {
align-items: center;
border: 1px solid #e5e7eb;
border-radius: 6px;
cursor: pointer;
display: flex;
justify-content: space-between;
padding: 10px 12px;
}
.feature-switch-row:focus-visible {
border-color: #1677ff;
outline: 2px solid rgb(22 119 255 / 20%);
outline-offset: 2px;
}
.feature-switch-control {
align-items: center;
display: inline-flex;
}
</style>

View File

@ -23,7 +23,6 @@ import {
Pagination,
Space,
Table,
Tag,
message,
} from 'antdv-next';
@ -56,21 +55,12 @@ const columns = [
{ dataIndex: 'userProfile', key: 'userProfile', title: '经理', width: 240 },
{ dataIndex: 'regionName', key: 'regionName', title: '区域', width: 140 },
{ dataIndex: 'contact', key: 'contact', title: '联系方式', width: 180 },
{ dataIndex: 'features', key: 'features', title: '功能开关', width: 340 },
{ dataIndex: 'createUserName', key: 'createUserName', title: '创建人', width: 140 },
{ dataIndex: 'updateUserName', key: 'updateUserName', title: '修改人', width: 140 },
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 120 },
];
const managerFeatureSwitches = [
{ key: 'canAddBdLeader', label: '加BD Leader' },
{ key: 'canGiftProps', label: '赠送道具' },
{ key: 'canBanUser', label: '封禁用户' },
{ key: 'canUnbanUser', label: '解封用户' },
{ key: 'canChangeCountry', label: '改国家' },
];
function hasPermission(code: string) {
const codes = accessStore.accessCodes || [];
return codes.length === 0 || codes.includes(code);
@ -85,46 +75,6 @@ function getUserShortId(profile?: Record<string, any> | null) {
return profile?.actualAccount || profile?.account || profile?.id || '-';
}
function normalizeFeatureEnabled(value: unknown) {
if (value === undefined || value === null || value === '') {
return true;
}
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
return value !== 0;
}
const normalized = String(value).trim().toLowerCase();
return !['0', 'false', 'n', 'no', 'off', '关', '关闭'].includes(normalized);
}
function camelToSnake(key: string) {
return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
}
function featurePayloadKey(key: string) {
const bodyKey = key.replace(/^can/, '');
return bodyKey ? `${bodyKey[0]?.toLowerCase()}${bodyKey.slice(1)}` : key;
}
function isFeatureEnabled(record: Record<string, any>, key: string) {
const snakeKey = camelToSnake(key);
let value = Object.prototype.hasOwnProperty.call(record || {}, key)
? record?.[key]
: undefined;
if (value === undefined && Object.prototype.hasOwnProperty.call(record || {}, snakeKey)) {
value = record?.[snakeKey];
}
if (value === undefined && record?.features) {
const payloadKey = featurePayloadKey(key);
value = Object.prototype.hasOwnProperty.call(record.features, payloadKey)
? record.features[payloadKey]
: record.features[key];
}
return normalizeFeatureEnabled(value);
}
async function loadData(reset = false) {
if (reset) {
query.cursor = 1;
@ -236,17 +186,6 @@ loadData(true);
<template v-else-if="column.key === 'contact'">
{{ canEdit ? record.contact || '-' : '-' }}
</template>
<template v-else-if="column.key === 'features'">
<Space wrap :size="[4, 4]">
<Tag
v-for="item in managerFeatureSwitches"
:key="item.key"
:color="isFeatureEnabled(record, item.key) ? 'green' : 'red'"
>
{{ item.label }}{{ isFeatureEnabled(record, item.key) ? '开' : '关' }}
</Tag>
</Space>
</template>
<template v-else-if="column.key === 'createTime'">
{{ formatDate(record.createTime) }}
</template>

View File

@ -5,7 +5,8 @@ const DEV_API_INTERNAL_PREFIX = '/__dev_api_internal__';
const DEV_API_PROD_PREFIX = '/__dev_api_prod__';
const DEV_INTERNAL_CONSOLE_TARGET = 'http://127.0.0.1:2700';
const DEV_INTERNAL_GOLANG_TARGET = 'http://127.0.0.1:2900';
const DEV_PROD_TARGET = 'https://yumiadmin-acc.global-interaction.com';
const DEV_PROD_GOLANG_TARGET = 'https://jvapi.haiyihy.com';
const DEV_PROD_TARGET = 'https://yumi-admin.haiyihy.com';
function getBundledAssetFileName(assetInfo: {
name?: string;
@ -78,11 +79,8 @@ export default defineConfig(async () => {
[`${DEV_API_PROD_PREFIX}/go`]: {
changeOrigin: true,
rewrite: (path) =>
path.replace(
createPrefixRegExp(`${DEV_API_PROD_PREFIX}/go`),
`${DEV_CONSOLE_PREFIX}/go`,
),
target: DEV_PROD_TARGET,
path.replace(createPrefixRegExp(DEV_API_PROD_PREFIX), ''),
target: DEV_PROD_GOLANG_TARGET,
ws: true,
},
[DEV_API_PROD_PREFIX]: {

View File

@ -650,71 +650,6 @@ SET
router = 'baishun-config'
WHERE alias = 'OperateBaishunGameManage';
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
next_id,
(
SELECT game_manager.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'OperateGameManager'
LIMIT 1
) AS game_manager
),
'热游配置',
'operate/hotgame-config',
2,
'form',
'0',
'0',
4,
0,
'hotgame-config',
'OperateHotgameConfig'
FROM (
SELECT COALESCE(MAX(id), 1700000000) + 1 AS next_id
FROM sys_menu
) AS menu_id
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'OperateHotgameConfig'
);
UPDATE sys_menu
SET
parent_id = (
SELECT game_manager.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'OperateGameManager'
LIMIT 1
) AS game_manager
),
menu_name = '热游配置',
path = 'operate/hotgame-config',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 4,
status = 0,
router = 'hotgame-config'
WHERE alias = 'OperateHotgameConfig';
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT
parent_role.role_id,
@ -734,8 +669,7 @@ JOIN sys_menu AS target_menu
'ResidentDailyTaskConfig',
'OperateGameManager',
'GameConfig',
'OperateBaishunGameManage',
'OperateHotgameConfig'
'OperateBaishunGameManage'
)
LEFT JOIN sys_role_menu AS existing_role_menu
ON existing_role_menu.role_id = parent_role.role_id
@ -743,21 +677,6 @@ LEFT JOIN sys_role_menu AS existing_role_menu
WHERE parent_role.menu_id = 1700000057
AND existing_role_menu.id IS NULL;
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT
source_role.role_id,
target_menu.id
FROM sys_role_menu AS source_role
JOIN sys_menu AS source_menu
ON source_menu.id = source_role.menu_id
JOIN sys_menu AS target_menu
ON target_menu.alias = 'OperateHotgameConfig'
LEFT JOIN sys_role_menu AS existing_role_menu
ON existing_role_menu.role_id = source_role.role_id
AND existing_role_menu.menu_id = target_menu.id
WHERE source_menu.alias IN ('OperateGameManager', 'GameConfig', 'OperateBaishunGameManage', 'OperateLingxianConfig')
AND existing_role_menu.id IS NULL;
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT
parent_role.role_id,
@ -912,10 +831,6 @@ UPDATE sys_menu
SET menu_name = CONVERT(0xE799BEE9A1BAE9858DE7BDAE USING utf8mb4)
WHERE alias = 'OperateBaishunGameManage';
UPDATE sys_menu
SET menu_name = CONVERT(0xE783ADE6B8B8E9858DE7BDAE USING utf8mb4)
WHERE alias = 'OperateHotgameConfig';
SET @resident_activity_id := (
SELECT id FROM sys_menu WHERE alias = 'ResidentActivityManager' LIMIT 1
);

View File

@ -1,81 +0,0 @@
SET NAMES utf8mb4;
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
next_id,
(
SELECT game_manager.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'OperateGameManager'
LIMIT 1
) AS game_manager
),
'热游配置',
'operate/hotgame-config',
2,
'form',
'0',
'0',
4,
0,
'hotgame-config',
'OperateHotgameConfig'
FROM (
SELECT COALESCE(MAX(id), 1700000000) + 1 AS next_id
FROM sys_menu
) AS menu_id
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'OperateHotgameConfig'
);
UPDATE sys_menu
SET
parent_id = (
SELECT game_manager.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'OperateGameManager'
LIMIT 1
) AS game_manager
),
menu_name = '热游配置',
path = 'operate/hotgame-config',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 4,
status = 0,
router = 'hotgame-config'
WHERE alias = 'OperateHotgameConfig';
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT DISTINCT
source_role.role_id,
target_menu.id
FROM sys_role_menu AS source_role
JOIN sys_menu AS source_menu
ON source_menu.id = source_role.menu_id
JOIN sys_menu AS target_menu
ON target_menu.alias = 'OperateHotgameConfig'
LEFT JOIN sys_role_menu AS existing_role_menu
ON existing_role_menu.role_id = source_role.role_id
AND existing_role_menu.menu_id = target_menu.id
WHERE source_menu.alias IN ('OperateGameManager', 'GameConfig', 'OperateBaishunGameManage', 'OperateLingxianConfig')
AND existing_role_menu.id IS NULL;