This commit is contained in:
polv committed 2021-10-25 06:46:53 +07:00
1 parent ffbe5fed18
commit b32007acd1
6 files changed
+935 -51

No files matched your search

+80 -24
View File
@@ -9,17 +9,21 @@ export interface ILevelBrowserByType {
export type ILevelBrowser = Record<ITypeLevel, ILevelBrowserByType> export type ILevelBrowser = Record<ITypeLevel, ILevelBrowserByType>
export interface IUserSettings { export interface IUserLocalSettings {
identifier: string
level: number
levelMin?: number levelMin?: number
level_browser?: ILevelBrowser level_browser?: ILevelBrowser
} }
export interface IUserSettings extends IUserLocalSettings {
identifier: string
level: number
}
export interface IEntry { export interface IEntry {
id: string id: string
entry: string[] list: string[]
reading: string[] reading: string[]
audio: string[]
translation: string[] translation: string[]
type: IType type: IType
description: string description: string
@@ -36,6 +40,17 @@ export interface ILibrary {
isShared?: boolean isShared?: boolean
} }
export interface IQuiz {
id: string
type: IType
direction: 'forward' | 'backward'
entry: string
nextReview?: string // format: date-time
wrongStreak?: number
hint: string
mnemonic: string
}
export type ILookup<T> = T & { export type ILookup<T> = T & {
offset?: number offset?: number
limit?: number limit?: number
@@ -109,17 +124,23 @@ class Api {
return r return r
} }
async user_updateSettings(u: Omit<IUserSettings, 'identifier' | 'level'>) { async user_updateSettings(u: IUserLocalSettings) {
return await this.axios.patch('/api/user', u) return await this.axios.patch('/api/user', u)
} }
async quiz_getMany(payload: { async quiz_getById(params: { id: string }) {
return await this.axios
.get('/api/quiz', { params })
.then((r) => r.data as IQuiz)
.catch(() => null)
}
async quiz_getManyByEntries(payload: {
entries: string[] entries: string[]
select: string[] type: string
type?: string
direction?: string direction?: string
}) { }) {
return await this.axios.post('/api/quiz/getMany', payload).then( return await this.axios.post('/api/quiz/getManyByEntries', payload).then(
(r) => (r) =>
r.data as { r.data as {
result: { result: {
@@ -142,6 +163,10 @@ class Api {
) )
} }
async quiz_updateSrsLevel(params: { id: string; df: number }) {
return await this.axios.patch('/api/quiz/srsLevel', { params })
}
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) =>
@@ -153,6 +178,12 @@ class Api {
) )
} }
async quiz_update(payload: Partial<IQuiz> & { id: string }) {
return await this.axios.patch('/api/quiz', payload, {
params: { id: payload.id },
})
}
async quiz_deleteMany(params: { entries: string[]; type: IType }) { async quiz_deleteMany(params: { entries: string[]; type: IType }) {
return await this.axios.post('/api/quiz/deleteMany', params) return await this.axios.post('/api/quiz/deleteMany', params)
} }
@@ -185,39 +216,51 @@ class Api {
} }
async character_getRadical(params: { entry: string }) { async character_getRadical(params: { entry: string }) {
return await this.axios.get('/api/character/radical', { params }).then( return await this.axios
.get('/api/character/radical', { params })
.then(
(r) => (r) =>
r.data as { r.data as {
result: {
sub: string[] sub: string[]
sup: string[] sup: string[]
variant: string[] variant: string[]
visual: string[] visual: string[]
} | null
} }
) )
.catch(() => null)
} }
async character_getVocabulary( async character_getVocabulary(
params: ILookup<{ entry: string; distinct?: 'id' }> params: ILookup<{ entry: string; distinct?: 'id' }>
) { ) {
return await this.axios.get('/api/character/vocabulary', { params }).then( params.offset = params.offset || 0
(r) => params.limit = params.limit || 5
r.data as ILookupResult<{
entry: string return await this.axios
}> .get('/api/character/vocabulary', { params })
) .then((r) => r.data as ILookupResult<IEntry>)
} }
async character_getSentence( async character_getSentence(
params: ILookup<{ entry: string; distinct?: 'id' }> params: ILookup<{ entry: string; distinct?: 'id' }>
) { ) {
return await this.axios.get('/api/character/sentence', { params }).then( params.offset = params.offset || 0
(r) => params.limit = params.limit || 5
r.data as ILookupResult<{
entry: string return await this.axios
}> .get('/api/character/sentence', { params })
) .then((r) => r.data as ILookupResult<IEntry>)
}
async vocabulary_getSentence(
params: ILookup<{ entry: string; distinct?: 'id' }>
) {
params.offset = params.offset || 0
params.limit = params.limit || 5
return await this.axios
.get('/api/vocabulary/sentence', { params })
.then((r) => r.data as ILookupResult<IEntry>)
} }
async all_getRandom(params: { type: IType }) { async all_getRandom(params: { type: IType }) {
@@ -263,6 +306,19 @@ class Api {
) )
} }
async all_tokenize(params: { entry: string }) {
return await this.axios
.get('/api/all/tokenize', {
params,
})
.then(
(r) =>
r.data as {
result: string[]
}
)
}
async all_getReading(params: { entry: string }) { async all_getReading(params: { entry: string }) {
return await this.axios return await this.axios
.get('/api/all/reading', { .get('/api/all/reading', {
+2 -3
View File
@@ -50,7 +50,7 @@
</li> </li>
<li v-if="entry"> <li v-if="entry">
<a role="button" @click="openInNewTab('Character', { q: entry })"> <a role="button" @click="openInNewTab('Character', { q: entry })">
{{ type === 'character' ? 'Show details' : 'Search for Hanzi' }} {{ type === 'character' ? 'Show details' : 'Search for Kanji' }}
</a> </a>
</li> </li>
<li v-if="entry && type !== 'character'"> <li v-if="entry && type !== 'character'">
@@ -135,9 +135,8 @@ export default class ContextMenu extends Vue {
async setQuiz() { async setQuiz() {
if (this.entries.length && this.type) { if (this.entries.length && this.type) {
const { result } = await api.quiz_getMany({ const { result } = await api.quiz_getManyByEntries({
entries: this.entries, entries: this.entries,
select: ['id', 'entry'],
type: this.type, type: this.type,
direction: this.direction, direction: this.direction,
}) })
+6 -6
View File
@@ -154,9 +154,9 @@ import ContextMenu from '../ContextMenu.vue'
type ILibraryEntry = Pick< type ILibraryEntry = Pick<
IEntry, IEntry,
'entry' | 'reading' | 'translation' | 'type' 'list' | 'reading' | 'translation' | 'type'
> & { > & {
originalEntry: string entry: string
} }
@Component<LibraryCard>({ @Component<LibraryCard>({
@@ -204,8 +204,8 @@ export default class LibraryCard extends Vue {
isList = false isList = false
list: ILibraryEntry[] = this.entries.map((el) => ({ list: ILibraryEntry[] = this.entries.map((el) => ({
originalEntry: el, entry: el,
entry: [el], list: [el],
reading: [], reading: [],
translation: [], translation: [],
type: this.type, type: this.type,
@@ -267,7 +267,7 @@ export default class LibraryCard extends Vue {
[...listMap].map(([k, v]) => [...listMap].map(([k, v]) =>
api api
.all_getLookup({ .all_getLookup({
entry: v.originalEntry, entry: v.entry,
type: this.type, type: this.type,
forced: true, forced: true,
limit: 1, limit: 1,
@@ -290,7 +290,7 @@ export default class LibraryCard extends Vue {
getTagClass(it: IEntry) { getTagClass(it: IEntry) {
const srsLevel = Math.min( const srsLevel = Math.min(
...it.entry.map((el) => this.srsLevel[el] || -Infinity) ...it.list.map((el) => this.srsLevel[el] || -Infinity)
) )
if (srsLevel === -Infinity) return 'is-light' if (srsLevel === -Infinity) return 'is-light'
+837
View File
@@ -0,0 +1,837 @@
<template>
<b-modal class="quiz-modal" :active.sync="isQuizModal" @close="endQuiz">
<div class="card">
<div v-if="current.id" class="card-content">
<div
v-show="!isQuizShownAnswer"
class="content hover-parent"
style="cursor: pointer"
@click="addHint"
>
<div v-if="current.type === 'character'">
<div v-if="current.direction === 'ec'">
<h4>Hanzi English-Chinese</h4>
<div
v-html="
doMask((current.english || []).join(' / '), current.entry)
"
></div>
</div>
<div v-else>
<h4>Hanzi Chinese-English</h4>
<div
class="font-han text-w-normal hanzi-display"
style="text-align: center"
>
{{ current.entry }}
</div>
</div>
</div>
<div v-else-if="current.type === 'vocabulary'">
<div v-if="current.direction === 'ec'">
<h4>Vocab English-Chinese</h4>
<ul v-if="Array.isArray(current.english)">
<li
v-for="(it, i) in current.english"
:key="i"
v-html="
doMask(
it,
current.entry,
...(current.reading || []),
...(current.alt || [])
)
"
></li>
</ul>
<div
v-else
v-html="
doMask(
(current.english || []).join(' / '),
current.entry,
...(current.reading || []),
...(current.alt || [])
)
"
></div>
</div>
<div v-else>
<h4 v-if="current.direction === 'te'">
Vocabulary Traditional-English
</h4>
<h4 v-else>Vocab Simplified-English</h4>
<div class="font-zh-simp text-w-normal" style="font-size: 2rem">
{{ current.entry }}
</div>
</div>
</div>
<div v-else>
<div v-if="current.direction === 'ec'">
<h4>Sentence English-Chinese</h4>
<ul>
<li v-for="(e, i) in current.english" :key="i">
{{ e }}
</li>
</ul>
</div>
<div v-else>
<h4>Sentence Chinese-English</h4>
<h2 class="font-zh-simp text-w-normal">
{{ current.entry }}
</h2>
</div>
</div>
<div class="mt-4 mb-4 hover-child">
<small>
{{ current.hint || 'Click to add hint' }}
</small>
</div>
</div>
<div v-show="isQuizShownAnswer" class="content">
<div v-if="current.type === 'character'">
<div
class="hanzi-display has-context font-han"
@click="openContext"
@contextmenu.prevent="openContext"
style="text-align: center"
>
{{ current.entry }}
</div>
<div style="text-align: center; margin-bottom: 1rem">
{{ (current.reading || []).join(' | ') }}
</div>
<div style="text-align: center">
{{ (current.english || []).join(' / ') }}
</div>
<ul v-if="getSentenceByCharacter(current.entry).length">
<li
v-for="(it, i) in getSentenceByCharacter(current.entry)"
:key="i"
>
<span
class="has-context"
:title="it.reading[0]"
@click="(ev) => openContext(ev, it.entry, 'sentence')"
@contextmenu.prevent="
(ev) => openContext(ev, it.entry, 'sentence')
"
>
{{ it.entry }}
</span>
<ul>
<li v-for="(e, j) in it.english" :key="j" class="english">
{{ e }}
</li>
</ul>
</li>
</ul>
</div>
<div v-else-if="current.type === 'vocabulary'">
<div
class="font-zh-simp text-w-normal has-context"
style="font-size: 2rem"
@click="openContext"
@contextmenu.prevent="openContext"
>
{{ current.simplified }}
</div>
<div
v-if="current.alt && current.alt.length"
class="font-zh-trad text-w-normal"
style="font-size: 1.7rem"
>
<span
v-for="(it, i) in current.alt"
:key="i"
class="traditional has-context"
@click="(ev) => openContext(ev, it)"
@contextmenu.prevent="(ev) => openContext(ev, it)"
>
{{ it }}
</span>
</div>
<div>
{{ (current.reading || []).join(' | ') }}
</div>
<ul>
<li v-for="(it, i) in current.english" :key="i">
{{ it }}
</li>
</ul>
<ul
v-if="current.sentences.length"
:key="dictionaryData.sentence.length"
>
<li
v-for="(it, i) in current.sentences"
:key="i"
class="sentence"
>
<span
class="has-context"
:title="dictionaryData.sentence[it].reading[0]"
@click="(ev) => openContext(ev, it, 'sentence')"
@contextmenu.prevent="(ev) => openContext(ev, it, 'sentence')"
>
{{ it }}
</span>
<ul>
<li
v-for="(e, j) in dictionaryData.sentence[it].english"
:key="j"
class="english"
>
{{ e }}
</li>
</ul>
</li>
</ul>
</div>
<div v-else>
<h2
class="font-zh-simp text-w-normal has-context"
:title="(current.reading || [])[0]"
@click="openContext"
@contextmenu.prevent="openContext"
>
{{ current.entry }}
</h2>
<ul v-if="current.english.length">
<li v-for="(it, i) in current.english" :key="i">
{{ it }}
</li>
</ul>
<ul v-if="current.vocabulary.length">
<li v-for="(it, i) in current.vocabulary" :key="i">
<span
class="has-context"
@click="(ev) => openContext(ev, it.entry, 'vocabulary')"
@contextmenu.prevent="
(ev) => openContext(ev, it.entry, 'vocabulary')
"
>
{{ it.entry }}
</span>
<span>[{{ it.reading.join(' | ') }}]</span>
<span>
{{ it.english.join(' / ') }}
</span>
</li>
</ul>
</div>
<div class="has-context mt-4 mb-4" @click="addMnemonic">
<small>
{{ current.mnemonic || 'Click to add mnemonic' }}
</small>
</div>
<div v-if="current.tag && current.tag.length" class="mb-4">
Tags:
<b-taglist style="display: inline-flex">
<b-tag
v-for="t in current.tag.slice(0, 5)"
:key="t"
type="is-info"
>
{{ t }}
</b-tag>
</b-taglist>
</div>
</div>
</div>
<div class="buttons-area">
<div v-if="quizIndex >= quizArray.length" class="buttons">
<button
ref="btnEndQuiz"
class="button is-warning"
@click="endQuiz"
@keypress="endQuiz"
>
End quiz
</button>
</div>
<div v-else-if="!isQuizShownAnswer" class="buttons">
<button
ref="btnShowAnswer"
class="button is-warning"
@click="isQuizShownAnswer = true"
@keypress="isQuizShownAnswer = true"
>
Show answer
</button>
</div>
<div v-else class="buttons-panel">
<div class="buttons">
<button
ref="btnMarkRight"
class="button is-success"
@click="doMark('right')"
@keypress="doMark('right')"
>
Right
</button>
<button
ref="btnMarkWrong"
class="button is-danger"
@click="doMark('wrong')"
@keypress="doMark('wrong')"
>
Wrong
</button>
<button
ref="btnMarkRepeat"
class="button is-warning"
@click="doMark('repeat')"
@keypress="doMark('repeat')"
>
Repeat
</button>
</div>
<div class="buttons">
<button
ref="btnHideAnswer"
class="button is-warning"
@click="isQuizShownAnswer = false"
@keypress="isQuizShownAnswer = false"
>
Hide answer
</button>
</div>
</div>
</div>
</div>
<ContextMenu ref="context" :entry="ctxEntry" :type="ctxType" />
</b-modal>
</template>
<script lang="ts">
import { Vue, Component, Prop, Ref } from 'vue-property-decorator'
import { api, IEntry, IQuiz } from '~/assets/api'
import { shuffle } from '~/assets/util'
import ContextMenu from '../ContextMenu.vue'
@Component({
components: {
ContextMenu,
},
})
export default class QuizCard extends Vue {
@Prop({ required: true }) quizArray!: string[]
@Ref() context!: ContextMenu
ctxEntry = ''
ctxType = ''
isQuizShownAnswer = false
isQuizModal = false
quizIndex = -1
quizData: {
[quizId: string]: IQuiz
} = {}
dictionaryData = {
character: {} as Record<string, IEntry>,
vocabulary: {} as Record<
string,
IEntry & {
sentence_set: Record<string, boolean>
}
>,
sentence: {} as Record<
string,
IEntry & {
vocabulary_set: Record<string, boolean>
}
>,
}
get current() {
const it = this.quizData[this.quizArray[this.quizIndex]!]
if (!it || !it.type || !it.entry) {
return {} as Partial<IQuiz>
}
return {
...it,
...this.dictionaryData[it.type][it.entry],
}
}
fromCharacter(c: string, type: 'vocabulary' | 'sentence') {
return shuffle(
Object.entries(this.dictionaryData[type]).filter(([entry]) =>
entry.includes(c)
)
)
.slice(0, 5)
.map(([entry, v]) => ({
...v,
entry,
}))
}
async startQuiz() {
this.quizIndex = -1
await this.initNextQuizItem()
}
endQuiz() {
this.isQuizModal = false
this.$emit('quiz:ended')
}
async doMark(type: 'right' | 'wrong' | 'repeat') {
const id = this.quizArray[this.quizIndex] as string | undefined
if (id) {
api.quiz_updateSrsLevel({
id,
df: {
right: 1,
wrong: -1,
repeat: 0,
}[type],
})
}
this.initNextQuizItem()
}
async initNextQuizItem() {
this.isQuizShownAnswer = false
Array(2)
.fill(null)
.map((_, i) => {
const front = this.checkFront(i + 1)
if (!front) {
this.cacheQuizItem({
relativePosition: i + 1,
})
}
})
if (this.quizArray[this.quizIndex + 1]) {
await this.cacheQuizItem({ relativePosition: 0 })
}
let it: IQuiz | undefined
while (true) {
it = this.quizData[this.quizArray[++this.quizIndex]!]
if (it && it.entry && it.type) {
break
}
if (this.quizIndex >= this.quizArray.length) {
break
}
}
this.isQuizModal = true
}
checkFront(relativePosition: number): boolean {
relativePosition++
const it = this.quizData[this.quizArray[this.quizIndex + relativePosition]!]
if (!it) {
return false
}
const { entry, type, direction } = it
if (!entry || !type || !direction) {
return false
}
return !!this.dictionaryData[type][entry]
}
openContext(ev: MouseEvent, entry?: string, type?: string) {
this.ctxEntry = entry || this.current.entry || ''
this.ctxType = type || this.current.type || ''
this.context.open(ev)
}
doMask(s = '', ...ws: string[]) {
ws.map((w) => {
s = s.replace(
new RegExp(
`(^| |[^a-z])(${w
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/\d/g, '\\d?')
.replace(/\s+/g, '\\s*')})($| |[^a-z])`,
'gi'
),
'$1<span class="gray-box">$2</span>$3'
)
})
return s
}
async cacheQuizItem(params: { quizId?: string; relativePosition?: number }) {
let { relativePosition = 0 } = params
relativePosition++
let { quizId } = params
quizId = quizId || this.quizArray[this.quizIndex + relativePosition]
if (!quizId) {
return
}
let q = this.quizData[quizId]
if (!q || !q.entry || !q.type || !q.direction) {
const r = await api.quiz_getById({
id: quizId,
})
if (r) {
if (q) {
Object.assign(q, r)
} else {
q = {
...r,
id: quizId,
type: r.type,
}
}
q.id = quizId
q.type = q.type
q.entry = q.entry as string
this.$set(this.quizData, quizId, q)
}
}
const type = q?.type
const entry = q?.entry
if (!entry || !type) {
return
}
if (!this.dictionaryData[type][entry]) {
const {
result: [r],
} = await api.all_getLookup({
entry,
type,
limit: 1,
forced: true,
})
this.dictionaryData[type][entry] = r!
this.$set(this.dictionaryData, type, this.dictionaryData[type])
}
if (type === 'character') {
await Promise.allSettled([
(async () => {
const subtype = 'vocabulary'
if (this.fromCharacter(entry, subtype).length < 5) {
const { result } = await api.character_getVocabulary({ entry })
result.map((r) => {
if (this.dictionaryData[subtype][r.list[0]!]) {
return
}
this.dictionaryData[subtype][r.list[0]!] = {
...r,
sentence_set: {},
}
})
this.$set(
this.dictionaryData,
subtype,
this.dictionaryData[subtype]
)
}
})(),
(async () => {
const subtype = 'sentence'
if (this.fromCharacter(entry, subtype).length < 5) {
const { result } = await api.character_getSentence({ entry })
result.map((r) => {
if (this.dictionaryData[subtype][r.list[0]!]) {
return
}
this.dictionaryData[subtype][r.list[0]!] = {
...r,
vocabulary_set: {},
}
})
this.$set(
this.dictionaryData,
subtype,
this.dictionaryData[subtype]
)
}
})(),
])
} else if (type === 'vocabulary') {
if (
Object.keys(this.dictionaryData[type][entry]!.sentence_set).length < 5
) {
const subtype = 'sentence'
const { result } = await api.vocabulary_getSentence({ entry })
result.map((r) => {
if (this.dictionaryData[subtype][r.list[0]!]) {
return
}
this.dictionaryData[subtype][r.list[0]!] = {
...r,
vocabulary_set: {},
}
})
this.dictionaryData[type][entry]!.sentence_set = result.reduce(
(prev, c) => ({
...prev,
[c.list[0]!]: true,
}),
{} as Record<string, boolean>
)
this.$set(this.dictionaryData, type, this.dictionaryData[type])
this.$set(this.dictionaryData, subtype, this.dictionaryData[subtype])
}
} else if (type === 'sentence') {
if (
!Object.keys(this.dictionaryData[type][entry]!.vocabulary_set).length
) {
const subtype = 'vocabulary'
const { result: tokens } = await api.all_tokenize({ entry })
await Promise.allSettled(
tokens.map(async (entry) => {
if (this.dictionaryData[subtype][entry]) {
return
}
const {
result: [r],
} = await api.all_getLookup({
entry,
type: subtype,
limit: 1,
forced: true,
})
this.dictionaryData[subtype][entry] = {
...r!,
sentence_set: {},
}
this.$set(
this.dictionaryData,
subtype,
this.dictionaryData[subtype]
)
})
)
}
}
}
addHint() {
const i = this.quizArray[this.quizIndex]
const it = this.quizData[i!]
if (!it) return
this.$buefy.dialog.prompt({
message: 'Hint',
inputAttrs: {
value: it.hint,
},
trapFocus: true,
onConfirm: async (hint) => {
await api.quiz_update({ hint, id: it.id })
it.hint = hint
this.$set(this.quizData, i!, it)
},
})
}
addMnemonic() {
const i = this.quizArray[this.quizIndex]
const it = this.quizData[i!]
if (!it) return
this.$buefy.dialog.prompt({
message: 'Mnemonic',
inputAttrs: {
value: it.mnemonic,
},
trapFocus: true,
onConfirm: async (mnemonic) => {
await api.quiz_update({ mnemonic, id: it.id })
it.mnemonic = mnemonic
this.$set(this.quizData, i!, it)
},
})
}
}
</script>
<style lang="scss" scoped>
.has-context {
cursor: pointer;
&:hover {
color: blue;
}
}
.hover-child {
display: none;
}
.hover-parent:hover .hover-child {
display: block;
}
.traditional {
& + &::before {
content: ' | ';
display: inline-block;
}
}
.quiz-modal .modal-content {
max-width: 500px !important;
}
.quiz-modal .buttons-area {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
padding-top: 1rem;
padding-bottom: 1rem;
}
.buttons-area .buttons {
margin-bottom: 0;
}
.quiz-modal .card-content {
min-height: 7.5rem;
max-height: calc(100vh - 300px);
overflow: scroll;
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
margin-left: 1.5rem;
margin-right: 1.5rem;
border-bottom: 1px solid hsla(0, 0%, 50%, 0.25);
scrollbar-width: none; /* For Firefox */
-ms-overflow-style: none; /* For Internet Explorer and Edge */
&::-webkit-scrollbar {
width: 0px; /* For Chrome, Safari, and Opera */
}
}
.buttons-panel {
display: flex;
flex-direction: column;
align-items: center;
}
.buttons-panel .buttons {
margin-bottom: 0;
}
.buttons-area button::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
transform: translate(-50%, -50%);
border-radius: 50%;
background-color: currentColor;
visibility: hidden;
z-index: 2;
}
.buttons-area button:not(:active)::before {
animation: ripple 0.4s cubic-bezier(0, 0, 0.2, 1);
transition: visibility 0.4s step-end;
}
.buttons-area button:active::before {
visibility: visible;
}
.edit-modal .card-footer {
padding: 1rem;
}
.sentence {
li.english {
visibility: hidden;
}
&:hover li.english {
visibility: visible;
}
}
@keyframes ripple {
0% {
width: 0;
height: 0;
opacity: 0.5;
}
100% {
width: 150px;
height: 150px;
opacity: 0;
}
}
</style>
+7 -15
View File
@@ -144,7 +144,7 @@
v-if="selected.entry.every((el) => el.length === 1)" v-if="selected.entry.every((el) => el.length === 1)"
value="character" value="character"
> >
Hanzi Kanji
</option> </option>
</b-select> </b-select>
</b-field> </b-field>
@@ -222,14 +222,14 @@ export default class BrowseTab extends Vue {
type: 'desc', type: 'desc',
} }
selected: Omit<IEntry, 'entry'> & { selected: IEntry & {
entry: string entry: string
list: string[]
} = { } = {
id: '', id: '',
entry: '', entry: '',
list: [], list: [],
reading: [], reading: [],
audio: [],
translation: [], translation: [],
type: 'vocabulary', type: 'vocabulary',
description: '', description: '',
@@ -296,10 +296,7 @@ export default class BrowseTab extends Vue {
} }
async doCreate() { async doCreate() {
const { id } = await api.extra_create({ const { id } = await api.extra_create(this.selected)
...this.selected,
entry: this.selected.list,
})
this.selected.id = id this.selected.id = id
await this.context.addToQuiz() await this.context.addToQuiz()
@@ -314,7 +311,6 @@ export default class BrowseTab extends Vue {
if (id) { if (id) {
await api.extra_update({ await api.extra_update({
...this.selected, ...this.selected,
entry: this.selected.list,
id, id,
}) })
@@ -345,6 +341,7 @@ export default class BrowseTab extends Vue {
entry: '', entry: '',
list: [], list: [],
reading: [], reading: [],
audio: [],
translation: [], translation: [],
type: 'vocabulary', type: 'vocabulary',
description: '', description: '',
@@ -354,15 +351,10 @@ export default class BrowseTab extends Vue {
this.isEditModal = true this.isEditModal = true
} }
async onTableContextmenu(row: IEntry, evt: MouseEvent) { async onTableContextmenu(row: IEntry & { entry: string }, evt: MouseEvent) {
evt.preventDefault() evt.preventDefault()
const o = jsonClone(row) this.selected = jsonClone(row)
this.selected = {
...o,
entry: o.entry[0]!,
list: o.entry,
}
this.context.open(evt) this.context.open(evt)
} }
+3 -3
View File
@@ -416,7 +416,7 @@ export default class CharacterTab extends Vue {
await Promise.all([ await Promise.all([
(async () => { (async () => {
const { result: r } = await api.character_getRadical({ const r = await api.character_getRadical({
entry: this.current, entry: this.current,
}) })
@@ -457,7 +457,7 @@ export default class CharacterTab extends Vue {
result result
.map((r) => .map((r) =>
api api
.all_getLookup({ entry: r.entry, type: 'vocabulary', limit: 1 }) .all_getLookup({ entry: r.list[0]!, type: 'vocabulary', limit: 1 })
.then((r) => r.result[0]!) .then((r) => r.result[0]!)
) )
.filter((r) => r) .filter((r) => r)
@@ -479,7 +479,7 @@ export default class CharacterTab extends Vue {
result result
.map((r) => .map((r) =>
api api
.all_getLookup({ entry: r.entry, type: 'sentence', limit: 1 }) .all_getLookup({ entry: r.list[0]!, type: 'sentence', limit: 1 })
.then((r) => r.result[0]!) .then((r) => r.result[0]!)
) )
.filter((r) => r) .filter((r) => r)