2 Commits 0801e0073c ... 5729f2ee34

Auteur SHA1 Bericht Datum
  王家程 5729f2ee34 feat: 支持tsx组件 10 maanden geleden
  王家程 eec340f309 feat: 多语言配置 10 maanden geleden

+ 5 - 1
.vscode/settings.json

@@ -1,3 +1,7 @@
 {
-  "nuxt.isNuxtApp": false
+  "nuxt.isNuxtApp": false,
+  "i18n-ally.localesPaths": [
+    "src/locale",
+    "src/locale/lang"
+  ]
 }

+ 15 - 3
README.md

@@ -2,7 +2,7 @@
  * @Author: wjc
  * @Date: 2024-05-27 17:21:43
  * @LastEditors: wjc
- * @LastEditTime: 2024-05-28 17:59:29
+ * @LastEditTime: 2024-07-11 11:27:30
  * @Description:
 -->
 
@@ -12,16 +12,28 @@ hbuilderx 创建模板工程 + 第三方依赖支持。
 
 使用 vscode 编辑,提交代码,hbuilderx 运行和构建。
 
+可以使用 `dev:h5` 模式来加快开发调试进度,但最终还需要以手机模拟器和真机的运行效果为准。
+
 ### 名词解释
 
 - node_modules: 第三方依赖库
 - uni_modules:uni-app 插件市场依赖
 
+### 运行
+
+> nodejs >= 18,如 v18.14.2
+> pnpm >= 9,如 v9.1.4
+
+```bash
+pnpm install
+pnpm dev:h5
+```
+
 ### husky 配置
 
-安装 husky
+> 本项目已经配置好完整的husky及相关校验工具,此处用于记录
 
