1.优化管理员提现记录审核为一个操作
2.修复创建玩家报错“参数%s不能为空” 3.修复玩家登录报错
This commit is contained in:
@@ -15,10 +15,15 @@ export default {
|
||||
review_admin_username: 'Reviewer',
|
||||
remark: 'Remark',
|
||||
create_time: 'Create time',
|
||||
review_btn: 'Review',
|
||||
review_title: 'Withdraw review',
|
||||
review_btn_approve: 'Approve',
|
||||
review_btn_reject: 'Reject',
|
||||
review_btn_back: 'Back',
|
||||
review_btn_confirm_reject: 'Confirm reject',
|
||||
review_approve_title: 'Approve order',
|
||||
review_reject_title: 'Reject order',
|
||||
review_reject_tip: 'Rejected orders will unfreeze the amount back to the admin wallet',
|
||||
review_remark_optional: 'Optional review remark',
|
||||
reject_reason_required: 'Please enter reject reason',
|
||||
stat_total_count: 'Total orders',
|
||||
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
section_admin_attribution: 'Administrator',
|
||||
admin_affiliation: 'Assigned admin',
|
||||
admin_affiliation_placeholder: 'Role group tree — only admins in your scope',
|
||||
admin_no_channel_bound: 'The selected admin is not bound to a channel; bind a channel in Admin management before creating users',
|
||||
register_invite_code_auto_placeholder: 'Filled from selected admin invite code',
|
||||
channel_id: 'Channel',
|
||||
channel__name: 'Channel',
|
||||
|
||||
@@ -15,10 +15,15 @@ export default {
|
||||
review_admin_username: '审核人',
|
||||
remark: '备注',
|
||||
create_time: '创建时间',
|
||||
review_btn: '审核',
|
||||
review_title: '提现审核',
|
||||
review_btn_approve: '通过',
|
||||
review_btn_reject: '拒绝',
|
||||
review_btn_back: '返回',
|
||||
review_btn_confirm_reject: '确认拒绝',
|
||||
review_approve_title: '通过审核',
|
||||
review_reject_title: '拒绝审核',
|
||||
review_reject_tip: '拒绝后冻结金额将退回管理员钱包',
|
||||
review_remark_optional: '可选填写审核备注',
|
||||
reject_reason_required: '请填写拒绝原因',
|
||||
stat_total_count: '提现总单数',
|
||||
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
section_admin_attribution: '管理员归属',
|
||||
admin_affiliation: '归属管理员',
|
||||
admin_affiliation_placeholder: '按角色组展开,仅展示您可管理范围内的管理员',
|
||||
admin_no_channel_bound: '所选归属管理员未绑定渠道,无法创建用户,请先在管理员管理中绑定渠道',
|
||||
register_invite_code_auto_placeholder: '随所选管理员邀请码自动带出',
|
||||
channel_id: '所属渠道',
|
||||
channel__name: '渠道名',
|
||||
|
||||
@@ -38,18 +38,19 @@
|
||||
</el-select>
|
||||
</div>
|
||||
<Table ref="tableRef"></Table>
|
||||
<PopupForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, reactive, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import PopupForm from './popupForm.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'order/adminWithdrawOrder',
|
||||
@@ -69,50 +70,21 @@ const stats = reactive({
|
||||
approved_amount: '0.00',
|
||||
})
|
||||
|
||||
const onReview = async (row: anyObj, action: 'approve' | 'reject') => {
|
||||
const needReason = action === 'reject'
|
||||
const { value } = await ElMessageBox.prompt(
|
||||
needReason ? t('order.adminWithdrawOrder.reject_reason_required') : t('order.adminWithdrawOrder.review_remark_optional'),
|
||||
action === 'approve' ? t('order.adminWithdrawOrder.review_approve_title') : t('order.adminWithdrawOrder.review_reject_title'),
|
||||
{
|
||||
confirmButtonText: t('Confirm'),
|
||||
cancelButtonText: t('Cancel'),
|
||||
inputPlaceholder: needReason ? t('order.adminWithdrawOrder.reject_reason_required') : t('order.adminWithdrawOrder.review_remark_optional'),
|
||||
inputPattern: needReason ? /.+/ : undefined,
|
||||
inputErrorMessage: needReason ? t('order.adminWithdrawOrder.reject_reason_required') : '',
|
||||
})
|
||||
await createAxios(
|
||||
{
|
||||
url: `/admin/order.AdminWithdrawOrder/${action}`,
|
||||
method: 'post',
|
||||
data: { id: row.id, remark: value || '' },
|
||||
},
|
||||
{ showSuccessMessage: true }
|
||||
)
|
||||
await loadStats()
|
||||
baTable.getData()
|
||||
}
|
||||
|
||||
const optButtons: OptButton[] = [
|
||||
{
|
||||
render: 'tipButton',
|
||||
name: 'approve',
|
||||
title: 'order.adminWithdrawOrder.review_btn_approve',
|
||||
name: 'review',
|
||||
title: 'order.adminWithdrawOrder.review_btn',
|
||||
text: '',
|
||||
type: 'success',
|
||||
icon: 'el-icon-Check',
|
||||
type: 'warning',
|
||||
icon: 'fa fa-check-square-o',
|
||||
display: (row: TableRow) => Number(row.status) === 0 && Number((row as anyObj).can_review ?? 0) === 1,
|
||||
click: (row: TableRow) => void onReview(row as anyObj, 'approve'),
|
||||
},
|
||||
{
|
||||
render: 'tipButton',
|
||||
name: 'reject',
|
||||
title: 'order.adminWithdrawOrder.review_btn_reject',
|
||||
text: '',
|
||||
type: 'danger',
|
||||
icon: 'el-icon-Close',
|
||||
display: (row: TableRow) => Number(row.status) === 0 && Number((row as anyObj).can_review ?? 0) === 1,
|
||||
click: (row: TableRow) => void onReview(row as anyObj, 'reject'),
|
||||
click: (row: TableRow) => {
|
||||
baTable.form.operate = 'Review'
|
||||
baTable.form.operateIds = [String(row[baTable.table.pk!])]
|
||||
baTable.form.items = { ...(row as anyObj) }
|
||||
baTable.form.loading = false
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -179,7 +151,9 @@ const baTable = new baTableClass(
|
||||
{ label: t('Operate'), align: 'center', width: 90, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
],
|
||||
},
|
||||
{}
|
||||
{
|
||||
defaultItems: {},
|
||||
}
|
||||
)
|
||||
|
||||
const onStatusFilterChange = () => {
|
||||
@@ -274,4 +248,3 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
293
web/src/views/backend/order/adminWithdrawOrder/popupForm.vue
Normal file
293
web/src/views/backend/order/adminWithdrawOrder/popupForm.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="ba-operate-dialog admin-withdraw-review-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:model-value="isOpen"
|
||||
width="560px"
|
||||
@close="onDialogClose"
|
||||
>
|
||||
<template #header>
|
||||
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
|
||||
{{ step === 'reject' ? t('order.adminWithdrawOrder.review_reject_title') : t('order.adminWithdrawOrder.review_title') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-scrollbar v-loading="loading" class="ba-table-form-scrollbar">
|
||||
<div class="ba-operate-form ba-edit-form">
|
||||
<el-form
|
||||
v-if="!loading && step === 'review'"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="(baTable.form.labelWidth ?? 120) + 'px'"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.order_no')">
|
||||
<el-input v-model="form.order_no" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.admin_username')">
|
||||
<el-input :model-value="form.admin_text" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.channel_name')">
|
||||
<el-input :model-value="form.channel_text" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.amount')">
|
||||
<el-input :model-value="form.amount_text" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.actual_amount')">
|
||||
<el-input :model-value="form.actual_amount_text" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.receive_type')">
|
||||
<el-input :model-value="form.receive_type_text" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.receive_account')">
|
||||
<el-input :model-value="form.receive_account" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.remark')">
|
||||
<el-input :model-value="form.remark" type="textarea" :rows="2" readonly />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-form
|
||||
v-if="!loading && step === 'reject'"
|
||||
ref="rejectFormRef"
|
||||
:model="rejectForm"
|
||||
:rules="rejectRules"
|
||||
:label-position="config.layout.shrink ? 'top' : 'right'"
|
||||
:label-width="(baTable.form.labelWidth ?? 120) + 'px'"
|
||||
@submit.prevent=""
|
||||
>
|
||||
<el-alert
|
||||
class="review-reject-hint"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
:title="t('order.adminWithdrawOrder.review_reject_tip')"
|
||||
/>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.order_no')">
|
||||
<el-input v-model="form.order_no" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('order.adminWithdrawOrder.remark')" prop="remark">
|
||||
<el-input
|
||||
v-model="rejectForm.remark"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
:placeholder="t('order.adminWithdrawOrder.reject_reason_required')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<template #footer>
|
||||
<div class="review-footer">
|
||||
<template v-if="step === 'review'">
|
||||
<el-button @click="onDialogClose">{{ t('Cancel') }}</el-button>
|
||||
<el-button type="danger" :loading="submitting" @click="gotoReject">{{ t('order.adminWithdrawOrder.review_btn_reject') }}</el-button>
|
||||
<el-button type="primary" v-blur :loading="submitting" @click="submitApprove">{{ t('order.adminWithdrawOrder.review_btn_approve') }}</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="backToReview">{{ t('order.adminWithdrawOrder.review_btn_back') }}</el-button>
|
||||
<el-button type="danger" v-blur :loading="submitting" @click="submitReject">{{ t('order.adminWithdrawOrder.review_btn_confirm_reject') }}</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormItemRule } from 'element-plus'
|
||||
import { computed, inject, reactive, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import createAxios from '/@/utils/axios'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
|
||||
const config = useConfig()
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
const { t } = useI18n()
|
||||
|
||||
const rejectFormRef = useTemplateRef<FormInstance>('rejectFormRef')
|
||||
|
||||
type Step = 'review' | 'reject'
|
||||
const step = ref<Step>('review')
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
id: 0,
|
||||
order_no: '',
|
||||
admin_text: '-',
|
||||
channel_text: '-',
|
||||
amount_text: '0.00',
|
||||
actual_amount_text: '0.00',
|
||||
receive_type_text: '-',
|
||||
receive_account: '-',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const rejectForm = reactive({
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const receiveTypeLabels: Record<string, string> = {
|
||||
bank: 'order.adminWithdrawOrder.receive_type_bank',
|
||||
ewallet: 'order.adminWithdrawOrder.receive_type_ewallet',
|
||||
crypto: 'order.adminWithdrawOrder.receive_type_crypto',
|
||||
}
|
||||
|
||||
const isOpen = computed(() => ['Review'].includes(baTable.form.operate ?? ''))
|
||||
|
||||
watch(isOpen, (visible) => {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
step.value = 'review'
|
||||
rejectForm.remark = ''
|
||||
})
|
||||
|
||||
watch(
|
||||
() => ({ visible: isOpen.value, loadingState: baTable.form.loading, items: baTable.form.items }),
|
||||
({ visible, loadingState }) => {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
loading.value = loadingState === true
|
||||
if (loadingState) {
|
||||
return
|
||||
}
|
||||
hydrate()
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
const hydrate = () => {
|
||||
const row = baTable.form.items as Record<string, unknown> | undefined
|
||||
if (!row || !row['id']) {
|
||||
return
|
||||
}
|
||||
form.id = Number(row['id'] ?? 0)
|
||||
form.order_no = String(row['order_no'] ?? '')
|
||||
form.amount_text = formatAmount(row['amount'])
|
||||
form.actual_amount_text = formatAmount(row['actual_amount'])
|
||||
form.receive_account = String(row['receive_account'] ?? '-')
|
||||
form.remark = String(row['remark'] ?? '')
|
||||
const receiveType = String(row['receive_type'] ?? '')
|
||||
const receiveKey = receiveTypeLabels[receiveType]
|
||||
form.receive_type_text = receiveKey ? t(receiveKey) : receiveType || '-'
|
||||
form.admin_text = resolveRelationText(row, 'admin', row['admin_id'])
|
||||
form.channel_text = resolveRelationText(row, 'channel', row['channel_id'])
|
||||
}
|
||||
|
||||
const rejectRules: Record<string, FormItemRule[]> = {
|
||||
remark: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_r, value, cb) => {
|
||||
const text = typeof value === 'string' ? value.trim() : ''
|
||||
if (text === '') {
|
||||
cb(new Error(t('order.adminWithdrawOrder.reject_reason_required')))
|
||||
return
|
||||
}
|
||||
cb()
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const onDialogClose = () => {
|
||||
if (submitting.value) {
|
||||
return
|
||||
}
|
||||
step.value = 'review'
|
||||
baTable.toggleForm()
|
||||
}
|
||||
|
||||
const gotoReject = () => {
|
||||
step.value = 'reject'
|
||||
rejectForm.remark = ''
|
||||
}
|
||||
|
||||
const backToReview = () => {
|
||||
step.value = 'review'
|
||||
}
|
||||
|
||||
const submitReview = async (action: 'approve' | 'reject', remark: string) => {
|
||||
submitting.value = true
|
||||
try {
|
||||
await createAxios(
|
||||
{
|
||||
url: '/admin/order.AdminWithdrawOrder/review',
|
||||
method: 'post',
|
||||
data: { id: form.id, action, remark },
|
||||
},
|
||||
{ showSuccessMessage: true }
|
||||
)
|
||||
baTable.onTableHeaderAction('refresh', {})
|
||||
baTable.toggleForm()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const submitApprove = async () => {
|
||||
await submitReview('approve', '')
|
||||
}
|
||||
|
||||
const submitReject = async () => {
|
||||
const formEl = rejectFormRef.value
|
||||
if (!formEl) {
|
||||
return
|
||||
}
|
||||
const valid = await formEl.validate().catch(() => false)
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
await submitReview('reject', rejectForm.remark.trim())
|
||||
}
|
||||
|
||||
function formatAmount(raw: unknown): string {
|
||||
if (raw === null || raw === undefined || raw === '') {
|
||||
return '0.00'
|
||||
}
|
||||
const n = Number(raw)
|
||||
if (!Number.isFinite(n)) {
|
||||
return String(raw)
|
||||
}
|
||||
return n.toFixed(2)
|
||||
}
|
||||
|
||||
function resolveRelationText(row: Record<string, unknown>, relationKey: string, fallbackId: unknown): string {
|
||||
const rel = row[relationKey]
|
||||
if (rel && typeof rel === 'object') {
|
||||
const r = rel as Record<string, unknown>
|
||||
const name = r['username'] ?? r['name']
|
||||
if (typeof name === 'string' && name !== '') {
|
||||
return name
|
||||
}
|
||||
}
|
||||
if (fallbackId === null || fallbackId === undefined || fallbackId === '') {
|
||||
return '-'
|
||||
}
|
||||
return 'ID: ' + String(fallbackId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-withdraw-review-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.review-reject-hint {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.review-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -277,7 +277,6 @@ function buildAdminMapsFromTree(nodes: TreeNode[]) {
|
||||
return { mapCh, mapInv }
|
||||
}
|
||||
|
||||
/** 鏄犲皠鏈懡涓椂浠庡師濮嬫爲鏌ユ壘锛堥槻姝?props 瑁佸壀鎴栧紓姝ユ椂搴忛棶棰橈級 */
|
||||
function findAdminMetaInTree(nodes: TreeNode[], adminId: string): { channel_id?: number; invite_code: string } | null {
|
||||
const target = String(adminId).trim()
|
||||
for (const n of nodes) {
|
||||
@@ -304,6 +303,28 @@ function findAdminMetaInTree(nodes: TreeNode[], adminId: string): { channel_id?:
|
||||
return null
|
||||
}
|
||||
|
||||
function resolveAdminChannelId(adminId: string | number | null | undefined): number | undefined {
|
||||
if (adminId === undefined || adminId === null || adminId === '') {
|
||||
return undefined
|
||||
}
|
||||
const key = typeof adminId === 'number' ? String(adminId) : String(adminId).trim()
|
||||
if (key === '') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let channelId = adminIdToChannelId.value[key]
|
||||
if (channelId === undefined) {
|
||||
const meta = findAdminMetaInTree(adminScopeTree.value, key)
|
||||
if (meta?.channel_id !== undefined) {
|
||||
channelId = meta.channel_id
|
||||
}
|
||||
}
|
||||
if (channelId === undefined || channelId === null || Number.isNaN(Number(channelId)) || Number(channelId) <= 0) {
|
||||
return undefined
|
||||
}
|
||||
return typeof channelId === 'number' ? channelId : parseInt(String(channelId), 10)
|
||||
}
|
||||
|
||||
const loadAdminScopeTree = async () => {
|
||||
const res = await createAxios({
|
||||
url: '/admin/user.User/adminScopeTree',
|
||||
@@ -360,8 +381,13 @@ const onAdminTreeChange = (val: string | number | null) => {
|
||||
|
||||
if (channelId !== undefined) {
|
||||
baTable.form.items.channel_id = channelId
|
||||
} else {
|
||||
delete baTable.form.items.channel_id
|
||||
}
|
||||
baTable.form.items.register_invite_code = inv !== undefined && inv !== null ? inv : ''
|
||||
nextTick(() => {
|
||||
formRef.value?.validateField('admin_id').catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -435,6 +461,16 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const validatorAdminChannel = (_rule: unknown, val: string | number | null | undefined, callback: (error?: Error) => void) => {
|
||||
if (val === undefined || val === null || val === '') {
|
||||
return callback()
|
||||
}
|
||||
if (resolveAdminChannelId(val) === undefined) {
|
||||
return callback(new Error(t('user.user.admin_no_channel_bound')))
|
||||
}
|
||||
return callback()
|
||||
}
|
||||
|
||||
const validatorGameUserPassword = (rule: any, val: string, callback: (error?: Error) => void) => {
|
||||
const operate = baTable.form.operate
|
||||
const v = typeof val === 'string' ? val.trim() : ''
|
||||
@@ -454,7 +490,10 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
|
||||
username: [buildValidatorData({ name: 'required', title: t('user.user.username') })],
|
||||
password: [{ validator: validatorGameUserPassword, trigger: 'blur' }],
|
||||
phone: [buildValidatorData({ name: 'required', title: t('user.user.phone') })],
|
||||
admin_id: [buildValidatorData({ name: 'required', title: t('user.user.admin_affiliation') })],
|
||||
admin_id: [
|
||||
buildValidatorData({ name: 'required', title: t('user.user.admin_affiliation') }),
|
||||
{ validator: validatorAdminChannel, trigger: 'change' },
|
||||
],
|
||||
create_time: [buildValidatorData({ name: 'date', title: t('user.user.create_time') })],
|
||||
update_time: [buildValidatorData({ name: 'date', title: t('user.user.update_time') })],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user