Files
dukang/apps/mini-user/src/pages/invoice-apply/index.tsx
T
jacy 62e61a9e63
CI / verify (pull_request) Waiting to run
v3.5.4版本提交,修改api
2026-08-21 16:22:53 +08:00

319 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react';
import { View, Text, Input, Radio } from '@tarojs/components';
import '../../styles/invoice.css';
import Taro, { useDidShow, useRouter } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import { request, toast } from '../../lib/api';
import { usePageView } from '../../lib/usePageView';
import {
INVOICE_CATEGORY_LABELS,
INVOICE_TITLE_TYPE_LABELS,
type InvoiceCategory,
type UserInvoiceTitleDto,
} from '@dukang/shared-types';
const isH5 = process.env.TARO_ENV === 'h5';
type InvoiceStatus = {
exists: boolean;
status?: string | null;
};
type OrderBrief = {
orderNo?: string;
productName?: string;
productSpec?: string;
quantity?: number;
payAmount?: number;
status?: string;
};
const INVOICE_STATUS_LABELS: Record<string, string> = {
PENDING: '开票中',
ISSUED: '已开票',
REJECTED: '已驳回',
};
export default function InvoiceApplyPage() {
const router = useRouter();
const orderId = router.params.orderId ?? '';
usePageView('user_invoice_apply_view', orderId ? { orderId } : undefined);
const [titles, setTitles] = useState<UserInvoiceTitleDto[]>([]);
const [selectedId, setSelectedId] = useState('');
const [invoiceCategory, setInvoiceCategory] = useState<InvoiceCategory>('LIQUOR');
const [remark, setRemark] = useState('');
const [emailOverride, setEmailOverride] = useState('');
const [phoneOverride, setPhoneOverride] = useState('');
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [invoiceStatus, setInvoiceStatus] = useState<InvoiceStatus | null>(null);
const [order, setOrder] = useState<OrderBrief | null>(null);
const load = useCallback(() => {
if (!orderId) return;
setLoading(true);
Promise.all([
request<UserInvoiceTitleDto[]>(`/trade/orders/${orderId}/invoice-titles`).catch(() => []),
request<InvoiceStatus>(`/trade/orders/${orderId}/invoice-status`).catch(() => ({ exists: false })),
request<OrderBrief>(`/trade/orders/${orderId}`).catch(() => ({})),
])
.then(([list, status, orderData]) => {
const titlesList = Array.isArray(list) ? list : [];
setTitles(titlesList);
setInvoiceStatus(status);
setOrder(orderData || {});
const def = titlesList.find((t) => t.isDefault);
setSelectedId(def?.id || (titlesList[0]?.id ?? ''));
})
.catch(() => {
setTitles([]);
setInvoiceStatus({ exists: false });
})
.finally(() => setLoading(false));
}, [orderId]);
useEffect(() => {
void load();
}, [load]);
useDidShow(() => {
void load();
});
const selected = titles.find((t) => t.id === selectedId);
async function submit() {
if (!orderId || submitting) return;
if (order?.status && order.status !== 'COMPLETED') {
toast('仅已完成订单可申请发票');
return;
}
if (!selected) {
toast('请先选择发票抬头');
return;
}
const email = (selected.email || emailOverride).trim();
const phone = (selected.phone || phoneOverride).trim();
if (!email) {
toast('请填写接收邮箱');
return;
}
if (!phone) {
toast('请填写联系电话');
return;
}
setSubmitting(true);
try {
await request(`/trade/orders/${orderId}/invoices`, {
method: 'POST',
data: {
titleId: selectedId,
invoiceKind: 'NORMAL',
invoiceCategory,
email,
phone,
remark: remark.trim() || undefined,
},
});
toast('发票申请已提交', 'success');
setTimeout(() => {
Taro.navigateBack().catch(() => {
Taro.redirectTo({ url: '/pages/orders/index?tab=completed' });
});
}, 1200);
} catch (e) {
toast(e instanceof Error ? e.message : '申请失败');
} finally {
setSubmitting(false);
}
}
function goAddTitle() {
Taro.navigateTo({
url: '/pages/invoice-titles/index',
});
}
const alreadyApplied = invoiceStatus?.exists;
const orderCompletable = !order?.status || order.status === 'COMPLETED';
const canApply = !alreadyApplied && orderCompletable;
const productLine = order
? [order.productName, order.productSpec, order.quantity ? `×${order.quantity}` : '']
.filter(Boolean)
.join(' ')
: '';
return (
<PageShell variant="sub" className="invoice-apply-page">
{isH5 ? (
<SubPageHeader
title="申请发票"
onBack={() => Taro.navigateBack().catch(() => Taro.switchTab({ url: '/pages/mine/index' }))}
/>
) : null}
<View className="invoice-apply-body">
{order?.orderNo ? (
<View className="invoice-apply-order">
<Text className="invoice-apply-order-label">订单编号</Text>
<Text className="invoice-apply-order-no">{order.orderNo}</Text>
{productLine ? (
<Text className="invoice-apply-order-meta">{productLine}</Text>
) : null}
{order.payAmount != null ? (
<Text className="invoice-apply-order-meta">
实付 ¥{Number(order.payAmount).toFixed(2)}
</Text>
) : null}
</View>
) : null}
{alreadyApplied ? (
<View className="invoice-apply-status-card">
<Text className="invoice-apply-status-icon"></Text>
<View>
<Text className="invoice-apply-status-title">已申请发票</Text>
<Text className="invoice-apply-status-desc">
状态:{INVOICE_STATUS_LABELS[invoiceStatus?.status ?? ''] || invoiceStatus?.status || '处理中'}
</Text>
</View>
</View>
) : null}
{!alreadyApplied && !orderCompletable ? (
<View className="invoice-apply-status-card">
<View>
<Text className="invoice-apply-status-title">当前订单不可开票</Text>
<Text className="invoice-apply-status-desc">仅已完成订单可申请发票</Text>
</View>
</View>
) : null}
{canApply ? (
<>
<View className="invoice-title-field" style={{ marginBottom: 16 }}>
<Text className="invoice-title-label">发票类型</Text>
<Text className="invoice-kind-static">增值税普通发票</Text>
</View>
<View className="invoice-title-field" style={{ marginBottom: 16 }}>
<Text className="invoice-title-label">类型</Text>
<View className="invoice-title-type-row">
{(['LIQUOR', 'CATERING'] as InvoiceCategory[]).map((k) => (
<Text
key={k}
className={`invoice-title-type-chip${invoiceCategory === k ? ' active' : ''}`}
onClick={() => setInvoiceCategory(k)}
>
{INVOICE_CATEGORY_LABELS[k]}
</Text>
))}
</View>
</View>
</>
) : null}
{canApply ? (
<View className="invoice-apply-section-title">
<Text>选择发票抬头</Text>
<Text className="invoice-apply-add-link" onClick={goAddTitle}>
+ 新增抬头
</Text>
</View>
) : null}
{loading ? <View className="u-empty">加载中…</View> : null}
{canApply && !loading && titles.length === 0 ? (
<View className="invoice-apply-empty-titles">
<Text className="u-empty">暂无发票抬头</Text>
<View className="invoice-apply-empty-cta" onClick={goAddTitle}>
去添加
</View>
</View>
) : null}
{canApply && !loading && titles.length > 0 ? (
<View className="invoice-apply-title-list">
{titles.map((t) => (
<View
key={t.id}
className={`invoice-apply-title-item${selectedId === t.id ? ' is-selected' : ''}`}
onClick={() => setSelectedId(t.id)}
>
<View className="invoice-apply-title-info">
<View className="invoice-apply-title-head">
<Text className="invoice-apply-title-name">{t.titleName}</Text>
<Text className="invoice-apply-title-type">
{INVOICE_TITLE_TYPE_LABELS[t.titleType] ?? t.titleType}
</Text>
{t.isDefault ? <Text className="invoice-apply-title-default">默认</Text> : null}
</View>
{t.taxNo ? <Text className="invoice-apply-title-sub">税号:{t.taxNo}</Text> : null}
{t.email ? <Text className="invoice-apply-title-sub">邮箱:{t.email}</Text> : null}
</View>
<Radio
checked={selectedId === t.id}
color="#A61D24"
onClick={() => setSelectedId(t.id)}
/>
</View>
))}
</View>
) : null}
{canApply && selected && !selected.email ? (
<View className="invoice-title-field" style={{ marginTop: 16 }}>
<Text className="invoice-title-label">
<Text className="invoice-title-star">*</Text>
接收邮箱
<Text className="invoice-title-label-hint">必填</Text>
</Text>
<Input
className="invoice-title-input"
placeholder="电子发票将发送至此邮箱"
value={emailOverride}
onInput={(e) => setEmailOverride(e.detail.value)}
/>
</View>
) : null}
{canApply && selected && !selected.phone ? (
<View className="invoice-title-field" style={{ marginTop: 12 }}>
<Text className="invoice-title-label">联系电话</Text>
<Input
className="invoice-title-input"
type="number"
placeholder="请填写联系电话"
value={phoneOverride}
onInput={(e) => setPhoneOverride(e.detail.value)}
/>
</View>
) : null}
{canApply ? (
<View className="invoice-title-field" style={{ marginTop: 16 }}>
<Text className="invoice-title-label">备注</Text>
<Input
className="invoice-title-input"
maxlength={200}
placeholder="选填,将显示在发票申请备注中"
value={remark}
onInput={(e) => setRemark(e.detail.value)}
/>
</View>
) : null}
</View>
{canApply && titles.length > 0 ? (
<View className="invoice-apply-footer">
<View
className={`invoice-apply-submit${submitting ? ' is-disabled' : ''}`}
onClick={() => { if (!submitting) void submit(); }}
>
{submitting ? '提交中…' : '提交申请'}
</View>
</View>
) : null}
</PageShell>
);
}