319 lines
11 KiB
TypeScript
319 lines
11 KiB
TypeScript
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>
|
||
);
|
||
}
|