ykt-wxapp/components/archive-detail/customer-profile-tab.vue

514 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/customer-detail/customer-profile.vue -->
<view class="wrap">
<view class="card">
<view id="anchor-base" class="section-title" @click="startEdit">
<text>基本信息</text>
<image class="pen" src="/static/icons/icon-pen.svg" />
</view>
<view class="rows">
<form-template
v-if="editing && effectiveBaseItems.length"
ref="baseFormRef"
:items="effectiveBaseItems"
:form="forms"
@change="onChange"
/>
<template v-else>
<view
v-for="item in effectiveBaseItems"
:key="item.title"
class="row"
>
<view class="label">{{ item.name || item.title }}</view>
<view
v-if="item.title === 'mobile' && displayValue(item) !== '-'"
class="val link"
@click="call(rawValue(item))"
>
{{ displayValue(item) }}
</view>
<view v-else class="val" :class="displayValue(item) === '-' ? 'muted' : ''">{{ displayValue(item) }}</view>
</view>
</template>
</view>
</view>
<view class="card">
<view id="anchor-internal" class="section-title" @click="startEdit">
<text>内部信息</text>
<image class="pen" src="/static/icons/icon-pen.svg" />
</view>
<view class="rows">
<view class="row" @click="openTransferRecord">
<view class="label">院内来源</view>
<view class="val link">
{{ latestTransferRecord?.executeTeamName || '点击查看' }}
<uni-icons type="arrowright" size="14" color="#4f6ef7" />
</view>
</view>
<form-template
v-if="editing && effectiveInternalItems.length"
ref="internalFormRef"
:items="effectiveInternalItems"
:form="forms"
:filterRule="filterRule"
@change="onChange"
/>
<template v-else>
<view
v-for="item in effectiveInternalItems"
:key="item.title"
class="row"
>
<view class="label">{{ item.name || item.title }}</view>
<view class="val" :class="displayValue(item) === '-' ? 'muted' : ''">{{ displayValue(item) }}</view>
</view>
</template>
</view>
</view>
<view v-if="editing" class="bottom-bar" :style="{ bottom: `${floatingBottom}px` }">
<button class="btn plain" @click="cancel">取消</button>
<button class="btn primary" @click="save">保存</button>
</view>
<uni-popup ref="transferPopupRef" type="bottom" :mask-click="true">
<view class="popup">
<view class="popup-title">
<view class="popup-title-text">院内流转记录</view>
<view class="popup-close" @click="closeTransferRecord">
<uni-icons type="closeempty" size="18" color="#666" />
</view>
</view>
<scroll-view scroll-y class="popup-body">
<view v-if="transferRecords.length" class="timeline">
<view class="line"></view>
<view v-for="r in transferRecords" :key="r._id" class="item">
<view class="dot"></view>
<view class="content">
<view class="time">{{ r.createTime }}</view>
<view class="card2">
<view v-if="r.executeTeamName" class="trow"><text class="tlabel">转入团队:</text>{{ r.executeTeamName }}</view>
<view v-if="r.eventTypeName" class="trow"><text class="tlabel">转入方式:</text>{{ r.eventTypeName }}</view>
<view v-if="r.creatorUserId" class="trow">
<text class="tlabel">操作人:</text>
<text v-if="r.creatorUserId === 'system'">系统自动建档</text>
<text v-else>{{ r.creatorUserId }}</text>
</view>
</view>
</view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-text">暂无流转记录</text>
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, reactive, ref, watch, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import dayjs from 'dayjs';
import FormTemplate from '@/components/form-template/index.vue';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import useTeamStore from '@/store/team';
const props = defineProps({
data: { type: Object, default: () => ({}) },
baseItems: { type: Array, default: () => ([]) },
internalItems: { type: Array, default: () => ([]) },
floatingBottom: { type: Number, default: 16 },
});
const emit = defineEmits(['save']);
const accountStore = useAccountStore();
const teamStore = useTeamStore();
const { account, doctorInfo } = storeToRefs(accountStore);
const { teams } = storeToRefs(teamStore);
const editing = ref(false);
const baseFormRef = ref(null);
const internalFormRef = ref(null);
const patch = reactive({});
const forms = computed(() => ({ ...(props.data || {}), ...patch }));
const fallbackBaseItems = [
{ title: 'name', name: '姓名', type: 'input', operateType: 'formCell', required: true, wordLimit: 20 },
{ title: 'sex', name: '性别', type: 'select', operateType: 'formCell', required: false, range: ['男', '女'] },
{ title: 'age', name: '年龄', type: 'input', inputType: 'number', operateType: 'formCell', required: false, wordLimit: 3 },
{ title: 'mobile', name: '联系电话', type: 'input', inputType: 'number', operateType: 'formCell', required: false, wordLimit: 11 },
];
const fallbackInternalItems = [
{ title: 'notes', name: '备注', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200 },
];
const effectiveBaseItems = computed(() => (Array.isArray(props.baseItems) && props.baseItems.length ? props.baseItems : fallbackBaseItems));
const effectiveInternalItems = computed(() => (Array.isArray(props.internalItems) && props.internalItems.length ? props.internalItems : fallbackInternalItems));
const filterRule = {
reference(formModel) {
const customerSource = Array.isArray(formModel.customerSource)
? formModel.customerSource
: typeof formModel.customerSource === 'string'
? [formModel.customerSource]
: [];
return ['同事推荐', '客户推荐'].includes(customerSource[0]) && customerSource.length === 1;
},
};
watch(
() => props.data,
() => {
if (!editing.value) Object.keys(patch).forEach((k) => delete patch[k]);
},
{ deep: true }
);
function normalizeChangeValue(value) {
if (value && typeof value === 'object' && 'value' in value) return value.value;
return value;
}
function startEdit() {
if (editing.value) return;
editing.value = true;
}
function cancel() {
editing.value = false;
Object.keys(patch).forEach((k) => delete patch[k]);
}
function save() {
if (baseFormRef.value?.verify && !baseFormRef.value.verify()) return;
if (internalFormRef.value?.verify && !internalFormRef.value.verify()) return;
const out = { ...patch };
emit('save', out);
editing.value = false;
Object.keys(patch).forEach((k) => delete patch[k]);
}
function onChange({ title, value }) {
patch[title] = normalizeChangeValue(value);
}
function call(mobile) {
if (!mobile) return;
uni.makePhoneCall({ phoneNumber: String(mobile) });
}
function rawValue(item) {
const title = item?.title;
if (!title) return '';
const v = forms.value?.[title];
return v === undefined || v === null ? '' : v;
}
function displayValue(item) {
const title = item?.title;
const type = item?.type;
if (!title) return '-';
const v = forms.value?.[title];
if (v === undefined || v === null || v === '') return '-';
if (type === 'files') {
const list = Array.isArray(v) ? v : typeof v === 'string' && v ? [v] : [];
return list.length ? `已上传${list.length}` : '-';
}
if (Array.isArray(v)) {
// 对于数组类型如标签查找range中的label
if (type === 'select' || type === 'radio' || type === 'selectAndImage' || type === 'tagPicker' || item.__originType === 'tag') {
const range = Array.isArray(item?.range) ? item.range : [];
if (range.length && typeof range[0] === 'object') {
const labels = v.map((val) => {
const found = range.find((i) => String(i?.value) === String(val));
return found ? String(found.label || found.value || val) : String(val);
}).filter(Boolean);
return labels.length ? labels.join('、') : '-';
}
}
return v.filter(Boolean).join('、') || '-';
}
if (typeof v === 'object') {
if ('label' in v) return String(v.label || '-');
if ('name' in v) return String(v.name || '-');
if ('value' in v) return String(v.value || '-');
return '-';
}
if (type === 'select' || type === 'radio' || type === 'selectAndImage') {
const range = Array.isArray(item?.range) ? item.range : [];
if (range.length && typeof range[0] === 'object') {
const found = range.find((i) => String(i?.value) === String(v));
if (found) return String(found.label || found.value || '-');
}
}
return String(v);
}
function scrollToAnchor(key) {
activeAnchor.value = key;
const selector = `#anchor-${key}`;
const query = uni.createSelectorQuery();
query.select(selector).boundingClientRect();
query.selectViewport().scrollOffset();
query.exec((res) => {
const rect = res[0];
const scroll = res[1];
if (!rect || !scroll) return;
uni.pageScrollTo({ scrollTop: rect.top + scroll.scrollTop - 60, duration: 200 });
});
}
const transferPopupRef = ref(null);
const transferRecords = ref([]);
const latestTransferRecord = computed(() => transferRecords.value[0]);
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
function getCorpId() {
const d = doctorInfo.value || {};
const a = account.value || {};
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
return String(d.corpId || a.corpId || team.corpId || '') || '';
}
const ServiceType = {
remindFiling: '自主开拓',
adminTransferTeams: '团队流转',
adminRemoveTeams: '移出团队',
systemAutoDistribute: '系统分配',
bindWechatCustomer: '绑定企业微信',
};
async function fetchTransferRecords() {
const customerId = props.data?._id;
if (!customerId) return;
try {
const res = await api('customerTransferRecord', { customerId });
if (res?.success && res.list) {
const allTeams = Array.isArray(teams.value) ? teams.value : [];
transferRecords.value = res.list.map((item) => {
const record = { ...item };
record.createTime = dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss');
record.executeTeamName = allTeams.find((team) => team.teamId === item.executeTeamId)?.name || item.executeTeamName;
record.eventTypeName = ServiceType[item.eventType] || item.eventType;
if (item.transferToTeamIds && Array.isArray(item.transferToTeamIds)) {
record.teamName = item.transferToTeamIds.map((teamId) => allTeams.find((team) => team.teamId === teamId)?.name).join('、');
}
if (item.removeTeamIds && Array.isArray(item.removeTeamIds)) {
record.teamName = item.removeTeamIds.map((teamId) => allTeams.find((team) => team.teamId === teamId)?.name).join('、');
}
if (item.eventType === 'remindFiling') record.eventTypeName = '自主开拓';
return record;
});
}
} catch (e) {
console.error('获取流转记录失败', e);
}
}
async function openTransferRecord() {
await fetchTransferRecords();
transferPopupRef.value?.open?.();
}
function closeTransferRecord() {
transferPopupRef.value?.close?.();
}
onMounted(() => {
fetchTransferRecords();
});
watch(() => props.data?._id, () => {
if (props.data?._id) fetchTransferRecords();
});
</script>
<style scoped>
.wrap {
padding: 12px 0 96px;
}
.card {
background: #fff;
margin-top: 10px;
}
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
font-size: 15px;
font-weight: 600;
color: #333;
border-bottom: 1px solid #f2f2f2;
}
.pen {
width: 18px;
height: 18px;
}
.rows {
padding: 2px 14px;
}
.row {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f6f6f6;
}
.row:last-child {
border-bottom: none;
}
.label {
width: 90px;
font-size: 14px;
color: #666;
}
.val {
flex: 1;
text-align: right;
font-size: 14px;
color: #333;
}
.val.link {
color: #4f6ef7;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 6px;
}
.input,
.picker {
flex: 1;
text-align: right;
font-size: 14px;
color: #333;
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
background: #fff;
padding: 12px 14px;
display: flex;
gap: 12px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
z-index: 30;
}
.btn {
flex: 1;
height: 44px;
line-height: 44px;
border-radius: 6px;
font-size: 15px;
}
.btn::after {
border: none;
}
.btn.plain {
background: #fff;
color: #4f6ef7;
border: 1px solid #4f6ef7;
}
.btn.primary {
background: #4f6ef7;
color: #fff;
}
.popup {
background: #fff;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
overflow: hidden;
}
.popup-title {
position: relative;
padding: 14px;
border-bottom: 1px solid #f0f0f0;
}
.popup-title-text {
text-align: center;
font-size: 16px;
font-weight: 600;
color: #333;
}
.popup-close {
position: absolute;
right: 12px;
top: 0;
height: 100%;
display: flex;
align-items: center;
}
.popup-body {
max-height: 70vh;
}
.timeline {
position: relative;
padding: 14px 14px 18px;
}
.line {
position: absolute;
left: 22px;
top: 18px;
bottom: 18px;
width: 2px;
background: #4f6ef7;
}
.item {
position: relative;
padding-left: 32px;
margin-bottom: 14px;
}
.dot {
position: absolute;
left: 16px;
top: 6px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #4f6ef7;
}
.time {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.card2 {
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 10px;
}
.trow {
font-size: 14px;
color: #333;
line-height: 20px;
}
.tlabel {
color: #666;
margin-right: 6px;
}
.empty-state {
padding: 40px 20px;
text-align: center;
}
.empty-text {
font-size: 14px;
color: #999;
}
</style>