-> nodejs >= 18
+安装 husky
 
 ```bash
 pnpm add husky@8.0.3 -D

+ 1 - 10
build/vite-plugin-uni-provider.ts

@@ -2,7 +2,7 @@
  * @Author: wjc
  * @Date: 2024-07-10 11:21:52
  * @LastEditors: wjc
- * @LastEditTime: 2024-07-10 11:23:48
+ * @LastEditTime: 2024-07-11 20:20:48
  * @Description:
  */
 import path from 'path'
@@ -23,8 +23,6 @@ export default function (options: Partial<Options> = {}) {
   let {
     pagesRE = /src[\/\\]pages[\/\\]((?!.+(component(s)?|static).+).)*\.vue$/,
     name = 'sys',
-    configFile = 'vite.config.js',
-    pagesBasePath = 'src/pages',
     pluginName = 'uni-provider',
     DEBUG = process.env.DEBUG,
   } = options
@@ -56,17 +54,10 @@ export default function (options: Partial<Options> = {}) {
     },
   }
 
-  function normalizePagePath(file) {
-    return normallize(path.relative(pagesBasePath, file))
-  }
-
   function normalizePagePathFromBase(file) {
     return normallize(path.relative(process.cwd(), file))
   }
 
-  function log(...args) {
-    console.log(c.dim(new Date().toLocaleTimeString()), c.bold(c.red(`[${pluginName}]`)), ...args)
-  }
   function debug(...args) {
     DEBUG &&
       console.log(

+ 1 - 0
package.json

@@ -53,6 +53,7 @@
     "@typescript-eslint/parser": "^7.10.0",
     "@uni-helper/vite-plugin-uni-layouts": "^0.1.10",
     "@uni-helper/vite-plugin-uni-pages": "^0.2.23",
+    "@vitejs/plugin-vue-jsx": "^4.0.0",
     "@vue/runtime-core": "^3.4.27",
     "@vue/tsconfig": "^0.5.1",
     "commitlint": "^19.3.0",

+ 20 - 0
pnpm-lock.yaml

@@ -96,6 +96,9 @@ importers:
       '@uni-helper/vite-plugin-uni-pages':
         specifier: ^0.2.23
         version: 0.2.23(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21)
+      '@vitejs/plugin-vue-jsx':
+        specifier: ^4.0.0
+        version: 4.0.0(vite@5.2.12(@types/node@20.14.2)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))
       '@vue/runtime-core':
         specifier: ^3.4.27
         version: 3.4.27
@@ -1648,6 +1651,13 @@ packages:
       vite: ^4.0.0 || ^5.0.0
       vue: ^3.0.0
 
+  '@vitejs/plugin-vue-jsx@4.0.0':
+    resolution: {integrity: sha512-A+6wL2AdQhDsLsDnY+2v4rRDI1HLJGIMc97a8FURO9tqKsH5QvjWrzsa5DH3NlZsM742W2wODl2fF+bfcTWtXw==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    peerDependencies:
+      vite: ^5.0.0
+      vue: ^3.0.0
+
   '@vitejs/plugin-vue@5.0.5':
     resolution: {integrity: sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ==}
     engines: {node: ^18.0.0 || >=20.0.0}
@@ -7522,6 +7532,16 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@vitejs/plugin-vue-jsx@4.0.0(vite@5.2.12(@types/node@20.14.2)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))':
+    dependencies:
+      '@babel/core': 7.24.7
+      '@babel/plugin-transform-typescript': 7.24.7(@babel/core@7.24.7)
+      '@vue/babel-plugin-jsx': 1.2.2(@babel/core@7.24.7)
+      vite: 5.2.12(@types/node@20.14.2)(sass@1.77.4)(terser@5.31.0)
+      vue: 3.4.27(typescript@5.4.5)
+    transitivePeerDependencies:
+      - supports-color
+
   '@vitejs/plugin-vue@5.0.5(vite@5.2.12(@types/node@20.14.2)(sass@1.77.4)(terser@5.31.0))(vue@3.4.27(typescript@5.4.5))':
     dependencies:
       vite: 5.2.12(@types/node@20.14.2)(sass@1.77.4)(terser@5.31.0)

+ 90 - 0
src/components/MCard/index.tsx

@@ -0,0 +1,90 @@
+/*
+ * @Author: wjc
+ * @Date: 2024-07-12 10:46:19
+ * @LastEditors: wjc
+ * @LastEditTime: 2024-07-12 15:02:10
+ * @Description:
+ */
+import { Fragment } from 'vue'
+import './styles.scss'
+
+export default defineComponent({
+  name: 'MCard',
+  props: {
+    bgColor: {
+      type: String,
+      default: 'var(--bg-card)',
+    },
+    rounded: {
+      type: Boolean,
+      default: true,
+    },
+    padding: {
+      type: Number,
+      default: 16,
+    },
+    border: {
+      type: Boolean,
+      default: true,
+    },
+    spacer: {
+      type: Boolean,
+      default: true,
+    },
+    space: {
+      type: Boolean,
+      default: true,
+    },
+    justify: {
+      type: String as PropType<'start' | 'end' | 'center' | 'between' | 'around' | 'evenly'>,
+      default: 'start',
+    },
+    direction: {
+      type: String as PropType<'vertical' | 'horizontal'>,
+      default: 'vertical',
+    },
+    align: {
+      type: String,
+      default: 'center',
+    },
+  },
+  setup(props, { slots }) {
+    const spaceClass = computed(() => {
+      if (props.space) {
+        let cls = `flex gap-8px items-${props.align}`
+        if (props.direction === 'vertical') {
+          cls = cls + ' flex-col'
+        }
+        if (props.justify) {
+          cls = cls + ` justify-${props.justify}`
+        }
+        return cls
+      } else {
+        return ''
+      }
+    })
+    return {
+      spaceClass,
+    }
+  },
+  render() {
+    const { spaceClass, rounded, padding, bgColor, spacer } = this
+    const children = this.$slots.default()
+
+    return (
+      <view
+        class={[`${rounded ? 'm-card rounded-12px' : 'm-card'}`, spaceClass]}
+        style={{ padding: `${padding}px`, background: `${bgColor}` }}
+      >
+        {children.map((child, i, arr) => {
+          return (
+            <>
+              <view class={['m-card-item']}>{child}</view>
+              {i % 2 === 0 && spacer ? <view class="m-card-spacer"></view> : null}
+            </>
+          )
+        })}
+      </view>
+    )
+  },
+})

+ 7 - 0
src/components/MCard/index.vue

@@ -1,3 +1,10 @@
+<!--
+ * @Author: wjc
+ * @Date: 2024-07-02 15:13:49
+ * @LastEditors: wjc
+ * @LastEditTime: 2024-07-12 11:13:57
+ * @Description: 
+-->
 <template>
   <view
     class="m-card"

+ 10 - 0
src/components/MCard/styles.scss

@@ -0,0 +1,10 @@
+.m-card {
+  @apply my-12px;
+  box-shadow: 0px 0px 10px rgba(0, 0, 0, 20%);
+  .m-card-item {
+    @apply w-full;
+  }
+  .m-card-spacer {
+    @apply h-1px w-full bg-border-1 my-4px;
+  }
+}

+ 2 - 2
src/components/MIcon/index.vue

@@ -2,8 +2,8 @@
  * @Author: wjc
  * @Date: 2024-07-01 10:05:11
  * @LastEditors: wjc
- * @LastEditTime: 2024-07-02 15:20:40
- * @Description: 
+ * @LastEditTime: 2024-07-11 11:49:28
+ * @Description: 仅支持 H5
 -->
 <template>
   <svg v-bind="$attrs" aria-hidden="true" class="m-icon">

+ 2 - 2
src/components/MTabBar/index.vue

@@ -2,7 +2,7 @@
  * @Author: wjc
  * @Date: 2024-07-08 16:31:21
  * @LastEditors: wjc
- * @LastEditTime: 2024-07-10 15:18:29
+ * @LastEditTime: 2024-07-12 11:41:28
  * @Description: 
 -->
 <template>
@@ -53,7 +53,7 @@
     @apply h-50px;
     :deep(.u-tabbar__content) {
       @apply bg-bg-card;
-      border-color: var(--border-color) !important;
+      border-color: var(--border-color-1) !important;
     }
     .tabbar-icon {
       @apply wh-32px;

+ 2 - 2
src/layouts/default.vue

@@ -2,7 +2,7 @@
  * @Author: wjc
  * @Date: 2024-06-25 15:42:58
  * @LastEditors: wjc
- * @LastEditTime: 2024-07-09 17:44:34
+ * @LastEditTime: 2024-07-11 09:30:19
  * @Description: 
 -->
 <template>
@@ -24,6 +24,6 @@
 
 <style scoped lang="scss">
   .default-layout {
-    @apply relative flex-1 h-full overflow-y-auto color-text-1;
+    @apply relative flex flex-col flex-1 h-full overflow-y-auto color-text-1;
   }
 </style>

+ 4 - 0
src/locale/en.json

@@ -0,0 +1,4 @@
+{
+  "index.custom.icon": "Custom Icon",
+  "index.lang.params": "hello,{appName}"
+}

+ 76 - 0
src/locale/index.ts

@@ -0,0 +1,76 @@
+/*
+ * @Author: wjc
+ * @Date: 2024-07-11 20:29:20
+ * @LastEditors: wjc
+ * @LastEditTime: 2024-07-12 10:36:18
+ * @Description:
+ */
+import { createI18n } from 'vue-i18n'
+
+import en from './en.json' // 英文
+import zhHans from './zh-Hans.json' // 简体中文
+
+const messages = {
+  en,
+  'zh-Hans': zhHans,
+}
+
+export type LangTypes = keyof typeof messages
+
+const i18n = createI18n({
+  locale: uni.getLocale(), // 获取已设置的语言
+  messages,
+})
+
+/**
+ * 可以拿到原始的语言模板,非 vue 文件使用这个方法,
+ * @param { string } key 多语言的key,eg: "app.name"
+ * @returns {string} 返回原始的多语言模板,eg: "{heavy}KG"
+ */
+export const getTemplateByKey = (key: string) => {
+  if (!key) {
+    console.error(`[i18n] Function getTemplateByKey(), key param is required`)
+    return ''
+  }
+  const locale = uni.getLocale()
+  console.log('locale:', locale)
+
+  const message = messages[locale] // 拿到某个多语言的所有模板(是一个对象)
+  if (Object.keys(message).includes(key)) {
+    return message[key]
+  }
+  console.error(`[i18n] Function getTemplateByKey(), key param ${key} is not existed.`)
+  return ''
+}
+
+/**
+ * 处理非 h5 端多语言传参无效问题
+ * formatI18n('我是{name},身高{detail.height},体重{detail.weight}',{name:'张三',detail:{height:178,weight:'75kg'}})
+ * 暂不支持数组
+ * @param template 多语言模板字符串,eg: `我是{name}`
+ * @param {Object|undefined} data 需要传递的数据对象,里面的key与多语言字符串对应,eg: `{name:'菲鸽'}`
+ * @returns
+ */
+export function formatI18n(template: string, data?: any) {
+  return template.replace(/\{([^}]+)\}/g, function (match, key: string) {
+    const arr = key.trim().split('.')
+    let result = data
+    while (arr.length) {
+      const first = arr.shift()
+      result = result[first]
+    }
+    return result
+  })
+}
+
+export function $$t(key, data?) {
+  return formatI18n(getTemplateByKey(key), data)
+}
+
+export function setupI18n(app) {
+  app.use(i18n)
+  // 注册全局的多语言函数
+  app.config.globalProperties.$$t = $$t
+}
+
+export default i18n

+ 4 - 0
src/locale/zh-Hans.json

@@ -0,0 +1,4 @@
+{
+  "index.custom.icon": "自定义图标",
+  "index.lang.params": "你好,{appName}"
+}

+ 4 - 1
src/main.ts

@@ -2,12 +2,13 @@
  * @Author: wjc
  * @Date: 2024-05-27 11:49:45
  * @LastEditors: wjc
- * @LastEditTime: 2024-07-02 10:27:33
+ * @LastEditTime: 2024-07-12 10:01:58
  * @Description:
  */
 import { createSSRApp } from 'vue'
 import * as Pinia from 'pinia'
 import uviewPlus from 'uview-plus'
+import { setupI18n } from '@/locale'
 import 'virtual:uno.css'
 import 'virtual:svg-icons-register'
 
@@ -22,6 +23,8 @@ export function createApp() {
   const app = createSSRApp(App)
 
   app.use(uviewPlus)
+
+  setupI18n(app)
   setupStores(app)
   setupInterceptors(app)
 

+ 9 - 0
src/models/appTypes.ts

@@ -1,4 +1,13 @@
+import { LangTypes } from '@/locale'
+
+export interface Lang {
+  label: string
+  value: LangTypes
+}
+
 export interface AppState {
   selectedTabbar: number
   isDark: boolean
+  lang: string
+  langs: Lang[]
 }

+ 6 - 3
src/pages/index/index.vue

@@ -2,7 +2,7 @@
  * @Author: wjc
  * @Date: 2019-08-22 19:41:20
  * @LastEditors: wjc
- * @LastEditTime: 2024-07-10 17:10:32
+ * @LastEditTime: 2024-07-12 10:26:56
  * @Description: 
 -->
 <template>
@@ -10,7 +10,9 @@
   <view class="content">
     <image class="logo" src="/static/logo.png"></image>
     <view class="text-area">
-      <text class="title">{{ title }}</text>
+      <view class="title">多语言文本:{{ $$t('index.custom.icon') }}</view>
+      <view class="title">多语言传参:{{ $$t('index.lang.params', { appName }) }}</view>
+      <view>多语言2: {{ $t('index.custom.icon') }}</view>
     </view>
     <view :class="`i-custom-${icon}`" class="wh-32px text-32px"></view>
     <view>
@@ -27,8 +29,8 @@
 <script setup lang="ts">
   import { onLaunch, onShow } from '@dcloudio/uni-app'
 
-  const title = ref('自定义图标')
   const icon = ref('anl')
+  const appName = ref('绘管家')
 
   const onTest = () => {
     uni.navigateTo({
@@ -37,6 +39,7 @@
   }
 
   onLaunch(() => {
+    uni.setLocale('en')
     uni.hideTabBar()
   })
   onShow(() => {

+ 48 - 6
src/pages/mine/index.vue

@@ -2,16 +2,48 @@
  * @Author: wjc
  * @Date: 2024-06-17 16:02:59
  * @LastEditors: wjc
- * @LastEditTime: 2024-07-10 16:59:31
+ * @LastEditTime: 2024-07-12 15:34:48
  * @Description: 
 -->
 <template>
   <MCard>
     <view>{{ userStore.userInfo.name }}</view>
   </MCard>
+  <MMCard :space="true" direction="vertical">
+    <view class="flex justify-between">
+      <view>深色模式</view>
+      <up-switch v-model="appStore.isDark"></up-switch>
+    </view>
+    <view class="flex justify-between">
+      <view>语言</view>
+      <view class="flex-1 text-right" @click="() => (showLang = true)">
+        <view>
+          {{ appStore.langs.find((item) => item.value === appStore.lang).label }}
+        </view>
+        <up-picker
+          :show="showLang"
+          :columns="langs"
+          key-name="label"
+          @cancel="cancel"
+          @confirm="confirm"
+        ></up-picker>
+      </view>
+    </view>
+  </MMCard>
   <MCard :space="true" direction="horizontal" justify="between">
-    <view>深色模式</view>
-    <up-switch v-model="appStore.isDark"></up-switch>
+    <view>语言</view>
+    <view class="flex-1 text-right" @click="() => (showLang = true)">
+      <view>
+        {{ appStore.langs.find((item) => item.value === appStore.lang).label }}
+      </view>
+      <up-picker
+        :show="showLang"
+        :columns="langs"
+        key-name="label"
+        @cancel="cancel"
+        @confirm="confirm"
+      ></up-picker>
+    </view>
   </MCard>
   <up-button type="primary" class="btn-primary" @click="handleConfirmLogout">退出登录</up-button>
   <up-modal :show="logoutShow">
@@ -30,24 +62,34 @@
   import { onLaunch } from '@dcloudio/uni-app'
   import { useUserStore } from '@/stores/modules/userStore'
   import { useAppStore } from '@/stores/modules/appStore'
+  import MMCard from '@/components/MCard/index'
 
   defineOptions({ name: 'Mine' })
 
   const userStore = useUserStore()
   const appStore = useAppStore()
-  const logoutShow = ref(false)
 
+  const showLang = ref(false)
+  const langs = ref([appStore.langs])
+  const confirm = (e) => {
+    appStore.setLang(e.value[0].value)
+    showLang.value = false
+    console.log('lang-', uni.getLocale())
+  }
+  const cancel = (e) => {
+    showLang.value = false
+  }
+
+  const logoutShow = ref(false)
   const handleCancel = () => {
     logoutShow.value = false
   }
   const handleConfirmLogout = () => {
     logoutShow.value = true
   }
-
   const handleConfirm = () => {
     onLogout()
   }
-
   const onLogout = () => {
     userStore.logoutAction().then((res) => {
       if (res) {

+ 2 - 2
src/static/styles/vars.scss

@@ -12,7 +12,7 @@
   --bg-page: #f1f1f3;
   --bg-card: #fff;
 
-  --border-color: #e3e8f7;
+  --border-color-1: #e3e8f7;
 }
 
 .dark {
@@ -21,7 +21,7 @@
   --bg-page: #242424;
   --bg-card: #1a1a1a;
 
-  --border-color: rgba(84, 84, 84, 0.48);
+  --border-color-1: rgba(84, 84, 84, 0.48);
 
   --text-color-1: rgba(255, 255, 255, 0.87);
 }

+ 18 - 1
src/stores/modules/appStore.ts

@@ -2,11 +2,12 @@
  * @Author: wjc
  * @Date: 2024-06-05 11:22:45
  * @LastEditors: wjc
- * @LastEditTime: 2024-06-05 17:16:15
+ * @LastEditTime: 2024-07-12 09:44:20
  * @Description:
  */
 import { defineStore } from 'pinia'
 
+import i18n, { LangTypes } from '@/locale'
 import type { AppState } from '@/models/appTypes'
 
 export const useAppStore = defineStore('app', {
@@ -14,6 +15,17 @@ export const useAppStore = defineStore('app', {
     return {
       selectedTabbar: 0,
       isDark: false,
+      lang: 'zh-Hans',
+      langs: [
+        {
+          label: '中文',
+          value: 'zh-Hans',
+        },
+        {
+          label: 'English',
+          value: 'en',
+        },
+      ],
     }
   },
   persist: true,
@@ -21,5 +33,10 @@ export const useAppStore = defineStore('app', {
     setTabbar(val: number) {
       this.selectedTabbar = val
     },
+    setLang(val: LangTypes) {
+      uni.setLocale(val)
+      i18n.global.locale = val
+      this.lang = val
+    },
   },
 })

+ 5 - 1
tsconfig.json

@@ -2,6 +2,9 @@
   "compilerOptions": {
     "composite": true,
     "skipLibCheck": true,
+    "jsx": "react",
+    "jsxFactory": "h",
+    "jsxFragmentFactory": "Fragment",
     "module": "ESNext",
     "moduleResolution": "Node",
     "resolveJsonModule": true,
@@ -27,7 +30,8 @@
     "vite.config.ts",
     "types/components.d.ts",
     "package.json",
-    "manifest.json"
+    "manifest.json",
+    "build/**/*.ts"
   ],
   "exclude": ["node_modules", "tests/server/**/*.ts", "dist", "**/*.js"]
 }

+ 1 - 1
types/global.d.ts

@@ -2,6 +2,6 @@
  * @Author: wjc
  * @Date: 2024-05-31 09:26:09
  * @LastEditors: wjc
- * @LastEditTime: 2024-06-05 16:00:04
+ * @LastEditTime: 2024-07-12 10:17:37
  * @Description:
  */

+ 5 - 2
types/shims-uni.d.ts

@@ -2,13 +2,16 @@
  * @Author: wjc
  * @Date: 2024-06-05 15:57:41
  * @LastEditors: wjc
- * @LastEditTime: 2024-06-05 17:42:44
+ * @LastEditTime: 2024-07-12 10:42:01
  * @Description:
  */
 /// <reference types='@dcloudio/types' />
 import 'vue'
+import { formatI18n } from '@/locale'
 
 declare module '@vue/runtime-core' {
   type Hooks = App.AppInstance & Page.PageInstance
-  interface ComponentCustomOptions extends Hooks {}
+  interface ComponentCustomOptions extends Hooks {
+    $$t?: typeof formatI18n
+  }
 }

+ 4 - 1
uno.config.ts

@@ -2,7 +2,7 @@
  * @Author: wjc
  * @Date: 2023-05-07 20:59:28
  * @LastEditors: wjc
- * @LastEditTime: 2024-07-09 17:46:51
+ * @LastEditTime: 2024-07-12 11:41:00
  * @Description:
  */
 import {
@@ -95,6 +95,9 @@ export default defineConfig({
         overlay: 'var(--bg-overlay)',
         primary: 'var(--color-primary)',
       },
+      border: {
+        1: 'var(--border-color-1)',
+      },
     },
   },
 })

+ 4 - 1
vite.config.ts

@@ -2,7 +2,7 @@
  * @Author: wjc
  * @Date: 2024-05-27 10:17:11
  * @LastEditors: wjc
- * @LastEditTime: 2024-07-10 16:37:37
+ * @LastEditTime: 2024-07-12 11:27:53
  * @Description:
  */
 import path from 'node:path'
@@ -12,6 +12,7 @@ import uni from '@dcloudio/vite-plugin-uni'
 import AutoImport from 'unplugin-auto-import/vite'
 import Components from 'unplugin-vue-components/vite'
 import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
+import vueJsx from '@vitejs/plugin-vue-jsx'
 
 import UniLayouts from '@uni-helper/vite-plugin-uni-layouts'
 // import UniPages from '@uni-helper/vite-plugin-uni-pages'
@@ -49,6 +50,8 @@ export default defineConfig(({ mode }) => {
       //自动注册页面全局组件
       UniProvider(),
       UnoCSS(),
+      vueJsx(),
+      // 仅支持 H5
       createSvgIconsPlugin({
         iconDirs: [pathResolve('src/static/icons')],
         symbolId: 'icon-[dir]-[name]',