Files
shanxia2.0/web-admin/src/views/basic-info/customer/index.vue
T
2026-05-17 23:08:38 +08:00

435 lines
14 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.
<!-- 客户管理 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<header class="page-head">
<h1>客户管理</h1>
<p class="page-desc">维护客户主数据;同一租户下身份证号不可重复。</p>
</header>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">客户列表</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="姓名/电话/身份证/标签"
clearable
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button v-if="hasAuth('add')" class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增客户
</el-button>
<el-button
v-if="hasAuth('delete')"
class="btn-ghost"
type="danger"
:disabled="selectedRows.length === 0"
@click="handleBatchDelete"
>
批量删除
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column
prop="customerName"
label="客户姓名"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column prop="sex" label="性别" width="72" align="center" header-align="center">
<template #default="{ row }">{{ sexLabel(row.sex) }}</template>
</el-table-column>
<el-table-column
prop="idCardNo"
label="身份证号"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="age"
label="年龄"
width="72"
align="center"
header-align="center"
/>
<el-table-column
prop="phoneNo"
label="联系电话"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="customerTags"
label="客户标签"
min-width="140"
show-overflow-tooltip
/>
<el-table-column
prop="createTime"
label="创建时间"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button v-if="hasAuth('edit')" class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="hasAuth('delete')" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
</div>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="720px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="biz-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="客户姓名" prop="customerName">
<el-input v-model="formData.customerName" placeholder="必填" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="选择" style="width: 100%">
<el-option label="男" value="0" />
<el-option label="女" value="1" />
<el-option label="未知" value="2" />
</el-select>
</el-form-item>
<el-form-item label="身份证号" prop="idCardNo">
<el-input
v-model="formData.idCardNo"
placeholder="可选;填写后租户内不可重复"
clearable
/>
</el-form-item>
<el-form-item label="年龄">
<el-input
type="number"
step="any"
v-model="formData.age"
:min="0"
:max="150"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="formData.phoneNo" placeholder="可选" clearable />
</el-form-item>
<el-form-item label="客户标签" class="form-col-span-2">
<el-input v-model="formData.customerTags" placeholder="多个标签用英文逗号分隔" />
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="dialogVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewRow?.customerName || '客户详情'"
:icon="User"
:meta="`身份证:${viewRow?.idCardNo || '—'}${viewRow?.createTime ? ' · ' + viewRow.createTime : ''}`"
:sections="viewSections"
/>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, User } from '@element-plus/icons-vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
fetchCustomerList,
fetchCreateCustomer,
fetchUpdateCustomer,
fetchDeleteCustomer
} from '@/api/admin'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { useAuth } from '@/hooks/core/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'BizCustomer' })
function sexLabel(s: string) {
if (s === '0') return '男'
if (s === '1') return '女'
return '未知'
}
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref<Api.BizCustomer.CustomerItem[]>([])
const selectedRows = ref<Api.BizCustomer.CustomerItem[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const dialogVisible = ref(false)
const dialogTitle = ref('新增客户')
const isEdit = ref(false)
const submitLoading = ref(false)
const formRef = ref()
const editingId = ref<string | null>(null)
// ---- 查看弹窗 ----
const viewVisible = ref(false)
const viewRow = ref<Api.BizCustomer.CustomerItem | null>(null)
const viewSections = computed(() => {
const row = viewRow.value
if (!row) return []
return [
{
title: '基本信息',
fields: [
{ label: '客户姓名', value: row.customerName },
{ label: '性别', value: sexLabel(row.sex) },
{ label: '身份证号', value: row.idCardNo },
{ label: '年龄', value: row.age },
{ label: '联系电话', value: row.phoneNo },
{ label: '客户标签', value: row.customerTags, span: true },
{ label: '创建时间', value: row.createTime },
{ label: '备注', value: row.remark, span: true }
]
}
]
})
function handleView(row: Api.BizCustomer.CustomerItem) {
viewRow.value = row
viewVisible.value = true
}
const formData = reactive({
customerName: '',
sex: '2',
idCardNo: '',
age: null as number | null,
phoneNo: '',
customerTags: '',
orderNum: 0,
remark: ''
})
const formRules = {
customerName: [{ required: true, message: '请输入客户姓名', trigger: 'blur' }]
}
const handleSelectionChange = (rows: Api.BizCustomer.CustomerItem[]) => {
selectedRows.value = rows
}
async function loadData() {
loading.value = true
try {
const res = await fetchCustomerList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
pagination.page = 1
loadData()
}
function resetForm() {
formData.customerName = ''
formData.sex = '2'
formData.idCardNo = ''
formData.age = null
formData.phoneNo = ''
formData.customerTags = ''
formData.orderNum = 0
formData.remark = ''
editingId.value = null
}
function handleAdd() {
isEdit.value = false
dialogTitle.value = '新增客户'
resetForm()
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
function handleEdit(row: Api.BizCustomer.CustomerItem) {
isEdit.value = true
dialogTitle.value = '编辑客户'
editingId.value = row.id
formData.customerName = row.customerName || ''
formData.sex = row.sex || '2'
formData.idCardNo = row.idCardNo || ''
formData.age = row.age ?? null
formData.phoneNo = row.phoneNo || ''
formData.customerTags = row.customerTags || ''
formData.orderNum = row.orderNum ?? 0
formData.remark = row.remark || ''
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
async function handleSubmit() {
await formRef.value?.validate?.()
submitLoading.value = true
try {
const payload = {
customerName: formData.customerName.trim(),
sex: formData.sex,
idCardNo: formData.idCardNo?.trim() || undefined,
age: formData.age,
phoneNo: formData.phoneNo?.trim() || undefined,
customerTags: formData.customerTags?.trim() || undefined,
orderNum: formData.orderNum,
remark: formData.remark?.trim() || undefined
}
if (isEdit.value && editingId.value) {
await fetchUpdateCustomer(editingId.value, payload)
} else {
await fetchCreateCustomer(payload)
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
await loadData()
} catch {
/* 全局处理 */
} finally {
submitLoading.value = false
}
}
async function handleDelete(row: Api.BizCustomer.CustomerItem) {
await ElMessageBox.confirm('确定要删除该客户吗?', '提示', { type: 'warning' })
await fetchDeleteCustomer(row.id)
ElMessage.success('删除成功')
await loadData()
}
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条客户吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteCustomer(id)
}
ElMessage.success('批量删除成功')
selectedRows.value = []
await loadData()
}, fullscreenLoadingText.batchDelete)
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.biz-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>