新增功能

This commit is contained in:
Your Name
2026-03-14 11:16:04 +08:00
parent eb283f008b
commit 4ddee40675
264 changed files with 5039 additions and 2813 deletions
+770
View File
@@ -0,0 +1,770 @@
<template>
<div class="fans-h5">
<!-- 顶部搜索 -->
<div class="h5-header">
<div class="h5-search">
<el-input
v-model="keyword"
placeholder="搜索姓名 / 手机号"
clearable
prefix-icon="Search"
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
</div>
<div class="h5-filter-row">
<el-select v-model="formData.gender" placeholder="性别" clearable size="small" @change="handleSearch">
<el-option label="男" :value="1" />
<el-option label="女" :value="2" />
</el-select>
<el-select v-model="formData.status" placeholder="状态" clearable size="small" @change="handleSearch">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
<el-button type="primary" size="small" @click="handleSearch">查询</el-button>
<el-button size="small" @click="handleReset">重置</el-button>
</div>
</div>
<!-- 粉丝卡片列表 -->
<div class="h5-list" v-loading="listLoading">
<div v-if="fanList.length === 0 && !listLoading" class="h5-empty">
<el-empty description="暂无粉丝数据" />
</div>
<div
v-for="item in fanList"
:key="item.id"
class="h5-card"
>
<div class="h5-card-header">
<div class="h5-card-name">
<span class="name">{{ item.name }}</span>
<el-tag size="small" :type="item.gender === 1 ? '' : item.gender === 2 ? 'danger' : 'info'" effect="plain">
{{ item.gender_desc }}
</el-tag>
<span v-if="item.age" class="age">{{ item.age }}</span>
</div>
<el-tag size="small" :type="item.status == 1 ? 'success' : 'danger'">
{{ item.status == 1 ? '启用' : '禁用' }}
</el-tag>
</div>
<div class="h5-card-body">
<div class="info-line">
<span class="label">手机</span>
<span class="value">{{ item.phone || '-' }}</span>
</div>
<div class="info-line">
<span class="label">身份证</span>
<span class="value">{{ item.id_card || '-' }}</span>
</div>
<div v-if="item.remark" class="info-line">
<span class="label">备注</span>
<span class="value remark">{{ item.remark }}</span>
</div>
</div>
<div class="h5-card-footer">
<div class="footer-left">
<div class="visit-info">
回访 <span class="visit-count">{{ item.visit_count || 0 }}</span>
</div>
<div v-if="item.creator_name" class="creator-info">
登记人{{ item.creator_name }}
</div>
</div>
<div class="actions">
<el-button type="primary" text size="small" @click="handleEdit(item)">编辑</el-button>
<el-button type="success" text size="small" @click="handleVisitRecords(item)">回访</el-button>
<el-button type="danger" text size="small" @click="handleDelete(item)">删除</el-button>
</div>
</div>
</div>
<!-- 加载更多 -->
<div v-if="hasMore" class="h5-load-more" @click="loadMore">
<span v-if="!moreLoading">加载更多</span>
<span v-else>加载中...</span>
</div>
<div v-if="!hasMore && fanList.length > 0" class="h5-no-more">没有更多了</div>
</div>
<!-- 悬浮新增按钮 -->
<div class="h5-fab" @click="handleAdd">
<el-icon :size="24"><Plus /></el-icon>
</div>
<!-- 新增/编辑粉丝 -->
<el-drawer
v-model="fanDrawerVisible"
:title="fanDialogType === 'add' ? '新增粉丝' : '编辑粉丝'"
direction="btt"
size="85%"
:close-on-click-modal="false"
@closed="resetFanForm"
>
<el-form
ref="fanFormRef"
:model="fanForm"
:rules="fanRules"
label-position="top"
class="h5-form"
>
<el-form-item label="姓名" prop="name">
<el-input v-model="fanForm.name" placeholder="请输入姓名" maxlength="50" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="fanForm.phone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="身份证号" prop="id_card">
<el-input v-model="fanForm.id_card" placeholder="请输入身份证号码" maxlength="18" />
</el-form-item>
<div class="h5-form-row">
<el-form-item label="年龄" prop="age" class="flex-1">
<el-input-number v-model="fanForm.age" :min="0" :max="150" controls-position="right" class="!w-full" />
</el-form-item>
<el-form-item label="性别" prop="gender" class="flex-1">
<el-radio-group v-model="fanForm.gender">
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
</div>
<el-form-item label="状态">
<el-switch v-model="fanForm.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="fanForm.remark" type="textarea" :rows="3" placeholder="请输入备注" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button class="flex-1" @click="fanDrawerVisible = false">取消</el-button>
<el-button class="flex-1" type="primary" :loading="fanSubmitLoading" @click="handleFanSubmit">确定</el-button>
</template>
</el-drawer>
<!-- 回访记录 -->
<el-drawer
v-model="visitDrawerVisible"
:title="`回访记录 - ${currentFan?.name || ''}`"
direction="rtl"
size="100%"
:close-on-click-modal="false"
>
<div class="h5-visit-header">
<el-button type="primary" size="small" @click="handleAddVisitRecord">+ 新增回访</el-button>
</div>
<div class="h5-visit-list" v-loading="visitLoading">
<div v-if="visitRecords.length === 0 && !visitLoading" class="h5-empty">
<el-empty description="暂无回访记录" :image-size="80" />
</div>
<div v-for="item in visitRecords" :key="item.id" class="h5-visit-card">
<div class="visit-card-header">
<div class="visit-card-tags">
<el-tag size="small" :type="visitTypeTagMap[item.visit_type]">{{ item.visit_type_desc }}</el-tag>
<span class="visit-time">{{ item.visit_time_text }}</span>
</div>
<div class="visit-card-actions">
<el-button type="primary" text size="small" @click="handleEditVisitRecord(item)">编辑</el-button>
<el-button type="danger" text size="small" @click="handleDeleteVisitRecord(item.id)">删除</el-button>
</div>
</div>
<div class="visit-card-content">{{ item.content }}</div>
<div v-if="item.result" class="visit-card-result">
<span class="label">结果</span>{{ item.result }}
</div>
<div class="visit-card-meta">
<span v-if="item.next_visit_time_text" class="next-visit">下次回访{{ item.next_visit_time_text }}</span>
<span v-if="item.operator_name" class="operator">{{ item.operator_name }}</span>
</div>
</div>
<div v-if="visitTotal > visitRecords.length" class="h5-load-more" @click="loadMoreVisitRecords">
加载更多
</div>
</div>
</el-drawer>
<!-- 新增/编辑回访记录 -->
<el-drawer
v-model="visitFormDrawerVisible"
:title="visitDialogType === 'add' ? '新增回访记录' : '编辑回访记录'"
direction="btt"
size="80%"
:close-on-click-modal="false"
@closed="resetVisitForm"
>
<el-form
ref="visitFormRef"
:model="visitForm"
:rules="visitRules"
label-position="top"
class="h5-form"
>
<el-form-item label="回访类型" prop="visit_type">
<el-radio-group v-model="visitForm.visit_type" class="h5-radio-group">
<el-radio-button :label="1">电话</el-radio-button>
<el-radio-button :label="2">微信</el-radio-button>
<el-radio-button :label="3">短信</el-radio-button>
<el-radio-button :label="4">上门</el-radio-button>
<el-radio-button :label="5">其他</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="回访时间" prop="visit_time">
<el-date-picker
v-model="visitForm.visit_time"
type="datetime"
placeholder="请选择回访时间"
value-format="YYYY-MM-DD HH:mm:ss"
class="!w-full"
/>
</el-form-item>
<el-form-item label="回访内容" prop="content">
<el-input v-model="visitForm.content" type="textarea" :rows="4" placeholder="请输入回访内容" />
</el-form-item>
<el-form-item label="回访结果">
<el-input v-model="visitForm.result" type="textarea" :rows="3" placeholder="请输入回访结果" />
</el-form-item>
<el-form-item label="下次回访时间">
<el-date-picker
v-model="visitForm.next_visit_time"
type="datetime"
placeholder="请选择下次回访时间"
value-format="YYYY-MM-DD HH:mm:ss"
class="!w-full"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button class="flex-1" @click="visitFormDrawerVisible = false">取消</el-button>
<el-button class="flex-1" type="primary" :loading="visitSubmitLoading" @click="handleVisitSubmit">确定</el-button>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts" name="fansH5">
import {
fanLists,
fanAdd,
fanEdit,
fanDelete,
fanVisitRecordLists,
fanVisitRecordAdd,
fanVisitRecordEdit,
fanVisitRecordDelete
} from '@/api/fans'
import feedback from '@/utils/feedback'
import dayjs from 'dayjs'
import { Plus } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
// ==================== 粉丝列表 ====================
const keyword = ref('')
const formData = ref({
name: '',
phone: '',
gender: '' as string | number,
status: '' as string | number
})
const fanList = ref<any[]>([])
const listLoading = ref(false)
const moreLoading = ref(false)
const pageNo = ref(1)
const pageSize = 15
const total = ref(0)
const hasMore = computed(() => fanList.value.length < total.value)
const loadList = async (isLoadMore = false) => {
if (isLoadMore) {
moreLoading.value = true
} else {
listLoading.value = true
}
try {
const params: any = {
page_no: pageNo.value,
page_size: pageSize,
...formData.value
}
if (keyword.value) {
if (/^\d+$/.test(keyword.value)) {
params.phone = keyword.value
} else {
params.name = keyword.value
}
}
const res = await fanLists(params)
if (isLoadMore) {
fanList.value = [...fanList.value, ...(res.lists || [])]
} else {
fanList.value = res.lists || []
}
total.value = res.count || 0
} catch (error) {
console.error(error)
} finally {
listLoading.value = false
moreLoading.value = false
}
}
const handleSearch = () => {
pageNo.value = 1
loadList()
}
const handleReset = () => {
keyword.value = ''
formData.value = { name: '', phone: '', gender: '', status: '' }
handleSearch()
}
const loadMore = () => {
if (moreLoading.value) return
pageNo.value++
loadList(true)
}
// ==================== 新增/编辑粉丝 ====================
const fanDrawerVisible = ref(false)
const fanDialogType = ref<'add' | 'edit'>('add')
const fanFormRef = ref<FormInstance>()
const fanSubmitLoading = ref(false)
const fanForm = ref({
id: 0, name: '', phone: '', id_card: '', age: 0, gender: 1, status: 1, remark: ''
})
const fanRules: FormRules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
id_card: [
{ pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: '身份证号格式不正确', trigger: 'blur' }
],
gender: [{ required: true, message: '请选择性别', trigger: 'change' }]
}
const handleAdd = () => {
fanDialogType.value = 'add'
fanDrawerVisible.value = true
}
const handleEdit = (row: any) => {
fanDialogType.value = 'edit'
fanForm.value = {
id: row.id, name: row.name, phone: row.phone, id_card: row.id_card,
age: row.age, gender: row.gender, status: row.status, remark: row.remark
}
fanDrawerVisible.value = true
}
const resetFanForm = () => {
fanForm.value = { id: 0, name: '', phone: '', id_card: '', age: 0, gender: 1, status: 1, remark: '' }
fanFormRef.value?.resetFields()
}
const handleFanSubmit = async () => {
await fanFormRef.value?.validate()
fanSubmitLoading.value = true
try {
if (fanDialogType.value === 'add') {
await fanAdd(fanForm.value)
} else {
await fanEdit(fanForm.value)
}
fanDrawerVisible.value = false
handleSearch()
} catch (error: any) {
} finally {
fanSubmitLoading.value = false
}
}
const handleDelete = async (item: any) => {
await feedback.confirm(`确定删除粉丝「${item.name}」吗?`)
try {
await fanDelete({ id: item.id })
handleSearch()
} catch (error: any) {
}
}
// ==================== 回访记录 ====================
const visitDrawerVisible = ref(false)
const currentFan = ref<any>(null)
const visitRecords = ref<any[]>([])
const visitLoading = ref(false)
const visitTotal = ref(0)
const visitPageNo = ref(1)
const visitTypeTagMap: Record<number, string> = {
1: '', 2: 'success', 3: 'info', 4: 'warning', 5: 'danger'
}
const handleVisitRecords = (row: any) => {
currentFan.value = row
visitDrawerVisible.value = true
visitPageNo.value = 1
visitRecords.value = []
loadVisitRecords()
}
const loadVisitRecords = async () => {
visitLoading.value = true
try {
const res = await fanVisitRecordLists({
fan_id: currentFan.value.id,
page_no: visitPageNo.value,
page_size: 15
})
if (visitPageNo.value === 1) {
visitRecords.value = res.lists || []
} else {
visitRecords.value = [...visitRecords.value, ...(res.lists || [])]
}
visitTotal.value = res.count || 0
} catch (error) {
console.error(error)
} finally {
visitLoading.value = false
}
}
const loadMoreVisitRecords = () => {
visitPageNo.value++
loadVisitRecords()
}
// ==================== 新增/编辑回访记录 ====================
const visitFormDrawerVisible = ref(false)
const visitDialogType = ref<'add' | 'edit'>('add')
const visitFormRef = ref<FormInstance>()
const visitSubmitLoading = ref(false)
const visitForm = ref({
id: 0, fan_id: 0, visit_type: 1, visit_time: '', content: '', result: '', next_visit_time: ''
})
const visitRules: FormRules = {
visit_type: [{ required: true, message: '请选择回访类型', trigger: 'change' }],
visit_time: [{ required: true, message: '请选择回访时间', trigger: 'change' }],
content: [{ required: true, message: '请输入回访内容', trigger: 'blur' }]
}
const handleAddVisitRecord = () => {
visitDialogType.value = 'add'
visitForm.value = {
id: 0, fan_id: currentFan.value.id, visit_type: 1,
visit_time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
content: '', result: '', next_visit_time: ''
}
visitFormDrawerVisible.value = true
}
const handleEditVisitRecord = (row: any) => {
visitDialogType.value = 'edit'
visitForm.value = {
id: row.id, fan_id: row.fan_id, visit_type: row.visit_type,
visit_time: row.visit_time_text || '', content: row.content || '',
result: row.result || '', next_visit_time: row.next_visit_time_text || ''
}
visitFormDrawerVisible.value = true
}
const resetVisitForm = () => {
visitForm.value = { id: 0, fan_id: 0, visit_type: 1, visit_time: '', content: '', result: '', next_visit_time: '' }
visitFormRef.value?.resetFields()
}
const handleVisitSubmit = async () => {
await visitFormRef.value?.validate()
visitSubmitLoading.value = true
try {
if (visitDialogType.value === 'add') {
await fanVisitRecordAdd(visitForm.value)
} else {
await fanVisitRecordEdit(visitForm.value)
}
visitFormDrawerVisible.value = false
visitPageNo.value = 1
loadVisitRecords()
} catch (error: any) {
} finally {
visitSubmitLoading.value = false
}
}
const handleDeleteVisitRecord = async (id: number) => {
await feedback.confirm('确定删除该回访记录吗?')
try {
await fanVisitRecordDelete({ id })
visitPageNo.value = 1
loadVisitRecords()
} catch (error: any) {
}
}
loadList()
</script>
<style lang="scss" scoped>
.fans-h5 {
max-width: 500px;
margin: 0 auto;
min-height: 100vh;
background: #f5f6fa;
padding-bottom: 80px;
}
.h5-header {
background: #fff;
padding: 12px 16px;
position: sticky;
top: 0;
z-index: 10;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.h5-search {
margin-bottom: 8px;
}
.h5-filter-row {
display: flex;
gap: 8px;
align-items: center;
.el-select {
width: 80px;
}
}
.h5-list {
padding: 12px 16px;
}
.h5-card {
background: #fff;
border-radius: 12px;
margin-bottom: 12px;
overflow: hidden;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.05);
}
.h5-card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px 0;
}
.h5-card-name {
display: flex;
align-items: center;
gap: 8px;
.name {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.age {
font-size: 12px;
color: #909399;
}
}
.h5-card-body {
padding: 10px 16px;
}
.info-line {
display: flex;
font-size: 13px;
line-height: 2;
.label {
color: #909399;
width: 50px;
flex-shrink: 0;
}
.value {
color: #606266;
flex: 1;
&.remark {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.h5-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
border-top: 1px solid #f2f3f5;
.footer-left {
display: flex;
flex-direction: column;
gap: 2px;
}
.visit-info {
font-size: 12px;
color: #909399;
}
.visit-count {
color: #67c23a;
font-weight: 600;
font-size: 14px;
}
.creator-info {
font-size: 11px;
color: #b0b3b8;
}
}
.h5-fab {
position: fixed;
right: 20px;
bottom: 40px;
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #409eff 0%, #337ecc 100%);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.4);
cursor: pointer;
z-index: 99;
transition: transform 0.2s;
&:active {
transform: scale(0.9);
}
}
.h5-load-more {
text-align: center;
padding: 16px;
font-size: 13px;
color: #409eff;
cursor: pointer;
}
.h5-no-more {
text-align: center;
padding: 16px;
font-size: 12px;
color: #c0c4cc;
}
.h5-empty {
padding: 40px 0;
}
.h5-form {
:deep(.el-form-item__label) {
font-weight: 500;
}
}
.h5-form-row {
display: flex;
gap: 12px;
}
.h5-radio-group {
display: flex;
flex-wrap: wrap;
gap: 0;
}
// 回访记录
.h5-visit-header {
margin-bottom: 12px;
}
.h5-visit-list {
padding: 0;
}
.h5-visit-card {
background: #f9fafb;
border-radius: 10px;
padding: 14px;
margin-bottom: 10px;
border-left: 3px solid #409eff;
}
.visit-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.visit-card-tags {
display: flex;
align-items: center;
gap: 8px;
.visit-time {
font-size: 12px;
color: #909399;
}
}
.visit-card-content {
font-size: 14px;
color: #303133;
line-height: 1.6;
margin-bottom: 6px;
}
.visit-card-result {
font-size: 13px;
color: #606266;
margin-bottom: 6px;
.label {
color: #909399;
}
}
.visit-card-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #909399;
.next-visit {
color: #e6a23c;
}
}
// 抽屉底部按钮
:deep(.el-drawer__footer) {
display: flex;
gap: 12px;
padding: 12px 16px;
border-top: 1px solid #f2f3f5;
}
</style>
+513
View File
@@ -0,0 +1,513 @@
<template>
<div class="fans-management">
<!-- 搜索区域 -->
<el-card class="!border-none" shadow="never">
<el-form class="ls-form" :model="formData" inline>
<el-form-item class="w-[280px]" label="姓名">
<el-input
placeholder="请输入姓名"
v-model="formData.name"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item class="w-[280px]" label="手机号">
<el-input
placeholder="请输入手机号"
v-model="formData.phone"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item class="w-[180px]" label="性别">
<el-select v-model="formData.gender" placeholder="请选择" clearable>
<el-option label="男" :value="1" />
<el-option label="女" :value="2" />
</el-select>
</el-form-item>
<el-form-item class="w-[180px]" label="状态">
<el-select v-model="formData.status" placeholder="请选择" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 列表区域 -->
<el-card class="!border-none mt-4" shadow="never">
<div class="mb-4">
<el-button type="primary" @click="handleAdd">新增粉丝</el-button>
</div>
<el-table :data="pager.lists" size="large" v-loading="pager.loading">
<el-table-column label="ID" prop="id" width="70" />
<el-table-column label="姓名" prop="name" min-width="100" />
<el-table-column label="手机号" prop="phone" min-width="130" />
<el-table-column label="身份证号" prop="id_card" min-width="180" />
<el-table-column label="性别" prop="gender_desc" width="80" />
<el-table-column label="年龄" prop="age" width="70" />
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
<el-table-column label="登记人" prop="creator_name" min-width="100">
<template #default="{ row }">
<span>{{ row.creator_name || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="回访次数" width="90" align="center">
<template #default="{ row }">
<el-tag v-if="row.visit_count > 0" type="success">{{ row.visit_count }}</el-tag>
<span v-else class="text-gray-400">0</span>
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag v-if="row.status == 1" type="success">启用</el-tag>
<el-tag v-else type="danger">禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" min-width="170">
<template #default="{ row }">
<span>{{ formatDateTime(row.create_time) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="success" link @click="handleVisitRecords(row)">回访记录</el-button>
<el-button type="danger" link @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex mt-4 justify-end">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<!-- 新增/编辑粉丝弹窗 -->
<el-dialog
v-model="fanDialogVisible"
:title="fanDialogType === 'add' ? '新增粉丝' : '编辑粉丝'"
width="600px"
:close-on-click-modal="false"
@closed="resetFanForm"
>
<el-form
ref="fanFormRef"
:model="fanForm"
:rules="fanRules"
label-width="100px"
>
<el-form-item label="姓名" prop="name">
<el-input v-model="fanForm.name" placeholder="请输入姓名" maxlength="50" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="fanForm.phone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="身份证号" prop="id_card">
<el-input v-model="fanForm.id_card" placeholder="请输入身份证号码" maxlength="18" />
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number v-model="fanForm.age" :min="0" :max="150" />
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="fanForm.gender">
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="fanForm.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="fanForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="fanDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="fanSubmitLoading" @click="handleFanSubmit">确定</el-button>
</template>
</el-dialog>
<!-- 回访记录抽屉 -->
<el-drawer
v-model="visitDrawerVisible"
:title="`回访记录 - ${currentFan?.name || ''}`"
size="700px"
:close-on-click-modal="false"
>
<div class="mb-4">
<el-button type="primary" @click="handleAddVisitRecord">新增回访记录</el-button>
</div>
<el-table :data="visitRecords" size="large" v-loading="visitLoading">
<el-table-column label="回访类型" width="100">
<template #default="{ row }">
<el-tag :type="visitTypeTagMap[row.visit_type]">
{{ row.visit_type_desc }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="回访时间" prop="visit_time_text" min-width="150" />
<el-table-column label="回访内容" prop="content" min-width="200" show-overflow-tooltip />
<el-table-column label="回访结果" prop="result" min-width="150" show-overflow-tooltip />
<el-table-column label="下次回访" prop="next_visit_time_text" min-width="150" />
<el-table-column label="操作人" prop="operator_name" width="100" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEditVisitRecord(row)">编辑</el-button>
<el-button type="danger" link @click="handleDeleteVisitRecord(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="visitTotal > visitRecords.length" class="text-center mt-4">
<el-button link type="primary" @click="loadMoreVisitRecords">加载更多</el-button>
</div>
</el-drawer>
<!-- 新增/编辑回访记录弹窗 -->
<el-dialog
v-model="visitDialogVisible"
:title="visitDialogType === 'add' ? '新增回访记录' : '编辑回访记录'"
width="600px"
:close-on-click-modal="false"
@closed="resetVisitForm"
>
<el-form
ref="visitFormRef"
:model="visitForm"
:rules="visitRules"
label-width="120px"
>
<el-form-item label="回访类型" prop="visit_type">
<el-select v-model="visitForm.visit_type" placeholder="请选择回访类型">
<el-option label="电话" :value="1" />
<el-option label="微信" :value="2" />
<el-option label="短信" :value="3" />
<el-option label="上门" :value="4" />
<el-option label="其他" :value="5" />
</el-select>
</el-form-item>
<el-form-item label="回访时间" prop="visit_time">
<el-date-picker
v-model="visitForm.visit_time"
type="datetime"
placeholder="请选择回访时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="回访内容" prop="content">
<el-input
v-model="visitForm.content"
type="textarea"
:rows="4"
placeholder="请输入回访内容"
/>
</el-form-item>
<el-form-item label="回访结果" prop="result">
<el-input
v-model="visitForm.result"
type="textarea"
:rows="3"
placeholder="请输入回访结果"
/>
</el-form-item>
<el-form-item label="下次回访时间" prop="next_visit_time">
<el-date-picker
v-model="visitForm.next_visit_time"
type="datetime"
placeholder="请选择下次回访时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visitDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="visitSubmitLoading" @click="handleVisitSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="fansManagement">
import {
fanLists,
fanAdd,
fanEdit,
fanDelete,
fanVisitRecordLists,
fanVisitRecordAdd,
fanVisitRecordEdit,
fanVisitRecordDelete
} from '@/api/fans'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import dayjs from 'dayjs'
import type { FormInstance, FormRules } from 'element-plus'
const formatDateTime = (timestamp: number | string) => {
if (!timestamp) return '-'
const ts = String(timestamp).length === 10 ? Number(timestamp) * 1000 : Number(timestamp)
return dayjs(ts).format('YYYY-MM-DD HH:mm:ss')
}
// ==================== 粉丝列表 ====================
const formData = ref({
name: '',
phone: '',
gender: '' as string | number,
status: '' as string | number
})
const { pager, getLists, resetParams, resetPage } = usePaging({
fetchFun: fanLists,
params: formData.value
})
// ==================== 新增/编辑粉丝 ====================
const fanDialogVisible = ref(false)
const fanDialogType = ref<'add' | 'edit'>('add')
const fanFormRef = ref<FormInstance>()
const fanSubmitLoading = ref(false)
const fanForm = ref({
id: 0,
name: '',
phone: '',
id_card: '',
age: 0,
gender: 1,
status: 1,
remark: ''
})
const fanRules: FormRules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
id_card: [
{ pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: '身份证号格式不正确', trigger: 'blur' }
],
gender: [{ required: true, message: '请选择性别', trigger: 'change' }]
}
const handleAdd = () => {
fanDialogType.value = 'add'
fanDialogVisible.value = true
}
const handleEdit = (row: any) => {
fanDialogType.value = 'edit'
fanForm.value = {
id: row.id,
name: row.name,
phone: row.phone,
id_card: row.id_card,
age: row.age,
gender: row.gender,
status: row.status,
remark: row.remark
}
fanDialogVisible.value = true
}
const resetFanForm = () => {
fanForm.value = {
id: 0,
name: '',
phone: '',
id_card: '',
age: 0,
gender: 1,
status: 1,
remark: ''
}
fanFormRef.value?.resetFields()
}
const handleFanSubmit = async () => {
await fanFormRef.value?.validate()
fanSubmitLoading.value = true
try {
if (fanDialogType.value === 'add') {
await fanAdd(fanForm.value)
} else {
await fanEdit(fanForm.value)
}
fanDialogVisible.value = false
getLists()
} catch (error: any) {
} finally {
fanSubmitLoading.value = false
}
}
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除该粉丝吗?')
try {
await fanDelete({ id })
getLists()
} catch (error: any) {
}
}
// ==================== 回访记录 ====================
const visitDrawerVisible = ref(false)
const currentFan = ref<any>(null)
const visitRecords = ref<any[]>([])
const visitLoading = ref(false)
const visitTotal = ref(0)
const visitPageNo = ref(1)
const visitTypeTagMap: Record<number, string> = {
1: '',
2: 'success',
3: 'info',
4: 'warning',
5: 'danger'
}
const handleVisitRecords = (row: any) => {
currentFan.value = row
visitDrawerVisible.value = true
visitPageNo.value = 1
visitRecords.value = []
loadVisitRecords()
}
const loadVisitRecords = async () => {
visitLoading.value = true
try {
const res = await fanVisitRecordLists({
fan_id: currentFan.value.id,
page_no: visitPageNo.value,
page_size: 15
})
if (visitPageNo.value === 1) {
visitRecords.value = res.lists || []
} else {
visitRecords.value = [...visitRecords.value, ...(res.lists || [])]
}
visitTotal.value = res.count || 0
} catch (error) {
console.error('获取回访记录失败:', error)
} finally {
visitLoading.value = false
}
}
const loadMoreVisitRecords = () => {
visitPageNo.value++
loadVisitRecords()
}
// ==================== 新增/编辑回访记录 ====================
const visitDialogVisible = ref(false)
const visitDialogType = ref<'add' | 'edit'>('add')
const visitFormRef = ref<FormInstance>()
const visitSubmitLoading = ref(false)
const visitForm = ref({
id: 0,
fan_id: 0,
visit_type: 1,
visit_time: '',
content: '',
result: '',
next_visit_time: ''
})
const visitRules: FormRules = {
visit_type: [{ required: true, message: '请选择回访类型', trigger: 'change' }],
visit_time: [{ required: true, message: '请选择回访时间', trigger: 'change' }],
content: [{ required: true, message: '请输入回访内容', trigger: 'blur' }]
}
const handleAddVisitRecord = () => {
visitDialogType.value = 'add'
visitForm.value = {
id: 0,
fan_id: currentFan.value.id,
visit_type: 1,
visit_time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
content: '',
result: '',
next_visit_time: ''
}
visitDialogVisible.value = true
}
const handleEditVisitRecord = (row: any) => {
visitDialogType.value = 'edit'
visitForm.value = {
id: row.id,
fan_id: row.fan_id,
visit_type: row.visit_type,
visit_time: row.visit_time_text || '',
content: row.content || '',
result: row.result || '',
next_visit_time: row.next_visit_time_text || ''
}
visitDialogVisible.value = true
}
const resetVisitForm = () => {
visitForm.value = {
id: 0,
fan_id: 0,
visit_type: 1,
visit_time: '',
content: '',
result: '',
next_visit_time: ''
}
visitFormRef.value?.resetFields()
}
const handleVisitSubmit = async () => {
await visitFormRef.value?.validate()
visitSubmitLoading.value = true
try {
if (visitDialogType.value === 'add') {
await fanVisitRecordAdd(visitForm.value)
} else {
await fanVisitRecordEdit(visitForm.value)
}
visitDialogVisible.value = false
visitPageNo.value = 1
loadVisitRecords()
} catch (error: any) {
} finally {
visitSubmitLoading.value = false
}
}
const handleDeleteVisitRecord = async (id: number) => {
await feedback.confirm('确定要删除该回访记录吗?')
try {
await fanVisitRecordDelete({ id })
visitPageNo.value = 1
loadVisitRecords()
} catch (error: any) {
}
}
getLists()
</script>