fix SalesOrderRefund.vue style
This commit is contained in:
@@ -1,40 +1,127 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="permissionCodes.fund.refund" class="sales-order-operation">
|
<div v-can="permissionCodes.fund.refund" class="sales-order-operation">
|
||||||
<t-cell-group v-if="order" title="销售订单" theme="card">
|
<t-cell-group
|
||||||
<t-cell title="订单号" :note="order.orderNo || order.id" />
|
|
||||||
<t-cell title="可退款金额" :note="formatPackageAmount((order.paidAmount || 0) - (order.refundedAmount || 0))" />
|
|
||||||
</t-cell-group>
|
|
||||||
<t-form v-if="order" class="form" label-align="left" label-width="6rem">
|
|
||||||
<t-form-item label="退款金额(元)" required-mark><t-input v-model.trim="amountText" type="number" align="right" borderless placeholder="请输入退款金额" /></t-form-item>
|
|
||||||
<t-form-item label="退款原因" required-mark><t-textarea v-model.trim="reason" borderless autosize maxlength="500" /></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" /></t-form-item>
|
|
||||||
</t-form>
|
|
||||||
<photo-scroller
|
|
||||||
v-if="order"
|
v-if="order"
|
||||||
:model-value="photoUrlList"
|
title="销售订单"
|
||||||
v-model:photo-items="photoItems"
|
theme="card"
|
||||||
class="photo-field"
|
v-entrance-transition
|
||||||
:upload="uploadPhotoList"
|
>
|
||||||
:edit-mode="true"
|
<t-cell title="订单号" :note="order.orderNo || order.id"/>
|
||||||
required
|
<t-cell
|
||||||
title="退款凭证照片"
|
title="可退款金额"
|
||||||
/>
|
:note="formatPackageAmount(refundableAmount)"
|
||||||
<t-empty v-if="!order" :description="loading ? '正在加载订单' : '订单不存在'" />
|
/>
|
||||||
<t-popup v-model="pickerVisible" placement="bottom">
|
</t-cell-group>
|
||||||
<t-date-time-picker v-model="draftTime" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="退款时间" @cancel="pickerVisible = false" @confirm="confirmTime" />
|
<t-form
|
||||||
|
v-if="order"
|
||||||
|
class="form"
|
||||||
|
label-width="6rem"
|
||||||
|
v-entrance-transition="70"
|
||||||
|
>
|
||||||
|
<t-form-item label="退款金额" required-mark>
|
||||||
|
<t-input
|
||||||
|
v-model.trim="amountText"
|
||||||
|
type="number"
|
||||||
|
align="right"
|
||||||
|
borderless
|
||||||
|
placeholder="输入退款金额"
|
||||||
|
>
|
||||||
|
<template #suffix-icon>
|
||||||
|
<t-button
|
||||||
|
size="extra-small"
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
|
@click="fillRefundAmount"
|
||||||
|
>全额退款</t-button>
|
||||||
|
</template>
|
||||||
|
</t-input>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="退款原因" required-mark>
|
||||||
|
<div class="reason">
|
||||||
|
<t-textarea v-model="reason" borderless autosize maxlength="500" placeholder="输入退款原因" />
|
||||||
|
<t-button
|
||||||
|
size="extra-small"
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
|
@click="openRefundReasonPicker"
|
||||||
|
>选择</t-button>
|
||||||
|
</div>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label-width="0">
|
||||||
|
<photo-scroller
|
||||||
|
v-if="order"
|
||||||
|
:model-value="photoUrlList"
|
||||||
|
v-model:photo-items="photoItems"
|
||||||
|
class="photo-field"
|
||||||
|
:upload="uploadPhotoList"
|
||||||
|
:edit-mode="true"
|
||||||
|
required
|
||||||
|
title="退款凭证照片"
|
||||||
|
/>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
<t-empty v-if="!order" :description="loading ? '正在加载订单' : '订单不存在'"/>
|
||||||
|
<t-popup v-model="refundReasonPickerVisible" placement="bottom">
|
||||||
|
<t-picker
|
||||||
|
v-if="refundReasonPickerVisible"
|
||||||
|
v-model="refundReasonPickerDraft"
|
||||||
|
:columns="refundReasonPickerColumns"
|
||||||
|
title="选择退款原因"
|
||||||
|
@cancel="refundReasonPickerVisible = false"
|
||||||
|
@confirm="confirmRefundReason"
|
||||||
|
/>
|
||||||
|
</t-popup>
|
||||||
|
<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>
|
</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 PhotoScroller from "@/components/PhotoScroller.vue";
|
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||||
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { SalesOrder } from "@/types/SalesOrder";
|
import type { SalesOrder } from "@/types/SalesOrder";
|
||||||
import type { PhotoItem } from "@/types/Photo";
|
import type { PhotoItem } from "@/types/Photo";
|
||||||
import { createPackageRequestNo, formatPackageAmount, parsePackageAmount } from "@/utils/package";
|
import { createPackageRequestNo, formatPackageAmount, formatPackageAmountInput, parsePackageAmount } from "@/utils/package";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
@@ -50,30 +137,98 @@ const loading = ref(false);
|
|||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const amountText = ref("");
|
const amountText = ref("");
|
||||||
const reason = ref("");
|
const reason = ref("");
|
||||||
|
const minimumDateTimeText = "1970-01-01 00:00";
|
||||||
const occurredAtText = ref("");
|
const occurredAtText = ref("");
|
||||||
const draftTime = ref("");
|
const occurredAtDraftText = ref("");
|
||||||
const pickerVisible = ref(false);
|
const occurredAtPickerVisible = ref(false);
|
||||||
|
const refundReasonPickerDraft = ref<string[]>([]);
|
||||||
|
const refundReasonPickerVisible = ref(false);
|
||||||
const remark = ref("");
|
const remark = ref("");
|
||||||
const photoItems = ref<PhotoItem[]>([]);
|
const photoItems = ref<PhotoItem[]>([]);
|
||||||
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
|
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
|
||||||
const requestNo = ref("");
|
const requestNo = ref("");
|
||||||
function confirmTime(value: string | number): void { occurredAtText.value = String(value); pickerVisible.value = false; }
|
const refundableAmount = computed(() => (order.value?.paidAmount || 0) - (order.value?.refundedAmount || 0));
|
||||||
function resolveTimestamp(value: string): number | undefined { if (!value) return undefined; const timestamp = new Date(value.replace(" ", "T")).getTime(); return Number.isFinite(timestamp) ? timestamp : undefined; }
|
const refundReasonOptions = [
|
||||||
function buildAttachments(): Partial<Attachment>[] { return photoItems.value.flatMap(item => item.tempFileId ? [{ tempFileId: item.tempFileId, attachType: "PHOTO" }] : []); }
|
"取消订单",
|
||||||
|
"客户主动申请退款",
|
||||||
|
"商品缺货,无法履约",
|
||||||
|
"商品或服务与描述不符",
|
||||||
|
"客户重复下单",
|
||||||
|
"收款金额有误",
|
||||||
|
"其他售后原因"
|
||||||
|
];
|
||||||
|
const refundReasonPickerColumns = refundReasonOptions.map(item => ({ label: item, value: item }));
|
||||||
|
|
||||||
|
function openOccurredAtPicker(): void {
|
||||||
|
occurredAtDraftText.value = occurredAtText.value || Time.formatDateTime(new Date());
|
||||||
|
occurredAtPickerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmOccurredAt(value?: string | number): void {
|
||||||
|
occurredAtText.value = String(value || occurredAtDraftText.value);
|
||||||
|
occurredAtPickerVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetOccurredAt(): void {
|
||||||
|
occurredAtText.value = "";
|
||||||
|
occurredAtDraftText.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillRefundAmount(): void {
|
||||||
|
amountText.value = formatPackageAmountInput(refundableAmount.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRefundReasonPicker(): void {
|
||||||
|
const currentReason = refundReasonOptions.includes(reason.value) ? reason.value : refundReasonOptions[0];
|
||||||
|
refundReasonPickerDraft.value = [currentReason];
|
||||||
|
refundReasonPickerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmRefundReason(value: Array<string | number>): void {
|
||||||
|
reason.value = String(value[0] || refundReasonPickerDraft.value[0] || "");
|
||||||
|
refundReasonPickerVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTimestamp(value: string): number | undefined {
|
||||||
|
if (!value) return undefined;
|
||||||
|
const timestamp = new Date(value.replace(" ", "T")).getTime();
|
||||||
|
return Number.isFinite(timestamp) ? timestamp : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAttachments(): Partial<Attachment>[] {
|
||||||
|
return photoItems.value.flatMap(item => item.tempFileId ? [{
|
||||||
|
tempFileId: item.tempFileId,
|
||||||
|
attachType: "PHOTO"
|
||||||
|
}] : []);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadData(): Promise<void> {
|
async function loadData(): Promise<void> {
|
||||||
const id = String(route.params.id || "");
|
const id = String(route.params.id || "");
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try { order.value = await SalesOrderAPI.detail(id); }
|
try {
|
||||||
catch (error) { Message.error(getErrorMessage(error, "订单加载失败")); }
|
order.value = await SalesOrderAPI.detail(id);
|
||||||
finally { loading.value = false; }
|
} catch (error) {
|
||||||
|
Message.error(getErrorMessage(error, "订单加载失败"));
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(): Promise<void> {
|
async function save(): Promise<void> {
|
||||||
if (saving.value || !order.value?.id) return;
|
if (saving.value || !order.value?.id) return;
|
||||||
const amount = parsePackageAmount(amountText.value); const attachmentList = buildAttachments();
|
const amount = parsePackageAmount(amountText.value);
|
||||||
if (amount === undefined || amount === 0 || !reason.value) { Message.warning("退款金额和原因不能为空"); return; }
|
const attachmentList = buildAttachments();
|
||||||
if (!attachmentList.length) { Message.warning("退款必须上传至少一张照片"); return; }
|
if (amount === undefined || amount === 0 || !reason.value) {
|
||||||
if (!requestNo.value) requestNo.value = createPackageRequestNo(); saving.value = true;
|
Message.warning("退款金额和原因不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!attachmentList.length) {
|
||||||
|
Message.warning("退款必须上传至少一张照片");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!requestNo.value) requestNo.value = createPackageRequestNo();
|
||||||
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
await SalesOrderAPI.refund({
|
await SalesOrderAPI.refund({
|
||||||
salesOrderId: order.value.id,
|
salesOrderId: order.value.id,
|
||||||
@@ -86,17 +241,51 @@ async function save(): Promise<void> {
|
|||||||
});
|
});
|
||||||
Message.success("退款登记已提交");
|
Message.success("退款登记已提交");
|
||||||
await router.replace(`/management/sales-orders/${order.value.id}`);
|
await router.replace(`/management/sales-orders/${order.value.id}`);
|
||||||
} catch (error) { Message.error(getErrorMessage(error, "退款登记失败,重试会复用同一请求号")); }
|
} catch (error) {
|
||||||
finally { saving.value = false; }
|
Message.error(getErrorMessage(error, "退款登记失败,重试会复用同一请求号"));
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onMounted(async () => { if (!userStore.hasPermissions(permissionCodes.fund.refund)) return; navBarStore.setRightText("提交退款", "var(--td-danger-color)", save); await loadData(); });
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!userStore.hasPermissions(permissionCodes.fund.refund)) return;
|
||||||
|
navBarStore.setRightText("提交", "var(--td-error-color)", save);
|
||||||
|
await loadData();
|
||||||
|
});
|
||||||
onUnmounted(() => navBarStore.clearRight());
|
onUnmounted(() => navBarStore.clearRight());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.sales-order-operation { gap: 1rem; padding: calc(var(--app-nav-height) + 1rem) 0 2rem; display: flex; flex-direction: column;
|
.sales-order-operation {
|
||||||
.form { margin: 0 1rem; padding: 0 .5rem; background: var(--td-bg-color-container); box-shadow: var(--td-shadow-1); border-radius: 1rem; overflow: hidden; }
|
gap: 1rem;
|
||||||
.picker-text { width: 100%; text-align: right; }
|
padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
|
||||||
.photo-field { padding: 0 1rem; }
|
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;
|
||||||
|
|
||||||
|
.reason {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-actions {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-field {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user