فهرست منبع

拍照留存迁移独立页面

yuanmingze 1 ماه پیش
والد
کامیت
fd5686290c

+ 7 - 0
src/pages.json

@@ -14,6 +14,13 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/index/photo/index",
+      "style": {
+        "navigationStyle": "custom",
+        "backgroundColor": "#000000"
+      }
+    },
     {
       "path": "pages/compliance-training/index",
       "style": {

+ 0 - 161
src/pages/index/components/ImagePreviewOverlay.vue

@@ -1,161 +0,0 @@
-<template>
-  <view v-if="visible" class="preview-mask">
-    <view class="preview-header">
-      <wd-navbar
-        fixed
-        placeholder
-        left-arrow
-        :bordered="false"
-        title="图片预览"
-        safeAreaInsetTop
-        customClass="navbar"
-        @click-left="close"
-      >
-      </wd-navbar>
-    </view>
-
-    <view class="preview-body">
-      <image v-if="watermarkSrc" class="preview-image" :src="watermarkSrc" mode="scaleToFill" />
-    </view>
-
-    <view class="preview-footer">
-      <view class="btn" @touchstart.stop="$emit('retake')">重拍</view>
-      <view class="btn primary" @touchstart.stop="download">下载照片</view>
-    </view>
-
-    <!-- 隐藏 canvas -->
-    <canvas
-      canvas-id="watermarkCanvas"
-      :style="`width: ${canvasWidth}px; height: ${canvasHeight}px; position: fixed; left: -9999px; top: 0`"
-    />
-  </view>
-</template>
-
-<script setup lang="ts">
-import { nextTick, ref, watch } from 'vue'
-import { getCurrentInstance } from 'vue'
-
-import { useSaveToAlbum } from '@/composables/photo/useSaveToAlbum'
-
-import { generateWatermark } from '@/utils/generateWatermark'
-
-const visible = defineModel<boolean>('visible', { default: false })
-
-const props = defineProps<{
-  src: string
-  address: string
-}>()
-
-defineEmits<{
-  (e: 'retake'): void
-}>()
-
-const watermarkSrc = ref('')
-
-const close = () => {
-  visible.value = false
-}
-
-const { saveToAlbum } = useSaveToAlbum()
-
-const download = () => {
-  if (!watermarkSrc.value) return
-  saveToAlbum(watermarkSrc.value)
-}
-
-const instance = getCurrentInstance()
-
-const canvasWidth = ref(10)
-const canvasHeight = ref(10)
-
-watch(
-  () => visible.value,
-  async (val) => {
-    if (!val) return
-    if (!props.src || !props.address) return
-
-    await new Promise<void>((resolve, reject) => {
-      uni.getImageInfo({
-        src: props.src,
-        success(res) {
-          canvasWidth.value = res.width
-          canvasHeight.value = res.height
-          resolve()
-        },
-        fail: reject,
-      })
-    })
-
-    // 等 canvas 真正挂载完成
-    await nextTick()
-    try {
-      watermarkSrc.value = await generateWatermark({
-        imageUrl: props.src,
-        address: props.address,
-        canvasId: 'watermarkCanvas',
-        instance,
-      })
-    } catch (err) {
-      console.error('generateWatermark error', err)
-    }
-  }
-)
-watch(visible, (val) => {
-  if (val) {
-    uni.hideTabBar({ animation: false })
-  } else {
-    uni.showTabBar({ animation: false })
-  }
-})
-</script>
-
-<style scoped lang="scss">
-.preview-mask {
-  position: fixed;
-  inset: 0;
-  background: #000;
-  z-index: 9999;
-  display: flex;
-  flex-direction: column;
-}
-
-.preview-header {
-  height: 96rpx;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  color: #fff;
-  padding: 0 24rpx;
-  box-sizing: border-box;
-}
-
-.preview-body {
-  height: calc(100vh - 96rpx - 128rpx);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-
-  .preview-image {
-    height: calc(100vh - 96rpx - 128rpx);
-    max-width: 100%;
-    width: 750rpx;
-  }
-}
-
-.preview-footer {
-  height: 128rpx;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 0 48rpx;
-  color: #fff;
-
-  .btn {
-    font-size: 32rpx;
-  }
-
-  .primary {
-    font-weight: 500;
-  }
-}
-</style>

+ 11 - 48
src/pages/index/components/WorkbenchTaskSection.vue

@@ -1,6 +1,6 @@
 <template>
   <view class="workbench-task-section">
-    <view class="workbench-task-section-header" @click.self.stop="onTakePhoto">
+    <view class="workbench-task-section-header" @click="onTakePhoto">
       <view class="workbench-task-section-title">任务实施</view>
       <view class="workbench-task-section-action">
         <wd-icon name="camera-fill" color="#fff" size="40rpx"></wd-icon>
@@ -19,27 +19,15 @@
         @click="onSelectTask(item)"
       />
     </view>
-
-    <ImagePreviewOverlay
-      v-model:visible="previewVisible"
-      :src="imageUrl"
-      :address="address"
-      @retake="retake"
-    />
   </view>
 </template>
 
 <script setup lang="ts">
 import { computed, ref } from 'vue'
 
-import { getLocation } from '@/lib/location'
-
 import { useUserStore } from '@/stores/modules/user'
 
-import { takePhoto } from '@/utils/image'
-
 import { DEFAULT_WORKBENCH_TASKS, INVESTMENT_MANAGER_TASKS } from '../workbench/workbench.config'
-import ImagePreviewOverlay from './ImagePreviewOverlay.vue'
 
 const emit = defineEmits<{
   (e: 'select-task', item: (typeof currentTaskList.value)[number]): void
@@ -60,41 +48,16 @@ const onSelectTask = (item: (typeof currentTaskList.value)[number]) => {
   emit('select-task', item)
 }
 
-const previewVisible = ref(false)
-const imageUrl = ref('')
-const address = ref('')
-const takePhotoFlow = async (): Promise<void> => {
-  const locationResult = await getLocation()
-
-  if (!locationResult?.address) {
-    return
-  }
-  address.value = locationResult.address
-  try {
-    const imageSrc = await takePhoto()
-    imageUrl.value = imageSrc
-    previewVisible.value = true
-  } catch {
-    // 用户取消 / 拍照失败 / 格式不支持
-  }
-}
-
-const retake = (): void => {
-  previewVisible.value = false
-  imageUrl.value = ''
-
-  takePhotoFlow()
-}
-
-const taking = ref(false)
-const onTakePhoto = async () => {
-  if (taking.value) return
-  taking.value = true
-  try {
-    await takePhotoFlow()
-  } finally {
-    taking.value = false
-  }
+const navigatingToPhoto = ref(false)
+const onTakePhoto = () => {
+  if (navigatingToPhoto.value) return
+  navigatingToPhoto.value = true
+  uni.navigateTo({
+    url: '/pages/index/photo/index',
+    complete: () => {
+      navigatingToPhoto.value = false
+    },
+  })
 }
 </script>
 <style lang="scss" scoped>

+ 197 - 0
src/pages/index/photo/index.vue

@@ -0,0 +1,197 @@
+<template>
+  <view class="photo-page">
+    <wd-navbar
+      fixed
+      placeholder
+      left-arrow
+      :bordered="false"
+      title="图片预览"
+      safeAreaInsetTop
+      @click-left="goBack"
+    />
+
+    <view class="preview-body">
+      <image v-if="watermarkSrc" class="preview-image" :src="watermarkSrc" mode="scaleToFill" />
+      <text v-else class="preview-status">{{ statusText }}</text>
+    </view>
+
+    <view v-if="imageUrl" class="preview-footer">
+      <view class="btn" @click="takePhotoFlow">重拍</view>
+      <view class="btn primary" :class="{ disabled: !watermarkSrc || saving }" @click="download">
+        下载照片
+      </view>
+    </view>
+
+    <canvas
+      canvas-id="watermarkCanvas"
+      :style="`width: ${canvasWidth}px; height: ${canvasHeight}px; position: fixed; left: -9999px; top: 0`"
+    />
+  </view>
+</template>
+
+<script setup lang="ts">
+import { onReady, onUnload } from '@dcloudio/uni-app'
+import { getCurrentInstance, nextTick, ref } from 'vue'
+
+import { useSaveToAlbum } from '@/composables/photo/useSaveToAlbum'
+import { getLocation } from '@/lib/location'
+import { generateWatermark } from '@/utils/generateWatermark'
+import { takePhoto } from '@/utils/image'
+
+const instance = getCurrentInstance()
+const { saveToAlbum } = useSaveToAlbum()
+
+const imageUrl = ref('')
+const watermarkSrc = ref('')
+const statusText = ref('正在获取位置信息...')
+const canvasWidth = ref(10)
+const canvasHeight = ref(10)
+const taking = ref(false)
+const saving = ref(false)
+
+let active = true
+let flowId = 0
+
+const isCurrentFlow = (id: number) => active && flowId === id
+
+const goBack = () => {
+  if (!active) return
+  active = false
+  flowId++
+  uni.navigateBack()
+}
+
+const getImageSize = (src: string): Promise<{ width: number; height: number }> =>
+  new Promise((resolve, reject) => {
+    uni.getImageInfo({
+      src,
+      success: ({ width, height }) => resolve({ width, height }),
+      fail: reject,
+    })
+  })
+
+const takePhotoFlow = async (): Promise<void> => {
+  if (taking.value || !active) return
+
+  taking.value = true
+  imageUrl.value = ''
+  watermarkSrc.value = ''
+  statusText.value = '正在获取位置信息...'
+  const currentFlow = ++flowId
+
+  try {
+    const location = await getLocation()
+    if (!isCurrentFlow(currentFlow)) return
+    if (!location?.address) {
+      goBack()
+      return
+    }
+
+    statusText.value = '正在打开相机...'
+    const photo = await takePhoto()
+    if (!isCurrentFlow(currentFlow)) return
+
+    imageUrl.value = photo
+    statusText.value = '正在生成水印...'
+    const { width, height } = await getImageSize(photo)
+    if (!isCurrentFlow(currentFlow)) return
+
+    canvasWidth.value = width
+    canvasHeight.value = height
+    await nextTick()
+    if (!isCurrentFlow(currentFlow)) return
+
+    watermarkSrc.value = await generateWatermark({
+      imageUrl: photo,
+      address: location.address,
+      canvasId: 'watermarkCanvas',
+      instance,
+    })
+  } catch (error) {
+    if (!isCurrentFlow(currentFlow)) return
+    if (!imageUrl.value) {
+      // 取消拍照或定位失败时,回到发起拍照的工作台。
+      goBack()
+      return
+    }
+
+    console.error('generateWatermark error', error)
+    statusText.value = '照片处理失败,请重拍'
+  } finally {
+    if (isCurrentFlow(currentFlow)) taking.value = false
+  }
+}
+
+const download = async () => {
+  if (!watermarkSrc.value || saving.value) return
+
+  saving.value = true
+  try {
+    await saveToAlbum(watermarkSrc.value)
+  } catch (error) {
+    console.error('保存照片失败:', error)
+    uni.showToast({ title: '保存失败,请重试', icon: 'none' })
+  } finally {
+    saving.value = false
+  }
+}
+
+onReady(() => {
+  void takePhotoFlow()
+})
+
+onUnload(() => {
+  active = false
+  flowId++
+})
+</script>
+
+<style scoped lang="scss">
+.photo-page {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background: #000;
+}
+
+.preview-body {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  .preview-image {
+    height: 100%;
+    width: 750rpx;
+    max-width: 100%;
+  }
+
+  .preview-status {
+    color: #fff;
+    font-size: 28rpx;
+  }
+}
+
+.preview-footer {
+  height: 128rpx;
+  flex-shrink: 0;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 48rpx env(safe-area-inset-bottom);
+  color: #fff;
+
+  .btn {
+    font-size: 32rpx;
+  }
+
+  .primary {
+    font-weight: 500;
+  }
+
+  .disabled {
+    opacity: 0.5;
+  }
+}
+</style>