|
|
|
@@ -0,0 +1,308 @@
|
|
|
|
|
<template>
|
|
|
|
|
<section class="orders-page">
|
|
|
|
|
<el-alert v-if="lastError" :title="lastError" type="error" show-icon closable @close="lastError = ''" />
|
|
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
|
<header class="panel-toolbar order-toolbar">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="section-kicker">M08-D · 订单运营</p>
|
|
|
|
|
<h3>订单筛选与处置</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="toolbar-actions">
|
|
|
|
|
<el-select v-model="storeId" class="filter-select" filterable placeholder="全部授权门店" @change="applyFilters">
|
|
|
|
|
<el-option label="全部授权门店" value="" />
|
|
|
|
|
<el-option v-for="store in stores" :key="store.id" :label="store.name" :value="store.id" />
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-select v-model="status" class="filter-select" placeholder="全部状态" @change="applyFilters">
|
|
|
|
|
<el-option label="全部状态" value="" />
|
|
|
|
|
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-button :icon="Download" :disabled="orders.items.length === 0" @click="exportOrders">导出当前页</el-button>
|
|
|
|
|
<el-button :icon="RefreshCw" :loading="loading" @click="loadOrders">刷新</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div class="order-page-summary">
|
|
|
|
|
<span><small>当前筛选总数</small><strong>{{ orders.total }}</strong></span>
|
|
|
|
|
<span><small>当前页实收</small><strong>{{ money(pagePaidCents) }}</strong></span>
|
|
|
|
|
<span><small>当前页待支付</small><strong>{{ pendingCount }}</strong></span>
|
|
|
|
|
<span><small>当前页进行中</small><strong>{{ activeCount }}</strong></span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-table v-loading="loading" :data="orders.items" class="data-table" row-key="id">
|
|
|
|
|
<el-table-column label="订单" min-width="190">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<div class="stack"><strong>{{ row.orderNo }}</strong><span>#{{ row.id }} · {{ formatDate(row.createdAt) }}</span></div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="门店 / 房间" min-width="190">
|
|
|
|
|
<template #default="{ row }"><div class="stack"><strong>{{ row.storeName }}</strong><span>{{ row.roomNo }} · {{ row.roomName }}</span></div></template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="使用时段" min-width="205">
|
|
|
|
|
<template #default="{ row }"><div class="stack"><strong>{{ formatDate(row.startAt) }}</strong><span>至 {{ formatDate(row.endAt) }}</span></div></template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="金额" min-width="135">
|
|
|
|
|
<template #default="{ row }"><div class="stack"><strong>{{ money(row.paidAmountCents) }}</strong><span>应收 {{ money(row.totalAmountCents) }}</span></div></template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="状态" width="130">
|
|
|
|
|
<template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag></template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" min-width="260" fixed="right">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<div class="row-actions">
|
|
|
|
|
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
|
|
|
|
|
<el-dropdown v-if="getAllowedActions(row.status).length" trigger="click" @command="openAction(row, $event)">
|
|
|
|
|
<el-button link type="primary">状态处置<ChevronDown :size="14" /></el-button>
|
|
|
|
|
<template #dropdown><el-dropdown-menu><el-dropdown-item v-for="action in getAllowedActions(row.status)" :key="action" :command="action">{{ actionLabel(action) }}</el-dropdown-item></el-dropdown-menu></template>
|
|
|
|
|
</el-dropdown>
|
|
|
|
|
<el-button link @click="openNote(row)">备注</el-button>
|
|
|
|
|
<el-button v-if="isAdjustable(row.status)" link @click="openAdjustment(row)">调整</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
<footer class="pager"><el-pagination v-model:current-page="orders.page" :page-size="orders.pageSize" :total="orders.total" layout="prev, pager, next, total" @current-change="loadOrders" /></footer>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<el-drawer v-model="detailDrawer" title="订单详情" size="min(720px, 94vw)">
|
|
|
|
|
<div v-if="detail" class="order-detail-stack">
|
|
|
|
|
<div class="detail-header"><div class="stack"><strong>{{ detail.orderNo }}</strong><span>#{{ detail.id }}</span></div><el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag></div>
|
|
|
|
|
<div class="detail-grid">
|
|
|
|
|
<span><small>门店</small><strong>{{ detail.storeName }}</strong></span>
|
|
|
|
|
<span><small>房间</small><strong>{{ detail.roomNo }} · {{ detail.roomName }}</strong></span>
|
|
|
|
|
<span><small>开始</small><strong>{{ formatDate(detail.startAt) }}</strong></span>
|
|
|
|
|
<span><small>结束</small><strong>{{ formatDate(detail.endAt) }}</strong></span>
|
|
|
|
|
<span><small>应收</small><strong>{{ money(detail.totalAmountCents) }}</strong></span>
|
|
|
|
|
<span><small>实收</small><strong>{{ money(detail.paidAmountCents) }}</strong></span>
|
|
|
|
|
<span><small>支付渠道</small><strong>{{ detail.latestPayment?.provider || '-' }}</strong></span>
|
|
|
|
|
<span><small>支付状态</small><strong>{{ detail.latestPayment?.status || '-' }}</strong></span>
|
|
|
|
|
</div>
|
|
|
|
|
<header class="drawer-section-title"><div><p class="section-kicker">状态流水</p><h3>{{ history.length }} 条记录</h3></div><el-button :icon="RefreshCw" :loading="loadingDetail" @click="loadDetail">刷新</el-button></header>
|
|
|
|
|
<el-timeline>
|
|
|
|
|
<el-timeline-item v-for="item in history" :key="item.id" :timestamp="formatDate(item.createdAt)" placement="top">
|
|
|
|
|
<div class="history-card"><strong>{{ actionLabel(item.action) }} · {{ statusLabel(item.toStatus) }}</strong><span>{{ item.fromStatus ? `${statusLabel(item.fromStatus)} → ` : '' }}{{ statusLabel(item.toStatus) }}</span><small>{{ item.source }} · {{ item.actorId ? `操作人 ${item.actorId}` : item.actorType }} · {{ item.reason || '无备注' }}</small><code v-if="item.traceId">{{ item.traceId }}</code></div>
|
|
|
|
|
</el-timeline-item>
|
|
|
|
|
</el-timeline>
|
|
|
|
|
<el-empty v-if="!loadingDetail && history.length === 0" description="暂无状态流水" />
|
|
|
|
|
</div>
|
|
|
|
|
</el-drawer>
|
|
|
|
|
|
|
|
|
|
<el-dialog v-model="actionDialog.open" :title="actionLabel(actionDialog.action)" width="min(520px, 94vw)">
|
|
|
|
|
<el-form label-position="top"><el-form-item label="处置原因"><el-input v-model="actionDialog.reason" type="textarea" :rows="4" maxlength="512" show-word-limit /></el-form-item></el-form>
|
|
|
|
|
<template #footer><el-button @click="actionDialog.open = false">取消</el-button><el-button type="primary" :loading="saving" @click="saveAction">确认处置</el-button></template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
<el-dialog v-model="noteDialog.open" title="订单运营备注" width="min(520px, 94vw)">
|
|
|
|
|
<el-form label-position="top"><el-form-item label="备注"><el-input v-model="noteDialog.note" type="textarea" :rows="4" maxlength="512" show-word-limit /></el-form-item></el-form>
|
|
|
|
|
<template #footer><el-button @click="noteDialog.open = false">取消</el-button><el-button type="primary" :loading="saving" @click="saveNote">保存备注</el-button></template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
<el-dialog v-model="adjustDialog.open" title="调整订单" width="min(620px, 94vw)">
|
|
|
|
|
<el-tabs v-model="adjustDialog.mode">
|
|
|
|
|
<el-tab-pane label="调整时段" name="time">
|
|
|
|
|
<el-form label-position="top"><el-form-item label="开始时间"><el-date-picker v-model="adjustDialog.startAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" /></el-form-item><el-form-item label="结束时间"><el-date-picker v-model="adjustDialog.endAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" /></el-form-item></el-form>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
<el-tab-pane label="续费延时" name="renew">
|
|
|
|
|
<el-form label-position="top"><el-form-item label="新的结束时间"><el-date-picker v-model="adjustDialog.renewEndAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" /></el-form-item><el-form-item label="计价策略"><el-radio-group v-model="adjustDialog.pricingPolicy"><el-radio value="LOCKED">沿用下单价</el-radio><el-radio value="CURRENT">使用当前价</el-radio></el-radio-group></el-form-item></el-form>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
<el-tab-pane label="更换房间" name="room">
|
|
|
|
|
<el-form label-position="top"><el-form-item label="目标房间"><el-select v-model="adjustDialog.roomId" filterable><el-option v-for="room in availableRooms" :key="room.id" :label="`${room.roomNo} · ${room.name} · ${money(room.basePriceCents)}`" :value="room.id" /></el-select></el-form-item></el-form>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
</el-tabs>
|
|
|
|
|
<el-form label-position="top"><el-form-item label="调整原因"><el-input v-model="adjustDialog.reason" type="textarea" maxlength="512" show-word-limit /></el-form-item></el-form>
|
|
|
|
|
<template #footer><el-button @click="adjustDialog.open = false">取消</el-button><el-button type="primary" :loading="saving" @click="saveAdjustment">确认调整</el-button></template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
|
|
import { ChevronDown, Download, RefreshCw } from '@lucide/vue';
|
|
|
|
|
import {
|
|
|
|
|
ApiError,
|
|
|
|
|
addManagedOrderNote,
|
|
|
|
|
adjustManagedOrderTime,
|
|
|
|
|
changeManagedOrderRoom,
|
|
|
|
|
executeManagedOrderAction,
|
|
|
|
|
getManagedOrder,
|
|
|
|
|
listManagedOrderHistory,
|
|
|
|
|
listManagedOrders,
|
|
|
|
|
listManagedRooms,
|
|
|
|
|
listManagedStores,
|
|
|
|
|
renewManagedOrder,
|
|
|
|
|
type ApiSession
|
|
|
|
|
} from '../api';
|
|
|
|
|
import { money } from '../format';
|
|
|
|
|
import type { ManagedOrder, ManagedRoom, ManagedStore, OrderAction, OrderHistoryItem, OrderStatus, PageResult } from '../types';
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{ session: ApiSession }>();
|
|
|
|
|
const stores = ref<ManagedStore[]>([]);
|
|
|
|
|
const availableRooms = ref<ManagedRoom[]>([]);
|
|
|
|
|
const orders = reactive<PageResult<ManagedOrder>>({ items: [], total: 0, page: 1, pageSize: 20 });
|
|
|
|
|
const storeId = ref('');
|
|
|
|
|
const status = ref<OrderStatus | ''>('');
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const loadingDetail = ref(false);
|
|
|
|
|
const saving = ref(false);
|
|
|
|
|
const lastError = ref('');
|
|
|
|
|
const detailDrawer = ref(false);
|
|
|
|
|
const detail = ref<ManagedOrder | null>(null);
|
|
|
|
|
const history = ref<OrderHistoryItem[]>([]);
|
|
|
|
|
const actionDialog = reactive<{ open: boolean; orderId: string; action: OrderAction; reason: string }>({ open: false, orderId: '', action: 'SUBMIT', reason: '' });
|
|
|
|
|
const noteDialog = reactive({ open: false, orderId: '', note: '' });
|
|
|
|
|
const adjustDialog = reactive({ open: false, orderId: '', storeId: '', currentRoomId: '', mode: 'time' as 'time' | 'renew' | 'room', startAt: '', endAt: '', renewEndAt: '', pricingPolicy: 'LOCKED' as 'CURRENT' | 'LOCKED', roomId: '', reason: '' });
|
|
|
|
|
|
|
|
|
|
const statusOptions: Array<{ value: OrderStatus; label: string }> = [
|
|
|
|
|
{ value: 'DRAFT', label: '草稿' }, { value: 'PENDING_PAYMENT', label: '待支付' },
|
|
|
|
|
{ value: 'PAID', label: '已支付' }, { value: 'RESERVED', label: '已预订' },
|
|
|
|
|
{ value: 'IN_PROGRESS', label: '使用中' }, { value: 'FINISHED', label: '已完成' },
|
|
|
|
|
{ value: 'CANCELLED', label: '已取消' }, { value: 'REFUNDING', label: '退款中' },
|
|
|
|
|
{ value: 'REFUNDED', label: '已退款' }, { value: 'CLOSED', label: '已关闭' }
|
|
|
|
|
];
|
|
|
|
|
const allowedActions: Record<OrderStatus, OrderAction[]> = {
|
|
|
|
|
DRAFT: ['SUBMIT', 'CANCEL', 'CLOSE'], PENDING_PAYMENT: ['CONFIRM_PAYMENT', 'CANCEL', 'CLOSE'],
|
|
|
|
|
PAID: ['RESERVE', 'START', 'CANCEL', 'BEGIN_REFUND'], RESERVED: ['START', 'CANCEL', 'BEGIN_REFUND'],
|
|
|
|
|
IN_PROGRESS: ['FINISH', 'BEGIN_REFUND'], FINISHED: ['BEGIN_REFUND', 'CLOSE'],
|
|
|
|
|
CANCELLED: ['BEGIN_REFUND', 'CLOSE'], REFUNDING: ['COMPLETE_REFUND'], REFUNDED: ['CLOSE'], CLOSED: []
|
|
|
|
|
};
|
|
|
|
|
const actionLabels: Record<string, string> = {
|
|
|
|
|
SUBMIT: '提交待支付', CONFIRM_PAYMENT: '确认支付', RESERVE: '确认预订', START: '开始使用',
|
|
|
|
|
FINISH: '结束使用', CANCEL: '取消订单', BEGIN_REFUND: '发起退款', COMPLETE_REFUND: '完成退款',
|
|
|
|
|
CLOSE: '关闭订单', CREATED: '创建订单', EXPIRED: '订单过期', MIGRATED: '历史迁移'
|
|
|
|
|
};
|
|
|
|
|
const pagePaidCents = computed(() => orders.items.reduce((sum, item) => sum + Number(item.paidAmountCents || 0), 0));
|
|
|
|
|
const pendingCount = computed(() => orders.items.filter((item) => item.status === 'PENDING_PAYMENT').length);
|
|
|
|
|
const activeCount = computed(() => orders.items.filter((item) => ['PAID', 'RESERVED', 'IN_PROGRESS'].includes(item.status)).length);
|
|
|
|
|
|
|
|
|
|
async function capture<T>(work: () => Promise<T>) {
|
|
|
|
|
lastError.value = '';
|
|
|
|
|
try { return await work(); }
|
|
|
|
|
catch (error) {
|
|
|
|
|
lastError.value = error instanceof ApiError ? `${error.code}${error.traceId ? ` · ${error.traceId}` : ''}` : error instanceof Error ? error.message : '操作失败';
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadWorkspace() {
|
|
|
|
|
try {
|
|
|
|
|
stores.value = await capture(() => listManagedStores(props.session));
|
|
|
|
|
await loadOrders();
|
|
|
|
|
} catch { /* displayed */ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadOrders() {
|
|
|
|
|
if (!props.session.token) return;
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
Object.assign(orders, await capture(() => listManagedOrders(props.session, {
|
|
|
|
|
page: orders.page, pageSize: orders.pageSize, status: status.value || undefined, storeId: storeId.value || undefined
|
|
|
|
|
})));
|
|
|
|
|
} catch { /* displayed */ } finally { loading.value = false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyFilters() { orders.page = 1; void loadOrders(); }
|
|
|
|
|
|
|
|
|
|
async function openDetail(order: ManagedOrder) {
|
|
|
|
|
detail.value = order;
|
|
|
|
|
detailDrawer.value = true;
|
|
|
|
|
await loadDetail();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadDetail() {
|
|
|
|
|
if (!detail.value) return;
|
|
|
|
|
loadingDetail.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const [order, items] = await Promise.all([
|
|
|
|
|
capture(() => getManagedOrder(props.session, detail.value!.id)),
|
|
|
|
|
capture(() => listManagedOrderHistory(props.session, detail.value!.id))
|
|
|
|
|
]);
|
|
|
|
|
detail.value = order;
|
|
|
|
|
history.value = items;
|
|
|
|
|
} catch { /* displayed */ } finally { loadingDetail.value = false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openAction(order: ManagedOrder, command: unknown) {
|
|
|
|
|
Object.assign(actionDialog, { open: true, orderId: order.id, action: String(command) as OrderAction, reason: '' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveAction() {
|
|
|
|
|
if (!actionDialog.reason.trim()) return ElMessage.warning('请填写处置原因');
|
|
|
|
|
saving.value = true;
|
|
|
|
|
try {
|
|
|
|
|
await capture(() => executeManagedOrderAction(props.session, actionDialog.orderId, actionDialog.action, actionDialog.reason.trim()));
|
|
|
|
|
actionDialog.open = false;
|
|
|
|
|
ElMessage.success('订单状态已更新');
|
|
|
|
|
await loadOrders();
|
|
|
|
|
if (detail.value?.id === actionDialog.orderId) await loadDetail();
|
|
|
|
|
} catch { /* displayed */ } finally { saving.value = false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openNote(order: ManagedOrder) { Object.assign(noteDialog, { open: true, orderId: order.id, note: '' }); }
|
|
|
|
|
async function saveNote() {
|
|
|
|
|
if (!noteDialog.note.trim()) return ElMessage.warning('请填写备注');
|
|
|
|
|
saving.value = true;
|
|
|
|
|
try {
|
|
|
|
|
await capture(() => addManagedOrderNote(props.session, noteDialog.orderId, noteDialog.note.trim()));
|
|
|
|
|
noteDialog.open = false;
|
|
|
|
|
ElMessage.success('订单备注已保存');
|
|
|
|
|
if (detail.value?.id === noteDialog.orderId) await loadDetail();
|
|
|
|
|
} catch { /* displayed */ } finally { saving.value = false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openAdjustment(order: ManagedOrder) {
|
|
|
|
|
Object.assign(adjustDialog, {
|
|
|
|
|
open: true, orderId: order.id, storeId: order.storeId, currentRoomId: order.roomId,
|
|
|
|
|
mode: 'time', startAt: toPickerValue(order.startAt), endAt: toPickerValue(order.endAt),
|
|
|
|
|
renewEndAt: toPickerValue(order.endAt), pricingPolicy: 'LOCKED', roomId: '', reason: ''
|
|
|
|
|
});
|
|
|
|
|
try { availableRooms.value = (await capture(() => listManagedRooms(props.session, order.storeId))).filter((room) => room.id !== order.roomId && room.configurationStatus === 'ENABLED'); }
|
|
|
|
|
catch { availableRooms.value = []; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveAdjustment() {
|
|
|
|
|
if (!adjustDialog.reason.trim()) return ElMessage.warning('请填写调整原因');
|
|
|
|
|
saving.value = true;
|
|
|
|
|
try {
|
|
|
|
|
if (adjustDialog.mode === 'time') {
|
|
|
|
|
if (!adjustDialog.startAt && !adjustDialog.endAt) return ElMessage.warning('至少填写一个时间');
|
|
|
|
|
await capture(() => adjustManagedOrderTime(props.session, adjustDialog.orderId, {
|
|
|
|
|
startAt: adjustDialog.startAt || undefined, endAt: adjustDialog.endAt || undefined, reason: adjustDialog.reason.trim()
|
|
|
|
|
}));
|
|
|
|
|
} else if (adjustDialog.mode === 'renew') {
|
|
|
|
|
if (!adjustDialog.renewEndAt) return ElMessage.warning('请选择新的结束时间');
|
|
|
|
|
await capture(() => renewManagedOrder(props.session, adjustDialog.orderId, {
|
|
|
|
|
endAt: adjustDialog.renewEndAt, pricingPolicy: adjustDialog.pricingPolicy, reason: adjustDialog.reason.trim()
|
|
|
|
|
}));
|
|
|
|
|
} else {
|
|
|
|
|
if (!adjustDialog.roomId) return ElMessage.warning('请选择目标房间');
|
|
|
|
|
await capture(() => changeManagedOrderRoom(props.session, adjustDialog.orderId, {
|
|
|
|
|
roomId: adjustDialog.roomId, reason: adjustDialog.reason.trim()
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
adjustDialog.open = false;
|
|
|
|
|
ElMessage.success('订单调整已完成');
|
|
|
|
|
await loadOrders();
|
|
|
|
|
if (detail.value?.id === adjustDialog.orderId) await loadDetail();
|
|
|
|
|
} catch { /* displayed */ } finally { saving.value = false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function exportOrders() {
|
|
|
|
|
const rows = [['订单号', '门店', '房间', '状态', '开始', '结束', '应收', '实收', '支付渠道', '支付状态']];
|
|
|
|
|
for (const order of orders.items) rows.push([order.orderNo, order.storeName, `${order.roomNo} ${order.roomName}`, statusLabel(order.status), formatDate(order.startAt), formatDate(order.endAt), money(order.totalAmountCents), money(order.paidAmountCents), order.latestPayment?.provider || '', order.latestPayment?.status || '']);
|
|
|
|
|
const csv = rows.map((row) => row.map(csvCell).join(',')).join('\r\n');
|
|
|
|
|
const url = URL.createObjectURL(new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8' }));
|
|
|
|
|
const link = document.createElement('a'); link.href = url; link.download = `orders-${Date.now()}.csv`; link.click(); URL.revokeObjectURL(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function csvCell(value: string) { const safe = String(value).replace(/\r?\n/g, ' '); return /[",\r\n]/.test(safe) ? `"${safe.replace(/"/g, '""')}"` : safe; }
|
|
|
|
|
function statusLabel(value: OrderStatus) { return statusOptions.find((item) => item.value === value)?.label || value; }
|
|
|
|
|
function statusType(value: OrderStatus) { return value === 'IN_PROGRESS' ? 'success' : ['PENDING_PAYMENT', 'REFUNDING'].includes(value) ? 'warning' : ['CANCELLED', 'CLOSED'].includes(value) ? 'info' : 'primary'; }
|
|
|
|
|
function actionLabel(value: string) { return actionLabels[value] || value; }
|
|
|
|
|
function getAllowedActions(value: OrderStatus) { return allowedActions[value] || []; }
|
|
|
|
|
function formatDate(value: string) { const date = new Date(value); return Number.isNaN(date.getTime()) ? value : new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }).format(date); }
|
|
|
|
|
function toPickerValue(value: string) { const date = new Date(value); return Number.isNaN(date.getTime()) ? '' : date.toISOString(); }
|
|
|
|
|
function isAdjustable(value: OrderStatus) { return ['PENDING_PAYMENT', 'PAID', 'RESERVED', 'IN_PROGRESS'].includes(value); }
|
|
|
|
|
|
|
|
|
|
watch(() => props.session.token, (token) => { if (token) void loadWorkspace(); }, { immediate: true });
|
|
|
|
|
</script>
|