新增全局 YjyfTabBar 组件,替换各页面自定义导航,实现一致的 tab 切换逻辑与图标样式 新增全局设计令牌与 SCSS 变量系统,统一色彩、间距、字阶、圆角与投影规范 新增 POST 请求防并发锁,避免重复提交 重构多个页面,添加骨架屏、空状态引导、点击反馈类,修复硬编码样式与假数据 删除废弃的后台入口文件与 nginx 配置,清理冗余代码
168 行
8.0 KiB
Vue
168 行
8.0 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="list">
|
||
<view class="item" v-for="a in list" :key="a.id">
|
||
<view class="item-top">
|
||
<text class="contact">{{ a.contact }}</text>
|
||
<text class="mobile">{{ a.mobile }}</text>
|
||
<text class="badge" v-if="Number(a.is_default)">默认</text>
|
||
</view>
|
||
<text class="addr">{{ a.province }}{{ a.city }}{{ a.district }}{{ a.address }}</text>
|
||
<text class="meta">楼层 {{ a.floor }} · {{ Number(a.has_elevator) ? '有电梯' : '无电梯' }}</text>
|
||
<view class="ops">
|
||
<view class="op" v-if="!Number(a.is_default)" @tap="setDefault(a)">设为默认</view>
|
||
<view class="op" @tap="edit(a)">编辑</view>
|
||
<view class="op danger" @tap="remove(a)">删除</view>
|
||
</view>
|
||
</view>
|
||
<view class="empty" v-if="!loading && !list.length">还没有服务地址,先添加一个吧</view>
|
||
</view>
|
||
|
||
<view class="bar">
|
||
<view class="bar-btn clickable-hover" @tap="openForm()">+ 新增服务地址</view>
|
||
</view>
|
||
|
||
<view class="mask" v-if="showForm" @tap="showForm = false">
|
||
<view class="sheet" @tap.stop>
|
||
<view class="sheet-head">
|
||
<text class="sheet-title">{{ form.id ? '编辑地址' : '新增地址' }}</text>
|
||
<text class="sheet-close" @tap="showForm = false">关闭</text>
|
||
</view>
|
||
<view class="fi"><text class="fk">联系人</text><input class="fv" v-model="form.contact" placeholder="收货人姓名" /></view>
|
||
<view class="fi"><text class="fk">手机号</text><input class="fv" v-model="form.mobile" type="number" maxlength="11" placeholder="联系手机号" /></view>
|
||
<view class="fi"><text class="fk">详细地址</text><input class="fv" v-model="form.address" placeholder="小区/楼栋/门牌号" /></view>
|
||
<view class="fi"><text class="fk">楼层</text>
|
||
<view class="stepper">
|
||
<view class="st-btn" @tap="step(-1)">-</view>
|
||
<text class="st-val">{{ form.floor }}</text>
|
||
<view class="st-btn" @tap="step(1)">+</view>
|
||
</view>
|
||
</view>
|
||
<view class="fi"><text class="fk">有无电梯</text>
|
||
<view class="seg">
|
||
<view class="seg-i" :class="{ on: form.has_elevator === 1 }" @tap="form.has_elevator = 1">有</view>
|
||
<view class="seg-i" :class="{ on: form.has_elevator === 0 }" @tap="form.has_elevator = 0">无</view>
|
||
</view>
|
||
</view>
|
||
<view class="fi"><text class="fk">设为默认</text>
|
||
<view class="seg">
|
||
<view class="seg-i" :class="{ on: form.is_default === 1 }" @tap="form.is_default = 1">是</view>
|
||
<view class="seg-i" :class="{ on: form.is_default === 0 }" @tap="form.is_default = 0">否</view>
|
||
</view>
|
||
</view>
|
||
<view class="save clickable-hover" :class="{ disabled: saving }" @tap="save">{{ saving ? '保存中...' : '保存' }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive } from 'vue';
|
||
import { onShow } from '@dcloudio/uni-app';
|
||
import api from '@/utils/api.js';
|
||
import { isLogin, toLogin } from '@/utils/request.js';
|
||
|
||
const list = ref([]);
|
||
const loading = ref(false);
|
||
const showForm = ref(false);
|
||
const saving = ref(false);
|
||
const form = reactive({ id: 0, contact: '', mobile: '', address: '', floor: 1, has_elevator: 1, is_default: 0 });
|
||
|
||
async function load() {
|
||
if (!isLogin()) { toLogin(); return; }
|
||
loading.value = true;
|
||
try {
|
||
const res = await api.addressList();
|
||
list.value = res.list || [];
|
||
} catch (e) {
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function openForm(a) {
|
||
form.id = a ? Number(a.id) : 0;
|
||
form.contact = a ? a.contact : '';
|
||
form.mobile = a ? a.mobile : '';
|
||
form.address = a ? a.address : '';
|
||
form.floor = a ? Number(a.floor) : 1;
|
||
form.has_elevator = a ? Number(a.has_elevator) : 1;
|
||
form.is_default = a ? Number(a.is_default) : 0;
|
||
showForm.value = true;
|
||
}
|
||
function edit(a) { openForm(a); }
|
||
function step(d) { form.floor = Math.max(1, form.floor + d); }
|
||
|
||
async function save() {
|
||
if (saving.value) return;
|
||
if (!form.contact || !form.mobile || !form.address) {
|
||
uni.showToast({ title: '请填写联系人/手机号/地址', icon: 'none' });
|
||
return;
|
||
}
|
||
saving.value = true;
|
||
try {
|
||
await api.addressSave({
|
||
id: form.id, contact: form.contact, mobile: form.mobile,
|
||
province: '贵州省', city: '遵义市', district: '', address: form.address,
|
||
floor: form.floor, has_elevator: form.has_elevator, is_default: form.is_default
|
||
});
|
||
uni.showToast({ title: '已保存' });
|
||
showForm.value = false;
|
||
load();
|
||
} catch (e) {
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
|
||
async function setDefault(a) {
|
||
try { await api.addressDefault(a.id); uni.showToast({ title: '已设为默认' }); load(); } catch (e) {}
|
||
}
|
||
|
||
function remove(a) {
|
||
uni.showModal({
|
||
title: '删除地址', content: '确认删除该地址?',
|
||
success: async (r) => {
|
||
if (!r.confirm) return;
|
||
try { await api.addressDelete(a.id); uni.showToast({ title: '已删除' }); load(); } catch (e) {}
|
||
}
|
||
});
|
||
}
|
||
|
||
onShow(() => load());
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page { min-height: 100vh; background: #F5F6F8; padding: 22rpx 28rpx 180rpx; }
|
||
.item { background: #fff; border-radius: 20rpx; padding: 22rpx 24rpx; margin-bottom: 20rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,.06); }
|
||
.item-top { display: flex; align-items: center; }
|
||
.contact { font-size: 28rpx; font-weight: 600; color: #1F1F1F; }
|
||
.mobile { margin-left: 16rpx; font-size: 24rpx; color: #666; }
|
||
.badge { margin-left: 12rpx; padding: 0 12rpx; line-height: 32rpx; border-radius: 8rpx; background: #FFF3E6; color: #E36B0C; font-size: 20rpx; }
|
||
.addr { display: block; margin-top: 10rpx; font-size: 26rpx; color: #333; line-height: 36rpx; }
|
||
.meta { display: block; margin-top: 6rpx; font-size: 22rpx; color: #9A9A9A; }
|
||
.ops { display: flex; justify-content: flex-end; margin-top: 14rpx; padding-top: 14rpx; border-top: 1rpx solid #F4F4F4; }
|
||
.op { margin-left: 16rpx; padding: 0 24rpx; line-height: 58rpx; border-radius: 29rpx; border: 2rpx solid #EEE; font-size: 24rpx; color: #666; }
|
||
.op.danger { color: #E8331B; border-color: #F8D9D4; }
|
||
.empty { padding: 110rpx 0; text-align: center; font-size: 24rpx; color: #9A9A9A; }
|
||
.bar { position: fixed; left: 0; right: 0; bottom: 0; height: 120rpx; padding: 0 28rpx; padding-bottom: env(safe-area-inset-bottom); display: flex; align-items: center; background: #fff; box-shadow: 0 -6rpx 24rpx rgba(0,0,0,.07); }
|
||
.bar-btn { flex: 1; height: 84rpx; display: flex; align-items: center; justify-content: center; border-radius: 42rpx; color: #fff; font-size: 30rpx; font-weight: 600; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); }
|
||
.mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,.45); display: flex; align-items: flex-end; z-index: 99; }
|
||
.sheet { width: 100%; background: #fff; border-radius: 24rpx 24rpx 0 0; padding: 26rpx 28rpx calc(30rpx + env(safe-area-inset-bottom)); }
|
||
.sheet-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12rpx; }
|
||
.sheet-title { font-size: 30rpx; font-weight: 700; color: #1F1F1F; }
|
||
.sheet-close { font-size: 24rpx; color: #9A9A9A; }
|
||
.fi { display: flex; align-items: center; min-height: 88rpx; border-bottom: 1rpx solid #F4F4F4; }
|
||
.fk { width: 160rpx; font-size: 26rpx; color: #666; }
|
||
.fv { flex: 1; height: 88rpx; font-size: 28rpx; color: #222; }
|
||
.stepper { display: flex; align-items: center; }
|
||
.st-btn { width: 52rpx; height: 52rpx; line-height: 50rpx; text-align: center; border-radius: 26rpx; background: #F5F5F5; color: #666; font-size: 26rpx; }
|
||
.st-val { min-width: 60rpx; text-align: center; font-size: 28rpx; color: #222; }
|
||
.seg { display: flex; }
|
||
.seg-i { padding: 0 26rpx; line-height: 52rpx; margin-right: 12rpx; border-radius: 26rpx; background: #F5F5F5; font-size: 24rpx; color: #666; }
|
||
.seg-i.on { background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); color: #fff; }
|
||
.save { height: 88rpx; margin-top: 22rpx; display: flex; align-items: center; justify-content: center; border-radius: 44rpx; color: #fff; font-size: 30rpx; font-weight: 600; background: linear-gradient(135deg,#FFA93F 0%,#F0710D 100%); }
|
||
.save.disabled { opacity: .7; }
|
||
</style>
|
||
|