[add]新增页面

embed/programWidget
embed/domainStatus
embed/partnership
embed/gameRtp
This commit is contained in:
2026-06-11 17:42:58 +08:00
parent 9b10f21663
commit 4a6f5501ba
17 changed files with 2315 additions and 4 deletions

View File

@@ -0,0 +1,32 @@
export default {
settingTitle: 'Domain Status Setting',
embedCode: 'Embed Code',
headerImage: 'Header Image URL',
customConfig: 'Custom Config',
loadTemplate: 'Load Template',
baseColor: 'Base Color',
borderColor: 'Border Color',
onlineColor: 'Online Color',
offlineColor: 'Offline Color',
titleColor: 'Title Color',
textColor: 'Text Color',
labelColor: 'Label Color',
save: 'SAVE',
copyCode: 'Copy Embed Code',
saved: 'Domain status settings saved',
invalidConfig: 'Custom Config contains invalid JSON',
copied: 'Embed code copied',
copyFailed: 'Unable to copy the embed code',
domainList: 'Domain List',
online: 'ONLINE',
offline: 'OFFLINE',
empty: 'No domains',
editDomainList: 'Click here to edit Domain List',
editTitle: 'Edit Domain List',
addDomain: 'Add Domain',
delete: 'Delete',
cancel: 'Cancel',
confirm: 'Confirm',
duplicateDomain: 'Domain names must be unique',
domainsSaved: 'Domain list saved',
}

View File

@@ -0,0 +1,56 @@
export default {
settingTitle: 'Game RTP Setting',
embedCode: 'Embed Code',
autoRtp: 'Auto Game RTP (%)',
autoAmount: 'Auto RTP Amount',
frequency: 'Auto Frequency (min)',
lastRefresh: 'Last Refresh',
providerDisplay: 'Provider Display',
providerHint: 'Leave it blank to display all providers',
customConfig: 'Custom Config',
loadTemplate: 'Load Template',
headerImage: 'Header Image URL',
textColor: 'Text Color',
baseColor: 'Base Color',
outlineColor: 'Outline Color',
buttonTextColor: 'Button Text Color',
buttonBgColor: 'Button BG Color',
progressBgColor: 'Progress Bar BG Color',
save: 'SAVE',
copyCode: 'Copy Embed Code',
dataTitle: 'Game RTP Data',
provider: 'Game Provider',
allProviders: '- All Game Providers -',
gameName: 'Game Name',
searchName: 'Search by Game Name',
search: 'Search',
clear: 'Clear',
gameId: 'Game ID',
imageUrl: 'Game Image URL',
rtp: 'RTP (%)',
status: 'Status',
action: 'Action',
active: 'ACTIVE',
inactive: 'INACTIVE',
edit: 'Edit',
delete: 'Delete',
totalRecords: 'Total {total} records',
create: 'CREATE NEW GAME RTP',
import: 'IMPORT & REFRESH (VIA JDK API)',
createTitle: 'Create New Game RTP',
editTitle: 'Edit Game RTP',
cancel: 'Cancel',
confirm: 'Confirm',
providerRequired: 'Please enter the game provider',
nameRequired: 'Please enter the game name',
urlRequired: 'Please enter the game image URL',
saved: 'Settings saved',
copied: 'Embed code copied',
copyFailed: 'Unable to copy the embed code',
operationSuccess: 'Operation completed',
deleteConfirm: 'Delete game "{name}"?',
warning: 'Warning',
deleted: 'Game deleted',
importConfirm: 'Import and refresh game data from the JDK API?',
imported: 'Game data refreshed',
}

View File

@@ -0,0 +1,52 @@
export default {
settingTitle: 'Partnership Setting',
embedCode: 'Embed Code',
headerImage: 'Header Image URL',
footerConfig: 'Footer Config',
customConfig: 'Custom Config',
loadTemplate: 'Load Template',
baseColor1: 'Base Color 1',
baseColor2: 'Base Color 2',
borderColor: 'Border Color',
highlightColor: 'Highlight Color',
titleColor: 'Title Color',
textColor: 'Text Color',
save: 'SAVE',
copyCode: 'Copy Embed Code',
dataTitle: 'Partner Data',
displayOrder: 'Display Order',
partnerName: 'Partner Name',
currency: 'Currency',
logoImage: 'Logo Image',
option: 'Option',
config: 'Config',
status: 'Status',
action: 'Action',
active: 'ACTIVE',
inactive: 'INACTIVE',
edit: 'Edit',
delete: 'Delete',
empty: 'No partner data',
create: 'CREATE NEW PARTNER',
export: 'EXPORT (PARTNER DATA)',
import: 'IMPORT & REPLACE (PARTNER DATA)',
createTitle: 'Create New Partner',
editTitle: 'Edit Partner',
cancel: 'Cancel',
confirm: 'Confirm',
nameRequired: 'Please enter the partner name',
currencyRequired: 'Please enter the currency',
saved: 'Partnership settings saved',
invalidConfig: 'Footer Config or Custom Config contains invalid JSON',
copied: 'Embed code copied',
copyFailed: 'Unable to copy the embed code',
invalidPartnerConfig: 'Option or Config contains invalid JSON',
operationSuccess: 'Operation completed',
warning: 'Warning',
deleteConfirm: 'Delete partner "{name}"?',
deleted: 'Partner deleted',
exported: 'Partner data exported',
importConfirm: 'Replace current partner data with {total} imported records?',
imported: 'Partner data imported',
importFailed: 'Unable to import this partner data file',
}

View File

@@ -0,0 +1,16 @@
export default {
title: 'Access All Program',
bannerCode: 'Program Banner Code',
bannerHelpPrefix: 'Put the code in',
bannerHelpSuffix: 'or anywhere you want to put it.',
preview: 'Preview',
copyBanner: 'Copy Banner Code',
functionCode: 'Program Function Code',
functionHelpPrefix: 'Put the code in',
copyFunction: 'Copy Function Code',
copied: 'Code copied',
copyFailed: 'Unable to copy the code',
previewTitle: 'Program Banner Preview',
previewNote: 'Preview clicks do not open the live program or submit account information.',
previewProgram: 'Program page {page} selected in preview',
}

View File

@@ -0,0 +1,32 @@
export default {
settingTitle: '域名状态设置',
embedCode: '嵌入代码',
headerImage: '顶部图片 URL',
customConfig: '自定义配置',
loadTemplate: '载入模板',
baseColor: '基础颜色',
borderColor: '边框颜色',
onlineColor: '在线颜色',
offlineColor: '离线颜色',
titleColor: '标题颜色',
textColor: '文字颜色',
labelColor: '标签颜色',
save: '保存',
copyCode: '复制嵌入代码',
saved: '域名状态设置已保存',
invalidConfig: '自定义配置不是有效的 JSON',
copied: '嵌入代码已复制',
copyFailed: '无法复制嵌入代码',
domainList: '域名列表',
online: '在线',
offline: '离线',
empty: '暂无域名',
editDomainList: '点击这里编辑域名列表',
editTitle: '编辑域名列表',
addDomain: '添加域名',
delete: '删除',
cancel: '取消',
confirm: '确认',
duplicateDomain: '域名不能重复',
domainsSaved: '域名列表已保存',
}

View File

@@ -0,0 +1,56 @@
export default {
settingTitle: '游戏 RTP 设置',
embedCode: '嵌入代码',
autoRtp: '自动游戏 RTP (%)',
autoAmount: '自动 RTP 范围',
frequency: '自动刷新频率(分钟)',
lastRefresh: '上次刷新',
providerDisplay: '显示供应商',
providerHint: '留空则显示所有供应商',
customConfig: '自定义配置',
loadTemplate: '载入模板',
headerImage: '顶部图片 URL',
textColor: '文字颜色',
baseColor: '基础颜色',
outlineColor: '边框颜色',
buttonTextColor: '按钮文字颜色',
buttonBgColor: '按钮背景颜色',
progressBgColor: '进度条背景颜色',
save: '保存',
copyCode: '复制嵌入代码',
dataTitle: '游戏 RTP 数据',
provider: '游戏供应商',
allProviders: '- 所有游戏供应商 -',
gameName: '游戏名称',
searchName: '根据游戏名称搜索',
search: '搜索',
clear: '清空',
gameId: '游戏 ID',
imageUrl: '游戏图片 URL',
rtp: 'RTP (%)',
status: '状态',
action: '操作',
active: '启用',
inactive: '停用',
edit: '编辑',
delete: '删除',
totalRecords: '共 {total} 条记录',
create: '新增游戏 RTP',
import: '从 JDK API 导入并刷新',
createTitle: '新增游戏 RTP',
editTitle: '编辑游戏 RTP',
cancel: '取消',
confirm: '确认',
providerRequired: '请输入游戏供应商',
nameRequired: '请输入游戏名称',
urlRequired: '请输入游戏图片 URL',
saved: '设置已保存',
copied: '嵌入代码已复制',
copyFailed: '无法复制嵌入代码',
operationSuccess: '操作成功',
deleteConfirm: '确定删除游戏“{name}”吗?',
warning: '提示',
deleted: '游戏已删除',
importConfirm: '确定从 JDK API 导入并刷新游戏数据吗?',
imported: '游戏数据已刷新',
}

