detail.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. <template>
  2. <div class="assessment-edit-wrapper">
  3. <!-- 可滚动内容区域 -->
  4. <div class="scrollable-content">
  5. <div class="form-container">
  6. <el-form v-if="!isFinished" ref="formRef" :model="formData" :rules="rules" label-position="top" :disabled="isView">
  7. <!-- 基本信息 -->
  8. <div class="form-section">
  9. <div class="section-title">基本信息</div>
  10. <el-row :gutter="40">
  11. <el-col :span="16">
  12. <el-form-item label="测评名称" prop="evaluationName">
  13. <el-input v-model="formData.evaluationName" placeholder="请输入名称" maxlength="100" show-word-limit />
  14. <div class="form-tip">最多可输入100个字符</div>
  15. </el-form-item>
  16. <el-form-item label="级别" prop="grade">
  17. <el-select v-model="formData.grade" placeholder="请选择" style="width: 100%">
  18. <el-option
  19. v-for="dict in main_position_level"
  20. :key="dict.value"
  21. :label="dict.label"
  22. :value="dict.value"
  23. />
  24. </el-select>
  25. </el-form-item>
  26. <el-form-item label="岗位" prop="positionId">
  27. <el-select
  28. v-model="formData.positionId"
  29. placeholder="请选择"
  30. style="width: 100%"
  31. filterable
  32. >
  33. <el-option
  34. v-for="item in positionOptions"
  35. :key="item.id"
  36. :label="item.postName"
  37. :value="item.id"
  38. />
  39. </el-select>
  40. </el-form-item>
  41. <el-form-item label="岗位类型" prop="positionType">
  42. <!-- --> <el-select v-model="formData.positionType" placeholder="请选择" style="width: 100%">
  43. <el-option
  44. v-for="dict in main_position_type"
  45. :key="dict.value"
  46. :label="dict.label"
  47. :value="dict.value"
  48. />
  49. </el-select>
  50. </el-form-item>
  51. <el-form-item label="详情描述" prop="detail">
  52. <editor v-model="formData.detail" :min-height="120" :height="150" placeholder="请输入详细描述,支持图文混排..." />
  53. </el-form-item>
  54. <el-form-item label="标签" prop="tags">
  55. <el-select
  56. v-model="formData.tags"
  57. multiple
  58. placeholder="请选择标签"
  59. style="width: 100%"
  60. >
  61. <el-option
  62. v-for="tag in tagOptions"
  63. :key="tag.id"
  64. :label="tag.name"
  65. :value="tag.name"
  66. />
  67. </el-select>
  68. </el-form-item>
  69. </el-col>
  70. <el-col :span="8">
  71. <el-form-item label="商品主图" prop="mainImage">
  72. <image-upload v-model="formData.mainImage" :limit="1" />
  73. <div class="upload-tip" v-if="!isView">点击上传或拖拽图片至此处<br/>支持JPG、PNG格式,建议尺寸800*800px</div>
  74. </el-form-item>
  75. <el-form-item label="商品相册" prop="imageAlbum">
  76. <image-upload v-model="formData.imageAlbum" :limit="10" />
  77. <div class="upload-tip" v-if="!isView">最多可上传10张图片,这些图片将在小程序详情页顶部轮播展示</div>
  78. </el-form-item>
  79. </el-col>
  80. </el-row>
  81. </div>
  82. <!-- 能力配置(作答信息) -->
  83. <div class="form-section">
  84. <div class="section-title">能力配置</div>
  85. <div class="ability-list">
  86. <div v-for="(item, index) in formData.abilityConfigs" :key="index" class="ability-item-card">
  87. <el-row :gutter="20">
  88. <el-col :span="6">
  89. <el-form-item :label="'能力名称'" :prop="'abilityConfigs.' + index + '.abilityName'" :rules="{ required: true, message: '请输入能力名称', trigger: 'blur' }">
  90. <el-input v-model="item.abilityName" placeholder="如:能力A" />
  91. </el-form-item>
  92. </el-col>
  93. <el-col :span="7">
  94. <el-form-item :label="'关联试卷'" :prop="'abilityConfigs.' + index + '.thirdExamInfoId'" :rules="{ required: true, message: '请选择试卷', trigger: 'change' }">
  95. <el-input
  96. v-model="item.thirdExamName"
  97. placeholder="点击选择试卷"
  98. readonly
  99. @click="!isView && showExamDialog(index)"
  100. >
  101. <template #append v-if="!isView">
  102. <el-button @click="showExamDialog(index)">选择</el-button>
  103. </template>
  104. </el-input>
  105. </el-form-item>
  106. </el-col>
  107. <el-col :span="3">
  108. <el-form-item :label="'时长(分)'" :prop="'abilityConfigs.' + index + '.thirdExamTime'">
  109. <el-input-number v-model="item.thirdExamTime" :min="1" controls-position="right" style="width: 100%" />
  110. </el-form-item>
  111. </el-col>
  112. <el-col :span="4">
  113. <el-form-item :label="'总分'" :prop="'abilityConfigs.' + index + '.thirdExamTotalScore'">
  114. <el-input-number v-model="item.thirdExamTotalScore" :min="0" controls-position="right" style="width: 100%" />
  115. </el-form-item>
  116. </el-col>
  117. <el-col :span="4">
  118. <el-form-item :label="'及格分'" :prop="'abilityConfigs.' + index + '.thirdExamPassMark'">
  119. <el-input-number v-model="item.thirdExamPassMark" :min="0" :max="item.thirdExamTotalScore || 100" controls-position="right" style="width: 100%" />
  120. </el-form-item>
  121. </el-col>
  122. </el-row>
  123. </div>
  124. </div>
  125. </div>
  126. <!-- 价格与上架 -->
  127. <div class="form-section no-border">
  128. <div class="section-title">价格与上架</div>
  129. <el-row :gutter="40">
  130. <el-col :span="12">
  131. <el-form-item label="销售价" prop="price">
  132. <el-input v-model="formData.price" placeholder="请输入价格">
  133. <template #prefix>¥</template>
  134. </el-input>
  135. </el-form-item>
  136. </el-col>
  137. <el-col :span="12">
  138. <el-form-item label="上架时间" prop="onlineStatus">
  139. <div class="online-status-wrapper">
  140. <el-radio-group v-model="formData.onlineStatus">
  141. <el-radio label="1">立即上架</el-radio>
  142. <el-radio label="2">定时上架</el-radio>
  143. <el-radio label="3">暂不上架</el-radio>
  144. </el-radio-group>
  145. <div v-if="formData.onlineStatus === '2'" class="date-picker-inline">
  146. <el-date-picker
  147. v-model="formData.onlineTimeRange"
  148. type="datetimerange"
  149. range-separator="至"
  150. start-placeholder="开始时间"
  151. end-placeholder="结束时间"
  152. value-format="YYYY-MM-DD HH:mm:ss"
  153. />
  154. </div>
  155. </div>
  156. </el-form-item>
  157. </el-col>
  158. </el-row>
  159. </div>
  160. </el-form>
  161. <!-- 成功状态 -->
  162. <div v-else class="success-content">
  163. <div class="success-center">
  164. <el-icon :size="80" color="#67C23A" style="margin-bottom: 20px">
  165. <SuccessFilled />
  166. </el-icon>
  167. <div class="success-text">{{ isEdit ? '修改成功' : '创建成功' }}</div>
  168. <el-button type="primary" @click="goBack" class="finish-btn">完成</el-button>
  169. </div>
  170. </div>
  171. </div>
  172. </div>
  173. <!-- 固定底部 -->
  174. <div v-if="!isFinished" class="fixed-footer">
  175. <el-button @click="goBack">{{ isView ? '返回' : '取消' }}</el-button>
  176. <el-button v-if="!isView" @click="handleSubmitDraft" :loading="loading">保存草稿</el-button>
  177. <el-button v-if="!isView" type="primary" @click="handleSubmit" :loading="loading">确认发布</el-button>
  178. </div>
  179. <!-- 试卷选择对话框 -->
  180. <el-dialog
  181. v-model="examDialog.visible"
  182. title="选择试卷"
  183. width="900px"
  184. append-to-body
  185. >
  186. <el-table
  187. v-loading="examDialog.loading"
  188. :data="examDialog.examList"
  189. @row-click="selectExam"
  190. style="cursor: pointer"
  191. >
  192. <el-table-column label="试卷名称" prop="examName" min-width="200" show-overflow-tooltip />
  193. <el-table-column label="考试时长" prop="examTime" width="100" align="center">
  194. <template #default="scope">
  195. {{ scope.row.examTime || scope.row.duration || 0 }}分钟
  196. </template>
  197. </el-table-column>
  198. <el-table-column label="总分" prop="examTotalScore" width="80" align="center">
  199. <template #default="scope">
  200. {{ scope.row.examTotalScore || 100 }}分
  201. </template>
  202. </el-table-column>
  203. <el-table-column label="及格分" prop="passMark" width="80" align="center">
  204. <template #default="scope">
  205. {{ scope.row.passMark || 0 }}分
  206. </template>
  207. </el-table-column>
  208. <el-table-column label="开始时间" prop="examStartTime" width="160" align="center" />
  209. <el-table-column label="操作" width="100" align="center" fixed="right">
  210. <template #default="scope">
  211. <el-button type="primary" size="small" @click.stop="selectExam(scope.row)">
  212. 选择
  213. </el-button>
  214. </template>
  215. </el-table-column>
  216. </el-table>
  217. <div class="pagination-container" style="margin-top: 20px; text-align: center;">
  218. <el-pagination
  219. v-model:current-page="examDialog.currentPage"
  220. :page-size="examDialog.pageSize"
  221. :total="examDialog.total"
  222. layout="prev, pager, next"
  223. @current-change="handlePageChange"
  224. />
  225. </div>
  226. </el-dialog>
  227. </div>
  228. </template>
  229. <script setup lang="ts" name="AssessmentDetail">
  230. import { ref, reactive, onMounted, onActivated, nextTick, getCurrentInstance, toRefs, watch } from 'vue';
  231. import { SuccessFilled } from '@element-plus/icons-vue';
  232. import { getEvaluation, addEvaluation, updateEvaluation, getThirdPartyExamList } from '@/api/main/evaluation';
  233. import { listTag } from '@/api/system/tag';
  234. import { listPosition } from '@/api/main/position';
  235. import { useRoute, useRouter } from 'vue-router';
  236. import Editor from '@/components/Editor/index.vue';
  237. import ImageUpload from '@/components/ImageUpload/index.vue';
  238. const route = useRoute();
  239. const router = useRouter();
  240. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  241. // 字典数据
  242. const { main_position_level, main_position_type } = toRefs<any>(
  243. proxy?.useDict('main_position_level', 'main_position_type')
  244. );
  245. const loading = ref(false);
  246. const isFinished = ref(false);
  247. const formRef = ref();
  248. const isEdit = ref(false);
  249. const isView = ref(false);
  250. const positionOptions = ref<any[]>([]);
  251. const tagOptions = ref<any[]>([]);
  252. // 试卷选择对话框相关
  253. const examDialog = reactive({
  254. visible: false,
  255. loading: false,
  256. examList: [],
  257. currentPage: 1,
  258. pageSize: 10,
  259. total: 0,
  260. currentAbilityIndex: -1 // 当前正在为哪个能力选择试卷
  261. });
  262. const createDefaultFormData = (): any => ({
  263. id: undefined,
  264. evaluationName: '',
  265. grade: '',
  266. positionId: undefined,
  267. positionType: '',
  268. status: '0',
  269. description: '',
  270. tags: '',
  271. remark: '',
  272. switchStatus: '0',
  273. onlineStatus: '1',
  274. onlineTimeRange: [],
  275. abilityConfigs: [
  276. { abilityName: '能力A', thirdExamInfoId: '', thirdExamName: '', thirdExamTime: 60, thirdExamPassMark: 60, thirdExamTotalScore: 100, thirdExamLink: '' },
  277. { abilityName: '能力B', thirdExamInfoId: '', thirdExamName: '', thirdExamTime: 60, thirdExamPassMark: 60, thirdExamTotalScore: 100, thirdExamLink: '' },
  278. { abilityName: '能力C', thirdExamInfoId: '', thirdExamName: '', thirdExamTime: 60, thirdExamPassMark: 60, thirdExamTotalScore: 100, thirdExamLink: '' }
  279. ]
  280. });
  281. const formData = ref<EvaluationVO>(createDefaultFormData());
  282. /** 重置表单 */
  283. const resetForm = () => {
  284. formData.value = createDefaultFormData();
  285. formRef.value?.resetFields();
  286. };
  287. const rules = {
  288. evaluationName: [
  289. { required: true, message: '测评名称不能为空', trigger: 'blur' }
  290. ],
  291. thirdExamInfoId: [
  292. { required: true, message: '请选择试卷', trigger: 'change' }
  293. ],
  294. grade: [
  295. { required: true, message: '级别不能为空', trigger: 'change' }
  296. ],
  297. positionId: [
  298. { required: true, message: '岗位不能为空', trigger: 'change' }
  299. ],
  300. positionType: [
  301. { required: true, message: '岗位类型不能为空', trigger: 'change' }
  302. ],
  303. price: [
  304. { required: true, message: '价格不能为空', trigger: 'blur' }
  305. ],
  306. onlineStatus: [
  307. { required: true, message: '请选择上架时间', trigger: 'change' }
  308. ],
  309. onlineTimeRange: [
  310. {
  311. validator: (rule: any, value: any, callback: any) => {
  312. if (formData.value.onlineStatus === '2' && (!value || value.length !== 2)) {
  313. callback(new Error('请选择定时上架时间范围'));
  314. } else {
  315. callback();
  316. }
  317. },
  318. trigger: 'change'
  319. }
  320. ]
  321. };
  322. /** 返回 */
  323. const goBack = () => {
  324. resetForm();
  325. router.replace({
  326. path: '/system/assessment',
  327. query: { t: Date.now() }
  328. });
  329. };
  330. /** 添加能力 */
  331. const addAbility = () => {
  332. formData.value.abilityConfigs.push({
  333. abilityName: '',
  334. thirdExamInfoId: '',
  335. thirdExamName: '',
  336. thirdExamTime: 60,
  337. thirdExamPassMark: 60,
  338. thirdExamTotalScore: 100,
  339. thirdExamLink: ''
  340. });
  341. };
  342. /** 删除能力 */
  343. const removeAbility = (index: number) => {
  344. formData.value.abilityConfigs.splice(index, 1);
  345. };
  346. /** 显示试卷选择对话框 */
  347. const showExamDialog = async (index: number) => {
  348. examDialog.currentAbilityIndex = index;
  349. examDialog.visible = true;
  350. examDialog.currentPage = 1;
  351. await loadExamList();
  352. };
  353. /** 加载试卷列表 */
  354. const loadExamList = async () => {
  355. examDialog.loading = true;
  356. try {
  357. const res = await getThirdPartyExamList(examDialog.currentPage);
  358. let examData = [];
  359. let total = 0;
  360. // 根据用户提供的特殊格式:数据在 msg 字段的 JSON 字符串中
  361. const rawData = (res as any).data || (res as any).msg;
  362. if (rawData && typeof rawData === 'string') {
  363. try {
  364. const parsedData = JSON.parse(rawData);
  365. if (parsedData.bizContent) {
  366. examData = parsedData.bizContent.rows || [];
  367. total = parsedData.bizContent.total || 0;
  368. }
  369. } catch (e) {
  370. console.error('解析考试列表失败:', e);
  371. }
  372. } else if (typeof res === 'object') {
  373. // 兼容普通对象格式
  374. const data: any = res;
  375. if (data.bizContent) {
  376. examData = data.bizContent.rows || [];
  377. total = data.bizContent.total || 0;
  378. }
  379. }
  380. examDialog.examList = examData;
  381. examDialog.total = total;
  382. } catch (error) {
  383. console.error('获取试卷列表失败:', error);
  384. proxy?.$modal.msgError('获取试卷列表失败');
  385. } finally {
  386. examDialog.loading = false;
  387. }
  388. };
  389. /** 分页变化处理 */
  390. const handlePageChange = (page: number) => {
  391. examDialog.currentPage = page;
  392. loadExamList();
  393. };
  394. /** 选择试卷 */
  395. const selectExam = (exam: any) => {
  396. const index = examDialog.currentAbilityIndex;
  397. if (index !== -1) {
  398. const ability = formData.value.abilityConfigs[index];
  399. ability.thirdExamName = exam.examName || exam.name;
  400. ability.thirdExamInfoId = exam.examInfoId || exam.id;
  401. ability.thirdExamTime = exam.examTime || exam.duration || 0;
  402. ability.thirdExamPassMark = exam.passMark || 0;
  403. ability.thirdExamTotalScore = exam.examTotalScore || exam.totalScore || 0;
  404. if (exam.examLink) {
  405. ability.thirdExamLink = exam.examLink;
  406. }
  407. }
  408. examDialog.visible = false;
  409. proxy?.$modal.msgSuccess('试卷选择成功');
  410. };
  411. /** 获取详情 */
  412. const getDetail = async (id: string | number) => {
  413. try {
  414. loading.value = true;
  415. const res = await getEvaluation(id);
  416. const data = res.data;
  417. let decodedDetail = data.detail || '';
  418. if (decodedDetail && !decodedDetail.includes('<') && /^[A-Za-z0-9+/=\r\n]+$/.test(decodedDetail) && decodedDetail.length % 4 === 0) {
  419. try {
  420. decodedDetail = decodeURIComponent(escape(atob(decodedDetail)));
  421. } catch (e) {
  422. console.warn('Detail decoding failed', e);
  423. }
  424. }
  425. formData.value = {
  426. id: data.id,
  427. evaluationName: data.evaluationName || '',
  428. grade: data.grade ? String(data.grade) : '',
  429. positionId: data.positionId,
  430. positionType: data.positionType ? String(data.positionType) : '',
  431. detail: decodedDetail,
  432. tags: data.tags ? data.tags.split(',') : [],
  433. mainImage: data.mainImage || '',
  434. imageAlbum: data.imageAlbum || '',
  435. price: data.price || 0,
  436. status: data.status || '0',
  437. onlineStatus: data.status === '0' ? '3' : '1',
  438. onlineTimeRange: (data.onTime && data.downTime) ? [data.onTime, data.downTime] : [],
  439. abilityConfigs: (data.abilityConfigs || []).map((item: any) => ({
  440. id: item.id,
  441. evaluationId: item.evaluationId,
  442. abilityName: item.abilityName || '',
  443. thirdExamInfoId: item.thirdExamInfoId || '',
  444. thirdExamName: item.thirdExamName || '',
  445. thirdExamTime: item.thirdExamTime || 60,
  446. thirdExamPassMark: item.thirdExamPassMark || 60,
  447. thirdExamTotalScore: item.thirdExamTotalScore || 100,
  448. thirdExamLink: item.thirdExamLink || ''
  449. }))
  450. };
  451. // 如果能力配置不足3个,补足到3个
  452. const currentLength = formData.value.abilityConfigs.length;
  453. if (currentLength < 3) {
  454. const names = ['能力A', '能力B', '能力C'];
  455. for (let i = currentLength; i < 3; i++) {
  456. formData.value.abilityConfigs.push({
  457. abilityName: names[i],
  458. thirdExamInfoId: '',
  459. thirdExamName: '',
  460. thirdExamTime: 60,
  461. thirdExamPassMark: 60,
  462. thirdExamTotalScore: 100,
  463. thirdExamLink: ''
  464. });
  465. }
  466. }
  467. } catch (error) {
  468. console.error('获取详情失败:', error);
  469. proxy?.$modal.msgError('获取详情失败');
  470. } finally {
  471. loading.value = false;
  472. }
  473. };
  474. /** 提交表单 */
  475. const handleSubmit = async () => {
  476. try {
  477. if (loading.value) {
  478. return;
  479. }
  480. await formRef.value?.validate();
  481. loading.value = true;
  482. const submitData = {
  483. ...formData.value,
  484. abilityConfigs: (formData.value.abilityConfigs || []).map((item: any, index: number) => ({
  485. id: item.id,
  486. evaluationId: item.evaluationId,
  487. abilityName: item.abilityName,
  488. thirdExamInfoId: item.thirdExamInfoId,
  489. thirdExamName: item.thirdExamName,
  490. thirdExamTime: item.thirdExamTime,
  491. thirdExamPassMark: item.thirdExamPassMark,
  492. thirdExamTotalScore: item.thirdExamTotalScore,
  493. thirdExamLink: item.thirdExamLink,
  494. sortOrder: index
  495. }))
  496. };
  497. // 标签转回字符串
  498. submitData.tags = Array.isArray(submitData.tags) ? submitData.tags.join(',') : submitData.tags;
  499. // 设置上架/下架时间
  500. if (submitData.onlineStatus === '2' && submitData.onlineTimeRange?.length === 2) {
  501. submitData.onTime = submitData.onlineTimeRange[0];
  502. submitData.downTime = submitData.onlineTimeRange[1];
  503. } else {
  504. submitData.onTime = null;
  505. submitData.downTime = null;
  506. }
  507. // 设置状态:0=草稿, 1=已发布, 2=已下架
  508. if (submitData.onlineStatus === '3') {
  509. submitData.status = '0';
  510. } else {
  511. submitData.status = '1';
  512. }
  513. const hasValidId = submitData.id !== undefined && submitData.id !== null && submitData.id !== '';
  514. if (isEdit.value) {
  515. if (!hasValidId) {
  516. proxy?.$modal.msgError('当前编辑数据缺少ID,请刷新后重试');
  517. return;
  518. }
  519. await updateEvaluation(submitData);
  520. proxy?.$modal.msgSuccess('修改成功');
  521. } else {
  522. await addEvaluation(submitData);
  523. proxy?.$modal.msgSuccess('新增成功');
  524. }
  525. goBack();
  526. } catch (error) {
  527. console.error('提交失败:', error);
  528. } finally {
  529. loading.value = false;
  530. }
  531. };
  532. /** 保存草稿 */
  533. const handleSubmitDraft = async () => {
  534. try {
  535. loading.value = true;
  536. const draftData = { ...formData.value };
  537. draftData.saveTime = new Date().toISOString();
  538. const draftKey = isEdit.value ? `assessment_draft_${formData.value.id}` : `assessment_draft_new`;
  539. localStorage.setItem(draftKey, JSON.stringify(draftData));
  540. proxy?.$modal.msgSuccess('草稿保存成功');
  541. } catch (error) {
  542. console.error('保存草稿失败:', error);
  543. } finally {
  544. loading.value = false;
  545. }
  546. };
  547. /** 加载岗位列表 */
  548. const getPositionList = async () => {
  549. try {
  550. const res: any = await listPosition({ pageNum: 1, pageSize: 999 });
  551. positionOptions.value = res.rows || [];
  552. } catch (error) {
  553. console.error('获取岗位列表失败:', error);
  554. }
  555. };
  556. /** 加载标签列表 */
  557. const getTagList = async () => {
  558. try {
  559. const res: any = await listTag({ status: '0' });
  560. // 后端返回的数据可能直接是数组,也可能是包含 rows 或 data 的对象
  561. tagOptions.value = Array.isArray(res) ? res : (res.rows || res.data || []);
  562. } catch (error) {
  563. console.error('获取标签列表失败:', error);
  564. }
  565. };
  566. onMounted(() => {
  567. getPositionList();
  568. getTagList();
  569. handleRouteChange(route.query);
  570. });
  571. onActivated(() => {
  572. handleRouteChange(route.query);
  573. });
  574. /** 统一处理路由参数逻辑 */
  575. const handleRouteChange = (query: any) => {
  576. isFinished.value = false;
  577. const id = query.id;
  578. isView.value = query.mode === 'view';
  579. if (id) {
  580. isEdit.value = true;
  581. getDetail(id as string);
  582. } else {
  583. isEdit.value = false;
  584. isView.value = false;
  585. resetForm();
  586. }
  587. };
  588. watch(() => route.query, (newQuery) => {
  589. handleRouteChange(newQuery);
  590. }, { immediate: false, deep: true });
  591. </script>
  592. <style scoped lang="scss">
  593. .assessment-edit-wrapper {
  594. height: 100vh;
  595. display: flex;
  596. flex-direction: column;
  597. background-color: #f5f7fa;
  598. overflow: hidden;
  599. }
  600. .scrollable-content {
  601. flex: 1;
  602. overflow-y: auto;
  603. padding: 30px 40px;
  604. background-color: #fff;
  605. }
  606. .form-container {
  607. width: 100%;
  608. }
  609. .success-content {
  610. height: 60vh;
  611. display: flex;
  612. align-items: center;
  613. justify-content: center;
  614. .success-center {
  615. display: flex;
  616. flex-direction: column;
  617. align-items: center;
  618. .success-text {
  619. font-size: 18px;
  620. color: #333;
  621. margin-bottom: 30px;
  622. }
  623. .finish-btn {
  624. width: 100px;
  625. }
  626. }
  627. }
  628. .form-section {
  629. background: #fff;
  630. padding: 24px;
  631. margin-bottom: 20px;
  632. border-radius: 4px;
  633. .section-title {
  634. font-size: 16px;
  635. font-weight: bold;
  636. margin-bottom: 24px;
  637. color: #303133;
  638. position: relative;
  639. padding-left: 12px;
  640. &::before {
  641. content: "";
  642. position: absolute;
  643. left: 0;
  644. top: 3px;
  645. bottom: 3px;
  646. width: 4px;
  647. background: #409eff;
  648. border-radius: 2px;
  649. }
  650. }
  651. }
  652. .no-border {
  653. margin-bottom: 0;
  654. }
  655. .fixed-footer {
  656. height: 64px;
  657. background: #fff;
  658. display: flex;
  659. align-items: center;
  660. justify-content: center;
  661. gap: 16px;
  662. border-top: 1px solid #e6e6e6;
  663. flex-shrink: 0;
  664. }
  665. .form-tip {
  666. font-size: 12px;
  667. color: #909399;
  668. margin-top: 4px;
  669. }
  670. .upload-tip {
  671. font-size: 12px;
  672. color: #909399;
  673. line-height: 1.6;
  674. margin-top: 8px;
  675. }
  676. .ability-list {
  677. display: flex;
  678. flex-direction: column;
  679. gap: 16px;
  680. .ability-item-card {
  681. background: #f8fafc;
  682. border: 1px solid #e2e8f0;
  683. border-radius: 8px;
  684. padding: 20px;
  685. position: relative;
  686. .ability-ops {
  687. display: flex;
  688. justify-content: flex-end;
  689. margin-top: -10px;
  690. }
  691. }
  692. .add-ability-btn {
  693. text-align: center;
  694. border: 1px dashed #dcdfe6;
  695. border-radius: 8px;
  696. padding: 12px;
  697. cursor: pointer;
  698. transition: all 0.3s;
  699. &:hover {
  700. border-color: #409eff;
  701. background: #f0f7ff;
  702. }
  703. }
  704. }
  705. .online-status-wrapper {
  706. .date-picker-inline {
  707. margin-top: 16px;
  708. }
  709. }
  710. :deep(.el-form-item__label) {
  711. font-weight: normal;
  712. padding-bottom: 8px !important;
  713. }
  714. :deep(.el-input-group__append) {
  715. background-color: #409eff;
  716. color: #fff;
  717. border-color: #409eff;
  718. .el-button {
  719. color: #fff;
  720. }
  721. }
  722. :deep(.editor) {
  723. width: 100% !important;
  724. .ql-container {
  725. height: 150px !important;
  726. }
  727. }
  728. </style>