1
This commit is contained in:
Vendored
+137
-137
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-222rrxOK.js"></script>
|
<script type="module" crossorigin src="/assets/index-D9cWJZKc.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D3fwIbOp.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D3fwIbOp.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -794,6 +794,8 @@ export async function getAdminGenerationRecords(params?: {
|
|||||||
status?: string;
|
status?: string;
|
||||||
engineId?: string;
|
engineId?: string;
|
||||||
includeMediaReferences?: boolean;
|
includeMediaReferences?: boolean;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}): Promise<{ total: number; items: any[] }> {
|
}): Promise<{ total: number; items: any[] }> {
|
||||||
@@ -804,6 +806,8 @@ export async function getAdminGenerationRecords(params?: {
|
|||||||
if (params?.includeMediaReferences !== undefined) {
|
if (params?.includeMediaReferences !== undefined) {
|
||||||
q.set('include_media_references', String(params.includeMediaReferences));
|
q.set('include_media_references', String(params.includeMediaReferences));
|
||||||
}
|
}
|
||||||
|
if (params?.startDate) q.set('start_date', params.startDate);
|
||||||
|
if (params?.endDate) q.set('end_date', params.endDate);
|
||||||
if (params?.page) q.set('page', String(params.page));
|
if (params?.page) q.set('page', String(params.page));
|
||||||
if (params?.pageSize) q.set('page_size', String(params.pageSize));
|
if (params?.pageSize) q.set('page_size', String(params.pageSize));
|
||||||
const qs = q.toString();
|
const qs = q.toString();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
DatePicker,
|
||||||
Empty,
|
Empty,
|
||||||
Input,
|
Input,
|
||||||
message,
|
message,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
FileImageOutlined,
|
FileImageOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { getAdminGenerationRecords, getVideoEngines, getImageEngines } from '../api';
|
import { getAdminGenerationRecords, getVideoEngines, getImageEngines } from '../api';
|
||||||
import type { AdminGenerationRecord, GenerationAIMediaReference } from '../types';
|
import type { AdminGenerationRecord, GenerationAIMediaReference } from '../types';
|
||||||
import { formatDate } from '../utils/formatDate';
|
import { formatDate } from '../utils/formatDate';
|
||||||
@@ -178,6 +180,9 @@ const InfoItem: React.FC<{ label: string; value?: React.ReactNode }> = ({ label,
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const todayStart = () => dayjs().startOf('day');
|
||||||
|
const todayEnd = () => dayjs().endOf('day');
|
||||||
|
|
||||||
const AdminGenerationRecords: React.FC = () => {
|
const AdminGenerationRecords: React.FC = () => {
|
||||||
const [records, setRecords] = useState<AdminGenerationRecord[]>([]);
|
const [records, setRecords] = useState<AdminGenerationRecord[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
@@ -195,6 +200,10 @@ const AdminGenerationRecords: React.FC = () => {
|
|||||||
const [videoPlaying, setVideoPlaying] = useState(false);
|
const [videoPlaying, setVideoPlaying] = useState(false);
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
|
||||||
|
// 时间筛选(默认当天)
|
||||||
|
const [createdRange, setCreatedRange] = useState<any>([todayStart(), todayEnd()]);
|
||||||
|
const [queryCreatedRange, setQueryCreatedRange] = useState<any>([todayStart(), todayEnd()]);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -203,6 +212,8 @@ const AdminGenerationRecords: React.FC = () => {
|
|||||||
status: filterStatus || undefined,
|
status: filterStatus || undefined,
|
||||||
engineId: filterEngineId || undefined,
|
engineId: filterEngineId || undefined,
|
||||||
includeMediaReferences: filterIncludeMedia === '' ? undefined : filterIncludeMedia === 'true',
|
includeMediaReferences: filterIncludeMedia === '' ? undefined : filterIncludeMedia === 'true',
|
||||||
|
startDate: queryCreatedRange?.[0]?.format?.('YYYY-MM-DD'),
|
||||||
|
endDate: queryCreatedRange?.[1]?.format?.('YYYY-MM-DD'),
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
});
|
});
|
||||||
@@ -246,7 +257,7 @@ const AdminGenerationRecords: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [filterStatus, filterUserId, filterEngineId, filterIncludeMedia, page, pageSize]);
|
}, [filterStatus, filterUserId, filterEngineId, filterIncludeMedia, page, pageSize, queryCreatedRange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load();
|
||||||
@@ -300,6 +311,19 @@ const AdminGenerationRecords: React.FC = () => {
|
|||||||
}, [preview]);
|
}, [preview]);
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
|
setPage(1);
|
||||||
|
setQueryCreatedRange(createdRange);
|
||||||
|
setReloadKey((v) => v + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setFilterStatus('');
|
||||||
|
setFilterUserId('');
|
||||||
|
setFilterEngineId('');
|
||||||
|
setFilterIncludeMedia('');
|
||||||
|
const defaultRange = [todayStart(), todayEnd()];
|
||||||
|
setCreatedRange(defaultRange);
|
||||||
|
setQueryCreatedRange(defaultRange);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
setReloadKey((v) => v + 1);
|
setReloadKey((v) => v + 1);
|
||||||
};
|
};
|
||||||
@@ -870,9 +894,23 @@ const AdminGenerationRecords: React.FC = () => {
|
|||||||
onPressEnter={handleSearch}
|
onPressEnter={handleSearch}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
|
<DatePicker.RangePicker
|
||||||
|
value={createdRange}
|
||||||
|
onChange={(dates) => {
|
||||||
|
if (dates && dates[0] && dates[1]) {
|
||||||
|
setCreatedRange([dates[0].startOf('day'), dates[1].endOf('day')]);
|
||||||
|
} else {
|
||||||
|
setCreatedRange(dates);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={['开始日期', '结束日期']}
|
||||||
|
/>
|
||||||
<Button type="primary" onClick={handleSearch} style={{ borderRadius: 8 }}>
|
<Button type="primary" onClick={handleSearch} style={{ borderRadius: 8 }}>
|
||||||
搜索
|
搜索
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={handleReset} style={{ borderRadius: 8 }}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Table, Button, Tag, Space, Typography, message, Modal, Card, Popconfirm, Empty, Input, DatePicker } from 'antd';
|
import { Table, Button, Tag, Space, Typography, message, Modal, Card, Popconfirm, Empty, Input, DatePicker } from 'antd';
|
||||||
import { CheckOutlined, CloseOutlined, EyeOutlined, FileTextOutlined, FilterOutlined } from '@ant-design/icons';
|
import { CheckOutlined, CloseOutlined, EyeOutlined, FileTextOutlined, FilterOutlined } from '@ant-design/icons';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { getAdminInvoices, getAdminInvoiceDetail, updateInvoiceStatus } from '../api';
|
import { getAdminInvoices, getAdminInvoiceDetail, updateInvoiceStatus } from '../api';
|
||||||
import { formatDate } from '../utils/formatDate';
|
import { formatDate } from '../utils/formatDate';
|
||||||
import type { InvoiceItem, InvoiceDetail } from '../types';
|
import type { InvoiceItem, InvoiceDetail } from '../types';
|
||||||
@@ -13,8 +14,8 @@ const AdminInvoices: React.FC = () => {
|
|||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
||||||
const [phoneFilter, setPhoneFilter] = useState<string>('');
|
const [phoneFilter, setPhoneFilter] = useState<string>('');
|
||||||
const [startDate, setStartDate] = useState<string>('');
|
const [createdRange, setCreatedRange] = useState<any>([dayjs().startOf('day'), dayjs().endOf('day')]);
|
||||||
const [endDate, setEndDate] = useState<string>('');
|
const [queryCreatedRange, setQueryCreatedRange] = useState<any>([dayjs().startOf('day'), dayjs().endOf('day')]);
|
||||||
|
|
||||||
// 详情弹窗
|
// 详情弹窗
|
||||||
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
||||||
@@ -35,8 +36,8 @@ const AdminInvoices: React.FC = () => {
|
|||||||
pageSize,
|
pageSize,
|
||||||
status: statusFilter || undefined,
|
status: statusFilter || undefined,
|
||||||
phone: phoneFilter || undefined,
|
phone: phoneFilter || undefined,
|
||||||
startDate: startDate || undefined,
|
startDate: queryCreatedRange?.[0]?.format?.('YYYY-MM-DD'),
|
||||||
endDate: endDate || undefined,
|
endDate: queryCreatedRange?.[1]?.format?.('YYYY-MM-DD'),
|
||||||
});
|
});
|
||||||
setData(res.items);
|
setData(res.items);
|
||||||
setTotal(res.total);
|
setTotal(res.total);
|
||||||
@@ -49,11 +50,20 @@ const AdminInvoices: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [page, pageSize, statusFilter, startDate, endDate]);
|
}, [page, pageSize, statusFilter, queryCreatedRange]);
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
setPage(1);
|
setPage(1);
|
||||||
fetchData();
|
setQueryCreatedRange(createdRange);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setStatusFilter(null);
|
||||||
|
setPhoneFilter('');
|
||||||
|
const defaultRange = [dayjs().startOf('day'), dayjs().endOf('day')];
|
||||||
|
setCreatedRange(defaultRange);
|
||||||
|
setQueryCreatedRange(defaultRange);
|
||||||
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewDetail = async (id: string) => {
|
const handleViewDetail = async (id: string) => {
|
||||||
@@ -267,15 +277,19 @@ const AdminInvoices: React.FC = () => {
|
|||||||
allowClear
|
allowClear
|
||||||
onPressEnter={handleSearch}
|
onPressEnter={handleSearch}
|
||||||
/>
|
/>
|
||||||
<DatePicker
|
<DatePicker.RangePicker
|
||||||
placeholder="开始日期"
|
value={createdRange}
|
||||||
onChange={(d) => setStartDate(d ? d.format('YYYY-MM-DD') : '')}
|
onChange={(dates) => {
|
||||||
/>
|
if (dates && dates[0] && dates[1]) {
|
||||||
<DatePicker
|
setCreatedRange([dates[0].startOf('day'), dates[1].endOf('day')]);
|
||||||
placeholder="结束日期"
|
} else {
|
||||||
onChange={(d) => setEndDate(d ? d.format('YYYY-MM-DD') : '')}
|
setCreatedRange(dates);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={['开始日期', '结束日期']}
|
||||||
/>
|
/>
|
||||||
<Button type="primary" onClick={handleSearch}>搜索</Button>
|
<Button type="primary" onClick={handleSearch}>搜索</Button>
|
||||||
|
<Button onClick={handleReset}>重置</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""发票抬头表迁移
|
||||||
|
|
||||||
|
创建 invoice_headers(发票抬头表),用于用户管理常用发票抬头。
|
||||||
|
|
||||||
|
Revision ID: 20260811_20260811
|
||||||
|
Revises: 20260810_20260810
|
||||||
|
Create Date: 2026-08-11 00:00:00.000000
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = '20260811_20260811'
|
||||||
|
down_revision: Union[str, None] = '20260810_20260810'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _comment_table(table_name: str, comment: str) -> None:
|
||||||
|
op.execute(f"COMMENT ON TABLE {table_name} IS '{comment}'")
|
||||||
|
|
||||||
|
|
||||||
|
def _comment_column(table_name: str, column_name: str, comment: str) -> None:
|
||||||
|
escaped = comment.replace("'", "''")
|
||||||
|
op.execute(f"COMMENT ON COLUMN {table_name}.{column_name} IS '{escaped}'")
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'invoice_headers',
|
||||||
|
sa.Column('id', sa.String(32), primary_key=True),
|
||||||
|
sa.Column('user_id', sa.String(32), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False),
|
||||||
|
sa.Column('type', sa.String(16), nullable=False),
|
||||||
|
sa.Column('name', sa.String(128), nullable=False),
|
||||||
|
sa.Column('tax_no', sa.String(32), nullable=True),
|
||||||
|
sa.Column('register_address', sa.String(256), nullable=True),
|
||||||
|
sa.Column('register_phone', sa.String(32), nullable=True),
|
||||||
|
sa.Column('bank_name', sa.String(128), nullable=True),
|
||||||
|
sa.Column('bank_account', sa.String(64), nullable=True),
|
||||||
|
sa.Column('email', sa.String(128), nullable=True),
|
||||||
|
sa.Column('is_default', sa.Boolean, nullable=False, server_default='false'),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||||
|
)
|
||||||
|
op.create_index('idx_invoice_headers_user', 'invoice_headers', ['user_id'])
|
||||||
|
|
||||||
|
# 表注释和字段注释
|
||||||
|
_comment_table('invoice_headers', '发票抬头表')
|
||||||
|
_comment_column('invoice_headers', 'id', '主键')
|
||||||
|
_comment_column('invoice_headers', 'user_id', '用户ID')
|
||||||
|
_comment_column('invoice_headers', 'type', '抬头类型: personal/company')
|
||||||
|
_comment_column('invoice_headers', 'name', '抬头名称')
|
||||||
|
_comment_column('invoice_headers', 'tax_no', '税号')
|
||||||
|
_comment_column('invoice_headers', 'register_address', '注册地址')
|
||||||
|
_comment_column('invoice_headers', 'register_phone', '注册电话')
|
||||||
|
_comment_column('invoice_headers', 'bank_name', '开户行')
|
||||||
|
_comment_column('invoice_headers', 'bank_account', '银行账号')
|
||||||
|
_comment_column('invoice_headers', 'email', '接收邮箱')
|
||||||
|
_comment_column('invoice_headers', 'is_default', '是否默认')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table('invoice_headers')
|
||||||
@@ -36,6 +36,7 @@ from app.api.v1.private_portrait import router as private_portrait_router
|
|||||||
from app.api.v1.private_portrait_virtual import router as private_portrait_virtual_router
|
from app.api.v1.private_portrait_virtual import router as private_portrait_virtual_router
|
||||||
from app.api.v1.upload_resource import router as upload_resource_router
|
from app.api.v1.upload_resource import router as upload_resource_router
|
||||||
from app.api.v1.invoices import router as invoices_router
|
from app.api.v1.invoices import router as invoices_router
|
||||||
|
from app.api.v1.invoice_headers import router as invoice_headers_router
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(auth_router)
|
api_router.include_router(auth_router)
|
||||||
@@ -74,3 +75,4 @@ api_router.include_router(private_portrait_router)
|
|||||||
api_router.include_router(private_portrait_virtual_router)
|
api_router.include_router(private_portrait_virtual_router)
|
||||||
api_router.include_router(upload_resource_router)
|
api_router.include_router(upload_resource_router)
|
||||||
api_router.include_router(invoices_router)
|
api_router.include_router(invoices_router)
|
||||||
|
api_router.include_router(invoice_headers_router)
|
||||||
|
|||||||
@@ -2210,6 +2210,8 @@ async def admin_list_generation_records(
|
|||||||
status: str | None = Query(None),
|
status: str | None = Query(None),
|
||||||
engine_id: str | None = Query(None),
|
engine_id: str | None = Query(None),
|
||||||
include_media_references: bool | None = Query(None),
|
include_media_references: bool | None = Query(None),
|
||||||
|
start_date: str | None = Query(None, description="创建时间起始,格式 YYYY-MM-DD"),
|
||||||
|
end_date: str | None = Query(None, description="创建时间结束,格式 YYYY-MM-DD"),
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
page_size: int = Query(20, ge=1, le=500),
|
page_size: int = Query(20, ge=1, le=500),
|
||||||
admin: User = Depends(get_admin_user),
|
admin: User = Depends(get_admin_user),
|
||||||
@@ -2232,6 +2234,10 @@ async def admin_list_generation_records(
|
|||||||
query = query.where(GenerationRecord.engine_id == engine_id)
|
query = query.where(GenerationRecord.engine_id == engine_id)
|
||||||
if include_media_references is not None:
|
if include_media_references is not None:
|
||||||
query = query.where(GenerationRecord.include_media_references.is_(include_media_references))
|
query = query.where(GenerationRecord.include_media_references.is_(include_media_references))
|
||||||
|
if start_date:
|
||||||
|
query = query.where(GenerationRecord.created_at >= datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=CST))
|
||||||
|
if end_date:
|
||||||
|
query = query.where(GenerationRecord.created_at < (datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)).replace(tzinfo=CST))
|
||||||
|
|
||||||
# Count total
|
# Count total
|
||||||
count_query = (
|
count_query = (
|
||||||
@@ -2247,6 +2253,10 @@ async def admin_list_generation_records(
|
|||||||
count_query = count_query.where(GenerationRecord.engine_id == engine_id)
|
count_query = count_query.where(GenerationRecord.engine_id == engine_id)
|
||||||
if include_media_references is not None:
|
if include_media_references is not None:
|
||||||
count_query = count_query.where(GenerationRecord.include_media_references.is_(include_media_references))
|
count_query = count_query.where(GenerationRecord.include_media_references.is_(include_media_references))
|
||||||
|
if start_date:
|
||||||
|
count_query = count_query.where(GenerationRecord.created_at >= datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=CST))
|
||||||
|
if end_date:
|
||||||
|
count_query = count_query.where(GenerationRecord.created_at < (datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)).replace(tzinfo=CST))
|
||||||
total_result = await db.execute(count_query)
|
total_result = await db.execute(count_query)
|
||||||
total = total_result.scalar() or 0
|
total = total_result.scalar() or 0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.dependencies import get_db, get_current_user
|
||||||
|
from app.models.user import User
|
||||||
|
from app.schemas.invoice import InvoiceHeaderCreate, InvoiceHeaderOut, InvoiceHeaderUpdate
|
||||||
|
from app.services.invoice_header import (
|
||||||
|
create_header,
|
||||||
|
delete_header,
|
||||||
|
get_user_headers,
|
||||||
|
set_default_header,
|
||||||
|
update_header,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger("videogen")
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/invoice-headers", tags=["invoice-headers"])
|
||||||
|
|
||||||
|
|
||||||
|
def _header_to_out(header) -> dict:
|
||||||
|
return {
|
||||||
|
"id": header.id,
|
||||||
|
"userId": header.user_id,
|
||||||
|
"type": header.type,
|
||||||
|
"name": header.name,
|
||||||
|
"taxNo": header.tax_no,
|
||||||
|
"registerAddress": header.register_address,
|
||||||
|
"registerPhone": header.register_phone,
|
||||||
|
"bankName": header.bank_name,
|
||||||
|
"bankAccount": header.bank_account,
|
||||||
|
"email": header.email,
|
||||||
|
"isDefault": header.is_default,
|
||||||
|
"createdAt": header.created_at.isoformat() if header.created_at else None,
|
||||||
|
"updatedAt": header.updated_at.isoformat() if header.updated_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=InvoiceHeaderOut)
|
||||||
|
async def create_invoice_header(
|
||||||
|
req: InvoiceHeaderCreate,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""创建发票抬头。"""
|
||||||
|
header = await create_header(db, current_user.id, req)
|
||||||
|
await db.commit()
|
||||||
|
return _header_to_out(header)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def list_invoice_headers(
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""获取当前用户的所有发票抬头。"""
|
||||||
|
headers = await get_user_headers(db, current_user.id)
|
||||||
|
return {"items": [_header_to_out(h) for h in headers]}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{header_id}", response_model=InvoiceHeaderOut)
|
||||||
|
async def update_invoice_header(
|
||||||
|
header_id: str,
|
||||||
|
req: InvoiceHeaderUpdate,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""更新发票抬头。"""
|
||||||
|
header = await update_header(db, header_id, current_user.id, req)
|
||||||
|
await db.commit()
|
||||||
|
return _header_to_out(header)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{header_id}")
|
||||||
|
async def delete_invoice_header(
|
||||||
|
header_id: str,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""删除发票抬头。"""
|
||||||
|
await delete_header(db, header_id, current_user.id)
|
||||||
|
await db.commit()
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{header_id}/set-default", response_model=InvoiceHeaderOut)
|
||||||
|
async def set_default_invoice_header(
|
||||||
|
header_id: str,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""设置默认发票抬头。"""
|
||||||
|
header = await set_default_header(db, header_id, current_user.id)
|
||||||
|
await db.commit()
|
||||||
|
return _header_to_out(header)
|
||||||
@@ -37,6 +37,7 @@ from app.models.user_oauth_app import UserOAuthApp
|
|||||||
from app.models.home_material import HomeMaterialAsset, HomeMaterialCategory, HomeMaterialWatermark
|
from app.models.home_material import HomeMaterialAsset, HomeMaterialCategory, HomeMaterialWatermark
|
||||||
from app.models.contact_request import ContactRequest
|
from app.models.contact_request import ContactRequest
|
||||||
from app.models.invoice import Invoice, InvoiceOrder
|
from app.models.invoice import Invoice, InvoiceOrder
|
||||||
|
from app.models.invoice_header import InvoiceHeader
|
||||||
from app.models.private_portrait import PrivatePortraitProject, PrivatePortraitValidateSession, PrivatePortraitAssetGroup, PrivatePortraitAsset
|
from app.models.private_portrait import PrivatePortraitProject, PrivatePortraitValidateSession, PrivatePortraitAssetGroup, PrivatePortraitAsset
|
||||||
from app.models.api import ApiKey, ApiGenerationTask, ApiUsageLog, ApiKeyUpscaleConfig, ApiUpscaleLink
|
from app.models.api import ApiKey, ApiGenerationTask, ApiUsageLog, ApiKeyUpscaleConfig, ApiUpscaleLink
|
||||||
|
|
||||||
@@ -58,5 +59,5 @@ __all__ = [
|
|||||||
"PrivatePortraitAssetGroup", "PrivatePortraitAsset",
|
"PrivatePortraitAssetGroup", "PrivatePortraitAsset",
|
||||||
"ApiKey", "ApiGenerationTask", "ApiUsageLog", "ApiKeyUpscaleConfig", "ApiUpscaleLink",
|
"ApiKey", "ApiGenerationTask", "ApiUsageLog", "ApiKeyUpscaleConfig", "ApiUpscaleLink",
|
||||||
"ApiModelPricing",
|
"ApiModelPricing",
|
||||||
"Invoice", "InvoiceOrder",
|
"Invoice", "InvoiceOrder", "InvoiceHeader",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Index
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.models.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceHeader(Base):
|
||||||
|
"""发票抬头表"""
|
||||||
|
__tablename__ = "invoice_headers"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String(32), primary_key=True, comment="主键")
|
||||||
|
user_id: Mapped[str] = mapped_column(
|
||||||
|
String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, comment="用户ID"
|
||||||
|
)
|
||||||
|
type: Mapped[str] = mapped_column(String(16), nullable=False, comment="抬头类型: personal/company")
|
||||||
|
name: Mapped[str] = mapped_column(String(128), nullable=False, comment="抬头名称")
|
||||||
|
tax_no: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="税号")
|
||||||
|
register_address: Mapped[str | None] = mapped_column(String(256), nullable=True, comment="注册地址")
|
||||||
|
register_phone: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="注册电话")
|
||||||
|
bank_name: Mapped[str | None] = mapped_column(String(128), nullable=True, comment="开户行")
|
||||||
|
bank_account: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="银行账号")
|
||||||
|
email: Mapped[str | None] = mapped_column(String(128), nullable=True, comment="接收邮箱")
|
||||||
|
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, comment="是否默认")
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, comment="创建时间"
|
||||||
|
)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, comment="更新时间"
|
||||||
|
)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
Index('idx_invoice_headers_user', 'user_id'),
|
||||||
|
)
|
||||||
@@ -81,3 +81,55 @@ class InvoiceOut(BaseModel):
|
|||||||
created_at: NaiveDatetimeOptional = None
|
created_at: NaiveDatetimeOptional = None
|
||||||
updated_at: NaiveDatetimeOptional = None
|
updated_at: NaiveDatetimeOptional = None
|
||||||
orders: list[InvoiceOrderOut] = []
|
orders: list[InvoiceOrderOut] = []
|
||||||
|
|
||||||
|
|
||||||
|
# ── 发票抬头 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
class InvoiceHeaderCreate(BaseModel):
|
||||||
|
"""创建发票抬头请求。"""
|
||||||
|
type: str = Field(..., pattern="^(personal|company)$", description="抬头类型")
|
||||||
|
name: str = Field(..., min_length=1, max_length=128, description="抬头名称")
|
||||||
|
tax_no: str | None = Field(None, max_length=32, description="税号")
|
||||||
|
register_address: str | None = Field(None, max_length=256, description="注册地址")
|
||||||
|
register_phone: str | None = Field(None, max_length=32, description="注册电话")
|
||||||
|
bank_name: str | None = Field(None, max_length=128, description="开户行")
|
||||||
|
bank_account: str | None = Field(None, max_length=64, description="银行账号")
|
||||||
|
email: str | None = Field(None, max_length=128, description="接收邮箱")
|
||||||
|
is_default: bool = Field(False, description="是否设为默认")
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_company_fields(self) -> "InvoiceHeaderCreate":
|
||||||
|
if self.type == "company" and not self.tax_no:
|
||||||
|
raise ValueError("企业抬头必须填写税号")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceHeaderUpdate(BaseModel):
|
||||||
|
"""更新发票抬头请求。"""
|
||||||
|
name: str | None = Field(None, min_length=1, max_length=128, description="抬头名称")
|
||||||
|
tax_no: str | None = Field(None, max_length=32, description="税号")
|
||||||
|
register_address: str | None = Field(None, max_length=256, description="注册地址")
|
||||||
|
register_phone: str | None = Field(None, max_length=32, description="注册电话")
|
||||||
|
bank_name: str | None = Field(None, max_length=128, description="开户行")
|
||||||
|
bank_account: str | None = Field(None, max_length=64, description="银行账号")
|
||||||
|
email: str | None = Field(None, max_length=128, description="接收邮箱")
|
||||||
|
is_default: bool | None = Field(None, description="是否设为默认")
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceHeaderOut(BaseModel):
|
||||||
|
"""发票抬头响应体。"""
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
type: str
|
||||||
|
name: str
|
||||||
|
tax_no: str | None = None
|
||||||
|
register_address: str | None = None
|
||||||
|
register_phone: str | None = None
|
||||||
|
bank_name: str | None = None
|
||||||
|
bank_account: str | None = None
|
||||||
|
email: str | None = None
|
||||||
|
is_default: bool = False
|
||||||
|
created_at: NaiveDatetimeOptional = None
|
||||||
|
updated_at: NaiveDatetimeOptional = None
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import select, update
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.invoice_header import InvoiceHeader
|
||||||
|
from app.schemas.invoice import InvoiceHeaderCreate, InvoiceHeaderUpdate
|
||||||
|
from app.utils.id import generate_id
|
||||||
|
|
||||||
|
logger = logging.getLogger("videogen")
|
||||||
|
|
||||||
|
|
||||||
|
async def create_header(db: AsyncSession, user_id: str, data: InvoiceHeaderCreate) -> InvoiceHeader:
|
||||||
|
"""创建发票抬头。"""
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
header = InvoiceHeader(
|
||||||
|
id=generate_id(),
|
||||||
|
user_id=user_id,
|
||||||
|
type=data.type,
|
||||||
|
name=data.name,
|
||||||
|
tax_no=data.tax_no,
|
||||||
|
register_address=data.register_address,
|
||||||
|
register_phone=data.register_phone,
|
||||||
|
bank_name=data.bank_name,
|
||||||
|
bank_account=data.bank_account,
|
||||||
|
email=data.email,
|
||||||
|
is_default=data.is_default,
|
||||||
|
created_at=now,
|
||||||
|
updated_at=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果设为默认,先将其他抬头取消默认
|
||||||
|
if data.is_default:
|
||||||
|
await db.execute(
|
||||||
|
update(InvoiceHeader)
|
||||||
|
.where(InvoiceHeader.user_id == user_id)
|
||||||
|
.values(is_default=False, updated_at=now)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(header)
|
||||||
|
await db.flush()
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_headers(db: AsyncSession, user_id: str) -> list[InvoiceHeader]:
|
||||||
|
"""获取用户的所有发票抬头。"""
|
||||||
|
result = await db.execute(
|
||||||
|
select(InvoiceHeader)
|
||||||
|
.where(InvoiceHeader.user_id == user_id)
|
||||||
|
.order_by(InvoiceHeader.is_default.desc(), InvoiceHeader.created_at.desc())
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
|
async def get_header_by_id(db: AsyncSession, header_id: str, user_id: str) -> InvoiceHeader | None:
|
||||||
|
"""获取指定发票抬头(仅限本人)。"""
|
||||||
|
result = await db.execute(
|
||||||
|
select(InvoiceHeader).where(
|
||||||
|
InvoiceHeader.id == header_id,
|
||||||
|
InvoiceHeader.user_id == user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def update_header(
|
||||||
|
db: AsyncSession, header_id: str, user_id: str, data: InvoiceHeaderUpdate
|
||||||
|
) -> InvoiceHeader:
|
||||||
|
"""更新发票抬头。"""
|
||||||
|
header = await get_header_by_id(db, header_id, user_id)
|
||||||
|
if not header:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="发票抬头不存在")
|
||||||
|
|
||||||
|
update_data = {}
|
||||||
|
for field, value in data.model_dump(exclude_unset=True).items():
|
||||||
|
update_data[field] = value
|
||||||
|
|
||||||
|
if update_data:
|
||||||
|
update_data["updated_at"] = datetime.now(timezone.utc)
|
||||||
|
await db.execute(
|
||||||
|
update(InvoiceHeader)
|
||||||
|
.where(InvoiceHeader.id == header_id)
|
||||||
|
.values(**update_data)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果设为默认,先将其他抬头取消默认
|
||||||
|
if data.is_default:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
await db.execute(
|
||||||
|
update(InvoiceHeader)
|
||||||
|
.where(InvoiceHeader.user_id == user_id, InvoiceHeader.id != header_id)
|
||||||
|
.values(is_default=False, updated_at=now)
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.refresh(header)
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_header(db: AsyncSession, header_id: str, user_id: str) -> None:
|
||||||
|
"""删除发票抬头。"""
|
||||||
|
header = await get_header_by_id(db, header_id, user_id)
|
||||||
|
if not header:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="发票抬头不存在")
|
||||||
|
|
||||||
|
await db.delete(header)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
|
||||||
|
async def set_default_header(db: AsyncSession, header_id: str, user_id: str) -> InvoiceHeader:
|
||||||
|
"""设置默认发票抬头。"""
|
||||||
|
header = await get_header_by_id(db, header_id, user_id)
|
||||||
|
if not header:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="发票抬头不存在")
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
# 先取消其他默认
|
||||||
|
await db.execute(
|
||||||
|
update(InvoiceHeader)
|
||||||
|
.where(InvoiceHeader.user_id == user_id, InvoiceHeader.id != header_id)
|
||||||
|
.values(is_default=False, updated_at=now)
|
||||||
|
)
|
||||||
|
# 设置当前为默认
|
||||||
|
header.is_default = True
|
||||||
|
header.updated_at = now
|
||||||
|
await db.flush()
|
||||||
|
return header
|
||||||
-613
File diff suppressed because one or more lines are too long
+613
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -27,7 +27,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
<title>民众智创</title>
|
<title>民众智创</title>
|
||||||
<script type="module" crossorigin src="/assets/index-Chp4mfc5.js"></script>
|
<script type="module" crossorigin src="/assets/index-D5aCA_p5.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DtYH0-uL.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DtYH0-uL.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -497,6 +497,66 @@ export async function getInvoiceDetail(id: string): Promise<any> {
|
|||||||
return api.get(`/invoices/${id}`);
|
return api.get(`/invoices/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Invoice Headers ──────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getInvoiceHeaders(): Promise<{ items: any[] }> {
|
||||||
|
return api.get('/invoice-headers');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createInvoiceHeader(data: {
|
||||||
|
type: 'personal' | 'company';
|
||||||
|
name: string;
|
||||||
|
taxNo?: string;
|
||||||
|
registerAddress?: string;
|
||||||
|
registerPhone?: string;
|
||||||
|
bankName?: string;
|
||||||
|
bankAccount?: string;
|
||||||
|
email?: string;
|
||||||
|
isDefault?: boolean;
|
||||||
|
}): Promise<any> {
|
||||||
|
return api.post('/invoice-headers', {
|
||||||
|
type: data.type,
|
||||||
|
name: data.name,
|
||||||
|
tax_no: data.taxNo,
|
||||||
|
register_address: data.registerAddress,
|
||||||
|
register_phone: data.registerPhone,
|
||||||
|
bank_name: data.bankName,
|
||||||
|
bank_account: data.bankAccount,
|
||||||
|
email: data.email,
|
||||||
|
is_default: data.isDefault ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateInvoiceHeader(id: string, data: {
|
||||||
|
name?: string;
|
||||||
|
taxNo?: string;
|
||||||
|
registerAddress?: string;
|
||||||
|
registerPhone?: string;
|
||||||
|
bankName?: string;
|
||||||
|
bankAccount?: string;
|
||||||
|
email?: string;
|
||||||
|
isDefault?: boolean;
|
||||||
|
}): Promise<any> {
|
||||||
|
return api.put(`/invoice-headers/${id}`, {
|
||||||
|
name: data.name,
|
||||||
|
tax_no: data.taxNo,
|
||||||
|
register_address: data.registerAddress,
|
||||||
|
register_phone: data.registerPhone,
|
||||||
|
bank_name: data.bankName,
|
||||||
|
bank_account: data.bankAccount,
|
||||||
|
email: data.email,
|
||||||
|
is_default: data.isDefault,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteInvoiceHeader(id: string): Promise<void> {
|
||||||
|
await api.delete(`/invoice-headers/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setDefaultInvoiceHeader(id: string): Promise<any> {
|
||||||
|
return api.put(`/invoice-headers/${id}/set-default`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCreditRatios(): Promise<any[]> {
|
export async function getCreditRatios(): Promise<any[]> {
|
||||||
return api.get('/credits/ratios');
|
return api.get('/credits/ratios');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Table, Tag, Empty, Spin, Pagination, Button, Typography, Modal, Form, Input, Radio, message, Space, DatePicker } from 'antd';
|
import { Table, Tag, Empty, Spin, Pagination, Button, Typography, Modal, Form, Input, Radio, message, Space, DatePicker } from 'antd';
|
||||||
import { FileTextOutlined, PlusOutlined, CloseOutlined, AlipayCircleOutlined, WechatOutlined } from '@ant-design/icons';
|
import { FileTextOutlined, PlusOutlined, CloseOutlined, AlipayCircleOutlined, WechatOutlined } from '@ant-design/icons';
|
||||||
import { getPaymentOrders, createInvoice, getInvoices, getInvoiceDetail } from '../api';
|
import {
|
||||||
import dayjs from 'dayjs';
|
getPaymentOrders, createInvoice, getInvoices, getInvoiceDetail,
|
||||||
|
getInvoiceHeaders, createInvoiceHeader, updateInvoiceHeader, deleteInvoiceHeader, setDefaultInvoiceHeader,
|
||||||
|
} from '../api';
|
||||||
|
|
||||||
interface InvoiceRecord {
|
interface InvoiceRecord {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -33,13 +35,6 @@ interface InvoiceHeader {
|
|||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockHeaders: InvoiceHeader[] = [
|
|
||||||
{ id: 'h1', name: '北京云图科技有限公司', type: 'company', taxNo: '91110108MA01XXXXX1', registerAddress: '北京市海淀区中关村大街1号', bankName: '中国工商银行北京海淀支行', registerPhone: '010-88888888', bankAccount: '0200 0000 0000 0001', isDefault: true },
|
|
||||||
{ id: 'h2', name: '上海智能图像有限公司', type: 'company', taxNo: '91310115MA1KXXXXX2', registerAddress: '上海市浦东新区张江高科技园区', bankName: '招商银行上海张江支行', registerPhone: '021-66666666', bankAccount: '1212 0000 0000 0002', isDefault: false },
|
|
||||||
{ id: 'h3', name: '深圳创新科技有限公司', type: 'company', taxNo: '91440300MA5XXXXX3', registerAddress: '深圳市南山区科技园', bankName: '建设银行深圳南山支行', registerPhone: '0755-99999999', bankAccount: '4400 0000 0000 0003', isDefault: false },
|
|
||||||
{ id: 'h4', name: '张三', type: 'personal', isDefault: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
const InvoicePage: React.FC = () => {
|
const InvoicePage: React.FC = () => {
|
||||||
const [records, setRecords] = useState<InvoiceRecord[]>([]);
|
const [records, setRecords] = useState<InvoiceRecord[]>([]);
|
||||||
const [recordsLoading, setRecordsLoading] = useState(false);
|
const [recordsLoading, setRecordsLoading] = useState(false);
|
||||||
@@ -63,14 +58,9 @@ const InvoicePage: React.FC = () => {
|
|||||||
const [orderPage, setOrderPage] = useState(1);
|
const [orderPage, setOrderPage] = useState(1);
|
||||||
const [orderPageSize] = useState(10);
|
const [orderPageSize] = useState(10);
|
||||||
const [occupiedOrderIds, setOccupiedOrderIds] = useState<Set<string>>(new Set());
|
const [occupiedOrderIds, setOccupiedOrderIds] = useState<Set<string>>(new Set());
|
||||||
// 默认日期范围:当前月份
|
// 日期筛选:默认不选,展示全部订单
|
||||||
const [orderDateFilter, setOrderDateFilter] = useState<[string | null, string | null]>(() => {
|
const [orderDateFilter, setOrderDateFilter] = useState<[string | null, string | null]>([null, null]);
|
||||||
const now = new Date();
|
const [orderDateRange, setOrderDateRange] = useState<any>([null, null]);
|
||||||
const start = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
|
|
||||||
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
||||||
const end = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
|
|
||||||
return [start, end];
|
|
||||||
});
|
|
||||||
|
|
||||||
// 说明弹窗
|
// 说明弹窗
|
||||||
const [noticeModalOpen, setNoticeModalOpen] = useState(false);
|
const [noticeModalOpen, setNoticeModalOpen] = useState(false);
|
||||||
@@ -173,6 +163,7 @@ const InvoicePage: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadInvoices();
|
loadInvoices();
|
||||||
loadOccupiedOrderIds();
|
loadOccupiedOrderIds();
|
||||||
|
loadHeaders();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -418,46 +409,57 @@ const InvoicePage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 选择弹窗中的抬头列表(合并 mockHeaders 和用户添加的)
|
// 选择弹窗中的抬头列表
|
||||||
const allHeadersForSelect = React.useMemo(() => {
|
const allHeadersForSelect = headers;
|
||||||
const merged = [...mockHeaders];
|
|
||||||
headers.forEach(h => {
|
// 加载发票抬头列表
|
||||||
const idx = merged.findIndex(m => m.id === h.id);
|
const loadHeaders = async () => {
|
||||||
if (idx >= 0) {
|
try {
|
||||||
merged[idx] = h;
|
const data = await getInvoiceHeaders();
|
||||||
} else {
|
const items: InvoiceHeader[] = (data.items || []).map((h: any) => ({
|
||||||
merged.push(h);
|
id: h.id,
|
||||||
}
|
name: h.name,
|
||||||
});
|
type: h.type,
|
||||||
return merged;
|
taxNo: h.taxNo,
|
||||||
}, [headers]);
|
registerAddress: h.registerAddress,
|
||||||
|
bankName: h.bankName,
|
||||||
|
registerPhone: h.registerPhone,
|
||||||
|
bankAccount: h.bankAccount,
|
||||||
|
email: h.email,
|
||||||
|
isDefault: h.isDefault,
|
||||||
|
}));
|
||||||
|
setHeaders(items);
|
||||||
|
} catch {
|
||||||
|
// 静默失败
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddHeader = async () => {
|
const handleAddHeader = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await headerForm.validateFields();
|
const values = await headerForm.validateFields();
|
||||||
const isCompany = values.type === 'company';
|
const isCompany = values.type === 'company';
|
||||||
const newHeader: InvoiceHeader = {
|
const isDefault = values.isDefault === 'yes' || (headers.length === 0 && !headers.some(h => h.isDefault));
|
||||||
id: Date.now().toString(),
|
await createInvoiceHeader({
|
||||||
name: values.name,
|
|
||||||
type: values.type,
|
type: values.type,
|
||||||
|
name: values.name,
|
||||||
taxNo: isCompany ? values.taxNo : undefined,
|
taxNo: isCompany ? values.taxNo : undefined,
|
||||||
registerAddress: isCompany ? values.registerAddress : undefined,
|
registerAddress: isCompany ? values.registerAddress : undefined,
|
||||||
bankName: isCompany ? values.bankName : undefined,
|
bankName: isCompany ? values.bankName : undefined,
|
||||||
registerPhone: isCompany ? values.registerPhone : undefined,
|
registerPhone: isCompany ? values.registerPhone : undefined,
|
||||||
bankAccount: isCompany ? values.bankAccount : undefined,
|
bankAccount: isCompany ? values.bankAccount : undefined,
|
||||||
email: values.email,
|
email: values.email,
|
||||||
isDefault: values.isDefault === 'yes' || (headers.length === 0 && !allHeadersForSelect.some(h => h.isDefault)),
|
isDefault,
|
||||||
};
|
});
|
||||||
if (newHeader.isDefault) {
|
|
||||||
setHeaders(prev => prev.map(h => ({ ...h, isDefault: false })));
|
|
||||||
}
|
|
||||||
setHeaders(prev => [...prev, newHeader]);
|
|
||||||
headerForm.resetFields();
|
headerForm.resetFields();
|
||||||
setAddModalOpen(false);
|
setAddModalOpen(false);
|
||||||
message.success('发票抬头添加成功');
|
message.success('发票抬头添加成功');
|
||||||
|
loadHeaders();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.errorFields) {
|
if (e?.errorFields) {
|
||||||
message.warning('请完善表单信息');
|
message.warning('请完善表单信息');
|
||||||
|
} else {
|
||||||
|
const msg = e?.message || e?.response?.data?.detail || '添加失败';
|
||||||
|
message.error(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -468,16 +470,26 @@ const InvoicePage: React.FC = () => {
|
|||||||
content: '确定要删除该发票抬头吗?',
|
content: '确定要删除该发票抬头吗?',
|
||||||
okText: '确定',
|
okText: '确定',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: () => {
|
onOk: async () => {
|
||||||
setHeaders(prev => prev.filter(h => h.id !== id));
|
try {
|
||||||
message.success('删除成功');
|
await deleteInvoiceHeader(id);
|
||||||
|
message.success('删除成功');
|
||||||
|
loadHeaders();
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '删除失败');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetDefault = (id: string) => {
|
const handleSetDefault = async (id: string) => {
|
||||||
setHeaders(prev => prev.map(h => ({ ...h, isDefault: h.id === id })));
|
try {
|
||||||
message.success('已设为默认');
|
await setDefaultInvoiceHeader(id);
|
||||||
|
message.success('已设为默认');
|
||||||
|
loadHeaders();
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '设置失败');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 开票流程:选择订单后确认 → 打开抬头选择弹窗
|
// 开票流程:选择订单后确认 → 打开抬头选择弹窗
|
||||||
@@ -838,23 +850,22 @@ const InvoicePage: React.FC = () => {
|
|||||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>支付时间:</span>
|
<span style={{ color: '#94a3b8', fontSize: 13 }}>支付时间:</span>
|
||||||
<DatePicker.RangePicker
|
<DatePicker.RangePicker
|
||||||
size="small"
|
size="small"
|
||||||
value={orderDateFilter[0] ? [dayjs(orderDateFilter[0]), orderDateFilter[1] ? dayjs(orderDateFilter[1]) : null] : [null, null]}
|
value={orderDateRange}
|
||||||
onChange={(dates) => {
|
onChange={(dates) => {
|
||||||
|
setOrderDateRange(dates);
|
||||||
if (dates && dates[0] && dates[1]) {
|
if (dates && dates[0] && dates[1]) {
|
||||||
const start = dates[0].format('YYYY-MM-DD');
|
const start = dates[0].format('YYYY-MM-DD');
|
||||||
const end = dates[1].format('YYYY-MM-DD');
|
const end = dates[1].format('YYYY-MM-DD');
|
||||||
setOrderDateFilter([start, end]);
|
setOrderDateFilter([start, end]);
|
||||||
} else {
|
} else {
|
||||||
// 清除时恢复默认当前月份
|
// 清除时展示全部
|
||||||
const now = new Date();
|
setOrderDateFilter([null, null]);
|
||||||
const start = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
|
|
||||||
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
||||||
const end = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
|
|
||||||
setOrderDateFilter([start, end]);
|
|
||||||
}
|
}
|
||||||
setOrderPage(1);
|
setOrderPage(1);
|
||||||
setTimeout(() => loadOrderData(1), 0);
|
setTimeout(() => loadOrderData(1), 0);
|
||||||
}}
|
}}
|
||||||
|
allowClear
|
||||||
|
placeholder={['开始日期', '结束日期']}
|
||||||
style={{ width: 280 }}
|
style={{ width: 280 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user