@roki-h5/create-roki-app 0.1.0

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.
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { cyan, green, red } from "kolorist";
8
+ import prompts from "prompts";
9
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ var TEMPLATE_DIR = path.resolve(__dirname, "../templates/h5");
11
+ async function main() {
12
+ const args = process.argv.slice(2);
13
+ let projectName = args[0];
14
+ if (!projectName) {
15
+ const result = await prompts({
16
+ type: "text",
17
+ name: "name",
18
+ message: "\u9879\u76EE\u540D\u79F0",
19
+ initial: "roki-h5",
20
+ validate: (value) => value.trim() ? true : "\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A"
21
+ });
22
+ if (!result.name) {
23
+ console.log(red("\u5DF2\u53D6\u6D88"));
24
+ process.exit(1);
25
+ }
26
+ projectName = result.name.trim();
27
+ }
28
+ const targetDir = path.resolve(process.cwd(), projectName);
29
+ if (fs.existsSync(targetDir)) {
30
+ console.log(red(`\u76EE\u5F55 ${projectName} \u5DF2\u5B58\u5728`));
31
+ process.exit(1);
32
+ }
33
+ copyDir(TEMPLATE_DIR, targetDir, projectName);
34
+ console.log();
35
+ console.log(green(`\u2713 \u9879\u76EE ${projectName} \u521B\u5EFA\u6210\u529F`));
36
+ console.log();
37
+ console.log(` cd ${projectName}`);
38
+ console.log(" pnpm install");
39
+ console.log(" pnpm dev");
40
+ console.log();
41
+ }
42
+ function copyDir(src, dest, projectName) {
43
+ fs.mkdirSync(dest, { recursive: true });
44
+ for (const file of fs.readdirSync(src)) {
45
+ const srcPath = path.join(src, file);
46
+ const destPath = path.join(dest, file);
47
+ if (fs.statSync(srcPath).isDirectory()) {
48
+ copyDir(srcPath, destPath, projectName);
49
+ continue;
50
+ }
51
+ if (isBinaryAsset(file)) {
52
+ fs.copyFileSync(srcPath, destPath);
53
+ } else {
54
+ let content = fs.readFileSync(srcPath, "utf-8");
55
+ content = content.replace(/__PROJECT_NAME__/g, projectName);
56
+ fs.writeFileSync(destPath, content);
57
+ }
58
+ console.log(cyan(` + ${path.relative(process.cwd(), destPath)}`));
59
+ }
60
+ }
61
+ function isBinaryAsset(filename) {
62
+ return /\.(png|jpe?g|gif|webp|ico|woff2?|ttf|eot)$/i.test(filename);
63
+ }
64
+ main().catch((err) => {
65
+ console.error(red(String(err)));
66
+ process.exit(1);
67
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@roki-h5/create-roki-app",
3
+ "version": "0.1.0",
4
+ "description": "Roki H5 项目脚手架",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "http://172.18.10.96/260613/roki-ui.git",
10
+ "directory": "packages/create-roki"
11
+ },
12
+ "bin": {
13
+ "create-roki-app": "./dist/index.js"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "templates"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "registry": "https://registry.npmjs.org/"
25
+ },
26
+ "dependencies": {
27
+ "kolorist": "^1.8.0",
28
+ "prompts": "^2.4.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.10.2",
32
+ "@types/prompts": "^2.4.9",
33
+ "tsup": "^8.3.5",
34
+ "typescript": "^5.7.2"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "dev": "tsup --watch",
39
+ "typecheck": "tsc --noEmit"
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+ VITE_BASE_API=
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <!-- viewport-fit=cover 必须,否则 iOS 刘海屏 safe-area-inset-top 为 0 -->
6
+ <meta
7
+ name="viewport"
8
+ content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover"
9
+ />
10
+ <meta name="mobile-web-app-capable" content="yes" />
11
+ <meta name="format-detection" content="telephone=no" />
12
+ <meta name="apple-touch-fullscreen" content="yes" />
13
+ <title>__PROJECT_NAME__</title>
14
+ </head>
15
+ <body>
16
+ <div id="app" class="roki-reset"></div>
17
+ <script type="module" src="/src/main.ts"></script>
18
+ </body>
19
+ </html>
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vue-tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@roki-h5/ui": "^0.1.0",
13
+ "axios": "^1.7.9",
14
+ "pinia": "^2.3.0",
15
+ "vue": "^3.5.13",
16
+ "vue-router": "^4.5.0"
17
+ },
18
+ "devDependencies": {
19
+ "@vitejs/plugin-vue": "^5.2.1",
20
+ "autoprefixer": "^10.4.20",
21
+ "postcss": "^8.4.49",
22
+ "postcss-pxtorem": "^6.1.0",
23
+ "typescript": "^5.7.2",
24
+ "vite": "^6.0.3",
25
+ "vue-tsc": "^2.1.10"
26
+ }
27
+ }
@@ -0,0 +1,17 @@
1
+ import autoprefixer from 'autoprefixer'
2
+ import postcssPxtorem from 'postcss-pxtorem'
3
+
4
+ export default {
5
+ plugins: [
6
+ autoprefixer(),
7
+ postcssPxtorem({
8
+ rootValue: 37.5,
9
+ unitPrecision: 6,
10
+ propList: ['*'],
11
+ selectorBlackList: ['#app', '.ignore-vw'],
12
+ replace: true,
13
+ mediaQuery: false,
14
+ minPixelValue: 1,
15
+ }),
16
+ ],
17
+ }
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <router-view />
3
+ </template>
@@ -0,0 +1,9 @@
1
+ import { devReq } from './request'
2
+
3
+ /** 示例:可按业务拆分 api 模块 */
4
+ export function fetchExample() {
5
+ return devReq({
6
+ url: '/example',
7
+ method: 'get',
8
+ })
9
+ }
@@ -0,0 +1,78 @@
1
+ import axios, { type AxiosRequestConfig } from 'axios'
2
+ import { useAppStore } from '@/stores'
3
+
4
+ function isRequestTimeout(error: unknown) {
5
+ if (!error || typeof error !== 'object') return false
6
+ const err = error as {
7
+ code?: string
8
+ message?: string
9
+ name?: string
10
+ }
11
+ return (
12
+ err.code === 'ECONNABORTED' ||
13
+ err.code === 'ETIMEDOUT' ||
14
+ (typeof err.message === 'string' &&
15
+ (err.message.toLowerCase().includes('timeout') ||
16
+ err.message.toLowerCase().includes('timed out'))) ||
17
+ err.name === 'TimeoutError'
18
+ )
19
+ }
20
+
21
+ function showError(message: string) {
22
+ console.warn('[API]', message)
23
+ }
24
+
25
+ /** 业务请求封装,成功条件:rc === 0 或 success === true */
26
+ export function devReq<T = unknown>(config: AxiosRequestConfig) {
27
+ return new Promise<T>((resolve, reject) => {
28
+ const appStore = useAppStore()
29
+ const instance = axios.create({
30
+ headers: {
31
+ 'Content-Type': 'application/json;charset=UTF-8',
32
+ 'app-id': 'roki_app_h5',
33
+ Authorization: `Bearer ${appStore.accessToken}`,
34
+ },
35
+ timeout: 10000,
36
+ baseURL: config.baseURL || import.meta.env.VITE_BASE_API,
37
+ })
38
+
39
+ instance(config)
40
+ .then((res) => {
41
+ const data = res.data as {
42
+ rc?: number
43
+ success?: boolean
44
+ msg?: string
45
+ message?: string
46
+ error_description?: string
47
+ error?: string
48
+ }
49
+ const ok = data.rc === 0 || data.success
50
+ if (ok) {
51
+ resolve(res.data as T)
52
+ return
53
+ }
54
+ const errMsg =
55
+ data.msg ||
56
+ data.message ||
57
+ data.error_description ||
58
+ data.error ||
59
+ '请求失败'
60
+ showError(errMsg)
61
+ reject(errMsg)
62
+ })
63
+ .catch((err) => {
64
+ if (err.response?.status === 0) {
65
+ reject(err)
66
+ return
67
+ }
68
+ if (isRequestTimeout(err)) {
69
+ showError('请求超时,请检查网络状态')
70
+ reject(err)
71
+ return
72
+ }
73
+ const desc = err.response?.data?.error_description
74
+ showError(desc || '请求异常')
75
+ reject(err)
76
+ })
77
+ })
78
+ }
@@ -0,0 +1,21 @@
1
+ html,
2
+ body {
3
+ margin: 0;
4
+ padding: 0;
5
+ background: #f5f6f8;
6
+ }
7
+
8
+ #app {
9
+ min-height: 100vh;
10
+ min-height: 100dvh;
11
+ position: relative;
12
+ isolation: isolate;
13
+ background:
14
+ url('../images/top-bg.png') no-repeat top center / 100% 800px,
15
+ #f0f2f5;
16
+ }
17
+
18
+ .page {
19
+ min-height: 100vh;
20
+ min-height: 100dvh;
21
+ }
@@ -0,0 +1,16 @@
1
+ import { createApp } from 'vue'
2
+ import { createPinia } from 'pinia'
3
+ import RokiUI from '@roki-h5/ui'
4
+ import '@roki-h5/ui/style.css'
5
+ import { setupRem } from '@/utils/rem'
6
+ import router from '@/router'
7
+ import App from './App.vue'
8
+ import '@/assets/styles/main.css'
9
+
10
+ setupRem()
11
+
12
+ const app = createApp(App)
13
+ app.use(createPinia())
14
+ app.use(router)
15
+ app.use(RokiUI)
16
+ app.mount('#app')
@@ -0,0 +1,24 @@
1
+ import { createRouter, createWebHashHistory } from 'vue-router'
2
+ import Home from '@/views/home/index.vue'
3
+
4
+ const router = createRouter({
5
+ history: createWebHashHistory(import.meta.env.BASE_URL),
6
+ routes: [
7
+ {
8
+ path: '/',
9
+ name: 'Home',
10
+ component: Home,
11
+ meta: { title: '__PROJECT_NAME__' },
12
+ },
13
+ ],
14
+ scrollBehavior: () => ({ top: 0 }),
15
+ })
16
+
17
+ router.afterEach((to) => {
18
+ const title = to.meta?.title
19
+ if (title != null && title !== '') {
20
+ document.title = String(title)
21
+ }
22
+ })
23
+
24
+ export default router
@@ -0,0 +1,15 @@
1
+ import { ref } from 'vue'
2
+ import { defineStore } from 'pinia'
3
+
4
+ export const useAppStore = defineStore('app', () => {
5
+ const accessToken = ref('')
6
+
7
+ function setAccessToken(token: string) {
8
+ accessToken.value = token
9
+ }
10
+
11
+ return {
12
+ accessToken,
13
+ setAccessToken,
14
+ }
15
+ })
@@ -0,0 +1,20 @@
1
+ export function debounce<T extends (...args: unknown[]) => unknown>(
2
+ fn: T,
3
+ wait: number,
4
+ options: { leading?: boolean; trailing?: boolean } = {},
5
+ ) {
6
+ const { leading = false, trailing = true } = options
7
+ let timer: ReturnType<typeof setTimeout> | null = null
8
+
9
+ return function debounced(this: unknown, ...args: Parameters<T>) {
10
+ const invoke = () => {
11
+ timer = null
12
+ if (trailing) fn.apply(this, args)
13
+ }
14
+
15
+ const callNow = leading && !timer
16
+ if (timer) clearTimeout(timer)
17
+ timer = setTimeout(invoke, wait)
18
+ if (callNow) fn.apply(this, args)
19
+ }
20
+ }
@@ -0,0 +1,37 @@
1
+ /** 375 设计稿 rem 自适应 */
2
+ export function setupRem() {
3
+ const baseSize = 37.5
4
+ const designWidth = 375
5
+ let lastWidth = window.innerWidth
6
+ let timer: ReturnType<typeof setTimeout> | null = null
7
+
8
+ function setRem() {
9
+ const isLandscape = window.innerWidth > window.innerHeight
10
+ const baseWidth = isLandscape ? window.innerHeight : window.innerWidth
11
+ const maxWidth = 375
12
+ const limitedWidth = Math.min(baseWidth, maxWidth)
13
+ const scale = limitedWidth / designWidth
14
+ const fontSize = baseSize * Math.min(scale, 2)
15
+ document.documentElement.style.fontSize = `${fontSize}px`
16
+ }
17
+
18
+ function debounceSetRem() {
19
+ if (timer) clearTimeout(timer)
20
+ timer = setTimeout(setRem, 100)
21
+ }
22
+
23
+ setRem()
24
+
25
+ window.addEventListener('resize', () => {
26
+ if (lastWidth !== window.innerWidth) {
27
+ lastWidth = window.innerWidth
28
+ debounceSetRem()
29
+ }
30
+ })
31
+
32
+ window.addEventListener('orientationchange', debounceSetRem)
33
+
34
+ window.addEventListener('pageshow', (event) => {
35
+ if (event.persisted) setRem()
36
+ })
37
+ }
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ function onBack() {
3
+ history.back()
4
+ }
5
+
6
+ function onMore() {
7
+ console.log('more')
8
+ }
9
+ </script>
10
+
11
+ <template>
12
+ <div class="page home-page">
13
+ <RokiNavBar title="__PROJECT_NAME__" @click-left="onBack">
14
+ <template #right>
15
+ <span
16
+ role="button"
17
+ tabindex="0"
18
+ class="home-page__more"
19
+ aria-label="更多"
20
+ @click.stop="onMore"
21
+ >
22
+
23
+ </span>
24
+ </template>
25
+ </RokiNavBar>
26
+
27
+ <main class="home-page__content">
28
+ <p class="home-page__tip">向下滚动,导航栏背景会从透明渐变为白色。</p>
29
+ <div class="home-page__spacer" />
30
+ </main>
31
+ </div>
32
+ </template>
33
+
34
+ <style scoped>
35
+ .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
+ }
45
+
46
+ .home-page__more {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ width: 42px;
51
+ height: 42px;
52
+ font-size: 20px;
53
+ line-height: 1;
54
+ color: rgba(0, 0, 0, 0.85);
55
+ }
56
+
57
+ .home-page__spacer {
58
+ height: 120vh;
59
+ }
60
+ </style>
@@ -0,0 +1,9 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_BASE_API: string
5
+ }
6
+
7
+ interface ImportMeta {
8
+ readonly env: ImportMetaEnv
9
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "jsx": "preserve",
8
+ "resolveJsonModule": true,
9
+ "isolatedModules": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
13
+ "noEmit": true,
14
+ "baseUrl": ".",
15
+ "paths": {
16
+ "@/*": ["src/*"]
17
+ }
18
+ },
19
+ "include": ["src/**/*.ts", "src/**/*.vue"]
20
+ }
@@ -0,0 +1,19 @@
1
+ import path from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { defineConfig } from 'vite'
4
+ import vue from '@vitejs/plugin-vue'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ export default defineConfig({
9
+ plugins: [vue()],
10
+ resolve: {
11
+ alias: {
12
+ '@': path.resolve(__dirname, 'src'),
13
+ },
14
+ },
15
+ server: {
16
+ host: true,
17
+ port: 5173,
18
+ },
19
+ })