第 5 阶段:纠纷、通知与后台-4
This commit is contained in:
@@ -29,7 +29,7 @@ npm run dev
|
||||
- 短信验证码使用 mock 适配器,验证码会打印在后端日志中。
|
||||
- 实名认证使用 mock 适配器,登录后请求 `POST /api/realname/start`,提交合法姓名和 18 位身份证号会直接通过。
|
||||
- 实名状态可通过 `GET /api/realname/status` 查询。
|
||||
- 租号发布需要登录并完成实名认证;开发态 `POST /api/listings/{id}/submit-review` 会自动审核通过并上架。
|
||||
- 租号发布需要登录并完成实名认证;`POST /api/listings/{id}/submit-review` 会进入待审核,后台通过后才上架。
|
||||
- 订单创建需要登录;开发态会直接锁定账号并进入待交接,暂不接真实支付和押金冻结。
|
||||
- 账号交接已支持号主提交说明、租客确认收号,确认后订单进入租赁中。
|
||||
- 归还流程已支持租客提交归还、号主确认归还,完成后账号重新上架。
|
||||
@@ -39,6 +39,7 @@ npm run dev
|
||||
- 系统配置已支持默认配置初始化和后台编辑,页面为 `http://localhost:5173/admin/system-configs`,更新会写入审计日志。
|
||||
- 后台已使用独立登录,页面为 `http://localhost:5173/admin/login`;开发态默认管理员为 `admin / admin123456`。
|
||||
- 后台仪表盘已接入真实统计数据,页面为 `http://localhost:5173/admin/dashboard`。
|
||||
- 商品审核后台已接入,页面为 `http://localhost:5173/admin/listings/review`。
|
||||
|
||||
## 文档
|
||||
|
||||
|
||||
@@ -43,3 +43,7 @@ type CreateRequest struct {
|
||||
}
|
||||
|
||||
type UpdateRequest = CreateRequest
|
||||
|
||||
type ReviewRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
@@ -79,6 +79,46 @@ func (h *Handler) SubmitReview(c *gin.Context) {
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) ListPendingReview(c *gin.Context) {
|
||||
items, err := h.service.ListPendingReview()
|
||||
if err != nil {
|
||||
writeListingError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *Handler) Approve(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
item, err := h.service.Approve(id)
|
||||
if err != nil {
|
||||
writeListingError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) Reject(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req ReviewRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "审核拒绝原因不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.Reject(id, req)
|
||||
if err != nil {
|
||||
writeListingError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) Offline(c *gin.Context) {
|
||||
ownerID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
@@ -102,6 +104,49 @@ func (r *Repository) SubmitReview(ownerID uint64, listingID uint64) (*ListingDTO
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if listing.Status == "rented" {
|
||||
return ErrListingLocked
|
||||
}
|
||||
listing.Status = "draft"
|
||||
listing.ReviewStatus = "pending"
|
||||
listing.ReviewReason = ""
|
||||
listing.PublishedAt = nil
|
||||
account.Status = "draft"
|
||||
if err := tx.Save(account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
dto = toDTO(*account, *listing)
|
||||
return nil
|
||||
})
|
||||
return dto, err
|
||||
}
|
||||
|
||||
func (r *Repository) ListPendingReview() ([]ListingDTO, error) {
|
||||
var rows []listingRow
|
||||
err := r.baseQuery().
|
||||
Where("l.review_status = ?", "pending").
|
||||
Order("l.updated_at ASC, l.id ASC").
|
||||
Limit(200).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rowsToDTO(rows), nil
|
||||
}
|
||||
|
||||
func (r *Repository) Approve(listingID uint64) (*ListingDTO, error) {
|
||||
var dto *ListingDTO
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
listing, account, err := r.findForReviewUpdate(tx, listingID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if listing.Status == "rented" {
|
||||
return ErrListingLocked
|
||||
}
|
||||
now := time.Now()
|
||||
listing.Status = "published"
|
||||
listing.ReviewStatus = "approved"
|
||||
@@ -114,6 +159,55 @@ func (r *Repository) SubmitReview(ownerID uint64, listingID uint64) (*ListingDTO
|
||||
if err := tx.Save(listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
listingID := listing.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: listing.OwnerID,
|
||||
Type: "listing_review",
|
||||
Title: "发布审核通过",
|
||||
Content: "你的租号发布已审核通过并上架。",
|
||||
BizType: "listing",
|
||||
BizID: &listingID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
dto = toDTO(*account, *listing)
|
||||
return nil
|
||||
})
|
||||
return dto, err
|
||||
}
|
||||
|
||||
func (r *Repository) Reject(listingID uint64, req ReviewRequest) (*ListingDTO, error) {
|
||||
var dto *ListingDTO
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
listing, account, err := r.findForReviewUpdate(tx, listingID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if listing.Status == "rented" {
|
||||
return ErrListingLocked
|
||||
}
|
||||
listing.Status = "draft"
|
||||
listing.ReviewStatus = "rejected"
|
||||
listing.ReviewReason = req.Reason
|
||||
listing.PublishedAt = nil
|
||||
account.Status = "draft"
|
||||
if err := tx.Save(account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
listingID := listing.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: listing.OwnerID,
|
||||
Type: "listing_review",
|
||||
Title: "发布审核未通过",
|
||||
Content: "你的租号发布未通过审核,请根据原因修改后重新提交。",
|
||||
BizType: "listing",
|
||||
BizID: &listingID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
dto = toDTO(*account, *listing)
|
||||
return nil
|
||||
})
|
||||
@@ -140,9 +234,7 @@ func (r *Repository) Offline(ownerID uint64, listingID uint64) error {
|
||||
|
||||
func (r *Repository) ListPublic() ([]ListingDTO, error) {
|
||||
var rows []listingRow
|
||||
err := r.db.Table("rental_listings AS l").
|
||||
Select("l.*, a.title, a.description, a.game_name, a.server_region, a.login_platform, a.rank_level, a.haf_coin_amount").
|
||||
Joins("JOIN game_accounts AS a ON a.id = l.account_id").
|
||||
err := r.baseQuery().
|
||||
Where("l.status = ? AND l.review_status = ?", "published", "approved").
|
||||
Order("l.published_at DESC, l.id DESC").
|
||||
Limit(100).
|
||||
@@ -155,9 +247,7 @@ func (r *Repository) ListPublic() ([]ListingDTO, error) {
|
||||
|
||||
func (r *Repository) ListMine(ownerID uint64) ([]ListingDTO, error) {
|
||||
var rows []listingRow
|
||||
err := r.db.Table("rental_listings AS l").
|
||||
Select("l.*, a.title, a.description, a.game_name, a.server_region, a.login_platform, a.rank_level, a.haf_coin_amount").
|
||||
Joins("JOIN game_accounts AS a ON a.id = l.account_id").
|
||||
err := r.baseQuery().
|
||||
Where("l.owner_id = ?", ownerID).
|
||||
Order("l.id DESC").
|
||||
Scan(&rows).Error
|
||||
@@ -189,9 +279,7 @@ func (r *Repository) findOwnedForUpdate(tx *gorm.DB, ownerID uint64, listingID u
|
||||
|
||||
func (r *Repository) findDTO(where string, args ...any) (*ListingDTO, error) {
|
||||
var row listingRow
|
||||
err := r.db.Table("rental_listings AS l").
|
||||
Select("l.*, a.title, a.description, a.game_name, a.server_region, a.login_platform, a.rank_level, a.haf_coin_amount").
|
||||
Joins("JOIN game_accounts AS a ON a.id = l.account_id").
|
||||
err := r.baseQuery().
|
||||
Where(where, args...).
|
||||
First(&row).Error
|
||||
if err != nil {
|
||||
@@ -201,6 +289,24 @@ func (r *Repository) findDTO(where string, args ...any) (*ListingDTO, error) {
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) baseQuery() *gorm.DB {
|
||||
return r.db.Table("rental_listings AS l").
|
||||
Select("l.*, a.title, a.description, a.game_name, a.server_region, a.login_platform, a.rank_level, a.haf_coin_amount").
|
||||
Joins("JOIN game_accounts AS a ON a.id = l.account_id")
|
||||
}
|
||||
|
||||
func (r *Repository) findForReviewUpdate(tx *gorm.DB, listingID uint64) (*model.RentalListing, *model.GameAccount, error) {
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, listingID).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var account model.GameAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &listing, &account, nil
|
||||
}
|
||||
|
||||
type listingRow struct {
|
||||
model.RentalListing
|
||||
Title string
|
||||
|
||||
@@ -45,6 +45,30 @@ func (s *Service) SubmitReview(ownerID uint64, id uint64) (*ListingDTO, error) {
|
||||
return s.repo.SubmitReview(ownerID, id)
|
||||
}
|
||||
|
||||
func (s *Service) ListPendingReview() ([]ListingDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ListPendingReview()
|
||||
}
|
||||
|
||||
func (s *Service) Approve(id uint64) (*ListingDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.Approve(id)
|
||||
}
|
||||
|
||||
func (s *Service) Reject(id uint64, req ReviewRequest) (*ListingDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if req.Reason == "" {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
return s.repo.Reject(id, req)
|
||||
}
|
||||
|
||||
func (s *Service) Offline(ownerID uint64, id uint64) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
|
||||
@@ -176,6 +176,9 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
adminRoutes.GET("/me", adminAuthHandler.Me)
|
||||
adminRoutes.POST("/auth/logout", adminAuthHandler.Logout)
|
||||
adminRoutes.GET("/dashboard", adminDashboardHandler.Summary)
|
||||
adminRoutes.GET("/listings/pending", listingHandler.ListPendingReview)
|
||||
adminRoutes.POST("/listings/:id/approve", listingHandler.Approve)
|
||||
adminRoutes.POST("/listings/:id/reject", listingHandler.Reject)
|
||||
adminRoutes.GET("/disputes", disputeHandler.AdminList)
|
||||
adminRoutes.POST("/disputes/:id/arbitrate", disputeHandler.AdminArbitrate)
|
||||
adminRoutes.GET("/system-configs", systemConfigHandler.List)
|
||||
|
||||
@@ -50,6 +50,9 @@ API 规划以 [项目计划](project-plan.md) 第 10 章为准。
|
||||
- `POST /api/admin/auth/logout`
|
||||
- `GET /api/admin/me`
|
||||
- `GET /api/admin/dashboard`
|
||||
- `GET /api/admin/listings/pending`
|
||||
- `POST /api/admin/listings/{id}/approve`
|
||||
- `POST /api/admin/listings/{id}/reject`
|
||||
- `GET /api/admin/disputes`
|
||||
- `POST /api/admin/disputes/{id}/arbitrate`
|
||||
- `GET /api/admin/system-configs`
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
- 发布租号前必须登录并完成实名认证。
|
||||
- 一期创建发布时同时创建 `game_accounts` 和 `rental_listings`。
|
||||
- 哈夫币数量是号主手动填报值,不代表实时值。
|
||||
- 当前提交审核会 mock 为自动通过并上架,后续再接后台人工审核。
|
||||
- 当前提交审核会进入 `review_status = pending`,由后台商品审核通过后才上架。
|
||||
- 后台审核通过后,发布状态变为 `published`,审核状态变为 `approved`,账号状态变为 `published`。
|
||||
- 后台审核拒绝后,发布保留为草稿,审核状态变为 `rejected`,拒绝原因写入 `review_reason`。
|
||||
- 公开列表只展示 `status = published` 且 `review_status = approved` 的发布。
|
||||
|
||||
## 开发态订单创建
|
||||
|
||||
@@ -75,3 +75,18 @@ export async function offlineListing(id: number) {
|
||||
const { data } = await apiClient.delete<ApiResponse<{ offline: boolean }>>(`/listings/${id}`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchPendingReviewListings() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: Listing[] }>>('/admin/listings/pending')
|
||||
return data.data.items
|
||||
}
|
||||
|
||||
export async function approveListing(id: number) {
|
||||
const { data } = await apiClient.post<ApiResponse<Listing>>(`/admin/listings/${id}/approve`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function rejectListing(id: number, reason: string) {
|
||||
const { data } = await apiClient.post<ApiResponse<Listing>>(`/admin/listings/${id}/reject`, { reason })
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Bell, House, Key, Operation, Phone, ScaleToOriginal, Shop, Tickets, UserFilled, Wallet } from '@element-plus/icons-vue'
|
||||
import { Bell, House, Key, Operation, Phone, ScaleToOriginal, Shop, Tickets, UserFilled, Wallet, View } from '@element-plus/icons-vue'
|
||||
|
||||
const navItems = [
|
||||
{ label: '首页', to: '/', icon: House },
|
||||
@@ -9,6 +9,7 @@ const navItems = [
|
||||
{ label: '通知', to: '/notifications', icon: Bell },
|
||||
{ label: '实名', to: '/realname', icon: UserFilled },
|
||||
{ label: '后台登录', to: '/admin/login', icon: Key },
|
||||
{ label: '审核', to: '/admin/listings/review', icon: View },
|
||||
{ label: '仲裁', to: '/admin/disputes', icon: ScaleToOriginal },
|
||||
{ label: '配置', to: '/admin/system-configs', icon: Operation },
|
||||
{ label: '登录', to: '/login', icon: Phone },
|
||||
|
||||
@@ -19,6 +19,7 @@ const router = createRouter({
|
||||
{ path: '/seller/earnings', name: 'seller-earnings', component: () => import('@/views/seller/SellerEarningsView.vue') },
|
||||
{ path: '/admin/login', name: 'admin-login', component: () => import('@/views/admin/AdminLoginView.vue') },
|
||||
{ path: '/admin/dashboard', name: 'admin-dashboard', component: () => import('@/views/admin/AdminDashboardView.vue') },
|
||||
{ path: '/admin/listings/review', name: 'admin-listing-review', component: () => import('@/views/admin/AdminListingReviewView.vue') },
|
||||
{ path: '/admin/disputes', name: 'admin-disputes', component: () => import('@/views/admin/AdminDisputesView.vue') },
|
||||
{ path: '/admin/system-configs', name: 'admin-system-configs', component: () => import('@/views/admin/AdminSystemConfigsView.vue') },
|
||||
],
|
||||
|
||||
@@ -75,10 +75,10 @@ function money(value?: number) {
|
||||
<span>待仲裁申诉</span>
|
||||
<strong>{{ dashboard.pending.disputes }}</strong>
|
||||
</RouterLink>
|
||||
<div class="pending-row">
|
||||
<RouterLink class="pending-row" to="/admin/listings/review">
|
||||
<span>待审核商品</span>
|
||||
<strong>{{ dashboard.pending.listing_reviews }}</strong>
|
||||
</div>
|
||||
</RouterLink>
|
||||
<div class="pending-row">
|
||||
<span>待交接订单</span>
|
||||
<strong>{{ dashboard.pending.pending_handoffs }}</strong>
|
||||
@@ -95,6 +95,10 @@ function money(value?: number) {
|
||||
<span>仲裁中心</span>
|
||||
<strong>进入</strong>
|
||||
</RouterLink>
|
||||
<RouterLink class="pending-row" to="/admin/listings/review">
|
||||
<span>商品审核</span>
|
||||
<strong>进入</strong>
|
||||
</RouterLink>
|
||||
<RouterLink class="pending-row" to="/admin/system-configs">
|
||||
<span>系统配置</span>
|
||||
<strong>进入</strong>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { approveListing, fetchPendingReviewListings, rejectListing, type Listing } from '@/api/listings'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const listings = ref<Listing[]>([])
|
||||
const activeListing = ref<Listing | null>(null)
|
||||
const rejectReason = ref('')
|
||||
|
||||
onMounted(loadListings)
|
||||
|
||||
async function loadListings() {
|
||||
loading.value = true
|
||||
try {
|
||||
listings.value = await fetchPendingReviewListings()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApprove(row: Listing) {
|
||||
submitting.value = true
|
||||
try {
|
||||
await approveListing(row.id)
|
||||
ElMessage.success('审核通过,商品已上架')
|
||||
await loadListings()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '审核失败'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openReject(row: Listing) {
|
||||
activeListing.value = row
|
||||
rejectReason.value = row.review_reason || ''
|
||||
}
|
||||
|
||||
async function handleReject() {
|
||||
if (!activeListing.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
await rejectListing(activeListing.value.id, rejectReason.value)
|
||||
ElMessage.success('已拒绝发布并通知号主')
|
||||
activeListing.value = null
|
||||
rejectReason.value = ''
|
||||
await loadListings()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '拒绝失败'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function readError(error: unknown, fallback: string) {
|
||||
if (typeof error === 'object' && error && 'response' in error) {
|
||||
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||
return response?.data?.message || fallback
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header-row">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Review</p>
|
||||
<h1>商品审核</h1>
|
||||
<p>审核号主提交的账号资产、价格、押金和租期规则。</p>
|
||||
</div>
|
||||
<el-button @click="loadListings">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="listings">
|
||||
<el-table-column prop="title" label="标题" min-width="180" />
|
||||
<el-table-column prop="owner_id" label="号主" width="90" />
|
||||
<el-table-column prop="server_region" label="区服" width="120" />
|
||||
<el-table-column prop="login_platform" label="平台" width="120" />
|
||||
<el-table-column prop="haf_coin_amount" label="哈夫币" width="110" />
|
||||
<el-table-column prop="rank_level" label="段位" width="110" />
|
||||
<el-table-column prop="price_hourly" label="时租" width="90" />
|
||||
<el-table-column prop="deposit_amount" label="押金" width="90" />
|
||||
<el-table-column prop="updated_at" label="提交时间" min-width="180" />
|
||||
<el-table-column label="操作" width="170" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" :loading="submitting" @click="handleApprove(row)">通过</el-button>
|
||||
<el-button size="small" type="danger" @click="openReject(row)">拒绝</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog :model-value="!!activeListing" title="拒绝发布" width="560px" @update:model-value="activeListing = null">
|
||||
<div v-if="activeListing" class="dialog-body">
|
||||
<p><strong>{{ activeListing.title }}</strong></p>
|
||||
<el-input v-model="rejectReason" type="textarea" :rows="4" placeholder="填写拒绝原因,号主会在通知中看到审核结果" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="activeListing = null">取消</el-button>
|
||||
<el-button type="danger" :loading="submitting" @click="handleReject">确认拒绝</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
@@ -20,7 +20,7 @@ async function loadListings() {
|
||||
|
||||
async function submitReview(id: number) {
|
||||
await submitListingReview(id)
|
||||
ElMessage.success('已提交审核并自动上架')
|
||||
ElMessage.success('已提交审核,等待后台处理')
|
||||
await loadListings()
|
||||
}
|
||||
|
||||
@@ -52,10 +52,13 @@ async function offline(id: number) {
|
||||
<el-table-column prop="deposit_amount" label="押金" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="110" />
|
||||
<el-table-column prop="review_status" label="审核" width="110" />
|
||||
<el-table-column label="操作" width="180">
|
||||
<el-table-column prop="review_reason" label="审核原因" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="190">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="submitReview(row.id)">上架</el-button>
|
||||
<el-button size="small" type="danger" @click="offline(row.id)">下架</el-button>
|
||||
<el-button size="small" :disabled="row.review_status === 'pending' || row.status === 'rented'" @click="submitReview(row.id)">
|
||||
提审
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" :disabled="row.status === 'rented'" @click="offline(row.id)">下架</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
Reference in New Issue
Block a user