Bläddra i källkod

第一次提交3

baiseventeen 2 år sedan
förälder
incheckning
eb1fe9e8f0

+ 14 - 0
.eslintrc.cjs

@@ -0,0 +1,14 @@
+/* eslint-env node */
+require('@rushstack/eslint-patch/modern-module-resolution')
+
+module.exports = {
+  root: true,
+  'extends': [
+    'plugin:vue/vue3-essential',
+    'eslint:recommended',
+    '@vue/eslint-config-typescript'
+  ],
+  parserOptions: {
+    ecmaVersion: 'latest'
+  }
+}

+ 30 - 0
.gitignore

@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo

+ 6 - 0
.vscode/extensions.json

@@ -0,0 +1,6 @@
+{
+  "recommendations": [
+    "Vue.volar",
+    "dbaeumer.vscode-eslint"
+  ]
+}

+ 1 - 0
env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

BIN
public/favicon.ico


+ 45 - 0
src/apis/ai.ts

@@ -0,0 +1,45 @@
+import axiosInstance from "@/apis/axios.config";
+import type {IDialogue} from "@/types/dialogue";
+
+const AI_PREFIX = "/api/ai"
+
+const aiApis = {
+    getAIDialogues(assignmentId: number, userId: number, page: number=0, size: number=20) {
+        return axiosInstance.get(`${AI_PREFIX}`, {
+            params: {
+                assignmentId,
+                userId,
+                page,
+                size
+            }
+        })
+    },
+    getRewriteResult(assignmentId: number, userId: number) {
+        return axiosInstance.get(`${AI_PREFIX}/rewrite/result`, {
+            params: {
+                assignmentId,
+                userId
+            }
+        })
+    },
+    requestAI(dialogueId: string, model: string, messages: IDialogue[]) {
+        return axiosInstance.put(`${AI_PREFIX}`, {
+            model: model,
+            messages: messages
+        }, {
+            params: {
+                dialogueId: dialogueId
+            }
+        })
+    },
+    rewrite(assignmentId: number, userId: number) {
+        return axiosInstance.post(`${AI_PREFIX}/rewrite`, {}, {
+            params: {
+                assignmentId,
+                studentId: userId
+            }
+        })
+    }
+}
+
+export default aiApis

+ 48 - 0
src/apis/course.ts

@@ -0,0 +1,48 @@
+import axiosInstance from "@/apis/axios.config";
+import type {CourseQuery} from "@/types/query";
+import type {CourseDTO, EnrollDTO} from "@/types/dto";
+
+const COURSE_PREFIX = "/api/course"
+
+const courseApis = {
+    createCourse(courseDTO: CourseDTO){
+      return axiosInstance.post(`${COURSE_PREFIX}`, courseDTO)
+    },
+    getCourseByStudentId(studentId: number, page: number=0, size: number=10){
+        return axiosInstance.get(`${COURSE_PREFIX}/byStudent/${studentId}`, {
+            params: {
+                page,
+                size
+            }
+        })
+    },
+    getCourseByTeacherId(teacherId: number, page: number=0, size: number=10){
+        return axiosInstance.get(`${COURSE_PREFIX}/byTeacher/${teacherId}`, {
+            params: {
+                page,
+                size
+            }
+        })
+    },
+    getCourseByCourseId(courseId: number, page: number=0, size: number=10){
+        return axiosInstance.get(`${COURSE_PREFIX}/${courseId}`, {
+            params: {
+                page,
+                size
+            }
+        })
+    },
+    getAllCourse(courseQuery: CourseQuery, page: number=0, size: number=10){
+        return axiosInstance.post(`${COURSE_PREFIX}/query`,courseQuery, {
+            params: {
+                page,
+                size
+            }
+        })
+    },
+    enrollCourse(enrollDTO: EnrollDTO){
+        return axiosInstance.post(`${COURSE_PREFIX}/enroll`, enrollDTO)
+    }
+}
+
+export default courseApis

+ 32 - 0
src/apis/dictionary.ts

@@ -0,0 +1,32 @@
+import axiosInstance from "@/apis/axios.config";
+
+const DICT_PREFIX = "/api/translate"
+
+interface dictQueryType {
+    q: string;
+    from: string;
+    to: string;
+}
+
+const dictApis = {
+    requestDictionary(query: dictQueryType, assignmentId: number, studentId: number) {
+        return axiosInstance.post(`${DICT_PREFIX}`, {
+            ...query
+        }, {
+            params: {
+                assignmentId,
+                studentId
+            }
+        })
+    },
+    getTranslations(assignmentId: number, studentId: number) {
+        return axiosInstance.get(`${DICT_PREFIX}`, {
+            params: {
+                assignmentId,
+                studentId
+            }
+        })
+    }
+}
+
+export default dictApis

