ssvfdn 3 месяцев назад
Родитель
Сommit
6c9959904d

+ 28 - 0
apps/web-antd/src/api/game_control/auto_rtp.ts

@@ -0,0 +1,28 @@
+import {requestBodyClient, requestClient} from "#/api/request";
+
+interface ApiResultListData {
+    data: Object;
+    status: number;
+    total: number;
+    list: Array<any>;
+}
+
+
+/**
+ * 获取自动RTP配置
+ */
+export async function getAutoRtpInfo() {
+    return requestClient.get<ApiResultData>('/auto_rtp/info');
+}
+
+/**
+ * 更新状态
+ * @param data
+ */
+export async function updateAutoRtpStatus(data:any) {
+    return requestClient.post<ApiResultData>('/auto_rtp/status', data);
+}
+
+export async function updateAutoRtpInfo(data:any) {
+    return requestBodyClient.post<ApiResultData>('/auto_rtp/update', data);
+}

+ 2 - 1
apps/web-antd/src/locales/langs/zh-CN/common.json

@@ -13,5 +13,6 @@
     "serial": "序号",
     "search_submit_button": "查询",
     "action": "操作",
-    "suc_msg": "操作成功"
+    "suc_msg": "操作成功",
+    "delete": "删除"
 }

+ 25 - 1
apps/web-antd/src/locales/langs/zh-CN/game_control.json

@@ -5,6 +5,8 @@
     "play_control_title": "玩家点控",
     "play_control_list_title": "点控列表",
     "play_control_record_title": "点控记录",
+    "control_auto_rtp_title": "自动调控RTP",
+    "control_auto_rtp_config_title": "自动调控配置",
     "game_list": {
         "game_title": "游戏名称",
         "game_en_title": "游戏英文名称",
@@ -43,6 +45,28 @@
         "cancel_control": "取消点控",
         "confirm_cancel_control": "请确认取消【{title}】该点控?",
         "confirm_all_cancel_control": "确认取消全部点控?",
-        "end_time": "结束时间"
+        "end_time": "结束时间",
+        "desc":"(玩家在调控期间每局赢奖倍数限制,此倍数与总押注相乘为每局最高可赢取额度)"
+    },
+    "auto_rtp": {
+        "new_user_number": "定义新手玩家的游戏局数",
+        "float_rate": "玩家RTP高于游戏设定RTP进入系统判断",
+        "rtp_check": "游戏内统计数据周期",
+        "rtp_float": "玩家RTP上下限的误差范围",
+        "status": "功能状态",
+        "auto_rtp_desc": "功能自动控制详情",
+        "setting_config": "降低配置",
+        "open_auto_rtp_desc": "确认关闭该功能?",
+        "close_auto_rtp_desc": "确认开启该功能?",
+        "rtp_tip_desc_title": "\"玩家降低控制\"自动控制详情展示",
+        "rtp_list_desc": "每经过10局游戏进行检测,玩家RTP处于{start}%-{end}%间触发判断,新手玩家将有{new}%概率被控制,非新手玩家将有{old}%概率控制。控制档位使用“RTP{float_rate}”进行控制",
+        "rtp_list_min_desc": "每经过10局游戏进行检测,玩家RTP处于{start}%-{end}%间触发判断。",
+        "new_user_number_desc": "玩家生涯的游戏局数,在此局数范围内为新手玩家,反之为非新手玩家",
+        "float_rate_desc": "玩家在游戏内的RTP高于游戏设定RTP时,玩家进入系统判断是否控制",
+        "rtp_list_config": "RTP各区间配置详情",
+        "user_rtp_change_desc": "玩家RTP区间(根据商户设定的游戏RTP会有浮动变化)",
+        "new_user": "新手",
+        "old_user": "非新手",
+        "new_old_diff_user": "新手和非新手玩家的被控制概率"
     }
 }

+ 20 - 0
apps/web-antd/src/router/routes/modules/game_control.ts

@@ -52,6 +52,26 @@ const routes: RouteRecordRaw[] = [
                     },
                 ]
             },
+            {
+                meta: {
+                    title: $t('game_control.control_auto_rtp_title'),
+                    icon:'solar:station-outline',
+                    keepAlive: true
+                },
+                name: 'GameControlGameAutoRtpControl',
+                path: '/game-control/auto-rtp',
+                children:[
+                    {
+                        meta: {
+                            title: $t('game_control.control_auto_rtp_config_title'),
+                            keepAlive: true
+                        },
+                        name: 'GameControlGameAutoRtpControlConfig',
+                        path: '/game-control/auto-rtp/config',
+                        component: () => import('#/views/game_control/auto_rtp/config/index.vue'),
+                    },
+                ]
+            },
         ],
     },
 ];

