Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e41b630e02 | ||
|
|
9678f262a9 | ||
|
|
25ccb09f31 | ||
|
|
a73b079333 | ||
|
|
7510dac801 | ||
|
|
5befd6297b | ||
|
|
cd9cbea7d6 | ||
|
|
d56fc0f50e | ||
|
|
915c8bf0be | ||
|
|
c22cff97c3 | ||
|
|
1995f9b078 | ||
|
|
33db3e0ea2 | ||
|
|
66568253aa | ||
|
|
b94e6db9af | ||
|
|
2ef1aa24ea | ||
|
|
a62e5a65a5 | ||
|
|
d92285ae4a | ||
|
|
b2fb090fb1 |
@ -41,9 +41,11 @@
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"@types/json-bigint": "catalog:",
|
||||
"ali-oss": "^6.23.0",
|
||||
"antdv-next": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"json-bigint": "catalog:",
|
||||
"pinia": "catalog:",
|
||||
"sockjs-client": "^1.6.1",
|
||||
"stompjs": "^2.3.3",
|
||||
|
||||
@ -108,3 +108,51 @@ 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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ export interface CountryDashboardMetric {
|
||||
d30RetentionRate: number | string;
|
||||
d30RetentionUser: number;
|
||||
dailyActiveUser: number;
|
||||
dailyRechargeUser: number;
|
||||
dealerRecharge: number | string;
|
||||
googleRecharge: number | string;
|
||||
gamePayoutRate: number | string;
|
||||
@ -32,6 +33,7 @@ export interface CountryDashboardMetric {
|
||||
luckyGiftUser: number;
|
||||
luckyGiftUserRate: number | string;
|
||||
mifapayRecharge: number | string;
|
||||
newDealerUserRecharge: number | string;
|
||||
newUserRecharge: number | string;
|
||||
officialRecharge: number | string;
|
||||
periodKey: string;
|
||||
@ -39,9 +41,11 @@ 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;
|
||||
@ -51,6 +55,12 @@ 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;
|
||||
|
||||
@ -212,6 +212,17 @@ 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',
|
||||
@ -230,6 +241,17 @@ 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',
|
||||
|
||||
@ -78,6 +78,13 @@ 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',
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import JsonBigint from 'json-bigint';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export interface LegacyPageResult<T = Record<string, any>> {
|
||||
@ -5,6 +7,16 @@ export interface LegacyPageResult<T = Record<string, any>> {
|
||||
total: number;
|
||||
}
|
||||
|
||||
const jsonBigint = JsonBigint({ storeAsString: true });
|
||||
|
||||
function parseLegacyBodyWithBigint<T>(raw: string) {
|
||||
const data = jsonBigint.parse(raw);
|
||||
if (data?.errorCode === 401 || data?.errorCode !== 0) {
|
||||
throw new Error(data?.errorMsg || data?.message || 'Error');
|
||||
}
|
||||
return data?.body as T;
|
||||
}
|
||||
|
||||
export async function pagePropsSource(params: Record<string, any>) {
|
||||
return requestClient.get<LegacyPageResult<Record<string, any>>>(
|
||||
'/props/source/record/page',
|
||||
@ -53,12 +65,18 @@ export async function listNotFamilyBySysOriginType(
|
||||
}
|
||||
|
||||
export async function pagePropsActivityRewardGroup(params: Record<string, any>) {
|
||||
return requestClient.get<LegacyPageResult<Record<string, any>>>(
|
||||
const response = await requestClient.get<{ data: string }>(
|
||||
'/props/activity/reward/group/page',
|
||||
{
|
||||
params,
|
||||
responseReturn: 'raw',
|
||||
responseType: 'text',
|
||||
transformResponse: [(data) => data],
|
||||
},
|
||||
);
|
||||
return parseLegacyBodyWithBigint<LegacyPageResult<Record<string, any>>>(
|
||||
response.data,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getByGroupId(id: number | string) {
|
||||
|
||||
@ -43,12 +43,23 @@ 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,
|
||||
|
||||
@ -216,11 +216,14 @@ export async function pageFreightRunningWater(params: Record<string, any>) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function exportFreightWaters(params: Record<string, any>) {
|
||||
export async function exportFreightWaters(
|
||||
params: Record<string, any>,
|
||||
filename = 'ExportFreightWaters',
|
||||
) {
|
||||
return downloadLegacyExcel(
|
||||
'/freight/running-water/export',
|
||||
params,
|
||||
'ExportFreightWaters',
|
||||
filename,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,12 @@ 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: '热游配置' },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -21,9 +21,12 @@ import {
|
||||
} from 'antdv-next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { submitGmGoldOperation } from '#/api/legacy/gm';
|
||||
import { getClsGoldRunningWater } from '#/api/legacy/operate';
|
||||
import { listMembers } from '#/api/legacy/team';
|
||||
import { pageGmGoldOperation, submitGmGoldOperation } from '#/api/legacy/gm';
|
||||
import {
|
||||
exportClsGoldRunningWater,
|
||||
getClsGoldRunningWater,
|
||||
} from '#/api/legacy/operate';
|
||||
import { listBackUserOptions } from '#/api/legacy/system';
|
||||
import { getUserBaseInfoBySysOriginAccount } from '#/api/legacy/user';
|
||||
import AccountInput from '#/components/account-input.vue';
|
||||
import UserProfileLink from '#/views/operate/components/user-profile-link.vue';
|
||||
@ -56,6 +59,8 @@ 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);
|
||||
@ -64,7 +69,7 @@ const rangeDate = ref<[string, string] | null>(null);
|
||||
const operators = ref<Array<Record<string, any>>>([]);
|
||||
|
||||
const query = reactive<Record<string, any>>({
|
||||
backOperationUser: undefined,
|
||||
backOperationUsers: [],
|
||||
context: '',
|
||||
endTime: '',
|
||||
lastId: '',
|
||||
@ -103,10 +108,14 @@ const showUsdQuantity = computed(
|
||||
);
|
||||
|
||||
const operatorOptions = computed(() =>
|
||||
operators.value.map((item) => ({
|
||||
label: `${item.nickname || item.loginName || item.id} / ${item.loginName || item.id}`,
|
||||
value: item.id,
|
||||
})),
|
||||
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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const currentPageList = computed(() => {
|
||||
@ -259,22 +268,81 @@ function filterOperatorOption(input: string, option: any) {
|
||||
}
|
||||
|
||||
async function loadOperators() {
|
||||
operators.value = (await listMembers()) || [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData(reset = false) {
|
||||
if (loading.value || loadMoreLoading.value) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadData(reset = false) {
|
||||
if (loading.value || loadMoreLoading.value) {
|
||||
return;
|
||||
}
|
||||
if (!validateQueryTimeRange()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -294,15 +362,10 @@ async function loadData(reset = false) {
|
||||
|
||||
try {
|
||||
const result = await getClsGoldRunningWater({
|
||||
backOperationUser: query.backOperationUser || undefined,
|
||||
...buildRunningWaterParams(),
|
||||
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];
|
||||
@ -352,6 +415,21 @@ 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;
|
||||
@ -420,15 +498,19 @@ void loadData(true);
|
||||
style="width: 240px"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="query.backOperationUser"
|
||||
v-model:value="query.backOperationUsers"
|
||||
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
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
<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,
|
||||
@ -113,6 +116,8 @@ 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) => ({
|
||||
@ -156,6 +161,27 @@ 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 }) => {
|
||||
@ -174,6 +200,8 @@ 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(),
|
||||
@ -204,6 +232,14 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(startTimePicker, (value) => {
|
||||
form.startTime = pickerToTimestamp(value);
|
||||
});
|
||||
|
||||
watch(expiredTimePicker, (value) => {
|
||||
form.expiredTime = pickerToTimestamp(value);
|
||||
});
|
||||
|
||||
async function loadCountries() {
|
||||
countriesLoading.value = true;
|
||||
try {
|
||||
@ -287,10 +323,18 @@ 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;
|
||||
@ -486,20 +530,22 @@ async function submitForm() {
|
||||
<div class="field">
|
||||
<div class="label">开始时间</div>
|
||||
<DatePicker
|
||||
v-model:value="form.startTime"
|
||||
v-model:value="startTimePicker"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
:get-popup-container="getDatePickerPopupContainer"
|
||||
show-time
|
||||
style="width: 100%"
|
||||
value-format="x"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="label">过期时间</div>
|
||||
<DatePicker
|
||||
v-model:value="form.expiredTime"
|
||||
v-model:value="expiredTimePicker"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
:get-popup-container="getDatePickerPopupContainer"
|
||||
show-time
|
||||
style="width: 100%"
|
||||
value-format="x"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
addOrUpdatePayCommodity,
|
||||
listCountrytSupportAmountChannels,
|
||||
} from '#/api/legacy/pay';
|
||||
|
||||
import {
|
||||
Collapse,
|
||||
CollapsePanel,
|
||||
@ -13,11 +8,16 @@ 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,6 +60,7 @@ const form = reactive<Record<string, any>>({
|
||||
content: '',
|
||||
id: '',
|
||||
payCountryId: '',
|
||||
payCountryIds: [],
|
||||
regionId: '',
|
||||
shelf: true,
|
||||
type: 'GOLD',
|
||||
@ -68,7 +69,27 @@ const form = reactive<Record<string, any>>({
|
||||
const isUpdate = computed(() => Boolean(form.id));
|
||||
const modalTitle = computed(() => (isUpdate.value ? '修改商品' : '创建商品'));
|
||||
const selectedApp = computed(() => props.row?.selectApp || {});
|
||||
const selectedCountry = computed(() => props.row?.country || {});
|
||||
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 regionName = computed(() => props.row?.regionName || '');
|
||||
|
||||
watch(
|
||||
@ -84,6 +105,14 @@ 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';
|
||||
@ -92,12 +121,26 @@ 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() {
|
||||
if (props.mode !== 'country' || !form.payCountryId || !isPositiveNumberPointTwo(String(form.amountUsd || ''))) {
|
||||
const currentPayCountryId = form.payCountryId || form.payCountryIds?.[0];
|
||||
if (props.mode !== 'country' || !currentPayCountryId || !isPositiveNumberPointTwo(String(form.amountUsd || ''))) {
|
||||
supportAmounts.value = [];
|
||||
computedAmount.value = '';
|
||||
amountUsdText.value = '';
|
||||
@ -105,8 +148,9 @@ async function loadSupportAmountChannels() {
|
||||
}
|
||||
associateLoading.value = true;
|
||||
try {
|
||||
// 新增允许多国家,渠道限额预览只展示首个国家;保存时后端会按每个国家拆分商品记录.
|
||||
const result = await listCountrytSupportAmountChannels(
|
||||
form.payCountryId,
|
||||
currentPayCountryId,
|
||||
form.amountUsd,
|
||||
);
|
||||
supportAmounts.value = result?.channels || [];
|
||||
@ -124,11 +168,20 @@ function handleAmountChange() {
|
||||
void loadSupportAmountChannels();
|
||||
}
|
||||
|
||||
function handleCountryIdsChange(value: Array<number | string>) {
|
||||
form.payCountryId = value?.[0] || '';
|
||||
void loadSupportAmountChannels();
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
if (props.mode === 'country' && !form.payCountryId) {
|
||||
if (props.mode === 'country' && isUpdate.value && !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;
|
||||
@ -162,13 +215,16 @@ 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,
|
||||
payCountryId: form.payCountryId || form.payCountryIds?.[0],
|
||||
payCountryIds: createCountryIds,
|
||||
regionId: form.regionId,
|
||||
shelf: form.shelf,
|
||||
type: form.type,
|
||||
@ -199,7 +255,16 @@ async function handleSubmit() {
|
||||
|
||||
<Form layout="vertical">
|
||||
<FormItem v-if="mode === 'country'" label="售卖国家">
|
||||
<div class="inline-meta">
|
||||
<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">
|
||||
<Image
|
||||
v-if="selectedCountry.icon"
|
||||
:preview="false"
|
||||
@ -252,7 +317,7 @@ async function handleSubmit() {
|
||||
<div v-else-if="supportAmounts.length === 0" class="support-empty">
|
||||
暂无可展示渠道
|
||||
</div>
|
||||
<Collapse v-else v-model:activeKey="activeKeys">
|
||||
<Collapse v-else v-model:active-key="activeKeys">
|
||||
<CollapsePanel
|
||||
v-for="item in supportAmounts"
|
||||
:key="String(item.channel?.channelCode || '')"
|
||||
|
||||
@ -1,14 +1,6 @@
|
||||
<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,
|
||||
@ -19,8 +11,15 @@ import {
|
||||
Table,
|
||||
} from 'antdv-next';
|
||||
|
||||
import { WEB_PRODUCT_TYPE_OPTIONS } from '../pay-shared';
|
||||
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(
|
||||
@ -185,7 +184,7 @@ function handlePageChange(page: number, pageSize: number) {
|
||||
void loadData();
|
||||
}
|
||||
|
||||
function handleApplicationChange(value: string | number) {
|
||||
function handleApplicationChange(value: number | string) {
|
||||
selectedApp.value =
|
||||
props.appInfo?.appList?.find((item: Record<string, any>) => item.id === value) ||
|
||||
{};
|
||||
@ -193,7 +192,7 @@ function handleApplicationChange(value: string | number) {
|
||||
void loadSupportData().then(() => loadData(true));
|
||||
}
|
||||
|
||||
function handleDimensionChange(value: string | number) {
|
||||
function handleDimensionChange(value: number | string) {
|
||||
selectedDimension.value =
|
||||
supportList.value.find((item) => item.id === value) || {};
|
||||
if (props.mode === 'country') {
|
||||
@ -218,10 +217,12 @@ 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'
|
||||
? {
|
||||
@ -243,8 +244,11 @@ 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: {
|
||||
@ -266,20 +270,23 @@ 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"
|
||||
@ -287,7 +294,8 @@ 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"
|
||||
@ -341,7 +349,7 @@ function openEdit(record: Record<string, any>) {
|
||||
:total="total"
|
||||
show-size-changer
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handlePageChange"
|
||||
@show-size-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
468
apps/src/views/operate/hotgame-config.vue
Normal file
468
apps/src/views/operate/hotgame-config.vue
Normal file
@ -0,0 +1,468 @@
|
||||
<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>
|
||||
@ -6,6 +6,8 @@ import {
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
@ -21,6 +23,7 @@ import { getAllowedSysOrigins } from '#/views/system/shared';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
DatePicker,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
@ -59,6 +62,7 @@ const salarySortKey = ref<'' | SalarySortKey>('');
|
||||
const salarySortOrder = ref<SalarySortOrder>('');
|
||||
|
||||
const query = reactive({
|
||||
billBelong: dayjs().format('YYYYMM'),
|
||||
countryCode: '',
|
||||
cursor: 1,
|
||||
limit: 20,
|
||||
@ -75,13 +79,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: 120 },
|
||||
{ dataIndex: 'expectedAgentSalary', key: 'expectedAgentSalary', title: '上级代理应发', width: 130 },
|
||||
{ dataIndex: 'expectedSalary', key: 'expectedSalary', title: '应发合计', width: 120 },
|
||||
{ dataIndex: 'payableMemberSalary', key: 'payableMemberSalary', title: '主播差额', width: 120 },
|
||||
{ dataIndex: 'payableAgentSalary', key: 'payableAgentSalary', title: '代理差额', width: 120 },
|
||||
{ dataIndex: 'payableSalary', key: 'payableSalary', title: '合计差额', width: 140 },
|
||||
{ dataIndex: 'payableAgentSalary', key: 'payableAgentSalary', title: '上级代理差额', width: 130 },
|
||||
{ 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 },
|
||||
@ -168,7 +172,7 @@ watch(
|
||||
);
|
||||
|
||||
watch(
|
||||
[() => query.sysOrigin, () => query.countryCode, () => query.userId],
|
||||
[() => query.sysOrigin, () => query.countryCode, () => query.userId, () => query.billBelong],
|
||||
() => {
|
||||
selectedUserIds.value = [];
|
||||
},
|
||||
@ -226,19 +230,8 @@ 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 shouldShowAgentSalary(record) ? formatMoney(record[key]) : '-';
|
||||
return formatMoney(record[key]);
|
||||
}
|
||||
|
||||
function formatInteger(value?: number | string) {
|
||||
@ -256,6 +249,13 @@ 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,6 +327,7 @@ 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 } : {}),
|
||||
@ -354,8 +355,8 @@ function confirmPay() {
|
||||
? '本次只发放已勾选的用户。'
|
||||
: '未勾选用户,本次将发放当前筛选国家的所有可发工资用户。',
|
||||
title: hasSelectedUsers
|
||||
? `确认发放已选择的 ${userIds.length} 人当前账期工资?`
|
||||
: `确认发放 ${countryLabel(query.countryCode)} 当前账期工资?`,
|
||||
? `确认发放已选择的 ${userIds.length} 人 ${billBelongLabel()} 工资?`
|
||||
: `确认发放 ${countryLabel(query.countryCode)} ${billBelongLabel()} 工资?`,
|
||||
async onOk() {
|
||||
paying.value = true;
|
||||
try {
|
||||
@ -383,7 +384,7 @@ function settleUser(record: Record<string, any>) {
|
||||
content: payableSalary < 0
|
||||
? '该用户应发小于实收,本次发放0并置为已结算,不会对用户扣款。'
|
||||
: '本次只结算当前这一行用户的工资。',
|
||||
title: `确认结算用户 ${record.userProfile?.account || userId} 的当前账期工资?`,
|
||||
title: `确认结算用户 ${record.userProfile?.account || userId} 的 ${billBelongLabel()} 工资?`,
|
||||
async onOk() {
|
||||
settlingUserId.value = userId;
|
||||
try {
|
||||
@ -428,6 +429,16 @@ 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"
|
||||
@ -546,7 +557,7 @@ void loadCountries();
|
||||
<template v-else-if="column.key === 'agentIssuedSalary'">
|
||||
<span>{{ formatAgentMoney(record, 'agentIssuedSalary') }}</span>
|
||||
<span
|
||||
v-if="shouldShowAgentSalary(record) && Number(record.agentOverIssuedSalary || 0) > 0"
|
||||
v-if="Number(record.agentOverIssuedSalary || 0) > 0"
|
||||
class="over-issued-salary"
|
||||
>
|
||||
({{ formatMoney(record.agentOverIssuedSalary) }})
|
||||
|
||||
@ -1,33 +1,30 @@
|
||||
<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 {
|
||||
@ -50,6 +47,7 @@ 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>>({});
|
||||
@ -137,10 +135,15 @@ async function loadData(reset = false) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildQueryParams() {
|
||||
function buildQueryParams(includePaging = true) {
|
||||
query.sysOrigins = getEffectiveSysOrigin();
|
||||
const params = { ...query };
|
||||
if (!includePaging) {
|
||||
delete params.lastId;
|
||||
delete params.limit;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries({ ...query }).filter(([, value]) => {
|
||||
Object.entries(params).filter(([, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0;
|
||||
}
|
||||
@ -153,6 +156,16 @@ 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;
|
||||
@ -233,6 +246,9 @@ void loadData(true);
|
||||
<Button :loading="loading" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</Button>
|
||||
<Button :loading="exporting" @click="handleExport">
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
|
||||
@ -8,6 +8,7 @@ import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
pagePayOpenCounty,
|
||||
syncPayOpenCountryCurrencyRates,
|
||||
updatePayOpenCountryCurrent,
|
||||
updatePayOpenCountryExchangeRate,
|
||||
updatePayOpenCountrySort,
|
||||
@ -41,6 +42,9 @@ 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: '',
|
||||
@ -172,6 +176,34 @@ 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>
|
||||
|
||||
@ -188,6 +220,7 @@ void loadData(true);
|
||||
/>
|
||||
<Button type="primary" @click="countrySelectOpen = true">添加</Button>
|
||||
<Button type="primary" @click="handleSearch">刷新</Button>
|
||||
<Button :loading="syncLoading" @click="openSyncRates">同步全球汇率</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
@ -268,6 +301,19 @@ 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>
|
||||
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
<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';
|
||||
@ -11,10 +7,10 @@ import { useAccessStore } from '@vben/stores';
|
||||
import {
|
||||
exportFreightWaters,
|
||||
pageFreightRunningWater,
|
||||
} from '#/api/legacy/user';
|
||||
} from '#/api/legacy/user';
|
||||
import { listBackUserOptions } from '#/api/legacy/system';
|
||||
import AccountInput from '#/components/account-input.vue';
|
||||
import { formatDate,
|
||||
getAllowedSysOrigins } from '#/views/system/shared';
|
||||
import { formatDate, getAllowedSysOrigins } from '#/views/system/shared';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@ -23,7 +19,7 @@ import {
|
||||
Pagination,
|
||||
Select,
|
||||
Table,
|
||||
message
|
||||
message,
|
||||
} from 'antdv-next';
|
||||
|
||||
import UserProfileLink from './components/user-profile-link.vue';
|
||||
@ -50,11 +46,14 @@ 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,
|
||||
@ -76,6 +75,17 @@ 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) => {
|
||||
@ -103,6 +113,63 @@ 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;
|
||||
@ -112,7 +179,7 @@ async function loadData(reset = false) {
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await pageFreightRunningWater({ ...query });
|
||||
const result = await pageFreightRunningWater(buildFreightRunningWaterParams());
|
||||
list.value = result.records || [];
|
||||
total.value = result.total || 0;
|
||||
} finally {
|
||||
@ -137,19 +204,26 @@ async function handleExport() {
|
||||
}
|
||||
exporting.value = true;
|
||||
try {
|
||||
await exportFreightWaters({ ...query });
|
||||
await exportFreightWaters(
|
||||
buildFreightRunningWaterParams(false),
|
||||
`ExportFreightWaters_${query.startTime || 'all'}_${query.endTime || 'all'}`,
|
||||
);
|
||||
message.success('导出成功');
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void loadOperators();
|
||||
void loadData(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page v-if="!queryBackOperation" title="货运代理流水">
|
||||
<Card>
|
||||
<component
|
||||
:is="queryBackOperation ? 'div' : Page"
|
||||
:title="queryBackOperation ? undefined : '货运代理流水'"
|
||||
>
|
||||
<component :is="queryBackOperation ? 'div' : Card">
|
||||
<div class="toolbar">
|
||||
<SysOriginSelect
|
||||
v-model:value="query.sysOrigin"
|
||||
@ -175,9 +249,21 @@ 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
|
||||
@ -234,8 +320,8 @@ void loadData(true);
|
||||
@showSizeChange="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</component>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
|
||||
import { pagePropsActivityRewardGroup } from '#/api/legacy/props';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
@ -13,8 +11,10 @@ import {
|
||||
Table,
|
||||
} from 'antdv-next';
|
||||
|
||||
import RewardRow from './reward-row.vue';
|
||||
import { pagePropsActivityRewardGroup } from '#/api/legacy/props';
|
||||
|
||||
import { PROPS_SHELF_STATUS_OPTIONS } from '../shared';
|
||||
import RewardRow from './reward-row.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
@ -52,6 +52,11 @@ const shelfStatusOptions = PROPS_SHELF_STATUS_OPTIONS.map((item) => ({
|
||||
value: item.value as any,
|
||||
}));
|
||||
|
||||
function normalizeID(value: unknown) {
|
||||
const text = String(value ?? '').trim();
|
||||
return text && text !== '0' ? text : '';
|
||||
}
|
||||
|
||||
async function loadData(reset = false) {
|
||||
if (!props.open || !query.sysOrigin) {
|
||||
return;
|
||||
@ -62,7 +67,10 @@ async function loadData(reset = false) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await pagePropsActivityRewardGroup({ ...query });
|
||||
list.value = result.records || [];
|
||||
list.value = (result.records || []).map((item: Record<string, any>) => ({
|
||||
...item,
|
||||
id: normalizeID(item.id),
|
||||
}));
|
||||
total.value = result.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@ -156,7 +164,7 @@ watch(
|
||||
:total="total"
|
||||
show-size-changer
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handlePageChange"
|
||||
@show-size-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
@ -16,8 +16,8 @@ import {
|
||||
Spin,
|
||||
Switch,
|
||||
Table,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Tabs,
|
||||
Tag,
|
||||
} from 'antdv-next';
|
||||
|
||||
@ -42,6 +42,7 @@ function createRowKey(seed?: number | string) {
|
||||
function createLevel(level = 1, rechargeAmount = 10) {
|
||||
return {
|
||||
enabled: true,
|
||||
googleProductId: '',
|
||||
id: null,
|
||||
level,
|
||||
rechargeAmount,
|
||||
@ -53,6 +54,15 @@ function createLevel(level = 1, rechargeAmount = 10) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeID(value: unknown) {
|
||||
const text = String(value ?? '').trim();
|
||||
return text && text !== '0' ? text : '';
|
||||
}
|
||||
|
||||
function hasPositiveID(value: unknown) {
|
||||
return /^\d+$/.test(normalizeID(value));
|
||||
}
|
||||
|
||||
function defaultLevels() {
|
||||
return [createLevel(1, 10), createLevel(2, 50), createLevel(3, 100)];
|
||||
}
|
||||
@ -76,6 +86,7 @@ const form = reactive<Record<string, any>>({
|
||||
enabled: false,
|
||||
id: 0,
|
||||
levelConfigs: defaultLevels(),
|
||||
minAppVersion: '',
|
||||
sysOrigin: '',
|
||||
updateTime: '',
|
||||
});
|
||||
@ -84,6 +95,7 @@ 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 },
|
||||
];
|
||||
@ -154,6 +166,7 @@ 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
|
||||
@ -166,11 +179,12 @@ 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,
|
||||
rechargeAmountCents: Number(item.rechargeAmountCents || 0),
|
||||
rewardGroupId: item.rewardGroupId ?? null,
|
||||
rewardGroupId: normalizeID(item.rewardGroupId) || null,
|
||||
rewardGroupName: String(item.rewardGroupName || ''),
|
||||
rewardItems: Array.isArray(item.rewardItems) ? item.rewardItems : [],
|
||||
rowKey: createRowKey(item.id || index),
|
||||
@ -254,8 +268,8 @@ function handleRewardGroupSelect(row: Record<string, any>) {
|
||||
groupPickerOpen.value = false;
|
||||
return;
|
||||
}
|
||||
target.rewardGroupId = row.id;
|
||||
target.rewardGroupName = row.name;
|
||||
target.rewardGroupId = normalizeID(row.id) || null;
|
||||
target.rewardGroupName = String(row.name || '');
|
||||
target.rewardItems = Array.isArray(row.rewardConfigList) ? row.rewardConfigList : [];
|
||||
groupPickerOpen.value = false;
|
||||
}
|
||||
@ -268,10 +282,10 @@ 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);
|
||||
const rewardGroupId = Number(item.rewardGroupId || 0);
|
||||
if (level <= 0) {
|
||||
message.warning('档位必须大于 0');
|
||||
return false;
|
||||
@ -280,7 +294,7 @@ function validateBeforeSave() {
|
||||
message.warning('首冲金额必须大于 0');
|
||||
return false;
|
||||
}
|
||||
if (rewardGroupId <= 0) {
|
||||
if (!hasPositiveID(item.rewardGroupId)) {
|
||||
message.warning('每个档位都需要配置奖励资源组');
|
||||
return false;
|
||||
}
|
||||
@ -292,6 +306,14 @@ 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));
|
||||
}
|
||||
@ -299,6 +321,10 @@ function validateBeforeSave() {
|
||||
message.warning('启用配置时至少要启用一个首冲奖励档位');
|
||||
return false;
|
||||
}
|
||||
if (form.enabled && !String(form.minAppVersion || '').trim()) {
|
||||
message.warning('启用配置时需要填写最低App版本');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -313,13 +339,15 @@ 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),
|
||||
rechargeAmountCents: toAmountCents(item.rechargeAmount),
|
||||
rewardGroupId: item.rewardGroupId ?? null,
|
||||
rewardGroupId: normalizeID(item.rewardGroupId) || null,
|
||||
rewardGroupName: String(item.rewardGroupName || ''),
|
||||
})),
|
||||
minAppVersion: String(form.minAppVersion || '').trim(),
|
||||
sysOrigin: selectedSysOrigin.value,
|
||||
});
|
||||
message.success('保存成功');
|
||||
@ -407,12 +435,19 @@ watch(activeTab, (value) => {
|
||||
<Button :loading="loading" @click="loadConfig">刷新</Button>
|
||||
</div>
|
||||
|
||||
<Tabs v-model:activeKey="activeTab">
|
||||
<Tabs v-model:active-key="activeTab">
|
||||
<TabPane key="config" tab="奖励配置">
|
||||
<div class="config-row">
|
||||
<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>
|
||||
@ -423,7 +458,7 @@ watch(activeTab, (value) => {
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="rowKey"
|
||||
:scroll="{ x: 900 }"
|
||||
:scroll="{ x: 1140 }"
|
||||
>
|
||||
<template #bodyCell="{ column, index, record }">
|
||||
<template v-if="column.key === 'enabled'">
|
||||
@ -445,6 +480,14 @@ 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>
|
||||
@ -518,7 +561,7 @@ watch(activeTab, (value) => {
|
||||
v-if="record.userAvatar"
|
||||
class="user-avatar"
|
||||
:style="avatarBackgroundStyle(record.userAvatar)"
|
||||
/>
|
||||
></div>
|
||||
<div class="user-main">
|
||||
<div class="user-name">{{ record.userNickname || '-' }}</div>
|
||||
<div class="sub-text">
|
||||
|
||||
@ -16,8 +16,8 @@ import {
|
||||
Spin,
|
||||
Switch,
|
||||
Table,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Tabs,
|
||||
Tag,
|
||||
} from 'antdv-next';
|
||||
|
||||
@ -54,6 +54,15 @@ function createLevel(level = 1, rechargeAmount = 100, rewardGold = 0) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeID(value: unknown) {
|
||||
const text = String(value ?? '').trim();
|
||||
return text && text !== '0' ? text : '';
|
||||
}
|
||||
|
||||
function hasPositiveID(value: unknown) {
|
||||
return /^\d+$/.test(normalizeID(value));
|
||||
}
|
||||
|
||||
function defaultLevels() {
|
||||
return [
|
||||
createLevel(1, 100, 0),
|
||||
@ -170,7 +179,7 @@ function normalizeConfig(result: null | Record<string, any> | undefined) {
|
||||
rechargeAmount: Number.isFinite(rechargeAmount) ? rechargeAmount : 0,
|
||||
rechargeAmountCents: Number(item.rechargeAmountCents || 0),
|
||||
rewardGold: Number(item.rewardGold || 0),
|
||||
rewardGroupId: item.rewardGroupId ?? null,
|
||||
rewardGroupId: normalizeID(item.rewardGroupId) || null,
|
||||
rewardGroupName: String(item.rewardGroupName || ''),
|
||||
rewardItems: Array.isArray(item.rewardItems) ? item.rewardItems : [],
|
||||
rowKey: createRowKey(item.id || index),
|
||||
@ -259,8 +268,8 @@ function handleRewardGroupSelect(row: Record<string, any>) {
|
||||
groupPickerOpen.value = false;
|
||||
return;
|
||||
}
|
||||
target.rewardGroupId = row.id;
|
||||
target.rewardGroupName = row.name;
|
||||
target.rewardGroupId = normalizeID(row.id) || null;
|
||||
target.rewardGroupName = String(row.name || '');
|
||||
target.rewardItems = Array.isArray(row.rewardConfigList) ? row.rewardConfigList : [];
|
||||
groupPickerOpen.value = false;
|
||||
}
|
||||
@ -277,7 +286,6 @@ function validateBeforeSave() {
|
||||
const level = Number(item.level || 0);
|
||||
const amountCents = toAmountCents(item.rechargeAmount);
|
||||
const rewardGold = Number(item.rewardGold || 0);
|
||||
const rewardGroupId = Number(item.rewardGroupId || 0);
|
||||
if (level <= 0) {
|
||||
message.warning('档位必须大于 0');
|
||||
return false;
|
||||
@ -286,7 +294,7 @@ function validateBeforeSave() {
|
||||
message.warning('充值金额必须大于 0');
|
||||
return false;
|
||||
}
|
||||
if (rewardGold <= 0 && rewardGroupId <= 0) {
|
||||
if (rewardGold <= 0 && !hasPositiveID(item.rewardGroupId)) {
|
||||
message.warning('每个档位至少要配置金币或奖励组');
|
||||
return false;
|
||||
}
|
||||
@ -324,7 +332,7 @@ async function submitForm() {
|
||||
rechargeAmount: formatAmount(item.rechargeAmount),
|
||||
rechargeAmountCents: toAmountCents(item.rechargeAmount),
|
||||
rewardGold: Number(item.rewardGold || 0),
|
||||
rewardGroupId: item.rewardGroupId ?? null,
|
||||
rewardGroupId: normalizeID(item.rewardGroupId) || null,
|
||||
rewardGroupName: String(item.rewardGroupName || ''),
|
||||
})),
|
||||
sysOrigin: selectedSysOrigin.value,
|
||||
@ -429,7 +437,7 @@ watch(activeTab, (value) => {
|
||||
<Button :loading="loading" @click="loadConfig">刷新</Button>
|
||||
</div>
|
||||
|
||||
<Tabs v-model:activeKey="activeTab">
|
||||
<Tabs v-model:active-key="activeTab">
|
||||
<TabPane key="config" tab="奖励配置">
|
||||
<div class="config-row">
|
||||
<Space align="center" wrap>
|
||||
@ -551,7 +559,7 @@ watch(activeTab, (value) => {
|
||||
v-if="record.userAvatar"
|
||||
class="user-avatar"
|
||||
:style="avatarBackgroundStyle(record.userAvatar)"
|
||||
/>
|
||||
></div>
|
||||
<div class="user-main">
|
||||
<div class="user-name">{{ record.userNickname || '-' }}</div>
|
||||
<div class="sub-text">
|
||||
|
||||
@ -196,7 +196,9 @@ const recordSummary = reactive<Record<string, any>>({
|
||||
endTime: '',
|
||||
participantCount: 0,
|
||||
startTime: '',
|
||||
totalDecorationRewardGold: 0,
|
||||
totalDrawTimes: 0,
|
||||
totalGiftRewardGold: 0,
|
||||
totalPaidGold: 0,
|
||||
totalRewardGold: 0,
|
||||
});
|
||||
@ -418,7 +420,9 @@ 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),
|
||||
});
|
||||
@ -1173,8 +1177,12 @@ watch(activePageTab, (value) => {
|
||||
<strong>{{ numberText(recordSummary.totalPaidGold) }}</strong>
|
||||
</div>
|
||||
<div class="record-summary-item">
|
||||
<span>礼物发放金币总价值</span>
|
||||
<strong>{{ numberText(recordSummary.totalRewardGold) }}</strong>
|
||||
<span>礼物价值</span>
|
||||
<strong>{{ numberText(recordSummary.totalGiftRewardGold) }}</strong>
|
||||
</div>
|
||||
<div class="record-summary-item">
|
||||
<span>装扮价值</span>
|
||||
<strong>{{ numberText(recordSummary.totalDecorationRewardGold) }}</strong>
|
||||
</div>
|
||||
<div class="record-summary-item">
|
||||
<span>用户总抽奖次数</span>
|
||||
@ -1318,7 +1326,7 @@ watch(activePageTab, (value) => {
|
||||
.record-summary {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr));
|
||||
grid-template-columns: repeat(5, minmax(160px, 1fr));
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
||||
@ -164,6 +164,7 @@ const metricColumns = new Set([
|
||||
'd7RetentionRate',
|
||||
'd30RetentionRate',
|
||||
'dailyActiveUser',
|
||||
'dailyRechargeUser',
|
||||
'dealerRecharge',
|
||||
'gamePayout',
|
||||
'gamePayoutRate',
|
||||
@ -181,15 +182,18 @@ const metricColumns = new Set([
|
||||
'luckyGiftUser',
|
||||
'luckyGiftUserRate',
|
||||
'mifapayRecharge',
|
||||
'newDealerUserRecharge',
|
||||
'newUserRecharge',
|
||||
'salaryExchange',
|
||||
'salaryTransfer',
|
||||
'totalRecharge',
|
||||
'userRecharge',
|
||||
]);
|
||||
|
||||
const integerMetricColumns = new Set([
|
||||
'countryNewUser',
|
||||
'dailyActiveUser',
|
||||
'dailyRechargeUser',
|
||||
'gameUser',
|
||||
'luckyGiftUser',
|
||||
]);
|
||||
@ -208,8 +212,11 @@ const percentMetricColumns = new Set([
|
||||
const summaryMetricKeys = [
|
||||
'countryNewUser',
|
||||
'dailyActiveUser',
|
||||
'dailyRechargeUser',
|
||||
'newUserRecharge',
|
||||
'dealerRecharge',
|
||||
'newDealerUserRecharge',
|
||||
'userRecharge',
|
||||
'mifapayRecharge',
|
||||
'googleRecharge',
|
||||
'salaryExchange',
|
||||
@ -235,8 +242,11 @@ const summaryMetricKeys = [
|
||||
const summaryMetricDefinitions = [
|
||||
{ key: 'countryNewUser', label: '新增' },
|
||||
{ key: 'dailyActiveUser', label: '当日活跃' },
|
||||
{ key: 'newUserRecharge', label: '新增充值' },
|
||||
{ key: 'dealerRecharge', label: '币商充值' },
|
||||
{ key: 'dailyRechargeUser', label: '新增当日充值用户数' },
|
||||
{ key: 'newUserRecharge', label: '新增用户充值' },
|
||||
{ key: 'dealerRecharge', label: '币商平台充值' },
|
||||
{ key: 'newDealerUserRecharge', label: '新增币商用户充值' },
|
||||
{ key: 'userRecharge', label: '用户充值' },
|
||||
{ key: 'mifapayRecharge', label: 'MifaPay 充值' },
|
||||
{ key: 'googleRecharge', label: 'Google 充值' },
|
||||
{ key: 'salaryExchange', label: '工资兑换' },
|
||||
@ -284,8 +294,11 @@ 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: 'newUserRecharge', key: 'newUserRecharge', title: '新增充值', width: 130 },
|
||||
{ align: 'right', dataIndex: 'dealerRecharge', key: 'dealerRecharge', title: '币商充值', width: 130 },
|
||||
{ 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: '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 },
|
||||
@ -315,7 +328,9 @@ 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: 140 },
|
||||
{ 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: '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 },
|
||||
@ -385,7 +400,7 @@ const rechargeDetailScrollX = rechargeDetailColumns.reduce(
|
||||
0,
|
||||
);
|
||||
const rechargeSourceNameMap: Record<string, string> = {
|
||||
DEALER: '币商充值',
|
||||
DEALER: '币商平台充值',
|
||||
MIFAPAY: 'MifaPay 充值',
|
||||
OFFICIAL: '官方充值',
|
||||
};
|
||||
@ -446,6 +461,7 @@ 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)) },
|
||||
@ -455,6 +471,10 @@ 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;
|
||||
@ -502,36 +522,18 @@ 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, dayArpuDashboard, threeDayArpuDashboard, weekArpuDashboard] =
|
||||
await Promise.all([
|
||||
countryDashboard(buildParams()),
|
||||
countryDashboard(buildRecentDayParams(1)),
|
||||
countryDashboard(buildRecentDayParams(3)),
|
||||
countryDashboard(buildRecentDayParams(7)),
|
||||
]);
|
||||
const mainDashboard = await countryDashboard(buildParams());
|
||||
dashboard.value = mainDashboard;
|
||||
arpuDashboards.value = {
|
||||
day: dayArpuDashboard,
|
||||
threeDay: threeDayArpuDashboard,
|
||||
week: weekArpuDashboard,
|
||||
day: mainDashboard.arpuSummary?.day || null,
|
||||
threeDay: mainDashboard.arpuSummary?.threeDay || null,
|
||||
week: mainDashboard.arpuSummary?.week || null,
|
||||
};
|
||||
if (activeTableTab.value === 'game') {
|
||||
await loadGameMetrics();
|
||||
@ -818,11 +820,24 @@ 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,
|
||||
@ -836,6 +851,7 @@ function emptySummary(): SummaryMetric {
|
||||
d30RetentionRate: 0,
|
||||
d30RetentionUser: 0,
|
||||
dailyActiveUser: 0,
|
||||
dailyRechargeUser: 0,
|
||||
dealerRecharge: 0,
|
||||
googleRecharge: 0,
|
||||
gamePayout: 0,
|
||||
@ -853,10 +869,12 @@ function emptySummary(): SummaryMetric {
|
||||
luckyGiftUser: 0,
|
||||
luckyGiftUserRate: 0,
|
||||
mifapayRecharge: 0,
|
||||
newDealerUserRecharge: 0,
|
||||
newUserRecharge: 0,
|
||||
salaryExchange: 0,
|
||||
salaryTransfer: 0,
|
||||
totalRecharge: 0,
|
||||
userRecharge: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1075,8 +1093,11 @@ async function renderVisualCharts() {
|
||||
}
|
||||
renderUserRechargeChart(baseChartOption('新增用户与充值', list, [
|
||||
amountSeries('新增用户', 'countryNewUser', list),
|
||||
amountSeries('新增充值', 'newUserRecharge', list),
|
||||
amountSeries('币商充值', 'dealerRecharge', list),
|
||||
amountSeries('当日充值用户数', 'dailyRechargeUser', list),
|
||||
amountSeries('新增用户充值', 'newUserRecharge', list),
|
||||
amountSeries('币商平台充值', 'dealerRecharge', list),
|
||||
amountSeries('新增币商用户充值', 'newDealerUserRecharge', list),
|
||||
amountSeries('用户充值', 'userRecharge', list),
|
||||
amountSeries('MifaPay 充值', 'mifapayRecharge', list),
|
||||
amountSeries('Google 充值', 'googleRecharge', list),
|
||||
amountSeries('工资兑换', 'salaryExchange', list),
|
||||
@ -1242,6 +1263,10 @@ 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>
|
||||
|
||||
@ -1583,7 +1608,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.metric-card-grid--recharge {
|
||||
grid-template-columns: minmax(180px, 260px);
|
||||
grid-template-columns: repeat(2, minmax(180px, 260px));
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
FormItem,
|
||||
Input,
|
||||
Modal,
|
||||
Switch,
|
||||
message,
|
||||
} from 'antdv-next';
|
||||
|
||||
@ -29,7 +30,19 @@ 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: '',
|
||||
@ -40,6 +53,46 @@ 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) => {
|
||||
@ -51,10 +104,23 @@ 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');
|
||||
@ -67,6 +133,11 @@ 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,
|
||||
@ -114,6 +185,123 @@ 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>
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
Pagination,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
message,
|
||||
} from 'antdv-next';
|
||||
|
||||
@ -55,12 +56,21 @@ 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);
|
||||
@ -75,6 +85,46 @@ 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;
|
||||
@ -186,6 +236,17 @@ 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>
|
||||
|
||||
@ -5,8 +5,7 @@ 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_GOLANG_TARGET = 'https://jvapi.haiyihy.com';
|
||||
const DEV_PROD_TARGET = 'https://yumi-admin.haiyihy.com';
|
||||
const DEV_PROD_TARGET = 'https://yumiadmin-acc.global-interaction.com';
|
||||
|
||||
function getBundledAssetFileName(assetInfo: {
|
||||
name?: string;
|
||||
@ -79,8 +78,11 @@ export default defineConfig(async () => {
|
||||
[`${DEV_API_PROD_PREFIX}/go`]: {
|
||||
changeOrigin: true,
|
||||
rewrite: (path) =>
|
||||
path.replace(createPrefixRegExp(DEV_API_PROD_PREFIX), ''),
|
||||
target: DEV_PROD_GOLANG_TARGET,
|
||||
path.replace(
|
||||
createPrefixRegExp(`${DEV_API_PROD_PREFIX}/go`),
|
||||
`${DEV_CONSOLE_PREFIX}/go`,
|
||||
),
|
||||
target: DEV_PROD_TARGET,
|
||||
ws: true,
|
||||
},
|
||||
[DEV_API_PROD_PREFIX]: {
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -480,6 +480,9 @@ importers:
|
||||
|
||||
apps:
|
||||
dependencies:
|
||||
'@types/json-bigint':
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.4
|
||||
'@vben/access':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/effects/access
|
||||
@ -534,6 +537,9 @@ importers:
|
||||
dayjs:
|
||||
specifier: 'catalog:'
|
||||
version: 1.11.20
|
||||
json-bigint:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.0
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))
|
||||
|
||||
@ -650,6 +650,71 @@ 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,
|
||||
@ -669,7 +734,8 @@ JOIN sys_menu AS target_menu
|
||||
'ResidentDailyTaskConfig',
|
||||
'OperateGameManager',
|
||||
'GameConfig',
|
||||
'OperateBaishunGameManage'
|
||||
'OperateBaishunGameManage',
|
||||
'OperateHotgameConfig'
|
||||
)
|
||||
LEFT JOIN sys_role_menu AS existing_role_menu
|
||||
ON existing_role_menu.role_id = parent_role.role_id
|
||||
@ -677,6 +743,21 @@ 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,
|
||||
@ -831,6 +912,10 @@ 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
|
||||
);
|
||||
|
||||
81
scripts/sql/sync_hotgame_menu.sql
Normal file
81
scripts/sql/sync_hotgame_menu.sql
Normal file
@ -0,0 +1,81 @@
|
||||
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;
|
||||
Loading…
x
Reference in New Issue
Block a user