baiseventeen 2 rokov pred
rodič
commit
aa3a78a16a

+ 1 - 0
src/assets/logo.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

+ 35 - 0
src/assets/main.css

@@ -0,0 +1,35 @@
+@import './base.css';
+
+#app {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 2rem;
+  font-weight: normal;
+}
+
+a,
+.green {
+  text-decoration: none;
+  color: hsla(160, 100%, 37%, 1);
+  transition: 0.4s;
+  padding: 3px;
+}
+
+@media (hover: hover) {
+  a:hover {
+    background-color: hsla(160, 100%, 37%, 0.2);
+  }
+}
+
+@media (min-width: 1024px) {
+  body {
+    display: flex;
+    place-items: center;
+  }
+
+  #app {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    padding: 0 2rem;
+  }
+}

+ 39 - 0
src/components/languageIcon/languageIcon.vue

@@ -0,0 +1,39 @@
+<!--
+  Description: 中英文转换效果组件,在字典组件中。
+  Author: BaiQi
+  Created Date: 2024-6-21
+-->
+<template>
+  <div :class="$style['language-icon']" @click="onChange">
+    <div class="text-icons.y">中</div>
+      <Refresh />
+    <div :class="['text-icons', language == 'en' ? 'z' : 'y']">英</div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import {Refresh} from "@element-plus/icons-vue"
+  import {defineProps, ref} from 'vue'
+
+  const props = defineProps<{
+    currentLanguage: string
+  }>()
+
+  let language = ref(props.currentLanguage)
+
+  const onChange = () =>{
+    console.log("点击了")
+    language.value = language.value == 'zh' ? 'en' : 'zh'
+    console.log(language.value)
+  }
+</script>
+
+<script lang="ts">
+export default {
+  name: "LanguageIcon"
+}
+</script>
+
+<style module>
+@import "index.scss";
+</style>

+ 57 - 0
src/layout/Layout/Layout.tsx

@@ -0,0 +1,57 @@
+import { AppContext } from '@/context/AppContext';
+import React, { useState } from 'react';
+import Header from '../Header/Header';
+import './index.scss';
+
+interface LayoutProps {
+  children: (current: string) => React.ReactNode;
+}
+
+const items = [
+  {
+    label: '课程广场',
+    key  : 'square',
+  },
+  {
+    label: '我的课程',
+    key  : 'course',
+  },
+  {
+    label: '我的作业',
+    key  : 'work',
+  },
+];
+
+
+// 简单的三段式布局
+const Layout: React.FC<LayoutProps> = ({ children }) => {
+  const [current, setCurrent] = useState(['square']);
+  const [onSetCurrent, setOnSetCurrent] = useState<Function | null>(null);
+
+  function onChange({ key }: { key: string }) {
+    const res = current[0];
+    if (res === 'work') {
+      const current = () => () => {
+        setCurrent([key]);
+        setOnSetCurrent(null);
+        return void 0;
+      };
+      // @ts-ignore
+      setOnSetCurrent(current);
+      return;
+    }
+    setCurrent([key]);
+  }
+
+  return (
+    // 监听onSetCurrent变化出现弹出离开 调用设置
+    <AppContext.Provider value={{ current, onSetCurrent }}>
+      <div className="app">
+        <Header items={items} onChange={onChange} current={current}></Header>
+        {children(current[0])}
+      </div>
+    </AppContext.Provider>
+  );
+};
+
+export default Layout;

+ 42 - 0
src/layout/Layout/Layout.vue

@@ -0,0 +1,42 @@
+<!--
+  Description: 主页面布局
+  Author: BaiQi
+  Created Date: 2024-6-30
+-->
+<script setup>
+
+import Header from "../Header/Header.vue";
+import Role from '@/views/Home/Role.vue'
+</script>
+
+<template>
+  <div class="wrapper">
+    <el-container style="height: max-content;">
+      <el-header height="40px" style="padding: 0;" class="fixed-header">
+        <Header></Header>
+      </el-header>
+    </el-container>
+    <el-container>
+      <el-main style="height: 100%;;padding: 0 0 0 0;margin-top: 40px">
+        <Role></Role>
+      </el-main>
+    </el-container>
+  </div>
+</template>
+
+<style scoped>
+.wrapper {
+  width: 100%;
+  height: 100%;
+  background-size: 100% 100%;
+}
+.fixed-header {
+  position: fixed;
+  width: 100%;
+  top: 0;
+  left: 0;
+  z-index: 1000; /* 确保 header 在内容之上 */
+  padding: 0;
+  height: 40px;
+}
+</style>

