change prices &...

This commit is contained in:
2026-06-03 11:57:40 +03:30
parent f4b98576f5
commit b055cd4a5d
16 changed files with 305 additions and 25 deletions
+2
View File
@@ -25,6 +25,7 @@ declare module 'vue' {
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
AUpload: typeof import('ant-design-vue/es')['Upload']
BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default']
Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default']
BasicActionSheet: typeof import('./src/components/BasicPacks/BasicActionSheet.vue')['default']
@@ -54,6 +55,7 @@ declare module 'vue' {
ListAnim: typeof import('./src/components/ListAnim.vue')['default']
Mode: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/mode.vue')['default']
PresettedRanges: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/presetted-ranges.vue')['default']
ProfileAvatarUpload: typeof import('./src/components/ProfileAvatarUpload.vue')['default']
RangePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/range-picker.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
+15
View File
@@ -0,0 +1,15 @@
import useAxios from '@/utils/useAxios'
export interface AvatarUploadResponse {
id: number
src?: string
full_src?: string
title?: string
}
/** POST multipart — expects backend route POST /customer/avatar */
export const uploadAvatar = async (file: File) => {
const formData = new FormData()
formData.append('file', file)
return useAxios.postForm<AvatarUploadResponse>('/customer/avatar', formData)
}
+119
View File
@@ -0,0 +1,119 @@
<template>
<div class="flex flex-col items-center gap-2">
<div class="relative">
<div
class="w-24 h-24 rounded-full overflow-hidden border-2 border-gray-200 bg-gray-100 flex items-center justify-center"
>
<img
v-if="previewUrl"
:src="previewUrl"
alt="عکس پروفایل"
class="w-full h-full object-cover"
/>
<BasicIcon v-else icon="vuesax-linear:user" size="40" color="#9CA3AF" />
</div>
<button
v-if="previewUrl && !uploading"
type="button"
class="absolute bottom-0 left-0 z-10 p-1 rounded-full bg-white shadow"
aria-label="حذف عکس"
@click.stop="onRemove"
>
<BasicIcon icon="vuesax-linear:trash" class="text-red-600" size="18" />
</button>
</div>
<a-upload
name="file"
:show-upload-list="false"
accept="image/jpeg,image/png,image/webp,image/gif"
:disabled="uploading"
:before-upload="beforeUpload"
:custom-request="customRequest"
>
<BasicButton size="small" type="default" :disabled="uploading">
<a-spin v-if="uploading" size="small" class="ml-1" />
{{ previewUrl ? 'تغییر عکس' : 'انتخاب عکس پروفایل' }}
</BasicButton>
</a-upload>
<p class="text-xs text-gray-500 text-center">فرمت jpg، png یا webp — حداکثر ۲ مگابایت</p>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { message, Upload } from 'ant-design-vue'
import type { UploadProps } from 'ant-design-vue'
type UploadRequestOption = Parameters<NonNullable<UploadProps['customRequest']>>[0]
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { uploadAvatar } from '@/api/avatar'
import { getFileUrl } from '@/utils/file'
const props = defineProps<{
imageUrl?: string | null
}>()
const emit = defineEmits<{
(e: 'uploaded', payload: { id: number; url: string }): void
(e: 'removed'): void
}>()
const previewUrl = ref<string | null>(null)
const uploading = ref(false)
watch(
() => props.imageUrl,
(url) => {
previewUrl.value = url || null
},
{ immediate: true }
)
function beforeUpload(file: File) {
if (!file.type.startsWith('image/')) {
message.error('فقط فایل تصویری مجاز است')
return Upload.LIST_IGNORE
}
if (file.size / 1024 / 1024 > 2) {
message.error('حجم تصویر نباید بیشتر از ۲ مگابایت باشد')
return Upload.LIST_IGNORE
}
return true
}
async function customRequest(options: UploadRequestOption) {
const file = options.file as File
uploading.value = true
const localPreview = URL.createObjectURL(file)
previewUrl.value = localPreview
try {
const res = await uploadAvatar(file)
const url = res.full_src || getFileUrl(res) || localPreview
previewUrl.value = url
if (res.id != null) {
emit('uploaded', { id: res.id, url })
message.success('عکس پروفایل آپلود شد')
options.onSuccess?.(res)
} else {
throw new Error('invalid response')
}
} catch {
previewUrl.value = props.imageUrl || null
message.error('آپلود عکس انجام نشد. (احتمالاً API سرور هنوز آماده نیست)')
options.onError?.(new Error('upload failed'))
} finally {
uploading.value = false
if (localPreview.startsWith('blob:')) {
URL.revokeObjectURL(localPreview)
}
}
}
function onRemove() {
previewUrl.value = null
emit('removed')
}
</script>
+1
View File
@@ -15,6 +15,7 @@ export interface Series {
export interface Price {
sell: number
buy: number
price: number
price_last: number
goldir:number
}
+5 -1
View File
@@ -19,7 +19,11 @@ export interface User {
name: string;
active: boolean;
mobile: string;
avatar_id?: any;
avatar_id?: number | null;
avatar_url?: string | null;
full_src?: string | null;
avatar?: { id?: number; src?: string; full_src?: string; url?: string };
file?: { id?: number; src?: string; full_src?: string; url?: string };
last_seen_at: string;
default_address_id?: any;
sts?: any;
+2 -2
View File
@@ -10,8 +10,8 @@ const projectName = getConfigApp('name')
export const baseURL: string = projectBase
// export const baseURL: string = 'https://api-gholaktala.z23.ir/'
// const baseUploadURL: string = '/site/file'
// const uploadURL: string = baseURL + baseUploadURL
/** Customer profile image upload (backend: POST /customer/avatar) */
export const avatarUploadURL: string = `${projectBase.replace(/\/$/, '')}/customer/avatar`
// const baseURLStorage: string = baseURL + '/v1'
const prefixCls: string = 'noname'
// const version: string = pkg.version
+12
View File
@@ -1,13 +1,16 @@
import { baseURL } from '@/themeConfig'
import type { User } from '@/models/login'
export interface ProductFile {
id?: number
url?: string | null
src?: string
full_src?: string
}
export function getFileUrl(file?: ProductFile | null): string | null {
if (!file) return null
if (file.full_src) return file.full_src
if (file.url) return file.url
if (!file.src) return null
if (/^https?:\/\//i.test(file.src)) return file.src
@@ -15,3 +18,12 @@ export function getFileUrl(file?: ProductFile | null): string | null {
const path = file.src.replace(/^\//, '')
return `${base}/${path}`
}
/** Resolve profile avatar URL from GET /customer/me payload */
export function getUserAvatarUrl(user?: User | null): string | null {
if (!user) return null
if (typeof user.avatar_url === 'string' && user.avatar_url) return user.avatar_url
if (typeof user.full_src === 'string' && user.full_src) return user.full_src
const nested = user.avatar || user.file
return getFileUrl(nested as ProductFile | undefined)
}
+1 -1
View File
@@ -40,7 +40,7 @@
</div>
</div>
<div v-if="config.config.name === 'garoosi'" class="wrap-fix bottom-0 px-10 pb-3rem flex gap-2 bg-white">
<a class="w-full" href="tel:09306612518">
<a class="w-full" href="tel:09031265925">
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
</a>
<a href="https://ble.ir/garoosigallery">
+37 -6
View File
@@ -308,8 +308,17 @@ const finalPrice = computed(() => {
}
})
const payablePrice = computed(() => finalPrice.value)
const equivalentPrice = computed(() => usePriceWithoutType(((isBuy ? price.value?.buy ?? 0 : price.value?.sell ?? 0) * (parseFloat(((form.value?.goldValue ?? 0) * 0.01).toFixed(4))) ).toFixed(0)) )
const payablePrice = computed(() =>
isBuy.value ? finalPrice.value + feeInRial.value : finalPrice.value - feeInRial.value
)
const feeInGrams = computed(() =>
parseFloat(((form.value?.goldValue ?? 0) * 0.01).toFixed(4))
)
const unitPrice = computed(() =>
(props?.ProductItem?.price_profit ?? 0) + (props?.ProductItem?.price ?? 0)
)
const feeInRial = computed(() => unitPrice.value * feeInGrams.value)
const equivalentPrice = computed(() => usePriceWithoutType(feeInRial.value.toFixed(0)))
const disableTransaction = computed(() =>
loading.value ||
@@ -506,6 +515,28 @@ const limitedPrice = computed(() => {
}
}
}*/
function syncFormWithLatestPrice() {
if (!price.value) return
const goldValue = form.value.goldValue
if (goldValue !== undefined && goldValue !== null && goldValue !== '' && !isNaN(Number(goldValue))) {
calculateCash(Number(goldValue))
return
}
const goldCash = form.value.goldCash
if (goldCash !== undefined && goldCash !== null && goldCash !== '' && !isNaN(Number(goldCash))) {
calculateGold(Number(goldCash))
}
}
watch(
() => [basePriceValue.value, isBuy.value],
() => {
syncFormWithLatestPrice()
}
)
function calculateCash(val: number) {
priceErrorVisible.value = false
if (price.value) {
@@ -597,10 +628,10 @@ const formatter = (value: number) => {
const price = computed((): Price => <Price>lastPrice.value ?? 0)
const goldPriceValue = computed(() => {
if (!price.value) return 0
return isBuy.value
? (price.value.buy ?? 0)
: (price.value.sell ?? 0)
if (!lastPrice.value?.price) return 0
return isBuy.value
? (lastPrice.value.buy ?? 0)
: (lastPrice.value.sell ?? 0)
})
async function initPrice() {
+1 -1
View File
@@ -89,7 +89,7 @@
</div>
<div v-if="config.config.name === 'garoosi'" class="wrap-fix bottom-0 px-10 py-6 flex gap-2 bg-white">
<a class="w-full" href="tel:09306612518">
<a class="w-full" href="tel:09031265925">
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
</a>
<a href="https://ble.ir/garoosigallery">
+37 -6
View File
@@ -167,7 +167,7 @@
<div class="flex justify-between mt-3">
<div>کارمزد:</div>
<div class="flex items-center gap-2">
<div class="font-bold text-xl">{{ parseFloat(((form.goldValue ?? 0) * (Number(feeAmount))).toFixed(4)) }}</div>
<div class="font-bold text-xl">{{ feeInGrams }}</div>
<div class="text-xs mr-1">گرم</div>
</div>
</div>
@@ -274,10 +274,18 @@ const sellPagination = ref<TableEngineInput>({
}], limit: 6, page: 0
})
const finalPrice = computed(() => (form.value?.goldValue * (isBuy.value ? price.value?.buy ?? 0 : price.value?.sell ?? 0)))
// const payablePrice = computed(() => finalPrice.value + finalPrice.value * 0.007)
const payablePrice = computed(() => finalPrice.value)
const equivalentPrice = computed(() => usePriceWithoutType(((isBuy ? price.value?.buy ?? 0 : price.value?.sell ?? 0) * (parseFloat(((form.value?.goldValue ?? 0) * 0.01).toFixed(4))) ).toFixed(0)) )
const unitPrice = computed(() =>
isBuy.value ? price.value?.buy ?? 0 : price.value?.sell ?? 0
)
const feeInGrams = computed(() =>
parseFloat(((form.value?.goldValue ?? 0) * Number(feeAmount.value)).toFixed(4))
)
const finalPrice = computed(() => (form.value?.goldValue ?? 0) * unitPrice.value)
const feeInRial = computed(() => unitPrice.value * feeInGrams.value)
const equivalentPrice = computed(() => usePriceWithoutType(feeInRial.value.toFixed(0)))
const payablePrice = computed(() =>
isBuy.value ? finalPrice.value + feeInRial.value : finalPrice.value - feeInRial.value
)
const disableTransaction = computed(() =>
loading.value ||
@@ -472,6 +480,28 @@ const limitedPrice = computed(() => {
return walletStore?.limitedPrice[0]
})
function syncFormWithLatestPrice() {
if (!price.value) return
const goldValue = form.value.goldValue
if (goldValue !== undefined && goldValue !== null && goldValue !== '' && !isNaN(Number(goldValue))) {
calculateCash(Number(goldValue))
return
}
const goldCash = form.value.goldCash
if (goldCash !== undefined && goldCash !== null && goldCash !== '' && !isNaN(Number(goldCash))) {
calculateGold(Number(goldCash))
}
}
watch(
() => [lastPrice.value?.buy, lastPrice.value?.sell, isBuy.value],
() => {
syncFormWithLatestPrice()
}
)
function calculateCash(val: number) {
priceErrorVisible.value = false
// console.log(val)
@@ -556,7 +586,8 @@ const formatter = (value: number) => {
const price = computed((): Price => <Price>lastPrice.value ?? 0)
const goldPriceValue = computed(() => {
if (!price.value) return 0
// if (!price.value) return 0
if (!lastPrice.value?.price) return 0
return isBuy.value
? (price.value.buy ?? 0)
: (price.value.sell ?? 0)
@@ -66,7 +66,7 @@ const chartOptions = computed(() => {
tickAmount: 8,
show: false,
labels: {
formatter: (val: number) => toPersianNumber(usePrice(val)),
formatter: (val: number) => toPersianNumber(usePrice(val, true, 'ه.ریال')),
style: { fontSize: '13px', fontFamily }
}
},
@@ -87,7 +87,7 @@ const chartOptions = computed(() => {
})}`
}
},
formatter: (val: number) => toPersianNumber(usePrice(val)),
formatter: (val: number) => toPersianNumber(usePrice(val, true, 'ه.ریال')),
style: { fontFamily }
},
colors: [config.config.color],
@@ -652,6 +652,24 @@ onMounted(async () => {
})
async function initPrice() {
try {
// chartLoading.value = true
await getLastPrices()
// await setIntervalGetPrice()
loading.value = true
price.value = lastPrice.value
/*props.ProductItem.price = lastPrice.value?.buy * props.ProductItem?.amount
props.ProductItem.sellPrice = lastPrice.value?.sell * props.ProductItem?.amount*/
props.ProductItem.price = lastPrice?.value?.price? (lastPrice?.value?.price + props.ProductItem?.cash_fee_buy) * props.ProductItem?.amount: 0
props.ProductItem.sellPrice = lastPrice?.value?.price? (lastPrice?.value?.price - props.ProductItem?.percent_fee_sell) * props.ProductItem?.amount:0
} catch (e) {
console.log(e)
} finally {
loading.value = false
// chartLoading.value = false
}
}
/*async function initPrice() {
try {
// chartLoading.value = true
await getLastPrices()
@@ -666,7 +684,7 @@ async function initPrice() {
loading.value = false
// chartLoading.value = false
}
}
}*/
// onMounted(() => {
// initPrice()
+1 -1
View File
@@ -45,7 +45,7 @@
</div>
</div>
<div v-if="config.config.name === 'garoosi'" class="wrap-fix bottom-0 px-10 pb-3rem flex gap-2 bg-white">
<a class="w-full" href="tel:09306612518">
<a class="w-full" href="tel:09031265925">
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
</a>
<a href="https://ble.ir/garoosigallery">
@@ -17,7 +17,7 @@
<!-- <img :src="permission1" alt="" class="w-full">-->
<div class="mt-5 text-xs"></div>
<div v-if="config.config.name === 'garoosi'" class="wrap-fix bottom-0 px-10 pb-3rem flex gap-2 bg-white">
<a class="w-full" href="tel:09306612518">
<a class="w-full" href="tel:09031265925">
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
</a>
<a href="https://ble.ir/garoosigallery">
+50 -3
View File
@@ -14,6 +14,17 @@
<div
class="border-gray-200 border-1 rounded-xl p-5 trans flex flex-col items-center mx-5 z-1000 relative bg-white shadow-lg"
>
<!-- <div
class="w-20 h-20 rounded-full overflow-hidden border-2 border-gray-200 bg-gray-100 flex items-center justify-center mb-3"
>
<img
v-if="profileAvatarUrl"
:src="profileAvatarUrl"
alt="عکس پروفایل"
class="w-full h-full object-cover"
/>
<BasicIcon v-else icon="vuesax-linear:user" size="36" color="#9CA3AF" />
</div>-->
<div class="text-primary font-bold w-full text-center flex justify-between">
نام و نام خانوادگی:
<div class="mr-2">{{ userStore.userInfo?.name }}</div>
@@ -189,6 +200,12 @@
</div>
</div>
<div class="h60vh overflow-y-auto pb20">
<!-- <ProfileAvatarUpload
:image-url="editAvatarUrl"
class="mb-4"
@uploaded="onAvatarUploaded"
@removed="onAvatarRemoved"
/>-->
<a-form ref="formRef" :model="userFormState" class="!mt-5" name="basic" @finish="ok">
<a-form-item :rules="[isValidPersian()]" label="نام" name="fname">
<BasicInput v-model:value="userFormState.fname" />
@@ -266,7 +283,7 @@
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, computed } from 'vue'
import { onMounted, ref, computed, watch } from 'vue'
import img from '@/assets/img/user_ly1.svg'
import gr_bottom from './assets/img/gr_bottom_white.svg'
import { loading } from '@/utils'
@@ -289,6 +306,8 @@ import { message } from 'ant-design-vue'
import { updateMe, updatePass } from '@/api/me'
import { useUserStore } from '@/store/userStore'
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker'
import ProfileAvatarUpload from '@/components/ProfileAvatarUpload.vue'
import { getUserAvatarUrl } from '@/utils/file'
import { useConfigStore } from '@/store/configStore'
import { getConfigApp, getReferralShareText, getReferralShareTitle } from '../../../utils/config'
@@ -311,11 +330,39 @@ const nameProject = useConfigStore().config.name_fa
// const editPassVisible = ref<boolean>(false)
const editPassVisible = ref<boolean>(false)
const editPasswordVisible = ref<boolean>(false)
const localAvatarUrl = ref<string | null>(null)
const profileAvatarUrl = computed(() => localAvatarUrl.value || getUserAvatarUrl(userStore.userInfo))
const editAvatarUrl = computed(() => localAvatarUrl.value || getUserAvatarUrl(userFormState.value))
function setForm() {
userFormState.value = userStore.userInfo
userFormState.value = { ...userStore.userInfo } as User
localAvatarUrl.value = getUserAvatarUrl(userStore.userInfo)
}
function onAvatarUploaded({ id, url }: { id: number; url: string }) {
userFormState.value.avatar_id = id
localAvatarUrl.value = url
if (userStore.userInfo) {
userStore.userInfo.avatar_id = id
userStore.userInfo.avatar_url = url
}
}
function onAvatarRemoved() {
userFormState.value.avatar_id = null
localAvatarUrl.value = null
if (userStore.userInfo) {
userStore.userInfo.avatar_id = null
userStore.userInfo.avatar_url = null
}
}
watch(editPassVisible, (open) => {
if (open) setForm()
})
function copyCode() {
// nextTick(() => {
const val = userStore.userInfo?.referral_id
@@ -386,9 +433,9 @@ function ok() {
loading.value = true
await updateMe(userFormState.value)
await userStore.getMyInfo()
localAvatarUrl.value = getUserAvatarUrl(userStore.userInfo)
editPassVisible.value = false
message.success('اطلاعات شما با موفقیت به روز رسانی شد')
location.reload()
} catch (e) {
console.log(e)
} finally {