Browse Source

feat(I18n): Add Ii8n

feat(LocaleDropdown): Add LocaleDropdown Component

feat(store): Add localeStore
kailong321200875 3 years ago
parent
commit
3810b8c3b2
42 changed files with 667 additions and 415 deletions
  1. 19 0
      .pnpm-debug.log
  2. 10 8
      package.json
  3. 210 288
      pnpm-lock.yaml
  4. 10 10
      src/App.vue
  5. 1 0
      src/assets/svgs/icon.svg
  6. 2 2
      src/components/ConfigGlobal/index.ts
  7. 0 0
      src/components/ConfigGlobal/src/ConfigGlobal.vue
  8. 1 1
      src/components/Form/index.ts
  9. 3 3
      src/components/Form/src/Form.vue
  10. 2 2
      src/components/Form/src/componentMap.ts
  11. 1 1
      src/components/Form/src/components/useRenderChcekbox.tsx
  12. 1 1
      src/components/Form/src/components/useRenderRadio.tsx
  13. 2 2
      src/components/Form/src/components/useRenderSelect.tsx
  14. 3 3
      src/components/Form/src/helper.ts
  15. 3 0
      src/components/Icon/index.ts
  16. 86 0
      src/components/Icon/src/Icon.vue
  17. 2 2
      src/components/InputPassword/index.ts
  18. 12 8
      src/components/InputPassword/src/InputPassword.vue
  19. 2 2
      src/components/LocaleDropdown/index.ts
  20. 34 0
      src/components/LocaleDropdown/src/LocaleDropdown.vue
  21. 0 19
      src/components/LocaleDropdown/src/VLocaleDropdown.vue
  22. 2 2
      src/components/ThemeSwitch/index.ts
  23. 4 2
      src/components/ThemeSwitch/src/ThemeSwitch.vue
  24. 6 0
      src/components/index.ts
  25. 1 1
      src/hooks/web/useConfigGlobal.ts
  26. 7 0
      src/hooks/web/useIcon.ts
  27. 35 0
      src/hooks/web/useLocale.ts
  28. 31 11
      src/main.ts
  29. 3 0
      src/plugins/i18n/helper.ts
  30. 37 11
      src/plugins/i18n/index.ts
  31. 3 0
      src/plugins/svgIcon/index.ts
  32. 2 3
      src/router/index.ts
  33. 1 10
      src/store/modules/app.ts
  34. 55 0
      src/store/modules/locale.ts
  35. 6 0
      src/styles/theme.less
  36. 1 0
      src/styles/var.css
  37. 27 6
      src/types/componentType.d.ts
  38. 7 0
      src/types/components.d.ts
  39. 3 1
      src/types/global.d.ts
  40. 19 2
      src/views/Login/Login.vue
  41. 2 7
      tsconfig.json
  42. 11 7
      vite.config.ts

+ 19 - 0
.pnpm-debug.log

@@ -0,0 +1,19 @@
+{
+  "0 debug pnpm:scope": {
+    "selected": 1
+  },
+  "1 error pnpm": {
+    "errno": 1,
+    "code": "ELIFECYCLE",
+    "pkgid": "butterfly-admin@3.0.0",
+    "stage": "clean",
+    "script": "npx rimraf docs/node_modules && npx rimraf node_modules",
+    "pkgname": "butterfly-admin",
+    "err": {
+      "name": "pnpm",
+      "message": "butterfly-admin@3.0.0 clean: `npx rimraf docs/node_modules && npx rimraf node_modules`\nExit status 1",
+      "code": "ELIFECYCLE",
+      "stack": "pnpm: butterfly-admin@3.0.0 clean: `npx rimraf docs/node_modules && npx rimraf node_modules`\nExit status 1\n    at EventEmitter.<anonymous> (C:\\Users\\Saber\\AppData\\Roaming\\npm\\pnpm-global\\5\\node_modules\\.pnpm\\registry.npmmirror.com+pnpm@6.24.4\\node_modules\\pnpm\\dist\\pnpm.cjs:103873:20)\n    at EventEmitter.emit (node:events:365:28)\n    at ChildProcess.<anonymous> (C:\\Users\\Saber\\AppData\\Roaming\\npm\\pnpm-global\\5\\node_modules\\.pnpm\\registry.npmmirror.com+pnpm@6.24.4\\node_modules\\pnpm\\dist\\pnpm.cjs:91802:18)\n    at ChildProcess.emit (node:events:365:28)\n    at maybeClose (node:internal/child_process:1067:16)\n    at Process.ChildProcess._handle.onexit (node:internal/child_process:301:5)"
+    }
+  }
+}

+ 10 - 8
package.json

