新增客户-产品多对多关联,列表可按产品筛选并复制邮箱;发证/下单自动回填历史关联。 Co-authored-by: Cursor <cursoragent@cursor.com>
236 lines
7.6 KiB
Vue
236 lines
7.6 KiB
Vue
<template>
|
|
<div>
|
|
<el-card shadow="never">
|
|
<div class="toolbar">
|
|
<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="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 }">
|
|
<el-switch :model-value="row.status === 1" @change="(v) => toggleStatus(row, v)" />
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="created_at" label="创建时间" width="170" />
|
|
<el-table-column label="操作" width="140" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button size="small" @click="edit(row)">编辑</el-button>
|
|
<el-button size="small" type="danger" @click="remove(row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<el-pagination
|
|
v-model:current-page="page" v-model:page-size="pageSize"
|
|
:total="total" layout="total, prev, pager, next" style="margin-top: 16px"
|
|
@current-change="loadData"
|
|
/>
|
|
</el-card>
|
|
|
|
<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>
|
|
<template #footer>
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="submit">确认</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
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: '', 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,
|
|
product_id: productFilter.value || undefined,
|
|
})
|
|
list.value = res.data.list || []
|
|
total.value = res.data.total
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function openCreate() {
|
|
Object.assign(form, {
|
|
id: null, name: '', company: '', email: '', phone: '', address: '', notes: '', product_ids: [],
|
|
})
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
function edit(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 {
|
|
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 { /* */ }
|
|
})
|
|
}
|
|
|
|
async function remove(row) {
|
|
await ElMessageBox.confirm(`确定删除客户「${row.name}」?`, '提示', { type: 'warning' })
|
|
await deleteCustomer(row.id)
|
|
loadData()
|
|
}
|
|
|
|
async function toggleStatus(row, val) {
|
|
await updateCustomerStatus(row.id, val ? 1 : 0)
|
|
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; 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>
|