@talknote/create-talknote-h5 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/bin/create-talknote-h5.js +14 -0
  2. package/package.json +1 -1
  3. package/template/.agents/skills/add-h5-page/SKILL.md +116 -0
  4. package/template/.eslintrc.js +2 -0
  5. package/template/README.md +143 -117
  6. package/template/_gitignore +0 -3
  7. package/template/config/config.ts +49 -4
  8. package/template/config/proxy.ts +28 -31
  9. package/template/docker/nginx/error_page.conf +51 -0
  10. package/template/docker/nginx/nginx.conf +54 -0
  11. package/template/docker/nginx/webApp.conf +13 -0
  12. package/template/docker/webApp.Dockerfile +12 -0
  13. package/template/package.json +6 -2
  14. package/template/packages/xiaozhi-chat/README.md +53 -0
  15. package/template/packages/xiaozhi-chat/XiaozhiChatPage.less +50 -0
  16. package/template/packages/xiaozhi-chat/XiaozhiChatPage.tsx +172 -0
  17. package/template/packages/xiaozhi-chat/assets/images/bg.jpg +0 -0
  18. package/template/packages/xiaozhi-chat/assets/images/icon-arrow-back.png +0 -0
  19. package/template/packages/xiaozhi-chat/assets/images/icon-close.png +0 -0
  20. package/template/packages/xiaozhi-chat/assets/images/icon-voice.png +0 -0
  21. package/template/packages/xiaozhi-chat/assets/images/keyboard.png +0 -0
  22. package/template/packages/xiaozhi-chat/assets/images/send-icon.png +0 -0
  23. package/template/packages/xiaozhi-chat/assets/images/stop-icon.png +0 -0
  24. package/template/packages/xiaozhi-chat/components/AssistantRichContent/index.less +3 -0
  25. package/template/packages/xiaozhi-chat/components/AssistantRichContent/index.tsx +44 -0
  26. package/template/packages/xiaozhi-chat/components/ChatHeader/index.less +84 -0
  27. package/template/packages/xiaozhi-chat/components/ChatHeader/index.tsx +44 -0
  28. package/template/packages/xiaozhi-chat/components/ChatInput/HoldToTalk.less +59 -0
  29. package/template/packages/xiaozhi-chat/components/ChatInput/HoldToTalk.tsx +167 -0
  30. package/template/packages/xiaozhi-chat/components/ChatInput/index.less +189 -0
  31. package/template/packages/xiaozhi-chat/components/ChatInput/index.tsx +203 -0
  32. package/template/packages/xiaozhi-chat/components/DeepThinkingWrapper/index.less +80 -0
  33. package/template/packages/xiaozhi-chat/components/DeepThinkingWrapper/index.tsx +54 -0
  34. package/template/packages/xiaozhi-chat/components/MarkdownContent/index.less +209 -0
  35. package/template/packages/xiaozhi-chat/components/MarkdownContent/index.tsx +41 -0
  36. package/template/packages/xiaozhi-chat/components/MessageList/index.less +74 -0
  37. package/template/packages/xiaozhi-chat/components/MessageList/index.tsx +195 -0
  38. package/template/packages/xiaozhi-chat/components/ThinkingCompleteEntry/index.less +56 -0
  39. package/template/packages/xiaozhi-chat/components/ThinkingCompleteEntry/index.tsx +65 -0
  40. package/template/packages/xiaozhi-chat/components/ToolCallList/index.less +41 -0
  41. package/template/packages/xiaozhi-chat/components/ToolCallList/index.tsx +51 -0
  42. package/template/packages/xiaozhi-chat/components/TypingIndicator/index.less +35 -0
  43. package/template/packages/xiaozhi-chat/components/TypingIndicator/index.tsx +12 -0
  44. package/template/packages/xiaozhi-chat/components/Welcome/index.less +105 -0
  45. package/template/packages/xiaozhi-chat/components/Welcome/index.tsx +84 -0
  46. package/template/packages/xiaozhi-chat/hooks/useChatSession.ts +235 -0
  47. package/template/packages/xiaozhi-chat/hooks/useRealtimeTranscription.ts +226 -0
  48. package/template/packages/xiaozhi-chat/hooks/useTypewriter.ts +215 -0
  49. package/template/packages/xiaozhi-chat/hooks/useViewportHeight.ts +43 -0
  50. package/template/packages/xiaozhi-chat/hooks/useVoiceRecording.ts +128 -0
  51. package/template/packages/xiaozhi-chat/index.ts +11 -0
  52. package/template/packages/xiaozhi-chat/package.json +34 -0
  53. package/template/packages/xiaozhi-chat/services/chatApi.ts +585 -0
  54. package/template/packages/xiaozhi-chat/types.ts +80 -0
  55. package/template/packages/xiaozhi-chat/utils/bridge.ts +131 -0
  56. package/template/packages/xiaozhi-chat/utils/deepThinking.ts +200 -0
  57. package/template/packages/xiaozhi-chat/utils/parseAssistantContent.ts +233 -0
  58. package/template/packages/xiaozhi-chat/utils/typewriter.ts +265 -0
  59. package/template/scripts/dev.js +6 -7
  60. package/template/src/app.tsx +1 -73
  61. package/template/src/assets/error-network.png +0 -0
  62. package/template/src/assets/error.png +0 -0
  63. package/template/src/assets/success.png +0 -0
  64. package/template/src/assets/welcome.png +0 -0
  65. package/template/src/components/Header/index.less +70 -0
  66. package/template/src/components/Header/index.tsx +46 -0
  67. package/template/src/components/TabBar/index.less +13 -0
  68. package/template/src/components/TabBar/index.tsx +58 -0
  69. package/template/src/global.css +27 -0
  70. package/template/src/layouts/BlankLayout/index.less +5 -0
  71. package/template/src/layouts/BlankLayout/index.tsx +14 -0
  72. package/template/src/layouts/HeaderLayout/index.less +25 -0
  73. package/template/src/layouts/HeaderLayout/index.tsx +42 -0
  74. package/template/src/layouts/LayoutOutlet.tsx +7 -0
  75. package/template/src/layouts/SSOLoginLayout/index.less +28 -0
  76. package/template/src/layouts/SSOLoginLayout/index.tsx +92 -0
  77. package/template/src/layouts/TabBarLayout/index.less +21 -0
  78. package/template/src/layouts/TabBarLayout/index.tsx +29 -0
  79. package/template/src/layouts/index.tsx +30 -0
  80. package/template/src/layouts/usePageHeaderProps.ts +9 -0
  81. package/template/src/pages/index/index.css +5 -5
  82. package/template/src/pages/index/index.tsx +15 -2
  83. package/template/src/pages/list/index.tsx +5 -5
  84. package/template/src/pages/test/index.tsx +12 -0
  85. package/template/src/pages/xiaozhi-chat/index.less +4 -0
  86. package/template/src/pages/xiaozhi-chat/index.tsx +26 -0
  87. package/template/src/routes.ts +49 -0
  88. package/template/src/services/api.ts +7 -2
  89. package/template/src/theme/variables.css +5 -0
  90. package/template/src/utils/ensureSsoLogin.ts +28 -0
  91. package/template/src/utils/goBack.ts +10 -0
  92. package/template/src/utils/request.ts +106 -0
  93. package/template/tsconfig.json +4 -1
  94. package/template/typings.d.ts +15 -0
  95. package/template/yarn.lock +2168 -61
  96. package/template/src/assets/demoIcon/home.png +0 -0
  97. package/template/src/assets/demoIcon/home1.png +0 -0
  98. package/template/src/assets/demoIcon/list.png +0 -0
  99. package/template/src/assets/demoIcon/list1.png +0 -0
  100. package/template/src/assets/demoIcon/setting.png +0 -0
  101. package/template/src/assets/demoIcon/setting1.png +0 -0
  102. package/template/src/assets/yay.jpg +0 -0
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import Header from '@/components/Header';
3
+ import type { HeaderProps } from '@/components/Header';
4
+ import { LayoutOutlet } from '../LayoutOutlet';
5
+ import { usePageHeaderProps } from '../usePageHeaderProps';
6
+ import styles from './index.less';
7
+
8
+ export type HeaderLayoutProps = HeaderProps & {
9
+ children?: React.ReactNode;
10
+ };
11
+
12
+ /**
13
+ * 顶栏固定 + 内容区滚动。Header 与滚动容器是兄弟节点,
14
+ * 不会被 iOS 橡皮筋(overscroll)带着拉伸。
15
+ */
16
+ const HeaderLayout: React.FC<HeaderLayoutProps> = ({
17
+ children,
18
+ title,
19
+ showBack,
20
+ extra,
21
+ onBack,
22
+ }) => {
23
+ const fromRoute = usePageHeaderProps();
24
+
25
+ return (
26
+ <div className={styles.layout}>
27
+ <div className={styles.headerWrap}>
28
+ <Header
29
+ title={title ?? fromRoute?.title}
30
+ showBack={showBack ?? fromRoute?.showBack ?? true}
31
+ extra={extra ?? fromRoute?.extra}
32
+ onBack={onBack ?? fromRoute?.onBack}
33
+ />
34
+ </div>
35
+ <div className={styles.content}>
36
+ <LayoutOutlet>{children}</LayoutOutlet>
37
+ </div>
38
+ </div>
39
+ );
40
+ };
41
+
42
+ export default HeaderLayout;
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { Outlet } from 'alita';
3
+
4
+ /** 作为路由布局时用 Outlet,作为组件嵌套时用 children */
5
+ export function LayoutOutlet({ children }: { children?: React.ReactNode }) {
6
+ return <>{children ?? <Outlet />}</>;
7
+ }
@@ -0,0 +1,28 @@
1
+ .gate {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ justify-content: center;
6
+ height: 100%;
7
+ min-height: 0;
8
+ gap: var(--spacing-md);
9
+ padding: var(--spacing-lg);
10
+ background: var(--color-background-bg);
11
+ }
12
+
13
+ .pending {
14
+ flex-direction: row;
15
+ gap: var(--spacing-sm);
16
+ }
17
+
18
+ .message {
19
+ margin: 0;
20
+ font-size: var(--font-size-sm);
21
+ color: var(--color-text-secondary);
22
+ text-align: center;
23
+ }
24
+
25
+ .errorImg {
26
+ width: 200Px;
27
+ height: 200Px;
28
+ }
@@ -0,0 +1,92 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { Button, DotLoading } from 'antd-mobile';
3
+ import { ensureSsoLogin, hasSsoSucceeded } from '@/utils/ensureSsoLogin';
4
+ import errorImg from '@/assets/error.png';
5
+ import styles from './index.less';
6
+
7
+ export type SSOLoginLayoutProps = {
8
+ children?: React.ReactNode;
9
+
10
+ /** false: skip ticket login and render children immediately */
11
+ enabled?: boolean;
12
+ };
13
+
14
+ type SsoStatus = 'pending' | 'ready' | 'failed';
15
+
16
+ /**
17
+ * Top-level gate: SSO ticket login before the app.
18
+ * Disable via `enabled={false}` (see `ssoLoginEnabled` in src/routes.ts).
19
+ */
20
+ const SSOLoginLayout: React.FC<SSOLoginLayoutProps> = ({ children, enabled = true }) => {
21
+ const [status, setStatus] = useState<SsoStatus>(() =>
22
+ !enabled || hasSsoSucceeded() ? 'ready' : 'pending',
23
+ );
24
+
25
+ const isInApp = useMemo(() => typeof window.vibeNoteBridge !== 'undefined', []);
26
+
27
+ const runSsoLogin = useCallback(() => {
28
+ if (!enabled) {
29
+ setStatus('ready');
30
+ return;
31
+ }
32
+ if (hasSsoSucceeded()) {
33
+ setStatus('ready');
34
+ return;
35
+ }
36
+ setStatus('pending');
37
+ ensureSsoLogin()
38
+ .then(() => setStatus('ready'))
39
+ .catch(() => setStatus('failed'));
40
+ }, [enabled]);
41
+
42
+ useEffect(() => {
43
+ if (!isInApp) {
44
+ setStatus('failed');
45
+ return;
46
+ }
47
+ runSsoLogin();
48
+ }, [runSsoLogin, isInApp]);
49
+
50
+ if (!enabled || status === 'ready') {
51
+ return <>{children}</>;
52
+ }
53
+
54
+ if (status === 'pending') {
55
+ return (
56
+ <div className={`${styles.gate} ${styles.pending}`}>
57
+ <DotLoading color="primary" />
58
+ </div>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <div className={styles.gate}>
64
+ <img src={errorImg} alt="error" className={styles.errorImg} />
65
+ {
66
+ isInApp ? (
67
+ <>
68
+ <p className={styles.message}>登录失败,请重试</p>
69
+ <Button
70
+ color="primary"
71
+ onClick={runSsoLogin}
72
+ style={{
73
+ marginTop: 'var(--spacing-md)',
74
+ borderRadius: '99Px',
75
+ fontWeight: 'var(--font-weight-medium)',
76
+ width: '180Px',
77
+ }}
78
+ >
79
+ 重试
80
+ </Button>
81
+ </>
82
+ ) : (
83
+ <p className={styles.message}>请在智在记录APP里访问</p>
84
+ )
85
+ }
86
+
87
+
88
+ </div>
89
+ );
90
+ };
91
+
92
+ export default SSOLoginLayout;
@@ -0,0 +1,21 @@
1
+ .layout {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ overflow: hidden;
6
+ background: var(--color-background-bg);
7
+ }
8
+
9
+ .body {
10
+ flex: 1;
11
+ min-height: 0;
12
+ overflow: hidden;
13
+ }
14
+
15
+ .content {
16
+ position: relative;
17
+ height: 100%;
18
+ overflow: auto;
19
+ overscroll-behavior: none;
20
+ -webkit-overflow-scrolling: touch;
21
+ }
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import NavBar from '@/components/TabBar';
3
+ import HeaderLayout from '../HeaderLayout';
4
+ import { LayoutOutlet } from '../LayoutOutlet';
5
+ import { usePageHeaderProps } from '../usePageHeaderProps';
6
+ import styles from './index.less';
7
+
8
+ /**
9
+ * 带底部导航的布局。配置了 headerProps 才渲染顶栏。
10
+ */
11
+ const TabBarLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
12
+ const headerProps = usePageHeaderProps();
13
+ const content = <LayoutOutlet>{children}</LayoutOutlet>;
14
+
15
+ return (
16
+ <div className={styles.layout}>
17
+ <div className={styles.body}>
18
+ {headerProps ? (
19
+ <HeaderLayout {...headerProps}>{content}</HeaderLayout>
20
+ ) : (
21
+ <div className={styles.content}>{content}</div>
22
+ )}
23
+ </div>
24
+ <NavBar />
25
+ </div>
26
+ );
27
+ };
28
+
29
+ export default TabBarLayout;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useKeepOutlets, useLocation } from 'alita';
3
+ import type { LayoutName } from '@/routes';
4
+ import { getLayoutName, ssoLoginEnabled } from '@/routes';
5
+ import BlankLayout from './BlankLayout';
6
+ import HeaderLayout from './HeaderLayout';
7
+ import SSOLoginLayout from './SSOLoginLayout';
8
+ import TabBarLayout from './TabBarLayout';
9
+
10
+ const layoutMap: Record<LayoutName, React.ComponentType<{ children?: React.ReactNode }>> = {
11
+ tabBar: TabBarLayout,
12
+ header: HeaderLayout,
13
+ blank: BlankLayout,
14
+ };
15
+
16
+ /**
17
+ * 全局外壳:SSO 闸门 + keepalive + 按 src/routes.ts 选择布局。
18
+ * 改 routes.ts(含 ssoLoginEnabled)走 HMR,不用重启 dev。
19
+ */
20
+ export default function Layout() {
21
+ const { pathname } = useLocation();
22
+ const element = useKeepOutlets();
23
+ const LayoutComp = layoutMap[getLayoutName(pathname)];
24
+
25
+ return (
26
+ <SSOLoginLayout enabled={ssoLoginEnabled}>
27
+ <LayoutComp>{element}</LayoutComp>
28
+ </SSOLoginLayout>
29
+ );
30
+ }
@@ -0,0 +1,9 @@
1
+ import { useLocation } from 'alita';
2
+ import { getHeaderProps } from '@/routes';
3
+ import type { HeaderProps } from '@/components/Header';
4
+
5
+ /** 从 src/routes.ts 读取当前页 headerProps;未配置则不展示 Header */
6
+ export function usePageHeaderProps(): HeaderProps | undefined {
7
+ const { pathname } = useLocation();
8
+ return getHeaderProps(pathname);
9
+ }
@@ -3,15 +3,15 @@
3
3
  flex-direction: column;
4
4
  align-items: center;
5
5
  padding: var(--spacing-xl);
6
- min-height: 100vh;
6
+ min-height: 100%;
7
7
  background: var(--color-background-bg);
8
8
  }
9
9
 
10
10
  .welcome {
11
- width: 200px;
12
- height: 200px;
13
- background: url('~@/assets/yay.jpg') no-repeat center 0;
14
- background-size: 200px 200px;
11
+ width: 246px;
12
+ height: 184px;
13
+ background: url('~@/assets/welcome.png') no-repeat center 0;
14
+ background-size: 246px 184px;
15
15
  border-radius: var(--radius-card);
16
16
  margin-top: var(--spacing-xxl);
17
17
  }
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import { useNavigate } from 'alita';
2
3
  import { Button } from 'antd-mobile';
3
4
  import styles from './index.css';
@@ -8,10 +9,9 @@ export default function () {
8
9
  <div className={styles.normal}>
9
10
  <div className={styles.welcome} />
10
11
  <p className={styles.description}>
11
- To get started, edit <code>src/pages/index.js</code> and save to reload.
12
+ Welcome to VibeNote application development.
12
13
  </p>
13
14
  <Button
14
- size="large"
15
15
  color="primary"
16
16
  fill="solid"
17
17
  block
@@ -23,6 +23,19 @@ export default function () {
23
23
  >
24
24
  Go to List
25
25
  </Button>
26
+ <Button
27
+ color="primary"
28
+ fill="outline"
29
+ block
30
+ onClick={() => navigate('/xiaozhi-chat')}
31
+ style={{
32
+ marginTop: 'var(--spacing-md)',
33
+ borderRadius: 'var(--radius-button)',
34
+ fontWeight: 'var(--font-weight-medium)',
35
+ }}
36
+ >
37
+ xiaozhi-chat
38
+ </Button>
26
39
  </div>
27
40
  );
28
41
  }
@@ -1,7 +1,7 @@
1
- import { Content, Header, Page } from '@alita/flow';
1
+ import { Content, Header as PageHeader, Page } from '@alita/flow';
2
2
  import { useNavigate } from 'alita';
3
3
  import { InfiniteScroll, List } from 'antd-mobile';
4
- import { useEffect, useState } from 'react';
4
+ import React, { useEffect, useState } from 'react';
5
5
  import SearchBar from '../../components/SearchBar';
6
6
 
7
7
  let count = 0;
@@ -47,9 +47,9 @@ export default () => {
47
47
  }, []);
48
48
  return (
49
49
  <Page>
50
- <Header>
51
- <SearchBar placeholder="请输入内容" />
52
- </Header>
50
+ <PageHeader>
51
+ <SearchBar />
52
+ </PageHeader>
53
53
  <Content>
54
54
  <List>
55
55
  {data.map((item, index) => (
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { useNavigate } from 'alita';
3
+ import { Button } from 'antd-mobile';
4
+
5
+ export default function () {
6
+ const navigate = useNavigate();
7
+ return (
8
+ <div vibe-element="safe-area-view" vibe-edges="top">
9
+ 2111
10
+ </div>
11
+ );
12
+ }
@@ -0,0 +1,4 @@
1
+ .wrap {
2
+ height: 100%;
3
+ overflow: hidden;
4
+ }
@@ -0,0 +1,26 @@
1
+ import React, { useCallback } from 'react';
2
+ import { useNavigate } from 'alita';
3
+ import { XiaozhiChatPage } from '@talknote/xiaozhi-chat';
4
+ import styles from './index.less';
5
+
6
+ /**
7
+ * Ask AI 路由壳:业务实现在 packages/xiaozhi-chat
8
+ * 布局为 BlankLayout,见 src/routes.ts
9
+ */
10
+ export default () => {
11
+ const navigate = useNavigate();
12
+
13
+ const handleBack = useCallback(() => {
14
+ if (window.history.length > 1) {
15
+ navigate(-1);
16
+ return;
17
+ }
18
+ navigate('/', { replace: true });
19
+ }, [navigate]);
20
+
21
+ return (
22
+ <div className={styles.wrap}>
23
+ <XiaozhiChatPage title="Ask AI" onBack={handleBack} />
24
+ </div>
25
+ );
26
+ };
@@ -0,0 +1,49 @@
1
+ import type { HeaderProps } from '@/components/Header';
2
+
3
+ export type LayoutName = 'tabBar' | 'header' | 'blank';
4
+
5
+ export type AppRoute = {
6
+ path: string;
7
+
8
+ /** tabBar:底栏;header:仅顶栏;blank:都没有 */
9
+ layout: LayoutName;
10
+
11
+ /** 不配则不展示 Header */
12
+ headerProps?: HeaderProps;
13
+ };
14
+
15
+ /** Top-level SSOLoginLayout. false = skip ticket login and enter the app. */
16
+ export const ssoLoginEnabled = false;
17
+
18
+ // 系统使用约定路由(src/pages 下各目录的 index.tsx),此处仅为路由配置layout 和 header,如不配置 layout 使用 blank、header不展示。
19
+ const routes: AppRoute[] = [
20
+ {
21
+ path: '/',
22
+ layout: 'tabBar',
23
+ headerProps: { title: '首页', showBack: false },
24
+ },
25
+ {
26
+ path: '/list',
27
+ layout: 'tabBar',
28
+ headerProps: { title: '列表', showBack: false },
29
+ },
30
+ ];
31
+
32
+ function matchRoute(pathname: string): AppRoute | undefined {
33
+ const exact = routes.find((item) => item.path === pathname);
34
+ if (exact) {
35
+ return exact;
36
+ }
37
+
38
+ return [...routes]
39
+ .filter((item) => item.path !== '/' && pathname.startsWith(`${item.path}/`))
40
+ .sort((a, b) => b.path.length - a.path.length)[0];
41
+ }
42
+
43
+ export function getLayoutName(pathname: string): LayoutName {
44
+ return matchRoute(pathname)?.layout ?? 'blank';
45
+ }
46
+
47
+ export function getHeaderProps(pathname: string): HeaderProps | undefined {
48
+ return matchRoute(pathname)?.headerProps;
49
+ }
@@ -1,5 +1,10 @@
1
- import { request } from 'alita';
1
+ import { post, request } from '@/utils/request';
2
2
 
3
3
  export async function query(): Promise<any> {
4
- return request('/api/hello', { method: 'POST' });
4
+ return request('/api/hello', { method: 'POST', prefix: false, unwrap: false });
5
+ }
6
+
7
+ /** POST /oauth2/ticketLogin — plants session cookie */
8
+ export function ssoLogin(params: { ticket: string }) {
9
+ return post('/oauth2/ticketLogin', params);
5
10
  }
@@ -135,3 +135,8 @@
135
135
  --line-height-normal: 1.5;
136
136
  --line-height-relaxed: 1.75;
137
137
  }
138
+
139
+ /* 覆盖 antd-mobile 默认主色,与 --color-primary 保持一致 */
140
+ :root:root {
141
+ --adm-color-primary: var(--color-primary);
142
+ }
@@ -0,0 +1,28 @@
1
+ import { ssoLogin } from '@/services/api';
2
+
3
+ let ssoPromise: Promise<void> | null = null;
4
+ let ssoSucceeded = false;
5
+
6
+ export const hasSsoSucceeded = () => ssoSucceeded;
7
+
8
+ /** First entry: get ticket from native bridge, then ticketLogin. Failures can retry. */
9
+ export const ensureSsoLogin = (): Promise<void> => {
10
+ if (ssoPromise) return ssoPromise;
11
+
12
+ const run = async () => {
13
+ const ticket = (await window.vibeNoteBridge?.getAuthTicket?.())?.ticket;
14
+ if (!ticket) {
15
+ throw new Error('empty ticket');
16
+ }
17
+ await ssoLogin({ ticket });
18
+ ssoSucceeded = true;
19
+ };
20
+
21
+ ssoPromise = run().catch((error) => {
22
+ ssoPromise = null;
23
+ ssoSucceeded = false;
24
+ throw error;
25
+ });
26
+
27
+ return ssoPromise;
28
+ };
@@ -0,0 +1,10 @@
1
+ import TalkNoteH5Bridge from '@talknote/h5-bridge';
2
+
3
+ /** 有浏览历史则后退,否则交给原生容器 */
4
+ export function goBack() {
5
+ if (window.history.length > 1) {
6
+ window.history.back();
7
+ return;
8
+ }
9
+ TalkNoteH5Bridge.goBack();
10
+ }
@@ -0,0 +1,106 @@
1
+ import { request as umiRequest } from 'alita';
2
+ import type { RequestOptionsInit } from 'alita';
3
+
4
+ export const API_PREFIX = process.env.API_PREFIX || './server/nes';
5
+ const SUCCESS_RESULT_CODE = '0';
6
+ const DEFAULT_TIMEOUT = 60_000;
7
+
8
+ export type ApiInnerResponse<T = unknown> = {
9
+ resultCode?: string | number;
10
+ resultObject?: T;
11
+ resultMsg?: string;
12
+ };
13
+
14
+ export type ApiResponse<T = unknown> = {
15
+ resultCode?: string | number;
16
+ resultObject?: ApiInnerResponse<T> | T;
17
+ resultMsg?: string;
18
+ };
19
+
20
+ export type AppRequestOptions = Omit<RequestOptionsInit, 'prefix'> & {
21
+
22
+ /** prepend API_PREFIX; default true */
23
+ prefix?: boolean;
24
+
25
+ /** check resultCode and unwrap resultObject; default true */
26
+ unwrap?: boolean;
27
+ };
28
+
29
+ export function withApiPrefix(path: string): string {
30
+ if (/^https?:\/\//.test(path) || path.startsWith(API_PREFIX)) {
31
+ return path;
32
+ }
33
+ const normalized = path.startsWith('/') ? path : `/${path}`;
34
+ return `${API_PREFIX}${normalized}`;
35
+ }
36
+
37
+ const isSuccessCode = (code: unknown) => String(code) === SUCCESS_RESULT_CODE;
38
+
39
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
40
+ value != null && typeof value === 'object' && !Array.isArray(value);
41
+
42
+ const getApiErrorMessage = (body: unknown, fallback: string) => {
43
+ if (!isRecord(body)) return fallback;
44
+ const inner = body.resultObject;
45
+ if (isRecord(inner) && typeof inner.resultMsg === 'string' && inner.resultMsg.trim()) {
46
+ return inner.resultMsg;
47
+ }
48
+ if (typeof body.resultMsg === 'string' && body.resultMsg.trim()) {
49
+ return body.resultMsg;
50
+ }
51
+ return fallback;
52
+ };
53
+
54
+ /** Single- or double-layer NES envelope */
55
+ export function unwrapApiData<T>(body: unknown): T {
56
+ if (body == null) {
57
+ throw new Error('业务请求失败');
58
+ }
59
+ if (!isRecord(body) || body.resultCode == null) {
60
+ return body as T;
61
+ }
62
+ if (!isSuccessCode(body.resultCode)) {
63
+ throw new Error(getApiErrorMessage(body, '业务请求失败'));
64
+ }
65
+
66
+ const inner = body.resultObject;
67
+ if (isRecord(inner) && inner.resultCode != null) {
68
+ if (!isSuccessCode(inner.resultCode)) {
69
+ throw new Error(getApiErrorMessage(body, '业务请求失败'));
70
+ }
71
+ return inner.resultObject as T;
72
+ }
73
+
74
+ return inner as T;
75
+ }
76
+
77
+ export async function request<T>(path: string, options: AppRequestOptions = {}): Promise<T> {
78
+ const { prefix = true, unwrap = true, ...rest } = options;
79
+ const url = prefix ? withApiPrefix(path) : path;
80
+
81
+ try {
82
+ const body = await umiRequest(url, {
83
+ credentials: 'include',
84
+ timeout: DEFAULT_TIMEOUT,
85
+ ...rest,
86
+ });
87
+ if (!unwrap) {
88
+ return body as T;
89
+ }
90
+ return unwrapApiData<T>(body);
91
+ } catch (error) {
92
+ if (error instanceof Error && error.message.trim()) {
93
+ throw error;
94
+ }
95
+ throw new Error('网络请求失败');
96
+ }
97
+ }
98
+
99
+ export const get = <T>(
100
+ path: string,
101
+ params?: Record<string, unknown>,
102
+ options?: AppRequestOptions,
103
+ ) => request<T>(path, { ...options, method: 'GET', params });
104
+
105
+ export const post = <T>(path: string, data?: unknown, options?: AppRequestOptions) =>
106
+ request<T>(path, { ...options, method: 'POST', data });
@@ -1,3 +1,6 @@
1
1
  {
2
- "extends": "./src/.umi/tsconfig.json"
2
+ "extends": "./src/.umi/tsconfig.json",
3
+ "compilerOptions": {
4
+ "jsx": "react"
5
+ }
3
6
  }
@@ -1 +1,16 @@
1
1
  import 'alita/typings';
2
+
3
+ declare module 'react' {
4
+ interface HTMLAttributes<T> {
5
+ 'vibe-element'?: string;
6
+ 'vibe-edges'?: string;
7
+ }
8
+ }
9
+
10
+ declare global {
11
+ interface Window {
12
+ vibeNoteBridge?: {
13
+ getAuthTicket?: () => Promise<{ ticket?: string }>;
14
+ };
15
+ }
16
+ }