feat(user): 添加用户钱包功能和成员推广弹窗
This commit is contained in:
@@ -5,4 +5,5 @@ ENV = 'development'
|
||||
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'
|
||||
|
||||
26
web/src/api/backend/user/wallet.ts
Normal file
26
web/src/api/backend/user/wallet.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -15,13 +15,16 @@
|
||||
|
||||
<!-- 表单 -->
|
||||
<PopupForm />
|
||||
<MembersPopupForm v-model="customFormVisible" :row="customFormRow" :wallet-data="customFormData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 PopupForm from './popupForm.vue'
|
||||
import MembersPopupForm from './membersPopupForm.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
@@ -33,6 +36,42 @@ defineOptions({
|
||||
})
|
||||
|
||||
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(
|
||||
new baTableApi('/admin/user.User/'),
|
||||
{
|
||||
@@ -90,11 +129,11 @@ const baTable = new baTableClass(
|
||||
{
|
||||
label: t('Operate'),
|
||||
align: 'center',
|
||||
width: '100',
|
||||
width: '160',
|
||||
render: 'buttons',
|
||||
buttons: defaultOptButtons(['edit', 'delete']),
|
||||
buttons: optButtons,
|
||||
operator: false,
|
||||
},
|
||||
}
|
||||
],
|
||||
dblClickNotEditColumn: [undefined],
|
||||
},
|
||||
|
||||
216
web/src/views/backend/user/user/membersPopupForm.vue
Normal file
216
web/src/views/backend/user/user/membersPopupForm.vue
Normal 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>
|
||||
Reference in New Issue
Block a user