33 changed files with 1882 additions and 1239 deletions
@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
|
||||
{ |
||||
"navigationBarTitleText": "拍照", |
||||
"component": true, |
||||
"usingComponents": { |
||||
"van-icon": "@vant/weapp/icon/index", |
||||
"van-button": "@vant/weapp/button/index" |
||||
@ -0,0 +1,97 @@
@@ -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 }) |
||||
}, |
||||
}, |
||||
}) |
||||
@ -1,6 +1,5 @@
@@ -1,6 +1,5 @@
|
||||
{ |
||||
"navigationBarTitleText": "图片裁剪", |
||||
"disableScroll": true, |
||||
"component": true, |
||||
"usingComponents": { |
||||
"van-button": "@vant/weapp/button/index", |
||||
"van-icon": "@vant/weapp/icon/index", |
||||
@ -0,0 +1,278 @@
@@ -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' }) |
||||
} |
||||
}) |
||||
}, |
||||
}, |
||||
}) |
||||
@ -0,0 +1,8 @@
@@ -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" |
||||
} |
||||
} |
||||
@ -0,0 +1,177 @@
@@ -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<void> { |
||||
return Promise.all( |
||||
imageList.map((item, index) => |
||||
new Promise<void>((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<string> { |
||||
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<WechatMiniprogram.GetImageInfoSuccessCallbackResult> { |
||||
return new Promise((resolve, reject) => { |
||||
wx.getImageInfo({ |
||||
src, |
||||
success: resolve, |
||||
fail: reject, |
||||
}) |
||||
}) |
||||
}, |
||||
}, |
||||
}) |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
<canvas type="2d" id="mergeCanvas-{{id}}" class="merge-canvas"></canvas> |
||||
@ -1,81 +0,0 @@
@@ -1,81 +0,0 @@
|
||||
Page({ |
||||
data: { |
||||
devicePosition: 'back', |
||||
flash: 'off', |
||||
previewImage: '', |
||||
}, |
||||
|
||||
onLoad() { |
||||
const app = getApp<IAppOption>() |
||||
this.setData({ |
||||
devicePosition: app.globalData?.cameraPosition || 'back', |
||||
}) |
||||
}, |
||||
|
||||
switchCamera() { |
||||
const newPosition = this.data.devicePosition === 'back' ? 'front' : 'back' |
||||
const app = getApp<IAppOption>() |
||||
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() |
||||
}, |
||||
}) |
||||
@ -1,246 +0,0 @@
@@ -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('图片加载失败') |
||||
} |
||||
}) |
||||
}, |
||||
}) |
||||
@ -1,10 +0,0 @@
@@ -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" |
||||
} |
||||
} |
||||
@ -1,266 +0,0 @@
@@ -1,266 +0,0 @@
|
||||
import Toast from '@vant/weapp/toast/toast' |
||||
|
||||
const app = getApp<IAppOption>() |
||||
|
||||
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<string> { |
||||
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<WechatMiniprogram.GetImageInfoSuccessCallbackResult> { |
||||
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, |
||||
}) |
||||
}, |
||||
}) |
||||
@ -1,108 +0,0 @@
@@ -1,108 +0,0 @@
|
||||
<van-toast id="van-toast" /> |
||||
|
||||
<view class="container"> |
||||
<!-- 标题 --> |
||||
<view class="header"> |
||||
<text class="title">图片拼接工具</text> |
||||
<text class="subtitle">选择多张图片,自动拼接成长图</text> |
||||
</view> |
||||
|
||||
<!-- 图片上传区域 --> |
||||
<view class="upload-section"> |
||||
<view class="section-title"> |
||||
<text>选择图片({{imageList.length}}/9)</text> |
||||
</view> |
||||
|
||||
<view class="image-grid"> |
||||
<!-- 已选择的图片 --> |
||||
<view |
||||
class="image-item" |
||||
wx:for="{{imageList}}" |
||||
wx:key="index" |
||||
data-index="{{index}}" |
||||
bindtap="previewImage" |
||||
> |
||||
<image class="preview-img" src="{{item}}" mode="aspectFill" /> |
||||
<view class="delete-btn" catchtap="deleteImage" data-index="{{index}}"> |
||||
<van-icon name="cross" size="12px" color="#fff" /> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 添加按钮 --> |
||||
<view class="add-btn" bindtap="chooseImage" wx:if="{{imageList.length < 9}}"> |
||||
<van-icon name="plus" size="32px" color="#999" /> |
||||
<text class="add-text">添加图片</text> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 操作按钮 --> |
||||
<view class="action-section" wx:if="{{imageList.length > 0}}"> |
||||
<van-button |
||||
type="primary" |
||||
block |
||||
round |
||||
loading="{{isLoading}}" |
||||
loading-text="拼接中..." |
||||
disabled="{{imageList.length < 2}}" |
||||
bindtap="mergeImages" |
||||
> |
||||
{{imageList.length < 2 ? '至少选择2张图片' : '开始拼接'}} |
||||
</van-button> |
||||
|
||||
<van-button |
||||
type="default" |
||||
block |
||||
round |
||||
custom-style="margin-top: 20rpx;" |
||||
bindtap="reset" |
||||
> |
||||
重新选择 |
||||
</van-button> |
||||
</view> |
||||
|
||||
<!-- 拼接结果预览 --> |
||||
<view class="result-section" wx:if="{{mergedImage}}"> |
||||
<view class="section-title"> |
||||
<text>拼接结果</text> |
||||
</view> |
||||
|
||||
<view class="result-preview" bindtap="previewMergedImage"> |
||||
<image class="merged-img" src="{{mergedImage}}" mode="widthFix" /> |
||||
<view class="preview-hint">点击预览大图</view> |
||||
</view> |
||||
|
||||
<van-button |
||||
type="primary" |
||||
block |
||||
round |
||||
custom-style="margin-top: 30rpx;" |
||||
bindtap="saveImage" |
||||
> |
||||
保存到相册 |
||||
</van-button> |
||||
</view> |
||||
|
||||
<!-- 提示信息 --> |
||||
<view class="tips" wx:if="{{imageList.length === 0}}"> |
||||
<van-icon name="info-o" size="16px" color="#999" /> |
||||
<text>支持上传2-9张图片,建议图片宽度相近以获得更好的拼接效果</text> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- Canvas(隐藏)用于图片拼接 --> |
||||
<canvas |
||||
type="2d" |
||||
id="mergeCanvas" |
||||
class="merge-canvas" |
||||
></canvas> |
||||
|
||||
<!-- 全屏预览弹窗 --> |
||||
<view class="preview-modal" wx:if="{{previewShow}}" bindtap="closePreview"> |
||||
<view class="preview-content"> |
||||
<image class="full-img" src="{{mergedImage}}" mode="widthFix" /> |
||||
</view> |
||||
<view class="close-btn"> |
||||
<van-icon name="cross" size="24px" color="#fff" /> |
||||
</view> |
||||
</view> |
||||
@ -0,0 +1,11 @@
@@ -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" |
||||
} |
||||
} |
||||
@ -0,0 +1,111 @@
@@ -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; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,89 @@
@@ -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('相机已关闭') |
||||
}, |
||||
}) |
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
<view class="container"> |
||||
<view class="header"> |
||||
<text class="title">图片处理工具</text> |
||||
<text class="subtitle">选择不同的图片处理功能</text> |
||||
</view> |
||||
|
||||
<view class="tools-list"> |
||||
<view class="tool-card" bindtap="openCrop"> |
||||
<view class="tool-icon"> |
||||
<van-icon name="crop" size="32px" color="#07c160" /> |
||||
</view> |
||||
<view class="tool-info"> |
||||
<text class="tool-name">图片裁剪</text> |
||||
<text class="tool-desc">调整图片大小和比例</text> |
||||
</view> |
||||
<van-icon name="arrow" size="16px" color="#999" /> |
||||
</view> |
||||
|
||||
<view class="tool-card" bindtap="openMerge"> |
||||
<view class="tool-icon"> |
||||
<van-icon name="coupon" size="32px" color="#1989fa" /> |
||||
</view> |
||||
<view class="tool-info"> |
||||
<text class="tool-name">图片拼接</text> |
||||
<text class="tool-desc">将多张图片拼接成一张</text> |
||||
</view> |
||||
<van-icon name="arrow" size="16px" color="#999" /> |
||||
</view> |
||||
|
||||
<view class="tool-card" bindtap="openCamera"> |
||||
<view class="tool-icon"> |
||||
<van-icon name="photograph" size="32px" color="#ff976a" /> |
||||
</view> |
||||
<view class="tool-info"> |
||||
<text class="tool-name">拍照</text> |
||||
<text class="tool-desc">拍摄照片</text> |
||||
</view> |
||||
<van-icon name="arrow" size="16px" color="#999" /> |
||||
</view> |
||||
</view> |
||||
|
||||
<view class="demo-section" wx:if="{{showMerge}}"> |
||||
<view class="demo-content"> |
||||
<view wx:if="{{showMerge}}" class="merge-demo"> |
||||
<view class="demo-tip">点击下方按钮使用示例图片演示拼接功能</view> |
||||
<van-button type="primary" block round loading="{{isMerging}}" disabled="{{isMerging}}" bindtap="demoMerge">演示图片拼接</van-button> |
||||
<image-merge id="merge" bindsave="onMergeSave" binderror="onMergeError" /> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<image-crop id="crop" bindsave="onCropSave" bindcancel="onCropCancel" binderror="onCropError" /> |
||||
|
||||
<camera id="camera-component" binduse="onCameraUse" bindcancel="onCameraCancel" /> |
||||
</view> |
||||
@ -0,0 +1,528 @@
@@ -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 |
||||
Loading…
Reference in new issue