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