更新
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 || '')
|
||||
|
||||
@@ -152,3 +152,211 @@ test('batch save retains partial failures and never reports queued work as remot
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
assert.match(state.operationResults.value[1].sync_error, /企微请求超时/)
|
||||
})
|
||||
|
||||
test('direct batch sync snapshots and deduplicates selection, limits concurrency, and waits for refresh', async () => {
|
||||
const pools = [pool(1), pool(2), pool(3), pool(4)]
|
||||
const pending = []
|
||||
const unrelatedCalls = []
|
||||
let active = 0
|
||||
let maximum = 0
|
||||
let refreshCount = 0
|
||||
let finishRefresh
|
||||
const api = Object.fromEntries([
|
||||
'wecomPromotionBatchUpdatePools', 'wecomPromotionSavePool', 'wecomPromotionSaveMember',
|
||||
'wecomPromotionBatchSetOperators', 'wecomPromotionSyncCustomers', 'wecomPromotionToggleMember'
|
||||
].map((name) => [name, async (payload) => { unrelatedCalls.push({ name, payload }); return {} }]))
|
||||
const { state, messages } = page({
|
||||
...api,
|
||||
wecomPromotionOverview: () => {
|
||||
refreshCount++
|
||||
return new Promise((resolve) => { finishRefresh = () => resolve({ pools }) })
|
||||
},
|
||||
wecomPromotionSyncMemberRange: ({ pool_id }) => new Promise((resolve) => {
|
||||
active++
|
||||
maximum = Math.max(maximum, active)
|
||||
pending.push({ id: pool_id, finish: () => {
|
||||
active--
|
||||
resolve({ pool_id, sync_status: 'synced', sync_error: '', sync_queued: false })
|
||||
} })
|
||||
})
|
||||
}, pools)
|
||||
state.selectedPoolIds.value = [1, 1, 2, 3]
|
||||
let completed = false
|
||||
const syncing = state.syncSelectedMemberRanges().then(() => { completed = true })
|
||||
assert.equal(state.syncingBatchMembers.value, true)
|
||||
assert.deepEqual(pending.map((item) => item.id), [1, 2])
|
||||
assert.deepEqual({ ...state.batchSyncProgress }, { completed: 0, total: 3 })
|
||||
|
||||
// A changed ref must not alter the already-dispatched batch's work list.
|
||||
state.selectedPoolIds.value = [4]
|
||||
pending[0].finish()
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
assert.deepEqual(pending.map((item) => item.id), [1, 2, 3])
|
||||
assert.equal(state.batchSyncProgress.completed, 1)
|
||||
assert.equal(refreshCount, 0)
|
||||
pending[1].finish()
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
assert.equal(state.batchSyncProgress.completed, 2)
|
||||
assert.equal(completed, false)
|
||||
assert.equal(refreshCount, 0)
|
||||
pending[2].finish()
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
assert.equal(maximum, 2)
|
||||
assert.equal(refreshCount, 1)
|
||||
assert.equal(state.syncingBatchMembers.value, true)
|
||||
assert.equal(state.batchSyncProgress.completed, 3)
|
||||
assert.equal(completed, false)
|
||||
finishRefresh()
|
||||
await syncing
|
||||
|
||||
assert.equal(state.syncingBatchMembers.value, false)
|
||||
assert.deepEqual({ ...state.batchSyncProgress }, { completed: 0, total: 0 })
|
||||
assert.deepEqual(state.selectedPoolIds.value, [])
|
||||
assert.deepEqual(state.operationResults.value.map((item) => item.id), [1, 2, 3])
|
||||
assert.ok(state.operationResults.value.every((item) => item.sync_only && item.sync_status === 'synced'))
|
||||
assert.deepEqual(unrelatedCalls, [])
|
||||
assert.equal(messages.at(-1).type, 'success')
|
||||
assert.match(messages.at(-1).message, /已确认同步 3 个/)
|
||||
assert.doesNotMatch(messages.at(-1).message, /本地.*保存/)
|
||||
for (const result of state.operationResults.value) {
|
||||
assert.match(state.operationResultText(result), /已确认同步/)
|
||||
assert.doesNotMatch(state.operationResultText(result), /本地.*保存/)
|
||||
}
|
||||
})
|
||||
|
||||
test('direct batch sync keeps failed, pending and blocked pools selected with precise results', async () => {
|
||||
const pools = [pool(1), pool(2), pool(3), { ...pool(4), official_link: null }, { ...pool(5), can_operate: false }]
|
||||
const calls = []
|
||||
let refreshCount = 0
|
||||
const { state, messages } = page({
|
||||
wecomPromotionOverview: async () => { refreshCount++; return { pools } },
|
||||
wecomPromotionSyncMemberRange: async ({ pool_id }) => {
|
||||
calls.push(pool_id)
|
||||
if (pool_id === 2) throw new Error('企微请求超时,请检查网络')
|
||||
return { pool_id, sync_status: pool_id === 1 ? 'synced' : 'pending', sync_queued: pool_id === 3, sync_error: '' }
|
||||
}
|
||||
}, pools)
|
||||
state.selectedPoolIds.value = [1, 2, 3, 4, 5]
|
||||
await state.syncSelectedMemberRanges()
|
||||
|
||||
assert.deepEqual(calls, [1, 2, 3])
|
||||
assert.equal(refreshCount, 1)
|
||||
assert.deepEqual(state.selectedPoolIds.value, [2, 3, 4, 5])
|
||||
assert.deepEqual(state.operationResults.value.map((item) => item.sync_status), ['synced', 'failed', 'pending', 'blocked', 'blocked'])
|
||||
assert.ok(state.operationResults.value.every((item) => item.sync_only))
|
||||
assert.match(state.operationResults.value[1].sync_error, /企微请求超时,请检查网络/)
|
||||
assert.equal(state.operationResults.value[2].sync_queued, true)
|
||||
assert.match(state.operationResultText(state.operationResults.value[3]), /链接/)
|
||||
assert.match(state.operationResultText(state.operationResults.value[4]), /权限|无权/)
|
||||
for (const result of state.operationResults.value) assert.doesNotMatch(state.operationResultText(result), /本地.*保存/)
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
assert.match(messages.at(-1).message, /已确认同步 1 个/)
|
||||
assert.match(messages.at(-1).message, /4 个尚未确认同步/)
|
||||
assert.doesNotMatch(messages.at(-1).message, /本地.*保存/)
|
||||
assert.equal(state.syncingBatchMembers.value, false)
|
||||
assert.deepEqual({ ...state.batchSyncProgress }, { completed: 0, total: 0 })
|
||||
})
|
||||
|
||||
test('direct batch sync prevents duplicate requests and freezes member and selection changes while busy', async () => {
|
||||
const pools = [pool(1), pool(2)]
|
||||
const calls = []
|
||||
const toggles = []
|
||||
let finish
|
||||
const { state } = page({
|
||||
wecomPromotionSyncMemberRange: ({ pool_id }) => {
|
||||
calls.push(pool_id)
|
||||
return new Promise((resolve) => { finish = () => resolve({ pool_id, sync_status: 'synced' }) })
|
||||
},
|
||||
wecomPromotionToggleMember: async (payload) => { toggles.push(payload); return { sync_status: 'synced' } }
|
||||
}, pools)
|
||||
state.selectedPoolIds.value = [1]
|
||||
const syncing = state.syncSelectedMemberRanges()
|
||||
await state.syncSelectedMemberRanges()
|
||||
await state.syncMemberRange(2)
|
||||
await state.handleMemberToggle(member(2), false)
|
||||
state.togglePoolSelection(pools[0], false)
|
||||
state.togglePoolSelection(pools[1], true)
|
||||
assert.deepEqual(state.selectedPoolIds.value, [1])
|
||||
state.toggleAllPoolSelection(false)
|
||||
assert.deepEqual(state.selectedPoolIds.value, [1])
|
||||
state.toggleAllPoolSelection(true)
|
||||
assert.deepEqual(state.selectedPoolIds.value, [1])
|
||||
assert.deepEqual(calls, [1])
|
||||
assert.deepEqual(toggles, [])
|
||||
finish()
|
||||
await syncing
|
||||
assert.equal(state.syncingBatchMembers.value, false)
|
||||
})
|
||||
|
||||
test('direct batch sync cannot begin while a single sync, member toggle or batch save is active', async () => {
|
||||
const calls = []
|
||||
const { state } = page({ wecomPromotionSyncMemberRange: async (payload) => { calls.push(payload); return { sync_status: 'synced' } } })
|
||||
state.selectedPoolIds.value = [1]
|
||||
for (const [busy, value] of [[state.syncingPoolId, 1], [state.togglingMemberId, 2], [state.savingBatchConfig, true]]) {
|
||||
busy.value = value
|
||||
await state.syncSelectedMemberRanges()
|
||||
assert.deepEqual(calls, [])
|
||||
assert.equal(state.syncingBatchMembers.value, false)
|
||||
busy.value = typeof value === 'boolean' ? false : 0
|
||||
}
|
||||
})
|
||||
|
||||
test('direct batch sync rejects empty and oversized selections but accepts 100 unique pools', async () => {
|
||||
const pools = Array.from({ length: 101 }, (_, index) => pool(index + 1))
|
||||
const calls = []
|
||||
const { state, messages } = page({ wecomPromotionSyncMemberRange: async ({ pool_id }) => {
|
||||
calls.push(pool_id)
|
||||
return { pool_id, sync_status: 'synced' }
|
||||
} }, pools)
|
||||
await state.syncSelectedMemberRanges()
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
assert.match(messages.at(-1).message, /选择|勾选/)
|
||||
assert.deepEqual(calls, [])
|
||||
state.selectedPoolIds.value = pools.map((item) => item.id)
|
||||
await state.syncSelectedMemberRanges()
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
assert.match(messages.at(-1).message, /100/)
|
||||
assert.deepEqual(calls, [])
|
||||
assert.equal(state.syncingBatchMembers.value, false)
|
||||
state.selectedPoolIds.value = [...pools.slice(0, 100).map((item) => item.id), 1]
|
||||
await state.syncSelectedMemberRanges()
|
||||
assert.deepEqual(calls, pools.slice(0, 100).map((item) => item.id))
|
||||
assert.equal(messages.at(-1).type, 'success')
|
||||
})
|
||||
|
||||
test('shared operators can select and sync pools while access and configuration stay manager-only', async () => {
|
||||
const pools = [
|
||||
{ ...pool(1), can_manage_access: false },
|
||||
{ ...pool(2), can_operate: false },
|
||||
{ ...pool(3), can_operate: false, can_manage_access: false },
|
||||
pool(4)
|
||||
]
|
||||
const calls = []
|
||||
const { state, messages } = page({ wecomPromotionSyncMemberRange: async ({ pool_id }) => {
|
||||
calls.push(pool_id)
|
||||
return { pool_id, sync_status: 'synced' }
|
||||
} }, pools)
|
||||
assert.deepEqual(state.selectablePoolIds.value, [1, 2, 4])
|
||||
state.togglePoolSelection(pools[0], true)
|
||||
state.togglePoolSelection(pools[2], true)
|
||||
await vue.nextTick()
|
||||
assert.deepEqual(state.selectedPoolIds.value, [1])
|
||||
state.openAccessDialog()
|
||||
assert.equal(state.accessDialogVisible.value, false)
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
state.openBatchConfigDialog()
|
||||
assert.equal(state.batchConfigDialogVisible.value, false)
|
||||
assert.equal(messages.at(-1).type, 'warning')
|
||||
await state.syncSelectedMemberRanges()
|
||||
assert.deepEqual(calls, [1])
|
||||
|
||||
state.toggleAllPoolSelection(true)
|
||||
assert.deepEqual(state.selectedPoolIds.value, [1, 2, 4])
|
||||
state.openAccessDialog()
|
||||
assert.deepEqual(state.accessForm.pool_ids, [2, 4])
|
||||
state.openBatchConfigDialog()
|
||||
assert.deepEqual(state.batchConfigForm.pool_ids, [2, 4])
|
||||
state.overview.pools[0].can_operate = false
|
||||
await vue.nextTick()
|
||||
assert.deepEqual(state.selectedPoolIds.value, [2, 4])
|
||||
})
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -20,6 +20,12 @@
|
||||
|
||||
部署后,对已经出现“待移出”的方案点击“同步成员范围”;只有显示“企微成员范围已确认同步”,才能认为当前计划已经由企业微信回读确认。批量重新保存也会主动同步各方案。后台分钟任务仍用于上限、有效期和失败补偿。
|
||||
|
||||
## 追加:直接批量同步成员范围
|
||||
|
||||
列表上方新增“批量同步成员范围”。左侧勾选或全选方案后可直接同步当前配置,无需再次保存方案。单次最多 100 个方案,最多 2 个请求同时执行;展示逐项结果及完成进度。同步未确认或受阻的方案保留勾选,可再次批量同步或单项重试。共享操作人可以勾选有操作权限的方案,访问授权和批量配置仍沿用管理权限。
|
||||
|
||||
新增 6 项回归,前端合计 13 项通过(包括实际 SFC、模板和 SCSS 编译),详见 [批量同步回归记录](batch-sync-tests.md)。浏览器用三个虚构方案验证批量同步,两项成功、一项模拟失败时,结果准确且仅保留失败方案勾选。全量类型检查已再次执行,仍因其他模块错误未通过;本次获客助手页面及 API 无类型报错。此次追加未重新执行全量生产构建,未部署或调用真实企微。
|
||||
|
||||
## 验证记录
|
||||
|
||||
见 [前端验证](frontend.md) 与 [隔离数据库验证](backend-tests.md)。所有测试使用虚构数据,未向真实企业微信发送请求。
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# 批量同步成员范围回归验证
|
||||
|
||||
验证日期:2026-09-09。
|
||||
|
||||
修改文件:`admin/tests/wecom-promotion-member-sync.test.cjs`。新增 6 个用例,保留原有 7 个用例。
|
||||
|
||||
## 覆盖行为
|
||||
|
||||
- 批量操作固定启动时的方案列表,去重后同步;同一时间最多 2 个请求。
|
||||
- 每个请求完成后更新进度,全部请求完成才刷新概览;概览刷新完成后才清理忙碌标记和进度。
|
||||
- 批量同步只调用成员范围同步接口,不调用方案配置、成员规则、操作权限、客户同步或成员上下线接口。
|
||||
- 完全成功时清空勾选并提示确认成功;结果使用 `sync_only`,不会声称本次进行了本地保存。
|
||||
- 混合同步成功、请求超时、待确认、缺少官方链接和无操作权限时,逐项保留准确结果;不对受阻方案发起请求,保留未确认方案勾选,汇总为警告。
|
||||
- 批量运行期间阻止重复批量同步、单方案同步和成员切换;勾选及全选操作无效。
|
||||
- 单方案同步、成员切换或批量配置保存期间不能启动批量同步。
|
||||
- 空选择和超过 100 个唯一方案时警告且不发请求;100 个唯一方案加重复 ID 可以正常完成。
|
||||
- 共享操作人能够勾选并同步有操作权限的方案;访问设置和批量配置仍仅接受有管理权限的方案;权限变化后清理不可选项。
|
||||
|
||||
## 执行结果
|
||||
|
||||
在 `D:/web/zyt/admin` 执行:
|
||||
|
||||
```text
|
||||
node --test tests/wecom-promotion-member-sync.test.cjs
|
||||
tests 13
|
||||
pass 13
|
||||
fail 0
|
||||
cancelled 0
|
||||
skipped 0
|
||||
```
|
||||
|
||||
首次运行 12 个通过,1 个因测试对“选择/勾选”的文案要求过窄而失败;放宽该同义表达断言后全部通过,无需修改实现。
|
||||
|
||||
测试直接编译实际页面的 Vue SFC 脚本、模板和 SCSS,并以模拟 API 执行业务方法。未连接真实网络、数据库或企业微信;页面视觉和浏览器交互由主任务另外验证。
|
||||
@@ -0,0 +1,109 @@
|
||||
src/components/chat-dialog/ChatMessageItem.vue(29,7): error TS7022: 'attrs' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
|
||||
src/components/editor/index.vue(29,44): error TS7016: Could not find a declaration file for module '@wangeditor/editor-for-vue'. 'D:/web/zyt/admin/node_modules/.pnpm/@wangeditor+editor-for-vue@_d49ef1161b4f4b880c450fdbfe3a0001/node_modules/@wangeditor/editor-for-vue/dist/index.esm.js' implicitly has an 'any' type.
|
||||
There are types at 'D:/web/zyt/admin/node_modules/@wangeditor/editor-for-vue/dist/src/index.d.ts', but this result could not be resolved when respecting package.json "exports". The '@wangeditor/editor-for-vue' library may need to update its package.json or typings.
|
||||
src/utils/call-local-recorder.ts(335,26): error TS2339: Property 'captureStream' does not exist on type 'HTMLVideoElement'.
|
||||
src/views/asset/user/index.vue(230,20): error TS2339: Property 'remark' does not exist on type 'never'.
|
||||
src/views/consumer/prescription/index.vue(1150,42): error TS2322: Type '(value: string[]) => void' is not assignable to type '(value: CascaderValue | null | undefined) => any'.
|
||||
Types of parameters 'value' and 'value' are incompatible.
|
||||
Type 'CascaderValue | null | undefined' is not assignable to type 'string[]'.
|
||||
Type 'undefined' is not assignable to type 'string[]'.
|
||||
src/views/consumer/prescription/index.vue(1952,9): error TS2322: Type '{ name: any; code: any; children: any; }[]' is not assignable to type 'never[]'.
|
||||
Type '{ name: any; code: any; children: any; }' is not assignable to type 'never'.
|
||||
src/views/consumer/prescription/index.vue(1958,13): error TS2322: Type '{ name: string; children: { name: string; children: { name: string; }[]; }[]; }' is not assignable to type 'never'.
|
||||
src/views/consumer/prescription/index.vue(1986,13): error TS2322: Type '{ name: string; children: { name: string; children: { name: string; }[]; }[]; }' is not assignable to type 'never'.
|
||||
src/views/consumer/prescription/index.vue(2001,13): error TS2322: Type '{ name: string; children: { name: string; children: { name: string; }[]; }[]; }' is not assignable to type 'never'.
|
||||
src/views/consumer/prescription/index.vue(2016,13): error TS2322: Type '{ name: string; children: { name: string; children: { name: string; }[]; }[]; }' is not assignable to type 'never'.
|
||||
src/views/consumer/prescription/index.vue(2256,32): error TS2304: Cannot find name 'searchPatientsAPI'.
|
||||
src/views/consumer/prescription/order_list.vue(253,26): error TS2322: Type 'unknown[]' is not assignable to type 'TreeNodeData[]'.
|
||||
Type 'unknown' is not assignable to type 'TreeNodeData'.
|
||||
src/views/consumer/prescription/order_list.vue(983,45): error TS2322: Type 'string | number | undefined' is not assignable to type 'number | null | undefined'.
|
||||
Type 'string' is not assignable to type 'number'.
|
||||
src/views/consumer/prescription/order_list.vue(1181,41): error TS2322: Type 'string | number | undefined' is not assignable to type 'number | null | undefined'.
|
||||
Type 'string' is not assignable to type 'number'.
|
||||
src/views/consumer/prescription/order_list.vue(1939,68): error TS7006: Parameter 'r' implicitly has an 'any' type.
|
||||
src/views/consumer/prescription/order_list.vue(2379,9): error TS2322: Type '{ value: any; label: any; code: any; children: any; }[]' is not assignable to type 'never[]'.
|
||||
Type '{ value: any; label: any; code: any; children: any; }' is not assignable to type 'never'.
|
||||
src/views/consumer/prescription/order_list.vue(3775,13): error TS2322: Type 'string | number | undefined' is not assignable to type 'number | undefined'.
|
||||
Type 'string' is not assignable to type 'number'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(271,26): error TS2322: Type 'unknown[]' is not assignable to type 'TreeNodeData[]'.
|
||||
Type 'unknown' is not assignable to type 'TreeNodeData'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(566,47): error TS2345: Argument of type 'Record<string, any>' is not assignable to parameter of type '{ id: number; }'.
|
||||
Property 'id' is missing in type 'Record<string, any>' but required in type '{ id: number; }'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(576,58): error TS2345: Argument of type 'Record<string, any>' is not assignable to parameter of type '{ id: number; }'.
|
||||
Property 'id' is missing in type 'Record<string, any>' but required in type '{ id: number; }'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(586,47): error TS2345: Argument of type 'Record<string, any>' is not assignable to parameter of type '{ id: number; }'.
|
||||
Property 'id' is missing in type 'Record<string, any>' but required in type '{ id: number; }'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(596,59): error TS2345: Argument of type 'Record<string, any>' is not assignable to parameter of type '{ id: number; }'.
|
||||
Property 'id' is missing in type 'Record<string, any>' but required in type '{ id: number; }'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(611,46): error TS2345: Argument of type 'Record<string, any>' is not assignable to parameter of type '{ id: number; express_company?: unknown; tracking_number?: unknown; ship_mode?: unknown; }'.
|
||||
Property 'id' is missing in type 'Record<string, any>' but required in type '{ id: number; express_company?: unknown; tracking_number?: unknown; ship_mode?: unknown; }'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(621,53): error TS2345: Argument of type 'Record<string, any>' is not assignable to parameter of type '{ id: number; diagnosis_id?: number | undefined; pay_order_ids?: number[] | undefined; }'.
|
||||
Property 'id' is missing in type 'Record<string, any>' but required in type '{ id: number; diagnosis_id?: number | undefined; pay_order_ids?: number[] | undefined; }'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(631,53): error TS2345: Argument of type 'Record<string, any>' is not assignable to parameter of type '{ id: number; }'.
|
||||
Property 'id' is missing in type 'Record<string, any>' but required in type '{ id: number; }'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(641,53): error TS2345: Argument of type 'Record<string, any>' is not assignable to parameter of type '{ id: number; }'.
|
||||
Property 'id' is missing in type 'Record<string, any>' but required in type '{ id: number; }'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(1550,45): error TS2322: Type 'string | number | undefined' is not assignable to type 'number | null | undefined'.
|
||||
Type 'string' is not assignable to type 'number'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(1738,41): error TS2322: Type 'string | number | undefined' is not assignable to type 'number | null | undefined'.
|
||||
Type 'string' is not assignable to type 'number'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(2509,68): error TS7006: Parameter 'r' implicitly has an 'any' type.
|
||||
src/views/consumer/prescription/order_list_h5.vue(2848,9): error TS2322: Type '{ value: any; label: any; code: any; children: any; }[]' is not assignable to type 'never[]'.
|
||||
Type '{ value: any; label: any; code: any; children: any; }' is not assignable to type 'never'.
|
||||
src/views/consumer/prescription/order_list_h5.vue(4694,13): error TS2322: Type 'string | number | undefined' is not assignable to type 'number | undefined'.
|
||||
Type 'string' is not assignable to type 'number'.
|
||||
src/views/decoration/component/tabbar/pc/attr.vue(10,18): error TS2345: Argument of type '{ modelValue: any; }' is not assignable to parameter of type '{ readonly modelValue: any[]; readonly max?: number | undefined; readonly min?: number | undefined; readonly itemData: any; readonly "onUpdate:modelValue"?: ((value: any) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'.
|
||||
Property 'itemData' is missing in type '{ modelValue: any; }' but required in type '{ readonly modelValue: any[]; readonly max?: number | undefined; readonly min?: number | undefined; readonly itemData: any; readonly "onUpdate:modelValue"?: ((value: any) => any) | undefined; }'.
|
||||
src/views/decoration/component/tabbar/pc/attr.vue(13,18): error TS2345: Argument of type '{ modelValue: any; }' is not assignable to parameter of type '{ readonly modelValue: any[]; readonly max?: number | undefined; readonly min?: number | undefined; readonly itemData: any; readonly "onUpdate:modelValue"?: ((value: any) => any) | undefined; } & VNodeProps & AllowedComponentProps & ComponentCustomProps & Record<...>'.
|
||||
Property 'itemData' is missing in type '{ modelValue: any; }' but required in type '{ readonly modelValue: any[]; readonly max?: number | undefined; readonly min?: number | undefined; readonly itemData: any; readonly "onUpdate:modelValue"?: ((value: any) => any) | undefined; }'.
|
||||
src/views/decoration/component/widgets/middle-banner/content.vue(6,33): error TS2339: Property 'height' does not exist on type '{}'.
|
||||
src/views/doctor/dept-tongji.vue(131,31): error TS7006: Parameter 'depts' implicitly has an 'any' type.
|
||||
src/views/doctor/dept-tongji.vue(132,17): error TS7034: Variable 'result' implicitly has type 'any[]' in some locations where its type cannot be determined.
|
||||
src/views/doctor/dept-tongji.vue(133,27): error TS7006: Parameter 'dept' implicitly has an 'any' type.
|
||||
src/views/doctor/dept-tongji.vue(136,30): error TS7005: Variable 'result' implicitly has an 'any[]' type.
|
||||
src/views/doctor/dept-tongji.vue(139,20): error TS7005: Variable 'result' implicitly has an 'any[]' type.
|
||||
src/views/doctor/tongji.vue(94,58): error TS2322: Type 'string' is not assignable to type '"success" | "warning" | "info" | "primary" | "danger" | undefined'.
|
||||
src/views/doctor/tongji.vue(125,58): error TS2322: Type 'string' is not assignable to type '"success" | "warning" | "info" | "primary" | "danger" | undefined'.
|
||||
src/views/doctor/tongji.vue(230,48): error TS2769: No overload matches this call.
|
||||
Overload 1 of 3, '(callbackfn: (previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown, initialValue: unknown): unknown', gave the following error.
|
||||
Argument of type '(total: number, count: number) => number' is not assignable to parameter of type '(previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown'.
|
||||
Types of parameters 'total' and 'previousValue' are incompatible.
|
||||
Type 'unknown' is not assignable to type 'number'.
|
||||
Overload 2 of 3, '(callbackfn: (previousValue: number, currentValue: unknown, currentIndex: number, array: unknown[]) => number, initialValue: number): number', gave the following error.
|
||||
Argument of type '(total: number, count: number) => number' is not assignable to parameter of type '(previousValue: number, currentValue: unknown, currentIndex: number, array: unknown[]) => number'.
|
||||
Types of parameters 'count' and 'currentValue' are incompatible.
|
||||
Type 'unknown' is not assignable to type 'number'.
|
||||
src/views/doctor/tongji.vue(236,51): error TS2769: No overload matches this call.
|
||||
Overload 1 of 3, '(callbackfn: (previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown, initialValue: unknown): unknown', gave the following error.
|
||||
Argument of type '(total: number, count: number) => number' is not assignable to parameter of type '(previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown'.
|
||||
Types of parameters 'total' and 'previousValue' are incompatible.
|
||||
Type 'unknown' is not assignable to type 'number'.
|
||||
Overload 2 of 3, '(callbackfn: (previousValue: number, currentValue: unknown, currentIndex: number, array: unknown[]) => number, initialValue: number): number', gave the following error.
|
||||
Argument of type '(total: number, count: number) => number' is not assignable to parameter of type '(previousValue: number, currentValue: unknown, currentIndex: number, array: unknown[]) => number'.
|
||||
Types of parameters 'count' and 'currentValue' are incompatible.
|
||||
Type 'unknown' is not assignable to type 'number'.
|
||||
src/views/fans/h5.vue(43,47): error TS2322: Type '"" | "info" | "danger"' is not assignable to type '"success" | "warning" | "info" | "primary" | "danger" | undefined'.
|
||||
Type '""' is not assignable to type '"success" | "warning" | "info" | "primary" | "danger" | undefined'.
|
||||
src/views/fans/h5.vue(167,51): error TS2322: Type 'string' is not assignable to type '"success" | "warning" | "info" | "primary" | "danger" | undefined'.
|
||||
src/views/fans/index.vue(158,34): error TS2322: Type 'string' is not assignable to type '"success" | "warning" | "info" | "primary" | "danger" | undefined'.
|
||||
src/views/message/notice/index.vue(12,44): error TS2322: Type '(opts?: { silent?: boolean; }) => Promise<any>' is not assignable to type '(name: TabPaneName) => any'.
|
||||
Types of parameters 'opts' and 'name' are incompatible.
|
||||
Type 'TabPaneName' is not assignable to type '{ silent?: boolean | undefined; } | undefined'.
|
||||
Type 'string' has no properties in common with type '{ silent?: boolean | undefined; }'.
|
||||
src/views/order/index.vue(446,33): error TS2367: This comparison appears to be unintentional because the types '"supplement"' and '"normal"' have no overlap.
|
||||
src/views/order/index.vue(1542,13): error TS2367: This comparison appears to be unintentional because the types '"supplement"' and '"normal"' have no overlap.
|
||||
src/views/organization/department/edit.vue(20,29): error TS2353: Object literal may only specify known properties, and 'value' does not exist in type 'TreeOptionProps'.
|
||||
src/views/permission/admin/edit.vue(44,29): error TS2353: Object literal may only specify known properties, and 'value' does not exist in type 'TreeOptionProps'.
|
||||
src/views/tcm/appointment/list.vue(90,26): error TS2322: Type 'unknown[]' is not assignable to type 'TreeNodeData[]'.
|
||||
Type 'unknown' is not assignable to type 'TreeNodeData'.
|
||||
src/views/tcm/diagnosis/add.vue(744,49): error TS2349: This expression is not callable.
|
||||
Type 'String' has no call signatures.
|
||||
src/views/tcm/diagnosis/add.vue(772,45): error TS2349: This expression is not callable.
|
||||
Type 'String' has no call signatures.
|
||||
src/views/tcm/diagnosis/components/BloodRecordList.vue(416,5): error TS2322: Type 'string' is not assignable to type 'number'.
|
||||
src/views/tcm/diagnosis/components/RecordingVideoPlayer.vue(145,54): error TS2554: Expected 0 arguments, but got 1.
|
||||
src/views/tcm/diagnosis/components/RecordingVideoPlayer.vue(146,50): error TS2554: Expected 0 arguments, but got 1.
|
||||
src/views/tcm/diagnosis/components/RecordingVideoPlayer.vue(147,47): error TS2554: Expected 0 arguments, but got 1.
|
||||
src/views/tcm/diagnosis/components/RecordingVideoPlayer.vue(148,47): error TS2554: Expected 0 arguments, but got 1.
|
||||
src/views/tcm/diagnosis/components/RecordingVideoPlayer.vue(154,26): error TS2554: Expected 0 arguments, but got 1.
|
||||
src/views/workbench/index.vue(541,18): error TS7006: Parameter 'd' implicitly has an 'any' type.
|
||||
@@ -21,20 +21,27 @@ const overview = {
|
||||
dispatch_sync: { status: 1, desired_version: 2, applied_version: 1, last_error: '' }
|
||||
}]
|
||||
}
|
||||
for (const id of [100, 101]) {
|
||||
const pool = structuredClone(overview.pools[0])
|
||||
Object.assign(pool, { id, name: `批量验证方案 ${id}`, public_key: `test-only-${id}` })
|
||||
overview.pools.push(pool)
|
||||
}
|
||||
overview.summary.pool_count = overview.summary.online_links = overview.pools.length
|
||||
const fixture = `const overview = ${JSON.stringify(overview)};
|
||||
async function invoke(name, params) {
|
||||
if (name === 'wecomPromotionOverview') return structuredClone(overview);
|
||||
if (name === 'wecomPromotionSyncMemberRange') {
|
||||
const pool = overview.pools[0];
|
||||
if (new URLSearchParams(location.search).get('mode') === 'failure') {
|
||||
const pool = overview.pools.find(item => item.id === params.pool_id);
|
||||
const mode = new URLSearchParams(location.search).get('mode');
|
||||
if (mode === 'failure' || (mode === 'mixed' && pool.id === 100)) {
|
||||
pool.dispatch_sync.status = 3; pool.dispatch_sync.last_error = '模拟企微网络错误';
|
||||
pool.member_rules.forEach(row => {row.sync_status = 3; row.sync_error = '模拟企微网络错误';});
|
||||
return {pool_id:99,sync_status:'failed',sync_error:'模拟企微网络错误',sync_queued:true};
|
||||
return {pool_id:pool.id,sync_status:'failed',sync_error:'模拟企微网络错误',sync_queued:true};
|
||||
}
|
||||
pool.official_link.range_userids = ['test_xuke']; pool.official_link.last_sync_time = Math.floor(Date.now()/1000);
|
||||
pool.dispatch_sync = {status:0,desired_version:2,applied_version:2,last_error:''};
|
||||
pool.member_rules.forEach(row => {row.is_in_remote_range = row.enabled === 1; row.sync_status = 0;});
|
||||
return {pool_id:99,sync_status:'synced',sync_error:'',sync_queued:false,range_userids:['test_xuke'],range_department_ids:[]};
|
||||
return {pool_id:pool.id,sync_status:'synced',sync_error:'',sync_queued:false,range_userids:['test_xuke'],range_department_ids:[]};
|
||||
}
|
||||
throw new Error('隔离页面未实现该操作:' + name);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user