Carson 9 місяців тому
батько
коміт
86a332da3f

+ 3 - 0
.env.example

@@ -0,0 +1,3 @@
+VITE_AWS_S3_ACCESS_KEY_ID=
+VITE_AWS_S3_SECRET_ACCESS_KEY=
+VITE_AWS_S3_REGION=

+ 2 - 1
.gitignore

@@ -15,4 +15,5 @@ examples-temp
 node_modules
 pnpm-global
 TODOs.md
-*.timestamp-*.mjs
+*.timestamp-*.mjs
+.env

+ 2 - 1
.vitepress/config.mts

@@ -29,6 +29,7 @@ export default defineConfig({
 
   vite: {
     publicDir: "../public",
+    envDir: '../',
     plugins: [
       VueI18nPlugin({
         /* options */
@@ -122,7 +123,7 @@ export default defineConfig({
         // ],
         sidebar: [
           { text: "关于CocoBlockly X", link: "/docs" },
-          { text: "常见问题解答", link: "/docs/faq" },
+          { text: "常见问题解答", link: "/docs/常见问题解答" },
           {
             text: "开始使用CocoBlockly X",
             link: "/docs/start-using-cocoblockly-x",

+ 2 - 1
README.md

@@ -28,10 +28,11 @@ element-ui plus
 #### 新增icon
 
 - 导出 figma svg,放在 assets/icons 目录下,是plain还是colored看情况而定
-  - plain: 单色图标,可以通过css color属性设置颜色
+  - plain: 单色图标,可以通过css 属性设置颜色(color)和大小
   - colored: 双色图标,固定颜色,一般是多种颜色的图标
 - 导入组件使用
   ```
   import IconNotebook from "~icons/ccrbi-plain/notebook";
   <IconNotebook color="blue" />
   ```
+- 如果要新增一个单独的icon组,需要编辑config.mts vite.Icons里面的内容

+ 80 - 0
components/Edit/AppendModal.vue

@@ -0,0 +1,80 @@
+<script setup lang="ts">
+import { ref, reactive, toRaw, watch } from "vue";
+import type { FormInstance, FormRules } from "element-plus";
+import _ from "lodash";
+import type { TreeData } from "./Tree";
+
+const props = defineProps<{ contextData: TreeData; appendLoading: boolean }>();
+const show = defineModel<boolean>("show");
+const emit = defineEmits(["ok", "cancel"]);
+
+const form$ = ref<FormInstance>();
+const formData = reactive({
+  isDir: false,
+  filename: "",
+});
+const rules = reactive<FormRules<typeof formData>>({
+  filename: [
+    { required: true, trigger: "change" },
+    {
+      validator: (rule, value, callback, source, options) => {
+        if (
+          _.includes(
+            props.contextData.children?.map((c) => c.key),
+            `${props.contextData.key ? `${props.contextData.key}/` : ""}${value}`
+          )
+        ) {
+          callback("文件重名");
+        }
+        callback();
+      },
+      trigger: "change",
+    },
+  ],
+});
+
+const onOk = async () => {
+  try {
+    const result = await form$.value?.validate();
+    if (result) {
+      emit("ok", _.cloneDeep(toRaw(formData)));
+    }
+  } catch (e) {
+    console.warn(e);
+  }
+};
+const onCancel = () => {
+  emit("cancel");
+  show.value = false;
+};
+watch(
+  () => show.value,
+  () => {
+    if (!show.value) {
+      form$.value?.resetFields();
+    }
+  }
+);
+</script>
+<template>
+  <el-dialog v-model="show">
+    <el-form ref="form$" :model="formData" :rules="rules">
+      <el-form-item label="是否文件夹" prop="isDir">
+        <el-switch v-model="formData.isDir" inline-prompt></el-switch>
+      </el-form-item>
+      <el-form-item :label="formData.isDir ? '文件夹名' : '文件名'" prop="filename">
+        <el-input v-model="formData.filename"> </el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          :disabled="!formData.filename"
+          :loading="appendLoading"
+          @click="onOk"
+          type="primary"
+          >ok</el-button
+        >
+        <el-button @click="onCancel">cancel</el-button>
+      </el-form-item>
+    </el-form>
+  </el-dialog>
+</template>

+ 54 - 0
components/Edit/Node.vue

@@ -0,0 +1,54 @@
+<script setup lang="ts">
+import { toRefs } from "vue";
+import type Node from "element-plus/es/components/tree/src/model/node";
+import {
+  Delete,
+  Plus,
+} from "@element-plus/icons-vue";
+import _ from "lodash";
+import type { TreeData } from "./Tree";
+
+const props = defineProps<{
+  node: Node;
+  data: TreeData;
+}>();
+const emit = defineEmits(["append", "remove"]);
+const { node, data } = toRefs(props);
+</script>
+<template>
+  <div class="node">
+    <el-tooltip :content="node.label" :show-after="500">
+      <span class="label">{{ node.label }}</span>
+    </el-tooltip>
+    <el-button-group>
+      <el-button
+        v-if="data.isDir"
+        type="primary"
+        link
+        @click.stop="() => emit('append')"
+        :icon="Plus"
+      >
+      </el-button>
+      <el-button
+        type="danger"
+        link
+        @click.stop="() => emit('remove')"
+        :icon="Delete"
+      >
+      </el-button>
+    </el-button-group>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.node {
+  flex: 1;
+  display: flex;
+  overflow: hidden;
+  .label {
+    text-overflow: ellipsis;
+    overflow: hidden;
+    flex: 1;
+  }
+}
+</style>

+ 6 - 0
components/Edit/Tree.d.ts

@@ -0,0 +1,6 @@
+export type TreeData = {
+  key: string;
+  label: string;
+  isDir?: boolean;
+  children?: TreeData[]
+}

+ 266 - 0
components/Edit/index.vue

@@ -0,0 +1,266 @@
+<script setup lang="ts">
+import { ref, reactive, toRaw, provide, onMounted } from "vue";
+import {v4 as uuid4} from 'uuid'
+import {
+  S3Client,
+  PutObjectCommand,
+  ListObjectsCommand,
+  GetObjectCommand,
+  DeleteObjectsCommand,
+} from "@aws-sdk/client-s3";
+import {
+  Plus,
+  Refresh,
+} from "@element-plus/icons-vue";
+import Node from "./Node.vue";
+import AppendModal from "./AppendModal.vue";
+import type { TreeData } from "./Tree";
+import { ElNotification, type ElTree } from "element-plus";
+import { MdEditor } from "md-editor-v3";
+import { pathsToTree, getTreeLeafs } from "@/utils/s3Helper";
+import "md-editor-v3/lib/style.css";
+
+const DOCS_LIST_BUCKET = "cococlass-help-docs";
+const DOCS_MEDIA_BUCKET = "cococlass-help-docs-medias";
+
+const s3 = new S3Client({
+  credentials: {
+    accessKeyId: import.meta.env.VITE_AWS_S3_ACCESS_KEY_ID,
+    secretAccessKey: import.meta.env.VITE_AWS_S3_SECRET_ACCESS_KEY,
+  },
+  region: import.meta.env.VITE_AWS_S3_REGION,
+});
+provide("s3", s3);
+provide("DOCS_LIST_BUCKET", DOCS_LIST_BUCKET);
+
+const tree$ = ref<InstanceType<typeof ElTree>>();
+
+const dataSource = ref<TreeData[]>([]);
+const sideLoading = ref(false);
+const loadS3DocsListObjects = async () => {
+  const command = new ListObjectsCommand({ Bucket: DOCS_LIST_BUCKET });
+  const result = await s3.send(command);
+  return result.Contents!.map((item) => item.Key!).slice(0, 200);
+};
+const loadSideBar = async () => {
+  sideLoading.value = true;
+  dataSource.value = pathsToTree(await loadS3DocsListObjects());
+  sideLoading.value = false;
+};
+onMounted(() => {
+  loadSideBar();
+});
+
+const currentOpenData = ref<TreeData>();
+
+const onNodeClick = async (data: TreeData, node) => {
+  if (data.isDir) {
+    return;
+  }
+  textLoading.value = true;
+  const command = new GetObjectCommand({
+    Bucket: DOCS_LIST_BUCKET,
+    Key: data.key,
+    ResponseCacheControl: "no-cache",
+  });
+  const file = await s3.send(command);
+  text.value = await file.Body?.transformToString()!;
+  currentOpenData.value = data;
+
+  textLoading.value = false;
+};
+
+const text = ref("");
+const textLoading = ref(false);
+
+const onUploadImg = async (files: File[], callback: (urls: string[] | { url: string; alt: string; title: string }[]) => void) => {
+  console.log(files)
+  let result = []
+  for (const file of files) {
+    try {
+      const key = `${uuid4()}::${file.name}`
+      const command = new PutObjectCommand({
+        Bucket: DOCS_MEDIA_BUCKET,
+        Key: key,
+        Body: file,
+        ACL: 'public-read',
+      });
+      const res = await s3.send(command);
+      result.push({url: `https://${DOCS_MEDIA_BUCKET}.s3.amazonaws.com/${key}`, alt: file.name, title: file.name})
+    } catch (e) {
+      console.error(e)
+      ElNotification.error(`${file.name} 上传失败`)
+    }
+  }
+    callback(result)
+};
+const onSave = async () => {
+  if (!currentOpenData.value) {
+    ElNotification.info('请先选择文件')
+    return;
+  }
+  textLoading.value = true
+  try {
+    const command = new PutObjectCommand({
+      Bucket: DOCS_LIST_BUCKET,
+      Key: currentOpenData.value?.key,
+      Body: text.value,
+    });
+    await s3.send(command);
+    ElNotification.success(`${currentOpenData.value?.key} 保存成功`)
+  } catch (e) {
+    console.error(e)
+    ElNotification.error(`${currentOpenData.value?.key} 保存失败`)
+  } finally {
+    textLoading.value = false
+  }
+};
+
+let resolveAppend: Function | undefined;
+let rejectAppend: Function | undefined;
+const showAppendModal = ref(false);
+const appendLoading = ref(false)
+const appendContextData = ref()
+const onAppend = async (data: TreeData) => {
+  const modalConfirmPromise = new Promise((resolve, reject) => {
+    resolveAppend = resolve;
+    rejectAppend = reject;
+  });
+  appendContextData.value = data
+  showAppendModal.value = true;
+  let filename, isDir
+  try {
+    ( { filename, isDir }  = await modalConfirmPromise);
+  } catch (e) {
+    return
+  }
+  try {
+    appendLoading.value = true
+    const key = `${data.key ? `${data.key}/` : ''}${filename}`
+    if (!isDir) {
+      const command = new PutObjectCommand({
+        Bucket: DOCS_LIST_BUCKET,
+        Key: key,
+        Body: "",
+      });
+      await s3.send(command);
+    }
+    const newChild: TreeData = {
+      key,
+      label: filename,
+      children: [],
+      isDir,
+    };
+    if (!data.children) {
+      data.children = [];
+    }
+    data.children.push(newChild);
+  } catch (e) {
+    console.error(e);
+    ElNotification.error('添加失败')
+  } finally {
+    appendLoading.value = false
+    showAppendModal.value = false;
+  }
+};
+const onRemove = async (node: Node, data: TreeData) => {
+  const parent = node.parent;
+  const children = parent.data.children || parent.data;
+  const index = children.findIndex((d) => d.key === data.key);
+
+  sideLoading.value = true
+  try {
+    const leafDatas = getTreeLeafs(data)
+    const command = new DeleteObjectsCommand({
+      Bucket: DOCS_LIST_BUCKET,
+      Delete: {
+        Objects: leafDatas.map(d => ({Key: d.key})),
+        Quiet: false,
+      }
+    })
+    await s3.send(command)
+    // TODO request S3 delete
+    children.splice(index, 1);
+  } catch (e) {
+    console.error(e)
+    ElNotification.error('删除失败')
+  } finally {
+    sideLoading.value = false
+  }
+};
+</script>
+<template>
+  <div class="md-container">
+    <div class="left" v-loading="sideLoading">
+      <el-button-group>
+        <el-button
+          @click="onAppend({ key: '', label: '', children: dataSource })"
+          type="primary"
+          :icon="Plus"
+        >
+        </el-button>
+        <el-button @click="loadSideBar" type="info" :icon="Refresh"></el-button>
+      </el-button-group>
+      <el-tree
+        ref="tree$"
+        :data="dataSource"
+        node-key="key"
+        default-expand-all
+        :expand-on-click-node="false"
+        @node-click="onNodeClick"
+      >
+        <template #default="{ node, data }">
+          <Node
+            :node="node"
+            :data="data"
+            @append="() => onAppend(data)"
+            @remove="() => onRemove(node, data)"
+          />
+        </template>
+      </el-tree>
+    </div>
+    <MdEditor
+      v-loading="textLoading"
+      :disabled="!currentOpenData"
+      v-model="text"
+      @save="onSave"
+      @uploadImg="onUploadImg"
+    />
+  </div>
+  <AppendModal
+    v-model:show="showAppendModal"
+    :contextData="appendContextData"
+    :appendLoading="appendLoading"
+    @ok="(formData) => resolveAppend?.(formData)"
+    @cancel="() => rejectAppend?.()"
+  />
+</template>
+
+<style lang="scss" scoped>
+.md-container {
+  width: 100vw;
+  height: 100dvh;
+  display: flex;
+  align-items: stretch;
+  .md-editor {
+    flex: 1;
+    height: 100%;
+  }
+  .left {
+    flex: 0 0 300px;
+    padding: 10px;
+    display: flex;
+    flex-direction: column;
+    align-items: stretch;
+    .el-tree {
+      overflow: auto;
+      flex: 1;
+      :deep(.el-tree-node.is-current) {
+        & > .el-tree-node__content {
+          background-color: aqua;
+        }
+      }
+    }
+  }
+}
+</style>

Різницю між файлами не показано, бо вона завелика
+ 2115 - 257
package-lock.json


+ 3 - 0
package.json

@@ -13,10 +13,13 @@
     "vitepress": "^1.2.3"
   },
   "dependencies": {
+    "@aws-sdk/client-s3": "^3.600.0",
     "@iconify/tools": "^4.0.4",
     "@vueuse/core": "^10.11.0",
     "element-plus": "^2.7.5",
     "lodash": "^4.17.21",
+    "md-editor-v3": "^4.17.0",
+    "uuid": "^10.0.0",
     "vue-i18n": "^9.13.1"
   }
 }

+ 7 - 0
pages/__edit__.md

@@ -0,0 +1,7 @@
+---
+layout: false
+---
+<script setup lang="ts">
+import Edit from '@/components/Edit/index.vue'
+</script>
+<Edit />

+ 2 - 2
pages/docs/index.md

@@ -5,12 +5,12 @@ CocoBlockly X是一个在线编程平台,集成图形化编程与Python代码
 
 ###### 1. AI模块编程界面
 
-<img src="@/media/关于CocoBlockly X/关于CocoBlockly X(1).png" width="650">
+<img src="../../media/关于CocoBlockly X/关于CocoBlockly X(1).png" width="650">
 
 
 ###### 2. IoT模块编程界面
 
-<img src="@/media/关于CocoBlockly X/关于CocoBlockly X(2).png" width="650"/>
+<img src="../../media/关于CocoBlockly X/关于CocoBlockly X(2).png" width="650"/>
 
 ---
 

+ 31 - 31
pages/docs/start-using-cocoblockly-x.md

@@ -6,25 +6,25 @@
 
 1. 单击界面右上角【登入】按钮,在弹出的登录窗口单击【注册】,或选择第三方登录,直接使用QQ或微信登录。
 
-<img src="@/media/开始使用CocoBlockly X/账号注册登录(1).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/账号注册登录(1).png" width="650"/>
 
-<img src="@/media/开始使用CocoBlockly X/账号注册登录(2).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/账号注册登录(2).png" width="650"/>
 
-<img src="@/media/开始使用CocoBlockly X/账号注册登录(3).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/账号注册登录(3).png" width="650"/>
 
 2. 进入注册界面后,输入电子邮箱、密码及学校名称(可不填)后,单击【注册】。
 
-<img src="@/media/开始使用CocoBlockly X/账号注册登录(4).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/账号注册登录(4).png" width="650"/>
 
 3. 提示注册成功,可可乐博会给该邮箱发送一封激活邮件。
 
-<img src="@/media/开始使用CocoBlockly X/账号注册登录(5).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/账号注册登录(5).png" width="650"/>
 
 4. 登录邮箱,在激活邮件中单击【点击进行激活】按键后在弹出的页面中输入注册时使用的邮箱及密码,登录完成后激活成功,注册完成。
 
-<img src="@/media/开始使用CocoBlockly X/账号注册登录(6).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/账号注册登录(6).png" width="650"/>
 
-<img src="@/media/开始使用CocoBlockly X/账号注册登录(7).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/账号注册登录(7).png" width="650"/>
 
 
 
@@ -34,43 +34,43 @@
 
 1. 打开谷歌Chrome浏览器。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(1).png" style="zoom:30%;" />
+   <img src="../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(1).png" style="zoom:30%;" />
 
 2. 在地址栏输入x.cocorobo.cn进入CocoBlockly X编程平台。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(2).png" width="650"/>
+   <img src="../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(2).png" width="650"/>
 
 3. 进入后,鼠标左键单击下载图标。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(3).png" width="650"/>
+   <img src="../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(3).png" width="650"/>
 
 4. 根据电脑的系统选择下载的版本,建议保存到桌面。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(4).png" width="650"/>
+   <img src="../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(4).png" width="650"/>
 
 5. 等待下载完成后,在下载位置找到后缀为.exe的安装包。鼠标左键双击安装包。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(5).png" style="zoom:50%;" />
+   <img src="../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(5).png" style="zoom:50%;" />
 
 6. 选择安装目标文件夹后,鼠标左键单击安装开始安装。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(6).png" width="650"/>
+   <img src="../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(6).png" width="650"/>
 
 7. 安装过程中,安装程序会自动安装驱动,按提示安装即可。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(7).png" width="650"/>
+   <img src="../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(7).png" width="650"/>
 
 8. 驱动安装完成后,等待CocoBlockly X Uploader安装完成,鼠标左键点击【完成】完成安装。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(8).png" width="650"/>
+   <img src="../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(8).png" width="650"/>
 
 9. 重新回到CocoBlockly X编程界面,检查编程环境,图标为绿色框+对号时,表示编程环境无误。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(9).png" width="650"/>
+   <img src="../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(9).png" width="650"/>
 
 10. 如果有IoT模块或AI模块,可以使用数据线将模块连接到电脑。电脑会自动识别到连接端口。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(10).png" width="650"/>
+   <img src="../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(10).png" width="650"/>
 
 
 
@@ -80,7 +80,7 @@
 
 ## 编程界面介绍
 
-<img src="@/media/开始使用CocoBlockly X/编程界面介绍(1).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/编程界面介绍(1).png" width="650"/>
 
 上图中我们将CocoBlockly X编程界面划分为7个区域:
 
@@ -112,43 +112,43 @@
 
 (1)鼠标左键单击【文件】,选择【导出】。
 
-   <img src="@/media/开始使用CocoBlockly X/编程文件存取(1).png" width="650"/>
+   <img src="../../media/开始使用CocoBlockly X/编程文件存取(1).png" width="650"/>
 
 (2)在弹出的对话框中选择保存文件的路径,选择完成后,重命名文件,鼠标左键单击【保存】即可保存.xml格式的程序文件。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(2).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/编程文件存取(2).png" width="650"/>
 
 2. **导入**
 
 (1)鼠标左键单击【文件】,选择【导入】。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(3).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/编程文件存取(3).png" width="650"/>
 
 (2)在弹出的对话框中选择之前保存文件的路径,选择完成后鼠标左键单击【打开】即可打开之前保存的.xml格式的程序文件。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(4).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/编程文件存取(4).png" width="650"/>
 
 (3)也可以通过直接将.xml格式的程序文件拖拽到CocoBlockly X积木编程区完成程序文件的导入。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(5).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/编程文件存取(5).png" width="650"/>
 
 3. 云端存储
 
 (1)鼠标左键单击【文件】,选择【云端存储】。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(6).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/编程文件存取(6).png" width="650"/>
 
 (2)如未登录,在弹出的对话框登录注册的账号。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(7).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/编程文件存取(7).png" width="650"/>
 
 (3)鼠标左键单击新建云端存储文件,命名并单击【保存】,来保存程序文件到云端。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(8).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/编程文件存取(8).png" width="650"/>
 
 (4)从云端加载程序文件,鼠标左键单击文件夹图标打开程序即可。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(9).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/编程文件存取(9).png" width="650"/>
 
 ## Python源代码
 
@@ -156,7 +156,7 @@
 
 CocoBlockly X的Python源代码区域,支持用户对代码进行编辑、复制、下载及上传,下载后缀为.py格式的python文档。
 
-<img src="@/media/开始使用CocoBlockly X/Python源代码(1).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/Python源代码(1).png" width="650"/>
 
 1. **代码编辑模式开关:** 能够通过此开关开启或关闭Python源代码编辑模式。
 2. **复制代码:** 鼠标左键单击此图标能够复制Python源代码区域的Python源代码。
@@ -171,10 +171,10 @@ CocoBlockly X的Python源代码区域,支持用户对代码进行编辑、复
 
 CocoBlockly X的串口交互窗区域,支持用户实现与电子模块的窗口交互,并提供曲线图的数据可视化效果。
 
-<img src="@/media/开始使用CocoBlockly X/串口交互窗(1).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/串口交互窗(1).png" width="650"/>
 
-<img src="@/media/开始使用CocoBlockly X/串口交互窗(2).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/串口交互窗(2).png" width="650"/>
 
 串口交互窗的温度数据:
 
-<img src="@/media/开始使用CocoBlockly X/串口交互窗(3).png" width="650"/>
+<img src="../../media/开始使用CocoBlockly X/串口交互窗(3).png" width="650"/>

+ 4 - 4
pages/docs/faq.md → pages/docs/常见问题解答.md

@@ -24,7 +24,7 @@ B:可能是运输过程中SD卡松动导致的,这种情况下需要重新
 
 **3.AI模块与屏幕模块的组合在连接电脑或电源后,屏幕红屏显示“Welcome to MaixPy”。**
 
-<img src="@/media/常见问题解答/w4.png" width="365"/><br>
+<img src="../../media/常见问题解答/w4.png" width="365"/><br>
 
 A:插拔电源线重新连接看能否正常启动。
 
@@ -93,15 +93,15 @@ C:更换SD卡,重新拷贝预置文件再试试。
 
 (1)点击控制面板;
 
-​         <img src="@/media/常见问题解答/w1.png" width="400"/>
+​         <img src="../../media/常见问题解答/w1.png" width="400"/>
 
 (2)点击程序里面的卸载程序;
 
-​         <img src="@/media/常见问题解答/w2.png"/>
+​         <img src="../../media/常见问题解答/w2.png"/>
 
 (3)找到所有的Python,全部右键卸载。
 
-​         <img src="@/media/常见问题解答/w3.png"/>
+​         <img src="../../media/常见问题解答/w3.png"/>
 
 
 

+ 6 - 6
pages/zh-HK/docs/faq.md

@@ -10,13 +10,13 @@
 
 A:如果您無TF 卡讀卡器,請在CocoBlockly X上按下圖步驟上載程式,嘗試解決問題。
 
-<img src="@/media/常见问题解答/常见问题解答(1).png" width="650"/><br>
+<img src="../../../media/常见问题解答/常见问题解答(1).png" width="650"/><br>
 
 B:如果您有TF 卡讀卡器,請您用TF 卡讀卡器檢查一下AI 模組中TF 卡,確認其中是否有包含main.py這個文件,如果沒有的話,可以從<a href=" /download/main.py">這裡</a>下載後放入TF 卡即可。
 
 ### 2. 我的AI模組顯示以下效果,應該怎麼辦
 
-<img src="@/media/常见问题解答/AI_lack_sd.jpg" width="365"/><br>
+<img src="../../../media/常见问题解答/AI_lack_sd.jpg" width="365"/><br>
 
 A:首先檢查AI模組上是否已經插入SD卡。
 
@@ -24,7 +24,7 @@ B:可能是運輸過程中SD卡鬆動導致的,這種情況下需要重新
 
 ### 3. AI模組與螢幕模組的組合在連接電腦或電源後,螢幕紅屏顯示“Welcome to MaixPy”。
 
-<img src="@/media/常见问题解答/w4.png" width="365"/><br>
+<img src="../../../media/常见问题解答/w4.png" width="365"/><br>
 
 A:插拔電源線重新連接看能否正常啟動。
 
@@ -92,15 +92,15 @@ C:更換SD卡,重新拷貝預置文件再次嘗試。
 
 (1)点击控制面板;
 
-​         <img src="@/media/常见问题解答/w1.png" width="400"/>
+​         <img src="../../../media/常见问题解答/w1.png" width="400"/>
 
 (2)点击程式里面的卸载程式;
 
-​         <img src="@/media/常见问题解答/w2.png"/>
+​         <img src="../../../media/常见问题解答/w2.png"/>
 
 (3)找到所有的Python,全部右键卸载。
 
-​         <img src="@/media/常见问题解答/w3.png"/>
+​         <img src="../../../media/常见问题解答/w3.png"/>
 
 
 

+ 2 - 2
pages/zh-HK/docs/index.md

@@ -5,12 +5,12 @@ CocoBlockly X是一個程式編寫平臺,集成了視覺化編程與Python代
 
 ###### 1. AI 模組編程界面
 
-<img src="@/media/关于CocoBlockly X/关于CocoBlockly X(1).png" width="650">
+<img src="../../../media/关于CocoBlockly X/关于CocoBlockly X(1).png" width="650">
 
 
 ###### 2. IoT 模組編程界面
 
-<img src="@/media/关于CocoBlockly X/关于CocoBlockly X(2).png" width="650"/>
+<img src="../../../media/关于CocoBlockly X/关于CocoBlockly X(2).png" width="650"/>
 
 ---
 

+ 31 - 31
pages/zh-HK/docs/start-using-cocoblockly-x.md

@@ -6,17 +6,17 @@
 
 1.單擊界面右上角【登入】按鈕,在彈出的登錄窗口單擊【登入】,或可直接使用Google賬號登入。
 
-<img src="@/media/开始使用CocoBlockly X/账号注册登录(1).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/账号注册登录(1).png" width="650"/>
 
-<img src="@/media/开始使用CocoBlockly X/账号注册登录(2).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/账号注册登录(2).png" width="650"/>
 
 2.進入註冊界面後,輸入電子郵箱、密碼及學校名稱(可不填)後,單擊【註冊】。
 
-<img src="@/media/开始使用CocoBlockly X/账号注册登录(4).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/账号注册登录(4).png" width="650"/>
 
 3.註冊成功之後會自動登入。
 
-<img src="@/media/开始使用CocoBlockly X/账号注册登录(5).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/账号注册登录(5).png" width="650"/>
 
 
 ## 軟件安裝指引
@@ -25,37 +25,37 @@
 
 1.打開谷歌Chrome瀏覽器。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(1).png" style="zoom:30%;" />
+   <img src="../../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(1).png" style="zoom:30%;" />
 
 2.在地址欄輸入x.cocorobo.hk進入CocoBlockly X編程平台。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(2).png" width="650"/>
+   <img src="../../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(2).png" width="650"/>
 
 3.進入後,鼠標左鍵單擊下載圖標。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(3).png" width="650"/>
+   <img src="../../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(3).png" width="650"/>
 
 4.根據電腦的系統選擇下載的版本,建議保存到桌面。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(4).png" width="650"/>
+   <img src="../../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(4).png" width="650"/>
 
 5.1 安裝CocoBlockly X Windows版本
 
    (1)等待下載完成後,在下載位置找到後綴為.exe的安裝包。鼠標左鍵雙擊安裝包。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(5).png" style="zoom:50%;" />
+   <img src="../../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(5).png" style="zoom:50%;" />
 
    (2)選擇安裝目標文件夾後,鼠標左鍵單擊安裝,開始安裝。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(6).png" width="650"/>
+   <img src="../../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(6).png" width="650"/>
 
    (3)安裝過程中,安裝程式會自動安裝驅動,按提示安裝即可。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(7).png" width="650"/>
+   <img src="../../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(7).png" width="650"/>
 
    (4)驅動安裝完成後,等待CocoBlockly X Uploader安裝完成,鼠標左鍵點擊【完成】完成安裝。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(8).png" width="650"/>
+   <img src="../../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(8).png" width="650"/>
 
    
 
@@ -63,23 +63,23 @@
 
    (1)點擊下載Mac安裝包,建議下載至桌面。
 
-   <img src="@/media/开始使用CocoBlockly X/mac安裝1.png" width="650"/>
+   <img src="../../../media/开始使用CocoBlockly X/mac安裝1.png" width="650"/>
 
    (2)雙擊安裝包進行安裝。
 
-   <img src="@/media/开始使用CocoBlockly X/mac安裝2.png" width="200"/>
+   <img src="../../../media/开始使用CocoBlockly X/mac安裝2.png" width="200"/>
 
    (3)根據安裝指示進行操作。點擊【繼續】-【安裝】(建議使用默認路徑)-【輸入帳戶密碼】允許安裝-安裝成功。
 
-   <img src="@/media/开始使用CocoBlockly X/mac安裝3.png" width="850"/>
+   <img src="../../../media/开始使用CocoBlockly X/mac安裝3.png" width="850"/>
 
 6.重新回到CocoBlockly X編程界面,檢查編程環境,圖標為綠色框+對號時,表示編程環境無誤。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(9).png" width="650"/>
+   <img src="../../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(9).png" width="650"/>
 
 7.如果有IoT模組或AI模組,可以使用數據線將模組連接到電腦。電腦會自動識別到連接端口。
 
-   <img src="@/media/开始使用CocoBlockly X/开始使用CocoBlockly X(10).png" width="650"/>
+   <img src="../../../media/开始使用CocoBlockly X/开始使用CocoBlockly X(10).png" width="650"/>
 
 
 
@@ -89,7 +89,7 @@
 
 ## 编程界面介绍
 
-<img src="@/media/开始使用CocoBlockly X/编程界面介绍(1).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/编程界面介绍(1).png" width="650"/>
 
 上圖中我們將CocoBlockly X編程界面劃分為7個區域:
 
@@ -119,43 +119,43 @@
 
 (1)鼠標左鍵單擊【存儲】,選擇【導出】。
 
-   <img src="@/media/开始使用CocoBlockly X/编程文件存取(1).png" width="650"/>
+   <img src="../../../media/开始使用CocoBlockly X/编程文件存取(1).png" width="650"/>
 
 (2)在彈出的對話框中選擇存儲文件的路徑,選擇完成後,重命名文件,鼠標左鍵單擊【保存】即可保存.xml格式的程式文件。(以Mac系統為例)
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(2).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/编程文件存取(2).png" width="650"/>
 
 2.**導入**
 
 (1)鼠標左鍵單擊【存儲】,選擇【導入】。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(3).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/编程文件存取(3).png" width="650"/>
 
 (2)在彈出的對話框中選擇之前存儲的路徑,選擇完成後鼠標左鍵單擊【打開】即可打開之前保存的.xml格式的程式文件。(以Mac系統為例)
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(4).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/编程文件存取(4).png" width="650"/>
 
 (3)也可以透過直接將.xml格式的程式文件拖拽到CocoBlockly X積木編程區完成程式文件的導入。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(5).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/编程文件存取(5).png" width="650"/>
 
 3.**雲端存儲**
 
 (1)鼠標左鍵單擊【文件】,選擇【雲端存儲】。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(6).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/编程文件存取(6).png" width="650"/>
 
 (2)如未登錄,在彈出的對話框登錄註冊的賬號。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(7).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/编程文件存取(7).png" width="650"/>
 
 (3)鼠標左鍵單擊新建雲端存儲文件,命名並單擊【保存】,來保存程式文件到雲端。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(8).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/编程文件存取(8).png" width="650"/>
 
 (4)從雲端加載程式文件,鼠標左鍵單擊文件夾圖標打開程式即可。
 
-<img src="@/media/开始使用CocoBlockly X/编程文件存取(9).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/编程文件存取(9).png" width="650"/>
 
 ## python程式碼
 
@@ -163,7 +163,7 @@
 
 CocoBlockly X的python程式碼區域,支持用戶對代碼進行編輯、複製、下載及上載,下載後綴為.py格式的python檔案。
 
-<img src="@/media/开始使用CocoBlockly X/Python源代码(1).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/Python源代码(1).png" width="650"/>
 
 1. **代碼編輯模式開關:** 能夠透過此開關開啟或關閉python程式碼編輯模式。
 2. **複製代碼:** 鼠標左鍵單擊此圖標能夠複製python程式碼區域的python程式碼。
@@ -178,10 +178,10 @@ CocoBlockly X的python程式碼區域,支持用戶對代碼進行編輯、複
 
 CocoBlockly X的序列埠互動窗區域,支持用戶實現與電子模組的窗口交互,並提供曲線圖的數據可視化效果。
 
-<img src="@/media/开始使用CocoBlockly X/串口交互窗(1).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/串口交互窗(1).png" width="650"/>
 
-<img src="@/media/开始使用CocoBlockly X/串口交互窗(2).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/串口交互窗(2).png" width="650"/>
 
 序列埠互動窗的溫度數據:
 
-<img src="@/media/开始使用CocoBlockly X/串口交互窗(3).png" width="650"/>
+<img src="../../../media/开始使用CocoBlockly X/串口交互窗(3).png" width="650"/>

+ 37 - 0
utils/s3Helper.ts

@@ -0,0 +1,37 @@
+import _ from "lodash";
+
+export const pathsToTree = (paths: string[]) => {
+  let __result = [];
+  let level = { __result, __prefix: "" };
+
+  paths.forEach((path) => {
+    path.split("/").reduce((r, label, i, a) => {
+      if (!r[label]) {
+        const __prefix = `${r.__prefix ? r.__prefix + "/" : ""}${label}`;
+        r[label] = { __result: [], __prefix };
+        r.__result.push({
+          key: __prefix,
+          label,
+          isDir: a.length !== i + 1,
+          children: r[label].__result,
+        });
+      }
+
+      return r[label];
+    }, level);
+  });
+  return __result;
+};
+
+type TreeNodeLike = {
+  children?: TreeNodeLike[];
+};
+
+export const getTreeLeafs = <T extends TreeNodeLike>(
+  treeNode: T
+): T[] => {
+  if (treeNode.children?.length) {
+    return _.flatMap<T, T>(treeNode.children as T[], getTreeLeafs);
+  }
+  return [treeNode];
+};

+ 12 - 0
vite-env.d.ts

@@ -0,0 +1,12 @@
+/// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+  readonly VITE_AWS_S3_ACCESS_KEY_ID: string
+  readonly VITE_AWS_S3_SECRET_ACCESS_KEY: string
+  readonly VITE_AWS_S3_REGION: string
+  // more env variables...
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv
+}

Деякі файли не було показано, через те що забагато файлів було змінено