更新
This commit is contained in:
@@ -58,22 +58,29 @@
|
||||
</div>
|
||||
<div class="section-heading-actions">
|
||||
<span v-if="selectedPoolIds.length" class="selection-count">已选 {{ selectedPoolIds.length }} 个方案</span>
|
||||
<el-button
|
||||
:icon="Refresh"
|
||||
:loading="syncingBatchMembers"
|
||||
:disabled="!selectedPoolIds.length || memberSyncBusy || togglingMemberId > 0 || deletingPoolId > 0"
|
||||
@click="syncSelectedMemberRanges"
|
||||
>批量同步成员范围</el-button>
|
||||
<el-button
|
||||
:icon="Edit"
|
||||
:disabled="!selectedPoolIds.length"
|
||||
:disabled="!selectedManageablePoolCount || memberSyncBusy"
|
||||
@click="openBatchConfigDialog()"
|
||||
>批量修改方案</el-button>
|
||||
<el-button
|
||||
:icon="User"
|
||||
:disabled="!selectedPoolIds.length"
|
||||
:disabled="!selectedManageablePoolCount || memberSyncBusy"
|
||||
@click="openAccessDialog()"
|
||||
>批量设置访问操作</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openPoolDialog()">新建分流方案</el-button>
|
||||
<el-button type="primary" :icon="Plus" :disabled="memberSyncBusy" @click="openPoolDialog()">新建分流方案</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="operationResults.length" class="member-sync-results" aria-live="polite">
|
||||
<strong>本次保存与企微同步结果</strong>
|
||||
<p v-if="syncingBatchMembers" role="status">正在同步企微 {{ batchSyncProgress.completed }} / {{ batchSyncProgress.total }} 个方案,请等待完成。</p>
|
||||
<ul>
|
||||
<li v-for="result in operationResults" :key="result.id">
|
||||
<span>{{ result.name || `方案 ${result.id}` }}:{{ operationResultText(result) }}</span>
|
||||
@@ -82,7 +89,7 @@
|
||||
type="primary"
|
||||
link
|
||||
:loading="syncingPoolId === result.id"
|
||||
:disabled="syncingPoolId > 0 || savingBatchConfig"
|
||||
:disabled="memberSyncBusy"
|
||||
@click="syncMemberRange(result.id)"
|
||||
>重试同步</el-button>
|
||||
</li>
|
||||
@@ -93,13 +100,13 @@
|
||||
<aside class="pool-sidebar">
|
||||
<div class="pool-select-all">
|
||||
<el-checkbox
|
||||
:model-value="allManageablePoolsSelected"
|
||||
:indeterminate="someManageablePoolsSelected"
|
||||
:disabled="!manageablePoolIds.length"
|
||||
aria-label="全选可管理的分流方案"
|
||||
:model-value="allSelectablePoolsSelected"
|
||||
:indeterminate="someSelectablePoolsSelected"
|
||||
:disabled="!selectablePoolIds.length || memberSyncBusy"
|
||||
aria-label="全选可操作的分流方案"
|
||||
@change="toggleAllPoolSelection"
|
||||
>全选</el-checkbox>
|
||||
<span>可选 {{ manageablePoolIds.length }} 个</span>
|
||||
<span>可选 {{ selectablePoolIds.length }} 个</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="pool in overview.pools"
|
||||
@@ -109,7 +116,7 @@
|
||||
>
|
||||
<el-checkbox
|
||||
:model-value="selectedPoolIds.includes(Number(pool.id))"
|
||||
:disabled="!pool.can_manage_access"
|
||||
:disabled="(!pool.can_manage_access && !pool.can_operate) || memberSyncBusy"
|
||||
:aria-label="`选择方案 ${pool.name}`"
|
||||
@change="(checked) => togglePoolSelection(pool, checked)"
|
||||
@click.stop
|
||||
@@ -143,12 +150,12 @@
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button :icon="CircleCheck" :loading="checkingApi" @click="checkApiPermission">验证 API</el-button>
|
||||
<el-button v-if="selectedPool.can_operate" :icon="Refresh" :loading="syncingPoolId === Number(selectedPool.id)" :disabled="!selectedPool.official_link || syncingPoolId > 0 || togglingMemberId > 0" @click="syncMemberRange(Number(selectedPool.id))">同步成员范围</el-button>
|
||||
<el-button v-if="selectedPool.can_operate" :icon="Refresh" :loading="syncingPoolId === Number(selectedPool.id)" :disabled="!selectedPool.official_link || memberSyncBusy || togglingMemberId > 0" @click="syncMemberRange(Number(selectedPool.id))">同步成员范围</el-button>
|
||||
<el-button :icon="DocumentCopy" :disabled="!selectedPool.main_url" @click="copyText(selectedPool.main_url, '官方获客链接')">复制链接</el-button>
|
||||
<el-button :icon="DocumentCopy" @click="copyText(selectedPool.install_code, 'JS 安装代码')">复制 JS</el-button>
|
||||
<el-button v-if="selectedPool.can_manage_access" :icon="User" @click="openAccessDialog([Number(selectedPool.id)])">设置访问操作</el-button>
|
||||
<el-button v-if="selectedPool.can_operate" :icon="Edit" @click="openPoolDialog(selectedPool)">编辑方案</el-button>
|
||||
<el-button v-if="selectedPool.can_delete" type="danger" plain :icon="Delete" :loading="deletingPoolId === Number(selectedPool.id)" :disabled="deletingPoolId > 0" @click="removePool(selectedPool)">删除</el-button>
|
||||
<el-button v-if="selectedPool.can_manage_access" :icon="User" :disabled="memberSyncBusy" @click="openAccessDialog([Number(selectedPool.id)])">设置访问操作</el-button>
|
||||
<el-button v-if="selectedPool.can_operate" :icon="Edit" :disabled="memberSyncBusy" @click="openPoolDialog(selectedPool)">编辑方案</el-button>
|
||||
<el-button v-if="selectedPool.can_delete" type="danger" plain :icon="Delete" :loading="deletingPoolId === Number(selectedPool.id)" :disabled="deletingPoolId > 0 || memberSyncBusy" @click="removePool(selectedPool)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -207,12 +214,12 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="上线" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch :model-value="Number(row.enabled) === 1" :disabled="!selectedPool.can_operate || togglingMemberId > 0 || syncingPoolId > 0" :loading="togglingMemberId === Number(row.id)" @change="(value) => handleMemberToggle(row, value)" />
|
||||
<el-switch :model-value="Number(row.enabled) === 1" :disabled="!selectedPool.can_operate || togglingMemberId > 0 || memberSyncBusy" :loading="togglingMemberId === Number(row.id)" @change="(value) => handleMemberToggle(row, value)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="selectedPool.can_operate" type="primary" link @click="openMemberDialog(row)">编辑规则</el-button>
|
||||
<el-button v-if="selectedPool.can_operate" type="primary" link :disabled="memberSyncBusy" @click="openMemberDialog(row)">编辑规则</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty :image-size="72" description="编辑方案并选择获客医助" /></template>
|
||||
@@ -808,8 +815,11 @@ const savingAccess = ref(false)
|
||||
const batchConfigDialogVisible = ref(false)
|
||||
const savingBatchConfig = ref(false)
|
||||
const syncingPoolId = ref(0)
|
||||
const syncingBatchMembers = ref(false)
|
||||
const memberSyncBusy = computed(() => syncingBatchMembers.value || syncingPoolId.value > 0 || savingBatchConfig.value)
|
||||
const batchSyncProgress = reactive({ completed: 0, total: 0 })
|
||||
const operationResults = ref<WecomPromotionBatchUpdatePoolResult[]>([])
|
||||
type MemberOperationResult = WecomPromotionBatchUpdatePoolResult & { sync_only?: boolean }
|
||||
const operationResults = ref<MemberOperationResult[]>([])
|
||||
const batchConfigBusy = ref(false)
|
||||
const batchConfigScroll = ref<HTMLElement>()
|
||||
const batchConfigError = ref('')
|
||||
@@ -864,14 +874,18 @@ const accessDialogPools = computed(() => overview.pools.filter((item: any) => ac
|
||||
const manageablePoolIds = computed(() => overview.pools
|
||||
.filter((pool: any) => pool.can_manage_access)
|
||||
.map((pool: any) => Number(pool.id)))
|
||||
const selectablePoolIds = computed(() => overview.pools
|
||||
.filter((pool: any) => pool.can_manage_access || pool.can_operate)
|
||||
.map((pool: any) => Number(pool.id)))
|
||||
const selectedManageablePoolCount = computed(() => {
|
||||
const selected = new Set(selectedPoolIds.value)
|
||||
return manageablePoolIds.value.filter((id) => selected.has(id)).length
|
||||
})
|
||||
const allManageablePoolsSelected = computed(() => manageablePoolIds.value.length > 0
|
||||
&& selectedManageablePoolCount.value === manageablePoolIds.value.length)
|
||||
const someManageablePoolsSelected = computed(() => selectedManageablePoolCount.value > 0
|
||||
&& selectedManageablePoolCount.value < manageablePoolIds.value.length)
|
||||
const selectedSelectablePoolCount = computed(() => selectablePoolIds.value.filter((id) => selectedPoolIds.value.includes(id)).length)
|
||||
const allSelectablePoolsSelected = computed(() => selectablePoolIds.value.length > 0
|
||||
&& selectedSelectablePoolCount.value === selectablePoolIds.value.length)
|
||||
const someSelectablePoolsSelected = computed(() => selectedSelectablePoolCount.value > 0
|
||||
&& selectedSelectablePoolCount.value < selectablePoolIds.value.length)
|
||||
const batchConfigPools = computed(() => overview.pools.filter((item: any) => batchConfigForm.pool_ids.includes(Number(item.id))))
|
||||
const batchMemberStatusOptions = computed<BatchMemberStatusOption[]>(() => {
|
||||
const memberOptionById = new Map(overview.member_options.map((member) => [Number(member.id), member]))
|
||||
@@ -969,14 +983,12 @@ const customerMemberOptions = computed(() => customerStats.member_options.length
|
||||
department_name: Array.isArray(item.dept_names) ? item.dept_names.join(' / ') : (item.department_name || item.dept_name || '')
|
||||
})))
|
||||
|
||||
watch(() => overview.pools.map((item: any) => `${Number(item.id)}:${item.can_manage_access ? 1 : 0}`).join(','), () => {
|
||||
watch(() => overview.pools.map((item: any) => `${Number(item.id)}:${item.can_manage_access ? 1 : 0}:${item.can_operate ? 1 : 0}`).join(','), () => {
|
||||
const ids = overview.pools.map((item: any) => Number(item.id))
|
||||
const manageableIds = new Set(overview.pools
|
||||
.filter((item: any) => item.can_manage_access)
|
||||
.map((item: any) => Number(item.id)))
|
||||
const selectableIds = new Set(selectablePoolIds.value)
|
||||
if (!selectedPoolId.value || !ids.includes(selectedPoolId.value)) selectedPoolId.value = ids[0]
|
||||
if (!selectedInstallPoolId.value || !ids.includes(selectedInstallPoolId.value)) selectedInstallPoolId.value = ids[0]
|
||||
selectedPoolIds.value = selectedPoolIds.value.filter((id) => manageableIds.has(id))
|
||||
selectedPoolIds.value = selectedPoolIds.value.filter((id) => selectableIds.has(id))
|
||||
}, { immediate: true })
|
||||
|
||||
watch(activeTab, (tab) => {
|
||||
@@ -1004,7 +1016,7 @@ async function loadOverview() {
|
||||
}
|
||||
|
||||
function togglePoolSelection(pool: any, checked: unknown) {
|
||||
if (!pool?.can_manage_access) return
|
||||
if (memberSyncBusy.value || (!pool?.can_manage_access && !pool?.can_operate)) return
|
||||
const id = Number(pool.id)
|
||||
const next = new Set(selectedPoolIds.value)
|
||||
checked ? next.add(id) : next.delete(id)
|
||||
@@ -1012,10 +1024,12 @@ function togglePoolSelection(pool: any, checked: unknown) {
|
||||
}
|
||||
|
||||
function toggleAllPoolSelection(checked: unknown) {
|
||||
selectedPoolIds.value = checked ? [...manageablePoolIds.value] : []
|
||||
if (memberSyncBusy.value) return
|
||||
selectedPoolIds.value = checked ? [...selectablePoolIds.value] : []
|
||||
}
|
||||
|
||||
function openBatchConfigDialog(poolIds: number[] = selectedPoolIds.value) {
|
||||
if (memberSyncBusy.value) return
|
||||
const manageableIds = new Set(overview.pools
|
||||
.filter((pool: any) => pool.can_manage_access)
|
||||
.map((pool: any) => Number(pool.id)))
|
||||
@@ -1068,16 +1082,17 @@ function syncErrorMessage(error: unknown, fallback: string) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
function operationResultText(result: WecomPromotionBatchUpdatePoolResult) {
|
||||
function operationResultText(result: MemberOperationResult) {
|
||||
if (!result.success) return `本地保存失败:${result.error || '请检查配置后重试'}`
|
||||
if (result.sync_status === 'synced') return '本地已保存,企微成员范围已确认同步'
|
||||
const prefix = result.sync_only ? '' : '本地已保存,'
|
||||
if (result.sync_status === 'synced') return `${prefix}企微成员范围已确认同步`
|
||||
if (result.sync_status === 'blocked' || result.sync_status === 'failed' || result.sync_error) {
|
||||
return `本地已保存,企微同步${result.sync_status === 'blocked' ? '受阻' : '失败'}:${result.sync_error || '请重试同步'}`
|
||||
return `${prefix}企微同步${result.sync_status === 'blocked' ? '受阻' : '失败'}:${result.sync_error || '请重试同步'}`
|
||||
}
|
||||
return '本地已保存,企微成员范围尚未确认同步,请重试同步'
|
||||
return `${prefix}企微成员范围尚未确认同步,请重试同步`
|
||||
}
|
||||
|
||||
function recordOperationResult(result: WecomPromotionBatchUpdatePoolResult) {
|
||||
function recordOperationResult(result: MemberOperationResult) {
|
||||
const index = operationResults.value.findIndex((item) => item.id === result.id)
|
||||
if (index < 0) operationResults.value.push(result)
|
||||
else operationResults.value[index] = result
|
||||
@@ -1110,20 +1125,20 @@ async function requestMemberSync(poolId: number): Promise<Partial<WecomPromotion
|
||||
}
|
||||
|
||||
async function syncMemberRange(poolId: number) {
|
||||
if (syncingPoolId.value || savingBatchConfig.value || !canSyncPool(poolId)) return
|
||||
if (memberSyncBusy.value || togglingMemberId.value || !canSyncPool(poolId)) return
|
||||
const pool = overview.pools.find((item: any) => Number(item.id) === poolId)
|
||||
syncingPoolId.value = poolId
|
||||
try {
|
||||
const result = await requestMemberSync(poolId)
|
||||
recordOperationResult({ id: poolId, name: pool.name, success: true, ...result })
|
||||
recordOperationResult({ id: poolId, name: pool.name, success: true, sync_only: true, ...result })
|
||||
await loadOverview()
|
||||
notifySavedResult('本地规则已保存', result)
|
||||
notifySavedResult('成员范围同步结果', result)
|
||||
} finally {
|
||||
syncingPoolId.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
async function syncBatchResults(results: WecomPromotionBatchUpdatePoolResult[]) {
|
||||
async function syncBatchResults(results: MemberOperationResult[]) {
|
||||
operationResults.value = results.map((item) => ({ ...item }))
|
||||
const queued = results.filter((item) => item.success && item.sync_queued)
|
||||
Object.assign(batchSyncProgress, { total: queued.length, completed: 0 })
|
||||
@@ -1139,8 +1154,41 @@ async function syncBatchResults(results: WecomPromotionBatchUpdatePoolResult[])
|
||||
}))
|
||||
}
|
||||
|
||||
async function syncSelectedMemberRanges() {
|
||||
if (memberSyncBusy.value || togglingMemberId.value || deletingPoolId.value
|
||||
|| savingPool.value || savingMember.value || savingAccess.value) return
|
||||
const ids = [...new Set(selectedPoolIds.value.map(Number))]
|
||||
if (!ids.length) return ElMessage.warning('请先勾选需要同步的分流方案')
|
||||
if (ids.length > 100) return ElMessage.warning('单次最多同步 100 个分流方案,请分批选择')
|
||||
const results: MemberOperationResult[] = ids.map((id) => {
|
||||
const pool = overview.pools.find((item: any) => Number(item.id) === id)
|
||||
const eligible = canSyncPool(id)
|
||||
return {
|
||||
id, name: pool?.name || `方案 ${id}`, success: true, sync_only: true,
|
||||
sync_status: eligible ? 'pending' : 'blocked', sync_queued: eligible,
|
||||
sync_error: eligible ? '' : !pool?.can_operate ? '没有该方案的操作权限' : '方案尚未生成官方链接,请先保存方案'
|
||||
}
|
||||
})
|
||||
syncingBatchMembers.value = true
|
||||
try {
|
||||
await syncBatchResults(results)
|
||||
selectedPoolIds.value = operationResults.value.filter((item) => item.sync_status !== 'synced').map((item) => item.id)
|
||||
await loadOverview()
|
||||
const synced = operationResults.value.filter((item) => item.sync_status === 'synced').length
|
||||
const unconfirmed = ids.length - synced
|
||||
ElMessage({
|
||||
type: unconfirmed ? 'warning' : 'success',
|
||||
message: `企微已确认同步 ${synced} 个方案${unconfirmed ? `,${unconfirmed} 个尚未确认同步,已保留勾选` : ''}。详情见方案列表上方。`,
|
||||
duration: 8000
|
||||
})
|
||||
} finally {
|
||||
syncingBatchMembers.value = false
|
||||
Object.assign(batchSyncProgress, { completed: 0, total: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBatchConfig() {
|
||||
if (savingBatchConfig.value || batchConfigBusy.value) return
|
||||
if (memberSyncBusy.value || batchConfigBusy.value) return
|
||||
batchConfigError.value = ''
|
||||
const hasAutomationChange = overview.automation_installed
|
||||
&& (batchConfigApply.reception || batchConfigApply.customer || batchConfigApply.welcome)
|
||||
@@ -1248,6 +1296,7 @@ async function saveBatchConfig() {
|
||||
}
|
||||
|
||||
function openAccessDialog(poolIds: number[] = selectedPoolIds.value) {
|
||||
if (memberSyncBusy.value) return
|
||||
const manageableIds = new Set(overview.pools
|
||||
.filter((pool: any) => pool.can_manage_access)
|
||||
.map((pool: any) => Number(pool.id)))
|
||||
@@ -1309,6 +1358,7 @@ function accessSummary(pool: any) {
|
||||
}
|
||||
|
||||
function openPoolDialog(pool?: any) {
|
||||
if (memberSyncBusy.value) return
|
||||
const selectableMemberIds = new Set(overview.member_options.map((member) => Number(member.id)))
|
||||
poolFormError.value = ''
|
||||
automationBusy.value = false
|
||||
@@ -1337,7 +1387,7 @@ function openPoolDialog(pool?: any) {
|
||||
}
|
||||
|
||||
async function savePool() {
|
||||
if (savingPool.value || automationBusy.value) return
|
||||
if (savingPool.value || automationBusy.value || memberSyncBusy.value) return
|
||||
poolFormError.value = ''
|
||||
const automation = cloneAutomationConfig(poolForm.automation_config)
|
||||
// Disabled sections do not submit unfinished draft rows or draft attachments.
|
||||
@@ -1380,6 +1430,7 @@ async function savePool() {
|
||||
}
|
||||
|
||||
async function removePool(pool: any) {
|
||||
if (memberSyncBusy.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`删除“${pool.name}”将同时永久删除企业微信后台中的官方获客链接,已投放的链接会失效且无法恢复。确认继续?`,
|
||||
@@ -1402,6 +1453,7 @@ async function removePool(pool: any) {
|
||||
}
|
||||
|
||||
function openMemberDialog(row: any) {
|
||||
if (memberSyncBusy.value) return
|
||||
const range = Number(row.active_start) > 0 && Number(row.active_end) > 0
|
||||
? [formatPickerTime(row.active_start), formatPickerTime(row.active_end)]
|
||||
: []
|
||||
@@ -1418,7 +1470,7 @@ function openMemberDialog(row: any) {
|
||||
}
|
||||
|
||||
async function saveMemberRule() {
|
||||
if (savingMember.value) return
|
||||
if (savingMember.value || memberSyncBusy.value) return
|
||||
savingMember.value = true
|
||||
const poolId = Number(selectedPool.value?.id)
|
||||
const poolName = String(selectedPool.value?.name || '')
|
||||
@@ -1440,7 +1492,7 @@ async function saveMemberRule() {
|
||||
}
|
||||
|
||||
async function handleMemberToggle(row: any, value: unknown) {
|
||||
if (togglingMemberId.value || syncingPoolId.value) return
|
||||
if (togglingMemberId.value || memberSyncBusy.value) return
|
||||
togglingMemberId.value = Number(row.id)
|
||||
const poolId = Number(selectedPool.value?.id)
|
||||
const poolName = String(selectedPool.value?.name || '')
|
||||
|
||||
Reference in New Issue
Block a user