TrainForm.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. <template>
  2. <div class="train-form bg-white p-6 rounded h-full overflow-y-auto">
  3. <div class="form-breadcrumb mb-4 text-gray-400 text-xs">
  4. 课程管理 / 培训课程 / {{ initialData ? '编辑' : '新增' }}{{ type === 'video' ? '视频' : type === 'live' ? '直播' : '线下' }}培训
  5. </div>
  6. <div class="form-header mb-8">
  7. <div class="text-xl font-bold">{{ initialData ? '编辑' : '新增' }}{{ type === 'video' ? '视频培训' : type === 'live' ? '直播培训' : '线下培训' }}</div>
  8. </div>
  9. <div class="form-wrapper border-2 border-blue-500 rounded-lg p-10 bg-white relative">
  10. <div class="section-title font-bold mb-8 text-base">基础信息</div>
  11. <el-form
  12. ref="formRef"
  13. :model="form"
  14. :rules="rules"
  15. label-width="100px"
  16. label-position="right"
  17. >
  18. <el-form-item label="培训名称" prop="name" required>
  19. <el-input v-model="form.name" placeholder="请输入培训名称" style="width: 400px" />
  20. </el-form-item>
  21. <template v-if="type === 'video'">
  22. <el-form-item label="培训封面" prop="thumbnail" required>
  23. <div class="thumbnail-uploader">
  24. <input
  25. ref="thumbnailInputRef"
  26. type="file"
  27. accept=".jpg,.jpeg,.png"
  28. class="hidden"
  29. @change="handleThumbnailFileChange"
  30. />
  31. <el-upload
  32. ref="thumbnailUploadRef"
  33. class="avatar-uploader"
  34. action="#"
  35. :auto-upload="false"
  36. :show-file-list="false"
  37. accept=".jpg,.jpeg,.png"
  38. @click="handleUploadThumbnail"
  39. >
  40. <img v-if="form.thumbnailUrl" :src="form.thumbnailUrl" class="avatar-preview" />
  41. <el-icon v-else class="avatar-uploader-icon"><i-ep-plus /></el-icon>
  42. </el-upload>
  43. <div class="upload-hint text-gray-400 text-xs mt-2">
  44. 建议尺寸640*320,小于10M的JPG、PNG格式图片
  45. </div>
  46. </div>
  47. </el-form-item>
  48. <el-form-item label="课程描述" prop="description">
  49. <el-input
  50. v-model="form.description"
  51. type="textarea"
  52. :rows="4"
  53. placeholder="从零开始学西班牙语,直达欧标B2水平!"
  54. style="width: 500px"
  55. />
  56. </el-form-item>
  57. <el-form-item label="课程类型" prop="videoList">
  58. <input
  59. ref="videoInputRef"
  60. type="file"
  61. accept=".mp4,.avi,.wmv,.mov,.flv,.rmvb,.3gp,.m4v,.mkv"
  62. class="hidden"
  63. @change="handleVideoFileChange"
  64. />
  65. <el-button type="primary" size="small" :loading="videoUploading" @click="handleUploadVideo">
  66. <el-icon class="mr-1"><i-ep-plus /></el-icon>
  67. 上传视频
  68. </el-button>
  69. <div class="upload-hint text-gray-400 text-xs mt-1 mb-2">
  70. 支持mp4、avi、wmv、mov、flv、rmvb、3gp、m4v、mkv格式;单个文件最大不超过10G
  71. </div>
  72. <el-table :data="form.videoList" size="small" class="video-table mt-2 border">
  73. <el-table-column label="视频信息" prop="name" min-width="150">
  74. <template #default="scope">
  75. <div class="flex items-center">
  76. <el-icon class="mr-2 text-blue-500"><i-ep-video-play /></el-icon>
  77. <span class="text-blue-500 underline cursor-pointer">{{ scope.row.name }}</span>
  78. </div>
  79. </template>
  80. </el-table-column>
  81. <el-table-column label="视频大小" prop="fileSize" width="100" />
  82. <el-table-column label="视频类型" prop="fileType" width="80" />
  83. <el-table-column label="视频时长" prop="duration" width="100" />
  84. <el-table-column label="操作" width="80" align="center">
  85. <template #default="scope">
  86. <el-link type="primary" :underline="false" @click="removeVideo(scope.$index)">删除</el-link>
  87. </template>
  88. </el-table-column>
  89. </el-table>
  90. </el-form-item>
  91. </template>
  92. <template v-else-if="type === 'offline'">
  93. <el-form-item label="培训描述" prop="description">
  94. <el-input
  95. v-model="form.description"
  96. type="textarea"
  97. :rows="4"
  98. placeholder="从零开始学西班牙语,直达欧标B2水平!"
  99. style="width: 500px"
  100. />
  101. </el-form-item>
  102. </template>
  103. <div class="flex flex-wrap items-center">
  104. <el-form-item label="岗位级别" prop="jobLevel" required>
  105. <el-select v-model="form.jobLevel" placeholder="请选择" style="width: 180px">
  106. <el-option
  107. v-for="item in jobLevelOptions"
  108. :key="item.value"
  109. :label="item.label"
  110. :value="item.value"
  111. />
  112. </el-select>
  113. </el-form-item>
  114. <el-form-item label="岗位" prop="job" class="ml-4">
  115. <el-select v-model="form.job" placeholder="请选择" style="width: 180px" filterable>
  116. <el-option
  117. v-for="item in jobOptions"
  118. :key="item.value"
  119. :label="item.label"
  120. :value="item.value"
  121. />
  122. </el-select>
  123. </el-form-item>
  124. </div>
  125. <el-form-item label="岗位类型" prop="jobType" required>
  126. <el-select v-model="form.jobType" placeholder="全职" style="width: 180px">
  127. <el-option
  128. v-for="item in jobTypeOptions"
  129. :key="item.value"
  130. :label="item.label"
  131. :value="item.value"
  132. />
  133. </el-select>
  134. </el-form-item>
  135. <template v-if="type === 'video'">
  136. <el-form-item label="排序号" prop="sortOrder">
  137. <el-input v-model="form.sortOrder" placeholder="请输入" style="width: 320px" />
  138. </el-form-item>
  139. </template>
  140. <template v-if="type === 'offline'">
  141. <el-form-item label="培训地址" prop="addressDetail" required>
  142. <div class="address-container">
  143. <el-cascader
  144. v-model="form.region"
  145. :options="regionOptions"
  146. :props="regionProps"
  147. clearable
  148. placeholder="省 / 市 / 区"
  149. class="region-cascader"
  150. />
  151. <el-input v-model="form.addressDetail" placeholder="请输入具体地址" class="address-detail-input" />
  152. </div>
  153. </el-form-item>
  154. <el-form-item label="培训时间" prop="timeRange" required>
  155. <el-date-picker
  156. v-model="form.timeRange"
  157. type="datetimerange"
  158. range-separator="-"
  159. start-placeholder="YYYY-MM-DD HH:mm:ss"
  160. end-placeholder="YYYY-MM-DD HH:mm:ss"
  161. value-format="YYYY-MM-DD HH:mm:ss"
  162. style="width: 400px"
  163. />
  164. </el-form-item>
  165. <el-form-item label="报名时间" prop="applyRange" required>
  166. <el-date-picker
  167. v-model="form.applyRange"
  168. type="datetimerange"
  169. range-separator="-"
  170. start-placeholder="YYYY-MM-DD HH:mm:ss"
  171. end-placeholder="YYYY-MM-DD HH:mm:ss"
  172. value-format="YYYY-MM-DD HH:mm:ss"
  173. style="width: 400px"
  174. />
  175. </el-form-item>
  176. <el-form-item label="主办单位" prop="organizer">
  177. <el-input v-model="form.organizer" placeholder="主办单位AAAAA" style="width: 320px" />
  178. </el-form-item>
  179. <el-form-item label="标签" prop="tags">
  180. <div class="flex items-center gap-2">
  181. <el-tag
  182. v-for="tag in tagList"
  183. :key="tag"
  184. closable
  185. @close="handleCloseTag(tag)"
  186. >
  187. {{ tag }}
  188. </el-tag>
  189. <el-input
  190. v-if="inputTagVisible"
  191. ref="inputTagRef"
  192. v-model="inputTagValue"
  193. size="small"
  194. style="width: 80px"
  195. @keyup.enter="handleInputTagConfirm"
  196. @blur="handleInputTagConfirm"
  197. />
  198. <el-button v-else size="small" @click="showInputTag">
  199. <el-icon><i-ep-plus /></el-icon>
  200. 标签
  201. </el-button>
  202. </div>
  203. </el-form-item>
  204. </template>
  205. <div class="flex gap-6 mt-12">
  206. <el-button type="primary" class="submit-btn" @click="handleSubmit">提交</el-button>
  207. <el-button class="back-btn" @click="$emit('cancel')">返回</el-button>
  208. </div>
  209. </el-form>
  210. </div>
  211. </div>
  212. </template>
  213. <script setup lang="ts">
  214. import { ref, reactive, computed, onMounted, nextTick } from 'vue';
  215. import { ElMessage } from 'element-plus';
  216. import { regionData } from 'element-china-area-data';
  217. import type { FormInstance, FormRules, UploadInstance } from 'element-plus';
  218. import { uploadTrainingFile } from '../../../api/main/training/index';
  219. import { listPosition } from '../../../api/main/position/index';
  220. import { getDicts } from '../../../api/system/dict/data/index';
  221. const props = defineProps<{
  222. initialData?: any;
  223. type: string;
  224. }>();
  225. const emit = defineEmits(['cancel', 'submit']);
  226. const formRef = ref<FormInstance>();
  227. const form = reactive({
  228. id: undefined as any,
  229. name: '',
  230. thumbnail: undefined as any,
  231. thumbnailUrl: '',
  232. description: '',
  233. videoList: [] as any[],
  234. jobLevel: '',
  235. job: '',
  236. jobType: '',
  237. sortOrder: '',
  238. city: '上海市',
  239. area: '黄浦区',
  240. region: ['上海市', '上海市', '黄浦区'] as string[],
  241. addressDetail: '',
  242. timeRange: [] as string[],
  243. applyRange: [] as string[],
  244. organizer: '',
  245. tags: '',
  246. status: 1
  247. });
  248. /** 将 element-china-area-data 的数字编码转为文字作为 value */
  249. const convertRegionToLabelValue = (data: any[]): any[] => {
  250. return data.map(item => ({
  251. value: item.label,
  252. label: item.label,
  253. children: item.children ? convertRegionToLabelValue(item.children) : undefined
  254. }));
  255. };
  256. const regionOptions = convertRegionToLabelValue(regionData);
  257. const regionProps = {
  258. checkStrictly: false,
  259. emitPath: true
  260. };
  261. /** 标签列表(从逗号分隔字符串解析) */
  262. const tagList = computed({
  263. get: () => form.tags ? form.tags.split(',').filter(Boolean) : [],
  264. set: (val: string[]) => { form.tags = val.join(','); }
  265. });
  266. const inputTagVisible = ref(false);
  267. const inputTagValue = ref('');
  268. const inputTagRef = ref();
  269. const thumbnailInputRef = ref<HTMLInputElement>();
  270. const thumbnailUploadRef = ref<UploadInstance>();
  271. const videoInputRef = ref<HTMLInputElement>();
  272. const thumbnailUploading = ref(false);
  273. const videoUploading = ref(false);
  274. const jobLevelOptions = ref<{ label: string; value: string }[]>([]);
  275. const jobTypeOptions = ref<{ label: string; value: string }[]>([]);
  276. const jobOptions = ref<{ label: string; value: string }[]>([]);
  277. const rules = reactive<FormRules>({
  278. name: [{ required: true, message: '请输入培训名称', trigger: 'blur' }],
  279. description: [{ required: true, message: '请输入描述', trigger: 'blur' }],
  280. jobLevel: [{ required: true, message: '请选择岗位级别', trigger: 'change' }],
  281. job: [{ required: true, message: '请选择岗位', trigger: 'change' }],
  282. jobType: [{ required: true, message: '请选择岗位类型', trigger: 'change' }],
  283. thumbnail: [{ required: true, message: '请上传培训封面', trigger: 'change' }],
  284. videoList: [{ required: true, type: 'array', min: 1, message: '请至少上传一个视频', trigger: 'change' }],
  285. addressDetail: [{ required: true, message: '请输入地址', trigger: 'blur' }],
  286. timeRange: [{ required: true, type: 'array', min: 2, message: '请选择培训时间', trigger: 'change' }],
  287. applyRange: [{ required: true, type: 'array', min: 2, message: '请选择报名时间', trigger: 'change' }]
  288. });
  289. const formatFileSize = (size: number) => {
  290. if (size >= 1024 * 1024 * 1024) {
  291. return `${(size / 1024 / 1024 / 1024).toFixed(2)}GB`;
  292. }
  293. if (size >= 1024 * 1024) {
  294. return `${(size / 1024 / 1024).toFixed(1)}MB`;
  295. }
  296. if (size >= 1024) {
  297. return `${(size / 1024).toFixed(1)}KB`;
  298. }
  299. return `${size}B`;
  300. };
  301. const formatDuration = (totalSeconds: number) => {
  302. const hours = Math.floor(totalSeconds / 3600);
  303. const minutes = Math.floor((totalSeconds % 3600) / 60);
  304. const seconds = totalSeconds % 60;
  305. return [hours, minutes, seconds].map((item) => item.toString().padStart(2, '0')).join(':');
  306. };
  307. const parseDuration = (duration?: string) => {
  308. if (!duration) return 0;
  309. const parts = duration.split(':').map((item) => Number(item));
  310. if (parts.length === 3) {
  311. return parts[0] * 3600 + parts[1] * 60 + parts[2];
  312. }
  313. if (parts.length === 2) {
  314. return parts[0] * 60 + parts[1];
  315. }
  316. return parts[0] || 0;
  317. };
  318. const calcTotalDuration = (videoList: any[]) => {
  319. const totalSeconds = videoList.reduce((sum, item) => sum + parseDuration(item.duration), 0);
  320. return totalSeconds > 0 ? formatDuration(totalSeconds) : '';
  321. };
  322. const getVideoDuration = (file: File) => new Promise<string>((resolve) => {
  323. const video = document.createElement('video');
  324. const objectUrl = URL.createObjectURL(file);
  325. video.preload = 'metadata';
  326. video.onloadedmetadata = () => {
  327. const duration = Number.isFinite(video.duration) ? Math.round(video.duration) : 0;
  328. URL.revokeObjectURL(objectUrl);
  329. resolve(formatDuration(duration));
  330. };
  331. video.onerror = () => {
  332. URL.revokeObjectURL(objectUrl);
  333. resolve('00:00:00');
  334. };
  335. video.src = objectUrl;
  336. });
  337. const loadSelectOptions = async () => {
  338. const [jobLevelRes, jobTypeRes, positionRes] = await Promise.all([
  339. getDicts('main_position_level'),
  340. getDicts('main_position_type'),
  341. listPosition({ pageNum: 1, pageSize: 999 })
  342. ]);
  343. jobLevelOptions.value = (jobLevelRes.data || []).map((item: any) => ({
  344. label: item.dictLabel,
  345. value: item.dictLabel
  346. }));
  347. jobTypeOptions.value = (jobTypeRes.data || []).map((item: any) => ({
  348. label: item.dictLabel,
  349. value: item.dictLabel
  350. }));
  351. jobOptions.value = (positionRes.rows || []).map((item: any) => ({
  352. label: item.postName,
  353. value: item.postName
  354. }));
  355. };
  356. onMounted(async () => {
  357. await loadSelectOptions();
  358. if (props.initialData) {
  359. // 编辑模式: 回显数据
  360. form.id = props.initialData.id;
  361. form.name = props.initialData.name || '';
  362. form.thumbnail = props.initialData.thumbnail;
  363. form.thumbnailUrl = props.initialData.thumbnailUrl || '';
  364. form.description = props.initialData.description || '';
  365. form.jobLevel = props.initialData.jobLevel || '';
  366. form.job = props.initialData.job || '';
  367. form.jobType = props.initialData.jobType || '';
  368. form.sortOrder = props.initialData.sortOrder?.toString() || '';
  369. form.status = props.initialData.status ?? 1;
  370. form.organizer = props.initialData.organizer || '';
  371. form.tags = props.initialData.tags || '';
  372. // 视频列表
  373. if (props.initialData.videoList && props.initialData.videoList.length > 0) {
  374. form.videoList = props.initialData.videoList.map((v: any) => ({
  375. id: v.id,
  376. name: v.name,
  377. fileSize: v.fileSize,
  378. fileType: v.fileType,
  379. duration: v.duration,
  380. ossId: v.ossId,
  381. fileUrl: v.fileUrl,
  382. sortOrder: v.sortOrder
  383. }));
  384. }
  385. // 线下培训地址
  386. form.city = props.initialData.city || '上海市';
  387. form.area = props.initialData.area || '黄浦区';
  388. form.region = ['上海市', form.city || '上海市', form.area || '黄浦区'];
  389. form.addressDetail = props.initialData.addressDetail || '';
  390. // 时间范围
  391. if (props.initialData.trainingStartTime && props.initialData.trainingEndTime) {
  392. form.timeRange = [props.initialData.trainingStartTime, props.initialData.trainingEndTime];
  393. }
  394. if (props.initialData.applyStartTime && props.initialData.applyEndTime) {
  395. form.applyRange = [props.initialData.applyStartTime, props.initialData.applyEndTime];
  396. }
  397. }
  398. });
  399. const handleUploadThumbnail = () => {
  400. thumbnailInputRef.value?.click();
  401. };
  402. const handleThumbnailFileChange = async (event: Event) => {
  403. const target = event.target as HTMLInputElement;
  404. const rawFile = target.files?.[0];
  405. if (!rawFile) {
  406. return;
  407. }
  408. if (!['image/jpeg', 'image/png'].includes(rawFile.type)) {
  409. ElMessage.error('请上传 JPG、PNG 格式图片');
  410. target.value = '';
  411. return;
  412. }
  413. if (rawFile.size / 1024 / 1024 > 10) {
  414. ElMessage.error('图片不能超过10M');
  415. target.value = '';
  416. return;
  417. }
  418. thumbnailUploading.value = true;
  419. try {
  420. const res = await uploadTrainingFile(rawFile);
  421. form.thumbnail = res.data?.ossId;
  422. form.thumbnailUrl = res.data?.url || URL.createObjectURL(rawFile);
  423. formRef.value?.clearValidate('thumbnail');
  424. ElMessage.success('封面上传成功');
  425. } catch (error) {
  426. ElMessage.error('封面上传失败');
  427. } finally {
  428. target.value = '';
  429. thumbnailUploading.value = false;
  430. }
  431. };
  432. const handleUploadVideo = () => {
  433. videoInputRef.value?.click();
  434. };
  435. const handleVideoFileChange = async (event: Event) => {
  436. const target = event.target as HTMLInputElement;
  437. const rawFile = target.files?.[0];
  438. if (!rawFile) {
  439. return;
  440. }
  441. const extension = rawFile.name.split('.').pop()?.toLowerCase() || '';
  442. const allowTypes = ['mp4', 'avi', 'wmv', 'mov', 'flv', 'rmvb', '3gp', 'm4v', 'mkv'];
  443. if (!allowTypes.includes(extension)) {
  444. ElMessage.error('视频格式不支持');
  445. target.value = '';
  446. return;
  447. }
  448. if (rawFile.size / 1024 / 1024 / 1024 > 10) {
  449. ElMessage.error('视频不能超过10G');
  450. target.value = '';
  451. return;
  452. }
  453. videoUploading.value = true;
  454. try {
  455. const duration = await getVideoDuration(rawFile);
  456. const res = await uploadTrainingFile(rawFile);
  457. form.videoList.push({
  458. name: res.data?.originalName || res.data?.fileName || rawFile.name,
  459. fileSize: formatFileSize(rawFile.size),
  460. fileType: extension.toUpperCase(),
  461. duration,
  462. ossId: res.data?.ossId,
  463. fileUrl: res.data?.url,
  464. sortOrder: form.videoList.length + 1
  465. });
  466. formRef.value?.clearValidate('videoList');
  467. ElMessage.success('视频上传成功');
  468. } catch (error) {
  469. ElMessage.error('视频上传失败');
  470. } finally {
  471. target.value = '';
  472. videoUploading.value = false;
  473. }
  474. };
  475. const removeVideo = (index: number) => {
  476. form.videoList.splice(index, 1);
  477. };
  478. const handleCloseTag = (tag: string) => {
  479. const list = tagList.value.filter(t => t !== tag);
  480. tagList.value = list;
  481. };
  482. const showInputTag = () => {
  483. inputTagVisible.value = true;
  484. nextTick(() => {
  485. inputTagRef.value?.focus();
  486. });
  487. };
  488. const handleInputTagConfirm = () => {
  489. if (inputTagValue.value) {
  490. const list = [...tagList.value, inputTagValue.value];
  491. tagList.value = list;
  492. }
  493. inputTagVisible.value = false;
  494. inputTagValue.value = '';
  495. };
  496. const handleSubmit = async () => {
  497. if (!formRef.value) return;
  498. await formRef.value.validate((valid) => {
  499. if (valid) {
  500. if (props.type === 'offline') {
  501. const [province, cityLevel, district] = form.region || [];
  502. // 合并省+市到 city(直辖市跳过"市辖区")
  503. const mergedCity = cityLevel && !cityLevel.includes('市辖区')
  504. ? `${province}${cityLevel}`
  505. : province;
  506. form.city = mergedCity || '上海市';
  507. form.area = district || '';
  508. }
  509. // 构造提交数据
  510. const submitData: any = {
  511. id: form.id,
  512. name: form.name,
  513. description: form.description,
  514. thumbnail: form.thumbnail,
  515. jobLevel: form.jobLevel,
  516. job: form.job,
  517. jobType: form.jobType,
  518. status: form.status
  519. };
  520. if (props.type === 'video') {
  521. submitData.sortOrder = form.sortOrder ? parseInt(form.sortOrder) : 0;
  522. submitData.duration = calcTotalDuration(form.videoList);
  523. submitData.videoList = form.videoList.map((v, i) => ({
  524. id: v.id,
  525. name: v.name,
  526. ossId: v.ossId,
  527. fileUrl: v.fileUrl,
  528. fileSize: v.fileSize,
  529. fileType: v.fileType,
  530. duration: v.duration,
  531. sortOrder: i + 1
  532. }));
  533. }
  534. if (props.type === 'offline') {
  535. submitData.city = form.city;
  536. submitData.area = form.area;
  537. submitData.addressDetail = form.addressDetail;
  538. submitData.organizer = form.organizer;
  539. submitData.tags = form.tags;
  540. if (form.timeRange && form.timeRange.length === 2) {
  541. submitData.trainingStartTime = form.timeRange[0];
  542. submitData.trainingEndTime = form.timeRange[1];
  543. }
  544. if (form.applyRange && form.applyRange.length === 2) {
  545. submitData.applyStartTime = form.applyRange[0];
  546. submitData.applyEndTime = form.applyRange[1];
  547. }
  548. }
  549. emit('submit', submitData);
  550. }
  551. });
  552. };
  553. </script>
  554. <style scoped lang="scss">
  555. .train-form {
  556. background-color: #f0f2f5;
  557. min-height: calc(100vh - 84px);
  558. padding: 20px 40px;
  559. @media (max-width: 768px) {
  560. padding: 10px;
  561. }
  562. .form-breadcrumb {
  563. margin-bottom: 20px;
  564. }
  565. .form-header {
  566. margin-bottom: 30px;
  567. }
  568. .form-wrapper {
  569. max-width: 1000px;
  570. margin: 0 auto;
  571. background-color: #fff;
  572. padding: 40px 60px;
  573. border: 2px solid #409eff;
  574. border-radius: 4px;
  575. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
  576. @media (max-width: 768px) {
  577. padding: 20px;
  578. }
  579. }
  580. .submit-btn {
  581. padding: 0 30px;
  582. height: 36px;
  583. background-color: #409eff;
  584. border-color: #409eff;
  585. }
  586. .back-btn {
  587. padding: 0 30px;
  588. height: 36px;
  589. color: #606266;
  590. }
  591. .thumbnail-uploader {
  592. .avatar-uploader {
  593. width: 160px;
  594. height: 80px;
  595. border: 1px dashed #d9d9d9;
  596. border-radius: 4px;
  597. cursor: pointer;
  598. position: relative;
  599. overflow: hidden;
  600. display: flex;
  601. align-items: center;
  602. justify-content: center;
  603. &:hover {
  604. border-color: #409eff;
  605. }
  606. }
  607. .avatar-preview {
  608. width: 100%;
  609. height: 100%;
  610. object-fit: cover;
  611. }
  612. .avatar-uploader-icon {
  613. font-size: 20px;
  614. color: #8c939d;
  615. }
  616. }
  617. .video-table {
  618. :deep(.el-table__header) {
  619. background-color: #f7f8fa;
  620. }
  621. }
  622. .address-container {
  623. display: flex;
  624. align-items: flex-start;
  625. gap: 12px;
  626. width: 100%;
  627. max-width: 720px;
  628. .region-cascader {
  629. width: 240px;
  630. flex-shrink: 0;
  631. }
  632. .address-detail-input {
  633. width: 400px;
  634. flex: 1;
  635. }
  636. }
  637. :deep(.el-form-item__label) {
  638. color: #4e5969;
  639. font-weight: 500;
  640. }
  641. }
  642. </style>