@@ -25,9 +25,10 @@
     "postinstall": "husky install"
   },
   "dependencies": {
-    "@vueuse/core": "^7.4.1",
+    "@iconify/iconify": "^2.1.0",
+    "@vueuse/core": "^7.5.1",
     "@zxcvbn-ts/core": "^1.2.0",
-    "element-plus": "1.2.0-beta.6",
+    "element-plus": "1.3.0-beta.1",
     "lodash-es": "^4.17.21",
     "pinia": "^2.0.9",
     "vue": "3.2.26",
@@ -38,8 +39,9 @@
   "devDependencies": {
     "@commitlint/cli": "^16.0.1",
     "@commitlint/config-conventional": "^16.0.0",
-    "@iconify/json": "^1.1.448",
+    "@iconify/json": "^1.1.450",
     "@intlify/vite-plugin-vue-i18n": "^3.2.1",
+    "@purge-icons/generated": "^0.7.0",
     "@types/lodash-es": "^4.17.5",
     "@types/node": "^17.0.5",
     "@typescript-eslint/eslint-plugin": "^5.8.1",
@@ -49,7 +51,7 @@
     "async-validator": "^4.0.7",
     "autoprefixer": "^10.4.1",
     "commitizen": "^4.2.4",
-    "eslint": "^8.5.0",
+    "eslint": "^8.6.0",
     "eslint-config-prettier": "^8.3.0",
     "eslint-define-config": "^1.2.1",
     "eslint-plugin-prettier": "^4.0.0",
@@ -69,14 +71,14 @@
     "stylelint-config-standard": "^24.0.0",
     "stylelint-order": "^5.0.0",
     "typescript": "4.5.4",
-    "unplugin-icons": "^0.13.0",
-    "vite": "2.7.9",
+    "vite": "2.7.10",
     "vite-plugin-eslint": "^1.3.0",
+    "vite-plugin-purge-icons": "^0.7.0",
     "vite-plugin-style-import": "^1.4.1",
-    "vite-plugin-svg-icons": "^1.0.5",
+    "vite-plugin-svg-icons": "^1.1.0",
     "vite-plugin-windicss": "^1.6.1",
     "vue-tsc": "^0.30.1",
-    "windicss": "^3.4.1",
+    "windicss": "^3.4.2",
     "windicss-analysis": "^0.3.5"
   },
   "engines": {

File diff suppressed because it is too large
+ 210 - 288
pnpm-lock.yaml


+ 10 - 10
src/App.vue

@@ -1,30 +1,30 @@
 <script setup lang="ts">
-import { computed, unref } from 'vue'
+import { computed } from 'vue'
 import { useAppStore } from '@/store/modules/app'
+import { useLocaleStore } from '@/store/modules/locale'
 import { ElConfigProvider } from 'element-plus'
-import { VConfigGlobal } from '@/components/ConfigGlobal'
-import zhCn from 'element-plus/lib/locale/lang/zh-cn'
-import en from 'element-plus/lib/locale/lang/en'
+import { ConfigGlobal } from '@/components/ConfigGlobal'
 import { isDark } from '@/utils/is'
 
 const appStore = useAppStore()
 
+const localeStore = useLocaleStore()
+
+const local = computed(() => localeStore.locale)
+
 function initDark() {
   const isDarkTheme = isDark()
   appStore.setIsDark(isDarkTheme)
 }
 initDark()
-
-const locale = computed(() => appStore.getLang)
-const setLocale = computed(() => (unref(locale) === 'zh-cn' ? zhCn : en))
 </script>
 
 <template>
-  <VConfigGlobal>
-    <ElConfigProvider :locale="setLocale">
+  <ConfigGlobal>
+    <ElConfigProvider :locale="local.elLocale">
       <RouterView />
     </ElConfigProvider>
-  </VConfigGlobal>
+  </ConfigGlobal>
 </template>
 
 <style lang="less">

+ 1 - 0
src/assets/svgs/icon.svg

@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.147.062a13 13 0 0 1 4.94.945c1.55.63 2.907 1.526 4.069 2.688a13.148 13.148 0 0 1 2.761 4.069c.678 1.55 1.017 3.245 1.017 5.086v102.3c0 3.681-1.187 6.733-3.56 9.155-2.373 2.422-5.352 3.633-8.937 3.633H12.992c-3.875 0-7-1.26-9.373-3.779-2.373-2.518-3.56-5.667-3.56-9.445V12.704c0-3.39 1.163-6.345 3.488-8.863C5.872 1.32 8.972.062 12.847.062h102.3zM81.434 109.047c1.744 0 3.003-.412 3.778-1.235.775-.824 1.163-1.914 1.163-3.27 0-1.26-.388-2.325-1.163-3.197-.775-.872-2.034-1.307-3.778-1.307H72.57c.097-.194.145-.485.145-.872V27.09h9.01c1.743 0 2.954-.436 3.633-1.308.678-.872 1.017-1.938 1.017-3.197 0-1.26-.34-2.325-1.017-3.197-.679-.872-1.89-1.308-3.633-1.308H46.268c-1.743 0-2.954.436-3.632 1.308-.678.872-1.018 1.938-1.018 3.197 0 1.26.34 2.325 1.018 3.197.678.872 1.889 1.308 3.632 1.308h8.138v72.075c0 .193.024.339.073.436.048.096.072.242.072.436H46.56c-1.744 0-3.003.435-3.778 1.307-.775.872-1.163 1.938-1.163 3.197 0 1.356.388 2.446 1.163 3.27.775.823 2.034 1.235 3.778 1.235h34.875z"/></svg>

+ 2 - 2
src/components/ConfigGlobal/index.ts

@@ -1,3 +1,3 @@
-import VConfigGlobal from './src/VConfigGlobal.vue'
+import ConfigGlobal from './src/ConfigGlobal.vue'
 
-export { VConfigGlobal }
+export { ConfigGlobal }

+ 0 - 0
src/components/ConfigGlobal/src/VConfigGlobal.vue → src/components/ConfigGlobal/src/ConfigGlobal.vue


+ 1 - 1
src/components/Form/index.ts

@@ -1,4 +1,4 @@
-import VFrom from './src/VForm.vue'
+import VFrom from './src/Form.vue'
 
 export interface VFormExpose {
   count: number

+ 3 - 3
src/components/Form/src/VForm.vue → src/components/Form/src/Form.vue

@@ -20,7 +20,7 @@ export default defineComponent({
   props: {
     // 生成Form的布局结构数组
     schema: {
-      type: Array as PropType<VFormSchema[]>,
+      type: Array as PropType<FormSchema[]>,
       required: true,
       default: () => []
     },
@@ -98,7 +98,7 @@ export default defineComponent({
     }
 
     // 渲染formItem
-    function renderFormItem(item: VFormSchema) {
+    function renderFormItem(item: FormSchema) {
       // 单独给只有options属性的组件做判断
       const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
       const slotsMap: Recordable = {
@@ -134,7 +134,7 @@ export default defineComponent({
     }
 
     // 渲染options
-    function renderOptions(item: VFormSchema) {
+    function renderOptions(item: FormSchema) {
       switch (item.component) {
         case 'Select':
           const { renderSelectOptions } = useRenderSelect(slots)

+ 2 - 2
src/components/Form/src/componentMap.ts

@@ -18,7 +18,7 @@ import {
   ElAutocomplete,
   ElDivider
 } from 'element-plus'
-import { VInputPassword } from '@/components/InputPassword'
+import { InputPassword } from '@/components/InputPassword'
 
 const componentMap: Recordable<Component, ComponentName> = {
   Radio: ElRadioGroup,
@@ -40,7 +40,7 @@ const componentMap: Recordable<Component, ComponentName> = {
   TimeSelect: ElTimeSelect,
   SelectV2: ElSelectV2,
   RadioButton: ElRadioGroup,
-  InputPassword: VInputPassword
+  InputPassword: InputPassword
 }
 
 export { componentMap }

+ 1 - 1
src/components/Form/src/components/useRenderChcekbox.tsx

@@ -2,7 +2,7 @@ import { ElCheckbox, ElCheckboxButton } from 'element-plus'
 import { defineComponent } from 'vue'
 
 export function useRenderChcekbox() {
-  function renderChcekboxOptions(item: VFormSchema) {
+  function renderChcekboxOptions(item: FormSchema) {
     // 如果有别名,就取别名
     const labelAlias = item?.componentProps?.optionsAlias?.labelField
     const valueAlias = item?.componentProps?.optionsAlias?.valueField

+ 1 - 1
src/components/Form/src/components/useRenderRadio.tsx

@@ -2,7 +2,7 @@ import { ElRadio, ElRadioButton } from 'element-plus'
 import { defineComponent } from 'vue'
 
 export function useRenderRadio() {
-  function renderRadioOptions(item: VFormSchema) {
+  function renderRadioOptions(item: FormSchema) {
     // 如果有别名,就取别名
     const labelAlias = item?.componentProps?.optionsAlias?.labelField
     const valueAlias = item?.componentProps?.optionsAlias?.valueField

+ 2 - 2
src/components/Form/src/components/useRenderSelect.tsx

@@ -4,7 +4,7 @@ import { Slots } from 'vue'
 
 export function useRenderSelect(slots: Slots) {
   // 渲染 select options
-  function renderSelectOptions(item: VFormSchema) {
+  function renderSelectOptions(item: FormSchema) {
     // 如果有别名,就取别名
     const labelAlias = item?.componentProps?.optionsAlias?.labelField
     return item?.componentProps?.options?.map((option) => {
@@ -25,7 +25,7 @@ export function useRenderSelect(slots: Slots) {
   }
 
   // 渲染 select option item
-  function renderSelectOptionItem(item: VFormSchema, option: ComponentOptions) {
+  function renderSelectOptionItem(item: FormSchema, option: ComponentOptions) {
     // 如果有别名,就取别名
     const labelAlias = item?.componentProps?.optionsAlias?.labelField
     const valueAlias = item?.componentProps?.optionsAlias?.valueField

+ 3 - 3
src/components/Form/src/helper.ts

@@ -16,7 +16,7 @@ interface PlaceholderMoel {
  * @returns 返回提示信息对象
  * @description 用于自动设置placeholder
  */
-export function setTextPlaceholder(schema: VFormSchema): PlaceholderMoel {
+export function setTextPlaceholder(schema: FormSchema): PlaceholderMoel {
   const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword']
   const selectMap = ['Select', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect']
   if (textMap.includes(schema?.component as string)) {
@@ -74,7 +74,7 @@ export function setGridProp(col: ColProps = {}): ColProps {
  * @param item 传入的组件属性
  * @returns 默认添加 clearable 属性
  */
-export function setComponentProps(item: VFormSchema): Recordable {
+export function setComponentProps(item: FormSchema): Recordable {
   const notNeedClearable = ['ColorPicker']
   const componentProps: Recordable = notNeedClearable.includes(item.component as string)
     ? { ...item.componentProps }
@@ -117,7 +117,7 @@ export function setItemComponentSlots(
  * @returns FormMoel
  * @description 生成对应的formModel
  */
-export function initModel(schema: VFormSchema[], formModel: Recordable) {
+export function initModel(schema: FormSchema[], formModel: Recordable) {
   const model: Recordable = { ...formModel }
   schema.map((v) => {
     // 如果是hidden,就删除对应的值

+ 3 - 0
src/components/Icon/index.ts

@@ -0,0 +1,3 @@
+import Icon from './src/Icon.vue'
+
+export { Icon }

+ 86 - 0
src/components/Icon/src/Icon.vue

@@ -0,0 +1,86 @@
+<script setup lang="ts">
+import { computed, unref, ref, watch, nextTick } from 'vue'
+import { ElIcon } from 'element-plus'
+import { useDesign } from '@/hooks/web/useDesign'
+import { propTypes } from '@/utils/propTypes'
+import Iconify from '@purge-icons/generated'
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('icon')
+
+const props = defineProps({
+  // icon name
+  icon: propTypes.string,
+  // icon color
+  color: propTypes.string,
+  // icon size
+  size: propTypes.number.def(16)
+})
+
+const elRef = ref<ElRef>(null)
+
+const isLocal = computed(() => props.icon.startsWith('icon:'))
+
+const symbolId = computed(() => {
+  return unref(isLocal) ? `#icon-${props.icon.split('icon:')[1]}` : props.icon
+})
+
+const getIconifyStyle = computed(() => {
+  const { color, size } = props
+  return {
+    fontSize: `${size}px`,
+    color
+  }
+})
+
+async function updateIcon(icon: string) {
+  if (unref(isLocal)) return
+
+  const el = unref(elRef)
+  if (!el) return
+
+  await nextTick()
+
+  if (!icon) return
+
+  const svg = Iconify.renderSVG(icon, {})
+  if (svg) {
+    el.textContent = ''
+    el.appendChild(svg)
+  } else {
+    const span = document.createElement('span')
+    span.className = 'iconify'
+    span.dataset.icon = icon
+    el.textContent = ''
+    el.appendChild(span)
+  }
+}
+
+watch(
+  () => props.icon,
+  (icon: string) => {
+    updateIcon(icon)
+  }
+)
+</script>
+
+<template>
+  <ElIcon :class="[prefixCls, $attrs.class]" :size="size" :color="color">
+    <svg v-if="isLocal" aria-hidden="true">
+      <use :xlink:href="symbolId" />
+    </svg>
+
+    <span v-else ref="elRef" :style="getIconifyStyle">
+      <span class="iconify" :data-icon="symbolId"></span>
+    </span>
+  </ElIcon>
+</template>
+
+<style lang="less" scoped>
+@prefix-cls: ~'@{namespace}-icon';
+
+.@{prefix-cls} {
+  display: inline-block;
+}
+</style>

+ 2 - 2
src/components/InputPassword/index.ts

@@ -1,3 +1,3 @@
-import VInputPassword from './src/VInputPassword.vue'
+import InputPassword from './src/InputPassword.vue'
 
-export { VInputPassword }
+export { InputPassword }

+ 12 - 8
src/components/InputPassword/src/VInputPassword.vue → src/components/InputPassword/src/InputPassword.vue

@@ -1,12 +1,9 @@
 <script setup lang="ts">
 import { ref, unref, computed, watch } from 'vue'
-import { ElInput, ElIcon } from 'element-plus'
-import EyeInvisibleOutlinedE from '~icons/ant-design/eyeInvisibleOutlined'
-import EyeOutlined from '~icons/ant-design/eye-outlined'
+import { ElInput } from 'element-plus'
 import { propTypes } from '@/utils/propTypes'
 import { useDesign } from '@/hooks/web/useDesign'
 import { useConfigGlobal } from '@/hooks/web/useConfigGlobal'
-const { configGlobal } = useConfigGlobal()
 import { zxcvbn } from '@zxcvbn-ts/core'
 import type { ZxcvbnResult } from '@zxcvbn-ts/core'
 
@@ -16,6 +13,8 @@ defineProps({
   modelValue: propTypes.string.def('')
 })
 
+const { configGlobal } = useConfigGlobal()
+
 const emit = defineEmits(['update:modelValue'])
 
 // 生成class前缀
@@ -43,16 +42,17 @@ const getPasswordStrength = computed(() => {
   const zxcvbnRef = zxcvbn(unref(valueRef)) as ZxcvbnResult
   return value ? zxcvbnRef.score : -1
 })
+
+const getIconName = computed(() =>
+  unref(textType) === 'password' ? 'ant-design:eye-invisible-outlined' : 'ant-design:eye-outlined'
+)
 </script>
 
 <template>
   <div :class="[prefixCls, `${prefixCls}--${configGlobal?.size}`]">
     <ElInput v-bind="$attrs" v-model="valueRef" :type="textType">
       <template #suffix>
-        <ElIcon class="el-input__icon el-input__clear">
-          <EyeInvisibleOutlinedE v-if="textType === 'password'" @click="changeTextType" />
-          <EyeOutlined v-else @click="changeTextType" />
-        </ElIcon>
+        <Icon class="el-input__icon" :icon="getIconName" @click="changeTextType" />
       </template>
     </ElInput>
     <div
@@ -69,6 +69,10 @@ const getPasswordStrength = computed(() => {
 @prefix-cls: ~'@{namespace}-input-password';
 
 .@{prefix-cls} {
+  :deep(.el-input__clear) {
+    margin-left: 5px;
+  }
+
   &__bar {
     background-color: var(--el-text-color-disabled-base);
     border-radius: var(--el-border-radius-base);

+ 2 - 2
src/components/LocaleDropdown/index.ts

@@ -1,3 +1,3 @@
-import VLocaleDropdown from './src/VLocaleDropdown.vue'
+import LocaleDropdown from './src/LocaleDropdown.vue'
 
-export { VLocaleDropdown }
+export { LocaleDropdown }

+ 34 - 0
src/components/LocaleDropdown/src/LocaleDropdown.vue

@@ -0,0 +1,34 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
+import { useLocaleStore } from '@/store/modules/locale'
+import { useCssVar } from '@vueuse/core'
+import { useLocale } from '@/hooks/web/useLocale'
+
+const localeStore = useLocaleStore()
+
+const langMap = computed(() => localeStore.localeMap)
+
+const textColor = useCssVar('--el-text-color-primary', document.documentElement)
+
+function setLang(lang: LocaleType) {
+  localeStore.setLocale({
+    lang
+  })
+  const { changeLocale } = useLocale()
+  changeLocale(lang)
+}
+</script>
+
+<template>
+  <ElDropdown trigger="click" @command="setLang">
+    <Icon icon="ion:language-sharp" :color="textColor" class="cursor-pointer" />
+    <template #dropdown>
+      <ElDropdownMenu>
+        <ElDropdownItem v-for="item in langMap" :key="item.lang" :command="item.lang">
+          {{ item.name }}
+        </ElDropdownItem>
+      </ElDropdownMenu>
+    </template>
+  </ElDropdown>
+</template>

+ 0 - 19
src/components/LocaleDropdown/src/VLocaleDropdown.vue

@@ -1,19 +0,0 @@
-<script setup lang="ts"></script>
-
-<template>
-  <div>s</div>
-  <!-- <el-dropdown trigger="click">
-    <span class="el-dropdown-link">
-      Dropdown List<el-icon class="el-icon--right"><arrow-down /></el-icon>
-    </span>
-    <template #dropdown>
-      <el-dropdown-menu>
-        <el-dropdown-item :icon="Plus">Action 1</el-dropdown-item>
-        <el-dropdown-item :icon="CirclePlusFilled"> Action 2 </el-dropdown-item>
-        <el-dropdown-item :icon="CirclePlus">Action 3</el-dropdown-item>
-        <el-dropdown-item :icon="Check">Action 4</el-dropdown-item>
-        <el-dropdown-item :icon="CircleCheck">Action 5</el-dropdown-item>
-      </el-dropdown-menu>
-    </template>
-  </el-dropdown> -->
-</template>

+ 2 - 2
src/components/ThemeSwitch/index.ts

@@ -1,3 +1,3 @@
-import VThemeSwitch from './src/VThemeSwitch.vue'
+import ThemeSwitch from './src/ThemeSwitch.vue'
 
-export { VThemeSwitch }
+export { ThemeSwitch }

+ 4 - 2
src/components/ThemeSwitch/src/VThemeSwitch.vue → src/components/ThemeSwitch/src/ThemeSwitch.vue

@@ -1,11 +1,13 @@
 <script setup lang="ts">
 import { ref } from 'vue'
 import { useAppStore } from '@/store/modules/app'
-import Sun from '~icons/emojione-monotone/sun'
-import CrescentMoon from '~icons/emojione-monotone/crescent-moon'
 import { ElSwitch } from 'element-plus'
 import { useCssVar } from '@vueuse/core'
 import { useDesign } from '@/hooks/web/useDesign'
+import { useIcon } from '@/hooks/web/useIcon'
+
+const Sun = useIcon({ icon: 'emojione-monotone:sun' })
+const CrescentMoon = useIcon({ icon: 'emojione-monotone:crescent-moon' })
 
 const appStore = useAppStore()
 

+ 6 - 0
src/components/index.ts

@@ -0,0 +1,6 @@
+import type { App } from 'vue'
+import { Icon } from './Icon'
+
+export function setupGlobCom(app: App<Element>): void {
+  app.component('Icon', Icon)
+}

+ 1 - 1
src/hooks/web/useConfigGlobal.ts

@@ -1,7 +1,7 @@
 import { inject } from 'vue'
 
 export function useConfigGlobal() {
-  const configGlobal = inject('configGlobal', {}) as VConfigGlobalTypes
+  const configGlobal = inject('configGlobal', {}) as ConfigGlobalTypes
 
   return {
     configGlobal

+ 7 - 0
src/hooks/web/useIcon.ts

@@ -0,0 +1,7 @@
+import { h } from 'vue'
+import type { VNode } from 'vue'
+import { Icon } from '@/components/Icon'
+
+export function useIcon(props: IconTypes): VNode {
+  return h(Icon, props)
+}

+ 35 - 0
src/hooks/web/useLocale.ts

@@ -0,0 +1,35 @@
+import { i18n } from '@/plugins/i18n'
+import { useLocaleStoreWithOut } from '@/store/modules/locale'
+import { setHtmlPageLang } from '@/plugins/i18n/helper'
+
+function setI18nLanguage(locale: LocaleType) {
+  const localeStore = useLocaleStoreWithOut()
+
+  if (i18n.mode === 'legacy') {
+    i18n.global.locale = locale
+  } else {
+    ;(i18n.global.locale as any).value = locale
+  }
+  localeStore.setLocale({
+    lang: locale
+  })
+  setHtmlPageLang(locale)
+}
+
+export function useLocale() {
+  // Switching the language will change the locale of useI18n
+  // And submit to configuration modification
+  async function changeLocale(locale: LocaleType) {
+    const globalI18n = i18n.global
+
+    const langModule = (await import(`../../locales/${locale}.ts`)) as any
+
+    globalI18n.setLocaleMessage(locale, langModule.default)
+
+    setI18nLanguage(locale)
+  }
+
+  return {
+    changeLocale
+  }
+}

+ 31 - 11
src/main.ts

@@ -1,22 +1,42 @@
 // 引入windi css
 import '@/plugins/windicss'
 
-import { createApp } from 'vue'
-import App from './App.vue'
-const app = createApp(App)
+// 导入全局的svg图标
+import '@/plugins/svgIcon'
 
-// 引入element-plus
-import { setupElementPlus } from '@/plugins/elementPlus'
-setupElementPlus(app)
+// 初始化多语言
+import { setupI18n } from '@/plugins/i18n'
 
 // 引入状态管理
 import { setupStore } from '@/store'
-setupStore(app)
+
+// 全局组件
+import { setupGlobCom } from '@/components'
+
+// 引入element-plus
+import { setupElementPlus } from '@/plugins/elementPlus'
 
 // 路由
-import router, { setupRouter } from './router'
-setupRouter(app)
+import { setupRouter } from './router'
+
+import { createApp } from 'vue'
+
+import App from './App.vue'
+
+async function setupAll() {
+  const app = createApp(App)
+
+  await setupI18n(app)
+
+  setupStore(app)
+
+  setupGlobCom(app)
+
+  setupElementPlus(app)
+
+  setupRouter(app)
 
-router.isReady().then(() => {
   app.mount('#app')
-})
+}
+
+setupAll()

+ 3 - 0
src/plugins/i18n/helper.ts

@@ -0,0 +1,3 @@
+export function setHtmlPageLang(locale: LocaleType) {
+  document.querySelector('html')?.setAttribute('lang', locale)
+}

+ 37 - 11
src/plugins/i18n/index.ts

@@ -1,16 +1,42 @@
+import type { App } from 'vue'
 import { createI18n } from 'vue-i18n'
+import { useLocaleStoreWithOut } from '@/store/modules/locale'
+import type { I18n, I18nOptions } from 'vue-i18n'
+import { setHtmlPageLang } from './helper'
 
-export const i18n = setupI18n()
+export let i18n: ReturnType<typeof createI18n>
 
-function setupI18n() {
-  const messages = Object.fromEntries(
-    Object.entries(import.meta.globEager('../../locales/*.ts')).map(([key, value]) => {
-      return [key.slice(14, -3), value.default]
-    })
-  )
-  return createI18n({
-    legacy: false,
-    locale: 'zh-CN',
-    messages
+async function createI18nOptions(): Promise<I18nOptions> {
+  const localeStore = useLocaleStoreWithOut()
+  const locale = localeStore.getLocale
+  const localeMap = localeStore.getLocaleMap
+  const defaultLocal = await import(`../../locales/${locale.lang}.ts`)
+  const message = defaultLocal.default ?? {}
+
+  setHtmlPageLang(locale.lang)
+
+  localeStore.setLocale({
+    lang: locale.lang
+    // elLocale: elLocal
   })
+
+  return {
+    legacy: false,
+    locale: locale.lang,
+    fallbackLocale: locale.lang,
+    messages: {
+      [locale.lang]: message
+    },
+    availableLocales: localeMap.map((v) => v.lang),
+    sync: true,
+    silentTranslationWarn: true,
+    missingWarn: false,
+    silentFallbackWarn: true
+  }
+}
+
+export async function setupI18n(app: App) {
+  const options = await createI18nOptions()
+  i18n = createI18n(options) as I18n
+  app.use(i18n)
 }

+ 3 - 0
src/plugins/svgIcon/index.ts

@@ -0,0 +1,3 @@
+import 'virtual:svg-icons-register'
+
+import '@purge-icons/generated'

+ 2 - 3
src/router/index.ts

@@ -2,8 +2,7 @@ import { createRouter, createWebHashHistory } from 'vue-router'
 import type { RouteRecordRaw } from 'vue-router'
 import type { App } from 'vue'
 // import { getParentLayout } from './helper'
-import { useI18n } from '@/hooks/web/useI18n'
-const { t } = useI18n()
+import { t } from '@/hooks/web/useI18n'
 
 export const constantRouterMap: AppRouteRecordRaw[] = [
   {
@@ -20,7 +19,7 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
 
 const router = createRouter({
   history: createWebHashHistory(),
-  strict: false,
+  strict: true,
   routes: constantRouterMap as RouteRecordRaw[],
   scrollBehavior: () => ({ left: 0, top: 0 })
 })

+ 1 - 10
src/store/modules/app.ts

@@ -1,5 +1,4 @@
 import { defineStore } from 'pinia'
-
 import { store } from '../index'
 
 export type LayoutType = 'Classic' | 'LeftTop' | 'Top' | 'Test'
@@ -24,7 +23,6 @@ export interface AppState {
   requestTime: boolean
   isDark: boolean
   size: ElememtPlusSzie
-  lang: string
 }
 
 export const useAppStore = defineStore({
@@ -48,8 +46,7 @@ export const useAppStore = defineStore({
     showMenuTab: false, // 是否固定一级菜单
     requestTime: false, // 是否在接口调用时添加时间戳,避免IE缓存
     isDark: false, // 是否是暗黑模式
-    size: 'default', // 组件尺寸
-    lang: 'zh-CN' // 语言
+    size: 'default' // 组件尺寸
   }),
   getters: {
     getCollapsed(): boolean {
@@ -108,9 +105,6 @@ export const useAppStore = defineStore({
     },
     getSize(): ElememtPlusSzie {
       return this.size
-    },
-    getLang(): string {
-      return this.lang
     }
   },
   actions: {
@@ -174,9 +168,6 @@ export const useAppStore = defineStore({
     },
     setSize(size: ElememtPlusSzie) {
       this.size = size
-    },
-    setLang(lang: string) {
-      this.lang = lang
     }
   }
 })

+ 55 - 0
src/store/modules/locale.ts

@@ -0,0 +1,55 @@
+import { defineStore } from 'pinia'
+import zhCn from 'element-plus/lib/locale/lang/zh-cn'
+import en from 'element-plus/lib/locale/lang/en'
+import { store } from '../index'
+
+const elLocaleMap = {
+  'zh-CN': zhCn,
+  en: en
+}
+
+export interface LocaleState {
+  locale: LocaleDropdownType
+  localeMap: LocaleDropdownType[]
+}
+
+export const useLocaleStore = defineStore({
+  id: 'locales',
+  state: (): LocaleState => ({
+    // 当前语言
+    locale: {
+      lang: 'zh-CN',
+      elLocale: elLocaleMap['zh-CN']
+    },
+    // 多语言
+    localeMap: [
+      {
+        lang: 'zh-CN',
+        name: '简体中文'
+      },
+      {
+        lang: 'en',
+        name: 'English'
+      }
+    ]
+  }),
+  getters: {
+    getLocale(): LocaleDropdownType {
+      return this.locale
+    },
+    getLocaleMap(): LocaleDropdownType[] {
+      return this.localeMap
+    }
+  },
+  actions: {
+    setLocale(localeMap: LocaleDropdownType) {
+      // this.locale = Object.assign(this.locale, localeMap)
+      this.locale.lang = localeMap?.lang
+      this.locale.elLocale = elLocaleMap[localeMap?.lang]
+    }
+  }
+})
+
+export function useLocaleStoreWithOut() {
+  return useLocaleStore(store)
+}

+ 6 - 0
src/styles/theme.less

@@ -0,0 +1,6 @@
+.text-color {
+  color: var(--el-text-color-regular);
+}
+.dark .dark\:text-color {
+  color: rgba(255, 255, 255, var(--dark-text-color));
+}

+ 1 - 0
src/styles/var.css

@@ -1,2 +1,3 @@
 :root {
+  --dark-text-color: #c9d1d9;
 }

+ 27 - 6
src/types/componentType.d.ts

@@ -2,7 +2,7 @@ import type { CSSProperties } from 'vue'
 import type { RuleItem } from 'async-validator'
 
 declare global {
-  // BfForm types start
+  // Form types start
   declare type ComponentName =
     | 'Radio'
     | 'RadioButton'
@@ -75,7 +75,7 @@ declare global {
     optionsSlot?: boolean
   } & Recordable
 
-  declare type VFormSchema = {
+  declare type FormSchema = {
     // 唯一值
     field: string
     // 标题
@@ -93,11 +93,32 @@ declare global {
     // 是否隐藏
     hidden?: boolean
   }
-  // VForm types end
+  // Form types end
 
-  // VConfigGlobal types start
-  declare interface VConfigGlobalTypes {
+  // ConfigGlobal types start
+  declare interface ConfigGlobalTypes {
     size?: ElememtPlusSzie
   }
-  // VConfigGlobal types end
+  // ConfigGlobal types end
+
+  // Icon type start
+  declare interface IconTypes {
+    size?: number
+    color?: string
+    icon: string
+  }
+  // Icon type end
+
+  // LocaleDropdown type start
+  declare interface Language {
+    el: Recordable
+    name: string
+  }
+
+  declare interface LocaleDropdownType {
+    lang: LocaleType
+    name?: string
+    elLocale?: Language
+  }
+  // LocaleDropdown type end
 }

+ 7 - 0
src/types/components.d.ts

@@ -0,0 +1,7 @@
+declare module 'vue' {
+  export interface GlobalComponents {
+    Icon: typeof import('../components/Icon/src/Icon.vue')['default']
+  }
+}
+
+export {}

+ 3 - 1
src/types/global.d.ts

@@ -6,10 +6,12 @@ declare type Nullable<T> = T | null
 
 declare type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>
 
-declare type ElememtPlusSzie = 'default' | 'medium' | 'small' | 'mini'
+declare type ElememtPlusSzie = 'default' | 'small' | 'large'
 
 declare type ElementPlusInfoType = 'success' | 'info' | 'warning' | 'danger'
 
 declare type Recordable<T = any, K = string> = Record<K extends null | undefined ? string : K, T>
 
 declare type ComponentRef<T> = InstanceType<T>
+
+declare type LocaleType = 'zh-CN' | 'en'

+ 19 - 2
src/views/Login/Login.vue

@@ -1,7 +1,11 @@
 <script setup lang="ts">
-import { VThemeSwitch } from '@/components/ThemeSwitch'
+import { ThemeSwitch } from '@/components/ThemeSwitch'
+import { LocaleDropdown } from '@/components/LocaleDropdown'
 import { useDesign } from '@/hooks/web/useDesign'
 import { useI18n } from '@/hooks/web/useI18n'
+import { ElCalendar } from 'element-plus'
+import { VFrom } from '@/components/Form'
+
 const { t } = useI18n()
 
 const { getPrefixCls } = useDesign()
@@ -10,7 +14,20 @@ const prefixCls = getPrefixCls('login')
 
 <template>
   <div :class="prefixCls" class="h-[calc(100%)] relative">
-    <VThemeSwitch />
+    <Icon icon="icon:icon" />
+    <Icon icon="ant-design:eye-outlined" />
+    <ThemeSwitch />
+    <LocaleDropdown />
+    <ElCalendar />
+    <VFrom
+      :schema="[
+        {
+          label: 'input',
+          field: 'field1',
+          component: 'InputPassword'
+        }
+      ]"
+    />
     {{ t('formDemo.default') }}
   </div>
 </template>

+ 2 - 7
tsconfig.json

@@ -23,14 +23,9 @@
     "paths": {
       "@/*": ["src/*"]
     },
-    "types": [
-      "@intlify/vite-plugin-vue-i18n/client",
-      "vite/client",
-      "element-plus/global",
-      "unplugin-icons/types/vue"
-    ],
+    "types": ["@intlify/vite-plugin-vue-i18n/client", "vite/client", "element-plus/global"],
     "typeRoots": ["./node_modules/@types/", "./src/types"]
   },
-  "include": ["src/**/*"],
+  "include": ["./src/**/*"],
   "exclude": ["dist", "node_modules"]
 }

+ 11 - 7
vite.config.ts

@@ -6,8 +6,9 @@ import WindiCSS from 'vite-plugin-windicss'
 import VueJsx from '@vitejs/plugin-vue-jsx'
 import EslintPlugin from 'vite-plugin-eslint'
 import VueI18n from '@intlify/vite-plugin-vue-i18n'
-import Icons from 'unplugin-icons/vite'
 import StyleImport, { ElementPlusResolve } from 'vite-plugin-style-import'
+import ViteSvgIcons from 'vite-plugin-svg-icons'
+import PurgeIcons from 'vite-plugin-purge-icons'
 
 // https://vitejs.dev/config/
 const root = process.cwd()
@@ -41,10 +42,6 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
           }
         }]
       }),
-      Icons({
-        compiler: 'vue3',
-        autoInstall: true
-      }),
       EslintPlugin({
         cache: false,
         include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件
@@ -53,7 +50,13 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
         runtimeOnly: true,
         compositionOnly: true,
         include: [resolve(__dirname, 'src/locales/**')]
-      })
+      }),
+      ViteSvgIcons({
+        iconDirs: [pathResolve('src/assets/svgs')],
+        symbolId: 'icon-[dir]-[name]',
+        svgoOptions: true
+      }),
+      PurgeIcons()
     ],
 
     css: {
@@ -116,7 +119,8 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
         'vue-router',
         'vue-types',
         'element-plus/lib/locale/lang/zh-cn',
-        'element-plus/lib/locale/lang/en'
+        'element-plus/lib/locale/lang/en',
+        '@iconify/iconify'
       ]
     }
   }

Some files were not shown because too many files changed in this diff