This commit is contained in:
lilydjwg 2021-10-02 16:42:41 +08:00
parent feefc22121
commit 466ca11cbb
5 changed files with 270 additions and 36 deletions

View File

@ -1,16 +1,16 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset='utf-8'> <meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'> <meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Svelte app</title> <title>落絮</title>
<link rel='icon' type='image/png' href='/favicon.png'> <link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'> <link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/build/bundle.css'> <link rel='stylesheet' href='/build/bundle.css'>
<script defer src='/build/bundle.js'></script> <script defer src='/build/bundle.js'></script>
</head> </head>
<body> <body>

View File

@ -18,8 +18,8 @@ function serve() {
writeBundle() { writeBundle() {
if (server) return; if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'], // stdio: ['ignore', 'inherit', 'inherit'],
shell: true // shell: true
}); });
process.on('SIGTERM', toExit); process.on('SIGTERM', toExit);
@ -71,6 +71,7 @@ export default {
production && terser() production && terser()
], ],
watch: { watch: {
buildDelay: 500,
clearScreen: false clearScreen: false
} }
}; };

View File

@ -1,30 +1,173 @@
<script> <script>
export let name; import { onMount, setContext } from 'svelte'
import Message from './Message.svelte'
const LUOXU_URL = 'https://lab.lilydjwg.me/luoxu'
let groups = []
let group
let query
let error
let result
let now = new Date()
let loading = false
setContext('LUOXU_URL', LUOXU_URL)
function parse_hash() {
const hash = location.hash
if(hash) {
const info = new Map()
for(const pair of hash.substring(1).split('&')){
const [key, value] = pair.split('=')
info.set(key, decodeURIComponent(value))
}
return info
}
}
onMount(async () => {
const res = await fetch(`${LUOXU_URL}/groups`)
groups = (await res.json()).groups
do_hash_search()
})
function update_title() {
let group_name
for(const g of groups) {
if(g.group_id == group) {
group_name = g.name
}
}
if(query && group_name) {
document.title = `搜索:${query} 于 ${group_name} - 落絮`
}else{
document.title = '落絮'
}
}
function do_hash_search() {
const info = parse_hash()
if(info) {
if(info.has('g')) {
group = info.get('g')|0
}
if(info.has('q')) {
query = info.get('q')
}
if(group && query) {
do_search()
}
}
}
async function do_search(more) {
if(!group) {
error = '请选择要搜索的群组'
return
}
if(!query) {
error = '请输入搜索关键字'
return
}
error = ''
console.log(`searching ${query} for group ${group}, older than ${more}`)
const q = `g=${group}&q=${encodeURIComponent(query)}`
let url
if(!more) {
location.hash = `#${q}`
update_title()
if(result) {
result.messages = []
}
url = `${LUOXU_URL}/search?${q}`
}else{
url = `${LUOXU_URL}/search?${q}&end=${more}`
}
now = new Date()
loading = true
try{
const res = await fetch(url)
const r = await res.json()
loading = false
if(more) {
return r
}else{
result = r
}
}catch(e){
error = e
loading = false
}
}
async function do_search_more() {
const more = result.messages[result.messages.length-1].t
const old_msgs = result.messages
const new_result = await do_search(more)
result.messages = [...old_msgs, ...new_result.messages]
result.has_more = new_result.has_more
}
</script> </script>
<svelte:window on:hashchange={do_hash_search}/>
<main> <main>
<h1>Hello {name}!</h1> <div id="searchbox">
<p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p> <select bind:value={group} on:change={() => error = ''}>
{#each groups as group}
<option value={group.group_id}>{group.name}</option>
{:else}
<option selected value=''>正在加载群组信息...</option>
{/each}
</select>
<input type="search" bind:value={query}
on:input={() => error = ''}
on:keyup={e => {if(e.key == 'Enter'){do_search()}}}
/>
<button on:click={do_search}>搜索</button>
</div>
{#if error}
<div class="error">{error}</div>
{/if}
{#if result}
{#each result.messages as message}
<Message msg={message} group={result.group_pub_id} now={now} />
{/each}
{/if}
{#if loading}
<div id="loading"><p>正在加载...</p></div>
{:else if result && result.has_more}
<div id="more"><button on:click={do_search_more}>加载更多</button></div>
{/if}
</main> </main>
<style> <style>
main { main {
text-align: center; margin: 1em;
padding: 1em; }
max-width: 240px;
margin: 0 auto;
}
h1 { #searchbox {
color: #ff3e00; display: flex;
text-transform: uppercase; }
font-size: 4em; #searchbox input[type=search] {
font-weight: 100; flex-grow: 1;
} }
@media (min-width: 640px) { .error {
main { color: red;
max-width: none; text-align: center;
} }
}
</style> #loading, #more {
display: flex;
justify-content: center;
}
#loading > * {
border: 1px #bfbfbf solid;
border-radius: 2em;
padding: 0.5em 1em;
}
</style>

93
src/Message.svelte Normal file
View File

@ -0,0 +1,93 @@
<script>
import { onMount, getContext } from 'svelte'
export let msg
export let group
export let now
const formatter = new Intl.DateTimeFormat(undefined, {
timeStyle: "full",
dateStyle: "full",
hour12: false,
})
let dt = new Date(msg.t * 1000)
let edited = msg.edited ? new Date(msg.edited * 1000) : null
let title = format_dt(dt) + (edited ? `\n最后编辑于${format_dt(edited)}` : '')
let relative_dt = format_relative_time(dt, now)
let iso_date = dt.toISOString()
function format_relative_time(d1, d2) {
// in miliseconds
const units = {
year : 24 * 60 * 60 * 1000 * 365,
month : 24 * 60 * 60 * 1000 * 365 / 12,
day : 24 * 60 * 60 * 1000,
hour : 60 * 60 * 1000,
minute: 60 * 1000,
second: 1000,
}
const rtf = new Intl.RelativeTimeFormat()
const elapsed = d1 - d2
for(const u in units) {
if(Math.abs(elapsed) > units[u] || u == 'second') {
return rtf.format(Math.round(elapsed/units[u]), u)
}
}
}
function format_dt(t) {
return formatter.format(t)
}
</script>
<div>
<div class="message">
<img src="{getContext('LUOXU_URL')}/avatar/{msg.from_id}.jpg" height="64" width="64" alt="{msg.from_name} 的头像"/>
<div>
<div class="name">{msg.from_name}</div>
<div class="body"><pre>{msg.text}</pre></div>
<div class="time"><a href="tg://resolve?domain={group}&post={msg.id}"><time datetime={iso_date} title={title}>{relative_dt}</time></a></div>
</div>
</div>
</div>
<style>
.message {
margin: 1em;
padding: 0.5em;
max-width: max-content;
border: 1px #eeeeee solid;
box-shadow: 0 0 3px gray;
border-radius: 5px;
display: flex;
}
.message::after {
content: '';
display: block;
clear: both;
}
img {
padding-right: 0.5em;
}
.name {
white-space: nowrap;
color: #1e90ff;
}
pre {
white-space: break-spaces;
margin: 0.2em 0;
}
.time {
font-size: 0.75em;
float: right;
}
.time > a {
color: gray;
}
</style>

View File

@ -1,10 +1,7 @@
import App from './App.svelte'; import App from './App.svelte';
const app = new App({ const app = new App({
target: document.body, target: document.body
props: {
name: 'world'
}
}); });
export default app; export default app;