diff --git a/package-lock.json b/package-lock.json
index d539710..a8ba3c9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"axios": "^1.13.5",
"echarts": "^6.0.0",
"element-plus": "^2.13.2",
- "goeasy": "^2.14.9",
+ "goeasy": "2.13.24",
"lodash": "^4.18.1",
"mitt": "^3.0.1",
"sortablejs": "^1.15.7",
@@ -1500,9 +1500,9 @@
}
},
"node_modules/goeasy": {
- "version": "2.14.9",
- "resolved": "https://registry.npmjs.org/goeasy/-/goeasy-2.14.9.tgz",
- "integrity": "sha512-AlHx7PCWfIUWKAvjILJx6AzM1GmquGM1MxxA7gPH6TiR9eTtiieJNk9nIXvVYRm0iTC8AfEnekRY2Clghm9xrA=="
+ "version": "2.13.24",
+ "resolved": "https://registry.npmjs.org/goeasy/-/goeasy-2.13.24.tgz",
+ "integrity": "sha512-DIoAS4C3E6ViQxW6dUDanQOkKVRwZyhFMLZkwuIqgoamIE36E9fHu0RDumNq4ec+aRt9FnbpA7/ibl38OSTTvA=="
},
"node_modules/gopd": {
"version": "1.2.0",
@@ -2739,9 +2739,9 @@
}
},
"goeasy": {
- "version": "2.14.9",
- "resolved": "https://registry.npmjs.org/goeasy/-/goeasy-2.14.9.tgz",
- "integrity": "sha512-AlHx7PCWfIUWKAvjILJx6AzM1GmquGM1MxxA7gPH6TiR9eTtiieJNk9nIXvVYRm0iTC8AfEnekRY2Clghm9xrA=="
+ "version": "2.13.24",
+ "resolved": "https://registry.npmjs.org/goeasy/-/goeasy-2.13.24.tgz",
+ "integrity": "sha512-DIoAS4C3E6ViQxW6dUDanQOkKVRwZyhFMLZkwuIqgoamIE36E9fHu0RDumNq4ec+aRt9FnbpA7/ibl38OSTTvA=="
},
"gopd": {
"version": "1.2.0",
diff --git a/package.json b/package.json
index 9baa1bf..485687a 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"axios": "^1.13.5",
"echarts": "^6.0.0",
"element-plus": "^2.13.2",
- "goeasy": "^2.14.9",
+ "goeasy": "2.13.24",
"lodash": "^4.18.1",
"mitt": "^3.0.1",
"sortablejs": "^1.15.7",
diff --git a/src/components/ChatDialog.vue b/src/components/ChatDialog.vue
index 9c618df..c12f25b 100644
--- a/src/components/ChatDialog.vue
+++ b/src/components/ChatDialog.vue
@@ -5,7 +5,7 @@
width="700px"
:close-on-click-modal="false"
destroy-on-close
- @open="onDialogOpen"
+ @opened="onDialogOpened"
@close="onDialogClose"
>
@@ -54,7 +54,7 @@
- 发送
+ 发送
@@ -63,16 +63,24 @@
@@ -311,4 +344,4 @@ onUnmounted(() => {
.send-sending { text-align: right; margin-top: 2px; margin-right: 42px; }
.input-area { padding: 8px 0; }
.loading-tip { text-align: center; color: #aaa; font-size: 13px; padding: 10px; }
-
\ No newline at end of file
+
diff --git a/src/components/ClubScopeHint.vue b/src/components/ClubScopeHint.vue
new file mode 100644
index 0000000..d9990f9
--- /dev/null
+++ b/src/components/ClubScopeHint.vue
@@ -0,0 +1,31 @@
+
+ {{ hint }}
+
+
+
+
+
diff --git a/src/components/KefuConnect.vue b/src/components/KefuConnect.vue
index c1b10e1..0a2452b 100644
--- a/src/components/KefuConnect.vue
+++ b/src/components/KefuConnect.vue
@@ -3,91 +3,108 @@
\ No newline at end of file
+
diff --git a/src/components/UserScopeHint.vue b/src/components/UserScopeHint.vue
new file mode 100644
index 0000000..fa6daf5
--- /dev/null
+++ b/src/components/UserScopeHint.vue
@@ -0,0 +1,61 @@
+
+
+
{{ hint }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Water_tixianshezhiyemian.vue b/src/components/Water_tixianshezhiyemian.vue
index c235a5e..e74c926 100644
--- a/src/components/Water_tixianshezhiyemian.vue
+++ b/src/components/Water_tixianshezhiyemian.vue
@@ -72,6 +72,7 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
+import { JITUAN_API } from '@/utils/club-context'
import WaterTankFishbowl from '@/components/WaterTankFishbowl.vue'
const router = useRouter()
@@ -119,7 +120,7 @@ const waterRemainingPercent = (role) => {
const fetchSettings = async () => {
loading.value = true
try {
- const res = await request.post('/houtai/hthqtxpz', { username: username.value })
+ const res = await request.post(JITUAN_API.withdrawGet, { username: username.value })
if (res.code === 0) {
const data = res.data
roles.value.forEach(role => {
@@ -192,7 +193,7 @@ const saveSettings = async () => {
saving.value = true
try {
- const res = await request.post('/houtai/htxgtxsz', {
+ const res = await request.post(JITUAN_API.withdrawUpdate, {
username: username.value,
rates: editData.rates,
quotas: editData.quotas,
diff --git a/src/router/index.js b/src/router/index.js
index 7ec26ee..1892ab2 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -1,5 +1,13 @@
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/views/Layout.vue'
+import request from '@/utils/request'
+import {
+ ensureMenuAccess,
+ getMenuAccess,
+ getFirstVisiblePath,
+ isMenuSessionFresh,
+ JITUAN_API,
+} from '@/utils/club-context'
const routes = [
{
@@ -9,7 +17,7 @@ const routes = [
{
path: '/',
component: Layout,
- redirect: '/order/platform',
+ redirect: () => getFirstVisiblePath() || '/order/platform',
children: [
@@ -126,6 +134,11 @@ const routes = [
component: () => import('@/views/user/ZuzhangDetail.vue'),
meta: { title: '组长详情' }
},
+ {
+ path: 'user/identity-tag',
+ component: () => import('@/views/user/IdentityTag.vue'),
+ meta: { title: '身份装饰标签' }
+ },
// 提现管理(原有)
{
path: 'withdraw/audit',
@@ -148,6 +161,16 @@ const routes = [
meta: { title: '提现详情' }
},
// 管理员管理(原有)
+ {
+ path: 'admin/club-config',
+ component: () => import('@/views/admin/ClubConfig.vue'),
+ meta: { title: '俱乐部密钥配置' }
+ },
+ {
+ path: 'admin/data-scope',
+ component: () => import('@/views/admin/DataScope.vue'),
+ meta: { title: '数据范围配置' }
+ },
{
path: 'admin/role',
component: () => import('@/views/admin/Role.vue'),
@@ -168,6 +191,11 @@ const routes = [
component: () => import('@/views/admin/AdminUserDetail.vue'),
meta: { title: '后台用户详情' }
},
+ {
+ path: 'admin/operation-log',
+ component: () => import('@/views/admin/OperationLog.vue'),
+ meta: { title: '操作日志' }
+ },
// 处罚管理(原有)
{
path: 'punishment',
@@ -218,6 +246,31 @@ const routes = [
component: () => import('@/views/miniapp/PopupNotice.vue'),
meta: { title: '弹窗公告管理' }
},
+ {
+ path: 'miniapp/carousel',
+ component: () => import('@/views/miniapp/CarouselManage.vue'),
+ meta: { title: '轮播图与公告' }
+ },
+ {
+ path: 'miniapp/assets',
+ component: () => import('@/views/miniapp/MiniappAssets.vue'),
+ meta: { title: '图标与频道' }
+ },
+ {
+ path: 'miniapp/leixing',
+ component: () => import('@/views/miniapp/ShangpinLeixingClub.vue'),
+ meta: { title: '抢单商品类型' }
+ },
+ {
+ path: 'miniapp/kaohe-tags',
+ component: () => import('@/views/miniapp/KaoheTagClub.vue'),
+ meta: { title: '抢单考核标签' }
+ },
+ {
+ path: 'miniapp/dashou-exam',
+ component: () => import('@/views/miniapp/DashouExam.vue'),
+ meta: { title: '打手接单考试' }
+ },
// ========== 店铺管理(原有) ==========
{
path: 'shop/list',
@@ -250,13 +303,69 @@ const router = createRouter({
routes
})
-router.beforeEach((to, from, next) => {
+/** 详情页等子路由:父模块在 visible_paths 中即允许访问 */
+function isRouteAllowed(path, visiblePaths) {
+ if (!visiblePaths?.length) return true
+ if (visiblePaths.some((p) => p && (path === p || path.startsWith(`${p}/`)))) {
+ return true
+ }
+ const rules = [
+ { prefix: '/withdraw/detail/', parents: ['/withdraw/audit', '/withdraw/data'] },
+ { prefix: '/punishment/detail/', parents: ['/punishment'] },
+ { prefix: '/order/detail/', parents: ['/order/platform', '/order/merchant'] },
+ { prefix: '/cross-order/detail/', parents: ['/cross-order'] },
+ { prefix: '/user/player/detail/', parents: ['/user/player'] },
+ { prefix: '/user/guanli/detail/', parents: ['/user/guanli'] },
+ { prefix: '/user/shangjia/detail/', parents: ['/user/shangjia'] },
+ { prefix: '/user/zuzhang/detail/', parents: ['/user/zuzhang'] },
+ { prefix: '/product/detail/', parents: ['/product/list'] },
+ { prefix: '/admin/role/detail/', parents: ['/admin/role'] },
+ { prefix: '/admin/user/detail/', parents: ['/admin/user'] },
+ { prefix: '/miniapp/assets', parents: ['/miniapp/carousel', '/miniapp/assets'] },
+ ]
+ for (const { prefix, parents } of rules) {
+ if (path.startsWith(prefix)) {
+ return visiblePaths.some((p) => parents.includes(p))
+ }
+ }
+ return false
+}
+
+router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem('token')
if (to.path !== '/login' && !token) {
next('/login')
- } else {
- next()
+ return
}
+ if (to.path === '/login') {
+ next()
+ return
+ }
+
+ try {
+ await ensureMenuAccess(async () => {
+ const res = await request.get(JITUAN_API.menuAccess)
+ return res.code === 0 ? res.data : null
+ })
+ } catch (e) {
+ console.error('路由加载菜单权限失败:', e)
+ }
+
+ const access = getMenuAccess()
+ if (!isMenuSessionFresh() || !access?.visible_paths?.length) {
+ next()
+ return
+ }
+
+ const path = to.path
+ if (!isRouteAllowed(path, access.visible_paths)) {
+ const fallback = getFirstVisiblePath()
+ if (fallback && path !== fallback) {
+ next(fallback)
+ return
+ }
+ }
+ next()
})
export default router
\ No newline at end of file
diff --git a/src/utils/club-context.js b/src/utils/club-context.js
new file mode 100644
index 0000000..93592e9
--- /dev/null
+++ b/src/utils/club-context.js
@@ -0,0 +1,211 @@
+const CLUB_ID_KEY = 'admin_club_id';
+const CLUB_SCOPE_KEY = 'admin_club_scope';
+const CLUB_CONTEXT_KEY = 'admin_club_context';
+const MENU_ACCESS_KEY = 'kefu_menu_access';
+const MENU_SESSION_KEY = 'kefu_menu_session_v';
+const MENU_SESSION_VERSION = '7';
+const DEFAULT_CLUB_ID = 'xq';
+
+let menuAccessInflight = null;
+
+export function getAdminClubId() {
+ return localStorage.getItem(CLUB_ID_KEY) || DEFAULT_CLUB_ID;
+}
+
+export function getAdminClubScope() {
+ return localStorage.getItem(CLUB_SCOPE_KEY) || 'single';
+}
+
+export function setAdminClubContext(ctx) {
+ if (!ctx) return;
+ localStorage.setItem(CLUB_CONTEXT_KEY, JSON.stringify(ctx));
+ localStorage.setItem(CLUB_ID_KEY, ctx.club_id || DEFAULT_CLUB_ID);
+ localStorage.setItem(CLUB_SCOPE_KEY, ctx.scope === 'ALL_CLUBS' ? 'all' : 'single');
+}
+
+export function mergeServerClubContext(serverCtx) {
+ if (!serverCtx) return;
+ const prevScope = localStorage.getItem(CLUB_SCOPE_KEY);
+ const prevClubId = localStorage.getItem(CLUB_ID_KEY);
+ const merged = { ...serverCtx };
+ if (prevScope) {
+ merged.scope = prevScope === 'all' ? 'ALL_CLUBS' : 'SINGLE_CLUB';
+ merged.club_id = prevClubId || merged.club_id || DEFAULT_CLUB_ID;
+ }
+ localStorage.setItem(CLUB_CONTEXT_KEY, JSON.stringify(merged));
+ if (prevClubId) {
+ localStorage.setItem(CLUB_ID_KEY, prevClubId);
+ } else {
+ localStorage.setItem(CLUB_ID_KEY, merged.club_id || DEFAULT_CLUB_ID);
+ }
+ if (prevScope) {
+ localStorage.setItem(CLUB_SCOPE_KEY, prevScope);
+ } else {
+ localStorage.setItem(CLUB_SCOPE_KEY, merged.scope === 'ALL_CLUBS' ? 'all' : 'single');
+ }
+ return merged;
+}
+
+export function getAdminClubContext() {
+ try {
+ const raw = localStorage.getItem(CLUB_CONTEXT_KEY);
+ return raw ? JSON.parse(raw) : null;
+ } catch {
+ return null;
+ }
+}
+
+export function setMenuAccess(access) {
+ if (!access) return;
+ localStorage.setItem(MENU_ACCESS_KEY, JSON.stringify(access));
+ sessionStorage.setItem(MENU_SESSION_KEY, MENU_SESSION_VERSION);
+}
+
+export function clearMenuAccess() {
+ localStorage.removeItem(MENU_ACCESS_KEY);
+ sessionStorage.removeItem(MENU_SESSION_KEY);
+}
+
+export function getMenuAccess() {
+ try {
+ const raw = localStorage.getItem(MENU_ACCESS_KEY);
+ return raw ? JSON.parse(raw) : null;
+ } catch {
+ return null;
+ }
+}
+
+export function isMenuSessionFresh() {
+ return sessionStorage.getItem(MENU_SESSION_KEY) === MENU_SESSION_VERSION;
+}
+
+export function shouldShowAllMenus(access) {
+ if (!isMenuSessionFresh()) return true;
+ if (!access) return true;
+ if (access.menu_ready === false) return true;
+ if (!Array.isArray(access.visible_page_ids) || !Array.isArray(access.visible_paths)) return true;
+ return false;
+}
+
+export function canShowMenuPage(pageId) {
+ const access = getMenuAccess();
+ if (shouldShowAllMenus(access)) return true;
+ if (access.visible_page_ids.length === 0) return false;
+ return access.visible_page_ids.includes(pageId);
+}
+
+export function getFirstVisiblePath() {
+ const access = getMenuAccess();
+ if (!access || !Array.isArray(access.visible_paths) || access.visible_paths.length === 0) {
+ return null;
+ }
+ return access.visible_paths[0];
+}
+
+export function canSwitchClubByMenu() {
+ const access = getMenuAccess();
+ if (!access || !isMenuSessionFresh()) {
+ const ctx = getAdminClubContext();
+ return !!ctx?.can_switch_club;
+ }
+ return !!access.can_switch_club;
+}
+
+export async function ensureMenuAccess(fetcher) {
+ if (!localStorage.getItem('token')) return getMenuAccess();
+ if (isMenuSessionFresh() && getMenuAccess()) {
+ return getMenuAccess();
+ }
+ if (!menuAccessInflight) {
+ menuAccessInflight = (async () => {
+ try {
+ const data = await fetcher();
+ if (data) setMenuAccess(data);
+ return data;
+ } catch (e) {
+ console.error('ensureMenuAccess failed:', e);
+ return getMenuAccess();
+ } finally {
+ menuAccessInflight = null;
+ }
+ })();
+ }
+ return menuAccessInflight;
+}
+
+export function applyClubHeaders(config = {}) {
+ const headers = { ...(config.headers || {}) };
+ const clubId = getAdminClubId();
+ const scope = getAdminClubScope();
+ headers['X-Club-Id'] = clubId;
+ if (scope === 'all') {
+ headers['X-Club-Scope'] = 'all';
+ }
+ return { ...config, headers };
+}
+
+export const JITUAN_API = {
+ kefuLogin: '/jituan/auth/kefu-login',
+ meContext: '/jituan/auth/me-context',
+ menuAccess: '/jituan/auth/menu-access',
+ clubList: '/jituan/club/list',
+ caiwu: '/jituan/houtai/caiwu',
+ szxx: '/jituan/houtai/szxx',
+ auditLog: '/jituan/houtai/audit-log',
+ orderTypes: '/jituan/houtai/cwddhqlx',
+ orderFinance: '/jituan/houtai/cwhqjtddsj',
+ chongzhiFinance: '/jituan/houtai/cwqtczhq',
+ huiyuanBankuai: '/jituan/houtai/cwhybkhq',
+ huiyuanStats: '/jituan/houtai/hybkjtsj',
+ userSummary: '/jituan/houtai/user-summary',
+ dashouList: '/jituan/houtai/kefuhqdslb',
+ guanshiList: '/jituan/houtai/hthqgslb',
+ shangjiaList: '/jituan/houtai/hqsjgllb',
+ zuzhangList: '/jituan/houtai/hthqzzlb',
+ operationLog: '/jituan/houtai/hqczrz',
+ withdrawGet: '/jituan/houtai/hthqtxpz',
+ withdrawUpdate: '/jituan/houtai/htxgtxsz',
+ lunboList: '/jituan/houtai/lunbo-list',
+ lunboManage: '/jituan/houtai/lunbo-manage',
+ gonggao: '/jituan/houtai/gonggao',
+ tupianList: '/jituan/houtai/tupian-list',
+ tupianManage: '/jituan/houtai/tupian-manage',
+ popupList: '/jituan/houtai/hthqtcxx',
+ popupModify: '/jituan/houtai/htxgtcxx',
+ penaltyStats: '/jituan/houtai/htglyhqcfsltj',
+ penaltyList: '/jituan/houtai/hthqfklb',
+ penaltyAction: '/jituan/houtai/glyclfk',
+ penaltyCreate: '/jituan/houtai/htfksc',
+ kefuCfgl: '/jituan/houtai/kefu-cfgl',
+ adminAssignments: '/jituan/houtai/admin-assignments',
+ clubManage: '/jituan/houtai/club-manage',
+ memberList: '/jituan/houtai/hthqhylb',
+ memberUpdate: '/jituan/houtai/htxghyxx',
+ memberAdd: '/jituan/houtai/httjhy',
+ memberCatalog: '/jituan/houtai/hthycatalog',
+ memberEnable: '/jituan/houtai/hthysj',
+ memberDisable: '/jituan/houtai/hthyxj',
+ identityTagList: '/jituan/houtai/identity-tag/list',
+ identityTagManage: '/jituan/houtai/identity-tag/manage',
+ identityTagBindList: '/jituan/houtai/identity-tag/bind-list',
+ identityTagBind: '/jituan/houtai/identity-tag/bind',
+ miniappIconList: '/jituan/houtai/miniapp-icon-list',
+ miniappIconManage: '/jituan/houtai/miniapp-icon-manage',
+ pindaoManage: '/jituan/houtai/pindao-manage',
+ clubLeixingList: '/jituan/houtai/club-leixing-list',
+ clubLeixingCatalog: '/jituan/houtai/club-leixing-catalog',
+ clubLeixingEnable: '/jituan/houtai/club-leixing-enable',
+ clubLeixingDisable: '/jituan/houtai/club-leixing-disable',
+ clubLeixingIcon: '/jituan/houtai/club-leixing-icon',
+ clubKaoheList: '/jituan/houtai/club-kaohe-list',
+ clubKaoheCatalog: '/jituan/houtai/club-kaohe-catalog',
+ clubKaoheEnable: '/jituan/houtai/club-kaohe-enable',
+ clubKaoheDisable: '/jituan/houtai/club-kaohe-disable',
+ khggl: '/jituan/houtai/khggl',
+ khgglAdd: '/jituan/houtai/khggl-add',
+ shgxgsj: '/jituan/houtai/shgxgsj',
+ dashouExamConfig: '/jituan/houtai/dashou-exam-config',
+ dashouExamQuestionList: '/jituan/houtai/dashou-exam-question-list',
+ dashouExamQuestionManage: '/jituan/houtai/dashou-exam-question-manage',
+ dashouExamImageManage: '/jituan/houtai/dashou-exam-image-manage',
+};
diff --git a/src/utils/kefuChat.js b/src/utils/kefuChat.js
new file mode 100644
index 0000000..cfb0b2b
--- /dev/null
+++ b/src/utils/kefuChat.js
@@ -0,0 +1,406 @@
+import GoEasy from 'goeasy'
+
+export const KEFU_TEAM_ID = 'support_team'
+
+const NOTIFY_DEBOUNCE_MS = 2500
+const ONLINE_TIMEOUT_MS = 12000
+
+let lastNotifyAt = 0
+let onlineInFlight = null
+
+export function getCsteam() {
+ if (!window.goeasy?.im) return null
+ return window.goeasy.im.csteam(KEFU_TEAM_ID)
+}
+
+export function isGoeasyConnected() {
+ if (!window.goeasy) return false
+ try {
+ const status = window.goeasy.getConnectionStatus()
+ return status === 'connected' || status === 'reconnected'
+ } catch (e) {
+ return !!window.goeasy?.im
+ }
+}
+
+export function calcUnreadFromConversations(conversations = []) {
+ return conversations.reduce((sum, c) => sum + (c.unread || 0), 0)
+}
+
+/** 等待 GoEasy 内部 queryTeams 完成,初始化 teamIds,避免 online 成功回调 crash */
+function waitCsTeamsReady(csteam) {
+ return new Promise((resolve) => {
+ if (!csteam?.isOnline) {
+ resolve()
+ return
+ }
+ csteam.isOnline({
+ onSuccess: () => resolve(),
+ onFailed: () => resolve()
+ })
+ })
+}
+
+export function checkCsOnlineStatus() {
+ return new Promise((resolve) => {
+ const csteam = getCsteam()
+ if (!csteam) {
+ resolve(false)
+ return
+ }
+ csteam.isOnline({
+ onSuccess: (online) => {
+ window.__kefuCsOnline = !!online
+ resolve(!!online)
+ },
+ onFailed: () => resolve(false)
+ })
+ })
+}
+
+export function refreshKefuUnread(emitter) {
+ if (!window.goeasy?.im) return
+ const im = window.goeasy.im
+ let activeUnread = 0
+
+ im.latestConversations({
+ onSuccess: (result) => {
+ const content = result.content || {}
+ const convs = content.conversations || []
+ const csActive = convs.filter(c => c.type === 'cs' && !c.ended)
+ activeUnread = content.unreadTotal || calcUnreadFromConversations(csActive)
+
+ im.pendingConversations({
+ onSuccess: (pendingResult) => {
+ const pending = pendingResult.content?.conversations || []
+ const pendingUnread = calcUnreadFromConversations(pending)
+ emitter?.emit('agent-unread', activeUnread + pendingUnread)
+ },
+ onFailed: () => {
+ emitter?.emit('agent-unread', activeUnread)
+ }
+ })
+ },
+ onFailed: () => {}
+ })
+}
+
+function emitCsOnline(emitter, online) {
+ window.__kefuCsOnline = !!online
+ emitter?.emit('kefu-cs-online', window.__kefuCsOnline)
+}
+
+export function ensureCsOnline({ phone, onSuccess, onFailed, emitter, force = false } = {}) {
+ if (!window.goeasy?.im) {
+ onFailed?.({ content: '消息服务未连接' })
+ return
+ }
+ const csteam = getCsteam()
+ if (!csteam) {
+ onFailed?.({ content: '客服团队未初始化' })
+ return
+ }
+
+ const finishSuccess = () => {
+ emitCsOnline(emitter, true)
+ onSuccess?.()
+ }
+ const finishFailed = (error) => {
+ emitCsOnline(emitter, false)
+ onFailed?.(error)
+ }
+
+ if (!force && window.__kefuCsOnline) {
+ checkCsOnlineStatus().then((online) => {
+ if (online) {
+ finishSuccess()
+ } else {
+ window.__kefuCsOnline = false
+ doCsOnline(csteam, phone, emitter, finishSuccess, finishFailed)
+ }
+ })
+ return
+ }
+
+ if (force) {
+ window.__kefuCsOnline = false
+ onlineInFlight = null
+ }
+
+ doCsOnline(csteam, phone, emitter, finishSuccess, finishFailed)
+}
+
+function doCsOnline(csteam, phone, emitter, onSuccess, onFailed) {
+ if (onlineInFlight) {
+ onlineInFlight
+ .then(() => onSuccess?.())
+ .catch((e) => onFailed?.(e))
+ return
+ }
+
+ const displayName = phone || localStorage.getItem('username') || '客服'
+ let finished = false
+
+ onlineInFlight = waitCsTeamsReady(csteam).then(() => new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ if (finished) return
+ finished = true
+ onlineInFlight = null
+ emitCsOnline(emitter, false)
+ reject({ content: '上线超时,请检查网络后重试' })
+ }, ONLINE_TIMEOUT_MS)
+
+ csteam.online({
+ teamData: { name: '星阙客服中心', avatar: '' },
+ agentData: { name: displayName, avatar: '' },
+ onSuccess: () => {
+ if (finished) return
+ finished = true
+ clearTimeout(timer)
+ onlineInFlight = null
+ emitCsOnline(emitter, true)
+ resolve(true)
+ },
+ onFailed: (error) => {
+ if (finished) return
+ finished = true
+ clearTimeout(timer)
+ onlineInFlight = null
+ const msg = (error?.content || '').toLowerCase()
+ if (msg.includes('online') || msg.includes('已') || msg.includes('already')) {
+ emitCsOnline(emitter, true)
+ resolve(true)
+ return
+ }
+ console.error('[kefuChat] 上线失败:', error)
+ emitCsOnline(emitter, false)
+ reject(error)
+ }
+ })
+ }))
+
+ onlineInFlight
+ .then(() => onSuccess?.())
+ .catch((e) => onFailed?.(e))
+}
+
+export function goOnlineManually({ phone, emitter, onSuccess, onFailed } = {}) {
+ window.__kefuUserWantsOffline = false
+ ensureCsOnline({
+ phone,
+ emitter,
+ force: true,
+ onSuccess,
+ onFailed
+ })
+}
+
+export function goOfflineManually({ emitter, onSuccess, onFailed } = {}) {
+ window.__kefuUserWantsOffline = true
+ onlineInFlight = null
+ const csteam = getCsteam()
+ if (!csteam) {
+ emitCsOnline(emitter, false)
+ onSuccess?.()
+ return
+ }
+ csteam.offline({
+ onSuccess: () => {
+ emitCsOnline(emitter, false)
+ onSuccess?.()
+ },
+ onFailed: (error) => onFailed?.(error)
+ })
+}
+
+export function disconnectKefuChat() {
+ onlineInFlight = null
+ if (window.__kefuUnreadCleanup) {
+ window.__kefuUnreadCleanup()
+ window.__kefuUnreadCleanup = null
+ }
+ if (window.goeasy) {
+ try {
+ window.goeasy.disconnect({})
+ } catch (e) {
+ // ignore
+ }
+ window.goeasy = null
+ }
+ window.__kefuCsOnline = false
+ window.__kefuUserWantsOffline = false
+ window.__kefuAgentId = null
+ window.__kefuAppkey = null
+}
+
+export function getCsMessageCustomerId(message, teamId = KEFU_TEAM_ID, agentId) {
+ if (!message) return null
+ const tid = message.teamId || teamId
+ const aid = agentId || window.__kefuAgentId
+ const sender = message.senderId
+ const to = message.to || message.receiverId
+
+ try {
+ if (typeof message.customerId === 'function') {
+ const cid = message.customerId()
+ if (cid && cid !== tid) return cid
+ }
+ } catch (e) {
+ // ignore
+ }
+
+ const parties = [sender, to].filter(Boolean)
+ for (const p of parties) {
+ if (p !== tid && p !== aid && !String(p).startsWith('KF')) return p
+ }
+ if (sender === aid && to) return to
+ if (to === aid && sender) return sender
+ if (sender === tid && to) return to
+ if (to === tid && sender) return sender
+ return null
+}
+
+export function isCsMessageForCustomer(message, customerId, teamId = KEFU_TEAM_ID, agentId) {
+ if (!message || !customerId) return false
+ if (message.teamId && message.teamId !== teamId) return false
+ const cid = getCsMessageCustomerId(message, teamId, agentId)
+ return cid === customerId
+}
+
+export function buildCsTextMessage(teamId, customerId, customerData, text) {
+ const message = window.goeasy.im.createTextMessage({
+ text,
+ to: {
+ type: GoEasy.IM_SCENE.CS,
+ id: customerId,
+ data: customerData || { name: customerId, avatar: '' }
+ }
+ })
+ message.teamId = teamId
+ message.accepted = true
+ return message
+}
+
+export function ensureCustomerAccepted(customerId, customerData) {
+ return new Promise((resolve, reject) => {
+ if (!window.goeasy?.im || !customerId) {
+ reject({ content: '连接未就绪' })
+ return
+ }
+ if (!window.__kefuCsOnline) {
+ reject({ content: '请先上线后再聊天' })
+ return
+ }
+ const csteam = getCsteam()
+ if (!csteam) {
+ reject({ content: '客服团队未初始化' })
+ return
+ }
+ csteam.accept({
+ customer: {
+ id: customerId,
+ data: customerData || { name: customerId, avatar: '' }
+ },
+ onSuccess: () => resolve(),
+ onFailed: (error) => {
+ const msg = (error?.content || '').toLowerCase()
+ if (msg.includes('already') || msg.includes('exist') || msg.includes('已')) {
+ resolve()
+ return
+ }
+ reject(error)
+ }
+ })
+ })
+}
+
+function maybeNotifyNewMessage(emitter, message) {
+ const aid = window.__kefuAgentId
+ if (message?.senderId && message.senderId === aid) return
+
+ const now = Date.now()
+ if (now - lastNotifyAt < NOTIFY_DEBOUNCE_MS) return
+ lastNotifyAt = now
+
+ const customerId = getCsMessageCustomerId(message)
+ const preview = message?.payload?.text || message?.payload?.name || '[新消息]'
+ emitter?.emit('kefu-message-notify', {
+ customerId,
+ preview: String(preview).slice(0, 80),
+ message
+ })
+}
+
+function setupGlobalListeners(emitter) {
+ if (!window.goeasy?.im) return () => {}
+
+ const im = window.goeasy.im
+ const onRefresh = () => refreshKefuUnread(emitter)
+ const onCsMessage = (message) => {
+ emitter?.emit('kefu-cs-new-message', message)
+ maybeNotifyNewMessage(emitter, message)
+ refreshKefuUnread(emitter)
+ }
+
+ im.on(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, onRefresh)
+ im.on(GoEasy.IM_EVENT.PENDING_CONVERSATIONS_UPDATED, onRefresh)
+ im.on(GoEasy.IM_EVENT.CS_MESSAGE_RECEIVED, onCsMessage)
+
+ refreshKefuUnread(emitter)
+
+ return () => {
+ im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, onRefresh)
+ im.off(GoEasy.IM_EVENT.PENDING_CONVERSATIONS_UPDATED, onRefresh)
+ im.off(GoEasy.IM_EVENT.CS_MESSAGE_RECEIVED, onCsMessage)
+ }
+}
+
+export function bindGlobalUnreadListeners(emitter) {
+ return setupGlobalListeners(emitter)
+}
+
+export function afterGoeasyConnected(emitter, phone) {
+ if (window.__kefuUnreadCleanup) {
+ window.__kefuUnreadCleanup()
+ }
+ window.__kefuUnreadCleanup = bindGlobalUnreadListeners(emitter)
+
+ if (window.__kefuUserWantsOffline) {
+ emitCsOnline(emitter, false)
+ refreshKefuUnread(emitter)
+ return
+ }
+
+ ensureCsOnline({
+ phone,
+ emitter,
+ force: false,
+ onSuccess: () => refreshKefuUnread(emitter),
+ onFailed: () => refreshKefuUnread(emitter)
+ })
+}
+
+export function resumeKefuSession(emitter, phone) {
+ if (!isGoeasyConnected()) {
+ emitter?.emit('kefu-goeasy-ready', false)
+ return
+ }
+ emitter?.emit('kefu-goeasy-ready', true)
+ afterGoeasyConnected(emitter, phone)
+}
+
+export function onGoeasyReconnected(emitter, phone) {
+ emitter?.emit('kefu-goeasy-ready', true)
+ if (window.__kefuUserWantsOffline) {
+ emitCsOnline(emitter, false)
+ refreshKefuUnread(emitter)
+ return
+ }
+ ensureCsOnline({
+ phone,
+ emitter,
+ force: false,
+ onSuccess: () => refreshKefuUnread(emitter),
+ onFailed: () => refreshKefuUnread(emitter)
+ })
+}
diff --git a/src/utils/request.js b/src/utils/request.js
index 0bf71ba..0c5c2ad 100644
--- a/src/utils/request.js
+++ b/src/utils/request.js
@@ -2,6 +2,7 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
+import { applyClubHeaders } from '@/utils/club-context'
// 创建 axios 实例,不设置 baseURL(改为动态拼接)
const service = axios.create({
@@ -22,7 +23,7 @@ service.interceptors.request.use(
config.url = (window.$baseURL || '') + config.url
}
- return config
+ return applyClubHeaders(config)
},
(error) => Promise.reject(error)
)
diff --git a/src/views/Layout.vue b/src/views/Layout.vue
index da295b1..469b44e 100644
--- a/src/views/Layout.vue
+++ b/src/views/Layout.vue
@@ -19,27 +19,27 @@
-
+
板块与财务
- 板块配置
- 财务数据
+ 板块配置
+ 财务数据
-
+
考核管理
- 考核官管理
- 考核配置
- 考核记录
+ 考核官管理
+ 考核配置
+ 考核记录
@@ -47,109 +47,119 @@
-
+
订单管理
- 平台订单
- 商家订单
+ 平台订单
+ 商家订单
-
+
跨平台管理
- 跨平台订单
- 跨平台设置
- 跨平台数据
- 预结算管理
+ 跨平台订单
+ 跨平台设置
+ 跨平台数据
+ 预结算管理
-
+
用户管理
- 打手管理
- 管事管理
- 商家管理
- 组长管理
+ 打手管理
+ 管事管理
+ 商家管理
+ 组长管理
+ 身份装饰标签
-
+
提现管理
- 提现审核
- 提现数据
- 提现设置
+ 提现审核
+ 提现数据
+ 提现设置
-
+
管理员管理
- 角色管理
- 后台用户
+ 角色管理
+ 数据范围配置
+ 俱乐部密钥配置
+ 后台用户
+ 操作日志
-
+
处罚管理
-
+
商品管理
- 商品列表
- 商品类型专区管理
- 商品数据分析
+ 商品列表
+ 商品类型专区管理
+ 商品数据分析
-
+
会员管理
- 会员管理
- 会员数据分析
+ 会员管理
+ 会员数据分析
-
+
小程序配置
- 弹窗公告管理
+ 轮播图与公告
+ 图标与频道
+ 弹窗公告管理
+ 抢单商品类型
+ 抢单考核标签
+ 打手接单考试
+ 提现与收款设置
-
+
店铺管理
- 店铺列表
- 店铺提现申请
- 店铺商品管理
+ 店铺列表
+ 店铺提现申请
+ 店铺商品管理
-
+
会话管理