Files
lucky_shop/pages_tool/card/admin/leads.vue
2026-10-19 16:44:02 +08:00

432 lines
10 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.
<template>
<view class="admin-leads">
<!-- 商家端三大入口平级切换(鸿蒙安全: $util.redirectTo) -->
<view class="page-switch">
<view class="switch-item" @tap="goManage">名片管理</view>
<view class="switch-item" @tap="goContacts">人脉管理</view>
<view class="switch-item active" @tap="goSelf">潜在客户</view>
</view>
<!-- 搜索栏 -->
<view class="search-bar">
<input class="search-input" :value="keyword" placeholder="搜索姓名 / 手机"
confirm-type="search" @input="e => keyword = e.detail.value" @confirm="doSearch" />
<view class="search-btn" @tap="doSearch">搜索</view>
</view>
<!-- 跟进状态筛选(与后端 Lead::STATUSES 对齐, setStatus 仅接受 0-3) -->
<scroll-view scroll-x class="tab-bar">
<view class="tab-inner">
<view v-for="tab in tabs" :key="tab.value" class="tab-item"
:class="{ active: status === tab.value }" @tap="switchTab(tab.value)">
{{ tab.label }}
</view>
</view>
</scroll-view>
<!-- 线索列表 -->
<view class="lead-list">
<view v-for="(item, index) in list" :key="item.id || index" class="lead-item">
<view class="lead-top" @tap="callPhone(item.mobile)">
<view class="lead-name">{{ item.name || '未留姓名' }}</view>
<view class="lead-status" :class="'st-' + item.status">{{ item.status_text || '新客户' }}</view>
</view>
<view class="lead-mobile" v-if="item.mobile" @tap="callPhone(item.mobile)">📱 {{ item.mobile }}</view>
<view class="lead-note" v-if="item.note">「{{ item.note }}」</view>
<view class="lead-meta">
<text class="meta-card">来自名片:{{ item.card_name }}</text>
<text class="meta-time">{{ item.create_time_txt }}</text>
</view>
<view class="lead-actions">
<view class="action-btn copy" v-if="item.wechat" @tap="copyText(item.wechat)">复制微信</view>
<view class="action-btn primary" :data-index="index" @tap="changeStatus">更新状态</view>
</view>
</view>
<view class="load-more" v-if="list.length > 0">
<text v-if="isLoading">加载中…</text>
<text v-else-if="page < pageCount">上拉加载更多</text>
<text v-else class="load-end">— 没有更多了 —</text>
</view>
<ns-empty v-if="!isLoading && list.length === 0" :isIndex="false"
:text="emptyText"></ns-empty>
</view>
</view>
</template>
<script>
/**
* 商家端 · 潜在客户
*
* 接口全部来自后端已就绪的 /card/api/lead/*(收口在 common/js/card-api.js):
* lead/lists 列表(可见范围=自己名下名片收到的线索 + 自己从人脉转入的线索;
* 没有名片时列表为空属正常现象) / lead/setStatus 更新跟进状态(0-3)。
*/
import { LEAD_STATUS } from '@/common/js/card-api.js';
export default {
data() {
return {
tabs: [{ value: '', label: '全部' }].concat(LEAD_STATUS),
status: '',
keyword: '',
list: [],
page: 1,
pageSize: 20,
pageCount: 1,
isLoading: false,
emptyText: '暂无意向客户,访客在你的名片页留资后会出现在这里'
};
},
onLoad() {
this.refresh();
},
onReady() {
// H5 uni-page-head 挂载早于路由 meta 就绪会回落默认标题 "title", onReady 再兜底一次
uni.setNavigationBarTitle({ title: '名片' });
// #ifdef H5
document.title = '名片';
// #endif
},
onPullDownRefresh() {
this.refresh(() => uni.stopPullDownRefresh());
},
onReachBottom() {
if (this.isLoading) return;
if (this.page >= this.pageCount) return;
this.loadPage(this.page + 1);
},
methods: {
// 平级入口: 鸿蒙禁裸 uni.switchTab, 走 $util.redirectTo
goManage() {
this.$util.redirectTo('/pages_tool/card/admin/manage');
},
goContacts() {
this.$util.redirectTo('/pages_tool/card/admin/contacts');
},
goSelf() {},
switchTab(value) {
if (this.status === value) return;
this.status = value;
this.refresh();
},
doSearch() {
this.refresh();
},
refresh(done) {
this.page = 1;
this.loadPage(1, done);
},
loadPage(pageNo, done) {
this.isLoading = true;
const data = { page: pageNo, page_size: this.pageSize };
// 空字符串 = 全部, 后端对 '' 不加 status 条件; status 只传 0-3
if (this.status !== '') data.status = this.status;
if (this.keyword.trim() !== '') data.keyword = this.keyword.trim();
this.$cardApi.leadLists({
data: data,
success: res => {
this.isLoading = false;
if (res.code == 0) {
const d = res.data || {};
const rows = d.list || [];
this.pageCount = d.page_count || 1;
this.page = pageNo;
this.list = pageNo === 1 ? rows : this.list.concat(rows);
this.emptyText = this.keyword.trim() || this.status !== ''
? '未找到匹配的客户'
: '暂无意向客户,访客在你的名片页留资后会出现在这里';
} else {
uni.showToast({ title: res.message || '加载失败', icon: 'none' });
}
typeof done === 'function' && done();
},
fail: () => {
this.isLoading = false;
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
typeof done === 'function' && done();
}
});
},
/**
* 取事件对应的行数据。小程序端 v-for 内联传对象会丢参, 统一 :data-index + 不传参。
* 兼容: 事件对象(data-index) / 数字索引 / 行对象本身。
*/
resolveItem(payload) {
if (payload == null) return null;
if (typeof payload === 'number') return this.list[payload] || null;
const ds = payload.currentTarget && payload.currentTarget.dataset;
if (ds) {
const i = Number(ds.index);
if (!isNaN(i) && this.list[i]) return this.list[i];
}
return payload.id ? payload : null;
},
changeStatus(payload) {
let item, index;
if (payload && payload.currentTarget) {
index = Number(payload.currentTarget.dataset.index);
item = this.list[index];
} else if (typeof payload === 'number') {
index = payload;
item = this.list[index];
} else {
item = payload;
index = this.list.indexOf(item);
}
if (!item) return;
uni.showActionSheet({
itemList: LEAD_STATUS.map(s => s.label),
success: res => {
const target = LEAD_STATUS[res.tapIndex];
if (!target || target.value === item.status) return;
this.doSetStatus(item, index, target.value);
}
});
},
doSetStatus(item, index, status) {
this.$cardApi.leadSetStatus({
data: { id: item.id, status: status },
success: res => {
if (res.code == 0) {
// 就地更新, 不整页刷新
this.list[index].status = status;
this.list[index].status_text = LEAD_STATUS.find(s => s.value === status).label;
uni.showToast({ title: '已更新', icon: 'success' });
} else {
uni.showToast({ title: res.message || '更新失败', icon: 'none' });
}
},
fail: () => {
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
}
});
},
callPhone(mobile) {
if (!mobile) return;
uni.makePhoneCall({ phoneNumber: mobile });
},
copyText(text) {
uni.setClipboardData({
data: text,
success: () => uni.showToast({ title: '已复制', icon: 'success' })
});
}
}
};
</script>
<style lang="scss" scoped>
.admin-leads {
min-height: 100vh;
background-color: #f6f7f9;
padding-bottom: 40rpx;
box-sizing: border-box;
}
.page-switch {
display: flex;
background-color: #ffffff;
padding: 0 12rpx;
.switch-item {
flex: 1;
text-align: center;
padding: 24rpx 0 20rpx;
font-size: 28rpx;
color: #666;
position: relative;
&.active {
color: var(--main-color, #0054a5);
font-weight: 600;
&::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 8rpx;
width: 48rpx;
height: 6rpx;
border-radius: 3rpx;
background-color: var(--main-color, #0054a5);
}
}
}
}
.search-bar {
display: flex;
align-items: center;
padding: 16rpx 24rpx;
.search-input {
flex: 1;
height: 64rpx;
line-height: 64rpx;
background-color: #ffffff;
border-radius: 32rpx;
padding: 0 28rpx;
font-size: 26rpx;
color: #333;
}
.search-btn {
font-size: 26rpx;
color: #666;
padding: 0 24rpx;
}
}
.tab-bar {
background-color: #ffffff;
white-space: nowrap;
.tab-inner {
display: inline-flex;
padding: 0 12rpx;
}
.tab-item {
display: inline-block;
padding: 24rpx 28rpx;
font-size: 28rpx;
color: #666;
position: relative;
&.active {
color: var(--main-color, #0054a5);
font-weight: 600;
&::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 10rpx;
width: 40rpx;
height: 6rpx;
border-radius: 3rpx;
background-color: var(--main-color, #0054a5);
}
}
}
}
.lead-list {
padding: 20rpx 24rpx 0;
}
.lead-item {
background-color: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
.lead-top {
display: flex;
align-items: center;
.lead-name {
flex: 1;
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.lead-status {
font-size: 22rpx;
padding: 6rpx 18rpx;
border-radius: 22rpx;
&.st-0 {
color: var(--main-color, #0054a5);
background-color: var(--main-color-shallow, #e8f1fb);
}
&.st-1 {
color: #e6a23c;
background-color: #fdf3e6;
}
&.st-2 {
color: #1daa39;
background-color: #e8f8ea;
}
&.st-3 {
color: #999;
background-color: #f2f2f2;
}
}
}
.lead-mobile {
font-size: 28rpx;
color: #333;
margin-top: 14rpx;
}
.lead-note {
font-size: 26rpx;
color: #666;
margin-top: 12rpx;
line-height: 1.5;
}
.lead-meta {
display: flex;
justify-content: space-between;
font-size: 22rpx;
color: #999;
margin-top: 14rpx;
.meta-card {
max-width: 60%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.lead-actions {
display: flex;
justify-content: flex-end;
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f0f0f0;
.action-btn {
font-size: 24rpx;
border-radius: 30rpx;
padding: 10rpx 30rpx;
margin-left: 20rpx;
&.copy {
color: #666;
border: 1rpx solid #ddd;
}
&.primary {
color: #ffffff;
background-color: var(--main-color, #0054a5);
}
}
}
}
.load-more {
text-align: center;
color: #999;
font-size: 24rpx;
padding: 24rpx 0;
.load-end {
color: #ccc;
}
}
</style>