Browse Source
- 新增 PaymentCountdown 组件用于显示支付倒计时 - 实现 usePaymentCountdown Hook 以支持倒计时逻辑 - 添加 useOrderStats Hook 用于获取订单统计信息 - 在订单列表和详情页面集成支付倒计时功能 - 优化订单状态显示和相关操作逻辑dev
8 changed files with 833 additions and 50 deletions
@ -0,0 +1,168 @@ |
|||
# PaymentCountdown 支付倒计时组件 |
|||
|
|||
基于订单创建时间的支付倒计时组件,支持静态显示和实时更新两种模式。 |
|||
|
|||
## 功能特性 |
|||
|
|||
- ✅ **双模式支持**:静态显示(列表页)和实时更新(详情页) |
|||
- ✅ **智能状态判断**:自动判断紧急程度并应用不同样式 |
|||
- ✅ **过期自动处理**:倒计时结束后触发回调 |
|||
- ✅ **灵活样式**:支持徽章模式和纯文本模式 |
|||
- ✅ **性能优化**:避免不必要的重渲染 |
|||
|
|||
## 使用方法 |
|||
|
|||
### 基础用法 |
|||
|
|||
```tsx |
|||
import PaymentCountdown from '@/components/PaymentCountdown'; |
|||
|
|||
// 订单列表页 - 静态显示 |
|||
<PaymentCountdown |
|||
createTime={order.createTime} |
|||
payStatus={order.payStatus} |
|||
realTime={false} |
|||
mode="badge" |
|||
/> |
|||
|
|||
// 订单详情页 - 实时更新 |
|||
<PaymentCountdown |
|||
createTime={order.createTime} |
|||
payStatus={order.payStatus} |
|||
realTime={true} |
|||
showSeconds={true} |
|||
mode="badge" |
|||
onExpired={() => { |
|||
console.log('支付已过期'); |
|||
}} |
|||
/> |
|||
``` |
|||
|
|||
### 高级用法 |
|||
|
|||
```tsx |
|||
// 自定义超时时间(12小时) |
|||
<PaymentCountdown |
|||
createTime={order.createTime} |
|||
payStatus={order.payStatus} |
|||
realTime={true} |
|||
timeoutHours={12} |
|||
showSeconds={true} |
|||
mode="badge" |
|||
className="custom-countdown" |
|||
onExpired={handlePaymentExpired} |
|||
/> |
|||
|
|||
// 纯文本模式 |
|||
<PaymentCountdown |
|||
createTime={order.createTime} |
|||
payStatus={order.payStatus} |
|||
realTime={false} |
|||
mode="text" |
|||
/> |
|||
``` |
|||
|
|||
## API 参数 |
|||
|
|||
| 参数 | 类型 | 默认值 | 说明 | |
|||
|------|------|--------|------| |
|||
| createTime | string | - | 订单创建时间 | |
|||
| payStatus | boolean | false | 支付状态 | |
|||
| realTime | boolean | false | 是否实时更新 | |
|||
| timeoutHours | number | 24 | 超时小时数 | |
|||
| showSeconds | boolean | false | 是否显示秒数 | |
|||
| className | string | '' | 自定义样式类名 | |
|||
| onExpired | function | - | 过期回调函数 | |
|||
| mode | 'badge' \| 'text' | 'badge' | 显示模式 | |
|||
|
|||
## 样式状态 |
|||
|
|||
### 正常状态 |
|||
- 红色渐变背景 |
|||
- 白色文字 |
|||
- 轻微阴影效果 |
|||
|
|||
### 紧急状态(< 1小时) |
|||
- 更深的红色背景 |
|||
- 脉冲动画效果 |
|||
|
|||
### 非常紧急状态(< 10分钟) |
|||
- 最深的红色背景 |
|||
- 快速闪烁动画 |
|||
|
|||
### 过期状态 |
|||
- 灰色背景 |
|||
- 无动画效果 |
|||
|
|||
## Hook 使用 |
|||
|
|||
如果需要单独使用倒计时逻辑,可以直接使用 Hook: |
|||
|
|||
```tsx |
|||
import { usePaymentCountdown, formatCountdownText } from '@/hooks/usePaymentCountdown'; |
|||
|
|||
const MyComponent = ({ order }) => { |
|||
const timeLeft = usePaymentCountdown( |
|||
order.createTime, |
|||
order.payStatus, |
|||
true, // 实时更新 |
|||
24 // 24小时超时 |
|||
); |
|||
|
|||
const countdownText = formatCountdownText(timeLeft, true); |
|||
|
|||
return ( |
|||
<div> |
|||
剩余时间:{countdownText} |
|||
</div> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
## 工具函数 |
|||
|
|||
```tsx |
|||
import { |
|||
formatCountdownText, |
|||
isUrgentCountdown, |
|||
isCriticalCountdown |
|||
} from '@/hooks/usePaymentCountdown'; |
|||
|
|||
// 格式化倒计时文本 |
|||
const text = formatCountdownText(timeLeft, true); // "2小时30分15秒" |
|||
|
|||
// 判断是否紧急 |
|||
const isUrgent = isUrgentCountdown(timeLeft); // < 1小时 |
|||
|
|||
// 判断是否非常紧急 |
|||
const isCritical = isCriticalCountdown(timeLeft); // < 10分钟 |
|||
``` |
|||
|
|||
## 注意事项 |
|||
|
|||
1. **性能考虑**:列表页建议使用 `realTime={false}` 避免过多定时器 |
|||
2. **内存泄漏**:组件会自动清理定时器,无需手动处理 |
|||
3. **时区问题**:确保 `createTime` 格式正确,建议使用 ISO 格式 |
|||
4. **过期处理**:`onExpired` 回调只在实时模式下触发 |
|||
|
|||
## 样式定制 |
|||
|
|||
可以通过 CSS 变量或覆盖样式类来自定义外观: |
|||
|
|||
```scss |
|||
.custom-countdown { |
|||
.payment-countdown-badge { |
|||
background: linear-gradient(135deg, #your-color-1, #your-color-2); |
|||
border-radius: 8px; |
|||
|
|||
&.urgent { |
|||
animation: customPulse 1.5s infinite; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@keyframes customPulse { |
|||
0%, 100% { opacity: 1; } |
|||
50% { opacity: 0.8; } |
|||
} |
|||
``` |
@ -0,0 +1,170 @@ |
|||
/* 支付倒计时样式 */ |
|||
|
|||
/* 徽章模式样式 */ |
|||
.payment-countdown-badge { |
|||
display: inline-block; |
|||
background: linear-gradient(135deg, #ff4757, #ff3838); |
|||
color: white; |
|||
padding: 4px 8px; |
|||
border-radius: 12px; |
|||
font-size: 12px; |
|||
font-weight: 500; |
|||
box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3); |
|||
margin-left: 8px; |
|||
|
|||
.countdown-text { |
|||
color: white; |
|||
font-size: 12px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
/* 紧急状态(少于1小时) */ |
|||
&.urgent { |
|||
background: linear-gradient(135deg, #ff6b6b, #ee5a52); |
|||
animation: pulse 2s infinite; |
|||
} |
|||
|
|||
/* 非常紧急状态(少于10分钟) */ |
|||
&.critical { |
|||
background: linear-gradient(135deg, #ff4757, #c44569); |
|||
animation: flash 1s infinite; |
|||
} |
|||
|
|||
/* 过期状态 */ |
|||
&.expired { |
|||
background: linear-gradient(135deg, #95a5a6, #7f8c8d); |
|||
animation: none; |
|||
} |
|||
} |
|||
|
|||
/* 纯文本模式样式 */ |
|||
.payment-countdown-text { |
|||
color: #ff4757; |
|||
font-size: 12px; |
|||
font-weight: 500; |
|||
|
|||
/* 紧急状态 */ |
|||
&.urgent { |
|||
color: #ff6b6b; |
|||
animation: textPulse 2s infinite; |
|||
} |
|||
|
|||
/* 非常紧急状态 */ |
|||
&.critical { |
|||
color: #ff4757; |
|||
animation: textFlash 1s infinite; |
|||
} |
|||
|
|||
/* 过期状态 */ |
|||
&.expired { |
|||
color: #95a5a6; |
|||
animation: none; |
|||
} |
|||
} |
|||
|
|||
/* 动画效果 */ |
|||
@keyframes pulse { |
|||
0% { |
|||
opacity: 1; |
|||
transform: scale(1); |
|||
} |
|||
50% { |
|||
opacity: 0.8; |
|||
transform: scale(1.02); |
|||
} |
|||
100% { |
|||
opacity: 1; |
|||
transform: scale(1); |
|||
} |
|||
} |
|||
|
|||
@keyframes flash { |
|||
0% { |
|||
opacity: 1; |
|||
transform: scale(1); |
|||
} |
|||
25% { |
|||
opacity: 0.7; |
|||
transform: scale(1.05); |
|||
} |
|||
50% { |
|||
opacity: 1; |
|||
transform: scale(1); |
|||
} |
|||
75% { |
|||
opacity: 0.7; |
|||
transform: scale(1.05); |
|||
} |
|||
100% { |
|||
opacity: 1; |
|||
transform: scale(1); |
|||
} |
|||
} |
|||
|
|||
@keyframes textPulse { |
|||
0% { opacity: 1; } |
|||
50% { opacity: 0.7; } |
|||
100% { opacity: 1; } |
|||
} |
|||
|
|||
@keyframes textFlash { |
|||
0% { opacity: 1; } |
|||
25% { opacity: 0.5; } |
|||
50% { opacity: 1; } |
|||
75% { opacity: 0.5; } |
|||
100% { opacity: 1; } |
|||
} |
|||
|
|||
/* 响应式调整 */ |
|||
@media (max-width: 375px) { |
|||
.payment-countdown-badge { |
|||
font-size: 11px; |
|||
padding: 3px 6px; |
|||
|
|||
.countdown-text { |
|||
font-size: 11px; |
|||
} |
|||
} |
|||
|
|||
.payment-countdown-text { |
|||
font-size: 11px; |
|||
} |
|||
} |
|||
|
|||
/* 详情页专用样式 */ |
|||
.order-detail-countdown { |
|||
.payment-countdown-badge { |
|||
font-size: 14px; |
|||
padding: 6px 12px; |
|||
border-radius: 16px; |
|||
margin: 8px 0; |
|||
|
|||
.countdown-text { |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
} |
|||
} |
|||
|
|||
.payment-countdown-text { |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
} |
|||
} |
|||
|
|||
/* 列表页专用样式 */ |
|||
.order-list-countdown { |
|||
.payment-countdown-badge { |
|||
font-size: 11px; |
|||
padding: 2px 6px; |
|||
border-radius: 10px; |
|||
margin-left: 6px; |
|||
|
|||
.countdown-text { |
|||
font-size: 11px; |
|||
} |
|||
} |
|||
|
|||
.payment-countdown-text { |
|||
font-size: 11px; |
|||
} |
|||
} |
@ -0,0 +1,89 @@ |
|||
import React from 'react'; |
|||
import { View, Text } from '@tarojs/components'; |
|||
import { |
|||
usePaymentCountdown, |
|||
formatCountdownText, |
|||
isUrgentCountdown, |
|||
isCriticalCountdown |
|||
} from '@/hooks/usePaymentCountdown'; |
|||
import './PaymentCountdown.scss'; |
|||
|
|||
export interface PaymentCountdownProps { |
|||
/** 订单创建时间 */ |
|||
createTime?: string; |
|||
/** 支付状态 */ |
|||
payStatus?: boolean; |
|||
/** 是否实时更新(详情页用true,列表页用false) */ |
|||
realTime?: boolean; |
|||
/** 超时小时数,默认24小时 */ |
|||
timeoutHours?: number; |
|||
/** 是否显示秒数 */ |
|||
showSeconds?: boolean; |
|||
/** 自定义样式类名 */ |
|||
className?: string; |
|||
/** 过期回调 */ |
|||
onExpired?: () => void; |
|||
/** 显示模式:badge(徽章模式) | text(纯文本模式) */ |
|||
mode?: 'badge' | 'text'; |
|||
} |
|||
|
|||
const PaymentCountdown: React.FC<PaymentCountdownProps> = ({ |
|||
createTime, |
|||
payStatus = false, |
|||
realTime = false, |
|||
timeoutHours = 24, |
|||
showSeconds = false, |
|||
className = '', |
|||
onExpired, |
|||
mode = 'badge' |
|||
}) => { |
|||
const timeLeft = usePaymentCountdown(createTime, payStatus, realTime, timeoutHours); |
|||
|
|||
// 如果已支付或没有创建时间,不显示倒计时
|
|||
if (payStatus || !createTime) { |
|||
return null; |
|||
} |
|||
|
|||
// 如果已过期,触发回调并显示过期状态
|
|||
if (timeLeft.isExpired) { |
|||
onExpired?.(); |
|||
if (mode === 'text') { |
|||
return ( |
|||
<Text className={`payment-countdown-text expired ${className}`}> |
|||
支付已过期 |
|||
</Text> |
|||
); |
|||
} |
|||
return ( |
|||
<View className={`payment-countdown-badge expired ${className}`}> |
|||
<Text className="countdown-text">支付已过期</Text> |
|||
</View> |
|||
); |
|||
} |
|||
|
|||
// 判断紧急程度
|
|||
const isUrgent = isUrgentCountdown(timeLeft); |
|||
const isCritical = isCriticalCountdown(timeLeft); |
|||
|
|||
// 格式化倒计时文本
|
|||
const countdownText = formatCountdownText(timeLeft, showSeconds); |
|||
const fullText = `等待付款 ${countdownText}`; |
|||
|
|||
// 纯文本模式
|
|||
if (mode === 'text') { |
|||
return ( |
|||
<Text className={`payment-countdown-text ${isUrgent ? 'urgent' : ''} ${isCritical ? 'critical' : ''} ${className}`}> |
|||
{fullText} |
|||
</Text> |
|||
); |
|||
} |
|||
|
|||
// 徽章模式
|
|||
return ( |
|||
<View className={`payment-countdown-badge ${isUrgent ? 'urgent' : ''} ${isCritical ? 'critical' : ''} ${className}`}> |
|||
<Text className="countdown-text">{fullText}</Text> |
|||
</View> |
|||
); |
|||
}; |
|||
|
|||
export default PaymentCountdown; |
@ -0,0 +1,117 @@ |
|||
import { useState, useEffect, useCallback } from 'react'; |
|||
import { UserOrderStats } from '@/api/user'; |
|||
import Taro from '@tarojs/taro'; |
|||
import {pageShopOrder} from "@/api/shop/shopOrder"; |
|||
|
|||
/** |
|||
* 订单统计Hook |
|||
* 用于管理用户订单各状态的数量统计 |
|||
*/ |
|||
export const useOrderStats = () => { |
|||
const [orderStats, setOrderStats] = useState<UserOrderStats>({ |
|||
pending: 0, // 待付款
|
|||
paid: 0, // 待发货
|
|||
shipped: 0, // 待收货
|
|||
completed: 0, // 已完成
|
|||
refund: 0, // 退货/售后
|
|||
total: 0 // 总订单数
|
|||
}); |
|||
|
|||
const [loading, setLoading] = useState(false); |
|||
const [error, setError] = useState<string | null>(null); |
|||
|
|||
/** |
|||
* 获取订单统计数据 |
|||
*/ |
|||
const fetchOrderStats = useCallback(async (showToast = false) => { |
|||
try { |
|||
setLoading(true); |
|||
setError(null); |
|||
|
|||
// TODO 读取订单数量
|
|||
const pending = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 0}) |
|||
const paid = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 1}) |
|||
const shipped = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 3}) |
|||
const completed = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 5}) |
|||
const refund = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId'), statusFilter: 6}) |
|||
const total = await pageShopOrder({ page: 1, limit: 1, userId: Taro.getStorageSync('UserId')}) |
|||
setOrderStats({ |
|||
pending: pending?.count || 0, |
|||
paid: paid?.count || 0, |
|||
shipped: shipped?.count || 0, |
|||
completed: completed?.count || 0, |
|||
refund: refund?.count || 0, |
|||
total: total?.count || 0 |
|||
}) |
|||
|
|||
if (showToast) { |
|||
Taro.showToast({ |
|||
title: '数据已更新', |
|||
icon: 'success', |
|||
duration: 1500 |
|||
}); |
|||
} |
|||
} catch (err: any) { |
|||
const errorMessage = err.message || '获取订单统计失败'; |
|||
setError(errorMessage); |
|||
|
|||
console.error('获取订单统计失败:', err); |
|||
|
|||
if (showToast) { |
|||
Taro.showToast({ |
|||
title: errorMessage, |
|||
icon: 'error', |
|||
duration: 2000 |
|||
}); |
|||
} |
|||
} finally { |
|||
setLoading(false); |
|||
} |
|||
}, []); |
|||
|
|||
/** |
|||
* 刷新订单统计数据 |
|||
*/ |
|||
const refreshOrderStats = useCallback(() => { |
|||
return fetchOrderStats(true); |
|||
}, [fetchOrderStats]); |
|||
|
|||
/** |
|||
* 获取指定状态的订单数量 |
|||
*/ |
|||
const getOrderCount = useCallback((status: keyof UserOrderStats) => { |
|||
return orderStats[status] || 0; |
|||
}, [orderStats]); |
|||
|
|||
/** |
|||
* 检查是否有待处理的订单 |
|||
*/ |
|||
const hasPendingOrders = useCallback(() => { |
|||
return orderStats.pending > 0 || orderStats.paid > 0 || orderStats.shipped > 0; |
|||
}, [orderStats]); |
|||
|
|||
/** |
|||
* 获取总的待处理订单数量 |
|||
*/ |
|||
const getTotalPendingCount = useCallback(() => { |
|||
return orderStats.pending + orderStats.paid + orderStats.shipped; |
|||
}, [orderStats]); |
|||
|
|||
// 组件挂载时自动获取数据
|
|||
useEffect(() => { |
|||
fetchOrderStats(); |
|||
}, [fetchOrderStats]); |
|||
|
|||
return { |
|||
orderStats, |
|||
loading, |
|||
error, |
|||
fetchOrderStats, |
|||
refreshOrderStats, |
|||
getOrderCount, |
|||
hasPendingOrders, |
|||
getTotalPendingCount |
|||
}; |
|||
}; |
|||
|
|||
export default useOrderStats; |
@ -0,0 +1,163 @@ |
|||
import { useState, useEffect, useMemo } from 'react'; |
|||
import dayjs from 'dayjs'; |
|||
import duration from 'dayjs/plugin/duration'; |
|||
|
|||
// 扩展dayjs支持duration
|
|||
dayjs.extend(duration); |
|||
|
|||
export interface CountdownTime { |
|||
hours: number; |
|||
minutes: number; |
|||
seconds: number; |
|||
isExpired: boolean; |
|||
totalMinutes: number; // 总剩余分钟数
|
|||
} |
|||
|
|||
/** |
|||
* 支付倒计时Hook |
|||
* @param createTime 订单创建时间 |
|||
* @param payStatus 支付状态 |
|||
* @param realTime 是否实时更新(详情页用true,列表页用false) |
|||
* @param timeoutHours 超时小时数,默认24小时 |
|||
*/ |
|||
export const usePaymentCountdown = ( |
|||
createTime?: string, |
|||
payStatus?: boolean, |
|||
realTime: boolean = false, |
|||
timeoutHours: number = 24 |
|||
): CountdownTime => { |
|||
const [timeLeft, setTimeLeft] = useState<CountdownTime>({ |
|||
hours: 0, |
|||
minutes: 0, |
|||
seconds: 0, |
|||
isExpired: false, |
|||
totalMinutes: 0 |
|||
}); |
|||
|
|||
// 计算剩余时间的函数
|
|||
const calculateTimeLeft = useMemo(() => { |
|||
return (): CountdownTime => { |
|||
if (!createTime || payStatus) { |
|||
return { |
|||
hours: 0, |
|||
minutes: 0, |
|||
seconds: 0, |
|||
isExpired: false, |
|||
totalMinutes: 0 |
|||
}; |
|||
} |
|||
|
|||
const createTimeObj = dayjs(createTime); |
|||
const expireTime = createTimeObj.add(timeoutHours, 'hour'); |
|||
const now = dayjs(); |
|||
const diff = expireTime.diff(now); |
|||
|
|||
if (diff <= 0) { |
|||
return { |
|||
hours: 0, |
|||
minutes: 0, |
|||
seconds: 0, |
|||
isExpired: true, |
|||
totalMinutes: 0 |
|||
}; |
|||
} |
|||
|
|||
const durationObj = dayjs.duration(diff); |
|||
const hours = Math.floor(durationObj.asHours()); |
|||
const minutes = durationObj.minutes(); |
|||
const seconds = durationObj.seconds(); |
|||
const totalMinutes = Math.floor(durationObj.asMinutes()); |
|||
|
|||
return { |
|||
hours, |
|||
minutes, |
|||
seconds, |
|||
isExpired: false, |
|||
totalMinutes |
|||
}; |
|||
}; |
|||
}, [createTime, payStatus, timeoutHours]); |
|||
|
|||
useEffect(() => { |
|||
if (!createTime || payStatus) { |
|||
setTimeLeft({ |
|||
hours: 0, |
|||
minutes: 0, |
|||
seconds: 0, |
|||
isExpired: false, |
|||
totalMinutes: 0 |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
// 立即计算一次
|
|||
const initialTime = calculateTimeLeft(); |
|||
setTimeLeft(initialTime); |
|||
|
|||
// 如果不需要实时更新,直接返回
|
|||
if (!realTime) { |
|||
return; |
|||
} |
|||
|
|||
// 如果需要实时更新,设置定时器
|
|||
const timer = setInterval(() => { |
|||
const newTimeLeft = calculateTimeLeft(); |
|||
setTimeLeft(newTimeLeft); |
|||
|
|||
// 如果已过期,清除定时器
|
|||
if (newTimeLeft.isExpired) { |
|||
clearInterval(timer); |
|||
} |
|||
}, 1000); |
|||
|
|||
return () => clearInterval(timer); |
|||
}, [createTime, payStatus, realTime, calculateTimeLeft]); |
|||
|
|||
return timeLeft; |
|||
}; |
|||
|
|||
/** |
|||
* 格式化倒计时文本 |
|||
* @param timeLeft 倒计时时间对象 |
|||
* @param showSeconds 是否显示秒数 |
|||
*/ |
|||
export const formatCountdownText = ( |
|||
timeLeft: CountdownTime, |
|||
showSeconds: boolean = false |
|||
): string => { |
|||
if (timeLeft.isExpired) { |
|||
return '已过期'; |
|||
} |
|||
|
|||
if (timeLeft.hours > 0) { |
|||
if (showSeconds) { |
|||
return `${timeLeft.hours}小时${timeLeft.minutes}分${timeLeft.seconds}秒`; |
|||
} else { |
|||
return `${timeLeft.hours}小时${timeLeft.minutes}分钟`; |
|||
} |
|||
} else if (timeLeft.minutes > 0) { |
|||
if (showSeconds) { |
|||
return `${timeLeft.minutes}分${timeLeft.seconds}秒`; |
|||
} else { |
|||
return `${timeLeft.minutes}分钟`; |
|||
} |
|||
} else { |
|||
return `${timeLeft.seconds}秒`; |
|||
} |
|||
}; |
|||
|
|||
/** |
|||
* 判断是否为紧急状态(剩余时间少于1小时) |
|||
*/ |
|||
export const isUrgentCountdown = (timeLeft: CountdownTime): boolean => { |
|||
return !timeLeft.isExpired && timeLeft.totalMinutes < 60; |
|||
}; |
|||
|
|||
/** |
|||
* 判断是否为非常紧急状态(剩余时间少于10分钟) |
|||
*/ |
|||
export const isCriticalCountdown = (timeLeft: CountdownTime): boolean => { |
|||
return !timeLeft.isExpired && timeLeft.totalMinutes < 10; |
|||
}; |
|||
|
|||
export default usePaymentCountdown; |
Loading…
Reference in new issue