feat:新增名片夹列表和详情页面,支持搜索和认领功能

This commit is contained in:
2026-10-12 11:31:25 +08:00
parent b49fa378e7
commit a957679957
4 changed files with 550 additions and 18 deletions

View File

@@ -120,6 +120,10 @@ const localDevConfig = ({
uniacid: 237,
domain: 'https://5g.aigc-quickapp.com/',
},
'5g-248': { //中瑞 工业称重系统垃圾吊电子秤,首页,图片链接部分的PDF文档打不开
uniacid: 248,
domain: 'https://5g.aigc-quickapp.com/',
},
'2847': { // 家居装饰品
uniacid: 2847,
domain: 'https://xcx.aigc-quickapp.com/',
@@ -152,10 +156,6 @@ const localDevConfig = ({
uniacid: 12,
domain: 'https://test.aigc-quickapp.com',
},
'248': { // 工业称重系统垃圾吊电子秤,支持自定义字体大小、颜色
uniacid: 248,
domain: 'https://5g.aigc-quickapp.com/',
},
'228': { // 反渗透阻垢剂RO装置 商品分类,没有立即咨询文字
uniacid: 228,
domain: 'https://5g.aigc-quickapp.com/',
@@ -214,27 +214,14 @@ const localDevConfig = ({
uniacid: 3007,
domain: 'https://xcx.aigc-quickapp.com/',
},
'2843': { // 蔬菜种子网
uniacid: 2843,
domain: 'https://xcx.aigc-quickapp.com/',
},
'5g-317': { // 电磁兼容,模板显示不全商品
uniacid: 317,
domain: 'https://5g.aigc-quickapp.com/',
},
'5g-312': { // 天然植物功效活性物
uniacid: 312,
domain: 'https://5g.aigc-quickapp.com/',
},
'5g-248': { // 工业称重
uniacid: 248,
domain: 'https://5g.aigc-quickapp.com/',
},
'5g-325': { // 检验检测机构
uniacid: 325,
domain: 'https://5g.aigc-quickapp.com/',
},
// })['5g-237']; // 商品分类,没有立即咨询文字
// })['5g-97']; // 大宗商品贸易,支持自定义字体大小、颜色
// })['5g-50']; // 燃气安全产品机器人,购物车无反应
@@ -257,7 +244,9 @@ const localDevConfig = ({
// })['2811']; // 本地
// })['5g-317']; // 电磁兼容
// })['5g-351']; //
})['2843']; // 本地
})['2881']; // 本地
// })['5g-248']; // 中瑞 工业称重系统垃圾吊电子秤,首页,图片链接部分的PDF文档打不开
// })['2881']; // 本地
// })['local-2']; // 本地
// })['local-11']; // 本地
// })['1-test']; // test

View File

@@ -1467,6 +1467,19 @@
"navigationBarTitleText": ""
}
},
{
"path": "card/list",
"style": {
"navigationBarTitleText": "我的名片夹",
"enablePullDownRefresh": true
}
},
{
"path": "card/detail",
"style": {
"navigationBarTitleText": "名片详情"
}
},
{
"path": "member/card",
"style": {

221
pages_tool/card/detail.vue Normal file
View File

@@ -0,0 +1,221 @@
<template>
<view class="card-detail">
<!-- 名片头部 -->
<view class="detail-head" v-if="relation">
<view class="head-avatar">
<text>{{ avatarText(relation.name) }}</text>
</view>
<view class="head-info">
<view class="head-name">{{ relation.name || '未命名名片' }}</view>
<view class="head-sub" v-if="relation.job_title || relation.company">
{{ [relation.job_title, relation.company].filter(Boolean).join(' · ') }}
</view>
</view>
</view>
<!-- 字段列表 -->
<view class="detail-fields" v-if="fieldRows.length > 0">
<view v-for="(row, index) in fieldRows" :key="index" class="field-row">
<view class="field-label">{{ row.label }}</view>
<view class="field-value">{{ row.value }}</view>
<view class="field-action" v-if="row.action"
@tap="row.action.type === 'call' ? callPhone(row.value) : copyText2(row.value)">
{{ row.action.text }}
</view>
</view>
</view>
<!-- 加载中 -->
<view class="detail-loading" v-if="isLoading">加载中…</view>
<!-- 不存在 / 无权查看 -->
<ns-empty v-if="!isLoading && !relation" isIndex="false"
:text="errorMsg || '卡片不存在或已被删除'"></ns-empty>
</view>
</template>
<script>
export default {
data() {
return {
cardId: '',
relation: null,
source: null,
shareList: [],
isLoading: true,
errorMsg: ''
};
},
computed: {
fieldRows() {
const r = this.relation;
if (!r) return [];
const rows = [];
const push = (label, value, action) => {
if (value !== undefined && value !== null && String(value).trim() !== '') {
rows.push({ label, value: String(value), action: action || null });
}
};
push('手机', r.mobile, { type: 'call', text: '拨打' });
push('电话', r.phone, { type: 'call', text: '拨打' });
push('邮箱', r.email, { type: 'copy', text: '复制' });
push('微信', r.wechat, { type: 'copy', text: '复制' });
push('网站', r.website);
push('地址', [r.country, r.province, r.city, r.address].filter(Boolean).join(' '));
push('邮编', r.postal_code);
push('Telegram', r.telegram);
push('Messenger', r.messenger);
return rows;
}
},
onLoad(options) {
this.cardId = decodeURIComponent(options.card_id || '');
this.loadDetail();
},
methods: {
avatarText(name) {
return (name || '?').trim().charAt(0).toUpperCase();
},
loadDetail() {
if (!this.cardId) {
this.isLoading = false;
this.errorMsg = '缺少名片编号';
return;
}
this.isLoading = true;
this.$api.sendRequest({
url: '/card/api/card/detail',
data: { card_id: this.cardId },
success: res => {
this.isLoading = false;
if (res.code == 0) {
const data = res.data || {};
this.relation = data.relation || null;
this.source = data.source || null;
this.shareList = data.share_list || [];
} else {
// 后端对"不存在"与"非本人名下"同一报错口径,不区分展示
this.errorMsg = res.message || '卡片不存在或已被删除';
this.relation = null;
}
},
fail: () => {
this.isLoading = false;
this.errorMsg = '网络异常,请稍后重试';
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
}
});
},
callPhone(mobile) {
if (!mobile) return;
uni.makePhoneCall({ phoneNumber: mobile });
},
copyText2(text) {
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' });
}
});
}
}
};
</script>
<style lang="scss" scoped>
.card-detail {
min-height: 100vh;
background-color: #f6f7f9;
padding-bottom: 60rpx;
box-sizing: border-box;
}
.detail-head {
display: flex;
align-items: center;
padding: 48rpx 32rpx;
background: linear-gradient(135deg, var(--main-color, #0054a5), var(--main-color-shallow, #3a7bc8));
color: #ffffff;
.head-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.25);
font-size: 52rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 28rpx;
}
.head-info {
min-width: 0;
.head-name {
font-size: 40rpx;
font-weight: 600;
}
.head-sub {
font-size: 26rpx;
opacity: 0.85;
margin-top: 12rpx;
}
}
}
.detail-fields {
margin: 24rpx;
background-color: #ffffff;
border-radius: 16rpx;
overflow: hidden;
.field-row {
display: flex;
align-items: center;
padding: 28rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.field-label {
width: 140rpx;
font-size: 28rpx;
color: #999;
flex-shrink: 0;
}
.field-value {
flex: 1;
font-size: 28rpx;
color: #333;
word-break: break-all;
}
.field-action {
margin-left: 20rpx;
flex-shrink: 0;
font-size: 24rpx;
color: var(--main-color, #0054a5);
border: 1rpx solid var(--main-color, #0054a5);
border-radius: 28rpx;
padding: 6rpx 24rpx;
}
}
}
.detail-loading {
text-align: center;
color: #999;
font-size: 26rpx;
padding: 120rpx 0;
}
</style>

309
pages_tool/card/list.vue Normal file
View File

@@ -0,0 +1,309 @@
<template>
<view class="card-wallet">
<!-- 搜索区 -->
<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 class="claim-btn" @tap="onClaimTap">认领</view>
</view>
<!-- 列表 -->
<view class="card-list">
<view v-for="(item, index) in list" :key="item.id || index" class="card-item"
@tap="goDetail(item)">
<view class="card-avatar">
<text>{{ 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-arrow">›</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>
export default {
data() {
return {
list: [],
page: 1,
pageSize: 20,
pageCount: 1,
count: 0,
keyword: '',
isLoading: false,
emptyText: '暂无名片,点击右上角「认领」添加'
};
},
onLoad() {
this.refresh();
},
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();
},
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;
this.$api.sendRequest({
url: '/card/api/card/lists',
data: {
page: pageNo,
page_size: this.pageSize,
keyword: this.keyword.trim()
},
success: res => {
if (res.code == 0) {
const data = res.data || {};
const rows = data.list || [];
this.pageCount = data.page_count || 1;
this.count = data.count || 0;
this.page = pageNo;
this.list = pageNo === 1 ? rows : this.list.concat(rows);
this.emptyText = this.keyword.trim()
? '未找到匹配的名片'
: '暂无名片,点击右上角「认领」添加';
} else {
this.handleApiError(res);
}
this.isLoading = false;
typeof done === 'function' && done();
},
fail: () => {
this.isLoading = false;
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
typeof done === 'function' && done();
}
});
},
goDetail(item) {
if (!item.card_id) return;
uni.navigateTo({
url: '/pages_tool/card/detail?card_id=' + encodeURIComponent(item.card_id)
});
},
onClaimTap() {
uni.showModal({
title: '认领名片',
editable: true,
placeholderText: '粘贴商家提供的名片编号(card_id)',
success: res => {
if (!res.confirm) return;
const cardId = (res.content || '').trim();
if (!cardId) {
uni.showToast({ title: '请输入名片编号', icon: 'none' });
return;
}
this.doClaim(cardId);
}
});
},
doClaim(cardId) {
uni.showLoading({ title: '认领中…' });
this.$api.sendRequest({
url: '/card/api/card/claim',
data: { card_id: cardId },
success: res => {
uni.hideLoading();
if (res.code == 0) {
const claimed = res.data && res.data.claimed === false;
uni.showToast({
title: claimed ? '该名片已在您的名片夹中' : '认领成功',
icon: 'success'
});
this.refresh();
} else {
this.handleApiError(res);
}
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
}
});
},
handleApiError(res) {
const msg = (res && res.message) || '操作失败';
// 未登录/凭证失效时引导登录(与全站约定一致:code < 0)
if (res && res.code < 0) {
uni.showToast({ title: msg, icon: 'none' });
return;
}
uni.showToast({ title: msg, icon: 'none' });
}
}
};
</script>
<style lang="scss" scoped>
.card-wallet {
min-height: 100vh;
background-color: #f6f7f9;
padding: 20rpx 24rpx 40rpx;
box-sizing: border-box;
}
.search-bar {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.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;
}
}
.claim-btn {
margin-left: 20rpx;
height: 72rpx;
line-height: 72rpx;
padding: 0 30rpx;
background-color: var(--main-color, #0054a5);
color: #ffffff;
font-size: 28rpx;
border-radius: 36rpx;
}
}
.card-list {
.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;
}
.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-arrow {
color: #ccc;
font-size: 40rpx;
margin-left: 16rpx;
}
}
}
.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>