|
|
@@ -0,0 +1,638 @@
|
|
|
+<template>
|
|
|
+ <view class="recognization-image">
|
|
|
+ <!-- 画布绘制阶段 -->
|
|
|
+ <view v-if="!resultImage" class="canvas-phase">
|
|
|
+ <canvas
|
|
|
+ canvas-id="pestCanvas"
|
|
|
+ id="pestCanvas"
|
|
|
+ :style="{ width: canvasW + 'px', height: canvasH + 'px' }"
|
|
|
+ ></canvas>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 加载中 -->
|
|
|
+ <view v-if="loading" class="loading-mask">
|
|
|
+ <view class="loading-spinner"></view>
|
|
|
+ <text class="loading-text">标注图片生成中...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 图片查看器:支持拖拽和缩放 -->
|
|
|
+ <movable-area
|
|
|
+ v-if="resultImage"
|
|
|
+ class="image-viewer"
|
|
|
+ :style="{ width: containerW + 'px', height: containerH + 'px' }"
|
|
|
+ >
|
|
|
+ <movable-view
|
|
|
+ direction="all"
|
|
|
+ :scale="true"
|
|
|
+ :scale-min="0.5"
|
|
|
+ :scale-max="5"
|
|
|
+ :scale-value="currentScale"
|
|
|
+ :inertia="true"
|
|
|
+ :damping="40"
|
|
|
+ :x="initX"
|
|
|
+ :y="initY"
|
|
|
+ :style="{ width: imgDisplayW + 'px', height: imgDisplayH + 'px' }"
|
|
|
+ class="movable-img"
|
|
|
+ @scale="onScale"
|
|
|
+ >
|
|
|
+ <image :src="resultImage" mode="scaleToFill" class="annotated-image" />
|
|
|
+ </movable-view>
|
|
|
+ </movable-area>
|
|
|
+
|
|
|
+ <!-- 缩放控制按钮 -->
|
|
|
+ <!-- <view v-if="resultImage" class="zoom-controls">
|
|
|
+ <view class="zoom-btn" @click="zoomIn">
|
|
|
+ <text class="zoom-icon">+</text>
|
|
|
+ </view>
|
|
|
+ <view class="zoom-btn" @click="zoomOut">
|
|
|
+ <text class="zoom-icon">-</text>
|
|
|
+ </view>
|
|
|
+ </view> -->
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+/**
|
|
|
+ * 害虫识别图片组件
|
|
|
+ * 功能:在原始图片上绘制害虫标注框和标签,生成标注图片,支持拖拽和缩放查看
|
|
|
+ *
|
|
|
+ * Props:
|
|
|
+ * imageSrc - 原始图片URL
|
|
|
+ * pestData - 害虫数据数组 [{startX, startY, width, height, text}]
|
|
|
+ *
|
|
|
+ * 使用示例:
|
|
|
+<RecognizationImage
|
|
|
+ :imageSrc="currentImg.addr"
|
|
|
+ :pestData="[
|
|
|
+ {
|
|
|
+ startX: 790.89,
|
|
|
+ startY: 211.76,
|
|
|
+ width: 169.65,
|
|
|
+ height: 189.47,
|
|
|
+ text: '二化螟',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ startX: 455.3,
|
|
|
+ startY: 313.3,
|
|
|
+ width: 102.78,
|
|
|
+ height: 90.4,
|
|
|
+ text: '二化螟',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ startX: 676.85,
|
|
|
+ startY: 472.55,
|
|
|
+ width: 135.01,
|
|
|
+ height: 101.26,
|
|
|
+ text: '草地贪夜蛾',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ startX: 321.37,
|
|
|
+ startY: 819.22,
|
|
|
+ width: -29.88,
|
|
|
+ height: -29.08,
|
|
|
+ text: '草地贪夜蛾2号',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ startX: 879.37,
|
|
|
+ startY: 79.9,
|
|
|
+ width: -49.03,
|
|
|
+ height: -45.74,
|
|
|
+ text: '666',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ startX: 845.62,
|
|
|
+ startY: 643.09,
|
|
|
+ width: 14.21,
|
|
|
+ height: 15.99,
|
|
|
+ text: '6663',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ startX: 370.17,
|
|
|
+ startY: 45.58,
|
|
|
+ width: 32.24,
|
|
|
+ height: 30.45,
|
|
|
+ text: '666',
|
|
|
+ },
|
|
|
+ ]"
|
|
|
+ style="width: 100%; height: 500rpx"
|
|
|
+ />
|
|
|
+ */
|
|
|
+
|
|
|
+// 害虫类型对应的颜色调色板
|
|
|
+const COLOR_PALETTE = [
|
|
|
+ '#FF0000',
|
|
|
+ '#00FFC2',
|
|
|
+ '#FF00A8',
|
|
|
+ '#120080',
|
|
|
+ '#BDFF00',
|
|
|
+ '#FFB800',
|
|
|
+ '#5E8000',
|
|
|
+ '#FF5C00',
|
|
|
+ '#00FF75',
|
|
|
+ '#00F0FF',
|
|
|
+ '#0094FF',
|
|
|
+ '#EB00FF',
|
|
|
+ '#00C2FF',
|
|
|
+ '#C74C4C',
|
|
|
+ '#FFFFFF',
|
|
|
+ '#FF6B6B',
|
|
|
+];
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'RecognizationImage',
|
|
|
+ props: {
|
|
|
+ // 原始图片地址
|
|
|
+ imageSrc: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ // 害虫标注数据
|
|
|
+ pestData: {
|
|
|
+ type: Array,
|
|
|
+ default: () => [],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ ctx: null,
|
|
|
+ loading: false,
|
|
|
+ // 画布尺寸
|
|
|
+ canvasW: 300,
|
|
|
+ canvasH: 300,
|
|
|
+ // 原始图片尺寸
|
|
|
+ originalW: 0,
|
|
|
+ originalH: 0,
|
|
|
+ // 缩放系数
|
|
|
+ imageScale: 1, // 图片适配画布的缩放比
|
|
|
+ baseScale: 0.25, // 基础缩放参考值
|
|
|
+ markScale: 1, // 标注坐标缩放比
|
|
|
+ // 生成的标注图片路径
|
|
|
+ resultImage: '',
|
|
|
+ // 图片显示尺寸(适配容器)
|
|
|
+ imgDisplayW: 300,
|
|
|
+ imgDisplayH: 300,
|
|
|
+ // movable-view 初始位置(居中)
|
|
|
+ initX: 0,
|
|
|
+ initY: 0,
|
|
|
+ // 容器尺寸
|
|
|
+ containerW: 300,
|
|
|
+ containerH: 300,
|
|
|
+ // 害虫类型→颜色映射
|
|
|
+ colorMap: {},
|
|
|
+ // 当前缩放值
|
|
|
+ currentScale: 1,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ imageSrc(newVal) {
|
|
|
+ if (newVal) {
|
|
|
+ this.render();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ pestData: {
|
|
|
+ handler() {
|
|
|
+ if (this.imageSrc) {
|
|
|
+ this.render();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ deep: true,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.getContainerSize().then(() => {
|
|
|
+ if (this.imageSrc) {
|
|
|
+ this.render();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ /**
|
|
|
+ * 获取容器尺寸
|
|
|
+ */
|
|
|
+ getContainerSize() {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ uni
|
|
|
+ .createSelectorQuery()
|
|
|
+ .in(this)
|
|
|
+ .select('.recognization-image')
|
|
|
+ .boundingClientRect((rect) => {
|
|
|
+ if (rect && rect.width > 0) {
|
|
|
+ this.containerW = rect.width;
|
|
|
+ this.containerH = rect.height;
|
|
|
+ }
|
|
|
+ resolve();
|
|
|
+ })
|
|
|
+ .exec();
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 主渲染流程:加载图片 → 绘制标注 → 导出图片
|
|
|
+ */
|
|
|
+ async render() {
|
|
|
+ if (!this.imageSrc) return;
|
|
|
+
|
|
|
+ this.loading = true;
|
|
|
+ this.resultImage = '';
|
|
|
+ this.colorMap = {};
|
|
|
+ this.currentScale = 1;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. 加载图片信息
|
|
|
+ const imgInfo = await this.loadImageInfo(this.imageSrc);
|
|
|
+ console.log('[RecognizationImage] 图片信息:', imgInfo);
|
|
|
+
|
|
|
+ if (imgInfo.width > 0 && imgInfo.height > 0) {
|
|
|
+ this.originalW = imgInfo.width;
|
|
|
+ this.originalH = imgInfo.height;
|
|
|
+ } else {
|
|
|
+ // 回退场景:无法获取尺寸,使用默认比例 4:3
|
|
|
+ console.warn(
|
|
|
+ '[RecognizationImage] 无法获取图片尺寸,使用默认 4:3 比例',
|
|
|
+ );
|
|
|
+ this.originalW = 4000;
|
|
|
+ this.originalH = 3000;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 计算缩放参数和画布尺寸
|
|
|
+ this.calcSizes();
|
|
|
+
|
|
|
+ // 3. 等待 canvas 渲染到 DOM
|
|
|
+ await this.$nextTick();
|
|
|
+ await this.delay(100);
|
|
|
+
|
|
|
+ // 4. 初始化画布上下文
|
|
|
+ this.ctx = uni.createCanvasContext('pestCanvas', this);
|
|
|
+
|
|
|
+ // 5. 绘制背景图片(使用 imageScale 缩放到画布尺寸)
|
|
|
+ const drawSrc = imgInfo.path || this.imageSrc;
|
|
|
+ const displayWidth = this.originalW * this.imageScale;
|
|
|
+ const displayHeight = this.originalH * this.imageScale;
|
|
|
+ console.log('[RecognizationImage] 绘制参数:', {
|
|
|
+ canvas: this.canvasW + 'x' + this.canvasH,
|
|
|
+ imageScale: this.imageScale,
|
|
|
+ baseScale: this.baseScale,
|
|
|
+ markScale: this.markScale,
|
|
|
+ displaySize: displayWidth + 'x' + displayHeight,
|
|
|
+ });
|
|
|
+ this.ctx.drawImage(drawSrc, 0, 0, displayWidth, displayHeight);
|
|
|
+
|
|
|
+ // 6. 绘制害虫标注(使用 markScale 缩放坐标)
|
|
|
+ this.drawAnnotations();
|
|
|
+
|
|
|
+ // 8. 执行绘制并导出
|
|
|
+ this.ctx.draw(false, () => {
|
|
|
+ setTimeout(() => {
|
|
|
+ this.exportCanvas();
|
|
|
+ }, 300);
|
|
|
+ });
|
|
|
+ } catch (err) {
|
|
|
+ console.error('[RecognizationImage] 渲染失败:', err);
|
|
|
+ this.loading = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算缩放参数和画布尺寸(与 Draw 组件一致的缩放逻辑)
|
|
|
+ */
|
|
|
+ calcSizes() {
|
|
|
+ const cw = this.containerW || 300;
|
|
|
+ const ch = this.containerH || 300;
|
|
|
+ const canvasAspect = cw / ch;
|
|
|
+ const imageAspect = this.originalW / this.originalH;
|
|
|
+
|
|
|
+ // 画布尺寸 = 容器尺寸
|
|
|
+ this.canvasW = cw;
|
|
|
+ this.canvasH = ch;
|
|
|
+
|
|
|
+ // 计算图片等比缩放系数(适配画布)
|
|
|
+ if (imageAspect > canvasAspect) {
|
|
|
+ this.imageScale = cw / this.originalW;
|
|
|
+ } else {
|
|
|
+ this.imageScale = ch / this.originalH;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据图片宽度定义基础缩放比例
|
|
|
+ if (this.originalW >= 5000) {
|
|
|
+ this.baseScale = 0.25;
|
|
|
+ } else if (this.originalW >= 4000) {
|
|
|
+ this.baseScale = 0.31;
|
|
|
+ } else {
|
|
|
+ this.baseScale = 0.4;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 标注坐标缩放比 = imageScale / baseScale
|
|
|
+ this.markScale = 1 - (this.baseScale - this.imageScale) / this.baseScale;
|
|
|
+
|
|
|
+ // 图片在画布上的实际显示尺寸
|
|
|
+ this.imgDisplayW = Math.round(this.originalW * this.imageScale);
|
|
|
+ this.imgDisplayH = Math.round(this.originalH * this.imageScale);
|
|
|
+
|
|
|
+ // movable-view 初始居中
|
|
|
+ this.initX = Math.round((cw - this.imgDisplayW) / 2);
|
|
|
+ this.initY = Math.round((ch - this.imgDisplayH) / 2);
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加载图片信息(支持多级回退)
|
|
|
+ * 1. uni.getImageInfo(首选,返回尺寸+本地路径)
|
|
|
+ * 2. uni.downloadFile → uni.getImageInfo(小程序域名白名单场景)
|
|
|
+ * 3. H5 直接使用 Image 对象获取尺寸
|
|
|
+ */
|
|
|
+ loadImageInfo(src) {
|
|
|
+ if (!src) return Promise.reject(new Error('imageSrc 为空'));
|
|
|
+
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ // 方式1:uni.getImageInfo
|
|
|
+ uni.getImageInfo({
|
|
|
+ src,
|
|
|
+ success: (res) => resolve(res),
|
|
|
+ fail: (err1) => {
|
|
|
+ console.warn(
|
|
|
+ '[RecognizationImage] getImageInfo 失败,尝试 downloadFile:',
|
|
|
+ src,
|
|
|
+ err1,
|
|
|
+ );
|
|
|
+ // 方式2:先下载再获取信息
|
|
|
+ uni.downloadFile({
|
|
|
+ url: src,
|
|
|
+ success: (dlRes) => {
|
|
|
+ if (dlRes.statusCode === 200) {
|
|
|
+ uni.getImageInfo({
|
|
|
+ src: dlRes.tempFilePath,
|
|
|
+ success: (res) => resolve(res),
|
|
|
+ fail: (err2) => {
|
|
|
+ console.warn(
|
|
|
+ '[RecognizationImage] 下载后 getImageInfo 也失败,使用下载路径:',
|
|
|
+ err2,
|
|
|
+ );
|
|
|
+ // 无法获取尺寸,需要后续处理
|
|
|
+ resolve({
|
|
|
+ path: dlRes.tempFilePath,
|
|
|
+ width: 0,
|
|
|
+ height: 0,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ reject(new Error('下载失败,状态码: ' + dlRes.statusCode));
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (dlErr) => {
|
|
|
+ console.error(
|
|
|
+ '[RecognizationImage] downloadFile 也失败:',
|
|
|
+ dlErr,
|
|
|
+ );
|
|
|
+ reject(dlErr);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 绘制害虫标注(使用 markScale 缩放坐标,与 Draw 组件一致)
|
|
|
+ */
|
|
|
+ drawAnnotations() {
|
|
|
+ if (!this.pestData || !this.pestData.length) return;
|
|
|
+
|
|
|
+ let colorIndex = 0;
|
|
|
+
|
|
|
+ // 根据画布尺寸计算合适的线宽和字号
|
|
|
+ const lineWidth = Math.max(1, Math.round(this.canvasW / 200));
|
|
|
+ const fontSize = Math.max(10, Math.round(this.canvasW / 25));
|
|
|
+ const labelPadding = Math.max(2, Math.round(fontSize / 4));
|
|
|
+
|
|
|
+ this.pestData.forEach((pest) => {
|
|
|
+ const { startX, startY, width, height, text } = pest;
|
|
|
+
|
|
|
+ // 为每种害虫分配颜色
|
|
|
+ if (!this.colorMap[text]) {
|
|
|
+ this.colorMap[text] =
|
|
|
+ COLOR_PALETTE[colorIndex % COLOR_PALETTE.length];
|
|
|
+ colorIndex++;
|
|
|
+ }
|
|
|
+ const color = this.colorMap[text];
|
|
|
+
|
|
|
+ // 使用 markScale 缩放坐标到画布空间
|
|
|
+ let x = Number(startX) * this.markScale;
|
|
|
+ let y = Number(startY) * this.markScale;
|
|
|
+ let w = Number(width) * this.markScale;
|
|
|
+ let h = Number(height) * this.markScale;
|
|
|
+
|
|
|
+ // 处理反向绘制(宽或高为负数):调整起点,宽高取绝对值
|
|
|
+ if (w < 0) {
|
|
|
+ x = x + w;
|
|
|
+ w = -w;
|
|
|
+ }
|
|
|
+ if (h < 0) {
|
|
|
+ y = y + h;
|
|
|
+ h = -h;
|
|
|
+ }
|
|
|
+ w = Math.max(1, w);
|
|
|
+ h = Math.max(1, h);
|
|
|
+
|
|
|
+ // --- 绘制半透明填充底色 ---
|
|
|
+ const r = parseInt(color.slice(1, 3), 16);
|
|
|
+ const g = parseInt(color.slice(3, 5), 16);
|
|
|
+ const b = parseInt(color.slice(5, 7), 16);
|
|
|
+ // this.ctx.setFillStyle(`rgba(${r},${g},${b},0.15)`);
|
|
|
+ // this.ctx.fillRect(x, y, w, h);
|
|
|
+
|
|
|
+ // --- 绘制矩形边框 ---
|
|
|
+ this.ctx.setStrokeStyle(color);
|
|
|
+ this.ctx.setLineWidth(lineWidth);
|
|
|
+ this.ctx.strokeRect(x, y, w, h);
|
|
|
+
|
|
|
+ // --- 绘制标签 ---
|
|
|
+ if (text) {
|
|
|
+ // 标签定位:优先在矩形上方,空间不够则放在矩形内部顶部
|
|
|
+ let labelX = x;
|
|
|
+ let labelY = y - fontSize - labelPadding;
|
|
|
+ if (labelY < 0) {
|
|
|
+ labelY = y + labelPadding;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 标签文字
|
|
|
+ this.ctx.setFillStyle(color);
|
|
|
+ this.ctx.setFontSize(fontSize);
|
|
|
+ this.ctx.fillText(
|
|
|
+ text,
|
|
|
+ labelX + labelPadding,
|
|
|
+ labelY + fontSize + labelPadding * 0.5,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 手指缩放回调,同步记录当前缩放值
|
|
|
+ */
|
|
|
+ onScale(e) {
|
|
|
+ if (e.detail.scale) {
|
|
|
+ this.currentScale = e.detail.scale;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按钮放大
|
|
|
+ */
|
|
|
+ zoomIn() {
|
|
|
+ this.currentScale = Math.min(5, this.currentScale + 0.3);
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按钮缩小
|
|
|
+ */
|
|
|
+ zoomOut() {
|
|
|
+ this.currentScale = Math.max(0.5, this.currentScale - 0.3);
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导出画布为图片
|
|
|
+ */
|
|
|
+ exportCanvas() {
|
|
|
+ uni.canvasToTempFilePath(
|
|
|
+ {
|
|
|
+ canvasId: 'pestCanvas',
|
|
|
+ quality: 1,
|
|
|
+ success: (res) => {
|
|
|
+ this.resultImage = res.tempFilePath;
|
|
|
+ this.loading = false;
|
|
|
+ this.$emit('generated', res.tempFilePath);
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('[RecognizationImage] 导出失败:', err);
|
|
|
+ this.loading = false;
|
|
|
+ this.$emit('error', err);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ this,
|
|
|
+ );
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 工具方法:延迟
|
|
|
+ */
|
|
|
+ delay(ms) {
|
|
|
+ return new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对外方法:重新渲染
|
|
|
+ */
|
|
|
+ refresh() {
|
|
|
+ this.render();
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对外方法:获取标注图片路径
|
|
|
+ */
|
|
|
+ getResultImage() {
|
|
|
+ return this.resultImage;
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.recognization-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ background-color: #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 画布阶段 */
|
|
|
+.canvas-phase {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+/* 加载遮罩 */
|
|
|
+.loading-mask {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ background-color: rgba(255, 255, 255, 0.85);
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner {
|
|
|
+ width: 36rpx;
|
|
|
+ height: 36rpx;
|
|
|
+ border: 4rpx solid #e0e0e0;
|
|
|
+ border-top-color: #1890ff;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 0.8s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ to {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.loading-text {
|
|
|
+ margin-top: 16rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666666;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图片查看器 */
|
|
|
+.image-viewer {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.movable-img {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.annotated-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+/* 缩放控制按钮 */
|
|
|
+.zoom-controls {
|
|
|
+ position: absolute;
|
|
|
+ right: 20rpx;
|
|
|
+ bottom: 40rpx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16rpx;
|
|
|
+ z-index: 20;
|
|
|
+}
|
|
|
+
|
|
|
+.zoom-btn {
|
|
|
+ width: 64rpx;
|
|
|
+ height: 64rpx;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+
|
|
|
+.zoom-icon {
|
|
|
+ font-size: 36rpx;
|
|
|
+ color: #ffffff;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+</style>
|