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

459 lines
12 KiB
Vue
Raw Permalink 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="sub-tabs">
<view
v-for="t in anchors"
:key="t.value"
class="sub-tab"
:class="{ active: activeAnchor === t.value }"
@click="scrollToAnchor(t.value)"
>
{{ t.label }}
</view>
</view>
<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">
{{ forms.creator || '点击查看' }}
<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">院内流转记录mock</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 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.time }}</view>
<view class="card2">
<view class="trow"><text class="tlabel">转入团队:</text>{{ r.team }}</view>
<view class="trow"><text class="tlabel">转入方式:</text>{{ r.type }}</view>
<view class="trow"><text class="tlabel">操作人:</text>{{ r.user }}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue';
import dayjs from 'dayjs';
import FormTemplate from '@/components/form-template/index.vue';
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 anchors = [
{ label: '基本信息', value: 'base' },
{ label: '内部信息', value: 'internal' },
];
const activeAnchor = ref('base');
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)) 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([]);
function openTransferRecord() {
transferRecords.value = [
{ _id: 't1', time: dayjs().subtract(120, 'day').format('YYYY-MM-DD HH:mm'), team: '口腔一科(示例)', type: '系统建档', user: '系统' },
{ _id: 't2', time: dayjs().subtract(30, 'day').format('YYYY-MM-DD HH:mm'), team: '正畸团队(示例)', type: '团队流转', user: '管理员A' },
];
transferPopupRef.value?.open?.();
}
function closeTransferRecord() {
transferPopupRef.value?.close?.();
}
</script>
<style scoped>
.wrap {
padding: 12px 0 96px;
}
.sub-tabs {
display: flex;
background: #f5f6f8;
border-bottom: 1px solid #f2f2f2;
}
.sub-tab {
flex: 1;
text-align: center;
height: 40px;
line-height: 40px;
font-size: 13px;
color: #666;
}
.sub-tab.active {
color: #4f6ef7;
font-weight: 600;
}
.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;
}
</style>