feat(project): implement all-orders page with modular structure
All checks were successful
Build and Deploy TenantUI / build-and-deploy (push) Successful in 55s
All checks were successful
Build and Deploy TenantUI / build-and-deploy (push) Successful in 55s
This commit is contained in:
140
apps/web-antd/src/api/order/index.ts
Normal file
140
apps/web-antd/src/api/order/index.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 文件职责:订单管理(全部订单)接口与类型定义。
|
||||
*/
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** 全部订单状态筛选值。 */
|
||||
export type OrderAllStatusFilter =
|
||||
| 'all'
|
||||
| 'cancelled'
|
||||
| 'completed'
|
||||
| 'delivering'
|
||||
| 'making'
|
||||
| 'pending'
|
||||
| 'pickup'
|
||||
| 'refunded';
|
||||
|
||||
/** 全部订单渠道筛选值。 */
|
||||
export type OrderAllChannelFilter = 'all' | 'delivery' | 'dine_in' | 'pickup';
|
||||
|
||||
/** 全部订单支付方式筛选值。 */
|
||||
export type OrderAllPaymentFilter =
|
||||
| 'alipay'
|
||||
| 'all'
|
||||
| 'balance'
|
||||
| 'card'
|
||||
| 'cash'
|
||||
| 'wechat';
|
||||
|
||||
/** 全部订单筛选参数。 */
|
||||
export interface OrderAllFilterQuery {
|
||||
channel?: OrderAllChannelFilter;
|
||||
endDate?: string;
|
||||
keyword?: string;
|
||||
paymentMethod?: OrderAllPaymentFilter;
|
||||
startDate?: string;
|
||||
status?: OrderAllStatusFilter;
|
||||
storeId: string;
|
||||
}
|
||||
|
||||
/** 全部订单列表查询参数。 */
|
||||
export interface OrderAllListQuery extends OrderAllFilterQuery {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** 全部订单列表行。 */
|
||||
export interface OrderAllListItemDto {
|
||||
amount: number;
|
||||
channel: string;
|
||||
customer: string;
|
||||
isDimmed: boolean;
|
||||
itemsSummary: string;
|
||||
orderNo: string;
|
||||
orderedAt: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** 全部订单列表结果。 */
|
||||
export interface OrderAllListResultDto {
|
||||
items: OrderAllListItemDto[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** 全部订单统计。 */
|
||||
export interface OrderAllStatsDto {
|
||||
averageAmount: number;
|
||||
refundCount: number;
|
||||
totalAmount: number;
|
||||
totalOrders: number;
|
||||
}
|
||||
|
||||
/** 全部订单详情商品行。 */
|
||||
export interface OrderAllDetailItemDto {
|
||||
name: string;
|
||||
quantity: number;
|
||||
spec: string;
|
||||
subTotal: number;
|
||||
unitPrice: number;
|
||||
}
|
||||
|
||||
/** 全部订单详情时间线。 */
|
||||
export interface OrderAllTimelineDto {
|
||||
label: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
/** 全部订单详情。 */
|
||||
export interface OrderAllDetailDto {
|
||||
channel: string;
|
||||
customerAddress: string;
|
||||
customerName: string;
|
||||
customerPhone: string;
|
||||
deliveryFee: number;
|
||||
discountAmount: number;
|
||||
finishedAt?: null | string;
|
||||
items: OrderAllDetailItemDto[];
|
||||
itemsAmount: number;
|
||||
orderNo: string;
|
||||
orderedAt: string;
|
||||
paidAmount: number;
|
||||
paidAt?: null | string;
|
||||
paymentMethod: string;
|
||||
remark: string;
|
||||
status: string;
|
||||
timeline: OrderAllTimelineDto[];
|
||||
}
|
||||
|
||||
/** 全部订单导出回执。 */
|
||||
export interface OrderAllExportDto {
|
||||
fileContentBase64: string;
|
||||
fileName: string;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
/** 查询全部订单列表。 */
|
||||
export async function getOrderAllListApi(params: OrderAllListQuery) {
|
||||
return requestClient.get<OrderAllListResultDto>('/order/all/list', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询全部订单统计。 */
|
||||
export async function getOrderAllStatsApi(params: OrderAllFilterQuery) {
|
||||
return requestClient.get<OrderAllStatsDto>('/order/all/stats', { params });
|
||||
}
|
||||
|
||||
/** 查询全部订单详情。 */
|
||||
export async function getOrderAllDetailApi(params: {
|
||||
orderNo: string;
|
||||
storeId: string;
|
||||
}) {
|
||||
return requestClient.get<OrderAllDetailDto>('/order/all/detail', { params });
|
||||
}
|
||||
|
||||
/** 导出全部订单 CSV。 */
|
||||
export async function exportOrderAllCsvApi(params: OrderAllFilterQuery) {
|
||||
return requestClient.get<OrderAllExportDto>('/order/all/export', { params });
|
||||
}
|
||||
27
apps/web-antd/src/router/routes/modules/order.ts
Normal file
27
apps/web-antd/src/router/routes/modules/order.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
/** 文件职责:订单管理模块静态路由。 */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:receipt-text',
|
||||
order: 15,
|
||||
title: '订单管理',
|
||||
},
|
||||
name: 'Order',
|
||||
path: '/order',
|
||||
children: [
|
||||
{
|
||||
name: 'OrderAll',
|
||||
path: '/order/all',
|
||||
component: () => import('#/views/order/all/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:list-ordered',
|
||||
title: '全部订单',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import type { OrderAllDetailDto } from '#/api/order';
|
||||
|
||||
import { Drawer, Empty, Spin, Tag } from 'ant-design-vue';
|
||||
|
||||
import { formatCurrency } from '../composables/order-all-page/helpers';
|
||||
|
||||
interface Props {
|
||||
detail: null | OrderAllDetailDto;
|
||||
loading: boolean;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'close'): void;
|
||||
}>();
|
||||
|
||||
function resolveStatusTagColor(status: string) {
|
||||
if (status.includes('退款')) return 'red';
|
||||
if (status.includes('取消')) return 'default';
|
||||
if (status.includes('完成')) return 'green';
|
||||
if (status.includes('接单') || status.includes('取餐')) return 'orange';
|
||||
if (status.includes('制作') || status.includes('配送')) return 'blue';
|
||||
return 'default';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer
|
||||
:open="props.open"
|
||||
width="560"
|
||||
:title="`订单详情 ${props.detail?.orderNo ?? ''}`"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<Spin :spinning="props.loading">
|
||||
<template v-if="props.detail">
|
||||
<div class="oa-section">
|
||||
<div class="oa-section-title">基本信息</div>
|
||||
<div class="oa-info-grid">
|
||||
<div>
|
||||
<span class="label">订单号:</span>{{ props.detail.orderNo }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">渠道:</span>{{ props.detail.channel }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">状态:</span>
|
||||
<Tag :color="resolveStatusTagColor(props.detail.status)">
|
||||
{{ props.detail.status }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">支付方式:</span>
|
||||
<span>{{ props.detail.paymentMethod }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">下单时间:</span>
|
||||
<span>{{ props.detail.orderedAt }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">支付时间:</span>
|
||||
<span>{{ props.detail.paidAt || '--' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">完成时间:</span>
|
||||
<span>{{ props.detail.finishedAt || '--' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oa-section">
|
||||
<div class="oa-section-title">顾客信息</div>
|
||||
<div class="oa-info-grid">
|
||||
<div>
|
||||
<span class="label">姓名:</span>
|
||||
<span>{{ props.detail.customerName }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">手机号:</span>
|
||||
<span>{{ props.detail.customerPhone }}</span>
|
||||
</div>
|
||||
<div class="full">
|
||||
<span class="label">收货地址:</span>
|
||||
<span>{{ props.detail.customerAddress }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oa-section">
|
||||
<div class="oa-section-title">商品明细</div>
|
||||
<table class="oa-detail-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>商品</th>
|
||||
<th>规格</th>
|
||||
<th>数量</th>
|
||||
<th>单价</th>
|
||||
<th class="right">小计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(item, index) in props.detail.items"
|
||||
:key="`${item.name}-${item.spec}-${index}`"
|
||||
>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.spec }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ formatCurrency(item.unitPrice) }}</td>
|
||||
<td class="right">{{ formatCurrency(item.subTotal) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="oa-amount-summary">
|
||||
<div>
|
||||
<span>商品合计</span>
|
||||
<span>{{ formatCurrency(props.detail.itemsAmount) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>配送费</span>
|
||||
<span>{{ formatCurrency(props.detail.deliveryFee) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>优惠减免</span>
|
||||
<span class="discount">
|
||||
-{{ formatCurrency(props.detail.discountAmount) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="total">
|
||||
<span>实付金额</span>
|
||||
<span>{{ formatCurrency(props.detail.paidAmount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oa-section">
|
||||
<div class="oa-section-title">状态时间线</div>
|
||||
<div class="oa-timeline">
|
||||
<div
|
||||
v-for="(item, index) in props.detail.timeline"
|
||||
:key="`${item.time}-${index}`"
|
||||
class="oa-timeline-item"
|
||||
>
|
||||
<span class="dot"></span>
|
||||
<span class="text">{{ item.label }}</span>
|
||||
<span class="time">{{ item.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oa-section">
|
||||
<div class="oa-section-title">备注</div>
|
||||
<div class="oa-remark">{{ props.detail.remark || '无' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<Empty v-else description="暂无详情" />
|
||||
</Spin>
|
||||
</Drawer>
|
||||
</template>
|
||||
119
apps/web-antd/src/views/order/all/components/OrderFilterBar.vue
Normal file
119
apps/web-antd/src/views/order/all/components/OrderFilterBar.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, Input, Select } from 'ant-design-vue';
|
||||
|
||||
import StoreScopeToolbar from '../../../shared/components/StoreScopeToolbar.vue';
|
||||
import {
|
||||
CHANNEL_OPTIONS,
|
||||
PAYMENT_OPTIONS,
|
||||
STATUS_OPTIONS,
|
||||
} from '../composables/order-all-page/constants';
|
||||
|
||||
interface OptionItem {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface FilterState {
|
||||
channel: string;
|
||||
endDate: string;
|
||||
keyword: string;
|
||||
paymentMethod: string;
|
||||
startDate: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
filters: FilterState;
|
||||
isExporting: boolean;
|
||||
isStoreLoading: boolean;
|
||||
selectedStoreId: string;
|
||||
storeOptions: OptionItem[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'export'): void;
|
||||
(event: 'reset'): void;
|
||||
(event: 'search'): void;
|
||||
(event: 'update:channel', value: string): void;
|
||||
(event: 'update:endDate', value: string): void;
|
||||
(event: 'update:keyword', value: string): void;
|
||||
(event: 'update:paymentMethod', value: string): void;
|
||||
(event: 'update:selectedStoreId', value: string): void;
|
||||
(event: 'update:startDate', value: string): void;
|
||||
(event: 'update:status', value: string): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<StoreScopeToolbar
|
||||
:selected-store-id="props.selectedStoreId"
|
||||
:store-options="props.storeOptions"
|
||||
:is-store-loading="props.isStoreLoading"
|
||||
:show-copy-button="false"
|
||||
@update:selected-store-id="(value) => emit('update:selectedStoreId', value)"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="oa-filter-actions">
|
||||
<Input
|
||||
class="oa-date-input"
|
||||
type="date"
|
||||
:value="props.filters.startDate"
|
||||
@update:value="
|
||||
(value) => emit('update:startDate', String(value ?? ''))
|
||||
"
|
||||
/>
|
||||
<span class="oa-date-sep">~</span>
|
||||
<Input
|
||||
class="oa-date-input"
|
||||
type="date"
|
||||
:value="props.filters.endDate"
|
||||
@update:value="(value) => emit('update:endDate', String(value ?? ''))"
|
||||
/>
|
||||
|
||||
<Select
|
||||
class="oa-select"
|
||||
:value="props.filters.status"
|
||||
:options="STATUS_OPTIONS"
|
||||
@update:value="
|
||||
(value) => emit('update:status', String(value ?? 'all'))
|
||||
"
|
||||
/>
|
||||
|
||||
<Select
|
||||
class="oa-select"
|
||||
:value="props.filters.channel"
|
||||
:options="CHANNEL_OPTIONS"
|
||||
@update:value="
|
||||
(value) => emit('update:channel', String(value ?? 'all'))
|
||||
"
|
||||
/>
|
||||
|
||||
<Select
|
||||
class="oa-select"
|
||||
:value="props.filters.paymentMethod"
|
||||
:options="PAYMENT_OPTIONS"
|
||||
@update:value="
|
||||
(value) => emit('update:paymentMethod', String(value ?? 'all'))
|
||||
"
|
||||
/>
|
||||
|
||||
<Input
|
||||
class="oa-search"
|
||||
:value="props.filters.keyword"
|
||||
placeholder="订单号/手机号"
|
||||
allow-clear
|
||||
@update:value="(value) => emit('update:keyword', String(value ?? ''))"
|
||||
@press-enter="emit('search')"
|
||||
/>
|
||||
|
||||
<Button @click="emit('reset')">重置</Button>
|
||||
<Button type="primary" @click="emit('search')">查询</Button>
|
||||
<Button :loading="props.isExporting" @click="emit('export')">
|
||||
导出
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</StoreScopeToolbar>
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { OrderAllStatsDto } from '#/api/order';
|
||||
|
||||
import { formatCurrency } from '../composables/order-all-page/helpers';
|
||||
|
||||
interface Props {
|
||||
stats: OrderAllStatsDto;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="oa-stats">
|
||||
<span>
|
||||
筛选结果<strong>{{ props.stats.totalOrders }} 单</strong>
|
||||
</span>
|
||||
<span>
|
||||
总金额<strong>{{ formatCurrency(props.stats.totalAmount) }}</strong>
|
||||
</span>
|
||||
<span>
|
||||
平均客单价<strong>{{ formatCurrency(props.stats.averageAmount) }}</strong>
|
||||
</span>
|
||||
<span>
|
||||
退款
|
||||
<strong class="oa-stats-refund">{{ props.stats.refundCount }} 单</strong>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
135
apps/web-antd/src/views/order/all/components/OrderTableCard.vue
Normal file
135
apps/web-antd/src/views/order/all/components/OrderTableCard.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import type { TablePaginationConfig, TableProps } from 'ant-design-vue';
|
||||
|
||||
import type { OrderAllListItemDto } from '#/api/order';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Button, Table, Tag } from 'ant-design-vue';
|
||||
|
||||
import { formatCurrency } from '../composables/order-all-page/helpers';
|
||||
|
||||
interface PaginationState {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
pagination: PaginationState;
|
||||
rows: OrderAllListItemDto[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'detail', orderNo: string): void;
|
||||
(event: 'pageChange', page: number, pageSize: number): void;
|
||||
}>();
|
||||
|
||||
function resolveChannelTagColor(channel: string) {
|
||||
if (channel.includes('外卖')) return 'blue';
|
||||
if (channel.includes('自提')) return 'green';
|
||||
if (channel.includes('堂食')) return 'orange';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
function resolveStatusTagColor(status: string) {
|
||||
if (status.includes('退款')) return 'red';
|
||||
if (status.includes('取消')) return 'default';
|
||||
if (status.includes('完成')) return 'success';
|
||||
if (status.includes('接单') || status.includes('取餐')) return 'orange';
|
||||
if (status.includes('制作') || status.includes('配送')) return 'processing';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
const columns: TableProps['columns'] = [
|
||||
{
|
||||
title: '订单号',
|
||||
dataIndex: 'orderNo',
|
||||
width: 170,
|
||||
},
|
||||
{
|
||||
title: '下单时间',
|
||||
dataIndex: 'orderedAt',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '渠道',
|
||||
dataIndex: 'channel',
|
||||
width: 100,
|
||||
customRender: ({ text }) =>
|
||||
h(Tag, { color: resolveChannelTagColor(String(text ?? '')) }, () =>
|
||||
String(text ?? '--'),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '顾客',
|
||||
dataIndex: 'customer',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '商品',
|
||||
dataIndex: 'itemsSummary',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
width: 120,
|
||||
customRender: ({ text }) => formatCurrency(Number(text || 0)),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
customRender: ({ text }) =>
|
||||
h(Tag, { color: resolveStatusTagColor(String(text ?? '')) }, () =>
|
||||
String(text ?? '--'),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 90,
|
||||
customRender: ({ record }) =>
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
type: 'link',
|
||||
onClick: () => emit('detail', String(record.orderNo ?? '')),
|
||||
},
|
||||
() => '详情',
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function handleTableChange(next: TablePaginationConfig) {
|
||||
emit('pageChange', Number(next.current || 1), Number(next.pageSize || 10));
|
||||
}
|
||||
|
||||
function resolveRowClassName(record: OrderAllListItemDto) {
|
||||
return record.isDimmed ? 'oa-row-dim' : '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="oa-table-card">
|
||||
<Table
|
||||
row-key="orderNo"
|
||||
:columns="columns"
|
||||
:data-source="props.rows"
|
||||
:loading="props.loading"
|
||||
:pagination="{
|
||||
current: props.pagination.page,
|
||||
pageSize: props.pagination.pageSize,
|
||||
total: props.pagination.total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
}"
|
||||
:row-class-name="resolveRowClassName"
|
||||
@change="handleTableChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { OptionItem, OrderAllFilterState } from '../../types';
|
||||
|
||||
import type {
|
||||
OrderAllChannelFilter,
|
||||
OrderAllPaymentFilter,
|
||||
OrderAllStatusFilter,
|
||||
} from '#/api/order';
|
||||
|
||||
import { getTodayDateString } from './helpers';
|
||||
|
||||
export const STATUS_OPTIONS: OptionItem[] = [
|
||||
{ label: '全部状态', value: 'all' },
|
||||
{ label: '待接单', value: 'pending' },
|
||||
{ label: '制作中', value: 'making' },
|
||||
{ label: '配送中', value: 'delivering' },
|
||||
{ label: '待取餐', value: 'pickup' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
{ label: '已退款', value: 'refunded' },
|
||||
];
|
||||
|
||||
export const CHANNEL_OPTIONS: OptionItem[] = [
|
||||
{ label: '全部渠道', value: 'all' },
|
||||
{ label: '外卖', value: 'delivery' },
|
||||
{ label: '自提', value: 'pickup' },
|
||||
{ label: '堂食', value: 'dine_in' },
|
||||
];
|
||||
|
||||
export const PAYMENT_OPTIONS: OptionItem[] = [
|
||||
{ label: '全部支付', value: 'all' },
|
||||
{ label: '微信支付', value: 'wechat' },
|
||||
{ label: '支付宝', value: 'alipay' },
|
||||
{ label: '余额支付', value: 'balance' },
|
||||
{ label: '现金', value: 'cash' },
|
||||
{ label: '刷卡', value: 'card' },
|
||||
];
|
||||
|
||||
export function createDefaultFilters(): OrderAllFilterState {
|
||||
const today = getTodayDateString();
|
||||
return {
|
||||
status: 'all' as OrderAllStatusFilter,
|
||||
channel: 'all' as OrderAllChannelFilter,
|
||||
paymentMethod: 'all' as OrderAllPaymentFilter,
|
||||
keyword: '',
|
||||
startDate: today,
|
||||
endDate: today,
|
||||
};
|
||||
}
|
||||
|
||||
export const DEFAULT_STATS = {
|
||||
totalOrders: 0,
|
||||
totalAmount: 0,
|
||||
averageAmount: 0,
|
||||
refundCount: 0,
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { OrderAllFilterState, OrderAllPaginationState } from '../../types';
|
||||
|
||||
import type { OrderAllListItemDto, OrderAllStatsDto } from '#/api/order';
|
||||
import type { StoreListItemDto } from '#/api/store';
|
||||
|
||||
import { getOrderAllListApi, getOrderAllStatsApi } from '#/api/order';
|
||||
import { getStoreListApi } from '#/api/store';
|
||||
|
||||
import { buildQueryPayload } from './helpers';
|
||||
|
||||
interface DataActionOptions {
|
||||
filters: OrderAllFilterState;
|
||||
isListLoading: { value: boolean };
|
||||
isStatsLoading: { value: boolean };
|
||||
isStoreLoading: { value: boolean };
|
||||
pagination: OrderAllPaginationState;
|
||||
rows: { value: OrderAllListItemDto[] };
|
||||
selectedStoreId: { value: string };
|
||||
stats: OrderAllStatsDto;
|
||||
stores: { value: StoreListItemDto[] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件职责:全部订单数据加载动作。
|
||||
*/
|
||||
export function createDataActions(options: DataActionOptions) {
|
||||
function resetStats() {
|
||||
options.stats.totalOrders = 0;
|
||||
options.stats.totalAmount = 0;
|
||||
options.stats.averageAmount = 0;
|
||||
options.stats.refundCount = 0;
|
||||
}
|
||||
|
||||
async function loadStores() {
|
||||
options.isStoreLoading.value = true;
|
||||
try {
|
||||
const result = await getStoreListApi({ page: 1, pageSize: 200 });
|
||||
options.stores.value = result.items;
|
||||
|
||||
if (result.items.length === 0) {
|
||||
options.selectedStoreId.value = '';
|
||||
options.rows.value = [];
|
||||
options.pagination.total = 0;
|
||||
resetStats();
|
||||
return;
|
||||
}
|
||||
|
||||
const matched = result.items.some(
|
||||
(store) => store.id === options.selectedStoreId.value,
|
||||
);
|
||||
if (!matched) {
|
||||
options.selectedStoreId.value = result.items[0]?.id ?? '';
|
||||
}
|
||||
} finally {
|
||||
options.isStoreLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPageData() {
|
||||
if (!options.selectedStoreId.value) {
|
||||
options.rows.value = [];
|
||||
options.pagination.total = 0;
|
||||
resetStats();
|
||||
return;
|
||||
}
|
||||
|
||||
const queryPayload = buildQueryPayload(
|
||||
options.selectedStoreId.value,
|
||||
options.filters,
|
||||
options.pagination.page,
|
||||
options.pagination.pageSize,
|
||||
);
|
||||
const { page: _page, pageSize: _pageSize, ...filterPayload } = queryPayload;
|
||||
|
||||
options.isListLoading.value = true;
|
||||
options.isStatsLoading.value = true;
|
||||
try {
|
||||
const [listResult, statsResult] = await Promise.all([
|
||||
getOrderAllListApi(queryPayload),
|
||||
getOrderAllStatsApi(filterPayload),
|
||||
]);
|
||||
|
||||
options.rows.value = listResult.items;
|
||||
options.pagination.total = listResult.total;
|
||||
options.pagination.page = listResult.page;
|
||||
options.pagination.pageSize = listResult.pageSize;
|
||||
|
||||
options.stats.totalOrders = statsResult.totalOrders;
|
||||
options.stats.totalAmount = statsResult.totalAmount;
|
||||
options.stats.averageAmount = statsResult.averageAmount;
|
||||
options.stats.refundCount = statsResult.refundCount;
|
||||
} finally {
|
||||
options.isListLoading.value = false;
|
||||
options.isStatsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadPageData,
|
||||
loadStores,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { OrderAllDetailDto } from '#/api/order';
|
||||
|
||||
import { getOrderAllDetailApi } from '#/api/order';
|
||||
|
||||
interface DrawerActionOptions {
|
||||
detail: { value: null | OrderAllDetailDto };
|
||||
isDetailLoading: { value: boolean };
|
||||
isDrawerOpen: { value: boolean };
|
||||
selectedStoreId: { value: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件职责:全部订单详情抽屉动作。
|
||||
*/
|
||||
export function createDrawerActions(options: DrawerActionOptions) {
|
||||
function setDrawerOpen(value: boolean) {
|
||||
options.isDrawerOpen.value = value;
|
||||
if (!value) {
|
||||
options.detail.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(orderNo: string) {
|
||||
if (!options.selectedStoreId.value || !orderNo) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.isDrawerOpen.value = true;
|
||||
options.detail.value = null;
|
||||
options.isDetailLoading.value = true;
|
||||
try {
|
||||
options.detail.value = await getOrderAllDetailApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
orderNo,
|
||||
});
|
||||
} finally {
|
||||
options.isDetailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
openDetail,
|
||||
setDrawerOpen,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { OrderAllFilterState } from '../../types';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { exportOrderAllCsvApi } from '#/api/order';
|
||||
|
||||
import { downloadBase64File, isDateRangeInvalid } from './helpers';
|
||||
|
||||
interface ExportActionOptions {
|
||||
filters: OrderAllFilterState;
|
||||
isExporting: { value: boolean };
|
||||
selectedStoreId: { value: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件职责:全部订单导出动作。
|
||||
*/
|
||||
export function createExportActions(options: ExportActionOptions) {
|
||||
async function handleExport() {
|
||||
if (!options.selectedStoreId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDateRangeInvalid(options.filters)) {
|
||||
message.warning('开始日期不能晚于结束日期');
|
||||
return;
|
||||
}
|
||||
|
||||
options.isExporting.value = true;
|
||||
try {
|
||||
const result = await exportOrderAllCsvApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
startDate: options.filters.startDate || undefined,
|
||||
endDate: options.filters.endDate || undefined,
|
||||
status:
|
||||
options.filters.status === 'all' ? undefined : options.filters.status,
|
||||
channel:
|
||||
options.filters.channel === 'all'
|
||||
? undefined
|
||||
: options.filters.channel,
|
||||
paymentMethod:
|
||||
options.filters.paymentMethod === 'all'
|
||||
? undefined
|
||||
: options.filters.paymentMethod,
|
||||
keyword: options.filters.keyword.trim() || undefined,
|
||||
});
|
||||
|
||||
downloadBase64File(result.fileName, result.fileContentBase64);
|
||||
message.success(`导出成功,共 ${result.totalCount} 条记录`);
|
||||
} finally {
|
||||
options.isExporting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleExport,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { OrderAllFilterState, OrderAllPaginationState } from '../../types';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { createDefaultFilters } from './constants';
|
||||
import { isDateRangeInvalid } from './helpers';
|
||||
|
||||
interface FilterActionOptions {
|
||||
filters: OrderAllFilterState;
|
||||
loadPageData: () => Promise<void>;
|
||||
pagination: OrderAllPaginationState;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件职责:全部订单筛选与分页行为。
|
||||
*/
|
||||
export function createFilterActions(options: FilterActionOptions) {
|
||||
function setStatus(value: string) {
|
||||
options.filters.status = (value || 'all') as OrderAllFilterState['status'];
|
||||
}
|
||||
|
||||
function setChannel(value: string) {
|
||||
options.filters.channel = (value ||
|
||||
'all') as OrderAllFilterState['channel'];
|
||||
}
|
||||
|
||||
function setPaymentMethod(value: string) {
|
||||
options.filters.paymentMethod = (value ||
|
||||
'all') as OrderAllFilterState['paymentMethod'];
|
||||
}
|
||||
|
||||
function setStartDate(value: string) {
|
||||
options.filters.startDate = value;
|
||||
}
|
||||
|
||||
function setEndDate(value: string) {
|
||||
options.filters.endDate = value;
|
||||
}
|
||||
|
||||
function setKeyword(value: string) {
|
||||
options.filters.keyword = value;
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
if (isDateRangeInvalid(options.filters)) {
|
||||
message.warning('开始日期不能晚于结束日期');
|
||||
return;
|
||||
}
|
||||
|
||||
options.pagination.page = 1;
|
||||
await options.loadPageData();
|
||||
}
|
||||
|
||||
async function handleReset() {
|
||||
const defaults = createDefaultFilters();
|
||||
options.filters.status = defaults.status;
|
||||
options.filters.channel = defaults.channel;
|
||||
options.filters.paymentMethod = defaults.paymentMethod;
|
||||
options.filters.keyword = defaults.keyword;
|
||||
options.filters.startDate = defaults.startDate;
|
||||
options.filters.endDate = defaults.endDate;
|
||||
options.pagination.page = 1;
|
||||
await options.loadPageData();
|
||||
}
|
||||
|
||||
async function handlePageChange(page: number, pageSize: number) {
|
||||
options.pagination.page = page;
|
||||
options.pagination.pageSize = pageSize;
|
||||
await options.loadPageData();
|
||||
}
|
||||
|
||||
return {
|
||||
handlePageChange,
|
||||
handleReset,
|
||||
handleSearch,
|
||||
setChannel,
|
||||
setEndDate,
|
||||
setKeyword,
|
||||
setPaymentMethod,
|
||||
setStartDate,
|
||||
setStatus,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { OrderAllFilterState, OrderAllQueryPayload } from '../../types';
|
||||
|
||||
import type {
|
||||
OrderAllChannelFilter,
|
||||
OrderAllPaymentFilter,
|
||||
OrderAllStatusFilter,
|
||||
} from '#/api/order';
|
||||
|
||||
export function getTodayDateString() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = `${now.getMonth() + 1}`.padStart(2, '0');
|
||||
const day = `${now.getDate()}`.padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function normalizeStatus(
|
||||
status: OrderAllStatusFilter,
|
||||
): OrderAllStatusFilter | undefined {
|
||||
return status === 'all' ? undefined : status;
|
||||
}
|
||||
|
||||
function normalizeChannel(
|
||||
channel: OrderAllChannelFilter,
|
||||
): OrderAllChannelFilter | undefined {
|
||||
return channel === 'all' ? undefined : channel;
|
||||
}
|
||||
|
||||
function normalizePayment(
|
||||
paymentMethod: OrderAllPaymentFilter,
|
||||
): OrderAllPaymentFilter | undefined {
|
||||
return paymentMethod === 'all' ? undefined : paymentMethod;
|
||||
}
|
||||
|
||||
export function buildQueryPayload(
|
||||
storeId: string,
|
||||
filters: OrderAllFilterState,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): OrderAllQueryPayload {
|
||||
return {
|
||||
storeId,
|
||||
page,
|
||||
pageSize,
|
||||
startDate: filters.startDate || undefined,
|
||||
endDate: filters.endDate || undefined,
|
||||
status: normalizeStatus(filters.status),
|
||||
channel: normalizeChannel(filters.channel),
|
||||
paymentMethod: normalizePayment(filters.paymentMethod),
|
||||
keyword: filters.keyword.trim() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatCurrency(value: number) {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(Number.isFinite(value) ? value : 0);
|
||||
}
|
||||
|
||||
export function decodeBase64ToBlob(base64: string) {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index += 1) {
|
||||
bytes[index] = binary.codePointAt(index) ?? 0;
|
||||
}
|
||||
return new Blob([bytes], { type: 'text/csv;charset=utf-8;' });
|
||||
}
|
||||
|
||||
export function downloadBase64File(
|
||||
fileName: string,
|
||||
fileContentBase64: string,
|
||||
) {
|
||||
const blob = decodeBase64ToBlob(fileContentBase64);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = fileName;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function isDateRangeInvalid(filters: OrderAllFilterState) {
|
||||
if (!filters.startDate || !filters.endDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filters.startDate > filters.endDate;
|
||||
}
|
||||
146
apps/web-antd/src/views/order/all/composables/useOrderAllPage.ts
Normal file
146
apps/web-antd/src/views/order/all/composables/useOrderAllPage.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import type { OrderAllDetailDto, OrderAllListItemDto } from '#/api/order';
|
||||
import type { StoreListItemDto } from '#/api/store';
|
||||
|
||||
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
createDefaultFilters,
|
||||
DEFAULT_STATS,
|
||||
} from './order-all-page/constants';
|
||||
import { createDataActions } from './order-all-page/data-actions';
|
||||
import { createDrawerActions } from './order-all-page/drawer-actions';
|
||||
import { createExportActions } from './order-all-page/export-actions';
|
||||
import { createFilterActions } from './order-all-page/filter-actions';
|
||||
|
||||
/**
|
||||
* 文件职责:全部订单页面状态与动作编排。
|
||||
*/
|
||||
export function useOrderAllPage() {
|
||||
const stores = ref<StoreListItemDto[]>([]);
|
||||
const selectedStoreId = ref('');
|
||||
const isStoreLoading = ref(false);
|
||||
|
||||
const filters = reactive(createDefaultFilters());
|
||||
const rows = ref<OrderAllListItemDto[]>([]);
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const stats = reactive({ ...DEFAULT_STATS });
|
||||
const isListLoading = ref(false);
|
||||
const isStatsLoading = ref(false);
|
||||
|
||||
const detail = ref<null | OrderAllDetailDto>(null);
|
||||
const isDrawerOpen = ref(false);
|
||||
const isDetailLoading = ref(false);
|
||||
const isExporting = ref(false);
|
||||
|
||||
const storeOptions = computed(() =>
|
||||
stores.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
);
|
||||
|
||||
const { loadPageData, loadStores } = createDataActions({
|
||||
stores,
|
||||
selectedStoreId,
|
||||
filters,
|
||||
rows,
|
||||
pagination,
|
||||
stats,
|
||||
isStoreLoading,
|
||||
isListLoading,
|
||||
isStatsLoading,
|
||||
});
|
||||
|
||||
const {
|
||||
handlePageChange,
|
||||
handleReset,
|
||||
handleSearch,
|
||||
setChannel,
|
||||
setEndDate,
|
||||
setKeyword,
|
||||
setPaymentMethod,
|
||||
setStartDate,
|
||||
setStatus,
|
||||
} = createFilterActions({
|
||||
filters,
|
||||
pagination,
|
||||
loadPageData,
|
||||
});
|
||||
|
||||
const { openDetail, setDrawerOpen } = createDrawerActions({
|
||||
selectedStoreId,
|
||||
detail,
|
||||
isDrawerOpen,
|
||||
isDetailLoading,
|
||||
});
|
||||
|
||||
const { handleExport } = createExportActions({
|
||||
selectedStoreId,
|
||||
filters,
|
||||
isExporting,
|
||||
});
|
||||
|
||||
function setSelectedStoreId(value: string) {
|
||||
selectedStoreId.value = value;
|
||||
}
|
||||
|
||||
watch(selectedStoreId, async (storeId) => {
|
||||
if (!storeId) {
|
||||
rows.value = [];
|
||||
pagination.total = 0;
|
||||
stats.totalOrders = DEFAULT_STATS.totalOrders;
|
||||
stats.totalAmount = DEFAULT_STATS.totalAmount;
|
||||
stats.averageAmount = DEFAULT_STATS.averageAmount;
|
||||
stats.refundCount = DEFAULT_STATS.refundCount;
|
||||
detail.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
pagination.page = 1;
|
||||
await loadPageData();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void loadStores();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
if (stores.value.length === 0 || !selectedStoreId.value) {
|
||||
void loadStores();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
detail,
|
||||
filters,
|
||||
handleExport,
|
||||
handlePageChange,
|
||||
handleReset,
|
||||
handleSearch,
|
||||
isDetailLoading,
|
||||
isDrawerOpen,
|
||||
isExporting,
|
||||
isListLoading,
|
||||
isStatsLoading,
|
||||
isStoreLoading,
|
||||
openDetail,
|
||||
pagination,
|
||||
rows,
|
||||
selectedStoreId,
|
||||
setChannel,
|
||||
setDrawerOpen,
|
||||
setEndDate,
|
||||
setKeyword,
|
||||
setPaymentMethod,
|
||||
setSelectedStoreId,
|
||||
setStartDate,
|
||||
setStatus,
|
||||
stats,
|
||||
storeOptions,
|
||||
};
|
||||
}
|
||||
82
apps/web-antd/src/views/order/all/index.vue
Normal file
82
apps/web-antd/src/views/order/all/index.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import OrderDetailDrawer from './components/OrderDetailDrawer.vue';
|
||||
import OrderFilterBar from './components/OrderFilterBar.vue';
|
||||
import OrderStatsBar from './components/OrderStatsBar.vue';
|
||||
import OrderTableCard from './components/OrderTableCard.vue';
|
||||
import { useOrderAllPage } from './composables/useOrderAllPage';
|
||||
|
||||
const {
|
||||
detail,
|
||||
filters,
|
||||
handleExport,
|
||||
handlePageChange,
|
||||
handleReset,
|
||||
handleSearch,
|
||||
isDetailLoading,
|
||||
isDrawerOpen,
|
||||
isExporting,
|
||||
isListLoading,
|
||||
isStoreLoading,
|
||||
openDetail,
|
||||
pagination,
|
||||
rows,
|
||||
selectedStoreId,
|
||||
setChannel,
|
||||
setDrawerOpen,
|
||||
setEndDate,
|
||||
setKeyword,
|
||||
setPaymentMethod,
|
||||
setSelectedStoreId,
|
||||
setStartDate,
|
||||
setStatus,
|
||||
stats,
|
||||
storeOptions,
|
||||
} = useOrderAllPage();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="全部订单" content-class="page-order-all">
|
||||
<div class="oa-page">
|
||||
<OrderFilterBar
|
||||
:selected-store-id="selectedStoreId"
|
||||
:store-options="storeOptions"
|
||||
:is-store-loading="isStoreLoading"
|
||||
:filters="filters"
|
||||
:is-exporting="isExporting"
|
||||
@update:selected-store-id="setSelectedStoreId"
|
||||
@update:start-date="setStartDate"
|
||||
@update:end-date="setEndDate"
|
||||
@update:status="setStatus"
|
||||
@update:channel="setChannel"
|
||||
@update:payment-method="setPaymentMethod"
|
||||
@update:keyword="setKeyword"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@export="handleExport"
|
||||
/>
|
||||
|
||||
<OrderStatsBar :stats="stats" />
|
||||
|
||||
<OrderTableCard
|
||||
:rows="rows"
|
||||
:loading="isListLoading"
|
||||
:pagination="pagination"
|
||||
@page-change="handlePageChange"
|
||||
@detail="openDetail"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<OrderDetailDrawer
|
||||
:open="isDrawerOpen"
|
||||
:loading="isDetailLoading"
|
||||
:detail="detail"
|
||||
@close="setDrawerOpen(false)"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
@import './styles/index.less';
|
||||
</style>
|
||||
11
apps/web-antd/src/views/order/all/styles/base.less
Normal file
11
apps/web-antd/src/views/order/all/styles/base.less
Normal file
@@ -0,0 +1,11 @@
|
||||
.page-order-all {
|
||||
.ant-card {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.oa-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
125
apps/web-antd/src/views/order/all/styles/drawer.less
Normal file
125
apps/web-antd/src/views/order/all/styles/drawer.less
Normal file
@@ -0,0 +1,125 @@
|
||||
.oa-section {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.oa-section-title {
|
||||
padding-left: 10px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: rgb(0 0 0 / 88%);
|
||||
border-left: 3px solid #1677ff;
|
||||
}
|
||||
}
|
||||
|
||||
.oa-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px 20px;
|
||||
font-size: 13px;
|
||||
|
||||
.label {
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
|
||||
.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.oa-detail-table {
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
border-collapse: collapse;
|
||||
|
||||
th {
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
text-align: left;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&.right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 10px;
|
||||
color: rgb(0 0 0 / 88%);
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
&.right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.oa-amount-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: rgb(0 0 0 / 65%);
|
||||
}
|
||||
|
||||
.discount {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.total {
|
||||
padding-top: 8px;
|
||||
margin-top: 4px;
|
||||
font-weight: 600;
|
||||
color: rgb(0 0 0 / 88%);
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.oa-timeline {
|
||||
padding-left: 4px;
|
||||
|
||||
.oa-timeline-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 0 0 12px 16px;
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #1677ff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgb(0 0 0 / 88%);
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 13px;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.oa-remark {
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
color: rgb(0 0 0 / 65%);
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
5
apps/web-antd/src/views/order/all/styles/index.less
Normal file
5
apps/web-antd/src/views/order/all/styles/index.less
Normal file
@@ -0,0 +1,5 @@
|
||||
@import './base.less';
|
||||
@import './layout.less';
|
||||
@import './table.less';
|
||||
@import './drawer.less';
|
||||
@import './responsive.less';
|
||||
45
apps/web-antd/src/views/order/all/styles/layout.less
Normal file
45
apps/web-antd/src/views/order/all/styles/layout.less
Normal file
@@ -0,0 +1,45 @@
|
||||
.oa-filter-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
.oa-date-input {
|
||||
width: 145px;
|
||||
}
|
||||
|
||||
.oa-date-sep {
|
||||
font-size: 13px;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
|
||||
.oa-select {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.oa-search {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.oa-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 0 4px;
|
||||
font-size: 13px;
|
||||
color: rgb(0 0 0 / 65%);
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
|
||||
strong {
|
||||
margin-left: 4px;
|
||||
font-weight: 600;
|
||||
color: rgb(0 0 0 / 88%);
|
||||
}
|
||||
}
|
||||
|
||||
.oa-stats-refund {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
28
apps/web-antd/src/views/order/all/styles/responsive.less
Normal file
28
apps/web-antd/src/views/order/all/styles/responsive.less
Normal file
@@ -0,0 +1,28 @@
|
||||
@media (max-width: 1600px) {
|
||||
.oa-stats {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.oa-filter-actions {
|
||||
.oa-date-input,
|
||||
.oa-select,
|
||||
.oa-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.oa-date-sep {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.oa-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.full {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
apps/web-antd/src/views/order/all/styles/table.less
Normal file
20
apps/web-antd/src/views/order/all/styles/table.less
Normal file
@@ -0,0 +1,20 @@
|
||||
.oa-table-card {
|
||||
padding: 6px 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 10px;
|
||||
|
||||
.ant-table-wrapper {
|
||||
.ant-table-thead > tr > th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.oa-row-dim {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.oa-row-dim:hover td {
|
||||
opacity: 0.75;
|
||||
}
|
||||
52
apps/web-antd/src/views/order/all/types.ts
Normal file
52
apps/web-antd/src/views/order/all/types.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type {
|
||||
OrderAllChannelFilter,
|
||||
OrderAllDetailDto,
|
||||
OrderAllListItemDto,
|
||||
OrderAllPaymentFilter,
|
||||
OrderAllStatsDto,
|
||||
OrderAllStatusFilter,
|
||||
} from '#/api/order';
|
||||
|
||||
export interface OrderAllFilterState {
|
||||
channel: OrderAllChannelFilter;
|
||||
endDate: string;
|
||||
keyword: string;
|
||||
paymentMethod: OrderAllPaymentFilter;
|
||||
startDate: string;
|
||||
status: OrderAllStatusFilter;
|
||||
}
|
||||
|
||||
export interface OrderAllPaginationState {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface OrderAllPageState {
|
||||
detail: null | OrderAllDetailDto;
|
||||
filters: OrderAllFilterState;
|
||||
isDetailLoading: boolean;
|
||||
isDrawerOpen: boolean;
|
||||
isExporting: boolean;
|
||||
isListLoading: boolean;
|
||||
isStatsLoading: boolean;
|
||||
rows: OrderAllListItemDto[];
|
||||
stats: OrderAllStatsDto;
|
||||
}
|
||||
|
||||
export interface OptionItem {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface OrderAllQueryPayload {
|
||||
channel?: OrderAllChannelFilter;
|
||||
endDate?: string;
|
||||
keyword?: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
paymentMethod?: OrderAllPaymentFilter;
|
||||
startDate?: string;
|
||||
status?: OrderAllStatusFilter;
|
||||
storeId: string;
|
||||
}
|
||||
Reference in New Issue
Block a user