ykt-wxapp/pages/case/plan-list.vue

496 lines
12 KiB
Vue
Raw Normal View History

<template>
<full-page customScroll pageStyle="background:#f4f5f7">
<template #header>
<view class="search-bar">
<input
v-model="keyword"
class="search-input"
placeholder="搜索"
confirm-type="search"
@input="onSearchInput"
@confirm="loadList"
/>
</view>
</template>
<view class="template-page">
<scroll-view scroll-y class="category-panel">
<view
v-for="item in categoryList"
:key="item.viewType"
class="category-item"
:class="{ active: activeViewType === item.viewType }"
@click="changeCategory(item.viewType)"
>
<text class="category-text">{{ item.label }}</text>
2026-01-22 17:39:23 +08:00
</view>
</scroll-view>
<view class="content-panel">
<view v-if="loading" class="state-text">加载中...</view>
<empty-data v-else-if="list.length === 0" fullCenter :text="emptyText" />
<scroll-view v-else scroll-y class="list-scroll">
<view v-for="p in list" :key="p.id" class="template-card">
<view class="card-header">
<view class="plan-name">{{ p.planName || '--' }}</view>
<view
v-if="canToggleFavorite(p)"
class="favorite-btn"
:class="{ collected: p.isFavorite }"
@click.stop="toggleFavorite(p)"
>
<uni-icons
class="favorite-icon"
:type="p.isFavorite ? 'star-filled' : 'star'"
size="18"
:color="p.isFavorite ? '#1f5cff' : '#667085'"
/>
</view>
</view>
<view class="creator" v-if="getCreatorText(p)">{{ getCreatorText(p) }}</view>
<view class="card-footer">
<view class="task-link" @click.stop="preview(p)">任务详情 </view>
<view class="select-btn" @click="select(p)">选择</view>
</view>
</view>
</scroll-view>
2026-01-22 17:39:23 +08:00
</view>
</view>
2026-02-12 14:44:58 +08:00
<template #footer>
<view class="footer">
<view class="service-entry" @click="toService">
<text class="service-muted">如没有符合的内容</text>
<text class="service-link">联系客服</text>
2026-02-12 14:44:58 +08:00
</view>
</view>
</template>
</full-page>
2026-01-22 17:39:23 +08:00
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
2026-01-26 15:39:14 +08:00
import { storeToRefs } from 'pinia';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import { toast } from '@/utils/widget';
import { getTodoEventTypeLabel } from '@/utils/todo-const';
2026-01-22 17:39:23 +08:00
import EmptyData from '@/components/empty-data.vue';
2026-02-12 14:44:58 +08:00
import fullPage from '@/components/full-page.vue';
2026-01-22 17:39:23 +08:00
const archiveId = ref('');
2026-01-26 15:39:14 +08:00
const loading = ref(false);
const keyword = ref('');
const activeViewType = ref('my');
const emptyText = ref('暂无回访计划');
2026-01-26 15:39:14 +08:00
const list = ref([]);
const categoryList = ref([
{ label: '我的回访', viewType: 'my' },
{ label: '团队回访', viewType: 'team' },
{ label: '柚助手', viewType: 'doctorMiniapp' },
]);
let searchTimer = null;
2026-01-26 15:39:14 +08:00
const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore);
const { getDoctorInfo } = accountStore;
2026-01-22 17:39:23 +08:00
onLoad(async (options) => {
2026-01-22 17:39:23 +08:00
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
await ensureDoctor();
buildCategoryList();
2026-01-26 15:39:14 +08:00
loadList();
2026-01-22 17:39:23 +08:00
});
2026-01-26 15:39:14 +08:00
async function ensureDoctor() {
if (doctorInfo.value) return;
if (!account.value?.openid) return;
try {
await getDoctorInfo();
} catch {
// ignore
}
}
function getUserId() {
const d = doctorInfo.value || {};
const a = account.value || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
}
function getCorpId() {
const t = uni.getStorageSync('ykt_case_current_team') || {};
const a = account.value || {};
const d = doctorInfo.value || {};
return String(t.corpId || a.corpId || d.corpId || '') || '';
}
function buildCategoryList() {
const d = doctorInfo.value || {};
const categories = [
{ label: '我的回访', viewType: 'my' },
{ label: '团队回访', viewType: 'team' },
];
if (d.hospitalId || d.hospitalName) {
categories.push({
label: String(d.hospitalName || '第一执业点'),
viewType: 'firstHospital',
});
}
if (d.secondPracticeHospitalId || d.secondPracticeHospitalName) {
categories.push({
label: String(d.secondPracticeHospitalName || '第二执业点'),
viewType: 'secondHospital',
});
}
categories.push({ label: '柚助手', viewType: 'doctorMiniapp' });
categoryList.value = categories;
if (!categories.some((item) => item.viewType === activeViewType.value)) {
activeViewType.value = categories[0]?.viewType || 'my';
}
2026-01-26 15:39:14 +08:00
}
function normalizePlan(raw) {
if (!raw || typeof raw !== 'object') return null;
const id = String(raw._id || raw.planId || '');
2026-01-26 15:39:14 +08:00
if (!id) return null;
const planId = String(raw.planId || raw._id || '');
2026-01-26 15:39:14 +08:00
const taskList = Array.isArray(raw.taskList) ? raw.taskList : [];
const first = taskList[0] && typeof taskList[0] === 'object' ? taskList[0] : {};
const eventType = String(first.eventType || '');
const taskContent = String(first.taskContent || '');
return {
...raw,
id,
_id: String(raw._id || id),
planId,
sourceTemplateId: String(raw.sourceTemplateId || raw._id || id),
sourceTemplatePlanId: String(raw.sourceTemplatePlanId || planId),
2026-01-26 15:39:14 +08:00
planName: String(raw.planName || ''),
planType: String(raw.planType || ''),
planDetail: String(raw.planDetail || ''),
firstEventType: eventType,
firstEventTypeLabel: eventType ? getTodoEventTypeLabel(eventType) : '',
taskContent,
taskCount: taskList.length,
taskList,
isFavorite: Boolean(raw.isFavorite),
2026-01-26 15:39:14 +08:00
};
}
function unwrapLibraryData(res) {
const data = res?.data;
if (data && typeof data === 'object' && data.data && typeof data.data === 'object') {
return data.data;
}
return data && typeof data === 'object' ? data : {};
2026-02-12 14:44:58 +08:00
}
2026-01-26 15:39:14 +08:00
async function loadList() {
if (loading.value) return;
loading.value = true;
try {
await ensureDoctor();
buildCategoryList();
2026-01-26 15:39:14 +08:00
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) {
2026-01-26 15:39:14 +08:00
list.value = [];
toast('缺少用户信息');
2026-01-26 15:39:14 +08:00
return;
}
const res = await api('getMyManagementPlanTemplateLibrary', {
2026-01-26 15:39:14 +08:00
corpId,
userId,
viewType: activeViewType.value,
keyword: keyword.value.trim(),
2026-01-26 15:39:14 +08:00
page: 1,
pageSize: 999,
});
if (!res?.success) {
list.value = [];
toast(res?.message || '获取回访计划失败');
return;
}
const payload = unwrapLibraryData(res);
const arr = Array.isArray(payload.list) ? payload.list : [];
list.value = arr.map(normalizePlan).filter(Boolean);
emptyText.value = payload.emptyState || '暂无回访计划';
2026-01-26 15:39:14 +08:00
} finally {
loading.value = false;
}
}
function changeCategory(viewType) {
if (activeViewType.value === viewType) return;
activeViewType.value = viewType;
list.value = [];
loadList();
}
function onSearchInput() {
if (searchTimer) clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
loadList();
}, 400);
}
function canToggleFavorite(plan) {
if (!plan || !plan._id) return false;
if (isCreatedByCurrentUser(plan)) return false;
if (activeViewType.value !== 'my') return true;
return Boolean(plan.isFavorite);
}
function isCreatedByCurrentUser(plan = {}) {
const userId = getUserId();
if (!userId) return false;
if (String(plan.createor || plan.creator || '') === userId) return true;
const signature = plan.creatorSignature && typeof plan.creatorSignature === 'object'
? plan.creatorSignature
: null;
return signature?.type === 'personal' && String(signature.person?.userId || '') === userId;
}
async function toggleFavorite(plan) {
const corpId = getCorpId();
const userId = getUserId();
const managementPlanId = String(plan?._id || '');
if (!corpId || !userId || !managementPlanId) return toast('缺少收藏信息');
const res = await api('toggleMyFollowupFavorite', {
corpId,
userId,
managementPlanId,
});
if (!res?.success) return toast(res?.message || '操作失败');
toast(res.message || '操作成功');
loadList();
}
function getCreatorText(plan = {}) {
const signature = plan.creatorSignature && typeof plan.creatorSignature === 'object'
? plan.creatorSignature
: null;
if (signature?.type === 'institution') {
return signature.institution?.name || '';
}
if (signature?.type === 'department') {
return signature.department?.name || '';
}
if (signature?.type === 'personal') {
return signature.person?.userName || '';
}
return String(plan.createorName || plan.creatorName || plan.teamName || plan.teanName || plan.createor || '');
}
function toService() {
uni.navigateTo({
url: '/pages/work/service/contact-service',
});
}
2026-01-22 17:39:23 +08:00
function select(plan) {
uni.setStorageSync('select-mamagement-plan', plan);
2026-01-26 15:39:14 +08:00
uni.navigateTo({ url: `/pages/case/plan-execute?archiveId=${encodeURIComponent(archiveId.value)}` });
2026-01-22 17:39:23 +08:00
}
function preview(plan) {
2026-01-26 15:39:14 +08:00
uni.setStorageSync('preview-mamagement-plan', plan);
uni.navigateTo({ url: `/pages/case/plan-preview?archiveId=${encodeURIComponent(archiveId.value)}` });
2026-01-22 17:39:23 +08:00
}
</script>
<style scoped>
.search-bar {
padding: 14rpx 18rpx 12rpx;
background: #fff;
border-bottom: 1rpx solid #edf0f2;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.search-input {
height: 68rpx;
padding: 0 24rpx;
border-radius: 12rpx;
background: #f3f5f8;
color: #111827;
font-size: 28rpx;
2026-01-26 15:39:14 +08:00
}
2026-02-12 14:44:58 +08:00
.template-page {
height: 100%;
2026-01-22 17:39:23 +08:00
display: flex;
min-height: 0;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.category-panel {
width: 210rpx;
height: 100%;
flex-shrink: 0;
background: #fbfcfe;
border-right: 1rpx solid #edf0f2;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.category-item {
position: relative;
min-height: 88rpx;
2026-01-22 17:39:23 +08:00
display: flex;
align-items: center;
margin: 8rpx 10rpx;
padding: 14rpx 12rpx 14rpx 16rpx;
border-radius: 12rpx;
color: #374151;
font-size: 30rpx;
line-height: 40rpx;
box-sizing: border-box;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.category-item.active {
color: #1667e8;
background: #eef5ff;
2026-01-22 17:39:23 +08:00
font-weight: 600;
}
.category-item.active::before {
content: "";
position: absolute;
left: 0;
top: 18rpx;
bottom: 18rpx;
width: 6rpx;
border-radius: 999rpx;
background: #1667e8;
}
.category-text {
width: 100%;
display: block;
overflow: visible;
word-break: break-all;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.content-panel {
position: relative;
flex: 1;
min-width: 0;
height: 100%;
background: #f5f6f8;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.list-scroll {
height: 100%;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.state-text {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #6b7280;
font-size: 30rpx;
}
.template-card {
position: relative;
margin: 16rpx 16rpx 0;
padding: 26rpx 24rpx 24rpx;
border: 1rpx solid #eef1f4;
border-radius: 14rpx;
2026-01-22 17:39:23 +08:00
background: #fff;
box-shadow: 0 4rpx 14rpx rgba(17, 24, 39, 0.04);
box-sizing: border-box;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.card-header {
display: flex;
align-items: flex-start;
gap: 16rpx;
}
.plan-name {
flex: 1;
min-width: 0;
color: #111827;
font-size: 34rpx;
font-weight: 700;
line-height: 46rpx;
word-break: break-word;
}
.favorite-btn {
flex-shrink: 0;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: auto;
}
.favorite-icon {
font-size: 32rpx;
line-height: 1;
}
.creator {
margin-top: 12rpx;
color: #6b7280;
font-size: 30rpx;
line-height: 40rpx;
word-break: break-all;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.task-link {
display: inline-flex;
color: #1667e8;
font-size: 30rpx;
line-height: 40rpx;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.card-footer {
2026-01-22 17:39:23 +08:00
display: flex;
align-items: center;
justify-content: space-between;
gap: 18rpx;
margin-top: 20rpx;
}
.select-btn {
flex-shrink: 0;
min-width: 76rpx;
height: 48rpx;
line-height: 46rpx;
padding: 0 18rpx;
border: 1rpx solid #1667e8;
border-radius: 999rpx;
color: #1667e8;
background: #fff;
font-size: 28rpx;
text-align: center;
box-sizing: border-box;
}
.footer {
position: relative;
background: #fff;
box-shadow: 0 -6rpx 18rpx rgba(15, 23, 42, 0.06);
}
.service-entry {
padding: 24rpx 30rpx;
text-align: center;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.service-muted {
color: #6b7280;
font-size: 30rpx;
2026-01-22 17:39:23 +08:00
}
2026-02-12 14:44:58 +08:00
.service-link {
color: #0877f1;
font-size: 30rpx;
2026-01-22 17:39:23 +08:00
}
</style>