更新
This commit is contained in:
@@ -293,6 +293,42 @@ export function wecomPromotionBatchSetOperators(params: WecomPromotionBatchSetOp
|
||||
})
|
||||
}
|
||||
|
||||
export interface WecomPromotionBatchUpdatePoolsParams {
|
||||
pool_ids: number[]
|
||||
changes: {
|
||||
skip_verify?: 0 | 1
|
||||
fallback_url?: string
|
||||
status?: 0 | 1
|
||||
automation_config?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface WecomPromotionBatchUpdatePoolResult {
|
||||
id: number
|
||||
name: string
|
||||
success: boolean
|
||||
sync_error?: string
|
||||
sync_queued?: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface WecomPromotionBatchUpdatePoolsResult {
|
||||
pool_ids: number[]
|
||||
updated: number
|
||||
failed: number
|
||||
sync_error_count: number
|
||||
sync_queued_count: number
|
||||
results: WecomPromotionBatchUpdatePoolResult[]
|
||||
}
|
||||
|
||||
export function wecomPromotionBatchUpdatePools(params: WecomPromotionBatchUpdatePoolsParams) {
|
||||
return request.post<WecomPromotionBatchUpdatePoolsResult>({
|
||||
url: '/firstvisit.wecomPromotion/batchUpdatePools',
|
||||
params,
|
||||
timeout: 120000
|
||||
}, { ignoreCancelToken: true })
|
||||
}
|
||||
|
||||
export function wecomPromotionDeletePool(params: { id: number }) {
|
||||
return request.post({ url: '/firstvisit.wecomPromotion/deletePool', params, timeout: 120000 })
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ export function qywxCustomerLists(params: any) {
|
||||
return request.get({ url: '/qywx.customer/lists', params })
|
||||
}
|
||||
|
||||
// 删除一条本地企业微信客户同步记录
|
||||
export function qywxCustomerDelete(params: { id: number }) {
|
||||
return request.post({ url: '/qywx.customer/delete', params })
|
||||
}
|
||||
|
||||
// 同步企业微信客户
|
||||
export function qywxCustomerSync() {
|
||||
return request.post({ url: '/qywx.customer/sync' })
|
||||
|
||||
+299
-21
@@ -94,15 +94,32 @@
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进人">
|
||||
<el-input
|
||||
v-model="queryParams.follow_user"
|
||||
<el-form-item label="跟进人">
|
||||
<el-input
|
||||
v-model="queryParams.follow_user"
|
||||
placeholder="跟进人姓名(后台姓名或企微账号)"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="添加时间">
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="渠道">
|
||||
<el-select
|
||||
v-model="queryParams.add_way"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="选择或搜索添加渠道"
|
||||
style="width: 240px"
|
||||
@change="resetPage"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in ADD_WAY_OPTIONS"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="添加时间">
|
||||
<el-date-picker
|
||||
v-model="addTimeRange"
|
||||
type="daterange"
|
||||
@@ -235,6 +252,32 @@
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="添加渠道" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div v-if="customerAddSources(row).length" class="flex items-center gap-1">
|
||||
<el-tooltip
|
||||
v-for="source in customerAddSources(row).slice(0, 1)"
|
||||
:key="source.key"
|
||||
:content="addSourceTooltip(source)"
|
||||
placement="top"
|
||||
>
|
||||
<span class="inline-block max-w-[150px] truncate align-middle">
|
||||
{{ source.label }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="customerAddSources(row).length > 1"
|
||||
:content="remainingAddSourcesTooltip(row)"
|
||||
placement="top"
|
||||
>
|
||||
<span class="text-primary whitespace-nowrap cursor-help">
|
||||
另 {{ customerAddSources(row).length - 1 }} 条
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">未记录</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="添加时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(firstExternalAddTime(row)) }}
|
||||
@@ -245,9 +288,19 @@
|
||||
{{ formatTime(row.update_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="viewDetail(row)">查看详情</el-button>
|
||||
<el-button
|
||||
v-perms="['qywx.customer/delete']"
|
||||
type="danger"
|
||||
link
|
||||
:loading="deletingCustomerId === Number(row.id)"
|
||||
:disabled="deletingCustomerId !== null"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -500,6 +553,21 @@
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="添加时间" :span="2">
|
||||
{{ formatTime(firstExternalAddTime(currentCustomer)) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="添加渠道" :span="2">
|
||||
<div v-if="customerAddSources(currentCustomer).length" class="flex flex-wrap gap-1">
|
||||
<el-tooltip
|
||||
v-for="source in customerAddSources(currentCustomer)"
|
||||
:key="source.key"
|
||||
:content="addSourceTooltip(source)"
|
||||
placement="top"
|
||||
>
|
||||
<el-tag size="small" type="info" effect="plain">
|
||||
{{ source.label }}
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">未记录</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">
|
||||
{{ formatTime(currentCustomer.update_time) }}
|
||||
@@ -549,8 +617,9 @@ import { Refresh, Setting, DataLine, CollectionTag } from '@element-plus/icons-v
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
import {
|
||||
qywxCustomerLists,
|
||||
qywxCustomerSync,
|
||||
qywxCustomerLists,
|
||||
qywxCustomerDelete,
|
||||
qywxCustomerSync,
|
||||
qywxCustomerStats,
|
||||
qywxSyncSettingsGet,
|
||||
qywxSyncSettingsSave,
|
||||
@@ -563,6 +632,7 @@ const syncing = ref(false)
|
||||
const showSyncSettings = ref(false)
|
||||
const showDetail = ref(false)
|
||||
const currentCustomer = ref<any>(null)
|
||||
const deletingCustomerId = ref<number | null>(null)
|
||||
|
||||
const stats = reactive({
|
||||
total: 0,
|
||||
@@ -591,17 +661,19 @@ const syncSettings = reactive({
|
||||
interval: 3600
|
||||
})
|
||||
|
||||
const queryParams = reactive<{
|
||||
name: string
|
||||
follow_user: string
|
||||
tag_ids: string[]
|
||||
const queryParams = reactive<{
|
||||
name: string
|
||||
follow_user: string
|
||||
add_way: number | ''
|
||||
tag_ids: string[]
|
||||
add_time_start: string
|
||||
add_time_end: string
|
||||
dedupe_mode: 'first' | 'any'
|
||||
}>({
|
||||
name: '',
|
||||
follow_user: '',
|
||||
tag_ids: [],
|
||||
name: '',
|
||||
follow_user: '',
|
||||
add_way: '',
|
||||
tag_ids: [],
|
||||
add_time_start: '',
|
||||
add_time_end: '',
|
||||
dedupe_mode: 'first'
|
||||
@@ -639,6 +711,22 @@ interface TagStatsPayload {
|
||||
groups: TagGroup[]
|
||||
}
|
||||
|
||||
interface AddChannel {
|
||||
state: string
|
||||
label: string
|
||||
source_type: 'promotion_pool' | 'state'
|
||||
pool_id: number
|
||||
user_id: string
|
||||
event_time: number
|
||||
}
|
||||
|
||||
interface AddSource extends AddChannel {
|
||||
key: string
|
||||
add_way: number | null
|
||||
channel_label: string
|
||||
staff_name: string
|
||||
}
|
||||
|
||||
const tagStats = reactive<TagStatsPayload>({
|
||||
total_tags: 0,
|
||||
total_relations: 0,
|
||||
@@ -847,10 +935,11 @@ const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
function handleReset() {
|
||||
queryParams.name = ''
|
||||
queryParams.follow_user = ''
|
||||
queryParams.tag_ids = []
|
||||
function handleReset() {
|
||||
queryParams.name = ''
|
||||
queryParams.follow_user = ''
|
||||
queryParams.add_way = ''
|
||||
queryParams.tag_ids = []
|
||||
queryParams.add_time_start = ''
|
||||
queryParams.add_time_end = ''
|
||||
queryParams.dedupe_mode = 'first'
|
||||
@@ -964,6 +1053,34 @@ function viewDetail(row: any) {
|
||||
showDetail.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(row: Record<string, any>) {
|
||||
const id = Number(row.id)
|
||||
if (!Number.isInteger(id) || id <= 0 || deletingCustomerId.value !== null) return
|
||||
|
||||
const customerName = String(row.name || row.external_userid || '该客户')
|
||||
try {
|
||||
await feedback.confirm(
|
||||
`确定删除企业微信客户“${customerName}”吗?此操作仅删除系统内的同步记录,不会删除企业微信中的客户关系;后续重新同步时可能再次出现。`
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
deletingCustomerId.value = id
|
||||
try {
|
||||
await qywxCustomerDelete({ id })
|
||||
if (pager.page > 1 && pager.lists.length === 1) {
|
||||
pager.page -= 1
|
||||
}
|
||||
await Promise.all([getLists(), loadStats(), loadTagStats()])
|
||||
feedback.msgSuccess('删除成功')
|
||||
} catch (e: any) {
|
||||
feedback.msgError(e?.message || e?.msg || '删除失败')
|
||||
} finally {
|
||||
deletingCustomerId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 列表接口会写入 admin_name(admin.work_wechat_userid = userid) */
|
||||
function formatFollowUser(user: Record<string, any>) {
|
||||
const adminName = String(user?.admin_name ?? '').trim()
|
||||
@@ -985,6 +1102,167 @@ function followStaffTooltip(user: Record<string, any>) {
|
||||
return parts.join('|')
|
||||
}
|
||||
|
||||
function customerAddChannels(row: Record<string, any> | null | undefined): AddChannel[] {
|
||||
if (!row) return []
|
||||
if (Array.isArray(row.add_channels)) {
|
||||
return row.add_channels
|
||||
.map((channel: Record<string, any>): AddChannel => ({
|
||||
state: String(channel?.state ?? '').trim(),
|
||||
label: String(channel?.label ?? channel?.state ?? '').trim(),
|
||||
source_type: channel?.source_type === 'promotion_pool' ? 'promotion_pool' : 'state',
|
||||
pool_id: Number(channel?.pool_id ?? 0),
|
||||
user_id: String(channel?.user_id ?? '').trim(),
|
||||
event_time: Number(channel?.event_time ?? 0)
|
||||
}))
|
||||
.filter((channel: AddChannel) => channel.state !== '')
|
||||
}
|
||||
|
||||
// 兼容仅返回原始渠道数组的旧接口/灰度节点。
|
||||
if (!Array.isArray(row.add_channel_states)) return []
|
||||
return row.add_channel_states
|
||||
.map((state: unknown) => String(state ?? '').trim())
|
||||
.filter((state: string) => state !== '')
|
||||
.map((state: string) => ({
|
||||
state,
|
||||
label: state,
|
||||
source_type: 'state' as const,
|
||||
pool_id: 0,
|
||||
user_id: '',
|
||||
event_time: 0
|
||||
}))
|
||||
}
|
||||
|
||||
const ADD_WAY_LABELS: Record<number, string> = {
|
||||
0: '未知添加方式',
|
||||
1: '通过扫描二维码添加',
|
||||
2: '通过搜索手机号添加',
|
||||
3: '通过名片分享添加',
|
||||
4: '通过群聊添加',
|
||||
5: '通过手机通讯录添加',
|
||||
6: '通过微信联系人添加',
|
||||
8: '安装第三方应用时自动添加',
|
||||
9: '通过搜索邮箱添加',
|
||||
10: '通过视频号添加',
|
||||
11: '通过日程参与人添加',
|
||||
12: '通过会议参与人添加',
|
||||
13: '通过微信好友添加',
|
||||
14: '通过智慧硬件专属客服添加',
|
||||
15: '通过上门服务客服添加',
|
||||
16: '通过获客链接添加',
|
||||
17: '通过定制开发添加',
|
||||
18: '通过需求回复添加',
|
||||
21: '通过第三方售前客服添加',
|
||||
22: '通过可能的商务伙伴添加',
|
||||
24: '通过接受微信好友申请添加',
|
||||
201: '通过内部成员共享添加',
|
||||
202: '通过管理员或负责人分配添加'
|
||||
}
|
||||
|
||||
const ADD_WAY_OPTIONS = Object.entries(ADD_WAY_LABELS).map(([value, label]) => ({
|
||||
value: Number(value),
|
||||
label
|
||||
}))
|
||||
|
||||
function normalizeAddWay(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isInteger(value) && value >= 0) return value
|
||||
if (typeof value !== 'string' || !/^\d+$/.test(value.trim())) return null
|
||||
return Number(value.trim())
|
||||
}
|
||||
|
||||
function addWayLabel(addWay: number) {
|
||||
return ADD_WAY_LABELS[addWay] || `其他添加方式(${addWay})`
|
||||
}
|
||||
|
||||
function customerAddSources(row: Record<string, any> | null | undefined): AddSource[] {
|
||||
if (!row) return []
|
||||
|
||||
const channels = customerAddChannels(row)
|
||||
const usedChannelIndexes = new Set<number>()
|
||||
const sources: AddSource[] = []
|
||||
const followUsers = Array.isArray(row.follow_users) ? row.follow_users : []
|
||||
|
||||
followUsers.forEach((user: Record<string, any>, index: number) => {
|
||||
const userId = String(user?.userid ?? user?.UserId ?? '').trim()
|
||||
const state = String(user?.state ?? user?.State ?? '').trim()
|
||||
const addWay = normalizeAddWay(user?.add_way ?? user?.AddWay)
|
||||
|
||||
let channelIndex = channels.findIndex(
|
||||
(channel, i) =>
|
||||
!usedChannelIndexes.has(i) &&
|
||||
userId !== '' &&
|
||||
state !== '' &&
|
||||
channel.user_id === userId &&
|
||||
channel.state === state
|
||||
)
|
||||
if (channelIndex < 0 && state !== '') {
|
||||
channelIndex = channels.findIndex(
|
||||
(channel, i) => !usedChannelIndexes.has(i) && channel.state === state
|
||||
)
|
||||
}
|
||||
if (channelIndex < 0 && userId !== '') {
|
||||
channelIndex = channels.findIndex(
|
||||
(channel, i) => !usedChannelIndexes.has(i) && channel.user_id === userId
|
||||
)
|
||||
}
|
||||
|
||||
const channel = channelIndex >= 0 ? channels[channelIndex] : undefined
|
||||
if (channelIndex >= 0) usedChannelIndexes.add(channelIndex)
|
||||
if (addWay === null && state === '' && !channel) return
|
||||
|
||||
const labelFromApi = String(user?.add_way_label ?? '').trim()
|
||||
const sourceType = channel?.source_type ?? (/^zyt_pool:[1-9]\d*$/.test(state) ? 'promotion_pool' : 'state')
|
||||
const label = labelFromApi || (addWay !== null
|
||||
? addWayLabel(addWay)
|
||||
: sourceType === 'promotion_pool'
|
||||
? '通过获客链接添加'
|
||||
: '通过其他渠道添加')
|
||||
|
||||
sources.push({
|
||||
key: `follow:${index}:${userId}:${addWay ?? 'unknown'}:${state}`,
|
||||
add_way: addWay,
|
||||
label,
|
||||
state: state || channel?.state || '',
|
||||
channel_label: channel?.label || '',
|
||||
source_type: sourceType,
|
||||
pool_id: channel?.pool_id || 0,
|
||||
user_id: userId || channel?.user_id || '',
|
||||
staff_name: formatFollowUser(user),
|
||||
event_time: channel?.event_time || Number(user?.createtime ?? 0)
|
||||
})
|
||||
})
|
||||
|
||||
// 兼容事件日志中仍有记录、但当前 follow_users 已不存在或旧接口未返回 add_way 的客户。
|
||||
channels.forEach((channel, index) => {
|
||||
if (usedChannelIndexes.has(index)) return
|
||||
sources.push({
|
||||
...channel,
|
||||
key: `channel:${index}:${channel.user_id}:${channel.state}`,
|
||||
add_way: channel.source_type === 'promotion_pool' ? 16 : null,
|
||||
label: channel.source_type === 'promotion_pool' ? '通过获客链接添加' : '通过其他渠道添加',
|
||||
channel_label: channel.label,
|
||||
staff_name: channel.user_id || '—'
|
||||
})
|
||||
})
|
||||
|
||||
return sources.sort((a, b) => b.event_time - a.event_time)
|
||||
}
|
||||
|
||||
function addSourceTooltip(source: AddSource) {
|
||||
const parts: string[] = []
|
||||
parts.push(`添加方式:${source.label}`)
|
||||
if (source.source_type === 'promotion_pool' && source.channel_label) {
|
||||
parts.push(`获客助手方案:${source.channel_label}`)
|
||||
}
|
||||
if (source.staff_name && source.staff_name !== '—') parts.push(`跟进人:${source.staff_name}`)
|
||||
if (source.event_time > 0) parts.push(`添加时间:${formatTime(source.event_time)}`)
|
||||
if (source.state) parts.push(`渠道参数:${source.state}`)
|
||||
return parts.join('|')
|
||||
}
|
||||
|
||||
function remainingAddSourcesTooltip(row: Record<string, any>) {
|
||||
return customerAddSources(row).slice(1).map(addSourceTooltip).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加时间:优先接口字段 external_first_add_time(同步写入 + 列表对未回填行按 JSON 兜底);
|
||||
* 再解析 follow_users;最后退回 create_time
|
||||
|
||||
+53
-31
@@ -2,9 +2,10 @@
|
||||
<div class="automation-form">
|
||||
<el-alert class="automation-note" type="info" show-icon :closable="false" title="自动化设置只作用于之后新添加的客户,不会写入企微获客链接详情中的“欢迎语/客户标签”配置。" description="系统会在客户添加回调中立即发送渠道欢迎语并添加标签,后台任务负责失败重试及其他补偿。测试时请使用系统复制的、带渠道参数的链接。" />
|
||||
|
||||
<section class="automation-section" :class="{ 'is-disabled': receptionDisabled }">
|
||||
<h3 class="form-section-title">接待设置</h3>
|
||||
<el-form-item label="接待模式">
|
||||
<el-radio-group v-model="config.reception_mode" :disabled="disabled">
|
||||
<el-radio-group v-model="config.reception_mode" :disabled="receptionDisabled">
|
||||
<el-radio value="always">全天接待</el-radio>
|
||||
<el-radio value="scheduled">按星期时段自动上下线</el-radio>
|
||||
</el-radio-group>
|
||||
@@ -12,50 +13,52 @@
|
||||
</el-form-item>
|
||||
<div v-if="config.reception_mode === 'scheduled'" class="reception-schedules">
|
||||
<div v-for="(slot, index) in config.reception_schedule" :key="index" class="schedule-card">
|
||||
<div class="schedule-heading"><strong>接待时段 {{ index + 1 }}</strong><el-button type="danger" text size="small" :disabled="disabled" @click="config.reception_schedule.splice(index, 1)">删除时段</el-button></div>
|
||||
<el-checkbox-group v-model="slot.weekdays" class="weekday-select" :disabled="disabled"><el-checkbox v-for="(day, i) in weekdays" :key="day" :value="i + 1">{{ day }}</el-checkbox></el-checkbox-group>
|
||||
<div class="time-row"><el-time-picker v-model="slot.start" :disabled="disabled" format="HH:mm" value-format="HH:mm" placeholder="开始时间" :clearable="false" /><span>至</span><el-time-picker v-model="slot.end" :disabled="disabled" format="HH:mm" value-format="HH:mm" placeholder="结束时间" :clearable="false" /><small v-if="slot.end < slot.start">次日结束</small></div>
|
||||
<el-select v-model="slot.member_admin_ids" :disabled="disabled" multiple filterable clearable placeholder="从上方主接待成员中选择" style="width: 100%">
|
||||
<div class="schedule-heading"><strong>接待时段 {{ index + 1 }}</strong><el-button type="danger" text size="small" :disabled="receptionDisabled" @click="config.reception_schedule.splice(index, 1)">删除时段</el-button></div>
|
||||
<el-checkbox-group v-model="slot.weekdays" class="weekday-select" :disabled="receptionDisabled"><el-checkbox v-for="(day, i) in weekdays" :key="day" :value="i + 1">{{ day }}</el-checkbox></el-checkbox-group>
|
||||
<div class="time-row"><el-time-picker v-model="slot.start" :disabled="receptionDisabled" format="HH:mm" value-format="HH:mm" placeholder="开始时间" :clearable="false" /><span>至</span><el-time-picker v-model="slot.end" :disabled="receptionDisabled" format="HH:mm" value-format="HH:mm" placeholder="结束时间" :clearable="false" /><small v-if="slot.end < slot.start">次日结束</small></div>
|
||||
<el-select v-model="slot.member_admin_ids" :disabled="receptionDisabled" multiple filterable clearable placeholder="从上方主接待成员中选择" style="width: 100%">
|
||||
<el-option v-for="member in mainMembers" :key="member.id" :value="Number(member.id)" :label="memberLabel(member)" />
|
||||
</el-select>
|
||||
<p v-if="slot.member_admin_ids.some((id) => !mainMemberIds.includes(id))" class="inline-error">该时段含已从主接待移除的成员,请重新选择。</p>
|
||||
</div>
|
||||
<el-button :icon="Plus" :disabled="disabled || config.reception_schedule.length >= 30" @click="addReceptionSlot">添加接待时段</el-button>
|
||||
<el-button :icon="Plus" :disabled="receptionDisabled || config.reception_schedule.length >= 30" @click="addReceptionSlot">添加接待时段</el-button>
|
||||
<p class="field-help">最多 30 个时段。跨午夜时段归属开始日,例如星期一 22:00 至 02:00 包含星期二凌晨;接待时段重叠时取成员并集。</p>
|
||||
</div>
|
||||
<el-form-item label="备用成员" :required="config.reception_mode === 'scheduled'">
|
||||
<el-select v-model="config.backup_member_admin_ids" :disabled="disabled" multiple filterable clearable collapse-tags collapse-tags-tooltip :max-collapse-tags="3" :multiple-limit="500" placeholder="主接待成员均不可用时由备用成员接待" style="width: 100%">
|
||||
<el-option v-for="member in members" :key="member.id" :value="Number(member.id)" :label="memberLabel(member)" :disabled="mainMemberIds.includes(Number(member.id))" />
|
||||
<el-select v-model="config.backup_member_admin_ids" :disabled="receptionDisabled" multiple filterable clearable collapse-tags collapse-tags-tooltip :max-collapse-tags="3" :multiple-limit="500" placeholder="主接待成员均不可用时由备用成员接待" style="width: 100%">
|
||||
<el-option v-for="member in members" :key="member.id" :value="Number(member.id)" :label="memberLabel(member)" :disabled="backupExcludedIds.includes(Number(member.id))" />
|
||||
<el-option v-for="id in missingBackupIds" :key="`missing-${id}`" :value="id" :label="`成员 ${id}(当前不可选,请移除后重新选择)`" disabled />
|
||||
</el-select>
|
||||
<p class="field-help">备用成员不能与主接待重复。按时段模式至少配置一名备用成员;仅当无可用主接待时进入官方成员范围。</p>
|
||||
</el-form-item>
|
||||
</section>
|
||||
|
||||
<section class="automation-section" :class="{ 'is-disabled': customerDisabled }">
|
||||
<h3 class="form-section-title">客户设置</h3>
|
||||
<el-form-item label="自动添加客户标签">
|
||||
<el-switch v-model="config.tags_enabled" :disabled="disabled || tagsCreating" />
|
||||
<el-switch v-model="config.tags_enabled" :disabled="customerDisabled || tagsCreating" />
|
||||
<div v-if="hasMultipleTags" class="legacy-tags-warning full-width" role="alert">
|
||||
<p>原方案设置了多个标签:{{ selectedTagNames }}。现在仅支持单选,请重新选择一个标签,或清空原标签。</p>
|
||||
<el-button size="small" :disabled="disabled || tagsCreating" @click="selectedTag = ''">清空原标签</el-button>
|
||||
<el-button size="small" :disabled="customerDisabled || tagsCreating" @click="selectedTag = ''">清空原标签</el-button>
|
||||
</div>
|
||||
<div v-if="config.tags_enabled" class="full-width tags-content">
|
||||
<div class="tag-select-row">
|
||||
<el-select v-model="selectedTag" :disabled="disabled || tagsCreating" :loading="tagsLoading" filterable clearable placeholder="选择一个企业微信客户标签" aria-label="企业微信客户标签" class="tag-select">
|
||||
<el-select v-model="selectedTag" :disabled="customerDisabled || tagsCreating" :loading="tagsLoading" filterable clearable placeholder="选择一个企业微信客户标签" aria-label="企业微信客户标签" class="tag-select">
|
||||
<el-option-group v-for="group in tagGroups" :key="group.group_id" :label="group.group_name">
|
||||
<el-option v-for="tag in group.tag" :key="tag.id" :value="tag.id" :label="tag.name" />
|
||||
</el-option-group>
|
||||
<el-option-group v-if="unknownTagIds.length" label="已选标签(名称暂不可用)"><el-option v-for="id in unknownTagIds" :key="id" :value="id" :label="`已选标签 · ${id}`" /></el-option-group>
|
||||
</el-select>
|
||||
<el-button :icon="Plus" :disabled="disabled || tagsCreating" @click="showCustomTag = !showCustomTag">自定义标签</el-button>
|
||||
<el-button :icon="Refresh" :disabled="disabled || tagsCreating" :loading="tagsLoading" @click="loadTags">{{ tagsError ? '重试' : '刷新标签' }}</el-button>
|
||||
<el-button :icon="Plus" :disabled="customerDisabled || tagsCreating" @click="showCustomTag = !showCustomTag">自定义标签</el-button>
|
||||
<el-button :icon="Refresh" :disabled="customerDisabled || tagsCreating" :loading="tagsLoading" @click="loadTags">{{ tagsError ? '重试' : '刷新标签' }}</el-button>
|
||||
</div>
|
||||
<p v-if="tagsError" role="alert" class="inline-error">{{ tagsError }} 已保留原有标签,点击“重试”重新加载。</p>
|
||||
<p v-else class="field-help">每个方案只选一个标签,可选择已有企业微信标签,也可自定义创建。客户添加成功后由系统调用企微接口打标,不会显示在企微获客链接详情的“客户标签”配置中。</p>
|
||||
<div v-if="showCustomTag" class="custom-tag-editor">
|
||||
<label for="promotion-custom-tag-name">自定义标签名称</label>
|
||||
<div class="custom-tag-row">
|
||||
<el-input id="promotion-custom-tag-name" v-model="customTagName" :disabled="disabled || tagsCreating" maxlength="30" show-word-limit placeholder="例如:官网咨询" @input="customTagError = ''" @keydown.enter.prevent="createCustomTag" />
|
||||
<el-button type="primary" :disabled="disabled || tagsLoading" :loading="tagsCreating" @click="createCustomTag">创建并选用</el-button>
|
||||
<el-input id="promotion-custom-tag-name" v-model="customTagName" :disabled="customerDisabled || tagsCreating" maxlength="30" show-word-limit placeholder="例如:官网咨询" @input="customTagError = ''" @keydown.enter.prevent="createCustomTag" />
|
||||
<el-button type="primary" :disabled="customerDisabled || tagsLoading" :loading="tagsCreating" @click="createCustomTag">创建并选用</el-button>
|
||||
</div>
|
||||
<p class="field-help">创建到企业微信“推广渠道”分组,同组同名标签会复用。创建后即保存到企微标签库,取消方案编辑不会删除标签。</p>
|
||||
<p v-if="customTagError" role="alert" class="inline-error">{{ customTagError }} 原有选择未改变。</p>
|
||||
@@ -64,22 +67,24 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="自动设置客户备注">
|
||||
<el-switch v-model="config.remark_enabled" :disabled="disabled" />
|
||||
<el-switch v-model="config.remark_enabled" :disabled="customerDisabled" />
|
||||
<div v-if="config.remark_enabled" class="full-width remark-content">
|
||||
<div class="token-buttons"><el-button v-for="token in templateTokens" :key="token.value" size="small" :disabled="disabled" @click="insertRemark(token.value)">插入{{ token.label }}</el-button></div>
|
||||
<el-input ref="remarkInput" v-model="config.remark_template" :disabled="disabled" maxlength="200" show-word-limit placeholder="例如:官网-{customer_name}" @select="rememberRemarkSelection" @keyup="rememberRemarkSelection" @click="rememberRemarkSelection" @blur="rememberRemarkSelection" />
|
||||
<div class="token-buttons"><el-button v-for="token in templateTokens" :key="token.value" size="small" :disabled="customerDisabled" @click="insertRemark(token.value)">插入{{ token.label }}</el-button></div>
|
||||
<el-input ref="remarkInput" v-model="config.remark_template" :disabled="customerDisabled" maxlength="200" show-word-limit placeholder="例如:官网-{customer_name}" @select="rememberRemarkSelection" @keyup="rememberRemarkSelection" @click="rememberRemarkSelection" @blur="rememberRemarkSelection" />
|
||||
<div class="remark-preview"><span>备注预览</span><strong>{{ remarkPreview || '—' }}</strong><small>{{ Array.from(remarkPreview).length }}/20 字</small></div>
|
||||
<p class="field-help">示例客户:张女士;员工:{{ employeeName }}。添加时间格式为 YYYY-MM-DD,生成后的备注最多保留前 20 字。</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="自动设置客户描述">
|
||||
<el-switch v-model="config.description_enabled" :disabled="disabled" />
|
||||
<el-input v-if="config.description_enabled" v-model="config.description" class="description-input" :disabled="disabled" type="textarea" :rows="3" maxlength="150" show-word-limit placeholder="请输入客户描述,最多 150 字" />
|
||||
<el-switch v-model="config.description_enabled" :disabled="customerDisabled" />
|
||||
<el-input v-if="config.description_enabled" v-model="config.description" class="description-input" :disabled="customerDisabled" type="textarea" :rows="3" maxlength="150" show-word-limit placeholder="请输入客户描述,最多 150 字" />
|
||||
</el-form-item>
|
||||
</section>
|
||||
|
||||
<section class="automation-section" :class="{ 'is-disabled': welcomeDisabled }">
|
||||
<h3 class="form-section-title">欢迎语设置</h3>
|
||||
<el-form-item label="欢迎语模式">
|
||||
<el-radio-group v-model="config.welcome_mode" :disabled="disabled || anyUploading">
|
||||
<el-radio-group v-model="config.welcome_mode" :disabled="welcomeDisabled || anyUploading">
|
||||
<el-radio value="channel">渠道欢迎语</el-radio>
|
||||
<el-radio value="default">默认欢迎语</el-radio>
|
||||
<el-radio value="none">不发送欢迎语</el-radio>
|
||||
@@ -90,20 +95,21 @@
|
||||
</el-form-item>
|
||||
<template v-if="config.welcome_mode === 'channel'">
|
||||
<div class="welcome-block"><h4>基础渠道欢迎语</h4><p class="field-help">未开启分时欢迎语,或新客户添加时间未匹配任何时段时,使用以下内容。</p>
|
||||
<WelcomeMessageEditor v-model="config.welcome" :disabled="disabled" :employee-name="employeeName" @busy="(busy) => updateBusy('basic', busy)" />
|
||||
<WelcomeMessageEditor v-model="config.welcome" :disabled="welcomeDisabled" :employee-name="employeeName" @busy="(busy) => updateBusy('basic', busy)" />
|
||||
</div>
|
||||
<el-form-item class="schedule-switch" label="分时欢迎语"><el-switch v-model="config.welcome_schedule_enabled" :disabled="disabled || anyUploading" /><span class="switch-help">按客户添加时的北京时间匹配,时段不能重叠</span></el-form-item>
|
||||
<el-form-item class="schedule-switch" label="分时欢迎语"><el-switch v-model="config.welcome_schedule_enabled" :disabled="welcomeDisabled || anyUploading" /><span class="switch-help">按客户添加时的北京时间匹配,时段不能重叠</span></el-form-item>
|
||||
<div v-if="config.welcome_schedule_enabled">
|
||||
<div v-for="(slot, index) in config.welcome_schedule" :key="index" class="schedule-card welcome-schedule">
|
||||
<div class="schedule-heading"><strong>欢迎语时段 {{ index + 1 }}</strong><el-button type="danger" text size="small" :disabled="disabled || anyUploading" @click="removeWelcomeSlot(index)">删除时段</el-button></div>
|
||||
<el-checkbox-group v-model="slot.weekdays" class="weekday-select" :disabled="disabled"><el-checkbox v-for="(day, i) in weekdays" :key="day" :value="i + 1">{{ day }}</el-checkbox></el-checkbox-group>
|
||||
<div class="time-row"><el-time-picker v-model="slot.start" :disabled="disabled" format="HH:mm" value-format="HH:mm" placeholder="开始时间" :clearable="false" /><span>至</span><el-time-picker v-model="slot.end" :disabled="disabled" format="HH:mm" value-format="HH:mm" placeholder="结束时间" :clearable="false" /><small v-if="slot.end < slot.start">次日结束</small></div>
|
||||
<WelcomeMessageEditor :model-value="slot" :disabled="disabled" :employee-name="employeeName" @update:model-value="(message) => Object.assign(slot, message)" @busy="(busy) => updateBusy(`slot-${index}`, busy)" />
|
||||
<div class="schedule-heading"><strong>欢迎语时段 {{ index + 1 }}</strong><el-button type="danger" text size="small" :disabled="welcomeDisabled || anyUploading" @click="removeWelcomeSlot(index)">删除时段</el-button></div>
|
||||
<el-checkbox-group v-model="slot.weekdays" class="weekday-select" :disabled="welcomeDisabled"><el-checkbox v-for="(day, i) in weekdays" :key="day" :value="i + 1">{{ day }}</el-checkbox></el-checkbox-group>
|
||||
<div class="time-row"><el-time-picker v-model="slot.start" :disabled="welcomeDisabled" format="HH:mm" value-format="HH:mm" placeholder="开始时间" :clearable="false" /><span>至</span><el-time-picker v-model="slot.end" :disabled="welcomeDisabled" format="HH:mm" value-format="HH:mm" placeholder="结束时间" :clearable="false" /><small v-if="slot.end < slot.start">次日结束</small></div>
|
||||
<WelcomeMessageEditor :model-value="slot" :disabled="welcomeDisabled" :employee-name="employeeName" @update:model-value="(message) => Object.assign(slot, message)" @busy="(busy) => updateBusy(`slot-${index}`, busy)" />
|
||||
</div>
|
||||
<el-button :icon="Plus" :disabled="disabled || anyUploading || config.welcome_schedule.length >= 30" @click="addWelcomeSlot">添加欢迎语时段</el-button>
|
||||
<el-button :icon="Plus" :disabled="welcomeDisabled || anyUploading || config.welcome_schedule.length >= 30" @click="addWelcomeSlot">添加欢迎语时段</el-button>
|
||||
<p class="field-help">最多 30 个时段,支持跨午夜。时段外自动使用基础渠道欢迎语,不会随机选择内容。</p>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -116,9 +122,22 @@ import WelcomeMessageEditor from './WelcomeMessageEditor.vue'
|
||||
import { previewTemplate, templateTokens, validateCustomTagName, weekdays } from './promotion-automation'
|
||||
import type { PromotionAutomationConfig, PromotionMemberChoice } from './promotion-automation'
|
||||
|
||||
const props = defineProps<{ modelValue: PromotionAutomationConfig; mainMemberIds: number[]; members: PromotionMemberChoice[]; disabled?: boolean }>()
|
||||
type AutomationSection = 'reception' | 'customer' | 'welcome'
|
||||
const props = defineProps<{
|
||||
modelValue: PromotionAutomationConfig
|
||||
mainMemberIds: number[]
|
||||
members: PromotionMemberChoice[]
|
||||
disabled?: boolean
|
||||
disabledSections?: AutomationSection[]
|
||||
backupExcludedMemberIds?: number[]
|
||||
}>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [config: PromotionAutomationConfig]; busy: [value: boolean] }>()
|
||||
const config = computed({ get: () => props.modelValue, set: (value) => emit('update:modelValue', value) })
|
||||
const sectionDisabled = (section: AutomationSection) => Boolean(props.disabled || props.disabledSections?.includes(section))
|
||||
const receptionDisabled = computed(() => sectionDisabled('reception'))
|
||||
const customerDisabled = computed(() => sectionDisabled('customer'))
|
||||
const welcomeDisabled = computed(() => sectionDisabled('welcome'))
|
||||
const backupExcludedIds = computed(() => props.backupExcludedMemberIds || props.mainMemberIds)
|
||||
const mainMembers = computed(() => props.members.filter((member) => props.mainMemberIds.includes(Number(member.id))))
|
||||
const missingBackupIds = computed(() => config.value.backup_member_admin_ids.filter((id) => !props.members.some((member) => Number(member.id) === id)))
|
||||
const employeeName = computed(() => mainMembers.value[0]?.name || '小陈')
|
||||
@@ -153,7 +172,9 @@ const unknownTagIds = computed(() => {
|
||||
const ids = new Set(tagGroups.value.flatMap((group) => group.tag.map((tag) => tag.id)))
|
||||
return config.value.tag_ids.filter((id) => !ids.has(id))
|
||||
})
|
||||
watch(() => config.value.tags_enabled, (enabled) => { if (enabled && !tagsLoaded.value && !tagsLoading.value) void loadTags() }, { immediate: true })
|
||||
watch([() => config.value.tags_enabled, customerDisabled], ([enabled, sectionIsDisabled]) => {
|
||||
if (enabled && !sectionIsDisabled && !tagsLoaded.value && !tagsLoading.value) void loadTags()
|
||||
}, { immediate: true })
|
||||
function memberLabel(member: PromotionMemberChoice) { return `${member.name} · ${member.dept_names?.join(' / ') || member.userid || '未分部门'}` }
|
||||
function addReceptionSlot() { config.value.reception_schedule.push({ weekdays: [1, 2, 3, 4, 5], start: '09:00', end: '18:00', member_admin_ids: [...props.mainMemberIds] }) }
|
||||
function addWelcomeSlot() { config.value.welcome_schedule.push({ weekdays: [1, 2, 3, 4, 5], start: '09:00', end: '18:00', text: '', attachments: [] }) }
|
||||
@@ -173,7 +194,7 @@ async function loadTags() {
|
||||
} finally { tagsLoading.value = false }
|
||||
}
|
||||
async function createCustomTag() {
|
||||
if (props.disabled || tagsCreating.value || tagsLoading.value) return
|
||||
if (customerDisabled.value || tagsCreating.value || tagsLoading.value) return
|
||||
customTagError.value = validateCustomTagName(customTagName.value)
|
||||
customTagSuccess.value = ''
|
||||
if (customTagError.value) return
|
||||
@@ -215,6 +236,7 @@ onBeforeUnmount(() => emit('busy', false))
|
||||
|
||||
<style scoped>
|
||||
.automation-form { width: 100%; }.automation-note { margin-top: 22px; }.automation-note :deep(.el-alert__description) { line-height: 1.7; }
|
||||
.automation-section { min-width: 0; transition: opacity .2s ease; }.automation-section.is-disabled { opacity: .58; }
|
||||
.form-section-title { margin: 28px 0 18px; padding: 0 0 12px; border-bottom: 1px solid #ebeef5; font-size: 15px; font-weight: 600; color: #303133; }.field-help { width: 100%; font-size: 12px; line-height: 1.7; margin: 6px 0 0; color: #909399; }.warning-help { color: #9f6d14; }.full-width { width: 100%; }.inline-error { width: 100%; color: #d93026; font-size: 12px; line-height: 1.7; margin: 8px 0 0; }
|
||||
.schedule-card { padding: 16px; border: 1px solid #e4e7ed; border-radius: 6px; background: #fafbfd; margin-bottom: 12px; }.schedule-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; font-size: 13px; }.weekday-select { display: flex; flex-wrap: wrap; gap: 0 18px; }.weekday-select :deep(.el-checkbox) { margin-right: 0; }.time-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 12px 0; }.time-row :deep(.el-date-editor.el-input) { width: 150px; }.time-row > span { font-size: 12px; color: #909399; }.time-row > small { font-size: 12px; color: #b88230; }.reception-schedules { margin: 0 0 20px; }
|
||||
.tags-content, .remark-content, .description-input { margin-top: 12px; }.tag-select-row { display: flex; gap: 10px; width: 100%; }.tag-select { flex: 1; min-width: 0; }.token-buttons { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }.token-buttons .el-button + .el-button { margin-left: 0; }.remark-preview { display: flex; gap: 14px; align-items: center; padding: 10px 12px; background: #f5f7fa; margin-top: 8px; border-radius: 4px; line-height: 1.7; }.remark-preview span, .remark-preview small { color: #909399; font-size: 12px; }.remark-preview strong { color: #303133; font-size: 13px; font-weight: 500; overflow-wrap: anywhere; }.remark-preview small { margin-left: auto; white-space: nowrap; }.welcome-block h4 { font-size: 13px; font-weight: 600; margin: 0 0 4px; }.welcome-block > .field-help { margin-bottom: 12px; }.schedule-switch { margin-top: 24px; }.switch-help { margin-left: 12px; color: #909399; font-size: 12px; }.welcome-schedule { background: #fff; }
|
||||
|
||||
@@ -58,6 +58,11 @@
|
||||
</div>
|
||||
<div class="section-heading-actions">
|
||||
<span v-if="selectedPoolIds.length" class="selection-count">已选 {{ selectedPoolIds.length }} 个方案</span>
|
||||
<el-button
|
||||
:icon="Edit"
|
||||
:disabled="!selectedPoolIds.length"
|
||||
@click="openBatchConfigDialog()"
|
||||
>批量修改方案</el-button>
|
||||
<el-button
|
||||
:icon="User"
|
||||
:disabled="!selectedPoolIds.length"
|
||||
@@ -138,7 +143,8 @@
|
||||
description="当前全部可用医助会同时写入官方链接的成员范围,由企业微信在打开和添加阶段直接进行多人路由。回调只用于统计实际承接结果,并在禁用、过期或达到上限后更新成员范围。"
|
||||
/>
|
||||
|
||||
<el-table :data="selectedMemberRules" class="link-table" stripe>
|
||||
<div class="member-table-area">
|
||||
<el-table :data="selectedMemberRules" class="link-table" height="100%" stripe>
|
||||
<el-table-column label="推广成员" min-width="210" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<div class="member-cell">
|
||||
@@ -174,7 +180,8 @@
|
||||
</el-table-column>
|
||||
<template #empty><el-empty :image-size="72" description="编辑方案并选择获客医助" /></template>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="创建方案并选择多个医助,保存后自动生成一个企业微信官方获客链接">
|
||||
<el-button type="primary" :icon="Plus" @click="openPoolDialog()">创建第一个方案</el-button>
|
||||
@@ -429,6 +436,94 @@
|
||||
<template #footer><span v-if="automationBusy" class="uploading-save-tip">正在处理标签或素材,请稍候</span><el-button :disabled="savingPool || automationBusy" @click="poolDialogVisible = false">取消</el-button><el-button type="primary" :loading="savingPool" :disabled="automationBusy" @click="savePool">{{ poolForm.id ? '保存方案' : '保存并生成链接' }}</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="batchConfigDialogVisible"
|
||||
title="批量修改分流方案"
|
||||
width="1000px"
|
||||
class="promotion-pool-dialog"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!savingBatchConfig && !batchConfigBusy"
|
||||
:show-close="!savingBatchConfig && !batchConfigBusy"
|
||||
>
|
||||
<div ref="batchConfigScroll" class="pool-form-scroll batch-config-scroll">
|
||||
<el-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
:title="`将统一修改 ${batchConfigForm.pool_ids.length} 个分流方案`"
|
||||
description="仅勾选的项目会覆盖到所选方案;未勾选项目保留各方案原值。表单初始值取第一个所选方案。"
|
||||
/>
|
||||
<el-alert v-if="batchConfigError" class="pool-form-error batch-config-error" :title="batchConfigError" type="error" show-icon :closable="false" role="alert" />
|
||||
|
||||
<div v-if="batchConfigPools.length" class="batch-pool-summary">
|
||||
<strong>已选方案</strong>
|
||||
<span v-for="pool in batchConfigPools.slice(0, 8)" :key="pool.id">{{ pool.name }}</span>
|
||||
<small v-if="batchConfigPools.length > 8">另有 {{ batchConfigPools.length - 8 }} 个</small>
|
||||
</div>
|
||||
|
||||
<el-form class="batch-config-form" label-position="top" :disabled="savingBatchConfig">
|
||||
<section class="batch-config-section">
|
||||
<h3>基础设置</h3>
|
||||
<div class="batch-field-grid">
|
||||
<div class="batch-field" :class="{ 'is-disabled': !batchConfigApply.skip_verify }">
|
||||
<el-checkbox v-model="batchConfigApply.skip_verify" :disabled="savingBatchConfig">批量修改验证方式</el-checkbox>
|
||||
<el-form-item label="添加客户时跳过验证">
|
||||
<el-switch v-model="batchConfigForm.skip_verify" :disabled="!batchConfigApply.skip_verify || savingBatchConfig" :active-value="1" :inactive-value="0" active-text="跳过验证" inactive-text="需要验证" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="batch-field" :class="{ 'is-disabled': !batchConfigApply.status }">
|
||||
<el-checkbox v-model="batchConfigApply.status" :disabled="savingBatchConfig">批量修改运行状态</el-checkbox>
|
||||
<el-form-item label="运行状态">
|
||||
<el-switch v-model="batchConfigForm.status" :disabled="!batchConfigApply.status || savingBatchConfig" :active-value="1" :inactive-value="0" active-text="运行" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="batch-field" :class="{ 'is-disabled': !batchConfigApply.fallback_url }">
|
||||
<el-checkbox v-model="batchConfigApply.fallback_url" :disabled="savingBatchConfig">批量修改兜底获客助手链接</el-checkbox>
|
||||
<el-form-item label="兜底获客助手链接">
|
||||
<el-input v-model="batchConfigForm.fallback_url" :disabled="!batchConfigApply.fallback_url || savingBatchConfig" placeholder="留空表示清除;仅供旧版兼容跳转使用" />
|
||||
<span class="form-tip">新生成的官方直链不经过本站跳转;此项仅兼容旧安装代码。</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="overview.automation_installed" class="batch-config-section automation-batch-section">
|
||||
<h3>自动化设置</h3>
|
||||
<p class="batch-section-tip">先选择要覆盖的分组。未选择的接待、客户或欢迎语设置不会随本次批量操作改变。</p>
|
||||
<div class="batch-section-selectors">
|
||||
<el-checkbox v-model="batchConfigApply.reception" :disabled="savingBatchConfig" border>批量修改接待设置</el-checkbox>
|
||||
<el-checkbox v-model="batchConfigApply.customer" :disabled="savingBatchConfig" border>批量修改客户设置</el-checkbox>
|
||||
<el-checkbox v-model="batchConfigApply.welcome" :disabled="savingBatchConfig" border>批量修改欢迎语设置</el-checkbox>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="batchConfigApply.reception && !batchSharedPrimaryMemberIds.length"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
title="所选方案没有共同主接待成员"
|
||||
description="仍可统一改为全天接待;若使用按时段接待,时段成员必须同时属于全部所选方案。"
|
||||
/>
|
||||
<PromotionAutomationForm
|
||||
v-model="batchConfigForm.automation_config"
|
||||
:main-member-ids="batchSharedPrimaryMemberIds"
|
||||
:backup-excluded-member-ids="batchPrimaryMemberUnion"
|
||||
:members="overview.member_options"
|
||||
:disabled="savingBatchConfig"
|
||||
:disabled-sections="batchDisabledAutomationSections"
|
||||
@busy="(busy) => batchConfigBusy = busy"
|
||||
/>
|
||||
</section>
|
||||
<el-alert v-else type="warning" show-icon :closable="false" title="自动化配置尚未安装" description="本次仍可批量修改验证方式、兜底链接和运行状态;安装自动化数据表后即可批量修改接待、客户和欢迎语设置。" />
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span v-if="batchConfigBusy" class="uploading-save-tip">正在处理标签或素材,请稍候</span>
|
||||
<el-button :disabled="savingBatchConfig || batchConfigBusy" @click="batchConfigDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="savingBatchConfig" :disabled="batchConfigBusy" @click="saveBatchConfig">批量保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="accessDialogVisible"
|
||||
title="批量设置他人访问操作"
|
||||
@@ -525,6 +620,7 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
wecomPromotionBatchSetOperators,
|
||||
wecomPromotionBatchUpdatePools,
|
||||
wecomPromotionCheckApiPermission,
|
||||
wecomPromotionCustomerStats,
|
||||
wecomPromotionDeletePool,
|
||||
@@ -539,8 +635,10 @@ import type { WecomPromotionCustomerChatStatus } from '@/api/first_visit'
|
||||
import WecomFloatingWidgetBuilder from './components/WecomFloatingWidgetBuilder.vue'
|
||||
import PromotionAutomationForm from './components/PromotionAutomationForm.vue'
|
||||
import { cloneAutomationConfig, defaultAutomationConfig, isWebUrl, serializeAutomationConfig, validateAutomationConfig } from './components/promotion-automation'
|
||||
|
||||
import type { PromotionAutomationConfig } from './components/promotion-automation'
|
||||
|
||||
type TabName = 'links' | 'customer-stats' | 'configuration' | 'install'
|
||||
type BatchAutomationSection = 'reception' | 'customer' | 'welcome'
|
||||
|
||||
interface PromotionDepartmentOption {
|
||||
id: number | string
|
||||
@@ -606,11 +704,18 @@ const togglingMemberId = ref(0)
|
||||
const deletingPoolId = ref(0)
|
||||
const accessDialogVisible = ref(false)
|
||||
const savingAccess = ref(false)
|
||||
const batchConfigDialogVisible = ref(false)
|
||||
const savingBatchConfig = ref(false)
|
||||
const batchConfigBusy = ref(false)
|
||||
const batchConfigScroll = ref<HTMLElement>()
|
||||
const batchConfigError = ref('')
|
||||
const poolForm = reactive({ id: 0, name: '', fallback_url: '', status: 1, member_admin_ids: [] as number[], skip_verify: 0, main_url: '', automation_config: defaultAutomationConfig() })
|
||||
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 memberForm = reactive({ id: 0, name: '', userid: '', daily_limit: 0, status: 1, active_range: [] as string[], remark: '' })
|
||||
const customerStatsLoading = ref(false)
|
||||
const customerStatsLoaded = ref(false)
|
||||
@@ -633,6 +738,20 @@ 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 batchConfigPools = computed(() => overview.pools.filter((item: any) => batchConfigForm.pool_ids.includes(Number(item.id))))
|
||||
const batchPrimaryMemberSets = computed(() => batchConfigPools.value.map((pool: any) => new Set<number>(
|
||||
(Array.isArray(pool.member_admin_ids) ? pool.member_admin_ids : []).map(Number)
|
||||
)))
|
||||
const batchPrimaryMemberUnion = computed(() => [...new Set(batchPrimaryMemberSets.value.flatMap((ids) => [...ids]))])
|
||||
const batchSharedPrimaryMemberIds = computed(() => {
|
||||
const sets = batchPrimaryMemberSets.value
|
||||
if (!sets.length) return []
|
||||
return [...sets[0]].filter((id) => sets.slice(1).every((ids) => ids.has(id)))
|
||||
})
|
||||
const batchDisabledAutomationSections = computed<BatchAutomationSection[]>(() => (
|
||||
(['reception', 'customer', 'welcome'] as BatchAutomationSection[])
|
||||
.filter((section) => !batchConfigApply[section])
|
||||
))
|
||||
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))
|
||||
@@ -692,6 +811,135 @@ function togglePoolSelection(pool: any, checked: unknown) {
|
||||
selectedPoolIds.value = [...next]
|
||||
}
|
||||
|
||||
function openBatchConfigDialog(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('请先选择可管理的分流方案')
|
||||
if (ids.length > 100) return ElMessage.warning('单次最多设置 100 个分流方案')
|
||||
const reference = overview.pools.find((pool: any) => Number(pool.id) === ids[0])
|
||||
Object.assign(batchConfigApply, {
|
||||
skip_verify: false,
|
||||
fallback_url: false,
|
||||
status: false,
|
||||
reception: false,
|
||||
customer: false,
|
||||
welcome: false
|
||||
})
|
||||
Object.assign(batchConfigForm, {
|
||||
pool_ids: ids,
|
||||
skip_verify: Number(reference?.skip_verify) === 1 ? 1 : 0,
|
||||
fallback_url: String(reference?.fallback_url || ''),
|
||||
status: Number(reference?.status) === 1 ? 1 : 0,
|
||||
automation_config: cloneAutomationConfig(reference?.automation_config)
|
||||
})
|
||||
batchConfigError.value = ''
|
||||
batchConfigBusy.value = false
|
||||
batchConfigDialogVisible.value = true
|
||||
}
|
||||
|
||||
function copyAutomationSection(
|
||||
target: PromotionAutomationConfig,
|
||||
source: PromotionAutomationConfig,
|
||||
keys: Array<keyof PromotionAutomationConfig>
|
||||
) {
|
||||
const targetRecord = target as unknown as Record<string, unknown>
|
||||
const sourceRecord = source as unknown as Record<string, unknown>
|
||||
keys.forEach((key) => { targetRecord[key] = JSON.parse(JSON.stringify(sourceRecord[key])) })
|
||||
}
|
||||
|
||||
function setBatchConfigError(message: string) {
|
||||
batchConfigError.value = message
|
||||
batchConfigScroll.value?.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
ElMessage.warning(message)
|
||||
}
|
||||
|
||||
async function saveBatchConfig() {
|
||||
if (savingBatchConfig.value || batchConfigBusy.value) return
|
||||
batchConfigError.value = ''
|
||||
const hasAutomationChange = overview.automation_installed
|
||||
&& (batchConfigApply.reception || batchConfigApply.customer || batchConfigApply.welcome)
|
||||
if (!batchConfigApply.skip_verify && !batchConfigApply.fallback_url && !batchConfigApply.status && !hasAutomationChange) {
|
||||
return setBatchConfigError('请至少勾选一项需要批量修改的配置')
|
||||
}
|
||||
if (batchConfigApply.fallback_url && batchConfigForm.fallback_url.trim() && !isWebUrl(batchConfigForm.fallback_url.trim())) {
|
||||
return setBatchConfigError('兜底获客助手链接必须为有效的 HTTP/HTTPS 地址')
|
||||
}
|
||||
|
||||
const automation = cloneAutomationConfig(batchConfigForm.automation_config)
|
||||
if (batchConfigApply.reception && automation.reception_mode !== 'scheduled') automation.reception_schedule = []
|
||||
if (batchConfigApply.welcome && automation.welcome_mode !== 'channel') automation.welcome = { text: '', attachments: [] }
|
||||
if (batchConfigApply.welcome && (automation.welcome_mode !== 'channel' || !automation.welcome_schedule_enabled)) automation.welcome_schedule = []
|
||||
if (hasAutomationChange) {
|
||||
const validationConfig = defaultAutomationConfig()
|
||||
if (batchConfigApply.reception) copyAutomationSection(validationConfig, automation, ['reception_mode', 'reception_schedule', 'backup_member_admin_ids'])
|
||||
if (batchConfigApply.customer) copyAutomationSection(validationConfig, automation, ['tags_enabled', 'tag_ids', 'remark_enabled', 'remark_template', 'description_enabled', 'description'])
|
||||
if (batchConfigApply.welcome) copyAutomationSection(validationConfig, automation, ['welcome_mode', 'welcome', 'welcome_schedule_enabled', 'welcome_schedule'])
|
||||
const backupConflict = batchConfigApply.reception
|
||||
&& automation.backup_member_admin_ids.some((id) => batchPrimaryMemberUnion.value.includes(Number(id)))
|
||||
const validationError = backupConflict
|
||||
? '备用成员不能是任一所选方案的主接待成员'
|
||||
: validateAutomationConfig(validationConfig, batchSharedPrimaryMemberIds.value)
|
||||
if (validationError) return setBatchConfigError(validationError)
|
||||
}
|
||||
|
||||
const changes: {
|
||||
skip_verify?: 0 | 1
|
||||
fallback_url?: string
|
||||
status?: 0 | 1
|
||||
automation_config?: Record<string, unknown>
|
||||
} = {}
|
||||
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 (hasAutomationChange) {
|
||||
const serialized = serializeAutomationConfig(automation) as unknown as Record<string, unknown>
|
||||
const automationPatch: Record<string, unknown> = {}
|
||||
const assignKeys = (keys: string[]) => keys.forEach((key) => { automationPatch[key] = serialized[key] })
|
||||
if (batchConfigApply.reception) assignKeys(['reception_mode', 'reception_schedule', 'backup_member_admin_ids'])
|
||||
if (batchConfigApply.customer) assignKeys(['tags_enabled', 'tag_ids', 'remark_enabled', 'remark_template', 'description_enabled', 'description'])
|
||||
if (batchConfigApply.welcome) assignKeys(['welcome_mode', 'welcome', 'welcome_schedule_enabled', 'welcome_schedule'])
|
||||
changes.automation_config = automationPatch
|
||||
}
|
||||
|
||||
savingBatchConfig.value = true
|
||||
try {
|
||||
const result = await wecomPromotionBatchUpdatePools({
|
||||
pool_ids: [...batchConfigForm.pool_ids],
|
||||
changes
|
||||
})
|
||||
if (result.updated === 0) {
|
||||
const detail = result.results
|
||||
.slice(0, 2)
|
||||
.map((item) => `${item.name || `方案 ${item.id}`}:${item.error || '保存失败'}`)
|
||||
.join(';')
|
||||
return setBatchConfigError(detail || '所选方案均未能保存,请检查配置后重试')
|
||||
}
|
||||
batchConfigDialogVisible.value = false
|
||||
selectedPoolIds.value = []
|
||||
await loadOverview()
|
||||
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}` : ''}`)
|
||||
} else if (result.sync_error_count > 0) {
|
||||
ElMessage.warning(`已更新 ${result.updated} 个方案,其中 ${result.sync_error_count} 个企微范围将在后台自动重试同步`)
|
||||
} else if (result.sync_queued_count > 0) {
|
||||
ElMessage.success(`已批量更新 ${result.updated} 个分流方案,${result.sync_queued_count} 个企微链接配置将在后台同步`)
|
||||
} else {
|
||||
ElMessage.success(`已批量更新 ${result.updated} 个分流方案`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
setBatchConfigError(error?.message || '批量修改分流方案失败')
|
||||
} finally {
|
||||
savingBatchConfig.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openAccessDialog(poolIds: number[] = selectedPoolIds.value) {
|
||||
const manageableIds = new Set(overview.pools
|
||||
.filter((pool: any) => pool.can_manage_access)
|
||||
@@ -1319,18 +1567,18 @@ h1, h2, h3, p { margin: 0; }
|
||||
.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-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-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); }
|
||||
.pool-main { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; }.pool-toolbar { display: flex; flex: 0 0 auto; align-items: center; justify-content: space-between; gap: 16px; min-height: 78px; padding: 12px 15px; border-bottom: 1px solid var(--line); }
|
||||
.pool-title-row { gap: 8px; }.pool-title-row h3 { font-size: 15px; }.pool-toolbar p { margin-top: 6px; color: #8b97a6; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 10px; }.toolbar-actions { flex-wrap: wrap; justify-content: flex-end; gap: 7px; }.toolbar-actions .el-button + .el-button { margin-left: 0; }
|
||||
.status-tag, .owner-tag, .availability { display: inline-flex; align-items: center; min-height: 22px; padding: 0 8px; border-radius: 11px; font-size: 10px; }.status-tag.is-online, .availability.is-ok { color: #16895f; background: #eaf8ef; }.status-tag.is-offline, .availability.is-muted { color: #788696; background: #eef2f5; }.owner-tag { color: #50728c; background: #edf4f8; }.availability.is-error { color: #d94b4b; background: #ffeded; }.availability.is-waiting { color: #b86c1f; background: #fff1dd; }
|
||||
.link-table, .account-table { --el-table-header-bg-color: #f7f9fb; }.link-table :deep(th.el-table__cell), .account-table :deep(th.el-table__cell) { color: #67768a; font-weight: 500; }.member-cell strong, .member-cell small { display: block; }.member-cell small { margin-top: 3px; color: #8d98a7; font-size: 10px; }.muted { color: #9aa4b0; }
|
||||
.legacy-sync-alert { flex: 0 0 auto; }.member-table-area { min-height: 0; flex: 1 1 auto; overflow: hidden; }.link-table, .account-table { --el-table-header-bg-color: #f7f9fb; }.link-table :deep(th.el-table__cell), .account-table :deep(th.el-table__cell) { color: #67768a; font-weight: 500; }.member-cell strong, .member-cell small { display: block; }.member-cell small { margin-top: 3px; color: #8d98a7; font-size: 10px; }.muted { color: #9aa4b0; }
|
||||
.remote-id { display: block; overflow: hidden; color: #68778b; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.link-id-cell { display: grid; gap: 4px; min-width: 0; }
|
||||
.wecom-url { display: block; overflow: hidden; color: #148f83; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; line-height: 1.4; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; }
|
||||
@@ -1368,9 +1616,15 @@ h1, h2, h3, p { margin: 0; }
|
||||
.access-pool-preview p { margin-top: 7px; }
|
||||
.pool-form-scroll { max-height: 70vh; overflow-y: auto; overflow-x: hidden; padding: 0 10px 6px 2px; }
|
||||
.pool-form-error { margin-bottom: 18px; }
|
||||
.batch-config-error { margin-top: 14px; }
|
||||
.batch-pool-summary { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; margin-top: 14px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; background: #f8fafb; }
|
||||
.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-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 { 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; } }
|
||||
@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; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user