1.新增商城参数配置菜单-管理兑换比例

This commit is contained in:
2026-05-06 15:21:59 +08:00
parent c2c2baeaec
commit ec499dce0f
31 changed files with 577 additions and 34 deletions

View File

@@ -0,0 +1,28 @@
import createAxios from '/@/utils/axios'
export const url = '/admin/mall.PlayxConfig/'
export const actionUrl = new Map([
['index', url + 'index'],
['save', url + 'save'],
])
export function index() {
return createAxios({
url: actionUrl.get('index'),
method: 'get',
})
}
export function save(data: anyObj) {
return createAxios(
{
url: actionUrl.get('save'),
method: 'post',
data,
},
{
showSuccessMessage: true,
}
)
}

View File

@@ -0,0 +1,33 @@
<template>
<el-alert class="ba-table-alert mall-page-intro" type="info" show-icon :closable="false">
<template #title>{{ title }}</template>
<div class="mall-page-intro__desc">{{ desc }}</div>
</el-alert>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps<{
/** 与 mall.pageIntro.<pageKey> 对应,如 dailyPush、userAsset */
pageKey: string
}>()
const { t } = useI18n()
const title = computed(() => t(`mall.pageIntro.${props.pageKey}.title`))
const desc = computed(() => t(`mall.pageIntro.${props.pageKey}.desc`))
</script>
<style scoped lang="scss">
.mall-page-intro {
margin-bottom: 12px;
}
.mall-page-intro__desc {
margin: 0;
line-height: 1.6;
color: var(--el-text-color-regular);
white-space: pre-line;
}
</style>

View File

@@ -1,13 +1,12 @@
export default {
id: 'id',
user_id: 'user_id',
date: 'date',
username: 'username',
yesterday_win_loss_net: 'yesterday_win_loss_net',
yesterday_total_deposit: 'yesterday_total_deposit',
lifetime_total_deposit: 'lifetime_total_deposit',
lifetime_total_withdraw: 'lifetime_total_withdraw',
create_time: 'create_time',
'quick Search Fields': 'id',
id: 'ID',
user_id: 'PlayX user ID',
date: 'Business date',
username: 'Username',
yesterday_win_loss_net: 'Yesterday net win/loss',
yesterday_total_deposit: 'Yesterday total deposit',
lifetime_total_deposit: 'Lifetime total deposit',
lifetime_total_withdraw: 'Lifetime total withdraw',
create_time: 'Created at',
'quick Search Fields': 'ID',
}

View File

@@ -0,0 +1,58 @@
export default {
userAsset: {
title: 'About this page',
desc: 'Manage mall user asset records, including PlayX binding and points fields used for claims and redemptions.',
},
address: {
title: 'About this page',
desc: 'View and manage user shipping addresses for physical orders.',
},
order: {
title: 'About this page',
desc: 'Central list of mall orders (approval, shipping, points changes) with search and admin actions.',
},
dailyPush: {
title: 'About this page',
desc: 'Daily T+1 snapshots pushed by the partner; rows are stored here and drive user-asset related updates.',
},
claimLog: {
title: 'About this page',
desc: 'History of users moving points from locked/pending to available, filterable by time and user.',
},
item: {
title: 'About this page',
desc: 'Manage catalog items, stock, and multilingual display text for the points mall.',
},
pintsOrder: {
title: 'About this page',
desc: 'Orders paid with points—use for redemption and fulfillment checks.',
},
redemptionOrder: {
title: 'About this page',
desc: 'Redemption-style orders (e.g. bonus payouts) for issuance and reconciliation.',
},
playxCenter: {
title: 'About this page',
desc: 'PlayX hub: orders, daily push, claim logs, and user assets in one place for integration and troubleshooting.',
},
playxOrder: {
title: 'About this page',
desc: 'PlayX-facing order list for cross-checking with the mall.',
},
playxDailyPush: {
title: 'About this page',
desc: 'PlayX daily push rows in admin (compare with the standalone Daily push menu when sources align).',
},
playxClaimLog: {
title: 'About this page',
desc: 'PlayX users claim history on the mall side for amount and frequency checks.',
},
playxUserAsset: {
title: 'About this page',
desc: 'PlayX user assets in list form—cross-check with the main User assets screen when needed.',
},
playxConfig: {
title: 'About this page',
desc: 'Edit return/unlock/points-to-cash ratios used by daily push and asset APIs; values are stored in mall_config and apply immediately after save.',
},
}

