feat(card): 员工端「我的人脉」页 + 与潜在客户导航互链
- 新增 contacts.vue: 搜索/来源筛选/列表卡(来源徽标)/编辑/转潜在客户/删除 - 新增 contact-edit.vue: 新增编辑复用, 姓名红星必填, 其余(选填) - leads.vue 顶部互链「我的人脉 ›」(导航合并第一步) - pages.json 注册两页; PAGE_KEY_MAP 补 PAGE_CARD_CONTACTS - card-platform.test.js 追加 4 测试(25/25)
This commit is contained in:
@@ -203,6 +203,7 @@ export const PAGE_KEY_MAP = {
|
||||
// === 名片云(card 插件) ===
|
||||
PAGE_CARD_LIST: '/pages_tool/card/list',
|
||||
PAGE_CARD_LEADS: '/pages_tool/card/leads',
|
||||
PAGE_CARD_CONTACTS: '/pages_tool/card/contacts',
|
||||
|
||||
// === VR ===
|
||||
PAGE_VR_INDEX: '/pages_tool/vr/index',
|
||||
|
||||
13
pages.json
13
pages.json
@@ -1493,6 +1493,19 @@
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "card/contacts",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的人脉",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "card/contact-edit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "编辑人脉"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "card/share",
|
||||
"style": {
|
||||
|
||||
229
pages_tool/card/contact-edit.vue
Normal file
229
pages_tool/card/contact-edit.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<view class="contact-edit">
|
||||
<view class="form-group">
|
||||
<view class="group-title">{{ isEdit ? '编辑人脉' : '添加人脉' }}</view>
|
||||
<view class="form-row">
|
||||
<view class="row-label"><text class="req-star">*</text>姓名</view>
|
||||
<input class="row-input" :value="form.name" placeholder="请输入姓名"
|
||||
@input="e => form.name = e.detail.value" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="row-label">手机</view>
|
||||
<input class="row-input" type="number" :value="form.mobile" placeholder="手机号(选填)"
|
||||
@input="e => form.mobile = e.detail.value" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="row-label">公司</view>
|
||||
<input class="row-input" :value="form.company" placeholder="公司名称(选填)"
|
||||
@input="e => form.company = e.detail.value" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="row-label">职位</view>
|
||||
<input class="row-input" :value="form.job_title" placeholder="职位(选填)"
|
||||
@input="e => form.job_title = e.detail.value" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="row-label">微信号</view>
|
||||
<input class="row-input" :value="form.wechat" placeholder="微信号(选填)"
|
||||
@input="e => form.wechat = e.detail.value" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="row-label">邮箱</view>
|
||||
<input class="row-input" :value="form.email" placeholder="邮箱(选填)"
|
||||
@input="e => form.email = e.detail.value" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="row-label">网站</view>
|
||||
<input class="row-input" :value="form.website" placeholder="个人主页 / 官网(选填)"
|
||||
@input="e => form.website = e.detail.value" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="row-label">地址</view>
|
||||
<input class="row-input" :value="form.address" placeholder="地址(选填)"
|
||||
@input="e => form.address = e.detail.value" />
|
||||
</view>
|
||||
<view class="form-row remark-row">
|
||||
<view class="row-label">备注</view>
|
||||
<textarea class="remark-input" :value="form.remark" placeholder="备注(选填)"
|
||||
@input="e => form.remark = e.detail.value" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 保存 -->
|
||||
<view class="save-bar">
|
||||
<button class="save-btn" :disabled="saving" @tap="save">{{ saving ? '保存中…' : '保存' }}</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 与后端 Contact::EDITABLE_FIELDS 对齐(status 由列表归档操作管理, 编辑页不放)
|
||||
const FIELDS = ['name', 'mobile', 'company', 'job_title', 'wechat', 'email', 'website', 'address', 'remark'];
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
id: 0,
|
||||
isEdit: false,
|
||||
form: {
|
||||
name: '', mobile: '', company: '', job_title: '',
|
||||
wechat: '', email: '', website: '', address: '', remark: ''
|
||||
},
|
||||
saving: false
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
this.id = parseInt(options.id || 0, 10) || 0;
|
||||
this.isEdit = this.id > 0;
|
||||
if (this.isEdit) {
|
||||
this.loadDetail();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadDetail() {
|
||||
this.$api.sendRequest({
|
||||
url: '/card/api/contact/info',
|
||||
data: { id: this.id },
|
||||
success: res => {
|
||||
if (res.code == 0) {
|
||||
const r = res.data || {};
|
||||
FIELDS.forEach(k => {
|
||||
if (r[k] !== undefined && r[k] !== null) {
|
||||
this.form[k] = String(r[k]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: res.message || '人脉不存在',
|
||||
showCancel: false,
|
||||
success: () => this.backToList()
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
save() {
|
||||
if (this.saving) return;
|
||||
if (!this.form.name.trim()) {
|
||||
uni.showToast({ title: '请填写姓名', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
|
||||
const data = { id: this.id, fields: {} };
|
||||
FIELDS.forEach(k => { data.fields[k] = this.form[k]; });
|
||||
|
||||
this.$api.sendJson({
|
||||
url: this.isEdit ? '/card/api/contact/update' : '/card/api/contact/add',
|
||||
data: this.isEdit ? data : { fields: data.fields },
|
||||
success: res => {
|
||||
this.saving = false;
|
||||
if (res.code == 0) {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||
setTimeout(() => this.backToList(), 600);
|
||||
} else {
|
||||
uni.showToast({ title: res.message || '保存失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
this.saving = false;
|
||||
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
// 鸿蒙安全回列表: $util.redirectTo(禁裸 uni.switchTab/uni.navigateBack 兜底)
|
||||
backToList() {
|
||||
this.$util.redirectTo('/pages_tool/card/contacts');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.contact-edit {
|
||||
min-height: 100vh;
|
||||
background-color: #f6f7f9;
|
||||
padding: 24rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.group-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 24rpx 24rpx 8rpx;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
width: 160rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 必填红星(与 card/edit.vue 同款) */
|
||||
.req-star {
|
||||
color: #f5222d;
|
||||
margin-right: 4rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row-input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.remark-row {
|
||||
align-items: flex-start;
|
||||
|
||||
.remark-input {
|
||||
flex: 1;
|
||||
min-height: 120rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.save-bar {
|
||||
padding: 20rpx 0 0;
|
||||
|
||||
.save-btn {
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background-color: var(--main-color, #0054a5);
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
border-radius: 44rpx;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.6;
|
||||
color: #ffffff;
|
||||
background-color: var(--main-color, #0054a5);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
423
pages_tool/card/contacts.vue
Normal file
423
pages_tool/card/contacts.vue
Normal file
@@ -0,0 +1,423 @@
|
||||
<template>
|
||||
<view class="contact-page">
|
||||
<!-- 平级入口: 与潜在客户页互链(导航合并第一步) -->
|
||||
<view class="page-switch" @tap="goLeads">
|
||||
<text class="switch-label">潜在客户</text>
|
||||
<text class="switch-arrow">›</text>
|
||||
</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 class="add-btn" @tap="goEdit()">+ 添加</view>
|
||||
</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: source === tab.value }" @tap="switchTab(tab.value)">
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 人脉列表 -->
|
||||
<view class="contact-list">
|
||||
<view v-for="item in list" :key="item.id" class="contact-item">
|
||||
<view class="item-top" @tap="callPhone(item.mobile)">
|
||||
<view class="item-name">{{ item.name }}</view>
|
||||
<view class="item-source" :class="'src-' + item.source">{{ item.source_text }}</view>
|
||||
</view>
|
||||
<view class="item-company" v-if="item.company || item.job_title">
|
||||
{{ item.company }}<text v-if="item.company && item.job_title"> · </text>{{ item.job_title }}
|
||||
</view>
|
||||
<view class="item-mobile" v-if="item.mobile" @tap="callPhone(item.mobile)">{{ item.mobile }}</view>
|
||||
<view class="item-remark" v-if="item.remark">「{{ item.remark }}」</view>
|
||||
<view class="item-meta">
|
||||
<text class="meta-time">{{ item.create_time_txt }}</text>
|
||||
<text class="meta-status" v-if="item.status == 1">已归档</text>
|
||||
</view>
|
||||
<view class="item-actions">
|
||||
<view class="action-btn plain" @tap="goEdit(item.id)">编辑</view>
|
||||
<view class="action-btn primary" @tap="convertToLead(item)">转为潜在客户</view>
|
||||
<view class="action-btn danger" @tap="delContact(item)">删除</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>
|
||||
// 来源筛选: 与后端 Contact::SOURCES 一致; scan/exchange 暂无产生入口, 本轮只放出 3 个 tab
|
||||
const SOURCE_TABS = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 'manual', label: '手工录入' },
|
||||
{ value: 'lead_in', label: '潜在客户转入' }
|
||||
];
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tabs: SOURCE_TABS,
|
||||
source: '',
|
||||
keyword: '',
|
||||
list: [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
pageCount: 1,
|
||||
isLoading: false
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.refresh();
|
||||
},
|
||||
onShow() {
|
||||
// 从编辑页保存后 redirectTo 回来会重走 onLoad; 从其它页返回时兜底刷新
|
||||
if (this.list.length > 0) {
|
||||
this.refresh();
|
||||
}
|
||||
},
|
||||
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, 静默失败)
|
||||
goLeads() {
|
||||
this.$util.redirectTo('/pages_tool/card/leads');
|
||||
},
|
||||
goEdit(id) {
|
||||
const url = id ? '/pages_tool/card/contact-edit?id=' + id : '/pages_tool/card/contact-edit';
|
||||
this.$util.redirectTo(url);
|
||||
},
|
||||
switchTab(value) {
|
||||
if (this.source === value) return;
|
||||
this.source = 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 };
|
||||
// 空字符串 = 全部, 后端对 '' 不加 source 条件
|
||||
if (this.source !== '') {
|
||||
data.source = this.source;
|
||||
}
|
||||
if (this.keyword.trim() !== '') {
|
||||
data.keyword = this.keyword.trim();
|
||||
}
|
||||
this.$api.sendRequest({
|
||||
url: '/card/api/contact/lists',
|
||||
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();
|
||||
}
|
||||
});
|
||||
},
|
||||
convertToLead(item) {
|
||||
uni.showModal({
|
||||
title: '转为潜在客户',
|
||||
content: '将「' + (item.name || '') + '」转入潜在客户跟进?人脉记录会保留。',
|
||||
success: res => {
|
||||
if (!res.confirm) return;
|
||||
this.doConvert(item);
|
||||
}
|
||||
});
|
||||
},
|
||||
doConvert(item) {
|
||||
uni.showLoading({ title: '转换中…', mask: true });
|
||||
this.$api.sendRequest({
|
||||
url: '/card/api/contact/toLead',
|
||||
data: { id: item.id },
|
||||
success: res => {
|
||||
uni.hideLoading();
|
||||
if (res.code == 0) {
|
||||
uni.showToast({ title: '已转入潜在客户', icon: 'success' });
|
||||
} else {
|
||||
uni.showToast({ title: res.message || '转换失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
delContact(item) {
|
||||
uni.showModal({
|
||||
title: '删除人脉',
|
||||
content: '删除后不可恢复,确定删除「' + (item.name || '') + '」?',
|
||||
confirmColor: '#f5222d',
|
||||
success: res => {
|
||||
if (!res.confirm) return;
|
||||
this.doDelete(item);
|
||||
}
|
||||
});
|
||||
},
|
||||
doDelete(item) {
|
||||
this.$api.sendRequest({
|
||||
url: '/card/api/contact/delete',
|
||||
data: { id: item.id },
|
||||
success: res => {
|
||||
if (res.code == 0) {
|
||||
this.list = this.list.filter(c => c.id !== item.id);
|
||||
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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.contact-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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
font-size: 26rpx;
|
||||
color: #ffffff;
|
||||
background-color: var(--main-color, #0054a5);
|
||||
border-radius: 32rpx;
|
||||
padding: 12rpx 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-list {
|
||||
padding: 20rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.item-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.item-name {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.item-source {
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 18rpx;
|
||||
border-radius: 22rpx;
|
||||
|
||||
&.src-manual {
|
||||
color: var(--main-color, #0054a5);
|
||||
background-color: var(--main-color-shallow, #e8f1fb);
|
||||
}
|
||||
|
||||
&.src-lead_in {
|
||||
color: #1daa39;
|
||||
background-color: #e8f8ea;
|
||||
}
|
||||
|
||||
&.src-scan,
|
||||
&.src-exchange {
|
||||
color: #999;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-company {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.item-mobile {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
|
||||
.item-remark {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-top: 12rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
|
||||
.item-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 26rpx;
|
||||
margin-left: 16rpx;
|
||||
|
||||
&.plain {
|
||||
color: #666;
|
||||
border: 1rpx solid #ddd;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: #ffffff;
|
||||
background-color: var(--main-color, #0054a5);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #f5222d;
|
||||
border: 1rpx solid #f5222d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
padding: 24rpx 0;
|
||||
|
||||
.load-end {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,11 @@
|
||||
<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">
|
||||
@@ -74,6 +80,10 @@ export default {
|
||||
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;
|
||||
@@ -164,6 +174,24 @@ export default {
|
||||
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;
|
||||
|
||||
@@ -343,3 +343,53 @@ test('选择链接: card 页面不在 TabBar(NO_TAB_PAGES 语义)不影响导航
|
||||
assert.ok(tabBlock, 'TAB_BAR_KEYS 应存在');
|
||||
assert.ok(!/card/.test(tabBlock[0]), 'TAB_BAR_KEYS 不应含 card 路径(card 非 tabBar 页)');
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 2.0.0 人脉库: contacts / contact-edit 注册 + 互链 + 鸿蒙安全
|
||||
// ============================================================
|
||||
|
||||
test('人脉库: PAGE_KEY_MAP 含 PAGE_CARD_CONTACTS', () => {
|
||||
const utilSrc = fs.readFileSync(path.resolve(__dirname, '../common/js/util.js'), 'utf8');
|
||||
const m = utilSrc.match(/PAGE_CARD_CONTACTS:\s*'([^']+)'/);
|
||||
assert.ok(m, 'PAGE_KEY_MAP 应有 PAGE_CARD_CONTACTS');
|
||||
assert.equal(m[1], '/pages_tool/card/contacts');
|
||||
});
|
||||
|
||||
test('人脉库: contacts/contact-edit 已注册进 pages.json 且不在 TabBar', () => {
|
||||
const pagesSrc = fs.readFileSync(path.resolve(__dirname, '../pages.json'), 'utf8');
|
||||
const stripped = pagesSrc.replace(/^\s*\/\/.*$/gm, '').replace(/,(\s*[}\]])/g, '$1');
|
||||
const pages = JSON.parse(stripped);
|
||||
const sub = (pages.subPackages || []).find(sp => sp.root === 'pages_tool');
|
||||
assert.ok(sub, 'pages_tool 分包应存在');
|
||||
const paths = (sub.pages || []).map(p => p.path);
|
||||
assert.ok(paths.includes('card/contacts'), `card/contacts 未注册: ${paths.join(',')}`);
|
||||
assert.ok(paths.includes('card/contact-edit'), `card/contact-edit 未注册: ${paths.join(',')}`);
|
||||
// TabBar 根页不在 pages_tool 分包, 天然不在 TabBar; 防回归: tabBar 列表不得含 card
|
||||
const tabPages = (pages.tabBar && pages.tabBar.list ? pages.tabBar.list : []).map(p => p.pagePath);
|
||||
assert.ok(!tabPages.some(p => /card/.test(p)), 'TabBar 不应含 card 页面');
|
||||
});
|
||||
|
||||
test('人脉库: contacts/leads 顶部互链都走 $util.redirectTo(鸿蒙静默失败回归)', () => {
|
||||
const contactsSrc = read('contacts.vue');
|
||||
const leadsSrc = read('leads.vue');
|
||||
// contacts → leads
|
||||
assert.ok(/goLeads\(\)\s*\{[\s\S]*?\$util\.redirectTo\('\/pages_tool\/card\/leads'\)/.test(contactsSrc),
|
||||
'contacts.vue 顶部「潜在客户」链接必须走 $util.redirectTo');
|
||||
// leads → contacts
|
||||
assert.ok(/goContacts\(\)\s*\{[\s\S]*?\$util\.redirectTo\('\/pages_tool\/card\/contacts'\)/.test(leadsSrc),
|
||||
'leads.vue 顶部「我的人脉」链接必须走 $util.redirectTo');
|
||||
// 两文件不得出现裸 uni.switchTab 调用
|
||||
for (const [name, src] of [['contacts.vue', contactsSrc], ['leads.vue', leadsSrc]]) {
|
||||
assert.ok(!/uni\.switchTab\s*\(/.test(src), `${name} 不得直接调 uni.switchTab`);
|
||||
}
|
||||
});
|
||||
|
||||
test('人脉库: contact-edit 保存成功回 contacts 走 $util.redirectTo(鸿蒙安全)', () => {
|
||||
const src = read('contact-edit.vue');
|
||||
assert.ok(/backToList\(\)\s*\{[\s\S]*?\$util\.redirectTo\('\/pages_tool\/card\/contacts'\)/.test(src),
|
||||
'保存成功回列表必须走 $util.redirectTo');
|
||||
assert.ok(!/uni\.switchTab\s*\(/.test(src), 'contact-edit.vue 不得直接调 uni.switchTab');
|
||||
// 姓名必填: 模板有 req-star + 保存前有空名校验
|
||||
assert.ok(/req-star/.test(src), '姓名必填应有 req-star 红星');
|
||||
assert.ok(/请填写姓名/.test(src), '保存前应校验姓名非空');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user