+ 20 - 0
src/main.ts

@@ -0,0 +1,20 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+// 引入ElementUI-Plus
+import ElementPlus from "element-plus"
+import 'element-plus/dist/index.css'
+// 引入pinia
+import { createPinia } from "pinia";
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
+import "./assets/global.css"
+
+const app = createApp(App)
+const pinia = createPinia()
+pinia.use(piniaPluginPersistedstate)
+
+app.use(router)
+app.use(ElementPlus)
+app.use(pinia)
+
+app.mount('#app')

+ 12 - 0
src/sass/public.scss

@@ -0,0 +1,12 @@
+.size-width-100 {
+  width: 100%;
+  height: 100%;
+}
+
+.position-center-center {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+

+ 176 - 0
src/views/Home/component/Silder/index.vue

@@ -0,0 +1,176 @@
+<!--
+  Description: 写作界面右侧,侧边栏组件。
+  Author: BaiQi
+  Created Date: 2024-6-21
+-->
+<template>
+  <div :class="rootClassName">
+    <div class="slider-icons" @click="toggleShrink">
+      <el-icon v-if="isShrink"><Expand/></el-icon>
+      <el-icon v-if="!isShrink"><Fold/></el-icon>
+    </div>
+    <div class="slider-item-box">
+      <!--折叠-->
+      <template v-if="isShrink">
+        <div class="slider-item" @click="toggleShrink">描述</div>
+        <div v-if="data.assignment.descriptionFile" class="slider-item" @click="toggleShrink">要求</div>
+        <div v-if="data.assignment.attachments" class="slider-item" @click="toggleShrink">附件</div>
+        <div class="slider-item" @click="toggleShrink">批改</div>
+      </template>
+      <!--展开-->
+      <template v-else>
+        <!--作业名称-->
+        <div>{{data.assignment.assignmentName}}</div>
+        <!--作业描述-->
+        <ExpandableParagraph :text="data.assignment.description" />
+        <!--下载按钮-->
+        <div class="down">
+          <el-link type="primary">下载</el-link>
+        </div>
+        <!--要求文件和附件-->
+        <template v-if="data.assignment.descriptionFile || data.assignment.attachments">
+          <div>范文、介绍</div>
+          <div class="slider-tag">
+            <template v-if="data.assignment.descriptionFile">
+              <div class="slider-tag-item" @click="openEditor(data.assignment.descriptionFile, data.assignment.assignmentName)">作业要求</div>
+            </template>
+            <template v-if="data.assignment.attachments">
+              <div v-for="(item, index) in data.assignment.attachments" :key="index" class="slider-tag-item" @click="openEditor(item, '附件' + index)">附件{{ index }}</div>
+            </template>
+          </div>
+        </template>
+        <div class="edit-box-header">
+          智能批改修改意见
+          <el-button @click="handleCorrect">智能批改</el-button>
+        </div>
+        <div class="capacity-item">
+<!--          <el-spin :spinning="loading" style="height: 100%; width: 100%; display: flex; align-items: center; justify-content: center;">-->
+<!--            <div v-if="!loading" class="remark">{{ aiRemark }}</div>-->
+<!--          </el-spin>-->
+          <div class='remark'>{{aiRemark}}</div>
+        </div>
+      </template>
+    </div>
+    <ViewsWordEditor v-if="data.assignment.descriptionFile" :alert="alertProps" :docxInfo="docxInfo"></ViewsWordEditor>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {defineProps, onMounted, reactive, ref, watch, watchEffect} from 'vue'
+  import "./index.scss"
+  import type {IAssignment} from "@/types/vo";
+  import useApis from "@/apis";
+  import {useStore} from "@/store";
+  import {Expand, Fold} from "@element-plus/icons-vue"
+  import ExpandableParagraph from "@/views/Home/component/Silder/component/ExpandableParagraph.vue";
+import ViewsWordEditor from "@/components/ViewsWordEditor/ViewsWordEditor.vue";
+import {useRoute} from "vue-router";
+
+  interface ISliderProps {
+    dialogueId: string;
+  }
+
+  interface IPreviewProps {
+    url: string;
+    title: string;
+  }
+
+  const apis = useApis()
+  const store = useStore()
+  const props = defineProps<ISliderProps>()
+
+  let isShrink = ref(false)
+  let aiRemark = ref("")
+  let loading = ref(false)
+  let open= ref(false)
+  let expanded = ref(false)
+
+  // 获得路由中的assignmentId
+  const route = useRoute()
+  const assignmentId = Number(route.params.assignmentId)
+
+  let data = reactive<{
+    assignment: IAssignment,
+    currentPreview: IPreviewProps
+  }>({
+    assignment: {} as IAssignment,
+    currentPreview: {} as IPreviewProps
+  })
+
+  const alertProps = reactive({
+    open: open.value,
+    onOk: () => (open.value = false),
+    onCancel: () => (open.value = false),
+  });
+
+  const docxInfo = reactive({
+    docxUrl: data.currentPreview.url,
+    docxTitle: data.currentPreview.title,
+  });
+
+  let rootClassName = ref(`slider-content ${isShrink.value ? "shrink" : ""}`);
+
+  const toggleShrink = () => {
+    isShrink.value = !isShrink.value;
+    rootClassName.value = `slider-content ${isShrink.value ? "shrink" : ""}`
+  };
+
+  // 获取作业信息
+  const fetchData = () => {
+    apis.getAssignmentById(assignmentId).then((res: any) => {
+      data.assignment = res.data.data;
+      console.log(res.data.data, "获取作业信息")
+    });
+  };
+
+  // 请求批改
+  const handleCorrect = () => {
+    loading.value = true;
+    apis.rewrite(assignmentId, store.user.id)
+        .then((res: any) => {
+          aiRemark.value = res.data.data.content;
+        })
+        .catch((err: any) => {
+          console.error(err);
+        })
+        .finally(() => {
+          loading.value = false;
+        });
+  };
+
+  const openEditor = (url: string, title: string) => {
+    open.value = true;
+    data.currentPreview.url = url;
+    data.currentPreview.title = title;
+    Object.assign(alertProps, {
+      open: open.value,
+      // onOk: () => (open.value = false),
+      onCancel: () => {
+        open.value = false
+        Object.assign(alertProps,{
+          open: open.value,
+          onOk: () => (open.value = false),
+          onCancel: () => (open.value = false),
+        })
+        Object.assign(docxInfo, {
+          docxUrl: data.currentPreview.url,
+          docxTitle: data.currentPreview.title,
+        })
+      },
+    })
+    Object.assign(docxInfo, {
+      docxUrl: data.currentPreview.url,
+      docxTitle: data.currentPreview.title,
+    })
+  };
+
+  onMounted(() => {
+    fetchData();
+  });
+</script>
+
+<script lang="ts">
+export default {
+  name: "Slider"
+}
+</script>

+ 149 - 0
src/views/HomeworkPage/index.vue

@@ -0,0 +1,149 @@
+<!--
+  Description: 我的作业界面
+  Author: BaiQi
+  Created Date: 2024-7-3
+-->
+<template>
+  <el-card style="margin: 10px auto 0;height: 625px; width: 80%" shadow="never">
+    <el-button :icon="Refresh" circle></el-button>
+    <el-select
+        v-model="state"
+        placeholder="状态"
+        size="default"
+        style="width: 150px; margin-left: 10px;"
+    >
+      <el-option
+          v-for="item in stateOptions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+      />
+    </el-select>
+    <el-select
+        v-model="state"
+        placeholder="课程"
+        size="default"
+        style="width: 150px; margin-left: 10px;"
+    >
+      <el-option
+          v-for="item in stateOptions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+      />
+    </el-select>
+
+    <el-table :data="homeworkList.tableData" style="width: 100%; height: 100%">
+      <el-table-column type="index"/>
+      <el-table-column prop="assignmentName" label="课程名称"/>
+      <el-table-column prop="description" label="描述"/>
+      <el-table-column prop="endTime" label="截止时间" sortable/>
+      <el-table-column prop="status" label="状态" sortable/>
+      <el-table-column>
+        <template #default="scope">
+          <el-button text @click="handleShowDetail(scope.row.assignmentId)" style="margin-right: 0; margin-left: 0;padding-left: 5px; padding-right: 5px">
+            <el-icon style="margin-right: 3px"><ZoomIn/></el-icon>
+            详情
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+import {Refresh, ZoomIn} from "@element-plus/icons-vue";
+import {onMounted, reactive, ref} from "vue";
+import useApis from "@/apis";
+import {useStore} from "@/store";
+import type {CourseVO, IAssignment} from "@/types/vo";
+import router from "@/router";
+
+const apis = useApis()
+const store = useStore()
+const role:'teachers' | 'student' = store.user.role === 'STUDENT' ?   'student':'teachers';
+
+let homeworkList = reactive<{
+  tableData: IAssignment[]
+}>({
+  tableData: []
+})
+
+const handleShowDetail = (assignmentId:number) => {
+  router.push(`/home/homeworkDetail/${assignmentId}`)
+
+}
+
+const fetchHomeworkListByCourseId = (courseId:number) => {
+  apis.getAssignmentByCourseId(courseId)
+      .then((res: any) => {
+        let data = res.data.data.records
+        for(let i = 0; i < data.length; i++) {
+          homeworkList.tableData.push(data[i])
+        }
+      })
+      .catch((err: any) => {
+        console.error(err);
+      })
+      .finally(() => {
+      });
+}
+
+const fetchHomeworkListByUserId = () => {
+  if (role === 'student') {
+    apis.getCourseByStudentId(store.user.id)
+        .then((res: any) => {
+          let data:CourseVO[] = res.data.data.records
+          for(let i = 0; i < data.length; i++){
+            fetchHomeworkListByCourseId(data[i].courseId)
+          }
+        })
+        .catch((err: any) => {
+          console.error(err);
+        })
+        .finally(() => {
+        });
+  } else if (role === "teachers") {
+    apis.getCourseByTeacherId(store.user.id)
+        .then((res: any) => {
+          let data:CourseVO[] = res.data.data.records
+          for(let i = 0; i < data.length; i++){
+            fetchHomeworkListByCourseId(data[i].courseId)
+          }
+        })
+        .catch((err: any) => {
+          console.error(err);
+        })
+        .finally(() => {
+        });
+  }
+}
+
+onMounted(() => {
+  fetchHomeworkListByUserId()
+  console.log(homeworkList)
+})
+
+// TODO:状态选择
+let state = ref(null)
+const stateOptions = [
+  {
+    value: 0,
+    label: "已提交"
+  },
+  {
+    value: 1,
+    label: "未提交"
+  },
+  {
+    value: 2,
+    label: "已截止"
+  },
+]
+</script>
+
+<script lang="ts">
+export default {
+  name: "HomeworkPage"
+}
+</script>

+ 48 - 0
src/views/Login.vue

@@ -0,0 +1,48 @@
+<!--
+  Description: 临时登录界面,已弃用
+  Author: BaiQi
+  Created Date: 2024-6-22
+-->
+<template>
+  <div>
+    <el-button type="primary" @click="handleLogin">登录</el-button>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import router from "@/router";
+import useApis from "@/apis";
+import {useStore} from "@/store";
+import {ref} from "vue";
+
+const apis = useApis()
+const store = useStore()
+const assignmentId = ref(1)
+
+const handleLogin = () => {
+  apis.login({
+    officialNumber: '181250126',
+    password: 'xiaosuniubi'
+  }).then((_res:any) => {
+    console.log(_res);
+    const data = _res.data.data
+    if (_res.data.code === 200) {
+      store.setUser(data);
+      store.setToken(data?.token)
+      localStorage.setItem('Authorization', data?.token);
+      localStorage.setItem('User', data);
+      router.push(`/assignment/${assignmentId.value}/edit`)
+    } else {
+      console.log("登陆失败")
+    }
+  }).catch((_err:any) => {
+    console.log("登陆失败1", _err)
+  })
+}
+</script>
+
+<script lang="ts">
+export default {
+  name: "Login"
+}
+</script>

+ 48 - 0
src/views/LoginPage/LoginPage.vue

@@ -0,0 +1,48 @@
+<!--
+  Description: 登录页面
+  Author: BaiQi
+  Created Date: 2024-6-28
+-->
+<template>
+  <div class="login-container">
+    <div class="container-box">
+      <div class="login-logo">
+        <div class="logo-image">
+          Logo
+        </div>
+      </div>
+      <div class="login-box">
+        <div class="box-content">
+          <div class="box-tabs">
+            <div
+              :class="{ 'box-tabs-item': true, 'active': isOfficialNumber }"
+              @click="isOfficialNumber = true">
+              账号密码
+            </div>
+            <div
+              :class="{ 'box-tabs-item': true, 'active': !isOfficialNumber }"
+              @click="isOfficialNumber = false">
+              邮箱验证码
+            </div>
+          </div>
+          <Login :isCode="!isOfficialNumber"/>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import "./index.scss"
+  import Login from "@/views/LoginPage/component/Login.vue";
+  import {ref} from 'vue'
+  
+  let isOfficialNumber = ref(true)
+  
+</script>
+
+<script lang="ts">
+export default {
+  name: "LoginPage"
+}
+</script>

+ 152 - 0
src/views/LoginPage/component/Login.vue

@@ -0,0 +1,152 @@
+<!--
+  Description: 登录组件,登录界面的登录输入框
+  Author: BaiQi
+  Created Date: 2024-6-28
+-->
+<template>
+  <div class="login-style">
+    <div class="box-vessel">
+      <div class="vessel-title">
+        {{ isCode ? '邮箱登录' : '账号登录' }}
+      </div>
+      <el-form
+          :model="data"
+          :rules="rules"
+          ref="form"
+          label-width="0"
+      >
+        <el-form-item :prop="isCode ? 'email' : 'officialNumber'">
+          <el-input
+              v-model="data.officialNumber"
+              :placeholder="isCode ? '请输入邮箱' : '请输入账号'"
+          />
+        </el-form-item>
+        <el-form-item :prop="isCode ? 'code' : 'password'">
+          <template v-if="isCode">
+            <el-input
+                v-model="data.code"
+                placeholder="请输入验证码"
+            >
+              <template #append>
+                <el-button
+                    :class="isStart? '' : 'code-button-active'"
+                    @click="getCode"
+                    :disabled="isStart"
+                >
+                  {{ isStart ? countdown : '获取验证码' }}
+                </el-button>
+              </template>
+            </el-input>
+          </template>
+          <template v-else>
+            <el-input
+                v-model="data.password"
+                placeholder="请输入密码"
+                show-password
+            />
+          </template>
+        </el-form-item>
+        <div class="forget">忘记密码?</div>
+        <el-form-item>
+          <el-button class="login-button" @click="handleLogin">用户登录</el-button>
+        </el-form-item>
+        <div class="sign" @click="onClickSign">去注册?</div>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { ref, reactive } from 'vue';
+  import { useStore } from '@/store';
+  import useApis from '@/apis';
+  import router from "@/router";
+  import { useCountdown } from '@/hooks/useCountdown';
+  import "./index.scss"
+  import { ElMessage } from 'element-plus';
+
+  interface LoginProps {
+    // 是否是手机号登录
+    isCode?: boolean
+    // 获取验证码
+    onCode?: () => void
+    // 提交
+    onSubmit?: (data: { password: string, username: string }) => void
+  }
+
+  const props = defineProps({
+    isCode: {
+      type: Boolean,
+      default: false,
+    },
+    onCode: {
+      type: Function,
+      default: null,
+    },
+    onSubmit: {
+      type: Function,
+      default: null,
+    },
+  });
+
+  const store = useStore();
+  const apis = useApis();
+  const { start, isStart, countdown } = useCountdown();
+
+  const data = reactive({
+    officialNumber: '',
+    password: '',
+    code: '',
+  });
+
+  const rules = reactive({
+    officialNumber: [
+      { required: true, message: props.isCode ? '邮箱不能为空' : '账号不能为空', trigger: 'blur' },
+    ],
+    password: [
+      { required: true, message: props.isCode ? '验证码不能为空' : '密码不能为空', trigger: 'blur' },
+    ],
+  });
+
+  // TODO:获取验证码方法未实现
+  const getCode = () => {
+    start();
+    props.onCode?.();
+    console.log('获取验证码');
+  };
+
+  let assignmentId = ref(1)
+  const handleLogin = () => {
+    apis.login({
+      officialNumber: data.officialNumber,
+      password: data.password
+    }).then((_res:any) => {
+      console.log(_res);
+      const data = _res.data.data
+      if (_res.data.code === 200) {
+        store.setUser(data);
+        store.setToken(data?.token)
+        localStorage.setItem('Authorization', data?.token);
+        localStorage.setItem('User', data);
+        // router.push("/")
+        router.push(`/home`)
+      } else {
+        console.log("登陆失败")
+      }
+    }).catch((_err:any) => {
+      console.log("登陆失败", _err)
+    })
+  };
+
+  const onClickSign = () => {
+    // router.push('/sign');
+    console.log("注册")
+  };
+
+</script>
+
+<script lang="ts">
+export default {
+  name: "Login"
+}
+</script>