feat: 引入uView Plus组件库,新增部分静态页面

This commit is contained in:
admin
2026-01-17 16:55:19 +08:00
parent 34c63780a8
commit 849647d3c9
562 changed files with 73370 additions and 121 deletions

View File

@@ -28,39 +28,41 @@
<!-- 订单列表 -->
<scroll-view class="order-list" scroll-y="true" :style="{ height: listHeight + 'px' }">
<view v-for="order in filteredOrders" :key="order.id" class="order-card">
<!-- 头部编号 -->
<view class="order-header">
<view class="order-badge">{{ order.type === 'pickup' ? '取' : '送' }}</view>
<view class="order-title">
<text class="shop-name">{{ order.shopName }}</text>
<text class="order-id">#{{ order.id }}</text>
</view>
<view class="order-status">{{ order.statusText }}</view>
</view>
<!-- 地址信息 -->
<view class="order-info">
<view class="address-row">
<view class="icon pickup"></view>
<view class="address-content">
<text class="address-title">{{ order.pickupAddress }}</text>
<text class="address-sub">商家已出餐 · {{ order.pickupNote || '' }}</text>
<view @click="toDetail(order.id)">
<!-- 头部编号 -->
<view class="order-header">
<view class="order-badge">{{ order.type === 'pickup' ? '取' : '送' }}</view>
<view class="order-title">
<text class="shop-name">{{ order.shopName }}</text>
<text class="order-id">#{{ order.id }}</text>
</view>
<view class="nav-icon" @click="openMap(order.pickupLat, order.pickupLng, order.pickupAddress)">导航</view>
<view class="order-status">{{ order.statusText }}</view>
</view>
<view class="address-row">
<view class="icon deliver"></view>
<view class="address-content">
<text class="address-title">{{ order.deliveryAddress }}</text>
<text class="address-sub">收货人{{ order.receiverName }} {{ order.receiverPhone ? ('尾号' + (order.receiverPhone + '').slice(-4)) : ''}}</text>
<!-- 地址信息 -->
<view class="order-info">
<view class="address-row">
<view class="icon pickup"></view>
<view class="address-content">
<text class="address-title">{{ order.pickupAddress }}</text>
<text class="address-sub">商家已出餐 · {{ order.pickupNote || '' }}</text>
</view>
<view class="nav-icon" @click="openMap(order.pickupLat, order.pickupLng, order.pickupAddress)">导航</view>
</view>
<view class="address-row">
<view class="icon deliver"></view>
<view class="address-content">
<text class="address-title">{{ order.deliveryAddress }}</text>
<text class="address-sub">收货人{{ order.receiverName }} {{ order.receiverPhone ? ('尾号' + (order.receiverPhone + '').slice(-4)) : ''}}</text>
</view>
<view class="nav-icon" @click="openMap(order.deliveryLat, order.deliveryLng, order.deliveryAddress)">导航</view>
</view>
<view class="nav-icon" @click="openMap(order.deliveryLat, order.deliveryLng, order.deliveryAddress)">导航</view>
</view>
</view>
<!-- 备注 -->
<view class="order-note" v-if="order.note">
<text>顾客{{ order.note }}</text>
<!-- 备注 -->
<view class="order-note" v-if="order.note">
<text>顾客{{ order.note }}</text>
</view>
</view>
<!-- 操作区 -->
@@ -86,6 +88,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import sheep from '@/sheep';
import { onShow } from '@dcloudio/uni-app';
// 驿站/骑手信息(从 store 获取或 mock
const driverInfo = ref({
@@ -214,20 +217,6 @@ function openManualInput() {
});
}
// 入口:计算列表高度适配底部栏
onMounted(() => {
try {
const sys = uni.getSystemInfoSync();
const windowHeight = sys.windowHeight || 667;
// 留出顶部和底部空间
listHeight.value = windowHeight - 200;
} catch (e) {
// ignore
}
});
// 顶部安全区处理:参考 pages/index/user.vue 的实现
import { onShow } from '@dcloudio/uni-app';
const headerStyle = ref({});
function setHeaderSafeArea() {
@@ -246,17 +235,15 @@ function setHeaderSafeArea() {
}
}
//跳转订单详情
function toDetail(id) {
uni.navigateTo({
url: `/pages/order/detail?orderId=${id}`
})
}
onMounted(() => {
setHeaderSafeArea();
// 计算列表高度适配底部栏
try {
const sys = uni.getSystemInfoSync();
const windowHeight = sys.windowHeight || 667;
// 留出顶部和底部空间
listHeight.value = windowHeight - 200;
} catch (e) {
// ignore
}
// setHeaderSafeArea();
});
onShow(() => {

View File

@@ -4,13 +4,16 @@
<view class="header-wrap" :style="headerStyle">
<view class="header-bg"></view>
<view class="header-inner">
<image class="avatar" :src="user.avatar || defautAvatar"></image>
<view class="user-meta">
<view class="user-name">{{ user.nickName || '姓名(账号)' }}</view>
<image class="avatar" :src="userInfo.avatar || defautAvatar"></image>
<view class="user-meta" v-if="userInfo.nickname">
<view class="user-name">{{ userInfo.nickname + `(${userInfo.mobile})` }}</view>
<view class="user-status" @click="handleStatusToggle">
{{ user.isOnline ? '在线' : '离线' }}<uni-icons style="margin-left:10rpx;" type="right" size="13" color="#fff"></uni-icons>
{{ userInfo.isOnline ? '在线' : '离线' }}<uni-icons style="margin-left:10rpx;" type="right" size="13" color="#fff"></uni-icons>
</view>
</view>
<view class="user-meta" v-else>
<view class="user-name" @tap="login">请登录</view>
</view>
</view>
</view>
@@ -73,7 +76,8 @@
<script setup>
import {
computed,
ref
ref,
watch
} from 'vue';
import {
onShow,
@@ -81,12 +85,15 @@
onPullDownRefresh
} from '@dcloudio/uni-app';
import sheep from '@/sheep';
import {
showAuthModal,
} from '@/sheep/hooks/useModal';
// 现有 store / 模板数据
const template = computed(() => sheep.$store('app').template.user);
const isLogin = computed(() => sheep.$store('user').isLogin);
const user = ref({});
const userInfo = computed(() => sheep.$store('user').userInfo);
const todayIncome = ref(0);
const todayOrders = ref(0);
const showBind = ref(false);
@@ -115,9 +122,8 @@
// 页面显示时拉取用户信息并填充统计数据(从 store 获取或使用占位)
onShow(async () => {
const data = await sheep.$store('user').getInfo();
const data = userInfo.value;
if (data) {
user.value = data;
// 兼容后端字段名,优先使用 data.todayIncome / data.income / placeholder
todayIncome.value = data.todayIncome ?? data.income ?? 137.9;
todayOrders.value = data.todayOrders ?? data.orders ?? 39;
@@ -149,13 +155,13 @@
// 判断用户是否被禁止接单(兼容多种字段)
function isUserForbidden() {
const u = user.value || {};
const u = userInfo.value || {};
return !!(u.forbidden || u.isForbidden || u.forbid || u.forbidReceive || u.disableReceive || u.receive === false);
}
// 点击状态:根据当前状态弹不同的确认框
function handleStatusToggle() {
if (user.value.isOnline) {
if (userInfo.value.isOnline) {
confirmType.value = 'offline';
modalTitle.value = '确认下线?';
modalMsg.value = '下线需平台进行核准\n此时正常接单请留意核准信息';
@@ -186,10 +192,10 @@
showStatusPopup.value = false;
if (type === 'online') {
// TODO: 调用后端接口变更上线状态
user.value.isOnline = true;
userInfo.value.isOnline = true;
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('已上线');
} else if (type === 'offline') {
user.value.isOnline = false;
userInfo.value.isOnline = false;
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('已下线');
} else if (type === 'forbidden') {
// 仅展示信息,无操作
@@ -242,6 +248,11 @@
url: '/pages/public/setting'
});
}
function login() {
showAuthModal();
}
</script>
<style scoped>

619
pages/order/detail.vue Normal file
View File

@@ -0,0 +1,619 @@
<template>
<s-layout title="订单详情" class="set-userinfo-wrap">
<view class="order-detail-page">
<!-- 地图占位可替换为原生 map 组件或第三方地图组件 -->
<view class="map-area">
<!-- 真实项目建议使用 <map> 并渲染 polyline/markers -->
<image class="map-image" src="/static/img/map-placeholder.png" mode="widthFix" v-if="!mapAvailable" />
<map v-else class="map-native" :latitude="order?.pickupLat" :longitude="order?.pickupLng" show-location enable-3D
enable-zoom :scale="16"></map>
<view class="map-overlay">
<view class="eta">距离商家{{ distanceText }}预计{{ etaText }}到达</view>
<view class="nav-btn" @click="navigateToShop">导航到商家</view>
</view>
</view>
<!-- 地址块 -->
<scroll-view class="content" scroll-y="true">
<view class="address-block">
<view class="addr-row">
<view class="badge pickup"></view>
<view class="addr-body">
<text class="addr-title">{{ order?.shopName || '店铺名称' }}</text>
<text class="addr-sub">{{ order?.pickupAddress }}</text>
</view>
<view class="nav-icon" @click="openMap(order?.pickupLat, order?.pickupLng, order?.pickupAddress)">导航</view>
</view>
<view class="addr-row">
<view class="badge deliver"></view>
<view class="addr-body">
<text class="addr-title">{{ order?.deliveryAddress }}</text>
<text
class="addr-sub">收货人{{ order?.receiverName }} {{ order?.receiverPhone ? ('尾号' + (order.receiverPhone + '').slice(-4)) : ''}}</text>
</view>
<view class="nav-icon" @click="openMap(order?.deliveryLat, order?.deliveryLng, order?.deliveryAddress)">导航
</view>
</view>
</view>
<!-- 顾客备注 -->
<view class="note" v-if="order?.note">
<text>顾客{{ order.note }}</text>
</view>
<!-- 商品清单 -->
<view class="goods-list">
<view class="goods-header">
<text>商品清单</text>
<text class="item-count">{{ totalCount }}</text>
<text class="total-price">¥{{ totalPrice.toFixed(2) }}</text>
</view>
<view class="goods-item" v-for="(g, idx) in order?.items || []" :key="idx">
<view class="g-left">
<text class="g-name">{{ g.name }}{{ g.spec ? ('·' + g.spec) : '' }}</text>
<text class="g-qty">×{{ g.quantity }}</text>
</view>
<view class="g-right">¥{{ (g.price || 0).toFixed(2) }}</view>
</view>
</view>
<!-- 联系/记录等 -->
<view class="action-record">
<!-- <view class="call" @click="callPhone(order?.receiverPhone)">
<text>电话联系</text>
</view> -->
<view class="record" @click="toRecord">交接记录</view>
</view>
<!-- 占位底部高度避免内容被底部按钮遮挡 -->
<view style="height:140rpx;"></view>
</scroll-view>
<!-- 底部操作 -->
<view class="fixed-actions">
<view class="left-actions">
<view class="icon-phone" @click="callPhone(order?.receiverPhone)"></view>
</view>
<view class="right-actions">
<view class="btn remind">催单</view>
<view class="btn remind" @click="openRemindPopup">电话联系</view>
<!-- <view class="btn confirm">转单</view> -->
<view class="btn confirm" @click="confirmArrive">确认到店</view>
</view>
</view>
<!-- 催单弹框uView Plus up-popup -->
<up-popup v-model:show="showRemind" mode="bottom" :closeable="false" border-radius="12">
<view class="remind-popup">
<view class="remind-row" @click="callShopPhone">
<text class="remind-title">联系商家</text>
<view class="remind-btn" @click.stop="callShopPhone">拨打电话</view>
</view>
<view class="remind-row" @click="callCustomerPhone">
<text class="remind-title">联系顾客</text>
<view class="remind-btn" @click.stop="callCustomerPhone">拨打电话</view>
</view>
</view>
<template #bottom>
<view class="remind-cancel" @click="showRemind = false">取消</view>
</template>
</up-popup>
</view>
</s-layout>
</template>
<script setup>
import {
ref,
computed
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
import sheep from '@/sheep';
const orderId = ref(null);
const order = ref(null);
const loading = ref(false);
const mapAvailable = ref(false); // 如果需要使用 map 组件,置为 true
// 入口:从页面参数取 orderId然后加载数据
onLoad((options = {}) => {
orderId.value = options.id || options.orderId || null;
fetchOrder();
});
async function fetchOrder() {
loading.value = true;
try {
// 优先尝试平台统一 request项目内可能封装在 sheep.request 或 sheep.api
if (sheep && typeof sheep.request === 'function') {
const res = await sheep.request({
url: '/order/detail',
method: 'GET',
data: {
id: orderId.value
}
});
// 根据封装不同,这里兼容 res.data 或 res
order.value = (res && res.data) ? res.data : res;
} else if (sheep && sheep.$api && sheep.$api.trade && typeof sheep.$api.trade.detail === 'function') {
const res = await sheep.$api.trade.detail({
id: orderId.value
});
order.value = res?.data || res;
} else {
// 回退 mock 数据,避免界面空白,开发时替换为真实接口
order.value = {
id: orderId.value || 1001,
shopName: '店铺名(示例)',
pickupAddress: '广东省广州市天河区学院站荷光路118-121号',
pickupLat: 23.1005,
pickupLng: 113.3301,
deliveryAddress: '广东省广州市天河区华景新城软件园区B栋西梯501',
deliveryLat: 23.105,
deliveryLng: 113.335,
receiverName: '张先生',
receiverPhone: '13900001234',
note: '依据餐量提供餐具',
items: [{
name: '商品名称A',
spec: '规格1',
quantity: 2,
price: 23.89
},
{
name: '商品名称B',
spec: '规格2',
quantity: 1,
price: 45.00
}
]
};
}
} catch (e) {
console.error('fetchOrder error', e);
// 友好提示
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('获取订单失败,请稍后重试');
} finally {
loading.value = false;
}
}
const totalCount = computed(() => {
if (!order.value || !order.value.items) return 0;
return order.value.items.reduce((s, it) => s + (it.quantity || 0), 0);
});
const totalPrice = computed(() => {
if (!order.value || !order.value.items) return 0;
return order.value.items.reduce((s, it) => s + ((it.price || 0) * (it.quantity || 0)), 0);
});
// 显示距离与预计时间(示例,真实项目可使用服务端或高德/百度 SDK 计算)
const distanceText = computed(() => {
// 此处为示例固定值,后续接入定位/路程计算替换
return order.value ? '873m' : '--';
});
const etaText = computed(() => {
return order.value ? '预计十分钟到达' : '--';
});
function goBack() {
uni.navigateBack();
}
function openMap(lat, lng, name) {
if (!lat || !lng) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('无法获取坐标');
return;
}
uni.openLocation({
latitude: Number(lat),
longitude: Number(lng),
name: name || '',
scale: 18
});
}
function navigateToShop() {
openMap(order.value?.pickupLat, order.value?.pickupLng, order.value?.pickupAddress);
}
function callPhone(phone) {
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到联系电话');
return;
}
uni.makePhoneCall({
phoneNumber: phone
});
}
// 催单弹框控制(使用 uView Plus 的 up-popup
const showRemind = ref(false);
function openRemindPopup() {
showRemind.value = true;
}
function closeRemindPopup() {
showRemind.value = false;
}
function callShopPhone() {
// 商家电话优先使用 order.shopPhone否则尝试 fallback
const phone = order.value?.shopPhone || order.value?.shopPhoneNumber || order.value?.receiverPhone || '';
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到商家电话');
return;
}
callPhone(phone);
closeRemindPopup();
}
function callCustomerPhone() {
const phone = order.value?.receiverPhone || '';
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到顾客电话');
return;
}
callPhone(phone);
closeRemindPopup();
}
function confirmArrive() {
// 确认到店:调用接口或本地改变状态
if (!order.value) return;
// 示例:调用后端接口(兼容性判断)
(async () => {
try {
if (sheep && typeof sheep.request === 'function') {
await sheep.request({
url: '/order/confirmArrive',
method: 'POST',
data: {
id: order.value.id
}
});
}
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('已确认到店');
// 可在此刷新订单状态
fetchOrder();
} catch (e) {
console.error(e);
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('确认失败,请重试');
}
})();
}
//跳转交接记录
function toRecord(id) {
uni.navigateTo({
url: `/pages/order/handoverRecord?orderId=${orderId.value}`
})
}
</script>
<style scoped lang="scss">
.order-detail-page {
// background: #fff;
background: #f7f7f7;
min-height: 100vh;
position: relative;
overflow-x: hidden;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
}
.header {
height: 88rpx;
display: flex;
align-items: center;
padding: 0 20rpx;
border-bottom: 1rpx solid #eee;
}
.back {
width: 44rpx;
font-size: 40rpx;
color: #333;
}
.title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: 700;
color: #333;
margin-right: 44rpx;
}
.map-area {
height: 360rpx;
background: #f3f3f3;
position: relative;
overflow: hidden;
}
.map-image {
width: 100%;
height: 100%;
display: block;
box-sizing: border-box;
}
.map-native {
width: 100%;
height: 100%;
}
.map-overlay {
position: absolute;
left: 20rpx;
right: 20rpx;
bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.eta {
background: rgba(255, 255, 255, 0.95);
padding: 10rpx 14rpx;
border-radius: 20rpx;
font-size: 24rpx;
color: #333;
}
.nav-btn {
background: #fff;
padding: 10rpx 14rpx;
border-radius: 20rpx;
color: #1e9fff;
border: 1rpx solid #dbeeff;
}
.content {
padding: 20rpx;
padding-left: 20rpx;
padding-right: 20rpx;
background: #f7f7f7;
min-height: 200rpx;
box-sizing: border-box;
overflow-x: hidden;
}
.address-block {
background: #fff;
padding: 16rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.04);
}
.addr-row {
display: flex;
align-items: flex-start;
padding: 12rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.addr-row:last-child {
border-bottom: none;
}
.badge {
width: 46rpx;
height: 46rpx;
border-radius: 23rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 20rpx;
margin-right: 12rpx;
}
.badge.pickup {
background: #87d6ff;
}
.badge.deliver {
background: #ffd591;
color: #333;
}
.addr-body {
flex: 1;
min-width: 0;
}
.addr-title {
display: block;
font-size: 28rpx;
font-weight: 600;
word-break: break-word;
}
.addr-sub {
display: block;
font-size: 22rpx;
color: #888;
margin-top: 6rpx;
}
.nav-icon {
color: #1e9fff;
padding: 6rpx 8rpx;
}
.note {
margin-top: 12rpx;
background: #fff;
padding: 12rpx;
border-radius: 8rpx;
color: #666;
}
.goods-list {
margin-top: 14rpx;
}
.goods-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx;
background: #fff;
border-radius: 8rpx;
font-weight: 700;
color: #333;
}
.goods-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx;
background: #fff;
margin-top: 8rpx;
border-radius: 8rpx;
box-sizing: border-box;
}
.g-left {
display: flex;
gap: 8rpx;
align-items: center;
}
.g-left {
min-width: 0;
}
.g-name {
font-size: 26rpx;
color: #333;
max-width: 62%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.g-qty {
color: #888;
margin-left: 8rpx;
}
.g-right {
color: #333;
font-weight: 600;
}
.action-record {
display: flex;
gap: 12rpx;
margin-top: 14rpx;
}
.call {
flex: 1;
background: #fff;
padding: 12rpx;
border-radius: 8rpx;
text-align: center;
border: 1rpx solid #ddd;
}
.record {
flex: 2;
background: #fff;
padding: 12rpx;
border-radius: 8rpx;
text-align: center;
border: 1rpx solid #ddd;
}
.fixed-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 140rpx;
display: flex;
align-items: center;
padding: 20rpx;
gap: 12rpx;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.left-actions {
width: 72rpx;
display: flex;
align-items: center;
justify-content: center;
}
.right-actions {
flex: 1;
display: flex;
gap: 12rpx;
justify-content: flex-end;
align-items: center;
}
.btn {
padding: 16rpx 18rpx;
border-radius: 12rpx;
color: #fff;
font-weight: 700;
}
.btn.remind {
background: #f39c12;
}
.btn.confirm {
background: #1e9fff;
}
.remind-popup {
padding: 20rpx 0;
}
.remind-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
background: #f6f6f6;
margin: 10rpx 16rpx;
border-radius: 8rpx;
}
.remind-title {
font-size: 28rpx;
font-weight: 700;
color: #333;
}
.remind-btn {
background: #1e9fff;
color: #fff;
padding: 10rpx 18rpx;
border-radius: 8rpx;
font-weight: 700;
}
.remind-cancel {
text-align: center;
padding: 18rpx 0 70rpx;
color: #666;
font-size: 26rpx;
background: #fff;
margin-top: 10rpx;
}
</style>

View File

@@ -0,0 +1,155 @@
<template>
<s-layout title="交接记录" class="set-userinfo-wrap">
<view class="transfer-page">
<scroll-view class="body" scroll-y>
<u-steps direction="column" :current="currentIndex" class="steps-wrap">
<u-steps-item v-for="(item, idx) in records" :key="idx">
<template #title>
<text class="step-title">{{ item.title }}</text>
</template>
<template #desc>
<text class="step-desc">{{ item.time }}</text>
</template>
<template #content>
<view class="step-content">
<text class="op-name">{{ item.operator }}</text>
<text class="op-note" v-if="item.note"> · {{ item.note }}</text>
</view>
</template>
</u-steps-item>
</u-steps>
<view v-if="records.length === 0" class="empty">暂无交接记录</view>
</scroll-view>
</view>
</s-layout>
</template>
<script setup>
import {
ref,
computed
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
import sheep from '@/sheep';
const records = ref([]);
const orderId = ref(null);
onLoad((options = {}) => {
orderId.value = options.id || options.orderId || null;
fetchRecords();
});
async function fetchRecords() {
try {
if (sheep && typeof sheep.request === 'function') {
const res = await sheep.request({
url: '/order/transferRecords',
method: 'GET',
data: {
id: orderId.value
}
});
records.value = (res && res.data) ? res.data.records || res.data : res.records || res;
} else {
// mock 数据
records.value = [{
title: '已接单',
time: '2026-01-15 10:02',
operator: '系统',
note: '订单自动接单'
},
{
title: '到店取货',
time: '2026-01-15 10:12',
operator: '骑手 张三',
note: '已取货'
},
{
title: '转单给同城骑手',
time: '2026-01-15 10:20',
operator: '客服 小李',
note: '因配送区域调整'
}
];
}
} catch (e) {
console.error('fetchRecords error', e);
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('获取交接记录失败');
records.value = [];
}
}
const currentIndex = computed(() => Math.max(0, records.value.length - 1));
function goBack() {
uni.navigateBack();
}
</script>
<style scoped>
.transfer-page {
background: #fff;
/* min-height: 100vh; */
}
.header {
height: 88rpx;
display: flex;
align-items: center;
padding: 0 20rpx;
border-bottom: 1rpx solid #eee;
}
.body {
padding: 20rpx;
/* background: #f7f7f7; */
/* min-height: calc(100vh - 88rpx); */
box-sizing: border-box;
}
.steps-wrap {
width: 100%;
}
.step-title {
font-weight: 700;
font-size: 32rpx;
color: #333;
display: block;
}
.step-desc {
font-size: 26rpx;
color: #999;
display: block;
margin-top: 6rpx;
}
.step-content {
margin-top: 10rpx;
font-size: 26rpx;
color: #666;
display: flex;
gap: 8rpx;
align-items: center;
flex-wrap: wrap;
}
.op-name {
font-weight: 600;
color: #333;
}
.op-note {
color: #666;
}
.empty {
text-align: center;
color: #999;
padding: 60rpx 0;
}
</style>

View File

@@ -0,0 +1,223 @@
<template>
<s-layout title="工资结算账户信息" class="set-userinfo-wrap">
<view class="page">
<up-form ref="acctForm" :model="form" :rules="rules" labelPosition="left" labelWidth="120">
<up-form-item label="开户行城市" prop="bankCity" :required="true">
<up-input readonly v-model="bankCityLabel" placeholder="省-市" @tap="regionShow = true" />
</up-form-item>
<up-form-item label="开户行别" prop="bankName" :required="true">
<up-picker hasInput :columns="bankOptions" v-model="bankNameLabel" @confirm="onBankConfirm">
<template #trigger>
<up-input readonly v-model="bankNameLabel" placeholder="请选择开户行" />
</template>
</up-picker>
</up-form-item>
<up-form-item label="开户行网点名称" prop="bankBranch" :required="true">
<up-picker hasInput :columns="branchOptions" v-model="bankBranchLabel" @confirm="onBranchConfirm">
<template #trigger>
<up-input readonly v-model="bankBranchLabel" placeholder="请选择网点名称" />
</template>
</up-picker>
</up-form-item>
<up-form-item label="银行卡号" prop="cardNo" :required="true">
<up-input v-model="form.cardNo" placeholder="请输入银行卡号" type="number" maxlength="23" />
</up-form-item>
<up-form-item label="持卡人姓名" prop="cardHolder" :required="true">
<up-input v-model="form.cardHolder" placeholder="请输入持卡人姓名" />
</up-form-item>
<up-form-item label="银行代码" prop="bankCode">
<up-input v-model="form.bankCode" placeholder="请输入银行代码(如有)" />
</up-form-item>
<up-form-item label="手机号" prop="phone" :required="true">
<view class="code-row">
<view style="width:280rpx;">
<up-input v-model="form.phone" placeholder="预留手机号码" type="number" maxlength="11" />
</view>
<view style="width:160rpx;margin-left:10rpx;">
<up-button :disabled="countdown > 0" plain @click="sendCode">
{{ countdown > 0 ? countdown + 's' : '获取验证码' }}
</up-button>
</view>
</view>
</up-form-item>
<up-form-item label="验证码" prop="captcha" :required="true">
<up-input v-model="form.captcha" placeholder="输入验证码" maxlength="6" />
</up-form-item>
<view class="agree-row" @click="agree = !agree">
<view class="checkbox" :class="{checked: agree}"></view>
<text class="agree-text">勾选同意 <text class="link">骑手协议</text> 提交成功后将会有专人与您联系</text>
</view>
<view class="submit-row">
<up-button type="primary" block @click="onSubmitAccount">提交申请审核</up-button>
</view>
</up-form>
</view>
<su-region-picker level="2" :show="regionShow" @confirm="onRegionConfirm" @cancel="regionShow = false" />
</s-layout>
</template>
<script setup>
import { reactive, ref, onBeforeMount } from 'vue'
const form = reactive({
bankCity: '',
bankName: '',
bankBranch: '',
cardNo: '',
cardHolder: '',
bankCode: '',
phone: '',
captcha: '',
})
const rules = {
bankCity: [{ required: true, message: '请选择开户城市' }],
bankName: [{ required: true, message: '请选择开户行别' }],
bankBranch: [{ required: true, message: '请选择网点名称' }],
cardNo: [{ required: true, message: '请输入银行卡号' }],
cardHolder: [{ required: true, message: '请输入持卡人姓名' }],
phone: [
{ required: true, message: '请输入手机号' },
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号码' },
],
captcha: [{ required: true, message: '请输入验证码' }],
}
const acctForm = ref(null)
const regionShow = ref(false)
const bankOptions = [
['中国工商银行', '中国建设银行', '中国农业银行', '中国银行', '交通银行', '招商银行']
]
const branchOptions = [
['请选择网点']
]
const bankNameLabel = ref([])
const bankBranchLabel = ref([])
const bankCityLabel = ref('')
const countdown = ref(0)
let timer = null
const agree = ref(false)
function onBankConfirm(selected) {
const first = Array.isArray(selected) ? selected[0] : selected
bankNameLabel.value = first?.value || first || ''
form.bankName = bankNameLabel.value
// 模拟获取分支列表,根据银行设置简单示例
branchOptions[0] = bankNameLabel.value ? [`${bankNameLabel.value} 总行`, `${bankNameLabel.value} 广州分行`, `${bankNameLabel.value} 天河支行`] : ['请选择网点']
}
function onBranchConfirm(selected) {
const first = Array.isArray(selected) ? selected[0] : selected
bankBranchLabel.value = first?.value || first || ''
form.bankBranch = bankBranchLabel.value
}
function onRegionConfirm(result) {
console.log("result", result);
form.bankCity = result
bankCityLabel.value = `${result.province_name || ''} ${result.city_name || ''}`.trim()
regionShow.value = false
}
function sendCode() {
// 简单校验手机号
if (!/^1\d{10}$/.test(form.phone)) {
uni.showToast({ title: '请输入正确手机号', icon: 'none' })
return
}
if (countdown.value > 0) return
// 触发发送(此处模拟)
uni.showToast({ title: '验证码已发送', icon: 'none' })
countdown.value = 60
timer = setInterval(() => {
if (countdown.value <= 1) {
clearInterval(timer)
countdown.value = 0
timer = null
} else {
countdown.value -= 1
}
}, 1000)
}
async function onSubmitAccount() {
try {
await acctForm.value.validate()
if (!agree.value) {
uni.showToast({ title: '请先同意骑手协议', icon: 'none' })
return
}
// 提交逻辑(示例):打印并提示
console.log('结算表单', JSON.parse(JSON.stringify(form)))
uni.showToast({ title: '提交申请成功', icon: 'none' })
// 跳转到审核中页面
setTimeout(() => {
uni.navigateTo({ url: '/pages/registered/audit' })
}, 600)
} catch (e) {
console.warn('结算表单校验未通过', e)
}
}
onBeforeMount(() => {
// 尝试从注册页恢复部分信息(如持卡人姓名)
try {
const saved = uni.getStorageSync('riderFormData') || null
if (saved) {
form.cardHolder = saved.realName || ''
}
} catch (err) {
// ignore
}
})
</script>
<style lang="scss" scoped>
.page {
padding: 38rpx;
background: #fff;
}
.code-row {
display: flex;
// gap: 12px;
align-items: center;
}
.agree-row {
display: flex;
align-items: center;
padding: 20rpx 0;
gap: 12rpx;
color: #999;
}
.checkbox {
width: 28rpx;
height: 28rpx;
border: 1px solid #ccc;
border-radius: 50%;
}
.checkbox.checked {
background: #09aaff;
border-color: #09aaff;
}
.agree-text {
font-size: 24rpx;
color: #999;
}
.link {
color: #09aaff;
}
.submit-row {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<s-layout class="audit-wrap">
<view class="audit-content">
<view class="icon-wrap" aria-hidden="true">
<svg viewBox="0 0 64 64" class="clock-svg" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="30" fill="#09aaff"/>
<path d="M32 18v14l10 6" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>
</view>
<view class="title">审核中</view>
<view class="desc">审核结果将以短信进行通知通过后分配订单哦~</view>
<view class="btn-row">
<up-button type="primary" plain @click="onDone">完成</up-button>
</view>
</view>
</s-layout>
</template>
<script setup>
import { } from 'vue'
function onDone() {
// 返回首页(重启栈)
uni.reLaunch({ url: '/pages/index/index' })
}
</script>
<style lang="scss" scoped>
.audit-wrap {
background: #fff;
min-height: 100vh;
}
.audit-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding-top: 140rpx;
}
.icon-wrap {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.clock-svg {
width: 100%;
height: 100%;
}
.title {
font-size: 36rpx;
font-weight: 700;
color: #222;
margin-bottom: 20rpx;
}
.desc {
font-size: 24rpx;
color: #999;
text-align: center;
padding: 0 40rpx;
margin-bottom: 60rpx;
}
.btn-row {
width: 520rpx;
}
</style>

View File

@@ -0,0 +1,691 @@
<template>
<s-layout title="注册骑手" class="set-userinfo-wrap">
<view class="page">
<up-form ref="riderForm" :model="form" :rules="rules" labelPosition="left" labelWidth="90">
<view class="section-title">基础信息</view>
<up-form-item label="真实姓名" prop="realName" :required="true">
<up-input v-model="form.realName" placeholder="请输入您的姓名" />
</up-form-item>
<up-form-item label="身份证号" prop="idNo" :required="true">
<up-input v-model="form.idNo" placeholder="数字开头18位号码" maxlength="18" />
</up-form-item>
<up-form-item label="生效日期" prop="birthDate" :required="true">
<up-datetime-picker hasInput v-model="form.birthDate" mode="date" placeholder="请选择身份证生效日期" />
</up-form-item>
<up-form-item label="失效日期" prop="expiryDate" :required="true">
<view class="expiry-row">
<up-radio-group v-model="form.expiryMode" direction="horizontal">
<up-radio name="long" label="长期有效"></up-radio>
<up-radio name="date" label="选择失效日期"></up-radio>
</up-radio-group>
<up-datetime-picker v-if="form.expiryMode === 'date'" hasInput
v-model="form.expiryDate" mode="date" placeholder="选择失效日期" />
</view>
</up-form-item>
<up-form-item label="性别" prop="gender" :required="true">
<up-radio-group v-model="form.gender" direction="horizontal">
<up-radio name="male" label="男"></up-radio>
<up-radio name="female" label="女"></up-radio>
</up-radio-group>
</up-form-item>
<up-form-item label="紧急联系人姓名" prop="emergencyName" :required="true">
<up-input v-model="form.emergencyName" placeholder="请输入" />
</up-form-item>
<up-form-item label="紧急联系人手机" prop="emergencyPhone" :required="true">
<up-input v-model="form.emergencyPhone" placeholder="请输入" type="tel" maxlength="11" />
</up-form-item>
<up-form-item label="上传身份证正反面" prop="idImages">
<view class="upload-row">
<view class="upload-box">
<up-upload :max-count="1" :show-file-list="false" @change="onUploadFront">
<view class="upload-placeholder" v-if="!frontImage">
<up-icon name="+" />
</view>
<up-image v-else :src="frontImage" mode="aspectFill" class="thumb" />
</up-upload>
<text class="hint">国徽面</text>
</view>
<view class="upload-box">
<up-upload :max-count="1" :show-file-list="false" @change="onUploadBack">
<view class="upload-placeholder" v-if="!backImage">
<up-icon name="+" />
</view>
<up-image v-else :src="backImage" mode="aspectFill" class="thumb" />
</up-upload>
<text class="hint">人像面</text>
</view>
</view>
</up-form-item>
<view class="section-title">接单选择</view>
<up-form-item label="职业身份" prop="occupation" :required="true">
<up-radio-group v-model="form.occupation" direction="horizontal">
<up-radio name="student" label="在校学生"></up-radio>
<up-radio name="worker" label="社会人员/职工"></up-radio>
</up-radio-group>
</up-form-item>
<!-- 学生视图兼职意愿可兼职时段健康证 -->
<template v-if="form.occupation === 'student'">
<up-form-item label="兼职意愿" prop="partTimeIntent" :required="true">
<up-picker hasInput :columns="partTimeOptions" v-model="partTimeLabel" @confirm="onPartTimeConfirm"></up-picker>
</up-form-item>
<up-form-item label="请选择可兼职时段" prop="partTimePeriods">
<view class="choose-row" @click="timeShow1 = true">
<text class="muted">{{ partTimePeriodsLabel }}</text>
<text class="status" :class="{empty: form.partTimePeriods.length===0}">{{ form.partTimePeriods.length===0 ? '待完善 >' : '已完善' }}</text>
</view>
</up-form-item>
<up-popup :show="timeShow1" mode="bottom" @close="timeShow1 = false">
<view class="popup-content">
<view class="popup-header">
<text>选择可兼职时段</text>
</view>
<view class="popup-body">
<!-- 网格每行为一天三段时段按钮可切换选中 -->
<view class="time-grid">
<view class="time-row" v-for="(day, dayIndex) in days" :key="dayIndex">
<view class="day-label">{{ day }}</view>
<view class="slots">
<view
class="slot-btn"
:class="{selected: isSlotSelected(dayIndex, 0)}"
@click="toggleSlot(dayIndex, 0)"
key="m"
>08:00~13:00</view>
<view
class="slot-btn"
:class="{selected: isSlotSelected(dayIndex, 1)}"
@click="toggleSlot(dayIndex, 1)"
key="a"
>13:00~17:30</view>
<view
class="slot-btn"
:class="{selected: isSlotSelected(dayIndex, 2)}"
@click="toggleSlot(dayIndex, 2)"
key="e"
>17:00~22:30</view>
</view>
</view>
</view>
</view>
<view class="popup-footer">
<up-button plain @click="onCancelTime">取消</up-button>
<up-button type="primary" @click="onSaveTime">保存</up-button>
</view>
</view>
</up-popup>
</template>
<!-- 社会人员/职工视图类别意向城市健康证 -->
<template v-else>
<up-form-item label="类别" prop="category" :required="true">
<up-picker hasInput :columns="categoryOptions" @confirm="onCategoryConfirm">
<template #trigger>
<up-input readonly v-model="categoryLabel" placeholder="请选择类别" />
</template>
</up-picker>
</up-form-item>
<up-form-item label="意向城市" prop="city" :required="true">
<up-input readonly v-model="cityLabel" placeholder="请选择" @tap="regionShow = true" />
</up-form-item>
</template>
<!-- 健康证学生/社会人员皆可上传 -->
<up-form-item label="健康证" prop="healthCert">
<view class="health-row" @click="openHealthPopup">
<view class="health-left">上传健康证</view>
<view class="health-right">
<up-icon name="arrow-right" color="#6c6c6c" size="21"></up-icon>
</view>
</view>
</up-form-item>
<view class="submit-row">
<up-button type="primary" @click="onSubmit">保存并下一步完善工资结算信息</up-button>
</view>
</up-form>
</view>
<!-- 健康证信息弹框 -->
<up-popup :show="popupShow" mode="bottom" @close="onCancelHealth" :round="16" safeAreaInsetBottom>
<view class="health-popup">
<view class="health-popup-header">
<text class="title">上传健康证</text>
</view>
<view class="health-popup-body">
<view class="field-row">
<text class="label">编号</text>
<up-input v-model="healthNumber" placeholder="请输入健康证编号" class="field-input" />
</view>
<view class="field-row">
<text class="label">类别</text>
<up-input v-model="healthCategory" placeholder="请输入健康证类别" class="field-input" />
</view>
<view class="field-row date-row">
<text class="label">有效日期</text>
<up-datetime-picker hasInput v-model="healthValidStart" mode="date" placeholder="年/月/日" class="date-input" />
<text class="dash"> </text>
<up-datetime-picker hasInput v-model="healthValidEnd" mode="date" placeholder="年/月/日" class="date-input" />
</view>
<view class="upload-list">
<text class="upload-title">上传健康证</text>
<view class="upload-row-inner">
<view v-for="(img, idx) in healthImages" :key="idx" class="upload-item">
<up-image :src="img" class="upload-thumb" mode="aspectFill" />
<view class="remove-btn" @click.stop="removeHealthImage(idx)">×</view>
</view>
<up-upload :max-count="1" :show-file-list="false" @change="onUploadHealth">
<view class="upload-add" v-if="healthImages.length < 4">
<up-icon name="+" />
</view>
</up-upload>
</view>
</view>
</view>
<view class="health-popup-footer">
<up-button plain @click="onCancelHealth">取消</up-button>
<up-button type="primary" class="save-btn" @click="onSaveHealth">保存</up-button>
</view>
</view>
</up-popup>
<su-region-picker :show="regionShow" @confirm="onRegionConfirm" @cancel="regionShow = false" />
</s-layout>
</template>
<script setup>
import { reactive, ref, computed, watch, onBeforeMount } from 'vue'
import AreaApi from '@/sheep/api/system/area';
// import SuRegionPicker from 'sheep/ui/su-region-picker/su-region-picker.vue'
const form = reactive({
realName: '',
idNo: '',
birthDate: '',
expiryMode: 'long',
expiryDate: '',
gender: 'male',
emergencyName: '',
emergencyPhone: '',
idImages: [],
// 接单选择相关字段
occupation: 'student',
partTimeIntent: '',
partTimePeriods: [],
healthCert: '',
// 健康证详情字段(弹窗保存到这里)
healthCertNumber: '',
healthCertCategory: '',
healthCertValidStart: '',
healthCertValidEnd: '',
category: '',
city: '',
})
const timeShow1 = ref(false)
const partTimeOptions = reactive([
['长期至少1学期', '非长期(临时/偶尔兼职)']
])
const partTimeLabel = ref([])
const categoryOptions = [
['全职', '兼职']
]
const categoryLabel = ref([])
const cityOptions = [
{ text: '请选择城市', value: '' },
{ text: '北京市', value: 'beijing' },
{ text: '上海市', value: 'shanghai' },
]
const cityLabel = ref('')
const regionShow = ref(false)
// 可选时段网格数据与选择状态
const days = ['周日','周一','周二','周三','周四','周五','周六']
const timeSlots = ['08:00~13:00','13:00~17:30','17:00~22:30']
// 使用二维布尔数组表示选择状态selectedGrid[dayIndex][slotIndex] = true/false
const selectedGrid = reactive(Array.from({ length: days.length }, () => [false, false, false]))
// 弹窗临时副本,打开时拷贝 selectedGrid 到 tempSelected用于取消恢复
const tempSelected = reactive(Array.from({ length: days.length }, () => [false, false, false]))
const popupShow = ref(false)
function isSlotSelected(dayIndex, slotIndex) {
return tempSelected[dayIndex][slotIndex]
}
function toggleSlot(dayIndex, slotIndex) {
tempSelected[dayIndex][slotIndex] = !tempSelected[dayIndex][slotIndex]
}
function onCancelTime() {
// 恢复原选中状态并关闭弹窗
for (let i = 0; i < days.length; i++) {
for (let j = 0; j < timeSlots.length; j++) {
tempSelected[i][j] = selectedGrid[i][j]
}
}
timeShow1.value = false
}
function onSaveTime() {
// 将 tempSelected 同步到 selectedGrid 和 form.partTimePeriods保存为可读文本
const selections = []
for (let i = 0; i < days.length; i++) {
for (let j = 0; j < timeSlots.length; j++) {
selectedGrid[i][j] = tempSelected[i][j]
if (tempSelected[i][j]) {
selections.push(`${days[i]} ${timeSlots[j]}`)
}
}
}
form.partTimePeriods = selections
timeShow1.value = false
}
// 打开弹窗时将当前选择拷贝到 tempSelected
watch(timeShow1, (val) => {
if (val) {
for (let i = 0; i < days.length; i++) {
for (let j = 0; j < timeSlots.length; j++) {
tempSelected[i][j] = selectedGrid[i][j]
}
}
}
})
const rules = {
realName: [{ required: true, message: '请输入真实姓名' }],
idNo: [
{ required: true, message: '请输入身份证号' },
{ pattern: /^[0-9A-Za-z]{15,18}$/, message: '请输入正确的身份证号' },
],
birthDate: [{ required: true, message: '请选择生效日期' }],
expiryDate: [
{
validator(value) {
if (form.expiryMode === 'date' && !value) {
return false
}
return true
},
message: '请选择失效日期',
},
],
gender: [{ required: true, message: '请选择性别' }],
emergencyName: [{ required: true, message: '请输入紧急联系人姓名' }],
emergencyPhone: [
{ required: true, message: '请输入紧急联系人手机' },
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号码' },
],
}
const riderForm = ref(null)
const frontImage = ref('')
const backImage = ref('')
const healthCert = ref('')
// 健康证弹窗临时状态
const healthImages = reactive([])
const healthNumber = ref('')
const healthCategory = ref('')
const healthValidStart = ref('')
const healthValidEnd = ref('')
const partTimePeriodsLabel = computed(() => {
return form.partTimePeriods.length ? form.partTimePeriods.join('、') : ''
})
function onUploadFront(event) {
const file = Array.isArray(event) ? event[0] : (event.detail || event)
const url = file?.url || file?.path || file?.thumb || ''
frontImage.value = url
form.idImages = [url, form.idImages[1] || '']
}
function onUploadBack(event) {
const file = Array.isArray(event) ? event[0] : (event.detail || event)
const url = file?.url || file?.path || file?.thumb || ''
backImage.value = url
form.idImages = [form.idImages[0] || '', url]
}
function onUploadHealth(event) {
const file = Array.isArray(event) ? event[0] : (event.detail || event)
const url = file?.url || file?.path || file?.thumb || ''
if (url) {
// 限制最多 4 张预览图
if (healthImages.length < 4) {
healthImages.push(url)
}
healthCert.value = url
// 保持兼容form.healthCert 存首张图片(若需要可改为数组)
form.healthCert = url
}
}
function onPartTimeConfirm(selected) {
const first = Array.isArray(selected) ? selected[0] : selected
if (first && (first.value || first.text)) {
form.partTimeIntent = first.value || first.text
partTimeLabel.value = first.text || first.value
}
}
function onCategoryConfirm(selected) {
const first = Array.isArray(selected) ? selected[0] : selected
if (first && (first.value || first.text)) {
form.category = first.value || first.text
categoryLabel.value = first.text || first.value
}
}
function onCityConfirm(selected) {
const first = Array.isArray(selected) ? selected[0] : selected
if (first && (first.value || first.text)) {
form.city = first.value || first.text
cityLabel.value = first.text || first.value
}
}
function onRegionConfirm(result) {
// result: { province_name, province_id, city_name, city_id, district_name, district_id }
form.city = result
cityLabel.value = `${result.province_name || ''} ${result.city_name || ''} ${result.district_name || ''}`.trim()
regionShow.value = false
}
function openHealthPopup() {
// 从表单恢复到弹窗临时状态
healthImages.splice(0, healthImages.length)
if (form.healthCert) {
healthImages.push(form.healthCert)
}
healthNumber.value = form.healthCertNumber || ''
healthCategory.value = form.healthCertCategory || ''
healthValidStart.value = form.healthCertValidStart || ''
healthValidEnd.value = form.healthCertValidEnd || ''
popupShow.value = true
}
function removeHealthImage(index) {
if (index >= 0 && index < healthImages.length) {
healthImages.splice(index, 1)
}
}
function onCancelHealth() {
// 直接关闭弹窗,放弃临时更改
popupShow.value = false
}
function onSaveHealth() {
// 保存弹窗数据回表单
form.healthCert = healthImages.length ? healthImages[0] : ''
healthCert.value = form.healthCert
form.healthCertNumber = healthNumber.value
form.healthCertCategory = healthCategory.value
form.healthCertValidStart = healthValidStart.value
form.healthCertValidEnd = healthValidEnd.value
popupShow.value = false
}
async function onSubmit() {
try {
await riderForm.value.validate()
console.log('提交表单', JSON.parse(JSON.stringify(form)))
// 将已填写的注册信息临时存储,供结算页继续使用
try {
uni.setStorageSync('riderFormData', JSON.parse(JSON.stringify(form)))
} catch (err) {
console.warn('存储注册信息失败', err)
}
// 跳转到工资结算账户信息页面
uni.navigateTo({ url: '/pages/registered/accountInfo' })
} catch (e) {
console.warn('表单校验未通过', e)
}
}
onBeforeMount(() => {
if (!!uni.getStorageSync('areaData')) {
return;
}
// 提前加载省市区数据
AreaApi.getAreaTree().then((res) => {
if (res.code === 0) {
uni.setStorageSync('areaData', res.data);
}
});
});
</script>
<style lang="scss" scoped>
.page {
padding: 38rpx;
background: #fff;
}
.section-title {
padding: 10px 0;
color: #666;
font-weight: 600;
}
.expiry-row {
display: flex;
flex-direction: column;
gap: 8px;
}
.upload-row {
display: flex;
gap: 12px;
}
.upload-box {
display: flex;
flex-direction: column;
align-items: center;
}
.upload-placeholder {
width: 88px;
height: 60px;
border: 1px dashed #ddd;
display: flex;
align-items: center;
justify-content: center;
}
.thumb {
width: 88px;
height: 60px;
border-radius: 4px;
}
.hint {
margin-top: 6px;
color: #999;
font-size: 12px;
}
.submit-row {
margin-top: 20px;
display: flex;
justify-content: center;
}
.time-grid {
padding: 10px 0;
}
.time-row {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.day-label {
width: 90rpx;
color: #333;
}
.slots {
display: flex;
gap: 10px;
flex: 1;
}
.slot-btn {
padding: 8px 12px;
border-radius: 6px;
background: #f5f5f5;
color: #333;
font-size: 24rpx;
}
.slot-btn.selected {
background: #09aaff;
color: #fff;
}
.popup-footer {
display: flex;
gap: 10px;
padding: 12px;
justify-content: space-between;
}
.popup-content {
padding: 50rpx 20rpx 20rpx;
}
.popup-body {
padding: 10rpx 15rpx 20rpx;
}
/* 健康证相关样式 */
.health-row {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding-right: 10rpx;
}
.health-left {
color: #333;
}
.health-right {
display: flex;
align-items: center;
gap: 10rpx;
}
.health-thumbs {
display: flex;
gap: 10rpx;
align-items: center;
}
.health-thumb-wrap {
width: 88px;
height: 88px;
}
.health-thumb {
width: 88px;
height: 88px;
border-radius: 6px;
}
.health-add {
width: 88px;
height: 88px;
border: 1px dashed #ddd;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
color: #999;
}
.health-popup {
padding: 30rpx 30rpx 30rpx;
background: #fff;
}
.health-popup-header .title {
font-weight: 700;
font-size: 32rpx;
text-align: center;
margin-bottom: 10px;
}
.health-popup-body {
padding: 10rpx 0 20rpx;
}
.field-row {
display: flex;
align-items: center;
gap: 38rpx;
margin-bottom: 12px;
}
.label {
width: 90rpx;
color: #666;
}
.field-input {
flex: 1;
}
.date-row .date-input {
width: 35%;
}
.dash {
width: 10rpx;
text-align: center;
color: #999;
}
.upload-list {
margin-top: 10px;
}
.upload-title {
color: #333;
margin-bottom: 8px;
display: block;
}
.upload-row-inner {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.upload-item {
position: relative;
width: 88px;
height: 88px;
}
.upload-thumb {
width: 88px;
height: 88px;
border-radius: 6px;
}
.remove-btn {
position: absolute;
top: -6px;
right: -6px;
background: rgba(0,0,0,0.6);
color: #fff;
width: 24px;
height: 24px;
border-radius: 12px;
text-align: center;
line-height: 24px;
font-size: 18px;
}
.upload-add {
width: 88px;
height: 88px;
border: 1px dashed #ddd;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
color: #999;
}
.health-popup-footer {
display: flex;
justify-content: space-between;
padding-top: 16px;
gap: 12px;
}
.save-btn {
width: 240rpx;
align-self: center;
}
</style>

214
pages/user/orderRecord.vue Normal file
View File

@@ -0,0 +1,214 @@
<template>
<s-layout title="接单记录" class="order-record-page">
<view class="page-content">
<view class="top-select" @tap="showPicker = true">
<view class="select-style">
<text style="margin-right:15rpx;">{{ selectedLabel }}</text>
<up-icon name="arrow-down" color="#757575" size="15"></up-icon>
</view>
</view>
<!-- up-picker -->
<up-picker
:show="showPicker"
:columns="columns"
:defaultIndex="[defaultIndex]"
@confirm="onConfirm"
@cancel="showPicker = false"
@close="showPicker = false"
></up-picker>
<!-- 顶部提示 -->
<view class="notice">数据统计均截止昨日23:59可能存在延迟请耐心等待</view>
<!-- 单量卡片 -->
<view class="card card-volume">
<view class="card-header">
<view class="card-title">单量</view>
<view class="card-link" @click="toList">
查看 <up-icon name="arrow-right" color="#757575" size="15"></up-icon>
</view>
</view>
<view class="card-body volume-body">
<text class="volume-number">10<span class="unit"></span></text>
<text class="volume-sub">已完成</text>
</view>
</view>
<!-- 转单记录卡片 -->
<!-- <view class="card card-transfer">
<view class="card-header">
<view class="left">
<text class="card-title">转单记录</text>
</view>
<view class="card-link">
查看 <up-icon name="arrow-right" color="#757575" size="15"></up-icon>
</view>
</view>
<view class="card-body transfer-body">
<view class="col">
<text class="col-number">0<span class="unit"></span></text>
<text class="col-label">待处理订单</text>
</view>
<view class="col">
<text class="col-number">0<span class="unit"></span></text>
<text class="col-label">我转出的</text>
</view>
<view class="col">
<text class="col-number">0<span class="unit"></span></text>
<text class="col-label">我接收的</text>
</view>
</view>
</view> -->
</view>
</s-layout>
</template>
<script setup>
import { ref } from 'vue';
// picker 显示控制
const showPicker = ref(false);
// 默认选中标签
const selectedLabel = ref('今日');
// 默认选中的索引0今日
const defaultIndex = 0;
// 列数据u-picker 接受 columns 为数组的数组
const columns = [
[
{ text: '今日', value: 'today' },
{ text: '昨日', value: 'yesterday' },
{ text: '近7天', value: 'last7' },
{ text: '本月', value: 'month' },
],
];
function onConfirm(e) {
// e.value 为选中的值数组(每列一个)
const value = e && e.value && e.value[0];
if (value) {
selectedLabel.value = value.text || String(value);
}
showPicker.value = false;
}
function toList() {
uni.navigateTo({
url: '/pages/user/recordList'
})
}
</script>
<style lang="scss" scoped>
.page-content {
padding: 16px;
background: transparent;
}
.top-select {
display: flex;
justify-content: flex-end;
padding: 0 0 25rpx;
}
.select-style {
display: flex;
}
.notice {
background: #f5f5f5;
color: #999;
padding: 10px 12px;
border-radius: 6px;
font-size: 12px;
margin-bottom: 12px;
}
.card {
background: #fff;
border-radius: 8px;
padding: 14px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.card-link {
display: flex;
font-size: 13px;
color: #999;
}
.card-header .left {
display: flex;
align-items: center;
gap: 8px;
}
.tag {
color: #e74c3c;
font-size: 12px;
}
.volume-body {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 8px 0 4px;
}
.volume-number {
font-size: 36px;
font-weight: 700;
color: #333;
}
.unit {
font-size: 18px;
margin-left: 4px;
}
.volume-sub {
color: #999;
font-size: 13px;
margin-top: 6px;
}
.transfer-body {
display: flex;
justify-content: space-between;
padding-top: 6px;
}
.col {
flex: 1;
text-align: center;
}
.col-number {
font-size: 22px;
font-weight: 700;
color: #333;
}
.col-label {
color: #999;
font-size: 12px;
margin-top: 6px;
display: block;
}
</style>

200
pages/user/recordList.vue Normal file
View File

@@ -0,0 +1,200 @@
<template>
<s-layout title="订单记录" class="record-list-page">
<view class="page-wrap">
<scroll-view class="list" scroll-y>
<view class="order-card" v-for="order in orders" :key="order.id">
<!-- 收入 -->
<view class="card-top">
<text class="income">本单收入 {{ order.income }}</text>
</view>
<!-- 分割线 -->
<view class="divider"></view>
<!-- 内容头 -->
<view class="card-header">
<view class="left">
<text class="label">订单号</text>
<text class="order-no">{{ order.orderNo }}</text>
</view>
<text class="status">{{ order.statusText }}</text>
</view>
<!-- 可能的提示 -->
<view v-if="order.notice" class="notice-orange">
{{ order.notice }}
</view>
<!-- 地址信息 -->
<view class="card-body">
<!-- 起点 -->
<view class="addr-row">
<text class="distance">{{ order.pickDistance }}</text>
<view class="addr-content">
<text class="place-title">{{ order.pickName }}</text>
<text class="place-addr">{{ order.pickAddr }}</text>
</view>
</view>
<!-- 终点 -->
<view class="addr-row to">
<text class="distance">{{ order.deliverDistance }}</text>
<view class="addr-content">
<text class="place-title">{{ order.deliverName }}</text>
<text class="place-addr">{{ order.deliverAddr }}</text>
</view>
</view>
</view>
<!-- 底部信息 -->
<view class="card-footer">
<text class="customer">{{ order.customer }}</text>
</view>
</view>
</scroll-view>
</view>
</s-layout>
</template>
<script setup>
import { ref } from 'vue';
// 静态示例数据,后续可对接接口
const orders = ref([
{
id: 1,
income: '12.5',
orderNo: '2021021115544',
statusText: '已完成',
notice: '',
pickDistance: '873m',
pickName: '乐易购-学院站',
pickAddr: '广东省广州市天河区学院站荷光路',
deliverDistance: '1.2km',
deliverName: '广东省广州市天河区**********',
deliverAddr: '张氏(先生) 屋号1254',
customer: '',
},
{
id: 2,
income: '12.5',
orderNo: '2021021115545',
statusText: '顾客取消订单',
notice: '提示:已取餐订单,顾客退款不影响配送费结算',
pickDistance: '873m',
pickName: '乐易购-学院站',
pickAddr: '广东省广州市天河区学院站荷光路',
deliverDistance: '1.2km',
deliverName: '广东省广州市天河区**********',
deliverAddr: '张氏(先生) 屋号1254',
customer: '',
},
{
id: 3,
income: '12.5',
orderNo: '2021021115546',
statusText: '顾客取消订单',
notice: '',
pickDistance: '873m',
pickName: '乐易购-学院站',
pickAddr: '广东省广州市天河区学院站荷光路',
deliverDistance: '1.2km',
deliverName: '广东省广州市天河区**********',
deliverAddr: '张氏(先生) 屋号1254',
customer: '',
},
]);
</script>
<style lang="scss" scoped>
.page-wrap {
padding: 16px;
background: transparent;
}
.list {
min-height: 200px;
}
.order-card {
background: #fff;
border-radius: 8px;
margin-bottom: 14px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.card-top {
padding: 10px 14px;
}
.income {
color: #e74c3c;
font-weight: 600;
}
.divider {
height: 1px;
background: #eee;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
}
.card-header .left {
display: flex;
align-items: center;
}
.label {
color: #666;
font-size: 13px;
margin-right: 6px;
}
.order-no {
color: #222;
font-weight: 600;
}
.status {
color: #999;
font-size: 13px;
}
.notice-orange {
background: #f3a23a;
color: #fff;
padding: 8px 12px;
margin: 6px 14px;
border-radius: 4px;
font-size: 12px;
}
.card-body {
padding: 10px 14px 6px;
}
.addr-row {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
}
.addr-row.to {
margin-top: 8px;
}
.distance {
color: #999;
width: 42px;
font-size: 12px;
}
.addr-content {
flex: 1;
}
.place-title {
display: block;
font-weight: 700;
color: #222;
margin-bottom: 6px;
}
.place-addr {
color: #999;
font-size: 13px;
}
.card-footer {
padding: 10px 14px 14px;
color: #999;
font-size: 13px;
}
</style>