feat(android-sdk): 添加完整的IM客户端SDK实现
- 实现了Android SDK的完整IM功能接口,包括消息、群组、好友、会话等核心功能 - 添加了消息收发、历史记录、撤回编辑等完整的消息操作能力 - 实现了群组管理功能,包括创建、成员管理、权限设置等操作 - 添加了好友关系链管理,支持添加、删除、分组等操作 - 实现了会话管理功能,包括置顶、免打扰、已读状态等 - 添加了黑名单、资料管理、搜索等辅助功能 - 补齐了批量操作接口,提升客户端操作效率 - 实现了WebSocket连接管理和事件监听机制 - 添加了离线消息同步和状态管理功能
这个提交包含在:
父节点
9406f21145
当前提交
6cd938cfbc
275
docs-site/docs/.vitepress/cache/deps/@theme_index.js
vendored
普通文件
275
docs-site/docs/.vitepress/cache/deps/@theme_index.js
vendored
普通文件
@ -0,0 +1,275 @@
|
||||
import {
|
||||
useMediaQuery
|
||||
} from "./chunk-EBBFFI5H.js";
|
||||
import {
|
||||
computed,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch
|
||||
} from "./chunk-XZXUNI6J.js";
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/index.js
|
||||
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/fonts.css";
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/without-fonts.js
|
||||
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/vars.css";
|
||||
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/base.css";
|
||||
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/icons.css";
|
||||
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/utils.css";
|
||||
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/components/custom-block.css";
|
||||
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code.css";
|
||||
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code-group.css";
|
||||
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/components/vp-doc.css";
|
||||
import "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/styles/components/vp-sponsor.css";
|
||||
import VPBadge from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
|
||||
import Layout from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/Layout.vue";
|
||||
import { default as default2 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
|
||||
import { default as default3 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPButton.vue";
|
||||
import { default as default4 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPDocAsideSponsors.vue";
|
||||
import { default as default5 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPFeatures.vue";
|
||||
import { default as default6 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPHomeContent.vue";
|
||||
import { default as default7 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPHomeFeatures.vue";
|
||||
import { default as default8 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPHomeHero.vue";
|
||||
import { default as default9 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPHomeSponsors.vue";
|
||||
import { default as default10 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPImage.vue";
|
||||
import { default as default11 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPLink.vue";
|
||||
import { default as default12 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPNavBarSearch.vue";
|
||||
import { default as default13 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPSocialLink.vue";
|
||||
import { default as default14 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPSocialLinks.vue";
|
||||
import { default as default15 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPSponsors.vue";
|
||||
import { default as default16 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPTeamMembers.vue";
|
||||
import { default as default17 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPTeamPage.vue";
|
||||
import { default as default18 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageSection.vue";
|
||||
import { default as default19 } from "/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-Web/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageTitle.vue";
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
|
||||
import { onContentUpdated } from "vitepress";
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/composables/outline.js
|
||||
import { getScrollOffset } from "vitepress";
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/support/utils.js
|
||||
import { withBase } from "vitepress";
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/composables/data.js
|
||||
import { useData as useData$ } from "vitepress";
|
||||
var useData = useData$;
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/support/utils.js
|
||||
function ensureStartingSlash(path) {
|
||||
return path.startsWith("/") ? path : `/${path}`;
|
||||
}
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/support/sidebar.js
|
||||
function getSidebar(_sidebar, path) {
|
||||
if (Array.isArray(_sidebar))
|
||||
return addBase(_sidebar);
|
||||
if (_sidebar == null)
|
||||
return [];
|
||||
path = ensureStartingSlash(path);
|
||||
const dir = Object.keys(_sidebar).sort((a, b) => {
|
||||
return b.split("/").length - a.split("/").length;
|
||||
}).find((dir2) => {
|
||||
return path.startsWith(ensureStartingSlash(dir2));
|
||||
});
|
||||
const sidebar = dir ? _sidebar[dir] : [];
|
||||
return Array.isArray(sidebar) ? addBase(sidebar) : addBase(sidebar.items, sidebar.base);
|
||||
}
|
||||
function getSidebarGroups(sidebar) {
|
||||
const groups = [];
|
||||
let lastGroupIndex = 0;
|
||||
for (const index in sidebar) {
|
||||
const item = sidebar[index];
|
||||
if (item.items) {
|
||||
lastGroupIndex = groups.push(item);
|
||||
continue;
|
||||
}
|
||||
if (!groups[lastGroupIndex]) {
|
||||
groups.push({ items: [] });
|
||||
}
|
||||
groups[lastGroupIndex].items.push(item);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
function addBase(items, _base) {
|
||||
return [...items].map((_item) => {
|
||||
const item = { ..._item };
|
||||
const base = item.base || _base;
|
||||
if (base && item.link)
|
||||
item.link = base + item.link;
|
||||
if (item.items)
|
||||
item.items = addBase(item.items, base);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/composables/sidebar.js
|
||||
function useSidebar() {
|
||||
const { frontmatter, page, theme: theme2 } = useData();
|
||||
const is960 = useMediaQuery("(min-width: 960px)");
|
||||
const isOpen = ref(false);
|
||||
const _sidebar = computed(() => {
|
||||
const sidebarConfig = theme2.value.sidebar;
|
||||
const relativePath = page.value.relativePath;
|
||||
return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : [];
|
||||
});
|
||||
const sidebar = ref(_sidebar.value);
|
||||
watch(_sidebar, (next, prev) => {
|
||||
if (JSON.stringify(next) !== JSON.stringify(prev))
|
||||
sidebar.value = _sidebar.value;
|
||||
});
|
||||
const hasSidebar = computed(() => {
|
||||
return frontmatter.value.sidebar !== false && sidebar.value.length > 0 && frontmatter.value.layout !== "home";
|
||||
});
|
||||
const leftAside = computed(() => {
|
||||
if (hasAside)
|
||||
return frontmatter.value.aside == null ? theme2.value.aside === "left" : frontmatter.value.aside === "left";
|
||||
return false;
|
||||
});
|
||||
const hasAside = computed(() => {
|
||||
if (frontmatter.value.layout === "home")
|
||||
return false;
|
||||
if (frontmatter.value.aside != null)
|
||||
return !!frontmatter.value.aside;
|
||||
return theme2.value.aside !== false;
|
||||
});
|
||||
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value);
|
||||
const sidebarGroups = computed(() => {
|
||||
return hasSidebar.value ? getSidebarGroups(sidebar.value) : [];
|
||||
});
|
||||
function open() {
|
||||
isOpen.value = true;
|
||||
}
|
||||
function close() {
|
||||
isOpen.value = false;
|
||||
}
|
||||
function toggle() {
|
||||
isOpen.value ? close() : open();
|
||||
}
|
||||
return {
|
||||
isOpen,
|
||||
sidebar,
|
||||
sidebarGroups,
|
||||
hasSidebar,
|
||||
hasAside,
|
||||
leftAside,
|
||||
isSidebarEnabled,
|
||||
open,
|
||||
close,
|
||||
toggle
|
||||
};
|
||||
}
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/composables/outline.js
|
||||
var ignoreRE = /\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\b/;
|
||||
var resolvedHeaders = [];
|
||||
function getHeaders(range) {
|
||||
const headers = [
|
||||
...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")
|
||||
].filter((el) => el.id && el.hasChildNodes()).map((el) => {
|
||||
const level = Number(el.tagName[1]);
|
||||
return {
|
||||
element: el,
|
||||
title: serializeHeader(el),
|
||||
link: "#" + el.id,
|
||||
level
|
||||
};
|
||||
});
|
||||
return resolveHeaders(headers, range);
|
||||
}
|
||||
function serializeHeader(h) {
|
||||
let ret = "";
|
||||
for (const node of h.childNodes) {
|
||||
if (node.nodeType === 1) {
|
||||
if (ignoreRE.test(node.className))
|
||||
continue;
|
||||
ret += node.textContent;
|
||||
} else if (node.nodeType === 3) {
|
||||
ret += node.textContent;
|
||||
}
|
||||
}
|
||||
return ret.trim();
|
||||
}
|
||||
function resolveHeaders(headers, range) {
|
||||
if (range === false) {
|
||||
return [];
|
||||
}
|
||||
const levelsRange = (typeof range === "object" && !Array.isArray(range) ? range.level : range) || 2;
|
||||
const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange;
|
||||
return buildTree(headers, high, low);
|
||||
}
|
||||
function buildTree(data, min, max) {
|
||||
resolvedHeaders.length = 0;
|
||||
const result = [];
|
||||
const stack = [];
|
||||
data.forEach((item) => {
|
||||
const node = { ...item, children: [] };
|
||||
let parent = stack[stack.length - 1];
|
||||
while (parent && parent.level >= node.level) {
|
||||
stack.pop();
|
||||
parent = stack[stack.length - 1];
|
||||
}
|
||||
if (node.element.classList.contains("ignore-header") || parent && "shouldIgnore" in parent) {
|
||||
stack.push({ level: node.level, shouldIgnore: true });
|
||||
return;
|
||||
}
|
||||
if (node.level > max || node.level < min)
|
||||
return;
|
||||
resolvedHeaders.push({ element: node.element, link: node.link });
|
||||
if (parent)
|
||||
parent.children.push(node);
|
||||
else
|
||||
result.push(node);
|
||||
stack.push(node);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
|
||||
function useLocalNav() {
|
||||
const { theme: theme2, frontmatter } = useData();
|
||||
const headers = shallowRef([]);
|
||||
const hasLocalNav = computed(() => {
|
||||
return headers.value.length > 0;
|
||||
});
|
||||
onContentUpdated(() => {
|
||||
headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline);
|
||||
});
|
||||
return {
|
||||
headers,
|
||||
hasLocalNav
|
||||
};
|
||||
}
|
||||
|
||||
// ../node_modules/vitepress/dist/client/theme-default/without-fonts.js
|
||||
var theme = {
|
||||
Layout,
|
||||
enhanceApp: ({ app }) => {
|
||||
app.component("Badge", VPBadge);
|
||||
}
|
||||
};
|
||||
var without_fonts_default = theme;
|
||||
export {
|
||||
default2 as VPBadge,
|
||||
default3 as VPButton,
|
||||
default4 as VPDocAsideSponsors,
|
||||
default5 as VPFeatures,
|
||||
default6 as VPHomeContent,
|
||||
default7 as VPHomeFeatures,
|
||||
default8 as VPHomeHero,
|
||||
default9 as VPHomeSponsors,
|
||||
default10 as VPImage,
|
||||
default11 as VPLink,
|
||||
default12 as VPNavBarSearch,
|
||||
default13 as VPSocialLink,
|
||||
default14 as VPSocialLinks,
|
||||
default15 as VPSponsors,
|
||||
default16 as VPTeamMembers,
|
||||
default17 as VPTeamPage,
|
||||
default18 as VPTeamPageSection,
|
||||
default19 as VPTeamPageTitle,
|
||||
without_fonts_default as default,
|
||||
useLocalNav,
|
||||
useSidebar
|
||||
};
|
||||
//# sourceMappingURL=@theme_index.js.map
|
||||
7
docs-site/docs/.vitepress/cache/deps/@theme_index.js.map
vendored
普通文件
7
docs-site/docs/.vitepress/cache/deps/@theme_index.js.map
vendored
普通文件
文件差异因一行或多行过长而隐藏
58
docs-site/docs/.vitepress/cache/deps/_metadata.json
vendored
普通文件
58
docs-site/docs/.vitepress/cache/deps/_metadata.json
vendored
普通文件
@ -0,0 +1,58 @@
|
||||
{
|
||||
"hash": "2a36dde7",
|
||||
"configHash": "739fef51",
|
||||
"lockfileHash": "2d1a3afd",
|
||||
"browserHash": "7d1e9e84",
|
||||
"optimized": {
|
||||
"vue": {
|
||||
"src": "../../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "0e8b32ca",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vue/devtools-api": {
|
||||
"src": "../../../../../node_modules/@vue/devtools-api/dist/index.js",
|
||||
"file": "vitepress___@vue_devtools-api.js",
|
||||
"fileHash": "732e7221",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vueuse/core": {
|
||||
"src": "../../../../../node_modules/@vueuse/core/index.mjs",
|
||||
"file": "vitepress___@vueuse_core.js",
|
||||
"fileHash": "fd9749e3",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vueuse/integrations/useFocusTrap": {
|
||||
"src": "../../../../../node_modules/@vueuse/integrations/useFocusTrap.mjs",
|
||||
"file": "vitepress___@vueuse_integrations_useFocusTrap.js",
|
||||
"fileHash": "5e56c8b9",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > mark.js/src/vanilla.js": {
|
||||
"src": "../../../../../node_modules/mark.js/src/vanilla.js",
|
||||
"file": "vitepress___mark__js_src_vanilla__js.js",
|
||||
"fileHash": "84c915aa",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > minisearch": {
|
||||
"src": "../../../../../node_modules/minisearch/dist/es/index.js",
|
||||
"file": "vitepress___minisearch.js",
|
||||
"fileHash": "e5b43ef7",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@theme/index": {
|
||||
"src": "../../../../../node_modules/vitepress/dist/client/theme-default/index.js",
|
||||
"file": "@theme_index.js",
|
||||
"fileHash": "b709525e",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-EBBFFI5H": {
|
||||
"file": "chunk-EBBFFI5H.js"
|
||||
},
|
||||
"chunk-XZXUNI6J": {
|
||||
"file": "chunk-XZXUNI6J.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
9719
docs-site/docs/.vitepress/cache/deps/chunk-EBBFFI5H.js
vendored
普通文件
9719
docs-site/docs/.vitepress/cache/deps/chunk-EBBFFI5H.js
vendored
普通文件
文件差异内容过多而无法显示
加载差异
7
docs-site/docs/.vitepress/cache/deps/chunk-EBBFFI5H.js.map
vendored
普通文件
7
docs-site/docs/.vitepress/cache/deps/chunk-EBBFFI5H.js.map
vendored
普通文件
文件差异因一行或多行过长而隐藏
12987
docs-site/docs/.vitepress/cache/deps/chunk-XZXUNI6J.js
vendored
普通文件
12987
docs-site/docs/.vitepress/cache/deps/chunk-XZXUNI6J.js
vendored
普通文件
文件差异内容过多而无法显示
加载差异
7
docs-site/docs/.vitepress/cache/deps/chunk-XZXUNI6J.js.map
vendored
普通文件
7
docs-site/docs/.vitepress/cache/deps/chunk-XZXUNI6J.js.map
vendored
普通文件
文件差异因一行或多行过长而隐藏
3
docs-site/docs/.vitepress/cache/deps/package.json
vendored
普通文件
3
docs-site/docs/.vitepress/cache/deps/package.json
vendored
普通文件
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
4505
docs-site/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js
vendored
普通文件
4505
docs-site/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js
vendored
普通文件
文件差异内容过多而无法显示
加载差异
文件差异因一行或多行过长而隐藏
583
docs-site/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
普通文件
583
docs-site/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
普通文件
@ -0,0 +1,583 @@
|
||||
import {
|
||||
DefaultMagicKeysAliasMap,
|
||||
StorageSerializers,
|
||||
TransitionPresets,
|
||||
assert,
|
||||
breakpointsAntDesign,
|
||||
breakpointsBootstrapV5,
|
||||
breakpointsElement,
|
||||
breakpointsMasterCss,
|
||||
breakpointsPrimeFlex,
|
||||
breakpointsQuasar,
|
||||
breakpointsSematic,
|
||||
breakpointsTailwind,
|
||||
breakpointsVuetify,
|
||||
breakpointsVuetifyV2,
|
||||
breakpointsVuetifyV3,
|
||||
bypassFilter,
|
||||
camelize,
|
||||
clamp,
|
||||
cloneFnJSON,
|
||||
computedAsync,
|
||||
computedEager,
|
||||
computedInject,
|
||||
computedWithControl,
|
||||
containsProp,
|
||||
controlledRef,
|
||||
createEventHook,
|
||||
createFetch,
|
||||
createFilterWrapper,
|
||||
createGlobalState,
|
||||
createInjectionState,
|
||||
createRef,
|
||||
createReusableTemplate,
|
||||
createSharedComposable,
|
||||
createSingletonPromise,
|
||||
createTemplatePromise,
|
||||
createUnrefFn,
|
||||
customStorageEventName,
|
||||
debounceFilter,
|
||||
defaultDocument,
|
||||
defaultLocation,
|
||||
defaultNavigator,
|
||||
defaultWindow,
|
||||
executeTransition,
|
||||
extendRef,
|
||||
formatDate,
|
||||
formatTimeAgo,
|
||||
get,
|
||||
getLifeCycleTarget,
|
||||
getSSRHandler,
|
||||
hasOwn,
|
||||
hyphenate,
|
||||
identity,
|
||||
increaseWithUnit,
|
||||
injectLocal,
|
||||
invoke,
|
||||
isClient,
|
||||
isDef,
|
||||
isDefined,
|
||||
isIOS,
|
||||
isObject,
|
||||
isWorker,
|
||||
makeDestructurable,
|
||||
mapGamepadToXbox360Controller,
|
||||
noop,
|
||||
normalizeDate,
|
||||
notNullish,
|
||||
now,
|
||||
objectEntries,
|
||||
objectOmit,
|
||||
objectPick,
|
||||
onClickOutside,
|
||||
onElementRemoval,
|
||||
onKeyDown,
|
||||
onKeyPressed,
|
||||
onKeyStroke,
|
||||
onKeyUp,
|
||||
onLongPress,
|
||||
onStartTyping,
|
||||
pausableFilter,
|
||||
promiseTimeout,
|
||||
provideLocal,
|
||||
provideSSRWidth,
|
||||
pxValue,
|
||||
rand,
|
||||
reactify,
|
||||
reactifyObject,
|
||||
reactiveComputed,
|
||||
reactiveOmit,
|
||||
reactivePick,
|
||||
refAutoReset,
|
||||
refDebounced,
|
||||
refDefault,
|
||||
refThrottled,
|
||||
refWithControl,
|
||||
resolveRef,
|
||||
resolveUnref,
|
||||
set,
|
||||
setSSRHandler,
|
||||
syncRef,
|
||||
syncRefs,
|
||||
templateRef,
|
||||
throttleFilter,
|
||||
timestamp,
|
||||
toArray,
|
||||
toReactive,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
tryOnBeforeMount,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
tryOnScopeDispose,
|
||||
tryOnUnmounted,
|
||||
unrefElement,
|
||||
until,
|
||||
useActiveElement,
|
||||
useAnimate,
|
||||
useArrayDifference,
|
||||
useArrayEvery,
|
||||
useArrayFilter,
|
||||
useArrayFind,
|
||||
useArrayFindIndex,
|
||||
useArrayFindLast,
|
||||
useArrayIncludes,
|
||||
useArrayJoin,
|
||||
useArrayMap,
|
||||
useArrayReduce,
|
||||
useArraySome,
|
||||
useArrayUnique,
|
||||
useAsyncQueue,
|
||||
useAsyncState,
|
||||
useBase64,
|
||||
useBattery,
|
||||
useBluetooth,
|
||||
useBreakpoints,
|
||||
useBroadcastChannel,
|
||||
useBrowserLocation,
|
||||
useCached,
|
||||
useClipboard,
|
||||
useClipboardItems,
|
||||
useCloned,
|
||||
useColorMode,
|
||||
useConfirmDialog,
|
||||
useCountdown,
|
||||
useCounter,
|
||||
useCssVar,
|
||||
useCurrentElement,
|
||||
useCycleList,
|
||||
useDark,
|
||||
useDateFormat,
|
||||
useDebounceFn,
|
||||
useDebouncedRefHistory,
|
||||
useDeviceMotion,
|
||||
useDeviceOrientation,
|
||||
useDevicePixelRatio,
|
||||
useDevicesList,
|
||||
useDisplayMedia,
|
||||
useDocumentVisibility,
|
||||
useDraggable,
|
||||
useDropZone,
|
||||
useElementBounding,
|
||||
useElementByPoint,
|
||||
useElementHover,
|
||||
useElementSize,
|
||||
useElementVisibility,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useEventSource,
|
||||
useEyeDropper,
|
||||
useFavicon,
|
||||
useFetch,
|
||||
useFileDialog,
|
||||
useFileSystemAccess,
|
||||
useFocus,
|
||||
useFocusWithin,
|
||||
useFps,
|
||||
useFullscreen,
|
||||
useGamepad,
|
||||
useGeolocation,
|
||||
useIdle,
|
||||
useImage,
|
||||
useInfiniteScroll,
|
||||
useIntersectionObserver,
|
||||
useInterval,
|
||||
useIntervalFn,
|
||||
useKeyModifier,
|
||||
useLastChanged,
|
||||
useLocalStorage,
|
||||
useMagicKeys,
|
||||
useManualRefHistory,
|
||||
useMediaControls,
|
||||
useMediaQuery,
|
||||
useMemoize,
|
||||
useMemory,
|
||||
useMounted,
|
||||
useMouse,
|
||||
useMouseInElement,
|
||||
useMousePressed,
|
||||
useMutationObserver,
|
||||
useNavigatorLanguage,
|
||||
useNetwork,
|
||||
useNow,
|
||||
useObjectUrl,
|
||||
useOffsetPagination,
|
||||
useOnline,
|
||||
usePageLeave,
|
||||
useParallax,
|
||||
useParentElement,
|
||||
usePerformanceObserver,
|
||||
usePermission,
|
||||
usePointer,
|
||||
usePointerLock,
|
||||
usePointerSwipe,
|
||||
usePreferredColorScheme,
|
||||
usePreferredContrast,
|
||||
usePreferredDark,
|
||||
usePreferredLanguages,
|
||||
usePreferredReducedMotion,
|
||||
usePreferredReducedTransparency,
|
||||
usePrevious,
|
||||
useRafFn,
|
||||
useRefHistory,
|
||||
useResizeObserver,
|
||||
useSSRWidth,
|
||||
useScreenOrientation,
|
||||
useScreenSafeArea,
|
||||
useScriptTag,
|
||||
useScroll,
|
||||
useScrollLock,
|
||||
useSessionStorage,
|
||||
useShare,
|
||||
useSorted,
|
||||
useSpeechRecognition,
|
||||
useSpeechSynthesis,
|
||||
useStepper,
|
||||
useStorage,
|
||||
useStorageAsync,
|
||||
useStyleTag,
|
||||
useSupported,
|
||||
useSwipe,
|
||||
useTemplateRefsList,
|
||||
useTextDirection,
|
||||
useTextSelection,
|
||||
useTextareaAutosize,
|
||||
useThrottleFn,
|
||||
useThrottledRefHistory,
|
||||
useTimeAgo,
|
||||
useTimeout,
|
||||
useTimeoutFn,
|
||||
useTimeoutPoll,
|
||||
useTimestamp,
|
||||
useTitle,
|
||||
useToNumber,
|
||||
useToString,
|
||||
useToggle,
|
||||
useTransition,
|
||||
useUrlSearchParams,
|
||||
useUserMedia,
|
||||
useVModel,
|
||||
useVModels,
|
||||
useVibrate,
|
||||
useVirtualList,
|
||||
useWakeLock,
|
||||
useWebNotification,
|
||||
useWebSocket,
|
||||
useWebWorker,
|
||||
useWebWorkerFn,
|
||||
useWindowFocus,
|
||||
useWindowScroll,
|
||||
useWindowSize,
|
||||
watchArray,
|
||||
watchAtMost,
|
||||
watchDebounced,
|
||||
watchDeep,
|
||||
watchIgnorable,
|
||||
watchImmediate,
|
||||
watchOnce,
|
||||
watchPausable,
|
||||
watchThrottled,
|
||||
watchTriggerable,
|
||||
watchWithFilter,
|
||||
whenever
|
||||
} from "./chunk-EBBFFI5H.js";
|
||||
import "./chunk-XZXUNI6J.js";
|
||||
export {
|
||||
DefaultMagicKeysAliasMap,
|
||||
StorageSerializers,
|
||||
TransitionPresets,
|
||||
assert,
|
||||
computedAsync as asyncComputed,
|
||||
refAutoReset as autoResetRef,
|
||||
breakpointsAntDesign,
|
||||
breakpointsBootstrapV5,
|
||||
breakpointsElement,
|
||||
breakpointsMasterCss,
|
||||
breakpointsPrimeFlex,
|
||||
breakpointsQuasar,
|
||||
breakpointsSematic,
|
||||
breakpointsTailwind,
|
||||
breakpointsVuetify,
|
||||
breakpointsVuetifyV2,
|
||||
breakpointsVuetifyV3,
|
||||
bypassFilter,
|
||||
camelize,
|
||||
clamp,
|
||||
cloneFnJSON,
|
||||
computedAsync,
|
||||
computedEager,
|
||||
computedInject,
|
||||
computedWithControl,
|
||||
containsProp,
|
||||
computedWithControl as controlledComputed,
|
||||
controlledRef,
|
||||
createEventHook,
|
||||
createFetch,
|
||||
createFilterWrapper,
|
||||
createGlobalState,
|
||||
createInjectionState,
|
||||
reactify as createReactiveFn,
|
||||
createRef,
|
||||
createReusableTemplate,
|
||||
createSharedComposable,
|
||||
createSingletonPromise,
|
||||
createTemplatePromise,
|
||||
createUnrefFn,
|
||||
customStorageEventName,
|
||||
debounceFilter,
|
||||
refDebounced as debouncedRef,
|
||||
watchDebounced as debouncedWatch,
|
||||
defaultDocument,
|
||||
defaultLocation,
|
||||
defaultNavigator,
|
||||
defaultWindow,
|
||||
computedEager as eagerComputed,
|
||||
executeTransition,
|
||||
extendRef,
|
||||
formatDate,
|
||||
formatTimeAgo,
|
||||
get,
|
||||
getLifeCycleTarget,
|
||||
getSSRHandler,
|
||||
hasOwn,
|
||||
hyphenate,
|
||||
identity,
|
||||
watchIgnorable as ignorableWatch,
|
||||
increaseWithUnit,
|
||||
injectLocal,
|
||||
invoke,
|
||||
isClient,
|
||||
isDef,
|
||||
isDefined,
|
||||
isIOS,
|
||||
isObject,
|
||||
isWorker,
|
||||
makeDestructurable,
|
||||
mapGamepadToXbox360Controller,
|
||||
noop,
|
||||
normalizeDate,
|
||||
notNullish,
|
||||
now,
|
||||
objectEntries,
|
||||
objectOmit,
|
||||
objectPick,
|
||||
onClickOutside,
|
||||
onElementRemoval,
|
||||
onKeyDown,
|
||||
onKeyPressed,
|
||||
onKeyStroke,
|
||||
onKeyUp,
|
||||
onLongPress,
|
||||
onStartTyping,
|
||||
pausableFilter,
|
||||
watchPausable as pausableWatch,
|
||||
promiseTimeout,
|
||||
provideLocal,
|
||||
provideSSRWidth,
|
||||
pxValue,
|
||||
rand,
|
||||
reactify,
|
||||
reactifyObject,
|
||||
reactiveComputed,
|
||||
reactiveOmit,
|
||||
reactivePick,
|
||||
refAutoReset,
|
||||
refDebounced,
|
||||
refDefault,
|
||||
refThrottled,
|
||||
refWithControl,
|
||||
resolveRef,
|
||||
resolveUnref,
|
||||
set,
|
||||
setSSRHandler,
|
||||
syncRef,
|
||||
syncRefs,
|
||||
templateRef,
|
||||
throttleFilter,
|
||||
refThrottled as throttledRef,
|
||||
watchThrottled as throttledWatch,
|
||||
timestamp,
|
||||
toArray,
|
||||
toReactive,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
tryOnBeforeMount,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
tryOnScopeDispose,
|
||||
tryOnUnmounted,
|
||||
unrefElement,
|
||||
until,
|
||||
useActiveElement,
|
||||
useAnimate,
|
||||
useArrayDifference,
|
||||
useArrayEvery,
|
||||
useArrayFilter,
|
||||
useArrayFind,
|
||||
useArrayFindIndex,
|
||||
useArrayFindLast,
|
||||
useArrayIncludes,
|
||||
useArrayJoin,
|
||||
useArrayMap,
|
||||
useArrayReduce,
|
||||
useArraySome,
|
||||
useArrayUnique,
|
||||
useAsyncQueue,
|
||||
useAsyncState,
|
||||
useBase64,
|
||||
useBattery,
|
||||
useBluetooth,
|
||||
useBreakpoints,
|
||||
useBroadcastChannel,
|
||||
useBrowserLocation,
|
||||
useCached,
|
||||
useClipboard,
|
||||
useClipboardItems,
|
||||
useCloned,
|
||||
useColorMode,
|
||||
useConfirmDialog,
|
||||
useCountdown,
|
||||
useCounter,
|
||||
useCssVar,
|
||||
useCurrentElement,
|
||||
useCycleList,
|
||||
useDark,
|
||||
useDateFormat,
|
||||
refDebounced as useDebounce,
|
||||
useDebounceFn,
|
||||
useDebouncedRefHistory,
|
||||
useDeviceMotion,
|
||||
useDeviceOrientation,
|
||||
useDevicePixelRatio,
|
||||
useDevicesList,
|
||||
useDisplayMedia,
|
||||
useDocumentVisibility,
|
||||
useDraggable,
|
||||
useDropZone,
|
||||
useElementBounding,
|
||||
useElementByPoint,
|
||||
useElementHover,
|
||||
useElementSize,
|
||||
useElementVisibility,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useEventSource,
|
||||
useEyeDropper,
|
||||
useFavicon,
|
||||
useFetch,
|
||||
useFileDialog,
|
||||
useFileSystemAccess,
|
||||
useFocus,
|
||||
useFocusWithin,
|
||||
useFps,
|
||||
useFullscreen,
|
||||
useGamepad,
|
||||
useGeolocation,
|
||||
useIdle,
|
||||
useImage,
|
||||
useInfiniteScroll,
|
||||
useIntersectionObserver,
|
||||
useInterval,
|
||||
useIntervalFn,
|
||||
useKeyModifier,
|
||||
useLastChanged,
|
||||
useLocalStorage,
|
||||
useMagicKeys,
|
||||
useManualRefHistory,
|
||||
useMediaControls,
|
||||
useMediaQuery,
|
||||
useMemoize,
|
||||
useMemory,
|
||||
useMounted,
|
||||
useMouse,
|
||||
useMouseInElement,
|
||||
useMousePressed,
|
||||
useMutationObserver,
|
||||
useNavigatorLanguage,
|
||||
useNetwork,
|
||||
useNow,
|
||||
useObjectUrl,
|
||||
useOffsetPagination,
|
||||
useOnline,
|
||||
usePageLeave,
|
||||
useParallax,
|
||||
useParentElement,
|
||||
usePerformanceObserver,
|
||||
usePermission,
|
||||
usePointer,
|
||||
usePointerLock,
|
||||
usePointerSwipe,
|
||||
usePreferredColorScheme,
|
||||
usePreferredContrast,
|
||||
usePreferredDark,
|
||||
usePreferredLanguages,
|
||||
usePreferredReducedMotion,
|
||||
usePreferredReducedTransparency,
|
||||
usePrevious,
|
||||
useRafFn,
|
||||
useRefHistory,
|
||||
useResizeObserver,
|
||||
useSSRWidth,
|
||||
useScreenOrientation,
|
||||
useScreenSafeArea,
|
||||
useScriptTag,
|
||||
useScroll,
|
||||
useScrollLock,
|
||||
useSessionStorage,
|
||||
useShare,
|
||||
useSorted,
|
||||
useSpeechRecognition,
|
||||
useSpeechSynthesis,
|
||||
useStepper,
|
||||
useStorage,
|
||||
useStorageAsync,
|
||||
useStyleTag,
|
||||
useSupported,
|
||||
useSwipe,
|
||||
useTemplateRefsList,
|
||||
useTextDirection,
|
||||
useTextSelection,
|
||||
useTextareaAutosize,
|
||||
refThrottled as useThrottle,
|
||||
useThrottleFn,
|
||||
useThrottledRefHistory,
|
||||
useTimeAgo,
|
||||
useTimeout,
|
||||
useTimeoutFn,
|
||||
useTimeoutPoll,
|
||||
useTimestamp,
|
||||
useTitle,
|
||||
useToNumber,
|
||||
useToString,
|
||||
useToggle,
|
||||
useTransition,
|
||||
useUrlSearchParams,
|
||||
useUserMedia,
|
||||
useVModel,
|
||||
useVModels,
|
||||
useVibrate,
|
||||
useVirtualList,
|
||||
useWakeLock,
|
||||
useWebNotification,
|
||||
useWebSocket,
|
||||
useWebWorker,
|
||||
useWebWorkerFn,
|
||||
useWindowFocus,
|
||||
useWindowScroll,
|
||||
useWindowSize,
|
||||
watchArray,
|
||||
watchAtMost,
|
||||
watchDebounced,
|
||||
watchDeep,
|
||||
watchIgnorable,
|
||||
watchImmediate,
|
||||
watchOnce,
|
||||
watchPausable,
|
||||
watchThrottled,
|
||||
watchTriggerable,
|
||||
watchWithFilter,
|
||||
whenever
|
||||
};
|
||||
//# sourceMappingURL=vitepress___@vueuse_core.js.map
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
文件差异内容过多而无法显示
加载差异
文件差异因一行或多行过长而隐藏
1665
docs-site/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js
vendored
普通文件
1665
docs-site/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js
vendored
普通文件
文件差异内容过多而无法显示
加载差异
文件差异因一行或多行过长而隐藏
1813
docs-site/docs/.vitepress/cache/deps/vitepress___minisearch.js
vendored
普通文件
1813
docs-site/docs/.vitepress/cache/deps/vitepress___minisearch.js
vendored
普通文件
文件差异内容过多而无法显示
加载差异
文件差异因一行或多行过长而隐藏
347
docs-site/docs/.vitepress/cache/deps/vue.js
vendored
普通文件
347
docs-site/docs/.vitepress/cache/deps/vue.js
vendored
普通文件
@ -0,0 +1,347 @@
|
||||
import {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBaseVNode,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
patchProp,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
} from "./chunk-XZXUNI6J.js";
|
||||
export {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createBaseVNode as createElementVNode,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
patchProp,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
};
|
||||
//# sourceMappingURL=vue.js.map
|
||||
7
docs-site/docs/.vitepress/cache/deps/vue.js.map
vendored
普通文件
7
docs-site/docs/.vitepress/cache/deps/vue.js.map
vendored
普通文件
@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@ -21,7 +21,9 @@ export default defineConfig({
|
||||
{ text: 'iOS', link: '/ios/' },
|
||||
{ text: 'React Native', link: '/rn/' },
|
||||
{ text: 'Vue3', link: '/vue3/' },
|
||||
{ text: 'Flutter', link: '/flutter/' },
|
||||
{ text: 'HarmonyOS', link: '/harmony/' },
|
||||
{ text: '微信小程序', link: '/miniprogram/' },
|
||||
],
|
||||
},
|
||||
{ text: 'API 速查', link: '/server/api' },
|
||||
@ -43,7 +45,6 @@ export default defineConfig({
|
||||
{ text: 'IM 接入', link: '/android/im' },
|
||||
{ text: '推送接入', link: '/android/push' },
|
||||
{ text: '版本管理', link: '/android/update' },
|
||||
{ text: 'API Reference', link: '/android/api' },
|
||||
],
|
||||
'/ios/': [
|
||||
{ text: '概览', link: '/ios/' },
|
||||
@ -51,7 +52,6 @@ export default defineConfig({
|
||||
{ text: 'IM 接入', link: '/ios/im' },
|
||||
{ text: '推送接入', link: '/ios/push' },
|
||||
{ text: '版本管理', link: '/ios/update' },
|
||||
{ text: 'API Reference', link: '/ios/api' },
|
||||
],
|
||||
'/rn/': [
|
||||
{ text: '概览', link: '/rn/' },
|
||||
@ -59,18 +59,24 @@ export default defineConfig({
|
||||
{ text: 'IM 接入', link: '/rn/im' },
|
||||
{ text: '群聊', link: '/rn/group' },
|
||||
{ text: '版本管理', link: '/rn/update' },
|
||||
{ text: 'API Reference', link: '/rn/api' },
|
||||
],
|
||||
'/vue3/': [
|
||||
{ text: '概览', link: '/vue3/' },
|
||||
{ text: '安装配置', link: '/vue3/setup' },
|
||||
{ text: 'IM 接入', link: '/vue3/im' },
|
||||
{ text: 'API Reference', link: '/vue3/api' },
|
||||
],
|
||||
'/flutter/': [
|
||||
{ text: '概览', link: '/flutter/' },
|
||||
],
|
||||
'/harmony/': [
|
||||
{ text: '概览', link: '/harmony/' },
|
||||
{ text: '安装配置', link: '/harmony/setup' },
|
||||
{ text: 'IM 接入', link: '/harmony/im' },
|
||||
{ text: '推送接入', link: '/harmony/#push-接入' },
|
||||
{ text: '版本管理', link: '/harmony/#update-接入' },
|
||||
],
|
||||
'/miniprogram/': [
|
||||
{ text: '概览', link: '/miniprogram/' },
|
||||
],
|
||||
'/server/': [
|
||||
{ text: 'API 速查', link: '/server/api' },
|
||||
|
||||
326
docs-site/docs/android/im.md
普通文件
326
docs-site/docs/android/im.md
普通文件
@ -0,0 +1,326 @@
|
||||
# Android IM 接入
|
||||
|
||||
基于 `sdk-im` 模块实现即时通讯功能。
|
||||
|
||||
---
|
||||
|
||||
## 初始化详情
|
||||
|
||||
在 `Application.onCreate()` 中完成初始化,只需传入 `appKey`:
|
||||
|
||||
```kotlin
|
||||
import com.xuqm.sdk.XuqmSDK
|
||||
import com.xuqm.sdk.core.LogLevel
|
||||
|
||||
class MyApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
XuqmSDK.initialize(
|
||||
context = this,
|
||||
appKey = "your_app_key",
|
||||
logLevel = LogLevel.WARN, // 可选:DEBUG / INFO / WARN / ERROR
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 服务器地址由 SDK 内置,**无需传 `serverUrl`**。
|
||||
|
||||
---
|
||||
|
||||
## 登录鉴权(UserSig)
|
||||
|
||||
`userSig` 由业务服务端用 `appSecret` 签发。登录只需 `userId + userSig`:
|
||||
|
||||
```kotlin
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.xuqm.sdk.XuqmSDK
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
lifecycleScope.launch {
|
||||
XuqmSDK.login(
|
||||
userId = "user_001",
|
||||
userSig = "your_user_sig_jwt",
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> 登录成功后,IM 模块会自动连接 WebSocket,Push 模块会自动注册设备 Token。
|
||||
|
||||
---
|
||||
|
||||
## 单聊消息收发
|
||||
|
||||
### 监听实时消息
|
||||
|
||||
```kotlin
|
||||
import com.xuqm.sdk.im.ImSDK
|
||||
import com.xuqm.sdk.im.listener.ImEventListener
|
||||
import com.xuqm.sdk.im.model.ImMessage
|
||||
|
||||
ImSDK.addListener(object : ImEventListener {
|
||||
override fun onConnected() { /* WebSocket 已连接 */ }
|
||||
override fun onMessage(msg: ImMessage) { /* 单聊消息 */ }
|
||||
override fun onGroupMessage(msg: ImMessage) { /* 群聊消息 */ }
|
||||
override fun onRead(msg: ImMessage) { /* 对方已读回执 */ }
|
||||
override fun onRevoke(msg: ImMessage) { /* 消息被撤回 */ }
|
||||
override fun onDisconnected(reason: String?) { /* 断线处理 */ }
|
||||
override fun onError(error: String) { /* 错误回调 */ }
|
||||
})
|
||||
```
|
||||
|
||||
### 发送文本消息
|
||||
|
||||
```kotlin
|
||||
ImSDK.sendTextMessage(
|
||||
toId = "user_002",
|
||||
chatType = "SINGLE",
|
||||
content = "Hello!",
|
||||
)
|
||||
```
|
||||
|
||||
### 发送多媒体消息
|
||||
|
||||
```kotlin
|
||||
// 图片(需先调用 FileSDK 上传)
|
||||
ImSDK.sendImageMessage(
|
||||
toId = "user_002",
|
||||
chatType = "SINGLE",
|
||||
file = uploadResult,
|
||||
width = 800,
|
||||
height = 600,
|
||||
)
|
||||
|
||||
// 视频
|
||||
ImSDK.sendVideoMessage(
|
||||
toId = "user_002",
|
||||
chatType = "SINGLE",
|
||||
file = uploadResult,
|
||||
width = 1920,
|
||||
height = 1080,
|
||||
durationMs = 15_000,
|
||||
)
|
||||
|
||||
// 语音
|
||||
ImSDK.sendAudioMessage(
|
||||
toId = "user_002",
|
||||
chatType = "SINGLE",
|
||||
file = uploadResult,
|
||||
durationMs = 5_000,
|
||||
)
|
||||
|
||||
// 文件
|
||||
ImSDK.sendFileMessage(
|
||||
toId = "user_002",
|
||||
chatType = "SINGLE",
|
||||
file = uploadResult,
|
||||
)
|
||||
|
||||
// 位置
|
||||
ImSDK.sendLocationMessage(
|
||||
toId = "user_002",
|
||||
chatType = "SINGLE",
|
||||
latitude = 31.2304,
|
||||
longitude = 121.4737,
|
||||
title = "上海",
|
||||
address = "上海市黄浦区",
|
||||
)
|
||||
```
|
||||
|
||||
### 获取历史消息
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
val history = ImSDK.fetchHistory("user_002", page = 0, size = 20)
|
||||
// 带筛选的历史消息
|
||||
val filtered = ImSDK.fetchHistoryWithFilters(
|
||||
toId = "user_002",
|
||||
page = 0,
|
||||
size = 20,
|
||||
msgType = "TEXT",
|
||||
keyword = "会议",
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 撤回与编辑
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
ImSDK.revokeMessage(messageId)
|
||||
ImSDK.editMessage(messageId, "新内容")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群聊
|
||||
|
||||
### 创建群组
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
val group = ImSDK.createGroup(
|
||||
name = "项目讨论",
|
||||
memberIds = listOf("user_002", "user_003"),
|
||||
groupType = "WORK", // 可选:WORK / PUBLIC / PRIVATE
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 订阅群消息
|
||||
|
||||
```kotlin
|
||||
ImSDK.subscribeGroup("group_xxx")
|
||||
```
|
||||
|
||||
### 发送群消息
|
||||
|
||||
```kotlin
|
||||
ImSDK.sendTextMessage(
|
||||
toId = "group_xxx",
|
||||
chatType = "GROUP",
|
||||
content = "大家好",
|
||||
)
|
||||
```
|
||||
|
||||
### 群成员管理
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
// 添加成员
|
||||
ImSDK.addGroupMember("group_xxx", "user_004")
|
||||
// 移除成员
|
||||
ImSDK.removeGroupMember("group_xxx", "user_004")
|
||||
// 批量添加/移除
|
||||
ImSDK.batchAddGroupMembers("group_xxx", listOf("user_005", "user_006"))
|
||||
ImSDK.batchRemoveGroupMembers("group_xxx", listOf("user_005", "user_006"))
|
||||
// 设置角色
|
||||
ImSDK.setGroupRole("group_xxx", "user_004", "ADMIN")
|
||||
// 禁言
|
||||
ImSDK.muteGroupMember("group_xxx", "user_004", minutes = 60)
|
||||
// 转让群主
|
||||
ImSDK.transferGroupOwner("group_xxx", "user_002")
|
||||
// 退出群聊
|
||||
ImSDK.leaveGroup("group_xxx")
|
||||
// 解散群聊
|
||||
ImSDK.dismissGroup("group_xxx")
|
||||
}
|
||||
```
|
||||
|
||||
### 群信息查询
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
val groups = ImSDK.listGroups()
|
||||
val publicGroups = ImSDK.listPublicGroups(keyword = "项目")
|
||||
val group = ImSDK.getGroupInfo("group_xxx")
|
||||
val members = ImSDK.listGroupMembers("group_xxx")
|
||||
val history = ImSDK.fetchGroupHistory("group_xxx", page = 0, size = 20)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 好友管理
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
// 好友列表
|
||||
val friends = ImSDK.listFriends()
|
||||
// 添加好友
|
||||
ImSDK.addFriend("user_002")
|
||||
// 删除好友
|
||||
ImSDK.removeFriend("user_002")
|
||||
// 批量操作
|
||||
ImSDK.batchAddFriends(listOf("user_002", "user_003"))
|
||||
ImSDK.batchRemoveFriends(listOf("user_002", "user_003"))
|
||||
// 好友分组
|
||||
ImSDK.setFriendGroup("user_002", "同事")
|
||||
val groups = ImSDK.listFriendGroups()
|
||||
// 好友请求
|
||||
ImSDK.sendFriendRequest("user_002", remark = "你好")
|
||||
val requests = ImSDK.listFriendRequests(direction = "incoming")
|
||||
ImSDK.acceptFriendRequest("request_id")
|
||||
ImSDK.rejectFriendRequest("request_id")
|
||||
// 黑名单
|
||||
ImSDK.addToBlacklist("user_002")
|
||||
ImSDK.removeFromBlacklist("user_002")
|
||||
val check = ImSDK.checkBlacklist("user_002")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 会话列表
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
val conversations = ImSDK.listConversations()
|
||||
// 置顶
|
||||
ImSDK.setConversationPinned("user_002", "SINGLE", true)
|
||||
// 免打扰
|
||||
ImSDK.setConversationMuted("user_002", "SINGLE", true)
|
||||
// 标记已读
|
||||
ImSDK.markRead("user_002", "SINGLE")
|
||||
// 草稿
|
||||
ImSDK.setDraft("user_002", "SINGLE", "草稿内容")
|
||||
// 删除会话
|
||||
ImSDK.deleteConversation("user_002", "SINGLE")
|
||||
// 总未读数
|
||||
val totalUnread = ImSDK.getTotalUnreadCount()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 离线消息同步
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
// 查询离线消息数量
|
||||
val count = ImSDK.offlineMessageCount()
|
||||
// 同步离线消息
|
||||
val offlineMessages = ImSDK.syncOfflineMessages()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 连接状态监听
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
ImSDK.connectionState.collect { state ->
|
||||
when (state) {
|
||||
is ImConnectionState.Connected -> { /* 已连接 */ }
|
||||
is ImConnectionState.Connecting -> { /* 连接中 */ }
|
||||
is ImConnectionState.Disconnected -> { /* 已断开:state.reason */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 消息类型
|
||||
|
||||
| MsgType | 说明 | content 结构 |
|
||||
|---------|------|-------------|
|
||||
| `TEXT` | 纯文本 | `String` |
|
||||
| `IMAGE` | 图片 | `{url, width, height, thumbnailUrl?}` |
|
||||
| `VIDEO` | 视频 | `{url, duration, thumbnailUrl, size}` |
|
||||
| `AUDIO` | 语音 | `{url, duration, size}` |
|
||||
| `FILE` | 文件 | `{url, name, size, mimeType}` |
|
||||
| `LOCATION` | 位置 | `{lat, lng, address, title}` |
|
||||
| `CUSTOM` | 自定义 | 任意 JSON |
|
||||
| `NOTIFY` | 系统通知 | `{title, content}` |
|
||||
| `RICH_TEXT` | 富文本 | `{html}` |
|
||||
| `CALL_AUDIO` | 语音通话信令 | `{action}` |
|
||||
| `CALL_VIDEO` | 视频通话信令 | `{action}` |
|
||||
| `FORWARD` | 转发 | `{originalSender, originalContent}` |
|
||||
| `QUOTE` | 引用 | `{quotedMsgId, quotedContent, text}` |
|
||||
| `MERGE` | 合并转发 | `{title, msgList}` |
|
||||
| `REVOKED` | 撤回 | 系统内部填充 |
|
||||
|
||||
[→ 完整 API Reference](./api)
|
||||
169
docs-site/docs/android/push.md
普通文件
169
docs-site/docs/android/push.md
普通文件
@ -0,0 +1,169 @@
|
||||
# Android 推送接入指南
|
||||
|
||||
**模块**:`com.xuqm:sdk-push` · **支持厂商**:华为、小米、OPPO、vivo、荣耀、FCM
|
||||
|
||||
SDK 在 `XuqmSDK.login()` 成功后会自动检测手机厂商、初始化对应 Push SDK、获取 Token 并上报到 Push 服务端。业务层**无需手动调用**任何 Push 注册 API。
|
||||
|
||||
如需主动控制,可参考以下独立接入方式。
|
||||
|
||||
---
|
||||
|
||||
## 1. 添加依赖
|
||||
|
||||
```kotlin
|
||||
// app/build.gradle.kts
|
||||
dependencies {
|
||||
implementation("com.xuqm:sdk-push:0.4.0")
|
||||
}
|
||||
```
|
||||
|
||||
各厂商需要额外添加对应 Push SDK 依赖(按需):
|
||||
|
||||
| 厂商 | 依赖 |
|
||||
|------|------|
|
||||
| 华为 | `com.huawei.hms:push:6.x.x.xxx` |
|
||||
| 小米 | `com.xiaomi.mipush:mipush:5.x.x` |
|
||||
| OPPO | `com.heytap.mcs:push:3.x.x` |
|
||||
| vivo | `com.vivo.pushsdk:pushsdk:3.x.x` |
|
||||
| 荣耀 | `com.hihonor.mcs:push:7.x.x.xxx` |
|
||||
| FCM | `com.google.firebase:firebase-messaging` |
|
||||
|
||||
---
|
||||
|
||||
## 2. AndroidManifest.xml 配置
|
||||
|
||||
在 `<application>` 下添加各厂商对应的 `meta-data`:
|
||||
|
||||
```xml
|
||||
<!-- 小米 -->
|
||||
<meta-data android:name="XUQM_XIAOMI_APP_ID"
|
||||
android:value="288230376xxxxxxxx" />
|
||||
<meta-data android:name="XUQM_XIAOMI_APP_KEY"
|
||||
android:value="xxxxxxxxxxxx" />
|
||||
|
||||
<!-- OPPO -->
|
||||
<meta-data android:name="XUQM_OPPO_APP_KEY"
|
||||
android:value="xxxxxxxx" />
|
||||
<meta-data android:name="XUQM_OPPO_APP_SECRET"
|
||||
android:value="xxxxxxxx" />
|
||||
```
|
||||
|
||||
> 华为、荣耀、vivo、FCM 无需在 `AndroidManifest.xml` 中配置 `meta-data`,其配置方式如下:
|
||||
> - **华为**:通过 `agconnect-services.json` 自动读取 `app_id`
|
||||
> - **荣耀**:在 `Application.onCreate()` 中调用 `HonorPushClient.getInstance().init(context, true)`
|
||||
> - **vivo**:在 `Application.onCreate()` 中调用 `PushClient.getInstance(context).initialize()`
|
||||
> - **FCM**:通过 `google-services.json` 自动集成
|
||||
|
||||
---
|
||||
|
||||
## 3. 初始化
|
||||
|
||||
在 `Application.onCreate()` 中调用:
|
||||
|
||||
```kotlin
|
||||
PushSDK.initializeVendors(context)
|
||||
```
|
||||
|
||||
SDK 会自动检测当前设备厂商(`HUAWEI / XIAOMI / OPPO / VIVO / HONOR / FCM`),并初始化对应的推送服务。
|
||||
|
||||
如需查询当前检测到的厂商:
|
||||
|
||||
```kotlin
|
||||
val vendor = PushSDK.detectVendor()
|
||||
// 返回:HUAWEI / XIAOMI / OPPO / VIVO / HONOR / FCM
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 手动上报 Token
|
||||
|
||||
如果业务层自定义了厂商推送服务(如自定义 `HmsMessageService`、`MiPushMessageReceiver` 等),需在收到 Token 回调后手动上报:
|
||||
|
||||
```kotlin
|
||||
// 华为 HMS 示例
|
||||
class MyHmsService : HmsMessageService() {
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
PushSDK.updateNativePushToken(context, PushVendor.HUAWEI, token)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// 小米示例
|
||||
class MyMiPushReceiver : PushMessageReceiver() {
|
||||
override fun onReceiveRegisterResult(context: Context, miPushCommandMessage: MiPushCommandMessage) {
|
||||
val token = miPushCommandMessage.commandArguments?.getOrNull(0)
|
||||
if (!token.isNullOrBlank()) {
|
||||
PushSDK.updateNativePushToken(context, PushVendor.XIAOMI, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// OPPO 示例(在 PushCallback.onRegister 中)
|
||||
PushSDK.updateNativePushToken(context, PushVendor.OPPO, regId)
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// vivo 示例(在 OpenClientPushMessageReceiver.onReceiveRegId 中)
|
||||
PushSDK.updateNativePushToken(context, PushVendor.VIVO, regId)
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// 荣耀示例(在 HonorMessageService.onNewToken 中)
|
||||
PushSDK.updateNativePushToken(context, PushVendor.HONOR, token)
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// FCM 示例(在 FirebaseMessagingService.onNewToken 中)
|
||||
class MyFcmService : FirebaseMessagingService() {
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
PushSDK.updateNativePushToken(this, PushVendor.FCM, token)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 接收推送消息
|
||||
|
||||
各厂商推送消息通过各自的 SDK 回调接收,SDK 不负责解析消息内容,业务层需自行实现:
|
||||
|
||||
- **华为**:`HmsMessageService.onMessageReceived(RemoteMessage)`
|
||||
- **小米**:`PushMessageReceiver.onReceivePassThroughMessage()` / `onNotificationMessageClicked()`
|
||||
- **OPPO**:`PushCallback.onGetPushMessage()`
|
||||
- **vivo**:`OpenClientPushMessageReceiver.onTransmissionMessage()`
|
||||
- **荣耀**:`HonorMessageService.onMessageReceived()`
|
||||
- **FCM**:`FirebaseMessagingService.onMessageReceived(RemoteMessage)`
|
||||
|
||||
> 建议在收到推送消息后,调用业务层的通知管理器展示本地通知,或跳转至对应会话页面。
|
||||
|
||||
---
|
||||
|
||||
## 6. 开启或关闭推送
|
||||
|
||||
```kotlin
|
||||
// 关闭推送接收
|
||||
PushSDK.setReceivePush(context, enabled = false)
|
||||
|
||||
// 开启推送接收
|
||||
PushSDK.setReceivePush(context, enabled = true)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 多模块统一登录
|
||||
|
||||
Push 模块与 IM、Update 模块共享同一套登录态:
|
||||
|
||||
```kotlin
|
||||
// 登录成功后自动触发
|
||||
XuqmSDK.login(userId = "user_001", userSig = "jwt_token")
|
||||
// ↓ 自动触发
|
||||
// · PushSDK.onSdkLogin → 初始化厂商 Push 并注册 Token
|
||||
```
|
||||
|
||||
无需业务层手动调用 `PushSDK.initializeVendors()` 或 `bindImUser()`。
|
||||
84
docs-site/docs/android/setup.md
普通文件
84
docs-site/docs/android/setup.md
普通文件
@ -0,0 +1,84 @@
|
||||
# Android 安装配置
|
||||
|
||||
**版本**:0.5.x · **最低 Android 版本**:API 24 (Android 7.0) · **语言**:Kotlin
|
||||
|
||||
---
|
||||
|
||||
## Gradle 仓库配置
|
||||
|
||||
在 `settings.gradle.kts` 中添加 XuqmGroup Maven 仓库:
|
||||
|
||||
```kotlin
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
maven("https://nexus.xuqinmin.com/repository/android/")
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 各模块 Artifact 和版本
|
||||
|
||||
| 模块 | Artifact | 功能 |
|
||||
|------|----------|------|
|
||||
| sdk-core | `com.xuqm:sdk-core` | 初始化、网络、鉴权、Token 存储 |
|
||||
| sdk-im | `com.xuqm:sdk-im` | 单聊、群聊、消息收发、会话、好友、群组 |
|
||||
| sdk-push | `com.xuqm:sdk-push` | 自动检测厂商、设备 Token 注册(华为/小米/OPPO/vivo/荣耀/FCM)|
|
||||
| sdk-update | `com.xuqm:sdk-update` | App 更新检查、下载安装 |
|
||||
|
||||
在 `app/build.gradle.kts` 中按需引入:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("com.xuqm:sdk-core:0.4.0")
|
||||
implementation("com.xuqm:sdk-im:0.4.0")
|
||||
implementation("com.xuqm:sdk-push:0.4.0") // 按需
|
||||
implementation("com.xuqm:sdk-update:0.4.0") // 按需
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最低 Android API 版本
|
||||
|
||||
- `minSdk = 24`(Android 7.0)
|
||||
- `compileSdk` 与项目保持一致(建议 34+)
|
||||
- Java 11 / Kotlin 1.9+
|
||||
|
||||
---
|
||||
|
||||
## ProGuard / R8 混淆规则
|
||||
|
||||
各模块已自带 `consumer-rules.pro`,业务方**无需额外配置**。如需手动维护,可添加:
|
||||
|
||||
```proguard
|
||||
# XuqmSDK Core
|
||||
-keep class com.xuqm.sdk.** { *; }
|
||||
-keepclassmembers class com.xuqm.sdk.** { *; }
|
||||
|
||||
# XuqmSDK IM
|
||||
-keep class com.xuqm.sdk.im.** { *; }
|
||||
-keepclassmembers class com.xuqm.sdk.im.** { *; }
|
||||
|
||||
# XuqmSDK Push
|
||||
-keep class com.xuqm.sdk.push.** { *; }
|
||||
|
||||
# XuqmSDK Update
|
||||
-keep class com.xuqm.sdk.update.** { *; }
|
||||
|
||||
# Gson(IM 消息序列化使用)
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation*
|
||||
-keep class com.google.gson.** { *; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
- [Android IM 接入 →](./im)
|
||||
- [Android Push 接入 →](./push)
|
||||
- [Android 版本更新 →](./update)
|
||||
119
docs-site/docs/android/update.md
普通文件
119
docs-site/docs/android/update.md
普通文件
@ -0,0 +1,119 @@
|
||||
# Android 版本更新接入指南
|
||||
|
||||
**模块**:`com.xuqm:sdk-update` · **功能**:App 版本检查、下载安装
|
||||
|
||||
---
|
||||
|
||||
## 1. 添加依赖
|
||||
|
||||
```kotlin
|
||||
// app/build.gradle.kts
|
||||
dependencies {
|
||||
implementation("com.xuqm:sdk-update:0.4.0")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 检查更新
|
||||
|
||||
```kotlin
|
||||
import com.xuqm.sdk.update.UpdateSDK
|
||||
|
||||
// 在协程中调用
|
||||
lifecycleScope.launch {
|
||||
val update = UpdateSDK.checkAppUpdate(context)
|
||||
if (update?.needsUpdate == true) {
|
||||
println("发现新版本: ${update.versionName}")
|
||||
println("更新日志: ${update.changeLog}")
|
||||
println("下载地址: ${update.downloadUrl}")
|
||||
|
||||
if (update.forceUpdate == true) {
|
||||
// 强制更新:必须升级后才能使用
|
||||
showForceUpdateDialog(update)
|
||||
} else {
|
||||
// 可选更新:提示用户
|
||||
showOptionalUpdateDialog(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 下载并安装
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
UpdateSDK.downloadAndInstall(context, downloadUrl) { progress ->
|
||||
// 更新下载进度 0-100
|
||||
updateProgressBar(progress)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`downloadAndInstall` 内部会:
|
||||
1. 下载 APK 到外部存储目录
|
||||
2. 通过 `FileProvider` 获取文件 URI
|
||||
3. 启动系统安装界面
|
||||
|
||||
**需提前配置 `FileProvider`**:
|
||||
|
||||
```xml
|
||||
<!-- AndroidManifest.xml -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- res/xml/file_paths.xml -->
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-files-path name="updates" path="." />
|
||||
</paths>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 强制更新处理
|
||||
|
||||
当服务端返回 `forceUpdate = true` 时,建议业务层:
|
||||
|
||||
1. 弹出不可取消的对话框
|
||||
2. 只允许用户点击「立即更新」
|
||||
3. 下载完成后自动调用安装
|
||||
|
||||
```kotlin
|
||||
fun showForceUpdateDialog(update: UpdateInfo) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle("发现重要更新")
|
||||
.setMessage("当前版本已不可用,请升级至 ${update.versionName}")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("立即更新") { _, _ ->
|
||||
lifecycleScope.launch {
|
||||
UpdateSDK.downloadAndInstall(context, update.downloadUrl!!) { progress ->
|
||||
// 显示下载进度
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 多模块统一登录
|
||||
|
||||
Update 模块与 IM、Push 模块共享同一套登录态:
|
||||
|
||||
```kotlin
|
||||
XuqmSDK.login(userId = "user_001", userSig = "jwt_token")
|
||||
// UpdateSDK 在 checkAppUpdate 时自动携带 appKey,无需额外登录操作
|
||||
```
|
||||
333
docs-site/docs/flutter/index.md
普通文件
333
docs-site/docs/flutter/index.md
普通文件
@ -0,0 +1,333 @@
|
||||
# Flutter SDK 概览
|
||||
|
||||
**包名**:`xuqm_flutter_sdk` · **版本**:0.2.x · **语言**:Dart
|
||||
|
||||
`xuqm_flutter_sdk` 是 XuqmGroup 的 Flutter 端统一入口,通过 MethodChannel 桥接原生能力,同时提供纯 Dart 实现的 IM HTTP API。
|
||||
|
||||
---
|
||||
|
||||
## 功能模块
|
||||
|
||||
| 模块 | 包 | 功能 |
|
||||
|------|-----|------|
|
||||
| `xuqm_flutter_common` | `packages/common` | 初始化、网络、配置管理 |
|
||||
| `xuqm_flutter_im` | `packages/im` | 单聊、群聊、消息收发、会话、好友、群组 |
|
||||
| `xuqm_flutter_push` | `packages/push` | 设备 Token 注册、厂商检测(Android)/ APNs(iOS)|
|
||||
| `xuqm_flutter_update` | `packages/update` | App 版本检查、商店跳转、APK 下载(Android)|
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
|
||||
在 `pubspec.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
xuqm_flutter_sdk: ^0.2.0
|
||||
```
|
||||
|
||||
配置私有仓库(如有):
|
||||
|
||||
```yaml
|
||||
dependency_overrides:
|
||||
xuqm_flutter_common:
|
||||
hosted:
|
||||
name: xuqm_flutter_common
|
||||
url: https://nexus.xuqinmin.com/repository/pub-hosted/
|
||||
version: ^0.2.0
|
||||
```
|
||||
|
||||
执行安装:
|
||||
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速接入
|
||||
|
||||
### 1. 初始化
|
||||
|
||||
```dart
|
||||
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
|
||||
|
||||
await XuqmSDK.initialize(XuqmInitOptions(
|
||||
appKey: 'your_app_key', // 在租户平台创建应用后获得
|
||||
debug: true, // 可选,开启详细日志
|
||||
));
|
||||
```
|
||||
|
||||
初始化时会自动向服务端请求远程配置(IM API 地址等),若网络异常则回退到内置默认值。
|
||||
|
||||
如需同步初始化(不拉取远程配置):
|
||||
|
||||
```dart
|
||||
XuqmSDK.init(XuqmInitOptions(appKey: 'your_app_key'));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. IM 登录
|
||||
|
||||
```dart
|
||||
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
|
||||
|
||||
// 只需要 userId + userSig
|
||||
await XuqmImSdk().login('user_001', 'your_user_sig_jwt');
|
||||
```
|
||||
|
||||
登录成功后会自动建立 WebSocket 连接。
|
||||
|
||||
---
|
||||
|
||||
### 3. 监听消息
|
||||
|
||||
```dart
|
||||
final im = XuqmImSdk();
|
||||
|
||||
im.ws.onConnected = () {
|
||||
print('WebSocket 已连接');
|
||||
};
|
||||
|
||||
im.ws.onMessage = (XuqmImMessage msg) {
|
||||
print('收到消息: ${msg.msgType} - ${msg.content}');
|
||||
};
|
||||
|
||||
im.ws.onDisconnected = (String? reason) {
|
||||
print('WebSocket 断开: $reason');
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 发送消息
|
||||
|
||||
```dart
|
||||
// 发送文本消息
|
||||
final msg = await im.sendTextMessage(
|
||||
'user_002',
|
||||
'SINGLE',
|
||||
'Hello from Flutter!',
|
||||
);
|
||||
|
||||
// 发送图片消息
|
||||
final imgMsg = await im.sendMessage(
|
||||
'user_002',
|
||||
'SINGLE',
|
||||
'IMAGE',
|
||||
jsonEncode({'url': 'https://cdn.example.com/img.jpg', 'width': 800, 'height': 600}),
|
||||
);
|
||||
|
||||
// 撤回消息
|
||||
await im.revokeMessage(msg.id);
|
||||
|
||||
// 编辑消息
|
||||
await im.editMessage(msg.id, '新内容');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 会话管理
|
||||
|
||||
```dart
|
||||
// 会话列表
|
||||
final conversations = await im.listConversations();
|
||||
|
||||
// 置顶会话
|
||||
await im.setConversationPinned('user_002', 'SINGLE', true);
|
||||
|
||||
// 免打扰
|
||||
await im.setConversationMuted('group_xxx', 'GROUP', true);
|
||||
|
||||
// 标记已读
|
||||
await im.markRead('user_002');
|
||||
|
||||
// 设置草稿
|
||||
await im.setDraft('user_002', 'SINGLE', '未完成的消息');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 好友与群组
|
||||
|
||||
```dart
|
||||
// 好友列表
|
||||
final friends = await im.listFriends();
|
||||
|
||||
// 添加好友
|
||||
await im.addFriend('user_002');
|
||||
|
||||
// 移除好友
|
||||
await im.removeFriend('user_002');
|
||||
|
||||
// 创建群组
|
||||
final group = await im.createGroup('Flutter 群', ['user_001', 'user_002']);
|
||||
|
||||
// 群组列表
|
||||
final groups = await im.listGroups();
|
||||
|
||||
// 添加群成员
|
||||
await im.addGroupMember(group.id, 'user_003');
|
||||
|
||||
// 退出群聊
|
||||
await im.leaveGroup(group.id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 历史消息
|
||||
|
||||
```dart
|
||||
// 单聊历史
|
||||
final history = await im.fetchHistory('user_002', page: 0, size: 20);
|
||||
|
||||
// 群聊历史
|
||||
final groupHistory = await im.fetchGroupHistory('group_xxx', page: 0, size: 50);
|
||||
|
||||
// 定位消息所在页
|
||||
final page = await im.locateHistoryPage(
|
||||
'user_002',
|
||||
messageId: 'msg_xxx',
|
||||
pageSize: 20,
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Push 接入
|
||||
|
||||
```dart
|
||||
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
|
||||
|
||||
// 检测当前设备推送厂商(Android)
|
||||
final vendor = await XuqmPushSdk.detectVendor();
|
||||
// 返回:HUAWEI / XIAOMI / OPPO / VIVO / HONOR / FCM / APNS
|
||||
|
||||
// 请求原生推送注册
|
||||
await XuqmPushSdk.requestNativeRegistration();
|
||||
|
||||
// 监听原生 Token 回调
|
||||
XuqmPushSdk.onPushToken.listen((tokenInfo) {
|
||||
final token = tokenInfo['token'];
|
||||
final vendor = tokenInfo['vendor'];
|
||||
print('收到 Push Token: $token, 厂商: $vendor');
|
||||
|
||||
// 手动上报到服务端(如需要)
|
||||
final pushSdk = XuqmPushSdk();
|
||||
await pushSdk.registerToken(
|
||||
'user_001',
|
||||
token!,
|
||||
vendor: vendor,
|
||||
platform: 'ANDROID',
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update 接入
|
||||
|
||||
```dart
|
||||
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
|
||||
|
||||
final updateSdk = XuqmUpdateSdk();
|
||||
|
||||
// 检查 App 更新
|
||||
final update = await updateSdk.checkAppUpdate();
|
||||
if (update.needsUpdate) {
|
||||
print('新版本: ${update.versionName}');
|
||||
print('更新日志: ${update.changeLog}');
|
||||
|
||||
if (update.forceUpdate) {
|
||||
// 强制更新
|
||||
showForceUpdateDialog(update);
|
||||
} else {
|
||||
// 可选更新
|
||||
showOptionalUpdateDialog(update);
|
||||
}
|
||||
}
|
||||
|
||||
// 打开商店 / 应用市场
|
||||
await updateSdk.openStore(update);
|
||||
|
||||
// Android:直接打开 APK 下载链接
|
||||
await updateSdk.openDownloadUrl(update);
|
||||
```
|
||||
|
||||
开发/测试环境可覆盖版本号:
|
||||
|
||||
```dart
|
||||
XuqmUpdateSdk.devSetAppVersion(100, '1.0.0');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 多模块统一登录
|
||||
|
||||
无论集成了哪些模块(IM、Push、Update),**初始化和登录永远只做一次**:
|
||||
|
||||
```dart
|
||||
// 初始化
|
||||
await XuqmSDK.initialize(XuqmInitOptions(appKey: 'your_app_key'));
|
||||
|
||||
// 登录(业务登录成功后调用一次)
|
||||
await XuqmImSdk().login('user_001', 'your_user_sig_jwt');
|
||||
|
||||
// 登出
|
||||
await XuqmImSdk().logout();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整示例
|
||||
|
||||
```dart
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
|
||||
|
||||
class ChatPage extends StatefulWidget {
|
||||
@override
|
||||
_ChatPageState createState() => _ChatPageState();
|
||||
}
|
||||
|
||||
class _ChatPageState extends State<ChatPage> {
|
||||
final im = XuqmImSdk();
|
||||
final List<XuqmImMessage> messages = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
await XuqmSDK.initialize(XuqmInitOptions(appKey: 'your_app_key'));
|
||||
await im.login('user_001', 'your_user_sig_jwt');
|
||||
|
||||
im.ws.onMessage = (msg) {
|
||||
setState(() => messages.add(msg));
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _send(String text) async {
|
||||
final msg = await im.sendTextMessage('user_002', 'SINGLE', text);
|
||||
setState(() => messages.add(msg));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Flutter IM')),
|
||||
body: ListView.builder(
|
||||
itemCount: messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final msg = messages[index];
|
||||
return ListTile(title: Text('${msg.fromId}: ${msg.content}'));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
114
docs-site/docs/guide/concepts.md
普通文件
114
docs-site/docs/guide/concepts.md
普通文件
@ -0,0 +1,114 @@
|
||||
# 平台概念
|
||||
|
||||
了解 XuqmGroup 平台的核心概念,有助于更好地接入 SDK。
|
||||
|
||||
---
|
||||
|
||||
## App / Tenant
|
||||
|
||||
| 概念 | 说明 |
|
||||
|------|------|
|
||||
| **Tenant(租户)** | 对应一个开发者账号/企业,可在控制台创建多个应用 |
|
||||
| **App(应用)** | 对应一个具体的客户端应用,拥有独立的 `appKey` 和 `appSecret` |
|
||||
| **appKey** | 应用唯一标识,客户端初始化时传入 |
|
||||
| **appSecret** | 应用密钥,**仅保存在服务端**,用于签发 UserSig |
|
||||
|
||||
---
|
||||
|
||||
## UserSig
|
||||
|
||||
UserSig 是 XuqmGroup 的登录鉴权凭证,由业务服务端用 `appSecret` 为用户签发的安全凭证。
|
||||
|
||||
### 特点
|
||||
|
||||
- 当前版本不过期,只校验 `userId + UserSig` 是否匹配
|
||||
- `appSecret` **绝不下发到客户端**
|
||||
- 若需撤销权限,可在租户平台重置 `appSecret` 或拉黑账号
|
||||
|
||||
### 签发方式(示例)
|
||||
|
||||
```ts
|
||||
// Node.js 示例
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
const userSig = jwt.sign(
|
||||
{ userId: 'user_001', appKey: 'your_app_key' },
|
||||
'your_app_secret',
|
||||
{ algorithm: 'HS256' }
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 消息类型
|
||||
|
||||
| 类型 | 说明 | content 结构 |
|
||||
|------|------|-------------|
|
||||
| TEXT | 纯文本 | `String` |
|
||||
| IMAGE | 图片 | `{url, width, height, thumbnailUrl?}` |
|
||||
| VIDEO | 视频 | `{url, duration, thumbnailUrl, size}` |
|
||||
| AUDIO | 语音 | `{url, duration, size}` |
|
||||
| FILE | 文件 | `{url, name, size, mimeType}` |
|
||||
| LOCATION | 位置 | `{lat, lng, address, title}` |
|
||||
| CUSTOM | 自定义 | 任意 JSON |
|
||||
| NOTIFY | 系统通知 | `{title, content}` |
|
||||
| RICH_TEXT | 富文本 | `{html}` |
|
||||
| CALL_AUDIO | 语音通话信令 | `{action}` |
|
||||
| CALL_VIDEO | 视频通话信令 | `{action}` |
|
||||
| QUOTE | 引用 | `{quotedMsgId, quotedContent, text}` |
|
||||
| MERGE | 合并转发 | `{title, msgList}` |
|
||||
| FORWARD | 转发 | `{originalSender, originalContent}` |
|
||||
| REVOKED | 撤回 | 系统内部填充 |
|
||||
|
||||
---
|
||||
|
||||
## 会话
|
||||
|
||||
会话(Conversation)是用户与单聊对象或群组的聊天关系抽象。
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| targetId | 对方用户 ID 或群 ID |
|
||||
| chatType | `SINGLE` / `GROUP` |
|
||||
| unreadCount | 未读消息数 |
|
||||
| isPinned | 是否置顶 |
|
||||
| isMuted | 是否免打扰 |
|
||||
| lastMsgContent | 最后一条消息内容 |
|
||||
| lastMsgTime | 最后一条消息时间(Unix 毫秒)|
|
||||
|
||||
---
|
||||
|
||||
## 群组
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| id | 群唯一 ID |
|
||||
| name | 群名称 |
|
||||
| creatorId | 创建者用户 ID |
|
||||
| memberIds | 成员列表(JSON 数组字符串)|
|
||||
| adminIds | 管理员列表(JSON 数组字符串)|
|
||||
| groupType | 群类型:`WORK` / `PUBLIC` / `PRIVATE` |
|
||||
| announcement | 群公告 |
|
||||
|
||||
### 群角色
|
||||
|
||||
| 角色 | 说明 |
|
||||
|------|------|
|
||||
| OWNER | 群主 |
|
||||
| ADMIN | 管理员 |
|
||||
| MEMBER | 普通成员 |
|
||||
|
||||
---
|
||||
|
||||
## 消息状态
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| SENDING | 发送中 |
|
||||
| SENT | 已发送 |
|
||||
| DELIVERED | 已送达 |
|
||||
| READ | 已读 |
|
||||
| FAILED | 发送失败 |
|
||||
| REVOKED | 已撤回 |
|
||||
|
||||
[→ 接入流程 →](./flow)
|
||||
137
docs-site/docs/guide/flow.md
普通文件
137
docs-site/docs/guide/flow.md
普通文件
@ -0,0 +1,137 @@
|
||||
# 接入流程
|
||||
|
||||
完整的 XuqmGroup SDK 接入流程,从注册账号到客户端收发消息。
|
||||
|
||||
---
|
||||
|
||||
## 1. 注册开发者账号
|
||||
|
||||
1. 访问 [XuqmGroup 控制台](https://dev.xuqinmin.com)
|
||||
2. 点击注册,填写企业/个人信息
|
||||
3. 完成邮箱/手机验证
|
||||
|
||||
---
|
||||
|
||||
## 2. 创建应用
|
||||
|
||||
1. 登录控制台 → 应用管理 → 创建应用
|
||||
2. 填写应用名称、平台类型(Android / iOS / Web / RN / 小程序 / HarmonyOS)
|
||||
3. 创建成功后获得:
|
||||
- `appKey`(客户端使用)
|
||||
- `appSecret`(服务端使用,**不可泄露**)
|
||||
|
||||
---
|
||||
|
||||
## 3. 获取 AppKey
|
||||
|
||||
在应用详情页复制 `appKey`,用于客户端 SDK 初始化:
|
||||
|
||||
```kotlin
|
||||
// Android
|
||||
XuqmSDK.initialize(context, appKey = "your_app_key")
|
||||
```
|
||||
|
||||
```swift
|
||||
// iOS
|
||||
let config = SDKConfig(appKey: "your_app_key", appSecret: "your_app_secret")
|
||||
XuqmSDK.shared.initialize(config: config)
|
||||
```
|
||||
|
||||
```ts
|
||||
// Vue3 / Web
|
||||
init({ appKey: 'your_app_key', appSecret: 'your_app_secret' })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 服务端签发 UserSig
|
||||
|
||||
`appSecret` 只应保存在业务服务端,用于为每个用户签发 `userSig`。
|
||||
|
||||
### 签发逻辑(示例)
|
||||
|
||||
```ts
|
||||
// Node.js
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
function generateUserSig(userId: string, appKey: string, appSecret: string): string {
|
||||
return jwt.sign(
|
||||
{ userId, appKey },
|
||||
appSecret,
|
||||
{ algorithm: 'HS256' }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# Python
|
||||
import jwt
|
||||
|
||||
def generate_user_sig(user_id: str, app_key: str, app_secret: str) -> str:
|
||||
return jwt.encode(
|
||||
{"userId": user_id, "appKey": app_key},
|
||||
app_secret,
|
||||
algorithm="HS256"
|
||||
)
|
||||
```
|
||||
|
||||
```go
|
||||
// Go
|
||||
import "github.com/golang-jwt/jwt/v5"
|
||||
|
||||
func GenerateUserSig(userID, appKey, appSecret string) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"userId": userID,
|
||||
"appKey": appKey,
|
||||
})
|
||||
return token.SignedString([]byte(appSecret))
|
||||
}
|
||||
```
|
||||
|
||||
### 接口设计建议
|
||||
|
||||
```
|
||||
POST /api/auth/xuqm-login
|
||||
Headers: Authorization: Bearer {your-app-auth-token}
|
||||
Body: { "userId": "user_001" }
|
||||
Response: { "userSig": "jwt_token_string" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 客户端接入 SDK
|
||||
|
||||
### 登录流程
|
||||
|
||||
```
|
||||
客户端
|
||||
→ 业务登录(用户名/密码)
|
||||
→ 业务服务端验证成功
|
||||
→ 业务服务端调用 generateUserSig(userId, appKey, appSecret)
|
||||
→ 返回 userSig 给客户端
|
||||
→ 客户端调用 XuqmSDK.login(userId, userSig)
|
||||
→ SDK 自动连接 WebSocket
|
||||
→ 开始收发消息
|
||||
```
|
||||
|
||||
### 各平台接入
|
||||
|
||||
| 平台 | 文档 |
|
||||
|------|------|
|
||||
| Android | [Android SDK →](/android/) |
|
||||
| iOS | [iOS SDK →](/ios/) |
|
||||
| React Native | [RN SDK →](/rn/) |
|
||||
| Vue3 / Web | [Vue3 SDK →](/vue3/) |
|
||||
| HarmonyOS | [HarmonyOS SDK →](/harmony/) |
|
||||
| 微信小程序 | [小程序 SDK →](/miniprogram/) |
|
||||
|
||||
---
|
||||
|
||||
## 安全提示
|
||||
|
||||
- `appSecret` **绝不下发到客户端**,仅用于服务端签发 UserSig
|
||||
- 若需撤销用户权限,可在租户平台重置 `appSecret` 或拉黑账号
|
||||
- 所有 API 通信使用 HTTPS / WSS
|
||||
- UserSig 当前版本不过期,业务方可自行控制签发逻辑
|
||||
|
||||
[→ 快速开始 →](./quickstart)
|
||||
194
docs-site/docs/harmony/im.md
普通文件
194
docs-site/docs/harmony/im.md
普通文件
@ -0,0 +1,194 @@
|
||||
# HarmonyOS IM 接入
|
||||
|
||||
基于 `@xuqm/harmony-sdk` 实现即时通讯功能。
|
||||
|
||||
---
|
||||
|
||||
## 初始化与登录
|
||||
|
||||
### 初始化
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/harmony-sdk'
|
||||
import common from '@ohos.app.ability.common'
|
||||
|
||||
const context = getContext(this) as common.UIAbilityContext
|
||||
await XuqmSDK.init(context, {
|
||||
appKey: 'your_app_key',
|
||||
appSecret: 'your_app_secret',
|
||||
})
|
||||
```
|
||||
|
||||
### 登录
|
||||
|
||||
```ts
|
||||
const session = await XuqmSDK.login('user_001', 'your_user_sig_jwt')
|
||||
```
|
||||
|
||||
### 登出
|
||||
|
||||
```ts
|
||||
await XuqmSDK.logout()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 消息收发
|
||||
|
||||
### 设置事件代理
|
||||
|
||||
```ts
|
||||
const im = XuqmSDK.im
|
||||
|
||||
im.delegate = {
|
||||
onConnected: () => {
|
||||
console.log('IM 已连接')
|
||||
},
|
||||
onDisconnected: (code: number, reason: string) => {
|
||||
console.log('断开连接:', code, reason)
|
||||
},
|
||||
onMessage: (msg: ImMessage) => {
|
||||
console.log('收到消息:', msg.msgType, msg.content)
|
||||
},
|
||||
onRead: (msg: ImMessage) => {
|
||||
console.log('已读回执:', msg.id)
|
||||
},
|
||||
onRevoke: (data: RevokeData) => {
|
||||
console.log('消息被撤回:', data.msgId)
|
||||
},
|
||||
onError: (message: string) => {
|
||||
console.error('IM 错误:', message)
|
||||
},
|
||||
}
|
||||
|
||||
im.connect()
|
||||
```
|
||||
|
||||
### 发送消息
|
||||
|
||||
```ts
|
||||
const outgoing = im.send({
|
||||
toId: 'user_002',
|
||||
chatType: 'SINGLE',
|
||||
msgType: 'TEXT',
|
||||
content: 'Hello!',
|
||||
})
|
||||
```
|
||||
|
||||
### 发送多媒体消息
|
||||
|
||||
```ts
|
||||
// 图片
|
||||
im.send({
|
||||
toId: 'user_002',
|
||||
chatType: 'SINGLE',
|
||||
msgType: 'IMAGE',
|
||||
content: JSON.stringify({ url: 'https://...', width: 800, height: 600 }),
|
||||
})
|
||||
|
||||
// 位置
|
||||
im.send({
|
||||
toId: 'user_002',
|
||||
chatType: 'SINGLE',
|
||||
msgType: 'LOCATION',
|
||||
content: JSON.stringify({ lat: 31.2304, lng: 121.4737, title: '上海' }),
|
||||
})
|
||||
```
|
||||
|
||||
### 撤回消息
|
||||
|
||||
```ts
|
||||
im.revoke('message_id')
|
||||
```
|
||||
|
||||
### 获取历史消息
|
||||
|
||||
```ts
|
||||
const result = await im.fetchHistory('user_002', 0, 20)
|
||||
// result.content — 消息列表
|
||||
// result.totalElements — 总数量
|
||||
|
||||
const groupResult = await im.fetchGroupHistory('group_xxx', 0, 50)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 会话列表
|
||||
|
||||
```ts
|
||||
const conversations = await im.listConversations(20)
|
||||
|
||||
// 标记已读
|
||||
await im.markRead('user_002', 'SINGLE')
|
||||
|
||||
// 草稿
|
||||
await im.setDraft('user_002', 'SINGLE', '草稿内容')
|
||||
|
||||
// 置顶
|
||||
await im.setConversationPinned('user_002', 'SINGLE', true)
|
||||
|
||||
// 免打扰
|
||||
await im.setConversationMuted('user_002', 'SINGLE', true)
|
||||
|
||||
// 删除会话
|
||||
await im.deleteConversation('user_002', 'SINGLE')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群聊
|
||||
|
||||
```ts
|
||||
// 创建群
|
||||
const group = await im.createGroup('项目讨论', ['user_002', 'user_003'], 'WORK')
|
||||
|
||||
// 群列表
|
||||
const groups = await im.listGroups()
|
||||
|
||||
// 群详情
|
||||
const groupInfo = await im.getGroupInfo('group_xxx')
|
||||
|
||||
// 群成员
|
||||
const members = await im.listGroupMembers('group_xxx')
|
||||
|
||||
// 添加/移除成员
|
||||
await im.addGroupMember('group_xxx', 'user_004')
|
||||
await im.removeGroupMember('group_xxx', 'user_004')
|
||||
|
||||
// 退出群聊
|
||||
await im.leaveGroup('group_xxx')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 好友管理
|
||||
|
||||
```ts
|
||||
const friends = await im.listFriends()
|
||||
await im.addFriend('user_002')
|
||||
await im.removeFriend('user_002')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 消息类型
|
||||
|
||||
| MsgType | 说明 |
|
||||
|---------|------|
|
||||
| `TEXT` | 纯文本 |
|
||||
| `IMAGE` | 图片 |
|
||||
| `VIDEO` | 视频 |
|
||||
| `AUDIO` | 语音 |
|
||||
| `FILE` | 文件 |
|
||||
| `LOCATION` | 位置 |
|
||||
| `CUSTOM` | 自定义 |
|
||||
| `NOTIFY` | 系统通知 |
|
||||
| `RICH_TEXT` | 富文本 |
|
||||
| `CALL_AUDIO` | 语音通话信令 |
|
||||
| `CALL_VIDEO` | 视频通话信令 |
|
||||
| `QUOTE` | 引用 |
|
||||
| `MERGE` | 合并转发 |
|
||||
| `FORWARD` | 转发 |
|
||||
| `REVOKED` | 撤回 |
|
||||
|
||||
[→ 完整 API Reference](./api)
|
||||
@ -22,6 +22,70 @@
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Push 接入
|
||||
|
||||
HarmonyOS SDK 提供 `PushSDK` 用于注册和注销推送 Token。业务层需在获取到 HarmonyOS 推送服务的 Token 后调用注册接口。
|
||||
|
||||
```typescript
|
||||
import { PushSDK } from '@xuqm/harmony-sdk'
|
||||
|
||||
// 注册 Push Token(在获取到系统推送 Token 后调用)
|
||||
await PushSDK.registerToken('harmony_push_token', 'user_001')
|
||||
|
||||
// 登出时注销 Token
|
||||
await PushSDK.unregisterToken('harmony_push_token')
|
||||
```
|
||||
|
||||
> HarmonyOS 推送 Token 通过系统 Push Kit 获取,具体接入请参考 HarmonyOS 官方推送文档。`vendor` 固定为 `HARMONY`,`platform` 固定为 `harmony`。
|
||||
|
||||
---
|
||||
|
||||
## 9. 版本更新
|
||||
|
||||
HarmonyOS SDK 提供整包检查和 RN Bundle 热更新能力。
|
||||
|
||||
### 9.1 检查 App 更新
|
||||
|
||||
```typescript
|
||||
import { UpdateSDK } from '@xuqm/harmony-sdk'
|
||||
|
||||
const result = await UpdateSDK.checkAppUpdate('your_app_key')
|
||||
if (result.hasUpdate) {
|
||||
console.log('发现新版本:', result.info?.latestVersionCode)
|
||||
// HarmonyOS 整包更新只提供应用市场跳转
|
||||
if (result.info?.marketUrl) {
|
||||
await UpdateSDK.openAppMarket(getContext(this), result.info.marketUrl)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 RN Bundle 热更新
|
||||
|
||||
```typescript
|
||||
const rnResult = await UpdateSDK.checkRNUpdate(
|
||||
'your_app_key',
|
||||
'home', // bundle 名称
|
||||
1 // 当前 bundle 版本号
|
||||
)
|
||||
|
||||
if (rnResult.hasUpdate) {
|
||||
console.log('发现 RN Bundle 更新:', rnResult.info?.bundleVersion)
|
||||
|
||||
// 下载 Bundle
|
||||
const destPath = await UpdateSDK.downloadRnBundle(
|
||||
getContext(this),
|
||||
rnResult.info!.downloadUrl,
|
||||
'home.bundle'
|
||||
)
|
||||
console.log('Bundle 下载完成:', destPath)
|
||||
// 缓存至本地,由 BundleRuntime 加载
|
||||
}
|
||||
```
|
||||
|
||||
> 与第 7 节「检查更新」中的示例相比,此处展示了 `UpdateSDK` 的完整独立 API。HarmonyOS 的整包更新只提供应用市场跳转,不提供本地安装包下载。
|
||||
|
||||
配置私有仓库(`.ohpmrc`):
|
||||
|
||||
```
|
||||
@ -168,3 +232,67 @@ struct ChatView {
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Push 接入
|
||||
|
||||
HarmonyOS SDK 提供 `PushSDK` 用于注册和注销推送 Token。业务层需在获取到 HarmonyOS 推送服务的 Token 后调用注册接口。
|
||||
|
||||
```typescript
|
||||
import { PushSDK } from '@xuqm/harmony-sdk'
|
||||
|
||||
// 注册 Push Token(在获取到系统推送 Token 后调用)
|
||||
await PushSDK.registerToken('harmony_push_token', 'user_001')
|
||||
|
||||
// 登出时注销 Token
|
||||
await PushSDK.unregisterToken('harmony_push_token')
|
||||
```
|
||||
|
||||
> HarmonyOS 推送 Token 通过系统 Push Kit 获取,具体接入请参考 HarmonyOS 官方推送文档。SDK 内部 `vendor` 固定为 `HARMONY`,`platform` 固定为 `harmony`。
|
||||
|
||||
---
|
||||
|
||||
## 9. 版本更新
|
||||
|
||||
HarmonyOS SDK 提供整包检查和 RN Bundle 热更新能力。
|
||||
|
||||
### 9.1 检查 App 更新
|
||||
|
||||
```typescript
|
||||
import { UpdateSDK } from '@xuqm/harmony-sdk'
|
||||
|
||||
const result = await UpdateSDK.checkAppUpdate('your_app_key')
|
||||
if (result.hasUpdate) {
|
||||
console.log('发现新版本:', result.info?.latestVersionCode)
|
||||
// HarmonyOS 整包更新只提供应用市场跳转
|
||||
if (result.info?.marketUrl) {
|
||||
await UpdateSDK.openAppMarket(getContext(this), result.info.marketUrl)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 RN Bundle 热更新
|
||||
|
||||
```typescript
|
||||
const rnResult = await UpdateSDK.checkRnUpdate(
|
||||
'your_app_key',
|
||||
'home', // bundle 名称
|
||||
1 // 当前 bundle 版本号
|
||||
)
|
||||
|
||||
if (rnResult.hasUpdate) {
|
||||
console.log('发现 RN Bundle 更新:', rnResult.info?.bundleVersion)
|
||||
|
||||
// 下载 Bundle
|
||||
const destPath = await UpdateSDK.downloadRnBundle(
|
||||
getContext(this),
|
||||
rnResult.info!.downloadUrl,
|
||||
'home.bundle'
|
||||
)
|
||||
console.log('Bundle 下载完成:', destPath)
|
||||
// 缓存至本地,由 BundleRuntime 加载
|
||||
}
|
||||
```
|
||||
|
||||
> 与第 7 节「检查更新」中的示例相比,此处展示了 `UpdateSDK` 的完整独立 API。HarmonyOS 的整包更新只提供应用市场跳转,不提供本地安装包下载。
|
||||
|
||||
106
docs-site/docs/harmony/setup.md
普通文件
106
docs-site/docs/harmony/setup.md
普通文件
@ -0,0 +1,106 @@
|
||||
# HarmonyOS 安装配置
|
||||
|
||||
**包名**:`@xuqm/harmony-sdk` · **版本**:0.1.0 · **语言**:ArkTS
|
||||
|
||||
---
|
||||
|
||||
## ohpm 安装
|
||||
|
||||
在 HarmonyOS 项目的 `oh-package.json5` 中添加:
|
||||
|
||||
```json5
|
||||
{
|
||||
"dependencies": {
|
||||
"@xuqm/harmony-sdk": "^0.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后执行:
|
||||
|
||||
```bash
|
||||
ohpm install
|
||||
```
|
||||
|
||||
> 发布仓库:`https://ohpm.openharmony.cn/ohpm/`
|
||||
|
||||
---
|
||||
|
||||
## 最低 API 版本
|
||||
|
||||
- API 12+(HarmonyOS 4.0+)
|
||||
- DevEco Studio 4.0 Release 或更高版本
|
||||
|
||||
---
|
||||
|
||||
## 权限配置
|
||||
|
||||
在 `module.json5` 中声明所需权限:
|
||||
|
||||
```json5
|
||||
{
|
||||
"module": {
|
||||
"requestPermissions": [
|
||||
{
|
||||
"name": "ohos.permission.INTERNET"
|
||||
},
|
||||
{
|
||||
"name": "ohos.permission.GET_NETWORK_INFO"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如需使用相机、相册、麦克风等功能,额外添加:
|
||||
|
||||
```json5
|
||||
{
|
||||
"module": {
|
||||
"requestPermissions": [
|
||||
{
|
||||
"name": "ohos.permission.CAMERA"
|
||||
},
|
||||
{
|
||||
"name": "ohos.permission.READ_MEDIA"
|
||||
},
|
||||
{
|
||||
"name": "ohos.permission.WRITE_MEDIA"
|
||||
},
|
||||
{
|
||||
"name": "ohos.permission.MICROPHONE"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 初始化
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/harmony-sdk'
|
||||
import common from '@ohos.app.ability.common'
|
||||
|
||||
// 在 EntryAbility.onCreate 或页面初始化时
|
||||
const context = getContext(this) as common.UIAbilityContext
|
||||
await XuqmSDK.init(context, {
|
||||
appKey: 'your_app_key',
|
||||
appSecret: 'your_app_secret',
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 登录
|
||||
|
||||
```ts
|
||||
const session = await XuqmSDK.login('user_001', 'your_user_sig_jwt')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
- [HarmonyOS IM 接入 →](./im)
|
||||
254
docs-site/docs/ios/im.md
普通文件
254
docs-site/docs/ios/im.md
普通文件
@ -0,0 +1,254 @@
|
||||
# iOS IM 接入
|
||||
|
||||
基于 `XuqmSDK` 中的 IM 子模块实现即时通讯功能。
|
||||
|
||||
---
|
||||
|
||||
## 初始化与登录
|
||||
|
||||
### 初始化
|
||||
|
||||
```swift
|
||||
import XuqmSDK
|
||||
|
||||
let config = SDKConfig(appKey: "your_app_key", appSecret: "your_app_secret")
|
||||
XuqmSDK.shared.initialize(config: config)
|
||||
```
|
||||
|
||||
### 登录(UserSig 模式)
|
||||
|
||||
```swift
|
||||
import XuqmSDK
|
||||
|
||||
try await XuqmSDK.shared.login(userId: "user_001", userSig: "your_user_sig_jwt")
|
||||
```
|
||||
|
||||
> `userSig` 由业务服务端用 `appSecret` 签发。若需更新登录态,直接重新登录即可。
|
||||
|
||||
---
|
||||
|
||||
## 消息收发
|
||||
|
||||
### 设置事件代理
|
||||
|
||||
```swift
|
||||
import XuqmSDK
|
||||
|
||||
ImSDK.shared.setDelegate(self)
|
||||
|
||||
extension ViewController: ImEventDelegate {
|
||||
func imClientDidConnect() { print("WS connected") }
|
||||
func imClientDidReceiveMessage(_ msg: ImMessage) { /* 处理单聊消息 */ }
|
||||
func imClientDidReceiveGroupMessage(_ msg: ImMessage) { /* 处理群消息 */ }
|
||||
func imClientDidReadMessage(_ msg: ImMessage) { /* 对方已读回执 */ }
|
||||
func imClientDidReceiveRevokedMessage(_ msg: ImMessage) { /* 消息被撤回 */ }
|
||||
func imClientDidDisconnect(reason: String?) { /* 断线 */ }
|
||||
func imClientDidError(_ error: String) { /* 错误 */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 发送文本消息
|
||||
|
||||
```swift
|
||||
let msg = ImSDK.shared.sendTextMessage(
|
||||
toId: "user_002",
|
||||
chatType: .single,
|
||||
content: "Hello!"
|
||||
)
|
||||
```
|
||||
|
||||
### 发送多媒体消息
|
||||
|
||||
```swift
|
||||
// 图片
|
||||
let msg = ImSDK.shared.sendImageMessage(
|
||||
toId: "user_002",
|
||||
chatType: .single,
|
||||
url: "https://cdn.example.com/img.jpg",
|
||||
width: 800,
|
||||
height: 600
|
||||
)
|
||||
|
||||
// 视频
|
||||
let msg = ImSDK.shared.sendVideoMessage(
|
||||
toId: "user_002",
|
||||
chatType: .single,
|
||||
url: "https://cdn.example.com/video.mp4",
|
||||
duration: 15,
|
||||
size: 5_000_000
|
||||
)
|
||||
|
||||
// 语音
|
||||
let msg = ImSDK.shared.sendAudioMessage(
|
||||
toId: "user_002",
|
||||
chatType: .single,
|
||||
url: "https://cdn.example.com/audio.mp3",
|
||||
duration: 5,
|
||||
size: 100_000
|
||||
)
|
||||
|
||||
// 文件
|
||||
let msg = ImSDK.shared.sendFileMessage(
|
||||
toId: "user_002",
|
||||
chatType: .single,
|
||||
url: "https://cdn.example.com/file.pdf",
|
||||
name: "document.pdf",
|
||||
size: 2_000_000
|
||||
)
|
||||
|
||||
// 位置
|
||||
let msg = ImSDK.shared.sendLocationMessage(
|
||||
toId: "user_002",
|
||||
chatType: .single,
|
||||
lat: 31.2304,
|
||||
lng: 121.4737,
|
||||
title: "上海",
|
||||
address: "上海市黄浦区"
|
||||
)
|
||||
```
|
||||
|
||||
### 撤回与编辑
|
||||
|
||||
```swift
|
||||
try await ImSDK.shared.revokeMessage(messageId: msg.id)
|
||||
try await ImSDK.shared.editMessage(messageId: msg.id, content: "新内容")
|
||||
```
|
||||
|
||||
### 获取历史消息
|
||||
|
||||
```swift
|
||||
let history = try await ImSDK.shared.fetchHistory(toId: "user_002", page: 0, size: 20)
|
||||
let groupHistory = try await ImSDK.shared.fetchGroupHistory(groupId: "group_xxx", page: 0, size: 20)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群聊
|
||||
|
||||
### 创建群组
|
||||
|
||||
```swift
|
||||
let group = try await ImSDK.shared.createGroup(
|
||||
name: "项目讨论",
|
||||
memberIds: ["user_001", "user_002"],
|
||||
groupType: "WORK"
|
||||
)
|
||||
```
|
||||
|
||||
### 订阅群消息
|
||||
|
||||
```swift
|
||||
ImSDK.shared.subscribeGroup(group.id)
|
||||
```
|
||||
|
||||
### 发送群消息
|
||||
|
||||
```swift
|
||||
ImSDK.shared.sendTextMessage(toId: group.id, chatType: .group, content: "大家好")
|
||||
```
|
||||
|
||||
### 群管理
|
||||
|
||||
```swift
|
||||
// 添加/移除成员
|
||||
try await ImSDK.shared.addGroupMember(groupId: group.id, userId: "user_003")
|
||||
try await ImSDK.shared.removeGroupMember(groupId: group.id, targetUserId: "user_003")
|
||||
|
||||
// 设置角色
|
||||
try await ImSDK.shared.setGroupRole(groupId: group.id, userId: "user_003", role: "ADMIN")
|
||||
|
||||
// 禁言
|
||||
try await ImSDK.shared.muteGroupMember(groupId: group.id, userId: "user_003", minutes: 60)
|
||||
|
||||
// 转让群主
|
||||
try await ImSDK.shared.transferGroupOwner(groupId: group.id, newOwnerId: "user_002")
|
||||
|
||||
// 退出/解散
|
||||
try await ImSDK.shared.leaveGroup(groupId: group.id)
|
||||
try await ImSDK.shared.dismissGroup(groupId: group.id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 好友管理
|
||||
|
||||
```swift
|
||||
let friends = try await ImSDK.shared.listFriends()
|
||||
try await ImSDK.shared.addFriend(friendId: "user_002")
|
||||
try await ImSDK.shared.removeFriend(friendId: "user_002")
|
||||
|
||||
// 好友请求
|
||||
try await ImSDK.shared.sendFriendRequest(toUserId: "user_002", remark: "你好")
|
||||
let requests = try await ImSDK.shared.listFriendRequests(direction: "incoming")
|
||||
try await ImSDK.shared.acceptFriendRequest(requestId: "request_id")
|
||||
try await ImSDK.shared.rejectFriendRequest(requestId: "request_id")
|
||||
|
||||
// 黑名单
|
||||
let blacklist = try await ImSDK.shared.listBlacklist()
|
||||
try await ImSDK.shared.addToBlacklist(blockedUserId: "user_002")
|
||||
try await ImSDK.shared.removeFromBlacklist(blockedUserId: "user_002")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 会话列表
|
||||
|
||||
```swift
|
||||
let conversations = try await ImSDK.shared.listConversations()
|
||||
|
||||
try await ImSDK.shared.setConversationPinned(targetId: "user_002", chatType: .single, pinned: true)
|
||||
try await ImSDK.shared.setConversationMuted(targetId: "user_002", chatType: .single, muted: true)
|
||||
try await ImSDK.shared.markRead(targetId: "user_002", chatType: .single)
|
||||
try await ImSDK.shared.setDraft(targetId: "user_002", chatType: .single, draft: "未完成")
|
||||
try await ImSDK.shared.deleteConversation(targetId: "user_002", chatType: .single)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 离线消息同步
|
||||
|
||||
```swift
|
||||
let count = try await ImSDK.shared.offlineMessageCount()
|
||||
let offlineMessages = try await ImSDK.shared.syncOfflineMessages()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 连接状态监听
|
||||
|
||||
```swift
|
||||
let state = ImSDK.shared.connectionState
|
||||
// .disconnected / .connecting / .connected
|
||||
|
||||
ImSDK.shared.addConnectionStateListener { state in
|
||||
switch state {
|
||||
case .connected: print("IM 已连接")
|
||||
case .connecting: print("IM 连接中...")
|
||||
case .disconnected: print("IM 已断开")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 消息类型
|
||||
|
||||
| MsgType | 说明 | content 结构 |
|
||||
|---------|------|-------------|
|
||||
| `.text` | 纯文本 | `String` |
|
||||
| `.image` | 图片 | `{url, width, height, thumbnailUrl?}` |
|
||||
| `.video` | 视频 | `{url, duration, thumbnailUrl, size}` |
|
||||
| `.audio` | 语音 | `{url, duration, size}` |
|
||||
| `.file` | 文件 | `{url, name, size, mimeType}` |
|
||||
| `.location` | 位置 | `{lat, lng, address, title}` |
|
||||
| `.custom` | 自定义 | 任意 JSON |
|
||||
| `.notify` | 系统通知 | `{title, content}` |
|
||||
| `.richText` | 富文本 | `{html}` |
|
||||
| `.callAudio` | 语音通话信令 | `{action}` |
|
||||
| `.callVideo` | 视频通话信令 | `{action}` |
|
||||
| `.forward` | 转发 | `{originalSender, originalContent}` |
|
||||
| `.quote` | 引用 | `{quotedMsgId, quotedContent, text}` |
|
||||
| `.merge` | 合并转发 | `{title, msgList}` |
|
||||
| `.revoked` | 撤回 | 系统内部填充 |
|
||||
|
||||
[→ 完整 API Reference](./api)
|
||||
142
docs-site/docs/ios/push.md
普通文件
142
docs-site/docs/ios/push.md
普通文件
@ -0,0 +1,142 @@
|
||||
# iOS 推送接入指南
|
||||
|
||||
**模块**:`XuqmPush` · **支持**:APNs(默认)、FCM(可选)
|
||||
|
||||
---
|
||||
|
||||
## 1. 添加依赖
|
||||
|
||||
在 `Package.swift` 中按需引入:
|
||||
|
||||
```swift
|
||||
.target(
|
||||
name: "MyApp",
|
||||
dependencies: [
|
||||
.product(name: "XuqmPush", package: "XuqmGroup-iOSSDK"),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
如需 FCM 支持,额外在 Xcode 中集成 `FirebaseMessaging`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 申请通知权限
|
||||
|
||||
在应用启动时请求用户授权:
|
||||
|
||||
```swift
|
||||
import XuqmPush
|
||||
|
||||
let granted = try await PushSDK.shared.requestAuthorization(options: [.alert, .badge, .sound])
|
||||
if granted {
|
||||
print("用户已授权通知")
|
||||
}
|
||||
```
|
||||
|
||||
`requestAuthorization` 内部会自动调用 `UIApplication.shared.registerForRemoteNotifications()`,无需额外处理。
|
||||
|
||||
---
|
||||
|
||||
## 3. 注册 APNs Token
|
||||
|
||||
在 `AppDelegate` 中转发系统回调:
|
||||
|
||||
```swift
|
||||
import XuqmSDK
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
|
||||
) {
|
||||
// 将 deviceToken 转发生给 SDK
|
||||
XuqmSDK.shared.registerDeviceToken(deviceToken)
|
||||
}
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFailToRegisterForRemoteNotificationsWithError error: Error
|
||||
) {
|
||||
print("注册远程通知失败: \(error.localizedDescription)")
|
||||
}
|
||||
```
|
||||
|
||||
> `XuqmSDK.shared.login()` 成功后会自动将 APNs Token 上报到 Push 服务端,业务层无需手动调用 `PushSDK.shared.registerDeviceToken()`。
|
||||
|
||||
---
|
||||
|
||||
## 4. FCM 支持(可选)
|
||||
|
||||
如需集成 Firebase Cloud Messaging:
|
||||
|
||||
1. 在 Xcode 中添加 `FirebaseMessaging` 依赖
|
||||
2. 配置 `GoogleService-Info.plist`
|
||||
3. 在获取到 FCM Token 后手动注册:
|
||||
|
||||
```swift
|
||||
import XuqmPush
|
||||
|
||||
// 在 Firebase Messaging 回调中获取 FCM Token
|
||||
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
|
||||
guard let token = fcmToken else { return }
|
||||
try await PushSDK.shared.registerFcmToken(token, userId: "user_001")
|
||||
}
|
||||
```
|
||||
|
||||
可通过 `PushSDK.shared.isFcmAvailable` 检查当前是否编译了 FCM:
|
||||
|
||||
```swift
|
||||
if PushSDK.shared.isFcmAvailable {
|
||||
print("FCM 模块已集成")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 接收推送消息
|
||||
|
||||
实现 `PushMessageDelegate` 处理推送消息:
|
||||
|
||||
```swift
|
||||
import XuqmPush
|
||||
|
||||
class MyPushHandler: NSObject, PushMessageDelegate {
|
||||
func pushSDK(_ sdk: PushSDK, didReceiveMessage message: PushMessage) {
|
||||
// 应用在前台时收到通知
|
||||
print("收到推送: \(message.title ?? "") - \(message.body ?? "")")
|
||||
print("Payload: \(message.payload)")
|
||||
}
|
||||
|
||||
func pushSDK(_ sdk: PushSDK, didTapNotification message: PushMessage) {
|
||||
// 用户点击通知栏消息
|
||||
print("用户点击了通知: \(message.title ?? "")")
|
||||
// 可在此跳转至对应会话页面
|
||||
}
|
||||
}
|
||||
|
||||
// 设置代理
|
||||
PushSDK.shared.delegate = MyPushHandler()
|
||||
```
|
||||
|
||||
`PushSDK` 内部已实现 `UNUserNotificationCenterDelegate`,会自动处理前台展示和点击事件。业务层只需设置 `delegate` 即可接收回调。
|
||||
|
||||
---
|
||||
|
||||
## 6. 多模块统一登录
|
||||
|
||||
Push 模块与 IM、Update 模块共享同一套登录态:
|
||||
|
||||
```swift
|
||||
// 登录成功后自动触发
|
||||
try await XuqmSDK.shared.login(userId: "user_001", userSig: userSig)
|
||||
// ↓ 自动触发
|
||||
// · PushSDK 注册 APNs Token 并上报
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 登出时注销 Token
|
||||
|
||||
```swift
|
||||
try await PushSDK.shared.unregisterToken(userId: "user_001")
|
||||
```
|
||||
94
docs-site/docs/ios/setup.md
普通文件
94
docs-site/docs/ios/setup.md
普通文件
@ -0,0 +1,94 @@
|
||||
# iOS 安装配置
|
||||
|
||||
**版本**:0.1.0 · **最低 iOS 版本**:iOS 16 · **语言**:Swift 5.9+
|
||||
|
||||
---
|
||||
|
||||
## Swift Package Manager(推荐)
|
||||
|
||||
在 Xcode → File → Add Package Dependencies 中输入:
|
||||
|
||||
```
|
||||
https://github.com/xuqm/XuqmGroup-iOSSDK
|
||||
```
|
||||
|
||||
或在 `Package.swift` 中添加:
|
||||
|
||||
```swift
|
||||
// swift-tools-version: 5.9
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MyApp",
|
||||
platforms: [.iOS(.v16)],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/xuqm/XuqmGroup-iOSSDK", from: "0.1.0")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "MyApp",
|
||||
dependencies: [
|
||||
.product(name: "XuqmSDK", package: "XuqmGroup-iOSSDK")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
> 目前 iOS SDK 以单模块 `XuqmSDK` 发布,内部包含 Core、IM、Push、Update 子模块。
|
||||
|
||||
---
|
||||
|
||||
## 最低 iOS 版本
|
||||
|
||||
- iOS 16.0+
|
||||
- macOS 13.0+(如需 Mac Catalyst)
|
||||
- Xcode 16.0+
|
||||
|
||||
---
|
||||
|
||||
## 权限声明
|
||||
|
||||
根据业务需要,在 `Info.plist` 中添加以下权限:
|
||||
|
||||
### Camera(拍照/视频通话)
|
||||
|
||||
```xml
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要访问相机以发送图片或进行视频通话</string>
|
||||
```
|
||||
|
||||
### Photo Library(发送图片/视频)
|
||||
|
||||
```xml
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要访问相册以发送图片和视频</string>
|
||||
```
|
||||
|
||||
### Microphone(语音消息/语音通话)
|
||||
|
||||
```xml
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>需要访问麦克风以发送语音消息或进行通话</string>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 初始化
|
||||
|
||||
在 `AppDelegate` 或 `SceneDelegate` 中:
|
||||
|
||||
```swift
|
||||
import XuqmSDK
|
||||
|
||||
let config = SDKConfig(appKey: "your_app_key", appSecret: "your_app_secret")
|
||||
XuqmSDK.shared.initialize(config: config)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
- [iOS IM 接入 →](./im)
|
||||
- [iOS Push 接入 →](./push)
|
||||
- [iOS 版本更新 →](./update)
|
||||
93
docs-site/docs/ios/update.md
普通文件
93
docs-site/docs/ios/update.md
普通文件
@ -0,0 +1,93 @@
|
||||
# iOS 版本更新接入指南
|
||||
|
||||
**模块**:`XuqmUpdate` · **功能**:App 版本检查、App Store 跳转
|
||||
|
||||
---
|
||||
|
||||
## 1. 添加依赖
|
||||
|
||||
在 `Package.swift` 中按需引入:
|
||||
|
||||
```swift
|
||||
.target(
|
||||
name: "MyApp",
|
||||
dependencies: [
|
||||
.product(name: "XuqmUpdate", package: "XuqmGroup-iOSSDK"),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 检查更新
|
||||
|
||||
```swift
|
||||
import XuqmUpdate
|
||||
|
||||
let appInfo = try await UpdateSDK.shared.checkAppUpdate(currentVersionCode: 1)
|
||||
if let info = appInfo, info.needsUpdate {
|
||||
print("发现新版本: \(info.versionName ?? "")")
|
||||
print("更新日志: \(info.changeLog ?? "")")
|
||||
|
||||
if info.forceUpdate == true {
|
||||
// 强制更新
|
||||
showForceUpdateAlert(info)
|
||||
} else {
|
||||
// 可选更新
|
||||
showOptionalUpdateAlert(info)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 跳转 App Store
|
||||
|
||||
```swift
|
||||
if let url = appInfo?.appStoreUrl {
|
||||
UpdateSDK.shared.openAppStore(url: url)
|
||||
}
|
||||
```
|
||||
|
||||
`openAppStore` 会调用 `UIApplication.shared.open()` 打开 App Store 页面,引导用户前往商店更新。
|
||||
|
||||
---
|
||||
|
||||
## 4. 强制更新处理
|
||||
|
||||
当服务端返回 `forceUpdate = true` 时,建议业务层:
|
||||
|
||||
1. 弹出 `UIAlertController`,设置 `isUserInteractionEnabled = false` 禁止关闭
|
||||
2. 只允许用户点击「前往更新」
|
||||
3. 调用 `UpdateSDK.shared.openAppStore(url:)` 跳转 App Store
|
||||
|
||||
```swift
|
||||
func showForceUpdateAlert(_ info: AppUpdateInfo) {
|
||||
let alert = UIAlertController(
|
||||
title: "发现重要更新",
|
||||
message: "当前版本已不可用,请升级至 \(info.versionName ?? "最新版")",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "前往更新", style: .default) { _ in
|
||||
if let url = info.appStoreUrl ?? info.downloadUrl {
|
||||
UpdateSDK.shared.openAppStore(url: url)
|
||||
}
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 多模块统一登录
|
||||
|
||||
Update 模块与 IM、Push 模块共享同一套登录态:
|
||||
|
||||
```swift
|
||||
// 初始化
|
||||
XuqmSDK.shared.initialize(config: config)
|
||||
|
||||
// 登录(业务登录成功后调用一次)
|
||||
try await XuqmSDK.shared.login(userId: "user_001", userSig: userSig)
|
||||
// UpdateSDK 在 checkAppUpdate 时自动携带 appId,无需额外登录操作
|
||||
```
|
||||
206
docs-site/docs/miniprogram/index.md
普通文件
206
docs-site/docs/miniprogram/index.md
普通文件
@ -0,0 +1,206 @@
|
||||
# 微信小程序 SDK 概览
|
||||
|
||||
**包名**:`@xuqm/miniprogram-sdk` · **版本**:0.1.0
|
||||
|
||||
---
|
||||
|
||||
## npm 安装
|
||||
|
||||
```bash
|
||||
npm install @xuqm/miniprogram-sdk
|
||||
```
|
||||
|
||||
或在微信开发者工具中:
|
||||
|
||||
1. 打开「工具」→「构建 npm」
|
||||
2. 在小程序 `package.json` 中添加上述依赖
|
||||
|
||||
---
|
||||
|
||||
## 初始化
|
||||
|
||||
```ts
|
||||
import { XuqmMiniProgramSDK } from '@xuqm/miniprogram-sdk'
|
||||
|
||||
const sdk = new XuqmMiniProgramSDK()
|
||||
|
||||
sdk.init({
|
||||
appKey: 'your_app_key',
|
||||
appSecret: 'your_app_secret',
|
||||
debug: true, // 可选
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IM 接入
|
||||
|
||||
### 登录
|
||||
|
||||
```ts
|
||||
// 使用 UserSig 登录
|
||||
await sdk.login('user_001', 'your_user_sig_jwt')
|
||||
|
||||
// 或使用演示账号快速登录(仅测试)
|
||||
const token = await sdk.loginWithDemo('user_001', '123456')
|
||||
```
|
||||
|
||||
### 监听消息
|
||||
|
||||
```ts
|
||||
sdk.on('connected', () => {
|
||||
console.log('IM 已连接')
|
||||
})
|
||||
|
||||
sdk.on('message', (msg) => {
|
||||
console.log('收到消息:', msg.msgType, msg.content)
|
||||
})
|
||||
|
||||
sdk.on('read', (msg) => {
|
||||
console.log('已读回执:', msg.id)
|
||||
})
|
||||
|
||||
sdk.on('revoke', (data) => {
|
||||
console.log('消息被撤回:', data.msgId)
|
||||
})
|
||||
|
||||
sdk.on('disconnected', (reason) => {
|
||||
console.log('断开连接:', reason)
|
||||
})
|
||||
|
||||
sdk.on('error', (error) => {
|
||||
console.error('IM 错误:', error)
|
||||
})
|
||||
```
|
||||
|
||||
### 发送消息
|
||||
|
||||
```ts
|
||||
const msg = await sdk.send({
|
||||
toId: 'user_002',
|
||||
chatType: 'SINGLE',
|
||||
msgType: 'TEXT',
|
||||
content: 'Hello!',
|
||||
})
|
||||
```
|
||||
|
||||
### 发送文本消息(快捷方法)
|
||||
|
||||
```ts
|
||||
await sdk.sendTextMessage('user_002', 'SINGLE', 'Hello!')
|
||||
```
|
||||
|
||||
### 历史消息
|
||||
|
||||
```ts
|
||||
const history = await sdk.fetchHistory('user_002')
|
||||
const groupHistory = await sdk.fetchGroupHistory('group_xxx')
|
||||
```
|
||||
|
||||
### 会话列表
|
||||
|
||||
```ts
|
||||
const conversations = await sdk.listConversations()
|
||||
await sdk.markRead('user_002')
|
||||
await sdk.setConversationPinned('user_002', 'SINGLE', true)
|
||||
await sdk.setConversationMuted('user_002', 'SINGLE', true)
|
||||
```
|
||||
|
||||
### 群聊
|
||||
|
||||
```ts
|
||||
const groups = await sdk.listGroups()
|
||||
const group = await sdk.getGroupInfo('group_xxx')
|
||||
const members = await sdk.listGroupMembers('group_xxx')
|
||||
```
|
||||
|
||||
### 好友管理
|
||||
|
||||
```ts
|
||||
const friends = await sdk.listFriends()
|
||||
await sdk.addFriend('user_002')
|
||||
await sdk.removeFriend('user_002')
|
||||
```
|
||||
|
||||
### 离线消息同步
|
||||
|
||||
```ts
|
||||
const count = await sdk.offlineMessageCount()
|
||||
const messages = await sdk.syncOfflineMessages()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Push 接入(小程序通知)
|
||||
|
||||
微信小程序使用**订阅消息**和**服务通知**实现 Push,由服务端调用微信 API 下发。
|
||||
|
||||
XuqmGroup 小程序 SDK 本身不直接处理 Push Token(小程序无设备 Token 概念),业务方需要:
|
||||
|
||||
1. 用户订阅消息模板
|
||||
2. 业务服务端调用 XuqmGroup Server SDK 的 `send_push` 接口
|
||||
3. 或业务服务端直接调用微信服务端 API 下发订阅消息
|
||||
|
||||
```python
|
||||
# Python Server SDK 示例
|
||||
from xuqm_im_server_sdk import XuqmImServerSdk
|
||||
|
||||
sdk = XuqmImServerSdk(config)
|
||||
sdk.send_push(
|
||||
user_id="user_001",
|
||||
title="新消息",
|
||||
body="你有一条未读消息",
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update 接入(小程序自带更新机制)
|
||||
|
||||
微信小程序的更新由微信客户端自动管理,开发者可通过微信 API 检查更新:
|
||||
|
||||
```ts
|
||||
const updateManager = wx.getUpdateManager()
|
||||
|
||||
updateManager.onCheckForUpdate((res) => {
|
||||
console.log('是否有新版本:', res.hasUpdate)
|
||||
})
|
||||
|
||||
updateManager.onUpdateReady(() => {
|
||||
wx.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本已准备好,是否重启应用?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
> XuqmGroup 小程序 SDK 不额外提供 Update 模块,直接使用微信小程序原生更新能力即可。
|
||||
|
||||
---
|
||||
|
||||
## 消息类型
|
||||
|
||||
| MsgType | 说明 |
|
||||
|---------|------|
|
||||
| `TEXT` | 纯文本 |
|
||||
| `IMAGE` | 图片 |
|
||||
| `VIDEO` | 视频 |
|
||||
| `AUDIO` | 语音 |
|
||||
| `FILE` | 文件 |
|
||||
| `LOCATION` | 位置 |
|
||||
| `CUSTOM` | 自定义 |
|
||||
| `NOTIFY` | 系统通知 |
|
||||
| `RICH_TEXT` | 富文本 |
|
||||
| `CALL_AUDIO` | 语音通话信令 |
|
||||
| `CALL_VIDEO` | 视频通话信令 |
|
||||
| `QUOTE` | 引用 |
|
||||
| `MERGE` | 合并转发 |
|
||||
| `FORWARD` | 转发 |
|
||||
| `REVOKED` | 撤回 |
|
||||
|
||||
[→ Server API 文档 →](/server/api)
|
||||
109
docs-site/docs/rn/group.md
普通文件
109
docs-site/docs/rn/group.md
普通文件
@ -0,0 +1,109 @@
|
||||
# React Native 群聊
|
||||
|
||||
基于 `@xuqm/rn-im` 模块实现群组相关功能。
|
||||
|
||||
---
|
||||
|
||||
## 创建群聊
|
||||
|
||||
```ts
|
||||
import { ImSDK } from '@xuqm/rn-im'
|
||||
|
||||
const group = await ImSDK.createGroup('项目讨论', ['user_002', 'user_003'])
|
||||
// group.id — 群 ID
|
||||
// group.name — 群名称
|
||||
// group.creatorId — 创建者
|
||||
```
|
||||
|
||||
> `createGroup` 第二个参数为初始成员列表,创建者自动加入。
|
||||
|
||||
---
|
||||
|
||||
## 邀请成员
|
||||
|
||||
```ts
|
||||
// 添加单个成员
|
||||
await ImSDK.addGroupMember('group_xxx', 'user_004')
|
||||
|
||||
// 批量添加成员
|
||||
await ImSDK.batchAddGroupMembers('group_xxx', ['user_004', 'user_005'])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 发送群消息
|
||||
|
||||
```ts
|
||||
const msg = await ImSDK.sendMessage(
|
||||
'group_xxx', // toId
|
||||
'GROUP', // chatType
|
||||
'TEXT', // msgType
|
||||
'大家好!' // content
|
||||
)
|
||||
```
|
||||
|
||||
发送多媒体群消息:
|
||||
|
||||
```ts
|
||||
// 图片
|
||||
await ImSDK.sendImageMessage('group_xxx', 'GROUP', '/path/to/image.jpg', 800, 600)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群成员管理
|
||||
|
||||
```ts
|
||||
// 移除成员
|
||||
await ImSDK.removeGroupMember('group_xxx', 'user_004')
|
||||
|
||||
// 批量移除
|
||||
await ImSDK.batchRemoveGroupMembers('group_xxx', ['user_004', 'user_005'])
|
||||
|
||||
// 退出群聊
|
||||
await ImSDK.leaveGroup('group_xxx')
|
||||
|
||||
// 设置管理员角色(示例)
|
||||
await ImSDK.setGroupRole('group_xxx', 'user_004', 'ADMIN')
|
||||
|
||||
// 禁言成员(示例)
|
||||
await ImSDK.muteGroupMember('group_xxx', 'user_004', 60)
|
||||
|
||||
// 转让群主(示例)
|
||||
await ImSDK.transferGroupOwner('group_xxx', 'user_002')
|
||||
|
||||
// 解散群聊(示例)
|
||||
await ImSDK.dismissGroup('group_xxx')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群信息查询
|
||||
|
||||
```ts
|
||||
// 群列表(仅返回当前用户所在的群)
|
||||
const groups = await ImSDK.listGroups()
|
||||
|
||||
// 群详情
|
||||
const group = await ImSDK.getGroupInfo('group_xxx')
|
||||
|
||||
// 群成员
|
||||
const members = await ImSDK.listGroupMembers('group_xxx')
|
||||
|
||||
// 群历史消息
|
||||
const history = await ImSDK.fetchGroupHistory('group_xxx', page, size)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群类型
|
||||
|
||||
创建群时可指定 `groupType`:
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| `WORK` | 工作群(默认)|
|
||||
| `PUBLIC` | 公开群 |
|
||||
| `PRIVATE` | 私有群 |
|
||||
|
||||
[→ 返回 RN IM 接入文档](./im)
|
||||
201
docs-site/docs/rn/im.md
普通文件
201
docs-site/docs/rn/im.md
普通文件
@ -0,0 +1,201 @@
|
||||
# React Native IM 接入
|
||||
|
||||
基于 `@xuqm/rn-im` 模块实现即时通讯功能,使用 WatermelonDB 进行本地消息存储。
|
||||
|
||||
---
|
||||
|
||||
## 初始化与登录
|
||||
|
||||
### 初始化
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/rn-common'
|
||||
|
||||
await XuqmSDK.initialize({
|
||||
appKey: 'your_app_key',
|
||||
logLevel: __DEV__ ? 'debug' : 'warn',
|
||||
})
|
||||
```
|
||||
|
||||
### 登录
|
||||
|
||||
```ts
|
||||
import { XuqmSDK } from '@xuqm/rn-common'
|
||||
|
||||
await XuqmSDK.login({
|
||||
userId: 'user_001',
|
||||
userSig: 'your_user_sig_jwt',
|
||||
})
|
||||
```
|
||||
|
||||
> 如果集成了 `rn-im`,`XuqmSDK.login` 会自动触发 `ImSDK` 连接 WebSocket,业务侧无需单独调用 `ImSDK.login`。
|
||||
|
||||
---
|
||||
|
||||
## 消息收发
|
||||
|
||||
### 监听实时消息
|
||||
|
||||
```ts
|
||||
import { ImSDK } from '@xuqm/rn-im'
|
||||
|
||||
ImSDK.addEventListener('message', (msg) => {
|
||||
console.log('收到消息:', msg.msgType, msg.content)
|
||||
})
|
||||
|
||||
ImSDK.addEventListener('read', (msg) => {
|
||||
console.log('已读回执:', msg.id)
|
||||
})
|
||||
|
||||
ImSDK.addEventListener('revoke', (data) => {
|
||||
console.log('消息被撤回:', data.msgId)
|
||||
})
|
||||
```
|
||||
|
||||
### 发送文本消息
|
||||
|
||||
```ts
|
||||
const msg = await ImSDK.sendMessage(
|
||||
'user_002', // toId
|
||||
'SINGLE', // chatType: 'SINGLE' | 'GROUP'
|
||||
'TEXT', // msgType
|
||||
'Hello!' // content
|
||||
)
|
||||
```
|
||||
|
||||
### 发送图片
|
||||
|
||||
```ts
|
||||
const msg = await ImSDK.sendImageMessage(
|
||||
'user_002',
|
||||
'SINGLE',
|
||||
'/path/to/image.jpg', // 本地 URI
|
||||
800, // 宽
|
||||
600 // 高
|
||||
)
|
||||
```
|
||||
|
||||
### 获取历史消息
|
||||
|
||||
```ts
|
||||
// 单聊历史
|
||||
const history = await ImSDK.fetchHistory('user_002', page, size)
|
||||
|
||||
// 群历史
|
||||
const groupHistory = await ImSDK.fetchGroupHistory('group_xxx', page, size)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WatermelonDB 本地存储
|
||||
|
||||
`rn-im` 使用 WatermelonDB(SQLite)存储本地消息,按 `appKey + userId` 自动隔离。
|
||||
|
||||
```ts
|
||||
import { ImDatabase } from '@xuqm/rn-im'
|
||||
|
||||
// 消息搜索(本地)
|
||||
const params: MessageSearchParams = {
|
||||
keyword: '会议',
|
||||
toId: 'user_002',
|
||||
chatType: 'SINGLE',
|
||||
msgTypes: ['TEXT'],
|
||||
limit: 20,
|
||||
}
|
||||
const results = await ImSDK.searchMessages(params)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群聊
|
||||
|
||||
```ts
|
||||
// 创建群
|
||||
const group = await ImSDK.createGroup('项目讨论', ['user_002', 'user_003'])
|
||||
|
||||
// 群列表
|
||||
const groups = await ImSDK.listGroups()
|
||||
|
||||
// 添加成员
|
||||
await ImSDK.addGroupMember('group_xxx', 'user_004')
|
||||
|
||||
// 移除成员
|
||||
await ImSDK.removeGroupMember('group_xxx', 'user_004')
|
||||
|
||||
// 退出群聊
|
||||
await ImSDK.leaveGroup('group_xxx')
|
||||
```
|
||||
|
||||
详见 [RN 群聊文档 →](./group)
|
||||
|
||||
---
|
||||
|
||||
## 会话列表
|
||||
|
||||
```ts
|
||||
// 订阅会话变化
|
||||
const unsub = ImSDK.subscribeConversations((conversations) => {
|
||||
console.log(conversations)
|
||||
})
|
||||
|
||||
// 置顶
|
||||
await ImSDK.setConversationPinned('user_002', 'SINGLE', true)
|
||||
|
||||
// 免打扰
|
||||
await ImSDK.setConversationMuted('group_xxx', 'GROUP', true)
|
||||
|
||||
// 标记已读
|
||||
await ImSDK.markRead('user_002')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 离线消息同步
|
||||
|
||||
```ts
|
||||
// 查询离线消息数量(示例)
|
||||
const count = await ImSDK.offlineMessageCount()
|
||||
|
||||
// 同步离线消息(示例)
|
||||
const messages = await ImSDK.syncOfflineMessages()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 断开连接
|
||||
|
||||
```ts
|
||||
ImSDK.disconnect()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 类型参考
|
||||
|
||||
```ts
|
||||
interface ImMessage {
|
||||
id: string
|
||||
fromId: string
|
||||
toId: string
|
||||
chatType: 'SINGLE' | 'GROUP'
|
||||
msgType: 'TEXT' | 'IMAGE' | 'AUDIO' | 'VIDEO' | 'FILE' |
|
||||
'LOCATION' | 'NOTIFY' | 'CUSTOM' | 'RICH_TEXT' |
|
||||
'CALL_AUDIO' | 'CALL_VIDEO' | 'FORWARD' | 'REVOKED'
|
||||
content: string
|
||||
status: 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED' | 'REVOKED'
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
interface ConversationData {
|
||||
targetId: string
|
||||
chatType: 'SINGLE' | 'GROUP'
|
||||
lastMsgContent: string
|
||||
lastMsgType: string
|
||||
lastMsgTime: number
|
||||
unreadCount: number
|
||||
isMuted: boolean
|
||||
isPinned: boolean
|
||||
}
|
||||
```
|
||||
|
||||
[→ 完整 API Reference](./api)
|
||||
108
docs-site/docs/rn/setup.md
普通文件
108
docs-site/docs/rn/setup.md
普通文件
@ -0,0 +1,108 @@
|
||||
# React Native 安装配置
|
||||
|
||||
**包名**:`@xuqm/rn-sdk` · **版本**:0.2.x · **RN 版本**:≥ 0.76.0
|
||||
|
||||
> `rn-sdk` 作为内部基础包存在,业务方正常接入时使用 `@xuqm/rn-common` 和各业务模块即可。
|
||||
|
||||
---
|
||||
|
||||
## npm / yarn 安装
|
||||
|
||||
在项目根目录创建 `.npmrc`:
|
||||
|
||||
```
|
||||
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm-hosted/
|
||||
```
|
||||
|
||||
只使用基础能力时,直接安装 `rn-common`:
|
||||
|
||||
```bash
|
||||
yarn add @xuqm/rn-common
|
||||
```
|
||||
|
||||
按需安装模块时,`rn-im` / `rn-push` / `rn-update` 都会自动带上 `rn-common` 和 `rn-sdk`:
|
||||
|
||||
```bash
|
||||
yarn add @xuqm/rn-common @xuqm/rn-im
|
||||
# 或全量安装
|
||||
yarn add @xuqm/rn-common @xuqm/rn-im @xuqm/rn-push @xuqm/rn-update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自动 / 手动链接
|
||||
|
||||
React Native 0.60+ 支持**自动链接**,安装后执行:
|
||||
|
||||
```bash
|
||||
cd ios && pod install
|
||||
```
|
||||
|
||||
> 若使用 Expo,需先执行 `expo prebuild` 生成原生项目后再执行 `pod install`。
|
||||
|
||||
---
|
||||
|
||||
## iOS 配置
|
||||
|
||||
### Pod 安装
|
||||
|
||||
```bash
|
||||
cd ios
|
||||
pod install
|
||||
```
|
||||
|
||||
### 权限声明
|
||||
|
||||
在 `Info.plist` 中添加:
|
||||
|
||||
```xml
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要访问相机</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要访问相册</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>需要访问麦克风</string>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Android 配置
|
||||
|
||||
### Gradle 仓库
|
||||
|
||||
在 `android/build.gradle` 中确保包含:
|
||||
|
||||
```gradle
|
||||
allprojects {
|
||||
repositories {
|
||||
maven { url "https://nexus.xuqinmin.com/repository/android/" }
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 最低版本
|
||||
|
||||
- `minSdkVersion = 24`
|
||||
- `compileSdkVersion = 34`
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
@xuqm/rn-sdk(meta-package,不建议业务方直接引用)
|
||||
├── @xuqm/rn-common ← 初始化、网络、设备信息
|
||||
├── @xuqm/rn-im ← IM 模块(依赖 WatermelonDB)
|
||||
├── @xuqm/rn-push ← Push 模块
|
||||
└── @xuqm/rn-update ← 更新模块
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
- [RN IM 接入 →](./im)
|
||||
- [RN 群聊 →](./group)
|
||||
- [RN 版本更新 →](./update)
|
||||
154
docs-site/docs/rn/update.md
普通文件
154
docs-site/docs/rn/update.md
普通文件
@ -0,0 +1,154 @@
|
||||
# React Native 版本更新接入指南
|
||||
|
||||
**包名**:`@xuqm/rn-update` · **功能**:App 版本检查、RN Bundle 热更新、打开商店
|
||||
|
||||
---
|
||||
|
||||
## 1. 安装
|
||||
|
||||
```bash
|
||||
yarn add @xuqm/rn-update
|
||||
```
|
||||
|
||||
`rn-update` 会自动依赖 `@xuqm/rn-common` 和 `@xuqm/rn-sdk`。
|
||||
|
||||
---
|
||||
|
||||
## 2. App 版本检查
|
||||
|
||||
```ts
|
||||
import { UpdateSDK } from '@xuqm/rn-update'
|
||||
|
||||
const appUpdate = await UpdateSDK.checkAppUpdate()
|
||||
if (appUpdate.needsUpdate) {
|
||||
console.log('新版本:', appUpdate.versionName)
|
||||
console.log('更新日志:', appUpdate.changeLog)
|
||||
console.log('下载地址:', appUpdate.downloadUrl)
|
||||
|
||||
if (appUpdate.forceUpdate) {
|
||||
// 强制更新
|
||||
showForceUpdateModal(appUpdate)
|
||||
} else {
|
||||
// 可选更新
|
||||
showOptionalUpdateModal(appUpdate)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
App 版本号自动从原生模块读取,无需手动传入。开发/模拟器环境可调用 `_devSetAppVersion` 覆盖:
|
||||
|
||||
```ts
|
||||
// 仅限开发环境使用
|
||||
UpdateSDK._devSetAppVersion(100, '1.0.0')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 打开商店
|
||||
|
||||
```ts
|
||||
await UpdateSDK.openStore(appUpdate.appStoreUrl, appUpdate.marketUrl)
|
||||
```
|
||||
|
||||
- iOS:打开 `appStoreUrl`
|
||||
- Android:打开 `marketUrl`
|
||||
|
||||
---
|
||||
|
||||
## 4. RN Bundle 热更新
|
||||
|
||||
### 4.1 注册插件
|
||||
|
||||
在插件 Bundle 的入口文件顶部注册插件元数据:
|
||||
|
||||
```ts
|
||||
// plugin/index.ts
|
||||
import { UpdateSDK } from '@xuqm/rn-update'
|
||||
import meta from './plugin.json'
|
||||
|
||||
UpdateSDK.registerPlugin(meta)
|
||||
```
|
||||
|
||||
`plugin.json` 示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"moduleId": "home",
|
||||
"version": "1.2.3"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 检查热更新
|
||||
|
||||
```ts
|
||||
const rnUpdate = await UpdateSDK.checkRnUpdate('home')
|
||||
if (rnUpdate.needsUpdate) {
|
||||
console.log('最新版本:', rnUpdate.latestVersion)
|
||||
console.log('下载地址:', rnUpdate.downloadUrl)
|
||||
console.log('更新说明:', rnUpdate.note)
|
||||
console.log('最低依赖版本:', rnUpdate.minCommonVersion)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 下载并缓存 Bundle
|
||||
|
||||
```ts
|
||||
// 下载 Bundle 源码
|
||||
const source = await UpdateSDK.downloadRnBundle(rnUpdate.downloadUrl)
|
||||
|
||||
// 缓存到本地
|
||||
await UpdateSDK.cacheRnBundle('home', rnUpdate.latestVersion, rnUpdate.md5, source)
|
||||
|
||||
// 读取已缓存的 Bundle
|
||||
const cached = await UpdateSDK.getCachedRnBundle('home')
|
||||
if (cached) {
|
||||
console.log('缓存版本:', cached.version)
|
||||
console.log('缓存时间:', cached.downloadedAt)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 获取已注册插件版本
|
||||
|
||||
```ts
|
||||
const version = UpdateSDK.getRegisteredPluginVersion('home')
|
||||
console.log('当前运行版本:', version)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 强制更新处理
|
||||
|
||||
当 `appUpdate.forceUpdate` 为 `true` 时,建议业务层:
|
||||
|
||||
1. 弹出不可关闭的 Modal
|
||||
2. 只允许用户点击「立即更新」
|
||||
3. 调用 `UpdateSDK.openStore()` 跳转商店
|
||||
|
||||
```ts
|
||||
function showForceUpdateModal(update: AppUpdateInfo) {
|
||||
// 使用 RN Modal 组件,设置不可取消
|
||||
Alert.alert(
|
||||
'发现重要更新',
|
||||
`当前版本已不可用,请升级至 ${update.versionName}`,
|
||||
[
|
||||
{
|
||||
text: '立即更新',
|
||||
onPress: () => UpdateSDK.openStore(update.appStoreUrl, update.marketUrl),
|
||||
},
|
||||
],
|
||||
{ cancelable: false }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 多模块统一登录
|
||||
|
||||
Update 模块与 IM、Push 模块共享同一套登录态:
|
||||
|
||||
```ts
|
||||
await XuqmSDK.initialize({ appKey: 'your_app_key' })
|
||||
await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig_jwt' })
|
||||
// UpdateSDK 在 checkAppUpdate 时自动携带 appId,无需额外登录操作
|
||||
```
|
||||
105
docs-site/docs/server/errors.md
普通文件
105
docs-site/docs/server/errors.md
普通文件
@ -0,0 +1,105 @@
|
||||
# 错误码文档
|
||||
|
||||
---
|
||||
|
||||
## HTTP 错误码
|
||||
|
||||
| 状态码 | 说明 | 常见场景 |
|
||||
|--------|------|----------|
|
||||
| 200 | 成功 | 请求正常处理 |
|
||||
| 400 | 请求参数错误 | 缺少必填字段、参数类型不匹配 |
|
||||
| 401 | 未授权 | Token 无效、过期、未携带 |
|
||||
| 403 | 禁止访问 | 权限不足、黑名单限制 |
|
||||
| 404 | 资源不存在 | 用户不存在、群不存在、消息不存在 |
|
||||
| 500 | 服务端内部错误 | 异常未捕获 |
|
||||
|
||||
所有接口统一返回格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"status": "OK",
|
||||
"data": { ... },
|
||||
"message": ""
|
||||
}
|
||||
```
|
||||
|
||||
业务错误时:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"status": "BAD_REQUEST",
|
||||
"data": null,
|
||||
"message": "参数错误:userId 不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WebSocket 错误码
|
||||
|
||||
| 关闭码 | 说明 | 处理建议 |
|
||||
|--------|------|----------|
|
||||
| 1000 | 正常关闭 | 无需重连(如用户主动登出)|
|
||||
| 1001 | 服务端关闭 | 尝试重连 |
|
||||
| 1006 | 异常断开 | 自动重连 |
|
||||
| 1008 | 策略违反(如 Token 无效)| 重新获取 Token 后重连 |
|
||||
| 1011 | 服务端异常 | 指数退避重连 |
|
||||
|
||||
---
|
||||
|
||||
## SDK 错误码
|
||||
|
||||
### Android
|
||||
|
||||
| 错误 | 说明 |
|
||||
|------|------|
|
||||
| `XuqmSDK not initialized. Call XuqmSDK.initialize() first.` | 未调用初始化 |
|
||||
| `WebSocket not connected` | 发送消息时未连接 |
|
||||
| `edit message failed` | 编辑消息接口返回空 |
|
||||
| `revoke message failed` | 撤回消息接口返回空 |
|
||||
|
||||
### iOS
|
||||
|
||||
| 错误 | 说明 |
|
||||
|------|------|
|
||||
| `XuqmSDK not initialized. Call XuqmSDK.shared.initialize() first.` | 未调用初始化 |
|
||||
|
||||
### Web / Vue3 / 小程序
|
||||
|
||||
| 错误 | 说明 |
|
||||
|------|------|
|
||||
| `XuqmSDK not initialized. Call init() first.` | 未调用初始化 |
|
||||
| `WebSocket not connected` | WebSocket 未连接时发送消息 |
|
||||
| `IM token is empty` | 未登录或 Token 已清空 |
|
||||
|
||||
---
|
||||
|
||||
## 常见错误排查
|
||||
|
||||
### Q: 登录后 WebSocket 无法连接
|
||||
|
||||
- 检查 `userSig` 是否有效(未过期、签名正确)
|
||||
- 检查网络是否允许 WebSocket(防火墙、代理)
|
||||
- 检查 WebSocket 地址是否正确
|
||||
|
||||
### Q: 发送消息返回 FAILED
|
||||
|
||||
- 检查 WebSocket 连接状态
|
||||
- 检查 `toId` 是否有效
|
||||
- 检查 `appKey` 是否正确配置
|
||||
|
||||
### Q: 收不到消息
|
||||
|
||||
- 确认已订阅 `/user/queue/messages`
|
||||
- 确认发送方和接收方使用同一 `appKey`
|
||||
- 检查是否被对方拉黑
|
||||
|
||||
### Q: Token 401
|
||||
|
||||
- `userSig` 由服务端用 `appSecret` 签发,确保签名算法一致
|
||||
- 检查 Token 是否在有效期内
|
||||
- 检查 HTTP Header 中 `Authorization: Bearer {token}` 格式
|
||||
|
||||
[→ Server API 文档 →](./api)
|
||||
192
docs-site/docs/server/websocket.md
普通文件
192
docs-site/docs/server/websocket.md
普通文件
@ -0,0 +1,192 @@
|
||||
# WebSocket 协议文档
|
||||
|
||||
XuqmGroup IM 使用 **STOMP over WebSocket** 协议进行实时消息通信。
|
||||
|
||||
---
|
||||
|
||||
## 连接地址
|
||||
|
||||
| 环境 | 地址 |
|
||||
|------|------|
|
||||
| 演示环境 | `wss://dev.xuqinmin.com/ws/im` |
|
||||
| 生产环境 | 由租户平台分配 |
|
||||
|
||||
连接时需携带 `token` 参数:
|
||||
|
||||
```
|
||||
wss://dev.xuqinmin.com/ws/im?token={userSig}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 心跳机制
|
||||
|
||||
客户端在 STOMP CONNECT 帧中声明心跳:
|
||||
|
||||
```
|
||||
CONNECT
|
||||
accept-version:1.2
|
||||
heart-beat:0,0
|
||||
host:dev.xuqinmin.com
|
||||
Authorization:Bearer {token}
|
||||
|
||||
\x00
|
||||
```
|
||||
|
||||
> 当前服务端不强制心跳,客户端可依赖 WebSocket 原生 `onclose`/`onerror` 检测断线。
|
||||
|
||||
---
|
||||
|
||||
## 消息格式(STOMP)
|
||||
|
||||
### CONNECT 帧
|
||||
|
||||
客户端建立 WebSocket 连接后,发送 STOMP CONNECT:
|
||||
|
||||
```
|
||||
CONNECT
|
||||
accept-version:1.2
|
||||
heart-beat:0,0
|
||||
host:dev.xuqinmin.com
|
||||
Authorization:Bearer {token}
|
||||
|
||||
\x00
|
||||
```
|
||||
|
||||
### CONNECTED 帧
|
||||
|
||||
服务端鉴权通过后返回:
|
||||
|
||||
```
|
||||
CONNECTED
|
||||
version:1.2
|
||||
|
||||
\x00
|
||||
```
|
||||
|
||||
### SUBSCRIBE 帧
|
||||
|
||||
订阅个人消息队列(自动执行):
|
||||
|
||||
```
|
||||
SUBSCRIBE
|
||||
id:sub-1
|
||||
destination:/user/queue/messages
|
||||
|
||||
\x00
|
||||
```
|
||||
|
||||
订阅群消息:
|
||||
|
||||
```
|
||||
SUBSCRIBE
|
||||
id:sub-2
|
||||
destination:/topic/group/{groupId}
|
||||
|
||||
\x00
|
||||
```
|
||||
|
||||
### SEND 帧 — 发送消息
|
||||
|
||||
```
|
||||
SEND
|
||||
destination:/app/chat.send
|
||||
content-type:application/json
|
||||
|
||||
{"appId":"ak_demo_chat","messageId":"...","toId":"user_002","chatType":"SINGLE","msgType":"TEXT","content":"Hello"}
|
||||
\x00
|
||||
```
|
||||
|
||||
### SEND 帧 — 撤回消息
|
||||
|
||||
```
|
||||
SEND
|
||||
destination:/app/chat.revoke
|
||||
content-type:application/json
|
||||
|
||||
{"appId":"ak_demo_chat","messageId":"..."}
|
||||
\x00
|
||||
```
|
||||
|
||||
### SEND 帧 — 同步离线消息
|
||||
|
||||
```
|
||||
SEND
|
||||
destination:/app/chat.sync
|
||||
content-type:application/json
|
||||
|
||||
{"appId":"ak_demo_chat"}
|
||||
\x00
|
||||
```
|
||||
|
||||
### MESSAGE 帧 — 接收消息
|
||||
|
||||
```
|
||||
MESSAGE
|
||||
destination:/user/queue/messages
|
||||
message-id:...
|
||||
|
||||
{"id":"...","fromId":"user_002","toId":"user_001","chatType":"SINGLE","msgType":"TEXT","content":"Hello","status":"SENT","createdAt":1715000000000}
|
||||
\x00
|
||||
```
|
||||
|
||||
### ERROR 帧
|
||||
|
||||
```
|
||||
ERROR
|
||||
message:Unauthorized
|
||||
|
||||
Unauthorized
|
||||
\x00
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 订阅路径
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `/user/queue/messages` | 个人消息队列(登录后自动订阅)|
|
||||
| `/topic/group/{groupId}` | 群消息频道 |
|
||||
|
||||
---
|
||||
|
||||
## 服务端配置
|
||||
|
||||
基于 Spring WebSocket + STOMP:
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker("/topic", "/queue");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
registry.setUserDestinationPrefix("/user");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws/im")
|
||||
.setAllowedOriginPatterns("*")
|
||||
.withSockJS();
|
||||
registry.addEndpoint("/ws/im")
|
||||
.setAllowedOriginPatterns("*");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自定义客户端接入
|
||||
|
||||
如果业务方不使用官方 SDK,可按以下流程自建客户端:
|
||||
|
||||
1. 通过 `wss://host/ws/im?token=xxx` 建立 WebSocket
|
||||
2. 发送 STOMP CONNECT 帧(携带 `Authorization: Bearer {token}`)
|
||||
3. 收到 CONNECTED 后,订阅 `/user/queue/messages`
|
||||
4. 发送消息时构造 STOMP SEND 帧,`destination: /app/chat.send`
|
||||
5. 收到服务端 MESSAGE 帧后解析 JSON body
|
||||
|
||||
[→ Server API 文档 →](./api)
|
||||
226
docs-site/docs/vue3/im.md
普通文件
226
docs-site/docs/vue3/im.md
普通文件
@ -0,0 +1,226 @@
|
||||
# Vue3 IM 接入
|
||||
|
||||
基于 `@xuqm/vue3-sdk` 实现即时通讯功能,支持原生 WebSocket + STOMP 协议。
|
||||
|
||||
---
|
||||
|
||||
## 初始化与登录
|
||||
|
||||
### 初始化
|
||||
|
||||
```ts
|
||||
import { init } from '@xuqm/vue3-sdk'
|
||||
|
||||
init({
|
||||
appKey: 'your_app_key',
|
||||
appSecret: 'your_app_secret',
|
||||
})
|
||||
```
|
||||
|
||||
### 登录
|
||||
|
||||
```ts
|
||||
import { login } from '@xuqm/vue3-sdk'
|
||||
|
||||
login('user_001', 'your_user_sig_jwt')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 消息收发
|
||||
|
||||
### 使用 ImClient
|
||||
|
||||
```ts
|
||||
import { ImClient } from '@xuqm/vue3-sdk'
|
||||
|
||||
const im = new ImClient()
|
||||
|
||||
im.on('connected', () => {
|
||||
console.log('IM 已连接')
|
||||
})
|
||||
|
||||
im.on('message', (msg) => {
|
||||
console.log('收到消息:', msg.msgType, msg.content)
|
||||
})
|
||||
|
||||
im.on('read', (msg) => {
|
||||
console.log('已读回执:', msg.id)
|
||||
})
|
||||
|
||||
im.on('revoke', (data) => {
|
||||
console.log('消息被撤回:', data.msgId, data.operatorId)
|
||||
})
|
||||
|
||||
im.on('disconnected', (code, reason) => {
|
||||
console.log('断开连接:', code, reason)
|
||||
})
|
||||
|
||||
im.on('error', (err) => {
|
||||
console.error('IM 错误:', err)
|
||||
})
|
||||
|
||||
im.connect()
|
||||
```
|
||||
|
||||
### 发送消息
|
||||
|
||||
```ts
|
||||
const outgoing = im.send({
|
||||
toId: 'user_002',
|
||||
chatType: 'SINGLE',
|
||||
msgType: 'TEXT',
|
||||
content: 'Hello!',
|
||||
})
|
||||
```
|
||||
|
||||
### 发送多媒体消息
|
||||
|
||||
```ts
|
||||
// 图片
|
||||
im.send({
|
||||
toId: 'user_002',
|
||||
chatType: 'SINGLE',
|
||||
msgType: 'IMAGE',
|
||||
content: JSON.stringify({ url: 'https://...', width: 800, height: 600 }),
|
||||
})
|
||||
|
||||
// 位置
|
||||
im.send({
|
||||
toId: 'user_002',
|
||||
chatType: 'SINGLE',
|
||||
msgType: 'LOCATION',
|
||||
content: JSON.stringify({ lat: 31.2304, lng: 121.4737, title: '上海' }),
|
||||
})
|
||||
```
|
||||
|
||||
### 撤回消息
|
||||
|
||||
```ts
|
||||
im.revoke('message_id')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用 Vue Composable(useIm)
|
||||
|
||||
```ts
|
||||
import { useIm } from '@xuqm/vue3-sdk'
|
||||
|
||||
const im = useIm()
|
||||
|
||||
// 连接
|
||||
im.connect()
|
||||
|
||||
// 发送消息
|
||||
im.send({ toId: 'user_002', chatType: 'SINGLE', msgType: 'TEXT', content: 'Hello' })
|
||||
|
||||
// 响应式状态
|
||||
console.log(im.connected.value) // boolean
|
||||
console.log(im.messages.value) // ImMessage[]
|
||||
console.log(im.conversations.value) // ConversationView[]
|
||||
console.log(im.error.value) // Event | null
|
||||
|
||||
// 历史消息
|
||||
const history = await im.loadHistory('user_002')
|
||||
|
||||
// 标记已读
|
||||
await im.setConversationRead('user_002')
|
||||
|
||||
// 置顶/免打扰
|
||||
await im.setConversationPinnedState('user_002', 'SINGLE', true)
|
||||
await im.setConversationMutedState('user_002', 'SINGLE', true)
|
||||
|
||||
// 删除会话
|
||||
await im.removeConversation('user_002', 'SINGLE')
|
||||
|
||||
// 断开
|
||||
im.disconnect()
|
||||
```
|
||||
|
||||
> `useIm` 在组件卸载时会自动调用 `disconnect()`。
|
||||
|
||||
---
|
||||
|
||||
## 会话列表
|
||||
|
||||
```ts
|
||||
import { listConversations } from '@xuqm/vue3-sdk'
|
||||
|
||||
const conversations = await listConversations()
|
||||
```
|
||||
|
||||
会话数据类型:
|
||||
|
||||
```ts
|
||||
interface ConversationView {
|
||||
targetId: string
|
||||
chatType: 'SINGLE' | 'GROUP'
|
||||
lastMsgContent?: string | null
|
||||
lastMsgType?: string | null
|
||||
lastMsgTime: number
|
||||
unreadCount: number
|
||||
isMuted: boolean
|
||||
isPinned: boolean
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 离线消息同步
|
||||
|
||||
```ts
|
||||
import { offlineMessageCount, syncOfflineMessages } from '@xuqm/vue3-sdk'
|
||||
|
||||
const count = await offlineMessageCount()
|
||||
const messages = await syncOfflineMessages()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群聊
|
||||
|
||||
```ts
|
||||
import { createGroup, listGroups, addGroupMember, removeGroupMember } from '@xuqm/vue3-sdk'
|
||||
|
||||
const group = await createGroup('项目讨论', ['user_002', 'user_003'])
|
||||
const groups = await listGroups()
|
||||
await addGroupMember('group_xxx', 'user_004')
|
||||
await removeGroupMember('group_xxx', 'user_004')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 好友管理
|
||||
|
||||
```ts
|
||||
import { listFriends, addFriend, removeFriend, sendFriendRequest } from '@xuqm/vue3-sdk'
|
||||
|
||||
const friends = await listFriends()
|
||||
await addFriend('user_002')
|
||||
await removeFriend('user_002')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 消息类型
|
||||
|
||||
| MsgType | 说明 |
|
||||
|---------|------|
|
||||
| `TEXT` | 纯文本 |
|
||||
| `IMAGE` | 图片 |
|
||||
| `VIDEO` | 视频 |
|
||||
| `AUDIO` | 语音 |
|
||||
| `FILE` | 文件 |
|
||||
| `LOCATION` | 位置 |
|
||||
| `CUSTOM` | 自定义 |
|
||||
| `NOTIFY` | 系统通知 |
|
||||
| `RICH_TEXT` | 富文本 |
|
||||
| `CALL_AUDIO` | 语音通话信令 |
|
||||
| `CALL_VIDEO` | 视频通话信令 |
|
||||
| `QUOTE` | 引用 |
|
||||
| `MERGE` | 合并转发 |
|
||||
| `FORWARD` | 转发 |
|
||||
| `REVOKED` | 撤回 |
|
||||
|
||||
[→ 完整 API Reference](./api)
|
||||
78
docs-site/docs/vue3/setup.md
普通文件
78
docs-site/docs/vue3/setup.md
普通文件
@ -0,0 +1,78 @@
|
||||
# Vue3 SDK 安装配置
|
||||
|
||||
**包名**:`@xuqm/vue3-sdk` · **版本**:0.1.0 · **Vue 版本**:^3.5.0
|
||||
|
||||
---
|
||||
|
||||
## npm 安装
|
||||
|
||||
```bash
|
||||
npm install @xuqm/vue3-sdk
|
||||
# 或
|
||||
yarn add @xuqm/vue3-sdk
|
||||
# 或
|
||||
pnpm add @xuqm/vue3-sdk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CDN 方式
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { init, ImClient } from 'https://cdn.xuqinmin.com/npm/@xuqm/vue3-sdk@0.1.0/dist/index.es.js'
|
||||
</script>
|
||||
```
|
||||
|
||||
> CDN 地址为示例,实际以您的 CDN 部署为准。
|
||||
|
||||
---
|
||||
|
||||
## TypeScript 支持
|
||||
|
||||
`@xuqm/vue3-sdk` 内置完整类型定义,安装后无需额外配置 `@types` 包。
|
||||
|
||||
```ts
|
||||
import type {
|
||||
SDKConfig,
|
||||
ImMessage,
|
||||
ImGroup,
|
||||
ChatType,
|
||||
MsgType,
|
||||
MsgStatus,
|
||||
ConversationView,
|
||||
PageResult,
|
||||
UserProfile,
|
||||
FriendRequest,
|
||||
GroupJoinRequest,
|
||||
BlacklistEntry,
|
||||
BlacklistCheckResult,
|
||||
GroupReadReceiptSummary,
|
||||
HistoryQuery,
|
||||
SendMessageParams,
|
||||
ImEventMap,
|
||||
ApiResponse,
|
||||
} from '@xuqm/vue3-sdk'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 初始化
|
||||
|
||||
```ts
|
||||
import { init } from '@xuqm/vue3-sdk'
|
||||
|
||||
init({
|
||||
appKey: 'your_app_key',
|
||||
appSecret: 'your_app_secret',
|
||||
debug: true, // 可选
|
||||
baseUrl: 'https://...', // 可选,默认 https://dev.xuqinmin.com
|
||||
wsUrl: 'wss://.../ws/im', // 可选,默认 wss://dev.xuqinmin.com/ws/im
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
- [Vue3 IM 接入 →](./im)
|
||||
@ -25,11 +25,18 @@ export interface TenantPage {
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export interface DailyTrendItem {
|
||||
date: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface Statistics {
|
||||
totalTenants: number
|
||||
todayNew: number
|
||||
activeApps: number
|
||||
onlineUsers: number
|
||||
dailyTrend: DailyTrendItem[]
|
||||
serviceDistribution: Record<string, number>
|
||||
}
|
||||
|
||||
export interface ServiceRequest {
|
||||
|
||||
@ -122,12 +122,25 @@ const ruleForm = reactive<RiskRuleForm>({
|
||||
})
|
||||
const ruleLoading = ref(false)
|
||||
|
||||
async function loadRules() {
|
||||
try {
|
||||
const res = await opsApi.getRiskRules()
|
||||
const cfg = res.data.data
|
||||
if (cfg) {
|
||||
ruleForm.ipRateLimit = cfg.ipRateLimit ?? 300
|
||||
ruleForm.loginFailThreshold = cfg.loginFailThreshold ?? 5
|
||||
ruleForm.loginLockMinutes = cfg.loginLockMinutes ?? 30
|
||||
ruleForm.abnormalDetection = cfg.abnormalDetection ?? true
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRules() {
|
||||
ruleLoading.value = true
|
||||
try {
|
||||
// TODO: 接入后端接口
|
||||
// await opsApi.saveRiskRules({ ...ruleForm })
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
await opsApi.saveRiskRules({ ...ruleForm })
|
||||
ElMessage.success('配置已保存')
|
||||
} catch {
|
||||
ElMessage.error('保存失败')
|
||||
@ -171,11 +184,10 @@ function levelTagType(level: string) {
|
||||
async function loadWords() {
|
||||
tableLoading.value = true
|
||||
try {
|
||||
// TODO: 接入后端接口
|
||||
// const res = await opsApi.listSensitiveWords(page.value - 1, size.value)
|
||||
await new Promise(r => setTimeout(r, 300))
|
||||
wordList.value = generateMockWords(page.value, size.value)
|
||||
total.value = 63
|
||||
const res = await opsApi.listSensitiveWords(page.value - 1, size.value)
|
||||
const data = res.data.data
|
||||
wordList.value = data.content ?? []
|
||||
total.value = data.total ?? 0
|
||||
} catch {
|
||||
ElMessage.error('加载失败')
|
||||
} finally {
|
||||
@ -183,27 +195,6 @@ async function loadWords() {
|
||||
}
|
||||
}
|
||||
|
||||
function generateMockWords(pageNum: number, pageSize: number): SensitiveWord[] {
|
||||
const words = ['违规', '垃圾广告', '诈骗', '恶意攻击', '敏感信息', '暴力', '赌博', '毒品']
|
||||
const levels: Array<'高' | '中' | '低'> = ['高', '中', '低']
|
||||
const categories = ['政治', '色情', '广告', '欺诈', '暴力']
|
||||
const result: SensitiveWord[] = []
|
||||
const start = (pageNum - 1) * pageSize
|
||||
for (let i = 0; i < pageSize; i++) {
|
||||
const idx = start + i
|
||||
if (idx >= 63) break
|
||||
result.push({
|
||||
id: `word_${idx}`,
|
||||
word: `${words[idx % words.length]}_${idx + 1}`,
|
||||
level: levels[idx % levels.length],
|
||||
category: categories[idx % categories.length],
|
||||
enabled: idx % 3 !== 0,
|
||||
updatedAt: new Date(Date.now() - idx * 86400000).toISOString(),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function openDialog(row?: SensitiveWord) {
|
||||
isEdit.value = !!row
|
||||
dialogTitle.value = row ? '编辑敏感词' : '新增敏感词'
|
||||
@ -220,13 +211,11 @@ async function submitForm() {
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
// TODO: 接入后端接口
|
||||
// if (isEdit.value && form.id) {
|
||||
// await opsApi.updateSensitiveWord(form.id, form as SensitiveWord)
|
||||
// } else {
|
||||
// await opsApi.createSensitiveWord(form as SensitiveWord)
|
||||
// }
|
||||
await new Promise(r => setTimeout(r, 400))
|
||||
if (isEdit.value && form.id) {
|
||||
await opsApi.updateSensitiveWord(form.id, form as SensitiveWord)
|
||||
} else {
|
||||
await opsApi.createSensitiveWord(form as SensitiveWord)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
dialogVisible.value = false
|
||||
loadWords()
|
||||
@ -239,8 +228,7 @@ async function submitForm() {
|
||||
|
||||
async function toggleWord(row: SensitiveWord) {
|
||||
try {
|
||||
// TODO: 接入后端接口
|
||||
// await opsApi.toggleSensitiveWord(row.id, row.enabled)
|
||||
await opsApi.toggleSensitiveWord(row.id, row.enabled)
|
||||
ElMessage.success('状态已更新')
|
||||
} catch {
|
||||
row.enabled = !row.enabled
|
||||
@ -251,8 +239,7 @@ async function toggleWord(row: SensitiveWord) {
|
||||
async function deleteWord(row: SensitiveWord) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除敏感词「${row.word}」吗?`, '提示', { type: 'warning' })
|
||||
// TODO: 接入后端接口
|
||||
// await opsApi.deleteSensitiveWord(row.id)
|
||||
await opsApi.deleteSensitiveWord(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
loadWords()
|
||||
} catch {
|
||||
@ -265,6 +252,7 @@ function fmt(value: string) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRules()
|
||||
loadWords()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -58,13 +58,13 @@ let trendChart: echarts.ECharts | null = null
|
||||
let pieChart: echarts.ECharts | null = null
|
||||
let barChart: echarts.ECharts | null = null
|
||||
|
||||
function initTrendChart() {
|
||||
function initTrendChart(dailyTrend: Array<{ date: string; count: number }>) {
|
||||
if (!trendChartRef.value) return
|
||||
const dates = Array.from({ length: 7 }, (_, i) => {
|
||||
const d = new Date()
|
||||
d.setDate(d.getDate() - (6 - i))
|
||||
return `${d.getMonth() + 1}/${d.getDate()}`
|
||||
const dates = dailyTrend.map(d => {
|
||||
const parts = d.date.split('-')
|
||||
return `${parseInt(parts[1])}/${parseInt(parts[2])}`
|
||||
})
|
||||
const values = dailyTrend.map(d => d.count)
|
||||
trendChart = echarts.init(trendChartRef.value)
|
||||
trendChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
@ -75,7 +75,7 @@ function initTrendChart() {
|
||||
name: '新增注册',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [12, 19, 8, 24, 32, 18, 27],
|
||||
data: values,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(64,158,255,0.3)' },
|
||||
@ -87,8 +87,12 @@ function initTrendChart() {
|
||||
})
|
||||
}
|
||||
|
||||
function initPieChart() {
|
||||
function initPieChart(serviceDistribution: Record<string, number>) {
|
||||
if (!pieChartRef.value) return
|
||||
const data = Object.entries(serviceDistribution).map(([name, value]) => ({ name, value }))
|
||||
if (data.length === 0) {
|
||||
data.push({ name: '暂无数据', value: 0 })
|
||||
}
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
pieChart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
@ -100,13 +104,7 @@ function initPieChart() {
|
||||
itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false },
|
||||
emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
|
||||
data: [
|
||||
{ value: 1048, name: 'IM' },
|
||||
{ value: 735, name: 'Push' },
|
||||
{ value: 580, name: 'Update' },
|
||||
{ value: 484, name: 'Webhook' },
|
||||
{ value: 300, name: '灰度发布' },
|
||||
],
|
||||
data,
|
||||
}],
|
||||
})
|
||||
}
|
||||
@ -126,11 +124,10 @@ function initBarChart() {
|
||||
xAxis: { type: 'category', data: dates },
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{ name: '发送消息', type: 'bar', data: [120, 132, 101, 134, 90, 230, 210], itemStyle: { color: '#409eff' } },
|
||||
{ name: '接收消息', type: 'bar', data: [220, 182, 191, 234, 290, 330, 310], itemStyle: { color: '#67c23a' } },
|
||||
{ name: '发送消息', type: 'bar', data: [0, 0, 0, 0, 0, 0, 0], itemStyle: { color: '#409eff' } },
|
||||
{ name: '接收消息', type: 'bar', data: [0, 0, 0, 0, 0, 0, 0], itemStyle: { color: '#67c23a' } },
|
||||
],
|
||||
})
|
||||
// TODO: 接入后端接口获取真实消息量数据
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
@ -147,10 +144,13 @@ onMounted(async () => {
|
||||
stats.value[1].value = d.todayNew ?? 0
|
||||
stats.value[2].value = d.activeApps ?? 0
|
||||
stats.value[3].value = d.onlineUsers ?? 0
|
||||
} catch {}
|
||||
initTrendChart(d.dailyTrend ?? [])
|
||||
initPieChart(d.serviceDistribution ?? {})
|
||||
} catch {
|
||||
initTrendChart([])
|
||||
initPieChart({})
|
||||
}
|
||||
|
||||
initTrendChart()
|
||||
initPieChart()
|
||||
initBarChart()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
@ -165,6 +165,8 @@ export interface WebhookConfig {
|
||||
secret?: string | null
|
||||
enabled: boolean
|
||||
createdAt: number
|
||||
consecutiveFailures?: number
|
||||
lastFailureAt?: number | null
|
||||
}
|
||||
|
||||
export interface WebhookConfigForm {
|
||||
@ -173,6 +175,32 @@ export interface WebhookConfigForm {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface WebhookDelivery {
|
||||
id: string
|
||||
appId: string
|
||||
callbackId: string
|
||||
callbackEvent: string
|
||||
url: string
|
||||
httpStatus: number
|
||||
responseBody?: string | null
|
||||
errorMessage?: string | null
|
||||
attempt: number
|
||||
success: boolean
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
export interface WebhookAlert {
|
||||
id: string
|
||||
appId: string
|
||||
webhookId: string
|
||||
webhookUrl: string
|
||||
alertType: string
|
||||
description?: string | null
|
||||
acknowledged: boolean
|
||||
createdAt: number
|
||||
acknowledgedAt?: number | null
|
||||
}
|
||||
|
||||
export interface GroupJoinRequest {
|
||||
id: string
|
||||
appId: string
|
||||
@ -517,4 +545,82 @@ export const imAdminApi = {
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
transferGroupOwner(appId: string, groupId: string, newOwnerId: string) {
|
||||
return imClient.post<{ data: ImGroup }>(
|
||||
`/api/im/admin/groups/${encodeURIComponent(groupId)}/owner`,
|
||||
{ newOwnerId },
|
||||
{ params: { appId } },
|
||||
)
|
||||
},
|
||||
|
||||
updateGroupAttributes(appId: string, groupId: string, attributes: Record<string, unknown>) {
|
||||
return imClient.put<{ data: ImGroup }>(
|
||||
`/api/im/admin/groups/${encodeURIComponent(groupId)}/attributes`,
|
||||
{ attributes },
|
||||
{ params: { appId } },
|
||||
)
|
||||
},
|
||||
|
||||
removeGroupAttributes(appId: string, groupId: string, keys: string[]) {
|
||||
return imClient.post<{ data: ImGroup }>(
|
||||
`/api/im/admin/groups/${encodeURIComponent(groupId)}/attributes/delete`,
|
||||
{ keys },
|
||||
{ params: { appId } },
|
||||
)
|
||||
},
|
||||
|
||||
getGroupReadReceipts(appId: string, groupId: string, messageIds?: string[]) {
|
||||
return imClient.post<{ data: Array<{ messageId: string; readCount: number; readUserIds: string[] }> }>(
|
||||
`/api/im/admin/groups/${encodeURIComponent(groupId)}/read-receipts`,
|
||||
{ messageIds },
|
||||
{ params: { appId } },
|
||||
)
|
||||
},
|
||||
|
||||
queryUserState(appId: string, userIds: string[]) {
|
||||
return imClient.get<{ data: Record<string, { online: boolean; lastSeenAt: number }> }>(
|
||||
'/api/im/admin/users/state',
|
||||
{ params: { userIds: userIds.join(',') } },
|
||||
)
|
||||
},
|
||||
|
||||
listWebhookDeliveries(
|
||||
appId: string,
|
||||
params: { callbackEvent?: string; success?: boolean; page?: number; size?: number } = {},
|
||||
) {
|
||||
return imClient.get<{ data: PagedResult<WebhookDelivery> }>('/api/im/admin/webhook-deliveries', {
|
||||
params: { appId, ...params },
|
||||
})
|
||||
},
|
||||
|
||||
listWebhookAlerts(
|
||||
appId: string,
|
||||
params: { acknowledged?: boolean; page?: number; size?: number } = {},
|
||||
) {
|
||||
return imClient.get<{ data: PagedResult<WebhookAlert> }>('/api/im/admin/webhook-alerts', {
|
||||
params: { appId, ...params },
|
||||
})
|
||||
},
|
||||
|
||||
acknowledgeWebhookAlert(appId: string, alertId: string) {
|
||||
return imClient.post<{ data: WebhookAlert }>(
|
||||
`/api/im/admin/webhook-alerts/${encodeURIComponent(alertId)}/acknowledge`,
|
||||
null,
|
||||
{ params: { appId } },
|
||||
)
|
||||
},
|
||||
|
||||
getWebhookHealth(appId: string, webhookId: string) {
|
||||
return imClient.get<{
|
||||
data: {
|
||||
webhookId: string
|
||||
url: string
|
||||
enabled: boolean
|
||||
consecutiveFailures: number
|
||||
lastFailureAt: number | null
|
||||
unacknowledgedAlerts: number
|
||||
}
|
||||
}>(`/api/im/admin/webhooks/${encodeURIComponent(webhookId)}/health`, { params: { appId } })
|
||||
},
|
||||
}
|
||||
|
||||
@ -61,6 +61,14 @@ const router = createRouter({
|
||||
path: 'apps/:appId/im-webhooks',
|
||||
component: () => import('@/views/im/ImWebhookView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'apps/:appId/im-webhooks/:webhookId/deliveries',
|
||||
component: () => import('@/views/im/WebhookDeliveryLogView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'apps/:appId/im-webhook-alerts',
|
||||
component: () => import('@/views/im/WebhookAlertView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'apps/:appId/im',
|
||||
component: () => import('@/views/im/ImManagementView.vue'),
|
||||
|
||||
@ -44,6 +44,15 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="在线" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="userOnlineState[row.userId]?.online" type="success" size="small">在线</el-tag>
|
||||
<el-tag v-else-if="userOnlineState[row.userId]?.lastSeenAt" type="info" size="small">
|
||||
{{ formatLastSeen(userOnlineState[row.userId].lastSeenAt) }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" size="small">离线</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="注册时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.createdAt) }}
|
||||
@ -470,6 +479,11 @@
|
||||
<el-descriptions-item label="公告">{{ managedGroup.announcement || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="managedGroup" class="toolbar" style="margin-bottom: 12px">
|
||||
<el-button type="primary" size="small" @click="openGroupAttributesDialog">扩展属性</el-button>
|
||||
<el-button type="info" size="small" @click="openReadReceiptsDialog">回执查询</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="groupManageTab">
|
||||
<el-tab-pane label="成员" name="members">
|
||||
<div class="toolbar toolbar-space-between">
|
||||
@ -519,8 +533,17 @@
|
||||
<el-table-column prop="createdAt" label="加入时间" width="180">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.userId !== managedGroup?.creatorId"
|
||||
link
|
||||
type="success"
|
||||
size="small"
|
||||
@click="transferGroupOwner(row)"
|
||||
>
|
||||
转让群主
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@ -606,6 +629,34 @@
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showGroupAttributesDialog" title="群扩展属性" width="560px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="属性 JSON">
|
||||
<el-input v-model="groupAttributesJson" type="textarea" :rows="6" placeholder='{"key": "value"}' />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showGroupAttributesDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitGroupAttributes">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showReadReceiptsDialog" title="群消息已读回执" width="640px">
|
||||
<div class="toolbar">
|
||||
<el-button @click="loadGroupReadReceipts" :loading="loadingReadReceipts">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="groupReadReceipts" v-loading="loadingReadReceipts" border stripe>
|
||||
<el-table-column prop="messageId" label="消息ID" min-width="200" />
|
||||
<el-table-column prop="readCount" label="已读人数" width="100" />
|
||||
<el-table-column prop="readUserIds" label="已读用户" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.readUserIds && row.readUserIds.length">{{ row.readUserIds.join(', ') }}</span>
|
||||
<span v-else class="el-text el-text--info">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="showWebhookDialog"
|
||||
:title="editingWebhookId ? '编辑回调配置' : '新增回调配置'"
|
||||
@ -737,6 +788,7 @@ const loadingUsers = ref(false)
|
||||
const userPage = ref(0)
|
||||
const userPageSize = 20
|
||||
const userTotal = ref(0)
|
||||
const userOnlineState = ref<Record<string, { online: boolean; lastSeenAt: number }>>({})
|
||||
|
||||
const groups = ref<ImGroup[]>([])
|
||||
const loadingGroups = ref(false)
|
||||
@ -841,6 +893,13 @@ const editGroupForm = ref({
|
||||
announcement: '',
|
||||
})
|
||||
|
||||
const showGroupAttributesDialog = ref(false)
|
||||
const groupAttributesJson = ref('{}')
|
||||
|
||||
const showReadReceiptsDialog = ref(false)
|
||||
const groupReadReceipts = ref<Array<{ messageId: string; readCount: number; readUserIds: string[] }>>([])
|
||||
const loadingReadReceipts = ref(false)
|
||||
|
||||
const memberSearchKeyword = ref('')
|
||||
const memberSearchResults = ref<ImUser[]>([])
|
||||
const searchingMembers = ref(false)
|
||||
@ -1093,6 +1152,15 @@ async function loadUsers() {
|
||||
const res = await imAdminApi.listUsers(appKey.value, userPage.value, userPageSize)
|
||||
users.value = res.data.data.content
|
||||
userTotal.value = res.data.data.totalElements
|
||||
if (users.value.length > 0) {
|
||||
const ids = users.value.map(u => u.userId)
|
||||
try {
|
||||
const stateRes = await imAdminApi.queryUserState(appKey.value, ids)
|
||||
userOnlineState.value = stateRes.data.data
|
||||
} catch {
|
||||
// ignore state query error
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('加载用户失败')
|
||||
} finally {
|
||||
@ -1100,6 +1168,18 @@ async function loadUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
function formatLastSeen(ts: number): string {
|
||||
if (!ts) return '未知'
|
||||
const diff = Date.now() - ts
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
if (minutes < 1) return '刚刚'
|
||||
if (minutes < 60) return `${minutes}分钟前`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return `${hours}小时前`
|
||||
const days = Math.floor(hours / 24)
|
||||
return `${days}天前`
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
loadingGroups.value = true
|
||||
try {
|
||||
@ -1297,6 +1377,59 @@ async function removeManagedGroupMember(user: ImUser) {
|
||||
await loadGroups()
|
||||
}
|
||||
|
||||
async function transferGroupOwner(user: ImUser) {
|
||||
const group = managedGroup.value
|
||||
if (!group) return
|
||||
await ElMessageBox.confirm(`确认将群主转让给 ${user.userId}?`, '转让群主', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
await imAdminApi.transferGroupOwner(appKey.value, group.id, user.userId)
|
||||
ElMessage.success('群主已转让')
|
||||
await loadManagedGroupMembers()
|
||||
await loadGroups()
|
||||
}
|
||||
|
||||
function openGroupAttributesDialog() {
|
||||
const group = managedGroup.value
|
||||
if (!group) return
|
||||
groupAttributesJson.value = '{}'
|
||||
showGroupAttributesDialog.value = true
|
||||
}
|
||||
|
||||
async function submitGroupAttributes() {
|
||||
const group = managedGroup.value
|
||||
if (!group) return
|
||||
try {
|
||||
const attributes = JSON.parse(groupAttributesJson.value)
|
||||
await imAdminApi.updateGroupAttributes(appKey.value, group.id, attributes)
|
||||
ElMessage.success('扩展属性已保存')
|
||||
showGroupAttributesDialog.value = false
|
||||
} catch {
|
||||
ElMessage.error('JSON 格式错误')
|
||||
}
|
||||
}
|
||||
|
||||
function openReadReceiptsDialog() {
|
||||
showReadReceiptsDialog.value = true
|
||||
loadGroupReadReceipts()
|
||||
}
|
||||
|
||||
async function loadGroupReadReceipts() {
|
||||
const group = managedGroup.value
|
||||
if (!group) return
|
||||
loadingReadReceipts.value = true
|
||||
try {
|
||||
const res = await imAdminApi.getGroupReadReceipts(appKey.value, group.id)
|
||||
groupReadReceipts.value = res.data.data ?? []
|
||||
} catch {
|
||||
groupReadReceipts.value = []
|
||||
} finally {
|
||||
loadingReadReceipts.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleManagedGroupRole(user: ImUser) {
|
||||
const group = managedGroup.value
|
||||
if (!group) return
|
||||
|
||||
@ -40,8 +40,15 @@
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<template #header>回调地址</template>
|
||||
<el-card style="margin-bottom:16px">
|
||||
<template #header>
|
||||
<div class="toolbar toolbar-space-between">
|
||||
<span>回调地址</span>
|
||||
<el-button link type="primary" @click="$router.push({ path: `/apps/${appId}/im-webhook-alerts` })">
|
||||
查看告警
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="toolbar toolbar-space-between">
|
||||
<el-button type="primary" @click="openCreateWebhookDialog">新增回调</el-button>
|
||||
<el-button @click="loadWebhooks" :loading="loadingWebhooks">刷新</el-button>
|
||||
@ -54,19 +61,32 @@
|
||||
{{ row.secret ? '******' : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enabled" label="启用" width="100">
|
||||
<el-table-column prop="enabled" label="启用" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">
|
||||
{{ row.enabled ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
||||
<el-table-column label="健康" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.consecutiveFailures && row.consecutiveFailures > 0"
|
||||
:type="row.consecutiveFailures >= 10 ? 'danger' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
失败 {{ row.consecutiveFailures }} 次
|
||||
</el-tag>
|
||||
<el-tag v-else type="success" size="small">正常</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEditWebhookDialog(row)">编辑</el-button>
|
||||
<el-button link type="info" size="small" @click="$router.push({ path: `/apps/${appId}/im-webhooks/${row.id}/deliveries` })">日志</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteWebhook(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div v-if="app">
|
||||
<el-page-header @back="$router.back()" content="Webhook 告警" style="margin-bottom:24px" />
|
||||
|
||||
<el-card style="margin-bottom:16px">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="应用名称">{{ app.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="未确认告警">
|
||||
<el-tag v-if="unacknowledgedCount > 0" type="danger">{{ unacknowledgedCount }}</el-tag>
|
||||
<span v-else>0</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<template #header>告警列表</template>
|
||||
<div class="toolbar toolbar-space-between" style="margin-bottom:16px">
|
||||
<div style="display:flex;gap:12px;align-items:center">
|
||||
<el-select v-model="filterAcknowledged" placeholder="确认状态" clearable style="width:140px">
|
||||
<el-option label="未确认" :value="false" />
|
||||
<el-option label="已确认" :value="true" />
|
||||
</el-select>
|
||||
<el-button @click="loadAlerts" :loading="loading">查询</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="alerts" v-loading="loading" border stripe>
|
||||
<el-table-column prop="webhookUrl" label="回调地址" min-width="260" show-overflow-tooltip />
|
||||
<el-table-column prop="alertType" label="类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.alertType === 'AUTO_DISABLED'" type="danger" size="small">自动禁用</el-tag>
|
||||
<el-tag v-else type="warning" size="small">{{ row.alertType }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="说明" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.acknowledged ? 'info' : 'danger'" size="small">
|
||||
{{ row.acknowledged ? '已确认' : '未确认' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="告警时间" width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="!row.acknowledged"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="acknowledge(row)"
|
||||
>确认</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="size"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
style="margin-top:16px"
|
||||
@change="loadAlerts"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { appApi, type App } from '@/api/app'
|
||||
import { imAdminApi, type WebhookAlert } from '@/api/im'
|
||||
|
||||
const route = useRoute()
|
||||
const app = ref<App | null>(null)
|
||||
const appId = computed(() => route.params.appId as string)
|
||||
|
||||
const alerts = ref<WebhookAlert[]>([])
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const size = ref(20)
|
||||
const total = ref(0)
|
||||
const filterAcknowledged = ref<boolean | ''>('')
|
||||
const unacknowledgedCount = ref(0)
|
||||
|
||||
async function loadApp() {
|
||||
const res = await appApi.get(appId.value)
|
||||
app.value = res.data.data
|
||||
}
|
||||
|
||||
async function loadAlerts() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: Record<string, unknown> = { page: page.value - 1, size: size.value }
|
||||
if (filterAcknowledged.value !== '') params.acknowledged = filterAcknowledged.value
|
||||
const res = await imAdminApi.listWebhookAlerts(appId.value, params)
|
||||
alerts.value = res.data.data.content
|
||||
total.value = res.data.data.totalElements
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUnacknowledgedCount() {
|
||||
const res = await imAdminApi.listWebhookAlerts(appId.value, { acknowledged: false, size: 1 })
|
||||
unacknowledgedCount.value = Number(res.data.data.totalElements)
|
||||
}
|
||||
|
||||
async function acknowledge(row: WebhookAlert) {
|
||||
await imAdminApi.acknowledgeWebhookAlert(appId.value, row.id)
|
||||
ElMessage.success('已确认')
|
||||
await loadAlerts()
|
||||
await loadUnacknowledgedCount()
|
||||
}
|
||||
|
||||
function formatTime(value: number) {
|
||||
return new Date(value).toLocaleString()
|
||||
}
|
||||
|
||||
watch([filterAcknowledged], () => {
|
||||
page.value = 1
|
||||
loadAlerts()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadApp(), loadAlerts(), loadUnacknowledgedCount()])
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div v-if="app">
|
||||
<el-page-header @back="$router.back()" content="Webhook 投递日志" style="margin-bottom:24px" />
|
||||
|
||||
<el-card style="margin-bottom:16px">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="应用名称">{{ app.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="回调地址">{{ webhookUrl }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<template #header>投递记录</template>
|
||||
<div class="toolbar toolbar-space-between" style="margin-bottom:16px">
|
||||
<div style="display:flex;gap:12px;align-items:center">
|
||||
<el-select v-model="filterEvent" placeholder="事件类型" clearable style="width:180px">
|
||||
<el-option label="message.sent" value="message.sent" />
|
||||
<el-option label="message.revoked" value="message.revoked" />
|
||||
<el-option label="message.edited" value="message.edited" />
|
||||
<el-option label="message.read" value="message.read" />
|
||||
</el-select>
|
||||
<el-select v-model="filterSuccess" placeholder="状态" clearable style="width:120px">
|
||||
<el-option label="成功" :value="true" />
|
||||
<el-option label="失败" :value="false" />
|
||||
</el-select>
|
||||
<el-button @click="loadDeliveries" :loading="loading">查询</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="deliveries" v-loading="loading" border stripe>
|
||||
<el-table-column prop="callbackEvent" label="事件" width="160" />
|
||||
<el-table-column prop="attempt" label="重试次数" width="90" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.success ? 'success' : 'danger'" size="small">
|
||||
{{ row.success ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="httpStatus" label="HTTP" width="90" />
|
||||
<el-table-column prop="errorMessage" label="错误信息" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="responseBody" label="响应体" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="createdAt" label="时间" width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="size"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
style="margin-top:16px"
|
||||
@change="loadDeliveries"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { appApi, type App } from '@/api/app'
|
||||
import { imAdminApi, type WebhookDelivery } from '@/api/im'
|
||||
|
||||
const route = useRoute()
|
||||
const app = ref<App | null>(null)
|
||||
const appId = computed(() => route.params.appId as string)
|
||||
const webhookId = computed(() => route.params.webhookId as string)
|
||||
const webhookUrl = ref('')
|
||||
|
||||
const deliveries = ref<WebhookDelivery[]>([])
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const size = ref(20)
|
||||
const total = ref(0)
|
||||
const filterEvent = ref('')
|
||||
const filterSuccess = ref<boolean | ''>('')
|
||||
|
||||
async function loadApp() {
|
||||
const res = await appApi.get(appId.value)
|
||||
app.value = res.data.data
|
||||
}
|
||||
|
||||
async function loadWebhook() {
|
||||
const res = await imAdminApi.listWebhooks(appId.value)
|
||||
const wh = res.data.data.find((w) => w.id === webhookId.value)
|
||||
if (wh) webhookUrl.value = wh.url
|
||||
}
|
||||
|
||||
async function loadDeliveries() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: Record<string, unknown> = { page: page.value - 1, size: size.value }
|
||||
if (filterEvent.value) params.callbackEvent = filterEvent.value
|
||||
if (filterSuccess.value !== '') params.success = filterSuccess.value
|
||||
const res = await imAdminApi.listWebhookDeliveries(appId.value, params)
|
||||
deliveries.value = res.data.data.content
|
||||
total.value = res.data.data.totalElements
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(value: number) {
|
||||
return new Date(value).toLocaleString()
|
||||
}
|
||||
|
||||
watch([filterEvent, filterSuccess], () => {
|
||||
page.value = 1
|
||||
loadDeliveries()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadApp(), loadWebhook(), loadDeliveries()])
|
||||
})
|
||||
</script>
|
||||
正在加载...
在新工单中引用
屏蔽一个用户