购物车
This commit is contained in:
BIN
src/assets/images/shopping-cart-null.png
Normal file
BIN
src/assets/images/shopping-cart-null.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -53,9 +53,9 @@ export function generateUUID(): string {
|
|||||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
return crypto.randomUUID()
|
return crypto.randomUUID()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 备用方案:手动生成UUID v4
|
// 备用方案:手动生成UUID v4
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
const r = Math.random() * 16 | 0
|
const r = Math.random() * 16 | 0
|
||||||
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||||
return v.toString(16)
|
return v.toString(16)
|
||||||
@@ -70,39 +70,37 @@ export {
|
|||||||
|
|
||||||
/** 时间格式化-自定义格式
|
/** 时间格式化-自定义格式
|
||||||
* @param value 时间对象|时间戳|时间字符串
|
* @param value 时间对象|时间戳|时间字符串
|
||||||
* @param format 格式化字符串,默认值为 'yyyy-MM-dd HH:mm:ss'
|
* @param format 格式化字符串,默认值为 'YYYY-MM-DD HH:mm:ss'
|
||||||
* @returns 格式化后的时间字符串
|
* @returns 格式化后的时间字符串
|
||||||
*/
|
*/
|
||||||
export function FormatDate(value: Date | number | string, format: string = 'yyyy-MM-dd HH:mm:ss') {
|
export function FormatDate(value: Date | number | string, format: string = 'YYYY-MM-DD HH:mm:ss') {
|
||||||
const date = new Date(value);
|
const d = new Date(value);
|
||||||
const yyyy = String(date.getFullYear());
|
if (!d || isNaN(d.getTime())) return 'Invalid Date';
|
||||||
const yy = String(date.getFullYear()).slice(-2);
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
const MM = String(date.getMonth() + 1).padStart(2, '0');
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
const M = String(date.getMonth() + 1);
|
const tokens = {
|
||||||
const dd = String(date.getDate()).padStart(2, '0');
|
YYYY: d.getFullYear(),
|
||||||
const d = String(date.getDate());
|
YY: String(d.getFullYear()).slice(-2),
|
||||||
const HH = String(date.getHours()).padStart(2, '0');
|
MM: pad(d.getMonth() + 1),
|
||||||
const H = String(date.getHours());
|
M: d.getMonth() + 1,
|
||||||
const mm = String(date.getMinutes()).padStart(2, '0');
|
SM: months[d.getMonth()],
|
||||||
const m = String(date.getMinutes());
|
DD: pad(d.getDate()),
|
||||||
const ss = String(date.getSeconds()).padStart(2, '0');
|
D: d.getDate(),
|
||||||
const s = String(date.getSeconds());
|
HH: pad(d.getHours()),
|
||||||
const str = format.replaceAll('yyyy', yyyy)
|
H: d.getHours(),
|
||||||
.replaceAll('yy', yy)
|
hh: pad(d.getHours() % 12 || 12),
|
||||||
.replaceAll('MM', MM)
|
h: d.getHours() % 12 || 12,
|
||||||
.replaceAll('M', M)
|
mm: pad(d.getMinutes()),
|
||||||
.replaceAll('dd', dd)
|
m: d.getMinutes(),
|
||||||
.replaceAll('d', d)
|
ss: pad(d.getSeconds()),
|
||||||
.replaceAll('HH', HH)
|
s: d.getSeconds(),
|
||||||
.replaceAll('H', H)
|
A: d.getHours() < 12 ? 'AM' : 'PM',
|
||||||
.replaceAll('mm', mm)
|
a: d.getHours() < 12 ? 'am' : 'pm'
|
||||||
.replaceAll('m', m)
|
}
|
||||||
.replaceAll('ss', ss)
|
const reg = new RegExp(Object.keys(tokens).join('|'), 'g')
|
||||||
.replaceAll('s', s);
|
return format.replace(reg, match => tokens[match]);
|
||||||
return str;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载图片
|
* 下载图片
|
||||||
* @param list 图片列表
|
* @param list 图片列表
|
||||||
@@ -163,7 +161,7 @@ export function encryptPassword(password: string): string {
|
|||||||
* @param url 图片URL
|
* @param url 图片URL
|
||||||
* @returns 无
|
* @returns 无
|
||||||
*/
|
*/
|
||||||
export async function shareImageToWhatsapp (url: string){
|
export async function shareImageToWhatsapp(url: string) {
|
||||||
// 把图片 URL 转为 Blob
|
// 把图片 URL 转为 Blob
|
||||||
const blob = await fetch(url).then((res) => res.blob())
|
const blob = await fetch(url).then((res) => res.blob())
|
||||||
|
|
||||||
@@ -195,3 +193,19 @@ export function CountDown(time: number) {
|
|||||||
const ss = String(time % 60).padStart(2, '0');
|
const ss = String(time % 60).padStart(2, '0');
|
||||||
return `${mm}:${ss}`;
|
return `${mm}:${ss}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字节转换为可读格式
|
||||||
|
* @param {number} bytes - 字节数
|
||||||
|
* @param {number} decimals - 保留小数位数,默认2位
|
||||||
|
* @returns {string} 格式化后的字符串
|
||||||
|
*/
|
||||||
|
export function FormatBytes(bytes, decimals = 2) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
if (!bytes || isNaN(bytes)) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
const value = bytes / Math.pow(k, i);
|
||||||
|
return `${Number(value.toFixed(decimals))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="shopping-cart">
|
<div class="shopping-cart">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<sc-list />
|
<sc-list @selected-change="(v) => (selectedList = v)" />
|
||||||
<sc-list is-mini style="flex: 0.6; height: var(--app-view-height)" />
|
<order-summary :brands-list="selectedList" />
|
||||||
<!-- <order-summary /> -->
|
|
||||||
</div>
|
</div>
|
||||||
<my-footer />
|
<my-footer />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, defineComponent, onActivated } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import orderSummary from './order-summary.vue'
|
import orderSummary from './order-summary.vue'
|
||||||
import scList from './sc-list.vue'
|
import scList from './sc-list.vue'
|
||||||
import myFooter from '@/components/Footer.vue'
|
import myFooter from '@/components/Footer.vue'
|
||||||
|
const selectedList = ref([])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|||||||
@@ -10,12 +10,9 @@
|
|||||||
<span class="icon"><svg-icon name="order-shop" size="18" /></span>
|
<span class="icon"><svg-icon name="order-shop" size="18" /></span>
|
||||||
<span class="text">Brands</span>
|
<span class="text">Brands</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="brands-item" v-for="v in brandsList" :key="v.name">
|
<div class="brands-item" v-for="v in brandsList" :key="v.brand">
|
||||||
<span class="label">{{ v.name }}</span>
|
<span class="label">{{ v.brand }}</span>
|
||||||
<span class="value"
|
<span class="value"><span>1</span>item</span>
|
||||||
><span>{{ v.count }}</span
|
|
||||||
>item</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="total-file-size">
|
<div class="total-file-size">
|
||||||
@@ -23,36 +20,47 @@
|
|||||||
<span class="icon"><svg-icon name="order-file" size="18" /></span>
|
<span class="icon"><svg-icon name="order-file" size="18" /></span>
|
||||||
<span class="text">Total File Size</span>
|
<span class="text">Total File Size</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="value">36 <span>mb</span></span>
|
<span class="value"
|
||||||
|
>{{ totalSize.size }} <span>{{ totalSize.unit }}</span></span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="hr"></div>
|
<div class="hr"></div>
|
||||||
<br />
|
<br />
|
||||||
<div class="total">
|
<div class="total">
|
||||||
<span class="label">Total</span>
|
<span class="label">Total</span>
|
||||||
<span class="value"><span>$45.00</span> HKD</span>
|
<span class="value"
|
||||||
|
><span>${{ totalAmount }}</span> HKD</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="hr"></div>
|
<div class="hr"></div>
|
||||||
<button class="checkout-btn" custom="black">CHECKOUT SELECTED</button>
|
<button class="checkout-btn" custom="black" @click="handleCheckout">CHECKOUT SELECTED</button>
|
||||||
<div class="tip">Digital assets. Creator retains copyright.</div>
|
<div class="tip">Digital assets. Creator retains copyright.</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
const brandsList = ref([
|
import { FormatBytes, FormatDate } from '@/utils/tools'
|
||||||
{
|
const props = defineProps({
|
||||||
name: 'Roaming Clouds',
|
brandsList: {
|
||||||
count: 1
|
type: Array,
|
||||||
},
|
default: () => []
|
||||||
{
|
|
||||||
name: 'Off Grid Apparel',
|
|
||||||
count: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Ivory Muse Studio',
|
|
||||||
count: 1
|
|
||||||
}
|
}
|
||||||
])
|
})
|
||||||
|
const totalSize = computed(() => {
|
||||||
|
const total = props.brandsList.reduce((pre, cur) => pre + cur.fileSize, 0)
|
||||||
|
const str = FormatBytes(total)
|
||||||
|
return {
|
||||||
|
size: str.split(' ')[0],
|
||||||
|
unit: str.split(' ')[1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const totalAmount = computed(() =>
|
||||||
|
props.brandsList.reduce((pre, cur) => pre + cur.amount, 0).toFixed(2)
|
||||||
|
)
|
||||||
|
const handleCheckout = () => {
|
||||||
|
console.log('购买:', props.brandsList)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|||||||
@@ -10,11 +10,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="options" v-if="!isMini">
|
<div class="options" v-if="!isMini">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<el-checkbox v-model="check" :indeterminate="true" />
|
<el-checkbox
|
||||||
<span class="count">3 Selected</span>
|
:model-value="allSelected"
|
||||||
|
:indeterminate="selectedCount === 0 ? false : selectedCount < list.length"
|
||||||
|
@click="handleAllAllClick"
|
||||||
|
/>
|
||||||
|
<span class="count">{{ selectedCount }} Selected</span>
|
||||||
<div class="hr"></div>
|
<div class="hr"></div>
|
||||||
<div class="btn">Select All</div>
|
<div class="btn" @click="handleAllAllClick(true)">Select All</div>
|
||||||
<div class="btn">Deselect All</div>
|
<div class="btn" @click="handleAllAllClick(false)">Deselect All</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<el-select v-model="sortBy" placeholder="Sort By" :teleported="false">
|
<el-select v-model="sortBy" placeholder="Sort By" :teleported="false">
|
||||||
@@ -33,8 +37,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list">
|
<div class="list">
|
||||||
|
<div class="list-null" v-show="list.length === 0">
|
||||||
|
<img src="@/assets/images/shopping-cart-null.png" alt="" />
|
||||||
|
<div class="title">Your Cart is empty</div>
|
||||||
|
<div class="tip">Discover new fashion assets and add them to your cart.</div>
|
||||||
|
<button custom v-if="!isMini" @click="handleExploreClick">EXPLORE DIGITAL ITEMS</button>
|
||||||
|
</div>
|
||||||
<div class="item" v-for="v in list" :key="v.id">
|
<div class="item" v-for="v in list" :key="v.id">
|
||||||
<el-checkbox v-model="v.checked" v-if="!isMini" />
|
<el-checkbox v-model="v.checked" v-if="!isMini" @change="handleSelectedChange" />
|
||||||
<img :src="v.url" />
|
<img :src="v.url" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="title">{{ v.title }}</div>
|
<div class="title">{{ v.title }}</div>
|
||||||
@@ -48,9 +58,12 @@
|
|||||||
<div class="size">
|
<div class="size">
|
||||||
<div class="icon"><svg-icon name="order-file" size="18" /></div>
|
<div class="icon"><svg-icon name="order-file" size="18" /></div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
{{ v.fileSize }}kb{{
|
<span>{{ FormatBytes(v.fileSize) }}</span>
|
||||||
isMini ? '' : ` | ${v.date}`
|
<span v-if="!isMini"
|
||||||
}}
|
> | {{
|
||||||
|
FormatDate(v.date, 'SM D, YYYY, h:mm A')
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer" v-if="isMini">
|
<div class="footer" v-if="isMini">
|
||||||
<div class="total">
|
<div class="total" v-show="list.length > 0">
|
||||||
<span class="label">Total</span>
|
<span class="label">Total</span>
|
||||||
<span class="value">$45.00<span> HKD</span></span>
|
<span class="value">$45.00<span> HKD</span></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,16 +87,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, onMounted } from 'vue'
|
||||||
const emit = defineEmits(['close'])
|
import { FormatBytes, FormatDate } from '@/utils/tools'
|
||||||
|
const emit = defineEmits(['close', 'selected-change'])
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isMini: {
|
isMini: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const check = ref(true)
|
const selectedCount = computed(() => list.value.filter((v) => v.checked).length)
|
||||||
|
const allSelected = computed(() => list.value.every((v) => v.checked))
|
||||||
const sortBy = ref('')
|
const sortBy = ref('')
|
||||||
const sortByOptions = ref([
|
const sortByOptions = ref([
|
||||||
{
|
{
|
||||||
@@ -107,7 +121,7 @@
|
|||||||
title: 'North Outfit Set',
|
title: 'North Outfit Set',
|
||||||
brand: 'Roaming Clouds',
|
brand: 'Roaming Clouds',
|
||||||
fileSize: 1024, // kb
|
fileSize: 1024, // kb
|
||||||
date: 'Feb 16, 2026, 11:34 PM',
|
date: '2026-5-20 5:20',
|
||||||
amount: 49.99,
|
amount: 49.99,
|
||||||
tags: ['female', 'skirt', 'blouse', 'outwear'],
|
tags: ['female', 'skirt', 'blouse', 'outwear'],
|
||||||
checked: true
|
checked: true
|
||||||
@@ -117,16 +131,31 @@
|
|||||||
url: 'http://118.31.39.42:3000/falls/shopping-cart-2.png',
|
url: 'http://118.31.39.42:3000/falls/shopping-cart-2.png',
|
||||||
title: 'Weekend Drift Co-ord',
|
title: 'Weekend Drift Co-ord',
|
||||||
brand: 'Urban Line Edit',
|
brand: 'Urban Line Edit',
|
||||||
fileSize: 100, // kb
|
fileSize: 1225, // kb
|
||||||
date: 'Feb 16, 2026, 11:34 PM',
|
date: '2026-5-21 13:14',
|
||||||
amount: 9.99,
|
amount: 9.99,
|
||||||
tags: ['female', 'skirt', 'blouse', 'outwear'],
|
tags: ['female', 'skirt', 'blouse', 'outwear'],
|
||||||
checked: false
|
checked: false
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
const handleAllAllClick = (checked?: boolean) => {
|
||||||
|
const checked_ = typeof checked === 'boolean' ? checked : !allSelected.value
|
||||||
|
list.value.forEach((v) => (v.checked = checked_))
|
||||||
|
handleSelectedChange()
|
||||||
|
}
|
||||||
|
const handleSelectedChange = () => {
|
||||||
|
const arr = list.value.filter((v) => v.checked)
|
||||||
|
emit('selected-change', arr)
|
||||||
|
}
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
handleSelectedChange()
|
||||||
|
})
|
||||||
|
const handleExploreClick = () => {
|
||||||
|
console.log('探索')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@@ -181,6 +210,11 @@
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
> .list {
|
||||||
|
> .list-null {
|
||||||
|
margin-top: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
> .header {
|
> .header {
|
||||||
border-bottom: 0.1rem solid #c4c4c4;
|
border-bottom: 0.1rem solid #c4c4c4;
|
||||||
@@ -232,6 +266,40 @@
|
|||||||
}
|
}
|
||||||
> .list {
|
> .list {
|
||||||
// height: 1000px;
|
// height: 1000px;
|
||||||
|
> .list-null {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
> img {
|
||||||
|
width: 14rem;
|
||||||
|
height: auto;
|
||||||
|
margin-bottom: 2.4rem;
|
||||||
|
}
|
||||||
|
> .title {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: #979797;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
}
|
||||||
|
> .tip {
|
||||||
|
width: 50%;
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #979797;
|
||||||
|
}
|
||||||
|
> button {
|
||||||
|
min-width: 30rem;
|
||||||
|
height: 4.4rem;
|
||||||
|
border: 0.1rem solid #c4c4c4;
|
||||||
|
font-family: KaiseiOpti-Medium;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: #979797;
|
||||||
|
margin-top: 4.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
> .item {
|
> .item {
|
||||||
border-bottom: 0.1rem solid #c4c4c4;
|
border-bottom: 0.1rem solid #c4c4c4;
|
||||||
padding: var(--sc-list-list-item-padding, 2.4rem 0);
|
padding: var(--sc-list-list-item-padding, 2.4rem 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user