feat: 新增部分页面静态页面

This commit is contained in:
admin
2026-01-24 17:45:54 +08:00
parent 849647d3c9
commit 8c1224999d
20 changed files with 1828 additions and 339 deletions

View File

@@ -1,26 +0,0 @@
<!-- 分类展示first-one 风格 -->
<template>
<view class="ss-flex-col">
<view class="goods-box" v-for="item in pagination.list" :key="item.id">
<s-goods-column
size="sl"
:data="item"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
/>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
const props = defineProps({
pagination: Object,
});
</script>
<style lang="scss" scoped>
.goods-box {
width: 100%;
}
</style>

View File

@@ -1,66 +0,0 @@
<!-- 分类展示first-two 风格 -->
<template>
<view>
<view class="ss-flex flex-wrap">
<view class="goods-box" v-for="item in pagination?.list" :key="item.id">
<view @click="sheep.$router.go('/pages/goods/index', { id: item.id })">
<view class="goods-img">
<image class="goods-img" :src="item.picUrl" mode="aspectFit" />
</view>
<view class="goods-content">
<view class="goods-title ss-line-1 ss-m-b-28">{{ item.name }}</view>
<view class="goods-price">{{ fen2yuan(item.price) }}</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { fen2yuan } from '@/sheep/hooks/useGoods';
const props = defineProps({
pagination: Object,
});
</script>
<style lang="scss" scoped>
.goods-box {
width: calc((100% - 20rpx) / 2);
margin-bottom: 20rpx;
.goods-img {
width: 100%;
height: 246rpx;
border-radius: 10rpx 10rpx 0px 0px;
}
.goods-content {
width: 100%;
background: #ffffff;
box-shadow: 0px 0px 20rpx 4rpx rgba(199, 199, 199, 0.22);
padding: 20rpx 0 32rpx 16rpx;
box-sizing: border-box;
border-radius: 0 0 10rpx 10rpx;
.goods-title {
font-size: 26rpx;
font-weight: bold;
color: #333333;
}
.goods-price {
font-size: 24rpx;
font-family: OPPOSANS;
font-weight: 500;
color: #e1212b;
}
}
&:nth-child(2n + 1) {
margin-right: 20rpx;
}
}
</style>

View File

@@ -0,0 +1,354 @@
<template>
<up-popup :show="show" mode="center" :round="5" safeAreaInsetBottom @close="onClose" :closeable="true">
<view class="popup-wrap" style="width:680rpx;">
<!-- 标题 -->
<view class="popup-header">
<text class="title">催单</text>
<text class="order-no">#{{ orderIndex }}</text>
</view>
<!-- 查询输入 -->
<view class="search-row">
<view style="width:620rpx;">
<input class="search-input" v-model="orderCode" placeholder="输入收件人手机尾号4位+取单号如8927#11" />
</view>
<view style="margin-left:15rpx;">
<up-button type="primary" size="small" text="查询" @click="queryOrder"></up-button>
</view>
</view>
<!-- 取货点 -->
<view class="section">
<view class="section-title">
<text class="icon">🏬</text>
<text class="text">取货点店铺名称</text>
</view>
<text class="muted">{{ pickupName }}</text>
</view>
<!-- 送餐地址 -->
<view class="section">
<view class="section-title">
<text class="icon">🚩</text>
<text class="text">送餐详细地址</text>
</view>
<text class="muted">{{ address }}</text>
<view class="recipient">收货人名称先生 尾号{{ recipientTail }}</view>
</view>
<!-- 客户备注 -->
<view class="customer-note">
<text>顾客{{ customerNote }}</text>
</view>
<!-- 交接备注 下拉 -->
<view class="form-row">
<text class="label">交接备注</text>
<picker :range="remarks" @change="onRemarkChange">
<view class="picker-display">{{ selectedRemarkText }}</view>
</picker>
</view>
<!-- 凭证 上传 -->
<view class="form-row proof-row">
<text class="label required">凭证</text>
<view class="proof-list">
<view v-for="(img, idx) in proofImages" :key="idx" class="proof-item">
<image :src="img" mode="aspectFill" class="proof-img" />
<view class="remove" @click="removeProofImage(idx)"></view>
</view>
<view class="proof-add" @click="addProofImage">+</view>
</view>
</view>
<!-- 底部操作 -->
<view class="footer-row">
<up-button type="success" text="联系上一个骑手" icon="phone" @click="callPhone('13131008612')"></up-button>
<view style="margin-left:15rpx;width:600rpx;">
<up-button type="primary" text="确认交接" @click="onConfirm"></up-button>
</view>
</view>
</view>
</up-popup>
</template>
<script setup>
import {
ref,
reactive,
computed
} from 'vue'
const props = defineProps({
show: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['close', 'confirm'])
// 响应式数据
const orderIndex = ref(1)
const orderCode = ref('')
const pickupName = ref('取货店铺详细地址广东省广州市天河区学院站荷光路')
const address = ref('送餐详细地址广东省广州市天河区华景新城软件园区A栋303室')
const recipientTail = ref('1254')
const customerNote = ref('依据餐量提供餐具')
const remarks = ['请选择 备注内容', '已核对身份证', '缺少配件', '地址异常']
const selectedRemark = ref(0)
const selectedRemarkText = computed(() => remarks[selectedRemark.value] || '')
const proofImages = ref([])
// 方法
const onClose = () => {
emit('close')
}
const queryOrder = () => {
// 简单模拟查询:如果输入有内容则把 orderIndex 加 1 并回填部分数据
if (!orderCode.value) {
uni.showToast({
title: '请输入查询条件',
icon: 'none'
})
return
}
orderIndex.value += 1
// 这里可以调用接口查询并填充 pickupName/address 等
uni.showToast({
title: '查询成功',
icon: 'success'
})
}
// 拨打电话
const callPhone = (phone) => {
if (!phone) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('未找到联系电话');
return;
}
uni.makePhoneCall({
phoneNumber: phone
});
}
const onRemarkChange = (e) => {
selectedRemark.value = e.detail.value
}
const addProofImage = () => {
// 使用 uni.chooseImage 上传
uni.chooseImage({
count: 4,
success(res) {
const tempFiles = res.tempFilePaths || res.tempFiles.map(f => f.path)
proofImages.value = proofImages.value.concat(tempFiles)
}
})
}
const removeProofImage = (idx) => {
proofImages.value.splice(idx, 1)
}
const onConfirm = () => {
// 校验凭证
if (proofImages.value.length === 0) {
uni.showToast({
title: '请上传凭证',
icon: 'none'
})
return
}
const payload = {
orderIndex: orderIndex.value,
orderCode: orderCode.value,
remark: selectedRemarkText.value,
proofImages: proofImages.value
}
emit('confirm', payload)
}
</script>
<style scoped>
.popup-wrap {
padding: 24rpx;
background: #fff;
border-radius: 12rpx;
}
.popup-header {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 18rpx;
}
.title {
color: #00a0df;
font-size: 30rpx;
font-weight: 700;
margin-right: 8rpx;
}
.order-no {
color: #666;
font-size: 24rpx;
}
.search-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.search-input {
flex: 1;
height: 64rpx;
border: 1rpx solid #ddd;
padding: 0 20rpx;
border-radius: 6rpx;
}
.search-btn {
width: 120rpx;
height: 64rpx;
font-size: 22rpx;
background: #fff;
border-left: 1rpx solid #ddd;
}
.section {
padding: 12rpx 0;
border-top: 1rpx solid #f0f0f0;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.section-title .icon {
margin-right: 12rpx;
font-size: 26rpx;
}
.section-title .text {
font-weight: 700;
font-size: 26rpx;
}
.muted {
color: #666;
font-size: 22rpx;
}
.recipient {
color: #999;
font-size: 20rpx;
margin-top: 6rpx;
}
.customer-note {
background: #f7f7f7;
padding: 12rpx;
border-radius: 6rpx;
margin: 12rpx 0;
color: #666;
}
.form-row {
display: flex;
align-items: center;
margin: 12rpx 0;
}
.label {
width: 180rpx;
color: #333;
}
.required {
color: #e53935;
}
.picker-display {
flex: 1;
height: 64rpx;
line-height: 64rpx;
border: 1rpx solid #e6e6e6;
padding: 0 12rpx;
border-radius: 6rpx;
}
.proof-row .proof-list {
display: flex;
align-items: center;
}
.proof-item {
position: relative;
width: 140rpx;
height: 140rpx;
margin-right: 14rpx;
}
.proof-img {
width: 100%;
height: 100%;
border-radius: 6rpx;
}
.remove {
position: absolute;
right: 4rpx;
top: 4rpx;
background: rgba(0, 0, 0, 0.5);
color: #fff;
padding: 2rpx 6rpx;
border-radius: 10rpx;
font-size: 20rpx;
}
.proof-add {
width: 140rpx;
height: 140rpx;
border: 1rpx dashed #ddd;
display: flex;
justify-content: center;
align-items: center;
font-size: 48rpx;
color: #999;
border-radius: 6rpx;
}
.footer-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 18rpx;
}
.phone-area {
display: flex;
align-items: center;
color: #333;
}
.phone-icon {
font-size: 30rpx;
margin-right: 8rpx;
}
.confirm-btn {
background: #0a99e6;
color: #fff;
padding: 14rpx 32rpx;
border-radius: 8rpx;
}
</style>

