fix some bugs
This commit is contained in:
@@ -118,9 +118,9 @@
|
||||
>
|
||||
<BasicInfoItem
|
||||
title="نام قلک"
|
||||
:value="item?.fund ? item?.fund?.title : 'کیف پول طلایی'"
|
||||
:value="item?.fund ? item?.fund?.title : metalLabel.goldWallet()"
|
||||
/>
|
||||
<BasicInfoItem title="مقدار طلا" :value="`${item?.gold} گرم`" />
|
||||
<BasicInfoItem :title="metalLabel.goldAmount()" :value="`${item?.gold} گرم`" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,6 +162,7 @@ import { createCustomer, deleteCustomer, updateCustomer } from '@/api/customer'
|
||||
import type { Charge } from '@/model/wallet'
|
||||
import customers from '@/router/routes/customers'
|
||||
import { chargeWallet } from '@/api/wallet'
|
||||
import { metalLabel } from '@/utils/adminMetalLabels'
|
||||
|
||||
//loading
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col gap-4 p-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="category-page flex h-full min-h-0 flex-col gap-4 p-4">
|
||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
||||
<BaseButton :loading="syncing" @click="handleSync">
|
||||
همگامسازی از محصولات
|
||||
</BaseButton>
|
||||
@@ -9,47 +9,58 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="items"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="middle"
|
||||
class="min-h-0 flex-1"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'active'">
|
||||
<a-switch
|
||||
:checked="asBool(record.active)"
|
||||
checked-children="فعال"
|
||||
un-checked-children="مخفی"
|
||||
@change="(checked: boolean) => toggleField(record, 'active', checked)"
|
||||
/>
|
||||
<div
|
||||
ref="tableWrapRef"
|
||||
class="category-page__table min-h-0 flex-1 overflow-hidden"
|
||||
:class="{ 'category-page__table--dragging': dragFromId !== null }"
|
||||
>
|
||||
<a-spin :spinning="loading" class="category-page__spin">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="items"
|
||||
:pagination="false"
|
||||
:scroll="tableScrollY ? { y: tableScrollY } : undefined"
|
||||
row-key="id"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'drag'">
|
||||
<span
|
||||
class="category-page__drag-handle"
|
||||
title="کشیدن برای تغییر ترتیب"
|
||||
@pointerdown="onHandlePointerDown(record.id, $event)"
|
||||
>
|
||||
<BaseIcon icon="vuesax-linear:menu" size="18" />
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'active'">
|
||||
<a-switch
|
||||
:checked="asBool(record.active)"
|
||||
checked-children="فعال"
|
||||
un-checked-children="مخفی"
|
||||
@change="(checked: boolean) => toggleField(record, 'active', checked)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'show_in_select'">
|
||||
<a-switch
|
||||
:checked="asBool(record.show_in_select)"
|
||||
@change="(checked: boolean) => toggleField(record, 'show_in_select', checked)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'sort_order'">
|
||||
<span class="text-gray-400">{{ record.sort_order }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'show_in_select'">
|
||||
<a-switch
|
||||
:checked="asBool(record.show_in_select)"
|
||||
@change="(checked: boolean) => toggleField(record, 'show_in_select', checked)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'sort_order'">
|
||||
<BasicInput
|
||||
class="!w-16"
|
||||
type="number"
|
||||
v-model:value="record.sort_order"
|
||||
@onchange="() => scheduleSortOrder(record)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-spin>
|
||||
</a-table>
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { TableColumnType } from 'ant-design-vue'
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import { debounce } from 'lodash'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { getCategories, syncCategoriesFromProducts, updateCategory } from '@/api/category'
|
||||
@@ -57,7 +68,17 @@ import type { Category } from '@/model/category'
|
||||
|
||||
const loading = ref(false)
|
||||
const syncing = ref(false)
|
||||
const savingOrder = ref(false)
|
||||
const items = ref<Category[]>([])
|
||||
const tableWrapRef = ref<HTMLElement | null>(null)
|
||||
const tableScrollY = ref<number>()
|
||||
const dragFromId = ref<number | null>(null)
|
||||
let highlightedRow: HTMLTableRowElement | null = null
|
||||
let draggingRow: HTMLTableRowElement | null = null
|
||||
let pointerDragActive = false
|
||||
let dragOverId: number | null = null
|
||||
let activePointerId: number | null = null
|
||||
let saveOrderGeneration = 0
|
||||
|
||||
type CategoryField = 'active' | 'show_in_select' | 'sort_order'
|
||||
|
||||
@@ -66,32 +87,216 @@ const debouncedFlush = new Map<number, ReturnType<typeof debounce>>()
|
||||
let syncInFlight: Promise<void> | null = null
|
||||
|
||||
const columns: TableColumnType<Category>[] = [
|
||||
{
|
||||
title: '',
|
||||
key: 'drag',
|
||||
width: 44,
|
||||
align: 'center',
|
||||
},
|
||||
{ title: 'نام دسته', dataIndex: 'name', key: 'name' },
|
||||
{
|
||||
title: 'نمایش محصولات در اپ',
|
||||
key: 'active',
|
||||
width: 160,
|
||||
align: 'center'
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'سلکت افزودن محصول',
|
||||
key: 'show_in_select',
|
||||
width: 150,
|
||||
align: 'center'
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'ترتیب',
|
||||
key: 'sort_order',
|
||||
width: 90,
|
||||
align: 'center'
|
||||
}
|
||||
width: 72,
|
||||
align: 'center',
|
||||
},
|
||||
]
|
||||
|
||||
function updateTableScrollY() {
|
||||
const height = tableWrapRef.value?.clientHeight ?? 0
|
||||
tableScrollY.value = height > 120 ? height - 56 : undefined
|
||||
}
|
||||
|
||||
useResizeObserver(tableWrapRef, () => {
|
||||
updateTableScrollY()
|
||||
})
|
||||
|
||||
/** پیشفرض: روشن — فقط صفر یا false یعنی خاموش */
|
||||
function asBool(value: boolean | number | undefined | null) {
|
||||
return value !== false && value !== 0
|
||||
}
|
||||
|
||||
function getTableBodyEl(): HTMLElement | null {
|
||||
return tableWrapRef.value?.querySelector('.ant-table-body') as HTMLElement | null
|
||||
}
|
||||
|
||||
function parseRowId(row: HTMLTableRowElement): number | null {
|
||||
const key = row.getAttribute('data-row-key')
|
||||
if (!key) return null
|
||||
const id = Number(key)
|
||||
return Number.isFinite(id) ? id : null
|
||||
}
|
||||
|
||||
function getRowFromPoint(clientX: number, clientY: number): HTMLTableRowElement | null {
|
||||
const wrap = tableWrapRef.value
|
||||
if (!wrap) return null
|
||||
|
||||
if (typeof document.elementsFromPoint === 'function') {
|
||||
for (const element of document.elementsFromPoint(clientX, clientY)) {
|
||||
if (!(element instanceof Element) || !wrap.contains(element)) continue
|
||||
const row = element.closest('tr.ant-table-row')
|
||||
if (row instanceof HTMLTableRowElement) return row
|
||||
}
|
||||
}
|
||||
|
||||
const rows = wrap.querySelectorAll('tr.ant-table-row')
|
||||
for (const row of rows) {
|
||||
const rect = row.getBoundingClientRect()
|
||||
if (
|
||||
clientY >= rect.top &&
|
||||
clientY <= rect.bottom &&
|
||||
clientX >= rect.left &&
|
||||
clientX <= rect.right
|
||||
) {
|
||||
return row as HTMLTableRowElement
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function setDragHighlight(row: HTMLTableRowElement | null) {
|
||||
if (highlightedRow && highlightedRow !== row) {
|
||||
highlightedRow.classList.remove('category-page__row--over')
|
||||
}
|
||||
highlightedRow = row
|
||||
row?.classList.add('category-page__row--over')
|
||||
}
|
||||
|
||||
function autoScrollAtPointer(clientY: number) {
|
||||
const body = getTableBodyEl()
|
||||
if (!body) return
|
||||
|
||||
const rect = body.getBoundingClientRect()
|
||||
const edge = 56
|
||||
if (clientY < rect.top + edge) {
|
||||
body.scrollTop -= Math.max(8, (rect.top + edge - clientY) / 3)
|
||||
} else if (clientY > rect.bottom - edge) {
|
||||
body.scrollTop += Math.max(8, (clientY - (rect.bottom - edge)) / 3)
|
||||
}
|
||||
}
|
||||
|
||||
function onHandlePointerDown(id: number, event: PointerEvent) {
|
||||
if (event.button !== 0) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const handle = event.currentTarget as HTMLElement
|
||||
handle.setPointerCapture(event.pointerId)
|
||||
|
||||
activePointerId = event.pointerId
|
||||
pointerDragActive = true
|
||||
dragFromId.value = id
|
||||
dragOverId = id
|
||||
|
||||
draggingRow = handle.closest('tr.ant-table-row')
|
||||
draggingRow?.classList.add('category-page__row--dragging')
|
||||
|
||||
window.addEventListener('pointermove', onPointerMove)
|
||||
window.addEventListener('pointerup', onPointerUp)
|
||||
window.addEventListener('pointercancel', onPointerUp)
|
||||
}
|
||||
|
||||
function onPointerMove(event: PointerEvent) {
|
||||
if (!pointerDragActive || activePointerId !== event.pointerId) return
|
||||
|
||||
event.preventDefault()
|
||||
autoScrollAtPointer(event.clientY)
|
||||
|
||||
const row = getRowFromPoint(event.clientX, event.clientY)
|
||||
setDragHighlight(row)
|
||||
dragOverId = row ? parseRowId(row) : null
|
||||
}
|
||||
|
||||
function onPointerUp(event: PointerEvent) {
|
||||
if (!pointerDragActive || activePointerId !== event.pointerId) return
|
||||
|
||||
const fromId = dragFromId.value
|
||||
const toId =
|
||||
dragOverId ?? (() => {
|
||||
const row = getRowFromPoint(event.clientX, event.clientY)
|
||||
return row ? parseRowId(row) : null
|
||||
})()
|
||||
|
||||
if (fromId !== null && toId !== null && fromId !== toId) {
|
||||
const fromIndex = items.value.findIndex((item) => item.id === fromId)
|
||||
const toIndex = items.value.findIndex((item) => item.id === toId)
|
||||
if (fromIndex >= 0 && toIndex >= 0) {
|
||||
void reorderByDrag(fromIndex, toIndex)
|
||||
}
|
||||
}
|
||||
|
||||
cleanupPointerDrag()
|
||||
}
|
||||
|
||||
function cleanupPointerDrag() {
|
||||
pointerDragActive = false
|
||||
activePointerId = null
|
||||
dragOverId = null
|
||||
dragFromId.value = null
|
||||
|
||||
draggingRow?.classList.remove('category-page__row--dragging')
|
||||
draggingRow = null
|
||||
setDragHighlight(null)
|
||||
|
||||
window.removeEventListener('pointermove', onPointerMove)
|
||||
window.removeEventListener('pointerup', onPointerUp)
|
||||
window.removeEventListener('pointercancel', onPointerUp)
|
||||
}
|
||||
|
||||
function applySortOrders(list: Category[]) {
|
||||
list.forEach((item, index) => {
|
||||
item.sort_order = index + 1
|
||||
})
|
||||
}
|
||||
|
||||
async function reorderByDrag(fromIndex: number, toIndex: number) {
|
||||
if (fromIndex === toIndex) return
|
||||
|
||||
const list = [...items.value]
|
||||
const [moved] = list.splice(fromIndex, 1)
|
||||
list.splice(toIndex, 0, moved)
|
||||
|
||||
const previousOrders = new Map(items.value.map((item) => [item.id, item.sort_order]))
|
||||
applySortOrders(list)
|
||||
items.value = list
|
||||
|
||||
const updates = list.filter((item) => previousOrders.get(item.id) !== item.sort_order)
|
||||
if (!updates.length) return
|
||||
|
||||
const generation = ++saveOrderGeneration
|
||||
savingOrder.value = true
|
||||
try {
|
||||
await Promise.all(
|
||||
updates.map((item) => updateCategory(item.id, { sort_order: item.sort_order }))
|
||||
)
|
||||
if (generation !== saveOrderGeneration) return
|
||||
message.success('ترتیب ذخیره شد')
|
||||
} catch (e) {
|
||||
if (generation !== saveOrderGeneration) return
|
||||
console.error(e)
|
||||
message.error('خطا در ذخیره ترتیب')
|
||||
await fetchList()
|
||||
} finally {
|
||||
if (generation === saveOrderGeneration) {
|
||||
savingOrder.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -101,6 +306,7 @@ async function fetchList() {
|
||||
message.error('خطا در دریافت دستهبندیها')
|
||||
} finally {
|
||||
loading.value = false
|
||||
updateTableScrollY()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +335,7 @@ async function flushSave(id: number) {
|
||||
const snapshot = {
|
||||
active: record.active,
|
||||
show_in_select: record.show_in_select,
|
||||
sort_order: record.sort_order
|
||||
sort_order: record.sort_order,
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -154,12 +360,6 @@ function toggleField(
|
||||
scheduleSave(record, { [field]: next })
|
||||
}
|
||||
|
||||
function scheduleSortOrder(record: Category) {
|
||||
const order = Number(record.sort_order)
|
||||
if (Number.isNaN(order)) return
|
||||
scheduleSave(record, { sort_order: order })
|
||||
}
|
||||
|
||||
async function syncList(showMessage = false) {
|
||||
if (syncInFlight) {
|
||||
return syncInFlight
|
||||
@@ -183,6 +383,7 @@ async function syncList(showMessage = false) {
|
||||
} finally {
|
||||
syncing.value = false
|
||||
syncInFlight = null
|
||||
updateTableScrollY()
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -194,12 +395,66 @@ async function handleSync() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void syncList()
|
||||
void fetchList()
|
||||
updateTableScrollY()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupPointerDrag()
|
||||
debouncedFlush.forEach((fn) => fn.cancel())
|
||||
debouncedFlush.clear()
|
||||
pendingPatches.clear()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.category-page__spin {
|
||||
display: block;
|
||||
height: 100%;
|
||||
|
||||
:deep(.ant-spin-container) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.category-page__drag-handle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: grab;
|
||||
color: #9ca3af;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
|
||||
&:hover {
|
||||
color: #6b7280;
|
||||
background: rgba(148, 163, 184, 0.16);
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.category-page__table--dragging {
|
||||
user-select: none;
|
||||
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.category-page__drag-handle) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.category-page__row--dragging > td) {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
:deep(.category-page__row--over > td) {
|
||||
background: rgba(59, 130, 246, 0.08) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user