add library, tsconfig.base.json
This commit is contained in:
1 parent
0348f84255
commit
ffbe5fed18
8 files changed
+758
-25
No files matched your search
+55
-9
@@ -26,6 +26,16 @@ export interface IEntry {
|
|||||||
tag: string[]
|
tag: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ILibrary {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
list: string[]
|
||||||
|
type: IType
|
||||||
|
description: string
|
||||||
|
tag: string[]
|
||||||
|
isShared?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type ILookup<T> = T & {
|
export type ILookup<T> = T & {
|
||||||
offset?: number
|
offset?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
@@ -48,9 +58,9 @@ class Api {
|
|||||||
async setWanikaniApiKey(apiKey?: string) {
|
async setWanikaniApiKey(apiKey?: string) {
|
||||||
const setApiKey = () => {
|
const setApiKey = () => {
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
this.axios.defaults.headers.common.Authorization = `Bearer ${apiKey}`
|
this.axios.defaults.headers.common['Authorization'] = `Bearer ${apiKey}`
|
||||||
} else {
|
} else {
|
||||||
delete this.axios.defaults.headers.common.Authorization
|
delete this.axios.defaults.headers.common['Authorization']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,20 +109,17 @@ class Api {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
async user_updateSettings(u: {
|
async user_updateSettings(u: Omit<IUserSettings, 'identifier' | 'level'>) {
|
||||||
levelMin?: number
|
|
||||||
level_browser?: ILevelBrowser
|
|
||||||
}) {
|
|
||||||
return await this.axios.patch('/api/user', u)
|
return await this.axios.patch('/api/user', u)
|
||||||
}
|
}
|
||||||
|
|
||||||
async quiz_getMany(params: {
|
async quiz_getMany(payload: {
|
||||||
entries: string[]
|
entries: string[]
|
||||||
select: string[]
|
select: string[]
|
||||||
type?: string
|
type?: string
|
||||||
direction?: string
|
direction?: string
|
||||||
}) {
|
}) {
|
||||||
return await this.axios.post('/api/quiz/getMany', params).then(
|
return await this.axios.post('/api/quiz/getMany', payload).then(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.data as {
|
r.data as {
|
||||||
result: {
|
result: {
|
||||||
@@ -123,6 +130,18 @@ class Api {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async quiz_getSrsLevel(payload: { entries: string[]; type: IType }) {
|
||||||
|
return await this.axios.post('/api/quiz/getSrsLevel', payload).then(
|
||||||
|
(r) =>
|
||||||
|
r.data as {
|
||||||
|
result: {
|
||||||
|
entry: string
|
||||||
|
srsLevel: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async quiz_create(payload: { entries: string[]; type: IType }) {
|
async quiz_create(payload: { entries: string[]; type: IType }) {
|
||||||
return await this.axios.put('/api/quiz', payload).then(
|
return await this.axios.put('/api/quiz', payload).then(
|
||||||
(r) =>
|
(r) =>
|
||||||
@@ -224,7 +243,9 @@ class Api {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async all_getLookup(params: ILookup<{ entry: string; type: IType }>) {
|
async all_getLookup(
|
||||||
|
params: ILookup<{ entry: string; type: IType; forced?: boolean }>
|
||||||
|
) {
|
||||||
params.offset = params.offset || 0
|
params.offset = params.offset || 0
|
||||||
params.limit = params.limit || 5
|
params.limit = params.limit || 5
|
||||||
|
|
||||||
@@ -271,6 +292,12 @@ class Api {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async library_getQuery(params: ILookup<{ q: string }>) {
|
||||||
|
return await this.axios
|
||||||
|
.get('/api/library/q', { params })
|
||||||
|
.then((r) => r.data as ILookupResult<ILibrary>)
|
||||||
|
}
|
||||||
|
|
||||||
async library_getLevelList(payload: {
|
async library_getLevelList(payload: {
|
||||||
type: ITypeLevel
|
type: ITypeLevel
|
||||||
settings: ILevelBrowserByType
|
settings: ILevelBrowserByType
|
||||||
@@ -285,6 +312,25 @@ class Api {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async library_create(payload: Omit<ILibrary, 'id'>) {
|
||||||
|
return await this.axios.put('/api/library', payload).then(
|
||||||
|
(r) =>
|
||||||
|
r.data as {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async library_update(payload: ILibrary) {
|
||||||
|
return await this.axios.patch('/api/library', payload, {
|
||||||
|
params: { id: payload.id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async library_delete(params: { id: string }) {
|
||||||
|
return await this.axios.delete('/api/library', { params })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = new Api()
|
export const api = new Api()
|
||||||
+3
-3
@@ -45,7 +45,7 @@ export const doMapKeypress = (
|
|||||||
export function shuffle<T>(a: T[]) {
|
export function shuffle<T>(a: T[]) {
|
||||||
for (let i = a.length - 1; i > 0; i--) {
|
for (let i = a.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1))
|
const j = Math.floor(Math.random() * (i + 1))
|
||||||
;[a[i], a[j]] = [a[j], a[i]]
|
;[a[i], a[j]] = [a[j]!, a[i]!]
|
||||||
}
|
}
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ export function sample<T>(arr: T[], size: number): T[] {
|
|||||||
outN.push(...allN.splice(Math.floor(Math.random() * allN.length), 1))
|
outN.push(...allN.splice(Math.floor(Math.random() * allN.length), 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
return outN.map((n) => arr[n])
|
return outN.map((n) => arr[n]!)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function capitalize(s: string): string {
|
export function capitalize(s: string): string {
|
||||||
@@ -72,7 +72,7 @@ export function capitalize(s: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!s) return s
|
if (!s) return s
|
||||||
return s[0].toLocaleUpperCase() + s.substr(1)
|
return s[0]!.toLocaleUpperCase() + s.substr(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function jsonClone<T>(o: T): T {
|
export function jsonClone<T>(o: T): T {
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header" style="display: flex; flex-direction: row">
|
||||||
|
<div class="card-header-title" style="flex-grow: 1">
|
||||||
|
<p
|
||||||
|
class="has-context"
|
||||||
|
@click="
|
||||||
|
(evt) => {
|
||||||
|
selected = currentData
|
||||||
|
$refs.context.open(evt)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@contextmenu.prevent="
|
||||||
|
(evt) => {
|
||||||
|
selected = currentData
|
||||||
|
$refs.context.open(evt)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
@click="isOpen = !isOpen"
|
||||||
|
style="flex-grow: 1; height: 100%; cursor: pointer"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
v-if="isOpen"
|
||||||
|
class="card-header-icon"
|
||||||
|
role="button"
|
||||||
|
@click="isList = !isList"
|
||||||
|
>
|
||||||
|
<b-icon :icon="isList ? 'th' : 'list-ul'"> </b-icon>
|
||||||
|
</a>
|
||||||
|
<a class="card-header-icon" role="button" @click="isOpen = !isOpen">
|
||||||
|
<b-icon :icon="isOpen ? 'caret-down' : 'caret-up'"> </b-icon>
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
class="card-content"
|
||||||
|
:data-mode="isList ? 'list' : 'item'"
|
||||||
|
>
|
||||||
|
<div v-if="isList">
|
||||||
|
<b-table
|
||||||
|
:data="list"
|
||||||
|
paginated
|
||||||
|
:per-page="perPage"
|
||||||
|
@page-change="(p) => makeList(p)"
|
||||||
|
>
|
||||||
|
<b-table-column field="entry" label="Entry" v-slot="props">
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
'tag clickable is-medium ' + getTagClass(props.row.entry)
|
||||||
|
"
|
||||||
|
@click.stop="
|
||||||
|
(evt) => {
|
||||||
|
selected = [props.row.entry]
|
||||||
|
$refs.context.open(evt)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@contextmenu.prevent="
|
||||||
|
(evt) => {
|
||||||
|
selected = [props.row.entry]
|
||||||
|
$refs.context.open(evt)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ props.row.entry }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-for="t in props.row.alt || []"
|
||||||
|
:key="t"
|
||||||
|
:class="'tag clickable is-medium ' + getTagClass(t)"
|
||||||
|
@click.stop="
|
||||||
|
(evt) => {
|
||||||
|
selected = [t]
|
||||||
|
$refs.context.open(evt)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@contextmenu.prevent="
|
||||||
|
(evt) => {
|
||||||
|
selected = [t]
|
||||||
|
$refs.context.open(evt)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ t }}
|
||||||
|
</span>
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column field="reading" label="Pinyin" v-slot="props">
|
||||||
|
{{ (props.row.reading || []).join(' / ') }}
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column field="english" label="English" v-slot="props">
|
||||||
|
<div
|
||||||
|
class="no-scrollbar"
|
||||||
|
style="max-width: 40vw; max-height: 200px"
|
||||||
|
>
|
||||||
|
{{ (props.row.english || []).join(' / ') }}
|
||||||
|
</div>
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<template slot="empty">
|
||||||
|
<div style="position: relative; height: 120px">
|
||||||
|
<b-loading active :is-full-page="false"></b-loading>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<span
|
||||||
|
v-for="t in currentData"
|
||||||
|
:key="t"
|
||||||
|
:class="'tag clickable is-medium ' + getTagClass(t)"
|
||||||
|
@click.stop="
|
||||||
|
(evt) => {
|
||||||
|
selected = [t]
|
||||||
|
$refs.context.open(evt)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@contextmenu.prevent="
|
||||||
|
(evt) => {
|
||||||
|
selected = [t]
|
||||||
|
$refs.context.open(evt)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ t }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ContextMenu
|
||||||
|
ref="context"
|
||||||
|
:type="type"
|
||||||
|
:entry="selected"
|
||||||
|
:description="title + ' ' + description"
|
||||||
|
:additional="additional"
|
||||||
|
@quiz:added="(evt) => reload(evt.entries)"
|
||||||
|
@quiz:removed="(evt) => reload(evt.entries)"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component, Ref, Prop } from 'nuxt-property-decorator'
|
||||||
|
import { api, IEntry, IType } from '~/assets/api'
|
||||||
|
import ContextMenu from '../ContextMenu.vue'
|
||||||
|
|
||||||
|
type ILibraryEntry = Pick<
|
||||||
|
IEntry,
|
||||||
|
'entry' | 'reading' | 'translation' | 'type'
|
||||||
|
> & {
|
||||||
|
originalEntry: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component<LibraryCard>({
|
||||||
|
components: {
|
||||||
|
ContextMenu,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
open() {
|
||||||
|
this.isOpen = this.open
|
||||||
|
},
|
||||||
|
isList() {
|
||||||
|
this.makeList()
|
||||||
|
},
|
||||||
|
isOpen() {
|
||||||
|
this.reload(this.entries)
|
||||||
|
},
|
||||||
|
entries() {
|
||||||
|
this.isList = false
|
||||||
|
this.list = []
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.reload(this.entries)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.isOpen = this.open
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class LibraryCard extends Vue {
|
||||||
|
@Prop() title!: string
|
||||||
|
@Prop() entries!: string[]
|
||||||
|
@Prop() type!: IType
|
||||||
|
@Prop({ default: '' }) description!: string
|
||||||
|
@Prop({ default: false }) open!: boolean
|
||||||
|
|
||||||
|
@Prop({ default: () => [] }) additional!: {
|
||||||
|
name: string
|
||||||
|
handler: () => void
|
||||||
|
}[]
|
||||||
|
|
||||||
|
@Ref() context!: ContextMenu
|
||||||
|
|
||||||
|
isOpen = false
|
||||||
|
isList = false
|
||||||
|
|
||||||
|
list: ILibraryEntry[] = this.entries.map((el) => ({
|
||||||
|
originalEntry: el,
|
||||||
|
entry: [el],
|
||||||
|
reading: [],
|
||||||
|
translation: [],
|
||||||
|
type: this.type,
|
||||||
|
}))
|
||||||
|
|
||||||
|
perPage = 5
|
||||||
|
|
||||||
|
selected: string[] = []
|
||||||
|
srsLevel: {
|
||||||
|
[entry: string]: number
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
readonly tagClassMap = [
|
||||||
|
(lv: number) => (lv > 2 ? 'is-success' : ''),
|
||||||
|
(lv: number) => (lv > 0 ? 'is-warning' : ''),
|
||||||
|
(lv: number) => (lv === 0 ? 'is-danger' : ''),
|
||||||
|
]
|
||||||
|
|
||||||
|
async reload(entries: string[]) {
|
||||||
|
if (!this.isOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.length > 0) {
|
||||||
|
entries = [...new Set(entries)]
|
||||||
|
|
||||||
|
const { result } = await api.quiz_getSrsLevel({
|
||||||
|
entries,
|
||||||
|
type: this.type,
|
||||||
|
})
|
||||||
|
|
||||||
|
entries.map((entry) => {
|
||||||
|
delete this.srsLevel[entry]
|
||||||
|
})
|
||||||
|
|
||||||
|
result.map(({ entry, srsLevel }) => {
|
||||||
|
this.srsLevel[entry] = srsLevel
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$set(this, 'srsLevel', this.srsLevel)
|
||||||
|
this.$forceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async makeList(p = 1) {
|
||||||
|
const offset = (p - 1) * this.perPage
|
||||||
|
|
||||||
|
const listMap = new Map<number, ILibraryEntry>()
|
||||||
|
Array(this.perPage)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, i) => {
|
||||||
|
const el = this.list[offset + i]
|
||||||
|
if (el && !el.reading.length) {
|
||||||
|
listMap.set(offset + i, el)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[...listMap].map(([k, v]) =>
|
||||||
|
api
|
||||||
|
.all_getLookup({
|
||||||
|
entry: v.originalEntry,
|
||||||
|
type: this.type,
|
||||||
|
forced: true,
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
.then((r) =>
|
||||||
|
listMap.set(k, {
|
||||||
|
...r.result[0]!,
|
||||||
|
...v,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [k, v] of listMap) {
|
||||||
|
this.list = [...this.list.slice(0, k), v, ...this.list.slice(k + 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.reload([...listMap].flatMap(([, { entry }]) => entry))
|
||||||
|
}
|
||||||
|
|
||||||
|
getTagClass(it: IEntry) {
|
||||||
|
const srsLevel = Math.min(
|
||||||
|
...it.entry.map((el) => this.srsLevel[el] || -Infinity)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (srsLevel === -Infinity) return 'is-light'
|
||||||
|
|
||||||
|
if (srsLevel === -1) {
|
||||||
|
return 'is-info'
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fn of this.tagClassMap) {
|
||||||
|
const c = fn(srsLevel)
|
||||||
|
if (c) {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'is-light'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tag {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content[data-mode='item'] {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-context:hover {
|
||||||
|
color: blue;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -257,7 +257,7 @@ export default class BrowseTab extends Vue {
|
|||||||
this.selected.reading = [
|
this.selected.reading = [
|
||||||
await api
|
await api
|
||||||
.all_getReading({
|
.all_getReading({
|
||||||
entry: this.selected.entry[0],
|
entry: this.selected.entry[0]!,
|
||||||
})
|
})
|
||||||
.then((r) => r.result),
|
.then((r) => r.result),
|
||||||
]
|
]
|
||||||
@@ -266,7 +266,7 @@ export default class BrowseTab extends Vue {
|
|||||||
if (!this.selected.translation.length) {
|
if (!this.selected.translation.length) {
|
||||||
this.selected.translation = await api
|
this.selected.translation = await api
|
||||||
.all_getMeaning({
|
.all_getMeaning({
|
||||||
entry: this.selected.entry[0],
|
entry: this.selected.entry[0]!,
|
||||||
})
|
})
|
||||||
.then((r) =>
|
.then((r) =>
|
||||||
r.result.filter((r0) => !r0.entry).map((r0) => r0.meaning)
|
r.result.filter((r0) => !r0.entry).map((r0) => r0.meaning)
|
||||||
@@ -360,7 +360,7 @@ export default class BrowseTab extends Vue {
|
|||||||
const o = jsonClone(row)
|
const o = jsonClone(row)
|
||||||
this.selected = {
|
this.selected = {
|
||||||
...o,
|
...o,
|
||||||
entry: o.entry[0],
|
entry: o.entry[0]!,
|
||||||
list: o.entry,
|
list: o.entry,
|
||||||
}
|
}
|
||||||
this.context.open(evt)
|
this.context.open(evt)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section>
|
<section>
|
||||||
<div class="HanziPage">
|
<div class="CharacterTab">
|
||||||
<form class="field" @submit.prevent="q = q0">
|
<form class="field" @submit.prevent="q = q0">
|
||||||
<label for="q" class="label">
|
<label for="q" class="label">
|
||||||
Search
|
Search
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6 entry-display">
|
<div class="column is-6 entry-display">
|
||||||
<div
|
<div
|
||||||
class="hanzi-display clickable font-han"
|
class="character-display clickable font-han"
|
||||||
@click="(evt) => openContext(evt, current, 'character')"
|
@click="(evt) => openContext(evt, current, 'character')"
|
||||||
@contextmenu.prevent="
|
@contextmenu.prevent="
|
||||||
(evt) => openContext(evt, current, 'character')
|
(evt) => openContext(evt, current, 'character')
|
||||||
@@ -278,12 +278,14 @@ import { Component, Prop, Ref, Vue, Watch } from 'nuxt-property-decorator'
|
|||||||
import ContextMenu from '@/components/ContextMenu.vue'
|
import ContextMenu from '@/components/ContextMenu.vue'
|
||||||
import { api, IEntry } from '~/assets/api'
|
import { api, IEntry } from '~/assets/api'
|
||||||
|
|
||||||
|
const TITLE = 'Kanji'
|
||||||
|
|
||||||
@Component<CharacterTab>({
|
@Component<CharacterTab>({
|
||||||
components: {
|
components: {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
this.$emit('title', 'Hanzi')
|
this.$emit('title', TITLE)
|
||||||
|
|
||||||
this.q = this.query.q || ''
|
this.q = this.query.q || ''
|
||||||
|
|
||||||
@@ -357,7 +359,7 @@ export default class CharacterTab extends Vue {
|
|||||||
|
|
||||||
@Watch('q')
|
@Watch('q')
|
||||||
async onQChange(q: string) {
|
async onQChange(q: string) {
|
||||||
this.$emit('title', (q ? q + ' - ' : '') + 'Hanzi')
|
this.$emit('title', (q ? q + ' - ' : '') + TITLE)
|
||||||
|
|
||||||
if (/\p{sc=Han}/u.test(q)) {
|
if (/\p{sc=Han}/u.test(q)) {
|
||||||
const qs = q.split('').filter((h) => /\p{sc=Han}/u.test(h))
|
const qs = q.split('').filter((h) => /\p{sc=Han}/u.test(h))
|
||||||
@@ -381,7 +383,7 @@ export default class CharacterTab extends Vue {
|
|||||||
@Watch('current')
|
@Watch('current')
|
||||||
load() {
|
load() {
|
||||||
if (this.current) {
|
if (this.current) {
|
||||||
this.loadHanzi()
|
this.loadCharacter()
|
||||||
this.loadVocab()
|
this.loadVocab()
|
||||||
this.loadSentences()
|
this.loadSentences()
|
||||||
} else {
|
} else {
|
||||||
@@ -399,7 +401,7 @@ export default class CharacterTab extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadHanzi() {
|
async loadCharacter() {
|
||||||
if (!/^\p{sc=Han}$/u.test(this.current)) {
|
if (!/^\p{sc=Han}$/u.test(this.current)) {
|
||||||
this.sub = []
|
this.sub = []
|
||||||
this.sup = []
|
this.sup = []
|
||||||
@@ -429,6 +431,7 @@ export default class CharacterTab extends Vue {
|
|||||||
} = await api.all_getLookup({
|
} = await api.all_getLookup({
|
||||||
entry: this.current,
|
entry: this.current,
|
||||||
type: 'character',
|
type: 'character',
|
||||||
|
limit: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.reading = r?.reading || []
|
this.reading = r?.reading || []
|
||||||
@@ -455,10 +458,9 @@ export default class CharacterTab extends Vue {
|
|||||||
.map((r) =>
|
.map((r) =>
|
||||||
api
|
api
|
||||||
.all_getLookup({ entry: r.entry, type: 'vocabulary', limit: 1 })
|
.all_getLookup({ entry: r.entry, type: 'vocabulary', limit: 1 })
|
||||||
.then((r) => r.result[0])
|
.then((r) => r.result[0]!)
|
||||||
)
|
)
|
||||||
.filter((r) => r)
|
.filter((r) => r)
|
||||||
.map((r) => r!)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,10 +480,9 @@ export default class CharacterTab extends Vue {
|
|||||||
.map((r) =>
|
.map((r) =>
|
||||||
api
|
api
|
||||||
.all_getLookup({ entry: r.entry, type: 'sentence', limit: 1 })
|
.all_getLookup({ entry: r.entry, type: 'sentence', limit: 1 })
|
||||||
.then((r) => r.result[0])
|
.then((r) => r.result[0]!)
|
||||||
)
|
)
|
||||||
.filter((r) => r)
|
.filter((r) => r)
|
||||||
.map((r) => r!)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<div class="LibraryPage">
|
||||||
|
<form @submit.prevent="q = q0">
|
||||||
|
<label for="q" class="label">
|
||||||
|
Search
|
||||||
|
<b-tooltip label="How to?" position="is-right">
|
||||||
|
<a
|
||||||
|
href="https://github.com/zhquiz/zhquiz/wiki/How-to-search-or-filter"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<b-icon icon="info-circle"></b-icon>
|
||||||
|
</a>
|
||||||
|
</b-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="field has-addons">
|
||||||
|
<p class="control is-expanded">
|
||||||
|
<input
|
||||||
|
v-model="q0"
|
||||||
|
class="input"
|
||||||
|
type="search"
|
||||||
|
name="q"
|
||||||
|
placeholder="Type here to search"
|
||||||
|
aria-label="search"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<button
|
||||||
|
class="button is-success"
|
||||||
|
type="button"
|
||||||
|
@click="openEditModal()"
|
||||||
|
>
|
||||||
|
Add new item
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="columns mt-4">
|
||||||
|
<div class="column">
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<LibraryCard
|
||||||
|
v-for="(it, i) in local.result"
|
||||||
|
:id="it.id"
|
||||||
|
:key="i"
|
||||||
|
:title="it.title"
|
||||||
|
:entries="it.entries"
|
||||||
|
:type="it.type"
|
||||||
|
:description="it.description"
|
||||||
|
:additional="additionalContext(it)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<b-pagination
|
||||||
|
v-if="local.count > local.perPage"
|
||||||
|
v-model="local.page"
|
||||||
|
:total="local.count"
|
||||||
|
:per-page="local.perPage"
|
||||||
|
icon-prev="angle-left"
|
||||||
|
icon-next="angle-right"
|
||||||
|
@change="(p) => (local.page = p)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<b-modal v-model="isEditModal">
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header">
|
||||||
|
<div v-if="!edited.id" class="card-header-title">New list</div>
|
||||||
|
<div v-else class="card-header-title">Edit list</div>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<b-field label="Title">
|
||||||
|
<b-input
|
||||||
|
v-model="edited.title"
|
||||||
|
placeholder="Must not be empty"
|
||||||
|
></b-input>
|
||||||
|
</b-field>
|
||||||
|
<b-field label="Entries">
|
||||||
|
<template slot="label">
|
||||||
|
Entries
|
||||||
|
<b-tooltip type="is-dark" label="Space or new-line separated">
|
||||||
|
<b-icon size="is-small" icon="info-circle"></b-icon>
|
||||||
|
</b-tooltip>
|
||||||
|
</template>
|
||||||
|
<b-input
|
||||||
|
v-model="entryString"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="Space or new-line separated, must not be empty"
|
||||||
|
></b-input>
|
||||||
|
</b-field>
|
||||||
|
<b-field label="Description">
|
||||||
|
<b-input v-model="edited.description" type="textarea"></b-input>
|
||||||
|
</b-field>
|
||||||
|
<b-field label="Tag">
|
||||||
|
<b-taginput v-model="edited.tag"></b-taginput>
|
||||||
|
</b-field>
|
||||||
|
<b-field label="Additional options">
|
||||||
|
<b-checkbox
|
||||||
|
:value="edited.isShared"
|
||||||
|
@input="(ev) => $set(edited, 'isShared', ev)"
|
||||||
|
>
|
||||||
|
Make it availble for others (shared library)
|
||||||
|
</b-checkbox>
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
<footer class="card-footer">
|
||||||
|
<div class="card-footer-item">
|
||||||
|
<button
|
||||||
|
class="button is-success"
|
||||||
|
type="button"
|
||||||
|
@click="edited.id ? doUpdate() : doCreate()"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button is-cancel"
|
||||||
|
type="button"
|
||||||
|
@click="isEditModal = false"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Watch } from 'nuxt-property-decorator'
|
||||||
|
import { api, ILibrary } from '~/assets/api'
|
||||||
|
import LibraryCard from '../cards/LibraryCard.vue'
|
||||||
|
|
||||||
|
@Component<LibraryTab>({
|
||||||
|
components: {
|
||||||
|
LibraryCard,
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$emit('title', 'Library')
|
||||||
|
this.updateLocal()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class LibraryTab extends Vue {
|
||||||
|
q = ''
|
||||||
|
q0 = ''
|
||||||
|
isEditModal = false
|
||||||
|
|
||||||
|
local: {
|
||||||
|
result: ILibrary[]
|
||||||
|
count: number
|
||||||
|
page: number
|
||||||
|
perPage: number
|
||||||
|
} = {
|
||||||
|
result: [],
|
||||||
|
count: 0,
|
||||||
|
page: 1,
|
||||||
|
perPage: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
edited: ILibrary = {
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
list: [],
|
||||||
|
type: 'vocabulary',
|
||||||
|
description: '',
|
||||||
|
tag: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
get entryString() {
|
||||||
|
return this.edited.list.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
set entryString(s) {
|
||||||
|
this.edited.list = s
|
||||||
|
.split('\n')
|
||||||
|
.filter((s) => s.trim())
|
||||||
|
.filter((a, i, r) => r.indexOf(a) === i)
|
||||||
|
}
|
||||||
|
|
||||||
|
additionalContext(it: ILibrary) {
|
||||||
|
if (!it.id) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Edit list',
|
||||||
|
handler: () => {
|
||||||
|
this.openEditModal()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete list',
|
||||||
|
handler: () => {
|
||||||
|
if (it.id) {
|
||||||
|
this.doDelete(it.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
openEditModal(it?: ILibrary) {
|
||||||
|
this.edited = it || {
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
list: [],
|
||||||
|
type: 'vocabulary',
|
||||||
|
description: '',
|
||||||
|
tag: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isEditModal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch('q')
|
||||||
|
@Watch('local.page')
|
||||||
|
async updateLocal() {
|
||||||
|
const r = await api.library_getQuery({
|
||||||
|
q: this.q,
|
||||||
|
offset: this.local.page * this.local.perPage,
|
||||||
|
limit: this.local.perPage,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.local = {
|
||||||
|
...this.local,
|
||||||
|
...r,
|
||||||
|
result: r.result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async doCreate() {
|
||||||
|
await api.library_create(this.edited)
|
||||||
|
|
||||||
|
this.$buefy.snackbar.open(`Created list: ${this.edited.title}`)
|
||||||
|
this.isEditModal = false
|
||||||
|
|
||||||
|
this.local.page = 1
|
||||||
|
await this.updateLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
async doUpdate() {
|
||||||
|
const { id } = this.edited
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
await api.library_update(this.edited)
|
||||||
|
|
||||||
|
this.$buefy.snackbar.open(`Updated list: ${this.edited.title}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isEditModal = false
|
||||||
|
|
||||||
|
this.local.page = 1
|
||||||
|
await this.updateLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
async doDelete(id: string) {
|
||||||
|
await api.library_delete({ id })
|
||||||
|
|
||||||
|
this.$buefy.snackbar.open(`Deleted list: ${this.edited.title}`)
|
||||||
|
|
||||||
|
this.local.page = 1
|
||||||
|
await this.updateLocal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
nav.pagination {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button + .button {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.is-cancel {
|
||||||
|
background-color: rgb(215, 217, 219);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
|
||||||
|
/* Basic Options */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
// "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||||
|
// "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
|
||||||
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
// "outDir": "./", /* Redirect output structure to the directory. */
|
||||||
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
"strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
"strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
|
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
"noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||||
|
"noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
|
||||||
|
/* Advanced Options */
|
||||||
|
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2018",
|
"target": "ES2018",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
|
|||||||
Reference in new issue
Block a user