商城c端前端
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<section class="m-home">
|
||||
<div class="m-home-hero" :style="bannerStyle">
|
||||
<label class="m-search">
|
||||
<n-icon :component="SearchOutline" />
|
||||
<input readonly placeholder="搜索活动、商品、问题等" @click="router.push('/app/mall')" />
|
||||
</label>
|
||||
<div class="m-greeting">
|
||||
<h1>Hi,{{ home?.profile?.username || '会员' }}</h1>
|
||||
<p>一起创造最美好的生活</p>
|
||||
</div>
|
||||
<div class="m-balance-card">
|
||||
<div
|
||||
><span>我的积分账户</span
|
||||
><strong>{{ money(home?.wallet?.available_amount, home?.wallet?.currency) }}</strong></div
|
||||
>
|
||||
<div class="m-coin" aria-hidden="true">¥</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m-home-body">
|
||||
<AsyncState :loading="loading" :error="error" @retry="load">
|
||||
<div class="m-shortcuts m-card">
|
||||
<button
|
||||
v-for="item in shortcuts"
|
||||
:key="item.label"
|
||||
:disabled="!item.enabled"
|
||||
@click="openShortcut(item)"
|
||||
>
|
||||
<span><img :src="item.icon" alt="" /></span><b>{{ item.label }}</b>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="home?.notices?.length" class="m-notice m-card">
|
||||
<n-icon :component="MegaphoneOutline" />
|
||||
<p>{{ home.notices[0].title || home.notices[0].content }}</p>
|
||||
</div>
|
||||
|
||||
<section class="m-section m-card m-home-ranking">
|
||||
<div class="m-section-title"
|
||||
><h2>荣誉榜</h2><router-link to="/app/leaderboard">更多 ›</router-link></div
|
||||
>
|
||||
<div v-if="leaderboard.length" class="m-home-rank-list">
|
||||
<router-link
|
||||
v-for="entry in leaderboard.slice(0, 4)"
|
||||
:key="entry.rank"
|
||||
to="/app/leaderboard"
|
||||
>
|
||||
<b :class="`rank-${entry.rank}`">{{ entry.rank === 1 ? '♛' : entry.rank }}</b>
|
||||
<span class="m-home-rank-avatar">
|
||||
<img v-if="safeImage(entry.avatar_url)" :src="safeImage(entry.avatar_url)" alt="" />
|
||||
<i v-else>{{ entry.username.slice(0, 1) }}</i>
|
||||
</span>
|
||||
<strong>{{ entry.username }}</strong>
|
||||
<em>{{ formatCount(entry.completed_count) }}</em>
|
||||
<small>{{ entry.status || '领取成功' }}</small>
|
||||
</router-link>
|
||||
</div>
|
||||
<p v-else class="m-home-rank-empty">暂无排行数据</p>
|
||||
</section>
|
||||
|
||||
<section class="m-section m-card">
|
||||
<div class="m-section-title"><h2>公司简介</h2></div>
|
||||
<div class="m-company-links">
|
||||
<router-link
|
||||
v-for="item in companyLinks"
|
||||
:key="item.label"
|
||||
:to="item.path"
|
||||
>
|
||||
<img :src="item.icon" alt="" /><span>{{ item.label }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
</AsyncState>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { SearchOutline, MegaphoneOutline } from '@vicons/ionicons5';
|
||||
import AsyncState from '../components/AsyncState.vue';
|
||||
import { errorText, money, request, safeImage } from '../api';
|
||||
import { mobileAssets } from '../assets';
|
||||
|
||||
interface HomeData {
|
||||
profile: { username: string; avatar_url: string; level_name: string };
|
||||
wallet: { available_amount: number; frozen_amount: number; currency: string };
|
||||
features: Record<string, boolean>;
|
||||
banners: Array<{ image_url?: string; title?: string }>;
|
||||
notices: Array<{ title?: string; content?: string }>;
|
||||
task_summary: {
|
||||
today_count: number;
|
||||
today_commission_amount: number;
|
||||
yesterday_commission_amount: number;
|
||||
};
|
||||
}
|
||||
interface LeaderboardEntry {
|
||||
rank: number;
|
||||
username: string;
|
||||
avatar_url: string;
|
||||
completed_count: number;
|
||||
status: string;
|
||||
}
|
||||
const router = useRouter();
|
||||
const home = ref<HomeData>();
|
||||
const loading = ref(true);
|
||||
const error = ref('');
|
||||
const leaderboard = ref<LeaderboardEntry[]>([]);
|
||||
const formatCount = (value: number) => new Intl.NumberFormat('zh-CN').format(value || 0);
|
||||
const bannerStyle = computed(() => {
|
||||
const url = safeImage(home.value?.banners?.[0]?.image_url);
|
||||
return {
|
||||
backgroundImage: url
|
||||
? `linear-gradient(180deg, rgba(16,13,8,.18), rgba(16,13,8,.54)), url(${url})`
|
||||
: `url(${mobileAssets.homeHero})`,
|
||||
};
|
||||
});
|
||||
const shortcuts = computed(() => [
|
||||
{
|
||||
label: '领取',
|
||||
path: '/app/claim',
|
||||
icon: mobileAssets.shortcutClaim,
|
||||
enabled: home.value?.features?.task_enabled !== false,
|
||||
},
|
||||
{
|
||||
label: '储入',
|
||||
path: '/app/recharge',
|
||||
icon: mobileAssets.shortcutRecharge,
|
||||
enabled: home.value?.features?.recharge_enabled !== false,
|
||||
},
|
||||
{
|
||||
label: '取出',
|
||||
path: '/app/profile/withdrawal',
|
||||
icon: mobileAssets.shortcutWithdraw,
|
||||
enabled: home.value?.features?.withdrawal_enabled !== false,
|
||||
},
|
||||
{
|
||||
label: '商城',
|
||||
path: '/app/mall',
|
||||
icon: mobileAssets.shortcutMall,
|
||||
enabled: home.value?.features?.manual_mall_enabled !== false,
|
||||
},
|
||||
]);
|
||||
const companyLinks = [
|
||||
{
|
||||
label: '平台介绍',
|
||||
path: '/app/content/platform_intro',
|
||||
icon: mobileAssets.companyPlatform,
|
||||
},
|
||||
{ label: '规则说明', path: '/app/rules', icon: mobileAssets.companyRules },
|
||||
{
|
||||
label: '公司资质',
|
||||
path: '/app/qualification',
|
||||
icon: mobileAssets.companyQualification,
|
||||
},
|
||||
{ label: '代理合作', path: '/app/agency', icon: mobileAssets.companyAgency },
|
||||
];
|
||||
|
||||
function openShortcut(item: { enabled: boolean; path: string }) {
|
||||
if (item.enabled) router.push(item.path);
|
||||
else window.$message?.warning('该功能暂未开放');
|
||||
}
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const [homeData, rankData] = await Promise.all([
|
||||
request<HomeData>({ url: '/v1/home' }),
|
||||
request<LeaderboardEntry[]>({ url: '/v1/leaderboard' }).catch(() => []),
|
||||
]);
|
||||
home.value = homeData;
|
||||
leaderboard.value = rankData || [];
|
||||
} catch (err) {
|
||||
error.value = errorText(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
onMounted(load);
|
||||
</script>
|
||||
Reference in New Issue
Block a user