View File

@@ -1,97 +0,0 @@
<!-- 分类展示second-one 风格 -->
<template>
<view>
<!-- 一级分类的名字 -->
<!-- <view class="title-box ss-flex ss-col-center ss-row-center ss-p-b-30">
<view class="title-line-left" />
<view class="title-text ss-p-x-20">{{ props.data[activeMenu].name }}</view>
<view class="title-line-right" />
</view> -->
<view class="title-box ss-flex ss-p-b-30">
<view class="theme-line"></view>
<view class="title-text">{{ props.data[activeMenu].name }}</view>
</view>
<!-- 二级分类的名字 -->
<view class="goods-item-box ss-flex ss-flex-wrap ss-p-b-20">
<view
class="goods-item"
v-for="item in props.data[activeMenu].children"
:key="item.id"
@tap="
sheep.$router.go('/pages/goods/list', {
categoryId: item.id,
})
"
>
<image class="goods-img" :src="item.picUrl" mode="aspectFill" />
<view class="ss-p-10">
<view class="goods-title ss-line-1">{{ item.name }}</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
activeMenu: [Number, String],
});
</script>
<style lang="scss" scoped>
.title-box {
font-weight: 800;
font-size: 32rpx;
color: #333333;
.title-line-left,
.title-line-right {
width: 15px;
height: 1px;
background: #d2d2d2;
}
}
.goods-item {
width: calc((100% - 20px) / 3);
margin-right: 10px;
margin-bottom: 10px;
&:nth-of-type(3n) {
margin-right: 0;
}
.goods-img {
width: calc((100vw - 140px) / 3);
height: calc((100vw - 140px) / 3);
}
.goods-title {
font-size: 26rpx;
font-weight: bold;
color: #333333;
line-height: 40rpx;
text-align: center;
}
.goods-price {
color: $red;
line-height: 40rpx;
}
}
.theme-line {
margin-right: 15rpx;
width: 8rpx;
height: 28rpx;
background: var(--ui-BG-Main);
border-radius: 8rpx 8rpx 8rpx 8rpx;
}
</style>

View File

@@ -6,7 +6,8 @@
<view class="top-bg"></view>
<view class="top-inner">
<view class="user-info">
<image class="user-avatar" :src="driverInfo.avatar || defaultAvatar" mode="cover" />
<image class="user-avatar" @tap="sheep.$router.go('/pages/index/user')"
:src="driverInfo.avatar || defaultAvatar" mode="cover" />
<view class="user-meta">
<!-- <text class="user-name">{{ driverInfo.nickName || '骑手姓名' }}</text> -->
<text class="user-status" @click="toggleOnline">{{ driverInfo.isOnline ? '在线中' : '离线' }}</text>
@@ -25,6 +26,9 @@
</view>
</view>
<!-- 输入用户单编码 弹窗 -->
<order-code-popup :show="orderPopupShow" @close="orderPopupShow = false" @confirm="onConfirmCode" />
<!-- 订单列表 -->
<scroll-view class="order-list" scroll-y="true" :style="{ height: listHeight + 'px' }">
<view v-for="order in filteredOrders" :key="order.id" class="order-card">
@@ -89,6 +93,7 @@
import { ref, computed, onMounted } from 'vue';
import sheep from '@/sheep';
import { onShow } from '@dcloudio/uni-app';
import OrderCodePopup from './components/order-code-popup.vue';
// 驿站/骑手信息(从 store 获取或 mock
const driverInfo = ref({
@@ -211,10 +216,18 @@ function scanQr() {
});
}
const orderPopupShow = ref(false);
function openManualInput() {
uni.navigateTo({
url: '/pages/index/user' // 示例跳转,按需替换为真实手动输入页面
});
// 打开手动输入弹窗(使用 uView Plus 的 up-popup
orderPopupShow.value = true;
}
function onConfirmCode(payload) {
// payload 包含 code, result, remark, images
// 这里简单展示提示,实际应调用后端或触发下一步逻辑
console.log('confirmed code:', payload);
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('交接已确认');
}
const headerStyle = ref({});

View File

