feat mobile handmade mall grid
This commit is contained in:
+27
-132
@@ -3543,105 +3543,40 @@ body {
|
|||||||
/* Olons 2 manual task mall */
|
/* Olons 2 manual task mall */
|
||||||
.m-task-mall {
|
.m-task-mall {
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
padding: calc(18px + env(safe-area-inset-top)) 20px 32px;
|
padding: 0 12px 32px;
|
||||||
background-color: #fffaf4;
|
background-color: #fffaf4;
|
||||||
background-size: 100% auto;
|
background-size: 100% auto;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
.m-mall-search {
|
.m-task-mall > .m-page-header {
|
||||||
display: grid;
|
margin-inline: -2px;
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.m-mall-search h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
.m-mall-search label {
|
|
||||||
height: 48px;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 0 14px;
|
|
||||||
border-radius: 25px;
|
|
||||||
background: #fff;
|
|
||||||
color: #aaa;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.m-mall-search label .n-icon {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
.m-mall-search input {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
border: 0;
|
|
||||||
outline: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.m-mall-search > button,
|
|
||||||
.m-mall-claim {
|
|
||||||
border: 0;
|
|
||||||
color: #fff;
|
|
||||||
background: #ff8b16;
|
|
||||||
}
|
|
||||||
.m-mall-search > button {
|
|
||||||
min-width: 72px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
}
|
||||||
.m-mall-banner {
|
.m-mall-banner {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 18px;
|
margin: 0 0 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.m-mall-tabs {
|
|
||||||
min-height: 62px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
.m-mall-tabs button {
|
|
||||||
min-height: 44px;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
font-size: 19px;
|
|
||||||
}
|
|
||||||
.m-mall-tabs button.active {
|
|
||||||
color: #ff6d50;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.m-mall-tabs > .n-icon {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
.m-mall-list {
|
.m-mall-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px 6px;
|
||||||
}
|
}
|
||||||
.m-mall-task {
|
.m-mall-task {
|
||||||
min-height: 128px;
|
min-width: 0;
|
||||||
padding: 12px;
|
padding: 0 0 11px;
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 104px minmax(0, 1fr) auto;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.m-mall-cover {
|
|
||||||
width: 104px;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--m-ink);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #f0f0f0;
|
text-align: left;
|
||||||
position: relative;
|
}
|
||||||
|
.m-mall-cover {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: #f0f0f0;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
.m-mall-cover img {
|
.m-mall-cover img {
|
||||||
@@ -3653,45 +3588,22 @@ body {
|
|||||||
color: #bbb;
|
color: #bbb;
|
||||||
font-size: 38px;
|
font-size: 38px;
|
||||||
}
|
}
|
||||||
.m-mall-cover span {
|
.m-mall-task > strong {
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
padding: 4px;
|
|
||||||
background: rgba(109, 48, 11, 0.88);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.m-mall-task > div {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.m-mall-task h2 {
|
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
margin: 0 0 7px;
|
min-height: 2.5em;
|
||||||
|
margin: 8px 10px 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 17px;
|
font-size: clamp(15px, 4.3vw, 19px);
|
||||||
line-height: 1.35;
|
font-weight: 600;
|
||||||
|
line-height: 1.22;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 3;
|
||||||
}
|
}
|
||||||
.m-mall-task strong {
|
.m-mall-task > small {
|
||||||
display: block;
|
display: block;
|
||||||
color: #ff684d;
|
margin: 0 10px;
|
||||||
font-size: 21px;
|
color: #555;
|
||||||
}
|
font-size: 13px;
|
||||||
.m-mall-task strong small {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.m-mall-task p {
|
|
||||||
margin: 5px 0 0;
|
|
||||||
color: #b5b5b5;
|
|
||||||
}
|
|
||||||
.m-mall-claim {
|
|
||||||
min-width: 66px;
|
|
||||||
height: 38px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Olons 2 leaderboard */
|
/* Olons 2 leaderboard */
|
||||||
@@ -4028,23 +3940,6 @@ body {
|
|||||||
.m-leaderboard {
|
.m-leaderboard {
|
||||||
padding-inline: 14px;
|
padding-inline: 14px;
|
||||||
}
|
}
|
||||||
.m-mall-task {
|
|
||||||
grid-template-columns: 92px minmax(0, 1fr) auto;
|
|
||||||
gap: 9px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.m-mall-cover {
|
|
||||||
width: 92px;
|
|
||||||
}
|
|
||||||
.m-mall-search {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.m-mall-search h1 {
|
|
||||||
font-size: 23px;
|
|
||||||
}
|
|
||||||
.m-mall-search > button {
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
.m-claim-result {
|
.m-claim-result {
|
||||||
padding-inline: 20px;
|
padding-inline: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<section v-if="!isDetail" class="m-task-mall" :style="{ backgroundImage: `url(${mobileAssets.mallBackground})` }">
|
<section v-if="!isDetail" class="m-task-mall" :style="{ backgroundImage: `url(${mobileAssets.mallBackground})` }">
|
||||||
<header class="m-mall-search">
|
<PageHeader title="手工商城" />
|
||||||
<h1>商城</h1>
|
|
||||||
<label><n-icon :component="SearchOutline" /><input v-model.trim="keyword" placeholder="搜索商品" /></label>
|
|
||||||
<button type="button" @click="applySearch">搜索</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<img class="m-mall-banner" :src="mobileAssets.mallBanner" alt="精选任务,轻松赚收益" />
|
<img class="m-mall-banner" :src="mobileAssets.mallBanner" alt="精选任务,轻松赚收益" />
|
||||||
|
|
||||||
<div class="m-mall-tabs">
|
|
||||||
<button :class="{ active: tab === 'recommended' }" @click="tab = 'recommended'">推荐任务</button>
|
|
||||||
<button :class="{ active: tab === 'limited' }" @click="tab = 'limited'">限时任务</button>
|
|
||||||
<n-icon :component="ChevronForwardOutline" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AsyncState
|
<AsyncState
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:error="error"
|
:error="error"
|
||||||
:empty="!visibleProducts.length"
|
:empty="!products.length"
|
||||||
:empty-text="tab === 'limited' ? '暂无限时任务' : '暂无推荐任务'"
|
empty-text="暂无手工商品"
|
||||||
@retry="loadList"
|
@retry="loadList"
|
||||||
>
|
>
|
||||||
<div class="m-mall-list">
|
<div class="m-mall-list">
|
||||||
<article v-for="item in visibleProducts" :key="item.id" class="m-mall-task">
|
<button v-for="item in products" :key="item.id" class="m-mall-task" type="button" @click="openSupport">
|
||||||
<button class="m-mall-cover" @click="openProduct(item.id)">
|
<span class="m-mall-cover">
|
||||||
<img v-if="safeImage(item.cover_url)" :src="safeImage(item.cover_url)" :alt="item.name" />
|
<img v-if="safeImage(item.cover_url)" :src="safeImage(item.cover_url)" :alt="item.name" />
|
||||||
<n-icon v-else :component="ImageOutline" />
|
<n-icon v-else :component="ImageOutline" />
|
||||||
<span v-if="item.is_limited">限时展示</span>
|
</span>
|
||||||
|
<strong>{{ item.name }}</strong>
|
||||||
|
<small>已领取:{{ item.claimed_count || 0 }}</small>
|
||||||
</button>
|
</button>
|
||||||
<div>
|
|
||||||
<h2>{{ item.name }}</h2>
|
|
||||||
<strong>{{ money(item.price_amount, item.currency) }}<small>/件</small></strong>
|
|
||||||
<p>已领取{{ item.claimed_count || 0 }}</p>
|
|
||||||
</div>
|
|
||||||
<button class="m-mall-claim" @click="openProduct(item.id)">{{ tab === 'limited' ? '抢单' : '领取' }}</button>
|
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
</AsyncState>
|
</AsyncState>
|
||||||
</section>
|
</section>
|
||||||
@@ -76,7 +61,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { ChevronForwardOutline, ImageOutline, SearchOutline } from '@vicons/ionicons5';
|
import { ImageOutline } from '@vicons/ionicons5';
|
||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import AsyncState from '../components/AsyncState.vue';
|
import AsyncState from '../components/AsyncState.vue';
|
||||||
import { errorText, money, request, safeImage } from '../api';
|
import { errorText, money, request, safeImage } from '../api';
|
||||||
@@ -89,29 +74,9 @@
|
|||||||
const error = ref('');
|
const error = ref('');
|
||||||
const products = ref<any[]>([]);
|
const products = ref<any[]>([]);
|
||||||
const product = ref<any>();
|
const product = ref<any>();
|
||||||
const keyword = ref('');
|
|
||||||
const submittedKeyword = ref('');
|
|
||||||
const tab = ref<'recommended' | 'limited'>('recommended');
|
|
||||||
let listController: AbortController | undefined;
|
let listController: AbortController | undefined;
|
||||||
|
function openSupport() {
|
||||||
const isLimited = (item: any) => item.is_limited === true;
|
router.push({ name: 'MobileSupport' });
|
||||||
const visibleProducts = computed(() => {
|
|
||||||
let items = products.value;
|
|
||||||
if (tab.value === 'limited') {
|
|
||||||
items = items.filter(isLimited);
|
|
||||||
} else {
|
|
||||||
const recommended = items.filter((item) => item.is_recommended && !isLimited(item));
|
|
||||||
items = recommended.length ? recommended : items.filter((item) => !isLimited(item));
|
|
||||||
}
|
|
||||||
const query = submittedKeyword.value.toLocaleLowerCase('zh-CN');
|
|
||||||
return query ? items.filter((item) => String(item.name || '').toLocaleLowerCase('zh-CN').includes(query)) : items;
|
|
||||||
});
|
|
||||||
|
|
||||||
function applySearch() {
|
|
||||||
submittedKeyword.value = keyword.value;
|
|
||||||
}
|
|
||||||
function openProduct(id: number) {
|
|
||||||
router.push(`/app/mall/${id}`);
|
|
||||||
}
|
}
|
||||||
async function loadList() {
|
async function loadList() {
|
||||||
listController?.abort();
|
listController?.abort();
|
||||||
|
|||||||
Reference in New Issue
Block a user