433 lines
10 KiB
Vue
433 lines
10 KiB
Vue
<template>
|
|
<view class="admin-manage">
|
|
<!-- 商家端三大入口平级切换(鸿蒙安全: $util.redirectTo, 禁裸 uni.switchTab) -->
|
|
<view class="page-switch">
|
|
<view class="switch-item active" @tap="goSelf">名片管理</view>
|
|
<view class="switch-item" @tap="goContacts">人脉管理</view>
|
|
<view class="switch-item" @tap="goLeads">潜在客户</view>
|
|
</view>
|
|
|
|
<!-- 功能预告: 查重合并/批量制作/导出等依赖后台接口, 就绪后开放 -->
|
|
<view class="notice-bar">
|
|
<text class="notice-icon">i</text>
|
|
<text class="notice-text">查重合并、批量制作、导出 CSV/vCard 等功能即将上线</text>
|
|
</view>
|
|
|
|
<!-- 搜索区 -->
|
|
<view class="search-bar">
|
|
<view class="search-input-wrap">
|
|
<input class="search-input" confirm-type="search" :value="keyword"
|
|
placeholder="搜索姓名 / 手机 / 公司" @confirm="onSearch" @input="onKeywordInput" />
|
|
<text v-if="keyword" class="search-clear" @tap="clearKeyword">✕</text>
|
|
<text class="iconfont icon-sousuo3" @tap="onSearch"></text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 名片列表 -->
|
|
<view class="card-list">
|
|
<view v-for="(item, index) in list" :key="item.card_id || index" :data-index="index" class="card-item"
|
|
@tap="goDetail">
|
|
<view class="card-avatar">
|
|
<image v-if="item.avatar" :src="$util.img(item.avatar)" class="avatar-img" mode="aspectFill" />
|
|
<text v-else>{{ avatarText(item.name) }}</text>
|
|
</view>
|
|
<view class="card-info">
|
|
<view class="card-name line1">{{ item.name || '未命名名片' }}</view>
|
|
<view class="card-sub line1" v-if="item.job_title || item.company">
|
|
{{ [item.job_title, item.company].filter(Boolean).join(' · ') }}
|
|
</view>
|
|
<view class="card-mobile" v-if="item.mobile">📱 {{ item.mobile }}</view>
|
|
</view>
|
|
<view class="card-side">
|
|
<text v-if="sourceLabel(item)" class="source-tag">{{ sourceLabel(item) }}</text>
|
|
<view class="card-actions" @tap.stop="">
|
|
<text class="act-btn" :data-index="index" @tap="goEdit">编辑</text>
|
|
<text class="act-btn primary" :data-index="index" @tap="goDetail">详情</text>
|
|
</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/lists + 既有 /pages_tool/card/detail、edit 页
|
|
* (路由参数 card_id, 字符串主键)。
|
|
* 查重合并 / 批量制作 / 新建数字名片 / 导出 CSV/vCard / 标签·状态·类型·跟进·收藏
|
|
* 筛选依赖后台未就绪接口, 后台文档(CARD_API_REFERENCE.md)到位后在本页扩展,
|
|
* 端点届时收口进 common/js/card-api.js, 页面只改 this.$cardApi 调用。
|
|
*/
|
|
import { CARD_SOURCE_TYPES } from '@/common/js/card-api.js';
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
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
|
|
},
|
|
onShow() {
|
|
// 从编辑页返回时刷新(编辑成功后数据已变)
|
|
if (this._loadedOnce) this.refresh();
|
|
this._loadedOnce = true;
|
|
},
|
|
onPullDownRefresh() {
|
|
this.refresh(() => uni.stopPullDownRefresh());
|
|
},
|
|
onReachBottom() {
|
|
if (this.isLoading) return;
|
|
if (this.page >= this.pageCount) return;
|
|
this.loadPage(this.page + 1);
|
|
},
|
|
methods: {
|
|
avatarText(name) {
|
|
return (name || '?').trim().charAt(0).toUpperCase();
|
|
},
|
|
// 来源标签: 优先后端译文(source_type_text), 缺失时按字典兜底(与后端 source_type 对齐)
|
|
sourceLabel(item) {
|
|
if (!item) return '';
|
|
if (item.source_type_text) return item.source_type_text;
|
|
const hit = CARD_SOURCE_TYPES.find(s => s.value === item.source_type);
|
|
return hit ? hit.label : '';
|
|
},
|
|
onKeywordInput(e) {
|
|
this.keyword = e.detail.value;
|
|
},
|
|
onSearch() {
|
|
this.refresh();
|
|
},
|
|
clearKeyword() {
|
|
this.keyword = '';
|
|
this.refresh();
|
|
},
|
|
refresh(done) {
|
|
this.page = 1;
|
|
this.loadPage(1, done);
|
|
},
|
|
loadPage(pageNo, done) {
|
|
this.isLoading = true;
|
|
// page_size 上限 100(后端硬限制), 这里 20 足够
|
|
this.$cardApi.cardLists({
|
|
data: {
|
|
page: pageNo,
|
|
page_size: this.pageSize,
|
|
keyword: this.keyword.trim()
|
|
},
|
|
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() ? '未找到匹配的名片' : '暂无名片刻';
|
|
} else {
|
|
// 后端 code!=0 的 message 可直接 toast
|
|
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 内联传对象(如 goDetail(item))会丢参,
|
|
* 统一改为 :data-index + 不传参, 这里从 dataset 取索引回表。
|
|
*/
|
|
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.card_id ? payload : null;
|
|
},
|
|
// 名片路由参数一律 card_id(字符串), encodeURIComponent 幂等
|
|
goDetail(payload) {
|
|
const item = this.resolveItem(payload);
|
|
if (!item || !item.card_id) return;
|
|
uni.navigateTo({
|
|
url: '/pages_tool/card/detail?card_id=' + encodeURIComponent(item.card_id)
|
|
});
|
|
},
|
|
goEdit(payload) {
|
|
const item = this.resolveItem(payload);
|
|
if (!item || !item.card_id) return;
|
|
uni.navigateTo({
|
|
url: '/pages_tool/card/edit?card_id=' + encodeURIComponent(item.card_id)
|
|
});
|
|
},
|
|
// 平级入口: 鸿蒙元服务上 uni.switchTab 静默失败, 必须走 $util.redirectTo
|
|
goContacts() {
|
|
this.$util.redirectTo('/pages_tool/card/admin/contacts');
|
|
},
|
|
goLeads() {
|
|
this.$util.redirectTo('/pages_tool/card/admin/leads');
|
|
},
|
|
goSelf() {
|
|
// 当前页, 无需跳转
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.admin-manage {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 功能预告条 */
|
|
.notice-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
margin: 20rpx 24rpx 0;
|
|
padding: 16rpx 20rpx;
|
|
background-color: var(--main-color-shallow, #e8f1fb);
|
|
border-radius: 12rpx;
|
|
|
|
.notice-icon {
|
|
width: 32rpx;
|
|
height: 32rpx;
|
|
line-height: 32rpx;
|
|
text-align: center;
|
|
border-radius: 50%;
|
|
background-color: var(--main-color, #0054a5);
|
|
color: #ffffff;
|
|
font-size: 22rpx;
|
|
flex-shrink: 0;
|
|
margin-right: 12rpx;
|
|
}
|
|
|
|
.notice-text {
|
|
font-size: 24rpx;
|
|
color: var(--main-color, #0054a5);
|
|
line-height: 1.4;
|
|
}
|
|
}
|
|
|
|
.search-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 20rpx 24rpx 0;
|
|
|
|
.search-input-wrap {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
background-color: #ffffff;
|
|
border-radius: 36rpx;
|
|
padding: 0 24rpx;
|
|
height: 72rpx;
|
|
|
|
.search-input {
|
|
flex: 1;
|
|
height: 72rpx;
|
|
font-size: 28rpx;
|
|
color: #333;
|
|
}
|
|
|
|
.search-clear {
|
|
padding: 0 12rpx;
|
|
color: #bbb;
|
|
font-size: 28rpx;
|
|
}
|
|
|
|
.iconfont {
|
|
color: #999;
|
|
font-size: 32rpx;
|
|
padding-left: 12rpx;
|
|
}
|
|
}
|
|
}
|
|
|
|
.card-list {
|
|
padding: 20rpx 24rpx 0;
|
|
}
|
|
|
|
.card-item {
|
|
display: flex;
|
|
align-items: center;
|
|
background-color: #ffffff;
|
|
border-radius: 16rpx;
|
|
padding: 28rpx 24rpx;
|
|
margin-bottom: 20rpx;
|
|
|
|
&:active {
|
|
background-color: #f0f2f5;
|
|
}
|
|
|
|
.card-avatar {
|
|
width: 88rpx;
|
|
height: 88rpx;
|
|
border-radius: 50%;
|
|
background-color: var(--main-color-shallow, #e8f1fb);
|
|
color: var(--main-color, #0054a5);
|
|
font-size: 36rpx;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
margin-right: 24rpx;
|
|
overflow: hidden;
|
|
|
|
.avatar-img {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
.card-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
|
|
.card-name {
|
|
font-size: 32rpx;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.card-sub {
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
margin-top: 8rpx;
|
|
}
|
|
|
|
.card-mobile {
|
|
font-size: 26rpx;
|
|
color: #999;
|
|
margin-top: 8rpx;
|
|
}
|
|
}
|
|
|
|
.card-side {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
flex-shrink: 0;
|
|
margin-left: 16rpx;
|
|
|
|
.source-tag {
|
|
font-size: 20rpx;
|
|
color: #999;
|
|
background-color: #f2f2f2;
|
|
border-radius: 20rpx;
|
|
padding: 4rpx 14rpx;
|
|
margin-bottom: 12rpx;
|
|
}
|
|
|
|
.card-actions {
|
|
display: flex;
|
|
|
|
.act-btn {
|
|
font-size: 24rpx;
|
|
color: #666;
|
|
border: 1rpx solid #ddd;
|
|
border-radius: 28rpx;
|
|
padding: 8rpx 22rpx;
|
|
|
|
& + .act-btn {
|
|
margin-left: 12rpx;
|
|
}
|
|
|
|
&.primary {
|
|
color: var(--main-color, #0054a5);
|
|
border-color: var(--main-color, #0054a5);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.load-more {
|
|
text-align: center;
|
|
color: #999;
|
|
font-size: 24rpx;
|
|
padding: 24rpx 0;
|
|
|
|
.load-end {
|
|
color: #ccc;
|
|
}
|
|
}
|
|
|
|
.line1 {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|