添加新页面

This commit is contained in:
not-like-juvenile
2026-01-23 17:13:39 +08:00
parent d5a3a4e8f0
commit b1c845d571
9 changed files with 2229 additions and 557 deletions

View File

@@ -0,0 +1,202 @@
<template>
<view class="ratings-page">
<!-- 顶部总览卡片 -->
<view class="summary-card">
<!-- 返回按钮 -->
<view class="back-box" @click="backToIndex">
<text class="back-icon"></text>
</view>
<view class="summary-left">
<text class="score">4.9</text>
<text class="stars">
<text class="star-on">★</text>
<text class="star-on">★</text>
<text class="star-on">★</text>
<text class="star-on">★</text>
<text class="star-on">★</text>
</text>
<text class="count">共收到 128 条评价</text>
</view>
<view class="summary-right">
<view class="rate-item">
<text class="rate-label">好评率</text>
<text class="rate-value">96.1%</text>
</view>
</view>
</view>
<!-- 标签筛选 -->
<view class="filter-bar">
<view
v-for="t in tabs"
:key="t.key"
:class="['filter-tab', currentTab===t.key?'active':'']"
@click="switchTab(t.key)"
>
<text>{{ t.label }}</text>
</view>
</view>
<!-- 评价列表 -->
<scroll-view
scroll-y
class="ratings-scroll"
refresher-enabled
:refresher-triggered="isRefreshing"
@scrolltolower="loadMore"
@refresherrefresh="onRefresh"
>
<view v-if="list.length" class="rating-list">
<view v-for="item in list" :key="item.id" class="rating-card">
<view class="rating-header">
<image :src="item.avatar" class="user-avatar" mode="aspectFill" />
<view class="user-info">
<text class="user-name">{{ item.userName }}</text>
<text class="rating-stars">
<text v-for="s in 5" :key="s" :class="s<=item.score?'star-on':'star-off'">★</text>
</text>
</view>
<text class="rating-time">{{ item.time }}</text>
</view>
<text v-if="item.comment" class="rating-comment">{{ item.comment }}</text>
<text v-else class="rating-comment empty">用户未写评价</text>
<view v-if="item.tags&&item.tags.length" class="rating-tags">
<view v-for="tag in item.tags" :key="tag" class="tag">{{ tag }}</view>
</view>
</view>
<view v-if="hasMore" class="load-tip">正在加载…</view>
<view v-else class="load-tip">已加载全部</view>
</view>
<view v-else class="no-data">
<text class="no-data-icon">📝</text>
<text class="no-data-text">暂无评价记录</text>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
/* 返回按钮 */
function backToIndex() {
uni.navigateBack({ url: '/pages/mall/delivery/index' })
}
/* mock 数据 */
const currentTab = ref('all')
const isRefreshing = ref(false)
const hasMore = ref(true)
const page = ref(1)
const list = ref<any[]>([])
const tabs = [
{ key: 'all', label: '全部' },
{ key: 'good', label: '好评' },
{ key: 'bad', label: '差评' }
]
function mockList() {
const tagPool = ['准时送达', '着装整洁', '服务热情', '配送快', '餐品完好']
return Array.from({ length: 10 }, (_, i) => ({
id: `${currentTab.value}_${page.value}_${i}`,
userName: '用户' + (Math.random() * 1000).toFixed(0),
avatar: 'https://img.yzcdn.cn/vant/cat.jpeg',
score: currentTab.value === 'bad' ? Math.floor(Math.random() * 2) + 1 : Math.floor(Math.random() * 2) + 4,
comment: Math.random() > 0.3 ? '配送很及时,服务态度很好!' : '',
tags: tagPool.slice(0, Math.floor(Math.random() * 3) + 1),
time: `${Math.floor(Math.random() * 60)}分钟前`
}))
}
async function fetchList(reset = false) {
if (reset) page.value = 1
const newList = mockList()
if (reset) list.value = newList
else list.value.push(...newList)
hasMore.value = page.value < 4
page.value++
}
function switchTab(key: string) {
if (currentTab.value === key) return
currentTab.value = key
fetchList(true)
}
function loadMore() {
if (!hasMore.value) return
fetchList(false)
}
function onRefresh() {
isRefreshing.value = true
fetchList(true).finally(() => isRefreshing.value = false)
}
onMounted(() => fetchList(true))
</script>
<style scoped>
.ratings-page{min-height:100vh;background:#f5f5f5;display:flex;flex-direction:column;}
.summary-card{
margin:20rpx 30rpx;
background:#fff;border-radius:20rpx;padding:40rpx;
display:flex;align-items:center;position:relative;
}
.back-box{
position:absolute;left:30rpx;top:50%;transform:translateY(-50%);
width:60rpx;height:60rpx;border-radius:50%;
background:rgba(0,0,0,.05);display:flex;align-items:center;justify-content:center;
}
.back-box:active{background:rgba(0,0,0,.15);}
.back-icon{font-size:40rpx;color:#333;}
.summary-left{flex:1;display:flex;flex-direction:center;align-items:center;}
.score{font-size:64rpx;font-weight:bold;color:#ff9500;margin-right:20rpx;}
.stars .star-on{color:#ff9500;}
.count{font-size:24rpx;color:#666;margin-left:20rpx;}
.summary-right .rate-item{text-align:center;}
.rate-label{font-size:24rpx;color:#666;}
.rate-value{font-size:40rpx;font-weight:bold;color:#4caf50;}
.filter-bar{
margin:0 30rpx 20rpx;
background:#fff;border-radius:20rpx;padding:20rpx;
display:flex;justify-content:space-around;
}
.filter-tab{
padding:10rpx 30rpx;border-radius:30rpx;font-size:26rpx;
background:#f0f0f0;color:#666;
}
.filter-tab.active{background:#74b9ff;color:#fff;}
.ratings-scroll{flex:1;}
.rating-list{padding:0 30rpx 30rpx;}
.rating-card{
background:#fff;border-radius:16rpx;padding:30rpx;margin-bottom:20rpx;
}
.rating-header{display:flex;align-items:center;}
.user-avatar{width:80rpx;height:80rpx;border-radius:50%;margin-right:20rpx;}
.user-info{flex:1;}
.user-name{font-size:28rpx;color:#333;}
.rating-stars{font-size:24rpx;}
.rating-stars .star-on{color:#ff9500;}
.rating-stars .star-off{color:#ddd;}
.rating-time{font-size:22rpx;color:#999;}
.rating-comment{margin:20rpx 0;font-size:28rpx;color:#333;line-height:1.5;}
.rating-comment.empty{color:#bbb;}
.rating-tags{display:flex;flex-wrap:wrap;gap:10rpx;margin-top:15rpx;}
.tag{background:#e8f4fd;color:#1976d2;font-size:22rpx;padding:4rpx 12rpx;border-radius:8rpx;}
.load-tip{text-align:center;font-size:24rpx;color:#999;padding:20rpx 0;}
.no-data{text-align:center;padding:120rpx 0;}
.no-data-icon{font-size:80rpx;}
.no-data-text{font-size:28rpx;color:#999;margin-top:20rpx;}
</style>