feat: support pwa

pull/48/MERGE
vben 2024-06-16 15:45:15 +08:00
parent 222c73963d
commit 382652e0f4
24 changed files with 1787 additions and 128 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@ -1,24 +1,47 @@
import { defineConfig } from '@vben/vite-config'; import { defineConfig } from '@vben/vite-config';
export default defineConfig({ export default defineConfig({
application: { application: ({ mode }) => {
compress: false, return {
compressTypes: ['brotli', 'gzip'], compress: false,
importmap: false, compressTypes: ['brotli', 'gzip'],
importmapOptions: { importmap: false,
// 通过 Importmap CDN 方式引入, importmapOptions: {
// 目前只有esm.sh源兼容性好一点jspm.io对于 esm 入口要求高 // 通过 Importmap CDN 方式引入,
defaultProvider: 'esm.sh', // 目前只有esm.sh源兼容性好一点jspm.io对于 esm 入口要求高
importmap: [ defaultProvider: 'esm.sh',
{ name: 'vue' }, importmap: [
{ name: 'pinia' }, { name: 'vue' },
{ name: 'vue-router' }, { name: 'pinia' },
{ name: 'vue-i18n' }, { name: 'vue-router' },
{ name: 'dayjs' }, { name: 'vue-i18n' },
{ name: 'vue-demi' }, { name: 'dayjs' },
], { name: 'vue-demi' },
}, ],
visualizer: false, },
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: { vite: {
server: { server: {

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import viteDtsPlugin from 'vite-plugin-dts';
import { createHtmlPlugin as viteHtmlPlugin } from 'vite-plugin-html'; import { createHtmlPlugin as viteHtmlPlugin } from 'vite-plugin-html';
import { libInjectCss as viteLibInjectCss } from 'vite-plugin-lib-inject-css'; import { libInjectCss as viteLibInjectCss } from 'vite-plugin-lib-inject-css';
import { viteMockServe as viteMockPlugin } from 'vite-plugin-mock'; import { viteMockServe as viteMockPlugin } from 'vite-plugin-mock';
import { VitePWA } from 'vite-plugin-pwa';
import viteVueDevTools from 'vite-plugin-vue-devtools'; import viteVueDevTools from 'vite-plugin-vue-devtools';
import { viteExtraAppConfigPlugin } from './extra-app-config'; import { viteExtraAppConfigPlugin } from './extra-app-config';
@ -100,6 +101,8 @@ async function getApplicationConditionPlugins(
importmapOptions, importmapOptions,
injectAppLoading, injectAppLoading,
mock, mock,
pwa,
pwaOptions,
turboConsole, turboConsole,
...commonOptions ...commonOptions
} = options; } = options;
@ -125,7 +128,24 @@ async function getApplicationConditionPlugins(
}, },
{ {
condition: injectAppLoading, 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, condition: isBuild && !!compress,

View File

@ -10,8 +10,8 @@ import { type PluginOption } from 'vite';
* apploading app -> index.html * apploading app -> index.html
*/ */
async function viteInjectAppLoadingPlugin( async function viteInjectAppLoadingPlugin(
isBuild: string, isBuild: boolean,
env: Record<string, any>, env: Record<string, any> = {},
): Promise<PluginOption | undefined> { ): Promise<PluginOption | undefined> {
const loadingHtml = await getLoadingRawByHtmlTemplate(); const loadingHtml = await getLoadingRawByHtmlTemplate();
const envRaw = isBuild ? 'prod' : 'dev'; const envRaw = isBuild ? 'prod' : 'dev';

View File

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

View File

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

View File

@ -1,13 +1,8 @@
:root.dark { :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: 220deg 13.04% 8%;
/* --color-background: 219 42% 11%; */
/* 基础文本颜色 */ /* 基础文本颜色 */
--color-foreground: 220 13% 91%; --color-foreground: 220 13% 91%;

View File

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

View File

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

View File

@ -296,43 +296,3 @@ function handleMouseleave() {
</div> </div>
</aside> </aside>
</template> </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>

View File

@ -68,7 +68,7 @@ function handlerSubmit() {
<SheetContent :style="contentStyle" class="!w-full pb-12 sm:rounded-l-lg"> <SheetContent :style="contentStyle" class="!w-full pb-12 sm:rounded-l-lg">
<SheetHeader <SheetHeader
:class="description ? 'h-16' : 'h-12'" :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 class="flex w-full items-center justify-between">
<div> <div>

View File

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

View File

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

View File

@ -12,8 +12,8 @@ defineOptions({
<template> <template>
<VbenButton <VbenButton
:title="$t('preferences.preferences')" :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> </VbenButton>
</template> </template>

View File

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

View File

@ -7,5 +7,5 @@ const VBEN_GITHUB_URL = 'https://github.com/vbenjs/vue-vben-admin';
* @zh_CN Vben Logo * @zh_CN Vben Logo
*/ */
const 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 }; export { VBEN_GITHUB_URL, VBEN_LOGO };

View File

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

View File

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

File diff suppressed because it is too large Load Diff