提交 fb449c89 authored 作者: lidongxu's avatar lidongxu

Merge branch 'release' into dev

...@@ -7,5 +7,6 @@ export {} ...@@ -7,5 +7,6 @@ export {}
declare global { declare global {
const showConfirmDialog: typeof import('vant/es')['showConfirmDialog'] const showConfirmDialog: typeof import('vant/es')['showConfirmDialog']
const showDialog: typeof import('vant/es')['showDialog'] const showDialog: typeof import('vant/es')['showDialog']
const showImagePreview: typeof import('vant/es')['showImagePreview']
const showNotify: typeof import('vant/es')['showNotify'] const showNotify: typeof import('vant/es')['showNotify']
} }
...@@ -73,6 +73,7 @@ declare module 'vue' { ...@@ -73,6 +73,7 @@ declare module 'vue' {
VanUploader: typeof import('vant/es')['Uploader'] VanUploader: typeof import('vant/es')['Uploader']
VersionNotice: typeof import('./src/components/VersionNotice/index.vue')['default'] VersionNotice: typeof import('./src/components/VersionNotice/index.vue')['default']
Week: typeof import('./src/components/Crontab/week.vue')['default'] Week: typeof import('./src/components/Crontab/week.vue')['default']
XLMobileUpload: typeof import('./src/components/XLMobileUpload/index.vue')['default']
XLSelect: typeof import('./src/components/XLSelect/index.vue')['default'] XLSelect: typeof import('./src/components/XLSelect/index.vue')['default']
XLToolTip: typeof import('./src/components/XLToolTip/index.vue')['default'] XLToolTip: typeof import('./src/components/XLToolTip/index.vue')['default']
Year: typeof import('./src/components/Crontab/year.vue')['default'] Year: typeof import('./src/components/Crontab/year.vue')['default']
......
...@@ -8,3 +8,4 @@ export const isMobile = () => { ...@@ -8,3 +8,4 @@ export const isMobile = () => {
} }
return false; return false;
} }
// 获取文件扩展名
export function getFileTypeExt(url) { export function getFileTypeExt(url) {
const fileName = url.split('/').pop() const fileName = url.split('/').pop()
const fileType = fileName.split('.').pop() const fileType = fileName.split('.').pop()
...@@ -6,14 +7,12 @@ export function getFileTypeExt(url) { ...@@ -6,14 +7,12 @@ export function getFileTypeExt(url) {
// 判断 URL 是一个图片还是视频 // 判断 URL 是一个图片还是视频
function isImage(url) { function isImage(url) {
console.log(url, '1')
const fileType = getFileTypeExt(url) const fileType = getFileTypeExt(url)
return ['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(fileType.toLowerCase()) return ['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(fileType.toLowerCase())
} }
// 判断 URL 是一个视频 // 判断 URL 是一个视频
function isVideo(url) { function isVideo(url) {
const fileType = getFileTypeExt(url) const fileType = getFileTypeExt(url)
console.log(['mp4', 'avi', 'mov', 'wmv', 'flv', '3gp', 'mkv', 'webm'].includes(fileType.toLowerCase()), 2)
return ['mp4', 'avi', 'mov', 'wmv', 'flv', '3gp', 'mkv', 'webm'].includes(fileType.toLowerCase()) return ['mp4', 'avi', 'mov', 'wmv', 'flv', '3gp', 'mkv', 'webm'].includes(fileType.toLowerCase())
} }
// 来个统一出口 // 来个统一出口
......
<!-- 移动端上传图片/视频组件 -->
<template>
<div class="xl-mobile-upload">
<!-- 苹果设备上传组件 -->
<van-uploader :max-count="maxCount"
:accept="supportedTypes"
:model-value="photoList"
:after-read="commitStorePhotosRead"
preview-size="2.13333rem"
@delete="deleteCommitStorePhotos"
:max-size="bigSize"
@oversize="onOversize">
<!-- 自定义上传按钮 -->
<template #default>
<div class="upload-btn">
<van-icon name="photograph" />
</div>
</template>
<!-- 自定义预览内容 -->
<template #preview-cover="{ url, index }">
<div class="preview-container"
@click.stop>
<!-- 图片预览 -->
<van-image v-if="getMediaType(url) === 'image'"
:src="url"
alt=""
class="preview-media"
fit="cover"
@click="previewImage(url)" />
<!-- 视频预览 -->
<div class="video-wrap"
v-else-if="getMediaType(url) === 'video'">
<video :src="url"
alt=""
ref="videoRef"
muted
autoplay
playsinline
preload="auto"
class="preview-media"
@loadeddata="handleVideoLoaded" />
<div class="play-hint"
@click.stop="showVideoFullscreen(url)">
</div>
</div>
<!-- 删除按钮 -->
<van-icon name="clear"
class="delete-icon" />
<!-- 类型标识 -->
<span v-if="getMediaType(url) === 'video'"
class="video-tag">
视频
</span>
</div>
</template>
</van-uploader>
<!-- 拍照方式选择 -->
<van-action-sheet v-model:show="show"
:actions="actions"
@select="onSelect" />
</div>
</template>
<script setup>
import { showImagePreview } from 'vant'
import { getMobileType } from '@/views/mobile/utils'
import { getMediaType } from '@/utils'
const emits = defineEmits(['confirm', 'delete'])
const props = defineProps({
// 最多支持上传数量
maxCount: {
type: Number,
default: 1
},
// 支持视频上传
supportVideo: {
type: Boolean,
default: false
},
// 文件组回显
photoList: { // 回显
type: Array,
default: () => []
},
// 多张照片,本照片索引
name: {
type: [String, Number],
default: ''
}
})
/**************** 工具函数 ***************/
// 计算支持的文件类型
const supportedTypes = computed(() => {
if (props.supportVideo) {
// 判断安卓还是苹果设备返回不同的字符串
if (getMobileType() === 'ios') {
return 'image/*,video/*'
} else {
if (selectedMediaType.value === 'image') {
return 'image/*'
} else {
return 'video/*'
}
}
}
return 'image/*'
})
/*************** android 拍照/录像方式确认 ***************/
// 显示菜单
const show = ref(false)
// 拍照/录像方式列表
const actions = ref([
{
name: '拍照片',
type: 'image'
},
{
name: '拍视频',
type: 'video'
}
])
// 安卓:拍照/录像方式确认
const onSelect = (item) => {
selectedMediaType.value = item.type
nextTick(() => {
if (uploaderRef.value) {
// 获取uploader组件实例
const uploaderInstance = uploaderRef.value
// 方法1: 如果组件暴露了open或click方法,则模拟一次点击上传
if (uploaderInstance.open) {
uploaderInstance.open()
} else if (uploaderInstance.click) {
uploaderInstance.click()
} else {
// 方法2: 直接查找内部的input元素并触发点击
// 查找uploader组件内的input[type="file"]元素
const inputElement = uploaderInstance.$el.querySelector('input[type="file"]')
if (inputElement) {
// 触发点击事件
inputElement.click()
} else {
console.error('未找到文件上传input元素')
}
}
} else {
console.error('uploader组件未加载完成')
}
// 隐藏底部选择菜单
show.value = false
})
}
// 上传组件和选择类型
const uploaderRef = ref(null)
const selectedMediaType = ref('')
/*************** 上传照片 ***************/
// 上传照片
const commitStorePhotosRead = async (file) => {
emits('confirm', file, { name: props.name })
}
// 删除照片
const deleteCommitStorePhotos = async (file, { name, index }) => {
emits('delete', '', { name: props.name, index })
}
// 文件大小限制
const bigSize = 5 * 1024 * 1024
const onOversize = (file) => {
showNotify({ type: 'danger', message: '文件大小不能超过 5 MB' })
}
/*************** 图片/视频预览 ***************/
// 图片预览
const previewImage = (url) => {
showImagePreview({
images: [url],
})
}
// 视频相关状态
const videoRef = ref(null)
const isPlaying = ref(true)
// 加载完第一帧暂停
const handleVideoLoaded = () => {
isPlaying.value = false
videoRef.value.pause()
}
// 全屏播放
const showVideoFullscreen = async (url) => {
const video = videoRef.value;
if (!video) return;
try {
// 先尝试播放视频
await video.play();
// 然后尝试全屏(跨浏览器兼容)
if (!document.fullscreenElement) {
await requestFullscreen(video).catch(err => {
console.error('全屏请求失败,但不影响播放:', err);
});
}
} catch (err) {
console.error('视频操作失败:', err);
}
};
// 跨浏览器请求全屏(保留之前的实现)
const requestFullscreen = (element) => {
if (!element) return Promise.reject(new Error('video 标签不存在'));
const methods = [
// 标准API
() => element.requestFullscreen(),
// WebKit (Safari, Chrome)
() => element.webkitRequestFullscreen(),
// iOS Safari特定API
() => {
if (element.webkitEnterFullscreen) {
element.webkitEnterFullscreen();
return Promise.resolve();
}
return Promise.reject(new Error('不支持全屏播放'));
}
];
// 尝试各种方法
return new Promise((resolve, reject) => {
for (const method of methods) {
try {
const result = method();
if (result && result.then) {
result.then(resolve).catch(() => { });
} else {
resolve();
return;
}
} catch (err) {
// 尝试下一个方法
continue;
}
}
reject(new Error('所有全屏播放方法失败'));
});
};
</script>
<style scoped
lang="scss">
.xl-mobile-upload {
.van-uploader {
::v-deep(.van-uploader__wrapper) {
gap: 10px;
/* 自定义上传区域样式 */
.upload-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f8f8;
width: 80px;
height: 80px;
border: 1px dashed #ccc;
border-radius: 4px;
color: #dcdee0;
font-size: 24px;
}
}
}
/* 预览区域 */
::v-deep(.van-uploader__preview) {
margin: 0;
.preview-container {
position: relative;
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
.video-wrap {
width: 100%;
height: 100%;
.play-hint {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
}
.preview-media {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-tag {
position: absolute;
bottom: 4px;
right: 4px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
}
.delete-icon {
position: absolute;
top: -6px;
right: -6px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
}
/* 原来上传预览内容样式 */
::v-deep(.van-uploader__file) {
.van-badge__wrapper {
display: none;
}
.van-ellipsis {
display: none;
}
}
}
</style>
\ No newline at end of file
...@@ -4,6 +4,8 @@ import { isMobile } from '@/utils' ...@@ -4,6 +4,8 @@ import { isMobile } from '@/utils'
import PickerSearch from './components/PickerSearch' import PickerSearch from './components/PickerSearch'
// 选择日期 // 选择日期
import PickerCalendar from './components/PickerCalendar' import PickerCalendar from './components/PickerCalendar'
// 上传照片和视频
import XLMobileUpload from './components/XLMobileUpload'
// 指令 // 指令
import longPress from './directive/touch' import longPress from './directive/touch'
// 覆盖样式 // 覆盖样式
...@@ -31,4 +33,6 @@ export default function (app) { ...@@ -31,4 +33,6 @@ export default function (app) {
app.component('PickerSearch', PickerSearch); app.component('PickerSearch', PickerSearch);
app.component('PickerCalendar', PickerCalendar); app.component('PickerCalendar', PickerCalendar);
app.directive('longPress', longPress) app.directive('longPress', longPress)
// 上传照片和视频
app.component('XLMobileUpload', XLMobileUpload)
} }
\ No newline at end of file
...@@ -25,19 +25,16 @@ ...@@ -25,19 +25,16 @@
auto-complete="off" auto-complete="off"
:rules="[{ required: true, message: '请输入费用' }]" :rules="[{ required: true, message: '请输入费用' }]"
@change="handleCostChange(index)" /> @change="handleCostChange(index)" />
<van-field label="常规陈列照片" <van-field label="常规陈列照片/视频"
label-align="top" label-align="top"
class="header-photo-section"> class="header-photo-section">
<template #input> <template #input>
<van-uploader :max-count="2" <XLMobileUpload :max-count="5"
accept="image/*" :photoList="obj.photoArr"
capture="camera"
:model-value="obj.photoArr"
:name="index" :name="index"
:after-read="displayPhotosRead" :supportVideo="true"
preview-size="2.13333rem" @confirm="displayPhotosRead"
@delete="deletedisplayPhotos"> @delete="deletedisplayPhotos" />
</van-uploader>
</template> </template>
</van-field> </van-field>
<van-field label="核查结果" <van-field label="核查结果"
...@@ -80,6 +77,7 @@ ...@@ -80,6 +77,7 @@
import { uploadFileToOSSAPI, createInspectionTaskAPI, createInspectionTaskDetailAPI, deleteInspectionTaskAPI } from '@/api' import { uploadFileToOSSAPI, createInspectionTaskAPI, createInspectionTaskDetailAPI, deleteInspectionTaskAPI } from '@/api'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { getFileTypeExt } from '@/utils'
const route = useRoute() const route = useRoute()
const props = defineProps({ const props = defineProps({
...@@ -184,7 +182,7 @@ const handleCostChange = async (index) => { ...@@ -184,7 +182,7 @@ const handleCostChange = async (index) => {
showNotify({ type: 'success', message: '费用,保存成功' }) showNotify({ type: 'success', message: '费用,保存成功' })
} }
// 上传照片 // 上传照片
const displayPhotosRead = async (file, { name, index }) => { const displayPhotosRead = async (file, { name }) => {
// name:是当前照片组件所在陈列组的索引 // name:是当前照片组件所在陈列组的索引
// index: 当前照片组的索引 // index: 当前照片组的索引
const date = new Date() const date = new Date()
...@@ -196,7 +194,7 @@ const displayPhotosRead = async (file, { name, index }) => { ...@@ -196,7 +194,7 @@ const displayPhotosRead = async (file, { name, index }) => {
status: 'uploading', status: 'uploading',
message: '上传中...' message: '上传中...'
} }
const pictureUrl = await uploadFileToOSSAPI(`risk/${date.getFullYear()}-${month}/displayPhoto/${useUserStore().empInfo.empNo}/${props.form.storeCode}/${uuidv4()}.png`, file.file) const pictureUrl = await uploadFileToOSSAPI(`risk/${date.getFullYear()}-${month}/displayPhoto/${useUserStore().empInfo.empNo}/${props.form.storeCode}/${uuidv4()}.${getFileTypeExt(file.file.name)}`, file.file)
target.photoArr[photoIndex] = { target.photoArr[photoIndex] = {
url: pictureUrl, url: pictureUrl,
status: 'done' status: 'done'
...@@ -210,7 +208,7 @@ const displayPhotosRead = async (file, { name, index }) => { ...@@ -210,7 +208,7 @@ const displayPhotosRead = async (file, { name, index }) => {
} }
}) })
showNotify({ type: 'success', message: '常规陈列照片,上传成功' }) showNotify({ type: 'success', message: '常规陈列照片/视频,上传成功' })
} }
// 删除照片 // 删除照片
const deletedisplayPhotos = async (file, { name, index }) => { const deletedisplayPhotos = async (file, { name, index }) => {
...@@ -223,7 +221,7 @@ const deletedisplayPhotos = async (file, { name, index }) => { ...@@ -223,7 +221,7 @@ const deletedisplayPhotos = async (file, { name, index }) => {
photoArr: target.photoArr.map(o => o.url) photoArr: target.photoArr.map(o => o.url)
} }
}) })
showNotify({ type: 'success', message: '常规陈列照片,删除成功' }) showNotify({ type: 'success', message: '常规陈列照片/视频,删除成功' })
} }
// 核查结果 // 核查结果
const handleVerifyChange = async (index) => { const handleVerifyChange = async (index) => {
......
...@@ -24,19 +24,16 @@ ...@@ -24,19 +24,16 @@
auto-complete="off" auto-complete="off"
:rules="[{ required: true, message: '请输入执行情况' }]" :rules="[{ required: true, message: '请输入执行情况' }]"
@change="handleCostChange(index)" /> @change="handleCostChange(index)" />
<van-field label="档期补差照片" <van-field label="档期补差照片/视频"
label-align="top" label-align="top"
class="header-photo-section"> class="header-photo-section">
<template #input> <template #input>
<van-uploader :max-count="3" <XLMobileUpload :max-count="5"
accept="image/*" :photoList="obj.photoArr"
capture="camera"
:model-value="obj.photoArr"
:name="index" :name="index"
:after-read="displayPhotosRead" :supportVideo="true"
preview-size="2.13333rem" @confirm="displayPhotosRead"
@delete="deletedisplayPhotos"> @delete="deletedisplayPhotos" />
</van-uploader>
</template> </template>
</van-field> </van-field>
<van-field label="补差核查结果" <van-field label="补差核查结果"
...@@ -79,6 +76,7 @@ ...@@ -79,6 +76,7 @@
import { uploadFileToOSSAPI, createInspectionTaskAPI, createInspectionTaskDetailAPI, deleteInspectionTaskAPI } from '@/api' import { uploadFileToOSSAPI, createInspectionTaskAPI, createInspectionTaskDetailAPI, deleteInspectionTaskAPI } from '@/api'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { getFileTypeExt } from '@/utils'
const route = useRoute() const route = useRoute()
const props = defineProps({ const props = defineProps({
...@@ -194,7 +192,7 @@ const displayPhotosRead = async (file, { name, index }) => { ...@@ -194,7 +192,7 @@ const displayPhotosRead = async (file, { name, index }) => {
status: 'uploading', status: 'uploading',
message: '上传中...' message: '上传中...'
} }
const pictureUrl = await uploadFileToOSSAPI(`risk/${date.getFullYear()}-${month}/scheduleAdjustmentPhoto/${useUserStore().empInfo.empNo}/${props.form.storeCode}/${uuidv4()}.png`, file.file) const pictureUrl = await uploadFileToOSSAPI(`risk/${date.getFullYear()}-${month}/scheduleAdjustmentPhoto/${useUserStore().empInfo.empNo}/${props.form.storeCode}/${uuidv4()}.${getFileTypeExt(file.file.name)}`, file.file)
target.photoArr[targetIndex] = { target.photoArr[targetIndex] = {
url: pictureUrl, url: pictureUrl,
status: 'done' status: 'done'
...@@ -208,7 +206,7 @@ const displayPhotosRead = async (file, { name, index }) => { ...@@ -208,7 +206,7 @@ const displayPhotosRead = async (file, { name, index }) => {
} }
}) })
showNotify({ type: 'success', message: '档期补差照片,上传成功' }) showNotify({ type: 'success', message: '档期补差照片/视频,上传成功' })
} }
// 删除照片 // 删除照片
const deletedisplayPhotos = async (file, { name, index }) => { const deletedisplayPhotos = async (file, { name, index }) => {
...@@ -221,7 +219,7 @@ const deletedisplayPhotos = async (file, { name, index }) => { ...@@ -221,7 +219,7 @@ const deletedisplayPhotos = async (file, { name, index }) => {
photoArr: target.photoArr.map(o => o.url) photoArr: target.photoArr.map(o => o.url)
} }
}) })
showNotify({ type: 'success', message: '档期补差照片,删除成功' }) showNotify({ type: 'success', message: '档期补差照片/视频,删除成功' })
} }
// 核查结果 // 核查结果
const handleVerifyChange = async (index) => { const handleVerifyChange = async (index) => {
......
...@@ -24,19 +24,16 @@ ...@@ -24,19 +24,16 @@
auto-complete="off" auto-complete="off"
:rules="[{ required: true, message: '请输入费用' }]" :rules="[{ required: true, message: '请输入费用' }]"
@change="handleCostChange(index)" /> @change="handleCostChange(index)" />
<van-field label="档期陈列照片" <van-field label="档期陈列照片/视频"
label-align="top" label-align="top"
class="header-photo-section"> class="header-photo-section">
<template #input> <template #input>
<van-uploader :max-count="2" <XLMobileUpload :max-count="5"
accept="image/*" :photoList="obj.photoArr"
capture="camera"
:model-value="obj.photoArr"
:name="index" :name="index"
:after-read="displayPhotosRead" :supportVideo="true"
preview-size="2.13333rem" @confirm="displayPhotosRead"
@delete="deletedisplayPhotos"> @delete="deletedisplayPhotos" />
</van-uploader>
</template> </template>
</van-field> </van-field>
<van-field label="核查结果" <van-field label="核查结果"
...@@ -79,6 +76,7 @@ ...@@ -79,6 +76,7 @@
import { uploadFileToOSSAPI, createInspectionTaskAPI, createInspectionTaskDetailAPI, deleteInspectionTaskAPI } from '@/api' import { uploadFileToOSSAPI, createInspectionTaskAPI, createInspectionTaskDetailAPI, deleteInspectionTaskAPI } from '@/api'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { getFileTypeExt } from '@/utils'
const route = useRoute() const route = useRoute()
const props = defineProps({ const props = defineProps({
...@@ -194,7 +192,7 @@ const displayPhotosRead = async (file, { name, index }) => { ...@@ -194,7 +192,7 @@ const displayPhotosRead = async (file, { name, index }) => {
status: 'uploading', status: 'uploading',
message: '上传中...' message: '上传中...'
} }
const pictureUrl = await uploadFileToOSSAPI(`risk/${date.getFullYear()}-${month}/scheduleDisplayPhoto/${useUserStore().empInfo.empNo}/${props.form.storeCode}/${uuidv4()}.png`, file.file) const pictureUrl = await uploadFileToOSSAPI(`risk/${date.getFullYear()}-${month}/scheduleDisplayPhoto/${useUserStore().empInfo.empNo}/${props.form.storeCode}/${uuidv4()}.${getFileTypeExt(file.file.name)}`, file.file)
target.photoArr[targetIndex] = { target.photoArr[targetIndex] = {
url: pictureUrl, url: pictureUrl,
status: 'done' status: 'done'
...@@ -208,7 +206,7 @@ const displayPhotosRead = async (file, { name, index }) => { ...@@ -208,7 +206,7 @@ const displayPhotosRead = async (file, { name, index }) => {
} }
}) })
showNotify({ type: 'success', message: '档期陈列照片,上传成功' }) showNotify({ type: 'success', message: '档期陈列照片/视频,上传成功' })
} }
// 删除照片 // 删除照片
const deletedisplayPhotos = async (file, { name, index }) => { const deletedisplayPhotos = async (file, { name, index }) => {
...@@ -221,7 +219,7 @@ const deletedisplayPhotos = async (file, { name, index }) => { ...@@ -221,7 +219,7 @@ const deletedisplayPhotos = async (file, { name, index }) => {
photoArr: target.photoArr.map(o => o.url) photoArr: target.photoArr.map(o => o.url)
} }
}) })
showNotify({ type: 'success', message: '档期陈列照片,删除成功' }) showNotify({ type: 'success', message: '档期陈列照片/视频,删除成功' })
} }
// 核查结果 // 核查结果
const handleVerifyChange = async (index) => { const handleVerifyChange = async (index) => {
......
export const getMobileType = () => {
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes('iphone') || userAgent.includes('ipad')) {
return 'ios';
} else if (userAgent.includes('android')) {
return 'android';
}
return 'android';
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论