feat: 抖音直播间弹幕监控 - Go+Wails 实现
后端: - SQLite 数据层 (rooms/users/chat_logs/settings/error_logs) - 抖音 WebSocket 抓取器 (proto 解析, 心跳, watchdog) - __ac_signature 算法 (纯 Go 移植) - a_bogus/sign.js 签名 (goja JS 引擎) - Echo HTTP API 服务器 (端口 5000) - SSE 事件广播 - 自动保活 + 授权守护 - PAYS 云授权对接 前端: - 顶栏: 品牌 + 日志/调试/客户名单/授权/文档/房间控制/主题切换 - 添加直播间 (链接解析 + 关键词) - Dashboard (卡片/列表视图, 公开/匿名房筛选, 分页) - 详情页 (多 tab 消息流, 右侧用户面板, 颜色标记, 精准客户, 已看标记) - 精准客户名单 (跨房间汇总) - 订阅授权页面 (登录/注册/购买) - 使用帮助 + 系统调试 - 白昼/黑夜主题切换 (CSS 变量 + iconfont) - 底部版权栏
This commit is contained in:
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>抖音直播间需求抓取</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
1919
frontend/package-lock.json
generated
Normal file
1919
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
frontend/package.json
Normal file
23
frontend/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"qrcode.react": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"vite": "^6.0.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
1
frontend/package.json.md5
Normal file
1
frontend/package.json.md5
Normal file
@@ -0,0 +1 @@
|
||||
2e6f182c838e63c2b9afa8bd486d948a
|
||||
1500
frontend/src/App.css
Normal file
1500
frontend/src/App.css
Normal file
File diff suppressed because it is too large
Load Diff
305
frontend/src/App.tsx
Normal file
305
frontend/src/App.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { errorReporter } from './errorReporter'
|
||||
import Header from './components/Header'
|
||||
import Dashboard from './components/Dashboard'
|
||||
import DetailView from './components/DetailView'
|
||||
import LeadView from './components/LeadView'
|
||||
import LicenseView from './components/LicenseView'
|
||||
import DocView from './components/DocView'
|
||||
import DebugView from './components/DebugView'
|
||||
import { ICONS } from './constants'
|
||||
import { useLs } from './utils'
|
||||
import type { RoomData, SysState, LicenseState, Page, RoomMessage } from './types'
|
||||
|
||||
errorReporter()
|
||||
|
||||
const API = (path: string, init?: RequestInit) => fetch(`/api${path}`, init)
|
||||
|
||||
function compactIcon(label: string, active: boolean, click: () => void) {
|
||||
return (
|
||||
<button className={`topbar-btn compact ${active ? 'active' : ''}`} onClick={click} title={label}>
|
||||
{ICONS.room}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [theme, setTheme] = useLs<'light'|'dark'>('theme', 'dark')
|
||||
const [rooms, setRooms] = useState<Record<string, RoomData>>({})
|
||||
const [activeRoom, setActiveRoom] = useState<string | null>(null)
|
||||
const [page, setPage] = useState<Page>('home')
|
||||
const [sys, setSys] = useState<SysState>({ version: '', rooms_total: 0, running: 0, error_count: 0, uptime: 0 })
|
||||
const [license, setLicense] = useState<LicenseState | null>(null)
|
||||
const [focusUser, setFocusUser] = useState<string | null>(null)
|
||||
const [logsOpen, setLogsOpen] = useState(false)
|
||||
const [debugMode, setDebugMode] = useLs('debugMode', false)
|
||||
const [viewMode, setViewMode] = useLs<'card'|'list'>('viewMode', 'card')
|
||||
const [cardCols, setCardCols] = useLs('cardCols', 3)
|
||||
const [listPerPage, setListPerPage] = useLs('listPerPage', 20)
|
||||
const [filterKind, setFilterKind] = useState<'all'|'public'|'anon'>('all')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const msgIntervals = useRef<Record<string, ReturnType<typeof setInterval>>>({})
|
||||
|
||||
const needAuth = license && (license.state === 'unconfigured' || license.state === 'expired')
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.theme = theme
|
||||
if (theme === 'dark') {
|
||||
;(window as any).WindowSetDarkTheme?.()
|
||||
} else {
|
||||
;(window as any).WindowSetLightTheme?.()
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
;(window as any).WindowSetDebugMode?.(debugMode)
|
||||
}, [debugMode])
|
||||
|
||||
// SSE
|
||||
useEffect(() => {
|
||||
const es = new EventSource('/stream')
|
||||
es.onmessage = e => {
|
||||
try {
|
||||
const msg = JSON.parse(e.data)
|
||||
if (msg.type === 'sys') setSys(msg.data)
|
||||
else if (msg.type === 'room') {
|
||||
setRooms(prev => ({ ...prev, [msg.data.id]: msg.data }))
|
||||
if (msg.data.status === 'stopped' || msg.data.status === 'error') {
|
||||
if (msgIntervals.current[msg.data.id]) {
|
||||
clearInterval(msgIntervals.current[msg.data.id])
|
||||
delete msgIntervals.current[msg.data.id]
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (msg.type === 'msg') {
|
||||
setRooms(prev => {
|
||||
const r = prev[msg.room_id]
|
||||
if (!r) return prev
|
||||
const exists = r.messages.some(m => m.id === msg.data.id)
|
||||
if (exists) return prev
|
||||
return { ...prev, [msg.room_id]: { ...r, messages: [...r.messages, msg.data].slice(-500) } }
|
||||
})
|
||||
}
|
||||
else if (msg.type === 'license') setLicense(msg.data)
|
||||
} catch {}
|
||||
}
|
||||
es.onerror = () => es.close()
|
||||
return () => es.close()
|
||||
}, [])
|
||||
|
||||
// Message polling
|
||||
useEffect(() => {
|
||||
Object.keys(rooms).forEach(id => {
|
||||
const r = rooms[id]
|
||||
if (r.status === 'running' && !msgIntervals.current[id]) {
|
||||
msgIntervals.current[id] = setInterval(() => {
|
||||
API(`/api/rooms/${encodeURIComponent(id)}/msgs`).then(r => r.json()).then(data => {
|
||||
setRooms(prev => {
|
||||
const cur = prev[id]
|
||||
if (!cur) return prev
|
||||
const newMsgs = data.messages?.filter((m: RoomMessage) => !cur.messages.some(cm => cm.id === m.id)) || []
|
||||
if (!newMsgs.length) return prev
|
||||
return { ...prev, [id]: { ...cur, messages: [...cur.messages, ...newMsgs].slice(-500) } }
|
||||
})
|
||||
}).catch(() => {})
|
||||
}, 2500)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
Object.values(msgIntervals.current).forEach(clearInterval)
|
||||
msgIntervals.current = {}
|
||||
}
|
||||
}, [rooms])
|
||||
|
||||
// Load initial data
|
||||
useEffect(() => {
|
||||
Promise.all([API('/api/sys').then(r => r.json()).then(setSys), API('/api/license').then(r => r.json()).then(setLicense).catch(() => {}), API('/api/rooms').then(r => r.json()).then(setRooms)])
|
||||
}, [])
|
||||
|
||||
const addRoom = useCallback(async (url: string) => {
|
||||
if (needAuth) { setPage('license'); return }
|
||||
try {
|
||||
const res = await API('/api/rooms', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) })
|
||||
const data = await res.json()
|
||||
if (data.id) setRooms(prev => ({ ...prev, [data.id]: data }))
|
||||
} catch (e) { console.error(e) }
|
||||
}, [needAuth])
|
||||
|
||||
const toggleRoom = useCallback(async (id: string, action?: string) => {
|
||||
if (needAuth && action === 'start') { setPage('license'); return }
|
||||
try {
|
||||
await API(`/api/rooms/${encodeURIComponent(id)}/${action || 'toggle'}`, { method: 'POST' })
|
||||
} catch (e) { console.error(e) }
|
||||
}, [needAuth])
|
||||
|
||||
const removeRoom = useCallback(async (id: string) => {
|
||||
await API(`/api/rooms/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
||||
setRooms(prev => { const n = { ...prev }; delete n[id]; return n })
|
||||
if (activeRoom === id) { setPage('home'); setActiveRoom(null) }
|
||||
}, [activeRoom])
|
||||
|
||||
const setSeen = useCallback((roomId: string, msgId: string) => {
|
||||
setRooms(prev => {
|
||||
const r = prev[roomId]
|
||||
if (!r) return prev
|
||||
return { ...prev, [roomId]: { ...r, messages: r.messages.map(m => m.id === msgId ? { ...m, is_seen: true } : m) } }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const setLead = useCallback((roomId: string, msgId: string, lead: boolean) => {
|
||||
setRooms(prev => {
|
||||
const r = prev[roomId]
|
||||
if (!r) return prev
|
||||
return { ...prev, [roomId]: { ...r, messages: r.messages.map(m => m.id === msgId ? { ...m, is_lead: lead } : m), lead_count: lead ? r.lead_count + 1 : Math.max(0, r.lead_count - 1) } }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const setMark = useCallback((roomId: string, userId: string, mark: string) => {
|
||||
setRooms(prev => {
|
||||
const r = prev[roomId]
|
||||
if (!r) return prev
|
||||
return { ...prev, [roomId]: { ...r, messages: r.messages.map(m => m.user_id === userId ? { ...m, marked: mark } : m) } }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const allRooms = Object.values(rooms)
|
||||
const roomList = allRooms.filter(r => {
|
||||
if (filterKind === 'public' && r.kind === 'anon') return false
|
||||
if (filterKind === 'anon' && r.kind === 'public') return false
|
||||
if (searchQuery && !r.title.toLowerCase().includes(searchQuery.toLowerCase()) && !r.anchor_id.toLowerCase().includes(searchQuery.toLowerCase())) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const handleSetActive = (id: string) => {
|
||||
setActiveRoom(id)
|
||||
setPage('detail')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<TopBar
|
||||
compact={false}
|
||||
theme={theme} setTheme={setTheme}
|
||||
logsOpen={logsOpen} setLogsOpen={setLogsOpen}
|
||||
debugMode={debugMode} setDebugMode={setDebugMode}
|
||||
roomCount={allRooms.length} runningCount={sys.running}
|
||||
needAuth={needAuth}
|
||||
onLeads={() => setPage('leads')}
|
||||
onLicense={() => setPage('license')}
|
||||
onDoc={() => setPage('doc')}
|
||||
onDebug={() => setPage('debug')}
|
||||
onStartAll={() => allRooms.filter(r=>r.status==='stopped').forEach(r=>toggleRoom(r.id,'start'))}
|
||||
onStopAll={() => allRooms.filter(r=>r.status==='running').forEach(r=>toggleRoom(r.id,'stop'))}
|
||||
onUser={() => {}}
|
||||
/>
|
||||
<div className="app-content">
|
||||
{page === 'home' && (
|
||||
<Header
|
||||
roomCount={roomList.length}
|
||||
viewMode={viewMode} setViewMode={setViewMode}
|
||||
cardCols={cardCols} setCardCols={setCardCols}
|
||||
listPerPage={listPerPage} setListPerPage={setListPerPage}
|
||||
filterKind={filterKind} setFilterKind={setFilterKind}
|
||||
searchQuery={searchQuery} setSearchQuery={setSearchQuery}
|
||||
needAuth={needAuth}
|
||||
onLeads={() => setPage('leads')}
|
||||
onLicense={() => setPage('license')}
|
||||
onDoc={() => setPage('doc')}
|
||||
onDebug={() => setPage('debug')}
|
||||
onLogToggle={() => setLogsOpen(v => !v)}
|
||||
onStartAll={() => allRooms.filter(r=>r.status==='stopped').forEach(r=>toggleRoom(r.id,'start'))}
|
||||
onStopAll={() => allRooms.filter(r=>r.status==='running').forEach(r=>toggleRoom(r.id,'stop'))}
|
||||
/>
|
||||
)}
|
||||
{page === 'home' && (
|
||||
<Dashboard
|
||||
rooms={roomList}
|
||||
viewMode={viewMode} cardCols={cardCols} listPerPage={listPerPage}
|
||||
onAddRoom={addRoom}
|
||||
onToggleRoom={toggleRoom}
|
||||
onRemoveRoom={removeRoom}
|
||||
onSetActive={handleSetActive}
|
||||
/>
|
||||
)}
|
||||
{page === 'detail' && activeRoom && (
|
||||
<DetailView
|
||||
room={rooms[activeRoom]}
|
||||
focusUser={focusUser} setFocusUser={setFocusUser}
|
||||
onBack={() => setPage('home')}
|
||||
onSeen={setSeen}
|
||||
onLead={setLead}
|
||||
onMark={setMark}
|
||||
/>
|
||||
)}
|
||||
{page === 'leads' && <LeadView rooms={allRooms} />}
|
||||
{page === 'license' && <LicenseView license={license} onBack={() => setPage('home')} />}
|
||||
{page === 'doc' && <DocView />}
|
||||
{page === 'debug' && <DebugView sys={sys} rooms={allRooms} />}
|
||||
</div>
|
||||
<div className="footer">
|
||||
<span>青梧映星软件开发工作室</span>
|
||||
<span>开发者: Geliebte</span>
|
||||
<button className="footer-link" onClick={() => (window as any).BrowserOpenURL?.('https://yser.ging.plus/')}>客户服务中心</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TopBarProps {
|
||||
compact: boolean
|
||||
theme: 'light' | 'dark'
|
||||
setTheme: (v: 'light'|'dark') => void
|
||||
logsOpen: boolean
|
||||
setLogsOpen: (v: boolean) => void
|
||||
debugMode: boolean
|
||||
setDebugMode: (v: boolean) => void
|
||||
roomCount: number
|
||||
runningCount: number
|
||||
needAuth: boolean
|
||||
onLeads: () => void
|
||||
onLicense: () => void
|
||||
onDoc: () => void
|
||||
onDebug: () => void
|
||||
onStartAll: () => void
|
||||
onStopAll: () => void
|
||||
onUser: () => void
|
||||
}
|
||||
|
||||
function TopBar({ compact, theme, setTheme, logsOpen, debugMode, roomCount, runningCount, needAuth, onLeads, onLicense, onDoc, onDebug, onStartAll, onStopAll, onUser }: TopBarProps) {
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="topbar">
|
||||
<button className="topbar-btn" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} title="切换主题">
|
||||
{theme === 'dark' ? '🌙' : '☀️'}
|
||||
</button>
|
||||
<button className="topbar-btn" onClick={onUser} title="用户">👤</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="topbar">
|
||||
<div className="topbar-brand">
|
||||
<span className="topbar-logo">🎬</span>
|
||||
<span className="topbar-title">抖音直播间需求抓取</span>
|
||||
<span className="room-count-top">{roomCount} 房 / {runningCount} 运行</span>
|
||||
</div>
|
||||
<div className="topbar-actions">
|
||||
<button className="topbar-btn" onClick={onDebug} title="调试" style={{ display: debugMode ? '' : 'none' }}>🔧 调试</button>
|
||||
<button className="topbar-btn" onClick={onLeads} title="客户名单">🎯 客户</button>
|
||||
<button className="topbar-btn" onClick={onLicense} title="授权" style={needAuth ? { color: '#ff4d4f' } : {}}>{needAuth ? '⚠️ 授权' : '🔑 授权'}</button>
|
||||
<button className="topbar-btn" onClick={onDoc} title="文档">📖 文档</button>
|
||||
{runningCount > 0
|
||||
? <button className="topbar-btn topbar-stop" onClick={onStopAll}>⏹ 全部停止</button>
|
||||
: roomCount > 0
|
||||
? <button className="topbar-btn topbar-start" onClick={onStartAll}>▶ 全部开始</button>
|
||||
: null
|
||||
}
|
||||
<button className="topbar-btn" onClick={() => onDebug()} title="日志">📋 日志{logsOpen ? '🟢' : ''}</button>
|
||||
<button className="topbar-btn" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} title="切换主题">
|
||||
{theme === 'dark' ? '🌙' : '☀️'}
|
||||
</button>
|
||||
<button className="topbar-btn" onClick={onUser} title="用户">👤</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
BIN
frontend/src/assets/fonts/fa/fa-solid-900.woff2
Normal file
BIN
frontend/src/assets/fonts/fa/fa-solid-900.woff2
Normal file
Binary file not shown.
41
frontend/src/components/AddRoomBar.tsx
Normal file
41
frontend/src/components/AddRoomBar.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { useState } from 'react'
|
||||
import RoomTools from './RoomTools'
|
||||
|
||||
interface AddRoomBarProps {
|
||||
onAdd: (url: string) => void
|
||||
}
|
||||
|
||||
export default function AddRoomBar({ onAdd }: AddRoomBarProps) {
|
||||
const [url, setUrl] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setError('')
|
||||
const trimmed = url.trim()
|
||||
if (!trimmed) { setError('请输入直播间URL'); return }
|
||||
try {
|
||||
await onAdd(trimmed)
|
||||
setUrl('')
|
||||
} catch (e: any) {
|
||||
setError(e.message || '添加失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="add-bar">
|
||||
<div className="add-input-wrap">
|
||||
<input
|
||||
className="add-input"
|
||||
type="text"
|
||||
placeholder="粘贴直播间URL或主播主页链接..."
|
||||
value={url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleSubmit()}
|
||||
/>
|
||||
<button className="add-btn" onClick={handleSubmit}>➕ 添加</button>
|
||||
</div>
|
||||
{error && <span className="add-error">{error}</span>}
|
||||
<RoomTools />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
102
frontend/src/components/Dashboard.tsx
Normal file
102
frontend/src/components/Dashboard.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import RoomCard from './RoomCard'
|
||||
import AddRoomBar from './AddRoomBar'
|
||||
import type { RoomData } from '../types'
|
||||
|
||||
interface DashboardProps {
|
||||
rooms: RoomData[]
|
||||
viewMode: 'card' | 'list'
|
||||
cardCols: number
|
||||
listPerPage: number
|
||||
onAddRoom: (url: string) => void
|
||||
onToggleRoom: (id: string, action?: string) => void
|
||||
onRemoveRoom: (id: string) => void
|
||||
onSetActive: (id: string) => void
|
||||
}
|
||||
|
||||
export default function Dashboard({ rooms, viewMode, cardCols, listPerPage, onAddRoom, onToggleRoom, onRemoveRoom, onSetActive }: DashboardProps) {
|
||||
const [page, setPage] = useState(0)
|
||||
|
||||
const paginatedRooms = useMemo(() => {
|
||||
if (viewMode === 'list') {
|
||||
const start = page * listPerPage
|
||||
return rooms.slice(start, start + listPerPage)
|
||||
}
|
||||
return rooms
|
||||
}, [rooms, page, listPerPage, viewMode])
|
||||
|
||||
const totalPages = viewMode === 'list' ? Math.ceil(rooms.length / listPerPage) : 0
|
||||
|
||||
return (
|
||||
<div className="dash-zone">
|
||||
<AddRoomBar onAdd={onAddRoom} />
|
||||
{viewMode === 'card' ? (
|
||||
<div className="room-grid" style={{ gridTemplateColumns: `repeat(${cardCols}, 1fr)` }}>
|
||||
{paginatedRooms.map(room => (
|
||||
<RoomCard
|
||||
key={room.id}
|
||||
room={room}
|
||||
onToggle={(a) => onToggleRoom(room.id, a)}
|
||||
onRemove={() => onRemoveRoom(room.id)}
|
||||
onDetail={() => onSetActive(room.id)}
|
||||
/>
|
||||
))}
|
||||
{paginatedRooms.length === 0 && (
|
||||
<div className="empty-state">
|
||||
<p>暂无房间</p>
|
||||
<p>在上方输入框添加直播间URL开始监控</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="room-list">
|
||||
<table className="list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>状态</th>
|
||||
<th>房间</th>
|
||||
<th>主播</th>
|
||||
<th>类型</th>
|
||||
<th>观众</th>
|
||||
<th>消息</th>
|
||||
<th>精准</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedRooms.map(room => (
|
||||
<tr key={room.id} className={`list-row ${room.status}`} onClick={() => onSetActive(room.id)}>
|
||||
<td><span className={`status-dot ${room.status}`} /></td>
|
||||
<td className="list-title">{room.title}</td>
|
||||
<td>{room.anchor_id}</td>
|
||||
<td><span className={`kind-badge ${room.kind}`}>{room.kind === 'public' ? '公开' : '匿名'}</span></td>
|
||||
<td>{room.viewers}</td>
|
||||
<td>{room.total_msg}</td>
|
||||
<td>{room.lead_count}</td>
|
||||
<td className="list-actions">
|
||||
<button className="list-btn start" onClick={e => { e.stopPropagation(); onToggleRoom(room.id, 'start') }}>▶</button>
|
||||
<button className="list-btn stop" onClick={e => { e.stopPropagation(); onToggleRoom(room.id, 'stop') }}>⏹</button>
|
||||
<button className="list-btn del" onClick={e => { e.stopPropagation(); onRemoveRoom(room.id) }}>✕</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{paginatedRooms.length === 0 && (
|
||||
<div className="empty-state">
|
||||
<p>暂无房间</p>
|
||||
<p>在上方输入框添加直播间URL开始监控</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{totalPages > 1 && (
|
||||
<div className="pagination">
|
||||
<button className="page-btn" disabled={page === 0} onClick={() => setPage(p => p - 1)}>‹</button>
|
||||
<span className="page-info">{page + 1} / {totalPages}</span>
|
||||
<button className="page-btn" disabled={page >= totalPages - 1} onClick={() => setPage(p => p + 1)}>›</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
103
frontend/src/components/DebugView.tsx
Normal file
103
frontend/src/components/DebugView.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import type { SysState, RoomData } from '../types'
|
||||
|
||||
interface DebugViewProps {
|
||||
sys: SysState
|
||||
rooms: RoomData[]
|
||||
}
|
||||
|
||||
export default function DebugView({ sys, rooms }: DebugViewProps) {
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
const [wsConnected, setWsConnected] = useState(false)
|
||||
const logEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const es = new EventSource('/stream')
|
||||
es.onmessage = e => {
|
||||
const line = `[${new Date().toLocaleTimeString()}] ${e.data}`
|
||||
setLogs(prev => [...prev.slice(-500), line])
|
||||
}
|
||||
es.onerror = () => setWsConnected(false)
|
||||
es.onopen = () => setWsConnected(true)
|
||||
return () => { es.close() }
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
logEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [logs])
|
||||
|
||||
return (
|
||||
<div className="debug-view">
|
||||
<button className="back-btn" onClick={() => window.history.back()}>← 返回</button>
|
||||
<h2>🔧 系统调试</h2>
|
||||
|
||||
<div className="debug-section">
|
||||
<h3>系统状态</h3>
|
||||
<div className="debug-grid">
|
||||
<div className="debug-item"><span className="label">版本</span><span className="value">{sys.version || '未知'}</span></div>
|
||||
<div className="debug-item"><span className="label">房间总数</span><span className="value">{sys.rooms_total}</span></div>
|
||||
<div className="debug-item"><span className="label">运行中</span><span className="value running">{sys.running}</span></div>
|
||||
<div className="debug-item"><span className="label">错误数</span><span className="value error">{sys.error_count}</span></div>
|
||||
<div className="debug-item"><span className="label">运行时长</span><span className="value">{formatUptime(sys.uptime)}</span></div>
|
||||
<div className="debug-item"><span className="label">WebSocket</span><span className={`value ${wsConnected ? 'connected' : 'disconnected'}`}>{wsConnected ? '已连接' : '未连接'}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="debug-section">
|
||||
<h3>房间状态 ({rooms.length})</h3>
|
||||
<div className="debug-grid">
|
||||
{rooms.map(r => (
|
||||
<div key={r.id} className={`debug-item room-item ${r.status}`}>
|
||||
<span className="label">{r.anchor_id}</span>
|
||||
<span className="value">{r.status} | 观众:{r.viewers} | 消息:{r.total_msg} | 精准:{r.lead_count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="debug-section">
|
||||
<h3>实时日志 <button className="clear-btn" onClick={() => setLogs([])}>清空</button></h3>
|
||||
<div className="log-container">
|
||||
{logs.length === 0 ? <div className="no-logs">暂无日志</div> : logs.map((l, i) => (
|
||||
<div key={i} className="log-line">{l}</div>
|
||||
))}
|
||||
<div ref={logEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="debug-section">
|
||||
<h3>手动测试</h3>
|
||||
<div className="test-actions">
|
||||
<button onClick={async () => {
|
||||
try {
|
||||
const r = await fetch('/api/sys')
|
||||
const d = await r.json()
|
||||
setLogs(prev => [...prev, `[TEST] /api/sys: ${JSON.stringify(d)}`])
|
||||
} catch (e) { setLogs(prev => [...prev, `[TEST] Error: ${e}`]) }
|
||||
}}>测试 /api/sys</button>
|
||||
<button onClick={async () => {
|
||||
try {
|
||||
const r = await fetch('/api/rooms')
|
||||
const d = await r.json()
|
||||
setLogs(prev => [...prev, `[TEST] /api/rooms: ${Object.keys(d).length} rooms`])
|
||||
} catch (e) { setLogs(prev => [...prev, `[TEST] Error: ${e}`]) }
|
||||
}}>测试 /api/rooms</button>
|
||||
<button onClick={async () => {
|
||||
try {
|
||||
const r = await fetch('/api/license')
|
||||
const d = await r.json()
|
||||
setLogs(prev => [...prev, `[TEST] /api/license: ${JSON.stringify(d)}`])
|
||||
} catch (e) { setLogs(prev => [...prev, `[TEST] Error: ${e}`]) }
|
||||
}}>测试 /api/license</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatUptime(s: number): string {
|
||||
const h = Math.floor(s / 3600)
|
||||
const m = Math.floor((s % 3600) / 60)
|
||||
const sec = s % 60
|
||||
return `${h}h ${m}m ${sec}s`
|
||||
}
|
||||
193
frontend/src/components/DetailView.tsx
Normal file
193
frontend/src/components/DetailView.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import React, { useState, useMemo, useEffect, useRef } from 'react'
|
||||
import { ICONS, TYPE_LABELS, MARKS, MARK_MAP } from '../constants'
|
||||
import { formatTime } from '../utils'
|
||||
import type { RoomData, RoomMessage } from '../types'
|
||||
|
||||
interface DetailViewProps {
|
||||
room: RoomData | undefined
|
||||
focusUser: string | null
|
||||
setFocusUser: (id: string | null) => void
|
||||
onBack: () => void
|
||||
onSeen: (roomId: string, msgId: string) => void
|
||||
onLead: (roomId: string, msgId: string, lead: boolean) => void
|
||||
onMark: (roomId: string, userId: string, mark: string) => void
|
||||
}
|
||||
|
||||
export default function DetailView({ room, focusUser, setFocusUser, onBack, onSeen, onLead, onMark }: DetailViewProps) {
|
||||
const [activeTab, setActiveTab] = useState('all')
|
||||
const [markFilter, setMarkFilter] = useState<string>('')
|
||||
const [showLeadOnly, setShowLeadOnly] = useState(false)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
if (!room) return <div className="detail-empty">房间不存在</div>
|
||||
|
||||
const tabs = ['all', 'chat', 'gift', 'enter', 'like', 'follow', 'stats', 'fansclub', 'rank', 'status', 'lead']
|
||||
|
||||
const filteredMsgs = useMemo(() => {
|
||||
let msgs = room.messages
|
||||
if (activeTab !== 'all') {
|
||||
if (activeTab === 'lead') {
|
||||
msgs = msgs.filter(m => m.is_lead)
|
||||
} else {
|
||||
msgs = msgs.filter(m => m.type === activeTab)
|
||||
}
|
||||
}
|
||||
if (markFilter) {
|
||||
msgs = msgs.filter(m => m.marked === markFilter)
|
||||
}
|
||||
if (focusUser) {
|
||||
msgs = msgs.filter(m => m.user_id === focusUser)
|
||||
}
|
||||
return msgs
|
||||
}, [room.messages, activeTab, markFilter, focusUser])
|
||||
|
||||
const userStats = useMemo(() => {
|
||||
const map = new Map<string, { name: string; count: number; leads: number; msgs: RoomMessage[] }>()
|
||||
room.messages.forEach(m => {
|
||||
const existing = map.get(m.user_id)
|
||||
if (existing) {
|
||||
existing.count++
|
||||
if (m.is_lead) existing.leads++
|
||||
} else {
|
||||
map.set(m.user_id, { name: m.username, count: 1, leads: m.is_lead ? 1 : 0, msgs: [m] })
|
||||
}
|
||||
})
|
||||
return Array.from(map.values()).sort((a, b) => b.count - a.count)
|
||||
}, [room.messages])
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
}, [filteredMsgs.length])
|
||||
|
||||
return (
|
||||
<div className="detail-view">
|
||||
<div className="detail-header">
|
||||
<button className="back-btn" onClick={onBack}>← 返回</button>
|
||||
<div className="detail-title">
|
||||
<span className="detail-room-name">{room.title || room.anchor_id}</span>
|
||||
<span className={`detail-status ${room.status}`}>{room.status === 'running' ? '● 直播中' : room.status === 'error' ? '✕ 错误' : '○ 已停止'}</span>
|
||||
</div>
|
||||
<div className="detail-stats">
|
||||
<span>👁 {room.viewers}</span>
|
||||
<span>💬 {room.total_msg}</span>
|
||||
<span>🎯 {room.lead_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="detail-main">
|
||||
<div className="detail-left">
|
||||
<div className="tab-bar">
|
||||
{tabs.map(tab => (
|
||||
<button key={tab} className={`tab-btn ${activeTab === tab ? 'active' : ''}`} onClick={() => setActiveTab(tab)}>
|
||||
{ICONS[tab as keyof typeof ICONS]} {TYPE_LABELS[tab] || tab}
|
||||
{tab === 'all' && <span className="tab-count">{room.messages.length}</span>}
|
||||
{tab === 'lead' && <span className="tab-count">{room.messages.filter(m => m.is_lead).length}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="detail-filters">
|
||||
<select value={markFilter} onChange={e => setMarkFilter(e.target.value)}>
|
||||
<option value="">全部标记</option>
|
||||
{MARKS.map(m => <option key={m.key} value={m.key}>{m.label}标记</option>)}
|
||||
</select>
|
||||
<label className="lead-filter">
|
||||
<input type="checkbox" checked={showLeadOnly} onChange={e => setShowLeadOnly(e.target.checked)} />
|
||||
仅看精准客户
|
||||
</label>
|
||||
{focusUser && (
|
||||
<button className="clear-focus" onClick={() => setFocusUser(null)}>清除用户筛选 ({focusUser})</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="msg-container" ref={scrollRef}>
|
||||
{filteredMsgs.length === 0 ? (
|
||||
<div className="no-msgs">暂无消息</div>
|
||||
) : (
|
||||
filteredMsgs.map(msg => (
|
||||
<MsgCard
|
||||
key={msg.id}
|
||||
msg={msg}
|
||||
isLead={msg.is_lead}
|
||||
onFocus={() => setFocusUser(msg.user_id)}
|
||||
onSeen={() => onSeen(room.id, msg.id)}
|
||||
onLead={(v) => onLead(room.id, msg.id, v)}
|
||||
onMark={(v) => onMark(room.id, msg.user_id, v)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="detail-right">
|
||||
<div className="user-panel">
|
||||
<h3>👥 用户统计</h3>
|
||||
<div className="user-list">
|
||||
{userStats.slice(0, 50).map(u => (
|
||||
<div key={u.name} className={`user-item ${focusUser === u.name ? 'focused' : ''} ${u.leads > 0 ? 'has-lead' : ''}`}>
|
||||
<div className="user-info">
|
||||
<span className="user-name" onClick={() => setFocusUser(focusUser === u.name ? null : u.name)}>{u.name}</span>
|
||||
<span className="user-count">{u.count}次</span>
|
||||
</div>
|
||||
<div className="user-actions">
|
||||
{u.leads > 0 && <span className="user-lead-count">🎯{u.leads}</span>}
|
||||
<select className="user-mark-select" value={MARKS.find(m => room.messages.some(mm => mm.user_id === (u.name)) && mm.marked === m.key)?.key || ''} onChange={e => {
|
||||
const userId = u.name
|
||||
onMark(room.id, userId, e.target.value || undefined)
|
||||
}}>
|
||||
<option value="">标记</option>
|
||||
{MARKS.map(m => <option key={m.key} value={m.key}>{m.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface MsgCardProps {
|
||||
msg: RoomMessage
|
||||
isLead: boolean
|
||||
onFocus: () => void
|
||||
onSeen: () => void
|
||||
onLead: (v: boolean) => void
|
||||
onMark: (v: string) => void
|
||||
}
|
||||
|
||||
function MsgCard({ msg, isLead, onFocus, onSeen, onLead, onMark }: MsgCardProps) {
|
||||
const typeIcon = ICONS[msg.type as keyof typeof ICONS] || ICONS.chat
|
||||
|
||||
return (
|
||||
<div className={`msg-card ${msg.type} ${isLead ? 'lead' : ''} ${msg.marked ? `marked-${msg.marked}` : ''} ${msg.is_seen ? 'seen' : ''}`}>
|
||||
<div className="msg-header">
|
||||
<span className="msg-type">{typeIcon} {TYPE_LABELS[msg.type] || msg.type}</span>
|
||||
<span className="msg-user" onClick={onFocus} title="点击筛选该用户">{msg.username}</span>
|
||||
{msg.fans_level > 0 && <span className="msg-fans">⭐{msg.fans_level}</span>}
|
||||
<span className="msg-time">{formatTime(msg.timestamp)}</span>
|
||||
</div>
|
||||
<div className="msg-body">
|
||||
{msg.type === 'gift' && msg.gift_name && (
|
||||
<span className="gift-info">{msg.gift_name} ×{msg.gift_num} ({msg.gift_price}抖币)</span>
|
||||
)}
|
||||
{msg.content && <span className="msg-text">{msg.content}</span>}
|
||||
</div>
|
||||
<div className="msg-actions">
|
||||
<button className={`mark-btn ${msg.marked || ''}`} onClick={() => onMark(msg.marked === 'blue' ? '' : 'blue')}>蓝</button>
|
||||
<button className={`mark-btn ${msg.marked || ''}`} onClick={() => onMark(msg.marked === 'red' ? '' : 'red')}>红</button>
|
||||
<button className={`mark-btn ${msg.marked || ''}`} onClick={() => onMark(msg.marked === 'yellow' ? '' : 'yellow')}>黄</button>
|
||||
<button className={`lead-btn ${isLead ? 'active' : ''}`} onClick={() => onLead(!isLead)}>{isLead ? '🎯已精准' : '🎯精准'}</button>
|
||||
{!msg.is_seen && <button className="seen-btn" onClick={onSeen}>✓已看</button>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
75
frontend/src/components/DocView.tsx
Normal file
75
frontend/src/components/DocView.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function DocView() {
|
||||
return (
|
||||
<div className="doc-view">
|
||||
<button className="back-btn" onClick={() => window.history.back()}>← 返回</button>
|
||||
<h2>📖 使用帮助</h2>
|
||||
|
||||
<div className="doc-section">
|
||||
<h3>一、快速开始</h3>
|
||||
<ol>
|
||||
<li>在首页输入框粘贴直播间URL(支持抖音直播间分享链接或主播主页链接)</li>
|
||||
<li>点击「添加」按钮,系统会自动解析并开始监控</li>
|
||||
<li>在房间卡片上点击「▶」启动监控,或点击「全部开始」一键启动所有房间</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="doc-section">
|
||||
<h3>二、房间操作</h3>
|
||||
<ul>
|
||||
<li><strong>启动/停止</strong>:点击卡片上的 ▶ / ⏹ 按钮,或鼠标悬停卡片显示控制按钮</li>
|
||||
<li><strong>查看详情</strong>:点击卡片进入详情页,查看所有消息流</li>
|
||||
<li><strong>删除房间</strong>:点击卡片右上角 ✕ 按钮</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="doc-section">
|
||||
<h3>三、消息标记与精准客户</h3>
|
||||
<ul>
|
||||
<li><strong>颜色标记</strong>:在消息操作栏点击「蓝」「红」「黄」,可对用户进行颜色标记,便于识别不同需求的用户</li>
|
||||
<li><strong>精准客户</strong>:点击消息的「🎯精准」按钮,将该用户标记为精准客户</li>
|
||||
<li><strong>已看标记</strong>:点击「✓已看」标记消息已浏览</li>
|
||||
<li><strong>用户筛选</strong>:点击详情页右侧用户列表中的用户名,可筛选仅显示该用户的消息</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="doc-section">
|
||||
<h3>四、视图切换</h3>
|
||||
<ul>
|
||||
<li><strong>卡片视图</strong>:网格展示房间卡片,可通过列数选择器调整列数(2-5列)</li>
|
||||
<li><strong>列表视图</strong>:表格展示房间列表,可通过每页条数选择器调整分页</li>
|
||||
<li><strong>搜索过滤</strong>:在搜索框输入关键词,可按房间名或主播ID过滤</li>
|
||||
<li><strong>类型过滤</strong>:点击「公开房」/「匿名房」按钮筛选房间类型</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="doc-section">
|
||||
<h3>五、精准客户汇总</h3>
|
||||
<p>点击顶部「🎯 客户」按钮进入客户名单页,查看所有房间的精准客户汇总,支持按房间筛选。</p>
|
||||
</div>
|
||||
|
||||
<div className="doc-section">
|
||||
<h3>六、主题切换</h3>
|
||||
<p>点击顶栏 🌙/☀️ 按钮切换深色/浅色主题,主题偏好会自动保存。</p>
|
||||
</div>
|
||||
|
||||
<div className="doc-section">
|
||||
<h3>七、常见问题</h3>
|
||||
<dl>
|
||||
<dt>Q: 添加房间后显示「授权失效」?</dt>
|
||||
<dd>A: 请点击顶栏「🔑 授权」按钮,输入授权码完成激活。</dd>
|
||||
<dt>Q: 消息不实时更新?</dt>
|
||||
<dd>A: 检查网络连接,如使用SSE连接失败会自动降级为轮询模式。</dd>
|
||||
<dt>Q: 如何导出精准客户数据?</dt>
|
||||
<dd>A: 在详情页点击精准客户筛选,右键复制消息内容,或联系开发者获取更多导出功能。</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="doc-section">
|
||||
<h3>八、技术支持</h3>
|
||||
<p>如有问题请联系客户服务中心:https://yser.ging.plus/</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
79
frontend/src/components/Header.tsx
Normal file
79
frontend/src/components/Header.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react'
|
||||
import { ICONS } from '../constants'
|
||||
import type { AppState } from '../types'
|
||||
|
||||
interface HeaderProps {
|
||||
roomCount: number
|
||||
viewMode: 'card' | 'list'
|
||||
setViewMode: (v: 'card' | 'list') => void
|
||||
cardCols: number
|
||||
setCardCols: (v: number) => void
|
||||
listPerPage: number
|
||||
setListPerPage: (v: number) => void
|
||||
filterKind: 'all' | 'public' | 'anon'
|
||||
setFilterKind: (v: 'all' | 'public' | 'anon') => void
|
||||
searchQuery: string
|
||||
setSearchQuery: (v: string) => void
|
||||
needAuth: boolean
|
||||
onLeads: () => void
|
||||
onLicense: () => void
|
||||
onDoc: () => void
|
||||
onDebug: () => void
|
||||
onLogToggle: () => void
|
||||
onStartAll: () => void
|
||||
onStopAll: () => void
|
||||
}
|
||||
|
||||
export default function Header({
|
||||
roomCount, viewMode, setViewMode, cardCols, setCardCols, listPerPage, setListPerPage,
|
||||
filterKind, setFilterKind, searchQuery, setSearchQuery, needAuth,
|
||||
onLeads, onLicense, onDoc, onDebug, onLogToggle, onStartAll, onStopAll
|
||||
}: HeaderProps) {
|
||||
return (
|
||||
<div className="header">
|
||||
<div className="header-left">
|
||||
<span className="room-count">{ICONS.room} {roomCount} 个房间</span>
|
||||
<div className="filter-group">
|
||||
<button className={`filter-btn ${filterKind === 'all' ? 'active' : ''}`} onClick={() => setFilterKind('all')}>全部</button>
|
||||
<button className={`filter-btn ${filterKind === 'public' ? 'active' : ''}`} onClick={() => setFilterKind('public')}>公开房</button>
|
||||
<button className={`filter-btn ${filterKind === 'anon' ? 'active' : ''}`} onClick={() => setFilterKind('anon')}>匿名房</button>
|
||||
</div>
|
||||
<input
|
||||
className="search-input"
|
||||
type="text"
|
||||
placeholder="搜索房间/主播..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="header-right">
|
||||
<button className="view-toggle" onClick={onLeads} title="客户名单">🎯</button>
|
||||
<button className={`view-toggle ${needAuth ? 'auth-warn' : ''}`} onClick={onLicense} title="授权">
|
||||
{needAuth ? '⚠️' : '🔑'}
|
||||
</button>
|
||||
<button className="view-toggle" onClick={onDoc} title="文档">📖</button>
|
||||
<button className="view-toggle" onClick={onDebug} title="调试">🔧</button>
|
||||
<button className={`view-toggle ${viewMode === 'card' ? 'active' : ''}`} onClick={() => setViewMode('card')} title="卡片视图">🗂</button>
|
||||
<button className={`view-toggle ${viewMode === 'list' ? 'active' : ''}`} onClick={() => setViewMode('list')} title="列表视图">☰</button>
|
||||
{viewMode === 'card' ? (
|
||||
<select className="col-select" value={cardCols} onChange={e => setCardCols(Number(e.target.value))}>
|
||||
<option value={2}>2列</option>
|
||||
<option value={3}>3列</option>
|
||||
<option value={4}>4列</option>
|
||||
<option value={5}>5列</option>
|
||||
</select>
|
||||
) : (
|
||||
<select className="col-select" value={listPerPage} onChange={e => setListPerPage(Number(e.target.value))}>
|
||||
<option value={10}>10条/页</option>
|
||||
<option value={20}>20条/页</option>
|
||||
<option value={50}>50条/页</option>
|
||||
<option value={100}>100条/页</option>
|
||||
</select>
|
||||
)}
|
||||
<button className="action-btn start-btn" onClick={onStartAll}>▶ 全部开始</button>
|
||||
<button className="action-btn stop-btn" onClick={onStopAll}>⏹ 全部停止</button>
|
||||
<button className="view-toggle" onClick={onLogToggle} title="日志开关">📋</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
13
frontend/src/components/KindBadge.tsx
Normal file
13
frontend/src/components/KindBadge.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
interface KindBadgeProps {
|
||||
kind: 'public' | 'anon'
|
||||
}
|
||||
|
||||
export default function KindBadge({ kind }: KindBadgeProps) {
|
||||
return (
|
||||
<span className={`kind-badge ${kind}`}>
|
||||
{kind === 'public' ? '📢 公开' : '🎭 匿名'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
53
frontend/src/components/LeadView.tsx
Normal file
53
frontend/src/components/LeadView.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import type { RoomData } from '../types'
|
||||
|
||||
interface LeadViewProps {
|
||||
rooms: RoomData[]
|
||||
}
|
||||
|
||||
export default function LeadView({ rooms }: LeadViewProps) {
|
||||
const leads = useMemo(() => {
|
||||
const map = new Map<string, { name: string; room: string; msgs: { content: string; time: number; type: string }[] }>()
|
||||
rooms.forEach(room => {
|
||||
room.messages.filter(m => m.is_lead).forEach(msg => {
|
||||
const existing = map.get(msg.user_id)
|
||||
if (existing) {
|
||||
existing.msgs.push({ content: msg.content, time: msg.timestamp, type: msg.type })
|
||||
} else {
|
||||
map.set(msg.user_id, { name: msg.username, room: room.title || room.anchor_id, msgs: [{ content: msg.content, time: msg.timestamp, type: msg.type }] })
|
||||
}
|
||||
})
|
||||
})
|
||||
return Array.from(map.values()).sort((a, b) => b.msgs.length - a.msgs.length)
|
||||
}, [rooms])
|
||||
|
||||
return (
|
||||
<div className="lead-view">
|
||||
<h2>🎯 跨房间精准客户汇总</h2>
|
||||
{leads.length === 0 ? (
|
||||
<div className="empty-state"><p>暂无精准客户</p></div>
|
||||
) : (
|
||||
<div className="lead-list">
|
||||
{leads.map(lead => (
|
||||
<div key={lead.name} className="lead-item">
|
||||
<div className="lead-header">
|
||||
<span className="lead-name">{lead.name}</span>
|
||||
<span className="lead-room">{lead.room}</span>
|
||||
<span className="lead-count">{lead.msgs.length}条精准消息</span>
|
||||
</div>
|
||||
<div className="lead-msgs">
|
||||
{lead.msgs.slice(-10).reverse().map((m, i) => (
|
||||
<div key={i} className="lead-msg">
|
||||
<span className="lead-msg-type">{m.type}</span>
|
||||
<span className="lead-msg-content">{m.content}</span>
|
||||
<span className="lead-msg-time">{new Date(m.time).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
108
frontend/src/components/LicenseView.tsx
Normal file
108
frontend/src/components/LicenseView.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import type { LicenseState } from '../types'
|
||||
|
||||
interface LicenseViewProps {
|
||||
license: LicenseState | null
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
export default function LicenseView({ license, onBack }: LicenseViewProps) {
|
||||
const [step, setStep] = useState<'login' | 'scan' | 'activate'>('login')
|
||||
const [token, setToken] = useState('')
|
||||
const [status, setStatus] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (license?.state === 'active') setStep('activate')
|
||||
else if (license?.qrcode) setStep('scan')
|
||||
}, [license])
|
||||
|
||||
const handleActivate = async () => {
|
||||
if (!token.trim()) return
|
||||
try {
|
||||
const res = await fetch('/api/license/activate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: token.trim() })
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.ok) {
|
||||
setStatus('授权成功!')
|
||||
setTimeout(onBack, 1500)
|
||||
} else {
|
||||
setStatus(data.error || '授权失败,请检查token')
|
||||
}
|
||||
} catch {
|
||||
setStatus('网络错误,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="license-view">
|
||||
<button className="back-btn" onClick={onBack}>← 返回</button>
|
||||
<h2>🔑 订阅授权</h2>
|
||||
|
||||
{license?.state === 'active' ? (
|
||||
<div className="license-active">
|
||||
<div className="license-card">
|
||||
<h3>✅ 授权有效</h3>
|
||||
<p>计划: {license.plan}</p>
|
||||
<p>有效期至: {license.expires_at ? new Date(license.expires_at).toLocaleDateString() : '永久'}</p>
|
||||
<p>最大房间数: {license.max_rooms}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : license?.state === 'expired' ? (
|
||||
<div className="license-expired">
|
||||
<p>⚠️ 授权已过期,请重新激活</p>
|
||||
<ActivateForm token={token} setToken={setToken} status={status} onActivate={handleActivate} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="license-unconfigured">
|
||||
<p>🔒 尚未配置授权,请选择以下方式激活:</p>
|
||||
<div className="license-steps">
|
||||
<div className="step">
|
||||
<h4>步骤 1: 登录/注册</h4>
|
||||
<p>访问客户服务中心获取授权码</p>
|
||||
<button className="open-url-btn" onClick={() => (window as any).BrowserOpenURL?.('https://yser.ging.plus/')}>
|
||||
打开客户服务中心
|
||||
</button>
|
||||
</div>
|
||||
<div className="step">
|
||||
<h4>步骤 2: 输入授权码</h4>
|
||||
<ActivateForm token={token} setToken={setToken} status={status} onActivate={handleActivate} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{license?.qrcode && (
|
||||
<div className="qrcode-wrap">
|
||||
<h4>扫码激活</h4>
|
||||
<QRCodeSVG value={license.qrcode} size={200} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ActivateForm({ token, setToken, status, onActivate }: {
|
||||
token: string
|
||||
setToken: (v: string) => void
|
||||
status: string
|
||||
onActivate: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="activate-form">
|
||||
<input
|
||||
className="token-input"
|
||||
type="text"
|
||||
placeholder="请输入授权码..."
|
||||
value={token}
|
||||
onChange={e => setToken(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && onActivate()}
|
||||
/>
|
||||
<button className="activate-btn" onClick={onActivate}>激活授权</button>
|
||||
{status && <span className="activate-status">{status}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
61
frontend/src/components/RoomCard.tsx
Normal file
61
frontend/src/components/RoomCard.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import KindBadge from './KindBadge'
|
||||
import { ICONS, TYPE_LABELS } from '../constants'
|
||||
import type { RoomData } from '../types'
|
||||
|
||||
interface RoomCardProps {
|
||||
room: RoomData
|
||||
onToggle: (action: 'start' | 'stop' | 'toggle') => void
|
||||
onRemove: () => void
|
||||
onDetail: () => void
|
||||
}
|
||||
|
||||
export default function RoomCard({ room, onToggle, onRemove, onDetail }: RoomCardProps) {
|
||||
const lastMsgs = room.messages.slice(-5)
|
||||
|
||||
return (
|
||||
<div className={`room-card ${room.status}`}>
|
||||
<div className="card-header">
|
||||
<div className="card-title-wrap">
|
||||
<KindBadge kind={room.kind} />
|
||||
<span className="card-title" title={room.title}>{room.title || room.anchor_id}</span>
|
||||
</div>
|
||||
<div className="card-controls">
|
||||
<button className="card-btn detail" onClick={onDetail} title="详情">🔍</button>
|
||||
<button className={`card-btn ${room.status === 'running' ? 'stop' : 'start'}`} onClick={() => onToggle(room.status === 'running' ? 'stop' : 'start')} title={room.status === 'running' ? '停止' : '启动'}>
|
||||
{room.status === 'running' ? '⏹' : '▶'}
|
||||
</button>
|
||||
<button className="card-btn del" onClick={onRemove} title="删除">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-stats">
|
||||
<span className="stat">👁 {room.viewers}</span>
|
||||
<span className="stat">💬 {room.total_msg}</span>
|
||||
<span className="stat lead-stat">🎯 {room.lead_count}</span>
|
||||
<span className={`stat status-stat ${room.status}`}>{room.status === 'running' ? '● 直播中' : room.status === 'starting' ? '◌ 启动中' : room.status === 'error' ? '✕ 错误' : '○ 已停止'}</span>
|
||||
</div>
|
||||
{lastMsgs.length > 0 && (
|
||||
<div className="card-preview">
|
||||
{lastMsgs.map(msg => (
|
||||
<div key={msg.id} className={`preview-msg ${msg.is_lead ? 'lead' : ''} ${msg.marked ? `marked-${msg.marked}` : ''}`}>
|
||||
<span className="msg-type-icon">{ICONS[msg.type as keyof typeof ICONS] || ICONS.chat}</span>
|
||||
<span className="msg-content">{truncate(msg.content, 40)}</span>
|
||||
<span className="msg-time">{formatTime(msg.timestamp)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function truncate(s: string, n: number) {
|
||||
return s.length > n ? s.slice(0, n) + '...' : s
|
||||
}
|
||||
|
||||
function formatTime(ts: number) {
|
||||
if (!ts) return ''
|
||||
const d = new Date(ts)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
35
frontend/src/components/RoomTools.tsx
Normal file
35
frontend/src/components/RoomTools.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
|
||||
interface RoomToolsProps {
|
||||
viewMode?: 'card' | 'list'
|
||||
cardCols?: number
|
||||
listPerPage?: number
|
||||
onToggleView?: (v: 'card' | 'list') => void
|
||||
onSetCols?: (n: number) => void
|
||||
onSetPerPage?: (n: number) => void
|
||||
}
|
||||
|
||||
export default function RoomTools({ viewMode, cardCols, listPerPage, onToggleView, onSetCols, onSetPerPage }: RoomToolsProps) {
|
||||
return (
|
||||
<div className="room-tools">
|
||||
<button className={`tool-btn ${viewMode === 'card' ? 'active' : ''}`} onClick={() => onToggleView?.('card')} title="卡片视图">🗂</button>
|
||||
<button className={`tool-btn ${viewMode === 'list' ? 'active' : ''}`} onClick={() => onToggleView?.('list')} title="列表视图">☰</button>
|
||||
{viewMode === 'card' && (
|
||||
<div className="col-sizer">
|
||||
<span className="col-label">列:</span>
|
||||
{[2, 3, 4, 5].map(n => (
|
||||
<button key={n} className={`col-btn ${cardCols === n ? 'active' : ''}`} onClick={() => onSetCols?.(n)}>{n}</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{viewMode === 'list' && (
|
||||
<div className="col-sizer">
|
||||
<span className="col-label">页:</span>
|
||||
{[10, 20, 50, 100].map(n => (
|
||||
<button key={n} className={`col-btn ${listPerPage === n ? 'active' : ''}`} onClick={() => onSetPerPage?.(n)}>{n}</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
frontend/src/constants.ts
Normal file
39
frontend/src/constants.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export const ICONS = {
|
||||
chat: '💬',
|
||||
gift: '🎁',
|
||||
enter: '👋',
|
||||
like: '❤️',
|
||||
follow: '➕',
|
||||
stats: '📊',
|
||||
fansclub: '⭐',
|
||||
status: 'ℹ️',
|
||||
rank: '🏆',
|
||||
room: '🏠',
|
||||
room_stats: '📊',
|
||||
emoji_chat: '😀',
|
||||
stream_adaptation: '⚙️',
|
||||
lead: '🎯'
|
||||
} as const
|
||||
|
||||
export const MARKS = [
|
||||
{ key: 'blue', label: '蓝', color: '#4a9eff', tint: 'rgba(74,158,255,0.10)' },
|
||||
{ key: 'red', label: '红', color: '#ff4d4f', tint: 'rgba(255,77,79,0.10)' },
|
||||
{ key: 'yellow', label: '黄', color: '#ffc53d', tint: 'rgba(255,197,61,0.10)' }
|
||||
] as const
|
||||
|
||||
export const MARK_MAP = Object.fromEntries(MARKS.map(m => [m.key, m])) as Record<string, typeof MARKS[0]>
|
||||
|
||||
export const TYPE_LABELS: Record<string, string> = {
|
||||
all: '全部',
|
||||
chat: '聊天',
|
||||
gift: '礼物',
|
||||
enter: '进场',
|
||||
like: '点赞',
|
||||
follow: '关注',
|
||||
stats: '统计',
|
||||
fansclub: '粉丝团',
|
||||
rank: '排行榜',
|
||||
status: '状态',
|
||||
room_stats: '统计',
|
||||
lead: '精准客户'
|
||||
}
|
||||
24
frontend/src/errorReporter.ts
Normal file
24
frontend/src/errorReporter.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
|
||||
export function errorReporter() {
|
||||
window.onerror = (msg, src, line, col, err) => {
|
||||
const body = JSON.stringify({
|
||||
type: 'js_error',
|
||||
message: String(msg),
|
||||
source: src,
|
||||
line,
|
||||
column: col,
|
||||
stack: err ? (err.stack || '') : '',
|
||||
ts: Date.now()
|
||||
})
|
||||
fetch('/api/error', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body }).catch(() => {})
|
||||
}
|
||||
window.addEventListener('unhandledrejection', e => {
|
||||
const body = JSON.stringify({
|
||||
type: 'unhandled_rejection',
|
||||
message: (e.reason && (e.reason as any).message) || String(e.reason),
|
||||
ts: Date.now()
|
||||
})
|
||||
fetch('/api/error', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body }).catch(() => {})
|
||||
})
|
||||
}
|
||||
8
frontend/src/main.tsx
Normal file
8
frontend/src/main.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './style.css'
|
||||
import './App.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<App />
|
||||
)
|
||||
28
frontend/src/style.css
Normal file
28
frontend/src/style.css
Normal file
@@ -0,0 +1,28 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
51
frontend/src/types.ts
Normal file
51
frontend/src/types.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import { ICONS, TYPE_LABELS } from './constants'
|
||||
|
||||
export interface RoomMessage {
|
||||
id: string
|
||||
type: string
|
||||
user_id: string
|
||||
username: string
|
||||
fans_level: number
|
||||
content: string
|
||||
gift_name?: string
|
||||
gift_num?: number
|
||||
gift_price?: number
|
||||
timestamp: number
|
||||
marked?: string
|
||||
is_lead?: boolean
|
||||
is_seen?: boolean
|
||||
}
|
||||
|
||||
export interface RoomData {
|
||||
id: string
|
||||
anchor_id: string
|
||||
room_id: string
|
||||
title: string
|
||||
kind: 'public' | 'anon'
|
||||
status: 'stopped' | 'starting' | 'running' | 'error'
|
||||
viewers: number
|
||||
total_msg: number
|
||||
lead_count: number
|
||||
messages: RoomMessage[]
|
||||
added_at: number
|
||||
}
|
||||
|
||||
export interface SysState {
|
||||
version: string
|
||||
rooms_total: number
|
||||
running: number
|
||||
error_count: number
|
||||
uptime: number
|
||||
}
|
||||
|
||||
export interface LicenseState {
|
||||
state: 'unconfigured' | 'active' | 'expired' | 'trial'
|
||||
plan: string
|
||||
expires_at: number
|
||||
max_rooms: number
|
||||
qrcode?: string
|
||||
token?: string
|
||||
}
|
||||
|
||||
export type Page = 'home' | 'leads' | 'doc' | 'debug' | 'license' | 'detail'
|
||||
23
frontend/src/utils.ts
Normal file
23
frontend/src/utils.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
|
||||
export function formatTime(ts: number): string {
|
||||
if (!ts) return ''
|
||||
const d = new Date(ts)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
export function useLs<T>(key: string, init: T): [T, (v: T) => void] {
|
||||
const [val, setVal] = React.useState<T>(() => {
|
||||
try {
|
||||
const s = localStorage.getItem(key)
|
||||
return s ? JSON.parse(s) : init
|
||||
} catch {
|
||||
return init
|
||||
}
|
||||
})
|
||||
React.useEffect(() => {
|
||||
try { localStorage.setItem(key, JSON.stringify(val)) } catch {}
|
||||
}, [key, val])
|
||||
return [val, setVal]
|
||||
}
|
||||
21
frontend/tsconfig.json
Normal file
21
frontend/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
24
frontend/vite.config.ts
Normal file
24
frontend/vite.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: '/',
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/stream': {
|
||||
target: 'http://localhost:8080',
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true
|
||||
}
|
||||
})
|
||||
5
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
5
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {context} from '../models';
|
||||
|
||||
export function Startup(arg1:context.Context):Promise<void>;
|
||||
7
frontend/wailsjs/go/main/App.js
Normal file
7
frontend/wailsjs/go/main/App.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Startup(arg1) {
|
||||
return window['go']['main']['App']['Startup'](arg1);
|
||||
}
|
||||
24
frontend/wailsjs/runtime/package.json
Normal file
24
frontend/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
330
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
330
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
||||
// Notification types
|
||||
export interface NotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string; // macOS and Linux only
|
||||
body?: string;
|
||||
categoryId?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
id?: string;
|
||||
title?: string;
|
||||
destructive?: boolean; // macOS-specific
|
||||
}
|
||||
|
||||
export interface NotificationCategory {
|
||||
id?: string;
|
||||
actions?: NotificationAction[];
|
||||
hasReplyField?: boolean;
|
||||
replyPlaceholder?: string;
|
||||
replyButtonTitle?: string;
|
||||
}
|
||||
|
||||
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||
// Initializes the notification service for the application.
|
||||
// This must be called before sending any notifications.
|
||||
export function InitializeNotifications(): Promise<void>;
|
||||
|
||||
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||
// Cleans up notification resources and releases any held connections.
|
||||
export function CleanupNotifications(): Promise<void>;
|
||||
|
||||
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||
// Checks if notifications are available on the current platform.
|
||||
export function IsNotificationAvailable(): Promise<boolean>;
|
||||
|
||||
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||
// Requests notification authorization from the user (macOS only).
|
||||
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||
// Checks the current notification authorization status (macOS only).
|
||||
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||
// Sends a basic notification with the given options.
|
||||
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||
// Sends a notification with action buttons. Requires a registered category.
|
||||
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||
|
||||
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||
// Removes a previously registered notification category.
|
||||
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||
|
||||
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||
// Removes all pending notifications from the notification center.
|
||||
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||
|
||||
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||
// Removes a specific pending notification by its identifier.
|
||||
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||
// Removes all delivered notifications from the notification center.
|
||||
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||
|
||||
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||
// Removes a specific delivered notification by its identifier.
|
||||
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||
// Removes a notification by its identifier (cross-platform convenience function).
|
||||
export function RemoveNotification(identifier: string): Promise<void>;
|
||||
298
frontend/wailsjs/runtime/runtime.js
Normal file
298
frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
|
||||
export function InitializeNotifications() {
|
||||
return window.runtime.InitializeNotifications();
|
||||
}
|
||||
|
||||
export function CleanupNotifications() {
|
||||
return window.runtime.CleanupNotifications();
|
||||
}
|
||||
|
||||
export function IsNotificationAvailable() {
|
||||
return window.runtime.IsNotificationAvailable();
|
||||
}
|
||||
|
||||
export function RequestNotificationAuthorization() {
|
||||
return window.runtime.RequestNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function CheckNotificationAuthorization() {
|
||||
return window.runtime.CheckNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function SendNotification(options) {
|
||||
return window.runtime.SendNotification(options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options) {
|
||||
return window.runtime.SendNotificationWithActions(options);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return window.runtime.RegisterNotificationCategory(category);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryId) {
|
||||
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return window.runtime.RemoveAllPendingNotifications();
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return window.runtime.RemovePendingNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return window.runtime.RemoveAllDeliveredNotifications();
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier) {
|
||||
return window.runtime.RemoveNotification(identifier);
|
||||
}
|
||||
Reference in New Issue
Block a user