feat: v0.7 全功能复刻与前端适配
后端: - 产品价格本模块(型材/玻璃/五金/配件, 供报价取价) - 报价单模块(BJ单号, 手动报价/样板算料/转订单) - 售后单(问题描述必填, 对齐原站校验)与状态流转 - 量尺单(洞口尺寸清单)与阳光房订单(order_type=1) - 个人中心: 资料/密码修改/邀请返利(HMC邀请码) 前端: - 新增8页面: 产品/报价单/售后/量尺/阳光房/商城/使用指南/个人中心 - 登录页注册双Tab, 响应式侧边栏(<900px自动折叠) - Element Plus 图标全局注册修复 - 共15个页面覆盖原站核心业务路由
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import 'element-plus/dist/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(createPinia()).use(router).use(ElementPlus, { locale: zhCn }).mount('#app')
|
||||
const app = createApp(App)
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
app.use(createPinia()).use(router).use(ElementPlus, { locale: zhCn }).mount('#app')
|
||||
|
||||
@@ -13,11 +13,19 @@ const router = createRouter({
|
||||
{ path: '', redirect: '/dashboard' },
|
||||
{ path: 'dashboard', component: () => import('../views/DashboardView.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: '报价单' } },
|
||||
{ path: 'contracts', component: () => import('../views/ContractsView.vue'), meta: { title: '合同管理' } },
|
||||
{ path: 'customers', component: () => import('../views/CustomersView.vue'), meta: { title: '客户管理' } },
|
||||
{ path: 'measure', component: () => import('../views/MeasureView.vue'), meta: { title: '量尺管理' } },
|
||||
{ path: 'aftersale', component: () => import('../views/AfterSaleView.vue'), meta: { title: '售后管理' } },
|
||||
{ path: 'templates', component: () => import('../views/TemplatesView.vue'), meta: { title: '样板库' } },
|
||||
{ path: 'products', component: () => import('../views/ProductsView.vue'), meta: { title: '产品管理' } },
|
||||
{ path: 'warehouse', component: () => import('../views/WarehouseView.vue'), meta: { title: '仓库库存' } },
|
||||
{ path: 'optimize', component: () => import('../views/OptimizeView.vue'), meta: { title: '优化下料' } },
|
||||
{ path: 'market', component: () => import('../views/MarketView.vue'), meta: { title: '商城' } },
|
||||
{ path: 'guide', component: () => import('../views/GuideView.vue'), meta: { title: '使用指南' } },
|
||||
{ path: 'personal', component: () => import('../views/PersonalView.vue'), meta: { title: '个人中心' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<div class="toolbar">
|
||||
<el-select v-model="status" placeholder="全部状态" clearable style="width:140px" @change="load">
|
||||
<el-option :value="0" label="待处理" /><el-option :value="1" label="处理中" /><el-option :value="2" label="已完成" />
|
||||
</el-select>
|
||||
<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="id" label="ID" width="70" />
|
||||
<el-table-column prop="title" label="标题" min-width="120" />
|
||||
<el-table-column prop="description" label="问题描述" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="order_no" label="关联订单" width="150" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="(['warning', 'primary', 'success'] as const)[row.status]">
|
||||
{{ ['待处理', '处理中', '已完成'][row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="提交时间" width="170" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status !== 2" size="small" @click="advance(row)">
|
||||
{{ row.status === 0 ? '开始处理' : '完成' }}</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="createVisible" title="提交售后" width="520px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="标题"><el-input v-model="form.title" /></el-form-item>
|
||||
<el-form-item label="问题描述"><el-input v-model="form.description" type="textarea" :rows="3" /></el-form-item>
|
||||
<el-form-item label="关联订单"><el-input v-model="form.order_no" placeholder="订单号(可选)" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createVisible = 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 } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const page_size = 15
|
||||
const status = ref<number | undefined>()
|
||||
const loading = ref(false)
|
||||
const createVisible = ref(false)
|
||||
const form = reactive({ title: '', description: '', order_no: '' })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const qs = `page=${page.value}&page_size=${page_size}${status.value !== undefined ? `&status=${status.value}` : ''}`
|
||||
const d = await api<any>('get', `/api/after_sales?${qs}`)
|
||||
rows.value = d.data
|
||||
total.value = d.total
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(form, { title: '', description: '', order_no: '' })
|
||||
createVisible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.description.trim()) return ElMessage.warning('问题描述必填')
|
||||
await api('post', '/api/after_sales', form)
|
||||
ElMessage.success('已提交')
|
||||
createVisible.value = false
|
||||
load()
|
||||
}
|
||||
|
||||
async function advance(row: any) {
|
||||
const next = row.status === 0 ? 1 : 2
|
||||
await api('put', `/api/after_sales/${row.id}`, { status: next })
|
||||
ElMessage.success('状态已更新')
|
||||
load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<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-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</template>
|
||||
@@ -1,25 +1,36 @@
|
||||
<template>
|
||||
<el-container class="layout">
|
||||
<el-aside width="200px">
|
||||
<div class="logo">华门窗</div>
|
||||
<el-menu :default-active="route.path" router background-color="#2b3a4d" text-color="#bfc9d4"
|
||||
active-text-color="#ffffff">
|
||||
<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="/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="/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-aside>
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<span class="title">{{ route.meta.title }}</span>
|
||||
<el-dropdown @command="cmd => cmd === 'logout' && doLogout()">
|
||||
<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>
|
||||
</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="logout">退出登录</el-dropdown-item>
|
||||
<el-dropdown-item command="personal">个人中心</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu></template>
|
||||
</el-dropdown>
|
||||
</el-header>
|
||||
@@ -29,23 +40,29 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const collapsed = ref(window.innerWidth < 900)
|
||||
watch(() => window.innerWidth, w => { if (w < 900) collapsed.value = true })
|
||||
function doLogout() { auth.logout(); router.push('/login') }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout { height: 100vh; }
|
||||
.el-aside { background: #2b3a4d; }
|
||||
.logo { color: #fff; font-size: 20px; font-weight: 700; text-align: center; padding: 20px 0; letter-spacing: 4px; }
|
||||
.aside { background: #2b3a4d; 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; }
|
||||
.el-menu { border-right: none; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center;
|
||||
background: #fff; border-bottom: 1px solid #e4e7ed; }
|
||||
background: #fff; border-bottom: 1px solid #e4e7ed; padding: 0 16px; }
|
||||
.header-left { display: flex; align-items: center; gap: 12px; }
|
||||
.fold-btn { cursor: pointer; font-size: 18px; color: #606266; }
|
||||
.title { font-size: 16px; font-weight: 600; color: #303133; }
|
||||
.user { cursor: pointer; color: #606266; display: inline-flex; gap: 6px; align-items: center; }
|
||||
.el-main { background: #f2f4f7; padding: 16px; }
|
||||
@media (max-width: 700px) { .el-main { padding: 8px; } }
|
||||
</style>
|
||||
|
||||
@@ -3,18 +3,34 @@
|
||||
<el-card class="login-card">
|
||||
<h2>华门窗云平台</h2>
|
||||
<p class="sub">画图 · 报价 · 订单 · 生产</p>
|
||||
<el-form :model="form" @keyup.enter="doLogin">
|
||||
<el-form-item>
|
||||
<el-input v-model="form.phone_number" placeholder="手机号" size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="form.password" type="password" placeholder="密码" size="large" show-password />
|
||||
</el-form-item>
|
||||
<el-button type="primary" size="large" style="width:100%" :loading="loading" @click="doLogin">
|
||||
登 录
|
||||
</el-button>
|
||||
</el-form>
|
||||
<p class="tip">开发账号:13800000000 / 123456</p>
|
||||
<el-tabs v-model="tab" stretch>
|
||||
<el-tab-pane label="登录" name="login">
|
||||
<el-form :model="loginForm" @keyup.enter="doLogin">
|
||||
<el-form-item>
|
||||
<el-input v-model="loginForm.phone_number" placeholder="手机号" size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="loginForm.password" type="password" placeholder="密码" size="large" show-password />
|
||||
</el-form-item>
|
||||
<el-button type="primary" size="large" style="width:100%" :loading="loading" @click="doLogin">登 录</el-button>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="注册" name="register">
|
||||
<el-form :model="regForm" @keyup.enter="doRegister">
|
||||
<el-form-item>
|
||||
<el-input v-model="regForm.phone_number" placeholder="手机号" size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="regForm.password" type="password" placeholder="设置密码(至少4位)" size="large" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="regForm.invite_code" placeholder="邀请码(可选)" size="large" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" size="large" style="width:100%" :loading="loading" @click="doRegister">注 册</el-button>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<p class="tip">开发账号:13800000000 / 123456 | 忘记密码请联系管理员</p>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -29,19 +45,36 @@ import { useAuthStore } from '../stores/auth'
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(false)
|
||||
const form = reactive({ phone_number: '', password: '' })
|
||||
const tab = ref('login')
|
||||
const loginForm = reactive({ phone_number: '', password: '' })
|
||||
const regForm = reactive({ phone_number: '', password: '', invite_code: '' })
|
||||
|
||||
async function doLogin() {
|
||||
if (!form.phone_number || !form.password) return ElMessage.warning('请输入手机号和密码')
|
||||
if (!loginForm.phone_number || !loginForm.password) return ElMessage.warning('请输入手机号和密码')
|
||||
loading.value = true
|
||||
try {
|
||||
const d = await api<{ token: string; user: { nickname: string } }>('post', '/api/auth/login', form)
|
||||
const d = await api<{ token: string; user: { nickname: string } }>('post', '/api/auth/login', loginForm)
|
||||
auth.setSession(d.token, d.user?.nickname ?? '')
|
||||
router.push('/dashboard')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function doRegister() {
|
||||
if (!regForm.phone_number || !regForm.password) return ElMessage.warning('请输入手机号和密码')
|
||||
loading.value = true
|
||||
try {
|
||||
const d = await api<{ token: string; user_id: number }>('post', '/api/auth/register', {
|
||||
phone_number: regForm.phone_number, password: regForm.password,
|
||||
})
|
||||
auth.setSession(d.token, regForm.phone_number.slice(-4))
|
||||
ElMessage.success('注册成功')
|
||||
router.push('/dashboard')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -49,6 +82,6 @@ async function doLogin() {
|
||||
background: linear-gradient(135deg, #35506b 0%, #6b8cae 100%); }
|
||||
.login-card { width: 380px; text-align: center; padding: 12px 8px; }
|
||||
h2 { margin: 8px 0 4px; color: #303133; }
|
||||
.sub { color: #909399; font-size: 13px; margin: 0 0 20px; }
|
||||
.sub { color: #909399; font-size: 13px; margin: 0 0 12px; }
|
||||
.tip { color: #c0c4cc; font-size: 12px; margin-top: 14px; }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :sm="12" :md="8" v-for="m in modules" :key="m.key">
|
||||
<el-card shadow="hover" class="module">
|
||||
<div class="m-head">
|
||||
<span class="m-name">{{ m.name }}</span>
|
||||
<el-tag size="small" :type="m.enabled ? 'success' : 'info'">{{ m.enabled ? '已开通' : '未开通' }}</el-tag>
|
||||
</div>
|
||||
<p class="m-desc">{{ m.desc }}</p>
|
||||
<div class="m-foot">
|
||||
<span class="m-price">{{ m.price > 0 ? `¥${m.price}/年` : '免费' }}</span>
|
||||
<el-button size="small" :type="m.enabled ? 'default' : 'primary'" :disabled="m.enabled"
|
||||
@click="buy(m)">{{ m.enabled ? '已开通' : '立即购买' }}</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
|
||||
const modules = ref<any[]>([])
|
||||
|
||||
// 全部功能对演示账户开放(后端 HMC_ENABLED_MODULES 为准)
|
||||
const MODULE_DEFS = [
|
||||
{ key: 'order', name: '订单管理', desc: '订单全流程:报价下单、生产进度、图纸管理', price: 299 },
|
||||
{ key: 'quote', name: '智能报价', desc: '画图自动算料报价,型材/玻璃/五金明细', price: 199 },
|
||||
{ key: 'optimize', name: '优化下料', desc: '一维型材优化切割,提高利用率', price: 199 },
|
||||
{ key: 'template', name: '样板库', desc: '254 套官方窗型样板 + 自定义样板', price: 0 },
|
||||
{ key: 'warehouse', name: '仓库库存', desc: '材料出入库、库存预警', price: 99 },
|
||||
{ key: 'render', name: '效果图渲染', desc: '照片级渲染(规划中)', price: 399 },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
let enabled: string[] = []
|
||||
try {
|
||||
const u = await api<any>('get', '/api/auth/current-user')
|
||||
enabled = u.enabled_modules ?? MODULE_DEFS.map(m => m.key)
|
||||
} catch { enabled = [] }
|
||||
modules.value = MODULE_DEFS.map(m => ({ ...m, enabled: enabled.includes(m.key) }))
|
||||
})
|
||||
|
||||
async function buy(m: any) {
|
||||
await ElMessageBox.confirm(
|
||||
`开通「${m.name}」需要 ¥${m.price}/年。演示环境未接入支付,可联系管理员在 HMC_ENABLED_MODULES 中开通。`,
|
||||
'购买', { type: 'info' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.module { margin-bottom: 16px; }
|
||||
.m-head { display: flex; justify-content: space-between; align-items: center; }
|
||||
.m-name { font-size: 16px; font-weight: 600; }
|
||||
.m-desc { color: #909399; font-size: 13px; min-height: 38px; margin: 10px 0; }
|
||||
.m-foot { display: flex; justify-content: space-between; align-items: center; }
|
||||
.m-price { color: #e6483d; font-weight: 700; }
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<div class="toolbar">
|
||||
<el-select v-model="status" placeholder="全部状态" clearable style="width:140px" @change="load">
|
||||
<el-option :value="0" label="待量尺" /><el-option :value="1" label="已量尺" /><el-option :value="2" label="已出图" />
|
||||
</el-select>
|
||||
<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="id" label="ID" width="70" />
|
||||
<el-table-column prop="customer_name" label="客户" min-width="110" />
|
||||
<el-table-column prop="phone" label="电话" width="130" />
|
||||
<el-table-column prop="address" label="地址" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="measure_date" label="量尺时间" width="160" />
|
||||
<el-table-column prop="surveyor" label="量尺人" width="90" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="(['warning', 'primary', 'success'] as const)[row.status]">
|
||||
{{ ['待量尺', '已量尺', '已出图'][row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="洞口数据" width="90">
|
||||
<template #default="{ row }">{{ (row.data || []).length }} 项</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<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="量尺单" width="680px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12"><el-form-item label="客户"><el-input v-model="form.customer_name" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="电话"><el-input v-model="form.phone" /></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="地址"><el-input v-model="form.address" /></el-form-item></el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="量尺时间">
|
||||
<el-date-picker v-model="form.measure_date" type="datetime" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12"><el-form-item label="量尺人"><el-input v-model="form.surveyor" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item label="洞口尺寸">
|
||||
<div style="width:100%">
|
||||
<div v-for="(d, i) in form.data" :key="i" class="row">
|
||||
<el-input v-model="d.name" placeholder="位置名" style="width:130px" />
|
||||
<el-input-number v-model="d.width" :min="0" placeholder="宽" controls-position="right" style="width:110px" />
|
||||
<el-input-number v-model="d.height" :min="0" placeholder="高" controls-position="right" style="width:110px" />
|
||||
<el-input-number v-model="d.qty" :min="1" placeholder="数量" controls-position="right" style="width:90px" />
|
||||
<el-button text type="danger" @click="form.data.splice(i, 1)">删</el-button>
|
||||
</div>
|
||||
<el-button text type="primary" @click="form.data.push({ name: '', width: 0, height: 0, qty: 1 })">+ 添加洞口</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width:140px">
|
||||
<el-option :value="0" label="待量尺" /><el-option :value="1" label="已量尺" /><el-option :value="2" label="已出图" />
|
||||
</el-select>
|
||||
</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-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const page_size = 15
|
||||
const status = ref<number | undefined>()
|
||||
const loading = ref(false)
|
||||
const editVisible = ref(false)
|
||||
const editingId = ref(0)
|
||||
const form = reactive<any>({ customer_name: '', phone: '', address: '', measure_date: null,
|
||||
surveyor: '', status: 0, data: [], note: '' })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const qs = `page=${page.value}&page_size=${page_size}${status.value !== undefined ? `&status=${status.value}` : ''}`
|
||||
const d = await api<any>('get', `/api/measures?${qs}`)
|
||||
rows.value = d.data
|
||||
total.value = d.total
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function openEdit(row?: any) {
|
||||
editingId.value = row?.id ?? 0
|
||||
Object.assign(form, row ?? { customer_name: '', phone: '', address: '', measure_date: null,
|
||||
surveyor: '', status: 0, data: [], note: '' })
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const body = { ...form, measure_date: form.measure_date ? new Date(form.measure_date).toISOString() : null }
|
||||
if (editingId.value) {
|
||||
await api('put', `/api/measures/${editingId.value}`, body)
|
||||
} else {
|
||||
await api('post', '/api/measures', body)
|
||||
}
|
||||
ElMessage.success('已保存')
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
.row { display: flex; gap: 8px; margin-bottom: 6px; align-items: center; }
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card header="个人资料">
|
||||
<el-form :model="profile" label-width="80px">
|
||||
<el-form-item label="手机号"><el-input v-model="profile.phone_number" disabled /></el-form-item>
|
||||
<el-form-item label="昵称"><el-input v-model="profile.nickname" /></el-form-item>
|
||||
<el-form-item label="邮箱"><el-input v-model="profile.email" /></el-form-item>
|
||||
<el-form-item label="到期时间"><el-input v-model="profile.try_expiration_time" disabled /></el-form-item>
|
||||
<el-button type="primary" @click="saveProfile">保存资料</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card header="修改密码">
|
||||
<el-form :model="pwd" label-width="80px">
|
||||
<el-form-item label="旧密码"><el-input v-model="pwd.old_password" type="password" show-password /></el-form-item>
|
||||
<el-form-item label="新密码"><el-input v-model="pwd.new_password" type="password" show-password /></el-form-item>
|
||||
<el-button type="primary" @click="savePwd">修改密码</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card header="邀请返利" style="margin-top:16px">
|
||||
<p>我的邀请码:<span class="code">{{ invite.invite_code }}</span></p>
|
||||
<p>已邀请:<b>{{ invite.invited_count }}</b> 人 | 奖励余额:<b>¥{{ invite.bounty }}</b></p>
|
||||
<p class="rule">{{ invite.rule }}</p>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
|
||||
const profile = reactive<any>({ phone_number: '', nickname: '', email: '', try_expiration_time: '' })
|
||||
const pwd = reactive({ old_password: '', new_password: '' })
|
||||
const invite = reactive<any>({ invite_code: '', invited_count: 0, bounty: '0.00', rule: '' })
|
||||
|
||||
onMounted(async () => {
|
||||
const u = await api<any>('get', '/api/auth/current-user')
|
||||
Object.assign(profile, u)
|
||||
Object.assign(invite, await api<any>('get', '/api/auth/invitation'))
|
||||
})
|
||||
|
||||
async function saveProfile() {
|
||||
await api('put', '/api/auth/profile', { nickname: profile.nickname, email: profile.email })
|
||||
ElMessage.success('资料已更新')
|
||||
}
|
||||
|
||||
async function savePwd() {
|
||||
await api('put', '/api/auth/password', pwd)
|
||||
ElMessage.success('密码已修改')
|
||||
pwd.old_password = pwd.new_password = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.code { font-weight: 700; color: #409eff; font-size: 16px; letter-spacing: 1px; }
|
||||
.rule { color: #909399; font-size: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<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-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-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: '', unit: '米', price: 0, cost: 0, note: '' })
|
||||
|
||||
const label = (m: string) => ({ 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/products?${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: '', unit: '米', price: 0, cost: 0, note: '' })
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (form.id) await api('put', `/api/products/${form.id}`, form)
|
||||
else await api('post', '/api/products', form)
|
||||
ElMessage.success('已保存')
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
|
||||
async function remove(row: any) {
|
||||
await ElMessageBox.confirm(`确定删除「${row.name}」?`, '提示', { type: 'warning' })
|
||||
await api('delete', `/api/products/${row.id}`)
|
||||
ElMessage.success('已删除')
|
||||
load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" placeholder="报价单号/客户" clearable style="width:220px" @keyup.enter="load" />
|
||||
<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="quote_no" label="报价单号" width="150" />
|
||||
<el-table-column prop="customer_name" label="客户" min-width="110" />
|
||||
<el-table-column label="窗型尺寸" width="150">
|
||||
<template #default="{ row }">{{ row.width_mm }} × {{ row.height_mm }} mm</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="glass_area" label="玻璃㎡" width="90" />
|
||||
<el-table-column prop="total" label="总价" width="110" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="(['info', 'primary', 'success', 'danger'] as const)[row.status]">
|
||||
{{ ['草稿', '已发送', '已成交', '已作废'][row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="170" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="detail(row)">详情</el-button>
|
||||
<el-button size="small" type="success" plain @click="toOrder(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="createVisible" title="手动报价" width="640px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="客户">
|
||||
<el-select v-model="form.customer_id" filterable clearable style="width:100%"
|
||||
placeholder="选择客户" @change="onCustomer">
|
||||
<el-option v-for="c in customerOptions" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="选样板算料">
|
||||
<el-select v-model="form.template_id" filterable clearable style="width:100%"
|
||||
placeholder="选择样板自动算料(可再手动调整)" @change="calcTemplate">
|
||||
<el-option v-for="t in templateOptions" :key="t.id" :label="t.name || `样板#${t.id}`" :value="t.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="明细">
|
||||
<el-table :data="form.items" size="small" border style="width:100%">
|
||||
<el-table-column label="项目" width="110">
|
||||
<template #default="{ row }"><el-input v-model="row.name" size="small" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" width="110">
|
||||
<template #default="{ row }"><el-input-number v-model="row.qty" :min="0" size="small" controls-position="right" style="width:90px" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单价" width="110">
|
||||
<template #default="{ row }"><el-input-number v-model="row.unit_price" :min="0" :precision="2" size="small" controls-position="right" style="width:90px" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="90">
|
||||
<template #default="{ row }">{{ (row.qty * row.unit_price).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="60">
|
||||
<template #default="{ $index }"><el-button text type="danger" size="small" @click="form.items.splice($index, 1)">删</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-button text type="primary" size="small" style="margin-top:6px"
|
||||
@click="form.items.push({ name: '', qty: 1, unit_price: 0 })">+ 添加行</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="总价">
|
||||
<span class="total">{{ totalCalc }} 元</span>
|
||||
</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="createVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存报价单</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情 -->
|
||||
<el-dialog v-model="detailVisible" :title="current?.quote_no" width="560px">
|
||||
<el-descriptions v-if="current" :column="2" border size="small">
|
||||
<el-descriptions-item label="客户">{{ current.customer_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总价">{{ current.total }} 元</el-descriptions-item>
|
||||
<el-descriptions-item label="尺寸">{{ current.width_mm }} × {{ current.height_mm }} mm</el-descriptions-item>
|
||||
<el-descriptions-item label="玻璃">{{ current.glass_area }} ㎡</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table v-if="current" :data="current.items" size="small" border style="margin-top:10px">
|
||||
<el-table-column prop="name" label="项目" />
|
||||
<el-table-column prop="qty" label="数量" width="80" />
|
||||
<el-table-column prop="unit_price" label="单价" width="90" />
|
||||
<el-table-column prop="amount" label="金额" width="90" />
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
|
||||
const router = useRouter()
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const page_size = 15
|
||||
const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
const createVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const current = ref<any>(null)
|
||||
const customerOptions = ref<any[]>([])
|
||||
const templateOptions = ref<any[]>([])
|
||||
const form = reactive<any>({ customer_id: undefined, customer_name: '', contract_id: null, template_id: undefined,
|
||||
drawing_data: '', items: [], glass_area: 0, width_mm: 0, height_mm: 0, total: 0, note: '', status: 0 })
|
||||
|
||||
const totalCalc = computed(() => form.items.reduce((s: number, r: any) => s + (r.qty || 0) * (r.unit_price || 0), 0).toFixed(2))
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const d = await api<any>('get', `/api/quotations?page=${page.value}&page_size=${page_size}&keyword=${keyword.value}`)
|
||||
rows.value = d.data
|
||||
total.value = d.total
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function openCreate() {
|
||||
Object.assign(form, { customer_id: undefined, customer_name: '', contract_id: null, template_id: undefined,
|
||||
drawing_data: '', items: [], glass_area: 0, width_mm: 0, height_mm: 0, total: 0, note: '', status: 0 })
|
||||
const [cs, ts] = await Promise.all([
|
||||
api<any>('get', '/api/customers?page=1&page_size=100'),
|
||||
api<any>('get', '/api/windoor/templates?page=1&page_size=254'),
|
||||
])
|
||||
customerOptions.value = cs.data
|
||||
templateOptions.value = ts
|
||||
createVisible.value = true
|
||||
}
|
||||
|
||||
function onCustomer(cid: number | undefined) {
|
||||
const c = customerOptions.value.find(x => x.id === cid)
|
||||
if (c) form.customer_name = c.name
|
||||
}
|
||||
|
||||
async function calcTemplate(tid: number | undefined) {
|
||||
if (!tid) return
|
||||
const r = await api<any>('post', '/api/offer-price', { template_id: tid })
|
||||
form.items = r.rows.map((row: any) => ({
|
||||
name: row.note || row.kind, qty: row.qty, unit_price: row.unit_price, amount: row.amount,
|
||||
}))
|
||||
form.glass_area = r.glass_area
|
||||
form.width_mm = r.width_mm
|
||||
form.height_mm = r.height_mm
|
||||
form.total = r.total
|
||||
form.drawing_data = ''
|
||||
const t = await api<any>('get', `/api/windoor/templates/${tid}`)
|
||||
form.drawing_data = t.drawing_data
|
||||
}
|
||||
|
||||
async function save() {
|
||||
form.total = Number(totalCalc.value)
|
||||
await api('post', '/api/quotations', form)
|
||||
ElMessage.success('报价单已保存')
|
||||
createVisible.value = false
|
||||
load()
|
||||
}
|
||||
|
||||
async function detail(row: any) {
|
||||
current.value = await api<any>('get', `/api/quotations/${row.id}`)
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
async function toOrder(row: any) {
|
||||
await ElMessageBox.confirm(`将报价单 ${row.quote_no} 转为订单?`, '提示')
|
||||
await api('post', '/api/orders', { customer_id: row.customer_id, money: row.total,
|
||||
drawing_data: '', note: `来自报价单${row.quote_no}` })
|
||||
ElMessage.success('订单已创建')
|
||||
router.push('/orders')
|
||||
}
|
||||
|
||||
async function remove(row: any) {
|
||||
await ElMessageBox.confirm(`确定删除 ${row.quote_no}?`, '提示', { type: 'warning' })
|
||||
await api('delete', `/api/quotations/${row.id}`)
|
||||
ElMessage.success('已删除')
|
||||
load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
.total { color: #e6483d; font-size: 18px; font-weight: 700; }
|
||||
</style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<div class="toolbar">
|
||||
<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="customer_name" label="客户" min-width="110" />
|
||||
<el-table-column prop="money" label="金额" width="110" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="(['info', 'primary', 'warning', 'success', 'danger'] as const)[row.status]">
|
||||
{{ ['意向单', '正式单', '生产中', '已完工', '已取消'][row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="170" />
|
||||
</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="createVisible" title="新建阳光房订单" width="480px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="客户">
|
||||
<el-select v-model="form.customer_id" filterable clearable style="width:100%" @change="onCustomer">
|
||||
<el-option v-for="c in customerOptions" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="金额"><el-input-number v-model="form.money" :min="0" :precision="2" /></el-form-item>
|
||||
<el-form-item label="面积㎡"><el-input-number v-model="form.area" :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="createVisible = 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 } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const page_size = 15
|
||||
const loading = ref(false)
|
||||
const createVisible = ref(false)
|
||||
const customerOptions = ref<any[]>([])
|
||||
const form = reactive<any>({ customer_id: undefined, money: 0, area: 0, note: '' })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const d = await api<any>('get', `/api/sunny_house_orders?page=${page.value}&page_size=${page_size}`)
|
||||
rows.value = d.data
|
||||
total.value = d.total
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function openCreate() {
|
||||
Object.assign(form, { customer_id: undefined, money: 0, area: 0, note: '' })
|
||||
customerOptions.value = (await api<any>('get', '/api/customers?page=1&page_size=100')).data
|
||||
createVisible.value = true
|
||||
}
|
||||
|
||||
function onCustomer(cid: number | undefined) {
|
||||
form.customer_name = customerOptions.value.find(x => x.id === cid)?.name ?? ''
|
||||
}
|
||||
|
||||
async function save() {
|
||||
await api('post', '/api/orders', { ...form, order_type: 1 })
|
||||
ElMessage.success('阳光房订单已创建')
|
||||
createVisible.value = false
|
||||
load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user