feat: v1.1 Element Plus 组件全面升级
- 布局: 面包屑/头像下拉/公告气泡(点击外部关闭)/菜单滚动条/路由切换动效 - 工作台: el-statistic 彩色统计卡/el-skeleton 骨架屏/公告轮播/快捷入口 - 订单: 日期范围筛选/列排序/详情抽屉 el-descriptions+el-tabs+el-timeline 日志 - 样板库: el-tree-select 分类树/el-image 大图预览/el-badge 引用徽标 - 客户表单校验规则/优化下料 dashboard 利用率进度环 - 全接口回归 13/13 通过, SPA 路由验证通过
This commit is contained in:
@@ -24,9 +24,11 @@
|
||||
: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-input v-model="form.phone" /></el-form-item>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="70px">
|
||||
<el-form-item label="姓名" prop="name"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="电话" prop="phone">
|
||||
<el-input v-model="form.phone" maxlength="20" />
|
||||
</el-form-item>
|
||||
<el-form-item label="地址"><el-input v-model="form.address" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="form.note" type="textarea" /></el-form-item>
|
||||
</el-form>
|
||||
@@ -50,7 +52,12 @@ const page_size = 15
|
||||
const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
const editVisible = ref(false)
|
||||
const formRef = ref()
|
||||
const form = reactive({ id: 0, name: '', phone: '', address: '', note: '' })
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入客户姓名', trigger: 'blur' }],
|
||||
phone: [{ pattern: /^[0-9+\-\s]{5,20}$/, message: '电话格式不正确', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
@@ -67,6 +74,7 @@ function openEdit(row?: any) {
|
||||
}
|
||||
|
||||
async function save() {
|
||||
await formRef.value.validate()
|
||||
if (form.id) await api('put', `/api/customers/${form.id}`, form)
|
||||
else await api('post', '/api/customers', form)
|
||||
ElMessage.success('已保存')
|
||||
|
||||
@@ -1,21 +1,65 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 统计卡 -->
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6" v-for="card in cards" :key="card.label">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat"><span class="num">{{ card.value }}</span><span class="label">{{ card.label }}</span></div>
|
||||
<el-col :xs="12" :sm="12" :md="6" v-for="(card, i) in cards" :key="card.label">
|
||||
<el-skeleton :loading="loading" animated style="padding: 8px 0">
|
||||
<template #template>
|
||||
<el-skeleton-item variant="rect" style="height: 98px; border-radius: 10px" />
|
||||
</template>
|
||||
<template #default>
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat">
|
||||
<el-icon class="stat-icon" :style="{ background: card.color }"><component :is="card.icon" /></el-icon>
|
||||
<div class="stat-text">
|
||||
<el-statistic :value="Number(card.value) || 0" :value-style="{ fontSize: '26px', color: card.color }" />
|
||||
<span class="label">{{ card.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" style="margin-top:16px">
|
||||
<el-col :xs="24" :md="14">
|
||||
<el-card>
|
||||
<template #header>快捷入口</template>
|
||||
<div class="quick">
|
||||
<div v-for="q in quick" :key="q.path" class="quick-item" @click="$router.push(q.path)">
|
||||
<el-icon :size="22" :color="q.color"><component :is="q.icon" /></el-icon>
|
||||
<span>{{ q.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card style="margin-top:16px">
|
||||
<template #header>
|
||||
<div class="head-row"><span>最新公告</span><el-button text type="primary" @click="$router.push('/guide')">更多</el-button></div>
|
||||
</template>
|
||||
<el-carousel height="120px" arrow="never" indicator-position="none" v-if="announcements.length">
|
||||
<el-carousel-item v-for="a in announcements.slice(0, 4)" :key="a.id">
|
||||
<div class="ann">
|
||||
<b>{{ a.name }}</b>
|
||||
<p>{{ a.desc }}</p>
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<el-empty v-else description="暂无公告" :image-size="60" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="10">
|
||||
<el-card header="业务导航">
|
||||
<el-menu class="nav-menu" @select="p => $router.push(p)">
|
||||
<el-menu-item index="/templates"><el-icon><Grid /></el-icon>浏览 254 套官方样板库</el-menu-item>
|
||||
<el-menu-item index="/quotations"><el-icon><PriceTag /></el-icon>手动报价单</el-menu-item>
|
||||
<el-menu-item index="/ai_render"><el-icon><MagicStick /></el-icon>AI 炫图生成效果图</el-menu-item>
|
||||
<el-menu-item index="/printer"><el-icon><Printer /></el-icon>打印生产订单</el-menu-item>
|
||||
<el-menu-item index="/guide"><el-icon><QuestionFilled /></el-icon>使用指南</el-menu-item>
|
||||
</el-menu>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-card style="margin-top:16px">
|
||||
<template #header>快捷入口</template>
|
||||
<el-space wrap>
|
||||
<el-button type="primary" @click="$router.push('/templates')">浏览样板库</el-button>
|
||||
<el-button @click="$router.push('/orders')">新建订单</el-button>
|
||||
<el-button @click="$router.push('/contracts')">录入合同</el-button>
|
||||
<el-button @click="$router.push('/optimize')">优化下料</el-button>
|
||||
</el-space>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -23,29 +67,53 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { api } from '../api'
|
||||
|
||||
const loading = ref(true)
|
||||
const announcements = ref<any[]>([])
|
||||
const cards = ref([
|
||||
{ label: '订单总数', value: '-' },
|
||||
{ label: '意向单', value: '-' },
|
||||
{ label: '客户数', value: '-' },
|
||||
{ label: '合同数', value: '-' },
|
||||
{ label: '订单总数', value: 0, color: '#3f5f80', icon: 'Tickets' },
|
||||
{ label: '意向单', value: 0, color: '#e6a23c', icon: 'Timer' },
|
||||
{ label: '客户数', value: 0, color: '#67c23a', icon: 'User' },
|
||||
{ label: '合同数', value: 0, color: '#5b3fd4', icon: 'Document' },
|
||||
])
|
||||
const quick = [
|
||||
{ path: '/templates', label: '样板库', icon: 'Grid', color: '#3f5f80' },
|
||||
{ path: '/orders', label: '新建订单', icon: 'Tickets', color: '#67c23a' },
|
||||
{ path: '/ai_render', label: 'AI 炫图', icon: 'MagicStick', color: '#5b3fd4' },
|
||||
{ path: '/optimize', label: '优化下料', icon: 'Scissor', color: '#e6a23c' },
|
||||
{ path: '/printer', label: '打印', icon: 'Printer', color: '#909399' },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
const [orders, customers, contracts] = await Promise.all([
|
||||
api<any>('post', '/api/orders/search', { page: 1, page_size: 1 }),
|
||||
api<any>('get', '/api/customers?page=1&page_size=1'),
|
||||
api<any>('get', '/api/contracts?page=1&page_size=1'),
|
||||
])
|
||||
cards.value[0].value = orders.total ?? 0
|
||||
cards.value[2].value = customers.total ?? 0
|
||||
cards.value[3].value = contracts.contracts?.total ?? 0
|
||||
const intentions = await api<any>('post', '/api/orders/search', { page: 1, page_size: 1, status: 0 })
|
||||
cards.value[1].value = intentions.total ?? 0
|
||||
try {
|
||||
const [orders, intentions, customers, contracts, anns] = await Promise.all([
|
||||
api<any>('post', '/api/orders/search', { page: 1, page_size: 1 }),
|
||||
api<any>('post', '/api/orders/search', { page: 1, page_size: 1, status: 0 }),
|
||||
api<any>('get', '/api/customers?page=1&page_size=1'),
|
||||
api<any>('get', '/api/contracts?page=1&page_size=1'),
|
||||
api<any>('get', '/api/announcements').catch(() => []),
|
||||
])
|
||||
cards.value[0].value = orders.total ?? 0
|
||||
cards.value[1].value = intentions.total ?? 0
|
||||
cards.value[2].value = customers.total ?? 0
|
||||
cards.value[3].value = contracts.contracts?.total ?? 0
|
||||
announcements.value = anns?.data?.slice(0, 4) ?? anns ?? []
|
||||
} finally { loading.value = false }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat { display: flex; flex-direction: column; align-items: center; padding: 8px 0; }
|
||||
.num { font-size: 30px; font-weight: 700; color: #35506b; }
|
||||
.label { color: #909399; font-size: 13px; margin-top: 4px; }
|
||||
.stat-card :deep(.el-card__body) { padding: 14px; }
|
||||
.stat { display: flex; align-items: center; gap: 14px; }
|
||||
.stat-icon { font-size: 24px; color: #fff; border-radius: 12px; padding: 12px; }
|
||||
.stat-text { display: flex; flex-direction: column; }
|
||||
.label { color: #909399; font-size: 12px; }
|
||||
.quick { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.quick-item { width: 100px; padding: 14px 0; display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||||
border: 1px solid #e4e7ed; border-radius: 10px; cursor: pointer; transition: all .2s; font-size: 13px; color: #606266; }
|
||||
.quick-item:hover { border-color: var(--el-color-primary); color: var(--el-color-primary); box-shadow: var(--shadow-card); }
|
||||
.head-row { display: flex; justify-content: space-between; align-items: center; }
|
||||
.ann { height: 100%; display: flex; flex-direction: column; justify-content: center; padding: 0 8px; }
|
||||
.ann b { color: #303133; font-size: 14px; }
|
||||
.ann p { color: #909399; font-size: 12px; margin: 6px 0 0; }
|
||||
.nav-menu { border-right: none; }
|
||||
</style>
|
||||
|
||||
@@ -2,74 +2,128 @@
|
||||
<el-container class="layout">
|
||||
<el-aside :width="collapsed ? '64px' : '200px'" class="aside">
|
||||
<div class="logo">{{ collapsed ? '门' : '华门窗' }}</div>
|
||||
<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>
|
||||
<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="/measure"><el-icon><ScaleToOriginal /></el-icon>量尺管理</el-menu-item>
|
||||
<el-menu-item index="/aftersale"><el-icon><Service /></el-icon>售后管理</el-menu-item>
|
||||
<el-menu-item index="/templates"><el-icon><Grid /></el-icon>样板库</el-menu-item>
|
||||
<el-menu-item index="/products"><el-icon><Goods /></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-item index="/cut_solution"><el-icon><SetUp /></el-icon>下料方案</el-menu-item>
|
||||
<el-menu-item index="/rendering"><el-icon><Picture /></el-icon>效果图渲染</el-menu-item>
|
||||
<el-menu-item index="/ai_render"><el-icon><MagicStick /></el-icon>AI 炫图</el-menu-item>
|
||||
<el-menu-item index="/threed"><el-icon><View /></el-icon>3D 展示</el-menu-item>
|
||||
<el-menu-item index="/printer"><el-icon><Printer /></el-icon>打印中心</el-menu-item>
|
||||
<el-menu-item index="/devices"><el-icon><Cpu /></el-icon>设备管理</el-menu-item>
|
||||
<el-menu-item index="/market"><el-icon><ShoppingCart /></el-icon>商城</el-menu-item>
|
||||
<el-menu-item index="/guide"><el-icon><QuestionFilled /></el-icon>使用指南</el-menu-item>
|
||||
</el-menu>
|
||||
<el-scrollbar class="menu-scroll">
|
||||
<el-menu :default-active="route.path" router :collapse="collapsed" background-color="#2b3a4d"
|
||||
text-color="#bfc9d4" active-text-color="#ffffff" :collapse-transition="false">
|
||||
<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>
|
||||
<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="/measure"><el-icon><ScaleToOriginal /></el-icon>量尺管理</el-menu-item>
|
||||
<el-menu-item index="/aftersale"><el-icon><Service /></el-icon>售后管理</el-menu-item>
|
||||
<el-menu-item index="/templates"><el-icon><Grid /></el-icon>样板库</el-menu-item>
|
||||
<el-menu-item index="/products"><el-icon><Goods /></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-item index="/cut_solution"><el-icon><SetUp /></el-icon>下料方案</el-menu-item>
|
||||
<el-menu-item index="/rendering"><el-icon><Picture /></el-icon>效果图渲染</el-menu-item>
|
||||
<el-menu-item index="/ai_render"><el-icon><MagicStick /></el-icon>AI 炫图</el-menu-item>
|
||||
<el-menu-item index="/threed"><el-icon><View /></el-icon>3D 展示</el-menu-item>
|
||||
<el-menu-item index="/printer"><el-icon><Printer /></el-icon>打印中心</el-menu-item>
|
||||
<el-menu-item index="/devices"><el-icon><Cpu /></el-icon>设备管理</el-menu-item>
|
||||
<el-menu-item index="/market"><el-icon><ShoppingCart /></el-icon>商城</el-menu-item>
|
||||
<el-menu-item index="/guide"><el-icon><QuestionFilled /></el-icon>使用指南</el-menu-item>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<div class="header-left">
|
||||
<el-icon class="fold-btn" @click="collapsed = !collapsed"><Expand v-if="collapsed" /><Fold v-else /></el-icon>
|
||||
<span class="title">{{ route.meta.title }}</span>
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item>华门窗</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>{{ route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<el-dropdown @command="cmd => cmd === 'logout' ? doLogout() : $router.push('/personal')">
|
||||
<span class="user"><el-icon><UserFilled /></el-icon>{{ auth.nickname || '用户' }}</span>
|
||||
<template #dropdown><el-dropdown-menu>
|
||||
<el-dropdown-item command="personal">个人中心</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu></template>
|
||||
</el-dropdown>
|
||||
<div class="header-right">
|
||||
<el-badge :value="announcements.length" :hidden="!announcements.length" class="bell">
|
||||
<el-tooltip content="公告" placement="bottom">
|
||||
<el-icon size="18" @click="annOpen = !annOpen"><Bell /></el-icon>
|
||||
</el-tooltip>
|
||||
</el-badge>
|
||||
<el-dropdown @command="cmd => cmd === 'logout' ? doLogout() : $router.push('/personal')">
|
||||
<span class="user">
|
||||
<el-avatar :size="28" style="background:#6b8cae">{{ (auth.nickname || '用')[0] }}</el-avatar>
|
||||
{{ auth.nickname || '用户' }}
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown><el-dropdown-menu>
|
||||
<el-dropdown-item command="personal">个人中心</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu></template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<!-- 公告气泡 -->
|
||||
<transition name="fade">
|
||||
<el-card v-if="annOpen" class="ann-pop" shadow="always">
|
||||
<template #header><span style="font-weight:600">平台公告</span></template>
|
||||
<el-empty v-if="!announcements.length" description="暂无公告" :image-size="60" />
|
||||
<div v-for="a in announcements" :key="a.id" class="ann-item">
|
||||
<b>{{ a.name }}</b>
|
||||
<p>{{ a.desc }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</transition>
|
||||
</el-header>
|
||||
<el-main><router-view /></el-main>
|
||||
<el-main>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { api } from '../api'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const collapsed = ref(window.innerWidth < 900)
|
||||
const annOpen = ref(false)
|
||||
const announcements = ref<any[]>([])
|
||||
|
||||
watch(() => window.innerWidth, w => { if (w < 900) collapsed.value = true })
|
||||
watch(() => route.path, () => { annOpen.value = false })
|
||||
function doLogout() { auth.logout(); router.push('/login') }
|
||||
|
||||
onMounted(async () => {
|
||||
try { announcements.value = (await api<any[]>('get', '/api/announcements')) ?? [] } catch { /* 静默 */ }
|
||||
document.addEventListener('click', (e: MouseEvent) => {
|
||||
if (annOpen.value && !(e.target as HTMLElement).closest('.ann-pop') && !(e.target as HTMLElement).closest('.bell')) {
|
||||
annOpen.value = false
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout { height: 100vh; }
|
||||
.aside { background: #2b3a4d; transition: width .2s; overflow-x: hidden; }
|
||||
.aside { background: linear-gradient(180deg, #2b3a4d 0%, #24303f 100%); transition: width .2s; overflow-x: hidden; }
|
||||
.logo { color: #fff; font-size: 20px; font-weight: 700; text-align: center; padding: 20px 0; letter-spacing: 4px; white-space: nowrap; }
|
||||
.menu-scroll { height: calc(100vh - 64px); }
|
||||
.el-menu { border-right: none; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center;
|
||||
background: #fff; border-bottom: 1px solid #e4e7ed; padding: 0 16px; }
|
||||
background: #fff; border-bottom: 1px solid #e4e7ed; padding: 0 16px; position: relative; z-index: 20; }
|
||||
.header-left { display: flex; align-items: center; gap: 12px; }
|
||||
.header-right { display: flex; align-items: center; gap: 18px; }
|
||||
.fold-btn { cursor: pointer; font-size: 18px; color: #606266; }
|
||||
.title { font-size: 16px; font-weight: 600; color: #303133; }
|
||||
.bell { cursor: pointer; color: #606266; display: flex; }
|
||||
.user { cursor: pointer; color: #606266; display: inline-flex; gap: 6px; align-items: center; }
|
||||
.ann-pop { position: absolute; right: 16px; top: 56px; width: 320px; z-index: 30; }
|
||||
.ann-item { padding: 8px 0; border-bottom: 1px dashed #ebeef5; }
|
||||
.ann-item:last-child { border-bottom: none; }
|
||||
.ann-item b { font-size: 13px; color: #303133; }
|
||||
.ann-item p { margin: 4px 0 0; font-size: 12px; color: #909399; }
|
||||
.el-main { background: #f2f4f7; padding: 16px; }
|
||||
@media (max-width: 700px) { .el-main { padding: 8px; } }
|
||||
</style>
|
||||
|
||||
@@ -30,13 +30,23 @@
|
||||
<el-button type="primary" :loading="loading" @click="run">开始优化</el-button>
|
||||
</div>
|
||||
<template v-if="result">
|
||||
<el-descriptions :column="4" border style="margin-top:14px">
|
||||
<el-descriptions-item label="总件数">{{ result.stats.total_pieces }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用料根数">{{ result.stats.stock_bars_used }}</el-descriptions-item>
|
||||
<el-descriptions-item label="利用率">{{ (result.stats.utilization * 100).toFixed(1) }}%</el-descriptions-item>
|
||||
<el-descriptions-item label="未排入">{{ result.unplaced.length }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table :data="result.plans" style="margin-top:14px" border>
|
||||
<div class="result-head">
|
||||
<el-progress type="dashboard" :percentage="Math.round(result.stats.utilization * 100)" :width="110"
|
||||
:color="[{ color: '#f56c6c', percentage: 50 }, { color: '#e6a23c', percentage: 75 }, { color: '#67c23a', percentage: 100 }]">
|
||||
<template #default><span class="util">{{ (result.stats.utilization * 100).toFixed(1) }}%</span><br /><small class="util-label">利用率</small></template>
|
||||
</el-progress>
|
||||
<el-descriptions :column="3" border style="flex:1">
|
||||
<el-descriptions-item label="总件数">{{ result.stats.total_pieces }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用料根数">{{ result.stats.stock_bars_used }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总需求">{{ result.stats.total_demand_mm }} mm</el-descriptions-item>
|
||||
<el-descriptions-item label="已用原料">{{ result.stats.total_stock_mm }} mm</el-descriptions-item>
|
||||
<el-descriptions-item label="锯口损耗">{{ result.stats.kerf_mm }} mm</el-descriptions-item>
|
||||
<el-descriptions-item label="未排入">
|
||||
<el-tag :type="result.unplaced.length ? 'danger' : 'success'" size="small">{{ result.unplaced.length }} 件</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<el-table :data="result.plans" style="margin-top:14px" border size="small">
|
||||
<el-table-column label="原料" width="200">
|
||||
<template #default="{ row }">{{ row.stock_label }}({{ row.stock_length }}mm)</template>
|
||||
</el-table-column>
|
||||
@@ -73,4 +83,7 @@ async function run() {
|
||||
|
||||
<style scoped>
|
||||
.row { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
|
||||
.result-head { display: flex; gap: 20px; align-items: center; flex-wrap: wrap; }
|
||||
.util { font-size: 20px; font-weight: 700; }
|
||||
.util-label { color: #909399; }
|
||||
</style>
|
||||
|
||||
@@ -2,28 +2,32 @@
|
||||
<div>
|
||||
<el-card>
|
||||
<div class="toolbar">
|
||||
<el-input v-model="query.keyword" placeholder="订单号/客户/手机号" clearable style="width:240px"
|
||||
@keyup.enter="load" />
|
||||
<el-select v-model="query.status" placeholder="全部状态" clearable style="width:140px">
|
||||
<el-input v-model="query.keyword" placeholder="订单号/客户/手机号" clearable style="width:220px"
|
||||
@keyup.enter="load">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-model="query.status" placeholder="全部状态" clearable style="width:130px">
|
||||
<el-option v-for="(label, val) in statusLabels" :key="val" :label="label" :value="Number(val)" />
|
||||
</el-select>
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始"
|
||||
end-placeholder="结束" value-format="YYYY-MM-DD" style="width:240px" @change="onDateRange" />
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<div style="flex:1" />
|
||||
<el-button type="primary" plain @click="openCreate">新建订单</el-button>
|
||||
</div>
|
||||
<el-table :data="rows" v-loading="loading" stripe>
|
||||
<el-table-column prop="order_no" label="订单号" width="160" />
|
||||
<el-table-column prop="order_no" label="订单号" width="160" sortable />
|
||||
<el-table-column prop="customer_name" label="客户" min-width="110" />
|
||||
<el-table-column prop="phone" label="电话" width="130" />
|
||||
<el-table-column prop="count" label="数量" width="70" />
|
||||
<el-table-column prop="area" label="面积㎡" width="90" />
|
||||
<el-table-column prop="money" label="金额" width="100" />
|
||||
<el-table-column prop="count" label="数量" width="70" sortable />
|
||||
<el-table-column prop="area" label="面积㎡" width="90" sortable />
|
||||
<el-table-column prop="money" label="金额" width="110" sortable />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="tagType(row.status)" size="small">{{ row.status_label }}</el-tag>
|
||||
<el-tag :type="tagType(row.status)" size="small" effect="light">{{ row.status_label }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="170" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="170" sortable />
|
||||
<el-table-column label="操作" width="290" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openDetail(row)">图纸</el-button>
|
||||
@@ -66,46 +70,64 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 图纸详情 -->
|
||||
<el-drawer v-model="detailVisible" size="62%" :title="detail?.order_no ?? '图纸'">
|
||||
<el-drawer v-model="detailVisible" size="68%" :title="detail?.order_no ?? '图纸'">
|
||||
<div v-if="detail" class="detail">
|
||||
<div class="meta">
|
||||
<el-tag>{{ detail.status_label }}</el-tag>
|
||||
<span>客户:{{ detail.customer_name || '-' }}</span>
|
||||
<span>金额:{{ detail.money }}</span>
|
||||
<span>面积:{{ detail.area }}㎡</span>
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="状态"><el-tag size="small">{{ detail.status_label }}</el-tag></el-descriptions-item>
|
||||
<el-descriptions-item label="客户">{{ detail.customer_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电话">{{ detail.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="金额">¥{{ detail.money }}</el-descriptions-item>
|
||||
<el-descriptions-item label="面积">{{ detail.area }} ㎡</el-descriptions-item>
|
||||
<el-descriptions-item label="数量">{{ detail.count }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="canvas-actions">
|
||||
<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 v-if="detail.drawing_data" ref="editRef" :drawing-data="detail.drawing_data"
|
||||
:editable="editing" @change="onCanvasChange" />
|
||||
<div v-else class="empty-tip">此订单未关联图纸——可从「样板库」选样板下单,或在合同中带图纸创建</div>
|
||||
</div>
|
||||
<div style="margin-top:8px; display:flex; gap:12px; align-items:center">
|
||||
<template v-if="!editing">
|
||||
<el-button size="small" @click="editing = true">编辑分格/尺寸</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="tip">改 W/H 或拖右下角手柄缩放;+竖梃/+横梃点击分割</span>
|
||||
<span class="live-price" v-if="liveTotal !== null">当前报价 ¥{{ liveTotal }}</span>
|
||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
||||
<el-button size="small" type="primary" :disabled="!dirty" @click="saveDrawing">保存图纸</el-button>
|
||||
</template>
|
||||
</div>
|
||||
<el-divider>生产进度</el-divider>
|
||||
<el-steps :active="processActive" align-center>
|
||||
<el-step v-for="p in detail.processes" :key="p.id" :title="p.name"
|
||||
:status="p.status === 2 ? 'success' : p.status === 1 ? 'process' : 'wait'" />
|
||||
</el-steps>
|
||||
<div style="margin-top:12px">
|
||||
<el-select v-model="processForm.name" style="width:120px">
|
||||
<el-option v-for="p in detail.processes" :key="p.name" :label="p.name" :value="p.name" />
|
||||
</el-select>
|
||||
<el-select v-model="processForm.status" style="width:110px">
|
||||
<el-option :value="0" label="未开始" /><el-option :value="1" label="进行中" /><el-option :value="2" label="完成" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="updateProcess">更新进度</el-button>
|
||||
</div>
|
||||
<el-tabs model-value="canvas">
|
||||
<el-tab-pane label="图纸" name="canvas">
|
||||
<div class="canvas-box">
|
||||
<WindoorCanvas v-if="detail.drawing_data" ref="editRef" :drawing-data="detail.drawing_data"
|
||||
:editable="editing" @change="onCanvasChange" />
|
||||
<div v-else class="empty-tip">此订单未关联图纸——可从「样板库」选样板下单,或在合同中带图纸创建</div>
|
||||
</div>
|
||||
<div style="margin-top:8px; display:flex; gap:12px; align-items:center">
|
||||
<template v-if="!editing">
|
||||
<el-button size="small" @click="editing = true">编辑分格/尺寸</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="tip">改 W/H 或拖右下角手柄缩放;+竖梃/+横梃点击分割</span>
|
||||
<span class="live-price" v-if="liveTotal !== null">当前报价 ¥{{ liveTotal }}</span>
|
||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
||||
<el-button size="small" type="primary" :disabled="!dirty" @click="saveDrawing">保存图纸</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="生产进度" name="process">
|
||||
<el-steps :active="processActive" align-center>
|
||||
<el-step v-for="p in detail.processes" :key="p.id" :title="p.name"
|
||||
:status="p.status === 2 ? 'success' : p.status === 1 ? 'process' : 'wait'" />
|
||||
</el-steps>
|
||||
<div style="margin-top:16px">
|
||||
<el-select v-model="processForm.name" style="width:130px">
|
||||
<el-option v-for="p in detail.processes" :key="p.name" :label="p.name" :value="p.name" />
|
||||
</el-select>
|
||||
<el-select v-model="processForm.status" style="width:110px">
|
||||
<el-option :value="0" label="未开始" /><el-option :value="1" label="进行中" /><el-option :value="2" label="完成" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="updateProcess">更新进度</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="操作日志" name="logs">
|
||||
<el-timeline style="padding-left:4px; margin-top:8px">
|
||||
<el-timeline-item v-for="g in logs" :key="g.id" :timestamp="g.created_at" placement="top"
|
||||
:type="g.action === 'delete' ? 'danger' : g.action.includes('status') ? 'primary' : undefined">
|
||||
<b>{{ logActionLabel(g.action) }}</b> — {{ g.detail }}
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-if="!logs.length" description="暂无日志" :image-size="60" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-drawer>
|
||||
<QuoteDialog ref="quoteRef" />
|
||||
@@ -127,7 +149,15 @@ function printOrder() { window.print() }
|
||||
|
||||
const statusLabels: Record<number, string> = { 0: '意向单', 1: '正式单', 2: '生产中', 3: '已完工', 4: '已取消' }
|
||||
|
||||
const query = reactive({ page: 1, page_size: 15, keyword: '', status: undefined as number | undefined })
|
||||
const query = reactive({ page: 1, page_size: 15, keyword: '', status: undefined as number | undefined,
|
||||
start_date: '', end_date: '' })
|
||||
const dateRange = ref<[string, string] | null>(null)
|
||||
|
||||
function onDateRange(v: [string, string] | null) {
|
||||
query.start_date = v?.[0] ?? ''
|
||||
query.end_date = v?.[1] ?? ''
|
||||
load()
|
||||
}
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
@@ -227,8 +257,15 @@ const editing = ref(false)
|
||||
const dirty = ref(false)
|
||||
const editRef = ref()
|
||||
const liveTotal = ref<number | null>(null)
|
||||
const logs = ref<any[]>([])
|
||||
let quoteTimer: number | undefined
|
||||
|
||||
const LOG_ACTIONS: Record<string, string> = {
|
||||
create: '创建订单', update: '更新订单', change_status: '状态变更',
|
||||
'up-order-process': '生产进度', delete: '删除',
|
||||
}
|
||||
function logActionLabel(a: string) { return LOG_ACTIONS[a] ?? a }
|
||||
|
||||
// 画布变化(改尺寸/加分格)→ 防抖自动重算报价
|
||||
function onCanvasChange() {
|
||||
dirty.value = true
|
||||
@@ -250,6 +287,7 @@ async function openDetail(row: any) {
|
||||
dirty.value = false
|
||||
liveTotal.value = null
|
||||
processActive.value = detail.value.processes.filter((p: any) => p.status === 2).length
|
||||
logs.value = await api<any>('get', `/api/orders/${row.id}/logs`)
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<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-tree-select v-model="catelogId" :data="catelogs" node-key="id"
|
||||
:props="{ label: 'name', children: 'children' }" check-strictly default-expand-all
|
||||
clearable style="width:170px" placeholder="全部分类" @change="load" />
|
||||
<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" />
|
||||
@@ -16,7 +16,11 @@
|
||||
</div>
|
||||
<div v-loading="loading" class="grid">
|
||||
<div v-for="t in rows" :key="t.id" class="item" @click="preview(t)">
|
||||
<el-image :src="t.pic_url" fit="contain" lazy class="pic" />
|
||||
<el-badge :value="t.times ? `${t.times}次` : ''" :hidden="!t.times" class="bdg">
|
||||
<el-image :src="t.pic_url" fit="contain" lazy class="pic"
|
||||
:preview-src-list="[t.pic_url]" preview-teleported :hide-on-click-modal="true"
|
||||
@click.stop />
|
||||
</el-badge>
|
||||
<div class="cap">{{ t.name || `样板#${t.id}` }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,8 +141,7 @@ function useInOrder() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const tree = await api<any[]>('get', '/api/windoor/catelogs')
|
||||
catelogs.value = tree.flatMap(t => [t, ...((t.children ?? []) as any[])])
|
||||
catelogs.value = await api<any[]>('get', '/api/windoor/catelogs')
|
||||
load()
|
||||
})
|
||||
</script>
|
||||
@@ -151,6 +154,7 @@ onMounted(async () => {
|
||||
transition: box-shadow .2s; display: flex; flex-direction: column; align-items: center; }
|
||||
.item:hover { box-shadow: 0 2px 12px rgba(0,0,0,.12); }
|
||||
.pic { width: 100%; height: 110px; }
|
||||
.bdg :deep(.el-badge__content) { z-index: 3; }
|
||||
.cap { font-size: 12px; color: #606266; margin-top: 6px; text-align: center; }
|
||||
.preview .canvas-box { height: 60vh; border: 1px solid #e4e7ed; border-radius: 6px; overflow: hidden; }
|
||||
.preview .meta { display: flex; gap: 20px; margin-top: 10px; color: #606266; font-size: 13px; align-items: center; flex-wrap: wrap; }
|
||||
|
||||
Reference in New Issue
Block a user