客服后台接入资金冻结配置:提现设置公共开关与各角色详情页冻结面板。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-10 04:06:09 +08:00
parent adaf9b2522
commit e44b1c5d2b
5 changed files with 356 additions and 3 deletions

View File

@@ -0,0 +1,179 @@
<template>
<el-card class="freeze-card" shadow="hover">
<template #header>
<div class="card-header">
<span>🔒 {{ title }}冻结配置</span>
<el-tag v-if="info.freeze_active" type="warning" size="small">冻结生效中</el-tag>
<el-tag v-else type="info" size="small">未启用冻结</el-tag>
</div>
</template>
<el-descriptions :column="2" border size="small" class="mb-12">
<el-descriptions-item label="可提现余额">¥{{ info.balance || '0.00' }}</el-descriptions-item>
<el-descriptions-item label="冻结资金池">¥{{ info.dongjie_chi || '0.00' }}</el-descriptions-item>
<el-descriptions-item label="最大可申请">¥{{ info.max_apply_jine || '0.00' }}</el-descriptions-item>
<el-descriptions-item label="配置来源">{{ sourceLabel }}</el-descriptions-item>
</el-descriptions>
<el-form label-width="140px" :disabled="!isEditMode">
<el-form-item label="单独冻结配置">
<el-switch v-model="form.dandu_dongjie_enabled" />
<span class="hint">开启后覆盖公共冻结配置</span>
</el-form-item>
<el-form-item label="冻结方式">
<el-radio-group v-model="form.dongjie_mode">
<el-radio :label="1">按比例</el-radio>
<el-radio :label="2">固定金额</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.dongjie_mode === 1" label="冻结比例(%)">
<el-input-number v-model="biliPercent" :min="0" :max="100" :precision="2" :step="1" />
</el-form-item>
<el-form-item v-else label="固定冻结目标(元)">
<el-input-number v-model="form.dongjie_guding_jine" :min="0" :precision="2" :step="10" />
</el-form-item>
</el-form>
<div class="unfreeze-row">
<el-input-number
v-model="unfreezeAmount"
:min="0"
:max="parseFloat(info.dongjie_chi || 0)"
:precision="2"
placeholder="解冻金额"
style="width: 160px"
/>
<el-input v-model="unfreezeRemark" placeholder="解冻备注(可选)" style="width: 200px; margin-left: 8px" />
<el-button type="warning" style="margin-left: 8px" :loading="unfreezing" @click="handleUnfreeze">
解冻到余额
</el-button>
</div>
<div v-if="isEditMode" class="actions">
<el-button type="primary" :loading="saving" @click="handleSave">保存冻结配置</el-button>
</div>
</el-card>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
const props = defineProps({
yonghuid: { type: String, required: true },
leixing: { type: Number, required: true },
title: { type: String, default: '' },
isEditMode: { type: Boolean, default: false },
})
const username = computed(() => localStorage.getItem('username') || '')
const info = reactive({
balance: '0.00',
dongjie_chi: '0.00',
max_apply_jine: '0.00',
freeze_active: false,
freeze_source: '',
})
const form = reactive({
dandu_dongjie_enabled: false,
dongjie_mode: 1,
dongjie_bili: '0.00',
dongjie_guding_jine: 0,
})
const biliPercent = computed({
get: () => parseFloat(form.dongjie_bili || 0) * 100,
set: (v) => { form.dongjie_bili = String((Number(v) || 0) / 100) },
})
const sourceLabel = computed(() => {
if (!info.freeze_active) return '未启用'
return info.freeze_source === 'user' ? '单独配置' : '公共配置'
})
const saving = ref(false)
const unfreezing = ref(false)
const unfreezeAmount = ref(0)
const unfreezeRemark = ref('')
const load = async () => {
if (!props.yonghuid) return
const res = await request.post('/houtai/hqdjdongjie', {
username: username.value,
action: 'get',
yonghuid: props.yonghuid,
leixing: props.leixing,
})
if (res.code === 0 && res.data) {
Object.assign(info, res.data)
form.dandu_dongjie_enabled = !!res.data.dandu_dongjie_enabled
form.dongjie_mode = res.data.dongjie_mode || 1
form.dongjie_bili = res.data.dongjie_bili || '0'
form.dongjie_guding_jine = parseFloat(res.data.dongjie_guding_jine || 0)
}
}
const handleSave = async () => {
saving.value = true
try {
const res = await request.post('/houtai/hqdjdongjie', {
username: username.value,
action: 'save',
yonghuid: props.yonghuid,
leixing: props.leixing,
dandu_dongjie_enabled: form.dandu_dongjie_enabled,
dongjie_mode: form.dongjie_mode,
dongjie_bili: form.dongjie_bili,
dongjie_guding_jine: form.dongjie_guding_jine,
})
if (res.code === 0) {
ElMessage.success('冻结配置已保存')
await load()
} else {
ElMessage.error(res.msg || '保存失败')
}
} finally {
saving.value = false
}
}
const handleUnfreeze = async () => {
if (!unfreezeAmount.value || unfreezeAmount.value <= 0) {
ElMessage.warning('请输入解冻金额')
return
}
unfreezing.value = true
try {
const res = await request.post('/houtai/hqdjdongjie', {
username: username.value,
action: 'unfreeze',
yonghuid: props.yonghuid,
leixing: props.leixing,
jiedong_jine: unfreezeAmount.value,
beizhu: unfreezeRemark.value,
})
if (res.code === 0) {
ElMessage.success('解冻成功')
unfreezeAmount.value = 0
unfreezeRemark.value = ''
await load()
} else {
ElMessage.error(res.msg || '解冻失败')
}
} finally {
unfreezing.value = false
}
}
watch(() => [props.yonghuid, props.leixing], load, { immediate: true })
defineExpose({ reload: load })
</script>
<style scoped>
.freeze-card { margin-top: 16px; }
.card-header { display: flex; align-items: center; gap: 8px; }
.mb-12 { margin-bottom: 12px; }
.hint { margin-left: 8px; color: #909399; font-size: 12px; }
.unfreeze-row { display: flex; align-items: center; flex-wrap: wrap; margin-top: 12px; }
.actions { margin-top: 12px; }
</style>

View File

@@ -272,6 +272,15 @@
</div>
</div>
<FreezeConfigPanel
v-if="baseForm.yonghuid"
:yonghuid="baseForm.yonghuid"
:leixing="2"
title="管事分红"
:is-edit-mode="true"
class="freeze-section"
/>
<!-- 手动修改余额弹窗 -->
<el-dialog v-model="balanceDialogVisible" title="修改余额" width="400px">
<el-form :model="balanceModify" label-width="100px">
@@ -347,6 +356,7 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import request from '@/utils/request'
import FreezeConfigPanel from '@/components/FreezeConfigPanel.vue'
import { cloneDeep, isEqual } from 'lodash-es'
const route = useRoute()

View File

@@ -346,6 +346,25 @@
</el-card>
</el-col>
</el-row>
<el-row v-if="playerInfo.yonghuid" :gutter="20" class="freeze-section">
<el-col :span="12">
<FreezeConfigPanel
:yonghuid="playerInfo.yonghuid"
:leixing="1"
title="打手佣金"
:is-edit-mode="true"
/>
</el-col>
<el-col :span="12">
<FreezeConfigPanel
:yonghuid="playerInfo.yonghuid"
:leixing="5"
title="打手押金"
:is-edit-mode="true"
/>
</el-col>
</el-row>
</div>
<!-- 添加会员弹窗 -->
@@ -379,6 +398,7 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Edit, Check, Close, CopyDocument, Avatar } from '@element-plus/icons-vue'
import request from '@/utils/request'
import FreezeConfigPanel from '@/components/FreezeConfigPanel.vue'
const route = useRoute()
const router = useRouter()

