item.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. <template>
  2. <div class="listItem d-flex align-center pa-3 mb-3" v-for="(item, index) in items" :key="'item_' + index">
  3. <div class="d-flex align-center">
  4. <div class="mr-5 font-size-16" style="color: orange; width: 96px;">{{ timesTampChange(item.time, 'Y-M-D h:m') }}</div>
  5. <v-avatar class="mr-2" size=40 :image="getUserAvatar(item?.person?.avatar, item?.person?.sex)"></v-avatar>
  6. <div class="d-flex flex-column mr-3" style="width: 110px;">
  7. <span class="ellipsis mb-1">{{ item?.person?.name }}</span>
  8. <span class="ellipsis" style="color: var(--color-999);">{{ item?.job?.name }}</span>
  9. </div>
  10. </div>
  11. <div class="d-flex align-center right-item">
  12. <div style="min-width: 80px;text-align: center;">
  13. <v-icon v-if="item?.phone" class="mx-1" size="20" color="primary">mdi-phone-outline</v-icon>
  14. <span>{{ item?.phone || '-' }}</span>
  15. </div>
  16. <div>
  17. <!-- 面试类型: 线下面试 -->
  18. <span v-if="item.type === '1'">
  19. <v-icon class="mx-3" size="20" color="primary">mdi-account-multiple-outline</v-icon>
  20. <span>{{ $t('interview.offlineInterview') }}</span>
  21. </span>
  22. <!-- 面试类型: 线上面试 -->
  23. <span v-else class="d-flex">
  24. <v-icon class="mx-3 mt-2" size="20" color="primary">mdi mdi-video-account</v-icon>
  25. <span class="d-flex flex-column">
  26. <span>{{ $t('interview.onlineInterview') }}</span>
  27. <span style="color: var(--color-999);">腾讯会议</span>
  28. </span>
  29. </span>
  30. </div>
  31. <!-- 面试状态: '待接受'/'已取消' -->
  32. <div :style="{ 'color': colorData[item.status] }">
  33. <v-icon v-if="statusList.find(e => e.value === item.status)?.label" size="30">mdi mdi-circle-small</v-icon>
  34. <span>
  35. {{ statusList.find(e => e.value === item.status)?.label }}
  36. <span v-if="item?.job?.status === '1'" style="font-size: 13px; color: grey;">[职位已关闭]</span>
  37. </span>
  38. </div>
  39. <div>
  40. <template v-if="item.job?.status !== '1'" >
  41. <span v-if="editStatus.indexOf(item.status) !== -1" class="font-size-15 color-primary" @click="handleActionClick('edit', item)">修改面试</span>
  42. <span v-if="againStatus.indexOf(item.status) !== -1" class="font-size-15 color-primary" @click="handleActionClick('edit', item)">重新邀约</span>
  43. </template>
  44. <span v-if="item.status === '1'" class="font-size-15 color-primary" @click="handleActionClick('completed', item)">完成面试</span>
  45. <span v-if="item.status === '3'" class="font-size-15 color-primary" @click="handleActionClick('feedback', item)">填写反馈</span>
  46. <v-menu v-if="actionItems(item).length">
  47. <template v-slot:activator="{ props }">
  48. <v-icon v-bind="props" class="mx-3" size="20" color="primary">mdi-dots-horizontal</v-icon>
  49. </template>
  50. <v-list>
  51. <v-list-item
  52. v-for="(k, index) in actionItems(item)"
  53. :key="index"
  54. :value="index"
  55. color="primary"
  56. @click="handleActionClick(k.value, item)"
  57. >
  58. <v-list-item-title>{{ k.title }}</v-list-item-title>
  59. </v-list-item>
  60. </v-list>
  61. </v-menu>
  62. </div>
  63. </div>
  64. </div>
  65. <!-- 修改面试、重新邀约 -->
  66. <CtDialog :visible="showInvite" :widthType="2" titleClass="text-h6" title="面试信息" @close="handleEditClose" @submit="handleEditSubmit">
  67. <InvitePage v-if="showInvite" ref="inviteRef" :itemData="itemData" :position="positionItems"></InvitePage>
  68. </CtDialog>
  69. <!-- 取消面试 -->
  70. <CtDialog :visible="cancelInvite" :widthType="2" titleClass="text-h6" title="取消面试" @close="handleCancelClose" @submit="handleCancelSubmit">
  71. <TextArea v-model="cancelQuery.reason" :item="textItems"></TextArea>
  72. </CtDialog>
  73. <!-- 爽约、填写反馈 -->
  74. <CtDialog :visible="show" :widthType="2" titleClass="text-h6" :title="currentAction === 'feedback' ? '填写反馈' : '填写爽约原因'" @close="handleClose" @submit="handleSubmit">
  75. <TextArea v-if="currentAction === 'feedback'" v-model="query.evaluate" :item="textItems2"></TextArea>
  76. <TextArea v-else v-model="query.reason" :item="textItems2"></TextArea>
  77. </CtDialog>
  78. </template>
  79. <script setup>
  80. defineOptions({ name: 'interview-item'})
  81. import { ref } from 'vue'
  82. import { timesTampChange } from '@/utils/date'
  83. import { useI18n } from '@/hooks/web/useI18n'
  84. import { completedInterviewInvite, cancelInterviewInvite, saveInterviewInvite, noAttendInterviewInvite, feedbackInterviewInvite } from '@/api/recruit/enterprise/interview'
  85. import InvitePage from './invite.vue'
  86. import Snackbar from '@/plugins/snackbar'
  87. import Confirm from '@/plugins/confirm'
  88. import { getUserAvatar } from '@/utils/avatar'
  89. defineProps({
  90. items: Array,
  91. statusList: Array,
  92. positionItems: Array
  93. })
  94. const emit = defineEmits(['refresh', 'action'])
  95. const { t } = useI18n()
  96. const editStatus = ['0'] // 修改面试状态
  97. const againStatus = ['98', '99'] // 重新邀约状态
  98. const actions = ref([
  99. { title: '完成面试', value: 'completed' },
  100. { title: '取消面试', value: 'cancel' },
  101. { title: '填写反馈', value: 'feedback' },
  102. { title: '爽约', value: 'attended' },
  103. { title: '修改面试', value: 'edit' }
  104. ])
  105. const colorData = {
  106. '0': 'orange',
  107. '1': 'green',
  108. '2': 'green',
  109. '3': 'var(--v-primary-base)',
  110. '4': 'var(--color-999)',
  111. '5': 'var(--v-error-base)',
  112. '98': 'var(--v-error-base)',
  113. '99': 'var(--color-999)'
  114. }
  115. // 邀请
  116. const itemData = ref({})
  117. const showInvite = ref(false)
  118. const inviteRef = ref()
  119. // 取消
  120. const cancelInvite = ref(false)
  121. const cancelQuery = ref({
  122. id: null,
  123. reason: null
  124. })
  125. const textItems = ref({
  126. label: '取消原因 *',
  127. clearable: true
  128. })
  129. // 爽约、反馈
  130. const currentAction = ref('feedback')
  131. const show = ref(false)
  132. const query = ref({})
  133. const textItems2 = ref({
  134. label: '反馈 *',
  135. clearable: true
  136. })
  137. const obj = {
  138. '0': [1],
  139. '1': [4, 1, 3],
  140. '2': [3]
  141. }
  142. const actionItems = (item) => {
  143. const status = item?.status
  144. const jobClosed = item.job?.status === '1'
  145. const type = jobClosed && obj[status] ? [0] : obj[status]
  146. if (!type || !type.length) return []
  147. let data = type.map(e => actions.value[e])
  148. return data
  149. }
  150. // 完成面试
  151. const handleFinish = (item) => {
  152. if (!item.id) return
  153. Confirm(t('common.confirmTitle'), '是否确认已完成面试?').then(async () => {
  154. await completedInterviewInvite(item.id)
  155. Snackbar.success(t('common.operationSuccessful'))
  156. emit('refresh')
  157. })
  158. }
  159. // 操作按钮
  160. const handleActionClick = (value, item) => {
  161. if (item.job?.status === '1' && (value === 'edit')) return Snackbar.warning('职位已关闭!')
  162. // 修改、重新邀约
  163. if (value === 'edit') {
  164. itemData.value = item
  165. showInvite.value = true
  166. }
  167. // 取消
  168. if (value === 'cancel') {
  169. cancelQuery.value.id = item.id
  170. cancelInvite.value = true
  171. }
  172. // 完成
  173. if (value === 'completed') handleFinish(item)
  174. // 爽约、反馈
  175. if (value === 'feedback' || value === 'attended') {
  176. currentAction.value = value
  177. textItems2.value.label = value === 'feedback' ? '反馈 *' : '爽约原因 *'
  178. query.value = value === 'feedback' ? { id: item.id, evaluate: null } : { id: item.id, reason: null }
  179. show.value = true
  180. }
  181. }
  182. // 修改面试、重新邀约
  183. const handleEditClose = () => {
  184. itemData.value = {}
  185. showInvite.value = false
  186. }
  187. const handleEditSubmit = async () => {
  188. const query = inviteRef.value.getQuery()
  189. if (!Object.keys(query).length) return
  190. await saveInterviewInvite(query)
  191. Snackbar.success(t('common.operationSuccessful'))
  192. handleEditClose()
  193. emit('refresh')
  194. }
  195. // 取消面试
  196. const handleCancelClose = () => {
  197. cancelInvite.value = false
  198. cancelQuery.value = {
  199. id: null,
  200. reason: null
  201. }
  202. }
  203. const handleCancelSubmit = async () => {
  204. if (!cancelQuery.value.reason) return Snackbar.warning('请填写取消原因')
  205. await cancelInterviewInvite(cancelQuery.value)
  206. Snackbar.success(t('common.operationSuccessful'))
  207. handleCancelClose()
  208. emit('refresh')
  209. }
  210. // 爽约、反馈
  211. const handleClose = () => {
  212. show.value = false
  213. query.value = {}
  214. }
  215. const handleSubmit = async () => {
  216. const key = currentAction.value === 'feedback' ? 'evaluate' : 'reason'
  217. if (!query.value[key]) return Snackbar.warning('请填写您的' + (currentAction.value === 'feedback' ? '反馈' : '爽约原因'))
  218. const api = currentAction.value === 'feedback' ? feedbackInterviewInvite : noAttendInterviewInvite
  219. await api(query.value)
  220. Snackbar.success(t('common.operationSuccessful'))
  221. emit('refresh')
  222. handleClose()
  223. }
  224. </script>
  225. <style scoped lang="scss">
  226. .listItem {
  227. cursor: pointer;
  228. width: 100%;
  229. min-width: 600px;
  230. overflow: auto;
  231. height: 76px;
  232. border: 1px solid #e5e6eb;
  233. border-radius: 5px;
  234. &:hover {
  235. background-color: var(--color-f8);
  236. }
  237. .right-item {
  238. width: 100%;
  239. div {
  240. width: 25%;
  241. }
  242. }
  243. }
  244. </style>