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>
<div class="header">
<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>
<h1 class="title" v-text="getFundOperationText(record.operationType)" />
<div class="amount">
@@ -12,19 +12,28 @@
<strong class="value red text-bold" v-text="formatPackageAmount(record.amount)" />
</div>
</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="状态">
<template #note>
<t-tag
:content="getFundStatusText(record.status)"
:theme="getStatusTheme(record.status)"
:content="getRecordStatusText(record)"
:theme="getRecordStatusTheme(record)"
variant="light-outline"
/>
</template>
</t-cell>
<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 v-if="record.reason" title="拒绝/撤销原因" :note="Text.display(record.reason)" />
<t-cell
@@ -43,7 +52,7 @@
/>
</div>
</t-cell-group>
<t-cell-group theme="card">
<t-cell-group theme="card" v-entrance-transition="210">
<t-cell
title="发起人"
:note="record.operator?.name || record.operator?.nick || '未知'"
@@ -66,7 +75,7 @@
<section
v-if="isPendingReceive || canRevoke"
class="actions"
v-entrance-transition="70"
v-entrance-transition="280"
>
<div class="pair">
<t-button
@@ -144,12 +153,23 @@ const isLoading = ref(false);
const isPendingReceive = computed(() => record.value?.operationType === "RECEIVE" && record.value.status === "PENDING");
const canVerifyReceive = computed(() => isPendingReceive.value && userStore.hasPermissions(permissionCodes.fundReceive.verify));
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" {
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> {
const id = String(route.params.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 {
const transitionDepth = Array.isArray(route.query.transitionDepth)
? route.query.transitionDepth[0]
@@ -216,7 +248,11 @@ async function verifyReceive(): Promise<void> {
const dialog = DialogPlugin.confirm({
title: "核销收款",
content: "核销后该金额才会计入套餐的已收款。确定继续?",
cancelBtn: "取消",
confirmBtn: "核销",
onCancel() {
dialog.destroy();
},
onConfirm: async () => {
isOperating.value = true;
try {
+47 -7
View File
@@ -30,9 +30,9 @@
<div class="title">
<span v-text="getFundOperationText(item.operationType)" />
<t-tag
:content="getFundStatusText(item.status)"
:content="getRecordStatusText(item)"
variant="light-outline"
:theme="getStatusTheme(item.status)"
:theme="getRecordStatusTheme(item)"
/>
</div>
</template>
@@ -48,14 +48,29 @@
<span class="text-bold" v-text="getVerifierText(item)" />
</span>
</div>
<div>
<pass-time-label :timestamp="item.occurredAt" underline />
</div>
</div>
</template>
<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="value red" v-text="formatPackageAmount(item.amount)" />
</div>
</div>
</template>
</t-cell>
</t-cell-group>
@@ -72,7 +87,7 @@ import { useNavBarStore } from "@/store/navBarStore";
import type { FundOperationType, FundRecord, FundRecordStatus } from "@/types/SalesOrder";
import { getErrorMessage } from "@/utils/error";
import { formatPackageAmount } from "@/utils/package";
import { getFundBizTypeText, getFundOperationText, getFundStatusText } from "@/utils/salesOrder";
import { getFundOperationText, getFundStatusText } from "@/utils/salesOrder";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
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";
}
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> {
if (item.id) {
await router.push({
@@ -256,7 +279,7 @@ onUnmounted(() => navBarStore.clearRight());
gap: 0;
color: var(--td-text-color-secondary);
display: flex;
font-size: .8125rem;
font-size: .75rem;
flex-wrap: wrap;
line-height: 1.5;
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;
}
}
+134 -38
View File
@@ -2,33 +2,89 @@
<div v-can="permissionCodes.fund.revoke" class="package-operation-edit">
<t-cell-group v-if="record" title="原资金记录" theme="card" v-entrance-transition>
<t-cell title="操作类型" :note="getFundOperationText(record.operationType)" />
<t-cell title="金额" :note="formatPackageAmount(record.amount)" />
<t-cell title="原状态" :note="getFundStatusText(record.status)" />
<t-cell title="销售订单" :note="record.bizId || '未知'" />
<t-cell title="金额">
<template #note>
<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-form v-if="record" class="form" label-align="left" label-width="6rem" 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="冲正时间" arrow @click="pickerVisible = true"><span class="picker-text" v-text="occurredAtText || '使用当前时间'" /></t-form-item>
<t-form-item label="备注"><t-textarea v-model.trim="remark" borderless autosize maxlength="500" placeholder="备注(可选)" /></t-form-item>
</t-form>
<t-form v-if="record" class="form" v-entrance-transition="70">
<t-form-item label="冲正原因" required-mark>
<t-textarea
v-model="reason"
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
v-if="record"
:model-value="photoUrlList"
v-model:photo-items="photoItems"
class="photo-field"
:upload="uploadPhotoList"
:edit-mode="true"
required
title="冲正凭证照片"
/>
</div>
</t-form>
<t-empty v-if="!record" :description="isLoading ? '正在加载资金记录' : '资金记录不存在'" />
<t-popup v-model="pickerVisible" 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-popup v-model="occurredAtPickerVisible" placement="bottom">
<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>
</div>
</template>
<script setup lang="ts">
import { Time } from "timi-web";
import { useUserStore, type Attachment } from "timi-web-api";
import SalesOrderAPI from "@/api/SalesOrderAPI";
import PhotoScroller from "@/components/PhotoScroller.vue";
@@ -52,25 +108,49 @@ const record = ref<FundRecord>();
const reason = ref("");
const remark = ref("");
const occurredAtText = ref("");
const draftTime = ref("");
const pickerVisible = ref(false);
const occurredAtDraftText = ref("");
const occurredAtPickerVisible = ref(false);
const photoItems = ref<PhotoItem[]>([]);
const isLoading = ref(false);
const isSaving = ref(false);
const requestNo = ref("");
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
const canReadSalesOrder = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.read));
function confirmTime(value: string | number): void {
occurredAtText.value = String(value);
pickerVisible.value = false;
const minimumDateTimeText = "1970-01-01 00:00";
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) {
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>[] {
@@ -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> {
if (isSaving.value) {
return;
}
if (!record.value?.id || record.value.status !== "VERIFIED" || record.value.operationType === "REVOKE") {
Message.warning("只有已核销且非冲正的资金记录可以冲正");
if (!record.value?.id || record.value.revokeRecordId || record.value.status !== "VERIFIED" || record.value.operationType === "REVOKE") {
Message.warning("只有已核销、未被冲正且不是冲正记录的资金记录可以冲正");
return;
}
if (!reason.value.trim()) {
@@ -116,7 +219,7 @@ async function save(): Promise<void> {
const result = await SalesOrderAPI.fundRecordRevoke({
requestNo: requestNo.value,
fundRecordId: record.value.id,
occurredAt: resolveTimestamp(occurredAtText.value),
occurredAt: buildOccurredAt(),
reason: reason.value.trim(),
attachmentList: buildAttachments(),
remark: remark.value.trim() || null
@@ -136,7 +239,7 @@ async function save(): Promise<void> {
onMounted(async () => {
if (userStore.hasPermissions(permissionCodes.fund.revoke)) {
navBarStore.setRightText("提交冲正", "var(--td-danger-color)", save);
navBarStore.setRightText("提交", "var(--td-error-color)", save);
}
await loadData();
});
@@ -151,22 +254,15 @@ onUnmounted(() => navBarStore.clearRight());
display: flex;
flex-direction: column;
.form {
margin: 0 1rem;
padding: 0 .5rem;
background: var(--td-bg-color-container);
box-shadow: var(--td-shadow-1);
border-radius: 1rem;
overflow: hidden;
.time-actions {
gap: .5rem;
display: flex;
align-items: center;
justify-content: flex-end;
}
.picker-text {
width: 100%;
text-align: right;
}
.photo-field {
padding: 0 1rem;
.photo-wrap {
padding: 1rem;
}
}
</style>
+9 -1
View File
@@ -40,7 +40,15 @@
/>
</div>
</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">
<photo-scroller
v-if="order"
+3
View File
@@ -141,7 +141,10 @@ export type FundRecord = {
operationType?: FundOperationType;
status?: FundRecordStatus;
amount?: number;
salesOrderAmount?: number;
salesOrderUnpaidAmountSnapshot?: number;
sourceRecordId?: string;
revokeRecordId?: string;
requestNo?: string;
operatorUserId?: string;
verifierUserId?: string;