+ 55 - 0
src/apis/engagement.ts

@@ -0,0 +1,55 @@
+import axiosInstance from "@/apis/axios.config";
+
+const ENGAGE_PREFIX = "/api/assignment/engage"
+
+const engagementApis = {
+    engageAssignment(assignmentId:number){
+        return axiosInstance.post(ENGAGE_PREFIX, {}, {
+            params: {
+                assignmentId
+            }
+        })
+    },
+    getEngagement: (studentId: number, assignmentId: number) => {
+        return axiosInstance.get(ENGAGE_PREFIX, {
+            params: {
+                studentId,
+                assignmentId
+            }
+        })
+    },
+    getDetailEngagement: (studentId: number, assignmentId: number) => {
+        return axiosInstance.get(ENGAGE_PREFIX+"/detail", {
+            params: {
+                studentId,
+                assignmentId
+            }
+        })
+    },
+    engagementSubmit: (studentId: number, assignmentId: number) => {
+        return axiosInstance.put(ENGAGE_PREFIX+"/submit", {}, {
+            params: {
+                studentId,
+                assignmentId
+            }
+        })
+    },
+    engagementStage: (studentId: number, assignmentId: number) => {
+        return axiosInstance.put(ENGAGE_PREFIX+"/stage", {}, {
+            params: {
+                studentId,
+                assignmentId
+            }
+        })
+    },
+    engagementCorrect: (studentId: number, assignmentId: number, correctDTO: {score: number, remark: string}) => {
+        return axiosInstance.put(`${ENGAGE_PREFIX}/correct`, correctDTO, {
+            params: {
+                studentId,
+                assignmentId
+            }
+        })
+    }
+}
+
+export default engagementApis

+ 225 - 0
src/components/AIChatter/AIChatter.vue

