last update

This commit is contained in:
2026-06-15 11:32:46 +03:30
parent 44f0f8da68
commit c58ed81e43
5 changed files with 284 additions and 89 deletions
-1
View File
@@ -9,7 +9,6 @@
<meta http-equiv="Expires" content="0"> <meta http-equiv="Expires" content="0">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<meta name="enamad" content="44889195"> <meta name="enamad" content="44889195">
<link rel="manifest" href="https://nerkh.gold/get_user/monifist.json"/>
<title></title> <title></title>
</head> </head>
-2
View File
@@ -4,7 +4,6 @@ import 'virtual:uno.css'
import {computed, createApp, nextTick, ref, watch} from 'vue' import {computed, createApp, nextTick, ref, watch} from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import '@unocss/reset/tailwind-compat.css' import '@unocss/reset/tailwind-compat.css'
import VueApexCharts from 'vue3-apexcharts'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import { authenticateWithFingerprint, getToken, isIphone, removeToken } from '@/utils' import { authenticateWithFingerprint, getToken, isIphone, removeToken } from '@/utils'
@@ -25,7 +24,6 @@ app.use(VueLazyLoad, {
// options... // options...
}) })
app.use(router) app.use(router)
app.use(VueApexCharts)
const userStore = useUserStore() const userStore = useUserStore()
const walletStore = useWalletStore() const walletStore = useWalletStore()
+160 -68
View File
@@ -6,104 +6,196 @@
<a-radio-button value="WEEKLY" @click="handleUpdate('WEEKLY')">هفته گذشته</a-radio-button> <a-radio-button value="WEEKLY" @click="handleUpdate('WEEKLY')">هفته گذشته</a-radio-button>
<a-radio-button value="MONTHLY" @click="handleUpdate('MONTHLY')">ماه گذشته</a-radio-button> <a-radio-button value="MONTHLY" @click="handleUpdate('MONTHLY')">ماه گذشته</a-radio-button>
</a-radio-group> </a-radio-group>
<div id="chart-timeline"> <div class="min-h-[200px] relative">
<apexchart <div v-if="hasChartData" ref="chartEl" class="h-[200px] w-full" />
type="area" <div
height="200" v-else
:options="chartOptions" class="h-[200px] flex items-center justify-center text-gray-400 text-sm"
:series="props.priceChartSeries" >
/> داده نمودار در دسترس نیست
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import * as echarts from 'echarts'
import type { EChartsOption } from 'echarts'
import { ref, computed, watch, onBeforeUnmount, nextTick } from 'vue'
import { useConfigStore } from '@/store/configStore' import { useConfigStore } from '@/store/configStore'
import {usePrice} from "@/utils/usePrice"; import { usePrice } from '@/utils/usePrice'
import {getProjectFontFamily} from "@/utils/config";
type ChartPoint = [number, number]
const props = defineProps<{ const props = defineProps<{
priceChartSeries: any[] priceChartSeries?: Array<{ name?: string; data?: unknown[] }> | null
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
updateData: [timeline: string] updateData: [timeline: string]
}>() }>()
const config = useConfigStore() const configStore = useConfigStore()
const selectedValue = ref('DAILY') const selectedValue = ref('DAILY')
const chartEl = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null
let renderToken = 0
const toPersianNumber = (num: number) => { function extractPoints(): ChartPoint[] {
return num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d]) const source = props.priceChartSeries
if (!Array.isArray(source) || source.length === 0) {
return []
}
const raw = source[0]?.data
if (!Array.isArray(raw)) {
return []
}
return raw
.map((item) => {
if (!Array.isArray(item) || item.length < 2) {
return null
}
let x = Number(item[0])
let y = Number(item[1])
if (!Number.isFinite(x) || !Number.isFinite(y) || y <= 0) {
return null
}
if (x > 0 && x < 1e11) {
x *= 1000
}
return [x, y] as ChartPoint
})
.filter((point): point is ChartPoint => point !== null)
.sort((a, b) => a[0] - b[0])
} }
const chartOptions = computed(() => { const chartPoints = computed(() => extractPoints())
const fontFamily = getProjectFontFamily() const hasChartData = computed(() => chartPoints.value.length >= 2)
const toPersianNumber = (num: number) =>
num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
function buildOption(points: ChartPoint[]): EChartsOption {
const color = configStore.config?.color || '#CEA34C'
return { return {
chart: { animation: true,
id: 'نوسانات قیمت', grid: {
type: 'area', left: 4,
height: 200, right: 4,
toolbar: { show: false }, top: 8,
zoom: { autoScaleYaxis: true } bottom: 4,
}, containLabel: false
grid: { },
padding: { top: 0, right: 0, bottom: 0, left: 0 }, xAxis: {
yaxis: { lines: { show: false } } type: 'time',
}, show: false,
stroke: { width: 2 }, boundaryGap: false
dataLabels: { },
enabled: false, yAxis: {
// formatter: (val) => toPersianNumber(val), type: 'value',
style: { show: false,
fontFamily scale: true
} },
}, tooltip: {
yaxis: { trigger: 'axis',
tickAmount: 8, confine: true,
show: false, formatter: (params) => {
labels: { const row = Array.isArray(params) ? params[0] : params
formatter: (val: number) => toPersianNumber(usePrice(val, true, 'ه.ریال')), if (!row || !Array.isArray(row.value)) {
style: { fontSize: '13px', fontFamily } return ''
} }
},
xaxis: { const [timestamp, value] = row.value as [number, number]
show: false, const date = new Date(timestamp)
type: 'datetime', const dateText = `${date.toLocaleDateString('fa-IR')} - ${date.toLocaleTimeString('fa-IR', {
axisTicks: { show: false },
tooltip: { enabled: false },
labels: { show: false }
},
tooltip: {
x: {
formatter: (value: number) => {
const date = new Date(value)
return `${date.toLocaleDateString('fa-IR')} - ${date.toLocaleTimeString('fa-IR', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'
})}` })}`
return `${dateText}<br/>${toPersianNumber(usePrice(value, true, 'ه.ریال'))}`
} }
}, },
formatter: (val: number) => toPersianNumber(usePrice(val, true, 'ه.ریال')), series: [
style: { fontFamily } {
}, name: 'قیمت',
colors: [config.config.color], type: 'line',
fill: { smooth: true,
type: 'gradient', showSymbol: false,
gradient: { data: points,
shadeIntensity: 1, lineStyle: {
opacityFrom: 0.7, width: 2,
opacityTo: 0.9, color
stops: [0, 100] },
areaStyle: {
color,
opacity: 0.25
}
}
]
}
}
function destroyChart() {
if (!chart) {
return
}
chart.dispose()
chart = null
}
async function renderChart() {
const token = ++renderToken
if (!hasChartData.value) {
destroyChart()
return
}
await nextTick()
await nextTick()
if (token !== renderToken || !chartEl.value) {
return
}
const points = chartPoints.value
try {
if (!chart || chart.isDisposed()) {
chart = echarts.init(chartEl.value, undefined, { renderer: 'canvas' })
} }
chart.setOption(buildOption(points), true)
chart.resize()
} catch (error) {
console.error('Price chart render failed:', error)
destroyChart()
} }
} }
watch(
() => [chartPoints.value.length, selectedValue.value, configStore.config?.color] as const,
() => {
void renderChart()
},
{ immediate: true, flush: 'post' }
)
onBeforeUnmount(() => {
renderToken++
destroyChart()
}) })
const handleUpdate = (timeline: string) => { const handleUpdate = (timeline: string) => {
selectedValue.value = timeline
emit('updateData', timeline) emit('updateData', timeline)
} }
</script> </script>
+35 -9
View File
@@ -1,6 +1,28 @@
import { ref } from 'vue' import { ref } from 'vue'
import { getConstByKey } from '@/api/const' import { getConstByKey } from '@/api/const'
function parseJsonArray(raw: unknown): unknown[] {
if (raw == null || raw === '') {
return []
}
if (Array.isArray(raw)) {
return raw
}
if (typeof raw !== 'string') {
return []
}
const trimmed = raw.trim()
if (!trimmed) {
return []
}
try {
const parsed = JSON.parse(trimmed)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
export function useBanners() { export function useBanners() {
const banners = ref([]) const banners = ref([])
const loading = ref(false) const loading = ref(false)
@@ -9,29 +31,33 @@ export function useBanners() {
try { try {
loading.value = true loading.value = true
const response = await getConstByKey('header_banner') const response = await getConstByKey('header_banner')
if (!response) return if (!response) {
banners.value = []
return
}
let list = [] let list: unknown[] = []
if (typeof response === 'string') { if (typeof response === 'string') {
list = JSON.parse(response) list = parseJsonArray(response)
} else if (typeof response === 'object' && !Array.isArray(response) && 'value' in response) { } else if (typeof response === 'object' && !Array.isArray(response) && 'value' in response) {
list = typeof response.value === 'string' ? JSON.parse(response.value) : response.value list = parseJsonArray((response as { value?: unknown }).value)
} else if (Array.isArray(response) && response.length > 0) { } else if (Array.isArray(response) && response.length > 0) {
const first = response[0] const first = response[0] as { value?: unknown; icon?: unknown }
if (first.value) { if (first.value != null) {
list = typeof first.value === 'string' ? JSON.parse(first.value) : first.value list = parseJsonArray(first.value)
} else if (first.icon) { } else if (first.icon) {
list = response list = response
} }
} }
banners.value = Array.isArray(list) ? list.filter(b => b.show === true) : [] banners.value = list.filter((b: { show?: boolean }) => b?.show === true)
} catch (error) { } catch (error) {
console.error('Error loading banners:', error) console.error('Error loading banners:', error)
banners.value = []
} finally { } finally {
loading.value = false loading.value = false
} }
} }
return { banners, loading, loadBanners } return { banners, loading, loadBanners }
} }
@@ -1,24 +1,104 @@
import { ref } from 'vue' import { ref } from 'vue'
import { getAllPrices } from '@/api/home' import { getAllPrices, getChart } from '@/api/home'
type ChartPoint = [number, number]
function mapTimelinePoints(items: unknown[]): ChartPoint[] {
if (!Array.isArray(items)) {
return []
}
return items
.map((item) => {
const row = item as { timestamp?: number; value?: number; price?: number }
let timestamp = Number(row.timestamp)
const value = Number(row.value ?? row.price ?? 0)
if (!Number.isFinite(timestamp) || !Number.isFinite(value) || value <= 0) {
return null
}
if (timestamp > 0 && timestamp < 1e11) {
timestamp *= 1000
}
return [timestamp, value] as ChartPoint
})
.filter((point): point is ChartPoint => point !== null)
}
async function fetchCandleFallback(): Promise<ChartPoint[]> {
try {
const rows = await getChart()
if (!Array.isArray(rows)) {
return []
}
return rows
.map((row) => {
if (!row) {
return null
}
const dateRaw = Array.isArray(row) ? row[0] : row.date
const candle = Array.isArray(row) ? row[1] : row.candle
let value = 0
if (Array.isArray(candle)) {
value = Number(candle[3] ?? candle[2] ?? candle[0] ?? 0)
} else if (candle && typeof candle === 'object') {
const candleRow = candle as { end?: number; high?: number; start?: number }
value = Number(candleRow.end ?? candleRow.high ?? candleRow.start ?? 0)
} else {
value = Number(candle ?? 0)
}
let timestamp = typeof dateRaw === 'number'
? dateRaw
: new Date(String(dateRaw)).getTime()
if (timestamp > 0 && timestamp < 1e11) {
timestamp *= 1000
}
if (!Number.isFinite(timestamp) || !Number.isFinite(value) || value <= 0) {
return null
}
return [timestamp, value] as ChartPoint
})
.filter((point): point is ChartPoint => point !== null)
} catch {
return []
}
}
export function usePriceChart() { export function usePriceChart() {
const priceChartSeries = ref([{ name: 'قیمت', data: [] }]) const priceChartSeries = ref([{ name: 'قیمت', data: [] as ChartPoint[] }])
const selection = ref('DAILY') const selection = ref('DAILY')
const chartLoading = ref(false) const chartLoading = ref(false)
const toPersianNumber = (num: number) => {
return num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
}
const fetchPrices = async () => { const fetchPrices = async () => {
try { try {
chartLoading.value = true chartLoading.value = true
const response = await getAllPrices() const response = await getAllPrices()
const data = response.data?.prices const prices = response?.data?.prices ?? response?.prices
const selectedData = data[selection.value] || []
priceChartSeries.value[0].data = selectedData.map((item: any) => [item.timestamp, item.value - 1]) let points: ChartPoint[] = []
if (prices && typeof prices === 'object') {
const selectedData = prices[selection.value]
points = mapTimelinePoints(Array.isArray(selectedData) ? selectedData : [])
}
if (points.length === 0) {
points = await fetchCandleFallback()
}
priceChartSeries.value = [{ name: 'قیمت', data: points }]
} catch (error) { } catch (error) {
console.error('Error fetching prices:', error) console.error('Error fetching prices:', error)
const fallback = await fetchCandleFallback()
priceChartSeries.value = [{ name: 'قیمت', data: fallback }]
} finally { } finally {
chartLoading.value = false chartLoading.value = false
} }