修改页面分页情况
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
@@ -17,14 +17,18 @@ router = APIRouter(prefix="/credits", tags=["credits"])
|
||||
|
||||
@router.get("", response_model=CreditBalanceOut)
|
||||
async def get_credits(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
records = await get_records(db, current_user.id)
|
||||
return CreditBalanceOut(
|
||||
credits=round(current_user.credits, 2),
|
||||
records=[CreditRecordOut.model_validate(r) for r in records],
|
||||
)
|
||||
records, total = await get_records(db, current_user.id, page, page_size)
|
||||
from fastapi.responses import JSONResponse
|
||||
return JSONResponse(content={
|
||||
"credits": round(current_user.credits, 2),
|
||||
"records": [CreditRecordOut.model_validate(r) for r in records],
|
||||
"total": total,
|
||||
})
|
||||
|
||||
|
||||
@router.get(
|
||||
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.models.notification import Notification
|
||||
from app.schemas.notification import NotificationOut, UnreadCountOut
|
||||
from app.schemas.notification import NotificationOut, NotificationListOut, UnreadCountOut
|
||||
from app.services.auth import decode_access_token
|
||||
from app.services.notification import (
|
||||
get_notifications,
|
||||
@@ -103,15 +103,16 @@ async def notifications_ws(websocket: WebSocket):
|
||||
_active_connections.pop(user_id, None)
|
||||
|
||||
|
||||
@router.get("", response_model=list[NotificationOut])
|
||||
@router.get("", response_model=NotificationListOut)
|
||||
async def list_notifications(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
is_read: bool = Query(None),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
items, _ = await get_notifications(db, current_user.id, page, page_size)
|
||||
return items
|
||||
items, total = await get_notifications(db, current_user.id, page, page_size, is_read)
|
||||
return {"items": items, "total": total}
|
||||
|
||||
|
||||
@router.put("/{notification_id}/read")
|
||||
|
||||
@@ -290,20 +290,29 @@ async def alipay_callback(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
@router.get("/orders", response_model=list[PaymentOrderOut])
|
||||
async def list_orders(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
# Auto-expire stale pending orders before returning
|
||||
from app.services.payment import _check_and_expire_order
|
||||
|
||||
count_query = select(func.count(PaymentOrder.id)).where(PaymentOrder.user_id == current_user.id)
|
||||
total = (await db.execute(count_query)).scalar() or 0
|
||||
|
||||
result = await db.execute(
|
||||
select(PaymentOrder)
|
||||
.where(PaymentOrder.user_id == current_user.id)
|
||||
.order_by(PaymentOrder.created_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
orders = result.scalars().all()
|
||||
for o in orders:
|
||||
await _check_and_expire_order(db, o)
|
||||
return orders
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
return JSONResponse(content={"items": orders, "total": total})
|
||||
|
||||
|
||||
@router.get("/orders/{order_no}", response_model=PaymentOrderOut)
|
||||
|
||||
@@ -14,5 +14,10 @@ class NotificationOut(BaseModel):
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class NotificationListOut(BaseModel):
|
||||
items: list[NotificationOut]
|
||||
total: int
|
||||
|
||||
|
||||
class UnreadCountOut(BaseModel):
|
||||
count: int
|
||||
|
||||
@@ -282,10 +282,15 @@ async def refund_credits(
|
||||
)
|
||||
|
||||
|
||||
async def get_records(db: AsyncSession, user_id: str) -> list[CreditRecord]:
|
||||
async def get_records(db: AsyncSession, user_id: str, page: int = 1, page_size: int = 20) -> tuple[list[CreditRecord], int]:
|
||||
count_query = select(func.count(CreditRecord.id)).where(CreditRecord.user_id == user_id)
|
||||
total = (await db.execute(count_query)).scalar() or 0
|
||||
|
||||
result = await db.execute(
|
||||
select(CreditRecord)
|
||||
.where(CreditRecord.user_id == user_id)
|
||||
.order_by(CreditRecord.created_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
return list(result.scalars().all()), total
|
||||
|
||||
@@ -74,16 +74,18 @@ async def create_notification(
|
||||
|
||||
|
||||
async def get_notifications(
|
||||
db: AsyncSession, user_id: str, page: int = 1, page_size: int = 20
|
||||
db: AsyncSession, user_id: str, page: int = 1, page_size: int = 20, is_read: bool = None
|
||||
) -> tuple[list[dict], int]:
|
||||
"""Get notifications for a user (personal + broadcast), with per-user read state."""
|
||||
user_is_read_expr = case(
|
||||
(Notification.user_id.is_(None), NotificationRead.id.is_not(None)),
|
||||
else_=Notification.is_read,
|
||||
).label("user_is_read")
|
||||
|
||||
query = (
|
||||
select(
|
||||
Notification,
|
||||
case(
|
||||
(Notification.user_id.is_(None), NotificationRead.id.is_not(None)),
|
||||
else_=Notification.is_read,
|
||||
).label("user_is_read"),
|
||||
user_is_read_expr,
|
||||
)
|
||||
.outerjoin(
|
||||
NotificationRead,
|
||||
@@ -96,11 +98,15 @@ async def get_notifications(
|
||||
Notification.user_id.is_(None),
|
||||
)
|
||||
)
|
||||
.order_by(Notification.created_at.desc())
|
||||
)
|
||||
|
||||
count_query = (
|
||||
select(func.count(Notification.id))
|
||||
.outerjoin(
|
||||
NotificationRead,
|
||||
(Notification.id == NotificationRead.notification_id)
|
||||
& (NotificationRead.user_id == user_id),
|
||||
)
|
||||
.where(
|
||||
or_(
|
||||
Notification.user_id == user_id,
|
||||
@@ -109,6 +115,12 @@ async def get_notifications(
|
||||
)
|
||||
)
|
||||
|
||||
if is_read is not None:
|
||||
query = query.where(user_is_read_expr == is_read)
|
||||
count_query = count_query.where(user_is_read_expr == is_read)
|
||||
|
||||
query = query.order_by(Notification.created_at.desc())
|
||||
|
||||
total = (await db.execute(count_query)).scalar() or 0
|
||||
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
|
||||
rows = result.all()
|
||||
|
||||
@@ -153,9 +153,15 @@ export async function generateVideo(recordId: string, params: GenerateParams): P
|
||||
});
|
||||
}
|
||||
// ── Credits ───────────────────────────────────────────────
|
||||
export async function getCredits(): Promise<{ credits: number; records: CreditRecord[] }> {
|
||||
if (USE_MOCK) return mock.mockGetCredits();
|
||||
return api.get('/credits');
|
||||
export async function getCredits(page = 1, pageSize = 20): Promise<{ credits: number; records: CreditRecord[]; total: number }> {
|
||||
if (USE_MOCK) {
|
||||
const data = await mock.mockGetCredits();
|
||||
return data;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', String(page));
|
||||
params.set('page_size', String(pageSize));
|
||||
return api.get(`/credits?${params.toString()}`);
|
||||
}
|
||||
// ── Captcha ───────────────────────────────────────────────
|
||||
export async function getSliderCaptcha(): Promise<{ captcha_id: string; bg_image: string; slider_image: string }> {
|
||||
@@ -191,9 +197,18 @@ export async function verifySms(phone: string, code: string): Promise<{ token: s
|
||||
return api.post('/sms/verify', { phone, code }, false);
|
||||
}
|
||||
// ── Notifications ─────────────────────────────────────────
|
||||
export async function getNotifications(): Promise<AdminNotification[]> {
|
||||
if (USE_MOCK) return mock.mockGetAdminNotifications();
|
||||
return api.get('/notifications');
|
||||
export async function getNotifications(page = 1, pageSize = 20, isRead?: boolean): Promise<{ items: AdminNotification[], total: number }> {
|
||||
if (USE_MOCK) {
|
||||
const items = await mock.mockGetAdminNotifications();
|
||||
return { items, total: items.length };
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', String(page));
|
||||
params.set('page_size', String(pageSize));
|
||||
if (isRead !== undefined) {
|
||||
params.set('is_read', String(isRead));
|
||||
}
|
||||
return api.get(`/notifications?${params.toString()}`);
|
||||
}
|
||||
export async function getUnreadCount(): Promise<number> {
|
||||
if (USE_MOCK) return mock.mockGetAdminNotifications().then(n => n.filter(x => !x.isRead).length);
|
||||
@@ -271,8 +286,11 @@ export async function createRechargeOrder(planId: string, method: string = 'wech
|
||||
return api.post('/payments/recharge', { plan: planId, method });
|
||||
}
|
||||
|
||||
export async function getPaymentOrders(): Promise<any[]> {
|
||||
return api.get('/payments/orders');
|
||||
export async function getPaymentOrders(page = 1, pageSize = 20): Promise<{ items: any[]; total: number }> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', String(page));
|
||||
params.set('page_size', String(pageSize));
|
||||
return api.get(`/payments/orders?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getPaymentOrder(orderNo: string): Promise<any> {
|
||||
|
||||
@@ -157,11 +157,12 @@ export async function mockGetUser(): Promise<User | null> {
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
export async function mockGetCredits(): Promise<{ credits: number; records: CreditRecord[] }> {
|
||||
export async function mockGetCredits(): Promise<{ credits: number; records: CreditRecord[]; total: number }> {
|
||||
await delay(400);
|
||||
return {
|
||||
credits: currentUser?.credits ?? 0,
|
||||
records: MOCK_CREDIT_RECORDS,
|
||||
total: MOCK_CREDIT_RECORDS.length,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ const NotificationPopup: React.FC = () => {
|
||||
|
||||
const fetchNotifications = useCallback(async () => {
|
||||
try {
|
||||
const data = await getNotifications();
|
||||
const result = await getNotifications();
|
||||
const data = result.items || [];
|
||||
const unread = data.filter((n: Notification) => !n.isRead);
|
||||
setNotifications(unread);
|
||||
if (unread.length > 0 && !visible) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Tag, Empty, Spin, Typography, Row, Col } from 'antd';
|
||||
import { Table, Tag, Empty, Spin, Typography, Row, Col, Pagination } from 'antd';
|
||||
import { WalletOutlined, PlusCircleOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import { getCredits } from '../api';
|
||||
|
||||
@@ -7,23 +7,31 @@ const CreditRecordsPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [records, setRecords] = useState<any[]>([]);
|
||||
const [credits, setCredits] = useState(0);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(10);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
}, [page]);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getCredits();
|
||||
const data = await getCredits(page, pageSize);
|
||||
setRecords(data.records || []);
|
||||
setCredits(data.credits || 0);
|
||||
setTotal(data.total || 0);
|
||||
} catch {
|
||||
setRecords([]);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '变动类型',
|
||||
@@ -139,13 +147,24 @@ const CreditRecordsPage: React.FC = () => {
|
||||
description={<span style={{ color: '#94a3b8' }}>暂无积分变动记录</span>}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
dataSource={records}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 10, size: 'small' }}
|
||||
bordered={false}
|
||||
/>
|
||||
<>
|
||||
<Table
|
||||
dataSource={records}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
bordered={false}
|
||||
/>
|
||||
<div style={{ padding: '16px', textAlign: 'right' }}>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
onChange={handlePageChange}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
@@ -1,36 +1,54 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Tag, Empty, Spin, Typography, Button } from 'antd';
|
||||
import { Table, Tag, Empty, Spin, Typography, Button, Pagination } from 'antd';
|
||||
import { BellOutlined, SettingOutlined, WalletOutlined, GiftOutlined } from '@ant-design/icons';
|
||||
import { getNotifications, markNotificationRead } from '../api';
|
||||
import { getNotifications, markNotificationRead, getUnreadCount } from '../api';
|
||||
|
||||
const MessagesPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [notifications, setNotifications] = useState<any[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(10);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
loadUnreadCount();
|
||||
}, [page]);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getNotifications();
|
||||
setNotifications(data || []);
|
||||
setUnreadCount(data.filter((n: any) => !(n.isRead ?? n.is_read)).length);
|
||||
const data = await getNotifications(page, pageSize);
|
||||
setNotifications(data.items || []);
|
||||
setTotal(data.total || 0);
|
||||
} catch {
|
||||
setNotifications([]);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const loadUnreadCount = async () => {
|
||||
try {
|
||||
const count = await getUnreadCount();
|
||||
setUnreadCount(count);
|
||||
} catch {
|
||||
setUnreadCount(0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkRead = async (id: string) => {
|
||||
try {
|
||||
await markNotificationRead(id);
|
||||
loadData();
|
||||
loadUnreadCount();
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '类型',
|
||||
@@ -131,13 +149,24 @@ const MessagesPage: React.FC = () => {
|
||||
description={<span style={{ color: '#94a3b8' }}>暂无消息</span>}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
dataSource={notifications}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 10, size: 'small' }}
|
||||
bordered={false}
|
||||
/>
|
||||
<>
|
||||
<Table
|
||||
dataSource={notifications}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
bordered={false}
|
||||
/>
|
||||
<div style={{ padding: '16px', textAlign: 'right' }}>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
onChange={handlePageChange}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Tag, Empty, Spin, Typography } from 'antd';
|
||||
import { Table, Tag, Empty, Spin, Typography, Pagination } from 'antd';
|
||||
import { FileTextOutlined, AlipayCircleOutlined, WechatOutlined } from '@ant-design/icons';
|
||||
import { getPaymentOrders } from '../api';
|
||||
|
||||
const OrderRecordsPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [orders, setOrders] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(10);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
}, [page]);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getPaymentOrders();
|
||||
setOrders(data || []);
|
||||
const data = await getPaymentOrders(page, pageSize);
|
||||
setOrders(data.items || []);
|
||||
setTotal(data.total || 0);
|
||||
} catch {
|
||||
setOrders([]);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '订单号',
|
||||
@@ -140,13 +148,24 @@ const OrderRecordsPage: React.FC = () => {
|
||||
description={<span style={{ color: '#94a3b8' }}>暂无订单记录</span>}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
dataSource={orders}
|
||||
columns={columns}
|
||||
rowKey="order_no"
|
||||
pagination={{ pageSize: 10, size: 'small' }}
|
||||
bordered={false}
|
||||
/>
|
||||
<>
|
||||
<Table
|
||||
dataSource={orders}
|
||||
columns={columns}
|
||||
rowKey="order_no"
|
||||
pagination={false}
|
||||
bordered={false}
|
||||
/>
|
||||
<div style={{ padding: '16px', textAlign: 'right' }}>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
onChange={handlePageChange}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user