+ 122 - 0
apps/web-antd/src/views/game_control/auto_rtp/config/index.vue

@@ -0,0 +1,122 @@
+<script setup>
+import { Page, useVbenModal,confirm } from '@vben/common-ui';
+
+import {Button, Card, Tag, Avatar, Switch, message} from "ant-design-vue";
+import {$t} from "@vben/locales";
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import {getAutoRtpInfo, updateAutoRtpStatus} from "#/api/game_control/auto_rtp.js";
+
+import { createIconifyIcon } from '@vben-core/icons';
+const EyeIcon = createIconifyIcon('solar:eye-bold');
+
+const gridOptions = {
+	border: true,
+	stripe: true,
+	checkboxConfig: {
+		highlight: true,
+	},
+	columns: [
+		{ field: 'new_user_number', title:  $t('game_control.auto_rtp.new_user_number'), width: 200},
+		{ field: 'float_rate', title: $t('game_control.auto_rtp.float_rate'), width: 300, slots: {default:"float_rate"} },
+		{ field: 'rtp_check', title: $t('game_control.auto_rtp.rtp_check'), width: 140},
+		{ field: 'rtp_float', title: $t('game_control.auto_rtp.rtp_float'), width: 200, slots: {default:"rtp_float"} },
+		{ field: 'status', title: $t('game_control.auto_rtp.status'), slots: {default:'status'}},
+		{ title: $t('game_control.auto_rtp.auto_rtp_desc'), width: 200, slots: {default:'auto_rtp_desc'}},
+		{ fixed: 'right', title: $t('common.action'),width:150, slots: {default:'action'}},
+	],
+	exportConfig: {},
+	// height: 'auto', // 如果设置为 auto,则必须确保存在父节点且不允许存在相邻元素,否则会出现高度闪动问题
+	keepSource: true,
+	proxyConfig: {
+		ajax: {
+			query: async ({ page }) => {
+				const list = await getAutoRtpInfo();
+				list.list.forEach((item) => {
+					item['status'] = item.status == 1;
+				})
+				return {
+					total: list.total,
+					items: list.list
+				}
+			},
+		},
+	},
+	rowConfig: {
+		isHover: true,
+	},
+	toolbarConfig: {
+		custom: true,
+		export: true,
+		// import: true,
+		refresh: true,
+		zoom: true,
+	},
+};
+
+const [Grid, gridApi] = useVbenVxeGrid({
+	gridOptions,
+});
+
+const changeStatus = async (row) => {
+	confirm(row.status ? $t('game_control.auto_rtp.open_auto_rtp_desc') : $t('game_control.auto_rtp.close_auto_rtp_desc')).then(async () => {
+		let res = await updateAutoRtpStatus({'id': row.id, 'status': row.status ? 0 : 1});
+		row.status = !row.status;
+		message.success($t('common.suc_msg'));
+	}).catch(() => {});
+}
+
+
+// 查看详情
+import ExtraModal from './info.vue';
+import {toRaw} from "vue";
+const [Modal, modalApi] = useVbenModal({
+	// 连接抽离的组件
+	connectedComponent: ExtraModal,
+	class:'w-[50%]',
+	footer: false,
+	title: $t('game_control.auto_rtp.rtp_tip_desc_title'),
+	onClosed:async function () {
+		let _data = await modalApi.getData();
+		if(_data.is_reload) {
+			await gridApi.reload();
+		}
+	}
+});
+const openInfo = (row, is_edit = false) => {
+	modalApi.setData({
+		data: toRaw(row),
+		is_edit: is_edit
+	}).open();
+}
+
+</script>
+
+<template>
+	<Page>
+		<Grid>
+			<template #status="{ row }">
+				<Switch :checked="row.status" @click="changeStatus(row)"></Switch>
+			</template>
+			<template #auto_rtp_desc=" {row} ">
+				<div style="display:flex; align-items: center; justify-content: center;">
+					<EyeIcon @click="openInfo(row)" style="font-size:24px;color:rgb(0, 107, 230);cursor:pointer;" />
+				</div>
+			</template>
+			<template #action="{ row }">
+				<Button type="link" @click="openInfo(row, true)"  danger>{{$t('game_control.auto_rtp.setting_config')}}</Button>
+			</template>
+
+			<template #float_rate="{ row }">
+				{{row.float_rate}}%
+			</template>
+			<template #rtp_float="{ row }">
+				{{row.rtp_float}}%
+			</template>
+		</Grid>
+		<Modal />
+	</Page>
+</template>
+
+<style scoped>
+
+</style>

+ 251 - 0
apps/web-antd/src/views/game_control/auto_rtp/config/info.vue