@@ -0,0 +1,225 @@
+<!--
+  Description: AI对话组件。
+  Author: BaiQi
+  Created Date: 2024-6-21
+-->
+<template>
+  <div :class="$style.container">
+    <!--大语言模型选择器-->
+    <div :class="$style.header">
+      <div :class="$style['model-selector']">
+        <el-dropdown trigger="click" @command="handleCommand">
+        <span style="font-size: 16px">
+          {{ data.model }}<el-icon ><arrow-down/></el-icon>
+        </span>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item v-for="model in modelOptions" :command="model">{{ model }}</el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
+    </div>
+    <!-- 消息区域,采用ChatGPT的交互模式 -->
+    <div :class="$style['message-box']" ref="containerTopRef">
+      <div
+          v-for="(item, index) in data.messages"
+          :key="index"
+          :class="$style.item"
+      >
+        <div>
+          <!--展示头像-->
+          <el-avatar
+              :size="32"
+              :shape="'circle'"
+              :src="index % 2 === 0 ? store.user.userAvatar : ''"
+          >
+            {{ index % 2 === 0 ? store.user.name.charAt(0) : data.model.charAt(0) }}
+          </el-avatar>
+        </div>
+        <div style="margin-left: 15px">
+          <!--名字-->
+          <div style="margin-bottom: 2px">{{ index % 2 === 0 ? 'You' : data.model }}</div>
+          <!--内容-->
+          <div :class="$style.content">{{ item.content }}</div>
+        </div>
+      </div>
+      <div ref="messagesEndRef" />
+    </div>
+
+    <!-- 对话框区域,用于AI提问和提示词模版展现 -->
+    <div :class="$style['question-box']">
+      <div :class="$style['input-group']">
+        <el-input
+            v-model="data.questionContent"
+            ref="inputRef"
+            type="textarea"
+            :autosize="{ minRows: 1, maxRows: 2 }"
+            :placeholder="`Message ${data.model} ...`"
+        />
+<!--        <el-spin v-if="data.loading" style="margin-right: 10px" />-->
+        <el-button
+            type="text"
+            :disabled="data.questionContent === ''"
+            @click="handleSendQuestion"
+            :class="$style.button"
+            v-loading="data.loading"
+        >
+          <el-icon v-if="!data.loading"><Top/></el-icon>
+        </el-button>
+        <el-dropdown @command="handleMenuClick">
+          <el-button type="text" :class="$style.button">
+            <el-icon><ArrowDown/></el-icon>
+          </el-button>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item
+                  v-for="(template, index) in questionTemplates"
+                  :key="index"
+                  :command="index"
+              >
+                {{ template }}
+              </el-dropdown-item>
+            </el-dropdown-menu>
+            </template>
+        </el-dropdown>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import "./AIChatter.module.scss";
+  import type {IDialogue} from "@/types/dialogue.ts";
+  import {useStore} from "@/store";
+  import useApis from "@/apis";
+  import {onMounted, reactive, ref, defineProps, watch, onUnmounted} from "vue";
+  import {ArrowDown, Top} from '@element-plus/icons-vue'
+
+  interface IProps {
+    dialogueId: string;
+    messages: IDialogue[]
+  }
+
+  const props = defineProps<{
+    dialogueId: string,
+    messages: IDialogue[]
+  }>()
+
+  const apis = useApis()
+  const store = useStore()
+
+  const modelOptions = ["ChatGPT", "ChatGLM3", "QWen14B"]
+  const questionTemplates = [
+    "xxx是什么意思?",
+    "英文如何描述xxx",
+    "请用英文描述一个场景或一个观点:xxx",
+    // 更多问题模板...
+  ];
+
+  const inputRef = ref(null);
+  const messagesEndRef = ref(null);
+  const containerTopRef = ref(null);
+
+  let data = reactive<{
+    model: string;
+    questionContent: string;
+    messages: IDialogue[];
+    loading: boolean;
+    loadedPage: number;
+    pageSize: number;
+  }>({
+    model: modelOptions[0],
+    questionContent: '',
+    messages: [],
+    loading: false,
+    loadedPage: 0,
+    pageSize: 20,
+  });
+
+  // 加载聊天记录,并滚动至最后
+  onMounted(() => {
+    if (props.messages) data.messages = props.messages;
+    scrollToEnd();
+  });
+
+  const scrollToEnd = () => {
+    messagesEndRef.value?.scrollIntoView({ behavior: 'smooth' });
+  };
+
+  watch(() => props.messages, (newMessages:any) => {
+    if (newMessages)
+      data.messages = newMessages;
+    scrollToEnd();
+  });
+
+  watch(() => data.messages, () => {
+    scrollToEnd();
+  });
+
+  onMounted(() => {
+    const handleScroll = () => {
+      if (containerTopRef.value) {
+        const { top } = containerTopRef.value.getBoundingClientRect() // 获取目标组件的位置
+        const reachedTop = top >= 0;// 检查组件是否到达顶部
+
+        // 确保,不重复加载数据
+        if (reachedTop && !data.loading) {
+          console.log("触顶了");
+        }
+      }
+    };
+
+    window.addEventListener('scroll', handleScroll);
+
+    onUnmounted(() => {
+      window.removeEventListener('scroll', handleScroll);
+    });
+  });
+
+  const handleSendQuestion = () => {
+    data.loading = true;
+
+    let requestBody:IDialogue[] = data.messages
+    requestBody.push({
+      role: 'user',
+      content: data.questionContent
+    })
+
+    apis.requestAI(props.dialogueId, data.model, requestBody)
+        .then((_res:any) => {
+          const resDialogue = _res.data.data;
+          console.log(resDialogue);
+          data.messages.push(resDialogue);
+        })
+        .catch((_err:any) => {
+          console.log(_err);
+        })
+        .finally(() => {
+          data.questionContent = '';
+          data.loading = false;
+          scrollToEnd();
+        });
+  };
+
+  const handleCommand = (command:string) => {
+    data.model = command
+  }
+
+  // 选择模板问题
+  const handleMenuClick = (key:any) => {
+    data.questionContent = questionTemplates[key];
+    inputRef.value.focus();// 设置焦点自动回到Input
+  };
+
+</script>
+
+<script lang="ts">
+  export default {
+    name: "AIChatter"
+  }
+</script>
+
+<style module>
+@import './AIChatter.module.scss';
+</style>

+ 68 - 0
src/layout/Header/Header.tsx

@@ -0,0 +1,68 @@
+import { Avatar, ConfigProvider, Menu, MenuProps } from 'antd';
+import './index.scss';
+import useStore from "@/store";
+
+interface HeaderProps {
+  items: MenuProps['items'];
+  current?: string[];
+  onChange: (...args: any) => void;
+}
+
+function Header({ items, current, onChange }: HeaderProps) {
+
+  const store = useStore();
+
+  function onClick(...args: any[]) {
+    onChange?.(...args);
+  }
+
+  return <div className="header">
+    <div className="header-icons">
+      <div>
+        Logo
+      </div>
+      <div>
+        /name
+      </div>
+    </div>
+    <div className="header-menu">
+      <ConfigProvider
+        theme={{
+          components: {
+            Menu: {
+              horizontalItemSelectedColor: '#597d3b',
+              algorithm                  : true,
+            },
+          },
+        }}
+      >
+        <Menu onClick={onClick} selectedKeys={current} mode="horizontal" items={items}/>
+      </ConfigProvider>
+    </div>
+    {
+      store.token !== '' ?
+          <div className="header-userName">
+            {
+              store.user.userAvatar ?
+                  <Avatar size={32} src={store.user.userAvatar} /> :
+                    <Avatar size={32}>
+                      user
+                    </Avatar>
+            }
+            <div className="nickName">
+              {store.user.name}
+            </div>
+          </div> :
+          <div className="header-userName">
+            <Avatar size={32}>
+              user
+            </Avatar>
+            <div className="nickName">
+              user
+            </div>
+          </div>
+    }
+  </div>;
+}
+
+export default Header;

