Files
Review-procedure/src/pages/check/index.vue
2026-02-07 16:07:10 +08:00

2098 lines
43 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="index-container">
<!-- 返回按钮 -->
<!-- <view v-if="showBackButton" class="back-button" @click="handleBack">
<IconFont name="arrow-left" color="#333" :size="20" />
<text class="back-text">返回</text>
</view> -->
<view v-if="!isBound" class="state-hint">
<view class="state-img-wrapper" @click="handleEmptyStateClick">
<image class="state-img" :src="emptyImg" mode="aspectFit" />
</view>
<view class="state-title">暂无数据</view>
</view>
<view v-else class="list-container">
<!-- 搜索框和状态筛选 -->
<view class="search-container">
<view class="search-box">
<IconFont name="search" color="#999" :size="16" />
<input :value="searchKeyword" @input="(e) => searchKeyword = e.detail.value" placeholder="请输入订单号搜索"
class="search-input" @confirm="handleSearch" confirm-type="search" />
<view v-if="searchKeyword" class="search-clear" @click="clearSearch">
<IconFont name="close" color="#999" :size="14" />
</view>
</view>
<!-- 状态筛选Tab 可换行显示 -->
<view class="status-tabs">
<view class="status-tab" v-for="(item, idx) in statusOptions" :key="idx"
:class="{ active: selectedStatusIndex === idx }" @click="onStatusTabClick(idx)">
{{ item.label }}
</view>
</view>
</view>
<OrderList
:check-status="selectedStatus"
:search-keyword="searchKeyword"
:show-action="true"
:can-view="canView"
:can-add="canAdd"
:can-edit="canEdit"
:can-delete="canDelete"
@item-click="handleItemClick"
@refresh="handleListRefresh"
@viewCheck="handleViewCheck"
@view-check="handleViewCheck"
ref="orderListRef" />
</view>
<!-- 图片预览弹窗 -->
<nut-popup v-model:visible="showImagePopup" position="center" :style="{ padding: '20rpx' }" round
:close-on-click-overlay="true">
<view class="image-popup">
<view class="image-popup-header">
<text class="image-popup-title">
图片预览 ({{ currentImageIndex + 1 }}/{{ currentImages.length }})
</text>
<view class="image-popup-close" @click="showImagePopup = false">
<IconFont name="close" color="#666" :size="20" />
</view>
</view>
<swiper class="image-swiper" :current="currentImageIndex" @change="onSwiperChange"
:indicator-dots="currentImages.length > 1" :indicator-color="'rgba(0, 0, 0, 0.3)'"
:indicator-active-color="'#1890ff'" :circular="true">
<swiper-item v-for="(image, index) in currentImages" :key="index">
<image :src="image" mode="aspectFit" class="preview-image" />
</swiper-item>
</swiper>
</view>
</nut-popup>
<!-- 质检表单弹窗 -->
<CheckFormPopup v-model="showFormPopup" :order-item="currentFormItem" :mode="formMode"
@submit-success="handleFormSubmitSuccess" />
<!-- 查看质检信息弹窗 -->
<CheckFormPopup v-model="showViewCheckPopup" :order-item="currentViewItem" mode="view" />
<!-- 绑定弹窗 -->
<BindPopup v-model="showBindPopup" @bind-success="handleBindSuccess" />
</view>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import Taro from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue'
import { request } from '../../utils/request'
import api from '../../api'
import emptyImg from '../../assets/empty-state.svg'
import OrderList from '../../components/order-list/index.vue'
import BindPopup from '../../components/bind-popup/index.vue'
import CheckFormPopup from '../../components/check-form-popup/index.vue'
import { useAuth } from '../../composables/useAuth'
// 导入图片工具函数
import { buildImgUrl, buildFileUrl } from '../../utils/image'
// 判断是否显示返回按钮页面栈长度大于1时显示
const showBackButton = computed(() => {
try {
const pages = Taro.getCurrentPages()
return pages.length > 1
} catch (e) {
return false
}
})
// 返回上一页
const handleBack = () => {
try {
const pages = Taro.getCurrentPages()
if (pages.length > 1) {
Taro.navigateBack()
} else {
// 如果没有上一页,跳转到首页
Taro.switchTab({
url: '/pages/home/index'
})
}
} catch (error) {
console.error('返回失败:', error)
// 如果 navigateBack 失败,尝试跳转到首页
Taro.switchTab({
url: '/pages/home/index'
})
}
}
// 图标列表NutUI内置图标使用 IconFont 的 name 属性)
const iconList = [
'home',
'my',
'setting',
'notice',
'cart',
'category',
'star',
'heart',
'share',
'download',
'uploader',
'edit',
'del',
'search',
'find',
'add',
'minus',
'check',
'close',
'arrow-right',
'star-fill',
'heart-fill',
'cart2',
'shop',
'people'
]
// 图标颜色列表(蓝色系)
const iconColors = [
'#1890ff',
'#40a9ff',
'#69c0ff',
'#91d5ff',
'#bae7ff',
'#096dd9',
'#0050b3'
]
// 标题模板
const titleTemplates = [
'项目管理系统',
'数据分析平台',
'客户关系管理',
'在线协作工具',
'内容管理系统',
'电商运营平台',
'移动应用开发',
'云存储服务',
'智能推荐系统',
'企业办公套件',
'在线教育平台',
'社交网络应用',
'视频会议系统',
'任务管理工具',
'财务管理系统'
]
// 描述模板
const descTemplates = [
'高效的项目管理解决方案,提升团队协作效率',
'强大的数据分析能力,助力业务决策',
'完善的客户管理功能,提升客户满意度',
'实时协作编辑,让团队工作更高效',
'灵活的内容管理,满足各种业务需求',
'全面的电商功能,助力业务增长',
'专业的移动应用开发服务',
'安全可靠的云存储解决方案',
'基于AI的智能推荐算法',
'一站式企业办公解决方案',
'优质的在线教育体验',
'连接你我,分享生活',
'高清流畅的视频会议体验',
'简单易用的任务管理工具',
'专业的财务管理解决方案'
]
// 位置列表
const locations = [
'北京',
'上海',
'广州',
'深圳',
'杭州',
'成都',
'武汉',
'西安',
'南京',
'重庆'
]
// 生成随机数据
const generateRandomItem = (id) => {
const randomIcon = iconList[Math.floor(Math.random() * iconList.length)]
const randomIconColor = iconColors[Math.floor(Math.random() * iconColors.length)]
const randomTitle = titleTemplates[Math.floor(Math.random() * titleTemplates.length)]
const randomDesc = descTemplates[Math.floor(Math.random() * descTemplates.length)]
const randomLocation = locations[Math.floor(Math.random() * locations.length)]
const randomTime = `${Math.floor(Math.random() * 12) + 1}小时前`
const images = getRandomImages(id)
return {
id,
icon: randomIcon,
iconColor: randomIconColor,
title: randomTitle,
description: randomDesc,
location: randomLocation,
time: randomTime,
images: images
}
}
// 状态文案
const getStatusText = (status) => {
const map = {
0: '待质检',
2: '质检通过',
3: '质检失败'
}
return map[status] || '未知状态'
}
// 列表数据
const listData = ref([])
const safeList = computed(() => {
const arr = Array.isArray(listData.value) ? listData.value : []
return arr.filter(Boolean)
})
const loading = ref(false)
const hasMore = ref(true)
const page = ref(1)
const pageSize = 10
const searchKeyword = ref('') // 搜索关键词
// 状态筛选Tab
// 状态:全部 / 待质检 / 已质检 / 拆包中 / 已驳回 / 质检完成 / 质检作废
const statusOptions = [
{ label: '全部', value: '' },
{ label: '待质检', value: 0 },
{ label: '已质检', value: 1 },
{ label: '拆包中', value: 2 },
{ label: '已驳回', value: 3 },
{ label: '质检完成', value: 4 },
{ label: '质检作废', value: 5 }
]
const selectedStatusIndex = ref(0) // 当前选中的状态索引
const selectedStatus = computed(() => statusOptions[selectedStatusIndex.value].value)
// 弹窗控制
const showImagePopup = ref(false)
const showFormPopup = ref(false)
const currentImages = ref([])
const currentImageIndex = ref(0)
const formRef = ref(null)
// 使用公共登录逻辑
const { token, isBound, showBindPopup, checkAuth, showBindPopupFromError } = useAuth()
// 权限检查
const permissions = ref([])
// 权限常量
const PERMISSIONS = {
VIEW: 'twopoint_zhxyOrderCheck_view', // 查看
ADD: 'twopoint_zhxyOrderCheck_add', // 新增
EDIT: 'twopoint_zhxyOrderCheck_edit', // 编辑
DEL: 'twopoint_zhxyOrderCheck_del' // 删除
}
// 权限检查函数
const hasPermission = (permission) => {
if (!permissions.value || permissions.value.length === 0) {
console.log('权限数组为空,返回 false')
return false
}
const has = permissions.value.includes(permission)
console.log(`检查权限 ${permission}:`, has, '权限数组:', permissions.value)
return has
}
// 计算属性:是否有查看权限
const canView = computed(() => hasPermission(PERMISSIONS.VIEW))
// 计算属性:是否有新增权限
const canAdd = computed(() => hasPermission(PERMISSIONS.ADD))
// 计算属性:是否有编辑权限
const canEdit = computed(() => hasPermission(PERMISSIONS.EDIT))
// 计算属性:是否有删除权限
const canDelete = computed(() => hasPermission(PERMISSIONS.DEL))
// 加载权限数据
const loadPermissions = () => {
try {
const storedPermissions = Taro.getStorageSync('permissions')
console.log('存储的权限数据:', storedPermissions)
console.log('权限数据类型:', typeof storedPermissions)
console.log('是否为数组:', Array.isArray(storedPermissions))
if (storedPermissions) {
// 如果是数组,直接使用
if (Array.isArray(storedPermissions)) {
permissions.value = storedPermissions
}
// 如果是对象,尝试提取权限数组
else if (typeof storedPermissions === 'object') {
// 可能是 { permissions: [...] } 格式
if (storedPermissions.permissions && Array.isArray(storedPermissions.permissions)) {
permissions.value = storedPermissions.permissions
}
// 可能是对象数组,需要提取权限标识
else if (Array.isArray(Object.values(storedPermissions))) {
// 尝试从对象中提取权限标识
permissions.value = Object.values(storedPermissions).filter(p => typeof p === 'string')
}
// 如果是对象,尝试获取所有键值
else {
permissions.value = Object.keys(storedPermissions).filter(k => typeof k === 'string')
}
}
// 如果是字符串,尝试解析
else if (typeof storedPermissions === 'string') {
try {
const parsed = JSON.parse(storedPermissions)
if (Array.isArray(parsed)) {
permissions.value = parsed
}
} catch (e) {
permissions.value = []
}
}
else {
permissions.value = []
}
} else {
permissions.value = []
}
console.log('最终权限数组:', permissions.value)
console.log('权限检查结果:', {
canView: canView.value,
canAdd: canAdd.value,
canEdit: canEdit.value,
canDelete: canDelete.value
})
} catch (error) {
console.error('加载权限失败:', error)
permissions.value = []
}
}
// 检查是否需要重新绑定(来自 424 错误)
const checkNeedRebind = () => {
const needRebind = Taro.getStorageSync('needRebind')
if (needRebind) {
showBindPopupFromError()
}
}
const canViewList = computed(() => isBound.value)
const isFirstLoad = ref(true) // 是否是首次加载
// 点击暂无数据图标,触发登录检查
const handleEmptyStateClick = async () => {
await checkAuth()
}
// 绑定成功回调
const handleBindSuccess = () => {
// 绑定成功后刷新列表
if (orderListRef.value) {
orderListRef.value.refresh()
}
// 绑定成功后如果有待打开的列表项根据checkStatus打开表单
if (currentItem.value) {
setTimeout(() => {
const checkStatus = currentItem.value.checkStatus
if (checkStatus === 0) {
openForm(currentItem.value, 'create')
} else if (checkStatus === 3) {
openForm(currentItem.value, 'edit')
}
currentItem.value = null
}, 500)
}
}
const formMode = ref('create') // 'create' 或 'edit'
const currentFormItem = ref(null) // 当前表单对应的订单项
const showViewCheckPopup = ref(false) // 查看质检信息弹窗
const currentViewItem = ref(null) // 当前查看的订单项
// 生成随机图片URL数组使用占位图服务
const getRandomImages = (id) => {
// 每个列表项生成1-5张随机图片
const imageCount = Math.floor(Math.random() * 5) + 1
const images = []
const width = 600
const height = 400
for (let i = 0; i < imageCount; i++) {
const seed = (id * 10 + i) % 100
images.push(`https://picsum.photos/seed/${seed}/${width}/${height}`)
}
return images
}
// 从 localImg 字符串拆解图片URL数组
const buildImagesFromLocalImg = (localImg) => {
if (!localImg) return []
const source = Array.isArray(localImg) ? localImg : String(localImg).split(/[,;\s]+/)
return source
.map((item) => String(item).trim())
.filter(Boolean)
.map((fileName) => buildFileUrl(fileName))
}
// 加载数据
const loadData = async (isRefresh = false) => {
if (loading.value) return
if (!canViewList.value) {
loading.value = false
return
}
loading.value = true
try {
// 调用真实的 API 接口
const params = {
quecurrent: isRefresh ? 1 : page.value,
size: pageSize
}
// 如果有搜索关键词添加orderNum参数
if (searchKeyword.value && searchKeyword.value.trim()) {
params.orderNum = searchKeyword.value.trim()
}
// 如果有选中的状态添加checkStatus参数
if (selectedStatus.value !== '') {
params.checkStatus = selectedStatus.value
}
const res = await api.getCheckOrder(params)
if (res.statusCode === 200) {
const raw =
res.data?.records ??
res.data?.data?.records ??
res.data?.data ??
res.data ??
[]
const newData = Array.isArray(raw) ? raw : []
// 规范化数据:处理 localImg 为图片数组,并确保 id 存在
const startIndex = isRefresh ? 0 : listData.value.length
const processed = newData.map((item, idx) => {
const images =
Array.isArray(item?.images) && item.images.length > 0
? item.images
: buildImagesFromLocalImg(item?.localImg)
const fallbackId =
item?.id ??
item?.contractId ??
`${item?.companyName || 'item'}-${startIndex + idx}`
return {
...item,
id: fallbackId,
images
}
})
if (isRefresh) {
listData.value = processed
page.value = 1
} else {
listData.value = [...listData.value, ...processed]
page.value++
}
// 判断是否还有更多数据
if (newData.length < pageSize) {
hasMore.value = false
}
} else {
throw new Error(res.data?.message || '请求失败')
}
} catch (error) {
Taro.showToast({
title: error.message || '加载失败,请重试',
icon: 'none',
duration: 2000
})
} finally {
loading.value = false
// 停止下拉刷新动画
if (isRefresh) {
Taro.stopPullDownRefresh()
}
}
}
// 下拉刷新函数
const onRefresh = () => {
if (!canViewList.value) {
Taro.showToast({
title: '请先登录并绑定手机号',
icon: 'none',
duration: 2000
})
Taro.stopPullDownRefresh()
return
}
if (orderListRef.value) {
orderListRef.value.refresh()
}
Taro.stopPullDownRefresh()
}
// 触底加载 - 使用页面滚动到底部事件
const onReachBottom = () => {
if (canViewList.value && orderListRef.value) {
orderListRef.value.loadMore()
}
}
// 点击图标 - 单图预览carImg
const handleIconClick = (item) => {
if (!item) return
const imgUrl = buildImgUrl(item.carImg || '')
if (!imgUrl) {
Taro.showToast({
title: '暂无车辆图片',
icon: 'none',
duration: 1500
})
return
}
Taro.previewImage({
current: imgUrl,
urls: [imgUrl]
})
}
// 拨打电话
const makePhoneCall = (phone) => {
if (!phone) {
Taro.showToast({
title: '当前司机无手机号',
icon: 'none',
duration: 1500
})
return
}
Taro.makePhoneCall({
phoneNumber: String(phone)
})
}
// Swiper 切换事件
const onSwiperChange = (e) => {
currentImageIndex.value = e.detail.current
}
// 存储当前点击的列表项,用于登录后打开表单
const currentItem = ref(null)
// 打开表单 - 使用组件
const openForm = (item, mode = 'create') => {
if (!item) return
currentFormItem.value = item
formMode.value = mode
showFormPopup.value = true
}
// 格式化时间:只显示年月日、时分
const formatDateTime = (dateTime) => {
if (!dateTime) return ''
try {
const date = new Date(dateTime)
if (isNaN(date.getTime())) {
// 如果不是标准日期格式,尝试其他格式
return dateTime
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
} catch (e) {
// 如果解析失败,返回原值
return dateTime
}
}
// 搜索处理
const orderListRef = ref(null)
const handleSearch = () => {
// 通过组件ref刷新列表
if (orderListRef.value) {
orderListRef.value.refresh()
}
}
// 清空搜索
const clearSearch = () => {
searchKeyword.value = ''
// 通过组件ref刷新列表
if (orderListRef.value) {
orderListRef.value.refresh()
}
}
// 状态筛选改变
const onStatusChange = (e) => {
const index = parseInt(e.detail.value)
selectedStatusIndex.value = index
// 状态改变时重置分页并刷新数据
page.value = 1
hasMore.value = true
listData.value = []
loadData(true)
}
// 复制订单号
const copyOrderNum = (orderNum) => {
if (!orderNum) return
Taro.setClipboardData({
data: String(orderNum),
success: () => {
Taro.showToast({
title: '订单号已复制',
icon: 'success',
duration: 2000
})
},
fail: () => {
Taro.showToast({
title: '复制失败',
icon: 'none',
duration: 2000
})
}
})
}
// 显示确认框后再打开表单
const showConfirmBeforeOpenForm = (item, mode) => {
// 获取车牌号和品类名称
const carNum = item.carNum || '--'
const categoryName = item.orderTypeName || '--'
Taro.showModal({
title: '确认质检',
content: `车牌号:${carNum}\n品类${categoryName}\n\n确认开始质检吗`,
confirmText: '确认',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 用户确认后打开表单
openForm(item, mode)
}
}
})
}
// 点击列表项其他部分 - 根据checkStatus判断
const handleItemClick = (item) => {
if (!item) return
// 检查绑定状态
if (!isBound.value) {
currentItem.value = item
showBindPopup.value = true
return
}
const checkStatus = item.checkStatus
if (checkStatus === 0) {
// 待质检:先显示确认框,再打开新增表单
// 权限检查在 showConfirmBeforeOpenForm 中进行,无权限会静默返回
if (canAdd.value) {
showConfirmBeforeOpenForm(item, 'create')
}
} else if (checkStatus === 3) {
// 供应商驳回:先显示确认框,再打开编辑表单
// 权限检查在 showConfirmBeforeOpenForm 中进行,无权限会静默返回
if (canEdit.value) {
showConfirmBeforeOpenForm(item, 'edit')
}
} else if (checkStatus === 2) {
// 供应商确认,拆包中:允许编辑
// 权限检查在 showConfirmBeforeOpenForm 中进行,无权限会静默返回
if (canEdit.value) {
showConfirmBeforeOpenForm(item, 'edit')
}
}
// 其他状态静默处理,不显示任何提示
}
// 列表刷新回调
const handleListRefresh = () => {
if (orderListRef.value) {
orderListRef.value.refresh()
}
}
// 查看质检信息
const handleViewCheck = (item) => {
if (!item) {
return
}
// 权限检查 - 静默处理,无权限直接返回
if (!canView.value) {
return
}
// 先设置订单项,再打开弹窗,确保数据传递正确
currentViewItem.value = { ...item }
showViewCheckPopup.value = true
}
// 状态Tab点击
const onStatusTabClick = (idx) => {
selectedStatusIndex.value = idx
// OrderList 组件的 watch 会自动监听 checkStatus 变化并刷新
}
// 表单提交成功回调
const handleFormSubmitSuccess = () => {
// 刷新列表
if (orderListRef.value) {
orderListRef.value.refresh()
}
}
// 初始化
onMounted(() => {
// 加载权限数据
loadPermissions()
// 页面进入时检查登录状态
checkAuth().then((isAuth) => {
// 检查是否需要重新绑定(来自 424 错误)
checkNeedRebind()
// 登录检查完成后,将刷新函数存储到全局,供 script 块使用
if (typeof globalThis !== 'undefined') {
globalThis.__pageRefreshHandler = onRefresh
globalThis.__pageReachBottomHandler = onReachBottom
}
// 检查 URL 参数,如果是从首页跳转过来的,自动打开表单
const pages = Taro.getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.options) {
const { orderId, autoOpen } = currentPage.options
if (orderId && autoOpen === 'true' && isAuth) {
// 从订单列表中找到对应的订单
setTimeout(async () => {
if (orderListRef.value) {
// 先刷新列表,确保数据是最新的
await orderListRef.value.refresh()
// 等待列表加载完成后再查找订单
setTimeout(() => {
// 这里需要从 OrderList 组件获取数据,或者直接调用接口获取订单详情
// 暂时先通过接口获取订单详情
api.getCheckOrder({ quecurrent: 1, size: 100 }).then(res => {
if (res.statusCode === 200) {
const orders = res.data?.records || res.data?.data?.records || res.data?.data || []
const targetOrder = orders.find(order =>
String(order.id) === String(orderId) ||
String(order.orderId) === String(orderId)
)
if (targetOrder) {
handleItemClick(targetOrder)
}
}
})
}, 500)
}
}, 300)
}
}
})
})
// 监听登录与绑定状态,只在首次加载时自动加载
watch(canViewList, (val) => {
if (val && isFirstLoad.value && listData.value.length === 0) {
hasMore.value = true
page.value = 1
loadData(true)
isFirstLoad.value = false
}
})
</script>
<script>
import Taro from '@tarojs/taro'
// Taro 页面生命周期
export default {
onPullDownRefresh() {
// 从全局获取刷新函数
if (typeof globalThis !== 'undefined' && globalThis.__pageRefreshHandler) {
globalThis.__pageRefreshHandler()
} else {
// 如果无法访问,至少停止刷新动画
setTimeout(() => {
Taro.stopPullDownRefresh()
}, 1000)
}
},
onReachBottom() {
// 触底加载
if (typeof globalThis !== 'undefined' && globalThis.__pageReachBottomHandler) {
globalThis.__pageReachBottomHandler()
}
}
}
</script>
<style lang="less">
.back-button {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
margin: 24rpx 24rpx 16rpx 24rpx;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
cursor: pointer;
}
.back-button:active {
opacity: 0.7;
}
.back-text {
margin-left: 8rpx;
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.index-container {
min-height: 100vh;
background-color: #f5f7fa;
}
.list-container {
padding: 24rpx;
padding-bottom: 148rpx;
background: linear-gradient(180deg, #f7f9fc 0%, #f5f7fa 100%);
}
/* 搜索框样式 */
.search-container {
padding: 0 0rpx 20rpx 0rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.search-box {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 0 24rpx;
height: 80rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-box .nut-icon {
margin-right: 16rpx;
flex-shrink: 0;
}
.search-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #333;
}
.search-clear {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 16rpx;
flex-shrink: 0;
cursor: pointer;
}
.status-filter {
padding: 0 0rpx;
}
.status-tabs {
margin-top: 12rpx;
width: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12rpx;
}
.status-tab {
display: inline-flex;
align-items: center;
justify-content: center;
height: 64rpx;
padding: 0 28rpx;
border-radius: 32rpx;
background: #f3f5f8;
color: #5f6b7a;
font-size: 26rpx;
border: 1rpx solid transparent;
white-space: nowrap;
flex-shrink: 0;
}
.status-tab.active {
background: #e6f0ff;
color: #1755ff;
border-color: #d6e4ff;
box-shadow: 0 4rpx 12rpx rgba(23, 85, 255, 0.12);
}
.state-hint {
min-height: 40vh;
padding: 60rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24rpx;
color: #555;
}
.state-title {
font-size: 30rpx;
font-weight: 500;
text-align: center;
line-height: 1.6;
}
.state-btn {
width: 280rpx;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
border-radius: 8rpx;
font-size: 30rpx;
border: none;
}
.state-btn::after {
border: none;
}
.state-img-wrapper {
cursor: pointer;
transition: all 0.3s ease;
display: inline-block;
&:active {
transform: scale(0.95);
opacity: 0.8;
}
}
.state-img {
width: 260rpx;
height: 200rpx;
object-fit: contain;
display: block;
}
.list-item {
margin-bottom: 24rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
&:active {
transform: translateY(1rpx);
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
}
}
// 订单头部
.order-header {
display: flex;
align-items: center;
gap: 16rpx;
padding: 24rpx 24rpx 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.order-number-section {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.order-number-label {
font-size: 26rpx;
color: #666;
margin-right: 8rpx;
}
.order-number-text {
font-size: 28rpx;
color: #333;
font-weight: 600;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.copy-btn {
margin-left: 16rpx;
padding: 4rpx 12rpx;
background: #f5f5f5;
border-radius: 4rpx;
cursor: pointer;
}
.copy-text {
font-size: 22rpx;
color: #1890ff;
}
.order-status-badge {
padding: 6rpx 16rpx;
border-radius: 4rpx;
font-size: 24rpx;
font-weight: 500;
white-space: nowrap;
}
// 订单内容
.order-content {
display: flex;
padding: 20rpx 24rpx;
gap: 20rpx;
}
.order-image-wrapper {
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f5f5f5;
flex-shrink: 0;
}
.order-image {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.order-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 16rpx;
}
// 车辆信息
.vehicle-details {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.detail-row {
display: flex;
align-items: center;
font-size: 26rpx;
}
.detail-label {
color: #999;
margin-right: 8rpx;
width: 120rpx;
flex-shrink: 0;
text-align: left;
}
.detail-value {
color: #333;
font-weight: 500;
flex: 1;
}
.phone-row-clickable {
cursor: pointer;
}
.phone-row-disabled {
cursor: default;
}
.phone-value {
color: #1890ff;
}
.phone-value-unknown,
.phone-row-disabled .phone-value {
color: #999;
}
.phone-icon {
margin-left: 8rpx;
font-size: 28rpx;
}
// 时间信息
.time-details {
display: flex;
flex-direction: column;
gap: 8rpx;
padding-top: 12rpx;
border-top: 1rpx solid #f5f5f5;
}
.time-detail-item {
display: flex;
align-items: center;
font-size: 24rpx;
}
.time-detail-label {
color: #999;
margin-right: 8rpx;
}
.time-detail-value {
color: #666;
}
.item-title {
font-size: 36rpx;
font-weight: 800;
color: #0f172a;
line-height: 1.45;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-desc {
font-size: 28rpx;
color: #4a5568;
line-height: 1.6;
margin-bottom: 18rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-order-num {
display: flex;
align-items: center;
margin-bottom: 12rpx;
padding: 8rpx 0;
gap: 8rpx;
}
.order-num-label {
font-size: 24rpx;
color: #5f6b7a;
flex-shrink: 0;
}
.order-num-text {
font-size: 30rpx;
color: #1f2937;
font-weight: 600;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-num-copy {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
cursor: pointer;
border-radius: 4rpx;
transition: background-color 0.2s;
padding: 4rpx 8rpx;
margin-left: 8rpx;
}
.copy-icon {
font-size: 24rpx;
color: #1765ad;
padding: 4rpx 10rpx;
border: 1rpx solid #d0e2ff;
border-radius: 6rpx;
background-color: #eff6ff;
}
.order-num-copy:active {
background-color: rgba(0, 0, 0, 0.06);
}
.order-num-copy:active .copy-icon {
background-color: rgba(24, 144, 255, 0.2);
}
.item-order-line {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
margin-bottom: 10rpx;
padding: 0 12rpx;
}
.item-order-line .item-title {
display: flex;
align-items: center;
gap: 8rpx;
}
.order-actions {
display: inline-flex;
align-items: center;
gap: 10rpx;
}
.status-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
padding: 0 12rpx;
}
// 状态标签样式
.status-0 {
background: #fff7e6;
color: #d48806;
}
.status-1 {
background: #e6f7ff;
color: #096dd9;
}
.status-2 {
background: #f6ffed;
color: #389e0d;
}
.status-3 {
background: #fff7e6;
color: #d46b08;
}
.status-unknown {
background: #f5f5f5;
color: #999;
}
.time-row {
display: flex;
justify-content: space-between;
gap: 16rpx;
margin-top: 4rpx;
padding: 0 12rpx;
}
.time-item {
flex: 1;
padding: 12rpx 14rpx;
background: #f7f9fc;
border-radius: 10rpx;
}
.time-label {
display: block;
font-size: 22rpx;
color: #6b7280;
margin-bottom: 6rpx;
}
.time-text {
font-size: 26rpx;
color: #1f2937;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vehicle-info {
margin: 10rpx 0 8rpx;
padding: 12rpx 18rpx;
background: #fcfdff;
border-radius: 14rpx;
color: #1f2a44;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx 20rpx;
}
.vehicle-left {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12rpx 20rpx;
}
.vehicle-right {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12rpx;
}
.vehicle-field {
display: inline-flex;
align-items: center;
gap: 10rpx;
}
.vehicle-label {
padding: 4rpx 10rpx;
border-radius: 10rpx;
background: rgba(31, 42, 68, 0.08);
font-size: 22rpx;
color: #1f2a44;
}
.vehicle-value {
font-size: 28rpx;
font-weight: 700;
color: #0f172a;
}
.vehicle-phone {
background: rgba(23, 85, 255, 0.12);
padding: 8rpx 14rpx;
border-radius: 14rpx;
cursor: pointer;
border: 1rpx solid rgba(23, 85, 255, 0.18);
}
.phone-emoji {
font-size: 30rpx;
line-height: 1;
color: #1755ff;
}
.item-actions {
margin-left: 16rpx;
flex-shrink: 0;
display: flex;
align-items: center;
}
// 操作按钮区域(恢复旧版“去质检”样式)
.item-footer {
display: flex;
flex-direction: column;
gap: 12rpx;
padding: 20rpx 24rpx;
background: #fafbfc;
border-top: 1rpx solid #eef0f3;
}
.footer-divider {
height: 1rpx;
background: linear-gradient(90deg, transparent 0%, #e6e9ef 20%, #e6e9ef 80%, transparent 100%);
}
/* footer-actions 由布局继承,无需额外样式,这里移除空规则避免告警 */
.check-btn {
min-width: 140rpx;
height: 64rpx;
line-height: 64rpx;
padding: 0 24rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 600;
border: none;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
transition: all 0.2s ease;
}
.check-btn::after {
border: none;
}
.check-btn:active {
transform: translateY(1rpx);
box-shadow: 0 2rpx 8rpx rgba(24, 144, 255, 0.25);
}
.item-meta {
display: flex;
align-items: center;
gap: 24rpx;
}
.meta-item {
display: flex;
align-items: center;
gap: 8rpx;
flex: 1;
min-width: 0;
}
.meta-text {
font-size: 24rpx;
color: #6b7280;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.item-arrow {
margin-left: 16rpx;
flex-shrink: 0;
}
// 旧的 footer 样式已移除,使用新的 order-actions
.loading-more,
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 0;
gap: 12rpx;
}
.loading-text,
.no-more-text {
font-size: 26rpx;
color: #999;
}
.no-more-text {
color: #ccc;
}
/* 图片预览弹窗样式 */
.image-popup {
width: 600rpx;
max-width: 90vw;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.image-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.image-popup-title {
font-size: 32rpx;
font-weight: 600;
color: #1a1a1a;
}
.image-popup-close {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.image-swiper {
width: 100%;
height: 500rpx;
}
.preview-image {
width: 100%;
height: 100%;
display: block;
}
/* 表单弹窗样式 */
.form-popup {
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.form-scroll {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.form-popup-header {
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
text-align: center;
}
.form-popup-title {
font-size: 36rpx;
font-weight: 600;
color: #1a1a1a;
}
.custom-form {
padding: 30rpx;
}
.form-item {
margin-bottom: 30rpx;
}
.form-label {
display: flex;
align-items: center;
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
font-weight: 500;
}
.form-label-row {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
font-weight: 500;
}
.required {
color: #ff4d4f;
margin-left: 4rpx;
}
.required-star {
color: #ff4d4f;
margin-right: 4rpx;
font-size: 28rpx;
}
.form-input {
width: 100%;
height: 80rpx;
padding: 0 24rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
.form-picker-wrapper {
width: 100%;
height: 80rpx;
padding: 0 24rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.form-picker-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #333;
background-color: transparent;
border: none;
padding: 0;
}
.form-picker-icon {
flex-shrink: 0;
margin-left: 12rpx;
}
.form-picker-wrapper.disabled {
color: #999;
background-color: #f0f0f0;
}
.form-picker-wrapper.disabled .form-picker-input {
color: #999;
}
.form-select-wrapper {
width: 100%;
position: relative;
}
.form-select-display {
width: 100%;
height: 80rpx;
padding: 0 24rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.form-select-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
.form-select-wrapper.disabled .form-select-display {
color: #999;
background-color: #f0f0f0;
cursor: not-allowed;
}
.form-select-wrapper.disabled .form-select-text {
color: #999;
}
.form-select-options {
position: absolute;
top: 88rpx;
left: 0;
right: 0;
background-color: #fff;
border-radius: 8rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
max-height: 400rpx;
overflow-y: auto;
z-index: 1000;
border: 1rpx solid #e8e8e8;
}
.form-select-option {
padding: 24rpx;
font-size: 28rpx;
color: #333;
border-bottom: 1rpx solid #f0f0f0;
cursor: pointer;
}
.form-select-option:last-child {
border-bottom: none;
}
.form-select-option:active {
background-color: #f5f5f5;
}
.form-select-option.active {
color: #1890ff;
background-color: #e6f7ff;
}
.form-textarea {
width: 100%;
min-height: 160rpx;
padding: 20rpx 24rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
line-height: 1.5;
}
.uploader-container {
width: 100%;
}
.form-actions-placeholder {
height: 120rpx;
}
.form-actions-fixed {
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #fff;
border-top: 1rpx solid #f0f0f0;
display: flex;
gap: 20rpx;
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.form-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 8rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
padding: 0;
margin: 0;
}
.form-btn-cancel {
background-color: #f5f5f5;
color: #333;
}
.form-btn-cancel::after {
border: none;
}
.form-btn-submit {
background-color: #1890ff;
color: #fff;
}
.form-btn-submit::after {
border: none;
}
.form-btn:active {
opacity: 0.8;
}
/* 登录弹窗样式 */
.login-popup {
width: 560rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.login-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.login-title {
font-size: 36rpx;
font-weight: 600;
color: #1a1a1a;
}
.login-close {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.login-content {
padding: 40rpx 30rpx;
text-align: center;
}
.login-desc {
font-size: 28rpx;
color: #666;
margin-bottom: 40rpx;
display: block;
}
.login-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
border-radius: 8rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
}
.login-btn::after {
border: none;
}
.login-btn:active {
opacity: 0.8;
}
/* 绑定弹窗样式 */
.bind-popup {
width: 640rpx;
background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%);
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
}
.bind-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 40rpx 40rpx 30rpx;
background: linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%);
border-bottom: 1rpx solid rgba(24, 144, 255, 0.1);
}
.bind-title-wrapper {
display: flex;
align-items: center;
gap: 12rpx;
}
.bind-title {
font-size: 36rpx;
font-weight: 600;
color: #1890ff;
letter-spacing: 0.5rpx;
}
.bind-close {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.04);
transition: all 0.3s;
}
.bind-close:active {
background-color: rgba(0, 0, 0, 0.08);
transform: scale(0.95);
}
.bind-content {
padding: 50rpx 40rpx 40rpx;
}
.bind-desc {
font-size: 26rpx;
color: #8c8c8c;
margin-bottom: 50rpx;
display: block;
text-align: center;
line-height: 1.6;
}
.bind-form {
margin-bottom: 50rpx;
}
.bind-form-item {
margin-bottom: 36rpx;
}
.form-item-label {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 16rpx;
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.input-wrapper {
position: relative;
background-color: #f8f9fa;
border-radius: 12rpx;
border: 2rpx solid #e8e8e8;
transition: all 0.3s;
}
.input-wrapper:focus-within {
border-color: #1890ff;
background-color: #fff;
box-shadow: 0 0 0 4rpx rgba(24, 144, 255, 0.1);
}
.bind-code-item {
display: flex;
gap: 16rpx;
align-items: flex-start;
}
.code-input-wrapper {
flex: 1;
}
.bind-input {
width: 100%;
height: 96rpx;
padding: 0 28rpx;
background-color: transparent;
border: none;
border-radius: 12rpx;
font-size: 30rpx;
color: #333;
box-sizing: border-box;
}
.bind-input::placeholder {
color: #bfbfbf;
font-size: 28rpx;
}
.bind-code-input {
flex: 1;
}
.bind-code-btn {
width: 200rpx;
height: 96rpx;
line-height: 96rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
border-radius: 12rpx;
font-size: 26rpx;
font-weight: 500;
border: none;
padding: 0;
margin: 0;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
transition: all 0.3s;
}
.bind-code-btn::after {
border: none;
}
.bind-code-btn:disabled {
background: #d9d9d9;
color: #fff;
box-shadow: none;
}
.bind-code-btn:active:not(:disabled) {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(24, 144, 255, 0.3);
}
.bind-actions {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.bind-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
border-radius: 48rpx;
font-size: 32rpx;
font-weight: 600;
border: none;
padding: 0;
margin: 0;
transition: all 0.3s;
letter-spacing: 1rpx;
}
.bind-btn-submit {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
box-shadow: 0 8rpx 20rpx rgba(24, 144, 255, 0.4);
order: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.bind-btn-submit::after {
border: none;
}
.bind-btn-submit:disabled {
opacity: 0.6;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.2);
}
.bind-btn-submit:active:not(:disabled) {
transform: translateY(2rpx);
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.4);
}
.bind-btn-cancel {
background-color: #f5f5f5;
color: #8c8c8c;
order: 2;
box-shadow: none;
}
.bind-btn-cancel::after {
border: none;
}
.bind-btn-cancel:disabled {
opacity: 0.6;
}
.bind-btn-cancel:active:not(:disabled) {
background-color: #e8e8e8;
transform: scale(0.98);
}
</style>