Przeglądaj źródła

添加“收货地址”模板页

icssoa 7 miesięcy temu
rodzic
commit
fce9d1998a

+ 13 - 0
pages.json

@@ -446,6 +446,19 @@
 					"style": {
 						"navigationStyle": "custom"
 					}
+				},
+				{
+					"path": "shop/address",
+					"style": {
+						"navigationBarTitleText": "收货地址",
+						"enablePullDownRefresh": true
+					}
+				},
+				{
+					"path": "shop/address-edit",
+					"style": {
+						"navigationBarTitleText": "编辑地址"
+					}
 				}
 			]
 		}

+ 4 - 0
pages/index/template.uvue

@@ -60,6 +60,10 @@ const list = computed<Item[]>(() => [
 			},
 			{
 				label: t("订单列表、详情")
+			},
+			{
+				label: t("收货地址"),
+				path: "/pages/template/shop/address"
 			}
 		]
 	},

+ 198 - 0
pages/template/shop/address-edit.uvue

@@ -0,0 +1,198 @@
+<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"
+						></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(async (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>

+ 216 - 0
pages/template/shop/address.uvue

@@ -0,0 +1,216 @@
+<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) => {
+	return request({
+		url: "/app/user/address/page",
+		method: "POST",
+		data
+	})
+		.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>

+ 10 - 0
pages/template/types/index.ts

@@ -0,0 +1,10 @@
+export type UserAddress = {
+	id?: number;
+	contact: string;
+	phone: string;
+	province: string;
+	city: string;
+	district: string;
+	address: string;
+	isDefault: boolean;
+};