@@ -4,7 +4,7 @@
<view class="header-wrap" :style="headerStyle">
<view class="header-bg"></view>
<view class="header-inner">
<image class="avatar" :src="userInfo.avatar || defautAvatar"></image>
<image class="avatar" :src="userInfo.avatar || defautAvatar" @tap="sheep.$router.go('/pages/user/info')"></image>
<view class="user-meta" v-if="userInfo.nickname">
<view class="user-name">{{ userInfo.nickname + `(${userInfo.mobile})` }}</view>
<view class="user-status" @click="handleStatusToggle">
@@ -23,14 +23,15 @@
<view class="stats-item">
<text class="stats-title">今日预计收入</text>
<text class="stats-value"> {{ formatMoney(todayIncome) }} </text>
<view class="stats-link" @click="openAccount">
<view class="stats-link" @tap="sheep.$router.go('/pages/user/account/index')">
<!-- @click="openAccount" -->
我的账户 <uni-icons type="right" size="13"></uni-icons>
</view>
</view>
<view class="stats-item">
<text class="stats-title">今日完成单量</text>
<text class="stats-value"> {{ todayOrders }} </text>
<view class="stats-link" @click="openOrders">
<view class="stats-link" @tap="sheep.$router.go('/pages/user/order/orderRecord')">
订单统计 <uni-icons type="right" size="13"></uni-icons>
</view>
</view>
@@ -43,11 +44,11 @@
<image class="shortcut-icon" src="/static/img/order1.png" mode="aspectFit" />
<text class="shortcut-text">考勤排班</text>
</view>
<view class="shortcut" @click="openSalary">
<view class="shortcut" @tap="sheep.$router.go('/pages/user/salary/salaryManage')">
<image class="shortcut-icon" src="/static/img/order2.png" mode="aspectFit" />
<text class="shortcut-text">薪资助手</text>
</view>
<view class="shortcut" @click="openSetting">
<view class="shortcut" @tap="sheep.$router.go('/pages/public/setting')">
<!-- <image class="shortcut-icon" src="/static/img/edit.png" mode="aspectFit" /> -->
<view class="shortcut-icon">
<uni-icons type="gear" size="43"></uni-icons>
@@ -214,7 +215,6 @@
onPageScroll(() => {});
// 跳转/交互方法(保留路由调用位置,用户可按需实现)
function goBack() {
uni.navigateBack();
}
@@ -224,31 +224,13 @@
url: '/pages/public/webview?type=account'
});
}
function openOrders() {
uni.navigateTo({
url: '/pages/index/order-list'
});
}
function openAttendance() {
uni.navigateTo({
url: '/pages/public/faq'
});
}
function openSalary() {
uni.navigateTo({
url: '/pages/public/richtext'
});
}
function openSetting() {
uni.navigateTo({
url: '/pages/public/setting'
});
}
function login() {
showAuthModal();
}

View File

@@ -11,23 +11,47 @@
<view class="container-list">
<uni-list :border="false">
<uni-list-item
title="个人信息"
showArrow
clickable
:border="false"
class="list-border"
@tap="
sheep.$router.go('/pages/user/info')
"
/>
<uni-list-item
title="可兼职时段报备"
showArrow
clickable
:border="false"
class="list-border"
@tap="
sheep.$router.go('/pages/user/info')
"
/>
<uni-list-item
title="通知与提示音"
showArrow
clickable
:border="false"
class="list-border"
@tap="
sheep.$router.go('/pages/user/info')
"
/>
<uni-list-item
title="隐私管理"
showArrow
clickable
:border="false"
class="list-border"
@tap="
sheep.$router.go('/pages/user/privacyManage/index')
"
/>
<!-- <uni-list-item
title="当前版本"
:rightText="appInfo.version"
showArrow
clickable
:border="false"
class="list-border"
@tap="onCheckUpdate"
/>
<uni-list-item
title="本地缓存"
:rightText="storageSize"
showArrow
:border="false"
class="list-border"
/>
<uni-list-item
title="关于我们"
showArrow
clickable
@@ -39,6 +63,22 @@
})
"
/> -->
<uni-list-item
title="本地缓存"
:rightText="storageSize"
showArrow
:border="false"
class="list-border"
/>
<uni-list-item
title="版本号"
:rightText="appInfo.version"
showArrow
clickable
:border="false"
class="list-border"
@tap="onCheckUpdate"
/>
<!-- 为了过审 只有 iOS-App 有注销账号功能 -->
<uni-list-item
v-if="isLogin && sheep.$platform.os === 'ios' && sheep.$platform.name === 'App'"
@@ -52,35 +92,6 @@
/>
</uni-list>
</view>
<view class="set-footer ss-flex-col ss-row-center ss-col-center">
<view class="agreement-box ss-flex ss-col-center ss-m-b-40">
<view class="ss-flex ss-col-center ss-m-b-10">
<!-- <view
class="tcp-text"
@tap="
sheep.$router.go('/pages/public/richtext', {
title: '用户协议'
})
"
>
用户协议
</view>
<view class="agreement-text"></view> -->
<view
class="tcp-text"
@tap="
sheep.$router.go('/pages/public/richtext', {
title: '隐私协议'
})
"
>
隐私协议
</view>
</view>
</view>
<!-- <view class="copyright-text ss-m-b-10">{{ appInfo.copyright }}</view> -->
<!-- <view class="copyright-text">{{ appInfo.copytime }}</view> -->
</view>
<su-fixed bottom placeholder>
<view class="ss-p-x-20 ss-p-b-40">
<button

View File

@@ -0,0 +1,169 @@
<template>
<s-layout title="设置" class="auth-manage-page">
<view class="card">
<view class="auth-item" v-for="item in items" :key="item.key" @click="openDialog(item.key)">
<text class="label">{{ item.label }}</text>
<up-icon name="arrow-right" color="#999" size="20"></up-icon>
</view>
</view>
<up-popup :show="showPopup" mode="bottom" @close="showPopup = false" :round="12" safeAreaInsetBottom>
<view class="popup-body">
<view class="title">{{ current.title }}</view>
<view class="desc">{{ current.desc }}</view>
<view class="btn-row">
<up-button plain @click="showPopup = false">再想想</up-button>
<up-button type="primary" @click="onGoSetting">去设置</up-button>
</view>
</view>
</up-popup>
</s-layout>
</template>
<script setup>
import { ref, reactive } from 'vue';
import sheep from '@/sheep';
const showPopup = ref(false);
const currentKey = ref('');
const items = [
{ key: 'album', label: '相册权限', title: '相册权限', desc: '关闭后,将无法上传相册中的照片或视频,也无法下载作品至你的相册' },
{ key: 'camera', label: '相机权限', title: '相机权限', desc: '关闭后,将无法拍摄照片或视频上传' },
{ key: 'location', label: '位置权限', title: '位置权限', desc: '关闭后,将无法获取位置信息,影响部分定位功能' },
{ key: 'microphone', label: '麦克风权限', title: '麦克风权限', desc: '关闭后,将无法录制语音或视频的声音' },
{ key: 'other', label: '其它权限', title: '其它权限', desc: '关闭后,可能影响部分功能的正常使用' },
];
const current = reactive({ title: '', desc: '' });
function openDialog(key) {
currentKey.value = key;
const item = items.find((i) => i.key === key) || items[0];
current.title = item.title;
current.desc = item.desc;
showPopup.value = true;
}
function onGoSetting() {
// 根据平台尝试申请或打开设置页
const key = currentKey.value;
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU
let mpScope = '';
if (key === 'album') mpScope = 'scope.writePhotosAlbum';
else if (key === 'camera') mpScope = 'scope.camera';
else if (key === 'location') mpScope = 'scope.userLocation';
else if (key === 'microphone') mpScope = 'scope.record';
if (mpScope) {
uni.authorize({
scope: mpScope,
success() {
uni.showToast({ title: '授权成功', icon: 'none' });
},
fail() {
// 打开小程序设置页
if (uni.openSetting) {
uni.openSetting({
success() {
//
},
});
} else {
uni.showToast({ title: '请在系统设置中开启权限', icon: 'none' });
}
},
});
}
// #endif
// #ifdef H5
// H5 无统一设置页,提示用户手动调整
uni.showModal({
title: current.title,
content: '请在浏览器或系统设置中为本应用开启该权限',
showCancel: true,
confirmText: '知道了',
});
// #endif
// #ifdef APP-PLUS
// 在 APP 中打开系统设置或应用设置页
openAppSetting();
// #endif
showPopup.value = false;
}
// APP 打开应用设置(参考 uview-plus 实现)
function openAppSetting() {
try {
const isIOS = (plus.os.name && plus.os.name.toLowerCase().indexOf('ios') !== -1);
if (isIOS) {
var UIApplication = plus.ios.import('UIApplication');
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import('NSURL');
var setting2 = NSURL2.URLWithString('app-settings:');
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
} else {
var Intent = plus.android.importClass('android.content.Intent');
var Settings = plus.android.importClass('android.provider.Settings');
var Uri = plus.android.importClass('android.net.Uri');
var mainActivity = plus.android.runtimeMainActivity();
var intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
var uri = Uri.fromParts('package', mainActivity.getPackageName(), null);
intent.setData(uri);
mainActivity.startActivity(intent);
}
} catch (e) {
console.warn('openAppSetting fail', e);
uni.showToast({ title: '打开设置失败,请手动前往系统设置', icon: 'none' });
}
}
</script>
<style scoped>
.card {
background: #fff;
border-radius: 12rpx;
padding: 6rpx 0;
margin: 20rpx;
}
.auth-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.label {
font-size: 28rpx;
color: #333;
}
.popup-body {
padding: 30rpx;
text-align: center;
}
.title {
font-size: 30rpx;
font-weight: 600;
margin-bottom: 18rpx;
}
.desc {
color: #999;
font-size: 26rpx;
line-height: 36rpx;
margin-bottom: 24rpx;
}
.btn-row {
display: flex;
gap: 18rpx;
justify-content: center;
}
.btn-row up-button {
width: 260rpx;
}
</style>