View File

@@ -0,0 +1,8 @@
export default {
return_ratio: 'Return ratio (return_ratio)',
return_ratio_tip: 'In daily push, when yesterday net win/loss is negative: locked increment = |net| × this ratio.',
unlock_ratio: 'Unlock ratio (unlock_ratio)',
unlock_ratio_tip: 'In daily push: daily claim cap = yesterday total deposit × this ratio.',
points_to_cash_ratio: 'Points to cash ratio',
points_to_cash_ratio_tip: 'For withdrawable cash display: cash ≈ available points × this ratio.',
}

View File

@@ -115,5 +115,8 @@ export default {
mall_playxUserAsset: 'playX user assets',
mall_pintsOrder: 'Points orders',
mall_redemptionOrder: 'Redemption orders',
mall_playxConfig: 'Mall parameters',
mall_playxConfig_index: 'View',
mall_playxConfig_save: 'Save',
},
}

View File

@@ -0,0 +1,58 @@
export default {
userAsset: {
title: '页面说明',
desc: '维护积分商城用户资产主数据,含 PlayX 用户绑定、积分字段等,用于前台领取与兑换鉴权。',
},
address: {
title: '页面说明',
desc: '查看与管理用户收货地址,供实物类订单发货使用。',
},
order: {
title: '页面说明',
desc: '集中查看商城订单(含审核、发货、积分变动等),支持按条件检索与后台操作。',
},
dailyPush: {
title: '页面说明',
desc: '展示合作方 T+1 每日推送的经营数据快照;接口写入后在此留痕,并用于同步用户资产相关字段。',
},
claimLog: {
title: '页面说明',
desc: '用户从待领取积分划转至可用积分的操作流水,可按时间与用户追溯。',
},
item: {
title: '页面说明',
desc: '维护积分商城上架商品、库存与多语言展示信息。',
},
pintsOrder: {
title: '页面说明',
desc: '以积分支付的订单列表,用于核对兑换与发货状态。',
},
redemptionOrder: {
title: '页面说明',
desc: '兑换类业务订单(如礼金等)列表,用于核对发放与对账。',
},
playxCenter: {
title: '页面说明',
desc: 'PlayX 相关能力的聚合入口:订单、每日推送、领取记录与用户资产分栏查看,便于联调与排障。',
},
playxOrder: {
title: '页面说明',
desc: '从 PlayX 视角查看与商城关联的订单数据,用于对接核对。',
},
playxDailyPush: {
title: '页面说明',
desc: 'PlayX 每日推送记录在后台的列表视图(与独立「每日推送」菜单数据源一致时可对照查看)。',
},
playxClaimLog: {
title: '页面说明',
desc: 'PlayX 用户在商城侧的领取流水,用于核对领取额度与频次。',
},
playxUserAsset: {
title: '页面说明',
desc: 'PlayX 用户资产在后台的列表视图,可与「用户资产」主菜单交叉核对。',
},
playxConfig: {
title: '页面说明',
desc: '配置每日推送用到的返还比例、解锁比例及积分折算现金比例保存后立即作用于接口逻辑mall_config。',
},
}

View File

@@ -0,0 +1,8 @@
export default {
return_ratio: '返还比例return_ratio',
return_ratio_tip: '每日推送中,仅当昨日净输赢为负时:待领取增量 = |净输赢| × 本比例。',
unlock_ratio: '解锁比例unlock_ratio',
unlock_ratio_tip: '每日推送中:今日可领取上限 = 昨日总充值 × 本比例。',
points_to_cash_ratio: '积分折算现金比例',
points_to_cash_ratio_tip: '用于资产接口中「可提现现金」等展示:现金 ≈ 可用积分 × 本比例。',
}

