feat):(course 实现课程创建功能及文件云组件

- 添加创建课程页面,支持章节与节的嵌套结构
- 实现可折叠章节组件(dragCollapse),支持展开/收起与删除操作
- 实现可拖拽表格组件(dragTable),支持跨表格拖拽排序与编辑
- 引入文件云API模块,支持文件夹与文件的基本操作
- 添加文件类型图标样式文件(filetypes.css)
- 新增文件选择弹窗组件(FileCloud),支持文件浏览与选择
- 优化common.scss样式文件,调整选择器缩进与渐变背景配置
This commit is contained in:
陈昱达
2025-11-19 15:46:20 +08:00
parent 2682f66111
commit 740ad58897
17 changed files with 2820 additions and 385 deletions

View File

@@ -0,0 +1,87 @@
import ajax from "./xajax";
/**
* @param {文件夹} folder
*/
const list = function (folder) {
return ajax.post("/systemapi/api/m/xfile/base/all/list", { folder });
};
const findByName = function (name) {
return ajax.post("/systemapi/api/m/xfile/base/all/find", { name });
};
/**
* 文件夹树
*/
const folderTree = function () {
return ajax.get("/systemapi/api/m/xfile/base/folder/tree");
};
/**
* 创建文件夹
* @param {*} data
*/
const folderCreate = function (data) {
return ajax.post("/systemapi/api/m/xfile/base/folder/create", data);
};
/**
* 重命名
* @param {*} id
* @param {*} name
*/
const folderRename = function (id, name) {
return ajax.post("/systemapi/api/m/xfile/base/folder/rename", { id, name });
};
/**
* 删除文件夹
* @param {*} id
*/
const folderDelete = function (id) {
return ajax.del("/systemapi/api/m/xfile/base/folder/delete?id=" + id);
};
/**
* 文章分页查询
* @param {*} data
*/
const filePageList = function (data) {
return ajax.post("/systemapi/api/m/xfile/base/file/pagelist", data);
};
const fileRename = function (id, name) {
return ajax.post("/systemapi/api/m/xfile/base/file/rename", { id, name });
};
const fileDelete = function (id, path) {
return ajax.post("/systemapi/api/m/xfile/base/file/delete", { id, path });
};
const fileMove = function (id, folderId) {
return ajax.post("/systemapi/api/m/xfile/base/file/rename", { id, folderId });
};
const fileDetail = function (id) {
return ajax.get("/systemapi/api/m/xfile/base/file/detail?id=" + id);
};
const fileSetDelete = function (id) {
// return ajax.del("/systemapi/api/m/xfile/base/file/setdelete?id=" + id);
};
export default {
list,
folderTree,
folderCreate,
folderRename,
folderDelete,
filePageList,
fileRename,
fileDelete,
fileMove,
fileDetail,
fileSetDelete,
findByName,
};

View File

@@ -0,0 +1,8 @@
import http from "./newConfig";
// 课程内容
export const getClassTree = function (sysResType) {
return http.get(
`/systemapi/xboe/type/tree-list?sysResType=${sysResType}&status=1`
);
};

View File

@@ -0,0 +1,107 @@
/*
* @Author: lixg lixg@dongwu-inc.com
* @Date: 2022-11-21 14:32:52
* @LastEditors: lixg lixg@dongwu-inc.com
* @LastEditTime: 2023-01-04 13:49:54
* @FilePath: /fe-manage/src/api/config.js
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { message } from "ant-design-vue";
import axios from "axios";
import router from "@/router";
import { REFRESH_TOKEN_API } from "@/api/ThirdApi";
import { boeRequest } from "@/api/request";
// import { getCookie } from '../api/method'
// const Qs = require("qs");
// axios.defaults.headers.post["Content-Type"] =
// "application/x-www-form-urlencoded";
export const FILE_UPLOAD_URL = process.env.VUE_APP_BASE_API + "/file/upload";
export const BATCH_IMPORT_SCORE =
process.env.VUE_APP_BASE_API + "/admin/offcourse/batchImportScore";
axios.defaults.withCredentials = true;
const http = axios.create({
// baseURL: process.env.VUE_APP_BASE_API,
timeout: 1000 * 15,
// headers: { "Content-Type": "multipart/form-data" },
headers: { "Content-Type": "application/json" },
});
http.interceptors.request.use(
(config) => {
// console.log("config", config);
// const token = localStorage.getItem("token");
// // const token = getCookie('token')
// // console.log('token', token)
// if (token) {
// config.headers.token = token; //测试1111
// } else {
// console.log("当前请求页面无token,请执行操作!!!");
// // 此处测试默认配置token
// config.headers.token =
// "eyJ0eXBlIjoidG9rZW4iLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC91LmJvZS5jb20iLCJpYXQiOjE2NzAxNTMxMDMsImV4cCI6MTY3MDE2MDMwMywiR2l2ZW5OYW1lIjoiYm9ldSIsInVzZXJJZCI6IjZCMDQ5RkFGLUMzMTQtN0NDRi0wRDI4LTBEMjNGNEM0MjUzMSIsInVJZCI6Ijk2NTM0MjAyNzQ5NzYwNzE2OCIsInBlcm1pc3Npb24iOiIifQ==.c937b2d3a59cbab2136fdde55fd38f06bdff041212aab0fa6741bc4be41e28a7";
// // }
return config;
},
(err) => {
console.log("登陆前拦截", err);
return Promise.reject(err);
}
);
http.interceptors.response.use(
(response) => {
// console.log('response', response)
let {
data: { code, msg, show, status },
} = response;
if (!code && status) {
code = status;
}
if (code === 0 || code === 200) {
return response.data;
}
if (code === 1000) {
window.location.href =
process.env.VUE_APP_LOGIN_URL +
encodeURIComponent(
window.location.protocol +
process.env.VUE_APP_BOE_API_URL +
process.env.VUE_APP_BASE +
router.currentRoute.value.fullPath
);
// TODO token过期后退出登录 清空当前用户标记 - 为了刷新页面使用
localStorage.removeItem("refreshPage");
return Promise.reject(response);
}
if (code === 1001) {
window.location.href =
process.env.VUE_APP_LOGIN_URL +
encodeURIComponent(
window.location.protocol +
process.env.VUE_APP_BOE_API_URL +
process.env.VUE_APP_BASE +
router.currentRoute.value.fullPath
);
return Promise.reject(response);
}
show ? message.error(msg) : message.error("系统接口数据异常,请联系管理员");
console.log("api %o", msg);
return Promise.reject(response);
},
function (error) {
if (error.message == "timeout of 1ms exceeded") {
message.destroy();
message.error("请求超时");
}
console.log("api error %o", error);
return Promise.reject(error);
}
);
export default http;
export function setHttpTimeout(newTimeout) {
http.defaults.timeout = newTimeout;
}

187
src/api/modules/xajax.js Normal file
View File

@@ -0,0 +1,187 @@
import axios from "axios";
import qs from "qs";
import { notification, Modal, message } from "ant-design-vue";
// 登录重定向 URL可根据环境变量配置
const ReLoginUrl = process.env.VUE_APP_LOGIN_URL || "/login";
const TokenName = "XBOE-Access-Token";
// JSON 请求实例Content-Type: application/json
const jsonRequest = axios.create({
headers: { "Content-Type": "application/json;charset=utf-8" },
// baseURL: process.env.VUE_APP_BASE_API,
timeout: 60000,
});
// 请求拦截器 - 不再携带 token你要求移除 getToken
jsonRequest.interceptors.request.use(
(config) => {
// ⚠️ 已移除 getToken 相关逻辑
// 如果后续需要手动加 token可在此处添加
// config.headers[TokenName] = 'your-token';
return config;
},
(error) => {
console.error("Request error:", error);
return Promise.reject(error);
}
);
// 响应拦截器
jsonRequest.interceptors.response.use(
(res) => {
const code = res.data.status || 200;
if (code === 200) {
return res.data;
} else {
if (code === 6001 || code === 401 || code === 402) {
Modal.warning({
title: "登录失效",
content: "您已被登出,可以取消继续留在该页面,或者重新登录",
okText: "重新登录",
onOk() {
window.location.href = ReLoginUrl;
},
});
} else if (code === 403) {
message.error("当前操作没有权限");
} else if (code === 302) {
window.location.href = ReLoginUrl;
} else {
// 其他业务错误,直接返回数据供调用方处理
return res.data; // 不 throw 错误,方便前端自定义处理
}
}
},
(error) => {
console.error("Response error:", error);
let msg = "未知错误,请稍后重试";
if (error.message === "Network Error") {
msg = "网络异常,请检查网络连接";
} else if (error.message.includes("timeout")) {
msg = "系统接口请求超时";
} else if (error.message.includes("Request failed with status code")) {
const statusCode = error.message.substr(-3);
msg = `系统接口 ${statusCode} 异常`;
if (statusCode === "500") {
notification.error({
message: "服务错误",
description: "服务器内部错误,请联系管理员。",
duration: 5,
});
}
}
message.error(msg, 5);
return Promise.reject(error);
}
);
// Form 请求实例x-www-form-urlencoded
const formRequest = axios.create({
headers: { "Content-Type": "application/x-www-form-urlencoded" },
// baseURL: process.env.VUE_APP_BASE_API,
timeout: 10000,
});
// 请求拦截器form
formRequest.interceptors.request.use(
(config) => {
// 同样不带 token
return config;
},
(error) => {
console.error("Form request error:", error);
return Promise.reject(error);
}
);
// 响应拦截器form
formRequest.interceptors.response.use(
(res) => {
const code = res.data.status || 200;
if (code === 200) {
return res.data;
} else {
if (code === 6001 || code === 401 || code === 402) {
Modal.warning({
title: "登录已过期",
content: "登录状态无效,即将跳转至登录页",
okText: "确认",
onOk() {
window.location.href = ReLoginUrl;
},
});
} else if (code === 403) {
message.error("暂无权限执行此操作");
} else if (code === 302) {
window.location.href = ReLoginUrl;
} else {
return res.data; // 返回原始数据供业务判断
}
}
},
(error) => {
console.error("Form response error:", error);
let msg = "请求失败";
if (error.message === "Network Error") {
msg = "网络连接失败";
} else if (error.message.includes("timeout")) {
msg = "请求超时";
} else if (error.message.includes("Request failed with status code")) {
msg = `服务端异常 (${error.message.slice(-3)})`;
}
message.error(msg, 5);
return Promise.reject(error);
}
);
// ================== API 方法封装 ==================
const requestJson = jsonRequest.request;
const get = (url, params, config) => {
const finalConfig = { ...config, params };
return formRequest.get(url, finalConfig);
};
const post = (url, data, config) => {
return formRequest.post(url, qs.stringify(data), config);
};
const postForm = (url, data, config) => {
return formRequest.post(url, data, config); // 不 stringify用于上传文件等
};
const postJson = jsonRequest.post;
const put = (url, data, config) => {
return formRequest.put(url, qs.stringify(data), config);
};
const putJson = jsonRequest.put;
const patch = (url, data, config) => {
return formRequest.patch(url, qs.stringify(data), config);
};
const patchJson = jsonRequest.patch;
const del = (url, config) => {
return formRequest.delete(url, config);
};
// 导出统一接口
export default {
request: jsonRequest.request, // 通用 request 方法
requestJson,
get,
post,
postForm,
postJson,
put,
putJson,
patch,
patchJson,
del,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -299,8 +299,8 @@ textarea {
display: inline-block;
.ant-select:not(.ant-select-customize-input)
.ant-select-selector
.ant-select-selection-search-input {
.ant-select-selector
.ant-select-selection-search-input {
background-color: rgba(255, 255, 255, 0);
border: none;
}
@@ -317,7 +317,7 @@ textarea {
.ant-select-focused:not(.ant-select-disabled).ant-select:not(
.ant-select-customize-input
)
.ant-select-selector {
.ant-select-selector {
box-shadow: none;
}
@@ -561,8 +561,8 @@ textarea {
}
.ant-table-tbody
> tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)
> td {
> tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)
> td {
background: #f6f9fd;
}
@@ -678,9 +678,9 @@ textarea {
width: 100%;
height: 68px;
background: linear-gradient(
0deg,
rgba(78, 166, 255, 0) 0%,
rgba(78, 166, 255, 0.2) 100%
0deg,
rgba(78, 166, 255, 0) 0%,
rgba(78, 166, 255, 0.2) 100%
);
}
@@ -697,25 +697,25 @@ textarea {
.ant-modal {
.modalHeader {
background: linear-gradient(
180deg,
rgba(103, 64, 255, 0.2) 0%,
rgba(166, 168, 255, 0) 100%
180deg,
rgba(103, 64, 255, 0.2) 0%,
rgba(166, 168, 255, 0) 100%
) !important;
}
.top {
background: linear-gradient(
180deg,
rgba(103, 64, 255, 0.2) 0%,
rgba(166, 168, 255, 0) 100%
180deg,
rgba(103, 64, 255, 0.2) 0%,
rgba(166, 168, 255, 0) 100%
) !important;
}
.del_header {
background: linear-gradient(
180deg,
rgba(103, 64, 255, 0.2) 0%,
rgba(166, 168, 255, 0) 100%
180deg,
rgba(103, 64, 255, 0.2) 0%,
rgba(166, 168, 255, 0) 100%
) !important;
}
}
@@ -736,4 +736,33 @@ textarea {
z-index: 100;
}
//loading--------------------------------------------------------
.el-select,
.el-cascader {
width: 100%;
}
////loading--------------------------------------------------------
//
//.default-form {
// .ant-form-item {
// .ant-form-item-label {
// line-height: 40px;
// }
// .ant-form-item-control {
// .ant-input,
// .ant-select-selector {
// width: 100%;
// line-height: 40px;
//
// height: 40px;
// //border-radius: 8px;
// }
// .ant-form-item-control-input-content {
// line-height: 40px;
// }
// .ant-checkbox-group,
// .ant-radio-group {
// line-height: 40px;
// }
// }
// }
//}

View File

@@ -0,0 +1,280 @@
.ft {
display: block;
width: 64px;
height: 64px;
background: url(../images/filetypes.png) no-repeat 0 0;
}
.ft_small {
display: block;
width: 16px;
height: 16px;
float: left;
background: url(../images/filetypes.png) no-repeat 0 0;
margin-right: 3px;
}
.ft_null {
background-position: 0 0px;
}
.ft_small_null {
background-position: -164px -48px;
}
.ft_folder {
background-position: 0 -124px;
}
.ft_small_folder {
background-position: -164px -172px;
}
.ft_ai {
background-position: 0 -248px;
}
.ft_small_ai {
background-position: -164px -296px;
}
.ft_aif {
background-position: 0 -372px;
}
.ft_small_aif {
background-position: -164px -420px;
}
.ft_aiff {
background-position: 0 -496px;
}
.ft_small_aiff {
background-position: -164px -544px;
}
.ft_asp {
background-position: 0 -620px;
}
.ft_small_asp {
background-position: -164px -668px;
}
.ft_apk {
background-position: 0 -744px;
}
.ft_small_apk {
background-position: -164px -792px;
}
.ft_avi {
background-position: 0 -868px;
}
.ft_small_avi {
background-position: -164px -916px;
}
.ft_bmp {
background-position: 0 -992px;
}
.ft_small_bmp {
background-position: -164px -1040px;
}
.ft_doc {
background-position: 0 -1116px;
}
.ft_small_doc {
background-position: -164px -1164px;
}
.ft_docx {
background-position: 0 -1116px;
}
.ft_small_docx {
background-position: -164px -1164px;
}
.ft_dvd {
background-position: 0 -1240px;
}
.ft_small_dvd {
background-position: -164px -1288px;
}
.ft_dwf {
background-position: 0 -1364px;
}
.ft_small_dwf {
background-position: -164px -1412px;
}
.ft_exe {
background-position: 0 -1488px;
}
.ft_small_exe {
background-position: -164px -1536px;
}
.ft_fla {
background-position: 0 -1612px;
}
.ft_small_fla {
background-position: -164px -1660px;
}
.ft_gif {
background-position: 0 -1736px;
}
.ft_small_gif {
background-position: -164px -1784px;
}
.ft_htc {
background-position: 0 -1860px;
}
.ft_small_htc {
background-position: -164px -1908px;
}
.ft_html {
background-position: 0 -1984px;
}
.ft_small_html {
background-position: -164px -2032px;
}
.ft_htm {
background-position: 0 -1984px;
}
.ft_small_htm {
background-position: -164px -2032px;
}
.ft_ics {
background-position: 0 -2108px;
}
.ft_small_ics {
background-position: -164px -2156px;
}
.ft_ico {
background-position: 0 -2232px;
}
.ft_small_ico {
background-position: -164px -2280px;
}
.ft_java {
background-position: 0 -2357px;
}
.ft_small_java {
background-position: -164px -2404px;
}
.ft_jpg {
background-position: 0 -2480px;
}
.ft_small_jpg {
background-position: -164px -2528px;
}
.ft_png {
background-position: 0 -2480px;
}
.ft_small_png {
background-position: -164px -2528px;
}
.ft_jsp {
background-position: 0 -2604px;
}
.ft_small_jsp {
background-position: -164px -2652px;
}
.ft_mov {
background-position: 0 -2728px;
}
.ft_small_mov {
background-position: -164px -2776px;
}
.ft_mp3 {
background-position: 0 -2852px;
}
.ft_small_mp3 {
background-position: -164px -2900px;
}
.ft_mp4 {
background-position: 0 -2976px;
}
.ft_small_mp4 {
background-position: -164px -3024px;
}
.ft_pdf {
background-position: 0 -3100px;
}
.ft_small_pdf {
background-position: -164px -3148px;
}
.ft_ppt {
background-position: 0 -3224px;
}
.ft_small_ppt {
background-position: -164px -3272px;
}
.ft_pptx {
background-position: 0 -3224px;
}
.ft_small_pptx {
background-position: -164px -3272px;
}
.ft_psd {
background-position: 0 -3348px;
}
.ft_small_psd {
background-position: -164px -3396px;
}
.ft_rm {
background-position: 0 -3472px;
}
.ft_small_rm {
background-position: -164px -3520px;
}
.ft_rif {
background-position: 0 -3596px;
}
.ft_small_rif {
background-position: -164px -3644px;
}
.ft_rar {
background-position: 0 -3719px;
}
.ft_small_rar {
background-position: -164px -3768px;
}
.ft_swf {
background-position: 0 -3844px;
}
.ft_small_swf {
background-position: -164px -3892px;
}
.ft_tif {
background-position: 0 -3968px;
}
.ft_small_tif {
background-position: -164px -4016px;
}
.ft_txt {
background-position: 0 -4092px;
}
.ft_small_txt {
background-position: -164px -4140px;
}
.ft_wma {
background-position: 0 -4216px;
}
.ft_small_wma {
background-position: -164px -4264px;
}
.ft_wri {
background-position: 0 -4339px;
}
.ft_small_wri {
background-position: -164px -4387px;
}
.ft_xls {
background-position: 0 -4464px;
}
.ft_small_xls {
background-position: -164px -4512px;
}
.ft_xlsx {
background-position: 0 -4464px;
}
.ft_small_xlsx {
background-position: -164px -4512px;
}
.ft_xsl {
background-position: 0 -4587px;
}
.ft_small_xsl {
background-position: -164px -4635px;
}
.ft_zip {
background-position: 0 -4712px;
}
.ft_small_zip {
background-position: -164px -4760px;
}

View File

@@ -0,0 +1,301 @@
<template>
<div style="min-width: 700px">
<a-modal
title="选择图片"
:visible="show"
width="60%"
:footer="null"
:mask-closable="false"
@cancel="chose"
>
<!-- Header with breadcrumb and search -->
<div>
<div
style="
display: flex;
justify-content: space-between;
padding: 5px;
border-bottom: 1px solid #e0e0e0;
"
>
<div style="padding-top: 10px">
<a-breadcrumb>
<a-breadcrumb-item>
<a @click.stop="toRootPath">根目录</a>
</a-breadcrumb-item>
<a-breadcrumb-item
v-for="(fpath, fidx) in folderPath"
:key="fidx"
>
<a @click.stop="toPath(fpath)">{{ fpath.name }}</a>
</a-breadcrumb-item>
</a-breadcrumb>
</div>
<div>
<a-input-search
placeholder="文件名"
style="width: 200px"
v-model:value="findName"
@search="findByName"
/>
</div>
</div>
<!-- File/Folder List -->
<div
style="
margin-top: 5px;
height: 500px;
overflow: auto;
position: relative;
"
>
<div
class="fitem"
:class="item.id === sub.id ? 'fitem_tips' : ''"
v-for="(item, idx) in folderData"
:key="idx"
>
<!-- Image Item -->
<div
v-if="!item.folder"
class="fitem-image"
@click.stop="chooseItem(item)"
>
<img
:style="{ 'object-fit': 'scale-down' }"
class="fitem-image"
:src="fileBasePath + item.path"
alt=""
/>
<div class="fitem-buttons" v-if="itemChecked.id === item.id">
<CheckCircleOutlined
style="font-size: 30px; cursor: pointer; color: #0055ff"
/>
</div>
</div>
<!-- Folder Item -->
<div
v-else
class="fitem-image"
style="padding-left: 50px"
@click.stop="clickFolder(item)"
>
<i
class="ft"
:class="[item.icon === '' ? 'ft_null' : 'ft_' + item.icon]"
></i>
</div>
<!-- Filename -->
<div class="fitem-name">
<span :title="item.name">{{ item.name }}</span>
</div>
</div>
</div>
</div>
<!-- Footer Buttons -->
<div class="dialog-footer" style="text-align: right; padding: 10px">
<a-button
type="primary"
:disabled="btnDisabled"
@click="saveChoose"
style="margin-right: 8px"
>
确认
</a-button>
<a-button @click="chose">取消</a-button>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from "vue";
import apiFilecloud from "@/api/modules/filecloud";
import { CheckCircleOutlined } from "@ant-design/icons-vue";
import { message } from "ant-design-vue";
// Props
const props = defineProps({
show: {
type: Boolean,
default: false,
},
});
// Emit events
const emit = defineEmits(["close", "choose"]);
// State
const findName = ref("");
const btnDisabled = ref(true);
const folderPath = ref([]);
const folderData = ref([]);
const loading = ref(false);
const fileBasePath = ref(process.env.VUE_APP_FILE_BASE_URL || "/files/");
const itemChecked = ref({});
const sub = ref({});
// Computed: 当前文件夹 ID
const getCurrentFloderId = () => {
const len = folderPath.value.length;
return len > 0 ? folderPath.value[len - 1].id : "0";
};
// Methods
const loadFolderData = () => {
if (loading.value) return;
loading.value = true;
const folderId = getCurrentFloderId();
if (folderId === "-1") {
loading.value = false;
return;
}
apiFilecloud
.list(folderId)
.then((rs) => {
if (rs.status === 200) {
console.log(rs, 32);
folderData.value = rs.result;
} else {
message.error(rs.message);
}
})
.catch((err) => {
message.error("加载失败:" + err.message);
})
.finally(() => {
loading.value = false;
});
};
const findByName = () => {
if (!findName.value.trim()) return;
apiFilecloud
.findByName(findName.value)
.then((rs) => {
if (rs.status === 200) {
folderPath.value = [
{
id: "-1",
name: `${findName.value} 搜索结果(${rs.result.length}`,
},
];
folderData.value = rs.result;
} else {
message.error(rs.message);
}
})
.catch(() => {
message.error("搜索失败");
});
};
const toRootPath = () => {
if (folderPath.value.length > 0) {
folderPath.value = [];
loadFolderData();
}
};
const toPath = (pathInfo) => {
const newPaths = [];
for (const p of folderPath.value) {
newPaths.push(p);
if (p.id === pathInfo.id) break;
}
folderPath.value = newPaths;
loadFolderData();
};
const clickFolder = (row) => {
sub.value = row;
if (loading.value) return;
const pathLen = folderPath.value.length;
if (pathLen === 0 || folderPath.value[pathLen - 1].id !== row.id) {
folderPath.value.push({
id: row.id,
name: row.name,
});
}
loadFolderData();
};
const chooseItem = (item) => {
itemChecked.value = item;
btnDisabled.value = false;
};
const chose = () => {
emit("close");
};
const saveChoose = () => {
if (!itemChecked.value.id) return;
emit("choose", itemChecked.value);
// Reset selection after choose
itemChecked.value = {};
};
onMounted(() => {
loadFolderData();
});
</script>
<script>
// 引入样式(保持原样)
export default {
name: "ImageSelectorModal",
};
</script>
<style lang="scss" scoped>
@import url("../../assets/scss/filetypes.css");
.fitem {
text-align: center;
position: relative;
padding: 5px;
width: 170px;
height: 130px;
display: inline-block;
margin: 5px;
.fitem-image {
width: 160px;
height: 90px;
line-height: 90px;
.fitem-buttons {
position: absolute;
height: 45px;
display: block;
top: 0px;
left: 0px;
line-height: 45px;
text-align: center;
width: 100%;
}
}
.fitem-name {
text-align: center;
padding: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.fitem_tips {
border: 1px solid #dadada;
background-color: #edf6ff;
}
.fitem:hover {
outline: 1px solid #dadada;
background-color: #edf6ff;
}
</style>

142
src/hooks/useCourseData.js Normal file
View File

@@ -0,0 +1,142 @@
import { ref, reactive } from "vue";
import { message } from "ant-design-vue";
/**
* 课程数据管理hook
* @returns
*/
export function useCourseData() {
// 课程元数据
const courseMetadata = reactive({
courseName: "",
createTime: "",
});
// 课程列表数据
const courseList = ref([
{
title: "课程1",
data: [
{ key: "1-1", name: "视频课件名称", type: "视频" },
{ key: "1-2", name: "音频课件名称", type: "音频" },
{ key: "1-3", name: "文档课件名称", type: "文档" },
],
},
{
title: "课程2",
data: [
{ key: "2-1", name: "图文课件名称", type: "图文" },
{ key: "2-2", name: "外部链接", type: "链接" },
],
},
{
title: "课程3",
data: [
{ key: "3-1", name: "SCORM", type: "SCORM" },
{ key: "3-2", name: "考试名称", type: "考试" },
{ key: "3-3", name: "自定义考试名称", type: "考试" },
{ key: "3-4", name: "作业名称", type: "作业" },
],
},
]);
// 课程操作映射
const courseOperations = {
addVideo: (index) => {
message.error("功能开发中");
console.log("添加视频功能调用,索引:", index);
if (index !== undefined && courseList.value[index]) {
courseList.value[index].title = "课程4";
}
},
addAudio: () => {
console.log("添加音频功能调用");
},
addDocument: () => {
console.log("添加文档功能调用");
},
addImageText: () => {
console.log("添加图文功能调用");
},
addExternalLink: () => {
console.log("添加外部链接功能调用");
},
addScorm: () => {
console.log("添加SCORM功能调用");
},
addExam: () => {
console.log("添加考试功能调用");
},
addHomework: () => {
console.log("添加作业功能调用");
},
addAssessment: () => {
console.log("添加评估功能调用");
},
};
// 执行课程操作
const executeCourseOperation = (operationName, data) => {
if (courseOperations[operationName]) {
courseOperations[operationName](data);
} else {
console.warn(`未找到操作: ${operationName}`);
}
};
// 课程操作按钮
const courseActionButtons = [
{
label: "添加视频",
icon: "",
fun: "addVideo",
},
{
label: "添加音频",
icon: "",
fun: "addAudio",
},
{
label: "添加文档",
icon: "",
fun: "addDocument",
},
{
label: "添加图文",
icon: "",
fun: "addImageText",
},
{
label: "外部链接",
icon: "",
fun: "addExternalLink",
},
{
label: "SCORM",
icon: "",
fun: "addScorm",
},
{
label: "添加考试",
icon: "",
fun: "addExam",
},
{
label: "添加作业",
icon: "",
fun: "addHomework",
},
{
label: "添加评估",
icon: "",
fun: "addAssessment",
},
];
return {
courseMetadata,
courseList,
courseActionButtons,
executeCourseOperation
};
}

View File

@@ -0,0 +1,50 @@
import { reactive, ref } from "vue";
/**
* 课程表单相关hook
* @returns
*/
export function useCourseForm() {
// 表单相关
const formRef = ref();
const formState = reactive({
courseName: "", // 课程名称
courseCategory: [], // 课程分类
resourceBelong: undefined, // 资源归属
lecturer: undefined, // 授课教师
targetGroup: "", // 目标人群
courseTags: [], // 课程标签
audience: undefined, // 受众
visibility: "Apple", // 可见性
coverIntro: "", // 封面介绍
courseValue: "", // 课程价值
courseIntro: "", // 课程简介
});
// 可见性选项
const visibilityOptions = [
{ label: "PC端可见", value: "Apple" },
{ label: "移动端可见", value: "Pear" },
{ label: "多端可见", value: "Orange", disabled: false },
];
// 表单重置
const resetForm = (courseCoverurl, fileList) => {
if (formRef.value) {
formRef.value.resetFields();
}
if (courseCoverurl) {
courseCoverurl.value = "";
}
if (fileList) {
fileList.value = [];
}
};
return {
formRef,
formState,
visibilityOptions,
resetForm
};
}

65
src/hooks/useUpload.js Normal file
View File

@@ -0,0 +1,65 @@
import { ref } from "vue";
import { ElMessage } from "element-plus";
import { Loading, Plus } from "@element-plus/icons-vue";
/**
* 文件上传相关hook
* @returns
*/
export function useUpload() {
// 上传相关
const fileList = ref([]);
const loading = ref(false);
const courseCoverurl = ref("");
// 获取图片base64
const getBase64 = (img, callback) => {
const reader = new FileReader();
reader.addEventListener("load", () => callback(reader.result));
reader.readAsDataURL(img);
};
// 上传状态变化处理
const handleChange = (info) => {
if (info.file.status === "uploading") {
loading.value = true;
return;
}
if (info.file.status === "done") {
// Get this url from response in real world.
getBase64(info.file.originFileObj, (base64Url) => {
courseCoverurl.value = base64Url;
loading.value = false;
});
}
if (info.file.status === "error") {
loading.value = false;
ElMessage.error("upload error");
}
};
// 上传前检查
const beforeUpload = (file) => {
const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png";
if (!isJpgOrPng) {
ElMessage.error("You can only upload JPG file!");
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
ElMessage.error("Image must smaller than 2MB!");
}
return isJpgOrPng && isLt2M;
};
return {
// 数据
fileList,
loading,
courseCoverurl,
// 方法
getBase64,
handleChange,
beforeUpload
};
}

View File

@@ -6,28 +6,33 @@
* @FilePath: /fe-manage/src/main.js
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import { message } from "ant-design-vue";
// import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import "element-plus/dist/index.css";
// import zhCn from 'element-plus/es/locale/lang/zh-cn'
import "@/assets/scss/common.scss"
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
import {request} from "@/api/request";
import {USER_INFO, USER_PERMISSION, VALIDATE_TOKEN} from "@/api/apis";
import "@/assets/scss/common.scss";
import Antd from "ant-design-vue";
import "ant-design-vue/dist/antd.css";
import { request } from "@/api/request";
import { USER_INFO, USER_PERMISSION, VALIDATE_TOKEN } from "@/api/apis";
import * as api1 from "@/api/index1";
import {getCookieForName} from "@/api/method";
import components from './components'
import axios from 'axios'
import { getCookieForName } from "@/api/method";
import components from "./components";
import axios from "axios";
import Cookies from "vue-cookies";
// axios.defaults.withCredentials = true;
// import zhCN from 'ant-design-vue/es/locale/zh_CN';
const app = createApp(App)
const app = createApp(App);
//全局注册
app.use(components)
app.use(components);
message.config({
top: "250px",
});
// 清理控制台warn信息
app.config.warnHandler = () => null;
// app.use(ElementPlus, {
@@ -35,102 +40,112 @@ app.config.warnHandler = () => null;
// })
router.beforeEach(async (to, from, next) => {
if (!getCookieForName("token")) {
window.location.href = process.env.VUE_APP_LOGIN_URL + encodeURIComponent(window.location.protocol + process.env.VUE_APP_BOE_API_URL + process.env.VUE_APP_BASE + router.currentRoute.value.fullPath)
return
if (!getCookieForName("token")) {
window.location.href =
process.env.VUE_APP_LOGIN_URL +
encodeURIComponent(
window.location.protocol +
process.env.VUE_APP_BOE_API_URL +
process.env.VUE_APP_BASE +
router.currentRoute.value.fullPath
);
return;
}
//第一次进入 没有用户信息
if (!store.state.userInfo.userId) {
try {
await request(VALIDATE_TOKEN);
await getUserInfo();
await getUserPermission();
init();
} catch (e) {
console.log("token失效 跳转到登录页");
}
//第一次进入 没有用户信息
if(!store.state.userInfo.userId){
try{
await request(VALIDATE_TOKEN)
await getUserInfo()
await getUserPermission();
init()
}catch (e){
console.log('token失效 跳转到登录页')
}
}
next();
})
}
next();
});
app.use(Antd);
app.use(router);
app.use(store);
app.mount('#app');
app.mount("#app");
async function getUserPermission() {
return request(USER_PERMISSION, {permissionType: 'PAGE'}).then(res => {
store.commit("SET_PERMISSION", res.data?.map(s => s.url));
})
return request(USER_PERMISSION, { permissionType: "PAGE" }).then((res) => {
store.commit(
"SET_PERMISSION",
res.data?.map((s) => s.url)
);
});
}
async function getUserInfo() {
const userInfo = await request(USER_INFO);
store.commit("SET_USER", userInfo.data);
axios({
method: "get",
url: "/userbasic/orgHrbp/reportOrgs",
params: {
workNum:userInfo.data.userNo
},
headers: {
"XBOR-Access-token": Cookies.get("token"),
},
}).then(res=>{
store.commit("SET_USER_ORGS", res.data);
})
const userInfo = await request(USER_INFO);
store.commit("SET_USER", userInfo.data);
axios({
method: "get",
url: "/userbasic/orgHrbp/reportOrgs",
params: {
workNum: userInfo.data.userNo,
},
headers: {
"XBOR-Access-token": Cookies.get("token"),
},
}).then((res) => {
store.commit("SET_USER_ORGS", res.data);
});
}
async function initDict(key) {
const list = await getDictList(key);
store.commit("SET_DICT", {key, data: list});
const list = await getDictList(key);
store.commit("SET_DICT", { key, data: list });
}
const getDictList = (param) => api1.getDictTree({code: param,}).then((res) => res.data.data);
const getDictList = (param) =>
api1.getDictTree({ code: param }).then((res) => res.data.data);
const initDictTree = (key) => {
axios({
method: "get",
url: "/systemapi/xboe/type/tree-list",
params: {
sysResType: "1",
status: "1",
},
headers: {
"XBOR-Access-token": Cookies.get("token"),
},
}).then(
(res) => {
console.log(res.data.result,'课程分类接口')
store.commit("SET_DICT", {key, data: res.data.result});
//转化为map放到状态中
let map=new Map();
res.data.result.forEach(item=>{
map.set(item.id, item.name);
if(item.children && item.children!=''){
item.children.forEach(child=>{
map.set(child.id, child.name);
if(child.children && child.children!=''){
child.children.forEach(last=>{
map.set(last.id, last.name);
})
}
})
}
axios({
method: "get",
url: "/systemapi/xboe/type/tree-list",
params: {
sysResType: "1",
status: "1",
},
headers: {
"XBOR-Access-token": Cookies.get("token"),
},
}).then(
(res) => {
console.log(res.data.result, "课程分类接口");
store.commit("SET_DICT", { key, data: res.data.result });
//转化为map放到状态中
let map = new Map();
res.data.result.forEach((item) => {
map.set(item.id, item.name);
if (item.children && item.children != "") {
item.children.forEach((child) => {
map.set(child.id, child.name);
if (child.children && child.children != "") {
child.children.forEach((last) => {
map.set(last.id, last.name);
});
}
});
store.commit("SET_SYSTYPEMAP", map);
},
(err) => {
message.error(err);
}
);
}
});
store.commit("SET_SYSTYPEMAP", map);
},
(err) => {
message.error(err);
}
);
};
async function init() {
// initDict("content_type"); //内容分类
initDictTree("content_type"); //内容分类换成type/tree-list接口
initDict("project_level"); //项目级别
initDict("project_sys"); //培训分类
initDict("project_pic"); //项目封面
initDict("router_pic"); //路径图封面
initDict("course_pic"); //课程封面
initDict("job_type"); //岗位
initDict("band"); //band
initDict("examine_cover") //讲师认证封面图
initDict("project_number") //项目编号
}
// initDict("content_type"); //内容分类
initDictTree("content_type"); //内容分类换成type/tree-list接口
initDict("project_level"); //项目级别
initDict("project_sys"); //培训分类
initDict("project_pic"); //项目封面
initDict("router_pic"); //路径图封面
initDict("course_pic"); //课程封面
initDict("job_type"); //岗位
initDict("band"); //band
initDict("examine_cover"); //讲师认证封面图
initDict("project_number"); //项目编号
}

View File

@@ -7,118 +7,122 @@
<div class="filterItems">
<div class="select">
<a-input
v-model:value="searchParam.name"
style="width: 200px; height: 40px; border-radius: 8px"
placeholder="请输入课程名称"
allowClear
showSearch
v-model:value="searchParam.name"
style="width: 200px; height: 40px; border-radius: 8px"
placeholder="请输入课程名称"
allowClear
showSearch
>
</a-input>
</div>
<div class="select">
<a-select
v-model:value="searchParam.category"
style="width: 200px"
placeholder="全部课程分类"
:options="categoryList"
allowClear
v-model:value="searchParam.category"
style="width: 200px"
placeholder="全部课程分类"
:options="categoryList"
allowClear
></a-select>
</div>
<div class="select">
<a-input
v-model:value="searchParam.teacher"
style="width: 200px; height: 40px; border-radius: 8px"
placeholder="授课教师"
allowClear
showSearch
v-model:value="searchParam.teacher"
style="width: 200px; height: 40px; border-radius: 8px"
placeholder="授课教师"
allowClear
showSearch
>
</a-input>
</div>
<div class="select addTimeBox">
<div class="addTime">培训时间</div>
<a-range-picker
v-model:value="searchParam.valueDate"
style="width:330px"
format="YYYY-MM-DD"
separator="至"
:placeholder="[' 开始时间', ' 结束时间']"
v-model:value="searchParam.valueDate"
style="width: 330px"
format="YYYY-MM-DD"
separator="至"
:placeholder="[' 开始时间', ' 结束时间']"
/>
</div>
<div class="select">
<a-select
v-model:value="searchParam.reviewStatus"
style="width: 200px"
placeholder="全部审核状态"
:options="reviewStatusList"
allowClear
v-model:value="searchParam.reviewStatus"
style="width: 200px"
placeholder="全部审核状态"
:options="reviewStatusList"
allowClear
></a-select>
</div>
<div class="select">
<a-select
v-model:value="searchParam.publishStatus"
style="width: 200px"
placeholder="全部发布状态"
:options="publishStatusList"
allowClear
v-model:value="searchParam.publishStatus"
style="width: 200px"
placeholder="全部发布状态"
:options="publishStatusList"
allowClear
></a-select>
</div>
<div class="select" style="display: flex; margin-bottom: 20px">
<div class="btn btn4" @click="toggleFilter">
<div class="search"></div>
<div class="btnText">{{ showSecondFilter ? '收起' : '展开' }} </div>
<div class="btnText">
{{ showSecondFilter ? "收起" : "展开" }}
</div>
</div>
</div>
</div>
<!-- 第二排搜索项 (通过 showSecondFilter 控制显隐) -->
<div class="filterItems" style="justify-content: space-between;width: 100%">
<div
class="filterItems"
style="justify-content: space-between; width: 100%"
>
<div class="filterItems">
<div class="select" v-show="showSecondFilter">
<div class="select" v-show="showSecondFilter">
<a-select
v-model:value="searchParam.enableStatus"
style="width: 200px"
placeholder="全部启用状态"
:options="enableStatusList"
allowClear
v-model:value="searchParam.enableStatus"
style="width: 200px"
placeholder="全部启用状态"
:options="enableStatusList"
allowClear
></a-select>
</div>
<div class="select" v-show="showSecondFilter" >
<div class="select" v-show="showSecondFilter">
<a-select
v-model:value="searchParam.isPublic"
style="width: 200px"
placeholder="是否公开课"
:options="isPublicList"
allowClear
v-model:value="searchParam.isPublic"
style="width: 200px"
placeholder="是否公开课"
:options="isPublicList"
allowClear
></a-select>
</div>
<div class="select" v-show="showSecondFilter" >
<div class="select" v-show="showSecondFilter">
<a-select
v-model:value="searchParam.resourceOwner"
style="width: 200px"
placeholder="全部资源归属"
:options="resourceOwnerList"
allowClear
v-model:value="searchParam.resourceOwner"
style="width: 200px"
placeholder="全部资源归属"
:options="resourceOwnerList"
allowClear
></a-select>
</div>
<div class="select" v-show="showSecondFilter">
<a-input
v-model:value="searchParam.teacher"
style="width: 200px; height: 40px; border-radius: 8px"
placeholder="创建人"
allowClear
showSearch
v-model:value="searchParam.teacher"
style="width: 200px; height: 40px; border-radius: 8px"
placeholder="创建人"
allowClear
showSearch
>
</a-input>
</div>
<div class="select" v-show="showSecondFilter">
<div class="select" v-show="showSecondFilter">
<a-select
v-model:value="searchParam.createSource"
style="width: 200px"
placeholder="全部创建来源"
:options="createSourceList"
allowClear
v-model:value="searchParam.createSource"
style="width: 200px"
placeholder="全部创建来源"
:options="createSourceList"
allowClear
></a-select>
</div>
</div>
@@ -163,22 +167,28 @@
<!-- 表格 -->
<div style="padding: 10px 35px">
<a-table
:header-cell-style="{ 'text-align': 'center' }"
style="border: 1px solid #f2f6fe"
:columns="columns"
:data-source="tableData"
:loading="tableLoading"
:scroll="{ x: 'max-content' }"
:pagination="false"
:expandable="false"
:header-cell-style="{ 'text-align': 'center' }"
style="border: 1px solid #f2f6fe"
:columns="columns"
:data-source="tableData"
:loading="tableLoading"
:scroll="{ x: 'max-content' }"
:pagination="false"
:expandable="false"
>
<template #bodyCell="{ record, column }">
<template v-if="column.key === 'operation'">
<a-space>
<a-button @click="handleEdit(record)" type="link">编辑</a-button>
<a-button @click="editMaterial(record)" type="link">编辑课件</a-button>
<a-button @click="grantPermission(record)" type="link">授权</a-button>
<a-button @click="deleteRecord(record)" type="link" danger>删除</a-button>
<a-button @click="editMaterial(record)" type="link"
>编辑课件</a-button
>
<a-button @click="grantPermission(record)" type="link"
>授权</a-button
>
<a-button @click="deleteRecord(record)" type="link" danger
>删除</a-button
>
</a-space>
</template>
</template>
@@ -186,15 +196,15 @@
<div class="tableBox">
<div class="pa">
<a-pagination
v-if="tableDataTotal > 10"
:showSizeChanger="false"
:showQuickJumper="true"
:hideOnSinglePage="true"
:pageSize="pageSize"
v-model:current="searchParam.pageNo"
:total="tableDataTotal"
class="pagination"
@change="changePagination"
v-if="tableDataTotal > 10"
:showSizeChanger="false"
:showQuickJumper="true"
:hideOnSinglePage="true"
:pageSize="pageSize"
v-model:current="searchParam.pageNo"
:total="tableDataTotal"
class="pagination"
@change="changePagination"
/>
</div>
</div>
@@ -211,330 +221,330 @@ export default {
// 控制第二排搜索项的显隐
showSecondFilter: false,
searchParam: {
name: '',
name: "",
category: undefined,
teacher: '',
teacher: "",
valueDate: [],
reviewStatus: undefined,
publishStatus: undefined,
enableStatus: undefined, // 新增:启用状态
isPublic: undefined, // 新增:是否公开课
isPublic: undefined, // 新增:是否公开课
resourceOwner: undefined, // 新增:资源归属
createSource: undefined, // 新增:创建来源
pageNo: 1,
pageSize: 10
pageSize: 10,
},
tableData: [
{
key: '1',
name: '《少走弯路的职场法则》-2021公开课',
teacher: '张三',
key: "1",
name: "《少走弯路的职场法则》-2021公开课",
teacher: "张三",
courseDuration: 10,
studyDuration: 10,
studentCount: 1,
rating: 5.0,
reviewStatus: '-',
publishStatus: '未发布'
reviewStatus: "-",
publishStatus: "未发布",
},
{
key: '2',
name: 'PDCA循环工作法',
teacher: '张三',
key: "2",
name: "PDCA循环工作法",
teacher: "张三",
courseDuration: 20,
studyDuration: 20,
studentCount: 2,
rating: 4.0,
reviewStatus: '审核中',
publishStatus: '未发布'
reviewStatus: "审核中",
publishStatus: "未发布",
},
{
key: '3',
name: 'BOE端到端的流程体系',
teacher: '张三',
key: "3",
name: "BOE端到端的流程体系",
teacher: "张三",
courseDuration: 30,
studyDuration: 30,
studentCount: 3,
rating: 3.0,
reviewStatus: '审核驳回',
publishStatus: '未发布'
reviewStatus: "审核驳回",
publishStatus: "未发布",
},
{
key: '4',
name: '结构性思维与表达-2023年公开课',
teacher: '张三',
key: "4",
name: "结构性思维与表达-2023年公开课",
teacher: "张三",
courseDuration: 40,
studyDuration: 40,
studentCount: 4,
rating: 5.0,
reviewStatus: '审核通过',
publishStatus: '已发布'
reviewStatus: "审核通过",
publishStatus: "已发布",
},
{
key: '5',
name: '标准化异常处理流程',
teacher: '张三',
key: "5",
name: "标准化异常处理流程",
teacher: "张三",
courseDuration: 10,
studyDuration: 10,
studentCount: 6,
rating: 4.0,
reviewStatus: '审核通过',
publishStatus: '已发布'
reviewStatus: "审核通过",
publishStatus: "已发布",
},
{
key: '6',
name: '企业经营法则',
teacher: '张三',
key: "6",
name: "企业经营法则",
teacher: "张三",
courseDuration: 20,
studyDuration: 20,
studentCount: 6,
rating: 3.0,
reviewStatus: '审核通过',
publishStatus: '已发布'
reviewStatus: "审核通过",
publishStatus: "已发布",
},
{
key: '7',
name: '京东方战略实践学习',
teacher: '张三',
key: "7",
name: "京东方战略实践学习",
teacher: "张三",
courseDuration: 30,
studyDuration: 30,
studentCount: 7,
rating: 5.0,
reviewStatus: '审核通过',
publishStatus: '已发布'
reviewStatus: "审核通过",
publishStatus: "已发布",
},
{
key: '8',
name: '市场营销精要之《如何在新环境下做好新产品整合营销上市》',
teacher: '张三',
key: "8",
name: "市场营销精要之《如何在新环境下做好新产品整合营销上市》",
teacher: "张三",
courseDuration: 40,
studyDuration: 40,
studentCount: 89,
rating: 4.0,
reviewStatus: '审核中',
publishStatus: '已发布'
reviewStatus: "审核中",
publishStatus: "已发布",
},
{
key: '9',
name: '2024热点论坛第三期:国际格局的基本结构及其相关问题',
teacher: '张三',
key: "9",
name: "2024热点论坛第三期:国际格局的基本结构及其相关问题",
teacher: "张三",
courseDuration: 10,
studyDuration: 10,
studentCount: 1,
rating: 3.0,
reviewStatus: '审核中',
publishStatus: '已发布'
reviewStatus: "审核中",
publishStatus: "已发布",
},
{
key: '10',
name: '2024热点论坛2-《新“人机”时代的生存与发展》',
teacher: '张三',
key: "10",
name: "2024热点论坛2-《新“人机”时代的生存与发展》",
teacher: "张三",
courseDuration: 20,
studyDuration: 20,
studentCount: 2,
rating: 5.0,
reviewStatus: '审核驳回',
publishStatus: '已发布'
}
reviewStatus: "审核驳回",
publishStatus: "已发布",
},
],
tableDataTotal: 100,
tableLoading: false,
categoryList: [
{ value: 'all', label: '全部课程分类' },
{ value: 'tech', label: '技术类' },
{ value: 'manage', label: '管理类' },
{ value: 'sale', label: '销售类' }
{ value: "all", label: "全部课程分类" },
{ value: "tech", label: "技术类" },
{ value: "manage", label: "管理类" },
{ value: "sale", label: "销售类" },
],
reviewStatusList: [
{ value: 'all', label: '全部审核状态' },
{ value: 'pending', label: '审核中' },
{ value: 'approved', label: '审核通过' },
{ value: 'rejected', label: '审核驳回' }
{ value: "all", label: "全部审核状态" },
{ value: "pending", label: "审核中" },
{ value: "approved", label: "审核通过" },
{ value: "rejected", label: "审核驳回" },
],
publishStatusList: [
{ value: 'all', label: '全部发布状态' },
{ value: 'draft', label: '未发布' },
{ value: 'published', label: '已发布' }
{ value: "all", label: "全部发布状态" },
{ value: "draft", label: "未发布" },
{ value: "published", label: "已发布" },
],
// 新增的下拉选项列表
enableStatusList: [
{ value: 'all', label: '全部启用状态' },
{ value: 'enabled', label: '已启用' },
{ value: 'disabled', label: '已禁用' }
{ value: "all", label: "全部启用状态" },
{ value: "enabled", label: "已启用" },
{ value: "disabled", label: "已禁用" },
],
isPublicList: [
{ value: 'all', label: '是否公开课' },
{ value: 'yes', label: '是' },
{ value: 'no', label: '否' }
{ value: "all", label: "是否公开课" },
{ value: "yes", label: "是" },
{ value: "no", label: "否" },
],
resourceOwnerList: [
{ value: 'all', label: '全部资源归属' },
{ value: 'self', label: '自有' },
{ value: 'third', label: '第三方' }
{ value: "all", label: "全部资源归属" },
{ value: "self", label: "自有" },
{ value: "third", label: "第三方" },
],
createSourceList: [
{ value: 'all', label: '全部创建来源' },
{ value: 'manual', label: '手动创建' },
{ value: 'import', label: '批量导入' },
{ value: 'api', label: 'API创建' }
{ value: "all", label: "全部创建来源" },
{ value: "manual", label: "手动创建" },
{ value: "import", label: "批量导入" },
{ value: "api", label: "API创建" },
],
columns: [
{
title: '课程名称',
dataIndex: 'name',
key: 'name',
className: 'h',
title: "课程名称",
dataIndex: "name",
key: "name",
className: "h",
ellipsis: true,
width: 200,
sorter: true,
fixed: "left"
fixed: "left",
},
{
title: '课程分类',
dataIndex: 'teacher',
className: 'h',
key: 'teacher',
align: 'center',
title: "课程分类",
dataIndex: "teacher",
className: "h",
key: "teacher",
align: "center",
sorter: true
sorter: true,
},
{
title: '授课教师',
dataIndex: 'teacher',
className: 'h',
key: 'teacher',
align: 'center',
title: "授课教师",
dataIndex: "teacher",
className: "h",
key: "teacher",
align: "center",
sorter: true
sorter: true,
},
{
title: '课程时长',
dataIndex: 'courseDuration',
className: 'h',
key: 'courseDuration',
align: 'center',
title: "课程时长",
dataIndex: "courseDuration",
className: "h",
key: "courseDuration",
align: "center",
sorter: true
sorter: true,
},
{
title: '学习时长',
dataIndex: 'studyDuration',
className: 'h',
key: 'studyDuration',
align: 'center',
title: "学习时长",
dataIndex: "studyDuration",
className: "h",
key: "studyDuration",
align: "center",
sorter: true
sorter: true,
},
{
title: '学习人数',
dataIndex: 'studentCount',
className: 'h',
key: 'studentCount',
align: 'center',
title: "学习人数",
dataIndex: "studentCount",
className: "h",
key: "studentCount",
align: "center",
sorter: true
sorter: true,
},
{
title: '课程评分',
dataIndex: 'rating',
className: 'h',
key: 'rating',
align: 'center',
title: "课程评分",
dataIndex: "rating",
className: "h",
key: "rating",
align: "center",
sorter: true
sorter: true,
},
{
title: '审核状态',
dataIndex: 'reviewStatus',
className: 'h',
key: 'reviewStatus',
align: 'center',
title: "审核状态",
dataIndex: "reviewStatus",
className: "h",
key: "reviewStatus",
align: "center",
sorter: true
sorter: true,
},
{
title: '发布状态',
dataIndex: 'publishStatus',
className: 'h',
key: 'publishStatus',
align: 'center',
title: "发布状态",
dataIndex: "publishStatus",
className: "h",
key: "publishStatus",
align: "center",
sorter: true
sorter: true,
},
{
title: '启停用状态',
dataIndex: 'publishStatus',
className: 'h',
key: 'publishStatus',
align: 'center',
title: "启停用状态",
dataIndex: "publishStatus",
className: "h",
key: "publishStatus",
align: "center",
sorter: true
sorter: true,
},
{
title: '排序值',
dataIndex: 'publishStatus',
className: 'h',
key: 'publishStatus',
align: 'center',
title: "排序值",
dataIndex: "publishStatus",
className: "h",
key: "publishStatus",
align: "center",
sorter: true
sorter: true,
},
{
title: '公开课',
dataIndex: 'publishStatus',
className: 'h',
key: 'publishStatus',
align: 'center',
title: "公开课",
dataIndex: "publishStatus",
className: "h",
key: "publishStatus",
align: "center",
sorter: true
sorter: true,
},
{
title: '资源归属',
dataIndex: 'publishStatus',
className: 'h',
key: 'publishStatus',
align: 'center',
title: "资源归属",
dataIndex: "publishStatus",
className: "h",
key: "publishStatus",
align: "center",
sorter: true
sorter: true,
},
{
title: '创建人',
dataIndex: 'publishStatus',
className: 'h',
key: 'publishStatus',
align: 'center',
title: "创建人",
dataIndex: "publishStatus",
className: "h",
key: "publishStatus",
align: "center",
sorter: true
sorter: true,
},
{
title: '创建来源',
dataIndex: 'publishStatus',
className: 'h',
key: 'publishStatus',
align: 'center',
title: "创建来源",
dataIndex: "publishStatus",
className: "h",
key: "publishStatus",
align: "center",
sorter: true
sorter: true,
},
{
title: '创建时间',
dataIndex: 'publishStatus',
className: 'h',
key: 'publishStatus',
align: 'center',
title: "创建时间",
dataIndex: "publishStatus",
className: "h",
key: "publishStatus",
align: "center",
sorter: true
sorter: true,
},
{
title: '操作',
dataIndex: 'operation',
key: 'operation',
className: 'h',
align: 'right',
fixed: 'right',
title: "操作",
dataIndex: "operation",
key: "operation",
className: "h",
align: "right",
fixed: "right",
width: 200,
scopedSlots: { customRender: 'action' },
sorter: false
scopedSlots: { customRender: "action" },
sorter: false,
},
]
],
};
},
methods: {
@@ -546,13 +556,17 @@ export default {
searchSubmit() {},
searchReset() {},
showModal1() {},
handleEdit() {},
handleEdit() {
this.$router.push({
path: "/ProfessionalMode",
});
},
editMaterial() {},
grantPermission() {},
deleteRecord() {},
exportData() {},
changePagination() {}
}
changePagination() {},
},
};
</script>

