419 lines
11 KiB
Vue
419 lines
11 KiB
Vue
<template>
|
||
<view class="lead-page">
|
||
<!-- 平级入口: 与我的人脉页互链(导航合并第一步) -->
|
||
<view class="page-switch" @tap="goContacts">
|
||
<text class="switch-label">我的人脉</text>
|
||
<text class="switch-arrow">›</text>
|
||
</view>
|
||
|
||
<!-- 状态筛选 -->
|
||
<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">
|
||
<!-- 点「别人的名片」= 打开对方名片; 拨号/按钮一律 .stop, 避免误触成打电话 -->
|
||
<view v-for="(item, index) in list" :key="item.id || index" :data-index="index" class="lead-item" @tap="viewCard">
|
||
<view class="lead-top">
|
||
<view class="lead-name">{{ item.name || '未留姓名' }}</view>
|
||
<view class="lead-status" :class="'st-' + item.status">{{ item.status_text || '新客户' }}</view>
|
||
</view>
|
||
<!-- 手机号是唯一拨号入口(H5 下调 tel: 浏览器会报 ERR_UNKNOWN_URL_SCHEME, 属正常) -->
|
||
<view class="lead-mobile" v-if="item.mobile" @tap.stop="callPhone(item.mobile)">📱 {{ item.mobile }}</view>
|
||
<view class="lead-note" v-if="item.note">「{{ item.note }}」</view>
|
||
<view class="lead-meta">
|
||
<view class="meta-card" :class="{ tappable: !!cardIdOf(item) }">
|
||
<text class="meta-card-name">来自名片:{{ item.card_name || '—' }}</text>
|
||
<text class="meta-card-arrow" v-if="cardIdOf(item)">查看名片 ›</text>
|
||
</view>
|
||
<text class="meta-time">{{ item.create_time_txt }}</text>
|
||
</view>
|
||
<view class="lead-actions">
|
||
<view class="action-btn copy" v-if="item.wechat" @tap.stop="copyText(item.wechat)">复制微信</view>
|
||
<view class="action-btn primary" :data-index="index" @tap.stop="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="'暂无意向客户,访客在你的名片页留资后会出现在这里'"></ns-empty>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
// 与后端 Lead::STATUSES 一致: 0=新客户 1=已联系 2=已成交 3=无效
|
||
const STATUS_MAP = [
|
||
{ value: 0, label: '新客户' },
|
||
{ value: 1, label: '已联系' },
|
||
{ value: 2, label: '已成交' },
|
||
{ value: 3, label: '无效' }
|
||
];
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
tabs: [{ value: '', label: '全部' }].concat(STATUS_MAP),
|
||
status: '',
|
||
list: [],
|
||
page: 1,
|
||
pageSize: 20,
|
||
pageCount: 1,
|
||
isLoading: false
|
||
};
|
||
},
|
||
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: {
|
||
// 顶部平级入口: $util.redirectTo(鸿蒙禁裸 uni.switchTab, 静默失败)
|
||
goContacts() {
|
||
this.$util.redirectTo('/pages_tool/card/contacts');
|
||
},
|
||
switchTab(value) {
|
||
if (this.status === value) return;
|
||
this.status = value;
|
||
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 条件
|
||
if (this.status !== '') {
|
||
data.status = this.status;
|
||
}
|
||
this.$cardApi.leadLists({
|
||
data: data,
|
||
success: res => {
|
||
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);
|
||
} else {
|
||
uni.showToast({ title: res.message || '加载失败', icon: 'none' });
|
||
}
|
||
this.isLoading = false;
|
||
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: STATUS_MAP.map(s => s.label),
|
||
success: res => {
|
||
const target = STATUS_MAP[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 = STATUS_MAP.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' })
|
||
});
|
||
},
|
||
// 该线索关联的名片编号(后端 lead/lists 行下发; 兼容常见字段名)
|
||
cardIdOf(item) {
|
||
if (!item) return '';
|
||
return item.card_id || item.from_card_id || item.source_card_id || '';
|
||
},
|
||
// 点整条客户卡片 → 打开对方名片
|
||
// 与交换名片(exchange.vue)同款写法: detail 接收 card_id(卡片编号字符串);
|
||
// 本人名下的卡 → 正常详情页; 非本人名下 → detail 兜底跳公开名片页(后端返回的 wap_url)
|
||
viewCard(payload) {
|
||
const item = this.resolveItem(payload);
|
||
const cardId = this.cardIdOf(item);
|
||
if (!cardId) {
|
||
// 后端 lead/lists 目前只下发 card_name(展示名), 没有名片编号 → 无法跳转。
|
||
// 这里把该行真实字段打到控制台, 便于确认后端是否用了别的字段名(如 relation_id)
|
||
console.warn('[card/leads] 该行缺少名片编号(card_id/from_card_id/source_card_id), 无法打开对方名片; 可用字段:',
|
||
item ? Object.keys(item) : 'event', item);
|
||
uni.showToast({ title: '该客户未关联名片,无法查看', icon: 'none' });
|
||
return;
|
||
}
|
||
uni.navigateTo({
|
||
url: '/pages_tool/card/detail?card_id=' + encodeURIComponent(cardId)
|
||
});
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.lead-page {
|
||
min-height: 100vh;
|
||
background-color: #f6f7f9;
|
||
padding-bottom: 40rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.page-switch {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
padding: 16rpx 24rpx 0;
|
||
|
||
.switch-label {
|
||
font-size: 26rpx;
|
||
color: var(--main-color, #0054a5);
|
||
}
|
||
|
||
.switch-arrow {
|
||
font-size: 26rpx;
|
||
color: var(--main-color, #0054a5);
|
||
margin-left: 4rpx;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
align-items: center;
|
||
font-size: 22rpx;
|
||
color: #999;
|
||
margin-top: 14rpx;
|
||
|
||
.meta-card {
|
||
display: flex;
|
||
align-items: center;
|
||
max-width: 64%;
|
||
|
||
// 有名片编号时才高亮/可点, 否则退化为普通文字
|
||
&.tappable .meta-card-name {
|
||
color: var(--main-color, #0054a5);
|
||
}
|
||
|
||
.meta-card-name {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.meta-card-arrow {
|
||
flex-shrink: 0;
|
||
margin-left: 8rpx;
|
||
color: var(--main-color, #0054a5);
|
||
}
|
||
}
|
||
}
|
||
|
||
.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>
|