View File

@@ -0,0 +1,52 @@
export default {
settingTitle: '合作伙伴设置',
embedCode: '嵌入代码',
headerImage: '顶部图片 URL',
footerConfig: '页脚配置',
customConfig: '自定义配置',
loadTemplate: '载入模板',
baseColor1: '基础颜色 1',
baseColor2: '基础颜色 2',
borderColor: '边框颜色',
highlightColor: '高亮颜色',
titleColor: '标题颜色',
textColor: '文字颜色',
save: '保存',
copyCode: '复制嵌入代码',
dataTitle: '合作伙伴数据',
displayOrder: '显示顺序',
partnerName: '合作伙伴名称',
currency: '币种',
logoImage: 'Logo 图片',
option: '选项',
config: '配置',
status: '状态',
action: '操作',
active: '启用',
inactive: '停用',
edit: '编辑',
delete: '删除',
empty: '暂无合作伙伴数据',
create: '新增合作伙伴',
export: '导出合作伙伴数据',
import: '导入并替换合作伙伴数据',
createTitle: '新增合作伙伴',
editTitle: '编辑合作伙伴',
cancel: '取消',
confirm: '确认',
nameRequired: '请输入合作伙伴名称',
currencyRequired: '请输入币种',
saved: '合作伙伴设置已保存',
invalidConfig: '页脚配置或自定义配置不是有效的 JSON',
copied: '嵌入代码已复制',
copyFailed: '无法复制嵌入代码',
invalidPartnerConfig: '选项或配置不是有效的 JSON',
operationSuccess: '操作成功',
warning: '提示',
deleteConfirm: '确定删除合作伙伴“{name}”吗?',
deleted: '合作伙伴已删除',
exported: '合作伙伴数据已导出',
importConfirm: '确定用导入的 {total} 条记录替换当前合作伙伴数据吗?',
imported: '合作伙伴数据已导入',
importFailed: '无法导入该合作伙伴数据文件',
}

View File

@@ -0,0 +1,16 @@
export default {
title: '访问所有程序',
bannerCode: '程序 Banner 代码',
bannerHelpPrefix: '将代码放到',
bannerHelpSuffix: ',或任何需要展示的位置。',
preview: '预览',
copyBanner: '复制 Banner 代码',
functionCode: '程序功能代码',
functionHelpPrefix: '将代码放到',
copyFunction: '复制功能代码',
copied: '代码已复制',
copyFailed: '无法复制代码',
previewTitle: '程序 Banner 预览',
previewNote: '预览点击不会打开真实程序,也不会提交账号信息。',
previewProgram: '已在预览中选择程序页面 {page}',
}

View File

@@ -56,6 +56,11 @@ export default {
Promotion: 'Promotion', Promotion: 'Promotion',
'Submittled Rewards': 'Submitted Rewards', 'Submittled Rewards': 'Submitted Rewards',
'Submitted Rewards': 'Submitted Rewards', 'Submitted Rewards': 'Submitted Rewards',
Embed: 'Embed',
'Domain Status': 'Domain Status',
'Program Widget': 'Program Widget',
'Game RTP': 'Game RTP',
Partnership: 'Partnership',
Report: 'Report', Report: 'Report',
'Annual Report': 'Annual Report', 'Annual Report': 'Annual Report',
'Daily Report': 'Daily Report', 'Daily Report': 'Daily Report',

View File

@@ -57,6 +57,11 @@ export default {
Promotion: '促销活动', Promotion: '促销活动',
'Submittled Rewards': '已提交奖励', 'Submittled Rewards': '已提交奖励',
'Submitted Rewards': '已提交奖励', 'Submitted Rewards': '已提交奖励',
Embed: '嵌入工具',
'Domain Status': '域名状态',
'Program Widget': '程序组件',
'Game RTP': '游戏 RTP',
Partnership: '合作伙伴',
Report: '报表', Report: '报表',
'Annual Report': '年度报表', 'Annual Report': '年度报表',
'Daily Report': '日报表', 'Daily Report': '日报表',

View File

@@ -60,11 +60,22 @@ const reportMenuPaths: Record<string, string> = {
客户报表: '/user/moneyLog/customerReport', 客户报表: '/user/moneyLog/customerReport',
} }
const embedMenuPaths: Record<string, string> = {
'Domain Status': '/embed/domainStatus',
域名状态: '/embed/domainStatus',
'Program Widget': '/embed/programWidget',
程序组件: '/embed/programWidget',
'Game RTP': '/embed/gameRtp',
'游戏 RTP': '/embed/gameRtp',
Partnership: '/embed/partnership',
合作伙伴: '/embed/partnership',
}
const onClickMenuItem = (menu: RouteRecordRaw) => { const onClickMenuItem = (menu: RouteRecordRaw) => {
const title = typeof menu.meta?.title === 'string' ? menu.meta.title : '' const title = typeof menu.meta?.title === 'string' ? menu.meta.title : ''
const reportPath = reportMenuPaths[title] const normalizedPath = reportMenuPaths[title] || embedMenuPaths[title]
if (reportPath) { if (normalizedPath) {
routePush(adminBaseRoutePath + reportPath) routePush(adminBaseRoutePath + normalizedPath)
return return
} }
onClickMenu(menu) onClickMenu(menu)

View File

@@ -27,6 +27,30 @@ const adminBaseRoute: RouteRecordRaw = {
title: `pagesTitle.loading`, title: `pagesTitle.loading`,
}, },
}, },
{
path: 'embed/domainStatus',
name: 'embed/domainStatus',
component: () => import('/@/views/backend/embed/domainStatus.vue'),
meta: { title: 'Domain Status', menu_type: 'tab', type: 'route', addtab: true },
},
{
path: 'embed/programWidget',
name: 'embed/programWidget',
component: () => import('/@/views/backend/embed/programWidget.vue'),
meta: { title: 'Program Widget', menu_type: 'tab', type: 'route', addtab: true },
},
{
path: 'embed/gameRtp',
name: 'embed/gameRtp',
component: () => import('/@/views/backend/embed/gameRtp.vue'),
meta: { title: 'Game RTP', menu_type: 'tab', type: 'route', addtab: true },
},
{
path: 'embed/partnership',
name: 'embed/partnership',
component: () => import('/@/views/backend/embed/partnership.vue'),
meta: { title: 'Partnership', menu_type: 'tab', type: 'route', addtab: true },
},
], ],
} }

View File

