InternshipPersonnelDetail.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <template>
  2. <a-spin :spinning="loading">
  3. <div class="internship-personnel-detail" v-if="detailData">
  4. <!-- 顶部人员概要信息 -->
  5. <div class="detail-header">
  6. <div class="header-avatar">
  7. <a-avatar :size="64" style="background-color: #1890ff">
  8. {{ detailData.fullName ? detailData.fullName.substring(0, 1) : '?' }}
  9. </a-avatar>
  10. </div>
  11. <div class="header-info">
  12. <div class="header-name">
  13. <span class="name">{{ detailData.fullName || '-' }}</span>
  14. <span class="gender">{{ detailData.gender || '' }}</span>
  15. <span class="age" v-if="detailData.age != null">{{ detailData.age }}岁</span>
  16. </div>
  17. <div class="header-tags">
  18. <a-tag v-if="detailData.majorTag" color="blue">{{ getDictText('internship_major_tag', detailData.majorTag) }}</a-tag>
  19. <a-tag v-if="detailData.minorTag" color="green">{{ getDictText('internship_minor_tag', detailData.minorTag) }}</a-tag>
  20. <a-tag v-if="detailData.internshipStatus" color="blue">{{ detailData.internshipStatus }}</a-tag>
  21. <a-tag v-if="detailData.postName" color="green">{{ detailData.postName }}</a-tag>
  22. <a-tag v-if="detailData.companyName" color="purple">{{ detailData.companyName }}</a-tag>
  23. <template v-if="detailData.customTag">
  24. <a-tag v-for="tag in customTagList" :key="tag" color="orange">{{ getDictText('internship_custom_tag', tag) || tag }}</a-tag>
  25. </template>
  26. </div>
  27. <div class="header-meta">
  28. <span v-if="detailData.education">{{ detailData.education }}</span>
  29. <span v-if="detailData.contactPhone"> | {{ detailData.contactPhone }}</span>
  30. <span v-if="detailData.jobSearchStatus"> | {{ detailData.jobSearchStatus }}</span>
  31. <span v-if="detailData.internshipStatus"> | {{ detailData.internshipStatus }}</span>
  32. <span v-if="detailData.postName"> | {{ detailData.postName }}</span>
  33. <span v-if="detailData.companyName"> | {{ detailData.companyName }}</span>
  34. </div>
  35. </div>
  36. </div>
  37. <a-divider style="margin: 12px 0" />
  38. <!-- 见习信息区域 -->
  39. <a-descriptions bordered :column="2" size="small" title="见习信息" style="margin-bottom: 16px">
  40. <a-descriptions-item label="见习状态">{{ detailData.internshipStatus || '-' }}</a-descriptions-item>
  41. <a-descriptions-item label="见习岗位">{{ detailData.postName || '-' }}</a-descriptions-item>
  42. <a-descriptions-item label="见习单位">{{ detailData.companyName || '-' }}</a-descriptions-item>
  43. <a-descriptions-item label="见习开始日期">{{ formatDate(detailData.startDate) }}</a-descriptions-item>
  44. <a-descriptions-item label="见习结束日期">{{ formatDate(detailData.endDate) }}</a-descriptions-item>
  45. <a-descriptions-item label="审核状态">{{ detailData.auditStatus || '-' }}</a-descriptions-item>
  46. <a-descriptions-item label="审核意见" :span="2">{{ detailData.auditOpinion || '-' }}</a-descriptions-item>
  47. </a-descriptions>
  48. <!-- 个人信息区域 -->
  49. <a-descriptions bordered :column="2" size="small" title="个人信息">
  50. <a-descriptions-item label="证件类型">{{ detailData.idType || '-' }}</a-descriptions-item>
  51. <a-descriptions-item label="证件号码">{{ detailData.idNumber || '-' }}</a-descriptions-item>
  52. <a-descriptions-item label="出生日期">{{ formatDate(detailData.birthDate) }}</a-descriptions-item>
  53. <a-descriptions-item label="年龄">{{ detailData.age != null ? detailData.age + '岁' : '-' }}</a-descriptions-item>
  54. <a-descriptions-item label="民族">{{ detailData.nation || '-' }}</a-descriptions-item>
  55. <a-descriptions-item label="国籍">{{ detailData.nationality || '-' }}</a-descriptions-item>
  56. <a-descriptions-item label="婚姻状况">{{ detailData.maritalStatus || '-' }}</a-descriptions-item>
  57. <a-descriptions-item label="政治面貌">{{ detailData.politicalStatus || '-' }}</a-descriptions-item>
  58. <a-descriptions-item label="学历">{{ detailData.education || '-' }}</a-descriptions-item>
  59. <a-descriptions-item label="毕业日期">{{ formatDate(detailData.graduationDate) }}</a-descriptions-item>
  60. <a-descriptions-item label="毕业院校">{{ detailData.graduateSchool || '-' }}</a-descriptions-item>
  61. <a-descriptions-item label="专业">{{ detailData.major || '-' }}</a-descriptions-item>
  62. <a-descriptions-item label="工作经验">{{ detailData.workExperience || '-' }}</a-descriptions-item>
  63. <a-descriptions-item label="职业技能等级">{{ detailData.skillLevel || '-' }}</a-descriptions-item>
  64. <a-descriptions-item label="户口性质">{{ detailData.householdType || '-' }}</a-descriptions-item>
  65. <a-descriptions-item label="户口所在地">{{ (xzqhMap[detailData.householdLocation] || xzqhMap[String(detailData.householdLocation).substring(0, 6)] || detailData.householdLocation || '').replace('广东省湛江市','') || '-' }}</a-descriptions-item>
  66. <a-descriptions-item label="现居住地">{{ xzqhMap[detailData.currentResidence] || xzqhMap[String(detailData.currentResidence).substring(0, 6)] || detailData.currentResidence || '-' }}</a-descriptions-item>
  67. <a-descriptions-item label="现居住地址">{{ detailData.currentAddress || '-' }}</a-descriptions-item>
  68. <a-descriptions-item label="求职人员类别">{{ getDictText('JobSeekerCategory', detailData.jobSeekerCategory) || detailData.jobSeekerCategory || '-' }}</a-descriptions-item>
  69. <a-descriptions-item label="联系电话">{{ detailData.contactPhone || '-' }}</a-descriptions-item>
  70. <a-descriptions-item label="邮箱">{{ detailData.email || '-' }}</a-descriptions-item>
  71. <a-descriptions-item label="QQ号码">{{ detailData.qqNumber || '-' }}</a-descriptions-item>
  72. <a-descriptions-item label="微信号">{{ detailData.wechatId || '-' }}</a-descriptions-item>
  73. <a-descriptions-item label="是否留学人才">{{ detailData.isOverseasTalent || '-' }}</a-descriptions-item>
  74. <a-descriptions-item label="求职状态">{{ detailData.jobSearchStatus || '-' }}</a-descriptions-item>
  75. <a-descriptions-item label="是否接受推荐职位">{{ detailData.acceptRecommend || '-' }}</a-descriptions-item>
  76. </a-descriptions>
  77. <!-- 标签信息区域 -->
  78. <a-divider style="margin: 16px 0" />
  79. <a-descriptions bordered :column="2" size="small" title="标签信息">
  80. <a-descriptions-item label="人员大类标签">
  81. <a-tag v-if="detailData.majorTag">{{ getDictText('internship_major_tag', detailData.majorTag) }}</a-tag>
  82. <span v-else>-</span>
  83. </a-descriptions-item>
  84. <a-descriptions-item label="人员小类标签">
  85. <a-tag v-if="detailData.minorTag">{{ getDictText('internship_minor_tag', detailData.minorTag) }}</a-tag>
  86. <span v-else>-</span>
  87. </a-descriptions-item>
  88. </a-descriptions>
  89. <!-- 自定义标签区域 -->
  90. <a-divider style="margin: 16px 0" />
  91. <a-descriptions bordered :column="1" size="small" title="自定义标签">
  92. <a-descriptions-item label="自定义标签">
  93. <template v-if="!editingCustomTag">
  94. <div class="custom-tag-view">
  95. <template v-if="customTagList.length > 0">
  96. <a-tag v-for="tagVal in customTagList" :key="tagVal" class="custom-tag-item">{{ getDictText('internship_custom_tag', tagVal) || tagVal }}</a-tag>
  97. </template>
  98. <span v-else class="tag-empty">-</span>
  99. <a-button type="dashed" size="small" class="edit-tag-btn" @click="startEditCustomTag">
  100. <template #icon><EditOutlined /></template>
  101. 编辑
  102. </a-button>
  103. </div>
  104. </template>
  105. <template v-else>
  106. <div class="custom-tag-edit">
  107. <a-select
  108. v-model:value="editingCustomTagList"
  109. mode="multiple"
  110. placeholder="请选择自定义标签"
  111. :options="customTagDictOptions"
  112. allow-clear
  113. />
  114. <div class="edit-actions">
  115. <a-button type="primary" size="small" :loading="saving" @click="saveCustomTags">保存</a-button>
  116. <a-button size="small" @click="cancelEditCustomTag">取消</a-button>
  117. </div>
  118. </div>
  119. </template>
  120. </a-descriptions-item>
  121. </a-descriptions>
  122. </div>
  123. <div v-else-if="!loading" style="text-align: center; padding: 40px; color: #999">
  124. 暂无数据
  125. </div>
  126. </a-spin>
  127. </template>
  128. <script lang="ts" setup>
  129. import { computed, ref, watch } from 'vue';
  130. import { EditOutlined } from '@ant-design/icons-vue';
  131. import { queryDetailById, saveOrUpdate, listCustomTags } from '../InternshipPersonnel.api';
  132. import dayjs from 'dayjs';
  133. import { useMessage } from '/@/hooks/web/useMessage';
  134. import { useDict } from '/@/hooks/dictionary/useDict';
  135. const { createMessage } = useMessage();
  136. // 从自定义 DICTIONARY_ITEM 表加载标签字典(含求职人员类别字典)
  137. const { getDictText, getDictOptions } = useDict(['internship_major_tag', 'internship_minor_tag', 'internship_custom_tag', 'JobSeekerCategory', 'XZQH']);
  138. const xzqhMap = computed(() => {
  139. const map: Record<string, string> = {};
  140. for (const item of getDictOptions('XZQH')) {
  141. map[item.value] = item.label;
  142. }
  143. return map;
  144. });
  145. const props = defineProps({
  146. record: { type: Object, default: () => ({}) },
  147. });
  148. const loading = ref<boolean>(false);
  149. const saving = ref<boolean>(false);
  150. const detailData = ref<any>(null);
  151. // 自定义标签字典选项(用于下拉多选)
  152. const customTagDictOptions = ref<any[]>([]);
  153. // 自定义标签编辑相关
  154. const editingCustomTag = ref<boolean>(false);
  155. const editingCustomTagList = ref<string[]>([]);
  156. /** 加载自定义标签字典选项 */
  157. async function loadCustomTagOptions() {
  158. try {
  159. const tags = await listCustomTags();
  160. if (Array.isArray(tags)) {
  161. // listCustomTags 返回 DictionaryItem 对象 { value, name, ... }
  162. customTagDictOptions.value = tags.map(tag => ({ label: tag.name, value: String(tag.value) }));
  163. }
  164. } catch (e) {
  165. console.error('加载自定义标签字典失败', e);
  166. }
  167. }
  168. /** 自定义标签列表(逗号分隔转数组) */
  169. const customTagList = computed(() => {
  170. if (!detailData.value?.customTag) return [];
  171. return detailData.value.customTag.split(',').filter((t: string) => t.trim());
  172. });
  173. /** 格式化日期 */
  174. function formatDate(date: any) {
  175. if (!date) return '-';
  176. return dayjs(date).format('YYYY-MM-DD');
  177. }
  178. /** 开始编辑自定义标签:将当前标签列表复制到编辑区 */
  179. function startEditCustomTag() {
  180. editingCustomTagList.value = [...customTagList.value];
  181. editingCustomTag.value = true;
  182. }
  183. /** 取消编辑 */
  184. function cancelEditCustomTag() {
  185. editingCustomTag.value = false;
  186. editingCustomTagList.value = [];
  187. }
  188. /** 保存自定义标签 */
  189. async function saveCustomTags() {
  190. const id = detailData.value?.id;
  191. if (!id) return;
  192. saving.value = true;
  193. try {
  194. const customTagStr = editingCustomTagList.value.join(',');
  195. await saveOrUpdate({ id, customTag: customTagStr }, true);
  196. detailData.value.customTag = customTagStr;
  197. editingCustomTag.value = false;
  198. createMessage.success('自定义标签保存成功');
  199. } catch (e) {
  200. console.error('保存自定义标签失败', e);
  201. createMessage.error('保存失败');
  202. } finally {
  203. saving.value = false;
  204. }
  205. }
  206. /** 加载详情 */
  207. async function loadDetail() {
  208. const id = props.record?.id;
  209. if (!id) return;
  210. loading.value = true;
  211. try {
  212. const res = await queryDetailById({ id });
  213. if (res) {
  214. detailData.value = res;
  215. }
  216. } catch (e) {
  217. console.error('加载详情失败', e);
  218. } finally {
  219. loading.value = false;
  220. }
  221. }
  222. /** 监听记录变化,加载详情和自定义标签选项 */
  223. watch(
  224. () => props.record,
  225. () => {
  226. loadDetail();
  227. loadCustomTagOptions();
  228. },
  229. { immediate: true, deep: true }
  230. );
  231. </script>
  232. <style lang="less" scoped>
  233. .internship-personnel-detail {
  234. padding: 16px;
  235. .detail-header {
  236. display: flex;
  237. align-items: center;
  238. gap: 16px;
  239. .header-info {
  240. flex: 1;
  241. .header-name {
  242. margin-bottom: 6px;
  243. .name {
  244. font-size: 20px;
  245. font-weight: 600;
  246. margin-right: 8px;
  247. }
  248. .gender, .age {
  249. font-size: 14px;
  250. color: #666;
  251. margin-right: 6px;
  252. }
  253. }
  254. .header-tags {
  255. margin-bottom: 6px;
  256. }
  257. .header-meta {
  258. font-size: 13px;
  259. color: #999;
  260. }
  261. }
  262. }
  263. /* 自定义标签查看模式:flex行内排列 */
  264. .custom-tag-view {
  265. display: flex;
  266. flex-wrap: wrap;
  267. align-items: center;
  268. gap: 6px;
  269. .custom-tag-item {
  270. margin: 0;
  271. border-radius: 4px;
  272. }
  273. .tag-empty {
  274. color: #999;
  275. }
  276. .edit-tag-btn {
  277. flex-shrink: 0;
  278. margin-left: 4px;
  279. }
  280. }
  281. /* 自定义标签编辑模式 */
  282. .custom-tag-edit {
  283. display: flex;
  284. flex-direction: column;
  285. gap: 10px;
  286. max-width: 500px;
  287. .edit-actions {
  288. display: flex;
  289. gap: 8px;
  290. }
  291. }
  292. }
  293. </style>