View File

@@ -0,0 +1,270 @@
<template>
<s-layout title="我的账户" class="account-page">
<view class="page-wrap">
<!-- 顶部周期选择 -->
<view class="top-row">
<view class="period-select" @tap="showPicker = true">
<text class="period-label">{{ selectedLabel }}</text>
<up-icon name="arrow-down" color="#333" size="14"></up-icon>
</view>
</view>
<!-- 汇总卡片 -->
<view class="summary-card">
<text class="summary-amount">{{ formatSigned(totalAmount) }}</text>
<text class="summary-sub">预计收入</text>
<up-divider text=""></up-divider>
<text class="summary-note">{{ summaryNote }}</text>
</view>
<!-- 交易列表 -->
<scroll-view class="list" scroll-y>
<view class="txn-item" v-for="item in items" :key="item.id">
<view class="left" style="width:530rpx;">
<text class="txn-title">{{ item.desc }}</text>
<text class="txn-sub">{{ item.date }} {{ item.time }}</text>
</view>
<view class="right">
<text class="txn-amount" :class="{ positive: item.amount >= 0 }">
{{ formatSigned(item.amount) }}
</text>
<text class="txn-status">{{ item.statusText }}</text>
</view>
</view>
<view class="no-more">没有更多了~</view>
</scroll-view>
<!-- picker -->
<up-picker
:show="showPicker"
:columns="columns"
@confirm="onConfirm"
@cancel="showPicker = false"
@close="showPicker = false"
></up-picker>
</view>
</s-layout>
</template>
<script setup>
import { ref, computed } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import PayWalletApi from '@/sheep/api/pay/wallet';
// picker
const showPicker = ref(false);
const selectedLabel = ref('今日账单');
const columns = [
[
{ text: '今日账单', value: 'today' },
{ text: '昨日账单', value: 'yesterday' },
{ text: '本月账单', value: 'month' },
],
];
// 数据
const items = ref([]);
const totalAmount = ref(0);
const summaryNote = ref('');
// 本地回退示例(便于开发)
const testList = [
{ id: 't1', time: '18:02', date: '06-15', desc: '配送收入-#59-林记番薯粥(潮汕白粥,小炒,海鲜鱼)', amount: 6.1, statusText: '未到账' },
{ id: 't2', time: '17:49', date: '06-15', desc: '配送收入-#57-林记番薯粥(潮汕白粥,小炒,海鲜鱼)', amount: 5.3, statusText: '未到账' },
{ id: 't3', time: '17:42', date: '06-15', desc: '配送收入-#25-桐坑粿条(原汤猪肠·柠檬粿条)', amount: 4.6, statusText: '未到账' },
{ id: 't4', time: '17:27', date: '06-15', desc: '配送收入-#110-万辉超市(潮阳店)', amount: 3.6, statusText: '未到账' },
{ id: 't5', time: '16:56', date: '06-15', desc: '配送收入-#145-仓鼠便利超市(潮阳店)', amount: 3.1, statusText: '未到账' },
];
function formatSigned(val) {
const n = Number(val) || 0;
const sign = n > 0 ? '+' : n < 0 ? '' : '+';
return `${sign}${Math.abs(n).toFixed(2)}`;
}
function toDateParts(t) {
const d = t ? new Date(t) : null;
if (!d || isNaN(d.getTime())) return { date: '', time: '' };
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
const hh = String(d.getHours()).padStart(2, '0');
const mi = String(d.getMinutes()).padStart(2, '0');
return { date: `${mm}-${dd}`, time: `${hh}:${mi}` };
}
// 根据 picker value 计算时间范围(返回 start/end Date
function rangeFor(value) {
const now = new Date();
if (value === 'today') {
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
return [start, end];
}
if (value === 'yesterday') {
const y = new Date(now);
y.setDate(now.getDate() - 1);
const start = new Date(y.getFullYear(), y.getMonth(), y.getDate(), 0, 0, 0);
const end = new Date(y.getFullYear(), y.getMonth(), y.getDate(), 23, 59, 59);
return [start, end];
}
// month
const start = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
return [start, end];
}
// 加载并过滤流水
async function load() {
try {
const res = await PayWalletApi.getWalletTransactionPage({ page: 1, size: 200 });
let records = [];
if (res && res.code === 0) {
if (Array.isArray(res.data)) {
records = res.data;
} else if (res.data && Array.isArray(res.data.records)) {
records = res.data.records;
}
}
const pickerValue = columns[0].find((c) => c.text === selectedLabel.value)?.value ?? 'today';
const [start, end] = rangeFor(pickerValue);
let filtered = [];
if (records && records.length) {
filtered = records.filter((r) => {
const t = r.createTime ?? r.createdAt ?? r.time ?? r.create_date ?? r.date;
if (!t) return false;
const d = new Date(t);
if (isNaN(d.getTime())) return false;
return d >= start && d <= end;
});
}
if (!filtered.length) {
// 使用本地回退示例
items.value = testList;
} else {
items.value = filtered.map((r, idx) => {
const t = r.createTime ?? r.createdAt ?? r.time ?? r.create_date ?? r.date;
const parts = toDateParts(t);
return {
id: r.id ?? `r-${idx}`,
date: parts.date,
time: parts.time,
desc: r.remark ?? r.note ?? r.title ?? r.typeName ?? r.description ?? '',
amount: Number(r.amount ?? r.price ?? r.income ?? r.value ?? r.money ?? 0),
statusText: r.statusText ?? r.stateText ?? r.payStatus ?? '',
};
});
}
} catch (e) {
// 回退示例
items.value = testList;
}
// 计算汇总
totalAmount.value = items.value.reduce((s, it) => s + (Number(it.amount) || 0), 0);
// 汇总说明(取前两条简短描述拼接)
summaryNote.value = items.value.slice(0, 2).map((i) => i.desc).join(' ');
}
function onConfirm(e) {
const v = e && e.value && e.value[0];
if (v) {
selectedLabel.value = v.text || String(v);
}
showPicker.value = false;
load();
}
onShow(() => {
load();
});
</script>
<style scoped>
.page-wrap {
padding: 12px;
background: transparent;
min-height: 100vh;
}
.top-row {
margin-bottom: 10px;
display: flex;
justify-content: flex-start;
}
.period-select {
display: flex;
align-items: center;
gap: 8px;
}
.period-label {
font-weight: 700;
}
.summary-card {
background: #fff;
padding: 16px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
margin-bottom: 12px;
}
.summary-amount {
font-size: 20px;
font-weight: 700;
color: #e74c3c;
display: block;
margin-bottom: 6px;
}
.summary-sub {
color: #999;
margin-bottom: 8px;
display: block;
}
.summary-note {
color: #bbb;
font-size: 13px;
}
.list {
margin-top: 10px;
}
.txn-item {
background: #fff;
padding: 12px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
.txn-title {
font-weight: 600;
color: #333;
display: block;
margin-bottom: 6px;
}
.txn-sub {
color: #999;
font-size: 13px;
}
.txn-amount {
font-weight: 700;
color: #e74c3c;
text-align: right;
}
.txn-amount.positive {
color: #e74c3c;
}
.txn-status {
color: #999;
font-size: 12px;
margin-top: 6px;
display: block;
text-align: right;
}
.no-more {
text-align: center;
color: #bbb;
padding: 20px 0;
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<s-layout title="紧急联系人设置" class="contact-page">
<view class="list-wrap">
<view class="list-item">
<text class="item-label">紧急联系人姓名</text>
<!-- <text class="item-value">紧急联系人姓名</text> -->
<view style="width:280rpx;">
<up-input border="0" inputAlign="right" v-model="displayName" placeholder="紧急联系人" />
</view>
</view>
<view class="list-item">
<text class="item-label">联系电话</text>
<view style="width:280rpx;">
<up-input border="0" inputAlign="right" type="number" v-model="displayPhone" placeholder="联系电话" />
</view>
</view>
</view>
<su-fixed bottom placeholder bg="none">
<view class="footer-box ss-p-20">
<up-button type="primary" block @click="onSave">保存</up-button>
</view>
</su-fixed>
</s-layout>
</template>
<script setup>
import { computed } from 'vue';
import sheep from '@/sheep';
import UserApi from '@/sheep/api/member/user';
const userInfo = computed(() => sheep.$store('user').userInfo || {});
const displayName = computed(() => {
return userInfo.value.emergencyContactName || userInfo.value.emergencyContact?.name || '';
});
const displayPhone = computed(() => {
return userInfo.value.emergencyContactPhone || userInfo.value.emergencyContact?.phone || '';
});
async function onSave() {
// 直接刷新本地用户信息并返回(如果需要,可在此处额外校验或请求后端)
await sheep.$store('user').updateUserData();
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('保存成功');
uni.navigateBack();
}
</script>
<style scoped>
.list-wrap {
background: #fff;
margin-top: 10rpx;
}
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.item-label {
color: #333;
font-size: 28rpx;
}
.item-value {
color: #999;
font-size: 28rpx;
}
.footer-box {
background: transparent;
}
</style>

View File

@@ -0,0 +1,219 @@
<template>
<s-layout title="上传健康证" class="health-cert-page">
<view class="page-body">
<view class="form-row">
<text class="label">编号</text>
<up-input v-model="healthNumber" placeholder="请输入健康证编号" clearable />
</view>
<view class="form-row">
<text class="label">类别</text>
<up-input v-model="healthCategory" placeholder="请输入健康证类别" clearable />
</view>
<view class="form-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-section">
<text class="upload-title">上传健康证</text>
<view class="upload-row">
<view v-for="(img, idx) in healthImages" :key="idx" class="upload-item">
<up-image :src="img" class="upload-thumb" mode="aspectFill" @click="previewImage(idx)" />
<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="plus" size="36" color="#999999" />
</view>
</up-upload>
</view>
</view>
<view class="save-row">
<up-button type="primary" block @click="onSave">保存</up-button>
</view>
</view>
</s-layout>
</template>
<script setup>
import { ref, onBeforeMount } from 'vue';
import FileApi from '@/sheep/api/infra/file';
import UserApi from '@/sheep/api/member/user';
import sheep from '@/sheep';
const healthNumber = ref('');
const healthCategory = ref('');
const healthValidStart = ref('');
const healthValidEnd = ref('');
const healthImages = ref([]); // 存放图片 url
// 页面初始化,从用户信息恢复(若已有则填充)
async function init() {
const user = await sheep.$store('user').getInfo();
if (user) {
healthNumber.value = user.healthCertNumber || user.healthCertNo || '';
healthCategory.value = user.healthCertCategory || '';
healthValidStart.value = user.healthCertValidStart || '';
healthValidEnd.value = user.healthCertValidEnd || '';
// 健康证可能只保留首张图片字符串,为兼容性处理成数组
const img = user.healthCert || user.healthCertImage || user.healthCertUrl || '';
if (img) {
healthImages.value = Array.isArray(img) ? img.slice() : [img];
}
}
}
onBeforeMount(() => {
init();
});
function previewImage(index) {
const urls = healthImages.value.slice();
uni.previewImage({
current: index,
urls,
});
}
function removeHealthImage(index) {
if (index >= 0 && index < healthImages.value.length) {
healthImages.value.splice(index, 1);
}
}
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.value.length < 4) {
healthImages.value.push(url);
}
// 兼容:在表单保存时取首张
}
}
// 如果用户通过本地选择图片(没有使用 up-upload 的自动上传),也支持手动选择并上传
async function chooseAndUpload() {
const res = await uni.chooseImage({ count: 1 });
if (!res || !res.tempFilePaths || !res.tempFilePaths.length) return;
const tempPath = res.tempFilePaths[0];
const { data } = await FileApi.uploadFile(tempPath);
if (data) {
if (healthImages.value.length < 4) {
healthImages.value.push(data);
}
}
}
async function onSave() {
// 简单校验:至少填写编号或上传图片
if (!healthNumber.value && !healthImages.value.length) {
sheep.$helper && sheep.$helper.toast && sheep.$helper.toast('请填写编号或上传健康证图片');
return;
}
const payload = {
healthCertNumber: healthNumber.value || undefined,
healthCertCategory: healthCategory.value || undefined,
healthCertValidStart: healthValidStart.value || undefined,
healthCertValidEnd: healthValidEnd.value || undefined,
// 为兼容后端,保留首张图片字段 healthCert若后端支持数组可改为数组
healthCert: healthImages.value.length ? healthImages.value[0] : undefined,
};
try {
const res = await UserApi.updateUser(payload);
if (res && res.code === 0) {
// 更新本地用户缓存
await sheep.$store('user').updateUserData();
uni.navigateBack();
}
} catch (err) {
console.warn('保存健康证失败', err);
}
}
</script>
<style scoped>
.health-cert-page .page-body {
padding: 20rpx 30rpx;
background: transparent;
}
.form-row {
display: flex;
align-items: center;
margin-bottom: 35rpx;
}
.label {
width: 140rpx;
color: #333333;
font-size: 28rpx;
}
.date-row .date-input {
width: 260rpx;
}
.dash {
margin: 0 10rpx;
color: #999999;
}
.upload-section {
margin-top: 28rpx;
}
.upload-title {
display: block;
margin-bottom: 12rpx;
color: #333;
font-size: 28rpx;
}
.upload-row {
display: flex;
align-items: center;
gap: 16rpx;
flex-wrap: wrap;
}
.upload-item {
position: relative;
width: 140rpx;
height: 140rpx;
border-radius: 8rpx;
overflow: hidden;
}
.upload-thumb {
width: 100%;
height: 100%;
}
.remove-btn {
position: absolute;
right: 6rpx;
top: 6rpx;
width: 30rpx;
height: 30rpx;
line-height: 30rpx;
text-align: center;
background: rgba(0,0,0,0.6);
color: #fff;
border-radius: 50%;
font-size: 24rpx;
z-index: 5;
}
.upload-add {
width: 140rpx;
height: 140rpx;
display: flex;
justify-content: center;
align-items: center;
border: 1px dashed #e6e6e6;
border-radius: 8rpx;
background: #fafafa;
}
.save-row {
margin-top: 80rpx;
}
</style>