@@ -0,0 +1,251 @@
+<script setup>
+import {Descriptions, DescriptionsItem, Input, InputGroup, Select, Button, InputNumber, message} from 'ant-design-vue';
+import {ref, reactive, toRaw, h} from "vue";
+import {useVbenModal} from '@vben/common-ui';
+import {$t} from "@vben/locales";
+import { useVbenForm} from '#/adapter/form';
+import {useVbenVxeGrid} from "#/adapter/vxe-table.js";
+import {updateAutoRtpInfo} from "#/api/game_control/auto_rtp.js";
+
+
+const is_edit = ref(false);
+let form = reactive({
+	rtp_kill_data: [],
+	float_rate: 15,
+	default_rtp: 97,
+	rtp_float: 2.5,
+	rtp_check: 10,
+	new_user_number: 150
+});
+
+const updateTestDesc = function () {
+	let diff_rtp = form.rtp_float * 2;
+	form.rtp_kill_data.forEach((item, idx) => {
+		const start = form.default_rtp + (diff_rtp * idx) + form.float_rate;
+		let end = start + diff_rtp;
+		const float_rate = form.float_rate + (diff_rtp * idx);
+		if(idx == form.rtp_kill_data.length - 1) {
+			end = 9999999.99;
+		}
+		item['idx'] = idx;
+		item['desc'] = $t('game_control.auto_rtp.rtp_list_desc', {
+			...item,
+			start,
+			end,
+			float_rate
+		});
+		item['min_desc'] = $t('game_control.auto_rtp.rtp_list_min_desc', {
+			...item,
+			start,
+			end,
+			float_rate
+		});
+	})
+}
+
+const [Modal, modalApi] = useVbenModal({
+	draggable: true,
+	async onOpenChange(isOpen) {
+		if (isOpen) {
+			let tempData = await modalApi.getData();
+			tempData = JSON.parse(JSON.stringify(tempData));
+			form.rtp_kill_data = tempData.data.rtp_kill_data;
+			form.float_rate = tempData.data.float_rate;
+			form.new_user_number = tempData.data.new_user_number;
+			form.default_rtp = tempData.data.default_rtp;
+			form.rtp_float = tempData.data.rtp_float;
+
+			updateTestDesc();
+			gridApi.setGridOptions({
+				data: form.rtp_kill_data,
+			});
+			formApi.setValues({
+				new_user_number: form.new_user_number,
+				float_rate: form.float_rate,
+			})
+			// $start = $rtp + ($diff_rtp * $idx) + $info['float_rate'];
+			// $end = $start + $diff_rtp;
+
+			is_edit.value = tempData.is_edit || false;
+			if(is_edit.value){
+				modalApi.setState({
+					footer: true,
+					class:'w-[70%]',
+				})
+			}else {
+				modalApi.setState({
+					footer: false,
+					class:'w-[50%]',
+				})
+			}
+		}
+	},
+	onConfirm:async function () {
+		let values = await formApi.getValues();
+		let _form = {
+			'new_user_number': values.new_user_number,
+			'float_rate': values.float_rate,
+			'rtp_kill_data': []
+		}
+		form.rtp_kill_data.forEach((item, idx) => {
+			_form.rtp_kill_data.push({
+				'new': item.new,
+				'old': item.old,
+			})
+		})
+		modalApi.lock();
+		const res = await updateAutoRtpInfo(_form);
+		modalApi.unlock();
+		if(res.state) {
+			modalApi.setData({
+				'is_reload': true
+			});
+			await modalApi.close();
+		}else {
+			message.error(res.message);
+		}
+		return false;
+	}
+});
+
+
+
+const userNumberList = [];
+[50, 60, 70, 80, 90, 100, 150, 200, 300, 400, 500].forEach((value) => {
+	userNumberList.push({
+		'label': value + '局',
+		'value': value,
+	})
+})
+const floatRateList = [];
+for(let i=1;i<=15;i++) {
+	floatRateList.push({
+		'label': i + '%',
+		'value': i,
+	})
+}
+
+
+const [Form, formApi] = useVbenForm({
+	// 所有表单项共用,可单独在表单内覆盖
+	commonConfig: {
+		// 所有表单项
+		componentProps: {
+			class: 'w-full',
+		},
+		labelWidth: 220,
+	},
+	scrollToFirstError: true,
+	layout: 'horizontal',
+	schema: [
+		{
+			component: 'Select',
+			componentProps: {
+				filterOption: true,
+				options: userNumberList,
+				showSearch: true,
+			},
+			defaultValue: form.new_user_number,
+			fieldName: 'new_user_number',
+			label: $t('game_control.auto_rtp.new_user_number'),
+			rules: 'required',
+			suffix: () =>  h('span', { style: 'font-size:14px;color:#666;' }, $t('game_control.auto_rtp.new_user_number_desc')),
+		},
+		{
+			component: 'Select',
+			componentProps: {
+				filterOption: true,
+				options: floatRateList,
+				showSearch: true,
+			},
+			defaultValue: form.float_rate,
+			fieldName: 'float_rate',
+			label: $t('game_control.auto_rtp.float_rate'),
+			rules: 'required',
+			suffix: () =>  h('span', { style: 'font-size:14px;color:#666;' }, $t('game_control.auto_rtp.float_rate_desc')),
+		},
+		{
+			component: 'Input',
+			fieldName: 'rtp_kill_data',
+			label: $t('game_control.auto_rtp.rtp_list_config'),
+			rules: 'required',
+		}
+	],
+	wrapperClass: 'grid-cols-1',
+	showDefaultActions: false,
+});
+
+const gridOptions = {
+	height: 400,
+	rowConfig: {
+		isHover: true,
+	},
+	columns: [
+		{ title: $t('common.serial'), type: 'seq', width: 50 },
+		{ field: 'min_desc', title: $t('game_control.auto_rtp.user_rtp_change_desc'), width: 480},
+		{
+			title: $t('game_control.auto_rtp.new_old_diff_user'),
+			headerAlign: 'center',
+			children: [
+				{ field: 'new', title: $t('game_control.auto_rtp.new_user'), slots: {'default':"new_input"}, minWidth: 120 },
+				{ field: 'old', title: $t('game_control.auto_rtp.old_user'), slots: {'default':"old_input"}, minWidth: 120 },
+			]
+		},
+		{
+			title:$t('common.action'),
+			slots:{'default':"action"},
+			minWidth: 100,
+		}
+	],
+	data: form.rtp_kill_data,
+	pagerConfig: {
+		enabled: false
+	},
+};
+
+const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
+
+
+const deleteKill = function (row) {
+	form.rtp_kill_data.splice(row.idx, 1);
+	updateTestDesc();
+}
+const addKill = function () {
+	form.rtp_kill_data.push({new: 0, old: 0});
+	updateTestDesc();
+}
+
+</script>
+
+<template>
+	<Modal>
+		<Descriptions v-if="!is_edit" bordered :labelStyle="{'width':'60px'}" :column="1">
+			<DescriptionsItem v-for="(item, index) in form.rtp_kill_data" :label=" index + 1">
+				{{item.desc}}
+			</DescriptionsItem>
+		</Descriptions>
+		<Form v-else class="grid-form-class-diy">
+			<template #rtp_kill_data="slotProps">
+				<Grid class="grid-class">
+					<template #new_input="{row}">
+						<InputNumber v-model:value="form.rtp_kill_data[row.idx]['new']" addon-after="%"></InputNumber>
+					</template>after
+					<template #old_input="{row}">
+						<InputNumber v-model:value="form.rtp_kill_data[row.idx]['old']" addon-after="%"></InputNumber>
+					</template>
+					<template #toolbar-actions>
+						<Button type="primary"  v-if="form.rtp_kill_data.length < 20" @click="addKill">添加</Button>
+					</template>
+					<template #action="{row}">
+						<Button v-if="form.rtp_kill_data.length > 1" type="primary" @click="deleteKill(row)" size="small" danger>{{$t('common.delete')}}</Button>
+					</template>
+				</Grid>
+			</template>
+		</Form>
+	</Modal>
+</template>
+
+<style scoped>
+.grid-class {width: 100%}
+.grid-form-class-diy ::v-deep .ant-select {width: 25%}
+</style>

+ 1 - 1
apps/web-antd/src/views/game_control/player_control/list/create_player.vue

@@ -141,7 +141,7 @@ const [Form, formApi] = useVbenForm({
 			},
 			fieldName: 'max_win_multi',
 			label: $t('game_control.player_control.max_win_multi'),
-			suffix: () =>  h('span', { style: 'font-size:14px;color:#666;' }, '(玩家在调控期间每局赢奖倍数限制,此倍数与总押注相乘为每局最高可赢取额度)'),
+			suffix: () =>  h('span', { style: 'font-size:14px;color:#666;' }, $t('game_control.player_control.desc')),
 		},
 		{
 			component: 'InputNumber',

+ 2 - 1
apps/web-antd/vite.config.mts

@@ -9,7 +9,8 @@ export default defineConfig(async () => {
           '/api': {
             changeOrigin: true,
             rewrite: (path) => path.replace(/^\/api/, ''),
-            target: 'https://merchant.w115.net',
+              target: 'https://merchant.w115.net',
+              // target: 'http://merchant.uwigb.mynatapp.cc',
             // ws: true,
           },
         },