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]) => `
`).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}级
`
// 绑定展开按钮事件
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) => `
})
`).join('')}
${covers.length > 1 ? `
${covers.map((_, index) => ``).join('')}
` : ''}
`
: displayAvatar
? `
`
: '🪴'
return `
${thumb}
${escapeHtml(plant.name)}
`
}
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
? `
`
: '🪴'
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.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)