item.vue 8.4 KB

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