feat: v0.9 样板数据完整性验证/解析加固/报表统计
- 双源确认样板库完整性: search接口与画图编辑器for_ht目录均为254条, 0遗漏 - 全量体检: 6套样板含7个畸形段(缺ps/pe)导致解析报错 前后端重放/算料统一加固过滤, 254样板0异常/1046窗格 - 报表统计模块(/chart): 近N天订单数金额按日柱图+8项总量+状态分布 - 样板库官方/我的样板双Tab(is_default过滤) - 使用指南接入帮助文章接口(种子8篇), seed分类树幂等修复 - 全接口回归18/18通过
This commit is contained in:
@@ -12,6 +12,7 @@ const router = createRouter({
|
||||
children: [
|
||||
{ path: '', redirect: '/dashboard' },
|
||||
{ path: 'dashboard', component: () => import('../views/DashboardView.vue'), meta: { title: '工作台' } },
|
||||
{ path: 'chart', component: () => import('../views/ReportView.vue'), meta: { title: '报表统计' } },
|
||||
{ path: 'orders', component: () => import('../views/OrdersView.vue'), meta: { title: '订单管理' } },
|
||||
{ path: 'sunny', component: () => import('../views/SunnyHouseView.vue'), meta: { title: '阳光房' } },
|
||||
{ path: 'quotations', component: () => import('../views/QuotationsView.vue'), meta: { title: '报价单' } },
|
||||
|
||||
@@ -141,7 +141,11 @@ export function replayShape(shape: FrameShape): {
|
||||
regions: Segment[][]
|
||||
clips: { sp: SplitLine; bb: { x0: number; y0: number; x1: number; y1: number } }[]
|
||||
} {
|
||||
const base = (shape.polygon?.polygon ?? []).map(r => r.map(s => ({ ps: { ...s.ps }, pe: { ...s.pe } })))
|
||||
const base = (shape.polygon?.polygon ?? [])
|
||||
.map(r => r
|
||||
.filter(s => s && typeof s === 'object' && s.ps && s.pe && Number.isFinite(s.ps.x) && Number.isFinite(s.pe.x))
|
||||
.map(s => ({ ps: { ...s.ps }, pe: { ...s.pe } })))
|
||||
.filter(r => r.length >= 3)
|
||||
const regions: Segment[][] = base.map(r => r)
|
||||
const clips: { sp: SplitLine; bb: { x0: number; y0: number; x1: number; y1: number } }[] = []
|
||||
for (const sp of (shape.mm?.sp ?? []) as SplitLine[]) {
|
||||
|
||||
@@ -2,29 +2,25 @@
|
||||
<el-card>
|
||||
<el-alert type="info" :closable="false" show-icon
|
||||
title="常用操作指南" description="点击左侧菜单进入各功能模块;画布支持滚轮缩放、拖拽平移、双击复位。" />
|
||||
<el-collapse style="margin-top:16px">
|
||||
<el-collapse-item title="如何创建订单?">
|
||||
<p>方式一:合同管理 → 选择合同 → 「下订单」。</p>
|
||||
<p>方式二:订单管理 → 新建订单 → 选择合同与样板(自动带入图纸)→ 创建。</p>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="如何编辑窗型?">
|
||||
<p>样板库:点击样板 → 「编辑分格/尺寸」;订单详情 → 「编辑分格/尺寸」。</p>
|
||||
<p>- 改 W/H 输入框或拖拽外框右下角手柄:整体按比例缩放(型材料宽不变)。</p>
|
||||
<p>- +竖梃 / +横梃:点击玻璃区域完成分割。</p>
|
||||
<p>- 点击已有中梃(变蓝)→ 拖动调整位置;「删除梃」移除。</p>
|
||||
<p>- 任何改动后自动重算报价;「保存图纸」写回订单,「另存为我的样板」生成新样板。</p>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="如何使用优化下料?">
|
||||
<p>优化下料 → 录入型材需求(长度×数量)与库存原料 → 开始优化。</p>
|
||||
<p>系统按首递减算法输出每根原料的切割组合、余料与利用率。</p>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="报价如何计算?">
|
||||
<p>报价 = 外框型材 + 中梃型材(按实际跨度)+ 扇框 + 玻璃面积 + 五金(按扇数)。</p>
|
||||
<p>默认价格可在产品管理中维护,报价时按价格本取价。</p>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="订单状态流转">
|
||||
<p>意向单 → 正式单 → 生产中 → 已完工;未完工可取消。生产进度按 下料/组装/玻璃/打胶/五金/质检/完工 七节点推进。</p>
|
||||
<el-collapse style="margin-top:16px" v-loading="loading">
|
||||
<el-collapse-item v-for="a in articles" :key="a.id" :title="a.title">
|
||||
<p>{{ a.content }}</p>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { api } from '../api'
|
||||
|
||||
const articles = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
articles.value = await api<any[]>('get', '/api/articles')
|
||||
} finally { loading.value = false }
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<el-menu :default-active="route.path" router :collapse="collapsed" background-color="#2b3a4d"
|
||||
text-color="#bfc9d4" active-text-color="#ffffff">
|
||||
<el-menu-item index="/dashboard"><el-icon><Odometer /></el-icon>工作台</el-menu-item>
|
||||
<el-menu-item index="/chart"><el-icon><TrendCharts /></el-icon>报表统计</el-menu-item>
|
||||
<el-menu-item index="/orders"><el-icon><Tickets /></el-icon>订单管理</el-menu-item>
|
||||
<el-menu-item index="/sunny"><el-icon><OfficeBuilding /></el-icon>阳光房</el-menu-item>
|
||||
<el-menu-item index="/quotations"><el-icon><PriceTag /></el-icon>报价单</el-menu-item>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="head">
|
||||
<span>经营报表(近 {{ days }} 天)</span>
|
||||
<el-radio-group v-model="days" size="small" @change="load">
|
||||
<el-radio-button :value="7">近7天</el-radio-button>
|
||||
<el-radio-button :value="14">近14天</el-radio-button>
|
||||
<el-radio-button :value="30">近30天</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-row :gutter="12">
|
||||
<el-col :xs="12" :sm="8" :md="6" v-for="t in totalsCards" :key="t.label">
|
||||
<div class="stat"><span class="num">{{ t.value }}</span><span class="label">{{ t.label }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<h4 class="sec">每日订单量 / 金额</h4>
|
||||
<div class="chart">
|
||||
<div class="col" v-for="d in byDay" :key="d.date">
|
||||
<div class="bars">
|
||||
<div class="bar money" :style="{ height: moneyH(d.money) + 'px' }" :title="`¥${d.money}`" />
|
||||
<div class="bar count" :style="{ height: countH(d.count) + 'px' }" :title="`${d.count} 单`" />
|
||||
</div>
|
||||
<span class="dlabel">{{ d.date.slice(5) }}</span>
|
||||
</div>
|
||||
<div v-if="!byDay.length" class="empty">近期暂无订单</div>
|
||||
</div>
|
||||
<div class="legend no-print">
|
||||
<span><i class="dot money" /> 金额(高度按金额比例)</span>
|
||||
<span><i class="dot count" /> 订单数</span>
|
||||
</div>
|
||||
|
||||
<h4 class="sec">订单状态分布</h4>
|
||||
<el-space wrap>
|
||||
<el-tag v-for="s in statusDist" :key="s.status" style="margin:2px">
|
||||
{{ statusLabel(s.status) }}:{{ s.count }}</el-tag>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { api } from '../api'
|
||||
|
||||
const days = ref(14)
|
||||
const byDay = ref<any[]>([])
|
||||
const totals = ref<any>({})
|
||||
const statusDist = ref<any[]>([])
|
||||
|
||||
const totalsCards = computed(() => [
|
||||
{ label: '订单总数', value: totals.value.orders ?? 0 },
|
||||
{ label: '意向单', value: totals.value.intentions ?? 0 },
|
||||
{ label: '已完工', value: totals.value.finished ?? 0 },
|
||||
{ label: '报价单', value: totals.value.quotations ?? 0 },
|
||||
{ label: '客户数', value: totals.value.customers ?? 0 },
|
||||
{ label: '合同数', value: totals.value.contracts ?? 0 },
|
||||
{ label: '量尺单', value: totals.value.measures ?? 0 },
|
||||
{ label: '售后处理中', value: totals.value.after_sales ?? 0 },
|
||||
])
|
||||
|
||||
const maxCount = computed(() => Math.max(1, ...byDay.value.map(d => d.count)))
|
||||
const maxMoney = computed(() => Math.max(1, ...byDay.value.map(d => d.money)))
|
||||
const countH = (c: number) => Math.round(c / maxCount.value * 90) + 2
|
||||
const moneyH = (m: number) => Math.round(m / maxMoney.value * 90) + 2
|
||||
|
||||
const statusLabel = (s: number) => ['意向单', '正式单', '生产中', '已完工', '已取消'][s] ?? s
|
||||
|
||||
async function load() {
|
||||
const d = await api<any>('get', `/api/reports/summary?days=${days.value}`)
|
||||
byDay.value = d.by_day
|
||||
totals.value = d.totals
|
||||
statusDist.value = d.status_dist
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.head { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||||
.stat { display: flex; flex-direction: column; align-items: center; padding: 8px 0; }
|
||||
.num { font-size: 26px; font-weight: 700; color: #35506b; }
|
||||
.label { color: #909399; font-size: 12px; }
|
||||
.sec { margin: 18px 0 8px; color: #303133; }
|
||||
.chart { display: flex; gap: 8px; align-items: flex-end; min-height: 120px; padding: 8px; overflow-x: auto; }
|
||||
.col { display: flex; flex-direction: column; align-items: center; gap: 4px; }
|
||||
.bars { display: flex; gap: 3px; align-items: flex-end; height: 96px; }
|
||||
.bar { width: 14px; border-radius: 3px 3px 0 0; }
|
||||
.bar.money { background: #6b8cae; }
|
||||
.bar.count { background: #67c23a; }
|
||||
.dlabel { font-size: 11px; color: #909399; white-space: nowrap; }
|
||||
.legend { display: flex; gap: 16px; font-size: 12px; color: #909399; margin-top: 8px; }
|
||||
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 4px; }
|
||||
.dot.money { background: #6b8cae; }
|
||||
.dot.count { background: #67c23a; }
|
||||
.empty { color: #909399; }
|
||||
</style>
|
||||
@@ -2,13 +2,17 @@
|
||||
<div>
|
||||
<el-card>
|
||||
<div class="toolbar">
|
||||
<el-radio-group v-model="tab" size="small" @change="load">
|
||||
<el-radio-button value="official">官方样板</el-radio-button>
|
||||
<el-radio-button value="mine">我的样板</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-select v-model="catelogId" placeholder="全部分类" clearable style="width:160px" @change="load">
|
||||
<el-option v-for="c in catelogs" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
<el-input v-model="name" placeholder="模板名称" clearable style="width:200px" @keyup.enter="load" />
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<div style="flex:1" />
|
||||
<span class="count">共 {{ total }} 套官方样板</span>
|
||||
<span class="count">共 {{ total }} 套{{ tab === 'mine' ? '我的' : '官方' }}样板</span>
|
||||
</div>
|
||||
<div v-loading="loading" class="grid">
|
||||
<div v-for="t in rows" :key="t.id" class="item" @click="preview(t)">
|
||||
@@ -71,6 +75,7 @@ const editing = ref(false)
|
||||
const dirty = ref(false)
|
||||
const editRef = ref()
|
||||
const liveTotal = ref<number | null>(null)
|
||||
const tab = ref<'official' | 'mine'>('official')
|
||||
let quoteTimer: number | undefined
|
||||
|
||||
// 画布变化(改尺寸/加分格)→ 防抖自动重算报价
|
||||
@@ -90,7 +95,7 @@ function onCanvasChange() {
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const qs = `page=${page.value}&page_size=${page_size}&name=${name.value}${catelogId.value ? `&catelog_id=${catelogId.value}` : ''}`
|
||||
const qs = `page=${page.value}&page_size=${page_size}&name=${name.value}&is_default=${tab.value === 'official' ? 1 : 0}${catelogId.value ? `&catelog_id=${catelogId.value}` : ''}`
|
||||
const d = await api<any>('get', `/api/windoor/templates?${qs}`)
|
||||
rows.value = d
|
||||
// 接口无分页元数据时按返回量估计
|
||||
|
||||
Reference in New Issue
Block a user