发布 v1.10 #5
@@ -4,7 +4,7 @@ VITE_BASE=/flow/
|
|||||||
VITE_GLOB_API_URL=/flow
|
VITE_GLOB_API_URL=/flow
|
||||||
|
|
||||||
# 是否开启压缩,可以设置为 none, brotli, gzip
|
# 是否开启压缩,可以设置为 none, brotli, gzip
|
||||||
VITE_COMPRESS=none
|
VITE_COMPRESS=gzip
|
||||||
|
|
||||||
# 是否开启 PWA
|
# 是否开启 PWA
|
||||||
VITE_PWA=false
|
VITE_PWA=false
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const overridesPreferences = defineOverridesPreferences({
|
|||||||
transition: {
|
transition: {
|
||||||
enable: false,
|
enable: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
progress: false,
|
progress: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { RouteRecordStringComponent } from '@easyflow/types';
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { disableUnsafeMenuRouteCaching } from '../route-cache-safety';
|
||||||
|
|
||||||
|
function createRoute(
|
||||||
|
path: string,
|
||||||
|
children?: RouteRecordStringComponent[],
|
||||||
|
): RouteRecordStringComponent {
|
||||||
|
return {
|
||||||
|
children,
|
||||||
|
component: '/example/Index',
|
||||||
|
meta: { keepAlive: true, title: path },
|
||||||
|
name: path,
|
||||||
|
path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('route cache safety', () => {
|
||||||
|
it('disables stale keep-alive metadata for affected menus recursively', () => {
|
||||||
|
const knowledgeRoute = createRoute('/ai/documentCollection');
|
||||||
|
const routes = [
|
||||||
|
createRoute('/ai/workflow'),
|
||||||
|
createRoute('/ai', [knowledgeRoute]),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(disableUnsafeMenuRouteCaching(routes)).toBe(routes);
|
||||||
|
expect(routes[0]?.meta?.keepAlive).toBe(false);
|
||||||
|
expect(knowledgeRoute.meta?.keepAlive).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not change unrelated route caching', () => {
|
||||||
|
const unrelatedRoute = createRoute('/example/cached');
|
||||||
|
|
||||||
|
disableUnsafeMenuRouteCaching([unrelatedRoute]);
|
||||||
|
|
||||||
|
expect(unrelatedRoute.meta?.keepAlive).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
} from '@easyflow/types';
|
} from '@easyflow/types';
|
||||||
|
|
||||||
import { generateAccessible } from '@easyflow/access';
|
import { generateAccessible } from '@easyflow/access';
|
||||||
|
import { loadMissingIconCollections } from '@easyflow/icons';
|
||||||
import { preferences } from '@easyflow/preferences';
|
import { preferences } from '@easyflow/preferences';
|
||||||
|
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
@@ -12,8 +13,35 @@ import { getAllMenusApi } from '#/api';
|
|||||||
import { BasicLayout, IFrameView } from '#/layouts';
|
import { BasicLayout, IFrameView } from '#/layouts';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { disableUnsafeMenuRouteCaching } from './route-cache-safety';
|
||||||
|
|
||||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||||
|
|
||||||
|
function collectMenuIcons(routes: unknown[]): string[] {
|
||||||
|
const icons: string[] = [];
|
||||||
|
routes.forEach((item) => {
|
||||||
|
if (!item || typeof item !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const route = item as Record<string, unknown>;
|
||||||
|
const meta =
|
||||||
|
route.meta && typeof route.meta === 'object'
|
||||||
|
? (route.meta as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
[route.icon, route.activeIcon, meta?.icon, meta?.activeIcon].forEach(
|
||||||
|
(icon) => {
|
||||||
|
if (typeof icon === 'string') {
|
||||||
|
icons.push(icon);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (Array.isArray(route.children)) {
|
||||||
|
icons.push(...collectMenuIcons(route.children));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return icons;
|
||||||
|
}
|
||||||
|
|
||||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||||
|
|
||||||
@@ -29,7 +57,9 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
|||||||
duration: 1500,
|
duration: 1500,
|
||||||
message: `${$t('common.loadingMenu')}...`,
|
message: `${$t('common.loadingMenu')}...`,
|
||||||
});
|
});
|
||||||
return await getAllMenusApi();
|
const menuRoutes = await getAllMenusApi();
|
||||||
|
await loadMissingIconCollections(collectMenuIcons(menuRoutes));
|
||||||
|
return disableUnsafeMenuRouteCaching(menuRoutes);
|
||||||
},
|
},
|
||||||
// 可以指定没有权限跳转403页面
|
// 可以指定没有权限跳转403页面
|
||||||
forbiddenComponent,
|
forbiddenComponent,
|
||||||
|
|||||||
31
easyflow-ui-admin/app/src/router/route-cache-safety.ts
Normal file
31
easyflow-ui-admin/app/src/router/route-cache-safety.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { RouteRecordStringComponent } from '@easyflow/types';
|
||||||
|
|
||||||
|
const UNSAFE_KEEP_ALIVE_MENU_PATHS = new Set([
|
||||||
|
'/ai/agents',
|
||||||
|
'/ai/documentCollection',
|
||||||
|
'/ai/model',
|
||||||
|
'/ai/skill',
|
||||||
|
'/ai/workflow',
|
||||||
|
'/dashboard/workspace',
|
||||||
|
'/sys/sysAccount',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable route-level caching that conflicts with independent hidden child
|
||||||
|
* routes. Assigning false also replaces stale true values in persisted tabs.
|
||||||
|
*/
|
||||||
|
function disableUnsafeMenuRouteCaching(
|
||||||
|
routes: RouteRecordStringComponent[],
|
||||||
|
): RouteRecordStringComponent[] {
|
||||||
|
for (const route of routes) {
|
||||||
|
if (UNSAFE_KEEP_ALIVE_MENU_PATHS.has(route.path) && route.meta) {
|
||||||
|
route.meta.keepAlive = false;
|
||||||
|
}
|
||||||
|
if (route.children?.length) {
|
||||||
|
disableUnsafeMenuRouteCaching(route.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { disableUnsafeMenuRouteCaching };
|
||||||
@@ -213,7 +213,7 @@ const loadProviderDetail = async (
|
|||||||
if (!options.keepDraft) {
|
if (!options.keepDraft) {
|
||||||
syncProviderDraft();
|
syncProviderDraft();
|
||||||
}
|
}
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
isDetailLoading.value = true;
|
isDetailLoading.value = true;
|
||||||
@@ -234,30 +234,64 @@ const loadProviderDetail = async (
|
|||||||
if (!options.keepDraft) {
|
if (!options.keepDraft) {
|
||||||
syncProviderDraft(selectedProvider.value);
|
syncProviderDraft(selectedProvider.value);
|
||||||
}
|
}
|
||||||
|
return res.data;
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || $t('ui.actionMessage.operationFailed'));
|
ElMessage.error(res.message || $t('ui.actionMessage.operationFailed'));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isDetailLoading.value = false;
|
isDetailLoading.value = false;
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshProviderCounts = async (list: any[]) => {
|
let providerCountRequestId = 0;
|
||||||
const entries = await Promise.all(
|
const PROVIDER_COUNT_CONCURRENCY = 2;
|
||||||
list.map(async (provider) => {
|
|
||||||
|
const refreshProviderCounts = async (
|
||||||
|
list: any[],
|
||||||
|
excludedProviderId?: string,
|
||||||
|
) => {
|
||||||
|
const currentRequestId = ++providerCountRequestId;
|
||||||
|
const pendingProviders = list.filter(
|
||||||
|
(provider) => String(provider.id) !== excludedProviderId,
|
||||||
|
);
|
||||||
|
const entries: Array<[string, number]> = [];
|
||||||
|
let cursor = 0;
|
||||||
|
|
||||||
|
const worker = async () => {
|
||||||
|
while (cursor < pendingProviders.length) {
|
||||||
|
const provider = pendingProviders[cursor++];
|
||||||
|
const providerId = String(provider.id);
|
||||||
try {
|
try {
|
||||||
const res = await api.get(
|
const res = await api.get(
|
||||||
`/api/v1/model/getList?providerId=${provider.id}`,
|
`/api/v1/model/getList?providerId=${providerId}`,
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
return [provider.id, res.errorCode === 0 ? countModels(res.data) : 0];
|
entries.push([
|
||||||
|
providerId,
|
||||||
|
res.errorCode === 0 ? countModels(res.data) : 0,
|
||||||
|
]);
|
||||||
} catch {
|
} catch {
|
||||||
return [provider.id, 0];
|
entries.push([providerId, 0]);
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(
|
||||||
|
{
|
||||||
|
length: Math.min(PROVIDER_COUNT_CONCURRENCY, pendingProviders.length),
|
||||||
|
},
|
||||||
|
worker,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
providerCounts.value = Object.fromEntries(entries);
|
if (currentRequestId === providerCountRequestId) {
|
||||||
|
providerCounts.value = {
|
||||||
|
...providerCounts.value,
|
||||||
|
...Object.fromEntries(entries),
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadProviders = async (
|
const loadProviders = async (
|
||||||
@@ -272,12 +306,15 @@ const loadProviders = async (
|
|||||||
const res = await getLlmProviderList();
|
const res = await getLlmProviderList();
|
||||||
const list = res?.data || [];
|
const list = res?.data || [];
|
||||||
providers.value = list;
|
providers.value = list;
|
||||||
|
providerCounts.value = Object.fromEntries(
|
||||||
if (refreshCounts) {
|
list.map((provider: any) => [
|
||||||
await refreshProviderCounts(list);
|
String(provider.id),
|
||||||
}
|
providerCounts.value[String(provider.id)] ?? 0,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
|
providerCountRequestId += 1;
|
||||||
selectedProviderId.value = '';
|
selectedProviderId.value = '';
|
||||||
syncProviderDraft();
|
syncProviderDraft();
|
||||||
syncGroupedModels({});
|
syncGroupedModels({});
|
||||||
@@ -300,6 +337,9 @@ const loadProviders = async (
|
|||||||
await loadProviderDetail(nextProviderId, {
|
await loadProviderDetail(nextProviderId, {
|
||||||
keepDraft: keepDraft && previousId === nextProviderId,
|
keepDraft: keepDraft && previousId === nextProviderId,
|
||||||
});
|
});
|
||||||
|
if (refreshCounts) {
|
||||||
|
void refreshProviderCounts(list, String(nextProviderId));
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isPageLoading.value = false;
|
isPageLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ import type {
|
|||||||
} from '#/components/page/CardList.vue';
|
} from '#/components/page/CardList.vue';
|
||||||
import type { OfflineImpactCheck } from '#/views/ai/shared/offline-impact';
|
import type { OfflineImpactCheck } from '#/views/ai/shared/offline-impact';
|
||||||
|
|
||||||
import { computed, h, markRaw, onMounted, ref } from 'vue';
|
import {
|
||||||
|
computed,
|
||||||
import ElXMarkdown from 'vue-element-plus-x/es/XMarkdown/index.js';
|
defineAsyncComponent,
|
||||||
|
h,
|
||||||
|
markRaw,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
import { useAccess } from '@easyflow/access';
|
import { useAccess } from '@easyflow/access';
|
||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
@@ -40,6 +45,7 @@ import {
|
|||||||
ElMessage,
|
ElMessage,
|
||||||
ElMessageBox,
|
ElMessageBox,
|
||||||
ElPopover,
|
ElPopover,
|
||||||
|
ElSkeleton,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
import { tryit } from 'radash';
|
import { tryit } from 'radash';
|
||||||
|
|
||||||
@@ -67,6 +73,10 @@ import {
|
|||||||
|
|
||||||
import WorkflowModal from './WorkflowModal.vue';
|
import WorkflowModal from './WorkflowModal.vue';
|
||||||
|
|
||||||
|
const ElXMarkdown = defineAsyncComponent(
|
||||||
|
() => import('vue-element-plus-x/es/XMarkdown/index.js'),
|
||||||
|
);
|
||||||
|
|
||||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
|
||||||
interface FieldDefinition {
|
interface FieldDefinition {
|
||||||
@@ -1007,11 +1017,16 @@ function handleHeaderButtonClick(data: any) {
|
|||||||
class="workflow-api-markdown-wrap"
|
class="workflow-api-markdown-wrap"
|
||||||
@click="handleApiDocClick"
|
@click="handleApiDocClick"
|
||||||
>
|
>
|
||||||
<ElXMarkdown
|
<Suspense>
|
||||||
:markdown="apiDocMarkdown"
|
<ElXMarkdown
|
||||||
:allow-html="true"
|
:markdown="apiDocMarkdown"
|
||||||
:sanitize="false"
|
:allow-html="true"
|
||||||
/>
|
:sanitize="false"
|
||||||
|
/>
|
||||||
|
<template #fallback>
|
||||||
|
<ElSkeleton :rows="6" animated />
|
||||||
|
</template>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { EchartsUIType } from '@easyflow/plugins/echarts';
|
import type { EchartsUIType } from '@easyflow/plugins/echarts/workspace';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
DashboardAssistantTrendSeries,
|
DashboardAssistantTrendSeries,
|
||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
import { AnalysisChartCard } from '@easyflow/common-ui';
|
import { AnalysisChartCard } from '@easyflow/common-ui';
|
||||||
import { EchartsUI, useEcharts } from '@easyflow/plugins/echarts';
|
import { EchartsUI, useEcharts } from '@easyflow/plugins/echarts/workspace';
|
||||||
import { useUserStore } from '@easyflow/stores';
|
import { useUserStore } from '@easyflow/stores';
|
||||||
import { convertToRgb, downloadFileFromBlob } from '@easyflow/utils';
|
import { convertToRgb, downloadFileFromBlob } from '@easyflow/utils';
|
||||||
|
|
||||||
@@ -307,17 +307,15 @@ async function loadOverview() {
|
|||||||
errorMessage.value = '';
|
errorMessage.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [data] = await Promise.all([
|
void loadAssistantOptions().catch(() => undefined);
|
||||||
getDashboardOverview(buildOverviewQuery()),
|
const data = await getDashboardOverview(buildOverviewQuery());
|
||||||
loadAssistantOptions().catch(() => undefined),
|
|
||||||
]);
|
|
||||||
overview.value = data;
|
overview.value = data;
|
||||||
resetAssistantTrendSelection();
|
resetAssistantTrendSelection();
|
||||||
await renderCharts();
|
await renderCharts();
|
||||||
if (data.chatStatus?.available === false) {
|
if (data.chatStatus?.available === false) {
|
||||||
resetUserRanks();
|
resetUserRanks();
|
||||||
} else {
|
} else {
|
||||||
await loadUserRanks();
|
void loadUserRanks();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
overview.value = null;
|
overview.value = null;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { isLocalIcon, loadIconCollection } from './index';
|
||||||
|
|
||||||
|
describe('offline icon collections', () => {
|
||||||
|
it('keeps common icons in the startup registry', () => {
|
||||||
|
expect(isLocalIcon('mdi:github')).toBe(true);
|
||||||
|
expect(isLocalIcon('ant-design:appstore-outlined')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads uncommon icons from a bundled collection on demand', async () => {
|
||||||
|
expect(isLocalIcon('mdi:ab-testing')).toBe(false);
|
||||||
|
|
||||||
|
await loadIconCollection('mdi');
|
||||||
|
|
||||||
|
expect(isLocalIcon('mdi:ab-testing')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,21 +1,40 @@
|
|||||||
import antDesign from '@iconify/json/json/ant-design.json';
|
|
||||||
import ep from '@iconify/json/json/ep.json';
|
|
||||||
import mdi from '@iconify/json/json/mdi.json';
|
|
||||||
import {
|
|
||||||
addCollection as addOfflineCollection,
|
|
||||||
addIcon as addOfflineIcon,
|
|
||||||
Icon as IconifyIcon,
|
|
||||||
} from '@iconify/vue/offline';
|
|
||||||
import type {
|
import type {
|
||||||
IconifyIcon as IconifyIconData,
|
IconifyIcon as IconifyIconData,
|
||||||
IconifyJSON,
|
IconifyJSON,
|
||||||
} from '@iconify/vue/offline';
|
} from '@iconify/vue/offline';
|
||||||
|
|
||||||
|
import {
|
||||||
|
addCollection as addOfflineCollection,
|
||||||
|
addIcon as addOfflineIcon,
|
||||||
|
Icon as IconifyIcon,
|
||||||
|
} from '@iconify/vue/offline';
|
||||||
|
|
||||||
import { LOCAL_ICON_DATA } from './local-icons';
|
import { LOCAL_ICON_DATA } from './local-icons';
|
||||||
|
import { STARTUP_ICON_DATA } from './startup-icons';
|
||||||
|
|
||||||
|
type LazyCollectionPrefix = 'ant-design' | 'ep' | 'mdi';
|
||||||
|
|
||||||
const LOCAL_ICON_NAMES = new Set<string>();
|
const LOCAL_ICON_NAMES = new Set<string>();
|
||||||
|
const COLLECTION_LOADERS: Record<
|
||||||
|
LazyCollectionPrefix,
|
||||||
|
() => Promise<IconifyJSON>
|
||||||
|
> = {
|
||||||
|
'ant-design': async () => {
|
||||||
|
const collection = await import('@iconify/json/json/ant-design.json');
|
||||||
|
return collection.default;
|
||||||
|
},
|
||||||
|
ep: async () => {
|
||||||
|
const collection = await import('@iconify/json/json/ep.json');
|
||||||
|
return collection.default;
|
||||||
|
},
|
||||||
|
mdi: async () => {
|
||||||
|
const collection = await import('@iconify/json/json/mdi.json');
|
||||||
|
return collection.default;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const COLLECTION_LOAD_PROMISES = new Map<LazyCollectionPrefix, Promise<void>>();
|
||||||
|
|
||||||
function getCollectionPrefix(data: IconifyJSON, prefix?: string | boolean) {
|
function getCollectionPrefix(data: IconifyJSON, prefix?: boolean | string) {
|
||||||
if (prefix === false) {
|
if (prefix === false) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -25,7 +44,7 @@ function getCollectionPrefix(data: IconifyJSON, prefix?: string | boolean) {
|
|||||||
/**
|
/**
|
||||||
* Register an icon collection in the offline Iconify store and local name index.
|
* Register an icon collection in the offline Iconify store and local name index.
|
||||||
*/
|
*/
|
||||||
function registerCollection(data: IconifyJSON, prefix?: string | boolean) {
|
function registerCollection(data: IconifyJSON, prefix?: boolean | string) {
|
||||||
addOfflineCollection(data, prefix);
|
addOfflineCollection(data, prefix);
|
||||||
const iconPrefix = getCollectionPrefix(data, prefix);
|
const iconPrefix = getCollectionPrefix(data, prefix);
|
||||||
Object.keys(data.icons).forEach((name) => {
|
Object.keys(data.icons).forEach((name) => {
|
||||||
@@ -62,12 +81,66 @@ function isLocalIcon(name: string) {
|
|||||||
return LOCAL_ICON_NAMES.has(name);
|
return LOCAL_ICON_NAMES.has(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerCollection(antDesign);
|
function isLazyCollectionPrefix(
|
||||||
registerCollection(ep);
|
prefix: string,
|
||||||
registerCollection(mdi);
|
): prefix is LazyCollectionPrefix {
|
||||||
Object.entries(LOCAL_ICON_DATA).forEach(([name, data]) => {
|
return Object.prototype.hasOwnProperty.call(COLLECTION_LOADERS, prefix);
|
||||||
registerIcon(name, data);
|
}
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* Load a bundled offline icon collection once, without using a remote API.
|
||||||
|
*/
|
||||||
|
async function loadIconCollection(prefix: string) {
|
||||||
|
if (!isLazyCollectionPrefix(prefix)) {
|
||||||
|
return listIcons('', prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pending = COLLECTION_LOAD_PROMISES.get(prefix);
|
||||||
|
if (!pending) {
|
||||||
|
pending = COLLECTION_LOADERS[prefix]()
|
||||||
|
.then((data) => {
|
||||||
|
registerCollection(data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
COLLECTION_LOAD_PROMISES.delete(prefix);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
COLLECTION_LOAD_PROMISES.set(prefix, pending);
|
||||||
|
}
|
||||||
|
await pending;
|
||||||
|
return listIcons('', prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preserve historical menu icons by loading only the collections they need.
|
||||||
|
*/
|
||||||
|
async function loadMissingIconCollections(iconNames: string[]) {
|
||||||
|
const prefixes = new Set<LazyCollectionPrefix>();
|
||||||
|
iconNames.forEach((name) => {
|
||||||
|
if (isLocalIcon(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prefix = name.split(':', 1)[0] ?? '';
|
||||||
|
if (isLazyCollectionPrefix(prefix)) {
|
||||||
|
prefixes.add(prefix);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(
|
||||||
|
[...prefixes].map(async (prefix) => {
|
||||||
|
try {
|
||||||
|
await loadIconCollection(prefix);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load menu icon collection: ${prefix}`, error);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries({ ...LOCAL_ICON_DATA, ...STARTUP_ICON_DATA }).forEach(
|
||||||
|
([name, data]) => {
|
||||||
|
registerIcon(name, data);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export * from './create-icon';
|
export * from './create-icon';
|
||||||
|
|
||||||
@@ -75,9 +148,11 @@ export * from './lucide';
|
|||||||
|
|
||||||
export type { IconifyIcon as IconifyIconStructure } from '@iconify/vue/offline';
|
export type { IconifyIcon as IconifyIconStructure } from '@iconify/vue/offline';
|
||||||
export {
|
export {
|
||||||
|
registerCollection as addCollection,
|
||||||
|
registerIcon as addIcon,
|
||||||
IconifyIcon,
|
IconifyIcon,
|
||||||
isLocalIcon,
|
isLocalIcon,
|
||||||
listIcons,
|
listIcons,
|
||||||
registerCollection as addCollection,
|
loadIconCollection,
|
||||||
registerIcon as addIcon,
|
loadMissingIconCollections,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import type { IconifyIcon } from '@iconify/vue/offline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icons referenced directly by application source or default persisted menus.
|
||||||
|
* Full icon collections are loaded lazily when an uncommon icon is requested.
|
||||||
|
*/
|
||||||
|
export const STARTUP_ICON_DATA: Record<string, IconifyIcon> = {
|
||||||
|
'ant-design:apartment-outlined': {
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
body: '<path fill="currentColor" d="M908 640H804V488c0-4.4-3.6-8-8-8H548v-96h108c8.8 0 16-7.2 16-16V80c0-8.8-7.2-16-16-16H368c-8.8 0-16 7.2-16 16v288c0 8.8 7.2 16 16 16h108v96H228c-4.4 0-8 3.6-8 8v152H116c-8.8 0-16 7.2-16 16v288c0 8.8 7.2 16 16 16h288c8.8 0 16-7.2 16-16V656c0-8.8-7.2-16-16-16H292v-88h440v88H620c-8.8 0-16 7.2-16 16v288c0 8.8 7.2 16 16 16h288c8.8 0 16-7.2 16-16V656c0-8.8-7.2-16-16-16m-564 76v168H176V716zm84-408V140h168v168zm420 576H680V716h168z"/>',
|
||||||
|
},
|
||||||
|
'ant-design:appstore-outlined': {
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
body: '<path fill="currentColor" d="M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16m-52 268H212V212h200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16m-52 268H612V212h200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16m-52 268H212V612h200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16m-52 268H612V612h200z"/>',
|
||||||
|
},
|
||||||
|
'ep:arrow-right': {
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
body: '<path fill="currentColor" d="M340.864 149.312a30.59 30.59 0 0 0 0 42.752L652.736 512L340.864 831.872a30.59 30.59 0 0 0 0 42.752a29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"/>',
|
||||||
|
},
|
||||||
|
'ep:expand': {
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
body: '<path fill="currentColor" d="M128 192h768v128H128zm0 256h512v128H128zm0 256h768v128H128zm576-352l192 160l-192 128z"/>',
|
||||||
|
},
|
||||||
|
'ep:fold': {
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
body: '<path fill="currentColor" d="M896 192H128v128h768zm0 256H384v128h512zm0 256H128v128h768zM320 384L128 512l192 128z"/>',
|
||||||
|
},
|
||||||
|
'mdi:chat-outline': {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
body: '<path fill="currentColor" d="M12 3C6.5 3 2 6.58 2 11a7.22 7.22 0 0 0 2.75 5.5c0 .6-.42 2.17-2.75 4.5c2.37-.11 4.64-1 6.47-2.5c1.14.33 2.34.5 3.53.5c5.5 0 10-3.58 10-8s-4.5-8-10-8m0 14c-4.42 0-8-2.69-8-6s3.58-6 8-6s8 2.69 8 6s-3.58 6-8 6"/>',
|
||||||
|
},
|
||||||
|
'mdi:clock-time-five-outline': {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
body: '<path fill="currentColor" d="M12 20c4.4 0 8-3.6 8-8s-3.6-8-8-8s-8 3.6-8 8s3.6 8 8 8m0-18c5.5 0 10 4.5 10 10s-4.5 10-10 10S2 17.5 2 12S6.5 2 12 2m3.3 14.2L14 17l-3-5.2V7h1.5v4.4z"/>',
|
||||||
|
},
|
||||||
|
'mdi:github': {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
body: '<path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"/>',
|
||||||
|
},
|
||||||
|
'mdi:hammer': {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
body: '<path fill="currentColor" d="M2 19.63L13.43 8.2l-.71-.7l1.42-1.43L12 3.89c1.2-1.19 3.09-1.19 4.27 0l3.6 3.61l-1.42 1.41h2.84l.71.71l-3.55 3.59l-.71-.71V9.62l-1.47 1.42l-.71-.71L4.13 21.76z"/>',
|
||||||
|
},
|
||||||
|
'mdi:home-outline': {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
body: '<path fill="currentColor" d="m12 5.69l5 4.5V18h-2v-6H9v6H7v-7.81zM12 3L2 12h3v8h6v-6h2v6h6v-8h3"/>',
|
||||||
|
},
|
||||||
|
'mdi:image-outline': {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
body: '<path fill="currentColor" d="M19 19H5V5h14m0-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-5.04 9.29l-2.75 3.54l-1.96-2.36L6.5 17h11z"/>',
|
||||||
|
},
|
||||||
|
'mdi:keyboard-esc': {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
body: '<path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2"/>',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -91,6 +91,7 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<li
|
<li
|
||||||
|
:data-menu-path="path"
|
||||||
:class="[
|
:class="[
|
||||||
rootMenu.theme,
|
rootMenu.theme,
|
||||||
b(),
|
b(),
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
close: [string, string[]];
|
close: [string, string[]];
|
||||||
|
enter: [string];
|
||||||
open: [string, string[]];
|
open: [string, string[]];
|
||||||
select: [string, string[]];
|
select: [string, string[]];
|
||||||
}>();
|
}>();
|
||||||
@@ -257,6 +258,25 @@ function handleMenuItemClick(data: MenuItemClicked) {
|
|||||||
emit('select', path, parentPaths);
|
emit('select', path, parentPaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let enteredPath = '';
|
||||||
|
|
||||||
|
function handleMenuItemEnter(event: Event) {
|
||||||
|
const target = event.target;
|
||||||
|
if (!(target instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const menuItem = target.closest<HTMLElement>('[data-menu-path]');
|
||||||
|
if (!menuItem || !menu.value?.contains(menuItem)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const path = menuItem.dataset.menuPath;
|
||||||
|
if (!path || path === enteredPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enteredPath = path;
|
||||||
|
emit('enter', path);
|
||||||
|
}
|
||||||
|
|
||||||
function handleSubMenuClick({ parentPaths, path }: MenuItemRegistered) {
|
function handleSubMenuClick({ parentPaths, path }: MenuItemRegistered) {
|
||||||
const isOpened = openedMenus.value.includes(path);
|
const isOpened = openedMenus.value.includes(path);
|
||||||
|
|
||||||
@@ -349,6 +369,9 @@ function getActivePaths() {
|
|||||||
]"
|
]"
|
||||||
:style="menuStyle"
|
:style="menuStyle"
|
||||||
role="menu"
|
role="menu"
|
||||||
|
@focusin="handleMenuItemEnter"
|
||||||
|
@mouseleave="enteredPath = ''"
|
||||||
|
@mouseover="handleMenuItemEnter"
|
||||||
>
|
>
|
||||||
<template v-if="mode === 'horizontal' && getSlot.showSlotMore">
|
<template v-if="mode === 'horizontal' && getSlot.showSlotMore">
|
||||||
<template v-for="item in getSlot.slotDefault" :key="item.key">
|
<template v-for="item in getSlot.slotDefault" :key="item.key">
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import type { VNode } from 'vue';
|
|||||||
import { computed, ref, useAttrs, watch, watchEffect } from 'vue';
|
import { computed, ref, useAttrs, watch, watchEffect } from 'vue';
|
||||||
|
|
||||||
import { usePagination } from '@easyflow/hooks';
|
import { usePagination } from '@easyflow/hooks';
|
||||||
import { EmptyIcon, Grip, listIcons } from '@easyflow/icons';
|
import {
|
||||||
|
EmptyIcon,
|
||||||
|
Grip,
|
||||||
|
listIcons,
|
||||||
|
loadIconCollection,
|
||||||
|
} from '@easyflow/icons';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -75,6 +80,30 @@ const currentPage = ref(1);
|
|||||||
const keyword = ref('');
|
const keyword = ref('');
|
||||||
const keywordDebounce = refDebounced(keyword, 300);
|
const keywordDebounce = refDebounced(keyword, 300);
|
||||||
const innerIcons = ref<string[]>([]);
|
const innerIcons = ref<string[]>([]);
|
||||||
|
let iconLoadRequestId = 0;
|
||||||
|
|
||||||
|
const commonSvgIcons = [
|
||||||
|
'svg:talk',
|
||||||
|
'svg:plugin',
|
||||||
|
'svg:workflow',
|
||||||
|
'svg:approval',
|
||||||
|
'svg:knowledge',
|
||||||
|
'svg:resource',
|
||||||
|
'svg:data-center',
|
||||||
|
'svg:llm',
|
||||||
|
'svg:account',
|
||||||
|
'svg:role',
|
||||||
|
'svg:menu',
|
||||||
|
'svg:department',
|
||||||
|
'svg:time',
|
||||||
|
'svg:log',
|
||||||
|
'svg:chat-history',
|
||||||
|
'svg:setting',
|
||||||
|
'svg:api',
|
||||||
|
'svg:user-feedback',
|
||||||
|
'svg:oauth',
|
||||||
|
'svg:mcp',
|
||||||
|
];
|
||||||
|
|
||||||
/* 当检索关键词变化时,重置分页 */
|
/* 当检索关键词变化时,重置分页 */
|
||||||
watch(keywordDebounce, () => {
|
watch(keywordDebounce, () => {
|
||||||
@@ -82,36 +111,39 @@ watch(keywordDebounce, () => {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
watchDebounced(
|
async function refreshInnerIcons(prefix?: string) {
|
||||||
() => props.prefix,
|
if (!prefix || prefix === 'svg' || !props.autoFetchApi) {
|
||||||
async (prefix) => {
|
return;
|
||||||
if (prefix && prefix !== 'svg' && props.autoFetchApi) {
|
}
|
||||||
innerIcons.value = [
|
const requestId = ++iconLoadRequestId;
|
||||||
'svg:talk',
|
try {
|
||||||
'svg:plugin',
|
await loadIconCollection(prefix);
|
||||||
'svg:workflow',
|
} catch (error) {
|
||||||
'svg:approval',
|
console.error(`Failed to load icon collection: ${prefix}`, error);
|
||||||
'svg:knowledge',
|
}
|
||||||
'svg:resource',
|
if (requestId === iconLoadRequestId && prefix === props.prefix) {
|
||||||
'svg:data-center',
|
innerIcons.value = [...commonSvgIcons, ...getLocalIconsData(prefix, true)];
|
||||||
'svg:llm',
|
}
|
||||||
'svg:account',
|
}
|
||||||
'svg:role',
|
|
||||||
'svg:menu',
|
watch(
|
||||||
'svg:department',
|
visible,
|
||||||
'svg:time',
|
(isVisible) => {
|
||||||
'svg:log',
|
if (isVisible) {
|
||||||
'svg:chat-history',
|
void refreshInnerIcons(props.prefix);
|
||||||
'svg:setting',
|
|
||||||
'svg:api',
|
|
||||||
'svg:user-feedback',
|
|
||||||
'svg:oauth',
|
|
||||||
'svg:mcp',
|
|
||||||
...getLocalIconsData(prefix),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true, debounce: 500, maxWait: 1000 },
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watchDebounced(
|
||||||
|
() => props.prefix,
|
||||||
|
(prefix) => {
|
||||||
|
if (visible.value) {
|
||||||
|
void refreshInnerIcons(prefix);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ debounce: 300, maxWait: 600 },
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentList = computed(() => {
|
const currentList = computed(() => {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export const ICONS_MAP: Recordable<string[]> = {};
|
|||||||
* @param prefix 图标集名称
|
* @param prefix 图标集名称
|
||||||
* @returns 图标集中包含的所有图标名称
|
* @returns 图标集中包含的所有图标名称
|
||||||
*/
|
*/
|
||||||
export function getLocalIconsData(prefix: string): string[] {
|
export function getLocalIconsData(prefix: string, refresh = false): string[] {
|
||||||
if (Reflect.has(ICONS_MAP, prefix) && ICONS_MAP[prefix]) {
|
if (!refresh && Reflect.has(ICONS_MAP, prefix) && ICONS_MAP[prefix]) {
|
||||||
return ICONS_MAP[prefix];
|
return ICONS_MAP[prefix];
|
||||||
}
|
}
|
||||||
ICONS_MAP[prefix] = listIcons('', prefix);
|
ICONS_MAP[prefix] = listIcons('', prefix);
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ const showHeaderNav = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
handleMenuEnter,
|
||||||
handleMenuSelect,
|
handleMenuSelect,
|
||||||
handleMenuOpen,
|
handleMenuOpen,
|
||||||
headerActive,
|
headerActive,
|
||||||
@@ -302,6 +303,7 @@ const headerSlots = computed(() => {
|
|||||||
:theme="headerTheme"
|
:theme="headerTheme"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
|
@enter="handleMenuEnter"
|
||||||
@select="handleMenuSelect"
|
@select="handleMenuSelect"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -330,6 +332,7 @@ const headerSlots = computed(() => {
|
|||||||
:rounded="isMenuRounded"
|
:rounded="isMenuRounded"
|
||||||
:theme="sidebarTheme"
|
:theme="sidebarTheme"
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
|
@enter="handleMenuEnter"
|
||||||
@open="handleMenuOpen"
|
@open="handleMenuOpen"
|
||||||
@select="handleMenuSelect"
|
@select="handleMenuSelect"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
enter: [string];
|
||||||
open: [string, string[]];
|
open: [string, string[]];
|
||||||
select: [string, string?];
|
select: [string, string?];
|
||||||
}>();
|
}>();
|
||||||
@@ -26,6 +27,10 @@ function handleMenuSelect(key: string) {
|
|||||||
function handleMenuOpen(key: string, path: string[]) {
|
function handleMenuOpen(key: string, path: string[]) {
|
||||||
emit('open', key, path);
|
emit('open', key, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMenuEnter(key: string) {
|
||||||
|
emit('enter', key);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -39,6 +44,7 @@ function handleMenuOpen(key: string, path: string[]) {
|
|||||||
:rounded="rounded"
|
:rounded="rounded"
|
||||||
scroll-to-active
|
scroll-to-active
|
||||||
:theme="theme"
|
:theme="theme"
|
||||||
|
@enter="handleMenuEnter"
|
||||||
@open="handleMenuOpen"
|
@open="handleMenuOpen"
|
||||||
@select="handleMenuSelect"
|
@select="handleMenuSelect"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useNavigation } from './use-navigation';
|
|||||||
|
|
||||||
function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
|
function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const { navigation, prefetch, willOpenedByWindow } = useNavigation();
|
const { navigation, schedulePrefetch, willOpenedByWindow } = useNavigation();
|
||||||
|
|
||||||
const menus = computed(() => useRootMenus?.value ?? accessStore.accessMenus);
|
const menus = computed(() => useRootMenus?.value ?? accessStore.accessMenus);
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMenuMouseEnter = (menu: MenuRecordRaw) => {
|
const handleMenuMouseEnter = (menu: MenuRecordRaw) => {
|
||||||
prefetch(menu.path);
|
schedulePrefetch(menu.children?.[0]?.path ?? menu.path);
|
||||||
|
|
||||||
if (!preferences.sidebar.expandOnHover) {
|
if (!preferences.sidebar.expandOnHover) {
|
||||||
const { findMenu } = findRootMenuByPath(menus.value, menu.path);
|
const { findMenu } = findRootMenuByPath(menus.value, menu.path);
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import { findRootMenuByPath } from '@easyflow/utils';
|
|||||||
|
|
||||||
import { useNavigation } from './use-navigation';
|
import { useNavigation } from './use-navigation';
|
||||||
|
|
||||||
|
const MAX_AUTOMATIC_PREFETCH_COUNT = 3;
|
||||||
|
const PREFETCH_INTERVAL = 120;
|
||||||
|
|
||||||
function useMixedMenu() {
|
function useMixedMenu() {
|
||||||
const { navigation, prefetch, willOpenedByWindow } = useNavigation();
|
const { navigation, prefetch, schedulePrefetch, willOpenedByWindow } =
|
||||||
|
useNavigation();
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const splitSideMenus = ref<MenuRecordRaw[]>([]);
|
const splitSideMenus = ref<MenuRecordRaw[]>([]);
|
||||||
@@ -97,11 +101,13 @@ function useMixedMenu() {
|
|||||||
if (!willOpenedByWindow(key)) {
|
if (!willOpenedByWindow(key)) {
|
||||||
rootMenuPath.value = rootMenu?.path ?? '';
|
rootMenuPath.value = rootMenu?.path ?? '';
|
||||||
splitSideMenus.value = _splitSideMenus;
|
splitSideMenus.value = _splitSideMenus;
|
||||||
_splitSideMenus.forEach((menu) => {
|
_splitSideMenus
|
||||||
if (menu.path) {
|
.slice(0, MAX_AUTOMATIC_PREFETCH_COUNT)
|
||||||
prefetch(menu.path);
|
.forEach((menu, index) => {
|
||||||
}
|
if (menu.path) {
|
||||||
});
|
schedulePrefetch(menu.path, PREFETCH_INTERVAL * (index + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_splitSideMenus.length === 0) {
|
if (_splitSideMenus.length === 0) {
|
||||||
@@ -130,6 +136,10 @@ function useMixedMenu() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMenuEnter = (key: string) => {
|
||||||
|
schedulePrefetch(key);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算侧边菜单
|
* 计算侧边菜单
|
||||||
* @param path 路由路径
|
* @param path 路由路径
|
||||||
@@ -166,6 +176,7 @@ function useMixedMenu() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
handleMenuEnter,
|
||||||
handleMenuSelect,
|
handleMenuSelect,
|
||||||
handleMenuOpen,
|
handleMenuOpen,
|
||||||
headerActive,
|
headerActive,
|
||||||
|
|||||||
@@ -4,11 +4,37 @@ import { useRouter } from 'vue-router';
|
|||||||
|
|
||||||
import { isHttpUrl, openRouteInNewWindow, openWindow } from '@easyflow/utils';
|
import { isHttpUrl, openRouteInNewWindow, openWindow } from '@easyflow/utils';
|
||||||
|
|
||||||
|
interface NetworkConnectionLike {
|
||||||
|
effectiveType?: string;
|
||||||
|
saveData?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canSpeculativelyPrefetch() {
|
||||||
|
if (typeof navigator === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const nav = navigator as Navigator & {
|
||||||
|
connection?: NetworkConnectionLike;
|
||||||
|
mozConnection?: NetworkConnectionLike;
|
||||||
|
webkitConnection?: NetworkConnectionLike;
|
||||||
|
};
|
||||||
|
const connection =
|
||||||
|
nav.connection ?? nav.mozConnection ?? nav.webkitConnection;
|
||||||
|
if (!connection) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
!connection.saveData &&
|
||||||
|
!['2g', '3g', 'slow-2g'].includes(connection.effectiveType ?? '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function useNavigation() {
|
function useNavigation() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const routeMetaMap = new Map<string, RouteRecordNormalized>();
|
const routeMetaMap = new Map<string, RouteRecordNormalized>();
|
||||||
const prefetchedPaths = new Set<string>();
|
const prefetchedPaths = new Set<string>();
|
||||||
const prefetchingPaths = new Set<string>();
|
const prefetchingPaths = new Set<string>();
|
||||||
|
const scheduledPrefetches = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
// 初始化路由映射
|
// 初始化路由映射
|
||||||
const initRouteMetaMap = () => {
|
const initRouteMetaMap = () => {
|
||||||
@@ -40,6 +66,12 @@ function useNavigation() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const prefetch = (path: string) => {
|
const prefetch = (path: string) => {
|
||||||
|
const scheduledPrefetch = scheduledPrefetches.get(path);
|
||||||
|
if (scheduledPrefetch) {
|
||||||
|
clearTimeout(scheduledPrefetch);
|
||||||
|
scheduledPrefetches.delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isHttpUrl(path) ||
|
isHttpUrl(path) ||
|
||||||
prefetchedPaths.has(path) ||
|
prefetchedPaths.has(path) ||
|
||||||
@@ -84,6 +116,24 @@ function useNavigation() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const schedulePrefetch = (path: string, delay = 120) => {
|
||||||
|
if (
|
||||||
|
!canSpeculativelyPrefetch() ||
|
||||||
|
isHttpUrl(path) ||
|
||||||
|
prefetchedPaths.has(path) ||
|
||||||
|
prefetchingPaths.has(path) ||
|
||||||
|
scheduledPrefetches.has(path)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
scheduledPrefetches.delete(path);
|
||||||
|
prefetch(path);
|
||||||
|
}, delay);
|
||||||
|
scheduledPrefetches.set(path, timer);
|
||||||
|
};
|
||||||
|
|
||||||
const navigation = async (path: string) => {
|
const navigation = async (path: string) => {
|
||||||
try {
|
try {
|
||||||
const route = routeMetaMap.get(path);
|
const route = routeMetaMap.get(path);
|
||||||
@@ -116,7 +166,7 @@ function useNavigation() {
|
|||||||
return shouldOpenInNewWindow(path);
|
return shouldOpenInNewWindow(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { navigation, prefetch, willOpenedByWindow };
|
return { navigation, prefetch, schedulePrefetch, willOpenedByWindow };
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useNavigation };
|
export { useNavigation };
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
"types": "./src/echarts/index.ts",
|
"types": "./src/echarts/index.ts",
|
||||||
"default": "./src/echarts/index.ts"
|
"default": "./src/echarts/index.ts"
|
||||||
},
|
},
|
||||||
|
"./echarts/workspace": {
|
||||||
|
"types": "./src/echarts/workspace.ts",
|
||||||
|
"default": "./src/echarts/workspace.ts"
|
||||||
|
},
|
||||||
"./vxe-table": {
|
"./vxe-table": {
|
||||||
"types": "./src/vxe-table/index.ts",
|
"types": "./src/vxe-table/index.ts",
|
||||||
"default": "./src/vxe-table/index.ts"
|
"default": "./src/vxe-table/index.ts"
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import type { EChartsOption } from 'echarts';
|
||||||
|
import type { EChartsType } from 'echarts/core';
|
||||||
|
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { Nullable } from '@easyflow/types';
|
||||||
|
|
||||||
|
import type EchartsUI from './echarts-ui.vue';
|
||||||
|
|
||||||
|
import { computed, nextTick, watch } from 'vue';
|
||||||
|
|
||||||
|
import { usePreferences } from '@easyflow/preferences';
|
||||||
|
|
||||||
|
import {
|
||||||
|
tryOnUnmounted,
|
||||||
|
useDebounceFn,
|
||||||
|
useResizeObserver,
|
||||||
|
useTimeoutFn,
|
||||||
|
useWindowSize,
|
||||||
|
} from '@vueuse/core';
|
||||||
|
|
||||||
|
type EchartsUIType = typeof EchartsUI | undefined;
|
||||||
|
|
||||||
|
interface EchartsEngine {
|
||||||
|
init: (element: HTMLElement) => EChartsType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUseEcharts(echarts: EchartsEngine) {
|
||||||
|
return function useEcharts(chartRef: Ref<EchartsUIType>) {
|
||||||
|
let chartInstance: EChartsType | null = null;
|
||||||
|
let cacheOptions: EChartsOption = {};
|
||||||
|
|
||||||
|
const { isDark } = usePreferences();
|
||||||
|
const { height, width } = useWindowSize();
|
||||||
|
const resizeHandler: () => void = useDebounceFn(resize, 200);
|
||||||
|
|
||||||
|
const getChartEl = (): HTMLElement | null => {
|
||||||
|
const refValue = chartRef?.value as unknown;
|
||||||
|
if (!refValue) return null;
|
||||||
|
if (refValue instanceof HTMLElement) {
|
||||||
|
return refValue;
|
||||||
|
}
|
||||||
|
const maybeComponent = refValue as { $el?: HTMLElement };
|
||||||
|
return maybeComponent.$el ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isElHidden = (el: HTMLElement | null): boolean => {
|
||||||
|
if (!el) return true;
|
||||||
|
return el.offsetHeight === 0 || el.offsetWidth === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOptions = computed((): EChartsOption => {
|
||||||
|
if (!isDark.value) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const initCharts = () => {
|
||||||
|
const el = chartRef?.value?.$el;
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chartInstance = echarts.init(el);
|
||||||
|
|
||||||
|
return chartInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderEcharts = (
|
||||||
|
options: EChartsOption,
|
||||||
|
clear = true,
|
||||||
|
): Promise<Nullable<EChartsType>> => {
|
||||||
|
cacheOptions = options;
|
||||||
|
const currentOptions = {
|
||||||
|
...options,
|
||||||
|
...getOptions.value,
|
||||||
|
};
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (chartRef.value?.offsetHeight === 0) {
|
||||||
|
useTimeoutFn(async () => {
|
||||||
|
resolve(await renderEcharts(currentOptions));
|
||||||
|
}, 30);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nextTick(() => {
|
||||||
|
const el = getChartEl();
|
||||||
|
if (isElHidden(el)) {
|
||||||
|
useTimeoutFn(async () => {
|
||||||
|
resolve(await renderEcharts(currentOptions));
|
||||||
|
}, 30);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
useTimeoutFn(() => {
|
||||||
|
if (!chartInstance) {
|
||||||
|
const instance = initCharts();
|
||||||
|
if (!instance) return;
|
||||||
|
}
|
||||||
|
clear && chartInstance?.clear();
|
||||||
|
chartInstance?.setOption(currentOptions);
|
||||||
|
resolve(chartInstance);
|
||||||
|
}, 30);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
const el = getChartEl();
|
||||||
|
if (isElHidden(el)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chartInstance?.resize({
|
||||||
|
animation: {
|
||||||
|
duration: 300,
|
||||||
|
easing: 'quadraticIn',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([width, height], () => {
|
||||||
|
resizeHandler?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
useResizeObserver(chartRef as never, resizeHandler);
|
||||||
|
|
||||||
|
watch(isDark, () => {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.dispose();
|
||||||
|
initCharts();
|
||||||
|
renderEcharts(cacheOptions);
|
||||||
|
resize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tryOnUnmounted(() => {
|
||||||
|
chartInstance?.dispose();
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
renderEcharts,
|
||||||
|
resize,
|
||||||
|
getChartInstance: () => chartInstance,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createUseEcharts };
|
||||||
|
export type { EchartsUIType };
|
||||||
@@ -1,146 +1,7 @@
|
|||||||
import type { EChartsOption } from 'echarts';
|
|
||||||
|
|
||||||
import type { Ref } from 'vue';
|
|
||||||
|
|
||||||
import type { Nullable } from '@easyflow/types';
|
|
||||||
|
|
||||||
import type EchartsUI from './echarts-ui.vue';
|
|
||||||
|
|
||||||
import { computed, nextTick, watch } from 'vue';
|
|
||||||
|
|
||||||
import { usePreferences } from '@easyflow/preferences';
|
|
||||||
|
|
||||||
import {
|
|
||||||
tryOnUnmounted,
|
|
||||||
useDebounceFn,
|
|
||||||
useResizeObserver,
|
|
||||||
useTimeoutFn,
|
|
||||||
useWindowSize,
|
|
||||||
} from '@vueuse/core';
|
|
||||||
|
|
||||||
import echarts from './echarts';
|
import echarts from './echarts';
|
||||||
|
import { createUseEcharts } from './use-echarts-base';
|
||||||
|
|
||||||
type EchartsUIType = typeof EchartsUI | undefined;
|
const useEcharts = createUseEcharts(echarts);
|
||||||
|
|
||||||
function useEcharts(chartRef: Ref<EchartsUIType>) {
|
|
||||||
let chartInstance: echarts.ECharts | null = null;
|
|
||||||
let cacheOptions: EChartsOption = {};
|
|
||||||
|
|
||||||
const { isDark } = usePreferences();
|
|
||||||
const { height, width } = useWindowSize();
|
|
||||||
const resizeHandler: () => void = useDebounceFn(resize, 200);
|
|
||||||
|
|
||||||
const getChartEl = (): HTMLElement | null => {
|
|
||||||
const refValue = chartRef?.value as unknown;
|
|
||||||
if (!refValue) return null;
|
|
||||||
if (refValue instanceof HTMLElement) {
|
|
||||||
return refValue;
|
|
||||||
}
|
|
||||||
const maybeComponent = refValue as { $el?: HTMLElement };
|
|
||||||
return maybeComponent.$el ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isElHidden = (el: HTMLElement | null): boolean => {
|
|
||||||
if (!el) return true;
|
|
||||||
return el.offsetHeight === 0 || el.offsetWidth === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOptions = computed((): EChartsOption => {
|
|
||||||
if (!isDark.value) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const initCharts = () => {
|
|
||||||
const el = chartRef?.value?.$el;
|
|
||||||
if (!el) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chartInstance = echarts.init(el);
|
|
||||||
|
|
||||||
return chartInstance;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderEcharts = (
|
|
||||||
options: EChartsOption,
|
|
||||||
clear = true,
|
|
||||||
): Promise<Nullable<echarts.ECharts>> => {
|
|
||||||
cacheOptions = options;
|
|
||||||
const currentOptions = {
|
|
||||||
...options,
|
|
||||||
...getOptions.value,
|
|
||||||
};
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (chartRef.value?.offsetHeight === 0) {
|
|
||||||
useTimeoutFn(async () => {
|
|
||||||
resolve(await renderEcharts(currentOptions));
|
|
||||||
}, 30);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
nextTick(() => {
|
|
||||||
const el = getChartEl();
|
|
||||||
if (isElHidden(el)) {
|
|
||||||
useTimeoutFn(async () => {
|
|
||||||
resolve(await renderEcharts(currentOptions));
|
|
||||||
}, 30);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
useTimeoutFn(() => {
|
|
||||||
if (!chartInstance) {
|
|
||||||
const instance = initCharts();
|
|
||||||
if (!instance) return;
|
|
||||||
}
|
|
||||||
clear && chartInstance?.clear();
|
|
||||||
chartInstance?.setOption(currentOptions);
|
|
||||||
resolve(chartInstance);
|
|
||||||
}, 30);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
const el = getChartEl();
|
|
||||||
if (isElHidden(el)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
chartInstance?.resize({
|
|
||||||
animation: {
|
|
||||||
duration: 300,
|
|
||||||
easing: 'quadraticIn',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch([width, height], () => {
|
|
||||||
resizeHandler?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
useResizeObserver(chartRef as never, resizeHandler);
|
|
||||||
|
|
||||||
watch(isDark, () => {
|
|
||||||
if (chartInstance) {
|
|
||||||
chartInstance.dispose();
|
|
||||||
initCharts();
|
|
||||||
renderEcharts(cacheOptions);
|
|
||||||
resize();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tryOnUnmounted(() => {
|
|
||||||
// 销毁实例,释放资源
|
|
||||||
chartInstance?.dispose();
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
renderEcharts,
|
|
||||||
resize,
|
|
||||||
getChartInstance: () => chartInstance,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useEcharts };
|
export { useEcharts };
|
||||||
|
export type { EchartsUIType } from './use-echarts-base';
|
||||||
export type { EchartsUIType };
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { LineChart } from 'echarts/charts';
|
||||||
|
import {
|
||||||
|
GridComponent,
|
||||||
|
LegendComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
} from 'echarts/components';
|
||||||
|
import * as echarts from 'echarts/core';
|
||||||
|
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
|
||||||
|
echarts.use([
|
||||||
|
LineChart,
|
||||||
|
TooltipComponent,
|
||||||
|
GridComponent,
|
||||||
|
LegendComponent,
|
||||||
|
LabelLayout,
|
||||||
|
UniversalTransition,
|
||||||
|
CanvasRenderer,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default echarts;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { createUseEcharts } from './use-echarts-base';
|
||||||
|
import workspaceEcharts from './workspace-echarts';
|
||||||
|
|
||||||
|
const useEcharts = createUseEcharts(workspaceEcharts);
|
||||||
|
|
||||||
|
export { default as EchartsUI } from './echarts-ui.vue';
|
||||||
|
export { useEcharts };
|
||||||
|
export type { EchartsUIType } from './use-echarts-base';
|
||||||
@@ -22,7 +22,7 @@ RUN rm -f /etc/nginx/conf.d/default.conf
|
|||||||
COPY --from=builder /app/app/dist/ /usr/share/nginx/html/flow/
|
COPY --from=builder /app/app/dist/ /usr/share/nginx/html/flow/
|
||||||
COPY scripts/deploy/nginx.conf /etc/nginx/nginx.conf
|
COPY scripts/deploy/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
RUN chown -R nginx:nginx /usr/share/nginx/html
|
RUN nginx -t && chown -R nginx:nginx /usr/share/nginx/html
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,22 @@ http {
|
|||||||
keepalive_timeout 65;
|
keepalive_timeout 65;
|
||||||
client_max_body_size 500m;
|
client_max_body_size 500m;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_static on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_comp_level 5;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_types
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
application/wasm
|
||||||
|
image/svg+xml
|
||||||
|
text/css
|
||||||
|
text/plain
|
||||||
|
text/xml;
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
map $http_upgrade $connection_upgrade {
|
||||||
default upgrade;
|
default upgrade;
|
||||||
'' close;
|
'' close;
|
||||||
@@ -90,9 +106,26 @@ http {
|
|||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ^~ /flow/ {
|
location = /flow/index.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /flow/_app.config.js {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* ^/flow/(?:avif|css|eot|gif|ico|jpeg|jpg|js|jse|png|svg|ttf|wasm|webp|woff|woff2)/ {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri =404;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /flow/ {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
try_files $uri $uri/ /flow/index.html;
|
try_files $uri $uri/ /flow/index.html;
|
||||||
|
add_header Cache-Control "no-cache" always;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user