feat(purchase): allow absolute price ceiling (#180)
This commit is contained in:
@@ -27,10 +27,18 @@
|
||||
<el-dialog v-model="dialog.open" :title="dialog.ruleId ? '编辑采购规则' : '新建采购规则'" width="760px" destroy-on-close>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-position="top">
|
||||
<el-form-item label="规则名称" prop="name"><el-input v-model="form.name" maxlength="120" show-word-limit /></el-form-item>
|
||||
<div class="ratio-grid">
|
||||
<el-form-item label="参考价倍率保护">
|
||||
<el-switch v-model="form.priceGuardEnabled" inline-prompt active-text="开启" inactive-text="关闭" />
|
||||
<div class="field-help">关闭后不再按采集参考价计算倍率范围,但仍强制校验单件绝对最高价。</div>
|
||||
</el-form-item>
|
||||
<div v-if="form.priceGuardEnabled" class="ratio-grid">
|
||||
<el-form-item label="最低价比例"><el-input-number v-model="form.minRatio" :min="0.1" :max="1" :step="0.05" :precision="4" controls-position="right" /><div class="field-help">范围 0.1~1.0,默认 0.2;最低价向下取整到分。</div></el-form-item>
|
||||
<el-form-item label="最高价比例"><el-input-number v-model="form.maxRatio" :min="1" :max="3" :step="0.05" :precision="4" controls-position="right" /><div class="field-help">范围 1.0~3.0,默认 1.5;最高价向上取整到分。</div></el-form-item>
|
||||
</div>
|
||||
<div v-else class="absolute-guard">
|
||||
<el-alert title="高风险:参考价倍率保护已关闭。新任务只受下方单件绝对最高价约束。" type="error" :closable="false" show-icon />
|
||||
<el-form-item label="单件绝对最高价(元)"><el-input-number v-model="form.absoluteMaxUnitPriceYuan" :min="0.01" :max="1000" :step="1" :precision="2" controls-position="right" /><div class="field-help">必填,范围 ¥0.01~¥1000.00;Android 创建订单前仍会严格校验。</div></el-form-item>
|
||||
</div>
|
||||
<el-form-item label="规则 JSON" prop="contentText"><el-input v-model="form.contentText" type="textarea" :rows="18" class="json-editor" spellcheck="false" /><div class="field-help">仅支持 schemaVersion=1、ruleType=pddPurchase;服务端会再次执行严格字段和安全动作校验。</div></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button :disabled="dialog.saving" @click="dialog.open=false">取消</el-button><el-button type="primary" :loading="dialog.saving" @click="save">保存</el-button></template>
|
||||
@@ -43,28 +51,49 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { createPurchaseRule, deletePurchaseRule, listPurchaseRules, setCurrentPurchaseRule, updatePurchaseRule } from '@/api/goauto/purchase-rules'
|
||||
|
||||
const defaultRule = { schemaVersion: 1, ruleType: 'pddPurchase', requiredCapabilities: ['purchase.live.v1'], priceGuard: { minRatio: 0.2, maxRatio: 1.5 }, actions: [{ type: 'openProduct' }, { type: 'verifyProduct' }, { type: 'openSpecPanel' }, { type: 'selectSpec' }, { type: 'setQuantity' }, { type: 'verifyUnitPrice' }, { type: 'verifyOrderSummary' }] }
|
||||
const defaultRule = { schemaVersion: 1, ruleType: 'pddPurchase', requiredCapabilities: ['purchase.live.v1'], priceGuard: { enabled: true, minRatio: 0.2, maxRatio: 1.5 }, actions: [{ type: 'openProduct' }, { type: 'verifyProduct' }, { type: 'openSpecPanel' }, { type: 'selectSpec' }, { type: 'setQuantity' }, { type: 'verifyUnitPrice' }, { type: 'verifyOrderSummary' }] }
|
||||
const defaultForm = () => ({ name: '', priceGuardEnabled: true, minRatio: 0.2, maxRatio: 1.5, absoluteMaxUnitPriceYuan: 50, contentText: '' })
|
||||
export default {
|
||||
name: 'GoAutoPurchaseRules',
|
||||
setup() { return { Plus, RefreshLeft, Search } },
|
||||
data() {
|
||||
const jsonRule = (_r, value, callback) => { try { const parsed = JSON.parse(value); if (!parsed || Array.isArray(parsed) || parsed.schemaVersion !== 1 || parsed.ruleType !== 'pddPurchase') return callback(new Error('请输入有效的 PDD 采购规则 JSON')); callback() } catch (_) { callback(new Error('请输入有效 JSON')) } }
|
||||
return { loading: false, switching: null, items: [], total: 0, query: { page: 1, pageSize: 20, name: '' }, dialog: { open: false, saving: false, ruleId: null }, form: { name: '', minRatio: 0.2, maxRatio: 1.5, contentText: '' }, rules: { name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }], contentText: [{ required: true, validator: jsonRule, trigger: 'blur' }] }}
|
||||
return { loading: false, switching: null, items: [], total: 0, query: { page: 1, pageSize: 20, name: '' }, dialog: { open: false, saving: false, ruleId: null }, form: defaultForm(), rules: { name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }], contentText: [{ required: true, validator: jsonRule, trigger: 'blur' }] }}
|
||||
},
|
||||
created() { this.load() },
|
||||
methods: {
|
||||
async load() { this.loading = true; try { const response = await listPurchaseRules(this.query); this.items = response.data.items; this.total = response.data.total } finally { this.loading = false } },
|
||||
search() { this.query.page = 1; this.load() }, reset() { this.query = { page: 1, pageSize: 20, name: '' }; this.load() },
|
||||
summary(content) { const guard = content.priceGuard || { minRatio: 0.2, maxRatio: 1.5 }; return `${content.requiredCapabilities?.length || 0} 项能力 · ${content.actions?.length || 0} 个动作 · 价格 ${guard.minRatio}~${guard.maxRatio} 倍` },
|
||||
openCreate() { this.dialog = { open: true, saving: false, ruleId: null }; this.form = { name: '', minRatio: 0.2, maxRatio: 1.5, contentText: JSON.stringify(defaultRule, null, 2) } },
|
||||
openEdit(row) { const guard = row.content.priceGuard || { minRatio: 0.2, maxRatio: 1.5 }; this.dialog = { open: true, saving: false, ruleId: row.id }; this.form = { name: row.name, minRatio: guard.minRatio, maxRatio: guard.maxRatio, contentText: JSON.stringify(row.content, null, 2) } },
|
||||
async save() { const valid = await this.$refs.form.validate().catch(() => false); if (!valid) return; if (this.form.minRatio > this.form.maxRatio) { ElMessage.error('最低价比例不能大于最高价比例'); return } this.dialog.saving = true; const content = JSON.parse(this.form.contentText); content.priceGuard = { minRatio: this.form.minRatio, maxRatio: this.form.maxRatio }; const payload = { requestId: crypto.randomUUID(), name: this.form.name.trim(), content }; try { if (this.dialog.ruleId) await updatePurchaseRule(this.dialog.ruleId, payload); else await createPurchaseRule(payload); ElMessage.success('采购规则已保存'); this.dialog.open = false; await this.load() } finally { this.dialog.saving = false } },
|
||||
async setCurrent(row) { await ElMessageBox.confirm(`设为当前后,新建及安全重试的采购任务将使用“${row.name}”。`, '切换当前采购规则', { type: 'warning', confirmButtonText: '确认切换', cancelButtonText: '取消' }); this.switching = row.id; try { await setCurrentPurchaseRule({ requestId: crypto.randomUUID(), ruleId: row.id }); ElMessage.success('当前采购规则已切换'); await this.load() } finally { this.switching = null } },
|
||||
summary(content) { const guard = content.priceGuard || { minRatio: 0.2, maxRatio: 1.5 }; const price = guard.enabled === false ? `倍率保护关闭 · 单件上限 ¥${this.formatYuan(guard.absoluteMaxUnitPriceCent)}` : `价格 ${guard.minRatio}~${guard.maxRatio} 倍`; return `${content.requiredCapabilities?.length || 0} 项能力 · ${content.actions?.length || 0} 个动作 · ${price}` },
|
||||
formatYuan(cent) { return (Number(cent || 0) / 100).toFixed(2) },
|
||||
openCreate() { this.dialog = { open: true, saving: false, ruleId: null }; this.form = { ...defaultForm(), contentText: JSON.stringify(defaultRule, null, 2) } },
|
||||
openEdit(row) { const guard = row.content.priceGuard || { minRatio: 0.2, maxRatio: 1.5 }; const enabled = guard.enabled !== false; this.dialog = { open: true, saving: false, ruleId: row.id }; this.form = { name: row.name, priceGuardEnabled: enabled, minRatio: guard.minRatio ?? 0.2, maxRatio: guard.maxRatio ?? 1.5, absoluteMaxUnitPriceYuan: enabled ? 50 : guard.absoluteMaxUnitPriceCent / 100, contentText: JSON.stringify(row.content, null, 2) } },
|
||||
async save() {
|
||||
const valid = await this.$refs.form.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (this.form.priceGuardEnabled && this.form.minRatio > this.form.maxRatio) { ElMessage.error('最低价比例不能大于最高价比例'); return }
|
||||
const absoluteYuan = Number(this.form.absoluteMaxUnitPriceYuan)
|
||||
const absoluteCent = Math.round(absoluteYuan * 100)
|
||||
if (!this.form.priceGuardEnabled) {
|
||||
if (!Number.isFinite(absoluteYuan) || absoluteYuan < 0.01 || absoluteYuan > 1000 || Math.abs(absoluteYuan * 100 - absoluteCent) > 1e-7) { ElMessage.error('单件绝对最高价必须为 ¥0.01~¥1000.00,最多两位小数'); return }
|
||||
const confirmed = await ElMessageBox.confirm(`关闭后,新任务不再校验参考价倍率,仅校验单件绝对最高价 ¥${absoluteYuan.toFixed(2)}。已有任务不变。`, '确认关闭参考价倍率保护', { type: 'error', confirmButtonText: '确认关闭并保存', cancelButtonText: '取消' }).then(() => true).catch(() => false)
|
||||
if (!confirmed) return
|
||||
}
|
||||
this.dialog.saving = true
|
||||
const content = JSON.parse(this.form.contentText)
|
||||
content.priceGuard = this.form.priceGuardEnabled ? { enabled: true, minRatio: this.form.minRatio, maxRatio: this.form.maxRatio } : { enabled: false, absoluteMaxUnitPriceCent: absoluteCent }
|
||||
const payload = { requestId: crypto.randomUUID(), name: this.form.name.trim(), content }
|
||||
try {
|
||||
if (this.dialog.ruleId) await updatePurchaseRule(this.dialog.ruleId, payload); else await createPurchaseRule(payload)
|
||||
ElMessage.success('采购规则已保存'); this.dialog.open = false; await this.load()
|
||||
} finally { this.dialog.saving = false }
|
||||
},
|
||||
async setCurrent(row) { const guard = row.content.priceGuard; const risk = guard?.enabled === false ? `该规则已关闭参考价倍率保护,单件绝对最高价为 ¥${this.formatYuan(guard.absoluteMaxUnitPriceCent)}。` : ''; await ElMessageBox.confirm(`设为当前后,新建及安全重试的采购任务将使用“${row.name}”。${risk}`, '切换当前采购规则', { type: guard?.enabled === false ? 'error' : 'warning', confirmButtonText: '确认切换', cancelButtonText: '取消' }); this.switching = row.id; try { await setCurrentPurchaseRule({ requestId: crypto.randomUUID(), ruleId: row.id }); ElMessage.success('当前采购规则已切换'); await this.load() } finally { this.switching = null } },
|
||||
async remove(row) { await ElMessageBox.confirm(`确定删除“${row.name}”吗?已有任务快照不受影响。`, '删除采购规则', { type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '取消' }); await deletePurchaseRule(row.id, { requestId: crypto.randomUUID() }); ElMessage.success('采购规则已删除'); await this.load() }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-card{min-height:calc(100vh - 124px)}.page-heading{display:flex;justify-content:space-between;gap:16px;margin-bottom:18px}.page-heading h1{margin:0 0 6px;font-size:24px;color:#1f2937}.page-heading p,.field-help{margin:0;color:#64748b;line-height:1.5}.search-form{margin:18px 0 16px;padding:16px 16px 0;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc}.ratio-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.ratio-grid :deep(.el-input-number){width:100%}:deep(.json-editor textarea){font-family:"Cascadia Code",Consolas,monospace;line-height:1.55}.field-help{margin-top:6px;font-size:13px}@media(max-width:768px){.page-heading{flex-direction:column}.ratio-grid{grid-template-columns:1fr}}
|
||||
.page-card{min-height:calc(100vh - 124px)}.page-heading{display:flex;justify-content:space-between;gap:16px;margin-bottom:18px}.page-heading h1{margin:0 0 6px;font-size:24px;color:#1f2937}.page-heading p,.field-help{margin:0;color:#64748b;line-height:1.5}.search-form{margin:18px 0 16px;padding:16px 16px 0;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc}.ratio-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.ratio-grid :deep(.el-input-number),.absolute-guard :deep(.el-input-number){width:100%}.absolute-guard{display:grid;gap:16px;margin-bottom:8px}:deep(.json-editor textarea){font-family:"Cascadia Code",Consolas,monospace;line-height:1.55}.field-help{margin-top:6px;font-size:13px}@media(max-width:768px){.page-heading{flex-direction:column}.ratio-grid{grid-template-columns:1fr}}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user