From 1c2647d093103bcdecea79eb90a9f7481f19998a Mon Sep 17 00:00:00 2001 From: safavimohadeseh78 Date: Sun, 17 May 2026 13:45:52 +0330 Subject: [PATCH] Insert image for product Co-authored-by: Cursor --- components.d.ts | 5 - src/api/file.ts | 27 ++ src/api/product.ts | 1 + .../ProductUploader/IndexImageUploader.vue | 32 ++- .../components/BaseTable/src/BaseTable.vue | 27 +- src/core/pages/login.vue | 2 +- src/core/utils/useAxios.ts | 13 +- src/model/File.ts | 5 +- src/model/product.ts | 4 + src/pages/menuPages/products/base/index.vue | 236 ++++++++++++------ src/utils/file.ts | 202 +++++++++++++++ 11 files changed, 457 insertions(+), 97 deletions(-) create mode 100644 src/api/file.ts create mode 100644 src/utils/file.ts diff --git a/components.d.ts b/components.d.ts index 4690207..eb6ea5c 100644 --- a/components.d.ts +++ b/components.d.ts @@ -15,7 +15,6 @@ declare module '@vue/runtime-core' { ABadge: typeof import('ant-design-vue/es')['Badge'] AButton: typeof import('ant-design-vue/es')['Button'] ACard: typeof import('ant-design-vue/es')['Card'] - ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] ADivider: typeof import('ant-design-vue/es')['Divider'] ADrawer: typeof import('ant-design-vue/es')['Drawer'] @@ -33,10 +32,7 @@ declare module '@vue/runtime-core' { AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] AModal: typeof import('ant-design-vue/es')['Modal'] APagination: typeof import('ant-design-vue/es')['Pagination'] - APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] - APopover: typeof import('ant-design-vue/es')['Popover'] ARadio: typeof import('ant-design-vue/es')['Radio'] - ARadioButton: typeof import('ant-design-vue/es')['RadioButton'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ASelect: typeof import('ant-design-vue/es')['Select'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] @@ -46,7 +42,6 @@ declare module '@vue/runtime-core' { ATable: typeof import('ant-design-vue/es')['Table'] ATabPane: typeof import('ant-design-vue/es')['TabPane'] ATabs: typeof import('ant-design-vue/es')['Tabs'] - ATextarea: typeof import('ant-design-vue/es')['Textarea'] ATooltip: typeof import('ant-design-vue/es')['Tooltip'] AUpload: typeof import('ant-design-vue/es')['Upload'] BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default'] diff --git a/src/api/file.ts b/src/api/file.ts new file mode 100644 index 0000000..57b67b2 --- /dev/null +++ b/src/api/file.ts @@ -0,0 +1,27 @@ +import useAxios from '@core/utils/useAxios' +import { pickImageFromRecord, resolveFileImageUrl, unwrapApiRecord } from '@/utils/file' + +export interface FileResource { + id?: number + src?: string + full_src?: string + path?: string + name?: string + title?: string + original_name?: string +} + +/** Load product image URL from product detail API (file GET endpoint does not exist on backend). */ +export async function fetchProductImageUrl(productId: number): Promise { + try { + const response = await useAxios.get(`/admin/product/${productId}`, { + meta: { silent: true } + }) + const record = unwrapApiRecord(response) + if (!record) return undefined + + return resolveFileImageUrl(pickImageFromRecord(record)) + } catch { + return undefined + } +} diff --git a/src/api/product.ts b/src/api/product.ts index c92f188..b093f09 100644 --- a/src/api/product.ts +++ b/src/api/product.ts @@ -40,6 +40,7 @@ export const getSingleArchiveTable = async (params: TableInput) => export const getCustomerById = async (id: number) => { return await useAxios.get(`/admin/product/${id}`) } + export const changeBuyActiveProduct = async (id?: number) => { return await useAxios.get(`/admin/product/open_buy/${id}`) } diff --git a/src/components/ProductUploader/IndexImageUploader.vue b/src/components/ProductUploader/IndexImageUploader.vue index 46ed57a..2454260 100644 --- a/src/components/ProductUploader/IndexImageUploader.vue +++ b/src/components/ProductUploader/IndexImageUploader.vue @@ -10,6 +10,7 @@ list-type="picture-card" :showUploadList="true" :multiple="false" + :max-count="1" :action="uploadURL" :capture="undefined" :headers="fileHeader" @@ -37,6 +38,7 @@ import { LoadingOutlined } from '@ant-design/icons-vue' import { usePrefix } from '@core/composable/usePrefix' import { uploadURL } from '@root/config' import { getToken } from '@/utils' +import { normalizeUploadFileResponse } from '@/utils/file' import { ref } from 'vue' const { prefixCls } = usePrefix('index-image-uploader') @@ -55,7 +57,11 @@ interface Props { const props = withDefaults(defineProps(), {}) -const emits = defineEmits(['update:fileList', 'ok', 'delete']) +const emits = defineEmits<{ + (e: 'update:fileList', value: UploadProps['fileList']): void + (e: 'ok', id: number, response?: Record): void + (e: 'delete', uid: string): void +}>() const fileList = computed({ get() { @@ -67,9 +73,10 @@ const fileList = computed({ }) const handlePreview = async (file: UploadProps['fileList'][number]) => { - previewImage.value = file.src + previewImage.value = file.url || file.thumbUrl || file.src previewVisible.value = true - previewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1) + previewTitle.value = + file.name || (file.url ? file.url.substring(file.url.lastIndexOf('/') + 1) : 'image') } const handleCancel = () => { @@ -99,8 +106,25 @@ function handleChange(info: UploadChangeParam) { if (info.file.status === 'done') { loading.value = false + const fileResource = normalizeUploadFileResponse(info.file.response) + + if (!fileResource?.id || !fileResource.full_src) { + fileList.value = [] + message.error('خطا در ساخت آدرس تصویر') + return + } + + fileList.value = [ + { + ...info.file, + uid: String(fileResource.id), + status: 'done', + url: fileResource.full_src, + thumbUrl: fileResource.full_src + } + ] message.success('تصویر شاخص با موفقیت بارگذاری شد') - emits('ok', info.file.response.id) + emits('ok', fileResource.id, info.file.response) } if (info.file.status === 'removed') { emits('delete', info.file.uid) diff --git a/src/core/components/BaseTable/src/BaseTable.vue b/src/core/components/BaseTable/src/BaseTable.vue index 38a3129..fc09059 100644 --- a/src/core/components/BaseTable/src/BaseTable.vue +++ b/src/core/components/BaseTable/src/BaseTable.vue @@ -285,7 +285,7 @@ import BaseDatePicker from '@core/components/BaseDatePicker' import { cloneDeep, get, isArray, omit } from 'lodash' import { LoadingOutlined } from '@ant-design/icons-vue' //import { filterOperatorList } from '../hooks/filterOperator' -import { ref } from 'vue' +import { nextTick, ref } from 'vue' import BaseSwitch from '@core/components/BaseSwitch/src/BaseSwitch.vue' import { mapColumnTypeFilterToFilterType } from '@core/type/baseTable' import { @@ -350,6 +350,8 @@ const orderColumnKey = ref(null) const containerRef = ref() //const route = useRoute() const loading = ref(false) +const tableReady = ref(false) +let callApiPromise: Promise | null = null const _props = computed(() => { return omit(props, ['columns', 'pagination', 'dataSource']) }) @@ -594,9 +596,9 @@ function handleResizeColumn(w: number, col: any) { } async function handleChangePageSize(value: number, option: any) { - if (tableInput.value) { - tableInput.value.limit = value - } + if (!tableReady.value || tableInput.value.limit === value) return + tableInput.value.limit = value + pageReset() await reload() } @@ -605,6 +607,7 @@ function pageReset() { } async function handleChangePage(page: number, pageSize: number) { + if (!tableReady.value || tableInput.value.page === page) return tableInput.value.page = page await callApi() } @@ -615,7 +618,11 @@ async function reload() { async function callApi() { const api = get(_props.value, 'api', null) - if (api != null) { + if (api == null) return + + if (callApiPromise) return callApiPromise + + callApiPromise = (async () => { try { loading.value = true if (orderColumnKey.value != null) { @@ -633,7 +640,6 @@ async function callApi() { tableInput.value = data.query tableOutput.value = data searchText.value = data.query.search ?? '' - //console.log('11110', get(data.query.filters ?? [], 'filters', [])) filters.value = cloneDeep(data.query.filters ?? []).filter((item) => { return Object.hasOwn(item, 'hidden') && item.hidden == false }) @@ -653,14 +659,19 @@ async function callApi() { console.log(e) } finally { loading.value = false + callApiPromise = null } - } + })() + + return callApiPromise } onMounted(async () => { + tableReady.value = false await callApi() emit('register', { reload }) - //console.log('router', route.query) + await nextTick() + tableReady.value = true }) defineExpose({ diff --git a/src/core/pages/login.vue b/src/core/pages/login.vue index d198522..48156f6 100644 --- a/src/core/pages/login.vue +++ b/src/core/pages/login.vue @@ -1,6 +1,6 @@