|
|
@@ -2,17 +2,41 @@
|
|
|
<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" v-loading="loading" element-loading-:text="t('上传中...')">
|
|
|
+ <div class="upload-box">
|
|
|
<div class="box-header">
|
|
|
<div class="upload-title">
|
|
|
<span>{{ t('上传照片') }}</span>
|
|
|
<span v-if="!uploadRequired" class="optional">{{ t('(可选)') }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <upload ref="uploadRef" :maxCount="10" :initImgArr="props.initImgArr" :enableIdentifyStatus="true"
|
|
|
- :before-read="beforeReadUpload" :after-read="afterReadUpload" @handleUpload="onUploadChange"
|
|
|
- @clickPreview="onClickPreview">
|
|
|
- </upload>
|
|
|
+ <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">
|
|
|
@@ -21,33 +45,29 @@
|
|
|
</div>
|
|
|
</popup>
|
|
|
|
|
|
- <popup v-model:show="showIdentifyPreview" class="identify-result-popup" teleport="body" z-index="3000">
|
|
|
- <div class="identify-preview-wrap">
|
|
|
- <img class="identify-preview-img" :src="previewImageUrl" alt="" />
|
|
|
+ <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-tabs">
|
|
|
- <span v-for="(tab, index) in resultTabs" :key="tab.key"
|
|
|
- class="result-tab" :class="{ active: activeResultTab === index }"
|
|
|
- @click="activeResultTab = index">{{ tab.label }}</span>
|
|
|
- </div>
|
|
|
- <div class="result-content">{{ currentTabContent }}</div>
|
|
|
+ <div class="result-content">{{ getIdentifyField(previewResult, ['kepu', 'harm', 'harm_desc', 'phenotype', 'phenotypic_characteristics']) }}</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- </popup>
|
|
|
+ </template>
|
|
|
+ </ImagePreviewPopup>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { useI18n } from "@/i18n";
|
|
|
const { t } = useI18n();
|
|
|
import { ref, computed, watch } from 'vue';
|
|
|
-import { Popup } from 'vant';
|
|
|
+import { Popup, Uploader } from 'vant';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
-import upload from '@/components/upload.vue';
|
|
|
+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';
|
|
|
|
|
|
@@ -56,10 +76,6 @@ const props = defineProps({
|
|
|
type: Boolean,
|
|
|
default: false,
|
|
|
},
|
|
|
- popupImageUploadLoading: {
|
|
|
- type: Boolean,
|
|
|
- default: false,
|
|
|
- },
|
|
|
initImgArr: {
|
|
|
type: Array,
|
|
|
default: () => [],
|
|
|
@@ -73,37 +89,29 @@ const props = defineProps({
|
|
|
type: Boolean,
|
|
|
default: true,
|
|
|
},
|
|
|
+ /** 是否开启上传后 AI 病虫识别(物候上传等场景关闭) */
|
|
|
+ enableIdentify: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
});
|
|
|
|
|
|
const emit = defineEmits(['update:show', 'cancel', 'confirm', 'reset']);
|
|
|
|
|
|
-const uploadRef = ref(null);
|
|
|
+const fileList = ref([]);
|
|
|
const popupInnerImgArr = ref([]);
|
|
|
/** 用户已上传/删除过图片后,以 popupInnerImgArr 为准,不再回退 props.initImgArr */
|
|
|
const imgTouched = ref(false);
|
|
|
-const popupInnerLoading = ref(false);
|
|
|
const uploadFileObj = new UploadFile();
|
|
|
const miniUserId = localStorage.getItem("MINI_USER_ID");
|
|
|
|
|
|
-const loading = computed(() => props.popupImageUploadLoading || popupInnerLoading.value);
|
|
|
-
|
|
|
const showIdentifyPreview = ref(false);
|
|
|
const previewImageUrl = ref('');
|
|
|
const previewResult = ref(null);
|
|
|
-const activeResultTab = ref(0);
|
|
|
-
|
|
|
-const resultTabs = computed(() => [
|
|
|
- { key: 'name', label: t('病虫的名称'), fields: ['disease_name', 'name', 'pest_name'] },
|
|
|
- { key: 'phenotype', label: t('病虫的表型'), fields: ['phenotype', 'phenotypic_characteristics'] },
|
|
|
- { key: 'harm', label: t('危害'), fields: ['harm', 'damage', 'kepu', 'harm_desc'] },
|
|
|
- { key: 'peak', label: t('高发时期'), fields: ['peak_period', 'high_incidence_period', 'outbreak_period'] },
|
|
|
-]);
|
|
|
-
|
|
|
-const currentTabContent = computed(() => {
|
|
|
- const tab = resultTabs.value[activeResultTab.value];
|
|
|
- if (!tab || !previewResult.value) return '-';
|
|
|
- return getIdentifyField(previewResult.value, tab.fields);
|
|
|
-});
|
|
|
+
|
|
|
+const identifyPreviewImages = computed(() => (
|
|
|
+ previewImageUrl.value ? [previewImageUrl.value] : []
|
|
|
+));
|
|
|
|
|
|
function getIdentifyField(result, fields) {
|
|
|
for (const field of fields) {
|
|
|
@@ -122,51 +130,50 @@ function getConfirmImgArr() {
|
|
|
return [...(props.initImgArr || [])];
|
|
|
}
|
|
|
|
|
|
-/** 与 upload 组件内 imgArr 同步(含删除),用于确认提交等 */
|
|
|
-function onUploadChange({ imgArr }) {
|
|
|
+function onDeleteUpload(_file, detail) {
|
|
|
imgTouched.value = true;
|
|
|
- popupInnerImgArr.value = [...(imgArr || [])];
|
|
|
+ popupInnerImgArr.value.splice(detail.index, 1);
|
|
|
}
|
|
|
|
|
|
-const beforeReadUpload = () => {
|
|
|
- popupInnerLoading.value = false;
|
|
|
- return true;
|
|
|
-};
|
|
|
-
|
|
|
const afterReadUpload = async (data) => {
|
|
|
if (!Array.isArray(data)) {
|
|
|
data = [data];
|
|
|
}
|
|
|
- popupInnerLoading.value = true;
|
|
|
- try {
|
|
|
- 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);
|
|
|
+ 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("图片上传失败,请稍后再试!");
|
|
|
}
|
|
|
+ } else {
|
|
|
+ file.status = "failed";
|
|
|
+ file.message = "上传失败";
|
|
|
+ ElMessage.error("图片上传失败,请稍后再试!");
|
|
|
}
|
|
|
- } finally {
|
|
|
- popupInnerLoading.value = false;
|
|
|
- uploadRef.value?.syncImgArr?.(popupInnerImgArr.value);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+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 {
|
|
|
@@ -181,23 +188,26 @@ async function identifySingleImage(file, resFilename) {
|
|
|
try {
|
|
|
const res = await VE_API.record.batchPestIdentify(params);
|
|
|
if (res.code === 200 && res.results?.length) {
|
|
|
- file.identifyStatus = 'done';
|
|
|
- file.identifyResult = res.results[0];
|
|
|
+ const patch = { identifyStatus: 'done', identifyResult: res.results[0] };
|
|
|
+ Object.assign(file, patch);
|
|
|
+ patchFileItem(resFilename, patch);
|
|
|
} else {
|
|
|
- file.identifyStatus = 'failed';
|
|
|
+ const patch = { identifyStatus: 'failed' };
|
|
|
+ Object.assign(file, patch);
|
|
|
+ patchFileItem(resFilename, patch);
|
|
|
ElMessage.error(t('AI识别失败,请稍后再试!'));
|
|
|
}
|
|
|
} catch {
|
|
|
- file.identifyStatus = 'failed';
|
|
|
+ const patch = { identifyStatus: 'failed' };
|
|
|
+ Object.assign(file, patch);
|
|
|
+ patchFileItem(resFilename, patch);
|
|
|
ElMessage.error(t('AI识别失败,请稍后再试!'));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function onClickPreview(file) {
|
|
|
- if (file.identifyStatus !== 'done' || !file.identifyResult) return;
|
|
|
- previewImageUrl.value = file.url || base_img_url2 + file.resFilename;
|
|
|
- previewResult.value = file.identifyResult;
|
|
|
- activeResultTab.value = 0;
|
|
|
+function openIdentifyPreview(file) {
|
|
|
+ previewImageUrl.value = file.url || base_img_url2 + (file.resFilename || '');
|
|
|
+ previewResult.value = file.identifyResult || null;
|
|
|
showIdentifyPreview.value = true;
|
|
|
}
|
|
|
|
|
|
@@ -211,17 +221,31 @@ function handleConfirm() {
|
|
|
}
|
|
|
|
|
|
function uploadReset() {
|
|
|
+ fileList.value = [];
|
|
|
popupInnerImgArr.value = [];
|
|
|
imgTouched.value = false;
|
|
|
- popupInnerLoading.value = false;
|
|
|
showIdentifyPreview.value = false;
|
|
|
previewImageUrl.value = '';
|
|
|
previewResult.value = null;
|
|
|
- activeResultTab.value = 0;
|
|
|
- uploadRef.value?.uploadReset?.();
|
|
|
}
|
|
|
|
|
|
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) {
|
|
|
@@ -262,6 +286,80 @@ defineExpose({
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ .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 {
|
|
|
@@ -291,83 +389,38 @@ defineExpose({
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.identify-result-popup {
|
|
|
- width: 100%;
|
|
|
- max-width: 100%;
|
|
|
- border-radius: 0;
|
|
|
- background: none;
|
|
|
+.identify-result-panel {
|
|
|
+ padding: 8px 10px;
|
|
|
+ background: rgba(0, 0, 0, 0.4);
|
|
|
+ border-radius: 6px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ margin: 18px 12px;
|
|
|
|
|
|
- .identify-preview-wrap {
|
|
|
- position: relative;
|
|
|
- width: 100%;
|
|
|
-
|
|
|
- .identify-preview-img {
|
|
|
- display: block;
|
|
|
- width: 100%;
|
|
|
- max-height: 75vh;
|
|
|
- object-fit: contain;
|
|
|
- background: #000;
|
|
|
+ .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;
|
|
|
}
|
|
|
|
|
|
- .identify-result-panel {
|
|
|
- position: absolute;
|
|
|
- left: 12px;
|
|
|
- right: 12px;
|
|
|
- bottom: 12px;
|
|
|
- padding: 10px 12px;
|
|
|
- background: rgba(61, 61, 61, 0.92);
|
|
|
- border-radius: 8px;
|
|
|
- backdrop-filter: blur(4px);
|
|
|
-
|
|
|
- .result-header {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 6px;
|
|
|
- margin-bottom: 8px;
|
|
|
-
|
|
|
- .accent-bar {
|
|
|
- width: 3px;
|
|
|
- height: 14px;
|
|
|
- border-radius: 2px;
|
|
|
- background: #f0d09c;
|
|
|
- flex-shrink: 0;
|
|
|
- }
|
|
|
-
|
|
|
- .result-name {
|
|
|
- color: #f0d09c;
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 500;
|
|
|
- line-height: 20px;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .result-tabs {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 8px 12px;
|
|
|
- margin-bottom: 8px;
|
|
|
-
|
|
|
- .result-tab {
|
|
|
- color: rgba(255, 255, 255, 0.75);
|
|
|
- font-size: 12px;
|
|
|
- line-height: 18px;
|
|
|
- cursor: pointer;
|
|
|
-
|
|
|
- &.active {
|
|
|
- color: #fff;
|
|
|
- font-weight: 500;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .result-content {
|
|
|
- color: #fff;
|
|
|
- font-size: 13px;
|
|
|
- line-height: 20px;
|
|
|
- white-space: pre-wrap;
|
|
|
- word-break: break-all;
|
|
|
- }
|
|
|
+ .result-name {
|
|
|
+ color: #FFD786;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 500;
|
|
|
+ line-height: 22px;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ .result-content {
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|