@roki-h5/create-roki-app 0.1.0 → 0.1.1

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 (53) hide show
  1. package/dist/index.js +15 -1
  2. package/package.json +1 -1
  3. package/templates/h5/config/env/.env.development +7 -0
  4. package/templates/h5/config/env/.env.development.local +2 -0
  5. package/templates/h5/config/env/.env.production +9 -0
  6. package/templates/h5/config/env/.env.test +9 -0
  7. package/templates/h5/config/env.ts +10 -0
  8. package/templates/h5/index.html +1 -1
  9. package/templates/h5/node_modules/.bin/autoprefixer +17 -0
  10. package/templates/h5/node_modules/.bin/browserslist +17 -0
  11. package/templates/h5/node_modules/.bin/tsc +17 -0
  12. package/templates/h5/node_modules/.bin/tsserver +17 -0
  13. package/templates/h5/node_modules/.bin/vite +17 -0
  14. package/templates/h5/node_modules/.bin/vue-tsc +17 -0
  15. package/templates/h5/node_modules/.vite/deps/_metadata.json +43 -0
  16. package/templates/h5/node_modules/.vite/deps/axios.js +3370 -0
  17. package/templates/h5/node_modules/.vite/deps/axios.js.map +7 -0
  18. package/templates/h5/node_modules/.vite/deps/chunk-CVVHEZGK.js +162 -0
  19. package/templates/h5/node_modules/.vite/deps/chunk-CVVHEZGK.js.map +7 -0
  20. package/templates/h5/node_modules/.vite/deps/chunk-HFUUWO4F.js +13072 -0
  21. package/templates/h5/node_modules/.vite/deps/chunk-HFUUWO4F.js.map +7 -0
  22. package/templates/h5/node_modules/.vite/deps/chunk-PZ5AY32C.js +9 -0
  23. package/templates/h5/node_modules/.vite/deps/chunk-PZ5AY32C.js.map +7 -0
  24. package/templates/h5/node_modules/.vite/deps/package.json +3 -0
  25. package/templates/h5/node_modules/.vite/deps/pinia.js +1561 -0
  26. package/templates/h5/node_modules/.vite/deps/pinia.js.map +7 -0
  27. package/templates/h5/node_modules/.vite/deps/vue-router.js +2241 -0
  28. package/templates/h5/node_modules/.vite/deps/vue-router.js.map +7 -0
  29. package/templates/h5/node_modules/.vite/deps/vue.js +347 -0
  30. package/templates/h5/node_modules/.vite/deps/vue.js.map +7 -0
  31. package/templates/h5/node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts +118 -0
  32. package/templates/h5/package.json +9 -6
  33. package/templates/h5/scripts/version.js +38 -0
  34. package/templates/h5/src/App.vue +29 -0
  35. package/templates/h5/src/api/index.ts +4 -0
  36. package/templates/h5/src/api/nativeApi.ts +72 -0
  37. package/templates/h5/src/api/request.ts +2 -1
  38. package/templates/h5/src/api/welcome.ts +24 -0
  39. package/templates/h5/src/assets/images/home/img-device.png +0 -0
  40. package/templates/h5/src/assets/images/home/logo-free.png +0 -0
  41. package/templates/h5/src/assets/images/logo.png +0 -0
  42. package/templates/h5/src/components/WelcomeInfo.vue +90 -0
  43. package/templates/h5/src/router/index.ts +1 -1
  44. package/templates/h5/src/stores/index.ts +96 -1
  45. package/templates/h5/src/utils/appInfo.ts +86 -0
  46. package/templates/h5/src/utils/browser.ts +26 -0
  47. package/templates/h5/src/utils/buildInfo.ts +3 -0
  48. package/templates/h5/src/utils/greeting.ts +38 -0
  49. package/templates/h5/src/views/home/index.vue +12 -11
  50. package/templates/h5/src/vite-env.d.ts +26 -0
  51. package/templates/h5/tsconfig.json +3 -2
  52. package/templates/h5/vite.config.ts +56 -11
  53. package/templates/h5/.env.development +0 -1
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { storeToRefs } from 'pinia'
4
+ import logoSrc from '@/assets/images/home/logo-free.png'
5
+ import deviceImg from '@/assets/images/home/img-device.png'
6
+ import { useAppStore } from '@/stores'
7
+
8
+ const { welcomeCopy } = storeToRefs(useAppStore())
9
+
10
+ const prefixText = computed(() => {
11
+ const prefix = welcomeCopy.value?.prefix
12
+ return prefix && String(prefix).trim() ? String(prefix).trim() : ''
13
+ })
14
+
15
+ const speechText = computed(() => {
16
+ const speech = welcomeCopy.value?.speech
17
+ return speech && String(speech).trim() ? String(speech).trim() : ''
18
+ })
19
+ </script>
20
+
21
+ <template>
22
+ <section class="welcome-info">
23
+ <div class="welcome-info__left">
24
+ <img class="welcome-info__logo" :src="logoSrc" alt="ROKI" />
25
+ <h3 v-if="prefixText" class="welcome-info__prefix">{{ prefixText }}</h3>
26
+ <p v-if="speechText" class="welcome-info__speech">{{ speechText }}</p>
27
+ </div>
28
+
29
+ <div class="welcome-info__right">
30
+ <img class="welcome-info__device" :src="deviceImg" alt="" />
31
+ </div>
32
+ </section>
33
+ </template>
34
+
35
+ <style scoped>
36
+ .welcome-info {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: space-between;
40
+ column-gap: 16px;
41
+ margin-bottom: 24px;
42
+ }
43
+
44
+ .welcome-info__left {
45
+ min-width: 0;
46
+ padding-top: 6px;
47
+ margin-left: 32px;
48
+ line-height: 1.5;
49
+ }
50
+
51
+ .welcome-info__logo {
52
+ display: block;
53
+ position: relative;
54
+ top: 0;
55
+ right: 4px;
56
+ width: 68px;
57
+ height: 68px;
58
+ border-radius: 14px;
59
+ }
60
+
61
+ .welcome-info__prefix {
62
+ margin: 8px 0 4px;
63
+ background: linear-gradient(90deg, #005da7 0%, #f87b4d 50%);
64
+ -webkit-background-clip: text;
65
+ background-clip: text;
66
+ color: transparent;
67
+ font-size: 20px;
68
+ font-weight: 400;
69
+ line-height: 1.4;
70
+ }
71
+
72
+ .welcome-info__speech {
73
+ margin: 0;
74
+ color: rgba(0, 0, 0, 0.8);
75
+ font-size: 12px;
76
+ line-height: 1.5;
77
+ }
78
+
79
+ .welcome-info__right {
80
+ position: relative;
81
+ flex-shrink: 0;
82
+ width: 200px;
83
+ height: 200px;
84
+ }
85
+
86
+ .welcome-info__device {
87
+ display: block;
88
+ width: 100%;
89
+ }
90
+ </style>
@@ -8,7 +8,7 @@ const router = createRouter({
8
8
  path: '/',
9
9
  name: 'Home',
10
10
  component: Home,
11
- meta: { title: '__PROJECT_NAME__' },
11
+ meta: { title: import.meta.env.VITE_APP_TITLE },
12
12
  },
13
13
  ],
14
14
  scrollBehavior: () => ({ top: 0 }),
@@ -1,15 +1,110 @@
1
1
  import { ref } from 'vue'
2
2
  import { defineStore } from 'pinia'
3
+ import { getWelcomeMessage, type WelcomeMessageData } from '@/api/welcome'
4
+
5
+ function formatRequestDate(date = new Date()) {
6
+ const pad = (n: number) => String(n).padStart(2, '0')
7
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
8
+ }
9
+
10
+ /** 可通过 updateState 批量更新的字段 */
11
+ export interface AppStateUpdates {
12
+ terminal?: string
13
+ accessToken?: string
14
+ familyId?: string
15
+ deviceType?: string
16
+ deviceCategory?: string
17
+ deviceName?: string
18
+ deviceId?: string
19
+ deviceGuid?: string
20
+ userId?: string
21
+ }
3
22
 
4
23
  export const useAppStore = defineStore('app', () => {
5
- const accessToken = ref('')
24
+ /** 应用展示名,可在启动时由接口或配置覆盖 */
25
+ const appName = ref('Roki H5')
26
+ const terminal = ref('android')
27
+ const deviceName = ref('燃气热水器·HT808-16')
28
+ const familyId = ref('')
29
+ const deviceId = ref('HT80888a68d0fb812')
30
+ const deviceGuid = ref('HT80888a68d0fb812')
31
+ const deviceType = ref('HT808')
32
+ const deviceCategory = ref('RRSQ')
33
+ const userId = ref('3243300064')
34
+
35
+ /** 开发联调用 token,生产环境由 App 注入覆盖 */
36
+ const accessToken = ref(
37
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE4MDI5Mjg0ODAsInVzZXJJZCI6MzI0MzMwMDA2NCwianRpIjoiMDI3ZjhiMGMtYmU5MS00NzA2LWI3ZDUtOGI5OTJlMjA0MzQ2IiwiY2xpZW50X2lkIjoicm9raV9jbGllbnQifQ.aW0hKvcv-k_4PocYNVWiml7IB15jZIcn24eXZFZCSz4',
38
+ )
39
+
40
+ /** `/rest/ops/ai/welcome` 的 data:prefix / speech */
41
+ const welcomeCopy = ref<WelcomeMessageData>({})
42
+
43
+ function setAppName(name: string) {
44
+ appName.value = name
45
+ }
6
46
 
7
47
  function setAccessToken(token: string) {
8
48
  accessToken.value = token
9
49
  }
10
50
 
51
+ function resetWelcomeCopy() {
52
+ welcomeCopy.value = {}
53
+ }
54
+
55
+ async function loadWelcomeMessage(params: Record<string, unknown> = {}) {
56
+ try {
57
+ const res = await getWelcomeMessage({
58
+ date: formatRequestDate(),
59
+ ...params,
60
+ })
61
+ welcomeCopy.value = res?.data ?? {}
62
+ } catch (e) {
63
+ welcomeCopy.value = {}
64
+ if (import.meta.env.DEV) {
65
+ console.warn('[App] loadWelcomeMessage failed', e)
66
+ }
67
+ }
68
+ }
69
+
70
+ function updateState(updates: AppStateUpdates) {
71
+ const stateRefs = {
72
+ terminal,
73
+ accessToken,
74
+ familyId,
75
+ deviceType,
76
+ deviceCategory,
77
+ deviceName,
78
+ deviceId,
79
+ deviceGuid,
80
+ userId,
81
+ } as const
82
+
83
+ Object.entries(updates).forEach(([key, value]) => {
84
+ if (key in stateRefs) {
85
+ stateRefs[key as keyof typeof stateRefs].value = value as never
86
+ } else if (import.meta.env.DEV) {
87
+ console.warn(`[App] updateState: unknown key "${key}"`)
88
+ }
89
+ })
90
+ }
91
+
11
92
  return {
93
+ appName,
94
+ terminal,
95
+ deviceName,
96
+ familyId,
97
+ deviceId,
98
+ deviceGuid,
99
+ deviceType,
100
+ deviceCategory,
101
+ userId,
12
102
  accessToken,
103
+ welcomeCopy,
104
+ setAppName,
13
105
  setAccessToken,
106
+ resetWelcomeCopy,
107
+ loadWelcomeMessage,
108
+ updateState,
14
109
  }
15
110
  })
@@ -0,0 +1,86 @@
1
+ import { useAppStore } from '@/stores'
2
+
3
+ export interface AppDeviceInfo {
4
+ deviceId?: string
5
+ deviceName?: string
6
+ deviceType?: string
7
+ deviceCategory?: string
8
+ familyId?: string
9
+ userId?: string
10
+ terminal?: string
11
+ accessToken?: string
12
+ }
13
+
14
+ export function patchAppStoreFromInfo(rawInfo: unknown) {
15
+ if (!rawInfo) return null
16
+
17
+ try {
18
+ const info =
19
+ typeof rawInfo === 'string'
20
+ ? (JSON.parse(rawInfo) as AppDeviceInfo)
21
+ : (rawInfo as AppDeviceInfo)
22
+
23
+ const {
24
+ deviceId,
25
+ deviceName,
26
+ deviceType,
27
+ deviceCategory,
28
+ familyId,
29
+ userId,
30
+ terminal,
31
+ accessToken,
32
+ } = info
33
+
34
+ const appStore = useAppStore()
35
+ const prevGuid = String(appStore.deviceGuid ?? '').trim()
36
+ const nextGuid = String(deviceId ?? '').trim()
37
+
38
+ appStore.updateState({
39
+ deviceId,
40
+ deviceName,
41
+ deviceGuid: deviceId,
42
+ deviceType,
43
+ deviceCategory,
44
+ familyId,
45
+ userId,
46
+ terminal,
47
+ accessToken,
48
+ })
49
+
50
+ if (nextGuid && nextGuid !== prevGuid) {
51
+ appStore.resetWelcomeCopy()
52
+ void appStore.loadWelcomeMessage()
53
+ }
54
+
55
+ return info
56
+ } catch (e) {
57
+ console.warn('[appInfo] 设备信息解析失败', e)
58
+ return null
59
+ }
60
+ }
61
+
62
+ /** 从混合 App 原生桥获取设备信息并写入 store */
63
+ export async function getAppData() {
64
+ let info: unknown = null
65
+
66
+ if (
67
+ window.nativeInterface &&
68
+ typeof window.nativeInterface.getDeviceInfo === 'function'
69
+ ) {
70
+ info = await Promise.resolve(window.nativeInterface.getDeviceInfo())
71
+ if (import.meta.env.DEV) {
72
+ console.log('[appInfo] 安卓/鸿蒙数据获取成功', info)
73
+ }
74
+ } else if (window.getDeviceInfo) {
75
+ const raw = window.getDeviceInfo
76
+ info = await Promise.resolve(
77
+ typeof raw === 'function' ? raw() : raw,
78
+ )
79
+ if (import.meta.env.DEV) {
80
+ console.log('[appInfo] iOS 数据获取成功', info)
81
+ }
82
+ }
83
+
84
+ if (!info) return null
85
+ return patchAppStoreFromInfo(info)
86
+ }
@@ -0,0 +1,26 @@
1
+ /** 轻量 UA 判断,供 JsBridge / nativeApi 区分 iOS 与 Android */
2
+ function getUa() {
3
+ return typeof navigator !== 'undefined' && navigator.userAgent
4
+ ? navigator.userAgent
5
+ : ''
6
+ }
7
+
8
+ const ua = getUa()
9
+
10
+ const android = /Android/i.test(ua)
11
+ const iPhone = /iPhone/i.test(ua) && !/iPod/i.test(ua)
12
+ const iPod = /iPod/i.test(ua)
13
+ const iPad =
14
+ /iPad/i.test(ua) ||
15
+ (typeof navigator !== 'undefined' &&
16
+ navigator.platform === 'MacIntel' &&
17
+ Number(navigator.maxTouchPoints) > 1)
18
+
19
+ export const browser = {
20
+ versions: {
21
+ android,
22
+ iPhone,
23
+ iPad,
24
+ iPod,
25
+ },
26
+ }
@@ -0,0 +1,3 @@
1
+ // 此文件由构建脚本自动生成,请勿手动修改
2
+ export const BUILD_TIME = '—'
3
+ export const VERSION = '0.0.0'
@@ -0,0 +1,38 @@
1
+ /** 时段及问候语 与后端 getGreeting 枚举一致 前端兜底方案 避免接口请求失败时展示不正确 */
2
+ export const TIME_PERIOD_GREETINGS = [
3
+ '早上好',
4
+ '中午好',
5
+ '下午好',
6
+ '傍晚好',
7
+ '夜深了',
8
+ ] as const
9
+
10
+ export function getGreetingByTime(dateTime = new Date()) {
11
+ const hours = dateTime.getHours()
12
+ const minutes = dateTime.getMinutes()
13
+ const totalMinutes = hours * 60 + minutes
14
+
15
+ if (totalMinutes >= 5 * 60 + 1 && totalMinutes <= 10 * 60) return '早上好'
16
+ if (totalMinutes >= 10 * 60 + 1 && totalMinutes <= 14 * 60) return '中午好'
17
+ if (totalMinutes >= 14 * 60 + 1 && totalMinutes <= 17 * 60) return '下午好'
18
+ if (totalMinutes >= 17 * 60 + 1 && totalMinutes <= 22 * 60) return '傍晚好'
19
+ return '夜深了'
20
+ }
21
+
22
+ export function extractTimePeriodGreeting(text: string) {
23
+ const t = String(text ?? '').trim()
24
+ if (!t) return ''
25
+ if (t === '深夜了' || t.includes('深夜')) return '夜深了'
26
+ return TIME_PERIOD_GREETINGS.find((greeting) => t === greeting) ?? ''
27
+ }
28
+
29
+ /** 解析首页问候 prefix:无接口数据或时段不一致时以本地时间为准 */
30
+ export function resolveWelcomePrefix(apiPrefix?: string, dateTime = new Date()) {
31
+ const localGreeting = getGreetingByTime(dateTime)
32
+ const trimmed = String(apiPrefix ?? '').trim()
33
+ if (!trimmed) return localGreeting
34
+
35
+ const apiGreeting = extractTimePeriodGreeting(trimmed)
36
+ if (apiGreeting && apiGreeting !== localGreeting) return localGreeting
37
+ return trimmed
38
+ }
@@ -1,16 +1,21 @@
1
1
  <script setup lang="ts">
2
+ import WelcomeInfo from '@/components/WelcomeInfo.vue'
3
+ import { nativeApi } from '@/api/nativeApi'
4
+
5
+ const appTitle = import.meta.env.VITE_APP_TITLE
6
+
2
7
  function onBack() {
3
8
  history.back()
4
9
  }
5
10
 
6
11
  function onMore() {
7
- console.log('more')
12
+ void nativeApi.skipDeviceMore('5.0')
8
13
  }
9
14
  </script>
10
15
 
11
16
  <template>
12
17
  <div class="page home-page">
13
- <RokiNavBar title="__PROJECT_NAME__" @click-left="onBack">
18
+ <RokiNavBar :title="appTitle" @click-left="onBack">
14
19
  <template #right>
15
20
  <span
16
21
  role="button"
@@ -18,14 +23,17 @@ function onMore() {
18
23
  class="home-page__more"
19
24
  aria-label="更多"
20
25
  @click.stop="onMore"
26
+ @keydown.enter.prevent="onMore"
27
+ @keydown.space.prevent="onMore"
21
28
  >
22
29
 
23
30
  </span>
24
31
  </template>
25
32
  </RokiNavBar>
26
33
 
34
+ <WelcomeInfo />
35
+
27
36
  <main class="home-page__content">
28
- <p class="home-page__tip">向下滚动,导航栏背景会从透明渐变为白色。</p>
29
37
  <div class="home-page__spacer" />
30
38
  </main>
31
39
  </div>
@@ -33,14 +41,7 @@ function onMore() {
33
41
 
34
42
  <style scoped>
35
43
  .home-page__content {
36
- padding: 24px 18px;
37
- }
38
-
39
- .home-page__tip {
40
- margin: 0;
41
- font-size: 14px;
42
- color: rgba(0, 0, 0, 0.6);
43
- line-height: 1.6;
44
+ padding: 0 18px 24px;
44
45
  }
45
46
 
46
47
  .home-page__more {
@@ -1,9 +1,35 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
+ declare module '*.png' {
4
+ const src: string
5
+ export default src
6
+ }
7
+
3
8
  interface ImportMetaEnv {
9
+ readonly VITE_ENV: string
10
+ readonly VITE_APP_TITLE: string
4
11
  readonly VITE_BASE_API: string
12
+ readonly VITE_NORMALDEVICE_BASE_API: string
13
+ readonly VITE_PUBLIC_BASE?: string
5
14
  }
6
15
 
7
16
  interface ImportMeta {
8
17
  readonly env: ImportMetaEnv
9
18
  }
19
+
20
+ interface WebViewJavascriptBridge {
21
+ callHandler: (
22
+ handlerName: string,
23
+ params: Record<string, unknown>,
24
+ callback: (responseData: unknown) => void,
25
+ ) => void
26
+ }
27
+
28
+ interface Window {
29
+ WebViewJavascriptBridge?: WebViewJavascriptBridge
30
+ WVJBCallbacks?: Array<(bridge: WebViewJavascriptBridge) => void>
31
+ nativeInterface?: {
32
+ getDeviceInfo?: () => unknown
33
+ }
34
+ getDeviceInfo?: (() => unknown) | unknown
35
+ }
@@ -13,8 +13,9 @@
13
13
  "noEmit": true,
14
14
  "baseUrl": ".",
15
15
  "paths": {
16
- "@/*": ["src/*"]
16
+ "@/*": ["src/*"],
17
+ "@config/*": ["config/*"]
17
18
  }
18
19
  },
19
- "include": ["src/**/*.ts", "src/**/*.vue"]
20
+ "include": ["src/**/*.ts", "src/**/*.vue", "config/env.ts"]
20
21
  }
@@ -1,19 +1,64 @@
1
+ import fs from 'node:fs'
1
2
  import path from 'node:path'
2
3
  import { fileURLToPath } from 'node:url'
3
- import { defineConfig } from 'vite'
4
+ import { defineConfig, loadEnv } from 'vite'
4
5
  import vue from '@vitejs/plugin-vue'
5
6
 
6
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+ const monorepoUiDir = path.resolve(__dirname, '../../../ui')
9
+ const isMonorepoDev = fs.existsSync(path.join(monorepoUiDir, 'package.json'))
7
10
 
8
- export default defineConfig({
9
- plugins: [vue()],
10
- resolve: {
11
- alias: {
12
- '@': path.resolve(__dirname, 'src'),
11
+ export default defineConfig(({ mode }) => {
12
+ const env = loadEnv(mode, path.resolve(__dirname, 'config/env'), '')
13
+
14
+ const rawPublicBase = (env.VITE_PUBLIC_BASE ?? '').trim()
15
+ const base =
16
+ mode === 'development'
17
+ ? '/'
18
+ : rawPublicBase === './' || rawPublicBase === '.'
19
+ ? './'
20
+ : rawPublicBase
21
+ ? rawPublicBase.endsWith('/')
22
+ ? rawPublicBase
23
+ : `${rawPublicBase}/`
24
+ : './'
25
+
26
+ return {
27
+ plugins: [vue()],
28
+ envDir: path.resolve(__dirname, 'config/env'),
29
+ resolve: {
30
+ alias: [
31
+ { find: '@', replacement: path.resolve(__dirname, 'src') },
32
+ { find: '@config', replacement: path.resolve(__dirname, 'config') },
33
+ ...(isMonorepoDev
34
+ ? [
35
+ {
36
+ find: '@roki-h5/ui/style.css',
37
+ replacement: path.resolve(
38
+ monorepoUiDir,
39
+ 'src/styles/base.css',
40
+ ),
41
+ },
42
+ {
43
+ find: '@roki-h5/ui',
44
+ replacement: path.resolve(monorepoUiDir, 'src/index.ts'),
45
+ },
46
+ ]
47
+ : []),
48
+ ],
49
+ },
50
+ base,
51
+ server: {
52
+ host: true,
53
+ port: 5174,
54
+ cors: true,
55
+ proxy: {
56
+ '/api': {
57
+ target: 'https://api-test.myroki.com',
58
+ changeOrigin: true,
59
+ rewrite: (p) => p.replace(/^\/api/, ''),
60
+ },
61
+ },
13
62
  },
14
- },
15
- server: {
16
- host: true,
17
- port: 5173,
18
- },
63
+ }
19
64
  })
@@ -1 +0,0 @@
1
- VITE_BASE_API=