View File

@@ -0,0 +1,105 @@
<script setup>
import dragCollapse from "./dragCollapse.vue";
import { ElButton, ElCheckbox } from "element-plus";
import dragTable from "./dragTable.vue";
import { message } from "ant-design-vue";
defineOptions({
name: "CreateCourse",
});
import { ref, reactive, watch, toRaw, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useCourseData } from "@/hooks/useCourseData";
// 使用课程数据hook
const { courseMetadata, courseList, courseActionButtons, executeCourseOperation } = useCourseData();
// 定义表格列
const tableColumns = [
{
title: "序号",
key: "index",
width: 80,
align: "center",
},
{
title: "节名称",
key: "name",
dataIndex: "name",
},
{
title: "类型",
key: "type",
dataIndex: "type",
align: "center",
},
{
title: "操作",
key: "action",
width: 220,
align: "center",
},
];
</script>
<template>
<div class="create-course">
<div class="course-header">
<div class="title">课程名称</div>
<span>创建时间{{ courseMetadata.createTime }}</span>
</div>
<div class="course-content">
<div style="padding: 10px">
<el-button>添加章</el-button>
<el-checkbox style="margin-left: 10px">顺序学习</el-checkbox>
</div>
<div>
<dragCollapse v-model:courseList="courseList">
<template #title="{ course }">{{ course.title }}</template>
<template #desc="{ course }"
>若课程只有一个章节将不在学员端显示该章节名称</template
>
<template #default="{ course, index }">
<div class="drag-course-btn-content">
<el-button
v-for="btn in courseActionButtons"
type="primary"
class="btn-item"
plain
@click="executeCourseOperation(btn.fun, index)"
>{{ btn.label }}</el-button
>
</div>
<div>
<!-- 修改添加 groupId tableId 属性以支持跨表格拖拽 -->
<dragTable
:data="course.data"
:columns="tableColumns"
:group-id="'course-chapters'"
:table-id="'chapter-' + index"
></dragTable>
</div>
</template>
</dragCollapse>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.create-course {
width: 100%;
padding: 10px 20px;
.course-header {
display: flex;
justify-content: space-between;
}
.drag-course-btn-content {
padding: 0 10px;
.btn-item + .btn-item {
margin-left: 10px;
}
}
}
</style>

