Insert image for product
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
-5
@@ -15,7 +15,6 @@ declare module '@vue/runtime-core' {
|
|||||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
AButton: typeof import('ant-design-vue/es')['Button']
|
||||||
ACard: typeof import('ant-design-vue/es')['Card']
|
ACard: typeof import('ant-design-vue/es')['Card']
|
||||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
|
||||||
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
||||||
ADivider: typeof import('ant-design-vue/es')['Divider']
|
ADivider: typeof import('ant-design-vue/es')['Divider']
|
||||||
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
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']
|
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
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']
|
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
|
||||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
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']
|
ATable: typeof import('ant-design-vue/es')['Table']
|
||||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||||
BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default']
|
BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default']
|
||||||
|
|||||||
@@ -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<string | undefined> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ export const getSingleArchiveTable = async (params: TableInput) =>
|
|||||||
export const getCustomerById = async (id: number) => {
|
export const getCustomerById = async (id: number) => {
|
||||||
return await useAxios.get(`/admin/product/${id}`)
|
return await useAxios.get(`/admin/product/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const changeBuyActiveProduct = async (id?: number) => {
|
export const changeBuyActiveProduct = async (id?: number) => {
|
||||||
return await useAxios.get(`/admin/product/open_buy/${id}`)
|
return await useAxios.get(`/admin/product/open_buy/${id}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
list-type="picture-card"
|
list-type="picture-card"
|
||||||
:showUploadList="true"
|
:showUploadList="true"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
|
:max-count="1"
|
||||||
:action="uploadURL"
|
:action="uploadURL"
|
||||||
:capture="undefined"
|
:capture="undefined"
|
||||||
:headers="fileHeader"
|
:headers="fileHeader"
|
||||||
@@ -37,6 +38,7 @@ import { LoadingOutlined } from '@ant-design/icons-vue'
|
|||||||
import { usePrefix } from '@core/composable/usePrefix'
|
import { usePrefix } from '@core/composable/usePrefix'
|
||||||
import { uploadURL } from '@root/config'
|
import { uploadURL } from '@root/config'
|
||||||
import { getToken } from '@/utils'
|
import { getToken } from '@/utils'
|
||||||
|
import { normalizeUploadFileResponse } from '@/utils/file'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const { prefixCls } = usePrefix('index-image-uploader')
|
const { prefixCls } = usePrefix('index-image-uploader')
|
||||||
@@ -55,7 +57,11 @@ interface Props {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {})
|
const props = withDefaults(defineProps<Props>(), {})
|
||||||
|
|
||||||
const emits = defineEmits(['update:fileList', 'ok', 'delete'])
|
const emits = defineEmits<{
|
||||||
|
(e: 'update:fileList', value: UploadProps['fileList']): void
|
||||||
|
(e: 'ok', id: number, response?: Record<string, any>): void
|
||||||
|
(e: 'delete', uid: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const fileList = computed({
|
const fileList = computed({
|
||||||
get() {
|
get() {
|
||||||
@@ -67,9 +73,10 @@ const fileList = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handlePreview = async (file: UploadProps['fileList'][number]) => {
|
const handlePreview = async (file: UploadProps['fileList'][number]) => {
|
||||||
previewImage.value = file.src
|
previewImage.value = file.url || file.thumbUrl || file.src
|
||||||
previewVisible.value = true
|
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 = () => {
|
const handleCancel = () => {
|
||||||
@@ -99,8 +106,25 @@ function handleChange(info: UploadChangeParam) {
|
|||||||
|
|
||||||
if (info.file.status === 'done') {
|
if (info.file.status === 'done') {
|
||||||
loading.value = false
|
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('تصویر شاخص با موفقیت بارگذاری شد')
|
message.success('تصویر شاخص با موفقیت بارگذاری شد')
|
||||||
emits('ok', info.file.response.id)
|
emits('ok', fileResource.id, info.file.response)
|
||||||
}
|
}
|
||||||
if (info.file.status === 'removed') {
|
if (info.file.status === 'removed') {
|
||||||
emits('delete', info.file.uid)
|
emits('delete', info.file.uid)
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ import BaseDatePicker from '@core/components/BaseDatePicker'
|
|||||||
import { cloneDeep, get, isArray, omit } from 'lodash'
|
import { cloneDeep, get, isArray, omit } from 'lodash'
|
||||||
import { LoadingOutlined } from '@ant-design/icons-vue'
|
import { LoadingOutlined } from '@ant-design/icons-vue'
|
||||||
//import { filterOperatorList } from '../hooks/filterOperator'
|
//import { filterOperatorList } from '../hooks/filterOperator'
|
||||||
import { ref } from 'vue'
|
import { nextTick, ref } from 'vue'
|
||||||
import BaseSwitch from '@core/components/BaseSwitch/src/BaseSwitch.vue'
|
import BaseSwitch from '@core/components/BaseSwitch/src/BaseSwitch.vue'
|
||||||
import { mapColumnTypeFilterToFilterType } from '@core/type/baseTable'
|
import { mapColumnTypeFilterToFilterType } from '@core/type/baseTable'
|
||||||
import {
|
import {
|
||||||
@@ -350,6 +350,8 @@ const orderColumnKey = ref<string | null>(null)
|
|||||||
const containerRef = ref()
|
const containerRef = ref()
|
||||||
//const route = useRoute()
|
//const route = useRoute()
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
|
const tableReady = ref(false)
|
||||||
|
let callApiPromise: Promise<void> | null = null
|
||||||
const _props = computed(() => {
|
const _props = computed(() => {
|
||||||
return omit(props, ['columns', 'pagination', 'dataSource'])
|
return omit(props, ['columns', 'pagination', 'dataSource'])
|
||||||
})
|
})
|
||||||
@@ -594,9 +596,9 @@ function handleResizeColumn(w: number, col: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleChangePageSize(value: number, option: any) {
|
async function handleChangePageSize(value: number, option: any) {
|
||||||
if (tableInput.value) {
|
if (!tableReady.value || tableInput.value.limit === value) return
|
||||||
tableInput.value.limit = value
|
tableInput.value.limit = value
|
||||||
}
|
pageReset()
|
||||||
await reload()
|
await reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,6 +607,7 @@ function pageReset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleChangePage(page: number, pageSize: number) {
|
async function handleChangePage(page: number, pageSize: number) {
|
||||||
|
if (!tableReady.value || tableInput.value.page === page) return
|
||||||
tableInput.value.page = page
|
tableInput.value.page = page
|
||||||
await callApi()
|
await callApi()
|
||||||
}
|
}
|
||||||
@@ -615,7 +618,11 @@ async function reload() {
|
|||||||
|
|
||||||
async function callApi() {
|
async function callApi() {
|
||||||
const api = get(_props.value, 'api', null)
|
const api = get(_props.value, 'api', null)
|
||||||
if (api != null) {
|
if (api == null) return
|
||||||
|
|
||||||
|
if (callApiPromise) return callApiPromise
|
||||||
|
|
||||||
|
callApiPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
if (orderColumnKey.value != null) {
|
if (orderColumnKey.value != null) {
|
||||||
@@ -633,7 +640,6 @@ async function callApi() {
|
|||||||
tableInput.value = data.query
|
tableInput.value = data.query
|
||||||
tableOutput.value = data
|
tableOutput.value = data
|
||||||
searchText.value = data.query.search ?? ''
|
searchText.value = data.query.search ?? ''
|
||||||
//console.log('11110', get(data.query.filters ?? [], 'filters', []))
|
|
||||||
filters.value = cloneDeep(data.query.filters ?? []).filter((item) => {
|
filters.value = cloneDeep(data.query.filters ?? []).filter((item) => {
|
||||||
return Object.hasOwn(item, 'hidden') && item.hidden == false
|
return Object.hasOwn(item, 'hidden') && item.hidden == false
|
||||||
})
|
})
|
||||||
@@ -653,14 +659,19 @@ async function callApi() {
|
|||||||
console.log(e)
|
console.log(e)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
callApiPromise = null
|
||||||
}
|
}
|
||||||
}
|
})()
|
||||||
|
|
||||||
|
return callApiPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
tableReady.value = false
|
||||||
await callApi()
|
await callApi()
|
||||||
emit('register', { reload })
|
emit('register', { reload })
|
||||||
//console.log('router', route.query)
|
await nextTick()
|
||||||
|
tableReady.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useHead } from '@vueuse/head'
|
import { useHead } from '@vueuse/head'
|
||||||
import { useDarkmode } from '/@/core/stores/darkmode'
|
import { useDarkmode } from '@/core/stores/darkmode'
|
||||||
//import { useUserSession } from '/@src/stores/userSession'
|
//import { useUserSession } from '/@src/stores/userSession'
|
||||||
//import { useNotyf } from '/@src/core/composable/useNotyf'
|
//import { useNotyf } from '/@src/core/composable/useNotyf'
|
||||||
//import sleep from '/@src/utils/sleep'
|
//import sleep from '/@src/utils/sleep'
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { message } from 'ant-design-vue'
|
|||||||
|
|
||||||
interface Meta {
|
interface Meta {
|
||||||
errorMessage?: 'message' | 'notification' | 'toast'
|
errorMessage?: 'message' | 'notification' | 'toast'
|
||||||
|
silent?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MyAxiosRequestConfig<D = any> extends AxiosRequestConfig<D> {
|
interface MyAxiosRequestConfig<D = any> extends AxiosRequestConfig<D> {
|
||||||
@@ -72,11 +73,17 @@ _axios.interceptors.response.use(
|
|||||||
// router.push({ name: 'login' })
|
// router.push({ name: 'login' })
|
||||||
await router.push({ name: 'login' })
|
await router.push({ name: 'login' })
|
||||||
} else if (error.response && error.response.status == 403) {
|
} else if (error.response && error.response.status == 403) {
|
||||||
message.error('شما دسترسی لازم برای انجام این عملیات را ندارید')
|
if (!error.config?.meta?.silent) {
|
||||||
|
message.error('شما دسترسی لازم برای انجام این عملیات را ندارید')
|
||||||
|
}
|
||||||
} else if (error.response && error.response.status == 422) {
|
} else if (error.response && error.response.status == 422) {
|
||||||
message.error(error.response.data?.message)
|
if (!error.config?.meta?.silent) {
|
||||||
|
message.error(error.response.data?.message)
|
||||||
|
}
|
||||||
} else if (error.response) {
|
} else if (error.response) {
|
||||||
message.error('خطا در بارگذاری اطلاعات')
|
if (!error.config?.meta?.silent) {
|
||||||
|
message.error('خطا در بارگذاری اطلاعات')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Any status codes that falls outside the range of 2xx cause this function to trigger
|
// Any status codes that falls outside the range of 2xx cause this function to trigger
|
||||||
// Do something with response error
|
// Do something with response error
|
||||||
|
|||||||
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
export interface File {
|
export interface File {
|
||||||
created_at?: string
|
created_at?: string
|
||||||
|
document_id?: number | null
|
||||||
ext?: string
|
ext?: string
|
||||||
has_thumb?: boolean
|
has_thumb?: boolean
|
||||||
id?: number
|
id?: number
|
||||||
@@ -7,7 +8,7 @@ export interface File {
|
|||||||
size?: number
|
size?: number
|
||||||
src?: string
|
src?: string
|
||||||
title?: string
|
title?: string
|
||||||
type?: string
|
type?: string | null
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
user_id?: number
|
user_id?: number | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { File } from '@/model/File'
|
||||||
import type { User } from '@/model/user'
|
import type { User } from '@/model/user'
|
||||||
import type { Wallet } from '@/model/wallet'
|
import type { Wallet } from '@/model/wallet'
|
||||||
|
|
||||||
@@ -30,6 +31,9 @@ export interface Product{
|
|||||||
auto_open_at: string
|
auto_open_at: string
|
||||||
metal_id: number
|
metal_id: number
|
||||||
auto_close_at: string
|
auto_close_at: string
|
||||||
|
file_id?: number
|
||||||
|
file?: File
|
||||||
|
original_image?: File
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MethodePrices {
|
export interface MethodePrices {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
:title="`${editId ? 'ویرایش' : 'افزودن'} محصول`"
|
:title="`${editId ? 'ویرایش' : 'افزودن'} محصول`"
|
||||||
@ok="ok"
|
@ok="ok"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@close="addUserVisible = false"
|
@close="closeProductModal"
|
||||||
>
|
>
|
||||||
<Collapse class="grid grid-cols-1 gap-2" v-model:activeKey="activeKey_">
|
<Collapse class="grid grid-cols-1 gap-2" v-model:activeKey="activeKey_">
|
||||||
<BaseCollapse title="اطلاعات محصول" active-key="0" :actives="activeKey_">
|
<BaseCollapse title="اطلاعات محصول" active-key="0" :actives="activeKey_">
|
||||||
@@ -228,18 +228,12 @@
|
|||||||
</BaseCollapse>
|
</BaseCollapse>
|
||||||
<BaseCollapse title="تصاویر محصول" active-key="1" :actives="activeKey_">
|
<BaseCollapse title="تصاویر محصول" active-key="1" :actives="activeKey_">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex gap-2">
|
<IndexImageUploader
|
||||||
<IndexImageUploader
|
:key="editId ?? 'new-product'"
|
||||||
v-model:file-list="ProductIndexImage"
|
v-model:file-list="ProductIndexImage"
|
||||||
@ok="uploadedIndexFile"
|
@ok="uploadedIndexFile"
|
||||||
@delete="deleteIndexFile"
|
@delete="deleteIndexFile"
|
||||||
/>
|
/>
|
||||||
<ProductImageUploader
|
|
||||||
v-model:file-list="ImageFileList"
|
|
||||||
@ok="uploadedFile"
|
|
||||||
@delete="deleteFile"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</BaseCollapse>
|
</BaseCollapse>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
@@ -431,7 +425,17 @@ import {confirmDeleteItem} from "@/utils/confirmModal";
|
|||||||
import {cloneDeep} from "lodash";
|
import {cloneDeep} from "lodash";
|
||||||
import type {Item} from "@/model/Item";
|
import type {Item} from "@/model/Item";
|
||||||
import type {Customer} from "@/model/customer";
|
import type {Customer} from "@/model/customer";
|
||||||
import {createProduct, deleteCustomer, updateCustomer} from "@/api/product";
|
import {createProduct, deleteCustomer, getCustomerById, updateCustomer} from "@/api/product";
|
||||||
|
import {fetchProductImageUrl} from "@/api/file";
|
||||||
|
import {
|
||||||
|
applyFileToUploadList,
|
||||||
|
normalizeFileResource,
|
||||||
|
normalizeUploadFileResponse,
|
||||||
|
pickImageFromRecord,
|
||||||
|
pickRawFileFromRecord,
|
||||||
|
resolveProductImageSrc,
|
||||||
|
unwrapApiRecord
|
||||||
|
} from "@/utils/file";
|
||||||
import type {Charge} from "@/model/wallet";
|
import type {Charge} from "@/model/wallet";
|
||||||
import customers from "@/router/routes/products";
|
import customers from "@/router/routes/products";
|
||||||
import {chargeWallet} from "@/api/wallet";
|
import {chargeWallet} from "@/api/wallet";
|
||||||
@@ -446,7 +450,6 @@ import type {Order} from "@/model/order";
|
|||||||
import Icon from "@core/components/BaseIcon/src/Icon.vue";
|
import Icon from "@core/components/BaseIcon/src/Icon.vue";
|
||||||
import type {MethodePrices, Product} from "@/model/product";
|
import type {MethodePrices, Product} from "@/model/product";
|
||||||
import IndexImageUploader from "@/components/ProductUploader/IndexImageUploader.vue";
|
import IndexImageUploader from "@/components/ProductUploader/IndexImageUploader.vue";
|
||||||
import ProductImageUploader from "@/components/ProductUploader/ProductImageUploader.vue";
|
|
||||||
|
|
||||||
//tabs
|
//tabs
|
||||||
const activeKey = ref("اطلاعات جامع");
|
const activeKey = ref("اطلاعات جامع");
|
||||||
@@ -474,19 +477,6 @@ const methodOption = ref<string>("1");
|
|||||||
const weightOption = ref<string>("1");
|
const weightOption = ref<string>("1");
|
||||||
|
|
||||||
const activeKey_ = ref(["0"]);
|
const activeKey_ = ref(["0"]);
|
||||||
const ImageFileList = ref<Array<FileList>>([]);
|
|
||||||
|
|
||||||
function uploadedFile(id: number) {
|
|
||||||
// console.log(id)
|
|
||||||
let arr: number[] | undefined = form.value.image_ids;
|
|
||||||
arr?.push(id);
|
|
||||||
form.value.image_ids = arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteFile() {
|
|
||||||
form.value.image_ids = ImageFileList?.value.map((x: FileList) => x.uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
import BaseCollapse from "@/pages/menuPages/products/base/components/BaseCollapse.vue";
|
import BaseCollapse from "@/pages/menuPages/products/base/components/BaseCollapse.vue";
|
||||||
import {methodePrices} from "@/api/price";
|
import {methodePrices} from "@/api/price";
|
||||||
import {getAllPrices} from "@/api/dashboard";
|
import {getAllPrices} from "@/api/dashboard";
|
||||||
@@ -494,17 +484,67 @@ import dayjs from "dayjs";
|
|||||||
|
|
||||||
const ProductIndexImage = ref<Array<FileList>>([]);
|
const ProductIndexImage = ref<Array<FileList>>([]);
|
||||||
|
|
||||||
function uploadedIndexFile(id: number) {
|
function uploadedIndexFile(id: number, response?: Record<string, any>) {
|
||||||
// console.log(id)
|
const fileRecord = unwrapApiRecord(response)
|
||||||
form.value.original_image_id = id;
|
const fileResource =
|
||||||
|
normalizeUploadFileResponse(response) ||
|
||||||
|
normalizeUploadFileResponse({ id, src: response?.src })
|
||||||
|
|
||||||
|
if (!fileRecord && !fileResource) {
|
||||||
|
form.value.file_id = id
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.value.file_id = fileRecord?.id ?? fileResource?.id ?? id
|
||||||
|
if (fileRecord) form.value.file = fileRecord
|
||||||
|
ProductIndexImage.value = applyFileToUploadList(form.value.file_id, fileResource) as Array<FileList>
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteIndexFile() {
|
function deleteIndexFile() {
|
||||||
form.value.original_image_id = null;
|
form.value.file_id = undefined;
|
||||||
form.value.original_image = null;
|
form.value.file = undefined;
|
||||||
ProductIndexImage.value = [];
|
ProductIndexImage.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setProductIndexImage(record: Record<string, any>, productId?: number) {
|
||||||
|
ProductIndexImage.value = [];
|
||||||
|
|
||||||
|
const rawFile = pickRawFileFromRecord(record);
|
||||||
|
const fileId = record.file_id ?? record.original_image_id ?? rawFile?.id;
|
||||||
|
|
||||||
|
if (!fileId && !rawFile) return;
|
||||||
|
|
||||||
|
if (fileId) form.value.file_id = Number(fileId);
|
||||||
|
if (rawFile) form.value.file = rawFile;
|
||||||
|
|
||||||
|
const image =
|
||||||
|
pickImageFromRecord(record) ||
|
||||||
|
(rawFile ? normalizeFileResource(rawFile) : null);
|
||||||
|
|
||||||
|
let uploadList = applyFileToUploadList(form.value.file_id ?? fileId ?? rawFile?.id, image);
|
||||||
|
|
||||||
|
if (!uploadList.length) {
|
||||||
|
let imageSrc = resolveProductImageSrc(record);
|
||||||
|
if (!imageSrc && productId) {
|
||||||
|
imageSrc = await fetchProductImageUrl(productId);
|
||||||
|
}
|
||||||
|
if (imageSrc) {
|
||||||
|
uploadList = [
|
||||||
|
{
|
||||||
|
uid: String(fileId ?? rawFile?.id),
|
||||||
|
name: rawFile?.original_name || rawFile?.title || image?.name || "image",
|
||||||
|
status: "done",
|
||||||
|
url: imageSrc,
|
||||||
|
thumbUrl: imageSrc
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
ProductIndexImage.value = uploadList as Array<FileList>;
|
||||||
|
}
|
||||||
|
|
||||||
let updateKey = ref<number>(0);
|
let updateKey = ref<number>(0);
|
||||||
|
|
||||||
|
|
||||||
@@ -647,16 +687,40 @@ let editId: number | undefined = undefined;
|
|||||||
async function edit(record: Customer) {
|
async function edit(record: Customer) {
|
||||||
addUserVisible.value = true;
|
addUserVisible.value = true;
|
||||||
editId = record.id;
|
editId = record.id;
|
||||||
console.log(record);
|
let productData = cloneDeep(record) as Record<string, any>;
|
||||||
const customForm = cloneDeep(record);
|
|
||||||
const start = dayjs(customForm.auto_open_at, "HH:mm:ss");
|
if (!pickRawFileFromRecord(productData) && productData.file_id) {
|
||||||
const close = dayjs(customForm.auto_close_at, "HH:mm:ss");
|
try {
|
||||||
delete customForm.auto_open_at;
|
const detail = unwrapApiRecord(await getCustomerById(record.id));
|
||||||
delete customForm.auto_close_at;
|
if (detail) productData = { ...productData, ...detail };
|
||||||
form.value.auto_open_at = start;
|
} catch {
|
||||||
form.value.auto_close_at = close;
|
/* use table row data */
|
||||||
console.log(form.value);
|
}
|
||||||
form.value = cloneDeep(customForm);
|
}
|
||||||
|
|
||||||
|
methodOption.value = productData.reading_method ? "2" : "1";
|
||||||
|
weightOption.value = productData.amount && productData.amount !== 1 ? "1" : "2";
|
||||||
|
|
||||||
|
const start = productData.auto_open_at
|
||||||
|
? dayjs(productData.auto_open_at, "HH:mm:ss")
|
||||||
|
: dayjs("00:00:00", "HH:mm:ss");
|
||||||
|
const close = productData.auto_close_at
|
||||||
|
? dayjs(productData.auto_close_at, "HH:mm:ss")
|
||||||
|
: dayjs("00:00:00", "HH:mm:ss");
|
||||||
|
|
||||||
|
form.value = {
|
||||||
|
...productData,
|
||||||
|
direct_price: productData.direct_price ?? 0,
|
||||||
|
auto_open_at: start.isValid() ? start : dayjs("00:00:00", "HH:mm:ss"),
|
||||||
|
auto_close_at: close.isValid() ? close : dayjs("00:00:00", "HH:mm:ss")
|
||||||
|
};
|
||||||
|
|
||||||
|
await setProductIndexImage(productData, record.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeProductModal() {
|
||||||
|
addUserVisible.value = false;
|
||||||
|
editId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
//add
|
//add
|
||||||
@@ -715,6 +779,9 @@ async function chargeGoldWallet() {
|
|||||||
//reset data
|
//reset data
|
||||||
const resetData = () => {
|
const resetData = () => {
|
||||||
formRef.value?.resetFields();
|
formRef.value?.resetFields();
|
||||||
|
ProductIndexImage.value = [];
|
||||||
|
methodOption.value = "1";
|
||||||
|
weightOption.value = "1";
|
||||||
form.value = {
|
form.value = {
|
||||||
unit: "",
|
unit: "",
|
||||||
group_type: "",
|
group_type: "",
|
||||||
@@ -723,7 +790,12 @@ const resetData = () => {
|
|||||||
cash_fee_buy: 0,
|
cash_fee_buy: 0,
|
||||||
percent_fee_sell: 0,
|
percent_fee_sell: 0,
|
||||||
cash_fee_sell: 0,
|
cash_fee_sell: 0,
|
||||||
metal_id: 1
|
metal_id: 1,
|
||||||
|
direct_price: 0,
|
||||||
|
file_id: undefined,
|
||||||
|
file: undefined,
|
||||||
|
auto_open_at: dayjs("00:00:00", "HH:mm:ss"),
|
||||||
|
auto_close_at: dayjs("00:00:00", "HH:mm:ss")
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -746,37 +818,53 @@ const userInfo = ref<Customer>();
|
|||||||
|
|
||||||
//submit user
|
//submit user
|
||||||
async function ok() {
|
async function ok() {
|
||||||
formRef.value?.validateFields().then(async () => {
|
try {
|
||||||
// console.log('salam', form.value)
|
await formRef.value?.validateFields();
|
||||||
// return
|
} catch {
|
||||||
loading.value = true;
|
return;
|
||||||
try {
|
}
|
||||||
if (methodOption.value === "1") delete form.value.reading_method;
|
|
||||||
else delete form.value.price;
|
loading.value = true;
|
||||||
if (weightOption.value === "2")
|
try {
|
||||||
form.value.amount = 1;
|
const payload = cloneDeep(form.value);
|
||||||
form.value.auto_open_at = dayjs(form.value.auto_open_at).format("HH:mm:ss");
|
if (methodOption.value === "1") delete payload.reading_method;
|
||||||
form.value.auto_close_at = dayjs(form.value.auto_close_at).format("HH:mm:ss");
|
else delete payload.price;
|
||||||
await methodePrices(form.value);
|
if (weightOption.value === "2") payload.amount = 1;
|
||||||
if (editId) {
|
|
||||||
await updateCustomer(editId, form.value);
|
payload.auto_open_at = payload.auto_open_at
|
||||||
} else {
|
? dayjs(payload.auto_open_at).format("HH:mm:ss")
|
||||||
await createProduct(form.value);
|
: "00:00:00";
|
||||||
}
|
payload.auto_close_at = payload.auto_close_at
|
||||||
addUserVisible.value = false;
|
? dayjs(payload.auto_close_at).format("HH:mm:ss")
|
||||||
refreshTable();
|
: "00:00:00";
|
||||||
resetData();
|
|
||||||
message.success("عملیات با موفقیت انجام شد");
|
delete payload.original_image;
|
||||||
// await getResponsible()
|
delete payload.original_image_id;
|
||||||
// await getQC()
|
delete payload.image_ids;
|
||||||
// await getManager()
|
|
||||||
} catch (e) {
|
if (payload.file_id && payload.file) {
|
||||||
console.error(e);
|
payload.file_id = payload.file.id ?? payload.file_id
|
||||||
// message.error('خطا در بارگذاری اطلاعات')
|
} else if (!payload.file_id) {
|
||||||
} finally {
|
delete payload.file
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
if (editId) {
|
||||||
|
await updateCustomer(editId, payload);
|
||||||
|
} else {
|
||||||
|
await createProduct(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
addUserVisible.value = false;
|
||||||
|
editId = undefined;
|
||||||
|
refreshTable();
|
||||||
|
resetData();
|
||||||
|
message.success("عملیات با موفقیت انجام شد");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
message.error("خطا در ثبت اطلاعات");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { baseURL, baseURLStorage } from '@root/config'
|
||||||
|
import { getConfigApp } from '@/utils/config'
|
||||||
|
import type { FileResource } from '@/api/file'
|
||||||
|
|
||||||
|
export function unwrapApiRecord(payload: unknown): Record<string, any> | null {
|
||||||
|
if (!payload || typeof payload !== 'object') return null
|
||||||
|
|
||||||
|
const data = payload as Record<string, unknown>
|
||||||
|
|
||||||
|
if (data.data && typeof data.data === 'object') {
|
||||||
|
return unwrapApiRecord(data.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.product && typeof data.product === 'object') {
|
||||||
|
return unwrapApiRecord(data.product)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.row && typeof data.row === 'object') {
|
||||||
|
return unwrapApiRecord(data.row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unwrapFileResource(payload: unknown): FileResource | null {
|
||||||
|
if (!payload || typeof payload !== 'object') return null
|
||||||
|
|
||||||
|
const data = payload as Record<string, unknown>
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof data.full_src === 'string' ||
|
||||||
|
typeof data.src === 'string' ||
|
||||||
|
typeof data.path === 'string' ||
|
||||||
|
typeof data.url === 'string'
|
||||||
|
) {
|
||||||
|
return normalizeFileResource(data as Record<string, any>)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.data && typeof data.data === 'object') {
|
||||||
|
return unwrapFileResource(data.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.file && typeof data.file === 'object') {
|
||||||
|
return unwrapFileResource(data.file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Site domain + API base, e.g. https://example.ir/server */
|
||||||
|
export function getFileBaseUrl(): string {
|
||||||
|
const fromConfig =
|
||||||
|
getConfigApp('node_server') ||
|
||||||
|
getConfigApp('storage_url') ||
|
||||||
|
getConfigApp('storage') ||
|
||||||
|
getConfigApp('file_url') ||
|
||||||
|
getConfigApp('file_base')
|
||||||
|
|
||||||
|
if (typeof fromConfig === 'string' && fromConfig.trim()) {
|
||||||
|
return fromConfig.replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageBase = baseURLStorage || baseURL
|
||||||
|
if (storageBase) {
|
||||||
|
return storageBase.replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return `${window.location.origin}/server`.replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns API paths like "storage/1405/02/27/file.jpg" into a full URL.
|
||||||
|
*/
|
||||||
|
export function toAbsoluteFileUrl(path?: string | null): string | undefined {
|
||||||
|
if (!path?.trim()) return undefined
|
||||||
|
|
||||||
|
const trimmed = path.trim()
|
||||||
|
|
||||||
|
if (/^https?:\/\//i.test(trimmed)) {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = getFileBaseUrl()
|
||||||
|
if (!base) return undefined
|
||||||
|
|
||||||
|
if (trimmed.startsWith('/')) {
|
||||||
|
return `${base}${trimmed}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${base}/${trimmed.replace(/^\//, '')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBareFileName(value: string): boolean {
|
||||||
|
return !value.includes('/') && /\.[a-z0-9]+$/i.test(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeFileResource(record: Record<string, any>): FileResource | null {
|
||||||
|
const src =
|
||||||
|
typeof record.src === 'string'
|
||||||
|
? record.src
|
||||||
|
: typeof record.path === 'string'
|
||||||
|
? record.path
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (!src && !record.full_src) return null
|
||||||
|
|
||||||
|
const absoluteUrl = toAbsoluteFileUrl(record.full_src || src)
|
||||||
|
if (!absoluteUrl) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: record.id != null ? Number(record.id) : undefined,
|
||||||
|
name: record.title || record.original_name || record.name,
|
||||||
|
src,
|
||||||
|
full_src: absoluteUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Normalize upload API response: { id, src: "storage/...", title, ... } */
|
||||||
|
export function normalizeUploadFileResponse(response: unknown): FileResource | null {
|
||||||
|
const record = unwrapApiRecord(response)
|
||||||
|
if (!record) return null
|
||||||
|
return normalizeFileResource(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFileImageUrl(file?: FileResource | null): string | undefined {
|
||||||
|
if (!file) return undefined
|
||||||
|
if (file.full_src) return file.full_src
|
||||||
|
return toAbsoluteFileUrl(file.src || file.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickRawFileFromRecord(record: Record<string, any>): Record<string, any> | null {
|
||||||
|
for (const key of ['file', 'original_image', 'image', 'index_image', 'product_image']) {
|
||||||
|
const value = record[key]
|
||||||
|
if (value && typeof value === 'object') return value as Record<string, any>
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveProductImageSrc(record: Record<string, any>): string | undefined {
|
||||||
|
const fromRelation = resolveFileImageUrl(pickImageFromRecord(record))
|
||||||
|
if (fromRelation) return fromRelation
|
||||||
|
|
||||||
|
const rawFile = pickRawFileFromRecord(record)
|
||||||
|
if (rawFile?.src) return toAbsoluteFileUrl(rawFile.src)
|
||||||
|
|
||||||
|
if (typeof record.file_src === 'string') return toAbsoluteFileUrl(record.file_src)
|
||||||
|
if (typeof record.image_src === 'string') return toAbsoluteFileUrl(record.image_src)
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickImageFromRecord(record: Record<string, any>): FileResource | null {
|
||||||
|
const keys = [
|
||||||
|
'file',
|
||||||
|
'original_image',
|
||||||
|
'image',
|
||||||
|
'index_image',
|
||||||
|
'product_image',
|
||||||
|
'image_file',
|
||||||
|
'uploaded_file'
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = record[key]
|
||||||
|
const resource = unwrapFileResource(value)
|
||||||
|
if (resource) return resource
|
||||||
|
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
const normalized = normalizeFileResource(value as Record<string, any>)
|
||||||
|
if (normalized) return normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof record.src === 'string') {
|
||||||
|
return normalizeFileResource(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyFileToUploadList(
|
||||||
|
fileId: number | string,
|
||||||
|
file?: FileResource | null,
|
||||||
|
fallbackName = 'image'
|
||||||
|
) {
|
||||||
|
const imageSrc = resolveFileImageUrl(file)
|
||||||
|
if (!imageSrc) return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
uid: String(fileId),
|
||||||
|
name: file?.name || fallbackName,
|
||||||
|
status: 'done',
|
||||||
|
url: imageSrc,
|
||||||
|
thumbUrl: imageSrc
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user