1591 lines
62 KiB
JavaScript
1591 lines
62 KiB
JavaScript
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, '"')
|
||
.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 => `
|
||
<button type="button" class="tag-chip-btn ${currentValue === tag.name ? 'active' : ''}" data-action="pick-mood-tag" data-target-input-selector="${escapeHtml(inputSelector)}" data-tag-value="${escapeHtml(tag.name)}">${escapeHtml(tag.name)}</button>
|
||
`).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]) => `
|
||
<div class="summary-card">
|
||
<div class="summary-label">${label}</div>
|
||
<div class="summary-value">${value}</div>
|
||
</div>
|
||
`).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 = `
|
||
<div class="weather-location">
|
||
<span class="location-icon">📍</span>
|
||
<div class="location-city">${cityName}</div>
|
||
<div class="location-env">${floorEnv}</div>
|
||
</div>
|
||
|
||
<div class="weather-main">
|
||
<div class="weather-temp">${temp}</div>
|
||
<div class="weather-text">${weatherText}</div>
|
||
<div class="weather-icon">${weatherIcon}</div>
|
||
<div class="weather-details">
|
||
<div class="detail-item">
|
||
<span class="detail-label">湿度</span>
|
||
<span class="detail-value">${humidity}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">风力</span>
|
||
<span class="detail-value">${windDir}${windScale}级</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="weather-advice">
|
||
<div class="advice-text" id="advice-text">${adviceText}</div>
|
||
<button class="expand-btn" id="expand-advice-btn">展开</button>
|
||
</div>
|
||
`
|
||
|
||
// 绑定展开按钮事件
|
||
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 = `<div>${adviceText}</div>`
|
||
|
||
// 定位弹出层
|
||
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
|
||
? `
|
||
<div class="plant-cover-rotator" data-cover-count="${covers.length}">
|
||
${covers.map((item, index) => `
|
||
<img class="plant-cover-slide ${index === 0 ? 'active' : ''}" src="${escapeHtml(item.file_path)}" alt="${escapeHtml(plant.name)}" data-cover-index="${index}">
|
||
`).join('')}
|
||
${covers.length > 1 ? `<div class="plant-cover-dots">${covers.map((_, index) => `<span class="plant-cover-dot ${index === 0 ? 'active' : ''}" data-cover-dot="${index}"></span>`).join('')}</div>` : ''}
|
||
</div>
|
||
`
|
||
: displayAvatar
|
||
? `<img src="${escapeHtml(displayAvatar)}" alt="${escapeHtml(plant.name)}">`
|
||
: '🪴'
|
||
return `
|
||
<article class="plant-card ${active ? 'active' : ''}" data-plant-id="${plant.id}">
|
||
<div class="plant-thumb">${thumb}</div>
|
||
<div class="plant-name">${escapeHtml(plant.name)}</div>
|
||
<div class="plant-meta">
|
||
<div>${escapeHtml(plant.room_area || plant.light_env || '未设置位置')}</div>
|
||
<div><span class="status-chip">${escapeHtml(plant.status_label || '状态良好')}</span></div>
|
||
${covers.length > 0 ? `<div>封面图:${covers.length} 张${covers.length > 1 ? ',卡片内轮播' : ''}</div>` : ''}
|
||
${nextReminder ? `<div>下次提醒:${escapeHtml(nextReminder.due_date)} · ${escapeHtml(nextReminder.title || '')}</div>` : '<div>暂未生成提醒</div>'}
|
||
</div>
|
||
</article>
|
||
`
|
||
}
|
||
|
||
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}
|
||
<article class="plant-card add-card" data-action="add-plant">
|
||
<div class="plant-thumb">+</div>
|
||
<div class="plant-name">添加植物</div>
|
||
<div class="plant-meta">新建植物档案、习性和提醒规则</div>
|
||
</article>
|
||
` : 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 `
|
||
<article class="reminder-item" data-type="${escapeHtml(reminder.reminder_type)}" data-reminder-id="${reminder.id}">
|
||
<div class="reminder-head">
|
||
<div>
|
||
<div class="reminder-title">${escapeHtml(reminder.name || '')} · ${escapeHtml(reminder.title || '')}</div>
|
||
<div class="photo-meta">${escapeHtml(reminder.due_date)} ${escapeHtml(reminder.due_time || '')} | ${escapeHtml(ruleText)}</div>
|
||
</div>
|
||
<span class="status-chip">${escapeHtml(reminder.reminder_type || '')}</span>
|
||
</div>
|
||
<div class="reminder-body">${escapeHtml(reminder.body || '')}</div>
|
||
<div class="reminder-actions">
|
||
<button class="mini-btn" data-action="resolve-reminder" data-reminder-id="${reminder.id}">完成并记日记</button>
|
||
${hasRelatedLog ? `<button class="mini-btn" data-action="view-log-detail" data-log-id="${reminder.related_log.log_id}">查看详情</button>` : ''}
|
||
${reminder.rule && reminder.rule.id ? `<button class="mini-btn" data-action="stop-rule" data-rule-id="${reminder.rule.id}">停止此提醒</button>` : ''}
|
||
</div>
|
||
</article>
|
||
`
|
||
}
|
||
|
||
const renderHomeRemindersWeekly = reminders => {
|
||
const box = $('#home-reminders-weekly')
|
||
if (!box) return
|
||
if (!Array.isArray(reminders) || reminders.length === 0) {
|
||
box.innerHTML = '<div class="empty-text">未来一周暂无待提醒事项</div>'
|
||
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 = '<div class="empty-text">暂无重要提醒</div>'
|
||
return
|
||
}
|
||
box.innerHTML = reminders.map(buildReminderItem).join('')
|
||
}
|
||
|
||
const renderPlantDetail = detail => {
|
||
const box = $('#plant-detail')
|
||
if (!box) return
|
||
if (!detail || !detail.plant) {
|
||
box.innerHTML = '<div class="empty-text">请选择一棵植物</div>'
|
||
return
|
||
}
|
||
const plant = detail.plant
|
||
const displayAvatar = plant.display_avatar_path || plant.avatar_path || ''
|
||
const thumb = displayAvatar
|
||
? `<img src="${escapeHtml(displayAvatar)}" alt="${escapeHtml(plant.name)}">`
|
||
: '🪴'
|
||
const diagnosis = (detail.diagnoses || [])[0]
|
||
const activeRules = Array.isArray(detail.active_rules) ? detail.active_rules : []
|
||
box.innerHTML = `
|
||
<div class="detail-cover">
|
||
<div class="plant-thumb">${thumb}</div>
|
||
<div>
|
||
<div class="detail-title">
|
||
<div>
|
||
<h3>${escapeHtml(plant.name)}</h3>
|
||
<div class="latin">${escapeHtml(plant.scientific_name || '')}</div>
|
||
<div class="photo-meta">${escapeHtml(plant.room_area || '')} | ${escapeHtml(plant.light_env || '')}</div>
|
||
</div>
|
||
<div class="interaction-actions">
|
||
<button class="ghost-btn" data-action="edit-plant">编辑</button>
|
||
<button class="ghost-btn" data-action="delete-plant">删除植物</button>
|
||
</div>
|
||
</div>
|
||
<div class="detail-grid">
|
||
<div class="metric-box"><div class="metric-label">光照</div><div class="metric-value">${escapeHtml(plant.light_text || plant.light_env || '待补充')}</div></div>
|
||
<div class="metric-box"><div class="metric-label">浇水</div><div class="metric-value">${escapeHtml(plant.watering_text || `${plant.watering_interval_days || 7}天/次`)}</div></div>
|
||
<div class="metric-box"><div class="metric-label">适温</div><div class="metric-value">${escapeHtml(plant.temp_text || '18~28℃')}</div></div>
|
||
<div class="metric-box"><div class="metric-label">通风</div><div class="metric-value">${escapeHtml(plant.ventilation_text || '中等通风')}</div></div>
|
||
<div class="metric-box"><div class="metric-label">补光灯</div><div class="metric-value">${escapeHtml(plant.grow_light_text || '按需补光')}</div></div>
|
||
<div class="metric-box"><div class="metric-label">施肥</div><div class="metric-value">${escapeHtml(plant.fertilizing_text || `${plant.fertilizing_interval_days || 30}天/次`)}</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="detail-summary">${escapeHtml(plant.ai_summary || plant.note || '这棵植物的详细档案还可以再补充,建议先导入 AI 习性。')}</div>
|
||
${diagnosis ? `<div class="detail-summary">最近问诊:${escapeHtml(diagnosis.summary || '')}</div>` : ''}
|
||
${activeRules.length ? `<div class="detail-summary">当前提醒:${activeRules.map(rule => `${escapeHtml(rule.title || '')} / ${escapeHtml(rule.cycle_text || '')} / 下次 ${escapeHtml(rule.next_due_date || '')}`).join('<br>')}</div>` : ''}
|
||
`
|
||
}
|
||
|
||
const renderLogs = logs => {
|
||
const box = $('#log-list')
|
||
if (!box) return
|
||
if (!Array.isArray(logs) || logs.length === 0) {
|
||
box.innerHTML = '<div class="empty-text">暂无日志</div>'
|
||
return
|
||
}
|
||
box.innerHTML = logs.map(log => `
|
||
<article class="timeline-item">
|
||
<div class="timeline-head">
|
||
<div>
|
||
<div class="timeline-title">${escapeHtml(log.name || '')} · ${escapeHtml(log.action_label || log.action_type || '')}</div>
|
||
<div class="photo-meta">${escapeHtml(log.action_at || '')}${log.mood_tag ? ` | ${escapeHtml(log.mood_tag)}` : ''}</div>
|
||
</div>
|
||
<div style="display:flex;gap:6px;">
|
||
<button class="mini-btn" data-action="view-log-detail" data-log-id="${log.id}">查看详情</button>
|
||
<button class="ghost-btn" data-action="delete-log" data-log-id="${log.id}">删除</button>
|
||
</div>
|
||
</div>
|
||
<div class="timeline-body">${escapeHtml(log.note || log.amount_text || '已记录')}</div>
|
||
<div class="asset-inline">
|
||
${log.asset_summary && log.asset_summary.photo_count ? `<span class="status-chip">图片 ${escapeHtml(log.asset_summary.photo_count)}</span>` : ''}
|
||
${log.asset_summary && log.asset_summary.video_count ? `<span class="status-chip">视频 ${escapeHtml(log.asset_summary.video_count)}</span>` : ''}
|
||
${log.asset_summary && log.asset_summary.attachment_count ? `<span class="status-chip">附件 ${escapeHtml(log.asset_summary.attachment_count)}</span>` : ''}
|
||
${log.reminder_rule ? `<span class="status-chip">提醒 ${escapeHtml(log.reminder_rule.cycle_text || '')}</span>` : ''}
|
||
</div>
|
||
</article>
|
||
`).join('')
|
||
}
|
||
|
||
const renderGallery = photos => {
|
||
const box = $('#gallery-list')
|
||
if (!box) return
|
||
if (!Array.isArray(photos) || photos.length === 0) {
|
||
box.innerHTML = '<div class="empty-text">暂无图片</div>'
|
||
return
|
||
}
|
||
box.innerHTML = photos.map(photo => `
|
||
<article class="photo-card">
|
||
<img src="${escapeHtml(photo.file_path)}" alt="${escapeHtml(photo.name || '')}">
|
||
<div class="photo-info">
|
||
<div><strong>${escapeHtml(photo.name || '')}</strong></div>
|
||
<div class="photo-meta">${escapeHtml(photo.shot_at || '')}</div>
|
||
<div class="photo-meta">${escapeHtml(photo.note || '')}</div>
|
||
<div class="photo-badges">
|
||
${photo.is_home_featured ? '<span class="status-chip">植物封面</span>' : ''}
|
||
${photo.photo_source === 'seed' ? '<span class="status-chip">标准图</span>' : ''}
|
||
${photo.log_id ? '<span class="status-chip">日志上传</span>' : ''}
|
||
</div>
|
||
<div class="interaction-actions">
|
||
<button class="mini-btn" data-action="diagnose-photo" data-photo-id="${photo.id}">AI问诊</button>
|
||
<button class="mini-btn" data-action="toggle-home-photo" data-photo-id="${photo.id}" data-value="${photo.is_home_featured ? 0 : 1}">${photo.is_home_featured ? '取消封面' : '设为植物封面'}</button>
|
||
<button class="mini-btn" data-action="delete-photo" data-photo-id="${photo.id}">删除图片</button>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
`).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 => `<option value="${plant.id}">${escapeHtml(plant.name)}</option>`).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 = '<div class="empty-text">加载中...</div>'
|
||
openDialog('log-detail-dialog')
|
||
try {
|
||
const data = await api(`/api/plant_home/logs/${logId}`)
|
||
const log = data.log
|
||
if (!log) {
|
||
content.innerHTML = '<div class="empty-text">日志不存在或已被删除</div>'
|
||
return
|
||
}
|
||
const photos = Array.isArray(log.photos) ? log.photos : []
|
||
const attachments = Array.isArray(log.attachments) ? log.attachments : []
|
||
const rule = log.reminder_rule
|
||
content.innerHTML = `
|
||
<div class="log-detail-section">
|
||
<div class="log-detail-field">
|
||
<span class="log-detail-label">植物</span>
|
||
<span>${escapeHtml(log.name || '')}</span>
|
||
</div>
|
||
<div class="log-detail-field">
|
||
<span class="log-detail-label">操作</span>
|
||
<span>${escapeHtml(log.action_label || '')}</span>
|
||
</div>
|
||
<div class="log-detail-field">
|
||
<span class="log-detail-label">日期</span>
|
||
<span>${escapeHtml(log.action_at || '')}</span>
|
||
</div>
|
||
${log.mood_tag ? `<div class="log-detail-field"><span class="log-detail-label">心情</span><span>${escapeHtml(log.mood_tag)}</span></div>` : ''}
|
||
</div>
|
||
${log.note ? `<div class="log-detail-section"><div class="log-detail-label">备注</div><div class="log-detail-note">${escapeHtml(log.note)}</div></div>` : ''}
|
||
${photos.length > 0 ? `
|
||
<div class="log-detail-section">
|
||
<div class="log-detail-label">图片(${photos.length})</div>
|
||
<div class="log-detail-photos">
|
||
${photos.map(p => `<img src="${escapeHtml(p.file_path)}" alt="" class="log-detail-photo" onclick="window.open(this.src)">`).join('')}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
${attachments.length > 0 ? `
|
||
<div class="log-detail-section">
|
||
<div class="log-detail-label">附件</div>
|
||
<div class="log-detail-attachments">
|
||
${attachments.map(a => `<div class="log-detail-attachment">${escapeHtml(a.file_name)}</div>`).join('')}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
${rule ? `
|
||
<div class="log-detail-section">
|
||
<div class="log-detail-label">关联提醒</div>
|
||
<div class="log-detail-note">${escapeHtml(rule.title || '')} · ${escapeHtml(rule.cycle_text || '')}</div>
|
||
</div>
|
||
` : ''}
|
||
`
|
||
} catch (e) {
|
||
content.innerHTML = `<div class="empty-text">加载失败:${escapeHtml(String(e.message || e))}</div>`
|
||
}
|
||
}
|
||
|
||
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)
|