更新
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** 企业微信 员工↔客户 消息收发(企业群发 + 会话内容存档) */
|
||||
|
||||
/** 会话列表(按最近消息时间倒序) */
|
||||
export interface QywxMsgSession {
|
||||
id: number
|
||||
staff_userid: string
|
||||
external_userid: string
|
||||
roomid: string
|
||||
session_type: number // 1=单聊 2=群聊
|
||||
last_msg_id: string
|
||||
last_msg_seq: number
|
||||
last_msg_time: number
|
||||
last_msg_type: string
|
||||
last_msg_summary: string
|
||||
unread_staff: number
|
||||
create_time: number
|
||||
update_time: number
|
||||
customer?: {
|
||||
name?: string
|
||||
avatar?: string
|
||||
type?: number
|
||||
gender?: number
|
||||
corp_name?: string
|
||||
unionid?: string
|
||||
} | null
|
||||
staff?: {
|
||||
id?: number
|
||||
name?: string
|
||||
avatar?: string
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface QywxMsgArchiveItem {
|
||||
id: number
|
||||
msgid: string
|
||||
seq: number
|
||||
action: 'send' | 'recall' | 'switch'
|
||||
from_user: string
|
||||
from_is_staff: boolean
|
||||
to_list: string[]
|
||||
roomid: string
|
||||
msgtype: string
|
||||
content: string
|
||||
media_id: string
|
||||
md5sum: string
|
||||
file_name: string
|
||||
file_size: number
|
||||
file_ext: string
|
||||
play_length: number
|
||||
send_time: number
|
||||
from_profile?: Record<string, any> | null
|
||||
media?: Array<{
|
||||
id: number
|
||||
sdkfileid: string
|
||||
status: number
|
||||
file_path: string
|
||||
file_url: string
|
||||
file_size: number
|
||||
}>
|
||||
}
|
||||
|
||||
export interface QywxSendTask {
|
||||
id: number
|
||||
admin_id: number
|
||||
sender_userid: string
|
||||
external_userids: string[]
|
||||
chat_type: number
|
||||
msg_payload: any
|
||||
msg_template_id: string
|
||||
fail_list: any[]
|
||||
status: number
|
||||
error: string
|
||||
create_time: number
|
||||
update_time: number
|
||||
}
|
||||
|
||||
export interface QywxMsgAttachment {
|
||||
msgtype: 'image' | 'video' | 'file' | 'link' | 'miniprogram'
|
||||
image?: { media_id: string }
|
||||
video?: { media_id: string }
|
||||
file?: { media_id: string }
|
||||
link?: { title: string; picurl?: string; desc?: string; url: string }
|
||||
miniprogram?: { title: string; pic_media_id: string; appid: string; page: string }
|
||||
}
|
||||
|
||||
export interface QywxSendPayload {
|
||||
text?: { content: string }
|
||||
attachments?: QywxMsgAttachment[]
|
||||
}
|
||||
|
||||
/** 会话列表 */
|
||||
export function qywxMsgSessionLists(params: {
|
||||
page_no?: number
|
||||
page_size?: number
|
||||
admin_id?: number
|
||||
staff_userid?: string
|
||||
external_userid?: string
|
||||
keyword?: string
|
||||
only_unread?: 0 | 1
|
||||
}) {
|
||||
return request.get({ url: '/qywx.message/session_list', params })
|
||||
}
|
||||
|
||||
/** 单会话消息历史 */
|
||||
export function qywxMsgArchiveLists(params: {
|
||||
page_no?: number
|
||||
page_size?: number
|
||||
session_id?: number
|
||||
staff_userid?: string
|
||||
external_userid?: string
|
||||
roomid?: string
|
||||
before_time?: number
|
||||
after_time?: number
|
||||
}) {
|
||||
return request.get({ url: '/qywx.message/archive_list', params })
|
||||
}
|
||||
|
||||
/** 清零会话未读 */
|
||||
export function qywxMsgMarkRead(session_id: number) {
|
||||
return request.post({ url: '/qywx.message/mark_read', params: { session_id } })
|
||||
}
|
||||
|
||||
/** 创建企业群发任务(员工代发) */
|
||||
export function qywxMsgSend(params: {
|
||||
sender_userid: string
|
||||
external_userids: string[]
|
||||
msg_payload: QywxSendPayload
|
||||
chat_type?: 'single' | 'group'
|
||||
}) {
|
||||
return request.post({ url: '/qywx.message/send', params })
|
||||
}
|
||||
|
||||
/** 群发任务列表 */
|
||||
export function qywxMsgSendTaskLists(params: {
|
||||
page_no?: number
|
||||
page_size?: number
|
||||
sender_userid?: string
|
||||
status?: number
|
||||
}) {
|
||||
return request.get({ url: '/qywx.message/send_task_list', params })
|
||||
}
|
||||
|
||||
/** 群发任务送达详情 */
|
||||
export function qywxMsgSendTaskDetail(task_id: number, cursor = '') {
|
||||
return request.get({ url: '/qywx.message/send_task_detail', params: { task_id, cursor } })
|
||||
}
|
||||
|
||||
/** 可代发员工(已绑定企微 userid) */
|
||||
export function qywxMsgStaffList(keyword = '') {
|
||||
return request.get({ url: '/qywx.message/staff_list', params: { keyword } })
|
||||
}
|
||||
|
||||
/** 某员工的客户 */
|
||||
export function qywxMsgCustomerOfStaff(params: {
|
||||
staff_userid: string
|
||||
keyword?: string
|
||||
limit?: number
|
||||
}) {
|
||||
return request.get({ url: '/qywx.message/customer_of_staff', params })
|
||||
}
|
||||
|
||||
/** 会话存档状态(SDK / 私钥诊断) */
|
||||
export function qywxMsgArchiveStatus() {
|
||||
return request.get({ url: '/qywx.message/archive_status' })
|
||||
}
|
||||
|
||||
/** 手动触发一次会话存档拉取 */
|
||||
export function qywxMsgPullArchive(max_batches = 5, download = false) {
|
||||
return request.post({ url: '/qywx.message/pull_archive', params: { max_batches, download } })
|
||||
}
|
||||
|
||||
/** 上传媒体文件到企微,返回 media_id(用于 attachments) */
|
||||
export async function qywxMsgUploadMedia(file: File, type: 'image' | 'voice' | 'video' | 'file') {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
fd.append('type', type)
|
||||
return request.post({
|
||||
url: '/qywx.message/upload_to_qywx',
|
||||
data: fd,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
@@ -307,7 +307,10 @@ export function prescriptionOrderLists(params: any) {
|
||||
return request.get({ url: '/tcm.prescriptionOrder/lists', params })
|
||||
}
|
||||
|
||||
/** 诊单下可关联的支付单(待支付/已支付的 zyt_order,已占用且未撤回的会排除;编辑时传 prescription_order_id 保留当前单已选) */
|
||||
/**
|
||||
* 诊单下可关联的支付单(已支付 zyt_order;已占用且未撤回的会排除;编辑时传 prescription_order_id 保留当前单已选)。
|
||||
* 服务端仅返回创建时间在 2026-04-20(含)之后的支付单;编辑时本单已关联的旧单仍会出现在列表中。
|
||||
*/
|
||||
export function prescriptionOrderPaidPayOrders(params: {
|
||||
diagnosis_id: number
|
||||
prescription_order_id?: number
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<div class="msg-bubble" :class="{ 'is-staff': fromStaff }">
|
||||
<el-avatar class="msg-avatar" :size="36" :src="avatarUrl">
|
||||
{{ shortName }}
|
||||
</el-avatar>
|
||||
<div class="msg-body">
|
||||
<div class="msg-meta">
|
||||
<span class="msg-sender">{{ senderName }}</span>
|
||||
<span class="msg-time">{{ formatTime(message.send_time) }}</span>
|
||||
<el-tag
|
||||
v-if="message.action === 'recall'"
|
||||
size="small"
|
||||
type="info"
|
||||
>
|
||||
已撤回
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="msg-content" :class="bubbleClass">
|
||||
<!-- 文本 / markdown -->
|
||||
<div
|
||||
v-if="message.msgtype === 'text' || message.msgtype === 'markdown'"
|
||||
class="content-text"
|
||||
>
|
||||
{{ message.content }}
|
||||
</div>
|
||||
|
||||
<!-- 图片 -->
|
||||
<el-image
|
||||
v-else-if="message.msgtype === 'image' && firstMedia?.file_url"
|
||||
:src="firstMedia.file_url"
|
||||
:preview-src-list="[firstMedia.file_url]"
|
||||
fit="contain"
|
||||
class="content-image"
|
||||
hide-on-click-modal
|
||||
/>
|
||||
<div v-else-if="message.msgtype === 'image'" class="content-pending">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>图片下载中…</span>
|
||||
</div>
|
||||
|
||||
<!-- 视频 -->
|
||||
<video
|
||||
v-else-if="message.msgtype === 'video' && firstMedia?.file_url"
|
||||
:src="firstMedia.file_url"
|
||||
controls
|
||||
class="content-video"
|
||||
/>
|
||||
<div v-else-if="message.msgtype === 'video'" class="content-pending">
|
||||
<el-icon><VideoCamera /></el-icon>
|
||||
<span>视频下载中…</span>
|
||||
</div>
|
||||
|
||||
<!-- 语音 -->
|
||||
<audio
|
||||
v-else-if="message.msgtype === 'voice' && firstMedia?.file_url"
|
||||
:src="firstMedia.file_url"
|
||||
controls
|
||||
class="content-audio"
|
||||
/>
|
||||
<div v-else-if="message.msgtype === 'voice'" class="content-pending">
|
||||
<el-icon><Microphone /></el-icon>
|
||||
<span>语音下载中…({{ message.play_length }}s)</span>
|
||||
</div>
|
||||
|
||||
<!-- 文件 -->
|
||||
<div v-else-if="message.msgtype === 'file'" class="content-file">
|
||||
<el-icon :size="24"><Document /></el-icon>
|
||||
<div class="file-meta">
|
||||
<div class="file-name" :title="message.file_name">
|
||||
{{ message.file_name || '未命名文件' }}
|
||||
</div>
|
||||
<div class="file-info">
|
||||
{{ formatSize(message.file_size) }}
|
||||
<template v-if="message.file_ext">· {{ message.file_ext }}</template>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="firstMedia?.file_url"
|
||||
size="small"
|
||||
type="primary"
|
||||
link
|
||||
@click="openFile(firstMedia.file_url)"
|
||||
>
|
||||
下载
|
||||
</el-button>
|
||||
<el-tag v-else size="small" type="info">下载中…</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 链接 / 小程序 / 聊天记录 / 位置等 -->
|
||||
<div v-else class="content-fallback">
|
||||
<div class="fallback-title">{{ fallbackLabel }}</div>
|
||||
<div class="fallback-desc">{{ message.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Document, Microphone, Picture, VideoCamera } from '@element-plus/icons-vue'
|
||||
import type { QywxMsgArchiveItem } from '@/api/qywx-msg'
|
||||
import { timeFormat } from '@/utils/util'
|
||||
|
||||
const props = defineProps<{ message: QywxMsgArchiveItem }>()
|
||||
|
||||
const fromStaff = computed(() => props.message.from_is_staff)
|
||||
|
||||
const firstMedia = computed(() => {
|
||||
const list = props.message.media || []
|
||||
return list.find((m) => m.status === 1) ?? null
|
||||
})
|
||||
|
||||
const senderName = computed(() => {
|
||||
const p = props.message.from_profile as any
|
||||
if (!p) return props.message.from_user
|
||||
return p.name || props.message.from_user
|
||||
})
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
const p = props.message.from_profile as any
|
||||
return p?.avatar || ''
|
||||
})
|
||||
|
||||
const shortName = computed(() => {
|
||||
const name = senderName.value || ''
|
||||
return name.slice(-2) || '?'
|
||||
})
|
||||
|
||||
const bubbleClass = computed(() => {
|
||||
const t = props.message.msgtype
|
||||
if (t === 'image' || t === 'video' || t === 'voice') return 'bubble-media'
|
||||
if (t === 'file') return 'bubble-file'
|
||||
return 'bubble-text'
|
||||
})
|
||||
|
||||
const fallbackLabel = computed(() => {
|
||||
switch (props.message.msgtype) {
|
||||
case 'link':
|
||||
return '[链接]'
|
||||
case 'weapp':
|
||||
return '[小程序]'
|
||||
case 'chatrecord':
|
||||
return '[聊天记录]'
|
||||
case 'location':
|
||||
return '[位置]'
|
||||
case 'card':
|
||||
return '[名片]'
|
||||
case 'meeting':
|
||||
return '[会议]'
|
||||
case 'docmsg':
|
||||
return '[文档]'
|
||||
case 'mixed':
|
||||
return '[混合消息]'
|
||||
case 'emotion':
|
||||
return '[表情]'
|
||||
default:
|
||||
return `[${props.message.msgtype}]`
|
||||
}
|
||||
})
|
||||
|
||||
function formatTime(sec: number) {
|
||||
if (!sec) return ''
|
||||
return timeFormat(sec * 1000, 'mm-dd hh:MM')
|
||||
}
|
||||
|
||||
function formatSize(bytes: number) {
|
||||
if (!bytes) return ''
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
||||
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
function openFile(url: string) {
|
||||
window.open(url, '_blank', 'noopener')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.msg-bubble {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px 20px;
|
||||
align-items: flex-start;
|
||||
|
||||
&.is-staff {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.msg-body {
|
||||
align-items: flex-end;
|
||||
}
|
||||
.msg-content.bubble-text,
|
||||
.msg-content.bubble-file {
|
||||
background: #95ec69;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-avatar {
|
||||
flex-shrink: 0;
|
||||
background: #d0d7de;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.msg-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-width: 65%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.msg-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.msg-sender {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.msg-content {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
word-break: break-word;
|
||||
line-height: 1.6;
|
||||
max-width: 100%;
|
||||
|
||||
&.bubble-media {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.content-text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.content-image {
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content-video {
|
||||
max-width: 320px;
|
||||
max-height: 320px;
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content-audio {
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.content-file {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-width: 220px;
|
||||
padding: 4px 8px;
|
||||
|
||||
.file-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.file-info {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
.content-pending {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.content-fallback {
|
||||
.fallback-title {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.fallback-desc {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,471 @@
|
||||
<template>
|
||||
<div class="send-panel">
|
||||
<div v-if="attachments.length" class="attachment-list">
|
||||
<div
|
||||
v-for="(att, idx) in attachments"
|
||||
:key="idx"
|
||||
class="attachment-item"
|
||||
:class="'attachment-' + att.msgtype"
|
||||
>
|
||||
<template v-if="att.msgtype === 'image'">
|
||||
<el-image
|
||||
:src="att.preview || ''"
|
||||
fit="cover"
|
||||
class="attachment-preview"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="att.msgtype === 'video'">
|
||||
<video :src="att.preview || ''" class="attachment-preview" muted />
|
||||
</template>
|
||||
<template v-else-if="att.msgtype === 'file'">
|
||||
<el-icon :size="20"><Document /></el-icon>
|
||||
<span class="attachment-filename">{{ att.file_name }}</span>
|
||||
</template>
|
||||
<template v-else-if="att.msgtype === 'link'">
|
||||
<el-icon :size="20"><Link /></el-icon>
|
||||
<span class="attachment-filename">{{ att.link?.title || att.link?.url }}</span>
|
||||
</template>
|
||||
<template v-else-if="att.msgtype === 'miniprogram'">
|
||||
<el-icon :size="20"><Grid /></el-icon>
|
||||
<span class="attachment-filename">{{ att.miniprogram?.title }}</span>
|
||||
</template>
|
||||
|
||||
<el-button
|
||||
v-if="att.uploading"
|
||||
type="primary"
|
||||
link
|
||||
loading
|
||||
size="small"
|
||||
/>
|
||||
<el-button
|
||||
v-else
|
||||
class="attachment-remove"
|
||||
type="danger"
|
||||
link
|
||||
@click="removeAttachment(idx)"
|
||||
>
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-input
|
||||
v-model="textContent"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="在此输入消息,Ctrl+Enter 发送。该消息以代发员工身份发给客户,员工手机端确认后送达。"
|
||||
resize="none"
|
||||
@keydown.enter.ctrl="onSend"
|
||||
/>
|
||||
|
||||
<div class="send-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-tooltip content="图片" placement="top">
|
||||
<el-button link @click="triggerFile('image')">
|
||||
<el-icon :size="18"><Picture /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="视频" placement="top">
|
||||
<el-button link @click="triggerFile('video')">
|
||||
<el-icon :size="18"><VideoCamera /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="文件" placement="top">
|
||||
<el-button link @click="triggerFile('file')">
|
||||
<el-icon :size="18"><Document /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="链接" placement="top">
|
||||
<el-button link @click="showLinkDialog = true">
|
||||
<el-icon :size="18"><Link /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="小程序" placement="top">
|
||||
<el-button link @click="showMiniprogramDialog = true">
|
||||
<el-icon :size="18"><Grid /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<span class="send-hint">
|
||||
员工手机端确认后送达客户 · Ctrl+Enter 快速发送
|
||||
</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!canSend"
|
||||
:loading="sending"
|
||||
@click="onSend"
|
||||
>
|
||||
发送
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input ref="fileInputRef" type="file" hidden @change="onFileSelected" />
|
||||
|
||||
<!-- 链接弹窗 -->
|
||||
<el-dialog v-model="showLinkDialog" title="添加链接" width="480px">
|
||||
<el-form label-width="80px" :model="linkForm">
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="linkForm.title" maxlength="128" />
|
||||
</el-form-item>
|
||||
<el-form-item label="链接地址">
|
||||
<el-input v-model="linkForm.url" placeholder="https://..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="封面图">
|
||||
<div class="link-picurl-row">
|
||||
<el-input v-model="linkForm.picurl" placeholder="https://..." />
|
||||
<el-button @click="triggerLinkImage">本地上传</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="linkForm.desc" type="textarea" :rows="2" maxlength="512" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showLinkDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmLink">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 小程序弹窗 -->
|
||||
<el-dialog v-model="showMiniprogramDialog" title="添加小程序" width="480px">
|
||||
<el-form label-width="100px" :model="miniForm">
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="miniForm.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="appid">
|
||||
<el-input v-model="miniForm.appid" placeholder="wx..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="页面路径">
|
||||
<el-input v-model="miniForm.page" placeholder="/pages/index/index" />
|
||||
</el-form-item>
|
||||
<el-form-item label="封面图">
|
||||
<el-button @click="triggerMiniImage">
|
||||
{{ miniForm.pic_media_id ? '已选择 · 重新选择' : '上传封面(必填)' }}
|
||||
</el-button>
|
||||
<div v-if="miniForm.preview" class="mini-preview">
|
||||
<el-image :src="miniForm.preview" fit="cover" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showMiniprogramDialog = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!miniForm.pic_media_id || !miniForm.appid" @click="confirmMiniprogram">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { Close, Document, Grid, Link, Picture, VideoCamera } from '@element-plus/icons-vue'
|
||||
import feedback from '@/utils/feedback'
|
||||
import {
|
||||
qywxMsgUploadMedia,
|
||||
type QywxMsgAttachment,
|
||||
type QywxSendPayload
|
||||
} from '@/api/qywx-msg'
|
||||
import { uploadImageBlob } from '@/api/file'
|
||||
|
||||
interface AttachmentViewModel extends QywxMsgAttachment {
|
||||
uploading?: boolean
|
||||
preview?: string
|
||||
file_name?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
canSendMessage: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'send', payload: QywxSendPayload): Promise<void> | void
|
||||
}>()
|
||||
|
||||
const textContent = ref('')
|
||||
const attachments = ref<AttachmentViewModel[]>([])
|
||||
const sending = ref(false)
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
const pendingUploadType = ref<'image' | 'video' | 'file' | 'link-cover' | 'mini-cover'>('image')
|
||||
|
||||
const showLinkDialog = ref(false)
|
||||
const linkForm = ref({ title: '', url: '', picurl: '', desc: '' })
|
||||
|
||||
const showMiniprogramDialog = ref(false)
|
||||
const miniForm = ref({
|
||||
title: '',
|
||||
appid: '',
|
||||
page: '',
|
||||
pic_media_id: '',
|
||||
preview: ''
|
||||
})
|
||||
|
||||
const canSend = computed(() => {
|
||||
if (!props.canSendMessage) return false
|
||||
if (sending.value) return false
|
||||
const hasText = textContent.value.trim().length > 0
|
||||
const hasAtt = attachments.value.some((a) => !a.uploading)
|
||||
return hasText || hasAtt
|
||||
})
|
||||
|
||||
function triggerFile(type: 'image' | 'video' | 'file') {
|
||||
if (attachments.value.length >= 9) {
|
||||
feedback.msgWarning('最多添加 9 个附件')
|
||||
return
|
||||
}
|
||||
pendingUploadType.value = type
|
||||
const accept =
|
||||
type === 'image' ? 'image/*' : type === 'video' ? 'video/*' : '*/*'
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.accept = accept
|
||||
fileInputRef.value.value = ''
|
||||
fileInputRef.value.click()
|
||||
}
|
||||
}
|
||||
|
||||
function triggerLinkImage() {
|
||||
pendingUploadType.value = 'link-cover'
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.accept = 'image/*'
|
||||
fileInputRef.value.value = ''
|
||||
fileInputRef.value.click()
|
||||
}
|
||||
}
|
||||
|
||||
function triggerMiniImage() {
|
||||
pendingUploadType.value = 'mini-cover'
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.accept = 'image/*'
|
||||
fileInputRef.value.value = ''
|
||||
fileInputRef.value.click()
|
||||
}
|
||||
}
|
||||
|
||||
async function onFileSelected(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const type = pendingUploadType.value
|
||||
if (type === 'link-cover') {
|
||||
try {
|
||||
const uploaded = await uploadImageBlob(file, file.name)
|
||||
linkForm.value.picurl = uploaded.uri || uploaded.url
|
||||
feedback.msgSuccess('封面上传成功')
|
||||
} catch (err: any) {
|
||||
feedback.msgError(err?.message || '封面上传失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
if (type === 'mini-cover') {
|
||||
try {
|
||||
const res: any = await qywxMsgUploadMedia(file, 'image')
|
||||
miniForm.value.pic_media_id = res.media_id
|
||||
const previewRes = await uploadImageBlob(file, file.name)
|
||||
miniForm.value.preview = previewRes.uri || previewRes.url
|
||||
feedback.msgSuccess('小程序封面上传成功')
|
||||
} catch (err: any) {
|
||||
feedback.msgError(err?.message || '封面上传失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 常规附件:image / video / file
|
||||
const preview = type === 'file' ? '' : URL.createObjectURL(file)
|
||||
const vm: AttachmentViewModel = {
|
||||
msgtype: type,
|
||||
uploading: true,
|
||||
preview,
|
||||
file_name: file.name
|
||||
}
|
||||
attachments.value.push(vm)
|
||||
const idx = attachments.value.length - 1
|
||||
|
||||
try {
|
||||
const res: any = await qywxMsgUploadMedia(file, type)
|
||||
const mediaId = res.media_id as string
|
||||
if (type === 'image') {
|
||||
vm.image = { media_id: mediaId }
|
||||
} else if (type === 'video') {
|
||||
vm.video = { media_id: mediaId }
|
||||
} else {
|
||||
vm.file = { media_id: mediaId }
|
||||
}
|
||||
vm.uploading = false
|
||||
} catch (err: any) {
|
||||
feedback.msgError(err?.message || '上传失败')
|
||||
attachments.value.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function removeAttachment(idx: number) {
|
||||
const vm = attachments.value[idx]
|
||||
if (vm?.preview && vm.preview.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(vm.preview)
|
||||
}
|
||||
attachments.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
function confirmLink() {
|
||||
const { title, url, picurl, desc } = linkForm.value
|
||||
if (!title.trim() || !url.trim() || !picurl.trim()) {
|
||||
feedback.msgWarning('请完整填写标题、链接地址与封面图')
|
||||
return
|
||||
}
|
||||
if (attachments.value.length >= 9) {
|
||||
feedback.msgWarning('最多添加 9 个附件')
|
||||
return
|
||||
}
|
||||
attachments.value.push({
|
||||
msgtype: 'link',
|
||||
link: { title, url, picurl, desc }
|
||||
})
|
||||
showLinkDialog.value = false
|
||||
linkForm.value = { title: '', url: '', picurl: '', desc: '' }
|
||||
}
|
||||
|
||||
function confirmMiniprogram() {
|
||||
const { title, appid, page, pic_media_id } = miniForm.value
|
||||
if (!title.trim() || !appid.trim() || !page.trim() || !pic_media_id) {
|
||||
feedback.msgWarning('请完整填写小程序信息')
|
||||
return
|
||||
}
|
||||
if (attachments.value.length >= 9) {
|
||||
feedback.msgWarning('最多添加 9 个附件')
|
||||
return
|
||||
}
|
||||
attachments.value.push({
|
||||
msgtype: 'miniprogram',
|
||||
miniprogram: { title, appid, page, pic_media_id }
|
||||
})
|
||||
showMiniprogramDialog.value = false
|
||||
miniForm.value = { title: '', appid: '', page: '', pic_media_id: '', preview: '' }
|
||||
}
|
||||
|
||||
async function onSend() {
|
||||
if (!canSend.value) return
|
||||
// 去掉 VM-only 字段,保留企微接口字段
|
||||
const pureAtts: QywxMsgAttachment[] = attachments.value
|
||||
.filter((a) => !a.uploading)
|
||||
.map((a) => {
|
||||
const base: QywxMsgAttachment = { msgtype: a.msgtype }
|
||||
if (a.image) base.image = a.image
|
||||
if (a.video) base.video = a.video
|
||||
if (a.file) base.file = a.file
|
||||
if (a.link) base.link = a.link
|
||||
if (a.miniprogram) base.miniprogram = a.miniprogram
|
||||
return base
|
||||
})
|
||||
const payload: QywxSendPayload = {}
|
||||
const trimmed = textContent.value.trim()
|
||||
if (trimmed) payload.text = { content: trimmed }
|
||||
if (pureAtts.length) payload.attachments = pureAtts
|
||||
|
||||
sending.value = true
|
||||
try {
|
||||
await emit('send', payload)
|
||||
textContent.value = ''
|
||||
attachments.value.forEach((a) => {
|
||||
if (a.preview?.startsWith('blob:')) URL.revokeObjectURL(a.preview)
|
||||
})
|
||||
attachments.value = []
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.send-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: #fafafa;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
.attachment-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
max-width: 220px;
|
||||
|
||||
.attachment-preview {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.attachment-filename {
|
||||
max-width: 140px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.attachment-remove {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.send-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.send-hint {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-picurl-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mini-preview {
|
||||
margin-top: 8px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
|
||||
.el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1544,7 +1544,9 @@ async function loadCreateOrderPaidOrders() {
|
||||
}
|
||||
try {
|
||||
createOrderPaidOrdersLoading.value = true
|
||||
const res: any = await prescriptionOrderPaidPayOrders({ diagnosis_id: did })
|
||||
const res: any = await prescriptionOrderPaidPayOrders({
|
||||
diagnosis_id: did
|
||||
})
|
||||
const lists = res?.lists ?? res?.data?.lists ?? []
|
||||
createOrderPaidOrders.value = Array.isArray(lists) ? lists : []
|
||||
const dep = Number(res?.deposit_min_amount ?? res?.data?.deposit_min_amount) || 0
|
||||
|
||||
@@ -444,10 +444,10 @@
|
||||
<el-descriptions-item label="处方日期">{{ detailPrescription.prescription_date || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="医师">{{ detailPrescription.doctor_name || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">{{ detailPrescription.prescription_type || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="临床诊断" :span="2">
|
||||
<el-descriptions-item label="临床诊断" :span="2" >
|
||||
{{ detailPrescription.clinical_diagnosis || '—' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="剂数 / 用法" :span="2">
|
||||
<el-descriptions-item label="剂数 / 用法" :span="2" v-if="showPrescriptionAuditFilter">
|
||||
{{ detailData.dose_count ?? detailPrescription.dose_count ?? '—' }} {{ detailData.dose_unit || '剂' }} ·
|
||||
{{ detailPrescription.usage_instruction || detailPrescription.usage_method || '—' }}
|
||||
</el-descriptions-item>
|
||||
@@ -1357,35 +1357,60 @@
|
||||
<!-- 处方详情查看(处方单样式) -->
|
||||
<el-drawer
|
||||
v-model="prescriptionViewVisible"
|
||||
title="查看处方"
|
||||
size="920px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full pr-2 gap-3">
|
||||
<span class="text-base font-medium text-gray-800">查看处方</span>
|
||||
<div v-if="prescriptionViewData && !prescriptionViewLoading" class="flex items-center gap-2 shrink-0">
|
||||
<el-button
|
||||
size="small"
|
||||
:loading="prescriptionSlipExporting"
|
||||
@click="downloadPrescriptionSlipImage"
|
||||
>
|
||||
下载图片
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:loading="prescriptionSlipExporting"
|
||||
@click="downloadPrescriptionSlipPdf"
|
||||
>
|
||||
下载PDF
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-loading="prescriptionViewLoading" class="cf-slip-wrap">
|
||||
<div v-if="prescriptionViewData" class="cf-slip-print">
|
||||
<div v-if="prescriptionViewData" ref="prescriptionSlipPrintRef" class="cf-slip-print">
|
||||
<div class="cf-slip-header">
|
||||
<h2 class="cf-slip-title">{{ SLIP_HOSPITAL_NAME }}处方笺</h2>
|
||||
<div class="cf-slip-type">
|
||||
<el-tag v-if="Number(prescriptionViewData.void_status) === 1" type="danger" size="small">已作废</el-tag>
|
||||
<el-tag
|
||||
v-if="Number(prescriptionViewData.void_status) === 1"
|
||||
type="danger"
|
||||
size="small"
|
||||
class="cf-slip-no-export"
|
||||
>已作废</el-tag>
|
||||
<el-tag
|
||||
v-else-if="Number(prescriptionViewData.business_prescription_audit_rejected) === 1"
|
||||
type="danger"
|
||||
size="small"
|
||||
class="mr-1"
|
||||
class="mr-1 cf-slip-no-export"
|
||||
>已驳回</el-tag>
|
||||
<el-tag
|
||||
v-else-if="Number(prescriptionViewData.audit_status) === 1"
|
||||
type="success"
|
||||
size="small"
|
||||
class="mr-1"
|
||||
class="mr-1 cf-slip-no-export"
|
||||
>已通过</el-tag>
|
||||
<el-tag
|
||||
v-else-if="Number(prescriptionViewData.audit_status) === 0"
|
||||
type="warning"
|
||||
size="small"
|
||||
class="mr-1"
|
||||
class="mr-1 cf-slip-no-export"
|
||||
>待审核</el-tag>
|
||||
<el-tag v-else type="danger" size="small" class="mr-1">已驳回</el-tag>
|
||||
<el-tag v-else type="danger" size="small" class="mr-1 cf-slip-no-export">已驳回</el-tag>
|
||||
<template v-if="Number(prescriptionViewData.void_status) !== 1">
|
||||
<div>普通处方</div>
|
||||
<div class="text-sm">当日有效</div>
|
||||
@@ -1397,7 +1422,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="Number(prescriptionViewData.business_prescription_audit_rejected) === 1"
|
||||
class="rounded border border-red-200 bg-red-50 text-red-800 text-sm px-3 py-2 mb-3"
|
||||
class="cf-slip-no-export rounded border border-red-200 bg-red-50 text-red-800 text-sm px-3 py-2 mb-3"
|
||||
>
|
||||
<div class="font-medium">审核状态:已驳回(业务订单·处方审核)</div>
|
||||
<div v-if="prescriptionViewData.business_prescription_audit_remark" class="mt-1">
|
||||
@@ -1407,7 +1432,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="Number(prescriptionViewData.audit_status) === 2"
|
||||
class="rounded border border-red-200 bg-red-50 text-red-800 text-sm px-3 py-2 mb-3"
|
||||
class="cf-slip-no-export rounded border border-red-200 bg-red-50 text-red-800 text-sm px-3 py-2 mb-3"
|
||||
>
|
||||
<div class="font-medium">消费者处方审核:已驳回</div>
|
||||
<div v-if="prescriptionViewData.audit_remark" class="mt-1">驳回意见:{{ prescriptionViewData.audit_remark }}</div>
|
||||
@@ -1451,6 +1476,9 @@
|
||||
<span class="cf-slip-inline-gap" />
|
||||
服用方式: {{ prescriptionViewData.usage_way ? ' ' + prescriptionViewData.usage_way : '' }}
|
||||
</div>
|
||||
<div v-if="slipChuwanVisible">
|
||||
出丸:{{ slipPillGrams === null ? '—' : slipPillGrams }} 克
|
||||
</div>
|
||||
<div v-if="slipDietaryText" class="cf-slip-dietary">忌口:{{ slipDietaryText }}</div>
|
||||
<div v-if="prescriptionViewData.usage_notes" class="cf-slip-notes">{{ prescriptionViewData.usage_notes }}</div>
|
||||
</div>
|
||||
@@ -1642,6 +1670,8 @@ import {
|
||||
prescriptionOrderPreviewGancaoRecipel,
|
||||
prescriptionDetail
|
||||
} from '@/api/tcm'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { jsPDF } from 'jspdf'
|
||||
import { getDictData } from '@/api/app'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
@@ -1926,10 +1956,19 @@ function formatTime(v: unknown) {
|
||||
return String(v)
|
||||
}
|
||||
|
||||
function canEditRow(row: { fulfillment_status?: number }) {
|
||||
function canEditRow(row: {
|
||||
fulfillment_status?: number
|
||||
gancao_reciperl_order_no?: string | null
|
||||
gancao_submit_time?: number | null
|
||||
}) {
|
||||
// 已发货(5)、已签收(6)、已完成(3)、已取消(4) 不可编辑
|
||||
const fs = Number(row.fulfillment_status)
|
||||
return fs === 1 || fs === 2
|
||||
if (fs !== 1 && fs !== 2) return false
|
||||
// 已成功提交甘草(已生成甘草处方单号 或 提交时间 > 0)不允许再编辑
|
||||
const gcNo = String(row.gancao_reciperl_order_no || '').trim()
|
||||
const gcTime = Number(row.gancao_submit_time || 0)
|
||||
if (gcNo !== '' || gcTime > 0) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function canRxAudit(row: { prescription_audit_status?: number; fulfillment_status?: number }) {
|
||||
@@ -2097,7 +2136,7 @@ async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrde
|
||||
prescription_order_id: prescriptionOrderId
|
||||
})
|
||||
const lists = res?.lists ?? res?.data?.lists ?? []
|
||||
// 过滤掉已关联的支付单
|
||||
// 列表已由服务端限制为 2026-04-20 及之后创建的支付单(本单已关联除外);此处再过滤已关联项
|
||||
const filtered = Array.isArray(lists)
|
||||
? lists.filter((item: any) => !linkedIds.includes(item.id))
|
||||
: []
|
||||
@@ -2386,6 +2425,7 @@ async function loadEditPaidOrders(diagnosisId: number, prescriptionOrderId?: num
|
||||
...(prescriptionOrderId ? { prescription_order_id: prescriptionOrderId } : {})
|
||||
})
|
||||
const lists = res?.lists ?? res?.data?.lists ?? []
|
||||
// 可选支付单:服务端仅 2026-04-20 及之后;编辑时含本单已关联的旧单
|
||||
editPaidOrders.value = Array.isArray(lists) ? lists : []
|
||||
const dep = Number(res?.deposit_min_amount ?? res?.data?.deposit_min_amount)
|
||||
if (!Number.isNaN(dep) && dep >= 0) {
|
||||
@@ -2993,7 +3033,7 @@ async function loadAddPayOrderAvailable(diagnosisId: number, prescriptionOrderId
|
||||
prescription_order_id: prescriptionOrderId
|
||||
})
|
||||
const lists = res?.lists ?? res?.data?.lists ?? []
|
||||
// 过滤掉当前订单已关联的支付单
|
||||
// 服务端仅 2026-04-20 及之后;再排除当前订单已关联
|
||||
const filtered = Array.isArray(lists)
|
||||
? lists.filter((item: any) => !currentLinkedIds.includes(item.id))
|
||||
: []
|
||||
@@ -3155,6 +3195,8 @@ const SLIP_ADDRESS_LINE = '地址:四川省成都市双流区黄甲街道黄
|
||||
const prescriptionViewVisible = ref(false)
|
||||
const prescriptionViewLoading = ref(false)
|
||||
const prescriptionViewData = ref<any>(null)
|
||||
const prescriptionSlipPrintRef = ref<HTMLElement | null>(null)
|
||||
const prescriptionSlipExporting = ref(false)
|
||||
|
||||
// 处方单辅助函数
|
||||
const slipHerbsList = computed(() => {
|
||||
@@ -3169,6 +3211,23 @@ const slipDietaryText = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
const slipChuwanVisible = computed(() => {
|
||||
const d = prescriptionViewData.value
|
||||
if (!d) return false
|
||||
return (d.prescription_type || '浓缩水丸') === '浓缩水丸'
|
||||
})
|
||||
|
||||
const slipPillGrams = computed(() => {
|
||||
const d = prescriptionViewData.value
|
||||
if (!d || (d.prescription_type || '浓缩水丸') !== '浓缩水丸') return null
|
||||
const days = Number(d.usage_days ?? d.dose_count)
|
||||
const times = Number(d.times_per_day)
|
||||
const doseG = Number(d.dosage_amount)
|
||||
if (!Number.isFinite(days) || !Number.isFinite(times) || !Number.isFinite(doseG)) return null
|
||||
if (days <= 0 || times <= 0 || doseG <= 0) return null
|
||||
return days * times * doseG
|
||||
})
|
||||
|
||||
function formatAuditTime(ts: number | null | undefined) {
|
||||
if (!ts) return ''
|
||||
const d = new Date(ts * 1000)
|
||||
@@ -3215,6 +3274,93 @@ async function openPrescriptionView(row: any) {
|
||||
prescriptionViewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function preloadSlipImagesToDataUrl(el: HTMLElement): Promise<void> {
|
||||
const imgs = el.querySelectorAll<HTMLImageElement>('img[src^="http"]')
|
||||
const tasks = Array.from(imgs).map(async (img) => {
|
||||
try {
|
||||
const res = await fetch(img.src, { mode: 'cors' })
|
||||
if (!res.ok) return
|
||||
const blob = await res.blob()
|
||||
const dataUrl = await new Promise<string>((resolve, reject) => {
|
||||
const r = new FileReader()
|
||||
r.onload = () => resolve(r.result as string)
|
||||
r.onerror = reject
|
||||
r.readAsDataURL(blob)
|
||||
})
|
||||
img.src = dataUrl
|
||||
} catch {
|
||||
/* 跨域失败时保留原 src */
|
||||
}
|
||||
})
|
||||
await Promise.allSettled(tasks)
|
||||
}
|
||||
|
||||
async function capturePrescriptionSlipCanvas(): Promise<HTMLCanvasElement> {
|
||||
await nextTick()
|
||||
const el = prescriptionSlipPrintRef.value
|
||||
if (!el) throw new Error('未找到处方笺内容')
|
||||
el.scrollIntoView({ behavior: 'instant', block: 'nearest' })
|
||||
await preloadSlipImagesToDataUrl(el)
|
||||
await new Promise((r) => setTimeout(r, 200))
|
||||
return html2canvas(el, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
backgroundColor: '#ffffff',
|
||||
ignoreElements: (node) =>
|
||||
node instanceof HTMLElement && node.classList.contains('cf-slip-no-export')
|
||||
})
|
||||
}
|
||||
|
||||
function slipDownloadBaseName() {
|
||||
const name = prescriptionViewData.value?.patient_name
|
||||
const safe = typeof name === 'string' && name.trim() ? name.trim().replace(/[\\/:*?"<>|]/g, '_') : '未命名'
|
||||
return `处方_${safe}_${Date.now()}`
|
||||
}
|
||||
|
||||
async function downloadPrescriptionSlipImage() {
|
||||
if (!prescriptionViewData.value) return
|
||||
prescriptionSlipExporting.value = true
|
||||
try {
|
||||
const canvas = await capturePrescriptionSlipCanvas()
|
||||
const link = document.createElement('a')
|
||||
link.download = `${slipDownloadBaseName()}.png`
|
||||
link.href = canvas.toDataURL('image/png')
|
||||
link.click()
|
||||
feedback.msgSuccess('图片已下载')
|
||||
} catch {
|
||||
feedback.msgError('下载图片失败')
|
||||
} finally {
|
||||
prescriptionSlipExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadPrescriptionSlipPdf() {
|
||||
if (!prescriptionViewData.value) return
|
||||
prescriptionSlipExporting.value = true
|
||||
try {
|
||||
const canvas = await capturePrescriptionSlipCanvas()
|
||||
const imgData = canvas.toDataURL('image/png')
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a5'
|
||||
})
|
||||
const pageW = pdf.internal.pageSize.getWidth()
|
||||
const pageH = pdf.internal.pageSize.getHeight()
|
||||
const scale = Math.min(pageW / canvas.width, pageH / canvas.height)
|
||||
const imgW = canvas.width * scale
|
||||
const imgH = canvas.height * scale
|
||||
pdf.addImage(imgData, 'PNG', 0, 0, imgW, imgH)
|
||||
pdf.save(`${slipDownloadBaseName()}.pdf`)
|
||||
feedback.msgSuccess('PDF已下载')
|
||||
} catch {
|
||||
feedback.msgError('下载PDF失败')
|
||||
} finally {
|
||||
prescriptionSlipExporting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user