fix FundRecordRevoke.vue style

This commit is contained in:
Timi
2026-09-05 18:10:48 +08:00
parent 9c6edca956
commit e4b642ebc3
5 changed files with 248 additions and 65 deletions
+44 -8
View File
@@ -4,7 +4,7 @@
<section class="hero" v-entrance-transition> <section class="hero" v-entrance-transition>
<div class="header"> <div class="header">
<span class="eyebrow" v-text="'资金流水'" /> <span class="eyebrow" v-text="'资金流水'" />
<t-tag :content="getFundStatusText(record.status)" :theme="getStatusTheme(record.status)" variant="light-outline" /> <t-tag :content="getRecordStatusText(record)" :theme="getRecordStatusTheme(record)" variant="light-outline" />
</div> </div>
<h1 class="title" v-text="getFundOperationText(record.operationType)" /> <h1 class="title" v-text="getFundOperationText(record.operationType)" />
<div class="amount"> <div class="amount">
@@ -12,19 +12,28 @@
<strong class="value red text-bold" v-text="formatPackageAmount(record.amount)" /> <strong class="value red text-bold" v-text="formatPackageAmount(record.amount)" />
</div> </div>
</section> </section>
<t-cell-group title="资金记录" theme="card" v-entrance-transition> <t-cell-group title="订单信息" theme="card" v-entrance-transition="70">
<t-cell title="订单总价" :note="formatPackageAmount(record.salesOrderAmount)" />
<t-cell title="剩余待收款(流水快照)" :note="formatPackageAmount(record.salesOrderUnpaidAmountSnapshot)" />
</t-cell-group>
<t-cell-group title="资金记录" theme="card" v-entrance-transition="140">
<t-cell title="操作类型" :note="getFundOperationText(record.operationType)" /> <t-cell title="操作类型" :note="getFundOperationText(record.operationType)" />
<t-cell title="状态"> <t-cell title="状态">
<template #note> <template #note>
<t-tag <t-tag
:content="getFundStatusText(record.status)" :content="getRecordStatusText(record)"
:theme="getStatusTheme(record.status)" :theme="getRecordStatusTheme(record)"
variant="light-outline" variant="light-outline"
/> />
</template> </template>
</t-cell> </t-cell>
<t-cell title="金额" :note="formatPackageAmount(record.amount)" /> <t-cell title="金额" :note="formatPackageAmount(record.amount)" />
<t-cell v-if="record.sourceRecordId" title="原资金记录" :note="record.sourceRecordId" /> <t-cell
v-if="record.sourceRecordId"
title="冲正流水"
arrow
@click="toSourceRecord"
/>
<t-cell title="备注" :note="Text.display(record.remark)" /> <t-cell title="备注" :note="Text.display(record.remark)" />
<t-cell v-if="record.reason" title="拒绝/撤销原因" :note="Text.display(record.reason)" /> <t-cell v-if="record.reason" title="拒绝/撤销原因" :note="Text.display(record.reason)" />
<t-cell <t-cell
@@ -43,7 +52,7 @@
/> />
</div> </div>
</t-cell-group> </t-cell-group>
<t-cell-group theme="card"> <t-cell-group theme="card" v-entrance-transition="210">
<t-cell <t-cell
title="发起人" title="发起人"
:note="record.operator?.name || record.operator?.nick || '未知'" :note="record.operator?.name || record.operator?.nick || '未知'"
@@ -66,7 +75,7 @@
<section <section
v-if="isPendingReceive || canRevoke" v-if="isPendingReceive || canRevoke"
class="actions" class="actions"
v-entrance-transition="70" v-entrance-transition="280"
> >
<div class="pair"> <div class="pair">
<t-button <t-button
@@ -144,12 +153,23 @@ const isLoading = ref(false);
const isPendingReceive = computed(() => record.value?.operationType === "RECEIVE" && record.value.status === "PENDING"); const isPendingReceive = computed(() => record.value?.operationType === "RECEIVE" && record.value.status === "PENDING");
const canVerifyReceive = computed(() => isPendingReceive.value && userStore.hasPermissions(permissionCodes.fundReceive.verify)); const canVerifyReceive = computed(() => isPendingReceive.value && userStore.hasPermissions(permissionCodes.fundReceive.verify));
const canCancelReceive = computed(() => isPendingReceive.value && userStore.hasPermissions(permissionCodes.fundReceive.create)); const canCancelReceive = computed(() => isPendingReceive.value && userStore.hasPermissions(permissionCodes.fundReceive.create));
const canRevoke = computed(() => record.value?.status === "VERIFIED" && record.value.operationType !== "REVOKE" && userStore.hasPermissions(permissionCodes.fund.revoke)); const canRevoke = computed(() => record.value?.status === "VERIFIED"
&& record.value.operationType !== "REVOKE"
&& !record.value.revokeRecordId
&& userStore.hasPermissions(permissionCodes.fund.revoke));
function getStatusTheme(status?: FundRecordStatus): "success" | "warning" | "danger" | "default" { function getStatusTheme(status?: FundRecordStatus): "success" | "warning" | "danger" | "default" {
return status === "VERIFIED" ? "success" : status === "PENDING" ? "warning" : status === "REJECTED" ? "danger" : "default"; return status === "VERIFIED" ? "success" : status === "PENDING" ? "warning" : status === "REJECTED" ? "danger" : "default";
} }
function getRecordStatusText(recordValue: FundRecord): string {
return recordValue.revokeRecordId ? "已被冲正" : getFundStatusText(recordValue.status);
}
function getRecordStatusTheme(recordValue: FundRecord): "success" | "warning" | "danger" | "default" {
return recordValue.revokeRecordId ? "danger" : getStatusTheme(recordValue.status);
}
async function loadData(): Promise<void> { async function loadData(): Promise<void> {
const id = String(route.params.id || ""); const id = String(route.params.id || "");
if (!id) { if (!id) {
@@ -179,6 +199,18 @@ async function toSalesOrder(): Promise<void> {
}); });
} }
// ---------- 原资金记录 ----------
async function toSourceRecord(): Promise<void> {
if (!record.value?.sourceRecordId) {
return;
}
await router.push({
path: `/management/fund-records/${record.value.sourceRecordId}`,
query: { transitionDepth: String(getCurrentRouteDepth() + 1) }
});
}
function getCurrentRouteDepth(): number { function getCurrentRouteDepth(): number {
const transitionDepth = Array.isArray(route.query.transitionDepth) const transitionDepth = Array.isArray(route.query.transitionDepth)
? route.query.transitionDepth[0] ? route.query.transitionDepth[0]
@@ -216,7 +248,11 @@ async function verifyReceive(): Promise<void> {
const dialog = DialogPlugin.confirm({ const dialog = DialogPlugin.confirm({
title: "核销收款", title: "核销收款",
content: "核销后该金额才会计入套餐的已收款。确定继续?", content: "核销后该金额才会计入套餐的已收款。确定继续?",
cancelBtn: "取消",
confirmBtn: "核销", confirmBtn: "核销",
onCancel() {
dialog.destroy();
},
onConfirm: async () => { onConfirm: async () => {
isOperating.value = true; isOperating.value = true;
try { try {
+47 -7
View File
@@ -30,9 +30,9 @@
<div class="title"> <div class="title">
<span v-text="getFundOperationText(item.operationType)" /> <span v-text="getFundOperationText(item.operationType)" />
<t-tag <t-tag
:content="getFundStatusText(item.status)" :content="getRecordStatusText(item)"
variant="light-outline" variant="light-outline"
:theme="getStatusTheme(item.status)" :theme="getRecordStatusTheme(item)"
/> />
</div> </div>
</template> </template>
@@ -48,14 +48,29 @@
<span class="text-bold" v-text="getVerifierText(item)" /> <span class="text-bold" v-text="getVerifierText(item)" />
</span> </span>
</div> </div>
<div>
<pass-time-label :timestamp="item.occurredAt" underline /> <pass-time-label :timestamp="item.occurredAt" underline />
</div> </div>
</div>
</template> </template>
<template #note> <template #note>
<div class="amount"> <div class="amounts">
<div class="order-amount">
<span class="label">订单总价</span>
<span>¥</span>
<span v-text="formatPackageAmount(item.salesOrderAmount)" />
</div>
<div class="unpaid-amount">
<span class="label">剩余待收</span>
<span>¥</span>
<span v-text="formatPackageAmount(item.salesOrderUnpaidAmountSnapshot)" />
</div>
<div class="record-amount">
<span class="label">流水金额</span>
<span class="red">¥</span> <span class="red">¥</span>
<span class="value red" v-text="formatPackageAmount(item.amount)" /> <span class="value red" v-text="formatPackageAmount(item.amount)" />
</div> </div>
</div>
</template> </template>
</t-cell> </t-cell>
</t-cell-group> </t-cell-group>
@@ -72,7 +87,7 @@ import { useNavBarStore } from "@/store/navBarStore";
import type { FundOperationType, FundRecord, FundRecordStatus } from "@/types/SalesOrder"; import type { FundOperationType, FundRecord, FundRecordStatus } from "@/types/SalesOrder";
import { getErrorMessage } from "@/utils/error"; import { getErrorMessage } from "@/utils/error";
import { formatPackageAmount } from "@/utils/package"; import { formatPackageAmount } from "@/utils/package";
import { getFundBizTypeText, getFundOperationText, getFundStatusText } from "@/utils/salesOrder"; import { getFundOperationText, getFundStatusText } from "@/utils/salesOrder";
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
import { PassTimeLabel } from "timi-web"; import { PassTimeLabel } from "timi-web";
@@ -166,6 +181,14 @@ function getStatusTheme(statusValue?: FundRecordStatus): "success" | "warning" |
return statusValue === "VERIFIED" ? "success" : statusValue === "PENDING" ? "warning" : statusValue === "REJECTED" ? "danger" : "default"; return statusValue === "VERIFIED" ? "success" : statusValue === "PENDING" ? "warning" : statusValue === "REJECTED" ? "danger" : "default";
} }
function getRecordStatusText(item: FundRecord): string {
return item.revokeRecordId ? "已被冲正" : getFundStatusText(item.status);
}
function getRecordStatusTheme(item: FundRecord): "success" | "warning" | "danger" | "default" {
return item.revokeRecordId ? "danger" : getStatusTheme(item.status);
}
async function toDetail(item: FundRecord): Promise<void> { async function toDetail(item: FundRecord): Promise<void> {
if (item.id) { if (item.id) {
await router.push({ await router.push({
@@ -256,7 +279,7 @@ onUnmounted(() => navBarStore.clearRight());
gap: 0; gap: 0;
color: var(--td-text-color-secondary); color: var(--td-text-color-secondary);
display: flex; display: flex;
font-size: .8125rem; font-size: .75rem;
flex-wrap: wrap; flex-wrap: wrap;
line-height: 1.5; line-height: 1.5;
flex-direction: column; flex-direction: column;
@@ -277,9 +300,26 @@ onUnmounted(() => navBarStore.clearRight());
} }
} }
.amount { .amounts {
display: flex;
font-size: .75rem;
align-items: end;
flex-direction: column;
.value { .label {
color: var(--td-text-color-placeholder);
}
.order-amount,
.unpaid-amount,
.record-amount {
gap: .125rem;
display: flex;
align-items: baseline;
white-space: nowrap;
}
.record-amount .value {
font-size: 1.25rem; font-size: 1.25rem;
} }
} }
+134 -38
View File
@@ -2,33 +2,89 @@
<div v-can="permissionCodes.fund.revoke" class="package-operation-edit"> <div v-can="permissionCodes.fund.revoke" class="package-operation-edit">
<t-cell-group v-if="record" title="原资金记录" theme="card" v-entrance-transition> <t-cell-group v-if="record" title="原资金记录" theme="card" v-entrance-transition>
<t-cell title="操作类型" :note="getFundOperationText(record.operationType)" /> <t-cell title="操作类型" :note="getFundOperationText(record.operationType)" />
<t-cell title="金额" :note="formatPackageAmount(record.amount)" /> <t-cell title="金额">
<t-cell title="原状态" :note="getFundStatusText(record.status)" /> <template #note>
<t-cell title="销售订单" :note="record.bizId || '未知'" /> <div class="red" v-text="formatPackageAmount(record.amount)" />
</template>
</t-cell>
<t-cell title="原状态" :note="getRecordStatusText(record)" />
<t-cell
title="销售订单详情"
:arrow="!!record.bizId && canReadSalesOrder"
@click="toSalesOrder"
/>
</t-cell-group> </t-cell-group>
<t-form v-if="record" class="form" label-align="left" label-width="6rem" v-entrance-transition="70"> <t-form v-if="record" class="form" v-entrance-transition="70">
<t-form-item label="冲正原因" required-mark><t-textarea v-model.trim="reason" borderless autosize maxlength="500" placeholder="请输入冲正原因" /></t-form-item> <t-form-item label="冲正原因" required-mark>
<t-form-item label="冲正时间" arrow @click="pickerVisible = true"><span class="picker-text" v-text="occurredAtText || '使用当前时间'" /></t-form-item> <t-textarea
<t-form-item label="备注"><t-textarea v-model.trim="remark" borderless autosize maxlength="500" placeholder="备注(可选)" /></t-form-item> v-model="reason"
</t-form> borderless
autosize
maxlength="500"
placeholder="请输入冲正原因"
/>
</t-form-item>
<t-form-item
label="冲正时间"
content-align="right"
>
<div class="time-actions">
<t-button
theme="primary"
variant="outline"
size="extra-small"
:content="occurredAtText || '此刻'"
@click="openOccurredAtPicker"
/>
<t-button
v-if="occurredAtText"
theme="primary"
variant="text"
size="extra-small"
content="此刻"
@click="resetOccurredAt"
/>
</div>
</t-form-item>
<t-form-item label="备注">
<t-textarea
v-model="remark"
borderless
autosize
maxlength="500"
placeholder="备注(可选)"
/>
</t-form-item>
<div class="photo-wrap">
<photo-scroller <photo-scroller
v-if="record" v-if="record"
:model-value="photoUrlList" :model-value="photoUrlList"
v-model:photo-items="photoItems" v-model:photo-items="photoItems"
class="photo-field"
:upload="uploadPhotoList" :upload="uploadPhotoList"
:edit-mode="true" :edit-mode="true"
required required
title="冲正凭证照片" title="冲正凭证照片"
/> />
</div>
</t-form>
<t-empty v-if="!record" :description="isLoading ? '正在加载资金记录' : '资金记录不存在'" /> <t-empty v-if="!record" :description="isLoading ? '正在加载资金记录' : '资金记录不存在'" />
<t-popup v-model="pickerVisible" placement="bottom"> <t-popup v-model="occurredAtPickerVisible" placement="bottom">
<t-date-time-picker v-model="draftTime" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="冲正时间" @cancel="pickerVisible = false" @confirm="confirmTime" /> <t-date-time-picker
v-if="occurredAtPickerVisible"
v-model="occurredAtDraftText"
:start="minimumDateTimeText"
:mode="['date', 'minute']"
format="YYYY-MM-DD HH:mm"
title="冲正时间"
@cancel="occurredAtPickerVisible = false"
@confirm="confirmOccurredAt"
/>
</t-popup> </t-popup>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Time } from "timi-web";
import { useUserStore, type Attachment } from "timi-web-api"; import { useUserStore, type Attachment } from "timi-web-api";
import SalesOrderAPI from "@/api/SalesOrderAPI"; import SalesOrderAPI from "@/api/SalesOrderAPI";
import PhotoScroller from "@/components/PhotoScroller.vue"; import PhotoScroller from "@/components/PhotoScroller.vue";
@@ -52,25 +108,49 @@ const record = ref<FundRecord>();
const reason = ref(""); const reason = ref("");
const remark = ref(""); const remark = ref("");
const occurredAtText = ref(""); const occurredAtText = ref("");
const draftTime = ref(""); const occurredAtDraftText = ref("");
const pickerVisible = ref(false); const occurredAtPickerVisible = ref(false);
const photoItems = ref<PhotoItem[]>([]); const photoItems = ref<PhotoItem[]>([]);
const isLoading = ref(false); const isLoading = ref(false);
const isSaving = ref(false); const isSaving = ref(false);
const requestNo = ref(""); const requestNo = ref("");
const photoUrlList = computed(() => photoItems.value.map(item => item.url)); const photoUrlList = computed(() => photoItems.value.map(item => item.url));
const canReadSalesOrder = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.read));
function confirmTime(value: string | number): void { const minimumDateTimeText = "1970-01-01 00:00";
occurredAtText.value = String(value);
pickerVisible.value = false; function openOccurredAtPicker(): void {
occurredAtDraftText.value = occurredAtText.value || Time.formatDateTime(new Date());
occurredAtPickerVisible.value = true;
} }
function resolveTimestamp(value: string): number | undefined { function confirmOccurredAt(value?: string | number): void {
occurredAtText.value = String(value || occurredAtDraftText.value);
occurredAtPickerVisible.value = false;
}
function resetOccurredAt(): void {
occurredAtText.value = "";
occurredAtDraftText.value = "";
}
function buildOccurredAt(): number {
return Time.toTimestamp(parseDateTime(occurredAtText.value));
}
function getRecordStatusText(recordValue: FundRecord): string {
return recordValue.revokeRecordId ? "已被冲正" : getFundStatusText(recordValue.status);
}
function parseDateTime(value: string): Date {
if (!value) { if (!value) {
return undefined; return new Date();
} }
const timestamp = new Date(value.replace(" ", "T")).getTime();
return Number.isFinite(timestamp) ? timestamp : undefined; const [dateText, timeText = "00:00"] = value.split(" ");
const [year, month, day] = dateText.split("-").map(Number);
const [hour, minute] = timeText.split(":").map(Number);
return new Date(year, month - 1, day, hour || 0, minute || 0, 0, 0);
} }
function buildAttachments(): Partial<Attachment>[] { function buildAttachments(): Partial<Attachment>[] {
@@ -92,12 +172,35 @@ async function loadData(): Promise<void> {
} }
} }
// ---------- 订单详情 ----------
async function toSalesOrder(): Promise<void> {
if (!record.value?.bizId || !canReadSalesOrder.value) {
return;
}
await router.push({
path: `/management/sales-orders/${record.value.bizId}`,
query: { transitionDepth: String(getCurrentRouteDepth() + 1) }
});
}
function getCurrentRouteDepth(): number {
const transitionDepth = Array.isArray(route.query.transitionDepth)
? route.query.transitionDepth[0]
: route.query.transitionDepth;
const depth = Number(transitionDepth);
if (transitionDepth && Number.isInteger(depth) && 0 <= depth && depth <= 20) {
return depth;
}
return Number(route.meta.depth ?? 0);
}
async function save(): Promise<void> { async function save(): Promise<void> {
if (isSaving.value) { if (isSaving.value) {
return; return;
} }
if (!record.value?.id || record.value.status !== "VERIFIED" || record.value.operationType === "REVOKE") { if (!record.value?.id || record.value.revokeRecordId || record.value.status !== "VERIFIED" || record.value.operationType === "REVOKE") {
Message.warning("只有已核销且非冲正的资金记录可以冲正"); Message.warning("只有已核销、未被冲正且不是冲正记录的资金记录可以冲正");
return; return;
} }
if (!reason.value.trim()) { if (!reason.value.trim()) {
@@ -116,7 +219,7 @@ async function save(): Promise<void> {
const result = await SalesOrderAPI.fundRecordRevoke({ const result = await SalesOrderAPI.fundRecordRevoke({
requestNo: requestNo.value, requestNo: requestNo.value,
fundRecordId: record.value.id, fundRecordId: record.value.id,
occurredAt: resolveTimestamp(occurredAtText.value), occurredAt: buildOccurredAt(),
reason: reason.value.trim(), reason: reason.value.trim(),
attachmentList: buildAttachments(), attachmentList: buildAttachments(),
remark: remark.value.trim() || null remark: remark.value.trim() || null
@@ -136,7 +239,7 @@ async function save(): Promise<void> {
onMounted(async () => { onMounted(async () => {
if (userStore.hasPermissions(permissionCodes.fund.revoke)) { if (userStore.hasPermissions(permissionCodes.fund.revoke)) {
navBarStore.setRightText("提交冲正", "var(--td-danger-color)", save); navBarStore.setRightText("提交", "var(--td-error-color)", save);
} }
await loadData(); await loadData();
}); });
@@ -151,22 +254,15 @@ onUnmounted(() => navBarStore.clearRight());
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.form { .time-actions {
margin: 0 1rem; gap: .5rem;
padding: 0 .5rem; display: flex;
background: var(--td-bg-color-container); align-items: center;
box-shadow: var(--td-shadow-1); justify-content: flex-end;
border-radius: 1rem;
overflow: hidden;
} }
.picker-text { .photo-wrap {
width: 100%; padding: 1rem;
text-align: right;
}
.photo-field {
padding: 0 1rem;
} }
} }
</style> </style>
+9 -1
View File
@@ -40,7 +40,15 @@
/> />
</div> </div>
</t-form-item> </t-form-item>
<t-form-item label="备注"><t-textarea v-model.trim="remark" borderless autosize maxlength="500" /></t-form-item> <t-form-item label="备注">
<t-textarea
v-model="remark"
borderless
autosize
maxlength="500"
placeholder="请输入备注"
/>
</t-form-item>
<t-form-item label-width="0"> <t-form-item label-width="0">
<photo-scroller <photo-scroller
v-if="order" v-if="order"
+3
View File
@@ -141,7 +141,10 @@ export type FundRecord = {
operationType?: FundOperationType; operationType?: FundOperationType;
status?: FundRecordStatus; status?: FundRecordStatus;
amount?: number; amount?: number;
salesOrderAmount?: number;
salesOrderUnpaidAmountSnapshot?: number;
sourceRecordId?: string; sourceRecordId?: string;
revokeRecordId?: string;
requestNo?: string; requestNo?: string;
operatorUserId?: string; operatorUserId?: string;
verifierUserId?: string; verifierUserId?: string;