贯通商品编号全流程
This commit is contained in:
@@ -12,6 +12,7 @@ type DisputeDTO struct {
|
|||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
OrderID uint64 `json:"order_id"`
|
OrderID uint64 `json:"order_id"`
|
||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
|
ListingNo string `json:"listing_no"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
InitiatorID uint64 `json:"initiator_id"`
|
InitiatorID uint64 `json:"initiator_id"`
|
||||||
TargetUserID uint64 `json:"target_user_id"`
|
TargetUserID uint64 `json:"target_user_id"`
|
||||||
|
|||||||
@@ -403,15 +403,17 @@ func roundMoney(value float64) float64 {
|
|||||||
|
|
||||||
func (r *Repository) baseQuery() *gorm.DB {
|
func (r *Repository) baseQuery() *gorm.DB {
|
||||||
return r.db.Table("disputes AS d").
|
return r.db.Table("disputes AS d").
|
||||||
Select("d.*, o.order_no, a.title").
|
Select("d.*, o.order_no, l.listing_no, a.title").
|
||||||
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 game_accounts AS a ON a.id = o.account_id")
|
Joins("JOIN game_accounts AS a ON a.id = o.account_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
type disputeRow struct {
|
type disputeRow struct {
|
||||||
model.Dispute
|
model.Dispute
|
||||||
OrderNo string
|
OrderNo string
|
||||||
Title string
|
ListingNo string
|
||||||
|
Title string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (row disputeRow) toDTO() DisputeDTO {
|
func (row disputeRow) toDTO() DisputeDTO {
|
||||||
@@ -419,6 +421,7 @@ func (row disputeRow) toDTO() DisputeDTO {
|
|||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
OrderID: row.OrderID,
|
OrderID: row.OrderID,
|
||||||
OrderNo: row.OrderNo,
|
OrderNo: row.OrderNo,
|
||||||
|
ListingNo: row.ListingNo,
|
||||||
Title: row.Title,
|
Title: row.Title,
|
||||||
InitiatorID: row.InitiatorID,
|
InitiatorID: row.InitiatorID,
|
||||||
TargetUserID: row.TargetUserID,
|
TargetUserID: row.TargetUserID,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type OrderDTO struct {
|
|||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
ListingID uint64 `json:"listing_id"`
|
ListingID uint64 `json:"listing_id"`
|
||||||
|
ListingNo string `json:"listing_no"`
|
||||||
AccountID uint64 `json:"account_id"`
|
AccountID uint64 `json:"account_id"`
|
||||||
OwnerID uint64 `json:"owner_id"`
|
OwnerID uint64 `json:"owner_id"`
|
||||||
RenterID uint64 `json:"renter_id"`
|
RenterID uint64 `json:"renter_id"`
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
|
|||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil {
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
snapshot, err := makeAccountSnapshot(account)
|
snapshot, err := makeAccountSnapshot(account, listing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1374,13 +1374,15 @@ func (r *Repository) findCheckout(id uint64) (*model.OrderCheckout, error) {
|
|||||||
|
|
||||||
func (r *Repository) baseQuery() *gorm.DB {
|
func (r *Repository) baseQuery() *gorm.DB {
|
||||||
return r.db.Table("rental_orders AS o").
|
return r.db.Table("rental_orders AS o").
|
||||||
Select("o.*, a.title, a.server_region, a.login_platform").
|
Select("o.*, l.listing_no, a.title, a.server_region, a.login_platform").
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) adminQuery() *gorm.DB {
|
func (r *Repository) adminQuery() *gorm.DB {
|
||||||
return r.db.Table("rental_orders AS o").
|
return r.db.Table("rental_orders AS o").
|
||||||
Select("o.*, a.title, a.server_region, a.login_platform, owner.phone AS owner_phone, renter.phone AS renter_phone").
|
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 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")
|
||||||
@@ -1389,6 +1391,7 @@ func (r *Repository) adminQuery() *gorm.DB {
|
|||||||
type orderRow struct {
|
type orderRow struct {
|
||||||
model.RentalOrder
|
model.RentalOrder
|
||||||
Title string
|
Title string
|
||||||
|
ListingNo string
|
||||||
ServerRegion string
|
ServerRegion string
|
||||||
LoginPlatform string
|
LoginPlatform string
|
||||||
OwnerPhone string
|
OwnerPhone string
|
||||||
@@ -1405,6 +1408,7 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
|||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
OrderNo: row.OrderNo,
|
OrderNo: row.OrderNo,
|
||||||
ListingID: row.ListingID,
|
ListingID: row.ListingID,
|
||||||
|
ListingNo: row.ListingNo,
|
||||||
AccountID: row.AccountID,
|
AccountID: row.AccountID,
|
||||||
OwnerID: row.OwnerID,
|
OwnerID: row.OwnerID,
|
||||||
RenterID: row.RenterID,
|
RenterID: row.RenterID,
|
||||||
@@ -1607,8 +1611,10 @@ func sanitizeOrderSnapshot(snapshot *datatypes.JSON, role string) {
|
|||||||
*snapshot = datatypes.JSON(raw)
|
*snapshot = datatypes.JSON(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeAccountSnapshot(account model.GameAccount) (datatypes.JSON, error) {
|
func makeAccountSnapshot(account model.GameAccount, listing model.RentalListing) (datatypes.JSON, error) {
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
|
"listing_id": listing.ID,
|
||||||
|
"listing_no": listing.ListingNo,
|
||||||
"account_id": account.ID,
|
"account_id": account.ID,
|
||||||
"title": account.Title,
|
"title": account.Title,
|
||||||
"game_name": account.GameName,
|
"game_name": account.GameName,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { arbitrateDispute, fetchAdminDisputes, type Dispute } from '@/features/d
|
|||||||
import { fetchAdminFileBlob } from '@/shared/api/files'
|
import { fetchAdminFileBlob } from '@/shared/api/files'
|
||||||
import { disputeStatusLabel } from '@/utils/statusLabels'
|
import { disputeStatusLabel } from '@/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
import { formatListingNo } from '@/utils/listingDisplay'
|
||||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -114,6 +115,9 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table v-loading="loading" class="table-panel" :data="disputes">
|
<el-table v-loading="loading" class="table-panel" :data="disputes">
|
||||||
|
<el-table-column label="商品编号" width="140">
|
||||||
|
<template #default="{ row }">{{ formatListingNo(row.listing_no) }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="order_no" label="订单号" min-width="210" />
|
<el-table-column prop="order_no" label="订单号" min-width="210" />
|
||||||
<el-table-column prop="title" label="账号" min-width="160" />
|
<el-table-column prop="title" label="账号" min-width="160" />
|
||||||
<el-table-column prop="type" label="类型" width="150" />
|
<el-table-column prop="type" label="类型" width="150" />
|
||||||
@@ -164,7 +168,8 @@ function readError(error: unknown, fallback: string) {
|
|||||||
>
|
>
|
||||||
<div v-if="activeDispute" class="dialog-body">
|
<div v-if="activeDispute" class="dialog-body">
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ activeDispute.order_no }}</strong> · {{ activeDispute.title }}
|
<strong>{{ activeDispute.order_no }}</strong> · 商品编号
|
||||||
|
{{ formatListingNo(activeDispute.listing_no) }} · {{ activeDispute.title }}
|
||||||
</p>
|
</p>
|
||||||
<p>{{ activeDispute.description }}</p>
|
<p>{{ activeDispute.description }}</p>
|
||||||
<el-select v-model="result" class="full-control" placeholder="选择裁决结果">
|
<el-select v-model="result" class="full-control" placeholder="选择裁决结果">
|
||||||
@@ -215,7 +220,8 @@ function readError(error: unknown, fallback: string) {
|
|||||||
>
|
>
|
||||||
<div v-if="evidenceDispute" class="dialog-body">
|
<div v-if="evidenceDispute" class="dialog-body">
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ evidenceDispute.order_no }}</strong> · {{ evidenceDispute.title }}
|
<strong>{{ evidenceDispute.order_no }}</strong> · 商品编号
|
||||||
|
{{ formatListingNo(evidenceDispute.listing_no) }} · {{ evidenceDispute.title }}
|
||||||
</p>
|
</p>
|
||||||
<div v-for="item in evidenceItems(evidenceDispute)" :key="item" class="evidence-row">
|
<div v-for="item in evidenceItems(evidenceDispute)" :key="item" class="evidence-row">
|
||||||
<span>{{ item }}</span>
|
<span>{{ item }}</span>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { CopyDocument } 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'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
} from '@/features/listings'
|
} from '@/features/listings'
|
||||||
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
import { formatListingCode } from '@/utils/listingDisplay'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -41,6 +43,16 @@ async function loadListing() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyListingCode() {
|
||||||
|
if (!listing.value) return
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(formatListingCode(listing.value))
|
||||||
|
ElMessage.success('商品编号已复制')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openAction(type: 'offline' | 'abnormal') {
|
function openAction(type: 'offline' | 'abnormal') {
|
||||||
actionType.value = type
|
actionType.value = type
|
||||||
reason.value = ''
|
reason.value = ''
|
||||||
@@ -104,11 +116,12 @@ function readError(error: unknown, fallback: string) {
|
|||||||
<section class="page" v-loading="loading">
|
<section class="page" v-loading="loading">
|
||||||
<div v-if="listing" class="page-header-row">
|
<div v-if="listing" class="page-header-row">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<p class="eyebrow">Listing #{{ listing.id }}</p>
|
<p class="eyebrow">商品编号:{{ formatListingCode(listing) }}</p>
|
||||||
<h1>商品详情</h1>
|
<h1>商品详情</h1>
|
||||||
<p>{{ listing.title }} · {{ listing.server_region }} / {{ listing.login_platform }}</p>
|
<p>{{ listing.title }} · {{ listing.server_region }} / {{ listing.login_platform }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
|
<el-button :icon="CopyDocument" @click="copyListingCode">复制编号</el-button>
|
||||||
<RouterLink to="/admin/listings">
|
<RouterLink to="/admin/listings">
|
||||||
<el-button>返回列表</el-button>
|
<el-button>返回列表</el-button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
@@ -143,6 +156,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
<div v-if="listing" class="dashboard-panels">
|
<div v-if="listing" class="dashboard-panels">
|
||||||
<div class="order-panel dashboard-panel">
|
<div class="order-panel dashboard-panel">
|
||||||
<h2>账号信息</h2>
|
<h2>账号信息</h2>
|
||||||
|
<p>商品编号:{{ formatListingCode(listing) }}</p>
|
||||||
<p>账号 ID:{{ listing.account_id }}</p>
|
<p>账号 ID:{{ listing.account_id }}</p>
|
||||||
<p>游戏:{{ listing.game_name }}</p>
|
<p>游戏:{{ listing.game_name }}</p>
|
||||||
<p>区服:{{ listing.server_region }}</p>
|
<p>区服:{{ listing.server_region }}</p>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from '@/features/listings'
|
} from '@/features/listings'
|
||||||
import {
|
import {
|
||||||
assetRegions,
|
assetRegions,
|
||||||
|
formatListingCode,
|
||||||
formatHafCoinM,
|
formatHafCoinM,
|
||||||
getCoinWan,
|
getCoinWan,
|
||||||
getListingConsumablePrice,
|
getListingConsumablePrice,
|
||||||
@@ -483,6 +484,9 @@ function riskTagType(level: RiskLevel) {
|
|||||||
|
|
||||||
function reviewSearchText(row: Listing) {
|
function reviewSearchText(row: Listing) {
|
||||||
return [
|
return [
|
||||||
|
row.listing_no,
|
||||||
|
row.id,
|
||||||
|
row.account_id,
|
||||||
row.title,
|
row.title,
|
||||||
row.owner_phone,
|
row.owner_phone,
|
||||||
row.owner_nickname,
|
row.owner_nickname,
|
||||||
@@ -606,7 +610,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
v-model="filters.keyword"
|
v-model="filters.keyword"
|
||||||
:prefix-icon="Search"
|
:prefix-icon="Search"
|
||||||
clearable
|
clearable
|
||||||
placeholder="搜索标题、客服、段位、皮肤"
|
placeholder="搜索编号、标题、客服、段位、皮肤"
|
||||||
/>
|
/>
|
||||||
<el-select v-model="filters.risk" placeholder="风险筛选">
|
<el-select v-model="filters.risk" placeholder="风险筛选">
|
||||||
<el-option label="全部待审" value="all" />
|
<el-option label="全部待审" value="all" />
|
||||||
@@ -626,7 +630,10 @@ function readError(error: unknown, fallback: string) {
|
|||||||
@click="selectListing(item)"
|
@click="selectListing(item)"
|
||||||
>
|
>
|
||||||
<div class="queue-title-row">
|
<div class="queue-title-row">
|
||||||
<strong>{{ item.title }}</strong>
|
<div>
|
||||||
|
<span class="queue-code">编号 {{ formatListingCode(item) }}</span>
|
||||||
|
<strong>{{ item.title }}</strong>
|
||||||
|
</div>
|
||||||
<span>{{ formatHafCoinM(getCoinWan(item)) }}</span>
|
<span>{{ formatHafCoinM(getCoinWan(item)) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="queue-meta">
|
<div class="queue-meta">
|
||||||
@@ -656,7 +663,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
<section class="review-hero-panel">
|
<section class="review-hero-panel">
|
||||||
<div class="hero-title">
|
<div class="hero-title">
|
||||||
<div>
|
<div>
|
||||||
<p>商品 #{{ selectedListing.id }}</p>
|
<p>商品编号 {{ formatListingCode(selectedListing) }}</p>
|
||||||
<h2>{{ selectedListing.title }}</h2>
|
<h2>{{ selectedListing.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
@@ -1154,6 +1161,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.queue-title-row strong {
|
.queue-title-row strong {
|
||||||
|
display: block;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: #1b2559;
|
color: #1b2559;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -1167,6 +1175,15 @@ function readError(error: unknown, fallback: string) {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.queue-title-row .queue-code {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eff6ff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.queue-meta,
|
.queue-meta,
|
||||||
.risk-strip,
|
.risk-strip,
|
||||||
.skin-list,
|
.skin-list,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '@/features/orders'
|
} from '@/features/orders'
|
||||||
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
|
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
import { formatListingNo } from '@/utils/listingDisplay'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -25,6 +26,9 @@ const handoffRecords = ref<HandoffRecord[]>([])
|
|||||||
const actionType = ref<'close' | 'abnormal' | ''>('')
|
const actionType = ref<'close' | 'abnormal' | ''>('')
|
||||||
const reason = ref('')
|
const reason = ref('')
|
||||||
const refundStatus = ref<RefundStatus | null>(null)
|
const refundStatus = ref<RefundStatus | null>(null)
|
||||||
|
const listingCode = computed(() =>
|
||||||
|
order.value ? formatListingNo(order.value.listing_no, order.value.listing_id) : '-'
|
||||||
|
)
|
||||||
const refunding = ref(false)
|
const refunding = ref(false)
|
||||||
|
|
||||||
const snapshotText = computed(() => JSON.stringify(order.value?.account_snapshot || {}, null, 2))
|
const snapshotText = computed(() => JSON.stringify(order.value?.account_snapshot || {}, null, 2))
|
||||||
@@ -144,7 +148,10 @@ function formatHandoffRecordType(type: string) {
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<p class="eyebrow">{{ order.order_no }}</p>
|
<p class="eyebrow">{{ order.order_no }}</p>
|
||||||
<h1>订单详情</h1>
|
<h1>订单详情</h1>
|
||||||
<p>{{ order.title }} · {{ order.server_region }} / {{ order.login_platform }}</p>
|
<p>
|
||||||
|
商品编号 {{ listingCode }} · {{ order.title }} · {{ order.server_region }} /
|
||||||
|
{{ order.login_platform }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<RouterLink to="/admin/orders">
|
<RouterLink to="/admin/orders">
|
||||||
@@ -200,6 +207,7 @@ function formatHandoffRecordType(type: string) {
|
|||||||
<div v-if="order" class="dashboard-panels">
|
<div v-if="order" class="dashboard-panels">
|
||||||
<div class="order-panel dashboard-panel">
|
<div class="order-panel dashboard-panel">
|
||||||
<h2>用户信息</h2>
|
<h2>用户信息</h2>
|
||||||
|
<p>商品编号:{{ listingCode }}</p>
|
||||||
<p>租客:{{ order.renter_phone || order.renter_id }}</p>
|
<p>租客:{{ order.renter_phone || order.renter_id }}</p>
|
||||||
<p>号主:{{ order.owner_phone || order.owner_id }}</p>
|
<p>号主:{{ order.owner_phone || order.owner_id }}</p>
|
||||||
<p>开始:{{ formatDateTime(orderRentedAt(), '未开始') }}</p>
|
<p>开始:{{ formatDateTime(orderRentedAt(), '未开始') }}</p>
|
||||||
@@ -230,7 +238,7 @@ function formatHandoffRecordType(type: string) {
|
|||||||
>
|
>
|
||||||
<div v-if="order" class="dialog-body">
|
<div v-if="order" class="dialog-body">
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ order.order_no }}</strong> · {{ order.title }}
|
<strong>{{ order.order_no }}</strong> · 商品编号 {{ listingCode }} · {{ order.title }}
|
||||||
</p>
|
</p>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="reason"
|
v-model="reason"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { fetchAdminOrders, type Order } from '@/features/orders'
|
import { fetchAdminOrders, type Order } from '@/features/orders'
|
||||||
import { handoffStatusLabel, orderStatusLabel, settlementStatusLabel } from '@/utils/statusLabels'
|
import { handoffStatusLabel, orderStatusLabel, settlementStatusLabel } from '@/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
import { formatListingNo } from '@/utils/listingDisplay'
|
||||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||||
|
|
||||||
const status = ref('')
|
const status = ref('')
|
||||||
@@ -65,6 +66,11 @@ const filteredOrders = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table v-loading="loading" class="table-panel" :data="filteredOrders">
|
<el-table v-loading="loading" class="table-panel" :data="filteredOrders">
|
||||||
|
<el-table-column label="商品编号" width="140">
|
||||||
|
<template #default="{ row }">{{
|
||||||
|
formatListingNo(row.listing_no, row.listing_id)
|
||||||
|
}}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="order_no" label="订单号" min-width="230" />
|
<el-table-column prop="order_no" label="订单号" min-width="230" />
|
||||||
<el-table-column prop="title" label="账号" min-width="170" />
|
<el-table-column prop="title" label="账号" min-width="170" />
|
||||||
<el-table-column prop="renter_phone" label="租客" min-width="130" />
|
<el-table-column prop="renter_phone" label="租客" min-width="130" />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface Dispute {
|
|||||||
id: number
|
id: number
|
||||||
order_id: number
|
order_id: number
|
||||||
order_no: string
|
order_no: string
|
||||||
|
listing_no: string
|
||||||
title: string
|
title: string
|
||||||
initiator_id: number
|
initiator_id: number
|
||||||
target_user_id: number
|
target_user_id: number
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
assetRegions,
|
assetRegions,
|
||||||
formatEstimatedRentalDuration,
|
formatEstimatedRentalDuration,
|
||||||
formatHafCoinM,
|
formatHafCoinM,
|
||||||
|
formatListingCode,
|
||||||
formatRatio,
|
formatRatio,
|
||||||
getCoinWan,
|
getCoinWan,
|
||||||
getDailyLoss,
|
getDailyLoss,
|
||||||
@@ -248,6 +249,16 @@ async function submitOrder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyListingCode() {
|
||||||
|
if (!listing.value) return
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(formatListingCode(listing.value))
|
||||||
|
ElMessage.success('商品编号已复制')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleAgreementScroll(type: 'virtual' | 'renter') {
|
function handleAgreementScroll(type: 'virtual' | 'renter') {
|
||||||
updateAgreementReadState(type)
|
updateAgreementReadState(type)
|
||||||
}
|
}
|
||||||
@@ -336,6 +347,9 @@ function listingPrice(item: Listing) {
|
|||||||
<span v-if="getLoginMethod(listing)">{{ getLoginMethod(listing) }}</span>
|
<span v-if="getLoginMethod(listing)">{{ getLoginMethod(listing) }}</span>
|
||||||
<span v-if="listing.rank_level">{{ listing.rank_level }}</span>
|
<span v-if="listing.rank_level">{{ listing.rank_level }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="detail-code-chip" type="button" @click="copyListingCode">
|
||||||
|
编号 {{ formatListingCode(listing) }}
|
||||||
|
</button>
|
||||||
<h1>{{ getListingTitle(listing) }}</h1>
|
<h1>{{ getListingTitle(listing) }}</h1>
|
||||||
<p>{{ getListingSubtitle(listing) }}</p>
|
<p>{{ getListingSubtitle(listing) }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -606,6 +620,19 @@ function listingPrice(item: Listing) {
|
|||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-code-chip {
|
||||||
|
width: fit-content;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1px solid rgba(147, 197, 253, 0.6);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(239, 246, 255, 0.92);
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-hero-overlay h1 {
|
.detail-hero-overlay h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
type HomeBannerSlide,
|
type HomeBannerSlide,
|
||||||
} from '@/features/listings/api/homeConfig'
|
} from '@/features/listings/api/homeConfig'
|
||||||
import {
|
import {
|
||||||
assetRegions,
|
|
||||||
formatHafCoinM,
|
|
||||||
getCoinM,
|
getCoinM,
|
||||||
getCoinWan,
|
getCoinWan,
|
||||||
getListingChips,
|
getListingChips,
|
||||||
@@ -102,7 +100,6 @@ type RangePresets = Record<string, Array<{ label: string; min: string; max: stri
|
|||||||
|
|
||||||
const selectedFilters = ref<SelectedFilters>({})
|
const selectedFilters = ref<SelectedFilters>({})
|
||||||
const rangeFilters = ref<RangeFilters>({})
|
const rangeFilters = ref<RangeFilters>({})
|
||||||
const searchValue = ref('')
|
|
||||||
const sortOpen = ref(false)
|
const sortOpen = ref(false)
|
||||||
const filterOpen = ref(false)
|
const filterOpen = ref(false)
|
||||||
|
|
||||||
@@ -306,7 +303,6 @@ function toggleSortPanel() {
|
|||||||
function clearFilters() {
|
function clearFilters() {
|
||||||
selectedFilters.value = {}
|
selectedFilters.value = {}
|
||||||
rangeFilters.value = {}
|
rangeFilters.value = {}
|
||||||
searchValue.value = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@@ -324,12 +320,7 @@ const activeFilterCount = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const filteredListings = computed(() => {
|
const filteredListings = computed(() => {
|
||||||
const keyword = searchValue.value.trim().toLowerCase()
|
const filtered = listings.value.filter(item => matchesFilters(item))
|
||||||
const filtered = listings.value.filter(item => {
|
|
||||||
if (!matchesFilters(item)) return false
|
|
||||||
if (!keyword) return true
|
|
||||||
return searchText(item).includes(keyword)
|
|
||||||
})
|
|
||||||
return sortListings(filtered)
|
return sortListings(filtered)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -371,24 +362,6 @@ function inRange(value: number, range: { min: string; max: string }) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchText(item: Listing) {
|
|
||||||
return [
|
|
||||||
item.title,
|
|
||||||
item.description,
|
|
||||||
getServerRegion(item),
|
|
||||||
getLoginMethod(item),
|
|
||||||
item.rank_level,
|
|
||||||
readAssetString(item, 'season_insurance'),
|
|
||||||
readAssetString(item, 'stamina_level'),
|
|
||||||
readAssetString(item, 'load_level'),
|
|
||||||
formatHafCoinM(getCoinWan(item)),
|
|
||||||
assetRegions(item).join(' '),
|
|
||||||
...publishOptions.value.skin_groups.map(group => getSkinGroup(item, group.key).join(' ')),
|
|
||||||
]
|
|
||||||
.join(' ')
|
|
||||||
.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortListings(items: Listing[]) {
|
function sortListings(items: Listing[]) {
|
||||||
const sorted = [...items]
|
const sorted = [...items]
|
||||||
if (sortBy.value === 'comprehensive') return sorted
|
if (sortBy.value === 'comprehensive') return sorted
|
||||||
@@ -438,19 +411,8 @@ function parseQuantityUnit(price: string) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索 + 排序 + 筛选入口 -->
|
<!-- 排序 + 筛选入口,搜索统一保留在首页顶部 -->
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="search-box">
|
|
||||||
<svg class="search-icon" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<input v-model="searchValue" placeholder="搜区服 / 段位 / 哈夫币数量" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="toolbar-btn sort-btn" @click="toggleSortPanel">
|
<button type="button" class="toolbar-btn sort-btn" @click="toggleSortPanel">
|
||||||
<span>{{ activeSortLabel }}</span>
|
<span>{{ activeSortLabel }}</span>
|
||||||
<svg
|
<svg
|
||||||
@@ -748,7 +710,7 @@ function parseQuantityUnit(price: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
/* 工具栏:搜索 + 排序 + 筛选 */
|
/* 工具栏:排序 + 筛选 */
|
||||||
/* ================================================================== */
|
/* ================================================================== */
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -757,47 +719,6 @@ function parseQuantityUnit(price: string) {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-box {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex: 0 1 360px;
|
|
||||||
height: 38px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #f4f6f8;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
transition:
|
|
||||||
border-color 0.15s,
|
|
||||||
box-shadow 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box:focus-within {
|
|
||||||
border-color: #1477ff;
|
|
||||||
box-shadow: 0 0 0 3px rgba(20, 119, 255, 0.1);
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: #a0aab6;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box input {
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
background: transparent;
|
|
||||||
color: #17233d;
|
|
||||||
font-size: 13px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box input::placeholder {
|
|
||||||
color: #a0aab6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-btn {
|
.toolbar-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { formatMoney } from '@/shared/utils/money'
|
|||||||
import {
|
import {
|
||||||
assetRegions,
|
assetRegions,
|
||||||
formatHafCoinM,
|
formatHafCoinM,
|
||||||
|
formatListingCode,
|
||||||
formatRatio,
|
formatRatio,
|
||||||
getCoinWan,
|
getCoinWan,
|
||||||
getDailyLoss,
|
getDailyLoss,
|
||||||
@@ -257,6 +258,16 @@ function readError(error: unknown, fallback: string) {
|
|||||||
}
|
}
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyListingCode() {
|
||||||
|
if (!listing.value) return
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(formatListingCode(listing.value))
|
||||||
|
showToast({ message: '商品编号已复制', icon: 'passed' })
|
||||||
|
} catch {
|
||||||
|
showToast({ message: '复制失败', icon: 'cross' })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -309,6 +320,9 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</van-tag>
|
</van-tag>
|
||||||
<van-tag v-if="listing.rank_level" plain size="medium">{{ listing.rank_level }}</van-tag>
|
<van-tag v-if="listing.rank_level" plain size="medium">{{ listing.rank_level }}</van-tag>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="mobile-code-chip" type="button" @click="copyListingCode">
|
||||||
|
编号 {{ formatListingCode(listing) }}
|
||||||
|
</button>
|
||||||
<h2 class="detail-title">{{ getListingTitle(listing) }}</h2>
|
<h2 class="detail-title">{{ getListingTitle(listing) }}</h2>
|
||||||
<p class="detail-desc">{{ getListingSubtitle(listing) }}</p>
|
<p class="detail-desc">{{ getListingSubtitle(listing) }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -601,6 +615,17 @@ function readError(error: unknown, fallback: string) {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-code-chip {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 4px 9px;
|
||||||
|
border: 1px solid #dbeafe;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-title {
|
.detail-title {
|
||||||
margin: 0 0 6px;
|
margin: 0 0 6px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface Order {
|
|||||||
id: number
|
id: number
|
||||||
order_no: string
|
order_no: string
|
||||||
listing_id: number
|
listing_id: number
|
||||||
|
listing_no: string
|
||||||
account_id: number
|
account_id: number
|
||||||
owner_id: number
|
owner_id: number
|
||||||
renter_id: number
|
renter_id: number
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
|
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
import { formatListingNo } from '@/utils/listingDisplay'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -43,6 +44,9 @@ const uploadingEvidence = ref(false)
|
|||||||
const openingChat = ref(false)
|
const openingChat = ref(false)
|
||||||
const order = ref<Order | null>(null)
|
const order = ref<Order | null>(null)
|
||||||
const handoffRecords = ref<HandoffRecord[]>([])
|
const handoffRecords = ref<HandoffRecord[]>([])
|
||||||
|
const listingCode = computed(() =>
|
||||||
|
order.value ? formatListingNo(order.value.listing_no, order.value.listing_id) : '-'
|
||||||
|
)
|
||||||
const handoffContent = ref('')
|
const handoffContent = ref('')
|
||||||
const checkoutForm = ref({
|
const checkoutForm = ref({
|
||||||
content: '',
|
content: '',
|
||||||
@@ -600,6 +604,16 @@ function getStatusTagType(status: string) {
|
|||||||
if (['cancelled', 'closed'].includes(status)) return 'danger'
|
if (['cancelled', 'closed'].includes(status)) return 'danger'
|
||||||
return 'danger'
|
return 'danger'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyListingCode() {
|
||||||
|
if (!order.value) return
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(listingCode.value)
|
||||||
|
showToast({ message: '商品编号已复制', icon: 'passed' })
|
||||||
|
} catch {
|
||||||
|
showToast({ message: '复制失败', icon: 'cross' })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -634,6 +648,9 @@ function getStatusTagType(status: string) {
|
|||||||
{{ orderStatusLabel(order.status) }}
|
{{ orderStatusLabel(order.status) }}
|
||||||
</van-tag>
|
</van-tag>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="listing-code-chip" type="button" @click="copyListingCode">
|
||||||
|
商品编号 {{ listingCode }}
|
||||||
|
</button>
|
||||||
<h2 class="order-title">{{ order.title }}</h2>
|
<h2 class="order-title">{{ order.title }}</h2>
|
||||||
<div class="order-meta-grid">
|
<div class="order-meta-grid">
|
||||||
<div class="meta-item">
|
<div class="meta-item">
|
||||||
@@ -762,7 +779,7 @@ function getStatusTagType(status: string) {
|
|||||||
|
|
||||||
<!-- Handoff Records list -->
|
<!-- Handoff Records list -->
|
||||||
<section class="card-section">
|
<section class="card-section">
|
||||||
<h3 class="section-title">交接日志</h3>
|
<h3 class="section-title">交接日志 · 商品编号 {{ listingCode }}</h3>
|
||||||
<van-empty
|
<van-empty
|
||||||
v-if="handoffRecords.length === 0"
|
v-if="handoffRecords.length === 0"
|
||||||
image="search"
|
image="search"
|
||||||
@@ -1276,6 +1293,17 @@ function getStatusTagType(status: string) {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listing-code-chip {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 4px 9px;
|
||||||
|
border: 1px solid #dbeafe;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.order-title {
|
.order-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '@/features/orders/api/orders'
|
} from '@/features/orders/api/orders'
|
||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
import { formatDateMinute } from '@/utils/time'
|
import { formatDateMinute } from '@/utils/time'
|
||||||
|
import { formatListingNo } from '@/utils/listingDisplay'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -139,6 +140,19 @@ function ownerActualIncome(order: Order) {
|
|||||||
const value = order.checkout?.owner_income_amount
|
const value = order.checkout?.owner_income_amount
|
||||||
return typeof value === 'number' ? value : null
|
return typeof value === 'number' ? value : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatListingCode(order: Order) {
|
||||||
|
return formatListingNo(order.listing_no, order.listing_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyListingCode(order: Order) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(formatListingCode(order))
|
||||||
|
showToast({ message: '商品编号已复制', icon: 'passed' })
|
||||||
|
} catch {
|
||||||
|
showToast({ message: '复制失败', icon: 'cross' })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -197,6 +211,9 @@ function ownerActualIncome(order: Order) {
|
|||||||
|
|
||||||
<!-- 卡片体:关键信息 -->
|
<!-- 卡片体:关键信息 -->
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<button class="listing-code-chip" type="button" @click.stop="copyListingCode(order)">
|
||||||
|
商品编号 {{ formatListingCode(order) }}
|
||||||
|
</button>
|
||||||
<h3 class="order-title">{{ order.title }}</h3>
|
<h3 class="order-title">{{ order.title }}</h3>
|
||||||
<div class="tag-row">
|
<div class="tag-row">
|
||||||
<span class="info-tag">{{ order.server_region }}</span>
|
<span class="info-tag">{{ order.server_region }}</span>
|
||||||
@@ -370,6 +387,17 @@ function ownerActualIncome(order: Order) {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listing-code-chip {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 4px 9px;
|
||||||
|
border: 1px solid #dbeafe;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
/* 状态徽章颜色 - 现代轻量化配色 */
|
/* 状态徽章颜色 - 现代轻量化配色 */
|
||||||
.badge-pending_payment {
|
.badge-pending_payment {
|
||||||
color: #ff6a00;
|
color: #ff6a00;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { ChatDotRound, Loading } from '@element-plus/icons-vue'
|
import { ChatDotRound, CopyDocument, Loading } from '@element-plus/icons-vue'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
|
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
import { formatListingNo } from '@/utils/listingDisplay'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -48,6 +49,10 @@ const handoffRecords = ref<HandoffRecord[]>([])
|
|||||||
const handoffContent = ref('')
|
const handoffContent = ref('')
|
||||||
const paymentDialogVisible = ref(false)
|
const paymentDialogVisible = ref(false)
|
||||||
const activePayment = ref<PaymentOrder | null>(null)
|
const activePayment = ref<PaymentOrder | null>(null)
|
||||||
|
|
||||||
|
const listingCode = computed(() =>
|
||||||
|
order.value ? formatListingNo(order.value.listing_no, order.value.listing_id) : '-'
|
||||||
|
)
|
||||||
const paymentQRCodeURL = ref('')
|
const paymentQRCodeURL = ref('')
|
||||||
const qrGenerating = ref(false)
|
const qrGenerating = ref(false)
|
||||||
const checkingPayment = ref(false)
|
const checkingPayment = ref(false)
|
||||||
@@ -675,6 +680,16 @@ function formatHandoffRecordType(type: string) {
|
|||||||
}
|
}
|
||||||
return typeMap[type] || type
|
return typeMap[type] || type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyListingCode() {
|
||||||
|
if (!order.value) return
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(listingCode.value)
|
||||||
|
ElMessage.success('商品编号已复制')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -685,6 +700,9 @@ function formatHandoffRecordType(type: string) {
|
|||||||
<p class="eyebrow">订单号:{{ order.order_no }}</p>
|
<p class="eyebrow">订单号:{{ order.order_no }}</p>
|
||||||
<h1>{{ order.title }}</h1>
|
<h1>{{ order.title }}</h1>
|
||||||
<p class="order-meta">{{ order.server_region }} / {{ order.login_platform }}</p>
|
<p class="order-meta">{{ order.server_region }} / {{ order.login_platform }}</p>
|
||||||
|
<el-button size="small" plain :icon="CopyDocument" @click="copyListingCode">
|
||||||
|
商品编号 {{ listingCode }}
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" :loading="openingChat" @click="openOrderChat">
|
<el-button type="primary" :loading="openingChat" @click="openOrderChat">
|
||||||
<el-icon style="margin-right: 4px"><ChatDotRound /></el-icon>
|
<el-icon style="margin-right: 4px"><ChatDotRound /></el-icon>
|
||||||
@@ -793,6 +811,7 @@ function formatHandoffRecordType(type: string) {
|
|||||||
<div v-if="order" class="timeline-section">
|
<div v-if="order" class="timeline-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>交接记录</h2>
|
<h2>交接记录</h2>
|
||||||
|
<span>商品编号 {{ listingCode }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-if="handoffRecords.length === 0" description="暂无交接记录" />
|
<el-empty v-if="handoffRecords.length === 0" description="暂无交接记录" />
|
||||||
<el-timeline v-else>
|
<el-timeline v-else>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { fetchOrders, type Order } from '@/features/orders'
|
|||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
import { orderStatusLabel } from '@/utils/statusLabels'
|
import { orderStatusLabel } from '@/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
import { formatListingNo } from '@/utils/listingDisplay'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const orders = ref<Order[]>([])
|
const orders = ref<Order[]>([])
|
||||||
@@ -40,6 +41,9 @@ const displayOrders = computed(() => {
|
|||||||
filtered = filtered.filter(
|
filtered = filtered.filter(
|
||||||
order =>
|
order =>
|
||||||
order.order_no.toLowerCase().includes(keyword) ||
|
order.order_no.toLowerCase().includes(keyword) ||
|
||||||
|
formatListingCode(order).toLowerCase().includes(keyword) ||
|
||||||
|
String(order.listing_id).includes(keyword) ||
|
||||||
|
String(order.account_id).includes(keyword) ||
|
||||||
order.title.toLowerCase().includes(keyword)
|
order.title.toLowerCase().includes(keyword)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -152,6 +156,19 @@ async function copyOrderNo(orderNo: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatListingCode(order: Order) {
|
||||||
|
return formatListingNo(order.listing_no, order.listing_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyListingCode(order: Order) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(formatListingCode(order))
|
||||||
|
ElMessage.success('商品编号已复制')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getPaymentDeadline(order: Order) {
|
function getPaymentDeadline(order: Order) {
|
||||||
if (order.status !== 'pending_payment' || !order.created_at) return null
|
if (order.status !== 'pending_payment' || !order.created_at) return null
|
||||||
if (order.payment_deadline_at) {
|
if (order.payment_deadline_at) {
|
||||||
@@ -182,7 +199,7 @@ function getCountdownMinutes(order: Order) {
|
|||||||
<div class="orders-toolbar">
|
<div class="orders-toolbar">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchKeyword"
|
v-model="searchKeyword"
|
||||||
placeholder="搜索订单号 / 账号 / 起始数字"
|
placeholder="搜索订单号 / 商品编号 / 账号"
|
||||||
:prefix-icon="Search"
|
:prefix-icon="Search"
|
||||||
clearable
|
clearable
|
||||||
class="search-input"
|
class="search-input"
|
||||||
@@ -220,6 +237,16 @@ function getCountdownMinutes(order: Order) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="商品编号" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="order-no-cell">
|
||||||
|
<span class="order-no-text">{{ formatListingCode(row) }}</span>
|
||||||
|
<el-icon class="copy-icon" @click="copyListingCode(row)">
|
||||||
|
<CopyDocument />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="title" label="账号" min-width="180" />
|
<el-table-column prop="title" label="账号" min-width="180" />
|
||||||
<el-table-column label="金额" width="132">
|
<el-table-column label="金额" width="132">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -299,6 +326,10 @@ function getCountdownMinutes(order: Order) {
|
|||||||
{{ orderRole(order) }}
|
{{ orderRole(order) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="listing-code-line" type="button" @click.stop="copyListingCode(order)">
|
||||||
|
商品编号 {{ formatListingCode(order) }}
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
</button>
|
||||||
<div class="card-title">{{ order.title }}</div>
|
<div class="card-title">{{ order.title }}</div>
|
||||||
<div class="card-meta">
|
<div class="card-meta">
|
||||||
<div class="meta-row">
|
<div class="meta-row">
|
||||||
@@ -425,6 +456,22 @@ function getCountdownMinutes(order: Order) {
|
|||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listing-code-line {
|
||||||
|
display: inline-flex;
|
||||||
|
width: fit-content;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 4px 9px;
|
||||||
|
border: 1px solid #dbeafe;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.copy-icon {
|
.copy-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useSessionStore } from '@/stores/session'
|
|||||||
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
|
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
import { formatMoney } from '@/shared/utils/money'
|
import { formatMoney } from '@/shared/utils/money'
|
||||||
|
import { formatListingNo } from '@/utils/listingDisplay'
|
||||||
|
|
||||||
const session = useSessionStore()
|
const session = useSessionStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -113,6 +114,11 @@ function money(value: unknown) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table v-loading="loading" class="table-panel" :data="displayOrders">
|
<el-table v-loading="loading" class="table-panel" :data="displayOrders">
|
||||||
|
<el-table-column label="商品编号" width="140">
|
||||||
|
<template #default="{ row }">{{
|
||||||
|
formatListingNo(row.listing_no, row.listing_id)
|
||||||
|
}}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="order_no" label="订单号" min-width="220" />
|
<el-table-column prop="order_no" label="订单号" min-width="220" />
|
||||||
<el-table-column prop="title" label="账号" min-width="180" />
|
<el-table-column prop="title" label="账号" min-width="180" />
|
||||||
<el-table-column label="金额" width="120">
|
<el-table-column label="金额" width="120">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Plus, Refresh } from '@element-plus/icons-vue'
|
import { CopyDocument, Plus, Refresh } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
type Listing,
|
type Listing,
|
||||||
} from '@/features/listings'
|
} from '@/features/listings'
|
||||||
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
||||||
import { getListingSellerPrice } from '@/utils/listingDisplay'
|
import { formatListingCode, getListingSellerPrice } from '@/utils/listingDisplay'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submittingID = ref<number | null>(null)
|
const submittingID = ref<number | null>(null)
|
||||||
@@ -106,6 +106,15 @@ function listingPrice(row: Listing) {
|
|||||||
return `¥${Math.round(getListingSellerPrice(row))}`
|
return `¥${Math.round(getListingSellerPrice(row))}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyListingCode(row: Listing) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(formatListingCode(row))
|
||||||
|
ElMessage.success('商品编号已复制')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function coinText(row: Listing) {
|
function coinText(row: Listing) {
|
||||||
const coinM = Number(row.haf_coin_amount || 0) / 1000000
|
const coinM = Number(row.haf_coin_amount || 0) / 1000000
|
||||||
return Number.isInteger(coinM) ? `${coinM}M` : `${coinM.toFixed(2)}M`
|
return Number.isInteger(coinM) ? `${coinM}M` : `${coinM.toFixed(2)}M`
|
||||||
@@ -185,7 +194,13 @@ function isPendingReview(row: Listing) {
|
|||||||
<article v-for="item in displayListings" :key="item.id" class="listing-card">
|
<article v-for="item in displayListings" :key="item.id" class="listing-card">
|
||||||
<div class="listing-main">
|
<div class="listing-main">
|
||||||
<div class="listing-title-row">
|
<div class="listing-title-row">
|
||||||
<h3>{{ item.title }}</h3>
|
<div class="listing-title-main">
|
||||||
|
<button class="listing-code-chip" type="button" @click="copyListingCode(item)">
|
||||||
|
编号 {{ formatListingCode(item) }}
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
</button>
|
||||||
|
<h3>{{ item.title }}</h3>
|
||||||
|
</div>
|
||||||
<div class="listing-tags">
|
<div class="listing-tags">
|
||||||
<span class="status-pill" :class="statusTone(item.status)">{{
|
<span class="status-pill" :class="statusTone(item.status)">{{
|
||||||
listingStatusLabel(item.status)
|
listingStatusLabel(item.status)
|
||||||
@@ -348,6 +363,27 @@ function isPendingReview(row: Listing) {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.listing-title-main {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-code-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
width: fit-content;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 4px 9px;
|
||||||
|
border: 1px solid #dbeafe;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.listing-title-row h3 {
|
.listing-title-row h3 {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -23,8 +23,15 @@ export function getCoinM(item: Listing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatListingCode(item: Listing) {
|
export function formatListingCode(item: Listing) {
|
||||||
if (item.listing_no) return item.listing_no
|
return formatListingNo(item.listing_no, item.id)
|
||||||
return `SP${String(item.id).padStart(6, '0')}`
|
}
|
||||||
|
|
||||||
|
export function formatListingNo(listingNo?: string, listingID?: number | string) {
|
||||||
|
if (listingNo) return listingNo
|
||||||
|
if (listingID !== undefined && listingID !== null && listingID !== '') {
|
||||||
|
return `SP${String(listingID).padStart(6, '0')}`
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatHafCoinM(amountWan: number) {
|
export function formatHafCoinM(amountWan: number) {
|
||||||
|
|||||||
@@ -23,8 +23,15 @@ export function getCoinM(item: Listing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatListingCode(item: Listing) {
|
export function formatListingCode(item: Listing) {
|
||||||
if (item.listing_no) return item.listing_no
|
return formatListingNo(item.listing_no, item.id)
|
||||||
return `SP${String(item.id).padStart(6, '0')}`
|
}
|
||||||
|
|
||||||
|
export function formatListingNo(listingNo?: string, listingID?: number | string) {
|
||||||
|
if (listingNo) return listingNo
|
||||||
|
if (listingID !== undefined && listingID !== null && listingID !== '') {
|
||||||
|
return `SP${String(listingID).padStart(6, '0')}`
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatHafCoinM(amountWan: number) {
|
export function formatHafCoinM(amountWan: number) {
|
||||||
|
|||||||
Reference in New Issue
Block a user