feat: 客户关联产品并支持按产品圈选通知群体
新增客户-产品多对多关联,列表可按产品筛选并复制邮箱;发证/下单自动回填历史关联。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
d0e1ca3729
commit
4c46088f4f
10 files changed
+337
-89
No files matched your search
@@ -2,14 +2,57 @@
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" placeholder="搜索客户/邮箱/公司" clearable style="width: 260px" @keyup.enter="loadData" @clear="loadData" />
|
||||
<el-button type="primary" @click="openCreate">新增客户</el-button>
|
||||
<div class="filters">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索客户/邮箱/公司"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
@keyup.enter="search"
|
||||
@clear="search"
|
||||
/>
|
||||
<el-select
|
||||
v-model="productFilter"
|
||||
placeholder="按产品筛选"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 180px"
|
||||
@change="search"
|
||||
>
|
||||
<el-option v-for="p in products" :key="p.id" :label="p.name" :value="p.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button :disabled="!productFilter || list.length === 0" @click="copyEmails">复制本页邮箱</el-button>
|
||||
<el-button type="primary" @click="openCreate">新增客户</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="客户可关联多个产品。按产品筛选后即可圈选通知群体;发证/购买会自动写入产品关联。"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe>
|
||||
<el-table-column prop="name" label="客户名称" min-width="120" />
|
||||
<el-table-column prop="company" label="公司" min-width="140" />
|
||||
<el-table-column prop="company" label="公司" min-width="120" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="180" />
|
||||
<el-table-column label="关联产品" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.products?.length">
|
||||
<el-tag
|
||||
v-for="p in row.products"
|
||||
:key="p.id"
|
||||
size="small"
|
||||
style="margin: 2px 4px 2px 0"
|
||||
>{{ p.name }}</el-tag>
|
||||
</template>
|
||||
<span v-else class="muted">未关联</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phone" label="电话" width="130" />
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
@@ -32,12 +75,24 @@
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="form.id ? '编辑客户' : '新增客户'" width="520px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-dialog v-model="dialogVisible" :title="form.id ? '编辑客户' : '新增客户'" width="560px" @closed="onDialogClosed">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item label="名称" prop="name"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="公司"><el-input v-model="form.company" /></el-form-item>
|
||||
<el-form-item label="邮箱" prop="email"><el-input v-model="form.email" /></el-form-item>
|
||||
<el-form-item label="电话"><el-input v-model="form.phone" /></el-form-item>
|
||||
<el-form-item label="关联产品" prop="product_ids">
|
||||
<el-select
|
||||
v-model="form.product_ids"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
placeholder="选择产品(可多选)"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="p in products" :key="p.id" :label="`${p.name} (${p.code})`" :value="p.id" />
|
||||
</el-select>
|
||||
</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.notes" type="textarea" :rows="2" /></el-form-item>
|
||||
</el-form>
|
||||
@@ -50,50 +105,91 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getCustomers, createCustomer, updateCustomer, deleteCustomer, updateCustomerStatus } from '@/api/customer'
|
||||
import { getAllProducts } from '@/api/product'
|
||||
|
||||
const list = ref([])
|
||||
const products = ref([])
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const productFilter = ref(null)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ id: null, name: '', company: '', email: '', phone: '', address: '', notes: '' })
|
||||
const form = reactive({
|
||||
id: null, name: '', company: '', email: '', phone: '', address: '', notes: '', product_ids: [],
|
||||
})
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
email: [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
function search() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getCustomers({ page: page.value, page_size: pageSize.value, keyword: keyword.value })
|
||||
const res = await getCustomers({
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
keyword: keyword.value,
|
||||
product_id: productFilter.value || undefined,
|
||||
})
|
||||
list.value = res.data.list || []
|
||||
total.value = res.data.total
|
||||
} finally { loading.value = false }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(form, { id: null, name: '', company: '', email: '', phone: '', address: '', notes: '' })
|
||||
Object.assign(form, {
|
||||
id: null, name: '', company: '', email: '', phone: '', address: '', notes: '', product_ids: [],
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function edit(row) {
|
||||
Object.assign(form, row)
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
company: row.company,
|
||||
email: row.email,
|
||||
phone: row.phone,
|
||||
address: row.address,
|
||||
notes: row.notes,
|
||||
product_ids: [...(row.product_ids || [])],
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
if (form.id) await updateCustomer(form.id, form)
|
||||
else await createCustomer(form)
|
||||
const payload = {
|
||||
name: form.name,
|
||||
company: form.company,
|
||||
email: form.email,
|
||||
phone: form.phone,
|
||||
address: form.address,
|
||||
notes: form.notes,
|
||||
product_ids: form.product_ids || [],
|
||||
}
|
||||
if (form.id) await updateCustomer(form.id, payload)
|
||||
else await createCustomer(payload)
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
} catch { /* */ }
|
||||
@@ -111,9 +207,29 @@ async function toggleStatus(row, val) {
|
||||
loadData()
|
||||
}
|
||||
|
||||
loadData()
|
||||
async function copyEmails() {
|
||||
const emails = list.value.map((r) => (r.email || '').trim()).filter(Boolean)
|
||||
if (!emails.length) {
|
||||
ElMessage.warning('本页没有可复制的邮箱')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(emails.join(', '))
|
||||
ElMessage.success(`已复制 ${emails.length} 个邮箱`)
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请手动选择')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await getAllProducts()
|
||||
products.value = res.data || []
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; justify-content: space-between; margin-bottom: 16px; }
|
||||
.toolbar { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.filters, .actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
.muted { color: #c0c4cc; font-size: 13px; }
|
||||
</style>
|
||||
Reference in new issue
Block a user