完善测试建表并统一前端格式
This commit is contained in:
@@ -42,7 +42,8 @@ export interface UpdateRulePayload {
|
||||
// ── 渠道 ──
|
||||
|
||||
export async function fetchPushChannels() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: PushChannel[] }>>('/admin/push-channels')
|
||||
const { data } =
|
||||
await apiClient.get<ApiResponse<{ items: PushChannel[] }>>('/admin/push-channels')
|
||||
return data.data.items
|
||||
}
|
||||
|
||||
@@ -52,17 +53,24 @@ export async function createPushChannel(payload: CreateChannelPayload) {
|
||||
}
|
||||
|
||||
export async function updatePushChannel(id: number, payload: UpdateChannelPayload) {
|
||||
const { data } = await apiClient.put<ApiResponse<PushChannel>>(`/admin/push-channels/${id}`, payload)
|
||||
const { data } = await apiClient.put<ApiResponse<PushChannel>>(
|
||||
`/admin/push-channels/${id}`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function deletePushChannel(id: number) {
|
||||
const { data } = await apiClient.delete<ApiResponse<{ deleted: boolean }>>(`/admin/push-channels/${id}`)
|
||||
const { data } = await apiClient.delete<ApiResponse<{ deleted: boolean }>>(
|
||||
`/admin/push-channels/${id}`
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function testPushChannel(id: number) {
|
||||
const { data } = await apiClient.post<ApiResponse<{ sent: boolean }>>(`/admin/push-channels/${id}/test`)
|
||||
const { data } = await apiClient.post<ApiResponse<{ sent: boolean }>>(
|
||||
`/admin/push-channels/${id}/test`
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElInput, ElMessage } from 'element-plus'
|
||||
import { Bell, Document, Operation, Picture, QuestionFilled, Warning } from '@element-plus/icons-vue'
|
||||
import {
|
||||
Bell,
|
||||
Document,
|
||||
Operation,
|
||||
Picture,
|
||||
QuestionFilled,
|
||||
Warning,
|
||||
} from '@element-plus/icons-vue'
|
||||
import type { Announcement } from '@/features/announcement'
|
||||
import type {
|
||||
CreateAnnouncementRequest,
|
||||
@@ -134,7 +141,9 @@ async function handleImageChange(event: Event) {
|
||||
uploadingImage.value = true
|
||||
try {
|
||||
const uploaded = await uploadAdminFile(file, 'announcement')
|
||||
insertContentAtCursor(``)
|
||||
insertContentAtCursor(
|
||||
``
|
||||
)
|
||||
ElMessage.success('图片已插入')
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '图片上传失败'))
|
||||
@@ -205,7 +214,9 @@ function applyImageSettings() {
|
||||
return
|
||||
}
|
||||
|
||||
const widthMark = imageSettingsForm.value.useCustomWidth ? `|w=${imageSettingsForm.value.width}` : ''
|
||||
const widthMark = imageSettingsForm.value.useCustomWidth
|
||||
? `|w=${imageSettingsForm.value.width}`
|
||||
: ''
|
||||
const nextMarkdown = ``
|
||||
const content = formData.value.content
|
||||
formData.value.content = `${content.slice(0, range.start)}${nextMarkdown}${content.slice(range.end)}`
|
||||
@@ -358,7 +369,11 @@ function escapeMarkdownImageText(text: string) {
|
||||
</el-form-item>
|
||||
<el-form-item label="显示宽度">
|
||||
<div class="image-width-control">
|
||||
<el-switch v-model="imageSettingsForm.useCustomWidth" active-text="自定义" inactive-text="原宽" />
|
||||
<el-switch
|
||||
v-model="imageSettingsForm.useCustomWidth"
|
||||
active-text="自定义"
|
||||
inactive-text="原宽"
|
||||
/>
|
||||
<el-slider
|
||||
v-model="imageSettingsForm.width"
|
||||
:min="120"
|
||||
|
||||
@@ -208,7 +208,12 @@ function accountTypeLabel(type: string) {
|
||||
:admin="true"
|
||||
:preview-src-list="withdrawal.certificate_urls"
|
||||
fit="cover"
|
||||
:image-style="{ width: '100px', height: '100px', borderRadius: '4px', cursor: 'pointer' }"
|
||||
:image-style="{
|
||||
width: '100px',
|
||||
height: '100px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
@@ -142,7 +142,9 @@ function shortDate(date: string) {
|
||||
<h2 class="section-title">
|
||||
<el-icon><Shop /></el-icon>
|
||||
账号上下架
|
||||
<small class="section-subtitle">按北京时间自然日 · 近 {{ dashboard.listing_daily.days }} 日</small>
|
||||
<small class="section-subtitle"
|
||||
>按北京时间自然日 · 近 {{ dashboard.listing_daily.days }} 日</small
|
||||
>
|
||||
</h2>
|
||||
|
||||
<div class="listing-daily-grid">
|
||||
@@ -167,7 +169,10 @@ function shortDate(date: string) {
|
||||
<div
|
||||
class="mini-bar publish"
|
||||
:style="{
|
||||
height: barHeight(row.published_count, trendMax(r => r.published_count)),
|
||||
height: barHeight(
|
||||
row.published_count,
|
||||
trendMax(r => r.published_count)
|
||||
),
|
||||
}"
|
||||
/>
|
||||
<em>{{ shortDate(row.date) }}</em>
|
||||
@@ -230,7 +235,10 @@ function shortDate(date: string) {
|
||||
<div
|
||||
class="mini-bar trade"
|
||||
:style="{
|
||||
height: barHeight(row.trade_leave_count, trendMax(r => r.trade_leave_count)),
|
||||
height: barHeight(
|
||||
row.trade_leave_count,
|
||||
trendMax(r => r.trade_leave_count)
|
||||
),
|
||||
}"
|
||||
/>
|
||||
<em>{{ shortDate(row.date) }}</em>
|
||||
|
||||
@@ -9,11 +9,7 @@ import { fetchAdminUsers, type AdminUserItem } from '@/features/admin/api/adminU
|
||||
import { fetchAdminFileBlob } from '@/shared/api/files'
|
||||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||
import { adminPath } from '@/shared/utils/adminPath'
|
||||
import {
|
||||
formatCentWithSymbol,
|
||||
formatMoneyWithSymbol,
|
||||
roundMoney,
|
||||
} from '@/shared/utils/money'
|
||||
import { formatCentWithSymbol, formatMoneyWithSymbol, roundMoney } from '@/shared/utils/money'
|
||||
import {
|
||||
adminMarkListingAbnormal,
|
||||
adminOfflineListing,
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { Plus, Refresh, View, Edit, Delete, Download, Upload, StarFilled } from '@element-plus/icons-vue'
|
||||
import {
|
||||
Plus,
|
||||
Refresh,
|
||||
View,
|
||||
Edit,
|
||||
Delete,
|
||||
Download,
|
||||
Upload,
|
||||
StarFilled,
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
import {
|
||||
fetchPaymentConfigs,
|
||||
@@ -432,7 +441,12 @@ function deleteDisabledReason(row: PaymentConfig) {
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="last_used_at" label="最后成功支付" min-width="180" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
prop="last_used_at"
|
||||
label="最后成功支付"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.last_used_at ? formatDateTime(row.last_used_at, '-') : '-' }}
|
||||
</template>
|
||||
|
||||
@@ -216,9 +216,15 @@ async function saveRule() {
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" :icon="Edit" @click="openEditChannel(row)">编辑</el-button>
|
||||
<el-button size="small" type="primary" :icon="Edit" @click="openEditChannel(row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button size="small" type="primary" @click="handleTestChannel(row)">测试</el-button>
|
||||
<el-button size="small" :type="row.enabled ? 'warning' : 'success'" @click="toggleChannel(row)">
|
||||
<el-button
|
||||
size="small"
|
||||
:type="row.enabled ? 'warning' : 'success'"
|
||||
@click="toggleChannel(row)"
|
||||
>
|
||||
{{ row.enabled ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" :icon="Delete" @click="handleDeleteChannel(row)" />
|
||||
@@ -226,7 +232,10 @@ async function saveRule() {
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-empty v-if="!channelsLoading && channels.length === 0" description="暂无推送渠道,点击上方按钮新增" />
|
||||
<el-empty
|
||||
v-if="!channelsLoading && channels.length === 0"
|
||||
description="暂无推送渠道,点击上方按钮新增"
|
||||
/>
|
||||
|
||||
<!-- 规则管理 -->
|
||||
<div class="section-header" style="margin-top: 32px">
|
||||
@@ -251,7 +260,9 @@ async function saveRule() {
|
||||
<el-table-column label="消息模板" min-width="250" prop="message_template" />
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" :icon="Edit" @click="openEditRule(row)">编辑</el-button>
|
||||
<el-button size="small" type="primary" :icon="Edit" @click="openEditRule(row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -276,11 +287,12 @@ async function saveRule() {
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-for="field in channelConfigFields(channelForm.type)" :key="field.key" :label="field.label">
|
||||
<el-input
|
||||
v-model="channelForm.config[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
<el-form-item
|
||||
v-for="field in channelConfigFields(channelForm.type)"
|
||||
:key="field.key"
|
||||
:label="field.label"
|
||||
>
|
||||
<el-input v-model="channelForm.config[field.key]" :placeholder="field.placeholder" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -300,7 +312,12 @@ async function saveRule() {
|
||||
<span class="form-hint">库存低于此值时触发预警</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="消息模板">
|
||||
<el-input v-model="ruleForm.message_template" type="textarea" :rows="3" placeholder="支持 {{.Count}} 占位符" />
|
||||
<el-input
|
||||
v-model="ruleForm.message_template"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="支持 {{.Count}} 占位符"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
||||
@@ -37,7 +37,12 @@ import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
const adminSession = useAdminSessionStore()
|
||||
const loading = ref(false)
|
||||
const qrcodes = ref<ChatQrCode[]>([])
|
||||
const stats = ref<QrCodeStats>({ unused_count: 0, used_count: 0, disabled_count: 0, total_count: 0 })
|
||||
const stats = ref<QrCodeStats>({
|
||||
unused_count: 0,
|
||||
used_count: 0,
|
||||
disabled_count: 0,
|
||||
total_count: 0,
|
||||
})
|
||||
const canManageQrCodes = computed(() => adminSession.hasPermission('qrcode:manage'))
|
||||
|
||||
const currentPage = ref(1)
|
||||
@@ -136,7 +141,9 @@ const editStatusOptions = computed(() =>
|
||||
const maxBatchUploadCount = 20
|
||||
const uploading = computed(() => uploadPendingCount.value > 0)
|
||||
const uploadBusy = computed(() => uploading.value || uploadSaving.value)
|
||||
const uploadedImageCountText = computed(() => `${uploadedImages.value.length}/${maxBatchUploadCount}`)
|
||||
const uploadedImageCountText = computed(
|
||||
() => `${uploadedImages.value.length}/${maxBatchUploadCount}`
|
||||
)
|
||||
const renameLoadingIds = ref(new Set<number>())
|
||||
const selectedRows = ref<ChatQrCode[]>([])
|
||||
const selectedCount = computed(() => selectedRows.value.length)
|
||||
@@ -228,7 +235,12 @@ function parseGroupNameFromOcrText(text: string) {
|
||||
return !excludedPatterns.some(pattern => pattern.test(line))
|
||||
})
|
||||
|
||||
return candidates.find(line => line.includes('群')) || candidates.find(line => /[-—]/.test(line)) || candidates[0] || ''
|
||||
return (
|
||||
candidates.find(line => line.includes('群')) ||
|
||||
candidates.find(line => /[-—]/.test(line)) ||
|
||||
candidates[0] ||
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
const ocrConcurrencyLimit = 4
|
||||
@@ -333,10 +345,6 @@ async function handleFileUpload(options: { file: File }) {
|
||||
}
|
||||
}
|
||||
|
||||
function imagePreviewSources() {
|
||||
return uploadedImages.value.map(item => item.url).filter(Boolean)
|
||||
}
|
||||
|
||||
function removeImage(index: number) {
|
||||
const [removed] = uploadedImages.value.splice(index, 1)
|
||||
if (removed?.previewUrl) {
|
||||
@@ -377,9 +385,7 @@ async function submitUpload() {
|
||||
}
|
||||
uploadSaving.value = true
|
||||
try {
|
||||
const expiresAt = uploadForm.expires_at
|
||||
? new Date(uploadForm.expires_at).toISOString()
|
||||
: null
|
||||
const expiresAt = uploadForm.expires_at ? new Date(uploadForm.expires_at).toISOString() : null
|
||||
const items: CreateQrCodePayload[] = uploadedImages.value.map(img => ({
|
||||
image_url: img.url,
|
||||
group_name: img.groupName,
|
||||
@@ -832,12 +838,7 @@ onMounted(reloadAll)
|
||||
:image-style="{ width: '52px', height: '52px', borderRadius: '6px' }"
|
||||
:preview-src-list="[img.url]"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
class="uploaded-local-preview"
|
||||
:src="img.previewUrl"
|
||||
alt=""
|
||||
/>
|
||||
<img v-else class="uploaded-local-preview" :src="img.previewUrl" alt="" />
|
||||
<button
|
||||
class="uploaded-remove"
|
||||
type="button"
|
||||
@@ -872,7 +873,11 @@ onMounted(reloadAll)
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="uploadForm.note" placeholder="如:7月企微群A(统一备注,适用于同一批次)" maxlength="50" />
|
||||
<el-input
|
||||
v-model="uploadForm.note"
|
||||
placeholder="如:7月企微群A(统一备注,适用于同一批次)"
|
||||
maxlength="50"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="过期时间">
|
||||
<el-date-picker
|
||||
@@ -932,10 +937,20 @@ onMounted(reloadAll)
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="群名">
|
||||
<el-input v-model="editForm.group_name" placeholder="企业微信群名" maxlength="64" clearable />
|
||||
<el-input
|
||||
v-model="editForm.group_name"
|
||||
placeholder="企业微信群名"
|
||||
maxlength="64"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="企微改名">
|
||||
<el-switch v-model="editForm.wecom_renamed" inline-prompt active-text="已改" inactive-text="未改" />
|
||||
<el-switch
|
||||
v-model="editForm.wecom_renamed"
|
||||
inline-prompt
|
||||
active-text="已改"
|
||||
inactive-text="未改"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.note" placeholder="如:7月企微群A" maxlength="50" />
|
||||
|
||||
@@ -130,11 +130,7 @@ onMounted(loadOrders)
|
||||
</div>
|
||||
|
||||
<div class="review-cards">
|
||||
<div
|
||||
v-for="order in orders"
|
||||
:key="order.id"
|
||||
class="review-card"
|
||||
>
|
||||
<div v-for="order in orders" :key="order.id" class="review-card">
|
||||
<div class="card-header">
|
||||
<div class="order-info">
|
||||
<span class="order-no" @click="openDetail(order)">{{ order.order_no }}</span>
|
||||
@@ -167,26 +163,23 @@ onMounted(loadOrders)
|
||||
<div class="info-row">
|
||||
<span class="info-label">订单总额</span>
|
||||
<span class="info-value"
|
||||
>{{ formatCentWithSymbol((order.rent_amount_cent || 0) + order.deposit_amount_cent) }}(租金{{
|
||||
formatCentWithSymbol(order.rent_amount_cent || 0)
|
||||
}}
|
||||
/ 押金{{ formatCentWithSymbol(order.deposit_amount_cent) }})</span
|
||||
>{{
|
||||
formatCentWithSymbol((order.rent_amount_cent || 0) + order.deposit_amount_cent)
|
||||
}}(租金{{ formatCentWithSymbol(order.rent_amount_cent || 0) }} / 押金{{
|
||||
formatCentWithSymbol(order.deposit_amount_cent)
|
||||
}})</span
|
||||
>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">退款金额</span>
|
||||
<span class="info-value refund-amount">{{ formatCentWithSymbol(order.refund_amount_cent || 0) }}</span>
|
||||
<span class="info-value refund-amount">{{
|
||||
formatCentWithSymbol(order.refund_amount_cent || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<el-button
|
||||
:icon="View"
|
||||
size="small"
|
||||
@click="openDetail(order)"
|
||||
>
|
||||
查看详情
|
||||
</el-button>
|
||||
<el-button :icon="View" size="small" @click="openDetail(order)"> 查看详情 </el-button>
|
||||
<el-button
|
||||
:icon="Check"
|
||||
type="primary"
|
||||
@@ -211,28 +204,16 @@ onMounted(loadOrders)
|
||||
</div>
|
||||
|
||||
<!-- 驳回弹窗 -->
|
||||
<el-dialog
|
||||
v-model="rejectVisible"
|
||||
title="驳回退款"
|
||||
width="420px"
|
||||
>
|
||||
<el-dialog v-model="rejectVisible" title="驳回退款" width="420px">
|
||||
<div v-if="rejectOrder" class="reject-dialog">
|
||||
<p class="reject-tip">请选择驳回方式:</p>
|
||||
<div class="reject-options">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="handleRejectRestore"
|
||||
>
|
||||
<el-button type="primary" :loading="submitting" @click="handleRejectRestore">
|
||||
驳回退款(恢复订单)
|
||||
</el-button>
|
||||
<p class="option-desc">仅驳回退款请求,订单恢复至待交接状态,交接流程可继续</p>
|
||||
|
||||
<el-button
|
||||
type="danger"
|
||||
:loading="submitting"
|
||||
@click="handleRejectClose"
|
||||
>
|
||||
<el-button type="danger" :loading="submitting" @click="handleRejectClose">
|
||||
驳回退款并关闭订单
|
||||
</el-button>
|
||||
<p class="option-desc">驳回退款且关闭订单,商品下架归档,订单不可再操作</p>
|
||||
|
||||
@@ -106,7 +106,8 @@ const listingGroupWelcomeConfig = computed(
|
||||
() => configs.value.find(item => item.key === 'chat.listing_group_welcome') || null
|
||||
)
|
||||
const renterRetentionConfig = computed(
|
||||
() => configs.value.find(item => item.key === 'chat.renter_retention_days_after_order_end') || null
|
||||
() =>
|
||||
configs.value.find(item => item.key === 'chat.renter_retention_days_after_order_end') || null
|
||||
)
|
||||
const paddleOcrTokenConfig = computed(() =>
|
||||
systemConfigByKey(
|
||||
|
||||
Reference in New Issue
Block a user