| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- <template>
- <popup :show="show" round :close-on-click-overlay="false" class="upload-progress-popup"
- @update:show="emit('update:show', $event)">
- <slot name="header"></slot>
- <div class="upload-box">
- <div class="box-header">
- <div class="upload-title">
- <span>{{ t('uploadPopup.uploadPhotos') }}</span>
- <span v-if="!uploadRequired" class="optional">{{ t('uploadPopup.optional') }}</span>
- </div>
- </div>
- <uploader class="popup-uploader" :class="{ 'popup-uploader-identify': enableIdentify }" v-model="fileList"
- multiple :max-count="10" :after-read="afterReadUpload"
- :preview-full-image="!enableIdentify" @delete="onDeleteUpload" @click-preview="openIdentifyPreview">
- <template v-if="enableIdentify" #preview-cover="previewItem">
- <div v-if="previewItem?.identifyStatus" class="identify-preview-cover"
- :class="{ 'is-done': previewItem.identifyStatus === 'done' }">
- <div class="identify-status-bar"
- @click.stop="previewItem.identifyStatus === 'done' && openIdentifyPreview(previewItem)">
- <template v-if="previewItem.identifyStatus === 'identifying'">
- <span class="identify-status-text">{{ t('正在识别中..') }}</span>
- </template>
- <template v-else-if="previewItem.identifyStatus === 'done'">
- <el-icon class="identify-done-icon" :size="14">
- <CircleCheck />
- </el-icon>
- <span class="identify-status-text">{{ t('识别完成') }}</span>
- <el-icon class="identify-arrow-icon" :size="12">
- <ArrowRight />
- </el-icon>
- </template>
- <template v-else-if="previewItem.identifyStatus === 'failed'">
- <span class="identify-status-text">{{ t('识别失败') }}</span>
- </template>
- </div>
- </div>
- </template>
- <img class="plus" src="@/assets/img/home/plus.png" alt="">
- </uploader>
- </div>
- <slot name="footer"></slot>
- <div class="upload-action-btns">
- <div class="cancel-btn" @click="emit('cancel')">{{ t('取消') }}</div>
- <div class="confirm-btn" @click="handleConfirm">{{ confirmText }}</div>
- </div>
- </popup>
- <ImagePreviewPopup v-if="enableIdentify" v-model:show="showIdentifyPreview" :images="identifyPreviewImages">
- <template #footer>
- <div class="identify-result-panel" v-if="previewResult">
- <div class="result-header">
- <span class="accent-bar"></span>
- <span class="result-name">{{ t('病虫名称:') }}{{ getIdentifyField(previewResult, ['disease_name', 'name', 'pest_name']) }}</span>
- </div>
- <div class="result-content">{{ getIdentifyField(previewResult, ['kepu', 'harm', 'harm_desc', 'phenotype', 'phenotypic_characteristics']) }}</div>
- </div>
- </template>
- </ImagePreviewPopup>
- </template>
- <script setup>
- import { useI18n } from "@/i18n";
- const { t } = useI18n();
- import { ref, computed, watch } from 'vue';
- import { Popup, Uploader } from 'vant';
- import { ElMessage } from 'element-plus';
- import { CircleCheck, ArrowRight } from '@element-plus/icons-vue';
- import ImagePreviewPopup from '@/components/popup/ImagePreviewPopup.vue';
- import UploadFile from "@/utils/upliadFile";
- import 'vant/lib/uploader/style';
- import { getFileExt } from "@/utils/util";
- import { base_img_url2 } from '@/api/config';
- const props = defineProps({
- show: {
- type: Boolean,
- default: false,
- },
- initImgArr: {
- type: Array,
- default: () => [],
- },
- confirmText: {
- type: String,
- default: '确认信息',
- },
- /** 是否必传图片,默认必传 */
- uploadRequired: {
- type: Boolean,
- default: true,
- },
- /** 是否开启上传后 AI 病虫识别(物候上传等场景关闭) */
- enableIdentify: {
- type: Boolean,
- default: false,
- },
- });
- const emit = defineEmits(['update:show', 'cancel', 'confirm', 'reset']);
- const fileList = ref([]);
- const popupInnerImgArr = ref([]);
- /** 用户已上传/删除过图片后,以 popupInnerImgArr 为准,不再回退 props.initImgArr */
- const imgTouched = ref(false);
- const uploadFileObj = new UploadFile();
- const miniUserId = localStorage.getItem("MINI_USER_ID");
- const showIdentifyPreview = ref(false);
- const previewImageUrl = ref('');
- const previewResult = ref(null);
- const identifyPreviewImages = computed(() => (
- previewImageUrl.value ? [previewImageUrl.value] : []
- ));
- function getIdentifyField(result, fields) {
- for (const field of fields) {
- const value = result?.[field];
- if (value !== undefined && value !== null && value !== '') {
- return value;
- }
- }
- return '-';
- }
- function getConfirmImgArr() {
- if (imgTouched.value || popupInnerImgArr.value.length) {
- return popupInnerImgArr.value.map((item) => base_img_url2 + item);
- }
- return [...(props.initImgArr || [])];
- }
- function onDeleteUpload(_file, detail) {
- imgTouched.value = true;
- popupInnerImgArr.value.splice(detail.index, 1);
- }
- const afterReadUpload = async (data) => {
- if (!Array.isArray(data)) {
- data = [data];
- }
- for (const file of data) {
- const fileVal = file.file;
- file.status = "uploading";
- file.message = "上传中...";
- const ext = getFileExt(fileVal.name);
- const key = `birdseye-look-mini/${miniUserId}/${new Date().getTime()}.${ext}`;
- const resFilename = await uploadFileObj.put(key, fileVal);
- if (resFilename) {
- file.status = "done";
- file.message = "";
- file.resFilename = resFilename;
- file.url = base_img_url2 + resFilename;
- imgTouched.value = true;
- popupInnerImgArr.value.push(resFilename);
- if (props.enableIdentify) {
- file.identifyStatus = 'identifying';
- identifySingleImage(file, resFilename);
- }
- } else {
- file.status = "failed";
- file.message = "上传失败";
- ElMessage.error("图片上传失败,请稍后再试!");
- }
- }
- };
- function patchFileItem(resFilename, patch) {
- let index = fileList.value.findIndex((item) => item.resFilename === resFilename);
- if (index === -1) {
- index = fileList.value.findIndex((item) => item.url?.includes(resFilename));
- }
- if (index === -1) return;
- Object.assign(fileList.value[index], patch);
- }
- async function identifySingleImage(file, resFilename) {
- let farmData = {};
- try {
- farmData = JSON.parse(localStorage.getItem('selectedFarmData') || '{}');
- } catch {
- farmData = {};
- }
- const params = {
- image_urls: [base_img_url2 + resFilename],
- crop_type: farmData.farm_variety,
- };
- try {
- const res = await VE_API.record.batchPestIdentify(params);
- if (res.code === 200 && res.results?.length) {
- const patch = { identifyStatus: 'done', identifyResult: res.results[0] };
- Object.assign(file, patch);
- patchFileItem(resFilename, patch);
- } else {
- const patch = { identifyStatus: 'failed' };
- Object.assign(file, patch);
- patchFileItem(resFilename, patch);
- ElMessage.error(t('AI识别失败,请稍后再试!'));
- }
- } catch {
- const patch = { identifyStatus: 'failed' };
- Object.assign(file, patch);
- patchFileItem(resFilename, patch);
- ElMessage.error(t('AI识别失败,请稍后再试!'));
- }
- }
- function openIdentifyPreview(file) {
- previewImageUrl.value = file.url || base_img_url2 + (file.resFilename || '');
- previewResult.value = file.identifyResult || null;
- showIdentifyPreview.value = true;
- }
- function handleConfirm() {
- const imgArr = getConfirmImgArr();
- if (props.uploadRequired && !imgArr.length) {
- ElMessage.warning(t('请先上传照片'));
- return;
- }
- emit('confirm', imgArr);
- }
- function uploadReset() {
- fileList.value = [];
- popupInnerImgArr.value = [];
- imgTouched.value = false;
- showIdentifyPreview.value = false;
- previewImageUrl.value = '';
- previewResult.value = null;
- }
- watch(
- () => props.initImgArr,
- (val) => {
- if (imgTouched.value || !val?.length) {
- return;
- }
- fileList.value = val.map((item) => ({
- url: base_img_url2 + item,
- isImage: true,
- resFilename: item,
- }));
- popupInnerImgArr.value = [...val];
- },
- { immediate: true, deep: true },
- );
- watch(
- () => props.show,
- (val) => {
- if (val) {
- uploadReset();
- emit('reset');
- }
- },
- );
- defineExpose({
- uploadReset,
- });
- </script>
- <style scoped lang="scss">
- .upload-progress-popup {
- width: 100%;
- padding: 24px 16px;
- background: linear-gradient(360deg, #FFFFFF 74.2%, #D1EBFF 100%);
- .upload-box {
- margin-bottom: 12px;
- position: relative;
- min-height: 88px;
- .box-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
- .upload-title {
- font-size: 16px;
- .optional {
- font-size: 12px;
- color: rgba(18, 18, 18, 0.2);
- }
- }
- }
- .popup-uploader {
- .plus {
- width: calc((100vw - 68px) / 4);
- height: calc((100vw - 68px) / 4);
- }
- ::v-deep {
- .van-uploader__wrapper {
- --van-uploader-size: 76.7px;
- --van-padding-xs: 6px;
- }
- }
- &.popup-uploader-identify {
- ::v-deep {
- .van-uploader__preview-cover {
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- height: 100%;
- pointer-events: none;
- }
- .van-uploader__preview-delete {
- z-index: 3;
- }
- }
- }
- }
- .identify-preview-cover {
- display: flex;
- flex-direction: column;
- justify-content: flex-end;
- width: 100%;
- height: 100%;
- position: relative;
- }
- .identify-status-bar {
- pointer-events: auto;
- display: flex;
- align-items: center;
- justify-content: flex-start;
- gap: 4px;
- width: 100%;
- padding: 4px 6px;
- box-sizing: border-box;
- background: rgba(0, 0, 0, 0.55);
- color: #fff;
- font-size: 11px;
- text-align: center;
- .identify-status-text {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .identify-done-icon {
- color: #52c41a;
- flex-shrink: 0;
- }
- .identify-arrow-icon {
- flex-shrink: 0;
- margin-left: auto;
- }
- }
- }
- .upload-action-btns {
- display: flex;
- gap: 10px;
- margin-top: 16px;
- .cancel-btn,
- .confirm-btn {
- flex: 1;
- border-radius: 4px;
- padding: 8px;
- text-align: center;
- font-size: 16px;
- }
- .cancel-btn {
- border: 1px solid #dcdfe6;
- color: #606266;
- background: #ffffff;
- }
- .confirm-btn {
- background: #2199f8;
- color: #ffffff;
- }
- }
- }
- .identify-result-panel {
- padding: 8px 10px;
- background: rgba(0, 0, 0, 0.4);
- border-radius: 6px;
- box-sizing: border-box;
- margin: 18px 12px;
- .result-header {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-bottom: 7px;
- .accent-bar {
- width: 4px;
- height: 15px;
- border-radius: 2px;
- background: #FFD786;
- flex-shrink: 0;
- }
- .result-name {
- color: #FFD786;
- font-size: 18px;
- font-weight: 500;
- line-height: 22px;
- }
- }
- .result-content {
- color: #ffffff;
- font-size: 13px;
- }
- }
- </style>
|