feat(user): 添加用户钱包功能和成员推广弹窗

This commit is contained in:
JiaJun
2026-04-16 17:13:09 +08:00
parent 5c33491ae2
commit 7f3a7c34f0
4 changed files with 287 additions and 5 deletions

View File

@@ -5,4 +5,5 @@ ENV = 'development'
VITE_BASE_PATH = './' VITE_BASE_PATH = './'
# 本地环境接口地址 - 尾部无需带'/' # 本地环境接口地址 - 尾部无需带'/'
VITE_AXIOS_BASE_URL = 'http://localhost:8000' #VITE_AXIOS_BASE_URL = 'http://localhost:8000'
VITE_AXIOS_BASE_URL = 'http://192.168.0.46:8000'

View File

@@ -0,0 +1,26 @@
import createAxios from '/@/utils/axios'
export const url = '/admin/user.User/'
export function getWallet(id: string | number) {
return createAxios({
url: url + 'wallet',
method: 'get',
params: {
id,
},
})
}
export function postWallet(id: string | number, score: string | number) {
const formData = new FormData()
formData.append('id', String(id))
formData.append('score', String(score))
return createAxios({
url: url + 'wallet',
method: 'post',
data: formData,
}, {
showSuccessMessage: true,
})
}

View File

@@ -15,13 +15,16 @@
<!-- 表单 --> <!-- 表单 -->
<PopupForm /> <PopupForm />
<MembersPopupForm v-model="customFormVisible" :row="customFormRow" :wallet-data="customFormData" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { provide } from 'vue' import { provide, ref } from 'vue'
import { getWallet } from '/@/api/backend/user/wallet'
import baTableClass from '/@/utils/baTable' import baTableClass from '/@/utils/baTable'
import PopupForm from './popupForm.vue' import PopupForm from './popupForm.vue'
import MembersPopupForm from './membersPopupForm.vue'
import Table from '/@/components/table/index.vue' import Table from '/@/components/table/index.vue'
import TableHeader from '/@/components/table/header/index.vue' import TableHeader from '/@/components/table/header/index.vue'
import { defaultOptButtons } from '/@/components/table' import { defaultOptButtons } from '/@/components/table'
@@ -33,6 +36,42 @@ defineOptions({
}) })
const { t } = useI18n() const { t } = useI18n()
const customFormVisible = ref(false)
const customFormRow = ref<TableRow | null>(null)
const customFormData = ref<anyObj | null>(null)
const walletLoadingId = ref<number | string | null>(null)
let walletRequestSeq = 0
const optButtons: OptButton[] = [
{
render: 'tipButton',
name: 'customAction',
title: 'Members Promotion',
text: '',
type: 'primary',
icon: 'el-icon-EditPen',
class: 'table-row-custom',
disabledTip: false,
loading: (row: TableRow) => walletLoadingId.value === row.id,
click: async (row: TableRow) => {
const requestSeq = ++walletRequestSeq
walletLoadingId.value = row.id
try {
const res = await getWallet(row.id)
if (requestSeq !== walletRequestSeq) return
customFormRow.value = { ...row }
customFormData.value = res.data?.row ?? res.data ?? {}
customFormVisible.value = true
} finally {
if (requestSeq === walletRequestSeq) {
walletLoadingId.value = null
}
}
},
},
...defaultOptButtons(['edit', 'delete']),
]
const baTable = new baTableClass( const baTable = new baTableClass(
new baTableApi('/admin/user.User/'), new baTableApi('/admin/user.User/'),
{ {
@@ -90,11 +129,11 @@ const baTable = new baTableClass(
{ {
label: t('Operate'), label: t('Operate'),
align: 'center', align: 'center',
width: '100', width: '160',
render: 'buttons', render: 'buttons',
buttons: defaultOptButtons(['edit', 'delete']), buttons: optButtons,
operator: false, operator: false,
}, }
], ],
dblClickNotEditColumn: [undefined], dblClickNotEditColumn: [undefined],
}, },

View File

@@ -0,0 +1,216 @@
<template>
<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :model-value="props.modelValue" @close="closeForm">
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
{{ t('Members Promotion') }}
</div>
</template>
<el-scrollbar class="ba-table-form-scrollbar">
<div class="ba-operate-form ba-edit-form" :style="config.layout.shrink ? '' : 'width: calc(100% - ' + state.labelWidth / 2 + 'px)'">
<el-form
ref="formRef"
:model="state.form"
:rules="rules"
:label-position="config.layout.shrink ? 'top' : 'right'"
:label-width="state.labelWidth"
@keyup.enter="onSubmit"
>
<el-form-item prop="gameType" label="">
<el-radio-group v-model="state.form.gameType">
<el-radio-button v-for="item in miniGameOptions" :key="item.value" :value="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
</el-form-item>
<div class="yellow-bg">Click Here Go To Member Program Page</div>
<template v-if="hasCurrentScore">
<el-form-item label="Approved Bonus:">
<div class="readonly-value">{{ approvedBonusText }}</div>
</el-form-item>
<el-form-item label="Rejected Bonus:">
<div class="readonly-value">{{ rejectedBonusText }}</div>
</el-form-item>
<el-form-item label="Unsubmitted Reward:">
<div class="readonly-value">{{ unsubmittedRewardText }}</div>
</el-form-item>
<el-form-item prop="currentSpins" label="Current Spins:">
<el-input-number v-model="state.form.currentSpins" :min="0" class="spins-input" />
</el-form-item>
</template>
<el-empty v-else description="No score data for this game" :image-size="90" />
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - ' + state.labelWidth / 1.8 + 'px)'">
<el-button @click="closeForm">{{ t('Cancel') }}</el-button>
<el-button v-blur :loading="state.submitLoading" :disabled="!hasCurrentScore" type="primary" @click="onSubmit">
{{ t('Save') }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, reactive, useTemplateRef, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { postWallet } from '/@/api/backend/user/wallet'
import { useConfig } from '/@/stores/config'
import { useI18n } from 'vue-i18n'
interface Props {
modelValue: boolean
row?: TableRow | null
walletData?: anyObj | null
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
row: null,
walletData: null,
})
const emits = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const config = useConfig()
const { t } = useI18n()
const formRef = useTemplateRef<FormInstance>('formRef')
const state = reactive({
labelWidth: 120,
submitLoading: false,
form: {
gameType: '',
scoreId: 0,
approvedBonus: 0,
rejectedBonus: 0,
unsubmittedReward: 0,
currentSpins: 0,
},
})
const miniGameOptions = computed(() => {
const miniGame = props.walletData?.mini_game ?? {}
return Object.entries(miniGame).map(([value, label]) => ({
value,
label: String(label),
}))
})
const currentScore = computed(() => {
const scoreList = Array.isArray(props.walletData?.score) ? props.walletData.score : []
return scoreList.find((item: anyObj) => String(item.game_type) === String(state.form.gameType))
})
const hasCurrentScore = computed(() => Boolean(currentScore.value))
const approvedBonusText = computed(() => Number(state.form.approvedBonus || 0).toFixed(2))
const rejectedBonusText = computed(() => Number(state.form.rejectedBonus || 0).toFixed(2))
const unsubmittedRewardText = computed(() => `$${formatCurrencyValue(state.form.unsubmittedReward)}`)
const rules = reactive<FormRules>({
currentSpins: [
{
validator: (_rule, value, callback) => {
if (value === null || value === undefined || value === '') {
return callback(new Error('请输入 Current Spins'))
}
const numericValue = Number(value)
if (Number.isNaN(numericValue)) {
return callback(new Error('Current Spins 必须是数字'))
}
if (numericValue < 0) {
return callback(new Error('Current Spins 不能小于 0'))
}
return callback()
},
trigger: ['blur', 'change'],
},
],
})
const closeForm = () => {
emits('update:modelValue', false)
}
const onSubmit = () => {
if (!hasCurrentScore.value) return
formRef.value?.validate((valid) => {
if (!valid) return
state.submitLoading = true
postWallet(state.form.scoreId, state.form.currentSpins)
.then(() => {
closeForm()
})
.finally(() => {
state.submitLoading = false
})
})
}
const formatCurrencyValue = (value: number | string) => {
const amount = Number(value || 0)
return Number.isInteger(amount) ? String(amount) : amount.toFixed(2)
}
const syncScoreForm = (gameType: string) => {
const scoreList = Array.isArray(props.walletData?.score) ? props.walletData.score : []
const scoreItem = scoreList.find((item: anyObj) => String(item.game_type) === String(gameType))
state.form.scoreId = Number(scoreItem?.id ?? 0)
state.form.approvedBonus = Number(scoreItem?.approved_score ?? 0)
state.form.rejectedBonus = Number(scoreItem?.rejected_score ?? 0)
state.form.unsubmittedReward = Number(scoreItem?.unsubmitted_score ?? 0)
state.form.currentSpins = Number(scoreItem?.score ?? 0)
}
watch(
() => props.modelValue,
(visible) => {
if (!visible) return
state.form.gameType = miniGameOptions.value[0]?.value ?? ''
syncScoreForm(state.form.gameType)
},
{ immediate: true }
)
watch(
() => state.form.gameType,
(gameType) => {
syncScoreForm(gameType)
}
)
</script>
<style scoped lang="scss">
:deep(.el-form-item__label) {
white-space: nowrap;
margin-left: 30px;
}
.yellow-bg {
width: 100%;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
background: #feb902;
margin: 10px 0 20px 0;
border-radius: 5px;
}
.readonly-value {
width: 100%;
color: var(--el-text-color-primary);
font-weight: 600;
line-height: 32px;
}
.spins-input {
width: 220px;
}
</style>