小程序初始提交
This commit is contained in:
199
cool-unix/pages/template/shop/address-edit.uvue
Normal file
199
cool-unix/pages/template/shop/address-edit.uvue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<cl-page>
|
||||
<view class="p-3">
|
||||
<view class="p-4 bg-white rounded-2xl dark:!bg-surface-800 mb-3">
|
||||
<cl-form ref="formRef" v-model="formData" :rules="rules" :disabled="saving">
|
||||
<cl-form-item :label="t('收货人')" prop="contact" required>
|
||||
<cl-input
|
||||
v-model="formData.contact"
|
||||
:placeholder="t('请输入收货人姓名')"
|
||||
></cl-input>
|
||||
</cl-form-item>
|
||||
|
||||
<cl-form-item :label="t('手机号')" prop="phone" required>
|
||||
<cl-input
|
||||
v-model="formData.phone"
|
||||
:placeholder="t('请输入手机号')"
|
||||
:maxlength="11"
|
||||
type="number"
|
||||
></cl-input>
|
||||
</cl-form-item>
|
||||
|
||||
<cl-form-item :label="t('地区')" prop="province" required>
|
||||
<cl-cascader
|
||||
v-model="regions"
|
||||
:placeholder="t('选择省市区')"
|
||||
:options="pcaOptions"
|
||||
@change="onRegionsChange"
|
||||
></cl-cascader>
|
||||
</cl-form-item>
|
||||
|
||||
<cl-form-item :label="t('详细地址')" prop="address" required>
|
||||
<cl-input
|
||||
v-model="formData.address"
|
||||
:placeholder="t('小区楼栋、门牌号、村等')"
|
||||
></cl-input>
|
||||
</cl-form-item>
|
||||
</cl-form>
|
||||
</view>
|
||||
|
||||
<cl-list>
|
||||
<cl-list-item :label="t('默认地址')">
|
||||
<cl-switch v-model="formData.isDefault"></cl-switch>
|
||||
</cl-list-item>
|
||||
</cl-list>
|
||||
</view>
|
||||
|
||||
<cl-footer>
|
||||
<cl-button @tap="save()">{{ t("保存") }}</cl-button>
|
||||
</cl-footer>
|
||||
</cl-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { router, isEmpty, type Response, request, parse } from "@/cool";
|
||||
import { t } from "@/locale";
|
||||
import { useCascader, useForm, useUi, type ClFormRule } from "@/uni_modules/cool-ui";
|
||||
import { type Ref, ref } from "vue";
|
||||
import pca from "@/data/pca.json";
|
||||
import type { UserAddress } from "../types";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
const ui = useUi();
|
||||
|
||||
const { formRef, validate } = useForm();
|
||||
|
||||
// 省市区级联选项数据
|
||||
const pcaOptions = useCascader(pca);
|
||||
|
||||
// 地区选择的值,格式为 [省, 市, 区]
|
||||
const regions = ref<string[]>([]);
|
||||
|
||||
// 表单数据,包含收货人、手机号、地区、详细地址、是否默认等字段
|
||||
const formData = ref<UserAddress>({
|
||||
contact: "",
|
||||
phone: "",
|
||||
province: "",
|
||||
city: "",
|
||||
district: "",
|
||||
address: "",
|
||||
isDefault: false
|
||||
}) as Ref<UserAddress>;
|
||||
|
||||
// 表单验证规则,校验收货人、手机号、详细地址、地区等必填项
|
||||
const rules = new Map<string, ClFormRule[]>([
|
||||
["contact", [{ required: true, message: t("收货人不能为空") }]],
|
||||
[
|
||||
"phone",
|
||||
[
|
||||
{ required: true, message: t("手机号不能为空") },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: t("手机号格式不正确") }
|
||||
]
|
||||
],
|
||||
["address", [{ required: true, message: t("详细地址不能为空") }]],
|
||||
["province", [{ required: true, message: t("所在地区不能为空") }]]
|
||||
]);
|
||||
|
||||
// 保存按钮loading状态
|
||||
const saving = ref(false);
|
||||
|
||||
/**
|
||||
* 保存地址信息
|
||||
* 1. 校验表单
|
||||
* 2. 组装数据
|
||||
* 3. 请求后端接口,新增或更新地址
|
||||
*/
|
||||
function save() {
|
||||
validate((valid, errors) => {
|
||||
if (valid) {
|
||||
ui.showLoading(t("保存中"));
|
||||
|
||||
// 解构地区信息
|
||||
const [province, city, district] = regions.value;
|
||||
|
||||
saving.value = true;
|
||||
|
||||
// 合并表单数据和地区信息
|
||||
const data = {
|
||||
...formData.value,
|
||||
province,
|
||||
city,
|
||||
district
|
||||
};
|
||||
|
||||
// 根据是否有id判断是新增还是编辑
|
||||
request({
|
||||
url: `/app/user/address/${props.id != "" ? "update" : "add"}`,
|
||||
method: "POST",
|
||||
data
|
||||
})
|
||||
.then(() => {
|
||||
// 保存成功返回上一页
|
||||
router.back();
|
||||
})
|
||||
.catch((err) => {
|
||||
// 保存失败提示错误信息
|
||||
ui.showToast({ message: (err as Response).message! });
|
||||
})
|
||||
.finally(() => {
|
||||
ui.hideLoading();
|
||||
saving.value = false;
|
||||
});
|
||||
} else {
|
||||
// 校验失败提示第一个错误
|
||||
ui.showToast({ message: errors[0].message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地址详情(编辑时调用)
|
||||
* 1. 请求后端接口获取地址详情
|
||||
* 2. 回填表单和地区选择
|
||||
*/
|
||||
function getInfo() {
|
||||
request({
|
||||
url: "/app/user/address/info",
|
||||
data: { id: props.id }
|
||||
})
|
||||
.then((res) => {
|
||||
if (res != null) {
|
||||
// 解析并赋值表单数据
|
||||
formData.value = parse<UserAddress>(res)!;
|
||||
// 回填地区选择
|
||||
regions.value = [
|
||||
formData.value.province,
|
||||
formData.value.city,
|
||||
formData.value.district
|
||||
];
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
ui.showToast({ message: (err as Response).message! });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 地区选择变化时触发
|
||||
* @param value 选中的地区数组 [省, 市, 区]
|
||||
*/
|
||||
function onRegionsChange(value: string[]) {
|
||||
const [province, city, district] = isEmpty(value) ? ["", "", ""] : value;
|
||||
|
||||
formData.value.province = province;
|
||||
formData.value.city = city;
|
||||
formData.value.district = district;
|
||||
}
|
||||
|
||||
onReady(() => {
|
||||
if (props.id != "") {
|
||||
getInfo();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
221
cool-unix/pages/template/shop/address.uvue
Normal file
221
cool-unix/pages/template/shop/address.uvue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<cl-page>
|
||||
<view class="p-3">
|
||||
<view
|
||||
class="flex flex-col bg-white rounded-2xl p-4 mb-3 dark:!bg-surface-800"
|
||||
:class="{
|
||||
'!mb-0': index == addressList.length - 1
|
||||
}"
|
||||
v-for="(item, index) in addressList"
|
||||
:key="item.id"
|
||||
>
|
||||
<view class="flex flex-col">
|
||||
<cl-text color="info" :pt="{ className: '!text-sm' }"
|
||||
>{{ item.province }} {{ item.city }} {{ item.district }}</cl-text
|
||||
>
|
||||
|
||||
<cl-text
|
||||
:pt="{
|
||||
className: 'my-1'
|
||||
}"
|
||||
>{{ item.address }}</cl-text
|
||||
>
|
||||
|
||||
<view class="flex flex-row">
|
||||
<cl-text :pt="{ className: '!text-sm' }">{{ item.contact }}</cl-text>
|
||||
<cl-text color="info" :pt="{ className: 'ml-3 !text-sm' }">{{
|
||||
item.phone
|
||||
}}</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="flex flex-row border border-solid border-gray-100 border-b-0 border-l-0 border-r-0 pt-4 mt-4 dark:!border-surface-700"
|
||||
>
|
||||
<cl-radio
|
||||
v-model="defaultId"
|
||||
active-icon="checkbox-circle-fill"
|
||||
inactive-icon="checkbox-blank-circle-line"
|
||||
:pt="{
|
||||
className: 'max-w-[300rpx]',
|
||||
label: {
|
||||
className: '!text-sm'
|
||||
},
|
||||
icon: {
|
||||
size: 32
|
||||
}
|
||||
}"
|
||||
:value="item.id"
|
||||
@change="onDefaultChange(item)"
|
||||
>{{ item.isDefault ? t("已设为默认") : t("设为默认") }}</cl-radio
|
||||
>
|
||||
|
||||
<view
|
||||
class="flex flex-row items-center justify-center ml-auto"
|
||||
@tap="onDelete(item.id!)"
|
||||
>
|
||||
<cl-icon name="delete-bin-line" :size="28"></cl-icon>
|
||||
<cl-text :pt="{ className: 'ml-2 !text-sm' }">{{ t("删除") }}</cl-text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="flex flex-row items-center justify-center ml-6"
|
||||
@tap="toEdit(item.id!)"
|
||||
>
|
||||
<cl-icon name="edit-line" :size="28"></cl-icon>
|
||||
<cl-text :pt="{ className: 'ml-2 !text-sm' }">{{ t("修改") }}</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<cl-empty v-if="list.length == 0"></cl-empty>
|
||||
</view>
|
||||
|
||||
<cl-footer>
|
||||
<cl-button @tap="toAdd()">{{ t("添加地址") }}</cl-button>
|
||||
</cl-footer>
|
||||
</cl-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUi } from "@/uni_modules/cool-ui";
|
||||
import { parse, request, router, usePager, type Response } from "@/cool";
|
||||
import { t } from "@/locale";
|
||||
import { computed, ref } from "vue";
|
||||
import type { UserAddress } from "../types";
|
||||
|
||||
const ui = useUi();
|
||||
|
||||
const { refresh, list, loadMore } = usePager(async (data, { render }) => {
|
||||
await request({
|
||||
url: "/app/user/address/page",
|
||||
method: "POST",
|
||||
data
|
||||
})
|
||||
.then((res) => {
|
||||
if (res != null) {
|
||||
render(res);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
ui.showToast({
|
||||
message: (err as Response).message!
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
ui.hideLoading();
|
||||
});
|
||||
});
|
||||
|
||||
// 默认地址id
|
||||
const defaultId = ref<number>(0);
|
||||
|
||||
// 地址列表数据
|
||||
const addressList = computed(() =>
|
||||
list.value.map((e) => {
|
||||
e["isDefault"] = e["isDefault"] == 1 ? true : false;
|
||||
|
||||
const d = parse<UserAddress>(e)!;
|
||||
|
||||
if (d.isDefault) {
|
||||
defaultId.value = d.id!;
|
||||
}
|
||||
|
||||
return d;
|
||||
})
|
||||
);
|
||||
|
||||
// 添加地址
|
||||
function toAdd() {
|
||||
router.to("/pages/template/shop/address-edit");
|
||||
}
|
||||
|
||||
// 编辑地址
|
||||
function toEdit(id: number) {
|
||||
router.push({
|
||||
path: "/pages/template/shop/address-edit",
|
||||
query: { id }
|
||||
});
|
||||
}
|
||||
|
||||
// 删除地址
|
||||
function onDelete(id: number) {
|
||||
ui.showConfirm({
|
||||
title: t("提示"),
|
||||
message: t("删除地址后无法恢复,确认要删除该地址吗?"),
|
||||
callback: (action) => {
|
||||
if (action == "confirm") {
|
||||
request({
|
||||
url: "/app/user/address/delete",
|
||||
method: "POST",
|
||||
data: { ids: [id] }
|
||||
})
|
||||
.then(() => {
|
||||
ui.showToast({
|
||||
message: t("删除成功")
|
||||
});
|
||||
|
||||
refresh({});
|
||||
})
|
||||
.catch((err) => {
|
||||
ui.showToast({
|
||||
message: (err as Response).message!
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 设为默认地址
|
||||
function onDefaultChange(item: UserAddress) {
|
||||
// 遍历地址列表,设置选中的地址为默认地址,其他地址取消默认
|
||||
addressList.value.forEach((e) => {
|
||||
if (e.id == item.id) {
|
||||
// 切换当前地址的默认状态
|
||||
e.isDefault = !e.isDefault;
|
||||
|
||||
// 如果取消了默认,则重置默认地址ID
|
||||
if (!e.isDefault) {
|
||||
defaultId.value = 0;
|
||||
}
|
||||
} else {
|
||||
// 其他地址全部取消默认
|
||||
e.isDefault = false;
|
||||
}
|
||||
});
|
||||
|
||||
request({
|
||||
url: "/app/user/address/update",
|
||||
method: "POST",
|
||||
data: {
|
||||
id: item.id,
|
||||
isDefault: item.isDefault
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
refresh({ page: 1 }).finally(() => {
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
});
|
||||
|
||||
onReachBottom(() => {
|
||||
loadMore();
|
||||
});
|
||||
|
||||
onReady(() => {
|
||||
ui.showLoading(t("加载中"));
|
||||
|
||||
// 默认请求
|
||||
refresh({
|
||||
page: 1,
|
||||
size: 20
|
||||
});
|
||||
|
||||
onPageShow(() => {
|
||||
refresh({});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
335
cool-unix/pages/template/shop/goods-category.uvue
Normal file
335
cool-unix/pages/template/shop/goods-category.uvue
Normal file
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<cl-page>
|
||||
<view class="flex flex-col h-full">
|
||||
<view class="flex flex-row p-3">
|
||||
<cl-input
|
||||
:pt="{
|
||||
className: parseClass(['flex-1 !border-2 !rounded-xl'])
|
||||
}"
|
||||
prefix-icon="search-line"
|
||||
placeholder="iPhone 16 Pro Max"
|
||||
></cl-input>
|
||||
</view>
|
||||
|
||||
<view class="flex flex-row flex-1">
|
||||
<!-- 左侧分类列表 -->
|
||||
<view class="h-full w-[200rpx] bg-white dark:bg-surface-800 mr-2 rounded-tr-xl">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" class="h-full">
|
||||
<view
|
||||
class="h-[100rpx] p-2"
|
||||
v-for="(item, index) in list"
|
||||
:key="item.label"
|
||||
@click="onCategoryChange(index)"
|
||||
>
|
||||
<view
|
||||
class="flex flex-row items-center justify-center h-full rounded-lg"
|
||||
:class="[
|
||||
categoryActive == index
|
||||
? isDark
|
||||
? 'bg-primary-500'
|
||||
: 'bg-primary-50'
|
||||
: ''
|
||||
]"
|
||||
>
|
||||
<cl-text
|
||||
:pt="{
|
||||
className: parseClass([
|
||||
[
|
||||
categoryActive == index,
|
||||
isDark ? '!text-white' : '!text-primary-500',
|
||||
isDark ? '!text-surface-300' : '!text-surface-500'
|
||||
]
|
||||
])
|
||||
}"
|
||||
>{{ item.label }}</cl-text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧商品列表 -->
|
||||
<view class="flex-1">
|
||||
<scroll-view
|
||||
direction="vertical"
|
||||
:scroll-into-view="scrollIntoView"
|
||||
:scroll-with-animation="!scrollLock"
|
||||
class="h-full"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<view class="pr-2">
|
||||
<view
|
||||
class="category-item flex rounded-xl bg-white dark:bg-surface-800 mb-2 pb-3"
|
||||
v-for="(item, index) in list"
|
||||
:key="item.label"
|
||||
:id="`category-item-${index}`"
|
||||
>
|
||||
<cl-text
|
||||
:pt="{
|
||||
className: 'p-3'
|
||||
}"
|
||||
>{{ item.label }}</cl-text
|
||||
>
|
||||
|
||||
<view class="px-1">
|
||||
<cl-row :gutter="10">
|
||||
<cl-col
|
||||
v-for="goods in item.list"
|
||||
:key="goods.name"
|
||||
:span="8"
|
||||
>
|
||||
<view class="flex items-center flex-col justify-center">
|
||||
<cl-image :src="goods.image"></cl-image>
|
||||
<cl-text
|
||||
:ellipsis="true"
|
||||
:pt="{ className: '!text-xs text-center mt-2' }"
|
||||
>{{ goods.name }}</cl-text
|
||||
>
|
||||
</view>
|
||||
</cl-col>
|
||||
</cl-row>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</cl-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isDark, parseClass } from "@/cool";
|
||||
import { getCurrentInstance, ref } from "vue";
|
||||
|
||||
const { proxy } = getCurrentInstance()!;
|
||||
|
||||
// 商品类型
|
||||
type Goods = {
|
||||
name: string;
|
||||
price: number;
|
||||
image: string;
|
||||
};
|
||||
|
||||
// 分类类型
|
||||
type Category = {
|
||||
label: string;
|
||||
top?: number;
|
||||
list: Goods[];
|
||||
};
|
||||
|
||||
// 商品分类示例数据
|
||||
const list = ref<Category[]>([
|
||||
{
|
||||
label: "推荐",
|
||||
list: [
|
||||
{
|
||||
name: "iPhone 15 Pro",
|
||||
price: 8999,
|
||||
image: "/static/goods/iphone15pro.png"
|
||||
},
|
||||
{
|
||||
name: "华为 Mate 60 Pro",
|
||||
price: 6999,
|
||||
image: "/static/goods/mate60pro.png"
|
||||
},
|
||||
{
|
||||
name: "小米 14 Ultra",
|
||||
price: 5999,
|
||||
image: "/static/goods/xiaomi14ultra.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Apple",
|
||||
list: [
|
||||
{
|
||||
name: "iPhone 15 Pro",
|
||||
price: 8999,
|
||||
image: "/static/goods/iphone15pro.png"
|
||||
},
|
||||
{
|
||||
name: "iPhone 14",
|
||||
price: 5999,
|
||||
image: "/static/goods/iphone14.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "华为",
|
||||
list: [
|
||||
{
|
||||
name: "华为 Mate 60 Pro",
|
||||
price: 6999,
|
||||
image: "/static/goods/mate60pro.png"
|
||||
},
|
||||
{
|
||||
name: "华为 P60",
|
||||
price: 4999,
|
||||
image: "/static/goods/p60.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "小米",
|
||||
list: [
|
||||
{
|
||||
name: "小米 14 Ultra",
|
||||
price: 5999,
|
||||
image: "/static/goods/xiaomi14ultra.png"
|
||||
},
|
||||
{
|
||||
name: "小米 13",
|
||||
price: 3999,
|
||||
image: "/static/goods/xiaomi13.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "三星",
|
||||
list: [
|
||||
{
|
||||
name: "三星 Galaxy S24",
|
||||
price: 7999,
|
||||
image: "/static/goods/galaxys24.png"
|
||||
},
|
||||
{
|
||||
name: "三星 Galaxy Z Flip5",
|
||||
price: 8999,
|
||||
image: "/static/goods/galaxyzflip5.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "OPPO",
|
||||
list: [
|
||||
{
|
||||
name: "OPPO Find X7",
|
||||
price: 4999,
|
||||
image: "/static/goods/findx7.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "VIVO",
|
||||
list: [
|
||||
{
|
||||
name: "VIVO X100 Pro",
|
||||
price: 5999,
|
||||
image: "/static/goods/x100pro.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "荣耀",
|
||||
list: [
|
||||
{
|
||||
name: "荣耀 Magic6",
|
||||
price: 4599,
|
||||
image: "/static/goods/magic6.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "一加",
|
||||
list: [
|
||||
{
|
||||
name: "一加 12",
|
||||
price: 4299,
|
||||
image: "/static/goods/oneplus12.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "红米",
|
||||
list: [
|
||||
{
|
||||
name: "红米 K70 Pro",
|
||||
price: 3299,
|
||||
image: "/static/goods/k70pro.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "魅族",
|
||||
list: [
|
||||
{
|
||||
name: "魅族 21",
|
||||
price: 3999,
|
||||
image: "/static/goods/meizu21.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "坚果",
|
||||
list: [
|
||||
{
|
||||
name: "坚果 R2",
|
||||
price: 2999,
|
||||
image: "/static/goods/nutR2.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "其他",
|
||||
list: [
|
||||
{
|
||||
name: "诺基亚 X30",
|
||||
price: 2599,
|
||||
image: "/static/goods/nokiax30.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
// 滚动到指定分类
|
||||
const scrollIntoView = ref("");
|
||||
|
||||
// 滚动锁定
|
||||
const scrollLock = ref(false);
|
||||
|
||||
// 当前选中的分类
|
||||
const categoryActive = ref(0);
|
||||
|
||||
// 分类切换
|
||||
function onCategoryChange(index: number) {
|
||||
categoryActive.value = index;
|
||||
|
||||
scrollIntoView.value = `category-item-${index}`;
|
||||
scrollLock.value = true;
|
||||
|
||||
setTimeout(() => {
|
||||
scrollLock.value = false;
|
||||
}, 350);
|
||||
}
|
||||
|
||||
// 滚动时,更新当前选中的分类
|
||||
function onScroll(e: UniScrollEvent) {
|
||||
if (scrollLock.value) return;
|
||||
|
||||
const top = e.detail.scrollTop;
|
||||
|
||||
list.value.forEach((e, i) => {
|
||||
if (top >= e.top!) {
|
||||
categoryActive.value = i;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
function init() {
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy!.$root)
|
||||
.selectAll(".category-item")
|
||||
.boundingClientRect((res) => {
|
||||
(res as NodeInfo[]).forEach((e, i, arr) => {
|
||||
list.value[i].top = (e.top ?? 0) - (arr[0].top ?? 0);
|
||||
});
|
||||
})
|
||||
.exec();
|
||||
}
|
||||
|
||||
onReady(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
81
cool-unix/pages/template/shop/goods-detail/comment.uvue
Normal file
81
cool-unix/pages/template/shop/goods-detail/comment.uvue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view class="flex">
|
||||
<view class="flex flex-row items-center mb-3">
|
||||
<cl-text>买家评论 78</cl-text>
|
||||
|
||||
<cl-icon name="arrow-right-s-line" :pt="{ className: 'ml-auto' }"></cl-icon>
|
||||
</view>
|
||||
|
||||
<view class="flex flex-col">
|
||||
<view class="flex flex-row my-3" v-for="item in list" :key="item.id">
|
||||
<cl-avatar :size="72" rounded :src="item.avatar"></cl-avatar>
|
||||
|
||||
<view class="flex-1 ml-4">
|
||||
<view class="flex flex-row items-center justify-between">
|
||||
<cl-text>{{ item.name }}</cl-text>
|
||||
<cl-text color="info" :pt="{ className: 'text-xs' }">{{
|
||||
item.time
|
||||
}}</cl-text>
|
||||
</view>
|
||||
|
||||
<cl-text ellipsis :lines="2" :pt="{ className: 'mt-1 text-sm' }">{{
|
||||
item.content
|
||||
}}</cl-text>
|
||||
</view>
|
||||
|
||||
<view class="ml-3 relative">
|
||||
<cl-image
|
||||
:height="100"
|
||||
:width="100"
|
||||
:pt="{
|
||||
inner: {
|
||||
className: '!rounded-lg'
|
||||
}
|
||||
}"
|
||||
src="https://unix.cool-js.com/images/demo/bg1.png"
|
||||
/>
|
||||
|
||||
<view class="bg-black/60 rounded-full px-1 absolute top-9 right-1">
|
||||
<text class="text-xs text-white">+3</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
type Comment = {
|
||||
id: number;
|
||||
avatar: string;
|
||||
name: string;
|
||||
content: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
const list = ref<Comment[]>([
|
||||
{
|
||||
id: 1,
|
||||
avatar: "https://unix.cool-js.com/images/demo/avatar-1.jpg",
|
||||
name: "李明",
|
||||
content: "导游讲解很专业,风景绝美,是一次难忘的旅行体验!",
|
||||
time: "几分钟前"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
avatar: "https://unix.cool-js.com/images/demo/avatar-2.jpg",
|
||||
name: "小芳",
|
||||
content: "酒店干净卫生,位置也很方便,强烈推荐给大家!",
|
||||
time: "1小时前"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
avatar: "https://unix.cool-js.com/images/demo/avatar-3.jpg",
|
||||
name: "王科",
|
||||
content: "行程安排合理,吃住都很满意,导游态度超级好。",
|
||||
time: "5天前"
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
15
cool-unix/pages/template/shop/goods-detail/desc.uvue
Normal file
15
cool-unix/pages/template/shop/goods-detail/desc.uvue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<view class="flex">
|
||||
<image v-for="item in list" :key="item" :src="item" mode="widthFix" class="w-full" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const list = ref<string[]>([
|
||||
"https://unix.cool-js.com/images/demo/goods/desc-1.jpg",
|
||||
"https://unix.cool-js.com/images/demo/goods/desc-2.jpg",
|
||||
"https://unix.cool-js.com/images/demo/goods/desc-3.jpg"
|
||||
]);
|
||||
</script>
|
||||
116
cool-unix/pages/template/shop/goods-detail/index.uvue
Normal file
116
cool-unix/pages/template/shop/goods-detail/index.uvue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<cl-page>
|
||||
<Topbar />
|
||||
|
||||
<cl-banner
|
||||
:list="bannerList"
|
||||
:height="800"
|
||||
:pt="{
|
||||
className: '!rounded-none',
|
||||
image: {
|
||||
className: '!rounded-none'
|
||||
}
|
||||
}"
|
||||
/>
|
||||
|
||||
<view class="card dark:!bg-surface-700" id="info">
|
||||
<Info />
|
||||
</view>
|
||||
|
||||
<view class="card !py-1 dark:!bg-surface-700">
|
||||
<view class="row is-border dark:!border-surface-600">
|
||||
<cl-icon name="apps-line"></cl-icon>
|
||||
|
||||
<view class="flex-1 ml-3">
|
||||
<cl-text>已选:黑色 128GB + 碎屏险</cl-text>
|
||||
</view>
|
||||
|
||||
<cl-icon name="arrow-right-s-line"></cl-icon>
|
||||
</view>
|
||||
|
||||
<view class="row is-border !items-start dark:!border-surface-600">
|
||||
<cl-icon name="truck-line" :pt="{ className: '!mt-1' }"></cl-icon>
|
||||
|
||||
<view class="flex-1 ml-3">
|
||||
<cl-text color="info">预计11月1日 周三 送达</cl-text>
|
||||
<cl-text color="info" :pt="{ className: '!mt-1' }">深圳益田假日广场</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="row">
|
||||
<cl-icon name="shield-check-line"></cl-icon>
|
||||
|
||||
<view class="flex-1 ml-3">
|
||||
<cl-text color="info">7天无理由退货 · 正品保证 · 极速发货 · 无忧售后</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card dark:!bg-surface-700" id="comment">
|
||||
<Comment />
|
||||
</view>
|
||||
|
||||
<view class="card !p-0 dark:!bg-surface-700" id="desc">
|
||||
<Desc />
|
||||
</view>
|
||||
|
||||
<cl-footer>
|
||||
<view class="flex flex-row overflow-visible">
|
||||
<view class="flex flex-row mr-auto overflow-visible">
|
||||
<view class="flex justify-center items-center px-4">
|
||||
<cl-icon name="heart-line" :size="42"></cl-icon>
|
||||
<cl-text :pt="{ className: 'text-xs mt-1' }">收藏</cl-text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="flex justify-center items-center px-4 overflow-visible"
|
||||
@tap="router.to('/pages/template/shop/shopping-cart')"
|
||||
>
|
||||
<cl-icon name="shopping-cart-line" :size="42"></cl-icon>
|
||||
<cl-text :pt="{ className: 'text-xs mt-1' }">购物车</cl-text>
|
||||
|
||||
<cl-badge
|
||||
type="error"
|
||||
:value="3"
|
||||
position
|
||||
:pt="{ className: '!right-[24rpx] !top-[-10rpx] !scale-80' }"
|
||||
></cl-badge>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<cl-button text border :pt="{ className: '!w-[220rpx]' }">加入购物车</cl-button>
|
||||
<cl-button :pt="{ className: '!w-[220rpx]' }">立即购买</cl-button>
|
||||
</view>
|
||||
</cl-footer>
|
||||
</cl-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { router } from "@/cool";
|
||||
import { ref } from "vue";
|
||||
import Comment from "./comment.uvue";
|
||||
import Info from "./info.uvue";
|
||||
import Desc from "./desc.uvue";
|
||||
import Topbar from "./topbar.uvue";
|
||||
|
||||
const bannerList = ref<string[]>([
|
||||
"https://unix.cool-js.com/images/demo/goods/banner-1.jpg",
|
||||
"https://unix.cool-js.com/images/demo/goods/banner-2.jpg",
|
||||
"https://unix.cool-js.com/images/demo/goods/banner-3.jpg",
|
||||
"https://unix.cool-js.com/images/demo/goods/banner-4.jpg"
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
@apply p-4 bg-white mb-3;
|
||||
|
||||
.row {
|
||||
@apply flex flex-row items-center py-4;
|
||||
|
||||
&.is-border {
|
||||
@apply border-b border-t-0 border-l-0 border-r-0 border-solid border-surface-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
cool-unix/pages/template/shop/goods-detail/info.uvue
Normal file
34
cool-unix/pages/template/shop/goods-detail/info.uvue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<view class="flex">
|
||||
<cl-text :pt="{ className: 'text-lg font-bold mb-3' }"
|
||||
>Apple/苹果 iPhone 15 (A3092) 128GB 黑色 支持移动联通电信5G 双卡双待手机</cl-text
|
||||
>
|
||||
|
||||
<view class="flex flex-row items-center mb-3">
|
||||
<view class="flex flex-row items-end">
|
||||
<cl-text color="error" :pt="{ className: 'font-bold text-sm' }">¥</cl-text>
|
||||
<cl-text
|
||||
type="amount"
|
||||
:value="8499"
|
||||
color="error"
|
||||
currency=""
|
||||
:pt="{
|
||||
className: 'font-bold ml-1 text-2xl'
|
||||
}"
|
||||
>
|
||||
</cl-text>
|
||||
</view>
|
||||
|
||||
<cl-text color="info" :pt="{ className: 'text-sm ml-auto' }" rounded
|
||||
>已售 100 件</cl-text
|
||||
>
|
||||
</view>
|
||||
|
||||
<view class="flex flex-row mb-3">
|
||||
<cl-tag type="error" plain>满199减50</cl-tag>
|
||||
<cl-tag type="error" plain>满299减100</cl-tag>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
124
cool-unix/pages/template/shop/goods-detail/topbar.uvue
Normal file
124
cool-unix/pages/template/shop/goods-detail/topbar.uvue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<cl-sticky>
|
||||
<cl-topbar fixed safe-area-top>
|
||||
<cl-tabs
|
||||
:list="list"
|
||||
v-model="active"
|
||||
height="36px"
|
||||
:gutter="20"
|
||||
@change="onTabChange"
|
||||
></cl-tabs>
|
||||
|
||||
<template #append>
|
||||
<view class="h-[44px] w-[30px] flex items-center justify-center mr-1">
|
||||
<cl-icon name="search-line"></cl-icon>
|
||||
</view>
|
||||
</template>
|
||||
</cl-topbar>
|
||||
</cl-sticky>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { debounce, getSafeAreaHeight, isNull } from "@/cool";
|
||||
import { usePage, type ClTabsItem } from "@/uni_modules/cool-ui";
|
||||
import { getCurrentInstance, onMounted, onUnmounted, ref } from "vue";
|
||||
|
||||
const { proxy } = getCurrentInstance()!;
|
||||
const page = usePage();
|
||||
|
||||
// 当前激活tab
|
||||
const active = ref("info");
|
||||
|
||||
// 滚动时激活tab
|
||||
const scrollActive = ref("");
|
||||
|
||||
// 卡片距离顶部偏移量
|
||||
const tops = ref<number[]>([]);
|
||||
|
||||
// tab项列表
|
||||
const list = ref<ClTabsItem[]>([
|
||||
{ label: "商品", value: "info" },
|
||||
{ label: "评价", value: "comment" },
|
||||
{ label: "详情", value: "desc" }
|
||||
]);
|
||||
|
||||
/**
|
||||
* 获取所有.card顶部坐标
|
||||
*/
|
||||
async function getTops(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy?.$root)
|
||||
.selectAll(".card")
|
||||
.boundingClientRect((res) => {
|
||||
const top = page.getScrollTop() - 44 - getSafeAreaHeight("top"); // 去头部高度
|
||||
|
||||
// 只计算有id的card
|
||||
tops.value = (res as NodeInfo[])
|
||||
.filter((e) => e.id != "" && !isNull(e.id))
|
||||
.map((e) => (e.top ?? 0) + top);
|
||||
|
||||
resolve();
|
||||
})
|
||||
.exec();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* tab切换
|
||||
*/
|
||||
async function onTabChange(value: string) {
|
||||
// 设置滚动时激活tab
|
||||
scrollActive.value = value;
|
||||
|
||||
// 重新获取卡片位置
|
||||
await getTops();
|
||||
|
||||
// 查找符合当前位置的tab索引
|
||||
const index = list.value.findIndex((e) => e.value == value);
|
||||
if (index < 0) return;
|
||||
|
||||
// 滚动到对应卡片位置
|
||||
page.scrollTo(tops.value[index] + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步当前tab
|
||||
*/
|
||||
const setActive = debounce(() => {
|
||||
active.value = scrollActive.value;
|
||||
}, 100);
|
||||
|
||||
/**
|
||||
* 滚动时激活tab
|
||||
*/
|
||||
function onScroll(top: number) {
|
||||
let index = -1;
|
||||
|
||||
// 查找符合当前位置的tab索引
|
||||
for (let i = 0; i < tops.value.length; i++) {
|
||||
if (top >= tops.value[i]) {
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置激活tab
|
||||
if (index >= 0 && index < list.value.length) {
|
||||
scrollActive.value = list.value[index].value as string;
|
||||
setActive();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取卡片位置
|
||||
getTops();
|
||||
|
||||
// 监听页面滚动
|
||||
page.onScroll(onScroll);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除监听
|
||||
page.offScroll(onScroll);
|
||||
});
|
||||
</script>
|
||||
416
cool-unix/pages/template/shop/shopping-cart.uvue
Normal file
416
cool-unix/pages/template/shop/shopping-cart.uvue
Normal file
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<cl-page>
|
||||
<cl-sticky>
|
||||
<cl-topbar fixed safe-area-top :title="`${$t('购物车 ({num})', { num: list.length })}`">
|
||||
<template #prepend>
|
||||
<!-- #ifdef MP -->
|
||||
<cl-text
|
||||
:pt="{
|
||||
className: 'ml-1'
|
||||
}"
|
||||
@tap="isDel = !isDel"
|
||||
>
|
||||
{{ isDel ? t("完成") : t("管理") }}
|
||||
</cl-text>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<!-- #ifndef MP -->
|
||||
<cl-text
|
||||
:pt="{
|
||||
className: 'mr-3'
|
||||
}"
|
||||
@tap="isDel = !isDel"
|
||||
>
|
||||
{{ isDel ? t("完成") : t("管理") }}
|
||||
</cl-text>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
</cl-topbar>
|
||||
</cl-sticky>
|
||||
|
||||
<view class="p-3">
|
||||
<view class="p-3 rounded-xl bg-white dark:!bg-surface-800 mb-3">
|
||||
<cl-text
|
||||
:pt="{
|
||||
className: 'text-sm'
|
||||
}"
|
||||
>🔥 最新降价商品,限时优惠,抓紧抢购!</cl-text
|
||||
>
|
||||
</view>
|
||||
|
||||
<cl-list-item
|
||||
v-for="(item, index) in list"
|
||||
:key="item.id"
|
||||
:pt="{
|
||||
className: parseClass([
|
||||
'rounded-2xl ',
|
||||
[index == list.length - 1, 'mb-0', 'mb-3']
|
||||
]),
|
||||
inner: {
|
||||
className: '!p-4'
|
||||
}
|
||||
}"
|
||||
swipeable
|
||||
>
|
||||
<view class="flex flex-row flex-1">
|
||||
<view class="flex flex-col mr-4 pt-[55rpx]" @tap="selectItem(item)">
|
||||
<cl-icon
|
||||
name="checkbox-circle-line"
|
||||
color="primary"
|
||||
:size="40"
|
||||
v-if="item.checked"
|
||||
></cl-icon>
|
||||
<cl-icon name="checkbox-blank-circle-line" :size="40" v-else></cl-icon>
|
||||
</view>
|
||||
|
||||
<cl-image :width="150" :height="150" :src="item.cover"></cl-image>
|
||||
|
||||
<view class="flex flex-col ml-3 flex-1">
|
||||
<cl-text
|
||||
:pt="{
|
||||
className: 'mb-2 font-bold'
|
||||
}"
|
||||
>{{ item.name }}</cl-text
|
||||
>
|
||||
|
||||
<view class="flex flex-row mb-2">
|
||||
<view
|
||||
class="bg-surface-100 dark:!bg-surface-700 rounded-md py-1 px-2 flex flex-row items-center"
|
||||
>
|
||||
<cl-text
|
||||
:pt="{
|
||||
className: 'text-xs'
|
||||
}"
|
||||
>{{ item.skuName }}</cl-text
|
||||
>
|
||||
<cl-icon name="arrow-down-s-line"></cl-icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex flex-row items-center mb-2">
|
||||
<cl-text
|
||||
:pt="{
|
||||
className: 'text-[22rpx] text-red-500 mr-[1rpx]'
|
||||
}"
|
||||
>¥</cl-text
|
||||
>
|
||||
<cl-text
|
||||
:pt="{
|
||||
className: 'text-red-500 text-[32rpx] mr-auto'
|
||||
}"
|
||||
>{{ item.price }}</cl-text
|
||||
>
|
||||
|
||||
<view
|
||||
v-if="isDel"
|
||||
class="p-[8rpx] bg-red-500 rounded-lg"
|
||||
@tap="delItem(index)"
|
||||
>
|
||||
<cl-icon name="delete-bin-line" color="white" :size="24"></cl-icon>
|
||||
</view>
|
||||
|
||||
<view class="flex" v-else>
|
||||
<cl-input-number
|
||||
v-model="item.count"
|
||||
:size="40"
|
||||
:min="1"
|
||||
:pt="{
|
||||
op: {
|
||||
plus: {
|
||||
className: '!rounded-full'
|
||||
},
|
||||
minus: {
|
||||
className: '!rounded-full'
|
||||
},
|
||||
icon: {
|
||||
size: 28
|
||||
}
|
||||
},
|
||||
value: {
|
||||
className: '!w-[60rpx] rounded-full !px-0',
|
||||
input: {
|
||||
className: 'text-[24rpx]'
|
||||
}
|
||||
}
|
||||
}"
|
||||
></cl-input-number>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<cl-text :size="22" color="error">比加入时降¥100</cl-text>
|
||||
|
||||
<cl-text :size="22">满1件可换购0.5元商品</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template #swipe-right>
|
||||
<cl-button
|
||||
type="error"
|
||||
:pt="{
|
||||
className: '!rounded-none h-full w-[160rpx]'
|
||||
}"
|
||||
@tap="delItem(index)"
|
||||
>{{ t("删除") }}</cl-button
|
||||
>
|
||||
</template>
|
||||
</cl-list-item>
|
||||
|
||||
<cl-empty v-if="list.length == 0"></cl-empty>
|
||||
</view>
|
||||
|
||||
<cl-footer>
|
||||
<view class="flex flex-row items-center h-[70rpx]">
|
||||
<cl-checkbox
|
||||
active-icon="checkbox-circle-line"
|
||||
inactive-icon="checkbox-blank-circle-line"
|
||||
:pt="{
|
||||
className: 'mr-auto'
|
||||
}"
|
||||
:size="28"
|
||||
v-model="selectAll"
|
||||
@change="onSelectAllChange"
|
||||
>{{ t("全选") }}</cl-checkbox
|
||||
>
|
||||
|
||||
<template v-if="isDel">
|
||||
<cl-button
|
||||
type="error"
|
||||
:pt="{
|
||||
className: '!px-5'
|
||||
}"
|
||||
@tap="delAll"
|
||||
>
|
||||
{{ t("删除") }}
|
||||
</cl-button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<view class="flex flex-col mr-3 items-end pt-1">
|
||||
<cl-text
|
||||
color="info"
|
||||
:pt="{
|
||||
className: 'text-xs'
|
||||
}"
|
||||
>{{ t("合计") }}</cl-text
|
||||
>
|
||||
<cl-text color="error" :value="totalPrice" type="amount"></cl-text>
|
||||
</view>
|
||||
|
||||
<cl-button
|
||||
type="error"
|
||||
:pt="{
|
||||
className: '!px-8'
|
||||
}"
|
||||
@tap="toSettle"
|
||||
>
|
||||
{{ t("去结算") }}
|
||||
</cl-button>
|
||||
</template>
|
||||
</view>
|
||||
</cl-footer>
|
||||
</cl-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { parseClass, isEmpty } from "@/cool";
|
||||
import { $t, t } from "@/locale";
|
||||
import { useUi } from "@/uni_modules/cool-ui";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const ui = useUi();
|
||||
|
||||
// 商品类型
|
||||
type Goods = {
|
||||
id: number;
|
||||
name: string;
|
||||
count: number;
|
||||
price: number;
|
||||
cover: string;
|
||||
skuName: string;
|
||||
checked: boolean;
|
||||
};
|
||||
|
||||
// 商品列表
|
||||
const list = ref<Goods[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "Apple iPad",
|
||||
count: 1,
|
||||
price: 450,
|
||||
cover: "https://img.yzcdn.cn/vant/ipad.png",
|
||||
skuName: "深空灰色 128GB WLAN版",
|
||||
checked: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Samsung Galaxy S24",
|
||||
count: 2,
|
||||
price: 699,
|
||||
cover: "https://img.yzcdn.cn/vant/samsung.png",
|
||||
skuName: "曜石黑 12GB+256GB 官方标配",
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Sony WH-1000XM5",
|
||||
count: 1,
|
||||
price: 299,
|
||||
cover: "https://img.yzcdn.cn/vant/sony.png",
|
||||
skuName: "黑色 无线蓝牙 官方标配",
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "小米手环8",
|
||||
count: 3,
|
||||
price: 49,
|
||||
cover: "https://img.yzcdn.cn/vant/xiaomi.png",
|
||||
skuName: "曜石黑 标准版 硅胶表带",
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Kindle Paperwhite",
|
||||
count: 1,
|
||||
price: 120,
|
||||
cover: "https://img.yzcdn.cn/vant/kindle.png",
|
||||
skuName: "黑色 8GB 官方标配",
|
||||
checked: false
|
||||
}
|
||||
]);
|
||||
|
||||
// 是否全选
|
||||
const selectAll = ref(false);
|
||||
|
||||
/**
|
||||
* 选择/取消选择单个商品
|
||||
* @param item 需要操作的商品
|
||||
*/
|
||||
function selectItem(item: Goods) {
|
||||
// 切换选中状态
|
||||
item.checked = !item.checked;
|
||||
// 判断是否所有商品都被选中,更新全选状态
|
||||
selectAll.value = list.value.every((item) => item.checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全选/取消全选
|
||||
* @param val 是否全选
|
||||
*/
|
||||
function onSelectAllChange(val: boolean) {
|
||||
list.value.forEach((item, index, arr) => {
|
||||
// item.checked = val; // 这样写,在 android 上无效
|
||||
arr[index].checked = val;
|
||||
});
|
||||
}
|
||||
|
||||
// 是否处于删除模式
|
||||
const isDel = ref(false);
|
||||
|
||||
/**
|
||||
* 删除单个商品
|
||||
* @param index 商品索引
|
||||
*/
|
||||
function delItem(index: number) {
|
||||
ui.showConfirm({
|
||||
title: t("温馨提示"),
|
||||
message: t("确定删除该商品吗?"),
|
||||
callback(action) {
|
||||
if (action === "confirm") {
|
||||
// 删除指定索引的商品
|
||||
list.value.splice(index, 1);
|
||||
|
||||
ui.showToast({
|
||||
message: t("删除成功")
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除所有已选中的商品
|
||||
*/
|
||||
function delAll() {
|
||||
const checked = list.value.filter((item) => item.checked);
|
||||
|
||||
// 如果没有选中商品,提示用户
|
||||
if (isEmpty(checked)) {
|
||||
return ui.showToast({
|
||||
message: t("请先选择商品")
|
||||
});
|
||||
}
|
||||
|
||||
ui.showConfirm({
|
||||
title: t("温馨提示"),
|
||||
message: t("确定删除选中的商品吗?"),
|
||||
callback(action) {
|
||||
if (action == "confirm") {
|
||||
// 只保留未选中的商品
|
||||
list.value = list.value.filter((item) => !item.checked);
|
||||
|
||||
// 如果之前是全选,删除后取消全选状态
|
||||
if (selectAll.value) {
|
||||
selectAll.value = false;
|
||||
}
|
||||
|
||||
ui.showToast({
|
||||
message: t("删除成功")
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空购物车
|
||||
*/
|
||||
function clear() {
|
||||
list.value = [];
|
||||
selectAll.value = false;
|
||||
isDel.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算已选中商品的总价
|
||||
*/
|
||||
const totalPrice = computed(() => {
|
||||
return list.value
|
||||
.filter((item) => item.checked)
|
||||
.reduce((acc, item) => acc + item.price * item.count, 0);
|
||||
});
|
||||
|
||||
/**
|
||||
* 结算操作
|
||||
*/
|
||||
function toSettle() {
|
||||
// 如果没有选中商品,提示用户
|
||||
if (totalPrice.value <= 0) {
|
||||
return ui.showToast({
|
||||
message: t("请先选择商品")
|
||||
});
|
||||
}
|
||||
|
||||
ui.showConfirm({
|
||||
title: t("温馨提示"),
|
||||
message: $t("您需支付 {price} 元,请确认支付", { price: totalPrice.value }),
|
||||
beforeClose(action, { showLoading, close }) {
|
||||
if (action == "confirm") {
|
||||
showLoading();
|
||||
|
||||
setTimeout(() => {
|
||||
ui.showToast({
|
||||
message: t("支付成功")
|
||||
});
|
||||
|
||||
close();
|
||||
}, 1000);
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user