课程管理的增删改查以及批量删除以及富文本加密以及名称查询
This commit is contained in:
@ -6,11 +6,11 @@ import router from "../router";
|
|||||||
|
|
||||||
const myAxios = axios.create({
|
const myAxios = axios.create({
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
//baseURL:'http://localhost:9091'
|
baseURL:'http://localhost:9091'
|
||||||
// baseURL:'http://1.94.237.210:3457'
|
// baseURL:'http://1.94.237.210:3457'
|
||||||
//baseURL:'http://1.94.237.210:8088'
|
//baseURL:'http://1.94.237.210:8088'
|
||||||
//baseURL:'http://27.30.77.229:9091/'
|
//baseURL:'http://27.30.77.229:9091/'
|
||||||
baseURL:'http://27.30.77.229:9092/'
|
//baseURL:'http://27.30.77.229:9092/'
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,8 +40,7 @@
|
|||||||
<span>课程管理</span>
|
<span>课程管理</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<a-menu-item key="/localCurriculum">本地课程</a-menu-item>
|
<a-menu-item key="/courseManagement">课程管理</a-menu-item>
|
||||||
<a-menu-item key="/linkedCourse">链接课程</a-menu-item>
|
|
||||||
</a-sub-menu>
|
</a-sub-menu>
|
||||||
<a-sub-menu>
|
<a-sub-menu>
|
||||||
<template #title>
|
<template #title>
|
||||||
|
@ -22,16 +22,6 @@ export const routes = [
|
|||||||
name: '首页',
|
name: '首页',
|
||||||
component: () => import("../view/Index.vue"),
|
component: () => import("../view/Index.vue"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/localCurriculum',
|
|
||||||
name: '本地课程',
|
|
||||||
component: () => import("../view/course/localCurriculum.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/linkedCourse',
|
|
||||||
name: '链接课程',
|
|
||||||
component: () => import("../view/course/linkedCourse.vue"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/workList',
|
path: '/workList',
|
||||||
name: '工作列表',
|
name: '工作列表',
|
||||||
@ -106,7 +96,27 @@ export const routes = [
|
|||||||
path: '/withdrawalApplicationRecord',
|
path: '/withdrawalApplicationRecord',
|
||||||
name: '提现申请记录',
|
name: '提现申请记录',
|
||||||
component: () => import("../view/settlement/withdrawalApplicationRecord.vue"),
|
component: () => import("../view/settlement/withdrawalApplicationRecord.vue"),
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
path: '/courseManagement',
|
||||||
|
name: '课程管理',
|
||||||
|
component: () => import("../view/course/courseManagement.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/addcourse',
|
||||||
|
name: '新增课程',
|
||||||
|
component: () => import("../view/course/addCourse.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:'/courseDetail',
|
||||||
|
name:'课程详情',
|
||||||
|
component: ()=> import("../view/course/courseDetail.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:'/chapterDetail',
|
||||||
|
name:'章节详情',
|
||||||
|
component: ()=> import("../view/course/chapterDetail.vue")
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
745
src/view/course/addCourse.vue
Normal file
745
src/view/course/addCourse.vue
Normal file
@ -0,0 +1,745 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="handleSubmit" class="modern-form">
|
||||||
|
<h2 class="form-title">新建课程</h2>
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<!-- 左列 - 手机预览区域 -->
|
||||||
|
<div class="form-column phone-preview-container">
|
||||||
|
<div class="phone-frame">
|
||||||
|
<div class="phone-header">
|
||||||
|
<div class="phone-camera"></div>
|
||||||
|
<div class="phone-speaker"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="phone-screen">
|
||||||
|
<div class="phone-content">
|
||||||
|
<!-- 课程信息 -->
|
||||||
|
<div class="phone-project-info">
|
||||||
|
<h2 class="phone-project-title">{{ formData.name }}</h2>
|
||||||
|
<div class="phone-project-image" :style="{ backgroundImage: `url(${formData.image})` }"></div>
|
||||||
|
<div class="phone-prices">
|
||||||
|
<span class="origin-price">¥{{ formData.originPrice }}</span>
|
||||||
|
<span class="discount-price">¥{{ formData.discountPrice }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 富文本内容区域 -->
|
||||||
|
<div class="phone-sections">
|
||||||
|
<!-- 课程详情 -->
|
||||||
|
<div class="phone-section">
|
||||||
|
<h3 class="phone-section-title">课程详情</h3>
|
||||||
|
<div class="phone-section-content" v-html="formData.detail"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 推广码说明 -->
|
||||||
|
<div class="phone-section">
|
||||||
|
<h3 class="phone-section-title">推广码说明</h3>
|
||||||
|
<div class="phone-section-content" v-html="formData.promoCodeDesc"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="phone-home-button"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右列 - 编辑区域 -->
|
||||||
|
<div class="form-column">
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">课程名称</span>
|
||||||
|
<input
|
||||||
|
v-model="formData.name"
|
||||||
|
type="text"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
placeholder="请输入课程名称"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">课程类型</span>
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<select
|
||||||
|
v-model="formData.type"
|
||||||
|
class="select-field"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="" disabled>请选择类型</option>
|
||||||
|
<option value="自媒体">自媒体</option>
|
||||||
|
<option value="考公考研">考公考研</option>
|
||||||
|
<option value="财经">财经</option>
|
||||||
|
</select>
|
||||||
|
<div class="select-arrow">▼</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">课程图片</span>
|
||||||
|
<div class="file-upload">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="file-input"
|
||||||
|
accept="image/*"
|
||||||
|
@change="handleFileUpload"
|
||||||
|
ref="fileInput"
|
||||||
|
>
|
||||||
|
<div class="upload-button">
|
||||||
|
<span v-if="!formData.image">点击上传图片</span>
|
||||||
|
<span v-else class="file-name">已选择:{{ fileName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="price-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">原价(元)</span>
|
||||||
|
<input
|
||||||
|
v-model.number="formData.originPrice"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
placeholder="请输入原价"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">折扣价(元)</span>
|
||||||
|
<input
|
||||||
|
v-model.number="formData.discountPrice"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
placeholder="请输入折扣价"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="commission-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">一级佣金比例(%)</span>
|
||||||
|
<input
|
||||||
|
v-model.number="formData.firstLevelRate"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
placeholder="0-100"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">二级佣金比例(%)</span>
|
||||||
|
<input
|
||||||
|
v-model.number="formData.secondLevelRate"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
placeholder="0-100"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-text-container">
|
||||||
|
<div class="rich-text-columns">
|
||||||
|
<div class="rich-text-group">
|
||||||
|
<span class="label-text">课程详情</span>
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="formData.detail"
|
||||||
|
:disable="false"
|
||||||
|
@content-change="(html) => formData.detail = html"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-text-group">
|
||||||
|
<span class="label-text">推广码说明</span>
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="formData.promoCodeDesc"
|
||||||
|
:disable="false"
|
||||||
|
@content-change="(html) => formData.promoCodeDesc = html"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="submit-button">
|
||||||
|
<span>立即创建</span>
|
||||||
|
<div class="button-sparkles"></div>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import RichTextEditor from '../components/RichTextEditor.vue';
|
||||||
|
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||||
|
import myAxios from "../../api/myAxios.ts";
|
||||||
|
import router from "../../router";
|
||||||
|
|
||||||
|
interface CourseForm {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
image: string;
|
||||||
|
detail: string;
|
||||||
|
promoCodeDesc: string;
|
||||||
|
originPrice: number;
|
||||||
|
discountPrice: number;
|
||||||
|
firstLevelRate: number;
|
||||||
|
secondLevelRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
const fileName = ref('');
|
||||||
|
|
||||||
|
const handleFileUpload = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadFormData = new FormData();
|
||||||
|
uploadFormData.append('biz', 'default');
|
||||||
|
uploadFormData.append('file', file);
|
||||||
|
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res: any = await myAxios.post('/file/upload', uploadFormData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
'Authorization': storedToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
formData.image = res.data;
|
||||||
|
fileName.value = file.name;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
alert('文件上传失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = reactive<CourseForm>({
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
image: '',
|
||||||
|
detail: '',
|
||||||
|
promoCodeDesc: '',
|
||||||
|
originPrice: 0,
|
||||||
|
discountPrice: 0,
|
||||||
|
firstLevelRate: 0,
|
||||||
|
secondLevelRate: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const encode64 = (text: string): string => {
|
||||||
|
return btoa(
|
||||||
|
encodeURIComponent(text).replace(
|
||||||
|
/%([0-9A-F]{2})/g,
|
||||||
|
(_, p1) => String.fromCharCode(parseInt(p1, 16))
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptValue = (value: any, key: string): any => {
|
||||||
|
// 数值型字段直接返回,不加密
|
||||||
|
if (key === 'originPrice' || key === 'discountPrice' ||
|
||||||
|
key === 'firstLevelRate' || key === 'secondLevelRate' ||
|
||||||
|
key === 'image' || key ==='name'||key === 'type') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const valueString = typeof value === 'object'
|
||||||
|
? JSON.stringify(value)
|
||||||
|
: String(value);
|
||||||
|
return encode64(valueString);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Base64编码失败:', error);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
if (!validateForm()) {
|
||||||
|
alert('请填写所有必填字段');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const encryptedFormData: Record<string, any> = {};
|
||||||
|
|
||||||
|
Object.entries(formData).forEach(([key, value]) => {
|
||||||
|
if (value === null || value === undefined || value === '') return;
|
||||||
|
encryptedFormData[key] = encryptValue(value, key);
|
||||||
|
});
|
||||||
|
|
||||||
|
const res: any = await myAxios.post(`/course/add`, encryptedFormData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': storedToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
alert('课程创建成功!');
|
||||||
|
Object.assign(formData, {
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
image: '',
|
||||||
|
detail: '',
|
||||||
|
promoCodeDesc: '',
|
||||||
|
originPrice: 0,
|
||||||
|
discountPrice: 0,
|
||||||
|
firstLevelRate: 0,
|
||||||
|
secondLevelRate: 0
|
||||||
|
});
|
||||||
|
fileName.value = '';
|
||||||
|
router.push('/courseManagement');
|
||||||
|
} else {
|
||||||
|
alert(`创建失败:${res.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('请求失败:', error);
|
||||||
|
alert('提交失败,请检查控制台获取详细信息');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
const requiredFields: (keyof CourseForm)[] = [
|
||||||
|
'name',
|
||||||
|
'type',
|
||||||
|
'image',
|
||||||
|
'detail',
|
||||||
|
'promoCodeDesc',
|
||||||
|
'originPrice',
|
||||||
|
'discountPrice',
|
||||||
|
'firstLevelRate',
|
||||||
|
'secondLevelRate'
|
||||||
|
];
|
||||||
|
|
||||||
|
return requiredFields.every(field => {
|
||||||
|
const value = formData[field];
|
||||||
|
return value !== '' && value !== null && value !== undefined;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modern-form {
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 2.5rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-group,
|
||||||
|
.commission-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field,
|
||||||
|
.select-wrapper,
|
||||||
|
.textarea-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem 1.2rem;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field:focus,
|
||||||
|
.select-wrapper:focus-within,
|
||||||
|
.textarea-field:focus {
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px dashed #cbd5e1;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-field {
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #94a3b8;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-field {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-sparkles {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: rgba(255,255,255,0.4);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: sparkle 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkle {
|
||||||
|
0% { transform: scale(0) translate(0,0); }
|
||||||
|
50% { transform: scale(1) translate(100px, -50px); }
|
||||||
|
100% { transform: scale(0) translate(200px, -100px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-form {
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-group,
|
||||||
|
.commission-group {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-container {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
height: 100%;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-height: 320px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group:focus-within {
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group .label-text {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3b4151;
|
||||||
|
padding: 12px 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机预览样式 */
|
||||||
|
.phone-preview-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-frame {
|
||||||
|
position: relative;
|
||||||
|
width: 320px;
|
||||||
|
height: 640px;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 40px;
|
||||||
|
padding: 15px;
|
||||||
|
box-shadow: 0 0 0 12px #1f1f1f, 0 0 30px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 180px;
|
||||||
|
height: 25px;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-camera {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 20px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #15294c;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-speaker {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 60px;
|
||||||
|
height: 6px;
|
||||||
|
background: #1f1f1f;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-screen {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 28px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-home-button {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #1f1f1f;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-project-info {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-project-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-project-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-prices {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.origin-price {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-price {
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-sections {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #2c3e50;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #4a5568;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content * {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content h1,
|
||||||
|
.phone-section-content h2,
|
||||||
|
.phone-section-content h3 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content ul,
|
||||||
|
.phone-section-content ol {
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -3,7 +3,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>链接课程</div>
|
<div>章节详情</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
886
src/view/course/courseDetail.vue
Normal file
886
src/view/course/courseDetail.vue
Normal file
@ -0,0 +1,886 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import myAxios from "../../api/myAxios";
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
import RichTextEditor from '../components/RichTextEditor.vue';
|
||||||
|
import {Form} from 'ant-design-vue'
|
||||||
|
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||||
|
|
||||||
|
interface CourseDetail {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
image: string;
|
||||||
|
detail: string;
|
||||||
|
promoCodeDesc: string;
|
||||||
|
originPrice: number;
|
||||||
|
discountPrice: number;
|
||||||
|
orderCount: number;
|
||||||
|
firstLevelRate: number;
|
||||||
|
secondLevelRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const loading = ref(false);
|
||||||
|
const courseData = ref<CourseDetail>({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
image: '',
|
||||||
|
detail: '',
|
||||||
|
promoCodeDesc: '',
|
||||||
|
originPrice: 0,
|
||||||
|
discountPrice: 0,
|
||||||
|
orderCount: 0,
|
||||||
|
firstLevelRate: 0,
|
||||||
|
secondLevelRate: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const editData = ref<CourseDetail | null>(null);
|
||||||
|
|
||||||
|
const courseId = ref<string | null>(null);
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
function encode64(text: string): string {
|
||||||
|
return btoa(encodeURIComponent(text).replace(/%([0-9A-F]{2})/g, (_, p1) => {
|
||||||
|
return String.fromCharCode(parseInt(p1, 16));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function decode64(text: string): string {
|
||||||
|
return decodeURIComponent(
|
||||||
|
Array.from(atob(text), char =>
|
||||||
|
'%' + ('00' + char.charCodeAt(0).toString(16)).slice(-2)
|
||||||
|
).join('')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptCourseData = (data: CourseDetail): CourseDetail => {
|
||||||
|
const decryptedData: CourseDetail = { ...data };
|
||||||
|
|
||||||
|
const fieldsToDecrypt: (keyof CourseDetail)[] = [
|
||||||
|
'detail',
|
||||||
|
'promoCodeDesc'
|
||||||
|
];
|
||||||
|
|
||||||
|
fieldsToDecrypt.forEach(field => {
|
||||||
|
const value = decryptedData[field];
|
||||||
|
if (typeof value === 'string' && value) {
|
||||||
|
try {
|
||||||
|
(decryptedData as Record<keyof CourseDetail, any>)[field] = decode64(value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Base64解码失败 (${field}):`, error);
|
||||||
|
(decryptedData as Record<keyof CourseDetail, any>)[field] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return decryptedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图片上传处理逻辑
|
||||||
|
const handleFileUpload = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 本地预览
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
previewImage.value = e.target?.result as string;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
// 上传服务器
|
||||||
|
const uploadFormData = new FormData();
|
||||||
|
uploadFormData.append('biz', 'default');
|
||||||
|
uploadFormData.append('file', file);
|
||||||
|
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res: any = await myAxios.post('/file/upload', uploadFormData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
'Authorization': storedToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 1 && res.data) {
|
||||||
|
if (editData.value) {
|
||||||
|
editData.value.image = res.data;
|
||||||
|
}
|
||||||
|
message.success('图片上传成功');
|
||||||
|
} else {
|
||||||
|
throw new Error(res.message || '上传失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
message.error('文件上传失败');
|
||||||
|
previewImage.value = '';
|
||||||
|
} finally {
|
||||||
|
// 重置文件输入,允许重复上传同一文件
|
||||||
|
if (input) input.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewImage = ref('');
|
||||||
|
|
||||||
|
const formRef = ref<typeof Form>();
|
||||||
|
|
||||||
|
// 添加粘贴校验方法
|
||||||
|
const handlePaste = (e: ClipboardEvent) => {
|
||||||
|
const pasteData = e.clipboardData?.getData('text/plain');
|
||||||
|
if (pasteData && !/^\d+$/.test(pasteData)) {
|
||||||
|
e.preventDefault();
|
||||||
|
message.error('只能粘贴数字');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishEditing = async () => {
|
||||||
|
try {
|
||||||
|
if (!editData.value) {
|
||||||
|
message.error('编辑数据不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
if (!storedToken) {
|
||||||
|
message.error('未找到登录信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: Record<string, any> = { ...editData.value };
|
||||||
|
|
||||||
|
const fieldsToEncode: (keyof CourseDetail)[] = [
|
||||||
|
'detail',
|
||||||
|
'promoCodeDesc'
|
||||||
|
];
|
||||||
|
|
||||||
|
fieldsToEncode.forEach(field => {
|
||||||
|
const value = payload[field];
|
||||||
|
if (typeof value === 'string' && value) {
|
||||||
|
payload[field] = encode64(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const res: any = await myAxios.post(
|
||||||
|
"/course/update",
|
||||||
|
JSON.stringify(payload),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': storedToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('课程更新成功');
|
||||||
|
courseData.value = editData.value;
|
||||||
|
editData.value = null;
|
||||||
|
router.push('/courseManagement')
|
||||||
|
} else {
|
||||||
|
message.error(res.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("更新失败:", error);
|
||||||
|
message.error('课程更新失败');
|
||||||
|
} finally {
|
||||||
|
isEditing.value = false;
|
||||||
|
previewImage.value = ''; // 清空预览图
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof route.query.id === 'string') {
|
||||||
|
courseId.value = route.query.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(courseId.value !== null) {
|
||||||
|
handleIdSearch(courseId.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleIdSearch = async (id: string) => {
|
||||||
|
if (!id) {
|
||||||
|
message.warning("请输入课程ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(id)) {
|
||||||
|
message.warning("ID必须为数字");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
if (!storedToken) {
|
||||||
|
message.error('未找到登录信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = await myAxios.post(
|
||||||
|
"/course/queryById",
|
||||||
|
{ id: parseInt(id) },
|
||||||
|
{ headers: { Authorization: storedToken } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1 && res.data) {
|
||||||
|
const decryptedData = decryptCourseData(res.data);
|
||||||
|
courseData.value = decryptedData;
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '查询失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("查询失败:", error);
|
||||||
|
message.error('课程查询失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCourse = () => {
|
||||||
|
if (!courseData.value) {
|
||||||
|
message.warning('课程数据尚未加载完成,请稍后再试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 创建编辑数据的深拷贝副本
|
||||||
|
editData.value = JSON.parse(JSON.stringify(courseData.value));
|
||||||
|
previewImage.value = ''; // 进入编辑模式时重置预览图
|
||||||
|
isEditing.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.push('/courseManagement')
|
||||||
|
};
|
||||||
|
|
||||||
|
// 触发文件选择
|
||||||
|
const triggerFileInput = () => {
|
||||||
|
if (fileInput.value) {
|
||||||
|
fileInput.value.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNumberInput = (e: KeyboardEvent) => {
|
||||||
|
const allowedKeys = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', '.'];
|
||||||
|
|
||||||
|
// 只允许数字和控制键
|
||||||
|
if (!allowedKeys.includes(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加范围限制方法 - 现在在输入时立即修正
|
||||||
|
const limitPrice = (value: number, field: keyof CourseDetail) => {
|
||||||
|
if (!editData.value) return;
|
||||||
|
|
||||||
|
// 确保是数字
|
||||||
|
if (isNaN(value)) {
|
||||||
|
(editData.value as any)[field] = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制价格不能为负数
|
||||||
|
if (value < 0) (editData.value as any)[field] = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const limitRate = (value: number, field: keyof CourseDetail) => {
|
||||||
|
if (!editData.value) return;
|
||||||
|
|
||||||
|
// 确保是数字
|
||||||
|
if (isNaN(value)) {
|
||||||
|
(editData.value as any)[field] = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制佣金比例在0-100之间
|
||||||
|
if (value < 0) (editData.value as any)[field] = 0;
|
||||||
|
if (value > 100) (editData.value as any)[field] = 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加输入时实时限制 - 立即修正值
|
||||||
|
const handlePriceInput = (value: number, field: keyof CourseDetail) => {
|
||||||
|
limitPrice(value, field);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRateInput = (value: number, field: keyof CourseDetail) => {
|
||||||
|
limitRate(value, field);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="course-detail-container">
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<div v-if="loading" class="loading">加载中...</div>
|
||||||
|
|
||||||
|
<!-- 详情页 -->
|
||||||
|
<div v-if="courseData && !isEditing" class="detail-card">
|
||||||
|
<!-- 头部区域 -->
|
||||||
|
<div class="header-section">
|
||||||
|
<h1 class="course-title">{{ courseData.name }}</h1>
|
||||||
|
<a-button class="custom-button" @click="updateCourse">编辑课程</a-button>
|
||||||
|
<a-button class="custom-button" @click="goBack">返回</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="basic-info">
|
||||||
|
<div class="course-image">
|
||||||
|
<img :src="downLoadImage+courseData.image" alt="课程封面图">
|
||||||
|
</div>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<label>课程类型</label>
|
||||||
|
<div class="value">{{ courseData.type }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>订单数量</label>
|
||||||
|
<div class="value">{{ courseData.orderCount }}单</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>原价</label>
|
||||||
|
<div class="value">¥{{ courseData.originPrice }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>折扣价</label>
|
||||||
|
<div class="value">¥{{ courseData.discountPrice }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>一级佣金比例</label>
|
||||||
|
<div class="value">{{ courseData.firstLevelRate }}%</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>二级佣金比例</label>
|
||||||
|
<div class="value">{{ courseData.secondLevelRate }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 富文本内容区块 -->
|
||||||
|
<div class="rich-section" v-if="courseData.detail">
|
||||||
|
<h2 class="section-title">课程详情</h2>
|
||||||
|
<div class="rich-content" v-html="courseData.detail"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-section" v-if="courseData.promoCodeDesc">
|
||||||
|
<h2 class="section-title">推广码说明</h2>
|
||||||
|
<div class="rich-content" v-html="courseData.promoCodeDesc"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑页 -->
|
||||||
|
<div v-if="isEditing && editData" class="add-course-container">
|
||||||
|
<a-form ref="formRef" :model="editData" layout="vertical" @submit.prevent="finishEditing">
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-header">
|
||||||
|
<h2>课程基本信息</h2>
|
||||||
|
<a-button class="custom-button" @click="isEditing = false">返回</a-button>
|
||||||
|
</div>
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="课程名称">
|
||||||
|
<a-input v-model:value="editData.name" placeholder="请输入课程名称" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="课程类型">
|
||||||
|
<a-select v-model:value="editData.type" placeholder="请选择课程类型">
|
||||||
|
<a-select-option value="自媒体">自媒体</a-select-option>
|
||||||
|
<a-select-option value="财经">财经</a-select-option>
|
||||||
|
<a-select-option value="考公考研">考公考研</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item
|
||||||
|
label="原价(元)"
|
||||||
|
name="originPrice"
|
||||||
|
>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="editData.originPrice"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="0"
|
||||||
|
@keypress="handleNumberInput"
|
||||||
|
@paste="handlePaste"
|
||||||
|
@change="(val:any) => handlePriceInput(val, 'originPrice')"
|
||||||
|
placeholder="请输入原价"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item
|
||||||
|
label="折扣价(元)"
|
||||||
|
name="discountPrice"
|
||||||
|
>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="editData.discountPrice"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="0"
|
||||||
|
@keypress="handleNumberInput"
|
||||||
|
@paste="handlePaste"
|
||||||
|
@change="(val:any) => handlePriceInput(val, 'discountPrice')"
|
||||||
|
placeholder="请输入折扣价"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item
|
||||||
|
label="一级佣金比例(%)"
|
||||||
|
name="firstLevelRate"
|
||||||
|
>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="editData.firstLevelRate"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
@keypress="handleNumberInput"
|
||||||
|
@paste="handlePaste"
|
||||||
|
@change="(val:any) => handleRateInput(val, 'firstLevelRate')"
|
||||||
|
placeholder="0-100"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item
|
||||||
|
label="二级佣金比例(%)"
|
||||||
|
name="secondLevelRate"
|
||||||
|
>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="editData.secondLevelRate"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
@keypress="handleNumberInput"
|
||||||
|
@paste="handlePaste"
|
||||||
|
@change="(val:any) => handleRateInput(val, 'secondLevelRate')"
|
||||||
|
placeholder="0-100"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-form-item label="课程图片">
|
||||||
|
<div class="file-upload">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="file-input"
|
||||||
|
accept="image/*"
|
||||||
|
@change="handleFileUpload"
|
||||||
|
ref="fileInput"
|
||||||
|
>
|
||||||
|
<div class="upload-area" @click="triggerFileInput">
|
||||||
|
<div class="upload-button">
|
||||||
|
<span>点击上传图片</span>
|
||||||
|
<div class="file-name">支持 JPG/PNG 格式,大小不超过 5MB</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="previewImage || editData.image" class="preview-image">
|
||||||
|
<img
|
||||||
|
v-if="previewImage"
|
||||||
|
:src="previewImage"
|
||||||
|
alt="上传预览"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
:src="downLoadImage + editData.image"
|
||||||
|
alt="课程封面预览"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h2>富文本配置</h2>
|
||||||
|
|
||||||
|
<div class="rich-text-container">
|
||||||
|
<div class="rich-text-columns">
|
||||||
|
<div class="rich-text-group">
|
||||||
|
<span class="label-text">课程详情</span>
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="editData.detail"
|
||||||
|
:disable="false"
|
||||||
|
@content-change="(html: string) => { if (editData) editData.detail = html }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-text-group">
|
||||||
|
<span class="label-text">推广码说明</span>
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="editData.promoCodeDesc"
|
||||||
|
:disable="false"
|
||||||
|
@content-change="(html: string) => { if (editData) editData.promoCodeDesc = html }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a-button class="custom-button" htmlType="submit">完成编辑</a-button>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-detail-container {
|
||||||
|
width: 100%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #666;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-image img {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item label {
|
||||||
|
display: block;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .value {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
border-left: 4px solid #ffa940;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-section {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content :deep(p) {
|
||||||
|
margin: 0.8em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content :deep(h2) {
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 1.2em 0 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content :deep(ul) {
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content :deep(li) {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.basic-info {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-course-container {
|
||||||
|
width: 100%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
border-left: 4px solid #ffa940;
|
||||||
|
padding-left: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-container {
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px dashed #cbd5e1;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button span {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:hover,
|
||||||
|
.custom-button:focus {
|
||||||
|
background-color: #fa8c16;
|
||||||
|
border-color: #fa8c16;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button[disabled] {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
opacity: 0.6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button.ant-btn-dangerous {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button.ant-btn-dangerous:hover,
|
||||||
|
.custom-button.ant-btn-dangerous:focus {
|
||||||
|
background-color: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-container {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
height: 100%;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-height: 320px;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group:focus-within {
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
margin: 10px 0 0 5px;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3b4151;
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group:focus-within .label-text {
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.rich-text-columns {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ql-container) {
|
||||||
|
height: 280px;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-field {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-field:focus {
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem 1.2rem;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
</style>
|
535
src/view/course/courseManagement.vue
Normal file
535
src/view/course/courseManagement.vue
Normal file
@ -0,0 +1,535 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div class="search-box">
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-space>
|
||||||
|
<a-form-item label="课程名称">
|
||||||
|
<a-input-search
|
||||||
|
style="width: 300px"
|
||||||
|
placeholder="请输入课程名称"
|
||||||
|
enter-button
|
||||||
|
@search="handleCourseSearch"
|
||||||
|
v-model:value="searchCourseName"
|
||||||
|
class="custom-search"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-button class="custom-button" @click="goAddCourse">新增课程</a-button>
|
||||||
|
<a-button class="custom-button" @click="reset">重置搜索</a-button>
|
||||||
|
<a-button
|
||||||
|
class="custom-button danger"
|
||||||
|
:disabled="selectedRowKeys.length === 0"
|
||||||
|
@click="showDeleteConfirm"
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据-->
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="tableData"
|
||||||
|
:scroll="{ x: 1500, y: 550 }"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
bordered
|
||||||
|
rowKey="id"
|
||||||
|
@change="handleTableChange"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<!-- 课程图片 -->
|
||||||
|
<template v-if="column.key === 'image'">
|
||||||
|
<a-avatar :src="downLoadImage+record.image" shape="square" :size="64"/>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'originPrice'">
|
||||||
|
{{record.originPrice }}¥
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'discountPrice'">
|
||||||
|
{{record.discountPrice}}¥
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'orderCount'">
|
||||||
|
{{record.orderCount}}人
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'firstLevelRate'">
|
||||||
|
{{record.firstLevelRate }}%
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'secondLevelRate'">
|
||||||
|
{{record.secondLevelRate }}%
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
@click="deleteCourse(record.id)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="showDetails(record.id)"
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="chapterDetails(record.id)"
|
||||||
|
>
|
||||||
|
章节
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import myAxios from "../../api/myAxios.ts";
|
||||||
|
import { message, Modal } from "ant-design-vue";
|
||||||
|
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||||
|
import router from "../../router";
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const total = ref(0);
|
||||||
|
const selectedRowKeys = ref<number[]>([]); // 存储选中的行ID
|
||||||
|
|
||||||
|
const searchCourseName = ref(""); // 改为项目名称搜索参数
|
||||||
|
const searchParams = ref({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortField: "id",
|
||||||
|
sortOrder: "ascend",
|
||||||
|
userRole: null,
|
||||||
|
name: "",
|
||||||
|
type:""
|
||||||
|
});
|
||||||
|
|
||||||
|
// 行选择配置
|
||||||
|
const rowSelection = ref({
|
||||||
|
selectedRowKeys: selectedRowKeys,
|
||||||
|
onChange: (selectedKeys: number[]) => {
|
||||||
|
selectedRowKeys.value = selectedKeys;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//用户表
|
||||||
|
const columns = [
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '课程ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
width: 45,
|
||||||
|
key: 'id',
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '课程图片',
|
||||||
|
dataIndex: 'image',
|
||||||
|
key: 'image',
|
||||||
|
width: 45,
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '课程名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: 65,
|
||||||
|
key: 'name',
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '课程类别',
|
||||||
|
dataIndex: 'type',
|
||||||
|
width: 55,
|
||||||
|
key: 'type',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '课程原价',
|
||||||
|
dataIndex: 'originPrice',
|
||||||
|
width: 55,
|
||||||
|
key: 'originPrice',
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '折扣价格',
|
||||||
|
dataIndex: 'discountPrice',
|
||||||
|
key: 'discountPrice',
|
||||||
|
width: 55,
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '已下单人数',
|
||||||
|
dataIndex: 'orderCount',
|
||||||
|
key: 'orderCount',
|
||||||
|
width: 55,
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '一级佣金比例',
|
||||||
|
dataIndex: 'firstLevelRate',
|
||||||
|
key: 'firstLevelRate',
|
||||||
|
width: 65,
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '二级佣金比例',
|
||||||
|
dataIndex: 'secondLevelRate',
|
||||||
|
key: 'secondLevelRate',
|
||||||
|
width: 65,
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 75,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ProjectRecord {
|
||||||
|
// 这里根据实际数据结构定义属性
|
||||||
|
superHostList?: string[];
|
||||||
|
// 其他属性...
|
||||||
|
}
|
||||||
|
// 项目名称搜索方法
|
||||||
|
const handleCourseSearch = async () => {
|
||||||
|
// 将搜索参数同步到分页查询参数
|
||||||
|
searchParams.value.name = searchCourseName.value;
|
||||||
|
searchParams.value.current = 1; // 重置到第一页
|
||||||
|
await getCourseList();
|
||||||
|
};
|
||||||
|
//用户分页查询
|
||||||
|
const getCourseList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
if (!storedToken) throw new Error('未找到登录信息');
|
||||||
|
|
||||||
|
const res:any = await myAxios.post("/course/page",
|
||||||
|
{
|
||||||
|
...searchParams.value,
|
||||||
|
projectName: searchParams.value.projectName
|
||||||
|
},
|
||||||
|
{ headers: { Authorization: storedToken } }
|
||||||
|
);
|
||||||
|
console.log(res)
|
||||||
|
if (res.code === 1 && res.data && Array.isArray(res.data.records)) {
|
||||||
|
tableData.value = res.data.records.map((item: ProjectRecord) => ({
|
||||||
|
...item,
|
||||||
|
superUserList: item.superHostList? item.superHostList.join(', ') : '无'
|
||||||
|
}));
|
||||||
|
// 同步总条数到分页组件
|
||||||
|
total.value = res.data.total;
|
||||||
|
pagination.value.total = res.data.total; // 新增此行
|
||||||
|
pagination.value.current = searchParams.value.current; // 同步当前页
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '请求失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("请求失败:", error);
|
||||||
|
message.error('获取数据失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(getCourseList);
|
||||||
|
//分页
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const pagination = ref({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total: number) => `共 ${total} 条`,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100']
|
||||||
|
});
|
||||||
|
const handleTableChange = (pag: any, _: any, sorter: any) => {
|
||||||
|
// 处理排序参数
|
||||||
|
let sortField = "id"; // 默认排序字段
|
||||||
|
let sortOrder = "ascend"; // 默认排序方式
|
||||||
|
if (sorter.field) {
|
||||||
|
sortField = sorter.field;
|
||||||
|
sortOrder = sorter.order;
|
||||||
|
}
|
||||||
|
searchParams.value = {
|
||||||
|
...searchParams.value,
|
||||||
|
current: pag.current,
|
||||||
|
pageSize: pag.pageSize,
|
||||||
|
sortField: sortField, // 设置排序字段
|
||||||
|
sortOrder: sortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
// 同步到分页组件
|
||||||
|
pagination.value = {
|
||||||
|
...pagination.value,
|
||||||
|
current: pag.current,
|
||||||
|
pageSize: pag.pageSize
|
||||||
|
};
|
||||||
|
|
||||||
|
getCourseList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ID查询方法
|
||||||
|
interface Project {
|
||||||
|
id: number;
|
||||||
|
projectName: string;
|
||||||
|
projectImage: string;
|
||||||
|
projectSettlementCycle: number;
|
||||||
|
maxPromoterCount: number;
|
||||||
|
projectStatus: string;
|
||||||
|
projectDescription: string;
|
||||||
|
settlementDesc: string;
|
||||||
|
// 其他可能存在的属性根据实际情况补充
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableData = ref<Project[]>([]);
|
||||||
|
|
||||||
|
|
||||||
|
// 删除项目 - 添加确认弹窗
|
||||||
|
const deleteCourse = (id: number) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: '确定要删除该课程吗?删除后数据将无法恢复!',
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res:any = await myAxios.post(
|
||||||
|
"/course/delete",
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
'AfterScript': 'required-script'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('删除成功');
|
||||||
|
await getCourseList();
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
message.error('删除操作失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
// 用户点击取消,不做操作
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量删除确认弹窗
|
||||||
|
const showDeleteConfirm = () => {
|
||||||
|
if (selectedRowKeys.value.length === 0) {
|
||||||
|
message.warning('请至少选择一门课程');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认批量删除',
|
||||||
|
content: `确定要删除选中的 ${selectedRowKeys.value.length} 门课程吗?删除后数据将无法恢复!`,
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
await deleteBatchCourses();
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
// 用户点击取消,不做操作
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量删除方法
|
||||||
|
const deleteBatchCourses = async () => {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res: any = await myAxios.post(
|
||||||
|
"/course/delBatch",
|
||||||
|
{ ids: selectedRowKeys.value },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success(`成功删除 ${selectedRowKeys.value.length} 门课程`);
|
||||||
|
selectedRowKeys.value = []; // 清空选择
|
||||||
|
await getCourseList(); // 刷新列表
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '批量删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量删除失败:', error);
|
||||||
|
message.error('批量删除操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置按钮
|
||||||
|
const reset = () => {
|
||||||
|
searchCourseName.value = "";
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
searchParams.value = {
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortField: "id",
|
||||||
|
sortOrder: "ascend",
|
||||||
|
userRole: null,
|
||||||
|
name: "",
|
||||||
|
type:""
|
||||||
|
};
|
||||||
|
getCourseList();
|
||||||
|
};
|
||||||
|
|
||||||
|
//去新增项目
|
||||||
|
const goAddCourse=()=>{
|
||||||
|
router.push('/addcourse')
|
||||||
|
}
|
||||||
|
|
||||||
|
const showDetails=(id:string)=>{
|
||||||
|
router.push({
|
||||||
|
path:'/courseDetail',
|
||||||
|
query:{
|
||||||
|
id:String(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const chapterDetails=(id:string)=>{
|
||||||
|
router.push({
|
||||||
|
path:'/chapterDetail',
|
||||||
|
query:{
|
||||||
|
id:String(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 分割线样式 */
|
||||||
|
:deep(.ant-divider-vertical) {
|
||||||
|
border-color: rgba(0, 0, 0, 0.15);
|
||||||
|
height: 1.2em;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-descriptions-item-label) {
|
||||||
|
font-weight: 600;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card :deep(.ant-card-head) {
|
||||||
|
background: #fafafa;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 角色颜色映射 */
|
||||||
|
:root {
|
||||||
|
--role-user: #87d068;
|
||||||
|
--role-admin: #f50;
|
||||||
|
--role-boss: #722ed1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 批量删除按钮样式 */
|
||||||
|
.danger {
|
||||||
|
background-color: #ff4d4f !important;
|
||||||
|
border-color: #ff4d4f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger:hover,
|
||||||
|
.danger:focus {
|
||||||
|
background-color: #ff7875 !important;
|
||||||
|
border-color: #ff7875 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*橙色按钮*/
|
||||||
|
.custom-button {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:hover,
|
||||||
|
.custom-button:focus {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button[disabled] {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
opacity: 0.6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险按钮样式 */
|
||||||
|
.custom-button.ant-btn-dangerous {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button.ant-btn-dangerous:hover,
|
||||||
|
.custom-button.ant-btn-dangerous:focus {
|
||||||
|
background-color: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持原有的其他样式不变 */
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button) {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button:hover),
|
||||||
|
.custom-search :deep(.ant-input-search-button:focus) {
|
||||||
|
background-color: #fa8c16;
|
||||||
|
border-color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持输入框原有样式不变 */
|
||||||
|
.custom-search :deep(.ant-input) {
|
||||||
|
border-right-color: #ffa940;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,11 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>本地课程</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
Reference in New Issue
Block a user