item.vue 9.6 KB

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