feat: v0.3 报价算料引擎与仓库库存模块
后端: - 报价算料: 从drawing_data几何提取型材清单(外框/中梃/扇框投影长度) 与玻璃面积(鞋带公式), 套价格本计价, 支持样板/订单/自定义图纸三入口 - 仓库库存: 材料CRUD/出入库/流水/负库存拦截/预警线 - SQLite默认路径锚定项目根目录(修复cwd依赖) 前端: - 仓库库存页(筛选/出入库对话框/预警高亮) - 算料报价弹窗(样板库与订单详情入口, 支持打印) - 路由与导航更新(8页面) 回归: 9个API端点全部200, 报价/优化/SPA路由验证通过
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="算料报价" width="640px">
|
||||
<div v-if="result" v-loading="loading">
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="窗型尺寸">{{ result.width_mm }} × {{ result.height_mm }} mm</el-descriptions-item>
|
||||
<el-descriptions-item label="玻璃面积">{{ result.glass_area_m2 }} ㎡</el-descriptions-item>
|
||||
<el-descriptions-item label="总价">
|
||||
<span class="total">{{ result.total }} 元</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table :data="result.rows" size="small" border style="margin-top:10px">
|
||||
<el-table-column prop="note" label="项目" min-width="110">
|
||||
<template #default="{ row }">{{ row.note || kindLabel(row.kind) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规格" width="110">
|
||||
<template #default="{ row }">{{ row.length ? row.length + 'mm' : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="qty" label="数量" width="80" />
|
||||
<el-table-column label="单价" width="90">
|
||||
<template #default="{ row }">{{ row.unit_price }} {{ row.unit }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额" width="100" />
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
<el-button type="primary" @click="print">打印报价单</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { api } from '../api'
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const result = ref<any>(null)
|
||||
let source: { drawing_data?: string; template_id?: number; order_id?: number } = {}
|
||||
|
||||
function kindLabel(k: string) {
|
||||
return ({ frame: '外框', mullion: '中梃', sash: '扇框', glass: '玻璃', hardware: '五金' } as Record<string, string>)[k] ?? k
|
||||
}
|
||||
|
||||
async function open(src: typeof source) {
|
||||
source = src
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
try {
|
||||
result.value = await api<any>('post', '/api/offer-price', src)
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function print() {
|
||||
window.print()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.total { color: #e6483d; font-size: 18px; font-weight: 700; }
|
||||
@media print { .el-dialog__footer { display: none; } }
|
||||
</style>
|
||||
@@ -16,6 +16,7 @@ const router = createRouter({
|
||||
{ path: 'contracts', component: () => import('../views/ContractsView.vue'), meta: { title: '合同管理' } },
|
||||
{ path: 'customers', component: () => import('../views/CustomersView.vue'), meta: { title: '客户管理' } },
|
||||
{ path: 'templates', component: () => import('../views/TemplatesView.vue'), meta: { title: '样板库' } },
|
||||
{ path: 'warehouse', component: () => import('../views/WarehouseView.vue'), meta: { title: '仓库库存' } },
|
||||
{ path: 'optimize', component: () => import('../views/OptimizeView.vue'), meta: { title: '优化下料' } },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<el-menu-item index="/contracts"><el-icon><Document /></el-icon>合同管理</el-menu-item>
|
||||
<el-menu-item index="/customers"><el-icon><User /></el-icon>客户管理</el-menu-item>
|
||||
<el-menu-item index="/templates"><el-icon><Grid /></el-icon>样板库</el-menu-item>
|
||||
<el-menu-item index="/warehouse"><el-icon><Box /></el-icon>仓库库存</el-menu-item>
|
||||
<el-menu-item index="/optimize"><el-icon><Scissor /></el-icon>优化下料</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
@@ -72,6 +72,8 @@
|
||||
<span>客户:{{ detail.customer_name || '-' }}</span>
|
||||
<span>金额:{{ detail.money }}</span>
|
||||
<span>面积:{{ detail.area }}㎡</span>
|
||||
<el-button size="small" type="success" plain @click="quoteRef.open({ order_id: detail.id })">算料报价</el-button>
|
||||
<el-button size="small" @click="printOrder">打印订单</el-button>
|
||||
</div>
|
||||
<div class="canvas-box">
|
||||
<WindoorCanvas :drawing-data="detail.drawing_data" />
|
||||
@@ -92,6 +94,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
<QuoteDialog ref="quoteRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -101,8 +104,12 @@ import { useRoute } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
import WindoorCanvas from '../components/WindoorCanvas.vue'
|
||||
import QuoteDialog from '../components/QuoteDialog.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const quoteRef = ref()
|
||||
|
||||
function printOrder() { window.print() }
|
||||
|
||||
const statusLabels: Record<number, string> = { 0: '意向单', 1: '正式单', 2: '生产中', 3: '已完工', 4: '已取消' }
|
||||
|
||||
|
||||
@@ -28,10 +28,12 @@
|
||||
<div class="meta">
|
||||
<span>尺寸限制: {{ current.low_width }}-{{ current.high_width }} × {{ current.low_height }}-{{ current.high_height }} mm</span>
|
||||
<span>引用次数: {{ current.times }}</span>
|
||||
<el-button type="success" size="small" @click="quoteRef.open({ template_id: current.id })">算料报价</el-button>
|
||||
<el-button type="primary" size="small" @click="useInOrder">用此样板下单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<QuoteDialog ref="quoteRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -40,8 +42,10 @@ import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { api } from '../api'
|
||||
import WindoorCanvas from '../components/WindoorCanvas.vue'
|
||||
import QuoteDialog from '../components/QuoteDialog.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const quoteRef = ref()
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<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="bom_code" label="编码" width="90" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" />
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">{{ materialLabel(row.material) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="spec" label="规格" width="110" />
|
||||
<el-table-column prop="color" label="颜色" width="90" />
|
||||
<el-table-column label="库存" width="110">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ low: row.warning > 0 && row.count <= row.warning }">{{ row.count }}</span>
|
||||
{{ row.unit }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="price" label="单价" width="90" />
|
||||
<el-table-column label="操作" width="170" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="success" plain @click="move(row, 1)">入</el-button>
|
||||
<el-button size="small" type="warning" plain @click="move(row, -1)">出</el-button>
|
||||
<el-button size="small" @click="openEdit(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="520px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<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-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.color" /></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.count" :min="0" /></el-form-item>
|
||||
<el-form-item label="预警线"><el-input-number v-model="form.warning" :min="0" /></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-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const page_size = 15
|
||||
const keyword = ref('')
|
||||
const material = ref<string | undefined>()
|
||||
const loading = ref(false)
|
||||
const editVisible = ref(false)
|
||||
const form = reactive<any>({ id: 0, name: '', material: 'profile', spec: '', color: '', unit: '根', price: 0, count: 0, warning: 0 })
|
||||
|
||||
function materialLabel(m: string) {
|
||||
return ({ profile: '型材', glass: '玻璃', hardware: '五金', addon: '配件' } as Record<string, string>)[m] ?? m
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const qs = `page=${page.value}&page_size=${page_size}&keyword=${keyword.value}${material.value ? `&material=${material.value}` : ''}`
|
||||
const d = await api<any>('get', `/api/warehouse/list?${qs}`)
|
||||
rows.value = d.data
|
||||
total.value = d.total
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function openEdit(row?: any) {
|
||||
Object.assign(form, row ?? { id: 0, name: '', material: 'profile', spec: '', color: '', unit: '根', price: 0, count: 0, warning: 0 })
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (form.id) await api('put', `/api/warehouse/${form.id}`, form)
|
||||
else await api('post', '/api/warehouse', form)
|
||||
ElMessage.success('已保存')
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
|
||||
async function move(row: any, direction: number) {
|
||||
const { value } = await ElMessageBox.prompt(
|
||||
`${row.name} ${direction === 1 ? '入库' : '出库'}数量(${row.unit})`, '出入库',
|
||||
{ inputValue: '1', inputPattern: /^\d+(\.\d+)?$/, inputErrorMessage: '请输入数字' })
|
||||
await api('post', '/api/warehouse/move', { item_id: row.id, direction, count: Number(value), note: '页面操作' })
|
||||
ElMessage.success('操作成功')
|
||||
load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
.low { color: #f56c6c; font-weight: 700; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user