Files
yuezi-saas/frontend/src/views/crm/customers/index.vue
T

247 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { customerApi } from '@/api/crm.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const dialogTitle = ref('新增客户')
const submitLoading = ref(false)
const detailVisible = ref(false)
const detailData = ref(null)
// 设置密码
const pwdVisible = ref(false)
const pwdForm = reactive({ password: '', confirm: '' })
const pwdTarget = ref(null)
const pwdLoading = ref(false)
const searchForm = reactive({ name: '', phone: '', status: '' })
const pagination = reactive({ page: 1, per_page: 20 })
const formRef = ref(null)
const form = reactive({
id: null, name: '', phone: '', id_card: '', wechat: '',
birthday: '', expected_date: '', actual_date: '', baby_count: 1,
tags: [], remark: '',
families: []
})
const isEdit = ref(false)
const formRules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
}
const statusMap = { 1: '潜在', 2: '签约', 3: '在住', 4: '离店', 5: '无效' }
const statusType = { 1: 'info', 2: 'primary', 3: 'success', 4: '', 5: 'danger' }
async function fetchList() {
loading.value = true
try {
const res = await customerApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { name: '', phone: '', status: '' }); handleSearch() }
function handleAdd() {
isEdit.value = false; dialogTitle.value = '新增客户'
Object.assign(form, { id: null, name: '', phone: '', id_card: '', wechat: '', birthday: '', expected_date: '', actual_date: '', baby_count: 1, tags: [], remark: '', families: [] })
dialogVisible.value = true
}
async function handleEdit(row) {
isEdit.value = true; dialogTitle.value = '编辑客户'
const res = await customerApi.getDetail(row.id)
const d = res.data
Object.assign(form, { id: d.id, name: d.name, phone: d.phone, id_card: d.id_card || '', wechat: d.wechat || '', birthday: d.birthday || '', expected_date: d.expected_date || '', actual_date: d.actual_date || '', baby_count: d.baby_count || 1, tags: d.tags || [], remark: d.remark || '', families: d.families || [] })
dialogVisible.value = true
}
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (isEdit.value) { await customerApi.update(form.id, form); ElMessage.success('更新成功') }
else { await customerApi.create(form); ElMessage.success('创建成功') }
dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除客户「${row.name}」吗?`, '删除确认', { type: 'warning' })
await customerApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
}
async function handleDetail(row) {
const res = await customerApi.getDetail(row.id)
detailData.value = res.data
detailVisible.value = true
}
function addFamily() { form.families.push({ name: '', phone: '', relation: '', is_emergency: false }) }
function removeFamily(idx) { form.families.splice(idx, 1) }
function openSetPassword(row) {
pwdTarget.value = row
Object.assign(pwdForm, { password: '', confirm: '' })
pwdVisible.value = true
}
async function submitSetPassword() {
if (!pwdForm.password || pwdForm.password.length < 6) {
ElMessage.warning('密码不少于 6 位'); return
}
if (pwdForm.password !== pwdForm.confirm) {
ElMessage.warning('两次密码不一致'); return
}
pwdLoading.value = true
try {
await customerApi.setPassword(pwdTarget.value.id, { password: pwdForm.password })
ElMessage.success('微客宝登录密码已设置')
pwdVisible.value = false
} finally { pwdLoading.value = false }
}
function handlePageChange(val) { pagination.page = val; fetchList() }
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
onMounted(() => fetchList())
</script>
<template>
<div class="p-6 space-y-4">
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm p-4 flex items-center flex-wrap gap-3">
<el-input v-model="searchForm.name" placeholder="客户姓名" style="width:160px" clearable @keydown.enter="handleSearch" />
<el-input v-model="searchForm.phone" placeholder="手机号" style="width:160px" clearable @keydown.enter="handleSearch" />
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
<el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="bg-white rounded-2xl border border-gray-100 shadow-sm">
<div class="flex items-center justify-between px-5 py-4 border-b border-gray-50">
<div class="flex items-center gap-3">
<span class="font-medium text-gray-700">客户列表</span>
</div>
<el-button type="primary" @click="handleAdd">新增客户</el-button>
</div>
<div class="px-2">
<el-table v-loading="loading" :data="tableData" style="width:100%">
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column prop="expected_date" label="预产期" min-width="110" />
<el-table-column prop="baby_count" label="宝宝数" width="80" align="center" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
</template>
</el-table-column>
<el-table-column label="负责人" min-width="100">
<template #default="{ row }">{{ row.owner?.name || '-' }}</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" min-width="160" />
<el-table-column label="操作" width="260" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" text @click="handleDetail(row)">详情</el-button>
<el-button size="small" type="primary" text @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="warning" text @click="openSetPassword(row)">设置密码</el-button>
<el-button size="small" type="danger" text @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="px-5 py-3 border-t border-gray-50">
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
</div>
</div>
<!-- Add/Edit -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="80px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="姓名" prop="name"><el-input v-model="form.name" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="手机号" prop="phone"><el-input v-model="form.phone" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="身份证"><el-input v-model="form.id_card" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="微信号"><el-input v-model="form.wechat" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="生日"><el-date-picker v-model="form.birthday" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="预产期"><el-date-picker v-model="form.expected_date" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="宝宝数"><el-input-number v-model="form.baby_count" :min="1" :max="5" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item></el-col>
</el-row>
<el-divider content-position="left">家属信息</el-divider>
<div v-for="(f, idx) in form.families" :key="idx" style="margin-bottom:10px">
<el-row :gutter="8">
<el-col :span="5"><el-input v-model="f.name" placeholder="姓名" /></el-col>
<el-col :span="5"><el-input v-model="f.phone" placeholder="手机号" /></el-col>
<el-col :span="5"><el-input v-model="f.relation" placeholder="关系" /></el-col>
<el-col :span="5"><el-checkbox v-model="f.is_emergency">紧急联系人</el-checkbox></el-col>
<el-col :span="4"><el-button type="danger" link @click="removeFamily(idx)">移除</el-button></el-col>
</el-row>
</div>
<el-button type="primary" link @click="addFamily">+ 添加家属</el-button>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
<!-- Detail -->
<el-drawer v-model="detailVisible" title="客户详情" size="500px">
<template v-if="detailData">
<el-descriptions :column="2" border>
<el-descriptions-item label="姓名">{{ detailData.name }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ detailData.phone }}</el-descriptions-item>
<el-descriptions-item label="微信号">{{ detailData.wechat || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态"><el-tag :type="statusType[detailData.status]" size="small">{{ statusMap[detailData.status] }}</el-tag></el-descriptions-item>
<el-descriptions-item label="预产期">{{ detailData.expected_date || '-' }}</el-descriptions-item>
<el-descriptions-item label="宝宝数">{{ detailData.baby_count }}</el-descriptions-item>
</el-descriptions>
<h4 style="margin:16px 0 8px">家属信息</h4>
<el-table :data="detailData.families || []" border size="small">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="phone" label="手机号" />
<el-table-column prop="relation" label="关系" />
<el-table-column label="紧急联系人" width="100"><template #default="{ row }"><el-tag :type="row.is_emergency ? 'danger' : 'info'" size="small">{{ row.is_emergency ? '是' : '否' }}</el-tag></template></el-table-column>
</el-table>
<h4 style="margin:16px 0 8px">合同列表</h4>
<el-table :data="detailData.contracts || []" border size="small">
<el-table-column prop="contract_no" label="合同编号" />
<el-table-column prop="actual_amount" label="实际金额" />
<el-table-column prop="check_in_date" label="入住日期" />
</el-table>
</template>
</el-drawer>
<!-- 设置微客宝登录密码 -->
<el-dialog v-model="pwdVisible" title="设置微客宝登录密码" width="400px" destroy-on-close>
<div style="margin-bottom:12px; color:#606266; font-size:13px;">
为客户 <strong>{{ pwdTarget?.name }}</strong>{{ pwdTarget?.phone }}设置手机号登录密码
</div>
<el-form label-width="90px">
<el-form-item label="新密码">
<el-input v-model="pwdForm.password" type="password" show-password placeholder="不少于 6 位" />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="pwdForm.confirm" type="password" show-password placeholder="再次输入密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="pwdVisible = false">取消</el-button>
<el-button type="primary" :loading="pwdLoading" @click="submitSetPassword">确认设置</el-button>
</template>
</el-dialog>
</div>
</template>