[+] Frontend

This commit is contained in:
2025-03-22 23:50:30 -04:00
parent 6a83fe8fae
commit 446d3e8486
14 changed files with 540 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
<script lang="ts">
import { post } from "./sdk";
let inputCard = ""
let inputName = ""
let [error, loading, done] = ["", false, false]
function scan(uid: string) {
loading = true
post('/scan', { uid })
.then(() => done = true)
.catch(err => error = err)
.finally(() => loading = false)
}
interface Card { id: string, name: string }
let cards: Card[] = JSON.parse(localStorage.getItem("cards") || "[]")
function addCard() {
if (!inputCard || !inputName) return
// Card must match either \d{20} or [0-9A-Fa-f]{16}
if (!/^\d{20}$/.test(inputCard) && !/^[0-9A-Fa-f]{16}$/.test(inputCard))
return error = "Invalid card ID"
cards.push({ id: inputCard, name: inputName })
cards = cards
localStorage.setItem("cards", JSON.stringify(cards))
inputCard = ""
inputName = ""
}
function deleteCard(card: Card) {
cards = cards.filter(c => c !== card)
localStorage.setItem("cards", JSON.stringify(cards))
}
</script>
<main>
<h1>AimeWeb</h1>
<div class="error">{error}</div>
{#each cards as card}
<div class="card">
<h2>{card.name}</h2>
<p>{card.id}</p>
<button on:click={() => scan(card.id)}>刷</button>
<button on:click={() => deleteCard(card)}>删</button>
</div>
{/each}
<div class="controls">
<div class="input">
<label for="add-card">卡号</label>
<input id="add-card" placeholder="卡号 (e.g. 50001234123412341234)" bind:value={inputCard}>
</div>
<div class="input">
<label for="add-name">名称</label>
<input id="add-name" placeholder="名称 (e.g. Aime)" bind:value={inputName}>
</div>
<button on:click={addCard}>添加</button>
</div>
</main>
+79
View File
@@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
+9
View File
@@ -0,0 +1,9 @@
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
const app = mount(App, {
target: document.getElementById('app')!,
})
export default app
+95
View File
@@ -0,0 +1,95 @@
const HOST = 'http://127.0.0.1:8249'
export type Dict = Record<string, any>
interface ExtReqInit extends RequestInit {
params?: { [index: string]: string }
json?: any
}
/**
* Modify a fetch url
*
* @param input Fetch url input
* @param callback Callback for modification
*/
export function reconstructUrl(input: URL | RequestInfo, callback: (url: URL) => URL | void): RequestInfo | URL {
let u = new URL((input instanceof Request) ? input.url : input)
const result = callback(u)
if (result) u = result
if (input instanceof Request) {
// @ts-ignore
return { url: u, ...input }
}
return u
}
/**
* Fetch with url parameters
*/
export function fetchWithParams(input: URL | RequestInfo, init?: ExtReqInit): Promise<Response> {
return fetch(reconstructUrl(input, u => {
u.search = new URLSearchParams(init?.params ?? {}).toString()
}), init)
}
/**
* Do something with the response when it's not ok
*
* @param res Response object
*/
async function ensureOk(res: Response) {
if (!res.ok) {
const text = await res.text()
console.error(`${res.status}: ${text}`)
// If 400 invalid token is caught, should invalidate the token and redirect to signin
if (text === 'Invalid token') {
localStorage.removeItem('token')
window.location.href = '/'
}
// Try to parse as json
let json
try {
json = JSON.parse(text)
} catch (e) {
throw new Error(text)
}
if (json.error) throw new Error(json.error)
}
}
/**
* Post to an endpoint and return the response in JSON while doing error checks
* and handling token (and token expiry) automatically.
*
* @param endpoint The endpoint to post to (e.g., '/pull')
* @param params An object containing the request body or any necessary parameters
* @param init Additional fetch/init configuration
* @returns The JSON response from the server
*/
export async function post(endpoint: string, params: Dict = {}, init?: ExtReqInit): Promise<any> {
return postHelper(endpoint, params, init).then(it => it.json())
}
/**
* Actual impl of post(). This does not return JSON but returns response object.
*/
async function postHelper(endpoint: string, params: Dict = {}, init?: ExtReqInit): Promise<any> {
// Add token if exists
const token = localStorage.getItem('token')
if (token && !('token' in params)) params = { ...(params ?? {}), token }
if (init?.json) {
init.body = JSON.stringify(init.json)
init.headers = { 'Content-Type': 'application/json', ...init.headers }
init.json = undefined
}
const res = await fetchWithParams(HOST + endpoint, { method: 'POST', params, ...init })
.catch(e => { console.error(e); throw new Error("Network error") })
await ensureOk(res)
return res
}
+2
View File
@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />