item.vue 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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>{{ 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 === '1'" 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 = ['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. { title: '修改面试', value: 'edit' }
  99. ])
  100. const colorData = {
  101. '0': 'orange',
  102. '1': 'green',
  103. '2': 'green',
  104. '3': 'var(--v-primary-base)',
  105. '4': 'var(--color-999)',
  106. '5': 'var(--v-error-base)',
  107. '98': 'var(--v-error-base)',
  108. '99': 'var(--color-999)'
  109. }
  110. // 邀请
  111. const itemData = ref({})
  112. const showInvite = ref(false)
  113. const inviteRef = ref()
  114. // 取消
  115. const cancelInvite = ref(false)
  116. const cancelQuery = ref({
  117. id: null,
  118. reason: null
  119. })
  120. const textItems = ref({
  121. label: '取消原因 *',
  122. clearable: true
  123. })
  124. // 爽约、反馈
  125. const currentAction = ref('feedback')
  126. const show = ref(false)
  127. const query = ref({})
  128. const textItems2 = ref({
  129. label: '反馈 *',
  130. clearable: true
  131. })
  132. const obj = {
  133. '0': [1],
  134. '1': [4, 1, 3],
  135. '2': [3]
  136. }
  137. const actionItems = (status) => {
  138. const type = obj[status]
  139. if (!type || !type.length) return []
  140. const data = type.map(e => actions.value[e])
  141. return data
  142. }
  143. // 完成面试
  144. const handleFinish = (item) => {
  145. if (!item.id) return
  146. Confirm(t('common.confirmTitle'), '是否确认已完成面试?').then(async () => {
  147. await completedInterviewInvite(item.id)
  148. Snackbar.success(t('common.operationSuccessful'))
  149. emit('refresh')
  150. })
  151. }
  152. // 操作按钮
  153. const handleActionClick = (value, item) => {
  154. // 修改、重新邀约
  155. if (value === 'edit') {
  156. itemData.value = item
  157. showInvite.value = true
  158. }
  159. // 取消
  160. if (value === 'cancel') {
  161. cancelQuery.value.id = item.id
  162. cancelInvite.value = true
  163. }
  164. // 完成
  165. if (value === 'completed') handleFinish(item)
  166. // 爽约、反馈
  167. if (value === 'feedback' || value === 'attended') {
  168. currentAction.value = value
  169. textItems2.value.label = value === 'feedback' ? '反馈 *' : '爽约原因 *'
  170. query.value = value === 'feedback' ? { id: item.id, evaluate: null } : { id: item.id, reason: null }
  171. show.value = true
  172. }
  173. }
  174. // 修改面试、重新邀约
  175. const handleEditClose = () => {
  176. itemData.value = {}
  177. showInvite.value = false
  178. }
  179. const handleEditSubmit = async () => {
  180. const query = inviteRef.value.getQuery()
  181. if (!Object.keys(query).length) return
  182. await saveInterviewInvite(query)
  183. Snackbar.success(t('common.operationSuccessful'))
  184. handleEditClose()
  185. emit('refresh')
  186. }
  187. // 取消面试
  188. const handleCancelClose = () => {
  189. cancelInvite.value = false
  190. cancelQuery.value = {
  191. id: null,
  192. reason: null
  193. }
  194. }
  195. const handleCancelSubmit = async () => {
  196. if (!cancelQuery.value.reason) return Snackbar.warning('请填写取消原因')
  197. await cancelInterviewInvite(cancelQuery.value)
  198. Snackbar.success(t('common.operationSuccessful'))
  199. handleCancelClose()
  200. emit('refresh')
  201. }
  202. // 爽约、反馈
  203. const handleClose = () => {
  204. show.value = false
  205. query.value = {}
  206. }
  207. const handleSubmit = async () => {
  208. const key = currentAction.value === 'feedback' ? 'evaluate' : 'reason'
  209. if (!query.value[key]) return Snackbar.warning('请填写您的' + (currentAction.value === 'feedback' ? '反馈' : '爽约原因'))
  210. const api = currentAction.value === 'feedback' ? feedbackInterviewInvite : noAttendInterviewInvite
  211. await api(query.value)
  212. Snackbar.success(t('common.operationSuccessful'))
  213. emit('refresh')
  214. handleClose()
  215. }
  216. </script>
  217. <style scoped lang="scss">
  218. .listItem {
  219. cursor: pointer;
  220. width: 100%;
  221. min-width: 600px;
  222. overflow: auto;
  223. height: 76px;
  224. border: 1px solid #e5e6eb;
  225. border-radius: 5px;
  226. &:hover {
  227. background-color: var(--color-f8);
  228. }
  229. .right-item {
  230. width: 100%;
  231. div {
  232. width: 25%;
  233. }
  234. }
  235. }
  236. </style>