chore:名片样式的名片模板第一个模板

This commit is contained in:
2026-10-14 16:53:03 +08:00
parent 022a8bb5bb
commit e60b3e2e52
7 changed files with 2691 additions and 0 deletions

View File

@@ -0,0 +1,450 @@
<template>
<view class="admin-contacts">
<!-- 商家端三大入口平级切换(鸿蒙安全: $util.redirectTo) -->
<view class="page-switch">
<view class="switch-item" @tap="goManage">名片管理</view>
<view class="switch-item active" @tap="goSelf">人脉管理</view>
<view class="switch-item" @tap="goLeads">潜在客户</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 class="add-btn" @tap="goEdit()">+ 添加</view>
</view>
<!-- 来源筛选(与后端 Contact::SOURCES 对齐) -->
<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>
/**
* 商家端 · 人脉管理
*
* 接口全部来自后端已就绪的 /card/api/contact/*(收口在 common/js/card-api.js):
* contact/lists 列表 / contact/add+update 增改(复用 contact-edit 页) /
* contact/toLead 转线索 / contact/delete 真删。
* 与员工端「我的人脉」的区别: 本页是商家后台视角, 带来源筛选与三大入口互链;
* 编辑页共用 contact-edit.vue(from=admin 时保存后回本页)。
*/
import { CONTACT_SOURCES } from '@/common/js/card-api.js';
// 来源筛选: scan/exchange 暂无产生入口, 只放出 manual / lead_in 两个 tab
const SOURCE_TABS = [{ value: '', label: '全部' }].concat(
CONTACT_SOURCES.filter(s => s.value === 'manual' || s.value === 'lead_in')
);
export default {
data() {
return {
tabs: SOURCE_TABS,
source: '',
keyword: '',
list: [],
page: 1,
pageSize: 20,
pageCount: 1,
isLoading: false
};
},
onLoad() {
this.refresh();
},
onShow() {
// 从编辑页保存返回(redirectTo 回本页)时刷新
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: {
// 平级入口: 鸿蒙禁裸 uni.switchTab, 走 $util.redirectTo
goManage() {
this.$util.redirectTo('/pages_tool/card/admin/manage');
},
goLeads() {
this.$util.redirectTo('/pages_tool/card/admin/leads');
},
goSelf() {},
// 编辑页共用 contact-edit; from=admin 时保存后回本页(见 contact-edit.vue#backToList)
goEdit(id) {
const url = id
? '/pages_tool/card/contact-edit?from=admin&id=' + id
: '/pages_tool/card/contact-edit?from=admin';
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.$cardApi.contactLists({
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);
} 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();
}
});
},
convertToLead(item) {
uni.showModal({
title: '转为潜在客户',
content: '将「' + (item.name || '') + '」转入潜在客户跟进?人脉记录会保留。',
success: res => {
if (!res.confirm) return;
this.doConvert(item);
}
});
},
doConvert(item) {
uni.showLoading({ title: '转换中…', mask: true });
// 后端: 写一条线索且不删人脉; 同员工同手机号已转过则报错(message 可直接展示)
this.$cardApi.contactToLead({
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.$cardApi.contactDelete({
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>
.admin-contacts {
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;
}
.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>

View File

@@ -0,0 +1,398 @@
<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" @tap="changeStatus(item, index)">更新状态</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();
},
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();
}
});
},
changeStatus(item, index) {
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>

View File

@@ -0,0 +1,409 @@
<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" class="card-item"
@tap="goDetail(item)">
<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" @tap="goEdit(item)">编辑</text>
<text class="act-btn primary" @tap="goDetail(item)">详情</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();
},
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();
}
});
},
/* ---------- 导航 ---------- */
// 名片路由参数一律 card_id(字符串), encodeURIComponent 幂等
goDetail(item) {
if (!item || !item.card_id) return;
uni.navigateTo({
url: '/pages_tool/card/detail?card_id=' + encodeURIComponent(item.card_id)
});
},
goEdit(item) {
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>

View File

@@ -0,0 +1,327 @@
<template>
<view class="ex-page">
<!-- 状态筛选: 全部 / 待处理(N) / 已同意 -->
<view class="ex-tabs">
<view v-for="tab in tabs" :key="tab.value" class="ex-tab"
:class="{ active: current === tab.value }" @tap="switchTab(tab.value)">
{{ tab.label }}
</view>
</view>
<!-- 交换请求列表 -->
<view class="ex-list">
<view v-for="item in shownList" :key="item.id" class="ex-item">
<view class="ex-top">
<view class="ex-avatar-wrap" @tap.stop="viewCard(item)">
<image v-if="item.avatar" class="ex-avatar" :src="item.avatar" mode="aspectFill" />
<view v-else class="ex-avatar ex-avatar-ph">{{ item.name.charAt(0) }}</view>
<view class="ex-avatar-tag">示例交换</view>
</view>
<view class="ex-info">
<view class="ex-name-row">
<text class="ex-name">{{ item.name }}</text>
<text class="ex-badge iconfont icon-dui"></text>
<text class="ex-badge iconfont icon-dui"></text>
</view>
<text class="ex-job">{{ item.job_title }}</text>
<view class="ex-company-row">
<text class="ex-tag">示例交换</text>
<text class="ex-company">{{ item.company }}</text>
</view>
</view>
<view v-if="item.status === 'pending'" class="ex-agree" @tap.stop="onAgree(item)">同意</view>
<text v-else class="ex-agreed">已同意</text>
</view>
<view class="ex-reason">对方发起了名片交换请求</view>
<view class="ex-time">{{ item.create_time_txt }} 向您发起名片交换</view>
</view>
<ns-empty v-if="shownList.length === 0" :isIndex="false" :text="emptyText"></ns-empty>
</view>
</view>
</template>
<script>
// 名片交换请求列表
// ⚠️ 目前为静态占位数据: 后端 /card/api/exchange/* 接口就绪后,
// 改为在 onLoad 里用 this.$cardApi.exchangeLists({ data: { page: 1 }, ... }) 拉取,
// 「同意」改成 cardApi.exchangeAgree({ data: { id: item.id } })。
export default {
data() {
return {
current: 'pending',
cardId: '',
list: [],
page: 1,
pageCount: 1,
loading: false
};
},
onLoad(options) {
this.cardId = decodeURIComponent(options.card_id || '');
this.loadList();
},
computed: {
tabs() {
const pending = this.list.filter(i => i.status === 'pending').length;
return [
{ value: 'all', label: '全部' },
{ value: 'pending', label: '待处理(' + pending + ')' },
{ value: 'agreed', label: '已同意' }
];
},
shownList() {
if (this.current === 'all') return this.list;
return this.list.filter(item => item.status === this.current);
},
emptyText() {
return this.current === 'agreed' ? '暂无已同意的交换' : '暂无名片交换请求';
}
},
methods: {
switchTab(value) {
if (this.current === value) return;
this.current = value;
},
// 待处理交换请求(后端只返回 status=0); 归一化 status 供模板区分
loadList() {
this.loading = true;
this.$cardApi.exchangeLists({
data: { page: this.page, page_size: 20 },
success: res => {
this.loading = false;
if (res.code == 0 && res.data) {
this.list = (res.data.list || []).map(it => ({
id: it.id,
from_card_id: it.from_card_id,
name: it.name,
avatar: it.avatar,
job_title: it.job_title,
company: it.company,
create_time_txt: it.create_time_txt,
status: it.status === 0 ? 'pending' : 'agreed'
}));
this.pageCount = res.data.page_count || 1;
} else if (res.message) {
uni.showToast({ title: res.message, icon: 'none' });
}
},
fail: () => {
this.loading = false;
}
});
},
// 点对方名片 → 跳转对方名片详情(用 from_card_id 字符串)
viewCard(item) {
if (!item || !item.from_card_id) return;
uni.navigateTo({
url: '/pages_tool/card/detail?card_id=' + encodeURIComponent(item.from_card_id)
});
},
onAgree(item) {
this.$cardApi.exchangeAgree({
data: { id: item.id },
success: res => {
if (res.code == 0) {
// receiver_created=0 也是正常成功(同人脉已存在被跳过), 不当失败
const d = res.data || {};
const txt = d.receiver_created === 0 ? '已交换(人脉已存在)' : '已交换,人脉已互加';
uni.showToast({ title: txt, icon: 'none' });
this.loadList();
} else if (res.message) {
uni.showToast({ title: res.message, icon: 'none' });
}
},
fail: () => {}
});
},
onIgnore(item) {
this.$cardApi.exchangeIgnore({
data: { id: item.id },
success: res => {
if (res.code == 0) {
uni.showToast({ title: '已忽略', icon: 'none' });
this.loadList();
} else if (res.message) {
uni.showToast({ title: res.message, icon: 'none' });
}
},
fail: () => {}
});
}
}
};
</script>
<style lang="scss" scoped>
.ex-page {
min-height: 100vh;
background-color: #f6f7f9;
padding-bottom: 40rpx;
box-sizing: border-box;
}
// 筛选胶囊: 选中为浅蓝底蓝字, 未选中白底灰字
.ex-tabs {
display: flex;
padding: 20rpx 24rpx;
.ex-tab {
padding: 12rpx 28rpx;
margin-right: 16rpx;
border-radius: 32rpx;
background-color: #ffffff;
font-size: 26rpx;
color: #666666;
&.active {
background-color: #e8f3ff;
color: #2aa5ff;
font-weight: 600;
}
}
}
.ex-list {
padding: 0 24rpx;
}
.ex-item {
background-color: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.ex-top {
display: flex;
align-items: center;
}
.ex-avatar-wrap {
position: relative;
width: 100rpx;
height: 100rpx;
flex-shrink: 0;
}
.ex-avatar {
width: 100%;
height: 100%;
border-radius: 16rpx;
background-color: #2aa5ff;
}
.ex-avatar-ph {
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 40rpx;
font-weight: 600;
}
.ex-avatar-tag {
position: absolute;
left: 50%;
bottom: -10rpx;
transform: translateX(-50%);
padding: 2rpx 10rpx;
border-radius: 6rpx;
background-color: #2aa5ff;
color: #ffffff;
font-size: 16rpx;
line-height: 1.5;
white-space: nowrap;
}
.ex-info {
flex: 1;
min-width: 0;
margin: 0 16rpx 0 20rpx;
}
.ex-name-row {
display: flex;
align-items: center;
}
.ex-name {
font-size: 30rpx;
font-weight: 600;
color: #333333;
}
.ex-badge {
margin-left: 6rpx;
font-size: 26rpx;
color: #2aa5ff;
}
.ex-job {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #7b8794;
}
.ex-company-row {
display: flex;
align-items: center;
margin-top: 10rpx;
}
.ex-tag {
flex-shrink: 0;
padding: 2rpx 12rpx;
margin-right: 10rpx;
border-radius: 6rpx;
background-color: #2aa5ff;
color: #ffffff;
font-size: 20rpx;
line-height: 1.5;
}
.ex-company {
flex: 1;
min-width: 0;
font-size: 26rpx;
color: #333333;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.ex-agree {
flex-shrink: 0;
padding: 12rpx 34rpx;
border-radius: 34rpx;
background-color: #2aa5ff;
color: #ffffff;
font-size: 26rpx;
}
.ex-agreed {
flex-shrink: 0;
font-size: 24rpx;
color: #9aa4b0;
}
.ex-reason {
margin-top: 20rpx;
padding: 20rpx;
border-radius: 10rpx;
background-color: #f5f6f8;
font-size: 24rpx;
color: #666666;
line-height: 1.5;
}
.ex-time {
margin-top: 14rpx;
font-size: 22rpx;
color: #9aa4b0;
}
</style>

609
pages_tool/card/style.vue Normal file
View File

@@ -0,0 +1,609 @@
<template>
<view class="cs-page">
<!-- 顶部段控: 名片模板 / 分享封面 -->
<view class="cs-seg">
<view class="cs-seg-item" :class="{ active: seg === 'tpl' }" @tap="switchSeg('tpl')">名片模板</view>
<view class="cs-seg-item" :class="{ active: seg === 'cover' }" @tap="switchSeg('cover')">分享封面</view>
</view>
<!-- ============ 名片模板 ============ -->
<block v-if="seg === 'tpl'">
<!-- 名片预览: 姓名/资料内容一律保持原样, 只改布局与配色
- 卡面底色跟随「选择背景」
- 选中的模板决定版式(pv-classic = 第一个模板, 参考图的纸感居中版) -->
<view class="cs-preview" :class="'pv-' + previewTplType" :style="previewStyle">
<text class="cs-name">{{ card.name }}</text>
<text class="cs-job">{{ card.job_title }}</text>
<text class="cs-company">{{ card.company }}</text>
<view class="cs-divider"></view>
<view class="cs-row" v-if="card.mobile">
<text class="cs-row-icon iconfont icon-dianhua"></text>
<text class="cs-row-text">{{ card.mobile }}</text>
</view>
<view class="cs-row" v-if="card.email">
<text class="cs-row-icon">@</text>
<text class="cs-row-text">{{ card.email }}</text>
</view>
<view class="cs-row" v-if="card.address">
<text class="cs-row-icon iconfont icon-dizhi"></text>
<text class="cs-row-text">{{ card.address }}</text>
</view>
</view>
<!-- 解锁更多 -->
<view class="cs-unlock" @tap="onUnlock">
<text class="cs-unlock-icon iconfont icon-huiyuan"></text>
<text class="cs-unlock-text">解锁更多新模板、个性化logo,自由排版</text>
<text class="cs-unlock-arrow iconfont icon-right"></text>
</view>
<!-- 选择模板 -->
<view class="cs-section">
<text class="cs-title">选择模板</text>
<scroll-view scroll-x class="cs-scroll">
<view class="cs-thumbs">
<view v-for="tpl in templates" :key="tpl.id" class="cs-thumb"
:class="'tpl-' + tpl.type" @tap="selectTpl(tpl.id)">
<view class="tpl-inner">
<view class="tpl-bar w70"></view>
<view class="tpl-bar w45"></view>
<view class="tpl-bar w55"></view>
</view>
<view v-if="selectedTpl === tpl.id" class="cs-check">
<view class="cs-check-mark"></view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 选择背景 -->
<view class="cs-section">
<text class="cs-title">选择背景</text>
<scroll-view scroll-x class="cs-scroll">
<view class="cs-thumbs">
<view v-for="bg in backgrounds" :key="bg.id" class="cs-thumb"
:style="'background-color:' + bg.color + ';' + (bg.border ? 'border:2rpx solid ' + bg.border + ';' : '')"
@tap="selectBg(bg.id)">
<view v-if="selectedBg === bg.id" class="cs-check">
<view class="cs-check-mark"></view>
</view>
</view>
</view>
</scroll-view>
</view>
</block>
<!-- ============ 分享封面 ============ -->
<block v-else>
<view class="cs-cover-preview">
<view class="cs-cover-inner">
<text class="cs-cover-name">{{ card.name }}</text>
<text class="cs-cover-job">{{ card.job_title }}</text>
<text class="cs-cover-company">{{ card.company }}</text>
</view>
</view>
<view class="cs-section">
<text class="cs-title">选择封面</text>
<scroll-view scroll-x class="cs-scroll">
<view class="cs-thumbs">
<view v-for="cover in covers" :key="cover.id" class="cs-thumb"
:style="'background-color:' + cover.color" @tap="selectCover(cover.id)">
<view v-if="selectedCover === cover.id" class="cs-check">
<view class="cs-check-mark"></view>
</view>
</view>
</view>
</scroll-view>
</view>
</block>
<!-- 底部保存 -->
<view class="cs-footer">
<view class="cs-save" @tap="onSave">保存</view>
</view>
</view>
</template>
<script>
// 名片样式: 本页只改样式(模板/背景), 名片资料一律取 card/detail 的真实数据回填, 不做任何改动。
// 「保存」待后端提供样式保存端点后再接入(card/style/save 之类)。
export default {
data() {
return {
seg: 'tpl',
cardId: '',
// 名片资料(真实数据, 由 loadCard 回填)
card: {
name: '',
job_title: '',
company: '',
mobile: '',
email: '',
address: ''
},
// 模板缩略图示意: type 决定缩略图配色(classic = 参考图的纸感米色块)
templates: [
{ id: 't1', type: 'classic' },
{ id: 't2', type: 'photo' },
{ id: 't3', type: 'lines' },
{ id: 't4', type: 'dark' },
{ id: 't5', type: 'navy' }
],
// 默认选中第一个(参考图)
selectedTpl: 't1',
backgrounds: [
// 第一个背景 = 参考图样式(纸感米色), 选中后改变卡面底色
{ id: 'b1', color: '#ece5db' },
{ id: 'b2', color: '#c9ced6' },
{ id: 'b3', color: '#243b5e' },
{ id: 'b4', color: '#8b95a3' },
{ id: 'b5', color: '#ffffff', border: '#2aa5ff' }
],
selectedBg: 'b1',
covers: [
{ id: 'c1', color: '#5a6473' },
{ id: 'c2', color: '#c9ced6' },
{ id: 'c3', color: '#243b5e' },
{ id: 'c4', color: '#8b95a3' },
{ id: 'c5', color: '#ffffff' }
],
selectedCover: 'c1'
};
},
onLoad(options) {
this.cardId = decodeURIComponent(options.card_id || '');
this.loadCard();
},
computed: {
// 卡面底色跟随「选择背景」; 姓名/资料内容始终原样, 不随模板改变
previewStyle() {
const bg = this.backgrounds.find(item => item.id === this.selectedBg);
return bg ? 'background-color:' + bg.color + ';' : '';
},
// 当前选中模板的类型(classic/photo/lines/dark/navy), 决定预览的版式类 pv-*
previewTplType() {
const tpl = this.templates.find(item => item.id === this.selectedTpl);
return tpl ? tpl.type : 'classic';
}
},
methods: {
// 拉取名片真实资料回填预览(只读展示, 本页不改资料)
loadCard() {
if (!this.cardId) return;
this.$cardApi.cardDetail({
data: { card_id: this.cardId },
success: res => {
if (res.code == 0 && res.data && res.data.relation) {
this.card = this.pickCard(res.data.relation);
}
},
fail: () => {
uni.showToast({ title: '名片资料加载失败', icon: 'none' });
}
});
},
// relation → 预览字段(口径与详情页 fieldRows 一致)
pickCard(r) {
return {
name: r.name || '',
job_title: r.job_title || '',
company: r.company || '',
mobile: r.mobile || r.phone || '',
email: r.email || '',
address: [r.country, r.province, r.city, r.address].filter(Boolean).join(' ')
};
},
switchSeg(value) {
if (this.seg === value) return;
this.seg = value;
},
selectTpl(id) {
this.selectedTpl = id;
},
selectBg(id) {
this.selectedBg = id;
},
selectCover(id) {
this.selectedCover = id;
},
onUnlock() {
uni.showToast({ title: '解锁更多模板(接口待接入)', icon: 'none' });
},
onSave() {
// 接口待接入: 先给反馈, 保证交互可验证
uni.showToast({ title: '已保存(接口待接入)', icon: 'none' });
}
}
};
</script>
<style lang="scss" scoped>
.cs-page {
min-height: 100vh;
background-color: #f6f7f9;
// 底部给固定保存条留位
padding: 0 24rpx 200rpx;
box-sizing: border-box;
}
// 段控: 灰底轨道 + 白色选中胶囊
.cs-seg {
display: flex;
margin-top: 20rpx;
padding: 6rpx;
background-color: #eceef1;
border-radius: 16rpx;
.cs-seg-item {
flex: 1;
height: 68rpx;
line-height: 68rpx;
text-align: center;
font-size: 28rpx;
color: #8a94a6;
border-radius: 12rpx;
&.active {
background-color: #ffffff;
color: #333333;
font-weight: 600;
}
}
}
// 名片预览: 白底 + 蓝色粗描边(姓名/资料原样, 仅底色可被背景覆盖)
.cs-preview {
margin-top: 24rpx;
padding: 32rpx 28rpx;
background-color: #ffffff;
border: 6rpx solid #2aa5ff;
border-radius: 12rpx;
box-sizing: border-box;
.cs-name {
display: block;
font-size: 44rpx;
font-weight: 600;
color: #333333;
line-height: 1.2;
}
.cs-job {
display: block;
margin-top: 12rpx;
font-size: 26rpx;
color: #555555;
}
.cs-company {
display: block;
margin-top: 6rpx;
font-size: 26rpx;
color: #555555;
}
.cs-divider {
height: 2rpx;
margin: 26rpx 0;
background-color: #eef0f3;
}
.cs-row {
display: flex;
align-items: center;
& + .cs-row {
margin-top: 14rpx;
}
}
.cs-row-icon {
width: 34rpx;
flex-shrink: 0;
margin-right: 12rpx;
font-size: 26rpx;
line-height: 1.2;
color: #4a5568;
text-align: center;
}
.cs-row-text {
flex: 1;
min-width: 0;
font-size: 26rpx;
color: #555555;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
// ===== 第一个模板「纸感版」(参考图) =====
// 只改版式与配色: 资料内容(姓名/职位/公司/手机号...)一律原样
// 参考图配色: 分割线及以上 = 古铜金; 分割线以下 = 暖灰褐
&.pv-classic {
// 参考图是无描边的纸卡, 只用柔和投影; 保留边框宽度避免尺寸跳动
border-color: transparent;
box-shadow: 0 8rpx 24rpx rgba(122, 104, 78, 0.16);
// 版式: 资料全部居中
.cs-name,
.cs-job,
.cs-company,
.cs-row-text {
text-align: center;
}
// 分割线以上: 姓名最大最粗, 职位/公司同色次之
.cs-name {
font-size: 42rpx;
font-weight: 700;
color: #b5813c;
}
.cs-job {
margin-top: 14rpx;
font-size: 26rpx;
font-weight: 600;
color: #b5813c;
}
.cs-company {
margin-top: 4rpx;
font-size: 26rpx;
font-weight: 600;
color: #b5813c;
}
// 分割线: 参考图是一条很淡的浅茶色细线
.cs-divider {
margin: 5rpx 0;
background-color: rgba(181, 129, 60, 0.3);
}
// 分割线以下: 居中纯文字(参考图无行首图标), 暖灰褐色, 间距紧凑
.cs-row {
justify-content: center;
& + .cs-row {
margin-top: -10rpx;
}
.cs-row-icon {
display: none;
}
.cs-row-text {
flex: none;
font-size: 30rpx;
color: #000000;
}
}
}
}
// 解锁更多新模板横幅
.cs-unlock {
display: flex;
align-items: center;
margin-top: 24rpx;
padding: 24rpx;
background-color: #eaf4ff;
border-radius: 12rpx;
.cs-unlock-icon {
flex-shrink: 0;
margin-right: 14rpx;
font-size: 32rpx;
color: #2aa5ff;
}
.cs-unlock-text {
flex: 1;
min-width: 0;
font-size: 24rpx;
color: #333333;
}
.cs-unlock-arrow {
flex-shrink: 0;
margin-left: 10rpx;
font-size: 26rpx;
color: #9aa4b0;
}
}
// 分区标题 + 横向滚动缩略图
.cs-section {
margin-top: 32rpx;
.cs-title {
display: block;
margin-bottom: 20rpx;
font-size: 28rpx;
font-weight: 600;
color: #333333;
}
}
.cs-scroll {
white-space: nowrap;
.cs-thumbs {
display: inline-flex;
}
}
.cs-thumb {
position: relative;
width: 152rpx;
height: 100rpx;
flex-shrink: 0;
margin-right: 20rpx;
border-radius: 10rpx;
box-sizing: border-box;
overflow: hidden;
background-color: #ffffff;
border: 2rpx solid #e8ebee;
// 模板内部线条示意
.tpl-inner {
padding: 18rpx 16rpx;
.tpl-bar {
height: 8rpx;
border-radius: 4rpx;
background-color: #dfe3e8;
& + .tpl-bar {
margin-top: 10rpx;
}
}
.w70 {
width: 70%;
}
.w45 {
width: 45%;
}
.w55 {
width: 55%;
}
}
// 深色模板示意
&.tpl-dark,
&.tpl-navy {
border-color: transparent;
.tpl-bar {
background-color: rgba(255, 255, 255, 0.45);
}
}
&.tpl-dark {
background-color: #4a5260;
}
&.tpl-navy {
background-color: #1f2a44;
}
// 模板一(参考图): 纸感米色纯色块
&.tpl-classic {
border-color: transparent;
background-color: #ece5db;
.tpl-inner {
display: none;
}
}
// 带人像的模板示意
&.tpl-photo {
.tpl-inner {
padding: 0;
.tpl-bar {
display: none;
}
}
&::after {
content: '';
position: absolute;
left: 0;
top: 0;
width: 56rpx;
height: 100%;
background-color: #6b7280;
}
}
}
// 选中角标(参考图): 白底蓝勾圆, 居中压在缩略图上
.cs-check {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 34rpx;
height: 34rpx;
border-radius: 50%;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
.cs-check-mark {
width: 14rpx;
height: 7rpx;
margin-top: -4rpx;
border-left: 3rpx solid #2aa5ff;
border-bottom: 3rpx solid #2aa5ff;
transform: rotate(-45deg);
}
}
// 分享封面预览
.cs-cover-preview {
margin-top: 24rpx;
padding: 32rpx;
height: 320rpx;
background-color: #ffffff;
border: 6rpx solid #2aa5ff;
border-radius: 12rpx;
box-sizing: border-box;
.cs-cover-inner {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
.cs-cover-name {
font-size: 40rpx;
font-weight: 600;
color: #333333;
}
.cs-cover-job {
margin-top: 10rpx;
font-size: 26rpx;
color: #555555;
}
.cs-cover-company {
margin-top: 6rpx;
font-size: 26rpx;
color: #555555;
}
}
// 底部固定保存条
.cs-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 24rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #ffffff;
.cs-save {
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 44rpx;
background-color: #2aa5ff;
color: #ffffff;
font-size: 32rpx;
}
}
</style>