diff --git a/src/views/finance/FundFreezeConfig.vue b/src/views/finance/FundFreezeConfig.vue
index f0bd629..05ebc87 100644
--- a/src/views/finance/FundFreezeConfig.vue
+++ b/src/views/finance/FundFreezeConfig.vue
@@ -9,7 +9,7 @@
type="warning"
:closable="false"
show-icon
- title="默认关闭。未开启时结算全额进可提现,与现网一致。仅对当前顶部所选俱乐部生效。"
+ title="打开「启用冻结」并保存后,该角色新入账立即按规则冻结;旧单仍按当时快照解冻。关闭则新入账全额可提现。定时任务约每 5 分钟扫描到期/条件解冻。"
style="margin-bottom: 16px"
/>
当前俱乐部:{{ clubId }}
@@ -24,16 +24,17 @@
@@ -121,10 +245,66 @@ const clubId = ref('')
const roles = ref([])
const activeRole = ref('dashou')
const form = ref(null)
-const installmentText = ref('')
-const conditionText = ref('')
+const steps = ref([])
+const cond = reactive({
+ enable_min_completed_orders: false,
+ min_completed_orders: 30,
+ enable_max_fine_rate: false,
+ max_fine_rate: 0.05,
+ fine_rate_window_days: 30,
+ enable_max_complaint_rate: false,
+ max_complaint_rate: 0.05,
+ complaint_rate_window_days: 30,
+ enable_min_valid_invites: false,
+ min_valid_invites: 0,
+})
const preview = reactive({ available: '100', frozen: '0' })
+function defaultStep() {
+ return { after_days: 7, type: 'ratio', value: 0.3, need_condition: false }
+}
+
+function syncCondFromJson(cj) {
+ const c = cj || {}
+ const minOrders = Number(c.min_completed_orders || 0)
+ const maxFine = Number(c.max_fine_rate ?? 1)
+ const maxComp = Number(c.max_complaint_rate ?? 1)
+ const minInv = Number(c.min_valid_invites || 0)
+ cond.enable_min_completed_orders = c.enable_min_completed_orders != null
+ ? !!c.enable_min_completed_orders
+ : minOrders > 0
+ cond.min_completed_orders = minOrders || 30
+ cond.enable_max_fine_rate = c.enable_max_fine_rate != null
+ ? !!c.enable_max_fine_rate
+ : maxFine >= 0 && maxFine < 1
+ cond.max_fine_rate = maxFine < 1 ? maxFine : 0.05
+ cond.fine_rate_window_days = Number(c.fine_rate_window_days || 30)
+ cond.enable_max_complaint_rate = c.enable_max_complaint_rate != null
+ ? !!c.enable_max_complaint_rate
+ : maxComp >= 0 && maxComp < 1
+ cond.max_complaint_rate = maxComp < 1 ? maxComp : 0.05
+ cond.complaint_rate_window_days = Number(c.complaint_rate_window_days || 30)
+ cond.enable_min_valid_invites = c.enable_min_valid_invites != null
+ ? !!c.enable_min_valid_invites
+ : minInv > 0
+ cond.min_valid_invites = minInv
+}
+
+function buildConditionJson() {
+ return {
+ enable_min_completed_orders: !!cond.enable_min_completed_orders,
+ min_completed_orders: Number(cond.min_completed_orders || 0),
+ enable_max_fine_rate: !!cond.enable_max_fine_rate,
+ max_fine_rate: Number(cond.max_fine_rate || 0),
+ fine_rate_window_days: Number(cond.fine_rate_window_days || 30),
+ enable_max_complaint_rate: !!cond.enable_max_complaint_rate,
+ max_complaint_rate: Number(cond.max_complaint_rate || 0),
+ complaint_rate_window_days: Number(cond.complaint_rate_window_days || 30),
+ enable_min_valid_invites: !!cond.enable_min_valid_invites,
+ min_valid_invites: Number(cond.min_valid_invites || 0),
+ }
+}
+
function syncFormFromRole(role) {
const item = roles.value.find((r) => r.role === role)
if (!item) {
@@ -139,9 +319,18 @@ function syncFormFromRole(role) {
min_credit_to_freeze: Number(item.min_credit_to_freeze || 0),
freeze_min_days: Number(item.freeze_min_days || 0),
source_scope: Array.isArray(item.source_scope) ? [...item.source_scope] : ['order_settle'],
+ display_reason: item.display_reason || item.remark || '',
}
- installmentText.value = JSON.stringify(item.installment_json || {}, null, 2)
- conditionText.value = JSON.stringify(item.condition_json || {}, null, 2)
+ const rawSteps = item.installment_json?.steps
+ steps.value = Array.isArray(rawSteps) && rawSteps.length
+ ? rawSteps.map((s) => ({
+ after_days: Number(s.after_days || 0),
+ type: s.type || 'ratio',
+ value: Number(s.value ?? (s.type === 'remain' ? 1 : 0.3)),
+ need_condition: !!s.need_condition,
+ }))
+ : [defaultStep(), { after_days: 14, type: 'ratio', value: 0.3, need_condition: true }, { after_days: 30, type: 'remain', value: 1, need_condition: true }]
+ syncCondFromJson(item.condition_json)
preview.available = item.preview_100?.available || '100'
preview.frozen = item.preview_100?.frozen || '0'
}
@@ -150,6 +339,15 @@ function onTabChange(name) {
syncFormFromRole(name)
}
+function addStep() {
+ steps.value.push(defaultStep())
+}
+
+function removeStep(index) {
+ if (steps.value.length <= 1) return
+ steps.value.splice(index, 1)
+}
+
async function loadConfig() {
loading.value = true
try {
@@ -204,20 +402,14 @@ watch(
async function save() {
if (!form.value) return
- let installment_json = {}
- let condition_json = {}
- try {
- installment_json = installmentText.value ? JSON.parse(installmentText.value) : {}
- } catch {
- ElMessage.error('分期 JSON 格式错误')
- return
- }
- try {
- condition_json = conditionText.value ? JSON.parse(conditionText.value) : {}
- } catch {
- ElMessage.error('条件 JSON 格式错误')
+ if (!form.value.source_scope?.length) {
+ ElMessage.error('请至少选择一个入账来源')
return
}
+ const installment_json = form.value.schedule_mode === 'installment'
+ ? { steps: steps.value.map((s, i) => ({ ...s, seq: i + 1 })) }
+ : { steps: [{ seq: 1, after_days: form.value.freeze_min_days || 0, type: 'remain', value: 1, need_condition: false }] }
+
saving.value = true
try {
const res = await request.post(JITUAN_API.fundFreezeConfig, {
@@ -235,11 +427,14 @@ async function save() {
schedule_mode: form.value.schedule_mode,
freeze_min_days: form.value.freeze_min_days,
installment_json,
- condition_json,
- remark: form.value.remark,
+ condition_json: buildConditionJson(),
+ display_reason: form.value.display_reason,
+ remark: form.value.display_reason,
})
if (res.code === 0) {
- ElMessage.success('已保存')
+ ElMessage.success(form.value.enabled
+ ? '已保存并启用:新入账将按规则冻结,解冻由定时任务自动处理'
+ : '已保存(未启用):新入账仍全额可提现')
await loadConfig()
} else {
ElMessage.error(res.msg || '保存失败')
@@ -279,4 +474,27 @@ onMounted(loadConfig)
color: #888;
font-size: 12px;
}
+.tip.block {
+ display: block;
+ margin: 6px 0 0;
+}
+.steps-wrap {
+ width: 100%;
+}
+.steps-toolbar {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+}
+.cond-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+.cond-row {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+}