This commit is contained in:
Your Name
2026-08-28 18:24:37 +08:00
parent 43ad07208f
commit ed48f8be31
383 changed files with 8673 additions and 2222 deletions
+24 -4
View File
@@ -238,10 +238,30 @@ export function wecomPromotionSavePool(params: Record<string, unknown>) {
return request.post({ url: '/firstvisit.wecomPromotion/savePool', params, timeout: 120000 })
}
export function wecomPromotionSaveWidget(params: Record<string, unknown>) {
return request.post({ url: '/firstvisit.wecomPromotion/saveWidget', params })
}
export function wecomPromotionSaveWidget(params: Record<string, unknown>) {
return request.post({ url: '/firstvisit.wecomPromotion/saveWidget', params })
}
export interface WecomPromotionBatchSetOperatorsParams {
pool_ids: number[]
operator_admin_ids: number[]
action: 'grant' | 'revoke'
}
export interface WecomPromotionBatchSetOperatorsResult {
action: 'grant' | 'revoke'
pool_ids: number[]
operator_admin_ids: number[]
affected: number
}
export function wecomPromotionBatchSetOperators(params: WecomPromotionBatchSetOperatorsParams) {
return request.post<WecomPromotionBatchSetOperatorsResult>({
url: '/firstvisit.wecomPromotion/batchSetOperators',
params
})
}
export function wecomPromotionDeletePool(params: { id: number }) {
return request.post({ url: '/firstvisit.wecomPromotion/deletePool', params, timeout: 120000 })
}
@@ -51,28 +51,51 @@
</nav>
<div v-if="activeTab === 'links'" class="tab-content links-tab">
<div class="section-heading">
<div class="section-heading">
<div>
<h2>获客助手分流方案</h2>
<p>选择几名医助就生成几条成员规则官方链接只在方案顶部复制成员行不重复展示链接</p>
</div>
<el-button type="primary" :icon="Plus" @click="openPoolDialog()">新建分流方案</el-button>
</div>
<div v-if="overview.pools.length" class="pool-layout">
<aside class="pool-sidebar">
<button
v-for="pool in overview.pools"
:key="pool.id"
class="pool-item"
:class="{ active: Number(pool.id) === selectedPoolId }"
@click="selectedPoolId = Number(pool.id)"
>
<span class="pool-status" :class="Number(pool.status) === 1 ? 'online' : 'offline'" />
<span><strong>{{ pool.name }}</strong><small>{{ memberSummary(pool) }} · {{ pool.main_url ? '官方链接已生成' : '待生成链接' }}</small></span>
<el-icon><ArrowRight /></el-icon>
</button>
</aside>
</div>
<div class="section-heading-actions">
<span v-if="selectedPoolIds.length" class="selection-count">已选 {{ selectedPoolIds.length }} 个方案</span>
<el-button
:icon="User"
:disabled="!selectedPoolIds.length"
@click="openAccessDialog()"
>批量设置访问操作</el-button>
<el-button type="primary" :icon="Plus" @click="openPoolDialog()">新建分流方案</el-button>
</div>
</div>
<div v-if="overview.pools.length" class="pool-layout">
<aside class="pool-sidebar">
<div
v-for="pool in overview.pools"
:key="pool.id"
class="pool-select-row"
:class="{ active: Number(pool.id) === selectedPoolId }"
>
<el-checkbox
:model-value="selectedPoolIds.includes(Number(pool.id))"
:disabled="!pool.can_manage_access"
:aria-label="`选择方案 ${pool.name}`"
@change="(checked) => togglePoolSelection(pool, checked)"
@click.stop
/>
<button
type="button"
class="pool-item"
@click="selectedPoolId = Number(pool.id)"
>
<span class="pool-status" :class="Number(pool.status) === 1 ? 'online' : 'offline'" />
<span>
<strong>{{ pool.name }}</strong>
<small>{{ memberSummary(pool) }} · {{ accessSummary(pool) }}</small>
</span>
<el-icon><ArrowRight /></el-icon>
</button>
</div>
</aside>
<div class="pool-main">
<div v-if="selectedPool" class="pool-toolbar">
@@ -90,8 +113,9 @@
<el-button :icon="CircleCheck" :loading="checkingApi" @click="checkApiPermission">验证 API</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 :icon="Edit" @click="openPoolDialog(selectedPool)">编辑方案</el-button>
<el-button 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" @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>
</div>
</div>
@@ -142,12 +166,12 @@
</el-table-column>
<el-table-column label="上线" width="90" align="center">
<template #default="{ row }">
<el-switch :model-value="Number(row.enabled) === 1" :loading="togglingMemberId === Number(row.id)" @change="(value) => handleMemberToggle(row, value)" />
<el-switch :model-value="Number(row.enabled) === 1" :disabled="!selectedPool.can_operate" :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 type="primary" link @click="openMemberDialog(row)">编辑规则</el-button>
<el-button v-if="selectedPool.can_operate" type="primary" link @click="openMemberDialog(row)">编辑规则</el-button>
</template>
</el-table-column>
<template #empty><el-empty :image-size="72" description="编辑方案并选择获客医助" /></template>
@@ -393,6 +417,70 @@
<template #footer><el-button :disabled="savingPool" @click="poolDialogVisible = false">取消</el-button><el-button type="primary" :loading="savingPool" @click="savePool">{{ poolForm.id ? '保存方案' : '保存并生成链接' }}</el-button></template>
</el-dialog>
<el-dialog
v-model="accessDialogVisible"
title="批量设置他人访问操作"
width="620px"
destroy-on-close
:close-on-click-modal="false"
:close-on-press-escape="!savingAccess"
>
<el-alert
type="info"
show-icon
:closable="false"
:title="`将设置 ${accessForm.pool_ids.length} 个分流方案`"
description="被添加的账号可以查看并编辑方案、成员规则和浮窗配置;不能删除方案,也不能继续分配操作权限。"
/>
<el-form class="access-form" label-position="top">
<el-form-item label="设置方式" required>
<el-radio-group v-model="accessForm.action" :disabled="savingAccess">
<el-radio-button value="grant">添加操作人</el-radio-button>
<el-radio-button value="revoke">移除操作人</el-radio-button>
</el-radio-group>
<span class="form-tip">批量添加不会覆盖各方案已有操作人批量移除只撤销本次选中的账号</span>
</el-form-item>
<el-form-item label="选择账号" required>
<el-select
v-model="accessForm.operator_admin_ids"
multiple
filterable
clearable
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="3"
:multiple-limit="100"
:disabled="savingAccess"
placeholder="搜索姓名或部门"
style="width: 100%"
>
<el-option
v-for="operator in overview.operator_options"
:key="operator.id"
:label="operatorOptionLabel(operator)"
:value="Number(operator.id)"
:disabled="accessForm.action === 'grant' && !operator.can_grant"
/>
</el-select>
<span class="form-tip">添加时仅可选择已启用且拥有本页面权限的账号失效账号仍可用于移除历史授权</span>
</el-form-item>
<div v-if="accessDialogPools.length" class="access-pool-preview">
<strong>当前授权</strong>
<div v-for="pool in accessDialogPools.slice(0, 5)" :key="pool.id">
<span>{{ pool.name }}</span>
<small>{{ operatorNames(pool) }}</small>
</div>
<p v-if="accessDialogPools.length > 5">另有 {{ accessDialogPools.length - 5 }} 个方案</p>
</div>
</el-form>
<template #footer>
<el-button :disabled="savingAccess" @click="accessDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="savingAccess" @click="savePoolOperators">
{{ accessForm.action === 'grant' ? '批量添加' : '批量移除' }}
</el-button>
</template>
</el-dialog>
<el-dialog v-model="memberDialogVisible" title="编辑医助分流规则" width="560px" destroy-on-close :close-on-click-modal="false">
<el-form label-position="top">
<el-form-item label="获客医助">
@@ -424,6 +512,7 @@ import {
Promotion, Refresh, Search, SetUp, User, Warning
} from '@element-plus/icons-vue'
import {
wecomPromotionBatchSetOperators,
wecomPromotionCheckApiPermission,
wecomPromotionCustomerStats,
wecomPromotionDeletePool,
@@ -454,6 +543,16 @@ interface PromotionMemberOption {
dept_names: string[]
}
interface PromotionOperatorOption {
id: number
name: string
disable: number
can_grant: boolean
display_dept_id?: number
dept_ids: number[]
dept_names: string[]
}
interface PromotionMemberTreeNode {
value: number | string
label: string
@@ -466,12 +565,13 @@ interface PromotionMemberTreeNode {
}
const emptyOverview = () => ({
meta: { scope_label: '', generated_at: '' },
meta: { admin_id: 0, scope_label: '', generated_at: '' },
config: { mode: 'internal', configured: false, ready: false, missing: [] as string[], corp_id_masked: '', agent_id: '', secret_configured: false, callback_ready: false, callback_url: '', official_doc: '' },
summary: { configured_apps: 0, pool_count: 0, online_links: 0, today_clicks: 0 },
pools: [] as any[],
links: [] as any[],
member_options: [] as PromotionMemberOption[],
operator_options: [] as PromotionOperatorOption[],
department_options: [] as PromotionDepartmentOption[],
customer_acquisition_link_example: 'https://work.weixin.qq.com/ca/xxxxxxxx'
})
@@ -481,6 +581,7 @@ const loading = ref(false)
const activeTab = ref<TabName>('links')
const selectedPoolId = ref<number>()
const selectedInstallPoolId = ref<number>()
const selectedPoolIds = ref<number[]>([])
const poolDialogVisible = ref(false)
const savingPool = ref(false)
const checkingApi = ref(false)
@@ -488,7 +589,10 @@ const memberDialogVisible = ref(false)
const savingMember = ref(false)
const togglingMemberId = ref(0)
const deletingPoolId = ref(0)
const accessDialogVisible = ref(false)
const savingAccess = ref(false)
const poolForm = reactive({ id: 0, name: '', fallback_url: '', status: 1, member_admin_ids: [] as number[], skip_verify: 0, main_url: '' })
const accessForm = reactive({ pool_ids: [] as number[], operator_admin_ids: [] as number[], action: 'grant' as 'grant' | 'revoke' })
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)
@@ -510,6 +614,7 @@ const customerStats = reactive({
const selectedPool = computed(() => overview.pools.find((item: any) => Number(item.id) === selectedPoolId.value))
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 memberTreeProps = { value: 'value', label: 'label', children: 'children', disabled: 'disabled' }
const memberDepartmentTree = computed(() => buildMemberDepartmentTree(overview.department_options, overview.member_options))
const memberTreeDefaultExpandedKeys = computed(() => memberDepartmentTree.value.map((node) => node.value))
@@ -527,17 +632,29 @@ 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)).join(','), () => {
const ids = overview.pools.map((item: any) => Number(item.id))
if (!selectedPoolId.value || !ids.includes(selectedPoolId.value)) selectedPoolId.value = ids[0]
if (!selectedInstallPoolId.value || !ids.includes(selectedInstallPoolId.value)) selectedInstallPoolId.value = ids[0]
}, { immediate: true })
watch(() => overview.pools.map((item: any) => `${Number(item.id)}:${item.can_manage_access ? 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)))
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))
}, { immediate: true })
watch(activeTab, (tab) => {
if (tab === 'customer-stats' && !customerStatsLoaded.value) loadCustomerStats()
})
watch(activeTab, (tab) => {
if (tab === 'customer-stats' && !customerStatsLoaded.value) loadCustomerStats()
})
watch(() => accessForm.action, (action) => {
if (action !== 'grant') return
const grantableIds = new Set(overview.operator_options
.filter((operator) => operator.can_grant)
.map((operator) => Number(operator.id)))
accessForm.operator_admin_ids = accessForm.operator_admin_ids.filter((id) => grantableIds.has(Number(id)))
})
async function loadOverview() {
async function loadOverview() {
loading.value = true
try {
const result: any = await wecomPromotionOverview()
@@ -546,9 +663,79 @@ async function loadOverview() {
ElMessage.error(error?.message || '企业微信推广配置加载失败')
} finally {
loading.value = false
}
}
}
}
function togglePoolSelection(pool: any, checked: unknown) {
if (!pool?.can_manage_access) return
const id = Number(pool.id)
const next = new Set(selectedPoolIds.value)
checked ? next.add(id) : next.delete(id)
selectedPoolIds.value = [...next]
}
function openAccessDialog(poolIds: number[] = selectedPoolIds.value) {
const manageableIds = new Set(overview.pools
.filter((pool: any) => pool.can_manage_access)
.map((pool: any) => Number(pool.id)))
const ids = [...new Set(poolIds.map(Number).filter((id) => manageableIds.has(id)))]
if (!ids.length) return ElMessage.warning('请先选择可管理的分流方案')
Object.assign(accessForm, {
pool_ids: ids,
operator_admin_ids: [],
action: 'grant'
})
accessDialogVisible.value = true
}
async function savePoolOperators() {
if (!accessForm.pool_ids.length) return ElMessage.warning('请至少选择一个分流方案')
if (!accessForm.operator_admin_ids.length) return ElMessage.warning('请至少选择一名操作人')
savingAccess.value = true
try {
const result = await wecomPromotionBatchSetOperators({
pool_ids: [...accessForm.pool_ids],
operator_admin_ids: [...accessForm.operator_admin_ids],
action: accessForm.action
})
accessDialogVisible.value = false
selectedPoolIds.value = []
await loadOverview()
const actionLabel = accessForm.action === 'grant' ? '添加' : '移除'
ElMessage.success(result?.affected
? `已批量${actionLabel} ${result.affected} 条方案操作权限`
: '权限设置已检查,没有需要变更的记录')
} catch (error: any) {
ElMessage.error(error?.message || '批量设置访问操作失败')
} finally {
savingAccess.value = false
}
}
function operatorOptionLabel(operator: PromotionOperatorOption) {
const departments = Array.isArray(operator.dept_names) && operator.dept_names.length
? operator.dept_names.join(' / ')
: '未分部门'
const unavailable = Number(operator.disable) === 1
? ' · 已禁用'
: (!operator.can_grant ? ' · 无页面权限' : '')
return `${operator.name} · ${departments}${unavailable}`
}
function operatorNames(pool: any) {
const operators = Array.isArray(pool?.operators) ? pool.operators : []
if (!operators.length) return '仅原管理范围可操作'
return operators
.map((operator: any) => `${operator.name || `账号 ${operator.id}`}${Number(operator.disable) === 1 ? '(已禁用)' : ''}`)
.join('、')
}
function accessSummary(pool: any) {
const linkState = pool?.main_url ? '官方链接已生成' : '待生成链接'
const operatorCount = Array.isArray(pool?.operator_admin_ids) ? pool.operator_admin_ids.length : 0
return operatorCount > 0 ? `${linkState} · ${operatorCount} 人可操作` : linkState
}
function openPoolDialog(pool?: any) {
const selectableMemberIds = new Set(overview.member_options.map((member) => Number(member.id)))
Object.assign(poolForm, pool ? {
@@ -1074,12 +1261,17 @@ h1, h2, h3, p { margin: 0; }
.tab-nav button { display: flex; align-items: center; gap: 7px; min-width: 132px; height: 54px; justify-content: center; border: 0; border-bottom: 3px solid transparent; color: #5e6d80; background: transparent; cursor: pointer; font-size: 14px; }
.tab-nav button.active { border-bottom-color: var(--teal); color: #0d8277; font-weight: 700; }
.tab-content { min-height: 460px; padding: 20px; }
.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 16px; }
.section-heading h2 { font-size: 17px; }.section-heading p { margin-top: 5px; color: var(--muted); font-size: 12px; }
.pool-layout { display: grid; grid-template-columns: 252px minmax(0, 1fr); min-height: 430px; border: 1px solid var(--line); border-radius: 11px; overflow: hidden; }
.pool-sidebar { padding: 8px; border-right: 1px solid var(--line); background: #f7f9fa; }
.pool-item { display: grid; grid-template-columns: 9px 1fr 16px; align-items: center; gap: 9px; width: 100%; min-height: 62px; margin-bottom: 5px; padding: 10px; border: 1px solid transparent; border-radius: 8px; text-align: left; background: transparent; cursor: pointer; }
.pool-item:hover { background: #fff; }.pool-item.active { border-color: #bfe1dc; background: #fff; box-shadow: 0 5px 16px rgba(25,69,70,.05); }
.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 16px; }
.section-heading h2 { font-size: 17px; }.section-heading p { margin-top: 5px; color: var(--muted); font-size: 12px; }
.section-heading-actions { display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.section-heading-actions .el-button + .el-button { margin-left: 0; }
.selection-count { color: #117f75; font-size: 11px; font-weight: 600; }
.pool-layout { display: grid; grid-template-columns: 252px minmax(0, 1fr); min-height: 430px; border: 1px solid var(--line); border-radius: 11px; overflow: hidden; }
.pool-sidebar { padding: 8px; border-right: 1px solid var(--line); background: #f7f9fa; }
.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; }
.pool-item:hover { background: #fff; }.pool-select-row.active .pool-item { border-color: #bfe1dc; background: #fff; box-shadow: 0 5px 16px rgba(25,69,70,.05); }
.pool-item strong, .pool-item small { display: block; }.pool-item strong { overflow: hidden; color: #253348; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }.pool-item small { margin-top: 4px; color: #8c98a7; font-size: 10px; }
.pool-status { width: 8px; height: 8px; border-radius: 50%; }.pool-status.online { background: #18a277; }.pool-status.offline { background: #aab3bf; }.pool-item > .el-icon { color: #9ca7b4; }
.pool-main { min-width: 0; }.pool-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 78px; padding: 12px 15px; border-bottom: 1px solid var(--line); }
@@ -1113,6 +1305,14 @@ h1, h2, h3, p { margin: 0; }
.member-tree-option__count { flex: 0 0 auto; color: #98a2b3; font-size: 11px; }
.member-tree-option__detail { overflow: hidden; color: #8b97a6; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
.form-grid, .rule-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 14px; }.form-tip { display: block; margin-top: 5px; color: #8b97a6; font-size: 10px; }.link-form .el-select, .link-form .el-input-number { width: 100%; }
@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; }.update-time { display: none; }.metric-grid, .customer-metric-grid, .form-grid, .rule-form-grid { grid-template-columns: 1fr; }.tab-nav { overflow-x: auto; }.tab-nav button { min-width: 112px; }.tab-content { padding: 14px; }.pool-layout { grid-template-columns: 1fr; }.pool-sidebar { display: flex; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--line); }.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-form { margin-top: 16px; }
.access-pool-preview { padding: 12px 14px; border: 1px solid var(--line); border-radius: 8px; background: #f8fafb; }
.access-pool-preview > strong { display: block; margin-bottom: 7px; font-size: 12px; }
.access-pool-preview > div { display: grid; grid-template-columns: minmax(120px, .8fr) minmax(0, 1.6fr); gap: 12px; padding: 6px 0; border-top: 1px solid #edf0f2; font-size: 11px; }
.access-pool-preview span { overflow: hidden; color: #344054; text-overflow: ellipsis; white-space: nowrap; }
.access-pool-preview small, .access-pool-preview p { color: #8491a2; font-size: 10px; }
.access-pool-preview small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.access-pool-preview p { margin-top: 7px; }
@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 { grid-template-columns: 1fr; }.tab-nav { overflow-x: auto; }.tab-nav button { min-width: 112px; }.tab-content { padding: 14px; }.pool-layout { grid-template-columns: 1fr; }.pool-sidebar { display: flex; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--line); }.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>