View File

@@ -0,0 +1,127 @@
<script setup>
import { ref, watch } from "vue";
import draggable from "vuedraggable";
import { ElIcon } from "element-plus";
import { Delete, ArrowUp, ArrowDown, Operation } from "@element-plus/icons-vue";
const props = defineProps({
courseList: {
type: Array,
default: () => [],
},
});
// 定义 emits 用于更新父组件数据
const emit = defineEmits(["update:courseList"]);
// 使用 ref 存储列表数据
const dragList = ref(
props.courseList.map((item) => ({
...item,
isCollapsed: false,
}))
);
// 监听 props 变化,同步更新 dragList
watch(
() => props.courseList,
(newVal) => {
dragList.value = newVal.map((item) => ({
...item,
isCollapsed: false,
}));
},
{ deep: true }
);
const toggleCollapse = (element) => {
element.isCollapsed = !element.isCollapsed;
};
const moveEnd = (e) => {
// 拖拽结束时,更新父组件数据
emit("update:courseList", dragList.value);
};
const moveStart = (e) => {
// 拖拽开始时,将 isCollapsed 设置为 false
dragList.value.forEach((item) => {
item.isCollapsed = true;
});
};
</script>
<template>
<draggable
v-model="dragList"
item-key="id"
handle=".move-icon"
animation="500"
@end="moveEnd"
@start="moveStart"
>
<template #item="{ element, index }">
<div class="drag-collapse">
<div class="drag-collapse-header">
<div>
<span class="drag-collapse-header-title">
<slot name="title" :course="element">视频可见名称</slot>
</span>
<span class="drag-collapse-header-desc">
<slot name="desc" :course="element">
若课程只有一个章节将不在学员端显示该章节名称
</slot>
</span>
</div>
<div>
<el-icon class="move-icon"><Operation /></el-icon>
<el-icon><Delete /></el-icon>
<span @click="toggleCollapse(element)">
<el-icon v-if="!element.isCollapsed"><ArrowUp /></el-icon>
<el-icon v-else><ArrowDown /></el-icon>
</span>
</div>
</div>
<div class="drag-collapse-content" v-show="!element.isCollapsed">
<slot :course="element" :index="index"></slot>
</div>
</div>
</template>
</draggable>
</template>
<style scoped lang="scss">
.drag-collapse {
.drag-collapse-header {
display: flex;
justify-content: space-between;
padding: 10px 10px 5px 10px;
border-bottom: 1px solid #e0e0e0;
.drag-collapse-header-title {
font-size: 16px;
font-weight: 500;
}
.drag-collapse-header-desc {
font-size: 12px;
color: #999;
margin-left: 15px;
}
.move-icon {
cursor: move;
}
}
.drag-collapse-content {
padding: 10px 0;
}
:deep(.el-icon) {
margin-left: 8px;
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,508 @@
<template>
<div class="drag-table-container">
<a-table
:columns="processedColumns"
:data-source="data"
:components="components"
:pagination="false"
:show-header="true"
bordered
/>
</div>
</template>
<script setup>
import { ref, h, computed } from "vue";
import draggable from "vuedraggable";
import { createVNode } from "vue";
import {
MenuOutlined,
VideoCameraOutlined,
AudioOutlined,
FileTextOutlined,
PictureOutlined,
LinkOutlined,
FolderOpenOutlined,
BankOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
SettingOutlined,
} from "@ant-design/icons-vue";
// 定义 props
const props = defineProps({
data: {
type: Array,
default: () => [],
},
columns: {
type: Array,
default: () => [],
},
// 新增:用于区分不同的表格组
groupId: {
type: String,
default: "default-group",
},
// 新增:表格唯一标识
tableId: {
type: String,
default: "",
},
// 新增:是否允许拖出
allowDragOut: {
type: Boolean,
default: true,
},
// 新增:是否允许拖入
allowDragIn: {
type: Boolean,
default: true,
},
});
// 根据类型返回对应图标组件
const getIconComponent = (type) => {
switch (type) {
case "视频":
return VideoCameraOutlined;
case "音频":
return AudioOutlined;
case "文档":
return FileTextOutlined;
case "图文":
return PictureOutlined;
case "链接":
return LinkOutlined;
case "SCORM":
return FolderOpenOutlined;
case "考试":
return BankOutlined;
case "作业":
return EditOutlined;
case "评估":
return BankOutlined; // 可替换为更合适的图标
default:
return FileTextOutlined;
}
};
// 当前正在编辑的行的key
const editingKey = ref("");
// 编辑时的临时值
const editValue = ref("");
// 开始编辑
const startEdit = (record) => {
editingKey.value = record.key;
editValue.value = record.name;
};
// 保存编辑
const saveEdit = (record) => {
record.name = editValue.value;
editingKey.value = "";
editValue.value = "";
};
// 删除处理函数
const handleDelete = (key) => {
const index = props.data.findIndex((item) => item.key === key);
if (index > -1) {
props.data.splice(index, 1);
console.log("删除了:", key);
}
};
// 处理列定义,添加自定义渲染逻辑
const processedColumns = computed(() => {
// 如果传入了 columns prop则使用它并添加自定义渲染
if (props.columns && props.columns.length > 0) {
return props.columns.map((col) => {
// 克隆列对象以避免修改原始 props
const processedCol = { ...col };
// 为序号列添加自定义渲染
if (col.key === "index") {
processedCol.customRender = ({ index }) => {
return h(
"span",
{
style: {
display: "flex",
justifyContent: "space-between",
},
},
[
h("span", { class: "drag-handle" }, [
createVNode(MenuOutlined, {
style: { fontSize: "14px", color: "#666" },
}),
]),
h("span", {}, index + 1),
]
);
};
}
// 为节名称列添加自定义渲染
if (col.key === "name") {
processedCol.customRender = ({ record }) => {
// 检查当前行是否处于编辑状态
const isEditing = record.key === editingKey.value;
// 如果处于编辑状态,显示输入框和确认按钮
if (isEditing) {
return h(
"span",
{ style: { display: "flex", alignItems: "center", gap: "8px" } },
[
h("input", {
value: editValue.value,
onInput: (e) => {
editValue.value = e.target.value;
},
style: {
border: "1px solid #d9d9d9",
borderRadius: "4px",
padding: "4px 11px",
width: "200px",
},
}),
h(
"a",
{
href: "javascript:void(0)",
style: { fontSize: "16px", color: "#52c41a" },
onClick: () => saveEdit(record),
},
"✓"
),
]
);
}
// 否则显示正常文本和编辑图标
const Icon = getIconComponent(record.type);
return h(
"span",
{ style: { display: "flex", alignItems: "center", gap: "8px" } },
[
createVNode(Icon, { style: { color: "#1890ff" } }),
h("span", {}, record.name),
h(
"a",
{
href: "javascript:void(0)",
style: { marginLeft: "4px", fontSize: "12px" },
onClick: () => startEdit(record),
},
"✎"
),
]
);
};
}
// 为操作列添加自定义渲染
if (col.key === "action") {
processedCol.customRender = ({ record }) => {
return h(
"span",
{
style: { display: "flex", justifyContent: "center", gap: "12px" },
},
[
// 设置
h("a", { href: "javascript:void(0)" }, [
createVNode(SettingOutlined, {
style: {
fontSize: "14px",
color: "#1890ff",
paddingRight: "10px",
},
}),
h(
"span",
{ style: { marginLeft: "4px", fontSize: "12px" } },
"设置"
),
]),
// 预览
h("a", { href: "javascript:void(0)" }, [
createVNode(EyeOutlined, {
style: { fontSize: "14px", color: "#1890ff" },
}),
h(
"span",
{ style: { marginLeft: "4px", fontSize: "12px" } },
"预览"
),
]),
// 删除
h(
"a",
{
href: "javascript:void(0)",
onClick: () => handleDelete(record.key),
},
[
createVNode(DeleteOutlined, {
style: { fontSize: "14px", color: "red" },
}),
h(
"span",
{ style: { marginLeft: "4px", fontSize: "12px" } },
"删除"
),
]
),
]
);
};
}
return processedCol;
});
}
// 否则使用默认列定义
return [
{
title: "序号",
key: "index",
width: 80,
align: "center",
customRender: ({ index }) => {
return h(
"span",
{
style: {
display: "flex",
justifyContent: "space-between",
},
},
[
h("span", { class: "drag-handle" }, [
createVNode(MenuOutlined, {
style: { fontSize: "14px", color: "#666" },
}),
]),
h("span", {}, index + 1),
]
);
},
},
{
title: "节名称",
key: "name",
dataIndex: "name",
customRender: ({ record }) => {
// 检查当前行是否处于编辑状态
const isEditing = record.key === editingKey.value;
// 如果处于编辑状态,显示输入框和确认按钮
if (isEditing) {
return h(
"span",
{ style: { display: "flex", alignItems: "center", gap: "8px" } },
[
h("input", {
value: editValue.value,
onInput: (e) => {
editValue.value = e.target.value;
},
style: {
border: "1px solid #d9d9d9",
borderRadius: "4px",
padding: "4px 11px",
width: "200px",
},
}),
h(
"a",
{
href: "javascript:void(0)",
style: { fontSize: "16px", color: "#52c41a" },
onClick: () => saveEdit(record),
},
"✓"
),
]
);
}
// 否则显示正常文本和编辑图标
const Icon = getIconComponent(record.type);
return h(
"span",
{ style: { display: "flex", alignItems: "center", gap: "8px" } },
[
createVNode(Icon, { style: { color: "#1890ff" } }),
h("span", {}, record.name),
h(
"a",
{
href: "javascript:void(0)",
style: { marginLeft: "4px", fontSize: "12px" },
onClick: () => startEdit(record),
},
"✎"
),
]
);
},
},
{
title: "类型",
key: "type",
dataIndex: "type",
align: "center",
},
{
title: "操作",
key: "action",
width: 220,
align: "center",
customRender: ({ record }) => {
return h(
"span",
{ style: { display: "flex", justifyContent: "center", gap: "12px" } },
[
// 设置
h("a", { href: "javascript:void(0)" }, [
createVNode(SettingOutlined, {
style: {
fontSize: "14px",
color: "#1890ff",
paddingRight: "10px",
},
}),
h(
"span",
{ style: { marginLeft: "4px", fontSize: "12px" } },
"设置"
),
]),
// 预览
h("a", { href: "javascript:void(0)" }, [
createVNode(EyeOutlined, {
style: { fontSize: "14px", color: "#1890ff" },
}),
h(
"span",
{ style: { marginLeft: "4px", fontSize: "12px" } },
"预览"
),
]),
// 删除
h(
"a",
{
href: "javascript:void(0)",
onClick: () => handleDelete(record.key),
},
[
createVNode(DeleteOutlined, {
style: { fontSize: "14px", color: "red" },
}),
h(
"span",
{ style: { marginLeft: "4px", fontSize: "12px" } },
"删除"
),
]
),
]
);
},
},
];
});
// 构造自定义 body wrapper 支持 vuedraggable v4+ 的插槽要求
const components = {
body: {
wrapper: (bodyProps, { slots }) => {
return h(
draggable,
{
tag: "tbody",
list: props.data, // 使用传入的 props.data
itemKey: "key",
handle: ".drag-handle",
ghostClass: "drag-ghost",
animation: "300",
// 新增:配置跨表格拖拽的组
group: {
name: props.groupId,
pull: props.allowDragOut,
put: props.allowDragIn,
},
onStart: () => {},
onEnd: (evt) => {
console.log("拖拽结束,新顺序:", props.data);
},
},
{
item: ({ element, index }) =>
h(
"tr",
{ key: element.key },
processedColumns.value.map((col) => {
const td = h(
"td",
{},
col.customRender
? col.customRender({
record: element,
index: index,
})
: element[col.dataIndex]
);
return td;
})
),
}
);
},
},
};
</script>
<style scoped lang="scss">
.drag-table-container {
padding: 20px;
margin: 0 auto;
h3 {
margin-bottom: 16px;
text-align: center;
color: #333;
}
:deep(.ant-table-tbody .drag-ghost) {
opacity: 0.5;
background: #fafafa;
}
:deep(.ant-table-tbody tr:hover) {
background-color: #f6ffed !important;
}
.current-order {
margin-top: 24px;
padding: 16px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
h4 {
margin-bottom: 8px;
}
.order-item {
padding: 4px 0;
}
}
}
</style>

View File

@@ -0,0 +1,410 @@
<script setup>
import { ref, defineOptions, reactive, onMounted } from "vue";
import { ElMessage } from "element-plus";
import {
ElForm,
ElFormItem,
ElRadio,
ElRadioGroup,
ElUpload,
ElButton,
ElInput,
ElCascader,
ElSelect,
} from "element-plus";
import { getClassTree } from "@/api/modules/newApi";
import filecloud from "@/components/FileCloud/index.vue";
import { useUpload } from "@/hooks/useUpload";
import { useCourseForm } from "@/hooks/useCourseForm";
defineOptions({
name: "ProfessionalMode",
});
// 使用上传hook
const {
fileList,
loading,
courseCoverurl,
handleChange,
beforeUpload
} = useUpload();
// 使用表单hook
const {
formRef,
formState,
visibilityOptions,
resetForm
} = useCourseForm();
// 表单相关
const labelCol = { style: { width: "80px" } };
// 数据相关
const data = ref({
typeOption: [],
});
// 文件选择对话框
const dlgFileChoose = ref({
show: false,
});
// 课程信息
const courseInfo = ref({
id: "",
name: "",
orderStudy: false,
type: 10,
orgId: "",
coverImg: "",
source: 1,
forUsers: "",
forScene: "",
value: "",
tags: "",
keywords: "",
device: 3,
status: 1,
summary: "",
overview: "",
visible: true,
refId: "",
refType: "",
});
const fileUrl = process.env.VUE_APP_BASE_API1 + process.env.VUE_APP_FILE_PATH;
// 方法定义
const chooseFile = () => {
dlgFileChoose.value.show = true;
};
const changeCourseImage = (img) => {
if (!img.path) {
return;
}
dlgFileChoose.value.show = false;
courseInfo.value.coverImg = img.path;
courseCoverurl.value = fileUrl + img.path;
};
const choseChoose = () => {
dlgFileChoose.value.show = false;
};
// 表单提交
const handleSubmit = () => {
// formRef.value
// .validate()
// .then(() => {
// console.log("Received values of form:", formState);
// ElMessage.success("表单提交成功");
// })
// .catch((error) => {
// console.log("Validate Failed:", error);
// ElMessage.error("请检查表单填写内容");
// });
console.log("Received values of form:", formState);
ElMessage.success("表单提交成功");
};
// 表单重置
const handleReset = () => {
resetForm(courseCoverurl, fileList);
};
// API调用
const fetchApi = {
getClassTree: () => {
return getClassTree(1).then((res) => {
data.value.typeOption = res.result;
});
},
};
const next = () => {
// 注意这里的路由跳转需要正确引入和使用vue-router
// this.$router.push({
// path: "/createcourse",
// query: {
// id: 1,
// },
// });
};
onMounted(() => {
fetchApi.getClassTree();
});
</script>
<template>
<div class="professional-mode">
<el-form
ref="formRef"
:model="formState"
:label-position="'right'"
label-width="80px"
class="default-form"
@submit.prevent="handleSubmit"
>
<div class="professional-mode-title">
<div class="professional-mode-title-text" style="padding-bottom: 20px">
基本信息
</div>
<div class="professional-mode-form">
<el-form-item
label="课程名称"
prop="courseName"
:rules="[{ required: true, message: '请输入课程名称' }]"
>
<el-input
size="large"
v-model="formState.courseName"
placeholder="请输入课程名称"
/>
</el-form-item>
<el-form-item
label="课程分类"
prop="courseCategory"
:rules="[{ required: true, message: '请选择课程分类' }]"
>
<el-cascader
size="large"
v-model="formState.courseCategory"
placeholder="请选择课程分类"
:options="data.typeOption"
:props="{
label: 'name',
value: 'id',
}"
/>
</el-form-item>
<el-form-item
label="资源归属"
prop="resourceBelong"
:rules="[{ required: true, message: '请选择资源归属' }]"
>
<el-select
size="large"
v-model="formState.resourceBelong"
placeholder="请选择资源归属"
>
<el-option label="选项1" value="1"></el-option>
<el-option label="选项2" value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item
label="授课教师"
prop="lecturer"
:rules="[{ required: true, message: '请选择授课教师' }]"
>
<el-select
size="large"
v-model="formState.lecturer"
placeholder="请选择授课教师"
>
<el-option label="教师1" value="1"></el-option>
<el-option label="教师2" value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item
label="目标人群"
prop="targetGroup"
:rules="[{ required: true, message: '请输入目标人群' }]"
>
<el-input
size="large"
v-model="formState.targetGroup"
placeholder="请输入目标人群"
/>
</el-form-item>
<el-form-item label="课程标签" prop="courseTags">
<el-select
size="large"
v-model="formState.courseTags"
multiple
filterable
allow-create
placeholder="请选择或输入课程标签"
>
<el-option label="标签1" value="标签1"></el-option>
<el-option label="标签2" value="标签2"></el-option>
</el-select>
</el-form-item>
<el-form-item label="受众" prop="audience">
<el-select
size="large"
v-model="formState.audience"
placeholder="请选择受众"
>
<el-option label="受众1" value="1"></el-option>
<el-option label="受众2" value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item label="可见性" prop="visibility">
<el-radio-group v-model="formState.visibility">
<el-radio
v-for="item in visibilityOptions"
:key="item.value"
:label="item.value"
>
<span
:style="{
color:
formState.visibility === item.value ? '#409eff' : '#000',
}"
>{{ item.label }}</span
>
</el-radio>
</el-radio-group>
</el-form-item>
</div>
<div class="professional-mode-title-text" style="padding-top: 0">
课程介绍
</div>
<div class="professional-mode-form">
<el-form-item label="封面介绍" prop="coverIntro">
<div style="display: flex; align-items: flex-end">
<el-upload
v-model:file-list="fileList"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-file-list="false"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:before-upload="beforeUpload"
:on-change="handleChange"
>
<img
v-if="courseCoverurl"
:src="courseCoverurl"
alt="avatar"
style="width: 100%"
/>
<div v-else>
<el-icon v-if="loading"><Loading /></el-icon>
<el-icon v-else><Plus /></el-icon>
<div class="el-upload-text">上传图片</div>
</div>
</el-upload>
<el-button type="primary" link @click="chooseFile"
>选择封面</el-button
>
</div>
<div class="upload-hint">
建议上传800px*450px16:9的图片格式为.png或.jpg大小不超过2M
</div>
</el-form-item>
<el-form-item
label="课程价值"
prop="courseValue"
:rules="[{ required: true, message: '请输入课程价值' }]"
>
<el-input
type="textarea"
size="large"
v-model="formState.courseValue"
:rows="3"
placeholder="请输入课程价值"
/>
</el-form-item>
<el-form-item
label="课程简介"
prop="courseIntro"
:rules="[{ required: true, message: '请输入课程简介' }]"
>
<el-input
type="textarea"
size="large"
v-model="formState.courseIntro"
:rows="4"
placeholder="请输入课程简介"
/>
</el-form-item>
</div>
<div class="form-actions">
<el-button
style="margin-left: 80px; margin-right: 12px"
size="large"
@click="handleReset"
>存草稿</el-button
>
<el-button type="primary" @click="next">创建下一步</el-button>
</div>
</div>
</el-form>
<filecloud
:show="dlgFileChoose.show"
@choose="changeCourseImage"
@close="choseChoose"
></filecloud>
</div>
</template>
<style scoped>
.professional-mode {
width: 70%;
.default-form {
width: 100%;
margin-bottom: 30px;
padding: 10px 15px;
}
.professional-mode-title {
width: 100%;
.professional-mode-title-text {
font-size: 15px;
padding: 10px 0;
font-weight: 600;
}
}
.professional-mode-form {
width: 100%;
.upload-hint {
margin-top: 8px;
font-size: 12px;
color: #999;
}
:deep(.el-form-item) {
margin-bottom: 22px;
}
}
.form-actions {
padding: 20px 0;
}
:deep(.el-upload--picture-card) {
width: 200px;
height: 112px;
}
:deep(.el-upload-list--picture-card) {
width: 200px;
}
:deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 200px;
height: 112px;
}
}
</style>