feat(订单): 添加借还动态功能

- 新增订单API模块,包含借还动态查询接口和类型定义
- 在商品详情页添加借还动态标签页,展示相关记录
- 实现分页加载和图片预览功能
- 更新环境配置,注释掉不再使用的API地址
- 移除过期的文档链接
- 修复商品详情页高度问题,适配动态列表展示
This commit is contained in:
dzq 2025-11-29 11:29:24 +08:00
parent db6f31228d
commit cdc59df3db
8 changed files with 509 additions and 80 deletions

View File

@ -132,8 +132,6 @@ http.post<T>('/api/users', data)
## Documentation
- **Standards**: `/doc/代码编写规范.md`
- **Cleanup**: `/doc/项目清理计划.md`
- **Migration**: `/doc/迁移指令.md`
- **wot-design-uni**: UI component library documentation located in `/doc/wot-design-uni/docs/`
- View `index.md` for main documentation
- Browse `guide/` for usage guides

2
env/.env vendored
View File

@ -22,7 +22,7 @@ VITE_APP_PROXY_ENABLE = true
VITE_APP_PROXY_PREFIX = '/api/'
# 第二个请求地址 (目前alova中可以使用)
VITE_API_SECONDARY_URL = 'https://wxshop.ab98.cn/shop-api/api/'
# VITE_API_SECONDARY_URL = 'https://wxshop.ab98.cn/shop-api/api/'
# 认证模式,'single' | 'double' ==> 单token | 双token
VITE_AUTH_MODE = 'single'

View File

@ -11,4 +11,4 @@ VITE_SHOW_SOURCEMAP = false
#VITE_HUIBANG_BASEURL = 'https://www.ab98.cn'
# VITE_SERVER_BASEURL = 'http://localhost:8090/api/'
VITE_SERVER_BASEURL = 'https://wxshop.ab98.cn/shop-api/api/'
# VITE_SERVER_BASEURL = 'https://wxshop.ab98.cn/shop-api/api/'

4
env/.env.production vendored
View File

@ -6,5 +6,5 @@ VITE_DELETE_CONSOLE = false
VITE_SHOW_SOURCEMAP = false
# 后台请求地址
VITE_SERVER_BASEURL = 'https://wxshop.ab98.cn/shop-api/api/'
VITE_HUIBANG_BASEURL = 'https://www.ab98.cn'
# VITE_SERVER_BASEURL = 'https://wxshop.ab98.cn/shop-api/api/'
VITE_HUIBANG_BASEURL = 'https://www.ab98.cn'

View File

@ -22,6 +22,10 @@ export interface PageResult<T> {
count: number;
}
export interface PageDTO<T> {
total: number;
rows: Array<T>;
};
/**
*
*/

75
src/api/order/index.ts Normal file
View File

@ -0,0 +1,75 @@
import { http } from "@/http/http";
import { PageDTO } from "..";
/** 借还动态查询参数 */
export interface SearchBorrowReturnDynamicQuery {
/** 商品ID精确筛选 */
goodsId?: number;
/** 格口ID精确筛选 */
cellId?: number;
/** 状态筛选(仅对归还记录有效) */
status?: number;
/** 动态类型筛选 */
dynamicType?: number;
/** 页码默认1 */
pageNum?: number;
/** 每页大小默认10 */
pageSize?: number;
}
/** 借还动态响应数据 */
export interface BorrowReturnDynamicDTO {
/** 订单商品ID */
orderGoodsId: number;
/** 动态类型0-借出 1-归还) */
dynamicType: number;
/** 动态类型描述 */
dynamicTypeStr: string;
/** 订单ID */
orderId: number;
/** 订单创建时间/借出时间 */
orderTime: string;
/** 商品ID */
goodsId: number;
/** 商品名称 */
goodsName: string;
/** 商品单价 */
goodsPrice: number;
/** 数量 */
quantity: number;
/** 支付方式 */
paymentMethod: string;
/** 订单姓名 */
orderName: string;
/** 订单手机号 */
orderMobile: string;
/** 格口ID */
cellId: number;
/** 格口号 */
cellNo: number;
/** 柜机ID */
cabinetId: number;
/** 柜机名称 */
cabinetName: string;
/** 归还/审批时间(归还记录时有效) */
operateTime?: string;
/** 审批ID归还记录时有效 */
approvalId?: number;
/** 审批状态(归还记录时有效) */
status: number;
/** 状态描述 */
statusStr: string;
/** 审批人(归还记录时有效) */
auditName?: string;
/** 审核说明(归还记录时有效) */
auditRemark?: string;
/** 归还图片(归还记录时有效) */
images?: string;
/** 审核图片(归还记录时有效) */
auditImages?: string;
}
/** 获取借还动态分页列表 */
export async function getBorrowReturnDynamicApi(query: SearchBorrowReturnDynamicQuery) {
return await http.get<PageDTO<BorrowReturnDynamicDTO>>("order/borrow-return-dynamic", query);
}

