From 6f88bc656b11d46b991ccb7df103ae8a813608df Mon Sep 17 00:00:00 2001 From: Blizzard Date: Tue, 10 Feb 2026 14:02:35 +0800 Subject: [PATCH] feat: Add plant identification feature with image upload, classification results display, and integration into the wiki page. --- app.json | 3 +- pages/garden/add/index.js | 50 +++- pages/garden/add/index.wxml | 106 ++++++-- pages/garden/add/index.wxss | 285 ++++++++++---------- pages/plant-detail/edit/index.js | 301 +++++++++++---------- pages/plant-detail/edit/index.wxml | 138 +++++----- pages/plant-detail/edit/index.wxss | 262 ++++++++++++------- pages/wiki/identify/index.js | 88 +++++++ pages/wiki/identify/index.json | 10 + pages/wiki/identify/index.wxml | 127 +++++++++ pages/wiki/identify/index.wxss | 407 +++++++++++++++++++++++++++++ pages/wiki/index.js | 32 ++- pages/wiki/index.wxml | 19 +- pages/wiki/index.wxss | 81 +++++- utils/request.js | 63 ++++- 15 files changed, 1481 insertions(+), 491 deletions(-) create mode 100644 pages/wiki/identify/index.js create mode 100644 pages/wiki/identify/index.json create mode 100644 pages/wiki/identify/index.wxml create mode 100644 pages/wiki/identify/index.wxss diff --git a/app.json b/app.json index 41d5541..3a867fa 100644 --- a/app.json +++ b/app.json @@ -9,7 +9,8 @@ "pages/profile/index", "pages/plant-detail/edit/index", "pages/plant-detail/index", - "pages/wiki/detail/index" + "pages/wiki/detail/index", + "pages/wiki/identify/index" ], "window": { "backgroundTextStyle": "light", diff --git a/pages/garden/add/index.js b/pages/garden/add/index.js index 6206f5d..35903c2 100644 --- a/pages/garden/add/index.js +++ b/pages/garden/add/index.js @@ -15,6 +15,12 @@ Page({ uploadedImageId: '', // Store the uploaded image ID + // Extra fields + potMaterial: '', + potSize: '', + sunlight: '', + plantingMaterial: '', + showActionSheet: false, actionSheetItems: [ { label: '拍摄', value: 'camera' }, @@ -85,7 +91,7 @@ Page({ }); // Show loading - wx.showLoading({ title: 'Uploading...' }); + wx.showLoading({ title: '上传中...' }); // Call upload API request.upload(tempFilePath).then(data => { @@ -103,13 +109,11 @@ Page({ uploadedImageId: imageId, isLocalImage: true }); - wx.showToast({ title: 'Success', icon: 'success' }); - } else { - wx.showToast({ title: 'No URL returned', icon: 'none' }); + } }).catch(err => { wx.hideLoading(); - wx.showToast({ title: 'Upload Failed', icon: 'none' }); + wx.showToast({ title: '上传失败', icon: 'none' }); }); }, fail: (err) => { @@ -123,6 +127,12 @@ Page({ onLocationInput(e) { this.setData({ newPlantLocation: e.detail.value }); }, onDateChange(e) { this.setData({ newPlantDate: e.detail.value }); }, + // Extra field inputs + onPotMaterialInput(e) { this.setData({ potMaterial: e.detail.value }); }, + onPotSizeInput(e) { this.setData({ potSize: e.detail.value }); }, + onSunlightInput(e) { this.setData({ sunlight: e.detail.value }); }, + onPlantingMaterialInput(e) { this.setData({ plantingMaterial: e.detail.value }); }, + handleAddCareTask() { const tasks = this.data.newCareTasks; const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'other'); @@ -162,7 +172,14 @@ Page({ onTaskFreqInput(e) { const { id } = e.currentTarget.dataset; - const tasks = this.data.newCareTasks.map(t => t.id === id ? { ...t, frequencyValue: parseInt(e.detail.value) || 1 } : t); + const raw = e.detail.value; + // Allow empty while editing; validate on save + const tasks = this.data.newCareTasks.map(t => { + if (t.id === id) { + return { ...t, frequencyValue: raw === '' ? '' : (parseInt(raw) || '') }; + } + return t; + }); this.setData({ newCareTasks: tasks }); }, @@ -223,10 +240,19 @@ Page({ return; } + // Validate care task periods + for (const task of newCareTasks) { + const p = parseInt(task.frequencyValue); + if (!p || p < 1) { + wx.showToast({ title: `"${task.taskName || '未命名事项'}" 的周期天数不合法`, icon: 'none' }); + return; + } + } + // Construct Care Plans const carePlans = newCareTasks.map(task => ({ name: task.taskName || '未命名事项', - period: task.frequencyValue || 1, + period: parseInt(task.frequencyValue) || 1, icon: JSON.stringify(task.taskIcon || {}) // Serialize icon details })); @@ -237,14 +263,14 @@ Page({ placement: newPlantLocation || '', ossIds: [uploadedImageId], carePlans: carePlans, - potMaterial: '', - potSize: '', - sunlight: '', - plantingMaterial: '' + potMaterial: this.data.potMaterial || '', + potSize: this.data.potSize || '', + sunlight: this.data.sunlight || '', + plantingMaterial: this.data.plantingMaterial || '' }; // Submit - wx.showLoading({ title: 'Creating...' }); + wx.showLoading({ title: '植物种植中...' }); request.post('/plant/add', payload).then(async () => { wx.hideLoading(); wx.showToast({ title: '添加成功', icon: 'success' }); diff --git a/pages/garden/add/index.wxml b/pages/garden/add/index.wxml index 96c5df1..85570c4 100644 --- a/pages/garden/add/index.wxml +++ b/pages/garden/add/index.wxml @@ -26,40 +26,92 @@ - - - 植物昵称 - - + + + + + 基本信息 - - - - 摆放位置 - - - - - - - 入家日期 - - - {{newPlantDate}} - + + + 植物昵称 + + - + + + + 摆放位置 + + + + + + + 入家日期 + + + {{newPlantDate}} + + + + - - - - 养护计划 + + + + + 养护环境 + + + + + 花盆材质 + + + + + + 花盆大小 + + + + + + + + + 光照条件 + + + + + + 植料/土壤 + + + + + + + + + + + + 养护计划 添加 + + + + + 暂无养护事项,点击右上角添加 + @@ -84,7 +136,7 @@ - + @@ -94,11 +146,9 @@ - - diff --git a/pages/garden/add/index.wxss b/pages/garden/add/index.wxss index 1d96940..5f7beef 100644 --- a/pages/garden/add/index.wxss +++ b/pages/garden/add/index.wxss @@ -5,7 +5,7 @@ page { } .add-plant-page { - background-color: #FFFFFF; + background-color: #F5F7F5; height: 100vh; display: flex; flex-direction: column; @@ -14,35 +14,20 @@ page { .page-content { height: calc(100vh - 140rpx - env(safe-area-inset-bottom)); - padding: 32rpx 40rpx; - background: #FFFFFF; + padding: 24rpx 32rpx; + background: #F5F7F5; box-sizing: border-box; } -/* Hide scrollbar - multiple approaches for compatibility */ -.page-content::-webkit-scrollbar { - display: none !important; - width: 0 !important; - height: 0 !important; - background: transparent !important; -} - -/* For scroll-view component */ ::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; } -scroll-view ::-webkit-scrollbar { - display: none !important; - width: 0 !important; - height: 0 !important; -} - -/* Upload Section */ +/* ======== Upload Section ======== */ .upload-section { - margin: 0 0 40rpx; + margin: 0 0 24rpx; display: flex; justify-content: center; } @@ -50,9 +35,9 @@ scroll-view ::-webkit-scrollbar { .image-upload-area { width: 100%; height: 240rpx; - border-radius: 32rpx; - border: 4rpx dashed #ddd; /* Match prototype dashed border */ - background: #fafafa; + border-radius: 28rpx; + border: 4rpx dashed #C5E1A5; + background: #FAFFF5; display: flex; flex-direction: column; align-items: center; @@ -62,21 +47,16 @@ scroll-view ::-webkit-scrollbar { transition: all 0.2s; } -.uploaded-img { - width: 100%; - height: 100%; - border-radius: 32rpx; /* Matches container */ - object-fit: cover; -} - -.image-upload-area:active { - border-color: #558B2F; /* var(--primary) */ - background: #F1F8E9; -} +.image-upload-area:active { opacity: 0.9; } .image-upload-area.has-image { border: none; - height: 360rpx; /* Taller when has image */ + height: 360rpx; +} + +.image-upload-area .uploaded-img { + width: 100%; + height: 100%; } .upload-placeholder { @@ -87,41 +67,74 @@ scroll-view ::-webkit-scrollbar { } .upload-placeholder text { - color: #BDBDBD; + color: #9CA3AF; font-size: 26rpx; } -/* Form Styles */ +/* ======== Section Cards ======== */ +.form-section { + background: #FFFFFF; + border-radius: 28rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03); +} + +.section-title-bar { + display: flex; + align-items: center; + gap: 12rpx; + margin-bottom: 28rpx; +} + +.section-dot { + width: 8rpx; + height: 32rpx; + border-radius: 4rpx; + background: linear-gradient(180deg, #558B2F, #689F38); +} + +.section-title-text { + font-size: 30rpx; + font-weight: 700; + color: #1F2937; + flex: 1; +} + +/* ======== Form Fields ======== */ .form-group { - margin-bottom: 40rpx; + margin-bottom: 28rpx; +} + +.form-group:last-child { + margin-bottom: 0; } .field-label { display: block; - font-size: 28rpx; + font-size: 26rpx; font-weight: 600; - color: #263238; - margin-bottom: 16rpx; + color: #374151; + margin-bottom: 12rpx; } .custom-input-box { - background: #f9f9f9; - border: 2rpx solid #e0e0e0; - border-radius: 24rpx; - padding: 24rpx 32rpx; - color: #263238; - font-size: 30rpx; + background: #F9FAFB; + border: 2rpx solid #E5E7EB; + border-radius: 20rpx; + padding: 22rpx 28rpx; + color: #1F2937; + font-size: 28rpx; transition: all 0.2s; } -.custom-input-box:active, .custom-input-box:focus-within { +.custom-input-box:focus-within { background: #FFFFFF; border-color: #558B2F; + box-shadow: 0 0 0 4rpx rgba(85, 139, 47, 0.1); } -.input-placeholder { - color: #999; -} +.input-placeholder { color: #9CA3AF; } .native-input { width: 100%; @@ -134,109 +147,93 @@ scroll-view ::-webkit-scrollbar { align-items: center; } -/* Care Section */ -.care-section-group { - margin-top: 24rpx; - margin-bottom: 48rpx; -} - -.section-header-row { +/* Two-column layout */ +.form-row { display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24rpx; + gap: 20rpx; + margin-bottom: 28rpx; } +.form-row:last-child { + margin-bottom: 0; +} + +.form-group.half { + flex: 1; + margin-bottom: 0; +} + +/* ======== Care Plan Section ======== */ .add-task-btn-small { - font-size: 26rpx; + font-size: 24rpx; color: #558B2F; background: #F1F8E9; - border: 2rpx dashed #558B2F; - padding: 12rpx 20rpx; + border: 2rpx dashed #A5D6A7; + padding: 10rpx 20rpx; border-radius: 16rpx; display: flex; align-items: center; - gap: 8rpx; + gap: 6rpx; + margin-left: auto; +} + +.add-task-btn-small:active { + background: #E8F5E9; +} + +/* Care empty state */ +.care-empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 48rpx 0 16rpx; + gap: 16rpx; +} + +.care-empty-text { + font-size: 24rpx; + color: #9CA3AF; } .care-list-styled { display: flex; flex-direction: column; - gap: 24rpx; + gap: 20rpx; } .care-row-styled { display: flex; align-items: center; - gap: 16rpx; + gap: 12rpx; } -.care-input-col.task-col { - flex: 1; -} - -.care-input-col.freq-col { - flex-shrink: 0; -} +.care-input-col.task-col { flex: 1; } +.care-input-col.freq-col { flex-shrink: 0; } .small-box { - padding: 24rpx; - background: #f9f9f9; + padding: 20rpx; + background: #F9FAFB; } .flex-row { display: flex; align-items: center; - padding-right: 20rpx; + padding-right: 16rpx; } -.center-text { - text-align: center; -} +.center-text { text-align: center; } .suffix-text { - color: #888; - font-size: 28rpx; + color: #9CA3AF; + font-size: 26rpx; font-weight: 500; } -.delete-btn-pink { - width: 84rpx; - height: 84rpx; - background: #FFEBEE; - border-radius: 24rpx; - display: flex; - align-items: center; - justify-content: center; - color: #EF5350; - flex-shrink: 0; -} - -/* Footer */ -.page-footer { - position: fixed; - bottom: 0; - left: 0; - right: 0; - padding: 32rpx 40rpx calc(32rpx + env(safe-area-inset-bottom)); - background: white; - z-index: 100; - box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.02); -} - -/* Footer Button */ -.page-footer t-button { - --td-button-font-weight: 600; - --td-button-primary-bg-color: #558B2F; - --td-button-primary-border-color: #558B2F; - box-shadow: 0 8rpx 32rpx rgba(85, 139, 47, 0.3); -} - /* Care Icon Button */ .care-icon-btn { - width: 84rpx; - height: 84rpx; - border-radius: 24rpx; + width: 80rpx; + height: 80rpx; + border-radius: 20rpx; display: flex; align-items: center; justify-content: center; @@ -244,17 +241,49 @@ scroll-view ::-webkit-scrollbar { transition: all 0.2s; } -.care-icon-btn:active { +.care-icon-btn:active { transform: scale(0.95); } + +.delete-btn-pink { + width: 80rpx; + height: 80rpx; + background: #FFF5F5; + border-radius: 20rpx; + display: flex; + align-items: center; + justify-content: center; + color: #EF5350; + flex-shrink: 0; + transition: all 0.15s; +} + +.delete-btn-pink:active { + background: #FFEBEE; transform: scale(0.95); } -/* Icon Picker Popup */ -.icon-picker-mask { +/* ======== Footer ======== */ +.page-footer { position: fixed; - top: 0; + bottom: 0; left: 0; right: 0; - bottom: 0; + padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom)); + background: white; + z-index: 100; + box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.03); +} + +.page-footer t-button { + --td-button-font-weight: 600; + --td-button-primary-bg-color: #558B2F; + --td-button-primary-border-color: #558B2F; + box-shadow: 0 8rpx 32rpx rgba(85, 139, 47, 0.3); +} + +/* ======== Icon Picker Popup ======== */ +.icon-picker-mask { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 1000; opacity: 0; @@ -269,9 +298,7 @@ scroll-view ::-webkit-scrollbar { .icon-picker-popup { position: fixed; - left: 0; - right: 0; - bottom: 0; + left: 0; right: 0; bottom: 0; background: #fff; border-radius: 32rpx 32rpx 0 0; z-index: 1001; @@ -280,9 +307,7 @@ scroll-view ::-webkit-scrollbar { padding-bottom: env(safe-area-inset-bottom); } -.icon-picker-popup.show { - transform: translateY(0); -} +.icon-picker-popup.show { transform: translateY(0); } .icon-picker-header { display: flex; @@ -323,9 +348,7 @@ scroll-view ::-webkit-scrollbar { padding: 20rpx 0; } -.icon-picker-item:active { - opacity: 0.7; -} +.icon-picker-item:active { opacity: 0.7; } .icon-circle { width: 96rpx; diff --git a/pages/plant-detail/edit/index.js b/pages/plant-detail/edit/index.js index 5b9e8af..006d8c8 100644 --- a/pages/plant-detail/edit/index.js +++ b/pages/plant-detail/edit/index.js @@ -12,7 +12,6 @@ Page({ isLocalImage: false, uploadedImageId: '', - // Extra fields requested by user struct potMaterial: '', potSize: '', sunlight: '', @@ -27,7 +26,6 @@ Page({ { label: '从手机相册选取', value: 'album' } ], - // Icon picker careTaskIcons: [], showIconPicker: false, currentEditingTaskId: null @@ -35,44 +33,26 @@ Page({ onLoad(options) { const { id } = options; - if (!id) { - wx.navigateBack(); - return; - } + if (!id) { wx.navigateBack(); return; } - this.setData({ - plantId: id, - careTaskIcons: CARE_TASK_ICONS - }); + this.setData({ plantId: id, careTaskIcons: CARE_TASK_ICONS }); - // Try to receive data from opener page first const eventChannel = this.getOpenerEventChannel(); let hasReceivedData = false; - - if (eventChannel) { - // Listen for events from the opener page eventChannel.on('acceptDataFromOpenerPage', (data) => { - if (data && data.plant) { hasReceivedData = true; - // Directly render with passed data this.renderPlantUI(data.plant); } }); } const isFromDetail = options.source === 'detail'; - - // If from detail, wait longer for event channel. If direct open, fetch immediately. - const waitTime = isFromDetail ? 2000 : 0; - setTimeout(() => { - if (!hasReceivedData) { - this.fetchPlantDetail(id); - } - }, waitTime); + if (!hasReceivedData) this.fetchPlantDetail(id); + }, isFromDetail ? 2000 : 0); }, fetchPlantDetail(id) { @@ -86,57 +66,37 @@ Page({ renderPlantUI(plant) { const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'water') || CARE_TASK_ICONS[0]; - // Parse carePlans (careSchedule is from detail UI structure, carePlans is from backend) - // If passed from detail page, it might already have 'careSchedule' with parsed icons. - // But backend structure 'carePlans' needs parsing. - // Let's handle both cases robustly. - let tasks = []; if (plant.careSchedule) { - // Data from Detail Page tasks = plant.careSchedule.map(cp => ({ - id: cp.id, - name: cp.name, - period: cp.period, - taskIcon: cp.taskIcon + id: cp.id, name: cp.name, period: cp.period, + taskIcon: cp.taskIcon, isNew: false, + _original: { name: cp.name, period: cp.period, icon: JSON.stringify(cp.taskIcon || {}) } })); } else if (plant.carePlans) { - // Data from Backend tasks = plant.carePlans.map(cp => { let iconObj = defaultIcon; if (typeof cp.icon === 'string' && cp.icon.startsWith('{')) { - try { - iconObj = JSON.parse(cp.icon); - } catch (e) { } + try { iconObj = JSON.parse(cp.icon); } catch (e) { } } + const iconStr = JSON.stringify(iconObj); return { - id: cp.id, - name: cp.name, - period: cp.period, - taskIcon: iconObj + id: cp.id, name: cp.name, period: cp.period, + taskIcon: iconObj, isNew: false, + _original: { name: cp.name, period: cp.period, icon: iconStr } }; }); } - // Map images: get first one if exists and handle path resolution - let imageUrl = ''; - let imageId = ''; - + let imageUrl = '', imageId = ''; if (plant.imgList && plant.imgList.length > 0) { imageUrl = plant.imgList[0].url || ''; imageId = plant.imgList[0].id || ''; } - // Path resolution for local vs remote - if (imageUrl && !imageUrl.startsWith('http') && !imageUrl.startsWith('/') && !imageUrl.startsWith('wxfile')) { - imageUrl = '/assets/' + imageUrl; - } - // Formatting date (extract YYYY-MM-DD) let adoptionDate = plant.plantTime || ''; - if (adoptionDate.includes('T')) { - adoptionDate = adoptionDate.split('T')[0]; - } + if (adoptionDate.includes('T')) adoptionDate = adoptionDate.split('T')[0]; this.setData({ newPlantName: plant.name || '', @@ -150,20 +110,23 @@ Page({ plantingMaterial: plant.plantingMaterial || '', newCareTasks: tasks }); + + // Store original base fields for change detection + this._originalPlant = { + name: plant.name || '', + placement: plant.placement || '', + potMaterial: plant.potMaterial || '', + potSize: plant.potSize || '', + sunlight: plant.sunlight || '', + plantingMaterial: plant.plantingMaterial || '' + }; }, - handleBack() { - wx.navigateBack(); - }, - - showActionSheet() { - this.setData({ showActionSheet: true }); - }, - - onActionSheetCancel() { - this.setData({ showActionSheet: false }); - }, + handleBack() { wx.navigateBack(); }, + // ======== Image Upload ======== + showActionSheet() { this.setData({ showActionSheet: true }); }, + onActionSheetCancel() { this.setData({ showActionSheet: false }); }, onActionSheetSelected(e) { const { value } = e.detail.selected; this.handleImageUpload(value); @@ -172,28 +135,18 @@ Page({ handleImageUpload(sourceType) { wx.chooseMedia({ - count: 1, - mediaType: ['image'], - sourceType: [sourceType], - camera: 'back', + count: 1, mediaType: ['image'], sourceType: [sourceType], camera: 'back', success: (res) => { const tempFilePath = res.tempFiles[0].tempFilePath; - this.setData({ - newPlantImage: tempFilePath, - isLocalImage: true - }); - - wx.showLoading({ title: '上传图片...' }); + this.setData({ newPlantImage: tempFilePath, isLocalImage: true }); + wx.showLoading({ title: '上传中...' }); request.upload(tempFilePath).then(data => { wx.hideLoading(); const fileData = data?.file || {}; if (fileData.id) { - this.setData({ - uploadedImageId: fileData.id, - newPlantImage: fileData.url - }); + this.setData({ uploadedImageId: fileData.id, newPlantImage: fileData.url }); } - }).catch(err => { + }).catch(() => { wx.hideLoading(); wx.showToast({ title: '上传失败', icon: 'none' }); }); @@ -201,102 +154,113 @@ Page({ }); }, + // ======== Form Inputs ======== onNameInput(e) { this.setData({ newPlantName: e.detail.value }); }, onLocationInput(e) { this.setData({ newPlantLocation: e.detail.value }); }, onDateChange(e) { this.setData({ newPlantDate: e.detail.value }); }, - - // Extra field inputs onPotMaterialInput(e) { this.setData({ potMaterial: e.detail.value }); }, onPotSizeInput(e) { this.setData({ potSize: e.detail.value }); }, onSunlightInput(e) { this.setData({ sunlight: e.detail.value }); }, onPlantingMaterialInput(e) { this.setData({ plantingMaterial: e.detail.value }); }, + // ======== Care Plan: Local Add ======== handleAddCareTask() { - const tasks = this.data.newCareTasks; const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'other') || CARE_TASK_ICONS[0]; - - tasks.push({ + const tasks = [...this.data.newCareTasks, { id: 'new_' + Date.now(), name: '', - period: 1, + period: 7, iconId: 'other', - taskIcon: defaultIcon - }); + taskIcon: defaultIcon, + isNew: true // Mark as new, not yet saved to backend + }]; - this.setData({ - newCareTasks: tasks, - scrollIntoViewId: '' - }, () => { + this.setData({ newCareTasks: tasks, scrollIntoViewId: '' }, () => { setTimeout(() => { this.setData({ scrollIntoViewId: 'care-list-bottom' }); }, 50); }); }, + // ======== Care Plan: Delete ======== handleRemoveCareTask(e) { const id = e.currentTarget.dataset.id; - const tasks = this.data.newCareTasks.filter(t => t.id !== id); - this.setData({ newCareTasks: tasks }); + const task = this.data.newCareTasks.find(t => t.id === id); + + // New (unsaved) tasks: just remove locally + if (task && task.isNew) { + const tasks = this.data.newCareTasks.filter(t => t.id !== id); + this.setData({ newCareTasks: tasks }); + return; + } + + // Existing tasks: confirm then call API + wx.showModal({ + title: '确认删除', + content: '确定要删除这个养护事项吗?', + confirmColor: '#EF5350', + success: (res) => { + if (!res.confirm) return; + wx.showLoading({ title: '删除中...' }); + request.get('/plant/plan/delete', { id: id }).then(() => { + wx.hideLoading(); + const tasks = this.data.newCareTasks.filter(t => t.id !== id); + this.setData({ newCareTasks: tasks }); + wx.showToast({ title: '已删除', icon: 'success' }); + }).catch(err => { + wx.hideLoading(); + console.error('Delete care plan failed', err); + }); + } + }); }, + // ======== Care Task Inline Editing ======== onTaskNameInput(e) { const { id } = e.currentTarget.dataset; - const tasks = this.data.newCareTasks.map(t => t.id === id ? { ...t, name: e.detail.value } : t); + const tasks = this.data.newCareTasks.map(t => + t.id === id ? { ...t, name: e.detail.value } : t + ); this.setData({ newCareTasks: tasks }); }, onTaskFreqInput(e) { const { id } = e.currentTarget.dataset; - const tasks = this.data.newCareTasks.map(t => t.id === id ? { ...t, period: parseInt(e.detail.value) || 1 } : t); + const raw = e.detail.value; + const val = raw === '' ? '' : (parseInt(raw) || ''); + const tasks = this.data.newCareTasks.map(t => + t.id === id ? { ...t, period: val } : t + ); this.setData({ newCareTasks: tasks }); }, + // ======== Icon Picker ======== showIconPickerForTask(e) { - const taskId = e.currentTarget.dataset.id; - this.setData({ - showIconPicker: true, - currentEditingTaskId: taskId - }); + this.setData({ showIconPicker: true, currentEditingTaskId: e.currentTarget.dataset.id }); }, - hideIconPicker() { - this.setData({ - showIconPicker: false, - currentEditingTaskId: null - }); + this.setData({ showIconPicker: false, currentEditingTaskId: null }); }, - selectIcon(e) { const iconId = e.currentTarget.dataset.iconid; const { currentEditingTaskId, careTaskIcons, newCareTasks } = this.data; - const selectedIcon = careTaskIcons.find(i => i.id === iconId); - if (selectedIcon && currentEditingTaskId) { const updatedTasks = newCareTasks.map(t => { if (t.id === currentEditingTaskId) { - return { - ...t, - iconId: iconId, - taskIcon: selectedIcon, - name: t.name || selectedIcon.name - }; + return { ...t, iconId, taskIcon: selectedIcon, name: t.name || selectedIcon.name }; } return t; }); - - this.setData({ - newCareTasks: updatedTasks, - showIconPicker: false, - currentEditingTaskId: null - }); + this.setData({ newCareTasks: updatedTasks, showIconPicker: false, currentEditingTaskId: null }); } }, + // ======== Save All ======== handleSavePlant() { const { plantId, newPlantName, newPlantLocation, potMaterial, potSize, - sunlight, plantingMaterial + sunlight, plantingMaterial, newCareTasks } = this.data; if (!newPlantName) { @@ -304,23 +268,93 @@ Page({ return; } - const payload = { + // Validate all care task periods + for (const task of newCareTasks) { + const p = parseInt(task.period); + if (!p || p < 1) { + wx.showToast({ title: `"${task.name || '未命名事项'}" 的周期天数不合法`, icon: 'none' }); + return; + } + if (!task.name) { + wx.showToast({ title: '请填写所有养护事项名称', icon: 'none' }); + return; + } + } + + // Split tasks into existing and new + const existingTasks = newCareTasks.filter(t => !t.isNew); + const newTasks = newCareTasks.filter(t => t.isNew); + + // Only include MODIFIED existing tasks in carePlans + const modifiedPlans = existingTasks.filter(task => { + if (!task._original) return false; + const currentIcon = JSON.stringify(task.taskIcon || {}); + return task.name !== task._original.name || + parseInt(task.period) !== task._original.period || + currentIcon !== task._original.icon; + }).map(task => ({ + id: String(task.id), + name: task.name, + period: parseInt(task.period) || 1, + icon: JSON.stringify(task.taskIcon || {}) + })); + + // Build payload for /plant/update (UpdateMyPlant struct) + const updatePayload = { id: plantId, name: newPlantName, placement: newPlantLocation || '', potMaterial: potMaterial || '', potSize: potSize || '', sunlight: sunlight || '', - plantingMaterial: plantingMaterial || '' + plantingMaterial: plantingMaterial || '', + carePlans: modifiedPlans }; - request.post('/plant/update', payload).then(() => { - wx.showToast({ title: '修改成功', icon: 'success' }); - setTimeout(() => { - wx.navigateBack(); - }, 1000); + // Build payload for /plant/plan/add if there are new tasks (AddPlans struct) + const addPayload = newTasks.length > 0 ? { + carePlan: newTasks.map(task => ({ + plantId: plantId, + name: task.name, + period: parseInt(task.period) || 1, + icon: JSON.stringify(task.taskIcon || {}) + })) + } : null; + + // Check if base fields changed + const orig = this._originalPlant || {}; + const baseChanged = newPlantName !== orig.name || + (newPlantLocation || '') !== orig.placement || + (potMaterial || '') !== orig.potMaterial || + (potSize || '') !== orig.potSize || + (sunlight || '') !== orig.sunlight || + (plantingMaterial || '') !== orig.plantingMaterial; + + const needUpdate = baseChanged || modifiedPlans.length > 0; + const needAdd = addPayload !== null; + + if (!needUpdate && !needAdd) { + wx.showToast({ title: '没有修改', icon: 'none' }); + return; + } + + wx.showLoading({ title: '保存中...' }); + + const promises = []; + if (needUpdate) { + promises.push(request.post('/plant/update', updatePayload)); + } + if (needAdd) { + promises.push(request.post('/plant/plan/add', addPayload)); + } + + Promise.all(promises).then(() => { + wx.hideLoading(); + wx.showToast({ title: '保存成功', icon: 'success' }); + setTimeout(() => { wx.navigateBack(); }, 1000); }).catch(err => { - console.error('Update plant failed', err); + wx.hideLoading(); + console.error('Save failed', err); }); }, @@ -331,13 +365,8 @@ Page({ confirmColor: '#EF5350', success: (res) => { if (res.confirm) { - // Assuming there might be a delete API later, but user didn't provide one. - // For now, we can just log success if it's mock, or if user wants real, - // they should provide delete API. I'll just keep the UI feedback. wx.showToast({ title: '已删除', icon: 'success' }); - setTimeout(() => { - wx.switchTab({ url: '/pages/garden/index' }); - }, 1000); + setTimeout(() => { wx.switchTab({ url: '/pages/garden/index' }); }, 1000); } } }); diff --git a/pages/plant-detail/edit/index.wxml b/pages/plant-detail/edit/index.wxml index 2786418..810f62d 100644 --- a/pages/plant-detail/edit/index.wxml +++ b/pages/plant-detail/edit/index.wxml @@ -19,14 +19,10 @@ height="100%" t-class="uploaded-img" /> - - 点击设置封面图 - - 更换照片 @@ -34,72 +30,98 @@ - - - 植物昵称 - - + + + + + 基本信息 - - - - 摆放位置 - - - - - - - 入家日期 - - - {{newPlantDate}} - + + + 植物昵称 + + - - - - - - 花盆材质 - - + + + + 摆放位置 + + + + + + + 入家日期 + + + {{newPlantDate}} + + + - - 花盆大小 - - + + + + + 养护环境 + + + + + 花盆材质 + + + + + + 花盆大小 + + + + + + + + + 光照条件 + + + + + + 植料/土壤 + + + + - - 光照条件 - - - - - - - 植料/土壤 - - - - - - - - - 养护计划 + + + + + 养护计划 添加 + + + + 暂无养护事项,点击右上角添加 + + - + + + + - + @@ -131,19 +153,17 @@ - - + 删除植物档案 - diff --git a/pages/plant-detail/edit/index.wxss b/pages/plant-detail/edit/index.wxss index 09d21dd..0d856f9 100644 --- a/pages/plant-detail/edit/index.wxss +++ b/pages/plant-detail/edit/index.wxss @@ -5,7 +5,7 @@ page { } .add-plant-page { - background-color: #FFFFFF; + background-color: #F5F7F5; height: 100vh; display: flex; flex-direction: column; @@ -14,21 +14,20 @@ page { .page-content { height: calc(100vh - 140rpx - env(safe-area-inset-bottom)); - padding: 32rpx 40rpx; - background: #FFFFFF; + padding: 24rpx 32rpx; + background: #F5F7F5; box-sizing: border-box; } -/* Hide scrollbar */ ::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; } -/* Upload Section */ +/* ======== Upload Section ======== */ .upload-section { - margin: 0 0 40rpx; + margin: 0 0 24rpx; display: flex; justify-content: center; } @@ -36,9 +35,9 @@ page { .image-upload-area { width: 100%; height: 240rpx; - border-radius: 32rpx; - border: 4rpx dashed #ddd; - background: #fafafa; + border-radius: 28rpx; + border: 4rpx dashed #C5E1A5; + background: #FAFFF5; display: flex; flex-direction: column; align-items: center; @@ -48,9 +47,7 @@ page { transition: all 0.2s; } -.image-upload-area:active { - opacity: 0.9; -} +.image-upload-area:active { opacity: 0.9; } .image-upload-area.has-image { border: none; @@ -70,11 +67,10 @@ page { } .upload-placeholder text { - color: #BDBDBD; + color: #9CA3AF; font-size: 26rpx; } -/* Edit Overlay Hint */ .edit-overlay { position: absolute; bottom: 24rpx; @@ -86,42 +82,75 @@ page { display: flex; align-items: center; gap: 8rpx; - color: #FFFFFF; + color: #FFF; font-size: 24rpx; font-weight: 500; } -/* Form Styles */ +/* ======== Section Cards ======== */ +.form-section { + background: #FFFFFF; + border-radius: 28rpx; + padding: 32rpx; + margin-bottom: 24rpx; + box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03); +} + +.section-title-bar { + display: flex; + align-items: center; + gap: 12rpx; + margin-bottom: 28rpx; +} + +.section-dot { + width: 8rpx; + height: 32rpx; + border-radius: 4rpx; + background: linear-gradient(180deg, #558B2F, #689F38); +} + +.section-title-text { + font-size: 30rpx; + font-weight: 700; + color: #1F2937; + flex: 1; +} + +/* ======== Form Fields ======== */ .form-group { - margin-bottom: 40rpx; + margin-bottom: 28rpx; +} + +.form-group:last-child { + margin-bottom: 0; } .field-label { display: block; - font-size: 28rpx; + font-size: 26rpx; font-weight: 600; - color: #263238; - margin-bottom: 16rpx; + color: #374151; + margin-bottom: 12rpx; } .custom-input-box { - background: #f9f9f9; - border: 2rpx solid #e0e0e0; - border-radius: 24rpx; - padding: 24rpx 32rpx; - color: #263238; - font-size: 30rpx; + background: #F9FAFB; + border: 2rpx solid #E5E7EB; + border-radius: 20rpx; + padding: 22rpx 28rpx; + color: #1F2937; + font-size: 28rpx; transition: all 0.2s; } .custom-input-box:focus-within { background: #FFFFFF; border-color: #558B2F; + box-shadow: 0 0 0 4rpx rgba(85, 139, 47, 0.1); } -.input-placeholder { - color: #999; -} +.input-placeholder { color: #9CA3AF; } .native-input { width: 100%; @@ -134,113 +163,171 @@ page { align-items: center; } -/* Care Section */ -.care-section-group { - margin-top: 24rpx; - margin-bottom: 48rpx; -} - -.section-header-row { +/* Two-column layout */ +.form-row { display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24rpx; + gap: 20rpx; + margin-bottom: 28rpx; } +.form-row:last-child { + margin-bottom: 0; +} + +.form-group.half { + flex: 1; + margin-bottom: 0; +} + +/* ======== Care Plan Section ======== */ .add-task-btn-small { - font-size: 26rpx; + font-size: 24rpx; color: #558B2F; background: #F1F8E9; - border: 2rpx dashed #558B2F; - padding: 12rpx 20rpx; + border: 2rpx dashed #A5D6A7; + padding: 10rpx 20rpx; border-radius: 16rpx; display: flex; align-items: center; - gap: 8rpx; + gap: 6rpx; + margin-left: auto; +} + +.add-task-btn-small:active { + background: #E8F5E9; +} + +/* Care empty state */ +.care-empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 48rpx 0 16rpx; + gap: 16rpx; +} + +.care-empty-text { + font-size: 24rpx; + color: #9CA3AF; } .care-list-styled { display: flex; flex-direction: column; - gap: 24rpx; + gap: 20rpx; } .care-row-styled { display: flex; align-items: center; - gap: 16rpx; + gap: 12rpx; + position: relative; } -.care-input-col.task-col { - flex: 1; +/* New task highlight */ +.care-row-new { + background: #FAFFF5; + border: 2rpx dashed #C5E1A5; + border-radius: 24rpx; + padding: 12rpx; + margin: -12rpx; + margin-bottom: 0; } -.care-input-col.freq-col { - flex-shrink: 0; +.new-badge { + position: absolute; + top: -4rpx; + right: -4rpx; + background: linear-gradient(135deg, #558B2F, #689F38); + color: #fff; + font-size: 18rpx; + font-weight: 700; + padding: 2rpx 12rpx; + border-radius: 12rpx 24rpx 12rpx 12rpx; + z-index: 1; } +.care-input-col.task-col { flex: 1; } +.care-input-col.freq-col { flex-shrink: 0; } + .small-box { - padding: 24rpx; - background: #f9f9f9; + padding: 20rpx; + background: #F9FAFB; } .flex-row { display: flex; align-items: center; - padding-right: 20rpx; + padding-right: 16rpx; } -.center-text { - text-align: center; -} +.center-text { text-align: center; } .suffix-text { - color: #888; - font-size: 28rpx; + color: #9CA3AF; + font-size: 26rpx; font-weight: 500; } +/* Care Icon Button */ +.care-icon-btn { + width: 80rpx; + height: 80rpx; + border-radius: 20rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all 0.2s; +} + +.care-icon-btn:active { transform: scale(0.95); } + .delete-btn-pink { - width: 84rpx; - height: 84rpx; - background: #FFEBEE; - border-radius: 24rpx; + width: 80rpx; + height: 80rpx; + background: #FFF5F5; + border-radius: 20rpx; display: flex; align-items: center; justify-content: center; color: #EF5350; flex-shrink: 0; + transition: all 0.15s; } -/* Delete Button for Edit Page */ +.delete-btn-pink:active { + background: #FFEBEE; + transform: scale(0.95); +} + +/* ======== Delete Plant ======== */ .delete-page-btn { display: flex; align-items: center; justify-content: center; gap: 12rpx; - color: #FF5252; + color: #EF5350; font-size: 28rpx; font-weight: 500; - padding: 20rpx; + padding: 24rpx; border: 2rpx solid #FFCDD2; border-radius: 24rpx; background: #FFF9F9; } -.delete-page-btn:active { - background: #FFEBEE; -} +.delete-page-btn:active { background: #FFEBEE; } -/* Footer */ +/* ======== Footer ======== */ .page-footer { position: fixed; bottom: 0; left: 0; right: 0; - padding: 32rpx 40rpx calc(32rpx + env(safe-area-inset-bottom)); + padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom)); background: white; z-index: 100; - box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.02); + box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.03); } .page-footer t-button { @@ -250,29 +337,10 @@ page { box-shadow: 0 8rpx 32rpx rgba(85, 139, 47, 0.3); } -/* Care Icon Button */ -.care-icon-btn { - width: 84rpx; - height: 84rpx; - border-radius: 24rpx; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - transition: all 0.2s; -} - -.care-icon-btn:active { - transform: scale(0.95); -} - -/* Icon Picker Popup */ +/* ======== Icon Picker Popup ======== */ .icon-picker-mask { position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; + top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 1000; opacity: 0; @@ -287,9 +355,7 @@ page { .icon-picker-popup { position: fixed; - left: 0; - right: 0; - bottom: 0; + left: 0; right: 0; bottom: 0; background: #fff; border-radius: 32rpx 32rpx 0 0; z-index: 1001; @@ -298,9 +364,7 @@ page { padding-bottom: env(safe-area-inset-bottom); } -.icon-picker-popup.show { - transform: translateY(0); -} +.icon-picker-popup.show { transform: translateY(0); } .icon-picker-header { display: flex; @@ -341,9 +405,7 @@ page { padding: 20rpx 0; } -.icon-picker-item:active { - opacity: 0.7; -} +.icon-picker-item:active { opacity: 0.7; } .icon-circle { width: 96rpx; diff --git a/pages/wiki/identify/index.js b/pages/wiki/identify/index.js new file mode 100644 index 0000000..c221ac9 --- /dev/null +++ b/pages/wiki/identify/index.js @@ -0,0 +1,88 @@ +// pages/wiki/identify/index.js +import request from '../../../utils/request'; + +Page({ + data: { + imagePath: '', + results: [], + isLoading: true, + hasError: false, + topResult: null + }, + + onLoad(options) { + // Image path is passed via global data (too long for URL params) + const app = getApp(); + const imagePath = app.globalData._identifyImagePath || ''; + + if (!imagePath) { + this.setData({ isLoading: false, hasError: true }); + return; + } + + this.setData({ imagePath }); + this.classifyPlant(imagePath); + }, + + classifyPlant(filePath) { + this.setData({ isLoading: true, hasError: false }); + + request.uploadToUrl('/classify/plant', filePath, 'file').then(res => { + const results = res.result || []; + + // Map results with percentage scores + const mappedResults = results.map((item, index) => ({ + index: index, + name: item.name, + score: item.score, + percent: (item.score * 100).toFixed(2), + description: (item.baike_info && item.baike_info.description) || '', + baikeUrl: (item.baike_info && item.baike_info.baike_url) || '', + isTop: index === 0 + })); + + this.setData({ + results: mappedResults, + topResult: mappedResults.length > 0 ? mappedResults[0] : null, + isLoading: false + }); + }).catch(err => { + console.error('Classify failed', err); + this.setData({ isLoading: false, hasError: true }); + }); + }, + + // Retry identification + handleRetry() { + if (this.data.imagePath) { + this.classifyPlant(this.data.imagePath); + } + }, + + // Go back and re-select image + handleReselect() { + wx.navigateBack(); + }, + + // Search in wiki + searchInWiki(e) { + const name = e.currentTarget.dataset.name; + // Navigate back to wiki and trigger search + const pages = getCurrentPages(); + if (pages.length >= 2) { + const wikiPage = pages[pages.length - 2]; + wikiPage.setData({ searchQuery: name }, () => { + wikiPage.fetchWikiList(true); + }); + } + wx.navigateBack(); + }, + + // Preview uploaded image + previewImage() { + wx.previewImage({ + urls: [this.data.imagePath], + current: this.data.imagePath + }); + } +}); diff --git a/pages/wiki/identify/index.json b/pages/wiki/identify/index.json new file mode 100644 index 0000000..6cc5381 --- /dev/null +++ b/pages/wiki/identify/index.json @@ -0,0 +1,10 @@ +{ + "navigationBarTitleText": "识别结果", + "usingComponents": { + "t-icon": "tdesign-miniprogram/icon/icon", + "t-tag": "tdesign-miniprogram/tag/tag", + "t-loading": "tdesign-miniprogram/loading/loading", + "t-button": "tdesign-miniprogram/button/button", + "t-empty": "tdesign-miniprogram/empty/empty" + } +} \ No newline at end of file diff --git a/pages/wiki/identify/index.wxml b/pages/wiki/identify/index.wxml new file mode 100644 index 0000000..d12e746 --- /dev/null +++ b/pages/wiki/identify/index.wxml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + 正在识别中... + AI 正在分析植物特征 + + + + + + + + + + + 识别失败 + 请检查网络连接后重试 + + + + 重新识别 + + + + 返回重选 + + + + + + + + + + + + + + + + + 匹配度 {{topResult.percent}}% + + {{topResult.name}} + + + + + + + + 1 + + {{topResult.name}} + 置信度 {{topResult.percent}}% + + + + + + + + + + + {{topResult.description}} + + + + + 在百科中搜索 + + + + + + + + 其他可能的结果 + + + {{index + 1}} + + {{item.name}} + + + + + {{item.percent}}% + + + + + + + + + + + + 重新识别 + + + + + + diff --git a/pages/wiki/identify/index.wxss b/pages/wiki/identify/index.wxss new file mode 100644 index 0000000..aad983c --- /dev/null +++ b/pages/wiki/identify/index.wxss @@ -0,0 +1,407 @@ +/* pages/wiki/identify/index.wxss */ + +.identify-page { + min-height: 100vh; + background: #F5F7F5; +} + +/* ========== Shared State Container ========== */ +.state-container { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 48rpx; +} + +.state-card { + background: #fff; + border-radius: 40rpx; + padding: 56rpx 48rpx; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 32rpx; + box-shadow: 0 12rpx 40rpx rgba(85, 139, 47, 0.08); +} + +/* ========== Loading State ========== */ +.loading-image-wrap { + width: 280rpx; + height: 280rpx; + border-radius: 32rpx; + overflow: hidden; + position: relative; + box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.1); +} + +.loading-preview { + width: 100%; + height: 100%; + display: block; +} + +/* Scan line animation */ +.scan-line { + position: absolute; + left: 0; + right: 0; + height: 4rpx; + background: linear-gradient(90deg, transparent, #558B2F, transparent); + animation: scan 2s ease-in-out infinite; + box-shadow: 0 0 16rpx rgba(85, 139, 47, 0.5); +} + +@keyframes scan { + 0% { top: 0; } + 50% { top: 100%; } + 100% { top: 0; } +} + +.loading-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 12rpx; +} + +/* Animated dots */ +.loading-dots { + display: flex; + gap: 12rpx; + margin-bottom: 8rpx; +} + +.dot { + width: 16rpx; + height: 16rpx; + border-radius: 50%; + background: #558B2F; + animation: dotPulse 1.4s infinite ease-in-out; +} + +.dot2 { animation-delay: 0.2s; } +.dot3 { animation-delay: 0.4s; } + +@keyframes dotPulse { + 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } + 40% { transform: scale(1); opacity: 1; } +} + +.state-title { + font-size: 34rpx; + font-weight: 700; + color: #1F2937; +} + +.state-hint { + font-size: 26rpx; + color: #9CA3AF; +} + +/* ========== Error State ========== */ +.error-icon-wrap { + width: 140rpx; + height: 140rpx; + border-radius: 50%; + background: #FFF5F5; + display: flex; + align-items: center; + justify-content: center; +} + +.state-actions { + display: flex; + gap: 24rpx; + margin-top: 16rpx; + width: 100%; + justify-content: center; +} + +/* ========== Shared Action Buttons ========== */ +.action-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 10rpx; + padding: 20rpx 36rpx; + border-radius: 48rpx; + font-size: 28rpx; + font-weight: 600; + transition: all 0.2s; +} + +.action-btn:active { + transform: scale(0.96); +} + +.action-btn.primary { + background: linear-gradient(135deg, #558B2F, #689F38); + color: #fff; + box-shadow: 0 8rpx 24rpx rgba(85, 139, 47, 0.25); +} + +.action-btn.primary text { + color: #fff; +} + +.action-btn.outline { + background: #fff; + color: #558B2F; + border: 2rpx solid #C5E1A5; +} + +.action-btn.outline text { + color: #558B2F; +} + +.action-btn.full { + width: 100%; +} + +/* ========== Results: Hero Section ========== */ +.results-scroll { + height: 100vh; +} + +.hero-section { + position: relative; + height: 480rpx; + overflow: hidden; +} + +.hero-image { + width: 100%; + height: 100%; + display: block; +} + +.hero-gradient { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 280rpx; + background: linear-gradient(180deg, transparent, rgba(0,0,0,0.65)); +} + +.hero-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 40rpx; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 8rpx; + background: rgba(85, 139, 47, 0.85); + backdrop-filter: blur(8rpx); + color: #fff; + font-size: 24rpx; + font-weight: 600; + padding: 8rpx 20rpx; + border-radius: 24rpx; + margin-bottom: 12rpx; +} + +.hero-badge text { + color: #fff; +} + +.hero-plant-name { + font-size: 52rpx; + font-weight: 800; + color: #fff; + display: block; + text-shadow: 0 4rpx 12rpx rgba(0,0,0,0.3); +} + +/* ========== Results: Detail Card ========== */ +.detail-card-wrapper { + padding: 0 32rpx; + margin-top: -40rpx; + position: relative; + z-index: 10; +} + +.detail-card { + background: #fff; + border-radius: 32rpx; + padding: 36rpx; + box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.06); +} + +.detail-card-header { + display: flex; + align-items: center; + gap: 20rpx; + margin-bottom: 24rpx; +} + +/* Rank badge */ +.result-rank { + width: 56rpx; + height: 56rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + font-weight: 800; + flex-shrink: 0; +} + +.result-rank.best { + background: linear-gradient(135deg, #558B2F, #689F38); + color: #fff; + box-shadow: 0 6rpx 16rpx rgba(85, 139, 47, 0.3); +} + +.result-rank.normal { + background: #F3F4F6; + color: #6B7280; +} + +.result-name-area { + flex: 1; +} + +.result-main-name { + font-size: 36rpx; + font-weight: 800; + color: #1F2937; + display: block; + margin-bottom: 4rpx; +} + +.confidence-text { + font-size: 24rpx; + color: #558B2F; + font-weight: 600; +} + +/* Confidence Bar */ +.confidence-bar-wrap { + margin-bottom: 24rpx; +} + +.confidence-bar-bg { + height: 12rpx; + background: #F3F4F6; + border-radius: 6rpx; + overflow: hidden; +} + +.confidence-bar-fill { + height: 100%; + background: linear-gradient(90deg, #689F38, #558B2F); + border-radius: 6rpx; + transition: width 0.8s ease-out; +} + +.result-description { + font-size: 26rpx; + color: #6B7280; + line-height: 1.8; + display: block; + margin-bottom: 28rpx; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 5; + -webkit-box-orient: vertical; +} + +.detail-card-actions { + display: flex; + gap: 16rpx; +} + +/* ========== Results: Other Results ========== */ +.other-section { + padding: 32rpx 32rpx 0; +} + +.section-title { + font-size: 30rpx; + font-weight: 700; + color: #374151; + display: block; + margin-bottom: 20rpx; + padding-left: 8rpx; +} + +.other-list { + background: #fff; + border-radius: 28rpx; + overflow: hidden; + box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03); +} + +.other-item { + display: flex; + align-items: center; + padding: 28rpx 32rpx; + gap: 20rpx; + border-bottom: 1rpx solid #F3F4F6; + transition: background 0.15s; +} + +.other-item:last-child { + border-bottom: none; +} + +.other-item:active { + background: #FAFFF5; +} + +.other-item-info { + flex: 1; + min-width: 0; +} + +.other-item-name { + font-size: 30rpx; + font-weight: 600; + color: #374151; + display: block; + margin-bottom: 10rpx; +} + +/* Mini bars */ +.mini-bar-wrap { + display: flex; + align-items: center; + gap: 12rpx; +} + +.mini-bar-bg { + flex: 1; + height: 8rpx; + background: #F3F4F6; + border-radius: 4rpx; + overflow: hidden; +} + +.mini-bar-fill { + height: 100%; + background: linear-gradient(90deg, #A5D6A7, #66BB6A); + border-radius: 4rpx; +} + +.mini-bar-text { + font-size: 22rpx; + color: #9CA3AF; + font-weight: 600; + flex-shrink: 0; + width: 80rpx; + text-align: right; +} + +/* ========== Bottom Section ========== */ +.bottom-section { + padding: 32rpx 32rpx 0; +} diff --git a/pages/wiki/index.js b/pages/wiki/index.js index 3293fe1..02e6c52 100644 --- a/pages/wiki/index.js +++ b/pages/wiki/index.js @@ -168,5 +168,35 @@ Page({ }); }, - closeIdentifyModal() { this.setData({ showIdentifyModal: false }); } + closeIdentifyModal() { this.setData({ showIdentifyModal: false }); }, + + // Handle plant identification: camera or album + handleIdentify(e) { + const source = e.currentTarget.dataset.source; // 'camera' or 'album' + + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: [source], + camera: 'back', + success: (res) => { + const tempFilePath = res.tempFiles[0].tempFilePath; + + // Close popup + this.setData({ showIdentifyModal: false }); + + // Store image path in global data for the results page + const app = getApp(); + app.globalData._identifyImagePath = tempFilePath; + + // Navigate to identify results page + wx.navigateTo({ + url: '/pages/wiki/identify/index' + }); + }, + fail: () => { + // User cancelled, do nothing + } + }); + } }) diff --git a/pages/wiki/index.wxml b/pages/wiki/index.wxml index 65a660c..ade0db0 100644 --- a/pages/wiki/index.wxml +++ b/pages/wiki/index.wxml @@ -119,26 +119,27 @@ - 识别植物 + 🌿 植物识别 + 拍照或上传图片,AI 帮你识别植物 - - - + + + 拍照识别 - - - + + + - 从相册上传 + 相册选取 - 取消 + 取消 diff --git a/pages/wiki/index.wxss b/pages/wiki/index.wxss index aa74680..e2e6c21 100644 --- a/pages/wiki/index.wxss +++ b/pages/wiki/index.wxss @@ -104,37 +104,92 @@ /* Popup Styles */ .popup-content { background: white; - border-radius: 40rpx 40rpx 0 0; - padding: 40rpx; - padding-bottom: env(safe-area-inset-bottom); + border-radius: 48rpx 48rpx 0 0; + padding: 48rpx 40rpx; + padding-bottom: calc(48rpx + env(safe-area-inset-bottom)); } .popup-header { text-align: center; - margin-bottom: 48rpx; + margin-bottom: 16rpx; } .popup-title { - font-size: 36rpx; + font-size: 40rpx; font-weight: 800; - color: var(--text-main); + background: linear-gradient(120deg, #33691E, #689F38); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + display: inline-block; +} + +.popup-subtitle { + font-size: 26rpx; + color: #9CA3AF; + text-align: center; + display: block; + margin-bottom: 48rpx; } .upload-options-grid { - display: flex; gap: 40rpx; justify-content: center; - margin-bottom: 48rpx; + display: flex; + gap: 32rpx; + justify-content: center; + margin-bottom: 48rpx; } .upload-opt-item { - display: flex; flex-direction: column; align-items: center; gap: 16rpx; + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 20rpx; + background: #F9FAFB; + border-radius: 32rpx; + padding: 40rpx 24rpx; + transition: all 0.2s; +} + +.upload-opt-item:active { + transform: scale(0.96); + background: #F0F7EB; } .opt-icon-circle { - width: 120rpx; height: 120rpx; border-radius: 40rpx; - display: flex; align-items: center; justify-content: center; + width: 112rpx; + height: 112rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06); +} + +.upload-opt-item text { + font-size: 28rpx; + font-weight: 600; + color: #374151; } .popup-footer { - display: flex; - justify-content: center; + display: flex; + justify-content: center; + padding-top: 8rpx; +} + +.popup-footer .cancel-btn { + width: 100%; + height: 88rpx; + line-height: 88rpx; + text-align: center; + font-size: 32rpx; + font-weight: 600; + color: #6B7280; + background: #F3F4F6; + border-radius: 44rpx; + border: none; +} + +.popup-footer .cancel-btn:active { + background: #E5E7EB; } diff --git a/utils/request.js b/utils/request.js index 1af19ea..acf03f0 100644 --- a/utils/request.js +++ b/utils/request.js @@ -166,6 +166,67 @@ class WxRequest { }); } + /** + * Upload file to a specific URL path + * @param {string} urlPath API path (e.g. '/classify/plant') + * @param {string} filePath Local file path + * @param {string} name Form field name (default: file) + * @param {Object} formData Additional form data + */ + uploadToUrl(urlPath, filePath, name = 'file', formData = {}) { + let config = { + url: this.baseUrl + urlPath, + header: { ...this.header }, + filePath: filePath, + name: name, + formData: formData + }; + + config = this.interceptors.request(config); + + return new Promise((resolve, reject) => { + wx.uploadFile({ + url: config.url, + filePath: config.filePath, + name: config.name, + formData: config.formData, + header: config.header, + success: (res) => { + let data; + try { + data = JSON.parse(res.data); + } catch (e) { + data = { code: 500, msg: 'Response parse error', data: res.data }; + } + + const responseObj = { ...res, data: data }; + const processedResponse = this.interceptors.response(responseObj); + const { statusCode, data: finalData } = processedResponse; + + if (statusCode >= 200 && statusCode < 300) { + const businessCode = finalData.code; + if (businessCode === 200) { + resolve(finalData.data); + } else { + this.handleError({ + errMsg: finalData.msg || 'Upload Error', + code: businessCode + }); + reject(finalData); + } + } else { + this.handleError({ errMsg: `HTTP Error: ${statusCode}`, ...res }); + reject(res); + } + }, + fail: (err) => { + this.handleError({ errMsg: 'Upload Network Error', ...err }); + reject(err); + } + }); + }); + } + get(url, data = {}, header = {}) { return this.request({ url, method: 'GET', data, header }); } @@ -178,7 +239,7 @@ class WxRequest { // Initialize with default instance const request = new WxRequest({ baseUrl: 'http://192.168.0.184:8889', - //baseUrl: 'https://prod.sundynix.cn/plant', + //baseUrl: 'https://go.sundynix.cn/api', header: { 'Content-Type': 'application/json' }