feat(distribution): 分站发布群只使用自己的二维码池
主站单独维护分站码池,池空时不再借用主站或其他分站,并去掉已停用的固定二维码入口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -170,3 +170,52 @@ export async function batchDeleteQrCodes(ids: number[]) {
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
function siteQrCodeBase(siteId: number) {
|
||||
return `/admin/distribution/sites/${siteId}/qrcodes`
|
||||
}
|
||||
|
||||
export async function fetchSiteQrCodes(siteId: number, query: QrCodeListQuery = {}) {
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<ChatQrCode>>>(
|
||||
siteQrCodeBase(siteId),
|
||||
{
|
||||
params: {
|
||||
status: query.status || undefined,
|
||||
keyword: query.keyword?.trim() || undefined,
|
||||
page: query.page || 1,
|
||||
limit: query.limit || 20,
|
||||
},
|
||||
silent: true,
|
||||
}
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchSiteQrCodeStats(siteId: number) {
|
||||
const { data } = await apiClient.get<ApiResponse<QrCodeStats>>(
|
||||
`${siteQrCodeBase(siteId)}/stats`,
|
||||
{ silent: true }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function batchCreateSiteQrCodes(siteId: number, items: CreateQrCodePayload[]) {
|
||||
const { data } = await apiClient.post<ApiResponse<ChatQrCode[]>>(
|
||||
`${siteQrCodeBase(siteId)}/batch`,
|
||||
{ items }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updateSiteQrCode(siteId: number, id: number, payload: UpdateQrCodePayload) {
|
||||
const { data } = await apiClient.patch<ApiResponse<null>>(
|
||||
`${siteQrCodeBase(siteId)}/${id}`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function deleteSiteQrCode(siteId: number, id: number) {
|
||||
const { data } = await apiClient.delete<ApiResponse<null>>(`${siteQrCodeBase(siteId)}/${id}`)
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Plus, Refresh, UploadFilled } from '@element-plus/icons-vue'
|
||||
|
||||
import {
|
||||
batchCreateSiteQrCodes,
|
||||
deleteSiteQrCode,
|
||||
fetchSiteQrCodeStats,
|
||||
fetchSiteQrCodes,
|
||||
getOCRJobResult,
|
||||
submitOCRJob,
|
||||
updateSiteQrCode,
|
||||
type ChatQrCode,
|
||||
type CreateQrCodePayload,
|
||||
type QrCodeStats,
|
||||
type QrCodeStatus,
|
||||
} from '@/features/admin/api/adminQrCodes'
|
||||
import {
|
||||
fetchDistributionSites,
|
||||
type DistributionSite,
|
||||
} from '@/features/admin/api/adminDistribution'
|
||||
import { uploadAdminFile } from '@/shared/api/files'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
import { optimizeImageForUpload } from '@/shared/utils/imageUpload'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||
|
||||
const adminSession = useAdminSessionStore()
|
||||
const canManage = computed(() => adminSession.hasPermission('qrcode:manage'))
|
||||
|
||||
const sites = ref<DistributionSite[]>([])
|
||||
const siteId = ref<number | undefined>()
|
||||
const loading = ref(false)
|
||||
const qrcodes = ref<ChatQrCode[]>([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const currentPageSize = ref(20)
|
||||
const statusFilter = ref<QrCodeStatus | ''>('')
|
||||
const stats = ref<QrCodeStats>({
|
||||
unused_count: 0,
|
||||
used_count: 0,
|
||||
disabled_count: 0,
|
||||
total_count: 0,
|
||||
})
|
||||
|
||||
const statusOptions: { label: string; value: QrCodeStatus | ''; type: string }[] = [
|
||||
{ label: '全部', value: '', type: 'info' },
|
||||
{ label: '待用', value: 'unused', type: 'success' },
|
||||
{ label: '已发放', value: 'used', type: 'primary' },
|
||||
{ label: '已停用', value: 'disabled', type: 'danger' },
|
||||
]
|
||||
const statusMap = computed(() => {
|
||||
const map: Record<string, { label: string; type: string }> = {}
|
||||
statusOptions.forEach(option => {
|
||||
if (option.value) map[option.value] = { label: option.label, type: option.type }
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
const uploadVisible = ref(false)
|
||||
const uploadSaving = ref(false)
|
||||
const uploadPendingCount = ref(0)
|
||||
const uploadNote = ref('')
|
||||
type UploadedQrImage = {
|
||||
id: string
|
||||
url: string
|
||||
previewUrl: string
|
||||
file: File
|
||||
groupName: string
|
||||
}
|
||||
const uploadedImages = ref<UploadedQrImage[]>([])
|
||||
const maxBatchUploadCount = 20
|
||||
|
||||
const selectedSite = computed(() => sites.value.find(site => site.id === siteId.value))
|
||||
|
||||
async function loadSites() {
|
||||
sites.value = await fetchDistributionSites()
|
||||
const firstSite = sites.value[0]
|
||||
if (!siteId.value && firstSite) {
|
||||
siteId.value = firstSite.id
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPool() {
|
||||
if (!siteId.value) {
|
||||
qrcodes.value = []
|
||||
total.value = 0
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const [list, nextStats] = await Promise.all([
|
||||
fetchSiteQrCodes(siteId.value, {
|
||||
status: statusFilter.value || undefined,
|
||||
page: currentPage.value,
|
||||
limit: currentPageSize.value,
|
||||
}),
|
||||
fetchSiteQrCodeStats(siteId.value),
|
||||
])
|
||||
qrcodes.value = list.items
|
||||
total.value = list.total
|
||||
stats.value = nextStats
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSiteChange() {
|
||||
currentPage.value = 1
|
||||
loadPool()
|
||||
}
|
||||
|
||||
function handleFilter() {
|
||||
currentPage.value = 1
|
||||
loadPool()
|
||||
}
|
||||
|
||||
async function recognizeGroupName(file: File) {
|
||||
const { job_id } = await submitOCRJob(file)
|
||||
const delays = [1000, 2000, 3000, 3000, 3000]
|
||||
for (const delay of delays) {
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
const record = await getOCRJobResult(job_id)
|
||||
if (record.status === 'done' && record.result) return record.result.group_name || ''
|
||||
if (record.status === 'failed') throw new Error(record.error || 'OCR 识别失败')
|
||||
}
|
||||
throw new Error('OCR 识别超时')
|
||||
}
|
||||
|
||||
async function handleFileUpload(options: { file: File }) {
|
||||
if (!siteId.value) {
|
||||
ElMessage.warning('请先选择分站')
|
||||
return
|
||||
}
|
||||
if (uploadedImages.value.length >= maxBatchUploadCount) {
|
||||
ElMessage.warning(`单次最多添加 ${maxBatchUploadCount} 张`)
|
||||
return
|
||||
}
|
||||
if (!options.file.type.startsWith('image/')) {
|
||||
ElMessage.warning('只能上传图片')
|
||||
return
|
||||
}
|
||||
uploadPendingCount.value += 1
|
||||
const item: UploadedQrImage = {
|
||||
id: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
||||
url: '',
|
||||
previewUrl: URL.createObjectURL(options.file),
|
||||
file: options.file,
|
||||
groupName: '',
|
||||
}
|
||||
uploadedImages.value.push(item)
|
||||
try {
|
||||
const normalized = await optimizeImageForUpload(options.file, 'qrcode')
|
||||
const [uploaded, groupName] = await Promise.all([
|
||||
uploadAdminFile(normalized, 'qrcode', { optimize: false }),
|
||||
recognizeGroupName(normalized).catch(error => {
|
||||
ElMessage.warning(readError(error, '群名识别失败,可手动填写'))
|
||||
return ''
|
||||
}),
|
||||
])
|
||||
item.url = uploaded.url
|
||||
item.groupName = groupName
|
||||
} catch {
|
||||
removeImage(item.id)
|
||||
ElMessage.error('图片上传失败')
|
||||
} finally {
|
||||
uploadPendingCount.value = Math.max(0, uploadPendingCount.value - 1)
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage(id: string) {
|
||||
const index = uploadedImages.value.findIndex(item => item.id === id)
|
||||
if (index < 0) return
|
||||
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 = []
|
||||
uploadPendingCount.value = 0
|
||||
}
|
||||
|
||||
function openUpload() {
|
||||
if (!canManage.value || !siteId.value) return
|
||||
clearUploadedImages()
|
||||
uploadNote.value = ''
|
||||
uploadVisible.value = true
|
||||
}
|
||||
|
||||
async function submitUpload() {
|
||||
if (!siteId.value) return
|
||||
if (uploadPendingCount.value > 0) {
|
||||
ElMessage.warning('图片还在上传,请稍后再保存')
|
||||
return
|
||||
}
|
||||
const ready = uploadedImages.value.filter(item => item.url)
|
||||
if (ready.length === 0) {
|
||||
ElMessage.warning('请先上传二维码图片')
|
||||
return
|
||||
}
|
||||
uploadSaving.value = true
|
||||
try {
|
||||
const items: CreateQrCodePayload[] = ready.map(item => ({
|
||||
image_url: item.url,
|
||||
group_name: item.groupName,
|
||||
note: uploadNote.value,
|
||||
}))
|
||||
await batchCreateSiteQrCodes(siteId.value, items)
|
||||
ElMessage.success(`已放入${selectedSite.value?.site_name || '该分站'}码池 ${items.length} 张`)
|
||||
uploadVisible.value = false
|
||||
await loadPool()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '保存失败'))
|
||||
} finally {
|
||||
uploadSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDisable(row: ChatQrCode) {
|
||||
if (!siteId.value || !canManage.value || row.status === 'disabled') return
|
||||
await ElMessageBox.confirm('停用后这张码不会再发给该分站的新发布群。', '停用确认', {
|
||||
type: 'warning',
|
||||
})
|
||||
await updateSiteQrCode(siteId.value, row.id, { status: 'disabled' })
|
||||
ElMessage.success('已停用')
|
||||
await loadPool()
|
||||
}
|
||||
|
||||
async function handleDelete(row: ChatQrCode) {
|
||||
if (!siteId.value || !canManage.value) return
|
||||
await ElMessageBox.confirm('确定删除这张二维码?已发出的记录也会从池里移除。', '删除确认', {
|
||||
type: 'warning',
|
||||
})
|
||||
await deleteSiteQrCode(siteId.value, row.id)
|
||||
ElMessage.success('已删除')
|
||||
await loadPool()
|
||||
}
|
||||
|
||||
watch(uploadVisible, visible => {
|
||||
if (!visible) clearUploadedImages()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSites()
|
||||
await loadPool()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearUploadedImages()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="site-qr-page">
|
||||
<header class="page-head">
|
||||
<div>
|
||||
<h1>分站二维码池</h1>
|
||||
<p>每个分站只用自己的池。池空了会等主站补码,不会发主站或其他分站的二维码。</p>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="canManage"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
:disabled="!siteId"
|
||||
@click="openUpload"
|
||||
>
|
||||
上传到当前分站
|
||||
</el-button>
|
||||
</header>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-select
|
||||
v-model="siteId"
|
||||
filterable
|
||||
placeholder="选择分站"
|
||||
class="site-select"
|
||||
@change="handleSiteChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="site in sites"
|
||||
:key="site.id"
|
||||
:label="`${site.site_name}(${site.site_code})`"
|
||||
:value="site.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="statusFilter"
|
||||
placeholder="状态"
|
||||
clearable
|
||||
class="status-select"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in statusOptions"
|
||||
:key="option.label"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button :icon="Refresh" @click="loadPool">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div>
|
||||
<strong>{{ stats.unused_count }}</strong>
|
||||
<span>待用</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ stats.used_count }}</strong>
|
||||
<span>已发放</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ stats.disabled_count }}</strong>
|
||||
<span>已停用</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ stats.total_count }}</strong>
|
||||
<span>合计</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="qrcodes" empty-text="这个分站还没有二维码">
|
||||
<el-table-column label="二维码" width="96">
|
||||
<template #default="{ row }">
|
||||
<AuthImage
|
||||
:source="row.image_url"
|
||||
fit="contain"
|
||||
:image-style="{ width: '64px', height: '64px', background: '#f7f9fc' }"
|
||||
:preview-src-list="[row.image_url]"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="群名" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.group_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="
|
||||
(statusMap[row.status]?.type as 'success' | 'primary' | 'danger' | 'info') || 'info'
|
||||
"
|
||||
size="small"
|
||||
>
|
||||
{{ statusMap[row.status]?.label || row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定群" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{
|
||||
row.bound_conversation_title || (row.conversation_id ? `#${row.conversation_id}` : '-')
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" min-width="170">
|
||||
<template #default="{ row }">{{
|
||||
row.expires_at ? formatDateTime(row.expires_at) : '永久'
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.note || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="canManage" label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
:disabled="row.status === 'disabled'"
|
||||
@click="handleDisable(row)"
|
||||
>
|
||||
停用
|
||||
</el-button>
|
||||
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<AdminTablePagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="currentPageSize"
|
||||
:total="total"
|
||||
:loading="loading"
|
||||
@page-change="loadPool"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="uploadVisible" title="上传到当前分站" width="640px" destroy-on-close>
|
||||
<p class="upload-tip">
|
||||
这些码只发给 {{ selectedSite?.site_name || '当前分站' }} 的发布群。默认 7 天后过期。
|
||||
</p>
|
||||
<el-upload
|
||||
drag
|
||||
multiple
|
||||
accept="image/*"
|
||||
:show-file-list="false"
|
||||
:http-request="(opt: any) => handleFileUpload(opt)"
|
||||
:disabled="uploadSaving || uploadedImages.length >= maxBatchUploadCount"
|
||||
>
|
||||
<el-icon class="upload-icon"><UploadFilled /></el-icon>
|
||||
<div>拖入或点击上传企微群二维码</div>
|
||||
</el-upload>
|
||||
<div v-if="uploadedImages.length" class="upload-list">
|
||||
<div v-for="item in uploadedImages" :key="item.id" class="upload-item">
|
||||
<img :src="item.previewUrl" alt="" />
|
||||
<el-input v-model="item.groupName" placeholder="群名,识别失败可手填" />
|
||||
<el-button link type="danger" @click="removeImage(item.id)">移除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="uploadNote"
|
||||
class="upload-note"
|
||||
placeholder="备注,可选,会写到本批每一张"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="uploadVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="uploadSaving || uploadPendingCount > 0"
|
||||
@click="submitUpload"
|
||||
>
|
||||
放入该分站码池
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.site-qr-page {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.page-head h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.page-head p,
|
||||
.upload-tip {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.toolbar,
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site-select {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.status-select {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.stats div {
|
||||
min-width: 88px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #e8eef5;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.stats strong {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.stats span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
margin-bottom: 8px;
|
||||
font-size: 28px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.upload-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.upload-item img {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
object-fit: contain;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.upload-note {
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
DocumentCopy,
|
||||
Refresh,
|
||||
Clock,
|
||||
Picture,
|
||||
Upload,
|
||||
Coin,
|
||||
Message,
|
||||
Edit,
|
||||
@@ -33,8 +31,6 @@ import {
|
||||
type OnboardDistributionSitePayload,
|
||||
type OnboardDistributionSiteResult,
|
||||
} from '@/features/admin/api/adminDistribution'
|
||||
import { uploadAdminFile } from '@/shared/api/files'
|
||||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
import {
|
||||
@@ -110,15 +106,6 @@ const receiveRatioForm = reactive({
|
||||
receive_cost_delta: 0,
|
||||
})
|
||||
|
||||
const qrDialogVisible = ref(false)
|
||||
const qrSubmitting = ref(false)
|
||||
const qrUploading = ref(false)
|
||||
const qrForm = reactive({
|
||||
site_id: 0,
|
||||
site_name: '',
|
||||
listing_qrcode_url: '',
|
||||
})
|
||||
|
||||
const smsDialogVisible = ref(false)
|
||||
const smsLoading = ref(false)
|
||||
const smsSubmitting = ref(false)
|
||||
@@ -204,51 +191,6 @@ async function handleReceiveRatioSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenQrcode(row: DistributionSite) {
|
||||
qrForm.site_id = row.id
|
||||
qrForm.site_name = row.site_name
|
||||
qrForm.listing_qrcode_url = row.listing_qrcode_url || ''
|
||||
qrDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function uploadSiteQrcode(file: File) {
|
||||
qrUploading.value = true
|
||||
try {
|
||||
const uploaded = await uploadAdminFile(file, 'qrcode')
|
||||
qrForm.listing_qrcode_url = uploaded.url
|
||||
ElMessage.success('二维码已上传,保存后生效')
|
||||
} catch (err) {
|
||||
ElMessage.error(readError(err, '上传二维码失败'))
|
||||
} finally {
|
||||
qrUploading.value = false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function clearSiteQrcode() {
|
||||
qrForm.listing_qrcode_url = ''
|
||||
}
|
||||
|
||||
async function handleQrcodeSubmit() {
|
||||
qrSubmitting.value = true
|
||||
try {
|
||||
await updateDistributionSite(qrForm.site_id, {
|
||||
listing_qrcode_url: qrForm.listing_qrcode_url.trim(),
|
||||
})
|
||||
ElMessage.success(
|
||||
qrForm.listing_qrcode_url.trim()
|
||||
? `【${qrForm.site_name}】发布群固定二维码已更新`
|
||||
: `【${qrForm.site_name}】已清空发布群固定二维码`
|
||||
)
|
||||
qrDialogVisible.value = false
|
||||
await loadSites()
|
||||
} catch (err) {
|
||||
ElMessage.error(readError(err, '更新发布群二维码失败'))
|
||||
} finally {
|
||||
qrSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpenSMS(row: DistributionSite) {
|
||||
smsForm.site_id = row.id
|
||||
smsForm.site_name = row.site_name
|
||||
@@ -676,8 +618,8 @@ onMounted(() => {
|
||||
<div>
|
||||
<h2 class="page-title">分销站点管理</h2>
|
||||
<p class="page-subtitle">
|
||||
开通分销站、配置结算冷却期、收号比例上限与发布群固定二维码。利润按 35% 收号 / 40% 卖出 /
|
||||
25% 服务费切分。
|
||||
开通分销站、配置结算冷却期与收号比例上限。发布群进群码在「分站二维码池」按站点维护。利润按
|
||||
35% 收号 / 40% 卖出 / 25% 服务费切分。
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
@@ -811,18 +753,6 @@ onMounted(() => {
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布群二维码" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<AuthImage
|
||||
v-if="row.listing_qrcode_url"
|
||||
:source="row.listing_qrcode_url"
|
||||
fit="cover"
|
||||
:image-style="{ width: '48px', height: '48px', borderRadius: '6px' }"
|
||||
:preview-src-list="[row.listing_qrcode_url]"
|
||||
/>
|
||||
<el-tag v-else type="info" size="small">未配置</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="created_at" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
@@ -854,9 +784,6 @@ onMounted(() => {
|
||||
<el-dropdown-item :icon="Coin" @click="handleOpenReceiveRatio(row)">
|
||||
收号比例
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :icon="Picture" @click="handleOpenQrcode(row)">
|
||||
发布二维码
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :icon="Message" @click="handleOpenSMS(row)">
|
||||
短信配置
|
||||
</el-dropdown-item>
|
||||
@@ -1089,58 +1016,6 @@ onMounted(() => {
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 发布群固定二维码 -->
|
||||
<el-dialog v-model="qrDialogVisible" title="发布群固定二维码" width="520px" destroy-on-close>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="号主在该分站上传账号后,会把这张固定二维码发到对应发布群。同一分站可重复使用,不消耗主站二维码池。"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="分销站点">
|
||||
<el-input :model-value="qrForm.site_name" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="固定二维码">
|
||||
<div class="qr-row">
|
||||
<AuthImage
|
||||
v-if="qrForm.listing_qrcode_url"
|
||||
:source="qrForm.listing_qrcode_url"
|
||||
fit="contain"
|
||||
:image-style="{
|
||||
width: '160px',
|
||||
height: '160px',
|
||||
borderRadius: '12px',
|
||||
background: '#f7f9fc',
|
||||
}"
|
||||
:preview-src-list="[qrForm.listing_qrcode_url]"
|
||||
/>
|
||||
<div v-else class="qr-empty">尚未配置</div>
|
||||
<div class="qr-actions">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:disabled="qrUploading"
|
||||
:before-upload="uploadSiteQrcode"
|
||||
>
|
||||
<el-button :icon="Upload" :loading="qrUploading">上传二维码</el-button>
|
||||
</el-upload>
|
||||
<el-button :disabled="!qrForm.listing_qrcode_url" @click="clearSiteQrcode">
|
||||
清空
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="qrDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="qrSubmitting" @click="handleQrcodeSubmit">
|
||||
保存设置
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 短信配置对话框 -->
|
||||
<el-dialog
|
||||
v-model="smsDialogVisible"
|
||||
@@ -1457,29 +1332,4 @@ onMounted(() => {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.qr-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.qr-empty {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 12px;
|
||||
background: #f7f9fc;
|
||||
color: #9ca3af;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.qr-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user