ykt-wxapp/components/archive-detail/service-info-tab.vue

461 lines
11 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/service-info/service-info.vue -->
<view class="wrap">
<view class="filters">
<picker mode="selector" :range="typeList" range-key="label" @change="pickType">
<view class="filter-pill">
<view class="pill-text" :class="{ muted: currentType.value === 'ALL' }">{{ currentType.value === 'ALL' ? '服务类型' : currentType.label }}</view>
<uni-icons type="arrowdown" size="12" color="#666" />
</view>
</picker>
<uni-datetime-picker v-model="dateRange" type="daterange" rangeSeparator="-" @change="changeDates">
<view class="filter-pill wide">
<view class="pill-text" :class="{ muted: !dateRange.length }">
{{ dateRange.length ? dateRange.join('~') : '服务时间' }}
</view>
<view class="pill-icon" @click.stop="clearDates">
<uni-icons v-if="dateRange.length" type="closeempty" size="14" color="#666" />
<uni-icons v-else type="arrowdown" size="12" color="#666" />
</view>
</view>
</uni-datetime-picker>
<picker mode="selector" :range="teamList" range-key="label" @change="pickTeam">
<view class="filter-pill">
<view class="pill-text">{{ currentTeam.label }}</view>
<uni-icons type="arrowdown" size="12" color="#666" />
</view>
</picker>
</view>
<view class="timeline">
<view v-for="(i, idx) in list" :key="i._id" class="cell" @click="toggleExpand(i)">
<view class="head">
<view class="dot"></view>
<view class="time">{{ i.timeStr }}</view>
<view v-if="i.hasFile" class="file-link" @click.stop="viewFile(i)">
{{ i.fileType === 'article' ? '查看文章' : '查看问卷' }}
</view>
</view>
<view class="meta">
<view class="tag">{{ i.typeStr }}</view>
<view class="meta-text">{{ i.executorName }}</view>
<view class="meta-text truncate">{{ i.executeTeamName }}</view>
</view>
<view class="body">
<view class="content" :class="{ clamp: !expandMap[i._id] }">
{{ i.taskContent || '暂无内容' }}<text v-if="i.result">处理结果: {{ i.result }}</text>
</view>
<image class="pen" src="/static/icons/icon-pen.svg" @click.stop="edit(i)" />
</view>
<view v-if="idx < list.length - 1" class="line"></view>
</view>
<view v-if="list.length === 0" class="empty">暂无数据</view>
<uni-load-more
v-if="list.length"
:status="moreStatus"
:contentText="loadMoreText"
@clickLoadMore="getMore"
/>
</view>
<view class="fab" :style="{ bottom: `${floatingBottom}px` }" @click="add">
<uni-icons type="plusempty" size="24" color="#fff" />
</view>
<uni-popup ref="filePopupRef" type="bottom" :mask-click="true">
<view class="popup">
<view class="popup-title">
<view class="popup-title-text">{{ fileTitle }}</view>
<view class="popup-close" @click="closeFilePopup">
<uni-icons type="closeempty" size="18" color="#666" />
</view>
</view>
<view class="popup-body2">
<view class="desc">{{ fileDesc }}</view>
<button class="btn primary" @click="copyFile">{{ fileAction }}</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import dayjs from 'dayjs';
import { ensureSeed, queryServiceRecords } from './mock';
const props = defineProps({
data: { type: Object, default: () => ({}) },
archiveId: { type: String, default: '' },
reachBottomTime: { type: [String, Number], default: '' },
floatingBottom: { type: Number, default: 16 },
});
const typeList = [
{ label: '全部', value: 'ALL' },
{ label: '电话回访', value: 'phone' },
{ label: '短信提醒', value: 'sms' },
{ label: '问卷', value: 'questionnaire' },
{ label: '文章', value: 'article' },
];
const teamList = [
{ label: '全部', value: 'ALL' },
{ label: '口腔一科(示例)', value: 'team_1' },
{ label: '正畸团队(示例)', value: 'team_2' },
];
const currentType = ref(typeList[0]);
const currentTeam = ref(teamList[0]);
const dateRange = ref([]);
const page = ref(1);
const pageSize = 10;
const pages = ref(1);
const loading = ref(false);
const list = ref([]);
const expandMap = ref({});
const moreStatus = computed(() => {
if (loading.value) return 'loading';
return page.value <= pages.value ? 'more' : 'no-more';
});
const loadMoreText = {
contentdown: '点击加载更多',
contentrefresh: '加载中...',
contentnomore: '没有更多了',
};
function mapRow(i) {
const hasFile = Boolean(i.pannedEventSendFile);
const fileType = i.pannedEventSendFile?.type === 'article' ? 'article' : i.pannedEventSendFile?.type === 'questionnaire' ? 'questionnaire' : '';
return {
...i,
hasFile,
fileType,
timeStr: i.executionTime ? dayjs(i.executionTime).format('YYYY-MM-DD HH:mm') : '--',
typeStr: typeList.find((t) => t.value === i.eventType)?.label || '',
};
}
function reset() {
page.value = 1;
pages.value = 1;
list.value = [];
getMore();
}
function getMore() {
if (!props.archiveId) return;
if (loading.value) return;
if (page.value > pages.value) return;
loading.value = true;
try {
const { list: arr, pages: p } = queryServiceRecords({
archiveId: props.archiveId,
page: page.value,
pageSize,
eventType: currentType.value.value,
teamId: currentTeam.value.value,
dateRange: dateRange.value,
});
pages.value = p;
const mapped = arr.map(mapRow);
list.value = page.value === 1 ? mapped : [...list.value, ...mapped];
page.value += 1;
} finally {
loading.value = false;
}
}
function toggleExpand(r) {
expandMap.value[r._id] = !expandMap.value[r._id];
}
function pickType(e) {
currentType.value = typeList[e.detail.value] || typeList[0];
reset();
}
function pickTeam(e) {
currentTeam.value = teamList[e.detail.value] || teamList[0];
reset();
}
function changeDates() {
reset();
}
function clearDates() {
if (!dateRange.value.length) return;
dateRange.value = [];
reset();
}
function add() {
uni.navigateTo({ url: `/pages/case/service-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=add` });
}
function edit(record) {
uni.navigateTo({ url: `/pages/case/service-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=edit&id=${encodeURIComponent(record._id)}` });
}
const filePopupRef = ref(null);
const fileTitle = ref('');
const fileDesc = ref('');
const fileAction = ref('');
const fileCopyText = ref('');
function viewFile(record) {
if (!record.hasFile) return;
if (record.fileType === 'article') {
fileTitle.value = '查看文章';
fileDesc.value = record.pannedEventSendFile?.url || '';
fileAction.value = '复制链接';
fileCopyText.value = record.pannedEventSendFile?.url || '';
} else {
fileTitle.value = '查看问卷';
fileDesc.value = `问卷ID: ${record.pannedEventSendFile?.surveryId || '--'}`;
fileAction.value = '复制问卷ID';
fileCopyText.value = record.pannedEventSendFile?.surveryId || '';
}
filePopupRef.value?.open?.();
}
function closeFilePopup() {
filePopupRef.value?.close?.();
}
function copyFile() {
if (!fileCopyText.value) return;
uni.setClipboardData({
data: fileCopyText.value,
success: () => uni.showToast({ title: '已复制', icon: 'success' }),
});
closeFilePopup();
}
onMounted(() => {
ensureSeed(props.archiveId, props.data);
reset();
uni.$on('archive-detail:service-record-changed', reset);
});
onUnmounted(() => {
uni.$off('archive-detail:service-record-changed', reset);
});
watch(
() => props.reachBottomTime,
() => getMore()
);
</script>
<style scoped>
.wrap {
padding: 12px 0 96px;
}
.filters {
padding: 10px 14px;
background: #f5f6f8;
border-bottom: 1px solid #f2f2f2;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.filter-pill {
background: #fff;
border: 1px solid #e6e6e6;
border-radius: 6px;
padding: 10px 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-width: 110px;
}
.filter-pill.wide {
min-width: 160px;
}
.pill-text {
font-size: 13px;
color: #333;
max-width: 190px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pill-text.muted {
color: #999;
}
.pill-icon {
padding-left: 8px;
}
.timeline {
background: #fff;
margin-top: 10px;
padding: 12px 0 70px;
}
.cell {
padding: 0 14px;
position: relative;
}
.head {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding-left: 18px;
}
.dot {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
border-radius: 50%;
background: #4f6ef7;
}
.time {
font-size: 14px;
font-weight: 600;
color: #1f1f1f;
}
.file-link {
font-size: 13px;
color: #4f6ef7;
}
.meta {
display: flex;
align-items: center;
gap: 10px;
padding-left: 18px;
margin-bottom: 6px;
}
.tag {
font-size: 12px;
color: #4f6ef7;
border: 1px solid #4f6ef7;
border-radius: 999px;
padding: 4px 8px;
}
.meta-text {
font-size: 13px;
color: #333;
}
.truncate {
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.body {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding-left: 18px;
padding-bottom: 12px;
}
.content {
flex: 1;
font-size: 13px;
color: #666;
line-height: 18px;
margin-right: 10px;
}
.content.clamp {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
.pen {
width: 18px;
height: 18px;
margin-top: 2px;
}
.line {
position: absolute;
left: 18px;
top: 34px;
bottom: 0;
width: 2px;
background: #4f6ef7;
opacity: 0.6;
}
.empty {
padding: 120px 0;
text-align: center;
color: #9aa0a6;
font-size: 13px;
}
.fab {
position: fixed;
right: 16px;
width: 52px;
height: 52px;
border-radius: 26px;
background: #4f6ef7;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10px 18px rgba(79, 110, 247, 0.35);
z-index: 20;
}
.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-body2 {
padding: 14px;
}
.desc {
font-size: 14px;
color: #333;
line-height: 20px;
word-break: break-all;
margin-bottom: 14px;
}
.btn {
width: 100%;
height: 44px;
line-height: 44px;
border-radius: 6px;
font-size: 15px;
}
.btn::after {
border: none;
}
.btn.primary {
background: #4f6ef7;
color: #fff;
}
</style>