View File

@@ -100,6 +100,15 @@
<el-descriptions-item label="本月流水">{{ formatMoney(merchant.jinyueliushui) }}</el-descriptions-item>
</el-descriptions>
</div>
<FreezeConfigPanel
v-if="merchant.yonghuid"
:yonghuid="merchant.yonghuid"
:leixing="6"
title="商家余额"
:is-edit-mode="true"
class="freeze-section"
/>
</div>
<!-- 错误状态 -->
@@ -145,6 +154,7 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Phone, ChatDotRound } from '@element-plus/icons-vue'
import request from '@/utils/request'
import FreezeConfigPanel from '@/components/FreezeConfigPanel.vue'
const route = useRoute()
const router = useRouter()

View File

@@ -147,6 +147,50 @@
</div>
</div>
</div>
<!-- 资金冻结公共配置 -->
<div class="freeze-settings-card">
<div class="card-title">资金冻结公共配置</div>
<el-alert
type="warning"
:closable="false"
show-icon
title="开启后,手动/自动提现申请时按规则冻结部分余额进入「冻结资金池」,驳回不退冻结部分。"
style="margin-bottom: 12px"
/>
<div class="freeze-global-row">
<span>全员冻结总开关</span>
<el-switch v-model="editData.dongjieQuanju" :disabled="!isEditMode" />
</div>
<div v-for="role in freezeRoleDefs" :key="role.leixing" class="freeze-role-block">
<div class="freeze-role-title">{{ role.name }}</div>
<div class="freeze-role-grid">
<label>启用</label>
<el-switch v-model="getFreezeRole(role.leixing).role_enabled" :disabled="!isEditMode" />
<label>方式</label>
<el-radio-group v-model="getFreezeRole(role.leixing).dongjie_mode" :disabled="!isEditMode">
<el-radio :label="1">比例</el-radio>
<el-radio :label="2">固定金额</el-radio>
</el-radio-group>
<template v-if="getFreezeRole(role.leixing).dongjie_mode === 1">
<label>比例%</label>
<el-input-number
v-model="getFreezeRole(role.leixing).bili_percent"
:disabled="!isEditMode"
:min="0" :max="100" :precision="2" :step="1" size="small"
/>
</template>
<template v-else>
<label>固定额()</label>
<el-input-number
v-model="getFreezeRole(role.leixing).dongjie_guding_jine"
:disabled="!isEditMode"
:min="0" :precision="2" :step="10" size="small"
/>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -210,15 +254,41 @@ const withdrawMode = ref(2) // 默认手动提现
// 编辑时的临时变量(用于绑定 el-radio-group
const displayMode = ref(withdrawMode.value)
// 资金冻结公共配置leixing 1/2/5/6
const freezeRoleDefs = [
{ leixing: 1, name: '打手佣金' },
{ leixing: 2, name: '管事分红' },
{ leixing: 5, name: '打手押金' },
{ leixing: 6, name: '商家余额' }
]
const defaultFreezeRole = (leixing) => ({
leixing,
role_enabled: false,
dongjie_mode: 1,
bili_percent: 0,
dongjie_guding_jine: 0
})
// 编辑数据
const editData = reactive({
rates: { ...DEFAULT_RATES },
quotas: { 1: 0, 2: 0, 3: 0 },
totalLimits: { ...DEFAULT_TOTAL_LIMITS },
orderRates: { ...DEFAULT_ORDER_RATES },
withdrawMode: 2
withdrawMode: 2,
dongjieQuanju: false,
dongjieRoles: freezeRoleDefs.map(r => defaultFreezeRole(r.leixing))
})
function getFreezeRole(leixing) {
let row = editData.dongjieRoles.find(r => r.leixing === leixing)
if (!row) {
row = defaultFreezeRole(leixing)
editData.dongjieRoles.push(row)
}
return row
}
// Canvas 引用和动画
const canvasMap = ref({})
let animationId = null
@@ -448,6 +518,18 @@ const fetchSettings = async () => {
withdrawMode.value = mode
displayMode.value = mode
editData.withdrawMode = mode
editData.dongjieQuanju = !!data.dongjie_quanju_enabled
const fetchedRoles = data.dongjie_roles || []
editData.dongjieRoles = freezeRoleDefs.map(def => {
const row = fetchedRoles.find(r => r.leixing === def.leixing) || {}
return {
leixing: def.leixing,
role_enabled: !!row.role_enabled,
dongjie_mode: row.dongjie_mode || 1,
bili_percent: parseFloat(row.dongjie_bili || 0) * 100,
dongjie_guding_jine: parseFloat(row.dongjie_guding_jine || 0),
}
})
drawAllTanks()
} else if (res.code === 403) {
hasPermission.value = false
@@ -468,7 +550,9 @@ const enterEditMode = () => {
quotas: { ...editData.quotas },
totalLimits: { ...editData.totalLimits },
orderRates: { ...editData.orderRates },
withdrawMode: withdrawMode.value
withdrawMode: withdrawMode.value,
dongjieQuanju: editData.dongjieQuanju,
dongjieRoles: editData.dongjieRoles.map(r => ({ ...r })),
}
isEditMode.value = true
}
@@ -480,6 +564,8 @@ const cancelEdit = () => {
editData.totalLimits = { ...originalData.totalLimits }
editData.orderRates = { ...originalData.orderRates }
editData.withdrawMode = originalData.withdrawMode
editData.dongjieQuanju = originalData.dongjieQuanju
editData.dongjieRoles = originalData.dongjieRoles.map(r => ({ ...r }))
displayMode.value = originalData.withdrawMode
roles.value.forEach(role => {
role.rate = editData.rates[role.type]
@@ -506,6 +592,15 @@ const hasSettingsChanged = () => {
for (const key of [1, 2, 3, 4, 5, 6]) {
if (editData.totalLimits[key] !== originalData.totalLimits[key]) return true
}
if (editData.dongjieQuanju !== originalData.dongjieQuanju) return true
for (const row of editData.dongjieRoles) {
const orig = originalData.dongjieRoles.find(r => r.leixing === row.leixing)
if (!orig) return true
if (row.role_enabled !== orig.role_enabled) return true
if (row.dongjie_mode !== orig.dongjie_mode) return true
if (row.bili_percent !== orig.bili_percent) return true
if (row.dongjie_guding_jine !== orig.dongjie_guding_jine) return true
}
return false
}
@@ -531,7 +626,15 @@ const saveSettings = async () => {
quotas: { ...editData.quotas },
total_limits: { ...editData.totalLimits },
order_rates: { ...editData.orderRates },
withdraw_mode: editData.withdrawMode
withdraw_mode: editData.withdrawMode,
dongjie_quanju_enabled: editData.dongjieQuanju,
dongjie_roles: editData.dongjieRoles.map(r => ({
leixing: r.leixing,
role_enabled: r.role_enabled,
dongjie_mode: r.dongjie_mode,
dongjie_bili: (Number(r.bili_percent) || 0) / 100,
dongjie_guding_jine: r.dongjie_guding_jine,
})),
}
const res = await request.post('/houtai/htxgtxsz', payload)
if (res.code === 0) {
@@ -794,4 +897,35 @@ onBeforeUnmount(() => {
:deep(.el-input-number .el-input__inner) {
color: #fff !important;
}
.freeze-settings-card {
background: rgba(10, 18, 28, 0.85);
border: 1px solid #2a6f8f;
border-radius: 16px;
padding: 20px;
margin-top: 20px;
}
.freeze-global-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
color: #d4e8f5;
}
.freeze-role-block {
border-top: 1px solid #2a6f8f;
padding-top: 12px;
margin-top: 12px;
}
.freeze-role-title {
font-weight: 600;
color: #6cc3e8;
margin-bottom: 8px;
}
.freeze-role-grid {
display: grid;
grid-template-columns: 60px 1fr;
gap: 8px 12px;
align-items: center;
color: #b8d4e8;
}
</style>