View File

@ -3,6 +3,9 @@ import { ref, computed, watch } from 'vue'
import type { Product } from '@/pinia/stores/product'
import { useCartStore } from '@/pinia/stores/cart'
import { storeToRefs } from 'pinia'
import { toHttpsUrl } from '@/utils'
// import { onLoad, onReachBottom, onShow } from '@dcloudio/uni-app'
import { getBorrowReturnDynamicApi, type BorrowReturnDynamicDTO, type SearchBorrowReturnDynamicQuery } from '@/api/order'
//
const props = defineProps<{
@ -22,6 +25,30 @@ const { cartItems } = storeToRefs(cartStore)
const showAddCart = ref<boolean>(false)
const quantity = ref<number>(1)
//
const activeTab = ref<number>(0)
//
const dynamicList = ref<BorrowReturnDynamicDTO[]>([]);
const total = ref<number>(-1);
const loading = ref<boolean>(false)
//
const pageNum = ref<number>(1)
const pageSize = ref<number>(20)
// loadmore
const loadmoreState = ref<'loading' | 'finished' | 'error'>('loading')
// scroll-view
const scrollHeight = computed(() => {
// - - -
const windowHeight = uni.getSystemInfoSync().windowHeight
const headerHeight = 60 //
const actionBarHeight = 64 //
const tabHeight = 44 //
console.log('scrollHeight', windowHeight, headerHeight, actionBarHeight, tabHeight)
return `${windowHeight - headerHeight - actionBarHeight - tabHeight}px`
})
const maxQuantity = computed(() => {
const existingItem = cartItems.value.find(item => item.product.id === props.product.id)
if (existingItem) {
@ -74,11 +101,96 @@ function doShowAddCart(): boolean {
return true
}
//
async function loadDynamicList(page: number = 1) {
console.log('loadDynamicList', page)
console.log('props.product', props.product)
if (!props.product?.id) return
loading.value = true
loadmoreState.value = 'loading'
try {
const query: SearchBorrowReturnDynamicQuery = {
goodsId: props.product.id,
cellId: props.product.cellId,
pageNum: page,
pageSize: pageSize.value
}
const response = await getBorrowReturnDynamicApi(query)
console.log('response', response)
if (response?.data?.rows) {
if (page === 1) {
//
dynamicList.value = response.data.rows
} else {
//
dynamicList.value = [...dynamicList.value, ...response.data.rows]
}
total.value = response.data.total
pageNum.value = page
//
console.log('dynamicList.value.length', dynamicList.value.length, 'total.value', total.value)
if (dynamicList.value.length >= total.value) {
loadmoreState.value = 'finished'
} else {
loadmoreState.value = 'loading'
}
}
} catch (error) {
console.error('获取借还动态失败:', error)
loadmoreState.value = 'error'
} finally {
loading.value = false
}
}
//
function loadMoreData() {
console.log('loadMoreData', pageNum.value, loadmoreState.value, loading.value, dynamicList.value.length, total.value)
if (loadmoreState.value === 'finished' || loading.value) return
if (dynamicList.value.length >= total.value) {
loadmoreState.value = 'finished'
return
}
loadDynamicList(pageNum.value + 1)
}
// scroll-view
function handleScrollToLower() {
console.log('handleScrollToLower', pageNum.value, loadmoreState.value, loading.value, dynamicList.value.length, total.value)
loadMoreData()
}
//
watch(activeTab, (newVal) => {
if (newVal === 1 && dynamicList.value.length === 0) {
pageNum.value = 1
loadDynamicList(1)
}
})
const refreshDynamicList = () => {
dynamicList.value = [];
pageNum.value = 1;
loadDynamicList(1);
}
onShow(() => {
refreshDynamicList();
})
watch(() => props.product, () => {
//
quantity.value = 1
showAddCart.value = false
}, { deep: true })
activeTab.value = 0;
quantity.value = 1;
showAddCart.value = false;
refreshDynamicList();
}, { deep: true, immediate: true })
</script>
<template>
@ -92,85 +204,152 @@ watch(() => props.product, () => {
</view>
<!-- 商品内容区域可滚动 -->
<scroll-view class="content-area" scroll-y>
<!-- 商品主图 -->
<view class="product-image-wrapper">
<wd-img
v-if="product"
:src="product.image"
width="100%"
height="275"
>
<view v-if="product.stock === 0" class="sold-out-overlay">
<text class="sold-out-text">已售罄</text>
</view>
<template #error>
<view class="custom-error">
图片加载失败
<view class="content-area">
<!-- 标签页内容 -->
<view class="tab-content">
<wd-tabs v-model="activeTab">
<!-- 标签1商品信息 -->
<wd-tab title="商品信息">
<!-- 商品主图 -->
<view class="product-image-wrapper">
<wd-img v-if="product" :src="toHttpsUrl(product.image)" width="100%" height="275">
<view v-if="product.stock === 0" class="sold-out-overlay">
<text class="sold-out-text">已售罄</text>
</view>
<template #error>
<view class="custom-error">
图片加载失败
</view>
</template>
</wd-img>
</view>
</template>
</wd-img>
<!-- 分隔线 -->
<view class="divider"></view>
<!-- 商品信息 -->
<view class="product-info">
<view class="product-name">
{{ product.name }}
</view>
<view class="price-row">
<text class="product-price">¥{{ product.price.toFixed(2) }}</text>
<text v-if="product.stock > 0" class="stock-count">
剩余{{ product.stock }}
</text>
</view>
</view>
<view class="description">
{{ product.description }}
</view>
</wd-tab>
<!-- 标签2借还动态 -->
<wd-tab title="借还动态">
<!-- 动态列表 -->
<scroll-view class="dynamic-list-scroll" scroll-y :style="{ height: scrollHeight }"
@scrolltolower="handleScrollToLower" :lower-threshold="100">
<view class="dynamic-list">
<view v-for="item in dynamicList" :key="String(item.orderGoodsId) + item.dynamicType"
class="dynamic-item">
<view class="dynamic-header">
<view class="dynamic-type"
:class="{ borrow: item.dynamicType === 0, return: item.dynamicType === 1 }">
{{ item.dynamicTypeStr }}
</view>
<!-- 归还记录显示额外信息 -->
<view v-if="item.dynamicType === 1" class="info-row">
<text class="info-value" :class="item.status === 2 ? 'success' : 'warning'">{{ item.statusStr
}}</text>
</view>
<text class="dynamic-time">{{ item.orderTime }}</text>
</view>
<view class="dynamic-body">
<view class="info-row">
<text class="info-label">借用人</text>
<text class="info-value">{{ item.orderName }}</text>
<text v-if="item.dynamicType === 1 && item.auditName" class="info-label audit-name">审批人</text>
<text v-if="item.dynamicType === 1 && item.auditName" class="info-value">{{ item.auditName
}}</text>
</view>
<view v-if="item.dynamicType === 1 && item.auditRemark && item.auditRemark != '自动审批'"
class="info-row">
<text class="info-label">审核说明</text>
<text class="info-value">{{ item.auditRemark }}</text>
</view>
<!-- 归还图片 -->
<view v-if="item.dynamicType === 1 && item.images" class="image-section">
<view class="info-label">归还图片</view>
<view class="image-list">
<view v-for="(image, index) in item.images.split(',')" :key="index" class="image-item">
<wd-img :src="toHttpsUrl(image.trim())" width="80" height="80" mode="aspectFill"
:enable-preview="true">
<template #error>
<view class="image-error">图片加载失败</view>
</template>
</wd-img>
</view>
</view>
</view>
<!-- 审核图片 -->
<view v-if="item.dynamicType === 1 && item.auditImages" class="image-section">
<view class="info-label">审核图片</view>
<view class="image-list">
<view v-for="(image, index) in item.auditImages.split(',')" :key="index" class="image-item">
<wd-img :src="toHttpsUrl(image.trim())" width="80" height="80" mode="aspectFill"
:enable-preview="true">
<template #error>
<view class="image-error">图片加载失败</view>
</template>
</wd-img>
</view>
</view>
</view>
</view>
</view>
<!-- 加载更多状态 -->
<view v-if="dynamicList.length > 0" class="loadmore-status">
<view v-if="loadmoreState === 'loading'" class="loading-text">
<wd-loading size="16px" color="#969799"></wd-loading>
<text>加载中...</text>
</view>
<view v-else-if="loadmoreState === 'finished'" class="finished-text">
<text>没有更多数据了</text>
</view>
<view v-else-if="loadmoreState === 'error'" class="error-text" @click="loadMoreData">
<text>加载失败点击重试</text>
</view>
</view>
</view>
</scroll-view>
</wd-tab>
</wd-tabs>
</view>
<!-- 商品信息 -->
<view class="product-info">
<view class="product-name">
{{ product.name }}
</view>
<view class="price-row">
<text class="product-price">¥{{ product.price.toFixed(2) }}</text>
<text v-if="product.stock > 0" class="stock-count">
剩余{{ product.stock }}
</text>
</view>
</view>
<!-- 分隔线 -->
<view class="divider"></view>
<!-- 商品详细描述 -->
<view v-if="!showAddCart" class="description">
{{ product.description }}
</view>
<!-- 购买数量选择 -->
<view v-if="showAddCart" class="action-row">
<view class="product-name">
购买数量
</view>
<view class="cart-actions">
<view
class="cart-btn-minus"
:class="{ disabled: quantity === 1 }"
@click.stop="handleRemoveFromCart()"
>
<view class="cart-btn-minus" :class="{ disabled: quantity === 1 }" @click.stop="handleRemoveFromCart()">
<wd-icon name="decrease" size="12px" :color="quantity === 1 ? '#fff' : '#F56C6C'"></wd-icon>
</view>
<text class="cart-count">{{ quantity }}</text>
<view
class="cart-btn-plus"
:class="{ disabled: maxQuantity }"
@click.stop="handleAddToCart()"
>
<view class="cart-btn-plus" :class="{ disabled: maxQuantity }" @click.stop="handleAddToCart()">
<wd-icon name="add" size="12px" color="#fff"></wd-icon>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 底部操作按钮栏 -->
<view class="action-bar">
<view
v-if="!showAddCart"
class="add-cart-btn"
:class="{ disabled: product?.stock === 0 }"
@click="doShowAddCart()"
>
<view v-if="activeTab === 0" class="action-bar">
<view v-if="!showAddCart" class="add-cart-btn" :class="{ disabled: product?.stock === 0 }"
@click="doShowAddCart()">
{{ product?.stock === 0 ? '已售罄' : '加入购物车' }}
</view>
<view
v-if="showAddCart"
class="add-cart-btn confirm"
@click="confirmAddCart()"
>
<view v-if="showAddCart" class="add-cart-btn confirm" @click="confirmAddCart()">
确认
</view>
</view>
@ -181,7 +360,7 @@ watch(() => props.product, () => {
.detail-container {
display: flex;
flex-direction: column;
height: 80vh;
height: 100%;
background: #f7f8fa;
}
@ -369,4 +548,176 @@ watch(() => props.product, () => {
background-color: #07c160;
}
}
//
.tab-content {
background: #fff;
}
.dynamic-content {
min-height: 200px;
}
.loading-state,
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 16px;
}
.empty-text {
color: #969799;
font-size: 14px;
}
// scroll-view
.dynamic-list-scroll {
width: 100%;
}
.dynamic-list {
min-height: 600rpx;
padding: 16px;
}
//
.loadmore-status {
display: flex;
justify-content: center;
align-items: center;
padding: 16px 0;
.loading-text {
display: flex;
align-items: center;
gap: 8px;
color: #969799;
font-size: 14px;
}
.finished-text {
color: #969799;
font-size: 14px;
}
.error-text {
color: #F56C6C;
font-size: 14px;
cursor: pointer;
&:active {
opacity: 0.7;
}
}
}
.dynamic-item {
margin-bottom: 12px;
padding: 12px;
background: #f7f8fa;
border-radius: 8px;
&:last-child {
margin-bottom: 0;
}
}
.dynamic-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.dynamic-type {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.borrow {
background: #EBF5FF;
color: #1989FA;
}
&.return {
background: #F0F9EB;
color: #52C41A;
}
}
.dynamic-time {
font-size: 12px;
color: #969799;
}
.dynamic-body {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-row {
display: flex;
align-items: center;
font-size: 13px;
}
.info-label {
color: #646566;
margin-right: 4px;
flex-shrink: 0;
}
.info-value {
color: #323233;
&.success {
color: #52C41A;
}
&.warning {
color: #FF976A;
}
}
.audit-name {
margin-left: 50rpx;
}
.image-section {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 8px;
.info-label {
color: #646566;
font-size: 13px;
}
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.image-item {
position: relative;
border-radius: 4px;
overflow: hidden;
}
.image-error {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
background: #f7f8fa;
color: #969799;
font-size: 12px;
}
</style>

View File

@ -5,6 +5,7 @@ import { useCartStore } from '@/pinia/stores/cart'
import { storeToRefs } from 'pinia'
import Detail from './detail.vue'
import Cart from './cart.vue'
import { toHttpsUrl } from '@/utils'
definePage({
style: {
@ -30,10 +31,10 @@ const activeCategory = ref<number>(0)
//
const showDetailPopup = ref<boolean>(false)
// ID
const currentProductId = ref<number>()
const currentCellId = ref<number>()
//
const currentProduct = computed(() =>
categories.value.find(p => p.id === currentProductId.value)
categories.value.find(p => p.cellId === currentCellId.value)
)
//
const showCartPopup = ref<boolean>(false)
@ -50,10 +51,10 @@ function handleCategoryClick(index: number) {
/**
* 打开商品详情弹窗
* @param productId - 要显示详情的商品ID
* @param cellId - 要显示详情的格口ID
*/
function showProductDetail(productId: number) {
currentProductId.value = productId
function showProductDetail(cellId: number) {
currentCellId.value = cellId
showDetailPopup.value = true
}
@ -136,9 +137,9 @@ function handleCheckout() {
<view class="category-section">
<view v-for="product in currentProducts" :key="product.cellId" class="product-item">
<view class="product-content">
<view class="product-image-wrapper" @click.stop="showProductDetail(product.id)">
<view class="product-image-wrapper" @click.stop="showProductDetail(product.cellId)">
<wd-img
:src="product.image"
:src="toHttpsUrl(product.image)"
width="80"
height="80"
border-radius="4"
@ -153,10 +154,10 @@ function handleCheckout() {
</view>
<view class="product-info">
<view class="product-name" @click.stop="showProductDetail(product.id)">
<view class="product-name" @click.stop="showProductDetail(product.cellId)">
{{ product.name }}
</view>
<view class="product-price" @click.stop="showProductDetail(product.id)">
<view class="product-price" @click.stop="showProductDetail(product.cellId)">
¥{{ product.price.toFixed(2) }}
</view>
<view class="action-row">
@ -442,6 +443,6 @@ function handleCheckout() {
}
.detail-container {
height: 80vh;
height: 92vh;
}
</style>