项目中心-支付模块

This commit is contained in:
wangzzpeng
2026-09-30 08:04:16 +08:00
parent 6b88c3805f
commit ce8210fb3f
35 changed files with 2618 additions and 175 deletions

View File

@@ -10,20 +10,25 @@ import type {
InvoiceTitle,
InvoiceTitlePayload,
PayMethod,
PayOrderResponse,
Payment,
PaymentResult,
} from '@/types/business/finance'
import {
applyInvoice as applyInvoiceApi,
createAlipayPageOrder as createAlipayPageOrderApi,
createInvoiceTitle,
createPayment as createPaymentApi,
createWechatNativeOrder as createWechatNativeOrderApi,
deleteInvoiceTitle,
exportReport as exportReportApi,
getBillDetail,
getBillList,
getCheckoutBills,
getFinanceStats,
getInvoiceList,
getInvoiceTitles,
getPlanPayResult,
getPayResult,
setDefaultTitle as setDefaultTitleApi,
switchPaymentMethod as switchPaymentMethodApi,
@@ -53,6 +58,13 @@ export const useFinanceStore = defineStore('finance', () => {
const payment = ref<Payment | null>(null)
const result = ref<PaymentResult | null>(null)
// —— 收银台(确认支付页)
/** 收银台待支付账单(由路由 `?ids=` 拉取) */
const checkoutBills = ref<Bill[]>([])
const checkoutLoading = ref(false)
/** 最近一次下单响应(微信 codeUrl / 支付宝跳转地址) */
const payOrder = ref<PayOrderResponse | null>(null)
// —— 发票
const invoices = ref<Invoice[]>([])
const invoiceTotal = ref(0)
@@ -108,8 +120,48 @@ export const useFinanceStore = defineStore('finance', () => {
payment.value = await switchPaymentMethodApi(billId, method)
}
async function fetchResult(billId: string): Promise<void> {
result.value = await getPayResult(billId)
async function fetchResult(billId: string, requirementId = ''): Promise<void> {
result.value = requirementId
? await getPlanPayResult(requirementId, billId)
: await getPayResult(billId)
}
// —— 收银台(确认支付)
/**
* 拉取收银台订单明细。
*
* <p>入参是**需求 id**(项目详情路由 `:id`):数据走项目详情那个现成接口
* `GET /web/requirementPool/get/finance/{需求id}` 的 `billPlans`,不另开端点。
* `ids` 为可选的期次 id(`vm_bill_plan.id`),用于精确指定付哪一期;
* 不传则取该需求下状态为「待支付」的期。</p>
*/
async function loadCheckout(requirementId: string, ids: string[] = []): Promise<void> {
checkoutLoading.value = true
try {
checkoutBills.value = await getCheckoutBills(requirementId, ids)
} finally {
checkoutLoading.value = false
}
}
/**
* 微信 Native 下单(拿到 codeUrl 后由页面渲染二维码)。
*
* @param billIds 本笔支付覆盖的账单 id。后端据此落 `vm_finance_payment` 并把它写进
* `request_payload`,支付成功回调再为每张账单落 `vm_finance_payment_bill`。
*/
async function payByWechatNative(orderNo: string, amount: number, billIds: string[]): Promise<void> {
payOrder.value = await createWechatNativeOrderApi(orderNo, amount, billIds)
}
/** 支付宝电脑网站支付下单(拿到跳转地址后由页面跳转) */
async function payByAlipayPage(orderNo: string, amount: number, billIds: string[]): Promise<void> {
payOrder.value = await createAlipayPageOrderApi(orderNo, amount, billIds)
}
/** 清空下单响应(关闭二维码弹窗时调用) */
function clearPayOrder(): void {
payOrder.value = null
}
// —— 发票
@@ -178,6 +230,9 @@ export const useFinanceStore = defineStore('finance', () => {
currentBill,
payment,
result,
checkoutBills,
checkoutLoading,
payOrder,
invoices,
invoiceTotal,
invoiceQuery,
@@ -192,6 +247,10 @@ export const useFinanceStore = defineStore('finance', () => {
createPayment,
switchPaymentMethod,
fetchResult,
loadCheckout,
payByWechatNative,
payByAlipayPage,
clearPayOrder,
fetchInvoices,
setInvoiceQuery,
searchInvoices,

114
src/stores/modules/oss.ts Normal file
View File

@@ -0,0 +1,114 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { fetchOssPreviewBlob } from '@/api/oss'
import { resolvePreviewKind, PREVIEW_TEXT_MAX_BYTES, type PreviewKind } from '@/utils/filePreview'
import type { FileInfo } from '@/types/api'
/**
* OSS 文件预览全局状态。
*
* <p>所有 `FileList` 的「预览」统一走这里,弹一个全局 `FilePreviewDialog`:
* 有 `ossId` 的文件经同源后端接口取 blob 内联渲染(绕开桶强制下载);
* 无 `ossId` 的图片可退化为公网直链(子资源渲染不受强制下载影响),其余提示「不支持在线预览」。</p>
*/
export const useOssStore = defineStore('oss', () => {
const visible = ref(false)
const file = ref<FileInfo | null>(null)
const kind = ref<PreviewKind>('unsupported')
const objectUrl = ref<string | null>(null)
const textContent = ref<string | null>(null)
const loading = ref(false)
const error = ref('')
/** 文本类文件超过预览上限被截断渲染 */
const truncated = ref(false)
/** 被截断文件的实际大小(字节) */
const totalBytes = ref(0)
function revokeObjectUrl(): void {
if (objectUrl.value && objectUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(objectUrl.value)
}
objectUrl.value = null
}
async function openPreview(target: FileInfo): Promise<void> {
closePreview()
file.value = target
visible.value = true
loading.value = true
error.value = ''
textContent.value = null
truncated.value = false
totalBytes.value = 0
const previewKind = resolvePreviewKind(target.name)
kind.value = previewKind
// 不支持内联渲染(office / 未知类型):直接提示,等用户下载
if (previewKind === 'unsupported') {
loading.value = false
return
}
// 无 ossId:图片可走公网直链(<img> 子资源渲染不受强制下载影响),其余无法预览
if (target.ossId === undefined || target.ossId === null) {
if (previewKind === 'image' && target.url) {
objectUrl.value = target.url
loading.value = false
return
}
error.value = '该文件暂不支持在线预览,请使用下载'
loading.value = false
return
}
try {
const blob = await fetchOssPreviewBlob(target.ossId)
if (previewKind === 'text') {
// 整文件塞进 <pre> 会卡死浏览器(如 5MB+ 的 .sql/.log),
// 仅读取前 PREVIEW_TEXT_MAX_BYTES 渲染,超出截断并提示下载
const total = blob.size
if (total > PREVIEW_TEXT_MAX_BYTES) {
textContent.value = await blob.slice(0, PREVIEW_TEXT_MAX_BYTES).text()
truncated.value = true
totalBytes.value = total
} else {
textContent.value = await blob.text()
}
} else {
// 图片 / PDF / 音视频由浏览器原生处理,5MB 量级毫无压力
objectUrl.value = URL.createObjectURL(blob)
}
loading.value = false
} catch {
error.value = '预览加载失败,请使用下载'
loading.value = false
}
}
function closePreview(): void {
revokeObjectUrl()
visible.value = false
file.value = null
kind.value = 'unsupported'
textContent.value = null
truncated.value = false
totalBytes.value = 0
loading.value = false
error.value = ''
}
return {
visible,
file,
kind,
objectUrl,
textContent,
loading,
error,
truncated,
totalBytes,
openPreview,
closePreview,
}
})