feat: P0-1 价格脚本引擎(安全公式解析+脚本CRUD+试算+报价集成)
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
<el-descriptions-item label="玻璃面积">{{ result.glass_area_m2 }} ㎡</el-descriptions-item>
|
||||
<el-descriptions-item label="总价">
|
||||
<span class="total">{{ result.total }} 元</span>
|
||||
<el-tag v-if="result.script_total || (result.scripted && result.scripted.length)" size="small"
|
||||
type="warning" effect="plain" style="margin-left:6px">脚本计价</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table :data="result.rows" size="small" border style="margin-top:10px">
|
||||
|
||||
@@ -1,54 +1,91 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" placeholder="名称/规格" clearable style="width:200px" @keyup.enter="load" />
|
||||
<el-select v-model="material" placeholder="全部类型" clearable style="width:130px" @change="load">
|
||||
<el-option value="profile" label="型材" /><el-option value="glass" label="玻璃" />
|
||||
<el-option value="hardware" label="五金" /><el-option value="addon" label="配件" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<div style="flex:1" />
|
||||
<el-button type="primary" plain @click="openEdit()">新增产品</el-button>
|
||||
</div>
|
||||
<el-table :data="rows" v-loading="loading" stripe>
|
||||
<el-table-column prop="name" label="名称" min-width="140" />
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">{{ label(row.material) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="spec" label="规格" width="120" />
|
||||
<el-table-column prop="unit" label="单位" width="70" />
|
||||
<el-table-column prop="price" label="售价" width="90" />
|
||||
<el-table-column prop="cost" label="成本" width="90" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" plain @click="remove(row)">删</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination style="margin-top:12px; justify-content:flex-end" background layout="total, prev, pager, next"
|
||||
:total="total" :page-size="page_size" v-model:current-page="page" @current-change="load" />
|
||||
|
||||
<el-dialog v-model="editVisible" :title="form.id ? '编辑产品' : '新增产品'" width="480px">
|
||||
<el-form :model="form" label-width="70px">
|
||||
<el-form-item label="名称"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="form.material" style="width:100%">
|
||||
<el-tabs v-model="tab">
|
||||
<!-- ============ 产品价格本 ============ -->
|
||||
<el-tab-pane label="产品价格本" name="products">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" placeholder="名称/规格" clearable style="width:200px" @keyup.enter="load" />
|
||||
<el-select v-model="material" placeholder="全部类型" clearable style="width:130px" @change="load">
|
||||
<el-option value="profile" label="型材" /><el-option value="glass" label="玻璃" />
|
||||
<el-option value="hardware" label="五金" /><el-option value="addon" label="配件" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="规格"><el-input v-model="form.spec" /></el-form-item>
|
||||
<el-form-item label="单位"><el-input v-model="form.unit" style="width:100px" /></el-form-item>
|
||||
<el-form-item label="售价"><el-input-number v-model="form.price" :min="0" :precision="2" /></el-form-item>
|
||||
<el-form-item label="成本"><el-input-number v-model="form.cost" :min="0" :precision="2" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="form.note" type="textarea" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<div style="flex:1" />
|
||||
<el-button type="primary" plain @click="openEdit()">新增产品</el-button>
|
||||
</div>
|
||||
<el-table :data="rows" v-loading="loading" stripe>
|
||||
<el-table-column prop="name" label="名称" min-width="140" />
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">{{ label(row.material) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="spec" label="规格" width="120" />
|
||||
<el-table-column prop="unit" label="单位" width="70" />
|
||||
<el-table-column prop="price" label="售价" width="90" />
|
||||
<el-table-column prop="cost" label="成本" width="90" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" plain @click="remove(row)">删</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination style="margin-top:12px; justify-content:flex-end" background layout="total, prev, pager, next"
|
||||
:total="total" :page-size="page_size" v-model:current-page="page" @current-change="load" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ============ 价格脚本 ============ -->
|
||||
<el-tab-pane label="价格脚本" name="scripts">
|
||||
<div class="toolbar">
|
||||
<span class="tip">按系列为各计价项配置公式,报价时启用即按脚本计价。变量:
|
||||
<el-tag v-for="v in SCRIPT_VARS" :key="v" size="small" type="info" effect="plain" class="var-tag">{{ v }}</el-tag>
|
||||
</span>
|
||||
<div style="flex:1" />
|
||||
<el-button type="primary" plain @click="openScript()">新增脚本</el-button>
|
||||
</div>
|
||||
<el-table :data="scripts" v-loading="scriptLoading" stripe>
|
||||
<el-table-column prop="name" label="系列/名称" min-width="130" />
|
||||
<el-table-column label="作用项" width="90">
|
||||
<template #default="{ row }">{{ targetLabel(row.target) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="formula" label="公式" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="启用" width="80">
|
||||
<template #default="{ row }"><el-switch :model-value="row.enabled === 1" @change="toggle(row, $event)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openScript(row)">编辑</el-button>
|
||||
<el-button size="small" type="success" plain @click="test(row)">试算</el-button>
|
||||
<el-button size="small" type="danger" plain @click="removeScript(row)">删</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 脚本编辑 -->
|
||||
<el-dialog v-model="scriptVisible" :title="scriptForm.id ? '编辑脚本' : '新增脚本'" width="560px">
|
||||
<el-form :model="scriptForm" label-width="80px">
|
||||
<el-form-item label="系列名称"><el-input v-model="scriptForm.name" placeholder="如 断桥铝-55系列" /></el-form-item>
|
||||
<el-form-item label="作用项">
|
||||
<el-select v-model="scriptForm.target" style="width:100%">
|
||||
<el-option v-for="(v, k) in TARGETS" :key="k" :value="k" :label="v" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="公式">
|
||||
<el-input v-model="scriptForm.formula" placeholder="如 (w+h)*2/1000*52" />
|
||||
<div class="formula-help">结果 = 单价;total 项结果 = 总价。支持 + - * / 括号与 min/max/round/abs/ceil/floor/sqrt</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="scriptForm.note" /></el-form-item>
|
||||
<el-form-item label="试算">
|
||||
<el-button size="small" @click="test(scriptForm)">按示例窗型试算</el-button>
|
||||
<el-tag v-if="testResult !== null" type="success" style="margin-left:8px">结果 ¥{{ testResult }}</el-tag>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="scriptVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveScript">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
@@ -57,6 +94,9 @@ import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
|
||||
const tab = ref('products')
|
||||
|
||||
// ---------- 产品价格本 ----------
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
@@ -99,9 +139,61 @@ async function remove(row: any) {
|
||||
load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
// ---------- 价格脚本 ----------
|
||||
const SCRIPT_VARS = ['w', 'h', 'area', 'perimeter', 'glass_area', 'sashes', 'meters_frame', 'meters_mullion', 'meters_sash', 'length']
|
||||
const TARGETS: Record<string, string> = { frame: '外框', mullion: '中梃', sash: '扇框', glass: '玻璃', hardware: '五金', total: '总价直取' }
|
||||
const targetLabel = (t: string) => TARGETS[t] ?? t
|
||||
|
||||
const scripts = ref<any[]>([])
|
||||
const scriptLoading = ref(false)
|
||||
const scriptVisible = ref(false)
|
||||
const testResult = ref<number | null>(null)
|
||||
const scriptForm = reactive<any>({ id: 0, name: '', target: 'frame', formula: '', note: '', enabled: 1 })
|
||||
|
||||
async function loadScripts() {
|
||||
scriptLoading.value = true
|
||||
try { scripts.value = (await api<any[]>('get', '/api/scripts')) ?? [] } finally { scriptLoading.value = false }
|
||||
}
|
||||
|
||||
function openScript(row?: any) {
|
||||
testResult.value = null
|
||||
Object.assign(scriptForm, row ?? { id: 0, name: '', target: 'frame', formula: '', note: '', enabled: 1 })
|
||||
scriptVisible.value = true
|
||||
}
|
||||
|
||||
async function saveScript() {
|
||||
if (scriptForm.id) await api('put', `/api/scripts/${scriptForm.id}`, scriptForm)
|
||||
else await api('post', '/api/scripts', scriptForm)
|
||||
ElMessage.success('已保存')
|
||||
scriptVisible.value = false
|
||||
loadScripts()
|
||||
}
|
||||
|
||||
async function toggle(row: any, v: any) {
|
||||
await api('put', `/api/scripts/${row.id}`, { ...row, enabled: v ? 1 : 0 })
|
||||
row.enabled = v ? 1 : 0
|
||||
}
|
||||
|
||||
async function removeScript(row: any) {
|
||||
await ElMessageBox.confirm(`确定删除脚本「${row.name}」?`, '提示', { type: 'warning' })
|
||||
await api('delete', `/api/scripts/${row.id}`)
|
||||
ElMessage.success('已删除')
|
||||
loadScripts()
|
||||
}
|
||||
|
||||
async function test(row: any) {
|
||||
const d = await api<any>('post', '/api/scripts/preview', { formula: row.formula, target: row.target })
|
||||
testResult.value = d.result
|
||||
if (!row.id) return
|
||||
ElMessage.success(`试算结果 ¥${d.result}`)
|
||||
}
|
||||
|
||||
onMounted(() => { load(); loadScripts() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
|
||||
.tip { font-size: 12px; color: #909399; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
|
||||
.var-tag { font-family: Consolas, monospace; }
|
||||
.formula-help { font-size: 12px; color: #909399; line-height: 1.5; margin-top: 4px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user