fix some bugs

This commit is contained in:
2026-06-23 22:31:07 +03:30
parent 4e7d6bca00
commit 71764f7e9a
68 changed files with 1941 additions and 1288 deletions
@@ -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)
+304 -49
View File
@@ -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>