|
|
@@ -1,10 +1,10 @@
|
|
|
<template>
|
|
|
<div class="train-form bg-white p-6 rounded h-full overflow-y-auto">
|
|
|
<div class="form-breadcrumb mb-4 text-gray-400 text-xs">
|
|
|
- 课程管理 / 培训课程 / {{ initialData ? '编辑' : '新增' }}{{ type === 'video' ? '视频' : '线下' }}培训
|
|
|
+ 课程管理 / 培训课程 / {{ initialData ? '编辑' : '新增' }}{{ type === 'video' ? '视频' : type === 'live' ? '直播' : '线下' }}培训
|
|
|
</div>
|
|
|
<div class="form-header mb-8">
|
|
|
- <div class="text-xl font-bold">{{ initialData ? '编辑' : '新增' }}{{ type === 'video' ? '视频培训' : '线下培训' }}</div>
|
|
|
+ <div class="text-xl font-bold">{{ initialData ? '编辑' : '新增' }}{{ type === 'video' ? '视频培训' : type === 'live' ? '直播培训' : '线下培训' }}</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="form-wrapper border-2 border-blue-500 rounded-lg p-10 bg-white relative">
|
|
|
@@ -23,13 +23,23 @@
|
|
|
<template v-if="type === 'video'">
|
|
|
<el-form-item label="培训封面" prop="thumbnail" required>
|
|
|
<div class="thumbnail-uploader">
|
|
|
+ <input
|
|
|
+ ref="thumbnailInputRef"
|
|
|
+ type="file"
|
|
|
+ accept=".jpg,.jpeg,.png"
|
|
|
+ class="hidden"
|
|
|
+ @change="handleThumbnailFileChange"
|
|
|
+ />
|
|
|
<el-upload
|
|
|
+ ref="thumbnailUploadRef"
|
|
|
class="avatar-uploader"
|
|
|
action="#"
|
|
|
:auto-upload="false"
|
|
|
:show-file-list="false"
|
|
|
+ accept=".jpg,.jpeg,.png"
|
|
|
+ @click="handleUploadThumbnail"
|
|
|
>
|
|
|
- <img v-if="form.thumbnail" :src="form.thumbnail" class="avatar-preview" />
|
|
|
+ <img v-if="form.thumbnailUrl" :src="form.thumbnailUrl" class="avatar-preview" />
|
|
|
<el-icon v-else class="avatar-uploader-icon"><i-ep-plus /></el-icon>
|
|
|
</el-upload>
|
|
|
<div class="upload-hint text-gray-400 text-xs mt-2">
|
|
|
@@ -49,7 +59,14 @@
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="课程类型" prop="videoList">
|
|
|
- <el-button type="primary" size="small" @click="handleUploadVideo">
|
|
|
+ <input
|
|
|
+ ref="videoInputRef"
|
|
|
+ type="file"
|
|
|
+ accept=".mp4,.avi,.wmv,.mov,.flv,.rmvb,.3gp,.m4v,.mkv"
|
|
|
+ class="hidden"
|
|
|
+ @change="handleVideoFileChange"
|
|
|
+ />
|
|
|
+ <el-button type="primary" size="small" :loading="videoUploading" @click="handleUploadVideo">
|
|
|
<el-icon class="mr-1"><i-ep-plus /></el-icon>
|
|
|
上传视频
|
|
|
</el-button>
|
|
|
@@ -65,8 +82,8 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="视频大小" prop="size" width="100" />
|
|
|
- <el-table-column label="视频类型" prop="type" width="80" />
|
|
|
+ <el-table-column label="视频大小" prop="fileSize" width="100" />
|
|
|
+ <el-table-column label="视频类型" prop="fileType" width="80" />
|
|
|
<el-table-column label="视频时长" prop="duration" width="100" />
|
|
|
<el-table-column label="操作" width="80" align="center">
|
|
|
<template #default="scope">
|
|
|
@@ -91,26 +108,35 @@
|
|
|
|
|
|
<div class="flex flex-wrap items-center">
|
|
|
<el-form-item label="岗位级别" prop="jobLevel" required>
|
|
|
- <el-select v-model="form.jobLevel" multiple collapse-tags placeholder="请选择" style="width: 180px">
|
|
|
- <el-option label="A1" value="A1" />
|
|
|
- <el-option label="A2" value="A2" />
|
|
|
- <el-option label="B1" value="B1" />
|
|
|
- <el-option label="B2" value="B2" />
|
|
|
+ <el-select v-model="form.jobLevel" placeholder="请选择" style="width: 180px">
|
|
|
+ <el-option
|
|
|
+ v-for="item in jobLevelOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="岗位" prop="job" class="ml-4">
|
|
|
- <el-select v-model="form.job" placeholder="审计" style="width: 180px">
|
|
|
- <el-option label="审计" value="审计" />
|
|
|
- <el-option label="会计" value="会计" />
|
|
|
+ <el-select v-model="form.job" placeholder="请选择" style="width: 180px" filterable>
|
|
|
+ <el-option
|
|
|
+ v-for="item in jobOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</div>
|
|
|
|
|
|
<el-form-item label="岗位类型" prop="jobType" required>
|
|
|
<el-select v-model="form.jobType" placeholder="全职" style="width: 180px">
|
|
|
- <el-option label="全职" value="全职" />
|
|
|
- <el-option label="兼职" value="兼职" />
|
|
|
- <el-option label="实习" value="实习" />
|
|
|
+ <el-option
|
|
|
+ v-for="item in jobTypeOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
|
|
|
@@ -121,16 +147,18 @@
|
|
|
</template>
|
|
|
|
|
|
<template v-if="type === 'offline'">
|
|
|
- <el-form-item label="培训地址" prop="address" required>
|
|
|
- <div class="flex gap-2 mb-2">
|
|
|
- <el-select v-model="form.city" placeholder="上海市" style="width: 120px">
|
|
|
- <el-option label="上海市" value="shanghai" />
|
|
|
- </el-select>
|
|
|
- <el-select v-model="form.area" placeholder="黄浦区" style="width: 120px">
|
|
|
- <el-option label="黄浦区" value="huangpu" />
|
|
|
- </el-select>
|
|
|
+ <el-form-item label="培训地址" prop="addressDetail" required>
|
|
|
+ <div class="address-container">
|
|
|
+ <el-cascader
|
|
|
+ v-model="form.region"
|
|
|
+ :options="regionOptions"
|
|
|
+ :props="regionProps"
|
|
|
+ clearable
|
|
|
+ placeholder="省 / 市 / 区"
|
|
|
+ class="region-cascader"
|
|
|
+ />
|
|
|
+ <el-input v-model="form.addressDetail" placeholder="请输入具体地址" class="address-detail-input" />
|
|
|
</div>
|
|
|
- <el-input v-model="form.addressDetail" placeholder="请输入具体地址" style="width: 400px" />
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="培训时间" prop="timeRange" required>
|
|
|
@@ -164,7 +192,7 @@
|
|
|
<el-form-item label="标签" prop="tags">
|
|
|
<div class="flex items-center gap-2">
|
|
|
<el-tag
|
|
|
- v-for="tag in form.tags"
|
|
|
+ v-for="tag in tagList"
|
|
|
:key="tag"
|
|
|
closable
|
|
|
@close="handleCloseTag(tag)"
|
|
|
@@ -198,8 +226,13 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, onMounted, nextTick } from 'vue';
|
|
|
-import type { FormInstance, FormRules } from 'element-plus';
|
|
|
+import { ref, reactive, computed, onMounted, nextTick } from 'vue';
|
|
|
+import { ElMessage } from 'element-plus';
|
|
|
+import { regionData } from 'element-china-area-data';
|
|
|
+import type { FormInstance, FormRules, UploadInstance } from 'element-plus';
|
|
|
+import { uploadTrainingFile } from '../../../api/main/training/index';
|
|
|
+import { listPosition } from '../../../api/main/position/index';
|
|
|
+import { getDicts } from '../../../api/system/dict/data/index';
|
|
|
|
|
|
const props = defineProps<{
|
|
|
initialData?: any;
|
|
|
@@ -210,70 +243,272 @@ const emit = defineEmits(['cancel', 'submit']);
|
|
|
|
|
|
const formRef = ref<FormInstance>();
|
|
|
const form = reactive({
|
|
|
- id: '',
|
|
|
+ id: undefined as any,
|
|
|
name: '',
|
|
|
- thumbnail: '',
|
|
|
+ thumbnail: undefined as any,
|
|
|
+ thumbnailUrl: '',
|
|
|
description: '',
|
|
|
videoList: [] as any[],
|
|
|
- jobLevel: [] as string[],
|
|
|
+ jobLevel: '',
|
|
|
job: '',
|
|
|
jobType: '',
|
|
|
sortOrder: '',
|
|
|
- city: 'shanghai',
|
|
|
- area: 'huangpu',
|
|
|
+ city: '上海市',
|
|
|
+ area: '黄浦区',
|
|
|
+ region: ['上海市', '上海市', '黄浦区'] as string[],
|
|
|
addressDetail: '',
|
|
|
- timeRange: [],
|
|
|
- applyRange: [],
|
|
|
+ timeRange: [] as string[],
|
|
|
+ applyRange: [] as string[],
|
|
|
organizer: '',
|
|
|
- tags: [] as string[],
|
|
|
- status: true
|
|
|
+ tags: '',
|
|
|
+ status: 1
|
|
|
});
|
|
|
|
|
|
-const regionOptions = [
|
|
|
- {
|
|
|
- value: 'shanghai',
|
|
|
- label: '上海市',
|
|
|
- children: [
|
|
|
- { value: 'huangpu', label: '黄浦区' },
|
|
|
- { value: 'xuhui', label: '徐汇区' }
|
|
|
- ]
|
|
|
- }
|
|
|
-];
|
|
|
+/** 将 element-china-area-data 的数字编码转为文字作为 value */
|
|
|
+const convertRegionToLabelValue = (data: any[]): any[] => {
|
|
|
+ return data.map(item => ({
|
|
|
+ value: item.label,
|
|
|
+ label: item.label,
|
|
|
+ children: item.children ? convertRegionToLabelValue(item.children) : undefined
|
|
|
+ }));
|
|
|
+};
|
|
|
+
|
|
|
+const regionOptions = convertRegionToLabelValue(regionData);
|
|
|
+
|
|
|
+const regionProps = {
|
|
|
+ checkStrictly: false,
|
|
|
+ emitPath: true
|
|
|
+};
|
|
|
+
|
|
|
+/** 标签列表(从逗号分隔字符串解析) */
|
|
|
+const tagList = computed({
|
|
|
+ get: () => form.tags ? form.tags.split(',').filter(Boolean) : [],
|
|
|
+ set: (val: string[]) => { form.tags = val.join(','); }
|
|
|
+});
|
|
|
|
|
|
const inputTagVisible = ref(false);
|
|
|
const inputTagValue = ref('');
|
|
|
const inputTagRef = ref();
|
|
|
+const thumbnailInputRef = ref<HTMLInputElement>();
|
|
|
+const thumbnailUploadRef = ref<UploadInstance>();
|
|
|
+const videoInputRef = ref<HTMLInputElement>();
|
|
|
+const thumbnailUploading = ref(false);
|
|
|
+const videoUploading = ref(false);
|
|
|
+const jobLevelOptions = ref<{ label: string; value: string }[]>([]);
|
|
|
+const jobTypeOptions = ref<{ label: string; value: string }[]>([]);
|
|
|
+const jobOptions = ref<{ label: string; value: string }[]>([]);
|
|
|
|
|
|
const rules = reactive<FormRules>({
|
|
|
name: [{ required: true, message: '请输入培训名称', trigger: 'blur' }],
|
|
|
- thumbnail: [{ required: true, message: '请上传培训封面', trigger: 'change' }],
|
|
|
description: [{ required: true, message: '请输入描述', trigger: 'blur' }],
|
|
|
- videoList: [{ required: true, type: 'array', min: 1, message: '请上传视频', trigger: 'change' }],
|
|
|
- jobLevel: [{ required: true, type: 'array', min: 1, message: '请选择岗位级别', trigger: 'change' }],
|
|
|
+ jobLevel: [{ required: true, message: '请选择岗位级别', trigger: 'change' }],
|
|
|
job: [{ required: true, message: '请选择岗位', trigger: 'change' }],
|
|
|
jobType: [{ required: true, message: '请选择岗位类型', trigger: 'change' }],
|
|
|
- address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
|
|
|
+ thumbnail: [{ required: true, message: '请上传培训封面', trigger: 'change' }],
|
|
|
+ videoList: [{ required: true, type: 'array', min: 1, message: '请至少上传一个视频', trigger: 'change' }],
|
|
|
+ addressDetail: [{ required: true, message: '请输入地址', trigger: 'blur' }],
|
|
|
timeRange: [{ required: true, type: 'array', min: 2, message: '请选择培训时间', trigger: 'change' }],
|
|
|
applyRange: [{ required: true, type: 'array', min: 2, message: '请选择报名时间', trigger: 'change' }]
|
|
|
});
|
|
|
|
|
|
-onMounted(() => {
|
|
|
+const formatFileSize = (size: number) => {
|
|
|
+ if (size >= 1024 * 1024 * 1024) {
|
|
|
+ return `${(size / 1024 / 1024 / 1024).toFixed(2)}GB`;
|
|
|
+ }
|
|
|
+ if (size >= 1024 * 1024) {
|
|
|
+ return `${(size / 1024 / 1024).toFixed(1)}MB`;
|
|
|
+ }
|
|
|
+ if (size >= 1024) {
|
|
|
+ return `${(size / 1024).toFixed(1)}KB`;
|
|
|
+ }
|
|
|
+ return `${size}B`;
|
|
|
+};
|
|
|
+
|
|
|
+const formatDuration = (totalSeconds: number) => {
|
|
|
+ const hours = Math.floor(totalSeconds / 3600);
|
|
|
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
|
+ const seconds = totalSeconds % 60;
|
|
|
+ return [hours, minutes, seconds].map((item) => item.toString().padStart(2, '0')).join(':');
|
|
|
+};
|
|
|
+
|
|
|
+const parseDuration = (duration?: string) => {
|
|
|
+ if (!duration) return 0;
|
|
|
+ const parts = duration.split(':').map((item) => Number(item));
|
|
|
+ if (parts.length === 3) {
|
|
|
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
|
+ }
|
|
|
+ if (parts.length === 2) {
|
|
|
+ return parts[0] * 60 + parts[1];
|
|
|
+ }
|
|
|
+ return parts[0] || 0;
|
|
|
+};
|
|
|
+
|
|
|
+const calcTotalDuration = (videoList: any[]) => {
|
|
|
+ const totalSeconds = videoList.reduce((sum, item) => sum + parseDuration(item.duration), 0);
|
|
|
+ return totalSeconds > 0 ? formatDuration(totalSeconds) : '';
|
|
|
+};
|
|
|
+
|
|
|
+const getVideoDuration = (file: File) => new Promise<string>((resolve) => {
|
|
|
+ const video = document.createElement('video');
|
|
|
+ const objectUrl = URL.createObjectURL(file);
|
|
|
+ video.preload = 'metadata';
|
|
|
+ video.onloadedmetadata = () => {
|
|
|
+ const duration = Number.isFinite(video.duration) ? Math.round(video.duration) : 0;
|
|
|
+ URL.revokeObjectURL(objectUrl);
|
|
|
+ resolve(formatDuration(duration));
|
|
|
+ };
|
|
|
+ video.onerror = () => {
|
|
|
+ URL.revokeObjectURL(objectUrl);
|
|
|
+ resolve('00:00:00');
|
|
|
+ };
|
|
|
+ video.src = objectUrl;
|
|
|
+});
|
|
|
+
|
|
|
+const loadSelectOptions = async () => {
|
|
|
+ const [jobLevelRes, jobTypeRes, positionRes] = await Promise.all([
|
|
|
+ getDicts('main_position_level'),
|
|
|
+ getDicts('main_position_type'),
|
|
|
+ listPosition({ pageNum: 1, pageSize: 999 })
|
|
|
+ ]);
|
|
|
+ jobLevelOptions.value = (jobLevelRes.data || []).map((item: any) => ({
|
|
|
+ label: item.dictLabel,
|
|
|
+ value: item.dictLabel
|
|
|
+ }));
|
|
|
+ jobTypeOptions.value = (jobTypeRes.data || []).map((item: any) => ({
|
|
|
+ label: item.dictLabel,
|
|
|
+ value: item.dictLabel
|
|
|
+ }));
|
|
|
+ jobOptions.value = (positionRes.rows || []).map((item: any) => ({
|
|
|
+ label: item.postName,
|
|
|
+ value: item.postName
|
|
|
+ }));
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await loadSelectOptions();
|
|
|
if (props.initialData) {
|
|
|
- Object.assign(form, props.initialData);
|
|
|
- if (props.type === 'video' && !props.initialData.videoList) {
|
|
|
- form.videoList = [{ name: '西班牙系列课程1', size: '100.0MB', type: 'MP4', duration: '00:09:00' }];
|
|
|
+ // 编辑模式: 回显数据
|
|
|
+ form.id = props.initialData.id;
|
|
|
+ form.name = props.initialData.name || '';
|
|
|
+ form.thumbnail = props.initialData.thumbnail;
|
|
|
+ form.thumbnailUrl = props.initialData.thumbnailUrl || '';
|
|
|
+ form.description = props.initialData.description || '';
|
|
|
+ form.jobLevel = props.initialData.jobLevel || '';
|
|
|
+ form.job = props.initialData.job || '';
|
|
|
+ form.jobType = props.initialData.jobType || '';
|
|
|
+ form.sortOrder = props.initialData.sortOrder?.toString() || '';
|
|
|
+ form.status = props.initialData.status ?? 1;
|
|
|
+ form.organizer = props.initialData.organizer || '';
|
|
|
+ form.tags = props.initialData.tags || '';
|
|
|
+
|
|
|
+ // 视频列表
|
|
|
+ if (props.initialData.videoList && props.initialData.videoList.length > 0) {
|
|
|
+ form.videoList = props.initialData.videoList.map((v: any) => ({
|
|
|
+ id: v.id,
|
|
|
+ name: v.name,
|
|
|
+ fileSize: v.fileSize,
|
|
|
+ fileType: v.fileType,
|
|
|
+ duration: v.duration,
|
|
|
+ ossId: v.ossId,
|
|
|
+ fileUrl: v.fileUrl,
|
|
|
+ sortOrder: v.sortOrder
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 线下培训地址
|
|
|
+ form.city = props.initialData.city || '上海市';
|
|
|
+ form.area = props.initialData.area || '黄浦区';
|
|
|
+ form.region = ['上海市', form.city || '上海市', form.area || '黄浦区'];
|
|
|
+ form.addressDetail = props.initialData.addressDetail || '';
|
|
|
+
|
|
|
+ // 时间范围
|
|
|
+ if (props.initialData.trainingStartTime && props.initialData.trainingEndTime) {
|
|
|
+ form.timeRange = [props.initialData.trainingStartTime, props.initialData.trainingEndTime];
|
|
|
}
|
|
|
- } else {
|
|
|
- // Default values for new form
|
|
|
- if (props.type === 'video') {
|
|
|
- form.name = '西班牙语0-B2高级直达';
|
|
|
- form.thumbnail = 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=320&h=160&fit=crop';
|
|
|
+ if (props.initialData.applyStartTime && props.initialData.applyEndTime) {
|
|
|
+ form.applyRange = [props.initialData.applyStartTime, props.initialData.applyEndTime];
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+const handleUploadThumbnail = () => {
|
|
|
+ thumbnailInputRef.value?.click();
|
|
|
+};
|
|
|
+
|
|
|
+const handleThumbnailFileChange = async (event: Event) => {
|
|
|
+ const target = event.target as HTMLInputElement;
|
|
|
+ const rawFile = target.files?.[0];
|
|
|
+ if (!rawFile) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!['image/jpeg', 'image/png'].includes(rawFile.type)) {
|
|
|
+ ElMessage.error('请上传 JPG、PNG 格式图片');
|
|
|
+ target.value = '';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (rawFile.size / 1024 / 1024 > 10) {
|
|
|
+ ElMessage.error('图片不能超过10M');
|
|
|
+ target.value = '';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ thumbnailUploading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await uploadTrainingFile(rawFile);
|
|
|
+ form.thumbnail = res.data?.ossId;
|
|
|
+ form.thumbnailUrl = res.data?.url || URL.createObjectURL(rawFile);
|
|
|
+ formRef.value?.clearValidate('thumbnail');
|
|
|
+ ElMessage.success('封面上传成功');
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error('封面上传失败');
|
|
|
+ } finally {
|
|
|
+ target.value = '';
|
|
|
+ thumbnailUploading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
const handleUploadVideo = () => {
|
|
|
- form.videoList.push({ name: '新增视频' + (form.videoList.length + 1), size: '50.0MB', type: 'MP4', duration: '00:05:00' });
|
|
|
+ videoInputRef.value?.click();
|
|
|
+};
|
|
|
+
|
|
|
+const handleVideoFileChange = async (event: Event) => {
|
|
|
+ const target = event.target as HTMLInputElement;
|
|
|
+ const rawFile = target.files?.[0];
|
|
|
+ if (!rawFile) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const extension = rawFile.name.split('.').pop()?.toLowerCase() || '';
|
|
|
+ const allowTypes = ['mp4', 'avi', 'wmv', 'mov', 'flv', 'rmvb', '3gp', 'm4v', 'mkv'];
|
|
|
+ if (!allowTypes.includes(extension)) {
|
|
|
+ ElMessage.error('视频格式不支持');
|
|
|
+ target.value = '';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (rawFile.size / 1024 / 1024 / 1024 > 10) {
|
|
|
+ ElMessage.error('视频不能超过10G');
|
|
|
+ target.value = '';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ videoUploading.value = true;
|
|
|
+ try {
|
|
|
+ const duration = await getVideoDuration(rawFile);
|
|
|
+ const res = await uploadTrainingFile(rawFile);
|
|
|
+ form.videoList.push({
|
|
|
+ name: res.data?.originalName || res.data?.fileName || rawFile.name,
|
|
|
+ fileSize: formatFileSize(rawFile.size),
|
|
|
+ fileType: extension.toUpperCase(),
|
|
|
+ duration,
|
|
|
+ ossId: res.data?.ossId,
|
|
|
+ fileUrl: res.data?.url,
|
|
|
+ sortOrder: form.videoList.length + 1
|
|
|
+ });
|
|
|
+ formRef.value?.clearValidate('videoList');
|
|
|
+ ElMessage.success('视频上传成功');
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error('视频上传失败');
|
|
|
+ } finally {
|
|
|
+ target.value = '';
|
|
|
+ videoUploading.value = false;
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
const removeVideo = (index: number) => {
|
|
|
@@ -281,7 +516,8 @@ const removeVideo = (index: number) => {
|
|
|
};
|
|
|
|
|
|
const handleCloseTag = (tag: string) => {
|
|
|
- form.tags = form.tags.filter(t => t !== tag);
|
|
|
+ const list = tagList.value.filter(t => t !== tag);
|
|
|
+ tagList.value = list;
|
|
|
};
|
|
|
|
|
|
const showInputTag = () => {
|
|
|
@@ -293,7 +529,8 @@ const showInputTag = () => {
|
|
|
|
|
|
const handleInputTagConfirm = () => {
|
|
|
if (inputTagValue.value) {
|
|
|
- form.tags.push(inputTagValue.value);
|
|
|
+ const list = [...tagList.value, inputTagValue.value];
|
|
|
+ tagList.value = list;
|
|
|
}
|
|
|
inputTagVisible.value = false;
|
|
|
inputTagValue.value = '';
|
|
|
@@ -303,7 +540,59 @@ const handleSubmit = async () => {
|
|
|
if (!formRef.value) return;
|
|
|
await formRef.value.validate((valid) => {
|
|
|
if (valid) {
|
|
|
- emit('submit', { ...form });
|
|
|
+ if (props.type === 'offline') {
|
|
|
+ const [province, cityLevel, district] = form.region || [];
|
|
|
+ // 合并省+市到 city(直辖市跳过"市辖区")
|
|
|
+ const mergedCity = cityLevel && !cityLevel.includes('市辖区')
|
|
|
+ ? `${province}${cityLevel}`
|
|
|
+ : province;
|
|
|
+ form.city = mergedCity || '上海市';
|
|
|
+ form.area = district || '';
|
|
|
+ }
|
|
|
+ // 构造提交数据
|
|
|
+ const submitData: any = {
|
|
|
+ id: form.id,
|
|
|
+ name: form.name,
|
|
|
+ description: form.description,
|
|
|
+ thumbnail: form.thumbnail,
|
|
|
+ jobLevel: form.jobLevel,
|
|
|
+ job: form.job,
|
|
|
+ jobType: form.jobType,
|
|
|
+ status: form.status
|
|
|
+ };
|
|
|
+
|
|
|
+ if (props.type === 'video') {
|
|
|
+ submitData.sortOrder = form.sortOrder ? parseInt(form.sortOrder) : 0;
|
|
|
+ submitData.duration = calcTotalDuration(form.videoList);
|
|
|
+ submitData.videoList = form.videoList.map((v, i) => ({
|
|
|
+ id: v.id,
|
|
|
+ name: v.name,
|
|
|
+ ossId: v.ossId,
|
|
|
+ fileUrl: v.fileUrl,
|
|
|
+ fileSize: v.fileSize,
|
|
|
+ fileType: v.fileType,
|
|
|
+ duration: v.duration,
|
|
|
+ sortOrder: i + 1
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (props.type === 'offline') {
|
|
|
+ submitData.city = form.city;
|
|
|
+ submitData.area = form.area;
|
|
|
+ submitData.addressDetail = form.addressDetail;
|
|
|
+ submitData.organizer = form.organizer;
|
|
|
+ submitData.tags = form.tags;
|
|
|
+ if (form.timeRange && form.timeRange.length === 2) {
|
|
|
+ submitData.trainingStartTime = form.timeRange[0];
|
|
|
+ submitData.trainingEndTime = form.timeRange[1];
|
|
|
+ }
|
|
|
+ if (form.applyRange && form.applyRange.length === 2) {
|
|
|
+ submitData.applyStartTime = form.applyRange[0];
|
|
|
+ submitData.applyEndTime = form.applyRange[1];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ emit('submit', submitData);
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
@@ -390,10 +679,27 @@ const handleSubmit = async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ .address-container {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+ width: 100%;
|
|
|
+ max-width: 720px;
|
|
|
+
|
|
|
+ .region-cascader {
|
|
|
+ width: 240px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .address-detail-input {
|
|
|
+ width: 400px;
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
:deep(.el-form-item__label) {
|
|
|
color: #4e5969;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
-
|