mirror of https://github.com/certd/certd
				
				
				
			chore: 插件编辑与运行测试beta
							parent
							
								
									c021dd03d3
								
							
						
					
					
						commit
						a0eeb17d73
					
				| 
						 | 
				
			
			@ -3,14 +3,23 @@ import { AbstractTaskPlugin } from "./api.js";
 | 
			
		|||
import { pluginGroups } from "./group.js";
 | 
			
		||||
 | 
			
		||||
const onRegister = ({ key, value }: OnRegisterContext<AbstractTaskPlugin>) => {
 | 
			
		||||
  //如果有相同名字的先移除
 | 
			
		||||
 | 
			
		||||
  for (const group of Object.values(pluginGroups)) {
 | 
			
		||||
    const index = group.plugins.findIndex(plugin => plugin.name === key);
 | 
			
		||||
    if (index > -1) {
 | 
			
		||||
      group.plugins.splice(index, 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const group = value?.define?.group as string;
 | 
			
		||||
  if (group) {
 | 
			
		||||
    if (pluginGroups.hasOwnProperty(group)) {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      pluginGroups[group].plugins.push(value.define);
 | 
			
		||||
    } else {
 | 
			
		||||
      pluginGroups.other.plugins.push(value.define);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  pluginGroups.other.plugins.push(value.define);
 | 
			
		||||
};
 | 
			
		||||
export const pluginRegistry = createRegistry<AbstractTaskPlugin>("plugin", onRegister);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,6 @@ export function registerWorker(name: string, worker: any) {
 | 
			
		|||
window.MonacoEnvironment = {
 | 
			
		||||
  //@ts-ignore
 | 
			
		||||
  getWorker(_, label) {
 | 
			
		||||
    debugger;
 | 
			
		||||
    const custom = WorkerBucket[label];
 | 
			
		||||
    if (custom) {
 | 
			
		||||
      return new custom();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -83,17 +83,6 @@ const emitValue = lodashDebounce((value: any) => {
 | 
			
		|||
  emits("update:modelValue", value);
 | 
			
		||||
}, props.debounce || 500);
 | 
			
		||||
 | 
			
		||||
// watch(
 | 
			
		||||
//   () => {
 | 
			
		||||
//     return props.modelValue;
 | 
			
		||||
//   },
 | 
			
		||||
//   (value: string) => {
 | 
			
		||||
//     if (instanceRef.value && value !== instanceRef.value.getValue()) {
 | 
			
		||||
//       // instanceRef.value.setValue(value);
 | 
			
		||||
//     }
 | 
			
		||||
//   }
 | 
			
		||||
// );
 | 
			
		||||
 | 
			
		||||
async function createEditor(ctx: EditorCodeCtx) {
 | 
			
		||||
  disposeEditor();
 | 
			
		||||
  const instance = monaco.editor.create(monacoRef.value, {
 | 
			
		||||
| 
						 | 
				
			
			@ -121,6 +110,9 @@ async function createEditor(ctx: EditorCodeCtx) {
 | 
			
		|||
  instanceRef = instance;
 | 
			
		||||
  ctx.instance = instance;
 | 
			
		||||
  emits("ready", ctx);
 | 
			
		||||
  if (props.modelValue) {
 | 
			
		||||
    instanceRef.setValue(props.modelValue);
 | 
			
		||||
  }
 | 
			
		||||
  return instance;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -224,6 +216,9 @@ watch(
 | 
			
		|||
        editor.setValue(newValue);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    immediate: true,
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,7 +49,6 @@ export async function initWorkers() {
 | 
			
		|||
      } else if (label === "typescript" || label === "javascript") {
 | 
			
		||||
        return new tsWorker.default();
 | 
			
		||||
      } else if (label === "yaml" || label === "yml") {
 | 
			
		||||
        debugger;
 | 
			
		||||
        //@ts-ignore
 | 
			
		||||
        return new yamlWorker.default();
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,7 +65,7 @@ export default router;
 | 
			
		|||
//   // 多页控制 打开新的页面
 | 
			
		||||
//   const pageStore = usePageStore();
 | 
			
		||||
//   // for (const item of to.matched) {
 | 
			
		||||
//   //   pageStore.keepAlivePush(item.name);
 | 
			
		||||
//   //   pageStore.cachePush(item.name);
 | 
			
		||||
//   // }
 | 
			
		||||
//   pageStore.open(to);
 | 
			
		||||
//   // 更改标题
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,7 +58,7 @@ function transformOneResource(resource: any, parent: any) {
 | 
			
		|||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    menu,
 | 
			
		||||
    route
 | 
			
		||||
    route,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -79,7 +79,7 @@ export const buildMenusAndRouters = (resources: any, parent: any = null) => {
 | 
			
		|||
  setIndex(menus);
 | 
			
		||||
  return {
 | 
			
		||||
    routes,
 | 
			
		||||
    menus
 | 
			
		||||
    menus,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,6 +47,7 @@ export const sysResources = [
 | 
			
		|||
        meta: {
 | 
			
		||||
          icon: "ion:earth-outline",
 | 
			
		||||
          permission: "sys:settings:view",
 | 
			
		||||
          cache: true,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +111,7 @@ export const sysResources = [
 | 
			
		|||
        meta: {
 | 
			
		||||
          icon: "ion:extension-puzzle-outline",
 | 
			
		||||
          permission: "sys:settings:view",
 | 
			
		||||
          cache: true,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
| 
						 | 
				
			
			@ -121,6 +123,7 @@ export const sysResources = [
 | 
			
		|||
          isMenu: false,
 | 
			
		||||
          icon: "ion:extension-puzzle",
 | 
			
		||||
          permission: "sys:settings:view",
 | 
			
		||||
          cache: true,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
| 
						 | 
				
			
			@ -145,6 +148,7 @@ export const sysResources = [
 | 
			
		|||
        meta: {
 | 
			
		||||
          icon: "ion:golf-outline",
 | 
			
		||||
          permission: "sys:settings:view",
 | 
			
		||||
          cache: true,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -88,7 +88,7 @@ function wrapperMenus(menus: MenuRecordRaw[], deep: boolean = true) {
 | 
			
		|||
    ? mapTree(menus, (item: any) => {
 | 
			
		||||
        return { ...cloneDeep(item), name: $t(item.name) };
 | 
			
		||||
      })
 | 
			
		||||
    : menus.map((item) => {
 | 
			
		||||
    : menus.map(item => {
 | 
			
		||||
        return { ...cloneDeep(item), name: $t(item.name) };
 | 
			
		||||
      });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -96,8 +96,8 @@ function wrapperMenus(menus: MenuRecordRaw[], deep: boolean = true) {
 | 
			
		|||
function toggleSidebar() {
 | 
			
		||||
  updatePreferences({
 | 
			
		||||
    sidebar: {
 | 
			
		||||
      hidden: !preferences.sidebar.hidden
 | 
			
		||||
    }
 | 
			
		||||
      hidden: !preferences.sidebar.hidden,
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -107,12 +107,12 @@ function clearPreferencesAndLogout() {
 | 
			
		|||
 | 
			
		||||
watch(
 | 
			
		||||
  () => preferences.app.layout,
 | 
			
		||||
  async (val) => {
 | 
			
		||||
  async val => {
 | 
			
		||||
    if (val === "sidebar-mixed-nav" && preferences.sidebar.hidden) {
 | 
			
		||||
      updatePreferences({
 | 
			
		||||
        sidebar: {
 | 
			
		||||
          hidden: false
 | 
			
		||||
        }
 | 
			
		||||
          hidden: false,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +124,7 @@ watch(i18n.global.locale, refresh, { flush: "post" });
 | 
			
		|||
 | 
			
		||||
const slots: SetupContext["slots"] = useSlots();
 | 
			
		||||
const headerSlots = computed(() => {
 | 
			
		||||
  return Object.keys(slots).filter((key) => key.startsWith("header-"));
 | 
			
		||||
  return Object.keys(slots).filter(key => key.startsWith("header-"));
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,14 +20,14 @@ const defaultPreferences: Preferences = {
 | 
			
		|||
    loginExpiredMode: "page",
 | 
			
		||||
    name: "",
 | 
			
		||||
    preferencesButtonPosition: "auto",
 | 
			
		||||
    watermark: false
 | 
			
		||||
    watermark: false,
 | 
			
		||||
  },
 | 
			
		||||
  breadcrumb: {
 | 
			
		||||
    enable: true,
 | 
			
		||||
    hideOnlyOne: false,
 | 
			
		||||
    showHome: false,
 | 
			
		||||
    showIcon: true,
 | 
			
		||||
    styleType: "normal"
 | 
			
		||||
    styleType: "normal",
 | 
			
		||||
  },
 | 
			
		||||
  copyright: {
 | 
			
		||||
    companyName: "greper",
 | 
			
		||||
| 
						 | 
				
			
			@ -36,33 +36,33 @@ const defaultPreferences: Preferences = {
 | 
			
		|||
    enable: false,
 | 
			
		||||
    icp: "",
 | 
			
		||||
    icpLink: "",
 | 
			
		||||
    settingShow: false
 | 
			
		||||
    settingShow: false,
 | 
			
		||||
  },
 | 
			
		||||
  footer: {
 | 
			
		||||
    enable: true,
 | 
			
		||||
    fixed: false
 | 
			
		||||
    fixed: false,
 | 
			
		||||
  },
 | 
			
		||||
  header: {
 | 
			
		||||
    enable: true,
 | 
			
		||||
    hidden: false,
 | 
			
		||||
    menuAlign: "start",
 | 
			
		||||
    mode: "fixed"
 | 
			
		||||
    mode: "fixed",
 | 
			
		||||
  },
 | 
			
		||||
  logo: {
 | 
			
		||||
    enable: true,
 | 
			
		||||
    source: "./static/images/logo/logo.svg"
 | 
			
		||||
    source: "./static/images/logo/logo.svg",
 | 
			
		||||
  },
 | 
			
		||||
  navigation: {
 | 
			
		||||
    accordion: true,
 | 
			
		||||
    split: true,
 | 
			
		||||
    styleType: "rounded"
 | 
			
		||||
    styleType: "rounded",
 | 
			
		||||
  },
 | 
			
		||||
  shortcutKeys: {
 | 
			
		||||
    enable: true,
 | 
			
		||||
    globalLockScreen: true,
 | 
			
		||||
    globalLogout: true,
 | 
			
		||||
    globalPreferences: true,
 | 
			
		||||
    globalSearch: true
 | 
			
		||||
    globalSearch: true,
 | 
			
		||||
  },
 | 
			
		||||
  sidebar: {
 | 
			
		||||
    autoActivateChild: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +72,7 @@ const defaultPreferences: Preferences = {
 | 
			
		|||
    expandOnHover: true,
 | 
			
		||||
    extraCollapse: false,
 | 
			
		||||
    hidden: false,
 | 
			
		||||
    width: 224
 | 
			
		||||
    width: 224,
 | 
			
		||||
  },
 | 
			
		||||
  tabbar: {
 | 
			
		||||
    draggable: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -86,7 +86,7 @@ const defaultPreferences: Preferences = {
 | 
			
		|||
    showMaximize: true,
 | 
			
		||||
    showMore: true,
 | 
			
		||||
    styleType: "chrome",
 | 
			
		||||
    wheelable: true
 | 
			
		||||
    wheelable: true,
 | 
			
		||||
  },
 | 
			
		||||
  theme: {
 | 
			
		||||
    builtinType: "default",
 | 
			
		||||
| 
						 | 
				
			
			@ -97,13 +97,13 @@ const defaultPreferences: Preferences = {
 | 
			
		|||
    mode: "light",
 | 
			
		||||
    radius: "0.5",
 | 
			
		||||
    semiDarkHeader: false,
 | 
			
		||||
    semiDarkSidebar: false
 | 
			
		||||
    semiDarkSidebar: false,
 | 
			
		||||
  },
 | 
			
		||||
  transition: {
 | 
			
		||||
    enable: true,
 | 
			
		||||
    loading: false,
 | 
			
		||||
    name: "fade-slide",
 | 
			
		||||
    progress: true
 | 
			
		||||
    progress: true,
 | 
			
		||||
  },
 | 
			
		||||
  widget: {
 | 
			
		||||
    fullscreen: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -113,8 +113,8 @@ const defaultPreferences: Preferences = {
 | 
			
		|||
    notification: false,
 | 
			
		||||
    refresh: true,
 | 
			
		||||
    sidebarToggle: true,
 | 
			
		||||
    themeToggle: true
 | 
			
		||||
  }
 | 
			
		||||
    themeToggle: true,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { defaultPreferences };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,7 +45,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
     * Close tabs in bulk
 | 
			
		||||
     */
 | 
			
		||||
    async _bulkCloseByPaths(paths: string[]) {
 | 
			
		||||
      this.tabs = this.tabs.filter((item) => {
 | 
			
		||||
      this.tabs = this.tabs.filter(item => {
 | 
			
		||||
        return !paths.includes(getTabPath(item));
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +60,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
      if (isAffixTab(tab)) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const index = this.tabs.findIndex((item) => item.fullPath === fullPath);
 | 
			
		||||
      const index = this.tabs.findIndex(item => item.fullPath === fullPath);
 | 
			
		||||
      index !== -1 && this.tabs.splice(index, 1);
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -85,7 +85,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
      const toParams = {
 | 
			
		||||
        params: params || {},
 | 
			
		||||
        path,
 | 
			
		||||
        query: query || {}
 | 
			
		||||
        query: query || {},
 | 
			
		||||
      };
 | 
			
		||||
      await router.replace(toParams);
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +99,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const tabIndex = this.tabs.findIndex((tab) => {
 | 
			
		||||
      const tabIndex = this.tabs.findIndex(tab => {
 | 
			
		||||
        return getTabPath(tab) === getTabPath(routeTab);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -109,13 +109,13 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
        const maxNumOfOpenTab = (routeTab?.meta?.maxNumOfOpenTab ?? -1) as number;
 | 
			
		||||
        // 如果动态路由层级大于 0 了,那么就要限制该路由的打开数限制了
 | 
			
		||||
        // 获取到已经打开的动态路由数, 判断是否大于某一个值
 | 
			
		||||
        if (maxNumOfOpenTab > 0 && this.tabs.filter((tab) => tab.name === routeTab.name).length >= maxNumOfOpenTab) {
 | 
			
		||||
        if (maxNumOfOpenTab > 0 && this.tabs.filter(tab => tab.name === routeTab.name).length >= maxNumOfOpenTab) {
 | 
			
		||||
          // 关闭第一个
 | 
			
		||||
          const index = this.tabs.findIndex((item) => item.name === routeTab.name);
 | 
			
		||||
          const index = this.tabs.findIndex(item => item.name === routeTab.name);
 | 
			
		||||
          index !== -1 && this.tabs.splice(index, 1);
 | 
			
		||||
        } else if (maxCount > 0 && this.tabs.length >= maxCount) {
 | 
			
		||||
          // 关闭第一个
 | 
			
		||||
          const index = this.tabs.findIndex((item) => !Reflect.has(item.meta, "affixTab") || !item.meta.affixTab);
 | 
			
		||||
          const index = this.tabs.findIndex(item => !Reflect.has(item.meta, "affixTab") || !item.meta.affixTab);
 | 
			
		||||
          index !== -1 && this.tabs.splice(index, 1);
 | 
			
		||||
        }
 | 
			
		||||
        this.tabs.push(tab);
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +125,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
        const mergedTab = {
 | 
			
		||||
          ...currentTab,
 | 
			
		||||
          ...tab,
 | 
			
		||||
          meta: { ...currentTab?.meta, ...tab.meta }
 | 
			
		||||
          meta: { ...currentTab?.meta, ...tab.meta },
 | 
			
		||||
        };
 | 
			
		||||
        if (currentTab) {
 | 
			
		||||
          const curMeta = currentTab.meta;
 | 
			
		||||
| 
						 | 
				
			
			@ -145,7 +145,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
     * @zh_CN 关闭所有标签页
 | 
			
		||||
     */
 | 
			
		||||
    async closeAllTabs(router: Router) {
 | 
			
		||||
      const newTabs = this.tabs.filter((tab) => isAffixTab(tab));
 | 
			
		||||
      const newTabs = this.tabs.filter(tab => isAffixTab(tab));
 | 
			
		||||
      this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1);
 | 
			
		||||
      await this._goToDefaultTab(router);
 | 
			
		||||
      this.updateCacheTabs();
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +155,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
     * @param tab
 | 
			
		||||
     */
 | 
			
		||||
    async closeLeftTabs(tab: TabDefinition) {
 | 
			
		||||
      const index = this.tabs.findIndex((item) => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
      const index = this.tabs.findIndex(item => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
 | 
			
		||||
      if (index < 1) {
 | 
			
		||||
        return;
 | 
			
		||||
| 
						 | 
				
			
			@ -176,13 +176,13 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
     * @param tab
 | 
			
		||||
     */
 | 
			
		||||
    async closeOtherTabs(tab: TabDefinition) {
 | 
			
		||||
      const closePaths = this.tabs.map((item) => getTabPath(item));
 | 
			
		||||
      const closePaths = this.tabs.map(item => getTabPath(item));
 | 
			
		||||
 | 
			
		||||
      const paths: string[] = [];
 | 
			
		||||
 | 
			
		||||
      for (const path of closePaths) {
 | 
			
		||||
        if (path !== tab.fullPath) {
 | 
			
		||||
          const closeTab = this.tabs.find((item) => getTabPath(item) === path);
 | 
			
		||||
          const closeTab = this.tabs.find(item => getTabPath(item) === path);
 | 
			
		||||
          if (!closeTab) {
 | 
			
		||||
            continue;
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			@ -198,7 +198,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
     * @param tab
 | 
			
		||||
     */
 | 
			
		||||
    async closeRightTabs(tab: TabDefinition) {
 | 
			
		||||
      const index = this.tabs.findIndex((item) => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
      const index = this.tabs.findIndex(item => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
 | 
			
		||||
      if (index !== -1 && index < this.tabs.length - 1) {
 | 
			
		||||
        const rightTabs = this.tabs.slice(index + 1);
 | 
			
		||||
| 
						 | 
				
			
			@ -227,7 +227,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
        this.updateCacheTabs();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const index = this.getTabs.findIndex((item) => getTabPath(item) === getTabPath(currentRoute.value));
 | 
			
		||||
      const index = this.getTabs.findIndex(item => getTabPath(item) === getTabPath(currentRoute.value));
 | 
			
		||||
 | 
			
		||||
      const before = this.getTabs[index - 1];
 | 
			
		||||
      const after = this.getTabs[index + 1];
 | 
			
		||||
| 
						 | 
				
			
			@ -252,7 +252,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
     */
 | 
			
		||||
    async closeTabByKey(key: string, router: Router) {
 | 
			
		||||
      const originKey = decodeURIComponent(key);
 | 
			
		||||
      const index = this.tabs.findIndex((item) => getTabPath(item) === originKey);
 | 
			
		||||
      const index = this.tabs.findIndex(item => getTabPath(item) === originKey);
 | 
			
		||||
      if (index === -1) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -268,7 +268,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
     * @param path
 | 
			
		||||
     */
 | 
			
		||||
    getTabByPath(path: string) {
 | 
			
		||||
      return this.getTabs.find((item) => getTabPath(item) === path) as TabDefinition;
 | 
			
		||||
      return this.getTabs.find(item => getTabPath(item) === path) as TabDefinition;
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * @zh_CN 新窗口打开标签页
 | 
			
		||||
| 
						 | 
				
			
			@ -283,7 +283,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
     * @param tab
 | 
			
		||||
     */
 | 
			
		||||
    async pinTab(tab: TabDefinition) {
 | 
			
		||||
      const index = this.tabs.findIndex((item) => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
      const index = this.tabs.findIndex(item => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
      if (index !== -1) {
 | 
			
		||||
        const oldTab = this.tabs[index];
 | 
			
		||||
        tab.meta.affixTab = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -292,9 +292,9 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
        this.tabs.splice(index, 1, tab);
 | 
			
		||||
      }
 | 
			
		||||
      // 过滤固定tabs,后面更改affixTabOrder的值的话可能会有问题,目前行464排序affixTabs没有设置值
 | 
			
		||||
      const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
 | 
			
		||||
      const affixTabs = this.tabs.filter(tab => isAffixTab(tab));
 | 
			
		||||
      // 获得固定tabs的index
 | 
			
		||||
      const newIndex = affixTabs.findIndex((item) => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
      const newIndex = affixTabs.findIndex(item => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
      // 交换位置重新排序
 | 
			
		||||
      await this.sortTabs(index, newIndex);
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -310,7 +310,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
      this.renderRouteView = false;
 | 
			
		||||
      startProgress();
 | 
			
		||||
 | 
			
		||||
      await new Promise((resolve) => setTimeout(resolve, 200));
 | 
			
		||||
      await new Promise(resolve => setTimeout(resolve, 200));
 | 
			
		||||
 | 
			
		||||
      this.excludeCachedTabs.delete(name as string);
 | 
			
		||||
      this.renderRouteView = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -324,7 +324,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
      if (tab?.meta?.newTabTitle) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const findTab = this.tabs.find((item) => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
      const findTab = this.tabs.find(item => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
      if (findTab) {
 | 
			
		||||
        findTab.meta.newTabTitle = undefined;
 | 
			
		||||
        await this.updateCacheTabs();
 | 
			
		||||
| 
						 | 
				
			
			@ -348,7 +348,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
     * @param title
 | 
			
		||||
     */
 | 
			
		||||
    async setTabTitle(tab: TabDefinition, title: string) {
 | 
			
		||||
      const findTab = this.tabs.find((item) => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
      const findTab = this.tabs.find(item => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
 | 
			
		||||
      if (findTab) {
 | 
			
		||||
        findTab.meta.newTabTitle = title;
 | 
			
		||||
| 
						 | 
				
			
			@ -389,7 +389,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
     * @param tab
 | 
			
		||||
     */
 | 
			
		||||
    async unpinTab(tab: TabDefinition) {
 | 
			
		||||
      const index = this.tabs.findIndex((item) => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
      const index = this.tabs.findIndex(item => getTabPath(item) === getTabPath(tab));
 | 
			
		||||
 | 
			
		||||
      if (index !== -1) {
 | 
			
		||||
        const oldTab = this.tabs[index];
 | 
			
		||||
| 
						 | 
				
			
			@ -399,7 +399,7 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
        this.tabs.splice(index, 1, tab);
 | 
			
		||||
      }
 | 
			
		||||
      // 过滤固定tabs,后面更改affixTabOrder的值的话可能会有问题,目前行464排序affixTabs没有设置值
 | 
			
		||||
      const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
 | 
			
		||||
      const affixTabs = this.tabs.filter(tab => isAffixTab(tab));
 | 
			
		||||
      // 获得固定tabs的index,使用固定tabs的下一个位置也就是活动tabs的第一个位置
 | 
			
		||||
      const newIndex = affixTabs.length;
 | 
			
		||||
      // 交换位置重新排序
 | 
			
		||||
| 
						 | 
				
			
			@ -428,11 +428,11 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
        cacheMap.add(name);
 | 
			
		||||
      }
 | 
			
		||||
      this.cachedTabs = cacheMap;
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  getters: {
 | 
			
		||||
    affixTabs(): TabDefinition[] {
 | 
			
		||||
      const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
 | 
			
		||||
      const affixTabs = this.tabs.filter(tab => isAffixTab(tab));
 | 
			
		||||
 | 
			
		||||
      return affixTabs.sort((a, b) => {
 | 
			
		||||
        const orderA = (a.meta?.affixTabOrder ?? 0) as number;
 | 
			
		||||
| 
						 | 
				
			
			@ -447,16 +447,16 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
      return [...this.excludeCachedTabs];
 | 
			
		||||
    },
 | 
			
		||||
    getTabs(): TabDefinition[] {
 | 
			
		||||
      const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
 | 
			
		||||
      const normalTabs = this.tabs.filter(tab => !isAffixTab(tab));
 | 
			
		||||
      return [...this.affixTabs, ...normalTabs].filter(Boolean);
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  persist: [
 | 
			
		||||
    // tabs不需要保存在localStorage
 | 
			
		||||
    {
 | 
			
		||||
      pick: ["tabs"],
 | 
			
		||||
      storage: sessionStorage
 | 
			
		||||
    }
 | 
			
		||||
      storage: sessionStorage,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  state: (): TabbarState => ({
 | 
			
		||||
    cachedTabs: new Set(),
 | 
			
		||||
| 
						 | 
				
			
			@ -464,8 +464,8 @@ export const useTabbarStore = defineStore("core-tabbar", {
 | 
			
		|||
    excludeCachedTabs: new Set(),
 | 
			
		||||
    renderRouteView: true,
 | 
			
		||||
    tabs: [],
 | 
			
		||||
    updateTime: Date.now()
 | 
			
		||||
  })
 | 
			
		||||
    updateTime: Date.now(),
 | 
			
		||||
  }),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 解决热更新问题
 | 
			
		||||
| 
						 | 
				
			
			@ -486,16 +486,16 @@ function cloneTab(route: TabDefinition): TabDefinition {
 | 
			
		|||
  return {
 | 
			
		||||
    ...opt,
 | 
			
		||||
    matched: (matched
 | 
			
		||||
      ? matched.map((item) => ({
 | 
			
		||||
      ? matched.map(item => ({
 | 
			
		||||
          meta: item.meta,
 | 
			
		||||
          name: item.name,
 | 
			
		||||
          path: item.path
 | 
			
		||||
          path: item.path,
 | 
			
		||||
        }))
 | 
			
		||||
      : undefined) as RouteRecordNormalized[],
 | 
			
		||||
    meta: {
 | 
			
		||||
      ...meta,
 | 
			
		||||
      newTabTitle: meta.newTabTitle
 | 
			
		||||
    }
 | 
			
		||||
      newTabTitle: meta.newTabTitle,
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -513,7 +513,7 @@ function isAffixTab(tab: TabDefinition) {
 | 
			
		|||
 */
 | 
			
		||||
function isTabShown(tab: TabDefinition) {
 | 
			
		||||
  const matched = tab?.matched ?? [];
 | 
			
		||||
  return !tab.meta.hideInTab && matched.every((item) => !item.meta.hideInTab);
 | 
			
		||||
  return !tab.meta.hideInTab && matched.every(item => !item.meta.hideInTab);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -528,6 +528,6 @@ function routeToTab(route: RouteRecordNormalized) {
 | 
			
		|||
  return {
 | 
			
		||||
    meta: route.meta,
 | 
			
		||||
    name: route.name,
 | 
			
		||||
    path: route.path
 | 
			
		||||
    path: route.path,
 | 
			
		||||
  } as TabDefinition;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,7 +37,6 @@ const domain = computed(() => {
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
function onUpdated(res: { uploadCert: any }) {
 | 
			
		||||
  debugger;
 | 
			
		||||
  emit("update:modelValue", res.uploadCert);
 | 
			
		||||
  const domains = getAllDomainsFromCrt(res.uploadCert.crt);
 | 
			
		||||
  emit("updated", { domains });
 | 
			
		||||
| 
						 | 
				
			
			@ -45,7 +44,6 @@ function onUpdated(res: { uploadCert: any }) {
 | 
			
		|||
 | 
			
		||||
const pipeline: any = inject("pipeline");
 | 
			
		||||
function onUploadClick() {
 | 
			
		||||
  debugger;
 | 
			
		||||
  openUpdateCertDialog({
 | 
			
		||||
    onSubmit: onUpdated,
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,20 +30,20 @@ import { Modal, notification } from "ant-design-vue";
 | 
			
		|||
import * as api from "./api";
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: "PipelineManager"
 | 
			
		||||
  name: "PipelineManager",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const certdFormRef = ref();
 | 
			
		||||
const groupDictRef = dict({
 | 
			
		||||
  url: "/pi/pipeline/group/all",
 | 
			
		||||
  value: "id",
 | 
			
		||||
  label: "name"
 | 
			
		||||
  label: "name",
 | 
			
		||||
});
 | 
			
		||||
const selectedRowKeys = ref([]);
 | 
			
		||||
const context: any = {
 | 
			
		||||
  certdFormRef,
 | 
			
		||||
  groupDictRef,
 | 
			
		||||
  selectedRowKeys
 | 
			
		||||
  selectedRowKeys,
 | 
			
		||||
};
 | 
			
		||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +70,7 @@ function batchDelete() {
 | 
			
		|||
      notification.success({ message: "删除成功" });
 | 
			
		||||
      await crudExpose.doRefresh();
 | 
			
		||||
      selectedRowKeys.value = [];
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -101,3 +101,11 @@ export async function SaveCommPluginConfigs(data: CommPluginConfig): Promise<voi
 | 
			
		|||
    data,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function DoTest(req: { id: number; input: any }): Promise<void> {
 | 
			
		||||
  return await request({
 | 
			
		||||
    url: apiPrefix + "/doTest",
 | 
			
		||||
    method: "post",
 | 
			
		||||
    data: req,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,8 @@
 | 
			
		|||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="more">
 | 
			
		||||
        <a-button type="primary" :loading="saveLoading" @click="doSave">保存</a-button>
 | 
			
		||||
        <a-button class="mr-1" type="primary" :loading="saveLoading" @click="doSave">保存</a-button>
 | 
			
		||||
        <a-button type="primary" @click="doTest">测试运行</a-button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <div class="pi-plugin-editor">
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +35,7 @@ import { onMounted, provide, ref } from "vue";
 | 
			
		|||
import { useRoute } from "vue-router";
 | 
			
		||||
import * as api from "./api";
 | 
			
		||||
import yaml from "js-yaml";
 | 
			
		||||
import { notification } from "ant-design-vue";
 | 
			
		||||
 | 
			
		||||
const CertApplyPluginNames = ["CertApply", "CertApplyLego", "CertApplyUpload"];
 | 
			
		||||
defineOptions({
 | 
			
		||||
| 
						 | 
				
			
			@ -48,39 +50,53 @@ async function getPlugin() {
 | 
			
		|||
  const pluginObj = await api.GetObj(id);
 | 
			
		||||
  if (!pluginObj.metadata) {
 | 
			
		||||
    pluginObj.metadata = yaml.dump({
 | 
			
		||||
      input: [
 | 
			
		||||
        {
 | 
			
		||||
          key: "cert",
 | 
			
		||||
      input: {
 | 
			
		||||
        cert: {
 | 
			
		||||
          title: "前置任务生成的证书",
 | 
			
		||||
          component: {
 | 
			
		||||
            name: "output-selector",
 | 
			
		||||
            from: [...CertApplyPluginNames],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      output: [],
 | 
			
		||||
      },
 | 
			
		||||
      output: {},
 | 
			
		||||
    });
 | 
			
		||||
  } else {
 | 
			
		||||
    pluginObj.metadata = "";
 | 
			
		||||
  }
 | 
			
		||||
  plugin.value = pluginObj;
 | 
			
		||||
}
 | 
			
		||||
getPlugin();
 | 
			
		||||
onMounted(async () => {});
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  getPlugin();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
provide("get:plugin", () => {
 | 
			
		||||
  return plugin;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const saveLoading = ref(false);
 | 
			
		||||
function doSave() {
 | 
			
		||||
async function doSave() {
 | 
			
		||||
  saveLoading.value = true;
 | 
			
		||||
  try {
 | 
			
		||||
    // api.Save(plugin.value);
 | 
			
		||||
    await api.UpdateObj(plugin.value);
 | 
			
		||||
    notification.success({
 | 
			
		||||
      message: "保存成功",
 | 
			
		||||
    });
 | 
			
		||||
  } finally {
 | 
			
		||||
    saveLoading.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function doTest() {
 | 
			
		||||
  await doSave();
 | 
			
		||||
  const result = await api.DoTest({
 | 
			
		||||
    id: plugin.value.id,
 | 
			
		||||
    input: {},
 | 
			
		||||
  });
 | 
			
		||||
  notification.success({
 | 
			
		||||
    message: "测试已开始",
 | 
			
		||||
    description: result,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="less">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ import { message, Modal } from "ant-design-vue";
 | 
			
		|||
import { DeleteBatch } from "./api";
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: "SysPlugin"
 | 
			
		||||
  name: "SysPlugin",
 | 
			
		||||
});
 | 
			
		||||
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +36,7 @@ const handleBatchDelete = () => {
 | 
			
		|||
        message.info("删除成功");
 | 
			
		||||
        crudExpose.doRefresh();
 | 
			
		||||
        selectedRowKeys.value = [];
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  } else {
 | 
			
		||||
    message.error("请先勾选记录");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,7 +42,12 @@ export class PluginController extends CrudController<PluginService> {
 | 
			
		|||
 | 
			
		||||
  @Post('/update', { summary: 'sys:settings:edit' })
 | 
			
		||||
  async update(@Body(ALL) bean: any) {
 | 
			
		||||
    return super.update(bean);
 | 
			
		||||
 | 
			
		||||
    const res = await super.update(bean);
 | 
			
		||||
      // 更新插件配置
 | 
			
		||||
    const info = await this.service.info(bean.id)
 | 
			
		||||
    await this.service.registerPlugin(info)
 | 
			
		||||
    return res
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Post('/info', { summary: 'sys:settings:view' })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ import { merge } from "lodash-es";
 | 
			
		|||
import { accessRegistry, pluginRegistry } from "@certd/pipeline";
 | 
			
		||||
import { dnsProviderRegistry } from "@certd/plugin-cert";
 | 
			
		||||
import { logger } from "@certd/basic";
 | 
			
		||||
 | 
			
		||||
import yaml from 'js-yaml'
 | 
			
		||||
@Provide()
 | 
			
		||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
 | 
			
		||||
export class PluginService extends BaseService<PluginEntity> {
 | 
			
		||||
| 
						 | 
				
			
			@ -158,12 +158,14 @@ export class PluginService extends BaseService<PluginEntity> {
 | 
			
		|||
        author: author
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    if (info.length > 0) {
 | 
			
		||||
    if (info&&info.length > 0) {
 | 
			
		||||
      const plugin = info[0];
 | 
			
		||||
      const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
 | 
			
		||||
      const getPluginClass =  new AsyncFunction(plugin.content);
 | 
			
		||||
      return await getPluginClass();
 | 
			
		||||
      const pluginClass = await getPluginClass({logger: logger});
 | 
			
		||||
      return new pluginClass()
 | 
			
		||||
    }
 | 
			
		||||
    throw new Error(`插件${pluginName}不存在`);
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * 从数据库加载插件
 | 
			
		||||
| 
						 | 
				
			
			@ -177,28 +179,40 @@ export class PluginService extends BaseService<PluginEntity> {
 | 
			
		|||
      })
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    for (const item of res) {
 | 
			
		||||
      const pluginName = item.author ? item.author +"/"+ item.name : item.name;
 | 
			
		||||
      let registry = null
 | 
			
		||||
      if(item.pluginType === 'access'){
 | 
			
		||||
        registry = accessRegistry;
 | 
			
		||||
      }else if (item.pluginType === 'plugin'){
 | 
			
		||||
        registry = pluginRegistry;
 | 
			
		||||
      }else if (item.pluginType === 'dnsProvider'){
 | 
			
		||||
        registry = dnsProviderRegistry
 | 
			
		||||
      }else {
 | 
			
		||||
        logger.warn(`插件${pluginName}类型错误:${item.pluginType}`)
 | 
			
		||||
        continue
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      registry.register(pluginName, {
 | 
			
		||||
        define:item,
 | 
			
		||||
        target: ()=>{
 | 
			
		||||
          return this.getPluginTarget(pluginName);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
        await this.registerPlugin(item);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async registerPlugin(plugin:PluginEntity){
 | 
			
		||||
    const metadata = yaml.load(plugin.metadata);
 | 
			
		||||
    const item = {
 | 
			
		||||
      ...plugin,
 | 
			
		||||
      ...metadata
 | 
			
		||||
    }
 | 
			
		||||
    delete item.metadata;
 | 
			
		||||
    delete item.content;
 | 
			
		||||
    if(item.author){
 | 
			
		||||
      item.name = item.author +"/"+ item.name;
 | 
			
		||||
    }
 | 
			
		||||
    let registry = null
 | 
			
		||||
    if(item.pluginType === 'access'){
 | 
			
		||||
      registry = accessRegistry;
 | 
			
		||||
    }else if (item.pluginType === 'plugin'){
 | 
			
		||||
      registry = pluginRegistry;
 | 
			
		||||
    }else if (item.pluginType === 'dnsProvider'){
 | 
			
		||||
      registry = dnsProviderRegistry
 | 
			
		||||
    }else {
 | 
			
		||||
      logger.warn(`插件${item.name}类型错误:${item.pluginType}`)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    registry.register(item.name, {
 | 
			
		||||
      define:item,
 | 
			
		||||
      target: ()=>{
 | 
			
		||||
        return this.getPluginTarget(item.name);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue