This commit is contained in:
Your Name
2026-09-07 10:07:47 +08:00
parent cf3fbdc5ef
commit d5164b7369
388 changed files with 19863 additions and 18720 deletions
+475 -423
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -300,6 +300,10 @@ export interface WecomPromotionBatchUpdatePoolsParams {
fallback_url?: string
status?: 0 | 1
automation_config?: Record<string, unknown>
member_status?: {
member_admin_ids: number[]
status: 0 | 1
}
}
}
@@ -309,6 +313,8 @@ export interface WecomPromotionBatchUpdatePoolResult {
success: boolean
sync_error?: string
sync_queued?: boolean
member_matched?: number
member_updated?: number
error?: string
}
@@ -318,6 +324,8 @@ export interface WecomPromotionBatchUpdatePoolsResult {
failed: number
sync_error_count: number
sync_queued_count: number
member_matched: number
member_updated: number
results: WecomPromotionBatchUpdatePoolResult[]
}
@@ -154,7 +154,7 @@
<div class="panel-heading panel-heading--table">
<div>
<h2>明细数据列表</h2>
<p>展开部门可查看人员明细加粉=总进线=区间新增加粉员工+客户去重包含区间内添加后已删客户剔除继承客户扫一扫/搜手机号/名片分享添加及区间前已加过的重加<template v-if="canViewDeletedFans">-N表示加粉总数中已删除</template>挂号=已支付且实收低于 10 元的订单预约=有效预约记录开口率=开口/加粉挂号率=挂号/加粉面诊率=面诊/挂号看挂号后流失预约率=面诊/预约看预约后未面诊面诊接诊率=接诊诊单/面诊接诊率=接诊诊单/总进线</p>
<p>展开部门可查看人员明细加粉=总进线=区间新增员工+客户组合同一员工的同一客户只计一次同一客户进入不同员工分别计数包含区间内添加后已删客户剔除继承客户扫一扫/搜手机号/名片分享添加及区间前已存在的相同组合<template v-if="canViewDeletedFans">-N表示加粉组合中已删除</template>挂号=已支付且实收低于 10 元的订单预约=有效预约记录开口率=开口/加粉挂号率=挂号/加粉面诊率=面诊/挂号看挂号后流失预约率=面诊/预约看预约后未面诊面诊接诊率=接诊诊单/面诊接诊率=接诊诊单/总进线</p>
</div>
<span>{{ dashboard.rows.length }} 个顶层节点</span>
</div>
@@ -193,7 +193,7 @@
v-if="hasFans(row.add_fans_count)"
type="button"
class="fan-count-value fan-detail-trigger fan-detail-trigger--table"
title="查看该行加粉客户明细"
title="查看该行加粉组合明细"
@click.stop="openFansDetail(row)"
>
{{ formatNumber(row.add_fans_count) }}
@@ -349,7 +349,7 @@
</template>
</el-table-column>
<template #empty>
<el-empty :image-size="68" description="当前条件下暂无加粉客户明细" />
<el-empty :image-size="68" description="当前条件下暂无加粉组合明细" />
</template>
</el-table>
@@ -448,7 +448,7 @@ const timeOptions = [
{ label: '自定义', value: 'custom' }
]
const metricCards: Array<{ key: string; label: string; type: MetricType; hint: string }> = [
{ key: 'add_fans_count', label: '加粉数', type: 'count', hint: '区间新增加粉(含已删除)' },
{ key: 'add_fans_count', label: '加粉数', type: 'count', hint: '区间新增员工+客户组合(不同员工分别计数,含已删除)' },
{ key: 'total_open_count', label: '开口数', type: 'count', hint: '来源于个人业绩录入' },
{ key: 'interview_count', label: '面诊', type: 'count', hint: '已完成预约' },
{ key: 'completed_order_count', label: '接诊诊单', type: 'count', hint: '业务订单,按创建人归属并过滤无效单' },
@@ -518,7 +518,7 @@ const rankingKind = computed(() => dashboard.meta.ranking_kind || (
))
const showRankings = computed(() => rankingKind.value !== 'hidden')
const rankingSubject = computed(() => rankingKind.value === 'member' ? '组内成员' : '小组')
const fansDetailTitle = computed(() => `${fansDetailEntity.value.name} · 加粉客户明细`)
const fansDetailTitle = computed(() => `${fansDetailEntity.value.name} · 加粉组合明细`)
const detailRangeText = computed(() => {
const startDate = dashboard.meta.start_date || query.start_date || ''
const endDate = dashboard.meta.end_date || query.end_date || ''
@@ -637,7 +637,7 @@ async function loadFansDetail() {
if (requestId !== latestFansDetailRequestId) return
fansDetailRows.value = []
fansDetailPager.total = 0
ElMessage.error(error?.message || '加粉客户明细加载失败')
ElMessage.error(error?.message || '加粉组合明细加载失败')
} finally {
if (requestId === latestFansDetailRequestId) fansDetailLoading.value = false
}
@@ -74,6 +74,16 @@
<div v-if="overview.pools.length" class="pool-layout">
<aside class="pool-sidebar">
<div class="pool-select-all">
<el-checkbox
:model-value="allManageablePoolsSelected"
:indeterminate="someManageablePoolsSelected"
:disabled="!manageablePoolIds.length"
aria-label="全选可管理的分流方案"
@change="toggleAllPoolSelection"
>全选</el-checkbox>
<span>可选 {{ manageablePoolIds.length }} </span>
</div>
<div
v-for="pool in overview.pools"
:key="pool.id"
@@ -488,6 +498,62 @@
</div>
</section>
<section class="batch-config-section">
<h3>员工上下线</h3>
<p class="batch-section-tip">所选员工会在其已加入的所选方案中统一上线或下线不属于某个方案的员工不会被加入该方案为保证方案级原子提交员工上下线需单独批量保存</p>
<div class="batch-field batch-member-status-field" :class="{ 'is-disabled': !batchConfigApply.member_status }">
<el-checkbox v-model="batchConfigApply.member_status" :disabled="savingBatchConfig">批量修改员工上线状态</el-checkbox>
<div class="batch-member-status-grid">
<el-form-item label="目标状态">
<el-radio-group v-model="batchConfigForm.member_status" :disabled="!batchConfigApply.member_status || savingBatchConfig">
<el-radio-button :value="1">批量上线</el-radio-button>
<el-radio-button :value="0">批量下线</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="选择员工">
<el-tree-select
v-model="batchConfigForm.member_admin_ids"
:data="batchMemberDepartmentTree"
:props="memberTreeProps"
:default-expanded-keys="batchMemberTreeDefaultExpandedKeys"
:filter-node-method="filterMemberTreeNode"
node-key="value"
multiple
filterable
clearable
show-checkbox
check-on-click-node
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="3"
:multiple-limit="100"
:render-after-expand="false"
:disabled="!batchConfigApply.member_status || savingBatchConfig"
placeholder="按部门勾选,或搜索姓名 / 部门 / 企微 userid"
style="width: 100%"
>
<template #default="{ data }">
<span class="member-tree-option" :class="`is-${data.kind}`">
<span class="member-tree-option__label">{{ data.label }}</span>
<span v-if="data.kind === 'department'" class="member-tree-option__count">{{ data.member_count }} </span>
<span v-else class="member-tree-option__detail">{{ data.detail }}</span>
</span>
</template>
</el-tree-select>
<span class="form-tip">勾选部门可全选其下员工已选 {{ batchConfigForm.member_admin_ids.length }} </span>
</el-form-item>
</div>
<el-alert
v-if="batchConfigApply.member_status && batchConfigForm.member_status === 0 && batchMemberOfflineBlockedPools.length"
type="warning"
show-icon
:closable="false"
:title="`${batchMemberOfflineBlockedPools[0].name} 至少需要保留一名当前可用的上线员工`"
:description="batchMemberOfflineBlockedPools.length > 1 ? `另有 ${batchMemberOfflineBlockedPools.length - 1} 个方案也会没有可用上线员工,请调整选择。` : '请减少下线员工,或先为该方案上线一名当前可用的员工。'"
/>
</div>
</section>
<section v-if="overview.automation_installed" class="batch-config-section automation-batch-section">
<h3>自动化设置</h3>
<p class="batch-section-tip">先选择要覆盖的分组未选择的接待客户或欢迎语设置不会随本次批量操作改变</p>
@@ -514,7 +580,7 @@
@busy="(busy) => batchConfigBusy = busy"
/>
</section>
<el-alert v-else type="warning" show-icon :closable="false" title="自动化配置尚未安装" description="本次仍可批量修改验证方式、兜底链接运行状态;安装自动化数据表后即可批量修改接待、客户和欢迎语设置。" />
<el-alert v-else type="warning" show-icon :closable="false" title="自动化配置尚未安装" description="本次仍可批量修改验证方式、兜底链接运行状态和员工上下线;安装自动化数据表后即可批量修改接待、客户和欢迎语设置。" />
</el-form>
</div>
<template #footer>
@@ -655,6 +721,12 @@ interface PromotionMemberOption {
dept_names: string[]
}
interface BatchMemberStatusOption extends PromotionMemberOption {
pool_count: number
online_count: number
offline_count: number
}
interface PromotionOperatorOption {
id: number
name: string
@@ -714,8 +786,8 @@ const poolFormScroll = ref<HTMLElement>()
const poolFormError = ref('')
const automationBusy = ref(false)
const accessForm = reactive({ pool_ids: [] as number[], operator_admin_ids: [] as number[], action: 'grant' as 'grant' | 'revoke' })
const batchConfigApply = reactive({ skip_verify: false, fallback_url: false, status: false, reception: false, customer: false, welcome: false })
const batchConfigForm = reactive({ pool_ids: [] as number[], skip_verify: 0, fallback_url: '', status: 1, automation_config: defaultAutomationConfig() })
const batchConfigApply = reactive({ skip_verify: false, fallback_url: false, status: false, member_status: false, reception: false, customer: false, welcome: false })
const batchConfigForm = reactive({ pool_ids: [] as number[], skip_verify: 0, fallback_url: '', status: 1, member_admin_ids: [] as number[], member_status: 1 as 0 | 1, automation_config: defaultAutomationConfig() })
const memberForm = reactive({ id: 0, name: '', userid: '', daily_limit: 0, status: 1, active_range: [] as string[], remark: '' })
const customerStatsLoading = ref(false)
const customerStatsLoaded = ref(false)
@@ -738,7 +810,84 @@ const selectedPool = computed(() => overview.pools.find((item: any) => Number(it
const selectedMemberRules = computed(() => Array.isArray(selectedPool.value?.member_rules) ? selectedPool.value.member_rules : [])
const selectedInstallPool = computed(() => overview.pools.find((item: any) => Number(item.id) === selectedInstallPoolId.value))
const accessDialogPools = computed(() => overview.pools.filter((item: any) => accessForm.pool_ids.includes(Number(item.id))))
const manageablePoolIds = computed(() => overview.pools
.filter((pool: any) => pool.can_manage_access)
.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 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]))
const grouped = new Map<number, BatchMemberStatusOption & { pool_ids: Set<number> }>()
batchConfigPools.value.forEach((pool: any) => {
const poolId = Number(pool.id)
const memberRules = Array.isArray(pool.member_rules) ? pool.member_rules : []
memberRules.forEach((rule: any) => {
const adminId = Number(rule.admin_id)
if (adminId <= 0) return
const member = memberOptionById.get(adminId)
const current = grouped.get(adminId) || {
id: adminId,
name: String(rule.name || member?.name || rule.userid || `员工 ${adminId}`),
userid: String(member?.userid || rule.userid || ''),
display_dept_id: Number(member?.display_dept_id || 0),
dept_ids: Array.isArray(member?.dept_ids) ? member.dept_ids.map(Number).filter((id) => id > 0) : [],
dept_names: Array.isArray(member?.dept_names)
? member.dept_names.map(String).filter(Boolean)
: (Array.isArray(rule.dept_names) ? rule.dept_names.map(String).filter(Boolean) : []),
pool_count: 0,
online_count: 0,
offline_count: 0,
pool_ids: new Set<number>()
}
if (!current.pool_ids.has(poolId)) {
current.pool_ids.add(poolId)
current.pool_count++
if (Number(rule.enabled) === 1) current.online_count++
else current.offline_count++
}
grouped.set(adminId, current)
})
})
return [...grouped.values()]
.map(({ pool_ids: _poolIds, ...member }) => member)
.sort((left, right) => left.name.localeCompare(right.name, 'zh-CN'))
})
const batchMemberStatusById = computed(() => new Map(
batchMemberStatusOptions.value.map((member) => [member.id, member])
))
const batchMemberDepartmentTree = computed(() => buildMemberDepartmentTree(
overview.department_options,
batchMemberStatusOptions.value,
{
selectableDepartments: true,
memberDetail: (member) => {
const status = batchMemberStatusById.value.get(member.id)
return status
? `${status.pool_count} 个方案 · 已上线 ${status.online_count} / 已下线 ${status.offline_count}`
: ''
}
}
))
const batchMemberTreeDefaultExpandedKeys = computed(() => batchMemberDepartmentTree.value.map((node) => node.value))
const batchMemberOfflineBlockedPools = computed(() => {
if (!batchConfigApply.member_status || batchConfigForm.member_status !== 0) return []
const targetAdminIds = new Set(batchConfigForm.member_admin_ids.map(Number))
return batchConfigPools.value.filter((pool: any) => {
const rules = Array.isArray(pool.member_rules) ? pool.member_rules : []
const targetedEnabled = rules.some((rule: any) => Number(rule.enabled) === 1 && targetAdminIds.has(Number(rule.admin_id)))
const remainingAvailable = rules.some((rule: any) => Number(rule.enabled) === 1
&& (batchConfigApply.reception || rule.reception_available)
&& !targetAdminIds.has(Number(rule.admin_id)))
return targetedEnabled && !remainingAvailable
})
})
const batchPrimaryMemberSets = computed(() => batchConfigPools.value.map((pool: any) => new Set<number>(
(Array.isArray(pool.member_admin_ids) ? pool.member_admin_ids : []).map(Number)
)))
@@ -811,6 +960,10 @@ function togglePoolSelection(pool: any, checked: unknown) {
selectedPoolIds.value = [...next]
}
function toggleAllPoolSelection(checked: unknown) {
selectedPoolIds.value = checked ? [...manageablePoolIds.value] : []
}
function openBatchConfigDialog(poolIds: number[] = selectedPoolIds.value) {
const manageableIds = new Set(overview.pools
.filter((pool: any) => pool.can_manage_access)
@@ -823,6 +976,7 @@ function openBatchConfigDialog(poolIds: number[] = selectedPoolIds.value) {
skip_verify: false,
fallback_url: false,
status: false,
member_status: false,
reception: false,
customer: false,
welcome: false
@@ -832,6 +986,8 @@ function openBatchConfigDialog(poolIds: number[] = selectedPoolIds.value) {
skip_verify: Number(reference?.skip_verify) === 1 ? 1 : 0,
fallback_url: String(reference?.fallback_url || ''),
status: Number(reference?.status) === 1 ? 1 : 0,
member_admin_ids: [],
member_status: 1,
automation_config: cloneAutomationConfig(reference?.automation_config)
})
batchConfigError.value = ''
@@ -860,9 +1016,23 @@ async function saveBatchConfig() {
batchConfigError.value = ''
const hasAutomationChange = overview.automation_installed
&& (batchConfigApply.reception || batchConfigApply.customer || batchConfigApply.welcome)
if (!batchConfigApply.skip_verify && !batchConfigApply.fallback_url && !batchConfigApply.status && !hasAutomationChange) {
const hasMemberStatusChange = batchConfigApply.member_status
const hasPoolConfigChange = batchConfigApply.skip_verify || batchConfigApply.fallback_url || batchConfigApply.status || hasAutomationChange
if (!batchConfigApply.skip_verify && !batchConfigApply.fallback_url && !batchConfigApply.status && !hasMemberStatusChange && !hasAutomationChange) {
return setBatchConfigError('请至少勾选一项需要批量修改的配置')
}
if (hasMemberStatusChange && hasPoolConfigChange) {
return setBatchConfigError('员工上下线需要单独批量保存,请取消其他方案配置修改')
}
if (hasMemberStatusChange && !batchConfigForm.member_admin_ids.length) {
return setBatchConfigError('请至少选择一名需要批量上线或下线的员工')
}
if (hasMemberStatusChange && batchConfigForm.member_admin_ids.length > 100) {
return setBatchConfigError('单次最多设置 100 名员工,请减少选择范围')
}
if (hasMemberStatusChange && batchConfigForm.member_status === 0 && batchMemberOfflineBlockedPools.value.length) {
return setBatchConfigError(`${batchMemberOfflineBlockedPools.value[0].name} 至少需要保留一名当前可用的上线员工`)
}
if (batchConfigApply.fallback_url && batchConfigForm.fallback_url.trim() && !isWebUrl(batchConfigForm.fallback_url.trim())) {
return setBatchConfigError('兜底获客助手链接必须为有效的 HTTP/HTTPS 地址')
}
@@ -889,10 +1059,17 @@ async function saveBatchConfig() {
fallback_url?: string
status?: 0 | 1
automation_config?: Record<string, unknown>
member_status?: { member_admin_ids: number[]; status: 0 | 1 }
} = {}
if (batchConfigApply.skip_verify) changes.skip_verify = batchConfigForm.skip_verify === 1 ? 1 : 0
if (batchConfigApply.fallback_url) changes.fallback_url = batchConfigForm.fallback_url.trim()
if (batchConfigApply.status) changes.status = batchConfigForm.status === 1 ? 1 : 0
if (hasMemberStatusChange) {
changes.member_status = {
member_admin_ids: [...batchConfigForm.member_admin_ids],
status: batchConfigForm.member_status
}
}
if (hasAutomationChange) {
const serialized = serializeAutomationConfig(automation) as unknown as Record<string, unknown>
const automationPatch: Record<string, unknown> = {}
@@ -919,19 +1096,25 @@ async function saveBatchConfig() {
batchConfigDialogVisible.value = false
selectedPoolIds.value = []
await loadOverview()
const memberStatusText = batchConfigForm.member_status === 1 ? '上线' : '下线'
const memberResultText = hasMemberStatusChange
? (result.member_updated > 0
? `${result.member_updated} 条员工规则已${memberStatusText}`
: `,所选员工均已处于${memberStatusText}状态`)
: ''
if (result.failed > 0) {
const detail = result.results
.filter((item) => !item.success)
.slice(0, 2)
.map((item) => `${item.name || `方案 ${item.id}`}${item.error || '保存失败'}`)
.join('')
ElMessage.warning(`已更新 ${result.updated} 个方案,${result.failed} 个失败${detail ? `${detail}` : ''}`)
ElMessage.warning(`已更新 ${result.updated} 个方案,${result.failed} 个失败${memberResultText}${detail ? `${detail}` : ''}`)
} else if (result.sync_error_count > 0) {
ElMessage.warning(`已更新 ${result.updated} 个方案,其中 ${result.sync_error_count} 个企微范围将在后台自动重试同步`)
ElMessage.warning(`已更新 ${result.updated} 个方案${memberResultText},其中 ${result.sync_error_count} 个企微范围将在后台自动重试同步`)
} else if (result.sync_queued_count > 0) {
ElMessage.success(`已批量更新 ${result.updated} 个分流方案,${result.sync_queued_count} 个企微链接配置将在后台同步`)
ElMessage.success(`已批量更新 ${result.updated} 个分流方案${memberResultText}${result.sync_queued_count} 个企微链接配置将在后台同步`)
} else {
ElMessage.success(`已批量更新 ${result.updated} 个分流方案`)
ElMessage.success(`已批量更新 ${result.updated} 个分流方案${memberResultText}`)
}
} catch (error: any) {
setBatchConfigError(error?.message || '批量修改分流方案失败')
@@ -946,6 +1129,7 @@ function openAccessDialog(poolIds: number[] = selectedPoolIds.value) {
.map((pool: any) => Number(pool.id)))
const ids = [...new Set(poolIds.map(Number).filter((id) => manageableIds.has(id)))]
if (!ids.length) return ElMessage.warning('请先选择可管理的分流方案')
if (ids.length > 100) return ElMessage.warning('单次最多设置 100 个分流方案')
Object.assign(accessForm, {
pool_ids: ids,
operator_admin_ids: [],
@@ -1286,7 +1470,11 @@ function todayCount(row: any) {
function buildMemberDepartmentTree(
departmentOptions: PromotionDepartmentOption[],
memberOptions: PromotionMemberOption[]
memberOptions: PromotionMemberOption[],
options: {
selectableDepartments?: boolean
memberDetail?: (member: PromotionMemberOption) => string
} = {}
): PromotionMemberTreeNode[] {
const availableDeptIds = new Set<number>()
const collectDeptIds = (departments: PromotionDepartmentOption[]) => {
@@ -1331,7 +1519,8 @@ function buildMemberDepartmentTree(
const placedMemberIds = new Set<number>()
const toMemberNode = (member: PromotionMemberOption): PromotionMemberTreeNode => {
const departments = member.dept_names.length ? member.dept_names.join(' / ') : '未分部门'
const detail = member.userid ? `${departments} · ${member.userid}` : departments
const detail = options.memberDetail?.(member)
|| (member.userid ? `${departments} · ${member.userid}` : departments)
return {
value: member.id,
label: member.name,
@@ -1366,7 +1555,7 @@ function buildMemberDepartmentTree(
value: `dept:${departmentId}`,
label: departmentName,
kind: 'department',
disabled: true,
disabled: !options.selectableDepartments,
member_count: memberCount,
search_text: `${path.join(' ')} ${children.map((child) => child.search_text).join(' ')}`.toLocaleLowerCase(),
children
@@ -1383,7 +1572,7 @@ function buildMemberDepartmentTree(
value: 'dept:unassigned',
label: '未分部门',
kind: 'department',
disabled: true,
disabled: !options.selectableDepartments,
member_count: children.length,
search_text: `未分部门 ${children.map((child) => child.search_text).join(' ')}`.toLocaleLowerCase(),
children
@@ -1569,6 +1758,7 @@ h1, h2, h3, p { margin: 0; }
.selection-count { color: #117f75; font-size: 11px; font-weight: 600; }
.pool-layout { display: grid; grid-template-columns: 252px minmax(0, 1fr); height: clamp(430px, calc(100vh - 330px), 720px); min-height: 430px; border: 1px solid var(--line); border-radius: 11px; overflow: hidden; }
.pool-sidebar { min-height: 0; padding: 8px; border-right: 1px solid var(--line); overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; background: #f7f9fa; }
.pool-select-all { display: flex; position: sticky; z-index: 2; top: 0; align-items: center; justify-content: space-between; min-height: 42px; margin: 0 0 8px; padding: 4px 8px; border-bottom: 1px solid var(--line); background: #f7f9fa; }.pool-select-all :deep(.el-checkbox) { margin-right: 0; }.pool-select-all span { color: #8c98a7; font-size: 10px; }
.pool-select-row { display: grid; grid-template-columns: 22px minmax(0, 1fr); align-items: center; gap: 4px; margin-bottom: 5px; }
.pool-select-row :deep(.el-checkbox) { justify-content: center; margin-right: 0; }
.pool-item { display: grid; grid-template-columns: 9px minmax(0, 1fr) 16px; align-items: center; gap: 9px; width: 100%; min-height: 62px; padding: 10px; border: 1px solid transparent; border-radius: 8px; text-align: left; background: transparent; cursor: pointer; }
@@ -1621,10 +1811,11 @@ h1, h2, h3, p { margin: 0; }
.batch-pool-summary strong { margin-right: 3px; font-size: 12px; }.batch-pool-summary span { padding: 4px 8px; border-radius: 12px; color: #426078; background: #eaf1f5; font-size: 10px; }.batch-pool-summary small { color: #8491a2; font-size: 10px; }
.batch-config-form { margin-top: 16px; }.batch-config-section { margin-bottom: 18px; padding: 16px; border: 1px solid var(--line); border-radius: 9px; background: #fff; }.batch-config-section > h3 { margin: 0 0 14px; color: #303b4d; font-size: 14px; }
.batch-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }.batch-field { padding: 12px; border: 1px solid #dfe6ec; border-radius: 8px; transition: opacity .2s ease, background .2s ease; }.batch-field.is-disabled { opacity: .58; background: #f7f8fa; }.batch-field :deep(.el-form-item) { margin: 10px 0 0; }
.batch-member-status-field { padding: 14px; }.batch-member-status-grid { display: grid; grid-template-columns: minmax(190px, .7fr) minmax(0, 1.6fr); gap: 14px; }
.batch-section-tip { margin: -6px 0 12px; color: #8491a2; font-size: 11px; line-height: 1.6; }.batch-section-selectors { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }.batch-section-selectors :deep(.el-checkbox) { margin-right: 0; }.automation-batch-section :deep(.automation-note) { margin-top: 14px; }
.uploading-save-tip { color: #b88230; font-size: 12px; margin-right: 16px; }
.sync-retry-tip { color: #b88230; font-size: 10px; line-height: 1.6; margin-top: 4px; }
:global(.promotion-pool-dialog) { max-width: calc(100vw - 32px); margin-top: 5vh; }
@media (max-width: 1100px) { .heading-actions { flex-wrap: wrap; justify-content: flex-end; }.metric-grid, .customer-metric-grid { grid-template-columns: repeat(2, minmax(0,1fr)); }.pool-layout { grid-template-columns: 210px minmax(0,1fr); }.pool-toolbar { align-items: flex-start; flex-direction: column; } }
@media (max-width: 760px) { .promotion-page { padding: 10px; }.page-header, .section-heading { align-items: flex-start; flex-direction: column; }.section-heading-actions { width: 100%; justify-content: flex-start; }.update-time { display: none; }.metric-grid, .customer-metric-grid, .form-grid, .rule-form-grid, .batch-field-grid { grid-template-columns: 1fr; }.tab-nav { overflow-x: auto; }.tab-nav button { min-width: 112px; }.tab-content { padding: 14px; }.pool-layout { height: auto; min-height: 0; grid-template-columns: 1fr; }.pool-sidebar { display: flex; overflow-x: auto; overflow-y: hidden; border-right: 0; border-bottom: 1px solid var(--line); scrollbar-gutter: auto; }.pool-main { overflow: visible; }.member-table-area { height: 420px; min-height: 320px; flex: none; }.pool-select-row { min-width: 220px; }.pool-item { min-width: 190px; }.callback-list > div { grid-template-columns: 1fr 48px; padding: 8px 0; }.callback-list dt { grid-column: 1 / -1; }.install-pool-select { width: 100%; }.customer-heading-actions { width: 100%; justify-content: flex-end; }.customer-filter-bar :deep(.el-form-item) { width: 100%; margin-right: 0; }.customer-filter-bar :deep(.el-form-item__content), .customer-filter-bar .el-select { width: 100%; }.customer-filter-bar .filter-actions :deep(.el-form-item__content) { justify-content: flex-end; }.customer-pagination { align-items: flex-start; flex-direction: column; }.customer-pagination :deep(.el-pagination) { max-width: 100%; flex-wrap: wrap; justify-content: flex-start; }.access-pool-preview > div { grid-template-columns: 1fr; gap: 3px; } }
@media (max-width: 760px) { .promotion-page { padding: 10px; }.page-header, .section-heading { align-items: flex-start; flex-direction: column; }.section-heading-actions { width: 100%; justify-content: flex-start; }.update-time { display: none; }.metric-grid, .customer-metric-grid, .form-grid, .rule-form-grid, .batch-field-grid, .batch-member-status-grid { grid-template-columns: 1fr; }.tab-nav { overflow-x: auto; }.tab-nav button { min-width: 112px; }.tab-content { padding: 14px; }.pool-layout { height: auto; min-height: 0; grid-template-columns: 1fr; }.pool-sidebar { display: flex; overflow-x: auto; overflow-y: hidden; border-right: 0; border-bottom: 1px solid var(--line); scrollbar-gutter: auto; }.pool-select-all { position: static; min-width: 126px; flex: 0 0 126px; margin: 0 6px 0 0; border-right: 1px solid var(--line); border-bottom: 0; }.pool-main { overflow: visible; }.member-table-area { height: 420px; min-height: 320px; flex: none; }.pool-select-row { min-width: 220px; }.pool-item { min-width: 190px; }.callback-list > div { grid-template-columns: 1fr 48px; padding: 8px 0; }.callback-list dt { grid-column: 1 / -1; }.install-pool-select { width: 100%; }.customer-heading-actions { width: 100%; justify-content: flex-end; }.customer-filter-bar :deep(.el-form-item) { width: 100%; margin-right: 0; }.customer-filter-bar :deep(.el-form-item__content), .customer-filter-bar .el-select { width: 100%; }.customer-filter-bar .filter-actions :deep(.el-form-item__content) { justify-content: flex-end; }.customer-pagination { align-items: flex-start; flex-direction: column; }.customer-pagination :deep(.el-pagination) { max-width: 100%; flex-wrap: wrap; justify-content: flex-start; }.access-pool-preview > div { grid-template-columns: 1fr; gap: 3px; } }
</style>
File diff suppressed because one or more lines are too long