feat: P1批次4 CRM+仓库域(客户跟进/业务员业绩/余料/领料/采购单)
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<el-tabs v-model="tab">
|
||||
<!-- 客户列表 -->
|
||||
<el-tab-pane label="客户列表" name="list">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" placeholder="姓名/电话/地址" clearable style="width:240px" @keyup.enter="load" />
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
@@ -18,15 +21,60 @@
|
||||
<el-table-column prop="address" label="地址" min-width="180" />
|
||||
<el-table-column prop="salesmen" label="业务员" width="100" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="140" fixed="right">
|
||||
<el-table-column label="操作" width="190" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="primary" plain @click="openFollow(row)">跟进</el-button>
|
||||
<el-button size="small" type="danger" plain @click="remove(row)">删</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination style="margin-top:12px; justify-content:flex-end" background layout="total, prev, pager, next"
|
||||
:total="total" :page-size="page_size" v-model:current-page="page" @current-change="load" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 业务员体系 -->
|
||||
<el-tab-pane label="业务员" name="salesmen">
|
||||
<el-row :gutter="14">
|
||||
<el-col :span="10">
|
||||
<el-card shadow="never" header="业务员名册">
|
||||
<el-table :data="salesmen" size="small" border>
|
||||
<el-table-column prop="name" label="姓名" min-width="80" />
|
||||
<el-table-column prop="phone" label="电话" width="110" />
|
||||
<el-table-column prop="commission_rate" label="提成%" width="70" />
|
||||
<el-table-column label="操作" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" text type="danger" @click="removeSalesman(row)">删</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="sm-add">
|
||||
<el-input v-model="smForm.name" size="small" placeholder="姓名" style="width:80px" />
|
||||
<el-input v-model="smForm.phone" size="small" placeholder="电话" style="width:110px" />
|
||||
<el-input-number v-model="smForm.commission_rate" size="small" :min="0" :max="100" style="width:90px" controls-position="right" />
|
||||
<el-button size="small" type="primary" plain @click="addSalesman">添加</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-card shadow="never" header="业绩统计(名下客户/合同/预估提成)">
|
||||
<el-table :data="smStats" size="small" border>
|
||||
<el-table-column prop="name" label="业务员" width="90" />
|
||||
<el-table-column prop="customer_count" label="客户数" width="80" />
|
||||
<el-table-column prop="contract_count" label="合同数" width="80" />
|
||||
<el-table-column label="合同总额" width="120">
|
||||
<template #default="{ row }">¥{{ row.contract_money }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预估提成">
|
||||
<template #default="{ row }">¥{{ row.commission }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!smStats.length" description="暂无在职业务员" :image-size="50" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="editVisible" :title="form.id ? '编辑客户' : '新增客户'" width="480px">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="70px">
|
||||
@@ -35,6 +83,11 @@
|
||||
<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-select v-model="form.salesmen" clearable style="width:160px" placeholder="选择业务员">
|
||||
<el-option v-for="s in salesmen" :key="s.id" :label="s.name" :value="s.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="form.note" type="textarea" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -42,6 +95,24 @@
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 跟进记录(customer_records 语义) -->
|
||||
<el-dialog v-model="followVisible" :title="`跟进记录 — ${followCustomer?.name ?? ''}`" width="520px">
|
||||
<el-timeline style="padding-left:2px; max-height:260px; overflow-y:auto">
|
||||
<el-timeline-item v-for="f in follows" :key="f.id" :timestamp="f.created_at" placement="top">
|
||||
<div>{{ f.content }}</div>
|
||||
<div v-if="f.next_date" class="follow-next">下次跟进:{{ f.next_date }}</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-if="!follows.length" description="暂无跟进记录" :image-size="50" />
|
||||
<el-divider content-position="left">新增跟进</el-divider>
|
||||
<el-input v-model="followForm.content" type="textarea" placeholder="跟进内容" />
|
||||
<div class="follow-add">
|
||||
<el-date-picker v-model="followForm.next_date" type="date" value-format="YYYY-MM-DD"
|
||||
placeholder="下次跟进日期(可选)" style="width:180px" />
|
||||
<el-button type="primary" @click="addFollow">记录</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
@@ -58,7 +129,7 @@ const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
const editVisible = ref(false)
|
||||
const formRef = ref()
|
||||
const form = reactive({ id: 0, name: '', phone: '', address: '', note: '' })
|
||||
const form = reactive<any>({ id: 0, name: '', phone: '', address: '', note: '', salesmen: '' })
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入客户姓名', trigger: 'blur' }],
|
||||
phone: [{ pattern: /^[0-9+\-\s]{5,20}$/, message: '电话格式不正确', trigger: 'blur' }],
|
||||
@@ -74,7 +145,7 @@ async function load() {
|
||||
}
|
||||
|
||||
function openEdit(row?: any) {
|
||||
Object.assign(form, { id: row?.id ?? 0, name: row?.name ?? '', phone: row?.phone ?? '', address: row?.address ?? '', note: row?.note ?? '' })
|
||||
Object.assign(form, { id: row?.id ?? 0, name: row?.name ?? '', phone: row?.phone ?? '', address: row?.address ?? '', note: row?.note ?? '', salesmen: row?.salesmen ?? '' })
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
@@ -109,9 +180,61 @@ async function doImport(opt: any) {
|
||||
function downloadTemplate() { apiDownload('/api/customers/excel-template', 'customers_template.xlsx') }
|
||||
function exportExcel() { apiDownload('/api/customers/excel-out', 'customers.xlsx') }
|
||||
|
||||
onMounted(load)
|
||||
// ---- 业务员体系 ----
|
||||
const tab = ref('list')
|
||||
const salesmen = ref<any[]>([])
|
||||
const smStats = ref<any[]>([])
|
||||
const smForm = reactive<any>({ name: '', phone: '', commission_rate: 0 })
|
||||
|
||||
async function loadSalesmen() {
|
||||
salesmen.value = (await api<any[]>('get', '/api/salesmen')) ?? []
|
||||
smStats.value = (await api<any[]>('get', '/api/salesmen/statistics')) ?? []
|
||||
}
|
||||
|
||||
async function addSalesman() {
|
||||
if (!smForm.name.trim()) return ElMessage.warning('请填写姓名')
|
||||
await api('post', '/api/salesmen', smForm)
|
||||
ElMessage.success('已添加')
|
||||
smForm.name = ''; smForm.phone = ''; smForm.commission_rate = 0
|
||||
loadSalesmen()
|
||||
}
|
||||
|
||||
async function removeSalesman(row: any) {
|
||||
await ElMessageBox.confirm(`确定删除业务员「${row.name}」?`, '提示', { type: 'warning' })
|
||||
await api('delete', `/api/salesmen/${row.id}`)
|
||||
loadSalesmen()
|
||||
}
|
||||
|
||||
// ---- 跟进记录 ----
|
||||
const followVisible = ref(false)
|
||||
const followCustomer = ref<any>(null)
|
||||
const follows = ref<any[]>([])
|
||||
const followForm = reactive<any>({ content: '', next_date: null })
|
||||
|
||||
function openFollow(row: any) {
|
||||
followCustomer.value = row
|
||||
followForm.content = ''; followForm.next_date = null
|
||||
loadFollows()
|
||||
followVisible.value = true
|
||||
}
|
||||
|
||||
async function loadFollows() {
|
||||
follows.value = (await api<any[]>('get', `/api/customers/${followCustomer.value.id}/follows`)) ?? []
|
||||
}
|
||||
|
||||
async function addFollow() {
|
||||
if (!followForm.content.trim()) return ElMessage.warning('请填写跟进内容')
|
||||
await api('post', `/api/customers/${followCustomer.value.id}/follows`, followForm)
|
||||
followForm.content = ''; followForm.next_date = null
|
||||
loadFollows()
|
||||
}
|
||||
|
||||
onMounted(() => { load(); loadSalesmen() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; }
|
||||
.sm-add { display: flex; gap: 6px; margin-top: 10px; align-items: center; }
|
||||
.follow-next { color: #e6a23c; font-size: 12px; margin-top: 2px; }
|
||||
.follow-add { display: flex; gap: 10px; margin-top: 8px; justify-content: flex-end; }
|
||||
</style>
|
||||
|
||||
@@ -15,6 +15,18 @@
|
||||
</template>
|
||||
<el-button type="primary" :loading="loading" :disabled="!contractId" @click="run">生成下料方案</el-button>
|
||||
<el-button v-if="result" type="success" plain @click="print">打印方案</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-popconfirm title="把方案余料(≥最小长度)入库?" confirm-button-text="入库" @confirm="surplusIn">
|
||||
<template #reference><el-button v-if="result" size="small" type="warning" plain>余料入库</el-button></template>
|
||||
</el-popconfirm>
|
||||
<el-popconfirm title="按领料清单从库存出库?" confirm-button-text="领料" @confirm="pickout">
|
||||
<template #reference><el-button v-if="result" size="small" type="primary" plain>领料出库</el-button></template>
|
||||
</el-popconfirm>
|
||||
<el-popconfirm title="按缺料清单生成采购单?" confirm-button-text="生成" @confirm="createPurchase">
|
||||
<template #reference>
|
||||
<el-button v-if="result && result.shortage?.length" size="small" type="danger" plain>缺料转采购</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
|
||||
<template v-if="result">
|
||||
@@ -96,6 +108,22 @@ async function run() {
|
||||
|
||||
function print() { window.print() }
|
||||
|
||||
// ---- 余料入库 / 领料 / 采购联动 ----
|
||||
async function surplusIn() {
|
||||
const d = await api<any>('post', '/api/warehouse/surplus-in', { contract_id: contractId.value, min_length: 300 })
|
||||
ElMessage.success(d.added ? `${d.added} 根余料已入库` : '无达标余料')
|
||||
}
|
||||
|
||||
async function pickout() {
|
||||
const d = await api<any>('post', '/api/warehouse/pickout', { contract_id: contractId.value, note: '车间切割' })
|
||||
ElMessage.success(`领料完成:${(d.picked ?? []).length} 项出库`)
|
||||
}
|
||||
|
||||
async function createPurchase() {
|
||||
const d = await api<any>('post', '/api/purchases', { contract_id: contractId.value, note: '下料缺料转采购' })
|
||||
ElMessage.success(`采购单 ${d.po_no} 已生成,可在仓库库存页管理`)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const d = await api<any>('get', '/api/contracts?page=1&page_size=100')
|
||||
contracts.value = d.contracts?.data ?? []
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<el-tabs v-model="tab">
|
||||
<!-- 库存 -->
|
||||
<el-tab-pane label="库存" name="stock">
|
||||
<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">
|
||||
@@ -7,6 +10,7 @@
|
||||
<el-option value="hardware" label="五金" /><el-option value="addon" label="配件" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button plain @click="loadSurplus">余料</el-button>
|
||||
<div style="flex:1" />
|
||||
<el-upload :show-file-list="false" :http-request="doImport" accept=".xlsx,.xls">
|
||||
<el-button type="warning" plain :loading="importing">导入 Excel</el-button>
|
||||
@@ -39,6 +43,47 @@
|
||||
</el-table>
|
||||
<el-pagination style="margin-top:12px; justify-content:flex-end" background layout="total, prev, pager, next"
|
||||
:total="total" :page-size="page_size" v-model:current-page="page" @current-change="load" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 采购单 -->
|
||||
<el-tab-pane label="采购单" name="purchase">
|
||||
<div class="toolbar">
|
||||
<el-select v-model="poStatus" placeholder="全部状态" clearable style="width:130px" @change="loadPurchases">
|
||||
<el-option :value="0" label="待采购" /><el-option :value="1" label="已入库" /><el-option :value="2" label="已取消" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="loadPurchases">查询</el-button>
|
||||
<div style="flex:1" />
|
||||
<span class="tip">缺料转采购入口在「优化下料 → 下料方案」</span>
|
||||
</div>
|
||||
<el-table :data="purchases" v-loading="poLoading" stripe>
|
||||
<el-table-column prop="po_no" label="采购单号" width="160" />
|
||||
<el-table-column label="明细" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-for="(it, i) in row.items" :key="i" size="small" style="margin:2px">
|
||||
{{ it.name }} ×{{ it.qty }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="备注" min-width="100" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="160" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="(['warning', 'success', 'info'] as const)[row.status]">
|
||||
{{ ['待采购', '已入库', '已取消'][row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-popconfirm v-if="row.status === 0" title="按明细入库到库存?" confirm-button-text="入库"
|
||||
@confirm="receivePo(row)">
|
||||
<template #reference><el-button size="small" type="success" plain>采购入库</el-button></template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination style="margin-top:12px; justify-content:flex-end" background layout="total, prev, pager, next"
|
||||
:total="poTotal" :page-size="po_page_size" v-model:current-page="poPage" @current-change="loadPurchases" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="editVisible" :title="form.id ? '编辑材料' : '新增材料'" width="520px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
@@ -115,6 +160,38 @@ async function move(row: any, direction: number) {
|
||||
load()
|
||||
}
|
||||
|
||||
// ---- 余料 / 采购单 ----
|
||||
const tab = ref('stock')
|
||||
const poStatus = ref<number | '' | undefined>(undefined)
|
||||
const purchases = ref<any[]>([])
|
||||
const poTotal = ref(0)
|
||||
const poPage = ref(1)
|
||||
const po_page_size = 15
|
||||
const poLoading = ref(false)
|
||||
|
||||
async function loadSurplus() {
|
||||
const d = await api<any>('get', `/api/warehouse/surplus?page=1&page_size=50`)
|
||||
rows.value = d.data
|
||||
total.value = d.total
|
||||
ElMessage.info(`已筛选余料(共 ${d.total} 根)`)
|
||||
}
|
||||
|
||||
async function loadPurchases() {
|
||||
poLoading.value = true
|
||||
try {
|
||||
const st = poStatus.value === '' || poStatus.value === undefined ? '' : `&status=${poStatus.value}`
|
||||
const d = await api<any>('get', `/api/purchases?page=${poPage.value}&page_size=${po_page_size}${st}`)
|
||||
purchases.value = d.data
|
||||
poTotal.value = d.total
|
||||
} finally { poLoading.value = false }
|
||||
}
|
||||
|
||||
async function receivePo(row: any) {
|
||||
await api('post', `/api/purchases/${row.id}/receive`)
|
||||
ElMessage.success('采购已入库')
|
||||
loadPurchases()
|
||||
}
|
||||
|
||||
// ---- Excel 导入导出 ----
|
||||
const importing = ref(false)
|
||||
|
||||
@@ -129,7 +206,7 @@ async function doImport(opt: any) {
|
||||
|
||||
function exportExcel() { apiDownload('/api/warehouse/excel-out', 'warehouse.xlsx') }
|
||||
|
||||
onMounted(load)
|
||||
onMounted(() => { load(); loadPurchases() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user