# Widget 可复用模式 生成 widget 时,按需引入以下模式。每个模式自包含,直接内联到 widget 文件中。 --- ## 1. 可拖拽定位 让 widget 可被鼠标拖拽移动,位置自动存入 localStorage。 **使用场景**:用户希望自由调整 widget 位置 **要点**: - 使用 `useRef` 获取 DOM 元素 - 纯 DOM 事件实现拖拽(mousedown/mousemove/mouseup) - `className` 中不要设 `bottom/top/left/right`,由组件内 state 控制 - 必须移除 `pointer-events: none` ```jsx import { React } from 'uebersicht' export const refreshFrequency = false export const className = ` position: fixed; font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif; ` const WIDGET_ID = 'my-widget' const DraggableWidget = () => { const { useState, useEffect, useRef, useCallback } = React const storageKey = `oc-pos-${WIDGET_ID}` const saved = JSON.parse(localStorage.getItem(storageKey) || 'null') const [pos, setPos] = useState(saved || { x: 200, y: 200 }) const dragging = useRef(false) const offset = useRef({ x: 0, y: 0 }) const onMouseDown = useCallback((e) => { dragging.current = true offset.current = { x: e.clientX - pos.x, y: e.clientY - pos.y } e.preventDefault() }, [pos]) useEffect(() => { const onMove = (e) => { if (!dragging.current) return const next = { x: e.clientX - offset.current.x, y: e.clientY - offset.current.y } setPos(next) localStorage.setItem(storageKey, JSON.stringify(next)) } const onUp = () => { dragging.current = false } window.addEventListener('mousemove', onMove) window.addEventListener('mouseup', onUp) return () => { window.removeEventListener('mousemove', onMove) window.removeEventListener('mouseup', onUp) } }, []) return (
{/* widget 内容 */}
) } export const render = () => ``` --- ## 2. 状态持久化(localStorage) widget 被 Übersicht 重新 mount 时恢复之前的状态,防止状态丢失。 **使用场景**:计时器、计数器、待办列表等需要持久状态的 widget ```jsx // 封装 hook:用法和 useState 一致,自动读写 localStorage const usePersistedState = (key, initial) => { const { useState, useEffect } = React const storageKey = `oc-${key}` const [value, setValue] = useState(() => { const saved = localStorage.getItem(storageKey) return saved !== null ? JSON.parse(saved) : initial }) useEffect(() => { localStorage.setItem(storageKey, JSON.stringify(value)) }, [value]) return [value, setValue] } // 用法示例 const Widget = () => { const [count, setCount] = usePersistedState('counter', 0) return
setCount(c => c + 1)}>{count}
} ``` --- ## 3. Shell 输出安全解析 command 的 stdout 可能包含换行、空值、特殊字符。统一用分隔符 + 安全解析。 **使用场景**:所有使用 `command` 获取数据的 widget ```jsx // command 中用 ||| 作分隔符 export const command = ` echo "field1:$(some_command)" echo "field2:$(other_command)" ` // 安全解析函数 const parseOutput = (raw, fields) => { const result = {} fields.forEach(f => { result[f] = '?' }) if (!raw) return result raw.trim().split('\n').forEach(line => { const idx = line.indexOf(':') if (idx > -1) { const key = line.slice(0, idx).trim() const val = line.slice(idx + 1).trim() if (fields.includes(key)) result[key] = val } }) return result } // 用法 export const render = ({ output }) => { const data = parseOutput(output, ['field1', 'field2']) return
{data.field1}
} ``` --- ## 4. 条件渲染 + 占位符 command 首次执行前 output 为空,避免闪烁或显示错误。 **使用场景**:所有 command-based widget ```jsx export const render = ({ output, error }) => { if (error) return
Error
if (!output) return
Loading...
// 正常渲染 return
{output.trim()}
} ``` ---