优化后台列表搜索
This commit is contained in:
@@ -51,6 +51,15 @@ type ArbitrateRequest struct {
|
|||||||
Remark string `json:"remark" binding:"required"`
|
Remark string `json:"remark" binding:"required"`
|
||||||
AmountCent int64 `json:"amount_cent"`
|
AmountCent int64 `json:"amount_cent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminListQuery struct {
|
||||||
|
Page int
|
||||||
|
PageSize int
|
||||||
|
Keyword string
|
||||||
|
Status string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
type AuditMeta = auditlog.Meta
|
type AuditMeta = auditlog.Meta
|
||||||
|
|
||||||
type PaginatedResult struct {
|
type PaginatedResult struct {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/middleware"
|
"hfb_sys/backend/internal/middleware"
|
||||||
"hfb_sys/backend/pkg/response"
|
"hfb_sys/backend/pkg/response"
|
||||||
@@ -113,7 +114,14 @@ func (h *Handler) CancelByOrder(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *Handler) AdminList(c *gin.Context) {
|
func (h *Handler) AdminList(c *gin.Context) {
|
||||||
page, pageSize := parsePagination(c)
|
page, pageSize := parsePagination(c)
|
||||||
result, err := h.service.ListAdmin(c.Request.Context(), page, pageSize)
|
query := AdminListQuery{
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
Keyword: strings.TrimSpace(c.Query("keyword")),
|
||||||
|
Status: strings.TrimSpace(c.Query("status")),
|
||||||
|
Type: strings.TrimSpace(c.Query("type")),
|
||||||
|
}
|
||||||
|
result, err := h.service.ListAdmin(c.Request.Context(), query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeDisputeError(c, err)
|
writeDisputeError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package dispute
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
|
|
||||||
@@ -39,14 +41,27 @@ func (r *Repository) FindForUser(ctx context.Context, userID uint64, id uint64)
|
|||||||
return &dto, nil
|
return &dto, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) ListAdmin(ctx context.Context, page, pageSize int) (*PaginatedResult, error) {
|
func (r *Repository) ListAdmin(ctx context.Context, query AdminListQuery) (*PaginatedResult, error) {
|
||||||
var total int64
|
var total int64
|
||||||
if err := r.db.WithContext(ctx).Model(&model.Dispute{}).Count(&total).Error; err != nil {
|
countDB := applyAdminListFilters(r.adminFilterQuery(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 []disputeRow
|
var rows []disputeRow
|
||||||
err := r.baseQuery(ctx).Order("d.id DESC").Offset(offset).Limit(pageSize).Scan(&rows).Error
|
err := applyAdminListFilters(r.baseQuery(ctx), query).
|
||||||
|
Order("d.id DESC").
|
||||||
|
Offset(offset).
|
||||||
|
Limit(pageSize).
|
||||||
|
Scan(&rows).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -54,13 +69,54 @@ func (r *Repository) ListAdmin(ctx context.Context, page, pageSize int) (*Pagina
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
||||||
return r.db.WithContext(ctx).Table("disputes AS d").
|
return r.adminFilterQuery(ctx).
|
||||||
Select(`d.*, o.order_no, o.status AS order_status, o.handoff_status, o.settlement_status,
|
Select(`d.*, o.order_no, o.status AS order_status, o.handoff_status, o.settlement_status,
|
||||||
o.owner_id, o.renter_id, owner.phone AS owner_phone, renter.phone AS renter_phone,
|
o.owner_id, o.renter_id, owner.phone AS owner_phone, renter.phone AS renter_phone,
|
||||||
l.listing_no, a.title`).
|
l.listing_no, a.title`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) adminFilterQuery(ctx context.Context) *gorm.DB {
|
||||||
|
return r.db.WithContext(ctx).Table("disputes AS d").
|
||||||
Joins("JOIN rental_orders AS o ON o.id = d.order_id").
|
Joins("JOIN rental_orders AS o ON o.id = d.order_id").
|
||||||
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").
|
||||||
Joins("JOIN users AS renter ON renter.id = o.renter_id")
|
Joins("JOIN users AS renter ON renter.id = o.renter_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyAdminListFilters(db *gorm.DB, query AdminListQuery) *gorm.DB {
|
||||||
|
if query.Status != "" {
|
||||||
|
db = db.Where("d.status = ?", query.Status)
|
||||||
|
}
|
||||||
|
if query.Type != "" {
|
||||||
|
db = db.Where("d.type = ?", query.Type)
|
||||||
|
}
|
||||||
|
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 d.id = ? OR d.order_id = ? OR o.listing_id = ? OR o.owner_id = ? OR o.renter_id = ?)`,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
id,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,11 +66,11 @@ func (s *Service) CancelByOrder(ctx context.Context, userID uint64, orderID uint
|
|||||||
return s.repo.CancelByOrder(ctx, userID, orderID)
|
return s.repo.CancelByOrder(ctx, userID, orderID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ListAdmin(ctx context.Context, page, pageSize int) (*PaginatedResult, error) {
|
func (s *Service) ListAdmin(ctx context.Context, query AdminListQuery) (*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) Arbitrate(ctx context.Context, adminID uint64, id uint64, req ArbitrateRequest, meta AuditMeta) (*DisputeDTO, error) {
|
func (s *Service) Arbitrate(ctx context.Context, adminID uint64, id uint64, req ArbitrateRequest, meta AuditMeta) (*DisputeDTO, error) {
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ type TransferOwnerRequest struct {
|
|||||||
|
|
||||||
type AdminListQuery struct {
|
type AdminListQuery struct {
|
||||||
OwnerID uint64
|
OwnerID uint64
|
||||||
|
Keyword string
|
||||||
Status string
|
Status string
|
||||||
ReviewStatus string
|
ReviewStatus string
|
||||||
Limit int
|
Limit int
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ func parseAdminListQuery(c *gin.Context) (AdminListQuery, bool) {
|
|||||||
}
|
}
|
||||||
query.PageSize = value
|
query.PageSize = value
|
||||||
}
|
}
|
||||||
|
query.Keyword = strings.TrimSpace(c.Query("keyword"))
|
||||||
query.Status = c.Query("status")
|
query.Status = c.Query("status")
|
||||||
query.ReviewStatus = c.Query("review_status")
|
query.ReviewStatus = c.Query("review_status")
|
||||||
return query, true
|
return query, true
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
@@ -56,6 +57,25 @@ func (r *Repository) applyAdminListFilters(db *gorm.DB, query AdminListQuery) *g
|
|||||||
if query.OwnerID > 0 {
|
if query.OwnerID > 0 {
|
||||||
db = db.Where("l.owner_id = ?", query.OwnerID)
|
db = db.Where("l.owner_id = ?", query.OwnerID)
|
||||||
}
|
}
|
||||||
|
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
||||||
|
like := "%" + keyword + "%"
|
||||||
|
if id, err := strconv.ParseUint(keyword, 10, 64); err == nil {
|
||||||
|
db = db.Where(
|
||||||
|
`(l.listing_no LIKE ? OR EXISTS (SELECT 1 FROM rental_orders AS o WHERE o.listing_id = l.id AND o.order_no LIKE ?) OR l.id = ? OR l.account_id = ? OR l.owner_id = ?)`,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
id,
|
||||||
|
id,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
db = db.Where(
|
||||||
|
`(l.listing_no LIKE ? OR EXISTS (SELECT 1 FROM rental_orders AS o WHERE o.listing_id = l.id AND o.order_no LIKE ?))`,
|
||||||
|
like,
|
||||||
|
like,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
if query.Status != "" {
|
if query.Status != "" {
|
||||||
db = db.Where("l.status = ?", query.Status)
|
db = db.Where("l.status = ?", query.Status)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { readError } from '@/shared/utils/error'
|
import { readError } from '@/shared/utils/error'
|
||||||
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
@@ -28,16 +29,10 @@ const amount = ref<number | undefined>()
|
|||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const currentPageSize = ref(20)
|
const currentPageSize = ref(20)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
const keywordFilter = ref('')
|
||||||
const statusFilter = ref('')
|
const statusFilter = ref('')
|
||||||
const typeFilter = ref('')
|
const typeFilter = ref('')
|
||||||
const isPartialRefund = computed(() => result.value === 'partial_refund')
|
const isPartialRefund = computed(() => result.value === 'partial_refund')
|
||||||
const filteredDisputes = computed(() => {
|
|
||||||
return disputes.value.filter(item => {
|
|
||||||
if (statusFilter.value && item.status !== statusFilter.value) return false
|
|
||||||
if (typeFilter.value && item.type !== typeFilter.value) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const pendingCount = computed(
|
const pendingCount = computed(
|
||||||
() => disputes.value.filter(item => ['open', 'processing'].includes(item.status)).length
|
() => disputes.value.filter(item => ['open', 'processing'].includes(item.status)).length
|
||||||
)
|
)
|
||||||
@@ -117,7 +112,11 @@ onMounted(loadDisputes)
|
|||||||
async function loadDisputes() {
|
async function loadDisputes() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await fetchAdminDisputes(currentPage.value, currentPageSize.value)
|
const res = await fetchAdminDisputes(currentPage.value, currentPageSize.value, {
|
||||||
|
keyword: keywordFilter.value.trim() || undefined,
|
||||||
|
status: statusFilter.value || undefined,
|
||||||
|
type: typeFilter.value || undefined,
|
||||||
|
})
|
||||||
disputes.value = res.items
|
disputes.value = res.items
|
||||||
total.value = res.total
|
total.value = res.total
|
||||||
} finally {
|
} finally {
|
||||||
@@ -129,6 +128,11 @@ async function handlePageChange() {
|
|||||||
await loadDisputes()
|
await loadDisputes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function queryDisputes() {
|
||||||
|
currentPage.value = 1
|
||||||
|
await loadDisputes()
|
||||||
|
}
|
||||||
|
|
||||||
function openArbitration(row: Dispute) {
|
function openArbitration(row: Dispute) {
|
||||||
if (!canArbitrate(row)) return
|
if (!canArbitrate(row)) return
|
||||||
activeDispute.value = row
|
activeDispute.value = row
|
||||||
@@ -138,8 +142,10 @@ function openArbitration(row: Dispute) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
|
keywordFilter.value = ''
|
||||||
statusFilter.value = ''
|
statusFilter.value = ''
|
||||||
typeFilter.value = ''
|
typeFilter.value = ''
|
||||||
|
void queryDisputes()
|
||||||
}
|
}
|
||||||
|
|
||||||
function evidenceItems(row: Dispute | null) {
|
function evidenceItems(row: Dispute | null) {
|
||||||
@@ -312,14 +318,34 @@ async function handleArbitrate() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-panel">
|
<div class="filter-panel">
|
||||||
<el-select v-model="statusFilter" clearable placeholder="全部状态" class="filter-control">
|
<el-input
|
||||||
|
v-model="keywordFilter"
|
||||||
|
clearable
|
||||||
|
placeholder="订单号 / 商品编号 / 手机号"
|
||||||
|
class="filter-control keyword-filter"
|
||||||
|
@keyup.enter="queryDisputes"
|
||||||
|
@clear="queryDisputes"
|
||||||
|
/>
|
||||||
|
<el-select
|
||||||
|
v-model="statusFilter"
|
||||||
|
clearable
|
||||||
|
placeholder="全部状态"
|
||||||
|
class="filter-control"
|
||||||
|
@change="queryDisputes"
|
||||||
|
>
|
||||||
<el-option label="待处理" value="open" />
|
<el-option label="待处理" value="open" />
|
||||||
<el-option label="处理中" value="processing" />
|
<el-option label="处理中" value="processing" />
|
||||||
<el-option label="已处理" value="resolved" />
|
<el-option label="已处理" value="resolved" />
|
||||||
<el-option label="已关闭" value="closed" />
|
<el-option label="已关闭" value="closed" />
|
||||||
<el-option label="已取消" value="cancelled" />
|
<el-option label="已取消" value="cancelled" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-select v-model="typeFilter" clearable placeholder="全部类型" class="filter-control">
|
<el-select
|
||||||
|
v-model="typeFilter"
|
||||||
|
clearable
|
||||||
|
placeholder="全部类型"
|
||||||
|
class="filter-control"
|
||||||
|
@change="queryDisputes"
|
||||||
|
>
|
||||||
<el-option label="无法登录" value="cannot_login" />
|
<el-option label="无法登录" value="cannot_login" />
|
||||||
<el-option label="描述不符" value="false_description" />
|
<el-option label="描述不符" value="false_description" />
|
||||||
<el-option label="账号封禁" value="account_banned" />
|
<el-option label="账号封禁" value="account_banned" />
|
||||||
@@ -329,10 +355,11 @@ async function handleArbitrate() {
|
|||||||
<el-option label="归还超时" value="return_timeout" />
|
<el-option label="归还超时" value="return_timeout" />
|
||||||
<el-option label="结账争议" value="checkout_dispute" />
|
<el-option label="结账争议" value="checkout_dispute" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<el-button type="primary" :icon="Search" @click="queryDisputes">查询</el-button>
|
||||||
<el-button @click="resetFilters">重置</el-button>
|
<el-button @click="resetFilters">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table v-loading="loading" class="table-panel dispute-table" :data="filteredDisputes">
|
<el-table v-loading="loading" class="table-panel dispute-table" :data="disputes">
|
||||||
<el-table-column label="商品编号" width="140">
|
<el-table-column label="商品编号" width="140">
|
||||||
<template #default="{ row }">{{ formatListingNo(row.listing_no) }}</template>
|
<template #default="{ row }">{{ formatListingNo(row.listing_no) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -604,6 +631,10 @@ async function handleArbitrate() {
|
|||||||
width: 180px;
|
width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keyword-filter {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
.main-cell,
|
.main-cell,
|
||||||
.party-cell {
|
.party-cell {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from '@/shared/utils/listingDisplay'
|
} from '@/shared/utils/listingDisplay'
|
||||||
|
|
||||||
const filters = reactive<AdminListingQuery>({
|
const filters = reactive<AdminListingQuery>({
|
||||||
|
keyword: '',
|
||||||
owner_id: '',
|
owner_id: '',
|
||||||
status: '',
|
status: '',
|
||||||
review_status: '',
|
review_status: '',
|
||||||
@@ -97,6 +98,7 @@ async function queryListings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
|
filters.keyword = ''
|
||||||
filters.owner_id = ''
|
filters.owner_id = ''
|
||||||
filters.status = ''
|
filters.status = ''
|
||||||
filters.review_status = ''
|
filters.review_status = ''
|
||||||
@@ -442,6 +444,16 @@ function formatQuantity(value: number) {
|
|||||||
<!-- 筛选和操作行 -->
|
<!-- 筛选和操作行 -->
|
||||||
<div class="listing-filter-row">
|
<div class="listing-filter-row">
|
||||||
<el-form class="listing-filter-bar" inline>
|
<el-form class="listing-filter-bar" inline>
|
||||||
|
<el-form-item label="编号">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="商品编号 / 订单编号"
|
||||||
|
style="width: 210px"
|
||||||
|
@keyup.enter="queryListings"
|
||||||
|
@clear="queryListings"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="号主 ID">
|
<el-form-item label="号主 ID">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="filters.owner_id"
|
v-model="filters.owner_id"
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ export interface Dispute {
|
|||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminDisputeQuery {
|
||||||
|
keyword?: string
|
||||||
|
status?: string
|
||||||
|
type?: string
|
||||||
|
}
|
||||||
|
|
||||||
export async function createDispute(
|
export async function createDispute(
|
||||||
orderId: number,
|
orderId: number,
|
||||||
payload: { type: string; description: string; evidence_urls?: string[] }
|
payload: { type: string; description: string; evidence_urls?: string[] }
|
||||||
@@ -60,9 +66,9 @@ export async function fetchDisputes(page = 1, pageSize = 20) {
|
|||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAdminDisputes(page = 1, pageSize = 20) {
|
export async function fetchAdminDisputes(page = 1, pageSize = 20, query: AdminDisputeQuery = {}) {
|
||||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<Dispute>>>('/admin/disputes', {
|
const { data } = await apiClient.get<ApiResponse<PaginatedResult<Dispute>>>('/admin/disputes', {
|
||||||
params: { page, page_size: pageSize },
|
params: { page, page_size: pageSize, ...query },
|
||||||
})
|
})
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ export async function fetchPendingReviewListings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminListingQuery {
|
export interface AdminListingQuery {
|
||||||
|
keyword?: string
|
||||||
owner_id?: string
|
owner_id?: string
|
||||||
status?: ListingStatus | ''
|
status?: ListingStatus | ''
|
||||||
review_status?: ListingReviewStatus | ''
|
review_status?: ListingReviewStatus | ''
|
||||||
|
|||||||
Reference in New Issue
Block a user