更新
This commit is contained in:
@@ -190,9 +190,9 @@ export function wecomPromotionOverview() {
|
|||||||
return request.get({ url: '/firstvisit.wecomPromotion/overview' })
|
return request.get({ url: '/firstvisit.wecomPromotion/overview' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wecomPromotionSavePool(params: Record<string, unknown>) {
|
export function wecomPromotionSavePool(params: Record<string, unknown>) {
|
||||||
return request.post({ url: '/firstvisit.wecomPromotion/savePool', params })
|
return request.post({ url: '/firstvisit.wecomPromotion/savePool', params, timeout: 120000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wecomPromotionSaveWidget(params: Record<string, unknown>) {
|
export function wecomPromotionSaveWidget(params: Record<string, unknown>) {
|
||||||
return request.post({ url: '/firstvisit.wecomPromotion/saveWidget', params })
|
return request.post({ url: '/firstvisit.wecomPromotion/saveWidget', params })
|
||||||
@@ -202,9 +202,17 @@ export function wecomPromotionDeletePool(params: { id: number }) {
|
|||||||
return request.post({ url: '/firstvisit.wecomPromotion/deletePool', params })
|
return request.post({ url: '/firstvisit.wecomPromotion/deletePool', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wecomPromotionSaveLink(params: Record<string, unknown>) {
|
export function wecomPromotionSaveLink(params: Record<string, unknown>) {
|
||||||
return request.post({ url: '/firstvisit.wecomPromotion/saveLink', params })
|
return request.post({ url: '/firstvisit.wecomPromotion/saveLink', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function wecomPromotionSaveMember(params: Record<string, unknown>) {
|
||||||
|
return request.post({ url: '/firstvisit.wecomPromotion/saveMember', params, timeout: 120000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wecomPromotionToggleMember(params: { id: number; status: number }) {
|
||||||
|
return request.post({ url: '/firstvisit.wecomPromotion/toggleMember', params, timeout: 120000 })
|
||||||
|
}
|
||||||
|
|
||||||
export function wecomPromotionCheckApiPermission() {
|
export function wecomPromotionCheckApiPermission() {
|
||||||
return request.post({ url: '/firstvisit.wecomPromotion/checkApiPermission' })
|
return request.post({ url: '/firstvisit.wecomPromotion/checkApiPermission' })
|
||||||
|
|||||||
+6
-3
@@ -239,7 +239,7 @@
|
|||||||
|
|
||||||
<div class="install-tip">
|
<div class="install-tip">
|
||||||
<el-icon><CircleCheck /></el-icon>
|
<el-icon><CircleCheck /></el-icon>
|
||||||
<span><strong>无需暴露链接池</strong>浮窗点击后仍由服务端执行可用性筛选、权重随机和访问统计。</span>
|
<span><strong>一个方案,一个官方链接</strong>浮窗会直接打开企业微信链接,多名医助由企业微信执行均衡分流。</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
@@ -274,6 +274,8 @@ interface PromotionPool {
|
|||||||
name?: string
|
name?: string
|
||||||
public_key?: string
|
public_key?: string
|
||||||
script_url?: string
|
script_url?: string
|
||||||
|
main_url?: string
|
||||||
|
compat_go_url?: string
|
||||||
go_url?: string
|
go_url?: string
|
||||||
install_code?: string
|
install_code?: string
|
||||||
trigger_code?: string
|
trigger_code?: string
|
||||||
@@ -437,11 +439,12 @@ async function copySnippet(value: string | undefined, label: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openTestLink() {
|
function openTestLink() {
|
||||||
if (!props.pool.go_url) {
|
const targetUrl = props.pool.main_url || props.pool.go_url || props.pool.compat_go_url
|
||||||
|
if (!targetUrl) {
|
||||||
ElMessage.warning('当前方案暂无测试链接')
|
ElMessage.warning('当前方案暂无测试链接')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
window.open(props.pool.go_url, '_blank', 'noopener,noreferrer')
|
window.open(targetUrl, '_blank', 'noopener,noreferrer')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<span class="heading-icon"><el-icon><Promotion /></el-icon></span>
|
<span class="heading-icon"><el-icon><Promotion /></el-icon></span>
|
||||||
<div>
|
<div>
|
||||||
<h1>企业微信获客助手</h1>
|
<h1>企业微信获客助手</h1>
|
||||||
<p>对接企业微信获客助手官方 API,按角色与部门管理获客成员,并将链接安全分流到推广页面</p>
|
<p>一个方案保留一条企业微信官方直链,按回调结果自动调度医助权重与数量</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="heading-actions">
|
<div class="heading-actions">
|
||||||
@@ -22,15 +22,15 @@
|
|||||||
</article>
|
</article>
|
||||||
<article class="metric-card">
|
<article class="metric-card">
|
||||||
<span class="metric-icon is-blue"><el-icon><SetUp /></el-icon></span>
|
<span class="metric-icon is-blue"><el-icon><SetUp /></el-icon></span>
|
||||||
<div><small>分流方案</small><strong>{{ overview.summary.pool_count }}</strong><p>每个方案生成独立 JS</p></div>
|
<div><small>分流方案</small><strong>{{ overview.summary.pool_count }}</strong><p>每个方案对应一个官方链接</p></div>
|
||||||
</article>
|
</article>
|
||||||
<article class="metric-card">
|
<article class="metric-card">
|
||||||
<span class="metric-icon is-green"><el-icon><Link /></el-icon></span>
|
<span class="metric-icon is-green"><el-icon><Link /></el-icon></span>
|
||||||
<div><small>在线链接</small><strong>{{ overview.summary.online_links }}</strong><p>停用或超限自动排除</p></div>
|
<div><small>官方链接</small><strong>{{ overview.summary.online_links }}</strong><p>链接不变,成员范围自动切换</p></div>
|
||||||
</article>
|
</article>
|
||||||
<article class="metric-card">
|
<article class="metric-card">
|
||||||
<span class="metric-icon is-orange"><el-icon><Mouse /></el-icon></span>
|
<span class="metric-icon is-orange"><el-icon><Mouse /></el-icon></span>
|
||||||
<div><small>今日分流点击</small><strong>{{ overview.summary.today_clicks }}</strong><p>只记录脱敏访问数据</p></div>
|
<div><small>今日实际获客</small><strong>{{ overview.summary.today_clicks }}</strong><p>按企业微信回调确认承接成员</p></div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -52,9 +52,9 @@
|
|||||||
|
|
||||||
<div v-if="activeTab === 'links'" class="tab-content links-tab">
|
<div v-if="activeTab === 'links'" class="tab-content links-tab">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>获客助手链接分流</h2>
|
<h2>获客助手分流方案</h2>
|
||||||
<p>访客点击时由服务端从当前可用的获客助手链接中按权重随机选择,页面端不会暴露完整链接池。</p>
|
<p>选择几名医助就生成几条成员规则;官方链接只在方案顶部复制,成员行不重复展示链接。</p>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" :icon="Plus" @click="openPoolDialog()">新建分流方案</el-button>
|
<el-button type="primary" :icon="Plus" @click="openPoolDialog()">新建分流方案</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
@click="selectedPoolId = Number(pool.id)"
|
@click="selectedPoolId = Number(pool.id)"
|
||||||
>
|
>
|
||||||
<span class="pool-status" :class="Number(pool.status) === 1 ? 'online' : 'offline'" />
|
<span class="pool-status" :class="Number(pool.status) === 1 ? 'online' : 'offline'" />
|
||||||
<span><strong>{{ pool.name }}</strong><small>{{ linkCount(pool.id) }} 条链接 · {{ formatNumber(pool.click_count) }} 次点击</small></span>
|
<span><strong>{{ pool.name }}</strong><small>{{ memberSummary(pool) }} · {{ pool.main_url ? '官方链接已生成' : '待生成链接' }}</small></span>
|
||||||
<el-icon><ArrowRight /></el-icon>
|
<el-icon><ArrowRight /></el-icon>
|
||||||
</button>
|
</button>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -84,100 +84,77 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="owner-tag">{{ selectedPool.dept_name || '未分部门' }} · {{ selectedPool.owner_name || '系统' }}</span>
|
<span class="owner-tag">{{ selectedPool.dept_name || '未分部门' }} · {{ selectedPool.owner_name || '系统' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>公开键:{{ selectedPool.public_key }}</p>
|
<p>公开键:{{ selectedPool.public_key }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<el-button :icon="CircleCheck" :loading="checkingApi" @click="checkApiPermission">验证 API</el-button>
|
<el-button :icon="CircleCheck" :loading="checkingApi" @click="checkApiPermission">验证 API</el-button>
|
||||||
<el-button :icon="Refresh" :loading="syncingRemote" @click="syncRemoteLinks">同步企业微信</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="DocumentCopy" @click="copyText(selectedPool.install_code, 'JS 安装代码')">复制 JS</el-button>
|
||||||
<el-button :icon="Edit" @click="openPoolDialog(selectedPool)">编辑方案</el-button>
|
<el-button :icon="Edit" @click="openPoolDialog(selectedPool)">编辑方案</el-button>
|
||||||
<el-button type="danger" plain :icon="Delete" @click="removePool(selectedPool)">删除</el-button>
|
<el-button type="danger" plain :icon="Delete" @click="removePool(selectedPool)">删除</el-button>
|
||||||
<el-button type="primary" :icon="Plus" @click="openLinkDialog()">创建官方获客链接</el-button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<el-alert
|
||||||
<el-alert
|
v-if="selectedPool.migration_state === 'needs_resolution'"
|
||||||
v-if="selectedLegacyCount > 0 && selectedOfficialCount === 0"
|
class="legacy-sync-alert"
|
||||||
class="legacy-sync-alert"
|
type="warning"
|
||||||
type="warning"
|
show-icon
|
||||||
show-icon
|
:closable="false"
|
||||||
:closable="false"
|
title="检测到旧版方案存在多个官方链接"
|
||||||
:title="`当前方案的 ${selectedLegacyCount} 条链接都是历史手工链接,企业微信同步不会返回它们`"
|
description="当前显示最新链接;保存方案后,其余旧链接只会在本地下线,企业微信端与历史客户归因仍会保留。"
|
||||||
description="官方接口只返回当前应用通过 API 创建的获客链接。现有链接仍可参与本地分流;如需同步 link_id、客户和官方统计,请点击“创建官方获客链接”。"
|
/>
|
||||||
/>
|
|
||||||
|
<el-alert
|
||||||
<el-table :data="selectedLinks" class="link-table" stripe>
|
class="legacy-sync-alert"
|
||||||
<el-table-column label="推广成员 / 分组" min-width="180" fixed="left">
|
type="info"
|
||||||
<template #default="{ row }">
|
show-icon
|
||||||
<div class="member-cell"><strong>{{ row.name }}</strong><small>{{ row.group_name || '默认分组' }}</small></div>
|
:closable="false"
|
||||||
</template>
|
title="回调驱动分流"
|
||||||
</el-table-column>
|
description="企业微信确认实际承接成员后,系统累计今日数量,并在符合启用状态、有效期和上限的成员中按权重随机抽取下一位;随机结果可能连续命中同一人,并发访问及企微同步存在短暂延迟。"
|
||||||
<el-table-column label="企业微信链接 ID" min-width="280">
|
/>
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="link-id-cell">
|
<el-table :data="selectedMemberRules" class="link-table" stripe>
|
||||||
<span v-if="row.remote_link_id" class="remote-id" :title="row.remote_link_id">{{ row.remote_link_id }}</span>
|
<el-table-column label="推广成员" min-width="210" fixed="left">
|
||||||
<span v-else class="legacy-tag">历史手工链接</span>
|
<template #default="{ row }">
|
||||||
<a
|
<div class="member-cell">
|
||||||
v-if="row.wecom_url"
|
<strong>{{ row.name || row.userid }}</strong>
|
||||||
class="wecom-url"
|
<small>{{ memberRuleDetail(row) }}</small>
|
||||||
:href="row.wecom_url"
|
</div>
|
||||||
:title="row.wecom_url"
|
</template>
|
||||||
target="_blank"
|
</el-table-column>
|
||||||
rel="noopener noreferrer"
|
<el-table-column label="调度" width="105" align="center">
|
||||||
@click.stop
|
<template #default="{ row }">
|
||||||
>{{ row.wecom_url }}</a>
|
<span v-if="row.is_current" class="status-tag is-online">{{ Number(row.sync_status) === 0 ? '当前命中' : '待切换' }}</span>
|
||||||
<el-button
|
<span v-else-if="row.is_applied" class="status-tag is-offline">切换中</span>
|
||||||
v-if="row.wecom_url"
|
<span v-else class="muted">候选</span>
|
||||||
type="primary"
|
</template>
|
||||||
link
|
</el-table-column>
|
||||||
class="copy-url-btn"
|
<el-table-column label="权重" prop="weight" width="85" align="center" />
|
||||||
@click.stop="copyText(row.wecom_url, '企业微信链接')"
|
<el-table-column label="今日 / 上限" width="125" align="center">
|
||||||
>复制链接</el-button>
|
<template #default="{ row }">{{ todayCount(row) }} / {{ Number(row.daily_limit) ? row.daily_limit : '不限' }}</template>
|
||||||
<span v-else class="muted">暂无链接地址</span>
|
</el-table-column>
|
||||||
</div>
|
<el-table-column label="累计获客" prop="total_count" width="105" align="center" />
|
||||||
</template>
|
<el-table-column label="当前可用性" min-width="130">
|
||||||
</el-table-column>
|
<template #default="{ row }">
|
||||||
<el-table-column label="可用成员" min-width="150">
|
<span class="availability" :class="eligibility(row).className">{{ eligibility(row).label }}</span>
|
||||||
<template #default="{ row }">
|
</template>
|
||||||
<span>{{ remoteMemberLabel(row) }}</span>
|
</el-table-column>
|
||||||
</template>
|
<el-table-column label="上线" width="90" align="center">
|
||||||
</el-table-column>
|
<template #default="{ row }">
|
||||||
<el-table-column label="权重" prop="weight" width="84" align="center" />
|
<el-switch :model-value="Number(row.enabled) === 1" :loading="togglingMemberId === Number(row.id)" @change="(value) => handleMemberToggle(row, value)" />
|
||||||
<el-table-column label="今日 / 上限" min-width="120" align="center">
|
</template>
|
||||||
<template #default="{ row }">{{ todayCount(row) }} / {{ Number(row.daily_limit) ? row.daily_limit : '不限' }}</template>
|
</el-table-column>
|
||||||
</el-table-column>
|
<el-table-column label="操作" min-width="100" fixed="right">
|
||||||
<el-table-column label="累计点击" prop="click_count" min-width="105" align="center" />
|
<template #default="{ row }">
|
||||||
<el-table-column label="当前可用性" min-width="130">
|
<el-button type="primary" link @click="openMemberDialog(row)">编辑规则</el-button>
|
||||||
<template #default="{ row }">
|
</template>
|
||||||
<span class="availability" :class="eligibility(row).className">{{ eligibility(row).label }}</span>
|
</el-table-column>
|
||||||
</template>
|
<template #empty><el-empty :image-size="72" description="编辑方案并选择获客医助" /></template>
|
||||||
</el-table-column>
|
</el-table>
|
||||||
<el-table-column label="上线" width="90" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-switch :model-value="Number(row.status) === 1" :disabled="Number(row.remote_status) === 2" @change="(value) => handleToggle(row, value)" />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" min-width="245" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button v-if="row.is_official && Number(row.remote_status) !== 2" type="primary" link :loading="refreshingLinkId === Number(row.id)" @click="refreshRemoteDetail(row)">刷新详情</el-button>
|
|
||||||
<el-button v-if="!row.is_official || Number(row.remote_status) !== 2" type="primary" link @click="openLinkDialog(row)">编辑</el-button>
|
|
||||||
<el-dropdown v-if="row.is_official" trigger="click" @command="(command) => handleLinkCommand(command, row)">
|
|
||||||
<el-button type="primary" link>更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item command="local">从本地移除</el-dropdown-item>
|
|
||||||
<el-dropdown-item v-if="Number(row.remote_status) !== 2" command="remote" divided class="danger-command">永久删除企业微信链接</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
<el-button v-else type="danger" link @click="removeLink(row)">从本地移除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<template #empty><el-empty :image-size="72" description="当前方案还没有获客助手链接" /></template>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-else description="先创建一个分流方案,再添加企业微信获客助手链接">
|
<el-empty v-else description="创建方案并选择多个医助,保存后自动生成一个企业微信官方获客链接">
|
||||||
<el-button type="primary" :icon="Plus" @click="openPoolDialog()">创建第一个方案</el-button>
|
<el-button type="primary" :icon="Plus" @click="openPoolDialog()">创建第一个方案</el-button>
|
||||||
</el-empty>
|
</el-empty>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,7 +197,7 @@
|
|||||||
show-icon
|
show-icon
|
||||||
:closable="false"
|
:closable="false"
|
||||||
title="消息次数回调尚未配置完成"
|
title="消息次数回调尚未配置完成"
|
||||||
description="复用企业微信自建应用现有的“API 接收消息”地址即可,无需新增第二个回调;请确保该地址为 QywxExternalContactCallback/notify,且服务器 Token、AESKey 与企微后台一致。未完成前无法得到精确接收消息次数。"
|
description="复用企业微信自建应用现有的“API 接收消息”地址即可,无需新增第二个回调;请确保该地址为 /api/qywx/external-contact/notify,且服务器 Token、AESKey 与企微后台一致。未完成前无法自动累计成员获客数量和切换下一位。"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<el-alert
|
<el-alert
|
||||||
@@ -363,75 +340,89 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<el-dialog v-model="poolDialogVisible" :title="poolForm.id ? '编辑分流方案' : '新建分流方案'" width="560px" destroy-on-close>
|
<el-dialog
|
||||||
<el-form label-position="top">
|
v-model="poolDialogVisible"
|
||||||
<el-form-item label="方案名称" required><el-input v-model="poolForm.name" maxlength="60" show-word-limit placeholder="例如:官网咨询分流" /></el-form-item>
|
:title="poolForm.id ? '编辑分流方案' : '新建分流方案'"
|
||||||
<el-form-item label="兜底获客助手链接">
|
width="680px"
|
||||||
<el-input v-model="poolForm.fallback_url" placeholder="可选;没有可用成员时跳转" />
|
destroy-on-close
|
||||||
<span class="form-tip">仅接受企业微信获客助手生成的 https://work.weixin.qq.com/ca/... 链接。</span>
|
:close-on-click-modal="false"
|
||||||
</el-form-item>
|
:close-on-press-escape="!savingPool"
|
||||||
<el-form-item label="运行状态"><el-switch v-model="poolForm.status" :active-value="1" :inactive-value="0" active-text="运行" inactive-text="停用" /></el-form-item>
|
>
|
||||||
</el-form>
|
<el-form label-position="top">
|
||||||
<template #footer><el-button @click="poolDialogVisible = false">取消</el-button><el-button type="primary" :loading="savingPool" @click="savePool">保存方案</el-button></template>
|
<el-form-item label="方案名称" required><el-input v-model="poolForm.name" :disabled="savingPool" maxlength="60" show-word-limit placeholder="例如:官网咨询分流" /></el-form-item>
|
||||||
</el-dialog>
|
<el-form-item label="获客医助" required>
|
||||||
|
<el-select
|
||||||
<el-dialog v-model="linkDialogVisible" :title="linkForm.id ? '编辑获客助手链接' : '创建企业微信官方获客链接'" width="720px" destroy-on-close>
|
v-model="poolForm.member_admin_ids"
|
||||||
<el-form label-position="top" class="link-form">
|
:disabled="savingPool"
|
||||||
<div class="form-grid">
|
multiple
|
||||||
<el-form-item label="获客链接名称" required><el-input v-model="linkForm.name" maxlength="80" placeholder="用于企业微信和后台识别" /></el-form-item>
|
filterable
|
||||||
<el-form-item label="分组"><el-input v-model="linkForm.group_name" maxlength="60" placeholder="默认分组" /></el-form-item>
|
collapse-tags
|
||||||
</div>
|
collapse-tags-tooltip
|
||||||
<el-form-item v-if="!linkForm.legacy" label="获客成员" required>
|
placeholder="选择后由企业微信生成一个多人分流链接"
|
||||||
<el-select v-model="linkForm.member_admin_ids" multiple filterable collapse-tags collapse-tags-tooltip placeholder="仅显示当前角色和部门可管理、且已绑定企业微信的成员" style="width: 100%">
|
style="width: 100%"
|
||||||
<el-option v-for="member in overview.member_options" :key="member.id" :label="memberOptionLabel(member)" :value="Number(member.id)" />
|
>
|
||||||
</el-select>
|
<el-option v-for="member in overview.member_options" :key="member.id" :label="memberOptionLabel(member)" :value="Number(member.id)" />
|
||||||
<span class="form-tip">官方链接按这里的企业微信 userid 建立可见范围;越权成员不会出现在列表中。</span>
|
</el-select>
|
||||||
</el-form-item>
|
<span class="form-tip">保存后会为每名医助生成一条成员规则,官方链接地址始终只有一个。</span>
|
||||||
<el-form-item v-else label="企业微信获客助手链接" required>
|
</el-form-item>
|
||||||
<el-input v-model="linkForm.wecom_url" type="textarea" :rows="2" :placeholder="overview.customer_acquisition_link_example" />
|
<el-form-item label="添加客户时跳过验证">
|
||||||
<span class="form-tip">这是历史手工链接,只更新本地分流规则,不会调用企业微信创建或修改接口。</span>
|
<el-switch v-model="poolForm.skip_verify" :disabled="savingPool" :active-value="1" :inactive-value="0" active-text="跳过验证" inactive-text="需要验证" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<div class="form-grid">
|
<el-form-item label="兜底获客助手链接">
|
||||||
<el-form-item label="分流权重" required><el-input-number v-model="linkForm.weight" :min="1" :max="100" controls-position="right" /></el-form-item>
|
<el-input v-model="poolForm.fallback_url" :disabled="savingPool" placeholder="可选;仅供旧版兼容跳转使用" />
|
||||||
<el-form-item label="每日点击上限"><el-input-number v-model="linkForm.daily_limit" :min="0" :max="1000000" controls-position="right" /><span class="form-tip">0 表示不限</span></el-form-item>
|
<span class="form-tip">新生成的官方直链不经过本站跳转;此项仅兼容旧安装代码。</span>
|
||||||
</div>
|
</el-form-item>
|
||||||
<el-form-item v-if="!linkForm.legacy" label="添加客户时跳过验证">
|
<el-form-item label="运行状态"><el-switch v-model="poolForm.status" :disabled="savingPool" :active-value="1" :inactive-value="0" active-text="运行" inactive-text="停用" /></el-form-item>
|
||||||
<el-switch v-model="linkForm.skip_verify" :active-value="1" :inactive-value="0" active-text="跳过验证" inactive-text="需要验证" />
|
</el-form>
|
||||||
</el-form-item>
|
<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-form-item label="上线状态"><el-switch v-model="linkForm.status" :active-value="1" :inactive-value="0" active-text="上线" inactive-text="下线" /></el-form-item>
|
</el-dialog>
|
||||||
<el-form-item label="生效时间段">
|
|
||||||
<el-date-picker v-model="linkForm.active_range" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" start-placeholder="开始时间" end-placeholder="结束时间" range-separator="至" />
|
<el-dialog v-model="memberDialogVisible" title="编辑医助分流规则" width="560px" destroy-on-close :close-on-click-modal="false">
|
||||||
<span class="form-tip">不选择表示长期有效</span>
|
<el-form label-position="top">
|
||||||
</el-form-item>
|
<el-form-item label="获客医助">
|
||||||
<el-form-item label="备注"><el-input v-model="linkForm.remark" maxlength="255" show-word-limit /></el-form-item>
|
<el-input :model-value="memberForm.name ? `${memberForm.name} · ${memberForm.userid}` : memberForm.userid" readonly />
|
||||||
</el-form>
|
</el-form-item>
|
||||||
<template #footer><el-button @click="linkDialogVisible = false">取消</el-button><el-button type="primary" :loading="savingLink" @click="saveLink">保存链接</el-button></template>
|
<div class="rule-form-grid">
|
||||||
</el-dialog>
|
<el-form-item label="权重" required>
|
||||||
|
<el-input-number v-model="memberForm.weight" :min="1" :max="100" :disabled="savingMember" style="width: 100%" />
|
||||||
|
<span class="form-tip">权重越高,在后续回调调度中被选中的频率越高。</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="每日获客上限">
|
||||||
|
<el-input-number v-model="memberForm.daily_limit" :min="0" :max="1000000" :disabled="savingMember" style="width: 100%" />
|
||||||
|
<span class="form-tip">0 表示不限;数量按实际获客回调累计。</span>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="有效时间">
|
||||||
|
<el-date-picker v-model="memberForm.active_range" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD HH:mm:ss" :disabled="savingMember" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="上线状态">
|
||||||
|
<el-switch v-model="memberForm.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用" :disabled="savingMember" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注"><el-input v-model="memberForm.remark" type="textarea" :rows="2" maxlength="255" show-word-limit :disabled="savingMember" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer><el-button :disabled="savingMember" @click="memberDialogVisible = false">取消</el-button><el-button type="primary" :loading="savingMember" @click="saveMemberRule">保存规则</el-button></template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" name="firstVisitWecomPromotionPage">
|
<script setup lang="ts" name="firstVisitWecomPromotionPage">
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
ArrowDown, ArrowRight, ChatDotRound, CircleCheck, Connection, DataAnalysis, Delete,
|
ArrowRight, ChatDotRound, CircleCheck, Connection, DataAnalysis, Delete,
|
||||||
DocumentCopy, Edit, Key, Link, Lock, Mouse, OfficeBuilding, Plus,
|
DocumentCopy, Edit, Key, Link, Lock, Mouse, OfficeBuilding, Plus,
|
||||||
Promotion, Refresh, Search, SetUp, User, Warning
|
Promotion, Refresh, Search, SetUp, User, Warning
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
wecomPromotionCheckApiPermission,
|
wecomPromotionCheckApiPermission,
|
||||||
wecomPromotionCustomerStats,
|
wecomPromotionCustomerStats,
|
||||||
wecomPromotionDeleteLink,
|
wecomPromotionDeletePool,
|
||||||
wecomPromotionDeleteRemoteLink,
|
wecomPromotionOverview,
|
||||||
wecomPromotionDeletePool,
|
wecomPromotionSaveMember,
|
||||||
wecomPromotionOverview,
|
wecomPromotionSavePool,
|
||||||
wecomPromotionRemoteLinkDetail,
|
wecomPromotionSyncCustomers,
|
||||||
wecomPromotionSaveLink,
|
wecomPromotionToggleMember
|
||||||
wecomPromotionSavePool,
|
} from '@/api/first_visit'
|
||||||
wecomPromotionSyncCustomers,
|
|
||||||
wecomPromotionSyncRemoteLinks,
|
|
||||||
wecomPromotionToggleLink
|
|
||||||
} from '@/api/first_visit'
|
|
||||||
import type { WecomPromotionCustomerChatStatus } from '@/api/first_visit'
|
import type { WecomPromotionCustomerChatStatus } from '@/api/first_visit'
|
||||||
|
|
||||||
import WecomFloatingWidgetBuilder from './components/WecomFloatingWidgetBuilder.vue'
|
import WecomFloatingWidgetBuilder from './components/WecomFloatingWidgetBuilder.vue'
|
||||||
@@ -447,17 +438,16 @@ const emptyOverview = () => ({
|
|||||||
const overview = reactive(emptyOverview())
|
const overview = reactive(emptyOverview())
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const activeTab = ref<TabName>('links')
|
const activeTab = ref<TabName>('links')
|
||||||
const selectedPoolId = ref<number>()
|
const selectedPoolId = ref<number>()
|
||||||
const selectedInstallPoolId = ref<number>()
|
const selectedInstallPoolId = ref<number>()
|
||||||
const poolDialogVisible = ref(false)
|
const poolDialogVisible = ref(false)
|
||||||
const linkDialogVisible = ref(false)
|
const savingPool = ref(false)
|
||||||
const savingPool = ref(false)
|
const checkingApi = ref(false)
|
||||||
const savingLink = ref(false)
|
const memberDialogVisible = ref(false)
|
||||||
const checkingApi = ref(false)
|
const savingMember = ref(false)
|
||||||
const syncingRemote = ref(false)
|
const togglingMemberId = ref(0)
|
||||||
const refreshingLinkId = ref(0)
|
const poolForm = reactive({ id: 0, name: '', fallback_url: '', status: 1, member_admin_ids: [] as number[], skip_verify: 0, main_url: '' })
|
||||||
const poolForm = reactive({ id: 0, name: '', fallback_url: '', status: 1 })
|
const memberForm = reactive({ id: 0, name: '', userid: '', weight: 1, daily_limit: 0, status: 1, active_range: [] as string[], remark: '' })
|
||||||
const linkForm = reactive({ id: 0, pool_id: 0, name: '', group_name: '默认分组', wecom_url: '', member_admin_ids: [] as number[], skip_verify: 0, legacy: false, weight: 1, status: 1, daily_limit: 0, active_range: [] as string[], remark: '' })
|
|
||||||
const customerStatsLoading = ref(false)
|
const customerStatsLoading = ref(false)
|
||||||
const customerStatsLoaded = ref(false)
|
const customerStatsLoaded = ref(false)
|
||||||
const syncingCustomers = ref(false)
|
const syncingCustomers = ref(false)
|
||||||
@@ -475,10 +465,8 @@ const customerStats = reactive({
|
|||||||
meta: { last_synced_at: '' }
|
meta: { last_synced_at: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedPool = computed(() => overview.pools.find((item: any) => Number(item.id) === selectedPoolId.value))
|
const selectedPool = computed(() => overview.pools.find((item: any) => Number(item.id) === selectedPoolId.value))
|
||||||
const selectedLinks = computed(() => overview.links.filter((item: any) => Number(item.pool_id) === selectedPoolId.value))
|
const selectedMemberRules = computed(() => Array.isArray(selectedPool.value?.member_rules) ? selectedPool.value.member_rules : [])
|
||||||
const selectedOfficialCount = computed(() => selectedLinks.value.filter((item: any) => Boolean(item.remote_link_id)).length)
|
|
||||||
const selectedLegacyCount = computed(() => selectedLinks.value.length - selectedOfficialCount.value)
|
|
||||||
const selectedInstallPool = computed(() => overview.pools.find((item: any) => Number(item.id) === selectedInstallPoolId.value))
|
const selectedInstallPool = computed(() => overview.pools.find((item: any) => Number(item.id) === selectedInstallPoolId.value))
|
||||||
const customerLinkOptions = computed(() => customerStats.link_options.length
|
const customerLinkOptions = computed(() => customerStats.link_options.length
|
||||||
? customerStats.link_options
|
? customerStats.link_options
|
||||||
@@ -516,24 +504,37 @@ async function loadOverview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkCount(poolId: number) {
|
function openPoolDialog(pool?: any) {
|
||||||
return overview.links.filter((item: any) => Number(item.pool_id) === Number(poolId)).length
|
Object.assign(poolForm, pool ? {
|
||||||
}
|
id: Number(pool.id),
|
||||||
|
name: pool.name,
|
||||||
function openPoolDialog(pool?: any) {
|
fallback_url: pool.fallback_url || '',
|
||||||
Object.assign(poolForm, pool ? { id: Number(pool.id), name: pool.name, fallback_url: pool.fallback_url || '', status: Number(pool.status) } : { id: 0, name: '', fallback_url: '', status: 1 })
|
status: Number(pool.status),
|
||||||
poolDialogVisible.value = true
|
member_admin_ids: Array.isArray(pool.member_admin_ids) ? pool.member_admin_ids.map(Number) : [],
|
||||||
}
|
skip_verify: Number(pool.skip_verify) === 1 ? 1 : 0,
|
||||||
|
main_url: pool.main_url || ''
|
||||||
async function savePool() {
|
} : {
|
||||||
if (!poolForm.name.trim()) return ElMessage.warning('请输入分流方案名称')
|
id: 0,
|
||||||
savingPool.value = true
|
name: '',
|
||||||
|
fallback_url: '',
|
||||||
|
status: 1,
|
||||||
|
member_admin_ids: [],
|
||||||
|
skip_verify: 0,
|
||||||
|
main_url: ''
|
||||||
|
})
|
||||||
|
poolDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePool() {
|
||||||
|
if (!poolForm.name.trim()) return ElMessage.warning('请输入分流方案名称')
|
||||||
|
if (!poolForm.member_admin_ids.length) return ElMessage.warning('请至少选择一名获客医助')
|
||||||
|
savingPool.value = true
|
||||||
try {
|
try {
|
||||||
const result: any = await wecomPromotionSavePool({ ...poolForm })
|
const result: any = await wecomPromotionSavePool({ ...poolForm })
|
||||||
poolDialogVisible.value = false
|
poolDialogVisible.value = false
|
||||||
await loadOverview()
|
await loadOverview()
|
||||||
if (result?.id) selectedPoolId.value = Number(result.id)
|
if (result?.id) selectedPoolId.value = Number(result.id)
|
||||||
ElMessage.success('分流方案已保存')
|
ElMessage.success(poolForm.id ? '分流方案已保存' : '分流方案和官方获客链接已创建')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(error?.message || '分流方案保存失败')
|
ElMessage.error(error?.message || '分流方案保存失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -541,8 +542,8 @@ async function savePool() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removePool(pool: any) {
|
async function removePool(pool: any) {
|
||||||
await ElMessageBox.confirm(`删除“${pool.name}”后,该方案下的获客助手链接也会停用。确认继续?`, '删除分流方案', { type: 'warning' })
|
await ElMessageBox.confirm(`删除“${pool.name}”后,本站将停止使用该链接,但不会永久删除企业微信后台中的官方链接。确认继续?`, '删除分流方案', { type: 'warning' })
|
||||||
try {
|
try {
|
||||||
await wecomPromotionDeletePool({ id: Number(pool.id) })
|
await wecomPromotionDeletePool({ id: Number(pool.id) })
|
||||||
ElMessage.success('分流方案已删除')
|
ElMessage.success('分流方案已删除')
|
||||||
@@ -550,50 +551,60 @@ async function removePool(pool: any) {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(error?.message || '删除失败')
|
ElMessage.error(error?.message || '删除失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openLinkDialog(row?: any) {
|
function openMemberDialog(row: any) {
|
||||||
if (!selectedPool.value) return
|
const range = Number(row.active_start) > 0 && Number(row.active_end) > 0
|
||||||
const range = row && Number(row.active_start) > 0 && Number(row.active_end) > 0
|
? [formatPickerTime(row.active_start), formatPickerTime(row.active_end)]
|
||||||
? [formatPickerTime(row.active_start), formatPickerTime(row.active_end)] : []
|
: []
|
||||||
const selectedUserIds = new Set<string>((row?.range_userids || []).map((item: unknown) => String(item)))
|
Object.assign(memberForm, {
|
||||||
const selectedMemberIds = overview.member_options
|
id: Number(row.id),
|
||||||
.filter((member: any) => selectedUserIds.has(String(member.userid)))
|
name: row.name || '',
|
||||||
.map((member: any) => Number(member.id))
|
userid: row.userid || '',
|
||||||
Object.assign(linkForm, row ? {
|
weight: Number(row.weight) || 1,
|
||||||
id: Number(row.id), pool_id: Number(row.pool_id),
|
daily_limit: Number(row.daily_limit) || 0,
|
||||||
name: row.name, group_name: row.group_name || '默认分组', wecom_url: row.wecom_url,
|
status: Number(row.enabled) === 1 ? 1 : 0,
|
||||||
member_admin_ids: selectedMemberIds, skip_verify: Number(row.skip_verify) === 1 ? 1 : 0, legacy: !row.is_official,
|
active_range: range,
|
||||||
weight: Number(row.weight) || 1, status: Number(row.status), daily_limit: Number(row.daily_limit) || 0,
|
remark: row.remark || ''
|
||||||
active_range: range, remark: row.remark || ''
|
})
|
||||||
} : {
|
memberDialogVisible.value = true
|
||||||
id: 0, pool_id: Number(selectedPool.value.id), name: '', group_name: '默认分组',
|
}
|
||||||
wecom_url: '', member_admin_ids: [], skip_verify: 0, legacy: false,
|
|
||||||
weight: 1, status: 1, daily_limit: 0, active_range: [], remark: ''
|
async function saveMemberRule() {
|
||||||
})
|
savingMember.value = true
|
||||||
linkDialogVisible.value = true
|
try {
|
||||||
}
|
const result: any = await wecomPromotionSaveMember({
|
||||||
|
...memberForm,
|
||||||
async function saveLink() {
|
active_start: memberForm.active_range?.[0] || '',
|
||||||
if (!linkForm.name.trim()) return ElMessage.warning('请输入获客链接名称')
|
active_end: memberForm.active_range?.[1] || ''
|
||||||
if (linkForm.legacy && !linkForm.wecom_url.trim()) return ElMessage.warning('请填写企业微信获客助手链接')
|
})
|
||||||
if (!linkForm.legacy && !linkForm.member_admin_ids.length) return ElMessage.warning('请至少选择一名获客成员')
|
memberDialogVisible.value = false
|
||||||
savingLink.value = true
|
await loadOverview()
|
||||||
try {
|
result?.sync_error
|
||||||
await wecomPromotionSaveLink({
|
? ElMessage.warning('规则已保存,企业微信成员范围将在后台自动重试同步')
|
||||||
...linkForm,
|
: ElMessage.success('医助分流规则已保存')
|
||||||
active_start: linkForm.active_range?.[0] || '',
|
} catch (error: any) {
|
||||||
active_end: linkForm.active_range?.[1] || ''
|
ElMessage.error(error?.message || '医助分流规则保存失败')
|
||||||
})
|
} finally {
|
||||||
linkDialogVisible.value = false
|
savingMember.value = false
|
||||||
await loadOverview()
|
}
|
||||||
ElMessage.success(linkForm.legacy ? '本地分流规则已保存' : '企业微信官方获客链接已保存')
|
}
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '获客助手链接保存失败')
|
async function handleMemberToggle(row: any, value: unknown) {
|
||||||
} finally {
|
togglingMemberId.value = Number(row.id)
|
||||||
savingLink.value = false
|
try {
|
||||||
}
|
const result: any = await wecomPromotionToggleMember({ id: Number(row.id), status: value ? 1 : 0 })
|
||||||
}
|
await loadOverview()
|
||||||
|
result?.sync_error
|
||||||
|
? ElMessage.warning('成员状态已保存,企业微信成员范围将在后台自动重试同步')
|
||||||
|
: ElMessage.success(value ? '医助已启用' : '医助已禁用并退出后续调度')
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error?.message || '成员状态更新失败')
|
||||||
|
await loadOverview()
|
||||||
|
} finally {
|
||||||
|
togglingMemberId.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkApiPermission() {
|
async function checkApiPermission() {
|
||||||
checkingApi.value = true
|
checkingApi.value = true
|
||||||
@@ -710,119 +721,39 @@ async function syncCustomers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncRemoteLinks() {
|
function eligibility(row: any) {
|
||||||
if (!selectedPool.value) return ElMessage.warning('请先选择分流方案')
|
const now = Math.floor(Date.now() / 1000)
|
||||||
syncingRemote.value = true
|
if (Number(row.enabled) !== 1) return { label: '已禁用', className: 'is-muted' }
|
||||||
try {
|
if (Number(row.active_start) > 0 && Number(row.active_start) > now) return { label: '尚未生效', className: 'is-waiting' }
|
||||||
const result: any = await wecomPromotionSyncRemoteLinks({ pool_id: Number(selectedPool.value.id) })
|
|
||||||
const message = `扫描 ${Number(result?.scanned || 0)} 条,新建 ${Number(result?.created || 0)} 条,更新 ${Number(result?.updated || 0)} 条,按权限跳过 ${Number(result?.skipped || 0)} 条,失败 ${Number(result?.failed || 0)} 条`
|
|
||||||
if (Number(result?.scanned || 0) === 0) {
|
|
||||||
await ElMessageBox.alert(
|
|
||||||
`${result?.empty_reason || '当前应用没有可同步的官方获客链接'}\n\n${result?.suggestion || ''}`,
|
|
||||||
'没有可同步的官方链接',
|
|
||||||
{ type: 'warning', confirmButtonText: '知道了' }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
result?.failed ? ElMessage.warning(message) : ElMessage.success(message)
|
|
||||||
}
|
|
||||||
await loadOverview()
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '同步企业微信获客链接失败')
|
|
||||||
} finally {
|
|
||||||
syncingRemote.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshRemoteDetail(row: any) {
|
|
||||||
refreshingLinkId.value = Number(row.id)
|
|
||||||
try {
|
|
||||||
await wecomPromotionRemoteLinkDetail({ id: Number(row.id) })
|
|
||||||
ElMessage.success('企业微信官方详情已刷新')
|
|
||||||
await loadOverview()
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '刷新官方详情失败')
|
|
||||||
} finally {
|
|
||||||
refreshingLinkId.value = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLinkCommand(command: string, row: any) {
|
|
||||||
if (command === 'local') {
|
|
||||||
await removeLink(row)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (command !== 'remote') return
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
`将永久删除企业微信后台中的“${row.name}”,现有推广页面会立即失效,且无法恢复。确认继续?`,
|
|
||||||
'永久删除企业微信获客链接',
|
|
||||||
{ type: 'error', confirmButtonText: '永久删除', cancelButtonText: '取消' }
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
await wecomPromotionDeleteRemoteLink({ id: Number(row.id) })
|
|
||||||
ElMessage.success('企业微信获客链接已永久删除,本地记录已保留并停止分流')
|
|
||||||
await loadOverview()
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '永久删除失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleToggle(row: any, value: unknown) {
|
|
||||||
try {
|
|
||||||
await wecomPromotionToggleLink({ id: Number(row.id), status: value ? 1 : 0 })
|
|
||||||
row.status = value ? 1 : 0
|
|
||||||
ElMessage.success(value ? '链接已上线' : '链接已下线')
|
|
||||||
await loadOverview()
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '状态更新失败')
|
|
||||||
await loadOverview()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeLink(row: any) {
|
|
||||||
await ElMessageBox.confirm(`仅从本地分流池移除“${row.name}”,不会删除企业微信后台的官方链接。确认继续?`, '从本地移除', { type: 'warning' })
|
|
||||||
try {
|
|
||||||
await wecomPromotionDeleteLink({ id: Number(row.id) })
|
|
||||||
ElMessage.success('已从本地分流池移除')
|
|
||||||
await loadOverview()
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(error?.message || '删除失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function eligibility(row: any) {
|
|
||||||
const now = Math.floor(Date.now() / 1000)
|
|
||||||
if (Number(row.remote_status) === 2) return { label: '企业微信已删除', className: 'is-error' }
|
|
||||||
if (row.valid_customer_acquisition_link === false) return { label: '非获客助手链接', className: 'is-error' }
|
|
||||||
if (Number(row.status) !== 1) return { label: '已下线', className: 'is-muted' }
|
|
||||||
if (Number(row.active_start) > 0 && Number(row.active_start) > now) return { label: '尚未生效', className: 'is-waiting' }
|
|
||||||
if (Number(row.active_end) > 0 && Number(row.active_end) < now) return { label: '已过期', className: 'is-error' }
|
if (Number(row.active_end) > 0 && Number(row.active_end) < now) return { label: '已过期', className: 'is-error' }
|
||||||
if (Number(row.daily_limit) > 0 && todayCount(row) >= Number(row.daily_limit)) return { label: '今日已达上限', className: 'is-waiting' }
|
if (Number(row.daily_limit) > 0 && todayCount(row) >= Number(row.daily_limit)) return { label: '今日已达上限', className: 'is-waiting' }
|
||||||
return { label: '可参与分流', className: 'is-ok' }
|
const isSyncTarget = Boolean(row.is_current || row.is_applied)
|
||||||
}
|
if (isSyncTarget && Number(row.sync_status) === 3) return { label: '企微同步重试中', className: 'is-waiting' }
|
||||||
|
if (isSyncTarget && Number(row.sync_status) === 4) return { label: '暂无可用调度', className: 'is-error' }
|
||||||
|
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())
|
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
|
return row.today_date === today ? Number(row.today_count || 0) : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function memberOptionLabel(member: any) {
|
function memberOptionLabel(member: any) {
|
||||||
const departments = Array.isArray(member.dept_names) && member.dept_names.length
|
const departments = Array.isArray(member.dept_names) && member.dept_names.length
|
||||||
? member.dept_names.join(' / ')
|
? member.dept_names.join(' / ')
|
||||||
: '未分部门'
|
: '未分部门'
|
||||||
return `${member.name} · ${departments} · ${member.userid}`
|
return `${member.name} · ${departments} · ${member.userid}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function memberSummary(pool: any) {
|
||||||
|
const ids = Array.isArray(pool?.member_admin_ids) ? pool.member_admin_ids : []
|
||||||
|
return ids.length ? `${ids.length} 名医助` : '未选择医助'
|
||||||
|
}
|
||||||
|
|
||||||
function remoteMemberLabel(row: any) {
|
function memberRuleDetail(row: any) {
|
||||||
const userIds = Array.isArray(row.range_userids) ? row.range_userids : []
|
const departments = Array.isArray(row.dept_names) && row.dept_names.length ? row.dept_names.join(' / ') : '未分部门'
|
||||||
const names = userIds.map((userid: unknown) => {
|
return `${departments} · ${row.userid || '未绑定 userid'}`
|
||||||
const member = overview.member_options.find((item: any) => String(item.userid) === String(userid))
|
}
|
||||||
return member?.name || String(userid)
|
|
||||||
})
|
|
||||||
if (names.length) return names.length > 2 ? `${names.slice(0, 2).join('、')} 等 ${names.length} 人` : names.join('、')
|
|
||||||
const departments = Array.isArray(row.range_department_ids) ? row.range_department_ids : []
|
|
||||||
if (departments.length) return `企微部门 ${departments.join('、')}`
|
|
||||||
return row.is_official ? '未返回成员范围' : '手工配置'
|
|
||||||
}
|
|
||||||
|
|
||||||
function memberFilterLabel(member: any) {
|
function memberFilterLabel(member: any) {
|
||||||
const department = member.department_name || member.dept_name || ''
|
const department = member.department_name || member.dept_name || ''
|
||||||
@@ -909,17 +840,17 @@ function formatCustomerRate(value: unknown) {
|
|||||||
return `${Number(value || 0).toFixed(1)}%`
|
return `${Number(value || 0).toFixed(1)}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatNumber(value: unknown) {
|
function formatNumber(value: unknown) {
|
||||||
return Number(value || 0).toLocaleString('zh-CN')
|
return Number(value || 0).toLocaleString('zh-CN')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPickerTime(value: unknown) {
|
||||||
|
const date = new Date(Number(value || 0) * 1000)
|
||||||
|
const pad = (number: number) => String(number).padStart(2, '0')
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||||
|
}
|
||||||
|
|
||||||
function formatPickerTime(value: unknown) {
|
async function copyText(value: string, label: string) {
|
||||||
const date = new Date(Number(value || 0) * 1000)
|
|
||||||
const pad = (number: number) => String(number).padStart(2, '0')
|
|
||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyText(value: string, label: string) {
|
|
||||||
if (!value) return ElMessage.warning(`${label}暂无内容`)
|
if (!value) return ElMessage.warning(`${label}暂无内容`)
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(value)
|
await navigator.clipboard.writeText(value)
|
||||||
@@ -930,8 +861,9 @@ async function copyText(value: string, label: string) {
|
|||||||
textarea.style.opacity = '0'
|
textarea.style.opacity = '0'
|
||||||
document.body.appendChild(textarea)
|
document.body.appendChild(textarea)
|
||||||
textarea.select()
|
textarea.select()
|
||||||
document.execCommand('copy')
|
const copied = document.execCommand('copy')
|
||||||
textarea.remove()
|
textarea.remove()
|
||||||
|
if (!copied) throw new Error('copy command failed')
|
||||||
}
|
}
|
||||||
ElMessage.success(`${label}已复制`)
|
ElMessage.success(`${label}已复制`)
|
||||||
}
|
}
|
||||||
@@ -1010,7 +942,7 @@ h1, h2, h3, p { margin: 0; }
|
|||||||
.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; }
|
.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; }
|
.configuration-actions { display: flex; align-items: center; gap: 14px; margin-top: 16px; }
|
||||||
.install-pool-select { width: 220px; }
|
.install-pool-select { width: 220px; }
|
||||||
.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%; }
|
.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: 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 { 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; } }
|
@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; } }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -45,9 +45,10 @@ export default defineConfig(({ mode }) => {
|
|||||||
: 'http://127.0.0.1:8080'
|
: 'http://127.0.0.1:8080'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base: '/admin/',
|
base: '/admin/',
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
port: 5555,
|
||||||
|
host: '0.0.0.0',
|
||||||
hmr: true,
|
hmr: true,
|
||||||
open: true,
|
open: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
Reference in New Issue
Block a user