商城c端前端
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<section class="m-claim2">
|
||||
<AsyncState :loading="loading" :error="error" @retry="load">
|
||||
<div class="m-claim2-hero">
|
||||
<img :src="mobileAssets.claimMachine" alt="每日参与任务,赚取更多佣金" />
|
||||
<button
|
||||
type="button"
|
||||
:disabled="claiming || !halls.length || eligibility?.eligible === false"
|
||||
aria-label="自动领取任务"
|
||||
@click="claim"
|
||||
>
|
||||
<img :src="mobileAssets.claimAutoButton" alt="自动领取" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="m-claim2-content">
|
||||
<section class="m-claim2-stats">
|
||||
<div v-for="item in statCards" :key="item.label">
|
||||
<img :src="item.icon" alt="" />
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="eligibility?.eligible === false" class="m-claim2-warning">
|
||||
<n-icon :component="AlertCircleOutline" />
|
||||
<span>{{ eligibility.message || '暂不满足领取条件' }}</span>
|
||||
<button v-if="eligibility.pending_order_id" type="button" @click="openPendingOrder">查看任务</button>
|
||||
</div>
|
||||
|
||||
<section class="m-claim2-rules">
|
||||
<h2><span></span>活动规则<span></span></h2>
|
||||
<ol>
|
||||
<li>平台运营时间每天 {{ businessHours }}。</li>
|
||||
<li>每一次储入请先咨询客服获取商家卡号,如有疑问请联系在线客服。</li>
|
||||
<li>领取后系统将自动分配任务,请按订单要求完成任务内容。</li>
|
||||
<li>领取时间根据会员等级不同,系统随机匹配订单。</li>
|
||||
<li>账户可用余额需达到 {{ money(eligibility?.minimum_amount) }} 才可领取。</li>
|
||||
<li>订单需要在规定时间内完成,请勿超时提交。</li>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</AsyncState>
|
||||
|
||||
<n-modal v-model:show="showResult" :mask-closable="false">
|
||||
<section class="m-claim-result" :class="`is-${resultState}`">
|
||||
<template v-if="resultState === 'matching'">
|
||||
<h2>正在匹配任务</h2>
|
||||
<p>{{ matchingHint }}</p>
|
||||
<div class="m-match-visual">
|
||||
<img class="orbit" :src="mobileAssets.claimSpinner" alt="" />
|
||||
<img class="gift" :src="mobileAssets.claimMatchingGift" alt="" />
|
||||
</div>
|
||||
<div class="m-match-progress"><i :style="{ width: `${matchProgress}%` }"></i></div>
|
||||
<strong class="m-match-percent">{{ matchProgress }}%</strong>
|
||||
<ol class="m-match-steps">
|
||||
<li v-for="(step, index) in matchSteps" :key="step" :class="{ active: index <= matchStep }">
|
||||
<span>✓</span>{{ step }}
|
||||
</li>
|
||||
</ol>
|
||||
<button class="m-result-secondary" @click="openOrders">后台匹配,去记录查看</button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<img class="m-result-icon" :src="resultIcon" alt="" />
|
||||
<h2>{{ resultTitle }}</h2>
|
||||
<p>{{ resultDescription }}</p>
|
||||
|
||||
<div v-if="order?.product?.name" class="m-result-product">
|
||||
<div>
|
||||
<img v-if="safeImage(order.product.cover_url)" :src="safeImage(order.product.cover_url)" alt="" />
|
||||
<n-icon v-else :component="ImageOutline" />
|
||||
</div>
|
||||
<section><small>任务商品</small><b>{{ order.product.name }}</b><strong>{{ money(order.total_amount, order.currency) }}</strong></section>
|
||||
</div>
|
||||
|
||||
<dl v-if="resultState === 'success'" class="m-result-details">
|
||||
<div><dt>预计佣金</dt><dd>{{ money(order?.commission_amount, order?.currency) }}</dd></div>
|
||||
<div><dt>佣金比例</dt><dd>{{ commissionRate }}</dd></div>
|
||||
<div><dt>任务编号</dt><dd>{{ order?.order_no || '--' }}</dd></div>
|
||||
<div><dt>任务时间</dt><dd>{{ dateTime(order?.created_at) }}</dd></div>
|
||||
</dl>
|
||||
|
||||
<div v-if="resultState === 'failed'" class="m-result-reason">
|
||||
<b>失败原因</b><span>{{ failureMessage }}</span>
|
||||
</div>
|
||||
|
||||
<div class="m-result-actions">
|
||||
<button v-if="resultState !== 'success'" class="m-result-secondary" @click="retryClaim">重新领取</button>
|
||||
<button class="m-result-primary" @click="primaryResultAction">{{ resultState === 'success' ? '查看记录' : '返回首页' }}</button>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</n-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { AlertCircleOutline, ImageOutline } from '@vicons/ionicons5';
|
||||
import AsyncState from '../components/AsyncState.vue';
|
||||
import { dateTime, errorText, money, request, safeImage } from '../api';
|
||||
import { mobileAssets } from '../assets';
|
||||
|
||||
type ResultState = 'matching' | 'success' | 'failed' | 'cancelled';
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(true);
|
||||
const claiming = ref(false);
|
||||
const error = ref('');
|
||||
const halls = ref<any[]>([]);
|
||||
const stats = ref<any>();
|
||||
const eligibility = ref<any>();
|
||||
const order = ref<any>();
|
||||
const showResult = ref(false);
|
||||
const resultState = ref<ResultState>('matching');
|
||||
const failureMessage = ref('当前任务库存未分配成功,请稍后重试');
|
||||
const matchProgress = ref(18);
|
||||
const pollFinished = ref(false);
|
||||
let pollTimer: number | undefined;
|
||||
let progressTimer: number | undefined;
|
||||
let pollCount = 0;
|
||||
|
||||
const statCards = computed(() => [
|
||||
{ label: '总资产', value: money(stats.value?.total_asset_amount), icon: mobileAssets.claimTotal },
|
||||
{ label: '昨日佣金', value: money(stats.value?.yesterday_commission_amount), icon: mobileAssets.claimYesterday },
|
||||
{ label: '今日佣金', value: money(stats.value?.today_commission_amount), icon: mobileAssets.claimToday },
|
||||
{ label: '今日领取数', value: stats.value?.today_claim_count ?? 0, icon: mobileAssets.claimCount },
|
||||
]);
|
||||
const businessHours = computed(() => {
|
||||
const start = String(eligibility.value?.business_start_at || '09:00').slice(0, 5);
|
||||
const end = String(eligibility.value?.business_end_at || '21:00').slice(0, 5);
|
||||
return `${start}-${end}`;
|
||||
});
|
||||
const matchStep = computed(() => (matchProgress.value < 35 ? 0 : matchProgress.value < 60 ? 1 : matchProgress.value < 85 ? 2 : 3));
|
||||
const matchSteps = ['正在匹配任务', '获取商品信息', '确认任务内容', '发放任务结果'];
|
||||
const matchingHint = computed(() => pollFinished.value ? '任务仍在匹配,可稍后前往记录查看' : '系统正在为您匹配任务,请稍等...');
|
||||
const resultIcon = computed(() => resultState.value === 'success' ? mobileAssets.claimSuccess : resultState.value === 'failed' ? mobileAssets.claimFailed : mobileAssets.claimCancelled);
|
||||
const resultTitle = computed(() => resultState.value === 'success' ? '任务获取成功' : resultState.value === 'failed' ? '领取失败' : '已取消任务');
|
||||
const resultDescription = computed(() => resultState.value === 'success' ? '恭喜你成功领取任务' : resultState.value === 'failed' ? '本次任务领取未成功,请稍后再试' : '本次任务已取消,可重新领取');
|
||||
const commissionRate = computed(() => {
|
||||
const total = Number(order.value?.total_amount || 0);
|
||||
return total > 0 ? `${((Number(order.value?.commission_amount || 0) / total) * 100).toFixed(2)}%` : '--';
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const [hallData, statData] = await Promise.all([
|
||||
request<any[]>({ url: '/v1/task-halls' }),
|
||||
request<any>({ url: '/v1/tasks/stats' }),
|
||||
]);
|
||||
halls.value = hallData || [];
|
||||
stats.value = statData;
|
||||
if (halls.value.length) await loadEligibility(halls.value[0].id);
|
||||
} catch (err) {
|
||||
error.value = errorText(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEligibility(hallID: number) {
|
||||
eligibility.value = await request({ url: '/v1/tasks/eligibility', params: { hall_id: hallID } });
|
||||
}
|
||||
|
||||
async function claim() {
|
||||
if (claiming.value || !halls.value.length) return;
|
||||
claiming.value = true;
|
||||
pollFinished.value = false;
|
||||
try {
|
||||
await loadEligibility(halls.value[0].id);
|
||||
if (!eligibility.value?.eligible) {
|
||||
window.$message.warning(eligibility.value?.message || '暂不满足领取条件');
|
||||
return;
|
||||
}
|
||||
openMatching();
|
||||
order.value = await request<any>({
|
||||
url: '/v1/task-orders/claim',
|
||||
method: 'POST',
|
||||
data: { hall_id: halls.value[0].id },
|
||||
});
|
||||
handleOrder(order.value);
|
||||
} catch (err) {
|
||||
failureMessage.value = errorText(err);
|
||||
resultState.value = 'failed';
|
||||
showResult.value = true;
|
||||
stopPolling();
|
||||
} finally {
|
||||
claiming.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openMatching() {
|
||||
stopPolling();
|
||||
resultState.value = 'matching';
|
||||
showResult.value = true;
|
||||
matchProgress.value = 18;
|
||||
pollCount = 0;
|
||||
progressTimer = window.setInterval(() => {
|
||||
matchProgress.value = Math.min(88, matchProgress.value + Math.ceil(Math.random() * 5));
|
||||
}, 700);
|
||||
}
|
||||
|
||||
function handleOrder(value: any) {
|
||||
order.value = value;
|
||||
if (value?.status === 'matching') {
|
||||
schedulePoll();
|
||||
} else if (['pending', 'processing', 'completed'].includes(value?.status)) {
|
||||
matchProgress.value = 100;
|
||||
resultState.value = 'success';
|
||||
stopPolling();
|
||||
} else if (value?.status === 'cancelled') {
|
||||
resultState.value = 'cancelled';
|
||||
stopPolling();
|
||||
} else {
|
||||
failureMessage.value = value?.message || failureMessage.value;
|
||||
resultState.value = 'failed';
|
||||
stopPolling();
|
||||
}
|
||||
}
|
||||
|
||||
function schedulePoll() {
|
||||
if (!order.value?.id) return;
|
||||
pollTimer = window.setTimeout(async () => {
|
||||
pollCount += 1;
|
||||
try {
|
||||
handleOrder(await request({ url: `/v1/task-orders/${order.value.id}` }));
|
||||
} catch (err) {
|
||||
if (pollCount >= 10) {
|
||||
pollFinished.value = true;
|
||||
stopPolling();
|
||||
} else schedulePoll();
|
||||
}
|
||||
if (resultState.value === 'matching' && pollCount >= 10) {
|
||||
pollFinished.value = true;
|
||||
stopPolling();
|
||||
}
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
function stopPolling(clearProgress = true) {
|
||||
if (pollTimer) window.clearTimeout(pollTimer);
|
||||
if (clearProgress && progressTimer) window.clearInterval(progressTimer);
|
||||
pollTimer = undefined;
|
||||
if (clearProgress) progressTimer = undefined;
|
||||
}
|
||||
|
||||
function closeResult() {
|
||||
showResult.value = false;
|
||||
stopPolling();
|
||||
}
|
||||
function openOrders() {
|
||||
closeResult();
|
||||
router.push('/app/orders');
|
||||
}
|
||||
function openPendingOrder() {
|
||||
const orderID = Number(eligibility.value?.pending_order_id || 0);
|
||||
router.push(orderID > 0 ? `/app/orders/${orderID}` : '/app/orders');
|
||||
}
|
||||
function retryClaim() {
|
||||
closeResult();
|
||||
void claim();
|
||||
}
|
||||
function primaryResultAction() {
|
||||
closeResult();
|
||||
router.push(resultState.value === 'success' ? `/app/orders/${order.value?.id}` : '/app/home');
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
onBeforeUnmount(() => stopPolling());
|
||||
</script>
|
||||
Reference in New Issue
Block a user