feat(init): 初始化远程组件项目
This commit is contained in:
commit
611cc5c6dd
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
123
README.md
Normal file
123
README.md
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# EasyAI 远程组件项目
|
||||||
|
|
||||||
|
## 项目介绍
|
||||||
|
|
||||||
|
本项目支持以插件的方式为EasyAI平台提供远程组件服务,以实现对EasyAI平台前端组件库进行扩展,实现自定义组件以满足业务需求。
|
||||||
|
|
||||||
|
## 功能描述
|
||||||
|
|
||||||
|
- 支持扩展绘画组件库
|
||||||
|
- 支持扩展首页组件库(即将上线)
|
||||||
|
|
||||||
|
## 组件开发
|
||||||
|
|
||||||
|
### 项目启动
|
||||||
|
|
||||||
|
1. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 访问地址:http://localhost:3201
|
||||||
|
4. 组件目录:components
|
||||||
|
5. 维护组件信息目录:manifest
|
||||||
|
6. 组件调试入口: app.vue
|
||||||
|
7. EasyAI平台,全局注入的数据和方法: types/common.ts
|
||||||
|
1. 注入的素材库数据:GlobalInjectMaterials
|
||||||
|
- mock数据:composables/mock/material.data.ts
|
||||||
|
- 在组件中使用:components/RecommendedImages.vue
|
||||||
|
```ts
|
||||||
|
const { materials: materialList } =
|
||||||
|
inject<GlobalInjectMaterials>(GlobalInjectKeyConst.AllMaterials, {
|
||||||
|
materials: ref<IImageSource>([]),
|
||||||
|
}) || {};
|
||||||
|
```
|
||||||
|
2. 注入的上传文件方法:GlobalInjectUploadFileToOSS
|
||||||
|
- 在组件中使用:components/drawPanne/ImageUpload.vue
|
||||||
|
```ts
|
||||||
|
const { useUtilsUploadFileToOSS } = inject<GlobalInjectUploadFileToOSS>(
|
||||||
|
GlobalInjectKeyConst.UploadFileToOSS,
|
||||||
|
{
|
||||||
|
useUtilsUploadFileToOSS: async (file: File | Blob, filename?: string) => "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 示例组件 (图片上传组件)
|
||||||
|
|
||||||
|
1. 组件位置:components/drawPanne/ImageUpload.vue
|
||||||
|
正常的nuxt(vue3)项目组件开发,直接在components文件夹下创建即可
|
||||||
|
2. 维护组件信息文件:manifest/ImageUpload.ts
|
||||||
|
需要参照IComponentMateInfo类型约束文件进行定义
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default {
|
||||||
|
name: "DrawImageUpload", // 组件名称
|
||||||
|
path: "./components/drawPanne/ImageUpload.vue", // 组件路径
|
||||||
|
scenes: ComponentSceneConst.DrawPanne, // 组件场景
|
||||||
|
description: "A image upload component", // 组件描述
|
||||||
|
data: { // 组件数据
|
||||||
|
paramName: "upload_image_path", // 参数名称
|
||||||
|
label: "图片上传", // 组件标签
|
||||||
|
icon: "icon-park:upload-picture", // 组件图标
|
||||||
|
group: ComponentGroupConst.IMAGE, // 组件分组
|
||||||
|
isRefComponent: true, // 是否为引用组件 (引用组件,在执行绘画的时候会执行参数赋值操作)
|
||||||
|
},
|
||||||
|
} satisfies IComponentMateInfo;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 组件调试入口:app.vue
|
||||||
|
|
||||||
|
```text
|
||||||
|
<template>
|
||||||
|
<div class="remote-ui">
|
||||||
|
<!-- 调试对象-->
|
||||||
|
<DrawPanneImageUpload />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
GlobalInjectKeyConst,
|
||||||
|
type GlobalInjectMaterials,
|
||||||
|
} from "~/types/common";
|
||||||
|
import { MockMaterials } from "~/composables/mock/material.data";
|
||||||
|
|
||||||
|
const materialData = ref(MockMaterials);
|
||||||
|
/**
|
||||||
|
* EasyAI平台,已全局注入素材库库信息这里,使用mock数据用于调试
|
||||||
|
*/
|
||||||
|
provide<GlobalInjectMaterials>(GlobalInjectKeyConst.AllMaterials, {
|
||||||
|
materials: materialData,
|
||||||
|
refreshMaterials: async () => [],
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
1. 打包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm serve
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 访问
|
||||||
|
|
||||||
|
```bash
|
||||||
|
http://localhost:3200
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用
|
22
app.vue
Normal file
22
app.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="remote-ui">
|
||||||
|
<!-- 调试对象-->
|
||||||
|
<DrawPanneImageUpload />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
GlobalInjectKeyConst,
|
||||||
|
type GlobalInjectMaterials,
|
||||||
|
} from "~/types/common";
|
||||||
|
import { MockMaterials } from "~/composables/mock/material.data";
|
||||||
|
|
||||||
|
const materialData = ref(MockMaterials);
|
||||||
|
/**
|
||||||
|
* EasyAI平台,已全局注入素材库库信息这里,使用mock数据用于调试
|
||||||
|
*/
|
||||||
|
provide<GlobalInjectMaterials>(GlobalInjectKeyConst.AllMaterials, {
|
||||||
|
materials: materialData,
|
||||||
|
refreshMaterials: async () => [],
|
||||||
|
});
|
||||||
|
</script>
|
3
assets/css/tailwind.css
Normal file
3
assets/css/tailwind.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
54
components/ImagePreviewWithUpload.vue
Normal file
54
components/ImagePreviewWithUpload.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useUploadImage } from "~/composables/draw/useUploadImage";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
maxSizeMB?: number;
|
||||||
|
}>(),
|
||||||
|
{ maxSizeMB: 5 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const image = defineModel<string>();
|
||||||
|
|
||||||
|
const updateImage = (file: File) => {
|
||||||
|
image.value = URL.createObjectURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { open } = useUploadImage(updateImage, props.maxSizeMB);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full w-full relative group">
|
||||||
|
<img
|
||||||
|
class="h-full w-full object-contain image-bg rounded"
|
||||||
|
:src="image"
|
||||||
|
alt="show image"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center p-2 px-3 gap-3 invisible group-hover:visible rounded-lg bg-[rgba(var(--v-theme-background),0.4)] backdrop-blur-md cursor-pointer"
|
||||||
|
>
|
||||||
|
<a-tooltip :title="1">
|
||||||
|
<icon name="mage:image-upload" size="18" @click="open" />
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip :title="2">
|
||||||
|
<icon
|
||||||
|
name="material-symbols:delete-outline"
|
||||||
|
size="18"
|
||||||
|
@click.stop="image = undefined"
|
||||||
|
/>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.image-bg {
|
||||||
|
@apply backdrop-blur-md bg-gradient-to-r
|
||||||
|
from-gray-300 via-gray-50 to-gray-300
|
||||||
|
dark:from-gray-600 dark:via-gray-300 dark:to-gray-600;
|
||||||
|
}
|
||||||
|
</style>
|
52
components/ImageUploadDropPasteClick.vue
Normal file
52
components/ImageUploadDropPasteClick.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useUploadImage } from "~/composables/draw/useUploadImage";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
maxSizeMB?: number;
|
||||||
|
}>(),
|
||||||
|
{ maxSizeMB: 5 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const image = defineModel<string>();
|
||||||
|
|
||||||
|
const updateImage = (file: File) => {
|
||||||
|
image.value = URL.createObjectURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { open, handleDrop, handlePaste } = useUploadImage(
|
||||||
|
updateImage,
|
||||||
|
props.maxSizeMB,
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener("paste", handlePaste);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener("paste", handlePaste);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="aspect-[5/2] flex justify-center items-center cursor-pointer"
|
||||||
|
@click="open"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-2 justify-center items-center rounded-lg"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
>
|
||||||
|
<slot name="description" :max-size-m-b="maxSizeMB">
|
||||||
|
<icon name="mdi:image-plus-outline" size="24" />
|
||||||
|
<p class="text-md">点击/拖拽/粘贴</p>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
请上传图片文件,文件大小不超过{{ maxSizeMB }}MB
|
||||||
|
</p>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
170
components/RecommendedImages.vue
Normal file
170
components/RecommendedImages.vue
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import {
|
||||||
|
GlobalInjectKeyConst,
|
||||||
|
type GlobalInjectMaterials,
|
||||||
|
} from "~/types/common";
|
||||||
|
|
||||||
|
interface IImageSource {
|
||||||
|
_id: string;
|
||||||
|
url: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { materials: materialList } =
|
||||||
|
inject<GlobalInjectMaterials>(GlobalInjectKeyConst.AllMaterials, {
|
||||||
|
materials: ref<IImageSource>([]),
|
||||||
|
}) || {};
|
||||||
|
|
||||||
|
const emit = defineEmits(["update-image"]);
|
||||||
|
const pageSize = defineModel<number>({ default: 10 });
|
||||||
|
|
||||||
|
const showRecommendedImage = computed(() => {
|
||||||
|
return materialList.value?.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前显示的列表
|
||||||
|
const visibleMaterials = ref<IImageSource[]>([]);
|
||||||
|
|
||||||
|
// 是否显示刷新按钮:总数大于 pageSize 才需要
|
||||||
|
const showRefreshButton = computed(() => {
|
||||||
|
return materialList.value?.length > pageSize.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化:随机抽取pageSize条
|
||||||
|
const initVisibleMaterials = () => {
|
||||||
|
if (!materialList.value?.length) return;
|
||||||
|
visibleMaterials.value = getRandomMaterials(
|
||||||
|
materialList.value,
|
||||||
|
pageSize.value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 随机取 count 条,如果不够就循环补足
|
||||||
|
const getRandomMaterials = (
|
||||||
|
list: IImageSource[],
|
||||||
|
count: number,
|
||||||
|
): IImageSource[] => {
|
||||||
|
if (!list.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const shuffled = [...list].sort(() => Math.random() - 0.5);
|
||||||
|
if (shuffled.length >= count) {
|
||||||
|
return shuffled.slice(0, count);
|
||||||
|
} else {
|
||||||
|
// 不够就循环补足
|
||||||
|
const times = Math.ceil(count / shuffled.length);
|
||||||
|
const extended = Array(times).fill(shuffled).flat();
|
||||||
|
return extended.slice(0, count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRotating = ref(false);
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
if (isRotating.value) return; // 防止多次触发
|
||||||
|
isRotating.value = true;
|
||||||
|
|
||||||
|
// 调用原本的刷新逻辑
|
||||||
|
visibleMaterials.value = getRandomMaterials(
|
||||||
|
materialList.value,
|
||||||
|
pageSize.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 动画持续 0.5s 后移除旋转(保证动画能再次触发)
|
||||||
|
setTimeout(() => {
|
||||||
|
isRotating.value = false;
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetImage = (url: string) => {
|
||||||
|
emit("update-image", url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibleMap = reactive<Record<string, boolean>>({});
|
||||||
|
const hoveringMap = reactive<Record<string, boolean>>({});
|
||||||
|
const timerMap: Record<string, ReturnType<typeof setTimeout>> = {};
|
||||||
|
|
||||||
|
const startDelay = (id: string) => {
|
||||||
|
hoveringMap[id] = true;
|
||||||
|
if (timerMap[id]) clearTimeout(timerMap[id]);
|
||||||
|
timerMap[id] = setTimeout(() => {
|
||||||
|
if (hoveringMap[id]) {
|
||||||
|
visibleMap[id] = true;
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelDelay = (id: string) => {
|
||||||
|
hoveringMap[id] = false;
|
||||||
|
if (timerMap[id]) clearTimeout(timerMap[id]);
|
||||||
|
visibleMap[id] = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
initVisibleMaterials();
|
||||||
|
watch(pageSize, () => {
|
||||||
|
initVisibleMaterials();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="showRecommendedImage"
|
||||||
|
class="px-2 py-1 flex flex-row items-center gap-2 rounded"
|
||||||
|
>
|
||||||
|
<div class="text-sm mb-2 flex-shrink-0">推荐:</div>
|
||||||
|
<div
|
||||||
|
class="grid gap-2"
|
||||||
|
:style="`grid-template-columns: repeat(${pageSize}, minmax(0, 1fr))`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="material in visibleMaterials"
|
||||||
|
:key="material._id"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="handleSetImage(material.url)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="inline-block"
|
||||||
|
@mouseenter="startDelay(material._id)"
|
||||||
|
@mouseleave="cancelDelay(material._id)"
|
||||||
|
@dragstart="cancelDelay(material._id)"
|
||||||
|
>
|
||||||
|
<a-popover :open="!!visibleMap[material._id]">
|
||||||
|
<img
|
||||||
|
:src="material.url"
|
||||||
|
alt="img"
|
||||||
|
class="object-cover w-full rounded aspect-[1/1]"
|
||||||
|
/>
|
||||||
|
<template #content>
|
||||||
|
<img
|
||||||
|
:src="material.url"
|
||||||
|
alt="img"
|
||||||
|
class="w-48 h-auto object-contain border cursor-pointer"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showRefreshButton"
|
||||||
|
class="cursor-pointer text-sm flex-shrink-0"
|
||||||
|
@click="refresh"
|
||||||
|
>
|
||||||
|
<a-popover>
|
||||||
|
<icon
|
||||||
|
name="hugeicons:exchange-01"
|
||||||
|
size="20"
|
||||||
|
:class="{ 'animate-spin': isRotating }"
|
||||||
|
/>
|
||||||
|
<template #content>
|
||||||
|
<div class="text-xs">刷新</div>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
80
components/drawPanne/ImageUpload.vue
Normal file
80
components/drawPanne/ImageUpload.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
GlobalInjectKeyConst,
|
||||||
|
type GlobalInjectUploadFileToOSS,
|
||||||
|
} from "~/types/common";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string; // 标题,显示在上传按钮上方
|
||||||
|
paramName?: string;
|
||||||
|
params?: { name: string; param?: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: "",
|
||||||
|
paramName: "upload_image_path",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { useUtilsUploadFileToOSS } = inject<GlobalInjectUploadFileToOSS>(
|
||||||
|
GlobalInjectKeyConst.UploadFileToOSS,
|
||||||
|
{
|
||||||
|
useUtilsUploadFileToOSS: async (file: File | Blob, filename?: string) => "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const image = ref<string | undefined>();
|
||||||
|
|
||||||
|
const handleSetImage = (url: string) => {
|
||||||
|
image.value = url;
|
||||||
|
};
|
||||||
|
const pageSize = ref<number>(8);
|
||||||
|
|
||||||
|
const handleUploadImg = async (image?: string) => {
|
||||||
|
if (!image) return;
|
||||||
|
if (image.startsWith("http")) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await ImageUrlToBlob(image);
|
||||||
|
|
||||||
|
const fileName = `upload_image_${new Date().getTime()}.png`;
|
||||||
|
return await useUtilsUploadFileToOSS(blob, fileName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBindParams = async () => {
|
||||||
|
return {
|
||||||
|
[props.paramName]: await handleUploadImg(image.value),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
handleBindParams,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="max-w-[460px] mt-1">
|
||||||
|
<div class="flex items-center py-1">
|
||||||
|
<icon name="icon-park:upload-picture" size="20" class="mr-1" />
|
||||||
|
{{ title || "图片上传" }}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center border rounded">
|
||||||
|
<!-- 图片上传、预览区域 -->
|
||||||
|
<div class="h-48 w-full flex justify-center">
|
||||||
|
<!-- 显示选中图片 -->
|
||||||
|
<div v-if="image" class="p-2 w-full">
|
||||||
|
<ImagePreviewWithUpload v-model="image" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片上传 -->
|
||||||
|
<ImageUploadDropPasteClick v-else v-model="image" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<RecommendedImages v-model="pageSize" @update-image="handleSetImage" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
53
composables/draw/useUploadImage.ts
Normal file
53
composables/draw/useUploadImage.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { useFileDialog } from "@vueuse/core";
|
||||||
|
|
||||||
|
export const useUploadImage = (
|
||||||
|
updateImageFn: (file: File) => void,
|
||||||
|
maxSizeMB: number = 5,
|
||||||
|
) => {
|
||||||
|
// 打开文件选择对话框
|
||||||
|
const { open, onChange } = useFileDialog({
|
||||||
|
accept: "image/*",
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUploadFile = async (files: File[] | FileList) => {
|
||||||
|
const file = files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const sizeMB = file.size / (1024 * 1024);
|
||||||
|
|
||||||
|
if (sizeMB > maxSizeMB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateImageFn(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听选择的文件
|
||||||
|
onChange(async (files) => {
|
||||||
|
if (files) {
|
||||||
|
await handleUploadFile(files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 拖拽上传
|
||||||
|
const handleDrop = (event: DragEvent) => {
|
||||||
|
if (event.dataTransfer?.files) {
|
||||||
|
handleUploadFile(event.dataTransfer.files).then();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 粘贴上传
|
||||||
|
const handlePaste = (event: ClipboardEvent) => {
|
||||||
|
const file = Array.from(event.clipboardData?.items || [])
|
||||||
|
.find((item) => item.type.startsWith("image/"))
|
||||||
|
?.getAsFile();
|
||||||
|
if (file) handleUploadFile([file]).then();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
open,
|
||||||
|
handleDrop,
|
||||||
|
handlePaste,
|
||||||
|
};
|
||||||
|
};
|
1
composables/index.ts
Normal file
1
composables/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./meta";
|
2
composables/meta/index.ts
Normal file
2
composables/meta/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./interface/meta.interface";
|
||||||
|
export * from "./interface/drawPanne.interface";
|
204
composables/meta/interface/drawPanne.interface.ts
Normal file
204
composables/meta/interface/drawPanne.interface.ts
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
/**
|
||||||
|
* 组件的的分类
|
||||||
|
*/
|
||||||
|
export const ComponentGroupConst = {
|
||||||
|
BASE: "base", // 基础组件
|
||||||
|
PROMPT: "prompt", // 提示词
|
||||||
|
HINT: "hint", // 提示类
|
||||||
|
SELECT: "select", // 下拉框
|
||||||
|
COLOR_SELECT: "color_select",
|
||||||
|
IMAGE: "image", // 图片组件
|
||||||
|
OUTPUT: "output", // 产出,无实际组件
|
||||||
|
BUSSINESS: "business",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ComponentGroupType =
|
||||||
|
(typeof ComponentGroupConst)[keyof typeof ComponentGroupConst];
|
||||||
|
|
||||||
|
/** 动态参数 */
|
||||||
|
export interface IDynamicOptions {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
hint?: string;
|
||||||
|
href?: string;
|
||||||
|
required?: boolean;
|
||||||
|
seedLength?: number; // 随机种子的长度
|
||||||
|
imageSelectItems?: IImageSelectItem[];
|
||||||
|
selectItems?: ISelectItem[];
|
||||||
|
enableLLM?: boolean; // 是否启用大模型微调功能
|
||||||
|
enableTranslate?: boolean; // 是否启用翻译功能
|
||||||
|
enableAutoTranslate?: boolean; // 是否启用自动翻译功能
|
||||||
|
presetPrompt?: string;
|
||||||
|
prePendPositive?: string; // 前置追加正向提示词
|
||||||
|
separator?: string; // 分割符号
|
||||||
|
limit?: number;
|
||||||
|
maskDomain?: number;
|
||||||
|
modelType?: PlatformModelType;
|
||||||
|
templateText?: string;
|
||||||
|
templateOptions?: Record<string, { label: string; value: string }[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预览选择组件参数 */
|
||||||
|
export interface IImageSelectItem {
|
||||||
|
src: string;
|
||||||
|
title: string;
|
||||||
|
value: string | number | boolean;
|
||||||
|
valueExtra: string | number | boolean;
|
||||||
|
valueExtra2?: string | number | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** picker 选项 */
|
||||||
|
export interface ISelectItem {
|
||||||
|
label: string;
|
||||||
|
value: string | number | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模型种类 */
|
||||||
|
export const PlatformModelTypeConst = {
|
||||||
|
IMAGE_GENERATE: "image_generate",
|
||||||
|
IMAGE_EDIT: "image_edit",
|
||||||
|
IMAGE_ANALYSIS: "image_analysis",
|
||||||
|
TEXT_GENERATE: "text_generate",
|
||||||
|
VIDEO_GENERATE: "video_generate",
|
||||||
|
IMAGE_TO_VIDEO: "image_to_video",
|
||||||
|
TOOLS_CALL: "tools_call",
|
||||||
|
AUDIO_GENERATE: "audio_generate",
|
||||||
|
TEXT_TO_MODEL: "text_to_model",
|
||||||
|
IMAGE_TO_MODEL: "image_to_model",
|
||||||
|
MULTIVIEW_TO_MODEL: "multiview_to_model",
|
||||||
|
} as const;
|
||||||
|
export type PlatformModelType =
|
||||||
|
(typeof PlatformModelTypeConst)[keyof typeof PlatformModelTypeConst];
|
||||||
|
|
||||||
|
// 用在外部接口的匹配
|
||||||
|
export interface ComfyUIProperties {
|
||||||
|
ckpt_name?: string;
|
||||||
|
lora_name?: string;
|
||||||
|
positive?: string;
|
||||||
|
positive_template?: string;
|
||||||
|
negative?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
seed?: number;
|
||||||
|
|
||||||
|
image_path?: string; // 整个工作流只有一张图片输入,一般使用这个参数
|
||||||
|
image_path_mask?: string; // 遮罩图片
|
||||||
|
image_path_face?: string; // 换脸场景,脸部参考图片
|
||||||
|
image_path_style?: string; // 参考图场景,例如ipdapter
|
||||||
|
denoise?: number;
|
||||||
|
output?: string;
|
||||||
|
// 视频参数
|
||||||
|
video_frames?: number;
|
||||||
|
fps?: number;
|
||||||
|
motion_bucket_id?: number;
|
||||||
|
augmentation_level?: number;
|
||||||
|
filename_prefix?: string;
|
||||||
|
cfg?: number;
|
||||||
|
steps?: number;
|
||||||
|
min_cfg?: number;
|
||||||
|
|
||||||
|
upscale_by?: number; // 放大倍数
|
||||||
|
// SD3专属参数
|
||||||
|
sd3_aspect_ratio?: SD3AspectRatio;
|
||||||
|
|
||||||
|
// 自定义下拉列表
|
||||||
|
custom_dropselect?: string;
|
||||||
|
custom_dropselect_number?: number;
|
||||||
|
|
||||||
|
advance_select_image_preview?: string; // 高级选择组件,带图片样式预览
|
||||||
|
advance_select_single_chips?: string; // 高级选择组件,纸片组单选
|
||||||
|
advance_select_multi_chips?: string; // 高级选择组件,纸片组多选
|
||||||
|
|
||||||
|
// 颜色选择
|
||||||
|
advance_select_color: string;
|
||||||
|
|
||||||
|
// 颜色预览选择
|
||||||
|
advance_select_preview_color: string;
|
||||||
|
|
||||||
|
// 在线遮罩编辑
|
||||||
|
advance_onlineEdit_origin?: string; // 在线遮罩编辑,原图(图像图层部分)
|
||||||
|
advance_onlineEdit_mask?: string; // 在线遮罩编辑,遮罩部分 advance_onlineEdit_origin?: string //在线遮罩编辑,原图(图像图层部分)
|
||||||
|
advance_onlineEdit_origin_2?: string;
|
||||||
|
advance_onlineEdit_mask_2?: string;
|
||||||
|
|
||||||
|
// 生成的图像批次数量
|
||||||
|
batch_size?: number;
|
||||||
|
|
||||||
|
// 自定义数字滑块
|
||||||
|
custom_number_slider?: number;
|
||||||
|
|
||||||
|
// 自定义参数
|
||||||
|
custom_number?: number;
|
||||||
|
custom_string?: string;
|
||||||
|
|
||||||
|
// 自定义批量处理
|
||||||
|
custom_batch_image_path_origin?: string;
|
||||||
|
custom_batch_image_path_origin_2?: string;
|
||||||
|
|
||||||
|
// 自定义提示信息
|
||||||
|
custom_hint?: string;
|
||||||
|
// 第二版
|
||||||
|
// 模型选择
|
||||||
|
advance_ckpt_name?: string;
|
||||||
|
advance_lora_name?: string;
|
||||||
|
// 3d图片路径
|
||||||
|
advance_image_path_3d?: string;
|
||||||
|
// 正向提示词
|
||||||
|
advance_positive?: string;
|
||||||
|
// 负向提示词
|
||||||
|
advance_negative?: string;
|
||||||
|
// 细节增强提示词
|
||||||
|
advance_adetailer?: string;
|
||||||
|
// 自定义分组标签
|
||||||
|
custom_group_label?: string;
|
||||||
|
// 高级图片上传-参考图
|
||||||
|
advance_image_upload?: string;
|
||||||
|
// 高级图片上传-间单图片
|
||||||
|
advance_sample_image_upload?: string;
|
||||||
|
// 高级图片上传-背景图片
|
||||||
|
advance_background_upload?: string;
|
||||||
|
// 高级图片上传-画板图片
|
||||||
|
advance_canvas_image_upload?: string;
|
||||||
|
// 高级图片上传-遮罩图片
|
||||||
|
advance_canvas_mask_upload?: string;
|
||||||
|
// 高级图片上传-拆件原图
|
||||||
|
advance_splitter_image_upload?: string;
|
||||||
|
// 高级图片上传-拆件遮罩原图
|
||||||
|
advance_splitter_mask_upload?: string;
|
||||||
|
// 自定义单选框-参考图
|
||||||
|
custom_radio?: string;
|
||||||
|
// 高级自定义数字滑块
|
||||||
|
advance_custom_number_slider?: number;
|
||||||
|
// 高级自定义下拉列表
|
||||||
|
advance_custom_dropselect?: string;
|
||||||
|
// 高级随机种子
|
||||||
|
advance_seed?: number;
|
||||||
|
// 点集
|
||||||
|
coordinates?: string; // 正样本点集坐标
|
||||||
|
neg_coordinates?: string; // 负样本点集坐标
|
||||||
|
|
||||||
|
canvas_swap_background?: string; // 交换背景图片
|
||||||
|
canvas_swap_origin?: string; // 交换背景原图
|
||||||
|
|
||||||
|
multi_image_path?: string; // 多张图片
|
||||||
|
multi_source_image_path?: string; // 多源图片选择
|
||||||
|
|
||||||
|
canvas_image_group?: string; // 图片合并
|
||||||
|
image_first_frame?: string; // 首帧图片
|
||||||
|
image_last_frame?: string; // 尾帧图片
|
||||||
|
model_alias?: string; // 模型别名
|
||||||
|
multi_view_images?: string; // 多视图图片上传
|
||||||
|
|
||||||
|
upload_image_path?: string; // 图片上传(本地/素材库)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SD3AspectRatio =
|
||||||
|
| "1:1"
|
||||||
|
| "16:9"
|
||||||
|
| "21:9"
|
||||||
|
| "2:3"
|
||||||
|
| "3:2"
|
||||||
|
| "3:4"
|
||||||
|
| "4:3"
|
||||||
|
| "9:16";
|
55
composables/meta/interface/meta.interface.ts
Normal file
55
composables/meta/interface/meta.interface.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import type {
|
||||||
|
ComfyUIProperties,
|
||||||
|
ComponentGroupType,
|
||||||
|
} from "~/composables/meta/interface/drawPanne.interface";
|
||||||
|
|
||||||
|
export const ComponentSceneConst = {
|
||||||
|
DynamicPage: "dynamicPage", // 自定义页面
|
||||||
|
Workflow: "workflow", // 工作流
|
||||||
|
DrawPanne: "drawPanne", // 绘画面板
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ComponentSceneType =
|
||||||
|
(typeof ComponentSceneConst)[keyof typeof ComponentSceneConst];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件元信息
|
||||||
|
* @property name
|
||||||
|
*/
|
||||||
|
export interface IComponentMateInfo<
|
||||||
|
T extends ComponentSceneType = ComponentSceneType,
|
||||||
|
> {
|
||||||
|
name: string; // 组件名称
|
||||||
|
path: string; // 组件路径
|
||||||
|
description: string; // 描述
|
||||||
|
scenes: T; // 所属场景
|
||||||
|
data?: ComponentDataMap[T]; // 组件数据
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComponentDataMap = {
|
||||||
|
[ComponentSceneConst.DrawPanne]: IDrawPanneData;
|
||||||
|
[ComponentSceneConst.DynamicPage]?: never;
|
||||||
|
[ComponentSceneConst.Workflow]?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 元信息
|
||||||
|
* @property remoteEntry 入口
|
||||||
|
*/
|
||||||
|
export interface IManifestMateInfo {
|
||||||
|
version: string; // 项目版本号
|
||||||
|
prefix: string; // 入口文件访问前缀
|
||||||
|
remoteEntry: string; // 远程入口文件
|
||||||
|
components: IComponentMateInfo<ComponentSceneType>[]; // 组件信息清单
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDrawPanneData {
|
||||||
|
paramName: keyof ComfyUIProperties;
|
||||||
|
label?: string;
|
||||||
|
example?: IDynamicOptions;
|
||||||
|
icon?: string;
|
||||||
|
group: ComponentGroupType;
|
||||||
|
isRefComponent?: boolean;
|
||||||
|
type?: "number" | "string" | "boolean";
|
||||||
|
isUnique?: boolean;
|
||||||
|
}
|
52
composables/mock/material.data.ts
Normal file
52
composables/mock/material.data.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export const MockMaterials = [
|
||||||
|
{
|
||||||
|
_id: "6850e2c2082fb640aaaced81",
|
||||||
|
url: "https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/image/temps/663e19cd4fa9d8078385c7c9/49817b32-9608-40a8-b205-7670adaf385a%20%281%29.png",
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "6850b98f4755729482155fbf",
|
||||||
|
tags: [],
|
||||||
|
url: "https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/image/temps/663e19cd4fa9d8078385c7c9/188g7b2m.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "684ff8e823353e832d3d79cb",
|
||||||
|
tags: [],
|
||||||
|
url: "https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/image/temps/663e19cd4fa9d8078385c7c9/4c8bccc1-8870-455f-add8-5086ea02a7d1.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "684ff8e123353e832d3d79c4",
|
||||||
|
tags: [],
|
||||||
|
url: "https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/image/temps/663e19cd4fa9d8078385c7c9/WechatIMG3.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "684ff8db23353e832d3d79bd",
|
||||||
|
tags: [],
|
||||||
|
url: "https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/image/temps/663e19cd4fa9d8078385c7c9/6df3fc3831556f32120fece8f58e91d7a4ef5d29f65675a697378d05bce889ab.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "684ff8d423353e832d3d79b7",
|
||||||
|
tags: [],
|
||||||
|
url: "https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/image/temps/663e19cd4fa9d8078385c7c9/YF1qBd-%25E6%2594%25B9%25E6%25AC%25BE%25E5%25A4%25A7%25E5%25B8%2588_00140_%20%282%29.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "684ff8cc23353e832d3d79b0",
|
||||||
|
tags: [],
|
||||||
|
url: "https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/image/temps/663e19cd4fa9d8078385c7c9/92e0df75-87ab-44d1-bb6a-f575d9fcb390.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "684ff80323353e832d3d7973",
|
||||||
|
tags: [],
|
||||||
|
url: "https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/image/temps/663e19cd4fa9d8078385c7c9/xCojEk-ComfyUI_00120_.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "684ff7fd23353e832d3d796d",
|
||||||
|
tags: [],
|
||||||
|
url: "https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/image/temps/663e19cd4fa9d8078385c7c9/lyZrMN-ComfyUI_01373_.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "684ff7f523353e832d3d7966",
|
||||||
|
tags: [],
|
||||||
|
url: "https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/image/temps/663e19cd4fa9d8078385c7c9/Vy_MP8-ps__00002_.png",
|
||||||
|
},
|
||||||
|
];
|
12
eslint.config.mjs
Normal file
12
eslint.config.mjs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import nuxtConfig from "@nuxtjs/eslint-config";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
nuxtConfig, // 直接包含继承的配置对象
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
defineNuxtConfig: "readonly",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
4
index.ts
Normal file
4
index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { loadRemoteStyle } from "./utils/loadStyle";
|
||||||
|
|
||||||
|
// 动态注入 Tailwind CSS
|
||||||
|
loadRemoteStyle("/_remote-ui-kit/style.css"); // 部署后对应 CSS URL
|
17
manifest/ImageUpload.ts
Normal file
17
manifest/ImageUpload.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ComponentSceneConst, type IComponentMateInfo } from "~/composables";
|
||||||
|
import { ComponentGroupConst } from "~/composables";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DrawImageUpload", // 组件名称
|
||||||
|
path: "./components/drawPanne/ImageUpload.vue", // 组件路径
|
||||||
|
scenes: ComponentSceneConst.DrawPanne, // 组件场景
|
||||||
|
description: "A image upload component", // 组件描述
|
||||||
|
data: {
|
||||||
|
// 组件数据
|
||||||
|
paramName: "upload_image_path", // 参数名称
|
||||||
|
label: "图片上传", // 组件标签
|
||||||
|
icon: "icon-park:upload-picture", // 组件图标
|
||||||
|
group: ComponentGroupConst.IMAGE, // 组件分组
|
||||||
|
isRefComponent: true, // 是否为引用组件 (引用组件,在执行绘画的时候会执行参数赋值操作)
|
||||||
|
},
|
||||||
|
} satisfies IComponentMateInfo;
|
50
modules/generate-manifest.ts
Normal file
50
modules/generate-manifest.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { defineNuxtModule } from "@nuxt/kit";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import pkg from "../package.json";
|
||||||
|
|
||||||
|
export default defineNuxtModule({
|
||||||
|
meta: { name: "generate-manifest" },
|
||||||
|
async setup(_, nuxt) {
|
||||||
|
const generateManifest = () => {
|
||||||
|
const manifestDir = path.resolve(nuxt.options.rootDir, "manifest");
|
||||||
|
if (!fs.existsSync(manifestDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只读取 ts 文件
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(manifestDir)
|
||||||
|
.filter((f) => f.endsWith(".ts"));
|
||||||
|
const components = files.map((f) => {
|
||||||
|
const mod = require(path.join(manifestDir, f));
|
||||||
|
return mod.default;
|
||||||
|
});
|
||||||
|
|
||||||
|
const remoteEntry = `remoteEntry_${pkg.version}.js`;
|
||||||
|
|
||||||
|
const outPath = path.join(process.cwd(), ".nuxt/generated/manifest.ts"); // 固定目录
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
outPath,
|
||||||
|
`// ⚠️ 自动生成,请勿修改
|
||||||
|
export const manifestInfo = ${JSON.stringify(
|
||||||
|
{ version: pkg.version, prefix: "_nuxt", remoteEntry, components },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)};
|
||||||
|
`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化就生成一次,保证 dev/build 第一阶段文件存在
|
||||||
|
generateManifest();
|
||||||
|
|
||||||
|
// build 前再生成一次
|
||||||
|
nuxt.hooks.hook("nitro:build:before", generateManifest);
|
||||||
|
},
|
||||||
|
});
|
147
nuxt.config.ts
Normal file
147
nuxt.config.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
import federation from "@originjs/vite-plugin-federation";
|
||||||
|
import topLevelAwait from "vite-plugin-top-level-await";
|
||||||
|
import { join } from "path";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: "2025-05-15",
|
||||||
|
devtools: { enabled: false },
|
||||||
|
imports: {
|
||||||
|
dirs: ["composables", "composables/**"],
|
||||||
|
},
|
||||||
|
modules: [
|
||||||
|
"@nuxt/icon",
|
||||||
|
"@nuxt/eslint",
|
||||||
|
"@nuxtjs/tailwindcss",
|
||||||
|
"~/modules/generate-manifest",
|
||||||
|
],
|
||||||
|
// ssr: false,
|
||||||
|
css: ["~/assets/css/tailwind.css"],
|
||||||
|
postcss: {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
"postcss-prefix-selector": {
|
||||||
|
prefix: ".remote-ui", // 远程组件根 class
|
||||||
|
transform(prefix: string, selector: string, prefixedSelector: string) {
|
||||||
|
// 保留全局基础样式
|
||||||
|
if (selector.startsWith("html") || selector.startsWith("body")) {
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
return prefixedSelector;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eslint: {
|
||||||
|
config: {
|
||||||
|
stylistic: {
|
||||||
|
commaDangle: "never",
|
||||||
|
braceStyle: "1tbs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vite: {
|
||||||
|
$client: {
|
||||||
|
base: process.env.NODE_ENV === "production" ? "/" : undefined,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
topLevelAwait({
|
||||||
|
promiseExportName: "__tla",
|
||||||
|
promiseImportName: (i) => `__tla_${i}`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: "./index.ts",
|
||||||
|
name: "RemoteUI",
|
||||||
|
fileName: "remote-ui-kit",
|
||||||
|
formats: ["es"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
hooks: {
|
||||||
|
"vite:extendConfig": async (config, { isClient }) => {
|
||||||
|
if (!isClient) return;
|
||||||
|
|
||||||
|
// 动态导入 manifest,dev/build 都安全
|
||||||
|
let manifest;
|
||||||
|
try {
|
||||||
|
// Nuxt buildDir 是 .nuxt
|
||||||
|
const manifestPath = join(process.cwd(), ".nuxt/generated/manifest.ts");
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
await fs.access(manifestPath);
|
||||||
|
|
||||||
|
// 动态导入
|
||||||
|
manifest = await import(manifestPath);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("manifest not found yet, using empty fallback.");
|
||||||
|
manifest = {
|
||||||
|
manifestInfo: { remoteEntry: "remoteEntry.js" },
|
||||||
|
exposes: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const components = manifest.manifestInfo?.components || [];
|
||||||
|
|
||||||
|
const exposes = components.map((c: { name: string; path: string }) => {
|
||||||
|
return { [c.name]: c.path } as const;
|
||||||
|
});
|
||||||
|
|
||||||
|
config.plugins = config.plugins || [];
|
||||||
|
config.plugins.push(
|
||||||
|
federation({
|
||||||
|
name: "dynamic-remote",
|
||||||
|
filename: manifest.manifestInfo?.remoteEntry || "remoteEntry.js",
|
||||||
|
exposes: exposes || {},
|
||||||
|
shared: { vue: { generate: false } },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
routeRules: {
|
||||||
|
"/_nuxt/**": {
|
||||||
|
headers: {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
serverBundle: {
|
||||||
|
collections: [
|
||||||
|
"ant-design",
|
||||||
|
"carbon",
|
||||||
|
"duo-icons",
|
||||||
|
"fluent",
|
||||||
|
"healthicons",
|
||||||
|
"hugeicons",
|
||||||
|
"icon-park",
|
||||||
|
"line-md",
|
||||||
|
"lsicon",
|
||||||
|
"lucide",
|
||||||
|
"material-symbols",
|
||||||
|
"material-symbols-light",
|
||||||
|
"mdi",
|
||||||
|
"mingcute",
|
||||||
|
"ph",
|
||||||
|
"ri",
|
||||||
|
"si",
|
||||||
|
"solar",
|
||||||
|
"stash",
|
||||||
|
"streamline-plump",
|
||||||
|
"tabler",
|
||||||
|
"uil",
|
||||||
|
"ic",
|
||||||
|
"akar-icons",
|
||||||
|
], // 本地打包的内容
|
||||||
|
// remote: 'jsdelivr' // 'unpkg' or 'github-raw', or a custom function
|
||||||
|
},
|
||||||
|
localApiEndpoint: "/icon/_nuxt_icon",
|
||||||
|
},
|
||||||
|
});
|
68
package.json
Normal file
68
package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "easyai-remote-ui-kit",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"prebuild": "node scripts/updateVersion.js",
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev --port 3201",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview PORT=3202",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"serve": "PORT=3200 node .output/server/index.mjs --cors --single"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design-vue/nuxt": "1.4.6",
|
||||||
|
"@nuxt/icon": "^2.0.0",
|
||||||
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
"@vueuse/core": "^13.6.0",
|
||||||
|
"ant-design-vue": "4.2.6",
|
||||||
|
"nuxt": "^4.0.3",
|
||||||
|
"vue": "^3.5.14",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/akar-icons": "^1.2.7",
|
||||||
|
"@iconify-json/ant-design": "^1.2.5",
|
||||||
|
"@iconify-json/carbon": "^1.2.11",
|
||||||
|
"@iconify-json/duo-icons": "^1.2.2",
|
||||||
|
"@iconify-json/fluent": "^1.2.28",
|
||||||
|
"@iconify-json/healthicons": "^1.2.10",
|
||||||
|
"@iconify-json/hugeicons": "^1.2.6",
|
||||||
|
"@iconify-json/ic": "^1.2.4",
|
||||||
|
"@iconify-json/icon-park": "^1.2.2",
|
||||||
|
"@iconify-json/line-md": "^1.2.8",
|
||||||
|
"@iconify-json/lsicon": "^1.2.5",
|
||||||
|
"@iconify-json/lucide": "^1.2.62",
|
||||||
|
"@iconify-json/material-symbols": "^1.2.31",
|
||||||
|
"@iconify-json/material-symbols-light": "^1.2.31",
|
||||||
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
|
"@iconify-json/mingcute": "^1.2.3",
|
||||||
|
"@iconify-json/ph": "^1.2.2",
|
||||||
|
"@iconify-json/ri": "^1.2.5",
|
||||||
|
"@iconify-json/si": "^1.2.3",
|
||||||
|
"@iconify-json/solar": "^1.2.2",
|
||||||
|
"@iconify-json/stash": "^1.2.4",
|
||||||
|
"@iconify-json/streamline-plump": "^1.2.0",
|
||||||
|
"@iconify-json/tabler": "^1.2.20",
|
||||||
|
"@iconify-json/uil": "^1.2.3",
|
||||||
|
"@nuxt/eslint": "^1.4.1",
|
||||||
|
"@nuxt/kit": "^4.0.3",
|
||||||
|
"@originjs/vite-plugin-federation": "^1.4.1",
|
||||||
|
"@tailwindcss/cli": "^4.1.12",
|
||||||
|
"@tailwindcss/postcss": "^4.1.12",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "^9.27.0",
|
||||||
|
"eslint-config-prettier": "^10.1.5",
|
||||||
|
"eslint-plugin-prettier": "^5.4.0",
|
||||||
|
"postcss-prefix-selector": "^2.1.1",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
|
"sass": "1.77.7",
|
||||||
|
"vite-plugin-top-level-await": "^1.6.0"
|
||||||
|
}
|
||||||
|
}
|
12201
pnpm-lock.yaml
Normal file
12201
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
14
scripts/updateVersion.js
Normal file
14
scripts/updateVersion.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const pkgPath = path.resolve(process.cwd(), "package.json");
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
||||||
|
|
||||||
|
// 自动 patch
|
||||||
|
const versionParts = pkg.version.split(".").map(Number);
|
||||||
|
versionParts[2] += 1; // 每次 build 自动 +1
|
||||||
|
pkg.version = versionParts.join(".");
|
||||||
|
|
||||||
|
// 写回 package.json
|
||||||
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), "utf-8");
|
||||||
|
console.log(`Version updated to ${pkg.version}`);
|
33
server/api/manifest.get.ts
Normal file
33
server/api/manifest.get.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { join } from "path";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { defineEventHandler } from "h3";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
// 跨域
|
||||||
|
event.node.res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
event.node.res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
|
||||||
|
event.node.res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||||
|
|
||||||
|
// 支持 preflight OPTIONS 请求
|
||||||
|
if (event.node.req.method === "OPTIONS") {
|
||||||
|
event.node.res.statusCode = 204;
|
||||||
|
event.node.res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Nuxt buildDir 是 .nuxt
|
||||||
|
const manifestPath = join(process.cwd(), ".nuxt/generated/manifest.ts");
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
await fs.access(manifestPath);
|
||||||
|
|
||||||
|
// 动态导入
|
||||||
|
const { manifestInfo } = await import(manifestPath);
|
||||||
|
|
||||||
|
return manifestInfo;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("manifest not found", err);
|
||||||
|
return { version: "0.0.0", components: [] };
|
||||||
|
}
|
||||||
|
});
|
3
server/tsconfig.json
Normal file
3
server/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../.nuxt/tsconfig.server.json"
|
||||||
|
}
|
75
tailwind.config.js
Normal file
75
tailwind.config.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// tailwind.config.js
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
import { customTheme } from "./utils/customTheme";
|
||||||
|
|
||||||
|
function generateThemeColorScale(colorName) {
|
||||||
|
const rgbValue = hexToRgb(customTheme.colors[colorName]);
|
||||||
|
return {
|
||||||
|
[`theme-${colorName}`]: {
|
||||||
|
DEFAULT: `oklch(from rgb(${rgbValue}) l c h)`,
|
||||||
|
50: `oklch(from rgb(${rgbValue}) calc(l + 0.3) calc(c * 0.2) h)`,
|
||||||
|
100: `oklch(from rgb(${rgbValue}) calc(l + 0.25) calc(c * 0.3) h)`,
|
||||||
|
200: `oklch(from rgb(${rgbValue}) calc(l + 0.2) calc(c * 0.4) h)`,
|
||||||
|
300: `oklch(from rgb(${rgbValue}) calc(l + 0.1) calc(c * 0.6) h)`,
|
||||||
|
400: `oklch(from rgb(${rgbValue}) calc(l + 0.05) calc(c * 0.8) h)`,
|
||||||
|
500: `oklch(from rgb(${rgbValue}) l c h)`,
|
||||||
|
600: `oklch(from rgb(${rgbValue}) calc(l - 0.05) calc(c * 0.9) h)`,
|
||||||
|
700: `oklch(from rgb(${rgbValue}) calc(l - 0.1) calc(c * 0.8) h)`,
|
||||||
|
800: `oklch(from rgb(${rgbValue}) calc(l - 0.15) calc(c * 0.7) h)`,
|
||||||
|
900: `oklch(from rgb(${rgbValue}) calc(l - 0.2) calc(c * 0.6) h)`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:将十六进制颜色转换为 RGB 格式
|
||||||
|
function hexToRgb(hex) {
|
||||||
|
// 移除 # 号(如果存在)
|
||||||
|
hex = hex.replace(/^#/, "");
|
||||||
|
|
||||||
|
// 解析十六进制值
|
||||||
|
const bigint = parseInt(hex, 16);
|
||||||
|
const r = (bigint >> 16) & 255;
|
||||||
|
const g = (bigint >> 8) & 255;
|
||||||
|
const b = bigint & 255;
|
||||||
|
|
||||||
|
return `${r}, ${g}, ${b}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorTheme = {
|
||||||
|
...generateThemeColorScale("primary"),
|
||||||
|
...generateThemeColorScale("success"),
|
||||||
|
...generateThemeColorScale("warning"),
|
||||||
|
...generateThemeColorScale("error"),
|
||||||
|
...generateThemeColorScale("info"),
|
||||||
|
...generateThemeColorScale("background"),
|
||||||
|
...generateThemeColorScale("surface"),
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./components/**/*.{vue,js,ts}",
|
||||||
|
"./layouts/**/*.vue",
|
||||||
|
"./pages/**/*.vue",
|
||||||
|
"./composables/**/*.{js,ts}",
|
||||||
|
"./app.vue",
|
||||||
|
"./plugins/**/*.{js,ts}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
screens: {
|
||||||
|
sm: "640px",
|
||||||
|
md: "768px",
|
||||||
|
lg: "1024px",
|
||||||
|
xl: "1280px",
|
||||||
|
"2xl": "1536px",
|
||||||
|
"3xl": "1920px",
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: colorTheme,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
corePlugins: {
|
||||||
|
// 可按需关闭某些插件,减少生成类
|
||||||
|
// preflight: false, // 如果你不想覆盖 base 样式
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
|
}
|
51
types/common.ts
Normal file
51
types/common.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
export enum MaterialTypeEnum {
|
||||||
|
common = "common", // 公共级别,用户素材
|
||||||
|
personal = "personal", // 个人级别,用户个人素材
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileType = "image" | "video" | "audio" | "3d" | "text";
|
||||||
|
|
||||||
|
export interface IMaterial {
|
||||||
|
_id: string;
|
||||||
|
name: string; // 素材名称
|
||||||
|
ext: string; // 素材后缀
|
||||||
|
type: MaterialTypeEnum; // 素材类型
|
||||||
|
fileType: FileType; // 素材文件类型
|
||||||
|
description: string; // 素材描述
|
||||||
|
tags: string[]; // 素材标签
|
||||||
|
width: number; // 素材宽度
|
||||||
|
height: number; // 素材高度
|
||||||
|
size: number; // 素材大小
|
||||||
|
url: string; // 素材链接
|
||||||
|
mtl?: string; // 3d材质链接
|
||||||
|
user_id: string; // 上传者
|
||||||
|
organizations: string[]; // 组织
|
||||||
|
created_at: number; // 创建时间
|
||||||
|
updated_at: number; // 更新时间
|
||||||
|
preview: string; // 素材预览图
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlobalInjectKeyConst = {
|
||||||
|
AllMaterials: "allMaterials",
|
||||||
|
UploadFileToOSS: "useUtilsUploadFileToOSS",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type GlobalInjectKeyEnum =
|
||||||
|
(typeof GlobalInjectKeyConst)[keyof typeof GlobalInjectKeyConst];
|
||||||
|
|
||||||
|
/** 全局注入的素材数据 */
|
||||||
|
export interface GlobalInjectMaterials {
|
||||||
|
materials: Ref<IMaterial[], IMaterial[]>;
|
||||||
|
refreshMaterials: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全局注入函数 */
|
||||||
|
/**
|
||||||
|
* 上传文件到OSS函数
|
||||||
|
*/
|
||||||
|
export interface GlobalInjectUploadFileToOSS {
|
||||||
|
useUtilsUploadFileToOSS: (
|
||||||
|
file: File | Blob,
|
||||||
|
filename?: string,
|
||||||
|
) => Promise<string>;
|
||||||
|
}
|
12
utils/customTheme.ts
Normal file
12
utils/customTheme.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export const customTheme = {
|
||||||
|
colors: {
|
||||||
|
primary: '#4338CA',
|
||||||
|
secondary: '#03DAC6',
|
||||||
|
success: '#4CAF50',
|
||||||
|
info: '#2196F3',
|
||||||
|
warning: '#FB8C00',
|
||||||
|
error: '#FC3C56',
|
||||||
|
surface: '#FFFFFF',
|
||||||
|
background: '#F5F5F5'
|
||||||
|
}
|
||||||
|
}
|
15
utils/fileUtils.ts
Normal file
15
utils/fileUtils.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 将URL转为blob
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export const ImageUrlToBlob = async (url: string): Promise<Blob> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fetch(url)
|
||||||
|
.then((r) => r.blob())
|
||||||
|
.then((blob) => {
|
||||||
|
resolve(blob);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
9
utils/loadStyle.ts
Normal file
9
utils/loadStyle.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export function loadRemoteStyle(url: string) {
|
||||||
|
if (!document.getElementById(url)) {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.id = url;
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = url;
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user