ykt-wxapp/components/archive-detail/follow-up-manage-tab.vue

624 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/followup-manage/followup-manage.vue -->
<view class="wrap">
<view class="top">
<view class="top-row">
<view class="my" @click="toggleMy">
<image :src="`/static/checkbox${query.isMy ? '-checked' : ''}.svg`" class="checkbox" />
<view class="my-text">我的</view>
</view>
<view class="status-scroll">
<scroll-view scroll-x>
<view class="status-tabs">
<view
v-for="t in statusTabs"
:key="t.value"
class="status-tab"
:class="{ active: query.status === t.value }"
@click="toggleStatus(t.value)"
>
{{ t.label }}
</view>
</view>
</scroll-view>
</view>
<view class="filter-btn" @click="openFilter">
<image class="filter-icon" :src="`/static/icons/icon-filter${filtered ? '-active' : ''}.svg`" />
</view>
</view>
<view class="total"><text class="total-num">{{ total }}</text></view>
</view>
<view class="list">
<view v-for="i in list" :key="i._id" class="card" @click="toDetail(i)">
<view class="head">
<view class="date">计划日期: <text class="date-val">{{ i.planDate }}</text></view>
<view class="executor truncate">{{ i.executorName }}<text v-if="i.executeTeamName">{{ i.executeTeamName }}</text></view>
</view>
<view class="body">
<view class="title-row">
<view class="type">{{ i.eventTypeLabel }}</view>
<view class="status" :class="`st-${i.status}`">{{ i.eventStatusLabel }}</view>
</view>
<view class="content">{{ i.taskContent || '暂无内容' }}</view>
<view v-if="i.status === 'treated'" class="result">处理结果 {{ i.result || '' }}</view>
<view class="footer">创建: {{ i.createTimeStr }} {{ i.creatorName }}</view>
</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>
<!-- 筛选弹窗简化 mock -->
<uni-popup ref="filterPopupRef" type="bottom" :mask-click="true">
<view class="popup">
<view class="popup-title">
<view class="popup-title-text">全部筛选</view>
<view class="popup-close" @click="closeFilter(true)">
<uni-icons type="closeempty" size="18" color="#666" />
</view>
</view>
<scroll-view scroll-y class="popup-body">
<view class="section">
<view class="section-title">任务类型</view>
<view class="chip-wrap">
<view
v-for="t in typeOptions"
:key="t.value"
class="chip"
:class="{ active: typeSelectedMap[t.value] }"
@click="toggleType(t.value)"
>
{{ t.label }}
</view>
</view>
</view>
<view class="section">
<view class="section-title">所属团队</view>
<picker mode="selector" :range="teamOptions" range-key="label" @change="pickTeam">
<view class="select-row">
<view class="select-text" :class="{ muted: teamPicked.value === 'ALL' }">{{ teamPicked.label }}</view>
<uni-icons type="right" size="16" color="#999" />
</view>
</picker>
</view>
<view class="section">
<view class="section-title">计划日期</view>
<view class="range-row">
<picker mode="date" @change="pickStart">
<view class="range-pill" :class="{ muted: !planRange[0] }">{{ planRange[0] || '开始日期' }}</view>
</picker>
<view class="sep">-</view>
<picker mode="date" @change="pickEnd">
<view class="range-pill" :class="{ muted: !planRange[1] }">{{ planRange[1] || '结束日期' }}</view>
</picker>
<view class="clear" @click="clearPlanRange">
<uni-icons type="closeempty" size="16" color="#999" />
</view>
</view>
</view>
</scroll-view>
<view class="actions">
<button class="btn plain" @click="resetFilter">重置</button>
<button class="btn primary" @click="confirmFilter">确定</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { ensureSeed, queryFollowups } from './mock';
const props = defineProps({
data: { type: Object, default: () => ({}) },
archiveId: { type: String, default: '' },
reachBottomTime: { type: [String, Number], default: '' },
floatingBottom: { type: Number, default: 16 },
});
const statusTabs = [
{ label: '全部', value: 'all' },
{ label: '待处理', value: 'processing' },
{ label: '未开始', value: 'notStart' },
{ label: '已完成', value: 'treated' },
{ label: '已取消', value: 'cancelled' },
{ label: '已过期', value: 'expired' },
];
const typeOptions = [
{ label: '回访', value: 'followup' },
{ label: '复诊提醒', value: 'revisit' },
{ label: '问卷', value: 'questionnaire' },
{ label: '其他', value: 'other' },
];
const teamOptions = [
{ label: '全部', value: 'ALL' },
{ label: '口腔一科(示例)', value: 'team_1' },
{ label: '正畸团队(示例)', value: 'team_2' },
];
const query = reactive({
isMy: false,
status: 'all',
eventTypes: [],
teamId: 'ALL',
planRange: ['', ''],
});
const list = ref([]);
const total = ref(0);
const page = ref(1);
const pageSize = 10;
const pages = ref(1);
const loading = ref(false);
const moreStatus = computed(() => {
if (loading.value) return 'loading';
return page.value <= pages.value ? 'more' : 'no-more';
});
const loadMoreText = {
contentdown: '点击加载更多',
contentrefresh: '加载中...',
contentnomore: '没有更多了',
};
const filtered = ref(false);
const typeSelectedMap = computed(() => {
const s = new Set(query.eventTypes || []);
return typeOptions.reduce((acc, cur) => ((acc[cur.value] = s.has(cur.value)), acc), {});
});
function resetList() {
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, total: t, pages: p } = queryFollowups({
archiveId: props.archiveId,
page: page.value,
pageSize,
status: query.status,
isMy: query.isMy,
eventTypes: query.eventTypes,
teamId: query.teamId,
planRange: query.planRange,
});
total.value = t;
pages.value = p;
list.value = page.value === 1 ? arr : [...list.value, ...arr];
page.value += 1;
} finally {
loading.value = false;
}
}
function toggleMy() {
query.isMy = !query.isMy;
resetList();
}
function toggleStatus(v) {
query.status = v;
resetList();
}
function add() {
uni.showActionSheet({
itemList: ['+新增任务', '+回访记录'],
success: ({ tapIndex }) => {
if (tapIndex === 0) {
uni.navigateTo({ url: `/pages/case/followup-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=add` });
} else if (tapIndex === 1) {
uni.navigateTo({ url: `/pages/case/followup-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=record` });
}
},
});
}
function toDetail(todo) {
uni.navigateTo({ url: `/pages/case/followup-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=edit&id=${encodeURIComponent(todo._id)}` });
}
// ---- filter popup ----
const filterPopupRef = ref(null);
const state = ref(null);
const teamPicked = ref(teamOptions[0]);
const planRange = ref(['', '']);
function openFilter() {
state.value = {
query: { ...query, eventTypes: [...(query.eventTypes || [])], planRange: [...(query.planRange || ['', ''])] },
team: { ...teamPicked.value },
range: [...planRange.value],
};
planRange.value = [...(query.planRange || ['', ''])];
teamPicked.value = teamOptions.find((i) => i.value === query.teamId) || teamOptions[0];
filterPopupRef.value?.open?.();
}
function closeFilter(revert) {
if (revert && state.value) {
Object.assign(query, state.value.query);
planRange.value = state.value.range;
teamPicked.value = state.value.team;
}
filterPopupRef.value?.close?.();
}
function toggleType(v) {
const set = new Set(query.eventTypes || []);
if (set.has(v)) set.delete(v);
else set.add(v);
query.eventTypes = Array.from(set);
}
function pickTeam(e) {
teamPicked.value = teamOptions[e.detail.value] || teamOptions[0];
query.teamId = teamPicked.value.value;
}
function pickStart(e) {
planRange.value = [e.detail.value || '', planRange.value[1] || ''];
query.planRange = [...planRange.value];
}
function pickEnd(e) {
planRange.value = [planRange.value[0] || '', e.detail.value || ''];
query.planRange = [...planRange.value];
}
function clearPlanRange() {
planRange.value = ['', ''];
query.planRange = ['', ''];
}
function resetFilter() {
query.eventTypes = [];
query.teamId = 'ALL';
teamPicked.value = teamOptions[0];
clearPlanRange();
}
function confirmFilter() {
filtered.value = Boolean((query.eventTypes && query.eventTypes.length) || query.teamId !== 'ALL' || query.planRange[0] || query.planRange[1]);
closeFilter(false);
resetList();
}
onMounted(() => {
ensureSeed(props.archiveId, props.data);
resetList();
uni.$on('archive-detail:followup-changed', resetList);
});
onUnmounted(() => {
uni.$off('archive-detail:followup-changed', resetList);
});
watch(
() => props.reachBottomTime,
() => getMore()
);
</script>
<style scoped>
.wrap {
padding: 0 0 96px;
}
.top {
background: #f5f6f8;
padding: 10px 14px 8px;
border-bottom: 1px solid #f2f2f2;
}
.top-row {
display: flex;
align-items: center;
}
.my {
display: flex;
align-items: center;
flex-shrink: 0;
}
.checkbox {
width: 18px;
height: 18px;
margin-right: 6px;
}
.my-text {
font-size: 13px;
color: #333;
}
.status-scroll {
flex: 1;
margin-left: 10px;
}
.status-tabs {
display: flex;
flex-wrap: nowrap;
}
.status-tab {
flex-shrink: 0;
padding: 8px 10px;
font-size: 12px;
border-radius: 6px;
margin-right: 8px;
background: #eaecef;
color: #333;
}
.status-tab.active {
background: #dbe6ff;
color: #4f6ef7;
}
.filter-btn {
flex-shrink: 0;
padding-left: 10px;
}
.filter-icon {
width: 20px;
height: 20px;
}
.total {
margin-top: 6px;
font-size: 12px;
color: #666;
}
.total-num {
color: #ff4d4f;
margin: 0 4px;
}
.list {
padding: 0 14px;
}
.card {
background: #fff;
border-radius: 8px;
margin-top: 10px;
overflow: hidden;
}
.head {
padding: 12px 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f2f2f2;
}
.date {
font-size: 13px;
color: #666;
flex-shrink: 0;
margin-right: 10px;
}
.date-val {
color: #333;
}
.executor {
flex: 1;
text-align: right;
font-size: 13px;
color: #333;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.body {
padding: 12px 12px;
}
.title-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.type {
font-size: 15px;
font-weight: 600;
color: #1f1f1f;
}
.status {
font-size: 12px;
padding: 6px 10px;
border-radius: 6px;
}
.st-processing {
background: #ffe5e5;
color: #ff4d4f;
}
.st-notStart {
background: #dbe6ff;
color: #4f6ef7;
}
.st-treated {
background: #dcfce7;
color: #16a34a;
}
.st-cancelled,
.st-expired {
background: #f3f4f6;
color: #666;
}
.content {
margin-top: 10px;
font-size: 13px;
color: #666;
line-height: 18px;
}
.result {
margin-top: 10px;
font-size: 13px;
color: #666;
}
.footer {
margin-top: 10px;
font-size: 12px;
color: #999;
}
.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-body {
max-height: 70vh;
}
.section {
padding: 14px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.chip-wrap {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.chip {
width: 110px;
text-align: center;
padding: 8px 0;
border: 1px solid #e6e6e6;
border-radius: 8px;
font-size: 12px;
color: #333;
}
.chip.active {
background: #4f6ef7;
border-color: #4f6ef7;
color: #fff;
}
.select-row {
height: 40px;
border: 1px solid #e6e6e6;
border-radius: 8px;
padding: 0 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.select-text {
font-size: 12px;
color: #333;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.select-text.muted {
color: #999;
}
.range-row {
display: flex;
align-items: center;
gap: 8px;
}
.range-pill {
width: 110px;
height: 40px;
line-height: 40px;
text-align: center;
border: 1px solid #e6e6e6;
border-radius: 8px;
font-size: 12px;
color: #333;
}
.range-pill.muted {
color: #999;
}
.sep {
color: #999;
}
.clear {
padding: 6px;
}
.actions {
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
display: flex;
gap: 12px;
}
.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;
}
</style>