优化模板界面内容

This commit is contained in:
dftre 2025-03-21 18:34:24 +08:00
parent 76e0db644d
commit f0200ac849
8 changed files with 878 additions and 619 deletions

View File

@ -56,169 +56,191 @@
</u-popup> </u-popup>
</template> </template>
<script> <script setup lang="ts">
import provinces from "./province.js"; import { ref, computed, onMounted, PropType } from 'vue';
import citys from "./city.js"; import provincesSource from "./province.js";
import areas from "./area.js"; import citysSource from "./city.js";
/** import areasSource from "./area.js";
* city-select 省市区级联选择器
* @property {String Number} z-index 弹出时的z-index值默认1075 //
* @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭Picker默认true interface Region {
* @property {String} default-region 默认选中的地区中文形式 label: string;
* @property {String} default-code 默认选中的地区编号形式 value: string;
*/ }
export default {
name: 'u-city-select', interface CitySelectResult {
props: { province: Region;
// city: Region;
modelValue: { area: Region;
type: Boolean, }
default: false
}, interface TabItem {
// ["", "", ""] name: string;
defaultRegion: { }
type: Array,
default() { // Props
return []; const props = defineProps({
} //
}, modelValue: {
// defaultRegionareaCodeareaCode["13", "1303", "130304"] type: Boolean,
areaCode: { default: false
type: Array,
default() {
return [];
}
},
// Picker
maskCloseAble: {
type: Boolean,
default: true
},
// z-index
zIndex: {
type: [String, Number],
default: 0
}
}, },
data() { // ["", "", ""]
return { defaultRegion: {
cityValue: "", type: Array as PropType<string[]>,
isChooseP: false, // default: () => []
province: 0, //
provinces: provinces,
isChooseC: false, //
city: 0, //
citys: citys[0],
isChooseA: false, //
area: 0, //
areas: areas[0][0],
tabsIndex: 0,
}
}, },
mounted() { // defaultRegionareaCodeareaCode["13", "1303", "130304"]
this.init(); areaCode: {
type: Array as PropType<string[]>,
default: () => []
}, },
computed: { // Picker
isChange() { maskCloseAble: {
return this.tabsIndex > 1; type: Boolean,
}, default: true
genTabsList() {
let tabsList = [{
name: "请选择"
}];
if (this.isChooseP) {
tabsList[0]['name'] = this.provinces[this.province]['label'];
tabsList[1] = {
name: "请选择"
};
}
if (this.isChooseC) {
tabsList[1]['name'] = this.citys[this.city]['label'];
tabsList[2] = {
name: "请选择"
};
}
if (this.isChooseA) {
tabsList[2]['name'] = this.areas[this.area]['label'];
}
return tabsList;
},
uZIndex() {
// z-index使
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
}
}, },
emits: ['city-change'], // z-index
methods: { zIndex: {
init() { type: [String, Number],
if (this.areaCode.length == 3) { default: 0
this.setProvince("", this.areaCode[0]); }
this.setCity("", this.areaCode[1]); });
this.setArea("", this.areaCode[2]);
} else if (this.defaultRegion.length == 3) { //
this.setProvince(this.defaultRegion[0], ""); const emit = defineEmits<{
this.setCity(this.defaultRegion[1], ""); (e: 'update:modelValue', value: boolean): void;
this.setArea(this.defaultRegion[2], ""); (e: 'close'): void;
}; (e: 'city-change', result: CitySelectResult): void;
}, }>();
setProvince(label = "", value = "") {
this.provinces.map((v, k) => { const cityValue = ref("");
if (value ? v.value == value : v.label == label) { const isChooseP = ref(false); //
this.provinceChange(k); const province = ref(0); //
} const provinces = ref<Region[]>(provincesSource);
}) const isChooseC = ref(false); //
}, const city = ref(0); //
setCity(label = "", value = "") { const citys = ref<Region[]>(citysSource[0]);
this.citys.map((v, k) => { const isChooseA = ref(false); //
if (value ? v.value == value : v.label == label) { const area = ref(0); //
this.cityChange(k); const areas = ref<Region[]>(areasSource[0][0]);
} const tabsIndex = ref(0);
}) const tabs = ref();
},
setArea(label = "", value = "") { //
this.areas.map((v, k) => { const isChange = computed(() => {
if (value ? v.value == value : v.label == label) { return tabsIndex.value > 1;
this.isChooseA = true; });
this.area = k;
} const genTabsList = computed((): TabItem[] => {
}) let tabsList: TabItem[] = [{
}, name: "请选择"
close() { }];
this.$emit('update:modelValue', false);
this.$emit('close'); if (isChooseP.value) {
}, tabsList[0].name = provinces.value[province.value].label;
tabsChange(value) { tabsList[1] = {
this.tabsIndex = value.index; name: "请选择"
}, };
provinceChange(index) {
this.isChooseP = true;
this.isChooseC = false;
this.isChooseA = false;
this.province = index;
this.citys = citys[index];
this.tabsIndex = 1;
},
cityChange(index) {
this.isChooseC = true;
this.isChooseA = false;
this.city = index;
this.areas = areas[this.province][index];
this.tabsIndex = 2;
},
areaChange(index) {
this.isChooseA = true;
this.area = index;
let result = {};
result.province = this.provinces[this.province];
result.city = this.citys[this.city];
result.area = this.areas[this.area];
this.$emit('city-change', result);
this.close();
}
} }
} if (isChooseC.value) {
tabsList[1].name = citys.value[city.value].label;
tabsList[2] = {
name: "请选择"
};
}
if (isChooseA.value) {
tabsList[2].name = areas.value[area.value].label;
}
return tabsList;
});
const uZIndex = computed(() => {
// z-index使
return props.zIndex ? props.zIndex : 1075; // $u.zIndex.popup1075
});
//
const setProvince = (label = "", value = "") => {
provinces.value.map((v, k) => {
if (value ? v.value == value : v.label == label) {
provinceChange(k);
}
});
};
const setCity = (label = "", value = "") => {
citys.value.map((v, k) => {
if (value ? v.value == value : v.label == label) {
cityChange(k);
}
});
};
const setArea = (label = "", value = "") => {
areas.value.map((v, k) => {
if (value ? v.value == value : v.label == label) {
isChooseA.value = true;
area.value = k;
}
});
};
const close = () => {
emit('update:modelValue', false);
emit('close');
};
const tabsChange = (value: { index: number }) => {
tabsIndex.value = value.index;
};
const provinceChange = (index: number) => {
isChooseP.value = true;
isChooseC.value = false;
isChooseA.value = false;
province.value = index;
citys.value = citysSource[index];
tabsIndex.value = 1;
};
const cityChange = (index: number) => {
isChooseC.value = true;
isChooseA.value = false;
city.value = index;
areas.value = areasSource[province.value][index];
tabsIndex.value = 2;
};
const areaChange = (index: number) => {
isChooseA.value = true;
area.value = index;
const result: CitySelectResult = {
province: provinces.value[province.value],
city: citys.value[city.value],
area: areas.value[area.value]
};
emit('city-change', result);
close();
};
//
onMounted(() => {
if (props.areaCode.length == 3) {
setProvince("", props.areaCode[0]);
setCity("", props.areaCode[1]);
setArea("", props.areaCode[2]);
} else if (props.defaultRegion.length == 3) {
setProvince(props.defaultRegion[0], "");
setCity(props.defaultRegion[1], "");
setArea(props.defaultRegion[2], "");
}
});
</script> </script>
<style lang="scss"> <style lang="scss">
.area-box { .area-box {
width: 100%; width: 100%;

View File

@ -2,22 +2,24 @@
import { ref, reactive, onMounted } from 'vue'; import { ref, reactive, onMounted } from 'vue';
import tab from '@/plugins/tab'; import tab from '@/plugins/tab';
import citySelect from '@/components/u-city-select/u-city-select.vue'; import citySelect from '@/components/u-city-select/u-city-select.vue';
import { useAddressEditPage } from './index';
// // 使Hook
const show = ref(false); const {
const defaultAddress = ref(false); isEdit,
const selectedTag = ref('家'); form,
const isEdit = ref(false); // defaultAddress,
const editId = ref(''); // ID selectedTag,
addressTags,
initEditPage,
saveAddress,
deleteAddress
} = useAddressEditPage();
// //
const form = reactive({ const showRegionPicker = ref(false);
name: '',
phone: '',
region: '',
address: ''
});
// - Vue
const formErrors = reactive({ const formErrors = reactive({
name: false, name: false,
phone: false, phone: false,
@ -25,101 +27,69 @@ const formErrors = reactive({
address: false address: false
}); });
//
function resetFormErrors() {
formErrors.name = false;
formErrors.phone = false;
formErrors.region = false;
formErrors.address = false;
}
//
onMounted(() => { onMounted(() => {
//
const pages = getCurrentPages(); const pages = getCurrentPages();
const currentPage = pages[pages.length - 1]; const currentPage: any = pages[pages.length - 1];
const options = currentPage.$page?.options; const options = currentPage.$page?.options;
if (options && options.id) { // hook
isEdit.value = true; initEditPage(options?.id);
editId.value = options.id; //
loadAddressData(options.id); resetFormErrors();
}
}); });
//
const loadAddressData = (id: string) => {
try {
const addressList = uni.getStorageSync('addressList') || [];
const address = addressList.find((item: any) => item.id === id);
if (address) {
form.name = address.name;
form.phone = address.phoneOriginal || address.phone; // 使
form.region = address.region;
form.address = address.address;
selectedTag.value = address.tag || '家';
defaultAddress.value = address.isDefault;
}
} catch (e) {
console.error('加载地址数据失败', e);
}
};
// //
const setDefault = (e: any) => { function handleSetDefault(e: any) {
defaultAddress.value = e.detail.value; defaultAddress.value = e.detail.value;
}; }
// //
const showRegionPicker = () => { function handleShowRegionPicker() {
show.value = true; showRegionPicker.value = true;
}; }
// //
const cityChange = (e) => { function handleCityChange(e: any) {
form.region = e.province.label + e.city.label + e.area.label; form.region = e.province.label + e.city.label + e.area.label;
formErrors.region = false; formErrors.region = false;
}; }
// //
const selectTag = (tag: string) => { function handleSelectTag(tag: string) {
selectedTag.value = tag; selectedTag.value = tag;
}; }
// //
const validateForm = () => { function validateForm(): boolean {
let isValid = true;
// //
if (!form.name.trim()) { formErrors.name = !form.name.trim();
formErrors.name = true;
isValid = false;
} else {
formErrors.name = false;
}
// //
const phoneReg = /^1[3-9]\d{9}$/; const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(form.phone)) { formErrors.phone = !phoneReg.test(form.phone);
formErrors.phone = true;
isValid = false;
} else {
formErrors.phone = false;
}
// //
if (!form.region) { formErrors.region = !form.region;
formErrors.region = true;
isValid = false;
} else {
formErrors.region = false;
}
// //
if (!form.address.trim()) { formErrors.address = !form.address.trim();
formErrors.address = true;
isValid = false; // false
} else { return !(formErrors.name || formErrors.phone || formErrors.region || formErrors.address);
formErrors.address = false; }
}
return isValid;
};
// //
const saveAddress = () => { function handleSaveAddress() {
// 使
if (!validateForm()) { if (!validateForm()) {
uni.showToast({ uni.showToast({
title: '请填写完整信息', title: '请填写完整信息',
@ -127,53 +97,26 @@ const saveAddress = () => {
}); });
return; return;
} }
try { try {
// const success = saveAddress();
let addressList = uni.getStorageSync('addressList') || [];
if (success) {
// uni.showToast({
const addressData = { title: isEdit.value ? '修改成功' : '添加成功',
id: isEdit.value ? editId.value : Date.now().toString(), icon: 'success'
name: form.name, });
phone: form.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'), // 4
phoneOriginal: form.phone, // //
region: form.region, setTimeout(() => {
address: form.address, tab.navigateBack();
tag: selectedTag.value, }, 1000);
isDefault: defaultAddress.value } else {
}; uni.showToast({
title: '保存失败',
if (defaultAddress.value) { icon: 'none'
//
addressList = addressList.map((item: any) => {
return { ...item, isDefault: false };
}); });
} }
if (isEdit.value) {
//
const index = addressList.findIndex((item: any) => item.id === editId.value);
if (index !== -1) {
addressList[index] = addressData;
}
} else {
//
addressList.push(addressData);
}
//
uni.setStorageSync('addressList', addressList);
uni.showToast({
title: isEdit.value ? '修改成功' : '添加成功',
icon: 'success'
});
//
setTimeout(() => {
tab.navigateBack();
}, 1000);
} catch (e) { } catch (e) {
console.error('保存地址失败', e); console.error('保存地址失败', e);
uni.showToast({ uni.showToast({
@ -181,30 +124,35 @@ const saveAddress = () => {
icon: 'none' icon: 'none'
}); });
} }
}; }
// //
const deleteAddress = () => { function handleDeleteAddress() {
if (!isEdit.value) return; if (!isEdit.value) return;
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '确定要删除此地址吗?', content: '确定要删除此地址吗?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
try { try {
let addressList = uni.getStorageSync('addressList') || []; const success = deleteAddress();
addressList = addressList.filter((item: any) => item.id !== editId.value);
uni.setStorageSync('addressList', addressList); if (success) {
uni.showToast({
uni.showToast({ title: '删除成功',
title: '删除成功', icon: 'success'
icon: 'success' });
});
setTimeout(() => {
setTimeout(() => { tab.navigateBack();
tab.navigateBack(); }, 1000);
}, 1000); } else {
uni.showToast({
title: '删除失败',
icon: 'none'
});
}
} catch (e) { } catch (e) {
console.error('删除地址失败', e); console.error('删除地址失败', e);
uni.showToast({ uni.showToast({
@ -215,8 +163,9 @@ const deleteAddress = () => {
} }
} }
}); });
}; }
</script> </script>
<template> <template>
<view class="wrap"> <view class="wrap">
<view class="container"> <view class="container">
@ -225,71 +174,50 @@ const deleteAddress = () => {
<view class="left"> <view class="left">
<text class="required">*</text>收货人 <text class="required">*</text>收货人
</view> </view>
<input <input type="text" v-model="form.name" placeholder-class="line" placeholder="请填写收货人姓名"
type="text" :class="{ 'error-input': formErrors.name }" />
v-model="form.name"
placeholder-class="line"
placeholder="请填写收货人姓名"
:class="{ 'error-input': formErrors.name }"
/>
<u-icon name="account" size="36rpx" color="#999"></u-icon> <u-icon name="account" size="36rpx" color="#999"></u-icon>
</view> </view>
<view class="error-msg" v-if="formErrors.name">请输入收货人姓名</view> <view class="error-msg" v-if="formErrors.name">请输入收货人姓名</view>
<view class="item"> <view class="item">
<view class="left"> <view class="left">
<text class="required">*</text>手机号码 <text class="required">*</text>手机号码
</view> </view>
<input <input type="number" v-model="form.phone" placeholder-class="line" placeholder="请填写收货人手机号" maxlength="11"
type="number" :class="{ 'error-input': formErrors.phone }" />
v-model="form.phone"
placeholder-class="line"
placeholder="请填写收货人手机号"
maxlength="11"
:class="{ 'error-input': formErrors.phone }"
/>
<u-icon name="phone" size="36rpx" color="#999"></u-icon> <u-icon name="phone" size="36rpx" color="#999"></u-icon>
</view> </view>
<view class="error-msg" v-if="formErrors.phone">请输入正确的手机号码</view> <view class="error-msg" v-if="formErrors.phone">请输入正确的手机号码</view>
<view class="item" @tap="showRegionPicker"> <view class="item" @tap="handleShowRegionPicker">
<view class="left"> <view class="left">
<text class="required">*</text>所在地区 <text class="required">*</text>所在地区
</view> </view>
<input <input disabled v-model="form.region" type="text" placeholder-class="line" placeholder="省市区县、乡镇等"
disabled :class="{ 'error-input': formErrors.region }" />
v-model="form.region"
type="text"
placeholder-class="line"
placeholder="省市区县、乡镇等"
:class="{ 'error-input': formErrors.region }"
/>
<u-icon name="arrow-right" size="36rpx" color="#999"></u-icon> <u-icon name="arrow-right" size="36rpx" color="#999"></u-icon>
</view> </view>
<view class="error-msg" v-if="formErrors.region">请选择所在地区</view> <view class="error-msg" v-if="formErrors.region">请选择所在地区</view>
<view class="item address"> <view class="item address">
<view class="left"> <view class="left">
<text class="required">*</text>详细地址 <text class="required">*</text>详细地址
</view> </view>
<textarea <textarea v-model="form.address" type="text" placeholder-class="line" placeholder="街道、楼牌等"
v-model="form.address" :class="{ 'error-textarea': formErrors.address }" />
type="text"
placeholder-class="line"
placeholder="街道、楼牌等"
:class="{ 'error-textarea': formErrors.address }"
/>
</view> </view>
<view class="error-msg" v-if="formErrors.address">请输入详细地址</view> <view class="error-msg" v-if="formErrors.address">请输入详细地址</view>
</view> </view>
<view class="bottom"> <view class="bottom">
<view class="tag"> <view class="tag">
<view class="left">标签</view> <view class="left">标签</view>
<view class="right"> <view class="right">
<text class="tags" :class="{'active': selectedTag === '家'}" @tap="selectTag('家')"></text> <text v-for="tag in addressTags" :key="tag" class="tags" :class="{ 'active': selectedTag === tag }"
<text class="tags" :class="{'active': selectedTag === '公司'}" @tap="selectTag('公司')">公司</text> @tap="handleSelectTag(tag)">
<text class="tags" :class="{'active': selectedTag === '学校'}" @tap="selectTag('学校')">学校</text> {{ tag }}
</text>
<view class="tags plus"><u-icon size="22" name="plus" color="#999"></u-icon></view> <view class="tags plus"><u-icon size="22" name="plus" color="#999"></u-icon></view>
</view> </view>
</view> </view>
@ -299,22 +227,22 @@ const deleteAddress = () => {
<view class="tips">提醒每次下单会默认推荐该地址</view> <view class="tips">提醒每次下单会默认推荐该地址</view>
</view> </view>
<view class="right"> <view class="right">
<switch color="#fa3534" :checked="defaultAddress" @change="setDefault" /> <switch color="#fa3534" :checked="defaultAddress" @change="handleSetDefault" />
</view> </view>
</view> </view>
</view> </view>
<view class="button-group"> <view class="button-group">
<view class="save-btn" @tap="saveAddress"> <view class="save-btn" @tap="handleSaveAddress">
{{ isEdit ? '保存修改' : '保存地址' }} {{ isEdit ? '保存修改' : '保存地址' }}
</view> </view>
<view v-if="isEdit" class="delete-btn" @tap="deleteAddress"> <view v-if="isEdit" class="delete-btn" @tap="handleDeleteAddress">
删除地址 删除地址
</view> </view>
</view> </view>
</view> </view>
<city-select v-model="show" @city-change="cityChange"></city-select> <city-select v-model="showRegionPicker" @city-change="handleCityChange"></city-select>
</view> </view>
</template> </template>
@ -352,7 +280,7 @@ const deleteAddress = () => {
width: 180rpx; width: 180rpx;
font-weight: 500; font-weight: 500;
color: #333; color: #333;
.required { .required {
color: #fa3534; color: #fa3534;
margin-right: 4rpx; margin-right: 4rpx;
@ -364,17 +292,17 @@ const deleteAddress = () => {
flex: 1; flex: 1;
height: 100rpx; height: 100rpx;
font-size: 30rpx; font-size: 30rpx;
&.error-input { &.error-input {
border-bottom: 1px solid #fa3534; border-bottom: 1px solid #fa3534;
} }
} }
u-icon { u-icon {
margin-left: 10rpx; margin-left: 10rpx;
} }
} }
.error-msg { .error-msg {
color: #fa3534; color: #fa3534;
font-size: 24rpx; font-size: 24rpx;
@ -400,7 +328,7 @@ const deleteAddress = () => {
padding: 20rpx; padding: 20rpx;
border-radius: 12rpx; border-radius: 12rpx;
font-size: 30rpx; font-size: 30rpx;
&.error-textarea { &.error-textarea {
border: 1px solid #fa3534; border: 1px solid #fa3534;
} }
@ -444,7 +372,7 @@ const deleteAddress = () => {
color: #333; color: #333;
line-height: 1; line-height: 1;
transition: all 0.3s; transition: all 0.3s;
&.active { &.active {
background-color: #ffebec; background-color: #ffebec;
color: #fa3534; color: #fa3534;
@ -471,7 +399,7 @@ const deleteAddress = () => {
color: #333; color: #333;
font-size: 30rpx; font-size: 30rpx;
} }
.tips { .tips {
font-size: 24rpx; font-size: 24rpx;
color: #999; color: #999;
@ -480,12 +408,12 @@ const deleteAddress = () => {
} }
} }
} }
.button-group { .button-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-top: 60rpx; margin-top: 60rpx;
.save-btn { .save-btn {
background: linear-gradient(90deg, #ff4034, #fa3534); background: linear-gradient(90deg, #ff4034, #fa3534);
color: #fff; color: #fff;
@ -498,7 +426,7 @@ const deleteAddress = () => {
box-shadow: 0 10rpx 20rpx rgba(250, 53, 52, 0.2); box-shadow: 0 10rpx 20rpx rgba(250, 53, 52, 0.2);
letter-spacing: 2rpx; letter-spacing: 2rpx;
} }
.delete-btn { .delete-btn {
margin-top: 30rpx; margin-top: 30rpx;
background: #ffffff; background: #ffffff;

View File

@ -0,0 +1,256 @@
import { ref, reactive, computed } from 'vue';
import { onShow } from '@dcloudio/uni-app';
/**
*
*/
export interface AddressInfo {
id: string; // 地址ID
name: string; // 收货人姓名
phone: string; // 手机号码(已脱敏)
region: string; // 地区(如: 广东省深圳市南山区)
address: string; // 详细地址
tag: string; // 地址标签(如: 家、公司、学校)
isDefault: boolean; // 是否为默认地址
}
/**
*
*/
const sampleAddresses: AddressInfo[] = [
{
id: '1',
name: '张三',
phone: '13712348888',
region: '广东省深圳市南山区',
address: '科技园南路888号创新大厦A座10楼',
tag: '公司',
isDefault: true
},
{
id: '2',
name: '李四',
phone: '13912345678',
region: '广东省深圳市福田区',
address: '福中路1000号海城大厦B座20楼2001室',
tag: '家',
isDefault: false
},
{
id: '3',
name: '王五',
phone: '15812342233',
region: '广东省广州市天河区',
address: '天河路100号天河城购物中心附近小区A栋3单元701室',
tag: '学校',
isDefault: false
}
];
// 共享的地址数据
const addressStore = {
list: ref<AddressInfo[]>([]),
tags: ref(['家', '公司', '学校'])
};
/**
* 4
*/
export function formatPhoneNumber(phone: string): string {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
/**
* Hook
* @description
*/
export function useAddressListPage() {
// 从共享store获取响应式数据
const addressList = addressStore.list;
// 使用计算属性计算是否为空状态
const emptyStatus = computed(() => addressList.value.length === 0);
// 更新地址列表
function refreshAddressList() {
addressList.value = sampleAddresses
// 实际项目中这里应该调用API获取最新的地址列表
// const response = await api.getAddressList();
// addressList.value = response.data;
}
// 设置默认地址
function setDefaultAddress(id: string): boolean {
const index = addressList.value.findIndex(item => item.id === id);
if (index === -1) return false;
// 更新所有地址的默认状态
addressList.value = addressList.value.map(item => ({
...item,
isDefault: item.id === id
}));
return true;
}
// 删除地址
function deleteAddress(id: string): boolean {
const initialLength = addressList.value.length;
addressList.value = addressList.value.filter(item => item.id !== id);
return addressList.value.length !== initialLength;
}
// 页面显示时刷新数据
onShow(() => {
refreshAddressList();
});
return {
// 响应式状态
addressList,
emptyStatus,
// 方法
setDefaultAddress,
deleteAddress,
refreshAddressList
};
}
/**
* Hook
* @description
*/
export function useAddressEditPage() {
// 从共享store获取响应式数据
const addressList = addressStore.list;
const addressTags = addressStore.tags;
// 页面状态
const isEdit = ref(false);
const editId = ref('');
const defaultAddress = ref(false);
const selectedTag = ref('家');
// 表单数据
const form = reactive({
name: '',
phone: '',
region: '',
address: ''
});
// 加载编辑数据
function loadAddressData(id: string): boolean {
const address = addressList.value.find(item => item.id === id);
if (!address) return false;
// 填充表单数据
form.name = address.name;
form.phone = address.phone;
form.region = address.region;
form.address = address.address;
selectedTag.value = address.tag;
defaultAddress.value = address.isDefault;
return true;
}
// 初始化页面
function initEditPage(id?: string) {
// 重置状态
isEdit.value = !!id;
editId.value = id || '';
defaultAddress.value = false;
selectedTag.value = '家';
form.name = '';
form.phone = '';
form.region = '';
form.address = '';
// 如果是编辑模式,加载地址数据
if (id) {
loadAddressData(id);
}
}
// 保存地址
function saveAddress(): boolean {
if (isEdit.value) {
// 编辑现有地址
const index = addressList.value.findIndex(item => item.id === editId.value);
if (index === -1) return false;
// 如果设为默认,更新其他地址
if (defaultAddress.value) {
addressList.value = addressList.value.map(item => {
if (item.id !== editId.value) {
return { ...item, isDefault: false };
}
return item;
});
}
// 更新当前地址
addressList.value[index] = {
...addressList.value[index],
name: form.name,
phone: form.phone,
region: form.region,
address: form.address,
tag: selectedTag.value,
isDefault: defaultAddress.value
};
} else {
// 添加新地址
const newId = Date.now().toString();
// 如果设为默认,更新其他地址
if (defaultAddress.value) {
addressList.value = addressList.value.map(item => ({
...item,
isDefault: false
}));
}
// 添加新地址
addressList.value.push({
id: newId,
name: form.name,
phone: form.phone,
region: form.region,
address: form.address,
tag: selectedTag.value,
isDefault: defaultAddress.value
});
}
return true;
}
// 删除地址
function deleteAddress(): boolean {
if (!isEdit.value) return false;
const initialLength = addressList.value.length;
addressList.value = addressList.value.filter(item => item.id !== editId.value);
return addressList.value.length !== initialLength;
}
return {
// 响应式状态
isEdit,
editId,
form,
defaultAddress,
selectedTag,
addressTags,
// 方法
initEditPage,
saveAddress,
deleteAddress
};
}

View File

@ -1,30 +1,14 @@
<script setup> <script setup lang="ts">
import { ref, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import tab from '@/plugins/tab'; import tab from '@/plugins/tab';
import { AddressInfo, useAddressListPage } from './index';
const siteList = ref([]); // 使Hook
const emptyStatus = ref(false); const {
addressList,
// 使onShow emptyStatus,
onShow(() => { setDefaultAddress,
getData(); deleteAddress
}); } = useAddressListPage();
//
function getData() {
try {
const addressList = uni.getStorageSync('addressList') || [];
siteList.value = addressList;
emptyStatus.value = addressList.length === 0;
} catch (e) {
console.error('获取地址列表失败', e);
uni.showToast({
title: '获取地址列表失败',
icon: 'none'
});
}
}
// //
function toAddSite() { function toAddSite() {
@ -32,25 +16,26 @@ function toAddSite() {
} }
// //
function toEditSite(id) { function toEditSite(id: string) {
tab.navigateTo(`/pages_template/pages/address/addSite?id=${id}`); tab.navigateTo(`/pages_template/pages/address/addSite?id=${id}`);
} }
// //
function setAsDefault(id) { function handleSetDefault(id: string) {
try { try {
let addressList = uni.getStorageSync('addressList') || []; const success = setDefaultAddress(id);
addressList = addressList.map(item => {
return { ...item, isDefault: item.id === id };
});
uni.setStorageSync('addressList', addressList); if (success) {
getData(); // uni.showToast({
title: '设置成功',
uni.showToast({ icon: 'success'
title: '设置成功', });
icon: 'success' } else {
}); uni.showToast({
title: '设置失败',
icon: 'none'
});
}
} catch (e) { } catch (e) {
console.error('设置默认地址失败', e); console.error('设置默认地址失败', e);
uni.showToast({ uni.showToast({
@ -61,22 +46,26 @@ function setAsDefault(id) {
} }
// //
function deleteAddress(id) { function handleDeleteAddress(id: string) {
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
content: '确定要删除此地址吗?', content: '确定要删除此地址吗?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
try { try {
let addressList = uni.getStorageSync('addressList') || []; const success = deleteAddress(id);
addressList = addressList.filter(item => item.id !== id);
uni.setStorageSync('addressList', addressList);
getData(); //
uni.showToast({ if (success) {
title: '删除成功', uni.showToast({
icon: 'success' title: '删除成功',
}); icon: 'success'
});
} else {
uni.showToast({
title: '删除失败',
icon: 'none'
});
}
} catch (e) { } catch (e) {
console.error('删除地址失败', e); console.error('删除地址失败', e);
uni.showToast({ uni.showToast({
@ -90,9 +79,9 @@ function deleteAddress(id) {
} }
// //
function selectAddress(address) { function selectAddress(address: AddressInfo) {
const pages = getCurrentPages(); const pages = getCurrentPages();
const prevPage = pages[pages.length - 2]; const prevPage: any = pages[pages.length - 2];
// //
if (prevPage && prevPage.$page?.options?.from === 'order') { if (prevPage && prevPage.$page?.options?.from === 'order') {
@ -102,6 +91,7 @@ function selectAddress(address) {
} }
} }
</script> </script>
<template> <template>
<view class="address-container"> <view class="address-container">
<!-- 空状态 --> <!-- 空状态 -->
@ -112,7 +102,7 @@ function selectAddress(address) {
<!-- 地址列表 --> <!-- 地址列表 -->
<view v-else> <view v-else>
<view class="item" v-for="(address, index) in siteList" :key="address.id"> <view class="item" v-for="(address, index) in addressList" :key="address.id">
<view class="top" @tap="selectAddress(address)"> <view class="top" @tap="selectAddress(address)">
<view class="name">{{ address.name }}</view> <view class="name">{{ address.name }}</view>
<view class="phone">{{ address.phone }}</view> <view class="phone">{{ address.phone }}</view>
@ -125,7 +115,7 @@ function selectAddress(address) {
{{ address.region }} {{ address.address }} {{ address.region }} {{ address.address }}
</view> </view>
<view class="actions"> <view class="actions">
<view class="action-btn" @tap="setAsDefault(address.id)" v-if="!address.isDefault"> <view class="action-btn" @tap="handleSetDefault(address.id)" v-if="!address.isDefault">
<u-icon name="checkmark-circle" color="#999" size="40rpx"></u-icon> <u-icon name="checkmark-circle" color="#999" size="40rpx"></u-icon>
<text>设为默认</text> <text>设为默认</text>
</view> </view>
@ -133,7 +123,7 @@ function selectAddress(address) {
<u-icon name="edit-pen" color="#999" size="40rpx"></u-icon> <u-icon name="edit-pen" color="#999" size="40rpx"></u-icon>
<text>编辑</text> <text>编辑</text>
</view> </view>
<view class="action-btn" @tap="deleteAddress(address.id)"> <view class="action-btn" @tap="handleDeleteAddress(address.id)">
<u-icon name="trash" color="#999" size="40rpx"></u-icon> <u-icon name="trash" color="#999" size="40rpx"></u-icon>
<text>删除</text> <text>删除</text>
</view> </view>

View File

@ -1,11 +1,6 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import citySelect from '@/components/u-city-select/u-city-select.vue'; import citySelect from '@/components/u-city-select/u-city-select.vue';
const height = ref(30);
const bgColor = ref(uni.$u.color.bgColor);
const marginTop = ref(30);
const marginBottom = ref(30);
const value = ref(false); const value = ref(false);
const input = ref(''); const input = ref('');

View File

@ -126,29 +126,43 @@ const getComment = () => {
.comment { .comment {
display: flex; display: flex;
padding: 30rpx; padding: 30rpx;
margin-bottom: 20rpx;
background-color: #ffffff;
border-radius: 12rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s;
&:hover {
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
}
.left { .left {
image { image {
width: 64rpx; width: 72rpx;
height: 64rpx; height: 72rpx;
border-radius: 50%; border-radius: 50%;
background-color: #f2f2f2; background-color: #f2f2f2;
border: 2rpx solid #eaeaea;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
} }
} }
.right { .right {
flex: 1; flex: 1;
padding-left: 20rpx; padding-left: 24rpx;
font-size: 30rpx; font-size: 28rpx;
line-height: 1.6;
.top { .top {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 10rpx; margin-bottom: 12rpx;
.name { .name {
color: #5677fc; color: #5677fc;
font-weight: 500;
font-size: 30rpx;
} }
.like { .like {
@ -156,9 +170,16 @@ const getComment = () => {
align-items: center; align-items: center;
color: #9a9a9a; color: #9a9a9a;
font-size: 26rpx; font-size: 26rpx;
padding: 4rpx 12rpx;
border-radius: 30rpx;
transition: all 0.2s;
&:active {
background-color: rgba(86, 119, 252, 0.1);
}
.num { .num {
margin-right: 4rpx; margin-right: 8rpx;
color: #9a9a9a; color: #9a9a9a;
} }
} }
@ -173,20 +194,32 @@ const getComment = () => {
} }
.content { .content {
margin-bottom: 10rpx; margin-bottom: 16rpx;
color: #333333;
line-height: 1.8;
} }
.reply-box { .reply-box {
background-color: rgb(242, 242, 242); background-color: #f7f7f7;
border-radius: 12rpx; border-radius: 12rpx;
margin-top: 12rpx;
margin-bottom: 8rpx;
overflow: hidden;
.item { .item {
padding: 20rpx; padding: 20rpx;
border-bottom: solid 2rpx $u-border-color; border-bottom: solid 1rpx rgba(0, 0, 0, 0.05);
.username { .username {
font-size: 24rpx; font-size: 26rpx;
color: #999999; color: #5677fc;
font-weight: 500;
margin-bottom: 6rpx;
}
.text {
font-size: 28rpx;
color: #333333;
} }
} }
@ -195,6 +228,12 @@ const getComment = () => {
display: flex; display: flex;
color: #5677fc; color: #5677fc;
align-items: center; align-items: center;
font-size: 26rpx;
transition: all 0.2s;
&:active {
background-color: rgba(86, 119, 252, 0.1);
}
.more { .more {
margin-left: 6rpx; margin-left: 6rpx;
@ -207,10 +246,18 @@ const getComment = () => {
display: flex; display: flex;
font-size: 24rpx; font-size: 24rpx;
color: #9a9a9a; color: #9a9a9a;
align-items: center;
.reply { .reply {
color: #5677fc; color: #5677fc;
margin-left: 10rpx; margin-left: 16rpx;
padding: 4rpx 16rpx;
border-radius: 30rpx;
transition: all 0.2s;
&:active {
background-color: rgba(86, 119, 252, 0.1);
}
} }
} }
} }

View File

@ -0,0 +1,197 @@
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
order: {
type: Object,
required: true
}
});
//
const priceDecimal = (val) => {
if (val !== parseInt(val)) return val.slice(-2);
else return '00';
};
//
const priceInt = (val) => {
if (val !== parseInt(val)) return val.split('.')[0];
else return val;
};
//
const totalPrice = (item) => {
let price = 0;
item.forEach(val => {
price += parseFloat(val.price);
});
return price.toFixed(2);
};
//
const totalNum = (item) => {
let num = 0;
item.forEach(val => {
num += val.number;
});
return num;
};
</script>
<template>
<view class="order">
<view class="top">
<view class="left">
<u-icon name="home" :size="30" color="rgb(94,94,94)"></u-icon>
<view class="store">{{ order.store }}</view>
<u-icon name="arrow-right" color="rgb(203,203,203)" :size="26"></u-icon>
</view>
<view class="right">{{ order.deal }}</view>
</view>
<view class="item" v-for="(item, index) in order.goodsList" :key="index">
<view class="left">
<image :src="item.goodsUrl" mode="aspectFill"></image>
</view>
<view class="content">
<view class="title u-line-2">{{ item.title }}</view>
<view class="type">{{ item.type }}</view>
<view class="delivery-time">发货时间 {{ item.deliveryTime }}</view>
</view>
<view class="right">
<view class="price">
{{ priceInt(item.price) }}
<text class="decimal">.{{ priceDecimal(item.price) }}</text>
</view>
<view class="number">x{{ item.number }}</view>
</view>
</view>
<view class="total">
{{ totalNum(order.goodsList) }}件商品 合计:
<text class="total-price">
{{ priceInt(totalPrice(order.goodsList)) }}.
<text class="decimal">{{ priceDecimal(totalPrice(order.goodsList)) }}</text>
</text>
</view>
<view class="bottom">
<view class="more"><u-icon name="more-dot-fill" color="rgb(203,203,203)"></u-icon></view>
<view class="logistics btn">查看物流</view>
<view class="exchange btn">卖了换钱</view>
<view class="evaluate btn">评价</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.order {
width: 710rpx;
background-color: #ffffff;
margin: 20rpx auto;
border-radius: 20rpx;
box-sizing: border-box;
padding: 20rpx;
font-size: 28rpx;
.top {
display: flex;
justify-content: space-between;
.left {
display: flex;
align-items: center;
.store {
margin: 0 10rpx;
font-size: 32rpx;
font-weight: bold;
}
}
.right {
color: $u-warning-dark;
}
}
.item {
display: flex;
margin: 20rpx 0 0;
.left {
margin-right: 20rpx;
image {
width: 200rpx;
height: 200rpx;
border-radius: 10rpx;
}
}
.content {
.title {
font-size: 28rpx;
line-height: 50rpx;
}
.type {
margin: 10rpx 0;
font-size: 24rpx;
color: $u-tips-color;
}
.delivery-time {
color: #e5d001;
font-size: 24rpx;
}
}
.right {
margin-left: 10rpx;
padding-top: 20rpx;
text-align: right;
.decimal {
font-size: 24rpx;
margin-top: 4rpx;
}
.number {
color: $u-tips-color;
font-size: 24rpx;
}
}
}
.total {
margin-top: 20rpx;
text-align: right;
font-size: 24rpx;
.total-price {
font-size: 32rpx;
}
}
.bottom {
display: flex;
margin-top: 40rpx;
padding: 0 10rpx;
justify-content: space-between;
align-items: center;
.btn {
line-height: 52rpx;
width: 160rpx;
border-radius: 26rpx;
border: 2rpx solid $u-border-color;
font-size: 26rpx;
text-align: center;
color: $u-info-dark;
}
.evaluate {
color: $u-warning-dark;
border-color: $u-warning-dark;
}
}
}
</style>

View File

@ -1,5 +1,6 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue'; import { ref, reactive, onMounted } from 'vue';
import OrderItem from './OrderItem.vue';
const orderList = ref([[], [], [], []]); const orderList = ref([[], [], [], []]);
const dataList = reactive([ const dataList = reactive([
@ -119,18 +120,6 @@ onMounted(() => {
getOrderList(3); getOrderList(3);
}); });
//
const priceDecimal = (val) => {
if (val !== parseInt(val)) return val.slice(-2);
else return '00';
};
//
const priceInt = (val) => {
if (val !== parseInt(val)) return val.split('.')[0];
else return val;
};
// //
const reachBottom = () => { const reachBottom = () => {
loadStatus.value.splice(current.value, 1, "loading"); loadStatus.value.splice(current.value, 1, "loading");
@ -151,34 +140,21 @@ const getOrderList = (idx) => {
loadStatus.value.splice(current.value, 1, "loadmore"); loadStatus.value.splice(current.value, 1, "loadmore");
}; };
//
const totalPrice = (item) => {
let price = 0;
item.forEach(val => {
price += parseFloat(val.price);
});
return price.toFixed(2);
};
//
const totalNum = (item) => {
let num = 0;
item.forEach(val => {
num += val.number;
});
return num;
};
// tab // tab
const change = ({ index }) => { const change = ({ index }) => {
current.value = index; // current
swiperCurrent.value = index; swiperCurrent.value = index;
getOrderList(index); getOrderList(index);
}; };
const animationfinish = ({ detail: { current } }) => { const animationfinish = (e) => {
swiperCurrent.value = current; const currentIndex = e.detail.current;
current.value = current; swiperCurrent.value = currentIndex;
current.value = currentIndex; // currentswipercurrent
}; };
// tabsref
const tabs = ref(null);
</script> </script>
<template> <template>
@ -193,62 +169,20 @@ const animationfinish = ({ detail: { current } }) => {
<scroll-view scroll-y style="height: 100%;width: 100%;" @scrolltolower="reachBottom" <scroll-view scroll-y style="height: 100%;width: 100%;" @scrolltolower="reachBottom"
v-if="orderlist.length !== 0"> v-if="orderlist.length !== 0">
<view class="page-box"> <view class="page-box">
<view class="order" v-for="(res, index) in orderlist" :key="res.id"> <OrderItem v-for="res in orderlist" :key="res.id" :order="res" />
<view class="top">
<view class="left">
<u-icon name="home" :size="30" color="rgb(94,94,94)"></u-icon>
<view class="store">{{ res.store }}</view>
<u-icon name="arrow-right" color="rgb(203,203,203)" :size="26"></u-icon>
</view>
<view class="right">{{ res.deal }}</view>
</view>
<view class="item" v-for="(item, index) in res.goodsList" :key="index">
<view class="left">
<image :src="item.goodsUrl" mode="aspectFill"></image>
</view>
<view class="content">
<view class="title u-line-2">{{ item.title }}</view>
<view class="type">{{ item.type }}</view>
<view class="delivery-time">发货时间 {{ item.deliveryTime }}</view>
</view>
<view class="right">
<view class="price">
{{ priceInt(item.price) }}
<text class="decimal">.{{ priceDecimal(item.price) }}</text>
</view>
<view class="number">x{{ item.number }}</view>
</view>
</view>
<view class="total">
{{ totalNum(res.goodsList) }}件商品 合计:
<text class="total-price">
{{ priceInt(totalPrice(res.goodsList)) }}.
<text class="decimal">{{ priceDecimal(totalPrice(res.goodsList)) }}</text>
</text>
</view>
<view class="bottom">
<view class="more"><u-icon name="more-dot-fill" color="rgb(203,203,203)"></u-icon>
</view>
<view class="logistics btn">查看物流</view>
<view class="exchange btn">卖了换钱</view>
<view class="evaluate btn">评价</view>
</view>
</view>
<u-loadmore :status="loadStatus[0]" bgColor="#f2f2f2"></u-loadmore> <u-loadmore :status="loadStatus[0]" bgColor="#f2f2f2"></u-loadmore>
</view> </view>
</scroll-view> </scroll-view>
<scroll-view scroll-y style="height: 100%;width: 100%;" v-else> <scroll-view scroll-y style="height: 100%;width: 100%;" v-else>
<view class="page-box"> <view class="page-box">
<view> <view class="centre">
<view class="centre"> <image src="https://cdn.uviewui.com/uview/template/taobao-order.png" mode="aspectFit" class="empty-image">
<image src="https://cdn.uviewui.com/uview/template/taobao-order.png" mode=""> </image>
</image> <view class="explain">
<view class="explain"> 您还没有相关的订单
您还没有相关的订单 <view class="tips">可以去看看有那些想买的</view>
<view class="tips">可以去看看有那些想买的</view>
</view>
<view class="btn">随便逛逛</view>
</view> </view>
<view class="btn">随便逛逛</view>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@ -268,128 +202,18 @@ page {
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
.order {
width: 710rpx;
background-color: #ffffff;
margin: 20rpx auto;
border-radius: 20rpx;
box-sizing: border-box;
padding: 20rpx;
font-size: 28rpx;
.top {
display: flex;
justify-content: space-between;
.left {
display: flex;
align-items: center;
.store {
margin: 0 10rpx;
font-size: 32rpx;
font-weight: bold;
}
}
.right {
color: $u-warning-dark;
}
}
.item {
display: flex;
margin: 20rpx 0 0;
.left {
margin-right: 20rpx;
image {
width: 200rpx;
height: 200rpx;
border-radius: 10rpx;
}
}
.content {
.title {
font-size: 28rpx;
line-height: 50rpx;
}
.type {
margin: 10rpx 0;
font-size: 24rpx;
color: $u-tips-color;
}
.delivery-time {
color: #e5d001;
font-size: 24rpx;
}
}
.right {
margin-left: 10rpx;
padding-top: 20rpx;
text-align: right;
.decimal {
font-size: 24rpx;
margin-top: 4rpx;
}
.number {
color: $u-tips-color;
font-size: 24rpx;
}
}
}
.total {
margin-top: 20rpx;
text-align: right;
font-size: 24rpx;
.total-price {
font-size: 32rpx;
}
}
.bottom {
display: flex;
margin-top: 40rpx;
padding: 0 10rpx;
justify-content: space-between;
align-items: center;
.btn {
line-height: 52rpx;
width: 160rpx;
border-radius: 26rpx;
border: 2rpx solid $u-border-color;
font-size: 26rpx;
text-align: center;
color: $u-info-dark;
}
.evaluate {
color: $u-warning-dark;
border-color: $u-warning-dark;
}
}
}
.centre { .centre {
text-align: center; text-align: center;
margin: 200rpx auto; margin: 200rpx auto;
font-size: 32rpx; font-size: 32rpx;
width: 100%;
image { .empty-image {
width: 164rpx; width: 164rpx;
height: 164rpx; height: 164rpx;
border-radius: 50%; border-radius: 50%;
margin-bottom: 20rpx; margin: 0 auto 20rpx;
display: block; /* 确保图片作为块级元素 */
} }
.tips { .tips {