[+] Frontend
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user