更新
This commit is contained in:
@@ -351,19 +351,35 @@
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="方案名称" required><el-input v-model="poolForm.name" :disabled="savingPool" maxlength="60" show-word-limit placeholder="例如:官网咨询分流" /></el-form-item>
|
||||
<el-form-item label="获客医助" required>
|
||||
<el-select
|
||||
<el-tree-select
|
||||
v-model="poolForm.member_admin_ids"
|
||||
:data="memberDepartmentTree"
|
||||
:props="memberTreeProps"
|
||||
:default-expanded-keys="memberTreeDefaultExpandedKeys"
|
||||
:filter-node-method="filterMemberTreeNode"
|
||||
:disabled="savingPool"
|
||||
node-key="value"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
show-checkbox
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="选择后由企业微信生成一个多人分流链接"
|
||||
:max-collapse-tags="2"
|
||||
:multiple-limit="500"
|
||||
:render-after-expand="false"
|
||||
placeholder="按部门展开,或搜索姓名 / 部门 / 企微 userid"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="member in overview.member_options" :key="member.id" :label="memberOptionLabel(member)" :value="Number(member.id)" />
|
||||
</el-select>
|
||||
<span class="form-tip">保存后会为每名医助生成一条成员规则,官方链接地址始终只有一个。</span>
|
||||
<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">已选 {{ poolForm.member_admin_ids.length }} 名;保存后会为每名医助生成一条成员规则,官方链接地址始终只有一个。</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="添加客户时跳过验证">
|
||||
<el-switch v-model="poolForm.skip_verify" :disabled="savingPool" :active-value="1" :inactive-value="0" active-text="跳过验证" inactive-text="需要验证" />
|
||||
@@ -421,13 +437,44 @@ import type { WecomPromotionCustomerChatStatus } from '@/api/first_visit'
|
||||
|
||||
import WecomFloatingWidgetBuilder from './components/WecomFloatingWidgetBuilder.vue'
|
||||
|
||||
type TabName = 'links' | 'customer-stats' | 'configuration' | 'install'
|
||||
const emptyOverview = () => ({
|
||||
meta: { 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 any[], customer_acquisition_link_example: 'https://work.weixin.qq.com/ca/xxxxxxxx'
|
||||
})
|
||||
type TabName = 'links' | 'customer-stats' | 'configuration' | 'install'
|
||||
|
||||
interface PromotionDepartmentOption {
|
||||
id: number | string
|
||||
name: string
|
||||
children?: PromotionDepartmentOption[]
|
||||
}
|
||||
|
||||
interface PromotionMemberOption {
|
||||
id: number
|
||||
name: string
|
||||
userid: string
|
||||
display_dept_id?: number
|
||||
dept_ids: number[]
|
||||
dept_names: string[]
|
||||
}
|
||||
|
||||
interface PromotionMemberTreeNode {
|
||||
value: number | string
|
||||
label: string
|
||||
kind: 'department' | 'member'
|
||||
disabled?: boolean
|
||||
member_count?: number
|
||||
detail?: string
|
||||
search_text: string
|
||||
children?: PromotionMemberTreeNode[]
|
||||
}
|
||||
|
||||
const emptyOverview = () => ({
|
||||
meta: { 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[],
|
||||
department_options: [] as PromotionDepartmentOption[],
|
||||
customer_acquisition_link_example: 'https://work.weixin.qq.com/ca/xxxxxxxx'
|
||||
})
|
||||
|
||||
const overview = reactive(emptyOverview())
|
||||
const loading = ref(false)
|
||||
@@ -462,8 +509,11 @@ 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 customerLinkOptions = computed(() => customerStats.link_options.length
|
||||
const selectedInstallPool = computed(() => overview.pools.find((item: any) => Number(item.id) === selectedInstallPoolId.value))
|
||||
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))
|
||||
const customerLinkOptions = computed(() => customerStats.link_options.length
|
||||
? customerStats.link_options
|
||||
: overview.links
|
||||
.filter((item: any) => item.id)
|
||||
@@ -739,16 +789,122 @@ function eligibility(row: any) {
|
||||
return { label: '可参与分流', className: 'is-ok' }
|
||||
}
|
||||
|
||||
function todayCount(row: any) {
|
||||
function todayCount(row: any) {
|
||||
const today = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date())
|
||||
return row.today_date === today ? Number(row.today_count || 0) : 0
|
||||
}
|
||||
|
||||
function memberOptionLabel(member: any) {
|
||||
const departments = Array.isArray(member.dept_names) && member.dept_names.length
|
||||
? member.dept_names.join(' / ')
|
||||
: '未分部门'
|
||||
return `${member.name} · ${departments} · ${member.userid}`
|
||||
}
|
||||
|
||||
function buildMemberDepartmentTree(
|
||||
departmentOptions: PromotionDepartmentOption[],
|
||||
memberOptions: PromotionMemberOption[]
|
||||
): PromotionMemberTreeNode[] {
|
||||
const availableDeptIds = new Set<number>()
|
||||
const collectDeptIds = (departments: PromotionDepartmentOption[]) => {
|
||||
departments.forEach((department) => {
|
||||
const departmentId = Number(department.id)
|
||||
if (departmentId > 0) availableDeptIds.add(departmentId)
|
||||
if (Array.isArray(department.children)) collectDeptIds(department.children)
|
||||
})
|
||||
}
|
||||
collectDeptIds(Array.isArray(departmentOptions) ? departmentOptions : [])
|
||||
|
||||
const members: PromotionMemberOption[] = []
|
||||
const seenMemberIds = new Set<number>()
|
||||
for (const rawMember of Array.isArray(memberOptions) ? memberOptions : []) {
|
||||
const memberId = Number(rawMember.id)
|
||||
if (memberId <= 0 || seenMemberIds.has(memberId)) continue
|
||||
seenMemberIds.add(memberId)
|
||||
members.push({
|
||||
id: memberId,
|
||||
name: String(rawMember.name || rawMember.userid || `成员 ${memberId}`),
|
||||
userid: String(rawMember.userid || ''),
|
||||
display_dept_id: Number(rawMember.display_dept_id || 0),
|
||||
dept_ids: Array.isArray(rawMember.dept_ids)
|
||||
? Array.from(new Set(rawMember.dept_ids.map(Number).filter((id) => id > 0)))
|
||||
: [],
|
||||
dept_names: Array.isArray(rawMember.dept_names)
|
||||
? rawMember.dept_names.map(String).filter(Boolean)
|
||||
: []
|
||||
})
|
||||
}
|
||||
|
||||
const membersByDept = new Map<number, PromotionMemberOption[]>()
|
||||
members.forEach((member) => {
|
||||
const explicitDeptId = Number(member.display_dept_id || 0)
|
||||
const displayDeptId = availableDeptIds.has(explicitDeptId)
|
||||
? explicitDeptId
|
||||
: (member.dept_ids.find((id) => availableDeptIds.has(id)) || 0)
|
||||
if (!membersByDept.has(displayDeptId)) membersByDept.set(displayDeptId, [])
|
||||
membersByDept.get(displayDeptId)?.push(member)
|
||||
})
|
||||
|
||||
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
|
||||
return {
|
||||
value: member.id,
|
||||
label: member.name,
|
||||
kind: 'member',
|
||||
detail,
|
||||
search_text: `${member.name} ${member.userid} ${member.dept_names.join(' ')}`.toLocaleLowerCase()
|
||||
}
|
||||
}
|
||||
const byMemberName = (left: PromotionMemberOption, right: PromotionMemberOption) => left.name.localeCompare(right.name, 'zh-CN')
|
||||
|
||||
const buildDepartments = (departments: PromotionDepartmentOption[], parentPath: string[]): PromotionMemberTreeNode[] => {
|
||||
const result: PromotionMemberTreeNode[] = []
|
||||
for (const department of Array.isArray(departments) ? departments : []) {
|
||||
const departmentId = Number(department.id)
|
||||
if (departmentId <= 0) continue
|
||||
const departmentName = String(department.name || `部门 ${departmentId}`)
|
||||
const path = [...parentPath, departmentName]
|
||||
const departmentChildren = buildDepartments(
|
||||
Array.isArray(department.children) ? department.children : [],
|
||||
path
|
||||
)
|
||||
const directMembers = [...(membersByDept.get(departmentId) || [])].sort(byMemberName)
|
||||
directMembers.forEach((member) => placedMemberIds.add(member.id))
|
||||
const memberChildren = directMembers.map(toMemberNode)
|
||||
const children = [...departmentChildren, ...memberChildren]
|
||||
if (!children.length) continue
|
||||
const memberCount = children.reduce(
|
||||
(count, child) => count + (child.kind === 'member' ? 1 : Number(child.member_count || 0)),
|
||||
0
|
||||
)
|
||||
result.push({
|
||||
value: `dept:${departmentId}`,
|
||||
label: departmentName,
|
||||
kind: 'department',
|
||||
disabled: true,
|
||||
member_count: memberCount,
|
||||
search_text: `${path.join(' ')} ${children.map((child) => child.search_text).join(' ')}`.toLocaleLowerCase(),
|
||||
children
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const tree = buildDepartments(Array.isArray(departmentOptions) ? departmentOptions : [], [])
|
||||
const unassigned = members.filter((member) => !placedMemberIds.has(member.id)).sort(byMemberName)
|
||||
if (unassigned.length) {
|
||||
const children = unassigned.map(toMemberNode)
|
||||
tree.push({
|
||||
value: 'dept:unassigned',
|
||||
label: '未分部门',
|
||||
kind: 'department',
|
||||
disabled: true,
|
||||
member_count: children.length,
|
||||
search_text: `未分部门 ${children.map((child) => child.search_text).join(' ')}`.toLocaleLowerCase(),
|
||||
children
|
||||
})
|
||||
}
|
||||
return tree
|
||||
}
|
||||
|
||||
function filterMemberTreeNode(keyword: string, data: any) {
|
||||
const query = String(keyword || '').trim().toLocaleLowerCase()
|
||||
return query === '' || data.search_text.includes(query)
|
||||
}
|
||||
|
||||
function memberSummary(pool: any) {
|
||||
@@ -946,8 +1102,13 @@ h1, h2, h3, p { margin: 0; }
|
||||
.configuration-state { display: flex; align-items: center; gap: 12px; }.configuration-state > span { display: grid; width: 36px; height: 36px; place-items: center; border-radius: 9px; color: #fff; background: var(--teal); font-size: 18px; }.is-pending .configuration-state > span { background: #e69a43; }.configuration-state strong { font-size: 14px; }.configuration-state p { margin-top: 4px; color: #718094; font-size: 11px; }
|
||||
.missing-fields { margin-top: 12px; color: #9c651f; font-size: 11px; }.missing-fields code { margin-left: 6px; padding: 3px 6px; border-radius: 4px; background: rgba(224,153,63,.11); }
|
||||
.callback-list { margin: 14px 0 0; border-top: 1px solid rgba(124,150,157,.16); }.callback-list > div { display: grid; grid-template-columns: 120px 1fr 50px; align-items: center; min-height: 40px; border-bottom: 1px solid rgba(124,150,157,.12); font-size: 11px; }.callback-list dt { color: #657488; }.callback-list dd { overflow: hidden; margin: 0; color: #27374c; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; text-overflow: ellipsis; white-space: nowrap; }.callback-list button { border: 0; color: #148f83; background: transparent; cursor: pointer; }
|
||||
.configuration-actions { display: flex; align-items: center; gap: 14px; margin-top: 16px; }
|
||||
.install-pool-select { width: 220px; }
|
||||
.configuration-actions { display: flex; align-items: center; gap: 14px; margin-top: 16px; }
|
||||
.install-pool-select { width: 220px; }
|
||||
.member-tree-option { display: inline-flex; min-width: 0; flex: 1; align-items: center; gap: 10px; }
|
||||
.member-tree-option__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.member-tree-option.is-department .member-tree-option__label { color: #344054; font-weight: 600; }
|
||||
.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; } }
|
||||
|
||||
Reference in New Issue
Block a user