客服后台接入资金冻结配置:提现设置公共开关与各角色详情页冻结面板。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
179
src/components/FreezeConfigPanel.vue
Normal file
179
src/components/FreezeConfigPanel.vue
Normal 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>
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user