[update]GameRtp页面接入后端

This commit is contained in:
2026-06-12 15:16:54 +08:00
parent 4a6f5501ba
commit afdab0792e
4 changed files with 315 additions and 60 deletions

View File

@@ -0,0 +1,94 @@
import createAxios from '/@/utils/axios'
export interface GameRtpParams {
provider: string
game_name: string
page: number
}
export function gameRtp(params: GameRtpParams) {
return createAxios({
url: '/admin/embed.Embed/gameRtp',
method: 'get',
params,
})
}
export interface AddGamePayload {
provider_site: string
game_name: string
image_url: string
rtp: number
status: 0 | 1
}
export function addGame(data: AddGamePayload) {
return createAxios(
{
url: '/admin/embed.Embed/addGame',
method: 'post',
data,
},
{
showSuccessMessage: true,
}
)
}
export interface EditGamePayload extends AddGamePayload {
id: number | string
}
export function editGame(data: EditGamePayload) {
return createAxios(
{
url: '/admin/embed.Embed/editGame',
method: 'post',
data,
},
{
showSuccessMessage: true,
}
)
}
export function del(data: { ids: (number | string)[] }) {
return createAxios(
{
url: '/admin/embed.Embed/del',
method: 'post',
data,
},
{
showSuccessMessage: true,
}
)
}
export interface SaveGameRtpPayload {
auto_game_rtp: 0 | 1
auto_rtp_amount: Record<string, string | number>
auto_frequency: number
provider_display: string
custom_config: string
header_image_url: string
text_color: string
button_text_color: string
base_color: string
button_bg_color: string
outline_color: string
progress_bar_bg_color: string
}
export function saveGameRtp(data: SaveGameRtpPayload) {
return createAxios(
{
url: '/admin/embed.Embed/saveGameRtp',
method: 'post',
data,
},
{
showSuccessMessage: true,
}
)
}

View File