@@ -276,6 +276,12 @@ export const addRouteAll = (viewsComponent: Record<string, any>, routes: any, pa
* @param analyticRelation 根据 name 从已注册路由分析父级路由 * @param analyticRelation 根据 name 从已注册路由分析父级路由
*/ */
export const addRouteItem = (viewsComponent: Record<string, any>, route: any, parentName: string, analyticRelation: boolean) => { export const addRouteItem = (viewsComponent: Record<string, any>, route: any, parentName: string, analyticRelation: boolean) => {
const routeName = normalizeRouteName(route)
const protectedEmbedRoutes = new Set(['embed/domainStatus', 'embed/programWidget', 'embed/gameRtp', 'embed/partnership'])
if (typeof routeName === 'string' && protectedEmbedRoutes.has(routeName) && router.hasRoute(routeName)) {
return
}
let path = '', let path = '',
component component
if (route.menu_type == 'iframe') { if (route.menu_type == 'iframe') {
@@ -301,7 +307,7 @@ export const addRouteItem = (viewsComponent: Record<string, any>, route: any, pa
const routeBaseInfo: RouteRecordRaw = { const routeBaseInfo: RouteRecordRaw = {
path: path, path: path,
name: normalizeRouteName(route), name: routeName,
component: component, component: component,
meta: { meta: {
title: route.title, title: route.title,

View File

@@ -0,0 +1,452 @@
<template>
<div class="default-main domain-status-page">
<section class="setting-grid">
<div class="panel setting-panel">
<div class="panel-title">{{ t('embed.domainStatus.settingTitle') }}</div>
<div class="panel-body setting-form">
<div class="form-row">
<label>{{ t('embed.domainStatus.headerImage') }}:</label>
<el-input v-model="settings.headerImage" placeholder="e.g. https://image.png" />
</div>
<div class="form-row textarea-row">
<label>{{ t('embed.domainStatus.customConfig') }}:</label>
<el-input v-model="settings.customConfig" type="textarea" :rows="7" :placeholder="configTemplate" resize="vertical" />
</div>
<div class="form-row template-row">
<label></label>
<el-button size="small" @click="settings.customConfig = configTemplate">
{{ t('embed.domainStatus.loadTemplate') }}
</el-button>
</div>
<div class="color-grid">
<div v-for="item in colorFields" :key="item.key" class="color-item">
<label>{{ t(item.label) }}:</label>
<el-color-picker v-model="settings[item.key]" />
<span>{{ settings[item.key] }}</span>
</div>
</div>
<el-button type="success" class="save-button" @click="saveSettings">
{{ t('embed.domainStatus.save') }}
</el-button>
</div>
</div>
<div class="panel embed-panel">
<div class="panel-title">{{ t('embed.domainStatus.embedCode') }}</div>
<div class="panel-body">
<el-input v-model="embedCode" type="textarea" :rows="17" readonly resize="none" />
<el-button class="copy-button" @click="copyEmbedCode">{{ t('embed.domainStatus.copyCode') }}</el-button>
</div>
</div>
</section>
<section class="panel domain-panel">
<div class="panel-title">{{ t('embed.domainStatus.domainList') }}</div>
<div class="domain-list">
<div v-for="domain in domains" :key="domain.id" class="domain-row">
<span class="domain-icon" aria-hidden="true"></span>
<span>{{ domain.name }}</span>
<el-tag :type="domain.online ? 'success' : 'danger'" effect="plain">
{{ domain.online ? t('embed.domainStatus.online') : t('embed.domainStatus.offline') }}
</el-tag>
</div>
<el-empty v-if="domains.length === 0" :description="t('embed.domainStatus.empty')" :image-size="60" />
</div>
</section>
<el-button link type="primary" class="edit-link" @click="domainDialogVisible = true">
{{ t('embed.domainStatus.editDomainList') }}
</el-button>
<el-dialog v-model="domainDialogVisible" :title="t('embed.domainStatus.editTitle')" width="620px">
<div class="domain-editor">
<div v-for="(domain, index) in domainDraft" :key="domain.id" class="domain-edit-row">
<el-input v-model="domain.name" placeholder="example.com" />
<el-switch
v-model="domain.online"
:active-text="t('embed.domainStatus.online')"
:inactive-text="t('embed.domainStatus.offline')"
/>
<el-button type="danger" link @click="domainDraft.splice(index, 1)">
{{ t('embed.domainStatus.delete') }}
</el-button>
</div>
<el-button class="add-domain-button" @click="addDomain">{{ t('embed.domainStatus.addDomain') }}</el-button>
</div>
<template #footer>
<el-button @click="resetDomainDraft">{{ t('embed.domainStatus.cancel') }}</el-button>
<el-button type="primary" @click="saveDomains">{{ t('embed.domainStatus.confirm') }}</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
defineOptions({
name: 'embed/domainStatus',
})
interface DomainRow {
id: number
name: string
online: boolean
}
type ColorSettingKey = 'baseColor' | 'borderColor' | 'onlineColor' | 'offlineColor' | 'titleColor' | 'textColor' | 'labelColor'
const { t } = useI18n()
const configTemplate = `{
"hideDomain": [
"domain1.com",
"domain2.com"
]
}`
const settings = reactive<Record<ColorSettingKey, string> & {
headerImage: string
customConfig: string
}>({
headerImage: '',
customConfig: '',
baseColor: '#1c2130',
borderColor: '#2a3147',
onlineColor: '#48bb78',
offlineColor: '#f56565',
titleColor: '#006dff',
textColor: '#ffffff',
labelColor: '#718096',
})
const colorFields: { key: ColorSettingKey; label: string }[] = [
{ key: 'baseColor', label: 'embed.domainStatus.baseColor' },
{ key: 'titleColor', label: 'embed.domainStatus.titleColor' },
{ key: 'borderColor', label: 'embed.domainStatus.borderColor' },
{ key: 'textColor', label: 'embed.domainStatus.textColor' },
{ key: 'onlineColor', label: 'embed.domainStatus.onlineColor' },
{ key: 'labelColor', label: 'embed.domainStatus.labelColor' },
{ key: 'offlineColor', label: 'embed.domainStatus.offlineColor' },
]
const embedCode = ref(`<div style="position: relative;max-width: 1000px;margin: 0 auto;overflow: hidden;border: none;">
<iframe id="domainstatusIframe" src="https://1xaud.em7bd7.co/embed/domain-status/domain-status.asp" style="width: 100%;overflow: hidden;border: none;"></iframe>
<script>
(() => {
const merchantId = (typeof _ !== "undefined" && typeof _.getVar === "function") ? (_.getVar("merchantId") ?? "") : "";
if (!merchantId) {
console.log("merchantId not found");
return;
}
function init(iframe) {
iframe.addEventListener('load', function() {
iframe.contentWindow.postMessage({ type: 'setIframeData', clientMerchant: merchantId }, '*');
});
window.addEventListener("message", function(event) {
if (event.data && event.data.type === "setDomainStatusIframeHeight") {
iframe.style.height = event.data.height + "px";
}
}, false);
}
var el = document.getElementById('domainstatusIframe');
if (el) { init(el); return; }
var attempts = 0;
var timer = setInterval(function() {
attempts++;
var el = document.getElementById('domainstatusIframe');
if (el) {
clearInterval(timer);
init(el);
} else if (attempts >= 5) {
clearInterval(timer);
alert("iframe not found");
}
}, 1000);
})();
<\/script>
</div>`)
const saveSettings = () => {
try {
if (settings.customConfig) JSON.parse(settings.customConfig)
ElMessage.success(t('embed.domainStatus.saved'))
} catch {
ElMessage.error(t('embed.domainStatus.invalidConfig'))
}
}
const copyEmbedCode = async () => {
try {
await navigator.clipboard.writeText(embedCode.value)
ElMessage.success(t('embed.domainStatus.copied'))
} catch {
ElMessage.error(t('embed.domainStatus.copyFailed'))
}
}
const domains = ref<DomainRow[]>([{ id: 1, name: '1xaud.com', online: true }])
const domainDraft = ref<DomainRow[]>([])
const domainDialogVisible = ref(false)
const cloneDomains = () => domains.value.map((domain) => ({ ...domain }))
watch(domainDialogVisible, (visible) => {
if (visible) domainDraft.value = cloneDomains()
})
const addDomain = () => {
const nextId = Math.max(0, ...domainDraft.value.map((domain) => domain.id)) + 1
domainDraft.value.push({ id: nextId, name: '', online: true })
}
const resetDomainDraft = () => {
domainDraft.value = cloneDomains()
domainDialogVisible.value = false
}
const saveDomains = () => {
const cleaned = domainDraft.value.map((domain) => ({ ...domain, name: domain.name.trim() })).filter((domain) => domain.name)
const names = cleaned.map((domain) => domain.name.toLowerCase())
if (new Set(names).size !== names.length) {
ElMessage.error(t('embed.domainStatus.duplicateDomain'))
return
}
domains.value = cleaned
domainDialogVisible.value = false
ElMessage.success(t('embed.domainStatus.domainsSaved'))
}
</script>
<style scoped lang="scss">
.domain-status-page {
padding: var(--ba-main-space);
color: var(--el-text-color-primary);
}
.setting-grid {
display: grid;
grid-template-columns: minmax(560px, 7fr) minmax(380px, 3fr);
gap: 14px;
margin-bottom: 14px;
}
.panel {
overflow: hidden;
border: 1px solid var(--el-border-color);
border-radius: 4px;
background: var(--el-bg-color);
}
.panel-title {
padding: 10px 14px;
color: #fff;
font-size: 14px;
font-weight: 700;
background: #333;
}
.panel-body {
padding: 16px;
}
.setting-form {
min-height: 410px;
}
.form-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
> label {
width: 195px;
flex: 0 0 195px;
text-align: right;
font-weight: 600;
}
:deep(.el-input) {
flex: 1;
}
}
.textarea-row {
align-items: flex-start;
}
.template-row {
margin-top: -2px;
margin-bottom: 10px;
}
.color-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 22px;
margin: 18px 0;
}
.color-item {
display: flex;
align-items: center;
gap: 8px;
label {
min-width: 145px;
text-align: right;
font-weight: 600;
}
span {
color: var(--el-text-color-secondary);
font-family: monospace;
font-size: 12px;
}
}
.save-button {
width: calc(100% - 207px);
margin-left: 207px;
font-weight: 700;
}
.embed-panel {
:deep(textarea) {
padding: 14px;
color: var(--el-text-color-primary);
background: var(--el-fill-color-light);
font-family: monospace;
font-size: 12px;
line-height: 1.55;
}
}
.copy-button {
width: 100%;
margin-top: 12px;
}
.domain-list {
min-height: 44px;
}
.domain-row {
display: flex;
align-items: center;
gap: 9px;
min-height: 44px;
padding: 0 14px;
border-bottom: 1px solid var(--el-border-color-lighter);
&:last-child {
border-bottom: 0;
}
.el-tag {
margin-left: auto;
}
}
.domain-icon {
position: relative;
width: 16px;
height: 16px;
flex: 0 0 16px;
border: 1.5px solid var(--el-text-color-secondary);
border-radius: 50%;
&::before,
&::after {
position: absolute;
content: '';
}
&::before {
top: 2px;
bottom: 2px;
left: 4px;
right: 4px;
border-right: 1px solid var(--el-text-color-secondary);
border-left: 1px solid var(--el-text-color-secondary);
border-radius: 50%;
}
&::after {
top: 7px;
right: 1px;
left: 1px;
border-top: 1px solid var(--el-text-color-secondary);
}
}
.edit-link {
margin-top: 10px;
padding-left: 0;
}
.domain-editor {
display: flex;
flex-direction: column;
gap: 12px;
}
.domain-edit-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
align-items: center;
gap: 12px;
}
.add-domain-button {
width: 100%;
}
@media (max-width: 1100px) {
.setting-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.domain-status-page {
padding: 12px;
}
.form-row {
align-items: stretch;
flex-direction: column;
gap: 6px;
> label {
width: auto;
flex-basis: auto;
text-align: left;
}
}
.color-grid {
grid-template-columns: 1fr;
}
.color-item label {
min-width: 130px;
text-align: left;
}
.save-button {
width: 100%;
margin-left: 0;
}
.domain-edit-row {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,626 @@
<template>
<div class="default-main game-rtp-page">
<section class="setting-grid">
<div class="panel setting-panel">
<div class="panel-title">{{ t('embed.gameRtp.settingTitle') }}</div>
<div class="panel-body setting-form">
<div class="form-row">
<label>{{ t('embed.gameRtp.autoRtp') }}:</label>
<el-select v-model="settings.autoRtp">
<el-option label="OFF" :value="0" />
<el-option label="ON" :value="1" />
</el-select>
</div>
<div class="form-row">
<label>{{ t('embed.gameRtp.autoAmount') }}:</label>
<el-input v-model="settings.autoAmount" placeholder='e.g. {"max":70,"min":20}' />
</div>
<div class="form-row">
<label>{{ t('embed.gameRtp.frequency') }}:</label>
<el-input v-model="settings.frequency" placeholder="e.g. 60" />
</div>
<div class="form-hint">
{{ t('embed.gameRtp.lastRefresh') }}: {{ lastRefresh }}
</div>
<div class="form-row">
<label>{{ t('embed.gameRtp.providerDisplay') }}:</label>
<el-input v-model="settings.providerDisplay" placeholder='e.g. ["XE88","LFC2","GMSL1"]' />
</div>
<div class="form-hint">{{ t('embed.gameRtp.providerHint') }}</div>
<div class="form-row textarea-row">
<label>{{ t('embed.gameRtp.customConfig') }}:</label>
<el-input
v-model="settings.customConfig"
type="textarea"
:rows="7"
:placeholder="configTemplate"
resize="vertical"
/>
</div>
<div class="form-row">
<label></label>
<el-button size="small" @click="loadTemplate">{{ t('embed.gameRtp.loadTemplate') }}</el-button>
</div>
<div class="form-row">
<label>{{ t('embed.gameRtp.headerImage') }}:</label>
<el-input v-model="settings.headerImage" placeholder="e.g. https://image.png" />
</div>
<div class="color-grid">
<div v-for="item in colorFields" :key="item.key" class="color-item">
<label>{{ t(item.label) }}:</label>
<el-color-picker v-model="settings[item.key]" />
<span>{{ settings[item.key] }}</span>
</div>
</div>
<el-button type="primary" class="save-button" @click="saveSettings">
{{ t('embed.gameRtp.save') }}
</el-button>
</div>
</div>
<div class="panel embed-panel">
<div class="panel-title">{{ t('embed.gameRtp.embedCode') }}</div>
<div class="panel-body">
<el-input v-model="embedCode" type="textarea" :rows="23" readonly resize="none" />
<el-button class="copy-button" @click="copyEmbedCode">{{ t('embed.gameRtp.copyCode') }}</el-button>
</div>
</div>
</section>
<section class="panel data-panel">
<div class="panel-title">{{ t('embed.gameRtp.dataTitle') }}</div>
<div class="filter-bar">
<div class="filter-field">
<label>{{ t('embed.gameRtp.provider') }}:</label>
<el-select v-model="filters.provider" filterable>
<el-option :label="t('embed.gameRtp.allProviders')" value="" />
<el-option v-for="provider in providers" :key="provider" :label="provider" :value="provider" />
</el-select>
</div>
<div class="filter-field">
<label>{{ t('embed.gameRtp.gameName') }}:</label>
<el-input v-model="filters.name" :placeholder="t('embed.gameRtp.searchName')" clearable @keyup.enter="search" />
</div>
<div class="filter-actions">
<el-button type="primary" @click="search">{{ t('embed.gameRtp.search') }}</el-button>
<el-button @click="clearSearch">{{ t('embed.gameRtp.clear') }}</el-button>
</div>
</div>
<div class="table-wrap">
<el-table :data="pagedRows" 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" />
<el-table-column :label="t('embed.gameRtp.imageUrl')" min-width="360">
<template #default="{ row }">
<a class="image-link" :href="row.imageUrl" target="_blank" rel="noopener noreferrer">{{ row.imageUrl }}</a>
</template>
</el-table-column>
<el-table-column prop="rtp" :label="t('embed.gameRtp.rtp')" width="100" align="center" />
<el-table-column :label="t('embed.gameRtp.status')" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" effect="plain">
{{ row.status === 1 ? t('embed.gameRtp.active') : t('embed.gameRtp.inactive') }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('embed.gameRtp.action')" width="150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDialog('edit', row)">{{ t('embed.gameRtp.edit') }}</el-button>
<el-button link type="danger" @click="removeRow(row)">{{ t('embed.gameRtp.delete') }}</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="table-footer">
<span>{{ t('embed.gameRtp.totalRecords', { total: filteredRows.length }) }}</span>
<el-pagination
v-model:current-page="currentPage"
background
layout="prev, pager, next"
:page-size="pageSize"
:total="filteredRows.length"
/>
</div>
</section>
<div class="bottom-actions">
<el-button type="success" @click="openDialog('create')">{{ t('embed.gameRtp.create') }}</el-button>
<el-button type="warning" @click="importGames">{{ t('embed.gameRtp.import') }}</el-button>
</div>
<el-dialog
v-model="dialogVisible"
:title="dialogMode === 'create' ? t('embed.gameRtp.createTitle') : t('embed.gameRtp.editTitle')"
width="520px"
destroy-on-close
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form-item :label="t('embed.gameRtp.provider')" prop="provider">
<el-input v-model="form.provider" placeholder="e.g. XE88" />
</el-form-item>
<el-form-item :label="t('embed.gameRtp.gameName')" prop="name">
<el-input v-model="form.name" placeholder="e.g. Aztec" />
</el-form-item>
<el-form-item :label="t('embed.gameRtp.imageUrl')" prop="imageUrl">
<el-input v-model="form.imageUrl" placeholder="e.g. https://image.png" />
</el-form-item>
<el-form-item :label="t('embed.gameRtp.rtp')" prop="rtp">
<el-input-number v-model="form.rtp" :min="0" :max="100" />
</el-form-item>
<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-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>
</template>
</el-dialog>
</div>
</template>
<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 { useI18n } from 'vue-i18n'
defineOptions({
name: 'embed/gameRtp',
})
interface GameRtpRow {
id: number
provider: string
name: string
imageUrl: string
rtp: number
status: number
}
type ColorSettingKey = 'textColor' | 'baseColor' | 'outlineColor' | 'buttonTextColor' | 'buttonBgColor' | 'progressBgColor'
const { t } = useI18n()
const configTemplate = `{
"customGameRTP": [
{"max": 99, "min": 95, "gameId": 153},
{"max": 80, "min": 60, "gameId": 723}
],
"providerNameMapping": {
"VP": "V-POWER",
"MG888H52": "MEGA888 H5"
}
}`
const settings = reactive<Record<ColorSettingKey, string> & {
autoRtp: number
autoAmount: string
frequency: string
providerDisplay: string
customConfig: string
headerImage: string
}>({
autoRtp: 1,
autoAmount: '{"max":"90","min":"30"}',
frequency: '10',
providerDisplay: '',
customConfig: '',
headerImage: '',
textColor: '#ffffff',
baseColor: '#045bb9',
outlineColor: '#000000',
buttonTextColor: '#ffffff',
buttonBgColor: '#1c202a',
progressBgColor: '#5f5f5f',
})
const colorFields: { key: ColorSettingKey; label: string }[] = [
{ key: 'textColor', label: 'embed.gameRtp.textColor' },
{ key: 'baseColor', label: 'embed.gameRtp.baseColor' },
{ key: 'outlineColor', label: 'embed.gameRtp.outlineColor' },
{ key: 'buttonTextColor', label: 'embed.gameRtp.buttonTextColor' },
{ key: 'buttonBgColor', label: 'embed.gameRtp.buttonBgColor' },
{ key: 'progressBgColor', label: 'embed.gameRtp.progressBgColor' },
]
const lastRefresh = ref('2026-06-11 18:23:54')
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>
<script>
(() => {
const merchantId = (typeof _ !== "undefined" && typeof _.getVar === "function") ? (_.getVar("merchantId") ?? "") : "";
if (!merchantId) {
console.log("merchantId not found");
return;
}
function init(iframe) {
iframe.addEventListener('load', function() {
iframe.contentWindow.postMessage({ type: 'setIframeData', clientMerchant: merchantId }, '*');
});
window.addEventListener("message", function(event) {
if (event.data && event.data.type === "setGameRtpIframeHeight") {
iframe.style.height = event.data.height + 50 + "px";
}
}, false);
}
var el = document.getElementById('gameRtpIframe');
if (el) { init(el); return; }
var attempts = 0;
var timer = setInterval(function() {
attempts++;
var el = document.getElementById('gameRtpIframe');
if (el) {
clearInterval(timer);
init(el);
} else if (attempts >= 5) {
clearInterval(timer);
alert("iframe not found");
}
}, 1000);
})();
<\/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 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 currentPage = ref(1)
const pageSize = 10
const pagedRows = computed(() => filteredRows.value.slice((currentPage.value - 1) * pageSize, currentPage.value * pageSize))
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 clearSearch = () => {
Object.assign(filters, { provider: '', name: '' })
search()
}
const loadTemplate = () => {
settings.customConfig = configTemplate
}
const saveSettings = () => {
lastRefresh.value = new Date().toLocaleString()
ElMessage.success(t('embed.gameRtp.saved'))
}
const copyEmbedCode = async () => {
try {
await navigator.clipboard.writeText(embedCode.value)
ElMessage.success(t('embed.gameRtp.copied'))
} catch {
ElMessage.error(t('embed.gameRtp.copyFailed'))
}
}
const dialogVisible = ref(false)
const dialogMode = ref<'create' | 'edit'>('create')
const editingId = ref<number>()
const formRef = ref<FormInstance>()
const form = reactive({
provider: '',
name: '',
imageUrl: '',
rtp: 0,
status: 1,
})
const rules = computed<FormRules>(() => ({
provider: [{ required: true, message: t('embed.gameRtp.providerRequired'), trigger: 'blur' }],
name: [{ required: true, message: t('embed.gameRtp.nameRequired'), trigger: 'blur' }],
imageUrl: [{ required: true, message: t('embed.gameRtp.urlRequired'), trigger: 'blur' }],
}))
const openDialog = (mode: 'create' | 'edit', row?: GameRtpRow) => {
dialogMode.value = mode
editingId.value = row?.id
Object.assign(form, row ?? { provider: '', name: '', imageUrl: '', rtp: 0, status: 1 })
dialogVisible.value = true
}
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 })
} else {
const index = rows.value.findIndex((row) => row.id === editingId.value)
if (index >= 0) rows.value[index] = { id: editingId.value!, ...form }
}
dialogVisible.value = false
ElMessage.success(t('embed.gameRtp.operationSuccess'))
}
const removeRow = async (row: GameRtpRow) => {
try {
await ElMessageBox.confirm(t('embed.gameRtp.deleteConfirm', { name: row.name }), t('embed.gameRtp.warning'), {
type: 'warning',
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'))
} catch {
return
}
}
const importGames = async () => {
try {
await ElMessageBox.confirm(t('embed.gameRtp.importConfirm'), t('embed.gameRtp.warning'), {
type: 'warning',
confirmButtonText: t('embed.gameRtp.confirm'),
cancelButtonText: t('embed.gameRtp.cancel'),
})
lastRefresh.value = new Date().toLocaleString()
ElMessage.success(t('embed.gameRtp.imported'))
} catch {
return
}
}
</script>
<style scoped lang="scss">
.game-rtp-page {
padding: var(--ba-main-space);
color: var(--el-text-color-primary);
}
.setting-grid {
display: grid;
grid-template-columns: minmax(520px, 7fr) minmax(360px, 3fr);
gap: 14px;
margin-bottom: 14px;
}
.panel {
overflow: hidden;
border: 1px solid var(--el-border-color);
border-radius: 4px;
background: var(--el-bg-color);
}
.panel-title {
padding: 10px 14px;
color: #fff;
font-size: 14px;
font-weight: 700;
background: #333;
}
.panel-body {
padding: 16px;
}
.setting-form {
min-height: 580px;
}
.form-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
> label {
width: 205px;
flex: 0 0 205px;
text-align: right;
font-weight: 600;
}
:deep(.el-input),
:deep(.el-select) {
flex: 1;
}
}
.textarea-row {
align-items: flex-start;
}
.form-hint {
margin: -6px 0 12px 217px;
color: var(--el-color-danger);
font-size: 12px;
font-weight: 600;
}
.color-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px 20px;
margin: 18px 0;
}
.color-item {
display: flex;
align-items: center;
gap: 8px;
label {
min-width: 145px;
text-align: right;
font-weight: 600;
}
span {
color: var(--el-text-color-secondary);
font-size: 12px;
font-family: monospace;
}
}
.save-button {
margin-left: 217px;
min-width: 110px;
}
.embed-panel {
:deep(textarea) {
padding: 14px;
color: var(--el-text-color-primary);
background: var(--el-fill-color-light);
font-family: monospace;
font-size: 12px;
line-height: 1.55;
}
}
.copy-button {
width: 100%;
margin-top: 12px;
}
.filter-bar {
display: flex;
align-items: flex-end;
flex-wrap: wrap;
gap: 14px 20px;
padding: 16px;
}
.filter-field {
display: flex;
align-items: center;
gap: 10px;
label {
font-weight: 600;
}
:deep(.el-input),
:deep(.el-select) {
width: 220px;
}
}
.table-wrap {
overflow-x: auto;
border-top: 1px solid var(--el-border-color);
}
.image-link {
display: block;
overflow: hidden;
color: var(--el-color-primary);
text-overflow: ellipsis;
white-space: nowrap;
}
.table-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 16px;
}
.bottom-actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 14px;
.el-button {
height: 38px;
margin: 0;
font-weight: 700;
}
}
@media (max-width: 1100px) {
.setting-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.game-rtp-page {
padding: 12px;
}
.form-row {
align-items: stretch;
flex-direction: column;
gap: 6px;
> label {
width: auto;
flex-basis: auto;
text-align: left;
}
}
.form-hint,
.save-button {
margin-left: 0;
}
.color-grid {
grid-template-columns: 1fr;
}
.color-item label {
min-width: 130px;
text-align: left;
}
.filter-bar,
.filter-field {
align-items: stretch;
flex-direction: column;
}
.filter-field :deep(.el-input),
.filter-field :deep(.el-select) {
width: 100%;
}
.table-footer {
align-items: flex-start;
flex-direction: column;
}
.bottom-actions {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,603 @@
<template>
<div class="default-main partnership-page">
<section class="setting-grid">
<div class="panel setting-panel">
<div class="panel-title">{{ t('embed.partnership.settingTitle') }}</div>
<div class="panel-body setting-form">
<div class="form-row">
<label>{{ t('embed.partnership.headerImage') }}:</label>
<el-input v-model="settings.headerImage" placeholder="e.g. https://image.png" />
</div>
<div class="form-row textarea-row">
<label>{{ t('embed.partnership.footerConfig') }}:</label>
<el-input v-model="settings.footerConfig" type="textarea" :rows="8" :placeholder="footerTemplate" resize="vertical" />
</div>
<div class="form-row template-row">
<label></label>
<el-button size="small" @click="settings.footerConfig = footerTemplate">
{{ t('embed.partnership.loadTemplate') }}
</el-button>
</div>
<div class="form-row textarea-row">
<label>{{ t('embed.partnership.customConfig') }}:</label>
<el-input v-model="settings.customConfig" type="textarea" :rows="7" :placeholder="customTemplate" resize="vertical" />
</div>
<div class="form-row template-row">
<label></label>
<el-button size="small" @click="settings.customConfig = customTemplate">
{{ t('embed.partnership.loadTemplate') }}
</el-button>
</div>
<div class="color-grid">
<div v-for="item in colorFields" :key="item.key" class="color-item">
<label>{{ t(item.label) }}:</label>
<el-color-picker v-model="settings[item.key]" />
<span>{{ settings[item.key] }}</span>
</div>
</div>
<el-button type="success" class="save-button" @click="saveSettings">
{{ t('embed.partnership.save') }}
</el-button>
</div>
</div>
<div class="panel embed-panel">
<div class="panel-title">{{ t('embed.partnership.embedCode') }}</div>
<div class="panel-body">
<el-input v-model="embedCode" type="textarea" :rows="19" readonly resize="none" />
<el-button class="copy-button" @click="copyEmbedCode">{{ t('embed.partnership.copyCode') }}</el-button>
</div>
</div>
</section>
<section class="panel data-panel">
<div class="panel-title">{{ t('embed.partnership.dataTitle') }}</div>
<div class="table-wrap">
<el-table :data="partners" border :empty-text="t('embed.partnership.empty')">
<el-table-column prop="displayOrder" :label="t('embed.partnership.displayOrder')" width="140" />
<el-table-column prop="name" :label="t('embed.partnership.partnerName')" min-width="180" />
<el-table-column prop="currency" :label="t('embed.partnership.currency')" width="130" />
<el-table-column :label="t('embed.partnership.logoImage')" min-width="240">
<template #default="{ row }">
<a v-if="row.logoImage" class="image-link" :href="row.logoImage" target="_blank" rel="noopener noreferrer">
{{ row.logoImage }}
</a>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="option" :label="t('embed.partnership.option')" min-width="180" show-overflow-tooltip />
<el-table-column prop="config" :label="t('embed.partnership.config')" min-width="220" show-overflow-tooltip />
<el-table-column :label="t('embed.partnership.status')" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" effect="plain">
{{ row.status === 1 ? t('embed.partnership.active') : t('embed.partnership.inactive') }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('embed.partnership.action')" width="150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDialog('edit', row)">{{ t('embed.partnership.edit') }}</el-button>
<el-button link type="danger" @click="removePartner(row)">{{ t('embed.partnership.delete') }}</el-button>
</template>
</el-table-column>
</el-table>
</div>
</section>
<div class="bottom-actions">
<el-button type="success" class="create-button" @click="openDialog('create')">
{{ t('embed.partnership.create') }}
</el-button>
<el-button type="primary" @click="exportPartners">{{ t('embed.partnership.export') }}</el-button>
<el-upload
accept=".json,application/json"
:auto-upload="false"
:show-file-list="false"
:on-change="importPartners"
>
<el-button type="warning">{{ t('embed.partnership.import') }}</el-button>
</el-upload>
</div>
<el-dialog
v-model="dialogVisible"
:title="dialogMode === 'create' ? t('embed.partnership.createTitle') : t('embed.partnership.editTitle')"
width="600px"
destroy-on-close
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<div class="dialog-grid">
<el-form-item :label="t('embed.partnership.displayOrder')" prop="displayOrder">
<el-input-number v-model="form.displayOrder" :min="0" />
</el-form-item>
<el-form-item :label="t('embed.partnership.currency')" prop="currency">
<el-input v-model="form.currency" placeholder="AUD" />
</el-form-item>
</div>
<el-form-item :label="t('embed.partnership.partnerName')" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item :label="t('embed.partnership.logoImage')" prop="logoImage">
<el-input v-model="form.logoImage" placeholder="https://image.png" />
</el-form-item>
<el-form-item :label="t('embed.partnership.option')">
<el-input v-model="form.option" type="textarea" :rows="3" placeholder="{}" />
</el-form-item>
<el-form-item :label="t('embed.partnership.config')">
<el-input v-model="form.config" type="textarea" :rows="3" placeholder="{}" />
</el-form-item>
<el-form-item :label="t('embed.partnership.status')">
<el-select v-model="form.status">
<el-option :label="t('embed.partnership.active')" :value="1" />
<el-option :label="t('embed.partnership.inactive')" :value="2" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">{{ t('embed.partnership.cancel') }}</el-button>
<el-button type="primary" @click="submitForm">{{ t('embed.partnership.confirm') }}</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules, UploadFile } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
defineOptions({
name: 'embed/partnership',
})
interface PartnerRow {
id: number
displayOrder: number
name: string
currency: string
logoImage: string
option: string
config: string
status: number
}
type ColorSettingKey = 'baseColor1' | 'baseColor2' | 'borderColor' | 'highlightColor' | 'titleColor' | 'textColor'
const { t } = useI18n()
const footerTemplate = `{
"infoText": "We specialise in curating Asia's top licensed gaming and entertainment platforms, delivering the safest, most transparent, and most rewarding experience for every member.",
"label": {
"highlight": ["PAGCOR Licensed", "SSL Encrypted"],
"normal": ["10+ Top Platforms", "Instant Payouts", "24/7 Support"]
},
"sections": [
{
"title": "Partner Advantages",
"items": [
"Exclusive negotiated deals you won't find anywhere else",
"New member welcome bonus up to 200% on first deposit",
"Unlimited daily cashback, the more you play, the more you earn"
]
},
{
"title": "Security Guarantee",
"items": [
"Platform-wide 256-bit SSL encryption end-to-end",
"RNG independently certified by third-party auditors",
"Segregated player funds"
]
},
{
"title": "Withdrawal Promise",
"items": [
"As fast as 3 minutes",
"24-hour withdrawal channel",
"Zero transaction fees"
]
}
]
}`
const customTemplate = `{
"hideSection": [
"partnerCard",
"partnerWithdrawal",
"payoutRates",
"partnerReviews",
"footer"
]
}`
const settings = reactive<Record<ColorSettingKey, string> & {
headerImage: string
footerConfig: string
customConfig: string
}>({
headerImage: '',
footerConfig: footerTemplate,
customConfig: '',
baseColor1: '#151921',
baseColor2: '#1c2130',
borderColor: '#2a3147',
highlightColor: '#f0b429',
titleColor: '#e2e8f0',
textColor: '#718096',
})
const colorFields: { key: ColorSettingKey; label: string }[] = [
{ key: 'baseColor1', label: 'embed.partnership.baseColor1' },
{ key: 'highlightColor', label: 'embed.partnership.highlightColor' },
{ key: 'baseColor2', label: 'embed.partnership.baseColor2' },
{ key: 'titleColor', label: 'embed.partnership.titleColor' },
{ key: 'borderColor', label: 'embed.partnership.borderColor' },
{ key: 'textColor', label: 'embed.partnership.textColor' },
]
const embedCode = ref(`<div style="position: relative;max-width: 1000px;margin: 0 auto;overflow: hidden;border: none;">
<iframe id="partnershipIframe" src="https://1xaud.em7bd7.co/embed/partnership/partnership.asp" style="width: 100%;overflow: hidden;border: none;"></iframe>
<script>
(() => {
const merchantId = (typeof _ !== "undefined" && typeof _.getVar === "function") ? (_.getVar("merchantId") ?? "") : "";
if (!merchantId) {
console.log("merchantId not found");
return;
}
function init(iframe) {
iframe.addEventListener('load', function() {
iframe.contentWindow.postMessage({ type: 'setIframeData', clientMerchant: merchantId }, '*');
});
window.addEventListener("message", function(event) {
if (event.data && event.data.type === "setPartnershipIframeHeight") {
iframe.style.height = event.data.height + "px";
}
}, false);
}
var el = document.getElementById('partnershipIframe');
if (el) { init(el); return; }
var attempts = 0;
var timer = setInterval(function() {
attempts++;
var el = document.getElementById('partnershipIframe');
if (el) {
clearInterval(timer);
init(el);
} else if (attempts >= 5) {
clearInterval(timer);
alert("iframe not found");
}
}, 1000);
})();
<\/script>
</div>`)
const partners = ref<PartnerRow[]>([])
const saveSettings = () => {
try {
if (settings.footerConfig) JSON.parse(settings.footerConfig)
if (settings.customConfig) JSON.parse(settings.customConfig)
ElMessage.success(t('embed.partnership.saved'))
} catch {
ElMessage.error(t('embed.partnership.invalidConfig'))
}
}
const copyEmbedCode = async () => {
try {
await navigator.clipboard.writeText(embedCode.value)
ElMessage.success(t('embed.partnership.copied'))
} catch {
ElMessage.error(t('embed.partnership.copyFailed'))
}
}
const dialogVisible = ref(false)
const dialogMode = ref<'create' | 'edit'>('create')
const editingId = ref<number>()
const formRef = ref<FormInstance>()
const form = reactive({
displayOrder: 0,
name: '',
currency: 'AUD',
logoImage: '',
option: '',
config: '',
status: 1,
})
const rules = computed<FormRules>(() => ({
name: [{ required: true, message: t('embed.partnership.nameRequired'), trigger: 'blur' }],
currency: [{ required: true, message: t('embed.partnership.currencyRequired'), trigger: 'blur' }],
}))
const openDialog = (mode: 'create' | 'edit', row?: PartnerRow) => {
dialogMode.value = mode
editingId.value = row?.id
Object.assign(
form,
row ?? {
displayOrder: partners.value.length + 1,
name: '',
currency: 'AUD',
logoImage: '',
option: '',
config: '',
status: 1,
}
)
dialogVisible.value = true
}
const submitForm = async () => {
if (!(await formRef.value?.validate().catch(() => false))) return
try {
if (form.option) JSON.parse(form.option)
if (form.config) JSON.parse(form.config)
} catch {
ElMessage.error(t('embed.partnership.invalidPartnerConfig'))
return
}
if (dialogMode.value === 'create') {
const nextId = Math.max(0, ...partners.value.map((partner) => partner.id)) + 1
partners.value.push({ id: nextId, ...form })
} else {
const index = partners.value.findIndex((partner) => partner.id === editingId.value)
if (index >= 0) partners.value[index] = { id: editingId.value!, ...form }
}
partners.value.sort((a, b) => a.displayOrder - b.displayOrder)
dialogVisible.value = false
ElMessage.success(t('embed.partnership.operationSuccess'))
}
const removePartner = async (row: PartnerRow) => {
try {
await ElMessageBox.confirm(t('embed.partnership.deleteConfirm', { name: row.name }), t('embed.partnership.warning'), {
type: 'warning',
confirmButtonText: t('embed.partnership.confirm'),
cancelButtonText: t('embed.partnership.cancel'),
})
partners.value = partners.value.filter((partner) => partner.id !== row.id)
ElMessage.success(t('embed.partnership.deleted'))
} catch {
return
}
}
const exportPartners = () => {
const blob = new Blob([JSON.stringify(partners.value, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'partner-data.json'
link.click()
URL.revokeObjectURL(url)
ElMessage.success(t('embed.partnership.exported'))
}
const importPartners = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) return
try {
const data = JSON.parse(await uploadFile.raw.text())
if (!Array.isArray(data)) throw new Error('Invalid partner data')
const imported = data.map((item, index) => ({
id: Number(item.id) || index + 1,
displayOrder: Number(item.displayOrder) || index + 1,
name: String(item.name ?? ''),
currency: String(item.currency ?? ''),
logoImage: String(item.logoImage ?? ''),
option: typeof item.option === 'string' ? item.option : JSON.stringify(item.option ?? {}),
config: typeof item.config === 'string' ? item.config : JSON.stringify(item.config ?? {}),
status: Number(item.status) === 2 ? 2 : 1,
}))
await ElMessageBox.confirm(t('embed.partnership.importConfirm', { total: imported.length }), t('embed.partnership.warning'), {
type: 'warning',
confirmButtonText: t('embed.partnership.confirm'),
cancelButtonText: t('embed.partnership.cancel'),
})
partners.value = imported
ElMessage.success(t('embed.partnership.imported'))
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(t('embed.partnership.importFailed'))
}
}
</script>
<style scoped lang="scss">
.partnership-page {
padding: var(--ba-main-space);
color: var(--el-text-color-primary);
}
.setting-grid {
display: grid;
grid-template-columns: minmax(560px, 7fr) minmax(380px, 3fr);
gap: 14px;
margin-bottom: 14px;
}
.panel {
overflow: hidden;
border: 1px solid var(--el-border-color);
border-radius: 4px;
background: var(--el-bg-color);
}
.panel-title {
padding: 10px 14px;
color: #fff;
font-size: 14px;
font-weight: 700;
background: #333;
}
.panel-body {
padding: 16px;
}
.setting-form {
min-height: 510px;
}
.form-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
> label {
width: 195px;
flex: 0 0 195px;
text-align: right;
font-weight: 600;
}
:deep(.el-input) {
flex: 1;
}
}
.textarea-row {
align-items: flex-start;
}
.template-row {
margin-top: -2px;
margin-bottom: 10px;
}
.color-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 22px;
margin: 18px 0;
}
.color-item {
display: flex;
align-items: center;
gap: 8px;
label {
min-width: 145px;
text-align: right;
font-weight: 600;
}
span {
color: var(--el-text-color-secondary);
font-family: monospace;
font-size: 12px;
}
}
.save-button {
width: calc(100% - 207px);
margin-left: 207px;
font-weight: 700;
}
.embed-panel {
:deep(textarea) {
padding: 14px;
color: var(--el-text-color-primary);
background: var(--el-fill-color-light);
font-family: monospace;
font-size: 12px;
line-height: 1.55;
}
}
.copy-button {
width: 100%;
margin-top: 12px;
}
.table-wrap {
overflow-x: auto;
}
.image-link {
display: block;
overflow: hidden;
color: var(--el-color-primary);
text-overflow: ellipsis;
white-space: nowrap;
}
.bottom-actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 14px;
.create-button {
grid-column: 1 / -1;
}
.el-button,
:deep(.el-upload),
:deep(.el-upload .el-button) {
width: 100%;
height: 38px;
margin: 0;
font-weight: 700;
}
}
.dialog-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
@media (max-width: 1100px) {
.setting-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.partnership-page {
padding: 12px;
}
.form-row {
align-items: stretch;
flex-direction: column;
gap: 6px;
> label {
width: auto;
flex-basis: auto;
text-align: left;
}
}
.color-grid,
.dialog-grid,
.bottom-actions {
grid-template-columns: 1fr;
}
.color-item label {
min-width: 130px;
text-align: left;
}
.save-button {
width: 100%;
margin-left: 0;
}
.bottom-actions .create-button {
grid-column: auto;
}
}
</style>

View File

@@ -0,0 +1,267 @@
<template>
<div class="default-main program-widget-page">
<section class="panel">
<div class="panel-title">{{ t('embed.programWidget.title') }}</div>
<div class="code-section">
<div class="section-heading">{{ t('embed.programWidget.bannerCode') }}:</div>
<p class="section-help">
{{ t('embed.programWidget.bannerHelpPrefix') }}
<strong>JDK Back-end &gt; DISPLAY &gt; SEO &gt; Heading 1</strong>
{{ t('embed.programWidget.bannerHelpSuffix') }}
</p>
<el-input v-model="bannerCode" type="textarea" :rows="7" readonly resize="vertical" />
<div class="section-actions">
<el-button @click="previewVisible = true">{{ t('embed.programWidget.preview') }}</el-button>
<el-button type="primary" plain @click="copyCode(bannerCode)">
{{ t('embed.programWidget.copyBanner') }}
</el-button>
</div>
</div>
<div class="code-section function-section">
<div class="section-heading">{{ t('embed.programWidget.functionCode') }}:</div>
<p class="section-help">
{{ t('embed.programWidget.functionHelpPrefix') }}
<strong>JDK Back-end &gt; LAYOUT &gt; Home Page · Floating Widget &gt; HTML</strong>
</p>
<el-input v-model="functionCode" type="textarea" :rows="7" readonly resize="vertical" />
<div class="section-actions">
<el-button type="primary" plain @click="copyCode(functionCode)">
{{ t('embed.programWidget.copyFunction') }}
</el-button>
</div>
</div>
</section>
<el-dialog v-model="previewVisible" :title="t('embed.programWidget.previewTitle')" width="760px">
<div class="preview-grid">
<button
v-for="program in programs"
:key="program.page"
type="button"
class="program-banner"
:style="{ backgroundImage: `url(${program.image})` }"
@click="previewProgram(program.page)"
>
<span>{{ program.label }}</span>
</button>
</div>
<p class="preview-note">{{ t('embed.programWidget.previewNote') }}</p>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
defineOptions({
name: 'embed/programWidget',
})
const { t } = useI18n()
const previewVisible = ref(false)
const programs = [
{ page: 2, label: 'LW', image: 'https://static.gwvkyk.com/media/39bea8462f7961673f860.gif' },
{ page: 5, label: 'GE', image: 'https://static.gwvkyk.com/media/989cd0562f796ef90eb9d.gif' },
{ page: 6, label: 'DM', image: 'https://static.gwvkyk.com/media/5ab815562f7961db8a170.gif' },
{ page: 7, label: 'PK', image: 'https://static.gwvkyk.com/media/fa67bd562f79668049b3b.gif' },
]
const bannerCode = ref(`<div class='promo-container'>
<div class='lw-wrapper promo-wrapper'><button class='lw-button promo-button' onclick='programPage(2);'>Click</button></div>
<div class='ge-wrapper promo-wrapper'><button class='ge-button promo-button' onclick='programPage(5);'>Click</button></div>
<div class='dm-wrapper promo-wrapper'><button class='dm-button promo-button' onclick='programPage(6);'>Click</button></div>
<div class='pk-wrapper promo-wrapper'><button class='pk-button promo-button' onclick='programPage(7);'>Click</button></div>
</div>
<style>
.lw-button { background-image: url(https://static.gwvkyk.com/media/39bea8462f7961673f860.gif); width: 100%; height: 100%; padding: 38% 5% 7% 11%; }
.ge-button { background-image: url(https://static.gwvkyk.com/media/989cd0562f796ef90eb9d.gif); width: 100%; height: 100%; padding: 38% 5% 7% 11%; }
.dm-button { background-image: url(https://static.gwvkyk.com/media/5ab815562f7961db8a170.gif); width: 100%; height: 100%; padding: 38% 5% 7% 11%; }
.pk-button { background-image: url(https://static.gwvkyk.com/media/fa67bd562f79668049b3b.gif); width: 100%; height: 100%; padding: 38% 5% 7% 11%; }
.promo-wrapper { background-size: 100% 100%; background-repeat: no-repeat; background-position: center; background-color: transparent; margin: 10px 0; }
.promo-button { background-size: 100% 100%; background-repeat: no-repeat; background-position: center; background-color: transparent; font-size: 0; border: 0; cursor: pointer; }
</style>`)
const functionCode = ref(`<script>
function programPage(page) {
let userId = '';
let merchantId = (typeof _ !== 'undefined' && typeof _.getVar === 'function') ? (_.getVar('merchantId') ?? '') : '';
if (!merchantId) { alert('merchantId not found'); return; }
var refValue = 'jdkUserId';
if (!localStorage.getItem('USER')) { alert('Please login first!'); return; }
userId = JSON.parse(localStorage.getItem('USER')).id;
if (typeof userId == 'undefined' || userId == '') { alert('Please login first!'); return; }
if (userId === null) { alert('user id is null'); return; }
var content = refValue + '=' + userId;
var encodedContent = btoa(content);
var form = document.createElement('form');
form.setAttribute('method', 'POST');
form.setAttribute('action', 'https://1xaud.g2ma.co/p' + page + '.aspx');
var input1 = document.createElement('input');
input1.setAttribute('type', 'hidden');
input1.setAttribute('name', 'userData');
input1.setAttribute('value', encodedContent);
var input2 = document.createElement('input');
input2.setAttribute('type', 'hidden');
input2.setAttribute('name', 'clientMerchant');
input2.setAttribute('value', merchantId);
form.appendChild(input1);
form.appendChild(input2);
document.body.appendChild(form);
form.submit();
form.remove();
}
<\/script>`)
const copyCode = async (code: string) => {
try {
await navigator.clipboard.writeText(code)
ElMessage.success(t('embed.programWidget.copied'))
} catch {
ElMessage.error(t('embed.programWidget.copyFailed'))
}
}
const previewProgram = (page: number) => {
ElMessage.info(t('embed.programWidget.previewProgram', { page }))
}
</script>
<style scoped lang="scss">
.program-widget-page {
padding: var(--ba-main-space);
color: var(--el-text-color-primary);
}
.panel {
overflow: hidden;
border: 1px solid var(--el-border-color);
border-radius: 4px;
background: var(--el-bg-color);
}
.panel-title {
padding: 10px 14px;
color: #fff;
font-size: 14px;
font-weight: 700;
background: #333;
}
.code-section {
padding: 14px;
}
.function-section {
border-top: 1px solid var(--el-border-color);
}
.section-heading {
margin-bottom: 5px;
color: var(--el-color-danger);
font-weight: 700;
}
.section-help {
margin: 0 0 10px;
color: var(--el-text-color-regular);
font-size: 13px;
strong {
color: var(--el-text-color-primary);
}
}
:deep(textarea) {
min-height: 120px !important;
padding: 10px;
color: var(--el-text-color-primary);
background: var(--el-fill-color-light);
font-family: monospace;
font-size: 12px;
line-height: 1.55;
}
.section-actions {
display: flex;
gap: 8px;
margin-top: 8px;
.el-button {
margin: 0;
}
}
.preview-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.program-banner {
position: relative;
min-height: 150px;
overflow: hidden;
border: 1px solid var(--el-border-color);
border-radius: 6px;
background-color: var(--el-fill-color-light);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
cursor: pointer;
transition:
transform 0.15s ease,
box-shadow 0.15s ease;
&:hover {
box-shadow: var(--el-box-shadow-light);
transform: translateY(-2px);
}
span {
position: absolute;
right: 10px;
bottom: 10px;
padding: 3px 8px;
border-radius: 3px;
color: #fff;
background: rgb(0 0 0 / 55%);
font-size: 12px;
font-weight: 700;
}
}
.preview-note {
margin: 14px 0 0;
color: var(--el-text-color-secondary);
font-size: 12px;
}
@media (max-width: 720px) {
.program-widget-page {
padding: 12px;
}
.preview-grid {
grid-template-columns: 1fr;
}
.section-actions {
align-items: stretch;
flex-direction: column;
.el-button {
width: 100%;
}
}
}
</style>