Files
xk-admin/apps/web-antd/src/components/public-account-lock/LockOrdersPanel.vue
李琦 02672b16bd
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CI / CI OK (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
feat: 公账周期账单与合并明细优化,退款弹窗与开方成功页按钮换行
周期账单生成与变动记录抽屉;查看合并按账单日 Tabs;退款原因必填;开方成功页操作按钮自动换行。
2026-08-26 15:09:57 +08:00

551 lines
14 KiB
Vue
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.
<script lang="ts" setup>
/**
* 锁店 / 查看合并:日账单 + 下属商品订单(只读)
* 按账单日用 Tabs 切换,避免多日堆叠难扫
*/
import { computed, onMounted, ref, watch } from 'vue';
import { Button, Spin, Tabs, Tag } from 'ant-design-vue';
import DailyChangeLogDrawer from '#/views/finance/public-account-pay/components/daily-change-log-drawer.vue';
import { getPublicAccountDailyLockOrders } from '#/views/finance/public-account-pay/api';
const props = defineProps<{
mergeId?: number;
dailyId?: number;
overdueDailyIds?: number[];
/** 锁店内页要返回;弹窗内隐藏 */
showBack?: boolean;
/** 弹窗内铺满宽度 */
embedded?: boolean;
}>();
const emit = defineEmits<{
back: [];
}>();
const showBackBtn = computed(() => props.showBack !== false);
const loading = ref(false);
const loadError = ref('');
const bundle = ref<Record<string, any> | null>(null);
/** 当前选中的日账单 id(Tabs) */
const activeDayId = ref<string>('');
const changeLogOpen = ref(false);
const days = computed(() => {
const list = bundle.value?.items;
return Array.isArray(list) ? list : [];
});
const activeDay = computed(() => {
const id = Number(activeDayId.value || 0);
if (id < 1) {
return days.value[0] || null;
}
return days.value.find((d) => Number(d.id) === id) || days.value[0] || null;
});
const summary = computed(() => {
const b = bundle.value;
if (!b) {
return null;
}
return {
range: b.bill_date_range_txt || '',
billCount: Number(b.bill_count || 0),
orderCount: Number(b.order_count || 0),
amount: b.platform_amount_total || '0.00',
periodEnd: b.period_end_at_txt || '',
};
});
function dayDateTxt(day: Record<string, any>) {
return day.bill_date_txt || String(day.bill_date || '—');
}
/** Tab 标题:月-日,短一点好扫 */
function dayTabLabel(day: Record<string, any>) {
const txt = dayDateTxt(day);
if (/^\d{4}-\d{2}-\d{2}$/.test(txt)) {
return txt.slice(5);
}
return txt;
}
function dayOrderCount(day: Record<string, any>) {
return (day.items || []).length;
}
async function loadOrders() {
loading.value = true;
loadError.value = '';
try {
const data = await getPublicAccountDailyLockOrders({
merge_id: Number(props.mergeId || 0) || undefined,
daily_id: Number(props.dailyId || 0) || undefined,
overdue_daily_ids: (props.overdueDailyIds || []).join(',') || undefined,
});
bundle.value = data && typeof data === 'object' ? data : null;
const first = days.value[0];
activeDayId.value = first ? String(Number(first.id || 0)) : '';
} catch (err: any) {
loadError.value = err?.message || '加载失败';
bundle.value = null;
activeDayId.value = '';
} finally {
loading.value = false;
}
}
onMounted(() => {
void loadOrders();
});
watch(
() => [
Number(props.mergeId || 0),
Number(props.dailyId || 0),
(props.overdueDailyIds || []).join(','),
],
() => {
void loadOrders();
},
);
</script>
<template>
<div class="pap-orders" :class="{ 'is-embedded': embedded }">
<div class="pap-orders-head">
<div class="pap-orders-head-main">
<div class="pap-orders-badge">只读明细</div>
<h3 class="pap-orders-title">合并账单明细</h3>
<div v-if="summary" class="pap-orders-stats">
<div class="pap-stat">
<span class="pap-stat-label">账单区间</span>
<strong class="pap-stat-value">{{ summary.range || '—' }}</strong>
</div>
<div class="pap-stat">
<span class="pap-stat-label">日账单</span>
<strong class="pap-stat-value">{{ summary.billCount }} 张</strong>
</div>
<div class="pap-stat">
<span class="pap-stat-label">商品订单</span>
<strong class="pap-stat-value">{{ summary.orderCount }} 笔</strong>
</div>
<div class="pap-stat pap-stat-amount">
<span class="pap-stat-label">合计应付款</span>
<strong class="pap-stat-value">¥{{ summary.amount }}</strong>
</div>
</div>
<p v-if="summary?.periodEnd" class="pap-orders-deadline">
周期截止 {{ summary.periodEnd }}
</p>
</div>
<div class="pap-orders-actions">
<Button
v-if="showBackBtn"
size="middle"
@click="emit('back')"
>
返回
</Button>
<Button
v-if="Number(mergeId || 0) > 0 || Number(dailyId || 0) > 0"
size="middle"
@click="changeLogOpen = true"
>
变动记录
</Button>
</div>
</div>
<DailyChangeLogDrawer
v-model:open="changeLogOpen"
:daily-bill-id="Number(dailyId || 0)"
:merge-id="Number(mergeId || 0)"
/>
<Spin :spinning="loading">
<p v-if="loadError" class="pap-orders-empty">{{ loadError }}</p>
<p v-else-if="!loading && !days.length" class="pap-orders-empty">
暂无日账单明细
</p>
<div v-else class="pap-orders-body">
<Tabs
v-model:active-key="activeDayId"
class="pap-day-tabs"
type="card"
size="small"
>
<Tabs.TabPane
v-for="day in days"
:key="String(Number(day.id || 0))"
>
<template #tab>
<span class="pap-day-tab">
<span class="pap-day-tab-date">{{ dayTabLabel(day) }}</span>
<span class="pap-day-tab-amt">¥{{ day.platform_amount || '0.00' }}</span>
</span>
</template>
</Tabs.TabPane>
</Tabs>
<div v-if="activeDay" class="pap-day-panel">
<div class="pap-day-panel-head">
<div class="pap-day-panel-title">
<strong>{{ dayDateTxt(activeDay) }}</strong>
<Tag>{{ activeDay.status_txt || '—' }}</Tag>
</div>
<div class="pap-day-panel-meta">
<span>{{ dayOrderCount(activeDay) }} 笔订单</span>
<span>应付款 ¥{{ activeDay.platform_amount || '0.00' }}</span>
<span v-if="activeDay.order_amount">
订单合计 ¥{{ activeDay.order_amount }}
</span>
</div>
</div>
<div
v-if="!(activeDay.items || []).length"
class="pap-order-empty"
>
该日暂无商品订单
</div>
<div v-else class="pap-order-list">
<div
v-for="order in activeDay.items || []"
:key="order.id"
class="pap-order-row"
>
<div class="pap-order-top">
<div class="pap-order-main">
<span class="pap-order-tag">商品订单</span>
<strong>{{ order.order_no || `#${order.order_id}` }}</strong>
</div>
<div class="pap-order-side">
<span>订单 ¥{{ order.order_amount || '0.00' }}</span>
<strong>平台 ¥{{ order.platform_amount || '0.00' }}</strong>
<span>{{ order.status_txt || '' }}</span>
</div>
</div>
<div
v-if="(order.products || []).length"
class="pap-order-products"
>
<div
v-for="product in order.products"
:key="product.id"
class="pap-product"
:class="{ 'is-chinese': Number(product.is_chinese) === 1 }"
>
<template v-if="Number(product.is_chinese) === 1">
<span class="pap-product-chip">
{{ product.drug_name || '—' }}×{{ product.number ?? 0 }}
</span>
</template>
<template v-else>
<img
v-if="product.drug_image"
class="pap-product-img"
:src="product.drug_image"
:alt="product.drug_name || '药品'"
/>
<div v-else class="pap-product-img is-empty">药</div>
<div class="pap-product-meta">
<strong>{{ product.drug_name || '—' }}</strong>
<span>×{{ product.number ?? 0 }}</span>
</div>
</template>
</div>
</div>
<div v-else class="pap-order-empty-products">暂无商品明细</div>
</div>
</div>
</div>
</div>
</Spin>
</div>
</template>
<style scoped>
.pap-orders {
width: min(720px, 100%);
max-width: 100%;
text-align: left;
}
.pap-orders.is-embedded {
width: 100%;
}
.pap-orders-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}
.pap-orders-head-main {
min-width: 0;
flex: 1;
}
.pap-orders-badge {
display: inline-block;
padding: 2px 10px;
margin-bottom: 8px;
font-size: 12px;
font-weight: 600;
color: hsl(var(--pap-accent, var(--primary)));
border: 1px solid hsl(var(--pap-accent, var(--primary)) / 40%);
border-radius: 999px;
background: hsl(var(--pap-accent, var(--primary)) / 12%);
}
.pap-orders-title {
margin: 0 0 10px;
font-size: var(--pap-fs-title, 20px);
font-weight: 700;
color: hsl(var(--foreground));
}
.pap-orders-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.pap-stat {
padding: 10px 12px;
border: 1px solid hsl(var(--border));
border-radius: 8px;
background: hsl(var(--muted) / 0.22);
}
.pap-stat-amount {
border-color: hsl(var(--pap-accent, var(--primary)) / 30%);
box-shadow: 0 0 6px hsl(var(--pap-accent, var(--primary)) / 14%);
}
.pap-stat-label {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.pap-stat-value {
font-size: 14px;
font-weight: 600;
color: hsl(var(--foreground));
word-break: break-all;
}
.pap-stat-amount .pap-stat-value {
color: hsl(var(--pap-accent, var(--primary)));
}
.pap-orders-deadline {
margin: 8px 0 0;
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.pap-orders-actions {
display: flex;
flex-shrink: 0;
flex-wrap: wrap;
gap: 8px;
}
.pap-orders-empty {
margin: 24px 0;
font-size: 14px;
color: hsl(var(--muted-foreground));
text-align: center;
}
.pap-orders-body {
border: 1px solid hsl(var(--border));
border-radius: 10px;
background: hsl(var(--card, var(--background)));
overflow: hidden;
}
.pap-day-tabs {
padding: 8px 8px 0;
background: hsl(var(--muted) / 0.18);
}
.pap-day-tabs :deep(.ant-tabs-nav) {
margin-bottom: 0;
}
.pap-day-tabs :deep(.ant-tabs-tab) {
padding: 6px 10px !important;
}
.pap-day-tab {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
line-height: 1.2;
}
.pap-day-tab-date {
font-size: 13px;
font-weight: 600;
color: hsl(var(--foreground));
}
.pap-day-tab-amt {
font-size: 11px;
color: hsl(var(--muted-foreground));
}
.pap-day-panel {
padding: 12px 14px 14px;
}
.pap-day-panel-head {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid hsl(var(--border));
}
.pap-day-panel-title {
display: flex;
align-items: center;
gap: 8px;
}
.pap-day-panel-title strong {
font-size: 15px;
color: hsl(var(--foreground));
}
.pap-day-panel-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.pap-order-empty {
padding: 16px 4px;
font-size: 13px;
color: hsl(var(--muted-foreground));
text-align: center;
}
.pap-order-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.pap-order-row {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
border: 1px solid hsl(var(--border));
border-radius: 8px;
background: hsl(var(--muted) / 0.18);
}
.pap-order-top {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.pap-order-main {
display: flex;
align-items: center;
gap: 8px;
}
.pap-order-tag {
padding: 1px 8px;
font-size: 11px;
color: hsl(var(--pap-accent, var(--primary)));
border: 1px solid hsl(var(--pap-accent, var(--primary)) / 30%);
border-radius: 999px;
background: hsl(var(--pap-accent, var(--primary)) / 10%);
}
.pap-order-main strong {
font-size: 13px;
color: hsl(var(--foreground));
}
.pap-order-side {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.pap-order-side strong {
color: hsl(var(--pap-accent, var(--primary)));
}
.pap-order-products {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.pap-product {
display: flex;
align-items: center;
gap: 6px;
width: calc(33.333% - 4px);
min-width: 0;
padding: 4px 6px;
border-radius: 6px;
background: hsl(var(--background) / 0.55);
border: 1px solid hsl(var(--border) / 70%);
}
.pap-product.is-chinese {
width: auto;
max-width: 100%;
padding: 2px 8px;
background: hsl(var(--muted) / 0.28);
}
.pap-product-chip {
font-size: 12px;
line-height: 1.3;
color: hsl(var(--foreground));
white-space: nowrap;
}
.pap-product-img {
width: 28px;
height: 28px;
object-fit: cover;
border-radius: 6px;
border: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.35);
flex-shrink: 0;
}
.pap-product-img.is-empty {
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: hsl(var(--muted-foreground));
}
.pap-product-meta {
display: flex;
flex: 1;
align-items: center;
justify-content: space-between;
gap: 4px;
min-width: 0;
}
.pap-product-meta strong {
font-size: 12px;
font-weight: 600;
line-height: 1.2;
color: hsl(var(--foreground));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pap-product-meta span {
flex-shrink: 0;
font-size: 11px;
color: hsl(var(--muted-foreground));
}
.pap-order-empty-products {
font-size: 12px;
color: hsl(var(--muted-foreground));
}
@media (max-width: 720px) {
.pap-orders-stats {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.pap-orders-head {
flex-direction: column;
}
.pap-orders-actions {
width: 100%;
}
.pap-product:not(.is-chinese) {
width: calc(50% - 3px);
}
}
</style>