feat: 修复了公账支付的东西
Some checks are pending
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Test (windows-latest) (push) Waiting to run
CI / Lint (ubuntu-latest) (push) Waiting to run
CI / Lint (windows-latest) (push) Waiting to run
CI / Check (ubuntu-latest) (push) Waiting to run
CI / Check (windows-latest) (push) Waiting to run
CI / CI OK (push) Blocked by required conditions
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Deploy Website on push / Deploy Push Playground Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Docs Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Antd Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Element Ftp (push) Waiting to run
Deploy Website on push / Deploy Push Naive Ftp (push) Waiting to run
Deploy Website on push / Rerun on failure (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run

This commit is contained in:
2026-09-14 08:57:46 +08:00
parent 2d8a328db2
commit aaaa00f789
6 changed files with 661 additions and 403 deletions

View File

@@ -7,37 +7,43 @@ import { onMounted, ref } from 'vue';
import { Button } from 'ant-design-vue';
import DailyUploadForm from '#/views/finance/public-account-pay/components/daily-upload-form.vue';
import { buildDailyUploadFormData } from '#/views/finance/public-account-pay/utils/daily-upload';
const props = defineProps<{
dailyId: number;
storeName?: string;
platformAmount?: string;
billDateTxt?: string;
dailyId: number;
platformAmount?: string;
storeName?: string;
unlockMode?: string;
}>();
const emit = defineEmits<{
back: [];
waiting: [payload?: {
bill_status?: number;
contact_phones?: { name?: string; phone: string }[];
}];
waiting: [
payload?: {
bill_status?: number;
contact_phones?: { name?: string; phone: string }[];
},
];
}>();
const formRef = ref<InstanceType<typeof DailyUploadForm>>();
const submitting = ref(false);
const unlockHint = props.unlockMode === 'voucher'
? '提交成功后立刻解锁,接诊和开方可继续。'
: '提交后仍锁定,需平台管理员确认到账后才会解锁。';
const unlockHint =
props.unlockMode === 'voucher'
? '提交成功后立刻解锁,接诊和开方可继续。'
: '提交后仍锁定,需平台管理员确认到账后才会解锁。';
onMounted(() => {
formRef.value?.load({
id: props.dailyId,
store_name: props.storeName,
platform_amount: props.platformAmount,
bill_date_txt: props.billDateTxt,
});
formRef.value?.load(
buildDailyUploadFormData({
id: props.dailyId,
store_name: props.storeName,
platform_amount: props.platformAmount,
bill_date_txt: props.billDateTxt,
}),
);
});
/** 可无附件直接声明已支付;若已选文件一并提交 */
@@ -51,9 +57,9 @@ async function onDeclarePaid() {
}
function onFormSuccess(payload: {
unlocked: boolean;
bill_status?: number;
contact_phones?: { name?: string; phone: string }[];
unlocked: boolean;
}) {
if (!payload.unlocked) {
emit('waiting', payload);
@@ -73,7 +79,9 @@ function onFormSuccess(payload: {
@success="onFormSuccess"
/>
<div class="pap-lock-pay-actions">
<Button size="large" class="pap-lock-btn is-ghost" @click="emit('back')">返回说明</Button>
<Button size="large" class="pap-lock-btn is-ghost" @click="emit('back')">
返回说明
</Button>
<Button
type="primary"
size="large"
@@ -96,19 +104,21 @@ function onFormSuccess(payload: {
max-width: 100%;
text-align: left;
}
.pap-lock-badge {
display: inline-block;
padding: 3px 10px;
margin-bottom: var(--pap-gap-sm, 14px);
font-size: var(--pap-fs-badge, 12px);
font-weight: 600;
letter-spacing: 0.08em;
color: hsl(var(--pap-accent));
letter-spacing: 0.08em;
background: hsl(var(--pap-accent) / 12%);
border: 1px solid hsl(var(--pap-accent) / 40%);
border-radius: 999px;
background: hsl(var(--pap-accent) / 12%);
box-shadow: 0 0 6px hsl(var(--pap-accent) / 18%);
}
.pap-lock-title {
margin: 0 0 var(--pap-gap-sm, 12px);
font-size: var(--pap-fs-title, 28px);
@@ -116,27 +126,32 @@ function onFormSuccess(payload: {
line-height: 1.25;
color: hsl(var(--foreground));
}
.pap-lock-desc {
margin: 0 0 var(--pap-gap-md, 20px);
font-size: var(--pap-fs-desc, 15px);
line-height: 1.55;
color: hsl(var(--muted-foreground));
}
.pap-lock-pay-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--pap-gap-sm, 12px);
margin-top: var(--pap-gap-md, 20px);
}
.pap-lock-btn {
width: 100%;
height: var(--pap-btn-h, 48px);
font-size: var(--pap-fs-btn, 16px);
}
.pap-lock-btn.is-ghost {
color: hsl(var(--foreground));
border-color: hsl(var(--pap-accent) / 40%);
}
.pap-lock-pay-tip {
margin: var(--pap-gap-sm, 12px) 0 0;
font-size: var(--pap-fs-label, 12px);
@@ -144,6 +159,7 @@ function onFormSuccess(payload: {
color: hsl(var(--muted-foreground));
text-align: center;
}
@media (max-width: 560px) {
.pap-lock-pay-actions {
grid-template-columns: 1fr;

View File

@@ -7,15 +7,15 @@ import { computed, ref, watch } from 'vue';
import { useUserStore } from '@vben/stores';
import { Button, Modal, message } from 'ant-design-vue';
import { Button, message, Modal } from 'ant-design-vue';
import { useAuthStore } from '#/store';
import {
PUBLIC_ACCOUNT_LOCK_PROMPT_Z_INDEX,
clearPublicAccountLock,
closePublicAccountLockHandle,
getPublicAccountLockState,
normalizeLockAlertColor,
PUBLIC_ACCOUNT_LOCK_PROMPT_Z_INDEX,
} from '#/utils/publicAccountLock';
import {
generatePublicAccountDailyBill,
@@ -23,15 +23,15 @@ import {
getPublicAccountLockStatus,
} from '#/views/finance/public-account-pay/api';
import LockOrdersPanel from './LockOrdersPanel.vue';
import LockPayPanel from './LockPayPanel.vue';
import LockWaitPanel from './LockWaitPanel.vue';
import LockOrdersPanel from './LockOrdersPanel.vue';
import LockWarningCanvas from './LockWarningCanvas.vue';
const userStore = useUserStore();
const authStore = useAuthStore();
const lockState = getPublicAccountLockState();
const panelMode = ref<'hint' | 'pay' | 'wait' | 'orders'>('hint');
const panelMode = ref<'hint' | 'orders' | 'pay' | 'wait'>('hint');
const payDailyId = ref(0);
const openingPay = ref(false);
const loggingOut = ref(false);
@@ -205,9 +205,9 @@ function backToHint() {
/** 只有这次上传成功且未解锁,才切到等待审核 */
function onWaiting(payload?: {
status?: number;
bill_status?: number;
contact_phones?: { name?: string; phone: string }[];
status?: number;
}) {
if (payload?.contact_phones?.length) {
lockState.payload.contact_phones = payload.contact_phones;
@@ -290,7 +290,7 @@ watch(
<div v-if="overlayOpen" class="pap-lock-mask">
<div
class="pap-lock-panel"
:class="['is-' + alertColor, { 'is-sub': panelMode !== 'hint' }]"
:class="[`is-${ alertColor}`, { 'is-sub': panelMode !== 'hint' }]"
role="alertdialog"
aria-modal="true"
>
@@ -304,6 +304,7 @@ watch(
<LockPayPanel
v-if="panelMode === 'pay'"
:daily-id="payDailyId"
:store-name="String(lockState.payload.store_name || '')"
:platform-amount="amountTxt"
:bill-date-txt="billDateRangeTxt"
:unlock-mode="unlockMode"
@@ -337,9 +338,7 @@ watch(
<template v-if="softHandle">
请尽快处理完成后可返回继续工作
</template>
<template v-else>
接诊开方已暂停本提示不可关闭
</template>
<template v-else> 接诊开方已暂停本提示不可关闭 </template>
<template v-if="mergeHint">
{{ mergeHint }}
</template>
@@ -366,9 +365,13 @@ watch(
后未处理即锁定更早的账单全天锁定
</li>
<li>诊所管理员在本提示中上传付款凭证转账截图</li>
<li v-if="unlockByVoucher">提交成功后立刻解锁接诊和开方可继续</li>
<li v-if="unlockByVoucher">
提交成功后立刻解锁接诊和开方可继续
</li>
<li v-else>提交后仍锁定等平台管理员确认到账后自动解锁</li>
<li v-if="!softHandle">未达解锁条件前无法关闭本提示也无法继续业务</li>
<li v-if="!softHandle">
未达解锁条件前无法关闭本提示也无法继续业务
</li>
</ol>
</div>
</template>
@@ -429,10 +432,10 @@ watch(
position: fixed;
inset: 0;
z-index: 2100;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: max(10px, env(safe-area-inset-top, 0px))
@@ -443,18 +446,10 @@ watch(
background: hsl(var(--background) / 62%);
backdrop-filter: blur(6px);
}
.pap-lock-panel {
position: relative;
display: flex;
flex-direction: column;
box-sizing: border-box;
width: min(820px, 100%);
max-width: 100%;
/* 相对遮罩内容盒,保证整框含警戒条都在视口内 */
max-height: 100%;
min-height: 0;
overflow: hidden;
--pap-accent: var(--warning);
/* 顶留白跟警示三角对齐:矮屏大幅压缩 */
--pap-pad-top-hint: clamp(52px, 9dvh + 20px, 150px);
--pap-pad-x: clamp(14px, 3vw, 40px);
@@ -476,84 +471,106 @@ watch(
--pap-btn-h: clamp(36px, 4.2dvh, 44px);
--pap-stat-cols: 1fr 1fr;
--pap-content-w: min(480px, 100%);
/* 给 canvas 上下警戒条留出露边,避免被底栏背景盖住 */
--pap-tape-h: 16px;
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
width: min(820px, 100%);
max-width: 100%;
min-height: 0;
/* 相对遮罩内容盒,保证整框含警戒条都在视口内 */
max-height: 100%;
overflow: hidden;
background: hsl(var(--card, var(--background)));
border: 1px solid hsl(var(--pap-accent) / 38%);
border-radius: clamp(10px, 1.2vw, 16px);
box-shadow: 0 0 6px hsl(var(--pap-accent) / 18%);
}
.pap-lock-panel.is-blue {
--pap-accent: 217 91% 53%;
}
.pap-lock-panel.is-yellow {
--pap-accent: var(--warning);
}
.pap-lock-panel.is-red {
--pap-accent: 0 72% 51%;
}
.pap-lock-panel.is-sub {
--pap-pad-top-hint: clamp(14px, 2dvh, 28px);
}
/* shell上滚下钉上下内边距露出警戒条动画 */
.pap-lock-shell {
position: relative;
z-index: 1;
box-sizing: border-box;
display: flex;
flex: 1 1 auto;
flex-direction: column;
box-sizing: border-box;
width: 100%;
min-height: 0;
max-height: 100%;
padding-top: var(--pap-tape-h);
padding-bottom: var(--pap-tape-h);
}
.pap-lock-scroll {
box-sizing: border-box;
display: flex;
flex: 1 1 auto;
flex-direction: column;
align-items: center;
box-sizing: border-box;
width: 100%;
min-height: 0;
padding: var(--pap-pad-top-hint) var(--pap-pad-x) var(--pap-gap-sm);
overflow-x: hidden;
overflow-y: auto;
overflow: hidden auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
text-align: center;
-webkit-overflow-scrolling: touch;
}
.pap-lock-foot {
box-sizing: border-box;
display: flex;
flex-shrink: 0;
flex-direction: column;
align-items: center;
box-sizing: border-box;
width: 100%;
padding: var(--pap-gap-sm) var(--pap-pad-x) var(--pap-pad-y);
border-top: 1px solid hsl(var(--pap-accent) / 22%);
/* 半透明,滚动区边缘仍能透出粒子;底边警戒条靠 shell padding 露白 */
background: hsl(var(--card, var(--background)) / 88%);
border-top: 1px solid hsl(var(--pap-accent) / 22%);
backdrop-filter: blur(4px);
}
.pap-lock-badge {
flex-shrink: 0;
padding: 3px 10px;
margin-bottom: var(--pap-gap-sm);
font-size: var(--pap-fs-badge);
font-weight: 600;
letter-spacing: 0.08em;
color: hsl(var(--pap-accent));
letter-spacing: 0.08em;
background: hsl(var(--pap-accent) / 12%);
border: 1px solid hsl(var(--pap-accent) / 40%);
border-radius: 999px;
background: hsl(var(--pap-accent) / 12%);
box-shadow: 0 0 6px hsl(var(--pap-accent) / 18%);
}
.pap-lock-hello {
max-width: min(560px, 100%);
margin: 0 0 var(--pap-gap-xs);
padding: 0 4px;
margin: 0 0 var(--pap-gap-xs);
font-size: var(--pap-fs-hello);
font-weight: 600;
line-height: 1.35;
@@ -561,6 +578,7 @@ watch(
word-break: break-word;
overflow-wrap: anywhere;
}
.pap-lock-title {
margin: 0 0 var(--pap-gap-xs);
font-size: var(--pap-fs-title);
@@ -568,6 +586,7 @@ watch(
line-height: 1.25;
color: hsl(var(--foreground));
}
.pap-lock-reason {
max-width: min(520px, 100%);
margin: 0 0 var(--pap-gap-xs);
@@ -575,6 +594,7 @@ watch(
line-height: 1.4;
color: hsl(var(--muted-foreground));
}
.pap-lock-desc {
max-width: min(520px, 100%);
margin: 0 0 var(--pap-gap-md);
@@ -582,6 +602,7 @@ watch(
line-height: 1.45;
color: hsl(var(--muted-foreground));
}
.pap-lock-stat {
display: grid;
grid-template-columns: var(--pap-stat-cols);
@@ -589,31 +610,38 @@ watch(
width: var(--pap-content-w);
margin-bottom: var(--pap-gap-md);
}
.pap-lock-stat-item {
min-width: 0;
padding: var(--pap-block-pad) calc(var(--pap-block-pad) + 2px);
text-align: left;
background: hsl(var(--card, var(--background)));
border: 1px solid hsl(var(--pap-accent) / 30%);
border-radius: 10px;
background: hsl(var(--card, var(--background)));
box-shadow: 0 0 6px hsl(var(--pap-accent) / 18%);
}
.pap-lock-stat-item.is-clickable {
cursor: pointer;
color: inherit;
font: inherit;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
color: inherit;
cursor: pointer;
transition:
border-color 0.15s ease,
box-shadow 0.15s ease;
}
.pap-lock-stat-item.is-clickable:hover {
border-color: hsl(var(--pap-accent) / 55%);
box-shadow: 0 0 10px hsl(var(--pap-accent) / 28%);
}
.pap-lock-stat-label {
display: block;
margin-bottom: var(--pap-gap-xs);
font-size: var(--pap-fs-label);
color: hsl(var(--muted-foreground));
}
.pap-lock-stat-item strong {
display: block;
font-size: var(--pap-fs-stat);
@@ -623,47 +651,55 @@ watch(
word-break: break-word;
overflow-wrap: anywhere;
}
.pap-lock-stat-item strong.is-amount {
color: hsl(var(--pap-accent));
}
.pap-lock-flow {
width: var(--pap-content-w);
padding: var(--pap-block-pad) calc(var(--pap-block-pad) + 2px);
margin-bottom: 0;
text-align: left;
background: hsl(var(--card, var(--background)));
border: 1px solid hsl(var(--pap-accent) / 30%);
border-radius: 10px;
background: hsl(var(--card, var(--background)));
box-shadow: 0 0 6px hsl(var(--pap-accent) / 18%);
}
.pap-lock-flow-title {
margin-bottom: var(--pap-gap-xs);
font-size: var(--pap-fs-flow);
font-weight: 600;
color: hsl(var(--pap-accent));
}
.pap-lock-flow-list {
margin: 0;
padding-left: 16px;
margin: 0;
font-size: var(--pap-fs-flow);
line-height: 1.45;
color: hsl(var(--muted-foreground));
}
.pap-lock-actions {
display: grid;
grid-template-columns: 1fr;
gap: var(--pap-gap-sm);
width: var(--pap-content-w);
}
.pap-lock-btn {
width: 100%;
height: var(--pap-btn-h);
font-size: var(--pap-fs-btn);
}
.pap-lock-btn.is-ghost {
color: hsl(var(--foreground));
border-color: hsl(var(--pap-accent) / 40%);
}
.pap-lock-hint {
max-width: var(--pap-content-w);
margin: 0;
@@ -671,9 +707,10 @@ watch(
line-height: 1.45;
color: hsl(var(--pap-accent));
}
.pap-lock-logout {
margin-top: var(--pap-gap-sm);
padding: 0;
margin-top: var(--pap-gap-sm);
font-size: var(--pap-fs-desc);
line-height: 1.4;
color: hsl(var(--muted-foreground));
@@ -682,23 +719,28 @@ watch(
background: transparent;
border: 0;
}
.pap-lock-logout:disabled {
opacity: 0.6;
cursor: not-allowed;
opacity: 0.6;
}
@media (max-height: 720px) {
.pap-lock-mask {
align-items: flex-start;
}
.pap-lock-panel {
/* 矮屏仍双列,省纵向高度;过窄才单列 */
--pap-pad-top-hint: clamp(44px, 7dvh + 16px, 96px);
}
}
@media (max-width: 560px) {
.pap-lock-mask {
padding: 8px;
}
.pap-lock-panel {
--pap-stat-cols: 1fr;
--pap-pad-x: 12px;
@@ -712,6 +754,7 @@ body.pap-lock-open .ant-message,
body.pap-lock-open .ant-notification {
z-index: 5000 !important;
}
/* 退出确认等 Modal 也要压过锁层 */
body.pap-lock-open .ant-modal-wrap,
body.pap-lock-open .ant-modal-mask {

View File

@@ -1,4 +1,6 @@
<script lang="ts" setup>
import type { DailyUploadRowLike } from '../utils/daily-upload';
/**
* 日汇总上传凭证表单:财务弹窗和锁店二级页共用
* 锁店页可无附件直接「我已支付」;财务弹窗默认仍建议传凭证
@@ -10,32 +12,43 @@ import { Image, message } from 'ant-design-vue';
import UploadDraggerPaste from '#/components/form/components/upload-dragger-paste.vue';
import { clearPublicAccountLock } from '#/utils/publicAccountLock';
import { getPublicAccountDailyBillInfo, uploadPublicAccountDailyBill } from '../api';
import {
getPublicAccountDailyBillInfo,
uploadPublicAccountDailyBill,
} from '../api';
import {
buildDailyUploadFormData,
formatDailyPlatformAmount,
normalizeDailyVoucherUrls,
resolveDailyBillDateTxt,
resolveDailyPlatformAmount,
resolveDailyStoreName,
resolveDailyUploadId,
} from '../utils/daily-upload';
export interface DailyUploadFormData {
export interface DailyUploadFormData extends DailyUploadRowLike {
id: number;
store_name?: string;
platform_amount?: string;
bill_date_txt?: string;
gridApi?: any;
}
const props = withDefaults(
defineProps<{
/** 预览层必须高于锁店遮罩 2100否则图会沉到底下 */
previewZIndex?: number;
/** 锁店页:允许不传文件直接声明已支付 */
allowEmptyVoucher?: boolean;
/** 预览层必须高于锁店遮罩 2100否则图会沉到底下 */
previewZIndex?: number;
}>(),
{ previewZIndex: 2500, allowEmptyVoucher: false },
);
const emit = defineEmits<{
success: [payload: {
unlocked: boolean;
bill_status?: number;
contact_phones?: { name?: string; phone: string }[];
}];
success: [
payload: {
bill_status?: number;
contact_phones?: { name?: string; phone: string }[];
unlocked: boolean;
},
];
}>();
const dailyId = ref(0);
@@ -56,37 +69,45 @@ const voucherHint = computed(() =>
: '支持转账截图JPG/PNG/WEBP与 PDF 回单,请至少上传 1 个文件。',
);
const displayStoreName = computed(() => storeName.value || '本店');
const displayBillDateTxt = computed(() => billDateTxt.value || '—');
const displayPlatformAmount = computed(() =>
formatDailyPlatformAmount(platformAmount.value),
);
function isImageUrl(url: string) {
return /\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(url || '');
}
/** 打开时灌入日汇总:锁店传入的周期金额/区间优先,详情只补凭证与店名 */
/** 打开时灌入日汇总:列表/锁店传入的周期金额/区间优先,详情只补凭证与店名 */
async function load(data?: DailyUploadFormData) {
dailyId.value = Number(data?.id || 0);
storeName.value = data?.store_name || '';
// 锁店页已算好周期合计,勿被单日详情盖掉
const lockAmount = String(data?.platform_amount || '').trim();
const lockDate = String(data?.bill_date_txt || '').trim();
platformAmount.value = lockAmount || '0.00';
billDateTxt.value = lockDate;
const normalized = buildDailyUploadFormData(data || {});
dailyId.value = resolveDailyUploadId(normalized);
storeName.value = resolveDailyStoreName(normalized);
billDateTxt.value = resolveDailyBillDateTxt(normalized);
platformAmount.value = resolveDailyPlatformAmount(normalized);
gridApi.value = data?.gridApi;
voucherUrls.value = [];
payRemark.value = '';
existVouchers.value = [];
const presetAmount = resolveDailyPlatformAmount(normalized);
const presetDate = resolveDailyBillDateTxt(normalized);
if (dailyId.value < 1) {
return;
}
loading.value = true;
try {
const detail = await getPublicAccountDailyBillInfo(dailyId.value);
existVouchers.value = detail?.vouchers || [];
storeName.value = detail?.store_name || storeName.value;
// 仅财务弹窗未带金额/日期时,才回落单日字段
if (!lockAmount || lockAmount === '0.00') {
platformAmount.value = String(detail?.platform_amount || platformAmount.value);
existVouchers.value = detail?.vouchers || detail?.payment_vouchers || [];
storeName.value = resolveDailyStoreName(detail) || storeName.value;
if (!presetAmount || presetAmount === '0.00') {
platformAmount.value = resolveDailyPlatformAmount(detail);
}
if (!lockDate) {
billDateTxt.value = detail?.bill_date_txt || billDateTxt.value;
if (!presetDate) {
billDateTxt.value = resolveDailyBillDateTxt(detail);
}
} finally {
loading.value = false;
@@ -97,8 +118,12 @@ async function load(data?: DailyUploadFormData) {
* 提交allowEmpty=true 时可无附件(我已支付)
*/
async function submit(options?: { allowEmpty?: boolean }): Promise<boolean> {
if (dailyId.value < 1) {
message.warning('无法识别日汇总,请刷新后重试');
return false;
}
const allowEmpty = Boolean(options?.allowEmpty || props.allowEmptyVoucher);
if (!voucherUrls.value.length && !allowEmpty) {
if (voucherUrls.value.length === 0 && !allowEmpty) {
message.warning('请至少上传 1 张付款凭证(截图或 PDF');
return false;
}
@@ -106,11 +131,11 @@ async function submit(options?: { allowEmpty?: boolean }): Promise<boolean> {
try {
const res = await uploadPublicAccountDailyBill({
id: dailyId.value,
voucher_urls: voucherUrls.value,
pay_remark: payRemark.value,
declare_paid: allowEmpty && !voucherUrls.value.length ? 1 : 0,
voucher_urls: normalizeDailyVoucherUrls(voucherUrls.value),
pay_remark: payRemark.value.trim(),
declare_paid: allowEmpty && voucherUrls.value.length === 0 ? 1 : 0,
});
message.success(voucherUrls.value.length ? '已上传' : '已声明支付');
message.success(voucherUrls.value.length > 0 ? '已上传' : '已声明支付');
if (res?.unlocked) {
clearPublicAccountLock();
}
@@ -132,11 +157,16 @@ defineExpose({ load, submit, submitting, loading, voucherUrls });
<template>
<div class="daily-upload-form">
<div v-if="loading" class="mb-3 text-sm text-[hsl(var(--muted-foreground))]">
<div
v-if="loading"
class="mb-3 text-sm text-[hsl(var(--muted-foreground))]"
>
正在加载日汇总...
</div>
<div class="mb-3 text-sm text-[hsl(var(--muted-foreground))]">
{{ storeName || '本店' }} · {{ billDateTxt || '—' }} · 应付款 ¥{{ platformAmount }}
{{ displayStoreName }} · {{ displayBillDateTxt }} · 应付款 ¥{{
displayPlatformAmount
}}
</div>
<div v-if="existVouchers.length" class="mb-3">
<div class="mb-1 text-sm text-[hsl(var(--foreground))]">已传凭证</div>
@@ -177,6 +207,6 @@ defineExpose({ load, submit, submitting, loading, voucherUrls });
class="mt-3 w-full rounded border border-[hsl(var(--border))] bg-[hsl(var(--background))] p-2 text-sm text-[hsl(var(--foreground))]"
rows="2"
placeholder="备注(选填)"
/>
></textarea>
</div>
</template>

View File

@@ -6,6 +6,7 @@ import { nextTick, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { buildDailyUploadFormData } from '../utils/daily-upload';
import DailyUploadForm from './daily-upload-form.vue';
const formRef = ref<InstanceType<typeof DailyUploadForm>>();
@@ -33,13 +34,7 @@ const [Modal, modalApi] = useVbenModal({
if (!isOpen) return;
await nextTick();
await formRef.value?.load(
modalApi.getData<{
id: number;
store_name?: string;
platform_amount?: string;
bill_date_txt?: string;
gridApi?: any;
}>(),
buildDailyUploadFormData(modalApi.getData<Record<string, any>>() || {}),
);
},
});

View File

@@ -8,7 +8,7 @@ import { useRoute } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { Image, Tabs, Tag, message } from 'ant-design-vue';
import { Image, message, Tabs, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { TableAction } from '#/components/table-action';
@@ -22,8 +22,8 @@ import {
getPublicAccountPayInfo,
getPublicAccountPayList,
} from './api';
import DailyUploadModal from './components/daily-upload-modal.vue';
import DailyChangeLogDrawer from './components/daily-change-log-drawer.vue';
import DailyUploadModal from './components/daily-upload-modal.vue';
import GenerateDailyModal from './components/generate-daily-modal.vue';
import MergeOrdersModal from './components/merge-orders-modal.vue';
import VoucherThumbs from './components/voucher-thumbs.vue';
@@ -35,19 +35,23 @@ import { dailyFormOptions } from './config/daily-search';
import { dailyGridOptions } from './config/daily-table';
import { formOptions } from './config/search';
import { gridOptions } from './config/table';
import {
buildDailyUploadFormData,
resolveDailyUploadId,
} from './utils/daily-upload';
defineOptions({ name: 'FinancePublicAccountPay' });
const route = useRoute();
const TAB_STORAGE_KEY = 'finance_public_account_pay_tab';
const activeTab = ref(localStorage.getItem(TAB_STORAGE_KEY) || 'daily');
const billMode = ref<'order' | 'daily'>('order');
const billMode = ref<'daily' | 'order'>('order');
const lockDays = ref(1);
const canConfirm = ref(false);
const canUpload = ref(false);
const canGenerate = ref(false);
const detail = ref<Record<string, any> | null>(null);
const dailyDetail = ref<Record<string, any> | null>(null);
const detail = ref<null | Record<string, any>>(null);
const dailyDetail = ref<null | Record<string, any>>(null);
const changeLogOpen = ref(false);
const changeLogDailyId = ref(0);
const changeLogMergeId = ref(0);
@@ -100,19 +104,14 @@ function openGenerateDaily() {
}
function openDailyUpload(row: Record<string, any>) {
const isPeriod = Number(row.is_period || 0) === 1;
const mergeId = Number(row.merge_id || 0);
// 上传接口需要真实日账单 id周期行取 merge_id=最早日账单 id
const uploadId = isPeriod ? mergeId : Number(row.id || 0);
dailyUploadModalApi.setData({
id: uploadId,
merge_id: mergeId,
is_period: isPeriod ? 1 : 0,
store_name: row.store_name,
platform_amount: row.platform_amount,
bill_date_txt: row.bill_date_range_txt || row.bill_date_txt,
gridApi: dailyGridApi,
});
const uploadId = resolveDailyUploadId(row);
if (uploadId < 1) {
message.warning('无法识别日汇总,请刷新后重试');
return;
}
dailyUploadModalApi.setData(
buildDailyUploadFormData(row, { gridApi: dailyGridApi }),
);
dailyUploadModalApi.open();
}
@@ -164,7 +163,11 @@ function tryOpenFromQuery() {
getPublicAccountDailyBillInfo(dailyId)
.then((row) => {
dailyDetail.value = row;
if (Number(row?.status) !== 1 && Number(row?.status) !== 2 && canUpload.value) {
if (
Number(row?.status) !== 1 &&
Number(row?.status) !== 2 &&
canUpload.value
) {
openDailyUpload(row);
}
})
@@ -215,9 +218,11 @@ onMounted(async () => {
const cfg = await getPublicAccountPayConfig().catch(() => null);
billMode.value = cfg?.bill_mode === 'daily' ? 'daily' : 'order';
lockDays.value = Math.max(1, Number(cfg?.lock_days ?? 1));
canConfirm.value = Number(cfg?.can_confirm) === 1 || cfg?.can_confirm === true;
canConfirm.value =
Number(cfg?.can_confirm) === 1 || cfg?.can_confirm === true;
canUpload.value = Number(cfg?.can_upload) === 1 || cfg?.can_upload === true;
canGenerate.value = Number(cfg?.can_generate) === 1 || cfg?.can_generate === true;
canGenerate.value =
Number(cfg?.can_generate) === 1 || cfg?.can_generate === true;
if (billMode.value === 'order') {
persistTab('order');
}
@@ -233,294 +238,312 @@ watch(
<template>
<Page auto-content-height title="公账支付账单">
<div class="pap-page">
<DailyUploadPayModal />
<GenerateDailyPayModal />
<MergeOrdersPayModal />
<DailyChangeLogDrawer
v-model:open="changeLogOpen"
:daily-bill-id="changeLogDailyId"
:merge-id="changeLogMergeId"
/>
<Tabs
v-if="billMode === 'daily'"
class="pap-tabs"
:active-key="activeTab"
@change="persistTab"
>
<Tabs.TabPane key="daily" tab="日汇总">
<DailyGrid>
<template #toolbar-actions>
<TableAction
:actions="[
{
label: lockDays > 1 ? '生成周期账单' : '生成日账单',
type: 'primary',
ifShow: canGenerate,
onClick: openGenerateDaily,
},
]"
/>
</template>
<template #status="{ row }">
<Tag :color="publicAccountDailyStatusMeta(row.status).color">
{{ row.status_txt || publicAccountDailyStatusMeta(row.status).label }}
</Tag>
</template>
<template #vouchers="{ row }">
<VoucherThumbs :items="row.vouchers" />
</template>
<template #arrival_vouchers="{ row }">
<VoucherThumbs :items="row.arrival_vouchers" />
</template>
<template #expand="{ row }">
<div class="expand-box">
<template v-if="Number(row.is_period || 0) === 1">
<div
v-for="item in row.items || []"
:key="'m-' + item.id"
class="expand-row"
>
日账单 #{{ item.id }} · {{ item.bill_date_txt || item.bill_date }} · ¥{{
item.platform_amount
}}
· {{ item.status_txt }}
</div>
</template>
<template v-else>
<div
v-for="item in row.items || []"
:key="item.id"
class="expand-row"
:class="{ 'text-muted-foreground line-through opacity-70': item.status === 2 }"
>
{{ item.order_no }} · ¥{{ item.platform_amount }} · {{ item.status_txt }}
</div>
<a
v-if="!(row.items || []).length"
class="text-sm text-primary"
@click="openDailyDetail(row.id)"
>
查看子单
</a>
</template>
</div>
</template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '上传凭证',
type: 'link',
ifShow:
canUpload &&
row.status !== 1 &&
row.status !== 2 &&
(Number(row.is_period || 0) === 1 ||
(Number(row.is_period || 0) !== 1 &&
Number(row.status) !== 4)),
onClick: openDailyUpload.bind(null, row),
},
{
label: '变动记录',
type: 'link',
ifShow: Number(row.id || 0) > 0 || Number(row.merge_id || 0) > 0,
onClick: openChangeLog.bind(null, row),
},
{
label: '查看合并',
type: 'link',
ifShow: Number(row.merge_id || 0) > 0,
onClick: openMergeDetail.bind(null, Number(row.merge_id)),
},
{
label: '详情',
type: 'link',
ifShow: Number(row.is_period || 0) !== 1,
onClick: openDailyDetail.bind(null, row.id),
},
{
label: '取消',
type: 'link',
danger: true,
ifShow:
canConfirm &&
Number(row.is_period || 0) !== 1 &&
row.status !== 1 &&
row.status !== 2 &&
row.status !== 4,
popConfirm: {
title: '确认取消该日汇总?子单将退回待出账',
confirm: handleDailyCancel.bind(null, row),
<DailyUploadPayModal />
<GenerateDailyPayModal />
<MergeOrdersPayModal />
<DailyChangeLogDrawer
v-model:open="changeLogOpen"
:daily-bill-id="changeLogDailyId"
:merge-id="changeLogMergeId"
/>
<Tabs
v-if="billMode === 'daily'"
class="pap-tabs"
:active-key="activeTab"
@change="persistTab"
>
<Tabs.TabPane key="daily" tab="日汇总">
<DailyGrid>
<template #toolbar-actions>
<TableAction
:actions="[
{
label: lockDays > 1 ? '生成周期账单' : '生成日账单',
type: 'primary',
ifShow: canGenerate,
onClick: openGenerateDaily,
},
},
]"
/>
</template>
</DailyGrid>
</Tabs.TabPane>
<Tabs.TabPane key="order" tab="订单明细">
<Grid>
<template #toolbar-actions>
<TableAction
:actions="[
{
label: lockDays > 1 ? '生成周期账单' : '生成日账单',
type: 'primary',
ifShow: canGenerate,
onClick: openGenerateDaily,
},
]"
/>
</template>
<template #status="{ row }">
<Tag :color="publicAccountPayStatusMeta(row.status).color">
{{ row.status_txt || publicAccountPayStatusMeta(row.status).label }}
</Tag>
</template>
<template #vouchers="{ row }">
<VoucherThumbs :items="row.vouchers" />
</template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '详情',
type: 'link',
onClick: openDetail.bind(null, row.id),
},
]"
/>
</template>
</Grid>
</Tabs.TabPane>
</Tabs>
<Grid v-else>
<template #status="{ row }">
<Tag :color="publicAccountPayStatusMeta(row.status).color">
{{ row.status_txt || publicAccountPayStatusMeta(row.status).label }}
</Tag>
</template>
<template #vouchers="{ row }">
<VoucherThumbs :items="row.vouchers" />
</template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '详情',
type: 'link',
onClick: openDetail.bind(null, row.id),
},
{
label: '取消',
type: 'link',
danger: true,
ifShow: row.status === 0,
popConfirm: {
title: '确认取消该待到账单?',
confirm: handleCancel.bind(null, row),
},
},
]"
/>
</template>
</Grid>
<div v-if="dailyDetail" class="detail-card mt-4">
<div class="mb-2 flex items-center justify-between">
<b>
日汇总 #{{ dailyDetail.id }} ·
{{ dailyDetail.bill_date_range_txt || dailyDetail.bill_date_txt }}
</b>
<a class="text-sm text-primary" @click="dailyDetail = null">关闭</a>
<a
class="ml-3 text-sm text-primary"
@click="openChangeLog(dailyDetail)"
>
变动记录
</a>
</div>
<div class="text-sm text-[hsl(var(--muted-foreground))]">
{{ dailyDetail.store_name }} · {{ dailyDetail.status_txt }} · 应付款
¥{{ dailyDetail.platform_amount || '0.00' }} · {{ dailyDetail.order_count }}
<template v-if="Number(dailyDetail.merge_id || 0) > 0">
· 合并ID
<a
class="text-primary"
@click="openMergeDetail(Number(dailyDetail.merge_id))"
>
#{{ dailyDetail.merge_id }}
</a>
]"
/>
</template>
<template #status="{ row }">
<Tag :color="publicAccountDailyStatusMeta(row.status).color">
{{
row.status_txt ||
publicAccountDailyStatusMeta(row.status).label
}}
</Tag>
</template>
<template #vouchers="{ row }">
<VoucherThumbs :items="row.vouchers" />
</template>
<template #arrival_vouchers="{ row }">
<VoucherThumbs :items="row.arrival_vouchers" />
</template>
<template #expand="{ row }">
<div class="expand-box">
<template v-if="Number(row.is_period || 0) === 1">
<div
v-for="item in row.items || []"
:key="`m-${ item.id}`"
class="expand-row"
>
日账单 #{{ item.id }} ·
{{ item.bill_date_txt || item.bill_date }} · ¥{{
item.platform_amount
}}
· {{ item.status_txt }}
</div>
</template>
<template v-else>
<div
v-for="item in row.items || []"
:key="item.id"
class="expand-row"
:class="{
'text-muted-foreground line-through opacity-70':
item.status === 2,
}"
>
{{ item.order_no }} · ¥{{ item.platform_amount }} ·
{{ item.status_txt }}
</div>
<a
v-if="!(row.items || []).length"
class="text-sm text-primary"
@click="openDailyDetail(row.id)"
>
查看子单
</a>
</template>
</div>
</template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '上传凭证',
type: 'link',
ifShow:
canUpload &&
row.status !== 1 &&
row.status !== 2 &&
(Number(row.is_period || 0) === 1 ||
(Number(row.is_period || 0) !== 1 &&
Number(row.status) !== 4)),
onClick: openDailyUpload.bind(null, row),
},
{
label: '变动记录',
type: 'link',
ifShow:
Number(row.id || 0) > 0 || Number(row.merge_id || 0) > 0,
onClick: openChangeLog.bind(null, row),
},
{
label: '查看合并',
type: 'link',
ifShow: Number(row.merge_id || 0) > 0,
onClick: openMergeDetail.bind(null, Number(row.merge_id)),
},
{
label: '详情',
type: 'link',
ifShow: Number(row.is_period || 0) !== 1,
onClick: openDailyDetail.bind(null, row.id),
},
{
label: '取消',
type: 'link',
danger: true,
ifShow:
canConfirm &&
Number(row.is_period || 0) !== 1 &&
row.status !== 1 &&
row.status !== 2 &&
row.status !== 4,
popConfirm: {
title: '确认取消该日汇总?子单将退回待出账',
confirm: handleDailyCancel.bind(null, row),
},
},
]"
/>
</template>
</DailyGrid>
</Tabs.TabPane>
<Tabs.TabPane key="order" tab="订单明细">
<Grid>
<template #toolbar-actions>
<TableAction
:actions="[
{
label: lockDays > 1 ? '生成周期账单' : '生成日账单',
type: 'primary',
ifShow: canGenerate,
onClick: openGenerateDaily,
},
]"
/>
</template>
<template #status="{ row }">
<Tag :color="publicAccountPayStatusMeta(row.status).color">
{{
row.status_txt || publicAccountPayStatusMeta(row.status).label
}}
</Tag>
</template>
<template #vouchers="{ row }">
<VoucherThumbs :items="row.vouchers" />
</template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '详情',
type: 'link',
onClick: openDetail.bind(null, row.id),
},
]"
/>
</template>
</Grid>
</Tabs.TabPane>
</Tabs>
<Grid v-else>
<template #status="{ row }">
<Tag :color="publicAccountPayStatusMeta(row.status).color">
{{ row.status_txt || publicAccountPayStatusMeta(row.status).label }}
</Tag>
</template>
</div>
<div v-if="(dailyDetail.items || []).length" class="mt-3 text-sm">
<div
v-for="item in dailyDetail.items"
:key="item.id"
class="expand-row"
:class="{ 'text-muted-foreground line-through opacity-70': item.status === 2 }"
>
{{ item.order_no }} · 平台 ¥{{ item.platform_amount }} · {{ item.status_txt }}
</div>
</div>
<div v-if="(dailyDetail.vouchers || []).length" class="mt-3">
<div class="mb-1 text-sm">付款凭证</div>
<div class="flex flex-wrap gap-2">
<Image
v-for="item in dailyDetail.vouchers"
:key="item.id"
:src="item.file_url"
:width="80"
:height="80"
<template #vouchers="{ row }">
<VoucherThumbs :items="row.vouchers" />
</template>
<template #action="{ row }">
<TableAction
:actions="[
{
label: '详情',
type: 'link',
onClick: openDetail.bind(null, row.id),
},
{
label: '取消',
type: 'link',
danger: true,
ifShow: row.status === 0,
popConfirm: {
title: '确认取消该待到账单?',
confirm: handleCancel.bind(null, row),
},
},
]"
/>
</template>
</Grid>
<div v-if="dailyDetail" class="detail-card mt-4">
<div class="mb-2 flex items-center justify-between">
<b>
日汇总 #{{ dailyDetail.id }} ·
{{ dailyDetail.bill_date_range_txt || dailyDetail.bill_date_txt }}
</b>
<a class="text-sm text-primary" @click="dailyDetail = null">关闭</a>
<a
class="ml-3 text-sm text-primary"
@click="openChangeLog(dailyDetail)"
>
变动记录
</a>
</div>
</div>
<div v-if="(dailyDetail.arrival_vouchers || []).length" class="mt-3">
<div class="mb-1 text-sm">到账凭证</div>
<div class="flex flex-wrap gap-2">
<Image
v-for="item in dailyDetail.arrival_vouchers"
<div class="text-sm text-[hsl(var(--muted-foreground))]">
{{ dailyDetail.store_name }} · {{ dailyDetail.status_txt }} · 应付款
¥{{ dailyDetail.platform_amount || '0.00' }} ·
{{ dailyDetail.order_count }}
<template v-if="Number(dailyDetail.merge_id || 0) > 0">
· 合并ID
<a
class="text-primary"
@click="openMergeDetail(Number(dailyDetail.merge_id))"
>
#{{ dailyDetail.merge_id }}
</a>
</template>
</div>
<div v-if="(dailyDetail.items || []).length" class="mt-3 text-sm">
<div
v-for="item in dailyDetail.items"
:key="item.id"
:src="item.file_url"
:width="80"
:height="80"
/>
class="expand-row"
:class="{
'text-muted-foreground line-through opacity-70':
item.status === 2,
}"
>
{{ item.order_no }} · 平台 ¥{{ item.platform_amount }} ·
{{ item.status_txt }}
</div>
</div>
<div v-if="(dailyDetail.vouchers || []).length" class="mt-3">
<div class="mb-1 text-sm">付款凭证</div>
<div class="flex flex-wrap gap-2">
<Image
v-for="item in dailyDetail.vouchers"
:key="item.id"
:src="item.file_url"
:width="80"
:height="80"
/>
</div>
</div>
<div v-if="(dailyDetail.arrival_vouchers || []).length" class="mt-3">
<div class="mb-1 text-sm">到账凭证</div>
<div class="flex flex-wrap gap-2">
<Image
v-for="item in dailyDetail.arrival_vouchers"
:key="item.id"
:src="item.file_url"
:width="80"
:height="80"
/>
</div>
</div>
</div>
</div>
<div v-if="detail" class="detail-card mt-4">
<div class="mb-2 flex items-center justify-between">
<b>账单 #{{ detail.id }} · {{ detail.order_no }}</b>
<a class="text-sm text-primary" @click="detail = null">关闭</a>
</div>
<div class="text-sm text-[hsl(var(--muted-foreground))]">
状态 {{ detail.status_txt }} · 分账 {{ detail.split_mode_txt || '未选' }} ·
入账 ¥{{ detail.paid_amount || '0.00' }}
</div>
<div v-if="(detail.payment_vouchers || []).length" class="mt-3">
<div class="mb-1 text-sm">付款凭证</div>
<div class="flex flex-wrap gap-2">
<Image
v-for="item in detail.payment_vouchers"
:key="item.id"
:src="item.file_url"
:width="80"
:height="80"
/>
<div v-if="detail" class="detail-card mt-4">
<div class="mb-2 flex items-center justify-between">
<b>账单 #{{ detail.id }} · {{ detail.order_no }}</b>
<a class="text-sm text-primary" @click="detail = null">关闭</a>
</div>
<div class="text-sm text-[hsl(var(--muted-foreground))]">
状态 {{ detail.status_txt }} · 分账
{{ detail.split_mode_txt || '未选' }} · 入账 ¥{{
detail.paid_amount || '0.00'
}}
</div>
<div v-if="(detail.payment_vouchers || []).length" class="mt-3">
<div class="mb-1 text-sm">付款凭证</div>
<div class="flex flex-wrap gap-2">
<Image
v-for="item in detail.payment_vouchers"
:key="item.id"
:src="item.file_url"
:width="80"
:height="80"
/>
</div>
</div>
<div v-if="(detail.arrival_vouchers || []).length" class="mt-3">
<div class="mb-1 text-sm">到账凭证</div>
<div class="flex flex-wrap gap-2">
<Image
v-for="item in detail.arrival_vouchers"
:key="item.id"
:src="item.file_url"
:width="80"
:height="80"
/>
</div>
</div>
</div>
<div v-if="(detail.arrival_vouchers || []).length" class="mt-3">
<div class="mb-1 text-sm">到账凭证</div>
<div class="flex flex-wrap gap-2">
<Image
v-for="item in detail.arrival_vouchers"
:key="item.id"
:src="item.file_url"
:width="80"
:height="80"
/>
</div>
</div>
</div>
</div>
</Page>
</template>
@@ -532,40 +555,48 @@ watch(
height: 100%;
min-height: 0;
}
.pap-page > :deep(.h-full) {
flex: 1;
min-height: 0;
}
.pap-tabs {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.pap-tabs :deep(.ant-tabs-nav) {
flex-shrink: 0;
margin-bottom: 8px;
}
.pap-tabs :deep(.ant-tabs-content-holder) {
flex: 1;
min-height: 0;
overflow: hidden;
}
.pap-tabs :deep(.ant-tabs-content) {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.pap-tabs :deep(.ant-tabs-tabpane),
.pap-tabs :deep(.ant-tabs-tabpane-active) {
flex: 1;
height: 100%;
min-height: 0;
}
.pap-page :deep(.vxe-grid) {
height: 100% !important;
}
.detail-card {
padding: 14px 16px;
background: hsl(var(--card, var(--background)));
@@ -573,12 +604,14 @@ watch(
border-radius: 10px;
box-shadow: 0 0 6px hsl(var(--primary) / 18%);
}
.expand-box {
padding: 8px 12px;
}
.expand-row {
padding: 4px 0;
color: hsl(var(--muted-foreground));
font-size: 13px;
color: hsl(var(--muted-foreground));
}
</style>

View File

@@ -0,0 +1,141 @@
/**
* 日汇总上传凭证:从列表/锁店/站内信等不同入口解析展示与提交字段
*/
export interface DailyUploadRowLike {
id?: number | string;
merge_id?: number | string;
is_period?: number | string;
store_id?: number | string;
store_name?: string;
store?: { name?: string };
platform_amount?: number | string;
platform_amount_total?: number | string;
bill_date_txt?: string;
bill_date_range_txt?: string;
items?: Array<{ id?: number | string; platform_amount?: number | string }>;
}
/** 上传接口需要的真实日账单 id周期行 id 常为 0 */
export function resolveDailyUploadId(row?: DailyUploadRowLike | null): number {
if (!row) {
return 0;
}
const directId = Number(row.id || 0);
if (directId > 0) {
return directId;
}
const mergeId = Number(row.merge_id || 0);
if (mergeId > 0) {
return mergeId;
}
const firstItem = row.items?.[0];
const itemId = Number(firstItem?.id || 0);
if (itemId > 0) {
return itemId;
}
return 0;
}
/** 列表/锁店展示的应付款(平台成本) */
export function resolveDailyPlatformAmount(
row?: DailyUploadRowLike | null,
): string {
if (!row) {
return '0.00';
}
const isPeriod = Number(row.is_period || 0) === 1;
const total = trimAmount(row.platform_amount_total);
const amount = trimAmount(row.platform_amount);
if (isPeriod) {
if (total) {
return total;
}
if (amount) {
return amount;
}
return sumItemPlatformAmounts(row.items);
}
if (amount) {
return amount;
}
if (total) {
return total;
}
return sumItemPlatformAmounts(row.items);
}
export function resolveDailyStoreName(row?: DailyUploadRowLike | null): string {
if (!row) {
return '';
}
return String(row.store_name || row.store?.name || '').trim();
}
export function resolveDailyBillDateTxt(
row?: DailyUploadRowLike | null,
): string {
if (!row) {
return '';
}
return String(row.bill_date_range_txt || row.bill_date_txt || '').trim();
}
/** 打开上传弹窗/表单时灌入的标准数据结构 */
export function buildDailyUploadFormData(
row: DailyUploadRowLike,
extras?: Record<string, any>,
) {
const uploadId = resolveDailyUploadId(row);
return {
...row,
...extras,
id: uploadId,
store_name: resolveDailyStoreName(row),
platform_amount: resolveDailyPlatformAmount(row),
bill_date_txt: resolveDailyBillDateTxt(row),
};
}
/** 金额展示:千分位 + 两位小数 */
export function formatDailyPlatformAmount(value: unknown): string {
const raw = String(value ?? '').trim();
if (!raw) {
return '0.00';
}
const n = Number(raw.replaceAll(/,/g, ''));
if (!Number.isFinite(n)) {
return raw;
}
return n.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
/** 提交给后端的凭证结构 */
export function normalizeDailyVoucherUrls(urls: string[]) {
return urls
.map((url) => String(url || '').trim())
.filter(Boolean)
.map((file_url) => ({ file_url, file_id: 0 }));
}
function trimAmount(value: unknown): string {
return String(value ?? '').trim();
}
function sumItemPlatformAmounts(
items?: Array<{ platform_amount?: number | string }>,
): string {
if (!items?.length) {
return '0.00';
}
let total = 0;
for (const item of items) {
const n = Number(item.platform_amount ?? 0);
if (Number.isFinite(n)) {
total += n;
}
}
return total.toFixed(2);
}