320 lines
10 KiB
Vue
320 lines
10 KiB
Vue
<template>
|
||
<div class="diagnosis-todo-list">
|
||
<div class="toolbar">
|
||
<el-button
|
||
type="primary"
|
||
:disabled="!props.diagnosisId || !hasDailyRecordPermission"
|
||
@click="handleAdd"
|
||
v-perms="DAILY_RECORD_PERMS"
|
||
>
|
||
+ 新增待办
|
||
</el-button>
|
||
<el-radio-group v-model="filterStatus" class="ml-3" @change="handleFilterChange">
|
||
<el-radio-button :value="''">全部</el-radio-button>
|
||
<el-radio-button :value="0">待执行</el-radio-button>
|
||
<el-radio-button :value="1">已发送</el-radio-button>
|
||
<el-radio-button :value="3">失败</el-radio-button>
|
||
<el-radio-button :value="2">已取消</el-radio-button>
|
||
</el-radio-group>
|
||
<el-button class="ml-3" @click="fetchList">刷新</el-button>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="pager.loading"
|
||
:data="pager.lists"
|
||
border
|
||
class="todo-table"
|
||
empty-text="暂无待办"
|
||
row-key="id"
|
||
>
|
||
<el-table-column label="提醒时间" prop="remind_time_text" width="160" />
|
||
<el-table-column label="内容" prop="content" min-width="240" show-overflow-tooltip />
|
||
<el-table-column label="状态" width="110" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="statusTagType(row.status)" effect="light" size="small">
|
||
{{ row.status_text }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="创建人" prop="creator_name" width="120">
|
||
<template #default="{ row }">{{ row.creator_name || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="推送时间" prop="notified_at_text" width="160">
|
||
<template #default="{ row }">{{ row.notified_at_text || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="失败原因" prop="error" min-width="200" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span v-if="row.status === 3" class="text-danger">{{ row.error || '未知错误' }}</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-button
|
||
v-if="!props.readOnly && row.status === 0 && row.can_cancel"
|
||
link
|
||
type="danger"
|
||
@click="handleCancel(row)"
|
||
v-perms="DAILY_RECORD_PERMS"
|
||
>
|
||
取消
|
||
</el-button>
|
||
<span v-else class="text-muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div class="pagination-wrap">
|
||
<pagination v-model="pager" @change="fetchList" />
|
||
</div>
|
||
|
||
<!-- 新增弹框 -->
|
||
<el-dialog
|
||
v-model="addDialogVisible"
|
||
title="新增待办"
|
||
width="520px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="100px">
|
||
<el-form-item label="提醒时间" prop="remindAt">
|
||
<el-date-picker
|
||
v-model="addForm.remindAt"
|
||
type="datetime"
|
||
placeholder="选择提醒时间"
|
||
format="YYYY-MM-DD HH:mm"
|
||
value-format="x"
|
||
:disabled-date="disablePastDate"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="提醒内容" prop="content">
|
||
<el-input
|
||
v-model="addForm.content"
|
||
type="textarea"
|
||
:rows="4"
|
||
:maxlength="500"
|
||
show-word-limit
|
||
placeholder="例:3 天后回访患者复测餐后血糖"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="提醒人">
|
||
<span class="text-muted">企微推送给当前登录账号(创建人本人)</span>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="addDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="addSubmitting" @click="handleAddSubmit">
|
||
提交
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import { diagnosisTodoLists, diagnosisTodoAdd, diagnosisTodoCancel } from '@/api/tcm'
|
||
import { computed } from 'vue'
|
||
import { hasPermission } from '@/utils/perm'
|
||
import feedback from '@/utils/feedback'
|
||
|
||
const DAILY_RECORD_PERMS = ['tcm.diagnosis/dailyRecord']
|
||
|
||
interface Props {
|
||
diagnosisId: number | null | undefined
|
||
patientId?: number | null
|
||
patientName?: string
|
||
readOnly?: boolean
|
||
}
|
||
|
||
const props = defineProps<Props>()
|
||
const hasDailyRecordPermission = computed(() => hasPermission(DAILY_RECORD_PERMS))
|
||
|
||
// pager 与项目内 <pagination v-model="pager" /> 约定的字段保持一致
|
||
const pager = reactive({
|
||
page: 1,
|
||
size: 15,
|
||
count: 0,
|
||
loading: false,
|
||
lists: [] as any[]
|
||
})
|
||
|
||
const filterStatus = ref<number | ''>('')
|
||
|
||
const fetchList = async () => {
|
||
if (!props.diagnosisId || Number(props.diagnosisId) <= 0 || !hasDailyRecordPermission.value) {
|
||
pager.lists = []
|
||
pager.count = 0
|
||
return
|
||
}
|
||
pager.loading = true
|
||
try {
|
||
const params: {
|
||
page_no: number
|
||
page_size: number
|
||
diagnosis_id: number
|
||
status?: number
|
||
} = {
|
||
page_no: pager.page,
|
||
page_size: pager.size,
|
||
diagnosis_id: Number(props.diagnosisId)
|
||
}
|
||
if (filterStatus.value !== '' && filterStatus.value !== undefined) {
|
||
params.status = Number(filterStatus.value)
|
||
}
|
||
const res: any = await diagnosisTodoLists(params)
|
||
pager.count = Number(res?.count ?? 0)
|
||
pager.lists = Array.isArray(res?.lists) ? res.lists : []
|
||
} catch {
|
||
// request 拦截器已有错误 toast
|
||
pager.lists = []
|
||
pager.count = 0
|
||
} finally {
|
||
pager.loading = false
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => props.diagnosisId,
|
||
() => {
|
||
pager.page = 1
|
||
fetchList()
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
const handleFilterChange = () => {
|
||
pager.page = 1
|
||
fetchList()
|
||
}
|
||
|
||
const statusTagType = (status: number): 'info' | 'success' | 'warning' | 'danger' => {
|
||
switch (Number(status)) {
|
||
case 0:
|
||
return 'info'
|
||
case 1:
|
||
return 'success'
|
||
case 2:
|
||
return 'warning'
|
||
case 3:
|
||
return 'danger'
|
||
default:
|
||
return 'info'
|
||
}
|
||
}
|
||
|
||
// ========== 新增 ==========
|
||
const addDialogVisible = ref(false)
|
||
const addSubmitting = ref(false)
|
||
const addFormRef = ref<FormInstance>()
|
||
const addForm = reactive({
|
||
remindAt: '' as string | number,
|
||
content: ''
|
||
})
|
||
const addRules: FormRules = {
|
||
remindAt: [{ required: true, message: '请选择提醒时间', trigger: 'change' }],
|
||
content: [
|
||
{ required: true, message: '请输入提醒内容', trigger: 'blur' },
|
||
{ max: 500, message: '内容最多 500 字', trigger: 'blur' }
|
||
]
|
||
}
|
||
|
||
const disablePastDate = (date: Date) => {
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
return date.getTime() < today.getTime()
|
||
}
|
||
|
||
const handleAdd = () => {
|
||
if (!props.diagnosisId || !hasDailyRecordPermission.value) {
|
||
feedback.msgWarning('诊单未保存,无法添加待办')
|
||
return
|
||
}
|
||
addForm.remindAt = ''
|
||
addForm.content = ''
|
||
addDialogVisible.value = true
|
||
nextTick(() => addFormRef.value?.clearValidate())
|
||
}
|
||
|
||
const handleAddSubmit = async () => {
|
||
if (!addFormRef.value) return
|
||
const valid = await addFormRef.value.validate().catch(() => false)
|
||
if (!valid) return
|
||
|
||
const remindMs = Number(addForm.remindAt)
|
||
if (!Number.isFinite(remindMs) || remindMs <= Date.now() + 30 * 1000) {
|
||
feedback.msgWarning('提醒时间必须晚于当前时间至少 30 秒')
|
||
return
|
||
}
|
||
|
||
addSubmitting.value = true
|
||
try {
|
||
await diagnosisTodoAdd({
|
||
diagnosis_id: Number(props.diagnosisId),
|
||
content: addForm.content.trim(),
|
||
remind_time: Math.floor(remindMs / 1000)
|
||
})
|
||
addDialogVisible.value = false
|
||
fetchList()
|
||
} catch {
|
||
// request 拦截器已有错误 toast
|
||
} finally {
|
||
addSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
// ========== 取消 ==========
|
||
const handleCancel = async (row: any) => {
|
||
if (!hasDailyRecordPermission.value || props.readOnly) return
|
||
try {
|
||
await feedback.confirm(`确认取消「${row.remind_time_text} · ${truncate(row.content, 30)}」吗?`)
|
||
} catch {
|
||
return
|
||
}
|
||
try {
|
||
await diagnosisTodoCancel({ id: Number(row.id) })
|
||
feedback.msgSuccess('已取消')
|
||
fetchList()
|
||
} catch {
|
||
// request 拦截器已有错误 toast
|
||
}
|
||
}
|
||
|
||
const truncate = (text: string, len: number) => {
|
||
if (!text) return ''
|
||
return text.length > len ? text.slice(0, len) + '...' : text
|
||
}
|
||
|
||
defineExpose({ refresh: fetchList })
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.diagnosis-todo-list {
|
||
.toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.todo-table {
|
||
width: 100%;
|
||
}
|
||
|
||
.pagination-wrap {
|
||
margin-top: 12px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.text-danger {
|
||
color: var(--el-color-danger);
|
||
}
|
||
|
||
.text-muted {
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
}
|
||
</style>
|