完善测试建表并统一前端格式

This commit is contained in:
yml2213
2026-07-14 01:32:14 +08:00
parent 2efcede800
commit 2ca6fd9306
36 changed files with 714 additions and 478 deletions
+12 -4
View File
@@ -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(`![${imageAltText(uploaded.filename)}|w=720](${uploaded.medium_url || uploaded.url})`)
insertContentAtCursor(
`![${imageAltText(uploaded.filename)}|w=720](${uploaded.medium_url || uploaded.url})`
)
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 = `![${escapeMarkdownImageText(alt)}${widthMark}](${url})`
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(