@@ -45,6 +45,7 @@ export default {
nameRequired: 'Please enter the game name',
urlRequired: 'Please enter the game image URL',
saved: 'Settings saved',
invalidAutoAmount: 'Auto RTP Amount must be a valid JSON object',
copied: 'Embed code copied',
copyFailed: 'Unable to copy the embed code',
operationSuccess: 'Operation completed',

View File

@@ -45,6 +45,7 @@ export default {
nameRequired: '请输入游戏名称',
urlRequired: '请输入游戏图片 URL',
saved: '设置已保存',
invalidAutoAmount: '自动 RTP 范围必须是有效的 JSON 对象',
copied: '嵌入代码已复制',
copyFailed: '无法复制嵌入代码',
operationSuccess: '操作成功',

View File

@@ -52,7 +52,7 @@
<span>{{ settings[item.key] }}</span>
</div>
</div>
<el-button type="primary" class="save-button" @click="saveSettings">
<el-button type="primary" class="save-button" :loading="savingSettings" @click="saveSettings">
{{ t('embed.gameRtp.save') }}
</el-button>
</div>
@@ -87,8 +87,8 @@
</div>
</div>
<div class="table-wrap">
<el-table :data="pagedRows" border stripe>
<div v-loading="loading" class="table-wrap">
<el-table :data="rows" border stripe>
<el-table-column prop="id" :label="t('embed.gameRtp.gameId')" width="100" />
<el-table-column prop="provider" :label="t('embed.gameRtp.provider')" min-width="140" />
<el-table-column prop="name" :label="t('embed.gameRtp.gameName')" min-width="180" />
@@ -115,13 +115,13 @@
</div>
<div class="table-footer">
<span>{{ t('embed.gameRtp.totalRecords', { total: filteredRows.length }) }}</span>
<span>{{ t('embed.gameRtp.totalRecords', { total }) }}</span>
<el-pagination
v-model:current-page="currentPage"
background
layout="prev, pager, next"
:page-size="pageSize"
:total="filteredRows.length"
:page-count="lastPage"
@current-change="loadGameRtp"
/>
</div>
</section>
@@ -153,13 +153,13 @@
<el-form-item :label="t('embed.gameRtp.status')" prop="status">
<el-select v-model="form.status">
<el-option :label="t('embed.gameRtp.active')" :value="1" />
<el-option :label="t('embed.gameRtp.inactive')" :value="2" />
<el-option :label="t('embed.gameRtp.inactive')" :value="0" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">{{ t('embed.gameRtp.cancel') }}</el-button>
<el-button type="primary" @click="submitForm">{{ t('embed.gameRtp.confirm') }}</el-button>
<el-button type="primary" :loading="submitting" @click="submitForm">{{ t('embed.gameRtp.confirm') }}</el-button>
</template>
</el-dialog>
</div>
@@ -168,8 +168,9 @@
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, reactive, ref, watch } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { addGame, del, editGame, gameRtp, saveGameRtp } from '/@/api/backend/embed'
defineOptions({
name: 'embed/gameRtp',
@@ -180,10 +181,44 @@ interface GameRtpRow {
provider: string
name: string
imageUrl: string
rtp: number
rtp: number | null
status: number
}
interface GameRtpSourceRow {
id?: number | string
game_id?: number | string
provider?: string
game_provider?: string
provider_site?: string
type?: string
name?: string
game_name?: string
image_url?: string
game_image_url?: string
url?: string
rtp?: number | string
game_rtp?: number | string
status?: number | string
game_status?: number | string
}
interface GameRtpSetting {
auto_game_rtp?: number | string
auto_rtp_amount?: string | Record<string, string | number> | null
auto_frequency?: number | string
provider_display?: string | null
custom_config?: string | null
header_image_url?: string | null
text_color?: string | null
button_text_color?: string | null
base_color?: string | null
button_bg_color?: string | null
outline_color?: string | null
progress_bar_bg_color?: string | null
update_time?: number | string | null
}
type ColorSettingKey = 'textColor' | 'baseColor' | 'outlineColor' | 'buttonTextColor' | 'buttonBgColor' | 'progressBgColor'
const { t } = useI18n()
@@ -229,9 +264,9 @@ const colorFields: { key: ColorSettingKey; label: string }[] = [
{ key: 'progressBgColor', label: 'embed.gameRtp.progressBgColor' },
]
const lastRefresh = ref('2026-06-11 18:23:54')
const lastRefresh = ref('')
const embedCode = ref(`<div style="position: relative;max-width: 1000px;margin: 0 auto;overflow: hidden;border: none;">
<iframe id="gameRtpIframe" src="https://1xaud.em7bd7.co/embed/game-rtp/game-rtp.asp" style="width: 100%;overflow: hidden;border: none;"></iframe>
<iframe id="gameRtpIframe" src="http://192.168.0.46:8000/index.php/api/external/gameRtp" style="width: 100%;overflow: hidden;border: none;"></iframe>
<script>
(() => {
const merchantId = (typeof _ !== "undefined" && typeof _.getVar === "function") ? (_.getVar("merchantId") ?? "") : "";
@@ -264,61 +299,156 @@ const embedCode = ref(`<div style="position: relative;max-width: 1000px;margin:
}
}, 1000);
})();
<\/script>
/script>
</div>`)
const sourceRows: GameRtpRow[] = [
{ id: 15996, provider: 'SCRATCH', name: 'Spiritual Fortune', imageUrl: 'https://bonzagame.example/thumb_xl_spiritualfortune.png', rtp: 33, status: 1 },
{ id: 15995, provider: 'SCRATCH', name: 'Monster Mayhem', imageUrl: 'https://bonzagame.example/thumb_xl_monstermayhem.png', rtp: 70, status: 1 },
{ id: 15994, provider: 'SCRATCH', name: 'The Fiery Tales', imageUrl: 'https://bonzagame.example/thumb_xl_thefierytales.png', rtp: 76, status: 1 },
{ id: 15993, provider: 'SCRATCH', name: 'The Chronicles', imageUrl: 'https://bonzagame.example/thumb_xl_thechronicles.png', rtp: 52, status: 1 },
{ id: 15992, provider: 'SCRATCH', name: 'Spiritual King', imageUrl: 'https://bonzagame.example/thumb_xl_spiritualking.png', rtp: 31, status: 1 },
{ id: 15991, provider: 'SCRATCH', name: 'Enchanted Forest', imageUrl: 'https://bonzagame.example/thumb_xl_enchantedforest.png', rtp: 66, status: 1 },
{ id: 15990, provider: 'SCRATCH', name: 'Fortune Dino', imageUrl: 'https://bonzagame.example/thumb_xl_fortunedino.png', rtp: 69, status: 1 },
{ id: 15989, provider: 'SCRATCH', name: 'Gold Hunter', imageUrl: 'https://bonzagame.example/thumb_xl_goldhunter.png', rtp: 42, status: 1 },
{ id: 15988, provider: 'SCRATCH', name: 'Space Hunter', imageUrl: 'https://bonzagame.example/thumb_xl_spacehunter.png', rtp: 58, status: 1 },
{ id: 15987, provider: 'SCRATCH', name: 'Prosperity Play', imageUrl: 'https://bonzagame.example/thumb_xl_prosperityplay.png', rtp: 73, status: 1 },
{ id: 15986, provider: 'SCRATCH', name: 'Wonderful Alice', imageUrl: 'https://bonzagame.example/thumb_xl_wonderfulalice.png', rtp: 32, status: 1 },
{ id: 15985, provider: 'SCRATCH', name: 'Galactic Trooper', imageUrl: 'https://bonzagame.example/thumb_xl_galactictrooper.png', rtp: 59, status: 2 },
]
const rows = ref<GameRtpRow[]>(sourceRows)
const rows = ref<GameRtpRow[]>([])
const filters = reactive({ provider: '', name: '' })
const appliedFilters = reactive({ provider: '', name: '' })
const providers = computed(() => [...new Set(rows.value.map((row) => row.provider))].sort())
const filteredRows = computed(() =>
rows.value.filter(
(row) =>
(!appliedFilters.provider || row.provider === appliedFilters.provider) &&
(!appliedFilters.name || row.name.toLowerCase().includes(appliedFilters.name.toLowerCase()))
)
)
const providerOptions = ref<string[]>([])
const providers = computed(() => [...new Set([...providerOptions.value, ...rows.value.map((row) => row.provider)].filter(Boolean))].sort())
const currentPage = ref(1)
const pageSize = 10
const pagedRows = computed(() => filteredRows.value.slice((currentPage.value - 1) * pageSize, currentPage.value * pageSize))
const lastPage = ref(1)
const pageSize = ref(50)
const total = ref(0)
const loading = ref(false)
watch(filteredRows, () => {
const lastPage = Math.max(1, Math.ceil(filteredRows.value.length / pageSize))
if (currentPage.value > lastPage) currentPage.value = lastPage
})
const search = () => {
Object.assign(appliedFilters, filters)
currentPage.value = 1
const toNumber = (value: unknown) => {
const number = Number(value)
return Number.isFinite(number) ? number : 0
}
const clearSearch = () => {
const normalizeStatus = (value: unknown) => {
if (typeof value === 'string') {
const normalized = value.trim().toUpperCase()
if (normalized === 'ACTIVE') return 1
if (normalized === 'INACTIVE') return 0
}
return toNumber(value) === 1 ? 1 : 0
}
const formatUpdateTime = (value: unknown) => {
if (!value) return ''
if (typeof value === 'string' && !/^\d+$/.test(value)) return value
const timestamp = toNumber(value)
if (!timestamp) return ''
return new Date(timestamp * (timestamp < 1e12 ? 1000 : 1)).toLocaleString()
}
const formatJsonSetting = (value: string | Record<string, unknown> | null | undefined) => {
if (!value) return ''
return typeof value === 'string' ? value : JSON.stringify(value)
}
const applyRtpSetting = (setting?: GameRtpSetting) => {
if (!setting) return
settings.autoRtp = toNumber(setting.auto_game_rtp) === 1 ? 1 : 0
settings.autoAmount = formatJsonSetting(setting.auto_rtp_amount)
settings.frequency = String(setting.auto_frequency ?? '')
settings.providerDisplay = setting.provider_display ?? ''
settings.customConfig = setting.custom_config ?? ''
settings.headerImage = setting.header_image_url ?? ''
settings.textColor = setting.text_color ?? ''
settings.buttonTextColor = setting.button_text_color ?? ''
settings.baseColor = setting.base_color ?? ''
settings.buttonBgColor = setting.button_bg_color ?? ''
settings.outlineColor = setting.outline_color ?? ''
settings.progressBgColor = setting.progress_bar_bg_color ?? ''
lastRefresh.value = formatUpdateTime(setting.update_time)
}
const normalizeResponse = (responseData: any) => {
const source = responseData?.data ?? responseData ?? {}
const list = Array.isArray(source.list) ? source.list : []
const normalizedRows: GameRtpRow[] = list.map((row: GameRtpSourceRow) => ({
id: toNumber(row.id ?? row.game_id),
provider: String(row.provider_site ?? row.game_provider ?? row.provider ?? row.type ?? ''),
name: String(row.game_name ?? row.name ?? ''),
imageUrl: String(row.game_image_url ?? row.image_url ?? row.url ?? ''),
rtp: row.game_rtp == null && row.rtp == null ? null : toNumber(row.game_rtp ?? row.rtp),
status: normalizeStatus(row.game_status ?? row.status),
}))
if (currentPage.value === 1 && normalizedRows.length) pageSize.value = normalizedRows.length
const normalizedTotal = toNumber(source.total ?? normalizedRows.length)
return {
rows: normalizedRows,
total: normalizedTotal,
lastPage: Math.max(1, Math.ceil(normalizedTotal / pageSize.value)),
providers: Array.isArray(source.provider) ? source.provider.map(String) : [],
setting: source.rtp_setting as GameRtpSetting | undefined,
}
}
const loadGameRtp = async () => {
loading.value = true
try {
const response = await gameRtp({
provider: appliedFilters.provider,
game_name: appliedFilters.name,
page: currentPage.value,
})
const normalized = normalizeResponse(response.data)
rows.value = normalized.rows
total.value = normalized.total
lastPage.value = normalized.lastPage
providerOptions.value = normalized.providers
applyRtpSetting(normalized.setting)
} finally {
loading.value = false
}
}
const search = async () => {
Object.assign(appliedFilters, filters)
currentPage.value = 1
await loadGameRtp()
}
const clearSearch = async () => {
Object.assign(filters, { provider: '', name: '' })
search()
await search()
}
const loadTemplate = () => {
settings.customConfig = configTemplate
}
const saveSettings = () => {
lastRefresh.value = new Date().toLocaleString()
ElMessage.success(t('embed.gameRtp.saved'))
const savingSettings = ref(false)
const saveSettings = async () => {
let autoRtpAmount: Record<string, string | number>
try {
const parsed = JSON.parse(settings.autoAmount)
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') throw new Error()
autoRtpAmount = parsed
} catch {
ElMessage.error(t('embed.gameRtp.invalidAutoAmount'))
return
}
savingSettings.value = true
try {
await saveGameRtp({
auto_game_rtp: settings.autoRtp as 0 | 1,
auto_rtp_amount: autoRtpAmount,
auto_frequency: toNumber(settings.frequency),
provider_display: settings.providerDisplay,
custom_config: settings.customConfig,
header_image_url: settings.headerImage,
text_color: settings.textColor,
button_text_color: settings.buttonTextColor,
base_color: settings.baseColor,
button_bg_color: settings.buttonBgColor,
outline_color: settings.outlineColor,
progress_bar_bg_color: settings.progressBgColor,
})
await loadGameRtp()
} finally {
savingSettings.value = false
}
}
const copyEmbedCode = async () => {
@@ -333,6 +463,7 @@ const copyEmbedCode = async () => {
const dialogVisible = ref(false)
const dialogMode = ref<'create' | 'edit'>('create')
const editingId = ref<number>()
const submitting = ref(false)
const formRef = ref<FormInstance>()
const form = reactive({
provider: '',
@@ -358,14 +489,39 @@ const submitForm = async () => {
if (!(await formRef.value?.validate().catch(() => false))) return
if (dialogMode.value === 'create') {
const nextId = Math.max(0, ...rows.value.map((row) => row.id)) + 1
rows.value.unshift({ id: nextId, ...form })
submitting.value = true
try {
await addGame({
provider_site: form.provider,
game_name: form.name,
image_url: form.imageUrl,
rtp: form.rtp,
status: form.status as 0 | 1,
})
dialogVisible.value = false
currentPage.value = 1
await loadGameRtp()
} finally {
submitting.value = false
}
} else {
const index = rows.value.findIndex((row) => row.id === editingId.value)
if (index >= 0) rows.value[index] = { id: editingId.value!, ...form }
if (editingId.value == null) return
submitting.value = true
try {
await editGame({
id: editingId.value,
provider_site: form.provider,
game_name: form.name,
image_url: form.imageUrl,
rtp: form.rtp,
status: form.status as 0 | 1,
})
dialogVisible.value = false
await loadGameRtp()
} finally {
submitting.value = false
}
}
dialogVisible.value = false
ElMessage.success(t('embed.gameRtp.operationSuccess'))
}
const removeRow = async (row: GameRtpRow) => {
@@ -375,8 +531,9 @@ const removeRow = async (row: GameRtpRow) => {
confirmButtonText: t('embed.gameRtp.confirm'),
cancelButtonText: t('embed.gameRtp.cancel'),
})
rows.value = rows.value.filter((item) => item.id !== row.id)
ElMessage.success(t('embed.gameRtp.deleted'))
await del({ ids: [row.id] })
if (rows.value.length === 1 && currentPage.value > 1) currentPage.value -= 1
await loadGameRtp()
} catch {
return
}
@@ -395,6 +552,8 @@ const importGames = async () => {
return
}
}
onMounted(loadGameRtp)
</script>
<style scoped lang="scss">