| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813 |
- <template>
- <view class="order-list-page">
- <nav-bar title="订单列表" :showBack="false"></nav-bar>
- <!-- 顶部状态栏 -->
- <view class="sticky-header">
- <scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
- <view class="tabs-row">
- <view v-for="tab in tabList" :key="tab.value"
- :class="['tab-item', { active: activeStatus === tab.value }]" @click="onTabClick(tab.value)">
- <text>{{ tab.title }}</text>
- </view>
- </view>
- </scroll-view>
- <!-- 搜索和过滤 -->
- <view class="filter-row">
- <picker :range="typeOptions" range-key="text" @change="onTypeChange">
- <view class="dropdown-btn">
- <text>{{ currentTypeName }}</text>
- <uni-icons type="bottom" size="12" color="#333"></uni-icons>
- </view>
- </picker>
- <view class="search-wrap">
- <uni-icons type="search" size="14" color="#999"></uni-icons>
- <input class="search-input" v-model="searchValue" placeholder="订单号/商户/宠主/手机号"
- placeholder-class="placeholder-style" @confirm="onSearch" />
- </view>
- </view>
- </view>
- <!-- 订单列表内容 -->
- <view class="list-container">
- <view class="order-card" v-for="order in orders" :key="order.id" @click="goToDetail(order)">
- <!-- 头部:订单号与状态 -->
- <view class="order-head">
- <text class="order-no">{{ order.id }}</text>
- <text :class="['status-text', order.statusClass]">{{ order.statusText }}</text>
- </view>
- <!-- 主体信息 -->
- <view class="order-body">
- <view class="service-row">
- <text class="service-name">{{ order.serviceType }}</text>
- <text class="service-tag tag-orange" v-if="order.serviceTags[0]">{{ order.serviceTags[0]
- }}</text>
- <text class="service-tag tag-blue" v-if="order.serviceTags[1] === '接'">接</text>
- <text class="service-tag tag-green" v-if="order.serviceTags[1] === '送'">送</text>
- </view>
- <view class="pet-row">
- <view class="pet-avatar-text">
- <text>{{ order.petName.substring(0, 1).toUpperCase() }}</text>
- </view>
- <view class="pet-desc">
- <text class="bold">{{ order.petName }}</text>
- <text class="sub">{{ order.petBreed }}</text>
- </view>
- <text class="user-desc">{{ order.userName }}</text>
- </view>
- <view class="info-list">
- <view class="info-item">
- <uni-icons type="location" size="14" color="#999"></uni-icons>
- <text>{{ order.address }}</text>
- </view>
- <view class="info-item">
- <uni-icons type="shop" size="14" color="#999"></uni-icons>
- <text>{{ order.shopName }} {{ order.userPhone }}</text>
- </view>
- <view class="info-item">
- <uni-icons type="calendar" size="14" color="#999"></uni-icons>
- <text>预约: {{ order.bookTime }}</text>
- </view>
- </view>
- </view>
- <!-- 履约与操作栏 -->
- <view class="order-foot">
- <view class="foot-left">
- <text class="create-time">下单: {{ order.createTime }}</text>
- <view class="assign-info">
- <text class="assign-label">履约信息:</text>
- <text class="assign-name" v-if="order.assigneeName">{{ order.assigneeName }}</text>
- <text class="assign-none" v-else>暂未指派</text>
- </view>
- <text class="cancel-time" v-if="order.statusText === '已取消' && order.cancelTime">取消: {{
- order.cancelTime }}</text>
- </view>
- <view class="actions">
- <template v-if="order.statusText === '待派单' || order.statusText === '待接单'">
- <button size="mini" class="action-btn btn-cancel"
- @click.stop="onCancelOrder(order)">取消</button>
- <button size="mini" class="action-btn btn-primary"
- @click.stop="goToDetail(order)">详情</button>
- </template>
- <template v-else-if="['服务中', '待商家确认', '已完成'].includes(order.statusText)">
- <button v-if="['服务中', '已完成'].includes(order.statusText)" size="mini"
- class="action-btn btn-cancel" @click.stop="onComplaint(order)">投诉</button>
- <button size="mini" class="action-btn btn-primary"
- @click.stop="goToDetail(order)">详情</button>
- </template>
- <template v-else>
- <button size="mini" class="action-btn btn-primary"
- @click.stop="goToDetail(order)">详情</button>
- </template>
- </view>
- </view>
- </view>
- <!-- 空状态 -->
- <view class="empty-state" v-if="!loading && orders.length === 0">
- <text class="empty-text">暂无相关订单</text>
- </view>
- <!-- 加载状态 -->
- <view class="loading-state" v-if="loading">
- <text class="loading-text">加载中...</text>
- </view>
- </view>
- <!-- 自定义取消订单弹窗 -->
- <view class="custom-modal" v-if="showCancelModal">
- <view class="modal-mask" @click="closeCancelModal"></view>
- <view class="modal-content">
- <view class="modal-title">提示</view>
- <view class="modal-body">
- <view style="margin-bottom: 20rpx; font-size: 28rpx; color: #666;">确定要取消订单 [{{ currentCancelOrder?.id }}] 吗?</view>
- <textarea class="cancel-input" v-model="cancelReason" placeholder="必填,请输入取消原因" placeholder-class="ph-color" :show-confirm-bar="false"></textarea>
- </view>
- <view class="modal-footer">
- <view class="modal-btn btn-cancel" @click="closeCancelModal">取消</view>
- <view class="modal-btn btn-confirm" @click="confirmCancelOrder">确定</view>
- </view>
- </view>
- </view>
- <custom-tabbar></custom-tabbar>
- </view>
- </template>
- <script setup>
- import { ref, computed, onMounted } from 'vue'
- import { onLoad } from '@dcloudio/uni-app'
- import navBar from '@/components/nav-bar/index.vue'
- import customTabbar from '@/components/custom-tabbar/index.vue'
- import orderStatusData from '@/json/orderStatus.json'
- import { listAll } from '@/api/service/list'
- import { listSubOrder, cancelSubOrder } from '@/api/order/subOrder'
- import { listAreaStation } from '@/api/system/areaStation'
- // 加载状态
- const loading = ref(false)
- // 筛选与搜索
- const activeStatus = ref(-1) // -1 表示全部,其他值为枚举值
- const filterType = ref(0)
- const searchValue = ref('')
- // 分页参数
- const pagination = ref({
- current: 1,
- size: 10,
- total: 0
- })
- // 从 orderStatus.json 生成 tabList
- const tabList = ref([
- { title: '全部订单', value: -1 },
- ...orderStatusData.map(item => ({
- title: item.label,
- value: item.value,
- color: item.color
- }))
- ])
- const typeOptions = ref([{ text: '全部类型', value: 0 }])
- const serviceList = ref([])
- const areaStationList = ref([])
- const areaStationMap = ref({})
- // 加载服务类型列表
- const loadServiceTypes = async () => {
- try {
- const services = await listAll()
- if (services && services.length > 0) {
- serviceList.value = services
- const serviceTypes = services.map((service, index) => ({
- text: service.name,
- value: index + 1,
- id: service.id
- }))
- typeOptions.value = [
- { text: '全部类型', value: 0 },
- ...serviceTypes
- ]
- }
- } catch (error) {
- console.error('加载服务类型失败:', error)
- }
- }
- // 加载区域站点列表
- const loadAreaStations = async () => {
- try {
- const res = await listAreaStation()
- if (res && res.data) {
- areaStationList.value = res.data
- const map = {}
- for (const item of res.data) {
- if (item && item.id !== undefined && item.id !== null) {
- map[item.id] = item
- }
- }
- areaStationMap.value = map
- }
- } catch (error) {
- console.error('加载区域站点失败:', error)
- }
- }
- onMounted(() => {
- loadServiceTypes()
- loadAreaStations()
- loadOrders()
- })
- const currentTypeName = computed(() => {
- const option = typeOptions.value.find(opt => opt.value === filterType.value)
- return option ? option.text : '全部类型'
- })
- // 根据枚举值获取状态信息
- const getStatusInfo = (value) => {
- return orderStatusData.find(item => item.value === value)
- }
- // 根据服务ID获取服务名称
- const getServiceName = (serviceId) => {
- const service = serviceList.value.find(s => s.id === serviceId)
- return service ? service.name : '未知服务'
- }
- // 获取城市/区域文本
- const getCityDistrictText = (row) => {
- if (!row || !row.site) return ''
- const station = areaStationMap.value[row.site]
- if (!station) return ''
- const parent = station.parentId ? areaStationMap.value[station.parentId] : undefined
- if (!parent) return station.name || ''
- if (parent.type === 0) return parent.name || ''
- if (parent.type === 1) {
- const city = parent.parentId ? areaStationMap.value[parent.parentId] : undefined
- return `${city?.name || ''}/${parent.name || ''}`
- }
- return parent.name || ''
- }
- // 获取服务模式标签
- const getServiceModeTag = (row) => {
- const t = row?.type
- if (t === 0 || t === '0' || t === 1 || t === '1') return '往返'
- return ''
- }
- // 获取服务订单类型标签
- const getServiceOrderTypeTag = (row) => {
- const t = row?.type
- if (t === 0 || t === '0') return { label: '接', type: 'blue' }
- if (t === 1 || t === '1') return { label: '送', type: 'green' }
- if (t === 2 || t === '2') return { label: '单程接', type: 'blue' }
- if (t === 3 || t === '3') return { label: '单程送', type: 'green' }
- return null
- }
- const onTabClick = (value) => {
- activeStatus.value = value
- pagination.value.current = 1
- loadOrders()
- }
- const onTypeChange = (e) => {
- const index = Number(e.detail.value)
- filterType.value = index
- pagination.value.current = 1
- loadOrders()
- }
- // 订单列表数据
- const orders = ref([])
- // 加载订单列表
- const loadOrders = async () => {
- loading.value = true
- try {
- const selectedType = typeOptions.value.find(opt => opt.value === filterType.value)
- const params = {
- pageNum: pagination.value.current,
- pageSize: pagination.value.size,
- status: activeStatus.value !== -1 ? activeStatus.value : undefined,
- service: selectedType && selectedType.id ? selectedType.id : undefined,
- content: searchValue.value || undefined
- }
- const res = await listSubOrder(params)
- console.log('后端返回数据:', res)
- if (res) {
- const rows = res.rows || []
- console.log('rows:', rows)
- orders.value = rows.map(row => transformOrder(row))
- console.log('转换后的orders:', orders.value)
- pagination.value.total = res.total || 0
- }
- } catch (error) {
- console.error('加载订单列表失败:', error)
- } finally {
- loading.value = false
- }
- }
- // 转换订单数据格式
- const transformOrder = (row) => {
- const statusInfo = getStatusInfo(row.status)
- const serviceName = getServiceName(row.service)
- const modeTag = getServiceModeTag(row)
- const typeTag = getServiceOrderTypeTag(row)
- const serviceTags = []
- if (modeTag) serviceTags.push(modeTag)
- if (typeTag) serviceTags.push(typeTag.label)
- return {
- // 先展开原始字段,后面的手动赋值具有更高优先级
- ...row,
- // 显示用的单号(优先用业务编号 code,否则用数据库 ID)
- id: row.code || row.id,
- rawId: row.id,
- serviceType: serviceName,
- serviceTags: serviceTags,
- petName: row.petName || '未知',
- petBreed: row.petBreed || '未知',
- userName: row.customerName || '未知',
- address: row.toAddress || row.fromAddress || getCityDistrictText(row),
- shopName: row.storeName || '未知',
- userPhone: row.contactPhoneNumber || '',
- bookTime: row.serviceTime || '',
- createTime: row.createTime || '',
- statusText: statusInfo ? statusInfo.label : '未知',
- statusClass: statusInfo ? `text-${statusInfo.color.replace('#', '')}` : 'text-gray',
- assigneeName: row.fulfillerName || '',
- cancelTime: row.cancelTime || ''
- }
- }
- // 搜索订单
- const onSearch = () => {
- pagination.value.current = 1
- loadOrders()
- }
- const showCancelModal = ref(false)
- const cancelReason = ref('')
- const currentCancelOrder = ref(null)
- // 取消订单
- const onCancelOrder = (order) => {
- currentCancelOrder.value = order
- cancelReason.value = ''
- showCancelModal.value = true
- }
- const closeCancelModal = () => {
- showCancelModal.value = false
- }
- const confirmCancelOrder = async () => {
- const reason = cancelReason.value.trim()
- if (!reason) {
- uni.showToast({ title: '取消原因不能为空', icon: 'none' })
- return
- }
- try {
- uni.showLoading({ title: '处理中' })
- await cancelSubOrder({ orderId: currentCancelOrder.value.rawId, reason })
- uni.hideLoading()
- uni.showToast({ title: '订单已取消', icon: 'success' })
- showCancelModal.value = false
- loadOrders()
- } catch (error) {
- uni.hideLoading()
- console.error('取消订单失败:', error)
- uni.showToast({ title: '取消失败', icon: 'none' })
- }
- }
- // 跳转到订单详情
- const goToDetail = (order) => {
- uni.navigateTo({
- url: `/pages/order/detail/index?id=${order.rawId}`
- })
- }
- // 投诉
- const onComplaint = (order) => {
- uni.navigateTo({
- url: `/pages/my/complaint/submit/index?orderId=${order.rawId}&fulfillerId=${order.fulfiller}&orderCode=${order.id}`
- })
- }
- </script>
- <style lang="scss" scoped>
- .order-list-page {
- background-color: #f2f2f2;
- min-height: 100vh;
- padding-bottom: 120rpx;
- }
- .sticky-header {
- position: sticky;
- top: calc(44px + var(--status-bar-height, 44px));
- z-index: 99;
- background-color: #fff;
- }
- .tabs-scroll {
- white-space: nowrap;
- border-bottom: 1rpx solid #f5f5f5;
- }
- .tabs-row {
- display: flex;
- padding: 0 16rpx;
- }
- .tab-item {
- padding: 24rpx 24rpx;
- font-size: 28rpx;
- color: #666;
- position: relative;
- flex-shrink: 0;
- }
- .tab-item.active {
- color: #333;
- font-weight: bold;
- }
- .tab-item.active::after {
- content: '';
- position: absolute;
- bottom: 0;
- left: 50%;
- transform: translateX(-50%);
- width: 48rpx;
- height: 6rpx;
- background-color: #f7ca3e;
- border-radius: 6rpx;
- }
- .filter-row {
- display: flex;
- align-items: center;
- padding: 12rpx 16rpx;
- background-color: #fff;
- border-top: 1rpx solid #f9f9f9;
- gap: 16rpx;
- }
- .dropdown-btn {
- display: flex;
- align-items: center;
- gap: 8rpx;
- background: #f5f5f5;
- border-radius: 34rpx;
- padding: 12rpx 24rpx;
- font-size: 26rpx;
- color: #333;
- }
- .search-wrap {
- flex: 1;
- display: flex;
- align-items: center;
- background: #f5f5f5;
- border-radius: 34rpx;
- padding: 12rpx 20rpx;
- gap: 12rpx;
- }
- .search-input {
- flex: 1;
- font-size: 24rpx;
- }
- .placeholder-style {
- color: #999;
- font-size: 24rpx;
- }
- .list-container {
- padding: 24rpx;
- }
- .order-card {
- padding: 28rpx;
- margin-bottom: 24rpx;
- background: #fff;
- border-radius: 24rpx;
- box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
- }
- .order-head {
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1rpx solid #f5f5f5;
- padding-bottom: 20rpx;
- margin-bottom: 20rpx;
- }
- .order-no {
- font-size: 26rpx;
- color: #666;
- }
- .status-text {
- font-size: 26rpx;
- font-weight: bold;
- }
- .text-red {
- color: #f44336;
- }
- .text-orange {
- color: #ff9800;
- }
- .text-blue {
- color: #2196f3;
- }
- .text-green {
- color: #4caf50;
- }
- .text-gray {
- color: #999;
- }
- .service-row {
- display: flex;
- align-items: center;
- margin-bottom: 20rpx;
- gap: 12rpx;
- }
- .service-name {
- font-size: 30rpx;
- font-weight: bold;
- color: #333;
- }
- .service-tag {
- font-size: 20rpx;
- padding: 2rpx 8rpx;
- border-radius: 8rpx;
- border: 1rpx solid;
- }
- .tag-orange {
- color: #ff9800;
- border-color: #ff9800;
- background: #fff3e0;
- }
- .tag-blue {
- color: #2196f3;
- border-color: #2196f3;
- background: #e3f2fd;
- }
- .tag-green {
- color: #4caf50;
- border-color: #4caf50;
- background: #e8f5e9;
- }
- .pet-row {
- display: flex;
- align-items: center;
- margin-bottom: 20rpx;
- background: #f7f8fa;
- padding: 16rpx 20rpx;
- border-radius: 16rpx;
- }
- .pet-avatar-text {
- width: 64rpx;
- height: 64rpx;
- border-radius: 50%;
- background-color: #e3f2fd;
- color: #2196f3;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: bold;
- font-size: 32rpx;
- margin-right: 20rpx;
- }
- .pet-desc {
- display: flex;
- align-items: baseline;
- gap: 12rpx;
- flex: 1;
- }
- .pet-desc .bold {
- font-size: 28rpx;
- font-weight: bold;
- color: #333;
- }
- .pet-desc .sub {
- font-size: 24rpx;
- color: #666;
- }
- .user-desc {
- font-size: 26rpx;
- color: #333;
- }
- .info-list {
- display: flex;
- flex-direction: column;
- gap: 12rpx;
- }
- .info-item {
- display: flex;
- align-items: center;
- font-size: 24rpx;
- color: #666;
- gap: 12rpx;
- }
- .order-foot {
- display: flex;
- justify-content: space-between;
- align-items: flex-end;
- margin-top: 24rpx;
- padding-top: 20rpx;
- border-top: 1rpx solid #f5f5f5;
- }
- .foot-left {
- display: flex;
- flex-direction: column;
- gap: 12rpx;
- }
- .create-time,
- .cancel-time {
- font-size: 22rpx;
- color: #999;
- }
- .assign-info {
- display: flex;
- align-items: center;
- font-size: 24rpx;
- gap: 12rpx;
- }
- .assign-label {
- color: #999;
- }
- .assign-none {
- color: #ccc;
- }
- .assign-name {
- color: #333;
- font-weight: bold;
- }
- .actions {
- display: flex;
- gap: 16rpx;
- }
- .action-btn {
- height: 56rpx;
- line-height: 56rpx;
- min-width: 120rpx;
- font-size: 24rpx;
- font-weight: 600;
- padding: 0 28rpx;
- border-radius: 28rpx;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- }
- .btn-cancel {
- border: 1rpx solid #ddd;
- color: #666;
- background: transparent;
- }
- .btn-primary {
- background: linear-gradient(90deg, #ffd53f, #ff9500);
- border: none;
- color: #fff;
- box-shadow: 0 6rpx 16rpx rgba(255, 149, 0, 0.3);
- }
- .empty-state {
- text-align: center;
- padding: 100rpx 0;
- }
- .empty-text {
- font-size: 28rpx;
- color: #999;
- }
- .loading-state {
- text-align: center;
- padding: 100rpx 0;
- }
- .loading-text {
- font-size: 28rpx;
- color: #999;
- }
- /* 自定义弹窗样式 */
- .custom-modal {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 999;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .modal-mask {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.5);
- }
- .modal-content {
- position: relative;
- width: 80%;
- background-color: #fff;
- border-radius: 16rpx;
- overflow: hidden;
- z-index: 1000;
- }
- .modal-title {
- padding: 30rpx 0 20rpx;
- text-align: center;
- font-size: 32rpx;
- font-weight: bold;
- color: #333;
- }
- .modal-body {
- padding: 10rpx 40rpx 30rpx;
- }
- .cancel-input {
- width: 100%;
- height: 160rpx;
- background-color: #f8f8f8;
- border-radius: 8rpx;
- padding: 20rpx;
- font-size: 28rpx;
- box-sizing: border-box;
- color: #333;
- }
- .ph-color {
- color: #999;
- }
- .modal-footer {
- display: flex;
- border-top: 1rpx solid #eee;
- }
- .modal-btn {
- flex: 1;
- height: 90rpx;
- line-height: 90rpx;
- text-align: center;
- font-size: 30rpx;
- font-weight: 500;
- }
- .btn-cancel {
- color: #666;
- border-right: 1rpx solid #eee;
- }
- .btn-confirm {
- color: #2196f3;
- }
- </style>
|