+ 0 - 0
src/types/course.ts


+ 29 - 0
src/utils/emitter.ts

@@ -0,0 +1,29 @@
+// 引入mitt
+import mitt from "mitt";
+
+// 创建emitter
+const emitter = mitt()
+
+/*
+  // 绑定事件
+  emitter.on('abc',(value)=>{
+    console.log('abc事件被触发',value)
+  })
+  emitter.on('xyz',(value)=>{
+    console.log('xyz事件被触发',value)
+  })
+
+  setInterval(() => {
+    // 触发事件
+    emitter.emit('abc',666)
+    emitter.emit('xyz',777)
+  }, 1000);
+
+  setTimeout(() => {
+    // 清理事件
+    emitter.all.clear()
+  }, 3000);
+*/
+
+// 创建并暴露mitt
+export default emitter

+ 63 - 0
src/views/CourseSquare/component/CourseDetails/component/CourseHomeworkTable.vue

@@ -0,0 +1,63 @@
+<!--
+  Description: 课程详情页面的课程作业表格组件
+  Author: BaiQi
+  Created Date: 2024-7-1
+-->
+<template>
+  <el-card>
+    <template v-if="props.role == 'teachers'">
+      <el-button color="#597d3b">新建作业</el-button>
+    </template>
+    <template v-if="props.tableData.length > 0">
+      <el-table :data="props.tableData" stripe style="width: 100%">
+        <el-table-column
+            v-for="column in props.columns"
+            :prop="column.dataIndex"
+            :label="column.title"
+        >
+        </el-table-column>
+        <el-table-column label="详情">
+          <template #default="scope">
+            <el-button color="#888" circle/>
+          </template>
+        </el-table-column>
+        <el-table-column :label="role == 'teachers'? '编辑':'写作'">
+          <template #default="scope">
+            <el-button color="#888" circle @click="handleWriting(scope.row.assignmentId)"/>
+          </template>
+        </el-table-column>
+      </el-table>
+    </template>
+    <template v-else>
+      <div style="text-align: center;">暂无作业</div>
+    </template>
+  </el-card>
+</template>
+
+
+<script lang="ts" setup>
+import {defineProps, onMounted} from 'vue';
+  import type {IAssignmentTableColumn, IAssignmentTableItem} from "@/types/vo";
+import router from "@/router";
+
+  const props = defineProps<{
+    tableData: IAssignmentTableItem[],
+    columns: IAssignmentTableColumn[],
+    role: string
+  }>()
+
+  const handleWriting = (assignmentId:number) => {
+    router.push(`/home/assignment/${assignmentId}/edit`);
+    // Location.reload()
+  }
+
+  onMounted(() => {
+    console.log(props)
+  })
+</script>
+
+<script lang="ts">
+export default {
+  name: "CourseHomeworkTable"
+}
+</script>

+ 66 - 0
src/views/Home/component/Silder/component/ExpandableParagraph.vue

@@ -0,0 +1,66 @@
+<!--
+  Description: 实现文字展开收缩效果的组件。
+  Author: BaiQi
+  Created Date: 2024-6-21
+-->
+<template>
+  <div>
+    <div
+        :class="{'collapsed': !expanded, 'expanded': expanded}"
+    >
+      <span>{{ $props.text }}</span>
+    </div>
+    <el-button
+        @click="toggle"
+        type="text"
+        class="toggle-button"
+    >
+      {{ expanded ? '收起' : '展开' }}
+    </el-button>
+  </div>
+</template>
+
+<script>
+import { ref, onMounted } from 'vue';
+
+export default {
+  name: 'ExpandableParagraph',
+  props: {
+    text: {
+      type: String,
+    },
+  },
+  setup(props) {
+    const expanded = ref(false);
+
+    const toggle = () => {
+      expanded.value = !expanded.value;
+    };
+
+    return {
+      expanded,
+      toggle,
+    };
+  },
+};
+</script>
+
+<style scoped>
+.collapsed {
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2; /* Limit to 2 lines */
+  max-height: 3em; /* Height for 2 lines of text */
+}
+
+.expanded {
+  overflow: visible;
+  max-height: none;
+}
+
+.toggle-button {
+  margin-top: -5px;
+  cursor: pointer;
+}
+</style>