feat: 页面提交

This commit is contained in:
huxuejian 2026-02-02 08:52:42 +08:00
parent 38553df861
commit 3328435be1
6 changed files with 407 additions and 35 deletions

39
baseData/index.js Normal file
View File

@ -0,0 +1,39 @@
export const ToDoEventType = {
followUpNoShow: "未到院回访",
followUpNoDeal: "未成交回访",
followUp: "诊后回访",
followUpPostSurgery: "术后回访",
followUpPostTreatment: "治疗后回访",
appointmentReminder: "就诊提醒",
followUpReminder: "复诊提醒",
medicationReminder: "用药提醒",
serviceSummary: "咨询服务",
eventNotification: "活动通知",
ContentReminder: "宣教发送",
questionnaire: "问卷调查",
followUpComplaint: "投诉回访",
followUpActivity: "活动回访",
other: "其他",
Feedback: "意见反馈",
// 预约相关服务类型
treatmentAppointment: "治疗预约",
followupAppointment: "复诊预约",
confirmArrival: "确认到院",
prenatalFollowUp: "孕期回访",
};
export const statusNames = {
notStart: "未开始",
treated: "已完成",
processing: "待处理",
cancelled: "已取消",
expired: "已过期",
};
export const statusClassNames = {
notStart: "text-primary",
treated: "text-success",
processing: "text-danger",
cancelled: "text-gray",
expired: "text-gray",
}

33
hooks/usePageList.js Normal file
View File

@ -0,0 +1,33 @@
import { computed, ref, watch } from "vue";
import useDebounce from '@/utils/useDebounce'
export default function usePageList(callback, options = {}) {
const keyword = ref('')
const list = ref([])
const page = ref(1)
const pageSize = ref(options.pageSize || 20)
const pages = ref(0);
const loading = ref(false)
const total = ref(0)
const hasMore = computed(() => page.value < pages.value)
const handleKeywordChange = useDebounce(() => {
getList()
}, options.debounce || 1000)
function changePage(p) {
if (loading.value) return
page.value = p
getList()
}
function getList() {
typeof callback === 'function' && callback()
}
watch(keyword, handleKeywordChange);
return { total, page, pageSize, keyword, list, pages, changePage, loading, hasMore }
}

View File