View File

@@ -116,5 +116,8 @@ export default {
mall_playxUserAsset: 'playX用户资产',
mall_pintsOrder: '积分订单',
mall_redemptionOrder: '兑换订单',
mall_playxConfig: '商城参数配置',
mall_playxConfig_index: '查看',
mall_playxConfig_save: '保存',
},
}

View File

@@ -53,8 +53,11 @@ export async function loadLang(app: App) {
*/
if (locale == 'zh-cn') {
assignLocale[locale].push(getLangFileMessage(import.meta.glob('./common/zh-cn/**/*.ts', { eager: true }), locale))
// 积分商城后台页表格列在 setup 阶段即 t(),若仅依赖路由懒加载会出现列头显示为 key
assignLocale[locale].push(getLangFileMessage(import.meta.glob('./backend/zh-cn/mall/**/*.ts', { eager: true }), locale))
} else if (locale == 'en') {
assignLocale[locale].push(getLangFileMessage(import.meta.glob('./common/en/**/*.ts', { eager: true }), locale))
assignLocale[locale].push(getLangFileMessage(import.meta.glob('./backend/en/mall/**/*.ts', { eager: true }), locale))
}
const messages = {

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="address" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<!-- 表格顶部菜单 -->
@@ -25,6 +26,7 @@ import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
import { defaultOptButtons } from '/@/components/table'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="claimLog" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
@@ -15,6 +16,7 @@
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="dailyPush" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
@@ -15,6 +16,7 @@
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="item" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<!-- 表格顶部菜单 -->
@@ -25,6 +26,7 @@ import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
import { defaultOptButtons } from '/@/components/table'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="order" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
@@ -18,6 +19,7 @@ import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
import createAxios from '/@/utils/axios'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="pintsOrder" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<!-- 表格顶部菜单 -->
@@ -25,6 +26,7 @@ import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
import { defaultOptButtons } from '/@/components/table'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'

View File

@@ -2,12 +2,7 @@
<div class="default-main">
<!-- 语言包注入是异步的 t() 调用和子组件渲染都延后到注入完成后避免 intlify Not found -->
<template v-if="langReady">
<el-card shadow="never" class="mb-4">
<template #header>
<div class="card-header">{{ t('mall.playxCenter.title') }}</div>
</template>
<div class="desc">{{ t('mall.playxCenter.desc') }}</div>
</el-card>
<MallPageIntro page-key="playxCenter" class="mb-4" />
<el-tabs v-model="activeName" type="border-card">
<el-tab-pane :label="t('mall.playxCenter.orders')" name="orders">
@@ -34,6 +29,7 @@ import PlayxOrderIndex from '/@/views/backend/mall/playxOrder/index.vue'
import PlayxDailyPushIndex from '/@/views/backend/mall/playxDailyPush/index.vue'
import PlayxClaimLogIndex from '/@/views/backend/mall/playxClaimLog/index.vue'
import PlayxUserAssetIndex from '/@/views/backend/mall/playxUserAsset/index.vue'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import { useConfig } from '/@/stores/config'
import { mergeMessage } from '/@/lang/index'
@@ -92,11 +88,5 @@ onMounted(async () => {
.mb-4 {
margin-bottom: 16px;
}
.card-header {
font-weight: 600;
}
.desc {
color: var(--el-text-color-secondary);
}
</style>

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="playxClaimLog" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
@@ -15,6 +16,7 @@
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'

View File

@@ -0,0 +1,144 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="playxConfig" />
<el-alert class="ba-table-alert" v-if="remark" :title="remark" type="info" show-icon />
<el-card shadow="never" v-loading="loading">
<el-form
v-if="!loading"
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
@submit.prevent=""
@keyup.enter="onSubmit()"
>
<el-form-item :label="t('mall.playxConfig.return_ratio')" prop="return_ratio">
<el-input-number
v-model="form.return_ratio"
:min="0"
:max="100"
:step="0.01"
:precision="2"
controls-position="right"
class="w100"
/>
<div class="form-tip">{{ t('mall.playxConfig.return_ratio_tip') }}</div>
</el-form-item>
<el-form-item :label="t('mall.playxConfig.unlock_ratio')" prop="unlock_ratio">
<el-input-number
v-model="form.unlock_ratio"
:min="0"
:max="100"
:step="0.01"
:precision="2"
controls-position="right"
class="w100"
/>
<div class="form-tip">{{ t('mall.playxConfig.unlock_ratio_tip') }}</div>
</el-form-item>
<el-form-item :label="t('mall.playxConfig.points_to_cash_ratio')" prop="points_to_cash_ratio">
<el-input-number
v-model="form.points_to_cash_ratio"
:min="0"
:max="100"
:step="0.01"
:precision="2"
controls-position="right"
class="w100"
/>
<div class="form-tip">{{ t('mall.playxConfig.points_to_cash_ratio_tip') }}</div>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="submitting" @click="onSubmit()">{{ t('Save') }}</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { onMounted, reactive, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { index, save } from '/@/api/backend/mall/playxConfig'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
defineOptions({
name: 'mall/playxConfig',
})
const { t } = useI18n()
const formRef = useTemplateRef<FormInstance>('formRef')
const loading = ref(true)
const submitting = ref(false)
const remark = ref('')
const form = reactive({
return_ratio: 0.1,
unlock_ratio: 0.1,
points_to_cash_ratio: 0.1,
})
const rules: FormRules = {
return_ratio: [{ required: true, message: t('Please input field', { field: t('mall.playxConfig.return_ratio') }) }],
unlock_ratio: [{ required: true, message: t('Please input field', { field: t('mall.playxConfig.unlock_ratio') }) }],
points_to_cash_ratio: [{ required: true, message: t('Please input field', { field: t('mall.playxConfig.points_to_cash_ratio') }) }],
}
const loadData = () => {
loading.value = true
index()
.then((res) => {
remark.value = res.data.remark ?? ''
const row = res.data.row ?? {}
if (row.return_ratio !== undefined && row.return_ratio !== null) {
form.return_ratio = parseFloat(String(row.return_ratio))
}
if (row.unlock_ratio !== undefined && row.unlock_ratio !== null) {
form.unlock_ratio = parseFloat(String(row.unlock_ratio))
}
if (row.points_to_cash_ratio !== undefined && row.points_to_cash_ratio !== null) {
form.points_to_cash_ratio = parseFloat(String(row.points_to_cash_ratio))
}
})
.finally(() => {
loading.value = false
})
}
const onSubmit = () => {
formRef.value?.validate((valid) => {
if (!valid) return
submitting.value = true
save({
return_ratio: form.return_ratio,
unlock_ratio: form.unlock_ratio,
points_to_cash_ratio: form.points_to_cash_ratio,
})
.then(() => {
loadData()
})
.finally(() => {
submitting.value = false
})
})
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.w100 {
width: 100%;
}
.form-tip {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.5;
}
</style>

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="playxDailyPush" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
@@ -15,6 +16,7 @@
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="playxOrder" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
@@ -15,6 +16,7 @@
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import createAxios from '/@/utils/axios'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="playxUserAsset" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
@@ -15,6 +16,7 @@
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { baTableApi } from '/@/api/common'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="redemptionOrder" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<!-- 表格顶部菜单 -->
@@ -25,6 +26,7 @@ import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
import { defaultOptButtons } from '/@/components/table'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'

View File

@@ -1,5 +1,6 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="userAsset" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
@@ -19,6 +20,7 @@ import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
import { defaultOptButtons } from '/@/components/table'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'