完善二维码池 OCR 配置与识别
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { Check, CopyDocument, Plus, Refresh, Search, UploadFilled } from '@element-plus/icons-vue'
|
||||
|
||||
import {
|
||||
batchCreateQrCodes,
|
||||
deleteQrCode,
|
||||
fetchQrCodeStats,
|
||||
fetchQrCodes,
|
||||
recognizeQrCodeGroupName,
|
||||
updateQrCode,
|
||||
type ChatQrCode,
|
||||
type CreateQrCodePayload,
|
||||
@@ -18,6 +19,7 @@ import { uploadAdminFile } from '@/shared/api/files'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
|
||||
const loading = ref(false)
|
||||
const qrcodes = ref<ChatQrCode[]>([])
|
||||
@@ -32,7 +34,15 @@ const statusFilter = ref<QrCodeStatus | ''>('')
|
||||
const uploadVisible = ref(false)
|
||||
const uploadPendingCount = ref(0)
|
||||
const uploadSaving = ref(false)
|
||||
const uploadedImages = ref<{ url: string; file: File }[]>([])
|
||||
type UploadedQrImage = {
|
||||
url: string
|
||||
previewUrl: string
|
||||
file: File
|
||||
groupName: string
|
||||
ocrStatus: 'recognizing' | 'done' | 'failed'
|
||||
}
|
||||
|
||||
const uploadedImages = ref<UploadedQrImage[]>([])
|
||||
const uploadForm = reactive({
|
||||
note: '',
|
||||
expires_at: '' as string,
|
||||
@@ -44,8 +54,10 @@ const editUploading = ref(false)
|
||||
const editForm = reactive({
|
||||
id: 0,
|
||||
image_url: '',
|
||||
group_name: '',
|
||||
note: '',
|
||||
status: 'unused' as QrCodeStatus,
|
||||
wecom_renamed: false,
|
||||
expires_at: '' as string,
|
||||
was_issued: false,
|
||||
})
|
||||
@@ -75,6 +87,7 @@ const maxBatchUploadCount = 20
|
||||
const uploading = computed(() => uploadPendingCount.value > 0)
|
||||
const uploadBusy = computed(() => uploading.value || uploadSaving.value)
|
||||
const uploadedImageCountText = computed(() => `${uploadedImages.value.length}/${maxBatchUploadCount}`)
|
||||
const renameLoadingIds = ref(new Set<number>())
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
@@ -108,6 +121,39 @@ function handlePageChange() {
|
||||
loadList()
|
||||
}
|
||||
|
||||
function parseGroupNameFromOcrText(text: string) {
|
||||
const lines = text
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.replace(/\s+/g, '').replace(/[||]/g, ''))
|
||||
.map(line => line.replace(/[^\u4e00-\u9fa5a-zA-Z0-9_()()《》\-—·]+/g, ''))
|
||||
.filter(Boolean)
|
||||
|
||||
const excludedPatterns = [
|
||||
/使用.*微信/,
|
||||
/企业微信/,
|
||||
/扫码/,
|
||||
/加入/,
|
||||
/二维码/,
|
||||
/有效/,
|
||||
/天内/,
|
||||
/前/,
|
||||
/^\d+$/,
|
||||
]
|
||||
|
||||
const candidates = lines.filter(line => {
|
||||
if (line.length < 3 || line.length > 64) return false
|
||||
if (!/[\u4e00-\u9fa5]/.test(line)) return false
|
||||
return !excludedPatterns.some(pattern => pattern.test(line))
|
||||
})
|
||||
|
||||
return candidates.find(line => line.includes('群')) || candidates.find(line => /[-—]/.test(line)) || candidates[0] || ''
|
||||
}
|
||||
|
||||
async function recognizeGroupName(file: File) {
|
||||
const result = await recognizeQrCodeGroupName(file)
|
||||
return result.group_name || parseGroupNameFromOcrText(result.raw_text || '')
|
||||
}
|
||||
|
||||
// 上传:逐个上传图片拿 URL
|
||||
async function handleFileUpload(options: { file: File }) {
|
||||
if (uploadedImages.value.length + uploadPendingCount.value >= maxBatchUploadCount) {
|
||||
@@ -121,17 +167,54 @@ async function handleFileUpload(options: { file: File }) {
|
||||
|
||||
uploadPendingCount.value += 1
|
||||
try {
|
||||
const uploaded = await uploadAdminFile(options.file, 'qrcode')
|
||||
uploadedImages.value.push({ url: uploaded.url, file: options.file })
|
||||
const item: UploadedQrImage = {
|
||||
url: '',
|
||||
previewUrl: URL.createObjectURL(options.file),
|
||||
file: options.file,
|
||||
groupName: '',
|
||||
ocrStatus: 'recognizing',
|
||||
}
|
||||
uploadedImages.value.push(item)
|
||||
|
||||
const [uploaded, groupName] = await Promise.all([
|
||||
uploadAdminFile(options.file, 'qrcode'),
|
||||
recognizeGroupName(options.file).catch(error => {
|
||||
console.warn('二维码群名 OCR 失败', error)
|
||||
ElMessage.warning(readError(error, '二维码群名 OCR 失败'))
|
||||
return ''
|
||||
}),
|
||||
])
|
||||
item.url = uploaded.url
|
||||
item.groupName = groupName
|
||||
item.ocrStatus = groupName ? 'done' : 'failed'
|
||||
if (!groupName) {
|
||||
console.warn('二维码群名 OCR 未识别到有效群名', options.file.name)
|
||||
}
|
||||
} catch {
|
||||
const index = uploadedImages.value.findIndex(item => item.file === options.file && !item.url)
|
||||
if (index >= 0) removeImage(index)
|
||||
ElMessage.error('图片上传失败')
|
||||
} finally {
|
||||
uploadPendingCount.value = Math.max(0, uploadPendingCount.value - 1)
|
||||
}
|
||||
}
|
||||
|
||||
function imagePreviewSources() {
|
||||
return uploadedImages.value.map(item => item.url).filter(Boolean)
|
||||
}
|
||||
|
||||
function removeImage(index: number) {
|
||||
uploadedImages.value.splice(index, 1)
|
||||
const [removed] = uploadedImages.value.splice(index, 1)
|
||||
if (removed?.previewUrl) {
|
||||
URL.revokeObjectURL(removed.previewUrl)
|
||||
}
|
||||
}
|
||||
|
||||
function clearUploadedImages() {
|
||||
uploadedImages.value.forEach(item => {
|
||||
if (item.previewUrl) URL.revokeObjectURL(item.previewUrl)
|
||||
})
|
||||
uploadedImages.value = []
|
||||
}
|
||||
|
||||
function handleUploadExceed() {
|
||||
@@ -139,7 +222,7 @@ function handleUploadExceed() {
|
||||
}
|
||||
|
||||
function openUpload() {
|
||||
uploadedImages.value = []
|
||||
clearUploadedImages()
|
||||
uploadForm.note = ''
|
||||
uploadForm.expires_at = ''
|
||||
uploadVisible.value = true
|
||||
@@ -161,6 +244,7 @@ async function submitUpload() {
|
||||
: null
|
||||
const items: CreateQrCodePayload[] = uploadedImages.value.map(img => ({
|
||||
image_url: img.url,
|
||||
group_name: img.groupName,
|
||||
note: uploadForm.note,
|
||||
expires_at: expiresAt,
|
||||
}))
|
||||
@@ -173,11 +257,17 @@ async function submitUpload() {
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearUploadedImages()
|
||||
})
|
||||
|
||||
function openEdit(row: ChatQrCode) {
|
||||
editForm.id = row.id
|
||||
editForm.image_url = row.image_url
|
||||
editForm.group_name = row.group_name || ''
|
||||
editForm.note = row.note
|
||||
editForm.status = row.status
|
||||
editForm.wecom_renamed = row.wecom_renamed
|
||||
editForm.expires_at = row.expires_at ? row.expires_at.replace('T', ' ').slice(0, 16) : ''
|
||||
editForm.was_issued = row.status === 'used' || Boolean(row.conversation_id || row.used_at)
|
||||
editVisible.value = true
|
||||
@@ -203,8 +293,10 @@ async function handleEditImageUpload(options: { file: File }) {
|
||||
async function submitEdit() {
|
||||
await updateQrCode(editForm.id, {
|
||||
image_url: editForm.image_url,
|
||||
group_name: editForm.group_name,
|
||||
note: editForm.note,
|
||||
status: editForm.status,
|
||||
wecom_renamed: editForm.wecom_renamed,
|
||||
expires_at: editForm.expires_at ? new Date(editForm.expires_at).toISOString() : undefined,
|
||||
clear_expires_at: !editForm.expires_at,
|
||||
})
|
||||
@@ -213,6 +305,51 @@ async function submitEdit() {
|
||||
await reloadAll()
|
||||
}
|
||||
|
||||
async function copyText(text: string, successMessage: string) {
|
||||
if (!text) {
|
||||
ElMessage.warning('暂无可复制内容')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success(successMessage)
|
||||
} catch {
|
||||
ElMessage.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
function boundConversationLabel(row: ChatQrCode) {
|
||||
if (row.bound_conversation_title) return row.bound_conversation_title
|
||||
if (row.conversation_id) return `#${row.conversation_id}`
|
||||
return ''
|
||||
}
|
||||
|
||||
function setRenameLoading(id: number, loading: boolean) {
|
||||
const next = new Set(renameLoadingIds.value)
|
||||
if (loading) {
|
||||
next.add(id)
|
||||
} else {
|
||||
next.delete(id)
|
||||
}
|
||||
renameLoadingIds.value = next
|
||||
}
|
||||
|
||||
async function handleRenameFlagChange(row: ChatQrCode, value: string | number | boolean) {
|
||||
const nextValue = Boolean(value)
|
||||
const previous = !nextValue
|
||||
row.wecom_renamed = nextValue
|
||||
setRenameLoading(row.id, true)
|
||||
try {
|
||||
await updateQrCode(row.id, { wecom_renamed: nextValue })
|
||||
ElMessage.success(nextValue ? '已标记企微已改名' : '已取消企微改名标记')
|
||||
} catch {
|
||||
row.wecom_renamed = previous
|
||||
ElMessage.error('标记失败')
|
||||
} finally {
|
||||
setRenameLoading(row.id, false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDisable(row: ChatQrCode) {
|
||||
await ElMessageBox.confirm('确定停用该二维码?停用后不再发放给新发布群。', '停用确认', {
|
||||
type: 'warning',
|
||||
@@ -305,14 +442,48 @@ onMounted(reloadAll)
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="群名" class-name="name-col" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<button
|
||||
class="copy-cell-button"
|
||||
type="button"
|
||||
:disabled="!row.group_name"
|
||||
@click="copyText(row.group_name, '群名已复制')"
|
||||
>
|
||||
<span>{{ row.group_name || '-' }}</span>
|
||||
<el-icon v-if="row.group_name"><CopyDocument /></el-icon>
|
||||
</button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" class-name="note-col" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="note-cell">{{ row.note || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定群" class-name="group-col" align="center">
|
||||
<el-table-column label="绑定群" class-name="group-col" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="group-cell">{{ row.conversation_id ? `#${row.conversation_id}` : '-' }}</span>
|
||||
<button
|
||||
class="copy-cell-button"
|
||||
type="button"
|
||||
:disabled="!boundConversationLabel(row)"
|
||||
@click="copyText(boundConversationLabel(row), '绑定群聊名称已复制')"
|
||||
>
|
||||
<span>{{ boundConversationLabel(row) || '-' }}</span>
|
||||
<el-icon v-if="boundConversationLabel(row)"><CopyDocument /></el-icon>
|
||||
</button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="企微改名" class-name="rename-col" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="row.wecom_renamed"
|
||||
:loading="renameLoadingIds.has(row.id)"
|
||||
inline-prompt
|
||||
:active-icon="Check"
|
||||
active-text="已改"
|
||||
inactive-text="未改"
|
||||
@change="(value: string | number | boolean) => handleRenameFlagChange(row, value)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" class-name="expire-col" align="center">
|
||||
@@ -360,7 +531,7 @@ onMounted(reloadAll)
|
||||
</el-card>
|
||||
|
||||
<!-- 上传弹窗 -->
|
||||
<el-dialog v-model="uploadVisible" title="批量添加企业微信群二维码" width="680px">
|
||||
<el-dialog v-model="uploadVisible" title="批量添加企业微信群二维码" width="680px" @close="clearUploadedImages">
|
||||
<el-form class="qrcode-upload-form" label-width="108px">
|
||||
<el-form-item label="二维码图片" required>
|
||||
<div class="batch-upload-area">
|
||||
@@ -386,23 +557,50 @@ onMounted(reloadAll)
|
||||
</el-upload>
|
||||
<div v-if="uploadedImages.length" class="uploaded-list">
|
||||
<div v-for="(img, idx) in uploadedImages" :key="idx" class="uploaded-item">
|
||||
<div class="uploaded-thumb">
|
||||
<AuthImage
|
||||
:source="img.url"
|
||||
fit="cover"
|
||||
:image-style="{ width: '52px', height: '52px', borderRadius: '6px' }"
|
||||
:preview-src-list="uploadedImages.map(item => item.url)"
|
||||
<div class="uploaded-main">
|
||||
<div class="uploaded-thumb">
|
||||
<AuthImage
|
||||
v-if="img.url"
|
||||
:source="img.url"
|
||||
fit="cover"
|
||||
:image-style="{ width: '52px', height: '52px', borderRadius: '6px' }"
|
||||
:preview-src-list="imagePreviewSources()"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
class="uploaded-local-preview"
|
||||
:src="img.previewUrl"
|
||||
alt=""
|
||||
/>
|
||||
<button
|
||||
class="uploaded-remove"
|
||||
type="button"
|
||||
:disabled="uploadBusy"
|
||||
@click="removeImage(idx)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="uploaded-meta">
|
||||
<div class="uploaded-name" :title="img.file.name">{{ img.file.name }}</div>
|
||||
<el-tag v-if="img.ocrStatus === 'recognizing'" size="small" type="info"
|
||||
>识别中</el-tag
|
||||
>
|
||||
<el-tag v-if="img.ocrStatus === 'done'" size="small" type="success">OCR</el-tag>
|
||||
<el-tag v-if="img.ocrStatus === 'failed'" size="small" type="warning"
|
||||
>待校对</el-tag
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uploaded-group-editor">
|
||||
<el-input
|
||||
v-model="img.groupName"
|
||||
size="small"
|
||||
placeholder="群名"
|
||||
maxlength="64"
|
||||
clearable
|
||||
/>
|
||||
<button
|
||||
class="uploaded-remove"
|
||||
type="button"
|
||||
:disabled="uploadBusy"
|
||||
@click="removeImage(idx)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="uploaded-name" :title="img.file.name">{{ img.file.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -467,6 +665,12 @@ onMounted(reloadAll)
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="群名">
|
||||
<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-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.note" placeholder="如:7月企微群A" maxlength="50" />
|
||||
</el-form-item>
|
||||
@@ -588,31 +792,39 @@ onMounted(reloadAll)
|
||||
|
||||
.qrcode-table :deep(.el-table__header-wrapper),
|
||||
.qrcode-table :deep(.el-table__body-wrapper) {
|
||||
overflow-x: hidden;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.qrcode-table :deep(.qr-col) {
|
||||
width: 12%;
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
.qrcode-table :deep(.status-col) {
|
||||
width: 10%;
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
.qrcode-table :deep(.name-col) {
|
||||
width: 18%;
|
||||
}
|
||||
|
||||
.qrcode-table :deep(.note-col) {
|
||||
width: 40%;
|
||||
width: 18%;
|
||||
}
|
||||
|
||||
.qrcode-table :deep(.group-col) {
|
||||
width: 10%;
|
||||
width: 18%;
|
||||
}
|
||||
|
||||
.qrcode-table :deep(.rename-col) {
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
.qrcode-table :deep(.expire-col) {
|
||||
width: 16%;
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
.qrcode-table :deep(.action-col) {
|
||||
width: 12%;
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.qrcode-thumb {
|
||||
@@ -631,12 +843,46 @@ onMounted(reloadAll)
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.group-cell,
|
||||
.expire-cell {
|
||||
color: #5f6b7a;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.copy-cell-button {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #303846;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.copy-cell-button span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.copy-cell-button .el-icon {
|
||||
flex: 0 0 auto;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.copy-cell-button:disabled {
|
||||
color: #a8b0bd;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -703,10 +949,10 @@ onMounted(reloadAll)
|
||||
|
||||
.uploaded-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
max-height: 224px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
border: 1px solid #eef1f5;
|
||||
@@ -716,6 +962,17 @@ onMounted(reloadAll)
|
||||
|
||||
.uploaded-item {
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #e7ebf2;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.uploaded-main {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.uploaded-thumb {
|
||||
@@ -725,12 +982,20 @@ onMounted(reloadAll)
|
||||
justify-content: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid #e7ebf2;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.uploaded-local-preview {
|
||||
display: block;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.uploaded-remove {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
@@ -756,16 +1021,31 @@ onMounted(reloadAll)
|
||||
}
|
||||
|
||||
.uploaded-name {
|
||||
margin-top: 5px;
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.uploaded-meta {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.uploaded-meta .el-tag {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.uploaded-group-editor {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.expired-tag {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,23 @@ const listingGroupWelcomeConfig = computed(
|
||||
const renterRetentionConfig = computed(
|
||||
() => configs.value.find(item => item.key === 'chat.renter_retention_days_after_order_end') || null
|
||||
)
|
||||
const paddleOcrTokenConfig = computed(() =>
|
||||
systemConfigByKey(
|
||||
'integration.paddle_ocr_token',
|
||||
'',
|
||||
'PaddleOCR API Token,用于二维码群名自动识别'
|
||||
)
|
||||
)
|
||||
const paddleOcrJobUrlConfig = computed(() =>
|
||||
systemConfigByKey(
|
||||
'integration.paddle_ocr_job_url',
|
||||
'https://paddleocr.aistudio-app.com/api/v2/ocr/jobs',
|
||||
'PaddleOCR 异步任务接口地址'
|
||||
)
|
||||
)
|
||||
const paddleOcrModelConfig = computed(() =>
|
||||
systemConfigByKey('integration.paddle_ocr_model', 'PaddleOCR-VL-1.6', 'PaddleOCR 识别模型')
|
||||
)
|
||||
|
||||
const regularConfigs = computed(() =>
|
||||
configs.value.filter(
|
||||
@@ -118,7 +135,10 @@ const regularConfigs = computed(() =>
|
||||
item.key !== 'profile.post_rental_notice' &&
|
||||
item.key !== 'chat.auto_welcome_message' &&
|
||||
item.key !== 'chat.listing_group_welcome' &&
|
||||
item.key !== 'chat.renter_retention_days_after_order_end'
|
||||
item.key !== 'chat.renter_retention_days_after_order_end' &&
|
||||
item.key !== 'integration.paddle_ocr_token' &&
|
||||
item.key !== 'integration.paddle_ocr_job_url' &&
|
||||
item.key !== 'integration.paddle_ocr_model'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -188,6 +208,19 @@ async function loadConfigs() {
|
||||
}
|
||||
}
|
||||
|
||||
function systemConfigByKey(key: string, fallbackValue: string, description: string): SystemConfig {
|
||||
const existing = configs.value.find(item => item.key === key)
|
||||
if (existing) return existing
|
||||
return {
|
||||
id: 0,
|
||||
key,
|
||||
value: fallbackValue,
|
||||
description,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit(row: SystemConfig) {
|
||||
currentEditingConfig.value = row
|
||||
if (row.key === 'listing.publish_options') {
|
||||
@@ -309,6 +342,9 @@ function formatHomeConfigStatus(row: SystemConfig | null, fallback: string) {
|
||||
}
|
||||
|
||||
function formatConfigValue(row: SystemConfig) {
|
||||
if (row.key.includes('token')) {
|
||||
return row.value.trim() ? '已配置' : '未配置'
|
||||
}
|
||||
const trimmed = row.value.trim()
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
return 'JSON 配置'
|
||||
@@ -599,6 +635,41 @@ function formatConfigValue(row: SystemConfig) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="publish-config-panel">
|
||||
<div class="publish-config-main">
|
||||
<div>
|
||||
<p class="eyebrow">PaddleOCR</p>
|
||||
<h2>二维码群名识别配置</h2>
|
||||
<span>配置企业微信群二维码上传时使用的 OCR 识别接口,保存后无需重新构建前端。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="publish-stat-grid home-stat-grid">
|
||||
<div class="publish-stat">
|
||||
<strong>{{ paddleOcrTokenConfig.value.trim() ? '已配置' : '未配置' }}</strong>
|
||||
<span>API Token</span>
|
||||
<el-button size="small" @click="openEdit(paddleOcrTokenConfig)">编辑</el-button>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>接口</strong>
|
||||
<span>{{ paddleOcrJobUrlConfig.value || '未配置' }}</span>
|
||||
<el-button size="small" @click="openEdit(paddleOcrJobUrlConfig)">编辑</el-button>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>{{ paddleOcrModelConfig.value || '未配置' }}</strong>
|
||||
<span>识别模型</span>
|
||||
<el-button size="small" @click="openEdit(paddleOcrModelConfig)">编辑</el-button>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>直连</strong>
|
||||
<span>二维码池上传时调用</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>更新</strong>
|
||||
<span>{{ formatHomeConfigStatus(paddleOcrTokenConfig, '未初始化') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="regularConfigs">
|
||||
<el-table-column prop="key" label="配置项" min-width="260" />
|
||||
<el-table-column label="当前值" min-width="180" show-overflow-tooltip>
|
||||
@@ -747,6 +818,7 @@ function formatConfigValue(row: SystemConfig) {
|
||||
.config-value {
|
||||
color: #4b5563;
|
||||
font-size: 13px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* 按钮文字清晰度优化 */
|
||||
|
||||
Reference in New Issue
Block a user