View File

@@ -7,6 +7,7 @@
labelPosition="left"
border
class="form-box"
label-width="235"
>
<!-- 头像 -->
<view class="ss-flex ss-row-center ss-col-center ss-p-t-60 ss-p-b-0 bg-white">
@@ -67,7 +68,7 @@
</view>
</uni-forms-item>
<uni-forms-item name="mobile" label="手机号" @tap="onChangeMobile">
<uni-forms-item name="mobile" label="常用手机号" @tap="onChangeMobile">
<uni-easyinput
v-model="userInfo.mobile"
placeholder="请绑定手机号"
@@ -87,91 +88,104 @@
</template>
</uni-easyinput>
</uni-forms-item>
<!-- <uni-forms-item name="password" label="登录密码" @tap="onSetPassword">
<!-- 身份证号 -->
<uni-forms-item name="idNumber" label="身份证号">
<uni-easyinput
v-model="userInfo.password"
placeholder="点击修改登录密码"
:value="displayIdNumber"
:inputBorder="false"
:styles="{ disableColor: '#fff' }"
disabled
placeholderStyle="color:#BBBBBB;font-size:28rpx;line-height:normal"
:styles="{ disableColor: '#fff' }"
:placeholderStyle="placeholderStyle"
:clearable="false"
/>
</uni-forms-item>
<!-- 健康证 -->
<uni-forms-item name="healthCert" label="健康证" @tap="onTapHealthCert">
<uni-easyinput
:value="state.model.healthCertStatusText || '未设置'"
:inputBorder="false"
disabled
:styles="{ disableColor: '#fff' }"
:placeholderStyle="placeholderStyle"
:clearable="false"
>
<template v-slot:right>
<view class="ss-flex ss-col-center">
<su-radio
class="ss-flex"
v-if="userInfo.verification?.password"
:modelValue="true"
/>
<button v-else class="ss-reset-button ss-flex ss-col-center ss-row-center">
<text class="_icon-forward" style="color: #bbbbbb; font-size: 26rpx" />
<button class="ss-reset-button ss-flex ss-col-center ss-row-center">
<text class="_icon-forward" style="color: #bbbbbb; font-size: 26rpx"></text>
</button>
</view>
</template>
</uni-easyinput>
</uni-forms-item>
<!-- 紧急联系人 -->
<uni-forms-item name="emergencyContact" label="紧急联系人" @tap="sheep.$router.go('/pages/user/contact/index')">
<uni-easyinput
:value="state.model.emergencyContact ? '已设置' : '未设置'"
:inputBorder="false"
disabled
:styles="{ disableColor: '#fff' }"
:placeholderStyle="placeholderStyle"
:clearable="false"
>
<template v-slot:right>
<view class="ss-flex ss-col-center">
<button class="ss-reset-button ss-flex ss-col-center ss-row-center">
<text class="_icon-forward" style="color: #bbbbbb; font-size: 26rpx"></text>
</button>
</view>
</template>
</uni-easyinput>
</uni-forms-item>
<!-- 所属公司 -->
<uni-forms-item name="company" label="所属公司">
<uni-easyinput
:value="state.model.companyName || state.model.company || '广东省XX有限公司'"
:inputBorder="false"
disabled
:styles="{ disableColor: '#fff' }"
:placeholderStyle="placeholderStyle"
:clearable="false"
/>
</uni-forms-item>
<!-- 所属运营站点 -->
<uni-forms-item name="site" label="所属运营站点">
<uni-easyinput
:value="state.model.stationName || state.model.siteName || 'XXXX学校XX校区'"
:inputBorder="false"
disabled
:styles="{ disableColor: '#fff' }"
:placeholderStyle="placeholderStyle"
:clearable="false"
/>
</uni-forms-item>
<!-- 联系站长 -->
<!-- <uni-forms-item name="stationContact" label="联系站长" @tap="onTapContactStation">
<uni-easyinput
:value="state.model.stationManagerName || state.model.stationContactName || ''"
:inputBorder="false"
disabled
:styles="{ disableColor: '#fff' }"
:placeholderStyle="placeholderStyle"
:clearable="false"
>
<template v-slot:right>
<view class="ss-flex ss-col-center">
<button class="ss-reset-button ss-flex ss-col-center ss-row-center">
<text class="_icon-forward" style="color: #bbbbbb; font-size: 26rpx"></text>
</button>
</view>
</template>
</uni-easyinput>
</uni-forms-item> -->
</view>
<!-- <view class="bg-white ss-m-t-14">
<uni-list>
<uni-list-item
clickable
@tap="sheep.$router.go('/pages/user/address/list')"
title="地址管理"
showArrow
:border="false"
class="list-border"
/>
</uni-list>
</view> -->
</uni-forms>
<!-- 当前社交平台的绑定关系只处理 wechat 微信场景 -->
<!-- <view v-if="sheep.$platform.name !== 'H5'">
<view class="title-box ss-p-l-30">第三方账号绑定</view>
<view class="account-list ss-flex ss-row-between">
<view v-if="'WechatOfficialAccount' === sheep.$platform.name" class="ss-flex ss-col-center">
<image
class="list-img"
:src="sheep.$url.static('/static/img/shop/platform/WechatOfficialAccount.png')"
/>
<text class="list-name">微信公众号</text>
</view>
<view v-if="'WechatMiniProgram' === sheep.$platform.name" class="ss-flex ss-col-center">
<image
class="list-img"
:src="sheep.$url.static('/static/img/shop/platform/WechatMiniProgram.png')"
/>
<text class="list-name">微信小程序</text>
</view>
<view v-if="'App' === sheep.$platform.name" class="ss-flex ss-col-center">
<image
class="list-img"
:src="sheep.$url.static('/static/img/shop/platform/wechat.png')"
/>
<text class="list-name">微信开放平台</text>
</view>
<view class="ss-flex ss-col-center">
<view class="info ss-flex ss-col-center" v-if="state.thirdInfo">
<image class="avatar ss-m-r-20" :src="sheep.$url.cdn(state.thirdInfo.avatar)" />
<text class="name">{{ state.thirdInfo.nickname }}</text>
</view>
<view class="bind-box ss-m-l-20">
<button
v-if="state.thirdInfo.openid"
class="ss-reset-button relieve-btn"
@tap="unBindThirdOauth"
>
解绑
</button>
<button v-else class="ss-reset-button bind-btn" @tap="bindThirdOauth">绑定</button>
</view>
</view>
</view>
</view> -->
<su-fixed bottom placeholder bg="none">
<view class="footer-box ss-p-20">
<button class="ss-rest-button logout-btn ui-Shadow-Main" @tap="onSubmit">保存</button>
@@ -208,6 +222,20 @@
];
const userInfo = computed(() => sheep.$store('user').userInfo);
// 身份证显示(对中间数字做掩码显示)
const displayIdNumber = computed(() => {
const id =
state.model?.idNumber ||
state.model?.id_card ||
state.model?.idNo ||
state.model?.id;
if (!id) return '';
if (typeof id === 'string' && id.length >= 8) {
return id.replace(/(\d{3})\d+(\d{4})/, '$1********$2');
}
return id;
});
// 选择性别
function onChangeGender(e) {
@@ -219,6 +247,16 @@
showAuthModal('changeMobile');
};
// 点击健康证(跳转到健康证或证件页)
function onTapHealthCert() {
uni.navigateTo({ url: '/pages/registered/accountInfo' });
}
// 联系站长(跳转到我的账户或站点详情页)
function onTapContactStation() {
uni.navigateTo({ url: '/pages/user/account/index' });
}
// 选择微信的头像,进行上传
function onChooseAvatar(e) {
const tempUrl = e.detail.avatarUrl || '';

View File

@@ -94,7 +94,7 @@ function onConfirm(e) {
function toList() {
uni.navigateTo({
url: '/pages/user/recordList'
url: '/pages/user/order/recordList'
})
}

View File

@@ -0,0 +1,70 @@
<template>
<s-layout class="set-wrap" title="隐私管理" :bgStyle="{ color: '#fff' }">
<view class="container-list">
<uni-list :border="false">
<uni-list-item
title="授权管理"
showArrow
clickable
:border="false"
class="list-border"
@tap="
sheep.$router.go('/pages/user/AuthorizaManage/index')
"
/>
<uni-list-item
title="用户协议"
showArrow
clickable
:border="false"
class="list-border"
@tap="
sheep.$router.go('/pages/public/richtext', {
title: '用户协议'
})
"
/>
<uni-list-item
title="隐私管理"
showArrow
clickable
:border="false"
class="list-border"
@tap="
sheep.$router.go('/pages/public/richtext', {
title: '隐私协议'
})
"
/>
<!-- 为了过审 只有 iOS-App 有注销账号功能 && sheep.$platform.os === 'ios' && sheep.$platform.name === 'App' -->
<uni-list-item
v-if="isLogin"
title="注销账号"
rightText=""
showArrow
clickable
:border="false"
class="list-border"
@click="onLogoff"
/>
</uni-list>
</view>
</s-layout>
</template>
<script setup>
import sheep from '@/sheep';
import { computed } from 'vue';
const isLogin = computed(() => sheep.$store('user').isLogin);
</script>
<style lang="scss" scoped>
.container-list {
padding-top: 15rpx;
background: #fff;
}
</style>

View File

@@ -0,0 +1,184 @@
<template>
<s-layout :title="titleText" class="salary-list-page">
<view class="page-wrap">
<scroll-view class="list" scroll-y>
<view class="salary-item" v-for="item in items" :key="item.id">
<view class="left">
<text class="date">{{ item.date }}</text>
<text class="desc" :class="{ 'muted': !item.desc }">{{ item.desc || '' }}</text>
</view>
<text
class="amount"
:class="{ positive: item.amount >= 0, negative: item.amount < 0 }"
>
{{ formatSigned(item.amount) }}
</text>
</view>
<view v-if="!items.length" class="empty">没有更多了~</view>
</scroll-view>
</view>
</s-layout>
</template>
<script setup>
import { ref } from 'vue';
import PayWalletApi from '@/sheep/api/pay/wallet';
import { onShow } from '@dcloudio/uni-app';
const items = ref([]);
const year = ref('');
const month = ref('');
const titleText = ref('薪资明细');
function pad(n) {
if (!n && n !== 0) return n;
return String(n).padStart(2, '0');
}
function formatSigned(val) {
const n = Number(val) || 0;
const sign = n > 0 ? '+' : n < 0 ? '' : '+';
// 保留两位小数
return `${sign}${Math.abs(n).toFixed(2)}`;
}
function toDateString(t) {
const d = t ? new Date(t) : null;
if (!d || isNaN(d.getTime())) return '';
const y = d.getFullYear();
const m = pad(d.getMonth() + 1);
const day = pad(d.getDate());
return `${y}-${m}-${day}`;
}
// 本地回退示例明细(供开发查看)
const testList = [
{ id: 't1', date: '2025-10-31', desc: '配送订单佣金收入23.9 时段考勤收入23.21', amount: 71.89 },
{ id: 't2', date: '2025-10-30', desc: '配送订单佣金收入23.9 时段考勤收入23.21', amount: 71.89 },
{ id: 't3', date: '2025-10-31', desc: '配送订单佣金收入23.9 时段考勤收入23.21', amount: 71.89 },
{ id: 't4', date: '2025-10-30', desc: '配送订单佣金收入23.9 时段考勤收入23.21', amount: 71.89 },
];
// 加载并过滤流水(按 year/month
async function loadList() {
try {
const res = await PayWalletApi.getWalletTransactionPage({ page: 1, size: 200 });
let records = [];
if (res && res.code === 0) {
if (Array.isArray(res.data)) {
records = res.data;
} else if (res.data && Array.isArray(res.data.records)) {
records = res.data.records;
}
}
// 若有返回,则客户端过滤指定年月
if (records && records.length) {
const filtered = records.filter((r) => {
const t = r.createTime ?? r.createdAt ?? r.time ?? r.create_date ?? r.date;
if (!t) return false;
const d = new Date(t);
if (isNaN(d.getTime())) return false;
const y = d.getFullYear();
const m = d.getMonth() + 1;
return Number(y) === Number(year.value) && Number(m) === Number(month.value);
});
items.value = filtered.map((r, idx) => {
const t = r.createTime ?? r.createdAt ?? r.time ?? r.create_date ?? r.date;
return {
id: r.id ?? `r-${idx}`,
date: toDateString(t),
desc: r.remark ?? r.note ?? r.title ?? r.typeName ?? '',
amount: Number(r.amount ?? r.price ?? r.income ?? r.value ?? r.money ?? 0),
};
});
// 如果过滤后为空则回退到示例数据,便于开发查看
if (!items.value.length) {
items.value = testList;
}
return;
}
} catch (e) {
// ignore
}
// 无接口返回,使用回退示例
items.value = testList;
}
// 获取路由参数uni-app 页面可通过 getCurrentPages 读取 options
function initParams() {
try {
const pages = getCurrentPages();
const cur = pages[pages.length - 1] || {};
const opts = cur.options || {};
year.value = opts.year || opts.y || String(new Date().getFullYear());
month.value = opts.month || opts.m || String(new Date().getMonth() + 1);
titleText.value = `${String(year.value).slice(-2)}${pad(month.value)}月薪资`;
} catch (e) {
year.value = String(new Date().getFullYear());
month.value = String(new Date().getMonth() + 1);
titleText.value = `${String(year.value).slice(-2)}${pad(month.value)}月薪资`;
}
}
// 页面显示时初始化并加载
onShow(() => {
initParams();
loadList();
});
</script>
<style scoped>
.page-wrap {
padding: 16px;
background: transparent;
min-height: 100vh;
}
.list {
margin-top: 8px;
}
.salary-item {
background: #fff;
padding: 14px 12px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
.salary-item .left {
flex: 1;
}
.date {
color: #333;
font-weight: 600;
display: block;
margin-bottom: 6px;
}
.desc {
color: #999;
font-size: 13px;
display: block;
}
.desc.muted {
opacity: 0.8;
}
.amount {
width: 160rpx;
text-align: right;
font-weight: 700;
}
.amount.positive {
color: #e74c3c;
}
.amount.negative {
color: #2db7a3;
}
.empty {
text-align: center;
color: #bbb;
padding: 20px 0;
}
</style>

View File

@@ -0,0 +1,252 @@
<template>
<s-layout title="账户余额" class="record-list-page">
<view class="page-wrap">
<!-- 余额卡片 -->
<view class="balance-card">
<view class="balance-top">
<text class="symbol">¥</text>
<text class="balance">{{ formatBalance(balance) }}</text>
</view>
<view class="balance-sub" @tap="openDetail()">
本月已入账 <up-icon name="arrow-right" color="#949494" size="15"></up-icon>
</view>
</view>
<!-- 列表按年分组的薪资账单 -->
<scroll-view class="list" scroll-y>
<view v-for="section in sections" :key="section.year" class="year-section">
<view class="year-header">{{ section.year }}年已出账薪资账单</view>
<view
class="month-row"
v-for="item in section.items"
:key="section.year + '-' + item.month"
@tap="openDetail(section.year, item.month)"
>
<text class="month-label">{{ item.month }}月份薪资</text>
<view class="month-right">
<text class="month-amount">¥ {{ formatAmount(item.amount) }}</text>
<up-icon name="arrow-right" color="#c7c7c7" size="16"></up-icon>
</view>
</view>
</view>
<view class="no-more">没有更多了~</view>
</scroll-view>
</view>
</s-layout>
</template>
<script setup>
import { ref } from 'vue';
import PayWalletApi from '@/sheep/api/pay/wallet';
import { onShow } from '@dcloudio/uni-app';
const balance = ref(0);
const sections = ref([]);
// 格式化显示余额(保留两位小数)
function formatBalance(val) {
if (val == null) return '0.00';
return (Number(val) || 0).toFixed(2);
}
// 列表金额格式(示例中是带一位小数,这里为兼容保留一位或两位)
function formatAmount(val) {
if (val == null) return '0.0';
// 如果本身有小数位则保留一位,否则保留一位
return (Number(val) || 0).toFixed(1);
}
// 将流水按 年 -> 月 聚合(支持后端 records 数组record.createTime 字段)
function groupTransactions(records = []) {
const map = {}; // { year: { month: amount } }
records.forEach((r) => {
// 兼容不同字段名,尝试 createTime 或 createdAt 或 time
const t = r.createTime ?? r.createdAt ?? r.time ?? r.create_date ?? r.date;
let d = t ? new Date(t) : null;
if (!d || isNaN(d.getTime())) {
// 如果没有时间则跳过
return;
}
const year = d.getFullYear();
const month = d.getMonth() + 1;
// 取金额字段amount / price / income / value / money
const amt = Number(r.amount ?? r.price ?? r.income ?? r.value ?? r.money ?? 0) || 0;
map[year] = map[year] || {};
map[year][month] = (map[year][month] || 0) + amt;
});
// 转换为数组并排序(年降序,月降序)
const result = Object.keys(map)
.map((y) => {
const monthsObj = map[y];
const items = Object.keys(monthsObj)
.map((m) => ({
month: Number(m),
amount: monthsObj[m],
}))
.sort((a, b) => b.month - a.month);
return {
year: Number(y),
items,
};
})
.sort((a, b) => b.year - a.year);
return result;
}
async function loadWallet() {
try {
const res = await PayWalletApi.getPayWallet();
if (res && res.code === 0 && res.data) {
balance.value = res.data.balance ?? res.data?.wallet?.balance ?? 0;
return;
}
} catch (e) {
// ignore
}
// 回退占位数据
balance.value = 1999.91;
}
async function loadTransactions() {
try {
// 请求分页(取足够多条用于按月聚合)
const res = await PayWalletApi.getWalletTransactionPage({ page: 1, size: 200 });
// 兼容常见返回结构:{ code:0, data: { records: [] } } 或 { code:0, data: [] }
let records = [];
if (res && res.code === 0) {
if (Array.isArray(res.data)) {
records = res.data;
} else if (res.data && Array.isArray(res.data.records)) {
records = res.data.records;
}
}
if (records.length) {
sections.value = groupTransactions(records);
return;
}
} catch (e) {
// ignore
}
// 回退示例数据(与原型一致)
sections.value = [
{
year: 2025,
items: [
{ month: 5, amount: 4563.5 },
{ month: 4, amount: 4563.5 },
{ month: 3, amount: 4563.5 },
{ month: 2, amount: 4563.5 },
{ month: 1, amount: 4563.5 },
],
},
{
year: 2024,
items: [{ month: 12, amount: 4563.5 }],
},
];
}
function openDetail(year, month) {
// 占位跳转:可以替换为真实的工资明细页
uni.navigateTo({
url: `/pages/user/salary/salaryList?year=${year}&month=${month}`,
});
}
function openMonthSummary() {
// 占位跳转到本月流水汇总
uni.navigateTo({
url: '/pages/user/walletSummary',
});
}
onShow(() => {
loadWallet();
loadTransactions();
});
</script>
<style>
.page-wrap {
padding: 16px;
background: transparent;
min-height: 100vh;
}
.balance-card {
background: #fff;
border-radius: 8px;
padding: 30px 16px;
margin-bottom: 12px;
text-align: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.balance-top {
display: flex;
justify-content: center;
align-items: baseline;
gap: 8px;
}
.symbol {
font-size: 28px;
color: #333;
margin-right: 6px;
}
.balance {
font-size: 40px;
font-weight: 700;
color: #333;
}
.balance-sub {
display: flex;
justify-content: center;
color: #999;
margin-top: 10px;
font-size: 14px;
}
.list {
margin-top: 8px;
}
.year-section {
margin-bottom: 12px;
}
.year-header {
background: #f5f5f5;
color: #666;
padding: 10px 12px;
font-weight: 600;
border-radius: 4px;
margin-bottom: 8px;
}
.month-row {
background: #fff;
padding: 14px 12px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
.month-label {
color: #333;
font-size: 15px;
}
.month-right {
display: flex;
align-items: center;
gap: 8px;
color: #999;
}
.month-amount {
color: #333;
font-weight: 600;
margin-right: 6px;
}
.no-more {
text-align: center;
color: #bbb;
padding: 20px 0;
}
</style>