Browse Source

feat: support pwa

vben 1 year ago
parent
commit
382652e0f4

+ 1 - 0
.gitignore

@@ -7,6 +7,7 @@ coverage
 **/.vitepress/cache
 .cache
 .turbo
+dev-dist
 .stylelintcache
 yarn.lock
 package-lock.json

+ 7 - 0
apps/web-antd/src/views/_essential/fallback/coming-soon.vue

@@ -0,0 +1,7 @@
+<script lang="ts" setup>
+import { Fallback } from '@vben/common-ui';
+</script>
+
+<template>
+  <Fallback status="hello" />
+</template>

+ 41 - 18
apps/web-antd/vite.config.mts

@@ -1,24 +1,47 @@
 import { defineConfig } from '@vben/vite-config';
 
 export default defineConfig({
-  application: {
-    compress: false,
-    compressTypes: ['brotli', 'gzip'],
-    importmap: false,
-    importmapOptions: {
-      // 通过 Importmap CDN 方式引入,
-      // 目前只有esm.sh源兼容性好一点,jspm.io对于 esm 入口要求高
-      defaultProvider: 'esm.sh',
-      importmap: [
-        { name: 'vue' },
-        { name: 'pinia' },
-        { name: 'vue-router' },
-        { name: 'vue-i18n' },
-        { name: 'dayjs' },
-        { name: 'vue-demi' },
-      ],
-    },
-    visualizer: false,
+  application: ({ mode }) => {
+    return {
+      compress: false,
+      compressTypes: ['brotli', 'gzip'],
+      importmap: false,
+      importmapOptions: {
+        // 通过 Importmap CDN 方式引入,
+        // 目前只有esm.sh源兼容性好一点,jspm.io对于 esm 入口要求高
+        defaultProvider: 'esm.sh',
+        importmap: [
+          { name: 'vue' },
+          { name: 'pinia' },
+          { name: 'vue-router' },
+          { name: 'vue-i18n' },
+          { name: 'dayjs' },
+          { name: 'vue-demi' },
+        ],
+      },
+      pwa: false,
+      pwaOptions: {
+        manifest: {
+          description:
+            'Vben Admin Pro is a modern admin dashboard template based on Vue 3. ',
+          icons: [
+            {
+              sizes: '192x192',
+              src: 'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.1/source/pwa-icon-192.png',
+              type: 'image/png',
+            },
+            {
+              sizes: '512x512',
+              src: 'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.1/source/pwa-icon-512.png',
+              type: 'image/png',
+            },
+          ],
+          name: `Vben Admin Pro ${mode}`,
+          short_name: `Vben Admin Pro ${mode}`,
+        },
+      },
+      visualizer: false,
+    };
   },
   vite: {
     server: {

+ 4 - 0
internal/tailwind-config/src/index.ts

@@ -57,6 +57,10 @@ export default {
         'collapsible-up': 'collapsible-up 0.2s ease-in-out',
         float: 'float 5s linear 0ms infinite',
       },
+      animationDuration: {
+        '2000': '2000ms',
+        '3000': '3000ms',
+      },
       borderRadius: {
         lg: 'var(--radius-base)',
         md: 'calc(var(--radius-base) - 2px)',

+ 1 - 0
internal/vite-config/package.json

@@ -36,6 +36,7 @@
     "html-minifier-terser": "^7.2.0",
     "resolve.exports": "^2.0.2",
     "vite-plugin-lib-inject-css": "^2.1.1",
+    "vite-plugin-pwa": "^0.20.0",
     "vite-plugin-vue-devtools": "^7.2.1"
   },
   "devDependencies": {

+ 10 - 3
internal/vite-config/src/config/application.ts

@@ -10,7 +10,8 @@ import { getApplicationConditionPlugins } from '../plugins';
 import { getCommonConfig } from './common';
 
 function defineApplicationConfig(options: DefineApplicationOptions = {}) {
-  return defineConfig(async ({ command, mode }) => {
+  return defineConfig(async (config) => {
+    const { command, mode } = config;
     const { application = {}, vite = {} } = options;
     const root = process.cwd();
     const isBuild = command === 'build';
@@ -28,8 +29,11 @@ function defineApplicationConfig(options: DefineApplicationOptions = {}) {
       isBuild,
       mock: true,
       mode,
+      pwa: true,
       turboConsole: false,
-      ...application,
+      ...(typeof application === 'function'
+        ? application(config)
+        : application),
     });
 
     const applicationConfig: UserConfig = {
@@ -91,7 +95,10 @@ function defineApplicationConfig(options: DefineApplicationOptions = {}) {
       await getCommonConfig(),
       applicationConfig,
     );
-    return mergeConfig(mergedConfig, vite);
+    return mergeConfig(
+      mergedConfig,
+      typeof vite === 'function' ? vite(config) : vite,
+    );
   });
 }
 

+ 7 - 3
internal/vite-config/src/config/library.ts

@@ -10,7 +10,8 @@ import { getLibraryConditionPlugins } from '../plugins';
 import { getCommonConfig } from './common';
 
 function defineLibraryConfig(options: DefineLibraryOptions = {}) {
-  return defineConfig(async ({ command, mode }) => {
+  return defineConfig(async (config) => {
+    const { command, mode } = config;
     const root = process.cwd();
     const { library = {}, vite = {} } = options;
     const isBuild = command === 'build';
@@ -20,7 +21,7 @@ function defineLibraryConfig(options: DefineLibraryOptions = {}) {
       injectLibCss: true,
       isBuild,
       mode,
-      ...library,
+      ...(typeof library === 'function' ? library(config) : library),
     });
 
     const { dependencies = {}, peerDependencies = {} } =
@@ -45,7 +46,10 @@ function defineLibraryConfig(options: DefineLibraryOptions = {}) {
     };
     const commonConfig = await getCommonConfig();
     const mergedConfig = mergeConfig(commonConfig, packageConfig);
-    return mergeConfig(mergedConfig, vite);
+    return mergeConfig(
+      mergedConfig,
+      typeof vite === 'function' ? vite(config) : vite,
+    );
   });
 }
 

+ 21 - 1
internal/vite-config/src/plugins/index.ts

@@ -21,6 +21,7 @@ import viteDtsPlugin from 'vite-plugin-dts';
 import { createHtmlPlugin as viteHtmlPlugin } from 'vite-plugin-html';
 import { libInjectCss as viteLibInjectCss } from 'vite-plugin-lib-inject-css';
 import { viteMockServe as viteMockPlugin } from 'vite-plugin-mock';
+import { VitePWA } from 'vite-plugin-pwa';
 import viteVueDevTools from 'vite-plugin-vue-devtools';
 
 import { viteExtraAppConfigPlugin } from './extra-app-config';
@@ -100,6 +101,8 @@ async function getApplicationConditionPlugins(
     importmapOptions,
     injectAppLoading,
     mock,
+    pwa,
+    pwaOptions,
     turboConsole,
     ...commonOptions
   } = options;
@@ -125,7 +128,24 @@ async function getApplicationConditionPlugins(
     },
     {
       condition: injectAppLoading,
-      plugins: async () => [await viteInjectAppLoadingPlugin(isBuild, env)],
+      plugins: async () => [await viteInjectAppLoadingPlugin(!!isBuild, env)],
+    },
+    {
+      condition: pwa,
+      plugins: () =>
+        VitePWA({
+          injectRegister: false,
+          workbox: {
+            globPatterns: [],
+          },
+          ...pwaOptions,
+          manifest: {
+            display: 'standalone',
+            start_url: '/',
+            theme_color: '#ffffff',
+            ...pwaOptions?.manifest,
+          },
+        }),
     },
     {
       condition: isBuild && !!compress,

+ 2 - 2
internal/vite-config/src/plugins/inject-app-loading/index.ts

@@ -10,8 +10,8 @@ import { type PluginOption } from 'vite';
  * 为多app提供loading样式,无需在每个 app -> index.html单独引入
  */
 async function viteInjectAppLoadingPlugin(
-  isBuild: string,
-  env: Record<string, any>,
+  isBuild: boolean,
+  env: Record<string, any> = {},
 ): Promise<PluginOption | undefined> {
   const loadingHtml = await getLoadingRawByHtmlTemplate();
   const envRaw = isBuild ? 'prod' : 'dev';

+ 12 - 5
internal/vite-config/src/typing.ts

@@ -1,6 +1,7 @@
 import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
-import type { PluginOption, UserConfig } from 'vite';
+import type { ConfigEnv, PluginOption, UserConfig } from 'vite';
 import type { PluginOptions } from 'vite-plugin-dts';
+import type { Options as PwaPluginOptions } from 'vite-plugin-pwa';
 
 import viteTurboConsolePlugin from 'unplugin-turbo-console/vite';
 
@@ -68,6 +69,10 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
   injectAppLoading?: boolean;
   /** mock 插件配置 */
   mock?: boolean;
+  /** 是否开启pwa */
+  pwa?: boolean;
+  /** pwa 插件配置 */
+  pwaOptions?: Partial<PwaPluginOptions>;
   /** turbo-console 插件配置 */
   turboConsole?: Parameters<typeof viteTurboConsolePlugin>[0] | boolean;
 }
@@ -85,13 +90,15 @@ interface ApplicationOptions extends ApplicationPluginOptions {}
 interface LibraryOptions extends LibraryPluginOptions {}
 
 interface DefineApplicationOptions {
-  application?: ApplicationOptions;
-  vite?: UserConfig;
+  application?:
+    | ((config: ConfigEnv) => ApplicationOptions)
+    | ApplicationOptions;
+  vite?: ((config: ConfigEnv) => UserConfig) | UserConfig;
 }
 
 interface DefineLibraryOptions {
-  library?: LibraryOptions;
-  vite?: UserConfig;
+  library?: ((config: ConfigEnv) => LibraryOptions) | LibraryOptions;
+  vite?: ((config: ConfigEnv) => UserConfig) | UserConfig;
 }
 
 type DefineConfig = {

+ 5 - 1
packages/@core/forward/preferences/src/preferences.ts

@@ -245,7 +245,11 @@ class PreferenceManager {
     this.initialPreferences = merge({}, overrides, defaultPreferences);
 
     // 加载并合并当前存储的偏好设置
-    const mergedPreference = merge({}, this.loadCachedPreferences(), overrides);
+    const mergedPreference = merge(
+      {},
+      this.loadCachedPreferences(),
+      this.initialPreferences,
+    );
 
     // 更新偏好设置
     this.updatePreferences(mergedPreference);

+ 0 - 5
packages/@core/shared/design-tokens/src/dark/index.scss → packages/@core/shared/design-tokens/src/dark/index.css

@@ -1,13 +1,8 @@
 :root.dark {
   /* 基础背景颜色颜色 */
 
-  /* --color-background: 240 6% 18%; */
-  // --color-body: 220deg 13.04% 8%;
-  // --color-body: hsl(240deg 11% 4%);
   --color-background: 220deg 13.04% 8%;
 
-  /* --color-background: 219 42% 11%; */
-
   /* 基础文本颜色 */
   --color-foreground: 220 13% 91%;
 

+ 0 - 10
packages/@core/shared/design-tokens/src/default/index.scss → packages/@core/shared/design-tokens/src/default/index.css

@@ -1,13 +1,6 @@
 /* https://gavin-yyc.github.io/colorconvert/ */
 :root {
-  /* 基础背景颜色颜色 */
-
-  /* --color-background: 210deg 25% 96.86%; */
-  // --color-main: 210deg 25% 96.86%;
   --color-background: 0 0 100%;
-  // --color-darken-background: 220deg 13.04% 8%;
-
-  /* --color-background: 220 14% 95%; */
 
   /* 基础文本颜色 */
   --color-foreground: 210 6% 21%;
@@ -85,12 +78,9 @@
   /* menu */
   --color-menu-dark: 225deg 12% 13%;
   --color-menu-dark-darken: 223deg 11% 10%;
-  // --color-menu-darken: var(--color-background);
-  // --color-menu-opened-dark: 225deg 12.12% 11%;
   --color-menu: 0deg 0% 100%;
   --color-menu-darken: 0deg 0% 95%;
 
   accent-color: var(--color-primary);
   color-scheme: light;
-  // --color-menu-opened: 0deg 0% 100%;
 }

+ 2 - 2
packages/@core/shared/design-tokens/src/index.ts

@@ -1,4 +1,4 @@
-import './default/index.scss';
-import './dark/index.scss';
+import './default/index.css';
+import './dark/index.css';
 
 export {};

+ 0 - 40
packages/@core/uikit/layout-ui/src/components/layout-sidebar.vue

@@ -296,43 +296,3 @@ function handleMouseleave() {
     </div>
   </aside>
 </template>
-
-<style scoped lang="scss">
-// @include b('sidebar') {
-//   --color-surface: var(--color-menu);
-
-//   @include is('dark') {
-//     --color-surface: var(--color-menu-dark);
-//   }
-
-//   @include e('shadow') {
-//     position: absolute;
-//     top: 0;
-//     z-index: 1;
-//     inline-size: 100%;
-//     block-size: 40px;
-//     height: 50px;
-//     pointer-events: none;
-//     background: linear-gradient(
-//       to bottom,
-//       hsl(var(--color-surface)),
-//       transparent
-//     );
-//     opacity: 0;
-//     transition: opacity 0.15s ease-in-out;
-//     will-change: opacity;
-
-//     &.scrolled {
-//       opacity: 1;
-//     }
-//   }
-
-//   @include is('dark') {
-//     .#{$namespace}-side__extra {
-//       &-content {
-//         border-color: hsl(var(--color-dark-border)) !important;
-//       }
-//     }
-//   }
-// }
-</style>

+ 1 - 1
packages/@core/uikit/shadcn-ui/src/components/sheet/sheet.vue

@@ -68,7 +68,7 @@ function handlerSubmit() {
     <SheetContent :style="contentStyle" class="!w-full pb-12 sm:rounded-l-lg">
       <SheetHeader
         :class="description ? 'h-16' : 'h-12'"
-        class="border-border flex flex-row items-center justify-between border-b pl-5 pr-3"
+        class="border-border flex flex-row items-center justify-between border-b pl-3 pr-3"
       >
         <div class="flex w-full items-center justify-between">
           <div>

+ 3 - 0
packages/business/common-ui/src/fallback/fallback.vue

@@ -49,6 +49,9 @@ const titleText = computed(() => {
     case 'offline': {
       return $t('fallback.offline-error');
     }
+    case 'hello': {
+      return $t('fallback.coming-soon');
+    }
     default: {
       return '';
     }

+ 8 - 7
packages/business/common-ui/src/preferences/preferences.vue

@@ -125,6 +125,10 @@ const { copy } = useClipboard();
 
 const tabs = computed((): SegmentedItem[] => {
   return [
+    {
+      label: $t('preferences.general'),
+      value: 'general',
+    },
     {
       label: $t('preferences.appearance'),
       value: 'appearance',
@@ -133,10 +137,7 @@ const tabs = computed((): SegmentedItem[] => {
       label: $t('preferences.layout'),
       value: 'layout',
     },
-    {
-      label: $t('preferences.general'),
-      value: 'general',
-    },
+
     {
       label: $t('preferences.shortcut-keys.title'),
       value: 'shortcutKey',
@@ -171,7 +172,7 @@ function handleReset() {
 </script>
 
 <template>
-  <div class="z-100 fixed right-0 top-1/3">
+  <div class="z-100 fixed right-0 top-2/3">
     <VbenSheet
       v-model:open="openPreferences"
       :description="$t('preferences.preferences-subtitle')"
@@ -194,8 +195,8 @@ function handleReset() {
         </VbenIconButton>
       </template>
 
-      <div class="p-5 pt-4">
-        <VbenSegmented :tabs="tabs" default-value="appearance">
+      <div class="p-4 pt-4">
+        <VbenSegmented :tabs="tabs" default-value="general">
           <template #appearance>
             <Block :title="$t('preferences.theme')">
               <Theme

+ 2 - 2
packages/business/common-ui/src/preferences/trigger.vue

@@ -12,8 +12,8 @@ defineOptions({
 <template>
   <VbenButton
     :title="$t('preferences.preferences')"
-    class="bg-primary flex-col-center h-9 w-9 cursor-pointer rounded-l-md rounded-r-none border-none"
+    class="bg-primary flex-col-center h-12 w-12 cursor-pointer rounded-l-lg rounded-r-none border-none"
   >
-    <IconSetting class="text-lg" />
+    <IconSetting class="duration-3000 animate-spin text-2xl" />
   </VbenButton>
 </template>

+ 0 - 1
packages/business/layouts/src/iframe/iframe-router-view.vue

@@ -67,7 +67,6 @@ function showSpinning(index: number) {
 </script>
 <template>
   <template v-if="showIframe">
-    {{ iframeRoutes.length }}
     <template v-for="(item, index) in iframeRoutes" :key="item.fullPath">
       <div
         v-if="canRender(item)"

+ 1 - 1
packages/constants/src/vben.ts

@@ -7,5 +7,5 @@ const VBEN_GITHUB_URL = 'https://github.com/vbenjs/vue-vben-admin';
  * @zh_CN Vben Logo
  */
 const VBEN_LOGO =
-  'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.0/source/logo-v1.webp';
+  'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.1/source/logo-v1.webp';
 export { VBEN_GITHUB_URL, VBEN_LOGO };

+ 1 - 0
packages/locales/src/langs/en-US.yaml

@@ -25,6 +25,7 @@ fallback:
   offline: Offline Page
   offline-error: Oops! Network Error
   offline-error-desc: Sorry, can't connect to the internet. Check your connection.
+  coming-soon: Coming soon
 
 widgets:
   document: Document

+ 1 - 0
packages/locales/src/langs/zh-CN.yaml

@@ -24,6 +24,7 @@ fallback:
   offline: 离线页面
   offline-error: 哎呀!网络错误
   offline-error-desc: 抱歉,无法连接到互联网,请检查您的网络连接并重试。
+  coming-soon: 即将推出
 
 widgets:
   document: 文档

File diff suppressed because it is too large
+ 629 - 87
pnpm-lock.yaml


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