feat:页面开发

This commit is contained in:
huxuejian 2026-02-02 17:49:17 +08:00
parent a7d3eeae3a
commit 6903fe2d02
8 changed files with 370 additions and 33 deletions

20
App.vue
View File

@ -154,6 +154,26 @@ uni-button[type="primary"]:not([disabled]):active {
background: rgb(248 113 113); background: rgb(248 113 113);
} }
.bg-light-text-color {
position: relative;
}
.bg-light-text-color::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.2;
background: currentColor;
}
.py-3 {
padding-top: 6rpx;
padding-bottom: 6rpx;
}
.py-5 { .py-5 {
padding-top: 10rpx; padding-top: 10rpx;
padding-bottom: 10rpx; padding-bottom: 10rpx;

View File

@ -258,6 +258,12 @@
"style": { "style": {
"navigationBarTitleText": "联系企微客服" "navigationBarTitleText": "联系企微客服"
} }
},
{
"path": "pages/work/todo/todo-detail",
"style": {
"navigationBarTitleText": "待办详情"
}
} }
], ],
"globalStyle": { "globalStyle": {

View File

@ -0,0 +1,313 @@
<template>
<full-page v-if="todo" pageClass="bg-white">
<view class="px-15 py-12 border-b">
<view class="flex items-center">
<view class="mr-5 text-lg text-dark font-semibold">{{ todo.eventTypeLabel }}</view>
<view class="bg-light-text-color px-10 py-3 leading-normal text-base rounded overflow-hidden"
:class="statusClassNames[todo.eventStatus] || 'text-gray'">
{{ todo.eventStatusLabel }}
</view>
</view>
<view v-if="!canEdit" class="mt-5 text-base text-gray break-all">
{{ todo.taskContent || "暂无任务内容" }}
</view>
</view>
<view class="px-15 py-12 border-b">
<view class="text-lg text-dark font-semibold">发送内容</view>
<view class="mt-5 text-base text-gray break-all">
{{ todo.sendContent }}
</view>
<view v-for="(file, idx) in todo.fileList" :key="idx" class="mt-5 text-base text-gray break-all">
附件{{ file.file.name }}
</view>
</view>
<template v-if="canEdit">
<picker mode="date" :value="formData.planDate" @change="changePlanDate($event)">
<view class="flex items-center py-10 px-15 border-b">
<view class="text-base text-gray">计划执行时间</view>
<view class="flex-grow"></view>
<view class="text-base text-dark">{{ formData.planDate }}</view>
<view class="flex-shrink-0">
<uni-icons type="right" size="16" color="#999999" />
</view>
</view>
</picker>
<picker mode="selector" :range="memberList" range-key="name" :value="formData.executorUserId"
@change="changeExecutorUserId($event)">
<view class="flex items-center py-10 px-15 border-b">
<view class="text-base text-gray">执行人</view>
<view class="flex-grow"></view>
<view class="text-base text-dark">{{ memberMap[formData.executorUserId] || '' }}</view>
<view class="flex-shrink-0">
<uni-icons type="right" size="16" color="#999999" />
</view>
</view>
</picker>
<picker mode="selector" :range="eventTypeList" range-key="label" :value="formData.eventType"
@change="changeEventType($event)">
<view class="flex items-center py-10 px-15 border-b">
<view class="text-base text-gray">回访类型</view>
<view class="flex-grow"></view>
<view class="text-base text-dark">{{ ToDoEventType[formData.eventType] || '' }}</view>
<view class="flex-shrink-0">
<uni-icons type="right" size="16" color="#999999" />
</view>
</view>
</picker>
<view class="px-15 py-12 border-b">
<view class="text-lg text-dark font-semibold">任务内容</view>
<view class="mt-5 p-10 border rounded">
<textarea :value="formData.taskContent" class="w-full h-120 text-base text-dark" @input="changeTaskContent($event)" />
</view>
</view>
</template>
<!-- v-else -->
<template v-else>
<view class="flex items-center justify-between py-10 px-15 border-b">
<view class="text-base text-gray">计划执行时间</view>
<view class="text-base text-dark">{{ todo.planDate }}</view>
</view>
<view class="flex items-center justify-between py-10 px-15 border-b">
<view class="text-base text-gray">执行人</view>
<view class="text-base text-dark">{{ memberMap[todo.executorUserId] || '' }}</view>
</view>
<view class="px-15 py-12 border-b">
<view class="text-lg text-dark font-semibold">回访结果</view>
<view class="mt-5 p-10 border rounded">
<textarea :value="formData.result" class="w-full h-120 text-base text-dark" @input="changeResult($event)" />
</view>
</view>
</template>
<view class="px-15 py-12 border-b">
<view class="text-base text-gray">创建人{{ todo.creatorUserId }}</view>
<view class="text-base text-gray">创建时间{{ todo.createTime }}</view>
<view v-if="todo.endTime" class="text-base text-gray">执行时间{{ todo.endTime }}</view>
</view>
<template #footer>
<button-footer v-if="canEdit" confirmText="保存" :showCancel="false" @confirm="save()" />
<button-footer v-else-if="editable" cancelText="取消任务" confirmText="设为完成" @cancel="cancelTask()"
@confirm="completeTask()" />
<button-footer v-else-if="canWriteResult" confirmText="保存" :showCancel="false" @confirm="saveResult()" />
</template>
</full-page>
</template>
<script setup>
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import dayjs from "dayjs";
import { statusNames, ToDoEventType, statusClassNames } from '@/baseData';
import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js";
import api from "@/utils/api.js";
import { confirm, toast } from "@/utils/widget";
import buttonFooter from '@/components/button-footer.vue';
import fullPage from '@/components/full-page.vue';
const { useLoad } = useGuard();
const { doctorInfo, account } = storeToRefs(useAccountStore());
const todo = ref(null);
const team = ref(null);
const form = ref({});
const isEditTodo = ref(false);
const formData = computed(() => ({ ...(todo.value || {}), ...form.value }));
const teamManager = computed(() => {
const memberLeaderList = team.value && Array.isArray(team.value.memberLeaderList) ? team.value.memberLeaderList : [];
return doctorInfo.value && memberLeaderList.includes(doctorInfo.value.userid);
})
const hasPermission = computed(() => {
const userid = doctorInfo.value && doctorInfo.value.userid;
return userid && todo.value && (userid === todo.value.executorUserId || teamManager.value);
})
const memberList = computed(() => {
const memberList = team.value && Array.isArray(team.value.memberList) ? team.value.memberList : [];
return memberList.map(i => ({ name: i.anotherName, value: i.userid }))
})
const memberMap = computed(() => memberList.value.reduce((m, item) => {
m[item.value] = item.name;
return m
}, {}))
const canEdit = computed(() => isEditTodo.value && ['notStart', 'processing'].includes(todo.value?.eventStatus))
const canWriteResult = computed(() => {
return hasPermission.value && 'treated' === todo.value?.eventStatus;
})
const editable = computed(() => {
const statusRight = ['notStart', 'processing'].includes(todo.value?.eventStatus);
return hasPermission.value && statusRight;
})
const eventTypeList = computed(() => Object.keys(ToDoEventType).map((key) => ({ label: ToDoEventType[key], value: key })));
function getVisitPlanStatus(row) {
if (row.eventStatus === "untreated" && dayjs().isBefore(dayjs(row.plannedExecutionTime))) {
return "notStart";
} else if (row.eventStatus === "untreated" && dayjs().isAfter(dayjs(row.plannedExecutionTime)) && dayjs().isBefore(dayjs(row.expireTime))) {
return "processing";
} else if (row.eventStatus === "treated") {
return "treated";
} else if (row.eventStatus === "closed") {
return "cancelled";
} else if (row.eventStatus === "expired" || dayjs().isAfter(row.expireTime)) {
return "expired";
}
return "";
}
function changePlanDate(e) {
form.value.planDate = e.detail.value;
}
function changeExecutorUserId(e) {
const idx = e.detail.value;
const user = memberList[idx];
form.value.executorUserId = user.value;
}
function changeEventType(e) {
const idx = e.detail.value;
const type = eventTypeList[idx];
form.value.eventType = type.value;
}
function changeTaskContent(e) {
form.value.taskContent = e.detail.value;
}
function changeResult(e) {
form.value.result = e.detail.value;
}
async function save() {
if (!formData.value.eventType) {
return toast('请选择回访类型');
}
if (!formData.value.planDate) {
return toast("请选择执行时间");
}
if (!formData.value.executorUserId) {
return toast("请选择处理人");
}
const taskParams = {
eventType: formData.value.eventType,
planExecutionTime: dayjs(formData.value.planDate).valueOf(),
executorUserId: formData.value.executorUserId,
executorTeamId: formData.value.executorTeamId || undefined,
executeTeamName: formData.value.executeTeamName || undefined,
taskContent: formData.value.taskContent || undefined,
}
const params = {
id: todo.value._id,
task: taskParams,
corpId: account.value.corpId
}
const res = await api('updateTaskTodo', params);
if (res && res.success) {
await toast('保存成功');
uni.navigateBack();
} else {
toast(res?.message || '保存失败')
}
}
async function cancelTask() {
await confirm('确定取消该回访任务吗');
const res = await api('setTodoStatus', {
id: todo.value._id,
eventStatus: 'closed',
result: formData.value.result || '已取消',
userId: doctorInfo.value.userid,
})
if (res && res.success) {
await toast('取消成功');
uni.navigateBack();
} else {
toast(res?.message || '取消失败')
}
}
async function completeTask() {
await confirm('确定完成该回访任务吗');
const res = await api('setTodoStatus', {
id: todo.value._id,
eventStatus: 'treated',
result: formData.value.result || '已完成',
userId: doctorInfo.value.userid,
})
if (res && res.success) {
await toast('操作成功');
uni.navigateBack();
} else {
toast(res?.message || '操作失败')
}
}
async function saveResult() {
if (typeof formData.value.result !== 'string' || formData.value.result.trim() === '') {
return toast('请填写回访结果')
}
const res = await api('updateTaskTodoResult', {
id: todo.value._id,
result: formData.value.result.trim(),
})
if (res && res.success) {
await toast('操作成功');
uni.navigateBack();
} else {
toast(res?.message || '操作失败')
}
}
async function getDetail(id) {
const res = await api('getTodoById', { id, corpId: account.value.corpId });
if (res && res.success) {
const eventStatus = getVisitPlanStatus(res.data);
todo.value = {
...res.data,
eventTypeLabel: ToDoEventType[res.data.eventType],
planDate: res.data.plannedExecutionTime && dayjs(res.data.plannedExecutionTime).isValid() ? dayjs(res.data.plannedExecutionTime).format("YYYY-MM-DD") : "",
endTime: res.data.endTime && dayjs(res.data.endTime).isValid() ? dayjs(res.data.endTime).format("YYYY-MM-DD HH:mm") : "",
createTime: res.data.createTime && dayjs(res.data.createTime).isValid() ? dayjs(res.data.createTime).format("YYYY-MM-DD HH:mm") : "",
eventStatus,
eventStatusLabel: statusNames[eventStatus],
fileList: Array.isArray(res.data.fileList) ? res.data.fileList.filter(i => i && i.file && i.file.name) : []
};
if (todo.value.executeTeamId) {
getTeamDetail(todo.value.executeTeamId)
}
console.log(ToDoEventType, res.data.eventType, ToDoEventType[res.data.eventType])
} else {
toast(res?.message || '获取数据失败')
}
}
async function getTeamDetail(teamId) {
const res = await api('getTeamData', { teamId, corpId: account.value.corpId });
if (res && res.success) {
team.value = res.data;
} else {
await toast(res.message || '获取团队信息失败')
uni.navigateBack();
}
}
useLoad(opts => {
console.clear()
isEditTodo.value = opts.editMode === 'YES';
getDetail(opts.id);
})
</script>
<style>
.mt-5 {
margin-top: 10rpx;
}
.h-120 {
height: 240rpx;
}
</style>

View File

@ -76,16 +76,17 @@
</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 list" :key="i._id" class="mb-10 shadow-lg bg-white"> <view v-for="i in list" :key="i._id" class="mb-10 shadow-lg bg-white" @click="toTodoDetail(i._id)">
<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">计划执行: {{ i.planDate }}</view> <view class="text-base text-dark">计划执行: {{ i.planDate }}</view>
<view class="flex items-center"> <view class="flex items-center" @click.stop="toTodoDetail(i._id, true)">
<view class="text-base text-dark">患者: {{ i.customerName }}</view> <view class="mr-5 text-base text-dark">患者: {{ i.customerName }}</view>
<image class="icon-edit" src="/static/work/edit-pen.svg" />
</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">{{ i.eventTypeLabel }}</view> <view class="mr-5 text-lg font-semibold">{{ i.eventTypeLabel }}</view>
<view class="bg-opacity px-10 py-3 leading-normal text-base rounded overflow-hidden" <view class="bg-light-text-color px-10 py-3 leading-normal text-base rounded overflow-hidden"
:class="statusClassNames[i.eventStatus] || 'text-gray'"> :class="statusClassNames[i.eventStatus] || 'text-gray'">
{{ i.eventStatusLabel }} {{ i.eventStatusLabel }}
</view> </view>
@ -97,7 +98,8 @@
发送内容{{ file.file.name }} 发送内容{{ file.file.name }}
</view> </view>
</view> </view>
<view class="bg-primary px-10 py-3 text-base text-white rounded-sm">发送</view> <view v-if="doctorInfo && doctorInfo.userid && i.executorUserId && doctorInfo.userid === i.executorUserId"
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 truncate"> <view class="mt-10 px-15 text-base leading-normal text-gray truncate">
{{ i.executorUserName }}{{ i.executeTeamName }} {{ i.executorUserName }}{{ i.executeTeamName }}
@ -210,6 +212,12 @@ function handleCert() {
} }
} }
function toTodoDetail(id, editMode = false) {
uni.navigateTo({
url: `/pages/work/todo/todo-detail?id=${id}&editMode=${editMode ? 'YES' : ''}`
})
}
async function getList() { async function getList() {
if (!doctorInfo.value || !doctorInfo.value.userid) { if (!doctorInfo.value || !doctorInfo.value.userid) {
return return
@ -255,7 +263,6 @@ useLoad(() => {
}); });
useShow(async () => { useShow(async () => {
console.log("工作台页面加!!!@@@载");
await getDoctorInfo(); await getDoctorInfo();
changePage(1) changePage(1)
}) })
@ -362,23 +369,8 @@ useShow(async () => {
height: 42rpx; height: 42rpx;
} }
.bg-opacity { .icon-edit {
position: relative; width: 24rpx;
} height: 24rpx;
.bg-opacity::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.2;
background: currentColor;
}
.py-3 {
padding-top: 6rpx;
padding-bottom: 6rpx;
} }
</style> </style>

View File

@ -180,5 +180,9 @@ export default [
{ {
path: 'pages/work/service/contact-service', path: 'pages/work/service/contact-service',
meta: { title: '联系企微客服' } meta: { title: '联系企微客服' }
},
{
path: 'pages/work/todo/todo-detail',
meta: { title: '待办详情' }
} }
] ]

1
static/work/edit-pen.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1770024483535" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4801" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M958.171429 950.857143H62.902857a36.571429 36.571429 0 1 0 0 73.142857h895.268572a36.571429 36.571429 0 1 0 0-73.142857zM84.845714 917.211429h10.971429l292.571428-87.771429a34.377143 34.377143 0 0 0 15.36-9.508571L976.457143 249.417143a68.754286 68.754286 0 0 0 0-98.011429L844.068571 19.748571a73.142857 73.142857 0 0 0-99.474285 0L171.885714 585.142857a35.108571 35.108571 0 0 0-8.045714 11.702857L50.468571 866.011429a37.302857 37.302857 0 0 0 34.377143 51.2zM795.062857 73.142857l125.805714 125.074286L830.171429 288.914286 705.097143 163.108571zM228.205714 635.611429l424.96-419.84 126.537143 125.074285-422.765714 418.377143-206.994286 62.902857z" fill="#0074ff" p-id="4802"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

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