添加“收货地址”模板页
This commit is contained in:
13
pages.json
13
pages.json
@@ -446,6 +446,19 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "shop/address",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "收货地址",
|
||||||
|
"enablePullDownRefresh": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "shop/address-edit",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "编辑地址"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ const list = computed<Item[]>(() => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("订单列表、详情")
|
label: t("订单列表、详情")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("收货地址"),
|
||||||
|
path: "/pages/template/shop/address"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
198
pages/template/shop/address-edit.uvue
Normal file
198
pages/template/shop/address-edit.uvue
Normal file
@@ -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
pages/template/shop/address.uvue
Normal file
216
pages/template/shop/address.uvue
Normal file
@@ -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
pages/template/types/index.ts
Normal file
10
pages/template/types/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export type UserAddress = {
|
||||||
|
id?: number;
|
||||||
|
contact: string;
|
||||||
|
phone: string;
|
||||||
|
province: string;
|
||||||
|
city: string;
|
||||||
|
district: string;
|
||||||
|
address: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user