@ -0,0 +1,211 @@
<template>
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="bg-white rounded overflow-hidden" style="width: 690rpx;">
<view class="px-15 py-12 text-center text-lg font-semibold text-dark border-b">
全部筛选
</view>
<view class="py-15">
<scroll-view scroll-y="true" style="max-height: 60vh;">
<view class="px-15">
<view class="text-base font-semibold text-dark">任务状态</view>
<view class="flex flex-wrap">
<view v-for="(i, idx) in tabs" :key="i.value"
class="mt-10 w-72 py-5 text-sm text-center leading-normal text-dark rounded-sm"
:class="[form.eventStatus === i.value ? 'bg-primary border-primary text-white' : 'border', idx % 4 === 0 ? '' : 'ml-5']"
@click="form.eventStatus = i.value">
{{ i.label }}
</view>
</view>
<view class="pt-10 text-base font-semibold text-dark">任务类型</view>
<view class="flex flex-wrap">
<view v-for="(i, idx) in eventTypeList" :key="i.value"
class="mt-10 w-72 py-5 text-sm text-center leading-normal text-dark truncate rounded-sm"
:class="[selectedType[i.value] ? 'bg-primary border-primary text-white' : 'border', idx % 4 === 0 ? '' : 'ml-5']"
@click="changeEvent(i.value)">
{{ i.label }}
</view>
</view>
<view class="py-10 text-base font-semibold text-dark">所属团队</view>
<view class="flex items-center justify-between px-10 py-5 border rounded-sm" @click="selectTeam()">
<view class="mr-10 w-0 flex-grow text-base" :class="teamName ? 'text-dark' : 'text-gray'">
{{ teamName || '全部' }}
</view>
<view class="flex-shrink-0" @click="clearTeam()">
<uni-icons v-if="teamName" type="closeempty" size="16" color="#999"></uni-icons>
<uni-icons v-else type="down" size="16" color="#999"></uni-icons>
</view>
</view>
<view class="py-10 text-base font-semibold text-dark">计划日期</view>
<view class="flex items-center justify-between px-10 py-5 border rounded-sm">
<view class="mr-10 w-0 flex-grow text-base truncate">
<uni-datetime-picker v-model="form.dates" type="daterange">
<view class="w-full truncate">
<text v-if="form.dates.length" class="text-base text-dark">
{{ form.dates[0] }} - {{ form.dates[1] }}
</text>
<text v-else class="text-base text-gray">请选择计划日期</text>
</view>
</uni-datetime-picker>
</view>
<view class="flex-shrink-0" @click="clearDates()">
<uni-icons v-if="form.dates.length" type="closeempty" size="16" color="#999"></uni-icons>
<uni-icons v-else type="down" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="flex justify-center items-center px-15 py-12 text-center">
<view class="mr-10 py-5 flex-grow border text-base text-dark rounded-sm" @click="close()">取消</view>
<view class="mr-10 py-5 flex-grow border-auto text-base text-primary rounded-sm" @click="reset()">重置</view>
<view class="py-5 flex-grow bg-primary text-base text-white rounded-sm" @click="confirm()">确定</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { storeToRefs } from "pinia";
import dayjs from 'dayjs';
import { ToDoEventType } from '@/baseData';
import useTeamStore from "@/store/team.js";
const emits = defineEmits(['close', 'confirm'])
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
}
})
const tabs = [
{ label: "全部", value: "all" },
{ label: "待处理", value: "processing" },
{ label: "未开始", value: "notStart" },
{ label: "已完成", value: "treated" },
{ label: "已取消", value: "cancelled" },
{ label: "已过期", value: "expired" },
];
const eventTypeList = [{ label: '全部', value: 'all' }, ...Object.keys(ToDoEventType).map(key => ({ label: ToDoEventType[key], value: key }))];
const popup = ref();
const { teams } = storeToRefs(useTeamStore());
const { getTeams } = useTeamStore();
const form = ref({ eventStatus: 'processing', dates: [], eventType: ['all'], teamId: '' });
const selectedType = computed(() => form.value.eventType.reduce((m, item) => {
m[item] = true;
return m
}, {}))
const teamName = computed(() => {
const t = teams.value.find(i => i.teamId === form.value.teamId);
return t ? t.name : ''
})
function changeEvent(val) {
if (val === 'all') {
form.value.eventType = ['all'];
return
}
if (selectedType.value[val]) {
let newList = form.value.eventType.filter(i => i !== val);
newList = newList.length ? newList : ['all'];
form.value.eventType = newList;
} else {
let newList = [...form.value.eventType].filter(i => i !== 'all');
newList.push(val);
form.value.eventType = newList;
}
}
function clearDates() {
if (form.value.dates.length) {
form.value.dates = [];
}
}
function clearTeam() {
form.value.teamId = '';
}
function close() {
emits('close')
}
function confirm() {
const data = {eventStatus: form.value.eventStatus};
if (form.value.eventType.length && form.value.eventType[0] !== 'all') {
data.eventType = form.value.eventType;
}
if (form.value.teamId) {
data.executeTeamId = form.value.teamId;
}
if (form.value.dates.length) {
data.startDate = form.value.dates[0];
data.endDate = form.value.dates[1];
}
emits('confirm', data)
close()
}
function init() {
const data = props.data || {};
const tab = tabs.find(i => i.value === data.eventStatus);
form.value.eventStatus = tab ? tab.value : 'all';
const eventType = Array.isArray(data.eventType) ? data.eventType : ['all'];
form.value.eventType = eventType;
form.value.teamId = data.executeTeamId || '';
const startDate = data.startDate && dayjs(data.startDate).isValid() ? dayjs(data.startDate).format('YYYY-MM-DD') : '';
const endDate = data.endDate && dayjs(data.endDate).isValid() ? dayjs(data.endDate).format('YYYY-MM-DD') : '';
if (startDate && endDate) {
form.value.dates = [startDate, endDate];
} else {
form.value.dates = [];
}
}
function reset() {
form.value = { eventStatus: 'processing', dates: [], eventType: ['all'], teamId: '' };
}
function selectTeam() {
const list = teams.value.map(i => i.name);
uni.showActionSheet({
itemList: ['全部', ...list],
success: res => {
const index = res.tapIndex - 1;
form.value.teamId = index === -1 ? '' : teams.value[index].teamId;
}
})
}
watch(() => props.visible, async n => {
if (n) {
if (teams.value.length === 0) {
getTeams();
}
init()
popup.value && popup.value.open()
} else {
popup.value && popup.value.close()
}
})
</script>
<style lang="scss" scoped>
.ml-5 {
margin-left: 10rpx;
}
.w-72 {
width: 144rpx;
}
</style>

View File

