Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -1,69 +1,76 @@
|
||||
<template>
|
||||
<q-page v-if="canUpdateLanguage" class="q-pa-md">
|
||||
<div class="row q-col-gutter-sm items-end q-mb-md">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
v-model="filters.q"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
label="Kelime ara"
|
||||
<template>
|
||||
<q-page v-if="canUpdateLanguage" class="q-pa-md translation-page">
|
||||
<div class="translation-toolbar sticky-toolbar">
|
||||
<div class="row q-col-gutter-sm items-end q-mb-md">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
v-model="filters.q"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
label="Kelime ara"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn color="primary" icon="search" label="Getir" @click="loadRows" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="sync"
|
||||
label="YENİ KELİMELERİ GETİR"
|
||||
:loading="store.saving"
|
||||
@click="syncSources"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-toggle v-model="autoTranslate" dense color="primary" label="Oto Çeviri" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-gutter-sm q-mb-sm">
|
||||
<q-btn
|
||||
color="accent"
|
||||
icon="g_translate"
|
||||
label="Seçilenleri Çevir"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="translateSelectedRows"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn color="primary" icon="search" label="Getir" @click="loadRows" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="sync"
|
||||
label="YENİ KELİMELERİ GETİR"
|
||||
icon="done_all"
|
||||
label="Seçilenleri Onayla"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="syncSources"
|
||||
@click="bulkApproveSelected"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="save"
|
||||
label="Seçilenleri Toplu Güncelle"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="bulkSaveSelected"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-toggle v-model="autoTranslate" dense color="primary" label="Oto Çeviri" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-gutter-sm q-mb-sm">
|
||||
<q-btn
|
||||
color="accent"
|
||||
icon="g_translate"
|
||||
label="Seçilenleri Çevir"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="translateSelectedRows"
|
||||
/>
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="done_all"
|
||||
label="Seçilenleri Onayla"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="bulkApproveSelected"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="save"
|
||||
label="Seçilenleri Toplu Güncelle"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="bulkSaveSelected"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
ref="tableRef"
|
||||
class="translation-table"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
virtual-scroll
|
||||
:virtual-scroll-sticky-size-start="56"
|
||||
row-key="t_key"
|
||||
:loading="store.loading || store.saving"
|
||||
:rows="pivotRows"
|
||||
:columns="columns"
|
||||
:rows-per-page-options="[0]"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
v-model:pagination="tablePagination"
|
||||
hide-bottom
|
||||
@virtual-scroll="onVirtualScroll"
|
||||
>
|
||||
<template #body-cell-actions="props">
|
||||
<q-td :props="props">
|
||||
@@ -91,7 +98,9 @@
|
||||
|
||||
<template #body-cell-source_text_tr="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'source_text_tr')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).source_text_tr" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<div class="source-text-label" :title="rowDraft(props.row.t_key).source_text_tr">
|
||||
{{ rowDraft(props.row.t_key).source_text_tr }}
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
@@ -111,37 +120,79 @@
|
||||
|
||||
<template #body-cell-en="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'en')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).en" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).en"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-de="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'de')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).de" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).de"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-es="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'es')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).es" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).es"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-it="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'it')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).it" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).it"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-ru="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'ru')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).ru" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).ru"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-ar="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'ar')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).ar" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).ar"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
@@ -155,7 +206,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
import { useTranslationStore } from 'src/stores/translationStore'
|
||||
@@ -169,6 +220,18 @@ const filters = ref({
|
||||
q: ''
|
||||
})
|
||||
const autoTranslate = ref(false)
|
||||
const tableRef = ref(null)
|
||||
const FETCH_LIMIT = 1400
|
||||
const loadedOffset = ref(0)
|
||||
const hasMoreRows = ref(true)
|
||||
const loadingMore = ref(false)
|
||||
const tablePagination = ref({
|
||||
page: 1,
|
||||
rowsPerPage: 0,
|
||||
sortBy: 'source_text_tr',
|
||||
descending: false
|
||||
})
|
||||
let filterReloadTimer = null
|
||||
|
||||
const sourceTypeOptions = [
|
||||
{ label: 'dummy', value: 'dummy' },
|
||||
@@ -179,15 +242,14 @@ const sourceTypeOptions = [
|
||||
const columns = [
|
||||
{ name: 'actions', label: 'Güncelle', field: 'actions', align: 'left' },
|
||||
{ name: 'select', label: 'Seç', field: 'select', align: 'left' },
|
||||
{ name: 't_key', label: 'Key', field: 't_key', align: 'left', sortable: true },
|
||||
{ name: 'source_text_tr', label: 'Türkçe kaynak', field: 'source_text_tr', align: 'left' },
|
||||
{ name: 'source_type', label: 'Veri tipi', field: 'source_type', align: 'left' },
|
||||
{ name: 'en', label: 'English', field: 'en', align: 'left' },
|
||||
{ name: 'de', label: 'Deutch', field: 'de', align: 'left' },
|
||||
{ name: 'es', label: 'Espanol', field: 'es', align: 'left' },
|
||||
{ name: 'it', label: 'Italiano', field: 'it', align: 'left' },
|
||||
{ name: 'ru', label: 'Русский', field: 'ru', align: 'left' },
|
||||
{ name: 'ar', label: 'العربية', field: 'ar', align: 'left' }
|
||||
{ name: 'source_text_tr', label: 'Türkçe Metin', field: 'source_text_tr', align: 'left', style: 'min-width: 340px' },
|
||||
{ name: 'source_type', label: 'Kaynak', field: 'source_type', align: 'left', style: 'min-width: 140px' },
|
||||
{ name: 'en', label: 'İngilizce', field: 'en', align: 'left', style: 'min-width: 220px' },
|
||||
{ name: 'de', label: 'Almanca', field: 'de', align: 'left', style: 'min-width: 220px' },
|
||||
{ name: 'es', label: 'İspanyolca', field: 'es', align: 'left', style: 'min-width: 220px' },
|
||||
{ name: 'it', label: 'İtalyanca', field: 'it', align: 'left', style: 'min-width: 220px' },
|
||||
{ name: 'ru', label: 'Rusça', field: 'ru', align: 'left', style: 'min-width: 220px' },
|
||||
{ name: 'ar', label: 'Arapça', field: 'ar', align: 'left', style: 'min-width: 220px' }
|
||||
]
|
||||
|
||||
const draftByKey = ref({})
|
||||
@@ -242,10 +304,33 @@ const pivotRows = computed(() => {
|
||||
return Array.from(byKey.values()).sort((a, b) => a.t_key.localeCompare(b.t_key))
|
||||
})
|
||||
|
||||
function snapshotDrafts () {
|
||||
function snapshotDrafts (options = {}) {
|
||||
const preserveDirty = Boolean(options?.preserveDirty)
|
||||
const draft = {}
|
||||
const original = {}
|
||||
for (const row of pivotRows.value) {
|
||||
const existingDraft = draftByKey.value[row.t_key]
|
||||
const existingOriginal = originalByKey.value[row.t_key]
|
||||
const keepExisting = preserveDirty &&
|
||||
existingDraft &&
|
||||
existingOriginal &&
|
||||
(
|
||||
existingDraft.source_text_tr !== existingOriginal.source_text_tr ||
|
||||
existingDraft.source_type !== existingOriginal.source_type ||
|
||||
existingDraft.en !== existingOriginal.en ||
|
||||
existingDraft.de !== existingOriginal.de ||
|
||||
existingDraft.es !== existingOriginal.es ||
|
||||
existingDraft.it !== existingOriginal.it ||
|
||||
existingDraft.ru !== existingOriginal.ru ||
|
||||
existingDraft.ar !== existingOriginal.ar
|
||||
)
|
||||
|
||||
if (keepExisting) {
|
||||
draft[row.t_key] = { ...existingDraft }
|
||||
original[row.t_key] = { ...existingOriginal }
|
||||
continue
|
||||
}
|
||||
|
||||
draft[row.t_key] = {
|
||||
source_text_tr: row.source_text_tr || '',
|
||||
source_type: row.source_type || 'dummy',
|
||||
@@ -280,8 +365,9 @@ function rowDraft (key) {
|
||||
}
|
||||
|
||||
function buildFilters () {
|
||||
const query = String(filters.value.q || '').trim()
|
||||
return {
|
||||
q: filters.value.q || undefined
|
||||
q: query || undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,10 +435,30 @@ function queueAutoSave (key) {
|
||||
autoSaveTimers.set(key, timer)
|
||||
}
|
||||
|
||||
async function fetchRowsChunk (append = false) {
|
||||
const params = {
|
||||
...buildFilters(),
|
||||
limit: FETCH_LIMIT,
|
||||
offset: append ? loadedOffset.value : 0
|
||||
}
|
||||
|
||||
await store.fetchRows(params, { append })
|
||||
const incomingCount = Number(store.count) || 0
|
||||
|
||||
if (append) {
|
||||
loadedOffset.value += incomingCount
|
||||
} else {
|
||||
loadedOffset.value = incomingCount
|
||||
}
|
||||
hasMoreRows.value = incomingCount === FETCH_LIMIT
|
||||
snapshotDrafts({ preserveDirty: append })
|
||||
}
|
||||
|
||||
async function loadRows () {
|
||||
try {
|
||||
await store.fetchRows(buildFilters())
|
||||
snapshotDrafts()
|
||||
loadedOffset.value = 0
|
||||
hasMoreRows.value = true
|
||||
await fetchRowsChunk(false)
|
||||
} catch (err) {
|
||||
console.error('[translation-sync][ui] loadRows:error', {
|
||||
message: err?.message || 'Çeviri satırları yüklenemedi'
|
||||
@@ -364,6 +470,42 @@ async function loadRows () {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreRows () {
|
||||
if (!hasMoreRows.value || loadingMore.value || store.loading || store.saving) return
|
||||
loadingMore.value = true
|
||||
try {
|
||||
await fetchRowsChunk(true)
|
||||
} finally {
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureEnoughVisibleRows (minRows = 120, maxBatches = 4) {
|
||||
let guard = 0
|
||||
while (hasMoreRows.value && pivotRows.value.length < minRows && guard < maxBatches) {
|
||||
await loadMoreRows()
|
||||
guard++
|
||||
}
|
||||
}
|
||||
|
||||
function onVirtualScroll (details) {
|
||||
const to = Number(details?.to || 0)
|
||||
if (!Number.isFinite(to)) return
|
||||
if (to >= pivotRows.value.length - 15) {
|
||||
void loadMoreRows()
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleFilterReload () {
|
||||
if (filterReloadTimer) {
|
||||
clearTimeout(filterReloadTimer)
|
||||
}
|
||||
filterReloadTimer = setTimeout(() => {
|
||||
filterReloadTimer = null
|
||||
void loadRows()
|
||||
}, 350)
|
||||
}
|
||||
|
||||
async function ensureMissingLangRows (key, draft, langs) {
|
||||
const missingLangs = []
|
||||
if (!langs.en && String(draft.en || '').trim() !== '') missingLangs.push('en')
|
||||
@@ -618,11 +760,81 @@ async function syncSources () {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRows()
|
||||
void loadRows()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (filterReloadTimer) {
|
||||
clearTimeout(filterReloadTimer)
|
||||
filterReloadTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => filters.value.q,
|
||||
() => { scheduleFilterReload() }
|
||||
)
|
||||
|
||||
watch(
|
||||
[() => tablePagination.value.sortBy, () => tablePagination.value.descending],
|
||||
() => { void ensureEnoughVisibleRows(120, 4) }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.translation-page {
|
||||
height: calc(100vh - 120px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.translation-toolbar {
|
||||
background: #fff;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.sticky-toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 35;
|
||||
}
|
||||
|
||||
.translation-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.translation-table :deep(.q-table__middle) {
|
||||
max-height: calc(100vh - 280px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.translation-table :deep(.q-table thead tr th) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.translation-table :deep(.q-table tbody td) {
|
||||
vertical-align: top;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.translation-table :deep(.q-field__native) {
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.source-text-label {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
max-height: 11.2em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cell-dirty {
|
||||
background: #fff3cd;
|
||||
}
|
||||
@@ -631,3 +843,4 @@ onMounted(() => {
|
||||
background: #d9f7e8;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user