const state = { dashboard: null, plants: [], selectedPlantId: 0, weather: null, logs: [], photos: [], moodTags: [], settings: null, notificationGranted: typeof Notification !== 'undefined' && Notification.permission === 'granted', notificationTimer: null, interactionContext: null, interactionTimer: null, confirmResolver: null, uiMode: 'debug', activeReminderRule: null, buttonStates: {} // Track button loading states } const getUiMode = () => state.uiMode // 按钮状态管理函数 const setButtonLoading = (button, isLoading, originalText = null) => { if (!button) return if (isLoading) { // 保存原始状态 if (!originalText) { originalText = button.textContent || button.innerText } state.buttonStates[button.id || button.name || button.classList?.value || 'temp'] = { originalText, originalDisabled: button.disabled } // 设置加载状态 button.disabled = true button.textContent = button.textContent.replace(/…$/, '') + '…' } else { // 恢复原始状态 const key = button.id || button.name || button.classList?.value || 'temp' const originalState = state.buttonStates[key] if (originalState) { button.disabled = originalState.originalDisabled button.textContent = originalState.originalText delete state.buttonStates[key] } } } // 包装异步操作的函数,自动处理按钮loading状态 const withButtonLoading = async (button, asyncFn) => { if (!button) return await asyncFn() setButtonLoading(button, true) try { return await asyncFn() } finally { setButtonLoading(button, false) } } const $ = selector => document.querySelector(selector) const $$ = selector => Array.from(document.querySelectorAll(selector)) const escapeHtml = value => String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') const todayStr = () => { const d = new Date() const y = d.getFullYear() const m = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') return `${y}-${m}-${day}` } const openDialog = id => { const dlg = document.getElementById(id) if (!dlg) return if (id === 'interaction-dialog') { if (typeof dlg.showModal === 'function' && !dlg.open) dlg.showModal() return } if (typeof dlg.showModal === 'function' && !dlg.open) dlg.showModal() } const closeDialog = id => { const dlg = document.getElementById(id) if (id === 'interaction-dialog') { clearInteractionTimer() if (dlg && typeof dlg.close === 'function') dlg.close() return } if (dlg && typeof dlg.close === 'function') dlg.close() } const clearInteractionTimer = () => { if (state.interactionTimer) { window.clearInterval(state.interactionTimer) state.interactionTimer = null } } const openToast = (message, type = 'info', duration = 2000) => { if (getUiMode() !== 'normal') return const id = `toast-${Date.now()}` const toast = document.createElement('div') toast.className = `toast toast-${type}` toast.id = id toast.textContent = message document.body.appendChild(toast) window.requestAnimationFrame(() => toast.classList.add('active')) window.setTimeout(() => { if (toast.classList.contains('active')) { toast.classList.remove('active') window.setTimeout(() => toast.remove(), 300) } }, duration) } const formatTime = input => { try { return new Date(input || Date.now()).toLocaleString('zh-CN', { hour12: false }) } catch { return String(input || '') } } const getFriendlyErrorMessage = error => { const raw = String((error && (error.message || error.error)) || error || '').trim() if (raw === 'dashscope_api_key_missing') { return '未配置 DashScope API Key,请先在 `config/plant_home.json` 的 `ai.api_key` 中填入有效密钥。' } if (raw === 'qweather_api_key_missing') { return '未配置和风天气 API Key,请先在 `config/plant_home.json` 的 `weather.api_key` 中填入有效密钥。' } if (raw === 'plant_name_required') { return '请先填写植物名称,再点“辅助填写”。' } if (raw === 'home_feature_limit_reached') { return '这棵植物最多只能设置 3 张封面图,请先取消一张现有封面。' } if (raw === 'active_rule_exists') { return '当前植物的同类提醒已经存在,请先确认是否覆盖旧提醒。' } if (raw === 'weekly_days_required') { return '每周循环至少要选择一个周几。' } if (raw === 'tag_name_required') { return '请先输入标签名称。' } if (raw === 'log_not_found') { return '这条日志已经不存在了,可能是旧页面残留数据。' } return raw || '发生未知错误' } const normalizeFormData = formData => { const result = {} for (const [key, value] of formData.entries()) { const nextValue = value instanceof File ? { name: value.name, size: value.size, type: value.type } : value if (Object.prototype.hasOwnProperty.call(result, key)) { if (!Array.isArray(result[key])) result[key] = [result[key]] result[key].push(nextValue) } else { result[key] = nextValue } } return result } const toPrettyText = value => { if (value == null || value === '') return '(空)' if (value instanceof FormData) return JSON.stringify(normalizeFormData(value), null, 2) if (typeof value === 'string') { try { return JSON.stringify(JSON.parse(value), null, 2) } catch { return value } } try { return JSON.stringify(value, null, 2) } catch { return String(value) } } const toArray = value => Array.isArray(value) ? value : (value == null ? [] : [value]) const summarizeRuleText = rule => { if (!rule) return '当前没有同类提醒' return `当前已有提醒:${rule.title || ''} / ${rule.cycle_text || ''} / 下次 ${rule.next_due_date || ''} ${rule.due_time || ''}`.trim() } const getWeekdayValuesFromForm = form => { return $$('#log-weekday-list input[type="checkbox"]:checked').map(input => Number(input.value)).filter(Boolean) } const syncWeeklyDaysField = () => { const form = $('#log-form') if (!form) return form.reminder_weekly_days_json.value = JSON.stringify(getWeekdayValuesFromForm(form)) $$('#log-weekday-list .tag-chip-option').forEach(label => { const input = label.querySelector('input') label.classList.toggle('active', !!(input && input.checked)) }) } const renderSingleMoodTagPicker = (containerSelector, inputSelector) => { const box = $(containerSelector) if (!box) return const currentValue = document.querySelector(inputSelector)?.value || '' box.innerHTML = state.moodTags.map(tag => ` `).join('') } const renderMoodTagPickers = () => { renderSingleMoodTagPicker('#log-mood-tag-list', '#log-form [name="mood_tag"]') renderSingleMoodTagPicker('#reminder-mood-tag-list', '#reminder-form [name="mood_tag"]') } const updateReminderModeFields = () => { const mode = $('#log-reminder-mode')?.value || 'once' $('#log-reminder-once')?.classList.toggle('hidden', mode !== 'once') $('#log-reminder-every-n')?.classList.toggle('hidden', mode !== 'every_n_days') $('#log-reminder-weekly')?.classList.toggle('hidden', mode !== 'weekly') $('#log-reminder-monthly')?.classList.toggle('hidden', mode !== 'monthly') } const updateReminderEnabledFields = () => { const enabled = !!$('#log-reminder-enabled')?.checked $('#log-reminder-fields')?.classList.toggle('hidden', !enabled) } const applyMoodTagToForm = (inputSelector, tagValue) => { const input = document.querySelector(inputSelector) if (!input) return input.value = input.value === tagValue ? '' : tagValue renderMoodTagPickers() } const appendInteractionLine = (session, label, value) => { if (!session) return const chunk = value === undefined ? String(label || '') : `${label}\n${toPrettyText(value)}` session.logs.push(chunk) const output = $('#interaction-output') if (output) { output.textContent = session.logs.join('\n\n') output.scrollTop = output.scrollHeight } } const setInteractionStatus = (text, type = '') => { const box = $('#interaction-status') if (!box) return box.textContent = text || '' box.className = `interaction-status${type ? ` ${type}` : ''}` } const openInteractionSession = ({ title, mode = 'normal' }) => { clearInteractionTimer() const session = { id: `interaction-${Date.now()}`, title: title || '本次交互详情', mode, startedAt: Date.now(), logs: [] } $('#interaction-title').textContent = session.title $('#interaction-subtitle').textContent = `开始时间:${formatTime(session.startedAt)}` $('#interaction-output').textContent = '' setInteractionStatus('进行中,正在记录详细交互内容...', '') openDialog('interaction-dialog') appendInteractionLine(session, '[交互开始]', { title: session.title, mode: session.mode, started_at: formatTime(session.startedAt) }) return session } const copyInteractionLog = async () => { const session = state.interactionContext const text = session ? session.logs.join('\n\n') : $('#interaction-output').textContent if (!navigator.clipboard || !navigator.clipboard.writeText) { throw new Error('clipboard_not_supported') } await navigator.clipboard.writeText(text || '') } const finishInteractionSuccess = session => { if (!session) return const elapsed = Date.now() - session.startedAt appendInteractionLine(session, '[交互完成]', { finished_at: formatTime(), elapsed_ms: elapsed }) if (session.mode === 'view') { let remain = 3 if (getUiMode() === 'normal') remain = 2 setInteractionStatus(`查看成功,${remain}s 后自动关闭`, 'success') clearInteractionTimer() state.interactionTimer = window.setInterval(() => { remain -= 1 if (remain <= 0) { clearInteractionTimer() closeDialog('interaction-dialog') return } setInteractionStatus(`查看成功,${remain}s 后自动关闭`, 'success') }, 1000) return } setInteractionStatus('执行成功,请确认明细后手动关闭窗口', 'success') } const finishInteractionError = (session, error) => { if (!session) return const elapsed = Date.now() - session.startedAt const friendlyMessage = getFriendlyErrorMessage(error) appendInteractionLine(session, '[交互失败]', { failed_at: formatTime(), elapsed_ms: elapsed, message: error && error.message ? error.message : String(error || 'unknown_error'), friendly_message: friendlyMessage, stack: error && error.stack ? String(error.stack) : '' }) setInteractionStatus(`出现异常:${friendlyMessage}`, 'error') } const runTrackedInteraction = async ({ title, mode = 'normal' }, executor) => { if (state.interactionContext) { appendInteractionLine(state.interactionContext, '[子任务开始]', { title, mode, at: formatTime() }) try { const result = await executor(state.interactionContext) appendInteractionLine(state.interactionContext, '[子任务完成]', { title, at: formatTime() }) return result } catch (error) { appendInteractionLine(state.interactionContext, '[子任务异常]', { title, at: formatTime(), message: error && error.message ? error.message : String(error || 'unknown_error') }) throw error } } if (getUiMode() === 'normal' && mode === 'view') { openToast(`${title}完成`, 'success', 2000) return await executor(null) } const session = openInteractionSession({ title, mode }) state.interactionContext = session try { const result = await executor(session) finishInteractionSuccess(session) return result } catch (error) { finishInteractionError(session, error) throw error } finally { if (state.interactionContext && state.interactionContext.id === session.id) { state.interactionContext = null } } } const api = async (url, options = {}) => { const method = String(options.method || 'GET').toUpperCase() const requestHeaders = { ...(options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }), ...(options.headers || {}) } const session = state.interactionContext const startedAt = Date.now() if (session) { appendInteractionLine(session, '[请求开始]', { url, method, started_at: formatTime(startedAt), headers: requestHeaders, body: options.body instanceof FormData ? normalizeFormData(options.body) : (options.body || '(空)') }) } let resp = null let rawText = '' try { resp = await fetch(url, { headers: requestHeaders, ...options }) rawText = await resp.text() } catch (error) { if (session) { appendInteractionLine(session, '[网络异常]', { url, method, elapsed_ms: Date.now() - startedAt, message: error && error.message ? error.message : String(error || 'network_error') }) } throw error } let data = {} try { data = rawText ? JSON.parse(rawText) : {} } catch { data = { raw_text: rawText } } if (session) { appendInteractionLine(session, '[请求返回]', { url, method, status: resp.status, ok: resp.ok, elapsed_ms: Date.now() - startedAt, response: data }) } if (!resp.ok || data.ok === false) { const error = new Error((data && data.error) || `request_failed_${resp.status}`) error.status = resp.status error.response = data throw error } return data } const openConfirmDialog = ({ title, message, detail = '', confirmText = '确认' }) => new Promise(resolve => { state.confirmResolver = resolve $('#confirm-title').textContent = title || '请确认' $('#confirm-message').textContent = message || '确认继续吗?' $('#confirm-detail').textContent = detail || '' $('#btn-ok-confirm').textContent = confirmText || '确认' openDialog('confirm-dialog') }) const resolveConfirmDialog = value => { const resolver = state.confirmResolver state.confirmResolver = null closeDialog('confirm-dialog') if (resolver) resolver(!!value) } const openDeleteConfirmDialog = ({ title, message, detail }) => openConfirmDialog({ title, message, detail: toPrettyText({ ...(detail || {}), warning: '该操作不可恢复' }), confirmText: '确认删除' }) const showAnalysis = (text, { inline = false } = {}) => { if (getUiMode() === 'normal' && inline) { openToast(`AI 分析结果已内联显示`, 'info', 2000) return } $('#analysis-output').textContent = text || '暂无结果' openDialog('analysis-dialog') } const setFieldValue = (form, name, value, fallback = '') => { if (!form || !form[name]) return form[name].value = value == null || value === '' ? fallback : value } const applyPlantDraftToForm = draft => { const form = $('#plant-form') const next = draft || {} const rules = next.rules || {} const adjust = rules.weather_adjust || {} setFieldValue(form, 'name', next.name, form.name.value || '') setFieldValue(form, 'alias', next.alias) setFieldValue(form, 'scientific_name', next.scientific_name) setFieldValue(form, 'room_area', next.room_area) setFieldValue(form, 'light_env', next.light_env) setFieldValue(form, 'plant_group', next.plant_group) setFieldValue(form, 'status_label', next.status_label, '状态良好') setFieldValue(form, 'light_text', next.light_text) setFieldValue(form, 'watering_text', next.watering_text) setFieldValue(form, 'temp_text', next.temp_text) setFieldValue(form, 'ventilation_text', next.ventilation_text) setFieldValue(form, 'grow_light_text', next.grow_light_text) setFieldValue(form, 'fertilizing_text', next.fertilizing_text) setFieldValue(form, 'soil_text', next.soil_text) setFieldValue(form, 'ai_summary', next.ai_summary) setFieldValue(form, 'watering_interval_days', rules.watering_interval_days, '7') setFieldValue(form, 'fertilizing_interval_days', rules.fertilizing_interval_days, '30') setFieldValue(form, 'pruning_interval_days', rules.pruning_interval_days, '90') setFieldValue(form, 'repot_interval_days', rules.repot_interval_days, '180') setFieldValue(form, 'watering_hot_delta', adjust.watering_hot_delta, '-1') setFieldValue(form, 'watering_humid_delta', adjust.watering_humid_delta, '1') setFieldValue(form, 'watering_cold_delta', adjust.watering_cold_delta, '2') } const assistPlantDraft = async () => { const form = $('#plant-form') const inputName = String(form.name.value || '').trim() if (!inputName) { await runTrackedInteraction({ title: '辅助填写植物档案', mode: 'normal' }, async () => { throw new Error('请先输入植物名称') }) return } await runTrackedInteraction({ title: 'AI 辅助填写植物档案', mode: 'normal' }, async session => { const data = await api('/api/plant_home/plants/assist_draft', { method: 'POST', body: JSON.stringify({ name: inputName, chosen_name: '' }) }) applyPlantDraftToForm(data.draft || {}) if (data.chosen_name) form.name.value = data.chosen_name appendInteractionLine(session, '[表单回填完成]', data.draft || {}) }) } const renderSummary = stats => { const box = $('#summary-grid') if (!box) return const cards = [ ['植物数', stats && stats.plant_count || 0], ['待处理提醒', stats && stats.due_today_count || 0], ['养护日志', stats && stats.log_count || 0], ['相册图片', stats && stats.photo_count || 0] ] box.innerHTML = cards.map(([label, value]) => `
${label}
${value}
`).join('') } const renderWeather = weather => { const bar = $('#weather-bar') if (!bar) return if (!weather) { bar.textContent = '天气暂无数据' return } // 根据icon字段值映射天气图标 const getWeatherIcon = (iconCode) => { const code = parseInt(iconCode || '999', 10) if (code === 100) return '☀️' // 晴 if (code >= 101 && code <= 104) return '🌤️' // 多云 if (code >= 300 && code <= 399) return '🌧️' // 雨 if (code >= 400) return '❄️' // 雪 return '🌈' // 兜底 } const location = weather.location || {} const cityName = escapeHtml(location.display_name || '上海宝山区') const floorEnv = escapeHtml(weather.floor_env || '17楼室内') // 假设从配置中获取楼层环境 let temp = escapeHtml(weather.temp || '--') let humidity = escapeHtml(weather.humidity || '--') const windDir = escapeHtml(weather.windDir || '') const windScale = escapeHtml(weather.windScale || '') const weatherText = escapeHtml(weather.text || '') const adviceText = escapeHtml(weather.advice_text || '暂无建议') const weatherIcon = getWeatherIcon(weather.icon) // 检查温度是否已经包含°C,如果没有则添加 if (temp && !temp.includes('°C')) { temp = temp + '°C' } // 检查湿度是否已经包含%,如果没有则添加 if (humidity && !humidity.includes('%')) { humidity = humidity + '%' } bar.innerHTML = `
📍
${cityName}
${floorEnv}
${temp}
${weatherText}
${weatherIcon}
湿度 ${humidity}
风力 ${windDir}${windScale}级
${adviceText}
` // 绑定展开按钮事件 const expandBtn = $('#expand-advice-btn') const adviceTextElement = $('#advice-text') if (expandBtn && adviceTextElement) { expandBtn.onclick = () => { // 创建弹出层显示完整内容 const popup = document.createElement('div') popup.className = 'advice-popup' popup.innerHTML = `
${adviceText}
` // 定位弹出层 const rect = adviceTextElement.getBoundingClientRect() popup.style.position = 'absolute' popup.style.left = rect.left + 'px' popup.style.top = (rect.bottom + 10) + 'px' document.body.appendChild(popup) // 点击其他地方关闭弹窗 const closePopup = (e) => { if (!popup.contains(e.target)) { document.body.removeChild(popup) document.removeEventListener('click', closePopup) } } document.addEventListener('click', closePopup) } } } const initPlantCardRotators = () => { $$('.plant-cover-rotator').forEach(rotator => { if (rotator.dataset.bound === '1') return rotator.dataset.bound = '1' const slides = Array.from(rotator.querySelectorAll('.plant-cover-slide')) const dots = Array.from(rotator.querySelectorAll('.plant-cover-dot')) if (slides.length <= 1) return let current = 0 const activate = index => { current = index slides.forEach((slide, idx) => slide.classList.toggle('active', idx === index)) dots.forEach((dot, idx) => dot.classList.toggle('active', idx === index)) } rotator.dataset.timerId = String(window.setInterval(() => { activate((current + 1) % slides.length) }, 3200)) }) } const buildPlantCard = (plant, active = false) => { const nextReminder = plant.next_reminder const covers = Array.isArray(plant.card_covers) ? plant.card_covers.slice(0, 3) : [] const displayAvatar = plant.display_avatar_path || plant.avatar_path || '' const thumb = covers.length > 0 ? `
${covers.map((item, index) => ` ${escapeHtml(plant.name)} `).join('')} ${covers.length > 1 ? `
${covers.map((_, index) => ``).join('')}
` : ''}
` : displayAvatar ? `${escapeHtml(plant.name)}` : '🪴' return `
${thumb}
${escapeHtml(plant.name)}
${escapeHtml(plant.room_area || plant.light_env || '未设置位置')}
${escapeHtml(plant.status_label || '状态良好')}
${covers.length > 0 ? `
封面图:${covers.length} 张${covers.length > 1 ? ',卡片内轮播' : ''}
` : ''} ${nextReminder ? `
下次提醒:${escapeHtml(nextReminder.due_date)} · ${escapeHtml(nextReminder.title || '')}
` : '
暂未生成提醒
'}
` } const renderPlantGrid = (selector, addCard = false) => { const box = $(selector) if (!box) return const html = state.plants.map(plant => buildPlantCard(plant, plant.id === state.selectedPlantId)).join('') box.innerHTML = addCard ? `${html}
+
添加植物
新建植物档案、习性和提醒规则
` : html } const buildReminderItem = reminder => { const ruleText = reminder.rule && reminder.rule.cycle_text ? reminder.rule.cycle_text : '单次提醒' const hasRelatedLog = reminder.related_log && reminder.related_log.log_id return `
${escapeHtml(reminder.name || '')} · ${escapeHtml(reminder.title || '')}
${escapeHtml(reminder.due_date)} ${escapeHtml(reminder.due_time || '')} | ${escapeHtml(ruleText)}
${escapeHtml(reminder.reminder_type || '')}
${escapeHtml(reminder.body || '')}
${hasRelatedLog ? `` : ''} ${reminder.rule && reminder.rule.id ? `` : ''}
` } const renderHomeRemindersWeekly = reminders => { const box = $('#home-reminders-weekly') if (!box) return if (!Array.isArray(reminders) || reminders.length === 0) { box.innerHTML = '
未来一周暂无待提醒事项
' return } box.innerHTML = reminders.map(buildReminderItem).join('') } const renderHomeRemindersImportant = reminders => { const box = $('#home-reminders-important') if (!box) return if (!Array.isArray(reminders) || reminders.length === 0) { box.innerHTML = '
暂无重要提醒
' return } box.innerHTML = reminders.map(buildReminderItem).join('') } const renderPlantDetail = detail => { const box = $('#plant-detail') if (!box) return if (!detail || !detail.plant) { box.innerHTML = '
请选择一棵植物
' return } const plant = detail.plant const displayAvatar = plant.display_avatar_path || plant.avatar_path || '' const thumb = displayAvatar ? `${escapeHtml(plant.name)}` : '🪴' const diagnosis = (detail.diagnoses || [])[0] const activeRules = Array.isArray(detail.active_rules) ? detail.active_rules : [] box.innerHTML = `
${thumb}

${escapeHtml(plant.name)}

${escapeHtml(plant.scientific_name || '')}
${escapeHtml(plant.room_area || '')} | ${escapeHtml(plant.light_env || '')}
光照
${escapeHtml(plant.light_text || plant.light_env || '待补充')}
浇水
${escapeHtml(plant.watering_text || `${plant.watering_interval_days || 7}天/次`)}
适温
${escapeHtml(plant.temp_text || '18~28℃')}
通风
${escapeHtml(plant.ventilation_text || '中等通风')}
补光灯
${escapeHtml(plant.grow_light_text || '按需补光')}
施肥
${escapeHtml(plant.fertilizing_text || `${plant.fertilizing_interval_days || 30}天/次`)}
${escapeHtml(plant.ai_summary || plant.note || '这棵植物的详细档案还可以再补充,建议先导入 AI 习性。')}
${diagnosis ? `
最近问诊:${escapeHtml(diagnosis.summary || '')}
` : ''} ${activeRules.length ? `
当前提醒:${activeRules.map(rule => `${escapeHtml(rule.title || '')} / ${escapeHtml(rule.cycle_text || '')} / 下次 ${escapeHtml(rule.next_due_date || '')}`).join('
')}
` : ''} ` } const renderLogs = logs => { const box = $('#log-list') if (!box) return if (!Array.isArray(logs) || logs.length === 0) { box.innerHTML = '
暂无日志
' return } box.innerHTML = logs.map(log => `
${escapeHtml(log.name || '')} · ${escapeHtml(log.action_label || log.action_type || '')}
${escapeHtml(log.action_at || '')}${log.mood_tag ? ` | ${escapeHtml(log.mood_tag)}` : ''}
${escapeHtml(log.note || log.amount_text || '已记录')}
${log.asset_summary && log.asset_summary.photo_count ? `图片 ${escapeHtml(log.asset_summary.photo_count)}` : ''} ${log.asset_summary && log.asset_summary.video_count ? `视频 ${escapeHtml(log.asset_summary.video_count)}` : ''} ${log.asset_summary && log.asset_summary.attachment_count ? `附件 ${escapeHtml(log.asset_summary.attachment_count)}` : ''} ${log.reminder_rule ? `提醒 ${escapeHtml(log.reminder_rule.cycle_text || '')}` : ''}
`).join('') } const renderGallery = photos => { const box = $('#gallery-list') if (!box) return if (!Array.isArray(photos) || photos.length === 0) { box.innerHTML = '
暂无图片
' return } box.innerHTML = photos.map(photo => `
${escapeHtml(photo.name || '')}
${escapeHtml(photo.name || '')}
${escapeHtml(photo.shot_at || '')}
${escapeHtml(photo.note || '')}
${photo.is_home_featured ? '植物封面' : ''} ${photo.photo_source === 'seed' ? '标准图' : ''} ${photo.log_id ? '日志上传' : ''}
`).join('') } const renderSettings = cfg => { state.settings = cfg state.uiMode = cfg && cfg.ui_mode || 'debug' const user = cfg && cfg.user_context || {} $('#settings-context').textContent = [ user.city || '', user.residence || '', user.balcony || '', user.supplement_light || '', user.reminder_preference || '' ].filter(Boolean).join(' | ') } const loadDashboard = async () => { const data = await api('/api/plant_home/dashboard') state.dashboard = data state.plants = data.plants || [] if (state.selectedPlantId && !state.plants.find(item => item.id === state.selectedPlantId)) state.selectedPlantId = 0 if (!state.selectedPlantId && state.plants[0]) state.selectedPlantId = state.plants[0].id renderSummary(data.stats) renderHomeRemindersWeekly(data.reminders_weekly) renderHomeRemindersImportant(data.reminders_important) renderPlantGrid('#plant-list', true) initPlantCardRotators() // 异步加载天气数据,不阻塞其他内容的渲染 setTimeout(async () => { try { const weatherData = await api('/api/plant_home/weather') renderWeather(weatherData.weather) } catch (error) { console.warn('天气数据加载失败:', error) // 显示默认天气信息 renderWeather(null) } }, 0) } const loadPlantDetail = async (plantId) => { if (!plantId) { renderPlantDetail(null) return } const detail = await api(`/api/plant_home/plants/${plantId}`) state.selectedPlantId = plantId renderPlantDetail(detail) renderPlantGrid('#plant-list', true) initPlantCardRotators() } const loadLogs = async () => { const data = await api('/api/plant_home/logs') state.logs = data.data || [] renderLogs(state.logs) } const loadPhotos = async () => { const data = await api('/api/plant_home/photos') state.photos = data.data || [] renderGallery(state.photos) } const loadSettings = async () => { const data = await api('/api/plant_home/settings') renderSettings(data.config || {}) } const loadMoodTags = async () => { const data = await api('/api/plant_home/mood_tags') state.moodTags = data.data || [] renderMoodTagPickers() } const loadActiveReminderRule = async () => { const form = $('#log-form') if (!form) return null const plantId = form.plant_id.value const reminderType = form.reminder_type.value || form.action_type.value || 'observe' if (!plantId || !reminderType) { state.activeReminderRule = null $('#log-existing-rule-box').textContent = '当前没有同类提醒' $('#log-existing-rule-box').classList.remove('is-active') form.stop_existing_rule_id.value = '' return null } const data = await api(`/api/plant_home/reminder_rules/active?plant_id=${encodeURIComponent(plantId)}&reminder_type=${encodeURIComponent(reminderType)}`) state.activeReminderRule = data.rule || null const box = $('#log-existing-rule-box') box.textContent = summarizeRuleText(state.activeReminderRule) box.classList.toggle('is-active', !!state.activeReminderRule) if (!state.activeReminderRule) form.stop_existing_rule_id.value = '' return state.activeReminderRule } const fillPlantOptions = () => { const plantOptions = state.plants.map(plant => ``).join('') $('#log-plant-select').innerHTML = plantOptions $('#photo-plant-select').innerHTML = plantOptions if (state.selectedPlantId) { $('#log-plant-select').value = String(state.selectedPlantId) $('#photo-plant-select').value = String(state.selectedPlantId) } if ($('#log-reminder-type')) $('#log-reminder-type').value = $('#log-action-type')?.value || 'watering' } const openPlantDialog = async (edit = false) => { const form = $('#plant-form') form.reset() form.plant_id.value = '' form.action = 'create' form.cover_shot_at.value = todayStr() $('#plant-dialog-title').textContent = edit ? '编辑植物' : '新增植物' if (edit && state.selectedPlantId) { await runTrackedInteraction({ title: '读取植物详情用于编辑', mode: 'view' }, async () => { const detail = await api(`/api/plant_home/plants/${state.selectedPlantId}`) const plant = detail.plant form.plant_id.value = plant.id form.name.value = plant.name || '' form.alias.value = plant.alias || '' form.scientific_name.value = plant.scientific_name || '' form.room_area.value = plant.room_area || '' form.light_env.value = plant.light_env || '' form.plant_group.value = plant.plant_group || '' form.status_label.value = plant.status_label || '' form.light_text.value = plant.light_text || '' form.watering_text.value = plant.watering_text || '' form.temp_text.value = plant.temp_text || '' form.ventilation_text.value = plant.ventilation_text || '' form.grow_light_text.value = plant.grow_light_text || '' form.fertilizing_text.value = plant.fertilizing_text || '' form.soil_text.value = plant.soil_text || '' form.ai_summary.value = plant.ai_summary || '' form.watering_interval_days.value = plant.watering_interval_days || 7 form.fertilizing_interval_days.value = plant.fertilizing_interval_days || 30 form.pruning_interval_days.value = plant.pruning_interval_days || 90 form.repot_interval_days.value = plant.repot_interval_days || 180 let weatherAdjust = {} try { weatherAdjust = JSON.parse(plant.weather_adjust_json || '{}') } catch { weatherAdjust = {} } form.watering_hot_delta.value = weatherAdjust.watering_hot_delta == null ? -1 : weatherAdjust.watering_hot_delta form.watering_humid_delta.value = weatherAdjust.watering_humid_delta == null ? 1 : weatherAdjust.watering_humid_delta form.watering_cold_delta.value = weatherAdjust.watering_cold_delta == null ? 2 : weatherAdjust.watering_cold_delta form.note.value = plant.note || '' }) } else { form.repot_interval_days.value = 180 form.watering_hot_delta.value = -1 form.watering_humid_delta.value = 1 form.watering_cold_delta.value = 2 } openDialog('plant-dialog') } const handlePlantSubmit = async event => { event.preventDefault() const form = event.target const submitButton = form.querySelector('button[type="submit"]') await withButtonLoading(submitButton, async () => { const fd = new FormData(form) const file = fd.get('cover_photo') const payload = Object.fromEntries(fd.entries()) delete payload.cover_photo await runTrackedInteraction({ title: payload.plant_id ? '修改植物档案' : '新增植物档案', mode: 'normal' }, async () => { let plant = null if (payload.plant_id) { const data = await api(`/api/plant_home/plants/${payload.plant_id}`, { method: 'PUT', body: JSON.stringify(payload) }) plant = data.plant || null } else { const data = await api('/api/plant_home/plants', { method: 'POST', body: JSON.stringify(payload) }) plant = data.plant || null } if (plant && file instanceof File && file.size > 0) { const photoFd = new FormData() photoFd.append('plant_id', String(plant.id)) photoFd.append('photo', file) photoFd.append('shot_at', payload.cover_shot_at || todayStr()) photoFd.append('note', payload.cover_note || '') if (payload.cover_make_avatar === '1') photoFd.append('make_avatar', '1') await api('/api/plant_home/photos/upload', { method: 'POST', body: photoFd }) } closeDialog('plant-dialog') await refreshAll('刷新植物与提醒数据') }) }) } const deletePlant = async () => { if (!state.selectedPlantId) return const plant = state.plants.find(item => item.id === state.selectedPlantId) const confirmed = await openDeleteConfirmDialog({ title: '删除植物', message: `确认删除“${plant && plant.name ? plant.name : '当前植物'}”吗?这会同时删除日志、图片、提醒和 AI 档案。`, detail: { plant_id: state.selectedPlantId, plant_name: plant && plant.name } }) if (!confirmed) return const deleteButton = document.querySelector('[data-action="delete-plant"]') await withButtonLoading(deleteButton, async () => { await runTrackedInteraction({ title: '删除植物', mode: 'normal' }, async () => { await api(`/api/plant_home/plants/${state.selectedPlantId}`, { method: 'DELETE' }) state.selectedPlantId = 0 await refreshAll('刷新植物、日志、相册与提醒数据') }) }) } const deleteLog = async logId => { const target = state.logs.find(item => item.id === logId) const confirmed = await openDeleteConfirmDialog({ title: '删除养护日志', message: '确认删除这条养护日志吗?', detail: target || { id: logId } }) if (!confirmed) return const deleteButton = document.querySelector(`[data-action="delete-log"][data-log-id="${logId}"]`) await withButtonLoading(deleteButton, async () => { try { await runTrackedInteraction({ title: '删除养护日志', mode: 'normal' }, async () => { await api(`/api/plant_home/logs/${logId}`, { method: 'DELETE' }) await refreshAll('刷新日志与相关数据') }) } catch (error) { if (String(error && error.message || '') === 'log_not_found') { state.logs = state.logs.filter(item => item.id !== logId) renderLogs(state.logs) await refreshAll('刷新日志与相关数据') await runTrackedInteraction({ title: '删除养护日志', mode: 'view' }, async session => { appendInteractionLine(session, '[提示]', { message: '这条旧日志已经不在数据库中,页面已自动刷新。' }) }) return } throw error } }) } const deletePhoto = async photoId => { const target = state.photos.find(item => item.id === photoId) const confirmed = await openDeleteConfirmDialog({ title: '删除植物图片', message: '确认删除这张图片吗?会同时移除相关 AI 问诊记录。', detail: target || { id: photoId } }) if (!confirmed) return const deleteButton = document.querySelector(`[data-action="delete-photo"][data-photo-id="${photoId}"]`) await withButtonLoading(deleteButton, async () => { await runTrackedInteraction({ title: '删除植物图片', mode: 'normal' }, async () => { await api(`/api/plant_home/photos/${photoId}`, { method: 'DELETE' }) await refreshAll('刷新相册与植物详情数据') }) }) } const toggleHomePhoto = async (photoId, nextValue) => { const target = state.photos.find(item => item.id === photoId) const toggleButton = document.querySelector(`[data-action="toggle-home-photo"][data-photo-id="${photoId}"]`) await withButtonLoading(toggleButton, async () => { await runTrackedInteraction({ title: nextValue ? '设置植物封面' : '取消植物封面', mode: 'normal' }, async () => { await api(`/api/plant_home/photos/${photoId}/home_feature`, { method: 'PUT', body: JSON.stringify({ value: Number(nextValue) === 1 }) }) await refreshAll(`刷新植物封面与相册${target && target.name ? `:${target.name}` : ''}`) }) }) } const resetLogForm = async () => { const form = $('#log-form') form.reset() form.action_at.value = todayStr() form.override_existing_rule.value = '0' form.stop_existing_rule_id.value = '' fillPlantOptions() $('#log-reminder-type').value = $('#log-action-type').value || 'watering' $('#log-reminder-enabled').checked = false updateReminderEnabledFields() updateReminderModeFields() syncWeeklyDaysField() await loadActiveReminderRule() renderMoodTagPickers() } const addMoodTagFromDialog = async () => { const input = $('#log-new-tag-name') const name = input.value.trim() if (!name) throw new Error('tag_name_required') await api('/api/plant_home/mood_tags', { method: 'POST', body: JSON.stringify({ name }) }) input.value = '' await loadMoodTags() applyMoodTagToForm('#log-form [name="mood_tag"]', name) } const stopActiveRuleFromLog = async () => { const rule = state.activeReminderRule if (!rule || !rule.id) return const confirmed = await openConfirmDialog({ title: '停止现有提醒', message: '确认停止当前同类提醒吗?停止后不会再继续循环。', detail: toPrettyText(rule), confirmText: '确认停止' }) if (!confirmed) return await api(`/api/plant_home/reminder_rules/${rule.id}/stop`, { method: 'POST' }) $('#log-form').stop_existing_rule_id.value = String(rule.id) state.activeReminderRule = null await loadActiveReminderRule() } const handleLogSubmit = async event => { event.preventDefault() const form = event.target const submitButton = form.querySelector('button[type="submit"]') await withButtonLoading(submitButton, async () => { syncWeeklyDaysField() const fd = new FormData(form) const reminderEnabled = String(fd.get('reminder_enabled') || '') === '1' const activeRule = state.activeReminderRule if (reminderEnabled && activeRule && String(fd.get('override_existing_rule') || '0') !== '1') { const confirmed = await openConfirmDialog({ title: '覆盖旧提醒', message: '当前已有同类提醒,是否用本次设置覆盖?', detail: toPrettyText(activeRule), confirmText: '覆盖旧提醒' }) if (!confirmed) return fd.set('override_existing_rule', '1') form.override_existing_rule.value = '1' } await runTrackedInteraction({ title: '新增生长日记', mode: 'normal' }, async () => { await api('/api/plant_home/logs', { method: 'POST', body: fd }) closeDialog('log-dialog') await refreshAll('刷新日志与提醒数据') }) }) } const handlePhotoSubmit = async event => { event.preventDefault() const form = event.target const submitButton = form.querySelector('button[type="submit"]') await withButtonLoading(submitButton, async () => { const fd = new FormData(form) await runTrackedInteraction({ title: '上传植物照片', mode: 'normal' }, async () => { const data = await api('/api/plant_home/photos/upload', { method: 'POST', body: fd }) closeDialog('photo-dialog') await refreshAll('刷新相册与植物数据') if (data.diagnosis) { showAnalysis(`AI 问诊结果\n\n${data.diagnosis.summary || ''}\n\n建议:${JSON.parse(data.diagnosis.suggestions_json || '[]').join(';')}`, { inline: true }) } }) }) } const openLogDetailDialog = async logId => { const dialog = $('#log-detail-dialog') const content = $('#log-detail-content') if (!dialog || !content) return content.innerHTML = '
加载中...
' openDialog('log-detail-dialog') try { const data = await api(`/api/plant_home/logs/${logId}`) const log = data.log if (!log) { content.innerHTML = '
日志不存在或已被删除
' return } const photos = Array.isArray(log.photos) ? log.photos : [] const attachments = Array.isArray(log.attachments) ? log.attachments : [] const rule = log.reminder_rule content.innerHTML = `
植物 ${escapeHtml(log.name || '')}
操作 ${escapeHtml(log.action_label || '')}
日期 ${escapeHtml(log.action_at || '')}
${log.mood_tag ? `
心情${escapeHtml(log.mood_tag)}
` : ''}
${log.note ? `
备注
${escapeHtml(log.note)}
` : ''} ${photos.length > 0 ? `
图片(${photos.length})
${photos.map(p => ``).join('')}
` : ''} ${attachments.length > 0 ? `
附件
${attachments.map(a => `
${escapeHtml(a.file_name)}
`).join('')}
` : ''} ${rule ? `
关联提醒
${escapeHtml(rule.title || '')} · ${escapeHtml(rule.cycle_text || '')}
` : ''} ` } catch (e) { content.innerHTML = `
加载失败:${escapeHtml(String(e.message || e))}
` } } const openReminderDialog = reminderId => { const form = $('#reminder-form') form.reset() form.reminder_id.value = String(reminderId || '') form.action_at.value = todayStr() renderMoodTagPickers() openDialog('reminder-dialog') } const handleReminderSubmit = async event => { event.preventDefault() const form = event.target const submitButton = form.querySelector('button[type="submit"]') await withButtonLoading(submitButton, async () => { const payload = Object.fromEntries(new FormData(form).entries()) await runTrackedInteraction({ title: '完成提醒并写入日记', mode: 'normal' }, async () => { await api(`/api/plant_home/reminders/${payload.reminder_id}/resolve`, { method: 'POST', body: JSON.stringify({ note: payload.note || '', mood_tag: payload.mood_tag || '', action_at: payload.action_at || todayStr() }) }) closeDialog('reminder-dialog') await refreshAll('刷新提醒与生长日志') }) }) } const requestNotification = async () => { const notifyButton = $('#btn-enable-notify') await withButtonLoading(notifyButton, async () => { await runTrackedInteraction({ title: '申请浏览器提醒权限', mode: 'normal' }, async session => { if (typeof Notification === 'undefined') { appendInteractionLine(session, '[本地能力检查]', '当前浏览器不支持 Notification') $('#notify-status').textContent = '当前浏览器不支持 Notification' return } appendInteractionLine(session, '[本地请求]', { action: 'Notification.requestPermission', before_status: Notification.permission }) const result = await Notification.requestPermission() appendInteractionLine(session, '[本地返回]', { after_status: result }) state.notificationGranted = result === 'granted' $('#notify-status').textContent = state.notificationGranted ? '已授权' : `当前状态:${result}` }) }) } const pollNotifications = async () => { if (!state.notificationGranted) return const data = await api('/api/plant_home/notifications/pending') const items = data.data || [] for (const item of items) { const notification = new Notification(item.title || '养护提醒', { body: item.body || '', tag: `plant-home-${item.id}` }) notification.onclick = () => window.focus() await api(`/api/plant_home/notifications/${item.id}/ack`, { method: 'POST' }) } } const refreshAll = async (title = '刷新页面数据') => { await runTrackedInteraction({ title, mode: 'view' }, async session => { appendInteractionLine(session, '[刷新范围]', { modules: ['dashboard', 'logs', 'photos', 'settings', 'plant_detail', 'mood_tags'] }) await loadDashboard() fillPlantOptions() await Promise.all([ loadLogs(), loadPhotos(), loadSettings(), loadPlantDetail(state.selectedPlantId), loadMoodTags() ]) }) } const importProfile = async () => { if (!state.selectedPlantId) return const importButton = document.getElementById('btn-import-profile') await withButtonLoading(importButton, async () => { await runTrackedInteraction({ title: 'AI 导入植物习性档案', mode: 'normal' }, async () => { const data = await api(`/api/plant_home/plants/${state.selectedPlantId}/import_profile`, { method: 'POST' }) await refreshAll('刷新植物档案与提醒') showAnalysis(JSON.stringify(data.profile || {}, null, 2), { inline: true }) }) }) } const analyzePattern = async () => { if (!state.selectedPlantId) return const analyzeButton = document.getElementById('btn-pattern-analysis') await withButtonLoading(analyzeButton, async () => { await runTrackedInteraction({ title: 'AI 分析养护规律', mode: 'normal' }, async () => { const data = await api(`/api/plant_home/plants/${state.selectedPlantId}/analyze_pattern`, { method: 'POST' }) showAnalysis(data.analysis || '暂无结果', { inline: true }) }) }) } const runScheduler = async () => { const schedulerButton = document.getElementById('btn-run-scheduler') await withButtonLoading(schedulerButton, async () => { await runTrackedInteraction({ title: '手动执行提醒调度器', mode: 'normal' }, async () => { const data = await api('/api/plant_home/scheduler/run', { method: 'POST' }) await refreshAll('刷新天气与提醒结果') showAnalysis(`手动调度完成\n\n新生成提醒:${(data.generate && data.generate.created) || 0}\n天气:${JSON.stringify(data.weather || {}, null, 2)}`, { inline: true }) }) }) } const switchTab = tab => { $$('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tab)) $$('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${tab}`)) } const bindEvents = () => { $$('.tab-btn').forEach(btn => { btn.addEventListener('click', () => switchTab(btn.dataset.tab)) }) document.body.addEventListener('click', async event => { const target = event.target if (target.matches('[data-close-dialog]')) { closeDialog(target.dataset.closeDialog) return } const plantCard = target.closest('.plant-card[data-plant-id]') if (plantCard) { const plantId = parseInt(String(plantCard.dataset.plantId || '0'), 10) if (plantId) { await runTrackedInteraction({ title: '查看植物详情', mode: 'view' }, async () => { await loadPlantDetail(plantId) }) } return } if (target.closest('[data-action="add-plant"]')) return openPlantDialog(false) if (target.closest('[data-action="edit-plant"]')) return openPlantDialog(true) if (target.closest('[data-action="delete-plant"]')) return deletePlant() if (target.closest('[data-action="resolve-reminder"]')) { const id = parseInt(target.dataset.reminderId || target.closest('[data-reminder-id]').dataset.reminderId, 10) return openReminderDialog(id) } if (target.closest('[data-action="stop-rule"]')) { const ruleId = parseInt(target.closest('[data-action="stop-rule"]').dataset.ruleId || '0', 10) if (!ruleId) return const confirmed = await openConfirmDialog({ title: '停止循环提醒', message: '确认停止这个提醒规则吗?', detail: toPrettyText({ rule_id: ruleId }), confirmText: '确认停止' }) if (!confirmed) return await api(`/api/plant_home/reminder_rules/${ruleId}/stop`, { method: 'POST' }) return refreshAll('刷新提醒与日志数据') } if (target.closest('[data-action="pick-mood-tag"]')) { const btn = target.closest('[data-action="pick-mood-tag"]') return applyMoodTagToForm(btn.dataset.targetInputSelector, btn.dataset.tagValue || '') } if (target.id === 'btn-log-add-tag') { const addButton = target return withButtonLoading(addButton, async () => addMoodTagFromDialog()) } if (target.closest('[data-action="diagnose-photo"]')) { const btn = target.closest('[data-action="diagnose-photo"]') const photoId = parseInt(btn.dataset.photoId || '0', 10) const diagnoseButton = btn return withButtonLoading(diagnoseButton, async () => { return runTrackedInteraction({ title: 'AI 图片问诊', mode: 'normal' }, async () => { const data = await api(`/api/plant_home/photos/${photoId}/diagnose`, { method: 'POST' }) const suggestions = JSON.parse(data.diagnosis && data.diagnosis.suggestions_json || '[]') showAnalysis(`${data.diagnosis && data.diagnosis.summary || ''}\n\n建议:\n- ${suggestions.join('\n- ')}`, { inline: true }) }) }) } if (target.closest('[data-action="delete-photo"]')) { const photoId = parseInt(target.closest('[data-action="delete-photo"]').dataset.photoId || '0', 10) return deletePhoto(photoId) } if (target.closest('[data-action="toggle-home-photo"]')) { const btn = target.closest('[data-action="toggle-home-photo"]') const photoId = parseInt(btn.dataset.photoId || '0', 10) const nextValue = parseInt(btn.dataset.value || '0', 10) return toggleHomePhoto(photoId, nextValue) } if (target.closest('[data-action="delete-log"]')) { const logId = parseInt(target.closest('[data-action="delete-log"]').dataset.logId || '0', 10) return deleteLog(logId) } }) $('#plant-form').addEventListener('submit', handlePlantSubmit) $('#log-form').addEventListener('submit', handleLogSubmit) $('#photo-form').addEventListener('submit', handlePhotoSubmit) $('#reminder-form').addEventListener('submit', handleReminderSubmit) $('#btn-assist-plant').addEventListener('click', () => { const assistButton = $('#btn-assist-plant') withButtonLoading(assistButton, async () => { await assistPlantDraft() }) }) $('#btn-add-plant').addEventListener('click', () => openPlantDialog(false)) $('#btn-add-log').addEventListener('click', async () => { await resetLogForm() openDialog('log-dialog') }) $('#btn-add-log-logs').addEventListener('click', async () => { await resetLogForm() openDialog('log-dialog') }) $('#btn-upload-photo').addEventListener('click', () => { $('#photo-form').reset() $('#photo-form').shot_at.value = todayStr() fillPlantOptions() openDialog('photo-dialog') }) $('#log-form').plant_id.addEventListener('change', () => loadActiveReminderRule().catch(() => {})) $('#log-form').action_type.addEventListener('change', () => { $('#log-reminder-type').value = $('#log-action-type').value || 'watering' loadActiveReminderRule().catch(() => {}) }) $('#log-form').reminder_type.addEventListener('change', () => loadActiveReminderRule().catch(() => {})) $('#log-form').reminder_rule_mode.addEventListener('change', updateReminderModeFields) $('#log-reminder-enabled').addEventListener('change', updateReminderEnabledFields) $('#log-weekday-list').addEventListener('change', syncWeeklyDaysField) $('#log-existing-rule-box').addEventListener('click', () => { if (state.activeReminderRule) stopActiveRuleFromLog().catch(() => {}) }) $('#btn-import-profile').addEventListener('click', importProfile) $('#btn-pattern-analysis').addEventListener('click', analyzePattern) $('#btn-refresh-dashboard').addEventListener('click', () => refreshAll('手动刷新首页与详情数据')) // 提醒 tab 切换 document.querySelectorAll('.reminder-tab-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.reminder-tab-btn').forEach(b => b.classList.remove('active')) btn.classList.add('active') const tab = btn.dataset.reminderTab document.querySelectorAll('[id^="home-reminders-"]').forEach(el => el.classList.add('hidden')) const target = $(`#home-reminders-${tab}`) if (target) target.classList.remove('hidden') }) }) // 查看日志详情(委托:首页提醒 + 养护日志列表) document.querySelector('.plant-main').addEventListener('click', async e => { const btn = e.target.closest('[data-action="view-log-detail"]') if (!btn) return const logId = parseInt(btn.dataset.logId || '0', 10) if (logId) await openLogDetailDialog(logId) }) $('#btn-refresh-weather').addEventListener('click', async () => { const weatherButton = $('#btn-refresh-weather') await withButtonLoading(weatherButton, async () => { await runTrackedInteraction({ title: '刷新天气数据', mode: 'normal' }, async () => { await api('/api/plant_home/weather/refresh', { method: 'POST' }) await refreshAll('刷新天气与页面数据') }) }) }) $('#btn-run-scheduler').addEventListener('click', runScheduler) $('#btn-enable-notify').addEventListener('click', requestNotification) $('#btn-copy-interaction').addEventListener('click', async () => { try { await copyInteractionLog() setInteractionStatus('交互日志已复制到剪贴板', 'success') } catch (error) { setInteractionStatus(`复制失败:${error.message || error}`, 'error') } }) $('#btn-copy-confirm').addEventListener('click', async () => { const text = [$('#confirm-title').textContent, $('#confirm-message').textContent, $('#confirm-detail').textContent].join('\n\n') try { await navigator.clipboard.writeText(text) } catch {} }) $('#btn-cancel-confirm').addEventListener('click', () => resolveConfirmDialog(false)) $('#btn-ok-confirm').addEventListener('click', () => resolveConfirmDialog(true)) } const init = async () => { bindEvents() $('#notify-status').textContent = state.notificationGranted ? '已授权' : '尚未授权' try { await refreshAll('初始化绿植的流浪家园') } catch (e) { showAnalysis(`初始化失败:${e.message || e}`, { inline: true }) } const interval = (state.settings && state.settings.notification && state.settings.notification.browser_poll_seconds) || 60 state.notificationTimer = window.setInterval(() => { pollNotifications().catch(() => {}) }, Math.max(15, Number(interval)) * 1000) pollNotifications().catch(() => {}) } window.addEventListener('load', init)