From 322c3c490eb0d6a2c4ac7a09a83f774051e46586 Mon Sep 17 00:00:00 2001 From: kola-web Date: Tue, 21 Apr 2026 14:12:08 +0800 Subject: [PATCH] image stash --- project.config.json | 2 +- project.private.config.json | 18 +- src/app.json | 4 +- src/app.ts | 12 +- src/patient/components/camera/index.json | 7 + src/patient/components/camera/index.scss | 138 +++++++ src/patient/components/camera/index.ts | 97 +++++ src/patient/components/camera/index.wxml | 41 ++ src/patient/components/image-crop/index.json | 8 + src/patient/components/image-crop/index.scss | 98 +++++ src/patient/components/image-crop/index.ts | 278 ++++++++++++++ src/patient/components/image-crop/index.wxml | 39 ++ src/patient/components/image-merge/index.json | 8 + src/patient/components/image-merge/index.scss | 186 +++++++++ src/patient/components/image-merge/index.ts | 177 +++++++++ src/patient/components/image-merge/index.wxml | 1 + src/patient/pages/camera/index.json | 7 - src/patient/pages/camera/index.scss | 123 ------ src/patient/pages/camera/index.ts | 81 ---- src/patient/pages/camera/index.wxml | 37 -- src/patient/pages/imageCrop/index.json | 9 - src/patient/pages/imageCrop/index.scss | 97 ----- src/patient/pages/imageCrop/index.ts | 246 ------------ src/patient/pages/imageCrop/index.wxml | 42 -- src/patient/pages/imageMerge/index.json | 10 - src/patient/pages/imageMerge/index.scss | 187 --------- src/patient/pages/imageMerge/index.ts | 266 ------------- src/patient/pages/imageMerge/index.wxml | 108 ------ src/patient/pages/imageProcessing/index.json | 11 + src/patient/pages/imageProcessing/index.scss | 111 ++++++ src/patient/pages/imageProcessing/index.ts | 89 +++++ src/patient/pages/imageProcessing/index.wxml | 55 +++ 接口文档.md | 528 ++++++++++++++++++++++++++ 33 files changed, 1882 insertions(+), 1239 deletions(-) create mode 100644 src/patient/components/camera/index.json create mode 100644 src/patient/components/camera/index.scss create mode 100644 src/patient/components/camera/index.ts create mode 100644 src/patient/components/camera/index.wxml create mode 100644 src/patient/components/image-crop/index.json create mode 100644 src/patient/components/image-crop/index.scss create mode 100644 src/patient/components/image-crop/index.ts create mode 100644 src/patient/components/image-crop/index.wxml create mode 100644 src/patient/components/image-merge/index.json create mode 100644 src/patient/components/image-merge/index.scss create mode 100644 src/patient/components/image-merge/index.ts create mode 100644 src/patient/components/image-merge/index.wxml delete mode 100644 src/patient/pages/camera/index.json delete mode 100644 src/patient/pages/camera/index.scss delete mode 100644 src/patient/pages/camera/index.ts delete mode 100644 src/patient/pages/camera/index.wxml delete mode 100644 src/patient/pages/imageCrop/index.json delete mode 100644 src/patient/pages/imageCrop/index.scss delete mode 100644 src/patient/pages/imageCrop/index.ts delete mode 100644 src/patient/pages/imageCrop/index.wxml delete mode 100644 src/patient/pages/imageMerge/index.json delete mode 100644 src/patient/pages/imageMerge/index.scss delete mode 100644 src/patient/pages/imageMerge/index.ts delete mode 100644 src/patient/pages/imageMerge/index.wxml create mode 100644 src/patient/pages/imageProcessing/index.json create mode 100644 src/patient/pages/imageProcessing/index.scss create mode 100644 src/patient/pages/imageProcessing/index.ts create mode 100644 src/patient/pages/imageProcessing/index.wxml create mode 100644 接口文档.md diff --git a/project.config.json b/project.config.json index 625d498..1d8faf4 100644 --- a/project.config.json +++ b/project.config.json @@ -71,5 +71,5 @@ } ] }, - "appid": "wxf9ce8010f1ad24aa" + "appid": "wx71ac9c27c3c3e3f4" } \ No newline at end of file diff --git a/project.private.config.json b/project.private.config.json index 16c5f6f..45f583a 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -23,27 +23,13 @@ "miniprogram": { "list": [ { - "name": "拍照", - "pathName": "patient/pages/camera/index", + "name": "patient/pages/imageProcessing/index", + "pathName": "patient/pages/imageProcessing/index", "query": "", "scene": null, "launchMode": "default" }, { - "name": "图片裁剪", - "pathName": "patient/pages/imageCrop/index", - "query": "", - "launchMode": "default", - "scene": null - }, - { - "name": "图片拼接", - "pathName": "patient/pages/imageMerge/index", - "query": "", - "launchMode": "default", - "scene": null - }, - { "name": "患者-qol", "pathName": "patient/pages/qol/index", "query": "pushId=81", diff --git a/src/app.json b/src/app.json index 2eb0c24..2fcae83 100644 --- a/src/app.json +++ b/src/app.json @@ -72,9 +72,7 @@ "pages/hormonesResult/index", "pages/medical/index", "pages/medicalDetail/index", - "pages/imageMerge/index", - "pages/imageCrop/index", - "pages/camera/index" + "pages/imageProcessing/index" ] }, { diff --git a/src/app.ts b/src/app.ts index cbd248d..d095bd7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -18,15 +18,15 @@ App({ // 测试号 wx2b0bb13edf717c1d // dev // appid:wxf9ce8010f1ad24aa - url: 'https://m.xd.hbraas.com', - upFileUrl: 'https://m.xd.hbraas.com/', - imageUrl: 'https://m.xd.hbraas.com/xd/', + // url: 'https://m.xd.hbraas.com', + // upFileUrl: 'https://m.xd.hbraas.com/', + // imageUrl: 'https://m.xd.hbraas.com/xd/', // pro // appid:wx71ac9c27c3c3e3f4 - // url: 'https://m.xd.hbsaas.com', - // upFileUrl: 'https://m.xd.hbsaas.com/', - // imageUrl: 'https://m.xd.hbsaas.com/api/xd/', + url: 'https://m.xd.hbsaas.com', + upFileUrl: 'https://m.xd.hbsaas.com/', + imageUrl: 'https://m.xd.hbsaas.com/api/xd/', loginState: '', isLogin: 0, diff --git a/src/patient/components/camera/index.json b/src/patient/components/camera/index.json new file mode 100644 index 0000000..33ea64d --- /dev/null +++ b/src/patient/components/camera/index.json @@ -0,0 +1,7 @@ +{ + "component": true, + "usingComponents": { + "van-icon": "@vant/weapp/icon/index", + "van-button": "@vant/weapp/button/index" + } +} \ No newline at end of file diff --git a/src/patient/components/camera/index.scss b/src/patient/components/camera/index.scss new file mode 100644 index 0000000..db97bcc --- /dev/null +++ b/src/patient/components/camera/index.scss @@ -0,0 +1,138 @@ +.camera-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + height: 100vh; + display: flex; + flex-direction: column; + background-color: #000; + z-index: 1000; +} + +.camera { + flex: 1; + width: 100%; + position: relative; +} + +.camera-frame { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 80%; + aspect-ratio: 3 / 4; + pointer-events: none; + border: 2rpx solid rgba(255, 255, 255, 0.3); +} + +.frame-corner { + position: absolute; + width: 40rpx; + height: 40rpx; + border: 4rpx solid #fff; +} + +.left-top { + top: 0; + left: 0; + border-right: none; + border-bottom: none; +} + +.right-top { + top: 0; + right: 0; + border-left: none; + border-bottom: none; +} + +.left-bottom { + bottom: 0; + left: 0; + border-right: none; + border-top: none; +} + +.right-bottom { + bottom: 0; + right: 0; + border-left: none; + border-top: none; +} + +.close-btn { + position: fixed; + top: 40rpx; + right: 40rpx; + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.5); + border-radius: 50%; + z-index: 10; +} + +.controls { + position: fixed; + bottom: 80rpx; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 80rpx; + padding: 30rpx 0; +} + +.control-btn { + display: flex; + align-items: center; + justify-content: center; + width: 80rpx; + height: 80rpx; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.2); +} + +.capture-btn { + width: 140rpx; + height: 140rpx; + background-color: rgba(255, 255, 255, 0.3); + border: 6rpx solid #fff; + + .capture-inner { + width: 110rpx; + height: 110rpx; + border-radius: 50%; + background-color: #fff; + } +} + +.preview-section { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #000; + display: flex; + flex-direction: column; + z-index: 10; + + .preview-image { + flex: 1; + width: 100%; + } + + .preview-actions { + display: flex; + justify-content: center; + padding: 40rpx; + background-color: #000; + } +} \ No newline at end of file diff --git a/src/patient/components/camera/index.ts b/src/patient/components/camera/index.ts new file mode 100644 index 0000000..e4c0fb1 --- /dev/null +++ b/src/patient/components/camera/index.ts @@ -0,0 +1,97 @@ +Component({ + properties: { + defaultPosition: { + type: String, + value: 'back', + }, + }, + + data: { + visible: false, + devicePosition: 'back', + flash: 'off' as 'off' | 'auto' | 'on', + previewImage: '', + }, + + lifetimes: { + attached() { + this.setData({ + devicePosition: this.properties.defaultPosition, + }) + }, + }, + + methods: { + openCamera() { + this.setData({ + visible: true, + previewImage: '', + }) + }, + + closeCamera() { + this.setData({ visible: false }) + this.triggerEvent('cancel') + }, + + switchCamera() { + const newPosition = this.data.devicePosition === 'back' ? 'front' : 'back' + this.setData({ + devicePosition: newPosition, + }) + this.triggerEvent('positionchange', { position: newPosition }) + }, + + toggleFlash() { + const flashModes: Array<'off' | 'auto' | 'on'> = ['off', 'auto', 'on'] + const currentIndex = flashModes.indexOf(this.data.flash) + const nextIndex = (currentIndex + 1) % flashModes.length + this.setData({ + flash: flashModes[nextIndex], + }) + }, + + takePhoto() { + const ctx = wx.createCameraContext() + + ctx.takePhoto({ + quality: 'high', + success: (res) => { + this.setData({ + previewImage: res.tempImagePath, + }) + this.triggerEvent('capture', { tempFilePath: res.tempImagePath }) + }, + fail: (err) => { + console.error('拍照失败:', err) + wx.showToast({ + title: '拍照失败', + icon: 'none', + }) + this.triggerEvent('error', { reason: 'capture_failed' }) + }, + }) + }, + + onCameraError(e: WechatMiniprogram.CustomEvent) { + console.error('相机错误:', e.detail) + wx.showToast({ + title: '相机权限未开启', + icon: 'none', + }) + this.triggerEvent('error', { reason: 'permission_denied' }) + }, + + retake() { + this.setData({ + previewImage: '', + }) + this.triggerEvent('retake') + }, + + usePhoto() { + this.triggerEvent('use', { tempFilePath: this.data.previewImage }) + this.setData({ visible: false }) + }, + }, +}) diff --git a/src/patient/components/camera/index.wxml b/src/patient/components/camera/index.wxml new file mode 100644 index 0000000..0f7c5fc --- /dev/null +++ b/src/patient/components/camera/index.wxml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 重拍 + 使用照片 + + + \ No newline at end of file diff --git a/src/patient/components/image-crop/index.json b/src/patient/components/image-crop/index.json new file mode 100644 index 0000000..4d7b384 --- /dev/null +++ b/src/patient/components/image-crop/index.json @@ -0,0 +1,8 @@ +{ + "component": true, + "usingComponents": { + "van-button": "@vant/weapp/button/index", + "van-icon": "@vant/weapp/icon/index", + "van-toast": "@vant/weapp/toast/index" + } +} \ No newline at end of file diff --git a/src/patient/components/image-crop/index.scss b/src/patient/components/image-crop/index.scss new file mode 100644 index 0000000..efaa84d --- /dev/null +++ b/src/patient/components/image-crop/index.scss @@ -0,0 +1,98 @@ +.container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + height: 100vh; + display: flex; + flex-direction: column; + background-color: #1a1a1a; + overflow: hidden; + z-index: 1000; +} + +.crop-wrapper { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.crop-inner { + position: relative; +} + +.crop-area { + position: relative; + background-color: #000; +} + +.source-image { + transform-origin: 0 0; +} + +.crop-frame { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + border: 2rpx solid rgba(255, 255, 255, 0.5); + box-shadow: 0 0 0 9999rpx rgba(0, 0, 0, 0.5); +} + +.frame-corner { + position: absolute; + width: 40rpx; + height: 40rpx; + border: 4rpx solid #fff; +} + +.left-top { + top: 0; + left: 0; + border-right: none; + border-bottom: none; +} + +.right-top { + top: 0; + right: 0; + border-left: none; + border-bottom: none; +} + +.left-bottom { + bottom: 0; + left: 0; + border-right: none; + border-top: none; +} + +.right-bottom { + bottom: 0; + right: 0; + border-left: none; + border-top: none; +} + +.tip-text { + display: flex; + align-items: center; + justify-content: center; + gap: 8rpx; + padding: 30rpx 0; + font-size: 24rpx; + color: #999; +} + +.action-buttons { + padding: 20rpx 30rpx; +} + +.crop-canvas { + position: fixed; + left: -9999px; + top: -9999px; +} diff --git a/src/patient/components/image-crop/index.ts b/src/patient/components/image-crop/index.ts new file mode 100644 index 0000000..ee26628 --- /dev/null +++ b/src/patient/components/image-crop/index.ts @@ -0,0 +1,278 @@ +interface TouchPoint { + x: number + y: number +} + +Component({ + properties: { + id: { + type: String, + value: 'default', + }, + }, + + data: { + visible: false, + imageSrc: '', + imgWidth: 0, + imgHeight: 0, + imgX: 0, + imgY: 0, + scale: 1, + minScale: 1, + maxScale: 4, + cropSize: 280, + originScale: 1, + lastTouches: [] as TouchPoint[], + lastDist: 0, + lastScale: 1, + }, + + lifetimes: { + attached() { + const systemInfo = wx.getSystemInfoSync() + const cropSize = Math.min(systemInfo.windowWidth - 60, 350) + this.setData({ cropSize }) + }, + }, + + methods: { + cropImage(imageSrc: string) { + this.setData({ visible: true }) + this.initImage(imageSrc) + }, + + cancelCrop() { + this.setData({ visible: false }) + this.triggerEvent('cancel') + }, + + chooseImage() { + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['album', 'camera'], + success: (res) => { + if (res.tempFiles.length > 0) { + this.initImage(res.tempFiles[0].tempFilePath) + } + }, + fail: () => { + this.triggerEvent('cancel') + }, + }) + }, + + initImage(src: string) { + wx.getImageInfo({ + src, + success: (res) => { + const { width, height } = res + const { cropSize } = this.data + + const scaleX = cropSize / width + const scaleY = cropSize / height + const scale = Math.max(scaleX, scaleY) + + const imgWidth = width * scale + const imgHeight = height * scale + const imgX = 0 + const imgY = (cropSize - imgHeight) / 2 + + this.setData({ + imageSrc: src, + imgWidth, + imgHeight, + imgX, + imgY, + scale: 1, + originScale: scale, + }) + + this.triggerEvent('imagechange', { src }) + }, + fail: () => { + wx.showToast({ title: '图片加载失败', icon: 'none' }) + this.triggerEvent('error', { reason: 'load_failed' }) + }, + }) + }, + + clampImgPosition() { + const { imgWidth, imgHeight, imgX, imgY, scale, cropSize } = this.data + const scaledWidth = imgWidth * scale + const scaledHeight = imgHeight * scale + + let newImgX = imgX + let newImgY = imgY + + if (scaledWidth <= cropSize) { + newImgX = (cropSize - scaledWidth) / 2 + } + else { + const minX = cropSize - scaledWidth + const maxX = 0 + newImgX = Math.max(minX, Math.min(maxX, imgX)) + } + + if (scaledHeight <= cropSize) { + newImgY = (cropSize - scaledHeight) / 2 + } + else { + const minY = cropSize - scaledHeight + const maxY = 0 + newImgY = Math.max(minY, Math.min(maxY, imgY)) + } + + if (newImgX !== imgX || newImgY !== imgY) { + this.setData({ imgX: newImgX, imgY: newImgY }) + } + }, + + onTouchStart(e: WechatMiniprogram.TouchEvent) { + const touches = e.touches + if (touches.length === 1) { + this.setData({ + lastTouches: [{ x: touches[0].clientX, y: touches[0].clientY }], + }) + } + else if (touches.length === 2) { + const currentDist = Math.hypot( + touches[0].clientX - touches[1].clientX, + touches[0].clientY - touches[1].clientY, + ) + this.setData({ + lastTouches: [ + { x: touches[0].clientX, y: touches[0].clientY }, + { x: touches[1].clientX, y: touches[1].clientY }, + ], + lastDist: currentDist, + lastScale: this.data.scale, + }) + } + }, + + onTouchMove(e: WechatMiniprogram.TouchEvent) { + const touches = e.touches + + if (touches.length === 1) { + const lastTouches = this.data.lastTouches + if (lastTouches.length === 1) { + const dx = touches[0].clientX - lastTouches[0].x + const dy = touches[0].clientY - lastTouches[0].y + + this.setData({ + imgX: this.data.imgX + dx, + imgY: this.data.imgY + dy, + lastTouches: [{ x: touches[0].clientX, y: touches[0].clientY }], + }) + + this.clampImgPosition() + } + } + else if (touches.length === 2) { + const currentDist = Math.hypot( + touches[0].clientX - touches[1].clientX, + touches[0].clientY - touches[1].clientY, + ) + + const lastTouches = this.data.lastTouches + if (lastTouches.length === 2 && this.data.lastDist > 0) { + const ratio = currentDist / this.data.lastDist + let newScale = this.data.lastScale * ratio + newScale = Math.max(this.data.minScale, Math.min(this.data.maxScale, newScale)) + + const { imgWidth, imgHeight, cropSize } = this.data + const scaledWidth = imgWidth * newScale + const scaledHeight = imgHeight * newScale + + if (scaledWidth < cropSize || scaledHeight < cropSize) { + newScale = Math.max(cropSize / imgWidth, cropSize / imgHeight) + } + + this.setData({ scale: newScale }) + this.clampImgPosition() + } + } + }, + + onTouchEnd() { + this.setData({ lastTouches: [], lastDist: 0 }) + }, + + chooseNewImage() { + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['album', 'camera'], + success: (res) => { + if (res.tempFiles.length > 0) { + this.initImage(res.tempFiles[0].tempFilePath) + } + }, + }) + }, + + saveCroppedImage() { + wx.showLoading({ title: '裁剪中...', mask: true }) + + const { imageSrc, imgWidth, imgHeight, imgX, imgY, scale, cropSize, id, originScale } = this.data + + const query = this.createSelectorQuery() + query + .in(this) + .select(`#cropCanvas-${id}`) + .fields({ node: true, size: true }) + .exec((res) => { + if (!res[0]) { + wx.hideLoading() + wx.showToast({ title: '裁剪失败', icon: 'none' }) + this.triggerEvent('error', { reason: 'canvas_not_found' }) + return + } + + const canvas = res[0].node + const ctx = canvas.getContext('2d') + const pixelRatio = wx.getWindowInfo().pixelRatio + + canvas.width = cropSize * pixelRatio + canvas.height = cropSize * pixelRatio + + const img = canvas.createImage() + img.src = imageSrc + + img.onload = () => { + const scaledWidth = imgWidth * scale + const scaledHeight = imgHeight * scale + + const sx = (-imgX / scale) * pixelRatio + const sy = (-imgY / scale) * pixelRatio + const sWidth = (cropSize / scale) * pixelRatio + const sHeight = (cropSize / scale) * pixelRatio + + ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, canvas.width, canvas.height) + + wx.canvasToTempFilePath({ + canvas, + success: (result) => { + wx.hideLoading() + this.setData({ visible: false }) + this.triggerEvent('save', { tempFilePath: result.tempFilePath }) + }, + fail: () => { + wx.hideLoading() + wx.showToast({ title: '裁剪失败', icon: 'none' }) + this.triggerEvent('error', { reason: 'canvas_to_temp_failed' }) + }, + }) + } + + img.onerror = () => { + wx.hideLoading() + wx.showToast({ title: '图片加载失败', icon: 'none' }) + this.triggerEvent('error', { reason: 'image_load_failed' }) + } + }) + }, + }, +}) diff --git a/src/patient/components/image-crop/index.wxml b/src/patient/components/image-crop/index.wxml new file mode 100644 index 0000000..5ffe1aa --- /dev/null +++ b/src/patient/components/image-crop/index.wxml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + 移动和缩放图片,调整裁剪区域 + + + + + 重新选择图片 + + + 保存裁剪图片 + + + + + diff --git a/src/patient/components/image-merge/index.json b/src/patient/components/image-merge/index.json new file mode 100644 index 0000000..4d7b384 --- /dev/null +++ b/src/patient/components/image-merge/index.json @@ -0,0 +1,8 @@ +{ + "component": true, + "usingComponents": { + "van-button": "@vant/weapp/button/index", + "van-icon": "@vant/weapp/icon/index", + "van-toast": "@vant/weapp/toast/index" + } +} \ No newline at end of file diff --git a/src/patient/components/image-merge/index.scss b/src/patient/components/image-merge/index.scss new file mode 100644 index 0000000..c75d4fa --- /dev/null +++ b/src/patient/components/image-merge/index.scss @@ -0,0 +1,186 @@ +.container { + height: 100vh; + display: flex; + flex-direction: column; + padding: 30rpx; + overflow: hidden; + box-sizing: border-box; +} + +.header { + flex-shrink: 0; + text-align: center; + margin-bottom: 40rpx; + + .title { + display: block; + font-size: 40rpx; + font-weight: bold; + color: #333; + } +} + +.upload-section { + flex-shrink: 0; + background-color: #fff; + border-radius: 16rpx; + padding: 30rpx; + margin-bottom: 30rpx; +} + +.section-title { + font-size: 30rpx; + font-weight: 500; + color: #333; + margin-bottom: 20rpx; +} + +.scroll-area { + flex: 1; + overflow-y: auto; + margin-bottom: 20rpx; +} + +.image-grid { + display: flex; + flex-wrap: wrap; + gap: 20rpx; +} + +.image-item { + position: relative; + width: calc((100% - 40rpx) / 3); + aspect-ratio: 1; + border-radius: 12rpx; + overflow: hidden; + + .preview-img { + width: 100%; + height: 100%; + } + + .delete-btn { + position: absolute; + top: 8rpx; + right: 8rpx; + width: 40rpx; + height: 40rpx; + background-color: rgba(0, 0, 0, 0.5); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + } +} + +.add-btn { + width: calc((100% - 40rpx) / 3); + aspect-ratio: 1; + border: 2rpx dashed #ddd; + border-radius: 12rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: #fafafa; + + .add-text { + font-size: 24rpx; + color: #999; + margin-top: 10rpx; + } +} + +.action-section { + margin-bottom: 30rpx; +} + +.result-section { + background-color: #fff; + border-radius: 16rpx; + padding: 30rpx; + margin-bottom: 30rpx; +} + +.result-preview { + position: relative; + border-radius: 12rpx; + overflow: hidden; + background-color: #f5f5f5; + + .merged-img { + width: 100%; + display: block; + } +} + +.tips { + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx; + text-align: center; + font-size: 24rpx; + color: #999; +} + +.merge-canvas { + position: fixed; + left: -9999px; + top: -9999px; + width: 1px; + height: 1px; +} + +.preview-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + + .preview-content { + width: 100%; + max-height: 100%; + } + + .full-img { + width: 100%; + display: block; + } + + .close-btn { + position: absolute; + top: 40rpx; + right: 40rpx; + width: 60rpx; + height: 60rpx; + display: flex; + align-items: center; + justify-content: center; + } + + .save-btn { + position: absolute; + bottom: 60rpx; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 8rpx; + padding: 20rpx 40rpx; + background-color: rgba(255, 255, 255, 0.2); + border-radius: 60rpx; + backdrop-filter: blur(10px); + + text { + color: #fff; + font-size: 28rpx; + } + } +} diff --git a/src/patient/components/image-merge/index.ts b/src/patient/components/image-merge/index.ts new file mode 100644 index 0000000..ffa75eb --- /dev/null +++ b/src/patient/components/image-merge/index.ts @@ -0,0 +1,177 @@ +interface ImageItem { + src: string + time?: string +} + +Component({ + properties: { + id: { + type: String, + value: 'default', + }, + }, + + data: { + imageList: [] as ImageItem[], + mergedImage: '', + isLoading: false, + }, + + methods: { + mergeImages(imageList: ImageItem[]) { + if (this.data.isLoading) { + return + } + + if (!imageList || imageList.length < 2) { + wx.showToast({ title: '至少需要2张图片才能拼接', icon: 'none' }) + return + } + + wx.showLoading({ title: '拼接中...', mask: true }) + this.setData({ isLoading: true, imageList }) + + this.validateImages(imageList) + .then(() => this.drawImagesOnCanvas()) + .then((mergedImage) => { + wx.hideLoading() + this.setData({ + mergedImage, + isLoading: false, + }) + this.triggerEvent('save', { tempFilePath: mergedImage }) + wx.previewImage({ + urls: [mergedImage], + current: mergedImage, + }) + }) + .catch((error) => { + wx.hideLoading() + console.error('拼接失败:', error) + this.setData({ isLoading: false }) + wx.showToast({ title: error.message || '拼接失败,请重试', icon: 'none' }) + this.triggerEvent('error', { reason: error.message }) + }) + }, + + validateImages(imageList: ImageItem[]): Promise { + return Promise.all( + imageList.map((item, index) => + new Promise((resolve, reject) => { + wx.getImageInfo({ + src: item.src, + success: () => resolve(), + fail: () => reject(new Error(`第${index + 1}张图片加载失败`)), + }) + }) + ) + ) + }, + + formatTime(date: Date): string { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}` + }, + + drawImagesOnCanvas(): Promise { + return new Promise((resolve, reject) => { + const { imageList, id } = this.data + const query = this.createSelectorQuery() + query + .in(this) + .select(`#mergeCanvas-${id}`) + .fields({ node: true, size: true }) + .exec((res) => { + if (!res[0]) { + reject(new Error('Canvas not found')) + return + } + + const canvas = res[0].node + const ctx = canvas.getContext('2d') + ctx.fillStyle = '#ffffff' + + Promise.all(imageList.map(item => this.getImageInfo(item.src))) + .then((imageInfos) => { + const targetWidth = 750 + const pixelRatio = wx.getWindowInfo().pixelRatio + const canvasWidth = Math.floor((targetWidth * pixelRatio) / 2) + const timeAreaHeight = 60 + const watermarkHeight = 80 + + let totalHeight = 0 + const scaledHeights: number[] = [] + + imageInfos.forEach((info) => { + const scale = canvasWidth / info.width + const scaledHeight = Math.floor(info.height * scale) + scaledHeights.push(scaledHeight) + totalHeight += scaledHeight + timeAreaHeight + }) + + totalHeight += watermarkHeight + + canvas.width = canvasWidth + canvas.height = totalHeight + + ctx.fillRect(0, 0, canvasWidth, totalHeight) + + let currentY = 0 + let loadedCount = 0 + + imageInfos.forEach((info, index) => { + const img = canvas.createImage() + img.src = info.path + img.onload = () => { + ctx.drawImage(img, 0, currentY, canvasWidth, scaledHeights[index]) + + const timeY = currentY + scaledHeights[index] + 40 + ctx.fillStyle = '#666666' + ctx.font = '24px sans-serif' + ctx.textAlign = 'center' + const timeText = imageList[index].time || this.formatTime(new Date()) + ctx.fillText(timeText, canvasWidth / 2, timeY) + + currentY += scaledHeights[index] + timeAreaHeight + loadedCount++ + + if (loadedCount === imageInfos.length) { + ctx.fillStyle = '#999999' + ctx.font = '28px sans-serif' + ctx.textAlign = 'right' + ctx.fillText('TED关爱小助手', canvasWidth - 30, totalHeight - 30) + + wx.canvasToTempFilePath({ + canvas, + success: (result) => { + resolve(result.tempFilePath) + }, + fail: reject, + }) + } + } + img.onerror = () => { + reject(new Error(`Failed to load image: ${info.path}`)) + } + }) + }) + .catch(reject) + }) + }) + }, + + getImageInfo(src: string): Promise { + return new Promise((resolve, reject) => { + wx.getImageInfo({ + src, + success: resolve, + fail: reject, + }) + }) + }, + }, +}) diff --git a/src/patient/components/image-merge/index.wxml b/src/patient/components/image-merge/index.wxml new file mode 100644 index 0000000..29fe0c9 --- /dev/null +++ b/src/patient/components/image-merge/index.wxml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/patient/pages/camera/index.json b/src/patient/pages/camera/index.json deleted file mode 100644 index 3b6cb78..0000000 --- a/src/patient/pages/camera/index.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "navigationBarTitleText": "拍照", - "usingComponents": { - "van-icon": "@vant/weapp/icon/index", - "van-button": "@vant/weapp/button/index" - } -} \ No newline at end of file diff --git a/src/patient/pages/camera/index.scss b/src/patient/pages/camera/index.scss deleted file mode 100644 index 88aac51..0000000 --- a/src/patient/pages/camera/index.scss +++ /dev/null @@ -1,123 +0,0 @@ -page { - background-color: #000; - height: 100vh; - overflow: hidden; -} - -.container { - height: 100vh; - display: flex; - flex-direction: column; -} - -.camera { - flex: 1; - width: 100%; - position: relative; -} - -.camera-frame { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 80%; - aspect-ratio: 3 / 4; - pointer-events: none; - border: 2rpx solid rgba(255, 255, 255, 0.3); -} - -.frame-corner { - position: absolute; - width: 40rpx; - height: 40rpx; - border: 4rpx solid #fff; -} - -.left-top { - top: 0; - left: 0; - border-right: none; - border-bottom: none; -} - -.right-top { - top: 0; - right: 0; - border-left: none; - border-bottom: none; -} - -.left-bottom { - bottom: 0; - left: 0; - border-right: none; - border-top: none; -} - -.right-bottom { - bottom: 0; - right: 0; - border-left: none; - border-top: none; -} - -.controls { - position: fixed; - bottom: 80rpx; - left: 0; - right: 0; - display: flex; - align-items: center; - justify-content: center; - gap: 80rpx; - padding: 30rpx 0; -} - -.control-btn { - display: flex; - align-items: center; - justify-content: center; - width: 80rpx; - height: 80rpx; - border-radius: 50%; - background-color: rgba(255, 255, 255, 0.2); -} - -.capture-btn { - width: 140rpx; - height: 140rpx; - background-color: rgba(255, 255, 255, 0.3); - border: 6rpx solid #fff; - - .capture-inner { - width: 110rpx; - height: 110rpx; - border-radius: 50%; - background-color: #fff; - } -} - -.preview-section { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #000; - display: flex; - flex-direction: column; - z-index: 100; -} - -.preview-image { - flex: 1; - width: 100%; -} - -.preview-actions { - display: flex; - justify-content: center; - padding: 40rpx; - background-color: #1a1a1a; -} \ No newline at end of file diff --git a/src/patient/pages/camera/index.ts b/src/patient/pages/camera/index.ts deleted file mode 100644 index 2952883..0000000 --- a/src/patient/pages/camera/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -Page({ - data: { - devicePosition: 'back', - flash: 'off', - previewImage: '', - }, - - onLoad() { - const app = getApp() - this.setData({ - devicePosition: app.globalData?.cameraPosition || 'back', - }) - }, - - switchCamera() { - const newPosition = this.data.devicePosition === 'back' ? 'front' : 'back' - const app = getApp() - if (app.globalData) { - app.globalData.cameraPosition = newPosition - } - this.setData({ - devicePosition: newPosition, - }) - }, - - toggleFlash() { - const flashModes = ['off', 'auto', 'on'] - const currentIndex = flashModes.indexOf(this.data.flash) - const nextIndex = (currentIndex + 1) % flashModes.length - this.setData({ - flash: flashModes[nextIndex], - }) - }, - - takePhoto() { - const ctx = wx.createCameraContext() - - ctx.takePhoto({ - quality: 'high', - success: (res) => { - this.setData({ - previewImage: res.tempImagePath, - }) - }, - fail: (err) => { - console.error('拍照失败:', err) - wx.showToast({ - title: '拍照失败', - icon: 'none', - }) - }, - }) - }, - - onCameraError(e: WechatMiniprogram.CustomEvent) { - console.error('相机错误:', e.detail) - wx.showToast({ - title: '相机权限未开启', - icon: 'none', - }) - }, - - retake() { - this.setData({ - previewImage: '', - }) - }, - - usePhoto() { - const pages = getCurrentPages() - const prevPage = pages[pages.length - 2] - - if (prevPage) { - prevPage.setData({ - photoPath: this.data.previewImage, - }) - } - - wx.navigateBack() - }, -}) \ No newline at end of file diff --git a/src/patient/pages/camera/index.wxml b/src/patient/pages/camera/index.wxml deleted file mode 100644 index 6b0fa5d..0000000 --- a/src/patient/pages/camera/index.wxml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - 重拍 - 使用照片 - - - \ No newline at end of file diff --git a/src/patient/pages/imageCrop/index.json b/src/patient/pages/imageCrop/index.json deleted file mode 100644 index 2c210d0..0000000 --- a/src/patient/pages/imageCrop/index.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "navigationBarTitleText": "图片裁剪", - "disableScroll": true, - "usingComponents": { - "van-button": "@vant/weapp/button/index", - "van-icon": "@vant/weapp/icon/index", - "van-toast": "@vant/weapp/toast/index" - } -} \ No newline at end of file diff --git a/src/patient/pages/imageCrop/index.scss b/src/patient/pages/imageCrop/index.scss deleted file mode 100644 index 525b9ff..0000000 --- a/src/patient/pages/imageCrop/index.scss +++ /dev/null @@ -1,97 +0,0 @@ -page { - background-color: #1a1a1a; - height: 100vh; - overflow: hidden; -} - -.container { - height: 100vh; - display: flex; - flex-direction: column; - padding: 30rpx; -} - -.crop-wrapper { - flex: 1; - display: flex; - align-items: center; - justify-content: center; -} - -.crop-inner { - position: relative; -} - -.crop-area { - position: relative; - background-color: #000; -} - -.source-image { - transform-origin: 0 0; -} - -.crop-frame { - position: absolute; - top: 0; - left: 0; - pointer-events: none; - border: 2rpx solid rgba(255, 255, 255, 0.5); - box-shadow: 0 0 0 9999rpx rgba(0, 0, 0, 0.5); -} - -.frame-corner { - position: absolute; - width: 40rpx; - height: 40rpx; - border: 4rpx solid #fff; -} - -.left-top { - top: 0; - left: 0; - border-right: none; - border-bottom: none; -} - -.right-top { - top: 0; - right: 0; - border-left: none; - border-bottom: none; -} - -.left-bottom { - bottom: 0; - left: 0; - border-right: none; - border-top: none; -} - -.right-bottom { - bottom: 0; - right: 0; - border-left: none; - border-top: none; -} - -.tip-text { - display: flex; - align-items: center; - justify-content: center; - gap: 8rpx; - padding: 30rpx 0; - font-size: 24rpx; - color: #999; -} - -.action-buttons { - padding: 20rpx 0; -} - -.crop-canvas { - position: fixed; - left: -9999px; - top: -9999px; -} - diff --git a/src/patient/pages/imageCrop/index.ts b/src/patient/pages/imageCrop/index.ts deleted file mode 100644 index b0930ac..0000000 --- a/src/patient/pages/imageCrop/index.ts +++ /dev/null @@ -1,246 +0,0 @@ -import Toast from '@vant/weapp/toast/toast' - -interface TouchPoint { - x: number - y: number -} - -Page({ - data: { - imageSrc: '', - imgWidth: 0, - imgHeight: 0, - imgX: 0, - imgY: 0, - scale: 1, - minScale: 0.5, - maxScale: 4, - cropSize: 280, - originScale: 1, - lastTouches: [] as TouchPoint[], - }, - - onLoad(options: { imageSrc?: string }) { - const systemInfo = wx.getSystemInfoSync() - const cropSize = Math.min(systemInfo.windowWidth - 60, 350) - this.setData({ cropSize }) - - if (options.imageSrc) { - this.setData({ imageSrc: decodeURIComponent(options.imageSrc) }) - } - else { - this.chooseImage() - } - }, - - chooseImage() { - wx.chooseMedia({ - count: 1, - mediaType: ['image'], - sourceType: ['album', 'camera'], - success: (res) => { - if (res.tempFiles.length > 0) { - this.initImage(res.tempFiles[0].tempFilePath) - } - }, - fail: () => { - Toast('请先选择图片') - wx.navigateBack() - }, - }) - }, - - initImage(src: string) { - wx.getImageInfo({ - src, - success: (res) => { - const { width, height } = res - const { cropSize } = this.data - - const scaleX = cropSize / width - const scaleY = cropSize / height - const scale = Math.max(scaleX, scaleY) - - const imgWidth = width * scale - const imgHeight = height * scale - const imgX = (cropSize - imgWidth) / 2 - const imgY = (cropSize - imgHeight) / 2 - - this.setData({ - imageSrc: src, - imgWidth, - imgHeight, - imgX, - imgY, - scale: 1, - originScale: 1, - }) - }, - fail: () => { - Toast('图片加载失败') - }, - }) - }, - - onTouchStart(e: WechatMiniprogram.TouchEvent) { - const touches = e.touches - if (touches.length === 1) { - this.setData({ - lastTouches: [{ x: touches[0].clientX, y: touches[0].clientY }], - }) - } - else if (touches.length === 2) { - const lastTouches = this.data.lastTouches - if (lastTouches.length < 2) { - this.setData({ - lastTouches: [ - { x: touches[0].clientX, y: touches[0].clientY }, - { x: touches[1].clientX, y: touches[1].clientY }, - ], - }) - } - } - }, - - onTouchMove(e: WechatMiniprogram.TouchEvent) { - const touches = e.touches - - if (touches.length === 1) { - const lastTouches = this.data.lastTouches - if (lastTouches.length === 1) { - const dx = touches[0].clientX - lastTouches[0].x - const dy = touches[0].clientY - lastTouches[0].y - - this.setData({ - imgX: this.data.imgX + dx, - imgY: this.data.imgY + dy, - lastTouches: [{ x: touches[0].clientX, y: touches[0].clientY }], - }) - } - } - else if (touches.length === 2) { - const lastTouches = this.data.lastTouches - if (lastTouches.length === 2) { - const currentDist = Math.hypot(touches[0].clientX - touches[1].clientX, touches[0].clientY - touches[1].clientY) - const lastDist = Math.hypot(lastTouches[0].x - lastTouches[1].x, lastTouches[0].y - lastTouches[1].y) - - let newScale = this.data.scale * (currentDist / lastDist) - newScale = Math.max(this.data.minScale, Math.min(this.data.maxScale, newScale)) - - const centerX = (touches[0].clientX + touches[1].clientX) / 2 - const centerY = (touches[0].clientY + touches[1].clientY) / 2 - const lastCenterX = (lastTouches[0].x + lastTouches[1].x) / 2 - const lastCenterY = (lastTouches[0].y + lastTouches[1].y) / 2 - - const dx = centerX - lastCenterX - const dy = centerY - lastCenterY - - const imgCenterX = this.data.imgX + (this.data.imgWidth * this.data.scale) / 2 - const imgCenterY = this.data.imgY + (this.data.imgHeight * this.data.scale) / 2 - - const newImgX = imgCenterX - (this.data.imgWidth * newScale) / 2 - const newImgY = imgCenterY - (this.data.imgHeight * newScale) / 2 - - this.setData({ - scale: newScale, - imgX: newImgX + dx, - imgY: newImgY + dy, - lastTouches: [ - { x: touches[0].clientX, y: touches[0].clientY }, - { x: touches[1].clientX, y: touches[1].clientY }, - ], - }) - } - } - }, - - onTouchEnd() { - this.setData({ lastTouches: [] }) - }, - - chooseNewImage() { - wx.chooseMedia({ - count: 1, - mediaType: ['image'], - sourceType: ['album', 'camera'], - success: (res) => { - if (res.tempFiles.length > 0) { - this.initImage(res.tempFiles[0].tempFilePath) - } - }, - }) - }, - - saveCroppedImage() { - Toast.loading({ message: '裁剪中...', forbidClick: true }) - - const { imageSrc, imgWidth, imgHeight, imgX, imgY, scale, cropSize } = this.data - - const query = wx.createSelectorQuery() - query - .select('#cropCanvas') - .fields({ node: true, size: true }) - .exec((res) => { - if (!res[0]) { - Toast.clear() - Toast('裁剪失败') - return - } - - const canvas = res[0].node - const ctx = canvas.getContext('2d') - const pixelRatio = wx.getWindowInfo().pixelRatio - - canvas.width = cropSize * pixelRatio - canvas.height = cropSize * pixelRatio - - ctx.fillStyle = '#ffffff' - ctx.fillRect(0, 0, canvas.width, canvas.height) - - const img = canvas.createImage() - img.src = imageSrc - - img.onload = () => { - const sx = (-imgX / scale) * pixelRatio - const sy = (-imgY / scale) * pixelRatio - const sWidth = (imgWidth / scale) * pixelRatio - const sHeight = (imgHeight / scale) * pixelRatio - - ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, canvas.width, canvas.height) - - wx.canvasToTempFilePath({ - canvas, - x: 0, - y: 0, - width: canvas.width, - height: canvas.height, - destWidth: cropSize, - destHeight: cropSize, - fileType: 'png', - quality: 1, - success: (result) => { - Toast.clear() - wx.saveImageToPhotosAlbum({ - filePath: result.tempFilePath, - success: () => { - Toast.success('保存成功') - }, - fail: () => { - Toast.fail('保存失败,请检查权限') - }, - }) - }, - fail: () => { - Toast.clear() - Toast('裁剪失败') - }, - }) - } - - img.onerror = () => { - Toast.clear() - Toast('图片加载失败') - } - }) - }, -}) diff --git a/src/patient/pages/imageCrop/index.wxml b/src/patient/pages/imageCrop/index.wxml deleted file mode 100644 index d8b315d..0000000 --- a/src/patient/pages/imageCrop/index.wxml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 移动和缩放图片,调整裁剪区域 - - - - - 重新选择图片 - - - 保存裁剪图片 - - - - - - diff --git a/src/patient/pages/imageMerge/index.json b/src/patient/pages/imageMerge/index.json deleted file mode 100644 index 9fad963..0000000 --- a/src/patient/pages/imageMerge/index.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "navigationBarTitleText": "图片拼接", - "usingComponents": { - "van-button": "@vant/weapp/button/index", - "van-icon": "@vant/weapp/icon/index", - "van-toast": "@vant/weapp/toast/index", - "van-dialog": "@vant/weapp/dialog/index", - "van-loading": "@vant/weapp/loading/index" - } -} diff --git a/src/patient/pages/imageMerge/index.scss b/src/patient/pages/imageMerge/index.scss deleted file mode 100644 index 56302cb..0000000 --- a/src/patient/pages/imageMerge/index.scss +++ /dev/null @@ -1,187 +0,0 @@ -page { - background-color: #f5f5f5; - min-height: 100vh; -} - -.container { - padding: 30rpx; -} - -.header { - text-align: center; - margin-bottom: 40rpx; - - .title { - display: block; - font-size: 40rpx; - font-weight: bold; - color: #333; - margin-bottom: 10rpx; - } - - .subtitle { - display: block; - font-size: 26rpx; - color: #999; - } -} - -.upload-section { - background-color: #fff; - border-radius: 16rpx; - padding: 30rpx; - margin-bottom: 30rpx; -} - -.section-title { - font-size: 30rpx; - font-weight: 500; - color: #333; - margin-bottom: 20rpx; -} - -.image-grid { - display: flex; - flex-wrap: wrap; - gap: 20rpx; -} - -.image-item { - position: relative; - width: calc((100% - 40rpx) / 3); - aspect-ratio: 1; - border-radius: 12rpx; - overflow: hidden; - - .preview-img { - width: 100%; - height: 100%; - } - - .delete-btn { - position: absolute; - top: 8rpx; - right: 8rpx; - width: 40rpx; - height: 40rpx; - background-color: rgba(0, 0, 0, 0.5); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - } -} - -.add-btn { - width: calc((100% - 40rpx) / 3); - aspect-ratio: 1; - border: 2rpx dashed #ddd; - border-radius: 12rpx; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background-color: #fafafa; - - .add-text { - font-size: 24rpx; - color: #999; - margin-top: 10rpx; - } -} - -.action-section { - margin-bottom: 30rpx; -} - -.result-section { - background-color: #fff; - border-radius: 16rpx; - padding: 30rpx; - margin-bottom: 30rpx; -} - -.result-preview { - position: relative; - border-radius: 12rpx; - overflow: hidden; - background-color: #f5f5f5; - - .merged-img { - width: 100%; - display: block; - } - - .preview-hint { - position: absolute; - bottom: 0; - left: 0; - right: 0; - padding: 20rpx; - background: linear-gradient(transparent, rgba(0, 0, 0, 0.5)); - color: #fff; - font-size: 24rpx; - text-align: center; - } -} - -.tips { - display: flex; - align-items: flex-start; - justify-content: center; - padding: 40rpx; - text-align: center; - - text { - font-size: 24rpx; - color: #999; - margin-left: 10rpx; - line-height: 1.5; - } -} - -// 隐藏的Canvas -.merge-canvas { - position: fixed; - left: -9999px; - top: -9999px; - width: 1px; - height: 1px; -} - -// 预览弹窗 -.preview-modal { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.9); - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; - - .preview-content { - width: 90%; - max-height: 80%; - overflow: auto; - - .full-img { - width: 100%; - } - } - - .close-btn { - position: absolute; - top: 40rpx; - right: 40rpx; - width: 60rpx; - height: 60rpx; - background-color: rgba(255, 255, 255, 0.2); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - } -} diff --git a/src/patient/pages/imageMerge/index.ts b/src/patient/pages/imageMerge/index.ts deleted file mode 100644 index 26f3ddc..0000000 --- a/src/patient/pages/imageMerge/index.ts +++ /dev/null @@ -1,266 +0,0 @@ -import Toast from '@vant/weapp/toast/toast' - -const app = getApp() - -Page({ - data: { - imageList: [] as string[], - imageTimes: [] as string[], // 存储每张图片的拍摄时间 - mergedImage: '', - canvasWidth: 0, - canvasHeight: 0, - isLoading: false, - previewShow: false, - }, - - onLoad() { - // 页面加载 - }, - - // 选择图片 - chooseImage() { - const remainingCount = 9 - this.data.imageList.length - if (remainingCount <= 0) { - Toast('最多只能选择9张图片') - return - } - - wx.chooseMedia({ - count: remainingCount, - mediaType: ['image'], - sourceType: ['album', 'camera'], - success: (res) => { - const newImages = res.tempFiles.map((file) => file.tempFilePath) - // 获取当前时间作为图片时间 - const now = new Date() - const timeStr = this.formatTime(now) - const newTimes = res.tempFiles.map(() => timeStr) - - this.setData({ - imageList: [...this.data.imageList, ...newImages], - imageTimes: [...this.data.imageTimes, ...newTimes], - }) - }, - }) - }, - - // 格式化时间 - formatTime(date: Date): string { - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - return `${year}-${month}-${day} ${hours}:${minutes}` - }, - - // 删除图片 - deleteImage(e: WechatMiniprogram.TouchEvent) { - const index = e.currentTarget.dataset.index - const newList = [...this.data.imageList] - const newTimes = [...this.data.imageTimes] - newList.splice(index, 1) - newTimes.splice(index, 1) - this.setData({ - imageList: newList, - imageTimes: newTimes, - }) - }, - - // 预览单张图片 - previewImage(e: WechatMiniprogram.TouchEvent) { - const index = e.currentTarget.dataset.index - wx.previewImage({ - current: this.data.imageList[index], - urls: this.data.imageList, - }) - }, - - // 拼接图片 - async mergeImages() { - if (this.data.imageList.length < 2) { - Toast('至少需要2张图片才能拼接') - return - } - - this.setData({ isLoading: true }) - - try { - const mergedImage = await this.drawImagesOnCanvas() - this.setData({ - mergedImage, - isLoading: false, - }) - Toast('拼接成功') - } catch (error) { - console.error('拼接失败:', error) - this.setData({ isLoading: false }) - Toast('拼接失败,请重试') - } - }, - - // 在Canvas上绘制图片(纵向拼接) - drawImagesOnCanvas(): Promise { - return new Promise((resolve, reject) => { - const { imageList, imageTimes } = this.data - const query = wx.createSelectorQuery() - query - .select('#mergeCanvas') - .fields({ node: true, size: true }) - .exec((res) => { - if (!res[0]) { - reject(new Error('Canvas not found')) - return - } - - const canvas = res[0].node - const ctx = canvas.getContext('2d') - - // 设置画布背景为白色 - ctx.fillStyle = '#ffffff' - - // 加载所有图片并获取尺寸 - Promise.all( - imageList.map((src) => this.getImageInfo(src)) - ).then((imageInfos) => { - // 计算目标宽度(取最大宽度或固定宽度) - const targetWidth = 750 // 固定宽度为750rpx对应的像素 - const pixelRatio = wx.getWindowInfo().pixelRatio - const canvasWidth = Math.floor(targetWidth * pixelRatio / 2) // 转换为实际像素 - - // 底部时间文字区域高度 - const timeAreaHeight = 60 - // 右下角水印区域高度 - const watermarkHeight = 80 - - // 计算每张图片缩放后的高度 - let totalHeight = 0 - const scaledHeights: number[] = [] - - imageInfos.forEach((info) => { - const scale = canvasWidth / info.width - const scaledHeight = Math.floor(info.height * scale) - scaledHeights.push(scaledHeight) - totalHeight += scaledHeight + timeAreaHeight - }) - - // 添加水印区域高度 - totalHeight += watermarkHeight - - // 设置画布尺寸 - canvas.width = canvasWidth - canvas.height = totalHeight - - // 填充白色背景 - ctx.fillRect(0, 0, canvasWidth, totalHeight) - - // 绘制所有图片 - let currentY = 0 - let loadedCount = 0 - - imageInfos.forEach((info, index) => { - const img = canvas.createImage() - img.src = info.path - img.onload = () => { - // 绘制图片 - ctx.drawImage(img, 0, currentY, canvasWidth, scaledHeights[index]) - - // 绘制时间文字(图片底部中间) - const timeY = currentY + scaledHeights[index] + 40 - ctx.fillStyle = '#666666' - ctx.font = '24px sans-serif' - ctx.textAlign = 'center' - ctx.fillText(imageTimes[index] || this.formatTime(new Date()), canvasWidth / 2, timeY) - - currentY += scaledHeights[index] + timeAreaHeight - loadedCount++ - - if (loadedCount === imageInfos.length) { - // 所有图片绘制完成,绘制右下角水印 - ctx.fillStyle = '#999999' - ctx.font = '28px sans-serif' - ctx.textAlign = 'right' - ctx.fillText('TED关爱小助手', canvasWidth - 30, totalHeight - 30) - - // 导出图片 - wx.canvasToTempFilePath({ - canvas, - success: (result) => { - resolve(result.tempFilePath) - }, - fail: reject, - }) - } - } - img.onerror = () => { - reject(new Error(`Failed to load image: ${info.path}`)) - } - }) - }).catch(reject) - }) - }) - }, - - // 获取图片信息 - getImageInfo(src: string): Promise { - return new Promise((resolve, reject) => { - wx.getImageInfo({ - src, - success: resolve, - fail: reject, - }) - }) - }, - - // 预览拼接结果 - previewMergedImage() { - if (!this.data.mergedImage) return - this.setData({ previewShow: true }) - }, - - // 关闭预览 - closePreview() { - this.setData({ previewShow: false }) - }, - - // 保存图片到相册 - saveImage() { - if (!this.data.mergedImage) { - Toast('请先拼接图片') - return - } - - wx.saveImageToPhotosAlbum({ - filePath: this.data.mergedImage, - success: () => { - Toast('保存成功') - }, - fail: (err) => { - console.error('保存失败:', err) - if (err.errMsg.includes('auth')) { - wx.showModal({ - title: '提示', - content: '需要授权保存到相册权限', - success: (res) => { - if (res.confirm) { - wx.openSetting() - } - }, - }) - } else { - Toast('保存失败') - } - }, - }) - }, - - // 重新选择 - reset() { - this.setData({ - imageList: [], - imageTimes: [], - mergedImage: '', - previewShow: false, - }) - }, -}) diff --git a/src/patient/pages/imageMerge/index.wxml b/src/patient/pages/imageMerge/index.wxml deleted file mode 100644 index 4408e23..0000000 --- a/src/patient/pages/imageMerge/index.wxml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - 图片拼接工具 - 选择多张图片,自动拼接成长图 - - - - - - 选择图片({{imageList.length}}/9) - - - - - - - - - - - - - - - 添加图片 - - - - - - - - {{imageList.length < 2 ? '至少选择2张图片' : '开始拼接'}} - - - - 重新选择 - - - - - - - 拼接结果 - - - - - 点击预览大图 - - - - 保存到相册 - - - - - - - 支持上传2-9张图片,建议图片宽度相近以获得更好的拼接效果 - - - - - - - - - - - - - - - diff --git a/src/patient/pages/imageProcessing/index.json b/src/patient/pages/imageProcessing/index.json new file mode 100644 index 0000000..3f9b1ee --- /dev/null +++ b/src/patient/pages/imageProcessing/index.json @@ -0,0 +1,11 @@ +{ + "navigationBarTitleText": "图片处理", + "navigationStyle": "default", + "usingComponents": { + "van-icon": "@vant/weapp/icon/index", + "van-button": "@vant/weapp/button/index", + "image-crop": "/patient/components/image-crop/index", + "image-merge": "/patient/components/image-merge/index", + "camera": "/patient/components/camera/index" + } +} \ No newline at end of file diff --git a/src/patient/pages/imageProcessing/index.scss b/src/patient/pages/imageProcessing/index.scss new file mode 100644 index 0000000..c74b453 --- /dev/null +++ b/src/patient/pages/imageProcessing/index.scss @@ -0,0 +1,111 @@ +page { + background-color: #f5f5f5; + min-height: 100vh; +} + +.container { + padding: 30rpx; +} + +.header { + text-align: center; + margin-bottom: 60rpx; + + .title { + display: block; + font-size: 44rpx; + font-weight: bold; + color: #333; + margin-bottom: 10rpx; + } + + .subtitle { + display: block; + font-size: 26rpx; + color: #999; + } +} + +.tools-list { + background-color: #fff; + border-radius: 16rpx; + overflow: hidden; +} + +.tool-card { + display: flex; + align-items: center; + padding: 30rpx; + border-bottom: 1rpx solid #f0f0f0; + + &:last-child { + border-bottom: none; + } +} + +.tool-icon { + width: 80rpx; + height: 80rpx; + border-radius: 16rpx; + background-color: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; + margin-right: 24rpx; +} + +.tool-info { + flex: 1; + + .tool-name { + display: block; + font-size: 32rpx; + font-weight: 500; + color: #333; + margin-bottom: 8rpx; + } + + .tool-desc { + display: block; + font-size: 24rpx; + color: #999; + } +} + +.demo-section { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #fff; + z-index: 100; + display: flex; + flex-direction: column; +} + +.demo-content { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + + image-crop { + flex: 1; + min-height: 0; + } + + .merge-demo { + flex: 1; + display: flex; + flex-direction: column; + padding: 30rpx; + + .demo-tip { + text-align: center; + color: #999; + font-size: 26rpx; + margin-bottom: 30rpx; + } + } +} diff --git a/src/patient/pages/imageProcessing/index.ts b/src/patient/pages/imageProcessing/index.ts new file mode 100644 index 0000000..e828ca1 --- /dev/null +++ b/src/patient/pages/imageProcessing/index.ts @@ -0,0 +1,89 @@ +Page({ + data: { + showCrop: false, + showMerge: false, + isMerging: false, + }, + + openCrop() { + const cropComponent = this.selectComponent('#crop') + if (cropComponent) { + this.setData({ showCrop: true }) + cropComponent.cropImage('https://picsum.photos/400/400') + } + }, + + openMerge() { + this.setData({ + showCrop: false, + showMerge: true, + isMerging: false, + }) + }, + + openCamera() { + const cameraComponent = this.selectComponent('#camera-component') + if (cameraComponent) { + cameraComponent.openCamera() + } + }, + + closeDemo() { + this.setData({ + showCrop: false, + showMerge: false, + }) + }, + + onCropSave(e) { + wx.previewImage({ + urls: [e.detail.tempFilePath], + }) + this.setData({ showCrop: false }) + }, + + onCropCancel() { + this.setData({ showCrop: false }) + }, + + onCropError(e: WechatMiniprogram.CustomEvent) { + console.error('裁剪失败:', e.detail.reason) + this.setData({ showCrop: false }) + }, + + onMergeSave() { + this.setData({ isMerging: false }) + }, + + onMergeError() { + this.setData({ isMerging: false }) + }, + + demoMerge() { + if (this.data.isMerging) { + return + } + + this.setData({ isMerging: true }) + + const mergeComponent = this.selectComponent('#merge') + if (mergeComponent) { + mergeComponent.mergeImages([ + { src: 'https://picsum.photos/400/300', time: '2024-01-01 10:00' }, + { src: 'https://picsum.photos/400/301', time: '2024-01-01 12:00' }, + { src: 'https://picsum.photos/400/302' }, + ]) + } + }, + + onCameraUse(e: WechatMiniprogram.CustomEvent) { + console.log('拍照成功:', e.detail.tempFilePath) + wx.previewImage({ + urls: [e.detail.tempFilePath], + }) + }, + + onCameraCancel() { + console.log('相机已关闭') + }, +}) diff --git a/src/patient/pages/imageProcessing/index.wxml b/src/patient/pages/imageProcessing/index.wxml new file mode 100644 index 0000000..e13c14c --- /dev/null +++ b/src/patient/pages/imageProcessing/index.wxml @@ -0,0 +1,55 @@ + + + 图片处理工具 + 选择不同的图片处理功能 + + + + + + + + + 图片裁剪 + 调整图片大小和比例 + + + + + + + + + + 图片拼接 + 将多张图片拼接成一张 + + + + + + + + + + 拍照 + 拍摄照片 + + + + + + + + + 点击下方按钮使用示例图片演示拼接功能 + 演示图片拼接 + + + + + + + + + diff --git a/接口文档.md b/接口文档.md new file mode 100644 index 0000000..5ece054 --- /dev/null +++ b/接口文档.md @@ -0,0 +1,528 @@ +# 突眼日记接口文档 + +## 一、接口概览 + +### 1.1 基本信息 + +- **模块名称**: 突眼日记 +- **控制器路径**: modules\xd_frontend\controllers\ProtrusionController +- **接口前缀**: ?r=xd/protrusion +- **请求方式**: 需要登录(Header中传入loginState) + +### 1.2 响应格式 + +```json +// 成功响应 +{ + "code": 0, + "data": { + // 业务数据 + } +} + +// 失败响应 +{ + "code": 1, + "msg": "错误信息" +} +``` + +--- + +## 二、接口详情 + +### 2.1 获取基准照状态 + +| 项目 | 内容 | +| ---------------- | -------------------------------- | +| **接口路径** | ?r=xd/protrusion/baseline-status | +| **请求方式** | GET | +| **是否需要登录** | 是 | + +#### 响应示例 + +```json +{ + "code": 0, + "data": { + "hasBaseline": true, + "baselineRecordId": 1 + } +} +``` + +#### 返回字段说明 + +| 字段名 | 类型 | 说明 | +| ---------------- | ---- | --------------------- | +| hasBaseline | bool | 是否已设置基准照 | +| baselineRecordId | int | 基准照记录ID(无则为0) | + +--- + +### 2.2 获取记录列表 + +| 项目 | 内容 | +| ---------------- | ---------------------------- | +| **接口路径** | ?r=xd/protrusion/record-list | +| **请求方式** | GET | +| **是否需要登录** | 是 | + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| -------- | ---- | ---- | ---------------- | +| page | int | 否 | 页码,默认1 | +| pageSize | int | 否 | 每页数量,默认10 | + +#### 响应示例 + +```json +{ + "code": 0, + "data": { + "list": [ + { + "recordId": 1, + "recordDate": "2026-04-10", + "isBaseline": 1, + "treatmentCount": 3, + "leftEye": 18.5, + "rightEye": 19.0, + "interorbitalDistance": 95.0, + "photoCount": 8, + "firstPhoto": "http://example.com/photo1.jpg", + "uploadCompleted": 0 + } + ], + "pages": 2, + "count": 15, + "page": 1 + } +} +``` + +#### 返回字段说明 + +| 字段名 | 类型 | 说明 | +| -------------------- | ------ | ----------------------------- | +| recordId | int | 记录ID | +| recordDate | string | 记录日期 | +| isBaseline | int | 是否为基准照(0-否 1-是) | +| treatmentCount | int | 替妥尤单抗使用次数 | +| leftEye | float | 左眼凸眼度(mm) | +| rightEye | float | 右眼凸眼度(mm) | +| interorbitalDistance | float | 框间距(mm) | +| photoCount | int | 已上传照片数量 | +| firstPhoto | string | 第一张图片URL | +| uploadCompleted | int | 记录完整性(0-未完成 1-已完成) | +| pages | int | 总页数 | +| count | int | 总记录数 | +| page | int | 当前页码 | + +--- + +### 2.3 新增记录 + +| 项目 | 内容 | +| ---------------- | --------------------------- | +| **接口路径** | ?r=xd/protrusion/record-add | +| **请求方式** | POST | +| **是否需要登录** | 是 | + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| -------------------- | ------ | ---- | ------------------------------ | +| recordDate | string | 是 | 记录日期,格式YYYY-MM-DD | +| isBaseline | int | 否 | 是否为基准照(0-否 1-是),默认0 | +| treatmentCount | int | 否 | 替妥尤单抗使用次数(0-9),默认0 | +| leftEye | float | 否 | 左眼凸眼度(mm) | +| rightEye | float | 否 | 右眼凸眼度(mm) | +| interorbitalDistance | float | 否 | 框间距(mm) | +| frontOpen | string | 否 | 正面睁眼照片URL | +| frontClose | string | 否 | 正面闭眼照片URL | +| frontUp | string | 否 | 正面仰头照片URL | +| sideLeft90 | string | 否 | 90°左侧照片URL | +| sideRight90 | string | 否 | 90°右侧照片URL | +| sideLeft45 | string | 否 | 45°左侧照片URL | +| sideRight45 | string | 否 | 45°右侧照片URL | +| eyeUpLeft | string | 否 | 左上眼球照片URL | +| eyeUp | string | 否 | 向上眼球照片URL | +| eyeUpRight | string | 否 | 右上眼球照片URL | +| eyeLeft | string | 否 | 向左眼球照片URL | +| eyeRight | string | 否 | 向右眼球照片URL | +| eyeDownLeft | string | 否 | 左下眼球照片URL | +| eyeDown | string | 否 | 向下眼球照片URL | +| eyeDownRight | string | 否 | 右下眼球照片URL | + +#### 响应示例 + +```json +{ + "code": 0, + "data": { + "recordId": 1 + } +} +``` + +#### 返回字段说明 + +| 字段名 | 类型 | 说明 | +| -------- | ---- | ------------ | +| recordId | int | 新增记录的ID | + +--- + +### 2.4 更新记录 + +| 项目 | 内容 | +| ---------------- | ------------------------------ | +| **接口路径** | ?r=xd/protrusion/record-update | +| **请求方式** | POST | +| **是否需要登录** | 是 | + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| -------------------- | ------ | ---- | ------------------------ | +| recordId | int | 是 | 记录ID | +| recordDate | string | 否 | 记录日期,格式YYYY-MM-DD | +| isBaseline | int | 否 | 是否为基准照(0-否 1-是) | +| treatmentCount | int | 否 | 替妥尤单抗使用次数(0-9) | +| leftEye | float | 否 | 左眼凸眼度(mm) | +| rightEye | float | 否 | 右眼凸眼度(mm) | +| interorbitalDistance | float | 否 | 框间距(mm) | +| frontOpen | string | 否 | 正面睁眼照片URL | +| frontClose | string | 否 | 正面闭眼照片URL | +| frontUp | string | 否 | 正面仰头照片URL | +| sideLeft90 | string | 否 | 90°左侧照片URL | +| sideRight90 | string | 否 | 90°右侧照片URL | +| sideLeft45 | string | 否 | 45°左侧照片URL | +| sideRight45 | string | 否 | 45°右侧照片URL | +| eyeUpLeft | string | 否 | 左上眼球照片URL | +| eyeUp | string | 否 | 向上眼球照片URL | +| eyeUpRight | string | 否 | 右上眼球照片URL | +| eyeLeft | string | 否 | 向左眼球照片URL | +| eyeRight | string | 否 | 向右眼球照片URL | +| eyeDownLeft | string | 否 | 左下眼球照片URL | +| eyeDown | string | 否 | 向下眼球照片URL | +| eyeDownRight | string | 否 | 右下眼球照片URL | + +#### 响应示例 + +```json +{ + "code": 0, + "data": { + "success": true + } +} +``` + +--- + +### 2.5 获取记录详情 + +| 项目 | 内容 | +| ---------------- | ------------------------------ | +| **接口路径** | ?r=xd/protrusion/record-detail | +| **请求方式** | GET | +| **是否需要登录** | 是 | + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| -------- | ---- | ---- | ------ | +| recordId | int | 是 | 记录ID | + +#### 响应示例 + +```json +{ + "code": 0, + "data": { + "recordId": 1, + "recordDate": "2026-04-10", + "isBaseline": 1, + "treatmentCount": 3, + "leftEye": 18.5, + "rightEye": 19.0, + "interorbitalDistance": 95.0, + "photoCount": 15, + "uploadCompleted": 1, + "frontOpen": "http://example.com/photo1.jpg", + "frontClose": "http://example.com/photo2.jpg", + "frontUp": "http://example.com/photo3.jpg", + "sideLeft90": "http://example.com/photo4.jpg", + "sideRight90": "http://example.com/photo5.jpg", + "sideLeft45": "http://example.com/photo6.jpg", + "sideRight45": "http://example.com/photo7.jpg", + "eyeUpLeft": "http://example.com/photo8.jpg", + "eyeUp": "http://example.com/photo9.jpg", + "eyeUpRight": "http://example.com/photo10.jpg", + "eyeLeft": "http://example.com/photo11.jpg", + "eyeRight": "http://example.com/photo12.jpg", + "eyeDownLeft": "http://example.com/photo13.jpg", + "eyeDown": "http://example.com/photo14.jpg", + "eyeDownRight": "http://example.com/photo15.jpg", + "frontOpenName": "正面睁眼", + "frontCloseName": "正面闭眼", + "frontUpName": "正面仰头", + "sideLeft90Name": "90°左侧", + "sideRight90Name": "90°右侧", + "sideLeft45Name": "45°左侧", + "sideRight45Name": "45°右侧", + "eyeUpLeftName": "左上", + "eyeUpName": "向上", + "eyeUpRightName": "右上", + "eyeLeftName": "向左", + "eyeRightName": "向右", + "eyeDownLeftName": "左下", + "eyeDownName": "向下", + "eyeDownRightName": "右下", + "createdAt": "2026-04-10 10:00:00" + } +} +``` + +#### 返回字段说明 + +| 字段名 | 类型 | 说明 | +| -------------------- | ------ | ------------------ | +| recordId | int | 记录ID | +| recordDate | string | 记录日期 | +| isBaseline | int | 是否为基准照 | +| treatmentCount | int | 替妥尤单抗使用次数 | +| leftEye | float | 左眼凸眼度 | +| rightEye | float | 右眼凸眼度 | +| interorbitalDistance | float | 框间距 | +| photoCount | int | 已上传照片数量 | +| uploadCompleted | int | 记录完整性 | +| frontOpen | string | 正面睁眼照片URL | +| frontClose | string | 正面闭眼照片URL | +| frontUp | string | 正面仰头照片URL | +| sideLeft90 | string | 90°左侧照片URL | +| sideRight90 | string | 90°右侧照片URL | +| sideLeft45 | string | 45°左侧照片URL | +| sideRight45 | string | 45°右侧照片URL | +| eyeUpLeft | string | 左上眼球照片URL | +| eyeUp | string | 向上眼球照片URL | +| eyeUpRight | string | 右上眼球照片URL | +| eyeLeft | string | 向左眼球照片URL | +| eyeRight | string | 向右眼球照片URL | +| eyeDownLeft | string | 左下眼球照片URL | +| eyeDown | string | 向下眼球照片URL | +| eyeDownRight | string | 右下眼球照片URL | +| frontOpenName | string | 正面睁眼名称 | +| frontCloseName | string | 正面闭眼名称 | +| frontUpName | string | 正面仰头名称 | +| sideLeft90Name | string | 90°左侧名称 | +| sideRight90Name | string | 90°右侧名称 | +| sideLeft45Name | string | 45°左侧名称 | +| sideRight45Name | string | 45°右侧名称 | +| eyeUpLeftName | string | 左上名称 | +| eyeUpName | string | 向上名称 | +| eyeUpRightName | string | 右上名称 | +| eyeLeftName | string | 向左名称 | +| eyeRightName | string | 向右名称 | +| eyeDownLeftName | string | 左下名称 | +| eyeDownName | string | 向下名称 | +| eyeDownRightName | string | 右下名称 | +| createdAt | string | 创建时间 | + +--- + +### 2.6 删除记录 + +| 项目 | 内容 | +| ---------------- | ------------------------------ | +| **接口路径** | ?r=xd/protrusion/record-delete | +| **请求方式** | POST | +| **是否需要登录** | 是 | + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| -------- | ---- | ---- | ------ | +| recordId | int | 是 | 记录ID | + +#### 响应示例 + +```json +{ + "code": 0, + "data": { + "success": true + } +} +``` + +> **注意**: 删除为软删除,仅更新状态,不实际删除数据 + +--- + +### 2.7 获取可对比记录列表 + +| 项目 | 内容 | +| ---------------- | ---------------------------------- | +| **接口路径** | ?r=xd/protrusion/compare-date-list | +| **请求方式** | GET | +| **是否需要登录** | 是 | + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| ------ | ------ | ---- | ------------ | +| angle | string | 是 | 照片角度标识 | + +#### 响应示例 + +```json +{ + "code": 0, + "data": { + "angleName": "正面睁眼", + "recordList": [ + { + "recordId": 5, + "recordDate": "2026-04-10" + }, + { + "recordId": 8, + "recordDate": "2026-05-15" + } + ] + } +} +``` + +#### 返回字段说明 + +| 字段名 | 类型 | 说明 | +| ---------- | ------ | ------------------------ | +| angleName | string | 当前角度中文名称 | +| recordList | array | 记录列表(不包含基准照) | +| recordId | int | 记录ID | +| recordDate | string | 记录日期 | + +--- + +### 2.8 获取对比详情 + +| 项目 | 内容 | +| ---------------- | ------------------------------- | +| **接口路径** | ?r=xd/protrusion/compare-detail | +| **请求方式** | GET | +| **是否需要登录** | 是 | + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| --------- | ------ | ---- | ---------------------- | +| angle | string | 是 | 照片角度标识 | +| recordIds | string | 是 | 记录ID,多个用逗号分隔 | + +#### 响应示例 + +```json +{ + "code": 0, + "data": { + "angleName": "正面睁眼", + "baselineRecord": { + "recordId": 1, + "recordDate": "2026-01-01", + "treatmentCount": 0, + "leftEye": 20.0, + "rightEye": 20.5, + "interorbitalDistance": 100.0, + "photoUrl": "http://example.com/baseline.jpg" + }, + "compareRecords": [ + { + "recordId": 5, + "recordDate": "2026-04-10", + "treatmentCount": 3, + "leftEye": 18.5, + "rightEye": 19.0, + "interorbitalDistance": 95.0, + "photoUrl": "http://example.com/compare1.jpg" + }, + { + "recordId": 8, + "recordDate": "2026-05-15", + "treatmentCount": 5, + "leftEye": 17.5, + "rightEye": 18.0, + "interorbitalDistance": 92.0, + "photoUrl": "http://example.com/compare2.jpg" + } + ] + } +} +``` + +#### 返回字段说明 + +| 字段名 | 类型 | 说明 | +| -------------- | ------ | ---------------- | +| angleName | string | 当前角度中文名称 | +| baselineRecord | object | 基准照记录 | +| compareRecords | array | 对比记录列表 | + +--- + +### 2.9 获取角度选项 + +| 项目 | 内容 | +| ---------------- | ------------------------------ | +| **接口路径** | ?r=xd/protrusion/angle-options | +| **请求方式** | GET | +| **是否需要登录** | 否 | + +#### 响应示例 + +```json +{ + "code": 0, + "data": { + "frontOpen": "正面睁眼", + "frontClose": "正面闭眼", + "frontUp": "正面仰头", + "sideLeft90": "90°左侧", + "sideRight90": "90°右侧", + "sideLeft45": "45°左侧", + "sideRight45": "45°右侧", + "eyeUpLeft": "左上", + "eyeUp": "向上", + "eyeUpRight": "右上", + "eyeLeft": "向左", + "eyeRight": "向右", + "eyeDownLeft": "左下", + "eyeDown": "向下", + "eyeDownRight": "右下" + } +} +``` + +--- + +## 三、错误码说明 + +| 错误码 | 说明 | +| ------ | -------- | +| 0 | 成功 | +| 1 | 通用错误 | +| 403 | 需要登录 | + +--- + +## 四、注意事项 + +1. **UserId获取**: 用户ID从登录上下文中自动获取,无需传入 +2. **基准照唯一性**: 设置新基准照时,系统会自动取消之前的基准照 +3. **照片上传**: 照片为URL链接,物理文件存储在其他服务器 +4. **软删除**: 删除记录采用软删除方式 +5. **记录完整性**: 当photoCount达到15时,uploadCompleted自动变为1