feat: 迭代

This commit is contained in:
2026-05-27 18:58:39 +08:00
parent 716a55744e
commit 9baf5f29f7
44 changed files with 928 additions and 2903 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
VITE_H5_API_BASE_URL=http://localhost:3000
# 小程序开发环境接口(真机调试使用局域网IP)
VITE_MP_API_BASE_URL=http://192.168.0.111:3000
VITE_MP_API_BASE_URL=http://localhost:3000
+12 -4
View File
@@ -15,7 +15,7 @@ export interface Withdrawal {
accountType: 'alipay' | 'wechat';
accountName: string;
accountNumber: string;
status: 'pending' | 'processing' | 'completed' | 'rejected';
status: 'pending' | 'approved' | 'rejected' | 'paid';
remark?: string;
processedAt?: string;
createdAt: string;
@@ -49,10 +49,18 @@ export const walletApi = {
page?: number;
pageSize?: number;
}) {
return request<{ items: Withdrawal[]; total: number }>({
url: '/api/app/finance/withdrawals',
// 构建查询字符串
const query = new URLSearchParams();
if (params?.status) query.append('status', params.status);
if (params?.page) query.append('page', params.page.toString());
if (params?.pageSize) query.append('pageSize', params.pageSize.toString());
const queryString = query.toString();
const url = queryString ? `/api/app/finance/withdrawals?${queryString}` : '/api/app/finance/withdrawals';
return request<{ list: Withdrawal[]; total: number }>({
url,
method: 'GET',
data: params,
});
},
};
+177 -35
View File
@@ -10,7 +10,7 @@
<!-- 定位城市 -->
<view class="locate-section">
<view class="locate-label">
<text class="locate-icon">📍</text>
<u-icon name="map-fill" :size="20" color="#FF6B35" />
<text class="locate-text">当前定位</text>
</view>
<view class="locate-city" @tap="selectLocatedCity">
@@ -34,8 +34,13 @@
</view>
<!-- 城市列表 -->
<scroll-view scroll-y class="city-list">
<view v-for="group in cityGroups" :key="group.letter" class="city-group">
<scroll-view
scroll-y
class="city-list"
:scroll-top="scrollTop"
scroll-with-animation
>
<view v-for="(group, index) in cityGroups" :key="group.letter" class="city-group" :data-index="index">
<text class="group-letter">{{ group.letter }}</text>
<view
v-for="city in group.cities"
@@ -47,12 +52,24 @@
</view>
</view>
</scroll-view>
<!-- 字母索引 -->
<view class="letter-index">
<view
v-for="group in cityGroups"
:key="group.letter"
:class="['letter-item', { active: currentLetter === group.letter }]"
@tap="scrollToLetter(group.letter)"
>
{{ group.letter }}
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, onMounted, watch, nextTick } from 'vue';
const props = defineProps<{
city?: string;
@@ -65,6 +82,15 @@ const emit = defineEmits<{
const visible = ref(false);
const selectedCity = ref(props.city || '上海');
const locatedCity = ref('');
const scrollTop = ref(0);
const currentLetter = ref('');
// 监听 props.city 变化
watch(() => props.city, (newCity) => {
if (newCity) {
selectedCity.value = newCity;
}
});
const hotCities = [
'上海', '北京', '广州', '深圳',
@@ -73,30 +99,33 @@ const hotCities = [
];
const cityGroups = [
{ letter: 'A', cities: ['安庆', '安阳', '鞍山', '安康'] },
{ letter: 'B', cities: ['北京', '保定', '包头', '蚌埠', '宝鸡', '滨州'] },
{ letter: 'C', cities: ['成都', '重庆', '长沙', '长', '常州', '沧州', '承德'] },
{ letter: 'D', cities: ['大连', '东莞', '大庆', '大同', '德州', '东营'] },
{ letter: 'F', cities: ['福州', '佛山', '抚顺', '阜阳'] },
{ letter: 'G', cities: ['广州', '贵阳', '桂林', '赣州'] },
{ letter: 'H', cities: ['杭州', '哈尔滨', '合肥', '海口', '呼和浩特', '惠州', '邯郸'] },
{ letter: 'J', cities: ['南', '吉林', '嘉兴', '金华', '济宁', '州', '九江'] },
{ letter: 'K', cities: ['昆明', '开封', '喀什'] },
{ letter: 'L', cities: ['兰州', '洛阳', '连云港', '临沂', '柳州', '泸州'] },
{ letter: 'M', cities: ['阳', '牡丹江', '茂名'] },
{ letter: 'N', cities: ['南京', '南昌', '南宁', '宁波', '南通', '南阳'] },
{ letter: 'P', cities: ['平顶山', '阳', '莆田'] },
{ letter: 'Q', cities: ['青岛', '秦皇岛', '泉州', '齐齐哈尔'] },
{ letter: 'S', cities: ['上海', '深圳', '沈阳', '石家庄', '州', '汕头', '绍兴', '三亚'] },
{ letter: 'T', cities: ['天津', '太原', '唐山', '台州', '泰安'] },
{ letter: 'W', cities: ['武汉', '无锡', '州', '乌鲁木齐', '潍坊', '威海', '芜湖'] },
{ letter: 'X', cities: ['西安', '厦门', '徐州', '襄阳', '咸阳', '新乡'] },
{ letter: 'Y', cities: ['烟台', '州', '银川', '宜昌', '盐城', '岳阳'] },
{ letter: 'Z', cities: ['郑州', '珠海', '淄博', '中山', '株洲', '漳州'] },
{ letter: 'A', cities: ['阿坝', '阿克苏', '阿拉善', '阿勒泰', '安康', '安庆', '安顺', '安阳', '鞍山'] },
{ letter: 'B', cities: ['白城', '白山', '白银', '百色', '蚌埠', '包头', '宝鸡', '保定', '保山', '北海', '北京', '本溪', '毕节', '滨州', '博尔塔拉'] },
{ letter: 'C', cities: ['沧州', '昌都', '昌吉', '长春', '长沙', '长治', '常德', '常州', '巢湖', '朝阳', '潮州', '郴州', '成都', '承德', '池州', '赤峰', '崇左', '滁州', '楚雄'] },
{ letter: 'D', cities: ['达州', '大连', '大庆', '大同', '大兴安岭', '丹东', '德宏', '德阳', '德州', '迪庆', '定西', '东莞', '东营', '鄂尔多斯', '鄂州', '恩施'] },
{ letter: 'E', cities: ['鄂尔多斯', '鄂州', '恩施'] },
{ letter: 'F', cities: ['防城港', '佛山', '福州', '抚顺', '抚州', '阜新', '阜阳'] },
{ letter: 'G', cities: ['甘南', '甘孜', '赣州', '固原', '广安', '广元', '广州', '贵港', '贵阳', '桂林', '果洛'] },
{ letter: 'H', cities: ['哈尔滨', '哈密', '海北', '海东', '海口', '海南', '海西', '邯郸', '汉中', '杭州', '州', '合肥', '河池', '河源', '菏泽', '贺州', '鹤壁', '鹤岗', '黑河', '衡水', '衡阳', '红河', '呼和浩特', '呼伦贝尔', '湖州', '葫芦岛', '怀化', '淮安', '淮北', '淮南', '黄冈', '黄南', '黄山', '黄石', '惠州'] },
{ letter: 'J', cities: ['鸡西', '吉安', '吉林', '济南', '济宁', '济源', '佳木斯', '嘉兴', '嘉峪关', '江门', '焦作', '揭阳', '金昌', '金华', '锦州', '晋城', '晋中', '荆门', '荆州', '景德镇', '九江', '酒泉'] },
{ letter: 'K', cities: ['开封', '喀什', '克拉玛依', '克孜勒苏', '昆明'] },
{ letter: 'L', cities: ['来宾', '莱芜', '兰州', '廊坊', '乐山', '丽江', '丽水', '连云港', '凉山', '辽阳', '辽源', '聊城', '临沧', '临汾', '临夏', '临沂', '林芝', '柳州', '六安', '六盘水', '龙岩', '陇南', '娄底', '泸州', '吕梁', '洛阳', '漯河', '泸州'] },
{ letter: 'M', cities: ['马鞍山', '茂名', '眉山', '梅州', '绵阳', '牡丹江'] },
{ letter: 'N', cities: ['那曲', '南昌', '南充', '南京', '南宁', '南平', '南通', '阳', '内江', '宁波', '宁德', '怒江'] },
{ letter: 'P', cities: ['盘锦', '攀枝花', '平顶山', '平凉', '萍乡', '莆田', '濮阳', '普洱'] },
{ letter: 'Q', cities: ['七台河', '齐齐哈尔', '黔东南', '黔南', '黔西南', '州', '秦皇岛', '青岛', '清远', '庆阳', '曲靖', '衢州', '泉州'] },
{ letter: 'R', cities: ['日喀则', '日照', '荣成'] },
{ letter: 'S', cities: ['三门峡', '三明', '三亚', '山南', '汕头', '汕尾', '商洛', '商丘', '上海', '上饶', '韶关', '邵阳', '绍兴', '深圳', '神农架', '沈阳', '十堰', '石家庄', '石嘴山', '双鸭山', '朔州', '四平', '松原', '州', '宿迁', '宿州', '绥化', '随州', '遂宁'] },
{ letter: 'T', cities: ['台州', '太原', '泰安', '泰州', '唐山', '天津', '天水', '铁岭', '通化', '通辽', '铜川', '铜陵', '铜仁', '吐鲁番', '图木舒克'] },
{ letter: 'W', cities: ['威海', '潍坊', '渭南', '州', '文山', '乌海', '乌兰察布', '乌鲁木齐', '无锡', '吴忠', '芜湖', '梧州', '武汉', '武威'] },
{ letter: 'X', cities: ['西安', '西宁', '西双版纳', '锡林郭勒', '厦门', '咸宁', '咸阳', '湘潭', '湘西', '襄阳', '孝感', '忻州', '新乡', '新余', '信阳', '兴安', '邢台', '徐州', '许昌', '宣城', 'xuancheng'] },
{ letter: 'Y', cities: ['雅安', '烟台', '延安', '延边', '盐城', '扬州', '阳江', '阳泉', '伊春', '伊犁', '宜宾', '宜昌', '宜春', '益阳', '银川', '鹰潭', '营口', '永州', '榆林', '玉林', '玉树', '玉溪', '岳阳', '云浮', '运城'] },
{ letter: 'Z', cities: ['枣庄', '湛江', '张家界', '张家口', '张掖', '漳州', '昭通', '肇庆', '镇江', '郑州', '中山', '中卫', '舟山', '周口', '株洲', '珠海', '驻马店', '资阳', '淄博', '自贡', '遵义'] },
];
function open() {
visible.value = true;
selectedCity.value = props.city || '上海';
}
function close() {
@@ -110,16 +139,52 @@ function selectCity(city: string) {
}
function selectLocatedCity() {
if (locatedCity.value && locatedCity.value !== '定位失败') {
if (locatedCity.value && locatedCity.value !== '定位失败' && locatedCity.value !== '定位中...') {
selectCity(locatedCity.value);
}
}
function getLocation() {
uni.getLocation({
type: 'gcj02',
success: () => {
locatedCity.value = '上海';
function scrollToLetter(letter: string) {
currentLetter.value = letter;
// 计算目标字母的索引
const index = cityGroups.findIndex(group => group.letter === letter);
if (index === -1) return;
// 计算滚动位置
// 每个分组的高度 = 标题高度(约60rpx) + 城市数量 * 城市项高度(约80rpx)
let scrollPosition = 0;
for (let i = 0; i < index; i++) {
const group = cityGroups[i];
// 标题高度 + 城市列表高度
scrollPosition += 60 + (group.cities.length * 80);
}
// 转换 rpx 到 px (假设 1rpx = 0.5px,实际根据设备不同)
scrollTop.value = scrollPosition * 0.5;
console.log(`滚动到字母 ${letter}, 索引 ${index}, 位置 ${scrollTop.value}px`);
}
// 逆地理编码:将经纬度转换为城市名(调用后端接口)
function reverseGeocode(latitude: number, longitude: number) {
uni.request({
url: `${import.meta.env?.VITE_MP_API_BASE_URL || 'http://localhost:3000'}/api/app/location/geocode`,
method: 'GET',
data: {
latitude,
longitude,
},
success: (res: any) => {
if (res.statusCode === 200 && res.data?.data?.city) {
const city = res.data.data.city;
locatedCity.value = city;
// 自动更新首页城市
emit('change', city);
} else {
locatedCity.value = '定位失败';
}
},
fail: () => {
locatedCity.value = '定位失败';
@@ -127,6 +192,36 @@ function getLocation() {
});
}
function getLocation() {
locatedCity.value = '定位中...';
uni.getLocation({
type: 'gcj02',
success: (res) => {
// 获取到经纬度后,调用后端接口进行逆地理编码
reverseGeocode(res.latitude, res.longitude);
},
fail: (err) => {
console.error('定位失败:', err);
locatedCity.value = '定位失败';
// 提示用户授权
if (err.errMsg && err.errMsg.includes('auth')) {
uni.showModal({
title: '需要定位权限',
content: '请在设置中开启定位权限以获取当前城市',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting();
}
},
});
}
},
});
}
onMounted(() => {
getLocation();
});
@@ -155,6 +250,8 @@ defineExpose({ open, close });
display: flex;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom);
position: relative;
overflow: hidden;
}
.picker-header {
@@ -183,18 +280,16 @@ defineExpose({ open, close });
border-bottom: 1rpx solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-start;
flex-shrink: 0;
gap: 24rpx;
}
.locate-label {
display: flex;
align-items: center;
gap: 8rpx;
}
.locate-icon {
font-size: 28rpx;
flex-shrink: 0;
}
.locate-text {
@@ -206,12 +301,17 @@ defineExpose({ open, close });
background: #fff1eb;
padding: 12rpx 24rpx;
border-radius: 24rpx;
flex: 0 0 auto;
max-width: 400rpx;
}
.locate-city .city-text {
font-size: 28rpx;
color: #FF6B35;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hot-section {
@@ -250,6 +350,7 @@ defineExpose({ open, close });
.city-list {
flex: 1;
overflow-y: auto;
position: relative;
}
.city-group {
@@ -275,4 +376,45 @@ defineExpose({ open, close });
font-weight: 500;
}
}
.letter-index {
position: absolute;
right: 8rpx;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 2rpx;
z-index: 10;
background: rgba(255, 255, 255, 0.8);
border-radius: 20rpx;
padding: 8rpx 4rpx;
max-height: 80%;
overflow-y: auto;
}
.letter-item {
font-size: 20rpx;
color: #999;
padding: 2rpx 6rpx;
font-weight: 500;
line-height: 1.2;
min-width: 32rpx;
text-align: center;
transition: all 0.2s ease;
&.active {
color: #FF6B35;
background: #fff1eb;
border-radius: 4rpx;
transform: scale(1.1);
}
&:active {
color: #FF6B35;
background: #fff1eb;
border-radius: 4rpx;
}
}
</style>
+9 -1
View File
@@ -15,7 +15,15 @@
"usingComponents": true,
"optimization": {
"subPackages": true
}
},
"permission": {
"scope.userLocation": {
"desc": "您的位置信息将用于为您推荐附近的酒店民宿"
}
},
"requiredPrivateInfos": [
"getLocation"
]
},
"mp-alipay": {
"usingComponents": true,
+2 -2
View File
@@ -7,9 +7,9 @@
<text class="city-name">{{ searchParams.city }}</text>
<u-icon name="arrow-down" :size="16" color="#fff" />
</view>
<view class="header-icon">
<!-- <view class="header-icon">
<u-icon name="bell" :size="22" color="#fff" />
</view>
</view> -->
</view>
<view class="header-title">
+2 -17
View File
@@ -141,6 +141,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { getCashbackRecords } from '@/api/user/invite';
import { formatRelativeDate } from '@/utils/date';
const records = ref<any[]>([]);
const page = ref(1);
@@ -234,23 +235,7 @@ function changeTime(value: string) {
currentTime.value = value;
}
function formatTime(dateStr: string) {
if (!dateStr) return '';
const d = new Date(dateStr);
const now = new Date();
const diff = now.getTime() - d.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) {
return `今天 ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
} else if (days === 1) {
return `昨天 ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
} else if (days < 7) {
return `${days}天前`;
} else {
return `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
}
const formatTime = formatRelativeDate;
function formatOrderNo(orderNo: string) {
if (!orderNo) return '';
+2 -22
View File
@@ -122,6 +122,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { getInviteRecords } from '@/api/user/invite';
import { formatRelativeTime } from '@/utils/date';
const records = ref<any[]>([]);
const page = ref(1);
@@ -208,28 +209,7 @@ function changeFilter(value: string) {
console.log('筛选后记录数:', filteredRecords.value.length);
}
function formatTime(dateStr: string) {
if (!dateStr) return '';
const d = new Date(dateStr);
const now = new Date();
const diff = now.getTime() - d.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) {
const hours = Math.floor(diff / (1000 * 60 * 60));
if (hours === 0) {
const minutes = Math.floor(diff / (1000 * 60));
return minutes <= 0 ? '刚刚' : `${minutes}分钟前`;
}
return `${hours}小时前`;
} else if (days === 1) {
return '昨天';
} else if (days < 7) {
return `${days}天前`;
} else {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
}
const formatTime = formatRelativeTime;
function getOrderBadgeClass(count: number) {
if (count === 0) return 'badge-gray';
@@ -149,9 +149,6 @@
<view class="room-content">
<view class="room-header">
<text class="room-name">{{ room.name }}</text>
<view class="room-type-tag">
<text class="type-text">{{ room.type }}</text>
</view>
</view>
<view class="room-specs">
@@ -1082,18 +1079,6 @@ onMounted(() => {
-webkit-box-orient: vertical;
}
.room-type-tag {
flex-shrink: 0;
padding: 4rpx 12rpx;
background: #F0F7FF;
border-radius: 8rpx;
}
.type-text {
font-size: 20rpx;
color: #4A90E2;
font-weight: 500;
}
.room-specs {
display: flex;
+1 -12
View File
@@ -278,6 +278,7 @@
import { ref, onMounted, onUnmounted } from 'vue';
import { getOrderDetail, cancelOrder, payOrder } from '@/api/user/order';
import { createReview, checkOrderReviewed } from '@/api/user/review';
import { formatDate, formatDateTime } from '@/utils/date';
const paymentMethodLabels: Record<string, string> = {
wechat: '微信支付',
@@ -379,18 +380,6 @@ function goRoomDetail() {
}
}
function formatDate(dateStr: string): string {
if (!dateStr) return '';
const d = new Date(dateStr);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function formatDateTime(dateStr: string): string {
if (!dateStr) return '';
const d = new Date(dateStr);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
function maskIdCard(idCard: string): string {
if (!idCard || idCard.length < 8) return idCard;
return idCard.substring(0, 6) + '********' + idCard.substring(idCard.length - 4);
+2 -5
View File
@@ -168,6 +168,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import * as orderApi from '@/api/user/order';
import { formatDateShort } from '@/utils/date';
interface Tab {
label: string;
@@ -228,11 +229,7 @@ function getStatusText(status: string): string {
// 格式化日期范围
function formatDateRange(checkIn: string, checkOut: string): string {
if (!checkIn || !checkOut) return '';
const formatDate = (dateStr: string) => {
const d = new Date(dateStr);
return `${d.getMonth() + 1}/${d.getDate()}`;
};
return `${formatDate(checkIn)} - ${formatDate(checkOut)}`;
return `${formatDateShort(checkIn)} - ${formatDateShort(checkOut)}`;
}
// 获取操作按钮
+3 -8
View File
@@ -183,6 +183,7 @@
import { ref, computed, onMounted } from 'vue';
import { getSellerOrders, confirmOrder, rejectOrder, checkinOrder } from '@/api/seller/order';
import LoadingState from '@/components/base/LoadingState.vue';
import { formatDateShort } from '@/utils/date';
const tabs = [
{ label: '全部', value: '', badge: 0 },
@@ -319,9 +320,7 @@ function getStatusText(status: string): string {
function formatDateRange(checkIn: string, checkOut: string): string {
if (!checkIn || !checkOut) return '日期待定';
const inDate = checkIn.split('T')[0].substring(5).replace('-', '/');
const outDate = checkOut.split('T')[0].substring(5).replace('-', '/');
return `${inDate} - ${outDate}`;
return `${formatDateShort(checkIn)} - ${formatDateShort(checkOut)}`;
}
function formatTime(time: string): string {
@@ -338,11 +337,7 @@ function formatTime(time: string): string {
if (hours < 24) return `${hours}小时前`;
if (days < 7) return `${days}天前`;
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours().toString().padStart(2, '0');
const minute = date.getMinutes().toString().padStart(2, '0');
return `${month}/${day} ${hour}:${minute}`;
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}
function getEmptyText(): string {
@@ -112,6 +112,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { financeApi } from '@/api/seller/finance';
import { formatDate, formatDateTime } from '@/utils/date';
const loading = ref(true);
const settlement = ref<any>(null);
@@ -163,18 +164,6 @@ function formatPeriod(startDate: string, endDate: string): string {
return `${start.getFullYear()}/${start.getMonth() + 1}/${start.getDate()} - ${end.getMonth() + 1}/${end.getDate()}`;
}
function formatDate(dateStr: string): string {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
}
function formatDateTime(dateStr: string): string {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}
function formatMoney(value: number | string): string {
const num = Number(value || 0);
return num.toFixed(2);
+2 -15
View File
@@ -90,6 +90,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { financeApi } from '@/api/seller/finance';
import { formatRelativeDate } from '@/utils/date';
const loading = ref(true);
const transactions = ref<any[]>([]);
@@ -203,21 +204,7 @@ function formatMoney(value: number | string): string {
return num.toFixed(2);
}
function formatTime(time: string): string {
if (!time) return '';
const date = new Date(time);
const now = new Date();
const diff = now.getTime() - date.getTime();
const day = 24 * 60 * 60 * 1000;
if (diff < day && date.getDate() === now.getDate()) {
return `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
} else if (diff < 2 * day && date.getDate() === now.getDate() - 1) {
return `昨天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
} else {
return `${date.getMonth() + 1}-${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}
}
const formatTime = formatRelativeDate;
function goBack() {
uni.navigateBack();
+125 -84
View File
@@ -35,29 +35,45 @@
class="withdrawal-item"
@tap="goDetail(item.id)"
>
<view class="item-header">
<view class="header-left">
<view class="account-info">
<u-icon :name="getAccountIcon(item.accountType)" :size="20" color="#FF6B35" />
<text class="account-text">{{ getAccountLabel(item.accountType) }}</text>
</view>
<!-- 顶部金额和状态 -->
<view class="item-top">
<view class="amount-section">
<text class="amount-label">提现金额</text>
<text class="amount-value">¥{{ formatMoney(item.amount) }}</text>
</view>
<view class="top-right">
<view :class="['status-badge', `status-${item.status}`]">
<text class="status-text">{{ getStatusLabel(item.status) }}</text>
</view>
<u-icon name="arrow-right" :size="16" color="#CCCCCC" />
</view>
<u-icon name="arrow-right" :size="16" color="#CCCCCC" />
</view>
<view class="item-amount">
<text class="amount-value">¥{{ formatMoney(item.amount) }}</text>
<!-- 中间账户信息 -->
<view class="item-middle">
<view v-if="item.accountType" class="info-row">
<text class="info-label">提现方式</text>
<view class="info-value-row">
<u-icon v-if="getAccountIcon(item.accountType)" :name="getAccountIcon(item.accountType)" :size="16" color="#FF6B35" />
<text class="info-value">{{ getAccountLabel(item.accountType) }}</text>
</view>
</view>
<view v-if="item.accountName || item.accountNumber" class="info-row">
<text class="info-label">账户信息</text>
<text class="info-value">{{ item.accountName }} {{ maskAccount(item.accountNumber) }}</text>
</view>
<view class="info-row">
<text class="info-label">申请时间</text>
<text class="info-value">{{ formatTime(item.createdAt) }}</text>
</view>
</view>
<view class="item-info">
<text class="info-text">{{ item.accountName }} {{ maskAccount(item.accountNumber) }}</text>
<text class="info-time">{{ formatTime(item.createdAt) }}</text>
</view>
<view v-if="item.remark" class="item-remark">
<!-- 底部备注 -->
<view v-if="item.remark" class="item-bottom">
<view class="remark-label">
<u-icon name="info-circle" :size="14" color="#FF6B35" />
<text class="remark-label-text">备注</text>
</view>
<text class="remark-text">{{ item.remark }}</text>
</view>
</view>
@@ -95,6 +111,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { financeApi } from '@/api/seller/finance';
import { formatDateTime } from '@/utils/date';
const loading = ref(true);
const withdrawals = ref<any[]>([]);
@@ -106,8 +123,8 @@ const hasMore = ref(true);
const statusList = [
{ label: '全部', value: '' },
{ label: '待审核', value: 'pending' },
{ label: '处理中', value: 'processing' },
{ label: '已完成', value: 'completed' },
{ label: '已通过', value: 'approved' },
{ label: '已打款', value: 'paid' },
{ label: '已拒绝', value: 'rejected' },
];
@@ -134,7 +151,7 @@ async function fetchWithdrawals(reset = true) {
}
const res = await financeApi.getWithdrawals(params);
const newData = res.data.items || [];
const newData = res.data.list || [];
if (reset) {
withdrawals.value = newData;
@@ -160,8 +177,8 @@ function loadMore() {
function getStatusLabel(status: string): string {
const statusMap: Record<string, string> = {
pending: '待审核',
processing: '处理中',
completed: '已完成',
approved: '已通过',
paid: '已打款',
rejected: '已拒绝',
};
return statusMap[status] || status;
@@ -176,13 +193,13 @@ function getAccountLabel(type: string): string {
return typeMap[type] || type;
}
function getAccountIcon(type: string): string {
function getAccountIcon(type: string): string | null {
const iconMap: Record<string, string> = {
alipay: 'pay-circle',
wechat: 'weixin',
bank: 'credit-card',
};
return iconMap[type] || 'help-circle';
return iconMap[type] || null;
}
function maskAccount(account: string): string {
@@ -197,11 +214,7 @@ function formatMoney(value: number | string): string {
return num.toFixed(2);
}
function formatTime(time: string): string {
if (!time) return '';
const date = new Date(time);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}
const formatTime = formatDateTime;
function goDetail(id: number) {
// 提现详情页面可以后续添加
@@ -307,76 +320,86 @@ function goBack() {
.withdrawal-item {
margin: 0 24rpx 16rpx;
padding: 24rpx;
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
}
.item-header {
/* 顶部区域 */
.item-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
padding: 24rpx 24rpx 20rpx;
background: linear-gradient(135deg, #FFF8F5 0%, #FFFFFF 100%);
border-bottom: 1rpx solid #F5F5F5;
}
.header-left {
.amount-section {
display: flex;
align-items: center;
gap: 12rpx;
flex: 1;
flex-direction: column;
gap: 8rpx;
}
.account-info {
.amount-label {
font-size: 24rpx;
color: #999;
}
.amount-value {
font-size: 44rpx;
font-weight: 700;
color: #FF6B35;
line-height: 1;
}
.top-right {
display: flex;
align-items: center;
gap: 8rpx;
}
.account-text {
font-size: 26rpx;
color: #666;
}
.status-badge {
padding: 4rpx 12rpx;
border-radius: 12rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-weight: 500;
&.status-pending {
background: #FFF7E6;
background: linear-gradient(135deg, #FFF7E6 0%, #FFE7BA 100%);
}
&.status-processing {
background: #E6F7FF;
&.status-approved {
background: linear-gradient(135deg, #E6F7FF 0%, #BAE7FF 100%);
}
&.status-completed {
background: #F6FFED;
&.status-paid {
background: linear-gradient(135deg, #F6FFED 0%, #D9F7BE 100%);
}
&.status-rejected {
background: #FFF1F0;
background: linear-gradient(135deg, #FFF1F0 0%, #FFCCC7 100%);
}
}
.status-text {
font-size: 22rpx;
font-size: 24rpx;
.status-pending & {
color: #FAAD14;
color: #FA8C16;
}
.status-processing & {
.status-approved & {
color: #1890FF;
}
.status-completed & {
.status-paid & {
color: #52C41A;
}
@@ -385,45 +408,63 @@ function goBack() {
}
}
.item-amount {
margin-bottom: 12rpx;
}
.amount-value {
font-size: 40rpx;
font-weight: 700;
color: #1A1A1A;
}
.item-info {
/* 中间区域 */
.item-middle {
padding: 20rpx 24rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12rpx;
border-top: 1rpx solid #F0F0F0;
justify-content: space-between;
}
.info-text {
.info-label {
font-size: 26rpx;
color: #666;
min-width: 140rpx;
}
.info-value-row {
display: flex;
align-items: center;
gap: 6rpx;
}
.info-value {
font-size: 26rpx;
color: #1A1A1A;
font-weight: 500;
}
/* 底部区域 */
.item-bottom {
padding: 16rpx 24rpx 20rpx;
background: #FFFBF5;
border-top: 1rpx solid #FFE7BA;
}
.remark-label {
display: flex;
align-items: center;
gap: 6rpx;
margin-bottom: 8rpx;
}
.remark-label-text {
font-size: 24rpx;
color: #999;
}
.info-time {
font-size: 24rpx;
color: #999;
}
.item-remark {
margin-top: 12rpx;
padding: 12rpx;
background: #FFF7E6;
border-radius: 8rpx;
color: #FF6B35;
font-weight: 500;
}
.remark-text {
font-size: 24rpx;
color: #FAAD14;
line-height: 1.5;
color: #666;
line-height: 1.6;
padding-left: 20rpx;
}
/* 加载更多 */
+3 -5
View File
@@ -78,6 +78,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { verifyIdentity, getVerifyStatus } from '@/api/user/auth';
import { formatDateTime } from '@/utils/date';
const verifyStatus = ref<any>({
isVerified: false,
@@ -151,11 +152,8 @@ function maskName(name: string) {
return name[0] + '*'.repeat(name.length - 1);
}
function formatDate(date: string | null) {
if (!date) return '';
const d = new Date(date);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
const formatDate = formatDateTime;
</script>
<style lang="scss" scoped>
+120 -87
View File
@@ -25,28 +25,42 @@
:key="item.id"
class="withdrawal-item"
>
<view class="item-header">
<view class="header-left">
<view class="account-info">
<u-icon :name="getAccountIcon(item.accountType)" :size="20" color="#FF6B35" />
<text class="account-text">{{ getAccountLabel(item.accountType) }}</text>
</view>
<view :class="['status-badge', `status-${item.status}`]">
<text class="status-text">{{ getStatusLabel(item.status) }}</text>
</view>
<!-- 顶部金额和状态 -->
<view class="item-top">
<view class="amount-section">
<text class="amount-label">提现金额</text>
<text class="amount-value">¥{{ formatMoney(item.amount) }}</text>
</view>
<view :class="['status-badge', `status-${item.status}`]">
<text class="status-text">{{ getStatusLabel(item.status) }}</text>
</view>
</view>
<view class="item-amount">
<text class="amount-value">¥{{ formatMoney(item.amount) }}</text>
<!-- 中间账户信息 -->
<view class="item-middle">
<view v-if="item.accountType" class="info-row">
<text class="info-label">提现方式</text>
<view class="info-value-row">
<u-icon v-if="getAccountIcon(item.accountType)" :name="getAccountIcon(item.accountType)" :size="16" color="#FF6B35" />
<text class="info-value">{{ getAccountLabel(item.accountType) }}</text>
</view>
</view>
<view v-if="item.accountName || item.accountNumber" class="info-row">
<text class="info-label">账户信息</text>
<text class="info-value">{{ item.accountName }} {{ maskAccount(item.accountNumber) }}</text>
</view>
<view class="info-row">
<text class="info-label">申请时间</text>
<text class="info-value">{{ formatTime(item.createdAt) }}</text>
</view>
</view>
<view class="item-info">
<text class="info-text">{{ item.accountName }} {{ maskAccount(item.accountNumber) }}</text>
<text class="info-time">{{ formatTime(item.createdAt) }}</text>
</view>
<view v-if="item.remark" class="item-remark">
<!-- 底部备注 -->
<view v-if="item.remark" class="item-bottom">
<view class="remark-label">
<u-icon name="info-circle" :size="14" color="#FF6B35" />
<text class="remark-label-text">备注</text>
</view>
<text class="remark-text">{{ item.remark }}</text>
</view>
</view>
@@ -84,6 +98,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { walletApi } from '@/api/user/wallet';
import { formatDateTime } from '@/utils/date';
const loading = ref(true);
const withdrawals = ref<any[]>([]);
@@ -95,8 +110,8 @@ const hasMore = ref(true);
const statusList = [
{ label: '全部', value: '' },
{ label: '待审核', value: 'pending' },
{ label: '处理中', value: 'processing' },
{ label: '已完成', value: 'completed' },
{ label: '已通过', value: 'approved' },
{ label: '已打款', value: 'paid' },
{ label: '已拒绝', value: 'rejected' },
];
@@ -123,7 +138,7 @@ async function fetchWithdrawals(reset = true) {
}
const res = await walletApi.getWithdrawals(params);
const newData = res.data.items || [];
const newData = res.data.list || [];
if (reset) {
withdrawals.value = newData;
@@ -149,8 +164,8 @@ function loadMore() {
function getStatusLabel(status: string): string {
const statusMap: Record<string, string> = {
pending: '待审核',
processing: '处理中',
completed: '已完成',
approved: '已通过',
paid: '已打款',
rejected: '已拒绝',
};
return statusMap[status] || status;
@@ -164,12 +179,12 @@ function getAccountLabel(type: string): string {
return typeMap[type] || type;
}
function getAccountIcon(type: string): string {
function getAccountIcon(type: string): string | null {
const iconMap: Record<string, string> = {
alipay: 'pay-circle',
wechat: 'weixin',
};
return iconMap[type] || 'help-circle';
return iconMap[type] || null;
}
function maskAccount(account: string): string {
@@ -184,11 +199,7 @@ function formatMoney(value: number | string): string {
return num.toFixed(2);
}
function formatTime(time: string): string {
if (!time) return '';
const date = new Date(time);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}
const formatTime = formatDateTime;
function goBack() {
uni.navigateBack();
@@ -252,70 +263,74 @@ function goBack() {
.withdrawal-item {
margin: 0 24rpx 16rpx;
padding: 24rpx;
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
}
.item-header {
/* 顶部区域 */
.item-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
padding: 24rpx 24rpx 20rpx;
background: linear-gradient(135deg, #FFF8F5 0%, #FFFFFF 100%);
border-bottom: 1rpx solid #F5F5F5;
}
.header-left {
.amount-section {
display: flex;
align-items: center;
gap: 12rpx;
flex: 1;
}
.account-info {
display: flex;
align-items: center;
flex-direction: column;
gap: 8rpx;
}
.account-text {
font-size: 26rpx;
color: #666;
.amount-label {
font-size: 24rpx;
color: #999;
}
.amount-value {
font-size: 44rpx;
font-weight: 700;
color: #FF6B35;
line-height: 1;
}
.status-badge {
padding: 4rpx 12rpx;
border-radius: 12rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-weight: 500;
&.status-pending {
background: #FFF7E6;
background: linear-gradient(135deg, #FFF7E6 0%, #FFE7BA 100%);
}
&.status-processing {
background: #E6F7FF;
&.status-approved {
background: linear-gradient(135deg, #E6F7FF 0%, #BAE7FF 100%);
}
&.status-completed {
background: #F6FFED;
&.status-paid {
background: linear-gradient(135deg, #F6FFED 0%, #D9F7BE 100%);
}
&.status-rejected {
background: #FFF1F0;
background: linear-gradient(135deg, #FFF1F0 0%, #FFCCC7 100%);
}
}
.status-text {
font-size: 22rpx;
font-size: 24rpx;
.status-pending & {
color: #FAAD14;
color: #FA8C16;
}
.status-processing & {
.status-approved & {
color: #1890FF;
}
.status-completed & {
.status-paid & {
color: #52C41A;
}
@@ -324,45 +339,63 @@ function goBack() {
}
}
.item-amount {
margin-bottom: 12rpx;
}
.amount-value {
font-size: 40rpx;
font-weight: 700;
color: #1A1A1A;
}
.item-info {
/* 中间区域 */
.item-middle {
padding: 20rpx 24rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12rpx;
border-top: 1rpx solid #F0F0F0;
justify-content: space-between;
}
.info-text {
.info-label {
font-size: 26rpx;
color: #666;
min-width: 140rpx;
}
.info-value-row {
display: flex;
align-items: center;
gap: 6rpx;
}
.info-value {
font-size: 26rpx;
color: #1A1A1A;
font-weight: 500;
}
/* 底部区域 */
.item-bottom {
padding: 16rpx 24rpx 20rpx;
background: #FFFBF5;
border-top: 1rpx solid #FFE7BA;
}
.remark-label {
display: flex;
align-items: center;
gap: 6rpx;
margin-bottom: 8rpx;
}
.remark-label-text {
font-size: 24rpx;
color: #999;
}
.info-time {
font-size: 24rpx;
color: #999;
}
.item-remark {
margin-top: 12rpx;
padding: 12rpx;
background: #FFF7E6;
border-radius: 8rpx;
color: #FF6B35;
font-weight: 500;
}
.remark-text {
font-size: 24rpx;
color: #FAAD14;
line-height: 1.5;
color: #666;
line-height: 1.6;
padding-left: 20rpx;
}
/* 加载更多 */
+91 -4
View File
@@ -5,13 +5,43 @@
/**
* 格式化日期为 YYYY-MM-DD
*/
export function formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
export function formatDate(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 格式化日期时间为 YYYY-MM-DD HH:mm
*/
export function formatDateTime(dateTime: Date | string): string {
if (!dateTime) return '';
const d = typeof dateTime === 'string' ? new Date(dateTime) : dateTime;
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
/**
* 格式化日期时间为 YYYY-MM-DD HH:mm:ss
*/
export function formatDateTimeFull(dateTime: Date | string): string {
if (!dateTime) return '';
const d = typeof dateTime === 'string' ? new Date(dateTime) : dateTime;
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
/**
* 格式化日期为 M月D日
*/
@@ -20,6 +50,15 @@ export function formatDateShort(date: Date | string): string {
return `${d.getMonth() + 1}${d.getDate()}`;
}
/**
* 格式化日期范围为 M月D日 - M月D日
*/
export function formatDateRange(startDate: Date | string, endDate: Date | string): string {
const start = typeof startDate === 'string' ? new Date(startDate) : startDate;
const end = typeof endDate === 'string' ? new Date(endDate) : endDate;
return `${formatDateShort(start)} - ${formatDateShort(end)}`;
}
/**
* 获取日期描述(今天、明天、后天、周X)
*/
@@ -64,3 +103,51 @@ export function getDefaultDates(): { today: string; tomorrow: string } {
tomorrow: formatDate(tomorrow),
};
}
/**
* 获取今天的日期字符串 YYYY-MM-DD
*/
export function getTodayString(): string {
return formatDate(new Date());
}
/**
* 格式化相对时间(刚刚、X分钟前、X小时前、X天前)
*/
export function formatRelativeTime(dateTime: Date | string): string {
if (!dateTime) return '';
const d = typeof dateTime === 'string' ? new Date(dateTime) : dateTime;
const now = new Date();
const diff = now.getTime() - d.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
if (hours < 24) return `${hours}小时前`;
if (days < 7) return `${days}天前`;
return formatDateTime(d);
}
/**
* 格式化相对时间(今天、昨天、日期)
*/
export function formatRelativeDate(dateTime: Date | string): string {
if (!dateTime) return '';
const d = typeof dateTime === 'string' ? new Date(dateTime) : dateTime;
const now = new Date();
const diff = now.getTime() - d.getTime();
const day = 24 * 60 * 60 * 1000;
const timeStr = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
if (diff < day && d.getDate() === now.getDate()) {
return `今天 ${timeStr}`;
} else if (diff < 2 * day && d.getDate() === now.getDate() - 1) {
return `昨天 ${timeStr}`;
} else {
return `${d.getMonth() + 1}-${d.getDate()} ${timeStr}`;
}
}
+3 -3
View File
@@ -2,15 +2,15 @@
function getBaseURL(): string {
// 判断当前运行平台
// #ifdef H5
return 'http://localhost:3000';
return import.meta.env?.VITE_H5_API_BASE_URL as string || 'http://localhost:3000';
// #endif
// #ifdef MP-WEIXIN
return 'http://192.168.0.111:3000';
return import.meta.env?.VITE_MP_API_BASE_URL as string || 'http://localhost:3000';
// #endif
// 其他平台默认值
return 'http://192.168.0.111:3000';
return import.meta.env?.VITE_MP_API_BASE_URL as string || 'http://localhost:3000';
}
interface RequestOptions {