优化订单管理
This commit is contained in:
@@ -83,6 +83,15 @@ type AdminActionRequest struct {
|
|||||||
Reason string `json:"reason" binding:"required"`
|
Reason string `json:"reason" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminOrderQuery struct {
|
||||||
|
Page int
|
||||||
|
PageSize int
|
||||||
|
Keyword string
|
||||||
|
Status string
|
||||||
|
HandoffStatus string
|
||||||
|
SettlementStatus string
|
||||||
|
}
|
||||||
|
|
||||||
type AuditMeta = auditlog.Meta
|
type AuditMeta = auditlog.Meta
|
||||||
|
|
||||||
type PaginatedResult struct {
|
type PaginatedResult struct {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (h *Handler) AdminList(c *gin.Context) {
|
func (h *Handler) AdminList(c *gin.Context) {
|
||||||
page, pageSize := parsePagination(c)
|
query := parseAdminOrderQuery(c)
|
||||||
result, err := h.service.ListAdmin(c.Request.Context(), page, pageSize)
|
result, err := h.service.ListAdmin(c.Request.Context(), query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeOrderError(c, err)
|
writeOrderError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package order
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/middleware"
|
"hfb_sys/backend/internal/middleware"
|
||||||
"hfb_sys/backend/pkg/response"
|
"hfb_sys/backend/pkg/response"
|
||||||
@@ -50,3 +51,15 @@ func parsePagination(c *gin.Context) (int, int) {
|
|||||||
}
|
}
|
||||||
return page, pageSize
|
return page, pageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseAdminOrderQuery(c *gin.Context) AdminOrderQuery {
|
||||||
|
page, pageSize := parsePagination(c)
|
||||||
|
return AdminOrderQuery{
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
Keyword: strings.TrimSpace(c.Query("keyword")),
|
||||||
|
Status: strings.TrimSpace(c.Query("status")),
|
||||||
|
HandoffStatus: strings.TrimSpace(c.Query("handoff_status")),
|
||||||
|
SettlementStatus: strings.TrimSpace(c.Query("settlement_status")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package order
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
@@ -33,16 +34,25 @@ func (r *Repository) ListForUser(ctx context.Context, userID uint64) ([]OrderDTO
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) ListAdmin(ctx context.Context, page, pageSize int) (*PaginatedResult, error) {
|
func (r *Repository) ListAdmin(ctx context.Context, query AdminOrderQuery) (*PaginatedResult, error) {
|
||||||
var total int64
|
var total int64
|
||||||
db := r.db.WithContext(ctx)
|
db := r.db.WithContext(ctx)
|
||||||
if err := r.adminQuery(ctx).Count(&total).Error; err != nil {
|
countDB := applyAdminOrderFilters(r.adminOrderJoinQuery(ctx), query)
|
||||||
|
if err := countDB.Count(&total).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
page := query.Page
|
||||||
|
pageSize := query.PageSize
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
offset := (page - 1) * pageSize
|
offset := (page - 1) * pageSize
|
||||||
var rows []orderRow
|
var rows []orderRow
|
||||||
err := r.adminQuery(ctx).
|
err := applyAdminOrderFilters(r.adminQuery(ctx), query).
|
||||||
Order("o.id DESC").
|
Order("o.id DESC").
|
||||||
Limit(pageSize).
|
Limit(pageSize).
|
||||||
Offset(offset).
|
Offset(offset).
|
||||||
@@ -67,6 +77,45 @@ func (r *Repository) ListAdmin(ctx context.Context, page, pageSize int) (*Pagina
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyAdminOrderFilters(db *gorm.DB, query AdminOrderQuery) *gorm.DB {
|
||||||
|
if query.Status != "" {
|
||||||
|
db = db.Where("o.status = ?", query.Status)
|
||||||
|
}
|
||||||
|
if query.HandoffStatus != "" {
|
||||||
|
db = db.Where("o.handoff_status = ?", query.HandoffStatus)
|
||||||
|
}
|
||||||
|
if query.SettlementStatus != "" {
|
||||||
|
db = db.Where("o.settlement_status = ?", query.SettlementStatus)
|
||||||
|
}
|
||||||
|
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
||||||
|
like := "%" + keyword + "%"
|
||||||
|
if id, err := strconv.ParseUint(keyword, 10, 64); err == nil {
|
||||||
|
db = db.Where(
|
||||||
|
`(o.order_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ? OR renter.phone LIKE ? OR o.id = ? OR o.listing_id = ? OR o.owner_id = ? OR o.renter_id = ?)`,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
id,
|
||||||
|
id,
|
||||||
|
id,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
db = db.Where(
|
||||||
|
`(o.order_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ? OR renter.phone LIKE ?)`,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) FindAdmin(ctx context.Context, orderID uint64) (*OrderDTO, error) {
|
func (r *Repository) FindAdmin(ctx context.Context, orderID uint64) (*OrderDTO, error) {
|
||||||
var row orderRow
|
var row orderRow
|
||||||
db := r.db.WithContext(ctx)
|
db := r.db.WithContext(ctx)
|
||||||
@@ -191,8 +240,12 @@ func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) adminQuery(ctx context.Context) *gorm.DB {
|
func (r *Repository) adminQuery(ctx context.Context) *gorm.DB {
|
||||||
|
return r.adminOrderJoinQuery(ctx).
|
||||||
|
Select("o.*, l.listing_no, a.title, a.server_region, a.login_platform, owner.phone AS owner_phone, renter.phone AS renter_phone")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) adminOrderJoinQuery(ctx context.Context) *gorm.DB {
|
||||||
return r.db.WithContext(ctx).Table("rental_orders AS o").
|
return r.db.WithContext(ctx).Table("rental_orders AS o").
|
||||||
Select("o.*, l.listing_no, a.title, a.server_region, a.login_platform, owner.phone AS owner_phone, renter.phone AS renter_phone").
|
|
||||||
Joins("JOIN rental_listings AS l ON l.id = o.listing_id").
|
Joins("JOIN rental_listings AS l ON l.id = o.listing_id").
|
||||||
Joins("JOIN game_accounts AS a ON a.id = o.account_id").
|
Joins("JOIN game_accounts AS a ON a.id = o.account_id").
|
||||||
Joins("JOIN users AS owner ON owner.id = o.owner_id").
|
Joins("JOIN users AS owner ON owner.id = o.owner_id").
|
||||||
|
|||||||
@@ -145,11 +145,11 @@ func (s *Service) ListForUser(ctx context.Context, userID uint64) ([]OrderDTO, e
|
|||||||
return s.repo.ListForUser(ctx, userID)
|
return s.repo.ListForUser(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ListAdmin(ctx context.Context, page, pageSize int) (*PaginatedResult, error) {
|
func (s *Service) ListAdmin(ctx context.Context, query AdminOrderQuery) (*PaginatedResult, error) {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return nil, ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
}
|
}
|
||||||
return s.repo.ListAdmin(ctx, page, pageSize)
|
return s.repo.ListAdmin(ctx, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) FindAdmin(ctx context.Context, orderID uint64) (*OrderDTO, error) {
|
func (s *Service) FindAdmin(ctx context.Context, orderID uint64) (*OrderDTO, error) {
|
||||||
|
|||||||
Vendored
-43
@@ -14,18 +14,7 @@ declare module 'vue' {
|
|||||||
ChatAttachmentImage: typeof import('./src/components/ChatAttachmentImage.vue')['default']
|
ChatAttachmentImage: typeof import('./src/components/ChatAttachmentImage.vue')['default']
|
||||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
|
||||||
ElCarousel: typeof import('element-plus/es')['ElCarousel']
|
|
||||||
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
|
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
|
||||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
|
||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
|
||||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
|
||||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
|
||||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
@@ -34,33 +23,17 @@ declare module 'vue' {
|
|||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElImage: typeof import('element-plus/es')['ElImage']
|
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
ElLink: typeof import('element-plus/es')['ElLink']
|
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
|
||||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
|
||||||
ElStep: typeof import('element-plus/es')['ElStep']
|
|
||||||
ElSteps: typeof import('element-plus/es')['ElSteps']
|
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
|
||||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
|
||||||
GuideImage: typeof import('./src/components/GuideImage.vue')['default']
|
GuideImage: typeof import('./src/components/GuideImage.vue')['default']
|
||||||
InAppBrowserPrompt: typeof import('./src/components/InAppBrowserPrompt.vue')['default']
|
InAppBrowserPrompt: typeof import('./src/components/InAppBrowserPrompt.vue')['default']
|
||||||
MobileBottomNav: typeof import('./src/components/MobileBottomNav.vue')['default']
|
MobileBottomNav: typeof import('./src/components/MobileBottomNav.vue')['default']
|
||||||
@@ -69,26 +42,10 @@ declare module 'vue' {
|
|||||||
VanButton: typeof import('vant/es')['Button']
|
VanButton: typeof import('vant/es')['Button']
|
||||||
VanCell: typeof import('vant/es')['Cell']
|
VanCell: typeof import('vant/es')['Cell']
|
||||||
VanCellGroup: typeof import('vant/es')['CellGroup']
|
VanCellGroup: typeof import('vant/es')['CellGroup']
|
||||||
VanCheckbox: typeof import('vant/es')['Checkbox']
|
|
||||||
VanCollapse: typeof import('vant/es')['Collapse']
|
|
||||||
VanCollapseItem: typeof import('vant/es')['CollapseItem']
|
|
||||||
VanEmpty: typeof import('vant/es')['Empty']
|
VanEmpty: typeof import('vant/es')['Empty']
|
||||||
VanField: typeof import('vant/es')['Field']
|
VanField: typeof import('vant/es')['Field']
|
||||||
VanIcon: typeof import('vant/es')['Icon']
|
VanIcon: typeof import('vant/es')['Icon']
|
||||||
VanList: typeof import('vant/es')['List']
|
|
||||||
VanLoading: typeof import('vant/es')['Loading']
|
|
||||||
VanNoticeBar: typeof import('vant/es')['NoticeBar']
|
|
||||||
VanPopup: typeof import('vant/es')['Popup']
|
|
||||||
VanPullRefresh: typeof import('vant/es')['PullRefresh']
|
|
||||||
VanRadio: typeof import('vant/es')['Radio']
|
|
||||||
VanRadioGroup: typeof import('vant/es')['RadioGroup']
|
|
||||||
VanSearch: typeof import('vant/es')['Search']
|
|
||||||
VanStepper: typeof import('vant/es')['Stepper']
|
VanStepper: typeof import('vant/es')['Stepper']
|
||||||
VanSwipe: typeof import('vant/es')['Swipe']
|
|
||||||
VanSwipeItem: typeof import('vant/es')['SwipeItem']
|
|
||||||
VanTab: typeof import('vant/es')['Tab']
|
|
||||||
VanTabs: typeof import('vant/es')['Tabs']
|
|
||||||
VanTag: typeof import('vant/es')['Tag']
|
|
||||||
}
|
}
|
||||||
export interface GlobalDirectives {
|
export interface GlobalDirectives {
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { Refresh, Search } from '@element-plus/icons-vue'
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { fetchAdminOrders, type Order } from '@/features/orders'
|
import { fetchAdminOrders, type AdminOrderQuery, type Order } from '@/features/orders'
|
||||||
import {
|
import {
|
||||||
handoffStatusLabel,
|
handoffStatusLabel,
|
||||||
orderStatusLabel,
|
orderStatusLabel,
|
||||||
@@ -13,19 +14,69 @@ import { formatListingNo } from '@/shared/utils/listingDisplay'
|
|||||||
import { adminPath } from '@/shared/utils/adminPath'
|
import { adminPath } from '@/shared/utils/adminPath'
|
||||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||||
|
|
||||||
const status = ref('')
|
const filters = reactive<AdminOrderQuery>({
|
||||||
|
keyword: '',
|
||||||
|
status: '',
|
||||||
|
handoff_status: '',
|
||||||
|
settlement_status: '',
|
||||||
|
})
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const orders = ref<Order[]>([])
|
const orders = ref<Order[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const currentPageSize = ref(20)
|
const currentPageSize = ref(20)
|
||||||
|
|
||||||
|
const orderStatusOptions = [
|
||||||
|
{ label: '待支付', value: 'pending_payment' },
|
||||||
|
{ label: '待交接', value: 'pending_handoff' },
|
||||||
|
{ label: '使用中', value: 'renting' },
|
||||||
|
{ label: '逾期中', value: 'overdue' },
|
||||||
|
{ label: '待号主确认结账', value: 'pending_checkout_confirm' },
|
||||||
|
{ label: '待租客确认修正', value: 'pending_checkout_accept' },
|
||||||
|
{ label: '结账争议中', value: 'checkout_disputing' },
|
||||||
|
{ label: '申诉中', value: 'disputing' },
|
||||||
|
{ label: '异常', value: 'abnormal' },
|
||||||
|
{ label: '已完成', value: 'completed' },
|
||||||
|
{ label: '已关闭', value: 'closed' },
|
||||||
|
{ label: '已取消', value: 'cancelled' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const handoffStatusOptions = [
|
||||||
|
{ label: '待号主交接', value: 'pending_owner' },
|
||||||
|
{ label: '号主交接超时', value: 'owner_timeout' },
|
||||||
|
{ label: '待租客确认', value: 'pending_renter_confirm' },
|
||||||
|
{ label: '已确认收号', value: 'received' },
|
||||||
|
{ label: '归还逾期', value: 'return_overdue' },
|
||||||
|
{ label: '待号主确认结账', value: 'pending_owner_checkout' },
|
||||||
|
{ label: '待租客确认修正', value: 'pending_renter_checkout' },
|
||||||
|
{ label: '已归还', value: 'returned' },
|
||||||
|
{ label: '客服关闭', value: 'admin_closed' },
|
||||||
|
{ label: '客服标记异常', value: 'admin_abnormal' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const settlementStatusOptions = [
|
||||||
|
{ label: '未结算', value: 'unsettled' },
|
||||||
|
{ label: '待结算', value: 'pending' },
|
||||||
|
{ label: '冻结中', value: 'frozen' },
|
||||||
|
{ label: '已结算', value: 'settled' },
|
||||||
|
{ label: '已退款', value: 'refunded' },
|
||||||
|
{ label: '已取消', value: 'cancelled' },
|
||||||
|
{ label: '已关闭', value: 'closed' },
|
||||||
|
{ label: '争议中', value: 'disputed' },
|
||||||
|
{ label: '已仲裁', value: 'arbitrated' },
|
||||||
|
] as const
|
||||||
|
|
||||||
onMounted(loadOrders)
|
onMounted(loadOrders)
|
||||||
|
|
||||||
async function loadOrders() {
|
async function loadOrders() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await fetchAdminOrders(currentPage.value, currentPageSize.value)
|
const result = await fetchAdminOrders(currentPage.value, currentPageSize.value, {
|
||||||
|
keyword: filters.keyword?.trim(),
|
||||||
|
status: filters.status,
|
||||||
|
handoff_status: filters.handoff_status,
|
||||||
|
settlement_status: filters.settlement_status,
|
||||||
|
})
|
||||||
orders.value = result.items
|
orders.value = result.items
|
||||||
total.value = result.total
|
total.value = result.total
|
||||||
} finally {
|
} finally {
|
||||||
@@ -37,10 +88,18 @@ async function handlePageChange() {
|
|||||||
await loadOrders()
|
await loadOrders()
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredOrders = computed(() => {
|
async function queryOrders() {
|
||||||
if (!status.value) return orders.value
|
currentPage.value = 1
|
||||||
return orders.value.filter(item => item.status === status.value)
|
await loadOrders()
|
||||||
})
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
filters.keyword = ''
|
||||||
|
filters.status = ''
|
||||||
|
filters.handoff_status = ''
|
||||||
|
filters.settlement_status = ''
|
||||||
|
void queryOrders()
|
||||||
|
}
|
||||||
|
|
||||||
function amountYuan(cent: unknown) {
|
function amountYuan(cent: unknown) {
|
||||||
if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0))
|
if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0))
|
||||||
@@ -50,10 +109,14 @@ function amountYuan(cent: unknown) {
|
|||||||
function money(value: unknown) {
|
function money(value: unknown) {
|
||||||
return formatMoney(Number(value || 0))
|
return formatMoney(Number(value || 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function userText(value: string | number | undefined) {
|
||||||
|
return value || '-'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<section class="page admin-orders-page">
|
||||||
<div class="page-header-row">
|
<div class="page-header-row">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<p class="eyebrow">Orders</p>
|
<p class="eyebrow">Orders</p>
|
||||||
@@ -61,54 +124,134 @@ function money(value: unknown) {
|
|||||||
<p>查看全量订单、交接状态、金额和结算状态。</p>
|
<p>查看全量订单、交接状态、金额和结算状态。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<el-select v-model="status" clearable placeholder="订单状态" style="width: 180px">
|
<el-button :icon="Refresh" @click="loadOrders">刷新</el-button>
|
||||||
<el-option label="待支付" value="pending_payment" />
|
|
||||||
<el-option label="待交接" value="pending_handoff" />
|
|
||||||
<el-option label="使用中" value="renting" />
|
|
||||||
<el-option label="逾期中" value="overdue" />
|
|
||||||
<el-option label="待结账确认" value="pending_return_confirm" />
|
|
||||||
<el-option label="待号主确认结账" value="pending_checkout_confirm" />
|
|
||||||
<el-option label="待租客确认修正" value="pending_checkout_accept" />
|
|
||||||
<el-option label="结账争议中" value="checkout_disputing" />
|
|
||||||
<el-option label="申诉中" value="disputing" />
|
|
||||||
<el-option label="异常" value="abnormal" />
|
|
||||||
<el-option label="已完成" value="completed" />
|
|
||||||
<el-option label="已关闭" value="closed" />
|
|
||||||
<el-option label="已取消" value="cancelled" />
|
|
||||||
</el-select>
|
|
||||||
<el-button @click="loadOrders">刷新</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table v-loading="loading" class="table-panel" :data="filteredOrders">
|
<div class="order-filter-bar">
|
||||||
<el-table-column label="商品编号" width="140">
|
<el-form inline @submit.prevent>
|
||||||
<template #default="{ row }">{{
|
<el-form-item label="关键词">
|
||||||
formatListingNo(row.listing_no, row.listing_id)
|
<el-input
|
||||||
}}</template>
|
v-model="filters.keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="订单号 / 商品编号 / 账号 / 手机号"
|
||||||
|
style="width: 280px"
|
||||||
|
@keyup.enter="queryOrders"
|
||||||
|
@clear="queryOrders"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单状态">
|
||||||
|
<el-select
|
||||||
|
v-model="filters.status"
|
||||||
|
clearable
|
||||||
|
placeholder="全部"
|
||||||
|
style="width: 150px"
|
||||||
|
@change="queryOrders"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in orderStatusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="交接状态">
|
||||||
|
<el-select
|
||||||
|
v-model="filters.handoff_status"
|
||||||
|
clearable
|
||||||
|
placeholder="全部"
|
||||||
|
style="width: 170px"
|
||||||
|
@change="queryOrders"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in handoffStatusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="结算状态">
|
||||||
|
<el-select
|
||||||
|
v-model="filters.settlement_status"
|
||||||
|
clearable
|
||||||
|
placeholder="全部"
|
||||||
|
style="width: 140px"
|
||||||
|
@change="queryOrders"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in settlementStatusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :icon="Search" :loading="loading" @click="queryOrders"
|
||||||
|
>查询</el-button
|
||||||
|
>
|
||||||
|
<el-button @click="resetFilters">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
class="table-panel admin-orders-table"
|
||||||
|
:data="orders"
|
||||||
|
empty-text="未找到匹配的订单"
|
||||||
|
>
|
||||||
|
<el-table-column label="订单信息" min-width="230">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="order-main-cell">
|
||||||
|
<RouterLink class="order-link" :to="adminPath(`orders/${row.id}`)">
|
||||||
|
{{ row.order_no }}
|
||||||
|
</RouterLink>
|
||||||
|
<span>商品编号 {{ formatListingNo(row.listing_no, row.listing_id) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="order_no" label="订单号" min-width="230" />
|
<el-table-column label="账号" min-width="170" show-overflow-tooltip>
|
||||||
<el-table-column prop="title" label="账号" min-width="170" />
|
<template #default="{ row }">
|
||||||
<el-table-column prop="renter_phone" label="租客" min-width="130" />
|
<div class="stacked-cell">
|
||||||
<el-table-column prop="owner_phone" label="号主" min-width="130" />
|
<strong>{{ row.title }}</strong>
|
||||||
<el-table-column label="状态" width="140">
|
<span>{{ row.server_region }} / {{ row.login_platform }}</span>
|
||||||
<template #default="{ row }">{{ orderStatusLabel(row.status) }}</template>
|
</div>
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="交接" width="180">
|
<el-table-column label="用户" min-width="170">
|
||||||
<template #default="{ row }">{{ handoffStatusLabel(row.handoff_status) }}</template>
|
<template #default="{ row }">
|
||||||
|
<div class="stacked-cell compact">
|
||||||
|
<span>租客 {{ userText(row.renter_phone || row.renter_id) }}</span>
|
||||||
|
<span>号主 {{ userText(row.owner_phone || row.owner_id) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="结算" width="120">
|
<el-table-column label="状态" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="stacked-cell compact">
|
||||||
|
<strong>{{ orderStatusLabel(row.status) }}</strong>
|
||||||
|
<span>{{ handoffStatusLabel(row.handoff_status) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="结算" width="110">
|
||||||
<template #default="{ row }">{{ settlementStatusLabel(row.settlement_status) }}</template>
|
<template #default="{ row }">{{ settlementStatusLabel(row.settlement_status) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="订单金额" width="100">
|
<el-table-column label="金额" width="130" align="right">
|
||||||
<template #default="{ row }">¥{{ money(amountYuan(row.rent_amount_cent)) }}</template>
|
<template #default="{ row }">
|
||||||
|
<div class="stacked-cell money-cell">
|
||||||
|
<strong>¥{{ money(amountYuan(row.rent_amount_cent)) }}</strong>
|
||||||
|
<span>押金 ¥{{ money(amountYuan(row.deposit_amount_cent)) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="押金" width="100">
|
<el-table-column label="创建时间" width="170">
|
||||||
<template #default="{ row }">¥{{ money(amountYuan(row.deposit_amount_cent)) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="创建时间" min-width="180">
|
|
||||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="100">
|
<el-table-column label="操作" width="88" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<RouterLink :to="adminPath(`orders/${row.id}`)">
|
<RouterLink :to="adminPath(`orders/${row.id}`)">
|
||||||
<el-button size="small">详情</el-button>
|
<el-button size="small">详情</el-button>
|
||||||
@@ -127,3 +270,110 @@ function money(value: unknown) {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-orders-page {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-filter-bar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 16px 18px 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-filter-bar :deep(.el-form) {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-filter-bar :deep(.el-form-item) {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-filter-bar :deep(.el-form-item__label) {
|
||||||
|
color: #6b7785;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-orders-table {
|
||||||
|
--el-table-fixed-right-column: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-orders-table :deep(.el-table__inner-wrapper) {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-orders-table :deep(.el-scrollbar__wrap) {
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-orders-table :deep(.el-scrollbar__bar.is-horizontal) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-main-cell,
|
||||||
|
.stacked-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-main-cell span,
|
||||||
|
.stacked-cell span {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #8f9bba;
|
||||||
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked-cell strong {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #1b2559;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked-cell.compact {
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-cell {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #2f6bff;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-link:hover {
|
||||||
|
color: #174edb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.order-filter-bar :deep(.el-input),
|
||||||
|
.order-filter-bar :deep(.el-select) {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-filter-bar :deep(.el-form-item) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -128,6 +128,13 @@ export interface SubmitCheckoutPayload {
|
|||||||
reason?: string
|
reason?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminOrderQuery {
|
||||||
|
keyword?: string
|
||||||
|
status?: string
|
||||||
|
handoff_status?: string
|
||||||
|
settlement_status?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface SubmitCheckoutRequest {
|
interface SubmitCheckoutRequest {
|
||||||
content: string
|
content: string
|
||||||
consumable_amount_cent?: number
|
consumable_amount_cent?: number
|
||||||
@@ -291,9 +298,9 @@ export async function counterCheckout(id: number, payload: SubmitCheckoutPayload
|
|||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAdminOrders(page = 1, pageSize = 20) {
|
export async function fetchAdminOrders(page = 1, pageSize = 20, query: AdminOrderQuery = {}) {
|
||||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<Order>>>('/admin/orders', {
|
const { data } = await apiClient.get<ApiResponse<PaginatedResult<Order>>>('/admin/orders', {
|
||||||
params: { page, page_size: pageSize },
|
params: { page, page_size: pageSize, ...query },
|
||||||
})
|
})
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user