added gholak

This commit is contained in:
2026-08-03 22:36:23 +03:30
parent d6608283f6
commit 46807e3089
6 changed files with 383 additions and 262 deletions
+2 -1
View File
@@ -6,12 +6,13 @@ import type { TableOutput } from '@/models/table'
import { getProductInventory } from '@/api/products'
export const useFundUserStore = defineStore('fund', () => {
/** دارایی‌ها / موجودی محصول (صفحهٔ /fund فعلی) */
const myFunds = ref<TableOutput<Fund> | undefined>(undefined)
/** قلک‌های واقعی (مدل Fund) */
const myPiggyFunds = ref<TableOutput<Fund> | undefined>(undefined)
async function getMyFund() {
try {
// دارایی‌ها — inventory؛ سازگار با صفحهٔ /fund
myFunds.value = await getProductInventory()
return myFunds.value
} catch (e) {
+18 -35
View File
@@ -68,101 +68,84 @@
<div class="mt-3 p-1 shadow-lg from-primary rounded-lg p-px to-transparent border-solid border border-primary">
<div class=" p-1 py-6 rounded-lg bg-white">
<div class="flex justify-between items-start gap-1">
<!-- <div
class="text-center w-full flex-flex-col items-center home_appear_fade_in_up"
style="text-align: -webkit-center"
@click="router.push({ name: 'buyProduct', query: { state: 'sell' } })"
>
<div
class="rounded-full w-[42px] h-[42px] bg-primary flex items-center justify-center"
style="border: solid 1px #a72b77"
>
<Icon class="text-tertiary" icon="vuesax-linear:money-send" />
</div>
<div class="text-primary">فروش</div>
</div>-->
<div
class="text-center min-w-0 flex-1 flex-flex-col items-center home_appear_fade_in_up"
class="text-center min-w-0 flex-1 home_appear_fade_in_up"
style="text-align: -webkit-center"
@click="router.push({ name: 'gold' })"
>
<div
class="rounded-full w-[50px] h-[50px] bg-primary flex items-center justify-center"
class="rounded-full w-[44px] h-[44px] mx-auto bg-primary flex items-center justify-center"
style="border: solid 1px #a72b77"
>
<Icon class="text-white" icon="vuesax-linear:money-receive" />
</div>
<div class="text-primary">خرید و فروش</div>
<div class="text-primary text-[11px] leading-tight mt-1">خرید و فروش</div>
</div>
<div
class="text-center w-full flex-flex-col items-center home_appear_fade_in_down"
class="text-center min-w-0 flex-1 home_appear_fade_in_down"
style="text-align: -webkit-center"
@click="router.push({ name: 'wallet', query: { state: 'deposit' } })"
>
<div
class="rounded-full w-[50px] h-[50px] bg-primary flex items-center justify-center"
class="rounded-full w-[44px] h-[44px] mx-auto bg-primary flex items-center justify-center"
style="border: solid 1px #a72b77"
>
<Icon class="text-white" icon="vuesax-linear:card-add" />
</div>
<div class="text-primary">واریز نقدی</div>
<div class="text-primary text-[11px] leading-tight mt-1">واریز نقدی</div>
</div>
<div
class="text-center w-full flex-flex-col items-center home_appear_fade_in_up"
class="text-center min-w-0 flex-1 home_appear_fade_in_up"
style="text-align: -webkit-center"
@click="openWholeBalanceVisible()"
>
<div
class="rounded-full w-[50px] h-[50px] bg-primary flex items-center justify-center "
class="rounded-full w-[44px] h-[44px] mx-auto bg-primary flex items-center justify-center"
style="border: solid 1px #a72b77"
>
<Icon class="text-white" icon="vuesax-linear:receipt-item" />
</div>
<div class="text-primary">موجودی</div>
<div class="text-primary text-[11px] leading-tight mt-1">موجودی</div>
</div>
<div
class="text-center w-full flex-flex-col items-center home_appear_fade_in_down"
class="text-center min-w-0 flex-1 home_appear_fade_in_down"
style="text-align: -webkit-center"
@click="router.push({ name: 'fund' })"
>
<div
class="rounded-full w-[50px] h-[50px] bg-primary flex items-center justify-center"
class="rounded-full w-[44px] h-[44px] mx-auto bg-primary flex items-center justify-center"
style=" border: solid 1px #a72b77"
>
<Icon class="text-white font-bold" icon="vuesax-linear:archive-1" />
</div>
<div class="text-primary">دارایی ها</div>
<div class="text-primary text-[11px] leading-tight mt-1">دارایی ها</div>
</div>
<div
v-if="productFundsEnabled"
class="text-center w-full flex-flex-col items-center home_appear_fade_in_down"
class="text-center min-w-0 flex-1 home_appear_fade_in_down"
style="text-align: -webkit-center"
@click="router.push({ name: 'piggy' })"
>
<div
class="rounded-full w-[50px] h-[50px] bg-primary flex items-center justify-center"
class="rounded-full w-[44px] h-[44px] mx-auto bg-primary flex items-center justify-center"
style=" border: solid 1px #a72b77"
>
<Icon class="text-white font-bold" icon="vuesax-linear:safe-home" />
</div>
<div class="text-primary">قلک</div>
<div class="text-primary text-[11px] leading-tight mt-1">قلک</div>
</div>
<div
class="text-center w-full flex-flex-col items-center home_appear_fade_in_down"
class="text-center min-w-0 flex-1 home_appear_fade_in_down"
style="text-align: -webkit-center"
@click="router.push({ name: 'earnMoney' })"
>
<div
class="rounded-full w-[50px] h-[50px] bg-primary flex items-center justify-center"
class="rounded-full w-[44px] h-[44px] mx-auto bg-primary flex items-center justify-center"
style=" border: solid 1px #a72b77"
>
<Icon class="text-white font-bold" icon="vuesax-linear:dollar-circle" />
</div>
<div class="text-primary">کسب درآمد</div>
<div class="text-primary text-[11px] leading-tight mt-1">کسب درآمد</div>
</div>
</div>
+161 -92
View File
@@ -1,60 +1,94 @@
<template>
<div class="piggy-page">
<header class="piggy-topbar">
<button type="button" class="piggy-icon-btn" aria-label="بازگشت" @click="goBack">
<header class="piggy-bar">
<button type="button" class="piggy-bar__btn" aria-label="بازگشت" @click="goBack">
<BasicIcon icon="vuesax-linear:arrow-right-1" size="22" />
</button>
<h1 class="piggy-topbar__title">قلک</h1>
<span class="w-9" />
<h1 class="piggy-bar__title">قلک‌ها</h1>
<button type="button" class="piggy-bar__btn piggy-bar__btn--primary" aria-label="قلک جدید" @click="goAdd">
<BasicIcon icon="vuesax-linear:add" size="20" />
</button>
</header>
<a-spin :spinning="busy">
<section class="piggy-hero">
<div class="piggy-hero__text">
<p class="piggy-hero__label">موجودی قلک‌ها</p>
<p class="piggy-hero__value" dir="ltr">
<span>{{ formatPiggyAmount(totalGold) }}</span>
<span class="piggy-hero__unit">گرم</span>
</p>
<p class="piggy-hero__hint">{{ rows.length }} قلک فعال</p>
</div>
<img class="piggy-hero__art" :src="heroImg" alt="" />
</section>
<section class="piggy-sheet">
<div v-if="!rows.length" class="piggy-empty">
<img :src="emptyImg" alt="" class="piggy-empty__img" />
<p class="piggy-empty__title">هنوز قلکی نساخته‌اید</p>
<p class="piggy-empty__desc">موجودی هر محصول را در قلک‌های جدا نگه دارید</p>
</div>
<div v-else class="piggy-grid">
<button
v-for="item in rows"
:key="item.id"
type="button"
class="piggy-tile"
@click="openInfo(item.id!)"
>
<div class="piggy-tile__icon-wrap">
<img :src="piggyIconForTitle(item.title)" alt="" class="piggy-tile__icon" />
<section class="piggy-summary">
<div class="piggy-summary__card">
<div class="piggy-summary__row">
<div>
<p class="piggy-summary__label">جمع موجودی داخل قلک‌ها</p>
<p class="piggy-summary__value" dir="ltr">
{{ formatPiggyAmount(totalGold) }}
<span class="piggy-summary__unit">گرم</span>
</p>
</div>
<div class="piggy-tile__title">{{ item.title || 'بدون عنوان' }}</div>
<div class="piggy-tile__product">{{ fundProductLabel(item) }}</div>
<div class="piggy-tile__amount" dir="ltr">
{{ formatPiggyAmount(fundWalletGold(item)) }}
<span class="piggy-tile__unit">گرم</span>
<div class="piggy-summary__badge">
<BasicIcon icon="vuesax-linear:safe-home" size="22" />
</div>
</button>
</div>
<div class="piggy-summary__meta">
<span>{{ rows.length }} قلک</span>
<span class="piggy-summary__dot" />
<span>{{ groups.length }} محصول</span>
</div>
</div>
<BasicButton class="!w-full mt-5" type="primary" size="middle" block @click="goAdd">
<BasicButton class="!w-full mt-3" type="primary" size="middle" block @click="goAdd">
<div class="flex items-center justify-center gap-2">
<BasicIcon icon="vuesax-linear:add-circle" size="18" />
<span>افزودن قلک</span>
<span>{{ rows.length ? 'افزودن قلک' : 'ساخت اولین قلک' }}</span>
</div>
</BasicButton>
</section>
<section class="piggy-body">
<div v-if="!rows.length" class="piggy-empty">
<div class="piggy-empty__icon">
<BasicIcon icon="vuesax-linear:safe-home" size="36" />
</div>
<p class="piggy-empty__title">قلکی ندارید</p>
<p class="piggy-empty__desc">
برای جدا کردن بخشی از موجودی هر محصول (مثلاً آبشده یا سکه) یک قلک بسازید.
</p>
</div>
<div v-else class="piggy-groups">
<div v-for="group in groups" :key="group.productId" class="piggy-group">
<div class="piggy-group__head">
<div class="min-w-0">
<p class="piggy-group__name">{{ group.productTitle }}</p>
<p class="piggy-group__count">{{ group.funds.length }} قلک</p>
</div>
<p class="piggy-group__total" dir="ltr">
{{ formatPiggyAmount(group.totalGold) }}
<span>گرم</span>
</p>
</div>
<div class="piggy-group__list">
<button
v-for="item in group.funds"
:key="item.id"
type="button"
class="piggy-row"
@click="openInfo(item.id!)"
>
<div class="piggy-row__icon">
<BasicIcon :icon="piggyIconName(item.title)" size="20" />
</div>
<div class="piggy-row__main">
<div class="piggy-row__title">{{ item.title || 'بدون عنوان' }}</div>
<div class="piggy-row__sub">کد {{ item.id }}</div>
</div>
<div class="piggy-row__amount" dir="ltr">
<strong>{{ formatPiggyAmount(fundWalletGold(item)) }}</strong>
<span>گرم</span>
</div>
<BasicIcon class="piggy-row__chevron" icon="vuesax-linear:arrow-left-1" size="16" />
</button>
</div>
</div>
</div>
</section>
</a-spin>
</div>
</template>
@@ -66,21 +100,19 @@ import { useFundUserStore } from '@/store/fundStore'
import { useSettingsStore } from '@/store/settingsStore'
import { message } from 'ant-design-vue'
import type { Fund } from '@/models/fund'
import cashImg from '@/assets/img/cash_img.svg'
import emptyImg from '@/assets/img/cash_empty.svg'
import {
formatPiggyAmount,
fundProductLabel,
fundWalletGold,
piggyIconForTitle,
groupFundsByProduct,
piggyIconName,
} from './piggyIcons'
const fundStore = useFundUserStore()
const settingsStore = useSettingsStore()
const busy = ref(false)
const heroImg = cashImg
const rows = computed(() => (fundStore.myPiggyFunds?.rows ?? []) as Fund[])
const groups = computed(() => groupFundsByProduct(rows.value))
const totalGold = computed(() => rows.value.reduce((sum, item) => sum + fundWalletGold(item), 0))
function goBack() {
@@ -111,77 +143,114 @@ onMounted(async () => {
<style scoped>
.piggy-page {
@apply min-h-full pb-28;
background: linear-gradient(180deg, #f3efe8 0%, #f8fafc 42%, #f8fafc 100%);
@apply relative min-h-full bg-gray-50 pb-24;
}
.piggy-topbar {
@apply sticky top-0 z-20 flex items-center justify-between px-3 py-3;
background: rgba(243, 239, 232, 0.92);
backdrop-filter: blur(8px);
.piggy-bar {
@apply sticky top-0 z-20 flex items-center justify-between bg-white px-3 py-3;
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.06);
}
.piggy-topbar__title {
.piggy-bar__title {
@apply m-0 text-base font-bold text-gray-900;
}
.piggy-icon-btn {
@apply flex h-9 w-9 items-center justify-center rounded-full border-none bg-white/80 text-gray-800;
.piggy-bar__btn {
@apply flex h-9 w-9 items-center justify-center rounded-full border-none bg-gray-100 text-gray-800;
}
.piggy-hero {
@apply relative flex items-end justify-between gap-3 overflow-hidden px-5 pb-8 pt-2;
.piggy-bar__btn--primary {
@apply bg-primary text-white;
}
.piggy-hero__label {
@apply m-0 text-sm text-gray-600;
.piggy-summary {
@apply px-4 pt-4;
}
.piggy-hero__value {
@apply m-0 mt-1 flex items-baseline gap-2 text-3xl font-bold text-gray-900;
.piggy-summary__card {
@apply rounded-2xl bg-primary p-4 text-white;
}
.piggy-hero__unit {
@apply text-sm font-bold text-gray-500;
.piggy-summary__row {
@apply flex items-start justify-between gap-3;
}
.piggy-hero__hint {
@apply m-0 mt-2 text-xs text-gray-500;
.piggy-summary__label {
@apply m-0 text-sm text-white/80;
}
.piggy-hero__art {
@apply h-28 w-28 shrink-0 object-contain;
.piggy-summary__value {
@apply m-0 mt-1 flex items-baseline gap-2 text-3xl font-bold;
}
.piggy-sheet {
@apply relative z-10 -mt-2 rounded-t-3xl bg-white px-4 pb-6 pt-5 shadow-sm;
min-height: 55vh;
.piggy-summary__unit {
@apply text-sm font-medium text-white/75;
}
.piggy-grid {
@apply grid grid-cols-2 gap-3;
.piggy-summary__badge {
@apply flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-white/15;
}
.piggy-tile {
@apply flex flex-col items-center rounded-2xl border-none bg-gray-50 p-3 text-center;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
.piggy-summary__meta {
@apply mt-4 flex items-center gap-2 text-xs text-white/75;
}
.piggy-tile__icon-wrap {
@apply mb-2 flex h-20 w-full items-center justify-center rounded-xl bg-sky-50;
.piggy-summary__dot {
@apply h-1 w-1 rounded-full bg-white/50;
}
.piggy-tile__icon {
@apply h-16 w-16 object-contain;
.piggy-body {
@apply px-4 pt-4;
}
.piggy-tile__title {
@apply w-full truncate text-sm font-bold text-gray-900;
.piggy-groups {
@apply flex flex-col gap-4;
}
.piggy-tile__product {
@apply mt-0.5 w-full truncate text-[11px] text-gray-500;
.piggy-group {
@apply overflow-hidden rounded-2xl bg-white;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
}
.piggy-tile__amount {
@apply mt-2 flex items-baseline justify-center gap-1 text-sm font-bold text-primary;
.piggy-group__head {
@apply flex items-center justify-between gap-3 border-0 border-b border-solid border-gray-100 px-4 py-3;
}
.piggy-tile__unit {
.piggy-group__name {
@apply m-0 truncate text-sm font-bold text-gray-900;
}
.piggy-group__count {
@apply m-0 mt-0.5 text-[11px] text-gray-500;
}
.piggy-group__total {
@apply m-0 flex shrink-0 items-baseline gap-1 text-sm font-bold text-primary;
}
.piggy-group__total span {
@apply text-[11px] font-medium text-gray-500;
}
.piggy-empty {
@apply flex flex-col items-center px-4 py-10 text-center;
.piggy-group__list {
@apply flex flex-col;
}
.piggy-empty__img {
@apply mb-3 h-28 w-28 object-contain opacity-90;
.piggy-row {
@apply flex w-full items-center gap-3 border-0 border-b border-solid border-gray-50 bg-transparent px-3 py-3 text-right last:border-b-0;
}
.piggy-row__icon {
@apply flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gray-50 text-primary;
}
.piggy-row__main {
@apply min-w-0 flex-1;
}
.piggy-row__title {
@apply truncate text-sm font-bold text-gray-900;
}
.piggy-row__sub {
@apply mt-0.5 text-[11px] text-gray-400;
}
.piggy-row__amount {
@apply flex shrink-0 flex-col items-end leading-tight;
}
.piggy-row__amount strong {
@apply text-sm font-bold text-gray-900;
}
.piggy-row__amount span {
@apply text-[11px] text-gray-500;
}
.piggy-row__chevron {
@apply shrink-0 text-gray-300;
}
.piggy-empty {
@apply rounded-2xl bg-white px-5 py-10 text-center;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
}
.piggy-empty__icon {
@apply mx-auto mb-3 flex h-16 w-16 items-center justify-center rounded-2xl bg-gray-50 text-primary;
}
.piggy-empty__title {
@apply m-0 text-base font-bold text-gray-800;
@apply m-0 text-base font-bold text-gray-900;
}
.piggy-empty__desc {
@apply m-0 mt-2 text-sm text-gray-500;
@apply m-0 mt-2 text-sm leading-6 text-gray-500;
}
</style>
+76 -70
View File
@@ -1,57 +1,59 @@
<template>
<div class="piggy-info">
<header class="piggy-info__top">
<button type="button" class="piggy-icon-btn" aria-label="بازگشت" @click="goBack">
<BasicIcon icon="vuesax-linear:arrow-right-1" size="22" />
</button>
<h1 class="piggy-info__title">{{ fund?.title || 'قلک' }}</h1>
<button type="button" class="piggy-icon-btn piggy-icon-btn--danger" aria-label="حذف" @click="onDelete">
<BasicIcon icon="vuesax-linear:trash" size="20" />
</button>
<header class="piggy-info__hero">
<div class="piggy-info__top">
<button type="button" class="piggy-info__btn" aria-label="بازگشت" @click="goBack">
<BasicIcon icon="vuesax-linear:arrow-right-1" size="22" />
</button>
<h1 class="piggy-info__title">{{ fund?.title || 'قلک' }}</h1>
<button type="button" class="piggy-info__btn" aria-label="حذف" @click="onDelete">
<BasicIcon icon="vuesax-linear:trash" size="18" />
</button>
</div>
<a-spin :spinning="busy">
<div class="piggy-info__balance-block">
<p class="piggy-info__product">{{ fund?.product?.title || '—' }}</p>
<p class="piggy-info__label">موجودی قلک</p>
<p class="piggy-info__value" dir="ltr">
{{ formatPiggyAmount(potGold) }}
<span>گرم</span>
</p>
<p class="piggy-info__free">
آزاد قابل واریز:
<strong dir="ltr">{{ formatPiggyAmount(freeGold) }}</strong>
گرم
</p>
</div>
</a-spin>
</header>
<a-spin :spinning="busy">
<section class="piggy-info__hero">
<div class="piggy-info__icon-wrap">
<img :src="iconSrc" alt="" class="piggy-info__icon" />
</div>
<p class="piggy-info__product">{{ fund?.product?.title || '—' }}</p>
<p class="piggy-info__balance-label">موجودی این قلک</p>
<p class="piggy-info__balance" dir="ltr">
<span>{{ formatPiggyAmount(potGold) }}</span>
<span class="piggy-info__unit">گرم</span>
</p>
<p class="piggy-info__free">
آزاد قابل واریز:
<span dir="ltr" class="font-bold text-gray-800">{{ formatPiggyAmount(freeGold) }}</span>
گرم
</p>
</section>
<div class="piggy-info__sheet">
<div class="piggy-info__actions">
<BasicButton class="!flex-1" size="middle" @click="openTx(false)">
<div class="flex items-center justify-center gap-2">
<BasicIcon icon="vuesax-linear:add-circle" size="18" />
<span>برداشت</span>
<BasicIcon icon="vuesax-linear:export" size="18" />
<span>برداشت به آزاد</span>
</div>
</BasicButton>
<BasicButton class="!flex-1" type="primary" size="middle" @click="openTx(true)">
<div class="flex items-center justify-center gap-2">
<BasicIcon icon="vuesax-linear:arrow-up-1" size="18" />
<span>واریز</span>
<BasicIcon icon="vuesax-linear:import" size="18" />
<span>واریز از آزاد</span>
</div>
</BasicButton>
</div>
<section class="piggy-info__tx">
<h2 class="piggy-info__tx-title">تراکنش‌ها</h2>
<h2 class="piggy-info__tx-title">تراکنش‌های این قلک</h2>
<div v-if="!transactions.length" class="piggy-info__tx-empty">
<img :src="emptyImg" alt="" class="piggy-info__tx-empty-img" />
<p class="m-0 text-sm text-gray-500">هیچ تراکنشی یافت نشد</p>
<BasicIcon icon="vuesax-linear:receipt-2" size="28" />
<p class="m-0 mt-2 text-sm text-gray-500">هنوز جابه‌جایی ثبت نشده</p>
</div>
<div v-else class="piggy-info__tx-list">
<div v-for="tx in transactions" :key="tx.id" class="piggy-info__tx-row">
<div class="min-w-0 text-right">
<div class="piggy-info__tx-dot" />
<div class="min-w-0 flex-1 text-right">
<div class="truncate text-sm font-bold text-gray-900">
{{ tx.type_str || tx.type || 'تراکنش' }}
</div>
@@ -63,12 +65,16 @@
</div>
</div>
</section>
</a-spin>
</div>
<BasicModal v-model:visible="txVisible" :title="isDeposit ? 'واریز به قلک' : 'برداشت از قلک'">
<div class="flex flex-col gap-3 p-1">
<p class="m-0 text-xs text-gray-500">
{{ isDeposit ? `حداکثر آزاد: ${formatPiggyAmount(freeGold)} گرم` : `حداکثر قلک: ${formatPiggyAmount(potGold)} گرم` }}
{{
isDeposit
? `از موجودی آزاد «${fund?.product?.title || 'محصول'}» — حداکثر ${formatPiggyAmount(freeGold)} گرم`
: `برگرداندن به آزاد — حداکثر ${formatPiggyAmount(potGold)} گرم`
}}
</p>
<BasicInput v-model:value="txValue" type="number" placeholder="مقدار به گرم" />
<BasicButton class="!w-full" type="primary" block :disabled="busy" @click="submitTx">
@@ -95,8 +101,7 @@ import { useWalletAssets } from '@/composables/useWalletAssets'
import { message, Modal } from 'ant-design-vue'
import type { Fund } from '@/models/fund'
import type { Transaction } from '@/models/transaction'
import emptyImg from '@/assets/img/cash_empty.svg'
import { formatPiggyAmount, piggyIconForTitle } from './piggyIcons'
import { formatPiggyAmount } from './piggyIcons'
const fundStore = useFundUserStore()
const settingsStore = useSettingsStore()
@@ -110,7 +115,6 @@ const isDeposit = ref(true)
const txValue = ref<string | number | undefined>()
const fundId = computed(() => parseInt(String(router.currentRoute.value.params.id), 10))
const iconSrc = computed(() => piggyIconForTitle(fund.value?.title))
const potGold = computed(() => {
const wallets = Array.isArray(fund.value?.wallet)
@@ -216,61 +220,63 @@ onMounted(async () => {
.piggy-info {
@apply min-h-full bg-gray-50 pb-28;
}
.piggy-info__hero {
@apply bg-primary px-3 pb-8 pt-3 text-white;
}
.piggy-info__top {
@apply sticky top-0 z-20 flex items-center justify-between bg-white px-3 py-3 shadow-sm;
@apply flex items-center justify-between;
}
.piggy-info__btn {
@apply flex h-9 w-9 items-center justify-center rounded-full border-none bg-white/15 text-white;
}
.piggy-info__title {
@apply m-0 max-w-[55%] truncate text-base font-bold text-gray-900;
@apply m-0 max-w-[55%] truncate text-base font-bold text-white;
}
.piggy-icon-btn {
@apply flex h-9 w-9 items-center justify-center rounded-full border-none bg-gray-50 text-gray-800;
}
.piggy-icon-btn--danger {
@apply text-red-500;
}
.piggy-info__hero {
@apply flex flex-col items-center px-4 pt-6;
}
.piggy-info__icon-wrap {
@apply flex h-36 w-36 items-center justify-center rounded-3xl bg-sky-50;
}
.piggy-info__icon {
@apply h-28 w-28 object-contain;
.piggy-info__balance-block {
@apply mt-6 text-center;
}
.piggy-info__product {
@apply m-0 mt-3 rounded-full bg-white px-3 py-1 text-xs text-gray-600 shadow-sm;
@apply m-0 inline-flex rounded-full bg-white/15 px-3 py-1 text-xs text-white/90;
}
.piggy-info__balance-label {
@apply m-0 mt-4 text-sm text-gray-500;
.piggy-info__label {
@apply m-0 mt-4 text-sm text-white/75;
}
.piggy-info__balance {
@apply m-0 mt-1 flex items-baseline gap-2 text-3xl font-bold text-gray-900;
.piggy-info__value {
@apply m-0 mt-1 flex items-baseline justify-center gap-2 text-4xl font-bold;
}
.piggy-info__unit {
@apply text-sm font-bold text-gray-500;
.piggy-info__value span {
@apply text-sm font-medium text-white/75;
}
.piggy-info__free {
@apply m-0 mt-2 text-xs text-gray-500;
@apply m-0 mt-3 text-xs text-white/70;
}
.piggy-info__free strong {
@apply text-white;
}
.piggy-info__sheet {
@apply relative z-10 -mt-4 rounded-t-3xl bg-gray-50 px-4 pt-5;
}
.piggy-info__actions {
@apply mt-5 flex gap-3 px-4;
@apply flex gap-2;
}
.piggy-info__tx {
@apply mt-6 px-4;
@apply mt-5;
}
.piggy-info__tx-title {
@apply m-0 mb-3 text-base font-bold text-gray-900;
}
.piggy-info__tx-empty {
@apply flex flex-col items-center rounded-2xl bg-white px-4 py-10 shadow-sm;
}
.piggy-info__tx-empty-img {
@apply mb-3 h-24 w-24 object-contain opacity-80;
@apply flex flex-col items-center rounded-2xl bg-white px-4 py-10 text-gray-400;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
}
.piggy-info__tx-list {
@apply flex flex-col gap-2;
}
.piggy-info__tx-row {
@apply flex items-center justify-between gap-3 rounded-2xl bg-white p-3 shadow-sm;
@apply flex items-center gap-3 rounded-2xl bg-white p-3;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
}
.piggy-info__tx-dot {
@apply h-2.5 w-2.5 shrink-0 rounded-full bg-primary;
}
</style>
+91 -51
View File
@@ -1,36 +1,59 @@
<template>
<div class="piggy-create">
<header class="piggy-create__top">
<button type="button" class="piggy-icon-btn" aria-label="بازگشت" @click="goBack">
<header class="piggy-bar">
<button type="button" class="piggy-bar__btn" aria-label="بازگشت" @click="goBack">
<BasicIcon icon="vuesax-linear:arrow-right-1" size="22" />
</button>
<h1 class="piggy-create__title">{{ editId ? 'ویرایش قلک' : 'ساخت قلک' }}</h1>
<h1 class="piggy-bar__title">{{ editId ? 'ویرایش قلک' : 'ساخت قلک' }}</h1>
<span class="w-9" />
</header>
<a-spin :spinning="busy">
<div class="piggy-create__body">
<div class="piggy-create__preview">
<img :src="previewImg" alt="" class="piggy-create__preview-img" />
</div>
<section class="piggy-create__step">
<div class="piggy-create__step-head">
<span class="piggy-create__step-num">۱</span>
<div>
<h2 class="piggy-create__step-title">محصول</h2>
<p class="piggy-create__step-desc">این قلک فقط برای همین محصول پر می‌شود</p>
</div>
</div>
<div class="piggy-create__card">
<label class="piggy-create__label">محصول قلک</label>
<BasicSelect
v-model:value="productId"
class="!w-full"
key-name="id"
value-name="title"
:items="products"
:disabled="Boolean(editId)"
placeholder="انتخاب محصول"
/>
<p class="piggy-create__hint">هر قلک فقط یک محصول دارد — طلا و سکه مخلوط نمی‌شوند</p>
</div>
<div v-if="editId" class="piggy-create__locked">
<BasicIcon icon="vuesax-linear:box-1" size="18" />
<span>{{ lockedProductTitle || 'محصول ثابت' }}</span>
</div>
<div v-else class="piggy-create__products">
<button
v-for="p in products"
:key="p.id"
type="button"
class="piggy-create__product"
:class="{ 'piggy-create__product--on': productId === p.id }"
@click="productId = p.id"
>
<BasicIcon icon="vuesax-linear:box-1" size="18" />
<span class="truncate">{{ p.title || `محصول ${p.id}` }}</span>
</button>
</div>
</section>
<section class="piggy-create__step">
<div class="piggy-create__step-head">
<span class="piggy-create__step-num">۲</span>
<div>
<h2 class="piggy-create__step-title">نام قلک</h2>
<p class="piggy-create__step-desc">مثلاً پس‌انداز، هدیه یا ازدواج</p>
</div>
</div>
<div class="piggy-create__title-row">
<div class="piggy-create__title-icon">
<BasicIcon :icon="piggyIconName(title)" size="22" />
</div>
<BasicInput v-model:value="title" class="!flex-1" placeholder="عنوان قلک" maxlength="25" />
</div>
<div class="piggy-create__card">
<label class="piggy-create__label">عنوان قلک</label>
<BasicInput v-model:value="title" placeholder="مثلاً پس‌انداز" maxlength="25" />
<div class="piggy-create__presets">
<button
v-for="preset in presets"
@@ -43,12 +66,10 @@
{{ preset }}
</button>
</div>
</div>
</div>
</section>
<div class="piggy-create__footer">
<BasicButton class="!w-full" type="primary" size="middle" block :disabled="busy" @click="submit">
{{ editId ? 'ذخیره تغییرات' : 'ساخت قلک' }}
{{ editId ? 'ذخیره' : 'ساخت قلک' }}
</BasicButton>
</div>
</a-spin>
@@ -64,7 +85,7 @@ import { useFundUserStore } from '@/store/fundStore'
import { useSettingsStore } from '@/store/settingsStore'
import { message } from 'ant-design-vue'
import type { Product } from '@/models/products'
import { PIGGY_PRESETS, piggyIconForTitle } from './piggyIcons'
import { PIGGY_PRESETS, piggyIconName } from './piggyIcons'
const fundStore = useFundUserStore()
const settingsStore = useSettingsStore()
@@ -72,6 +93,7 @@ const busy = ref(false)
const title = ref('پس انداز')
const productId = ref<number | undefined>()
const products = ref<Product[]>([])
const lockedProductTitle = ref('')
const presets = [...PIGGY_PRESETS]
const editId = computed(() => {
@@ -80,8 +102,6 @@ const editId = computed(() => {
return Number.isFinite(n) && n > 0 ? n : 0
})
const previewImg = computed(() => piggyIconForTitle(title.value))
function goBack() {
router.back()
}
@@ -127,6 +147,9 @@ onMounted(async () => {
const fund = await getFundById(editId.value)
title.value = fund?.title ?? ''
productId.value = fund?.product_id
lockedProductTitle.value = fund?.product?.title || ''
} else if (products.value.length === 1) {
productId.value = products.value[0]?.id
}
} catch (e) {
console.log(e)
@@ -138,45 +161,62 @@ onMounted(async () => {
<style scoped>
.piggy-create {
@apply min-h-full bg-gray-50 pb-28;
@apply min-h-full bg-gray-50 pb-24;
}
.piggy-create__top {
@apply sticky top-0 z-20 flex items-center justify-between bg-white px-3 py-3 shadow-sm;
.piggy-bar {
@apply sticky top-0 z-20 flex items-center justify-between bg-white px-3 py-3;
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.06);
}
.piggy-create__title {
.piggy-bar__title {
@apply m-0 text-base font-bold text-gray-900;
}
.piggy-icon-btn {
@apply flex h-9 w-9 items-center justify-center rounded-full border-none bg-gray-50 text-gray-800;
.piggy-bar__btn {
@apply flex h-9 w-9 items-center justify-center rounded-full border-none bg-gray-100 text-gray-800;
}
.piggy-create__body {
@apply flex flex-col items-center gap-4 px-4 pt-6;
@apply flex flex-col gap-4 px-4 pt-4;
}
.piggy-create__preview {
@apply flex h-32 w-32 items-center justify-center rounded-2xl bg-sky-50;
.piggy-create__step {
@apply rounded-2xl bg-white p-4;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
}
.piggy-create__preview-img {
@apply h-24 w-24 object-contain;
.piggy-create__step-head {
@apply mb-4 flex items-start gap-3;
}
.piggy-create__card {
@apply w-full rounded-2xl bg-white p-4 shadow-sm;
.piggy-create__step-num {
@apply flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-sm font-bold text-white;
}
.piggy-create__label {
@apply mb-2 block text-sm font-bold text-gray-800;
.piggy-create__step-title {
@apply m-0 text-sm font-bold text-gray-900;
}
.piggy-create__hint {
@apply m-0 mt-2 text-xs leading-5 text-gray-500;
.piggy-create__step-desc {
@apply m-0 mt-1 text-xs text-gray-500;
}
.piggy-create__products {
@apply grid grid-cols-2 gap-2;
}
.piggy-create__product {
@apply flex items-center gap-2 rounded-xl border-none bg-gray-50 px-3 py-3 text-right text-sm text-gray-700;
}
.piggy-create__product--on {
@apply bg-primary text-white;
}
.piggy-create__locked {
@apply flex items-center gap-2 rounded-xl bg-gray-50 px-3 py-3 text-sm font-bold text-gray-800;
}
.piggy-create__title-row {
@apply flex items-center gap-2;
}
.piggy-create__title-icon {
@apply flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-gray-50 text-primary;
}
.piggy-create__presets {
@apply mt-3 flex gap-2 overflow-x-auto whitespace-nowrap pb-1;
@apply mt-3 flex flex-wrap gap-2;
}
.piggy-create__chip {
@apply shrink-0 rounded-full border-none bg-gray-100 px-3 py-2 text-xs text-gray-600;
@apply rounded-full border-none bg-gray-100 px-3 py-2 text-xs text-gray-600;
}
.piggy-create__chip--on {
@apply bg-primary text-white;
}
.piggy-create__footer {
@apply fixed bottom-20 left-0 right-0 z-20 px-4;
}
</style>
+35 -13
View File
@@ -1,9 +1,3 @@
import homeImg from '@/assets/img/cash_home.svg'
import giftImg from '@/assets/img/cash_gift.svg'
import circulationImg from '@/assets/img/cash_circulation.svg'
import loveImg from '@/assets/img/cash_love.svg'
import repairsImg from '@/assets/img/cash_repairs.svg'
import savingImg from '@/assets/img/cash_saving.svg'
import type { Fund } from '@/models/fund'
export const PIGGY_PRESETS = [
@@ -15,21 +9,22 @@ export const PIGGY_PRESETS = [
'خرید لوازم منزل',
] as const
export function piggyIconForTitle(title?: string | null): string {
/** آیکون vuesax برای عنوان قلک — بدون تصویر سه‌بعدی گیج‌کننده */
export function piggyIconName(title?: string | null): string {
switch ((title ?? '').trim()) {
case 'خرید لوازم منزل':
return homeImg
return 'vuesax-linear:house-2'
case 'هدیه':
return giftImg
return 'vuesax-linear:gift'
case 'گردش':
return circulationImg
return 'vuesax-linear:airplane'
case 'ازدواج':
return loveImg
return 'vuesax-linear:heart'
case 'تعمیرات':
return repairsImg
return 'vuesax-linear:setting-2'
case 'پس انداز':
default:
return savingImg
return 'vuesax-linear:safe-home'
}
}
@@ -44,8 +39,35 @@ export function fundProductLabel(item?: Fund | null): string {
return item.product?.title || (item as { product_title?: string }).product_title || '—'
}
export function fundProductId(item?: Fund | null): number {
return Number(item?.product_id ?? item?.product?.id ?? 0)
}
export function formatPiggyAmount(value: number): string {
const n = Number(value) || 0
if (Math.abs(n) < 0.0005) return '۰'
return n.toLocaleString('fa-IR', { maximumFractionDigits: 3 })
}
export type PiggyProductGroup = {
productId: number
productTitle: string
totalGold: number
funds: Fund[]
}
export function groupFundsByProduct(funds: Fund[]): PiggyProductGroup[] {
const map = new Map<number, PiggyProductGroup>()
for (const fund of funds) {
const productId = fundProductId(fund) || -1
const productTitle = fundProductLabel(fund)
let group = map.get(productId)
if (!group) {
group = { productId, productTitle, totalGold: 0, funds: [] }
map.set(productId, group)
}
group.funds.push(fund)
group.totalGold += fundWalletGold(fund)
}
return [...map.values()].sort((a, b) => a.productTitle.localeCompare(b.productTitle, 'fa'))
}