diff --git a/apps/web-antd/src/api/order/index.ts b/apps/web-antd/src/api/order/index.ts new file mode 100644 index 0000000..f94f75a --- /dev/null +++ b/apps/web-antd/src/api/order/index.ts @@ -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('/order/all/list', { + params, + }); +} + +/** 查询全部订单统计。 */ +export async function getOrderAllStatsApi(params: OrderAllFilterQuery) { + return requestClient.get('/order/all/stats', { params }); +} + +/** 查询全部订单详情。 */ +export async function getOrderAllDetailApi(params: { + orderNo: string; + storeId: string; +}) { + return requestClient.get('/order/all/detail', { params }); +} + +/** 导出全部订单 CSV。 */ +export async function exportOrderAllCsvApi(params: OrderAllFilterQuery) { + return requestClient.get('/order/all/export', { params }); +} diff --git a/apps/web-antd/src/router/routes/modules/order.ts b/apps/web-antd/src/router/routes/modules/order.ts new file mode 100644 index 0000000..5f81827 --- /dev/null +++ b/apps/web-antd/src/router/routes/modules/order.ts @@ -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; diff --git a/apps/web-antd/src/views/order/all/components/OrderDetailDrawer.vue b/apps/web-antd/src/views/order/all/components/OrderDetailDrawer.vue new file mode 100644 index 0000000..9ff1143 --- /dev/null +++ b/apps/web-antd/src/views/order/all/components/OrderDetailDrawer.vue @@ -0,0 +1,162 @@ + + + diff --git a/apps/web-antd/src/views/order/all/components/OrderFilterBar.vue b/apps/web-antd/src/views/order/all/components/OrderFilterBar.vue new file mode 100644 index 0000000..bb89fa9 --- /dev/null +++ b/apps/web-antd/src/views/order/all/components/OrderFilterBar.vue @@ -0,0 +1,119 @@ + + + diff --git a/apps/web-antd/src/views/order/all/components/OrderStatsBar.vue b/apps/web-antd/src/views/order/all/components/OrderStatsBar.vue new file mode 100644 index 0000000..1af4986 --- /dev/null +++ b/apps/web-antd/src/views/order/all/components/OrderStatsBar.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/web-antd/src/views/order/all/components/OrderTableCard.vue b/apps/web-antd/src/views/order/all/components/OrderTableCard.vue new file mode 100644 index 0000000..4dc50fc --- /dev/null +++ b/apps/web-antd/src/views/order/all/components/OrderTableCard.vue @@ -0,0 +1,135 @@ + + + diff --git a/apps/web-antd/src/views/order/all/composables/order-all-page/constants.ts b/apps/web-antd/src/views/order/all/composables/order-all-page/constants.ts new file mode 100644 index 0000000..316b6b2 --- /dev/null +++ b/apps/web-antd/src/views/order/all/composables/order-all-page/constants.ts @@ -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, +}; diff --git a/apps/web-antd/src/views/order/all/composables/order-all-page/data-actions.ts b/apps/web-antd/src/views/order/all/composables/order-all-page/data-actions.ts new file mode 100644 index 0000000..7f32293 --- /dev/null +++ b/apps/web-antd/src/views/order/all/composables/order-all-page/data-actions.ts @@ -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, + }; +} diff --git a/apps/web-antd/src/views/order/all/composables/order-all-page/drawer-actions.ts b/apps/web-antd/src/views/order/all/composables/order-all-page/drawer-actions.ts new file mode 100644 index 0000000..f5f2d9d --- /dev/null +++ b/apps/web-antd/src/views/order/all/composables/order-all-page/drawer-actions.ts @@ -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, + }; +} diff --git a/apps/web-antd/src/views/order/all/composables/order-all-page/export-actions.ts b/apps/web-antd/src/views/order/all/composables/order-all-page/export-actions.ts new file mode 100644 index 0000000..8043cc3 --- /dev/null +++ b/apps/web-antd/src/views/order/all/composables/order-all-page/export-actions.ts @@ -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, + }; +} diff --git a/apps/web-antd/src/views/order/all/composables/order-all-page/filter-actions.ts b/apps/web-antd/src/views/order/all/composables/order-all-page/filter-actions.ts new file mode 100644 index 0000000..74db7a2 --- /dev/null +++ b/apps/web-antd/src/views/order/all/composables/order-all-page/filter-actions.ts @@ -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; + 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, + }; +} diff --git a/apps/web-antd/src/views/order/all/composables/order-all-page/helpers.ts b/apps/web-antd/src/views/order/all/composables/order-all-page/helpers.ts new file mode 100644 index 0000000..cea0573 --- /dev/null +++ b/apps/web-antd/src/views/order/all/composables/order-all-page/helpers.ts @@ -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; +} diff --git a/apps/web-antd/src/views/order/all/composables/useOrderAllPage.ts b/apps/web-antd/src/views/order/all/composables/useOrderAllPage.ts new file mode 100644 index 0000000..e1e4518 --- /dev/null +++ b/apps/web-antd/src/views/order/all/composables/useOrderAllPage.ts @@ -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([]); + const selectedStoreId = ref(''); + const isStoreLoading = ref(false); + + const filters = reactive(createDefaultFilters()); + const rows = ref([]); + 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); + 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, + }; +} diff --git a/apps/web-antd/src/views/order/all/index.vue b/apps/web-antd/src/views/order/all/index.vue new file mode 100644 index 0000000..579e477 --- /dev/null +++ b/apps/web-antd/src/views/order/all/index.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/apps/web-antd/src/views/order/all/styles/base.less b/apps/web-antd/src/views/order/all/styles/base.less new file mode 100644 index 0000000..4efa68f --- /dev/null +++ b/apps/web-antd/src/views/order/all/styles/base.less @@ -0,0 +1,11 @@ +.page-order-all { + .ant-card { + border-radius: 10px; + } +} + +.oa-page { + display: flex; + flex-direction: column; + gap: 12px; +} diff --git a/apps/web-antd/src/views/order/all/styles/drawer.less b/apps/web-antd/src/views/order/all/styles/drawer.less new file mode 100644 index 0000000..54633a1 --- /dev/null +++ b/apps/web-antd/src/views/order/all/styles/drawer.less @@ -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; +} diff --git a/apps/web-antd/src/views/order/all/styles/index.less b/apps/web-antd/src/views/order/all/styles/index.less new file mode 100644 index 0000000..45d3eb9 --- /dev/null +++ b/apps/web-antd/src/views/order/all/styles/index.less @@ -0,0 +1,5 @@ +@import './base.less'; +@import './layout.less'; +@import './table.less'; +@import './drawer.less'; +@import './responsive.less'; diff --git a/apps/web-antd/src/views/order/all/styles/layout.less b/apps/web-antd/src/views/order/all/styles/layout.less new file mode 100644 index 0000000..5c8e6d7 --- /dev/null +++ b/apps/web-antd/src/views/order/all/styles/layout.less @@ -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; + } +} diff --git a/apps/web-antd/src/views/order/all/styles/responsive.less b/apps/web-antd/src/views/order/all/styles/responsive.less new file mode 100644 index 0000000..b9895c0 --- /dev/null +++ b/apps/web-antd/src/views/order/all/styles/responsive.less @@ -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; + } + } +} diff --git a/apps/web-antd/src/views/order/all/styles/table.less b/apps/web-antd/src/views/order/all/styles/table.less new file mode 100644 index 0000000..7d72172 --- /dev/null +++ b/apps/web-antd/src/views/order/all/styles/table.less @@ -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; +} diff --git a/apps/web-antd/src/views/order/all/types.ts b/apps/web-antd/src/views/order/all/types.ts new file mode 100644 index 0000000..165add5 --- /dev/null +++ b/apps/web-antd/src/views/order/all/types.ts @@ -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; +}