@ -46,23 +46,29 @@
<view class="mt-15 px-15 py-12 flex items-center justify-between bg-white"> <view class="mt-15 px-15 py-12 flex items-center justify-between bg-white">
<view class="text-dark text-lg font-semibold">待办列表</view> <view class="text-dark text-lg font-semibold">待办列表</view>
<view class="flex text-base rounded-full bg-gray"> <view class="flex text-base rounded-full bg-gray">
<view class="py-5 px-15 rounded-full bg-primary text-white">个人</view> <view class="py-5 px-15 rounded-full" :class="followUpType === 'person' ? 'bg-primary text-white' : ''"
<view class="py-5 px-15">团队</view> @click="changeFollowType('person')">
个人
</view>
<view class="py-5 px-15 rounded-full" :class="followUpType === 'team' ? 'bg-primary text-white' : ''"
@click="changeFollowType('team')">
团队
</view>
</view> </view>
</view> </view>
<view class="py-10 px-15 flex items-center"> <view class="py-10 px-15 flex items-center">
<view class="flex-shrink-0 text-sm mr-10"> <view class="flex-shrink-0 text-sm mr-10">
<text class="text-dark"></text> <text class="text-dark"></text>
<text class="text-danger">23</text> <text class="text-danger">{{ total }}</text>
<text class="text-dark"></text> <text class="text-dark"></text>
</view> </view>
<view class="flex"> <view class="flex">
<view v-for="i in statusList" :key="i.value" class="mr-5 py-5 px-10 bg-white text-sm rounded-sm" <view v-for="i in statusList" :key="i.value" class="mr-5 py-5 px-10 bg-white text-sm rounded-sm"
:class="current == i.value ? 'text-primary' : 'text-dark'"> :class="filterData.eventStatus == i.value ? 'text-primary' : 'text-dark'" @click="changeStatus(i.value)">
{{ i.label }} {{ i.label }}
</view> </view>
</view> </view>
<view class="flex-shrink-0 flex-grow flex justify-end" @click="filtered = !filtered"> <view class="flex-shrink-0 flex-grow flex justify-end" @click="filterVisible = !filterVisible">
<image class="icon-filter" :src="`/static/work/icon-filter${filtered ? 'ed' : ''}.svg`" /> <image class="icon-filter" :src="`/static/work/icon-filter${filtered ? 'ed' : ''}.svg`" />
</view> </view>
@ -70,28 +76,35 @@
</template> </template>
<scroll-view v-if="list.length" scroll-y="true" class="h-full"> <scroll-view v-if="list.length" scroll-y="true" class="h-full">
<view v-for="i in 10" :key="i" class="mb-10 shadow-lg bg-white"> <view v-for="i in list" :key="i._id" class="mb-10 shadow-lg bg-white">
<view class="flex items-center justify-between px-15 py-10 border-b"> <view class="flex items-center justify-between px-15 py-10 border-b">
<view class="text-base text-dark">计划执行: 2025-10-22</view> <view class="text-base text-dark">计划执行: {{ i.planDate }}</view>
<view class="flex items-center"> <view class="flex items-center">
<view class="text-base text-dark">患者: 李珊珊</view> <view class="text-base text-dark">患者: {{ i.customerName }}</view>
</view> </view>
</view> </view>
<view class="py-10 px-15 flex items-center"> <view class="py-10 px-15 flex items-center">
<view class="mr-5 text-lg font-semibold">患者满意度调查</view> <view class="mr-5 text-lg font-semibold">{{ i.eventTypeLabel }}</view>
<view class="bg-opacity px-10 py-3 leading-normal text-base text-success rounded overflow-hidden"> <view class="bg-opacity px-10 py-3 leading-normal text-base rounded overflow-hidden"
待处理 :class="statusClassNames[i.eventStatus] || 'text-gray'">
{{ i.eventStatusLabel }}
</view> </view>
</view> </view>
<view class="px-15 text-base leading-normal text-gray">对于门诊就诊患者的满意度做统计以便优化</view> <view v-if="i.sendContent" class="px-15 text-base leading-normal text-gray">{{ i.sendContent }}</view>
<view class="mt-10 px-15 flex items-center"> <view v-if="i.enableSend && i.fileList.length" class="mt-10 px-15 flex items-center">
<view class="mr-5 w-0 flex-grow truncate text-base leading-normal text-dark"> <view class="mr-5 w-0 flex-grow">
发送内容XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX <view v-for="(file, idx) in i.fileList" :key="idx" class="truncate text-base leading-normal text-dark">
发送内容{{ file.file.name }}
</view>
</view> </view>
<view class="bg-primary px-10 py-3 text-base text-white rounded-sm">发送</view> <view class="bg-primary px-10 py-3 text-base text-white rounded-sm">发送</view>
</view> </view>
<view class="mt-10 px-15 text-base leading-normal text-gray">张敏西张敏希服务团队</view> <view class="mt-10 px-15 text-base leading-normal text-gray truncate">
<view class="px-15 pb-10 text-base leading-normal text-gray">创建2026-01-08 张敏西</view> {{ i.executorUserName }}{{ i.executeTeamName }}
</view>
<view class="px-15 pb-10 text-base leading-normal text-gray truncate">
创建{{ i.createTime }} {{ i.creatorUserName }}
</view>
</view> </view>
</scroll-view> </scroll-view>
@ -103,19 +116,28 @@
</template> </template>
</full-page> </full-page>
<cert-popup :visible="visible" @close="visible = false" /> <cert-popup :visible="visible" @close="visible = false" />
<filter-popup :data="filterData" :visible="filterVisible" @close="filterVisible = false"
@confirm="changeFilterData($event)" />
</template> </template>
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { statusNames, ToDoEventType, statusClassNames } from '@/baseData';
import useGuard from "@/hooks/useGuard.js"; import useGuard from "@/hooks/useGuard.js";
import useInfoCheck from '@/hooks/useInfoCheck'; import useInfoCheck from '@/hooks/useInfoCheck';
import usePageList from '@/hooks/usePageList';
import useAccountStore from "@/store/account.js"; import useAccountStore from "@/store/account.js";
import useTeamStore from "@/store/team.js";
import api from '@/utils/api';
import { toast } from '@/utils/widget';
import certPopup from "./components/cert-popup.vue"; import certPopup from "./components/cert-popup.vue";
import filterPopup from './components/filter-popup.vue';
import EmptyData from "@/components/empty-data.vue"; import EmptyData from "@/components/empty-data.vue";
import fullPage from '@/components/full-page.vue'; import fullPage from '@/components/full-page.vue';
import { toast } from '@/utils/widget'; import dayjs from 'dayjs';
const certConfig = { const certConfig = {
failed: { text: '认证失败', classnames: 'bg-danger text-white' }, failed: { text: '认证失败', classnames: 'bg-danger text-white' },
@ -123,27 +145,24 @@ const certConfig = {
verifying: { text: '认证中', classnames: 'bg-warning text-white' }, verifying: { text: '认证中', classnames: 'bg-warning text-white' },
unverified: { text: '未认证', classnames: 'bg-gray text-dark' }, unverified: { text: '未认证', classnames: 'bg-gray text-dark' },
}; };
const statusList = [{ label: '全部', value: 'all' }, { label: '待处理', value: 'pending' }, { label: '已处理', value: 'processed' }]
const statusList = [{ label: '全部', value: 'all' }, { label: '待处理', value: 'processing' }, { label: '未开始', value: 'notStart' }]
const { useLoad, useShow } = useGuard(); const { useLoad, useShow } = useGuard();
const { getDoctorInfo } = useAccountStore(); const { getDoctorInfo } = useAccountStore();
const { doctorInfo } = storeToRefs(useAccountStore()); const { account, doctorInfo } = storeToRefs(useAccountStore());
const { chargeTeams } = storeToRefs(useTeamStore());
const { getTeams } = useTeamStore();
const { withInfo } = useInfoCheck(); const { withInfo } = useInfoCheck();
const list = ref([1]);
const visible = ref(false); const visible = ref(false);
const current = ref('all'); const filtered = ref(false);
const filtered = ref(false) const filterVisible = ref(false);
const filterData = ref({ eventStatus: 'processing' });
const followUpType = ref('person') // person team
const { total, list, page, pages, pageSize, changePage } = usePageList(getList)
const certStatus = computed(() => doctorInfo.value?.verifyStatus ? certConfig[doctorInfo.value.verifyStatus] : null) const certStatus = computed(() => doctorInfo.value?.verifyStatus ? certConfig[doctorInfo.value.verifyStatus] : null)
//
const handleVerify = () => {
uni.showToast({
title: "跳转到认证页面",
icon: "none",
});
};
// //
const invitePatient = withInfo(() => uni.navigateTo({ url: '/pages/work/team/invite/invite-patient' })); const invitePatient = withInfo(() => uni.navigateTo({ url: '/pages/work/team/invite/invite-patient' }));
@ -157,6 +176,26 @@ const handleMore = withInfo(() => {
}); });
}) })
function changeFilterData(data) {
filterData.value = data;
const case1 = Object.keys(data).filter(i => i != 'eventStatus').length > 0;
const case2 = statusList.some(i => i.value === data.eventStatus);
filtered.value = case1 || !case2;
changePage(1);
}
function changeFollowType(type) {
if (followUpType.value === type) return;
followUpType.value = type;
changePage(1);
}
function changeStatus(val) {
if (filterData.value.eventStatus === val) return;
filterData.value.eventStatus = val;
changePage(1);
}
function editProfile() { function editProfile() {
uni.navigateTo({ uni.navigateTo({
url: "/pages/work/profile", url: "/pages/work/profile",
@ -171,12 +210,54 @@ function handleCert() {
} }
} }
async function getList() {
if (!doctorInfo.value || !doctorInfo.value.userid) {
return
}
const data = {
corpId: account.value.corpId,
startDate: filterData.value.startDate,
endDate: filterData.value.endDate,
page: page.value,
pageSize: pageSize.value
}
if (followUpType.value === 'person') {
data.executorUserId = doctorInfo.value.userid;
} else {
data.teamIds = chargeTeams.value.map(i => i.teamId);
}
if (filterData.value.eventStatus !== 'all') {
data.statusList = [filterData.value.eventStatus]
}
if (filterData.value.eventType) {
data.eventType = filterData.value.eventType;
}
const res = await api('getTeamTodos', data);
const arr = res && Array.isArray(res.data) ? res.data.map(i => ({
...i,
eventTypeLabel: ToDoEventType[i.eventType],
planDate: i.plannedExecutionTime && dayjs(i.plannedExecutionTime).isValid() ? dayjs(i.plannedExecutionTime).format("YYYY-MM-DD") : "",
endTime: i.endTime && dayjs(i.endTime).isValid() ? dayjs(i.endTime).format("YYYY-MM-DD HH:mm") : "",
createTime: i.createTime && dayjs(i.createTime).isValid() ? dayjs(i.createTime).format("YYYY-MM-DD HH:mm") : "",
eventStatusLabel: statusNames[i.eventStatus],
fileList: Array.isArray(i.fileList) ? i.fileList.filter(i => i && i.file && i.file.name) : []
})) : [];
list.value = page.value === 1 ? arr : [...list.value, ...arr];
total.value = res && res.total > 0 ? res.total : 0;
pages.value = res && res.pages > 0 ? res.pages : 0;
if (!res && !res.success) {
toast(res?.message || '查询待办失败')
}
}
useLoad(() => { useLoad(() => {
console.log("工作台页面加载"); console.log("工作台页面加载");
}); });
useShow(() => { useShow(async () => {
getDoctorInfo(); console.log("工作台页面加!!!@@@载");
await getDoctorInfo();
changePage(1)
}) })
</script> </script>

View File

@ -1,4 +1,4 @@
import { ref } from "vue"; import { computed, ref } from "vue";
import { defineStore, storeToRefs } from "pinia"; import { defineStore, storeToRefs } from "pinia";
import api from '@/utils/api'; import api from '@/utils/api';
import { toast } from '@/utils/widget'; import { toast } from '@/utils/widget';
@ -9,6 +9,13 @@ export default defineStore("teamStore", () => {
const { account, doctorInfo } = storeToRefs(useAccountStore()); const { account, doctorInfo } = storeToRefs(useAccountStore());
const teams = ref([]); const teams = ref([]);
const chargeTeams = computed(() => {
const userid = doctorInfo.value?.userid;
return teams.value.filter(team => {
const memberLeaderList = Array.isArray(team.memberLeaderList) ? team.memberLeaderList : [];
return memberLeaderList.includes(userid);
});
})
async function getTeam(teamId) { async function getTeam(teamId) {
if (!teamId || !account.value?.corpId) return; if (!teamId || !account.value?.corpId) return;
const res = await api('getTeamData', { teamId, corpId: account.value.corpId }); const res = await api('getTeamData', { teamId, corpId: account.value.corpId });
@ -27,5 +34,5 @@ export default defineStore("teamStore", () => {
teams.value = res && Array.isArray(res.data) ? res.data : []; teams.value = res && Array.isArray(res.data) ? res.data : [];
} }
return { teams, getTeam, getTeams } return { teams, chargeTeams, getTeam, getTeams }
}) })

View File

@ -121,6 +121,7 @@ const urlsConfig = {
// 客户流转记录 // 客户流转记录
customerTransferRecord: 'customerTransferRecord', customerTransferRecord: 'customerTransferRecord',
// sendConsultRejectedMessage: "sendConsultRejectedMessage" // sendConsultRejectedMessage: "sendConsultRejectedMessage"
getTeamTodos: 'getTeamTodos'
} }
} }