更新
This commit is contained in:
+38
-36
@@ -4,48 +4,50 @@ import useAppStore from '@/stores/modules/app'
|
|||||||
import { getToken } from '@/utils/auth'
|
import { getToken } from '@/utils/auth'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export type MaterialUploadType = 'image' | 'video' | 'file'
|
||||||
|
|
||||||
|
/** 本地上传素材(与 `components/upload` 相同:POST /upload/{type},字段 file + cid) */
|
||||||
|
export async function uploadMaterialFile(
|
||||||
|
file: File,
|
||||||
|
type: MaterialUploadType = 'image',
|
||||||
|
cid: string | number = 0
|
||||||
|
) {
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('cid', String(cid))
|
||||||
|
const url = `${config.baseUrl}${config.urlPrefix}/upload/${type}`
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
token: getToken() || '',
|
||||||
|
version: appStore.config.version || ''
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code !== RequestCodeEnum.SUCCESS) {
|
||||||
|
throw new Error(json.msg || '上传失败')
|
||||||
|
}
|
||||||
|
return json.data as { id: number; uri: string; url: string }
|
||||||
|
}
|
||||||
|
|
||||||
/** 本地上传图片(与素材库「本地上传」同一接口),返回 data 含 url 相对路径 */
|
/** 本地上传图片(与素材库「本地上传」同一接口),返回 data 含 url 相对路径 */
|
||||||
export async function uploadImageBlob(file: Blob, filename = 'screenshot.jpg') {
|
export async function uploadImageBlob(file: Blob, filename = 'screenshot.jpg') {
|
||||||
const appStore = useAppStore()
|
const fileObj =
|
||||||
const formData = new FormData()
|
file instanceof File
|
||||||
formData.append('file', file, filename)
|
? file
|
||||||
formData.append('cid', '0')
|
: new File([file], filename, { type: file.type || 'image/jpeg' })
|
||||||
const url = `${config.baseUrl}${config.urlPrefix}/upload/image`
|
return uploadMaterialFile(fileObj, 'image', 0)
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
token: getToken() || '',
|
|
||||||
version: appStore.config.version || ''
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
const json = await res.json()
|
|
||||||
if (json.code !== RequestCodeEnum.SUCCESS) {
|
|
||||||
throw new Error(json.msg || '上传失败')
|
|
||||||
}
|
|
||||||
return json.data as { id: number; uri: string; url: string }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 本地上传视频(管理端 /upload/video),返回 data 含 uri 完整访问地址、url 相对路径 */
|
/** 本地上传视频(管理端 /upload/video),返回 data 含 uri 完整访问地址、url 相对路径 */
|
||||||
export async function uploadVideoBlob(file: Blob, filename = 'recording.webm') {
|
export async function uploadVideoBlob(file: Blob, filename = 'recording.webm') {
|
||||||
const appStore = useAppStore()
|
const fileObj =
|
||||||
const formData = new FormData()
|
file instanceof File
|
||||||
formData.append('file', file, filename)
|
? file
|
||||||
formData.append('cid', '0')
|
: new File([file], filename, { type: file.type || 'video/webm' })
|
||||||
const url = `${config.baseUrl}${config.urlPrefix}/upload/video`
|
return uploadMaterialFile(fileObj, 'video', 0)
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
token: getToken() || '',
|
|
||||||
version: appStore.config.version || ''
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
const json = await res.json()
|
|
||||||
if (json.code !== RequestCodeEnum.SUCCESS) {
|
|
||||||
throw new Error(json.msg || '上传失败')
|
|
||||||
}
|
|
||||||
return json.data as { id: number; uri: string; url: string }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fileCateAdd(params: Record<string, any>) {
|
export function fileCateAdd(params: Record<string, any>) {
|
||||||
|
|||||||
@@ -9,7 +9,17 @@
|
|||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<template v-if="!hiddenUpload" #trigger>
|
<template v-if="!hiddenUpload" #trigger>
|
||||||
<div class="material-select__trigger clearfix" @click.stop>
|
<div
|
||||||
|
class="material-select__trigger clearfix"
|
||||||
|
:class="{ 'is-drop-hover': dropActive }"
|
||||||
|
tabindex="0"
|
||||||
|
@click.stop
|
||||||
|
@dragenter.prevent.stop="onTriggerDragEnter"
|
||||||
|
@dragleave.prevent.stop="onTriggerDragLeave"
|
||||||
|
@dragover.prevent.stop="onTriggerDragOver"
|
||||||
|
@drop.prevent.stop="onTriggerDrop"
|
||||||
|
@paste="onTriggerPaste"
|
||||||
|
>
|
||||||
<draggable class="draggable" v-model="fileList" animation="300" item-key="id">
|
<draggable class="draggable" v-model="fileList" animation="300" item-key="id">
|
||||||
<template v-slot:item="{ element, index }">
|
<template v-slot:item="{ element, index }">
|
||||||
<div
|
<div
|
||||||
@@ -82,8 +92,10 @@
|
|||||||
import { useThrottleFn } from '@vueuse/core'
|
import { useThrottleFn } from '@vueuse/core'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
|
|
||||||
|
import { uploadMaterialFile } from '@/api/file'
|
||||||
import Popup from '@/components/popup/index.vue'
|
import Popup from '@/components/popup/index.vue'
|
||||||
import useAppStore from '@/stores/modules/app'
|
import useAppStore from '@/stores/modules/app'
|
||||||
|
import feedback from '@/utils/feedback'
|
||||||
|
|
||||||
import FileItem from './file.vue'
|
import FileItem from './file.vue'
|
||||||
import Material from './index.vue'
|
import Material from './index.vue'
|
||||||
@@ -149,6 +161,11 @@ export default defineComponent({
|
|||||||
excludeDomain: {
|
excludeDomain: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
/** 拖拽/粘贴直传时的素材分组 id(与弹窗内「本地上传」一致,默认未分组) */
|
||||||
|
uploadCid: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -162,8 +179,149 @@ export default defineComponent({
|
|||||||
const select = ref<any[]>([])
|
const select = ref<any[]>([])
|
||||||
const isAdd = ref(true)
|
const isAdd = ref(true)
|
||||||
const currentIndex = ref(-1)
|
const currentIndex = ref(-1)
|
||||||
|
const dragDepth = ref(0)
|
||||||
|
const externalUploading = ref(false)
|
||||||
const { disabled, limit, modelValue } = toRefs(props)
|
const { disabled, limit, modelValue } = toRefs(props)
|
||||||
const { getImageUrl } = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const getImageUrl = (url: string) => appStore.getImageUrl(url)
|
||||||
|
|
||||||
|
const allowDropPaste = computed(
|
||||||
|
() =>
|
||||||
|
!props.hiddenUpload &&
|
||||||
|
(props.type === 'image' || props.type === 'video' || props.type === 'file')
|
||||||
|
)
|
||||||
|
|
||||||
|
const dropActive = computed(() => dragDepth.value > 0)
|
||||||
|
|
||||||
|
function hasFileDrag(dt: DataTransfer | null) {
|
||||||
|
if (!dt) return false
|
||||||
|
const types = Array.from(dt.types as unknown as string[])
|
||||||
|
return types.includes('Files') || types.includes('application/x-moz-file')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAcceptedExtension(fileName: string, uploadType: string): boolean {
|
||||||
|
if (uploadType === 'file') return true
|
||||||
|
const ext = fileName.includes('.')
|
||||||
|
? fileName.split('.').pop()?.toLowerCase() || ''
|
||||||
|
: ''
|
||||||
|
if (uploadType === 'image') {
|
||||||
|
if (!ext) return true
|
||||||
|
return ['jpg', 'jpeg', 'png', 'gif', 'ico'].includes(ext)
|
||||||
|
}
|
||||||
|
if (uploadType === 'video') {
|
||||||
|
if (!ext) return true
|
||||||
|
return ['wmv', 'avi', 'mpg', 'mpeg', '3gp', 'mov', 'mp4', 'flv', 'rmvb', 'mkv'].includes(
|
||||||
|
ext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRelativeStoredUrl(fullUrl: string): string {
|
||||||
|
if (!fullUrl || !fullUrl.startsWith('http')) return fullUrl
|
||||||
|
const oss = String(appStore.config?.oss_domain || '')
|
||||||
|
.trim()
|
||||||
|
.replace(/\/$/, '')
|
||||||
|
if (oss && fullUrl.startsWith(oss)) {
|
||||||
|
return fullUrl.slice(oss.length).replace(/^\//, '')
|
||||||
|
}
|
||||||
|
const origin =
|
||||||
|
typeof window !== 'undefined' ? window.location.origin.replace(/\/$/, '') : ''
|
||||||
|
if (origin && fullUrl.startsWith(origin)) {
|
||||||
|
return fullUrl.slice(origin.length).replace(/^\//, '')
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new URL(fullUrl).pathname.replace(/^\//, '')
|
||||||
|
} catch {
|
||||||
|
return fullUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clipboardFiles(e: ClipboardEvent): File[] {
|
||||||
|
const uploadType = props.type
|
||||||
|
const fromFiles = Array.from(e.clipboardData?.files || []).filter((f) =>
|
||||||
|
isAcceptedExtension(f.name, uploadType)
|
||||||
|
)
|
||||||
|
if (fromFiles.length) return fromFiles
|
||||||
|
if (uploadType !== 'image') return []
|
||||||
|
const items = e.clipboardData?.items
|
||||||
|
if (!items) return []
|
||||||
|
const out: File[] = []
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const it = items[i]
|
||||||
|
if (it.kind === 'file' && it.type.startsWith('image/')) {
|
||||||
|
const f = it.getAsFile()
|
||||||
|
if (f) out.push(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runExternalUpload(files: File[]) {
|
||||||
|
if (externalUploading.value) return
|
||||||
|
const maxSlots =
|
||||||
|
limit.value === -1 ? Infinity : Math.max(0, limit.value - fileList.value.length)
|
||||||
|
if (!maxSlots) {
|
||||||
|
feedback.msgWarning('已达到上传数量上限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const queue = files.slice(0, maxSlots)
|
||||||
|
if (!queue.length) return
|
||||||
|
|
||||||
|
const uploadType =
|
||||||
|
props.type === 'video' ? 'video' : props.type === 'file' ? 'file' : 'image'
|
||||||
|
externalUploading.value = true
|
||||||
|
try {
|
||||||
|
const paths: string[] = []
|
||||||
|
for (const file of queue) {
|
||||||
|
const data = await uploadMaterialFile(file, uploadType, props.uploadCid)
|
||||||
|
paths.push(props.excludeDomain ? toRelativeStoredUrl(data.url) : data.url)
|
||||||
|
}
|
||||||
|
fileList.value = [...fileList.value, ...paths]
|
||||||
|
const valueImg = limit.value != 1 ? fileList.value : fileList.value[0] || ''
|
||||||
|
emit('update:modelValue', valueImg)
|
||||||
|
emit('change', valueImg)
|
||||||
|
handleClose()
|
||||||
|
} catch (err: any) {
|
||||||
|
feedback.msgError(err?.message || '上传失败')
|
||||||
|
} finally {
|
||||||
|
externalUploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTriggerDragEnter = (e: DragEvent) => {
|
||||||
|
if (!allowDropPaste.value || disabled.value) return
|
||||||
|
if (!hasFileDrag(e.dataTransfer)) return
|
||||||
|
dragDepth.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTriggerDragLeave = () => {
|
||||||
|
if (!allowDropPaste.value) return
|
||||||
|
dragDepth.value = Math.max(0, dragDepth.value - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTriggerDragOver = (e: DragEvent) => {
|
||||||
|
if (!allowDropPaste.value || disabled.value) return
|
||||||
|
if (hasFileDrag(e.dataTransfer)) e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTriggerDrop = async (e: DragEvent) => {
|
||||||
|
dragDepth.value = 0
|
||||||
|
if (!allowDropPaste.value || disabled.value) return
|
||||||
|
const files = Array.from(e.dataTransfer?.files || []).filter((f) =>
|
||||||
|
isAcceptedExtension(f.name, props.type)
|
||||||
|
)
|
||||||
|
if (!files.length) return
|
||||||
|
await runExternalUpload(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTriggerPaste = async (e: ClipboardEvent) => {
|
||||||
|
if (!allowDropPaste.value || disabled.value) return
|
||||||
|
const files = clipboardFiles(e)
|
||||||
|
if (!files.length) return
|
||||||
|
e.preventDefault()
|
||||||
|
await runExternalUpload(files)
|
||||||
|
}
|
||||||
const tipsText = computed(() => {
|
const tipsText = computed(() => {
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
case 'image':
|
case 'image':
|
||||||
@@ -264,7 +422,13 @@ export default defineComponent({
|
|||||||
showPreview,
|
showPreview,
|
||||||
handlePreview,
|
handlePreview,
|
||||||
handleClose,
|
handleClose,
|
||||||
getImageUrl
|
getImageUrl,
|
||||||
|
dropActive,
|
||||||
|
onTriggerDragEnter,
|
||||||
|
onTriggerDragLeave,
|
||||||
|
onTriggerDragOver,
|
||||||
|
onTriggerDrop,
|
||||||
|
onTriggerPaste
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -272,6 +436,13 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.material-select {
|
.material-select {
|
||||||
|
.material-select__trigger {
|
||||||
|
outline: none;
|
||||||
|
&.is-drop-hover {
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 0 0 2px var(--el-color-primary-light-5);
|
||||||
|
}
|
||||||
|
}
|
||||||
.material-upload,
|
.material-upload,
|
||||||
.material-preview {
|
.material-preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -138,7 +138,7 @@ class DiagnosisLogic extends BaseLogic
|
|||||||
private static function generatePatientId(): int
|
private static function generatePatientId(): int
|
||||||
{
|
{
|
||||||
// 获取当前最大的患者ID
|
// 获取当前最大的患者ID
|
||||||
$maxPatientId = Diagnosis::max('patient_id') ?? 100000;
|
$maxPatientId = Diagnosis::max('id') ?? 10000000;
|
||||||
|
|
||||||
// 返回下一个患者ID
|
// 返回下一个患者ID
|
||||||
return $maxPatientId + 1;
|
return $maxPatientId + 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user